О, спасибо за статью! Я на досуге вайбкодом балуюсь (да-да, позор!), астра мне делает игру моей мечты (без корованов). Чисто процедурная бродилка-гляделка по лиминальным помещениям на JS (наверное) прямо в html-странице, но полностью автономная. Оч хочу похвастаться, только не знаю как выложить.
Задумывались ли вы когда-нибудь о том, как разработчики 3D-игр умудрялись выжимать красивую графику из слабого железа в 90-х годах? Вспомните NFS High Stakes, Quake 3, Unreal или Jurassic Park: Tresspaser — все они, несмотря на простоту технологий, выглядят сногсшибательно - в них были и зеркала, и отражения на металлических поверхностях, и продвинутый статический свет с рейтрейсингом, и даже четкие тени...
...но немногие знают, что на видеокартах уровня 3dfx Voodoo 2 это достигалось путём использования хитрых техник, хаков и особенностей видеокарт тех лет. В этой статьи мы с вами из первых уст рассмотрим такие техники на практике — ведь одним из моих хобби является написание 3D-демок и игр под ретро-железо.
О железе
Конечно начать рассказ стоит с железа, на котором работали 3D-игры в те годы. Среднестатистический компьютер в середине девяностых состоял из:
Процессора — Intel 386/486 на частоте 16-50МГц, и его аналогов от AMD/Cyrix в сборках попроще, а Pentium 66 и 133 можно было встретить в моделях побогаче.
Оперативной памяти — 4-8 мегабайт в бюджетных сборках и 16-32 в более богатых
Видеокарты — простой VGA-адаптер без ускорения 2D, или же 2D-ускоритель типа S3 Trio с поддержкой аппаратного блиттинга. 3D ускорять такие видеокарты не умели.
HDD — от ~100 до 500МБ на бюджетных сборках, и до ~1ГБ на некоторых дорогих.
О 3D-ускорении в домашних ПК тогда можно было только мечтать, однако многие разработчики всё равно делали полноценные, пусть и несложные по графике 3D-игры и демки — просто в них отрисовкой мира занимался процессор. Такой подход назывался Software rendering и стал возможен благодаря развитию FPU в x86-процессорах — диковинке, о которой большинству чипов на ARM/MIPS оставалось только мечтать (FPU не было даже в PlayStation 1).
У софтрендеров были как плюсы, так и минусы. Главным плюсом можно отметить то, что в отличии от ранних видеокарт, у них не было фиксированного конвейера и разработчик мог реализовать отрисовку именно так, как он хочет — иными словами, он мог встроить «шейдеры» прямо в функцию растеризации треугольника. Другим важным плюсом я считаю то, что рендер имел «бесплатный» прямой доступ к буферу кадра, что позволяло реализовывать разные пост-эффекты по типу LUT, блюра или капель дождя на стекле.
Пример реализации дождя в Дальнобойщиках. Хотя именно в Д1 могли реализовать не пост-эффектом, а полупрозрачной текстурой-маской, которая формируется процессором, или вообще через стенсильный буфер.
Но были и минусы, и самый важный — огромная нагрузка на процессор, которая росла обратно пропорционально разрешению игры. В 320x200 Pentium 66 ещё мог кое-как справиться с простой сценой без особого овердрава (как в Quake 1), но на 640x480 и 800x600 FPS падал в однозначные числа. Второй минус происходит из первого — отсутствие фильтрации и перспективной коррекции текстур во многих играх, из-за чего текстуры полов и стен под определенным углом начинают искажаться.
Что тако Overdraw?
Overdraw — процент отрисовки поверхностей, которые перекрывают уже отрисованный ранее фрагмент.
Простой пример — мы сидим в доме и смотрим в стену. Если игра нарисует сначала ландшафт за домом, затем забор, затем качели за забором, и только потом стену перед нашим носом — это уже Overdraw. Некоторые игры умудряются рисовать за кадр практически весь уровень и это сильно бьёт по филлрейту.
В Quake-подобных такого не было, поскольку там работал хитрый PVS-куллинг, который отсекал даже те стенки, которые сейчас находятся прямо за стенкой на которую мы смотрим.
Гигагерцовый Vortex86 уже не вывозил Quake 1 в 1280x1024 даже под DOS!
Первой 3D-видеокартой на ПК была STG2000 (также известная как Diamond Edge 3D), выпущенная в 1995 году на чипе NV1 — который по совместительству был первым видеочипом Nvidia. На практике, NV1 был целым мультимедийным комбайном: в одном чипе скрывался 3D-ускоритель, 2D-ускоритель, аудио и MIDI-контроллер, и даже контроллер геймпадов Sega Saturn. И всё бы хорошо, но видеочип от Nvidia оперировал не привычными треугольниками, а квадами с хитрым способом представления текстурных координат — портирование 3D-игр на «треугольных» софтрендерах было слишком сложным, требовало переделывание всей геометрии (а значит и переобучения 3D-моделеров), из-за чего большинство разработчиков решило не заморачиваться и NV1 провалился, чуть не обанкротив молодую Nvidia.
Фото с сайта Vintage3D
Всего через год, в 1996 году, появилась первая 3dfx Voodoo, которая перевернула мир 3D-игр на ПК с ног на голову. Это был первый полноценный 3D-ускоритель, который мог реально разгрузить процессор и рендерить игры в куда большем разрешении с приличным FPS — можно было замахнуться не только на 640x480, но и 800x600 при 16-битном цвете! Главным нюансом было то, что 3dfx Voodoo был именно ускорителем — то есть чисто технически, он умел только растеризовывать треугольники, интерполировать цвета и наносить на них текстуры, и накладывать туман: у него не было ни вершинного конвейера, ни клиппинга, ни освещения — всё это делал процессор. Но под Voodoo было легко программировать, у него был понятный API Glide, а ещё он стоил относительно приемлемых денег — что и сделало 3dfx Voodoo стандартом на рынке конца 90-х.
3dfx Voodoo 1 с одним TMU. Фото с TechPowerUp
Однако во времена первой 3dfx Voodoo, игры были сильно ограничены в плане графики из-за конвейера. Дело в том, что Voodoo 1, как и ViRGE с ATi 3D Rage, умели наносить лишь одну текстуру за проход отрисовки — из-за чего для рендеринга уровней с освещением приходилось рисовать сцену два раза — первый проход с основной текстурой, второй с лайтмапой в режиме альфа-блендинга с смешиванием, а если были ещё и detail-текстуры — то добавлялся и третий проход! Учтите что GL/D3D были ещё глупыми, и на каждый такой проход приходилось снова и снова трансформировать и клипать геометрию что также било по процессору.
Решением стало появление текстурных комбайнеров в ~1997-1998 году, которые были прямым предком пиксельных шейдеров. В зависимости от числа текстурных юнитов, видеокарта могла смешивать сразу несколько текстур за один проход отрисовки, и делать не только умножение — но и сэмплинг по маске (полезно было для реализации ландшафтов, ранняя вариация Splatmapping'а), умножение итогового цвета в два раза (для реализации эффекта псевдоблума), DOT3-бампмаппинг, Environment mapped bump-mapping (помните демки из конца 90-х с красивой отражающейся водичкой? Вот это и есть EMBM) и ещё много разных интересных эффектов.
Помните красивую шейдерную водичку из Morrowind? Она была сделана по сэмплу из D3D8. Ничего сложного в этой водичке нет: отражение наносилось с помощью проекционной матрицы для текстурных координат, а бамп маппинг работал за счет комбайнеров.
В 1999 году появился первый GPU — GeForce 256, который имел поддержку T&L и переносил вершинный конвейер и хранение геометрии на видеокарту, а в 2001'ом — GeForce 3 с поддержкой полноценных вершинных и пиксельных шейдеров. С того момента всё и изменилось: игры стали ещё более крутыми, графонистыми, появились новые техники освещения помимо затенения по Фонгу, и так мы с вами дошли до PBR с GI в реалтайме в любом мейнстримном движке...
Освещение
...но давайте же вспомним, как разработчики реализовывали свет на ограниченном железе в 90-х годах. Причём в некоторых играх освещение было достаточно сложным и реалистичным — взять как пример Quake и Half-Life, которые вероятно одними из первых адаптировали технику Radiosity-лайтмап в играх.
Сцена в Half-Life 1 без освещения
Radiosity — техника просчета глобального освещения с переотражениями и тенями за счет использования программного рейтрейсинга. QRAD бьёт всё сцену на так называемые патчи, считает для них влияние других патчей (в основном видимость и дистанцию) и симулирует аппроксимацию того, как свет проходил бы и отражался через них в реальном мире. То есть это именно физическая симуляция работы света, а не её красивая аппроксимация в виде шедоумап/Blinn-Phong в более современных играх с реалтайм освещением.
Главным нюансом Radiosity было то, что его невозможно было просчитывать в реальном времени до появления RTX-карт, поэтому освещение, рассчитанное этим алгоритмом, запекалось в специальные текстуры и затем наносилось на этапе рендеринга как второй слой.
Освещение в реальном времени реализовывалось схожим с современными играми способом, однако распределение света работало несколько по другому. В основе лежали всё те же алгоритмы затенения по Фонгу и ставший классикой N dot l, однако поскольку видеокарты в те годы не умели считать освещение сами и этим занимался процессор, программа (или OpenGL/D3D) рассчитывала свет для каждой вершины модели, а не пикселя, а полученный цвет от освещения записывался прямо в вершину.
В растеризации треугольников есть такое понятие, как барицентрические координаты — это интерполированные между тремя вершинами точки, которые описывают текущую «позицию» в системе координат треугольника и используются как для нанесения текстур и расчета попиксельного света, так и для интерполяции цвета. Как раз за счет интерполяции цвета между всеми тремя вершинами мы и получаем плавное освещение на каждом треугольнике, что в сумме даёт реалистичное освещение и блики, но только если у модели достаточная детализация. Такой подход называется затенением по Гуро.
Почему освещение в реальном времени использовали так мало?
Само по себе освещение в реальном времени не было прям таким уж тяжелым в те годы, но его использовали очень мало. Почему? Да потому что в графических API было строгое ограничение на число одновременных источников света для модели — всего 8 штук. Это нормально для модели персонажа, но мало для куска уровня, на который может влиять 10 источников света от фонарей/лампочек и т.д.
Решением проблемы стало появление Forward+ и Deferred-рендеринга, где свет считается в скринспейсе только для видимых пикселей. В мобильных играх повершинное освещение активно использовалось примерно до 2015 года.
Слева затенение по Гуро, справа попиксельный Blinn-Phong. Иллюстрация отсюда.
Интересным способом реализовывали и фонарики, а также свет от ночных фонарей и других источников света, которые дают конусообразное освещение. Тут было две техники: либо использование честных Spotlight'ов (поддержка которых была как в D3D, так и в OpenGL), либо использование проективных текстур. Концепция проективных текстур очень простая: если умножить WorldSpace-позицию треугольника на комбинированную ViewProject-матрицу проектора (т.е позиции с которой светит фонарик), то мы получаем ClipSpace-координаты в пространстве проектора (верные и для треугольника) — и таким образом можем нанести на него текстуру так, будто на него текстуру проецирует проектор из реального мира.
Здесь свет от лампы — лайтмапа, а свет от фонарика — как раз проецированная текстура
Таким же способом реализовывали и так называемые проективные тени — ближайший предок шедоумап. Там суть заключалась в том, что в отдельную текстуру рендерится объект с позиции источника света, а затем на дорогу или пол проецируется текстура тени, создавая мягкие, красивые и дорогие по VRAM тени задолго до PCF-шедоумап! Такой подход использовался в Mafia 1, Half-Life 2 и Street Legal Racing: Redline.
Здесь можно увидеть четкие и относительно мягкие тени, пусть и с альясингом. Для 2003 года это отличный результат.
В Portal 2 тоже активно использовались проективные текстуры. Из Вики valvedevelopernetwork.
Ну и конечно проективные текстуры использовались для реализации простых blob-теней во многих аркадах нулевых и 2010-х годов.
Пример блоб тени. Взято из мануала Unity.
Раз уж мы с вами заговорили о тенях, то нужно обязательно вспомнить о так называемых Shadow Volumes или стенсильных тенях. На рубеже 90-х и 2000-х, вопрос теней в реальном времени в играх стоял остро. Shadowmap уже, конечно, придумали, однако видеокарты всё ещё не умели сравнивать Z-координату в шедоумапах до появления специального расширения в OpenGL и D3D9. Более того, шедоумапы без фильтрации страдали от альясинга и очень сомнительного качества, пусть даже у них было полноценное самозатенение.
Пример Shadowmap из статьи на gamedev.ru. Кстати, сделаны только на FFP!
Shadow Volumes работали по другому принципу, нежели image-based техники и предлагали весьма интересный способ решения проблемы:
Сначала программа вытягивает силуэтные грани модели по направлению от источника света. Получившаяся модель называется теневым объёмом.
Затем рисуется теневой объем только с передними видимыми гранями, а в стенсил-буфер записывается значение 1.
Далее рисуется всё тот же теневой объем, но только с задними видимыми гранями, которые перезаписывают в стенсил-буфер значение 0 в местах не связанных с тенью.
После рисования всей сцены, рисуется серый полупрозрачный прямоугольник на весь экран с включенной проверкой по стенсилу. Если в стенсиле значение 0 — значит тени в этом пикселе нет, если 1 — значит есть, и таким образом мы получаем корректные, четкие и красивые тени без проблем, свойственных ShadowMap'ам.
Стенсильный буфер работает по тому же принципу, что и трафарет при рисовании на листе бумаги, только в 3D-графике. Он имеет такое же разрешение, как и сама игра, и каждый пиксель в нем является флагом «можно ли рисовать в этом месте или нет» для основного экрана (рендертаргета) игры.
Силуэт о которм мы говорили. Скриншот из статьи Семёна Козлова на gamedev.ru.
Стенсильные тени использовались во многих играх вплоть до ~2005 года. Самые известные — GTA San Andreas (там тени падали от фонарных столбов, пешеходов и машин) и F.E.A.R, однако поскольку просчет объёмов приходилось делать на процессоре для каждого источника света отдельно — техника устарела к появлению D3D10, и даже геометрические шейдеры, где их можно было считать почти бесплатно, волюмы не спасли.
Еще одна интересная техника тех лет — попытка симуляции атмосферного эффекта Godrays, или так называемые лучи Иакова: когда свет проходит сквозь узкие щели в пыльной и темной комнате, он выглядит как объемные лучи и многие разработчики делали красивый фейк этого эффекта за счет простейших вытянутых коробок с градиентной полупрозрачной текстурой. Эффектом пользуются еще с 90-х годов по сей день:
NFS High Stakes PS1
Прошу прощения за мыло :(
Отражения
Не менее интересно были реализованы и отражения. Сейчас в сети часто можно встретить жалобы игроков на то, что в современных играх отражения слишком примитивные, особенно это касается гонок, где в зеркалах автомобиля можно увидеть только статическую Reflection prob'у, а в зеркалах зданий разве что замыленную копию мира, отрисованную в рендертаргет низкого разрешения. В пример игроки приводят игры из нулевых и даже девяностых, где отражения были честными!
В GTA в зеркало можно было стрелять из снайперки и даже самого себя пристрелить. Многие думают что за зеркалом прячется клон игрока в виде отдельного игрового объекта, однако на практике снайперка — единственное оружие в игре, где пуля реально является объектом, а не рейкастом и она просто попадала по хитбоксу игрока сразу в тот момент, как была выпущена.
Секрет таких отражений в использовании всё того же стенсил-буфера. Игра сначала рисует объект зеркала с записью маски «зеркальных» пикселей в стенсил, затем рисует мир еще раз задом-наперед относительно зеркала и с помощью стенсилбуфера обрезает все пиксели, что попадают за пределы зеркала. Да, да, эта техника буквально строится на концепции «зазеркалья» и использовалась даже в Duke Nukem 3D 1995 года!
Скрин из статьи на Хабре.
Однако у техники есть куча минусов: например с ней зеркало может быть только прямой формы, никаких отражающихся чайников и кузовов автомобилей. Кроме того, она требует реализации адекватного отсечения по перекрытию, иначе в отражении еще раз может отрисоваться половина мира и уронить FPS (самый простой способ — поделить уровень на комнаты и рисовать только ту комнату, в которой находится игрок, в таком случае зеркала превращаются в частный случай порталов). Но многие игры использовали ее для воды до появления нормального рендера в текстуру в качестве альтернативы screenspace-отражений.
Для кузовов автомобилей и других не-плоских объектов применялась иная, довольно хитрая техника. До появления кубических карт текстур (из которых сэмплить можно в «3д» координатах, которые в свою очередь можно посчитать от угла взгляда на объект) активно применялась техника сферических карт отражений. Выглядели они как 360 фотографии:
Поскольку геометрию трансформировал процессор, на этапе перемножения вершин на MVP-матрицу он генерировал новые текстурные координаты, которые брали нормаль вершины в пространстве камеры, делили его на 2 и прибавляли 0.5, дабы перевести из View-space в Normalized-space. Получалось что координата идеально огибала сферу и делала красивые отражения. Бонусом эту же карту можно было использовать в качестве неба (которое анимировалось путем того, что сфера прсто крутилась вокруг своей оси) — и таким образом мы получали статический Reflection-probe.
На скриншоте из NFS Hot Pursuit 1 можно увидеть отражения не только на автомобилях, но и на дороге. Техника была по сути «бесплатной» на видеокартах с мультитекстурированием — как например на 3dfx Voodoo 2.
Анимация
Кроме того, думаю многим читателям будет интересно узнать о том, как в те годы делали анимацию. Ни для кого не секрет, что в подавляющем большинстве современных игр для анимации используется скиннинг — то есть модель разбивается на иерархию (кости), где трансформация каждого элемента зависит от трансформации родительского элемента. Это очень удобно для гуманоидных персонажей, поскольку позволяет легко одевать персонажа в броню или положить ему в руки пушку, а также позволяет использовать одну анимацию на десятках разных персонажей.
Однако до появления скиннинга существовала еще одна очень похожая система, называемая Object Transform. Работает она по тому же принципу, но положение объектов зависит не от костей и их весов, а от позиции/поворота самих объектов. Иными словами, персонажи собирались просто из набора кусочков — руки, локти, торс, ноги, и всё анимировалось отдельно. Помните угловатых персонажей в GTA III и Quake 3? Вот это оно и есть :)
Обратите внимание на сочленения — видно что колени и бедра как бы впиваются в таз персонажа, поскольку это отдельные объекты. Модель Томми выглядит естественно благодаря использованию скиннинга. Скриншот с gtaforums.
Помимо Object Transform, в те годы также активно использовался морфинг. Это самая простая система анимации, которая «запекает» позиции вершин для каждого кадра отдельно, после чего вершины интерполируются движком для создания плавности. Несмотря на возможность реализации интересных эффектов по типу реакции на взрывы и лицевой анимации, она является самой неэффективной, поскольку занимает много памяти и не умеет ретаргетинг (применение одной анимации к нескольким моделям). Кроме того, с этой системой сложно положить пушку в руки персонажу, из-за чего во многих играх оружие было неотъемлемой частью персонажа.
В Quake можно было отключить интерполяцию и анимация монстров становилась дерганной и резкой. В современных играх этот способ тоже используется для объектов вдалеке - для экономии числа вычислений в вершинном шейдере.
Однако у этой системы есть неоспоримый плюс — нулевая нагрузка на процессор, благодаря чему ее использовали в Quake 1 и 2, и иногда используют для некоторых особых случаев в современных играх.
❯ Заключение
Вот такими техниками пользовались разработчики игр в 90-х годах. Многие из них используются и по сей день — например лайтмаппинг, шедоумапы, морфинг и PVS никуда не делись, и все эти техники за последние 20 лет сильно прокачались. А вам интересно было узнать о том, как работали графические фишки в играх прошлых лет? Стоит ли писать подобный материал для 2D-игр (там выйдет поменьше, но тоже есть о чем рассказать)? Пишите свое мнение в комментариях!
К слову можно считать эту статью предисловием к будущему материалу о разработке 3D-игры для ретро-компьютеров с нуля, без каких либо движков! В качестве тестовых машин, мне было бы интересно пощупать что‑то ещё из семейства Nvidia ION/Radeon Xpress Graphics/VIA Chrome9 HC/Trident Blade. Если у кого‑то есть всякие интересные тонкие клиенты или ноутбуки на прикольном железе, или что‑то AMD'шное времен 939/462 — пишите в ЛС, обязательно сделаю контент!
Олды блога могут вспомнить мою демку про самолетики. Возможно скоро будет статья про куда более крутую игру для ретро компьютеров и возможно даже PSP и PS2!
Ну а я надеюсь, что вам было интересно. Подписывайтесь на блог, чтобы не пропускать новые статьи каждую неделю! А если вам интересна тематика ремонта, моддинга и программирования для гаджетов прошлых лет — подписывайтесь на мой Telegram‑канал «Клуб фанатов балдежа„, куда я выкладываю бэкстейджи статей, ссылки на новые статьи и видео, а также иногда выкладываю полезные посты. А ролики (не всегда дублирующие статьи) можно найти на моём YouTube канале.“»
А если вы хотите что‑нибудь подарить из железа и увидеть о нём статью — пишите мне в Telegram, Max или ВК. Меня очень интересуют самые разные гаджеты: начиная от игровых консолей и любых связанных с геймингом устройств, телефонов, смартфонов, КПК, заканчивая ретро‑компьютерами и ноутбуками. Кто знает, может героем следующейподобной статьи окажется ноутбук из 90-х?:‑)
Особенно сильно я люблю ковыряться в ноутбуках из начала 90-х. Источник
После обзоров устройства не продаются, а остаются в моей коллекции. Когда‑нибудь я хочу сделать музей, где к каждому устройству можно будет приложить QR и почитать мою статью. Кто знает, вдруг на следующей неделе я также подробно расскажу про девайс из вашей юности?:)
Кстати, у меня есть GameBoy Advance SP, под который я очень хочу написать игру. Однако мой экземпляр был залит водой и кофе. Может у кого‑то есть донор с дохлой платой, откуда я смог бы взять контроллер питания? У меня AGS-101.
В апреле 2023 года мы запустили необычный для нас проект - ремонтный сервис нового формата. Первого клиента нашли только в начале лета, но уже с первого проекта удалось достичь запланированной прибыли. Расскажу, как мы создали современную компанию для поколения "новых денег", которые не готовы к тому, что прораб будет класть половину стоимости ремонта себе в карман, а сроки растягиваются без ограничений и почему это оказалось прибыльным бизнесом.
Как всё начиналось
Конец 2022 года. Я работал менеджером проектов в крупной IT-компании, мой друг Антон - тимлидом в другой известной технологической компании, а Дима занимал позицию backend-разработчика в третьей. У всех стабильные зарплаты, интересные проекты - жизнь шла своим чередом, пока Антон не решил купить квартиру в новостройке.
После долгих поисков он остановился на 85-метровой квартире в ЖК около метро Саларьево - современный комплекс с подземным паркингом и зелёным двором. Как типичный программист, к поиску ремонтников он подошёл педантично: изучил десятки предложений, провёл собеседования с кандидатами. В итоге выбрал бригаду с Авито с "15-летним опытом работы".
Денис (это я:) - один из создателей сервиса
За время ремонта у Антона мы с Димой часто приезжали к нему - помочь, посоветовать, и просто поржать над очередными косяками строителей. У нас обоих за плечами был серьёзный опыт управления командами, внедрения процессов, автоматизации и контроля качества. И знаете что? Нам казалось, что главное в ремонтном бизнесе - это грамотный менеджмент и организация процессов. И на самом деле, все примерно так и есть.
День когда мы решились
Если честно, мысли о собственной ремонтной компании появились у нас задолго до этого случая. В IT-тусовке часто шутят, что в России есть три верных пути: вебкам, айти и стройка. И мы, насмотревшись на хаос в ремонтной сфере, не раз обсуждали возможность открытия своей “фирмы”.
Мы выделили основные проблемы, которые должны были исправить с помощью своего сервиса
Первое - это постоянно растущие сметы и "задранная стоимость" на работы. Ну вы серьезно? Почему для меня ремонт в квартире должен чем-то отличаться от заказа носков на Вайлдберриз? Почему-то там нет такого, что цена носков может вырасти в 2 раза за время доставки. А тут начальная смета в 1 миллион превратилась почти в 1,7! И каждый раз это "ой, тут стены кривые", "ой, а тут придется докупить", "ой, а это мы не учли".
Второе - это бесконечно растущие сроки. Это еще хуже. Как люди с опытом в менеджменте, мы прекрасно знали, что растущие сроки – это всегда раздолбайство управления и ленивые рабочие. Но тут было чуть посложнее, об этом чуть позже. У Антона обещали сделать за 4 месяца, а в итоге растянули на 8.
Третье - хитрые прорабы. Это вообще отдельная история. Когда прораб говорит "давайте я сам закуплю материалы, у меня скидки", знайте - он накинет сверху процентов 30-40. А еще есть "срочные работы", которые почему-то не были учтены в смете. И конечно, вечная классика - экономия на материалах: заказываешь одно, а привозят что-то попроще, но по той же цене.
Целевая аудитория
А параллельно с этим мы определили нашу целевую аудиторию. Чтобы выделиться на конкурентном рынке, мы решили ориентироваться на молодых IT-специалистов и представителей “современных” профессий, живущих в Москве в возрасте 30-40 лет. Тех, кто покупает квартиру в новостройке и привыкших к хорошему сервису. Кто не готов, что его будут водить за нос и разводить на деньги.
Что с ценой?
При этом очень важно было держать цены НИЖЕ, чем у других компаний, иначе на рынке просто не выжить. В премиум сегмент нужен "пропуск" в виде большого количества работ для определенного типа клиентов, поэтому нам было понятно, что "брать" много - пока что, непозволительно. Но мы нашли способ: автоматизация процессов, прямые поставки материалов, четкое планирование работ и никаких скрытых накруток. Но несбыточной мечтой это покажется только обывателю: на самом деле сервис и правда должен быть таким, вообще любой.
История первого заказа летом 2023 года
А теперь самое интересное - наш первый заказ. Это была отдельная эпопея. Начало лета 2023, звонок от потенциального клиента.
Первый клиент оказался прямо "в яблочко" - тоже айтишник (пришедший из первой контекстной рекламы в яндексе запущенной на 20 тыс.), работал в крупной компании, купил двушку в панельке 80-х годов. Причем достаточно убитую - стены местами с дырками, старый паркет, трубы помнящие еще Горбачева... Но нам, если честно, даже повезло что первый объект был не новостройкой. На старом фонде учиться легче - там хотя бы есть на что посмотреть, в отличие от новостроек, где все "сюрпризы" спрятаны в голых стенах.
Дырка в стене была ДО демонтажа. Клиент купил квартиру с "убитым" ремонтом подешевле, в надежде сэкономить
Как мы мастеров искали
Начали с поиска бригады. Вот тут был первый холодный душ - оказалось, что найти классных специалистов без своей наработанной базы очень сложно. Мастера хорошего уровня либо уже при деле, либо просят космос. А те, кто сразу соглашается - ну, вы понимаете. За две недели провели больше 10 собеседований (назовем их так😊). Причем процесс собеседования мы сразу сделали "как в айти" - тестовые задания, проверка портфолио, разбор технологий работы. Бригадиры были в шоке - некоторые прямо уходили с собеседований, когда мы начинали распрашивать про технологии укладки плитки и показывали фотографии косяков с объектов с форумов.
В итоге нашли бригаду - два брата, которые до этого работали в крупной ремонтной компании, но ушли потому что устали от "серых" схем – по отношению К НИМ ЖЕ. В основном - задержки з/п и прорабы.
В том же санузле, но уже после демонтажных и некоторых черновых работ. Братья очень помогли нам с первым ремонтом, кроме этого, они дали нам контакты проверенных рабочих, которые впоследствии делали ремонт вместе с ними
Купили первый комплект инструмента - это отдельная история на 100 тысяч рублей. Ещё сразу купили свою тепловую пушку - это из необычного. Большинство компаний привлекают "потолочников" на субподряд. Мы захотели сразу начать формировать свой инструментарий для будущих сотрудников. Зато потом эта закупка очень помогла - не зависели от того, что принесет с собой бригада. Позже, когда начали расширяться купили свою штукатурную станцию. Это, на удивление, было самой выгодной покупкой.
Первые сложности
Начали ремонт. И тут поехало... Первый сюрприз - когда начали снимать старую штукатурку, оказалось что стены буквально рассыпались. Плюс под старым линолеумом в коридоре нашли деревянный пол с насыпью под ним.
Тут был первый серьезный разговор с заказчиком. Объяснили ситуацию, показали все фотки, рассказали что сколько будет стоит в соответствии с прайс-листом. Тут очень помогло то, что мы сразу взяли за правило - фотографировать вообще все. Каждый этап, каждый слой, каждую трубу.
С материалами тоже было весело. Первый месяц я лично объездил все крупные строительные магазины в радиусе 30 км. Искал поставщиков, договаривался о скидках. Помню, как чуть не купили партию плитки по "супер-выгодной" цене, а потом выяснилось что это условно "второй сорт" - брак, который продают через "серые" схемы.
Cделали систему закупок: (Первый заказ полностью сделан из материалов, купленных в Петровиче и на маркетплейсах). Бригада присылает список - мы проверяем цены у 3-4 поставщиков и заказываем где дешевле.
Мы сразу внедрили систему ежедневных фото-отчетов и еженедельных созвонов. Плюс сделали чат в телеграме, куда скидывали все чеки, все накладные, все фотки. Такая прозрачность очень помогла - клиент видел, что мы не химичим с ценами и не пытаемся на нем нажиться.
В итоге сделали.
Братья советовали нам недостающих специалистов по ходу ремонта и постепенно, укладываясь в срок мы сдавали клиенту новый и новый этап. Честно, это было не так сложно как казалось в начале, но нужно понимать, что на самом деле, проблем по ходу ремонта и мелких трудностей было ОЧЕНЬ много, просто мы как люди привыкшие, понимали что сейчас это и есть наша работа , за которую нам платит клиент - взять на себя все заботы по ремонту. Но серьезных форс-мажоров не возникало, максимум понервничали, когда внесли предоплату потолочникам, которых нашли по объявлению, после чего они явно начали задерживать сроки, но в итоге, все обошлось. Нам и до этого было ясно, что чем быстрее мы соберем свой , хотя бы, небольшой штат, тем лучше и выгоднее. Но для этого надо быть уверенным в постоянном потоке заказов, что мы на тот момент, не могли себе позволить.
Главное - мы поняли как надо и как не надо. Все закупки только через нас. Все договоренности - только в письменном виде.
По деньгам первый проект получился условно в ноль - не считая наших вложений в инструмент и рекламу. Но мы получили бесценный опыт, наработали связи с поставщиками (если это можно назвать так пафосно) и главное - поняли, что наша модель рабочая, хотя на тот момент не были в этом до конца уверены. Наши сомнения развеяло лето 2023 года.
1/5
Наша первая работа, которую нам помогли закончить "братья", советуя недостающих мастеров на каждый этап работ. Цена работ для заказчика вышла 750 000 рублей. Летом 2023 года, за аналогичный объём работ авитовские бригады просили до миллиона(!)
Сайт и маркетинг или как мы общаемся с клиентами ещё до первого звонка
Кстати, про сайт. Первую версию мы сделали за пару вечеров на Тильде - максимально простую: фотки, цены, контакты. Знаете что? Он у нас до сих пор такой – но с значительно изменённым UI/UX. Я иногда ловлю себя на мысли - вот мы же, можем сделать что-то крутое, с личным кабинетом, онлайн-сметами... Но потом смотрю вордстат, статистику и думаю - а зачем? Клиенты находят нас, звонят, все работает. Иногда лучшее решение - самое простое.
1/2
На главной странице "в лоб" о том кто мы, наши конкурентные преимущества, специализация и примеры работ
1/2
Сразу "ярко" заявляем о действующих скидках. Кроме этого, простым текстом пытаемся общаться с клиентом и убедить его в нашем сервисе, пока что, не общаясь с ним лично - конверсия в лиды увеличилась значительно, но важно, чтобы все предложения были реальны
А вот ворд стат. За последний месяц нас искали 680 раз не считая статистики переходов по прямой ссылке. Доказательство того, что дорогой сайт не нужен. Наш стоит 6 000 рублей в год, а сделан за 20 часов
Примеры лучших работ как маркетинговый инструмент
1/4
Оставляли подобные фото с примерами работ и вотермаркой в отзывах к жилым комплексам. Отличный маркетинговый инструмент - сразу определяем нужную ЦА и бесплатно заявляем о себе. Точно знаем, что минимум один клиент был из отзывов яндекс - поэтому работает
Как мы масштабировались и разбирались в проблемах ремонта
Теперь про то, как мы начали разбираться в проблемах ремонта и потихоньку расти. Знаете, за этот год мы много думали - почему вообще ремонтные компании задерживают сроки и не могут сделать работу по первоначальной цене? И нашли несколько интересных моментов.
Первое - сроки. Тут даже копать глубоко не пришлось - сам рынок сформирован так, что клиенты просто привыкли к задержкам. "А, ну это ремонт, тут всегда так" - классическая фраза. Благо тут нам помог опыт организации процессов из айти. Четкие спринты, ежедневные отчеты, календарный план. Работает как часы, если не филонить.
Второе - вечно растущая цена. Вот это было сложнее. В новостройках еще можно точно посчитать, а во вторичке... Но благодаря наработкам этот вопрос стал гораздо яснее. При правильно организации процессов, непредвиденные работы лучше оплатить из своих средств, чем терять клиента. Только так и делается сервис.
Третье - больная тема - воровство на материалах. Тут мы конкретно закрутили гайки. Завели строгий учет, до последнего мешка штукатурки. Прорабам сразу сказали - любая жалоба от клиента на нечестность - это конец сотрудничества. Знаете что самое смешное? Когда начали работать честно, оказалось что маржа все равно хорошая. Просто многие привыкли что надо обязательно накручивать еще и еще.
Неприятные особенности прозрачного сервиса
Большинство компаний, которые начинают работать честно, быстро сдуваются. Потому что сам потребитель привык к плохому сервису. Приходишь к клиенту, говоришь "мы все сделаем четко по смете и в срок", а он смотрит с недоверием - "да ладно, так не бывает". Сейчас в 2024-м ситуация получше, но рынок все еще очень консервативный.
Продвижение или как мы брали “Новую Москву”
Наш любимый Прокшино в Новой Москве - в 2024 году, это самый перспективный район в котором покупатели - наши потенциальные клиенты
Расти начали потихоньку. Сначала сделали специальные цены для новой Москвы - там как раз много новостроек, где мы хорошо научились работать. Потом пошли заказы по рекомендациям. Когда я говорю “пошли” заказы нужно понимать, что это 1-3 заказа в месяц стабильно, что на самом деле, тогда казалось не мало. С рекламой экспериментировали забавно - оставляли визитки в подъездах (да, просто старые добрые визитки!), ставили рекламные ящики у офисов продаж в дни выдачи ключей.
Был случай, когда на одном ЖК поставили такую стойку, а через неделю риелтор оттуда сам позвонил и предложил взять себе процент, если клиент пойдет к нам.
Сейчас берем 2-5 заказов в месяц, (летом было 5 в месяц, если разделить). Большинство клиентов приходят по рекомендациям. Штат за год работы постепенно собрали свой. Трудоустроено 14 сотрудников. Если их не хватало (как летом), привлекали мастеров с которым уже работали раньше, по договору ГПХ. Просили всех регистрировать самозанятость или ИП – это было тяжело, но зато честно платим налоги по Доходы – расходы.
Как устроен процесс
Знаете, что самое сложное в ремонтном бизнесе? Нет, не поиск клиентов и не работа с материалами. Самое сложное - это выстроить систему, которая будет удовлетворять запросы клиента и при этом позволит компании зарабатывать. Мы долго думали над этим и придумали несколько интересных решений.
Начнем с оплаты. Если посмотреть на московский рынок, там забавная система - вроде бы без предоплаты, но это только за первый, самый дешевый этап. А потом - бац, и клиент должен внести 50% от всей суммы. И попробуй не внеси - бригада уйдет на другой объект. Мы сделали иначе: разбили весь ремонт на этапы (их получилось 12 для капитального ремонта), и берем предоплату только за текущий этап. То есть клиент всегда понимает, за что именно платит, и может контролировать процесс.
С договорами тоже интересно получилось. Сначала делали один стандартный, а потом поняли - люди разные, запросы разные. Сейчас у нас три варианта: просто на работы (для тех, кто сам хочет закупать материалы), на работы плюс материалы (самый популярный), и полный пакет с дизайн-проектом. Кстати, про дизайн - мы не держим штатных дизайнеров, работаем с фрилансерами. Так получается и дешевле для клиента, и качество выше - каждый дизайнер специализируется на своем стиле.
Первый образец договора, который ставили на сайт
Расчёт стоимости
Расчет стоимости работ - отдельная история. Когда начинали, было очень сложно. На рынке нет четких цен - один мастер берет 3 тысячи за квадрат стяжки, другой - тысячу, третий вообще говорит "ну давайте посмотрим по факту". Сначала работали по среднему прайсу, но часто уходили в минус по отдельным этапам - то бригада дороже встанет, то материала больше уйдет. Сейчас, когда появились постоянные сотрудники, стало проще - мы точно знаем их расценки и можем давать фиксированную цену.
Материалы
Выяснилось, что в крупных магазинах часто дешевле в сравнении с рынками стройматериалов, особенно если ловить акции. Плюс появились маркетплейсы - там вообще красота по ценам. И главное - полная прозрачность, клиент сам видит все позиции. Если находим материал дешевле - возвращаем разницу. Был случай, когда на керамограните сэкономили клиенту почти 30 тысяч - просто потому что, нашли ту же плитку в другом магазине по акции.
Как работаем сейчас
Сейчас берем 2-5 заказов в месяц, зависит от сезона и сложности объектов.
Процесс обкатан до мелочей: сначала выезд на замер, потом предварительная смета. Если клиента все устраивает - детальный расчет и договор. Дальше начинается самое интересное - согласование материалов, закупки, работа.
Особенности продвижения и подводные камни
С продвижением интересно получилось. Сначала, как все, полезли на Авито. И знаете что? Не зашло. Там своя специфическая аудитория и конкретно у нас, с Авито “ не пошло”.
Свежий пример страницы с авито в категории "Ремонт квартир". Как клиенты определяются с подрядчиком среди тысяч однотипных объявлений - хороший маркетинговый вопрос, который мы решаем, после чего полноценно "зайдём" на авито
Пробовали контекстную рекламу яндекс - тоже так себе результат. Клики дорогие, конверсия низкая. Зато отлично зашла таргетированная реклама в Instagram* (*запрещен в РФ) и реклама в районных пабликах ВКонтакте. Особенно в группах новостроек в ТГ - там как раз наша целевая аудитория.
Но главный канал - сарафанное радио. Причем работает оно интересно. Всегда знаешь , от кого пришли заказчики и проще выстраивать коммуникацию. Есть клиент - он доволен, рассказал коллегам - и понеслось. За три месяца получили еще два заказа от сотрудников той же компании. Выходит такой “корпоративный ремонт”)))
Были, конечно, и провалы в продвижении. Пытались работать с риелторами - предлагали им комиссию за клиентов. Не пошло - у них свои схемы, свои бригады. Пробовали давать рекламу в лифтах, входных группах - тоже мимо, ни одного обращения, но тут скорее из-за того что эта реклама буквально на день.
ИТОГ
Сейчас мы существуем уже чуть больше года и в целом смотрим на ремонтный рынок не так, как раньше. За это время мы сделали несколько важных заметок для себя:
Оказывается, большинство проблем в ремонте - это не технические сложности, а банальная организация процессов. Когда все четко спланировано и каждый знает что делать - даже самый сложный ремонт идет как по маслу.
Мы поняли, почему многие компании "химичат" с ценами и сроками - просто потому что не умеют нормально считать и планировать. Когда все процессы выстроены правильно, можно работать честно и все равно оставаться в плюсе.
Самое сложное - это не найти клиентов или научиться делать ремонт. Самое сложное - это переломить привычку рынка работать "по-серому". И заказчики, и исполнители настолько привыкли к непрозрачным схемам, что честный подход вызывает удивление.
Мы до сих пор учимся. Каждый объект - это новый опыт и новые вызовы (особенно по части коммуникаций с клиентами). Но главное - мы видим, что наша модель работает и пока что, нам удается держать ценник немного ниже рыночного, за счёт того что до сих пор не имеем в найме маркетологов, IT, отдела продаж и менеджеров по продажам. Стараемся нанимать именно рабочих, все остальное можно отдать на аутсорс.
Уважаемые пикабушники, были рады составить для вас эту статью! С любовью, СК Комната
...которая работает на первых Android-смартфонах в мире, компьютерах из 90-х и даже Mac'ах! Часть 2.
Иногда у меня лежит душа просто взять и написать какую-нибудь небольшую игрушку с нуля, без использования готовых движков. В процессе разработки я ставлю перед собой интересные задачки: игра должна весить как можно меньше, работать на как можно большем числе платформ и использовать нетипичный для меня архитектурный паттерн. Недавно я начал писать ремейк классических «танчиков» и в рамках серии статей готов рассказать о всех деталях разработки трёхмерной игры с нуля в 2025 году. Если вам интересно узнать, как работают небольшие 3D-демки «под капотом» от написания фреймворка до разработки геймплея — жду вас под катом!
❯ Предисловие
Ещё в начале этого года, мне взбрело в голову проверить насколько концепция «Write once, run anywhere» правдива. Все мы знаем, что Java достаточно обширно используется в Enterprise-секторе по типу банков, Android-гаджетах в качестве языка, на котором написано около 80% системы и даже в смарт-карточках, куда входят привычные нам SIM и банковские карты.
Изначально я хотел написать игру, которая работала бы не только на самых первых Android-смартфонах в мире, но ещё и на ретро-кнопочных телефонах, и при всём этом была 3D. В течении недели, я успел написать некоторые наработки для трёхмерной гоночки с примитивной физикой на основе «линий»:
В игре был мультиреднер для M3G и MascotCapsule... не хуже игр Fishlabs :))
Но затем я понял, что лишаюсь очень многих фич языка. Дело в том, что игры для Java-телефонов писались не столько на самой «джаве», сколько на её своеобразном диалекте. В мире C/C++ такой подход принято называть «C с классами», но в случае Java - подход заключался в написании большей части логики в одном-двух классах для улучшения производительности игры. Наследование, полиморфизм и абстракции на кнопочных телефонах использовать не рекомендуется. Кроме того, версия JDK в кнопочных телефонах была на уровне 1.3 — а значит, никаких дженериков и иных полезных фишек Java.
Про разработку игр для кнопочных телефонов я писал отдельную статью. Дабы не отвлекать вас от прочтения этой, ссылку оставлю в закрепленном комментарии :)
По итогу я решил сфокусироваться на относительно свежем HTC Dream — первом серийном Android-смартфоне в мире, который вышел в далёком 2008 году с Android 1.0 на борту. В нём используется уже не JVM, а своя виртуальная машина Dalvik с собственным байткодом и версией JDK — 1.5, да и процессор здесь значительно помощнее, а следовательно и куда больше возможностей для разработки!
Поскольку игру я разрабатываю и отлаживаю на ПК, у меня также есть отдельный билд и для ретро-компьютеров с GPU из 90-х и нулевых. И в рамках статьи, мы, конечно же, сделаем с вами практические тесты!
❯ Рендер
В первой части мы с вами закончили на том, что написали основу для игры — фреймворк, который включает в себя рендерер, менеджер ресурсов на слабых ссылках, некое подобие графа сцены с компонентной системой и загрузчик уровней. Но этого всё ещё мало для 3D-игры и, что самое важное, все эти модули ещё не оптимизированы.
Например, если грузить уровень «в лоб» и на каждый кубик выделять по отдельному игровому объекту, который «рисует сам себя отдельно» — мы быстро столкнемся с тем, что количество вызовов отрисовки (DIP'ов) превысит все разумные нормы. Для уровня в 16x16 блоков это уже целых 256 DIP'ов - а вкупе с другими танчиками и UI - не менее 260-270.
Самая базовая оптимизация в таком случае — это отсечение по пирамиде видимости (Frustum culling). Концепция простая: для отрисовки всего, что мы видим с вами на экране используется три матрицы размерности 4x4: мировая (позиция и поворот объекта в мире), вида (камера, позиция из «глаз») и проекции. При перемножении, они образуют так называемую WorldViewProjection-матрицу и если каждую вершину модели умножить на эту матрицу — то мы получаем её позицию в Clip-Space (или NDC) пространстве. Далее растеризатор берёт каждые три трансформированные вершины в качестве углов треугольника и отрисовывает их в рендертаргет - в нашем случае, это экран. Именно за счёт перспективной матрицы проекции и Z-буфера, мы с вами и получаем тот самый эффект трёхмерного пространства.
Если взять произведение матрицы вида с матрицей проекции и разбить её на плоскости, соответствующие каждой стороне мира (вверх, вниз, влево, вправо, вперёд, назад) — то путём выполнения простейшей проверки можно понять — находится ли точка мирового пространства в текущей позиции камеры:
Далее проверить попадает ли наш кубик или танчик в кадр — дело техники. Есть два подхода: подсчитать Bounding-sphere для модели (радиус относительно самой нижней и самой верхней вершины), или Bounding-box. В самом простом случае, можно обойтись проверкой самой нижней и самой верхней точки Bounding-box'а, однако в некоторых случаях такой алгоритм может давать сбой — например если уткнутся в «стенку» носом в игре:
publicboolean isMeshRendererInFrustum(MeshRenderer renderer) { float x = renderer.Parent.Position.X; float y = renderer.Parent.Position.Y; float z = renderer.Parent.Position.Z; Vector min = renderer.Mesh.BoundingMin; Vector max = renderer.Mesh.BoundingMax;
Конкретно в нашем случае, такая оптимизация помогает сэкономить около 100 DIP'ов и даёт неплохой прирост FPS. На Galaxy S3 с Mali 400MP4 мы получаем стабильные 60FPS, в то время как на Xperia Play — около 30... Что-ж, этого всё равно мало, тем более для смартфона, в котором GPU — кровный брат Xenos в Xbox 360...
Нарисовать 256 кубиков для GPU, даже мобильного — не проблема, особенно если они не бьют по филлрейту. Однако на классических мобильных GPU был строгий бюджет на число DIP'ов — в идеале не более 100, иначе FPS заметно просаживается даже на примитивной геометрии. Поэтому для оптимизации можно использовать технику батчинга: объединяем все кубики с одним материалом в сцене в одну большую модель и рисуем за один вызов DIPUP:
После этого, FPS поднимается до очень приятных значений - целых 45! Однако есть и обратная сторона: эта техника очень сильно бьёт не только по памяти, но и в случае динамического батчинга (танки ведь уничтожают кубики) - по процессору. Однако можно и далее оптимизировать этот алгоритм путём разбиения батчей на сетку, чтобы отсекать невидимые группы "кубиков" :)
Следующая тема — это материалы для поверхностей, описывающие внешний вид модели на экране. В первой статье я написал базовую систему материалов, которая оборачивала в себе набор рендерстейтов и парочку текстур: Diffuse и Detail. Но мало кто помнит, что ещё до шейдеров, в FFP был довольно мощный инструмент, именуемый комбайнерами. По сути, комбайнеры — это возможность задействования сразу нескольких текстурных юнитов для смешивания двух и более текстур за один вызов отрисовки.
Пример использования комбайнеров — плавное смешивание двух текстур на ландшафте с использованием маски. Эдакая вариация техники Splat mapping
Поэтому я решил написать загрузчик для материалов, описанных в простом текстовом формате по типу ini-файлов. В секции Texture описываются используемые текстуры, которые затем подгружаются из пула ресурсов, в RenderStates — напрямую указаны поля в классе Material, а в Combiners — очень-очень примитивная вариация на тему шейдеров!
Изначально я хотел сделать чтобы материалы описывали эдакий набор инструкций как «шейдеры» в Quake 3. Однако учитывая отсутствие лямбд в Java 1.5, реализация на интерфейсах (и тем более на рефлексии) не впечатлила своей производительностью и я решил сделать «программируемыми» только сами комбайнеры. Суть простая: отдельные псевдо-шейдеры реализуют интерфейс FixedFunctionShader и в теле метода onApply применяют необходимые операции над комбайнерами. При этом строго запрещается менять стейт самого графического API кроме биндинга текстур:
Затем при вызове отрисовки модели, рендерер выполняет «инструкции» для таких комбайнеров по одному и если нужно — откатывается до простой «однотекстурной» версии (драйвер GLES на Mali-400 и VideoCore IV не поддерживает комбайнеры, несмотря на то, что спецификация требует их поддержки). Получается довольно шустро:
if(GPUClass.QualityLevel >= com.monobogdan.engine.GPUClass.QUALITY_LEVEL_NORMAL) { for (int i = 0; i < Material.COMBINER_STAGE_COUNT; i++) { // Reset combiner state glActiveTexture(GL_TEXTURE0 + i); glDisable(GL_TEXTURE_2D); }
for (int i = 0; i < material.Shaders.length; i++) { Material.ShaderInstance instance = material.Shaders[i];
glActiveTexture(GL_TEXTURE0 + i); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glEnable(GL_TEXTURE_2D); instance.Shader.onApply(material, i, instance.Params); } } else { // Single texture fallback for very slow GPU's glActiveTexture(GL_TEXTURE0); setState(GL_TEXTURE_2D, material.Textures[0] != null); material.Textures[0].bind(); }
Наполовину кирпичный танк — видели ли вы когда-нибудь такой камуфляж? :)
Следующая тема — рендеринг текста. В более ранних статьях я обычно не парился над демками и просто рисовал текст нативными средствами системы в текстуру, а затем рисовал полноэкранный квад. Такая методика работает шустро на смартфонах, но очень тормозная на ПК и более того, такая текстура занимает слишком много VRAM! Однако чаще всего я использую так называемые битмапные шрифты, которые состоят из атласа — текстуры с «запеченными» буквами и информации о том, где какой символ в ней находится. Для генерации таких шрифтов я использую утилиту BMFont, а сам код рендеринга получается очень простым:
publicvoid drawString(BitmapFont font, Vector color, float x, float y, String str) { if(font == null) thrownew NullPointerException("font was null");
if(str == null) return;
int sz = font.Size / 2;
for(int i = 0; i < str.length(); i++) { char chr = str.charAt(i);
В целом, далее особо оптимизировать и нечего для рендерера. Инстансинга в FFP нет, шейдеров — тоже, а рендер идентичный и на Android, и на ПК. Поэтому имеем что имеем!
❯ Аллокации
Однако когда я начал отлаживать игру на смартфонах, я заметил резкие просадки кадров и абсолютно нестабильный FPS. При этом характер лагов был константный: раз в 2-3 секунды просадка в 20 кадров. Заглянув в logcat, я обнаружил что Dalvik постоянно вызывает GC (сборщик мусора) и блокирует все потоки на невероятные 16мс — даже для простейших объектов в «куче»! В зависимости от устройства, Dalvik выделяет от 8 до 32Мб памяти для каждого приложения - что очень немного!
В первой статье я рассказывал о том, что большинство объектов у меня мутабельные и предполагают аллокацию не в update/draw, а в конструкторе компонента. Это касается векторов, матриц и иных примитивных классов для различных расчетов — ведь в отличии от .NET, в Java нет Value-типов, которые можно выделить на стеке, кроме примитивов. Например, если в C# написать такой код для сложения двух векторов:
struct Vector3 { publicfloat X, Y, Z;
public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
publicstatic Vector3 operator +(Vector3 a, Vector3 b) { returnnew Vector3(a.X + b.X, a.Y + b.y, a.Z + b.z); } }
...
Transform.Position += Velocity;
То из-за того, что Vector3 — простая структура без ссылок на управляемые объекты, которая не требует контроля от GC, рантайм .NET выделит её на стеке, а не в куче и автоматически удалит при выходе из скоупа метода, где она использовалась. Если попытаться сделать такое в Java:
public static Vector3 add(Vector3 a, Vector3 b) { returnnew Vector3(a.X + b.X, a.Y + b.y, a.Z + b.z); }
То мы получим аллокацию для каждого объекта, вызывающий этот участок кода на каждый кадр. И когда придёт время вызывать GC — он обязательно тормознет игру и вызовет огромные фризы, прямо как в Minecraft на ПК. Главный нюанс здесь в том, что Dalvik оптимизирован под минимальное потребление памяти и поэтому начинает слишком часто вызывать GC, тормозя работу игры. В смартфонах с большим объёмом ОЗУ (хотя-бы 1Гб) таких проблем уже нет.
Но как я уже и сказал выше — мои игровые объекты и компоненты написаны так, чтобы не нагружать ни GC, ни кучу, но сборщик мусора всё равно продолжает тормозить игру, а значит нужно максимально экономить аллокации. Начав профайлить код, я обнаружил что огромное число аллокаций приходится на... итераторы! Да-да, та же самая проблема, что и в примере с векторами: даже несмотря на крошечный вес в памяти, итерации в каждом кадре засоряют хип и по итогу вызывают GC. Решение: перевести все индексированные списки на классический for:
for(int i = 0; i < GameObjects.size(); i++) { GameObjects.get(i).onUpdate(); }
// Second pass for late updates for(int i = 0; i < GameObjects.size(); i++) GameObjects.get(i).onLateUpdate();
И после этого, частота вызова GC наконец-то стабилизировалась!
❯ Ввод
Отдельный вопрос — это грамотная обработка ввода. Хочется чтобы наша игра поддерживала не только клавиатуру, но и геймпады, а на смартфонах — ещё и виртуальные джойстики. Чтобы не размазывать подсистему ввода в игре на 150 источников как в Unity, есть смысл её абстрагировать на некий виртуальный геймпад с необходимыми для игры кнопками: в нашем случае это стрелки и кнопка стрельбы.
Затем необходимо замаппить физические кнопки на наш виртуальный геймпад. Для этого, на смартфонах я сделал таблицу с маппингом, которая подходит для большинства игровых гаджетов: Xperia Play, игровых консолей на Android'е из 2012-го и даже смартфонов с аппаратными QWERTY-клавиатурами. И если захочется добавить возможность переназначения кнопок — это тоже не станет проблемой!
По итогу, у нас есть унифицированное управление на ПК и смартфонах, покататься в нашей демке можно даже на легендарной Xperia Play!
Для смартфонов без аппаратной клавиатуры, виртуальный геймпад пишется буквально за 5 минут. Главное — использовать относительные нормализованные координаты для адаптивности и учитывать Aspect Ratio устройства, который может быть разным:
Пришло время протестировать то, что мы успели с вами сделать за неделю. И сегодня в тестах участвует сразу несколько машинок: Asus eeePC 4G в роли «компьютера из 90-х», Sony Ericsson Xperia Play, iPhone 4S с нюансом и Samsung Galaxy Y Pro. Все гаджеты по своему хороши, имеют разные GPU и всех их объединяет статус легендарных.
Начинаем с SE Xperia Play 2011 года выпуска, который изначально позиционировался как игровой смартфон. По сути, Xperia Play - чуточку переделанный Xperia Pro, где QWERTY-клавиатуру заменили на геймпад, при этом аппаратная платформа почти всех "сонериков" 2011 года идентичная: чипсет Qualcomm MSM8250 с ARMv7-совместимым ядром Scorpio на частоте 1ГГц и GPU Adreno 205 (ребрендинг ATI Imageon Z430, на архитектуре Xenos), 512Мб ОЗУ типа DDR1 и 512Мб флэш-памяти. С смартфонами в те годы была такая же ситуация, как и с компьютерами в начале нулевых: прогресс был слишком быстрым и уже в 2012 году, Xperia Play не тянул многие свежие игры из-за слабенького процессора и GPU! Но в нашем случае, он показывает себя неплохо и стабильно тянет рендеринг уровня и танчика в 40-45 FPS... В играх на Unity3D, Adreno 205 таким результатом похвастаться не мог.
Переходим к iPhone 4S, который, как я уже сказал, с некоторым нюансом: это китайская реплика на Android. При этом довольно интересен тот факт, что у копии очень крутая IPS-матрица почти такого же разрешения (800x480 против 960x640), как и на оригинальном айфоне. Работает "клон" на базе чипсета MediaTek MT6515 2012 года выпуска с одним ядром Cortex-A9, работающим на частоте 1ГГц и GPU PowerVR SGX531 Ultra. Также в смартфоне установлено 256Мб оперативной памяти и 256Мб постоянной - в общем, типичный бюджетник тех лет. GPU от PowerVR - главное достоинство этого смартфона в плане гейминга, наша демка спокойно выдаёт 50-60 стабильных FPS. Я считаю что это прекрасный результат.
ERTY-клавиатурой, но и очень диковинным (и родственным Raspberry Pi) процессором Broadcom BCM21553 с одним ARMv6-совместимым ядром на частоте 832МГц и крайне необычным GPU собственной разработки VideoCore IV. Дело в том, что GPU в чипсетах Broadcom выполняет роль системного монитора и по архитектуре заметно отличается от классических видеоускорителей. По сути, это DSP с очень крутым векторным сопроцессором из-за чего его отчасти можно назвать софтрендером. Однако ранние драйвера для этого GPU были очень сырыми из-за чего большинство игр выдавали артефакты или работали очень медленно. Наша игрушка - не исключение, всего лишь 20 FPS при 240x320...
Переходим к довольно необычной машинке: Asus eeePC 4G. Первые модели легендарной линейки нетбуков отличались очень низкой ценой, довольно слабым и прожорливым процессором Celeron M 353 на архитектуре Dothan (прямой поток Pentium III Tualatin) и частоте 900МГц, встроенной графикой Intel GMA900 с поддержкой пиксельных шейдеров 2.0 и довольно небольшим объёмом ОЗУ в 512Мб типа DDR2. Здесь я проводил тесты на JRE 1.7 - и получил почти 60 FPS... за вычетом того, что раз в 3-4 секунды я получаю микрофризы и нагрузку на процессор в 80%. Однако сама JRE здесь не причём: такая высокая нагрузка связана с тем, что у GPU нет аппаратного вершинного конвейера и поэтому вся трансформация геометрии происходит на процессоре. Такой вот нюанс:
❯ Заключение
Вот такая статья о разработке 3D-игры с нуля у нас с вами получилась. Прошлые статьи в этой рубрике я писал в стиле туториала, но в этой я решил рассмотреть конкретные кейсы и архитектурные решения. И может она не настолько простая и понятная, как статья про разработку «самолетиков» или Top-Down стрелялки по зомби, думаю своего читателя она точно нашла! Если вам интересно, с кодом можно ознакомиться на моём Github.
А если вам интересна тематика ремонта, моддинга и программирования для гаджетов прошлых лет — подписывайтесь на мой Telegram-канал «Клуб фанатов балдежа», куда я выкладываю бэкстейджи статей, ссылки на новые статьи и видео, а также иногда выкладываю полезные посты и щитпостю. А ролики (не всегда дублирующие статьи) можно найти на моём YouTube канале.
Что думаете о таком формате статей?
Очень важно! Разыскиваются девайсы для будущих статей!
Друзья! Для подготовки статей с разработкой самопальных игрушек под необычные устройства, объявляется розыск телефонов и консолей! В 2000-х годах, китайцы часто делали дешевые телефоны с игровым уклоном — обычно у них было подобие геймпада (джойстика) или хотя бы две кнопки с верхней части устройства, выполняющие функцию A/B, а также предустановлены эмуляторы NES/Sega. Фишка в том, что на таких телефонах можно выполнять нативный код и портировать на них новые эмуляторы, чем я и хочу заняться и написать об этом подробную статью и записать видео! Если у вас есть телефон подобного формата и вы готовы его задонатить или продать, пожалуйста напишите мне в Telegram (@monobogdan) или в комментарии. Также интересуют смартфоны-консоли на Android (на рынке РФ точно была Func Much-01), там будет контент чуточку другого формата :)
А также я ищу старые (2010-2014) подделки на брендовые смартфоны Samsung, Apple и т. п. Они зачастую работают на весьма интересных чипсетах и поддаются хорошему моддингу, парочку статей уже вышло, но у меня ещё есть идеи по их моддингу! Также может у кого-то остались самые первые смартфоны Xiaomi (серии Mi), Meizu (ещё на Exynos) или телефоны Motorola на Linux (например, EM30, RAZR V8, ROKR Z6, ROKR E2, ROKR E5, ZINE ZN5 и т. п., о них я хотел бы подготовить специальную статью и видео т. к. на самом деле они работали на очень мощных для своих лет процессорах, поддавались серьезному моддингу и были способны запустить даже Quake!). Всем большое спасибо за донаты!
Дисклеймер: игра была написана как простенькая, но познавательная демка именно для PowerVR MBX и именно для Axim X51v. Именно поэтому здесь нет нормального Update-таймера, расчёта дельты времени, а игра прибита к константным временным отрезкам и величинам скорости!
Итак, как же игры подобного планы работают «под капотом»? По факту, обычно мы с вами никуда не едем: фоновые модели ландшафта и дороги просто скроллятся и телепортируются друг за другом, когда одна из частей уходят за экран, что создаёт эффект бесконечной дороги. И эта техника используется во многих играх! Что же касается машинок, от которых мы должны лавировать, то это не мы едем на них, это они едут на нас! По итогу создаётся эффект будто мы с вами куда-то едем и уворачиваемся от машинок, хотя на деле это не так!
Начинаем с реализации базовой вещи в архитектуре любой современной игры, а именно системы игровых объектов. В нашей игре нет необходимости в реализации сложного графа сцены с комплексной компонентной системой, или, например, ECS. Хватит классического линейного списка игровых объектов (который использовался, например, в Half-Life), по которому объект World проходится каждый кадр, вызывая необходимые функции для обновления состояния объекта и его отрисовки:
public abstractclass Entity { public Transform Transform;
foreach (Entity ent in entityRemovalList) Entities.Remove(ent);
entityRemovalList.Clear(); }
publicvoid Draw() { sky.Draw();
renderer.Draw();
foreach (Entity ent in Entities) ent.Draw(); }
Самым первым нашим объектом будет машинка игрока, которой можно будет управлять! Модельки я взял лоуполи со скетчфаба, вот ссылка на ВАЗ 21099 и VW Golf Mk2. Спасибо авторам моделей за их работу!
Наследуемся от Entity и реализуем абстрактные методы с логикой объекта. Здесь мы получаем состояние аппаратных кнопок влево и вправо, в зависимости от них вычисляем направление поворота машинки и, собственно, поворачиваем машинку путём сложения с координатой X вычисленного направления, помноженного на «скорость» поворота машинки. Для лучшего визуального эффекта, мы также плавно поворачиваем машинку эффектом а-ля EaseIn/EaseOut:
Теперь нам нужно, чтобы машинка где-то «ездила». Для этого мы моделируем в блендере примитивный кусок дороги с элементами ландшафта:
А затем реализуем примитивный рендерер фона, который будет скроллить два одинаковых seamless-куска уровня и как я уже говорил ранее, просто телепортировать их друг за другом, создавая эффект бесконечности.
public SectorRenderer() { road = Model.FromFile("road.mdl"); roadMaterial.Diffuse = Texture2D.FromFile("road.tex");
Артефакты на видео — следствие проблем с точностью float у MBX Lite в процессе клиппинга геометрии при ближней плоскости отсечения в 0.1f. Меняем на 1.0f и всё снова работает нормально :) Чуть изменяем проекцию, переместив камеру выше и наклонив на 45 градусов и игра уже похожа на Traffic Racer!
publicstaticvoid Preload() { PreloadedCars = new Model[1]; PreloadedMaterials = new Material[1];
LoadTrafficModel(0, "traffic1"); }
А сама их логика предельно проста. При спавне, машинка выбирает себе полосу, по которой будет ехать и рандомный множитель скорости, который вносит разнообразие в игру:
Переходим к обработке столкновений. Помним, что мы на этапе конвертации моделей посчитали Axis Aligned Bounding Box для каждой модели? В качестве алгоритма мы будем использовать классический AABB — или Rect vs rect:
public bool Intersects(BoundingBox box) { return (X < box.X + box.X2 && Y < box.Y + box.Y2 && Z < box.Z + box.Z2 && box.X < X + X2 && box.Y < Y + Y2 && box.Z < Z + Z2); }
Теперь для проверки столкновения между ними, нам надо посчитать абсолютный Bounding Box для каждого игрового объекта:
Затем итерируемся по списку всех игровых объектов в сцене, и если у нас есть машинка трафика, то проверяем на столкновение с машинкой игрока. Если столкнулись, то помечаем машинку игрока как разбитую и предлагаем игроку рестартнуть игру.
foreach (Entity ent in Game.Current.World.Entities) { if (ent is TrafficCar) { if (Player.Bounds.Intersects(((TrafficCar)ent).Bounds)) { // TODO: Damage logic Player.IsDestroyed = true; } } }
Уже что-то немного похожее на игру. Добавим конечное препятствие — необходимость рестарта при столкновении с другой машинкой и для демки пока-что хватит.
Правда, что на МКАДе каждый вечер такое? Я просто не с МСК :)
❯ Заключение
Вот такой у нас получился материал про PowerVR MBX! С выходом iPhone, этот GPU дал толчок для появления красивых мобильных игр с уровнем графики, близким к полноценным домашним консолям… жаль, что золотая эра интересных, самодостаточных и бездонатных мобильных игр и закончилась во времена iPhone 5 :(
В остальном же, надеюсь материал был достаточно интересен и познавателен для всех моих читателей, даже тех, кто никогда не программировал игры! Был у вас Dell Axim X51v? Пишите в комментариях!
Исходный код демки и бинарники можно найти на моём гитхабе.
А ещё у меня есть своя телега, куда я публикую бэкстейдж статей и вовремя публикую ссылки на новый материал!
Понравился материал?
А ещё я собираю деньги на проект с уже настоящим, физическим ТАЗом и его электронным дооснащением бортовым компьютером "по самому дешману" своими руками! Уже собрано 50.000 рублей из планируемых 70.000 на машину, из них 45.000 моих личных сбережений и 5.000 рублей - помощь читателей, за что вам большое спасибо :)
Пожалуй, многие из вас помнят, какими были мобильные игры до и после выхода первого iPhone. В начале 2000-х годов, ещё до появления яблочного смартфона, игры для телефонов в основном были весьма интересными, но тем не менее, достаточно простенькими с точки зрения графики и реализации в целом. После запуска AppStore в 2008 году, на iPhone начали выходить самые разные красочные, невиданные раннее по уровню детализации и проработке 2D и 3D игры. Но появление таких игр — отнюдь не заслуга Apple, а относительной малоизвестной компании PowerVR (подразделение Imagination Tech), которая смогла разработать на базе видеочипа Dreamcast и внедрить один из первых действительно массовых мобильных 3D-ускорителей, имя которому — PowerVR MBX! Сейчас мы с вами привыкли, что почти любой дешевый смартфон может отрисовывать графику уровня PS3 в 1080p, а то и выше, но когда-то даже уровень PS2 был роскошью… Сегодня мы с вами: узнаем предысторию появления аппаратно-ускоренной 3D-графики на телефонах, рассмотрим такую фирменную фишку PowerVR, как тайловый рендеринг, а в практической части статьи нам поможет легендарный КПК Dell Axim X51v с MBX на борту, под который мы напишем 3D-игру «про жигули» с нуля! Интересно? Тогда добро пожаловать под кат!
❯ Мобильная 3D-графика. Начало
Пожалуй, 3D-графика на мобильных устройствах начала развиваться ещё с самого начала 2000-х годов. К тому моменту, как мобильные телефоны научились запускать сторонние Java-приложения, практически сразу же появился прибыльный рынок мобильных игр. Ещё до появления поддержки jar-приложений, люди ставили рекорды в «Змейке» на телефонах Nokia, таскали ящики в «Строителе» на Siemens и играли в другие предустановленные игры на девайсах других брендов, поэтому было очевидно, что игры на мобильных телефонах рано или поздно смогут занять немалую часть сегмента портативных игровых устройств.
Именно появление J2ME дало тот самый толчок для развития мобильного гейминга. Производители телефонов активно развивали и дорабатывали мобильную платформу, добавляя в неё различные API-расширения — например, активацию приложений через СМС и доступ в WAP-интернет. Сама платформа J2ME была достаточно простой для изучения и имела низкий порог вхождения не только для людей, имевших какой-то опыт программирования, но даже для совсем новичков, которые никогда не писали код и тем более игр! Благодаря этому, появились сотни игр, многие из которых до сих пор помнят и любят: это и легендарный «мячик» Bounce, и «зайчик с морковками» Bobby Carrot, и весьма крутой Gish, а также множество различных платформеров по известным фильмам и «большим» играм!
В 2003 году появился Nokia N-Gage — первый массовый телефон, ориентированный именно на мобильный гейминг, который поддерживал не только Java-игры, но и собственные Symbian-игры с достаточно крутой 3D-графикой! Примерно в том же 2003 году, для платформы Java вышло сразу два API-расширения, которые добавляли поддержку симпатичной 3D-графики даже в самые простенькие и бюджетные телефоны: Mobile 3D Graphics (M3G, была почти везде) и Mascot Capsule (эта платформа была только на Sony Ericsson и Motorola). Именно благодаря этим API, мы с вами увидели такие легендарные игры, как V-Rally, Galaxy on Fire, Deep3D и многие другие! Но тем не менее, эти API были относительно медленными из-за программной растеризации на процессоре без отдельного 3D-ускорителя и весьма ограниченными в функционале. Ближайший пример по функционалу — уровень софтрендера первой кваки на первом Pentium! Кстати, про 3D на мобильных телефонах я писал отдельную статью, там в практической части мы пишем 3D-бродилку для Sony Ericsson!
Но помимо кнопочных телефонов, существовал сегмент High-end мультимедийных устройств, которые предоставляли гораздо больший функционал и производительность за немалые деньги. И речь, конечно же, о КПК! Девайсы, работавшие на базе шустрых процессоров Intel PXA и Samsung S3C с Windows Mobile на борту были заметно более перспективными для игр… но как-то не задалось из-за отсутствия нормальных каналов для распространения. Но тем не менее, Intel (иронично, но один из самых больших производителей ARM-чипсетов для КПК в те годы), которая уже занималась развитием десктопной графики GMA и PowerVR активно работали в этой сфере и результатом стало появление видеоускорителя 2700G, который представлял из себя не только 3D GPU PowerVR MBX Lite, но и аппаратный декодер видео, позволявший смотреть видео в высоком качестве! MBX Lite позволял запустить даже Quake 3 в 640x480 (!), пусть и в 10-15 FPS… Ещё за 5 лет до этого, далеко не все десктопные видеокарты могли выдать больше 30 FPS в 800x600!
Конечно в 2004 году уже вышел PSP, выставивший новую планку уровня 3D-графики для портативного гейминга, однако для смартфонов и КПК, уровень графики, разрешение и производительность 3D-игр на MBX Lite был просто немыслимым! Одним из самых легендарных и популярных устройств с 2700G, которое вы можете приобрести достаточно дешево и сейчас, был КПК Dell Axim X51v, флагманская модель с VGA-дисплеем тех лет. Но нельзя сказать, что только PowerVR работала в этом направлении. Параллельно NVidia выпустили GoForce, крайне редко попадающийся в «полноценном» виде (NVidia предлагала дешевле лицензировать только видео-декодер с отключением 3D-части, как это было в Toshiba Portege G900) и ATI Imageon, который чаще всего можно встретить в виде Adreno на ранних Android-чипсетах Qualcomm (Adreno — анаграмма Radeon :)).
Тем не менее, решение PowerVR было действительно массовым: компания не предлагала отдельный чип (что обычно было дороже), как конкуренты, а лицензировала другим компаниям уже готовые IP-ядра, которые производители чипов могли синтезировать и использовать в своих собственных чипсетах, или, сопроцессорах, как в случае с 2700G. Благодаря этому, MBX появился в чипсете TI OMAP 2430, использовавшийся в легендарных Nokia N93i и Nokia N95, Samsung INNOV8, Asus Lamborghini, Nokia E90 и некоторых других. Кроме того, PowerVR MBX использовался в процессоре Samsung S5L8900, судя по всему, разработанный для iPhone 2G и 3G! Благодаря этому, его можно считать одним из первых массовых 3D GPU в телефонах!
Одна из игр для iPhone 2G и N95 — Assasins Creed
И Asphalt 5!
Весьма симпатично, согласитесь?
❯ Под капотом
Но MBX, конечно же, не появился «из ниоткуда» и был основан на более ранних разработках компании Imagination Tech, а именно GPU из полноценной домашней консоли SEGA Dreamcast — PowerVR CLX2, который в свою очередь был основан на ранних десктопных GPU PowerVR из середины-конца 90-х годов. Основная фишка PowerVR была в использовании так называемой техники отложного тайлового рендеринга (TBDR), которая, в отличии от классической растеризации и сортировки с помощью Z-буфера (или ручной сортировки треугольников) всех примитивов «в лоб» (методика, используемая в PSP, PS2 и большинстве видеокарт 2000-х годов), сначала ждёт от программы списка всех рисуемых треугольников в кадре, разбивает весь экран на тайлы (небольшие прямоугольные области), которые содержат в себе информацию о пересекающихся треугольниках, а затем процессом, несколько схожим с рейтрейсингом, определяет, какой из пикселей треугольника ближе всего находится к камере наблюдателя. Таким образом, мы избавляемся от необходимости сортировки геометрии с помощью Z-буфера (который сам по себе занимает достаточно много, по меркам тех лет, памяти и страдает от проблем точности и Z-fighting'а), а также такой метод позволяет реализовать более дешевый альфа-блендинг без ручной сортировки полупрозрачных примитивов и имеет ещё одну приятную фишку — «бесплатный» Occlusion Query, который можно использовать для реализации продвинутых техник отсечения невидимой глазу геометрии.
Производительность PowerVR MBX была весьма достойной для своих лет: при частоте работы в 200МГц, видеочип обеспечивал филлрейт в 100Мп, обрабатывал до 1млн треугольников в секунду. Нативным графическим API MBX был OpenGL ES 1.1 — специальная урезанная версия OpenGL для встраиваемых устройств, из которой выбросили все ненужное и которая заточена не только под floating-point, но и под fixed-point арифметику. В остальном, особо никаких отличий для программиста по сравнению с обычными GPU не было, можно было без проблем портировать уже существующие приложения для десктопого OpenGL для мобильные девайсы, чем и пользовались энтузиасты при портировании Quake 3 на Nokia E90, КПК и другие девайсы. Также, PowerVR MBX поддерживал D3DM — графический API Windows Mobile, о котором мы поговорим позднее.
Однако PowerVR MBX был GPU с фиксированным конвейером (FFP), а не программируемым, как принято в современных 3D-ускорителях. Что-же такое программируемый и фиксированный конвейер? Давайте разберемся:
Фиксированный конвейер: для того, чтобы задать визуальную составляющую рисуемой геометрии, программист оперирует набором заранее определенных при проектировании видеочипа параметров, которые позволяют управлять внешним видом растеризуемых примитивов. Например, для реализации света, программист задает параметры каждого из 8 источников света влияющих на рисуемый объект. Если программисту необходимо наложить несколько текстур за один проход (например, для реализации плавных переходов текстур на ландшафте или нанесения карты отражений на модель), он оперировал комбайнерами, которые позволяли задавать для каждого сэмплера параметры наложения. Такой подход использовался на десктопных GPU эпохи до GeForce 3 (т. е. примерно до 2000 года), до PS3 на Sony PlayStation (Xbox сразу вышел с GeForce 3) и до PSP включительно на портативках. Очевидно, что такой подход сильно ограничивает программиста в том, как будет выглядеть его игра на той или иной видеокарте.
Программируемый конвейер: в программируемом подходе, для управления визуальной составляющей программист пишет небольшие программы для видеокарты, называемые шейдерами. Всего есть два базовых (в современных GPU их больше) этапа программируемого конвейера: первый из них — вершинный шейдер, отвечающий за трансформацию геометрии (перевод из мировой системы координат в экранную) и, например, анимацию. Трансформированные вершины отправляются в следующий этап конвейера — растеризацию, где выполняется уже пиксельный шейдер, который определяет цвет пикселя (или более корректно — фрагмента в терминологии 3D графики) — т.е например, окрас объекта в определенной цвет, текстуру (или несколько текстур), рассчитывает попиксельное освещение, накладывает тени и т. д. Кроме того, такой подход позволяет реализовать сложные техники типа Ambient Occlusion, SSR, а также пост-эффекты (например блюр/блум, правда эти два можно «сэмулировать» и на FFP при определенной сноровке).
К 2007 году, Khronos выпустили спецификацию второй версии OpenGL ES, которая добавляла в мобильные устройства поддержку программируемого конвейера и шейдеров. Таким образом, мобильные GPU всё ближе приближались к уровню консолей и могли выдавать вполне годную графику, близкую к консолям. Даже была когда-то такая консоль, как Zeebo, которая работала на базе смартфонного чипсета Qualcomm с графикой ATI Imageon (!). PowerVR уже в 2009 выпустила серию SGX, которая также использовалась в iPhone, iPad, многих Android-смартфонах и планшетах, а также PS Vita!
Modern Combat 3 на iPad
Но статья с пересказом фишек PowerVR MBX была бы не особо интересной без практической части с написанием 3D-игры под этот GPU с нуля! Поэтому предлагаю посмотреть на нашего сегодняшнего гостя, легендарный флагманский КПК Dell Axim X51v из далекого 2005 года! Для тех лет, это настоящий «жир»:
Его мне подарил мой читатель Сергей с Хабра, за что ему огромное спасибо! Девайс был в полной комплектации, даже с флэшкой и усиленной АКБ, которая до сих пор неплохо держит заряд, однако у него не работал тачскрин. Если вам интересен только процесс программирования игры, а не аппаратного ремонта, то листайте ниже сразу до следующего абзаца :)
❯ Практическая часть: ремонтируем КПК
По факту, девайс полностью работал, однако в некоторые моменты времени не откликался на кнопки и тачскрин, и по всем симптомам это напоминало дребезг кнопок. При этом тачскрин сам по себе реагировал нормально во всех местах, что, фактически, исключало вероятность его поломки (хотя резистивные тач-панели сами по себе не особо надежные, в отличии от емкостных тачскринов). Дело было вот в чём: во многих КПК тех лет был отдельный аппаратный переключатель блокировки клавиатуры и тачскрина, который можно было использовать при просмотре фильмов. Однако на моем девайсе он был слишком разболтанным…
Разбирается КПК несложно: выкручиваем 4 винта и снимаем переднюю часть корпуса. На всякий случай я прочистил грязь между тачем и верхней частью корпуса — она тоже бывает влияет на ложные нажатия и чувствительность тачскрина:
А вот и виновник наших проблем: рычажок переключателя был отломан, но все еще находится в положении «разблокирован». Даже если в выжать в упор — он все равно не работал. Ну что ж, фен в руки, сдуваем переключатель и ставим вот такую перемычку (на фото флюс ещё не отмыт):
Включаем девайс и смотрим — теперь всё работает! Вот такой простой и быстрый ремонт Axim'а. КПК мне сразу очень понравился, я и ранее знал о его легендарности, но теперь узнал и о том, что он очень круто спроектирован и собран! Кстати, есть смысл сразу сдуть концевой выключатель, который прижимает задняя крышка и заменить на перемычку. GPU не очень хорошо работает на кастомных прошивок, на которую прошиты многие Axim X51v. Поэтому есть смысл прошить сток: качаем прошивку (Файл отката), закидываем на SD-карту и ребутим девайс нажатием клавиш Wi-Fi + включение + Reset. После этого, девайс пойдет прошиваться.
Теперь девайс чистый, как с завода! Можно приступить к написанию небольшой демки-игрушки, которая сможет продемонстрировать нам перспективы нашего КПК в 3D!
❯ Практическая часть: подготовка
Изначально, в практической части статьи должна была участвовать не менее легендарная Nokia N95. Однако вот незадача: несмотря на то, что под Symbian сохранился SDK (который работает нормально только под Windows XP), на устройствах с системой старше 9.x необходимо взламывать installserver, дабы иметь возможность ставить хоумбрю программы (к которым относится и наша игра) и отладчик TRK.
И хотя свой девайс я пропатчил, дебаггер нормально поднять мне так и не удалось. Я смог проинициализировать контекст GLES, запилить примитивный рендерер с загрузкой ассетов из памяти устройства но потом решил перевести проект на WinMobile… Проблем с разработкой под Symbian много: если приложение крашится — то оно просто закрывается, без сообщений и логов. Добавьте к этому то, что в Symbian вообще нет исключений и не всегда можно записать ошибки в лог и отладка превращается в ужас. Ситуацию исправляет Qt, который работает на N95, но в котором нет поддержки GLES (по крайней мере, в виде обычного QOpenGL, хотя возможность юзать API системы из Qt есть и дебаггер там работает нормально, так что не всё потеряно). Если вы когда-то что-то пилили под Symbian, особенно в Carbide — пишите свой опыт в комментариях, интересно почитать :)
WinMobile не менее интересен тем, что в нём поддерживается сразу два графических API: классический OpenGLES в профиле Common Lite (только fixed-point арифметика) и мобильная версия Direct3D — D3DM.dll, которая предоставляет API очень похожее на DX9, но без поддержки шейдеров. Что не менее приятно — есть официальные биндинги от Microsoft к D3DM в .NET Compact Framework, что позволяет легко писать 3D-игры под WM на C#/VB.NET. Поскольку WinMobile — достаточно открытая для пользователя система, хватит лишь накатить VS2005/2008 на машину с WinXP/WinVista/Win7/Win8 и сразу начать разрабатывать под неё приложения, никаких проблем с отладкой и запуском приложений тут нет. На Win10/Win11 совместимость с WM5 поломали :(
Создаём приложение для смарт-устройств, выбираем в качестве целевой платформы WM5-устройство (эмулятор будет слишком медленным для наших целей, он даже для 2D-игр не подойдет) и, наконец-то, приступаем к написанию игры!
Что же за игра у нас будет? Я решил сделать эдакое 3D-переосмысление популярного в прошлом бесконечного раннера из «тетриса», где мы едем на машинке F1 и обгоняем другие машины, стараясь в них не врезаться. Основной целью является набрать как можно больше очков. Подобные игры достаточно популярны на мобильных девайсах и сейчас: вспомнить хотя-бы Highway Traffic, однако мой вариант будет весьма колоритным: ведь в моей демке мы будем кататься на ТАЗе 21099 и уворачиваться от гнилых «вторых гольфов». Ну а почему бы и нет, я просто очень люблю старые гнилые жигули и это не первый мой проект про машины этого производителя :)
❯ Практическая часть: «движок»
Как и у настоящей машины, у каждой игры должен быть собственный движок! Однако в случае конкретно нашей игры, это скорее небольшой фреймворк, который предоставляет ровно тот функционал, который нужен игре без каких либо излишеств. Необходимо изначально распланировать требования для будущего фреймворка, дабы написание игры не скатилось в процесс, известный в узких кругах как «движкописание» :)
Рендерер: с графической точки зрения, фреймворк должен реализовывать весьма небольшой функционал. Загружать геометрию и текстуры из файлов в специально-подготовленном формате, реализовывать концепцию камеры, отрисовывать статическую геометрию, а также спрайты и текст, реализовывать примитивную систему материалов, которая позволяет наносить на геометрию текстуры, красить их в определенный цвет и управлять повершинным освещением, а также наносить на геометрию отражения с помощью специально подготовленных enviornment-текстур. Кроме того, рендерер должен уметь рисовать симпатичное анимированное небо в виде полусферы.
Звук: воспроизведение wav-звуков и музыки из файлов. Да и всё пожалуй — что ещё нужно от звуковой подсистемы? :) Стерео ведь нет, поэтому и 3D-звук не нужен.
Ввод: обработка нажатий на тачскрин и аппаратные кнопки устройства, маппинг кейкодов в виртуальный «геймпад». GUI-подсистему тоже частично можно отнести именно сюда!
Физика: AABB и Sphere vs Sphere столкновения. Никакого полноценного солвера тут и не нужно :)
Начинаем, пожалуй, с реализации рендерера. Сначала нам необходимо создать окно и контекст D3DM. Процесс практически идентичен D3D8 и D3D9: передаём информацию о нужном адаптере (видеочипе) и заполняем структуру PresentationParameters, однако есть важные нюансы: аппаратный FSAA лучше всего отключить (MultisampleQuality), а также передавайте точный размер окна, в которое собираетесь рендерить изображение, иначе система начнёт софтварно (!) скейлить рендертаргет до размера окна каждый кадр, что, как сами понимаете, крайне медленно.
Из форматов Depth-Stencil форматов поддерживается D16, D24S8 и D32. Желательно использовать D16 (несмотря на тайловую архитектуру, насколько мне известно, в MBX все равно есть fallback до классического рендеринга при некоторых условиях). Практически на всех КПК и коммуникаторах использовался 16-битный цвет, т.е RGB565, но можно указать Unknown — тогда GAPI подцепит тот формат пикселя, что используется в остальной системе.
Переходим сразу же к рисованию геометрии! Для начала рендеринга, нам необходимо подготовить состояние контекста: посчитать и установить матрицы вида (т. е. камеры) и проекции для трансформации геометрии, задать рендерстейты (список состояний, например нужно ли рисовать модельку с освещением, или нет), очистить экран и Z-буфер и установить параметры фильтрации текстур. Перспективная коррекция текстур — достаточно тяжелая операция и использовать её стоит лишь при необходимости:
public void EndScene() { device.EndScene(); device.Present();
System.Threading.Thread.Sleep(16); }
Чтобы какую-то модельку нарисовать, нам нужно сначала её загрузить! Для возможности напрямую прочитать треугольники из файла и сразу записать их в вершинный буфер, я написал небольшой конвертер из формата SMD (GoldSrc) в собственный, очень простой и легковесный формат, который состоит из позиции вершины и её текстурных координат:
foreach (SmdTriangle triangle in mesh.Triangles) { for (int i = 0; i < 3; i++) { writer.Write(FloatToFixedPoint(triangle.Verts[i].Position.X)); writer.Write(FloatToFixedPoint(triangle.Verts[i].Position.Y)); writer.Write(FloatToFixedPoint(triangle.Verts[i].Position.Z));
Обратите внимание, PowerVR MBX оперирует fixed-point арифметикой! D3DM, конечно, может автоматически преобразовывать float-координаты вершин в числа с фиксированной точкой, вот только реализовано это криво и косо: драйвер будет конвертировать все вершины в fixed-point каждый вызов отрисовки, вместо того, чтобы один раз преобразовать их после Unlock'а вершинного буфера. Теперь представьте, насколько это тормозно для хоть сколь-либо комплексной модели :)
При этом загрузчик модели при таком подходе будет очень простым и будет работать шустро даже на таком слабеньком железе:
public Model(string debugName, Stream strm) { BinaryReader reader = new BinaryReader(strm);
int hdr = reader.ReadInt32(); int numVerts = reader.ReadInt32(); int vertSize = 20;
Переходим к текстурам. Грузить напрямую png/jpg на КПК слишком долго, поэтому их я тоже перегоняю в собственный примитивный формат, который состоит из описания ширины/высоты, а также формата текстуры и собственно, самих пикселей. На данный момент поддерживаются только RGB565 текстуры — с ними MBX работает лучше всего:
public sealedclass TextureConverter { publicconstint Header = 0x1234;
Загрузчик тоже получился примитивным и шустрым донельзя, пусть и без какой либо компрессии. PowerVR MBX поддерживает собственный формат компрессии — PVRTC:
BinaryReader reader = new BinaryReader(strm);
int hdr = reader.ReadInt32(); int fmt = reader.ReadInt32();
Handle = new Texture(Engine.Current.Graphics.device, Width, Height, 1, Usage.Lockable, Format.R5G6B5, Pool.VideoMemory);
int pitch; GraphicsStream gs = Handle.LockRectangle(0, LockFlags.None, out pitch); gs.Write(data, 0, data.Length); Handle.UnlockRectangle(0);
strm.Close();
Переходим, наконец, к фактическому рисованию модели! Для этого мы строим мировую матрицу для трансформации нашей модели, а также задаем вершинный буфер для, собственно, вершинного конвейера и посылаем видеочипу команду отрисовки. ZBufferWriteEnable нужен для отрисовки геометрии без записи в Z-буфер, что можно использовать, например, для реализации скайбоксов:
public void DrawModel(Model model, Transform transform, Material material) { Matrix matrix = Matrix.RotationY(transform.Rotation.Y * MathUtils.DegToRad) * Matrix.Translation(transform.Position); device.SetTransform(TransformType.World, matrix);
Переходим к обработке ввода. Тут ничего сложного нет, ловим события KeyUp/KeyDown формы и назначаем виртуальным кнопкам их состояние.
public Input(Form parentForm) { keyState = newbool[(int)GamepadKey.Count];
parentForm.KeyPreview = true;
parentForm.KeyDown += new KeyEventHandler(OnKeyDown); parentForm.KeyUp += new KeyEventHandler(OnKeyUp); }
private GamepadKey ResolveKeyCode(Keys key) { GamepadKey k = GamepadKey.Count;
switch (key) { case Keys.Left: k = GamepadKey.Left; break; case Keys.Right: k = GamepadKey.Right; break; case Keys.Up: k = GamepadKey.Up; break; case Keys.Down: k = GamepadKey.Down; break; case Keys.Return: k = GamepadKey.OK; break; }
Теперь мы сможем управлять нашей машинкой в игре (которой пока ещё нет). Самая-самая основа для реализации игры подобного плана у нас есть, пора переходить к геймплею!
Думаю, большинство моих читателей успела застать эру кнопочных телефонов с поддержкой Java-приложений. Помните ли вы, как мониторили разделы с загрузками на WAP-сайтах и ждали выхода новых игр, которые подойдут под ваш телефон и разрешение экрана? А какой восторг вызывали свежие 3D-игры, где графика с каждым релизом становилась всё лучше и была вполне на уровне PlayStation 1? V-Rally, Galaxy On Fire, Asphalt Urban GT, Deep3D, Sony Ericsson Tennis, Left 2 Die, Counter Terrorism 3D — думаю, хотя бы один из этих тайтлов вам знаком. Но задумывались ли вы, как работали эти игры «под капотом»? Каким образом разработчикам удавалось адаптировать полноценные 3D-шутеры и гонки под железо, где не было 3D-ускорителей (видеокарт), сопроцессора для чисел с плавающей точкой (FPU), а одноядерный процессор, работающий на частоте 100-200МГц, помимо игры обрабатывал ещё и звук, ввод, а также радиомодуль? Сегодня мы узнаем: как разрабатывали игры под J2ME, какие графические API существовали и на каких телефонах поддерживались, почему игры на Sony Ericsson шли лучше, чем на Nokia, а на «закуску» сами с нуля напишем 3D-бродилку в практической части! Интересно? Тогда добро пожаловать под кат!
❯ Предыстория
Обычно принято считать, что полноценные 3D-игры на кнопочных телефонах начали выходить примерно в 2004-2005 году, как раз с выходом графического API M3G. Однако, история мобильного трёхмерного гейминга тянется несколько дальше и уходит корнями в самое начало двухтысячных годов — как раз, когда телефоны только-только начинали обрастать мультимедийным функционалом.
Мы с вами привыкли, что существовали Java-игры для простых кнопочных телефонов и BREW-игры для телефонов Qualcomm. Но на рубеже нулевых сразу несколько перспективных компаний боролось за право стать разработчиком основной мобильной платформы и вытеснить J2ME. Одной из самых известных была Mophun, которая представляла из себя кроссплатформенную виртуальную машину, исполняющую байткод в собственном формате. И уже в ~2002-2003 году, Mophun представили 3D API для разработки очень симпатичных мобильных игр, которые и работали шустро.
Кстати, опробовать Mophun-библиотеку вы можете и сами: в РФ из Mophun-девайсов был как минимум Sony Ericsson T610, которые сейчас можно купить чуть ли не по «сотке» на вторичке.
Помимо Mophun, на французских телефонах производства Alcatel и Sagem была установлена платформа In-Fusio, тоже основанная на J2ME (а может и какой-то собственный сабсет API), однако со своим специфическим набором API, ориентированном на разработку игр. Мы с EXL даже в прошивке OT535 копались, прямо в HEX-редакторе, чтобы найти информацию о встроенной 2.5D-игре-бродилке:
Как можно заметить, спрос на 3D-игры появился практически в тот момент, когда в мобильные девайсы начали ставить достаточно мощные по тем годам процессоры: ~60-100МГц. Разработчики реально верили, что телефоны можно превратить не только в мультимедийное устройство, но и портативную хэндхэлд-консоль с графикой уровня не хуже GBA!
В сегодняшнем материале я хотел бы сосредоточиться именно на трехмерных Java-играх. Очевидно, что вручную рисовать 3D-графику без использования внешнего нативного API, написанного, например, на C, было бы проблематичным — девайс просто не вывез бы оверхеда Java-машины (хотя есть и исключение — некоторые 2.5D игры используют собственный портальный рендерер по типу того, что был в Duke Nukem 3D).
В процессе существования J2ME любая компания могла внести предложение по необязательному расширению мобильной платформы: это называется JCP (Java Community Process), а спецификации в ней — JSR. Таким образом, появилось множество разных расширений: AWT, GLES, M3G, PIM, Bluetooth API и примерно к 2005 году, M3G появился на большинстве кнопочных телефонов. Но мы ведь не одним M3G ограничены! Давайте глянем, какие ещё GAPI существовали на мобилках.
❯ Какие были 3D GAPI?
Под J2ME телефоны существовало сразу три стандартизированных графических API для отрисовки трёхмерной графики, которые были описаны в виде JSR. Вероятно, были ещё какие-то особенные API для телефонов из Азии (где традиционно телефоны акцентировались на играх с крутой 3D-графикой), однако они не поддерживались на обычных устройствах и информации о них очень мало.
Рассмотрим основные GAPI, которые использовались на большинстве телефонов:
M3G JSR184: Mobile 3D Graphics — самый популярный графический API, который поддерживался на большинстве Java-телефонов. Известен своей открытостью, функционалом и довольно неплохой производительностью. Строго говоря, M3G — это не только Immediate API для отрисовки треугольников в духе OpenGL или D3D как мы привыкли, но и уже готовый граф сцены (а-ля Unity), формат моделей, система материалов, обработки столкновений, освещения и т. д. Был достаточно хорошо оптимизирован и имел низкий порог вхождения, благодаря чему стал стандартом на мобилках.
Mascot Capsule: графический движок, разработанный японской компанией Hi Corp. Использовался в основном на телефонах для азиатского рынка и выдавал очень хороший на момент выхода уровень графики. Во многих аспектах лучше, чем M3G, однако порог вхождения в него несколько выше. Несколько напоминает D3D/OpenGL. Поддерживался на Sony Ericsson и на Motorola.
OpenGL ES JSR239: поддерживался только на некоторых моделях и вышел достаточно поздно (в контексте Java-телефонов), из-за чего популярности на простых телефонах не получил, зато активно использовался в смартфонах (стоит взглянуть на игры для iPhone 2G для сравнения). Является самым «тяжелым» и функциональным графическим API из перечисленных. Что интересно: изначально Google полностью перенесли JSR239 на Android, дабы поспособствовать портированию игр с Java-телефонов на смартфоны с зеленым роботом. По первой это, скорее всего, даже помогало.
Большинство читателей застали игры, использующие именно платформу M3G, которая, помимо отрисовки 3D-графики, предоставляла ещё много самых разных фишек: например, уже упомянутый граф сцены с собственным форматом файлов. Поскольку плагины для импорта и экспорта в 3d max были доступны каждому, а сам M3G плохо располагает к тому, чтобы использовать собственные форматы файлов, вскоре на некоторые игры начали повально появляться моды. Пожалуй, одним из рекордсменов по числу модов является игра Left 2 Die, которую как только не переделывали: и под Half-Life, и под Quake, и под обычную Left 4 Dead закос делали… чего только не было
Другой игрой, на которую часто делали моды — это Comcraft, написанная студентом в начале 2010-х годов. Там, в основном, моды имели чисто характер ретекстура а-ля «новые типы блоков». Всё это было возможно благодаря наличию на Java-телефонах различных Zip-архиваторов, благодаря чему молодые моддеры могли перепаковывать ресурсы игр как им угодно.
Ну и третья легендарная игра, которую расковыряли через пару лет после выхода — это, конечно-же, Galaxy on Fire 2. Изначально, она была рассчитана для запуска на мощных устройствах уровня Symbian-смартфонов и телефонов Sony Ericsson. Однако умельцы урезали звуки, музыку и игра запускалась даже на s40!
А вот с глобальными модами, меняющими геймплей игры, не задалось. Несмотря на то, что программы на Java легко декомпилируются, большинство разработчиков обфусцировали код при публикации игры. По каким-то причинам никто не хотел копаться и деобфусцировать чужой код (хотя это явно гораздо проще, чем копаться в нативном коде в IDA Pro) и хотя бы попытаться сделать некоторое подобие «SDK для модов». Увы :(
❯ Секреты производительности
Характеристики типичного кнопочного телефона тех лет были не особо впечатляющими:
Процессор: 104-200МГц, ARM926EJ-S ядро, иногда с поддержкой нативного выполнения Java-байткода. Без сопроцессора для чисел с плавающей точкой (FPU), без какого-либо видеоускорителя (за некоторыми исключениями) — вся нагрузка ложилась на процессор и иногда вспомогательный DSP.
ОЗУ: 8-16Мб SDRAM-памяти. Java-приложениям не доставалась вся память, а лишь её небольшой кусок, называемый кучей (Heap). Обычно размер Heap был от 800Кб до 2Мб. Умельцы даже патчили Java-машины некоторых телефонах, дабы выделить программам больше памяти. От размера кучи зависела работа тяжелый игр и программ: когда heap заканчивался, приложение падало в OutOfMemoryException.
Память: 10Мб-8Гб Flash-памяти.
Дисплей: CSTN, TN или AMOLED (редко) матрица с разрешением от 128x128, до 480x320 (возможно бывало и выше).
Очевидно, что на устройстве с такими характеристиками классические техники отрисовки 3D-графики не будут работать из-за малого количества памяти. Поэтому в ход шли некоторые интересные хаки, знакомые нам со времен PlayStation 1 и даже более старых консолей!
Начнём с сортировки примитивов. Представим, что у нас есть машинка и домик позади неё. Если мы отрисуем сначала машинку, а затем домик — то домик окажется перед машинкой, что полностью сломает эффект погружения и какую либо проекцию:
Пример такого эффекта можно найти в большинстве игр для PlayStation 1 — вот тут, например, лапки обезьяны видны поверх камня, чего быть не должно.
В современных приложениях, для сортировки геометрии по удаленности от наблюдателя используется screen-space техника Z-buffering, которая позволяет «дешево» отсекать невидимую глазу часть геометрии. Принцип её прост: по размерам окна приложения создаётся буфер, где каждый пиксель содержит информацию не о цветности, а о дальности фрагмента в этой конкретной точке. По итогу, во время отрисовки машинки, в Z-буфер будет записана глубина (дальность) конкретного фрагмента (участка геометрии), а когда будет нарисована домик, то видеочип сравнит значение глубины фрагмента машинки с машинкой и если значение глубины, хранящееся в буфере окажется меньше (или больше — зависит от реализации) прошлого значения — тогда часть машинки будет нарисована там, где нужно. Думаю, такое объяснение более чем понятное :)
Однако, Z-буфер требует драгоценную память (минимум ширина экрана * высота экрана * 2 — число байт в half float — т. е. 153 килобайта для экрана 240х320 как минимум) и наличие FPU для достаточной точности буфера глубины. Если же попробовать использовать обычные числа для этого, то вскоре мы столкнемся с проблемами точности, из-за чего будем видеть depth-fighting и от техники будет больше проблем, чем пользы.
В телефонах используется точно такая же техника, как и в PS1: сортировка отдельных полигонов. На PS1 с этим помогал отдельный векторный сопроцессор, который управлял трансформацией геометрии, освещением и мог эффективно сортировать треугольники, не нагружая основной процессор. А вот на телефонах этим занят основной процессор, вместе с растеризацией, обработкой игровой логики, звука и радиомодуля. Поэтому для корректной сортировки, вся отрисовка в GAPI на телефонах делится на «батчи», где программист отсылает набор нужных ему команд (отрисовать такую-то модельку с такой-то текстурой и такой-то трансформацией), а API затем уже трансформирует и сортирует вершины наиболее эффективным способом.
Второй интересный момент — это система координат. Как я уже говорил ранее, аппаратной поддержки float-чисел в телефонах зачастую не было. Однако j2me-машина и компилятор C (в те годы для прошивок чаще использовали ADS, чем GCC) предоставляли программную реализацию float-чисел, которая была ощутимо медленнее аппаратного FPU. Поэтому даже такие операции, как вычисление синуса и косинуса могли стать относительно тяжелыми для телефона, чего уж говорить о перемножении матриц. Для обхода этого ограничения использовалось две техники: fixed-point числа, где число с дробной частью представлено в виде обычного целого чисел, с которым умеет работать процессор (M3G) и обычные целые числа, которые представляют нормализованные числа 0.0… 1.0 (Mascot Capsule). Оба способа имеют ограниченную точность и в некоторых моментах могут давать артефакты, но здесь всё сильно зависит от самой игры. Например, из-за низкой точности при движении персонажа мир может «трясти».
И третий момент — это текстурирование и освещение. Сама концепция идентична той, которая используется в большинстве современных игр, однако из неё исключается важный этап: перспективно-корректное наложение текстур. Если говорить простыми словами, то при линейном наложении текстуры на геометрию «как есть», если подойти к модельке домика слишком близко — мы увидим искажения текстуры на его поверхности. Другой пример — шахматная доска, которая при приближении будет не идеально ровной, а если подойти совсем близко — то мы получим серьезные артефакты, которые полностью исказят визуальное представление. Для перспективной коррекции нужен тоже нужен FPU: это не бесплатная операция, поэтому от неё обычно отказываются (исключение — M3G), потому игры под J2ME иногда выглядят весьма странно:
Один из «универсальных» советов: желательно на этапе проектирования уровня и геймплея не ставить слишком близко друг к другу разные объекты и не давать камере игрока «смотреть» слишком близко на большие объекты.
В процессе подготовки статьи, я декомпилировал и изучил несколько 2.5D-игр из нулевых а-ля Wolfenstein 3D. Многие из них для лучшей производительности использовали пакет Nokia UI с DirectGraphics, который предоставлял некоторые плюшки для 2D-игр и возможность блиттинга произвольных изображений напрямую в экранный буфер. Там разработчики на что только не шли: и классический рейкастинг, и портальный рендерер — и всё это работало довольно шустро :)
Но вы ведь сюда не учебник по «матану» и не OpenGL Red Book пришли читать, верно? Поэтому давайте напишем свою 3D-бродилку под Java-телефоны с нуля! Да, это не совсем игра, но даст явное представление о том, как писали игры в те годы.
❯ Пишем свою «игру» с нуля
А напишем мы не просто что-то совсем примитивное, а основу для 3D-шутера от первого лица! Да, вот так сразу :) Конечно, не уровень Crysis, графика из 90-х, но для кнопочных девайсов вполне неплохо. Более того, эту игрушку я написал за полтора дня: основной рендерер за полдня и ещё какую-то базовую часть геймплея за день.
В качестве графического API я решил использовать Mascot Capsule. Материала о нём в сети относительно мало и для многих вообще остается загадкой, как он работает «под капотом». Про M3G писал немного aNNiMON, да и некоторая информация в сети есть, а про JSR239 особо из-за плохой поддержки на реальных девайсах. Тут важно понимать, что M3G и Mascot Capsule — это не DX11 и не Vulkan, порог вхождения у них достаточно низкий и понять их довольно легко, если иметь базовые представления о том, как работает 3D-графика. Поэтому создаём проект, мидлет (приложение в терминологии J2ME) и погнали!
Рендерер
Начинаем, конечно же, с инициализации контекста. В J2ME принято наследоваться от GameCanvas и реализовывать игровой цикл именно в нём. Для начала работы с MascotCapsule и M3G достаточно лишь создать объект Graphics3D (в случае M3G — получить ссылку на синглтон), а также выделить необходимые ресурсы — в нашем случае, это матрицы, которые здесь называются AffineTrans.
Самый примитивный игровой цикл будет выглядеть так. Сначала мы заливаем экран цветом для предотвращения эффекта Hall of mirrors и биндим объект Graphics к Mascot Capsule, затем рисуем сцену и освобождаем контекст, а потом вызываем flushGraphics для вывода изображения на экран:
Результат: синий экран.
Давайте что-нибудь нарисуем. Mascot Capsule может использовать как собственный формат моделей mbac и формат анимаций с ActionTable, так и произвольную геометрию. Юзать готовые форматы слишком просто, да и накатывать 3ds Max с плагинами не очень хочется, поэтому мы будем генерировать геометрию сами. Начнем с отрисовки треугольника и трансформации геометрии.
Effect3D — это материал в терминологии Mascot Capsule. Данный объект позволяет задавать визуальные параметры для геометрии: освещение и источник света, Toon-shading для придания эффекта мультяшности, настройки альфа-блендинга и некоторых других эффектов.
Далее идёт трансформация геометрии: процесс преобразования треугольников из локальной системы координат в мировую, экранную и затем уже Clip-space. Где affineMatrix — основная матрица, хранящая в себе результат перемножения viewProj матриц, а affineRotationY и affineTranslate — матрицы трансформации сначала для камеры, а затем уже для преобразования модели в мировые координаты. При этом проекция — тоже часть AffineTrans. Вот такая вот путаница.
В FOV (512) задается значение в радианах, где 0 — это 0, 4096 — это 3.14 * 2 (360 градусов). Это же касается и углов в поворотах.
Обратите внимание — матрицы имеют размерность 3x3, а не 4x4, как это принято в «больших» системах. translate здесь нет — только lookAt.
Идём дальше — к фактической отрисовке треугольников. Геометрия может иметь текстурные координаты, цвета и нормали. Формат вершины можно задавать динамически — необязательно передавать сразу все аттрибуты вершин. Из-за особенностей Mascot Capsule, геометрия не будет отрисована, если не переданы текстурные координаты или цвет.
UV-координаты указываются в абсолютных координатах текстуры. т. е. не 0..255, как на 3dfx Voodoo, например, а 0… ширина текстуры и 0… высота текстуры. Учитывая, что класс текстуры не позволяет даже её размер узнать… решение так себе.
Результат:
Добавляем второй треугольник, дабы нарисовать квад:
На этом, половина рендерера написана. Я не шучу :)
Теперь генерируем геометрию для кубика. Для наглядности я написал простенький класс для генерации геометрии на лету. Отдельный формат для моделей нам пока не нужен, поэтому я «запеку» геометрию в отдельный массив вершин.
Результат:
Текстурированные кубики рисовать умеем, камера у нас тоже есть: уже можно реализовать бродилку :)
Уровни
Уровни делать мы будем… прямо в IDE. Каким образом? Уровень будет храниться классическим для подобных игр способом: сетка x на x, где каждое число указывает ID-текстуры (и в оставшихся битах можно разместить какие-нибудь атрибуты), где 0 — блок отсутствует. Все блоки предполагаются твердыми.
З.Ы На Пикабу нет тега с кодом, поэтому пришлось вставлять код в виде скринов. А поскольку на кол-во медиа-элементов в посте есть ограничение в 25 блоков, пришлось остальной код кастрировать :(
Отрисовка подобного уровня в самом простом случае очень простая: мы просто проходимся по всей сетке и рисуем каждый куб, если ему назначена текстура. Однако, это не очень эффективный метод: в случае больших уровней с множеством перекрытых комнат, на GAPI ложится лишняя работа по сортировке геометрии, а также страдает филлрейт. Лучше всего разделить такие уровни на комнаты и рисовать только видимые участки уровня.
Теперь нам необходимо реализовать возможность ходьбы по уровню. Для этого мы будем поворачивать камеру по координате Y при нажатии кнопок вправо и влево, а для движения вперед и назад вычислять forward-вектор, указывающий на направление персонажа относительно угла поворота, который представляет из себя синус и косинус угла поворота персонажа в радианах.
Напомню, что углы хранятся в виде 4096 = 360гр. = 3.14 * 2.
Однако без пола и потолка игра выглядит не особо привлекательно. Самое время их добавить! Настоящий вертикальный потолок реализовать будет затруднительно из-за отсутствия коррекции текстур — пол будет постоянно «плыть», что не очень красиво. Поэтому мы воспользуется дедовскими методами и закрасим нижнюю часть экрана серым, а потолок сделаем фоновой картинкой! Таким образом, каким бы большим не был уровень, в центре экрана всегда будет какая-то стенка, из-за чего мы не сломаем нашу перспективную проекцию!
Смешивать Graphics и Graphics3D одновременно нельзя — сильно падает производительность. А вот использовать Graphics для отрисовки интерфейса поверх Graphics3D после отрисовки кадра — можно! Суть хака простая: рисуем с ортографической (параллельной) матрицей половинку экрана с текстурой неба, а вторую половинку — просто серый квад. Всё очень легко и просто!
Обратите внимание на все артефакты, о которых я рассказывал в разделе оптимизации игр. И текстуры плывут, и мир дребезжит — всё это результаты оптимизаций со стороны разработчиков GAPI. Зато работает шустро!
Обработка столкновений
В любом шутере нужна обработка столкновений. Даже в космическом скроллшутере :) Тут я чуток наговнокодил, поскольку нет необходимости проверять столкновение игрока с вообще всеми кубами на сцене: достаточно сделать выборку из ближайших восьми блоков, но для наглядности оставил так.
Принцип прост: обычная проверка AABB, весь резолвинг — это откидывание игрока обратно, если после последнего движения он «врезался в стену».
Но как насчет реального девайса? Для тестов у меня есть SE W200i, W610i и J10i2! Все поддерживают Mascot Capsule, так что с тестами проблем не будет. Ниже тест на W200i, на остальных девайсах в моем первом комменте - Пикабу не даёт загрузить слишком много картинок/видео в один пост :(
❯ Заключение
Вот как-то так в нулевых и писали 3D-игры для кнопочных телефонов. Да, мы практически не затронули тему 3D-игр на Symbian-смартфонах (ранее я писал статью о разработке игры под WinMobile), но обсудили множество тонкостей и написали собственный Proof of Concept бродилки для подобных Java-мобилок. Исходным кодом бродилки я делюсь, кто угодно может использовать его как основу для своей игры или что-то типа такого. По крайней мере, видел пару лет назад несколько Java-игр, которые кто-то всё ещё делает.
Пишите свой опыт с Java-играми в комментариях :)
P. S.: Друзья! Время от времени я пишу пост о поиске различных китайских девайсов (подделок, реплик, закосов на айфоны, самсунги, сони, HTC и т. п.) для будущих статей. Однако очень часто читатели пишут «где ж ты был месяц назад, мешок таких выбросил!», поэтому я решил в заключение каждой статьи вставлять объявление о поиске девайсов для контента. Есть желание что-то выкинуть или отправить в чермет? Даже нерабочую «невключайку» или полурабочую? А может, у этих девайсов есть шанс на более интересное существование! Смотрите в соответствующем посте, что я делаю с китайскими подделками на айфоны, самсунги, макбуки и айпады!
Понравился материал? У меня есть канал в Телеге, куда я публикую бэкстейдж со статей, всякие мысли и советы касательно ремонта и программирования под различные девайсы, а также вовремя публикую ссылки на свои новые статьи. 1-2 поста в день, никакого мусора!