SNES Mode 7: псевдо-3D аффинным преобразованием
1/z, которое в настоящем 3D считают на пиксель, здесь считают раз на строку.(Sx,Sy) железо считает обратным отображением, из какого текселя карты брать цвет. Сам по себе это плоский поворот/зум. Иллюзию «уходящей в горизонт трассы» даёт построчный хак: HDMA меняет матрицу на каждой строке развёртки, и каждая строка получает свой масштаб ∝ 1/p (p — пикселей ниже горизонта). «3D-объекты» — это обычные спрайты, приклеенные поверх. Дешёвая иллюзия, отлитая в кремнии.
Механизм: аффинная матрица и обратное отображение
Обычный фон рисуется тайл-в-тайл. Mode 7 вместо этого берёт квадрат карты 1024×1024 и натягивает его на экран произвольным аффинным преобразованием — поворот, масштаб, сдвиг, скос (всё, кроме изгиба: прямые остаются прямыми). Ключевой трюк железа — оно идёт не «куда лёг бы каждый тексель карты» (прямое отображение оставило бы дыры и наложения), а наоборот: проходит по экранным пикселям слева-направо, сверху-вниз и для каждого спрашивает «из какого текселя я?». Это backward mapping — ни дыр, ни перекрытий.
Для экранного пикселя (Sx,Sy) тексель (Tx,Ty) считается через матрицу [A B; C D] вокруг точки-центра (Mx,My):
(плюс скролл M7HOFS/M7VOFS, который сдвигает картинку после преобразования). Все коэффициенты — знаковый fixed-point 8.8: 8 бит целой части, 8 дробной, деление на 256. Регистры так и называются: M7A…M7D — матрица, M7X/M7Y — центр.
Геометрический смысл столбцов матрицы прямой: (A,C) — на сколько текселей шагнуть по карте, когда сдвигаешься на 1 пиксель вправо по экрану; (B,D) — шаг по карте на 1 пиксель вниз. Отсюда всё:
- Тождество
(1,0)(0,1)→ 1 пиксель = 1 тексель → обычный фон. - Масштаб — длина векторов:
(2,0)(0,2)→ 2 текселя на пиксель → карта вдвое мельче (отдалили). - Поворот на θ — это матрица
(cosθ, sinθ)(−sinθ, cosθ). - Скос — неравномерная добавка в один столбец.
Откуда берётся перспектива: построчное масштабирование
Сама матрица плоская — она поворачивает и тянет, но не создаёт глубины. Перспектива «в горизонт» — это разная матрица на каждой строке развёртки, заливаемая через HDMA. Почему именно построчно? Потому что горизонтальная строка плоского пола лежит на постоянной глубине z. Перспективное деление 1/z вдоль строки не меняется → достаточно посчитать масштаб один раз на строку.
Камера на высоте h над полом, горизонт проецируется в фиксированную строку экрана. Для строки на p пикселей ниже горизонта глубина пола и масштаб (текселей на пиксель):
(f — фокусное в пикселях). У горизонта p→0 → масштаб → ∞ (бесконечно сжато, далеко); внизу экрана p велико → масштаб мал (близко, крупно). Эту 1/p-зависимость HDMA и зашивает: на каждой строке свои M7A=M7D=масштаб(p).
Числовой пример. Пусть h = 256 текселей. Строка у горизонта p=8 → масштаб 32 текселя/пиксель; p=64 → 4; нижняя строка p=128 → 2. Один и тот же ориентир в 256 текселей у горизонта займёт 256/32 = 8 px, а под колёсами — 256/2 = 128 px. Вот градиент глубины: дальнее сжато в 16 раз сильнее ближнего — потому что масштаб(8)/масштаб(128) = 128/8 = 16.
🕹 В какие игры поиграть — и что заметить
Один приём — «крутить и масштабировать один слой» — даёт от плоского вращения комнаты до гонки до горизонта. По каждому кейсу: как сделано и во что сыграть, чтобы увидеть руками.
Стартовый тайтл SNES, без сопроцессора. Один плоский слой-трасса + построчный HDMA-масштаб = дорога до горизонта на 60 кадрах. Машина — это спрайт по центру, который не поворачивается вовсе: руль крутит весь мир под ней (матрица поворота слоя). Над горизонтом — отдельный растровый небосвод, под ним — тот самый трансформируемый слой. Однопользовательский: полноэкранная построчная матрица съедает почти весь бюджет кадра.
🎮 Сыграй: в F-Zero войди в долгий поворот и смотри на машину — она стоит, а мир вращается вокруг неё. Поймай взглядом линию горизонта: всё выше неё — статичное небо, всё ниже — единственный аффинный слой.
Перспективы мало — нужно ещё быстро считать коэффициенты матрицы на каждый кадр (проекция, тангаж). 65816 в этом слаб, поэтому в картридж вложили математический сопроцессор DSP-1. Mario Kart (1992) к тому же двухэкранный: каждая половина — свой независимый Mode-7-слой, потерю площади компенсируют картой/зеркалом заднего вида. Вырос из прототипа двухпользовательского F-Zero.
🎮 Сыграй: в Super Mario Kart на двоих заметь, что верх и низ экрана — две разные Mode-7-плоскости. Свались с трассы — увидишь, как «земля» наклоняется и масштабируется фоном поверх спрайтов (тот самый трюк «фон перед спрайтом»).
Mode-7-пол для обзорной карты; города и партия — обычные спрайты, скомпонованные сверху. Наклон/вираж дирижабля = поворот плоскости под неподвижным спрайтом корабля.
🎮 Сыграй: сядь в дирижабль в FF6 и разворачивайся — карта мира крутится под тобой (Mode 7), но города и сам корабль остаются плоскими спрайтами, которые лишь масштабируются. Этот разрыв «трансформируемый пол vs плоские наклейки» и выдаёт 2.5D, а не 3D.
Комнаты, которые целиком вращаются, и зумы — демонстрация чистого Mode-7-поворота одного слоя. Глубины нет: за объект не зайти, всё лежит в одной плоскости, которую крутят.
🎮 Сыграй: в Castlevania IV дойди до вращающейся комнаты — заметь, что нельзя оказаться «за» чем-либо: это один плоский слой, который спинает вокруг центра (M7X,M7Y).
Вертикальные этапы Axelay имитируют «выпуклую планету» построчным растровым скроллом (line-scroll, «rolling-pin»), а не Mode 7: поверхность изгибается, но не вращается. У Sega Genesis аппаратного Mode 7 не было вовсе — эффект подделывали в софте, дорого для CPU и хуже на вид (Tales of Phantasia на SNES, на Genesis — единичные программные трюки).
🎮 Сыграй: сравни изогнутый горизонт Axelay (line-scroll) с настоящим Mode-7-полом F-Zero. Тест: line-scroll гнёт строки, но не умеет повернуть сцену; аффинный слой — поворачивает, но строки остаются прямыми.
Хардкор · теория: обратное отображение, fixed-point и почему перспектива «бесплатна» построчноможно пропустить
Почему именно backward mapping
Прямое отображение (тексель → экранный пиксель) при увеличении оставляет дыры (соседние тексели разлетаются), при уменьшении — перекрытия (несколько текселей в один пиксель, кто последний — тот и виден, мерцание). Обратное (пиксель → тексель) гарантирует ровно одно чтение на выходной пиксель: покрытие плотное и однозначное. Цена — нужно уметь инвертировать преобразование; для аффинного это просто обратная матрица, поэтому железо считает (Tx,Ty) по (Sx,Sy), а не наоборот.
Перспектива как вырожденный случай 1/z
Перспективно-корректное текстурирование требует деления на глубину для каждого экранного пикселя: u = (u/z)·z. В общем 3D глубина z меняется вдоль отрезка → деление на пиксель (дорого; Quake делал его раз в 16 пикселей — см. дальше). Но горизонтальная строка плоского пола под камерой лежит на постоянной z → 1/z постоянна вдоль строки → одно деление на строку. Mode 7 — это и есть честная перспектива в вырожденном случае «вся строка на одной глубине». HDMA просто подаёт посчитанный масштаб(p)=h/p построчно.
Fixed-point 8.8 и мерцание
Шаг по карте (A,C) хранится как 8.8 (точность 1/256 текселя). У горизонта масштаб огромен (десятки текселей на пиксель), поэтому крошечная дробная разница в шаге между кадрами перепрыгивает через много текселей. Без мип-уровней и фильтрации (их у Mode 7 нет, выборка — nearest) это даёт алиасинг и «рябь» вдали. Та же причина, по которой далёкая текстура без мипмапов «искрит» на любом железе.
Хардкор · инженерия: HDMA, бюджет кадра и сопроцессорможно пропустить
- HDMA = DMA по строкам. Перед каждой строкой развёртки контроллер DMA сам пишет новые
M7A…M7Dиз таблицы в RAM, не трогая CPU. Так задаётся «разная матрица на строку» без участия процессора в реальном времени. - CPU всё равно считает таблицу. «Бесплатна» только растеризация. Коэффициенты
масштаб(p)на ~224 строки на каждый кадр кто-то должен посчитать — это работа CPU (или DSP-1). Отсюда1/p-таблица часто предвычислена. - DSP-1 — математический сопроцессор на картридже (Pilotwings, Super Mario Kart): проекция, умножение матриц, тригонометрия. Голый 65816 это тянет только в простом случае одного плоского слоя (F-Zero) — поэтому F-Zero без чипа, а Mario Kart с чипом.
- Один слой. Mode 7 — это BG1 и только он; параллакс-слоёв нет. Небо над горизонтом рисуют отдельно (растровый сплит цвета/смена настроек в середине кадра), а «объекты» — спрайтами поверх. Никакой настоящей окклюзии: сквозь спрайт «видно», потому что нет буфера глубины.
- Genesis: быстрее CPU ≠ Mode 7. Это фиксированная функция в PPU SNES, а не вопрос мегагерц. VDP Genesis аффинного режима не имел; софт-эмуляция съедала кадр и выглядела хуже.
Графика / обработка изображений: любое текстурирование, поворот/ресайз картинки, image warping, ремаппинг линз — это backward map + интерполяция (так не возникает дыр). Перспективно-корректное текстурирование Quake — общий случай построчного деления Mode 7 (следующий урок).
ML / AI: Spatial Transformer Networks и операция grid_sample — это буквально обратное аффинное отображение карты признаков с билинейной выборкой, тот же Mode 7 в дифференцируемом виде; на нём же стоят img2img-варпы и аугментации. Глубже — принцип «вынеси инвариант из внутреннего цикла»: считать дорогое там, где оно постоянно (раз на строку), — это loop-invariant code motion, переиспользование KV-cache (один префикс на много токенов), пер-батч, а не пер-пример.
Перф / системы: предвычисленная таблица 1/p вместо деления на пиксель — классический «таблица вместо арифметики» (LUT). HDMA — это смена состояния по расписанию строк, прообраз command-buffer'а / пер-draw-call апдейтов юниформов в современном рендере.
Принцип: аппроксимация, выбранная под структуру задачи (плоскость → строка постоянной глубины), бьёт честное общее решение, когда структура есть и её видно.
M7A…M7D, центр, скролл — увидишь поворот/масштаб/скос на живой текстуре. Затем в bsnes-plus / Mesen-S запусти F-Zero, открой просмотр слоёв и трассировку HDMA: увидишь, как перед каждой строкой в матрицу пишутся новые значения масштаба. Выключи BG1 — мир исчезнет, останутся спрайт-машина и небо.Если Mode 7 крутит один плоский слой, почему в F-Zero машина не поворачивается, а мир вращается вокруг неё?
Почему перспективное деление в Mode 7 — одно на строку, а в Quake — почти на пиксель?
z → 1/z вдоль строки не меняется → одно деление на строку. В настоящем 3D отрезок полигона уходит вглубь, z меняется вдоль него → нужна перспективная коррекция на пиксель. Quake амортизировал её, деля раз в 16 пикселей и интерполируя между ними аффинно. Mode 7 — это предельный случай, где «раз в N пикселей» превратилось в «раз на всю строку», потому что N = вся ширина.Genesis был мощнее SNES по тактовой CPU — почему у него нет Mode 7?
Зачем Mario Kart и Pilotwings чип DSP-1, если Mode 7 «бесплатный» в железе?
Почему текстура у горизонта в Mode 7 рябит и «искрит»?
- SNESdev Wiki — «Mode 7 transform» и «Mode 7 perspective effects»: точные регистры
M7A…M7D / M7X / M7Y, формула, HDMA. - Kulor, «Guide to Mode 7 Perspective Planes» (форумы NESDev) — как строить
1/p-таблицу. - Retro Game Mechanics Explained — «SNES Mode 7» (разбор механизма за ~10 минут).
- Модуль 2, раздел «SNES Graphics: Mode 7» (
02-console-wars-1985-1993.md).