Shader Tutorial 1: Create a Vertex Shader
Microsoft DirectX 9.0 SDK Update (October 2004)

Shader Tutorial 1: Create a Vertex Shader


This tutorial creates a single vertex shader that implements standard lighting and transforms. The shader is compiled and rendered with the Effect system. To understand this tutorial, you should be familiar with writing C functions, declaring HLSL variables (see Variable Declaration Syntax), declaring HLSL functions (see Function Declaration Syntax), and HLSL vector and matrix math (see HLSL Implements Per Component Math Operations).

Duration

This tutorial can take as few as 10-15 minutes if you are familiar with the prerequisites. Otherwise, you may want to start by reading about HLSL data types and matrix math before implementing this vertex shader.

Step 1: Identify Shader Inputs

Description

Declare a vertex shader function that:

Solution

void VertexShader1(
    float3 InPos  : POSITION, 
    float3 InNormal : NORMAL 
    )
{
  ...
}

The shader is named VertexShader1, which is an arbitrary function name. At this point, the function has no body so it returns nothing. The function will take two inputs, each of which is a three component vector containing x,y,z data. Both inputs have semantics attached to them (the string after the semicolon - see Varying Shader Inputs and Semantics). Semantics attach pipeline data to shader inputs. In this function the two input semantics specify that the shader inputs are position and normal data.

Step 2: Identify Shader Outputs

Description

Implement a return type that contains three outputs:

Solution

struct VS_OUTPUT
{
    float4 Position  : POSITION;
    float4 Diffuse   : COLOR0;
    float4 Specular  : COLOR1;
};

In C, a function returns a single type. HLSL functions can return more than one type by declaring a structure and using it for the return type of the function. This structure contains three members: one for position, one for diffuse color and one for specular color. Each member is a four component type. You can think of the position data as x,y,z,w data, and the color data as r,g,b,a data. Each member also has a semantic attached so that the vertex shader outputs can be attached to the next pipeline stage (primitive processing), or a pixel shader. Vertex shader output semantics are defined in Shader Semantic Syntax.

Step 3: Implement The Position Transform

Description

Implement the following in the shader body:

Solution

VS_OUTPUT VS(
    float3 InPos  : POSITION, 
    float3 InNormal : NORMAL 
    )
{
    VS_OUTPUT Out = (VS_OUTPUT)0;

    // Transform the position
    float3 Pos = mul(float4(InPos, 1), World);         

    Out.Pos  = mul(float4(Pos, 1), ViewProjection);

    return Out;
}

The position data is in model space (object space) when the shader runs. It needs to be transformed into projection space. This shader implements the transform in two steps: a transform from model space to world space followed by a transform from world space to projection space.

Each transform is done with a matrix multiply that combines a 1x4 vector and a 4x3 matrix. In the code, notice the:

    float4(InPos, 1);         

This is a float4 type constuctor. The constructor which initializes the first three values to the x,y,z values from InPos, and sets the fourth component to a 1. This constructor converts a 1x3 vector to a 1x4 vector to prepare for the matrix multiply. The matrix multiply returns a 1x3 result.

Every vertex shader must at a minimum, implement a position transform. Anything else you implement in a vertex shader is optional.

Step 4: Implement Ambient, Diffuse and Specular Lighting

Description

Implement calculations for ambient, diffuse, specular lighting equations that take into account:

Solution

VS_OUTPUT VS(
    float3 InPos  : POSITION, 
    float3 InNormal : NORMAL 
    )
{
    VS_OUTPUT Out = (VS_OUTPUT)0;

    float3 Normal = normalize(mul(InNormal, (float3x3)World));   
    float3 EyeToVertex = normalize(Pos - CameraPos);
    
    float4 Amb  = CalcAmbient();
    float4 Diff = CalcDiffuse(Normal, -DirFromLight); 
    float4 Spec = CalcSpecular(Normal, DirFromLight, EyeToVertex);

    Out.Diff = Amb + Diff ; 
    Out.Spec = Spec; 
    
    return Out;
}

The lighting calculations for ambient, diffuse and specular lighting are done with three helper functions: CalcAmbient, CalcDiffuse, and CalcSpecular. Since some of these functions require a normal, and others require the view vector (EyeToVertex), these vectors are transformed into world space before the helper functions are called.

When the normal is calculated, it is tranformed with the mul function just like the position was transformed. Additionally, the normalize function is used to convert the result into a normalized vector.

The helper functions for each of the lighting components are shown next:

float4 CalcAmbient()
{
    return LightAmbientIntensity * MaterialAmbientIntensity; 
}

float4 CalcDiffuse(float3 Normal, float3 DirToLight)
{
    return MaterialDiffuseColor * LightDiffuseColor * max(0, dot(Normal, DirToLight));
}

float4 CalcSpecular(float3 Normal, float3 DirFromLight, float3 EyeToVertex)
{
    float3 R = normalize(reflect(DirFromLight, Normal));
    return MaterialSpecularColor * LightSpecularColor * pow(max(0, dot(R, -EyeToVertex)), MaterialSpecularPower/4);
}

