This is kinda a tutorial snippet.
We’ve just released version 1.09 of Forester Pro an easy to use tree creator that exports to X and DBO format for Dark Basic. One of the features of Forester Pro is it can export tree models with either single or double-sided leaf meshes. Models with double-sided meshes can be used in DBPro just by loading them with the load object command. Models with single-sided meshes, which have a lower polycount and thus better performance, need to use a shader to display both sides of the leaf meshes in Dark Basic.
Since shaders are liable to make you run for the hills we thought it would be a good idea to produce a tutorial/snippet on how a double-sided shader works, and how it is implemented in Dark Basic. Once you have a good understanding of shaders it is amazing what you can do with Dark Basic. Shaders are nothing to be frightened of…..well actually….a modicum of fear is inherent…but they aren’t as difficult as they first appear. Hope this is the right place for an easy level shader tutorial.
What is a Shader
Okay, but some people won't know. A shader is a little program that runs on the graphics card (usually) and “beautifies” your game application by changing the appearance of models by adding effects. In Dark Basic they are referred to as effects in code and can do spectacular things such as make a flat plane look like the surface of the ocean, make a simple sphere glow like the sun, or generate mirror-like reflections. Shaders, however, can do some much more down to earth things, such as apply lighting to models and turn single sided planes into double-sided planes. Shaders are magic and well worth learning.
How to Apply a Shader to an Object in Dark Basic
Shaders are little programs in fx files that can be applied to objects in Dark Basic. To load a shader as effect no 1 and apply it to an object we simply use the following code. Depending on the shader you may also need to ensure the model is in the right format.
Load effect “shaders/ambientdiffuse.fx”, 1, 0
Set object effect obj, 1
Shaders and HLSL
For Dark Basic shaders are written in High Level Shader Language, and initially will look very scary. All the code in the shader is contained in the fx file. Lets start with the header of our double-side plane shader, the header is by far the least scary part.
float4x4 wvp : WorldViewProjection;
float4 AmbientColor = {1.0f, 1.0f, 1.0f, 1.0f};
float AmbientIntensity = 0.0;
float4x4 wit : WorldInverseTranspose;
float3 DiffuseLightDirection = {0.0f, 1.0f, 0.0f};
float4 DiffuseColor = float4(1, 1, 1, 1);
float DiffuseIntensity = 1.0;
This is just a list of variable declarations, just like the global and dim statements we use to declare variables in Dark Basic. These all define matrices of data (tables like arrays) so a float4 is a matrix with four pieces of data all of which have decimal points (are floating point data), for example, the colour of the ambient light the shader will apply to our model is contain in a 4 by 1 matrix and contains four floating point numbers, whilst the direction of that light is contained in a 3 by 1 matrix as a unit vector. If I’ve already scared you by mentioning the word vector, then sorry…think of them as directions given in the form, left a bit, up a bit, forward a bit.
There are some strange definitions here too, like float4x4 wvp:WorldViewProjection. This is a variable wvp which is a 4 by 4 matrix and stores data that allows us to convert coordinates from world to view (camera) coordinates.
The important thing to understand about variable declarations in this header is we can over-ride them in our Dark Basic code, for example we can use the following command to change the diffuse light intensity.
Set effect constant float 1, “DiffuseIntensity”, 0.8
The next part of our shader will get the texture data that we’ve applied to our model using the Dark Basic texture object command. This defines a variable called detailMap1 that we can use to access the texture data when calculating how light effects the texture.
texture detailMap1 < string ResourceName = ""; >;
sampler2D textureSampler = sampler_state {
Texture = <detailMap1>; //Get the texture from the 0 stage of the model
MagFilter = Linear;
MinFilter = Linear;
AddressU = Wrap; //Use repeating boundary for UV coordinates
AddressV = Wrap;
};
The rest of the shader will do the number crunching to calculate how the texture will be lit on the model. The number crunching occurs in two parts, first the shader will do some calculations based on the shape of the model, from the positions of vertices and the orientations of faces (i.e. the normal) – this part of the shader is known as the vertex shader, then the output from the vertex shader’s calculations will be used to calculate the colour of each pixel on the screen and, if necessary combine that colour with the colour of the background. This part of the shader is known as the pixel shader. In other words, first the shader deals with shapes, the second gets the colour of screen pixels from that shape.
This is the vertex shader for our double-sided tree shader. It looks intimidating but we will take it apart piece by piece.
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
//simply pass the input texture to the output
output.TextureCoordinate = input.TextureCoordinate;
//transform to view position
output.Position = mul(input.Position, wvp);
//get view centred normals
float4 innormal = input.Normal;
float4 normalV = mul(input.Normal, wvp);
if (normalV.z < 0){
innormal = -1.0*innormal;
//DiffuseIntensity = 1.0;
}
//transform normals to object centred
float4 normal = mul(innormal, wit);
float lightIntensity = dot(normal, DiffuseLightDirection);
//calculate diffuse colour from light intensity
output.Color = DiffuseColor * DiffuseIntensity * lightIntensity;
return output;
}
First things first. We need to know how to pass data into and out of the vertex shader. This is one of the annoying things about HLSL, everything need to be declared. The input definition is:
struct VertexShaderInput
{
float4 Position : POSITION0;
float4 Normal : NORMAL0;
float2 TextureCoordinate : TEXCOORD0;
};
So we are passing in the positions of vertexes in a variable called Position, the normal and the texture coordinates.
The output definition looks like this (it is also the input to the pixel shader).
struct VertexShaderOutput
{
float4 Position : POSITION0;
float4 Color : COLOR0;
float2 TextureCoordinate : TEXCOORD1;
};
So we are passing out positions, colours and a new set of texture coordinates. Anyway lets get stuck into the vertex shader. Just looking at my nice comments in the code you should be able to work out a lot about what it does. Notice how it uses the matrix multiply command to convert the positions of vertexes in world coordinates into view coordinates (the coordinates of camera) which are then passed to the output using the simple notation of output.Position=mul(input.Position,wvp). Remember that wvp from the declaration? This is what it is for.
The vertex shader plays the same trick with normals, remember the normals of a face determines which way it is pointing. The vertex shader transforms the normal into view coordinates and then reverses any normal that is facing away from us (that has a negative z component). This means that any face, such as a leaf, that is facing away from us (from the camera) will be treated as if it is facing towards us. If you think about this for a second you can see this is the guts of the shader. Even though the planes are single sided, their normal are swapped if they are facing away from us, making them look like they have two sides!
The next part of the vertex shader calculates the intensity of light at any point on our object from the diffuse light direction and then gets the colour simply by multiplying the colour of the diffuse light, by the light intensity and the original intensity of the diffuse light. If you are wondering how this one line can do something quite so spectacular, then remember it is being run on every vertex and face in the model.
The pixel shader of our double-sided tree editor is actually uber simple:
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
//Get the texture colours for each pixel
float4 textureColor = tex2D(textureSampler, input.TextureCoordinate);
//combine diffuse and ambient light
float4 output1 = textureColor*(input.Color + AmbientColor*AmbientIntensity);
output1.a = textureColor.a;
return output1;
}
The first part simply gets the colour of the texture from the uv coordinates. It then combines the colours on the model produced by diffuse light (the directional light) and the ambient light (the background light) and multiplies them by the pixel colour from the texture, storing them in the variable output1. Since this is for a leaf we need the texture to have transparent areas as given by the alpha channel of the image. To ensure these areas are transparent the last line simply copies the alpha from the texture to the output alpha. This will only affect the transparency and not the colour of the pixel. Then finally we pass back the colour of all the pixels to the screen.
This isn’t quite the end of the shader, theres one more part that pulls together the vertex shader and the pixel shader and tells the code what to do with them. This technique is shown below:
technique Diffuse
{
pass Pass1
{
AlphaBlendEnable = TRUE;
CullMode = None;
DestBlend = INVSRCALPHA;
SrcBlend = ONE;
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
Shaders can have more than one technique, and we can change which one is used from code. They can also run several sets of vertex and pixel shaders one after another, hence the Pass1 notation. In this case there is only one technique and only one pass.
You’ll see that there are several assignments in the technique. Such as AlphaBlendEnable=TRUE; which makes sure the pixel colour is blended with the background if it is partially transparent. The CullMode is set to None, it could be CCW, which means show counter clockwise faces as forward facing, or CW which means show clockwise winding faces as forward facing. In our vertex shader we are controlling which faces we can see by changing their normal so we don’t want culling from preventing them from being visible.
Anyway I hope you can see that actually shaders are very useful and not as mind-blowingly complex as they appear at a first glance. The trick with shaders is not to get too confused at the weirdness. The best way to learn is to take an existing shader and change it just a little and see what it does.
You’ll find the full double-sided shader for trees in the shader directory of Forester Pro available from our website (there is a free version). It is called ambientdiffuse.fx. You are welcome to use this shader in your own projects. Trying changing it a little (but keep a copy) to see how it changes the appearance of trees in Forester Pro. If the tree goes black, you may have introduced a syntax error…the most common are wrong variable names (they are case sensitive), leaving out a semicolon at the end of the line, or using float3 instead of float4 or vice a versa. Dark Shader is an excellent way of tracking down errors in shaders, I thoroughly recommend it.
http://www.hptware.co.uk/forester.htm
Anyway…finally…your challenge if you should be willing to accept. Change the shader to:
(1) Make the colours on the model show the model’s normals rather than its diffuse lit texture.
(2) Make the entire tree half transparent.
(3) Make the tree move using the shader.
Make your suggestions on how to do this below (but not you GG, we know you can do it…beginners only).
GrumpyOne - the natural state of the programmer