Skip to content
Meshes & Surfaces

Meshes & Surfaces

Draw a surface’s faces as one seam-free shape, beside the same faces filled one at a time.

Meshes & Surfaces

Usage

from std.math import cos, sin, sqrt

from canvas.buffer import Canvas
from canvas.color import Color
from canvas.geometry import FPoint
from canvas.io.bmp import write_bmp
from canvas.io.png import write_png
from canvas.path import Path
from canvas.shapes.mesh import fill_mesh, fill_mesh_shaded
from canvas.text.render import draw_text

comptime N = 28
comptime BG = Color(255, 255, 255)


def _height(u: Float64, v: Float64) -> Float64:
    """The surface: a ripple, in [-1, 1]."""
    var r = sqrt(u * u + v * v) * 3.0
    return cos(r) * 0.8 - v * 0.15


def _project(u: Float64, v: Float64, h: Float64, ox: Float64) -> FPoint:
    """A fixed oblique view: `u` runs right and a little down, `v` runs
    down-left, height runs straight up."""
    return FPoint(
        ox + 200.0 + u * 190.0 - v * 120.0,
        190.0 + u * 55.0 + v * 130.0 - h * 60.0,
    )


def _shade(h: Float64, nx: Float64, ny: Float64) -> Color:
    """Height maps to a blue-to-yellow ramp; a lambert term from the
    surface slope darkens faces turned away from the light."""
    var t = (h + 1.0) / 2.0
    var r = 30.0 + t * 220.0
    var g = 70.0 + t * 150.0
    var b = 180.0 - t * 150.0
    # Light from the upper left; the normal's slope gives the term.
    var lit = 0.55 + 0.45 * max(0.0, (-nx * 0.6 + ny * 0.3 + 0.75) / 1.0)
    return Color(
        UInt8(min(255.0, r * lit)),
        UInt8(min(255.0, g * lit)),
        UInt8(min(255.0, b * lit)),
    )


def _surface(
    ox: Float64,
    mut pts: List[FPoint],
    mut faces: List[Int],
    mut colors: List[Color],
):
    """The grid's vertices projected into `pts`, its faces sorted far
    to near into `faces`, and a shade per face into `colors`."""
    for j in range(N + 1):
        for i in range(N + 1):
            var u = Float64(i) / Float64(N) * 2.0 - 1.0
            var v = Float64(j) / Float64(N) * 2.0 - 1.0
            pts.append(_project(u, v, _height(u, v), ox))
    # Each cell as two triangles, keyed by its depth along the view --
    # larger `u + v` is nearer under this projection -- so a sort puts
    # far cells first.
    var keys = List[Float64]()
    var tris = List[Int]()
    var shades = List[Color]()
    for j in range(N):
        for i in range(N):
            var a = j * (N + 1) + i
            var u = (Float64(i) + 0.5) / Float64(N) * 2.0 - 1.0
            var v = (Float64(j) + 0.5) / Float64(N) * 2.0 - 1.0
            var h = _height(u, v)
            var e = 0.01
            var nx = (_height(u + e, v) - _height(u - e, v)) / (2.0 * e)
            var ny = (_height(u, v + e) - _height(u, v - e)) / (2.0 * e)
            var c = _shade(h, nx, ny)
            keys.append(u + v)
            tris.append(a)
            tris.append(a + 1)
            tris.append(a + N + 2)
            shades.append(c)
            keys.append(u + v)
            tris.append(a)
            tris.append(a + N + 2)
            tris.append(a + N + 1)
            shades.append(c)
    # Insertion sort on the keys, carrying the faces and shades along:
    # far to near is the painter's order the mesh relies on.
    var order = List[Int]()
    for k in range(len(keys)):
        order.append(k)
    for k in range(1, len(order)):
        var idx = order[k]
        var m = k - 1
        while m >= 0 and keys[order[m]] > keys[idx]:
            order[m + 1] = order[m]
            m -= 1
        order[m + 1] = idx
    for k in range(len(order)):
        var t = order[k]
        faces.append(tris[t * 3])
        faces.append(tris[t * 3 + 1])
        faces.append(tris[t * 3 + 2])
        colors.append(shades[t])


def _vertex_colors(mut colors: List[Color]):
    """A shade per vertex, from the height and slope there: what the
    smooth panel interpolates."""
    for j in range(N + 1):
        for i in range(N + 1):
            var u = Float64(i) / Float64(N) * 2.0 - 1.0
            var v = Float64(j) / Float64(N) * 2.0 - 1.0
            var e = 0.01
            var nx = (_height(u + e, v) - _height(u - e, v)) / (2.0 * e)
            var ny = (_height(u, v + e) - _height(u, v - e)) / (2.0 * e)
            colors.append(_shade(_height(u, v), nx, ny))


def main() raises:
    var c = Canvas(1440, 400, BG)

    # Left: one fill per face, which is what a loop over `fill_path_aa`
    # gives, and what leaves the mesh of light seams.
    var pts = List[FPoint]()
    var faces = List[Int]()
    var colors = List[Color]()
    _surface(0.0, pts, faces, colors)
    for f in range(0, len(faces), 3):
        var a = pts[faces[f]]
        var b = pts[faces[f + 1]]
        var d = pts[faces[f + 2]]
        var p = Path()
        p.move_to(a.x, a.y)
        p.line_to(b.x, b.y)
        p.line_to(d.x, d.y)
        p.close()
        c.fill_path_aa(p, colors[f // 3])

    # Right: the same faces as one mesh.
    var pts2 = List[FPoint]()
    var faces2 = List[Int]()
    var colors2 = List[Color]()
    _surface(480.0, pts2, faces2, colors2)
    fill_mesh(c, pts2, faces2, colors2)

    # Far right: a color per vertex, interpolated across each face.
    var pts3 = List[FPoint]()
    var faces3 = List[Int]()
    var colors3 = List[Color]()
    _surface(960.0, pts3, faces3, colors3)
    var vertex = List[Color]()
    _vertex_colors(vertex)
    fill_mesh_shaded(c, pts3, faces3, vertex)

    draw_text(c, 40.0, 370.0, "one fill per face", Color(60, 60, 60), 16.0)
    draw_text(c, 520.0, 370.0, "fill_mesh", Color(60, 60, 60), 16.0)
    draw_text(c, 1000.0, 370.0, "fill_mesh_shaded", Color(60, 60, 60), 16.0)

    write_bmp(c, "examples/out_mesh.bmp")
    write_png(c, "examples/out_mesh.png")
    print("wrote examples/out_mesh.bmp and .png")