Object Abstraction in OpenGL
IntroductionWe are beginning to see more and more use of the notion of an object in OpenGL. An object is simply represented by an unsigned integer that is used as the parameter to various state setting functions. In many cases when working with objects in OpenGL you will be performing the same common tasks with these objects: Creation, Binding and Destruction. I realised this when I was implementing the sub-classes Texture1D, Texture2D, Texture3D etc.. and found I was simply writing the same code over and over, but with just a different constant (in this case, although we see that this can be generalised to different entry points) and decided that an abstraction was needed. My immediate thought was to move this into the base class GLTexture, however I realised I would come across this problem again when implementing vertex and fragment programs(ARB, EXT, NV), vertex buffers(VBO, VAR, VAO). Also note that mechanisms such as ARB_occlusion_query uses a similar notion of an object. The problem with providing a common abstraction is that each of these extensions uses different entry points and/or parameters. The solution to this problem is to use templates and to move the entry point calls into a helper class. So my initial implementation of the Object interface was: template<class ObjectOps> class Object { public: Object() : ID(ObjectOps::GenerateID()) {} ~Object() { ObjectOps::Destroy(ID); } void Bind() const { ObjectOps::Bind(ID); } protected: GLuint ID; }; Notice how simple this class is. The work is done in the ObjectOps classes. I've opted to use all static member functions for the ObjectOps interface, but you could make it into a class and use the Pimpl idiom, allocating an instance as a private variable, but I choose to avoid the overhead of the allocation of the object, any data needed can be stored as private static variables. A sample implementation of an ObjectOps class(for ARB_vertex_program): class ARBVertexProgramOps { public: static GLuint GenerateID() { GLuint ID; glGenProgramsARB(1, &ID); return ID; } static void Destroy(GLuint ID) { glDeleteProgramsARB(1, &ID); } static void Bind(GLuint ID) { if(ID != currentID) { glBindProgramARB(GL_VERTEX_PROGRAM_ARB, ID); currentID = ID; } } private: static GLuint currentID; }; Still implementing individual Ops classes for each of the texture types is still going to be tedious so we can take advantage of templates again to change the constant for us: template<GLenum target> class TexOps { public: static GLuint GenerateID() { GLuint ID; glGenTextures(1, &ID); return ID; } static void Bind(GLuint ID) { glBindTexture(target, ID); } static void Destroy(GLuint ID) { glDeleteTextures(1, &ID); } }; I've omitted the currently bound state memory in this case since a little more work is needed remembering the state for each texture unit if multitexture is used. I'll leave that as an exercise for the reader. You can also do a similar thing with the ARBVertexProgramOps class to support ARB_fragment_program: template<GLenum target> class ARBProgramOps { public: static GLuint GenerateID() { GLuint ID; glGenProgramsARB(1, &ID); return ID; } static void Destroy(GLuint ID) { glDeleteProgramsARB(1, &ID); } static void Bind(GLuint ID) { if(ID != currentID) { glBindProgramARB(target, ID); currentID = ID; } } private: static GLuint currentID; }; Taking a step in the API independence directionYou'll notice that in the above code, the usage to say create a 2D texture class would be: class Texture2D : public Object< TexOps<GL_TEXTURE_2D> > { public: /* 2D Texture specific functions */ }; Now this is ok if you know that the texture you are dealing with is a 2D texture, but it is useful to be able to have a base class GLTexture, that you can refer to all textures by. Now this causes us a problem: we would like the base texture class to have a method void Bind() const. However, this method is defined in Object<> so simply using the seemingly obvious solution of multiple inheritance won't work. The solution comes in the form of a diamond inheritance hierarchy. This formation is normally used as an argument against multiple inheritance, but in this case we can use a handy trick called delegation to a sister class[1] to enable us to achieve our aim. As an indirect side effect of this we also achieve the ability to have an API independent base class for our textures! This is what our hierarchy will look like. To avoid having two copies of the base texture object we need to make the inheritance of Texture in GLObject<> and GLTexture virtual. Now this poses us a new problem, we've just made GLObject<> dependent upon Texture! Again templates come to the rescue, making the interface now: class Texture { public: virtual ~Texture() { } virtual void Bind() const = 0; /* Other abstract Texture functions */ }; template<class ObjectOps, class ObjectBase> class Object: public virtual ObjectBase { public: Object() : ID(ObjectOps::GenerateID()) {} ~Object() { ObjectOps::Destroy(ID); } void Bind() const { ObjectOps::Bind(ID); } protected: GLuint ID; }; class GLTexture : public virtual Texture // Notice the virtual keyword - very important! { public: /*GL specific functions and implementations of pure virtual functions from Texture */ }; class Texture2D : public GLTexture, private GLObject<TexOps<GL_TEXTURE_2D>, Texture> { public: /* 2D texture specific functions */ }; Unfortunately this breaks any existing code using the old structure, however this can be easily resolved using an empty base class as a default template parameter. Now we can obtain polymorphic behaviour on pointers to the GLTexture type. Calling the Bind function delegates the call to the GLObject<> sister class. Note that we can use private inheritance for GLObject<> since we are only using it to provide an implementation of the interface, not as an interface itself. When providing access to textures outside your renderer, then you simply return a pointer to a Texture object. This gives you API independence in a way that won't break any client code as long as the Texture interface doesn't change. This article doesn't consider the overall state management of the extensions that these classes affect, but this is a trivial matter to implement in a state management class. You may also want to try to implement a manager class for each of the base classes you have (textures, vertex/fragment programs) that selects at runtime the type of class to instantiate based upon the data given, for example it is quite simple to determine whether a vertex program is a NV or ARB program from the first string in the program. I have provided some sample header files to implement the common extensions that use this technique, however I will not be providing any of the concrete classes that are derived from them. Most of the code is presented here in the article anyway. References[1] C++ FAQ lite: http://www.parashift.com/c++-faq-lite/multiple-inheritance.html#faq-25.10 [2] SGI OpenGL Extension Registry http://oss.sgi.com/projects/ogl-sample/registry/ Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|