Трассировка лучей в реальном времени в 1 КБ кода

от автора

Долгий путь к рождению Chrome Revenge

PENTRACE

Всё началось в 1994 году, когда я прочитал в Dr. Dobbs Journal несколько интересных статей о FPU (математическом сопроцессоре) нового процессора Pentium. Я пришёл к пониманию того, что численная производительность Pentium очень чувствительна к использованию и порядку команд FPU, и что дополнительными командами FXCH можно значительно увеличить скорость выполнения.

В то время при необходимости трассировки сцены лучами для получения результата требовались часы или даже дни. Я решил написать трассировщик лучей, похожий на POV-Ray или BOB, только на языке ассемблера, чтобы код при этом был сильно оптимизирован под FPU процессора Pentium. Это был «Pentrace», мой дипломный проект в колледже.

CHROME

Существовала очень известная и популярная анимация под названием Chrome, созданная трассировкой лучей. В основном она распространялась среди пользователей Amiga. Я решил, что будет круто показать ту же анимацию зрителям демопати Assembly, но всего в 4 КБ!


Интро Chrome на 4 КБ заняло в 1995 году четвёртое из 44 мест.

CHROME 2

Chrome я заканчивал уже на пати, прямо перед дедлайном, а на следующий год я первым бросил гибкий диск в коробку участников соревнования. Это интро стало первым продуктом с элементами трассировки лучей в реальном времени, только в очень низком разрешении, всего 144×102 пикселя.

Однако я шёл туда за победой! К сожалению, интро не демонстрировали в категории compo, потому что организаторы потеряли мою дискету и его показали только позже, между двумя блоками compo.


В конечном итоге, в 1996 году интро Chrome2 в категории 4kb заняло пятое место, но на следующей неделе мой продукт был первым по скачиваниям с Hornet Archive.

CODER-L

До 2001 года я управлял очень успешным венгерским списком рассылки для кодеров. В этом списке CODER-L было много обсуждений теоретических возможностей трассировки лучей в реальном времени.

Позже на основе этих обсуждений появились такие творения, как Slumpism by Pathos (при выполнении трассировки исходящих из камеры лучей он проводил перед каждой растровой строкой тест пересечения плоскости камеры и объекта) или Heaven Seven by Exceed (использовал для лучей камеры адаптивное субсэмплирование).

Однако тем временем, в конце 90-х, я потерял интерес к демосцене и мотивацию…

COLORFUL

Вернулся я в 2016 году, поучаствовав в qbparty. Меня поразило то, что в демосцене по-прежнему оставалось место для кодинга на ассемблере для DOS, поэтому я начал публиковать 256-байтные интро.

Однако с прежних времён аппаратная среда сильно изменилась, и после тестирования на многих современных компьютерах я понял, что единственным вариантом для трассировщика лучей остаётся видеорежим 640×480 c 32 битным цветом (с банкируемой памятью).


Моим первым 256-байтным интро в режиме HiRes стало Colorful (заняло второе место на Function 2016).

256B SEMINAR

В 2017 году я представил на Function свой движок трассировки лучей с покеболами. В нём использовалось адаптивное субсэмплирование, однако только в горизонтальном направлении из-за ужасного переключения банков в видеопамяти. И да, он был довольно быстрым.

SEASHELL

В этом демо я впервые использовал для лучей камеры ортогональную проекцию. Она выглядела достаточно красивой, была быстрой и занимала малый объём кода.


256-байтное интро Seashell заняло первое место на Riverwash 2017.

TCTRONIC

Мы нарушили правила compo на Function 2017. В нашем интро была музыка PWM PC-Speaker, воспроизводящая хорошо известный байтбит.


256-байтное интро TCTRONIC заняло второе место на демопати Function.

Plasmifier Cover 256b

Первое 256-байтное интро, имеющее музыку с программным синтезатором вместо просто байтбита.

Plasmifier Cover заняло второе место на Chaos Constructions 2018.

