|
Features

Implementing Lighting Models With HLSL
Diffuse
Lighting
In
a diffuse lighting model (or "positional lighting model")
the location of the light is considered. Another characteristic
of diffuse lighting is that reflections are independent of the observer's
position. Therefore the surface of an object in a diffuse lighting
model reflects equally well in all directions. This is why diffuse
lighting is commonly used to simulate matte surfaces (another more
advanced lighting model to simulate matte surfaces was developed
by Oren-Nayar [Fosner] [Valient]).
The
diffuse lighting model following Lambert's law is described with
the help of two vectors (read more in [RTR] and [Savchenko]). The
light vector L describes the position of light and the normal vector
N describes the normal of a vertex of the object:
The
diffuse reflection has its peak (cos alpha = = 1) when L
and N are aligned; in other words, when the surface is perpendicular
to the light beam. The diffuse reflection diminishes for smaller
angles. Therefore light intensity is directly proportional to cos
a.
To
implement the diffuse reflection in an efficient way, a property
of the dot product of two n-dimensional vectors is used:
N.L
= ||N|| * ||L|| * cos alpha
If
the light and normal vectors are of unit length (normalized), this
leads to
N.L
= cos alpha
When
N.L is equal to cos alpha , the diffuse lighting component
can be described with the dot product of N and L. This diffuse lighting
component is usually added to the ambient lighting component like
this:
I
= Aintensity * Acolor + Dintensity * Dcolor * N.L + Specular
The
example uses the following simplified version:
I
= A + D * N.L + Specular
The
following source code shows the HLSL vertex and pixel shaders:
|
|
float4x4
matWorldViewProj;
float4x4 matWorld;
float4 vecLightDir;
struct
VS_OUTPUT
{
float4 Pos : POSITION;
float3 Light : TEXCOORD0;
float3 Norm : TEXCOORD1;
};
VS_OUTPUT
VS(float4 Pos : POSITION, float3 Normal : NORMAL)
{
VS_OUTPUT Out = (VS_OUTPUT)0;
Out.Pos = mul(Pos, matWorldViewProj);
// transform Position
Out.Light = vecLightDir; // output
light vector
Out.Norm = normalize(mul(Normal,
matWorld)); // transform Normal
and normalize it
return Out;
}
float4
PS(float3 Light: TEXCOORD0, float3 Norm : TEXCOORD1) : COLOR
{
float4 diffuse = { 1.0f, 0.0f, 0.0f,
1.0f};
float4 ambient = {0.1, 0.0, 0.0,
1.0};
return ambient + diffuse * saturate(dot(Light,
Norm));
}
|
 |
 |
 |
|
Compared
to the previous example, the vertex shader gets additionally a vertex
normal as input data. The semantic NORMAL
shows the compiler how to bind the data to the vertex shader registers.
The world-view-projection matrix, the world matrix and the light
vector are provided to the vertex shader via the constants matWorldViewProj,
matWorld, vecLightDir.
All these constants are provided by the application to the vertex
shader.
The
vertex shader VS outputs additionally the N and L vectors in the
variables Light and
Norm here. Both vectors
are normalized in the vertex shader with the intrinsic function
normalize(). This
function returns the normalized vector v = v/length(v). If the length
of v is 0, the result is indefinite.
The
normal vector is transformed by beeing multiplied with the world
matrix (read more on transformation of normals in [RTR2]). This
is done with the function mul(a, b), which performs a matrix multiplication
between a and b. If a is a vector, it is treated as a row vector.
If b is a vector, it is treated as a column vector. The inner dimensions
acolumns and brows must be equal.
The result has the dimension arows * bcolumns.
In this example mul()
gets the position vector as the first parameter --therefore it is
treated as a row vector -- and the transformation matrix, consisting
of 16 floating-point values (float4x4),
as the second parameter. The following figure shows the row vector
and the matrix:
The
whole lighting formula consisting of an ambient and a diffuse component
is implemented in the return statement. The diffuse and ambient
constant values were defined in the pixel shader to make the source
code easier to understand. In a real-world application these values
might be loaded from the 3D model file.
Specular Lighting
Whereas
diffuse lighting considers the location of the light vector and
ambient lighting does not consider the location of the light or
the viewer at all, specular lighting considers the location of the
viewer. Specular lighting is used to simulate smooth, shiny and/or
polished surfaces.
In
the specular lighting model developed by Bui Tong Phong [Foley],
two vectors are used to calculate the specular component: the viewer
vector V that describes the direction of the viewer (in other words
the camera), and the reflection vector R that describes the direction
of the reflections from the light vector.
The
angle between V and R is ß. The more V is aligned with R,
the brighter the specular light should be. Therefore cos ß
can be used to describe the specular reflection. Additionally to
characterize shiny properties an exponentn is used.
Therefore
the specular reflection can be described with
cos
(ß)n
In
an implementation of a specular lighting model, a property of the
dot product can be used as follows:
R.V
= ||R|| * ||V|| * cos ß
If
both vectors are of unit length, R.V can replace cos ß. Therefore
the specular reflection can be described with
(R.V)n
It
is quite common to calculate the reflection vector with the following
formula (read more in [Foley2]):
R
= 2 * (N.L) * N - L
The
whole Phong specular lighting formula now looks like this:
I
= Aintensity * Acolor + Dintensity
* Dcolor * N.L + Sintensity * Scolor
* (R.V)n
Baking
some values together and using white as the specular color leads
to (find a more advanced implementation of specular lighting together
with cube map shadow mapping at [Persson] [Valient]):
I
= A + D * N.L + (R.V)n
The
following source shows the implementation of the Phong specular
lighting model:
|
|
float4x4
matWorldViewProj;
float4x4 matWorld;
float4 vecLightDir;
float4 vecEye;
struct
VS_OUTPUT
{
float4 Pos : POSITION;
float3 Light : TEXCOORD0;
float3 Norm : TEXCOORD1;
float3 View : TEXCOORD2;
};
VS_OUTPUT
VS(float4 Pos : POSITION, float3 Normal : NORMAL)
{
VS_OUTPUT Out = (VS_OUTPUT)0;
Out.Pos = mul(Pos, matWorldViewProj);
// transform Position
Out.Light = vecLightDir; //
L
float3 PosWorld = normalize(mul(Pos,
matWorld));
Out.View = vecEye - PosWorld;
// V
Out.Norm = mul(Normal, matWorld);
// N
return Out;
}
float4
PS(float3 Light: TEXCOORD0, float3 Norm : TEXCOORD1,
float3 View
: TEXCOORD2) : COLOR
{
float4 diffuse = { 1.0f, 0.0f, 0.0f,
1.0f};
float4 ambient = { 0.1f, 0.0f, 0.0f,
1.0f};
float3 Normal = normalize(Norm);
float3 LightDir = normalize(Light);
float3 ViewDir = normalize(View);
float4 diff = saturate(dot(Normal,
LightDir)); // diffuse component
// R = 2 * (N.L) * N - L
float3 Reflect = normalize(2 * diff
* Normal - LightDir);
float4 specular = pow(saturate(dot(Reflect,
ViewDir)), 8); // R.V^n
// I = Acolor + Dcolor * N.L + (R.V)n
return ambient + diffuse * diff
+ specular;
}
|
 |
 |
 |
