Rendering Engine Design - отрисовка кода API для ресурсов - программирование

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. Любые советы по моей ситуации будут очень признательны.

4b9b3361

Ответ 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, я бы лучше понял, что мне нужно, чтобы сделать код рендеринга, поэтому , который будет подходящим временем для рефакторинга и вытащить более абстрактный интерфейс.