The ambient helper simply combines the ambient light intensity with the material ambient intensity. Each function returns a four component value which can be thought of as r,g,b,a data. The diffuse helper uses the dot function to calculate the N dot L dot, which is combined with the light and the material color. The diffuse helper also uses the max function to clamp the result. This is because a negative result is light that occurs on the backside of an object. By clamping the results to only be positive, we do not waste time calculating light we cannot see.

The specular helper uses the pow function to calculate the specular highlights. This requires a reflection vector which is calculated first. Then the helper function combines the specular calculation with the light and the material color. The specular power factor is divided by 4, which is a number that you can modify to adjust the size of your specular highlights.

Step 5: Setup Default Values for the Uniform Shader Variables

Description

Implement default values for the following shader constants:

Solution

float3 DirFromLight < string UIDirectional = "Light Direction"; > = 
  {0.577, -0.577, 0.577};

float4 LightAmbientIntensity  = { 0.8f, 0.8f, 0.8f, 1.0f };    
float4 LightDiffuseColor      = { 1.0f, 0.9f, 0.8f, 1.0f };    
float4 LightSpecularColor     = { 1.0f, 1.0f, 1.0f, 1.0f };    

float4 MaterialAmbientIntensity = { 0.5f, 0.5f, 0.5f, 1.0f };    
float4 MaterialDiffuseColor     = { 0.4f, 0.4f, 0.4f, 1.0f };    
float4 MaterialSpecularColor    = { 0.1f, 0.1f, 0.1f, 1.0f };    
int    MaterialSpecularPower    = 32;                            

float4x3 World          : WORLD;
float4x3 View           : VIEW;
float4x4 ViewProjection : VIEWPROJECTION;
float3 CameraPos        : CAMERAPOSITION;    

Uniform shader variables are variables that are set once for each invocation of the shader. Every vertex processed by the shader sees the same value for each usiform shader constant.

In this tutorial, each constant is declared like a global variable in an effect file. Some variables have default values explicitly declared in the effect file. Those that don't will need to be initialized by the application before the shader is rendered.

This is an example of a variable, that is a uniform constant, which contains an explicit declaration:

float3 DirFromLight < string UIDirectional = "Light Direction"; > = 
  {0.577, -0.577, 0.577};

The DirFromLight will be initialized to the values shown here when the application calls Effect->SetDefaults. This variable uses an annotation which is the declaration inside of the angle brackets. The annotation can be used by the application to find out more about the parameter DirFromLight.

This variable contains explicit default values only. It is also initialized when calling Effect->SetDefaults.

float4 LightAmbientIntensity  = { 0.8f, 0.8f, 0.8f, 1.0f };    

This variable contains explicit default values only. It is also initialized when calling Effect->SetDefaults.

This uniform shader constant has no explicit values declared, but does have a semantic attached to it:

float4x3 World          : WORLD;

Since there are no values, the matrix will need to be initialized by calling Effect->SetMatrix in the application. The semantic attached means that this variable is automatically tied to the world transform in the pipeline. Semantics are used to attach shader inputs and outputs to each other or to the pipeline.

Step 6: Implement the Effect Technique

Description

Implement a technique that:

Solution

technique TransformAndLitVertices
{
    pass P0
    {
        VertexShader = compile vs_2_0 VertexShader1();
    }
}

An effect contains one of more techniques. Each technique contains one or more passes. Each pass contains the state that uniquely identifies how to render a particular effect.

In this case, the technique implements one pass which contains a single shader. The shader contains the minimum state necessay to implement an effect that performs transformation and lighting. The pass contains a single compile statement that identifies the compile target and the name of the shader function to compile. When the shader is successfully compiled, a VertexShader object is generated.

Step 7: Compile The Effect

Description

Implement calculations for ambient, diffuse, specular lighting equations that take into account:

Solution

ID3DXEffect*            g_pEffect = NULL;       // D3DX effect interface

    // Read the D3DX effect file
    WCHAR str[MAX_PATH];
    V_RETURN( DXUTFindDXSDKMediaFileCch( str, MAX_PATH, L"BasicHLSL.fx" ) );

    DWORD dwShaderFlags = 0;
    #ifdef DEBUG_VS
        dwShaderFlags |= D3DXSHADER_FORCE_VS_SOFTWARE_NOOPT;
    #endif
    #ifdef DEBUG_PS
        dwShaderFlags |= D3DXSHADER_FORCE_PS_SOFTWARE_NOOPT;
    #endif

    // Preshaders are parts of the shader that the effect system pulls out of the 
    // shader and runs on the host CPU. They should be used if you are GPU limited. 
    // The D3DXSHADER_NO_PRESHADER flag disables preshaders.
    if( !g_bEnablePreshader )
        dwShaderFlags |= D3DXSHADER_NO_PRESHADER;

    // Read the D3DX effect file
    WCHAR str[MAX_PATH];
    V_RETURN( DXUTFindDXSDKMediaFileCch( str, MAX_PATH, L"BasicHLSL.fx" ) );

    // If this fails, there should be debug output as to 
    // why the .fx file failed to compile
    V_RETURN( D3DXCreateEffectFromFile( pd3dDevice, str, NULL, NULL, dwShaderFlags, NULL, &g_pEffect, NULL ) );

