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:
-
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?
-
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.