Shader4 - Apply a texture map with lighting

DirectX8

Microsoft DirectX 8.1 (version 1.0, 1.1)

Shader4 - Apply a texture map with lighting

This example uses a vertex shader to apply a texture map and add lighting to the scene. The object used is a sphere. The sample code applies a texture map of the earth to the sphere and applies diffuse lighting to simulate night and day.

The code sample adds to the Shader3 example by adding lighting to a texture mapped object. Refer to Shader3 for information about loading the texture map and setting up the texture stage states.

There is a detailed explanation of the sample code framework at Sample Framework. You can cut and paste the sample code into the sample framework to quickly get a working sample.

Create Vertex Shader

The vertex data has been modified from the Shader3 sample to include vertex normals. For lighting to appear, the object must have vertex normals. The data structure for the vertex data and the flexible vertex format (FVF) macro used to declare the data type are shown below.

struct CUSTOMVERTEX_POS_NORM_COLOR1_TEX1
{
    float       x, y, z;        // position
    float       nx, ny, nz;     // normal
    DWORD       color1;         // diffuse color
    float       tu1, tv1;       // texture coordinates
};
#define D3DFVF_CUSTOMVERTEX_POS_NORM_COLOR1_TEX1 (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_DIFFUSE|D3DFVF_TEX1)

A shader declaration defines the input vertex registers and the data associated with them. This matches the FVF macro used to create the vertex buffer data later.

DWORD dwDecl[] =
{
    D3DVSD_STREAM(0),
    D3DVSD_REG(0,  D3DVSDT_FLOAT3),      // position
    D3DVSD_REG(4,  D3DVSDT_FLOAT3),      // normal
    D3DVSD_REG(7,  D3DVSDT_D3DCOLOR),    // diffuse color
    D3DVSD_REG(8,  D3DVSDT_FLOAT2),      // texture coordinate
    D3DVSD_END()
};

This declares one stream of data, which contains the vertex position, normal, diffuse color, and texture coordinates. The integer in each line is the register number that will contain the data. So, the texture coordinate data will be in register v8, for instance.

Next, create the shader file. You can create a shader from an ASCII text string or load it from a shader file that contains the same instructions. This example uses a shader file.

// Shader file
// v7  vertex diffuse color used for the light color
// v8  texture 
// c4  view projection matrix
// c12 light direction
vs.1.0                       // version instruction
m4x4 oPos,    v0,    c4      // transform vertices using view projection transform
dp3  r0     , v4   , c12     // perform lighting N dot L calculation in world space        
mul  oD0    , r0.x , v7      // calculate final pixel color from light intensity and
                             // interpolated diffuse vertex color 
mov  oT0.xy , v8             // copy texture coordinates to output   

You always enter the version instruction first. The last instruction moves the texture data to the output register oT0. After you write the shader instructions, you can use them to create the shader.

// Now that the file exists, use it to create a shader.
TCHAR        strVertexShaderPath[512];
LPD3DXBUFFER pCode;
DXUtil_FindMediaFile( strVertexShaderPath, _T("VShader3.vsh") );
D3DXAssembleShaderFromFile( strVertexShaderPath, 0, NULL, &pCode, NULL );
m_pd3dDevice->CreateVertexShader( dwDecl, (DWORD*)pCode->GetBufferPointer(), &m_hVertexShader, 0 );
pCode->Release();

After the file is located, Direct3D creates the vertex shader and returns a shader handle and the assembled shader code. This sample uses a shader file, which is one method for creating a shader. The other method is to create an ASCII text string with the shader instructions in it. For an example, see Programmable Shaders for DirectX 8.0.

Vertex Shader Constants

You can define vertex shader constants outside of the shader file as shown in the following example. Here, you use constants to provide the shader with a view/projection matrix, a diffuse light color, RGBA, and the light direction vector.

float constants[4] = {0, 0.5f, 1.0f, 2.0f};
m_pd3dDevice->SetVertexShaderConstant( 0, &constants;, 1 );

D3DXMATRIX mat;
D3DXMatrixMultiply( &mat;, &m;_matView, &m;_matProj );
D3DXMatrixTranspose( &mat;, &mat; );
m_pd3dDevice->SetVertexShaderConstant( 4, &mat;, 4 );

float color[4] = {1,1,1,1};
m_pd3dDevice->SetVertexShaderConstant( 8, &color;, 1 );

float lightDir[4] = {-1,0,1,0}; // fatter slice
m_pd3dDevice->SetVertexShaderConstant( 12, &lightDir;, 1 );

You can also define constants inside a shader using the def instruction

Render

After you write the shader instructions, connect the vertex data to the correct vertex registers and initialize the constants, you should render the output. Rendering code tells Direct3D where to find the vertex buffer data stream and provides Direct3D with the shader handle. Because you use a texture, you must set texture stages to tell Direct3D how to use the texture data.

