Panda3D Game Development: Scene Effects and Shaders

Exclusive offer: get 50% off this eBook here
Panda3D 1.7 Game Developer's Cookbook

Panda3D 1.7 Game Developer's Cookbook — Save 50%

Over 80 recipes for developing 3D games with Panda3D, a full-scale 3D game engine

$26.99    $13.50
by Christoph Lang | April 2011 | Cookbooks Open Source Web Graphics & Video

Get exclusive offers on Open Source Graphic Application and Library books through out this month. For more information click here.

Lights, shadows, and particles are some of this article's topics. Apply shader effects to models. Take control of the advanced shader generator system of Panda3D and learn how to implement your own custom shader generator.

In this article by Christoph Lang, author of Panda3D game developer's cookbook, we will cover:

  • Adding lights and shadows and Using light ramps
  • Creating particle effects and Animating textures
  • Creating a flashlight effect and Making objects reflect the scene
  • Adding a custom shader generator and Applying a custom Cg shader

 

Panda3D 1.7 Game Developer's Cookbook

Panda3D 1.7 Game Developer's Cookbook

Over 80 recipes for developing 3D games with Panda3D, a full-scale 3D game engine

        Read more about this book      

(For more resources on Panda3D, see here.)

Introduction

While brilliant gameplay is the key to a fun and successful game, it is essential to deliver beautiful visuals to provide a pleasing experience and immerse the player in the game world. The looks of many modern productions are massively dominated by all sorts of visual magic to create the jaw-dropping visual density that is soaked up by players with joy and makes them feel connected to the action and the gameplay they are experiencing.

The appearance of your game matters a lot to its reception by players. Therefore it is important to know how to leverage your technology to get the best possible looks out of it. This is why this article will show you how Panda3D allows you to create great looking games using lights, shaders, and particles.

Adding lights and shadows

Lights and shadows are very important techniques for producing a great presentation. Proper scene lighting sets the mood and also adds depth to an otherwise flat-looking scene, while shadows add more realism, and more importantly, root the shadow-casting objects to the ground, destroying the impression of models floating in mid-air.

This recipe will show you how to add lights to your game scenes and make objects cast shadows to boost your visuals.

Getting ready

You need to create the setup presented in Setting up the game structure before proceeding, as this recipe continues and builds upon this base code.

How to do it...

This recipe consists of these tasks:

  1. Add the following code to Application.py:

    from direct.showbase.ShowBase import ShowBase
    from direct.actor.Actor import Actor
    from panda3d.core import *

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)

    self.panda = Actor("panda", {"walk": "panda-walk"})
    self.panda.reparentTo(render)
    self.panda.loop("walk")

    cm = CardMaker("plane")
    cm.setFrame(-10, 10, -10, 10)
    plane = render.attachNewNode(cm.generate())
    plane.setP(270)

    self.cam.setPos(0, -40, 6)

    ambLight = AmbientLight("ambient")
    ambLight.setColor(Vec4(0.2, 0.1, 0.1, 1.0))
    ambNode = render.attachNewNode(ambLight)
    render.setLight(ambNode)
    dirLight = DirectionalLight("directional")
    dirLight.setColor(Vec4(0.1, 0.4, 0.1, 1.0))
    dirNode = render.attachNewNode(dirLight)
    dirNode.setHpr(60, 0, 90)
    render.setLight(dirNode)

    pntLight = PointLight("point")
    pntLight.setColor(Vec4(0.8, 0.8, 0.8, 1.0))
    pntNode = render.attachNewNode(pntLight)
    pntNode.setPos(0, 0, 15)
    self.panda.setLight(pntNode)

    sptLight = Spotlight("spot")
    sptLens = PerspectiveLens()
    sptLight.setLens(sptLens)
    sptLight.setColor(Vec4(1.0, 0.0, 0.0, 1.0))
    sptLight.setShadowCaster(True)
    sptNode = render.attachNewNode(sptLight)
    sptNode.setPos(-10, -10, 20)
    sptNode.lookAt(self.panda)
    render.setLight(sptNode)

    render.setShaderAuto()

  2. Start the program with the F6 key. You will see the following scene:

    Panda3D Game Development: Scene Effects and Shaders

How it works...

As we can see when starting our program, the panda is lit by multiple lights, casting shadows onto itself and the ground plane. Let's see how we achieved this effect.

After setting up the scene containing our panda and a ground plane, one of each possible light type is added to the scene. The general pattern we follow is to create new light instances before adding them to the scene using the attachNewNode() method. Finally, the light is turned on with setLight(), which causes the calling object and all of its children in the scene graph to receive light. We use this to make the point light only affect the panda but not the ground plane.

Shadows are very simple to enable and disable by using the setShadowCaster() method, as we can see in the code that initializes the spotlight.

