Peach Tree Generator in Houdini

Jessica Beckenbach
9 min readFeb 24, 2019

--

This is my dailies blog for a peach tree generator I am creating for my final project for VSFX 406: Concept Development for Visual Effects. I intend to use this as a chance to become more familiar with vectors in Houdini.

Project Scope

End goal: The goal of this final project is to create a procedural tool that creates a peach tree. I intend to use this as a chance to become more familiar with vectors in Houdini.

Finished shots: 3–5 5 second turntables of various configurations of the peach tree tool.

Equipment used: SCAD’s computer labs, Houdini 17.0.352

Schedule:

Week 1: Making the trunk and branches
Week 2: Making/placing the leaves and peaches
Week 3: Making/placing peach blossoms and rendering

Update 1: Trunk and Branches

In progress trunk and branches

The basic vector code was based off of a low-poly tree generator made by Ryan Gold, a classmate of mine.

Making the trunk:

First, in a geometry SOP, plop down an add node and add a point at the origin. Now let’s put in a point wrangle and start mucking around with vectors.

//Make two parameters named "seed" and "maxAngle" 
//at the object level
float seed = ch("../seed");
float maxAngle = ch("../maxAngle");
//"topPoint" is actually {0,0,0} because of where we put the
//first point. It's used to generate the randomVector
vector topPoint = getbbox_max(0);
vector randomNumbers = set(rand(seed),rand(seed),rand(seed));
vector randomVector = sample_sphere_cone(topPoint,maxAngle,randomNumbers);
v@dir = normalize(randomVector);
//sets the point to spawn another point
setpointattrib(0, 'newPt', @ptnum, 1);

Let’s spawn the trunk. In the object level, make a parameter called trunkLength. Go back down to the geometry level and put down a for-each loop. Here’s the settings for the for-each loop:

//In for-each begin:
Method = "Fetch Feedback"
//In for-each end:
Iteration Method = "By Count"
Gather Method = "Merge Each Iteration"
Iterations = ch("../trunkLength")

Now put a point wrangle inside the for each loop. Here’s the code for it:

//"seed" is the same seed as before
float seed = ch("../seed");
//These are all new parameters to be made at the obj level
float minAngle = ch("../minAngle");
float maxAngle = ch("../maxAngle");
float gravityF = ch("../gravity")*@ptnum;
float gnarl = ch("../gnarl");
float leanX = ch("../leanX")*@ptnum;
float leanZ = ch("../leanZ")*@ptnum;
vector randNumbers = set(rand(seed+@ptnum),rand(seed*3+@ptnum),rand(seed*2+@ptnum));
float randAngle = radians(fit01(rand(ch('seed')), minAngle, maxAngle));
vector randVector = sample_sphere_cone(set(leanX,1,leanZ),randAngle,rand(@ptnum + seed));
vector gravity = set(0, gravityF, 0);
if(@newPt==1)
{
//get position of next point
v@dir = normalize(randVector) * gnarl;
vector pos = @P + @dir - gravity;
//add the next point
int new_pt = addpoint(0, pos);
//make sure it spawns the next point
setpointattrib(0, 'newPt', new_pt, 1);

//make the line that connects the two points
addprim(0, 'polyline',@ptnum,newPt);
}
//stop the old point from spawning any extra new points
setpointattrib(0, 'newPt', @ptnum, 0);

Now we have a line that will become our trunk. One problem — there are a bunch of points in the exact same spot. Put down a Fuse SOP to solve that. Then put down a Group SOP and name the group “trunkGroup.” Finally, put down an Attribute Delete SOP and delete all attributes except P.

Making the branches:

Wire a Polypath SOP underneath our trunk to start making our branches. Now put a Carve SOP underneath it, and check on “First U” and “Second U.”

//make two parameters, ch("../branchMin") and ch("../branchMax")
Values:
First U = ch("../branchMin")
Second U = ch("../branchMax")

Put down a point wrangle. In this wrangle, we’ll be determining which points of the trunk will grow branches.

//make these two parameters at the obj level
float seed = ch("../branchSeed");
float branchAmount = ch("../branchAmount");
float num = rand(@ptnum + seed);//determines which points grow branches
if (@ptnum != ch("../foreach_end1/iterations") && @ptnum != 0 && num < branchAmount)
{
setpointattrib(0, 'branchPt', @ptnum, 1);
}

Now put down yet another point wrangle. This one grows the first segment of each branch.

