Наскоро участвах в състезанието за демосцени Revision 2019 в категорията „PC 4k intro“ и моето демо зае първото място. Аз направих кода и графиката, а dixan съчини музиката. Основното правилно в това състезание е да се създаде изпълним файл или уеб сайт с размер едва 4096 байта. Това означава, че всичко трябва да се генерира с помощта на математика и алгоритми, понеже по никакъв друг начин няма как да се компресират изображения, видео и аудио в толкова малко памет. Тук ще опиша рендирането на моята демосцена Newton Protocol. По-горе може да бъде видян крайния резултат А ето тук може да се види как изглеждаше всичко по време на Revision.

Изключително популярна в дисциплината 4k intro е техниката Ray marching distance fields, понеже дава възможност за формирането на сложни форми само с няколко реда програмен код. Но този метод е характерен с твърде ниската си скорост на работа. За рендирането на сцени е необходимо да се намерят различните точки на пресичане на лъчите със сцената – например, на виртуалния лъч от камерата, както и лъчите от източниците на светлина до обектите, за да може правилно да се изобрази осветяването. От друга страна, при използването на технологията за трасиране на лъчите (ray tracing), може да се намери точното пресичане чрез проверката на всеки обект по няколко пъти. Но в този случай, броят на генерираните фигури е много ограничен, понеже за изчсляването на различните пресичания на лъчите е необходима математическа формула за всеки тип фигури.

В тази демосцена аз исках да симулираm съвсем точно осветяване. За тази цел в сцената трябва да бъдат отразени милиони лъчи и за постигане на този ефект логичния избор бе рейтрейсинга. Ограничих се с изобразяването на само една фигура – сфера, понеже за нея пресичането на лъчите се изчислява сравнително лесно. Дори и стените в това демо са много големи сфери. Освен това, по този начин се опрости и симулацията на физиката – достатъчно бе да се отчетат само колизиите между сферите.

За да илюстрирам обемa на кода, побран в 4096 байта, по долу представям пълния сорс код на тази демосцена. Всичките части, с изключение на HTML в края, са компресирани като PNG изображение, за да може да се намали техния обем. Без тази компресия кодът щеше да заеме почти 8900 байта. Частта с име Synth е орязана версия на SoundBox. За пакетирането на кода в този минимизиран формат, използвах Google Closure Compiler и Shader Minifier. В края почти всичко е компресирано в PNG с помощта на JsExe. Пълният конвейер на компилацията може да се види в моята предишна 4К демосцена Core Critical, като тук съм използвал съвсем същия код.

Музиката и синтезаторът изцяло са написани на JavaScript. Делът на WebGL е разделен на две части (показани са със зелен цвят), като те служат за настройка на конвейерите на рендера. Елементите на физиката и трасирането на лъчите са GLSL шейдъри. Останалата част от кода е кодирана като PNG изображение, а HTML е добавен в края на това изображение без никакви промени. Браузърът игнорира данните на изображението и изпълнява само HTML кода, който от своя страна декомпресира PNG формата обратно в JavaScript код и го изпълнява

Конвейерът на рендирането

В изображението по-долу е показан конвейера на рендирането. Той е съставен от две части. Първата част на този конвейер е емулатор на физиката. В демосцената участват 50 сфери, които се сблъскват една с друга в една виртуална стая. Самата стая е съставена от 6 сфери с различен размер, за да се създаде по-нестандартно пространство. Двата източника на светлина, разположени в горните ъгли също са сфери – тоест, в цялото демо участват общо 58 сфери.

Втората част на контейнера включва трасирането на лъчите, което рендира цялата сцена. На показаната по-горе схема е показано рендирането на един кадър в t момент от времето. Емулаторът на физиката взема предишния кадър (t-1) и емулира неговото текущо състояние. Алгоритъмът за трасиране на лъчите взема текущата позиция и позицията от предишния кадър, за да може да осъществи самото рендиране. След това, по време на постобработката се комбинират предишните 5 кадъра с текущия кадър, с което силно се намалява нивото на шума. Едва след това се представя готовия резултат.

Алгоритъмът за емулиране на физиката е доста опростен и в интернет могат да бъдат намерени много материали за създаване на примитивна емулация на физичните закони за сферите. Позицията, радиусът, скоростта и масата на сферите се съхраняват в две текстури с резолюция 1х58. Използвах функционалността Webgl 2, даваща възможност за рендиране на няколко обекта едновременно и ето защо, данните на две от текстурите се записват едновременно. Същата функционалност се използва и в алгоритъма за трасиране на лъчите, за създаване на три текстури. Webgl не предоставя никакъв достъп до API за технологиите за трасиране на лъчите NVidia RTX и DirectX Raytracing (DXR) – ето защо, всичко трябваше да се направи от нулата.

Трасирането на лъчите

