Saturday, March 16, 2024

Графічний процесор - Стиснення буферів кольору (DCC)

На цій сторінці, я очікую, що ви вже знаєте основні поняття про графічний процесор та його внутрішню роботу. Якщо ви цього не знаєте, це відео роз'яснить цю тему українською мовою :) https://www.youtube.com/watch?v=2huFN60RIJQ&t=697s

У графічних процесорах, ми постійно використовуємо буфери кольорів або рендер-цілі/render targets. Це буфер, де ми зберігаємо наші кадри, які ігри потім використовують у післяобробку/post-processing та різних інших рендер-ефектів. Однак ці буфери займають дуже багато байтів, а читання даних з пам'яті це дуже повільна операція. 

Завантаження/зберігання дані до рендер-цілей використовує багато пропускної здатності/bandwidth процесора, тому графічні процесори шукають способи прискорити ці операції. Один зі способів вирішення цієї проблеми - просто побудувати швидший процесор, але це не завжди можливо. Тому інженери графічних процесорів вирішили стискувати рендер-цілі, щоб можна було завантажувати/зберігати цю саму інформацію з меншим обсягом байтів, знижуючи використану пропускну здатність для цих операцій. Це все відбувається за вашою спиною (рендерингові APIs не дають функції для створення стиснених буферів), тому легко ніколи не знати, що ця оптимізація відбувається, але варто її розуміти, щоб знати, як її використовувати в всіх можливих випадках.

Тому у цьому пості я розгляну, як графічний процесор стискує буфери кольору та як це допомагає прискорити графічні аплікації. Також розгляну деякі аспекти, на які варто звернути увагу, щоб максимально використовувати переваги цієї оптимізації.

Делта стиснення кольору

Графічні процесори вже вміють зберігати текстури у стисненому форматі. Ці формати називаються BCn формати, але вони відрізняються від тих, що використовуються для буферів кольору. Основна відмінність полягає в тому, що BCn формати стискають кольори з втратами (lossy compression), тоді як метод стиснення, який використовує процесор для рендер-цілей, стискає без втрат (lossless compression).

Причина того, що рендер-ціль має бути стиснутою без втрат, полягає втому, що користувачі DX11/OpenGL/DX12/Vulkan навіть не знають, що таке стиснення відбувається. Графічний процесор робить це все за спиною користувача, тому результат повинен бути ідентичним до результату без стиснення. 

Метод, який використовують графічні процесори для стиснення рендер-цілей, називається дельта стиснення кольору/delta color compression (DCC). Його суть полягає втому, що колір у кожному пікселі наближений до кольору його сусідів у натуральному зображенні. Оскільки більшість кадрів, які ми рендеримо в іграх, мають природні характеристики, такі рендер-цілі можна ефективно стиснути (іх дані не є випадковими/random).

Щоб стиснути дані, графічний процесор розділяє рендер-ціль на блоки пікселів (можна уявити, що це блоки 8х8 пікселів), і зберігає колір лише першого пікселя в кожному блоці. Потім, він обчислює різницю між коліром кожного іншого пікселя у блоці та першим пікселем і зберігає цю різницю замість звичайного кольору. Як наслідок, у блоці отримуємо різні значення, які переважно є невеликими у натуральних зображеннях (оскільки різниця між сусідними пікселями зазвичай невелика). Тому процесор може зберегти ці значення, застосовуючи менше бітів. Наприклад, якщо у нас є рендер-ціль у форматі RGBA16, процесор може зберегти не 64 бітів для кожного кольору, а скажімо, 24, 32, 42 бітів або іншу кількість, не впливаючи на кінцевий колір зображення :)

Малі значення використовують менше біти

У цьому процесрі, графічний процесор вирішив викостировувати різні кілкості бітів для кожного пікселя, і він мусить пам'ятати, яку кількість він вжив, щоб знати, як пізніше розшифрувати дані з блоку пікселів. Тому кожен блок пікселів також зберігає метадані, які вказують, у якому форматі збережені кольори у цьому блоку. 

Це візуалізація того, як процесор може зберігати ці блоки у пам'яті. Як саме процесор вирішує організувати це все, невідомо
Це візуалізація того, як процесор може зберігати ці блоки у пам'яті. Як саме процесор вирішує організувати це все, невідомо

