Reputation: 3376
I'm trying to implement a model(gfx) class but I can't seem to find a proper design for it.
What I have is this: [pseudocode]
class Material {
public:
virtual void SetPerFrameInfo(void* pData)=0;
//[...]
}
class SpecificMaterial : public Material {
public:
void SetPerFrameInfo(void* pData);
//[...]
}
void SpecificMaterial::SetPerFrameInfo(void* pData) {
ThisMaterialPerFrameInfoStruct* pInfo = (ThisMaterialPerFrameInfoStruct*)pData;
//[...]
}
class Model {
public:
Model(Material* pMaterial){m_pMatirial = pMaterial;}
void Draw();
private:
Material* m_pMaterial
//[...]
}
void Model::Draw() {
PerFrameInformation info;
m_pMaterial->SetPerFrameInfo(&info);
}
How to use it:
Material* pMaterial = new SpecificMaterial();
Model someModel(pMaterial);
As you might see, I have two main problems:
1) The type of struct passed to SetPerFrameInfo() depends on the actual material, so how should I fill it in?
2) The Model class might need some other datamembers depending on the actual material, since for some materials I might decide to implement a timer, and for some I might not. It would be a memory waste to have datamembers in the Model class for all materials.
Please help, I can't seem to find any proper design pattern.
Upvotes: 3
Views: 314
Reputation: 2081
1.) I think the problem you are experience is due to trying to put too much in the base class. Since the Model needs to know what particular material type it is working with when it is calling the SetPerFrameInfo function, why not just give each Material-derived class its own Set..() function that takes the proper type instead of void*? Because the Material base class has no way to interact with its per-frame info (it is void*), then why generalize it into the base class?
2.) Just put the material-specific datamembers into the derived material classes. I assume each Material instance corresponds to a unique Model, so the material-specific information for the Model should be in its Material-derived class.
Okay, I misunderstood the relationship between the two classes. Basically you want, in the ideal scenario, a set of Materials classes that don't know the details of a Model, and a set of Models that can work with any Material, without knowing the details of the Material. So you may want to apply a Rough material instance to six different models.
I don't think there's any question that, if each Material instance is shared by several Models and each Model instance knows nothing about the different Materials, a third per-Model object is needed to store the timers or whatever instance data is needed for drawing. So each Model receives its own AppliedMaterial object which points to a shared Material instance as well as the data members needed to apply the material to that model. There would have to be a different AppliedMaterial subclass for each Material type, storing the data values pertinent to each material. Example of using a third context object:
struct GlossMaterialContext
{
Timer t;
int someContextValue;
};
class GlossMaterial : Material
{
void * NewApplicator() { return new GlossMaterialContext; }
void FreeApplicator(void *applicator) { delete (GlossMaterialContext*)applicator; }
void ApplyMaterial(void *applicator)
{
// Set up shader...
// Apply texture...
}
};
class 3DModel
{
Material *material;
void *materialContext;
void SetMaterial(Material *m)
{
material = m;
materialContext = m->NewApplicator();
}
void Draw()
{
material->ApplyMaterial(materialContext);
}
};
It is not an incredibly clean technique. I always feel like dealing with void* pointers is a kludge. You could also have the 3rd context object be based on a class, like:
class AppliedGlossMaterial : AppliedMaterial
and:
class GlossMaterial : Material
{
AppliedMaterial * NewApplicator() { return new AppliedGlossMaterial; }
...
If the datamembers depend on both the Model type and the Material type, then your models and materials are way too tightly coupled to make them into separate classes that are supposed to know nothing about each other. You would have to create subclasses of your Model: Glossy3DModel, Rough3DModel, etc.
Upvotes: 1
Reputation: 601
Based on what you've said, it sounds like your Material class abstracts your shaders, and PerFrameData is differing between the materials because each shader can take a different set of inputs. The problem is that you have a single data storage class, Model, storing what could be a variety of input permutations. Ie, does your model contain textures, vertex color, both? What vertex format is used? Are triangles indexed as strips or lists? Does the model expose a normal map? Lightmap? Spec? And so on.
You really need to define a standard set of components that your rendering system understands, and an interface that knows about all of those components and can therefore be the bridge between Model and Material. Material needs to be able to query Model's data through this interface and validate that it exposes the right things. Model can construct the class and pass it to the Material to render with.
Another thing to consider is performance. If your renderer loops over all Model objects calling Render
, that can be very non-performant, since each Model can have a variety of Materials and therefore you'll likely be incurring huge penalties for switching shader states many times per frame. If you instead group your render passes by Material, you can save a lot of time.
Upvotes: 1
Reputation: 4275
Let me preface this by saying I haven't implemented such a system before, but here are my thoughts.
The problem stems from the fact that the Model::Draw
changes the material by itself, when it has no knowledge of the specific class the material has. This screams for this "material modification" to be done outside.
void Model::Draw() {
// do draw by using the current material
}
void Model::UpdateMaterial(Material* mat) {
m_pMaterial = mat
}
Since you modify the material somewhere else per frame, you should know its type at that point. This changed material is queued up for the particular object and frame
You can then have a preparation stage before a frame is rendered where you do the necessary bookkeeping and swap out the materials for the new ones.
I'd also recommend using something like boost::shared_ptr
to manage your Material
objects, as keeping track of which objects are sharing a material and whether it needs to be deleted is a huge pain in the ass.
Upvotes: 1
Reputation: 1011
I think most rendering engines collect the actual rendering into a central algorithm/family of algorithms, thus draw()
would be implemented in a central renderer class which would hold a collection of all the objects to be rendered in the upcoming frame. It would then call methods to get the appropriate low-level data (vertices and colors, shaders, etc.) which would be exposed through the Model and Material interfaces. This is because a Material might know a lot about being a material, but it probably shouldn't know anything about DirectX or OpenGl.
However, with what you've posted, you could implement something like:
class PerFrameData
{
//Methods to get data in a standard way
}
class Material
{
setPerFrameData(PerFrameData data)
{
if (data.hasVertices())
// or whatever
}
}
class Model
{
PerFrameData data;
Material material;
draw()
{
material->setPerFrameData(data);
}
}
Upvotes: 0