shadow using projectTexture and a offscreen renderer

Just copy the whole thing into a py-file and run it :slight_smile: I hope the comments are good enought.

"""
ShadowCaster, by Reto Spoerri

Thanks to ynjh_jo for some improvement hints

This is a modified version from the sombras.py by Edwood Grant
(A Rez Clone Project), a little bit simplified and more automatic.

Create a cheesy shadow effect by rendering the view of an
object (e.g. the local avatar) from a special camera as seen from
above (as if from the sun), using a solid gray foreground and a
solid white background, and then multitexturing that view onto the
world.

This is meant primarily as a demonstration of multipass and
multitexture rendering techniques.  It's not a particularly great
way to do shadows.

But it does work mostly :)

Known problems:
- the shadowtexture is projected onto the shadow receiving object from the
lightsource, so if a shadow receiving object is in front
(viewed from the lightsource) of the shadow making object
it will receive the shadows anyway.

- check the number of texture stages your graphics card supports if you
dont see shadows.
(you'll see the NUMBER OF TEXTURE STAGES when you run the programm)
"""

from pandac.PandaModules import Point3, Vec3, VBase4, Texture, Camera, NodePath, OrthographicLens, TextureStage, Mat4

SHADOWCOLOR = [0.5, 0.5, 0.5, 1, 1]

# enable to see the render outputs of the shadow cameras
#base.bufferViewer.toggleEnable()

# global
shadowCasterObjectCounter = 0
class ShadowCasterClass:
    texXSize = 2048
    texYSize = 2048
    groundPath = None
    objectPath = None
    
    def __init__(self, objectPath, groundPath, lightPos=Vec3(0,0,1)):
        """ ShadowCaster::__init__
        objectPath is the shadow casting object
        groundPath is the shadow receiving object
        lightPos is the lights relative position to the objectPath
        """
        
        # uniq id-number for each shadow
        global shadowCasterObjectCounter
        shadowCasterObjectCounter += 1
        self.objectShadowId = shadowCasterObjectCounter
        
        # the object which will cast shadows
        self.objectPath = objectPath
        
        # get the objects bounds center and radius to
        # define the shadowrendering camera position and filmsize
        try:
            objectBoundRadius = self.objectPath.getBounds().getRadius()
            objectBoundCenter = self.objectPath.getBounds().getCenter()
        except:
            print "failed"
            objectBoundCenter = Point3( 0,0,0 )
            objectBoundRadius = 1
        
        lightPath = objectPath.attachNewNode('lightPath%i'%self.objectShadowId)
        # We can change this position at will to change the angle of the sun.
        lightPath.setPos( objectPath.getParent(), objectBoundCenter )
        self.lightPath = lightPath
        
        # the film size is the diameter of the object
        self.filmSize = objectBoundRadius * 2
        
        # Create an offscreen buffer to render the view of the avatar
        # into a texture.
        self.buffer = base.win.makeTextureBuffer(
            'shadowBuffer%i'%self.objectShadowId, self.texXSize, self.texYSize)

        # The background of this buffer--and the border of the
        # texture--is pure white.
        clearColor = VBase4(1, 1, 1, 1)
        self.buffer.setClearColor(clearColor)
        
        self.tex = self.buffer.getTexture()
        self.tex.setBorderColor(clearColor)
        self.tex.setWrapU(Texture.WMBorderColor)
        self.tex.setWrapV(Texture.WMBorderColor)

        # Set up a display region on this buffer, and create a camera.
        dr = self.buffer.makeDisplayRegion()
        self.camera = Camera('shadowCamera%i'%self.objectShadowId)
        self.cameraPath = self.lightPath.attachNewNode(self.camera)
        self.camera.setScene(self.objectPath)
        dr.setCamera(self.cameraPath)
        
        self.setLightPos( lightPos )
        
        # Use a temporary NodePath to define the initial state for the
        # camera.  The initial state will render everything in a
        # flat-shaded gray, as if it were a shadow.
        initial = NodePath('initial%i'%self.objectShadowId)
        initial.setColor( *SHADOWCOLOR )
        initial.setTextureOff(2)
        self.camera.setInitialState(initial.getState())
        
        # Use an orthographic lens for this camera instead of the
        # usual perspective lens.  An orthographic lens is better to
        # simulate sunlight, which is (almost) orthographic.  We set
        # the film size large enough to render a typical avatar (but
        # not so large that we lose detail in the texture).
        self.lens = OrthographicLens()
        self.lens.setFilmSize(self.filmSize, self.filmSize)
        self.camera.setLens(self.lens)
        
        # Finally, we'll need a unique TextureStage to apply this
        # shadow texture to the world.
        self.stage = TextureStage('shadow%i'%self.objectShadowId)

        # Make sure the shadowing object doesn't get its own shadow
        # applied to it.
        self.objectPath.setTextureOff(self.stage)
        
        # the object which will receive shadows
        self.setGround( groundPath )
    
    def setLightPos( self, lightPos ):
        """ sets the position of the light
        """
        self.cameraPath.setPos( lightPos )
        self.cameraPath.lookAt( self.lightPath, 0,0,0 )
    
    def setGround(self, groundPath):
        """ Specifies the part of the world that is to be considered
        the ground: this is the part onto which the rendered texture
        will be applied. """
        
        if self.groundPath:
            self.groundPath.clearProjectTexture(self.stage)
        
        self.groundPath = groundPath
        self.groundPath.projectTexture(self.stage, self.tex, self.cameraPath)

    def clear(self):
        """ Undoes the effect of the ShadowCaster. """
        if self.groundPath:
            self.groundPath.clearProjectTexture(self.stage)
            self.groundPath = None

        if self.lightPath:
            self.lightPath.detachNode()
            self.lightPath = None

        if self.cameraPath:
            self.cameraPath.detachNode()
            self.cameraPath = None
            self.camera = None
            self.lens = None

        if self.buffer:
            base.graphicsEngine.removeWindow(self.buffer)
            self.tex = None
            self.buffer = None