А що відбувається, коли дані у буфері кольору не можна стиснути, оскільки вони за випадкові? У такому випадку процесор зберігає інформацію у метаданих, що цей блок пікселів не стиснений, і просто зберігає нестиснені кольори для цього блоку. Це рідко трапляється, але таким чином завжди можна зберегти який небудь блок пікселів у DCC форматі. 

Коли графічний процесор творить рендер-ціль, він не знає, котрі блоки пікселів будуть стиснені і наскільки, а котрі ні, тому він завжди виділяє пам'ять для найгіршого варіанту. У найгіршому випадку жоден блок пікселів стиснений, тому просто зберігаються всі нестиснені пікселі. Проте DCC формат все одно застосовує метадані, тому їх також потрібно врахувати при виділення ресурсу. Таким чином, DCC збільшує розмір ресурса у пам'яті, навіть коли він стискує колір. Проте, варто пам'ятати, що його основна мета полягає не у зменшенні розмір ресурсу, а у покращенні продуктивності операцій з пам'яттю.

Щодо того, що саме міститься у мета-дані та в яких конкретних форматах можуть бути закодовані кольори у блоках - залишається невідомим. Кожен графічний процесор має свої власні формати, розміри блоків пікселів та алгоритми для виконання стиснення кольорів, і компанїї, такі як AMD та NVIDIA, не публікують цю інформацію. Якщо ви працюєте з консолями/consoles, можливо, ви маєте доступ до документації щодо їхніх DCC форматів, але через те, що ці формати постійно змінюються з кожними новими графічними процесорами, це насправді не важливо нам знати, як докладно вони стискають дані. Головне - це розуміти, що ця операція прискорює наші програми :)

DCC і графічний конвеєр

Тепер, як це стиснення інтергується у графічний конвеєр? У яких процесорних блоках процесор стискує та розшифрувовує дані від стисненних рендер-цілей? Я поясню, як це все виконують AMD процесори. На NVIDIA повинно бути подібно, але вони надають набагато менше документації на цю тему.

Коли ми викликуємо Device->CreateResource у DX12 або vkCreateImage у Vulkan для створення рендер-цілі, спочатку вона не містить жодної корисної інформації. Якщо вона покриває попередньо вживану частину пам'яті, то може навіть зберігати ці попередні дані, які переважно відображатимуться як випадкові кольори у пікселях (сміття :P). Щоб правильно ініцювати цю рендер-ціль, маємо дві опції:

1) Викликати функцію Discard у DX12 або vkCmdBeginRenderPass з VkRenderPass з loadOp = LOAD_OP_DONT_CARE та initialLayout = VK_IMAGE_LAYOUT_UNDEFINED у Vulkan. Ця операція ініціює метадані для кожного блоку пікселів, хоча вона не змінює дані у цих пікселях, тому там може залишитися сміттєві значення. Цю функцію використовується, якщо ви знаєте що подальший прохід рендерингу/render pass перезапише кожен піксель у цьому буфері або перезапише колір лише у тих пікселях, які ваша аплікація буде читати (операції перезаписування кольорів передбачають, що метадані рендер-цілей ініціалізовані).

2) Викликати функцію ClearRenderTargets у DX12 або vkCmdBeginRenderPass з VkRenderPass з loadOp = LOAD_OP_CLEAR у Vulkan. Ця операція ініцює метадані як і Discard, а також перезаписує кожен піксель якимось кольором.

Якщо ви не ініціюєте ваші рендер-цілі, очікуйте отаких результатів :)

Файна дискотека XD

Растеризація та обчислюванльні шейдери передбачають, що їхні рендер-цілі ініцйовані, тому звертайте увагу на цю помилку :P. 

Після того, як ми ініцюємо рендер-ціль, ми можемо малювати на неї трикутники або записувати з обчислювального шейдера/compute shader. Давайте розглянемо, як стиснена рендер-ціль впливає на ці операції:

Малювання: 

Під час малювання на рендер-ціль, графічний процесор отримує нові кольори від піксельного шейдера, які ми хочемо зберегти у буфері кольору. Піксельний шейдер висилає ці кольори до рендернгових бекендів/render backends (ROPS) що є блоки процесора, відповідальні за запис даних до буферів кольору. Ці бекенди отримують нові кольори, визначають, які блоки пікселів вони покривають, стискають ці дані та оновлюють метадані для кожного з цих блоків.

