Doom работа­ет на всем! На дис­плее вей­па, внут­ри тес­та на беремен­ность, в кишеч­ных бак­тери­ях E. coli (30 секунд на кадр, пол­ное про­хож­дение — 600 лет) и на трак­торе John Deere. Сегод­ня мы запус­тим Doom внут­ри шриф­та. Как? Сей­час рас­ска­жу.
 

VM в шрифтах?

Да, в шриф­тах дей­стви­тель­но есть вир­туаль­ная машина, и она впол­не может быть инте­рес­на как необыч­ный век­тор ата­ки.

Ког­да ты откры­ваешь стра­ницу, бра­узер заг­ружа­ет шрифт и дол­жен как‑то нарисо­вать бук­вы. Казалось бы, берешь век­торные кон­туры гли­фов и рас­теризу­ешь в нуж­ном раз­мере. Проб­лема в том, что если прос­то отмас­шта­биро­вать век­тор и залить пик­сели, на мел­ких кег­лях получа­ется каша: тон­кие засеч­ки про­пада­ют, вер­тикаль­ные штри­хи пры­гают меж­ду пик­селями, а эле­мен­ты, которые дизай­нер задумал оди­нако­вой тол­щины, получа­ются раз­ными. В 90-х, ког­да экра­ны были 72–96 dpi, это было катас­тро­фой.

В 1991 году прог­раммис­ты Apple при­дума­ли решение: пусть сам шрифт содер­жит прог­рамму, которая под­ска­зыва­ет рас­териза­тору, как пра­виль­но натянуть кон­туры на пик­сель­ную сет­ку. Она называ­ется хин­тингом (от англий­ско­го hint — «под­сказ­ка») и выпол­няет­ся в сте­ковой машине с поч­ти 200 инс­трук­циями.

В Microsoft под­хва­тили идею — так TrueType стал стан­дартом де‑фак­то. Сегод­ня хин­тинг есть в каж­дом фай­ле TTF, и каж­дая бук­ва на экра­не — резуль­тат выпол­нения нес­коль­ких сотен инс­трук­ций этой вир­туаль­ной машины. Интер­пре­татор живет в сис­теме FreeType в Linux и Android, в Windows это называ­ется DirectWrite, а в macOS — Core Text. До Windows 10 Anniversary Update раз­бор шриф­тов вооб­ще выпол­нялся в ядре (win32k.sys) — к это­му мы еще вер­немся в раз­деле про безопас­ность, будет инте­рес­но.

Так вот, набор инс­трук­ций этой VM ока­зал­ся… щед­рым. Слиш­ком щед­рым для задачи «сдвинь вер­шину кон­тура на пол­пиксе­ля». Там есть условные перехо­ды (IF, ELSE и EIF), рекур­сивные вызовы фун­кций (FDEF и CALL), область памяти Storage Area на 65 535 яче­ек, ариф­метика и логика. Все, что нуж­но, что­бы машина ока­залась Тьюринг‑пол­ной.

www

На Gwern.net ведут ка­талог «неожи­дан­но Тьюринг‑пол­ных» сис­тем, и TrueType дав­но в спис­ках.

Кто‑то дол­жен был исполь­зовать это для чего‑нибудь поин­терес­нее счет­чика. Я решил: почему бы не Doom?

 

Рейкастинг на пальцах

Преж­де чем лезть в бай­ткод, раз­берем­ся, как вооб­ще работа­ет 3D в Doom и Wolfenstein 3D.

Идея рей­кас­тинга гени­аль­на тем, что поз­воля­ет рисовать псев­до-3D на железе, которое не уме­ет работать с 3D. Кар­та — это дву­мер­ная сет­ка 16×16 кле­ток, в каж­дой — пус­тота или сте­на. Игрок сто­ит где‑то на сет­ке и смот­рит в какую‑то сто­рону.

Даль­ше алго­ритм такой: экран делим на вер­тикаль­ные полос­ки (нап­ример, 16 — по одной на каж­дый стол­бец). Для каж­дой полос­ки из позиции игро­ка «бро­саем» луч в нап­равле­нии этой час­ти экра­на. Луч летит по дву­мер­ной кар­те, пока не упрется в сте­ну. Изме­ряем рас­сто­яние до сте­ны. Чем даль­ше сте­на, тем ниже мы рису­ем ее на экра­не. Все, 3D готово.

Тон­кость в том, что «летит по кар­те» — это не бук­валь­но на пик­сель за раз: так мож­но прос­кочить сте­ну меж­ду шагами. Исполь­зует­ся алго­ритм DDA: пры­гаем сра­зу до сле­дующей гра­ницы клет­ки — горизон­таль­ной или вер­тикаль­ной, смот­ря какая бли­же. Так гаран­тирован­но не прос­кочим.

Джон Кар­мак в 1992 году делал это на 386-м про­цес­соре без FPU, поэто­му все счи­талось в целых чис­лах с эму­ляци­ей дро­би (fixed-point), а синусы и косину­сы бра­лись из пред­вычис­ленных таб­лиц. Сей­час это покажет­ся нам очень зна­комым — потому что внут­ри хин­тинга TTF мы ока­жем­ся ров­но в той же ситу­ации.

 

Архитектура

Идея TTF-DOOM — раз­делить обра­бот­ку на две час­ти.

JavaScript на стра­нице чита­ет кла­виату­ру (WASD и стрел­ки), отсле­жива­ет позицию игро­ка и угол поворо­та, рису­ет на Canvas все, для чего нуж­ны спрай­ты: вра­гов, ору­жие, HUD. А шрифт дела­ет тяжелую геомет­ричес­кую работу: получа­ет коор­динаты игро­ка, бро­сает лучи, счи­тает рас­сто­яния до стен и воз­вра­щает мас­сив высот. По сути, это GPU, толь­ко очень стран­ный и мед­ленный.

Воп­рос: как передать шриф­ту дан­ные, если шрифт — ста­тичес­кий файл? Через CSS, как ни стран­но.

В OpenType есть variable fonts — шриф­ты с «ося­ми вари­ации». Ты зада­ешь в CSS такую строч­ку:

font-variation-settings: 'MOVX' 123, 'MOVY' 456, 'TURN' 789

А потом бра­узер дол­жен переза­пус­тить хин­тинг при каж­дом изме­нении. В хин­тинге есть спе­циаль­ная инс­трук­ция GETVARIATION (опкод 0x91) — она кла­дет на стек текущие зна­чения всех осей. Вот твой IPC меж­ду миром JavaScript и миром сте­ковой машины.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии