Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rendering Engine Design - Abstracting away API specific code for Resources [closed]

I have a very big design stumbling block in my rendering code. Basically what this is, is not requiring API specific code (such as OpenGL code or DirectX). Now I've thought of numerous ways on how to solve the problem, however I'm not sure which one to use, or how I should improve upon these ideas.

To give a brief example, I will use a Texture as an example. A texture is an object which represents a texture in GPU memory, implementation wise it may be resembled in any particular way, i.e. whether implementation uses a GLuint or LPDIRECT3DTEXTURE9 to resemble the texture.

Now here are the ways I've thought of to actually implement this. I'm quite unsure if there is a better way, or which way is better than another.


Method 1: Inheritance

I could use inheritance, it seems the most obvious choice for this matter. However, this method requires virtual functions, and would require a TextureFactory class in order to create Texture objects. Which would require calls to new for each Texture object (e.g. renderer->getTextureFactory()->create()).

Here's how I'm thinking of using inheritance in this case:

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;
};

and then in order for OpenGL (or any other API specific) textures to be created, a sub-class would have to be made, such as OglTexture.

Method 2: Use a 'TextureLoader' or some other class

This method is as simple as it sounds, I use another class to handle loading of textures. This may or may not use virtual functions, depending on the circumstance (or whether I feel it is necessary).

e.g. A polymorphic texture loader

 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;
 };

If I were to use this, a Texture object would only be a POD type. However, in order for this to work, a handle object/ID would have to be present within the Texture class.

For example, this is how I would more than likely implement it. Although, I may be able to generalise the whole ID thing, using a base class. Such as a Resource base class in which case holds an ID for a graphics resource.

Method 3: The Pimpl Idiom

I could use the pimpl idiom, which implements how to load/reload/etc. textures. This would more than likely require an abstract factory class for creation of textures. I am unsure how this is better than using inheritance. This pimpl idiom could be used in conjunction with Method 2, i.e. Texture objects would have a reference (pointer) to their loader.

Method 4: Using concepts/compile-time polymorphism

I could on the other hand, use compile-time polymorphism and basically use what I presented in the inheritance method, except without declaring virtual functions. This would work, but if I wanted to dynamically switch from OpenGL rendering to DirectX rendering, this would not be the best solution. I would simply put OpenGL/D3D specific code within the Texture class, where there would be multiple texture classes with some-what the same interface (load/reload/getImage/etc.), wrapped inside some namespace (resembling which API it uses, e.g. ogl, d3d, etc.).

Method 5: Using integers

I could just use integers to store handles to texture objects, this seems fairly simple, but may produce some-what "messy" code.


This problem is also present for other GPU resources such as Geometry, Shaders, and ShaderPrograms.

I've also thought of just making the Renderer class handle the creation, loading, and etc. of graphical resources. However this would violate SPR. e.g.

Texture* texture = renderer->createTexture(Image("something.png"));
Image image = renderer->getImage(texture);

Can someone please guide me, I think I'm thinking too heavily about this. I've tried observing various rendering engines, such as Irrlicht, Ogre3D, and others I have found online. Ogre and Irrlicht use inheritance, however I am unsure that this is the best route to take. As some others just use void*, integers, or just put API specific (mainly OpenGL) code within their classes (e.g. GLuint directly within the Texture class). I really cannot decide which design would be the most appropriate for me.

The platforms I am going to target are:

  • Windows/Linux/Mac
  • iOS
  • Possibly Android

I have considered to just use OpenGL specific code, as OpenGL works for all of those platforms. However, I feel that if I do that I will have to change my code quite a lot if I wish to port to other platforms that cannot use OpenGL, such as the PS3. Any advice on my situation will be greatly appreciated.

like image 894
miguel.martin Avatar asked Mar 21 '13 13:03

miguel.martin


3 Answers

