COMMANDER KEEN – ОДНА ИЗ ПЕРВЫХ ФРАНШИЗ ID SOFTWARE ДЛЯ DOS
скриншот из игры
Commander Keen – серия интерактивных приключений молодого паренька, который решил изучить “космическую карту” недалеко от родной планеты, чтобы повысить рейтинг эрудированности и убедится, что “земля круглая”. Первая часть игры появилась в 1990 году.
Для своего времени – это был инновационный продукт. Многие люди знают, о чём я говорю – (те специалисты, которые устанавливали DOS и Norton Commander) на первые, относительно современные, офисные коробки. Я расскажу о своём опыте ознакомления с игрой. Было две или три “лысых коробки” с начинкой Intel P60. И недорогой видеокартой S3 Trio 3D. Карта, не смотря на название никакого реального сглаживания пикселей не имела и играть можно было в дебютную версию DOOM – ощущая себя в псевдо-трехмерном мире.
На борту видеокарты находилось около 1 Мегабайта видеопамяти, что уже позволяло играть в платформеры для DOS и псевдо-трёхмерные игры, например Wolfenstein, имея 8 Мегабайт оперативной памяти (2 модуля SIMM по 4 Мегабайта). Для описываемого продукта, где использовалась псевдо-изометрия – такой начинки хватало с лихвой. Полноценное VGA разрешение (640*480 пикселей) даже превышало аппаратные требования первых частей игры.
Поиграть или почитать подробнее можно отсюда.
Продолжение поста «Игровая легенда из 90-х: Как работала 3dfx Voodoo "под капотом"? Пишем 3D-приложение нуля на Glide (1/2)»
Это вторая часть лонга про 3dfx Voodoo. Пришлось разбить его на две части из-за ограничений в 30.000 символов на пост у Пикабу. Сначала читайте первую часть.
Скрин с криво-наложенной текстурой: здесь уже был настроен сэмплер, но неправильно. А ещё тут видно артефакты отсутствия Z-сортировки наглядно.
Пока не впечатляет, да? Где же текстуры? А об этом — в следующей главе!
❯ Текстуры
Плоские модельки без текстур — это не очень круто. Ну что можно сделать с плоскими моделями, пусть даже они будут с освещением?
Те читатели, которые имеют опыт программирования графики наверняка знают, что видеодрайвер сам распоряжается видеопамятью с точки зрения аллокатора (механизм, управляющий выделением динамической памятью — т. е. той памятью, которая может быть выделена под объект, освобождена и затем занята другим объектом). Программист лишь создаёт текстуру, указывает число мипов и выгружает её на видеокарту — сейчас даже генерация мипов это задача видеодрайвера и самой видеокарты.
Но в Glide всё было иначе — ведь там не было понятия текстуры как объекта! Как-так? Glide позволял нам получить верхнюю и нижнюю границу адресного пространства памяти одного конкретного TMU и программист волен был выгружать текстуру куда угодно! Затем программист сохранял указатель на текстуру в видеопамяти и передавал её… в комбайнер, дабы он мог использовать текстуру по назначению! При этом TMU даже характеристики текстур не знал — эту информацию отсылал программист.
TMU поддерживает множество пиксельформатов: RGB332 (8 бит на пиксель), RGB565 (16 бит на пиксель), палитровый и собственный формат сжатия с NCC-компрессией. Тем не менее, 565 у 3dfx требует какого-то особого формата пикселей, иначе текстуры превращаются в кашу. Благо для загрузки текстур с диска, в Glide есть удобные функции и тулза texUS для создания текстур и всего набора мипов для них — gu3dfGetInfo и gu3dfLoad. Кроме того, есть функция grTexCalcMemRequired для расчета необходимого размера для текстуры в видеопамяти с учетом мипов, формата и выравнивания.
Я не стал писать сложный аллокатор, поскольку игра не требует динамической видеопамяти и может сразу загрузить уровень «пачкой», а при загрузке следующего — просто освободить всю память.
После этого, нам необходимо загрузить текстуру в видеопамять юнита с помощью grTexDownloadMipMap.
Как же теперь указать текстуру для сэмплинга, ведь glBindTexture здесь нет? Для этого есть функция glTexSource, которая принимает адрес первого мипа и конфигурацию текстуры — которая хранится на стороне ЦПУ!
Но если мы сейчас запустим программу, то никакой текстуры мы не увидим. Потому что сначала нужно настроить сэмплер и комбайнеры!
Для этого, мы настраиваем комбайнеры на сэмплинг текстур напрямую, без умножения на цвет вершины. Альфа-канал мы не трогаем вообще — у нас нет альфа-буфера для него.
Запускаем программу и вот результат:
Да, первая полноценная 3D-модель с текстурой! Мы реализовали половину работы видеодрайвера вручную, однако по концовке всё равно очень приятно! Игры здесь пока ещё нет — материал вышел бы слишком большим.
❯ А где практика?
К сожалению, сегодня без практической части :( Изначально я думал что у меня всё схвачено и моя материнка на 478 с AGP-слотом вполне справиться с ролью тестового стенда для нашей демки. Однако, я не учел важный факт — существовало несколько физических версий AGP и на 478 уже поздний вариант с 1.5в/0.8в уровнями.
Материал был обещан в четверг на 11 утра, времени на заказ через почту у меня не было (новый год, посылки 1.5-2 недели задерживаются только на сортировке в Краснодаре), поэтому я начал написывать всем сервисникам у себя в городе, в надежде что у кого-то лежит на складе материнка на PGA370… в любом состоянии.
И материнка нашлась! Ей оказалась поздняя ECS P6IPAT на 815 чипсете с универсальным AGP-разъемом, который поддерживал все стандарты AGP одновременно. Продавал её мужичок всего за 100 рублей, сразу с процом и охладом :) Однако возникли определенные проблемы с поднятием платы (все электролиты необходимо менять «вкруг», а нужных номиналов под рукой не оказалось, плата стартовала раза с 3го) и накатыванием винды (помер IDE-привод), поэтому практическая часть немного откладывается…
❯ Заключение
И мы приходим к выводам, что для написания 3D-игры, программист в 90-х годах должен был как минимум:
Иметь представлении о трансформации геометрии, что такое матрицы (геометрию можно трансформировать и без матриц, однако это не очень удобно).
Понимать, как работает конвейер видеокарты, что такое стейты, комбайнеры, каким образом происходит управление памятью, организация фреймбуфера и Depth-буфера.
Иметь представление об основных техниках в растеризации 3D-графики: что такое перспективное деление, Z-буфер, форматы вершин, фильтрация текстур, мипмаппинг, затенение по Гуро, какие-либо методы анимации, если была необходимость и т. п.
Материал получился очень объёмным, для меня это абсолютный рекорд. Я старался собрать всю информацию о 3dfx Voodoo, которую изучил и поделится с вами не только архитектурой конкретно видеокарт, но и рассказать о программировании графических API на низком уровне и подробно рассказать, как же строится изображение «под капотом» и вашей видеокарты.
Касательно баек насчет 3dfx в СНГ: я лично родился в 2001 году, так что могу судить исключительно из услышанных мной баек и историй. А какая история с 3dfx Voodoo была у вас? Пишите в комментах!
Надеюсь, материал был вам интересен. :) Статья писалась несколько бессонных ночей, дабы успеть под новый год! Больше бэкстейджа, мыслей и проектов у меня вTelegram.
Материал подготовлен при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud, дабы не пропускать новые статьи каждую неделю!
Игровая легенда из 90-х: Как работала 3dfx Voodoo «под капотом»? Пишем 3D-приложение нуля на Glide (1/2)
Полагаю, многие мои читатели так или иначе знакомы с такими видеокартами, как 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? Дойдем и до этого, уже совсем скоро!
Уже после отрисовки сцены, нам необходимо поменять бэкбуфер и фронтбуфер местами — таким образом, мы избежим мерцания при отрисовке следующего кадра.
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 строит мировую матрицу на основе позиции, поворота и масштабирования, которые вы задаете в инспекторе/коде.
В 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.
Дешевые китайские консоли с «AliExpress» — на чём работают бюджетные игровые гаджеты «за тыщу»?
Совсем недавно ярассказывалвам о такой популярной в прошлом консоли, как Тетрис и подробно описал возможности процессора, который в нём использовался. Думаю вам, моим читателям, тематика с разбором «подкапотки» различных редких девайсов как минимум достаточно интересна. Полагаю, многие мои читатели, которые увлекаются играми, а особенно ретро-геймингом, видели на маркетплейсах типа AliExpress «новодельные» игровые консоли с названиями X7, X12 и т. п, которые внешне повторяют Nintendo Switch и предлагают кучу пиратских ромов прямо из коробки! Сегодня мы с вами: выясним, что из себя представляют эти консоли изнутри, на каком чипсете они работают, узнаем немного об их программной платформе и разберемся, причём здесь MP5-плееры из нулевых. Интересно? Тогда жду вас в статье!
❯ Небольшая предыстория
Честно сказать, игровые консоли с эмуляторами ретро-систем были популярны всегда, это отнюдь не какой-то современный тренд. Начиная с их появления в середине-конце нулевых, эти девайсы постоянно сметались с полок магазинов благодаря какой-то неадекватной дешевизне, огромному количеству предварительно загруженных игр и неплохому функционалу, помимо, собственно, эмуляции игр. Основной ЦА таких девайсов предполагаются дети: в более юном возрасте все мы были не искушены вариативным геймплеем или крутой графикой, для многих из нас за счастье было попрыгать, играя за Марио, хотя среди пользователей очень часто находились и взрослые люди, которые хотели бы испытать те же эмоции, что испытывали когда-то сидя перед экраном советского телевизора.
Вообще, принято считать, что основными устройствами на рынке портативных систем являются консоли от Nintendo и Sony. Однако в наше время, это не совсем так: сейчас производители ретро-консолей создают свои собственные бренды и выходят на рынок с гораздо более интересными устройствами: вспомнить хотя-бы Miyoo, которые работают на базе собственного дистрибутива Linux, для которого можно писать свой нативный софт помимо запуска эмуляторов, или устройства от Anbernic, которые совмещают в себе функционал Android-смартфона и портативного игрового гаджета. Можно также вспомнить Sup GameBox — нашумевшая ультрадешёвая (~800 рублей или ~8$ на момент написания статьи) консоль с кучей игр для NES (Денди), возможностью подключения к телевизору, а также игры вдвоём с помощью дополнительного геймпада.
История «эмуляторных» консолей достаточно богата на события и устройства. Те, кто крутится в теме эмуляции достаточно давно, помнят такие легендарные устройства как Dingoo A320: очень популярная в своё время консоль с возможностью запуска сторонних эмуляторов и нативных игр (в основном, от китайских студий). Известна большим количеством несовместимых с ней клонов. Толчок популярности дал порт Linux: энтузиасты портировали ядро на MIPS-чипсет Ingenic JZ4760, благодаря чему появилось большое количество эмуляторов и портов игр, которые используют библиотеку SDL. Вышла в 2009 году.
Также была популярна консоль Ritmix RZX-50, которая являлась духовным наследником A320 и работала на базе того же чипсета от Ingenic. Отличия заключались в увеличенном объёме ОЗУ и более удобном дизайне: «кирпичик» A320 нравился не всем. Вышла в 2012 году.
Ближе к 2012-2013 году, стали появляться «эмуляторные» консоли на базе планшетного железа и ОС Android. Эти устройства работали на базе самых разных чипсетов: чаще всего использовались чипсеты Amlogic AML8726-M3 (1 ядро, 1ГГц, Cortex-A9, Mali 400), где-то использовались процессоры AllWinner (я видел только A10: одно ядро, 1ГГц, Cortex A8, Mali 400), в совсем топовых устройствах использовались чипы от Rockchip (и эти устройства были самыми ненадежными). Можно сказать, это было «новое дыхание» для этого рынка: во первых, помимо эмуляторов они тянули большинство Android-игр тех лет. В 2012 году, далеко не у всех были устройства на Android, многие ещё продолжали ходить с тачфонами или кнопочниками а-ля Samsung SGH-E250 и игры на смартфонах для них создавали вау-эффект. А во вторых, эти устройства вполне заменяли планшеты: у них был Wi-Fi, а иногда и 3G, благодаря чему покупатель потенциально получал сразу два устройства: консоль с эмуляторами и физическими кнопками + планшет для серфинга в интернете. Из подобных устройств вспоминаются устройства от JXD и их локализации в РФ: Exeq, Func, Smaggi и иные бренды.
Подобные устройства могут представлять игровую ценность и сейчас: на вторичке они очень быстро дешевеют до «шапки сухарей» — рабочий девайс можно взять в пределах 500 рублей.
И это я не говорю об огромном количестве «безымянных» устройств, которые просто выходили на рынок, локализовывались и продавались за довольно небольшой прайс. Относительно недавно рынок заполонили очень дешевые игровые консоли, которые формой и расцветкой напоминают PS Vita и Nintendo Switch. Брендов у этих устройств нет: обычно используются названия X7, X12 и т. п. — каждая обозначает, видимо, размер дисплея. При этом в программном плане они отличаются, в устройствах с одинаковым корпусом могут встретиться разные прошивки. Производитель обещает тысячи встроенных игр, а также кучу мультимедийных возможностей типа видео-плеера и даже… камеры. На «алике» и российских маркетплейсах стоят они достаточно недорого: в среднем 2-3 тысячи рублей за новое устройство. На барахолках новое или почти новое устройство можно взять за 1.000-1.500 (~10$) рублей — вполне лояльный прайс. Так поступил и я: взял X12 Plus за 1.000 рублей с интересующей меня прошивкой.
Правда я взял девайс с небольшим нюансов: левый аналоговый стик не работал по направлению вниз и на дрифт это не было похоже.
И вот, консоль пришла. Самое время её распаковать!
❯ Распаковка
Устройство поставляется в небольшой коробочке, где вкратце описаны характеристики устройства. Весьма забавляет маркетинг производителя: заявляется о «профессиональном» игровом чипе, большом количестве игр и т. п. В целом, производитель нигде не лукавит: в девайсе действительно предустановлено большое количество ромов, некоторые из них даже вынесены в основное меню.
Комплектация девайса не особо богатая: зарядка, кабель для подключения к телевизору (TV-Out, однако чипсеты этого производителя точно умеют HDMI и возможно на других ревизиях консоли он тоже распаян), инструкция и конечно же сама консоль. Сам девайс ощущается действительно большим, после классического форм-фактора PSP.
У девайса есть кнопка включения и рычажок питания, который видимо напрямую коммутирует массу с аккумулятора: дабы не портить АКБ при долгом простое.
❯ Что внутри?
Предлагаю разобрать девайс и узнать что у него внутри. Как я уже говорил, продавец заявил о нерабочем направлении «вниз» на левом стике, помимо этого, у консоли также туго нажимался левый триггер. Самое время посмотреть, на чем она работает под капотом и обслужить консоль! Разбирается девайс довольно просто — выкручиваем 4 винтика и снимаем крышку с клипс.
Разобрав девайс мы увидим следующую картину: приклеенный к плате аккумулятор, припаянный динамик (причём на корпусе есть место под второй динамик, а на плате пятачки под него — зависит от ревизии), на некоторых устройствах припаянный коаксиал с антенной. В целом, сборка консоли и разводка платы нареканий не вызывает — процесс производства таких консолей отлажен более 10 лет назад.
У устройств подобного плана высокая ремонтопригодность: резинки для кнопок можно попытаться найти в донорских устройствах, спикеры подходят от некоторых планшетов, в качестве АКБ вообще можно использовать хоть BL-4C, а про дисплеи мы поговорим немного позже.
А вот и причина плохой работоспособности нашего триггера. Боковые SMD-кнопки имеют свойство отваливаться, если их сильно и часто нажимать. Кнопки имеют два сигнальных контакта с обратной стороны и две крепежных ножки — именно они чаще всего выламываются. Пофиксить легко: некоторые снимают шелкографию и добавляют дополнительный припой для лучшего крепления, некоторые садят на клей. Я чаще капаю клей под «пузо» кнопки и запаиваю — работает нормально.
Переходим к стикам, которые взаимозаменяемы и их без проблем можно выпаять из донорских китайских консолей — они полностью идентичны. Конструкция их довольно простая: по сути, это два переменных резистора, которые выдают напряжение от 0в до 3.3в (референсное, может быть любым) по каждой оси. Разбираются они легко: поддеваем металлические крепления с длинной стороны (не с короткой!) и аккуратно разбираем стик. Здесь его достаточно почистить и всё будет работать нормально.
Однако несмотря на то, что стик фактический аналоговый, в большинстве подобных устройств он обрабатывается какцифровой— т. е. влево, вправо, вниз и вверх. Причём далеко не всегда есть возможность одновременно зажать несколько кнопок направления — что может стать проблемой в некоторых играх.
Самое интересное у устройства находится с обратной стороны. Отпаиваем аккумулятор, откручиваем винтики, крепящие плату, отсоединяем шлейфы и вытаскиваем материнку. Характеристики нашего устройства следующие:
Чипсет: ATJ2279B.
ОЗУ: Одна банка DDR1 NANYA на 64Мб. Эти чипы уже очень давно не производятся.
NAND: Infineon 29FI6808CCMEI. На чипе стоит маркировка ©'09. Потенциально, этот чип лежал на складе аж с 2009 года — более 14 лет! Даташит на этого чип не нашлось, на этикетке консоли написано 16Гб, по факту система видит 8Гб.
Дисплей: На этот раз, довольно «свежая» матрица MLHD5 2022 года выпуска.
Сам по себе ATJ2279B — это полноценная система на кристалле, которая уходит корнями аж в 2010 год. Да, никаких изменений за 13 лет в этих устройствах не произошло, кроме портирования новых эмуляторов. На данный чип естьподробный даташит, который описывает его возможности.
Вычислительное ядро: MIPS, на частоте до 450МГц с 16Кб кэша данных и 16Кб кэша инструкций.
GPU: 2D-графический ускоритель с поддержкой OpenVG 1.0. В прошивке, похоже, не используется — анимации тормозные.
Память: Контроллер DDR1/DDR2 памяти с максимальным объёмом до 256Мб и контроллер NAND-памяти с автоматической коррекцией ошибкой и ремаппингом бэдблоков. Теоретически, на данном чипсете можно запустить Android — поддерживаемый объём ОЗУ и производительность ядра позволяли это сделать, но производительность была бы достаточно низкой.
Дисплей: Поддержка TTL-матриц с разрешением до 1024x1024.
Ввод: Матричная клавиатура + резистивный тачскрин
ТВ: TVOut + HDMI в чипсете (однако на самой плате, HDMI не разведен).
USB: OTG хост + ведомое устройство.
Питание: 3.3-4.2в, встроенный контроллер для зарядки литий-ионных АКБ + ADC для мониторинга вольтажа аккумулятора при зарядке.
В начале статьи я говорил о том, что данные консоли имеют кое-что общее с MP5-плеерами нулевых. Процессоры компании Actions Semiconductor когда-то использовались в подобных устройствах именно как мультимедийные — они включали в себя DSP-сопроцессор для декодирования звука и работы с видео. Компания славилась тем, что предоставляла исходный код прошивки (на базе RTOS UCOS-II) с готовым плеером, драйверами и.т.п — благодаря чему, на рынке появилось много дешевых устройств, где производителям оставалось лишь кастомизировать интерфейс под себя. Со временем, производители портировали на эту прошивку различные эмуляторы, а сама прошивка научилась запускать сторонние бинарники — мне на флешке попадались so-библиотеки эмуляторов.
Так и появилось кучу самых разных мультимедийных игровых консолей за копейки. Процессоры Actions Semiconductor понемногу развивались — была даже вариацияG1000, которая имелся даже отдельный 3D GPU — Vivante GC и тянул игры уровня PS1, однако серьезного буста в производительности он не давал.
JXD5000 на базе G1000.
Производитель предлагал собственный SDK для разработки игр и эмуляторов под устройства на базе этих чипов. Китайские студии делали игры под эти устройства, но в публичный доступ утекало только SDK на Dingoo A320 и есть homebrew SDK для SPMP8k (консоль на котором я ищу). Естьнекий SDKс частичным исходным кодом прошивки: инструкций по сборке нет, но попробовать разобраться можно.
Отдельно хочется сказать про дисплей — их можно было найти в бюджетных планшетах 2010-2014 годов, в основном на базе чипов WM8650, AllWinner A10/A13, AMLogic AML8726-M и дешевых Rockchip. Поскольку родной дисплей по качеству очень «так себе», можно провести апгрейд, взяв матрицу с какого-нибудь нерабочего планшета за 100 рублей с юлито. 100% подходят матрицы от китайских реплик первого iPad с диагональю 7". Это повышает ремонтопригодность гаджета — битые планшеты на всяких скупках можно найти почти в каждом городе. Главное обращайте внимание на форму шлейфа, перед покупкой гуглите "<модель планшета> матрица" и сверяйте с своим. В остальном они должны быть совместимы.
Родная матрица.
Подкинул дисплей от реплики iPad.
Общение с дисплеем идёт по протоколу RGB, 60 пиновый шлейф, распиновка стандартная: её я прикладываю ниже. Дисплеи 40pin (навигаторы), 50pin (планшеты), 60pin (чуть более свежие планшеты с отдельной подсветкой) хорошо стандартизированы и обычно без каких либо проблем взаимозаменяемы:
Собираем девайс обратно. Самое время протестировать устройство в играх!
❯ Играем
При включении устройства, нас встречает забавная анимация и главное меню, которое предлагает следующие возможности:
Какие эмуляторы у нас есть? Давайте смотреть:
NES
SNES
SMD
GB
GBA
В целом — весьма неплохо. Но как у них с производительностью? Давайте посмотрим:
Эмулятор GBA идёт отлично. Даже довольно тяжелые 3D-игры типа NFSU2 идут без каких либо проблем и «рваного» звука. Не могу ничего сказать насчет серьезного пропуска кадров, но в целом этот ром играется неплохо, как и например Street Fighter II:
Эмулятор SMD идёт плюс-минус нормально. Видны кое-где корректировки пропуска кадров, но в целом вполне играбельно. Что странно: при таком объеме встроенной памяти, в консоли всего чуть больше 40 ромов для NES и среди них нет ни Earthworm Jim, ни Sonic The Hedgehod! Ну как так-то! 2.5D игры идут неплохо.
Эмулятор SFC (SNES) идёт сносно. Ромов реально довольно много и среди них попадаются такие занимательные игры, как Metal Warriors: Run & Gun сайдскроллер про огромного меха. Игры идут вполне хорошо, правда я не тестировал более тяжелые игры для SNES, которые могут использовать альфа-канал, например.
Ну и эмулятор NES, с учетом того, что SNES и GBA здесь идут нормально, тоже работает хорошо. Для этой консоли здесь больше всего ромов: как минимум, несколько сотен. Возможности поиска (вроде-бы нет), поэтому навигация по играм может быть не очень удобной.
В целом, консоль весьма неплохо справляется с прямым предназначением — эмуляцией. Кое-где есть слабые места, но это не так критично. Консоль умеет прикидываться USB-флэшкой, благодаря чему на неё можно залить новые ромы или, например, музыку, видео или электронные книги (дисплей здесь очень «так себе» для чтения, глаза быстро устанут). С музыкой есть нюанс: возможно мне попался брак, илиу моих наушников джек слишком короткий, но в моей консоли работает только один канал на звук. Качество вполне неплохое, на уровне MP3-плееров начала 2010х готов, но с Hi-Fi плеерами очевидно не сравнится.
❯ Заключение
Лично как по мне, платформа сама по себе перспективная, как и вся задумка в целом, но подкачала реализация. Если бы китайцы выпустили нормальное SDK для инди-разработчиков, то авось выходили бы порты новых эмуляторов и весьма интересных игр. Конечно на этой консоли можно без проблем поменять дисплей на более качественный, или поставить АКБ побольше (благо места в корпусе просто завались), но экспиренс от игры улучшается совсем незначительно. Если у вас небольшой бюджет, я порекомендую смотреть в сторону старых консолей на Android: у них обычно и дисплеи гораздо лучше, и производительность отличная и они вполне могут послужить и планшетом: клиенты ВК и YouTube на Android 4.x есть.
Вот так работают игровые консоли «под капотом». Эти устройства практически не поменялись за 10 лет, да и зачем? Свою функцию ультрадешевых устройств они выполняют нормально, а это самое главное. Конечно хотелось бы иметь версию с чуть-более качественным IPS-дисплеем и нормальным аналоговым стиком, но если так посмотреть — они уже есть и от более именитых производителей. Причем в разных форм-факторах: кому-то нравится GBA, а кому-то PSP.
Покупать ли такой девайс себе? В качестве основного устройства для игры — я бы не стал, ребенку — вполне возможно. Решайте сами :)
Статья подготовлена при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud , чтобы не пропускать новые статьи каждую неделю!
Сам написал, сам погонял: Как я написал 3D-гонки «на жигулях» за неделю, полностью с нуля?
Статьи про инди-разработку игр — это всегда интересно и занимательно. Но статьи про разработку игр с нуля, без каких-либо игровых движков — ещё интереснее! У меня есть небольшой фетиш, заключающийся в разработке минимально играбельных 3D-демок, которые нормально работали бы даже на железе 20-летней давности. Полтора года назад, в мае 2022 года, я написал демку гоночной игры с очень знакомым всем нам сеттингом — жигули, девятки, десятки, и всё это даже с тюнингом! В этой статье я расскажу вам о разработке 3D-игр практически с нуля: рендерер, менеджер ресурсов, загрузка уровней и граф сцены, 3D-звук, ввод и интеграция физического движка. Интересна подробнейшая статья формата "старого Пикабу" о разработке игры с нуля? Тогда добро пожаловать!
❯ Предыстория
На момент написания статьи, я всё ещё остаюсь достаточно юным — буквально 5 дней назад мне исполнилось 22 года. Но если откатиться на 4 года назад и вспомнить момент наступления моего совершеннолетия, то на ум приходят сразу два значимых события: отец приходит в один день и говорит «открывай юлито, будем смотреть авто за 40 тыщ рублей». Понятное дело, что за эту сумму (~700$ по тому курсу) особо не разгуляешься, поэтому мой выбор пал на карбюраторную «семерочку», свою ровесницу (2001 год) синего цвета. Приехали с батькой смотреть на машину, обкатали и приняли решение — надо брать!
С тех пор я ездил на своем «тазе» и горя не знал — машинка не ломалась, ни разу не подводила, вложений в себя не требовала, а я начал все больше увлекаться автомобилями и изучать тематический материал. Со временем я полюбил и другие российские модели автомобилей, но особенно мне нравился АвтоВАЗ. В один момент, вспомнив про популярный и провальный некогдаLada Racing Club, мне захотелось написать «гоночки на жигулях» самому, причём полностью с нуля. А поскольку нормального арта для города у меня не было, игру я решил назвать просто и понятно: «Ралли-кубок ТАЗов» :)
Поём всем Хабром!
Но с чего начинать писать такой объемный проект самому? Тут нам, конечно же, нужен план. У меня уже был готовый самопальный 3D-фреймворк для игрушек, который я использовал в одной из прошлых демок: арена-шутер от первого лица с модельками из модов для Quake. Фреймворк был вполне рабочим, но требовал некоторой доработки для использования в «кубке тазов».
На момент начала разработки гоночки у меня уже были готовы следующие фишки:
Рендерер: Direct3D9, причём полностью FFP — для того, чтобы игра запускалась даже на встройках Intel, где нормальной поддержки шейдеров до HD-серии вообще не было. Практически все текстурные техники работали через комбайнеры — дальний предок пиксельных шейдеров, где программист оперировал целыми стадиями пиксельного конвейера, а не писал программу напрямую, что накладывало множество ограничений. Поддерживались: многослойные материалы, однопроходной сплат-маппинг для плавного текстурирования ландшафтов, отражения в кубмапах, плавный морфинг (вершинная анимация) с линейной интерполяцией между кадрами, MSAA (это заслуга GAPI), отсечение невидимой геометрии по пирамиде видимости и примитивный альфа-блендинг с ручной сортировкой.
Звук: 3D-звук на DirectSound с позиционированием относительно источника звука, ускорением и т. п. Тут моей заслуги особо нет, кроме загрузчика wav-файлов я ничего не писал.
Ввод: WinAPI + DirectInput. Клавиатура опрашивалась с помощью классического GetAsyncKeyState, в то время как геймпады с помощью DirectInput. Была и абстракция осей ввода — дабы не адаптировать управление под кучу разных контроллеров.
Менеджер ресурсов: достаточно примитивен. К менеджеру ресурсов я отнесу и загрузчики — фреймворк поддерживал модели в форматах SMD (не анимированные меши, формат Half-life) и MD2 (анимированные меши, формат Quake 2, строго один материал на один меш), звуки — wav и простенький самопальный формат конфигов. Стандартный набор: трекинг ресурсов на слабых ссылках, пул ассетов для исключения дублирующейся загрузки и т. п.
Фреймворк выдавал не слишком крутую графику:
Зато был очень простым «под капотом», имел довольно неплохую расширяемую архитектуру и в целом, на нем можно было запилить что-то прикольное. Где-то за неделю я запилил вот такую демку шутера от первого лица:
Игрушка даже на VIA UniChrome работала — последователе всем известного S3 Savage/S3 Virge!
Минимальное приложение выглядело примерно так:
Приведённый выше код нарисует модельку с текстурой перед лицом игрока. Всё просто и понятно. Однако, как это всё работает «под капотом»? Давайте попробуем разобраться:
❯ Основа и рендерер
В своих играх я стараюсь придерживаться одной архитектуры: есть условный класс Engine, который управляет созданием платформозависимых окон, организацией главного цикла игры и инициализацией подсистем. Поскольку в одном процессе обычно запущен только один экземпляр игры (исключение — выделенные авторитарные серверы с комнатами, на манер Left 4 Dead), сам по себе Engine является синглтоном и содержит в себе ссылки на все необходимые подмодули.
Game.Initialize(new GameApp());
Game.Current.Run();
Как я уже говорил выше, сам по себе рендерер построен на базе графического API Direct3D9. Выбор DX9 обусловлен его распространенностью на железе прошлых лет, хорошей совместимостью (DX9 легко запускается на железе времен DX8 и даже DX7) и иногда лучшей производительностью на видеочипах от ATI. По сути, всё начинается с создания контекста или устройства в терминологии DirectX: в параметрах создания контекста указывается ширина и высота вторичного буфера, желаемый уровень сглаживания MSAA, видеорежим и частота желаемая частота обновления экрана.
При создании контекста есть свои нюансы, которые необходимо учитывать — например, большинство встроенных видеокарт не поддерживают аппаратную обработку вершин (D3DCREATE_HARDWARE_VERTEXPROCESSING), из-за чего создание контекста будет заканчиваться ошибкой без соответствующего флага, разные видеокарты поддерживают разные форматы буфера глубины и трафарета (сейчас видеокарты нативно даже 24х-битный RGB для рендертаргетов не умеют использовать, только выравненный XRGB), а видеокарты до GF5xxx-GF6xxx не поддерживали Pure режим D3D, который предполагает, что программист возлагает всю обработку ошибок на себя, при этом количество проверок в самом GAPI уменьшается, благодаря чему мы получаем небольшой выигрыш в производительности.
Важно так же отметить такой аспект, как управление ресурсами. К ресурсам видеокарты в терминологии старых GAPI относятся текстуры и буферы (как вершинные, так и индексные). В OpenGL особо нет такого понятия, как Device Lost. Если пользователь сворачивает ваше приложение из полноэкранного режима, или, например, видеодрайвер крашится — то GL сам должен позаботится о перезагрузке ресурсов обратно в видеопамять (исключение — Android и iOS, на мобилках контекст не уничтожится, но ресурсы будут выгружены и их хендлы станут некорректными). У D3D есть событие Lost, которое вызывается при потенциальной потере контекста — и его тоже нужно грамотно обрабатывать. Поэтому в D3D есть несколько пулов:
Managed: D3D9 сам сохраняет копию текстуры или геометрии в ОЗУ, а затем при потере контекста пересоздаёт аппаратные буферы и перезагружает нужные данные сам.
Default: данные загружаются напрямую в видеопамять (в терминологии D3D — AGP memory), или, если видеопамяти не хватает — в ОЗУ, если видеокарта, конечно, поддерживает Shared Memory Architecture.
System: загрузка ресурсов только в ОЗУ. Этот пул обычно не используется в играх — слишком медленно.
И грузить данные желательно в пул Default. Иначе при относительно большом количестве ресурсов, игра начнет «жрать» ОЗУ не в себя (пример — Civilization 5). При потере контекста, ресурсы нужно перезагружать с диска «на горячую»!
Переходим к самому важному — отрисовке геометрии. Для задания внешнего вида объектов на экране, используются так называемые материалы, которые содержат в себе данные о том, какая текстура должна быть наложена на объект, насколько объект отражает свет, какая техника должна использоваться и т. п. В современных движках система материалов обычно гибкая, поскольку шейдеры могут принимать самые разные параметры. В нашем случае шейдеров нет вообще, набор параметров фиксирован и зависит от видеокарты: стандартные техники типа повершинного затенения по Фонгу/Гуро, цвет объекта, туман и т. п.
Формат материалов в фреймворке выглядит вот так:
Однако даже без шейдеров была возможность сделать относительно гибкую систему материалов — с помощью комбайнеров, как это делала Quake 3. Самые первые 3D-ускорители не поддерживали смешивание нескольких текстур за один вызов отрисовки, поэтому некоторые игры шли на ухищрение: к примеру Quake вручную сортировал геометрию по отдаленности без использования буфера глубины, он просто… накладывал альфа-блендингом ту же самую геометрию с затененной текстурой освещения (лайтмапа). Это называется многопроходной рендеринг. Комбайнеры, которые появились ближе к концу 90-х, позволяли смешивать несколько текстур с помощью различных операций (Add, Sub, Mul, Xor и т. п.), а также умножать финальный цвет на определенный коэффициент. Именно комбайнеры я использовал в своём фреймворке для реализации некоторых относительно сложных эффектов — например, плавное смешивание текстур на ландшафте:
Основная проблема комбайнеров — каша из стейтов, поэтому код выглядит не особо презентабельно. Входная текстура-маска выглядит вот так:
Переходим к отрисовке. По сути, за рисование полигональной геометрии отвечает один метод — DrawMesh, с несколькими перегрузками (в идеале — основной должен принимать матрицу трансформации, а остальные принимать обычные World-space координаты, из которых будет построена матрица трансформации). В оригинале метод рисует геометрию с помощью DIPUP, поскольку практически вся геометрия в игре была анимирована (и анимация, само собой, обрабатывалась для каждой вершины софтварно, на ЦПУ, поэтому я не видел разницы между перезаливкой геометрии на GPU каждый кадр и DIPUP), однако в одном из бранчей фреймворка я переписал отрисовку статику на обычный DIP. Обратите внимание, что DIPUP для комплексной геометрии на старых GPU будет слишком медленным — когда-то этим страдал графический движок Irrlicht.
В более позднем бранче добавилось отсечение по дистанции от «глаз» игрока и по пирамиде видимости.
Переходим к анимации. Есть три основных метода анимации геометрии в играх:
Скиннинг: анимация вершин относительно скелета модели. Очень хорошо подходит для различных персонажей. Весь скелет является иерархией, где каждый элемент трансформируется относительно позиции родителя, что позволяет легко интегрировать «скелетку» в граф-сцены самого движка (Unity — самый яркий пример). Иногда скелетку используют и для «неоживленных» предметов — например, анимация подвески авто.
Морфинг: классический способ анимации суть которого заключается в «запекании» всех кадров в виде множества мешей. Затем игра интерполирует вершины между кадрами анимации, благодаря чему достигается эффект плавности.
Object-Transform: классический метод иерархической анимации, очень похож на скиннинг, только трансформируются не сами вершины, а привязанные к ним объекты. Применялась, например, во многих играх на PS1 и в GTA III (замечали отсутствие плавности в местах сочленений персонажей — это и есть OT).
Я не умею нормально работать с скиннингом моделей в 3D-редакторах и обычно не юзаю скиннинг в своих игрушках — для небольших демок хватает обычного морфинга с интерполяцией. Если интерполяцию не использовать, то анимация будет выглядеть топорно (в Quake 1 при отключении CVar'а такая и была):
Работа с анимациями выглядела вот так:
По сути, одна из самых комплексных частей — рендерер, готова. Однако в играх требуются и другие подсистемы, которые реализовать куда проще.
❯ Звук и ввод
Реализация звука в играх задача не шибко сложная, если дело не доходит до программной реализации микшера, 3D-позиционирования и различных эффектов. Большинству игр хватает обычного не-сжатого wav, звук в котором хранится в виде PCM-потока.
В качестве API для звука я выбрал DirectSound. Очень удобное API, хотя сейчас его фактически вытеснил XAudio. DirectSound поддерживает любые звуковые карты, сам занимается микшированием звука, а в некоторых старичках типа AC97 умеет даже аппаратное ускорение! На современных машинах обычно микширование реализовано полностью софтварно, дабы не упираться в количество каналов/память на борту звукового адаптера, но в прошлом это помогало снизить нагрузку на процессор.
В DirectSound есть два основных объекта: сам IDirectSound8, представляющий интерфейс к звуковой карте и управлению её ресурсами и буфер — который может быть подкреплен как собственными данными, так и данными из другого буфера. В играх, они делятся на три базовых понятия:
Слушатель: описание позиции и иных параметров «слушателя» — позиции ушей в игровом мире. Обычно позиция слушателя совпадает с позицией игрока.
Источник: описание источника звука в 3D-пространстве. Например, если мимо нас проносится машина, то звуковому API необходимо знать позицию, ускорение и дальность звука, дабы правильно скорректировать звук в пространстве.
Поток: поток, который содержит в себе звук. Может быть как обычным буфером, куда звук уже предварительно загружен, так и потоковым буфером, куда загружается часть музыки или другого длинного трека.
Переходим к реализации примитивного звука:
Теперь мы можем воспроизводить звуки в нашей игре!
Однако, нам нужно чтобы пользователь мог взаимодействовать с нашей игрой. Для этого в разных системах есть различные API для взаимодействия с устройствами ввода. В случае Windows — это DirectInput для обычных USB-геймпадов и рулей, и XInput для геймпадов, совместимых с Xbox 360/Xbox One. Нажатия с клавиатуры можно обрабатывать двумя способами: с помощью событий WM_KEYDOWN и WM_KEYUP и функции WinAPI GetAsyncKeyState.
Пока что мне нужна только клавиатура и мышь:
В идеале обработку ввода лучше абстрагировать от физических устройств взаимодействия. Для этого вводятся понятия осей, кнопок действий и т. п. Желательно сразу продумать, как игра будет работать с геймпадом и уже затем назначать кнопкам на клавиатуре действия абстрактного геймпада.
Ну что ж, самый базовый фреймворк для игр у нас есть. Пора переходить к написанию самой игры!
❯ Редактор уровней
Поскольку у фреймворка нет какого-либо своего графа сцены, я реализовываю механизм загрузки уровней в каждой игре с нуля — под конкретные нужды. В какой-то игре нужен стриминг для открытого мира, в другой — быстрая загрузка уровней, где есть множество объектов с разными параметрами. Изначально я использовал Blender в качестве редактора уровней и экспортировал карты небольшим скриптом, который сохранял основные параметры в файл.
Однако блендер (особенно 2.79 и ниже) не очень удобный редактор для работы с достаточно большими картами. Поэтому в определенный момент встал вопрос с организацией графа сцены и собственного редактора карт.
Граф сцены и графом то не назовешь — это просто линейный список объектов, которые присутствуют на сцене. Каждый объект наследуется от базового абстрактного типа Entity, если это «невидимый» объект, или PhysicsEntity, если объект должен интегрироваться с физическим движком. У базового объекта есть только имя и флаг выборки в редакторе.
Вообще, для редактирования уровней можно хоть редактор Unity использовать, предварительно написав экспортер в самопальный формат. Однако я решил реализовать свой редактор: как обычное Windows Forms приложение + панель, в которую движок рендерит картинку. В его реализации нет ничего необычного: он точно также загружает уровень, как и основная игра, но при этом не создаёт игрока и ботов и имеет свободную камеру.
Формат уровней примитивный донельзя. В процессе разработки небольших игрушек я обычно следую принципу KISS и не люблю распыляться сложными сериализаторами/десериализаторами и прочими заморочками, реализовывая лишь самые необходимый функционал. Формат карт — текстовый, одна линия на один объект:
p ferns 0 0 10.8 0 0 0 1
Где p — «класс» объекта, в случае p — это Prop, «декорация».
ferns — модель пропа. При этом сами пропы описаны в отдельных текстовых файлах, где в виде key-value значений хранятся настройки коллизии, материала, текстуры и т. п.
XYZ — позиция в мире.
XYZ — поворот в мировых координатах, задаётся в углах Эйлера (это только для статики, которая не подвержена Gimbal Lock, под капотом вся работа идёт с кватернионами).
❯ Физика автомобилей
После того, как граф сцены был готов, я приступил к реализации физики автомобилей. Но как я уже говорил, физический движок я использовал готовый — т. е. вся работа по резолвингу столкновений, распаралелливанию вычислений и Joint'ам сводилась чисто к нему. Я лишь использовав физику колеса, реализовал поведение машинки, внеся в него некоторые изменения: в основном — вынес в публичные свойства параметры трения колеса.
Само колесо реализовано по классическому принципу рейкастинга — колесо пускает под себя лучи и определяет трение относительно поверхности, на котором стоит, при этом двигая остальное тело используя собственное ускорение. Сейчас в играх для более точной симуляции используется Pacejka Magic Formula — формула, позволяющая рассчитать физически корректное поведение покрышки с различными диаметрами.
Поскольку класс сущности машинки слишком большой и требует контроля самых разных аспектов (коробка передач, аспекты тюнинга, отрисовка и материалы), я вынес часть физики в отдельный класс CarPhysics:
Как можно видеть из метода Move, наша машинка полноприводная и имеет две управляющие оси (передние, само собой). Конфигурацию привода легко можно модифицировать в будущем.
Коллизия кузова машинки — обычный OBB прямоугольник, ну или «коробка».
А вот как это работает на практике:
Пока что гонки на утюгах. Но ездит же. :))
Но с кем мы гоняемся?
❯ Боты
Я не стал называть этот раздел ИИ — боты в игре слишком примитивные. Здесь нет никакого поиска пути, боты просто ездят по заранее отмеченным точкам на карте, которые называются
вейпоинтами. Это стандартная практика во многих гонках, однако её реализация отличается от игры к игре. Вообще, для гонок есть несколько общеизвестных практик реализации навигации противников:
Вейпоинты с поиском путей: довольно комплексный метод, который позволяет сделать, например, гонки в открытом городе, где боты сами смогут находить путь к чекпоинтам. Подобный способ используется для гонок в GTA, например. Строго говоря, сам по себе поиск путей — это тоже набор чекпоинтов и преград, поэтому для такого метода навигации необходимо довольно большое количество информации (пути для трафика, светофоры и т. п).
Вручную раставленые вейпоинты: классика. Левелдизайнер вручную расставляет вейпоинты и задает им параметры: например, на этом повороте нужно притормозить, а на этой прямой можно поддать газку.
Заранее записанные пути: способ с вейпоинтами, где разработчики сначала сами катаются по трассе, стараясь выбить лучшее время, а затем используют записанный набор вейпоинтов для противников.
При этом некоторые разработчики не стесняются красивых фейков: реализация реалистичного входа в поворот с крутой физикой может быть сложной, особенно когда боты «тупые», поэтому в некоторых играх ботам намеренно подкручивали управляемость или максимальную скорость. Помните, как быстро нагоняли соперники в NFS Underground? Вот то-то же. :)
Некоторые могут вообще записать фейковый трек, по которым машина будет просто скользить, без учета физики авто. Но «беспалевные» реализации этого способа я пока не видел.
По настоящему «трушный» способ — это когда противники используют всё те же способы, которые использует игрок — т. е. также «нажимают» на виртуальные кнопки и управляют осями автомобиля. Кроме того, частенько каждому сопернику подмешивают дополнительный фактор, куда он будет ехать — иначе машинки будут толпиться друг за другом и будет выглядеть не интересно.
Я использую классические вейпоинты с подсказками.
Сначала нам необходимо получить угол между машинкой игрока и вейпоинтом. Для этого нам нужно перевести координаты текущего вейпоинта в локальные координаты машинки с учетом её поворота (т. е. относительно неё и её Forward-вектора). Поскольку поворот автомобиля считается в кватернионах, нам нужно помножить матрицу поворота на матрицу позиции машинки в мире и умножить позицию вейпоинта используя полученную инвертированную матрицу. Звучит сложно, на деле легко:
private Vector3 WorldToLocalSpace(Vector3 worldPoint)
{
Matrix transform = Matrix.Invert(Matrix.RotationQuaternion(Rigidbody.Rotation) * Matrix.Translation(Rigidbody.Position));
Vector4 vector4 = Vector4.Transform(new Vector4(worldPoint, 1f), transform);
return new Vector3(vector4.X, vector4.Y, vector4.Z);
}
Если очень условно, то это выражение эквивалентно a — b с учетом поворота. Поскольку мы вычислили локальные координаты вейпоинта, нам остаётся только вычислить угол между ними с помощью классического atan2 и перевести радианы в градусы:
private float AngleBetween(Vector3 v1) {
return (float) Math.Atan2((double) v1.X, (double) v1.Z) * 57.29578f;
}
Полностью логика бота выглядит так:
Легко и просто, да?
❯ Гараж и гонки
Какой интерес в гонках без… гонок? Поскольку у меня не было особо ассетов для создания пригорода, я решил сделать пересеченную местность. А на пересеченной местности у нас есть как кольцевые гонки, так и спринт — от точки до точки.
Помимо этого, в игре должен быть гараж, где игрок мог бы купить новую машину или тюнинговать текущую. В начале игры выдавалась бы старая дедова копейка (модельки Оки не нашел), а то и Москвич, а потом игрок выигрывал бы в гонках и получал возможности про прокачке тачек и покупке новых. Эээх, лавры Lada Racing Club не дали покоя!
Начал я с реализации гаража. Сам по себе гараж — отдельный уровень, который обрабатывается своим контроллером, также в гараже применяется самый первый доступный UI в фреймворке — меню со списками. Сам гараж поделен на множество подменю: тюнинг, гонки и автосалон.
https://pastebin.com/dakm4AvbПараллельно с гаражом, была проработана система тюнячек — они тоже описывались в простых текстовых файлах и так или иначе влияли на ходовые качества машины. Правда, визуального тюнинга не было предусмотрена — некому моделлить апгрейды. :(
Сами гонки можно было начать, обратившись к RaceManager и передав структуру RaceParameters:
public struct RaceParameters {
public string Name;
public string Mode;
public int NumOpponents;
public int Difficulty;
public int Prize;
public int ProgressAffection;
}
После этого, игра загружала уровень, создавала ботов на месте spawnPoint (игрок оказывался, как обычно, последним) и запускала гонку.
А затем каждый кадр просчитывала позиции каждого участника гонки:
Всё! Логикой движения и всем остальным заправляли уже боты. Хотя, там и был костыль на первых этапах, который помечает флаг конца гонки, в остальном — функционал гоночек рабочий. :)
Вот мы и дошли до этапа, когда простенькая, но рабочая демка игры у нас уже есть! Игра запускается на GF4, однако работает не совсем корректно — но оптимизировать её под видеокарты тех лет не составит труда (в основном — пережать текстуры, убрать некоторые техники на комбайнерах и запечь статические пропы в батчи).
❯ Заключение
Вот так я и написал гоночки за неделю. Время разработки демки с нуля до состояния, которое вы видите в статье — всего неделю. Да, за это время реально написать прототип гоночной игры. И я ведь знаю, что в комментариях игру будут сравнивать с Lada Racing Club и шутить о сроках её разработки — ведь в этом и суть! Слишком мало реально прикольных ламповых гоночек на жигулях. Вот что у меня получилось в итоге:
Исходниками игры я конечно же поделюсь: тык на GitHub.
А вот линки на загрузку демки:
Гоночки
Шутан
Ну а для меня это был своеобразный челлендж. И я его выполнил — у меня получилась рабочая демка на выходе! Я вижу что вам, моим читателям, интересна тематика самопальной разработки игр. Судя по комментариям, вам нравится тематика геймдева, программирования графики и разработки игр. Темой одной из следующих статей может стать описание архитектуры графических ускорителей конца 90х, история их API (без D3D) и написание 3D-игры для 3dfx Voodoo с нуля, на базе Glide!
Кроме того, я хотел бы рассказать о графическом API известного многим «3D декселератора» S3 Virge. Интересна ли вам такая рубрика? Пишите в комментариях!
Статья подготовлена при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud, , чтобы не пропустить новые статьи каждую неделю!
Сам себе игровая консоль: как я сделал свой «тетрис» с нуля. Что происходит, когда программист встречается с железом?
Я, как и многие мои читатели, очень люблю игры. Уже довольно обширное число моих статей было посвящено ремонту и моддингу самых разных игровых консолей — как китайских «нонеймов», так и брендовых PSP и PS Vita! Однако, меня тянет к железу не только желание отремонтировать и поставить в строй «устаревшие» девайсы, но и мания делать и созидать что-то своё! А ещё я очень люблю программировать игры и графику сам. Недавно я загорелся идеей разработать с нуля свой портативный «тетрис»: от схемы и разводки платы, до написания прошивки и игр под нее. Что получается, когда программист, который поставил электронику практически во главе своей жизни, пытается сделать свое устройство? Читайте в статье!
❯ Как я к этому вообще пришел?
Проекты разработки самодельных игровых приставок стали очень популярны к нашему времени. Если раньше embedded-разработка была достаточно дорогой и доступной лишь для избранных, то сейчас на рынке можно найти все что хочешь — и мощные микроконтроллеры с кучей периферии за 300 рублей, и готовые дисплейные модули по 250 рублей, и макетные платы с удобными dupont коннекторами за весьма скромные деньги.
Собрать свой гаджет в пределах одной-двух тысяч рублей стало вполне реальным. Люди собирают себе самые разные устройства, а игровые приставки — одна из самых популярных тем. Однако, для многих людей, которые только начинают знакомится с миром embedded-электроники, собрать консоль в своем корпусе с Raspberry Pi на борту и RetroPie в качестве оболочки — за счастье.
Однако есть определенная категория электронщиков, к которой отношусь и я — нам нужно делать всё с нуля! Свои проекты я стараюсь реализовывать на самопальных фреймворках/движках, точно также я мыслю и в подходе электроники — ну не могу я использовать чужие решения и стараюсь разобраться в вопросе сам. За моей спиной есть весьма интересные демки. Например, это моя игрушка с незамысловатым названием «ралли-кубок ТАЗов», которую я написал за неделю с нуля (рендерер, звук, ввод, редактор уровней — все свое) в 2022 году:
Вот так, с любовью программировать игры, я и пришел к мысли сделать свою консоль, так как вижу её именно я. Только без чужих библиотек и наработок, но не прям уж bare metal. Сел я и начал думать, на чём же мы будем строить наш игровой девайс!
❯ Из чего будем делать?
Как я уже говорил выше, в наше время выбор железа для создания своих девайсов большой — тут и мощные микроконтроллеры/одноплатники, по производительности сравнимые с телефонами 2005-2006 годов, и различная периферия — аж глаза разбегаются. Однако проектировать будущую консоль нужно исходя из некоторых требований.
Характеристики моего девайса следующие:
Процессор: двухядерный ARM микроконтроллер RP2040 на частоте 133мгц, построенный на архитектуре Cortex-M3. Сам процессор распаян на плате Raspberry Pi Pico.
ОЗУ: 260 килобайт SRAM, встроена в процессор. Немного, но если грамотно распоряжаться ресурсами — то хватит.
ПЗУ: 2Мб SPI Flash-памяти, также распаяны на плате.
Дисплей: 1.8" TFT-матрица с разрешением 128x160. Выбор разрешения обусловлен производительностью будущей консоли — процессор банально не сможет заполнять матрицу с относительно высоким разрешением.
Ввод: 6 кнопок, 4 из которых — направление, 2 — действий. В будущем могут добавиться еще несколько.
Звук: динамик. Пока не знаю, с чего рулить будем — возможно, возьмем «железный» ШИМ-контроллер процессора, а возможно прикрутим внешний ЦАП с i2s.
Питание: 3.7в аккумулятор BL-4C. Да, да, тот самый с Nokia и современных кнопочников! Аккумулятора, емкостью в 800мАч должно хватать хотя-бы на 4-5 часов игры. При этом зарядка АКБ обеспечивается модулем TP4056.
Весьма неплохо для самоделки, согласны? Как я уже говорил раннее, эти характеристики примерно соответствуют мобильным телефонам 2004-2006 годов — Nokia 6600, Sony Ericsson K510i, Samsung D800. Отличие лишь в ОЗУ (в телефонах её 2-4 мегабайта) и периферийных модулях типа контроллера дисплея.
На фото E398 — мобилка 2004 года выпуска, но она здесь не просто так. :)
Важную пометку нужно сделать касательно дисплеев: эти 1.8" матрицы бывают приходят с «синевой» — это не железная проблема и не совсем брак. Сам контроллер в дисплея в них сильно греется (хотя токоограничивающий резистор стоит) и негативно влияет на клей, из-за чего матрицы отклеивается от подсветки и слои поляризации начинают «синить» картинку. Лечится проклееванием подложки матрицы суперклеем.
RPi Pico я решил выбрать, поскольку информации про них достаточно мало, характеристики хорошие и пока что никто особо ничего на них не делал, тем более в рунете. А ещё у них очень удобное и простое SDK, практически bare-metal. ESP32, например, работает на FreeRTOS и имеет кучу библиотек, здесь же API простое и понятное.
Закупаем все необходимое и начинаем творить!
❯ Графика
В первую очередь нам нужно подключить дисплей и что-нибудь на него вывести. Заодно и SPI погоняем на незнакомом чипсете, благо работа с ним очень простая — задаем конфигурацию пинам (gpio_set_function), настраиваем SPI-контроллер и можно посылать данные.
SPI у RP2040 работает на частоте вплоть до ~60мгц — это достойная скорость передачи, в том числе и для быстрого вывода графики. На самом деле, SPI даже предпочтительнее чем параллельный 8080-интерфейс для использования в микроконтроллерах: дело не только в количестве занимаемых пинов, но и в возможности использования DMA!
В подобных проектах всегда нужно делать так, чтобы дисплей можно было при необходимости поменять, а желательно вообще научить работать его с несколькими контроллерами: разные дисплеи одной диагонали могут использовать разные контроллеры. В моём случае, этоST7735. Для разрешений 240x320 используются ILI9325, ILI9341, ST7789. Команды инициализации дисплея честно позаимствованы, но именно в этом нет ничего зазорного — сама система команд относительно стандартизирована, отличается лишь первичная настройка питания, гамма-коррекции и т. д — часто init sequence вставляет сам производитель в даташит.
После инициализации дисплея пробуем что-нибудь вывести. Да, все работает без проблем. Пару важных нюансов: ST7735 требует посаженный на землю CS, в воздухе его оставлять нельзя, как некоторые ILI (вы ведь навряд ли будете вешать несколько устройств на одну шину с дисплеем, когда есть вторая?) и логическое состояние 1 на пине RESET (в воздухе и «на земле» он будет висеть в постоянном ресете).
Для полустатичной графики, можно обойтись лишь командами дисплея — например, тут есть удобные функции для заливки прямоугольников (setArea и пишем цвет без остановки) или скроллинга. Сделано это для более слабых микроконтроллеров. Нам они не подойдут — выделяем память под фреймбуфер/бэкбуфер и настраиваем канал DMA для разгрузки процессора в процессе передачи данных:
Саму картинку подготавливает процессор: именно он рисует картинки и он же делает их прозрачными. На него ложится основная работа, однако мы можем ему помочь разгрузиться, если отдадим передачу уже подготовленного кадра на дисплей на DMA (Direct Memory Access) — устройство в микроконтроллере, которое позволяет процессору настроить параметры передачи данных, а DMA их будет сам копировать из памяти или в память. Таким образом, можно реализовать асинхронное копирование нескольких блоков ОЗУ, или, как в моем случае — передачу буфера кадра на дисплей, пока процессор готовит следующий. Чем больше разрешение — тем больше эффекта от DMA!
Кроме того, важно выбрать формат цвета для нашего дисплея: я выбрал 2-х байтный RGB565 (5 бит красный, 6 бит зеленый, 5 бит синий). Это экономичный формат который выглядит красивее палитровой графики и кушает не так уж и много драгоценной памяти. Кроме того, на данный момент мы умеем отрисовывать изображения произвольных размеров с прозрачностью — вместо альфа-канала здесь используется так называемый colorkey — концепция, очень близкая к хромакею, только она берет в качестве трафарета конкретный цвет. В нашем случае это «255 0 255» (ярко розовый).
Общая производительность рендерера порадовала: он легко осилит около сотни-двух различных спрайтов с адекватной производительностью, в зависимости от их размера. Но для такого разрешения экрана и будущих игр — это неплохой результат!
❯ Ввод
Теперь нам нужно как-то управлять нашим девайсом. Для этого пора сделать реализовать геймпад: в рамках этой статьи, я собрал его на макетке.
Кидаем общий минус на все кнопки, а второй вывод размыкателя кидаем на соответствующие пины GPIO. Я выбрал предпоследние т. к. на них ничего важного не висит, текущая конфигурация занимает 6 пинов. На фото выглядит не очень красиво — на то она и макетка.
Переходим к реализации драйвера. Игры могут слушать события кнопок из специальной структуры —CInput, где на каждую кнопку выделено по одному полю. В будущем конфигурация геймпада может поменяться — например, я захочу добавить аналоговый стик.
Есть ещё способ реализации больших клавиатур и геймпадов: когда все кнопки вешаются на пару линий, где на выходе каждой кнопки есть резистор определенного номинала. ЦАП микроконтроллера считывает это значение (допустим — 1024 это вверх, а 2048 — вниз) и таким образом определяет текущую нажатую кнопку. Таким раньше любили промышлять китайцы, из-за чего нельзя было нажать одновременно вверх и вправо, или вниз и влево и т. п.
❯ Пишем игру
Теперь у нас есть минимально-необходимая основа для написания игры. Первой игрой для своей консоли я решил написать классический шутер в космосе — летаем на кораблике и сбиваем врагов, попутно уворачиваясь от их пулек. Заодно проверим консоль на стабильность.
Писать я её решил в классическом C-стиле, как и принято в embedded-мире: без std и тем более stl, без ООП и виртуальных методов, аллокаций по минимуму. В общем, примерно как писали игры под GBA! В первую очередь, подготавливаем спрайты нашей игры, прямо в пейнте, а затем конвертируем их в представление обычного массива байтов в виде header-файла. На первых порах это удобнее, чем делать свой ассет-пул:
Архитектуру я организовал в виде нескольких подфункций, каждая из которых занимается своим стейтом (world/menu) и своими объектами (playerUpdate) и их отдельные версии для отрисовки. Сами игровые объекты я описал в виде структур, а центральным объектом сделал CWorld.
Время я решил описывать в тиках, а не миллисекундах, как я обычно это делаю на ПК — у консоли железо одно и там следить за этим нужно меньше.
Единственные аллокации, что я использовал — это для пулов с пулями, и с врагами. Оба пула четко ограничены — до 8 врагов на экране, и до 16 пулек — вполне хватает. Динамические аллокации помогли мне найти серьезную ошибку в коде — в один из моментов игра просто валилась с Out Of Memory. После того, как я немного поменял условия и делал аллокейты тех же самых объектов каждый кадр — игра переставала крашится. Причина оказалась простая — невнимательность (вместо >= было >), по итогу при отрисовке спрайтов за пределами экрана, программа сама начинала портить вунтренние структуры аллокатара и самой игры (проявлялось в глюках и телепортациях). После фикса, все заработало как нужно. :)
Ну и для основной части геймплея с выстрелами и столкновениями, я предусмотрел несколько функций, которые спавнят игровые объекты и сами управляют пулом. Противники обновляются как обычно, для коллизий используется AABB (axis aligned bounding box, ну или его 2D-подмножество в виде rect vs rect).
По итогу, у нас получилось простенькая, но рабочая игрушка, которая без проблем работала почти все время, что я писал этот материал, а значит устройство работает стабильно. И я очень горд, что у меня получилось сделать рабочий прототип своего собственного гаджета!
Ниже выкладываю принципиальную схему устройства, она очень простая, поэтому смысла делить ее на несколько листов нет. Разводить учился, читая сервис-мануалы и схемы :)
❯ Заключение
Полная цена сборки прототипа составила:
Raspberry Pi Pico — 557 рублей (но я брал на Яндекс Маркете, на «алике» дешевле — около 300 рублей).
Дисплей — 380 рублей, заказывал на «алике».
Макетка — 80 рублей, в местном радиомагазине.
Кнопки. По 5 или 10 рублей штучка, пусть будет 60 рублей.
По итогу, прототип мне обошелся в 1077 рублей. Бюджетненько, да, с учетом того, что можно сделать еще дешевле? Я тут так подумал, у меня есть желание развивать и поддерживать консоль в будущем и под консоль уже можно делать что-то своё… может, если вам будет интересно, делать их на заказ? Соберу вам по себестоимости (до 1.000 рублей) + доставка, если хочется попрограммировать под что-то маленькое, но самому паять не хочется. Мне было бы очень приятно. Пишите в личку или комменты, если вас заинтересовало бы такое! :)
Весь процесс разработки этого девайса занял у меня всего несколько дней. Я и до этого понимал концепцию работы 2D-графики на видеокартах прошлого века, поэтому ничего особо нового я для себя не открыл. Однако, я попробовал свои силы в разработке игровых девайсов, которые могут приносить удовольствие — как ментальное от самого процесса сборки и программирования, так и физическое от осознания того, что игра на нем работает. :)
Однако, это далеко не конец проекта! У нас ещё много работы: нужно развести и протравить полноценную плату, реализовать звук и API для сторонних игр, придумать корпус и распечатать его 3D-принтере. Кстати, я ведь обещал что скоро будут и другие интересные проекты с 3D-принтером: как минимум, мы доделаем предыдущий проект игровой консоли из планшета с нерабочим тачскрином и RPi Pico.
Пост подготовлен при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud, чтобы не пропускать новые статьи каждую неделю!
Atomic Heart как двигатель прогресса, или сколько стоит поиграть?
Вот не собирался я новый компьютер покупать, даже в планах не держал. Да, в армагеддон поигрывал, но какие там требования? А сейчас что-то призадумался. Сколько ж можно на десятилетнем ноутбуке сидеть, пора на старости лет приобщиться, что ли.
Сижу, изучаю чего там с чем сейчас надо соединять, уж давненько не интересовался темой. Вроде как тыщ в 40 можно уложиться, это где-то начальный уровень.
В принципе, радикальный апгрейд и так был нужен, а тут еще незабываемые близняшки стимулируют, советский антураж из далекой юности.
Так вот, сколько может стоить системный блок, чтобы поиграть в Атомик на средне-низких настройках? Или нечего уж начинать, коли всю жизнь обходился? Роликов в Ютубе и так уже полно...;)