|
Features

Implementing Lighting Models With HLSL
Self-Shadowing Term
It's
common to use a self-shadowing term together with a specular lighting
model to receive a geometric self-shadowing effect. Such a term
sets the light brightness to zero, if the light vector is obscured
by geometry and allows a linear scaling of the light brightness.
This helps to reduce pixel popping when using bump maps. The screenshot
in Figure 10 shows an example of a self-shadowing term on the right
and an example that does not use a self-shadowing term on the left.
There
are several ways to calculate this term. The following example uses
(read more at [Frazier]):
S
= saturate(4 * N.L)
This
implementation just re-uses the N.L calculation to calculate the
self-shadowing term. This leads to the following lighting formula:
I
= A + S * (D * N.L + (R.V)n)
The
self-shadowing term reduces the diffuse and specular component of
the lighting model to null, in case the diffuse component is null.
In other words: the diffuse component is used to diminish the intensity
of the specular component. This is shown in the following pixel
shader:
|
|
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.1, 0.0, 0.0, 1.0};
float3 Normal = normalize(Norm);
float3 LightDir = normalize(Light);
float3 ViewDir = normalize(View);
float4 diff = saturate(dot(Normal, LightDir));
// diffuse component
// compute self-shadowing term
float shadow = saturate(4* diff);
float3 Reflect = normalize(2 * diff * Normal -
LightDir); // R
float4 specular = pow(saturate(dot(Reflect, ViewDir)),
8); // R.V^n
// I = ambient + shadow * (Dcolor * N.L + (R.V)n)
return ambient + shadow * (diffuse * diff +
specular);
}
|
 |
 |
 |
|
Bump Mapping
As
you are probably aware, bump mapping fakes the existance of geometry
(grooves, nodges, bulges). The screenshot in Figure 11 shows an
example of a program that gives the viewer the illusion that regions
with mountains are higher than the water regions on earth.
Whereas
a color map stores color values, a bump map is a graphics file (*.dds,
*.tga etc.) that stores normals that are used instead of the vertex
normals to calculate the lighting. These normals are stored in the
most common bump mapping technique in what is called texture space
or tangent space. The light vector is usually handled in object
or world space, which leads to the problem, that the light vector
has to be transformed into the same space as the normals in the
bump map, to obtain proper results. This is done with the help of
a texture or tangent space coordinate system.
The
easiest way to obtain such a texture space coordinate system is
to use the D3DXComputeNormal()
and D3DXComputeTangent()
functions provided with the Direct3D utility library Direct3DX.
Source code implementations of the functionality covered by these
functions can be found in the NVMeshMender library, that can be
downloaded from the Nvidia web site (developer.nvidia.com). Calculating
a texture space coordinate system can work similar to the calculation
of the vertex normal. For example, if a vertex shares three triangles,
the face normal/face tangent of each triangle is calculated first,
then these face normals of all three triangles are added together
at the vertex that connects these triangles to form the vertex normal/vertex
tangent. This example uses the vertex normal instead of a W vector
for the texture space coordinate system and calculates the U vector
with the help of the D3DXComputeTangent()
function. The V vector is retrieved by calculating the cross product
of the W and the U vector. This is done with the cross()
function in the vertex shader:
|
|
struct
VS_OUTPUT
{
float4 Pos : POSITION;
float2 Tex : TEXCOORD0;
float3 Light : TEXCOORD1;
float3 View : TEXCOORD2;
};
VS_OUTPUT
VS(float4 Pos : POSITION, float2 Tex : TEXCOORD, float3
Normal : NORMAL, float3 Tangent : TANGENT )
{
VS_OUTPUT Out = (VS_OUTPUT)0;
Out.Pos = mul(Pos, matWorldViewProj); // transform
Position
// compute the 3x3 tranform matrix
// to transform from world space to tangent
space
float3x3 worldToTangentSpace;
worldToTangentSpace[0] = mul(Tangent, matWorld);
worldToTangentSpace[1] = mul(cross(Tangent,
Normal), matWorld);
worldToTangentSpace[2] = mul(Normal, matWorld);
Out.Tex = Tex.xy;
Out.Light.xyz = mul(worldToTangentSpace, vecLightDir);
// L
float3 PosWorld = normalize(mul(Pos, matWorld));
float3 Viewer = vecEye - PosWorld; // V
Out.View = mul(worldToTangentSpace, Viewer);
return Out;
}
float4
PS(float2 Tex: TEXCOORD0, float3 Light : TEXCOORD1,
float3 View : TEXCOORD2) : COLOR
{
float4 color = tex2D(ColorMapSampler, Tex);
// fetch color map
float3 bumpNormal = 2 * (tex2D(BumpMapSampler,
Tex) - 0.5); // bump map
float3 LightDir = normalize(Light); // L
float3 ViewDir = normalize(View); // V
float4 diff = saturate(dot(bumpNormal, LightDir));
// diffuse comp.
float shadow = saturate(4 * diff); // compute
self-shadowing term
float3 Reflect = normalize(2 * diff * bumpNormal
- LightDir); // R
// gloss map in color.w restricts spec reflection
float4 spec = min(pow(saturate(dot(Reflect,
ViewDir)), 3), color.w);
return 0.2 * color + shadow * (color * diff
+ spec);
}
|
 |
 |
 |