Обчислювальний шейдер: 

Коли дані зберігаються у цьому шейдері, ми обминаємо рендернгові бекенди, оскільки вони застосовані лише під час растеризації. Таким чином, на старших процесорах, наприклад RX 580, неможливо зберігати дані з таких шейдерів у стиснені буфери. Якщо аплікація передає позначку ALLOW_UNORDERED_ACCESS (цей позначок вказує, що обчислювальний шейдер буде писати до цього ресурсу), то драйвер для цих старших процесорів створює рендер-ціль без стиснення, і ви не отримуєте прискорення від стисненного буферу.  

У теперішних процесорах (RDNA1 або новший), це вже не така велика проблема, і обчислювальний шейдер може писати до стиснених рендер-цілей. Але де ж тоді відбувається розшифрування даних, оскільки ця операція обминає рендерингові бекенди? 

Сучасні RDNA графічні процесори мають певний рівень кешу, через який взаємодіє кожна частина процесора. Таким чином, коли ми завантажуємо або зберігаємо дані, всі вони проходять через цей рівень кешу. У цьому кеші розташований блок розшифрування DCC, який стискує кольори та оновлює метадані, так само, як це відбувається у рендернгових бекендах. 

Завантаження рендер-цілей: 

А що відбувається, коли ми читаємо зі стисненного буфера, наприклад, у піксельному або обчислюваному шейдері? Як бачимо у вищому образі, L2 кеш містить блок стиснення та розшифрування DCC, який також розшифровує DCC дані, перед тим, як шейдер їх отримує. У такий спосіб, блоки піскелів залишаються стисненими у L2 кеші, а у нижчих кешах, вони вже розшифорвані, і шейдер може читати їх як звичайні пікселі.

Попередньо я пояснював, що цей блок стиснення та розшифрування знаходиться лише в новших процесорах, але у старших він також присутній, проте виконує виключно розшифрування даних, а не їх стискання.

Тепер на NVIDIA та майбутніх процесорах не відомо, де саме знаходяться ці блоки DCC, але знову ж таки, це не дуже важливо для програмістів. Я надаю вам цю інформацію, щоб ви мали уявлення, як це функціонує на справжніх процесорах, і на AMD процесорах, це справді так було зорганізовано.

Практичні переваги DCC

Таким чином, ми розуміємо, що таке DCC, приблизно, як він працює, і як графічний процесор виконує його алгоритм. Тепер давайте з'ясуємо, наскільки це корисно? Які переваги ми отримуємо від цієї внутрішної оптимізації?

У проходах рендерингу/render passes, де малюємо геометрію (наприклад GBuffer, освітлення) або де обчислюємо післяобробку/post-processing, стиснення рендер-ціль може прискорити їх на 5-10%. Більш того, якщо основним обмеженням продуктивності у проходах рендерингу є пропускна здатність пам'яті, то це стиснення може і навіть більше допомогти. 

Також, стиснені формати набагато скоріше очищують рендер-ціль порівнянно з не стисненими буферами. Оскільки стиснена рендер-ціль вже містить метадані для кожного блоку пікселів, вона також зберігає деякі біти, що вказують, чи цей блок пікселів очищенний до певного значення (зазвичай 0 або 1). Таким чином, замість запису значення до кожного пікселя під час очищення стисненої рендер-цілі, драйвер просто змінює декілька бітів у метаданих, що відбувається набагато швидше. Тоді, блоки розшифрування автоматично перетворять цю метадану на цей колір, хоч ми його не зберігаємо у кожному пікселі у блоку.

Тут приведемо приклад очищення рендер-ціль розміром 3840x2160 та форматом R8G8B8A8_UNORM на процесорі RX 580:

Тут очищуємо без стиснення, ця операція забирає 219 мікросекунд для виконання

Тут очищуємо стиснений буфер. Операція лише змінює метадані, і через це виконується за 8 мікросекунд (це понад 27 разів швидше!)

Як знати коли рендер ціль стисненний?

Це переважно міняється між графічними процесорами. Старі процесори більш обмеженні як нові, але є програми, які можуть вам сказати, які з ваших рендер-цілей стиснені на конкретному процесорі.

