image


Полагаю, многие мои читатели так или иначе знакомы с такими видеокартами, как 3dfx Voodoo. Эти легендарные графические ускорители из середины\конца 90-х годов был чуть ли не в каждой второй сборке для игр, а о их производительности слагали легенды. До сих пор есть относительно небольшое сообщество фанатов ретро-игр, которые ценят, любят и собирают с цветмета те немногие видеокарты от 3dfx, что остались в СНГ. Однако обзоров на 3dfx Voodoo много, тестов игр — тоже, а вот материала «простыми словами» о его внутренней архитектуре и более того, практической части с написанием 3D-игры практически нет! Недавно я прикупил себе Voodoo 3, и начал зубрить Programmer's Manual с желанием запилить что-нибудь эдакое… Статью я долго и упорно готовил дабы успеть к новому году и сегодня у нас с вами: краткая история компании 3dfx, подробный разбор архитектуры видеочипов 3dfx «под капотом», что должен был уметь программист 3D-графики в 90х и написание 3D-приложения на Glide полностью с нуля. Интересно? Тогда жду вас в статье!

Предисловие


Материал про архитектуру S3 ViRGE показал, что рубрика с разбором «подкапотки» видеочипов 90-х годов оказалась довольно интересной. Многие люди приходят почитать, поностальгировать в комментариях, а иногда даже пишут в личку и спрашивают детали реализации конкретных видеочипов! Думаю, эта рубрика станет одной из основных, в будущем году, мы рассмотрим с вами:

  1. PSP;
  2. PS1 (как раз читатель недавно задарил фаточку и слимку);
  3. ATI Rage;
  4. Вероятно, GeForce 2.

image

Само собой, я не мог пройти мимо 3dfx Voodoo. И дело не только в ценности и легендарности данных видеочипов, но и во внутренней архитектуре, которая очень сильно отличается от современных GPU. Про 3dfx я знаю уже более 10 лет, ещё с самой юности, а знакомство произошло в одном из выпусков «16-бит тому назад», однако прикупить себе один экземпляр решился только сейчас.

image


Чем же был обусловлен культовый статус 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 позволял загрузить текстуры с более высоким разрешением, при этом на частоту видеоядра тогда смотрели гораздо реже.

image


Другим интересным моментом было собственное графическое API. В те годы доминировал OpenGL, пришедший к нам с профессиональных графических станций: SGI выделили собственное API в отдельную спецификацию, выкинули оттуда различные функции, не касающиеся вывода графики и сделали полностью открытой. В 1996 году, DirectX 2.0 с первой версией Direct3D только-только появился и был достаточно нестабильным GAPI (даже Кармак его ругал), а больше вариантов и не было. Результатом стало появление 3dfx Glide — низкоуровневого графического API, которое позволяло управлять видеокартой похожим на OpenGL путём, но при этом на ещё более низком уровне, имея возможность тонкой настройки всего конвейера. Одной из важных фишек Glide стала поддержка не только Windows, но и DOS, что было достаточно редким, если не уникальным случаем для тех лет.

image

Игры для 3dfx Voodoo не заставили себя ждать. Благодаря относительной простоте Glide и понятному мануалу, а также открытости SDK, начали появляться игры с поддержкой данного GAPI: Tomb Raider, MechWarrior, Quake — какие-то игры работали через прослойку (как вышеупомянутая квака, которая реализовывала прослойку Glide -> минимальное подмножество OpenGL, которого хватало для работы игры), какие-то нативно. Выход игр с поддержкой 3dfx Glide обеспечил успех новой видеокарте, поскольку игры зачастую не просто работали шустрее, но и выглядели гораздо симпатичнее.

image

После выхода первой 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 с точки зрения системы были простыми консольными приложениями — даже без собственных окон!

image


Немного позже, 3dfx добавила модуль 2D ускорения, сделав полноценную видеокарту, которая могла работать в системе в одиночку. В 1999 году, через год после релиза 3dfx Voodoo 2, вышла Voodoo 3, которая стала системой на кристалле и объединила FBI и два TMU в один кристалл. Видеокарта стала гораздо меньше и обзавелась интерфейсом AGP (который очень близок к PCI сам по себе).

image

Конкуренты у 3dfx Voodoo были серьезные, пожалуй, самым серьезным был ATI Rage: это была небольшая система на кристалле, которая помимо ускорения 3D-графики обладала декодером видео DVD-качества (в те времена, посмотреть видео в 480p и нормальном качестве с софтварным декодером возможно было далеко не на всех процессорах. Насколько мне известно, 486 и Pentium 1 сразу в пролете), умела выводить изображение на телевизор (сам себе домашняя игровая консоль!) и поддерживала не только D3D и OpenGL (причем насколько мне известно, OpenGL поддерживался плохо и такая тенденция сохранялась до покупки ATI компанией AMD), но и собственное проприетарного GAPI ATI CIF (C Interface), которое обладало весьма широкими возможностями и было… ну очень низкоуровневым, приходилось вручную создавать контекст DirectDraw, аттачить его к 3D-контексту, вручную делать backface-отсечение и т. п.

image
\

Нельзя не вспомнить и за Riva TNT: очень шустрая видеокарта для своих лет, которая также обладала ТВ-выходом и хорошей поддержкой OpenGL. Кроме того, Riva TNT гораздо лучше себя проявляла при 24-х битном цвете: 3dfx Voodoo отрисовывала всё в 16-битном формате. Вон, люди на Riva TNT как-то даже в Морровинд играли!

image


Ну и конечно же была конкуренция в бюджетном сегменте рынка. 3dfx поставляла OEM-видеокарты для сборщиков ПК, в том числе и недорогие. Тут уж конкуренция была серьезнее: и PowerVR с десктопной видеокартой, и Intel с i740 (эдакий предок GMA), и видеокарты от SiS, и конечно же S3 Graphics!

image


В пост-советском пространстве, 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 улетают в космос. Так что скромненько, конечно, но тоже нормально :)

image


Придя домой и распаковав посылку, я сразу же установил Glide SDK и принялся изучать Programmers Manual, дабы запилить что-нибудь интересное…

Архитектура «под капотом»


Но сначала давайте поговорим об архитектуре 3dfx Voodoo «под капотом», на более низком уровне, дабы было общее понимание, что и как работает. Без этого, понять принцип работы GAPI и видеокарты в целом может быть сложно. Предлагаю рассмотреть FBI, TMU и RAMDAC по отдельности:

image

FBI — FrameBuffer Interface. Как уже было сказано выше, этот модуль является основой видеоускорителя: в его задачи входит коммуникации через PCI/AGP, растеризация примитивов, клиппинг, расчет вершинного освещения, подготовка текстур к заливке на TMU, перспективная коррекция, альфа-блендинг и работа с Z-буфером, а также двойная/тройная буферизация и управление самим фреймбуфером.

image

В Voodoo 3, под радиатором находится система на кристалле, где FBI и TMU находятся в одном чипе.

FBI делились на несколько поколений — SST-1 (3dfx Voodoo 1), SST-96 (3dfx Voodoo 2) и.т. п. Судя по всему, у одного FBI могло быть несколько ревизий, но в чем их отличие — неизвестно.