|
The
3x3 matrix, consisting of the U, V and W (== N) vectors, is created
in the vertex shader and is used there to transform L and V to texture
space.
Compared
to the previous example, the major differences in the pixel shader
are the usage of a color map and a bump map, that are fetched with
tex2D(). The function
tex2D() is declared
as tex2D(s, t), whereas
s is a sampler object and t is a 2D texture coordinate. Please consult
the DirectX 9 documentation for a lot of other texture sampler functions,
like texCUBE(s, t),
which fetches a cube map.
The
normal from the bump map is used instead of the normal from the
vertex throughout the whole pixel shader. It is fetched from the
normal map by biasing (- 0.5) and scaling (* 2.0) its values. This
has to be done, because the normal map was stored in a unsigned
texture format with a value range of 0..1 to allow older hardware
to operate correctly. Therefore the normals have to be expanded
back to their signed range.
Compared
to previous examples, this pixel shader restricts the region where
a specular reflection might happen to the water regions of the shown
earth model. This is done with the help of the min()
function and a gloss map that is stored in the alpha values of the
color map. min()
is defined as min(a, b)
and it selects the lesser of a and b.
In
the return statement, the ambient term is replaced by an intensity-decreased
color value from the color map. This way, if the self-shadowing
term reduces the diffuse and specular lighting component, at least
a very dark Earth is still visible.
Point Light
The
last example adds a per-pixel point light to the previous example.
Contrary to the parallel light beams of a directional light source,
the light beams of a point light spread out from the position of
the point light uniformerly in all directions.
Using
a point light with an attenuation map is extensively discussed in
articles by Sim Dietrich [Dietrich2], by Dan Ginsburg/Dave Gosselin
[Ginsburg/Gosselin], by Kelly Dempski [Dempski] and others.
Sim
Dietrich shows in his article, that the following function delivers
good enough results:
attenuation
= 1 - d * d // d = distance
which
stands for
attenuation
= 1 - (x * x + y * y + z * z)
Dan
Ginsburg/Dave Gosselin and Kelly Dempski divide the squared distance
through a constant, which stands for the range of distance, in which
the point light attenuation effect should happen:
attenuation
= 1 - ((x/r)2 + (y/r)2 + (z/r)2)
The
x/r, y/r and z/r values to the pixel shader via a TEXCOORDn
channel and multiply them there with the help of the mul()
function. The relevant source code to do this is:
|
|
VS_OUTPUT
VS(float4 Pos : POSITION, float2 Tex : TEXCOORD,
float3 Normal : NORMAL, float3 Tangent : TANGENT )
{
...
float LightRange = 1.0;
// point light
Out.Att = Light * LightRange;
...
}
float4
PS(float2 Tex: TEXCOORD0, float3 Light : TEXCOORD1,
float3 View : TEXCOORD2, float3 Att : TEXCOORD3) : COLOR
{
...
// attenuation
float4 Attenuation = mul(Att, Att);
// colormap * (self-shadow-term * diffuse + ambient)
+ self-
shadow-term * // specular
return (color * (shadow * diff + ambient) + shadow
* spec) * (1 -Attenuation);
}
|
 |
 |
 |
