I wrote a nightly Rust library called immense for synthesizing 3D structures with simple composable rules, inspired by Structure Synth. In the docs I cover the basics, and in this article I’ll go over making a mesh from start to finish.

Here’s a little demo of how expressive it can be:

rule![
    tf![
        Tf::saturation(0.8),
        Tf::hue(160.0),
        Replicate::n(36, vec![Tf::rz(10.0), Tf::ty(0.1)]),
        Replicate::n(36, vec![Tf::ry(10.0), Tf::tz(1.2), Tf::hue(3.4)]),
    ] => cube()
]

Table of Contents

We’ll create this render:

To follow along I assume you have Rust and at least some familiarity with it. Also keep open the docs! They are thorough.

View a Mesh

Let’s output a mesh and see it before we start iterating.

rustup default nightly
cargo new --bin structure
cd structure

cargo install cargo-edit

cargo add immense
cargo add failure
cargo add rand

In src/main.rs, paste:

use failure::Error;
use immense::*;
use std::fs::File;

fn main() -> Result<(), Error> {
    let rule = cube();
    let meshes = rule.generate();
    let mut output_file = File::create("my_mesh.obj")?;
    write_meshes(ExportConfig::default(), meshes, &mut output_file)?;
    Ok(())
}

If you cargo run you should see a new file called my_mesh.obj.

Now we’ll need an object file viewer. I personally use MeshLab for its reload button. Open my_mesh.obj in MeshLab and you should see:

Notice the reload button I’ve highlighted. You can click this to refresh the mesh from disk whenever you cargo run to see your updates.

Synthesizing Our Structure

We’ll first create a diorama shape (marked in red), then make each tile in the planes into a piano key pattern (marked in blue).

Diorama Shape

First we’ll define a function to make a grid of a given rule:

fn grid(rows: usize, cols: usize, tile: impl ToRule) -> Rule {
    rule![
            tf![
                Replicate::n(rows, Tf::tz(1.0)),
                Replicate::n(cols, Tf::tx(1.0)),
            ] => tile
    ]
}

and change our rule definition to

let shrunk_cube = rule![Tf::s(0.9) =>  cube()];
let rule = grid_of(5, 5, shrunk_cube);

This repeats a downscaled (at 0.9) cube 5x5. We downscale just so it’s easier to see the borders in the mesh viewer. It should look like this:

Now we’ll repeat this rule with some rotations to get a diorama shape:

fn diorama(size: usize, rule: impl ToRule + Clone) -> Rule {
	rule![
	    Tf::rx(-90.0) => grid(size, size, rule.clone()),
	    Tf::rz(90.0) => grid(size, size, rule.clone()),
	    None => grid(size, size, &rule)
	]
}
let rule = diorama(size: usize, shrunk_cube);

Piano Keys

Our piano key rule needs to be generated lazily so that each instance is potentially different in color and height . For this we’ll implement ToRule so that immense will call on it to generate a rule for each instance.

use rand::*;

struct PianoKey;

/// Generates a cube which is either slightly elevated or sligthly
/// depressed, and either white or black.
impl ToRule for PianoKey {
    fn to_rule(&self) -> Rule {
        let elevation: Tf = *thread_rng().choose(&[
	        Tf::ty(0.2),
	        Tf::ty(-0.2)
	    ]).unwrap();
        let color: Tf = *thread_rng().choose(&[
            // White
            Tf::color(Hsv::new(0.0, 0.0, 1.0)),
            // Black
            Tf::color(Hsv::new(0.0, 0.0, 0.0)),
        ]).unwrap();
        rule![
            tf![elevation, color] => cube(),
        ]
    }
}

Now we need to squeeze a few of these into the x and z dimensions of the unit cube so we can plug a piano keys rule into our diorama rule. To do that we’ll shrink each one on x to 1/keys and shift them -1* (0.5+0.5/keys). This is my best effort at a helpful diagram:

fn piano_keys(keys: usize) -> Rule {
    rule![tf![
        // Shift the cursor left to align our shrunk cubes]
        // with the unit cube.
        Tf::tx(-0.5 - (0.5/(keys as f32))),
        // Shift each soon-to-be-shrunk cube by 1/keys
        Replicate::n(keys, Tf::tx(1.0/(keys as f32))),
        // Shrink each cube down to 1/keys on x dimension
        Tf::sby(1.0/(keys as f32), 1.0, 1.0),
    ] => PianoKey {}]
}

Finally we’ll make this our rule and enable colors in our export config.

let rule = diorama(5, piano_keys(8));
let meshes = rule.generate();
let mut output_file = File::create("my_mesh.obj")?;
write_meshes(
    ExportConfig {
        export_colors: Some(String::from("my_colors.mtl")),
        ..ExportConfig::default()
    },
    meshes,
    &mut output_file,
)?;

You should see something like this in your viewer.

The full code for generating the mesh is:

use failure::Error;
use immense::*;
use rand::*;
use std::fs::File;

fn grid(rows: usize, cols: usize, tile: impl ToRule) -> Rule {
    rule![
            tf![
                Replicate::n(rows, Tf::tz(1.0)),
                Replicate::n(cols, Tf::tx(1.0)),
            ] => tile
    ]
}

fn diorama(size: usize, rule: impl ToRule + Clone) -> Rule {
    rule![
	    Tf::rx(-90.0) => grid(size, size, rule.clone()),
	    Tf::rz(90.0) => grid(size, size, rule.clone()),
	    None => grid(size, size, rule)
	]
}

struct PianoKey;

impl ToRule for PianoKey {
    fn to_rule(&self) -> Rule {
        let elevation: Tf = *thread_rng().choose(&[Tf::ty(0.2), Tf::ty(-0.2)]).unwrap();
        let color: Tf = *thread_rng()
            .choose(&[
                // White
                Tf::color(Hsv::new(0.0, 0.0, 1.0)),
                // Black
                Tf::color(Hsv::new(0.0, 0.0, 0.0)),
            ])
            .unwrap();
        rule![
            tf![elevation, color] => cube(),
        ]
    }
}

fn piano_keys(keys: usize) -> Rule {
    rule![tf![
            Tf::hue(160.0),
            Tf::tx(-0.5 - (0.5/(keys as f32))),
            Replicate::n(keys, Tf::tx(1.0/(keys as f32))),
            Tf::sby(1.0/(keys as f32), 1.0, 1.0),
        ] => PianoKey {}]
}

fn main() -> Result<(), Error> {
    let rule = diorama(5, piano_keys(8));
    let meshes = rule.generate();
    let mut output_file = File::create("my_mesh.obj")?;
    write_meshes(
        ExportConfig {
            export_colors: Some(String::from("my_colors.mtl")),
            grouping: MeshGrouping::ByColor,
        },
        meshes,
        &mut output_file,
    )?;
    Ok(())
}

Rendering

A real walk through on using a renderer is out of scope for this tutorial, but for fun’s sake I’ve prepared a blender file for our mesh for anyone who got this far and isn’t familiar with any renderers. Download

  1. Blender, a free and open source 3D toolkit with some renderers built in.
  2. The template blender file I prepared for this mesh.

Open the template file and import your mesh object file:

Press F12 and you should start seeing render progress!

When it’s done you can save your result by pressing F3.


If you have any issues with immense or want to request a feature, please submit a github issue.