# global
lightAngle = 0

def objectShadowClass( objectPath, groundPath ):
    """ add a shadow to the objectPath, which is cast on the groundPath
    from the lightPos
    """
    # define the source position of the light, relative to the objectPath
    lightPos = Vec3( 0,0,100 )
    # add shadows to the object
    sc = ShadowCasterClass(objectPath, groundPath, lightPos)
    
    from direct.task import Task
    import math
    
    def lightRotate( task, sc = sc ):
        """ rotate the light around the object
        """
        global lightAngle
        lightAngle += math.pi / 180 * globalClock.getDt() * 10
        r = 50
        sc.setLightPos( Vec3( r * math.cos(lightAngle), r * math.sin(lightAngle), 100 ) )
        return Task.cont
    
    taskMgr.add(lightRotate, 'lightRotateTask')
    
    return sc

if __name__ == '__main__':
    import direct.directbase.DirectStart
    
    print 'MAXIMUM # OF TEXTURE STAGES (SHADOWS + TEXTURES) :',base.win.getGsg().getMaxTextureStages()
    print '''if the texture count on the objects you use,
is the same like the texture stages, you cant add shadows to them'''

    # change the model paths
    from pandac.PandaModules import getModelPath
    from pandac.PandaModules import getTexturePath
    from pandac.PandaModules import getSoundPath 
    modelPath = '/usr/local/panda'
    getModelPath( ).appendPath( modelPath )
    getTexturePath( ).appendPath( modelPath )
    getSoundPath( ).appendPath( modelPath ) 
    
    camera.setPos(55,-30,25)
    camera.lookAt(render)
    camera.setP(camera,3)
    cameraMat=Mat4(camera.getMat())
    cameraMat.invertInPlace()
    base.mouseInterfaceNode.setMat(cameraMat)
    
    # load the models
    # shadow casting object
    modelNp = loader.loadModelCopy( 'models/panda.egg' )
    modelNp.reparentTo( render )
    # shadow receiving object
    groundNp = loader.loadModelCopy( 'models/environment.egg' )
    groundNp.reparentTo( render )
    # add the shadow to the shadow receiving object
    objectShadowClass( modelNp, groundNp )
    
    run()
  1. If the models’ initial pos is not at (0,0,0), the lightpath would be shifted away from the bounds’ center. The bounds’ center is at the model’s parent coord space, so the correct pos is relative to the parent :
        lightPath.setPos(objectPath.getParent(), objectBoundCenter )
  1. adding initial camera position would be better :
    camera.setPos(55,-30,25)
    camera.lookAt(render)
    camera.setP(camera,3)
    cameraMat=Mat4(camera.getMat())
    cameraMat.invertInPlace()
    base.mouseInterfaceNode.setMat(cameraMat)
  1. perhaps mentioning that it is bounded by the graphics card’s multitexture limit would be better for further consideration.
    print 'MAXIMUM # OF SHADOWS (TEXTURE STAGES) :',base.win.getGsg().getMaxTextureStages()