image

Для нужд FBI выделено строго 2Мб видеопамяти, которые используются для 3 буферов, один из которых можно использовать для произвольных целей. Первые два буфера называются Front и Back буферами и отображают то, что сейчас находится на экране, а третий является aux-буфером с 16-битным форматом цвета, который можно использовать для альфа-блендинга, либо как Z-буфер. Видеокарта поддерживает несколько форматов фреймбуфера, однако «под капотом» все расчёты ведутся в 16-битном RGB565 — что когда-то и стало «визитной карточкой» видеокарт 3dfx. Программист может получить прямой доступ к записи в буфер экрана с помощью lfb-функций (аналог в OpenGL — glCopyPixels/glReadPixels).

#define GR_COLORFORMAT_ARGB     0x0
#define GR_COLORFORMAT_ABGR     0x1

#define GR_COLORFORMAT_RGBA     0x2
#define GR_COLORFORMAT_BGRA     0x3

Для 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 же все вершины рисовались сразу же в экранных координатах, то есть так:


Vertex v, v2, v3;

v.Position = Vector3(0, 0, 0);
v2.Position = Vector3(15, 0, 0);
v3.Position = Vector3(15, 15, 0);

grDrawTriangle(&v, &v2, &v3);

В 3dfx Voodoo 2 ввели расчеты в Clip-Space, пояснив что растеризация в абсолютных оконных координатах слишком медленная для будущих поколений видеокарт. По итогу, если программисты хотели, чтобы их игра работала на 3dfx Voodoo 1, им нужно было выбирать «старый» режим работы и преобразовывать Clip-Space координаты в оконные вручную.

image

Как уже было сказано выше, вся видеопамять выделялась отдельным TMU и FBI. Но где-же тогда хранилась геометрия? Опытный читатель помнит про такие функции, как glBegin/glEnd. В те годы, видеопамять было принято считать именно текстурной, а геометрию пересылал процессор каждый кадр вручную — причём сразу же трансформированную, подготовленную для вывода на экран. Для каждой вершины в примитиве был свой набор регистров, задающий координаты, цвет и UV для текстур, что и позволяло сделать гибкий формат вершин, чем активно пользуется Glide.

image

Если говорить совсем уж просто, то 3dfx Voodoo был растеризатором треугольников, который ко всему прочему умел Backface-отсечение. Например, не было Near-clipping'а — процесса перестроения примитивов, вершины которых частично уходят за камеру. Если этого не делать — то когда примитив уйдет за камеру, мы все равно продолжим видеть его на экране, а если это дело аппроксимировать, отсекая целые примитивы — некоторая геометрия будет пропадать, если расположена слишком близко к камере. Многие концепции отрисовки 3D-графики программист должен был знать заранее, никаких упрощений, как с современными GAPI, не было.

TMU — Texture Mapping Unit. Как уже было сказано ранее, этот чип отдельно отвечал за сэмплинг текстур, а также управление своими банками видеопамяти и отправкой конечного результата обратно FBI.

image

EDO-чипы EliteMT

Ключевой особенностью TMU была поддержка комбайнеров — своеобразная альтернатива пиксельных шейдеров в те годы. Программист мог задать функцию и параметры работы каждого TMU, дабы он мог выполнять операции умножения текстуры на цвет, или, например, использовать текстуру как маску, манипулировать её альфаканалом и сэмплить сразу две текстуры за один проход! В те годы сама возможность отрисовать геометрию с несколькими текстурами за один проход (т.е за один вызов DrawTriangle) была прорывом и позволяла, например, вместо двух вызовов отрисовки стен в квейке (один проход для текстурированной стены, второй, отрисованный с блендингом, для лайтмапы) рисовать стены за один проход:

image

Максимальный размер текстуры — 256x256 пикселей, также была поддержка текстур с нестандартным соотношением сторон и мипмаппинга (техника, которая позволяет убрать рябь на дальних текстурированных объектах с помощью снижения разрешения текстуры в зависимости от дистанции до наблюдателя). Поддержки Non Power Of Two (не кратные степени двойки) текстур не было вообще. Поддерживалась билинейная и Point-фильтрация. По какой-то причине, 3dfx не могли сделать поддержку текстур большего разрешения чем 256х256, в то время как первая Riva TNT уже умела в текстуры 1024х1024.

image

Память у каждого TMU была отдельной, а Glide как GAPI не предоставлял никакого аллокатора для хранения текстур, программист должен был распоряжаться видеопамятью полностью сам. При этом каждый TMU может сэмплить текстуры только из своей памяти.
Текстуры загружались сразу в виде набора мипмап, однако сам сет мипов мог быть не полным и мог быть расположен в нескольких регионах памяти «вразброс». Таким образом, частично решалась проблема фрагментации памяти. Тем не менее, в те годы динамическая загрузка ресурсов на уровне — весьма нечастое явление, поэтому все текстуры можно было линейно загрузить в память и не заморачиваться с менеджментом.

DAC — RAMDAC производства компании ICS. Данный чип служит для вывода определенной части видеопамяти, т.е фреймбуфера на монитор. Дело в том, что старые VGA-мониторы с ЭЛТ, по электрической части были аналоговыми: на входе было три сигнала — красный, зеленый и синий, а также сигнальные линии горизонтальной и вертикальной синхронизации. Поскольку многие видеокарты не имели на борту ЦАП'а, в 90-х и начале нулевых устанавливались отдельные RAMDAC'и для фактического вывода картинки на дисплей. При этом вывод на ТВ с помощью «тюльпанов» мог реализовываться ещё одним RAMDAC'ом — уже специально для ТВ.

image

Сами по себе ЭЛТ-мониторы формально не имели такого понятия, как разрешение, однако существовало несколько общепринятых «пиксельклоков» — частот стробов HSYNC и VSYNC, которые и задавали виртуальное разрешение дисплея. Управляя настройками RAMDAC, FBI мог настраивать разрешение картинки, частоту синхронизации и иные параметры — например, настройки гаммы. Именно от используемого RAMDAC зависело качество изображение на ЭЛТ-мониторе, с плохим RAMDAC картинка как-бы «плыла».

Настраиваем окружение


Дабы пилить игры под 3dfx Voodoo, необязательно иметь ПК на WinXP и тулчейн уровня VC98. Строго говоря, даже самой видеокартой обладать необязательно — существует множество эмуляторов Glide, которые перехватывают вызовы игр к GAPI и преобразуют их в вызовы отрисовки D3D или Vulkan, что позволяет отлаживать ваши игры в профайлере.

image

Glide в его нативном виде работает под Windows XP. Поэтому минимальный тулчейн, который можно использовать — это VC2015 с поддержкой Windows XP. Но если вы хотите максимальной «трушности» и иметь возможность запускать софт на Win98/Win95 — нужно использовать Visual Studio 2005. При этом, совершенно необязательно отказываться от плюшек современных IDE: Visual Studio автоматически подхватывает все установленные тулчейны в системе, достаточно лишь выбрать нужный:

