Архитектура движка.Статьи

Архитектура рендера. (data holder <-> render agent architecture)

Автор:

Автор: Владислав Гусев aka xmvlad
Архитектура рендера. (data holder <-> render agent architecture) 

Каждый программист знает, что дьявол находится в деталях, хочу предупредить сразу, что дьявола сегодня не будет, поэтому разговор пойдет в основном про ангелов(обитающих в концепциях и идеях),  щедро приправленных моим имхо :)

Любая архитектура отражает точку зрения ее проектировщика. При этом каждый уверен в том, что его решения являются наиболее верными и правильными, спорить в таких случаях бессмысленно.  Архитектура это путь, для того что бы попасть в какую-то точку(реализовать необходимую функциональность), можно пройти несколькими путями и часто, трудно сказать, какой из путей лучше или хуже.  Но если, одна дорога - ведет прямо к цели, а другая -  витиевато уводит в глушь, стоит задуматься куда и зачем мы собираемся идти.

Путь №1 - "Рендер это большая обертка над 3D API!".

Концепция: Рендер является тонким слоем абстракции над некоторым/некоторыми 3D API(D3D/OGL).

Проблема заключается в том, что низко уровневая обертка - не является 3Д движком, помимо обертки необходимо еще очень много другой - "высоко уровневой"  функциональности(отсечение, LOD, освещение, тени, различные эффекты(отражение, blur, ....),  ландшафты,  системы частиц, скелетная анимация,  водичка, etc). И в случаи развития этой "высоко уровневой" функциональности, происходит следующее:

а) обеспечение потребностей "высоко уровневого" кода в "низко уровневой" функциональности(уровня 3D API) приводит к разрастанию обертки, в результате обертка "покрывая" функциональность 3D API, становится все больше и больше похожей на API, то есть просто дублирует ее. (обычно выражается в мегабайтах "ничего не делающего/бесполезного" кода)

б) обертка лишь усложняет реализацию мультиплатформенности/мультиапишность, механизм тот же самый, что и в пункте а):
"высокоуровневому" коду практически всегда необходима связь с конкретной платформой(в силу архитектурных отличий самих платформы),  в результате - обертка, предоставляя необходимый низкоуровневый интерфейс, дублирует каждую из платформ.
Ярким представителем данного направления является XEngine, и в той или иной степени, многие open source движки.


Путь №2 - "Наши объекты, самые абстрактные и толстые объекты в мире!"

Концепция: Общая иерархия "объектов"(статические меши, скелетная анимация, системы частиц и другие "объекты сцены") во главе со Scene_Object. Взаимодействие с рендером реализовано через этот интерфейс(Scene_Object), то есть рендер получает Scene_Object(или другой но все такой же "абстрактный" интерфейс). Что рендер будет с ним делать дальше - его сугубо личные проблемы, которые никого не ......, волнуют. Одна из причин - абстрактный интерфейс Scene_Object, единственное, что вываливается из используемых _однородных_ структур. (BSP, quadtree, octtreee, etc)

Очевидно, возникают следующие проблемы, для того что бы рендер мог взаимодействовать с объектами в иерархии, нужен:
а) либо, "очень толстый и пухлый" интерфейс Scene_Object, отражающий объекты лежащие ниже в иерархии
б) либо, использовать QueryInterface, rtti или другую подобную - type code/switch-like технику, для получения нужного интерфейса.


Путь №3 - "Посмотрите, как я умею красиво себя рисовать!"

Концепция: Каждый объект сцены рендерит себя сам.

Плюсы: код осуществляющий рендеринг работает с  _конкретным_  типом объекта, поэтому проблемы возникающие в №2 отсутствуют. Но возникают другие...
Минусы: Простейший пример: для того что бы собрать  и отсортировать по материалам батч, необходимы данные от всех/нескольких объектов сцены. Этот пример не единственный, очевидно встает задача: централизованного сбора/обработки "общих" структур данных, используемых в процессе рендеринга. И так же, декомпозиции самого рендера, который в случае централизации обработки в нем, сам не слабо "распухнет".


Путь №4 - "У меня есть знакомый агент, пусть он и рисует!"

Концепция: Scene_Object(АБК) - предоставляет простейший интерфейс для управления рендерингом. Остальные объекты сцены наследуются от Scene_Object  и являются - data holders, то есть хранят данные + реализуют простой интерфейс для пользователя, через который он ими управляет. Вся обязанность и ответсвенность за рендеринг, возлагается на агентов рендера - render agents. Каждый конкретный рендер агент знает, как рендерить некоторых конкретных data holders. То есть например, Static_Mesh_Render знает как рендерить Static_Mesh(и соответсвенно, тех кто наследует интерфейс Static_Mesh), Sceleton_Render знает как ренедерить Sceleton_Anim(и тех кто наследует интерфейс Sceleton), Landscape_Render знает как рендерить Landsape_Mesh etc. При этом рендер агент не только знает как рендерить, но и организует сбор и централизованную обработку данных, то есть осуществляет сортировку по состояниям, LOD и т.д

Плюсы:
а) каждый конкретный рендер агент имеет доступ к каждому конкретному объекту(дата холдеру). Под конкретным здесь подразумевается _нужный_ уровень абстракции.

из а) следует:

в) простая реализация мульти апи, мульти платформенности (за счет того, что весь процесс обработки изменяется на "высоком" уровне, заточить можно подо что угодно, достаточно добавить дата холдеров и их рендер агентов под конкретную платформу.... не нужны, тонкие и бессмысленные обертки над апи :) опять же скининг можно сделать несколько вариантов, на шейдерах, на цпу и т.д :)

г) отличная декомпозиция -> простота, гибкость, прямая архитектура.

Понять без кода, что это такое я предлагаю, не просто, поэтому:

// базовый класс дата холдеров
class Object_3D
{
public:
        virtual void render() = 0;
        virtual void render_shadow() = 0;
};

class Static_Mesh: public Object_3D
{
public:
      void render()
      {
              static_mesh_render->render_static_mesh(this); // this - Static_Mesh
      }

private:
    Static_Mesh_Render*   static_mesh_render;
};

class Sceleton: public Object_3D
{
public:
      void render()
      {
              sceleton_render->render_sceleton(this); // this - Sceleton
      }

private:
    Sceleton_Render*   sceleton_render;
};

class Render_Agent
{
};

class Static_Mesh_Render: public Render_Agent
{
public:
           // concrete, not virtual method !
           void render_static_mesh(Static_Mesh* static_mesh)
           {
                  // получаем данные от дата холдера, раскладываем по текстурам и шейдерам
                  // производим необходимую обработку скелетка и т.д.
                 render_data
           }

          void do_real_rendering()
          {
                 // рендерим сохраненные данные батчами
          }

private:
          //  данные дата холдеров
          Render_Data    render_data;
};

class Sceleton_Render: public Render_Agent
{
public:
            // concrete, not virtual method !
            void render_sceleton(Sceleton_Mesh* sceleton_mesh)
            {
                  // получаем данные от дата холдера, раскладываем по текстурам и шейдерам
                  // производим необходимую обработку скелетка и т.д.
                 render_data
            }

          void do_real_rendering()
          {
                 // рендерим сохраненные данные батчами
          }

private:
          //  данные дата холдеров
          Render_Data2    render_data2;
};

// рендер агенты, если есть необходимость, шарят общие структуры данных. 

PS:
У каждого рендер агента есть две функции, одна - render_some_object, например, у конкретного рендера Static_Mesh_Render::render_static_mesh(Static_Mesh*), через этот метод объект(Static_Mesh) передает себя(this указатель на конкретный тип). Внутри метода производится простейшая обработка - сохранение данных объекта, во внутренние структуры рендер агента(Static_Mesh_Render), а так же простейшая их обработка. После того, как каждый конкретные объект отправил данные своему ренедер агенту(одному для каждого типа объектов), происходит сортировка собранных данных по материалам + дополнительная обработка, затем рендеринг батчами. Задача отсечения невидимой геометрии осущетвляется, до передачи информации рендеру, обычно для этого используются различные однородные структуры данных (octtree, bsp, quadtree) на "выходе", которых будет Object_3D, вызов ->render() и далее  по вышеуказанной схеме. :)

ЭПИЛОГ

Всё, последний ангел улетел вдаль :) возможно получилось путано(хотя я честное слово, пытался излагать мысли просто и доступно),  возможно я говорил о слишком простых и тривиальных вещах.
В любом случаи мне важно ваше мнение, оставьте свой комментарий!

6 января 2006

Комментарии [92]