Step 8: Render The Shader

Description

Implement calculations for ambient, diffuse, specular lighting equations that take into account:

Solution

IDirect3DTexture9*      g_pMeshTexture = NULL;  // Mesh texture

    V_RETURN( D3DXCreateTextureFromFileEx( pd3dDevice, str, D3DX_DEFAULT, D3DX_DEFAULT, 
                                       D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, 
                                       D3DX_DEFAULT, D3DX_DEFAULT, 0, 
                                       NULL, NULL, &g_pMeshTexture ) );

    // Set effect variables as needed
    D3DXCOLOR colorMtrlDiffuse(1.0f, 1.0f, 1.0f, 1.0f);
    D3DXCOLOR colorMtrlAmbient(0.35f, 0.35f, 0.35f, 0);

    V_RETURN( g_pEffect->SetValue("g_MaterialAmbientColor", &colorMtrlAmbient, sizeof(D3DXCOLOR) ) );
    V_RETURN( g_pEffect->SetValue("g_MaterialDiffuseColor", &colorMtrlDiffuse, sizeof(D3DXCOLOR) ) );    
    V_RETURN( g_pEffect->SetTexture( "g_MeshTexture", g_pMeshTexture) );
void CALLBACK OnFrameRender( IDirect3DDevice9* pd3dDevice, double fTime, float fElapsedTime )
{
    HRESULT hr;
    D3DXMATRIXA16 mWorldViewProjection;
    D3DXVECTOR3 vLightDir[MAX_LIGHTS];
    D3DXCOLOR   vLightDiffuse[MAX_LIGHTS];
    UINT iPass, cPasses;
    D3DXMATRIXA16 mWorld;
    D3DXMATRIXA16 mView;
    D3DXMATRIXA16 mProj;
   
    // Clear the render target and the zbuffer 
    V( pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DXCOLOR(0.0f,0.25f,0.25f,0.55f), 1.0f, 0) );

    // Render the scene
    if( SUCCEEDED( pd3dDevice->BeginScene() ) )
    {
        // Get the projection & view matrix from the camera class
        mWorld = *g_Camera.GetWorldMatrix();       
        mProj = *g_Camera.GetProjMatrix();       
        mView = *g_Camera.GetViewMatrix();

        mWorldViewProjection = mWorld * mView * mProj;

        // Render the light spheres so the user can 
        // visually see the light dir
        for( int i=0; iSetValue( "g_LightDir", &vLightDir, sizeof(D3DXVECTOR3)*MAX_LIGHTS ) );
        V( g_pEffect->SetValue( "g_LightDiffuse", &vLightDiffuse, sizeof(D3DXVECTOR4)*MAX_LIGHTS ) );

        // Update the effect's variables.  Instead of using strings, it would 
        // be more efficient to cache a handle to the parameter by calling 
        // ID3DXEffect::GetParameterByName
        V( g_pEffect->SetMatrix( "g_mWorldViewProjection", &mWorldViewProjection ) );
        V( g_pEffect->SetMatrix( "g_mWorld", &mWorld ) );
        V( g_pEffect->SetFloat( "g_fTime", (float)fTime ) );

        D3DXCOLOR vWhite = D3DXCOLOR(1,1,1,1);
        V( g_pEffect->SetValue("g_MaterialDiffuseColor", &vWhite, sizeof(D3DXCOLOR) ) );
        V( g_pEffect->SetFloat( "g_fTime", (float)fTime ) );      
        V( g_pEffect->SetInt( "g_nNumLights", g_nNumActiveLights ) );      

        // Render the scene with this technique 
        // as defined in the .fx file
        switch( g_nNumActiveLights )
        {
            case 1: V( g_pEffect->SetTechnique( "RenderSceneWithTexture1Light" ) ); break;
            case 2: V( g_pEffect->SetTechnique( "RenderSceneWithTexture2Light" ) ); break;
            case 3: V( g_pEffect->SetTechnique( "RenderSceneWithTexture3Light" ) ); break;
        }


        // Apply the technique contained in the effect 
        V( g_pEffect->Begin(&cPasses, 0) );

        for (iPass = 0; iPass < cPasses; iPass++)
        {
            V( g_pEffect->BeginPass(iPass) );

            // The effect interface queues up the changes and performs them 
            // with the CommitChanges call. You do not need to call CommitChanges if 
            // you are not setting any parameters between the BeginPass and EndPass.
            // V( g_pEffect->CommitChanges() );

            // Render the mesh with the applied technique
            V( g_pMesh->DrawSubset(0) );

            V( g_pEffect->EndPass() );
        }
        V( g_pEffect->End() );

        g_HUD.OnRender( fElapsedTime ); 
        g_SampleUI.OnRender( fElapsedTime );

        RenderText( fTime );
        
        V( pd3dDevice->EndScene() );
    }
}


© 2004 Microsoft Corporation. All rights reserved.
Feedback? Please provide us with your comments on this topic.
For more help, visit the DirectX Developer Center.