Как для разработчика игр, некоторые из моих самых креативных работ появились из принятия ограничений, а не борьбы с ними. Как бы нелогично это ни звучало, ограничение возможностей оборудования или абстракций заставляет вас мыслить нестандартно гораздо больше.
Чтобы дать вам такой опыт, существуют онлайн фэнтезийные игровые консоли, такие как PICO-8 (небесплатная) и TIC80, которые делают очень доступным создание прототипов и получение минимальных навыков. Также есть аппаратные системы, такие как Playdate (пропиетарная), которые работают с методами ввода и форм-факторами еще больше ограничивая вашу площадку для игр. Наконец, есть процветающие сообщества энтузиастов-любителей вокруг таких консолей как SNES и N64 (посмотрите на этот потрясающий демейк Portal!).
Лично я всегда питал слабость к Wii. Отчасти потому, что я вырос на ее невероятных играх, таких как Super Mario Galaxy 2, но также потому, что моддинг игр для Wii дал мне возможность заглянуть в то, что позже станет моей карьерой: разработку игр. Хотя я и занимался разработкой для Wii в прошлом, я никогда не чувствовал что действительно понимаю что делаю. Пару месяцев назад я решил это исправить. Имея законченное задание по DirectX для университетского курса программирования графики и открытые возможности «вы можете добавлять дополнительные функции, чтобы повысить свои оценки, но они не являются обязательными», я подумал: что, если я приду на экзамены со своим Wii и сделаю презентацию на нем?

DirectX на Wii (шутка)
Как бы я ни был возбужден, воплощая эту идею в жизнь, я знал, что я не собираюсь просто скомпилировать свои DirectX шейдеры и код для процессора Wii и закончить на этом. DirectX не очень переносим на Wii или совместим с ней. Wii оснащен графическим процессором под кодовым названием «Hollywood», который имеет немыслимые 24 МБ видеопамяти, а также не имеет аппаратной поддержки для каких-либо шейдеров. Это действительно заставляет вас оценить некоторые из потрясающих сцен, созданных на этой консоли.