image

Далее, по классике добавляем либы, которые без проблем слинкуются относительно современным линкером с вашими бинарниками.

image


Ну что ж, на этом интро-часть материала закончена. Самое время перейти от теории к практике!

Инициализация


Концептуально Glide очень близок к OpenGL, поэтому тем людям, кто имел опыт программирования 3D-графики, сам процесс настройки стейтов и рисования примитивов будет знаком. Однако поскольку основной целью Glide было создание облегченного GAPI, которое только рисует примитивы и управляет текстурными юнитами, мы не найдем здесь utility-функций (в Glide 2.x они были, причем очень даже полезные, но в 3.x gu убрали), таких как работа с матрицами — множество вещей нужно реализовывать самому, полностью с нуля. Однако я постараюсь подробно и понятно всё объяснить, в свойственной мне манере!

image

Наша игра начинается с инициализации и конфигурации контекста Glide, который может быть только один в системе. Связано это с тем, что каждое приложение эксклюзивно занимает ВСЕ ресурсы видеочипа для себя — возможности рисовать в отдельное окно, напоминаю, нет. Инициализация очень простая: сначала нужно проверить количество видеочипов в системе, а затем выбрать один из них как основной и создать контекст.

Параметры нашего контекста: разрешение 640x480 в формате RGBA и частотой обновления 60Гц, два буфера для двойной буферизации и один aux-буфер для Z-буфера.


        int numBoards = 0;
	grGet(GR_NUM_BOARDS, sizeof(numBoards), (FxI32*)&numBoards);

	if (numBoards < 1)
	{
		printf("No Glide GPU is present\n");
                return -1;
	}

	printf("Initializing glide renderer\n");
	grGlideInit();
	grSstSelect(0);

	gContext = grSstWinOpen(0, GR_RESOLUTION_640x480, GR_REFRESH_60Hz, GR_COLORFORMAT_RGBA, GR_ORIGIN_UPPER_LEFT,
		2, 1); // RGBA, double buffered, 1 depth-buffer

Всё! Ни окон создавать не нужно, ни ловить события через WndProc. Просто создал контекст — и уже можно в цикле продолжать работу. После этого, нам нужно настроить состояние контекста — базовая концепция в программировании графики, которая заключается в том, что у видеокарты есть множество стейтов, которые управляют выводом итоговой картинки на экран. Примеры стейтов: цвет освещения, ширина линий, Z-буфер и Color-буфер.

Для того, чтобы наша игра работала даже на первых видеоускорителях 3dfx, нам необходимо выбрать абсолютную координатную систему, а также настроить Depth-буфер и Cull-mode. Cull-mode, или Backface culling позволяет отсекать примитивы, если они повернуты к камере обратной стороной — это позволяет не рисовать лишние фрагменты. Именно из-за этой техники, залетая внутрь модельки простенького здания или персонажа, внутри она оказывается прозрачной!


        grCoordinateSpace(GR_WINDOW_COORDS);
	grDepthBufferMode(GR_DEPTHBUFFER_ZBUFFER);
	grDepthBufferFunction(GR_CMP_GREATER);
	grDepthMask(FXTRUE);
        grCullMode(GR_CULL_POSITIVE);

Что такое Depth-буфер?
Буфер глубины — это дешевая screen-space техника отрисовки перекрываемой друг другом геометрии без фактической сортировки примитивов по отдалению от камеры. Суть простая: каждая вершина треугольника имеет собственную координату Z, которая и обозначает дальность вершины от камеры. При растеризации треугольника, Z-значение интерполируется и записывается в картинку точно таких-же размеров, как и основное игровое окно, только вместо цвета записывается это самое Z-значение.

При отрисовке следующих примитивов, видеокарта проверяет Z-координату рисуемых треугольников с Z-координатами в буфере и если Z-координата фрагмента больше (т.е объект за другим объектом), видеокарта просто не рисует пиксель.

В классических GAPI принято использовать режимы сравнения LEQUAL и LESS, однако с буфером глубины в Glide есть особенности: в самой видеокарте они хранятся в виде целочисленного 16-битного short (т.е до 65535 значений), причем обратного (1 / z). Поэтому в нашем случае, чем объект дальше от камеры, тем меньше его Z-координаты, а посему для корректной сортировки нужно необходимо выставлять режим сравнения GREATER. Но об этом немного позже — когда дойдем до фактической отрисовки треугольников.

Переходим к настройкам формата вершины.


#define GR_VERTEX_X_OFFSET              0
#define GR_VERTEX_Y_OFFSET              1
#define GR_VERTEX_OOZ_OFFSET            2
#define GR_VERTEX_OOW_OFFSET            3
#define GR_VERTEX_R_OFFSET              4
#define GR_VERTEX_G_OFFSET              5
#define GR_VERTEX_B_OFFSET              6
#define GR_VERTEX_A_OFFSET              7
#define GR_VERTEX_Z_OFFSET              8
#define GR_VERTEX_SOW_TMU0_OFFSET       9
#define GR_VERTEX_TOW_TMU0_OFFSET       10
#define GR_VERTEX_OOW_TMU0_OFFSET       11
#define GR_VERTEX_SOW_TMU1_OFFSET       12
#define GR_VERTEX_TOW_TMU1_OFFSET       13
#define GR_VERTEX_OOW_TMU1_OFFSET       14
#if (GLIDE_NUM_TMU > 2)
#define GR_VERTEX_SOW_TMU2_OFFSET       15
#define GR_VERTEX_TOW_TMU2_OFFSET       16
#define GR_VERTEX_OOW_TMU2_OFFSET       17
#endif

typedef struct {
	float  U;                   /* s texture ordinate (s over w) */
	float  V;                   /* t texture ordinate (t over w) */
}  GrTmuVertex;

typedef struct
{
	float X, Y;         /* X and Y in screen space */
	float Z;          /* 65535/Z (used for Z-buffering) */
	float Q;          /* 1/W (used for W-buffering, texturing) */
	float r, g, b, a;   /* R, G, B, A [0..255.0] */
	GrTmuVertex  tmuvtx[GLIDE_NUM_TMU];
} Vertex;      


        grVertexLayout(GR_PARAM_XY, GR_VERTEX_X_OFFSET << 2, GR_PARAM_ENABLE);
	grVertexLayout(GR_PARAM_RGB, GR_VERTEX_R_OFFSET << 2, GR_PARAM_ENABLE);
	grVertexLayout(GR_PARAM_A, GR_VERTEX_A_OFFSET << 2, GR_PARAM_ENABLE);
	grVertexLayout(GR_PARAM_Z, GR_VERTEX_OOZ_OFFSET << 2, GR_PARAM_ENABLE);
	grVertexLayout(GR_PARAM_Q, GR_VERTEX_OOW_OFFSET << 2, GR_PARAM_ENABLE);
	grVertexLayout(GR_PARAM_ST0, GR_VERTEX_SOW_TMU0_OFFSET << 2, GR_PARAM_ENABLE);
	

