Procedural Trees

Return to Code Snippets

Procedural Trees

Postby Praios » Sun Dec 12, 2010 3:57 am

I took the code from The Fractal Plants Sample Program, removed the inside-out-bug and made it into a class.
Although it is based on the example-code most of the code is rewritten.
Now its much more flexible, I think, and you can grow trees step-by-step.
Code: Select all
'''
Created on 11.12.2010
Based on Kwasi Mensah's (kmensah@andrew.cmu.edu) "The Fractal Plants Sample Program" from 8/05/2005
@author: Praios

'''
from panda3d.core import NodePath, Geom, GeomNode, GeomVertexArrayFormat, TransformState, GeomVertexWriter, GeomTristrips, GeomVertexRewriter, GeomVertexReader, GeomVertexData, GeomVertexFormat, InternalName
from panda3d.core import Mat4, Vec3, Vec4, CollisionNode, CollisionTube, Point3
import math, random

#this is for making the tree not too straight
def _randomAxis(vecList, scale=2):
    fwd = vecList[0]
    perp1 = vecList[1]   
    perp2 = vecList[2]
    nfwd = fwd + perp1 * (scale * random.random() - scale / 2.0) + perp2 * (scale * random.random() - scale / 2.0)
    nfwd.normalize()
    nperp2 = nfwd.cross(perp1)
    nperp2.normalize()
    nperp1 = nfwd.cross(nperp2)
    nperp1.normalize()
    return [nfwd, nperp1, nperp2]

#this is for branching
def _angleRandomAxis(vecList, angle):
    fwd = vecList[0]
    perp1 = vecList[1]   
    perp2 = vecList[2]
    nangle = angle + math.pi * (0.125 * random.random() - 0.0625)
    nfwd = fwd * (0.5 + 2 * random.random()) + perp1 * math.sin(nangle) + perp2 * math.cos(nangle)
    nfwd.normalize()
    nperp2 = nfwd.cross(perp1)
    nperp2.normalize()
    nperp1 = nfwd.cross(nperp2)
    nperp1.normalize()
    return [nfwd, nperp1, nperp2]
   
class FractalTree(NodePath):
    __format = None
    def __init__(self, barkTexture, leafModel, lengthList, numCopiesList, radiusList):
        NodePath.__init__(self, "Tree Holder")
        self.numPrimitives = 0
        self.leafModel = leafModel
        self.barkTexture = barkTexture
        self.bodies = NodePath("Bodies")
        self.leaves = NodePath("Leaves")
        self.coll = self.attachNewNode(CollisionNode("Collision"))   
        self.bodydata = GeomVertexData("body vertices", self.__format, Geom.UHStatic)
        numCopiesList = list(numCopiesList)
        self.numCopiesList = numCopiesList
        lengthList = list(lengthList)
        radiusList = list(radiusList)
        self.radiusList = radiusList
        self.lengthList = lengthList
        self.iterations = 1
        self.makeEnds()
        self.makeFromStack(True)
        #self.coll.show()       
        self.bodies.setTexture(barkTexture)
        self.coll.reparentTo(self)
        self.bodies.reparentTo(self)
        self.leaves.reparentTo(self)
       
    #this makes a flattened version of the tree for faster rendering...
    def getStatic(self):
        np = NodePath(self.node().copySubgraph())

        np.flattenStrong()
        return np       
   
    #this should make only one instance
    @classmethod
    def makeFMT(cls):
        if cls.__format is not None:
            return
        formatArray = GeomVertexArrayFormat()
        formatArray.addColumn(InternalName.make("drawFlag"), 1, Geom.NTUint8, Geom.COther)   
        format = GeomVertexFormat(GeomVertexFormat.getV3n3t2())
        format.addArray(formatArray)
        cls.__format = GeomVertexFormat.registerFormat(format)
   
    def makeEnds(self, pos=Vec3(0, 0, 0), vecList=[Vec3(0, 0, 1), Vec3(1, 0, 0), Vec3(0, -1, 0)]):
        self.ends = [(pos, vecList, 0)]
       
    def makeFromStack(self, makeColl=False):
        stack = self.ends
        to = self.iterations
        lengthList = self.lengthList
        numCopiesList = self.numCopiesList
        radiusList = self.radiusList
        ends = []
        while stack:
            pos, vecList, depth = stack.pop()
            length = lengthList[depth]
            if depth != to and depth + 1 < len(lengthList):               
                self.drawBody(pos, vecList, radiusList[depth])     
                #move foward along the right axis
                newPos = pos + vecList[0] * length.length()
                if makeColl:
                    self.makeColl(pos, newPos, radiusList[depth])
                numCopies = numCopiesList[depth] 
                if numCopies:       
                    for i in xrange(numCopies):
                        stack.append((newPos, _angleRandomAxis(vecList, 2 * math.pi * i / numCopies), depth + 1))
                        #stack.append((newPos, _randomAxis(vecList,3), depth + 1))
                else:
                    #just make another branch connected to this one with a small variation in direction
                    stack.append((newPos, _randomAxis(vecList, 0.25), depth + 1))
            else:
                ends.append((pos, vecList, depth))
                self.drawBody(pos, vecList, radiusList[depth], False)
                self.drawLeaf(pos, vecList)
        self.ends = ends
       
    def makeColl(self, pos, newPos, radius):
        tube = CollisionTube(Point3(pos), Point3(newPos), radius)
        self.coll.node().addSolid(tube)
         
    #this draws the body of the tree. This draws a ring of vertices and connects the rings with
    #triangles to form the body.
    #this keepDrawing paramter tells the function wheter or not we're at an end
    #if the vertices before you were an end, dont draw branches to it
    def drawBody(self, pos, vecList, radius=1, keepDrawing=True, numVertices=16):
        vdata = self.bodydata
        circleGeom = Geom(vdata)
        vertWriter = GeomVertexWriter(vdata, "vertex")
        #colorWriter = GeomVertexWriter(vdata, "color")
        normalWriter = GeomVertexWriter(vdata, "normal")
        drawReWriter = GeomVertexRewriter(vdata, "drawFlag")
        texReWriter = GeomVertexRewriter(vdata, "texcoord")
        startRow = vdata.getNumRows()
        vertWriter.setRow(startRow)
        #colorWriter.setRow(startRow)
        normalWriter.setRow(startRow)       
        sCoord = 0   
        if (startRow != 0):
            texReWriter.setRow(startRow - numVertices)
            sCoord = texReWriter.getData2f().getX() + 1           
            drawReWriter.setRow(startRow - numVertices)
            if(drawReWriter.getData1f() == False):
                sCoord -= 1
        drawReWriter.setRow(startRow)
        texReWriter.setRow(startRow)   
        angleSlice = 2 * math.pi / numVertices
        currAngle = 0
        #axisAdj=Mat4.rotateMat(45, axis)*Mat4.scaleMat(radius)*Mat4.translateMat(pos)
        perp1 = vecList[1]
        perp2 = vecList[2]     
        #vertex information is written here
        for i in xrange(numVertices):
            adjCircle = pos + (perp1 * math.cos(currAngle) + perp2 * math.sin(currAngle)) * radius
            normal = perp1 * math.cos(currAngle) + perp2 * math.sin(currAngle)       
            normalWriter.addData3f(normal)
            vertWriter.addData3f(adjCircle)
            texReWriter.addData2f(sCoord, (i + 0.001) / (numVertices - 1))
            #colorWriter.addData4f(0.5, 0.5, 0.5, 1)
            drawReWriter.addData1f(keepDrawing)
            currAngle += angleSlice 
        drawReader = GeomVertexReader(vdata, "drawFlag")
        drawReader.setRow(startRow - numVertices)   
        #we cant draw quads directly so we use Tristrips
        if (startRow != 0) and (drawReader.getData1f() != False):
            lines = GeomTristrips(Geom.UHStatic)
            half = numVertices / 2
            lines.addVertex(startRow)
            lines.addVertex(startRow - half)           
            for i in xrange(numVertices - 1, -1, -1):
                lines.addVertex(i + startRow)
                if i < half:
                    lines.addVertex(i + startRow - half)
                else:
                    lines.addVertex(i + startRow - half - numVertices)
            lines.closePrimitive()
            lines.decompose()
            circleGeom.addPrimitive(lines)           
            circleGeomNode = GeomNode("Debug")
            circleGeomNode.addGeom(circleGeom)   
            self.numPrimitives += numVertices * 2
            self.bodies.attachNewNode(circleGeomNode)
   
    #this draws leafs when we reach an end       
    def drawLeaf(self, pos=Vec3(0, 0, 0), vecList=[Vec3(0, 0, 1), Vec3(1, 0, 0), Vec3(0, -1, 0)], scale=0.125):
        #use the vectors that describe the direction the branch grows to make the right
        #rotation matrix
        newCs = Mat4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
        newCs.setRow(0, vecList[2]) #right
        newCs.setRow(1, vecList[1]) #up
        newCs.setRow(2, vecList[0]) #forward
        newCs.setRow(3, Vec3(0, 0, 0))
        newCs.setCol(3, Vec4(0, 0, 0, 1))   
        axisAdj = Mat4.scaleMat(scale) * newCs * Mat4.translateMat(pos)       
        leafModel = NodePath("leaf")
        self.leafModel.instanceTo(leafModel)
        leafModel.reparentTo(self.leaves)
        leafModel.setTransform(TransformState.makeMat(axisAdj))

       
    def grow(self, num=1, removeLeaves=True, leavesScale=1):
        self.iterations += num
        while num > 0:
            self.setScale(self, 1.1)
            self.leafModel.setScale(self.leafModel, leavesScale / 1.1)
            if removeLeaves:
                for c in self.leaves.getChildren():
                    c.removeNode()
            self.makeFromStack()
            self.bodies.setTexture(self.barkTexture)         
            num -= 1