Самото трасиране на лъчите е достатъчно примитивна техника. Ние пропускаме в сцената виртуален лъч, който се отразява 4 пъти и ако попадне в източник на светлина, то цветът на отразяването се натрупва. В противен случай имаме черен цвят. В тези 4096 байта (включващи музика, синтезатор, физика и рендиране) няма място за използване на сложни алгоритми, които биха ускорили трасирането на лъчите. Ето защо, тук използвам метода на грубата сила за проверката на всички варианти. Всичките 57 сфери се проверяват за пресичане с всеки виртуален лъч, без да се правят каквито и да било оптимизации за изключване на някои техни части. Това означава, че за да постигнем 60 кадъра в секунда при 1080р резолюция е достатъчно да се използват само от 2 до 6 виртуални лъча или семпъла на пиксел. А това е съвсем недостатъчно за създаването на плавно осветяване на обектите.

1 кадър на пиксел

Как да се справим с това? В началото се спрях на алгоритъма за трасиране на лъчите, но той и така бе опростен до краен предел. Успях леко да повиша неговата производителност, като избегнах случаите, когато лъчът преминава през вътрешността на сферите, но това няма как да се използва в този случай, понеже в нашата демосцена участват само непрозрачни обекти. Но не се получи.

6 кадъра на пиксел

Съседните пиксели би трябвало да имат сходна осветеност и защо да не използваме това свойство при изчисляването осветеността на единичния пиксел? Ние не искаме да размиваме текстурите, а само осветяването и затова те трябва да се рендират в отделни канали. Също така, не искаме да получим размити обекти и трябва да отчитаме техните идентификатори, за да знаем кои точно пиксели можем да размазваме от гледна точка на техните изображения.

Също така, имаме отразяващи светлината обекти, а тъй като имаме нужда от ясни отражения, трябва да разберем ID на първия обект с който ще се сблъска лъча, което съвсем не е лесно. Аз използвах частния случай при съвсем чисти отражателни материали, за да мога да включа в канала на идентификаторите на обектите и ID идентификатора на първия и втория обекти, видими в отраженията. В този случай, размиването може да изглажда осветяването на обектите в отраженията, като в същото време се запазват границите на самите обекти.

Каналът на текстурите, който не трябва да се размива
Тук червеният канал съдържа ID на първия обект, зеления – на втория и синия – на третия обект. В кода те всички се записват в едно float число, в което в цялата част са записани идентификаторите на обектите, а дробната обозначава грапавината (roughness)

Обектите в демосцената са с различна грапавина (някои сфери са грапави, други разсейват осветяването, а трети са с огледално отражение). Ето защо се използва грапавината за управлението на радиуса на размиването. В демото няма малки детайли и за размиването се използва голямо ядро с размер 50х50 и тегло във вид на обратни квадрати. По този начин се получава достатъчно гладко изображение, но все още много добре се забелязват многото артефакти, особено при движение.

Когато обектите се намират на сцената и заснемащата камера бавно се движи, осветеността във всеки един кадър трябва да остане постоянна. Ето защо ще трябва да осъществим размиване не само в XY координатите на екрана, а и по време. Ако предположим, че осветеността няма да се промени особено много в рамките на 100 милисекунди, то тя може да бъде осреднена в рамките на 6 кадъра.

Канал за скоростите на пикселите, в който се вижда къде се е намирал пикселът в последния кадър, на базата на движението на обекта и на камерата
За да избегнем съвместното размиване на обектите, отново използваме стек за техните идентификатори. В този случай се съобразяваме само с първия обект, с който се е сблъскал лъчът. По този начин осъществяваме изглаждане (антиалайзинг) в пределите на обекта – тоест,в отраженията

Разбира се, пикселът от предния кадър може да се окаже невидим – да е скрит зад друг обект или да се намира извън полето на видимост на камерата. В тези случаи не можем да използваме предишната информация. Тази проверка се извършва отделно за всеки един кадър, а ние натрупваме информацията от 1 до 6 кадъра за всеки пиксел и използваме само тези пиксели, които е необходимо. По-долната илюстрация показва, че за бавните обекти това не е проблем.

Когато обектите се движат и разкриват нови области от сцената, ние нямаме 6 предишни кадъра с информация, за да я усредним за пикселите в тези области. Тук са показани областите, в които има шест кадъра (бял цвят), както и области, в които информацията е недостатъчна (потъмняващите отенъци)
Размитото осветяване е усреднено за 6 кадъра. Артефактите са вече почти незабележими и резултатът е стабилен с течение на времето

Комбинирайки всичко това, получаваме готовото изображение. Осветяването се размива по съседните пиксели, а текстурите и отраженията остават ясни и детайлни. С течение на времето се използва усредняване на всеки 6 кадъра, с което се създава гладко и стабилно във времето изображение.

Готовото изображение

Като цяло съм много доволен от тази демосцена. Успях да вмъкна в нея всичко, което бях планирал. Въпреки почти незабележимите бъгове, резултатът е много качествен. В бъдеще обмислям да изчистя бъговете и да подобря изглаждането. Мисля да пробвам с въвеждането на прозрачност, motion blur, с по-различни фигури и с трансформация на обектите.