Developer Documentation
Tutorial: Implementation of The Toon Renderer Plugin

A renderer plugin class has to implement at least the OpenFlipper RenderInterface class. If the plugin wants to make use of the ACG shader pipeline which allows the renderer to have complete control over every draw call, the renderer also has to implement the ACG::IRenderer interface. The IRenderer interface already provides basic convenience functions, such as building and sorting the list of draw-calls to help getting started. In this tutorial we will focus on the implementation process of the toon renderer plugin, which makes use of the ACG::IRenderer interface and its convenience functions.

Cel shading with ShaderModifiers

The toon renderer requires a set of modified shaders, that replace the default lighting with cel shading. A ShaderModifier is used to replace the lighting code provided by the ACG::ShaderGenerator with custom shader code. The new lighting functions are defined in the shader include file "celshading.glsl", which is loaded and inserted at run-time.

The file contains cel shading functions for directional, spot and point lights called: LitDirLight_Cel(), LitPointLight_Cel(), LitSpotLight_Cel. We include this file in the generated vertex and fragment shader with the modifyVertexIO and modifyFragmentIO functions of the ShaderModifier. Additionally a new uniform is added to the uniform list of each shader, which is related to the cel shading technique.

void modifyVertexIO( ACG::ShaderGenerator* _shader ) {
// include cel lighting functions defined in CELSHADING_INCLUDE_FILE
QString includeCelShading = ACG::ShaderProgGenerator::getShaderDir() + QDir::separator() + QString(CELSHADING_INCLUDE_FILE);
_shader->addIncludeFile(includeCelShading);
// add shader constant that defines the number of different intensity levels used in lighting
_shader->addUniform("float g_celPaletteSize", "//number of palettes/intensity levels for cel shading");
}
void modifyFragmentIO( ACG::ShaderGenerator* _shader ) {
// include cel lighting functions defined in CELSHADING_INCLUDE_FILE
QString includeCelShading = ACG::ShaderProgGenerator::getShaderDir() + QDir::separator() + QString(CELSHADING_INCLUDE_FILE);
_shader->addIncludeFile(includeCelShading);
// Note: We include the cel lighting functions in both shader stages
// because the ShaderGenerator may call modifyLightingCode() for either a vertex or fragment shader.
// It is not yet known in which stage the lighting is performed.
// Additionally write the depth of each fragment to a secondary render-target.
// This depth texture is used in a post-processing outlining step.
_shader->addOutput("float outDepth");
_shader->addUniform("float g_celPaletteSize", "//number of palettes/intensity levels for cel shading");
}

Next, we tell the ShaderGenerator that our modifier replaces the default lighting functions by implementing the corresponding function.

// modifier replaces default lighting with cel lighting
bool replaceDefaultLightingCode() {return true;}

The ShaderGenerator then proceeds to call the modifyLightingCode() implementation of the modifier, which is supposed to generate the lighting shader code for each active scene light. To do this we check the type of the light, call the appropiate cel shading function and accumulate the lighting colors:

void modifyLightingCode(QStringList* _code, int _lightId, ACG::ShaderGenLightType _lightType) {
// use cel shading functions instead of default lighting:
QString buf;
switch (_lightType) {
case ACG::SG_LIGHT_DIRECTIONAL:
buf.sprintf("sg_cColor.xyz += LitDirLight_Cel(sg_vPosVS.xyz, sg_vNormalVS, g_vLightDir_%d, g_cLightAmbient_%d, g_cLightDiffuse_%d, g_cLightSpecular_%d, g_celPaletteSize);", _lightId, _lightId, _lightId, _lightId);
break;
case ACG::SG_LIGHT_POINT:
buf.sprintf("sg_cColor.xyz += LitPointLight_Cel(sg_vPosVS.xyz, sg_vNormalVS, g_vLightPos_%d, g_cLightAmbient_%d, g_cLightDiffuse_%d, g_cLightSpecular_%d, g_vLightAtten_%d, g_celPaletteSize);", _lightId, _lightId, _lightId, _lightId, _lightId);
break;
case ACG::SG_LIGHT_SPOT:
buf.sprintf("sg_cColor.xyz += LitSpotLight_Cel(sg_vPosVS.xyz, sg_vNormalVS, g_vLightPos_%d, g_vLightDir_%d, g_cLightAmbient_%d, g_cLightDiffuse_%d, g_cLightSpecular_%d, g_vLightAtten_%d, g_vLightAngleExp_%d, g_celPaletteSize);", _lightId, _lightId, _lightId, _lightId, _lightId, _lightId, _lightId);
break;
default: break;
}
_code->push_back(buf);
}

For a list of uniforms and variables generated by the ShaderGenerator refer to the "Dynamic shader assembly overview" documentation page.