The line render.setShaderAuto() enables the shader generator, which causes the lighting to be calculated pixel perfect. Additionally, for using shadows, the shader generator needs to be enabled. If this line is removed, lighting will look coarser and no shadows will be visible at all.

Watch the amount of lights you are adding to your scene! Every light that contributes to the scene adds additional computation cost, which will hit you if you intend to use hundreds of lights in a scene! Always try to detect the nearest lights in the level to use for lighting and disable the rest to save performance.

There's more...

In the sample code, we add several types of lights with different properties, which may need some further explanation.

Ambient light sets the base tone of a scene. It has no position or direction—the light color is just added to all surface colors in the scene, which avoids unlit parts of the scene to appear completely black. You shouldn't set the ambient color to very high intensities. This will decrease the effect of other lights and make the scene appear flat and washed out.

Directional lights do not have a position, as only their orientation counts. This light type is generally used to simulate sunlight—it comes from a general direction and affects all light-receiving objects equally.

A point light illuminates the scene from a point of origin from which light spreads towards all directions. You can think of it as a (very abstract) light bulb.

Spotlights, just like the headlights of a car or a flashlight, create a cone of light that originates from a given position and points towards a direction. The way the light spreads is determined by a lens, just like the viewing frustum of a camera.

Using light ramps

The lighting system of Panda3D allows you to pull off some additional tricks to create some dramatic effects with scene lights. In this recipe, you will learn how to use light ramps to modify the lights affect on the models and actors in your game scenes.

Getting ready

In this recipe we will extend the code created in Adding lights and shadows found in this article. Please review this recipe before proceeding if you haven't done so yet.

How to do it...

Light ramps can be used like this:

  1. Open Application.py and add and modify the existing code as shown:

    from direct.showbase.ShowBase import ShowBase
    from direct.actor.Actor import Actor
    from panda3d.core import *
    from direct.interval.IntervalGlobal import *

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)

    self.panda = Actor("panda", {"walk": "panda-walk"})
    self.panda.reparentTo(render)
    self.panda.loop("walk")

    cm = CardMaker("plane")
    cm.setFrame(-10, 10, -10, 10)
    plane = render.attachNewNode(cm.generate())
    plane.setP(270)

    self.cam.setPos(0, -40, 6)

    ambLight = AmbientLight("ambient")
    ambLight.setColor(Vec4(0.3, 0.2, 0.2, 1.0))
    ambNode = render.attachNewNode(ambLight)
    render.setLight(ambNode)

    dirLight = DirectionalLight("directional")
    dirLight.setColor(Vec4(0.3, 0.9, 0.3, 1.0))
    dirNode = render.attachNewNode(dirLight)
    dirNode.setHpr(60, 0, 90)
    render.setLight(dirNode)

    pntLight = PointLight("point")
    pntLight.setColor(Vec4(3.9, 3.9, 3.8, 1.0))
    pntNode = render.attachNewNode(pntLight)
    pntNode.setPos(0, 0, 15)
    self.panda.setLight(pntNode)

    sptLight = Spotlight("spot")
    sptLens = PerspectiveLens()
    sptLight.setLens(sptLens)
    sptLight.setColor(Vec4(1.0, 0.4, 0.4, 1.0))
    sptLight.setShadowCaster(True)
    sptNode = render.attachNewNode(sptLight)
    sptNode.setPos(-10, -10, 20)
    sptNode.lookAt(self.panda)
    render.setLight(sptNode)

    render.setShaderAuto()

    self.activeRamp = 0
    toggle = Func(self.toggleRamp)
    switcher = Sequence(toggle, Wait(3))
    switcher.loop()

    def toggleRamp(self):
    if self.activeRamp == 0:
    render.setAttrib(LightRampAttrib.makeDefault())
    elif self.activeRamp == 1:
    render.setAttrib(LightRampAttrib.makeHdr0())
    elif self.activeRamp == 2:
    render.setAttrib(LightRampAttrib.makeHdr1())
    elif self.activeRamp == 3:
    render.setAttrib(LightRampAttrib.makeHdr2())
    elif self.activeRamp == 4:
    render.setAttrib(LightRampAttrib.
    makeSingleThreshold(0.1, 0.3))
    elif self.activeRamp == 5:
    render.setAttrib(LightRampAttrib.
    makeDoubleThreshold(0, 0.1, 0.3, 0.8))

    self.activeRamp += 1
    if self.activeRamp > 5:
    self.activeRamp = 0

  2. Press F6 to start the sample and see it switch through the available light ramps as shown in this screenshot:

Panda3D Game Development: Scene Effects and Shaders

How it works...

The original lighting equation that is used by Panda3D to calculate the final screen color of a lit pixel limits color intensities to values within a range from zero to one. By using light ramps we are able to go beyond these limits or even define our own ones to create dramatic effects just like the ones we can see in the sample program.

In the sample code, we increase the lighting intensity and add a method that switches between the available light ramps, beginning with LightRampAttrib.makeDefault() which sets the default clamping thresholds for the lighting calculations.

Then, the high dynamic range ramps are enabled one after another. These light ramps allow you to have a higher range of color intensities that go beyond the standard range between zero and one. These high intensities are then mapped back into the displayable range, allocating different amounts of values within it to displaying brightness.

By using makeHdr0(), we allocate a quarter of the displayable range to brightness values that are greater than one. With makeHdr1() it is a third and with makeHdr2() we are causing Panda3D to use half of the color range for overly bright values. This doesn't come without any side effects, though. By increasing the range used for high intensities, we are decreasing the range of color intensities available for displaying colors that are within the limits of 0 and 1, thus losing contrast and making the scene look grey and washed out.

Finally, with the makeSingleThreshold() and makeDoubleThreshold() methods, we are able to create very interesting lighting effects. With a single threshold, lighting values below the given limit will be ignored, while anything that exceeds the threshold will be set to the intensity given in the second parameter of the method.

The double threshold system works analogous to the single threshold, but lighting intensity will be normalized to two possible values, depending on which of the two thresholds was exceeded.

Panda3D 1.7 Game Developer's Cookbook Over 80 recipes for developing 3D games with Panda3D, a full-scale 3D game engine
Published: March 2011
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on Panda3D, see here.)

Creating particle effects

Ranging from dust kicked up by an out of control race car spinning out into a run-off area over smoke that ascends from a battlefield to sparks spraying from a magic wand, particles are a great tool for adding life and visual fidelity to the graphics of a game. Therefore, this recipe will show you how to create a simple particle effect.

Getting ready

This recipe is based upon the project setup presented in Setting up the game structure. Please follow this recipe before proceeding.

How to do it...

Let's try the following Panda3D's particle effect system:

  1. Open the file Application.py and add the following code:

    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *
    from direct.particles.Particles import Particles
    from direct.particles.ParticleEffect import ParticleEffect

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)

    self.enableParticles()

    particles = Particles()
    particles.setPoolSize(1000)
    particles.setBirthRate(0.1)
    particles.setLitterSize(10)
    particles.setLitterSpread(3)
    particles.setFactory("PointParticleFactory")
    particles.setRenderer("GeomParticleRenderer")
    particles.setEmitter("SphereVolumeEmitter")
    smiley = loader.loadModel("smiley")
    smiley.setScale(0.1)
    particles.getRenderer().setGeomNode(smiley.node())
    particles.enable()

    self.effect = ParticleEffect("peffect", particles)
    self.effect.reparentTo(render)
    self.effect.enable()

    self.cam.setPos(0, -10, 0)

  2. Press F6 to launch the program and see the following output:

    Panda3D Game Development: Scene Effects and Shaders

How it works...

We placed a simple particle emitter into our sample scene. It spawns new particles, represented by the smiley model, and makes them move to all directions. Let's see what we had to do to create this effect.

In the first line of our Application class' constructor, we enable tracking and updating of particles. After this is done, we can start setting up a particle system.

First, we set the pool size to 1000. This is the maximum amount of particles that are allowed to exist at the same time. Then we set up how many and how often particles are spawned with calls to setBirthRate(), setLitterSize(), and setLitterSpread(), where the last of these methods defines the maximum deviation from the litter size. With our setup, this means the particle system will spawn ten particles each 0.1 seconds with a variation of ±3 particles.

Next, we set up the particle system to use the point factory, want our particles rendered as geoms, and set the particles to be emitted within a spherical volume.

Finally, we load and attach the smiley model to the particle system renderer and add the new ParticleEffect that uses our settings to the scene.

There's more...

As you already may have noticed in the code sample, Panda3D allows you to choose between various factories, renderers, and emitters for your particle systems.

Particle Factories

Panda3D comes with two particle factory types: PointParticleFactory and ZSpinParticleFactory. While the former factory creates particles as points without orientation, the latter is responsible for the creation of particles that spin about their Z-axis.

Particle Renderers

The way particles emitted from a specific system are drawn depends on which particle renderer is set.

The GeomParticleRenderer allows you to assign a GeomNode to the particle system that is used to draw each of the particles, as shown in this recipe's sample code.

If you use the PointParticleRenderer, the LineParticleRenderer or the SparkleParticleRenderer, particles are rendered as points, lines, or little stars, respectively.

This leaves the SpriteParticleRenderer, which allows you to assign a texture image that is used as on-screen representation for your particles.

Particle Emitters

The SphereVolumeEmitter used in this recipe's code defines a sphere-shaped volume within the particles are emitted with a velocity that goes from the center of the sphere to the hull of the volume.

With a BoxVolumeEmitter, particles are spawned inside a box without any velocity.

A DiscVolumeEmitter works similar to the SphereVolumeEmitter, with the difference of using a flat, disc-shaped bounding volume.

