Cg Tutorial Part 10

Don't mind the mess!

We're currently in the process of migrating the Panda3D Manual to a new service. This is a temporary layout in the meantime.

Python C++

This page is not in the table of contents.

Cg Tutorial Part 10: Per vertex diffuse lighting with shaders

This time we start with diffuse lighting on our own. There should be no
difference between and this example. If there is a small difference, than
it is possible that your GPU does not emulate the old fixed function pipeline
with shaders.
import sys
import math
import direct.directbase.DirectStart
from direct.interval.LerpInterval import LerpFunc
from panda3d.core import PointLight
base.setBackgroundColor(0.0, 0.0, 0.0)
base.camLens.setNearFar(1.0, 50.0)
camera.setPos(0.0, -20.0, 10.0)
camera.lookAt(0.0, 0.0, 0.0)
root = render.attachNewNode("Root")
Because we calculate our own light we do not need a Panda3D point light anymore.
But we create a dummy node to see where the light is.

light = render.attachNewNode("Light")
modelLight = loader.loadModel("misc/Pointlight.egg.pz")
modelCube = loader.loadModel("cube.egg")
cubes = []
for x in [-3.0, 0.0, 3.0]:
cube = modelCube.copyTo(root)
cube.setPos(x, 0.0, 0.0)
cubes += [ cube ]
In the previous example we had to use the setLight method. This time we have to
apply a shader input manually. If we move around our light node Panda3D updates
the shader input automagically.
Try to enable the light only on one or two cubes.

shader = loader.loadShader("lesson10.sha")
for cube in cubes:
cube.setShaderInput("light", light)
base.accept("escape", sys.exit)
base.accept("o", base.oobe)
def animate(t):
radius = 4.3
angle = math.radians(t)
x = math.cos(angle) * radius
y = math.sin(angle) * radius
z = math.sin(angle) * radius
light.setPos(x, y, z)
def intervalStartPauseResume(i):
if i.isStopped():
elif i.isPaused():
interval = LerpFunc(animate, 10.0, 0.0, 360.0)
base.accept("i", intervalStartPauseResume, [interval])
def move(x, y, z):
root.setX(root.getX() + x)
root.setY(root.getY() + y)
root.setZ(root.getZ() + z)
base.accept("d", move, [1.0, 0.0, 0.0])
base.accept("a", move, [-1.0, 0.0, 0.0])
base.accept("w", move, [0.0, 1.0, 0.0])
base.accept("s", move, [0.0, -1.0, 0.0])
base.accept("e", move, [0.0, 0.0, 1.0])
base.accept("q", move, [0.0, 0.0, -1.0])
/* lesson10.sha */

We now try to implement what was written in the introduction in To
summarize: We like to calculate our lighting equation for each vertex. For this
we need the position of the light and the normal of the vertex.

To achieve this task we need to first talk about spaces. As you already should
now, the vertex shader is feed with the raw vertices (vtx_vertices) from the egg
file, the normals that are introduced here are also unmodified (vtx_normal).
Each cube has his own model space. Maybe you have read the example 2.sha more
closely and have seen that there is a second, commented, vertex shader. In this
shader the most elaborate version transform each vertex from model space to
world space, then from world space to view space and last to the so called clip
space. The light itself is a node like any other, but without any assigned
vertices. So a light has its own "model" space like any other node. When we move
around a cube, we do not modify the cubes model space, nor do we change the
model space of a light if we move the light around. Maybe you see already a new
problem that arises. If we take the blindly take the light and the cubes and
start to do some fancy lighting, we do not get an useful result. What we have to
do first is to transform the cube and/or the light in a space where can do our
calculations in respect to each other. Maybe you think: "Why do we not take the
world space then?". World space sound nice because in this space, the distance
between two nodes can be calculated easily if you use the length function. If
you move the light 10 units farther away from a node, the output of length to
this light is increased by 10. Problem here is that we have to transform all
model and all lights to world space. In our shaders we never have done this,
because there was no need for it. But we always transformed our models from
model space to clip space with the mat_modelproj matrix. You may ask then: "So
why do we not do our lighting calculations in clip space". Problem is that clip
space is in 2D and the length function does not work as intended, because we
have applied the projection matrix. The farther away the scene, the closer are
the cubes on your screen. But should we change the lighting only because the
cubes are far away or not? Ok, back to our world space. If we transform our
model to world space, we also have to transform the normals. If you have rotated
your model e.g. the normals of your model also have to be rotated. This may lead
to a problem, that needs some in depth matrix math. In the next sample I try to
explain what the problem is, but for the moment we stick to another possibility.
Instead of transforming your cubes and lights into world space, we can transform
the lights into model space. But because each cube has its own model space, we
need to transform each light into each cubes model space. This sounds
complicated, but remember, until yet, every transformation we have done, we have
done with a matrix, and Pand3D never forsake us. I have already written that
there is problem if we need to transform normals. But with this version, we do
not need to modify the normals because we transform the light position to the
cubes model space. One more question we may answer here: "We do talk about this
world space transformations if it calls for problems anyway?". The answer
depends on what you like to do. In more complicated shaders you often need to
transform your vertices anyway into world space e.g. for reflections. The other
problems if you e.g. have a static light e.g. a sun. If there is no sunrise and
sunset in your application you maybe encode your light position directly as
constant into the shader. For this case it would be a bit cumbersome to apply
the correct transformation, because you have to do anything on your own. Now we
need to now we transform our light to model space. Long section, simple
solution: You create an uniform called "mspos_light". That means that Panda3D
transform the position of the node with the name light into model space. Panda3D
changes this matrix for each cube automagically.