Как я уже говорил ранее, 3dfx Voodoo оперирует координатами в абсолютных экранных координатах, про 3D он формально ничего не знает. Создать трёхмерное представление — задача программиста. X и Y — координаты треугольника в экранных координатах, Z-значение — для указания дальности вершины от камеры и сортировки, Q — т. н/ W-координата, необходимая для перспективного деления и перспективно-корректного текстурирования (про второе позже). R, G, B, A — цвет вершины. Позволяет, например, раскрасить ландшафт с помощью комбайнера в несколько проходов, или просто сделать модельку любого цвета, а TmuVertex — UV-координаты для корректного наложения текстур. Подробнее об этом будет в разделе текстур.

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

Это по сути минимальная инициализация Glide для рисования трёхмерной графики. После создания контекста, мы увидим синий экран. Нормальный синий экран/ :)

image


Рисуем примитивы


Теперь переходим к самому интересному — рисованию треугольников! Однако до полноценного 3D-представления нам пока ещё рано. Сначала хотя-бы просто научиться выводить примитивы на экран.
Впрочем, ничего сложного в этом нет.

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


// Buffer clear operations aren't free on Voodoo. When scene will have skybox, comment this line
	grBufferClear(RGB(0, 128, 0), 0, 0);

Треугольники рисуются одной-единственной функций: grDrawTriangle, которая принимает ссылку на три структуры с описанием вершины. Формат вершины мы уже указали при инициализации, поэтому для отрисовки нам нужно лишь заполнить их данными:


Vertex v0;
v0.X = 0;
v0.Y = 0;

Vertex v1;
v1.X = 15;
v1.Y = 0;

Vertex v2;
v1.X = 15;
v1.Y = 15;

grDrawTriangle(&v0, &v1, &v2);

Результат:

image


Откуда же берутся текстуры, окраска и освещение, спросите вы? Может, есть какая-то система материалов как в Unity/UE? Дойдем и до этого, уже совсем скоро!

Уже после отрисовки сцены, нам необходимо поменять бэкбуфер и фронтбуфер местами — таким образом, мы избежим мерцания при отрисовке следующего кадра.

grBufferSwap(1);

Теперь у нас есть треугольник, выведенный средствами 3dfx Voodoo! Уже неплохое начало, но… где обещанное 3D!? Об этом — в следующем разделе!

Математика и трансформации


Приготовьтесь, этот раздел будет звучать как учебник по матану — относительно сложно, но я постарался объяснить как можно проще и понятнее :) Если всё таки окажется сложновато — переходите к коду, понять будет гораздо проще. Строго говоря, по началу вам вообще необязательно знать детали реализации перемножения матриц и самих матриц трансформации, поскольку есть очень удобные математические библиотеки (glm и DXMath, например).

Важной составляющей в 3D-графике являются трансформации и матрицы. Если говорить простыми словами, то матрицы — многомерный массив чисел (в 3D-графике матрицы обычно имеют размерность 4х4), который позволяет представить трансформацию нашей будущей геометрии — например, модельки кораблика или героя. Например, умножив матрицу поворота на матрицу перемещения:


Matrix.RotationY(Math.DegToRad(90)) * Matrix.Translation(0, 0, 10);

Мы передвинем персонажа на 10 юнитов и повернем его на 90 градусов по оси Y. Например, компонент Transform в Unity строит мировую матрицу на основе позиции, поворота и масштабирования, которые вы задаете в инспекторе/коде.

image


В 3D-графике есть три основные матрицы, которые отвечают за положение объекта в мире и его представление из глаз наблюдателя:

  • Мировая матрица/матрица модели (OpenGL) — Положение рисуемой геометрии в глобальных мировых координатах. Пример с передвижением и поворотом относится именно к мировой матрице.
  • Матрица вида — Положение камеры в игровом мире. Трансформация камеры в мире имеет инвертированную систему координат — поскольку все передвижения объектов в глазах игрока — это как-бы вычитание позиции камеры из позиции объекта. Это значит, что для соответствия систем координат, все углы поворота и координаты необходимо инвертировать (-x, -y, -z). Умножив мировую матрицу на матрицу вида — мы получим координаты геометрии в пространстве наблюдателя.
  • Матрица проекции — Самая непонятная для некоторых матрица. Именно она преобразует координаты из пространства наблюдателя в Clip-Space (т.е абсолютные координаты треугольников в пространстве окна) и выдает нам W-координату, необходимую для перспективной проекции!

Таким образом, перед тем как быть нарисованной на экране, каждая вершина должна быть умножена на три матрицы — World/Model, View, Projection. Затем, на полученную матрицу необходимо умножить координаты самой вершины (в свою очередь, координаты вершины — это само строение 3D-модели).

$v * (model * view * projection)$



Насколько мне известно, с математической точки зрения это неверно — матрицы могут быть умножены только на матрицы той-же размерности. 4х-мерный вектор матрицой вообще не является, но там используется своя формула. Пожалуй, совсем уж в детали реализации математики не буду — это за рамками содержания статьи, но для общего понимания принципа работы 3D-графики, не только на 3dfx Voodoo, это необходимо.

Переходим к деталям реализации, здесь уже всё гораздо проще. Ниже приведена примитивнейшая, неэффективная и неоптимизированная «матлиба». Ну а что вы хотели, у нас таргет — Pentium MMX, там даже SIMD не было. :) Зато вполне наглядно.



#define MATRIX_SET(matrix, row, column, val) matrix[row * 4 + column] = val
#define MATRIX_GET(matrix, row, column) matrix[row * 4 + column]

void MatrixIdentity(float* matrix)
{
	memset(matrix, 0, sizeof(float) * 16);
	MATRIX_SET(matrix, 0, 0, 1);
	MATRIX_SET(matrix, 1, 1, 1);
	MATRIX_SET(matrix, 2, 2, 1);
	MATRIX_SET(matrix, 3, 3, 1);
}

void MatrixTranslation(float* matrix, float x, float y, float z)
{
	float m2[16];
	MatrixIdentity((float*)&m2);

	MATRIX_SET(m2, 3, 0, x);
	MATRIX_SET(m2, 3, 1, y);
	MATRIX_SET(m2, 3, 2, z);

	MatrixMultiply(matrix, (float*)&m2);
}

void MatrixRotationX(float* matrix, float rot)
{
	float m2[16];
	MatrixIdentity((float*)&m2);

	// TODO: Optimize it
	MATRIX_SET(m2, 1, 1, (float)cos(rot));
	MATRIX_SET(m2, 1, 2, (float)sin(rot));
	MATRIX_SET(m2, 2, 1, -(float)sin(rot));
	MATRIX_SET(m2, 2, 2, (float)cos(rot));

	MatrixMultiply(matrix, (float*)&m2);
}

void MatrixRotationY(float* matrix, float rot)
{
	float m2[16];
	MatrixIdentity((float*)&m2);

	// TODO: Optimize it
	MATRIX_SET(m2, 0, 0, (float)cos(rot));
	MATRIX_SET(m2, 0, 2, -(float)sin(rot));
	MATRIX_SET(m2, 2, 0, (float)sin(rot));
	MATRIX_SET(m2, 2, 2, (float)cos(rot));

	MatrixMultiply(matrix, (float*)&m2);
}

