Как стать автором
Обновить

Как мы написали самый¹ быстрый 2.5D шутерный движок за историю человечества и как он работает

Уровень сложностиСредний
Время на прочтение37 мин
Количество просмотров6.6K

¹Но это не точно. Мы не сравнивали, конечно. Но крайне вероятно, что действительно самый быстрый. И вообще, он ещё не в машкодах.

У нас был 6502-й, алгоритм Брезенхема, самый нелепищный и несуразный графический ускоритель и немного переключаемых страниц с маппером, в которые мы могли положить свои таблицы.

Единственное, что внушало мне опасение — это обратный квадратный корень, который даже самого Кармака довёл до состояния «ватзефак». Нету твари более нелинейной, внезапной, максимально зловредно меняющей зависимость выходных данных от всех входных данных в разных сочетаниях и пожирающей весь возможный диапазон входных значений, чем единица, делённая на квадратный корень из суммы квадратов.

Но я знал, что рано или поздно мы докатимся и до этой дряни.

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

DDA — это, конечно, не про 6502-й сказано. Данное «устройство процессорсодержащее» не имеет даже целочисленного аппаратного умножения! Да что там — даже сложение с переносом делается руками, через три крошечных однобайтных регистра, каждый раз требуя доступа к памяти! 8086 и 8088 на его фоне — просто звери, а 80286 — вообще ядро суперкомпьютера. Они могут перемножить два 16-битных числа аппаратно, разом, пусть и «за когда‑нибудь» в смысле количества тактов! И даже разделить! Мы этой роскошью не владели и сразу перешли от DDA к слегка доработанному напильником «Брезентычу», решавшему (как я показал в прошлой статье) вопрос трассировки лучей при помощи почти одних сложений.

Почти.

В прошлой статье я не особо касался вопроса, а что, собственно говоря, нам делать с этими лучами. Поэтому сейчас я разберу самый простой, базовый и красивый случай: плоский экран, геометрически верная картинка, ссылки на мобов и минимальные модификаторы геометрии.

 Рис. 1. Сферовакуумный рейкаст в стиле Wolf3D, вид сверху (ваш Кэп).
Рис. 1. Сферовакуумный рейкаст в стиле Wolf3D, вид сверху (ваш Кэп).

Слева внизу, в отдельно вырезанном участке, показан волшебный мир бесконечно малых приращений (дельт). Ну, или не совсем бесконечно малых — но, в любом случае, оно всё существует «внутри точки, являющейся камерой», т. е. в масштабах лабиринта всё это является одной точкой, поэтому пришлось «выкусить» часть картинки (иначе получалось, что экран пересекает границы клеток, что суть абсурд). Состоит этот наномир из, собственно, точки самой камеры, из которой расходятся все лучи (жирная красная), вектора, в котором направлен взгляд этой камеры (игрока), который обозначен красной стрелкой, и экрана, на который проецируется всё это действо (тёмно‑сине‑зелёный «кинескопный цвет»), который, как правило, перпендикулярен взгляду и простирается симметрично в обе стороны от его вектора.

Чтобы прочертить все лучи, нужно взять каждую горизонтальную координату экрана (их 320 штук для Wolf3D‑шного разрешения 320×200) и определить её дельты. Поскольку эти координаты находятся друг от друга на однопиксельном расстоянии — расчёт получается довольно элементарный. Повернув мысленно всю картину так, чтобы игрок смотрел (по схеме) горизонтально направо, мы увидим, что dX у нас равна единице для всех точек, а dY меняется от -1 до 1, переходя через 0 в центре поля зрения камеры (предполагаем самый простой вариант — поле зрения у нас ровно 90 градусов, хотя, кажется, на рисунке у меня получилось немного меньше). Дёшево и сердито, никакой тригонометрии. Однако, реальные игроки смотрят не только на тысячу ярдов абы куда, но и крутят полем зрения, как заблагорассудится! Поэтому все лучи надо повернуть при помощи простейшего умножения матриц. Звучит страшно, но на самом деле это очень простой код:

for (i=0; i<320; i++)
{
  RayDX = DX + DY*(i-160)/160;
  RayDY = DY — DX*(i-160)/160;
* * *//здесь могла быть ваша ре... весь остальной движок:)
} 

DX и DY — это дельты, собственно, «главного вектора», который указывает направление взгляда. Их придётся целый один раз на всю сцену вычислить при помощи настоящих синусов и косинусов. Но, поскольку точность человеческой руки даже с самой лучшей мышкой сильно меньше 1.8%, алгоритм Бхаскары Первого для этой цели вполне пригоден. RayDX и RayDY, естественно, дельты отдельных лучей, которые мы и вычисляем тут. И вот эта незатейливая сумма из двух слагаемых — это и есть та самая страшная матрица поворота на заданный угол, для двухмерного случая, конечно.

Теперь пришла пора обратить внимание на одну пикантную особенность плоских экранов. Они — плоские. Очевидно? Да. А вот что менее очевидно — это то, что расстояние от камеры до экрана меняется по Пифагору. От единички в самом центре до корня из двух на краю (опять же, для 90-градусного поля зрения). На первый взгляд, это не имеет значения — но мы скоро столкнёмся с последствиями этого.

Определившись с параметрами луча, мы начинаем смело им шагать, как было описано в прошлой статье. Сначала мы ставим луч в начало клетки, потом шагаем до столкновения хотя бы с чем‑то... всё это вы уже, конечно, знаете. Но что у нас дальше?

В простейшем случае мы просто пересекаемся со стенкой. В Wolf3D стенки одного блока со всех сторон имеют одинаковую текстуру (отличающуюся только яркостью для стенок север‑юг и стенок запад‑восток), но, конечно, в общем случае это вовсе не обязательно так (в Wolf3D, кстати, некоторые стенки тоже отличались значительно, но возможности задавать 4 произвольные текстуры там всё‑таки не было). Установив точку пересечения, мы можем определить ту высоту, до которой должны растянуть колонку соответствующей текстуры на экране. Для этого мы вычисляем разницу её координат с камерой, берём по Пифагору итоговое расстояние и... и делим на него расстояние от камеры до экрана. Да‑да, то самое, которое гуляет от 1 в центре до √2 на краях. Потому что высота текстуры на экране — это соотношение расстояния от камеры до секущей сцену экранной плоскости и расстояния от камеры до точки пересечения с текстурой.

Такие дела.

В самом Wolf3D Кармак наверняка смухлевал, и не раз, но мы продолжаем рассматривать пока наш «идеально‑абстрактный» случай. А он, как мы видим, после окончания трассировки луча потребует от нас вычесть координаты друг из друга, потом возвести в квадрат, сложить, взять корень и на результат поделить табличное значение расстояния до экрана в данной колонке (серьёзно, неужели кто‑то будет и его тоже каждый раз рассчитывать по Пифагору заново? В отличие от расстояния до стенки, оно‑то у нас не меняется). К счастью, можно обойтись одними умножениями — знаменитый «обратный корень Кармака» (на самом деле не совсем его, а очень вероятно, что вовсе даже не менее легендарного Абраша) всё сделает за нас, если у нас есть хотя бы какая‑то плавающая точка.

Какие у нас ещё бывают случаи, кроме простейшего? Самый необходимый — это, конечно, мобы. В общем случае моб имеет неприятное свойство локтями, плечами и полужопиями (а то и щупальцами, трогальцами и тентаклями) занимать около 4 смежных клеток. Поэтому моб при движении сразу оставляет в лабиринте ссылки на свой номерок, а после того, как последнее трогальце из клетки выползет — ссылки подчищаются. Обозначив моба нашей любимой «пырпырчатой» фуксией, мы увидим, что его ссылки — это четыре клеточки, которые мы обозначим жёлтым. При попадании луча в клетку мы должны вычислить колонку на текстуре моба при помощи немножко «более других» геометрических построений, потому что моб всегда предполагается повёрнутым своим спрайтом ортогонально к тому лучу, который проходит через его центр. Плюс мы волей‑неволей должны иметь хотя бы небольшой Z‑буфер, потому что моб, как правило, не занимает всю высоту клетки в каждой своей колонке, если только мы не нарисовали гигантского Губку Боба, конечно. Поэтому после вычислений, связанных с мобом, мы должны продолжить каст до настоящей стенки.

А иногда — и дальше.

Некоторые типы стенок являются модификаторами геометрии. Попадание в них вызывает обращение к специально захардкоденным случаям, которые разруливают ту или иную «неквадратную клетку». В нашем примере мы легко можем увидеть зелёную дверь в классическом Wolf3D‑стиле, которая имеет совершенно нетипичные (по сравнению с остальными стенками) точки коллизий с лучом, т. е. при попадании в неё луч будет отдельно обрабатываться, просчитываться ширина, до которой раскрыта в данный момент дверь, и после всего луч может вообще «вернуться ни с чем» для дальнейшего каста — ну не повезло, в приоткрывшуюся часть двери попал, такты процессора потрачены напрасно. Кроме этого, он может благополучно выйти из каста с атипичной точкой пересечения (попал в полотно двери, надо добавить пол‑блока к координатам в соответствующей пропорции), а может — попасть в соседние блоки, где точка пересечения совершенно обычная, но вот текстура подменяется на текстуру «дверной косяк» (именно такова была судьба изображённого мной единственного лучика из числа влетевших в этот блок). Нетрудно видеть, что таких «сложных случаев» на пути каста должно быть как можно меньше, а в идеале полностью открытая дверь вообще должна каститься как обычный пустой блок, не считая модификаторов соседних текстур.

Разумеется, мы вольны добавлять какие угодно «кастомные блоки», лишь бы они не нарушали общую 2.5-мерность. Круглая колонна (настоящая, не спрайт, как в Wolf)? Запросто. Скруглённый угол стенки в повороте коридора? Да сколько угодно. Ниша в блоке, в которой лежит ништячок? Ещё проще, но только строго от пола до потолка, разумеется, как и всё остальное. В целом это довольно благодатная тема для развлекательного программирования движков типа Klooni (причём играть в них, как показал опыт, не менее «развлекательно», чем их писать — классический квадратно‑гнездовой геймплей неисчерпаем, как и более современные варианты).

Однако вернёмся к нашему 6502-му. @Swamp_Dok, ознакомившись с моими прошлыми выкладками, сразу выдал идею — рассчитать все возможные варианты лучей заранее, ибо умножение матриц — это явно для него тяжеловато даже в таком «детском» варианте. Сначала мне эта идея не очень зашла и захотелось как‑то обойтись без таких крайностей, ибо это означало переход от плоского экрана к «экрану‑барабану», на котором «рыбоглазые» искажения неизбежны.

 Рис. 2. Лучи с «экрана‑барабана» к плоскому экрану в принципе не приводятся — всегда будет «где густо, где пусто».
Рис. 2. Лучи с «экрана‑барабана» к плоскому экрану в принципе не приводятся — всегда будет «где густо, где пусто».

Итак, сперва испугавшись того, что колонки в принципе не будут попадать на свои места и стенки будут по горизонтали где сжаты, а где и растянуты (и вертикальными коэффициентами это в принципе никак не поправить) — я сначала уповал было на то, что расчёт одного поворота через произведение матриц не сильно скажется на производительности. Но по мере вдумчивого чтения документации на «устройство процессорсодержащее» и ознакомления с его «возможностями»... точнее, с его невозможностями — лицо моё вытягивалось, а идея реализовать «шестерёнку Дока» начинала выглядеть всё заманчивее и заманчивее. Вообще Док подал много идей, в том числе разбиение карты на чанки 16×16 (позволяют хранить номер клетки в одном байте), ну и подобное тому. Не всё из этого позволило мне сэкономить хотя бы один такт (а в большинстве своём как бы хуже не делало) — «но Фуззи‑Вуззи — этот стоил всех!»

По мере реализации «шестерёнки» я обнаруживал всё новые и новые пласты математических действий, которые становились с ней лишними. И действия эти посыпались из кода вон, как горох. Это была даже не оптимизация — это был камнепад математических операций.

Первое и самое замечательное — это то, что «все возможные варианты лучей» сводятся к весьма малому их количеству. Дело в том, что прямого доступа к пикселам у нас тоже нет — мы работаем с экраном через, наверное, наименее подходящую для трёхмерных движков видеоподсистему. Через систему тайлов 32×30 штучечек, которые, спасибо Нинтендо, можно хотя бы дружно вместе сдвинуть по горизонтали или по вертикали. Вертикаль нас мало волнует, а вот горизонталь... заметили у «экрана‑барабана» одну положительную черту, являющуюся прямым продолжением его недостатка?

Именно.

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

Итого, учитывая, что нам нужно не более 32 лучей на один экран (что в 10 раз меньше, чем 320 у Wolf3D), ибо больше мы всё равно не разглядим с нашими 32 колонками спрайтов — получается, что у нас существует всего 128 возможных углов для рейкаста, то есть по 32 на каждые 90° обзора. А все остальные промежуточные углы мы просто добираем попиксельным смещением всего итогового изображения, получая в итоге вполне плавную камеру, пусть и с грубыми полосками самих стен (слишком грубыми, чтобы имело смысл делать хотя бы какие‑то модификаторы геометрии, так что двери, видимо, будут силовыми полями, вырубающимися разом во всём объёме блока).

Лавина стронулась.

128 углов — это очень, очень мало. Для них можно иметь таблицы практически всех данных, требующихся для их каста. Не только дельты самого луча — нееет, даже необходимые для «выхода на стартовую позицию» и для получения конечной точки страшные тангенсы и котангенсы. Да что там их — результаты умножения на них, собственно, остаточных дистанций! В смысле, всех возможных остаточных дистанций. И всё это — строго восьмибитное. Не верится? А оно именно так, потому что все остаточные дистанции всегда меньше 256, ибо сама клетка у нас 256×256 (можно и меньше, но лучше работать с байтами, которые не приходится потом «обкусывать»). В смысле, не только те дистанции, которые у нас имеются в явном виде, но и те, которые мы хотим вычислить через тангенсы и котангенсы (т. е. задача типа «на сколько пикселов у нас ушёл луч по координате шага, если по координате смещения он попал в стенку через N пикселов»). Это делает таблицу простой, лёгкой, восьмибитной и безбожно неоднородной — произведений на малые тангенсы у нас куча, а большие тангенсы быстро достигают результата умножения в 256 и более, что означает, что дальше можно не продолжать: «в естественной среде обитания» невозможны ситуации, когда этот тангенс пришлось бы умножать на такую величину.

Естественно, с ветвлениями у нас на 6502 всё тоже весьма кисло. Поэтому делать общий алгоритм шага, где на каждом шаге выбирается направление, остатки и так далее — не наш выбор. Да и данных получается ощутимо больше (хотя бы тот же знак шага, ага). Хотим загнать всё в один байт? Хотим! Что мы для этого делаем? Разбираем все случаи шага отдельно!

Случаев у нас не так уж и много — ровно 8. Сначала, по старой памяти, мы разбиваем их на две большие группы — шаг по X, смещение по Y и шаг по Y, смещение по X. Потом каждую из групп разбиваем на две — положительный шаг и отрицательный шаг. И каждую из них, как вы все уже догадались, по знаку смещения разбиваем ещё на две. Теперь у нас есть отличная возможность сделать каждый вариант идеально прямолинейным, быстрым и без лишних ветвлений!

Фиксированный угол — значит, известный заранее тангенс. Да они ещё и повторяются! Отражаются зеркально по углам, переносятся переносом... у нас на 128 углов нужно всего 32 тангенса! Котангенсы тоже получаются из них же, соответствующим отражением. То есть для каждого луча надо таблично задать его дельты, его тип (из 8 возможных случаев) и два указателя. Один — на начало «таблицы умножения» всех возможных вариантов на тангенс этого луча. Второй — на котангенс (заработавшись вконец, я в ходе написания кода называл мысленно про себя их «нормальный и котячий»). Чтобы понять, на сколько надо сдвинуться по одной координате, чтобы вторая прошла заданный остаток до ровной целой клетки — просто одним действием берём из таблицы готовый ответ. Ничего не проверяем и не переносим — переполнений и переносов не бывает. Никаких разметок наш набор таблиц умножения не требует — когда кончается один тангенс, начинается другой, так и лежат блобом. Лепота!

А что же у нас с самими шагами? У нас ведь 16 бит на координату, нет? Нет. Имея 8 бит на координату клетки в лабиринте и 8 бит на координату пиксела внутри клетки (можно и меньше, но зачем нам лишние операции по обрезанию бит?) мы, по сути, имеем две раздельные величины. И перенос из одной в другую бывает настолько редко, что нам не приходится вообще говорить о 16-битной арифметике! А самое смешное — это то, что координаты клеток мы можем положить рядом друг с другом так, чтобы они образовывали общий 16-битный указатель. Именно X и Y клеток, а не X клетки и X пиксела (и аналогично с Y)! Обратившись по этому указателю одним‑единственным LDA в соответствующем режиме адресации, мы сразу получаем информацию о том, нащупали мы стенку или нет. Нам даже адрес в массиве с картой не надо вычислять! После того, как вычислен очередной шаг, адрес уже сам образовался в памяти. Мы, по сути, отдаём всё адресное пространство под карту, просто следим за тем, чтобы игрок не выполз за пределы настоящей карты и не начал гулять среди, скажем, исполнимого кода (хотя при определённых условиях это могло бы выглядеть прикольно).

Короче говоря, прямая выгода от «шестерёнки Дока» — отказ от расчёта лучей через произведение матриц для плоского экрана — оказалась примерно нулевой на фоне косвенной выгоды, вызванной резким «усыханием» числа возможных вариантов луча. Не потому, что первая мала — потому, что вторая колоссальна.

Ну и, наконец, босс, победа над которым и стала тем моментом, когда я могу считать, что движок уже действительно написан (пусть он и не портирован на настоящий 6502-й, а существует пока только в виде всё того же испытательно‑рабочего кода). Гнусный, максимально нелинейный, вредительски меняющий свой характер нелинейности в зависимости от второй координаты, требующий полноценных 16 бит обратный корень. И Кармак с Абрашем тут нам не помощники — за любое разумное время получить сумму произведений расстояний в виде float нам не светит, а их хак базируется именно на том факте, что float при прочтении его как int является грубым логарифмом самого числа по основанию 2, то есть перевод в формат float для этого неизбежен.

Я не буду перечислять все ракеты, которые разбились об этого кибердемона, скажу только, что его одолело. Я разбил весь набор всех возможных входных значений на множество участков, на которых выходное значение реально было аппроксимировать линейно, в смысле плоскостью. Подстава тут в том, что шаг этих значений, мягко говоря, неоднородный! Фактически — логарифмический. Причём сами «лоскутки» слишком малы, чтобы можно было просто взять номер того разряда, где нам встретилась старшая единица. Точности не хватает, линейность успевает нарушиться и в результате стенки «идут зигзагом», а глаз видит поворот за угол там, где его на самом деле нет — сами понимаете, для шутера это категорически недопустимо. Это привело меня к такому шедевру обсценной лексики и маргинальной математики, как «собачий логарифм» (ну раз был «котячий тангенс», то сами понимаете...) На самом деле я вообще ни разу не сомневаюсь, что его раз двести уже придумали до меня — но я не знаю, как его называли все эти разы. То же самое, кстати, касается и «Бреза» в целом — везде на буржуйских форумах пишут «ну сделай рейкаст через DDA или Брез», но никто никогда не писал подробно, как именно это делается. То есть все выкладки, изложенные мной в прошлой статье, все проделывают самостоятельно каждый для себя, с нуля :)

«Собачий логарифм» — это, по сути, хэш, значение которого зависит от двоичного логарифма входного значения, плюс‑минус. Он равен номеру разряда старшей единицы, умноженному на два, и плюс ещё один, если в следующем разряде тоже есть единица. Исключая только первый разряд: для величин от 0 до 3 «собачий логарифм» равен самому числу. То есть как‑то так: 0 даёт значение сцукорифма в 0, 1 — в 1, 2 (10) — в 2, 3 (11) — в 3 же (для 2 и 3, как нетрудно видеть, определения «равен самому числу» и «равен номеру разряда со старшей единицей плюс бла бла» тождественны), 4 и 5 (то есть 10x) — в 4, 6 и 7 (11x) — в 5, ну и так далее до 11xxxxxx xxxxxxxx, для которых «собачий логарифм» равен 31. Как легко видеть, его можно довольно быстро подсчитать, даже имея 6502-й с его сдвигами всего на один бит за раз.

Если хотя бы одна из координат имеет «собачий логарифм» от разницы между положением камеры и точкой пересечения луча со стенкой более, чем 27 — дальше можно ничего не считать. С такого расстояния стенка при всех вариантах углов будет иметь однопиксельную высоту. А вот если она поближе — мы снова лезем в таблицу! В идеале её бы стоило сделать треугольной, но такой удобной возможности с указателями, как была в случае тангенсов, в этот раз нет, а специально добавлять ветвления — противоречит концепции максимальной скорости работы. В таблице у нас лежат коэффициенты для выражения H = B - X*M - Y*N, то есть нашего линейного представления обратного корня от суммы квадратов. H — высота текстуры, B — константа, а M и N — коэффициенты при X и Y, которые у нас, очевидно, хранят расстояния от камеры до пересечения луча со стенкой, по X и Y. Даже в квадраты возводить их не пришлось! Но есть незадача: умножить их на произвольные M и N мы тоже быстро не можем. Поэтому я использовал представление этих коэффициентов в виде суммы (X>>m1)+(X>>m2), то есть, по сути, X умножается на двоичную величину, состоящую из почти одних нулей и максимум двух единиц (а то и вообще одной единицы, если m1 = m2 — очевидно, это грубо эквивалентно X>>(m-1), потому что мы сначала сдвинули на m бит вправо, а потом сложили само с собой, разница получается не более чем в младшем бите). Ну, и с Y и его N — то же самое. То есть от полноценного умножения на каждый бит по очереди мы снова увернулись, сведя его к нескольким смещениям и сумме.

Код, который должен отработать один раз «за когда‑нибудь» — не должен быть слишком хорош. Усилия, потраченные на его написание, разумнее было бы потратить на написание того кода, который будет работать у юзера, причём постоянно. Увы, обычно всё бывает совершенно наоборот — код «для внутреннего употребления» получается быстрый, потому что «проект сдавать ещё вчера», менеджеры орут и общая атмосфера скандала и истерики, а вот у юзера мигание курсора половину проца сжирает, потому что «да кому оно нужно, железо сейчас у всех мощное». К счастью, развлекательного программирования это не касается — воображаемого менеджера можно в своём воображении с наслаждением отпинать коваными сапогами, а код написать сообразно принципам здравого смысла. Поэтому я использовал метод научного тыка и последовательного перебора (плюс‑минус, без лишних повторений уже пройденных вариантов) и запустил его генерировать таблицу с минимумом итоговой ошибки.

Код справился за неделю работы 24/7. Только очень дальновидный менеджер смог бы вписать такие задержки в график разработки, дабы всё было заранее запущено и считалось, не отвлекая людей от следующих этапов.

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

Рис. 3. Предполагается, что столбцы можно делать любой высоты — просто добавив специальные тайлы высотой от 1 до 7 пикселов сверху и снизу. А ещё можно сделать их скошенными — мы ведь знаем угол, под которым поймали стенку!
Рис. 3. Предполагается, что столбцы можно делать любой высоты — просто добавив специальные тайлы высотой от 1 до 7 пикселов сверху и снизу. А ещё можно сделать их скошенными — мы ведь знаем угол, под которым поймали стенку!

Да, вам не показалось — тайловую графическую подсистему в результате действительно можно использовать как трёхмерный графический ускоритель, и она действительно будет ускорять. 32 столбца кастуем, 256 она рисует (как уж умеет) — чем не ускорение?

Давайте теперь разберём уже практически написанный движок, а вы попробуете «на глаз» посчитать в нём такты 6502-го, на отдельные операции которого код уже фактически разбит (что делает портирование движка в реальные 6502-е машкоды довольно тривиальной задачей). Уверен, их удалось сделать достаточно мало, чтобы «глаз» справился с достаточной точностью!

VGA‑шные блиттеры и клонированные из «Вольфа» клиппинги я разбирать не буду как не относящиеся к теме, а перейду сразу к следующей (по коду) функции — к «собачьему логарифму».

inline unsigned short DogLg (unsigned short Value)	//Собачий логарифм
{
	unsigned short Mask = 0x8000;
	unsigned char DogLg;
	for (DogLg = 30; DogLg >= 4; DogLg -= 2)
	{
		if (Value & Mask)
			if (Value & (Mask>>1))
				return DogLg+1;
			else return DogLg;
		Mask >>= 1;
	}
	return Value;	//В области линейности (меньше 4) собачий логарифм равен самому числу.
}

Смысл его мы уже разбирали, ну а тут видно, насколько у него нехитрый код — однако, при портировании на 6502 будет уже не так весело, потому что оба байта разности придётся обрабатывать по очереди, да ещё с довольно мерзким пограничным случаем, когда единичку нашли в младшем бите старшего байта, а следующую надо проверять в старшем бите младшего байта. Хорошо, что это всё рассчитывается один раз на колонку, а не на каждый шаг рейкаста!

unsigned short FastScale (unsigned short X, unsigned short Y)
{
	unsigned char Lg2X, Lg2Y;
	unsigned short Val;

	Lg2X=DogLg(X);
	Lg2Y=DogLg(Y);
	if (Lg2X>27||Lg2Y>27) return 1;	//Однопиксельные варианты к рассмотрению не принимаются :)

	if (LinAppr[Lg2Y][Lg2X].ShX > 0)
	{
		if (LinAppr[Lg2Y][Lg2X].ShY>0)
			Val = LinAppr[Lg2Y][Lg2X].Base - (X>>LinAppr[Lg2Y][Lg2X].ShX) - (Y>>LinAppr[Lg2Y][Lg2X].ShY);
		else    Val = LinAppr[Lg2Y][Lg2X].Base - (X>>LinAppr[Lg2Y][Lg2X].ShX) - (Y<<(-LinAppr[Lg2Y][Lg2X].ShY));
	} else {
		if (LinAppr[Lg2Y][Lg2X].ShY>0)
			Val = LinAppr[Lg2Y][Lg2X].Base - (X<<(-LinAppr[Lg2Y][Lg2X].ShX)) - (Y>>LinAppr[Lg2Y][Lg2X].ShY);
		else 	Val = LinAppr[Lg2Y][Lg2X].Base - (X<<(-LinAppr[Lg2Y][Lg2X].ShX)) - (Y<<(-LinAppr[Lg2Y][Lg2X].ShY));
	}

	if (LinAppr[Lg2Y][Lg2X].ShX2 > 0)
	{
		if (LinAppr[Lg2Y][Lg2X].ShY2>0)
			return Val - (X>>LinAppr[Lg2Y][Lg2X].ShX2) - (Y>>LinAppr[Lg2Y][Lg2X].ShY2);
		else    return Val - (X>>LinAppr[Lg2Y][Lg2X].ShX2) - (Y<<(-LinAppr[Lg2Y][Lg2X].ShY2));
	} else {
		if (LinAppr[Lg2Y][Lg2X].ShY2>0)
			return Val - (X<<(-LinAppr[Lg2Y][Lg2X].ShX2)) - (Y>>LinAppr[Lg2Y][Lg2X].ShY2);
		else 	return Val - (X<<(-LinAppr[Lg2Y][Lg2X].ShX2)) - (Y<<(-LinAppr[Lg2Y][Lg2X].ShY2));
	}
}

А вот и его «клиент»: быстрое вычисление обратного корня вместе со всеми квадратами координат, оптом. Имеет три случая: «слишком далеко» (сразу выдаём высоту в 1 пиксел), «слишком близко» (таблица там не заполнена, потому что этот случай никогда не должен «выстреливать») и «в самый раз». Структура кода выглядит несколько всра... сомнительно, потому что код писался копипастой и перебором вариантов. Но, в принципе, позволяет понять, что Base — это константа, из которой вычитается расстояние по X и по Y. Сначала — «умноженное» на первую единичку коэффициента, затем — на вторую. Чем больше расстояние, тем меньше с этого расстояния выглядит текстура, я просто «поймал» участки, где эту квадратишно‑коренастую зависимость можно выразить линейно, при помощи простого коэффициента. Теперь тут явно предстоит поломать голову над тем, как минимизировать число сдвигов! Ведь в разных случаях явно получаются разные варианты экономии, когда в процессе получения одного слагаемого второе слагаемое тоже получается «по ходу дела». Если бы ещё сдвиг не был в разные стороны, в зависимости от порядка дистанции...

struct
{
	unsigned short ColumnHeight;
	unsigned char IsMOb, TextureNum;
	signed char Angle;
	
} NanoZBuffer[1];	//A single wall.

А вот и наш крошка‑ЗБуфер. Из одной позиции — то есть на одну стенку и на ноль мобов. В принципе, можно сделать на двух‑трёх (за ними всё равно не будет видно следующего) — как я предлагал в камментах, чтобы не обеднять геймплей, можно сделать фиксированные спрайты «вид с лица, уменьшенное разрешение» всем мобам, кроме ближайшего (которого чертим полностью, по полигонам). Чтобы можно было просто понять краем глаза, что там за спиной ближайшего гада ещё какая‑то подтанцовка мерзких подсуетюнчиков норовит своё свинцовое мнение вставить в наш конструктивный диалог. Но я тут не настаиваю от слова «совсем» — мобы и полигоны целиком развлечение Дока, я развлекаюсь со стенками и рейкастами и портить ему удовольствие не хочу :)

Высоту колонки я задал в меру адекватности своих VGA‑шных тестов, через U16. IsMOb на данной стадии явно по жизни ноль, номер текстуры сейчас пока тупо сводится к её цвету (хотя кое‑какие варианты плоских тайловых текстур, не ломающие ощущение «взгляда под углом», я таки потестировал), ну и угол — это закладка на будущее, потому что для того, чтобы чертить сверху и снизу скошенные торцы тайловых колонок, нужна (не благодарите, ваш Кэп) инфа о том, под каким именно углом мы видим эту колонку.

int AddToZBuffer8Bit (	unsigned char FactorIndex,
			unsigned char CameraXCell, unsigned char CameraXPixel, unsigned char CameraYCell, unsigned char CameraYPixel,
			unsigned char XCell, unsigned char XPixel, unsigned char YCell, unsigned char YPixel,
			unsigned char Type, unsigned char AngleIndex)
{
	signed long DistX, DistY;
	
	//if (Type<128)	{ProcessMObMarker(Type)} else {

	DistX = (signed long)XCell*256+(signed long)XPixel, DistY = (signed long)YCell*256+(signed long)YPixel;
	DistX -= (signed long)CameraXCell*256+(signed long)CameraXPixel, DistY -= (signed long)CameraYCell*256+(signed long)CameraYPixel;

	NanoZBuffer[0].ColumnHeight = FastScale (abs(DistX), abs(DistY));
	NanoZBuffer[0].ColumnHeight = (float)FactorGPU[FactorIndex] * (float)NanoZBuffer[0].ColumnHeight / 32767.0;	//Оставлю тут тупо умножение -- у Дока своя таблица умножения вроде есть для таких случаев.

	NanoZBuffer[0].IsMOb = 0;
	NanoZBuffer[0].TextureNum = Type;	//We have colors instead of textures here.

	NanoZBuffer[0].Angle=AngleIndex-64;
if (AngleIndex == UNSUPPORTED_WIP_TODO) NanoZBuffer[0].Angle = -128;

	//return 0;}

	return 1;
}

А вот и она, область стыка пекашных тестов и кода, писавшегося под 6502, чуть ли не на грани эмуляции последнего. В силу этой двойственности — не самое приятное место кода, но и не совсем мерзкое. Парочки Cell‑Pixel — в сущности, просто 16-битные координаты. X камеры, Y камеры, аналогичные парочки для точки попадания луча в текстуру... не всё из этого, конечно, придётся передавать в функцию. Всё равно эти данные в общей области памяти лежат, функция сама их может из глобалов взять (ну то есть из квази‑регистров 6502-го, располагающихся в младшей странице). Просто иногда ей придётся передать, что именно ей надо брать.

Не обращайте внимание на слово long — это результат каких‑то промежуточных действий с пекашными тестами. Эта функция — вотчина непуганой 16-битной арифметики, и таки да, тут придётся честно реализовывать модуль разности двух 16-битных (спасибо хоть модуль!) Но, опять же, это всё вычисляется один раз, после окончания каста луча (ну или два, если был моб) — поэтому тут это не так страшно, как аналогичные действия при каждом шаге. Возможно, карту можно сделать не как у меня (32×256, байт X‑координаты клетки используется полностью), а ближе к Wolf3D, что‑то типа 64×64. Это позволит использовать допущения о максимальной величине разности и где‑то когда‑то игнорировать какие‑то старшие биты. Но пока я в этом не вижу возможности что‑то реально выиграть, как не увидел её и в микро‑чанках Дока — всё то же самое можно получить и без жертв.

Закомментированные сравнения со 128 — это, конечно, проверка, является данная клетка стенкой (блоком) или же той ссылкой на моба, которую на первой картинке мы могли видеть как четыре жёлтых смежных клеточки. За неимением мобов пока это ветвление пустует, но я вижу его себе как приблизительное заполнение области 2×2 «временными стенками», сложенными из дендячьих спрайтов, не тайлов, но расположенных почти по той же строгой сетке. На них нарисован полигональный моб в том виде, как он бы проецировался на эти прозрачные стенки — но, слегка их двигая, мы можем чуть накладывать их друг на друга и чуть масштабировать, не перерисовывая всё целиком. А зачем же привязывать их к сетке лабиринта? А чтобы не вычёркивать из спрайтов области, где стенка закрыла часть моба! Если они более‑менее соответствуют общей сетке — мы просто рисуем моба на спрайтах, а мой 2.5D рейкастер дальше делает всё сам, потому что если перед мобом есть стенка, то луч просто заканчивается в ней и соответствующие «жёлтые ссылки» на моба «не выстреливают», соответствующие спрайты не рисуются, а рисуется просто стенка. Итого перерисовка полигонов может делаться, скажем, раз в четыре кадра — а рейкастер фигачит полный FPS. А что до того, что «приблизительно привязаны» к сетке не означает «точно привязаны» и на пару пикселов моб будет иногда наползать своим краем на ту стенку, которая, по идее, находится перед ним — ну вы это ещё попробуйте заметить за один кадр, при 60 FPS, посреди жёсткого заруба, уворачиваясь от ракет отчаянными стрейфами! Что же до второстепенных мобов‑«подтанцовки» — их и вовсе можно нарисовать на точно привязанных стенках, строгое положение им не столь и важно...

Ну и, напоследок, те самые углы, которые задают скосы верха и низа колонки. Пока что я на(ш)кодил их только для положительных шагов и смещений, ибо там они элементарны, а для остальных — надо на бумажке рисовать и думать, а мне не терпится поделиться тем фактом, что последний бастион пал и обратный квадратный корень более над нами не властен.

DrawColumnOfGPUTiles я комментировать не буду — это не самый быстрый, а хорошо если не самый медленный скалер на свете. Его задача — показать, что дендевская графическая подсистема нарисует всё правильно, и не более. Там я перебираю сейчас варианты текстур, чтобы не заливать всё одним цветом — но поскольку это не дописано толком, на анимированной гифке этого нет, я отключил эту часть при записи. Сейчас, конечно, она включена — можете побегать, если есть где запустить DOS16 без DOSBox (или как‑то выкрутив DOSBox на полную скорость, возможно, это поможет). В части поля зрения текстуры будут (какие уж будут). Пропустив также инициализацию карты с лабиринтом, VGA, пекашной мышки и «регистров 6502», до которых пока дело не дошло и переменные используются в совершенно другом качестве, я перейду сразу к делу.

//Сейчас пусть будут дублироваться, для отладки так проще. Потом закомментирую старые пекашные переменные и оставлю только 6502-е. Если мы не перейдём к немедленному портированию, конечно.
	unsigned short PlayerX=256*1.5, PlayerY=256*1.5;	//inside the Cell 1x1
	signed short PlayerA=0;		//Angle+-32768 = +-180 deg.

	//Здесь мы распределим первые 256 байт (квази-регистры) под самые "горячие" переменные.
	#define PLAYER_X_CELL	0x80	//Начнём со 128, думаю, остального хватит по уши :) Этот -- координата игрока X, старший байт (номер клетки в лабиринте)
	#define PLAYER_X_PIXEL	0x81	//Это -- младший (номер пиксела в клетке)
	#define PLAYER_Y_CELL	0x82	//Старший по Y
	#define PLAYER_Y_PIXEL	0x83	//Младший по Y
	#define PLAYER_A_RAY	0x84	//Старший байт угла игрока, указывает угол с точностью до кастуемого луча (используется 7 бит, потому что полный оборот = 128, а поле зрения = 32, по числу колонок экрана).
	#define PLAYER_A_PIXEL	0x85	//Младший байт угла игрока, указывает остаток в пикселах от 0 до 7 (т. е. три бита), чтобы аппаратно довернуть всю сцену уже в тайлах.

//	signed short BrezStep, BrezShift, BrezToStart, BrezToFinish;

	//BrezStep не используем, вместо этого делаем не две ветви, а четыре (каждую ветвь для горизонтального/вертикального шага делим на ветви для положительного/отрицательного).
	#define BREZ_SHIFT	0x86	//Их тоже делим пополам (положительное и отрицательное смещение), итого -- 8 кейсов, каждый заточен под свой случай и решает его максимально быстро.
	#define BREZ_TOST	0x87
	#define BREZ_TOFIN	0x88	//...и многое другое, которое сюда придётся вписать по ходу дела, потому что промежуточных переменных будет много, а регистров -- только три.

//	unsigned short BrezPos, BrezPrev, BrezCell;

	//Вот эта парочка должна идти подряд! Они вместе образуют в квазирегистровой области памяти полный 16-битный адрес, по которому мы можем проверить наличие стенки при рейкасте всего одной командой LDA!
	//Никаких суммирований с началом карты в памяти, никаких сложений, копирований, переносов -- после любого смещения и/или шага правильный адрес уже там. Быстрее КМК просто нереально :)
	//Но! Поскольку, в зависимости от того, шагаем мы по Y или по X, в этих переменных будет лежать XY или YX -- при ассемблировании в половине случаев их придётся взаимно переименовать, чтобы всегда Y был в старшем
	//байте, а X -- в младшем. Это чисто синтаксическое переименование, и оно, в зависимости от выбранного тут порядка следования, будет или в case 0..3, или в case 4..7. Оставим это на "сладкое" (на момент фактического перевода на ассемблер 6502).
	#define BREZ_CELL	0x89	//тут нам хватит 8 бит, мы сразу избавляемся от остатка "до края следующей ячейки" и дальше шагаем сугубо по ячейке.
	#define BREZ_POSCELL	0x8A	//а вот смещение состоит из 2 байт. Этот -- номер ячейки.

	#define BREZ_POSPIX	0x8B	//Этот -- номер пиксела.
	#define BREZ_PREVPIX	0x8C	//Тут хранится только номер пиксела, потому что номер ячейки для вычислений остатка шага не важен.
	
	#define COLNUM		0x8D	//Ну и эту штуку заодно, раз уж пошло такое дело... хотя она вообще не принципиальная, пока не началось реальное ассемблирование.
	
	unsigned char WallNum_IF_Temp;	//The value is assigned inside the comparsion. You've been warned.

	signed short DX=1024, DY=0;	//cos and sin of PlayerA X 1024.
	signed short A_0_180;	//В рейкасте больше не участвует, только в окружающем его пекашном отладочном коде.

Тут мне нечего добавить, я и в статью‑то это просто скопипастил для полноты картины, чтобы не приходилось сверяться с сырцами. Всё уже прокомментировано «до упора». Просто привыкайте к тому, что Memory6502[0][бла‑бла] — это просто такая переменная бла‑бла, лежащая в квазирегистровой памяти. У нас дальше почти все переменные будут писаться таким вот диким образом, а сейчас мы переходим к той части, которая не является движком — это инициализация таблиц, делающаяся пока «на лету» (они не требуют недели для расчёта, хотя качественно ничем не отличаются от той таблицы и тоже должны стать в итоге чем‑то типа «= {бла, бла, бла};», как им стала та).

	unsigned short RayIndex;

	static struct
	{
		unsigned char Case;		//Который из 8 случаев, в смысле комбинации соотношения модулей и знаков.
		unsigned char BrezShift;	//Заранее посчитанное смещение (шаг-то понятно, что 256 без вариантов)
		unsigned char *tgPtr, *ctgPtr;	//Таблицы умножения величин от 0 до 255 на тангенс и котангенс данного угла, без учёта знака. Таблица обрывается ранее, чем результат умножения достигнет 256.
	} Swamp_Dok_Angle_Wheel_Fixed_Tile[128];	//Убрал на фиг запас по разрешению, ну и углы для косо срезанных тайлов тоже пока выкинул -- не до них.
	#define TOTAL_TG 5910	//Экспериментально подсчитал :)
	static unsigned char MulByTg[TOTAL_TG], *tgPtr[32];	//MulByTg стопудово надо будет класть внутри Memory6502 и "указатели" делать уже сугубо 6502-е.

Да, когда‑то было больше 128 лучей, но я от этого отказался — мы их всё равно не увидим при разрешении 32×30 тайлов :) Что же касается последнего каммента, то я что‑то как‑то сомневаюсь, что это всё вообще будет делаться — скорее всего, сразу перейдём к ассемблированию под 6502-й (ну то есть Док ассемблирует, а я подсказываю, что как работает, если это не буквально‑очевидным образом следует из моего кода и/или этой статьи). Но, возможно, всё‑таки я углублюсь в свою недо‑эмуляцию — Док предполагал, что, может быть, захочет сначала написать статью про свои подходы к движку. Или не захочет. Чёрт, да тут даже про себя не всегда можешь точно сказать — захочешь или не захочешь! Может, вштырит написать, а может, потеряешь интерес. Если вы читаете это — значит, вштырило и я не просто сел покодить, а дописал до конца, включая статью. Тут как с @bodyawm— я могу надеяться на то, что ему в какой‑то момент будет интересно посмотреть что‑то из моего железа и я с этого поимею какой‑то профит в виде ремонта, тюнинга или хотя бы занимательного разбора в статье, но не более, чем надеяться. Неисповедимы пути вштыра. Может, сейчас, может, завтра, может, никогда. Если кому-то скучно — можете пройтись по моим публикациям, там много недоделанного, что-то из этого может вштырить и лично тебя, дорогой читатель!

tgPtr — временное хранилище указателей на разные таблицы умножения. В отличие от остальных таблиц, нужно только на стадии генерации окончательных, как и RayIndex. Остальное — вечные ценности, которые в итоге идут в ПЗУ картриджа.

	for (i=-64,RayIndex=0; i<64; i++,RayIndex++)	//Эту таблицу посчитали и в ROM положили. На 6502 этого кода вообще не будет, только его выхлоп.
	{
		signed short BrezShift;	//Чисто временная переменная теперь, для расчётов на пека готовых таблиц.
		
		double RayDX = cos((i+.5)*PI/64.0);	//Они больше не входят в таблицу и будут вычисляться только временно, на пека, для заполнения этой таблицы.
		double RayDY = sin((i+.5)*PI/64.0);

		//Тут мы заранее заполним все возможные случаи из 8. То есть все сочетания знаков шага/смещения и соотношения модулей (кто из них шаг, а кто -- смещение).
		//Ветвиться внутри каста -- смерть, потому что ветвление у 6502-го очень унылое. А вот switch{case} у него аппаратный и по всей памяти. Его и используем :)
//		if (abs(Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].RayDX) > abs(Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].RayDY))
		if (fabs(RayDX) > fabs(RayDY))
		{	//Главное деление -- кто шаг, а кто смещение.
			Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].Case = 0;	//Шаг по X

//			if (Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].RayDX < 0)	//Шаг X, отрицательный
			if (RayDX < 0)	//Шаг X, отрицательный
			{	//Знак шага. Позволяет вовсе BrezStep выкинуть.
				Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].Case += 2;
				BrezShift = -256.0*sin((i+.5)*PI/64.0)/cos((i+.5)*PI/64.0);
			} else {							//Шаг X, положительный
				BrezShift = 256.0*sin((i+.5)*PI/64.0)/cos((i+.5)*PI/64.0);
			}

		} else {
			Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].Case = 4;	//Шаг по Y

//			if (Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].RayDY < 0)	//Шаг Y, отрицательный
			if (RayDY < 0)	//Шаг Y, отрицательный
			{	//Знак шага. Позволяет вовсе BrezStep выкинуть.
				Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].Case += 2;
				BrezShift = -256.0*cos((i+.5)*PI/64.0)/sin((i+.5)*PI/64.0);
			} else {							//Шаг Y, положительный
				BrezShift = 256.0*cos((i+.5)*PI/64.0)/sin((i+.5)*PI/64.0);
			}

		}
		if (BrezShift < 0) Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].Case++;
		BrezShift = abs(BrezShift);
		if (BrezShift>255) BrezShift=255;
		Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].BrezShift = BrezShift;
	}

Первая и главная вечная ценность — «шестерёнка Дока», которая стронула всё с мёртвой точки. Угол рассыпался на старшие биты, которые мы реально рейкастим, и младшие биты, которые мы добираем смещением всей тайловой сцены, координаты рассыпались на старшие байты, которые сразу задают не то что клетку на карте — готовый 16-битный адрес этой клетки, и младшие байты, из которых не всегда и инкремент‑то прилетает, а полноценных переносов не бывает вовсе, общие случаи шага и смещения рассыпались на четыре штуки каждый, итого 8 уникальных вариантов, а общие случаи синуса и косинуса рассыпались на 32 готовых тангенса, повторяющихся в разных вариациях в виде 128 тангенсов и 128 же котангенсов.

	for (i=-16; i<16; i++)	//only 32 columns per screen
	{
		FactorGPU[i+16] = 32768 / cos((i+.5)*PI/64.0);
	}
	for (i=0,RayIndex=0; i<32; i++)	//Ну вот и вишенка на торте -- тангенсы считаем :)
	{
		double TG = sin((i+.5)*PI/64.0) / cos((i+.5)*PI/64.0);

		tgPtr[i] = MulByTg+RayIndex;
		for (long i=0; i<256; i++) if (i*TG < 256.0)
		{
			if (RayIndex >= TOTAL_TG) return;	//Алярм, упячка, голактеко опасносте, произошло невозможное, пыщ-пыщ! Выходим из main()
			MulByTg[RayIndex] = i*TG;
			RayIndex++;
		}
	}

Таблица поправок на того самого Пифагора (дистанции от камеры до плоского экрана, с определённой долей условности сопоставленного экрану‑барабану). Да, «где густо, где пусто», но это лучше, чем ничего. Если и этого не делать — нам потребуется экран, обёрнутый вокруг ЩАЧЛА игрока так, чтобы закрыть 90 градусов его поля зрения и вдобавок находиться на постоянном расстоянии от глаз. На нём, возможно (но это не точно), выпученная в середине фишайная стенка будет выглядеть естественно. На обычном экране — ни в коем случае. Впрочем, по анигифке уже видно, что при отсутствии реальных текстур получилось не просто «лучше, чем ничего» — получилось отлично. Главный недостаток «экрана‑барабана» поди ещё заметь!

Дальше мы считаем тангенсы (наконец‑то). Пока просто считаем, для угла от 0 до 90, и для каждого заполняем «таблицу умножения». Временную, в виде tgPtr[i].

	for (i=-64,RayIndex=0; i<64; i++,RayIndex++)
	{
		if (i<-32)
		{
			Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].tgPtr = tgPtr[i+64];
			Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].ctgPtr = tgPtr[-33-i];
			continue;
		}
		if (i<0)
		{
			Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].tgPtr = tgPtr[-1-i];
			Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].ctgPtr = tgPtr[32+i];
			continue;
		}
		if (i<32)
		{
			Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].tgPtr = tgPtr[i];
			Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].ctgPtr = tgPtr[31-i];
			continue;
		}
		Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].tgPtr = tgPtr[63-i];
		Swamp_Dok_Angle_Wheel_Fixed_Tile[RayIndex].ctgPtr = tgPtr[i-32];		
	}

Ну и напоследок распределяем эти таблицы умножения на их постоянные места, отзеркаливая их по углам и знакам так, чтобы получить нужные тангенсы и котангенсы в «шестерёнке». С инициализацией покончено, все таблицы сгенерированы, кроме той, которая генерировалась неделю и после этого тупо задана в момент инициализации массива. Зачаточную физику в виде main loop и беготни я пропускаю вместе с клиппингом. Не о них речь, перепрыгиваю сразу в рейкаст.

		int WasFlag;	//Позволяет запомнить факт наличия флага и тем самым поддерживать в коде структуру "как в 6502-м ассемблере".
				//Когда код уже будет окончательно разбит на 6502-е команды, "идентичные натуральным", вместо этой переменной тоже будет флаг из регистра флагов.


		Memory6502[0][PLAYER_X_CELL] = PlayerX>>8;
		Memory6502[0][PLAYER_X_PIXEL] = PlayerX&0xFF;
		Memory6502[0][PLAYER_Y_CELL] = MAP_PAGE_6502 + (PlayerY>>8);	//А давайте смотреть на вещи проще! Пусть логически вся память будет картой, просто не будем героя выпускать за пределы области памяти, где реально карта лежит. Меньше придётся адресов прибавлять :)
		Memory6502[0][PLAYER_Y_PIXEL] = PlayerY&0xFF;
		Memory6502[0][PLAYER_A_RAY] = ((long)PlayerA+32768) >> 9;
		Memory6502[0][PLAYER_A_PIXEL] = (((long)PlayerA+32768) >> 6)&7;	//Ну, это тоже типа как часть отладки. Допустим, что по итогам игрового процесса эти переменные у нас поддерживаются актуальными, а вовсе не копируются каждый раз из сишного "отладочного обрамления".

