_______________________


Rob Galanakis
Technical Artist
516.680.1603
robg@robg3d.com

Assembling an OGRE Material Library

One of the most important pipeline decisions I had to make while working on Blood and Iron was creating a material library for my OGRE materials and shaders. The project had no tools for this, and there were no published solutions or examples of how these have been handled by other Ogre projects, so I had to take into account that the library and materials would be done completely by hand.

What I hope to do in this article is explain my material library and how it is designed, and the rationale behind those design choices, as well as what I would do differently or other suggestions. The material library is available in the tech demos available from http://www.bloodandirongame.com . I will also assume for the article's use as a tutorial, the reader is familiar with Ogre's material system. The article is still useful as an explanation but I will not explain the basics of the material system in most instances.

Planning

First I had to figure out my requirements: easily readable because we lacked tools, easily editable because all the materials, shaders, and textures were in development, use custom shaders for every material, and easily extendable because the assets and content were constantly being changed, added, and removed.

Second, I had to take stock of what I had working for and against me. Against me was, as stated, the lack of tools, as well as lack of experience and expertise in pipeline. However, Ogre had a very intuitive and full material instancing system, is excellently documented, and I had a good knowledge of HLSL and Ogre's material system. On the other hand, the open-endedness of Ogre solutions meant that while its Fixed-Function Pipeline is extremely easy to use, its Programmable Pipeline (ie Shaders) has much lessed "canned" control and is entirely in the hands of the user instead of just variables to set inside the material system.

I won't go into the details of OGRE's material system, as it is so thoroughly covered in the manual. I also won't go into how shaders work in Ogre, as I've covered that in another tutorial you can get from my website.

Shader Files

I break each shader into its own self-contained file: input and output structures, global variables and constants, and the shader code for the shader (a shader here is a vertex OR pixel shader, the two are never in the same shader file). There are a few important things here:

  • Modularity: Each shader is in its own file, with a proper arbitrary, but consistent, extension (I named it .hlsl to indicate the shading language, so I would also use .glsl and .cg)
  • Naming: The shaders must have a consistent and informative name. Every vertex shader begins with a "vs" to differentiate it from "ps" for pixel shaders. After that, I will indicate the "space" the shader is in: WS for world space, OS for object space, or TS for tangent space. After that, I will specify the number of lights passed in the shader (1L, 2L, or 3L), and finally, the bones if applicable (blank, 2B, 3B, 4B). So, an example vertex shader would be vsWS3L. I didn't use second UV sets but if you do, that info should be added in.
  • Naming Pixel Shaders. Pixel shaders all begin with "ps". The type of material follows that, and is arbitrary; for example, I use "Normal, Offset, Fabric, Metal, etc." If there is Opacity, I will add that in after the material type. Finally, is the type of specular map: 8 for greyscale, 24 for RGB. So examples would be psOffset8, psNormalOpac24, etc.
  • Alpha Channel Arragement. This should follow some logic to make life easy for artists. Create some rules: a height map (for offset) will always be in the normal map alpha channel, an opacity map will always be in the diffuse alpha, a specular map (8-bit) will occupy the empty slot but will default to the diffuse map.
  • Entry Points. I use "v" as the entry point for all vertex shaders, and "f" as the entry point for all pixel shaders. This must be consistent, every vertex shader should have the same string as an entry point, as should every pixel shader.
  • Structures. Your Output Structures need to be consistent and logical. A single vertex shader will be used by a dozen pixel shaders, so you need to make sure they use the same structures. Basically, your TEXCOORDs should match. I have my UVs in TEXCOORD0, eye vector in TEXCOORD1, and then follow some logic: for all my world space vertex shaders, I have the worldspace tangent, binormal, and normal in TEXCOORD 2, 3, and 4, respectively. The remainder I will use my lights in (so up to the three next TEXCOORDs).
  • Sampler names. This will make your life easier later. Make your texture sampler names correspond with your alpha channel: diffuseSpecSampler, normalHeightSampler, diffuseOpacitySampler, etc.