Благодарю Provod и Frag за то, что показали мне малозатратную огибающую IMUL xx,xx,-1 на стриме ikubun. И спасибо Gargaj за то, что обратил моё внимание на мастеринг в своей речи на Assembly.

Balls Harmony

HellMood открыл интересную тему о комбинировании записи, которое может ускорить запись в видеопамять. Особенно от этого выигрывали интро в True color.

При комбинировании записи я мог трассировать каждый пиксель экрана в своём интро (однако в эмуляторе это так не работало).


256-байтное интро Balls Harmony заняло второе место на Function 2019.

SSE 4.1

С Kuemmel мы начали код-гольфинг демо разработчика Frigo для Function (Space Fungus). Мы попытались ускорить его с помощью команд SSE. И запись в видеопамять при помощи MOVAPS [ES:DI],XMM7 оказалась даже быстрее, чем комбинирование записи.

Кроме того, Kuemmel из темы на Tiny Intro Toolbox узнал, насколько легко ограничивать значения цветов при помощи команд SSE.

SSE настолько крутое, там можно нормализовать векторы без SQRT и DIV!

Разработка графической части (автор: TomCat)

Технические подробности

HiRes TrueColor

Наименьшее разрешение TrueColor на любом современном PC — это 640 x 480 с 32 битами на пиксель. Можно легко задать его при помощи VESA BIOS.

 MOV BX,112H  MOV AX,4F02H  INT 10H

Однако номер видеорежима в разных VGA-картах может различаться. Чаще всего используется номер 112H. Он работает на nVidia, Intel, DosBox и т.п., но не на ATI. В VGA-картах ATI/AMD он устанавливает режим 640x480x24 бит. Поэтому в этом случае нужен номер 121H.

К сожалению, в VESA-драйверах виртуальных машин используются ещё более странные номера: VirtualBox — 142H, VMware — 13FH.

Видеопамять

Под DOS самым быстрым способом записи в видеопамять является команда MOVAPS, если её поддерживает процессор. Она может записывать по 16 байт (4 пикселя) за раз.

 MOVAPS [ES:DI],XMM7

Чтобы использовать её, нужно собрать 4 пикселя в регистры. Нижние 4 байта XMM2 — это новые пиксельные данные для одного пикселя. Повернув XMM7 влево на 4 байта, мы можем вставить новый пиксель.

 SHUFPS XMM7,XMM7,10010011B  MOVSS XMM7,XMM2

При вычислении цветовых компонентов пикселя мы получаем значения float. Их нужно преобразовать в integer и ограничить интервалом от 0 и 255. Для этого очень пригождаются команды SSE:

 CVTPS2DQ XMM2,XMM2  PACKSSDW XMM2,XMM2  PACKUSWB XMM2,XMM2

Видеопамять VESA high-color упорядочена в банки, поэтому после 4096 операций записи мы должны переключиться на следующий банк.

 ADD DI,16  JNZ .4  PUSHA  SUB BX,BX  MOV AX,4F05H  INT 10H  POPA  INC DX    ; DL: number of memory bank .4:

Адаптивное субсэмплирование

По сути, мы трассируем каждый четвёртый луч из камеры. Во время трассировки я вычисляю байт метки, то есть уникальное значение, зависящее от того, с чем мы пересеклись.

Если байт метки такой же, как и 4 пикселями ранее, то можно выполнить интерполяцию между цветами. Если нет, то нужно оттрасировать больше лучей камеры между двумя пикселями.

 INSERTPS XMM7,XMM2,00110000B ; XMM7: insert new color on the top .2:  SHUFPS XMM7,XMM7,10010011B   ; XMM7: rotate left  PAVGB XMM2,XMM7              ; XMM2: averaging the colors  MOVSS XMM7,XMM2              ; XMM7: put interpolated color on the bottom  CMP [BP+SI],BL               ; is it the same stampbyte?  LOOPNZ .3                    ; if no, then trace the next pixel  TEST CL,3                    ; was the fourth pixel?  JNZ .2                       ; if no, then interpolate the next pixel .3:  TEST CL,3                    ; was the fourth pixel?    JNZ .4                       ; if no, then skip putpixel  CALL putpixel  SHUFPS XMM7,XMM7,11111111B   ; XMM7: fill by the last color  MOV BL,[BP+SI]               ; store the stampbyte  ADD CX,8                     ; go to right by 8 pixels .4:  CMP CX,RESX/2+4              ; was it the last pixel in the raw?  JNE nextpixel                ; if no, then go to the next pixel                 