Тут всё сказано комментариями. Переходим в перебор 32 лучей, по лучу на столбец экранных тайлов.

		for (Memory6502[0][COLNUM]=0; Memory6502[0][COLNUM]<32; Memory6502[0][COLNUM]++)	//Это тут пусть пока тоже побудет по-сишному, сначала перепишем сам каст (одного луча).
		{

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

			X6502 = (Memory6502[0][PLAYER_A_RAY] + 16 - Memory6502[0][COLNUM]  +  128)  %  128;	//Следы попытки начать разбивать на буквальные 6502-е команды. Типа, в этом месте мог быть ваш регистр X.

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

			switch (Swamp_Dok_Angle_Wheel_Fixed_Tile[X6502].Case)
			{
				case 0:

Разбираем первый кейс из 8 — шаг по X, все положительные.

					Memory6502[0][BREZ_POSCELL] = Memory6502[0][PLAYER_Y_CELL];
					Memory6502[0][BREZ_POSPIX] = Memory6502[0][PLAYER_Y_PIXEL];
					Memory6502[0][BREZ_CELL] = Memory6502[0][PLAYER_X_CELL];
					Memory6502[0][BREZ_SHIFT] = Swamp_Dok_Angle_Wheel_Fixed_Tile[X6502].BrezShift;

Как уже говорилось выше, при ассемблировании этого под реальный 6502 надо будет вернуться от наглядных BREZ_CELL, BREZ_POSPIX и BREZ_POSCELL, соответствующих первой моей статье, к BREZ_X_CELL, BREZ_X_PIX, BREZ_Y_CELL и BREZ_Y_PIX, используя их в качестве этих трёх сообразно тому, по какой координате у нас шаг, а по какой — смещение. Я этого делать не стал, чтобы не нарушать читаемости (всё‑таки соответствие первой публикации — фактор немаловажный!) Но для скорости работы нужно, чтобы BREZ_X_CELL и BREZ_Y_CELL лежали в квази‑регистрах подряд, образуя полный адрес без лишних телодвижений.

					//1st step. Must reach the very beginning of a nearest cell in the Step direction, unless a wall in Shift direction blocks our ray.
					Memory6502[0][BREZ_TOST] = 256 - Memory6502[0][PLAYER_X_PIXEL];
		
					Memory6502[0][BREZ_PREVPIX] = Memory6502[0][BREZ_POSPIX];

Выходим на стартовую позицию, всё как в прошлой статье писалось. Для этого надо посчитать, сколько надо шагнуть (тривиально) и на сколько сместиться (требует умножать на тангенс).

					WasFlag = 256&(Memory6502[0][BREZ_POSPIX]+Swamp_Dok_Angle_Wheel_Fixed_Tile[X6502].tgPtr[Memory6502[0][BREZ_TOST]]);	//Carry flag
					Memory6502[0][BREZ_POSPIX]+=Swamp_Dok_Angle_Wheel_Fixed_Tile[X6502].tgPtr[Memory6502[0][BREZ_TOST]];	//Разумеется, в настоящем 6502-м мы получим флаг сразу по итогу этой операции.

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

					if (WasFlag)
					{
						Memory6502[0][BREZ_POSCELL]++;
						//Если не было переноса по координате смещения -- то и нечего вообще проверять, мы всё в той же ячейке.
						if ((WallNum_IF_Temp=Memory6502[Memory6502[0][BREZ_POSCELL]][Memory6502[0][BREZ_CELL]])!=' ')	//Hit the Wall/MOb after first Shift
						{
							Memory6502[0][BREZ_TOFIN] = 256-Memory6502[0][BREZ_PREVPIX];
							if ( AddToZBuffer8Bit (Memory6502[0][COLNUM], Memory6502[0][PLAYER_X_CELL], Memory6502[0][PLAYER_X_PIXEL], Memory6502[0][PLAYER_Y_CELL], Memory6502[0][PLAYER_Y_PIXEL],
								Memory6502[0][PLAYER_X_CELL],Memory6502[0][PLAYER_X_PIXEL] + Swamp_Dok_Angle_Wheel_Fixed_Tile[X6502].ctgPtr[Memory6502[0][BREZ_TOFIN]], Memory6502[0][BREZ_POSCELL], 0, WallNum_IF_Temp-1,	//Вообще блок может иметь 4 разных цвета для разных граней. Ограничимся двумя -- обычным и декрементированным. Так же было и в Wolf3D, кстати!
																					 UNSUPPORTED_WIP_TODO)) goto HitOpaque;
						}
					}

Вместо WallNum_IF_Temp надо было, конечно, помянуть аккумулятор — пижонствовать, так уж до конца :) Ведь именно в нём окажется результат LDA по адресу из BREZ_X_CELL и BREZ_Y_CELL, которые появятся в финальном коде. И сразу мы проверим и этот if, и получим значение для последующего вызова AddToZBuffer8Bit. Но это значение там тоже не выживет долго, нам же ещё Memory6502[0][BREZ_TOFIN] = 256-Memory6502[0][BREZ_PREVPIX] и даже Memory6502[0][PLAYER_X_PIXEL] + Swamp_Dok_Angle_Wheel_Fixed_Tile[X6502].ctgPtr[Memory6502[0][BREZ_TOFIN]] считать перед вызовом, значит, аккумулятор мы затёрли и это значение где‑то ещё. Не, не будем пижонствовать зря :)

					Memory6502[0][BREZ_CELL]++;
					if ((WallNum_IF_Temp=Memory6502[Memory6502[0][BREZ_POSCELL]][Memory6502[0][BREZ_CELL]])!=' ')	//Hit the wall/MOb after getting to Step position, even before first Step
					{
						if ( AddToZBuffer8Bit (Memory6502[0][COLNUM], Memory6502[0][PLAYER_X_CELL], Memory6502[0][PLAYER_X_PIXEL], Memory6502[0][PLAYER_Y_CELL], Memory6502[0][PLAYER_Y_PIXEL],
							Memory6502[0][BREZ_CELL], 0, Memory6502[0][BREZ_POSCELL], Memory6502[0][BREZ_POSPIX], WallNum_IF_Temp, X6502)) goto HitOpaque;
					}

А вот мы инкрементировали шаг. В прошлой статье, где всё было буквальным и 16-битным, шаги «в плюс» имели нулевой младший байт, а «в минус» — 255, т. е. начинались с начала клетки карты. А здесь мы и так знаем, что где находится, потому что у нас 8 раздельных случаев! Мы и вовсе не храним информацию о младшем байте, а старший нуждается всего лишь в инкременте. Я ведь говорил, что такты процессора можно будет «на глазок» подсчитать! ;) Пальцев, конечно, не хватит, но, тем не менее... :)

					for (i=0;i<256;i++)
					{

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

						Memory6502[0][BREZ_PREVPIX] = Memory6502[0][BREZ_POSPIX];
						
						WasFlag = 256&(Memory6502[0][BREZ_POSPIX]+Memory6502[0][BREZ_SHIFT]); //Carry flag
						Memory6502[0][BREZ_POSPIX] += Memory6502[0][BREZ_SHIFT];
						if (WasFlag)
						{
							Memory6502[0][BREZ_POSCELL]++;
							//Аналогично, зачем лезть в память два раза в одно и то же место, если не было переноса :)
							if ((WallNum_IF_Temp=Memory6502[Memory6502[0][BREZ_POSCELL]][Memory6502[0][BREZ_CELL]])!=' ')	//Hit the wall/MOb after Shift
							{
								Memory6502[0][BREZ_TOFIN] = 256-Memory6502[0][BREZ_PREVPIX];
								if ( AddToZBuffer8Bit (Memory6502[0][COLNUM], Memory6502[0][PLAYER_X_CELL], Memory6502[0][PLAYER_X_PIXEL], Memory6502[0][PLAYER_Y_CELL], Memory6502[0][PLAYER_Y_PIXEL],
									Memory6502[0][BREZ_CELL], Swamp_Dok_Angle_Wheel_Fixed_Tile[X6502].ctgPtr[Memory6502[0][BREZ_TOFIN]], Memory6502[0][BREZ_POSCELL], 0, WallNum_IF_Temp-1, UNSUPPORTED_WIP_TODO )) goto HitOpaque;
							}
						}

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

						Memory6502[0][BREZ_CELL]++;
						if ((WallNum_IF_Temp=Memory6502[Memory6502[0][BREZ_POSCELL]][Memory6502[0][BREZ_CELL]])!=' ')	//Hit the wall/MOb after Step
						{
							if ( AddToZBuffer8Bit (Memory6502[0][COLNUM], Memory6502[0][PLAYER_X_CELL], Memory6502[0][PLAYER_X_PIXEL], Memory6502[0][PLAYER_Y_CELL], Memory6502[0][PLAYER_Y_PIXEL],
								Memory6502[0][BREZ_CELL], 0, Memory6502[0][BREZ_POSCELL], Memory6502[0][BREZ_POSPIX], WallNum_IF_Temp, X6502) ) goto HitOpaque;
						}

..а тут мы обсчитываем шаг, а пижонство достигает своего апогея — не спрашивайте меня, как до этого вызова доживёт значение X6502! Как я уже каялся, это пока просто переменная :) А её наличие тут — это как раз начало моих игрищ с текстурами, требующих знать угол, под которым луч впилился в стенку. Доку это тоже будет необходимо, если у него хватит тайлов на скошенные края сверху и снизу! Что, конечно, вовсе не факт — текстуры, которые я тут не показал даже на гифке, тоже много сожрут...

					}
				break;

…Переходим к остальным 7 вариантам, которые можно посмотреть в Аппендиксе на itch.io. Там же, но в основных архивах — полностью пекашная версия движка, для первой статьи. Можно легко сравнить, насколько Гермиона изменилась за лето :‑D

HitOpaque:		DrawColumnOfGPUTiles(Body256, Memory6502[0][COLNUM], Memory6502[0][PLAYER_A_PIXEL]);

Ну и этот друг собирает выхлоп из всех вариантов каста, подводит итоги всего, что нам встретилось в z‑буфере текущей колонки, и рисует «типа так будут выглядеть тайлы» (тоже по‑пекашному, сразу задавая позиции сообразно младшим битам угла камеры, хотя настоящая приставка будет учитывать их потом — когда вся сцена уже заполнена тайлами, а мы задаём смещение этой сцены по горизонтали. Отладка такая отладка!)

Кстати, а как будет «собачий логарифм» на поросячьей латыни? О_О

Теги:
Хабы:
+44
Комментарии2

Публикации

Ближайшие события