// Identify the vertex buffer data source.
m_pd3dDevice->SetStreamSource(0, m_pVB, sizeof(CUSTOMVERTEX_POS_NORM_COLOR1_TEX1));
// Identify the shader.
m_pd3dDevice->SetVertexShader( m_hVertexShader );

// Define the texture stage(s) and set the texture(s) used
m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP,   D3DTOP_MODULATE );
m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
m_pd3dDevice->SetTexture( 0, m_pTexture0 );

// Draw the object.
DWORD dwNumSphereVerts = 2 * m_dwNumSphereRings*(m_dwNumSphereSegments + 1);
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, dwNumSphereVerts - 2);

// Set the texture stage to NULL after the render commands. Leaving this
// out will cause a memory leak.
m_pd3dDevice->SetTexture( 0, NULL );

The output image follows:

With the texture map applied, the sphere looks like the planet Earth. The lighting creates a bright to dark gradient on the face of the globe.

Additional Code

There is additional code required to support this example. Shown below are a few of the other methods for creating the sphere object, loading the texture, and checking for the correct version of pixel shader support.

// Confirm that the hardware supports version 1 shader instructions.
if( D3DSHADER_VERSION_MAJOR( pCaps->VertexShaderVersion ) < 1 )
    return E_FAIL;

// Load texture map for the sphere object.
LPDIRECT3DTEXTURE8 m_pTexture0;
D3DUtil_CreateTexture( m_pd3dDevice, _T("earth.bmp"), &m;_pTexture0, D3DFMT_R5G6B5 );

// Create the sphere object.
DWORD dwNumSphereVerts = 2*m_dwNumSphereRings*(m_dwNumSphereSegments+1);
// once for the top, once for the bottom vertices

// Get the World-View(WV) matrix set.
D3DXMATRIX matWorld, matView, matWorldView;
m_pd3dDevice->GetTransform( D3DTS_WORLD, &matWorld; );
m_pd3dDevice->GetTransform( D3DTS_VIEW,  &matView; );
D3DXMatrixMultiply( &matWorldView;, &matWorld;, &matView; );
m_pd3dDevice->CreateVertexBuffer(
    dwNumSphereVerts*sizeof(CUSTOMVERTEX_POS_NORM_COLOR1_TEX1),
    D3DUSAGE_WRITEONLY, 
    D3DFVF_CUSTOMVERTEX_POS_NORM_COLOR1_TEX1,
    D3DPOOL_DEFAULT, 
    &m;_pVB)	);


CUSTOMVERTEX_POS_NORM_COLOR1_TEX1* pVertices;
HRESULT hr;
hr = m_pVB->Lock(0, dwNumSphereVerts*sizeof(pVertices), 
					(BYTE**)&pVertices;, 0);
if(SUCCEEDED(hr))
{
    FLOAT fDeltaRingAngle = ( D3DX_PI / m_dwNumSphereRings );
    FLOAT fDeltaSegAngle  = ( 2.0f * D3DX_PI / m_dwNumSphereSegments );

    // Generate the group of rings for the sphere.
    for( DWORD ring = 0; ring < m_dwNumSphereRings; ring++ )
    {
        FLOAT r0 = sinf( (ring+0) * fDeltaRingAngle );
        FLOAT r1 = sinf( (ring+1) * fDeltaRingAngle );
        FLOAT y0 = cosf( (ring+0) * fDeltaRingAngle );
        FLOAT y1 = cosf( (ring+1) * fDeltaRingAngle );

        // Generate the group of segments for the current ring.
        for( DWORD seg = 0; seg < (m_dwNumSphereSegments+1); seg++ )
        {
            FLOAT x0 =  r0 * sinf( seg * fDeltaSegAngle );
            FLOAT z0 =  r0 * cosf( seg * fDeltaSegAngle );
            FLOAT x1 =  r1 * sinf( seg * fDeltaSegAngle );
            FLOAT z1 =  r1 * cosf( seg * fDeltaSegAngle );

            // Add two vertices to the strip, which makes up the sphere
            // (using the transformed normal to generate texture coords).
            pVertices->x = x0;
            pVertices->y = y0;
            pVertices->z = z0;
            pVertices->nx = x0;
            pVertices->ny = y0;
            pVertices->nz = z0;
            pVertices->color = HIGH_WHITE_COLOR;
            pVertices->tu = -((FLOAT)seg)/m_dwNumSphereSegments;
            pVertices->tv = (ring+0)/(FLOAT)m_dwNumSphereRings;
            pVertices++;

            pVertices->x = x1;
            pVertices->y = y1;
            pVertices->z = z1;
            pVertices->nx = x1;
            pVertices->ny = y1;
            pVertices->nz = z1;
            pVertices->color = HIGH_WHITE_COLOR;
            pVertices->tu = -((FLOAT)seg)/m_dwNumSphereSegments;
            pVertices->tv = (ring+1)/(FLOAT)m_dwNumSphereRings;
            pVertices++;
        }

        hr = m_pVB->Unlock();
    }
}