На графічних процесорах AMD слід захоплити кадр/frame capture за допомогою Radeon Developer Tools Suite (RDTS). Відкрийте Radeon Developer Panel (RDP), встановіть його на режим Profiling/Профілювання, та захопіть кадр. Потім, відкрийте захоплену кадр у Radeon GPU Profiler (RGP), де будуть дані про обчислення одної кадри. У Overview -> Render/depth targets, побачите всі рендер-цілі, що використовуються у цьому кадрі, а також інформацію про те, які буфери стисненні, а які - ні. Хоча у цьому блозі я говорив лише про стиснення буферів кольору, у цій аплікації побачите, що навіть буфери глибини можуть бути стиснуті, але це вже інша тема.

На NVIDIA процесорах, я не впевнений, як можна отримати цю інформацію. Можливо, NSIGHT Graphics десь ховає цю інформацію, але я не моху знайти в Інтернеті, як перевірити, чи рендер-ціль стиснена. Їхні процесори також стискають буфери кольору, але вони, переважно, не надають багато інформації про те, як це все працює.

Деталі над якими слід важати

Є різні деталі, які враховуються, щоб визначити, чи буде рендер-ціль стисненою:

1) На старших процесорах (перед RDNA1-2), якщо рендер-ціль створена з позначкою ALLOW_UNORDERED_ACCESS, то стиснення буде вимкнено. 

2) Якщо формат буфера кольору є TYPELESS/без типу (наприклад R16G16B16A16_TYPELESS), то стиснення буде вимкнено, оскільки процесор не може визначити, чи дані у пікселі представлені числом з рухомою комою, цілим числом, чи іншим типом. Можливо, новші або майбутні процесори не матимуть цієї проблеми, але як я раніше казав, це відрізняється між процесорами :).

3) Існують й інші евристики, які драйвер застосовує, щоб вирішити, чи ресурс повинен бути стисненим, оскільки стиснення не завжди прискорює малювання, а причини цього відрізняються між процесорами.

До того ж, є деякі деталі, яких варто враховувати, коли рендер-ціль стиснена:

1) При очищенні рендер-цілі, очищуйте до значення 0 або 1, щоб ця операція проводилася швидше (fast clear/швидке очищування). Метадані не мають можливості зберігати різноманітні кольори очищення, які користувач може бажати застосовувати, тому вони переважно обмежені значеннями 0 або 1.

2) Коли зберігаєте кольори від обчислювального шейдера до стисненого рендер-цілі, слід перемінювати всі пікселі у DCC блоку. Якщо шейдер пише до різних випадкових пікселів, то це зберігання може навіть повільніше здійснюватись коли буфер стисненний, оскільки в не стисненному буфері, ми просто переписуємо колір пікселя, а у стисненному, процесор оновлює мета-дану, оновлює стисненний формат блоку тощо. Розмір блоку переважно не відомий, але процесори вибирають цей розмір так, щоб група потіків виконання з розміром 8х8 переписувала весь блок. Тому якщо кожен потік виконання пише до одного пікселя у цьому блоці, то ви ефективно використовуватиме блок розшифрування DCC.

3) Подібно до вищого пункту, якщо стисненний рендер ціль зберігає наприклад 4 колірні канали, краще їх всіх перемінювати коли зберігаєте дані у пікселі, навіть якщо записуєте це саме значення що вже там було. Отже коли ви хочете переписати перші 2 канали пікселя, часом краще переписати всі 4, і просто записати ці самі значення до останні два канали, що вже у пам'яті є. 

Кінцеві слова

Сподіваюсь, що ця тема є цікавою і корисною. Пам'ятайте, що все це може змінитися з кожним новим графічним процесором. AMD та NVIDIA постійно шукають способи прискорити їхні процесори, і один зі спосібів це зробити - це покращення інтеграції та форматів DCC. 

Джерела

https://gpuopen.com/learn/dcc-overview/ (тут також пояснюється, у яких випадках бар'єр зможе розшифувати весь буфер кольору, оскільки якась майбутня операція може не мати змоги читати стиснуті блоки)

https://gpuopen.com/learn/rdna-performance-guide/ (шукайте за compression тутай)

https://gpuopen.com/presentations/2019/Surfing_the_Wavefronts.pdf - починаючи з сторінки 53

