ShadowNode

This is based on Sothh’s shaderless shadow snippet.

from panda3d.core import *
import direct.directbase.DirectStart
from direct.gui.OnscreenImage import OnscreenImage
from direct.filter.CommonFilters import CommonFilters

class ShadowNode:
	def __init__(self, fromNode=None, toNode=None, bitMask=1, quality=512, useBlur=True, blurriness=0.04, shadowCameraOffset=(2.5,-2.5,20), filmSize=10, shadowColor=(0.6,0.6,0.6,1), shadowClippingRange=20, fovAngle=60):
		"""
		A class which takes a "toNode" and "fromNode" NodePaths and
		projects shadows on the toNode by using fromNode's location.
		
		Can optionally use CommonFilters class to smooth the shadow.
		
		NodePaths can be excluded from casting shadows by hiding them
		with the appropriate BitMask.
		
		ex:
		    NodePath.hide(BitMask32.bit(value))
		    
		By default assumes a DirectionalLight is used with the shadows.
		Use setLightType() to change that.
		"""
		
		self.fromNode = fromNode
		self.toNode = toNode
		self.bitMask = bitMask
		self.useBlur = useBlur
		self.blurriness = blurriness
		self.shadowCameraOffset = shadowCameraOffset
		self.filmSize = filmSize
		self.shadowClippingRange = shadowClippingRange
		self.fovAngle = fovAngle
		
		# create a buffer for storing the shadow texture
		self.shadowBuffer = base.win.makeTextureBuffer("ShadowBuffer", quality, quality)
		self.shadowBuffer.setClearColorActive(True)
		self.shadowBuffer.setClearColor((0,0,0,1))
		
		# create a shadow camera
		self.shadowCamera = base.makeCamera(self.shadowBuffer)
		self.shadowCamera.reparentTo(render)
		
		self.lens = OrthographicLens()
		self.shadowCamera.node().setLens(self.lens)
		
		self.shadowCamera.node().getLens().setFilmSize(self.filmSize)
		
		self.shadowCamera.node().getLens().setNear(1)
		self.shadowCamera.node().getLens().setFar(shadowClippingRange) # use your own Lens otherwise affects main camera too
		
		self.shadowCamera.node().setCameraMask(BitMask32.bit(self.bitMask))
		self.toNode.hide(BitMask32.bit(self.bitMask))
		
		# set shadow color
		self.initial = NodePath("initial")
		self.initial.setColor(1-shadowColor[0], 1-shadowColor[1], 1-shadowColor[2], shadowColor[3])
		self.initial.setTextureOff(2)
		self.initial.setColorScaleOff(2)
		self.initial.setMaterialOff(2)
		self.initial.setLightOff(2)
		self.initial.setFogOff(2)
		self.shadowCamera.node().setInitialState(self.initial.getState())
		
		# the camera's offset from the fromNode affects the shadows direction
		self.shadowCamera.setPos(self.fromNode.getPos() + self.shadowCameraOffset)
		self.shadowCamera.lookAt(self.fromNode, 0,0,0)
		taskMgr.add(self._followCaster, "followCaster", sort=40)
		
		# allow smooth shadows by using CommonFilters and the blur filter
		# very unusual way of using CommonFilters, but still good
		if useBlur == True:
			self.filters = CommonFilters(self.shadowBuffer,self.shadowCamera)
			self.filters.setBlurSharpen(blurriness)
		
		# Texture for shadows
		self.shadowTexture = self.shadowBuffer.getTexture()
		self.shadowTexture.setBorderColor(VBase4(0,0,0,1))
		self.shadowTexture.setWrapU(Texture.WMBorderColor)
		self.shadowTexture.setWrapV(Texture.WMBorderColor)
		
		# TextureStage for the shadow texture
		self.shadowStage = TextureStage("ShadowStage")
		self.shadowStage.setMode(TextureStage.MBlend)
		
		# shadow buffer viewer for debugging
		self.bufferViewer = OnscreenImage(image = self.shadowTexture, pos = (-0.75,0,0.75), scale = 0.2)
		self.bufferViewer.hide()
		
		# project the shadow texture on to the toNode
		self.toNode.projectTexture(self.shadowStage, self.shadowTexture, self.shadowCamera)
		
	def _followCaster(self, task):
		self.shadowCamera.setPos(self.fromNode.getPos() + self.shadowCameraOffset)
		return task.cont
	
	def setFromNode(nodePath):
		"""
		Set the "fromNode", the NodePath the shadow camera is relative to
		"""
		self.fromNode = nodePath
		self.shadowCamera.setPos(self.fromNode.getPos() + self.shadowCameraOffset) # in case toggleUpdatePositions task is disabled
		
	def setToNode(nodePath):
		"""
		Set the "toNode", the NodePath which gets shadows on itself
		"""
		self.toNode.clearProjectTexture(self.shadowStage)
		
		self.toNode.show(BitMask32.bit(self.bitMask)) # undo previous
		self.toNode = nodePath
		self.toNode.hide(BitMask32.bit(self.bitMask))
		
		self.toNode.projectTexture(self.shadowStage, self.shadowTexture, self.shadowCamera)
	
	def changeBitMask(self, value=1):
		"""
		Changes the BitMask value for the toNode.
		You must update other NodePath visibility yourself!
		"""
		self.shadowCamera.node().setCameraMask(BitMask32.bit(self.bitMask))
		self.toNode.show(self.bitMask) # undo previous
		self.bitMask = value
		self.toNode.hide(self.bitMask)
	
	def toggleUpdatePositions(self, state):
		"""
		Toggle updating shadow camera positions with the "fromNode"
		"""
		taskMgr.remove("followCaster")
		if state == True:
			taskMgr.add(self._followCaster, "followCaster", sort=40)
	
	def setLightType(self, type="ortho"):
		"""
		For DirectionalLights Orthograpic lens are used, for other lights
		(Point and Spot) - Perspective
		
		types: "ortho", "perspective"
		"""
		if type == "ortho":
			self.lens = OrthographicLens()
			self.shadowCamera.node().getLens().setFilmSize(self.filmSize)
		elif type == "perspective":
			self.lens = PerspectiveLens()
			self.shadowCamera.node().getLens().setFov(self.fovAngle)
		
		self.shadowCamera.node().setLens(self.lens)
		self.shadowCamera.node().getLens().setNear(1)
		self.shadowCamera.node().getLens().setFar(self.shadowClippingRange)
		
	def setQuality(self, quality=512):
		"""
		Set shadow quality in pixels, power-of-two number
		"""
		self.shadowBuffer.setSize(quality)
	
	def toggleBlur(self, state=True):
		if state == True:
			if self.filters == None:
				self.filters = CommonFilters(self.shadowBuffer,self.shadowCamera)
				self.filters.setBlurSharpen(self.blurriness)
				self.useBlur = True
		else:
			if self.filters != None:
				self.filters.cleanup()
				self.filters = None
				self.useBlur = False
		
	def setBlurriness(self, blurriness=0.04):
		"""
		Set shadow blurriness level.
		A value between 0.0 and 2.0. You can take values smaller than 0.0
		or larger than 2.0, but this usually gives ugly artifacts.
		A value of 0.0 means maximum blur.
		A value of 1.0 does nothing, and if you go past 1.0, the texture
		will be sharpened instead of blurred.
		"""
		self.blurriness = blurriness
		if self.useBlur == True:
			self.filters.setBlurSharpen(self.blurriness)
	
	def setShadowCameraOffset(self, shadowCameraOffset=(2.5,-2.5,20)):
		"""
		Set shadow camera's offset (position) relative to the "fromNode".
		This is what determines the shadow angle.
		"""
		self.shadowCameraOffset = shadowCameraOffset
		self.shadowCamera.setPos(self.fromNode.getPos() + self.shadowCameraOffset)
		self.shadowCamera.lookAt(self.fromNode, 0,0,0)
		
	def setColor(self, shadowColor=(0.6,0.6,0.6,1)):
		"""
		Set shadow color.
		"""
		self.initial.setColor(1-shadowColor[0], 1-shadowColor[1], 1-shadowColor[2], shadowColor[3])
		self.shadowCamera.node().setInitialState(self.initial.getState())
	
	def setClippingRange(self, shadowClippingRange=20):
		"""
		Set clipping range of the shadow camera, if the geometry of
		"toNode" is further than that, shadows aren't casted.
		Useful for multiple floor buildings or dungeons.
		
		Only if you're using a PerspectiveLens.
		"""	
		self.shadowClippingRange = shadowClippingRange
		self.shadowCamera.node().getLens().setFar(self.shadowClippingRange) # use your own Lens otherwise affects main camera too
	
	def setAreaSize(self, size=10):
		"""
		Only useful with Orthographic lens. For Perspective lens change
		ShadowCamera's (z) offset instead.
		"""
		self.filmSize = size
		self.shadowCamera.node().getLens().setFilmSize(self.filmSize)
	
	def setShadowFovAngle(self, fovAngle=60):
		"""
		Only useful with Orthographic lens
		"""
		self.fovAngle = fovAngle
		self.shadowCamera.node().getLens().setFov(self.fovAngle)
	
	def toggleBufferViewer(self, state=True):
		"""
		Toggle the debug shadow buffer viewer
		"""
		if state == True:
			self.bufferViewer.show()
		else:
			self.bufferViewer.hide()
	
	def cleanup(self):
		"""
		Call this before destroying the instance
		"""
		self.toNode.clearProjectTexture(self.shadowStage)
		self.toNode.show(BitMask32.bit(self.bitMask))
		taskMgr.remove("followCaster")
		self.shadowCamera.removeNode()
		if self.filters != None: self.filters.cleanup()
		self.bufferViewer.destroy()

There are 2 problems now:

  1. Near clipping doesn’t work. If you’ll stand under a tree, the top of the tree will get the player’s shadow. Any ideas?

  2. If you have set it to get updated with the fromNode, the program often hangs when something gets culled, then rendered again. Looks like something is being regenerated, but not sure what.