|
Decreasing
the LightRange parameter
increases the range of light, whereas increasing this value leads
to a shorter light range. The attenuation value is multiplied with
itself in the pixel shader because this is more efficient than using
the exponent 2. In the last line of the pixel shader, the attenuation
value is multiplied with the result of the lighting computation
to decrease or increase light intensity depending on the distance
of the light source.
Wrapping Up
This
article had covered the syntax and some of the intrinsic functions
of HLSL by showing how to implement some common lighting formulas.
It introduced you to six working (and generally concise) code examples,
getting you ready to generate your own shaders. To move on from
here, I recommend lurking into the RenderMonkey examples on ATI's
web site and playing around with the source code found there.
An interactive RenderMonkey tutorial can be found on my
website.
Furthermore,
two books on shader programming (in the "ShaderX2"
series) will be published by Wordware in August 2003. These books
contain articles about shader programming by over 50 authors, and
cover many aspects of shader programming (more info can be found
at www.shaderx2.com). In a
few weeks, Ron Fosner will publish an article on HLSL programming
on Gamasutra that covers the use of RenderMonkey and Oren-Nayar
Diffuse lighting. If you have any comments regarding this article,
I appreciate any feedback -- send it to me at wolf@shaderx.com.
Acknowledgements
I
have to thank the following people for proof-reading this text and
sending me comments (in alphabetical order):
·
Wessam Bahnassi
· Stefano Cristiano
· Ron Fosner
· Muhammad Haggag
· Dale LaForce
· William Liebenberg
· Vincent Prat
· Guillermo Rodríguez
· Mark Wang
This
text is an excerpt from the following upcoming book:
Wolfgang
F. Engel, Beginning Direct3D Game Programming with DirectX 9
Featuring Vertex and Pixel Shaders, May 2003, ISBN 1-93184-139-X
References
[Beaudoin/Guardado]
Philippe Beaudoin, Juan Guardado, A Non-Integer Power Function on
the Pixel Shader, http://www.gamasutra.com/features/20020801/beaudoin_01.htm
(This feature is an excerpt from Direct3D ShaderX: Vertex and
Pixel Shader Tips and Tricks, edited by Wolfgang Engel)
[Dempski]
Kelly Dempski, Real-Time Rendering Tricks and Techniques in DirectX,
Premier Press, Inc., pp 578 - 585, 2002, ISBN 1-931841-27-6
[Dietrich]
Sim Dietrich, "Per-Pixel Lighting", Nvidia developer web-site.
[Dietrich2]
Sim Dietrich, "Attenuation Maps", Game Programming
Gems, Charles River Media Inc., pp 543 - 548, 2000, ISBN 1-58450-049-2
[Foley]
James D. Foley, Andries van Dam, Steven K. Feiner, John F. Hughes,
Computer Graphics - Principles and Practice, Second Edition,
pp. 729 - 731, Addison Wesley, ISBN 0-201-84840-6.
[Foley2]
James D. Foley, Andries van Dam, Steven K. Feiner, John F. Hughes,
Computer Graphics - Principles and Practice, Second Edition,
p. 730, Addison Wesley, ISBN 0-201-84840-6.
[Fosner]
Ron Fosner, Real-Time Shader Programming, Morgan Kauffmann,
pp 54 - 58, 2003, ISBN 1 - 55860-853-2
[Frazier]
Ronald Frazier, "Advanced Real-Time Per-Pixel Lighting in OpenGL",
http://www.ronfrazier.net/apparition/index.asp?appmain=research/
advanced_per_pixel_lighting.html
[Ginsburg/Gosselin]
Dan Ginsburg/Dave Gosselin, "Dynamic Per-Pixel Lighting Techniques",
Game Programming Gems 2, Charles River Media Inc., pp 452
- 462, 2001, ISBN 1-58450-054-9
[Halpin]
Matthew Halpin, "Specular Bump mapping", ShaderX2 -
Shader Tips & Tricks, Wordware Ltd. August 2003, ISBN ??
[Mitchell/Peeper]
Jason L. Mitchell, Craig Peeper, "Introduction to the DirectX
9 High Level Shading Language", ShaderX2 - Shader Programming
Introduction & Tutorials, Wordware Ltd., persumably August
2003, ISBN ??
[RTR]
Thomas Möller, Eric Haines, Real-Time Rendering (Second
Edition), A K Peters, Ltd., p 73, 2002, ISBN 1-56881-182-9.
[RTR2]
Thomas Möller, Eric Haines, Real-Time Rendering (Second
Edition), A K Peters, Ltd., pp 35 - 36, 2002, ISBN 1-56881-182-9.
[Persson]
Emil Persson, "Fragment level Phong Illumination", ShaderX2
- Shader Tips & Tricks, Wordware Ltd. Autumn 2003,
[Savchenko]
Sergei Savchenko, "3D Graphics Programming Games and Beyond",
SAMS, Seite 266, 2000, ISBN 0-672-31929-2.
[Valient]
Michal Valient, "Advanced Lighting and Shading with DirectX
9", ShaderX2 - Shader Programming Introduction & Tutorials,
Wordware Ltd., August 2003.
______________________________________________________
|