shadow.py


some stencil shadow code

"""
                    Shadow.py    
    by
        ___ ____ ____ ____ ____ ____ ____ _  _ 
         |  |__/ |___ |___ |___ |  | |__/ |\/| 
         |  |  \ |___ |___ |    |__| |  \ |  | 
                    
    Directions: 
                                      
    #create ShadowSystem before you draw
    ShadowSystem() 
    #to attach shadow to one or more objects and ligths  
    Shadow(object,light)
    # genereate a shadow node
    shnode = sh.generate()
    # reparent it to the render
    shnode.reparentTo(render)
"""


from pandac.PandaModules import loadPrcFileData
loadPrcFileData("","""
framebuffer-stencil #t
stencil-bits 8
""")

import direct.directbase.DirectStart
from pandac.PandaModules import CardMaker, NodePath, Vec3, Vec4, Point3
from pandac.PandaModules import GeomVertexReader,GeomVertexData, GeomVertexWriter, GeomTriangles, Geom, GeomNode, GeomVertexFormat
from pandac.PandaModules import StencilAttrib,ColorWriteAttrib,DepthWriteAttrib
from pandac.PandaModules import CullFaceAttrib

def ShadowSystem(shadowColor = Vec4(0,0,0,.3)):    
    """ creates an overly screen wich is drawn last wich is our shadow""" 
    screen = StencilAttrib.make(
         1,
         StencilAttrib.SCFNotEqual,
         StencilAttrib.SOKeep,
         StencilAttrib.SOKeep,
         StencilAttrib.SOKeep,
         0,
         0xFFFFFFFF,
         0xFFFFFFFF)
    cm = CardMaker("screener")
    cm.setFrame(-1,1,-1,1)
    node =render2d.attachNewNode( cm.generate())
    node.setAttrib(screen)
    node.setColor(shadowColor)
    node.setTransparency(True)
    node.setBin("unsorted",0)
    

class Connectivity:
    """ connectivity class which will be useful for many algorithms """ 
    
    class Face:
        """ logic face """
        vertexes = []
        plane = None
        edges = []
        neighbours = []
        visible = True
        
    class Plane:
        """ plane equation """
        a = 0 
        b = 0
        c = 0
        d = 0
    
    def __init__(self,object):        
        """ creates connectivity per objects basis """        
        self.object = object
        self.faces = []
        self.process()
        self.computeConnectivity()
         
    def computeConnectivityOnFaseList(self,faceList):
        """ computes conectivey """
        # for every face in the list see if there is an edge
        # that matches on another face, then they are neighbours
        for faceA in faceList:             
            for e1,edge1 in enumerate(faceA.edges):
                if faceA.neighbours[e1] == None:
                    for faceB in faceList:
                        if faceB != faceA:
                            next = False
                            for e2,edge2 in enumerate(faceB.edges):
                                if edge1[0] == edge2[0] and edge1[1] == edge2[1]:
                                    faceA.neighbours[e1] = faceB
                                    faceB.neighbours[e2] = faceA
                                    next = True
                                    break
                                elif edge1[1] == edge2[0] and edge1[0] == edge2[1]:
                                    faceA.neighbours[e1] = faceB
                                    faceB.neighbours[e2] = faceA
                                    next = True
                                    break
                            if next: break
            
    def computeConnectivity(self):
        """ now and little more compiles one """
        i = 0
        mach = {}
        # puts every vertex into a mach tabe inside a 
        # list with faces that share that vertex 
        for face in self.faces:  
            for vertex in face.vertexes:
                vd =  int(vertex[0]*1000),int(vertex[1]*1000),int(vertex[2]*1000)
                if vd in mach:
                    mach[vd].append(face)
                else:
                    mach[vd] = [face]
        # now look through the list and see if any face matches    
        for matches in mach.itervalues(): 
            if len(matches) > 1:
                self.computeConnectivityOnFaseList(matches)
          
    def process(self):
        self.faces = []
        geomNodeCollection = self.object.findAllMatches('**/+GeomNode')
        for nodePath in geomNodeCollection.asList():
            geomNode = nodePath.node()
            self.processGeomNode(geomNode)
          
          
    def processGeomNode(self,geomNode):        
        for i in range(geomNode.getNumGeoms()):
            geom = geomNode.getGeom(i)
            state = geomNode.getGeomState(i)
            self.processGeom(geom)
                
    def processGeom(self,geom):
        vdata = geom.getVertexData()
        for i in range(geom.getNumPrimitives()):
            prim = geom.getPrimitive(i)
            self.processPrimitive(prim, vdata)  
            
    def processPrimitive(self,prim, vdata):
        vertex = GeomVertexReader(vdata, 'vertex')
        prim = prim.decompose()
        for p in range(prim.getNumPrimitives()):
            s = prim.getPrimitiveStart(p)
            e = prim.getPrimitiveEnd(p)
            vertexes = []
            for i in range(s, e):
                vi = prim.getVertex(i)
                vertex.setRow(vi)
                v = vertex.getData3f()
                vertexes.append(v)
            face = self.Face()
            face.vertexes = vertexes
            face.plane = self.Plane()
            face.plane.a = vertexes[0][1]*(vertexes[1][2]-vertexes[2][2]) + vertexes[1][1]*(vertexes[2][2]-vertexes[0][2]) + vertexes[2][1]*(vertexes[0][2]-vertexes[1][2]);
            face.plane.b = vertexes[0][2]*(vertexes[1][0]-vertexes[2][0]) + vertexes[1][2]*(vertexes[2][0]-vertexes[0][0]) + vertexes[2][2]*(vertexes[0][0]-vertexes[1][0]);
            face.plane.c = vertexes[0][0]*(vertexes[1][1]-vertexes[2][1]) + vertexes[1][0]*(vertexes[2][1]-vertexes[0][1]) + vertexes[2][0]*(vertexes[0][1]-vertexes[1][1]);
            face.plane.d = -( vertexes[0][0]*( vertexes[1][1]*vertexes[2][2] - vertexes[2][1]*vertexes[1][2] ) +
                vertexes[1][0]*(vertexes[2][1]*vertexes[0][2] - vertexes[0][1]*vertexes[2][2]) +
                vertexes[2][0]*(vertexes[0][1]*vertexes[1][2] - vertexes[1][1]*vertexes[0][2]) );
            face.edges = [(vertexes[0],vertexes[1]),(vertexes[1],vertexes[2]),(vertexes[2],vertexes[0])]
            face.neighbours = [None,None,None]
            self.faces.append(face)

