3. Meshes in Monocurl

META We'll now talk about meshes, the objects that we see on screen. This section is a bit long, but a lot of it is just examples. The optional sections discuss some of the decisions behind Monocurl, but are not that practical so feel free to skim them.

Accompanying Video

3.1 Following Along

In this section, you finally have many opportunities to follow along!

Simply create a new scene in the Monocurl editor, and in the slide_1 section, add the required code for creating the meshes.

We haven't officially discussed animations yet, but for now you'll have to use the following lines to show the meshes we create. You might also want to change the background to WHITE for some scenes with black meshes.

tree main = ...
p += Set:
    vars&: main

A full working example looks something like this (try it out!). Remember that these lines should be present in the slide_1 section.

tree circ = Circle:
    center: ORIGIN
    radius: 1
    tag: {}
    color: default

p += Set:
    /* replace circ with whatever variable contains the meshes */
    vars&: circ

3.2 Background Knowledge

IMPORTANT The final prerequisite before we actually get to meshes is understanding the coordinate system.

Monocurl operates in a right handed coordinate system (so positive z faces away from the screen). Other than that, the following image gives a good glimpse at the default system. Thus, if you want to have a circle span left to right, its radius should be 4.

Coordinate System

Also, whenever a function asks for a color, it means a vector of four numbers (red, green, blue, alpha), each in the range of 0 to 1. To actually change the color of most meshes, you'll likely have to click on the <> icons next to color (see functor modes from previous lesson).

3.3 Meshes

DEFINITION A mesh is a connected* collection of dots, lines, and triangles. Each vertex has an associated color.

Primitive meshes are meshes that you can create directly (i.e. no prerequisite meshes needed). Some common examples: Meshes example

Operator meshes (or more accurately, operators on meshes) take in one or more mesh-trees, and spit out a new a mesh-tree. Here are a few examples (you can find more in the documentation section):

3.4 Mesh-Tree

Typically, we want to consider not just a single mesh, but a collection of them. This is where mesh-trees come in.

DEFINITION A mesh-tree is defined recursively. A mesh is a mesh-tree. A vector of mesh-trees is a mesh-tree.

Informally, we can think of a mesh-tree as just a list of meshes, that might have similar such lists inside of them. Example (assuming all the variables are meshes): {mesh_a, mesh_b, {{{{mesh_c}}}}, {mesh_d, {mesh_e}}}.

REMARK To those who ask why we can't just use a flat vector, note that mesh-trees allow us to be sloppy with the structure of our variables, which makes programming much easier. It also provides some sort of abstraction where we can ignore the exact details of how functions structure their returned mesh-trees. For instance, notice the following simple example where the baloons vector is not a flat list:

func Balloon(position) = identity:
    let head = Circle:
        center: position
        radius: 0.5
        tag: {}
        stroke: BLACK

    let string = Line:
        start: head
        end: vec_sub(position, {0, 2, 0})
        tag: {}
        stroke: BLACK

    element: {head, string}

tree balloons = {}
balloons += Balloon:    
    position: {-1, 0, 0}
balloons += Balloon:    
    position: {1, 0, 0}

/* at this point balloons has the following structure: */
/* {{head_1, string_1}, {head_2, string_2}} */

p += Set:
    vars&: balloons

Balloons

IMPORTANT Most library functions dealing with meshes allow you to input an entire mesh-tree. Operations on a mesh-tree simply operate on all the meshes contained within the tree.

3.5 (Optional) Why meshes?

In many computer graphics contexts, Bezier curves are used instead of the meshes we just talked about. The decision to use meshes primarily comes as a way to make all screen objects the same type, which is harder to do with Bezier curves. On the other hand, there are instances where meshes are much less smooth than Bezier curves, which is undesirable.

For instance, the following would require many Bezier curve objects, but can be done in a single mesh.

Color Grid

3.6 Complex Example

To create complex scenes, you have to think about the constiuent parts, and how each own can be written as a pipeline of primitives and operators. Here's an example and the result:

Cardiods

Code:

/* helper function, takes in number of lines and radius */
func Cardiod(n, r) = identity:
    var ret = {}
    ret += Circle:
        center: ORIGIN
        radius: r
        tag: {}
        stroke: LIGHT_GRAY

    func theta_to_point(theta) = {r * cos(theta), r * sin(theta), 0}
    for i in 0 :< n
        let next = mod(i * 2, n)
        let org_theta = TAU * i / n
        let new_theta = TAU * next / n
        ret += Line:
            start: theta_to_point(org_theta)
            end: theta_to_point(new_theta)
            tag: {}
            stroke: BLACK

    element: ret

/* create 9 raw cardiods */
let raw_cards = map:
    v: 1 :< 10
    f(x): Cardiod:
        n: 2 * x + 1
        r: 0.25

/* use operators to position them */
tree cards = Centered:
    /* for simple operators it can be okay to use functions */
    mesh: XStack(raw_cards)
    at: ORIGIN

p += Set:
    vars&: cards

3.7 More Examples!

