strange performance problem

I’ve met a strange performance problem while modifying the Yarr example program.

Basically I have not changed much on Yarr’s water part. I’ve extended the terrain shader with fog support. The fog part works fine.

After that, I add back the water shader part back to the program. It also works fine. However, I find that the frame rate dropped from 75 to 36. I used sync video at 75. The original Yarr can run at 75 fps. I can’t find any place that cause this performance hit.

I am sure that it is not related to the fog part as I removed them also. Then I find that task.time is set as shader input for water.sha to do water effect animation. If I take out this line:
render.setShaderInput(‘time’, task.time)
The fps goes back to 75 fps. (although the water animation is stopped).

It sounds very strange as the performance shall not be affected by the value of shader input in this case ?! My video card is new and the original Yarr program runs fast.

Any idea on how to troubleshoot this problem ?

Makes sense. You’re rendering the same scene twice, so you end up with 2x the rendering time.

Are you sure that the water shader is still working?
If the shader input ‘time’ is missing, it will throw an error at console and it won’t show the shader, which is probably why your fps is still high.

Note that when you have video sync enabled, the frame rate is constrained to an integer fraction of the sync rate. Which is to say, if your sync rate is 75 Hz, then your frame rate must be one of 75 fps, 75 / 2 = 37.5 fps, 75 / 3 = 25 fps, 75 / 4 = 18.25 fps, 75 /5 = 15 fps, and so on. There’s nothing between 37.5 fps and 75 fps, so if your frame is just a tiny bit slower than 75 fps, it will drop all the way down to 37.5 fps instead.

This means that if you were originally right on the line, just barely making 75 fps, and you add even a tiny bit of complexity to the scene, suddenly you’ll be making only half that.

If you want to interpret the frame rate number meaningfully, you have to disable video sync. You should have video sync enabled when you deliver your final application, but it’s usually best to have it disabled while you are doing performance tuning.


Thank you for the answers.

Well, I do not explain my problem very well.
Actually I give the ‘time’ input at 0 at the very beginning and do not update it by a task afterward. The water shader is working and only the water animation is stopped.

If ‘time’ is updated regularly, the FPS go down to about 36 (actually is just 36 if sync video is off).
If ‘time’ is not updated regularly, the FPS sit at 75 (actually over 150 if sync video is off).
If I run the original Yarr, the FPS is always at 75 (actually over 100 if sync video is off)

I think I better restart from Yarr original and modify it bit by bit to track down the problem.

It turns out that I have other scenes turned on the auto shader
and it cause the Yarr demo run slowly.

If I turn off the auto shader option, or I do not update the ‘time’ shader input regularly, the program will not suffer a performance hit.

So if auto shader is on, Panda generates shaders for the scene, including the water plane node…what exactly is happening with the water shader and the setShaderInput(‘time’, task.time) ???

Can you explain about it ?

So, you’re saying the program runs more slowly when the shader is enabled? That’s not in itself surprising, since the shader involves some difficult computations. Also, even for simple shaders, some graphics cards run shaders much more slowly than traditional fixed-function rendering.


Yes. I think I just need to know more about how setShaderAuto work.

So far it is like that. Since Yarr already have it own shader, I shall not call setShaderAuto. However I accidentally call it and have the following findings:

  1. The overall performance drop significantly. Can you explain what is actually happening if setShaderAuto and the Yarr’s shader is set simultaneously ?

  2. If the shader input ‘time’ is not updated, the overall performance seems recovered. Why and how a shader input affect the overall performance ?


You can’t set them both simultaneously. Whichever one is set on the more specific node, or most recently on a given node, overrides. In this case, the auto-shader is winning. The auto-shader is probably more expensive than the default shader. Note that the auto-shader involves per-pixel lighting computations, which can be expensive.

As pro-rsoft said above, if a required shader input is missing, the console will show an error and the shader will not run. Thus, without the shader input ‘time’, it is as if there is no shader at all in effect.


Actually at the very beginning, I have set the ‘time’ shader input to zero and avoid updating later. So the shader does not complain.

I can still see the water effect but the water is just not moving. Although shader ‘time’ input is not changing, the shader should be still running. Is that right ? So why it has a performance difference ?

Well, if you can still see the water effect, it must not be using the auto-generated shader after all, mustn’t it?

It’s probably time for you to just post your code so we can see what you are seeing.


It just mainly the orginal
Search for “clcheung”, I mark the changes on it.
Run it, it is quiet slow.
Then, remove the setShaderInput(‘time’, task.time)
it runs fast again.

