Sunday, 8 July 2012

Basic HLSL Lighting Techniques - Episode 3: Colour, Glow, Bump and Reflective Texture Maps



I guess the next natural step is texturing a model. In this episode I am going to cover basic texturing, then go onto what I call glow maps, then bump mapping and then reflective textures. Now ALL the models and the textures in this episode are taken directly from the DirectX 9 SDK. The only thing I have altered is the sphere.x file as it has a reference to the diffuse (colour map) texture and as I like to store my textures else where and I am also not covering the content pipeline here, I have removed the reference. Also, this is my take on how the textures should be used, and I think it works quite well, guess you guys and girls will be the best judge of that though.

Colour Map





So, we have a model that we want to apply a texture to, in this case it's a sphere and the texture is a colour map of the earth. What you need to do is see the colour map as the resulting diffuse colour on the model, so we take the diffuse value on the model and multiply the texture colour by it to get the resulting colour required. As ever we require the World * View * Projection matrix, and the World matrix, I am also going to include ambient light here, but not pass it and just use the default values, but this will show how you can add the earlier lighting to this effect and as we are doing diffuse lighting we need a light direction again. Now comes the new bit, two new elements, a texture and a sampler. The texture is the parameter that will hold the texture we are passing to the shader, in this case the ColorMap. The sampler is what is used in the pixel shader to reference the texture and get the required pixel from it.

float4x4 wvp : WorldViewProjection;
float4x4 world : World;

float AmbientIntensity = 1;
float4 AmbientColor : AMBIENT = float4(0,0,0,1);

float3 LightDirection : Direction = float3(0,1,1);

texture ColorMap : Diffuse;
sampler ColorMapSampler = sampler_state
{
texture = <ColorMap>;
};


VS_IN has a new member added to it, TexCoord, now your model NEEDS to have these in them or you can not (to my knowledge) apply a texture to it correctly. The TexCoord is a float2 (Vector2) value at each vert that ranges from 0 to 1 and represents a position on the texture to be applied to the model. So if a vert had the value 0.5,0.5 for it's TexCoord then this would be a pixel at the very centre of the texture. The two values are also referred to as U and V, like X and Y but for textures, hence UV mapping. Our VS_OUT structure has the usual Position, Light and Normal, but again has a TexCoord added.

struct VS_IN
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
float3 Normal : NORMAL;
};
struct VS_OUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
float3 Light : TEXCOORD1;
float3 Normal : TEXCOORD2;
};
struct PS_OUT
{
float4 Color : COLOR;
};

In our vertex shader we do all the same things we did before for the diffuse lighting only we add the bit to pass on the TexCoord to the pixel shader.

VS_OUT VS_ColorMap(VS_IN input)
{
VS_OUT output = (VS_OUT)0;
output.Position = mul(input.Position,wvp);
output.Light = LightDirection;
output.TexCoord = input.TexCoord;
output.Normal = mul(input.Normal,world);
return output;
}

In our pixel shader we calculate the light direction and the amount of diffuse light as before as well as the ambient light, but we also get the current pixel for this given set of TexCoords from the sampler for this pixel and adjust it's colour by the diffused light we just calculated. This is then added to the ambient light value to give the pixel colour.

PS_OUT PS_ColorMap(VS_OUT input)
{
PS_OUT output = (PS_OUT)0;
float3 LightDir = normalize(input.Light);
float Diffuse = saturate(dot(LightDir,normalize(input.Normal)));
float4 texCol = tex2D(ColorMapSampler,input.TexCoord);
float4 Ambient = AmbientIntensity * AmbientColor;
texCol *= Diffuse;
output.Color = Ambient + texCol;
return output;
}





Glow Map





That was pretty painless eh... So is this. We add another texture and sampler to the shader so we can pass in the glow map texture.

texture GlowMap : Diffuse;
sampler GlowMapSampler = sampler_state
{
texture = <GlowMap>;
};

The structures stay the same and we just add the glow map calculation to the pixel shader. The glow map is effectively multiplying the glow map pixel by the opposite of the calculated diffuse, we then add this to the colour to be passed to the screen making the pixel shader look like this.

PS_OUT PS_ColorGlowMap(VS_OUT input)
{
PS_OUT output = (PS_OUT)0;
float3 LightDir = normalize(input.Light);
float Diffuse = saturate(dot(LightDir,normalize(input.Normal)));
float4 texCol = tex2D(ColorMapSampler,input.TexCoord);
float4 glowCol = tex2D(GlowMapSampler,input.TexCoord);
float4 Ambient = AmbientIntensity * AmbientColor;
float4 glow = glowCol * saturate(1-Diffuse);
texCol *= Diffuse;
output.Color = Ambient + texCol + glow;
return output;
}

This gives us the following effect:




Bump Map





Now to get bump mapping to work we have to do a few things to both the shader and the model (via the core content importer). First of all we need our model to have "Tangents". Tangents are required to generate the correct "tangent space" for the calculation of the effect, Wolfgang explains this much better than I could and I don't wish to plagiarise him here so if you would like a better description I suggest you buy the book and read chapter 7 page 111. To tell the content pipeline that we want our model to use tangents we simply select the model, expand the Content Processor and set the Generate Tangent Frame to True, it is False by default.




Now we have done that when the model compiles (if it has the correct Vertex Channels, if it does not you will get a compile error, many posts ago I posted a workaround to this, but event hen you will still not get tangent data if the channels are not there) it will have tangent data that we can use in the shader.
We add yet another texture map for the bump map