Here's the code for a Koch Curve and the result:

func move(start, mag, rotation) = identity:
    let delta = {mag * cos(rotation), mag * sin(rotation), 0}
    element: vec_add(start, delta)

func KochCurve(level, start, length, rotation) = identity:
    var ret = {}
    if level > 0
        var pos = start
        var rot = rotation
        ret += KochCurve:
            level: level - 1
            start: pos
            length: length / 3
            rotation: rot

        pos = move(pos, length / 3, rot)
        rot += PI / 3
        ret += KochCurve:
            level: level - 1
            start: pos
            length: length / 3
            rotation: rot

        pos = move(pos, length / 3, rot)
        rot += 4 * PI / 3
        ret += KochCurve:
            level: level - 1
            start: pos
            length: length / 3
            rotation: rot

        pos = move(pos, length / 3, rot)
        rot += PI / 3
        ret += KochCurve:
            level: level - 1
            start: pos
            length: length / 3
            rotation: rot
    else
        ret += Line:
            start: start
            end: move(start, length, rotation)
            tag: {}
            stroke: BLACK

    element: ret

let l = 4
tree k = KochCurve:
    level: 5
    start: {-l / 2, 0, 0}
    length: l
    rotation: 0

p += Set:
    vars&: k

Koch Curve example

Here's one for the difference between two functions

func f(x) = sin(x) ** 2
func g(x) = sin(x) * 4 / 5

tree scene = {}
scene += Axis2d:
    center: ORIGIN
    x_unit: 2
    x_rad: 4
    x_label: "x"
    y_unit: 1
    y_rad: 1.5
    y_label: "y"
    grid: off
    tag: {}
    color: BLACK

var graphs = {}
graphs += ExplicitFunc:
    start: -4
    stop: 4
    f(x): f(x)
    tag: {}
    stroke: RED

graphs += ExplicitFunc:
    start: -4
    stop: 4
    f(x): g(x)
    tag: {}
    stroke: BLUE

graphs += ExplicitFuncDiff:
    start: -4
    stop: 4
    f(x): f(x)
    g(x): g(x)
    tag: {}
    pos_fill: {1,0,0,0.2}
    neg_fill: {0,0,1,0.2}

/* helps morph meshes into the axis space */
scene += EmbedInSpace:
    mesh: graphs
    axis_center: ORIGIN
    x_unit: 2
    y_unit: 1
    z_unit: 1

p += Set:
    vars&: scene

Function difference example

Riemann rectangles

func LRAM(start, stop, n, q(x)) = identity:
    var ret = {}
    let delta = (stop - start) / n
    for i in 0 :< n
        let x_start = start + delta * i
        let x_end = x_start + delta
        let val = q(x_start)
        ret += Rect:
            center: {(x_start + x_end) / 2, val / 2, 0}
            width: delta
            height: val
            tag: {}
            stroke: BLACK
            fill: {1,1,0,0.2}

    element: ret

func q(x) = x * x

tree scene = {}
scene += Axis2d:
    center: {-1.5,-1.5,0}
    x_unit: 1
    x_min: 0
    x_max: 3
    x_label: "x"
    y_unit: 3
    y_min: 0
    y_max: 9
    y_label: "y"
    grid: off
    tag: {}
    color: BLACK

var graphs = {}
graphs += ExplicitFunc:
    start: 0
    stop: 3
    f(x): q(x)
    tag: {}
    stroke: RED
graphs += LRAM:
    start: 0
    stop: 3
    n: 6
    q(x): q(x)
scene += EmbedInSpace:
    mesh: graphs
    axis_center: scene[0].center
    x_unit: 1
    y_unit: 3
    z_unit: 1

p += Set:
    vars&: scene

LRAM example

The color grid from before

tree grid = ColorGrid:
    x_min: 0
    x_max: 1
    y_min: 0
    y_max: 1
    x_step: 0.2
    y_step: 0.2
    tag: {}
    color_at(pos): {pos[0], pos[1], 1, 1}
    stroke: BLACK

p += Set:
    vars&: grid

Color Grid

Some famous LaTeX equations

tree equations = Text:
    var str = "\begin{align*}"
    str += "\int_{-\infty}^{\infty}e^{-x^2}dx &= \sqrt{\pi} \\"
    str += "e^{\pi i} + 1 &= 0 \\"
    str += "x &= \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} \\"
    str += "\end{align*}"
    text: str
    scale: 0.75
    stroke: CLEAR
    fill: BLACK
equations = Centered:
    mesh: equations
    at: ORIGIN

p += Set:
    vars&: equations

Famous math equations

Torus (with some camera work)

/* checkerboard */
let uv_base = ColorGrid:
    x_min: 0
    x_max: 1
    y_min: 0
    y_max: 1
    x_step: 0.05
    y_step: 0.05
    tag: {}
    color_at(pos): identity:
        let r = round(pos[0] / 0.05)
        let c = round(pos[1] / 0.05)
        var col = BLACK
        if mod(r + c, 2)
            col = RED
        element: col

