Научете основните концепции и напредналите техники за рендиране на сенки в реално време в WebGL. Ръководството обхваща shadow mapping, PCF, CSM и решения на чести артефакти.
WebGL Shadow Mapping: Цялостно ръководство за рендиране в реално време
В света на 3D компютърната графика малко елементи допринасят повече за реализма и потапянето в средата от сенките. Те предоставят ключови визуални знаци за пространствените взаимоотношения между обектите, местоположението на светлинните източници и цялостната геометрия на сцената. Без сенки 3D световете могат да изглеждат плоски, несвързани и изкуствени. За уеб-базирани 3D приложения, задвижвани от WebGL, имплементирането на висококачествени сенки в реално време е отличителен белег за професионално изживяване. Това ръководство предоставя задълбочен поглед върху най-фундаменталната и широко използвана техника за постигане на това: Shadow Mapping.
Независимо дали сте опитен програмист на графики или уеб разработчик, навлизащ в третото измерение, тази статия ще ви предостави знанията да разбирате, прилагате и отстранявате проблеми със сенките в реално време във вашите WebGL проекти. Ще преминем от основната теория до практическите детайли по имплементацията, изследвайки често срещани капани и напредналите техники, използвани в съвременните графични енджини.
Глава 1: Основи на Shadow Mapping
В своята същност, shadow mapping е умна и елегантна техника, която определя дали дадена точка в сцената е в сянка, като задава прост въпрос: „Може ли тази точка да бъде видяна от светлинния източник?“ Ако отговорът е „не“, това означава, че нещо блокира светлината и точката трябва да е в сянка. За да отговорим на този въпрос програмно, използваме подход на рендиране в два прохода.
Какво е Shadow Mapping? Основната концепция
Цялата техника се върти около рендирането на сцената два пъти, всеки път от различна гледна точка:
- Проход 1: Проход за дълбочина (Перспективата на светлината). Първо, рендираме цялата сцена от точната позиция и ориентация на светлинния източник. В този проход обаче не се интересуваме от цветове или текстури. Единствената информация, от която се нуждаем, е дълбочината. За всеки рендиран обект записваме разстоянието му от светлинния източник. Тази колекция от стойности на дълбочина се съхранява в специална текстура, наречена карта на сенките или карта на дълбочината. Всеки пиксел в тази карта представлява разстоянието до най-близкия обект от гледната точка на светлината в определена посока.
- Проход 2: Проход за сцената (Перспективата на камерата). След това рендираме сцената, както бихме го направили обикновено, от перспективата на основната камера. Но за всеки отделен пиксел, който се чертае, извършваме допълнително изчисление. Определяме позицията на този пиксел в 3D пространството и след това питаме: „Колко далеч е тази точка от светлинния източник?“ След това сравняваме това разстояние със стойността, съхранена в нашата карта на сенките (от Проход 1) на съответното място.
Логиката е проста:
- Ако текущото разстояние на пиксела от светлината е по-голямо от разстоянието, съхранено в картата на сенките, това означава, че има друг обект, по-близо до светлината по същата линия на видимост. Следователно текущият пиксел е в сянка.
- Ако разстоянието на пиксела е по-малко или равно на разстоянието в картата на сенките, това означава, че нищо не го блокира и пикселът е напълно осветен.
Подготовка на сцената
За да имплементирате shadow mapping в WebGL, са ви необходими няколко ключови компонента:
- Светлинен източник: Това може да бъде насочена светлина (като слънцето), точкова светлина (като крушка) или прожектор. Типът светлина ще определи вида на проекционната матрица, използвана по време на прохода за дълбочина.
- Framebuffer Object (FBO): WebGL обикновено рендира в стандартния фреймбуфер на екрана. За да създадем нашата карта на сенките, се нуждаем от цел за рендиране извън екрана. FBO ни позволява да рендираме в текстура вместо на екрана. Нашият FBO ще бъде конфигуриран с прикачена текстура за дълбочина.
- Два комплекта шейдъри: Ще ви е необходима една шейдърна програма за прохода за дълбочина (много проста) и друга за финалния проход на сцената (която ще съдържа логиката за изчисляване на сенките).
- Матрици: Ще ви трябват стандартните матрици за модел, изглед и проекция за камерата. От решаващо значение е, че ще ви е необходима и матрица за изглед и проекция за светлинния източник, често комбинирани в една-единствена „матрица на светлинното пространство“.
Глава 2: Подробен преглед на двупроходния рендиращ конвейер
Нека разгледаме двата прохода на рендиране стъпка по стъпка, като се съсредоточим върху ролите на матриците и шейдърите.
Проход 1: Проход за дълбочина (От перспективата на светлината)
Целта на този проход е да попълним нашата текстура за дълбочина. Ето как работи:
- Свързване на FBO: Преди да рисувате, инструктирайте WebGL да рендира във вашия персонализиран FBO вместо в платното (canvas).
- Конфигуриране на Viewport: Задайте размерите на viewport-а да съответстват на размера на вашата текстура за карта на сенките (напр. 1024x1024 пиксела).
- Изчистване на буфера за дълбочина: Уверете се, че буферът за дълбочина на FBO е изчистен преди рендиране.
- Създаване на матриците на светлината:
- Матрица на изгледа на светлината: Тази матрица трансформира света в гледната точка на светлината. За насочена светлина, тя обикновено се създава с функция `lookAt`, където „окото“ е позицията на светлината, а „целта“ е посоката, в която сочи.
- Проекционна матрица на светлината: За насочена светлина, която има паралелни лъчи, се използва ортографска проекция. За точкови светлини или прожектори се използва перспективна проекция. Тази матрица определя обема в пространството (кутия или фрустум), който ще хвърля сенки.
- Използване на шейдърната програма за дълбочина: Това е минималистичен шейдър. Единствената задача на вертексния шейдър е да умножи позицията на върха по матриците за изглед и проекция на светлината. Фрагментният шейдър е още по-прост: той просто записва стойността на дълбочината на фрагмента (неговата z-координата) в текстурата за дълбочина. В съвременния WebGL често дори не е необходим персонализиран фрагментен шейдър, тъй като FBO може да бъде конфигуриран да улавя автоматично буфера за дълбочина.
- Рендиране на сцената: Нарисувайте всички обекти, хвърлящи сянка, във вашата сцена. FBO вече съдържа нашата завършена карта на сенките.
Проход 2: Проход за сцената (От перспективата на камерата)
Сега рендираме финалното изображение, използвайки картата на сенките, която току-що създадохме, за да определим сенките.
- Разкачане на FBO: Превключете обратно към рендиране в стандартния фреймбуфер на платното.
- Конфигуриране на Viewport: Върнете размерите на viewport-а към тези на платното.
- Изчистване на екрана: Изчистете буферите за цвят и дълбочина на платното.
- Използване на шейдърната програма за сцената: Тук се случва магията. Този шейдър е по-сложен.
- Вертексен шейдър: Този шейдър трябва да направи две неща. Първо, той изчислява крайната позиция на върха, използвайки матриците за модел, изглед и проекция на камерата, както обикновено. Второ, той трябва също така да изчисли позицията на върха от перспективата на светлината, използвайки матрицата на светлинното пространство от Проход 1. Тази втора координата се предава на фрагментния шейдър като varying променлива.
- Фрагментен шейдър: Това е ядрото на логиката за сенките. За всеки фрагмент:
- Получава интерполираната позиция в светлинното пространство от вертексния шейдър.
- Извършва перспективно деление на тази координата (разделя x, y, z на w). Това я трансформира в Нормализирани координати на устройството (NDC), вариращи от -1 до 1.
- Трансформира NDC в текстурни координати (които варират от 0 до 1), за да можем да семплираме нашата карта на сенките. Това е проста операция за мащабиране и отместване: `texCoord = ndc * 0.5 + 0.5;`.
- Използва тези текстурни координати, за да семплира текстурата на картата на сенките, създадена в Проход 1. Това ни дава `depthFromShadowMap`.
- Текущата дълбочина на фрагмента от перспективата на светлината е неговият z-компонент от трансформираната координата в светлинното пространство. Нека я наречем `currentDepth`.
- Сравняване на дълбочините: Ако `currentDepth > depthFromShadowMap`, фрагментът е в сянка. Ще трябва да добавим малко отместване (bias) към тази проверка, за да избегнем артефакт, наречен „shadow acne“, който ще обсъдим по-нататък.
- Въз основа на сравнението се определя коефициент на сянка (напр. 1.0 за осветен, 0.3 за засенчен).
- Прилага се този коефициент на сянка към крайното изчисление на цвета (напр. умножава се амбиентната и дифузната компонента на осветлението по коефициента на сянка).
- Рендиране на сцената: Нарисувайте всички обекти в сцената.
Глава 3: Често срещани проблеми и техните решения
Имплементирането на основен shadow mapping бързо ще разкрие няколко често срещани визуални артефакта. Разбирането и коригирането им е от решаващо значение за постигането на висококачествени резултати.
Shadow Acne (Артефакти от самозасенчване)
Проблемът: Може да видите странни, неправилни шарки от тъмни линии или подобни на Моаре шарки върху повърхности, които би трябвало да са напълно осветени. Това се нарича „shadow acne“. То се получава, защото стойността на дълбочината, съхранена в картата на сенките, и стойността на дълбочината, изчислена по време на прохода за сцената, са за една и съща повърхност. Поради неточности с плаваща запетая и ограничената резолюция на картата на сенките, малки грешки могат да накарат фрагмента неправилно да определи, че се намира зад себе си, което води до самозасенчване.
Решението: Отместване на дълбочината (Depth Bias). Най-простото решение е да се въведе малко отместване към `currentDepth` преди сравнението. Като правим фрагмента да изглежда малко по-близо до светлината, отколкото е в действителност, ние го „избутваме“ извън собствената му сянка.
float shadow = currentDepth > depthFromShadowMap + bias ? 0.3 : 1.0;
Намирането на правилната стойност за отместване е деликатен баланс. Ако е твърде малка, артефактът остава. Ако е твърде голяма, получавате следващия проблем.
Peter Panning
Проблемът: Този артефакт, кръстен на героя, който може да лети и е загубил сянката си, се проявява като видима празнина между обект и неговата сянка. Той кара обектите да изглеждат сякаш се носят във въздуха или са откъснати от повърхностите, върху които би трябвало да лежат. Това е пряк резултат от използването на твърде голямо отместване на дълбочината.
Решението: Отместване на дълбочината, мащабирано по наклона (Slope-Scale Depth Bias). По-стабилно решение от постоянното отместване е то да зависи от стръмността на повърхността спрямо светлината. По-стръмните полигони са по-податливи на „acne“ и изискват по-голямо отместване. По-плоските полигони се нуждаят от по-малко. Повечето графични API, включително WebGL, предоставят функционалност за автоматично прилагане на такъв тип отместване по време на прохода за дълбочина, което обикновено е за предпочитане пред ръчното отместване във фрагментния шейдър.
Перспективен алиасинг (Назъбени ръбове)
Проблемът: Ръбовете на вашите сенки изглеждат блокови, назъбени и пикселизирани. Това е форма на алиасинг. Случва се, защото резолюцията на картата на сенките е ограничена. Един пиксел (или тексел) в картата на сенките може да покрие голяма площ върху повърхност във финалната сцена, особено за повърхности близо до камерата или такива, гледани под остър ъгъл. Това несъответствие в резолюцията причинява характерния блоков вид.
Решението: Увеличаването на резолюцията на картата на сенките (напр. от 1024x1024 на 4096x4096) може да помогне, но това идва със значителни разходи за памет и производителност и не решава напълно основния проблем. Истинските решения се крият в по-напреднали техники.
Глава 4: Напреднали техники за Shadow Mapping
Основният shadow mapping предоставя основа, но професионалните приложения използват по-сложни алгоритми, за да преодолеят неговите ограничения, особено алиасинга.
Percentage-Closer Filtering (PCF)
PCF е най-често срещаната техника за омекотяване на ръбовете на сенките и намаляване на алиасинга. Вместо да взема една единствена проба от картата на сенките и да взема бинарно решение (в сянка или не), PCF взема множество проби от областта около целевата координата.
Концепцията: За всеки фрагмент ние семплираме картата на сенките не само веднъж, а в мрежов модел (напр. 3x3 или 5x5) около проектираната текстурна координата на фрагмента. За всяка от тези проби извършваме сравнение на дълбочината. Крайната стойност на сянката е средната стойност от всички тези сравнения. Например, ако 4 от 9 проби са в сянка, фрагментът ще бъде 4/9 засенчен, което води до гладка полусянка (мекия ръб на сянката).
Имплементация: Това се прави изцяло във фрагментния шейдър. Включва цикъл, който итерира през малко ядро (kernel), семплирайки картата на сенките при всяко отместване и натрупвайки резултатите. WebGL 2 предлага хардуерна поддръжка (`texture` със `sampler2DShadow`), която може да извърши сравнението и филтрирането по-ефективно.
Предимство: Драстично подобрява качеството на сенките, като заменя твърдите, назъбени ръбове с гладки, меки.
Цена: Производителността намалява с броя на пробите, взети за всеки фрагмент.
Каскадни карти на сенките (Cascaded Shadow Maps - CSM)
CSM е индустриалният стандарт за рендиране на сенки от един насочен светлинен източник (като слънцето) върху много голяма сцена. Той се справя директно с проблема с перспективния алиасинг.
Концепцията: Основната идея е, че обектите близо до камерата се нуждаят от много по-висока резолюция на сенките от обектите, които са далеч. CSM разделя зрителния обем (view frustum) на камерата на няколко секции, или „каскади“, по неговата дълбочина. След това за всяка каскада се рендира отделна, висококачествена карта на сенките. Каскадата, най-близо до камерата, покрива малка площ от световното пространство и по този начин има много висока ефективна резолюция. Каскадите, които са по-далеч, покриват прогресивно по-големи площи със същия размер на текстурата, което е приемливо, тъй като тези детайли са по-малко видими за играча.
Имплементация: Това е значително по-сложно.
- На CPU, разделете фрустума на камерата на 2-4 каскади.
- За всяка каскада изчислете плътно прилягаща ортографска проекционна матрица за светлината, която перфектно обхваща тази част от фрустума.
- В рендиращия цикъл извършете прохода за дълбочина многократно — по веднъж за всяка каскада, рендирайки в различна карта на сенките (или в регион от текстурен атлас).
- Във финалния фрагментен шейдър на прохода за сцената определете към коя каскада принадлежи текущият фрагмент въз основа на разстоянието му от камерата.
- Семплирайте картата на сенките на съответната каскада, за да изчислите сянката.
Предимство: Осигурява постоянно висока резолюция на сенките на огромни разстояния, което го прави идеален за външни среди.
Variance Shadow Maps (VSM)
VSM е друга техника за създаване на меки сенки, но тя използва различен подход от PCF.
Концепцията: Вместо да съхранява само дълбочината в картата на сенките, VSM съхранява две стойности: дълбочината (първият момент) и дълбочината на квадрат (вторият момент). Тези две стойности ни позволяват да изчислим вариацията на разпределението на дълбочината. Използвайки математически инструмент, наречен неравенство на Чебишев, можем да оценим вероятността един фрагмент да е в сянка. Ключовото предимство е, че VSM текстура може да бъде замъглена с помощта на стандартно хардуерно ускорено линейно филтриране и мипмапинг, нещо, което е математически невалидно за стандартна карта на дълбочина. Това позволява много големи, меки и гладки полусенки с фиксирана цена за производителност.
Недостатък: Основната слабост на VSM е „просмукването на светлина“, където светлината може да изглежда, че преминава през обекти в ситуации с припокриващи се закриващи обекти, тъй като статистическата апроксимация може да се провали.
Глава 5: Практически съвети и производителност
Избор на резолюция на картата на сенките
Резолюцията на вашата карта на сенките е директен компромис между качество и производителност. По-голямата текстура осигурява по-резки сенки, но консумира повече видео памет и отнема повече време за рендиране и семплиране. Често срещаните размери включват:
- 1024x1024: Добра отправна точка за много приложения.
- 2048x2048: Предлага забележимо подобрение в качеството за десктоп приложения.
- 4096x4096: Високо качество, често използвано за ключови активи или в енджини със стабилно отсичане (culling).
Оптимизиране на зрителния обем (frustum) на светлината
За да извлечете максимума от всеки пиксел във вашата карта на сенките, е изключително важно проекционният обем на светлината (нейната ортографска кутия или перспективен фрустум) да е възможно най-плътно прилепнал към елементите на сцената, които се нуждаят от сенки. За насочена светлина това означава да се напасне нейната ортографска проекция така, че да обхваща само видимата част от фрустума на камерата. Всяко изгубено пространство в картата на сенките е изгубена резолюция.
WebGL разширения и версии
WebGL 1 срещу WebGL 2: Въпреки че shadow mapping е възможно в WebGL 1, то е много по-лесно и по-ефективно в WebGL 2. WebGL 1 изисква разширението `WEBGL_depth_texture` за създаване на текстура за дълбочина. WebGL 2 има тази функционалност вградена. Освен това, WebGL 2 предоставя достъп до семплери за сенки (`sampler2DShadow`), които могат да извършват хардуерно ускорен PCF, предлагайки значително повишаване на производителността в сравнение с ръчните PCF цикли в шейдъра.
Отстраняване на грешки при сенките
Сенките могат да бъдат изключително трудни за отстраняване на грешки. Единствената най-полезна техника е да визуализирате картата на сенките. Временно модифицирайте приложението си, за да рендирате текстурата на дълбочината от конкретен светлинен източник директно върху четириъгълник на екрана. Това ви позволява да видите точно какво „вижда“ светлината. Това може незабавно да разкрие проблеми с матриците на вашата светлина, отсичането на фрустума или рендирането на обекти по време на прохода за дълбочина.
Заключение
Рендирането на сенки в реално време е крайъгълен камък на съвременната 3D графика, превръщайки плоските, безжизнени сцени в правдоподобни и динамични светове. Въпреки че концепцията за рендиране от перспективата на светлината е проста, постигането на висококачествени резултати без артефакти изисква дълбоко разбиране на основните механики, от двупроходния конвейер до нюансите на отместването на дълбочината и алиасинга.
Като започнете с основна имплементация, можете прогресивно да се справите с често срещани артефакти като shadow acne и назъбени ръбове. Оттам можете да подобрите визията си с напреднали техники като PCF за меки сенки или Cascaded Shadow Maps за мащабни среди. Пътешествието в рендирането на сенки е перфектен пример за комбинацията от изкуство и наука, която прави компютърната графика толкова завладяваща. Насърчаваме ви да експериментирате с тези техники, да разширите техните граници и да внесете ново ниво на реализъм във вашите WebGL проекти.