A New Pipe in Your Production Line: Real-Time Render Engines!
By Homam Bahnassi
This tutorial is divided into the following sections:
Skill Requirements
- Good understanding in XSI's render passes.
- Ability to build shaders with the render tree.
- Basic knowledge in real-time shaders programming with the HLSL language.
Introduction
During the last few years, CG production became generally more complex and techie;
especially the rendering and compositing phases which require more work in order to achieve
today's acceptable rendering quality. This is mainly due to the way rendering engines opened themselves
to allow total control and customization of every aspect in the rendering pipeline.
Meanwhile, new advanced software and hardware capabilities have been introduced to serve us
accomplish our rendering tasks more quickly.
Some of these new capabilities were introduced for other industries, such as tools for the 3D games industry,
or the new mathematical researches for simulations. What's nice about CG is that you can adapt other industries'
researches to achieve similar effects in your pretty scenes. Today, we'll be talking about a good example just on this.
In this tutorial, we'll take benefit of some of the new capabilities that XSI offers for the game industry by incorporating
real-time shaders into our daily rendering work.
We'll learn how to use hardware real-time shaders to render common mentalRay passes that are used frequently during editing and compositing.
These passes are available as presets in the XSI Passes menu, so you can compare the results and the rendering time of mentalRay
against the hardware rendered version (I propose you skip the timing comparison. Hey we're talking real-time man!).
For each pass we'll take a brief look at how it's created and rendered in mentalRay before moving on and building it for the real-time
rendering engine.
In order to match the real-time setup we'll use, some of the passes we'll implement need rebuilding for mentalRay with a different way
than the one used in the XSI passes.
Oh, and one important matter. The sample code in this article uses Microsoft's HLSL with XSI's integrated DirectX9 shaders
(time to make real use out of these, eh?). The same effects can be easily converted to other available engines such as Cg or
DSK|ShaderBass (actually I developed the effects on DSK|ShaderBass first, then ported them to XSI's DX9 shaders).
Now with nothing left in our face, let's start with our first render pass.
Note:
The sample scene contains all the shaders we'll talk about here. Follow this
link to get it if you haven't yet!
RT Passes: The White Matte Pass
This is our first pass because it's easy and simple. You can create the mentalRay version of it by getting it
from the Passes menu in the Render toolbar. This pass renders separate objects either with constant white
with white alpha (if they're in the background partition) or constant black with black alpha (if they're in the matte partition).
This pass is mainly used for compositing work. So after you create it, add some objects to its partitions and do a render test.
Now, if you open its partitions in the explorer, you'll notice overrides that are controlling the effect.
We'll recreate the same effect for mentalRay but with a different approach that matches the real-time method.
In the explorer select the matte partition and add a constant material. Set its color and alpha to white.
For the other partition, get another constant material. In its ppg, set the color to black and the alpha to zero.
Now remove all the overrides and do another render test. You should get a result that matches the previous one.
We will now build a very similar setup to the latter mentalRay version using DirectX9 shaders.
Back to the render tree for the first partition. Grab a DXDraw node and a DXHLSLProgram node. Connect these two nodes as shown:
Open the DXHLSLProgram node's ppg. You'll be faced with some sort of a text editor. This is where we'll write the HLSL shader code.
Before we start typing our code, we need to set the profile for the shader we are writing.
The Profile option determines the type of the shader you're writing, and the version you'll compile it to
(also known as hardware platform). Our shaders will be quite simple and won't need the advanced features that
higher shader profiles provide. So, we'll use the vertex shader 1.1 profile, which works on all programmable hardware out in the market.
Next, specify the Entry point for the shader. This will be main.
Now we're ready to write the code… Caution: programmers talk below!
First, we declare two structures. One holds information input to the shader, the other carries the values output from the shader.
This is plain C we're writing here, so lay low:
// input structure
struct VS_INPUT
{
float3 vPos : POSITION; // vertices position vector
};
// output structure
struct VS_OUTPUT
{
float4 vPos : POSITION; // transformed vertices position vector
float4 vDiffuse : COLOR0; // vertices diffuse color
};
Next, we will write the main function. The one that actually does the calcul. This function returns an instance of VS_OUTPUT
and takes a VS_INPUT instance, and a 4x4 matrix named simodelviewprojT as arguments (or parameters).
Note that the second argument is special (simodelviewprojT). XSI looks for certain variable names and fills them with relevant info.
The XSI documentation names these Softimage semantics. In our case, simodelviewprojT will be filled with the transpose
of the Model x View x Projection matrix. We'll see where to use this shortly. You can find more about Softimage semantics
in the Softimage|XSI user's manual.
Back to the main function, we'll simply output constant white with black alpha to the vertices like what we do in the mentalRay version…
// main function
VS_OUTPUT main(VS_INPUT Input, uniform float4x4 simodelviewprojT)
{
// Declare an instance of our output structure
VS_OUTPUT Out;
// Transform input position from model space to screen space
Out.vPos = mul(float4(Input.vPos,1),simodelviewprojT);
// Output vertex color as constant white (r,g,b,a)
Out.vDiffuse = float4(1,1,1,0);
return Out;
}
Now to test this shader, we need to change the viewport's display mode to Real-time>DirectX9. Don't forget to change the
Build parameter in the DXHLSLProgram ppg to compile and run. All objects in this partition should now appear in constant white color.
For the other pass partition, we'll repeat all the same steps we did, but change the output color in the main function to
black with white alpha (0,0,0,1).
There… the same mentalRay result. The only thing left now is to render these results to disk.
In XSI 4, this can be done easily for any real-time engine exactly the same way you render with mentalRay.
The only difference is that you'll have to switch the render engine to DirectX9 and voila!
Note:
In the shader code above, we set the alpha channel color for the matte partition to black and the background objects to white.
This is because the DirectX9 renderer sets the background alpha value always to white, and it isn't affected by the background
color in the render engine ppg. Thus we have to invert output alpha data to workaround this bug.
You must invert it (again) prior to using it (or else you'll get holes where you don't expect).
RT Passes Reloaded: The Depth Pass
Now for a more interesting shader: The Depth Pass. Depth passes are very common in CG work since they're used
in a wide variety of effects and compositing (e.g. fog effects and depth of field). So it's really worthy to have a faster way to render the thing.
As usual, add the mentalRay version of this pass then investigate it. You’ll notice that it relies on the constant density
volume shader to render the depth pass for objects.
For the real-time version, we’ll use a simple approach to fake depth rendering without using volume shaders or other complex things.
Taking the interpolated vertices distance from the camera, this gives quite a close result to a mentalRay implementation using
ray length mode in the scalar state node.
So in the render tree for this partition, grab a DXDraw node and a DXHLSLProgram node and connect them one after the other
in the same way we did in the white-matte pass.
In the DXHLSLProgram ppg, clone the same settings we set at the White Matte pass, and add the same input/output structures to the code pane:
// input structure
struct VS_INPUT
{
float3 vPos : POSITION; // vertices position vector
};
// output structure
struct VS_OUTPUT
{
float4 vPos : POSITION; // transformed vertices position vector
float4 vDiffuse : COLOR0; // vertices diffuse color
};
Next we’ll write the main function for the shader with the same arguments we used in the White Matte pass:
VS_OUTPUT main(VS_INPUT Input, uniform float4x4 simodelviewprojT)
{
// Specify the near/far distance from the camera to control the
// range of the depth effect change it to fit your scene…
float DepthControls_Far = 300;
float DepthControls_Near = 100;
// Output the vertices in screen space
VS_OUTPUT Out;
Out.vPos = mul(float4(Input.vPos,1),simodelviewprojT);
// Computing the distance of each vertex from the camera
float fNF1 = 1/(DepthControls_Far-DepthControls_Near);
float fNF2 = -DepthControls_Near/(DepthControls_Far-DepthControls_Near);
float fZCam = Out.vPos.z * fNF1 + fNF2;
// Outputting the diffuse color for each vertex
Out.vDiffuse = 1-fZCam;
return Out;
}
Compile and run the shader in your DirectX9 viewport.
We can improve the shader a little bit using the custom parameters for controlling the near and far ranges without hacking
in the code each time we need change them. So select the DXHLSLProgram shader in the explorer (you need to change the explorer
scope to show all nodes) and add a custom parameter set with name of DepthControls. Now add two float parameters to it.
Name them near and far.
Now what remains is to hook these new parameters so they control variables in the shader code. So back at the DXHLSLProgram ppg, modify the
main function header as following:
VS_OUTPUT main(VS_INPUT Input,
uniform float DepthControls_Near,
uniform float DepthControls_Far,
uniform float4x4 simodelviewprojT)
and remove (or comment) the near and far variables:
// float DepthControls_Far = 300;
// float DepthControls_Near = 100;
That’s it for this shader. You can now modify the depth ranges and animate them using the custom parameters we just added.
Let’s now move to the last shader in our real-time trilogy!
RT Passes Revolutions: Normal Pass
This pass is not available in XSI as a pass preset (as was the case with the latter couple of passes). Instead, it is available
in the Render options as an additional output file like zpic, tag and motion vectors.
So we need to build it from scratch with mentalRay shaders before we port it to the real-time rendering engine.
Create an empty pass and add a new material to its partition.
Open the Render Tree and grab a vector state node. Change its State Parameter to Normal Vector and connect it
to the Surface of the material. This will render the interpolated normals without biasing
(you'll see dark or black areas at certain places, which isn't rational for normalized normals).
Normalized normals can have their components (x,y,z) in range [-1,1]. However, when converting these values directly to colors,
we'll lose all negative info, because negative colors are clamped to zero directly.
We need a way to express normals in the [0,1] range rather than [-1,1] without losing information.
So we'll use what is called scaling and biasing. Scaling because we first divide each channel's value by two,
and biasing because we add 0.5 to the result after scaling. Try it out on any number in the [-1,1] range and see how it
converts to the [0,1] range. Magical, eh? To the implementation:
Grab two vector math nodes and set them as following:
Make a render test and compare it with the file that XSI renders with normal output.
For the real-time version, we will use exactly the same method to build the effect.
Grab a DXDraw and a DXHLSLProgram and connect them as we did previously.
In the DXHLSLProgram ppg, set all the same setting as in previous shaders, and add the two input/output structures we always add.
However, we will add one more field to the input structure. We need to add the normal vector so we can use its value in our calculations.
// input structure
struct VS_INPUT
{
float3 vPos : POSITION; // vertices position vector
float3 vNormal : NORMAL; // vertices normal vector
};
// output structure
struct VS_OUTPUT
{
float4 vPos : POSITION; // transformed vertices position vector
float4 vDiffuse : COLOR0; // vertices diffuse color
};
Now for the main function, add the same header we added in previous shaders.
VS_OUTPUT main(VS_INPUT Input, uniform float4x4 simodelviewprojT)
{
// Output the vertices in screen space
VS_OUTPUT Out;
Out.vPos = mul(float4(Input.vPos,1),simodelviewprojT);
// Scale and biasing normal vector
float3 vec3Biased = Input.vNormal / 2.0f;
vec3Biased += 0.5f;
// Output result to vertex diffuse color
Out.vDiffuse = float4(vec3Biased,1);
return Out;
}
Compile and execute the shader in your DirectX9 viewport, and that’s it for today's shader trilogy.
After you've done these prototype passes, you can save them as presets so you can use them easily in your daily rendering work.
Note:
XSI's DirectX9 renderer outputs images in integer format (8 bits per channel). This might not be useful in cases where high precision is required
from the information contained in the normal pass (or the depth pass, for what is worth). DSK|ShaderBass supports floating-point
formats (32 bits per channel) given that your hardware supports it (i.e. ATI's Radeon 9800, or nVIDIA's GeForce 6800 cards).
Conclusion
Throughout this article, we saw how much of an aid real-time rendering can be.
We saw also how mature this type of renderers have become.
Sometimes they can be programmed with just the same way advanced renderers are programmed with.
Each day I deal with real-time shaders, I discover how flexible, powerful and fast are these genius little things.
We should really consider using these powerful tools when planning to do production rendering,
especially when you have scenes that take a lot of rendering time.
With the shaders I showed here as a starting point, you can achieve a wide variety of effects,
and that's still without touching pixel shaders, which open a whole new set of effects (e.g. post processing effects).
That's it for today. See ya in the next article…