class Shadow:
    """ main class of the shadow """
    
    frontSide = StencilAttrib.make(
                1,
                StencilAttrib.SCFAlways,
                StencilAttrib.SOKeep,
                StencilAttrib.SOKeep,
                StencilAttrib.SOIncrement,
                0xFFFFFFFF,
                0xFFFFFFFF,
                0xFFFFFFFF)

    backSide = StencilAttrib.make(
                1,
                StencilAttrib.SCFAlways,
                StencilAttrib.SOKeep,
                StencilAttrib.SOKeep,
                StencilAttrib.SODecrement,
                0xFFFFFFFF,
                0xFFFFFFFF,
                0xFFFFFFFF)

    def __init__(self,object,light):
        """ connect the object with the light """
        self.object = object
        self.light = light
        self.con = Connectivity(object)
        self.faces = self.con.faces
                 
    def lightFaces(self):
        """ runs through all the faces and see if they are lit """
        l = self.light.getPos(self.object)
        for face in self.faces:
            side = (face.plane.a*l[0]+
                face.plane.b*l[1]+
                face.plane.c*l[2]+
                face.plane.d)
            print side
            if side > 0 :
                face.visible = True
            else:
                face.visible = False
                 
    def generate(self):
        """ generate a shadow volume based on the light and the object """
        self.lightFaces()
        l = self.light.getPos(self.object)
        vdata = GeomVertexData('shadow', GeomVertexFormat.getV3() , Geom.UHStatic)
        vertex = GeomVertexWriter(vdata, 'vertex')
        number = 0
        for face in  self.faces:
            if face.visible:
                for e,edge in enumerate(face.edges):
                    if face.neighbours[e] == None or not face.neighbours[e].visible:
                        v1 = edge[0]
                        v2 = v1+(v1-l)*10
                        v3 = edge[1]
                        v4 = v3+(v3-l)*10
                        vertex.addData3f(v1)
                        vertex.addData3f(v2)
                        vertex.addData3f(v3)
                        vertex.addData3f(v3)
                        vertex.addData3f(v2)
                        vertex.addData3f(v4)
                        number = number + 2
        prim = GeomTriangles(Geom.UHStatic)
        for n in range(number):
            prim.addVertices(n*3,n*3+1,n*3+2)
        prim.closePrimitive()
        geom = Geom(vdata)
        geom.addPrimitive(prim)
        geomnode = GeomNode('gnode')
        geomnode.addGeom(geom)
        # make the 1st pass        
        self.front = NodePath("front")
        self.front.attachNewNode(geomnode)
        self.front.setColor(0,0,0,.1)
        self.front.setAttrib( ColorWriteAttrib.make(ColorWriteAttrib.MOff) )
        self.front.setAttrib( CullFaceAttrib.make() )
        self.front.setAttrib( DepthWriteAttrib.make(DepthWriteAttrib.MOff ) )
        self.front.setAttrib( self.frontSide )
        self.front.reparentTo(self.object)
        self.front.setBin("fixed",1)
        # make the second pass
        self.back = NodePath("back")
        self.back.attachNewNode(geomnode)
        self.back.setColor(0,0,0,.1)
        self.back.setAttrib( ColorWriteAttrib.make(ColorWriteAttrib.MOff) )
        self.back.setAttrib( CullFaceAttrib.makeReverse() )
        self.back.setAttrib( DepthWriteAttrib.make(DepthWriteAttrib.MOff ) )
        self.back.setAttrib( self.backSide )
        self.back.reparentTo(self.object)
        self.back.setBin("fixed",2)
        return NodePath("shadow of %s"%object)