void MatrixRotationZ(float* matrix, float rot)
{
	float m2[16];
	MatrixIdentity((float*)&m2);

	// TODO: Optimize it
	MATRIX_SET(m2, 1, 1, (float)cos(rot));
	MATRIX_SET(m2, 1, 2, (float)sin(rot));
	MATRIX_SET(m2, 2, 1, -(float)sin(rot));
	MATRIX_SET(m2, 2, 2, (float)cos(rot));

	MatrixMultiply(matrix, (float*)&m2);
}

void MatrixRotationAngle(float* matrix, float x, float y, float z)
{
	MatrixRotationY(matrix, y * DEGTORAD);
}

void MatrixPrint(float* matrix)
{
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 4; j++)
			printf("%f ", matrix[i * 4 + j]);

		printf("\n");
	}
}

// FOV - в радианах
void MatrixPerspective(float* matrix, float fov, float aspect, float _near, float _far)
{
	float m2[16];
	MatrixIdentity((float*)&m2);

	float yScale = 1.0f / (float)tan(fov / 2);
	float xScale = yScale / aspect;

	MATRIX_SET(m2, 0, 0, xScale);
	MATRIX_SET(m2, 1, 1, yScale);
	MATRIX_SET(m2, 2, 2, _far / (_far - _near));
	MATRIX_SET(m2, 3, 2, -_near * _far / (_far - _near));
	MATRIX_SET(m2, 3, 3, 0);
	MATRIX_SET(m2, 2, 3, 1);
	// Perspective FOV left-handed matrix

	MatrixMultiply(matrix, (float*)&m2);
}

void MatrixMultiply(float* matrix1, float* matrix2)
{
	float mat1[16];
	memcpy((float*)&mat1, matrix1, sizeof(float) * 16);
        // Первая базовая оптимизация - развернуть цикл. Современный компилятор это сделает сам, но тем не менее. Умножение матриц замечательно ускоряется с помощью SIMD-инструкций.
	for (int j = 0; j < 4; j++) {
		for (int i = 0; i < 4; i++) {
			MATRIX_SET(matrix1, j, i,
				MATRIX_GET(mat1, j, 0) * MATRIX_GET(matrix2, 0, i) +
				MATRIX_GET(mat1, j, 1) * MATRIX_GET(matrix2, 1, i) +
				MATRIX_GET(mat1, j, 2) * MATRIX_GET(matrix2, 2, i) +
				MATRIX_GET(mat1, j, 3) * MATRIX_GET(matrix2, 3, i));
		}
	}
}


И вот, наконец-то мы переходим к отрисовке самой графики!

Рисуем 3D-модель!


Итак, что мы поняли из предыдущих разделов? Первым делом, нам нужно умножить каждую вершину на матрицу ModelViewProjection и исходя из позиционирования в абсолютных координатах, перевести координаты из Clip space в оконные. Ничего сложного!

Но сначала, давайте загрузим модельку. Я быстренько состряпал конвертер из SMD (собственный загрузчик, который таскаю из проекта в проект) в свой формат моделей.

string fileName = args[0];
                SmdMesh model = new SmdMesh(File.OpenRead(fileName));

                Stream output = File.Create(Path.GetFileNameWithoutExtension(fileName) + ".msh");
                BinaryWriter writer = new BinaryWriter(output);

                writer.Write((ushort)Version);
                writer.Write((ushort)(model.Triangles.Count * 3));

                foreach(SmdTriangle tri in model.Triangles)
                {
                    for(int i = 0; i < 3; i++)
                    {
                        writer.Write(tri.Verts[i].Position.X * 0.1f);
                        writer.Write(tri.Verts[i].Position.Y * 0.1f);
                        writer.Write(tri.Verts[i].Position.Z * 0.1f);

                        writer.Write(tri.Verts[i].UV.X);
                        writer.Write(tri.Verts[i].UV.Y);
                    }
                }

                Console.WriteLine("Total {0} vertices", model.Triangles.Count * 3);

                output.Close();

И написал загрузчик. Обратите внимание, что формат вершин для Glide и для ваших вершин должен отличаться!


MeshHeader header;
	memset(&header, 0, sizeof(header));
	FILE* f = fopen(fileName, "rb");
	fread(&header, sizeof(header), 1, f);

	if (header.Version != MESH_VERSION)
	{
		printf("Mesh version mismatch for %s\n", fileName);

		return 0;
	}

	Mesh* ret = new Mesh();
	ret->NumVertices = header.NumVertices;
	ret->Vertices = new MeshVertex[header.NumVertices];
	fread(ret->Vertices, sizeof(MeshVertex), ret->NumVertices, f);
	fclose(f);

	return ret;

Теперь, когда у нас есть 3D-модель, её можно нарисовать.
Сначала нам необходимо трансформировать каждую вершину геометрии в Clip-space, матрица — ModelViewProj. Обратите внимание, что на этом этапе, нормальный рендерер реализовывает клиппинг геометрии. У нас его нет, используется аппроксимация, что обязательно будет выливаться в пропадание геометрии, если одна из её вершин уходят «назад» за камеру. Современные видеокарты делают клиппинг аппаратно:


// Project vertex from world space to clip space. matrix - MVP matrix.
__inline bool GraphicsProjectVertex(Vertex* vert, float* matrix)
{
	float m[4][4];
	memcpy(&m, matrix, sizeof(m)); // Temporary
	Vertex ret;

	// We treat vertex as float4(pos, 1)
	ret.X = vert->X  * m[0][0] + vert->Y  * m[1][0] + vert->Z * m[2][0] + (1 * m[3][0]);
	ret.Y = vert->X  * m[0][1] + vert->Y * m[1][1] + vert->Z * m[2][1] + (1 * m[3][1]);
	ret.Z = vert->X  * m[0][2] + vert->Y * m[1][2] + vert->Z * m[2][2] + (1 * m[3][2]);
	ret.Q = vert->X  * m[0][3] + vert->Y * m[1][3] + vert->Z * m[2][3] + (1 * m[3][3]);

	*vert = ret;

	return ret.Q >= 0;
}

После того, как мы подготовили вершины, необходимо преобразовать их из Clip-Space в абсолютные координаты окна и посчитать для них UV в координатной системе 3dfx Voodoo:


__inline bool GraphicsConvertToWindowSpace(Vertex* inV, Vertex* outV, float* matrix)
{
	Vertex v = *inV;
	if (!GraphicsProjectVertex(&v, (float*)matrix)) // Если вершина не проходит в клиппинг - мы не рисуем весь примитив
		return false;

	outV->X = XVALUE_TO_WINDOW_SPACE(v.X / v.Q, 640);
	outV->Y = YVALUE_TO_WINDOW_SPACE(v.Y / v.Q, 480);
	outV->Z = 65535.0f / absF(v.Z);
	outV->Q = 1 / v.Q;

	outV->tmuvtx[0].U = v.tmuvtx[0].U / v.Q;
	outV->tmuvtx[0].V = v.tmuvtx[0].V / v.Q;

	return true;
}

