Procedural Trees

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)

'''
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:

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()

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.