Skip to content
This repository has been archived by the owner on Dec 10, 2022. It is now read-only.

1vanK/Urho3DTutor01

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Более актуальная версия находится тут: https://github.com/urho3d-learn/direct-gapi-usage.


Urho3D: Движок в движке

Данный туториал является продолжением цикла статей на Хабрахабре.

Urho3D является сцено-ориентированным движком. То есть вы просто создаете иерархию объектов, определяете их свойства и поведение, и всё там живет своей жизнью, самостоятельно рендерится и тому подобное. Это удобно в подавляющем большинстве случаев. Но иногда вам может понадобиться олдскульный подход, когда вы каждый кадр сами рисуете содержимое экрана. Ярчайшим примером такого подхода является игра Terraria, где весь мир хранится в гигантском двухмерном массиве, а на экран выводятся только видимые тайлы.

Внедряемся в движок

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

void Engine::Render()
{
    GetSubsystem<Renderer>()->Render(); // Рендерится игра.
    GetSubsystem<UI>()->Render(); // Рендерится интерфейс.
    graphics->EndFrame();
}

void Renderer::Render()
{
    graphics_->Clear(...);
    // Очистка экрана. Все события до этого момента не пригодны для ручной прорисовки.

    for(...) // Цикл рендерит все вьюпорты.
        views_[i]->Render();

    // Функция View::Render() генерирует серию событий. Событие E_ENDVIEWRENDER возникает последним
    // и пригодится, если вы хотите внедрить свою геометрию в уже отрендеренную сцену. Но нам оно не
    // подходит, так как у нас вообще не будет ни одного вьюпорта и ни одной сцены, так что эти
    // события даже не будут возникать.

    SendEvent(E_ENDALLVIEWSRENDER);
    // Это то, что нам нужно. Рисуем здесь свою геометрию и она будет находиться под интерфейсом
    // (вдруг вам понадобится отладочный худ).
}

void Graphics::EndFrame()
{
    SendEvent(E_ENDRENDERING);
    // Это событие возникает после рендеринга интерфейса. Может быть полезно, если вы хотите
    // самостоятельно нарисовать анимированный курсор мыши.
}

Рисуем треугольник

Urho3D поддерживает OpenGL и DirectX. Между ними довольно много различий, но Urho3D унифицирует доступ к обоим API там, где это возможно. Для прямого вызова функций графического API предназначен класс Graphics. Обратитесь к примеру Step1/Game.cpp. Всё проще некуда:

  1. Создаем вершинный буфер и записываем в него координаты трех вершин. Помните, что вершины нужно задавать по часовой стрелке, так как порядок вершин определяет лицевую грань полигона.
  2. Устанавливаем текущий вершинный буфер и текущую шейдерную программу (в общем, настраиваем Render State).
  3. Вызываем функцию Graphics::Draw(...), которая отрендерит текущую геометрию текущим шейдером.

Код из примера без изменений скомпилируется как для OpenGL, так и для DirectX, однако шейдеры для разных API требуются разные: glsl, hlsl. Urho3D сам будет загружать нужный шейдер, в зависимости от используемого API.

Скриншот

В примере показаны два способа задания формата вершин: старый с фиксированным порядком элементов (атрибутов) и более новый с произвольным порядком (с одним исключением: координаты вершины всегда должны быть первыми, чтобы работал рейкаст). Второй способ был добавлен в этом патче. Кстати, это также стало причиной изменения формата моделей. Порядок атрибутов можно посмотреть тут или в файле Urho3D/Graphics/GraphicsDefs.h.

Индексные буферы и собственный убершейдер

В первом примере для описания модели использовался только вершинный буфер. На практике так поступают редко. К примеру, возьмем квадрат. Он составляется из двух треугольников. Значит, требуется шесть вершин, причем две вершины одинаковы для обоих треугольников. В среднестатистической модели число совпадающих вертексов огромно. Поэтому все уникальные вершины хранятся в вершинном буфере, а сами модели описываются с помощью дополнительного индексного буфера, который вместо самих вершин хранит индексы вершин из вертексного буфера (извините за тавтологию).

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

Скриншот

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

Обратите внимание на центральный прямоугольник. На самом деле он является квадратом. Дело в том, что оконные координаты находятся в диапазоне [-1, 1] по вертикали и горизонтали (ось Y направлена вверх), а само окно не квадратное. Поэтому происходит искажение. Фиолетовый квадрат имеет абсолютно те же размеры, однако он скорректирован с помощью матрицы модели. На эту матрицу умножаются координаты каждой вершины в функции GetWorldPos() (смотрите шейдер Transform).

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

graphics->SetDepthTest(CMP_LESSEQUAL);

и воспользоваться матрицами из камеры

graphics->SetShaderParameter(VSP_VIEWPROJ, camera->GetGPUProjection() * camera->GetView());

Чтобы геометрия была видна с обеих сторон, можно также отключить отбрасывание задних граней

graphics->SetCullMode(CULL_NONE);

Естественно, при выводе геометрии подобным образом на нее не будут действовать никакие источники света, если вы сами не передадите информацию об освещении в шейдеры.

Скриншот

Полный исходник не приводится, поскольку он тривиален.

Батчинг

Число драв коллов (вызовов функции Draw) и переключений стейта (Render State) при рендеринге должно быть минимизировано (смотрите Оптимизация OpenGL и Direct3D Performance Optimizations). Кстати, есть интересная статья, разъясняющая, что узким местом при этом является процессор, а не видеокарта. Все, что можно отрендерить без переключения шейдеров и текстур, необходимо сгруппировать (именно поэтому эффективны текстурные атласы).

Обратитесь к третьему примеру (SpriteBatch.h, SpriteBatch.cpp, Game.cpp). Принцип работы очень прост:

  1. Когда пользователь вызывает функцию SpriteBatch::Draw(), спрайт не рендерится мгновенно, а просто добавляется в список.
  2. При вызове функции SpriteBatch::End() все подряд идущие спрайты с одинаковой текстурой записываются в один буфер и выводятся за один Draw Call.

Скриншот

Этот пример является максимально урезанной и упрощенной версией SpriteBatch (для легкости понимания). А полная версия доступна тут.

Дополнение

В DirectX 9 (в отличие от OpenGL и DirectX 10 и выше) есть смещение на пол пикселя. Это учтено в полной версии.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published