Scene depth texture with ShaderModifiers

The toon renderer not only performs cel shading, but also implements a post-processing step that outlines the silhouette of objects. A texture containing scene depth information is needed in this step, which can be initialized with a ShaderModifier. In fact, a single modifier is sufficient which can do both: cel shading and depth output.

The depth output texture is declared in modifyFragmentIO():

void modifyFragmentIO(ACG::ShaderGenerator* _shader){
...
_shader->addOutput("float outDepth"); // write to a second texture target which contains only one channel for depth values.
...

We still have to generate a code snippet that writes to the new fragment output channel. This shall be done at the end of the fragment shader, thus we implement the modifyFragmentEndCode() function:

void modifyFragmentEndCode(QStringList* _code) {
_code->push_back("outDepth = gl_FragCoord.z;"); // write depth to secondary render texture
}

Registering the shader modifier

A modifier has to be registered to the ShaderGenerator before it can be used. Once the modifier is registered, is has received a unique modifierID which is used to apply a modifier at run-time to a shader.

The following steps are necessary to register the modifer:

// class definition of the modifier
public:
....
static CelShadingModifier instance;
}
..
// register modifier once to the generator, for example in the constructor of the renderer plugin
ToonRenderer::ToonRenderer() {
ACG::ShaderProgGenerator::registerModifier(&CelShadingModifier::instance);
}

The modifier can now be used and applied to any shader generated by ShaderGenerator. A modified shader is generated by setting the usage parameter in either the ShaderProgGenerator or the ShaderCache:

// modify a shader with cel-shading and depth-output:
GLSL::Program* prog = ACG::ShaderCache::getInstance()->getProgram(&shaderDesc, CelShadingModifier::instance);

Viewport management

The toon renderer supports multiple viewports by allocating a separate offscreen FBO for each viewer. The currently active viewport can be queried with the ViewerProperties descriptor, which is passed to the render() function. It is recommended that any render plugin treats viewport specific ressources such as FBOs in the same manner, if offscreen rendering is performed.

The main render() implementation of the toon plugin

Gathering RenderObjects and executing draw calls is done with the help of convenience functions provided by the IRenderer interface:

// collect renderobjects + prepare OpenGL state
prepareRenderingPipeline(_glState, _properties.drawMode(), PluginFunctions::getSceneGraphRootNode());

Next, we render the scene into an FBO with the cel-shading modifier. The FBO has two color attachments: attachment0 contains an RGBA texture storing scene color, attachment1 contains an R32f texture storing scene depths. This information has to be passed to the shader program such that the fragment outputs are mapped to the correct attachments: If the output targets do not match the FBO setup, the shader has to be relinked.

// render every object
for (int i = 0; i < getNumRenderObjects(); ++i) {
// Take original shader and apply the celshading modifier
GLSL::Program* prog = ACG::ShaderCache::getInstance()->getProgram(&sortedObjects_[i]->shaderDesc, CelShadingModifier::instance);
// eventually the shader has to be relinked after modifiction.
// The fragment shader outputs have to match the current fbo setup:
// attachment0 -> RGBA scene colors
// attachment1 -> R32F scene depth
// scene color is stored in attachment 0: outFragment
// scene depth is stored in attachment 1: outDepth
int bRelink = 0;
if (prog->getFragDataLocation("outFragment") != 0) {
prog->bindFragDataLocation(0, "outFragment");
bRelink++;
}
if (prog->getFragDataLocation("outDepth") != 1) {
prog->bindFragDataLocation(1, "outDepth");
bRelink++;
}
if (bRelink)
prog->link();
prog->use();
prog->setUniform("g_celPaletteSize", numShades);
renderObject(sortedObjects_[i], prog);
}

RenderObjects, which represent a draw call with many opengl states, can be accesses via the inherited sortedObjects_ array. The function getNumRenderObjects() returns the total number of render-objects in the list. A draw call can be executed with the convenience function renderObject(), but can also be performed manually with the information contained in the RenderObject data.

In a second pass, a post processing shader is used to perform silhouettte outlining on the previously computed FBO. This pass renders into the input FBO, which in most cases is the back buffer. A simple call to the convenience function from IRenderer is enough to bind the input FBO:

restoreInputFbo();

After configuring the post processing shader, a screen aligned quad which covers the complete viewport is rendered. Since this is an often used technique in post-processing, ACG provides another convenience function for drawing screen quads with post projected coordinates:

// draws a counterclockwise quad with coordinates [-1,-1] .. [1,1] and z = -1
// (progOutline_ is our post processing GLSL::Program)
ACG::ScreenQuad::draw(progOutline_);

The render() implementation ends with a call to another convenience function: finishRenderingPipeline(). This restores common OpenGL states to their default value.

finishRenderingPipeline();