https://www.amd.com/system/files/documents/polaris-whitepaper.pdf - сторінка 10 показує на скільки може приспішити DCC пропускну здатність

https://www.anandtech.com/show/10325/the-nvidia-geforce-gtx-1080-and-1070-founders-edition-review/8 

https://www.es.ele.tue.nl/~heco/courses/ECA/GPU-papers/GeForce_GTX_1080_Whitepaper_FINAL.pdf - сторінка 12

https://gpuopen.com/wp-content/uploads/2019/08/RDNA_Architecture_public.pdf - сторінка 30

Tuesday, January 30, 2024

Обчислення дотичного та бідотичного векторів

Було питання на твітері щодо того, як обчислити дотичний/tangent та бідотичний/bitangent вектори, знаючи тільки положення, \(UV\), та нормалю кожної вершини трикутника. У цьому пості я виведу формулу для вирішення цього задання. Ці вектори часто використовуються для відображення нормалей/normal mapping.

По-перше, що таке дотичний/бідотичний вектори? Це вектори, які разом із нормаллю утворюють систему координат. Нормаль вектор виходить від поверхні об'єкта, а дотичний/бідотичний вектори рухаються по поверхні об'єкта.

Дотичний/бідотичний вектори можуть бути будь-які вектори, які перпендикулярні та направлені вздовж поверхні об'єкта. Проте в графіці ми вже зберігаємо \(UV\) координати для кожної вершини, а \(UV\) координати утворюють двовимірну систему координат, яка орієнтована вздовж поверхні об'єкта. Тому ми зазвичай визначаємо дотичний вектор так, щоб його напрямок був таким самим як \(U\), а бідотичний вектор як \(V\). 

Отже трикутник фактично існує у двох просторах одночасно: у просторі світу (коли використовуємо положення кожної вершини), та у дотичному просторі (коли використовуємо \(UV\) координати кожної вершини, це той самий простір, що і \(UV\) space):

Ми знаємо, що напрямки векторів \(U\) та \(V\) такі самі, як напрямки дотичного/бідотичного векторів, але ми хочемо знайти ці напрямки у просторі світу (у дотичному просторі, вони просто дорівнюють \(U = (1, 0), V = (0, 1)\)). Наприклад, положення вершини у просторі світу є перетвореним положенням вершини у просторі \(UV\) (і це саме хочемо знайти для цих двох векторів). Тому зараз, виведемо формулу яка обчислить вектор \(U\) та \(V\), у дотичному просторі, і перетворимо цю формулу щоб результат був у просторі світу.

Дані змінні: 

\(UV_{0}, UV_{1}, UV_{2} \) - це положення кожної вершини у дотичному просторі

\(P_{0}, P_{1}, P_{2}\) - це положення кожної вершини у світовому просторі

\(T\) - це дотичний вектор у дотичному просторі

\(B\) - це бідотичний вектор у бідотичному просторі

Те, що шукаємо:

