This page tracks my progress using Python to script the 3D modeling program Rhino in order to build the interior structure of 18th century ships. I chose such a project based on the limitations of 3D modeling. Rhino in general handles planar shapes most easily; it is when one edits more complex forms that its limitations become more apparent. Rhino’s strength however is its ability to model physical structures such as injection molded plastic, jewelry or, most importantly, woodworking.
Wooden ships have uniquely difficult features for such a program to manage. There are no 90 degree angles in a wooden ship- the construction is instead shaped by the unique intersection of hydrodynamics and the natural strengths of wood. For this reason, it seemed the only way that their shape could be generated was with the kinds of organic forms allowed by parametric modeling in rhino. This is the unique challenge that attracted me to such a task, the process of “building” a ship in 3D space. For this example, the subject is a frigate from the 1750s, but the program is generalizable to any ship of this era.
This example is mainly based on the plans from the book Architectura Navalis Mercatoria (1768) by the Danish shipwright and scientist Fredrik Henrik af Chapman.
Title page of 1768 edition (source)
from Architectura Navalis Mercatoria (1768)
a set of typical “lines” from plate III of A N M
This specific example uses the "lines" Chapman gives as typical of French designs of the 1750s. plate III. However, given the correct hull contours, this program can construct the interior fittings for any size of ship of the mid-18th century.
It is most accurate to Western European building techniques, (French, Netherlands, Britain, Spain) of the era 1705-1795. After this era, rapid technological change that accompanied the Napoleonic wars, as well as the burgeoning industrial revolution caused drastic changes in the interior structure of ships.
The program produces a complete interior structure of the ship. This currently consists of the keel, ribs, crossbeams, and lengthwise beams. This is enhanced with details such as windows and stern galleries
Grasshopper preview geometry
The program that creates these models is written in the GHPython environment within the Rhino plugin Grasshopper. GHPython essentially uses the Python coding language to control normal Rhino commands, allowing one to automate tasks such as modeling repetitive shapes.
However, the documentation for GHPython is spotty, much of it exists, but it is overshadowed by the older Rhino script language and pure Grasshopper documentation. Nonetheless, the pythonic approaches for controlling Rhino are powerful tools that eventually become straightforward. The true power of Python in Rhino is that it allows one to handle data in ways impossible in Rhino. Grasshoppers’ visual based scripting system does not handle patterns and nested lists as well as Python. As a result, manipulation of dictionaries, lists, and sets allows one to call specific commands with far more precision than in pure Grasshopper. Furthermore, it allows one to divide the output of the definition in a more concise manner, and to better assure the accuracy fo the definition.
The below definition is a sample of the code I used to generate the ribs in the center section of the ship. Like the process of lofting out a ship’s ribs in an actual shipyard, it takes the contours it should be following and then builds the interior structure inwards. The ribs thicken as they near the keel, assuring a stiff construction with extra support for the ballast.
Grasshopper function diagram
Grasshopper's strengths lie however in its intuitive visual coding structure. The GHpython module lends itself to this language as well. Functions like the code below can be called as tools within the grasshopper definition, and inputs and outputs can be specified and tailored to the model with which one is working.
First, the import statements which feed modules to the programs. Then, the loop that instantiates the inputs, and feeds them to the various definitions. Following that are the functions that manipulate data within this GHPython module
rhinoscriptsyntax are the rhino commands rewritten as callable python functions.
likewise, ghpython.lib as ghcomp are grasshopper commands callable as python functions.
import Rhino allows one to instantiate the objects as Rhino geometry, when needed
import rhinoscriptsyntax as rs
import ghpythonlib.components as ghcomp
import Rhino
This loop requires some inputs to lay the groundwork, as can be seen in the GHPython module at the top. This consists of the beginning and end points of the section of parallel ribs, the number required, and then the mesh itself which has been lofted from the lines in Chapman’s illustrations.
rs.EnableRedraw(False)
start, ending = x[1], x[0]
start = rs.AddPoint( start[0],(start[1] + 10), start[2])
obj = rs.AddLine(ending, start)
hull = ghcomp.MeshfromSubD(hull, 2)
points = rs.DivideCurve(obj, iter)
intr_lines =[]
points2 = []
planepoints_f, planepoints_r = [], []
riblines0, riblines1 = [], []
rib_ins_0, rib_ins_1 = [], []
ribsolids = []
for i, point in enumerate(points):
norm = rs.CurveNormal(obj)
planepoint_f = rs.PlaneFromNormal(point, norm)
planepoint_f = rs.RotatePlane(planepoint_f, 90, planepoint_f.XAxis)
planepoints_f.append(planepoint_f)
point2 = rs.AddPoint( point[0],(point[1] - 10), point[2])
points2.append(point2)
planepoint_r = rs.PlaneFromNormal(point2, norm)
planepoint_r = rs.RotatePlane(planepoint_r, 90, planepoint_r.XAxis)
planepoints_r.append(planepoint_r)
ribline0 = ghcomp.MeshXPlane(hull, planepoint_r)
ribline1 = ghcomp.MeshXPlane(hull, planepoint_f)
ghcomp.SimplifyCurve(ribline0)
ghcomp.SimplifyCurve(ribline1)
sweep, boxes = get_rib_ends_sweep(ribline0, ribline1, planepoint_r, planepoint_f)
ribsolids.append(sweep)
ribflatsl = rs.MirrorObjects(ribsolids, ending, start, copy=True)
rs.EnableRedraw(True)
end of loops
This function calls out all the necessary tasks to build the lines into a smooth NURBS solid without much distortion in the lines.
For each pair it first calls rib_split_bylines() to return only the needed side of the rib.
Then, it assembles a pair of outer ribs and inner ribs for both the front plane, and and the rear. In all of this project i have followed the convention of working stern to bow.
From this pair of pairs [(the rear outer rib, the rear inner rib) , (the front outer rib, the front inner rib)] and the points along them generated by curve_in_ends(), it then creates boxes using rib_box_build()
These boxes are then scaled larger as they approach the center by scale_rib_boxes_indv()
Finally, the definition lofts a surface through these boxes, assuring a much smoother solid than rhino commands such as Sweep2 create.
def get_rib_ends_sweep(ribline0, ribline1, planepoint_r, planepoint_f):
#head def of the sweep function
ribline0 = rib_split_bylines(ribline0)
ribline1 = rib_split_bylines(ribline1)
riblines0.append(ribline0)
riblines1.append(ribline1)
ribline0_ends, rib_0_in, rib_0in_ends = curve_in_ends(ribline0, planepoint_r)
ribline1_ends, rib_1_in, rib_1in_ends = curve_in_ends(ribline1, planepoint_f)
rib_ins_0.append(rib_0_in)
rib_ins_1.append(rib_1_in)
boxes = rib_box_build(ribline0_ends, ribline1_ends, rib_1in_ends, rib_0in_ends)
boxes = scale_rib_boxes_indv(boxes)
sweep = rs.AddLoftSrf(boxes, simplify_method=0, value=0, closed=False)[0]
rs.CapPlanarHoles(sweep)
return sweep, boxes
this function splits the line by the world xy plane
def rib_split_bylines(line):
zy_plane = rs.WorldYZPlane()
zy0 = ghcomp.CurveXPlane(line, zy_plane)
print("zy0", zy0[1])
if zy0[0] != None:
line = ghcomp.Shatter(line, zy0[1])[0]
print("line", line)
return line
else:
print(zy0)
makes the beams get larger as they reach the keel of the ship- as the beams were constructed in real life.
def scale_rib_boxes_indv(boxes):
for box in boxes:
old_boxes.append(box)
newboxes =[]
x = 0
for i, box in enumerate(boxes):
if i < (len(boxes)-1):
x = (x + 0.04) #changeable value
else:
x = x + 1.6 #changeable value
vec0 = rs.VectorCreate(box[3],box[0])
vec0 = rs.VectorScale(vec0, x)
vec1 = rs.VectorCreate(box[2],box[1])
vec1 = rs.VectorScale(vec1, x)
newbox = ghcomp.PointDeform(box, (box[3],box[2] ), (vec0, vec1))
all_boxes.append(newbox)
newboxes.append(newbox)
return newboxes
For each ribline, this function first finds the endpoints of the curve.
It then offsets the curve of the plane’s intersections with the hull
It then uses the above function rib_split_bylines() to split the smaller intersection according to the line of symetry of the hull
This completed, the lines are fed through rib_rotate_intersect() to generate eventual boxes for the ribs.
It then sorts these points and returns points along both the smaller and larger section of single rib.
def curve_in_ends(rib, plane):
rib_endpts = ghcomp.EndPoints(rib)
num = 18
rib_mids = rib_rotate_intersect(rib, num)
rib_ends = [rib_endpts[1]]
rib_ends.extend(rib_mids)
rib_ends.append(rib_endpts[0])
rib_ends = ghcomp.SortAlongCurve(rib_ends, rib)[0]
print("RIB: \n", rib_ends)
rib_in = ghcomp.OffsetCurve(rib, 8, plane, 3)
rib_in = ghcomp.SimplifyCurve(rib_in)[0]
rib_in = rib_split_bylines(rib_in)
rib_in_endpts = ghcomp.EndPoints(rib_in)
rib_in_mids = rib_rotate_intersect(rib_in, num)
rib_in_ends = [rib_in_endpts[0]]
rib_in_ends.extend(rib_in_mids)
rib_in_ends.append(rib_in_endpts[1])
rib_in_ends = ghcomp.SortAlongCurve(rib_in_ends, rib_in)
rib_in_ends = rib_in_ends[0]
#print("RIB OFFST: \n", rib_in_ends)
return rib_ends, rib_in, rib_in_ends
This generates points along the ribs.
The function spins a plane around by changeable variables in angle.
This allows the user to select the level of detail that the eventaul rib NURBS solids will take.
def rib_rotate_intersect(rib, num):
intersects = []
zee = 375
origin = Rhino.Geometry.Point3d(0,0, zee)
ox,oy = Rhino.Geometry.Point3d(2000,0, zee), Rhino.Geometry.Point3d(0,2000, zee)
plane = Rhino.Geometry.Plane(origin, ox, oy)
i = 97/num
for n in range(1, num):
intersect = ghcomp.CurveXPlane(rib, plane)
intersect = intersect[0]
#intersect = Rhino.Geometry.Point3d(intersect)
print(intersect)
intersects.append(intersect)
plane = rs.RotatePlane(plane, i, plane.YAxis)
zee = zee-(i*3)
plane = rs.MovePlane(plane, (0,0,zee))
intersects.reverse()
return intersects
The points generated from running curve_in_ends() for two continuous ribs are then assembled together into groups of four.
A polyline is then drawn through this collection of points, then appended to a list. The list for each rib is then passed to a list of lists, boxes.
def rib_box_build(ribline0_ends, ribline1_ends, rib_1in_ends, rib_0in_ends):
boxes = []
for i, point in enumerate(ribline0_ends):
pts = (point, ribline1_ends[i], rib_1in_ends[i], rib_0in_ends[i], point)
print("PTS:: " , pts)
box = Rhino.Geometry.Polyline(pts)
boxes.append(box)
return boxes