🔒 Locked
Implementing a simple material system
Advertisement
Original Post
Hi I'm currently working on a small 3d engine in opengl (my first 3d engine). So far I am using interleaved VBO's to render textured objects. I'd like to implement a small material system in order to spice up my renderings a bit. I've never done it before so I would like some criticism. I am planning to implement it like this: 1. Have some form of manager (possible a singleton class) with a dictionary (hash/tree table) storing materials by name, filling it up with new materials as I read in the models 2. Sort all model faces by material type (if they aren't already) building a list containing a set of ranges (of faces to be rendered with one material). 3. When I render lock the VBO's and loop trough the list of ranges, setting a material and then calling glDrawRangeElements() to draw that "range". Is that how you guys do it? have I overlooked something? is there a smarter way?. oh and should I store textures as "material" too? or store it separately in another manager? Hope my question isn't too general.
One piece of advice that really helped me had to do with shaders and a lighting model.
Are you planning on using OpenGLs lighting model for your engine?
keep in mind making your own lighting model, material, and shader system can be intense, but you can learn a TON.
Are you planning on using OpenGLs lighting model for your engine?
keep in mind making your own lighting model, material, and shader system can be intense, but you can learn a TON.
I'm currently doing it like this:
material DB:
individual material:
Currently i've only implemented diffuse maps, but I plan to use a different renderer anyway. (I'm currently using a custom renderer but its functionality is limited). Its simple but effective.
SOIL is an OpenGL image loading library, which I use for all my OpenGL applications. Google SOIL Opengl to find it. It has my recommendation.
material DB:
class material_db;#ifndef class_material_db#define class_material_db 1#include "material.h"#include "sdl_gl_include.h"#include <string>#include <map>#include <iostream>class material_db{private: std::map<std::string,material*>materials; material default_material; //bound when the texture passed in cannot be found. std::string default_folder;public: material_db(); ~material_db(){}; bool load_table(std::string filename); //load all files listed in file filename bool load_material(std::string name); //manually load materal with that name. returns false on error. //material is loaded from {this.default_folder}/. bool bind_material(std::string name); //material of that name is bound. Returns false on error.};#endifindividual material:
class material;#ifndef class_material_h#define class_material_h 1#include "soil.h"#include "sdl_gl_include.h"#include<string>class material{private:public: GLuint texture; GLuint bump; GLuint normal; std::string texture_location; float rgb[3]; float diffuse[3]; float ambient[3]; float emissive[3]; float specular[3]; float shinyness; float transparency; material(); void create_checker_texture(); //make a default texture bool load_color_map(std::string F); //takes actual path and filename bool load_color_map(char * F); //takes actual path and filename void apply(); ~material(); };#endifCurrently i've only implemented diffuse maps, but I plan to use a different renderer anyway. (I'm currently using a custom renderer but its functionality is limited). Its simple but effective.
SOIL is an OpenGL image loading library, which I use for all my OpenGL applications. Google SOIL Opengl to find it. It has my recommendation.
Quote:
Original post by bisonmontana
...
1. Have some form of manager (possible a singleton class) with a dictionary (hash/tree table)
storing materials by name, filling it up with new materials as I read in the models
...
No, no and no. Get singletons out of your vocab right away. They are bad, bad and once more for emphasis, bad.
Search these forums and you will find out why from some very smart people.
@speciesUnknown
bool load_table(std::string filename);
Should likely be,
bool load_table(const std::string &filename);
Const correctness just makes code more readable and gives the compiler a better chance of catching your errors and knowing what you meant to do. Also pass by reference whenever possible requires less memory and CPU time (no copying of the class to another class).
bool load_table(std::string filename);
Should likely be,
bool load_table(const std::string &filename);
Const correctness just makes code more readable and gives the compiler a better chance of catching your errors and knowing what you meant to do. Also pass by reference whenever possible requires less memory and CPU time (no copying of the class to another class).
Quote:
Original post by bisonmontana
1. Have some form of manager (possible a singleton class)
Correction - not a singleton class [smile]
Quote:
with a dictionary (hash/tree table)
storing materials by name, filling it up with new materials as I read in the models
How are you planning to be populating this table with materials when you're reading in models? It makes more sense to populate the table of materials when you read in materials.
Quote:
2. Sort all model faces by material type (if they aren't already) building a list containing a set of ranges (of faces to be rendered with one material).
Don't sort faces individually at runtime, do it as a pre-process.
A single model may have several materials, therefore its geometry should be pre-sorted and grouped by material into separate geometry-chunks. At runtime those chunks are simply handed over to the render-core and no per-face sorting need be done.
It's to be expected that several models may happen to share a material (or a few materials), in which case each model will have its own geometry-chunk for that material. At rendertime the render-core will receive all the geometry-chunks and it can sort on a per-chunk (not per-face) basis by material so that chunks with the same material are all rendered together.
Quote:
3. When I render lock the VBO's and loop trough the list of ranges, setting a material and then calling glDrawRangeElements() to draw that "range".
Yep.
Quote:
oh and should I store textures as "material" too? or store it separately in another manager?
Actually, I find it helps to make a distinction between a shader and a material:
A shader is used to setup the pipeline state (like blending state, enabling lights, binding textures, etc)
A material is used to actually associate a shader with the data it needs (so which textures to use, colours, etc)
Quote:
Original post by Mike2343
@speciesUnknown
bool load_table(std::string filename);
Should likely be,
bool load_table(const std::string &filename);
Const correctness just makes code more readable and gives the compiler a better chance of catching your errors and knowing what you meant to do. Also pass by reference whenever possible requires less memory and CPU time (no copying of the class to another class).
Thanks for the heads up, I'll make these modifications some time. I'm one of those terrible self-taught self-styled programmers...
Quote:I suggest to avoid using the name unless for direct, expensive content lookups.
Original post by bisonmontana
1. Have some form of manager (NOT a singleton class) with a dictionary (hash/tree table)
storing materials by name, filling it up with new materials as I read in the models
For example, when content is loaded, an header builds a name->resource mapping. The resource manager actually loads them up and builds a assetResourceList->globalResource association.
Later references in the asset are given by IDs rather than names to speed up lookups - and I was surprised in seeing the benefit (it's nothing to scream for but it's a valuable improvement anyway).
Further headers are "merged" in the global one before loading.
Also, I would suggest to leave hashes out. While the THETA(1) thing may sound a big deal at first, it really doesn't buy you much over O(n) unless you have thousands of materials in fly at the same time. HL2 has thousands of materials in the whole game so this is unlikely to happen.
Quote:As dmatter suggests, doing this in preprocessing is going to save alot of time. I'm not saying you should do this at runtime. You should do this before, such as in a "map compile" stage. Assuming the loaded asset to be already optimal will make everything easier and nicer. There's no real reason to NOT build "geometry chunks" at asset generation.
Original post by bisonmontana
2. Sort all model faces by material type (if they aren't already) building a list containing a set of ranges (of faces to be rendered with one material).
Quote:Personally I haven't found much improvement in setting the range nicely. I suppose it's an evolutionary artifact. While we're at it, I would like to know if somebody got an improvement using this.
Original post by bisonmontana
3. When I render lock the VBO's and loop trough the list of ranges, setting a material and then calling glDrawRangeElements() to draw that "range".
Quote:Textures are NOT materials and shouldn't be stored as such. Doing this would complicate lookups pretty much (I can say this by experience) and although providing the "default material" isn't difficult, it doesn't look as elegant as specifying it manually at DCC stage and the benefit is debatable at best.
Original post by bisonmontana
oh and should I store textures as "material" too? or store it separately in another manager?
Note that a "material" system nowadays is considerably more complex than you might expect. For example, depending on your tech expected life-time it may make more sense to deprecate the GL material system completely and go shader-only. This generally requires some reworking in the data structures as well since they may need to incapsulate whole "blobs" per vertex in an opaque way.
This in turn implies that you must make sure the blobs does fit the actual material which will be loaded, a simple signature is generally enough, but more involved approaches are possible. Do NOT think at CRC'ing or SHA'ing the material code as a whole, this would make impossible to replace materials without rebuilding everything. However, in some cases this could be useful or even required. I personally like more the idea of allowing material replacing as long as the signature is compatible... whatever it exactly means. ;)
Quote:Copy. Sometimes they do some really funny stuff.
Original post by Mike2343
Best way to be, you really wanted to learn. Unlike a few people I know who went to school and I still help out lol.
"I've proofed this executes in THETA(n) so it's better than yours"
"Good, but I see you're leaking half of your pointers"
Believe it or not, it's a true story! ;) (OMG, our schools here are so bad at delivering industry-oriented developers! I'm sure this was a rather extreme case, I apologize to everyone but I just want to say the hard way rocks better!)
Quote:
Copy. Sometimes they do some really funny stuff.
"I've proofed this executes in THETA(n) so it's better than yours"
"Good, but I see you're leaking half of your pointers"
Believe it or not, it's a true story! ;) (OMG, our schools here are so bad at delivering industry-oriented developers! I'm sure this was a rather extreme case, I apologize to everyone but I just want to say the hard way rocks better!)
QFT except that on my C course they are teaching about safe memory management by default. (they just covered malloc and free and the tutors seem to really know what they are doing. Perhaps we're lucky?)
First of all, thanks for all the lengthy replies. I'll be reading them a few times more to be sure I don't miss anything :).
to dmatter and krohm:
Don't worry, I am doing all the face sorting and material lookups when I load the geometry, not while rendering. Oh and I am building the material database while loading materials and not models of course :).
What I ended up doing is basically this:
I load all materials into a material database, as of right now my material class also holds a texture id (I don't think I understand the difference between shaders and materials) and it seems logical since mesh file formats and 3d editors associate materials and textures.
For each mesh, not touching the order of the vertices, I instead sort the faces according to material.
This preprocess leaves me with 2 VBO's per mesh, one containing vertices, normals and texcoords (interleaved), the other containing faces sorted by material.
My preprocessor also builds a structure I call a FaceMaterialList for each material used in the mesh. This structure holds a direct reference (no hash/tree lookup while rendering) to a material as well as VBO/array offset and length.
When I render the mesh I simply loop trough all FaceMaterialLists, setting material and drawing the range of faces with glDrawRangeElements.
Krohm:
Replacing the lightning system with something homemade is a bit to advanced for me as of yet. I am still using the fixed pipeline for everything :).
Why the hate for the singleton pattern?
Oh perhaps I should mention that I am doing this project in java with lwjgl. But I am a lot more familiar with C++. Reason I chose java is because it will allow me to do accelerated 3d on 3 platforms, just by clicking on a webstart link (of course I am aware of the performance penalties but I am not making the next Crysis).
oh and a little screenshot, no textures just materials and some test models I borrowed from Scifimeshes.com (great site): http://indianernavn.dk/s.png
to dmatter and krohm:
Don't worry, I am doing all the face sorting and material lookups when I load the geometry, not while rendering. Oh and I am building the material database while loading materials and not models of course :).
What I ended up doing is basically this:
I load all materials into a material database, as of right now my material class also holds a texture id (I don't think I understand the difference between shaders and materials) and it seems logical since mesh file formats and 3d editors associate materials and textures.
For each mesh, not touching the order of the vertices, I instead sort the faces according to material.
This preprocess leaves me with 2 VBO's per mesh, one containing vertices, normals and texcoords (interleaved), the other containing faces sorted by material.
My preprocessor also builds a structure I call a FaceMaterialList for each material used in the mesh. This structure holds a direct reference (no hash/tree lookup while rendering) to a material as well as VBO/array offset and length.
When I render the mesh I simply loop trough all FaceMaterialLists, setting material and drawing the range of faces with glDrawRangeElements.
Krohm:
Replacing the lightning system with something homemade is a bit to advanced for me as of yet. I am still using the fixed pipeline for everything :).
Why the hate for the singleton pattern?
Oh perhaps I should mention that I am doing this project in java with lwjgl. But I am a lot more familiar with C++. Reason I chose java is because it will allow me to do accelerated 3d on 3 platforms, just by clicking on a webstart link (of course I am aware of the performance penalties but I am not making the next Crysis).
oh and a little screenshot, no textures just materials and some test models I borrowed from Scifimeshes.com (great site): http://indianernavn.dk/s.png
Topic Locked
This topic has been locked by a moderator. New replies are not allowed.
Sign in to reply to this topic.
Advertisement