//same seed as above
float seed = ch("../branchSeed");
//make all these parameters at obj level
float minAngle = ch("../branchMinAngle");
float maxAngle = ch("../branchMaxAngle");
float minGnarl = ch("../branchMinGnarl");
float maxGnarl = ch("../branchMaxGnarl");
float leanX = ch("../branchLeanX");
float leanZ = ch("../branchLeanZ");
float gravityF = ch("../branchGravity");
float gnarl = fit01(rand(@ptnum+seed),minGnarl,maxGnarl);
float randAngle = radians(fit01(rand(seed),minAngle,maxAngle));
vector gravity = set(0, gravityF, 0);
vector randVector = sample_sphere_cone(set(leanX,1,leanZ),randAngle,rand(@ptnum + seed));if(@branchPt==1)
{
//sets direction
v@branchDirOrig = normalize(randVector) * gnarl;

//makes sure that the branches face upward
@branchDirOrig.y = abs(@branchDirOrig.y);

//sets point position
vector pos = @P + @branchDirOrig - gravity;

//make new point
int newPt = addpoint(0,pos);
//make sure next points will spawn correctly
setpointattrib(0,"branchDirOrig",newPt,@branchDirOrig);
setpointattrib(0,"branchPt",@ptnum,0);
setpointattrib(0,"branchPt",newPt,1);

//make line that connects old point with new
addprim(0,"polyline",@ptnum,newPt);
}

Now put down a for-each loop. The settings are the same as the previous for-each loop, except use ch(“../branchLength”) as the iteration amount. Wire a point wrangle into the for-each loop. Here we go:

//same parameters as before
float seed = ch("../branchSeed");
float maxAngle = ch("../branchMax");
float minGnarl = ch("../branchMinGnarl");
float maxGnarl = ch("../branchMaxGnarl");
float leanX = ch("../branchLeanX");
float leanZ = ch("../branchLeanZ");
float gravityF = ch("../branchGravity");
float gnarl = fit01(rand(@ptnum+seed),minGnarl,maxGnarl);
vector gravity = set(leanX, gravityF, leanZ);
v@randVector = sample_sphere_cone(v@branchDirOrig,maxAngle,rand(@ptnum + seed));if(@branchPt==1)
{
//all stuff that changes vector direction
v@branchDir = normalize(@randVector) * gnarl;

//this is a checkbox parameter at the obj level
//if checked on, then the vector will face the same quadrant
//that the point is in
if(ch("../branchOut")==1)
{
@branchDir.x = abs(@branchDir.x)*sign(@P.x);
@branchDir.z = abs(@branchDir.z)*sign(@P.z);
}

//makes sure that branch points up
@branchDir.y = abs(@branchDir.y);
//changes exactly where the branch points to
@branchDir.y*=(1+ch("../branchGravity2"));

//new point position and new line
vector pos = @P + v@branchDir - gravity;
int newPt = addpoint(0,pos);
addprim(0,"polyline",@ptnum,newPt);

//attributes to keep the loop going
v@branchDirOrig = v@branchDir;
setpointattrib(0,"branchPt",newPt,1);
setpointattrib(0,"branchDirOrig",newPt,@branchDirOrig);
}

Now let’s fuse all the branch points together with the Fuse SOP, delete all attributes except P with the Attribute Delete SOP, and put the bottom point into the trunkGroup that we made earlier. I did this by plopping this bit of code in a point wrangle:

setpointgroup(0,"trunkGroup",0,1,"set");

Now let’s merge the trunk and the branches together.

First, put down a Fuse SOP with no values changed. Next, put down a Fuse SOP, but set it to only affect the trunkGroup and set the Distance Value to 2. Now that the trunkGroup has served its purpose, we can delete it with the Group Delete SOP.

Put down a Smooth SOP, with the strength set to the parameter ch(“../smoothAmount”) from the object level. Use a Sort SOP to sort all the points “By Y.” This way, there won’t be any weird thicknesses in the final branch geometry. Now, put down a point wrangle.

//uses the point number to prepare the ramp
float gradient = float(@ptnum+1)/float(@numpt);
//makes the ramp. "trunkWidthRamp" (float) and
//"trunkWidthMultiplier" (ramp-float) are both parameters
//at the obj level
@width = chramp("../trunkWidthRamp",gradient)*ch("../trunkWidthMultiplier");
//makes sure that the bottom of the tree is on the ground
setpointattrib(0,"P",0,{0,0,0},"set");

The next point wrangle makes sure that the ends of the branches are the thinnest.

if(@ptnum!=0&&neighbourcount(0,@ptnum)==1)
{
@width = point(0,"width",@numpt-1);
}

Now, put down a Polywire SOP to actually get the shape of the tree. Change the Wire Radius to @width. This changes the thickness of the Polywire to follow our width attribute. Divide the tree. Then subdivide it using OpenSubdiv Bilinear to prevent smoothing. Subdivide it again, this time using OpenSubdiv Catmull-Clark to introduce some smoothing.

Moving Forward:

  • Getting a better smooth on the trunk
  • Adding leaves and blossoms

Update 2: Adding Leaves

Making the leaves:

The leaves are formed from a semicircle. Settings of interest:

Primitive Type: Polygon
Orientation: ZX Plane
Radius: {1,0.3}
Center: {ch(radx),0,0}
Divisions: 12
Arc Type: Open Arc
Arc Angles: {180,360}

The points of the leaf are then moved around in a point wrangle to get a more pointed shape on one end:

@P.x = (@ptnum>=11) ? @P.x+=0.1 : @P.x;
@P.x = (@ptnum==12) ? @P.x+=0.1 : @P.x;

It is then rotated -30 degrees in x and then merged with a line. For the line, a spare input is added. This spare input is connected to the transformed line. Line settings are as follows:

