Build Guide · No. 001
One equation,
photographed in a room
that never existed
Obsidian Bloom is a single procedural flower of black glass and chrome that opens as you scroll. There are no 3D models, no textures, and no AI assets on the page — only mathematics, light, and about 560 lines of JavaScript. This is exactly how it works, so you can build your own.
I
The concept
The design bet was absolute focus: one object, one camera, one gesture. Scrolling does not move
the page so much as it turns a single number — uBloom, 0 to 1 — and everything
the visitor sees (petals unfolding, the void warming, the HUD percentage) is a function of that number.
Dark-luxury art direction: Italiana for display type, Manrope for body, one champagne-gold accent
(#c8a25e) against near-black.
II
Petals grown in the vertex shader
The CPU never computes a petal. Geometry is built once as a bare grid of (u,v)
coordinates — 25 petals × 3 whorls, ≈20k vertices — and a GLSL function positions every vertex, every frame,
on the GPU:
// dat.x = angle around stem · dat.y = whorl (0 outer → 1 inner) · dat.z = per-petal random vec3 petalPos(vec2 pr, vec3 dat) { float u = pr.x; // 0..1 along the petal float wn = dat.y; float L = mix(1.45, 0.85, wn); // outer petals are longer // staggered opening: outer whorl first, each petal its own delay float st = wn * 0.35 + dat.z * 0.10; float b = smoothstep(st, st + 0.55, uBloom); ... }
Normals cannot come from a static mesh whose vertices move in the shader, so they are computed
numerically inside onBeforeCompile — the surface is evaluated three times
(at p, p+∂u, p+∂v) and the
normal is the cross product of the two tangents:
vec3 pP0 = petalPos(aParam, aData); vec3 pPu = petalPos(aParam + vec2(pe_, 0.0), aData); vec3 pPv = petalPos(aParam + vec2(0.0, pe_), aData); vec3 n = normalize(cross(pPu - pP0, pPv - pP0));
The petal material is a MeshPhysicalMaterial tuned like lacquered obsidian —
near-black base, high clearcoat, low roughness — so all its character comes from what it reflects.
III
Jewellery lighting without a single light
Chrome is lit, not painted. Instead of loading an HDRI, the scene builds an invisible black photo studio —
emissive planes acting as softboxes — and folds it into an environment map with
PMREMGenerator:
softbox(5, 3, 0xeef2ff, 9, [ 0, 6, 1.5]); // broad cold key overhead softbox(0.5,7, 0xdfe6ff, 7, [-5.5, 1.5, 0.5]); // knife-edge strip → long chrome streaks softbox(0.5,7, 0xffffff, 5, [ 5.5, 2.0,-0.5]); softbox(6, 0.4,0xcfd8ff, 4, [ 0, 2.5,-6 ]); // rim behind softbox(4, 1.2,0xffc76e, 2.2, [ 0, -3.5, 4 ]); // warm champagne bounce const pmrem = new THREE.PMREMGenerator(renderer); scene.environment = pmrem.fromScene(studio, 0.035).texture;
Inside the flower sits a gold phyllotaxis heart — 64 instanced spheres placed on a Vogel spiral
(θ = i × 2.39996, the golden angle), fully metallic, so the throat of the flower
burns with reflected studio light at full bloom.
IV
Scroll as the only verb
GSAP ScrollTrigger scrubs scroll progress into a target value, which is then eased once more every frame
(a classic double-smoothing trick — scrub for position, lerp for weight) before landing in the shader uniform
and the HUD counter. The backdrop is not flat black either: a back-face sphere with its own tiny shader warms
up as uBloom rises, and UnrealBloomPass + a generated 128×128 film-grain tile give
the final image its photographic finish.
// the one number that runs the site ScrollTrigger.create({ start: 0, end: 'max', scrub: 0.6, onUpdate: (self) => { bloomTarget = self.progress; } }); // per-frame: uBloom → petals, backdrop, HUD, exposure
- Petals
- 25 · 3 whorls
- Geometry
- ≈20k vertices, positioned in GLSL
- Normals
- Numeric ∂/∂u × ∂/∂v
- Lighting
- Procedural studio → PMREM
- Post
- UnrealBloom · grain · vignette
- Stack
- Three.js r170 · GSAP 3.12
V
The atelier — dials into the live shader
Halfway down the page sits an interactive station: three sliders — bloom,
whorl tightness, petal length — wired directly to shader uniforms
(uBloom, uWhorl,
uLen). Tightness scales the open-tilt and receptacle radius;
length scales the petal's parametric extent, with width following at 35% so proportions
stay floral. Four presets (Bud Noir, Corolla, Medusa, Chalice) tween the sliders
themselves with GSAP, and every uniform is lerped per-frame — nothing snaps, so handling
the flower feels like handling jewellery. Beneath the dials a live hallmark engraves
the current recipe (OB·001 — B088 · W1.40 · L1.28), like an
assay mark that rewrites itself as you turn the stones.
// hand-off: scroll owns the bloom, until the bench is centred in view atelierMix += (visibility - atelierMix) * ease; target = storyBloom * (1.0 - atelierMix) + dialBloom * atelierMix; // entering the atelier syncs the dial to wherever scroll left the flower, // and the camera lerps to a jeweller's three-quarter view, flower off-axis
The blend runs on section visibility measured every frame, so scrolling in or out of the atelier never pops — control is handed over, both for the bloom value and for the camera.
VI
Materials & process, told on the page
Two documentary sections back the object up. Lacquer & light shows the
material recipe as animated gauges (metalness 0.85, roughness 0.22, clearcoat 1.0) beside
an SVG elevation of the invisible studio — five lamps that breathe in sequence and flare
on hover, each labelled with its real intensity from the code. The lamps are live
switches: click one and the corresponding softbox mesh is hidden, the studio scene is
re-folded through PMREMGenerator on the spot (each on/off
combination cached by bitmask), and the streak it owned visibly leaves the lacquer above.
From equation to light
is a four-step timeline (equation → normals → studio → bloom) whose gold rail draws itself
across with a scrubbed ScrollTrigger. Both live on glass panels
(backdrop-filter: blur) floating over the still-live flower,
and the sticky nav gains a blurred veil once the hero is left behind. The page closes with
a proper footer. Everything remains procedural — the diagrams are hand-set SVG, not images.
VII
Replicate it
1 — Ship a grid, not a mesh: encode (u,v) + per-petal data as attributes and do all
shaping in onBeforeCompile.
2 — Build light as geometry: emissive planes → PMREM beats any free HDRI for product-shot chrome.
3 — Drive everything from one scrubbed, re-eased scalar so motion feels like a decision, not a mechanism.
4 — Cap devicePixelRatio at 2, listen for webglcontextlost,
and honour prefers-reduced-motion by opening the flower instantly.
AI assets used: none. Every pixel is procedural.