X-tasy (ecstasy) with Real-Time Shaders
By Homam Bahnassi
This tutorial is divided into the following sections:
Skill Requirements
Basic knowledge in the XSI Render Tree System. Some background in Assembly Shader Language won't hurt anyone too.
X-plaining DSK|ShaderBass®
People keep talking about the evolution of today’s graphics hardware,
and keep wondering whether it will ever be able to produce our production-quality renderers.
Well, that’s why we’re here today! If you’ve ever wanted to tackle those real-time shaders they keep talking about,
then this is your day. And by the end of this tutorial, we might have an answer… (or not!)
In order to drive these advanced hardware capabilities, you need something able to expose them to you
with ease and control. We’ve seen recently different released frameworks for developing and writing
advanced effects and shaders. The list includes ATI’s RenderMonkey, nVIDIA’s CgFX… and
DSK|ShaderBass®!
Q: But, what's DSK|ShaderBass in the first place?
A: DSK|ShaderBass is a real-time 3D effects development framework.
Q: Oh really? Then what makes DSK|ShaderBass so special?
A: The big thing that’s missing from most of the individual frameworks released is their weak
integration with 3D packages, which is very important to instantly visualize and do required
changes in the geometry.
DSK|ShaderBass tightly integrates into your 3D authoring software to allow you maximum control
and instant visualization of real-time effects. The kit contains a bunch of real-time shader nodes
and a render window. This render window is based on DirectSkeleton’s advanced rendering capabilities,
which means that any effect you build will be exactly visualized in your DirectSkeleton-based applications
without any need for additional conversion work that’s usually required to verify and achieve such effects.
In this tutorial we'll learn how to build an advanced effect using the first version of DSK|ShaderBass,
which is currently available only for Softimage|XSI 3.
So before scrolling down your mouse for the rest of the tutorial, make sure you download
and install both DSK|ShaderBass and the tutorial project on your XSI system.
Download DSK|ShaderBass®
Download xsi_rtshaders2.zip
X-Ray in real-time
Every one who dug around in XSI|Net’s render samples must have found mentalRay’s X-Ray shader sample.
The sample shows how to render 3D objects as if they were taken through X-Ray. Looks cool enough? It’d better be,
because we’ll be rebuilding this same effect in DSK|ShaderBass! Welcome!
X-ecuting DSK|ShaderBass®
One good practice about real-time shaders development is to have a draft version of the final effect
that’s done already using standard (non-real time) shaders. This way you can easily concentrate on the
artistic result without limiting your mind with technical stuff. This pre-building phase is not necessary
for basic effects, but when it comes for complex multi-pass effects, you'll find it helpful and more
logical to use standard shaders to design the functionality, unless you feel very comfortable with the
current real-time shader programming languages!
Luckily for us, this tutorial's "draft" mentalRay version is already done for us. All what we need is just
a little bit of analyzing to understand how it works. Then, we can begin implementing the thing in DSK|ShaderBass.
With XSI running, open its Net View to the Library section. There, find the X-Ray sample
under the Render tab. Drag the link from Net View to any viewport to open the scene.
Draw a render region in the camera view and take a look at the rendered result. That’s what we’re heading
for tonight.
Open the Render Tree to view how the effect is built.
You can easily find out the role of each node by disconnecting all the nodes then re-connecting them one by one
while keeping an eye on the result (you can also use the Node Preview – obsolete in the render tree).
I won’t explain all the shaders in this tree because they’re already explained well in XSI’s documentation
under the X-Ray example. However, we’ll get a closer look at the incidence shader.
In the example, the incidence shader is used to control the objects transparency in a custom way.
Actually what this shader does, is to calculate the incidence angle between the object’s normals and one of the
available vectors. The one we’re interested in here is the camera view vector, which is used in the sample.
This vector updates dynamically as you change the camera properties and orientation, thus you always get
transparent faces (in case you connect it to the object transparency) when their normals are nearly coincident
to the camera’s view vector (or simply when they face the camera).
So, the key to simulate this shader in real-time is to write a custom effect in DSK|ShaderBass to achieve
the same mental ray incidence shader functionality.
X-ecuting DSK|ShaderBass®
The first thing we should do before we can build any effect is to invoke DSK|ShaderBass’s render engine
to display our work. As with other real-time (RT for short) viewing systems in XSI, this is simply
done by switching one of the view ports to the RT mode and connecting an RT-shader to the suitable input port
in the material’s node of the object.
In our case, the first shader node we'll place is the DSK3DRenderPass node.
This can be found in the RealTime shader presets folder.
Successfully doing this step will cause DSK|ShaderBass’s render window to pop up and draw the objects that
have a DSK3DRenderPass node connected to them. In our case, these are the feet bones.
Now, open the DSK3DRenderPass property page (PPG for short) and skim over it a little to familiarize
yourself with it. Try tweaking the parameters in the Basic Options and see how that
reflect on the objects directly.
Another important node we're gonna need in this tutorial is the DSK3DShader node. Add one to the shader
and connect it to DSK3DRenderPass's Previous Parameter port.
Don’t feel bad if you didn’t understand DSK3DShader, because
that's what we're gonna talk about in the rest of this tutorial.
X-ploiting DSK|ShaderBass’s Effect Renderer
The DSK3DShader node is something like the secret lab where you invent
all your wicked effects. Effects are written in one of the voodoo shader languages that use other
DSK|ShaderBass nodes as their ingredients.
It’s through this lab we’re gonna cook the X-Ray shader. This implies that we'll have to do some shader
programming using one of the available shader programming languages. For this tutorial, we'll write a vertex
shader with the assembly shader language to achieve the desired effect (an HLSL version is
available on In|Framez NetView also).
It would be great if you have some background about the assembly language (and vertex shaders in general).
For complete reference on the assembly language, you can refer to the
MSDN Library.
We can write the effect's code either by typing it directly in the Manual Source Entry field,
or by writing it in an external text editor and saving it as an .fx file. This file then can be read be
DSK3DShader by specifying its name in the Source File field.
Personally, I prefer to write effects in Visual Studio, because it provides syntax coloring,
which helps clarifying the code much.
After you finish editing your code, you need to click Recompile... to have DSK|ShaderBass
re-evaluate and build the new effect from the file. If the effect fails to build, XSI will log an error message
telling you why.
Let's start writing a basic effect file that renders all objects with a user-defined constant color.
First we need to declare variables that will be used in the code. A variable is declared simply by listing
its type first, followed by its name.
vector vDiffuse; // Diffuse Color
matrix mTotal; // World*View*Proj Matrix
The first variable is vDiffuse, which is declared as a 4D vector. This will be used to store the
RGBA color information entered by the user.
The second variable mTotal is a 4x4 matrix variable, and it will be used to store the matrix
that transforms object vertices into screen space.
Now that we have our variables declared, we need to connect something that controls their values.
This is where we add the various DSK3DParamSomething shader nodes.
Here, we need two parameter controlling nodes. One that controls the vDiffuse variable,
which will finally affect the final color of the objects. The second node is required to compute the
vertices transformations matrix.
So, grab a DSK3DParamColor node and connect it to the Previous Parameter port in the
DSK3DShader node. This is a simple node that allows for entering color information into 4D vector
variables (as well as integer ones). Open its PPG and edit the Name field so it contains the
variable name vDiffuse. This binds the DSK3DParamColor to vDiffuse, so vDiffuse always
contains the color value shown in DSK3DParamColor.
Note that the effect code and variable names are case sensitive! Typing the a variable name without
regard to its case will fail the parameter connection.
Now we’re left with mTotal. This one needs a DSK3DParamMatrixStack to fill it.
DSK3DParamMatrixStack performs matrix concatenation and saves the result in the variable it’s connected to.
Grab yourself an instance of this node, connect it, and open its ppg. Set the Name field to mTotal.
Now DSK3DParamMatrixStack is bound to mTotal.
We’ll have to specify how the resulting matrix is calculated. This is done by specifying the operands of a simple
equation that just multiplies those operands together.
In M1, choose Local-To-World Matrix.
For M2, choose the View Matrix.
And for M3, choose the Projection Matrix.
We’ll leave M4 to Identity Matrix, which is a neutral value.
The node will multiply the chosen matrices and store the final result in the mTotal variable.
We’ll nearly see how can we use mTotal and vDiffuse in our effect.
After we’ve finished declaring variables, we can start writing the effect’s techniques and passes.
The Effect Renderer is based on what is called Techniques. Techniques are different
methods for rendering the same final result, but on hardware cards with different capabilities.
So each effect should contain at least one technique. For our case we'll write only one technique.
Luckily for us, this technique we’re gonna achieve doesn’t require much advanced hardware capabilities.
We can declare a technique like in the following example:
// First Technique, uses a single pass
technique tecXRaySinglePass
{
Note that tecXRaySinglePass is an arbitrarily chosen name and can be anything else
(John_Kennedy, for example).
After declaring the effect’s technique, we need now to declare our render passes. Passes in
DSK|ShaderBass’s Effect Renderer are something like render passes in XSI; they are used when you can't render
your effect in one pass because of hardware limitations. Here we only use one pass to render the objects.
We declare a pass in much the same way we declare a technique, but we use the keyword pass instead
of technique. Note that passes must be declared inside technique blocks.
pass passAll
{
A pass can contain whatever states you wish to set on the rendering device. Since we’ll be writing a vertex shader,
then we’ll use our pass to update the vertex shader’s constants.
These constants will hold values from the variables we declared at the beginning of the effect.
A single constant slot can fit a 4D vector value. So, our vDiffuse variable will just fit in a single
constant slot. However, the matrix variable type is an exception. Since mTotal is a 4x4 matrix, this means
it’s composed of 4 different 4D vectors at once! So, mTotal will allocate four consecutive constant slots
from the vertex shader’s constants table.
The constant table is located on the GPU in case we’re running the vertex shader on hardware (video card processor).
This is the only place a shader can read user-data from. Our constants are now filled with values from our
variables as in the following lines:
// Load shader constants
VertexShaderConstant[0] = <vDiffuse>; // Material Diffuse
VertexShaderConstant[1] = <mTotal>; // World*View*Proj Matrix
Now, we have to write the shader’s functionality itself. The process goes as follows:
First we specify what language will this shader be written with. In our case we’ll use the assembly language,
so we type asm after the keyword VertexShader =, and open up the code block bracket ‘{‘.
The first instruction a shader must have is the version instruction. Valid versions include:
vs_1_1, vs_2_0 and vs_2_x. In this tutorial, we’ll stick to vs_1_1 for two reasons:
First, vs_1_1 gives us all the instructions we need for our effect, two, support for vs_2_0 is limited
to advanced hardware that’s not so popular as to the writing of this tutorial.
// Definition of the vertex shader, declarations then assembly.
VertexShader =
asm
{
vs_1_1 // Version instruction
A real-time vertex shader has registers that allows it to read/write data through its operation.
These registers come in 4 flavors:
-
Constant Registers: We talked about those above. Constant registers are read-only in the shader’s function,
and can only be modified on per-object basis. Constant registers are named ‘cX’ where ‘X’ is the
index of the required constant, like c0, c4 and so on.
-
Input Registers: These contain data taken directly from the geometry data to be rendered. For example,
vertex position comes through v0, and normal data comes through v1 …etc. Like constant registers,
Input registers are read-only.
-
Output Registers: These are the reason why a vertex shader is there. The shader must output its final results
to those registers. Otherwise, the shader is considered invalid. A vertex shader is usually expected to output final
vertex positions through the oPos register, and the calculated vertex color through oD0. There are
other output registers that we’ll see together soon. Output registers are write-only.
-
Temporary Registers: Use’em to store intermediate calculation results. Temporary registers are named
rX, where ‘X’ is a number. For example, r0, r1 and r2. Temporary registers
are limited, so don’t go nasty with them. Plus, the less registers you use, the faster your shader is.
Temporary registers are fully read/write.
Ok, so we know what registers are. Back to our shader.
Directly following the version instruction, there must be a list of input registers used through out the shader.
Input registers are declared by usage and input register number. Our shader’s input declaration is:
dcl_position v0 // Declare vertex position
dcl_normal v3 // Declare vertex normal
Whew, now we’re done with all that groundwork stuff. We’re now ready to write the first effective
instruction in our shader. This is the m4x4 instruction. I don’t want to turn this tutorial
into a programming tutorial, but it would be good to know that m4x4 transforms a given vector by some matrix.
The following instruction transforms our object’s vertices from local (object) space into screen (projection) space.
Then it outputs the result to the oPos output register. Like this:
m4x4 oPos, v0, c1 // Transform vertices by world/view/projection matrix
Note how we pass the instruction’s parameters. First comes the instruction’s target register.
Then comes the first operand, which is the vector to be transformed, followed by the index of a constant
holding a matrix (c1 in our case).
To finish this shader, we’re left with calculating the vertex’s final color. Here, we simply emit the diffuse
color that the user had specified in the DSK3DParamColor node. This is done with mov instruction.
This instruction simply copies (not moves) the diffuse color from c0 to the oD0 output register.
Don't forget to close any opened } brackets before compiling the effect.
// Diffuse Color
mov oD0, c0
};
}
}
Now that we finished this part of the effect, press the Recompile… button in the DSK3DShader
node, and see how the effect renders. Try tweaking the color values in the DSK3DParamColor node to modify
the object’s color.
Congratulations, we've built something like the constant shader in mental-ray (Hey I know it’s not the
greatest effect yet, but for a start…).
(Result that can be viewed in step1.scn)
X-amining the Incidence Shader
Before we continue writing the effect file, we need to stop a little and closely examine how the incidence
shader works in mentalRay.
As I said before, the idea behind the incidence shader is to softly detect the geometry’s edges based on the
camera vector and object’s normals.
So to mimic its functionality, we need to find some way to track the camera’s view vector, and pass it down
to our shader. Luckily for us, DSK|ShaderBass provides us with the flexible DSK3DParamLCTracker node.
This node gives us the ability to track any camera or light in the scene and return their properties
(including their direction vector) for further calculations in the shader.
Get a DSK3DParamLCTracker node and connect it to the last Previous Parameter input port.
Open the node’s PPG and in the Object Name field, specify the name of the camera you want to track
(named 'camera' in the tutorial's scenes).
From the tracked object type choose Camera to instruct DSK|ShaderBass that we’re tracking a camera rather
than a light. Type 'Camera' in the tracked object name field. Set the tracked data to Direction.
At the top of the PPG, make sure you type the variable name vViewDir so DSK3DParamLCTracker
connects the direction value with vViewDir in the effect. In the tracked data space option, choose
Object Space to pass the camera’s view vector in local object space. Now, get back to the effect file,
and declare the vViewDir variable which is bound to the DSK3DParamLCTracker node.
vector vViewDir; // View Direction in Object Space
vector vDiffuse; // Diffuse Color
matrix mTotal; // World*View*Proj Matrix
// First Technique, uses a single pass
technique tecXRaySinglePass
{
pass passAll
{
// Load shader constants
VertexShaderConstant[0] = <vDiffuse>; // Material Diffuse
VertexShaderConstant[1] = <mTotal>; // World*View*Proj Matrix
As with other Varibles, we need to load vViewDir in one of the many vertex shader constants available to us.
Note that we used c5 to hold the view direction. Why not c2? If we look at c1, we notice
that it’s filled with a matrix variable. And since a matrix is 4 4D vectors, then it will take 4 constant registers
at once. These are c1, c2, c3 and c4. So, our next valid constant is c5.
VertexShaderConstant[5] = <vViewDir>; // View Direction in Object Space
// Definition of the vertex shader, declarations then assembly.
VertexShader =
asm
{
vs_1_1 // Version instruction
dcl_position v0 // Declare vertex position
dcl_normal v3 // Declare vertex normal
m4x4 oPos, v0, c1 // Transform vertices by world/view/projection matrix
// Diffuse Color
mov oD0, c0 // Output diffuse color
For the edge detection calculations, we need to compute the dot product between the normal vector
coming through v3 with the view direction vector that is loaded into c5.
When performing calculations on different vectors, they should be all in the same space.
Now, normals come to us through v3 in object space. So, we have to get in the camera direction
in object space also. Otherwise we would be doing the wrong calculations.
For the sake of simplicity, we'll directly emit the result to the diffuse color’s red channel only.
This is done by "masking" the output register like oD0.x (XYZW in oD0 are mapped to RGBA respectively).
// Edge-based density calculation
dp3 oD0.x, v3, c5 // dot product, emit the result on R channel
};
}
}
Again, save the effect file and recompile it from the DSK3DShader PPG to see the results in the
DSK|ShaderBass render window.
(Result that can be viewed in step2.scn)
Don't be surprise if you don't get anything but that boring constant color on your objects.
This happened because the result of the dp3 instruction requires some more processing to be meaningful.
The dp3 instruction can return a value ranging from -1 to 1, which is invalid as a color result.
So, we have to scale this value so it fits in the 0-1 range.
Before we go on writing this in the shader, we’ll add another node that will be used to control the edge’s density.
Add a DSK3DParamVector node to the end of the render tree. Open its ppg, type vDensAmount in the
Name field.
Head up to the effect file, and declare it as a vector variable:
vector vViewDir; // View Direction in Object Space
vector vDiffuse; // Diffuse Color
matrix mTotal; // World*View*Proj Matrix
vector vDensAmount; // Density
// First Technique, uses a single pass
technique tecXRaySinglePass
{
pass passAll
{
// Load shader constants
VertexShaderConstant[0] = <vDiffuse>; // Material Diffuse
VertexShaderConstant[1] = <mTotal>; // World*View*Proj Matrix
VertexShaderConstant[5] = <vViewDir>; // View Direction in Object Space
Load the vDensAmount variable in a vertex shader constant. In addition, we fill one more shader
constant with some values we’re gonna need to the scaling operation we talked about:
VertexShaderConstant[6] = <vDensAmount>; // Density Amount
VertexShaderConstant[7] = {0,0.99,0.5,1.0}; // Some useful constants
// Definition of the vertex shader, declarations then assembly.
VertexShader =
asm
{
vs_1_1 // Version instruction
dcl_position v0 // Declare vertex position
dcl_normal v3 // Declare vertex normal
m4x4 oPos, v0, c1 // Transform vertices by world/view/projection matrix
// Diffuse Color
mov oD0.xyz, c0.xyz // Output diffuse color
Head up to the edge detection code, let’s add the code that scales the dp3 result to 0-1 range.
We’ll store the result of the dp3 instruction in a temp register for further calculations as follows:
// Edge-based density calculation (not normalized yet)
dp3 r1.x, v3, c5 // Compute r1 = dot product
mad r1.x, r1.x, c7.z, c7.z // Scale to [0,1]
max r1.x, r1.x, c7.x // Clamp r1 to zero
min r1.x, r1.x, c7.w // Clamp r1.x to 1.0
I won’t go into the details of each instruction, but to cap things off, r1 now contains the dp3
value scaled to 0-1 range. Next, we’ll add the density control parameters to the mix:
mad oD0.x, r1.x, c6.y, c6.x // Add modifiers and emit in the red channel
};
}
}
The x and y members of the vDensAmount variable control the density and falloff of the
shader’s result. Remember that vDensAmount is bound to the DSK3DParamVector node, so you can use
that to control the density value.
Save and recompile the effect file. Open the DSK3DParamVector PPG and tackle the X and Y
parameters. Note how the intensity and falloff of the shader are changing.
Values like 'X: -0.05' 'Y: 5.0' will give similar result to the mentalRay sample.
(Result that can be viewed in step3.scn)
X-tending results to Alpha
Now, instead of emitting the edge detection results to the red channel, we'll emit them to the alpha
channel to drive the object’s transparency.
In the edge detection code, just modify the diffuse output channel in the mad instruction from
oD0.x to oD0.w. This will change the diffuse output channel from the red channel to the alpha channel.
// Edge-based density calculation
dp3 r1.x, v3, c5 // Compute r1 = dot product
mad r1.x, r1.x, c7.z, c7.z // Scale to [0,1]
max r1.x, r1.x, c7.x // Clamp r1 to zero
min r1.x, r1.x, c7.w // Clamp r1.x to 1.0
mad oD0.w, r1.x, c6.y, c6.x // Add modifiers and emit as vertex alpha
};
}
}
Save and recompile.
After we got the edge detecting result on the diffuse alpha channel, we need now to set proper blending
properties in DSK3DRenderPass.
Enable the Alpha Blending option in DSK3DRenderPass. Then choose Source Alpha for the
Source Blend Factor, and Inverted Source Alpha for the Destination Blend Factor.
Make sure that Add (Source+Destination) appears as the Blend Operation.
These settings will result in the object being transparent more where the alpha value nears 0
(normal alpha-transparency).
With those settings intact, you should now see the objects render transparently according to the alpha information
that comes from the edge detection shader.
You might notice some rendering errors due to objects intersecting with each other. In that case, we can
disable z-testing between them so they render correctly. This can be achieved through DSK3DRenderPass's
Advanced Options tab. Just clear the check box that reads Depth Test and voila!
(Result that can be viewed in step4.scn)
X-hibiting the Fractal Texture
Well, it’s already fine by now, but there’s still something missing when compared to mentalRay’s version.
Notice that nice diffusion in the mentalRay sample? We’ll try simulating this in our RT shader.
This requires either an already calculated fractal picture to use it as 2D texture, or a 2D fractal generator shader
like the one that is used in the mentalRay sample. For this tutorial we'll avoid the second method
(which requires writing a pixel shader), and we'll stick to the easy picture method.
Get a DSK3DParamTexture and a DSK3DTextureStage node, connect them to the end of the render tree.
Don’t worry much about the order because it doesn’t matter in DSK|ShaderBass.
These nodes are something like the texture and clip nodes in XSI, where the DSK3DParamTexture acts
as the clip node and the DSK3DTextureStage acts as the texture node.
Open the DSK3DParamTexture PPG and specify the picture file name. Browse in the tutorial project
and choose the fractal.bmp from Picture directory. This time instead of specifying a Name for the
node, we need to specify which Sampler this texture will be set on. We have a maximum of 8 samplers for each
material, or in other words, we can blend a maximum of 8 textures on the same material -of course this number
is affected by your hardware's capabilities-. Leave the Sampler set to 0. Click Refresh Texture…
at the bottom of the PPG so it gets loaded by DSK|ShaderBass.
Now, in the DSK3DTextureStage PPG we need to set a bunch of options.
First we need to define a valid texture projection. Under the Basics tab, choose Use UV's from object's texture space
in the texture space list(Source).
Make sure that the Stage is set to 0 so it matches the Sampler in the DSK3DParamTexture.
Until now, we just added a texture and defined its projection. Now, we'll set the blending operation for the texture,
which is something like controlling the mixing options in the mixer_2colors node in XSI.
In the DSK3DTextureStage move to the Texture Operations tab. This is where you can set all your
texture blending options for a single stage. We'll do a simple blending effect by multiplying the texture
color with the object’s output diffuse color (the one calculated from the vertex shader).
So, in the First Argument choose Texture to get the texture color. In the Second Argument
choose Current to get the shader’s output diffuse color (Diffuse will work too).
Choose Modulate in the Operation to multiply the color information in Argument1 with
Argument2. Leave the rest to their defaults.
Finally, Return to the darn effect file and add the texture implementation code:
dcl_texcoord v7 // Declare 2D texture coordinate set
// Emit first texture coordinates (decal fractal)
mov oT0, v7 // Output decal texture UV
For the last time recompile the effect file and note how the fractal texture adds nice experience to the final result.
To get a more interesting look, change the object’s diffuse color to bright green and set the background color
in DSK|ShaderBass render window to black. You can also change DSK3DRenderPass’s Destination Blend Factor
to One to get a color screen effect.
Don't forget also to turn off the grid display.
X-cuse me now
Ok, by now, you should’ve gained some info on what things are involved in writing a real-time shader.
Based on the effect you want to achieve, you might be required to do much more complex calculations,
and more than one render pass.
We’ve seen today how XSI and DSK|ShaderBass interact to provide us with a nice level of control over shader inputs.
Many of the nodes we set have animatable values, which you can use to do nifty effects and animations.
Well, I think that you can now answer the question of how long it will take to achieve post-production
rendering results in real-time rendering engines. These are wonderful days for the graphics industry,
and it would be a real shame for us if we don’t take these powerful capabilities to the limits to aid
in our daily work. Until a next time then… See ya!