Обратите внимание на то, что каждая координата в 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));

Результат (обратите внимание, что треугольник оказался в центре экрана и уменьшился в размерах — это и есть эффект от перспективного деления и перевода из Clip-space, где -1 — левая часть окна, 0 — центр, 1 — правая часть):

image


И теперь, мы наконец-то, готовы нарисовать 3D-модель! Обратите внимание, что UV-координаты переводятся из 0..1 в 0..255 из-за особенностей TMU.


void GraphicsDrawTriangle(MeshVertex* verts, float* matrix)
{
	if (verts)
	{
		// Convert generic vertex format to Glide format first
		Vertex v[3];
		memset(&v, 0, sizeof(v));

		for (int i = 0; i < 3; i++)
		{
			v[i].X = verts[i].Position.X;
			v[i].Y = verts[i].Position.Y;
			v[i].Z = verts[i].Position.Z;
			v[i].tmuvtx[0].U = verts[i].UV.X * 255.0f;
			v[i].tmuvtx[0].V = verts[i].UV.Y * 255.0f;
		}

		Vertex v1, v2, v3;
		if (!GraphicsConvertToWindowSpace(&v[0], &v1, matrix))
			return;

		if (!GraphicsConvertToWindowSpace(&v[1], &v2, matrix))
			return;

		if (!GraphicsConvertToWindowSpace(&v[2], &v3, matrix))
			return;

		grDrawTriangle(&v1, &v2, &v3);
	}
}

void GraphicsDrawMeshEx(Mesh* mesh, Material* material, float* matrix)
{
	if (mesh && mesh->NumVertices > 0)
	{
		// Setup render state
		GraphicsSetupTextureStage(material);

		for (int i = 0; i < mesh->NumVertices / 3; i++)
		{
			GraphicsDrawTriangle(&mesh->Vertices[i * 3], matrix);
		}
	}
}

...

float matrix[16];
MatrixIdentity((float*)&matrix);
MatrixRotationAngle((float*)&matrix, 0, rot, 0);
MatrixTranslation((float*)&matrix, rot, 0, 15);
MatrixPerspective((float*)&matrix, 90 * 0.0174533f, 640.0f / 480, 1, 100);

GraphicsDrawMeshEx(mesh, &mat, (float*)&matrix);

Результат:

image

Скрин с криво-наложенной текстурой: здесь уже был настроен сэмплер, но неправильно. А ещё тут видно артефакты отсутствия Z-сортировки наглядно.

Пока не впечатляет, да? Где же текстуры? А об этом — в следующей главе!

Текстуры


Плоские модельки без текстур — это не очень круто. Ну что можно сделать с плоскими моделями, пусть даже они будут с освещением?

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

Но в Glide всё было иначе — ведь там не было понятия текстуры как объекта! Как-так? Glide позволял нам получить верхнюю и нижнюю границу адресного пространства памяти одного конкретного TMU и программист волен был выгружать текстуру куда угодно! Затем программист сохранял указатель на текстуру в видеопамяти и передавал её… в комбайнер, дабы он мог использовать текстуру по назначению! При этом TMU даже характеристики текстур не знал — эту информацию отсылал программист.

TMU поддерживает множество пиксельформатов: RGB332 (8 бит на пиксель), RGB565 (16 бит на пиксель), палитровый и собственный формат сжатия с NCC-компрессией. Тем не менее, 565 у 3dfx требует какого-то особого формата пикселей, иначе текстуры превращаются в кашу. Благо для загрузки текстур с диска, в Glide есть удобные функции и тулза texUS для создания текстур и всего набора мипов для них — gu3dfGetInfo и gu3dfLoad. Кроме того, есть функция grTexCalcMemRequired для расчета необходимого размера для текстуры в видеопамяти с учетом мипов, формата и выравнивания.

Я не стал писать сложный аллокатор, поскольку игра не требует динамической видеопамяти и может сразу загрузить уровень «пачкой», а при загрузке следующего — просто освободить всю память.


        gTextureHeapMin = grTexMinAddress(GR_TMU0);
	gTextureHeapMax = grTexMaxAddress(GR_TMU0);

        Gu3dfInfo info;
	gu3dfGetInfo(fileName, &info);

	FxU32 required = grTexCalcMemRequired(info.header.small_lod, info.header.large_lod, info.header.aspect_ratio, info.header.format);

	if (gTextureHeapMin + gTextureHeapUsage + required > gTextureHeapMax)
	{
		printf("Failed to allocate texture %s: VRAM exhausted\n", fileName);
		exit(-1);
	}

	FxU32 linearAddr = gTextureHeapMin + gTextureHeapUsage;

	info.data = malloc(info.mem_required);
	gu3dfLoad(fileName, &info);

После этого, нам необходимо загрузить текстуру в видеопамять юнита с помощью grTexDownloadMipMap.


        GrTexInfo tInfo;
	tInfo.aspectRatioLog2 = info.header.aspect_ratio;
	tInfo.smallLodLog2 = info.header.small_lod;
	tInfo.largeLodLog2 = info.header.large_lod;
	tInfo.format = info.header.format;
	tInfo.data = info.data;

	grTexDownloadMipMap(0, linearAddr, GR_MIPMAPLEVELMASK_BOTH, &tInfo);

Как же теперь указать текстуру для сэмплинга, ведь glBindTexture здесь нет? Для этого есть функция glTexSource, которая принимает адрес первого мипа и конфигурацию текстуры — которая хранится на стороне ЦПУ!

grTexSource(0, linearAddr, GR_MIPMAPLEVELMASK_BOTH, &tInfo);

	NativeTextureHandle* tex = new NativeTextureHandle();
	tex->TexInfo = tInfo;
	tex->LinearAddr = linearAddr;

	Texture* ret = new Texture();
	ret->Width = 0;
	ret->Height = 0;
	ret->NativeHandle = tex;

Но если мы сейчас запустим программу, то никакой текстуры мы не увидим. Потому что сначала нужно настроить сэмплер и комбайнеры!

Для этого, мы настраиваем комбайнеры на сэмплинг текстур напрямую, без умножения на цвет вершины. Альфа-канал мы не трогаем вообще — у нас нет альфа-буфера для него.

grColorCombine(GR_COMBINE_FUNCTION_SCALE_OTHER,
		GR_COMBINE_FACTOR_ONE,
		GR_COMBINE_LOCAL_NONE,
		GR_COMBINE_OTHER_TEXTURE,
		FXFALSE);
	grAlphaCombine(GR_COMBINE_FUNCTION_SCALE_OTHER,
		GR_COMBINE_FACTOR_ONE,
		GR_COMBINE_LOCAL_NONE,
		GR_COMBINE_OTHER_TEXTURE,
		FXFALSE);
	grAlphaBlendFunction(GR_BLEND_SRC_ALPHA,
		GR_BLEND_ONE_MINUS_SRC_ALPHA,
		GR_BLEND_ZERO,
		GR_BLEND_ZERO);

Запускаем программу и вот результат:

image


