Rendering Engine Design - отрисовка кода API для ресурсов
В моем коде рендеринга у меня очень большой блок преткновения для дизайна. В основном, что это такое, не требуется API-код (например, OpenGL-код или DirectX). Теперь я подумал о многочисленных способах решения проблемы, однако я не уверен, какой из них использовать, или как я должен улучшить эти идеи.
Чтобы привести краткий пример, я буду использовать текстуру в качестве примера. Текстура представляет собой объект, который представляет собой текстуру в памяти графического процессора, причем реализация мудрая может быть похожа каким-либо конкретным образом, то есть ли реализация использует GLuint
или LPDIRECT3DTEXTURE9
, чтобы напоминать текстуру.
Теперь вот как я думал о том, чтобы на самом деле реализовать это. Я совершенно не уверен, есть ли лучший способ, или какой путь лучше другого.
Метод 1: Наследование
Я мог бы использовать наследование, это кажется самым очевидным выбором в этом вопросе. Однако для этого метода требуются виртуальные функции, и для создания объектов Texture требуется класс TextureFactory. Для чего потребуется вызов new
для каждого объекта Texture
(например, renderer->getTextureFactory()->create()
).
Вот как я думаю об использовании наследования в этом случае:
class Texture
{
public:
virtual ~Texture() {}
// Override-able Methods:
virtual bool load(const Image&, const urect2& subRect);
virtual bool reload(const Image&, const urect2& subRect);
virtual Image getImage() const;
// ... other texture-related methods, such as wrappers for
// load/reload in order to load/reload the whole image
unsigned int getWidth() const;
unsigned int getHeight() const;
unsigned int getDepth() const;
bool is1D() const;
bool is2D() const;
bool is3D() const;
protected:
void setWidth(unsigned int);
void setHeight(unsigned int);
void setDepth(unsigned int);
private:
unsigned int _width, _height, _depth;
};
а затем для создания текстур OpenGL (или любых других API), должен быть создан подкласс, например OglTexture
.
Способ 2. Используйте "TextureLoader" или какой-либо другой класс
Этот метод так же прост, как кажется, я использую другой класс для обработки загрузки текстур. Это может или не может использовать виртуальные функции, в зависимости от обстоятельств (или я считаю, что это необходимо).
например. Полиморфный загрузчик текстур
class TextureLoader
{
public:
virtual ~TextureLoader() {}
virtual bool load(Texture* texture, const Image&, const urect2& subRect);
virtual bool reload(Texture* texture, const Image&, const urect2& subRect);
virtual Image getImage(Texture* texture) const;
};
Если бы я использовал это, объект Texture
был бы только POD-типом. Однако для того, чтобы это сработало, в классе Texture
должен присутствовать объект /ID дескриптора.
Например, это то, как я бы более чем мог реализовать его. Хотя, я могу обобщить всю идентификационную вещь, используя базовый класс. Например, базовый класс Resource
в этом случае содержит идентификатор графического ресурса.
Метод 3: Идиома Pimpl
Я мог бы использовать идиому pimpl, которая реализует загрузку/перезагрузку/etc. текстуры. Это более чем вероятно потребует абстрактного класса factory для создания текстур. Я не уверен, как это лучше, чем использование наследования. Эта идиома pimpl может использоваться в сочетании с методом 2, т.е. Объекты текстуры будут иметь ссылку (указатель) на свой загрузчик.
Способ 4. Использование концепций/полиморфизм времени компиляции
Я мог бы с другой стороны использовать полиморфизм времени компиляции и в основном использовать то, что я представил в методе наследования, кроме как без объявления виртуальных функций. Это сработает, но если бы я хотел динамически переключиться с рендеринга OpenGL на рендеринг DirectX, это не было бы лучшим решением. Я бы просто поместил специальный код OpenGL/D3D в класс Texture, где было бы несколько классов текстур с каким-то одним и тем же интерфейсом (load/reload/getImage/etc.), Завернутым в какое-то пространство имен (похожее на тот API, который он использует, например, ogl
, d3d
и т.д.).
Метод 5: Использование целых чисел
Я мог бы просто использовать целые числа для хранения дескрипторов для объектов текстуры, это кажется довольно простым, но может создать какой-то "грязный" код.
Эта проблема также присутствует и для других ресурсов графического процессора, таких как Geometry, Shaders и ShaderPrograms.
Я также подумал о том, как сделать класс Renderer обработкой создания, загрузки и т.д. графических ресурсов. Однако это нарушит SPR. например
Texture* texture = renderer->createTexture(Image("something.png"));
Image image = renderer->getImage(texture);
Может ли кто-нибудь, пожалуйста, направить меня, я думаю, что слишком много думал об этом. Я пробовал наблюдать за различными механизмами рендеринга, такими как Irrlicht, Ogre3D и другие, которые я нашел в Интернете. Ogre и Irrlicht используют наследование, однако я не уверен, что это лучший путь. Поскольку некоторые другие просто используют void *, целые числа или просто добавляют API-код (в основном OpenGL) в свои классы (например, GLuint непосредственно в классе Texture). Я действительно не могу решить, какой дизайн будет наиболее подходящим для меня.
Платформами, на которые я нацелен, являются:
- Windows/Linux/Mac
- IOS
- Возможно, Android
Я рассматриваю просто использовать специальный код OpenGL, поскольку OpenGL работает для всех этих платформ. Тем не менее, я чувствую, что если я это сделаю, мне придется очень сильно изменить свой код, если я хочу портировать другие платформы, которые не могут использовать OpenGL, например, PS3. Любые советы по моей ситуации будут очень признательны.
Ответы
Ответ 1
Я решил пойти на гибридный подход, с использованием методов (2), (3), (5) и, возможно, (4) в будущем.
Что я в основном сделал:
У каждого ресурса есть дескриптор, прикрепленный к нему. Этот дескриптор описывает объект. Каждый дескриптор имеет связанный с ним идентификатор, который является простым целым числом. Чтобы поговорить с GPU с каждым ресурсом, создается интерфейс для каждого дескриптора. Этот интерфейс на данный момент является абстрактным, но может быть выполнен с помощью шаблонов, если я захочу сделать это в будущем. Класс ресурсов имеет указатель на интерфейс.
Проще говоря, дескриптор описывает фактический объект GPU, а ресурс - всего лишь оболочка над дескриптором и интерфейс для соединения дескриптора и графического процессора вместе.
Это то, что в основном выглядит:
// base class for resource handles
struct ResourceHandle
{
typedef unsigned Id;
static const Id NULL_ID = 0;
ResourceHandle() : id(0) {}
bool isNull() const
{ return id != NULL_ID; }
Id id;
};
// base class of a resource
template <typename THandle, typename THandleInterface>
struct Resource
{
typedef THandle Handle;
typedef THandleInterface HandleInterface;
HandleInterface* getInterface() const { return _interface; }
void setInterface(HandleInterface* interface)
{
assert(getHandle().isNull()); // should not work if handle is NOT null
_interface = interface;
}
const Handle& getHandle() const
{ return _handle; }
protected:
typedef Resource<THandle, THandleInterface> Base;
Resource(HandleInterface* interface) : _interface(interface) {}
// refer to this in base classes
Handle _handle;
private:
HandleInterface* _interface;
};
Это позволяет мне довольно легко распространяться и допускает синтаксис, например:
Renderer renderer;
// create a texture
Texture texture(renderer);
// load the texture
texture.load(Image("test.png");
Где Texture
происходит от Resource<TextureHandle, TextureHandleInterface>
и где у рендерера есть соответствующий интерфейс для загрузки объектов обработки текстуры.
У меня есть короткий рабочий пример этого здесь.
Надеюсь, это сработает, я могу выбрать его в будущем, если так я буду обновлять. Критика будет оценена.
EDIT:
Я действительно изменил способ, которым я снова это делаю. Решение, которое я использую, очень похоже на описанное выше, но вот как оно отличается:
- API вращается вокруг "backends", это объекты, которые имеют общий интерфейс и взаимодействуют с низкоуровневым API (например, Direct3D или OpenGL).
- Ручки больше не целые /ID. Бэкэнд имеет специфический typedef для каждого типа дескриптора ресурса (например,
texture_handle_type
, program_handle_type
, shader_handle_type
).
- Ресурсы не имеют базового класса и требуют только одного параметра шаблона (a
GraphicsBackend
). Ресурс хранит дескриптор и ссылку на принадлежащий ему графический сервер. Затем ресурс имеет удобный API и использует общий интерфейс интерфейса и общий интерфейс для взаимодействия с "фактическим" ресурсом. то есть объекты ресурсов являются в основном оболочками дескрипторов, которые позволяют использовать RAII.
- Объект graphics_device вводится для создания ресурсов (шаблон factory, например
device.createTexture()
или device.create<my_device_type::texture>()
,
Например:
#include <iostream>
#include <string>
#include <utility>
struct Image { std::string id; };
struct ogl_backend
{
typedef unsigned texture_handle_type;
void load(texture_handle_type& texture, const Image& image)
{
std::cout << "loading, " << image.id << '\n';
}
void destroy(texture_handle_type& texture)
{
std::cout << "destroying texture\n";
}
};
template <class GraphicsBackend>
struct texture_gpu_resource
{
typedef GraphicsBackend graphics_backend;
typedef typename GraphicsBackend::texture_handle_type texture_handle;
texture_gpu_resource(graphics_backend& backend)
: _backend(backend)
{
}
~texture_gpu_resource()
{
// should check if it is a valid handle first
_backend.destroy(_handle);
}
void load(const Image& image)
{
_backend.load(_handle, image);
}
const texture_handle& handle() const
{
return _handle;
}
private:
graphics_backend& _backend;
texture_handle _handle;
};
template <typename GraphicBackend>
class graphics_device
{
typedef graphics_device<GraphicBackend> this_type;
public:
typedef texture_gpu_resource<GraphicBackend> texture;
template <typename... Args>
texture createTexture(Args&&... args)
{
return texture{_backend, std::forward(args)...};
}
template <typename Resource, typename... Args>
Resource create(Args&&... args)
{
return Resource{_backend, std::forward(args)...};
}
private:
GraphicBackend _backend;
};
class ogl_graphics_device : public graphics_device<ogl_backend>
{
public:
enum class feature
{
texturing
};
void enableFeature(feature f)
{
std::cout << "enabling feature... " << (int)f << '\n';
}
};
// or...
// typedef graphics_device<ogl_backend> ogl_graphics_device
int main()
{
ogl_graphics_device device;
device.enableFeature(ogl_graphics_device::feature::texturing);
auto texture = device.create<decltype(device)::texture>();
texture.load({"hello"});
return 0;
}
/*
Expected output:
enabling feature... 0
loading, hello
destroying texture
*/
Live demo: http://ideone.com/Y2HqlY
Этот проект в настоящее время используется с моей библиотекой rojo ( note: эта библиотека все еще в тяжелом развитии).
Ответ 2
Подумайте об этом с высокоуровневой точки зрения. Как ваш код рендеринга будет работать с остальной частью вашей модели игры/приложения? Другими словами, как вы планируете создавать объекты в своей сцене и в какой степени модульности? В моей предыдущей работе с двигателями конечный результат хорошо продуманного двигателя обычно имеет пошаговую процедуру, которая следует за шаблоном. Например:
//Components in an engine could be game objects such as sprites, meshes, lights, audio sources etc.
//These resources can be created via component factories for convenience
CRenderComponentFactory* pFactory = GET_COMPONENT_FACTORY(CRenderComponentFactory);
Как только компонент был получен, обычно существует множество перегруженных методов, которые вы могли бы использовать для построения объекта. Используя спрайт в качестве примера, SpriteComponent
может содержать все потенциально необходимое для спрайта в виде подкомпонентов; например, TextureComponent
.
//Create a blank sprite of size 100x100
SpriteComponentPtr pSprite = pFactory->CreateSpriteComponent(Core::CVector2(100, 100));
//Create a sprite from a sprite sheet texture page using the given frame number.
SpriteComponentPtr pSprite = pFactory->CreateSpriteComponent("SpriteSheet", TPAGE_INDEX_SPRITE_SHEET_FRAME_1);
//Create a textured sprite of size 100x50, where `pTexture` is your TextureComponent that you've set-up elsewhere.
SpriteComponentPtr pSprite = pFactory->CreateSpriteComponent(Core::CVector2(100, 50), pTexture);
Тогда это просто вопрос добавления объекта к сцене. Это можно сделать, создав сущность, которая представляет собой просто общий набор информации, который будет содержать все необходимое для манипулирования сценой; положение, ориентация и т.д. Для каждого объекта в вашей сцене ваш метод AddEntity
добавит этот новый объект по умолчанию к вашему рендерингу factory, извлекая другую зависимую от визуализации информацию из подкомпонентов. Например:
//Put our sprite onto the scene to be drawn
pSprite->SetColour(CColour::YELLOW);
EntityPtr pEntity = CreateEntity(pSprite);
mpScene->AddEntity(pEntity);
То, что у вас есть, - это хороший способ создания объектов и модульный способ кодирования вашего приложения без ссылки на "draw" или другой код, предназначенный для визуализации. Хороший графический конвейер должен быть чем-то вроде:
![enter image description here]()
Это - хороший ресурс для рендеринга дизайна двигателя (также, где находится вышеприведенное изображение). Перейдите на страницу 21 и прочитайте дальше, где вы увидите подробные объяснения того, как работают сценографы и общая теория проектирования двигателей.
Ответ 3
Я не думаю, что здесь есть один правильный ответ, но если бы это был я, я бы:
-
Планируйте использовать только OpenGL для начала.
-
Держите код рендеринга отдельно от другого кода (это просто хороший дизайн), но не пытайтесь обернуть его дополнительным слоем абстракции - просто делайте то, что наиболее естественно для OpenGL.
-
Рисунок, что если и когда я портировал PS3, я бы лучше понял, что мне нужно, чтобы сделать код рендеринга, поэтому , который будет подходящим временем для рефакторинга и вытащить более абстрактный интерфейс.