3-D cube with image-stamped faces and wireframe overlay

Projects a 3D cube onto a 2D canvas, applying a source image to each visible face with realistic Lambertian shading and a vector wireframe overlay.

INPUT
INPUT — 3-D cube with image-stamped faces and wireframe overlay
OUTPUT
OUTPUT — 3-D cube with image-stamped faces and wireframe overlay
JavaScript
// 3-D perspective cube — same image on every visible face
// demo_cube.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT

// Projects a textured, Lambertian-shaded cube onto a flat canvas
// using the engine's built-in glMatrix support, stamps the source
// image onto each visible face via stampAt's 4-point perspective,
// and overlays a wireframe via the vector canvas.
//
// No new engine method needed — all Mat4 / Vec3 / Pixel / Path
// primitives ship with the prelude.

const W = 700, H = 700;

// Camera setup
const EYE    = Matrix.Vec3.fromValues(3.2, 2.4, 3.2);
const CENTER = Matrix.Vec3.fromValues(0, 0, 0);
const view   = new Matrix.Mat4().lookAt(EYE, CENTER, Matrix.Vec3.fromValues(0, 1, 0));
const proj   = new Matrix.Mat4().perspectiveNO(Math.PI / 3.6, W / H, 0.1, 100);
const mvp    = Matrix.Mat4.clone(proj).multiply(view);

function project(v) {
    const p = Matrix.Vec3.fromValues(...v).transformMat4(mvp);
    return [(p[0] + 1) / 2 * W, (1 - (p[1] + 1) / 2) * H];
}

// Cube vertices + visible faces (indices into V, plus face normal).
const V = [[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1],
           [-1,-1,1], [1,-1,1], [1,1,1], [-1,1,1]];
const FACES = [
    { vi: [7, 6, 5, 4], n: [0, 0, 1] },  // front
    { vi: [2, 6, 5, 1], n: [1, 0, 0] },  // right
    { vi: [3, 2, 6, 7], n: [0, 1, 0] },  // top
];

const sun = Matrix.Vec3.fromValues(0.6, 0.8, 0.4).normalize();
const src = Engine.loadImage(INPUT);
const out = Engine.createImage(W, H).tint(new Pixel(0.05, 0.05, 0.08, 1), 1);

// Stamp image onto each visible face with Lambertian shading.
for (const face of FACES) {
    const pts   = face.vi.map(i => project(V[i]));
    const shade = Math.max(0.55, 0.55 + 0.45 *
        Matrix.Vec3.fromValues(...face.n).dot(sun));
    const tile  = src.clone().resize(300, 300).brightness(shade);
    out.stampAt(tile, pts.map(([x, y]) => px(x, y)));
    tile.free();
}

// Vector wireframe overlay
const canvas = Engine.createCanvas(W, H);
const edges  = [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]];
canvas.pen(new Pixel(0.7, 0.85, 1.0, 0.6), 1.2);
for (const [a, b] of edges) {
    const [ax, ay] = project(V[a]);
    const [bx, by] = project(V[b]);
    canvas.drawPath(Engine.createPath().moveTo(ax, ay).lineTo(bx, by), true);
}
out.blendAt(canvas.toImage(), px(0, 0), 1.0, Blend.Over).save(OUTPUT);

src.free();
out.free();

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0