Thanks for the hints, i have updated the above example according to your suggestions.

I didn’t see any changes. Did you submit it ?

Oups… i remember i had the edit windows open… but most probably didnt submit as you said… now it’s updated. And thanks for your hints.

You also need to clear any material off shadowCaster as well as any texture.

initial.setTextureOff(1)
initial.setMaterialOff(1)	# also suppress any material it has

I have tweaked your code to generate shadows from lights, using different lenses for each kind of light.

from pandac.PandaModules import *

def toggleBufferView():
	# show the render-to-texture buffers in small windows	(see Tut-Fireflies)
	base.bufferViewer.setPosition("llcorner")	# default = lrcorner
	base.bufferViewer.setCardSize(0,0.4)
	base.bufferViewer.setLayout("vline")			# default = hline
	base.bufferViewer.toggleEnable()

class ShadowFromLight:
	texXSize = 2048
	texYSize = 2048
	__Id=0				# hidden outside this class

	def __init__(self, lightSource,shadowCaster,shadowReceiver,shadeHue=Vec4(0,0,0,1)):
		""" ShadowFromLight::__init__
		lightSource		light (nodepath)
		shadowCaster		object(s) that casts shadows (nodepath, or tree of nodepaths)
		shadowReceiver	receives shadows i.e. the ground (nodepath)
		shadeHue			hue of shadows, default = completely black
		"""
		if not(lightSource and shadowCaster and shadowReceiver):
			return		# no shadows to be cast

		lightNode=lightSource.node()
		assert isinstance(lightNode, 
			(DirectionalLight,Spotlight,PointLight)), \
			'lightSource.node() is not (DirectionalLight,Spotlight,PointLight) : '+\
			str(lightNode)

		# save values for documentation & freeing resources in clear()
		self.lightSource=lightSource
		self.shadowCaster=shadowCaster
		self.shadowReceiver=shadowReceiver
		self.shadeHue=shadeHue
		self.id=ShadowFromLight.__Id
		ShadowFromLight.__Id+=1			# next id number

		# get the object's bounds center and radius to
		# define the shadow-rendering camera's target and filmsize
		shadowCasterCentre = shadowCaster.getBounds().getCenter()
		shadowCasterRadius = shadowCaster.getBounds().getRadius()

		# Create an offscreen buffer to render the view of shadowCaster
		# into a texture.
		self.buffer = base.win.makeTextureBuffer(
			'shadowBuffer%i'%self.id, ShadowFromLight.texXSize, ShadowFromLight.texYSize)

		# The background of this buffer--and the border of the
		# texture--is pure white.
		clearColor = VBase4(1, 1, 1, 1)		# do not change - affects final scene
		self.buffer.setClearColor(clearColor)

		self.tex = self.buffer.getTexture()
		self.tex.setBorderColor(clearColor)
		self.tex.setWrapU(Texture.WMBorderColor)
		self.tex.setWrapV(Texture.WMBorderColor)

		# create a camera at position of the light source
		self.camera = Camera('shadowCamera%i'%self.id)
		self.cameraPath = lightSource.attachNewNode(self.camera)
		self.camera.setScene(shadowCaster)
		# Set up a display region on this buffer & attach the camera
		self.buffer.makeDisplayRegion().setCamera(self.cameraPath)

		# Use a temporary NodePath to define the initial state for the
		# camera.  The initial state will render everything in a
		# flat-shaded gray, as if it were a shadow.
		initial = NodePath('initial%i'%self.id)
		initial.setColor(shadeHue)
		initial.setTextureOff(1)	# suppress any texture in object shadowCaster
		initial.setMaterialOff(1)	# also suppress any material it has
		self.camera.setInitialState(initial.getState())

		# need a unique TextureStage to apply this shadow texture to the world.
		self.stage = TextureStage('shadow%i'%self.id)
		# Make sure the shadowing object doesn't get its own shadow
		# applied to it.
		shadowCaster.setTextureOff(self.stage)	# no effect - unnecessary?
		# apply to the object which will receive shadows
		shadowReceiver.clearProjectTexture(self.stage)
		shadowReceiver.projectTexture(self.stage, self.tex, self.cameraPath)

		# what kind of light is it?  Set shadow camera lens to suit.
		if isinstance(lightNode,PointLight):
			# use a wide-angle perspective lens for point light
			# field of view has no effect upon shadow size,
			# just needs to see all shadowCaster
			# but too wide causes pixellated shadows
			self.lens = PerspectiveLens()
			self.lens.setFov(80)
			self.camera.setLens(self.lens)
			self.cameraPath.lookAt(render,shadowCasterCentre)	# need w.r.t. render
		elif isinstance(lightNode,DirectionalLight):
			# Use an orthographic lens for DirectionalLight
			self.lens = OrthographicLens()
			self.lens.setFilmSize(shadowCasterRadius*2.0)
			self.camera.setLens(self.lens)
			# calculate position, using light's direction away from shadowCasterCentre
			# ensure outside shadowCasterRadius (or could adjust clip planes)
			self.cameraPath.setPos(shadowCasterCentre -
				lightNode.getDirection()*shadowCasterRadius*2.0)
			self.cameraPath.lookAt(shadowCasterCentre)
		else:		# Spotlight
			# use the spotlight's lens for the shadow camera
			self.camera.setLens(lightNode.getLens())

		self.camera.showFrustum()	# make Field of View visible
	# __init__()

	def clear(self):
		""" Frees resources & un-does shadow effect. """
		if self.shadowReceiver:
			self.shadowReceiver.clearProjectTexture(self.stage)
			self.shadowReceiver = None

		if self.cameraPath:
			self.cameraPath.detachNode()
			self.cameraPath = None
			self.camera = None
			self.lens = None

		if self.buffer:
			base.graphicsEngine.removeWindow(self.buffer)
			self.tex = None
			self.buffer = None
		print "cleared"
	# clear()