PointEmitter, RectangleEmitter, and SphereSurfaceEmitter spawn particles from a single point, within a rectangle and on the outer hull of a sphere, without assigning any velocity to the newly created particles.

The RingEmitter and TangentRingEmitter emitters both create new particles on a ring. Particles spawned by a RingEmitter move on an axis that points from the center of the ring to the outside, whereas the TangentRingEmitter gives particles an initial velocity with a direction that is tangential to the ring that forms the emitter's bounding volume.

Animating textures

Many great effects found in games can be achieved using very simple measures. The effect you will learn about in this recipe falls into this category. Animated textures are used very often to create the illusion of a flowing lava stream, waves on a lake, or details like a transparent plasma container mounted onto an alien gun.

This recipe will teach you how to put a texture onto an object and animate its position, scale, and rotation.

Getting ready

Please make sure you completed the recipe Setting up the game structure before proceeding with the following steps. Also be sure to add a folder called textures to the project's folder structure and add it to Panda3D's content search paths, and have a texture file in PNG format at hand that can be used for this sample.

How to do it...

  1. Copy your texture file to the textures directory and rename it to texture.png.
  2. Open Application.py and add the following code:

    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *
    from math import *

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    cm = CardMaker("plane")
    cm.setFrame(-3, 3, -3, 3)

    self.plane = render.attachNewNode(cm.generate())
    tex = loader.loadTexture("texture.png")
    self.plane.setTexture(tex)

    self.cam.setPos(0, -12, 0)
    taskMgr.add(self.animate, "texanim")
    def animate(self, task):
    texStage = TextureStage.getDefault()

    offset = self.plane.getTexOffset(texStage)
    offset.setY(offset.getY() - 0.005)
    self.plane.setTexOffset(texStage, offset)

    scale = sin(offset.getY()) * 2
    self.plane.setTexScale(texStage, scale, scale)

    rotate = sin(offset.getY()) * 80
    self.plane.setTexRotate(texStage, rotate)
    return task.cont

  3. Press F6 to run the program. The texture will be zoomed in and out while it is being rotated:

    Panda3D Game Development: Scene Effects and Shaders

How it works...

We begin this sample by adding a textured quad to the scene using the CardMaker class and kicking off the task that will call the animate() method on every frame at runtime.

Texture stages are used to handle the properties of textures that are mapped onto an object. In our sample, there is only one texture map applied to the geometry, so we use the getDefault() method to get a reference to the default TextureStage that holds all necessary data for the next operations.

We then use the methods setTexOffset(), setTexScale(), and setTexRotate() to apply transformation, scaling, and rotation to the texture coordinates. This means that we need to think "in reverse" to get the proper results. To move the texture up, we need to decrease the offset along the y-axis as shown in the code. The same applies to scaling and rotation—bigger values mean that the texture will appear smaller, and turning the coordinate system to the right means that the texture will rotate to the left.

We are using a sinus function for animating the rotation and scale of the texture. This makes these two properties go back and forth, which means we are zooming in and out of the texture while it is rotated to the left and then to the right. We also apply a transformation to the texture that makes it go upwards.

Used together, all these animated properties twist and turn the texture a lot. Experiment and try adding and removing the modifications to the texture offset, scale, and rotation to find new and interesting effects!

Adding ribbon trails to an object

This recipe will show you how to implement a ribbon trail effect that is often used for emphasizing high-speed movements such as sword slashes, very fast vehicles passing by or, as you will see after finishing this recipe, the fastest running panda in the world.

Getting ready

Follow the instructions of Setting up the game structure before you proceed to create a basic project setup.

How to do it...

