Irregular Stone Walls
Some things you just can’t code.
Table of Contents:
- Introduction
- Black-and-White Wall Profile Image
- Extracting Contours
- Generating Textures
- Making the Stone Set
- Making the Wall
- Observations and Considerations
- Resources
Introduction
(Top)
Regular brick and stone patterns lend themselves to code. Loops, modulus 2 to alternate the rows, and an identical brick/stone shape and you can lay them out all day, any size (your computer can handle). But stone masons set out to vex us, they sometimes just toss order and symmetry out the window. My second station structure project, D&RG station at Antonito, Colorado, has an irregular layoutof consistent-height stones:
Note that the horizontal pattern has only a semblance of order, and the courses aren’t all the same height. Add in the window sill and pointed crown stones, and there’s no way to code the pattern, ‘cept to lay out individually crafted stones.
So, if you can’t re-create a pattern, why not use the original one? A photograph of the wall
captures the essential information, so how would one go about translating that into a shape
for printing? Now, one rather straight forward way to do this is with photogrammetric tools.
Using a laser rangefinder or LIDAR instrument, a wall can be scanned to read an array of ranges
that can be directly converted to a mesh. Haven’t tried that yet.
Another really straightforward way would be to just use a photograph of the wall. Most performant CAD programs can build a heightmap from an image, using the brightness of each pixel to make the height values. I did try this, but unmodified photographs don’t cleanly convey all the stone outlines, and the tonality of the other parts of the image can confuse the pattern. I tried hand-coloring the image to clean up such, but that was tedious work.
The technique I finally cobbled together starts with a black-and-white image of the stone pattern, black stones and white mortar. All it conveys is shape and position, no texture. This image is run through a program to extract the contours of the stone outlines, readily identifiable as the black-white edges between mortar and stone. The program produces a set of OpenSCAD-syntax polygon arrays. Next, the stone textures are generated using noisetool, another program I wrote for that purpose, described here:
Textures for OpenSCAD with noisetool
Then, the contours and textures are used to generate a set of stones. Finally, an OpenSCAD script loads the stone meshes to lay out the wall. Specifics of this workflow are given below.
Note: This is a complicated workflow, involving three programs run from the command line, along with shell scripting. That said, I’m going to attempt to describe things generically, with the idea that the workflow can be translated into other CAD environments.
Black-and-White Wall Profile Image
(Top)
I’ll describe the workflow with one of the walls for the Antonito station. For this step, here’s the objective:
The image can be created any one of a number of ways, probably the easiest to understand is to draw it with a paint program like Microsoft Paint. The key attribute is the distinction between the black stones and the white mortar, important to the program used in the next step. Oh, it also needs to be a PNG image. Note the larger white voids, the center one is for the window opening and the narrow side ones are for mounting the corbels.
The image width and length, and the corresponding rows and columns of pixels, determines how things in the image relate to dimensions in the CAD model. Note: OpenSCAD is dimension-agnostic, it treats unit values as whatever measurement you want. Also, the surface() module in OpenSCAD interprets every row and column in a text or pixel input to take up quantity 1 in the model. Accordingly, it would be straightforward to treat every pixel in the image as a unit of 1 inch in width and height. But, in my early experiments with HO scale stones, 1 inch of texture resolution is rather coarse. So, I use 1 pixel = 1/2 inch when making the image, then later when the stone is created I scale it down by half, or scale(0.5). This is the convention in the sample image which is 172x262 pixels; this translates to a wall that is 86x131 inches (width x height).
I tried drawing this image by hand in Paint, but it was really tedius to lay out straight lines and I had to do a lot of erase-redo. So I scrapped that approach and went looking for a way to draw the image with a program or a script. This became a sub-hobby, and I finally wrote a program that reads a script to draw the image, gddraw. The program reads a script of drawing commands, here’s the top of the script used to draw the example wall:
$s=2;
$w=86;
$h=131;
$sw=$w*$s;
$sh=$h*$s;
#window dimensions
$wx1=24*$s;
$wy1=32*$s;
$ww1=34*$s;
$wh1=77*$s;
## create a new image (width, height)
image($w*$s, $h*$s);
fillcolor('black');
rectangle(0, 0, $w*$s, $h*$s);
linecolor('white');
pensize(2);
#Border:
rectangle(0, 0, $sw-1, $sh-1);
#First course:
$c=12;
$b=0;
$t=$b+$c*$s;
line(0,$t,$sw,$t);
#verticals:
line(30*$s,$b,30*$s,$t);
line(60*$s,$b,60*$s,$t);
...
...
The complete file can be had here: necornerwall.gddraw
If you think this looks like Perl, you would be astute. I started with Perl and the GD graphics library; indeed, I drew all the walls with that. But I couldn’t come up with a way to easily do that on a Windows computer, so after a long hiatus from modeling I came up with gddraw. The script language looks like Perl because I wanted to translate the Perl scripts to gddraw with minimum change to the logic. Here are the language features:
- Each statement occupies a single line.
- Variables can be defined as “$var=value;” where the variable name starts with a ‘$’. The value can either be a number or a simple math expression.
- Image drawing commands have the form, “cmd(parm,parm,parm,…); parameters can be either numbers or simple math expressions. A list of available commands can be had by just running gddraw without an input file.
An overview of the language can be found at the Github repo, link at the bottom of the post.
At the top of the script, note the variable definitions $w and $h, set to the wall’s width and height values in inches. Following that are the definitions of $sw and $sh, equal to the respective width or height x 2. Also, there’s a variable $s set to 2. This variable is used to scale numbers to the right size for the image, and keep the numbers you specify as inches.
To draw the Antonito station walls, I took advantage of the consistent height of each course. I laid out each of the horizontal mortar lines, then saved that script as a template for the other walls.
However you create the image, make sure it uses black and white to unambiguously distinguish stones from mortar; this is needed for the nominal performance of the program in the next step. It also needs to have a white border so the next step doesn’t try to make a large image-sized stone.
Extracting Contours
(Top)
The next step is to extract the contours from the image. I wrote a program to do that, aptly named ‘contours’. It’s a command line program; you can get a .zip file containing it at the Github repository for contours, link in Resources. contours uses about nine functions from a humungous image processing library named OpenCV, the most important one being FindContours(). When you extract the .exe file from the .zip file, put it in a place where your favorite command shell can find it, nominally in a directory listed in your PATH variable. Alternatively, you can just make a directory for all your work, put contours.exe in it along with the other files and run it from there.
Running ‘contours necornerwall.png’ will spit out a bunch of OpenSCAD code; it’s prudent to pipe that to a file, like this: ‘contours necornerwall.png > contours.scad’. contours.scad will then contain a set of OpenSCAD array definitions for various uses, documented in the Github README. The most useful (and largest) array is p; this array contains arrays that define the polygon of each stone in the image. You can then render all these polygons with a simple script, like this:
Note1: Normally, in the loop definition, len(p)-2 would be len(p)-1, but the last polygon is a wall-sized stone, need a better white border.
Note2: With respect to the XY plane where we’ll model the wall, the image polygons are upside down.
This is because the origin (0,0) of the image is in the top-left corner, in OpenSCAD the origin is
at the bottom-left as we’re going to build the wall. This will be fixed with a rotate() or mirror()
call when the wall is completed.
Note3: We didn’t have to translate() each stone into position. This is because the contour coordinates are already wall-oriented, so each stone automatically knows where to go.
Generating Textures
(Top)
This step generates a text texture for each stone. Another command line program, noisetool, is used for the task, using a noise network that looks like this:
It uses a RidgedMulti noise generator to make the ridged topography predominant on most “broken” stones. This generator leaves flat areas between the ridges, so its output is combined with a Perlin generator to fill in those areas. The Rotate node was an experiment with re-orienting the ridge topography, doesn’t make much difference. The ScaleBias node scales the texture more/less pronounced. Finally, in the output module the edgegradient parameters define a curve-to-zero modification of the texture edges, corresponding to how stone is cut into blocks.
In irregular stone patterns, the texture has to be translated to the respective stone’s position. To accommodate that, contours includes another array, ‘pt’, which contains translate coordinate triples for each contour, indexed in the same order as ‘p’.
The actual generation of the textures is described in the next section.
Making the Stone Set
(Top)
This could be done in the OpenSCAD script that makes the wall. However, I’ve found that putting the entire burden of generating the meshes that make up the wall brings my computer to its knees. So I decided to split out the stone generation, make a set of .stl files one for each stone, and import them into the wall script already meshed. The command-line programs also need to be worked into the flow, so I wrote a bash script to do the whole stone generation thing from image to stone set. Called wallstone.sh, here ’tis:
#!/bin/bash
#Usage: wallstone.sh <imagefile>
#generate openscad include file.
contours $1 border=3 epsilon=0.0 >contours.scad
#generate stone width/heights for use in generating textures
declare -a pw=$(contours $1 border=3 epsilon=1.0 bashwidths)
#generate each stone as a texture heightmap, looping through the pw bash array
for ((i=0;i<${#pw[@]};i++))
do
#calculate a random bounds with which to sample the noise topology
bounds=$((1 + $RANDOM % 150)),$((1 + $RANDOM % 150)),"${pw[$i]}"
#generate the texture into texture.txt
noisetool ridgedstone1.txt destfile=texture.txt bounds=$bounds destsize="${pw[$i]}"
#run OpenSCAD in batch mode to assemble the stone with the stone.scad script
openscad --backend=manifold -Dst=$i stone.scad -o $i.stl
done
and the stone.scad script looks like this:
include <contours.scad>
bt=6;
sc=1.04;
translate([0,0,bt])
intersection() {
translate(pt[st]) scale(sc) surface("texture.txt");
linear_extrude(bt) polygon(p[st]);
}
linear_extrude(bt) polygon(p[st]);
A lot to unpack…
In the wallstone.sh script, there are two invocations of contours. The first one does what
we described earlier, generates contours.scad with all the arrays. There are a couple of
parameters added, border=3 makes a three-pixel white border around the image edge.
epsilon=1.0 engages a (slight) simplify algorithm that reduces the number of polygon points.
The second run of contours generates a set of stone width,height pairs for use in running noisetool. Running ‘contours bashwidths’ disables all the OpenSCAD array generation and instead pukes out a set of width,height values corresponding to each stone, in a bash-ingestable form. The way contours is invoked in the script pipes its output into a bash variable, pw.
The loop iterates through pw to do what’s described in the comment lines:
- calculate a random bounds with which to sample the noise map
- use the random bounds and ridgedmulti1.txt to run noisetool to make texture.txt
- use stone.scad in a command-line invocation of OpenSCAD to integrate texture.txt and the contour polygon p[i] to make a .stl file named with the loop index.
stone.scad makes the texture using texture.txt in a surface() invocation and intersects it with an extruded polygon of the contour. This shapes the texture to fit the contour. The second linear_extrude makes the backing cube to add thickness to the stone. Note that the variable st is not declared in the script; it is passed from the command line with the ‘-Dst=$i’ parameter in the openscad command.
The result from wallstone.sh is a set of .stl files, 1.stl, 2.stl, etc. corresponding to the stones of the wall.
Making the Wall
(Top)
The wall is all OpenSCAD script:
include <contours.scad>
w=88;
h=132;
t=8.9;
module NECornerWall() {
//stones:
rotate([90,0,0])
translate([w,h+0.5,-2])
rotate([0,0,180])
for (i = [0:1:len(p)-2]) {
translate(pr[i]) scale(0.5) import(str(i,".stl"));
//scale(0.5) translate([0,0,5]) translate(pc[i]) linear_extrude(3) text(str(i));
}
//wall/window cutout:
difference() {
cube([w,10,h+t]);
translate([26,-12,31]) cube([33,24,79]);
}
}
cutter=24;
scale(25.4)
scale(1/87)
difference() {
NECornerWall();
translate([-7.5,8,h/2])
rotate([0,0,-22.5])
cube([cutter,cutter,h*2], center=true);
translate([w+6.8,8,h/2])
rotate([0,0,22.5])
cube([cutter,cutter,h*2], center=true);
}
This version of the script doesn’t include the window and door and corbel models, just the code needed to produce the wall. The module NECornerWall() first runs a loop to import the stones, then creates the wall structure as a cube of the wall dimensions differenced with a cube to cut out the window opening. Note that the stone loop is prepended with a rotate of 180 degrees on the Z axis to put the stone set right-side-up, followed by the translate to move the wall back into position.
The call to NECornerWall() is surrounded by the code to cut the wall corners to 22.5 degrees in order for them to properly meet the corners of the adjacent walls. All of that is prepended with the scale() calls to scale the model in prototype inches to HO scale printing millimeters.
Here’s a screenshot of the script in action:
Note the commented-out line in the NECornerWall() module with the text() module at the end; it writes the contour number on top of the corresponding stone. This is used in a rather hacky way with an array in contours.scad to translate selected stones in or out of the rest of the stone set. The array, ‘pr’, is a set of [0,0,0] triples corresponding to the contours; the zeros are by-hand replaced with values that are used in the translate() call in the import() line.
For instance, the window sill stone is supposed to stand proud of the wall by about 1 inch. If the window sill stone is contour #9, then pr[9] is manually set to [0,0,1] and the sill is translated proud by that value. So, uncomment the text() line and render; note the numbers of the stones to be made proud, then open contours.scad in a text editor and change the specific pr[0,0,0] triples to [0,0,1].
Observations and Considerations
(Top)
What a convoluted way to make a stone wall. Not only that, the process relies on three command line programs. How many of you are still running command line programs?
With all that in mind, I decided to pack up a demo .zip file containing all the programs and scripts to generate the northeast corner wall of the Antonito depot; link to irregular_stone.zip is in the Resources section. Unpack the .zip file contents to a directory, follow the instructions in the included readme.txt and you end up with an OpenSCAD model of the wall. Once you’ve proved you can do that, you can draw your own stone wall profile image and use the programs and scripts unmodified to generate your own wall. You will have to modify stone_wall.scad to accommodate your wall dimensions, door/wall cutouts, and beveled corners.
Of note, I included a wallstone.bat equivalent of wallstone.sh, which runs in a Windows command shell. I figured that not too many model railroaders would also want to take on Unix stuff in addition to the command line shenanigans. Window batch script is not one of my programming languages of proficiency; I tested the script, watched it work successfully, but that doesn’t mean it’s problem-free. If you find errors in the batch script, you can find me at the Model Railroad Hobbyist forum.
I release the scripts to the public domain, have fun with them. The programs have their own assigned open-source licenses, available for perusal at their Github repositories.
Resources
(Top)
The three programs:
- https://github.com/butcherg/gddraw
- https://github.com/butcherg/contours
- https://github.com/butcherg/noisetool
A .zip file with all the programs and scripts described above is here: irregular_stone.zip