""" a plant/tree featuring simple procedural modelling (l-system like). this version for kalleria3d. made for interactive controls: besides growing, and 'ungrow' (revert). done: - simple working 3d, using coordsys transformations .. perhaps this works as a soya coordsys tutorial, as there is none other? - branch class to get reference to the previous one when removing branches * now also almost all of the growth code there todo/wishlist: - parameterzation of the growth * this is little/perhaps similar to Weber&Penn (p119-weber.pdf, siggraph '9x) where lot of control and nice results. that technique could be drawn from - mesh support for body (now edges/faces (lines) only) * change the current branches from lines to vectors (new_vector now there) construct meshes based on those (Weber&Penn uses cones etc.) - leaves (one solution also in Weber&Penn) - smooth growing/reverting (animate) * make 'todo' in grow(), work based on that in advance_time - many in parallel or still one by one? - .. - good interactive controls (for VJing) - better camera automatization, + controls - support use from outside, e.g. for generating game scenes * some nice generating method / class, instead of step-by-step growing .. or would some other model be better for that, if this is for VJing? """ import random import soya, soya.sdlconst from math import * class Plant(soya.World): """a plant/tree featuring simple procedural modelling (l-system like). made for interactive controls: besides growing, and 'ungrow' (revert). """ def __init__(self, scene): soya.World.__init__(self, scene) self.trunk = Branch(self, None, #this is the only branch with no prev (soya.Point(self, 0,0,0), soya.Point(self, 0,1,0))) self.branches = [self.trunk] #all branches self.branchfrom = self.trunk #the branch which the next new will extend self.branchfrom_queue = [] #queue for coming branchpoints, used as FIFO #options (no random yet) #self.d_angle = 90 - 35 #not used now (in 3d version) self.length_ratio = 0.8 self.branchamount = 3 def next_branchfrom(self): """updates the internal pointer to next branchpoint, based on the internal queue of points to branch from. """ try: self.branchfrom = self.branchfrom_queue.pop(0) except IndexError: self.branchfrom = self.trunk print "branchfrom queue empty, branching set to trunk." def grow(self): if len(self.branchfrom.branches) == self.branchamount: #branching limit achieved self.next_branchfrom() #making 3d - endpoints on a circle. #should make use d_angle again interval_angle = 360.0 / self.branchamount branch = self.branchfrom.branch(interval_angle, self.length_ratio) self.branches.append(branch) #needed? just to get latest in remove? self.branchfrom_queue.append(branch) return True def revert(self): try: latest = self.branches.pop() #takes latest branch away except IndexError: #this is the trunk pass #print "no branches to remove" else: #a branch was removed try: self.branchfrom_queue.remove(latest) #also from the branchpoint queue except ValueError: pass #print "removed branch not in branchfrom_queue", latest, self.branchfrom_queue latest.remove() #finally the actual removal (Plant, prev.branches) #this is safe because the trunk is not in Plant.branches #XXX this is not safe now, crashes when removing 0/1 branch! if len(latest.prev.branches) == 0: #was last in there self.branchfrom = latest.prev #from what removed branched #this is why the whole Branch class was introduced.. #otherwise branchfrom remains (is same as latest.prev?) class Branch: """Plants consist of these. To hold references to prev branches, to know the 'right' order when 'ungrowing' might not make sense - was not adding this to avoid 'over-OO'. now does include all the 3d data, hopefully nice when adding shape """ def __init__(self, world, prev, line): #for removing self.world = world #for the linked list / tree structure self.prev = prev self.branches = [] #the visual 3d data v1 = soya.Vertex(world, diffuse = (0.0, 1.0, 1.0, 1.0)) v1.move(line[0]) v2 = soya.Vertex(world, diffuse = (0.0, 1.0, 0.0, 1.0)) v2.move(line[1]) #when animating, this will happen gradually self.mesh = soya.Face(world, (v1, v2)) self.cs = soya.CoordSyst(self.world) #applies Plant rotation #info for Plant.grow .. that now actually is in self.branch self.begin = self.mesh.vertices[0] self.end = self.mesh.vertices[-1] self.update_info() def update_info(self): #for trunk, called when mouse has moved .end #local coordsys for branches of this branch to use #i.e. for making upcoming new branch relative to thís old self.cs.move(self.begin) #beginning of this branch self.cs.look_at_y(self.end) #orientation to match .. self self.cs.move(self.end) #beginning of next branch #.. as there is no .orientation to set&get, this an easy way to do.. def branch(self, anglemultiplier, length_ratio): length = (self.begin >> self.end).length() * length_ratio angle = anglemultiplier * len(self.branches) dx = sin(radians(angle)) dz = cos(radians(angle)) dy = 1 #all to same height (first?). this could apply d_angle endpoint = soya.Point(self.cs, dx, dy, dz) new_vector = self.end >> endpoint #to adjust length new_vector.set_length(length) end = soya.Point(self.world, #new_vector in self.cs translated to Plant self.end.x + new_vector.x, self.end.y + new_vector.y, self.end.z + new_vector.z) branch = Branch(self.world, self, (self.end, end)) self.branches.append(branch) return branch def remove(self): self.prev.branches.remove(self) self.world.remove(self.mesh) class PlantToy(soya.World): from soya.sdlconst import * keybindings = {K_ESCAPE: "soya.IDLER.stop()", K_LEFT: "plant.turn_lateral(-10)", K_RIGHT: "plant.turn_lateral(10)", K_UP: "plant.grow()", K_DOWN: "plant.revert()", K_a: "camera.set_xyz(camera.x, camera.y, camera.z - 0.1)", K_z: "camera.set_xyz(camera.x, camera.y, camera.z + 0.1)", } def __init__(self): #should get plant etc. (current globals) soya.World.__init__(self) soya.set_grab_input(1) def begin_round(self): soya.World.begin_round(self) #is there anything? for event in soya.process_event(): if event[0] == soya.sdlconst.KEYDOWN: try: cmd = self.keybindings[event[1]] except KeyError: pass else: eval(cmd) elif event[0] == soya.sdlconst.MOUSEMOTION: rel_x = event[3] rel_y = event[4] if len(plant.branches) == 0: #freely move trunk end mouse_x = event[1] mouse_y = event[2] mouse_pos = camera.coord2d_to_3d(mouse_x, mouse_y, (plant % camera).z) mouse_pos.convert_to(self) plant.branchfrom.end.x = mouse_pos.x plant.branchfrom.update_info() else: #spin the tree with x movement soya.set_mouse_pos(300, 300) #window middle would be nice soya.process_event() #resets rel mouse plant.turn_lateral(rel_x) #growth control in y axis if rel_y > 0: if plant.grow(): camera.z += 0.05 if rel_y < 0: plant.revert() if camera.z > 2: camera.z -= 0.05 if __name__ == '__main__': soya.init(width = 700, height = 700) scene = PlantToy() plant = Plant(scene) #globals (show for scene & plant) light = soya.Light(scene) light.set_xyz(1.0, 0.7, 1.0) light2 = soya.Light(scene) light2.set_xyz(-1.0, 0.9, 0.7) light3 = soya.Light(scene) light3.set_xyz(0.3, 0.8, -0.7) camera = soya.Camera(scene) camera.set_xyz(0.0, 1.2, 2.0) soya.set_root_widget(camera) #Start idler = soya.Idler(scene) idler.idle()