# Author: Gsk
# based partially on "Roaming Ralph" and the "Nature Demo" from the panda forums
# Models: Gsk, Jeff Styers, Reagan Heller
# Last Updated: 7/2/2008
# this example program demonstrates
# - GeoMipTerrain (height map and alpha maps made in L3DT)
# - alpha splatted terrain with lighting using a shader
# - fragment shader based clipping (used to clip away terrain below the water surface)
# - shader based water with reflection, refraction and animated distortion
# - .egg model based skybox (textures made with terragen, model made and uv textured in blender)

import direct.directbase.DirectStart
from pandac.PandaModules import CollisionTraverser,CollisionNode
from pandac.PandaModules import CollisionHandlerQueue,CollisionRay
from pandac.PandaModules import Filename
from pandac.PandaModules import PandaNode,NodePath,Camera,TextNode
from pandac.PandaModules import Vec3,Vec4,BitMask32
from pandac.PandaModules import TextureStage
from pandac.PandaModules import TexGenAttrib
from pandac.PandaModules import GeoMipTerrain
from pandac.PandaModules import CardMaker
from pandac.PandaModules import Texture
from pandac.PandaModules import TextureStage
from pandac.PandaModules import WindowProperties
from pandac.PandaModules import TransparencyAttrib
from pandac.PandaModules import AmbientLight
from pandac.PandaModules import DirectionalLight
from pandac.PandaModules import VBase4
from pandac.PandaModules import Vec4
from pandac.PandaModules import Point3

from pandac.PandaModules import Plane
from pandac.PandaModules import PlaneNode
from pandac.PandaModules import PStatClient
from pandac.PandaModules import CullFaceAttrib
from pandac.PandaModules import RenderState
from pandac.PandaModules import ShaderAttrib

from direct.gui.OnscreenText import OnscreenText
from import Actor
from direct.task.Task import Task
from direct.showbase.DirectObject import DirectObject
import random, sys, os, math

SPEED = 0.5

# Figure out what directory this program is in.
print('running from:'+MYDIR)

# Function to put instructions on the screen.
def addInstructions(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
            pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)

def addTextField(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
            pos=(-1.3, pos), align=TextNode.ALeft, scale = .05, mayChange=True)

# Function to put title on the screen.
def addTitle(text):
    return OnscreenText(text=text, style=1, fg=(1,1,1,1),
                    pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)

class WaterNode():
    def __init__(self, world, x1, y1, x2, y2, z):
        print('setting up water plane at z='+str(z))

        # Water surface
        maker = CardMaker( 'water' )
        maker.setFrame( x1, x2, y1, y2 )

        world.waterNP = render.attachNewNode(maker.generate())
        world.waterNP.setTransparency(TransparencyAttrib.MAlpha )
        world.waterNP.setShader(loader.loadShader( 'shaders/water.sha' ))
        world.waterNP.setShaderInput('wateranim', Vec4( 0.03, -0.015, 64.0, 0 )) # vx, vy, scale, skip
        # offset, strength, refraction factor (0=perfect mirror, 1=total refraction), refractivity
        world.waterNP.setShaderInput('waterdistort', Vec4( 0.4, 4.0, 0.4, 0.45 ))

        # Reflection plane
        world.waterPlane = Plane( Vec3( 0, 0, z+1 ), Point3( 0, 0, z ) )

        planeNode = PlaneNode( 'waterPlane' )
        planeNode.setPlane( world.waterPlane )

        # Buffer and reflection camera
        buffer = 'waterBuffer', 512, 512 )
        buffer.setClearColor( Vec4( 0, 0, 0, 1 ) )

        cfa = CullFaceAttrib.makeReverse( )
        rs = RenderState.make(cfa)

        world.watercamNP = base.makeCamera( buffer )

        sa = ShaderAttrib.make()
        sa = sa.setShader(loader.loadShader('shaders/splut3Clipped.sha') )

        cam = world.watercamNP.node()
        cam.getLens( ).setFov( base.camLens.getFov( ) )
        cam.setInitialState( rs )
        cam.setTagState('True', RenderState.make(sa))

        # ---- water textures ---------------------------------------------

        # reflection texture, created in realtime by the 'water camera'
        tex0 = buffer.getTexture( )
        ts0 = TextureStage( 'reflection' )
        world.waterNP.setTexture( ts0, tex0 )

        # distortion texture
        tex1 = loader.loadTexture('textures/water.png')
        ts1 = TextureStage('distortion')
        world.waterNP.setTexture(ts1, tex1)