FractalTree.makeFMT()


class DefaultTree(FractalTree):
    def __init__(self):       
        barkTexture = base.loader.loadTexture("models/tree/barkTexture.jpg")
        leafModel = base.loader.loadModel('models/tree/shrubbery')
        leafModel.clearModelNodes()
        leafModel.flattenStrong()
        leafTexture = base.loader.loadTexture('models/tree/material-10-cl.png')
        leafModel.setTexture(leafTexture, 1)       
        lengthList = self.makeLengthList(Vec3(1, 1, 1), 64)
        numCopiesList = self.makeNumCopiesList(4, 3, 64)
        radiusList = self.makeRadiusList(0.5, 64, numCopiesList)
        FractalTree.__init__(self, barkTexture, leafModel, lengthList, numCopiesList, radiusList)
       
    @staticmethod
    def makeRadiusList(radius, iterations, numCopiesList, scale=1.5):
        l = [radius]
        for i in xrange(1, iterations):
            #if i % 3 == 0:
            if i != 1 and numCopiesList[i - 2]:
                radius /= numCopiesList[i - 2] ** 0.5
            else:
                radius /= scale ** (1.0 / 3)
            l.append(radius)
        return l
   
    @staticmethod
    def makeLengthList(length, iterations, sx=2.0, sy=2.0, sz=1.25):
        l = [length]
        for i in xrange(1, iterations):
            #if i % 3 == 0:
                #decrease dimensions when we branch
                #length = Vec3(length.getX() / 2, length.getY() / 2, length.getZ() / 1.1)
            length = Vec3(length.getX() / sx ** (1.0 / 3), length.getY() / sy ** (1.0 / 3), length.getZ() / sz ** (1.0 / 3))
            l.append(length)
        return l
   
    @staticmethod
    def makeNumCopiesList(numCopies, branchat, iterations):
        l = list()
        for i in xrange(iterations):
            if i % int(branchat) == 0:
                l.append(numCopies)
            else:
                l.append(0)
        return l

#this grows a tree
if __name__ == "__main__":
    from direct.showbase.ShowBase import ShowBase
    base = ShowBase()
    base.cam.setPos(0, -80, 10)
    t = DefaultTree()
    t.reparentTo(base.render)
    #make an optimized snapshot of the current tree
    np = t.getStatic()
    np.setPos(10, 10, 0)
    np.reparentTo(base.render)
    #demonstrate growing
    last = [0] # a bit hacky
    def grow(task):
        if task.time > last[0] + 1:
            t.grow()
            last[0] = task.time
            #t.leaves.detachNode()
        if last[0] > 10:
            return task.done
        return task.cont
    base.taskMgr.add(grow, "growTask")
    base.run()

