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.
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 8.py 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.
"""
#Lesson10.py
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.disableMouse()
base.camLens.setNearFar(1.0, 50.0)
base.camLens.setFov(45.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")
modelLight.reparentTo(light)
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 ]
"""
DIRTY
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.setShader(shader)
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():
i.start()
elif i.isPaused():
i.resume()
else:
i.pause()
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])
run()
//Cg /* lesson10.sha */ /* We now try to implement what was written in the introduction in 8.py. 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: http://www.panda3d.net/wiki/index.php/Shaders_and_Coordinate_Spaces 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 happens. 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 like). 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 = mspos_light.xyz; float3 modelposition = vtx_position.xyz; 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 disregarded). http://en.wikipedia.org/wiki/Euclidean_geometry 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. */Top