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 XSI Samples database that is installed with Softimage. You can find this database in the \Data\XSI_SAMPLES subdirectory of the installation path.

Per-Pixel Phong Lighting

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 > More... and select the DX Draw node from the browser window.

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 Softimage. 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.

 



Autodesk Softimage v.7.5