\(T'\) - це дотичний вектор у світовому просторі

\(B'\) - це бідотичний вектор у світовому просторі

Відтепер, якщо змінна має апостроф (наприклад \(T'\)), то це означає, що ця змінна знаходиться у світовому просторі. А якщо апострофа немає, то змінна у дотичному просторі. Тепер давайте зосередимося на трикутника у дотичному просторі:

Якщо обчислимо дельту UV координат між вершинами \(UV_0\) та \(UV_1\), то отримаємо \(\triangle U_1\) та \(\triangle V_1\). Ми бачимо, що \(triangle U_1\) має цей сам напрямок як \(T\), а \(\triangle V_1\) має той сам напрямок, що й \(B\). Можемо це формалізувати у цьому рівнянні:

\[ E_1 = UV_1 - UV_0 = \triangle U_1 \cdot T + \triangle V_1 \cdot B \]

де \(\triangle U_1\) і \(\triangle V_1\) це скаляри.

Уявімо, що ми не знаємо до чого \(T\) та \(B\) дорівнюють у цьому просторі. Тоді, у нас рівняння з двома невідомими змінними, тому нам потрібно отримати ще одне рівняння, щоб обчислити ці дві змінні. Ми можемо обчислити нове рівняння застосовуючи цей самий метод, що і попередньо, але тепер між \(UV_2\) та \(UV_1\):

\[ E_2 = UV_2 - UV_1 = \triangle U_2 \cdot T + \triangle V_2 \cdot B \]

Тепер у нас два рівняння та дві змінні, які ми хочемо обчислити:

\[ E_1 = \triangle U_1 \cdot T + \triangle V_1 \cdot B \]

\[ E_2 = \triangle U_2 \cdot T + \triangle V_2 \cdot B \]

Є багато способів вирішити цих двох змінних (метод підстановки, метод усунення), але я виведу формули для цих змінних застосовючи метод матриць. Спочатку перемінимо обидва верхні рівняння в одне матричне рівняння:

\begin{align*} \begin{bmatrix} E_{1x} & E_{1y} & E_{1z} \\ E_{2x} & E_{2y} & E_{2z} \\ \end{bmatrix} = \begin{bmatrix} \triangle U_1 & \triangle V_1 \\ \triangle U_2 & \triangle V_2 \\ \end{bmatrix} \begin{bmatrix} T_{x} & T_{y} & T_{z} \\ B_{x} & B_{y} & B_{z} \\ \end{bmatrix} \end{align*}

Ми хочемо отримати формулу, де права матриця залишається самою з одного боку рівняння, а всі інші матриці знаходяться з другого боку. Щоб так змінити цю формулу, обчислимо обернену матрицю наших дельт, і помножимо обидва боки рівняння на неї:

\begin{align*} \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1}\begin{bmatrix} \triangle V_2 & -\triangle V_1 \\ -\triangle U_2 & \triangle U_1 \\ \end{bmatrix} \begin{bmatrix} E_{1x} & E_{1y} & E_{1z} \\ E_{2x} & E_{2y} & E_{2z} \\ \end{bmatrix} = \begin{bmatrix} T_{x} & T_{y} & T_{z} \\ B_{x} & B_{y} & B_{z} \\ \end{bmatrix} \end{align*}

\begin{align*} \Rightarrow \begin{bmatrix} T_{x} & T_{y} & T_{z} \\ B_{x} & B_{y} & B_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1}\begin{bmatrix} \triangle V_2 & -\triangle V_1 \\ -\triangle U_2 & \triangle U_1 \\ \end{bmatrix} \begin{bmatrix} E_{1x} & E_{1y} & E_{1z} \\ E_{2x} & E_{2y} & E_{2z} \\ \end{bmatrix} \end{align*}

І ось так, ми можемо обчислити дотичний/бідотичний вектори! Але ця формула обчислюється у дотичному просторі, а ми хочемо обчислити ці вектори у світовому просторі. Якщо ми перетворимо обидва боки цього рівняння до простору світу, то побачимо, що нам слід перемінити всі вектори до світового простору (отже \(E_1\), \(E_2\) перетворюються до \(E'_1\), \(E'_2\), а всі скаляри залишаються такими самими). Таким чином, ми отримаємо нашу кінцеву формулу. Якщо вас цікавить, як перетворити обидва боки цього рівняння до простору світу, то решта того посту покаже вам, як це зробити, та як вивести остаточні формули. А якщо ні, кінцева формула знаходиться на кінець цієї сторінки.

Ми знаємо що перетворення між дотичним простором та світовим простором є лінійним перетворенням (множення на матрицю). А лінійне перетворення задовольняє ці аксіоми:

\begin{align*} &\text{(1) Адитивність:} \quad L(\mathbf{u} + \mathbf{v}) = L(\mathbf{u}) + L(\mathbf{v}) \\ &\text{(2) Гомогенність:} \quad L(k\mathbf{u}) = kL(\mathbf{u}) \quad \text{для всіх скалярів } k \\ \end{align*}

де \(L\) це лінійне перетворення, а \(\mathbf{u}\), \(\mathbf{v}\) це вектори.

Таким чином, скажімо що функція \(L\) це лінійне перетворення від дотичного простору до простору світу. Використовимо її для перетворення наших рівнянь до простору світу. Тільки зауважте, у нас є матриці, а наші аксіоми визначаються тільки для скалярів або векторів. Тому спочатку я розмножу матриці з правого боку рівняння, а після напишу векторну форму нашого рівняння для \(T\) та \(B\):

\begin{align*} \begin{bmatrix} \triangle V_2 & -\triangle V_1 \\ -\triangle U_2 & \triangle U_1 \\ \end{bmatrix} \begin{bmatrix} E_{1x} & E_{1y} & E_{1z} \\ E_{2x} & E_{2y} & E_{2z} \\ \end{bmatrix}\end{align*}

\begin{align*} = \begin{bmatrix} (\triangle V_2 \cdot E_{1x} - \triangle V_1 \cdot E_{2x}) & (\triangle V_2 \cdot E_{1y} - \triangle V_1 \cdot E_{2y}) & (\triangle V_2 \cdot E_{1z} - \triangle V_1 \cdot E_{2z}) \\ (-\triangle U_2 \cdot E_{1x} + \triangle U_1 \cdot E_{2x}) & (-\triangle U_2 \cdot E_{1y} + \triangle U_1 \cdot E_{2y}) & (-\triangle U_2 \cdot E_{1z} + \triangle U_1 \cdot E_{2z}) \\ \end{bmatrix} \end{align*}

Тепер, перемінемо наше матричне рівняння до векторні форми:

\begin{align*} \begin{bmatrix} T_{x} \\ T_{y} \\ T_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \begin{bmatrix} (\triangle V_2 \cdot E_{1x} - \triangle V_1 \cdot E_{2x}) \\ (\triangle V_2 \cdot E_{1y} - \triangle V_1 \cdot E_{2y}) \\ (\triangle V_2 \cdot E_{1z} - \triangle V_1 \cdot E_{2z}) \\ \end{bmatrix} \end{align*}

\begin{align*} \begin{bmatrix} B_{x} \\ B_{y} \\ B_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \begin{bmatrix} (-\triangle U_2 \cdot E_{1x} + \triangle U_1 \cdot E_{2x}) \\ (-\triangle U_2 \cdot E_{1y} + \triangle U_1 \cdot E_{2y}) \\ (-\triangle U_2 \cdot E_{1z} + \triangle U_1 \cdot E_{2z}) \\ \end{bmatrix} \end{align*}

Я змінив вектори на стовпчиковi вектори, щоб все помістити у блог-пост :P. Ці дві формули разом створюють матричну формулу, але у такій формі можна застосовувати аксіоми лінійного перетворення. Почнемо перетворювати рівняння для \(T\) до простору світу:

\begin{align*} \Rightarrow L\left(\begin{bmatrix} T_{x} \\ T_{y} \\ T_{z} \\ \end{bmatrix}\right) = L\left(\frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \begin{bmatrix} (\triangle V_2 \cdot E_{1x} - \triangle V_1 \cdot E_{2x}) \\ (\triangle V_2 \cdot E_{1y} - \triangle V_1 \cdot E_{2y}) \\ (\triangle V_2 \cdot E_{1z} - \triangle V_1 \cdot E_{2z}) \\ \end{bmatrix}\right) \end{align*}

\begin{align*} \Rightarrow \begin{bmatrix} T'_{x} \\ T'_{y} \\ T'_{z} \\ \end{bmatrix} = L\left(\frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \cdot \left(\triangle V_2 \cdot \begin{bmatrix} E_{1x} \\ E_{1y} \\ E_{1z} \\ \end{bmatrix} - \triangle V_1 \cdot \begin{bmatrix} E_{2x} \\ E_{2y} \\ E_{2z} \\ \end{bmatrix}\right)\right) \end{align*}

через те, що \(T'\) - це \(T\) але у просторі світу.

\begin{align*} \Rightarrow \begin{bmatrix} T'_{x} \\ T'_{y} \\ T'_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \cdot L\left(\triangle V_2 \cdot \begin{bmatrix} E_{1x} \\ E_{1y} \\ E_{1z} \\ \end{bmatrix} - \triangle V_1 \cdot \begin{bmatrix} E_{2x} \\ E_{2y} \\ E_{2z} \\ \end{bmatrix}\right) \end{align*}

застосовуючи гомогенність (2) (перший термін це скаляр).

\begin{align*} \Rightarrow \begin{bmatrix} T'_{x} \\ T'_{y} \\ T'_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \cdot \left(L\left(\triangle V_2 \cdot \begin{bmatrix} E_{1x} \\ E_{1y} \\ E_{1z} \\ \end{bmatrix}\right) - L\left(\triangle V_1 \cdot \begin{bmatrix} E_{2x} \\ E_{2y} \\ E_{2z} \\ \end{bmatrix}\right)\right) \end{align*}

застосовуючи адитивність (1).

\begin{align*} \Rightarrow \begin{bmatrix} T'_{x} \\ T'_{y} \\ T'_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \cdot \left(\triangle V_2 \cdot L\left(\begin{bmatrix} E_{1x} \\ E_{1y} \\ E_{1z} \\ \end{bmatrix}\right) - \triangle V_1 \cdot L\left(\begin{bmatrix} E_{2x} \\ E_{2y} \\ E_{2z} \\ \end{bmatrix}\right)\right) \end{align*}

застосовуючи гомогенність (2) (\(\triangle V_1\) та \(\triangle V_2\) - це скаляри).

Тепер, як ми можемо обчислити перетворення змінних \(E_1\) та \(E_2\)? Ну, ми маємо формули для цих змінних, які просто віднімають точки у дотичному просторі, отже ми також момежо відобразити їх з перетворенням \(L\):

\[ L\left(E_1\right) = L\left(UV_{1} - UV_{0}\right)\]

\[ \Rightarrow E'_1 = L\left(UV_{1}\right) - L\left(UV_{0}\right)\]

застосовуючи адитивність (2)

\[ \Rightarrow E'_1 = P_{1} - P_{0}\]

тому що \(P_0\) та \(P_1\) - це ті самі точки, але у просторі світу, до якого \(L\) відображає. Подібну формулу можемо отримати для \(E_2\):

\[ E'_2 = P_{2} - P_{1}\]

Це перетворює нашу формулу для \(Т'\) у наступне:

\begin{align*} \Rightarrow \begin{bmatrix} T'_{x} \\ T'_{y} \\ T'_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \cdot \left(\triangle V_2 \cdot \begin{bmatrix} E'_{1x} \\ E'_{1y} \\ E'_{1z} \\ \end{bmatrix} + \triangle V_1 \cdot \begin{bmatrix} E'_{2x} \\ E'_{2y} \\ E'_{2z} \\ \end{bmatrix}\right) \end{align*}

І, як бачите, з правої сторони рівняння, ми використовуємо положення вершин у просторі світу (щоб обчислити \(E'_1\), \(E'_2\)) та \(UV\) координати, щоб обчислити змінні дельти, які є величинами, які нам відомі! І ця формула обчислює дотичний вектор у просторі світу, що є саме тим, що ми шукаємо! Подібну формулу можемо отримати для бідотичного вектора. Отже, ось остаточні формули:

\begin{align*} \begin{bmatrix} T'_{x} \\ T'_{y} \\ T'_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \cdot \left(\triangle V_2 \cdot \begin{bmatrix} E'_{1x} \\ E'_{1y} \\ E'_{1z} \\ \end{bmatrix} - \triangle V_1 \cdot \begin{bmatrix} E'_{2x} \\ E'_{2y} \\ E'_{2z} \\ \end{bmatrix}\right) \end{align*}

\begin{align*} \begin{bmatrix} B'_{x} \\ B'_{y} \\ B'_{z} \\ \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \cdot \left(-\triangle U_2 \cdot \begin{bmatrix} E'_{1x} \\ E'_{1y} \\ E'_{1z} \\ \end{bmatrix} + \triangle U_1 \cdot \begin{bmatrix} E'_{2x} \\ E'_{2y} \\ E'_{2z} \\ \end{bmatrix}\right) \end{align*}

де 

\[ E'_1 = P_{1} - P_{0}\] \[ E'_2 = P_{2} - P_{1}\] \[ \triangle U_1 = UV_{1x} - UV_{0x}\] \[ \triangle V_1 = UV_{1y} - UV_{0y}\] \[ \triangle U_2 = UV_{2x} - UV_{1x}\] \[ \triangle V_2 = UV_{2y} - UV_{1y}\]

І ось так, ми вивели формули, які обчислюють дотичний та бідотичний вектори, використовуючи дані у вершинах трикутника :)

Графічний процесор - Стиснення буферів кольору (DCC)

На цій сторінці, я очікую, що ви вже знаєте основні поняття про графічний процесор та його внутрішню роботу. Якщо ви цього не знаєте, це від...