## Procedural Trees

### Procedural Trees

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.2010Based 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, InternalNamefrom panda3d.core import Mat4, Vec3, Vec4, CollisionNode, CollisionTube, Point3import math, random #this is for making the tree not too straightdef _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 branchingdef _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 -= 1FractalTree.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 treeif __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...

Praios

Posts: 238
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

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

Praios

Posts: 238
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

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, Quatimport 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,DirectionalLightfrom panda3d.core import NodePathfrom panda3d.core import Vec3,Vec4,Mat4,VBase4,Point3from direct.task.Task import Taskfrom 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 correctlydef 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()`

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: 331
Joined: Thu Jul 02, 2009 8:55 pm

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

Praios

Posts: 238
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

Updated the random-functions and uploaded the current version.
For the Source see here.
What am I actually talking about? - I don't know...

Praios

Posts: 238
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

### Another take on the same

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.2010Based on Kwasi Mensah's (kmensah@andrew.cmu.edu) "The Fractal Plants Sample Program" from 8/05/2005@author: PraiosEdited by Craig MacomberCreated on Thu Nov 24 19:35:08 2011based on the above authors and editors@author: Shawn Updegraff'''import sysfrom direct.showbase.ShowBase import ShowBasefrom panda3d.core import NodePath, Geom, GeomNode, GeomVertexArrayFormat, TransformState, GeomVertexWriter, GeomTristrips, GeomVertexRewriter, GeomVertexReader, GeomVertexData, GeomVertexFormat, InternalNamefrom panda3d.core import Mat4, Vec4, Vec3, CollisionNode, CollisionTube, Point3, Quatfrom math import sin,cos,pi, sqrtimport randomfrom collections import namedtuple#from panda3d.core import PStatClient#PStatClient.connect()#import pycallgraph#pycallgraph.start_trace()_polySize = 5class 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 newRadclass 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)

supus71

Posts: 22
Joined: Tue Nov 08, 2011 7:35 pm

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

Praios

Posts: 238
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

### Re: Procedural Trees

What am I actually talking about? - I don't know...

Praios

Posts: 238
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

### Re: Procedural Trees

This is pretty cool.

I do recommend enabling mipmapping on the bark texture to prevent flickering artifacts when moving the camera around.
rdb

Posts: 11228
Joined: Mon Dec 04, 2006 5:58 am
Location: Netherlands

### Re: Procedural Trees

Happy to see someone likes it

mipmapping activated
What am I actually talking about? - I don't know...

Praios

Posts: 238
Joined: Fri Aug 31, 2007 2:23 pm
Location: Germany

### Re: Procedural Trees

I'll benefit of your advices. They are very useful and I appreciate your knowledge. I greet you.
brilliant

Posts: 1
Joined: Wed Jan 11, 2017 12:05 pm