edit: asynchronous flattening makes trouble - removed
Last edited by Praios on Mon Jan 10, 2011 5:37 am, edited 1 time in total.
What am I actually talking about? - I don't know...
User avatar
Praios
 
Posts: 234
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

Postby Praios » Mon Jan 10, 2011 4:54 am

I forgot to say you need the stuff from the example. Put it in "models/tree/".
What am I actually talking about? - I don't know...
User avatar
Praios
 
Posts: 234
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

Postby Craig » Thu Feb 17, 2011 8:43 pm

Major improvement. Very nice.

There are a lot of things (mostly from the original version) that seem odd to me. The random bending methods invert the sign on 2 of the vectors they return for example. Also, the use of lists of orthogonal vectors when a mat or quat would work seems like a poor choice. Also, the bark texture goes the wrong way, and has a vertical seam in the UV, so have my fixed version: (I'm sure I broke some things, it lacks testing)

Code: Select all
'''
Created on 11.12.2010
Based on Kwasi Mensah's (kmensah@andrew.cmu.edu) "The Fractal Plants Sample Program" from 8/05/2005
@author: Praios

Edited by Craig Macomber

'''
from panda3d.core import NodePath, Geom, GeomNode, GeomVertexArrayFormat, TransformState, GeomVertexWriter, GeomTristrips, GeomVertexRewriter, GeomVertexReader, GeomVertexData, GeomVertexFormat, InternalName
from panda3d.core import Mat4, Vec3, Vec4, CollisionNode, CollisionTube, Point3, Quat
import math, random

#this is for making the tree not too straight
def _randomBend(inQuat, maxAngle=20):
    q=Quat()
    angle=random.random()*2*math.pi
   
    #power of 2 here makes distrobution even withint a circle
    # (makes larger bends are more likley as they are further spread)
    ammount=(random.random()**2)*maxAngle
    q.setHpr((math.sin(angle)*ammount,math.cos(angle)*ammount,0))
    return inQuat*q

# TODO : This needs to get updated to work with quat. Using tmp hack.
def _angleRandomAxis(quat, angle):
#     fwd = quat.getUp()
#     perp1 = quat.getRight()   
#     perp2 = quat.getForward() 
#     nangle = angle + math.pi * (0.125 * random.random() - 0.0625)
#     nfwd = fwd * (0.5 + 2 * random.random()) + perp1 * math.sin(nangle) + perp2 * math.cos(nangle)
#     nfwd.normalize()
#     nperp2 = nfwd.cross(perp1)
#     nperp2.normalize()
#     nperp1 = nfwd.cross(nperp2)
#     nperp1.normalize()
    return _randomBend(quat,60)


class FractalTree(NodePath):
    __format = None
    def __init__(self, barkTexture, leafModel, lengthList, numCopiesList, radiusList):
        NodePath.__init__(self, "Tree Holder")
        self.numPrimitives = 0
        self.leafModel = leafModel
        self.barkTexture = barkTexture
        self.bodies = NodePath("Bodies")
        self.leaves = NodePath("Leaves")
        self.coll = self.attachNewNode(CollisionNode("Collision"))   
        self.bodydata = GeomVertexData("body vertices", self.__format, Geom.UHStatic)
        self.numCopiesList = list(numCopiesList)   
        self.radiusList = list(radiusList) 
        self.lengthList = list(lengthList) 
        self.iterations = 1
       
        self.makeEnds()
        self.makeFromStack(True)
        #self.coll.show()       
        self.bodies.setTexture(barkTexture)
        self.coll.reparentTo(self)
        self.bodies.reparentTo(self)
        self.leaves.reparentTo(self)
       
    #this makes a flattened version of the tree for faster rendering...
    def getStatic(self):
        np = NodePath(self.node().copySubgraph())

        np.flattenStrong()
        return np       
   
    #this should make only one instance
    @classmethod
    def makeFMT(cls):
        if cls.__format is not None:
            return
        formatArray = GeomVertexArrayFormat()
        formatArray.addColumn(InternalName.make("drawFlag"), 1, Geom.NTUint8, Geom.COther)   
        format = GeomVertexFormat(GeomVertexFormat.getV3n3t2())
        format.addArray(formatArray)
        cls.__format = GeomVertexFormat.registerFormat(format)
   
    def makeEnds(self, pos=Vec3(0, 0, 0), quat=None):
        if quat is None: quat=Quat()
        self.ends = [(pos, quat, 0)]
       
    def makeFromStack(self, makeColl=False):
        stack = self.ends
        to = self.iterations
        lengthList = self.lengthList
        numCopiesList = self.numCopiesList
        radiusList = self.radiusList
        ends = []
        while stack:
            pos, quat, depth = stack.pop()
            length = lengthList[depth]
            if depth != to and depth + 1 < len(lengthList):               
                self.drawBody(pos, quat, radiusList[depth])     
                #move foward along the right axis
                newPos = pos + quat.getUp() * length.length()
                if makeColl:
                    self.makeColl(pos, newPos, radiusList[depth])
                numCopies = numCopiesList[depth] 
                if numCopies:       
                    for i in xrange(numCopies):
                        stack.append((newPos, _angleRandomAxis(quat, 2 * math.pi * i / numCopies), depth + 1))
                        #stack.append((newPos, _randomAxis(vecList,3), depth + 1))
                else:
                    #just make another branch connected to this one with a small variation in direction
                    stack.append((newPos, _randomBend(quat, 20), depth + 1))
            else:
                ends.append((pos, quat, depth))
                self.drawBody(pos, quat, radiusList[depth], False)
                self.drawLeaf(pos, quat)
        self.ends = ends
       
    def makeColl(self, pos, newPos, radius):
        tube = CollisionTube(Point3(pos), Point3(newPos), radius)
        self.coll.node().addSolid(tube)
         
    #this draws the body of the tree. This draws a ring of vertices and connects the rings with
    #triangles to form the body.
    #this keepDrawing paramter tells the function wheter or not we're at an end
    #if the vertices before you were an end, dont draw branches to it
    def drawBody(self, pos, quat, radius=1, keepDrawing=True, numVertices=16):
        vdata = self.bodydata
        circleGeom = Geom(vdata)
        vertWriter = GeomVertexWriter(vdata, "vertex")
        #colorWriter = GeomVertexWriter(vdata, "color")
        normalWriter = GeomVertexWriter(vdata, "normal")
        drawReWriter = GeomVertexRewriter(vdata, "drawFlag")
        texReWriter = GeomVertexRewriter(vdata, "texcoord")
        startRow = vdata.getNumRows()
        vertWriter.setRow(startRow)
        #colorWriter.setRow(startRow)
        normalWriter.setRow(startRow)       
        sCoord = 0   
        if (startRow != 0):
            texReWriter.setRow(startRow - numVertices)
            sCoord = texReWriter.getData2f().getX() + 1           
            drawReWriter.setRow(startRow - numVertices)
            if(drawReWriter.getData1f() == False):
                sCoord -= 1
        drawReWriter.setRow(startRow)
        texReWriter.setRow(startRow)   
       
        angleSlice = 2 * math.pi / numVertices
        currAngle = 0
        #axisAdj=Mat4.rotateMat(45, axis)*Mat4.scaleMat(radius)*Mat4.translateMat(pos)
        perp1 = quat.getRight()
        perp2 = quat.getForward()   
        #vertex information is written here
        for i in xrange(numVertices+1): #doubles the last vertex to fix UV seam
            adjCircle = pos + (perp1 * math.cos(currAngle) + perp2 * math.sin(currAngle)) * radius
            normal = perp1 * math.cos(currAngle) + perp2 * math.sin(currAngle)       
            normalWriter.addData3f(normal)
            vertWriter.addData3f(adjCircle)
            texReWriter.addData2f(1.0*i / numVertices,sCoord)
            #colorWriter.addData4f(0.5, 0.5, 0.5, 1)
            drawReWriter.addData1f(keepDrawing)
            currAngle += angleSlice 
        drawReader = GeomVertexReader(vdata, "drawFlag")
        drawReader.setRow(startRow - numVertices)   
        #we cant draw quads directly so we use Tristrips
        if (startRow != 0) and (drawReader.getData1f() != False):
            lines = GeomTristrips(Geom.UHStatic)         
            for i in xrange(numVertices+1):
                lines.addVertex(i + startRow)
                lines.addVertex(i + startRow - numVertices-1)
            lines.addVertex(startRow)
            lines.addVertex(startRow - numVertices)
            lines.closePrimitive()
            #lines.decompose()
            circleGeom.addPrimitive(lines)           
            circleGeomNode = GeomNode("Debug")
            circleGeomNode.addGeom(circleGeom)   
            self.numPrimitives += numVertices * 2
            self.bodies.attachNewNode(circleGeomNode)
   
    #this draws leafs when we reach an end       
    def drawLeaf(self, pos=Vec3(0, 0, 0), quat=None, scale=0.125):
        #use the vectors that describe the direction the branch grows to make the right
        #rotation matrix
        newCs = Mat4()#0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
#         newCs.setRow(0, vecList[2]) #right
#         newCs.setRow(1, vecList[1]) #up
#         newCs.setRow(2, vecList[0]) #forward
#         newCs.setRow(3, Vec3(0, 0, 0))
#         newCs.setCol(3, Vec4(0, 0, 0, 1))   
        quat.extractToMatrix(newCs)
        axisAdj = Mat4.scaleMat(scale) * newCs * Mat4.translateMat(pos)       
        leafModel = NodePath("leaf")
        self.leafModel.instanceTo(leafModel)
        leafModel.reparentTo(self.leaves)
        leafModel.setTransform(TransformState.makeMat(axisAdj))

       
    def grow(self, num=1, removeLeaves=True, leavesScale=1):
        self.iterations += num
        while num > 0:
            self.setScale(self, 1.1)
            self.leafModel.setScale(self.leafModel, leavesScale / 1.1)
            if removeLeaves:
                for c in self.leaves.getChildren():
                    c.removeNode()
            self.makeFromStack()
            self.bodies.setTexture(self.barkTexture)         
            num -= 1
FractalTree.makeFMT()


class DefaultTree(FractalTree):
    def __init__(self):       
        barkTexture = base.loader.loadTexture("models/tree/barkTexture.jpg")
        leafModel = base.loader.loadModel('models/tree/shrubbery')
        leafModel.clearModelNodes()
        leafModel.flattenStrong()
        leafTexture = base.loader.loadTexture('models/tree/material-10-cl.png')
        leafModel.setTexture(leafTexture, 1)       
        lengthList = self.makeLengthList(Vec3(1, 1, 1), 64)
        numCopiesList = self.makeNumCopiesList(4, 3, 64)
        radiusList = self.makeRadiusList(0.5, 64, numCopiesList)
        FractalTree.__init__(self, barkTexture, leafModel, lengthList, numCopiesList, radiusList)
       
    @staticmethod
    def makeRadiusList(radius, iterations, numCopiesList, scale=1.5):
        l = [radius]
        for i in xrange(1, iterations):
            #if i % 3 == 0:
            if i != 1 and numCopiesList[i - 2]:
                radius /= numCopiesList[i - 2] ** 0.5
            else:
                radius /= scale ** (1.0 / 3)
            l.append(radius)
        return l
   
    @staticmethod
    def makeLengthList(length, iterations, sx=2.0, sy=2.0, sz=1.25):
        l = [length]
        for i in xrange(1, iterations):
            #if i % 3 == 0:
                #decrease dimensions when we branch
                #length = Vec3(length.getX() / 2, length.getY() / 2, length.getZ() / 1.1)
            length = Vec3(length.getX() / sx ** (1.0 / 3), length.getY() / sy ** (1.0 / 3), length.getZ() / sz ** (1.0 / 3))
            l.append(length)
        return l
   
    @staticmethod
    def makeNumCopiesList(numCopies, branchat, iterations):
        l = list()
        for i in xrange(iterations):
            if i % int(branchat) == 0:
                l.append(numCopies)
            else:
                l.append(0)
        return l

#this grows a tree
if __name__ == "__main__":
    from direct.showbase.ShowBase import ShowBase
    base = ShowBase()
    base.cam.setPos(0, -10, 10)
    t = DefaultTree()
    t.reparentTo(base.render)
    #make an optimized snapshot of the current tree
    np = t.getStatic()
    np.setPos(10, 10, 0)
    np.reparentTo(base.render)
    #demonstrate growing
    last = [0] # a bit hacky
    def grow(task):
        if task.time > last[0] + 1:
            t.grow()
            last[0] = task.time
            #t.leaves.detachNode()
        if last[0] > 10:
            return task.done
        return task.cont
    base.taskMgr.add(grow, "growTask")
    base.run()


Put that in a tree.py, and you can have fancy lighting with this hacked up file:
Code: Select all
from panda3d.core import Light,AmbientLight,DirectionalLight
from panda3d.core import NodePath
from panda3d.core import Vec3,Vec4,Mat4,VBase4,Point3
from direct.task.Task import Task

from tree import *

from direct.showbase.ShowBase import ShowBase
base = ShowBase()
base.disableMouse()
#base.cam.setPos(0, -80, 10)
t = DefaultTree()
t.reparentTo(base.render)
#make an optimized snapshot of the current tree
np = t.getStatic()
np.setPos(10, 10, 0)
np.reparentTo(base.render)
#demonstrate growing
last = [0] # a bit hacky
def grow(task):
    if task.time > last[0] + 1:
        t.grow()
        last[0] = task.time
        #t.leaves.detachNode()
    if last[0] > 10:
        return task.done
    return task.cont
base.taskMgr.add(grow, "growTask")


dlight = DirectionalLight('dlight')

dlnp = render.attachNewNode(dlight)
dlnp.setHpr(0, 0, 0)
render.setLight(dlnp)

alight = AmbientLight('alight')

alnp = render.attachNewNode(alight)
render.setLight(alnp)

#rotating light to show that normals are calculated correctly
def updateLight(task):
    base.camera.setHpr(task.time/20.0*360,0,0)
   
   
    base.camera.setPos(0,0,0)
    base.camera.setPos(base.cam,0,-80,15)
    base.camera.setP(-4)
   
    h=task.time/10.0*360+180
   
    dlnp.setHpr(0,h,0)
    h=h+90
    h=h%360
    h=min(h,360-h)
    #h is now angle from straight up
    hv=h/180.0
    hv=1-hv
    sunset=max(0,1.0-abs(hv-.5)*8)
    if hv>.5: sunset=1
    #sunset=sunset**.2
    sunset=VBase4(0.8, 0.5, 0.0, 1)*sunset
    sun=max(0,hv-.5)*2*4
    sun=min(sun,1)
    dlight.setColor((VBase4(0.4, 0.9, 0.8, 1)*sun*2+sunset))
    alight.setColor(VBase4(0.2, 0.2, 0.3, 1)*sun+VBase4(0.2, 0.2, 0.3, 1)+sunset*.2)
    return Task.cont   

taskMgr.add(updateLight, "rotating Light")

base.run()



Image

Image


Praios, assuming you give permission to BSD license your changes, we should get these fixes in version control somewhere where it will be easier to work on. Either in the Panda repository, or a separate one.
Craig
 
Posts: 330
Joined: Thu Jul 02, 2009 8:55 pm

Postby Praios » Sat Feb 19, 2011 4:27 am

I created an Origo-project and put my version in the repository and added "BSD-license".
Project: http://pandaplant.origo.ethz.ch/
Repository: https://svn.origo.ethz.ch/pandaplant/
What am I actually talking about? - I don't know...
User avatar
Praios
 
Posts: 234
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

Postby Praios » Thu Feb 24, 2011 5:56 am

Updated the random-functions and uploaded the current version.
For the Source see here.
What am I actually talking about? - I don't know...
User avatar
Praios
 
Posts: 234
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

Another take on the same

Postby supus71 » Sat Jan 14, 2012 12:03 pm

So I started with this code and it is great, for all kinds of reasons. (output as well as teaching concepts)
However, it seemed to make the same "look" tree all the time so I wanted to see if could generalize this more....and I went waaaaay off course! (but had fun doing it). This isn't completely robust or beautiful or anything but it can make a variety of branching types. (basically the old fractal always split so you couldn't get say a pine tree looking tree with a main trunk)
This was turning into its own project and I still analyze real trees every time I going outside but I've set it aside for now. i wanted to share back to this post since it is the genesis of it. If there is any interest in this, I can try to document it some, clean it up, etc etc. I* know how to use it since I wrote it :) but I'm sure what each factor is attempting to do is not obvious. In addition, it is also a generalized class with the intent that you subclass and define your own "branch" functions, if you need to extend it.
One note. The current config of var's is setup to mimic Fractal tree. there is a little side effect I didn't fix that the branches are not all "connected" geometry; so you will occasionally see some weird things sticking out or gaps.
Code: Select all
# -*- coding: utf-8 -*-
'''
Created on 11.12.2010
Based on Kwasi Mensah's (kmensah@andrew.cmu.edu) "The Fractal Plants Sample Program" from 8/05/2005
@author: Praios

Edited by Craig Macomber

Created on Thu Nov 24 19:35:08 2011
based on the above authors and editors
@author: Shawn Updegraff
'''