Think of it from a high-level point of view. How will your rendering code work with the rest of you game/application model? In other words, how do you plan to create objects in your scene and to what degree of modularity? In my previous work with engines, the end result of a well-designed engine generally has a step-by-step procedure that follows a pattern. For example:

//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);

Once a component has been obtained there are usually a variety of overloaded methods you could use to construct the object. Using a sprite as an example, a SpriteComponent could contain everything potentially needed by a sprite in the form of sub-components; like a TextureComponent for instance.

//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);

Then it's simply a matter of adding the object to the scene. This could be done by making an entity, which is simply a generic collection of information that would contain everything needed for scene manipulation; position, orientation, etc. For every entity in your scene, your AddEntity method would add that new entity by default to your render factory, extracting other render-dependent information from sub-components. E.g:

//Put our sprite onto the scene to be drawn
pSprite->SetColour(CColour::YELLOW);
EntityPtr pEntity = CreateEntity(pSprite);
mpScene->AddEntity(pEntity);

What you then have is a nice way of creating objects and a modular way of coding your application without having to reference 'draw' or other render-specific code. A good graphics pipeline should be something along the lines of:

enter image description here

This is a nice resource for rendering engine design (also where the above image is from). Jump to page 21 and read onwards where you'll see in-depth explainations of how scenegraphs operate and general engine design theory.

like image 170
KillAWatt1705 Avatar answered Oct 01 '22 00:10

KillAWatt1705


I don't think there's any one right answer here, but if it were me, I would:

  1. Plan on using only OpenGL to start with.

  2. Keep rendering code separate from other code (that's just good design), but don't try to wrap it in an extra layer of abstraction - just do whatever is most natural for OpenGL.

  3. Figure that if and when I was porting to PS3, I would have a much better grasp of what I need my rendering code to do, so that would be the right time to refactor and pull out a more abstract interface.

like image 22
Russell Zahniser Avatar answered Oct 01 '22 00:10

Russell Zahniser


I've decided to go for a hybrid approach, with method (2), (3), (5) and possibly (4) in the future.

What I've basically done is:

Every resource has a handle attached to it. This handle describes the object. Each handle has an ID associated with it, which is a simple integer. In order to talk to the GPU with each resource, an interface for each handle is made. This interface is at the moment abstract, but could be done with templates, if I choose to do so in the future. The resource class has a pointer to an interface.

Simply put, a handle describes the actual GPU object, and a resource is just a wrapper over the handle and an interface to connect the handle and the GPU together.

This is what it basically looks like:

// 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;
};

This allows me to extend quite easily, and allows for syntax such as:

Renderer renderer;

// create a texture
Texture texture(renderer);

// load the texture
texture.load(Image("test.png");

Where Texture derives from Resource<TextureHandle, TextureHandleInterface>, and where renderer has the appropriate interface for loading texture handle objects.

I have a short working example of this here.

Hopefully this works, I may choose to redesign it in the future, if so I will update. Criticism would be appreciated.

EDIT:

I have actually changed the way I do this again. The solution I am using is quite similar to the one described above, but here is how it is different:

  1. The API revolves around "backends", these are objects that have a common interface and communicate with a low-level API (e.g. Direct3D or OpenGL).
  2. Handles are no longer integers/IDs. A backend has specific typedef's for each resource handle type (e.g. texture_handle_type, program_handle_type, shader_handle_type).
  3. Resources do not have a base class, and only require one template parameter (a GraphicsBackend). A resource stores a handle and a reference to the graphics backend it belongs to. Then the resource has a user-friendly API and uses the handle and graphics backend common interface to interact with the "actual" resource. i.e. resource objects are basically wrappers of handles that allow for RAII.
  4. A graphics_device object is introduced to allow construction of resources (factory pattern; e.g. device.createTexture() or device.create<my_device_type::texture>(),

For example:

#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

This design is currently being put in use with my library rojo (note: this library is still under heavy development).

like image 27
miguel.martin Avatar answered Oct 01 '22 01:10

miguel.martin