Seashell Generator in Houdini

Jessica Beckenbach
5 min readApr 3, 2019
Rendered results.

This is a seashell generator created for Exercise 1 of VSFX 428: Particles and Procedural Effects. The goal of this project is to be comfortable with both VEX and VOPS. It is based off a SIGGRAPH paper, Modeling Seashells. The generator can create the shell using both VEX and VOPs. A drop down menu switches between the two methods of forming the shell. Additional features implemented include shell thickness, horizontal and vertical ridges, and ribs.

Parameters:

//Code settings:Use VEX / VOPs: Switches between the VEX and VOP versions of the seashell generator.Divisions: Controls the number of divisions of the seashell. Also changes the number of vertical ridges (see below).Number of Turns: Controls the number of whorls in the seashell.Initial Radius: Controls the initial radius of the shell.Radius Growth: Exponential control of the shell’s radius.Initial Y: Controls the initial y-value of the shell.Y Growth: Exponential control of the shell’s y-values.Initial Scale: Controls the scale of the shell in the XZ axes.Scale Growth: Exponential control of the shell’s XZ axes.//Other Settings:Horizontal Ridges: Changes the depth of ridges on the XZ axes.Vertical Ridges: Changes the depth of ridges on the Y axis.Ridge Amount: Changes the number of horizontal ridges.Extrude Thickness: Changes the thickness of the shell.Subdivision: Changes the subdivision amount of the shell.Circle Orientation: Changes the orientation of the generating curve.Grow Ribs:Turns growing ribs on and off.Rib Frequency:Changes the frequency of ribs.Rib Thickness: Changes how much the ribs stick out.Rib Width: Changes the width of the ribs.Rotation Variance: Varies the rotation value of the ribs slightly, allowing the ribs to be slightly misaligned.

Breakdown:

VEX:

//make parameters
int divisions = chi("divisions");
float numTurns = ch("numTurns");
float initRadius = ch("initRadius");
float radGrowth = ch("radiusGrowth");
float yInitial = ch("yInitial");
float yGrowth = ch("yGrowth");
float sInitial = ch("sInitial");
float sGrowth = ch("sGrowth");

//determine number of points
int npts = int(divisions * numTurns);

//make variables
float theta, radius, yy, scale;

for(int i=0; i < npts; i++)
{
//set theta
theta = 2.0 * M_PI * i/float(divisions);

//set radius, scale, y value
radius = initRadius * pow(radGrowth, theta);
scale = sInitial * pow(sGrowth, theta);
yy = yInitial - yInitial*pow(yGrowth,theta);

//determine point location and make point there
v@loc=set(radius*cos(theta),yy,radius*sin(theta));
addpoint(geoself(),@loc);

//make @N and @pscale
v@norm=set(-radius*sin(theta),0,radius*cos(theta));
setpointattrib(geoself(),"N",i,@norm);
setpointattrib(geoself(),"pscale",i,scale);
}

VOPs:

These two are wired into a switch node, which is controlled by the Use VEX/VOPs parameter.

Making the circle:

This circle is the one that gets copied onto the points from the previous section. Parameters of note:

Scale: 0.1
Divisions: ch("../ridgeAmt")

There are actually two circles with these parameters: one with the orientation of YZ and one with the orientation of XY. These two circles are fed into a switch node controlled by the Circle Orientation parameter. The purpose of this switch is to create flexibility in the direction that the circle faces so the user can change it directly on the top level rather than diving into the network.

The switch is fed into a point wrangle, which puts every other point in a point group to facilitate the creation of horizontal ridges.

if(@ptnum%2==0)
{
setpointgroup(0,"evenNum",@ptnum,1,"set");
//new point group is named evenNum
}

This then goes through a transform SOP to create the horizontal ridges. Parameters changed:

Group: evenNum
Group Type: Points
//takes the value of scale y only if the circle is XY
Scale X: if(ch("../circleOrient")==0,ch("sy"),1)
//scales down to create the ridges
Scale Y: 1-ch("../ridges")
//takes the value of scale y only if the circle is XZ
Scale Z: if(ch("../circleOrient")==1,ch("sy"),1)

Copying to Points:

The transform SOP is then fed into a copy to points SOP. The switch SOP that toggles between VEX and VOPs is fed into the copy to points SOP’s second input.

Creating Vertical Ridges and Skinning:

A primitive wrangle is connected to the copy to points SOP. This primitive wrangle takes every second primitive and puts it into a primitive group.

if(@primnum%2==0)
{
setprimgroup(0,"evenNum",@primnum,1,"set");
//new prim group is named evenNum
}

This then gets fed into a transform SOP to create the vertical ridges.

Group: evenNum
Uniform Scale: 1+ch("../verticalRidges")

A skin SOP is used to create the geometry of the shell. A fuse SOP is then connected to the skin SOP. This prevents the subdivide SOP from having a weird edge if the subdivide parameter is larger than zero. A polyextrude SOP then creates thickness on the shell. Extrusion thickness is set to ch(“../extrude”).

Making the ribs:

Pipe the copy to points SOP into a fuse SOP to consolidate points. Then use a delete SOP to delete circles that aren’t going to be turned into ribs.

Operation: Delete by Range
Select #1: ch("select2")-1
Select #2: ch("../ribFrequency")

Then use an attribute promote SOP to promote @pscale from a point attribute to a primitive attribute. A for-each primitive loop is used to create the ribs. Put a transform SOP and two polyextrude SOPs into the for-each loop.

In the transform SOP:

//ch(“variance”) is a spare parameter which has the value of 
//ch(“../rotVariance”), which is the rotation variance parameter
Rotate Y: fit01(rand(prim(0,0,”pscale”,0)),-ch(“variance”),ch(“variance”))

In the first polyextrude SOP:

//changes how far the rib sticks out. @pscale is used to change the scale of individual ribs
Distance: ch(“../ribThickness”)*prim(0,0,”pscale”,0)

In the second polyextrude SOP:

//changes the width of the rib. @pscale is used to change the scale of individual ribs
Distance: ch(“../ribWidth”)*prim(0,0,”pscale”,0)

This is the end of the for-each primitive loop. A switch node is then added. A null node goes into the first input, and the for-each loop goes into the second input. The Grow Ribs parameter `ch(“../ribs”)` controls whether ribs are grown or not.

Merge the switch node with the polyextrude SOP from the skinning part of the shell. A subdivide SOP is placed below the merge node. The Subdivision parameter `ch(“../subdiv”)` controls how much the shell is subdivided. A null node is placed at the end to signify the end of the node network.

--

--

Jessica Beckenbach

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