import sys

from direct.showbase.ShowBase import ShowBase
from panda3d.core import NodePath, Geom, GeomNode, GeomVertexArrayFormat, TransformState, GeomVertexWriter, GeomTristrips, GeomVertexRewriter, GeomVertexReader, GeomVertexData, GeomVertexFormat, InternalName
from panda3d.core import Mat4, Vec4, Vec3, CollisionNode, CollisionTube, Point3, Quat
from math import sin,cos,pi, sqrt
import random
from collections import namedtuple

#from panda3d.core import PStatClient
#PStatClient.connect()
#import pycallgraph
#pycallgraph.start_trace()

_polySize = 5


class Bud(object):
    # still need bud objects to pack info by name; easier that way
    def __init__(self,position=Vec3(0,0,0),Hpr=Vec3(0,0,0),length=0,rad=0):
        self.pos = position
        self.Hpr = Hpr
        self.maxL = length
        self.maxRad = rad
       
class Branch(NodePath):
    def __init__(self, nodeName, L, initRadius, nSeg):
        NodePath.__init__(self, nodeName)
        self.numPrimitives = 0
        self.nodeList = []    # for the branch geometry itself
        self.buds = []        # a list of children. "buds" for next gen of branchs
        self.length = L            # total length of this branch; note Node scaling will mess this up!
        self.R0 = initRadius
        self.nSeg = nSeg
        self.gen = 0        # ID's generation of this branch (trunk = 0, 1 = primary branches, ...)
        # contains 2 Vec3:[ position, and Hpr]. Nominally these are set by the parent Tree class
        # with it's add children function(s)
       
        self.bodydata = GeomVertexData("body vertices", GeomVertexFormat.getV3n3t2(), Geom.UHStatic)       
        self.bodies = NodePath("Bodies")
        self.bodies.reparentTo(self)
       
        self.coll = self.attachNewNode(CollisionNode("Collision"))       
        self.coll.show()       
        self.coll.reparentTo(self)

    #this draws the body of the tree. This draws a ring of vertices and connects the rings with
    #triangles to form the body.
    #this keepDrawing paramter tells the function wheter or not we're at an end
    #if the vertices before you were an end, dont draw branches to it
    def drawBody(self, pos, quat, radius=1,UVcoord=(1,1), numVertices=_polySize):
