ShadowNode

Return to Code Snippets

ShadowNode

Postby preusser » Tue Jul 10, 2012 12:07 pm

This is based on Sothh's shaderless shadow snippet.

Code: Select all
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.

Image
preusser
 
Posts: 516
Joined: Sun Mar 27, 2011 10:07 am

Return to Code Snippets

Who is online

Users browsing this forum: No registered users and 0 guests