Direction: {1,0,0}
Length: bbox(-1,D_XMAX)
Points: npoints(-1)-1
Spare Input 0: /obj/treeGen/transform1

Fuse the points together and then use an Add SOP to delete the geometry but keep the points. Now it’s time to add the polygons that make up the leaf. Put the following code into a detail wrangle:

for(int i=2; i<11; i++)
{
addprim(0,"poly",i,i+1,i+12,i+11);
}
addprim(0,"poly",0,1,13);
addprim(0,"poly",1,2,13);
addprim(0,"poly",11,12,22);
//this assumes that you have 12 divisions on your circle and 12 points in your line

Reverse the polygons (if the fact that the polygons face the wrong way is bothersome) and then mirror it over the Z-axis. Put down a Bend SOP to bend the leaves. These were the settings I used:

Bend: -19.5
Capture Origin: {0.6,0,0}

Preparing the leaves to be copy stamped:

Make three parameters at the object level: leafHeight, leafDensity, and leafAngle. Connect a Resample SOP to the Smooth SOP of the trunk. Reference the leaf density parameter in the Length parameter of the Resample SOP. Next, use an add node to delete the geometry but keep the points. In a point wrangle, use this code to delete points:

float delValue = ch("../leafHeight");
if(@P.y<=@yVal*delValue)
{
removepoint(0,@ptnum);
}
//this deletes points by height. it makes sure that leaves don't grow on the trunk

After the point wrangle, use an Attribute Delete SOP to delete the yVal point and detail attributes. Put down another point wrangle. This one randomizes the orientation of the leaves:

float leafAngle = ch("../leafAngle");
f@angle = (@ptnum*leafAngle*rand(@ptnum))%360;
vector axis = rand(@ptnum*3);
@orient = quaternion(@angle, axis);

Now, put down a Copy to Points SOP. Plug the leaf into the first input and the points that were just made into the second input. Delete all attributes except P and merge it with the trunk.

Update 3

A more accurate way to place the leaves:

After the class was over, I decided to remake the leaf scattering because leaves don’t grow from a certain point upwards. They grow on the ends of branches. Simply put, my old method worked, but it was wrong.

Let’s get to it.

First, at the object level, delete the leafHeight parameter and replace it with leafScale.

Connect an Attribute Delete SOP to the Smooth SOP that was created in the trunk. First, in a point wrangle, find all the end points that aren’t in the trunk and put them in a group. Also, make an attribute to store the number of neighbors each point has. If a point has 1 neighbor, it is an end point.

Here’s the code:

if(@ptnum!=0&&neighbourcount(0,@ptnum)==1)
{
setpointgroup(0,"endPtGroup",@ptnum,1,"set");
}
i@neighbors = neighbourcount(0,@ptnum);

In the next point wrangle, if a point is in the End Point Group that was just created, store its neighboring point in an attribute called @neighborPoint.

i@neighborPoint;
if(inpointgroup(0,"endPtGroup",@ptnum)==1)
{
@neighborPoint=neighbour(0,@ptnum,0);
}

Put down another point wrangle. Not all of the end points connect directly to a branching point. Let’s find the points that aren’t branching points and find their branching points.

i@nNCount;
i@branchPt;
if(inpointgroup(0,"endPtGroup",@ptnum)==1)
{
@nNCount=point(0,"neighbors",i@neighborPoint);
//@nNCount is the number of neighbors the point has
if(@nNCount==2)
{
@branchPt=neighbour(0,@neighborPoint,0);
if(@branchPt==@ptnum)
{ //makes sure the branching point isn't the same as endPt
@branchPt=neighbour(0,@neighborPoint,1);
}
}
}

Now, in an Add SOP, delete all geometry but keep the points. It’s time to add lines only where we want leaves to grow. Throw the following code in a point wrangle:

if(inpointgroup(0,"endPtGroup",@ptnum)==1)
{ //for every end point
if(@branchPt==0)
{ //if it's directly connected to a branching point
addprim(0,"polyline",i@ptnum,i@neighborPoint);
}
else
{ //if it's not directly connected to a branching point
addprim(0,"polyline",@ptnum,@neighborPoint,i@branchPt);
}
}

In yet another Add SOP, remove unused points. In a Resample SOP, set Length to be ch(“../leafDensity”). In an Add SOP, delete all geometry but keep the points. Delete all groups and attributes except @P, and put down one last point wrangle. This wrangle determines the rotation of the leaves.

float leafAngle = ch("../leafAngle");
f@angle = (@ptnum*leafAngle*rand(@ptnum))%360;
vector axis = rand(@ptnum*3);
@orient = quaternion(@angle, axis);

Plug this into the copy to points node from Update 2. Now, this peach tree is growing leaves properly. One more thing: remember that leafScale parameter at the beginning of this update? Set uniform scale in the transform node at the end of the leaf nodes to equal ch(“../leafScale”) instead of 1.

--

--

Jessica Beckenbach

Houdini / Technical Artist. SCAD Visual Effects ‘20. Lives and breathes Houdini, Nuke, Unreal, and (sometimes) Maya.