#        if isRoot:
#            self.bodydata = GeomVertexData("body vertices", GeomVertexFormat.getV3n3t2(), Geom.UHStatic)
        vdata = self.bodydata
        circleGeom = Geom(vdata) # this was originally a copy of all previous geom in vdata...
        vertWriter = GeomVertexWriter(vdata, "vertex")
        #colorWriter = GeomVertexWriter(vdata, "color")
        normalWriter = GeomVertexWriter(vdata, "normal")
#        drawReWriter = GeomVertexRewriter(vdata, "drawFlag")
        texReWriter = GeomVertexRewriter(vdata, "texcoord")

        startRow = vdata.getNumRows()
        vertWriter.setRow(startRow)
        #colorWriter.setRow(startRow)
        normalWriter.setRow(startRow)       
        texReWriter.setRow(startRow)   
       
        #axisAdj=Mat4.rotateMat(45, axis)*Mat4.scaleMat(radius)*Mat4.translateMat(pos)
        perp1 = quat.getRight()
        perp2 = quat.getForward()   
       
#TODO: PROPERLY IMPLEMENT RADIAL NOISE       
        #vertex information is written here
        angleSlice = 2 * pi / numVertices
        currAngle = 0
        for i in xrange(numVertices+1):
            adjCircle = pos + (perp1 * cos(currAngle) + perp2 * sin(currAngle)) * radius * (.5+bNodeRadNoise*random.random())
            normal = perp1 * cos(currAngle) + perp2 * sin(currAngle)       

            normalWriter.addData3f(normal)
            vertWriter.addData3f(adjCircle)
            texReWriter.addData2f(float(UVcoord[0]*i) / numVertices,UVcoord[1])            # UV SCALE HERE!
            #colorWriter.addData4f(0.5, 0.5, 0.5, 1)
            currAngle += angleSlice
       
        #we cant draw quads directly so we use Tristrips
        if (startRow != 0):
            lines = GeomTristrips(Geom.UHStatic)         
            for i in xrange(numVertices+1):
                lines.addVertex(i + startRow)
                lines.addVertex(i + startRow - numVertices-1)
            lines.addVertex(startRow)
            lines.addVertex(startRow - numVertices)
            lines.closePrimitive()
            #lines.decompose()
            circleGeom.addPrimitive(lines)           
            circleGeomNode = GeomNode("Debug")
            circleGeomNode.addGeom(circleGeom)   
            self.numPrimitives += numVertices * 2
            self.bodies.attachNewNode(circleGeomNode)
            return circleGeomNode
       
    def generate(self, Params):
        # defines a "branch" as a list of BranchNodes and then calls branchfromNodes
        # Creates a scaled length,width, height geometry to be later
        # otherwise can not maintain UV per unit length (if that's desired)
        # returns non-rotated, unpositioned geom node
       