То есть нам нужно трассировать больше, чем каждый четвёртый луч из камеры, но в среднем, как мне кажется, значение меньше, чем каждый третий пиксель.

Ортогональная проекция

Испускание лучей камеры выполняется ортогонально к плоскости X-Y (другими словами, параллельно оси Z). Вектор направления всегда равен [0,0,1] а позиция глаза (камеры) является координатами X, Y экрана плюс любое отрицательное значение Z. Если конкретнее, то после корректировки соотношения сторон P равен [+94..-94,-160..+160,-8260].

 MOV AX,RESY/2 nextline:  MOV CX,-RESX/2+4 nextpixel:  PUSHA                      ; -20:DI SI BP SP BX DX CX AX 1 0  PMOVSXWD XMM6,[BX-8]       ; XMM6: P = x,y,1,0  CVTDQ2PS XMM6,XMM6  MOVAPS XMM5,XMM6  MULPS XMM6,[SI]            ; *Aspect [SI]=[0.5028877,0.39081812,-8260.683]  SHUFPS XMM5,XMM5,11101111B ; XMM5: D = 0,0,1,0

Векторы

Выполнение вычислений с тремя (или четырьмя) векторными координатами одновременно при помощи команд SSE само по себе является ускорением работы. Вот как я хранил следующие векторы в разных регистрах SSE:

;XMM0: temporary #1 ;XMM1: temporary #2 ;XMM2: color coordinates ;XMM3: reflection vector ;XMM4: normal vector ;XMM5: direction vector ;XMM6: point ;XMM7: collector for colors of 4 pixels

Нормализация?

Сегодня наиболее затратными командами являются деление и квадратный корень. При нормализации используются обе эти команды, поэтому я попытался избежать нормализации векторов. Именно поэтому мы испускаем лучи с ортогональной проекцией. Лучи камеры являются единичными векторами.

Отражённые лучи также являются единичными векторами из-за свойства отражения; нам не нужно их нормализовать. Единственными векторами, для которых нормализации нельзя избежать, являются лучи теней. К счастью, существует специализированная команда для вычисления величин, обратных квадратным корням.

 MOVAPS XMM0,XMM5         ; XMM5: D = VNORM(D)  DPPS XMM0,XMM0,01111111B  RSQRTPS XMM0,XMM0        ; instead of SQRTPS XMM0,XMM0  MULPS XMM5,XMM0          ; instead of DIVPS XMM5,XMM0

Примечание: RSQRTPS даёт серьёзный прирост производительности. Однако она ОЧЕНЬ приблизительная: она создаёт результаты с относительной погрешностью менее 1.5 * 2^-12. Учитывая то, что машинный эпсилон чисел float с одиночной точностью равен 2^-24, можно сказать, что эта аппроксимация имеет примерно половинную точность. Её нельзя использовать для лучей камеры, но она неплоха для лучей теней.

Когда я попытался нормализовать лучи камеры при помощи RSQRTPS, это привело к множественным артефактам в контуре сферы.

Затенение

Единственный источник света — это слишком скучно, поэтому у нас их два. Один источник имеет координаты (255,255,255), а второй противоположен ему и находится в (-255,-255,-255).

Для затенения я использовал модель Фонга. Компонент рассеянного освещения очень прост: dot(normal,shadow)

 MOVAPS XMM1,XMM4         ; XMM1: N.S  DPPS XMM1,XMM5,01110001B  MOVAPS [DI],XMM1  CMP [DI+3],CH  JLE @F                   ; Ambient  FADD DWORD [DI]          ; Ambient+Diffuse @@:

Компонент зеркального освещения более интересен: dot(reflected,shadow)^2^2^2

 DPPS XMM5,XMM3,01110001B ; XMM5: R.S  MOVAPS [DI],XMM5  FLD1  FADD DWORD [DI]          ; Specular Ambient+Diffuse @@:  FMUL ST0,ST0             ; Specular=Specular^2  INC CX  JPO @B                   ; loop x3

Рекурсия

Три уровня рекурсии очень заметны в отражениях, но дополнительные уровни будут пустой тратой ресурсов. Для проверки уровня текущей рекурсии я использовал регистр указателя стека.

 CMP SP,-22-2*maxlevel  ; Max recursion level = 3  LOOPE Tquit            ; JE Tquit + DEC CX  MOVAPS XMM5,XMM3       ; D = R  FMUL DWORD [SI]        ; level/2 0  FILD DWORD [SI]        ; big number for min  CALL Trace0 Tquit: RETN

На каждом уровне рекурсии я вдвое уменьшал яркость отражённого цвета.

Сцены

Источником вдохновения для первой сцены стала часть интро Chrome2, выполняемая в реальном времени. Но на этот раз сцена вычисляется в полный экран и с красивыми пересечениями.

В парке аттракционов Walt Disney Studios в основном холле наверху магазина вращается три сферы. Мне это очень понравилось.

Из соображений скорости во второй сцене есть только одна шина, состоящая из сфер, остальные — это отражения.

Я уже пытался воссоздать это гипнотическое движение в своём 256-байтном интро, но результат мне не понравился.

Эта трассируемая лучами сцена намного лучше с сохраняющимися цветами и отражениями.

Создание музыки (автор: ern0)

Написание Sizesong

Характеристики платформы

Традиционные платформы с ограниченной музыкой (например, монофонические рингтоны, Amiga mod, General MIDI и т.п.) имеют «высеченные в камне» ограничения: полифонию или набор тонов, но обычно не имеют значительных ограничений на размер данных. В нашем случае, на платформе динамика PC-DOS, справедливо обратное. Мы можем сгенерировать любую волну, нет ограничений на проигрыватель, однако вычисление каждого тона и функции секвенсора отнимает место у визуальных эффектов.

Мысли о кавере

Во-первых, создание кавера хорошо известной песни для ограниченной платформы — это интересная и сложная задача. Если кавер точный, то при прослушивании в нашей голове звучит и оригинал, что усиливает ощущения. С другой стороны, мы не можем изменять партитуру, чтобы выиграть несколько байтов. Мы можем просто вырезать части целиком (например, пропускать строки), убирать инструменты (например, не использовать бас) или упрощать их (например, применять упрощённые ударные). В крайних случаях, наподобие 549Notes, мы не можем пропускать и изменять ничего.

Структура композиции

Песня «I Remember» Deadmau5 и DJ Kaskade — идеальный кандидат на создание sizecode-музыки.

Её аккордовая последовательность достаточно характерна, чтобы представлять целую композицию, поэтому я могу отказаться от вокала. Кроме того, она имеет длину всего в 8 паттернов и содержит повторения:

 1 2 R R  3 4 R R 

Так как каждый аккорд состоит из 4 тонов, нам нужно хранить 5×4 = 20 нот.

Есть только одна сложность: смена аккорда происходит на четверть раньше ожидаемого, но только в каждом 2 из 4 случаев:

 [1 1 1 1] [1 1 1 2]  [2 2 2 2] [2 2 2 R]  [R R R R] [R R R R]  [R R R R] [R R R R]   [3 3 3 3] [3 3 3 4]  [4 4 4 4] [4 4 4 R]  [R R R R] [R R R R]  [R R R R] [R R R R]

К счастью, партия ударных довольно простая и повторяющаяся:

 [BD HT BD+SN HT] [BD HT BD+SN HT]  [BD HT BD+SN HT] etc. 