Таким образом нам придется работать с собственным API Hollywood (называемым GX), чтобы заставить его отрисовать сетку с текстурами и прозрачностью (как того требует задание).
ПРИМЕЧАНИЕ: В финальном проекте я создал папку GX для хранения всего кода, специфичного для GX, и изолировал материалы DirectX в отдельную папку под названием SDL. Таким образом я могу контролировать какой специфичный для платформы код используется с помощью простой опции CMake. Если вы готовы действовать вслед за мной, вы можете найти все здесь.
libogc
Для доступа к этому API из C++ есть библиотека, поддерживаемая ребятами из @devkitPro@mastodon.gamedev.place, которая называется libogc. Эта библиотека в сочетании с набором инструментов PowerPC позволяет создавать программы, ориентированные на Wii (и GameCube, поскольку они так похожи!).
ПРИМЕЧАНИЕ: всякий раз, когда я буду ссылаться на Wii, это (в основном) также относится и к GameCube.
Хотя devkitPro сам не имеет доступного toolchain-файла CMake, мне удалось найти файл с лицензией MIT, любезно предоставленный энтузиастами объединенными вокруг игры rehover. Передача этого toolchain-файла в CMake автоматически настраивает его на сборку для Wii. Крутая штука!
ПРИМЕЧАНИЕ: Остальную часть этого поста я пытался сделать максимально точной насколько мог, но, вероятно, я мог что-то неправильно понять! Если вам нужна более точная информация, предлагаю вам взглянуть на gx.h libogc со всеми функциями и комментариями, а также на официальные примеры devkitPro GX, а не следовать моему собственному коду. Комментарии, вопросы и исправления, как всегда, приветствуются на моем fedi-хэндле @mat@mastodon.gamedev.place ! (от переводчика: автор статьи не говорит по-русски, поэтому если надумаете ему писать — используйте английский)
Настройка видео
Я не буду слишком долго останавливаться на инициализации, так как большая её часть просто взята из примера libogc, это не особо интересно. Но что круто, так это то, как для выполнения v-sync мы создаем два «буфера кадров», которые являются просто целочисленными массивами… на CPU? Вот где проявляется одно из самых больших отличий в аппаратной конструкции Wii по сравнению с современным компьютером: и CPU, и GPU имеют доступ к 24 МБ общей оперативной памяти. Между тем на современном ПК GPU будет иметь исключительно свою собственную выделенную оперативную память, к которой CPU не может обращаться напрямую.
Эта общая оперативная память — то место, где мы храним упомянутые массивы буферов кадров, названная Wii «eXternal Frame Buffers» или XFB (источник). Поскольку работа графического процессора с XFB напрямую была бы медленной, у графического процессора есть свой собственный кусочек фактически частной оперативной памяти, в которой хранится то, что официально называется «Embedded Frame Buffer» (EFB). Команды рисования GX работают на сверхбыстром EFB, и когда наш кадр готов, мы можем скопировать EFB в XFB чтобы видеоинтерфейс мог его прочитать и, наконец, отобразить на экране. Эта буферная копия примерно эквивалентна «представлению» кадра как это сделано в API к которым мы привыкли.
┌─────────┐ │ │ ┌─────────┤ CPU ├───────────────────┐ │ │ │ │ │ └─────────┘ ▼ │ Вызовы отрисовки GX │ │ ▼ ┌────────────▼─────────────┐ Создаем массивы XFB │ │ │ │ Частная память GPU (EFB) │ │ │ │ │ └────────────┬─────────────┘ │ ▼ │ Копируем EFB в текущий XFB │ │ │ ┌───────────────────────────┐ │ │ │ │ │ └──────► Общая память MEM1 (24MB) ◄─────┘ │ │ └─────────────┬─────────────┘ ▼ Отображаемый кадр ┌────────▼────────┐ │ │ │ Видеоинтерфейс │ │ │ └─────────────────┘
Следующий код занимается специфической обработкой кадра и его отображением:
void GraphicsContext::Swap() { GX_DrawDone(); GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); GX_SetColorUpdate(GX_TRUE); GX_CopyDisp(g_Xfb[g_WhichFB], GX_TRUE); VIDEO_SetNextFramebuffer(g_Xfb[g_WhichFB]); VIDEO_Flush(); VIDEO_WaitVSync(); g_WhichFB ^= 1; // flip framebuffer }
Каждый раз, когда кадр завершен, мы сообщаем графическому процессору об этом через GX_DrawDone()
, а затем копируем EFB в XFB через GX_CopyDisp(g_Xfb[g_WhichFB], GX_TRUE)
где g_Xfb
— два XFB и g_WhichFB
— это один бит, который мы переворачиваем в каждом кадре. Затем мы уведомляем видеоинтерфейс о том, какой буфер кадра он должен выводить, с помощью вызова VIDEO_SetNextFramebuffer(g_Xfb[g_WhichFB])
. Наконец, VIDEO_Flush()
и Video_WaitVSync()
гарантируют, что мы не начнем рендеринг следующего кадра до того, как будет отображен этот.
Рисование сетки
Теперь, когда мы знаем как работают буферы кадров на Wii, давайте приступим к рисованию нашей сетки!
Настройка атрибутов вершин
Прежде чем мы начнем проталкивать треугольники через GX, мы должны сообщить ему какие данные ожидать. Это делается в два этапа:
-
Во-первых, мы сообщаем GX, что будем передавать ему данные вершин напрямую в каждом кадре, а не заставлять его извлекать их из массива через индексы:
GX_SetVtxDesc(GX_VA_POS, GX_DIRECT);
-
Затем мы обращаемся к таблице форматов вершин по индексу 0 (
GX_VTXFMT0
) и устанавливаем ее атрибут положения (GX_VA_POS
) следующим образом:
-
Данные состоят из трех значений координат XYZ (
GX_POS_XYZ
) -
Каждое значение представляет собой 32-битное число с плавающей точкой (
GX_F32
) -
Я не совсем понимаю, для чего нужен последний аргумент, но ноль мне подошел
GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
Обе эти функции затем при необходимости повторяются для нормалей и текстур.
ПРИМЕЧАНИЕ: GPU Wii поддерживает индексированную отрисовку где данные вершин хранятся в массиве и отображаются с использованием индексов в этом массиве. Это позволяет определять меньше вершин при их повторном использовании.
Я не знал об этом, пока не закончил этот проект, поэтому мы будем придерживаться неиндексированной отрисовки. Концепция довольно похожа, но вы устанавливаете описание вершины в
GX_INDEX8
и связываете массив перед вызовомGX_Begin
. Затем вы передаете индексы вместо данных вершины внутри блока begin/end.
Вызовы отрисовки (drawcalls)
В каждом кадре мы должны добавлять в очередь некоторые команды в «first-in-first-out» буфере GPU. Мы можем сообщить GX что пришло время рисовать какие-то примитивы с помощью функции GX_Begin, передав тип примитивов (треугольники!), индекс в таблице форматов вершин, которую мы заполнили ранее, и количество вершин, которые мы будем рисовать. После этого мы можем передать ему данные по порядку, вызвав соответствующую функцию для каждого настроенного нами атрибута. Наконец, мы завершаем все это вызовом GX_End (который libogc просто определяет как пустую функцию, так что, я думаю, это может быть просто синтаксическим сахаром в API).
GX_Begin(GX_TRIANGLES, GX_VTXFMT0, m_Vertices.size()); for(uint32_t index : m_Indices) { // ПРИМЕЧАНИЕ: очень жаль, что я не использовал поддержку индексации GX... const Vertex& vert = m_Vertices[index]; GX_Position3f32(vert.pos.x, vert.pos.y, vert.pos.z); GX_Normal3f32(vert.normal.x, vert.normal.y, vert.normal.z); GX_TexCoord2f32(vert.uv.x, vert.uv.y); } GX_End();
Трансформации
ПРИМЕЧАНИЕ: В этом разделе предполагается что вы знакомы с преобразованиями матриц. Если вы не знаете, что это такое, вот ссылка на первую из двух страниц в руководстве OpenGL, где это объясняется, и которое в конце концов помогло мне понять как это устроено.
Первая важная матрица — это матрица модели. Задача этой матрицы — преобразовывать вершины пространства модели (model-space) в пространства мира (world-space). Это полезно когда мы хотим вращать, масштабировать или перемещать объект внутри мира.
Чтобы посмотреть вокруг в нашей сцене, нам нужно настроить матрицу вида, которая заботится о переводе пространства мира в пространство вида (view-space). Наконец, нам понадобится матрица проекции, которая преобразует заданное пространство вида в пространство клипа (clip-space), и в этот момент GPU берет на себя управление и обрабатывает такие вещи как выбраковка и преобразование в неоднородные координаты.
В обычной графике мы склонны объединять вид и проекцию вместе и оставлять модель наедине с собой для трансформации нормалей и других данных в пространство мира в шейдере. Однако Wii использует другой подход: загружает объединенную матрицу modelView и отдельно обрабатывает проекцию.
Причина этого довольно интересна: GX ожидает что вы дадите ему информацию об освещении в пространстве вида, а не как обычно в пространстве мира. Мы рассмотрим простое освещение в следующем разделе.
Итак, нам нужно только настроить эти две матрицы, чтобы удовлетворить все наши потребности в преобразовании:
GX_LoadProjectionMtx(projectionMat, GX_PERSPECTIVE); // используйте одну и ту же матрицу для позиций и нормалей GX_LoadPosMtxImm(modelViewMat, GX_PNMTX0); GX_LoadNrmMtxImm(modelViewMat, GX_PNMTX0);

Текстуры
Текстуры на самом деле очень просты! Мы можем напрямую привязать массив байтов как текстуру, поскольку у CPU и GPU есть 24 МБ общей оперативной памяти.
Сначала я пытался использовать родной формат Wii (TPL), который имеет несколько действительно крутых функций, таких как CMPR кодирование сжатых текстур. Он предполагает что графический процессор распаковывает текстуру в реальном времени когда ему нужны данные без (по-видимому) потери производительности. Потрясающе!
К сожалению, мне не удалось это сделать…

Даже при использовании базового TPL наблюдались некоторые неприятные артефакты:

В конце концов я сдался и решил просто использовать PNG и декодировать его в сырой массив байтов RGBA8 полностью минуя TPL. Это избавило от артефактов, так что, полагаю, мы никогда не узнаем почему они возникли!
GX_InitTexObj(&m_Texture, decodedData, width, height, GX_TF_RGBA8, GX_CLAMP, GX_CLAMP, GX_FALSE);
Чтобы использовать текстуру, мы можем просто привязать объект текстуры, полученный во время инициализации, к индексу из которого мы хотим сделать выборку:
GX_LoadTexObj(const_cast<GXTexObj*>(&m_Texture), GX_TEXMAP0);
По умолчанию GX считывает данные из GX_TEXMAP0
при рисовании треугольников, так что это фактически все, что нам нужно было сделать!

Прозрачные текстуры
Мы можем настроить смешивание с альфа-каналом следующим образом:
GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_OR);
Это сообщает GX что при смешивании двух прозрачных образцов он должен взять альфа-значение предыдущего пикселя (GX_BL_SRCALPHA
) и обратное значение альфа-канала нового пикселя (GX_BL_INVSRCALPHA
). Я не уверен для чего нужен GX_LO_OR
, но смешивание, определенно, работает, поэтому я его оставляю. Хорошее объяснение конкретно этой функции смешивания есть на LearnOpenGL.
Хотя на первый взгляд кажется, что прозрачность работает, есть довольно большая проблема, которая проявляется, если посмотреть на эффект огня вблизи (у меня нет скриншота сборки для Wii, поэтому этот снимок сделан с DirectX, однако тут виден тот же эффект)!