#        branchlen = Params['L']
#        branchSegs = Params['nSegs']
        Params.update({'iSeg':0})
        rootPos = Vec3(0,0,0) # + self.PositionFunc(**Params) #add noise to root node; same as others in loop
        rootNode = BranchNode._make([rootPos,self.R0,Vec3(0,0,0),Quat(),_uvScale,0,self.length]) # initial node      # make a starting node flat at 0,0,0       
        self.nodeList = [rootNode] # start new branch list with newly created rootNode
        prevNode = rootNode
       
        for i in range(1,self.nSeg+1): # start a 1, 0 is root node, now previous
            Params.update({'iSeg':i})
            newPos = self.PositionFunc(**Params)
            fromVec = newPos - prevNode.pos # point
            dL = (prevNode.deltaL + fromVec.length()) # cumulative length accounts for off axis node lengths; percent of total branch length
            radius = self.RadiusFunc(position=dL/self.length,**Params) # pos.length() wrt to root. if really curvy branch, may want the sum of segment lengths instead..

# MOVE TO UVfunc
#            perim = 2*_polySize*radius*sin(pi/_polySize) # use perimeter to calc texture length/scale
            # if going to use above perim calc, probably want a high number of BranchNodes to minimuze the Ushift at the nodes
            perim = 1 # integer tiling of uScale; looks better; avoids U shifts at nodes     
            UVcoord = (_uvScale[0]*perim, rootNode.texUV[1] + dL*float(_uvScale[1]) ) # This will keep the texture scale per unit length constant
##

            newNode = BranchNode._make([newPos,radius,fromVec,rootNode.quat,UVcoord,dL,self.length-dL]) # i*Lseg = distance from root
            self.nodeList.append(newNode)
            prevNode = newNode # this is now the starting point on the next iteration

        # sends the BranchNode list to the drawBody function to generate the
        # actual geometry
        for i,node in enumerate(self.nodeList):
#            if i == 0: isRoot = True
#            else: isRoot = False
#            if i == len(nodeList)-1: keepDrawing = True
#            else:
            self.drawBody(node.pos, node.quat, node.radius,node.texUV)
        return self.nodeList
       
    def addNewBuds(self):
#TODO: GENERALIZE THIS SECTION
#        budPos = budHpr = []
#        rad = maxL = 0
           
        [gH,gP,gR] = self.getHpr(base.render) # get this branch global Hpr for later
#        nbud = budPerLen * self.length
#        budPosArr = [x*minBudSpacing for x in range(self.length/minBudSpacing)]
#        sampList = random.choice(budPosArr,nbud)
       