The following steps are necessary for implementing the ribbon trail effect:

  1. Add a new file called Ribbon.py to the project and add the following code:

    from direct.task import Task
    from direct.task.TaskManagerGlobal import taskMgr
    from panda3d.core import *

    class RibbonNode():
    def __init__(self, pos, damping):
    self.pos = Vec3(pos)
    self.damping = damping
    self.delta = Vec3()

    def update(self, pos):
    self.delta = (pos - self.pos) * self.damping
    self.pos += self.delta

    class Ribbon():
    def __init__(self, parent, color, thickness, length, damping):

    self.parent = parent
    self.length = length
    self.thickness = thickness
    self.color = color

    self.lineGen = MeshDrawer()
    self.lineGen.setBudget(100)
    genNode = self.lineGen.getRoot()
    genNode.reparentTo(render)
    genNode.setTwoSided(True)
    genNode.setTransparency(True)

    pos = parent.getPos(render)

    self.trailPoints = []
    for i in range(length):
    self.trailPoints.append(RibbonNode(pos, damping))

    taskMgr.add(self.trail, "update trail")

    def getRoot(self):
    return self.lineGen.getRoot()

    def trail(self, task):
    pos = self.parent.getPos(render)
    self.trailPoints[0].update(pos)

    for i in range(1, self.length):
    self.trailPoints[i].update(self.trailPoints[i - 1].pos)

    self.lineGen.begin(base.cam, render)
    color = Vec4(self.color)
    thickness = self.thickness

    for i in range(self.length - 1):
    p1 = self.trailPoints[i].pos
    p2 = self.trailPoints[i + 1].pos

    startColor = Vec4(color)
    endColor = Vec4(color)
    endColor.setW(color.getW() - 0.2)
    color = Vec4(endColor)
    self.lineGen.unevenSegment(p1, p2, 0, thickness,
    startColor, thickness - 0.3, endColor)
    thickness -= 0.3
    self.lineGen.end()
    return task.cont

  2. Open Application.py and enter the following lines of code:

    from direct.showbase.ShowBase import ShowBase
    from direct.showbase.RandomNumGen import RandomNumGen
    from direct.actor.Actor import Actor
    from panda3d.core import *
    from direct.interval.IntervalGlobal import *
    from Ribbon import Ribbon

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.panda = Actor("panda", {"walk": "panda-walk"})
    self.panda.reparentTo(render)
    self.panda.loop("walk")
    self.panda.setHpr(-90, 0, 0)

    self.ribbon = Ribbon(self.panda, Vec4(1, 1, 1, 1), 3, 10, 0.3)
    self.ribbon.getRoot().setZ(5)

    self.walkIval1 = self.panda.posInterval(1, Vec3(-12, 0,
    0), startPos = Vec3(12, 0, 0))
    self.walkIval2 = self.panda.posInterval(1, Vec3(12, 0, 0),
    startPos = Vec3(-12, 0, 0))
    self.turnIval1 = self.panda.hprInterval(0.1, Vec3(90, 0,
    0), startHpr = Vec3(-90, 0, 0))
    self.turnIval2 = self.panda.hprInterval(0.1, Vec3(-90, 0,
    0), startHpr = Vec3(90, 0, 0))
    self.pandaWalk = Sequence(self.walkIval1, self.turnIval1,
    self.walkIval2, self.turnIval2)
    self.pandaWalk.loop()

    self.cam.setPos(0, -60, 6)
    self.cam.lookAt(0, 0, 6)

  3. Press F6 to start the program and see the panda running:

    Panda3D Game Development: Scene Effects and Shaders

How it works...

Our code puts a trail behind our panda actor that slowly fades out. Let's take a closer look at the code that produced this effect.

In the constructor of the Ribbon class, after initializing our member variables, we set up a new MeshDrawer, which is a very convenient class for working with dynamically updated geometry like our ribbons. We configure it to use a budget of 100 triangles and enable transparency and double sided rendering for the generated geometry.

After this is done, we fill a list of RibbonNodes. Each of these nodes will then try to follow its predecessor in the list, but will be hampered by the amount of damping we specified in the constructor parameter, so our nodes are keeping some distance, between which we span some geometry using a MeshDrawer and unevenSegment() method that draws line segments with different sized ends. Not only the size of the line decreases, but we also make the alpha smaller and smaller with each segment until the trail smoothly fades out.

This leaves us with building a little test scene in our Application class, connecting the ribbon to the panda that is moved back and forth using intervals.

There's more...

The Ribbon class is far from complete, but it does its job and shows nicely how the MeshDrawer class can help to procedurally generate and modify geometry. Some points you may want to extend, for example, are the way the alpha value and the ribbon size are controlled.

Additionally, you could experiment with different damping values and behaviors. Instead of using the same damping value for all RibbonNode objects in the trail, you could try to assign different values to the nodes, making the ones in the back of the trail slower, for example.

Panda3D 1.7 Game Developer's Cookbook Over 80 recipes for developing 3D games with Panda3D, a full-scale 3D game engine
Published: March 2011
eBook Price: $26.99
Book Price: $44.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on Panda3D, see here.)

Creating a flashlight effect

This recipe will show you how to implement an effect that makes the scene look like it was lit from a small flashlight. This really nice effect can help you make your dark and creepy games even darker and creepier!

Getting ready

Follow the steps from Setting up the game structure and add a directory called textures to the project.

Additionally, you will need a texture that represents the light point created by the flashlight, like the one shown as follows:

Panda3D Game Development: Scene Effects and Shaders

How to do it...

