-- ===================================================================== -- Генератор d2_table.tbl (рейкастер DOOM2 / Sprinter Sp2000) -- 16-шаговая ближняя зона, формат-совместимый с Bin/d2_table.tbl. -- -- Запуск: -- sjasmplus --nologo Resources/tools/gen_d2_table/gen_d2_table.asm -- (из КОРНЯ проекта; эмулятор не нужен) -- -- Вход: нет. -- Выход: Build/d2_table.gen.tbl -- (Bin/d2_table.tbl — канонический рабочий ассет, не трогаем). -- -- Документация формата и математики: INFO/doom-энжин.md. -- -- ФОРМАТ (как в оригинале): -- * 524288 байт = 32 страницы по 16384 (PAGE). -- * Страницы парами CORNER=0..15: чётная = «offset», нечётная = «projection». -- * Каждая страница = 512 записей по 32 байта (REC). -- * Запись (32 б): -- EVEN: byte[0..15] = низкий байт адреса DDA-клетки в отн.карте -- TABLE_W (квадрант 16×16, страйд 1, base=#2000+QUAD*256); -- byte[16..31] = 0. -- ODD : byte[0..15] = код высоты (индекс в table_x.tbl); -- byte[16..31] = текстурная u-координата столбца стены. -- * Адресация записи в кадре: H ∈ #40..#7F (6 бит = крупный угол -- внутри квадранта), L_top3 ∈ {0,32,…,224} (3 бита = подгруппа -- колонок); вместе 64×8 = 512 = страница. За кадр движок проходит -- 40 H × 8 L = 320 экранных лучей (60°/90° квадранта). -- -- МАТЕМАТИКА (см. INFO/doom-энжин.md §«Закон проекции»): -- * supercover-DDA, tie tx<=ty -> X (как в оригинале). -- * перспектива по перпендикулярной дистанции: N = K / perp, K=315; -- perp = d_eucl * cos(ray_ang - view_ang) (fisheye-фикс). -- * height_code = index в table_x.tbl, грубо A = clamp((256-N)/2, 0..127). -- * u_col = round(u_fraction * 64) & 63. -- -- CORNER (0..15) кодирует субпозицию игрока в клетке (4×4). -- Запись (record_idx 0..511) кодирует «виртуальный экранный угол» -- внутри квадранта: ray_ang = (record_idx + 0.5)/512 * (pi/2). -- ===================================================================== local PAGES = 32 local PAGE = 16384 local REC = 32 local RECS = PAGE // REC -- 512 local STEPS = 16 local SUBPOS = 16 local GRID = 16 -- TABLE_W квадрант = 16×16 local K_PROJ = 315.0 local FOV_R = math.pi / 3.0 -- 60° local SCR_W = 320 local QUAD = math.pi / 2.0 -- 90° local OUT = "Build/d2_table.gen.tbl" -- Субпозиция CORNER=0..15 -> (sx, sy) в [0..1] (углы 1/8 от стенки). local function corner_xy(c) local sx = (c % 4) / 4.0 + 1.0 / 8.0 local sy = (c // 4) / 4.0 + 1.0 / 8.0 return sx, sy end -- supercover-DDA, 16 шагов. Возвращает массив { {cx,cy,d_eucl,u}, ... }. -- Ось +X = «вперёд» (вид игрока). cx,cy — клеточные координаты относит. -- стартовой клетки (в которой игрок). local function trace(sx, sy, ang) local dx = math.cos(ang); if math.abs(dx) < 1e-9 then dx = (dx < 0 and -1e-9 or 1e-9) end local dy = math.sin(ang); if math.abs(dy) < 1e-9 then dy = (dy < 0 and -1e-9 or 1e-9) end local x, y = sx, sy local cx, cy = math.floor(x), math.floor(y) local out = {} for _ = 1, STEPS do local nx = cx + (dx > 0 and 1 or 0) local ny = cy + (dy > 0 and 1 or 0) local tx = (nx - x) / dx local ty = (ny - y) / dy local u if tx <= ty then x = nx; y = y + dy * tx; cx = cx + (dx > 0 and 1 or -1) u = y - math.floor(y) else y = ny; x = x + dx * ty; cy = cy + (dy > 0 and 1 or -1) u = x - math.floor(x) end local de = math.sqrt((x - sx)^2 + (y - sy)^2) out[#out + 1] = { cx, cy, de, u } end return out end -- offset байта = (cy & (GRID-1)) * GRID + (cx & (GRID-1)) -- (вне квадранта 16×16 «оборачиваем» по 8-битной арифметике -- движка; реальный движок попадает в нулевые ячейки relmap). local function off_byte(cx, cy) local oc = cx % GRID local oy = cy % GRID if oc < 0 then oc = oc + GRID end if oy < 0 then oy = oy + GRID end return (oy * GRID + oc) & 0xFF end local function height_code(N) if N <= 256 then local A = (256 - N) // 2 if A < 0 then A = 0 end if A > 127 then A = 127 end return A end -- full-screen с клипом: A = 128 + 6-битный буфер-offset (как в оригинале). local over = (N - 256) // 8 if over > 63 then over = 63 end return 128 + over end local function ucol(uf) local v = math.floor(uf * 64.0 + 0.5) return v & 63 end -- Кадр движка читает 320 экранных лучей подряд из 512 записей. Поэтому -- виртуальный угол луча в кадре = view_ang + atan2(cam_x, halfproj), -- где cam_x шагает по 320. Но в таблице 512 записей покрывают 90° квадранта, -- так что одной записи соответствует ray_ang = ((idx + 0.5)/512) * 90° -- (без поправки fisheye-камеры — она применяется через perp ниже). local function ray_angle(record_idx) return (record_idx + 0.5) / RECS * QUAD end local function gen_pair(corner) local sx, sy = corner_xy(corner) local even = {}; for i = 1, PAGE do even[i] = 0 end local odd = {}; for i = 1, PAGE do odd[i] = 0 end -- view_ang = 0 (ось +X — вперёд); MAKE_MAP в движке поворачивает уровень -- под квадрант. Луч ray_ang отсчитывается от +X. for r = 0, RECS - 1 do local ang = ray_angle(r) local path = trace(sx, sy, ang) local base = r * REC for k = 1, STEPS do local cx, cy, de, u = path[k][1], path[k][2], path[k][3], path[k][4] -- EVEN: offset байт в relmap (TABLE_W) для шага k. even[base + k] = off_byte(cx, cy) -- ODD : код высоты + текстурная u. local perp = de * math.cos(ang - 0.0) -- view_ang=0 -> perp=de*cos(ang) if perp < 0.05 then perp = 0.05 end local N = K_PROJ / perp odd[base + k] = height_code(N) & 0xFF odd[base + STEPS + k] = ucol(u) end end return even, odd end local out = {} for i = 1, PAGES * PAGE do out[i] = 0 end for c = 0, SUBPOS - 1 do local even, odd = gen_pair(c) local eb = (c * 2) * PAGE local ob = (c * 2 + 1) * PAGE for i = 1, PAGE do out[eb + i] = even[i] out[ob + i] = odd[i] end end do local t = {} for i = 1, #out do t[i] = string.char(out[i] & 0xFF) end local w = io.open(OUT, "wb"); w:write(table.concat(t)); w:close() end print(string.format("[gen_d2_table] %s размер=%d б = %d страниц × %d б", OUT, PAGES * PAGE, PAGES, PAGE)) print(string.format("[gen_d2_table] CORNER=0..15, 16 шагов, %d записей × %d б на страницу", RECS, REC)) print("[gen_d2_table] NB: формат-совместим, но не бит-в-бит с Bin/d2_table.tbl") print("[gen_d2_table] (оригинальный fixed-point утерян; см. INFO/doom-энжин.md).")