#        sampList = random.sample(self.nodeList[_skipChildren:-1],5)
        sampList = [self.nodeList[-1]]
        for nd in sampList: # just use nodes for now
            budPos = nd.pos
            maxL = lfact*self.length
            rad = rfact*nd.radius # NEW GEN RADIUS - THIS SHOULD BE A PARAMETER!!!
   
            #Child branch Ang func - orient the node after creation
            # trunk bud multiple variables
            budsPerNode = random.randint(1,3) +1    # +1 since 1 branch will always "continue"
            hdg = range(0,360,360/budsPerNode)
#            budRot = random.randint(-hdg[1],hdg[1]) # add some noise to the trunk bud angles
            for i,h in enumerate(hdg):                       
                angP = random.gauss(budP0,budPnoise)
                angR = random.randint(-45,45)
                if i==0:
                    budHpr = Vec3(gH,gP,gR) # at least 1 branch continues on current heading                   
                else:
                    budHpr = Vec3(gH+h,angP,angR)
                self.buds.append([budPos,rad,budHpr,maxL])
               
    def interpLen(self,inLen):
#BranchNode = namedtuple('BranchNode','pos radius fromVector quat texUV deltaL d2t')
        outLen = []       
        for node in self.nodeList:
            if node.deltaL >= inLen:
                prevNode = node
                break
        delta = inLen - prevNode.deltaL
        outLen = prevNode.pos + prevNode.fromVector*delta
#TODO: TOVECTORS ARE WRONG> THEY ARE REALLY FROM VECTORS...need To's       
        return outLen
       
    def UVfunc(*args,**kwargs):
        pass # STUB
    def Circumfunc(*args,**kwargs):
        pass # STUB

    def PositionFunc(self,*args,**kwargs):
        upVector = kwargs['upVector']
        iSeg = kwargs['iSeg']
        nAmp = kwargs['Anoise']   
#        branchSegs = kwargs['nSegs']
        cXfactors = kwargs['cXfactors']
        cYfactors = kwargs['cYfactors']
       
        Lseg = float(self.length)/self.nSeg # self.length set at init()
        relPos = Lseg*iSeg / self.length
   
        dX = sum([term[0]*sin(2*pi*term[1]*relPos+term[2]) for term in cXfactors])
        dY = sum([term[0]*sin(2*pi*term[1]*relPos+term[2]) for term in cYfactors])
        noise = Vec3(-1+2*random.random(),-1+2*random.random(),0)*nAmp
        newPos = Vec3(0,0,0) + upVector*Lseg*iSeg  + noise + Vec3(dX,dY,0)
        return newPos
       
#    def PositionFunc(self,*args,**kwargs):
#        upVector = kwargs['upVector']
#        iSeg = kwargs['iSeg']
#        nAmp = kwargs['Anoise']   
##        branchlen = kwargs['L']
#        branchSegs = kwargs['nSegs']
#   
#        Lseg = float(self.length)/branchSegs # self.length set at init()
#        noise = Vec3(-1+2*random.random(),-1+2*random.random(),0)*nAmp
#        newPos = Vec3(0,0,0) + upVector*Lseg*iSeg  + noise
#        return newPos
       
    def RadiusFunc(self,*args,**kwargs):
        # radius proportional to length from root of this branch to some power
        rTaper = kwargs['rTaper']
        relPos = kwargs['position']
#        R0 = kwargs['R0']
        newRad = self.R0*(1 - (1-rTaper)*relPos) # linear taper Vs length. pretty typical       
        return newRad

class GeneralTree(NodePath):
   
    def __init__(self,L0,R0,numSegs,bark,nodeName):
        NodePath.__init__(self,nodeName)       
        self.L0 = L0
        self.R0 = R0
        self.numSegs = numSegs
        self.trunk = Branch("Trunk",L0,R0,2*numSegs)
        self.trunk.reparentTo(self)
        self.trunk.setTexture(bark)
   
    def generate(self,*args,**kwargs):
#        lfact = kwargs['lfact']
       
        self.trunk.generate(Params)
        self.trunk.addNewBuds()
        children = [self.trunk] # each node in the trunk will span a branch
        nextChildren = []
        leafNodes = []
       
        for gen in range(1,numGens+1):
            Lgen = self.L0*lfact**gen
            print "Calculating branches..."
#            print "Generation: ", gen, " children: ", len(children), "Gen Len: ", Lgen
            for thisBranch in children:
                for ib,bud in enumerate(thisBranch.buds):
                    # Create Child NodePath instance
                   
                    # experimental curvature terms
                    Params.update(cXfactors = [(.1*bud[3]*random.gauss(0,.333),0.25,0),
                                                  (.1*bud[3]*random.gauss(0,.333),.5,0),
                                                    (.1*bud[3]*random.gauss(0,.333),.75,0),
                                                    (.1*bud[3]*random.gauss(0,.333),1,0)])
                    Params.update(cYfactors = [(.1*bud[3]*random.gauss(0,.333),0.25,0),
                                                  (.1*bud[3]*random.gauss(0,.333),.5,0),
                                                    (.1*bud[3]*random.gauss(0,.333),.75,0),
                                                    (.0*bud[3]*random.gauss(0,.333),1,0)])
                   
                                                   
                    newBr = Branch("Branch1",bud[3],bud[1],self.numSegs+1-gen)
                    newBr.reparentTo(thisBranch)
    #                newBr.setTexture(bark) # If you wanted to do each branch with a unqiue texture
                    newBr.gen = gen
                   
                    # Child branch Radius func
    #                Rparams['R0']= bud[1]
   
                    # Child branch position Func
                    newBr.setPos(bud[0])               
                    lFunc = Lgen*(1-Lnoise/2 - Lnoise*random.random())
                    Params['Anoise'] = bud[1]*posNoise    # noise func of tree or branch?
                    Params.update({'L':lFunc,'nSegs':self.numSegs+1-gen})
                    Params.update({'rTaper':rTaper**gen})
                    newBr.setHpr(base.render,bud[2])
                   
                    #Create the actual geometry now
                    newBr.generate(Params)
   
                    # Create New Children Function
                    newBr.addNewBuds()
                    # just add this branch to the new Children;
                    nextChildren.append(newBr)                 
                    # use it's bud List for new children branches.               
            children = nextChildren # assign Children for the next iteration
            nextChildren = []

        if _DoLeaves:
            print "adding foliage...hack adding to nodes, not buds!"
            for thisBranch in children:
    #            for node in thisBranch.nodeList[_skipChildren:]:
                self.drawLeaf(thisBranch,thisBranch.nodeList[-1].pos,_LeafScale)


    #this draws leafs when we reach an end       
    def drawLeaf(self,parent,pos, scale=0.125):
        leafNode = NodePath("leaf")
        leafNode.setTwoSided(1)
        leafMod.instanceTo(leafNode)
        leafNode.reparentTo(parent)
        leafNode.setPos(pos)
        leafNode.setScale(scale)
        leafNode.setHpr(0,0,0)


   
