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

``````'''
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)
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()
format = GeomVertexFormat(GeomVertexFormat.getV3n3t2())
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
ends = []
while stack:
pos, vecList, depth = stack.pop()
length = lengthList[depth]
if depth != to and depth + 1 < len(lengthList):
#move foward along the right axis
newPos = pos + vecList[0] * length.length()
if makeColl:
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.drawLeaf(pos, vecList)
self.ends = ends

#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
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)
texReWriter.addData2f(sCoord, (i + 0.001) / (numVertices - 1))
currAngle += angleSlice
#we cant draw quads directly so we use Tristrips
if (startRow != 0) and (drawReader.getData1f() != False):
lines = GeomTristrips(Geom.UHStatic)
half = numVertices / 2
for i in xrange(numVertices - 1, -1, -1):
if i < half:
else:
lines.addVertex(i + startRow - half - numVertices)
lines.closePrimitive()
lines.decompose()
circleGeomNode = GeomNode("Debug")
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)

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):
leafModel.clearModelNodes()
leafModel.flattenStrong()
leafModel.setTexture(leafTexture, 1)
lengthList = self.makeLengthList(Vec3(1, 1, 1), 64)
numCopiesList = self.makeNumCopiesList(4, 3, 64)
FractalTree.__init__(self, barkTexture, leafModel, lengthList, numCopiesList, radiusList)

@staticmethod
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)
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
if task.time > last[0] + 1:
t.grow()
#t.leaves.detachNode()
if last[0] > 10:
base.run()``````

edit: asynchronous flattening makes trouble - removed

I forgot to say you need the stuff from the example. Put it in “models/tree/”.

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.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()
format = GeomVertexFormat(GeomVertexFormat.getV3n3t2())
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
ends = []
while stack:
pos, quat, depth = stack.pop()
length = lengthList[depth]
if depth != to and depth + 1 < len(lengthList):
#move foward along the right axis
newPos = pos + quat.getUp() * length.length()
if makeColl:
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.drawLeaf(pos, quat)
self.ends = ends

#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
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)
currAngle += angleSlice
#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.closePrimitive()
#lines.decompose()
circleGeomNode = GeomNode("Debug")
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)

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):
leafModel.clearModelNodes()
leafModel.flattenStrong()
leafModel.setTexture(leafTexture, 1)
lengthList = self.makeLengthList(Vec3(1, 1, 1), 64)
numCopiesList = self.makeNumCopiesList(4, 3, 64)
FractalTree.__init__(self, barkTexture, leafModel, lengthList, numCopiesList, radiusList)

@staticmethod
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)
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
if task.time > last[0] + 1:
t.grow()
#t.leaves.detachNode()
if last[0] > 10:
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 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
if task.time > last[0] + 1:
t.grow()
#t.leaves.detachNode()
if last[0] > 10:

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

base.camera.setPos(0,0,0)
base.camera.setPos(base.cam,0,-80,15)
base.camera.setP(-4)

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)

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.

I created an Origo-project and put my version in the repository and added “BSD-license”.
Project: pandaplant.origo.ethz.ch/
Repository: svn.origo.ethz.ch/pandaplant/

Updated the random-functions and uploaded the current version.
For the Source see here.

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.

``````# -*- 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
self.pos = position
self.Hpr = Hpr
self.maxL = length

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

perp1 = quat.getRight()
perp2 = quat.getForward()

#vertex information is written here
angleSlice = 2 * pi / numVertices
currAngle = 0
for i in xrange(numVertices+1):
normal = perp1 * cos(currAngle) + perp2 * sin(currAngle)

texReWriter.addData2f(float(UVcoord[0]*i) / numVertices,UVcoord[1])            # UV SCALE HERE!
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.closePrimitive()
#lines.decompose()
circleGeomNode = GeomNode("Debug")
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:
return self.nodeList

#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

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

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

# 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

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

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

#    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

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.setPos(-(R0+.25),0,1) # z = .5 *2scale
ruler.setScale(.05,1,2) #2 unit tall board
ruler.setTwoSided(1)
ruler.reparentTo(base.render)

tree.setH(phi)

base.cam.setPos(0,-4*L0, L0/2)
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)

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)

Moved to github.com/janbrohl/pandaplant

This is pretty cool.

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

Happy to see someone likes it

mipmapping activated

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