Log in | Imprint
last updated 7/23/2009 | 204215 views
On Twitter
On Vimeo
On YouTube

Main . Blog

7/23/2009: A shader-driven rendering pipeline

Today's entry's focus will once again be on the rendering pipeline, following my short introduction to its basic functionality about a year ago.

As already stated in the title, the beezEngine rendering pipeline is fully shader-driven, allowing programmers to change the way the scene is rendered and processed simply by changing shader code. This enables programmers to introduce a great variety of entirely new shaders to the engine without any need for custom application-side integrational code.

Communication between engine and shaders is handled by the so-called effect binders. The engine offers a whole bunch of these classes, providing the data necessary to transform and render objects as well as to perform lighting, shadowing and processing. In addition, these effect binders handle the creation of temporary, permanent and persistent render targets, automatically setting, swapping and scaling them at the request of the given shader. Effect binders even allow for dynamic flow control when it comes to rendering multiple passes, repeating or skipping passes according to the context in which the shader is used.

This all sounds extremely general, so here you go with some example code:

// Enables additive Blending
pass AdditiveBlending < bool bSkip = true; >
{
AlphaBlendEnable = true;
BlendOp = ADD;
SrcBlend = ONE;
DestBlend = ONE;
}

// Light pass
pass LightDP < string Type = "Main"; string LightTypes = "Directional,Point";
bool bRepeat = true; string PostPassState = "AdditiveBlending"; >
{
VertexShader = compile vs_2_0 RenderPhongVS();
PixelShader = compile ps_2_0 RenderPhongPS(GetLightingPermutation(LIGHT_DIRECTIONAL, LIGHT_POINT));
}

This snippet is taken from the engine's default phong shader which makes use of quite a lot of the features described in the text above: The first pass is only used as a state block, the annotation bSkip telling the effect binder being responsible for dynamic flow control to skip this pass during normal rendering. The second pass is one of the passes that actually perform lighting, each of these passes providing one permutation for all of the different (pre-sorted) possible light type combinations. The Type annotation specifies that this pass is to be applied during the main (shading) stage of the current frame, there are also a pre (depth and additional data) stage as well as several processing stages. The LightTypes annotation specifies order and type of the lights that may be applied to this pass, bRepeat stating that the pass may be repeated several times if there are more lights to follow fitting this permutation. Finally, the PostPassState annotation specifies the name of the state-block pass defined earlier, thus once a lighting pass has successfully been applied, render states are changed to enable additive blending of the passes to follow.

Here's another extract taken from the tonemapping effect:

// Average luminance
pass LogLuminance < string Destination0 = "LuminanceTexture";
float ScaleX = 0.25f; float ScaleY = 0.25f; >
{
VertexShader = compile vs_3_0 Prototype::RenderScreenVS();
PixelShader = compile ps_3_0 RenderDownscaledLogLuminancePS(g_screenSampler, g_fScreenResolution);
}
pass AvgLuminance < string Destination0 = "LuminanceTexture";
float ScaleX = 0.25f; float ScaleY = 0.25f;
int ResolutionX = 4;
bool bRepeat = true; >
{
VertexShader = compile vs_2_0 Prototype::RenderScreenVS();
PixelShader = compile ps_2_0 RenderDownscaledLuminancePS(g_luminanceSampler, g_fLuminanceResolution);
}
pass ExpLuminance < string Destination0 = "AdaptedLuminanceTexture"; int ResolutionX = 1; int ResolutionY = 1; >
{
VertexShader = compile vs_2_0 Prototype::RenderScreenVS();
PixelShader = compile ps_2_0 RenderDownscaledLuminancePS(g_luminanceSampler, g_fLuminanceResolution, true);
}
This snippet shows off some of the post-processing features provided by the effect binders. The first pass specifies a custom temporary texture to render the average logarithmic luminance to, at the same time requesting the engine to scale the render target down by 1/4. The second pass further averages the logarithmic luminance rendered by the previous pass, repeating the down-scaling in steps of 1/4 until a x resolution of 4 is reached. Note that as this is a processing effect, render targets are automatically swapped unless explicitly specified otherwise. The third pass performs one more step of averaging, outputting the exponential value of the result into a different custom persistent render target of the resolution 1x1 (persistent, as the result needs to be blended with the luminance value of the previous frame to simulate eye adaption). Of course, the 0 in the Destination0 implies that it is possible to make use of multiple render targets at once.

The creation of new render targets to perform averaging, blurring and similar stuff on, is easy as pie:

// Luminance texture
Texture g_luminanceTexture : LuminanceTexture <
string Type = "Temporary";
string Format = "R32F";
>;
// Adapted luminance texture
Texture g_adaptedLuminanceTexture : AdaptedLuminanceTexture <
string Type = "Persistent";
string Format = "R32F";
>;
In that way, the whole rendering pipeline may be customized simply by changing shader code:

// Depth texture
Texture g_sceneDepthTexture : SceneDepthTexture <
string Type = "Permanent";
string Format = "R32F";
string DefaultIn = "Pre";
string FinalIn = "Pre";
bool bClear = true;
bool bClearDepth = true;
float4 ClearColor = 2.0e16f;
>;

// Scene texture
Texture g_sceneTexture : SceneTexture <
string Type = "Permanent";
string Format = "R16G16B16A16F"; // HDR
#ifndef SCREEN_PROCESSING
string DefaultIn = "Main,Processing";
string FinalIn = "Main,Processing";
#else
string DefaultIn = "Main";
string FinalIn = "Main";
#endif
string DefaultSlot = 0; // Allows for MRT
bool bClear = true;
float4 ClearColor = float4(0.0f, 0.0f, 0.0f, 1.0f);
>;

The DefaultIn and FinalIn annotations also explain why it is not always necessary to specify custom render targets: DefaultIn specifies the stages that the render target is to be used in whenever there is no explicit destination given. FinalIn specifies the stages during which the render target is to be promoted onto the screen or onto different objects (e.g. when rendering reflections), given that one of the stages specified is the last to be rendered.

One more important feature concerning the shader-drivenness is the possibility to define render queues inside the shader framework:

// Solid renderables
RenderQueue g_solidRenderQueue : SolidRenderQueue <
unsigned int Layer = 0;
bool bDefault = true;
>;

// Canvas renderables
RenderQueue g_canvasRenderQueue : CanvasRenderQueue <
unsigned int Layer = 1;
bool bPrePass = false;
bool bDepthSort = true;
>;

// Alpha renderables
RenderQueue g_alphaRenderQueue : AlphaRenderQueue <
unsigned int Layer = 2;
bool bPrePass = false;
bool bDepthSort = true;
>;

Render queues specify a layer number that influences the order in which the queues are rendered (similar to the css layer attribute, haha), as well as certain flags, such as switching off specific stages for a particular queue, or enabling depth sort for alpha-transparent objects. A shader may then specify a render queue inside its technique annotations:

technique SM_2_0 < string RenderQueue = "AlphaRenderQueue"; >

« Back

© 2017 Tobias Zirr. All rights reserved.