BranchNode = namedtuple('BranchNode','pos radius fromVector quat texUV deltaL d2t')
# fromVector is TO next node (delta of pos vectors)
# deltaL is cumulative distance from the branch root (sum of node lengths)

if __name__ == "__main__":
    base = ShowBase()   
#    random.seed(11*math.pi)
    _UP_ = Vec3(0,0,1) # General axis of the model as a whole

    # TRUNK AND BRANCH PARAMETERS
    numGens = 2    # number of branch generations to calculate (0=trunk only), usually don't want much more than 4-6..if that!
    print numGens
   
    L0 = 6      # initial length
    R0 = 4        # initial radius
    numSegs = 2    # number of nodes per branch; +1 root = 7 total BranchNodes per branch
    taper0 = 1
   
    _skipChildren = 0 # how many nodes in from the base to exclude from children list; +1 to always exclude base
    lfact = 0.8   # length ratio between branch generations
    # often skipChildren works best as a function of total lenggth, not just node count       
    rfact = 1     # radius ratio between generations
    rTaper = .7 # taper factor; % reduction in radius between tip and base ends of branch
    budP0 = 45    # a new bud's nominal pitch angle
   
    budPnoise = 10 # variation in bud's pitch angle
    bNodeRadNoise = 0.1 # EXPERIMENTAL: ADDING Vertex RADIAL NOISE for shape interest
    posNoise = 0.0    # random noise in The XY plane around the growth axis of a branch
    Lnoise = 3    # percent(0-1) length variation of new branches

    _uvScale = (2,1.0/3) #repeats per unit length (around perimeter, along the tree axis)
#    _BarkTex_ = "barkTexture.jpg"
    _BarkTex_ ='./resources/models/barkTexture-1z.jpg'
   

    # LEAF PARAMETERS
#    _LeafTex = 'Green Leaf.png'
    _LeafModel = 'myLeafModel7.x'
#    _LeafModel = 'shrubbery'
#    _LeafTex = 'material-10-cl.png'
   
#    leafTex = base.loader.loadTexture('./resources/models/'+ _LeafTex)
    leafMod = base.loader.loadModel('./resources/models/'+ _LeafModel)
#    leafMod.setScale(.01)
    leafMod.setZ(-.5)
#    leafMod.setScale(2,2,1)
    leafMod.flattenStrong()
    _LeafScale = 4
    _DoLeaves = 1 # not ready for prime time; need to add drawLeaf to Tree Class
 
    bark = base.loader.loadTexture(_BarkTex_)   

    Params = {'L':L0,'nSegs':numSegs,'Anoise':posNoise*R0,'upVector':_UP_}
    Params.update({'rTaper':rTaper,'R0':R0})
    Params.update(cXfactors = [(.05*R0,1,0),(.05*R0,2,0)],cYfactors = [(.05*R0,1,pi/2),(.05*R0,2,pi/2)])

    np = 6
    plts = range(np**2) # 6x6 array
    ds = 5.0
    for it in range(10):
        tree = GeneralTree(L0,R0,numSegs,bark,"my tree")   
        tree.generate(Params)
        tree.reparentTo(base.render)

        # DONE GENERATING. WRITE OUT UNSCALED MODEL
        print "writing out file"
        tree.setScale(1)
        tree.setH(180)
        tree.setZ(-0.1)
        tree.flattenStrong()
        tree.writeBamFile('./resources/models/sampleTree'+str(it)+'.bam')

        p = random.choice(plts)
        tx = ds*(p/np)
        ty = ds*(p%np)
        tree.setPos(tx,ty,0)

##############################

    ruler = base.loader.loadModel('./resources/models/plane')
    ruler.setPos(-(R0+.25),0,1) # z = .5 *2scale
    ruler.setScale(.05,1,2) #2 unit tall board
    ruler.setTwoSided(1)
    ruler.reparentTo(base.render)
   
    def rotateTree(task):
        phi = 10*task.time
        tree.setH(phi)
        return task.cont
    base.taskMgr.add(rotateTree,"merrygoround")

    base.cam.setPos(0,-4*L0, L0/2)   
    base.render.setShaderAuto()
    base.setFrameRateMeter(1)   
#    base.toggleWireframe()
    base.accept('escape',sys.exit)
    base.accept('z',base.toggleWireframe)
#    pycallgraph.make_dot_graph('treeXpcg.png')
    base.setBackgroundColor((.0,.5,.9))
    base.render.analyze()
    base.run()

#TODO:
#   - clean up: move branch Hpr out of bud and into child branch property
#    - choose "bud" locations other than branch nodes and random circumfrentially.
#    - numSegs to parameter into branch; numSegs = f(generation), fewer on younger
#        prob set a buds per length var
#    - COULD do a render to texture for the last limbs and leaves; define multiple plane
        # cross sections, create them, then append them to the last real node before RTT
#     make parameters: probably still need a good Lfunc.
#     Distribute branches uniform around radius.
#     "Crown" the trunk; possibly branches. - single point; no rad func and connect all previous nodes to point
#     define circumference function (pull out of drawBody())
#


this is also stored in http://jafp3dd.googlecode.com (SVN repo)
User avatar
supus71
 
Posts: 22
Joined: Tue Nov 08, 2011 7:35 pm

Postby Praios » Fri Jun 01, 2012 10:35 am

As origo shut down i uploaded my last working copy to https://code.google.com/p/pandaplant/
Now im looking for a new roadmap - i wrote something on origo but its gone :(

Edit: Now i have a roadmap - its more a todo-list.
My main target is to seperate the tree representation from rendering so the same tree can be rendered in different ways (for example for lod)
What am I actually talking about? - I don't know...
User avatar
Praios
 
Posts: 234
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany


Return to Code Snippets

Who is online

Users browsing this forum: No registered users and 0 guests

cron