# class ShadowFromLight

if __name__ == '__main__':
	import direct.directbase.DirectStart

	print 'MAXIMUM # OF TEXTURE STAGES (SHADOWS + TEXTURES) :',base.win.getGsg().getMaxTextureStages()
	print '''	if the texture count on the objects you use
	equals the number of texture stages,
	you can't add shadows to them'''

	# shadow-casting object(s)
	panda = loader.loadModel( 'models/panda.egg' )
	panda.setScale(0.5)
	panda.reparentTo( render )
	
	# shadow receiving object(s)
	ground = loader.loadModel( 'models/environment.egg' )
	ground.reparentTo(render)
	ground.setScale(0.25)
	ground.setPos(-8,42,0)
	
	# various lights ...
	#		AmbientLight (no shadows!)
	ambientNode=AmbientLight('ambient')
	ambientNode.setColor( Vec4(.3,.3,.3, 1 ) )
	ambientLight=render.attachNewNode(ambientNode)
	render.setLight(ambientLight)

	#	DirectionalLight - white, dark shadow
	directionalNode=DirectionalLight('directional')
	directionalNode.setColor(Vec4(0.7,0.7,0.7,1))
	directionalNode.setDirection(Vec3(0,-2,-1))		# no setPos()
	directionalLight=render.attachNewNode(directionalNode)
	render.setLight(directionalLight)
	directionalShadow=ShadowFromLight(directionalLight,panda,ground,Vec4(0.3,0.3,0.3,1))

	#	Spotlight - red light, blue shadow
	rotator1=render.attachNewNode('rotator1')
	rotator1.hprInterval(15,Vec3(360,0,0)).loop()
	spotModel=loader.loadModel('SpotLight.egg')
	spotModel.reparentTo(rotator1)
	spotModel.setPos(18,0,12)
	spotModel.lookAt(panda.getBounds().getCenter())
	spotNode=Spotlight('spot')
	spotNode.setColor(Vec4(1,0,0,1))
	spotNode.getLens().setFov( 55 )	#  care: tiny FoV clips shadow, but not lighting!
	spotLight=spotModel.attachNewNode(spotNode)
	render.setLight(spotLight)
	spotShadow=ShadowFromLight(spotLight,panda,ground,Vec4(0,0,1,1))	# blue shadow

	#	PointLight - green light, red shadow
	rotator2=render.attachNewNode('rotator2')
	rotator2.hprInterval(22,Vec3(0,0,0),Vec3(360,0,0)).loop()
	pointModel=loader.loadModel('LightBulb.egg')
	pointModel.reparentTo(rotator2)
	pointModel.setPos(-11,0,11)
	pointNode=PointLight('point')
	pointNode.setColor(Vec4(0,1,0,1))
	pointLight=pointModel.attachNewNode(pointNode)
	render.setLight(pointLight)
	pointShadow=ShadowFromLight(pointLight,panda,ground,Vec4(1,0,0,1))	# red shadow

	# spotShadow.clear()	# to clear shadow & resources, if needed later
	
	camera.setPos(20,-50,55)
	camera.lookAt(panda)
	cameraMat=Mat4(camera.getMat())
	cameraMat.invertInPlace()
	base.mouseInterfaceNode.setMat(cameraMat)

	# see all the render-to-texture buffers
	toggleBufferView()

	run()
