HLSL Programmable Shader Example: Per-Pixel Phong Lighting
In this example, you will to use a combination of vertex shaders, pixel shaders, 2D textures and cubic textures to create per-pixel lit Phong shading.
About Textures
All of the textures used in this example are available from the SOFTIMAGE|XSI Samples database that is installed with XSI. You can find this database in the \Data\XSI_SAMPLES subdirectory of the installation path.
The goal is to create a pixel shader that takes the terms of the lighting equation and applies the equation on a per-pixel basis. In order to do so, you will need the following data:
• The normal at the pixel. This is provided in one of the texture coordinates, which is set by the vertex shader.
• The color at the pixel:
- The ambient color at the pixel is provided as the primary color and is set by the vertex shader.
- The diffuse and specular colors at the pixel are provided as texture coordinates and are set by the vertex shader.
• The light direction at the pixel. This is provided in one of the texture coordinates, which is set by the vertex shader.
• The half vector at the pixel. This is also provided in one of the texture coordinates, which is also set by the vertex shader.
The final lighting equation is as follows:
Diffuse = normal . light direction
Specular = (normal . half vector) ^ some exponent
Final color = (Diffuse * color) + specular
Since the data required by the pixel shader is not provided by the fixed function pipeline, you need to create a vertex shader that will provide it. The vertex shader will be responsible for:
• Generating the position of the vertex in screen space.
• Passing through the texture coordinates required for the normal and color maps.
• Computing the light direction and storing it in one of the texture coordinates.
• Computing the half vector and storing it in one of the texture coordinates.
Normalizing the Light Direction and Half Vector
The vertex shader previously described provides the necessary data for the pixel shader, but there’s one problem: the light direction and the half vector will come in the pixel shader unnormalized, meaning that they won’t have unit length. This will cause artifacts in the final lighting equation.
There are two ways to solve this problem:
• Normalize the incoming light direction and half vector in the pixel shader. This consumes more instructions but is more accurate.
or
• Use a special cubic texture map called a normalization map. This consumes only one pixel shader instruction, but it also consumes one texture target, and is less accurate.
For this example, you are going to use a normalization map in order for the example to work with all the supported profiles.
Creating Per-Pixel Phong Lighting
The time has come to create Phong Lighting.
To prepare the scene
1. Set any viewport’s Views menu, choose Realtime Shaders > DirectX9. This allows you to view the realtime shader effect you’re about to create.
2. The effect you’re about to create only works with a single infinite light. If necessary, choose Get > Primitive > Light > Infinite from the Render toolbar to get an infinite light.
Position the infinite light as desired and then delete all of the other lights in the scene.
3. Choose Get > Primitive > Sphere from any toolbar.
4. With the sphere selected, choose Get > Material > Phong from the Render toolbar.
5. Press 7 to open the object’s render tree.
6. Select and delete the Phong node.
To draw the object
7. From the render tree menu, choose Nodes
> Realtime DirectX >
DX Draw.
8. Connect the DX Draw node to the Material node’s Realtime port.
To create the vertex shader setup
9. From the render tree menu, choose Nodes
> Realtime DirectX >
DX Program (HLSL).
10. Double-click the DX Program node to open its property editor and do the following:
- Set the Profile to one of the vertex program profiles.
- Enter the following code in the DX Program window:
struct v2f
{
float4 pos : POSITION;
float3 normal : TEXCOORD0;
float3 lightdir : TEXCOORD1;
float3 halfvector : TEXCOORD2;
float3 ambient : COLOR0;
float3 diffuse : COLOR1;
float3 specular : TEXCOORD3;
};
v2f main
(
float4 pos : POSITION,
float4 nrm : NORMAL,
uniform float4x4 simodelviewproj,
uniform float4x4 simodelviewIT,
uniform float4x4 simodelview,
uniform float4 silightdirection_0,
uniform float Ambient_red,
uniform float Ambient_green,
uniform float Ambient_blue,
uniform float Diffuse_red,
uniform float Diffuse_green,
uniform float Diffuse_blue,
uniform float Specular_red,
uniform float Specular_green,
uniform float Specular_blue
)
{
v2f output;
output.pos = mul(simodelviewproj, pos);
output.normal = normalize(mul(simodelviewIT, nrm).xyz);
float3 v_pos = normalize(mul(simodelview, pos));
output.lightdir = normalize(silightdirection_0);
output.halfvector = normalize(v_pos - output.lightdir);
output.normal.y = -output.normal.y;
output.ambient = float4(Ambient_red, Ambient_green, Ambient_blue,1);
output.diffuse = float4(Diffuse_red, Diffuse_green, Diffuse_blue,1);
output.specular = float4(Specular_red, Specular_green, Specular_blue,1);
return output;
}
11. Once you’ve written the code, you can set the Build option to Compile and Execute.
To create the pixel shader setup
12. From the render tree menu, choose Nodes
> Realtime DirectX >
DX Program (HLSL).
13. Double-click the new DX Program node to open its property editor and do the following:
- Set the Profile to one of the pixel program profiles.
- Enter the following code in the DX Program window:
float4 main
(
float4 pos : POSITION,
float3 normal : TEXCOORD0,
float3 lightdir : TEXCOORD1,
float3 halfvector : TEXCOORD2,
float4 ambientCol : COLOR0,
float4 diffuseCol : COLOR1,
float3 specularCol : TEXCOORD3,
uniform sampler2D specularPowers,
uniform samplerCUBE normalizationMap1,
uniform samplerCUBE normalizationMap2,
uniform float Specular_decay
) : COLOR
{
float4 output;
float4 nrml = (texCUBE(normalizationMap1, normal) - 0.5)*2;
float4 nrml2 = (texCUBE(normalizationMap2, normal) - 0.5)*2;
float2 specCompute;
specCompute.x = -dot(halfvector , nrml);
specCompute.y = Specular_decay;
float4 specular = tex2D(specularPowers, specCompute);
float diffuse = dot(lightdir, nrml2);
output = ambientCol + (diffuse * diffuseCol) + (specular * specularCol.xyzz);
return output;
}
Without going into too much detail, here are a few observations about how the DX program shader uses this code:
- The specular power function is stored in a texture which is set in target 0.
- The normalization map should be set in targets 1 and 2
- Normals stored as colors are halved and biased, you need to unpack these normals properly, like this: texCUBE(normalizationMap2, normal) - 0.5)*2
- Normalizing a vector with a normalization map is easy. Simply sample the normalization map with U, V, and W coordinates and the returned color will be the normalized vector of (U,V,W).
14. Once you’ve written the code, you can set the Build option to Compile and Execute.
To set up the required textures
Earlier, you saw that the pixel shader code requires three textures. In the following steps, you’ll set up these textures.
15. From the render tree’s Nodes > Realtime DirectX menu, get the following shaders:
- Two DX Cubic Texture shaders
- A DX Texture shader
- A DX Texture Transform shader
16. Connect the shaders together as follows:
- Connect the first DX Program shader (the one where you set up the vertex program) to the second DX Program Shader’s (the one where you set up the pixel program) previous input.
- Connect the second DX Program Shader node to the DX Texture node’s previous port.
- Connect the DX Texture node to the DX Texture Transform node’s previous port.
- Connect the DX Texture Transform node to the first DX Cubic Texture node’s previous port.
- Connect the first DX Cubic Texture node to the second DX Cubic Texture node’s previous port.
- Connect the second DX Cubic Texture node to the DX Draw node’s previous port.
The render tree should now look something like this:
17. Set the properties of the DX Cubic Texture shaders as follows:
- Set the first DX Cubic Texture shader’s target to 1.
- Set the second DX Cubic Texture shader’s target to 2.
- Set the texture images for each face of the cubic projection. Both shaders should use the same six images.
|
The images used in this example are available from the samples database that is installed with XSI. You can find this database in the \Data\XSI_SAMPLES subdirectory of the installation path. |
18. Set the properties of the DX Texture shader as follows:
- Set the target to 0.
- Set the texture image to the specular power function image, which is available from the samples database.
19. Set the DX Texture Transform node’s target to 0 so that it applies to the DX Texture node.
The final effect should look something like this.
SOFTIMAGE|XSI v.6.01