Архитектура рендера. (data holder <-> render agent architecture)
Автор: Vladislav Gusev
Автор: Владислав Гусев 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