Overview
The proposal in summary:
- Add much-needed improvements to the material system
- Add a Sample demonstrating the new features
- Create a default "uber shader" used by the Sample Browser that includes some of the most commonly used shader techniques
- Port several materials and shaders from existing Components and Samples to the new system where appropriate
The Ogre material system works great for fixed function materials.
Unfortunately, using lots of different shader effects is incredibly tedious right now.
When coding shaders, you typically need to compile a new shader for each combination of features that are in use. Every new feature that you want to be able to toggle individually (such as normal mapping, hardware instancing, etc) essentially doubles the number of potential shaders. It's an O(2^n) problem.
A commonly used technique to mitigate this is to use an "uber shader" - that is, a single shader file that includes all features, guarded by "#if XYZ" directives. This is currently not supported well in Ogre: preprocessor defines need to be set up in advance in a GPU program definition. A material is unable to impact this definition in any way. If a different combination of defines is needed, you'll need another program definition for it - and we're back to the O(2^n) problem.
Another issue is that shader files (even with a C-style preprocessor) lack flexibility. Often, factors such as the number of texturing layers for an alpha splatting shader, number of lights in a multi light shader etc are not known in advance.
A "last resort" taken by many Ogre users (and even Ogre itself, in the current version of the Terrain component and the Deferred Shading sample) is to manually assemble the shader code from within C++ code. This practice is highly prone to error, frustrating to debug, makes it harder to grasp what the shader does, and in general hardcodes something that clearly should not be hardcoded. An indicator that we have a serious problem.
Design proposal
Note: The concepts below are completely optional. Existing "low level" materials will continue to function as before. Low level material access is still useful in areas where permutations are not needed (e.g. most compositors).
Properties
We add the concept of abstract material properties. A property can be any kind of string key-value pair. The only special property is the "generator" property - it specifies the type of material generator to use (see further below).
For materials with a generator, the techniques section would usually be left blank in the material script, as the techniques will be generated at runtime later.
Inheriting properties from other materials works great in a material script:
Examples
Code: Select all
material foo
{
generator main
diffuse_map foo.png
diffuse 1.0 1.0 1.0
}
material foo1 : foo
{
normal_map foo_n.png
}
material foo2 : foo1
{
diffuse 0.5 0.5 0.5
}
material ocean
{
generator water
wave_speed 20
choppiness 10
}
material lake : ocean
{
wave_speed 2
choppiness 5
}
The material generator's job is to fill in the techniques of a material when it's loaded. It does so by looking at the material's properties and then creating techniques, passes, texture units, and shaders (invoking the shader generator, see below) as appropriate. This can also be affected by external factors, such as graphics settings, current number of lights in the scene etc. These factors can also change during runtime - to respond to this, the materials can be reloaded so that the generator is invoked again.
Since there's no material generator that can be truly generic, it should be a pure virtual class in the Ogre core, left to be implemented by users. A sample implementation including some of the most common shader effects should be added to the sample browser, so the majority of users can just copy that and modify it for their purposes.
Specialised Components such as the Terrain component should ship with a ready-made MaterialGenerator subclass and its associated shader file(s).
To create a material generator, a class should be derived from the pure virtual Ogre::MaterialGenerator class. Instances of that class can then be registered with the MaterialManager.
Usually one instance per class should be sufficient. A use case for multiple instances of the same material generator is when you'd like to store extra per-material information in the material generator object. This would usually be done for materials created programatically, as it's rather tedious to store information in the property map (and would involve lots of unnecessary string conversions).
An example: storing layer information in material generator objects
Code: Select all
class TerrainMaterialGenerator : public MaterialGenerator
{
struct LayerInfo
{
std::string colourMap;
std::string normalMap;
uint8 flags;
}
std::vector<LayerInfo> layers;
virtual void generate (MaterialPtr material);
}
Code: Select all
MyMaterialGenerator::generate(MaterialPtr mat)
{
const map<string,string>& properties = mat->getProperties();
mat->removeAllTechniques();
Technique* tech = mat->createTechnique();
tech->getPass(0)->createTextureUnitState(properties["diffuse_map"]);
if (!properties["normal_map"].empty() && mNormalMappingEnabledInUserSettings)
{
tech->getPass(0)->createTextureUnitState(properties["normal_map"]);
}
tech->getPass(0)->setVertexProgram (mVertexShaderGenerator->getShader(properties));
tech->getPass(0)->setFragmentProgram (mFragmentShaderGenerator->getShader(properties));
}
The default shader generator uses plain old shader files (typically containing GLSL or HLSL code) combined with a couple of macros for added flexibility (see "Macro overview" section). Custom shader generators can also be plugged in, allowing for fancier setups, e.g. building shaders in Lua or another scripting language.
Any number of shader generators can be registered, and it's up to the material generator to decide which shader generator(s) should be used. Example: one shader generator for Deferred materials, one for regular Forward materials, one for Terrain, etc.
To reduce the amount of shaders compiled, shader generators track the permutations of compiled shaders and just return an already existing shader if the same permutation is requested again. A permutation basically comes down to the combination of values from any macros (see below) that are retrieved.
Preprocessor
The shader generator needs a very fast, C-like preprocessor to handle includes, flow control, line directives, expressions and macros (object-like and function-like). Speed is more important here than handling every possible edge use case.
Directives intended for the shader compiler such as #version, #extension need to be kept obviously.
Macro overview
To give a satisfying degree of flexibility, several macros are implemented in the default shader generator, serving as a kind of "extension" to the shader language and as interface to certain Ogre facilities from within the shader file.
OGRE_SHADER_LANG
Gets expanded to the shader language currently being used. Useful for handling GLSL and GLSLES in the same file since they're very similar.
Example:
Code: Select all
#if OGRE_SHADER_LANG == OGRE_LANG_GLSLES
// Add precision qualifiers which are only used in GLSL ES
precision highp int;
#endif
Expanded to "1" or "0" depending on the type of shader currently being compiled. Makes it possible to handle vertex and fragment shader in the same file, which is useful to get the inputs/outputs to match correctly.
Example:
Code: Select all
// declare Inputs/outputs here
#if OGRE_VERTEX_SHADER
// Vertex shader code here
#else
// Fragment shader code here
#endif
Properties are passed to the shader generator upon invocation.
Expanded to "0" if the property with this name does not exist or is 0, otherwise expanded to "1".
Example:
Code: Select all
#if @property(normal_map)
// Declare the normal map sampler only if we use a normal map
uniform sampler2D normalMap;
#endif
Code block gets repeated count times. Useful for multiple lights in a single pass, multi layer texture splatting, etc.
Current iteration can be retrieved via @(symbol).
The count parameter can be a literal or a @property macro.
#line directives for the shader compiler will be inserted automatically to keep the line numbers valid when debugging compile errors.
Example:
Code: Select all
#foreach(n, 2)
This is iteration number @{n}
#endforeach
Code: Select all
#line 2
This is iteration number 0
#line 2
This is iteration number 1
@param_named_auto(...)
Identical to param_named / param_named_auto in a program definition. There are several reasons this macro is very useful:
• Do not bind the parameter if in a dead branch (as the parameter would not even exist)
• Bind indexed parameters (e.g. per light parameters) in a foreach construct
@counter
Unfortunately, some shader languages (such as HLSL and CG) only provide indexed float4 TEXCOORD<n> interpolator registers (with 0 <= n < 8 ), rather than a variable like concept (such as 'varying' in GLSL).
This is rather inconvenient when dealing with permutations - you keep having to change the indices as you add and remove features.
A very simple but effective fix is to add a counter macro. Only invocations that survive the preprocessing step will add to the count.
Example
Code: Select all
out float4 normal : TEXCOORD@counter
#if NORMAL_MAP
out float4 tangent : TEXCOORD@counter
#endif
out float4 position : TEXCOORD@counter
Code: Select all
out float4 normal : TEXCOORD0
out float4 position : TEXCOORD1
1. @property
2. #foreach
3. Run the preprocessor - eliminates dead branches
5. @counter
4. @param_named[_auto]
Schedule
Core changes (~4 weeks)
- Implement the preprocessor and create associated unit tests
- Add MaterialGenerator class and integrate with MaterialManager and MaterialSerializer
- Implement the default shader generator and create unit tests for its macros
- Create a sample with a few entities and lights that will serve as a testbed
- Write an uber shader and an associated MaterialGenerator with some commonly used techniques such as normal mapping, HW skinning, HW instancing, [PSSM] shadows, specular maps, parallax, environment mapping, point / directional / spot lights, variable amount of lights, multiple lighting techniques (phong, BDRFs, etc)
- Add buttons/controls to toggle each shader feature individually, and to change the current shader language
- Demonstrate automatic generation of LOD techniques
- Demonstrate automatic generation of scheme techniques (e.g. a small RTT box in the corner with slightly altered shaders)
Port existing materials & shaders to the generator where appropriate (~5 weeks)
- Port the Terrain material. Current implementation is ~2.3k lines of code, so this will take a while. The refactored version will be much smaller and more efficient.
- Port the Deferred Shading Sample
- Refactor Sample Browser materials in general to use the uber shader and material generator where appropriate; delete no longer used shaders. This will also help with getting more consistent, better looking materials in many samples.
Documentation and cleanup (~1 weeks)
- Write a manual section about the added features
- Make sure doxygen comments are complete
- Make sure helpful exceptions are thrown in case of user error
- Double check for any code quality issues
- Run static & dynamic analysis
If there's any time left, work on some additional features (see below), otherwise move them post-GSOC.
Optional tasks
I have selected a few other tasks related to the material system I would like to work on. They are by no means essential so can be moved post-GSOC if there's no time left.
Terrain material improvements
After the terrain component is ported, working on it will be much easier and nicer. Time to put that to use! I have a few ideas for improving the terrain material, which I've already implemented in personal projects long ago, so will be rather straightforward to add:
• Triplanar texturing support
• Multiple lights support and point light support
• Multi-pass splatting, essentially removing the layer limit
Improve microcode caching
Since Ogre 1.8, microcode caching can be used to speed up shader load times.
The main issue with this (and probably the reason it has not been enabled in the sample browser yet) is that editing a shader file (or pulling in a new version of it) has no effect if a cache has already been saved.
It would be nice to be able to benefit from shader caching not just in a final product, but also during development. The way I deal with it in my own projects is to save the last write time of the shader source files along with the cache, and check if a file has been modified before trying to load its cache. It should be investigated if this solution can be added to the Ogre core in a clean way. If not, we should at least add it to the Sample Browser.
Note that to get change tracking to work properly with #include, a dependency list needs to be built for each shader.
Faster shader editing
The current process for editing shaders appears to be: make adjustments, launch program, wait for it to load, check the result - and repeat.
A much nicer way would be to have the application automatically react to shader changes and reload them. Therefore you would not have to restart the application every time to test something out.
I am not sure if the file monitoring part is Core-worthy. It would need to be optional in any case, of course. If not in the core, the sample browser would be a good place.
It also seems there is currently a bug affecting this, which would need to be fixed first: https://ogre3d.atlassian.net/browse/OGRE-325
Get rid of CG sample materials
The reason we used CG in the first place was to write shaders once and use them in both OpenGL and DX renderers.
Unfortunately, due to CG not being free software and being unavailable (or having limited support) on several platforms / hardware, GLSL alternatives for the sample materials have to be provided anyway.
A lot of work on this has been done already. Compiling Ogre without CG at this moment has most samples working. I would like to complete this by adding the missing GLSL shaders and then removing the CG materials completely, as it makes no sense to maintain them any longer.
The CG plugin itself can be kept for now, though maybe it should be marked as deprecated at some point - the new GL3 render system doesn't support CG anyway.
A promising alternative to CG could be mojoshader: "construction of a full HLSL compiler is in progress".
Integrate modular shaders
With GLSL, Ogre supports the use of modular shaders. Frequently used functions can be put into modules compiled separately and then attached/linked to individual programs.
This should be integrated with the default shader generator. Obviously making each included header into a module automatically is problematic, and impossible if the header itself needs permutations. A better way would be to add a macro that turns the current file into a module.
All this does is reduce compile time, so it's a rather low priority feature.
Why this is an important project
That was a pretty long post, so let us recap what we would gain from the project:
Ogre users are given a set of powerful and versatile shaders they can start with.
Shader writers are given all the tools they need to write reusable shaders quickly and efficiently.
Artists are given a more abstract material system interface and do not have to worry about shader implementation details.
Ogre projects adopting the new system (including our own Sample Browser) benefit from more efficient, easier to maintain code.
Why I am the person for this project
I have 3 years of experience in using Ogre. I've only contributed small patches so far - but I would like to get involved more, so this GSOC is a great chance.
I already worked with the Ogre::Terrain component a lot, but I also wrote my own terrain system for large worlds from scratch, which can be seen here: http://www.ogre3d.org/forums/viewtopic.php?f=11&t=79026
I have written the shader pipelines for two projects I use Ogre in. As I kept adding more features, writing all permutations by hand became extremely tedious. I went for generating the shaders entirely in C++ code next, but quickly realized that every new feature or update I wanted to do would be rather painful with such a system.
To improve this, I needed some sort of helper library to deal with the permutation problem - and the shiny material system was born. In hindsight, I am not 100% happy with it. If I had to rewrite it from scratch, I would do a few things different. But it was a valuable experience none the less because I have learned a lot about material/shader generation systems which will be useful for this GSOC.