Let's get to the code behind this interesting effect:

  1. Copy your texture file to the textures directory and rename it to flashlight.png.
  2. Open Application.py and add the following code:

    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)

    self.world = loader.loadModel("environment")
    self.world.reparentTo(render)
    self.world.setScale(0.5)
    self.world.setPos(-8, 80, 0)

    self.proj = render.attachNewNode(LensNode("proj"))
    lens = PerspectiveLens()
    self.proj.node().setLens(lens)
    self.proj.reparentTo(self.cam)
    self.proj.setHpr(0, -5, 0)
    self.proj.setPos(0, 10, 0)

    tex = loader.loadTexture("flashlight.png")
    tex.setWrapU(Texture.WMClamp)
    tex.setWrapV(Texture.WMClamp)
    ts = TextureStage('ts')
    self.world.projectTexture(ts, tex, self.proj)

    self.cam.setPos(0, -10, 10)

  3. Press F6 to run the application. You will see a dark scene like the one shown in the following screenshot. Only a small portion of the scene will be Lit up, just like using a flashlight in a very dark environment. Use Left Mouse Button + Right Mouse Button to look around.

How it works...

After loading the environment model into our scene, we add a new LensNode, that will be used to project our flashlight texture onto the scene. We also need to assign a new PerspectiveLens to this scene node, which defines the frustum used for projecting the texture so that the light blob appears as a small dot on near objects and becomes bigger if we point at objects that are further away. Additionally, we reparent the projector lens to the camera, move it a bit in front of it, and let it point down slightly.

Then we load the flashlight texture and set its wrap mode to WMClamp. This means that instead of repeating the whole texture image, only the outermost pixel color is repeated. In our case this means that we have only one light blob and everything else appears black.

To conclude our effect implementation, we use the projectTexture() method to put the flashlight texture image onto our environment model.

Making objects reflect the scene

This recipe will show you how to enable and use cube mapping to make your models and actors dynamically reflect any other game objects and the surrounding environment. This is a very useful effect for emphasizing movement by creating a glossy car paint effect in a racing game, for example.

Getting ready

This recipe requires you to have finished the steps of the recipe Setting up the game structure and will follow up to where this recipe left off.

How to do it...

These are the tasks for this recipe:

  1. In the file Application.py, add the following code:

    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *
    from direct.interval.IntervalGlobal import *

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)

    self.world = loader.loadModel("environment")
    self.world.reparentTo(render)
    self.world.setScale(0.5)
    self.world.setPos(-8, 80, 0)

    self.teapot = loader.loadModel("teapot")
    self.teapot.reparentTo(render)
    self.teapot.setPos(0, 0, 10)

    cubeCams = NodePath("cubeCams")
    cubeBuffer = self.win.makeCubeMap("cubemap", 128, cubeCams)
    cubeCams.reparentTo(self.teapot)

    tex = TextureStage.getDefault()
    self.teapot.setTexGen(tex, TexGenAttrib.MWorldCubeMap)
    self.teapot.setTexture(cubeBuffer.getTexture())

    rotate = self.teapot.hprInterval(10, Vec3(360, 0, 0),
    startHpr = Vec3(0, 0, 0))
    move = self.teapot.posInterval(10, Vec3(-10, 0, 10),
    startPos = Vec3(10, 0, 10))
    rotate.loop()
    move.loop()

    self.cam.setPos(0, -30, 10)

  2. Hit the F6 key to run the code and see the scene similar to the one shown in the following screenshot. Note how the teapot model reflects the scene:

How it works...

To render the reflections on the teapot model, we are using a technique called cube mapping. This effect renders the scene around a center point (marked by the object that uses the reflection texture) into six different textures, forming a so-called 'texture cube'. Each side of the cube is formed by a texture that stores what can be seen if we look up, down, left, right, forward, or backward from the center point. Finally, the six textures are mapped onto a model, making it appear as if it reflected its surroundings.

In the first few lines of code, we set up the scene that we are going to work with, before we add a new dummy node that will mark the center point from where the cube map will be generated.

Next we call the makeCubeMap() method, which initializes everything needed for rendering the teapot's surroundings into a texture with a size of 128x128 pixels, specified by the second parameter. The dummy node we pass as the third parameter to this method will act as parent node to the six cameras. These six cameras point at each direction from the center point and capture the image data that is then put into our cube map texture. This also means that our scene will be rendered six additional times, so be aware of the performance implications this brings!

Finally, we use TexGenAttrib.MWorldCubeMap to enable automatic and proper generation of texture coordinates on the teapot, before assigning the cube map texture by calling the getTexture() method on our cube map buffer.

In the closing steps, two intervals for moving and rotating the teapot are added to show off that the cube map texture is updated dynamically.

Adding a custom shader generator

Modern graphics cards and graphics APIs like Direct3D or OpenGL allow developers to program, and therefore customize, the behavior of parts of the graphics pipeline using shader programs, written in a C-like programming language. These shader programs define how vertices are being transformed, which textures are used for retrieving color values, and which final color pixels we have on the screen.

Luckily, Panda3D adds a very nice abstraction layer on top of this shader system, which is called the shader generator. As soon as you call the setShaderAuto() method on a node in the scene graph, the shader generator kicks in and pieces together the right shaders, depending on the render state (textures, colors, lights, and so on.) of your objects.