texture BumpMap ;
sampler BumpMapSampler = sampler_state
{
Texture = <BumpMap>;
};

The structures change too....

struct VS_IN
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
float3 Normal : NORMAL;
float3 Tangent : TANGENT;
};
struct VS_OUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
float3 Light : TEXCOORD1;
};

We have added the Tangent data to the VS_IN structure so we can use the newly generated tangent data and in the VS_OUT structure we have removed the Normal member. This is because we are going to use the world tangent space to transpose the light in the vertex shader and then use the bump (normal) map to obtain the normal.

The vertex shader now looks like this

VS_OUT VS_ColorGlowBump(VS_IN input)
{
VS_OUT output = (VS_OUT)0;
output.Position = mul(input.Position,wvp);
float3x3 worldToTangentSpace;
worldToTangentSpace[0] = mul(input.Tangent,world);
worldToTangentSpace[1] = mul(cross(input.Tangent,input.Normal),world);
worldToTangentSpace[2] = mul(input.Normal,world);
output.Light = mul(worldToTangentSpace,LightDirection);
output.TexCoord = input.TexCoord;
return output;
}

As you can see, we transform the position as before, then calculate the tangent space matrix, and using that transform the light direction.

PS_OUT PS_ColorGlowBump(VS_OUT input)
{
PS_OUT output = (PS_OUT)0;
float3 Normal = (2 * (tex2D(BumpMapSampler,input.TexCoord))) - 1.0;
float3 LightDir = normalize(input.Light);
float Diffuse = saturate(dot(LightDir,Normal));
float4 texCol = tex2D(ColorMapSampler,input.TexCoord);
float4 glowCol = tex2D(GlowMapSampler,input.TexCoord);
float4 Ambient = AmbientIntensity * AmbientColor;
float4 glow = glowCol * saturate(1-Diffuse);
texCol *= Diffuse;
output.Color = Ambient + texCol + glow;
return output;
}
In the above pixel shader we get the normal from the bump map, normalise the light direction, then calculate the diffuse as before but with the values generated from tangent space. The rest of the shader is as before giving this result.




Reflective Map





Now this is as easy as the colour and the glow map implementations. What we are going to do is implement specular lighting effect but only where the reflection map tells us to. Again we add another texture and sampler to manage the reflection map.

texture ReflectionMap : Diffuse;
sampler ReflectionMapSampler = sampler_state
{
texture = <ReflectionMap>;
};

We now need to add the members we require for specular lighting as we did in the last episode to the structures. VS_IN stays the same but VS_OUT needs to be modified for specular lighting.

struct VS_OUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
float3 Light : TEXCOORD1;
float3 CamView : TEXCOORD2;
float4 posS : TEXCOORD3;
float3 Normal : TEXCOORD4;
};

We then add our specular lighting code to the vertex shader

VS_OUT VS_Ambient(VS_IN input)
{
VS_OUT output = (VS_OUT)0;
output.Position = mul(input.Position,wvp);
float3x3 worldToTangentSpace;
worldToTangentSpace[0] = mul(input.Tangent,world);
worldToTangentSpace[1] = mul(cross(input.Tangent,input.Normal),world);
worldToTangentSpace[2] = mul(input.Normal,world);
float4 PosWorld = mul(input.Position,world);
output.Light = mul(worldToTangentSpace,LightDirection);
output.CamView = CameraPosition - mul(input.Position,world);
output.posS = input.Position;
output.TexCoord = input.TexCoord;
output.Normal = mul(input.Normal,world);
return output;
}

And now we apply the specular effect to the areas the reflection map tells us to in the pixel shader

PS_OUT PS_Ambient(VS_OUT input)
{
PS_OUT output = (PS_OUT)0;
float3 Normal = (2 * (tex2D(BumpMapSampler,input.TexCoord))) - 1.0;
float3 LightDir = normalize(input.Light);
float Diffuse = saturate(dot(LightDir,Normal));
float4 texCol = tex2D(ColorMapSampler,input.TexCoord);
float4 glowCol = tex2D(GlowMapSampler,input.TexCoord);
float4 Ambient = AmbientIntensity * AmbientColor;
float4 glow = glowCol * saturate(1-Diffuse);
texCol *= Diffuse;
float3 Half = normalize(normalize(LightDirection) + normalize(input.CamView));
float specular = pow(saturate(dot(normalize(input.Normal),Half)),25);
float4 specCol = 2 * tex2D(ReflectionMapSampler,input.TexCoord) * (specular * Diffuse);
output.Color = Ambient + texCol + glow + specCol;
return output;
}

I have high lighted the import ant bit in purple and bold. We now have an effect that looks like this.




Also, if you open up my solution and take a look at the images, in the Content Processor of each I have also set the Resize To Power of Two to True, this makes the images more graphics pipeline friendly.

You can download the sample for this episode here.

5 comments:

  1. Hi Charles,

    Can I use your colour map in one of my non-commercial project? I am doing a virtual exhibition at an university.

    Thanks,
    Cong

    ReplyDelete
    Replies
    1. You can use what ever you like of mine in both commercial and none commercial projects, the assets in this post are from the MS SDK, so you may need to check out the licensing for that, but for none profit/educational applications I think they are fine, but you best check before you use them.

      Anything you do use of mine, all I ask is you give me a credit, and if you are feeling generous, a link too :)

      Thanks for asking by the way :)

      Delete