class myGeoMipTerrain(GeoMipTerrain):
    def __init__(self, name):
        GeoMipTerrain.__init__(self, name)

    def update(self, dummy):

    def setMonoTexture(self):
        root = self.getRoot()
        ts = TextureStage('ts')
        tex = loader.loadTexture('textures/land01_tx_512.png')
        root.setTexture(ts, tex)

    def setMultiTexture(self):
        root = self.getRoot()
        # root.setShader(loader.loadShader('shaders/splut3.sha'))
        root.setShaderInput('tscale', Vec4(16.0, 16.0, 16.0, 1.0))    # texture scaling

        tex1 = loader.loadTexture('textures/grass_ground2.jpg')
        tex2 = loader.loadTexture('textures/rock_02.jpg')
        tex3 = loader.loadTexture('textures/sable_et_gravier.jpg')

        alp1 = loader.loadTexture('textures/land01_Alpha_1.png')
        alp2 = loader.loadTexture('textures/land01_Alpha_2.png')
        alp3 = loader.loadTexture('textures/land01_Alpha_3.png')

        ts = TextureStage('tex1')    # stage 0
        root.setTexture(ts, tex1)
        ts = TextureStage('tex2')    # stage 1
        root.setTexture(ts, tex2)
        ts = TextureStage('tex3')    # stage 2
        root.setTexture(ts, tex3)

        ts = TextureStage('alp1')    # stage 3
        root.setTexture(ts, alp1)
        ts = TextureStage('alp2')    # stage 4
        root.setTexture(ts, alp2)
        ts = TextureStage('alp3')    # stage 5
        root.setTexture(ts, alp3)

        # enable use of the two separate tagged render states for our two cameras
        root.setTag( 'Normal', 'True' )
        root.setTag( 'Clipped', 'True' )