Согласно правилам жанра house, композиция начинается только с ударных, но только на половину повтора. Затем идут четыре полных повтора, с проигрыванием в каждом повторе более долгих нот. На четвёртом повторе с самыми длинными нотами дорожка ударных замолкает. Затем композиция продолжается почти с начала, но пропускает интро только с ударными.

  [drums only] loop point:   [drums + chords - short]   [drums + chords - long]   [drums + chords - longer]   [no drums, only chords - longest]     continue at loop point

Альтернативная версия

Мы попытались связаться с авторами, но нам это не удалось, поэтому у нас нет разрешения на публикацию кавера. Возможно, у нас есть на это право, но правила пати строже, поэтому нам пришлось изменить музыку.

Я хотел внести как можно меньше изменений, поэтому изменил только аккорды, оставив всё остальное неизменным, даже структуру аккордов:

 1 2 R R  3 4 R R

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

Оригинал мне нравится больше; он очень уникален и имеет необычно хорошо сделанную последовательность аккордов, особенно для современной композиции; он напоминает мне о 80-х, золотой эпохе поп-музыки.

Но я не разочарован своей версией; на самом деле, я начинал писать небольшую счастливую композицию, в которой эта тема будет рефреном.

Создание музыки, часть 2 (автор: TomCat)

Программный синтезатор PC-Speaker

Сэмпл

Чтобы PC speaker работал наподобие устройства PWM, мы переключили канал 2 PIT в нулевой режим работы («прерывание по счётчику терминала»).

 MOV AL,90H  OUT 43H,AL

Благодаря этому мы можем выводить 6-битный сэмпл. Значение 0 — это тишина, значение 63 — максимальная громкость.

IRQ:  PUSHA  MOV AL,2   ; Sample [0...127]  SHR AL,1   ; scaled to 6-bit  JZ .1      ; zero means pause (no sound)  OUT 42H,AL ; out the 6bit sample .1:  ...  POPA IRET

Темп и ноты

Мы присвоили счётчику таймера значение 68 (позже мы назвали его Divider), потому что хотели получить темп 129 bpm.

 MOV AL,68  OUT 40H,AL  MOV AL,0  OUT 40H,AL

Тогда частота проигрывателя будет 17,5 кГц.

Hz=105000000/88/Divider,

bpm(low)=Hz*60sec/2/2/2/2/2/2/2/2/2/2/2/2/2/2,

bpm(high)=Hz*60sec/2/2/2/2/2/2/2/2/2/2/2/2/2.

Для проигрывания музыкальных нот нужно умножать несущую волну на постоянные значения. Мы можем вычислять эти значения в зависимости от частот, например, нота A4 (ля четвёртой октавы) вычисляется так: A_4=(32*256*44000/Hz+50)/100.

У меня есть файл inlcude для нот: notes.inc

Ударные

Мы хорошо повеселились с ударными программного синтезатора pc-speaker в выпущенном ранее продукте.

И этот очень простой паттерн ударных (bd — ht — sn/bd — ht) использовался и в другом интро, но с другим темпом: qblove.

Басы ударных реализованы простым делением: постоянное значение делится на время (или счётчик таймера).

KICKDRUM:  MOV AX,16128   ; constant value  CWD            ; extend the value to 2 words  MOV CX,8191    ; get some lower bits of  AND CX,SI      ; the timer counter (SI)  INC CX         ; avoiding division by zero  DIV CX         ; get one bit only  AND AL,64      ; 0 or 64 will be the volume (the sample)

Сравнивая SN (слева) с BD (справа), можно заметить, что малый барабан намного дольше и более случайный, чем басовый.

Хай-хэту тоже требуется случайный шум, поэтому HT и SN начинаются с одного фрагмента кода.

 XCHG AX,DI    ; DI: pointer to video memory, which is quite random here :)  MUL SI        ; SI: the timer counter  XCHG AX,DX    ; AX: random number depending on time  AND AX,32     ; get only one random bit

