The first problem I see here is that you have a "cube" class. You really shouldn't -- within the domain of a rendering system, what about a cube is different from a sphere, in terms of type or interface? Since rendering interfaces should not be responsible for non-rendering task (such as collision, et cetera) then the answer is really that there's nothing different.
On the whole, the only difference between a cube and a sphere is the data (the actual vertex elements), which can be abstracted. Instead of Cube
, you should have a Geometry
class. This class would contains two important pieces of data:
- A list (more accurately, an array or vector) of vertices.
- Information describing the format of an individual vertex.
The vertex format can be as simple or complex as you need -- you can simply decide to pre-define a set of vertex formats your game will use ({position, color}; {position, color, normal, texture coordinates}; et cetera) and represent the format as enumerated type. At the other end of the spectrum you can implement a free-form VertexFormat
object that allows specification of arbitrary vertex element semantics at arbitrary byte offsets within a vertex.
The actual vertex data should be represented as an unsigned char*
and you can cast to an appropriate vertex format structure if needed, based on the vertex format enumeration. This allows you to store any shape/mesh in any vertex format without having to create subclasses -- having to create those subclasses is actually a design and maintainability problem.
Once you have this in place, it should be pretty straightforward to then add a Material*
to the Geometry
class. If you give the Material
class a vertex format as well, you can even enforce correct bindings between materials and geometry: you can raise errors when you try to assign a material whose shader requires {position, color, normal} to a geometry object that only has {position, color} for example.
This also allows you to externalize data definitions and thus make your APIs more data-driven. You can now construct Geometry
objects with arbitrary collections of vertices. In terms of that API you can then implement another interface (it should be separate from Geometry
) that loads vertex lists from a data file format of your choice, or an interface that generates a list of vertices that form a cube, or sphere, or whatever other procedural primitive you'd like, with whatever vertex properties you desire.
EDIT: A clarification on some ways you can handle vertex formats:
The Geometry
object should accept and store only raw, untyped bytes of vertex data. You can work directly with these raw bytes, although I do understand the appeal of having a 'vertex structure' type that represents an individual vertex for ease of manipulation. In order to be useful that type must perforce be a POD (or you couldn't send arrays of them to the GPU anyhow), and so it is sane to be casting between the raw bytes and the vertex structure, provided the data in the bytes does actually match up to the structure's schema. You could thus provide an interface to the vertex data in the geometry object like:
template< typename VERTEX_STRUCTURE >
VERTEX_STRUCTURE* GetVertexData() {
// Assume 'vertices' is an unsigned char* instance member.
return reinterpret_cast<VERTEX_STRUCTURE*>(vertices);
}
You still need to know the appropriate format at compile time when you call this method, but that's usually not a problem because you are going to be directly manipulating the individual properties of the vertices when you call this (usually) so you'll know anyhow.
But if that's still a problem you can even manipulate the raw bytes directly according to the vertex format at runtime. Assuming you have a series of memory IO routines that allow you to write arbitrary primitive types (ints, floats, et cetera) into arbitrary byte buffers, and you have something that can query a vertex format object for information about whether or not that format contains normals or colors or whatever, you could write to an arbitrary format buffer like this:
unsigned char* buffer = /* get raw buffer from Geometry object */
VertexFormat format = /* get format description from Geometry object */
// WriteToBuffer() writes the provided value, advances the buffer pointer
// by an appropriate size and returns that updated pointer, allowing for us
// to do sequential writes. It's a relatively easy thing to implement.
buffer = WriteToBuffer(buffer, x);
buffer = WriteToBuffer(buffer, y);
buffer = WriteToBuffer(buffer, z);
if(format.HasNormals()) {
buffer = WriteToBuffer(buffer, normal.x);
buffer = WriteToBuffer(buffer, normal.y);
buffer = WriteToBuffer(buffer, normal.z);
}
if(format.HasColor()) {
buffer = WriteToBuffer(buffer, color);
}
// Et cetera.
This latter technique would be useful in code that generated a vertex buffer for a sphere procedurally, for example: you could pass that code information about what types of vertex properties you wanted your generated sphere to include, and generate them at runtime.