Если один из треугольников, на котором проявляется эффект, отрисовывается над другим треугольником, который должен отображаться под первым, первый записывается в Z-буфер, в результате чего второй треугольник отбрасываться. Обычно это хорошо, потому что пропускается рисование пикселей, которые полностью перекрыты и гарантирует что то, что находится позади модели, не будет отрисовано поверх нее. Однако в случае полупрозрачных изображений мы получаем артефакты, подобные приведенному выше.
Это изображение было отрендерено с полностью отключенным Z-буфером, что показывает, зачем он нам нужен:

К счастью, решение довольно простое:
if (m_UseZBuffer) { GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); } else { GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_FALSE); }
Установите m_UseZBuffer
равным false для моделей, использующих прозрачные текстуры. Последний параметр — GX_FALSE
в GX_SetZMode
отключает запись в Z-буфер. Обратите внимание, что нам по-прежнему нужно чтение (первый параметр — GX_TRUE
), так как в противном случае эффект огня в итоговом изображении будет отображаться поверх нашего транспортного средства!
«««Шейдеры»»»
В отличие от современных API, графический процессор Wii не программируется с произвольными шейдерами. Вместо этого мы можем работать с чем-то довольно мощным, называемым этапами оценки текстур (texture evaluation — TEV). У нас есть целых 16 этапов TEV для игры, которые Nintendo снисходительно называет «гибким фиксированным конвейером» (flexible fixed-pipeline). Каждый этап по сути является настраиваемой линейной интерполяцией между двумя значениями A и B с коэффициентом C. После к результату добавляется четвертое значение D.
u8 TEV_stage(u8 a, u8 b, u8 c, u8 d){ return d + (a * (1.0 - c) + b * c); }
ПРИМЕЧАНИЕ: Также есть необязательные отрицание, масштабирование, смещение и фиксация. Я их здесь пропускаю, потому что в итоге не использовал. Более полная документация доступна здесь.
Источники A, B, C и D можно настроить на каждом этапе. Например, можно сделать так, чтобы они линейно интерполировались между цветом текстуры и цветом света в зависимости от количества получаемого им зеркального освещения. Я пытался это настроить и мне много помогал Джаспер (спасибо еще раз!), но в конечном итоге ничего не вышло. Я планирую попробовать еще раз в будущем!
Рассеянное освещение
В графический процессор Wii встроено повершинное освещение. Это означает что вы можете (опционально) указать ему рассчитать сколько света получает каждая вершина от источников света (их может быть до восьми), которые могут быть ослаблены расстоянием (как лампа) или углом (как прожектор).
GX предоставляет тип данных GXLightObj
, который мы можем загрузить и настроить со всеми нашими параметрами. Для рендерера, который я делал, мне нужно было настроить «солнечный» свет, который является очень далеким точечным светом практически без затухания.
ПРИМЕЧАНИЕ: обычно в программировании графики это делается с помощью простого направленного света. Однако способ, которым я заставил это работать на Wii, был в имитации упомянутой модели точечного света без затухания и я остановился на нем
Вот фрагмент кода, который инициализирует его в каждом кадре:
GX_SetChanAmbColor(GX_COLOR0, ambientColor); GX_SetChanMatColor(GX_COLOR0, materialColor); GX_SetChanCtrl( GX_COLOR0, GX_ENABLE, GX_SRC_REG, GX_SRC_REG, GX_LIGHT0, GX_DF_CLAMP, GX_AF_NONE); guVector lightPos = { -lightDirWorld.x * 100.f, -lightDirWorld.y * 100.f, -lightDirWorld.z * 100.f }; guVecMultiply(viewMatNoTrans, &lightPos, &lightPos); GXLightObj lightObj; GX_InitLightPos(&lightObj, lightPos.x, lightPos.y, lightPos.z); GX_InitLightColor(&lightObj, lightColor); GX_LoadLightObj(&lightObj, GX_LIGHT0);
Давайте рассмотрим каждый шаг.
Регистры цвета
Сначала мы сообщаем GX какие цвета окружающей среды и материала мы будем использовать. Цвет окружающей среды используется для освещения всех вершин независимо от полученного света. Это гарантирует что задняя часть нашей сетки не будет просто чисто черной. Цвет материала будет подкрашивать всю вашу модель (это как глобальный цвет вершин), поэтому я оставляю его белым.
Настройка канала
GX_SetChanCtrl
настраивает канал освещения, который мы будем использовать. Мы хотим, чтобы свет влиял на GX_COLOR0
там, где будет наша текстура. Мы указываем ему получить цвет окружающей среды и материала из регистров которые мы установили только что через GX_SRC_REG
. Мы устанавливаем GX_LIGHT0
как источник света, который влияет на этот канал, с функцией диффузии по умолчанию GX_DF_CLAMP
. Наконец, мы отключаем затухание, передавая GX_AF_NONE
, что означает что наш свет может быть бесконечно далеко, но при этом освещать нашу модель так, как будто он находится прямо рядом с ней.
Трансформация позиции
Затем мы вычисляем положение света который находится очень далеко противоположно направлению в котором он будет светить. Обратите внимание что мы умножаем его на матрицу вида (с удаленной частью трансляции), поскольку свет находится в пространстве вида!
Создание объектов испускающих свет
В конце мы создаем наш GXLightObj
, задаем ему его положение и цвет, и загружаем его в канал GX_LIGHT0
. Обязательно отключите освещение у огня (он сам является источником света и не должен отбрасывать тень) и бац! Вот и наше солнце!

Вы можете найти весь мой код освещения и TEV в Effect.cpp. Имя файла неудачное, но, поскольку изначально это был проект DirectX, я увяз с именем из заголовка.
Вот и все!
Я быстро собрал версию GameCube накануне даты сдачи задания и отправил требуемый .exe вместе с моими сомнительными двоичными файлами .dol без каких-либо пояснений. Я хотел сделать сюрприз. На следующий день я пришел в кампус с очень тяжелым рюкзаком и, когда пришло время, вытащил Wii чтобы представить свои «дополнительные функции для повышения ваших оценок, но они не являются обязательными». Казалось, это произвело настоящий фурор! Похоже я не единственный, кто вырос с Wii
Скачать сборку можно здесь. Поддерживаются элементы управления Wiimote и GameCube!
ссылка на оригинал статьи https://habr.com/ru/articles/897288/
Добавить комментарий