if __name__ == "__main__":
    # creae fore
    cm = CardMaker("back")
    cm.setFrame(10,-10,-10,10)
    cmNode = render.attachNewNode(cm.generate())
    cmNode.setR(180)
    # load teapot
    object = loader.loadModelCopy('teapot')  
    object.reparentTo(render)
    object.setPos(0,0,0)
    object.setScale(.7,.7,.7)
    object.setP(-90)     
    base.cam.setPos(10,10,-5)
    base.cam.lookAt(Point3(0,0,0),Vec3(0,1,0))   
    # set up shadow system
    ShadowSystem()
    # create light 
    light = NodePath("light")
    light.setPos(6,6,-6)
    # attach shadow to object and light
    sh = Shadow(object,light)
    # generate a shadow object 
    shnode = sh.generate()
    # reparent it to the frame
    shnode.reparentTo(render)
    run()

warning the algorithm is still pretty slow and will choke on very complex geometry

any help with bugs and suggestions is very welcome!
Thanks!
-treeform

finally, but have you tried to use z fail instead of z pass ?
But I guess you never need to go inside the shadow, right ?

i am working on fixing the artifacts caused by the complex models. That would be a consideration. Yes stepping inside the shadow also works for simple test objects.

The reason it took so long is that i deleted the original shadow system in a system reformat and had to rewrite it again from memory.

Thanks for sharing this awesome code Treeform!

I just have to add shading myself.

well shading was not added because i wanted this as simple as possible and to demonstrate only the stencil techniques.

Wow, that’s really, really ambitious. I’m impressed.

One thing you might consider doing is putting the shadow extrusion on the GPU. That way, you could just construct the shadow geometry once, and then as the model moves around, you could let the GPU do the extrusion.

yes that would be an improvement but i want to get my math right with simple stuff 1st. And it does not look like it is working on more complex shapes. The other hard part is only the border of the shadow is drawn and i compute lit/unlit triangles and i have no clue how to do that in the GPU.

thinking about it i could add extra column to the vertex buffer and use that for lit/unlit and connectivity computation.

I haven’t read your code closely, but most shadow volume codes require a perfect mesh — ie, a sealed volume. Does yours?

I ask because I know that the utah teapot is not a sealed volume.

no it does not ‘require’ but it works best with :slight_smile:. The tea pot still looks good even though few holes in it. While the Katana fighter-boomer in the last screen has 100ds of edges that is why it breaks my algorithm.

becuz u made this script, mr panda doesnt have shadows built in? ö

no mr panda does not have stancil shadows built in

why not? only projected shadows? Ö

i dont know, because no one made stencil shadows? I am trying to but they don’t work well on non closed objects.

would this lag on 2000 poly soldiers?

Depends on the number of soldiers and the environment, but yes, quite possibly.

A standard technique with stencil shadows is to create shadow volumes using lower LOD (level of detail) models than the models rendered directly, because a shadow does not need to have nearly the detail of a normal model to still look good.

You have to be careful to avoid bad self shadowing though, because a low LOD polygon might stick out of the space the higher LOD model occupies, causing an object to shadow itself where it isn’t supposed. One way to work around this is to ensure that all model LODs used in shadowing fit inside higher LODs, though I’m not sure how easy this is to acheive with 3D modeling software. There are probably other techniques to avoid bad self shadowing, but I’m not sure what these would be.

how would i make it shadow a lod instead of the original