class World(DirectObject):

    def setMouseBtn(self, btn, value):
        self.mousebtn[btn] = value

    def _setup_camera(self):

        sa = ShaderAttrib.make( )
        sa = sa.setShader(loader.loadShader('shaders/splut3Normal.sha'))

        cam =
        cam.setTagState('True', RenderState.make(sa))

    def __init__(self):
        # clcheung
        render.setShaderInput('time', 0)

        # some constants
        self._water_level = Vec4(0.0, 0.0, 12.0, 1.0)

        print(str( + ' texture stages available')
        # PStatClient.connect()

        self.keyMap = \
        {"left":0, "right":0, "forward":0, "cam-left":0, \
         "cam-right":0, "cam-up":0, "cam-down":0, "mouse":0 },0,0,1))

        # Post the instructions
        self.title = addTitle("Panda3D Tutorial: Yet Another Roaming Ralph (Walking on uneven terrain too)")
        self.inst1 = addInstructions(0.95, "[ESC]: Quit")
        self.inst2 = addInstructions(0.90, "[a]: Rotate Ralph Left")
        self.inst3 = addInstructions(0.85, "[d]: Rotate Ralph Right")
        self.inst4 = addInstructions(0.80, "[s]: Run Ralph Forward")
        self.inst4 = addInstructions(0.70, "[Left Button]: move camera forwards")
        self.inst4 = addInstructions(0.65, "[Right Button]: move camera backwards")
        self.loc_text = addTextField(0.45, "[LOC]: ")

        # -------------------------------------------------------------------
        # Set up the environment

        # GeoMipTerrain
        self.terrain = myGeoMipTerrain('terrain')

        # Set terrain properties

        # Store the root NodePath for convenience
        root = self.terrain.getRoot()
        root.setSz(30)    # z (up) scale

        # Generate it.

        # texture
        # self.terrain.setMonoTexture()
        self.environ = self.terrain    # make available for original ralph code below

        # water
        self.water = WaterNode(self, 0, 0, 256, 256, self._water_level.getZ())

        # add some lighting
        ambient = Vec4(0.34, 0.3, 0.3, 1)
        direct = Vec4(0.74, 0.7, 0.7, 1)

        # ambient light
        alight = AmbientLight('alight')
        alnp = render.attachNewNode(alight)

        # directional ("the sun")
        dlight = DirectionalLight('dlight')
        dlnp = render.attachNewNode(dlight)

        # make waterlevel and lights available to the terrain shader
        root.setShaderInput('lightvec', Vec4(0.7, 0.2, -0.2, 1))
        root.setShaderInput('lightcolor', direct)
        root.setShaderInput('ambientlight', ambient)
        wl.setZ(wl.getZ()-0.05)    # add some leeway (gets rid of some mirroring artifacts)
        root.setShaderInput('waterlevel', self._water_level)

        # skybox
        self.skybox = loader.loadModel('models/skybox.egg')
        # make big enough to cover whole terrain, else there'll be problems with the water reflections
        self.skybox.setBin('background', 1)

        # Create the main character, Ralph

        # ralphStartPos = self.environ.find("**/start_point").getPos()
        ralphStartPosX = 100
        ralphStartPosY = 100
        ralphStartPosZ = self.terrain.getElevation(ralphStartPosX, ralphStartPosY) * root.getSz()

        self.ralph = Actor("models/ralph",
        self.ralph.setPos(ralphStartPosX, ralphStartPosY, ralphStartPosZ)

        self.skybox.setPos(ralphStartPosX, ralphStartPosY, ralphStartPosZ)

        # Create a floater object.  We use the "floater" as a temporary
        # variable in a variety of calculations.

        self.floater = NodePath(PandaNode("floater"))

        # Set the current viewing target for the mouse based controls
        self.focus = Vec3(ralphStartPosX, ralphStartPosY+10, ralphStartPosZ+2)
        self.heading = 180
        self.pitch = 0
        self.mousex = 0
        self.mousey = 0
        self.last = 0
        self.mousebtn = [0,0,0]

        # Accept the control keys for movement and rotation

        self.accept("escape", sys.exit)
        self.accept("arrow_left", self.setKey, ["cam-left",1])
        self.accept("arrow_right", self.setKey, ["cam-right",1])
        self.accept("arrow_up", self.setKey, ["cam-up",1])
        self.accept("arrow_down", self.setKey, ["cam-down",1])
        self.accept("w", self.setKey, ["forward",1])
        self.accept("a", self.setKey, ["left",1])
        self.accept("d", self.setKey, ["right",1])

        self.accept("arrow_left-up", self.setKey, ["cam-left",0])
        self.accept("arrow_right-up", self.setKey, ["cam-right",0])
        self.accept("arrow_up-up", self.setKey, ["cam-up",0])
        self.accept("arrow_down-up", self.setKey, ["cam-down",0])
        self.accept("w-up", self.setKey, ["forward",0])
        self.accept("a-up", self.setKey, ["left",0])
        self.accept("d-up", self.setKey, ["right",0])

        # mouse controls
        self.accept("mouse1", self.setMouseBtn, [0, 1])
        self.accept("mouse1-up", self.setMouseBtn, [0, 0])
        self.accept("mouse2", self.setMouseBtn, [1, 1])
        self.accept("mouse2-up", self.setMouseBtn, [1, 0])
        self.accept("mouse3", self.setMouseBtn, [2, 1])
        self.accept("mouse3-up", self.setMouseBtn, [2, 0])

        # ---- tasks -------------------------------------
        # ralph movement
        # Add a task to keep updating the terrain
        taskMgr.add(self.terrain.update, "update")
        # mouse camera movement
        taskMgr.add(self.controlCamera, "camera-task")

        # Game state variables
        self.prevtime = 0
        self.isMoving = False

        # disable std. mouse
        props = WindowProperties()

        # Set up the camera
        self._setup_camera(), self.ralph.getY()+10, 2)

        # We will detect the height of the terrain by creating a collision
        # ray and casting it downward toward the terrain.  One ray will
        # start above ralph's head, and the other will start above the camera.
        # A ray may hit the terrain, or it may hit a rock or a tree.  If it
        # hits the terrain, we can detect the height.  If it hits anything
        # else, we rule that the move is illegal.

        self.cTrav = CollisionTraverser()

        self.ralphGroundRay = CollisionRay()
        self.ralphGroundCol = CollisionNode('ralphRay')
        self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol)
        self.ralphGroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundHandler)

        self.camGroundRay = CollisionRay()
        self.camGroundCol = CollisionNode('camRay')
        self.camGroundColNp =
        self.camGroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler)

        # Uncomment this line to see the collision rays

        #Uncomment this line to show a visual representation of the
        #collisions occuring
        # self.cTrav.showCollisions(render)

    #Records the state of the arrow keys
    def setKey(self, key, value):
        self.keyMap[key] = value

    # Accepts arrow keys to move either the player or the menu cursor,
    # Also deals with grid checking and collision detection
    def move(self, task):

        elapsed = task.time - self.prevtime

        # If the camera-left key is pressed, move camera left.
        # If the camera-right key is pressed, move camera right.
        camright =
        if (self.keyMap["cam-left"]!=0):
   - camright*(elapsed*20))
        if (self.keyMap["cam-right"]!=0):
   + camright*(elapsed*20))
        if (self.keyMap["cam-up"]!=0):
   + elapsed*10)
        if (self.keyMap["cam-down"]!=0):
   - elapsed*10)

        # save ralph's initial position so that we can restore it,
        # in case he falls off the map or runs into something.

        startpos = self.ralph.getPos()

        # If a move-key is pressed, move ralph in the specified direction.

        if (self.keyMap["left"]!=0):
            self.ralph.setH(self.ralph.getH() + elapsed*300)
        if (self.keyMap["right"]!=0):
            self.ralph.setH(self.ralph.getH() - elapsed*300)
        if (self.keyMap["forward"]!=0):
            backward = self.ralph.getNetTransform().getMat().getRow3(1)
            self.ralph.setPos(self.ralph.getPos() - backward*(elapsed*5))

        # If ralph is moving, loop the run animation.
        # If he is standing still, stop the animation.

        if (self.keyMap["forward"]!=0) or (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0):
            if self.isMoving is False:
                self.isMoving = True
            if self.isMoving:
                self.isMoving = False

        # If the camera is too far from ralph, move it closer.
        # If the camera is too close to ralph, move it farther.

        camvec = self.ralph.getPos() -
        camdist = camvec.length()
        if (camdist > 10.0):
   + camvec*(camdist-10))
            camdist = 10.0
        if (camdist < 5.0):
   - camvec*(5-camdist))
            camdist = 5.0

        # Now check for collisions.

        # Adjust ralph's Z coordinate.  If ralph's ray hit terrain,
        # update his Z. If it hit anything else, or didn't hit anything, put
        # him back where he was last frame.

        entries = []
        for i in range(self.ralphGroundHandler.getNumEntries()):
            entry = self.ralphGroundHandler.getEntry(i)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):

        # just use terrain height
        x = self.ralph.getX()
        y = self.ralph.getY()

        # loc output
        self.loc_text.setText('[LOC] : %03.2f, %03.2f,%03.2f ' % \
                              ( self.ralph.getX(), self.ralph.getY(), self.ralph.getZ() ) )

        # Keep the camera at one foot above the terrain,
        # or two feet above ralph, whichever is greater.

        entries = []
        for i in range(self.camGroundHandler.getNumEntries()):
            entry = self.camGroundHandler.getEntry(i)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
        if ( < self.ralph.getZ() + .5):
   + .5)
        # if ( > self.ralph.getZ() + 2.0):
            # + 2.0)

        # The camera should look in ralph's direction,
        # but it should also try to stay horizontal, so look at
        # a floater which hovers above ralph's head.

        self.floater.setZ(self.ralph.getZ() + 2.0)

        # Store the task time and continue.
        self.prevtime = task.time
        return Task.cont

    # mouse controled main camera
    def controlCamera(self, task):
        # figure out how much the mouse has moved (in pixels)
        md =
        x = md.getX()
        y = md.getY()
        if, 100, 100):
            self.heading = self.heading - (x - 100)*0.2
            self.pitch = self.pitch - (y - 100)*0.2
        if (self.pitch < -89): self.pitch = -89
        if (self.pitch >  89): self.pitch =  89,self.pitch,0)
        dir =
        elapsed = task.time - self.last
        if (self.last == 0): elapsed = 0
        if (self.mousebtn[0]):
            self.focus = self.focus + dir * elapsed*30
        if (self.mousebtn[1]) or (self.mousebtn[2]):
            self.focus = self.focus - dir * elapsed*30 - (dir*5))

        # Time for water distortions
        # clcheung, remove next line with have a better performance
        render.setShaderInput('time', task.time)

        # move the skybox with the camera
        campos =

        # update matrix of the reflection camera
        mc = )
        mf = self.waterPlane.getReflectionMat( )
        self.watercamNP.setMat(mc * mf)

        self.focus = + (dir*5)
        self.last = task.time
        return Task.cont

print('instancing world...')
w = World()

print('calling run()...')

Try setting as “time” value something like 1.235 instead of 0 - it wouldn’t surprise me if the shader would calculate with 0 faster than a different value.

I can’t download the original right now, so I can’t run your sample code to see what’s going on–but in looking at it, I do see that you have simply called render.setShaderAuto() which turns on the auto-shader (and per-pixel lighting) on the entire scene, except that which has another shader already applied to it (like the water).

Per-pixel lighting is of course more expensive than normal fixed-function per-vertex lighting, so it is not surprising that render.setShaderAuto() should slow down your frame rate.

As to why changing the ‘time’ value has any effect at all, pro-rsoft’s guess is a good one. Other than that I have no idea.


I see. I try pro-rsoft’s suggestion but not much improvement.

I just re-read the code and find that turn off the shader in the sky-box will fix the problem.

Sorry for all the troubles ! I will look deeper and see how the water shader is slowing down by the sky-box by the shader input…may be it is an accumulation effects or the water-level code make a difference ?! Good lesson to me on using auto shaders.