let r1 = 1
let r2 = 0.5
tree torus = PointMapped:
    mesh: uv_base
    point_map(point): identity:
        let u = point[0] * TAU
        let v = point[1] * TAU
        let x = (r1 + r2 * cos(v)) * cos(u)
        let y = (r1 + r2 * cos(v)) * sin(u)
        let z = r2 * sin(v)
        element: {x,y,z}

p += Set:
    vars&: torus

Torus

Vector Field

/* somewhat hard coded for sake of example */

let min_norm = 1
let max_norm = 5
let max_scaled_norm = 0.4

var gradient = {:}
gradient[min_norm] = PURPLE
gradient[1.5] = GREEN
gradient[max_norm] = BLUE
let gradient_c = gradient

func VectorField(f(x,y)) = Field:
    x_min: -4
    x_max: 4
    y_min: -2
    y_max: 2

    mesh_at(pos): Vector:
        var raw_delta = f(pos[0], pos[1]) + 0

        let mag = norm(raw_delta)
        let color = keyframe_lerp(gradient_c, mag)

        var scale = 1
        if mag > max_scaled_norm
            scale = max_scaled_norm / mag
            raw_delta = vec_mul(scale, raw_delta)

        tail: pos
        delta: raw_delta
        tag: {}
        stroke: CLEAR
        fill: color

tree field = VectorField:
    f(x, y):  {-y+x+1,x*y}

p += Set:
    vars&: field

Vector Field

Tree (Graph Theory)

func tint(color) = {color[0], color[1], color[2], 0.1}
func Node(position, color, label) = identity:
    var ret = {}
    ret += Circle:
        center: position
        radius: 0.2
        tag: {}
        stroke: BLACK
        fill: tint(color)

    ret += Number:
        value: label
        precision: 0
        scale: 0.5
        tag: {}
        stroke: CLEAR
        fill: BLACK

    ret[1] = Centered:
        mesh: ret[1]
        at: position

    element: ret

/* we can add edges as well using a technique in lesson 7 */
/* without that, they are hard to add unfortunately */
func Tree(adj, colors, curr, position) = identity:
    var ret = {}
    ret += Node:
        position: position
        color: colors[curr]
        label: curr

    var children = {}
    for child in adj[curr]
        children += Tree(adj, colors, child, position)

    /* position */
    children = XStack:
        mesh_vector: children
        align_dir: UP

    ret += children
    ret = Stack:
        mesh_vector: ret
        dir: DOWN
        align: center

    element: ret

/* adjacency list */
let adj = {{1,2}, {3, 4}, {5, 8}, {6}, {7}, {}, {}, {}, {}}
let colors = {RED, ORANGE, YELLOW, RED, GREEN, BLUE, RED, BLUE, ORANGE}
tree graph = Tree:
    adj: adj
    colors: colors

    /* 0 is root */
    curr: 0
    position: {0,1,0}

p += Set:
    vars&: graph

Tree

Neural Network

func NeuralNetwork(layer_counts) = identity:
    var layers = {}
    var edges = {}

    var last_layer = {}
    for i in 0 :< len(layer_counts)

        var new_layer = {}
        for j in 0 :< layer_counts[i]
            new_layer += Circle:
                center: {0, 0, 0}
                radius: 0.2
                tag: {}
                stroke: RED
                fill: WHITE
        new_layer = YStack(new_layer)
        new_layer = Centered:
            mesh: new_layer
            at: {i, 0, 0}

        layers += new_layer

        for prev in last_layer
            for curr in new_layer
                edges += Line:
                    start: prev
                    end: curr
                    tag: {}
                    stroke: BLACK

        last_layer = new_layer

    element: Centered:
        mesh: {edges, layers}
        at: ORIGIN

tree nn = NeuralNetwork:
    layer_counts: {4, 6, 6, 2, 1}

p += Set:
    vars&: nn

Neural Network

Look at more examples over here

3.8 Practice

Finally, you can try playing around with Monocurl! We still can't do animations yet, but as a challenge, try creating the following:

Practice example

Hint, you can make the previous using just the following three meshes and operators (and a bit of math):

First get the placements right, then aim for the colors.

3.9 Conclusion

REMARK This sections covered the basics of meshes. The point isn't that you know all meshes by now, but rather you have a good enough idea that it's easy to learn new ones. We definitely recommend going to the docs page and exploring existing options. In particular, look out for the ones with a star, as those are pretty useful.

Also, hopefully you can see the importance of creating your own functions from the very few examples we gave. As scenes get more complex, functions are of even more importance.

3.10 (Optional) Remarks

REMARK I dont really like positioning (and think it gets pretty redundant), and there are some stuff to be improved. But overall, I think meshes are good enough for the beta release.

Sections

3.1 Following Along3.2 Background Knowledge3.3 Meshes3.4 Mesh-Tree3.5 (Optional) Why meshes?3.6 Complex Example3.7 More Examples!3.8 Practice3.9 Conclusion3.10 (Optional) Remarks