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
def __init__(self,position=Vec3(0,0,0),Hpr=Vec3(0,0,0),length=0,rad=0):
self.pos = position
self.Hpr = Hpr
self.maxL = length
self.maxRad = rad
class Branch(NodePath):
def __init__(self, nodeName, L, initRadius, nSeg):
NodePath.__init__(self, nodeName)
self.numPrimitives = 0
self.nodeList = [] # for the branch geometry itself
self.buds = [] # a list of children. "buds" for next gen of branchs
self.length = L # total length of this branch; note Node scaling will mess this up!
self.R0 = initRadius
self.nSeg = nSeg
self.gen = 0 # ID's generation of this branch (trunk = 0, 1 = primary branches, ...)
# contains 2 Vec3:[ position, and Hpr]. Nominally these are set by the parent Tree class
# with it's add children function(s)
self.bodydata = GeomVertexData("body vertices", GeomVertexFormat.getV3n3t2(), Geom.UHStatic)
self.bodies = NodePath("Bodies")
self.bodies.reparentTo(self)
self.coll = self.attachNewNode(CollisionNode("Collision"))
self.coll.show()
self.coll.reparentTo(self)
#this draws the body of the tree. This draws a ring of vertices and connects the rings with
#triangles to form the body.
#this keepDrawing paramter tells the function wheter or not we're at an end
#if the vertices before you were an end, dont draw branches to it
def drawBody(self, pos, quat, radius=1,UVcoord=(1,1), numVertices=_polySize):
# if isRoot:
# self.bodydata = GeomVertexData("body vertices", GeomVertexFormat.getV3n3t2(), Geom.UHStatic)
vdata = self.bodydata
circleGeom = Geom(vdata) # this was originally a copy of all previous geom in vdata...
vertWriter = GeomVertexWriter(vdata, "vertex")
#colorWriter = GeomVertexWriter(vdata, "color")
normalWriter = GeomVertexWriter(vdata, "normal")
# drawReWriter = GeomVertexRewriter(vdata, "drawFlag")
texReWriter = GeomVertexRewriter(vdata, "texcoord")
startRow = vdata.getNumRows()
vertWriter.setRow(startRow)
#colorWriter.setRow(startRow)
normalWriter.setRow(startRow)
texReWriter.setRow(startRow)
#axisAdj=Mat4.rotateMat(45, axis)*Mat4.scaleMat(radius)*Mat4.translateMat(pos)
perp1 = quat.getRight()
perp2 = quat.getForward()
#TODO: PROPERLY IMPLEMENT RADIAL NOISE
#vertex information is written here
angleSlice = 2 * pi / numVertices
currAngle = 0
for i in xrange(numVertices+1):
adjCircle = pos + (perp1 * cos(currAngle) + perp2 * sin(currAngle)) * radius * (.5+bNodeRadNoise*random.random())
normal = perp1 * cos(currAngle) + perp2 * sin(currAngle)
normalWriter.addData3f(normal)
vertWriter.addData3f(adjCircle)
texReWriter.addData2f(float(UVcoord[0]*i) / numVertices,UVcoord[1]) # UV SCALE HERE!
#colorWriter.addData4f(0.5, 0.5, 0.5, 1)
currAngle += angleSlice
#we cant draw quads directly so we use Tristrips
if (startRow != 0):
lines = GeomTristrips(Geom.UHStatic)
for i in xrange(numVertices+1):
lines.addVertex(i + startRow)
lines.addVertex(i + startRow - numVertices-1)
lines.addVertex(startRow)
lines.addVertex(startRow - numVertices)
lines.closePrimitive()
#lines.decompose()
circleGeom.addPrimitive(lines)
circleGeomNode = GeomNode("Debug")
circleGeomNode.addGeom(circleGeom)
self.numPrimitives += numVertices * 2
self.bodies.attachNewNode(circleGeomNode)
return circleGeomNode
def generate(self, Params):
# defines a "branch" as a list of BranchNodes and then calls branchfromNodes
# Creates a scaled length,width, height geometry to be later
# otherwise can not maintain UV per unit length (if that's desired)
# returns non-rotated, unpositioned geom node
# branchlen = Params['L']
# branchSegs = Params['nSegs']
Params.update({'iSeg':0})
rootPos = Vec3(0,0,0) # + self.PositionFunc(**Params) #add noise to root node; same as others in loop
rootNode = BranchNode._make([rootPos,self.R0,Vec3(0,0,0),Quat(),_uvScale,0,self.length]) # initial node # make a starting node flat at 0,0,0
self.nodeList = [rootNode] # start new branch list with newly created rootNode
prevNode = rootNode
for i in range(1,self.nSeg+1): # start a 1, 0 is root node, now previous
Params.update({'iSeg':i})
newPos = self.PositionFunc(**Params)
fromVec = newPos - prevNode.pos # point
dL = (prevNode.deltaL + fromVec.length()) # cumulative length accounts for off axis node lengths; percent of total branch length
radius = self.RadiusFunc(position=dL/self.length,**Params) # pos.length() wrt to root. if really curvy branch, may want the sum of segment lengths instead..
# MOVE TO UVfunc
# perim = 2*_polySize*radius*sin(pi/_polySize) # use perimeter to calc texture length/scale
# if going to use above perim calc, probably want a high number of BranchNodes to minimuze the Ushift at the nodes
perim = 1 # integer tiling of uScale; looks better; avoids U shifts at nodes
UVcoord = (_uvScale[0]*perim, rootNode.texUV[1] + dL*float(_uvScale[1]) ) # This will keep the texture scale per unit length constant
##
newNode = BranchNode._make([newPos,radius,fromVec,rootNode.quat,UVcoord,dL,self.length-dL]) # i*Lseg = distance from root
self.nodeList.append(newNode)
prevNode = newNode # this is now the starting point on the next iteration
# sends the BranchNode list to the drawBody function to generate the
# actual geometry
for i,node in enumerate(self.nodeList):
# if i == 0: isRoot = True
# else: isRoot = False
# if i == len(nodeList)-1: keepDrawing = True
# else:
self.drawBody(node.pos, node.quat, node.radius,node.texUV)
return self.nodeList
def addNewBuds(self):
#TODO: GENERALIZE THIS SECTION
# budPos = budHpr = []
# rad = maxL = 0
[gH,gP,gR] = self.getHpr(base.render) # get this branch global Hpr for later
# nbud = budPerLen * self.length
# budPosArr = [x*minBudSpacing for x in range(self.length/minBudSpacing)]
# sampList = random.choice(budPosArr,nbud)
# sampList = random.sample(self.nodeList[_skipChildren:-1],5)
sampList = [self.nodeList[-1]]
for nd in sampList: # just use nodes for now
budPos = nd.pos
maxL = lfact*self.length
rad = rfact*nd.radius # NEW GEN RADIUS - THIS SHOULD BE A PARAMETER!!!
#Child branch Ang func - orient the node after creation
# trunk bud multiple variables
budsPerNode = random.randint(1,3) +1 # +1 since 1 branch will always "continue"
hdg = range(0,360,360/budsPerNode)
# budRot = random.randint(-hdg[1],hdg[1]) # add some noise to the trunk bud angles
for i,h in enumerate(hdg):
angP = random.gauss(budP0,budPnoise)
angR = random.randint(-45,45)
if i==0:
budHpr = Vec3(gH,gP,gR) # at least 1 branch continues on current heading
else:
budHpr = Vec3(gH+h,angP,angR)
self.buds.append([budPos,rad,budHpr,maxL])
def interpLen(self,inLen):
#BranchNode = namedtuple('BranchNode','pos radius fromVector quat texUV deltaL d2t')
outLen = []
for node in self.nodeList:
if node.deltaL >= inLen:
prevNode = node
break
delta = inLen - prevNode.deltaL
outLen = prevNode.pos + prevNode.fromVector*delta
#TODO: TOVECTORS ARE WRONG> THEY ARE REALLY FROM VECTORS...need To's
return outLen
def UVfunc(*args,**kwargs):
pass # STUB
def Circumfunc(*args,**kwargs):
pass # STUB
def PositionFunc(self,*args,**kwargs):
upVector = kwargs['upVector']
iSeg = kwargs['iSeg']
nAmp = kwargs['Anoise']
# branchSegs = kwargs['nSegs']
cXfactors = kwargs['cXfactors']
cYfactors = kwargs['cYfactors']
Lseg = float(self.length)/self.nSeg # self.length set at init()
relPos = Lseg*iSeg / self.length
dX = sum([term[0]*sin(2*pi*term[1]*relPos+term[2]) for term in cXfactors])
dY = sum([term[0]*sin(2*pi*term[1]*relPos+term[2]) for term in cYfactors])
noise = Vec3(-1+2*random.random(),-1+2*random.random(),0)*nAmp
newPos = Vec3(0,0,0) + upVector*Lseg*iSeg + noise + Vec3(dX,dY,0)
return newPos
# def PositionFunc(self,*args,**kwargs):
# upVector = kwargs['upVector']
# iSeg = kwargs['iSeg']
# nAmp = kwargs['Anoise']
## branchlen = kwargs['L']
# branchSegs = kwargs['nSegs']
#
# Lseg = float(self.length)/branchSegs # self.length set at init()
# noise = Vec3(-1+2*random.random(),-1+2*random.random(),0)*nAmp
# newPos = Vec3(0,0,0) + upVector*Lseg*iSeg + noise
# return newPos
def RadiusFunc(self,*args,**kwargs):
# radius proportional to length from root of this branch to some power
rTaper = kwargs['rTaper']
relPos = kwargs['position']
# R0 = kwargs['R0']
newRad = self.R0*(1 - (1-rTaper)*relPos) # linear taper Vs length. pretty typical
return newRad
class GeneralTree(NodePath):
def __init__(self,L0,R0,numSegs,bark,nodeName):
NodePath.__init__(self,nodeName)
self.L0 = L0
self.R0 = R0
self.numSegs = numSegs
self.trunk = Branch("Trunk",L0,R0,2*numSegs)
self.trunk.reparentTo(self)
self.trunk.setTexture(bark)
def generate(self,*args,**kwargs):
# lfact = kwargs['lfact']
self.trunk.generate(Params)
self.trunk.addNewBuds()
children = [self.trunk] # each node in the trunk will span a branch
nextChildren = []
leafNodes = []
for gen in range(1,numGens+1):
Lgen = self.L0*lfact**gen
print "Calculating branches..."
# print "Generation: ", gen, " children: ", len(children), "Gen Len: ", Lgen
for thisBranch in children:
for ib,bud in enumerate(thisBranch.buds):
# Create Child NodePath instance
# experimental curvature terms
Params.update(cXfactors = [(.1*bud[3]*random.gauss(0,.333),0.25,0),
(.1*bud[3]*random.gauss(0,.333),.5,0),
(.1*bud[3]*random.gauss(0,.333),.75,0),
(.1*bud[3]*random.gauss(0,.333),1,0)])
Params.update(cYfactors = [(.1*bud[3]*random.gauss(0,.333),0.25,0),
(.1*bud[3]*random.gauss(0,.333),.5,0),
(.1*bud[3]*random.gauss(0,.333),.75,0),
(.0*bud[3]*random.gauss(0,.333),1,0)])
newBr = Branch("Branch1",bud[3],bud[1],self.numSegs+1-gen)
newBr.reparentTo(thisBranch)
# newBr.setTexture(bark) # If you wanted to do each branch with a unqiue texture
newBr.gen = gen
# Child branch Radius func
# Rparams['R0']= bud[1]
# Child branch position Func
newBr.setPos(bud[0])
lFunc = Lgen*(1-Lnoise/2 - Lnoise*random.random())
Params['Anoise'] = bud[1]*posNoise # noise func of tree or branch?
Params.update({'L':lFunc,'nSegs':self.numSegs+1-gen})
Params.update({'rTaper':rTaper**gen})
newBr.setHpr(base.render,bud[2])
#Create the actual geometry now
newBr.generate(Params)
# Create New Children Function
newBr.addNewBuds()
# just add this branch to the new Children;
nextChildren.append(newBr)
# use it's bud List for new children branches.
children = nextChildren # assign Children for the next iteration
nextChildren = []
if _DoLeaves:
print "adding foliage...hack adding to nodes, not buds!"
for thisBranch in children:
# for node in thisBranch.nodeList[_skipChildren:]:
self.drawLeaf(thisBranch,thisBranch.nodeList[-1].pos,_LeafScale)
#this draws leafs when we reach an end
def drawLeaf(self,parent,pos, scale=0.125):
leafNode = NodePath("leaf")
leafNode.setTwoSided(1)
leafMod.instanceTo(leafNode)
leafNode.reparentTo(parent)
leafNode.setPos(pos)
leafNode.setScale(scale)
leafNode.setHpr(0,0,0)
BranchNode = namedtuple('BranchNode','pos radius fromVector quat texUV deltaL d2t')
# fromVector is TO next node (delta of pos vectors)
# deltaL is cumulative distance from the branch root (sum of node lengths)
if __name__ == "__main__":
base = ShowBase()
# random.seed(11*math.pi)
_UP_ = Vec3(0,0,1) # General axis of the model as a whole
# TRUNK AND BRANCH PARAMETERS
numGens = 2 # number of branch generations to calculate (0=trunk only), usually don't want much more than 4-6..if that!
print numGens
L0 = 6 # initial length
R0 = 4 # initial radius
numSegs = 2 # number of nodes per branch; +1 root = 7 total BranchNodes per branch
taper0 = 1
_skipChildren = 0 # how many nodes in from the base to exclude from children list; +1 to always exclude base
lfact = 0.8 # length ratio between branch generations
# often skipChildren works best as a function of total lenggth, not just node count
rfact = 1 # radius ratio between generations
rTaper = .7 # taper factor; % reduction in radius between tip and base ends of branch
budP0 = 45 # a new bud's nominal pitch angle
budPnoise = 10 # variation in bud's pitch angle
bNodeRadNoise = 0.1 # EXPERIMENTAL: ADDING Vertex RADIAL NOISE for shape interest
posNoise = 0.0 # random noise in The XY plane around the growth axis of a branch
Lnoise = 3 # percent(0-1) length variation of new branches
_uvScale = (2,1.0/3) #repeats per unit length (around perimeter, along the tree axis)
# _BarkTex_ = "barkTexture.jpg"
_BarkTex_ ='./resources/models/barkTexture-1z.jpg'
# LEAF PARAMETERS
# _LeafTex = 'Green Leaf.png'
_LeafModel = 'myLeafModel7.x'
# _LeafModel = 'shrubbery'
# _LeafTex = 'material-10-cl.png'
# leafTex = base.loader.loadTexture('./resources/models/'+ _LeafTex)
leafMod = base.loader.loadModel('./resources/models/'+ _LeafModel)
# leafMod.setScale(.01)
leafMod.setZ(-.5)
# leafMod.setScale(2,2,1)
leafMod.flattenStrong()
_LeafScale = 4
_DoLeaves = 1 # not ready for prime time; need to add drawLeaf to Tree Class
bark = base.loader.loadTexture(_BarkTex_)
Params = {'L':L0,'nSegs':numSegs,'Anoise':posNoise*R0,'upVector':_UP_}
Params.update({'rTaper':rTaper,'R0':R0})
Params.update(cXfactors = [(.05*R0,1,0),(.05*R0,2,0)],cYfactors = [(.05*R0,1,pi/2),(.05*R0,2,pi/2)])
np = 6
plts = range(np**2) # 6x6 array
ds = 5.0
for it in range(10):
tree = GeneralTree(L0,R0,numSegs,bark,"my tree")
tree.generate(Params)
tree.reparentTo(base.render)
# DONE GENERATING. WRITE OUT UNSCALED MODEL
print "writing out file"
tree.setScale(1)
tree.setH(180)
tree.setZ(-0.1)
tree.flattenStrong()
tree.writeBamFile('./resources/models/sampleTree'+str(it)+'.bam')
p = random.choice(plts)
tx = ds*(p/np)
ty = ds*(p%np)
tree.setPos(tx,ty,0)
##############################
ruler = base.loader.loadModel('./resources/models/plane')
ruler.setPos(-(R0+.25),0,1) # z = .5 *2scale
ruler.setScale(.05,1,2) #2 unit tall board
ruler.setTwoSided(1)
ruler.reparentTo(base.render)
def rotateTree(task):
phi = 10*task.time
tree.setH(phi)
return task.cont
base.taskMgr.add(rotateTree,"merrygoround")
base.cam.setPos(0,-4*L0, L0/2)
base.render.setShaderAuto()
base.setFrameRateMeter(1)
# base.toggleWireframe()
base.accept('escape',sys.exit)
base.accept('z',base.toggleWireframe)
# pycallgraph.make_dot_graph('treeXpcg.png')
base.setBackgroundColor((.0,.5,.9))
base.render.analyze()
base.run()
#TODO:
# - clean up: move branch Hpr out of bud and into child branch property
# - choose "bud" locations other than branch nodes and random circumfrentially.
# - numSegs to parameter into branch; numSegs = f(generation), fewer on younger
# prob set a buds per length var
# - COULD do a render to texture for the last limbs and leaves; define multiple plane
# cross sections, create them, then append them to the last real node before RTT
# make parameters: probably still need a good Lfunc.
# Distribute branches uniform around radius.
# "Crown" the trunk; possibly branches. - single point; no rad func and connect all previous nodes to point
# define circumference function (pull out of drawBody())
#
this is also stored in http://jafp3dd.googlecode.com (SVN repo)