Да, первая полноценная 3D-модель с текстурой! Мы реализовали половину работы видеодрайвера вручную, однако по концовке всё равно очень приятно! Игры здесь пока ещё нет — материал вышел бы слишком большим.

А где практика?



К сожалению, сегодня без практической части :( Изначально я думал что у меня всё схвачено и моя материнка на 478 с AGP-слотом вполне справиться с ролью тестового стенда для нашей демки. Однако, я не учел важный факт — существовало несколько физических версий AGP и на 478 уже поздний вариант с 1.5в/0.8в уровнями.

Материал был обещан в четверг на 11 утра, времени на заказ через почту у меня не было (новый год, посылки 1.5-2 недели задерживаются только на сортировке в Краснодаре), поэтому я начал написывать всем сервисникам у себя в городе, в надежде что у кого-то лежит на складе материнка на PGA370… в любом состоянии.

И материнка нашлась! Ей оказалась поздняя ECS P6IPAT на 815 чипсете с универсальным AGP-разъемом, который поддерживал все стандарты AGP одновременно. Продавал её мужичок всего за 100 рублей, сразу с процом и охладом :) Однако возникли определенные проблемы с поднятием платы (все электролиты необходимо менять «вкруг», а нужных номиналов под рукой не оказалось, плата стартовала раза с 3го) и накатыванием винды (помер IDE-привод), поэтому практическая часть немного откладывается…

image


Заключение


И мы приходим к выводам, что для написания 3D-игры, программист в 90-х годах должен был как минимум:

  1. Иметь представлении о трансформации геометрии, что такое матрицы (геометрию можно трансформировать и без матриц, однако это не очень удобно).
  2. Понимать, как работает конвейер видеокарты, что такое стейты, комбайнеры, каким образом происходит управление памятью, организация фреймбуфера и Depth-буфера.
  3. Иметь представление об основных техниках в растеризации 3D-графики: что такое перспективное деление, Z-буфер, форматы вершин, фильтрация текстур, мипмаппинг, затенение по Гуро, какие-либо методы анимации, если была необходимость и т. п.

Материал получился очень объёмным, для меня это абсолютный рекорд. Я старался собрать всю информацию о 3dfx Voodoo, которую изучил и поделится с вами не только архитектурой конкретно видеокарт, но и рассказать о программировании графических API на низком уровне и подробно рассказать, как же строится изображение «под капотом» и вашей видеокарты.

Касательно баек насчет 3dfx в СНГ: я лично родился в 2001 году, так что могу судить исключительно из услышанных мной баек и историй. А какая история с 3dfx Voodoo была у вас? Пишите в комментах!
Надеюсь, материал был вам интересен. :) Статья писалась несколько бессонных ночей, дабы успеть под новый год! Больше бэкстейджа, мыслей и проектов у меня в Telegram.



Читайте также:


