Glass cubes
The glass system in URM Lab: one rigid material in three qualities, a puzzle that plays inside, and a single continuous shot from cube to page. The demos on this page run the code at the bottom, unchanged.
Principles
Glass carries meaning
A cube's glass says what kind of model is inside, so the quality is never decoration. Crystal is a model trained at scale elsewhere. Glass is one trained here. Frost is a model still learning, and it clears as accuracy rises, so you can watch uncertainty leave the glass.
Light, not lines
No strokes or outlines. Edges are defined by refraction and by coloured light from the surround. Colour comes from the four-colour palette, used as light: inside the cube, behind it, in its dispersion.
Rigid and precise
Glass never bends, stretches or wobbles. Cubes respond to touch by turning, leaning and lifting, and the shot that opens a cube is exact: tiles land on the page's cells to the pixel.
One continuous shot
Opening a cube never cuts. The cube turns, parts, the camera dives, the tiles glide onto the board, and the page cross-fades in over them. Going back plays the same shot in reverse.
Never flash
The glass reflects a dim coloured surround and there are no direct lights, so turning a cube never sweeps a white highlight across the screen. Bloom only catches the brightest tiles.
Time, not frames
Every animation is a function of elapsed time. On a slow or throttled display things move less smoothly but arrive at the same moment. With reduced motion, cubes appear in place and nothing drifts.
Cheap to run
No lights to shade, a capped pixel ratio, shaders compiled before they are needed, and a Solve cube that only renders while something in it moves. The cube scene holds the display's full frame rate.
Qualities
Each quality is a set of physical material values. The table is generated from QUALITIES in the code below.
| Quality | Means | Roughness | IOR | Dispersion | Thickness | Iridescence | Clearcoat | Attenuation | Env |
|---|
Assignment
| Model | Quality | Why |
|---|---|---|
| Imported, trained at scale | Crystal | Dense, high index of refraction, splits light sharply. |
| Trained on this machine, finished | Glass | Lighter index, clear, with a stronger thin-film sheen. |
| Still training | Frosted | Roughness goes from 0.45 to 0.08 as the held-out solve rate goes from 0 to 1. |
Shared material values
| Base | MeshPhysicalMaterial, white, metalness 0, transmission 1, depthWrite off |
| Tint | The challenge's colour mixed 72% toward white, as attenuationColor: the body of the glass is faintly the cube's colour |
| Iridescence | IOR 1.3, film thickness 140 to 520 nm |
| Clearcoat roughness | 0.06 |
| Specular intensity | 0.8 |
| Geometry | Rounded box 1.5 units, corner radius 0.2, 8 segments; the faces it parts into are 0.07 thick with radius 0.03 |
Content
Each cube holds a real, unseen puzzle of its kind, as tiles on a plane through the cube's centre. The tiles always face the viewer through the cube's resting tilt (not through your spin), so the puzzle reads once a cube settles.
| Cell | Tile |
|---|---|
| Given digit | Its digit's colour, hue4(d / 9), at 0.42 brightness, 86% of a cell |
| Answer digit | The same colour at 1.2 brightness: a solved Sudoku reads as a mosaic where every row, column and box holds all nine colours |
| Blank, no answer | A dim dot, 32% of a cell |
| Maze corridor / ends / route | Dim white / bright white / colour along the diagonal |
| Life alive / dead | White (given) or coloured (answer) / a faint tile |
The solve loop
Every 8 seconds a cube shows its model's work: the answer tiles arrive in a diagonal wave from 1.1 s over 2.3 s, each popping in over 420 ms with an overshoot. When the last one lands the cube's glow pulses once, the puzzle holds solved, and the answers clear from 6.7 to 7.3 s. Each cube starts at a random point in the loop, so they never pulse together.
Interaction
| Input | Response |
|---|---|
| Hover | Leans toward the pointer (up to 0.45 rad), comes 0.5 units forward, scales 1.07 with a brief pulse, inner glow ×1.6; its label brightens |
| Press | Scales to 0.95, uniformly |
| Drag | Turns under the finger (0.012 rad per px) and follows the pointer on an elastic tether (16% of the distance, at most 0.45 units) |
| Throw | Keeps spinning at the release speed (capped at 14 rad/s), friction e^(−1.6t), then eases back to face you once under 1.2 rad/s |
| Release | The tether springs back, critically damped |
| Click (under 6 px of travel, under 450 ms) | Opens the cube |
| Keyboard | Each label is a button: Tab to it, Enter opens its cube |
The shot
Opening takes 1.5 s. The app first lays the page out invisibly where it will appear and measures every cell; the camera then flies to the exact framing that puts the cube's tiles over those cells, while each tile glides to its cell. At 72% the page starts to cross-fade in over the tiles; at 100% it is simply the page.
Closing takes 1.35 s and is the same shot reversed: the page fades out over 380 ms while the tiles, sitting exactly on its cells, glide back into the cube; the faces come together; the cube turns back to its resting tilt. The other cubes fly in from the edges, starting 380 ms in.
Framing math
For a board of local width W inside a cube of scale s, to cover a page rectangle of width w px in a viewport of height H px with vertical field of view θ: distance d = W·s·H / (w·2·tan(θ/2)), world per pixel k = 2·d·tan(θ/2) / H, and the camera sits at (cx − (rx − Vw/2)·k, cy + (ry − H/2)·k, cz + d) looking straight ahead, where (rx, ry) is the rectangle's centre. Tiles go to ((x − rx)·W/w, −(y − ry)·W/w) at 84% of the cell's size.
Rendering
| Surround | An equirectangular canvas: near-black with soft blue, red, yellow, green, violet and cyan light, prefiltered with PMREM. Nothing white and bright. |
| Backdrop | A large plane 9 units behind with the four colours, low alpha. It must be opaque: transmission only refracts opaque objects, and a transparent backdrop leaves the glass nothing to bend, so it vanishes. |
| Lights | None. The surround lights the glass; each cube has a soft additive halo of its colour inside, and its tiles are unlit and bright. |
| Tone | ACES filmic, exposure 0.82, sRGB output |
| Bloom | Strength 0.5, radius 0.45, threshold 0.9: only the brightest tiles glow |
| Resolution | Device pixel ratio capped at 1.25; tiles are one instanced mesh per cube |
| Shaders | Compiled in the background as soon as the cubes exist (a throwaway scene holds one mesh per material, including the hidden faces), so opening a cube never stalls |
| Solve cube | No tone mapping, so its background is exactly the page colour; renders only while points arrive, glide or the cube is turning |
| Camera | 30° field of view, resting at (0, 0.1, 12.5) |
API
| Export | Use |
|---|---|
new GlassStage(host, labels, hooks) | The cube scene. hooks.prepare(id) returns { rect, cells } (viewport px) for the page to land on, or null; hooks.reveal(id, ms) starts the page's cross-fade; hooks.done(id) ends the shot. |
stage.setItems(items) | [{ id, name, line, color, quality, progress, live, puzzle: { rows, cols, box, given, answer } }]; id is the puzzle kind |
stage.update(items) | Labels, liveness and quality, in place |
stage.setPuzzle(id, puzzle) | Put another puzzle in a cube |
stage.enter({ from, target }) | Fly in; with from and target the named cube closes around the page first |
stage.choose(id) / stage.leave() | Open a cube / stop rendering |
new ThoughtCube(el, slots, quality) | The thought-cloud in glass; set(points, count), begin(), reset(), setLive, setIdle, setHot, setQuality |
glassMaterial(quality, tint, opts) / setQuality(m, q, progress) | The material on its own |
QUALITIES, MOTION, SCENE, EASE, hue4 | The tokens |
Do and don't
Do
- Pick the quality from what the model is, never for looks.
- Keep a direct light out of the scene; let the surround light the glass.
- Keep the backdrop opaque so the glass has something to refract.
- Answer touch with turning, leaning and lifting; keep the glass rigid.
- Hand off to the page through
prepare/reveal/done, so the tiles land on the real cells. - Drive everything from elapsed time; respect reduced motion.
Don't
- Spin cubes on their own: a constant turn sweeps reflections and reads as flashing.
- Add outlines, rings or bars to show selection; use light, lean and lift.
- Stretch, squash or wobble the glass, or render the Solve cube when nothing in it is moving.
- Cut from the cube to the page, or fade to black between them.
- Raise bloom until the glass itself glows; only the tiles should.
- Put a puzzle in a cube that the model has seen in training.
Code
The complete module, web/glass.js. It needs three.js r170 (the import map at the top of this file). The demos above run exactly this text.