Полагаю, многие мои читатели так или иначе знакомы с такими видеокартами, как 3dfx Voodoo. Эти легендарные графические ускорители из середины\конца 90-х годов был чуть ли не в каждой второй сборке для игр, а о их производительности слагали легенды. До сих пор есть относительно небольшое сообщество фанатов ретро-игр, которые ценят, любят и собирают с цветмета те немногие видеокарты от 3dfx, что остались в СНГ. Однако обзоров на 3dfx Voodoo много, тестов игр — тоже, а вот материала «простыми словами» о его внутренней архитектуре и более того, практической части с написанием 3D-игры практически нет! Недавно я прикупил себе Voodoo 3, и начал зубрить Programmer's Manual с желанием запилить что-нибудь эдакое… Статью я долго и упорно готовил дабы успеть к новому году и сегодня у нас с вами: краткая история компании 3dfx, подробный разбор архитектуры видеочипов 3dfx «под капотом», что должен был уметь программист 3D-графики в 90х и написание 3D-приложения на Glide полностью с нуля. Интересно? Тогда жду вас в статье!
❯ Предисловие
Материал про архитектуру S3 ViRGE показал, что рубрика с разбором «подкапотки» видеочипов 90-х годов оказалась довольно интересной. Многие люди приходят почитать, поностальгировать в комментариях, а иногда даже пишут в личку и спрашивают детали реализации конкретных видеочипов! Думаю, эта рубрика станет одной из основных, в будущем году, мы рассмотрим с вами:
PSP;
PS1 (как раз читатель недавно задарил фаточку и слимку);
ATI Rage;
Вероятно, GeForce 2.
Само собой, я не мог пройти мимо 3dfx Voodoo. И дело не только в ценности и легендарности данных видеочипов, но и во внутренней архитектуре, которая очень сильно отличается от современных GPU. Про 3dfx я знаю уже более 10 лет, ещё с самой юности, а знакомство произошло в одном из выпусков «16-бит тому назад», однако прикупить себе один экземпляр решился только сейчас.
Чем же был обусловлен культовый статус 3dfx Voodoo с точки зрения игрока? В первую очередь, 3dfx Voodoo вышел достаточно рано и стал одним из первых (наряду с Rendition Verite) потребительских GPU с оптимальной производительностью и ценой для 3D-игр тех лет. Игры с полноценной 3D-графикой существовали и ранее: вспомнить хотя бы PS1, N64, или японские игровые автоматы из 90-х, однако в играх на ПК в те годы практически всегда был софтварный рендерер, а 3dfx Voodoo позволял достичь графики на уровне, а то и лучше домашних консолей тех лет. За год до выхода Voodoo 1, на рынке появился NVidia NV1 — мультимедийный чип, одной из задач которого было ускорение 3D-графики. Однако, он оперировал не треугольной геометрией, как это принято сейчас, а квадами — т.е прямоугольниками и не был совместим с основными графическими API тех лет, посему и популярности не получил.
Помимо этого, видеокарты Voodoo были модульными и состояли как минимум из нескольких чипов, что положительно сказывалось на возможных конфигурациях и цене конечных устройств:
Один FBI: Чип, отвечающий за обмен данными с хост-ПК через PCI/AGP и растеризацию треугольников. Это главный чип в видеокарте, который может работать в паре с другим FBI, образовывая систему из двух видеокарт — SLI.
До трёх TMU: Один или несколько чипов, отвечающий за сэмплинг — нанесение текстуры на треугольник. В процессе отрисовки геометрии, FBI передаёт на каждый из указанных программистом TMU данные о текстуре, которую требуется наложить и параметры наложения (фильтрация, мипы и.т.п).
Именно в те годы появилась гонка за объемами видеопамяти на борту и разделение их по классу. Видеокарта с общим объёмом памяти в 16Мб считалась гораздо круче видеокарты с 8Мб: и ведь действительно, больший объём VRAM позволял загрузить текстуры с более высоким разрешением, при этом на частоту видеоядра тогда смотрели гораздо реже.
Другим интересным моментом было собственное графическое API. В те годы доминировал OpenGL, пришедший к нам с профессиональных графических станций: SGI выделили собственное API в отдельную спецификацию, выкинули оттуда различные функции, не касающиеся вывода графики и сделали полностью открытой. В 1996 году, DirectX 2.0 с первой версией Direct3D только-только появился и был достаточно нестабильным GAPI (даже Кармак его ругал), а больше вариантов и не было. Результатом стало появление 3dfx Glide — низкоуровневого графического API, которое позволяло управлять видеокартой похожим на OpenGL путём, но при этом на ещё более низком уровне, имея возможность тонкой настройки всего конвейера. Одной из важных фишек Glide стала поддержка не только Windows, но и DOS, что было достаточно редким, если не уникальным случаем для тех лет.
Игры для 3dfx Voodoo не заставили себя ждать. Благодаря относительной простоте Glide и понятному мануалу, а также открытости SDK, начали появляться игры с поддержкой данного GAPI: Tomb Raider, MechWarrior, Quake — какие-то игры работали через прослойку (как вышеупомянутая квака, которая реализовывала прослойку Glide -> минимальное подмножество OpenGL, которого хватало для работы игры), какие-то нативно. Выход игр с поддержкой 3dfx Glide обеспечил успех новой видеокарте, поскольку игры зачастую не просто работали шустрее, но и выглядели гораздо симпатичнее.
После выхода первой 3dfx Voodoo, компания сосредоточилась на улучшении Glide и доработке архитектуры видео-ускорителей. В 1998 году, спустя два года после выхода первой Voodoo, компания выпустила Voodoo 2 с объемами памяти 8Мб (по 2Мб на TMU) или 12Мб (по 4Мб на TMU), которая архитектурно была похожа на первый видеочип, однако несла в себе несколько серьезных изменений:
64-битная шина для обмена данными с TMU. Помимо этого, 3dfx отмечала то, что благодаря широкой шине, ей удалось реализовать сэмплинг сразу двух текстур за такт.
Поддержка Clip-Space координат. Прошлый видеочип рисовал треугольники в абсолютных координатах экрана, а не нормализованных, что в некоторой степени негативно влияло на производительность. При этом для обратной совместимости, поддержка абсолютных координат была оставлена.
Поддержка SLI для объединения двух видеокарт для обработки одного кадра. При этом каждая видеокарта работала над своим набором сканлайнов — строк на экране. То есть, первый видеочип мог обрабатывать нечетные строки, а второй четные.
Но тем не менее, 3dfx Voodoo оставались именно видео-ускорителями и полностью заменить видеокарту не могли, поскольку у них не было блока для работы с 2D-графикой (этот блок, в свою очередь, помимо софтварной поддержки VESA-режимов должен включать в себя аппаратное ускорение BitBLT, рисования некоторых примитивов и.т.п). По этой же причине, игры с использованием Glide нельзя было свернуть с помощью Alt-Tab (на самом деле можно, но хаком) и они не могут быть запущены в оконном режиме. По сути, 3dfx Voodoo подключался к полноценной 2D-видеокарте (которой мог быть и ATI Rage, и S3 ViRGE, и ISA-видеокарта с VGA) и полностью переключал сигнал на выход из своего RAMDAC при запуске игры. В общем, игры на Glide с точки зрения системы были простыми консольными приложениями — даже без собственных окон!
Немного позже, 3dfx добавила модуль 2D ускорения, сделав полноценную видеокарту, которая могла работать в системе в одиночку. В 1999 году, через год после релиза 3dfx Voodoo 2, вышла Voodoo 3, которая стала системой на кристалле и объединила FBI и два TMU в один кристалл. Видеокарта стала гораздо меньше и обзавелась интерфейсом AGP (который очень близок к PCI сам по себе).
Конкуренты у 3dfx Voodoo были серьезные, пожалуй, самым серьезным был ATI Rage: это была небольшая система на кристалле, которая помимо ускорения 3D-графики обладала декодером видео DVD-качества (в те времена, посмотреть видео в 480p и нормальном качестве с софтварным декодером возможно было далеко не на всех процессорах. Насколько мне известно, 486 и Pentium 1 сразу в пролете), умела выводить изображение на телевизор (сам себе домашняя игровая консоль!) и поддерживала не только D3D и OpenGL (причем насколько мне известно, OpenGL поддерживался плохо и такая тенденция сохранялась до покупки ATI компанией AMD), но и собственное проприетарного GAPI ATI CIF (C Interface), которое обладало весьма широкими возможностями и было… ну очень низкоуровневым, приходилось вручную создавать контекст DirectDraw, аттачить его к 3D-контексту, вручную делать backface-отсечение и т. п.
Нельзя не вспомнить и за Riva TNT: очень шустрая видеокарта для своих лет, которая также обладала ТВ-выходом и хорошей поддержкой OpenGL. Кроме того, Riva TNT гораздо лучше себя проявляла при 24-х битном цвете: 3dfx Voodoo отрисовывала всё в 16-битном формате. Вон, люди на Riva TNT как-то даже в Морровинд играли!
Ну и конечно же была конкуренция в бюджетном сегменте рынка. 3dfx поставляла OEM-видеокарты для сборщиков ПК, в том числе и недорогие. Тут уж конкуренция была серьезнее: и PowerVR с десктопной видеокартой, и Intel с i740 (эдакий предок GMA), и видеокарты от SiS, и конечно же S3 Graphics!
В пост-советском пространстве, 3dfx Voodoo была достаточно дорогой и для некоторых оставалась мечтой. Ситуация изменилась ближе к 1999-2001 годам, когда 3dfx с Voodoo 4 и Voodoo 5 была на грани банкротства из-за отсутствия T&L и программируемых шейдеров, а NV и AMD взяли верх, то и Voodoo начали стоить адекватных денег на вторичке: тут уже и GeForce 3 с шейдерами подоспевал, но более бедные геймеры все еще могли поиграть в Quake 3 и другие игры с приемлемым FPS!
Наш сегодняшний герой будет на 3 года меня старше — мой выбор пал на 3dfx Voodoo 3 на 8Мб в AGP-версии, которая среди любителей видеокарт 3dfx считается «не трушной» и посему, самой дешевой. Основной причиной выбора Voodoo 3 была достаточно низкая цена на конкретно это поколение видео-ускорителей — я заплатил 3.400 рублей. 1 и 2 Voodoo стоят в среднем от 4-5 тысяч рублей, а цены Voodoo 4 улетают в космос. Так что скромненько, конечно, но тоже нормально :)
Придя домой и распаковав посылку, я сразу же установил Glide SDK и принялся изучать Programmers Manual, дабы запилить что-нибудь интересное…
❯ Архитектура «под капотом»
Но сначала давайте поговорим об архитектуре 3dfx Voodoo «под капотом», на более низком уровне, дабы было общее понимание, что и как работает. Без этого, понять принцип работы GAPI и видеокарты в целом может быть сложно. Предлагаю рассмотреть FBI, TMU и RAMDAC по отдельности:
FBI — FrameBuffer Interface. Как уже было сказано выше, этот модуль является основой видеоускорителя: в его задачи входит коммуникации через PCI/AGP, растеризация примитивов, клиппинг, расчет вершинного освещения, подготовка текстур к заливке на TMU, перспективная коррекция, альфа-блендинг и работа с Z-буфером, а также двойная/тройная буферизация и управление самим фреймбуфером.
В Voodoo 3, под радиатором находится система на кристалле, где FBI и TMU находятся в одном чипе.
FBI делились на несколько поколений — SST-1 (3dfx Voodoo 1), SST-96 (3dfx Voodoo 2) и.т. п. Судя по всему, у одного FBI могло быть несколько ревизий, но в чем их отличие — неизвестно.
Для нужд FBI выделено строго 2Мб видеопамяти, которые используются для 3 буферов, один из которых можно использовать для произвольных целей. Первые два буфера называются Front и Back буферами и отображают то, что сейчас находится на экране, а третий является aux-буфером с 16-битным форматом цвета, который можно использовать для альфа-блендинга, либо как Z-буфер. Видеокарта поддерживает несколько форматов фреймбуфера, однако «под капотом» все расчёты ведутся в 16-битном RGB565 — что когда-то и стало «визитной карточкой» видеокарт 3dfx. Программист может получить прямой доступ к записи в буфер экрана с помощью lfb-функций (аналог в OpenGL — glCopyPixels/glReadPixels).
Для FBI выделен отдельный чип памяти на 2Мб или 4Мб. При 2Мб, максимальное разрешение с Aux-буфером составляло 640x480, без Aux-буфера — 800x600, при 4Мб можно было создать буфер с разрешением 800x600, при этом с Aux-буфером.
Ключевой особенностью растеризатора FBI в SST-1 была в том, что вся отрисовка геометрии происходила в абсолютных оконных координатах. В 3D-графике принято все расчеты проводить в т. н. Clip-Space координатах, которые представляют из себя нормализованные экранные координаты (NDC) в пределах -1..1. То есть, точка 0 — центр экрана, -1 — левая сторона экрана, 1 — правая. В SST-1 же все вершины рисовались сразу же в экранных координатах, то есть так:
В 3dfx Voodoo 2 ввели расчеты в Clip-Space, пояснив что растеризация в абсолютных оконных координатах слишком медленная для будущих поколений видеокарт. По итогу, если программисты хотели, чтобы их игра работала на 3dfx Voodoo 1, им нужно было выбирать «старый» режим работы и преобразовывать Clip-Space координаты в оконные вручную.
Как уже было сказано выше, вся видеопамять выделялась отдельным TMU и FBI. Но где-же тогда хранилась геометрия? Опытный читатель помнит про такие функции, как glBegin/glEnd. В те годы, видеопамять было принято считать именно текстурной, а геометрию пересылал процессор каждый кадр вручную — причём сразу же трансформированную, подготовленную для вывода на экран. Для каждой вершины в примитиве был свой набор регистров, задающий координаты, цвет и UV для текстур, что и позволяло сделать гибкий формат вершин, чем активно пользуется Glide.
Если говорить совсем уж просто, то 3dfx Voodoo был растеризатором треугольников, который ко всему прочему умел Backface-отсечение. Например, не было Near-clipping'а — процесса перестроения примитивов, вершины которых частично уходят за камеру. Если этого не делать — то когда примитив уйдет за камеру, мы все равно продолжим видеть его на экране, а если это дело аппроксимировать, отсекая целые примитивы — некоторая геометрия будет пропадать, если расположена слишком близко к камере. Многие концепции отрисовки 3D-графики программист должен был знать заранее, никаких упрощений, как с современными GAPI, не было.
TMU — Texture Mapping Unit. Как уже было сказано ранее, этот чип отдельно отвечал за сэмплинг текстур, а также управление своими банками видеопамяти и отправкой конечного результата обратно FBI.
Ключевой особенностью TMU была поддержка комбайнеров — своеобразная альтернатива пиксельных шейдеров в те годы. Программист мог задать функцию и параметры работы каждого TMU, дабы он мог выполнять операции умножения текстуры на цвет, или, например, использовать текстуру как маску, манипулировать её альфаканалом и сэмплить сразу две текстуры за один проход! В те годы сама возможность отрисовать геометрию с несколькими текстурами за один проход (т.е за один вызов DrawTriangle) была прорывом и позволяла, например, вместо двух вызовов отрисовки стен в квейке (один проход для текстурированной стены, второй, отрисованный с блендингом, для лайтмапы) рисовать стены за один проход:
Максимальный размер текстуры — 256x256 пикселей, также была поддержка текстур с нестандартным соотношением сторон и мипмаппинга (техника, которая позволяет убрать рябь на дальних текстурированных объектах с помощью снижения разрешения текстуры в зависимости от дистанции до наблюдателя). Поддержки Non Power Of Two (не кратные степени двойки) текстур не было вообще. Поддерживалась билинейная и Point-фильтрация. По какой-то причине, 3dfx не могли сделать поддержку текстур большего разрешения чем 256х256, в то время как первая Riva TNT уже умела в текстуры 1024х1024.
Память у каждого TMU была отдельной, а Glide как GAPI не предоставлял никакого аллокатора для хранения текстур, программист должен был распоряжаться видеопамятью полностью сам. При этом каждый TMU может сэмплить текстуры только из своей памяти.
Текстуры загружались сразу в виде набора мипмап, однако сам сет мипов мог быть не полным и мог быть расположен в нескольких регионах памяти «вразброс». Таким образом, частично решалась проблема фрагментации памяти. Тем не менее, в те годы динамическая загрузка ресурсов на уровне — весьма нечастое явление, поэтому все текстуры можно было линейно загрузить в память и не заморачиваться с менеджментом.
DAC — RAMDAC производства компании ICS. Данный чип служит для вывода определенной части видеопамяти, т.е фреймбуфера на монитор. Дело в том, что старые VGA-мониторы с ЭЛТ, по электрической части были аналоговыми: на входе было три сигнала — красный, зеленый и синий, а также сигнальные линии горизонтальной и вертикальной синхронизации. Поскольку многие видеокарты не имели на борту ЦАП'а, в 90-х и начале нулевых устанавливались отдельные RAMDAC'и для фактического вывода картинки на дисплей. При этом вывод на ТВ с помощью «тюльпанов» мог реализовываться ещё одним RAMDAC'ом — уже специально для ТВ.
Сами по себе ЭЛТ-мониторы формально не имели такого понятия, как разрешение, однако существовало несколько общепринятых «пиксельклоков» — частот стробов HSYNC и VSYNC, которые и задавали виртуальное разрешение дисплея. Управляя настройками RAMDAC, FBI мог настраивать разрешение картинки, частоту синхронизации и иные параметры — например, настройки гаммы. Именно от используемого RAMDAC зависело качество изображение на ЭЛТ-мониторе, с плохим RAMDAC картинка как-бы «плыла».
❯ Настраиваем окружение
Дабы пилить игры под 3dfx Voodoo, необязательно иметь ПК на WinXP и тулчейн уровня VC98. Строго говоря, даже самой видеокартой обладать необязательно — существует множество эмуляторов Glide, которые перехватывают вызовы игр к GAPI и преобразуют их в вызовы отрисовки D3D или Vulkan, что позволяет отлаживать ваши игры в профайлере.
Glide в его нативном виде работает под Windows XP. Поэтому минимальный тулчейн, который можно использовать — это VC2015 с поддержкой Windows XP. Но если вы хотите максимальной «трушности» и иметь возможность запускать софт на Win98/Win95 — нужно использовать Visual Studio 2005. При этом, совершенно необязательно отказываться от плюшек современных IDE: Visual Studio автоматически подхватывает все установленные тулчейны в системе, достаточно лишь выбрать нужный:
Далее, по классике добавляем либы, которые без проблем слинкуются относительно современным линкером с вашими бинарниками.
Ну что ж, на этом интро-часть материала закончена. Самое время перейти от теории к практике!
❯ Инициализация
Концептуально Glide очень близок к OpenGL, поэтому тем людям, кто имел опыт программирования 3D-графики, сам процесс настройки стейтов и рисования примитивов будет знаком. Однако поскольку основной целью Glide было создание облегченного GAPI, которое только рисует примитивы и управляет текстурными юнитами, мы не найдем здесь utility-функций (в Glide 2.x они были, причем очень даже полезные, но в 3.x gu убрали), таких как работа с матрицами — множество вещей нужно реализовывать самому, полностью с нуля. Однако я постараюсь подробно и понятно всё объяснить, в свойственной мне манере!
Наша игра начинается с инициализации и конфигурации контекста Glide, который может быть только один в системе. Связано это с тем, что каждое приложение эксклюзивно занимает ВСЕ ресурсы видеочипа для себя — возможности рисовать в отдельное окно, напоминаю, нет. Инициализация очень простая: сначала нужно проверить количество видеочипов в системе, а затем выбрать один из них как основной и создать контекст. Все сниппеты будут на pastebin.com - у Pikabu нет тега "код", а число картинок ограничено :(
Параметры нашего контекста: разрешение 640x480 в формате RGBA и частотой обновления 60Гц, два буфера для двойной буферизации и один aux-буфер для Z-буфера.
Всё! Ни окон создавать не нужно, ни ловить события через WndProc. Просто создал контекст — и уже можно в цикле продолжать работу. После этого, нам нужно настроить состояние контекста — базовая концепция в программировании графики, которая заключается в том, что у видеокарты есть множество стейтов, которые управляют выводом итоговой картинки на экран. Примеры стейтов: цвет освещения, ширина линий, Z-буфер и Color-буфер.
Для того, чтобы наша игра работала даже на первых видеоускорителях 3dfx, нам необходимо выбрать абсолютную координатную систему, а также настроить Depth-буфер и Cull-mode. Cull-mode, или Backface culling позволяет отсекать примитивы, если они повернуты к камере обратной стороной — это позволяет не рисовать лишние фрагменты. Именно из-за этой техники, залетая внутрь модельки простенького здания или персонажа, внутри она оказывается прозрачной!
Буфер глубины — это дешевая screen-space техника отрисовки перекрываемой друг другом геометрии без фактической сортировки примитивов по отдалению от камеры. Суть простая: каждая вершина треугольника имеет собственную координату Z, которая и обозначает дальность вершины от камеры. При растеризации треугольника, Z-значение интерполируется и записывается в картинку точно таких-же размеров, как и основное игровое окно, только вместо цвета записывается это самое Z-значение.
При отрисовке следующих примитивов, видеокарта проверяет Z-координату рисуемых треугольников с Z-координатами в буфере и если Z-координата фрагмента больше (т.е объект за другим объектом), видеокарта просто не рисует пиксель.
В классических GAPI принято использовать режимы сравнения LEQUAL и LESS, однако с буфером глубины в Glide есть особенности: в самой видеокарте они хранятся в виде целочисленного 16-битного short (т.е до 65535 значений), причем обратного (1 / z). Поэтому в нашем случае, чем объект дальше от камеры, тем меньше его Z-координаты, а посему для корректной сортировки нужно необходимо выставлять режим сравнения GREATER. Но об этом немного позже — когда дойдем до фактической отрисовки треугольников.
Переходим к настройкам формата вершины.
Как я уже говорил ранее, 3dfx Voodoo оперирует координатами в абсолютных экранных координатах, про 3D он формально ничего не знает. Создать трёхмерное представление — задача программиста. X и Y — координаты треугольника в экранных координатах, Z-значение — для указания дальности вершины от камеры и сортировки, Q — т. н/ W-координата, необходимая для перспективного деления и перспективно-корректного текстурирования (про второе позже). R, G, B, A — цвет вершины. Позволяет, например, раскрасить ландшафт с помощью комбайнера в несколько проходов, или просто сделать модельку любого цвета, а TmuVertex — UV-координаты для корректного наложения текстур. Подробнее об этом будет в разделе текстур.
Кроме этого, нам необходимо каждый кадр очищать буфер цвета и глубины, а в конце рисования сцены — поменять передний буфер с задним, дабы мы могли увидеть нашу картинку на экране.
Это по сути минимальная инициализация Glide для рисования трёхмерной графики. После создания контекста, мы увидим синий экран. Нормальный синий экран/ :)
❯ Рисуем примитивы
Теперь переходим к самому интересному — рисованию треугольников! Однако до полноценного 3D-представления нам пока ещё рано. Сначала хотя-бы просто научиться выводить примитивы на экран.
Впрочем, ничего сложного в этом нет.
Перед рисованием геометрии, нам необходимо очистить (т. е. залить одним цветом) бэкбуфер и Depth-буфер. Бэкбуфер необходимо очищать только если ваша сцена не перекрывает весь экран — т. е. в ней есть не закрашенные участки. На современных видеокартах эта операция бесплатная, однако на видеокартах 90х от нее есть некоторый оверхед. Если бэкбуфер не чистить, то при передвижении камеры, мы будем наблюдать т.н «эффект зеркал» — поскольку новая геометрия рисуется поверх старой, то мы будем видеть старый кадр внахлест с новым:
grBufferClear(RGB(0, 128, 0), 0, 0);
Треугольники рисуются одной-единственной функций: grDrawTriangle, которая принимает ссылку на три структуры с описанием вершины. Формат вершины мы уже указали при инициализации, поэтому для отрисовки нам нужно лишь заполнить их данными:
Откуда же берутся текстуры, окраска и освещение, спросите вы? Может, есть какая-то система материалов как в Unity/UE? Дойдем и до этого, уже совсем скоро!
Уже после отрисовки сцены, нам необходимо поменять бэкбуфер и фронтбуфер местами — таким образом, мы избежим мерцания при отрисовке следующего кадра.
Теперь у нас есть треугольник, выведенный средствами 3dfx Voodoo! Уже неплохое начало, но… где обещанное 3D!? Об этом — в следующем разделе!
❯ Математика и трансформации
Приготовьтесь, этот раздел будет звучать как учебник по матану — относительно сложно, но я постарался объяснить как можно проще и понятнее :) Если всё таки окажется сложновато — переходите к коду, понять будет гораздо проще. Строго говоря, по началу вам вообще необязательно знать детали реализации перемножения матриц и самих матриц трансформации, поскольку есть очень удобные математические библиотеки (glm и DXMath, например).
Важной составляющей в 3D-графике являются трансформации и матрицы. Если говорить простыми словами, то матрицы — многомерный массив чисел (в 3D-графике матрицы обычно имеют размерность 4х4), который позволяет представить трансформацию нашей будущей геометрии — например, модельки кораблика или героя. Например, умножив матрицу поворота на матрицу перемещения:
Matrix.RotationY(Math.DegToRad(90)) * Matrix.Translation(0, 0, 10);
Мы передвинем персонажа на 10 юнитов и повернем его на 90 градусов по оси Y. Например, компонент Transform в Unity строит мировую матрицу на основе позиции, поворота и масштабирования, которые вы задаете в инспекторе/коде.
В 3D-графике есть три основные матрицы, которые отвечают за положение объекта в мире и его представление из глаз наблюдателя:
Мировая матрица/матрица модели (OpenGL) — Положение рисуемой геометрии в глобальных мировых координатах. Пример с передвижением и поворотом относится именно к мировой матрице.
Матрица вида — Положение камеры в игровом мире. Трансформация камеры в мире имеет инвертированную систему координат — поскольку все передвижения объектов в глазах игрока — это как-бы вычитание позиции камеры из позиции объекта. Это значит, что для соответствия систем координат, все углы поворота и координаты необходимо инвертировать (-x, -y, -z). Умножив мировую матрицу на матрицу вида — мы получим координаты геометрии в пространстве наблюдателя.
Матрица проекции — Самая непонятная для некоторых матрица. Именно она преобразует координаты из пространства наблюдателя в Clip-Space (т.е абсолютные координаты треугольников в пространстве окна) и выдает нам W-координату, необходимую для перспективной проекции!
Таким образом, перед тем как быть нарисованной на экране, каждая вершина должна быть умножена на три матрицы — World/Model, View, Projection. Затем, на полученную матрицу необходимо умножить координаты самой вершины (в свою очередь, координаты вершины — это само строение 3D-модели):
vertex * (model * view * projection)
Насколько мне известно, с математической точки зрения это неверно — матрицы могут быть умножены только на матрицы той-же размерности. 4х-мерный вектор матрицой вообще не является, но там используется своя формула. Пожалуй, совсем уж в детали реализации математики не буду — это за рамками содержания статьи, но для общего понимания принципа работы 3D-графики, не только на 3dfx Voodoo, это необходимо.
Переходим к деталям реализации, здесь уже всё гораздо проще. Ниже приведена примитивнейшая, неэффективная и неоптимизированная «матлиба». Ну а что вы хотели, у нас таргет — Pentium MMX, там даже SIMD не было. :) Зато вполне наглядно.
И вот, наконец-то мы переходим к отрисовке самой графики!
❯ Рисуем 3D-модель!
Итак, что мы поняли из предыдущих разделов? Первым делом, нам нужно умножить каждую вершину на матрицу ModelViewProjection и исходя из позиционирования в абсолютных координатах, перевести координаты из Clip space в оконные. Ничего сложного!
Но сначала, давайте загрузим модельку. Я быстренько состряпал конвертер из SMD (собственный загрузчик, который таскаю из проекта в проект) в свой формат моделей.
И написал загрузчик. Обратите внимание, что формат вершин для Glide и для ваших вершин должен отличаться!
Теперь, когда у нас есть 3D-модель, её можно нарисовать.
Сначала нам необходимо трансформировать каждую вершину геометрии в Clip-space, матрица — ModelViewProj. Обратите внимание, что на этом этапе, нормальный рендерер реализовывает клиппинг геометрии. У нас его нет, используется аппроксимация, что обязательно будет выливаться в пропадание геометрии, если одна из её вершин уходят «назад» за камеру. Современные видеокарты делают клиппинг аппаратно:
После того, как мы подготовили вершины, необходимо преобразовать их из Clip-Space в абсолютные координаты окна и посчитать для них UV в координатной системе 3dfx Voodoo:
Обратите внимание на то, что каждая координата в Clip-Space делится на координату W — это называется перспективным делением, благодаря которому вершины отдаляются и приближаются в зависимости от позиции относительно глаз игрока! Здесь же рассчитывается координата Z для отрисовки перекрытой геометрии.
Перевод из Clip-Space в абсолютные координаты:
#define XVALUE_TO_WINDOW_SPACE(srcX, width) (width / 2) + (srcX * width);
#define YVALUE_TO_WINDOW_SPACE(srcY, width) width - ((width / 2) + (srcY * width));
И теперь, мы наконец-то, готовы нарисовать 3D-модель! Обратите внимание, что UV-координаты переводятся из 0..1 в 0..255 из-за особенностей TMU.
Вторая часть материала вот здесь. Пришлось бить на две части из-за ограничения в 30.000 символов.
Больше бэкстейджа, мыслей и проектов у меня вTelegram.