Описание отрисовки 3D объектов в окне с использованием компонентной системы.
Используемые термины
- Компонент - объект, содержащий информацию об одном аспекте игрового объекта (таких как текстура, меш, положение и т.п.).
Игровой объект - объект в игровом мире, содержащий информацию в виде набора компонентов.
- Игровой мир - набор игровых объектов, разбитый на иерархическую структуру из ячеек, каждая из которых содержит несколько игровых объектов; назначение - оптимизация подгрузки и выгрузки игровых объектов (сразу ячейками).
- Игровая сцена - набор игровых объектов, непосредственно участвующих в игровом процессе; объекты подгружаются (по мере необходимости) из игрового мира и это именно те объекты, которые должны обновляться в процессе игры.
- Рендер - объект, предназначенный для отображения игровых объектов в 3D сцене, создается на основе одного или нескольких компонентов.
- Объект 3D сцены - объект (набор рендеров), из которых состоит 3D сцена.
- 3D сцена - набор объектов 3D сцены, которые отображаются в окне программы; является 'зеркалом' той части объектов игровой сцены, которые попадают в поле зрения камеры.
- Заметки
- В данном описании под объектом подразумевается 'объект 3D сцены', а под сценой - '3D сцена'.
- Обозначение компонента как State.Scissor подразумевает объект компонента, у которого установлены параметры type == 'State' и kind == 'Scissor'.
- Под объектом компонента подразумевается объект C++ класса компонента.
Подробнее о компонентах и их параметрах см. Компонентная система.
Общее описание
Используемый формат вершин:
Отрисовка производится при помощи формирования сцены, содержащей рендеры:
- Камеры - уникальный для сцены объект, определяющий способ отрисовки сцены.
- Общих объектов, таких как настройки конвеера рендеринга и источники света.
- Объектов, которые непосредственно формируют требуемое изображение на экране.
Сцена определяется набором идентификаторов ее объектов, таких сцен может быть несколько, добавлять объекты разных сцен можно в очереди рендеринга разных проходов, при рендеринге нескольких сцен текущая сцена будет отрисоваваться поверх предыдущей; также можно сформировать несколько наборов идентификаторов объектов, каждый из которых отрисовывать в зависимости от неких условий.
Создание и удаление рендеров для компонентов, а также рендеринг объектов 3D сцены производится через объект класса covellite::expanse::Window, который должен быть создан при старте программы и передан окну формирования 3D сцены.
- Предупреждения
- Реализация OpenGL требует, чтобы рендеры создавались и активировались в том же потоке, в котором было создано окно графического Api.
- Заметки
- При использовании в разных объектах компонентов с одним и тем же id будет использоваться один и тот же объект рендера (это позволяет, например, один раз сформировать набор общих для всех объектов рендеров, а затем обращаться к ним через объекты компонентов, содержащих только идентификаторы), но при этом следует учитывать, что для некоторых типов компонентов рендеры не создаются (например, компоненты Data). Для таких компонентов параметр id не имеет смысла и при необходимости использования несколькими объектами одного и того же компонента (например, для совместного перемещения группы объектов), следует использовать один и тот же объект компонента.
Разные объекты могут использовать рендеры, выполняющие одно и то же действие, в этом случае для экономии ресурсов можно создать эти рендеры (один раз) заранее, а дальнейшем использовать их через компоненты с соответствующими id.
Камера и настройки конвеера рендеринга
- Заметки
- Здесь и далее все создаваемые наборы рендеров добавляются в общий набор объектов, а сцена представляет собой список идентификаторов объектов в этом наборе.
В данном примере формируется объект, состоящий из рендеров:
- Перспективной камеры с компонентом позиции в которую смотрит камера.
- Компонента, очищающего задний буфер и заливающего его указанным цветом.
- Компонента, включающего использование буфера глубины и очищающего его.
- Компонента вертексного шейдера по умолчанию.
- Компонента пиксельного шейдера по умолчанию.
- Компонента updatre'a состояния сцены.
return CreateObject(
{
Component_t::Make(
{
{ uT("id"), uT("Example.Camera.") + Id.GetStringId() },
{ uT("type"), uT("Camera") },
{ uT("kind"), uT("Perspective") },
{ uT("distance"), 10.0f },
{ uT("fov"), 90.0f },
{ uT("service"), GameObject_t{ pCameraPosition } },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.State.Clear") },
{ uT("type"), uT("State") },
{ uT("kind"), uT("Clear") },
{ uT("color"), 0xFF0000FF },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.State.Depth") },
{ uT("type"), uT("State") },
{ uT("kind"), uT("Depth") },
{ uT("enabled"), true },
{ uT("clear"), true },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Shader.Vertex.Default") },
{ uT("type"), uT("Shader") },
{ uT("entry"), uT("vsVolume") },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Shader.Pixel.Default") },
{ uT("type"), uT("Shader") },
{ uT("entry"), uT("psTextured") },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Updater.Simple3DObject.") + Id.GetStringId() },
{ uT("type"), uT("Updater") },
{ uT("function"), GetUpdater() },
}),
});
Источники света
Фоновый и направленный источники света:
if (IsActive(Lights::Ambient))
{
Lights.Ambient.IsValid = 1;
Lights.Ambient.Color = ARGBtoFloat4(0xFF303030);
}
if (IsActive(Lights::Directional))
{
Lights.Direction.IsValid = 1;
Lights.Direction.Color = ARGBtoFloat4(0xFF808080);
Lights.Direction.Direction = ::glm::vec4{ 1.0f, -1.0f, 1.0f, 1.0f };
}
Рендеринг объектов
Объект, отображаемый на экране, представляет собой набор рендеров, который создается на основе набора компонентов и должен включать:
- Вершинный шейдер.
- Пиксельный шейдер.
- Текстуру.
- Вертексный буфер.
- Компонент трансформации.
- Present с индексным буфером (именно он осуществляет отрисовку объекта, поэтому должен располагаться последним).
Компоненты Data.Position/Data.Rotation/Data.Scale используются для перемещения/вращения/изменения размеров объектов без их пересоздания (достаточно установить новые значения параметров при обновлении сцены), причем:
- Компонентов одного типа может быть несколько (что позволяет, например, создавать группы объектов, которые должны быть расположены относительно друг друга определенным образом, но при этом двигаться/вращаться вместе; во втором случае необходимо использовать общий объект компонента, использование разных объектов с одинаковым id нужного эффекта не даст).
- Важен порядок следования компонентов (поворот-перемещение даст сначала поворот вокруг точки [0, 0, 0] с нулевым радиусом, а затем уже смещение повернутого объекта, а перемещение-поворот даст сначала смещение от точки [0, 0, 0], а затем уже поворот вокруг нее с радиусом смещения).
- Необходимо сделать:
- Добавить картинку, объясняющую разницу между поворот-смещение и смещение-поворот.
Текстурированный объект
Текстуры, которые будут использоваться, можно подгрузить заранее, чтобы потом ссылаться на них по их идентификатору.
const image::Universal_t<image::pixel::RGBA> Image
{
::covellite::app::Vfs_t::GetInstance().GetData(
PathToTextureDirectory / _RelativePathToSourceFile)
};
(*_pData)[uT("content")] = Image.GetData().Buffer;
(*_pData)[uT("width")] = static_cast<int>(Image.GetData().Width);
(*_pData)[uT("height")] = static_cast<int>(Image.GetData().Height);
return
{
Component_t::Make(
{
{ uT("id"), _Id },
{ uT("type"), uT("Texture") },
{ uT("service"), GameObject_t{ _pData } },
}),
};
Пример формирования текстурированного объекта; по порядку:
- Вертексный шейдер для отрисовки текстурированных объектов.
- Пиксельный шейдер для отрисовки объектов c текстурой.
- Текстуры.
- Вертексный буфер, содержащий уникальные для данного объекта данные вершин.
- Компонент трансформации, включающий:
- Компонент масштабирования.
- Компонент вращения (создан заранее, что можно было обновлять его значения перед отрисовкой каждого кадра).
- Компонент смещения.
- Компонент отрисовки объекта.
Component_t::Make(
{
{ uT("id"), uT("Example.State.Sampler") },
{ uT("type"), uT("State") },
{ uT("kind"), uT("Sampler") },
}),
pVertexShader,
Component_t::Make(
{
{ uT("id"), uT("Example.Buffer.Vertex.Cube") },
{ uT("type"), uT("Buffer") },
{ uT("content"), m_VertexData },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Shader.Pixel.Cube.PBR") },
{ uT("type"), uT("Shader") },
{ uT("content"), PixelShaderData },
{ uT("entry"), uT("psPbrSimple") },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Texture.Albedo") },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Texture.Reflection") },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Texture.Normal") },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Texture.Environment") },
}),
return CreateObject(
{
Component_t::Make(
{
{ uT("id"), uT("Example.Transform.") + Id.GetStringId() },
{ uT("type"), uT("Transform") },
{ uT("service"), Transform },
}),
Component_t::Make(
{
{ uT("id"), uT("Example.Present") },
{ uT("type"), uT("Present") },
{ uT("content"), m_IndexData },
})
});
- Заметки
- Можно использовать текстуру, содержащую несколько изображений для разных объектов, но при этом все равно необходимо устанавливать компонент этой текстуры каждому объекту, т.к. Present отключает использование текстур после отрисовки объекта.
Скайбокс
Для отрисовки скайбокса необходимо сформировать отдельную сцену, содержащую компоненты:
- Камеры, которая 'смотрит' в начало координат с нулевой дистанции и вращается синхронно с камерой, привязанной к персонажу, управляемому игроком.
- Компонента, отключающего использование буфер глубины.
- Фоновый источник света и материал белого цвета (0xFFFFFFFF).
- Шейдеры, текстуру и куб в начале координат, вертексный буфер которого сформирован таким образом, чтобы отрисовывались его внутренние поверхности.
Подготовка 3D модели
- Предупреждения
- Visual Studio умеет отображать и редактировать 3D модели (форматы .obj, .dae и .fbx), но при сохранении результата в файл .obj он сохраняется как utf8 без сигнатуры, после чего при повторном открытии этого файла он открывается как текстовый файл. Чтобы Visual Studio открыл его в 3D редакторе, необходимо пересохранить файл как utf8 с сигнатурой.
При подготовке файла 3D модели к использованию фреймворком возможны следующие действия:
Триангуляция
3D редактор Visual Studio:
Перенос/поворот/масштабирование модели
3D редактор Visual Studio:
Объединение нескольких текстур в одну
Фреймворк поддерживает использование только одной текстуры для одной модели (модель = вертексный + индексный буфер), поэтому для файлов, содержащих несколько отдельных моделей, каждой из которых задана своя текстура, необходимо произвести объединение мешей (это делает 3D редактор Visual Studio) и текстур (например, при помощи GIMP). Проблема заключается в том, что у 3D модели необходимо модифицировать текстурные координаты таким образом, чтобы она правильно отображалась с объединенной текстурой.
При помощи Blender'а это можно сделать так:
- Открываем файл 3D модели и переключаемся в режим редактирования текстурных координат.
- Для материала каждой модели - выбираем его, наводим курсор на поле развертки и нажимаем A, чтобы выделить всю развертку целиком.
- Для изменения масштаба нажимаем S (после чего X или Y, если масштабирование требуется только по одной оси) и, перемещая мышь, изменяем размер развертки до требуемого (с зажатым Ctrl - с шагом 0.1).
- Для перемещения нажимаем G и, перемещая мышь, перемещаем развертку в нужное место текстуры (с зажатым Ctrl - с шагом 1/8 размера текстуры).
- После экспорта результата модель будет использовать новые текстурные координаты.