Sometimes the built-in shader generator and the code it creates may not suit your needs. Therefore, this recipe will explore and show how to add a custom shader generator to Panda3D.

Getting ready

This recipe requires you to modify and build the source code of Panda3D. Please take your time to review Building Panda3D from source code to get set before proceeding with the following steps.

How to do it...

Complete the following steps to create a custom shader generator:

  1. From the top level directory of your unpacked Panda3D source tree, navigate to the subfolder panda\src\pgraphnodes.
  2. Create two new files called customShaderGenerator.h and customShaderGenerator.cxx.
  3. Open customShaderGenerator.h and add the following code:

    #ifndef CUSTOMSHADERGENERATOR_H
    #define CUSTOMSHADERGENERATOR_H

    #include "shaderGenerator.h"

    class EXPCL_PANDA_PGRAPHNODES CustomShaderGenerator : public
    ShaderGenerator {
    PUBLISHED:
    CustomShaderGenerator(PT(GraphicsStateGuardianBase) gsg,
    PT(GraphicsOutputBase) host);
    virtual ~CustomShaderGenerator();
    virtual CPT(RenderAttrib) synthesize_shader(const RenderState
    *rs);

    public:
    static TypeHandle get_class_type() {
    return _type_handle;
    }
    static void init_type() {
    ShaderGenerator::init_type();
    register_type(_type_handle, "CustomShaderGenerator",
    ShaderGenerator::get_class_type());
    }
    virtual TypeHandle get_type() const {
    return get_class_type();
    }
    virtual TypeHandle force_init_type() {init_type(); return get_
    class_type();}
    private:
    static TypeHandle _type_handle;
    };

    #endif

  4. Open the file customShaderGenerator.cxx and add these lines of code:

    #include "customShaderGenerator.h"

    TypeHandle CustomShaderGenerator::_type_handle;

    CustomShaderGenerator::CustomShaderGenerator(PT(GraphicsStateGuard
    ianBase) gsg, PT(GraphicsOutputBase) host) :
    ShaderGenerator(gsg, host) {
    }

    CustomShaderGenerator::~CustomShaderGenerator() {
    }

    CPT(RenderAttrib) CustomShaderGenerator::
    synthesize_shader(const RenderState *rs) {
    }

    The last line of customShaderGenerator.cxx has to be blank for the code to compile properly!

  5. Open shaderGenerator.cxx and copy and paste the method body of synthesize_shader() to the synthesize_shader() method in customShaderGenerator.cxx.
  6. In customShaderGenerator.cxx, replace all occurrences of saturate(dot(l_eye_normal.xyz, lvec.xyz)) with saturate(0.5 * dot(l_eye_normal.xyz, lvec.xyz) + 0.5).
  7. Open the file lightLensNode.h. Find the following code lines and add the highlighted code:

    friend class GraphicsStateGuardian;
    friend class ShaderGenerator;
    friend class CustomShaderGenerator;

  8. In the file pgraphnodes_composite2.cxx, find the line that reads #include "shaderGenerator.cxx" and add the line #include "customShaderGenerator.cxx" below it.
  9. Open config_pgraphnodes.cxx. Below the line #include "shaderGenerator.h", add the line #include "customShaderGenerator.h". Also find this line of code: ShaderGenerator::init_type();. Add a new line below, containing CustomShaderGenerator::init_type();.
  10. Go to the panda\src\dxgsg9 subdirectory of the source tree.
  11. In the file dxGraphicsStateGuardian9.cxx, add the line #include "customShaderGenerator.h" below the other includes. Also find the following code line:

    _shader_generator = new ShaderGenerator(this, _scene_setup->get_
    display_region()->get_window());

    and replace with the following code line:

    _shader_generator = new CustomShaderGenerator(this, _scene_setup-
    >get_display_region()->get_window());

  12. Repeat step 11 for the file glGraphicsStateGuardian_src.cxx in the panda\src\glstuff subdirectory.
  13. Proceed through the steps of Building Panda3D from source code to compile your custom build of Panda3D.

How it works...

We begin implementing our custom shader generator by defining the interface of our new CustomShaderGenerator. We derive this class from the default shader generator and declare our own constructor, destructor, and synthesize_shader() implementations. The code of the synthesize_shader() method will later be handling the generation of the shader code.

Don't be irritated by the PUBLISHED: line and the TypeHandle code. This stuff is needed internally to register the class and methods with Python.

We then proceed to add method implementations to customShaderGenerator.cxx. The constructor calls its base constructor and the destructor remains empty. The real magic happens within the synthesize_shader() method, which we base upon the code of the original code to remain compatible with the existing render states. Unfortunately, the shader generator system is not written in a very modular way, which means we need to copy the method body of the original implementation of the method.

For the purpose of this recipe, we then change the standard Lambert lighting equation slightly by moving the range of possible results from [-1, 1] to [0, 1], making lit scenes appear brighter, as shown in the following comparison with the standard implementation to the left and our custom lighting to the right:

In step 7, we declare our CustomShaderGenerator class to be a friend of LightLensNode, because our base class needs to access some private and protected members of this class when the shader is put together.

The last steps before we can compile our custom version of Panda3D are necessary to add the new class to the build system and register it with the Python API. The most important steps in this closing part are 11 and 12, where we replace the instantiation of the standard ShaderGenerator class with our CustomShaderGenerator.

There's more...

This recipe only made very slight changes to the original implementation of the shader generator. For more extensive changes to this system, you might consider taking a close look at the ShaderGenerator class and its analyze_renderstate() method, which operates on instances of RenderState to determine which shader parts are then needed in synthesize_shader() to produce the proper shader permutation.

Applying a custom Cg shader

Shaders are one of the most powerful concepts in today's graphics programming, allowing programmers to program the graphics hardware and thus provide great flexibility for creating amazing effects.

This recipe will show you how to use shaders written in the Cg shading language with the Panda3D engine.

Getting ready

Setup your project as described in Setting up the game structure. Add an additional folder called shaders in the top-level source directory and add it to Panda3D's resource search paths.

How to do it...

Let's create a shader and apply it to a model:

  1. Add the following code snippet to Application.py:

    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *

    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)

    self.world = loader.loadModel("environment")
    self.world.reparentTo(render)
    self.world.setScale(0.5)
    self.world.setPos(-8, 80, 0)

    shader = loader.loadShader("shader.cg")
    render.setShader(shader)

    self.cam.setPos(0, -40, 10)

  2. Create a new file called shader.cg in the shaders subdirectory and enter the code below:

    //Cg
    void vshader(uniform float4x4 mat_modelproj,
    in float4 vtx_position:POSITION,
    in float2 vtx_texcoord0:TEXCOORD0,
    out float4 l_position:POSITION,
    out float2 l_texcoord0:TEXCOORD0)
    {
    l_position = mul(mat_modelproj, vtx_position);
    l_texcoord0 = vtx_texcoord0;
    }
    void fshader(uniform sampler2D tex_0,
    in float2 l_texcoord0:TEXCOORD0,
    out float4 o_color:COLOR0)
    {
    float4 fullColor = tex2D(tex_0, l_texcoord0);
    float3 rgb = fullColor.xyz;
    rgb *= 8;
    rgb = floor(rgb);
    rgb /= 8;
    o_color = float4(rgb, fullColor.w);
    }

  3. Press F6 to launch the application and see something similar to the following screenshot. The colors will appear somewhat strange, but don't worry, this is what our shader is supposed to do:

How it works...

In our Python code, loading and applying a shader doesn't require any heavy lifting. We just use the loadShader() method and then enable it on models and actors of our choice, as well as all their children in the scene graph using setShader(). One thing to note though, is that if we use such a custom shader, all render states and all the functionality of the shader generator are overridden and need to be reimplemented within the shader file.

After our Python code is set and ready, we implement the shader code. One very important thing about writing shaders can already be found in the first line, where the line //Cg must be found for the engine to be able to recognize the file as Cg shader code.

The vertex shader function must be called vshader, just as the pixel or fragment shader function needs to be called fshader. The names of the function parameters were not chosen arbitrarily, either. These names have to comply with the hard-coded naming convention of Panda3D, so the data provided by the engine can be used by our shader code.

Our simple vertex shader just transforms the scene vertices to their proper position on the screen and hands the texture coordinates on to the pixel shader.

In the pixel shader, we sample from the texture at the main color texture image channel using the tex2D() function. As a special twist, we limit color output to only 8 possible values, creating an old-school look for our scene.

Summary

This article explored Lights, shadows, and particles. We applied shader effects to models and implement our own custom shader generator.


Further resources on this subject:


About the Author :


Christoph Lang

Christoph Lang is a game developer currently working for Mi’pu’mi Games in Vienna. He has a BSc in Computer Science and an MSc in Game Engineering and Simulation Technology, both from UAS Technikum Wien. You can find him blogging on altdevblogaday.com and tweeting as @moorx.

Books From Packt


Panda3D 1.6 Game Engine Beginner's Guide
Panda3D 1.6 Game Engine Beginner's Guide

Python Multimedia
Python Multimedia

3D Graphics with XNA Game Studio 4.0
3D Graphics with XNA Game Studio 4.0

Flash Game Development by Example
Flash Game Development by Example

ZBrush 4 Sculpting for Games: Beginner's Guide
ZBrush 4 Sculpting for Games: Beginner's Guide

Unity 3D Game Development by Example Beginner's Guide
Unity 3D Game Development by Example Beginner's Guide

Hacking Vim 7.2
Hacking Vim 7.2

Python 2.6 Graphics Cookbook
Python 2.6 Graphics Cookbook


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software