Комментарии (44)


  1. bodyawm Автор
    21.12.2023 08:12

    Друзья, с новым годом! За все время подготовки статьи я постил бэкстейдж у себя в тг. Один из читателей увидел заинтересованность в видяхах и предложил заслать isa vga видяху.

    Как думаете, что это за видяшка? Явно поздняя по числу чипов, год 94 минимум


    1. bodyawm Автор
      21.12.2023 08:12

      @dlinyj, может быть ты знаешь?


    1. Timnet
      21.12.2023 08:12

      OAK Technology OTiVGA 037C 256Kb Hercules CGA EGA VGA 16 Bit ISA


      1. bodyawm Автор
        21.12.2023 08:12

        Блин, я на OAK не обратил внимания :( Думал может это какая-то нонейм карточка из Тайваня.

        Спасибо!


    1. MaFrance351
      21.12.2023 08:12

      Ну, кстати, я бы не назвал её совсем уж поздней. На чипах надпись 9012 - это 1990 год. Это как раз расцвет таких ISA-видюх. Ещё на ней виден второй разъём - надо полагать, это EGA.

      Поздними можно назвать карты 1993-1995 годов с куда более простой схемотехникой, где часто кроме видеочипа, пары микросхем памяти и ПЗУшки с биосом ничего нет. Всякие Realtek, Trident и подобные. Многие из таких карт, кстати, весьма ценятся, так как умеют работать, будучи всунутыми в восьмибитный слот, что актуально для машин типа XT, где других слотов попросту нет. А EGA-монитор есть не у каждого.


    1. Javian
      21.12.2023 08:12

      офф. Может быть тут найдутся эксперты, которые скажут что за перемычки на видеокарте выведены на панель.


      1. Timnet
        21.12.2023 08:12

        1. Javian
          21.12.2023 08:12

          Большое спасибо. PDF точно от неё. Не ожидал, что это оказался целый комбайн - ISA 8/16, VGA/EGA/CGA/MDA.


      1. MaFrance351
        21.12.2023 08:12

        Выбор типа монитора и ещё некоторые настройки.


  1. KoMaTo3
    21.12.2023 08:12

    Круть! В районе нулевых у меня была Voodoo Banshee, играл в Unreal и NFS Porshe Unleashed в глайде, и мы с братом разделяли мнение, что они выглядят красивее opengl/d3d, то ли какая цветопередача, то ли сглаживание, мы так и не поняли из-за чего.


    1. bodyawm Автор
      21.12.2023 08:12

      Да, у многих было такое время. С чем связано - сам не знаю :)


      1. Hidon
        21.12.2023 08:12

        на glide картинка была глаже =)


  1. AlexanderKudryavy
    21.12.2023 08:12

    Контент хорош, с наступающим!


    1. bodyawm Автор
      21.12.2023 08:12

      Спасибо)


  1. MiDeT
    21.12.2023 08:12

    У меня был Voodoo Rush....

    Этакий мутант с 6 мб памяти, в 98 году вроде.... по моему он даже до сих пор лежит где-то))

    Не было тогда компьютеров у всех подряд, а у кого были, часто не было не то что 3dfx, а даже банальной аудиокарты. Да и по ценам все это дело стоило денег.


    1. Limping
      21.12.2023 08:12

      Причём за 3dfx Voodoo приходилось припачивать в дополнение к основной видеокарте.


  1. MaFrance351
    21.12.2023 08:12

    Понравился пост. К разработке под такие железки никакого отношения я не имею, но увидеть, как оно, круто.

    И материнка нашлась! Ей оказалась поздняя ECS P6IPAT на 815 чипсете с универсальным AGP-разъемом, который поддерживал все стандарты AGP одновременно.

    Самое главное - не втыкать эту карту в платы по типу поздних 478 от ASRock (где были PCI-E и AGP на пару). Слот там зачем-то поставили без ключа, но уровни исключительно 1,5 В, при вставке видюхи старого образца материнка немедленно и бесповоротно сдохнет. Рядом со слотом даже написано что-то в духе "3.3 V AGP card will cause permanent damage".

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


    1. bodyawm Автор
      21.12.2023 08:12

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


      1. MaFrance351
        21.12.2023 08:12

        А будет ещё что-то про S3? Чтобы именно какую-то игру или демку под неё написать. Сама карта и машина, куда её воткнуть, у вас, насколько помню, есть, инструментарий тоже вроде ищется. Было бы неплохое продолжение того первого поста.


        1. bodyawm Автор
          21.12.2023 08:12

          Можно. Фактически придется делать все тоже самое, что и с вуду - т.е видяха ток треугольники рисует


          1. MaFrance351
            21.12.2023 08:12

            Ну, тут простота именно в запуске на реальном железе. Эти карты простые и дубовые, заводятся на любой плате с PCI. Раз у вас есть машина на 478, то вставить точно есть куда.


      1. shiru8bit
        21.12.2023 08:12

        Да вроде 3.5 тысяч просмотров за 15 часов - это вполне себе нормальная статистика для подобных узкоспециальных тем.


  1. rbdr
    21.12.2023 08:12

    Матрицы 1х4 (вектор) вполне себе умножаются на 4х4. Там же правило MxN NxK -> MxK... Размерность одной общей стороны даёт "совместимость" для умножения.


    1. bodyawm Автор
      21.12.2023 08:12

      Да, спасибо, меня скорректировали на другом ресурсе уже :)


  1. 0xdead926e
    21.12.2023 08:12

    RAMDAC производства компании ICS

    GENDAC он. там унутре PLL встроенный, и аж два его выхода клочат фби и память (причем не напрямую, а тоже через фби)

    возможности рисовать в отдельное окно, напоминаю, нет.

    технически- есть, но не реализовано в виндовом драйвере (как и возможность выводить туда рабочий стол). если очень-очень-очень-очень хочется, наверн можно соорудить в линуксе без переписывания всего драйвера. там и рабочий стол на ней работает.


    1. bodyawm Автор
      21.12.2023 08:12

      Хм, но именно с точки зрения видяхи, есть всего один контекст на все. На уровне месы можно жонглировать вьюпортами и автоматически подгонять NDC...


      1. 0xdead926e
        21.12.2023 08:12

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

        а в мезе уже ограничить рендер scissor'ом или как он там тогда звался до размера окна. все равно физически там только экранные координаты поддерживаются и все надо пересчитывать.


        1. bodyawm Автор
          21.12.2023 08:12

          Эээ не, так просто ридбек в какой-нибудь сурфейс там не пройдет - lfb сядет на шину при нескольких окнах. Да и что делать с разрешением? Что, если хост-приложение захочет контекст 1024х768?

          Учитывая что на шине уже сидит вся геометрия и заливка текстур, если требуется.


          1. 0xdead926e
            21.12.2023 08:12

            технически максимальный размер фронтбуффера там 1024*1024. правда тогда не влезет третий буфер, а без глубины уже грустно.

            если не хотим одновременно игорать и смотреть кинцо- вполне адекватно будет по нагрузке на шину, как по мне.

            вот если взять voodoo2- то там даже еще веселее будет. там уже фби научился в некий аналог командбуферов из вулкана (грепни даташит по cmdfifo) и там уже точно будет упор в ожидание voodoo idle.

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

            ну и pci- параллельная. какая-нибудь сетевуха/ide-контроллер/usb-хост все пожрут.


  1. firehacker
    21.12.2023 08:12

    Эпоху Glide застал, но карту не имел и API не щнал. С такой концепцией оно должно было называться 2dfx.


    1. bodyawm Автор
      21.12.2023 08:12

      В те годы насколько мне известно во всех десктопных видеокартах был софтварный вершинный конвейер. А вот на консолях - аппаратный, см. хотя-бы PS1.


      1. Limping
        21.12.2023 08:12

        И консоли здорово выигрывали у ПК именно по качеству графики.


        1. bodyawm Автор
          21.12.2023 08:12

          А вот тут мимо. PS1 точно не выигрывала у десктопа при наличии дискретного GPU


          1. Limping
            21.12.2023 08:12

            Давайте сравнивать девайсы примерно одного периода.
            PlayStation вышла в 1994.
            PlayStation 2 — в 2000, это как раз эпоха становления дискретных GPU.


            1. bodyawm Автор
              21.12.2023 08:12

              Ну дк 3dfx Voodoo вышла в 1996 :)

              PS2 эт уже времена GeForce 2.


              1. Limping
                21.12.2023 08:12

                Странное дело, у меня воспоминания что купил 3dfx Voodoo как только завезли в Москву, мне ещё достался бракованный экземпляр, намучился - пока не додумался поменять. Но это был точно не 1996 год.

                А. Понял.
                Это готовый продукт Diamond Multimedia Monster 3D привезли в Москву летом 1997 года. И наверное тогда же на Кунцевской случилась первая оффлайновая встреча 3dfx.ru с пивом в парке.


      1. firehacker
        21.12.2023 08:12

        Но API-то тоже не инкапсулирует это.

        Можно было назвать продукт 3dfx и спрятать обработку вершин в программной части. Но нет: вот вам железо и софт для отрисовки на плоскости текстурированных треугольников


        1. bodyawm Автор
          21.12.2023 08:12

          3dfx Voodoo поддерживал и d3d, а там вершинный конвейер был уже готовый.


  1. loxx
    21.12.2023 08:12

    А почему тут черный треугольник https://habrastorage.org/r/w1560/getpro/habr/post_images/a89/1b1/4cc/a891b14cc7cbf77446e50032f5e60166.png хотя написано, что полностью синий экран?

    Vertex v2; v1.X = 15; v1.Y = 15; может все-таки v2.x = 15; v2.Y = 15; ?


    1. bodyawm Автор
      21.12.2023 08:12

      Да, там очепятка. Та часть материала писалась глубокой ночью.

      Скрины перепутал :(


  1. Azrak
    21.12.2023 08:12

    STB Voodoo 3 2000 моя первая "полноценная" видеокарта для игр. До неё была I740, на которой толком нельзя было играть. До сих пор помню, как ехал в магазин через весь город, со 100 долларовой купюрой, которую обменял на вожделенную ВУДУ.


  1. dlinyj
    21.12.2023 08:12

    Прикол ускорителя 3dfx Voodoo заключался в следующем. Был у меня комп Compaq Pentium 120 Mhz с 32 метрами памяти. Гоняю я там во вторую кваку и эйдж оф эмпаерс, и тут втыкаю туда вуду. И просто космос, квака вторая выглядеть стала космически, даже с трудом, скрипом идёт космический Quake III, и т.п. И это поражало воображение тех лет.


  1. esher
    21.12.2023 08:12

    Rendition Verite 2000 была у моего товарища, и она сильно проигрывала Voodoo в сравнении. Уже не помню в чем, но в общем была хоть и дешевле, но и слабее. Насчет декодирования видео - да, помнится, я пытался посмотреть на Pentium 233MMX первое кино в формате divx с болванки, стандартным объемом в 699Мб. После нескольких неудачных попыток разгона в общем-то и так самого топового пня, я продал свой старенький комп по частям и пропил эти копейки, вспоминая относительно недавнюю итоговую стоимость всех этих комплектующих в 3500$


  1. leremin
    21.12.2023 08:12

    Наконец-то узнал, что такое 3D ускоритель. Ну то есть и тогда интуитивно понимал, что это не совсем видеокарта. Но вот теперь все понял.