Now it is a good time to read some more information about spaces and the
flexibility of Panda3D:

Now we have two positions and a normal and all these things are in the same
space. If you remember, we like to do a diffuse lighting effect. For this effect
we need the angle between the vertex normal and the direction from the light to
our vertex. The direction we get when we subtract both positions, the normal we
already have. Sometimes it helps to draw a 2D draft on a paper to see what

In the following drawing L is the light position, in this example at position
(1, 1). V is the vertex position in this example at (7, 1). The vertex has a
normal direction of (-1, 0). The direction vector between the light and the
vertex is (-6, 0).

 L   <-V

If you imagine this two directions (-1, 0) and (-6, 0) they both lay on each
other, that means the angle between this two directions is 0 degree. If you have
read the Wikipedia article about the dot product you may have seen a cosine
there. If we now take the cosine of 0 degree we get 1. 1 is the maximum a cosine
can yield, therefore 1 must mean maximal lighting. If you look back at our draft
you can see that this is exactly what we want here. Here is another example.

 L   V

The Light is at (1, 5), the vertex is at (5, 5) and the vertex normal points
toward (0, 1). The direction between the light and vertex is (-4, 0). But this
time the angle between this two directions is 90 degree. The cosine of 90 degree
is 0. Imagine a light beam that travels to a face where all vertex normal look
upwards like in the drawing. This light beam will not touch a single point on
this surface. Therefore if our cosine yields zero or lesser, that means no
lighting at all. In the vertex shader there is a function saturate. Saturate
clamps a value to the range 0 - 1. A cosine yields a number between -1 and +1.
With saturate we can trim it to 0 - 1.

There is one final question about this vertex shader. There are two calls to
normalize. Normalize modifies a vector so it has an exact length of one unit
afterwards. There are tons of reasons why you should normalize, here we talk
only about one reason. Wikipedia says the following about the dot product:

dot(a, b) = length(a) * length(b) * cos(phi)

Do you see the problem? We only care about cos(phi). Not even phi itself
interests us. But if a and b are not normalized, the length of this two
direction vectors may be large. With other words, the farther away our light,
the brighter our light shines. Normally we want the exact opposite, but in this
example we say it is a point sunlight so we have no attenuation at all. If we
normalize our vectors correctly, their length is by definition exactly 1, there
neither length(a) nor length(b) will influence the dot product.

The only problem here is that normalize is an expensive function because it
introduces a square root. 4.sha has the same note about the length function. The
function normalize is defined as follow:

def normalize(n):
    return n / length(n)

Besides the mathematical problems in this sample, there is nothing really new
here. Only the uniform vtx_normal and the uniform mspos_light are new here, but
I hope they are already explained enough.

Maybe you remember the sample 2.sha. There was more than one vertex shader,
although all had an identical result. If you are a brave one, try to rewrite
this shader so it works with world positions. I have written that there are some
problems if we transform the normals, but for the moment forget this (it is only
a problem in some circumstances). The following uniforms may help you:

trans_model_to_world wspos_light

The uniform mspos_light is not needed anymore. Besides this two new uniforms,
two new call to the function mul are needed (You may send me the result if you

you only need to modify the vertex shader for this. You do not have to touch the
python sample. If you have modified the vertex shader make screen shot of our
output and compare it with the unmodified shader here. Only if there is no
visual difference between them you have done everything right. After you have
modified the shader, use the method setHpr (e.g. setH(90.0)) on any cube and
look once more to you example (do not use setScale or setShear for this test).
Still no difference? */
void vshader(
    uniform float4x4 mat_modelproj,
    uniform float4 mspos_light,
    in float4 vtx_position : POSITION,
    in float3 vtx_normal : NORMAL,
    in float4 vtx_color : COLOR,
    out float4 l_color : COLOR,
    out float4 l_position : POSITION)
    l_position = mul(mat_modelproj, vtx_position);

    float3 lightposition =;
    float3 modelposition =;
    float3 normal = normalize(vtx_normal);
    float3 direction = normalize(lightposition - modelposition);
    float brightness = saturate(dot(normal, direction));

    l_color = vtx_color * brightness;

If something has to be explained here, then something went wrong.

Because all the work is done in the vertex shader, this form if lighting is
called per vertex lighting.
void fshader(
    in float4 l_color : COLOR,
    out float4 o_color : COLOR)
    o_color = l_color;

A small note why some calculations are done in world space or model space. This
two spaces, behave (at least in Panda3D) like our physical space (relativity

E.g. If you have two cubes with a distance of 10 units you can transform them in
any other space with Euclidean properties, and the distance between the two
cubes are still 10 units.