Program Files

The program files are where the things really come together. There's nothing really special here, I just name my program whatever the name of the shader file is (so vsWS2L or psNormal24).

Material Files

Well this is where the magic happens, really. The backbone here is Ogre's material instancing system. I will create a material file for each type of object, even better would be to divide them up according to pixel shader used, though you can divide them up however you want (or even put them all in the same material file if you have a way to collapse and view them nicely).

At the start of the material file, I will define a generic material: fabric, armour, skin, etc. I will just use generic variables and texture names. Important here is your texture_alias name: DO NOT name it the same as your sampler. You want to have as few texture aliases as possible, so if you change the type of parent material for a child material (such as "sigurdHead" child material uses "skin" parent material) you do not have to update the texture alias if it doesn't correspond. You'll come across this especially because of the alpha channels. So, just stick to, DiffuseMap, NormalMap, SpecMap, etc. Sometimes here you can define textures you won't need to change, such as cubemaps.

You can use the generic materials to make derivative parent materials as well, especially useful for the same material which would use a hardware skinning vertex shader (4B etc) and everything else would be the same (such as a basic normal mapped material).

After that, just create your child materials as you normally would, using texture aliases.

So, for example, you would have a material, normal8, for a regular normal mapped material with 8-bit specular (stored in a standard place, such as the normal map's alpha). I'd also have normal84B, which is the same version but with 4-bone hardware skinning. After that, I'd have drystanHauberkA : normal84B, which instances the normal84B material, and with the proper texture aliases. It makes a very clean, modular, extensible system, that rarely needs to be rewritten or changed at a high level as long as you are clean and consistent.

Texture Names

Again, be descriptive, and consistent. I name everything with the "programmer convention" of no spaces (NEVER use spaces), and using capital letters to differentiate words, not underscores ("iUseCapitalLetters", like that). After the texture name, I put an underscore "_", and then the type of map: "d" for diffuse, "n" for normal, "s" for specular. If there are two letters, the second is what's in the alpha channel: "o" for opacity, "s" for specular, "h" for height. Sometimes if I'm using a non-traditional shader (such as my skin shader), where the RGB channels are unrelated, I'll just use a description for each channel: "skin_aoPoreSSS" stands for ambient occlusion, pore mask, and sub-surface-scattering mask.

Exporting and Material Names

When I designed on our material pipeline, I decided to not export the materials directly (exporting done through oFusion in 3dsmax, but I assume the following would be true for any software and exporter). I would build "master" material files with the proper names, and just drop in new names as needed, but other than for names, I'd throw away the exported material file.

So in 3dsmax, I'd make a multi-sub material, called, for example, "cathedral". Everything part of the cathedral object would be a sub-material. The result is exported names like cathedral/column, cathedral/floor, etc., which makes a good, descriptive naming system. I'd then set up the proper names in the proper material file, instancing the proper material (cathedral/column : normal8, cathedral/floor : offset8, etc.), and assigning the proper textures. I'd export, throw away the material file, drop the .mesh files in, and they should have the proper materials.

If you don't use multi-sub materials, you will get all sorts of wacky material names. Its also a real trouble (and not worthwhile) to set up your proper materials in 3dsmax, as the resulting material file may be very dirty and hard to read. Using a hand-made material file makes sure everything is as efficient and clean as possible, and since you are often working directly in the files and not exclusively through tools, its much safer this way and a much better solution. I didn't set up a single oFusion material, instead I just use multi-sub materials and Standard materials with the Diffuse map applied so I can get a rough visual approximation, and for reference.

Conclusions

Overall I was very happy with how the material system worked out, definately one of the higher points of the Blood and Iron project. I hope the lessons I've learned (naming conventions, modularity, instancing, etc.) are lessons you take away from reading this article if you are developing any hobby project, especially with Ogre.

If you have any questions, feel free to e-mail me, and I will answer them promptly and maybe post the Question and Answer here to clarify things. Thanks for reading!