#	-end-

Wow, I like these. But: Is there any obvious reason why both examples won’t run in ‘pandagl’ display mode? When using ‘pandadx8’ everything is fine.
Thanks for any suggestion,
Christof.

Edit: To make things clear: Using ‘pandagl’ mode, Python crashes with ‘Fatal Python error: (pygame parachute) Segmentation Fault’
Neither ‘notify-level-glgsg debug’ nor ‘notify-level debug’ gives further information on what’s the causing problem.

You’re right !
but unfortunately not even dx helps here… :stuck_out_tongue:
After some hunts, I found that it’s caused by projectTexture call.

I have heard of this behavior.

But If panda crashes thats usually a library conflict (using the wrong python (like2.5) with panda3d, or using a library which is for a different version of python). What bothers me the most with the errormessage you describe is that it’s a “pygame” segementation fault. Which is actually not used in this example.

I actually have never seen panda3d segfaulting when programming in python. However panda3d 1.4.0 might have some bugs.

By the way. The new shadow example’s are ways better then this one. To see the main problem of this solution, you have to look at the terrain, from where the sun would shine onto the character, you will see that the shadow is applied to all objects in the way of the sunlight.

This doesnt matter greatly if you have a sunlight from above and the terrain is flat. However if you have unevent terrain and the light direction comes at a small angle relative to the terrain you will have a not so nice experience.

Christof – don’t think I can help, being new to this.

However, my code & the original ShadowCaster runs ok with pandagl, both with Panda v 1.4.2 and older v1.3.2. I had hacked the v1.3.2 installation to use Python 2.5 ('cos that’s what comes with Ubuntu 7.04 Linux) and there were no problems apart from a slew of warnings about interface version number mismatches whenever Panda started.
Maybe it is a problem involving “pygame”, as Hypnos suggests.

Coupla wild guesses -

  1. Does OpenGL work under Windows?
    Vague recollections of trying & failing long ago, before I changed to Linux - because unsupported or out of date?
  2. Try installing DX9.c instead of DX8
    That is much better. For example, it happily runs old DX7 games, whereas DX8 won’t.

Hypnos – I quite agree about the strange shadows being cast behind the light as well as in front of it.

However, I’ve just tried the code in
https://discourse.panda3d.org/viewtopic.php?t=3282&highlight=offscreen+buffer
(if that is what you mean by “The new shadow example”).

That has its peculiarities too. If you set “Push-Bias” (whatever that is) above 0.1, then you get the shadow of the teapot on the front of the teapot itself !

The Shadow-Mapping sample released with v1.4.2 won’t run at all for me, and it may be that treeform’s (pro-soft’s?) hack has errors.
Or maybe I am completely misunderstanding.

Is this the same as ynjh_jo’s magnificent
https://discourse.panda3d.org/viewtopic.php?t=2172&highlight=shadow+mapping
, which I’ve yet to try?

I meant the shadow example, from the examples, by one of the new shadow codes, ynjh_jo’s code should also work better.

But this example is afaik the only one not requiring shaders. And it’s about the only one working for myself (osx).