|
Like
the previous example, the vertex shader input values are the position
values and a normal vector. Additionally, the vertex shader gets
as constants the matWorldViewProj
matrix, the matWorld
matrix, the position of the eye in vecEye
and the light vector in vecLightDir
from the application.
In
the vertex shader, the viewer vector V
is calculated by subtracting the vertex position in world space
from the eye position. The vertex shader outputs the position, the
light, normal and viewer vector. The vectors are also the input
values of the pixel shader.
All
three vectors are normalized with normalize()
in the pixel shader, whereas in the previous example vectors were
normalized in the vertex shader. Normalizing vectors in the pixel
shader is quite expensive, because every pixel shader version has
a restricted number of assembly instruction slots available for
use by the output of the HLSL compiler. If more instructions slots
are used than available, the compiler will display an error message
like the following:
error
X4532: cannot map expression to pixel shader instruction set
The
number of instruction slots available in a specific Direct3D pixel
shader version usually corresponds to the number of instruction
slots available in graphics cards. The high-level language compiler
can not choose a suitable shader version on its own, this has to
be done by the programmer. If your game will targets something other
than the least common denominator of graphics cards on the target
market, several shader versions must be provided.
Here
is a list of all vertex and pixel shader versions supported by DirectX
9.
| Version |
Inst.
Slots |
Constant
Count |
| ====== |
======== |
===========
|
| vs_1_1
|
128 |
at
least 96 cap'd (4) |
| vs_2_0 |
256 |
cap'd
(4) |
| vs_2_x |
256 |
cap'd
(4) |
| vs_2_sw |
unlimited |
8192 |
| vs_3_0
|
cap'd
(1) |
cap'd
(4) |
| ps_1_1
- ps_1_3 |
12
|
8 |
| ps_1_4 |
28
(in two phases) |
8 |
| ps_2_0 |
96 |
32 |
| ps_2_x |
cap'd
(2) |
32 |
| ps_3_0
|
cap'd
(3) |
224 |
| ps_3_sw
|
unlimited
|
8192 |
(1)
D3DCAPS9.MaxVertexShader30InstructionSlots
(2) D3DCAPS9.D3DPSHADERCAPS2_0.NumInstructionSlots
(3) D3DCAPS9.MaxPixelShader30InstructionSlots
(4) D3DCAPS9.MaxVertexShaderConst
The
capability bits above have to be checked to obtain the maximum number
of instructions supported by a specific card. This can be done in
the DirectX Caps viewer or via the D3DCAPS9
structure in the application. Checking the availability of specific
shader versions is usually done while the application starts up.
The application can then choose the proper version from a list of
already prepared shaders or the application can put together a shader
consisting of already prepared shader fragments with the help of
the shader fragment linker.
Note
that the examples shown here require mostly pixel shader version
2.0 or higher. With some tweaking, some of the effects might be
implemented with a lower shader version by trading off some image
quality, but that's out of the scope of this article.
The
pixel shader example above calculates the diffuse reflection in
the source code line below the lines that are used to normalize
the input vectors. The function saturate()
is used here to clamp all values to the range 0..1. The reflection
vector is retrieved by re-using the result from the diffuse reflection
calculation. To get the specular power value, the function pow()
is used. It is declared as pow(x,
y) and returns xy.
This function is only available in pixel shaders >= ps_2_0. Getting
a smooth specular power value in pixel shader versions lower than
ps_2_0 is quite a challenge (read more at [Beaudoin/Guardado], [Halpin]).
The
last line that starts with the return
statement corresponds to the implementation of the lighting formula
shown above.
______________________________________________________
|