Аккорды

Один аккорд состоит из четырёх просуммированных нот.

 MOV BP,NOTES CHORDS:  SALC            ; AL: zero  MOV AH,[BP+DI]  ; AX: pitch F#, ...  MUL SI          ; SI: the timer counter  AND DX,1FH      ; DX: get a saw wave sample  ADD BH,DL       ; BH: sample (sum of the notes)  INC DI          ; next note  TEST DI,3       ; loop 4x  JNZ CHORDS

Идея создания кавера популярной песни в среде с ограничениями — это очень увлекательно. Именно поэтому длительность нот отличается от степени двойки.

Аккорды имеют длину 7, 8 и 17. Мне пришлось преобразовать длительности в 8, 8 и 16. Поэтому я сместил начало и в последнюю долю поместил тишину.

 TEST SI,1111000000000000B  JZ SKIP               ; no chords at the last beat  ...                   ; chords here DRUMS:  SUB SI,1000000000000B ; shifting back the drums by one beat

Но после того, как музыка провалилась на Revision, мы её изменили. Я считаю, что ern0 нашёл гораздо более интересные аккорды. Мне нравится настроение новой композиции.

Паттерн

Хранимые данные музыкальных нот почти всегда можно ужать. «Сырые» данные имеют размер 32 байта:

NOTES:  DB A_3,B_3,D_4,Fs4  DB B_3,D_4,E_4,Gs4  DB Cs4,E_4,Fs4,A_4  DB Cs4,E_4,Fs4,A_4   DB B_3,D_4,Fs4,B_4  DB B_3,Cs4,E_4,Gs4  DB Cs4,E_4,Fs4,A_4  DB Cs4,E_4,Fs4,A_4

Вот простой декодер:

 MOV DI,SI  SHRD DI,BX,2+16-5  AND DI,7*4

Видите, мы можем сэкономить 12 байт данных…

NOTES:  DB A_3,B_3,D_4,Fs4  DB B_3,D_4,E_4,Gs4   DB B_3,D_4,Fs4,B_4  DB B_3,Cs4,E_4,Gs4   DB Cs4,E_4,Fs4,A_4

А на декодере мы потеряли всего 7 байт.

 MOV DI,4*4  SHR BL,1  JC .1  MOV DI,AX  SHLD DI,SI,3  AND DI,3*4 .1:

Огибающие

У нас есть две огибающие. Простая ADSR: без атаки, только затухание.

Подготавливаем регистры CL и CH перед циклом аккордов:

 MOV AX,BX       ; BL: pattern  MOV CX,3  SHR AL,1  JC .1  MOV CH,128 .1:  DEC AX          ; skipping the drums-only intro  SHR AL,1  AND CL,AL

Одна огибающая для тона…

 IMUL AX,SI,-16  OR AL,111B  ROR AX,CL  MUL BX          ; DH: sample

и одна для громкости…

 IMUL AX,SI,-1  OR AH,CH        ; CH: 128 or 0  MUL DX          ; DH: sample

Мастеринг

После сложения каждого инструмента, барабана и аккорда я проверяю максимальную громкость. Я всегда храню отладочную информацию в памяти и вывожу её при помощи внешней утилиты.

if maxvol=1  CMP [FS:8000H],BH  JAE .1  MOV [FS:8000H],BH .1: end if

На этот раз максимальное значение сэмпла равно 181. Поэтому я ограничу его всего 7 битами.

 MOV AL,180       ; vol 181 -> vol 127  MUL BH           ; mastering  XCHG BX,AX DONE:  MOV [BP+IRQ.SAMPLE-1],BH

Но на части подавления я убираю это снижение размерности и аккорды становятся ещё громче.

Вот сгенерированная песня целиком:

Готовое интро можно скачать отсюда: https://www.pouet.net/prod.php?which=87078

ссылка на оригинал статьи https://habr.com/ru/post/527090/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *