• About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us
AimactGrow
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
AimactGrow
No Result
View All Result

Exploring Procedural Geometry with Three.js and WebGPU

Admin by Admin
August 12, 2026
Home Coding
Share on FacebookShare on Twitter



Editor’s observe: With the primary Three.js Convention heading to Paris this September, we’ve been a number of the artistic work rising from the Three.js neighborhood. At this time, we’re excited to characteristic Chiro Visuals, whose placing experiments have been turning heads on X. On this tutorial, he takes us behind his Geometry Painter, exploring how Three.js, WebGPU, and TSL can remodel a easy stroke into every little thing from glowing crystals and molten cracks to aurora silk and a dwelling bioluminescent reef. There’s plenty of stunning experimentation right here, but in addition some intelligent concepts about constructing the system behind it. We hope you take pleasure in exploring it with us.

🎟️ Nonetheless no ticket? As a part of our partnership with the primary Three.js Convention, Codrops readers can use the code CODROPS to get 15% off. Get your ticket →

Drag a stroke throughout a floating sphere and watch crystal veins, molten cracks, aurora silk or a bioluminescent reef develop out of the floor, a have a look at instancing, TSL node supplies, dwell parameter programs, and the small architectural choice that lets a brand new portray mode plug in with out touching a single line of the portray code.

The primary model painted vegetation, was referred to as VegetationGeneratorThreeJS, and did precisely one factor: bushes alongside a stroke. After per week of dwelling with it, I realised the fascinating half wasn’t the bushes in any respect. It was the seam between “here’s a path throughout a floor” and “here’s what grows on it”. Every thing on the left of that seam is identical ceaselessly: raycasting, resampling, undo, orbit. Every thing on the appropriate is a special artwork challenge each time.

So I threw the vegetation out and rebuilt the factor round that seam. 4 modes ship at the moment: crystals, molten fissures, aurora silk, and a bioluminescent reef, and each was written with out opening the portray code as soon as.

Right here’s what I need to cowl:

  • Turning a drag into geometry: choosing with a BVH, why uncooked pointer occasions are ineffective as a path, and the coordinate-space bug that eats a day in the event you don’t see it coming.
  • The mode contract: the twenty strains that make a portray mode pluggable.
  • Making each slider dwell: how one can construct a system the place dragging a density slider by no means regenerates something, and why that constraint made the modes higher, not simply quicker.
  • 4 shaders: transmissive quartz, a blackbody crack ribbon, fold-locked aurora silk, and a bioluminescence wave that lives in world area.
  • The look: a studio constructed out of six glowing rectangles, and the submit chain on prime of it.

Every thing beneath has a demo web page you may open, poke, and watch by itself. They dwell below /demos within the repo. Run npm run dev and go to /demos/; they use the manufacturing code wherever doable, and I’ll hyperlink the related one as we go. Actually, I constructed them for this text after which stored utilizing them for debugging, which in all probability tells you one thing about how I ought to have been working all alongside. Discover all of the demos.

The idea

The reference picture in my head was a geode: a uninteresting gray rock that someone reduce open, and inside, this violent violet inside that has no enterprise being there. That distinction is the entire thought. The canvas sphere is intentionally boring: satin basalt, a whisper of clearcoat, as a result of the crystals need to be the one fascinating factor in body.

That call drove extra of the codebase than I anticipated. If the canvas is matte and darkish, then no matter you paint on it should provide its personal gentle: transmission, emissive, additive ribbons, level lights using alongside the stroke. Each mode ended up with some model of “this factor glows from the within”, not as a result of I deliberate it, however as a result of something that didn’t glow simply vanished into the sphere.

The implementation

One pointer occasion, one floor pattern

Portray on a mesh is a raycast, which is the simple half. What every mode truly wants is a bit more than a success level:

export interface SurfaceSample {
  /** World-space hit — used just for the dwell stroke preview beads. */
  place: THREE.Vector3;
  regular: THREE.Vector3;
  /** Anchor-space hit, captured at choose time. */
  native: THREE.Vector3;
  localNormal: THREE.Vector3;
}

From the traditional, we construct a tangent body, and that body is the place every little thing will get planted. A crystal leans off n by rotating towards t1/t2; a fissure expands sideways alongside t1 × n; an anemone tendril followers out round n. It’s three strains, and so they’re value staring without delay:

const t1 = new THREE.Vector3(1, 0, 0);
if (Math.abs(n.x) > 0.9) t1.set(0, 1, 0); // choose an axis that is not parallel to n
t1.cross(n).normalize();
const t2 = new THREE.Vector3().crossVectors(n, t1);

The Math.abs(n.x) > 0.9 guard is the one fascinating bit. Cross two parallel vectors and also you get a zero vector, and a zero vector normalised is NaN, and NaN in a matrix means an occasion silently disappears. One can find this bug on the poles of a sphere at 2am.

For choosing itself, I patch three-mesh-bvh in globally and construct a bounds tree on the canvas:

(THREE.Mesh.prototype as any).raycast = acceleratedRaycast;
// ...
(raycaster as any).firstHitOnly = true;

The firstHitOnly flag issues greater than the BVH does, in a manner. With out it the raycaster collects each intersection and types them, and portray on a closed sphere means you hit the again face too. With it, the traversal bails the second it has the closest triangle. The demo web page beneath picks towards a 33,600-triangle sphere and prints the price of a single choose, with a checkbox to drop the bounds tree and return to testing each triangle.

View the “Floor choosing & tangent body” demo

Pointer occasions are usually not a path

Right here’s a factor no one tells you. Pointer occasions arrive at a set charge, not a set distance. Transfer your hand slowly and also you get forty samples in a centimetre. Sweep it and also you get 4 throughout the entire sphere. Should you scatter one crystal cluster per pattern, which is the apparent first implementation and the one I shipped for about an hour, you find yourself with a dense clot wherever the person hesitated and nothing in any respect the place they moved.

So each mode resamples earlier than it builds something:

let travelled = 0;
let subsequent = 0;
for (let i = 0; i < samples.size; i++) {
  if (i > 0) travelled += samples[i].native.distanceTo(samples[i - 1].native);
  if (travelled < subsequent && i !== samples.size - 1) proceed;
  subsequent = travelled + PATH_STEP;
  // ... emit a path level with its tangent body
}

That’s it. Stroll by means of the uncooked samples, maintain monitor of the gathered arc size, and emit some extent each PATH_STEP world models. Every thing else will get thrown away. The crystals drop a cluster each 0.0625 models, whereas the fissure ribbon steps its centreline each 0.025. The travelled worth it offers again finally ends up doing double obligation later too, because it’s additionally the space coordinate that drives the entire development animation.

The demo beneath reveals a fairly typical stroke: 46 uncooked samples with gaps starting from 0.002 to 0.089 world models. That’s a forty-four-fold distinction between the tightest and loosest pair, all inside a single stroke. And this isn’t even an particularly erratic one.

View the “From pointer occasions to a centreline” demo

The canvas strikes when you’re portray

The sphere floats and bobs gently on a sluggish sine wave whereas it rotates, as a result of a very static topic simply seems like a screenshot. Portray labored tremendous within the first construct, proper up till I let go of the mouse, watched the crystals develop in, after which watched them slide proper off the floor like decals on a moist windscreen.

The samples have been in world area. After all they have been, that’s what the raycaster offers you. However world area is just actually significant on the precise second of the hit. By the point the expansion animation completed, the sphere had rotated 15°. So every pattern will get transformed into the anchor’s native area instantly, at choose time:

this.anchor.updateWorldMatrix(true, false);
this.invAnchor.copy(this.anchor.matrixWorld).invert();
return {
  place: h.level.clone(),
  regular,
  native: h.level.clone().applyMatrix4(this.invAnchor),
  localNormal: regular.clone().transformDirection(this.invAnchor),
};

The vital half is that the inverse will get recomputed on each choose as a substitute of being cached for the entire stroke. It needs to be, as a result of the anchor is transferring between pointer occasions. A stroke that takes two seconds to attract could be sampled towards 100 barely completely different matrices. Should you cache the inverse as soon as at pointerdown, you get a extra delicate model of the identical smear. It solely reveals up on sluggish, cautious strokes, which is precisely the place persons are going to note it.

View the “Pointing on a canvas that strikes” demo

The mode contract

Now the seam. That is the entire extensibility story and it matches on one display screen:

export interface StrokeInstance {
  group: THREE.Group;
  replace(dt: quantity, time: quantity): void;
  finishGrowth(): void;
  applySettings?(settings: unknown): void;
  dispose(): void;
}

export interface PaintMode {
  readonly id: string;
  createStroke(samples: SurfaceSample[], seed: quantity, settings: S): StrokeInstance;
}

A mode is only a manufacturing facility that turns samples right into a dwelling object. The app dad and mom group below the
floating anchor, calls replace each body, calls dispose on undo, and in any other case doesn’t have to
know what’s inside. Registering one is a line in a file:

non-public modes: File> = {
  'Crystals': crystalMode as PaintMode,
  'Molten fissures': fissureMode as PaintMode,
  'Aurora silk': auroraMode as PaintMode,
  'Bioluminescent reef': reefMode as PaintMode,
};

The elective applySettings? is the fascinating half, and I’ll come again to it in a second. The
brief model is {that a} mode that may re-derive its look from new settings will get dwell sliders for
free. A mode that may’t nonetheless has the choice of falling again to a rebuild with out forcing the app
to know something particular about that mode.

The seed dealing with is value mentioning too. Every stroke retains a secure index, and the efficient seed mixes that with the worldwide seed:

non-public effectiveSeed(index: quantity): quantity {
  return ((this.settings.seed * 2654435761) ^ (index * 40503 + 1)) >>> 0;
}

Knuth’s multiplicative fixed, an odd multiplier for the index, and XOR. Nothing notably intelligent. The result’s that each stroke seems completely different from its neighbours, whereas the entire scene nonetheless reshuffles coherently once you change the worldwide seed. You don’t get each stroke leaping to the identical new association.

Generate on the most, cull with the slider

Right here’s the constraint I set myself, and it ended up being some of the productive selections in
the challenge: dragging a slider mustn’t ever allocate something.

The naive density slider disposes the stroke and rebuilds it. That works tremendous with twenty
cases. At two thousand, with lil-gui firing onChange sixty occasions a second, it turns into a
slideshow. Worse, each rebuild rerolls the random numbers, so the geometry shimmers when you
drag.

You’ll be able to’t decide a glance that gained’t maintain nonetheless.

So the rule is easy: generate every little thing on the slider maxima as soon as, then let the sliders determine
what’s seen.

export const MAX_DENSITY = 16;
export const MAX_SHARDS = 16;

Each crystal is saved as its generative parameters, not as a baked matrix. We maintain its cluster
place, tangent body, and a handful of secure random values between 0 and 1:

interface CrystalInstance {
  anchor: THREE.Vector3;  // cluster's anchor-local floor level
  n: THREE.Vector3; t1: THREE.Vector3; t2: THREE.Vector3;
  clusterRnd: quantity;     // density culling rank
  shardIndex: quantity;     // shard-count culling rank
  offAz: quantity; offFrac: quantity; heightBase: quantity; jitterRnd: quantity;
  leanRnd: quantity; leanAz: quantity; spin: quantity;
  hueRnd: quantity; satRnd: quantity; clearRnd: quantity;
  // ...derived cache, rewritten by applySettings()
}

applySettings then recomposes each matrix and color in place. Culling is only a comparability
towards a rank the occasion has had because it was created:

const densityFrac = s.clusterDensity / MAX_DENSITY;
inst.seen =
inst.clusterRnd <= densityFrac &&
(inst.form !== 'shard' || inst.shardIndex < shardCap);

A culled occasion will get a zero-scale matrix. It stays within the buffer, stays within the draw name, makes use of the identical reminiscence it did a body in the past, and prices a vertex shader invocation that produces a degenerate triangle. On any GPU made this decade, that’s principally free.

The secure rank is vital. Increase the density slider and the identical crystals seem in the identical order. Present clusters don’t transfer. It reads as “extra of this” as a substitute of “a very completely different factor.” The identical thought applies to the clear-quartz combine:

inst.clearRnd < s.clearMix converts the identical crystals each time.

The clear/tinted cut up is principally the identical trick one stage up. Each crystal owns a slot in two InstancedMesh objects, one utilizing the palette materials and one utilizing the clear refractive quartz. Solely one in every of them ever will get an actual pose. The opposite will get a zero-scale matrix.

Which means “35% of those needs to be clear quartz” turns into a slider that switches supplies per occasion, which instancing usually doesn’t allow you to do.

5 form variants multiplied by two materials units offers ten InstancedMesh objects per stroke. A stroke about two-thirds of the best way throughout the sphere generates 537 crystals. On the default slider settings, 119 are seen and the opposite 418 are sitting within the buffers at zero scale, ready. Both manner, it’s ten draw calls.

View the “Generate at a most, cull with the slider” demo

Development is a distance, not a timer

Each mode grows in because the stroke fills. None of them use a tween library, a timeline, or a timer per occasion.

There are simply two numbers:

  • start: the space alongside the stroke the place an occasion was seeded, determined as soon as throughout
    technology.
  • grown: how far the expansion entrance has travelled, superior by dt * growthSpeed every body.

The animation is solely the distinction between them:

const t = (this.grown - inst.start) / GROW_WINDOW;
if (t <= 0) proceed; // not born but — matrix stays zero const okay = t >= 1 ? 1 : easeOutBack(t);
_s.set(inst.scale.x * okay * (0.6 + 0.4 * okay), inst.scale.y * okay, inst.scale.z * okay * (0.6 + 0.4 * okay));
_m.compose(inst.pos, inst.quat, _s);
mesh.setMatrixAt(i, _m);

GROW_WINDOW is 0.45 world models, so at any second the crystals inside a 0.45-unit band behind the entrance are mid-growth. Every thing else is both invisible or completed.

Development velocity is a dwell slider as a result of it solely modifications how rapidly grown strikes. Replaying the animation is grown = 0. Snapping to totally grown is grown = whole + window + 1. There’s no state to unwind.

Two particulars in that snippet do a lot of the visible work. First is easeOutBack, which overshoots by about 5% earlier than settling. That’s the distinction between “a mesh appeared” and “a crystal snapped into being.”

The second is the (0.6 + 0.4 * okay) on the width however not the peak. Crystals begin slim after which chill out outward. That’s roughly how minerals develop and, extra importantly right here, it stops the animation from trying like a uniform scale-up.

The pose loop additionally stops updating itself. As soon as each occasion is previous t = 1, it units a performed flag and replace() returns instantly. A completed stroke prices nothing per body, so you may cowl the sphere with out body time persevering with to climb.

View “The expansion entrance” demo

4 modes

Crystals: getting glass to learn on a darkish sphere

The quartz factors are hexagonal prisms with a tapered shaft and an off-axis pyramidal termination. They’re constructed non-indexed so computeVertexNormals() offers genuinely flat aspects.

The aspects are the entire learn. A clean crystal simply seems like a blob.

There are two small geometry particulars I in all probability would have skipped if I hadn’t regarded carefully at pictures. The side columns are jittered as soon as per column, so the prism edges keep straight from base to tip as a substitute of turning into noise. The bottom cap additionally closes to some extent barely beneath the bottom airplane:

const backside = new THREE.Vector3(0, -0.02, 0); // tiny below-base apex closes tilted crystals

With out that, a crystal leaning 30 levels away from the traditional can present a flat, floating hexagon the place it meets the floor.

The fabric took longer than anything within the challenge, and the 2 errors I made are value calling out.

Mistake one: full transmission. Setting transmission: 1 on a crystal sitting on a virtually black sphere offers you a black crystal. It’s apparent in hindsight. Transmission means you see what’s behind the crystal, and what’s behind it’s a darkish matte ball.

The repair is to maintain some diffuse:

mat = new THREE.MeshPhysicalMaterial({
  coloration: 0xffffff,
  roughness: 0.05,
  transmission: 0.7,     // NOT 1 — full transmission over a darkish sphere reads as black glass
  ior: 1.55,
  thickness: 0.4,
  attenuationColor: p.attenuation,
  attenuationDistance: 0.5,
  dispersion: 0.3,       // chromatic fringing contained in the glass — the "gem hearth"
  iridescence: 0.4,
  clearcoat: 0.5,
  envMapIntensity: 1.6,
});

At 0.7 you continue to get the glass depth, however about 30% of the floor shades side by side. That’s what offers an actual amethyst cluster that milky translucence.

Mistake two: tinting twice. My first go set coloration to the palette color and attenuationColor to a darker model of the identical color. The outcome was darkish, muddy, and weirdly opaque as a result of the tint successfully multiplied into itself: as soon as as albedo, then once more as absorption.

The bottom color is now white. The palette lives within the per-instance colors and within the attenuation, and nowhere else.

Alongside the tinted materials, there’s one shared clear-quartz materials: transmission: 1, roughness: 0.02, dispersion: 0.4, with a protracted attenuationDistance of 1.6 so gentle barely picks up a solid passing by means of it.

It’s principally a spotlight materials. A cluster that’s completely amethyst seems like plastic. Add a couple of clear crystals and abruptly it begins trying like a mineral.

Molten fissures: a ribbon that has no width

A crack is only a strip of geometry following the stroke. The apparent technique to construct it’s to compute the 2 edges on the CPU: for each centreline level, push one vertex left by width / 2 and one proper.

It really works, but it surely additionally means altering the width slider requires rebuilding the buffer.

As a substitute, each vertex stays on the centreline. Each vertices begin at precisely the identical place, and the strip will get pushed aside within the vertex stage:

mat.positionNode = positionLocal.add(
  aSide.mul(this.uWidth.mul(0.5).mul(aAcross).mul(aJit)).mul(taper.mul(sel)),
);

aAcross is ±1, aSide is the per-point throughout path, and aJit is a baked-in random stroll that provides the crack an natural, uneven width. uWidth is only a uniform.

So the crack width slider writes one float and doesn’t contact the buffers.

A typical crack with its branches has 352 centreline factors, which implies 704 vertices and 674 triangles. Each a kind of vertices has a twin sitting at precisely the identical coordinates till the vertex shader runs.

As soon as width is within the shader, the remainder follows naturally. Branches are generated as soon as as lightning-like walks that step throughout the floor, veer, and re-project onto the sphere. Every department carries three additional attributes: aRank, a random 0–1 worth; aWalk, the space from the department origin; and aMaxWalk.

const sel = step(aRank, this.uBranchFrac);              // department density
const taper = float(1)
  .sub(aWalk.div(aMaxWalk.mul(this.uLenFrac).add(1e-4)))
  .clamp(0, 1)
  .pow(0.7);                                            // department size

sel is 1 for branches that survive the density slider and 0 for the remainder. A culled department will get multiplied right down to zero width, so it collapses again into the centreline and disappears.

The primary crack has rank 0, so it at all times survives. taper pinches a department to some extent wherever the size slider presently ends.

Two sliders, two uniforms, zero rebuilds, and no CPU work per body.

View the “A ribbon with no width” demo

The color of the crack is managed by one float. There’s no texture and no gentle touching it. Warmth is the product of 4 phrases, adopted by a ramp:

const openness = smoothstep(0.0, 0.1, this.uGrown.sub(aDist));
const heart = smoothstep(0.12, 1.0, abs(aAcross)).oneMinus();
const pulse = aDist.mul(7).sub(time.mul(this.uPulse.mul(2.6))).sin().mul(0.28).add(0.72);
const flicker = time.mul(9).add(aDist.mul(41)).sin().mul(0.08).add(0.94);
const flash = smoothstep(0.0, 0.22, abs(this.uGrown.sub(aDist))).oneMinus().mul(1.6).mul(tip);

const warmth = heart.mul(pulse).mul(flicker).mul(this.uHeat)
  .mul(taper.mul(0.35).add(0.65))
  .mul(tip.mul(0.85).add(0.15))
  .add(flash);

const cSeam = vec3(0.02, 0.004, 0.002);
const cRed = vec3(1.1, 0.1, 0.01);
const cOrange = vec3(2.6, 0.85, 0.1);
const cWhite = vec3(4.6, 3.6, 2.4);
let coloration = combine(cSeam, cRed, smoothstep(0.0, 0.55, warmth));
coloration = combine(coloration, cOrange, smoothstep(0.55, 1.15, warmth));
coloration = combine(coloration, cWhite, smoothstep(1.15, 2.1, warmth));

Every time period has one job. heart controls the cross-section: vibrant on the seam and gone on the edges. pulse is the wave travelling alongside the crack, which makes it breathe. flicker provides high-frequency variation so the sunshine by no means seems fully nonetheless. flash is the white-hot band travelling with the propagation entrance, and it’s the half that makes the crack appear to be it’s tearing somewhat than merely fading in.

The ramp colors are intentionally manner above 1. cWhite is (4.6, 3.6, 2.4), which will get clipped to white by ACES tone mapping and, extra importantly, goes straight previous the bloom threshold.

The glow isn’t some post-processing trick utilized to the crack. The crack is genuinely that vibrant, and bloom is simply reporting it.

View the “Constructing the warmth ramp” demo

The crack additionally makes use of AdditiveBlending, and that’s greater than only a glow alternative. When two fissures cross, or a department meets its dad or mum, their gentle provides collectively into a warmer junction as a substitute of 1 crack merely portray over the opposite.

It’s the type of impact you get at no cost from the appropriate mix mode and would in any other case spend a day making an attempt to pretend.

Aurora silk: gentle the folds, not the sheet

The aurora curtain is a grid constructed alongside the stroke, with each vertex initially sitting on the hem. Top and billow are utilized within the vertex stage.

That’s the identical reasoning because the crack ribbon, so curtain top can keep a dwell uniform:

const foldPhase = aDist.mul(6.3).add(T.mul(1.1)).add(section);
const sway = foldPhase.sin()
  .add(aDist.mul(11.7).sub(T.mul(0.7)).add(aV.mul(1.8)).add(section).sin().mul(0.5));
const amp = this.uWave.mul(0.17).mul(aV.pow(1.35)).mul(unfurl).mul(breath);

mat.positionNode = positionLocal
  .add(aUp.mul(raise.add(ripple.mul(0.4))))
  .add(aSide.mul(amp.mul(sway).add(ripple)));

The amplitude scales with aV^1.35, which is the peak fraction. That retains the hem pinned to the floor whereas the crest billows. With out it, your complete sheet slides round like a flag that got here off its pole.

However the vertex wave by itself solely offers you a wobbling airplane, not cloth. The factor that sells it’s one line within the fragment stage:

const folds = abs(cos(foldPhase)).pow(1.6).mul(0.85).add(0.4);

The identical foldPhase that strikes the vertices now controls the brightness. The place the fabric turns away from you, the place you’d successfully be trying by means of extra of it, it glows.

As a result of each levels use the identical section, the brilliant bands journey with the folds as a substitute of sliding throughout them.

That’s actual behaviour: translucent cloth seen edge-on is brighter. An aurora is principally a curtain that you simply’re from the facet.

I maintain coming again to this one as a result of it has in all probability the very best ratio of visible payoff to characters typed in the entire challenge. One shared variable between two shader levels.

View the “Fold-locked brightness” demo

Two curtains use the identical geometry however completely different phases and heights. There’s a entrance sheet and a shorter, dimmer again sheet. Collectively they learn as separate bands of 1 aurora somewhat than a single flat airplane.

It’s concerning the most cost-effective depth cue you’ll ever purchase: one additional draw name and one modified fixed.

The reef: one heartbeat for the entire thing

The bioluminescent reef is essentially the most standard geometry within the challenge: recursively branched staghorn corals, anemone tendrils, and canvas-drawn gorgonian followers.

It’s additionally the mode with the least standard lighting logic.

The polyps don’t blink on their very own clocks. Their brightness comes from a wave that lives in world area:

operate colonyPulse() {
  return positionWorld.dot(vec3(1.6, 1.1, 1.35)).mul(2.6)
    .sub(time.mul(uPulse.mul(2.1)))
    .sin().mul(0.5).add(0.5).pow(2.5);
}

Undertaking the world place onto a path, subtract time, take the sine, then sharpen it with a pow. The result’s a airplane wave sweeping by means of the scene, and each polyp, tendril tip, and fan vein in each stroke samples that very same wave.

The fascinating half is what occurs subsequent. Paint one colony on the left facet of the sphere and one other on the appropriate 5 minutes later, and so they nonetheless pulse in the appropriate order. The wave reaches one after which the opposite, with the delay you’d anticipate from the space between them.

No person explicitly wired that up. It falls out of sampling a shared subject as a substitute of giving each object its personal section.

Every polyp additionally will get a small per-instance blink from hash(instanceIndex), so the reef seems like one organism comprised of individually twitchy elements.

The pow(2.5) is doing a little quiet work right here too. It turns the lazy spherical hump of a sine wave right into a sharper crest with longer darkish troughs. Bioluminescence feels extra like a flash with a sluggish restoration than a dimmer being easily turned up and down.

View the “One heartbeat, many colonies” demo

The look

The surroundings is the lighting

Crystals are principally reflection. Virtually every little thing you see on them isn’t actually shading. It’s an image of the room. So the room is the factor value constructing, and it’s simply six emissive quads:

panel(0xfff6ea, 9, 4.5, 3, [1.5, 8, 2]);      // overhead softbox, biased towards digital camera
panel(0xffffff, 22, 0.7, 4.5, [-2.5, 5, -6]); // exhausting top-back strip — side glints
panel(0x9db8ff, 5, 1.2, 7, [-7, 2, -2]);      // cool strip, camera-left
panel(0xffd9b0, 3.5, 1.6, 5, [6, 1.5, 3]);    // heat strip, camera-right
panel(0x8a5cff, 4, 6, 3.5, [0, 2.5, -8]);     // violet wash behind the topic
panel(0x2e3c58, 1.2, 9, 9, [0, -5, 0]);       // dim flooring bounce

const pmrem = new THREE.PMREMGenerator(this.renderer);
this.scene.surroundings = pmrem.fromScene(env, 0.04).texture;

MeshBasicMaterial with a color pushed previous 1, prefiltered by PMREMGenerator. That’s principally the entire studio.

The intensities aren’t arbitrary. That 22 on the slim top-back strip is what produces the exhausting specular glints alongside the crystal side edges. It must be that vibrant as a result of the strip is just 0.7 models large and principally misses the crystals.

Should you’ve ever lit a product shot, this could all really feel acquainted: massive smooth key, exhausting rim, cool/heat separation, and a wash behind the topic to raise it away from the background.

The three precise lights within the scene do a special job. The important thing spot casts the smooth shadow below the floating sphere. The rear pair, a blue-ish directional and a violet kicker, exist as a result of transmission responds to gentle arriving from behind the floor.

They’re what make the crystals gentle up from inside.

That’s additionally why “backlight” is a slider within the UI whereas “key gentle” isn’t.

View the “The surroundings is the lightning” demo

Publish-processing

Publish-processing is the ultimate go over the rendered scene: that is the place the picture will get its bloom, vignette, anti-aliasing, and tone mapping.

4 issues, in a single node graph:

const scenePass = go(this.scene, this.digital camera, { samples: 4 });
const coloration = scenePass.getTextureNode();
this.bloomNode = bloom(coloration, this.settings.bloomStrength, 0.6, this.settings.bloomThreshold);
const vignette = float(1).sub(smoothstep(0.5, 0.92, screenUV.distance(vec2(0.5, 0.5))).mul(0.35));
this.submit.outputNode = coloration.add(this.bloomNode).mul(vignette);

MSAA at 4 samples on the scene go, bloom choosing up something above the edge, a 35% vignette into the corners, and ACES filmic tone mapping on output.

The vignette might be the most affordable “this was shot with a quick lens” sign there may be. I’d put it on every little thing if I may.

Refinement

The one place a rebuild is unavoidable

Reseeding genuinely regenerates the geometry. New random numbers imply new geometry, so there’s no manner round that.

A couple of settings on some modes can also’t be re-derived in place, which is what the elective
applySettings? is for. That leaves us with one rebuild path, and it’s throttled adaptively:

const interval = this.regrowPending.mode === 'animate'
  ? 0
  : THREE.MathUtils.clamp(this.regrowCost * 3, 60, 400);
if (now - this.lastRegrowAt >= interval) {
  const t0 = efficiency.now();
  this.regrow(req.mode === 'animate');
  this.regrowCost = efficiency.now() - t0;   // measure, then again off proportionally
  this.lastRegrowAt = efficiency.now();
}

Requests get coalesced right into a single pending flag and serviced in the course of the tick. The interval is then primarily based on how lengthy the final rebuild truly took.

An empty scene rebuilds each 60ms and feels prompt. A sphere coated in reef colonies backs off towards 400ms and stays draggable.

It adapts to the machine in addition to to the scene, which is loads higher than choosing some fixed and hoping it really works in every single place.

Two WebGPU issues that may catch you

Factors are one pixel. PointsMaterial with measurement: 0.02 and sizeAttenuation: true renders as a single pixel per level below the WebGPU backend. Level primitives merely don’t have a measurement there.

The embers, plankton, and star motes on this challenge are all instanced quads with a smooth radial sprite. They don’t even billboard. Each will get a set random orientation when it spawns, which seems fully tremendous for a spark and saves updating a rotation each body for lots of of particles.

Line width is ignored. Identical story. The violet path following the cursor began as a Line with linewidth: 3. It stayed that manner for about ten minutes earlier than I seen it was principally a hairline.

It’s now an InstancedMesh comprised of overlapping spheres. One bead per pattern, with a radius chosen so consecutive beads overlap sufficient to appear to be one steady stroke:

this.beads = new THREE.InstancedMesh(
  new THREE.SphereGeometry(STROKE_RADIUS, 12, 8), glow({ opacity: 1 }), MAX_BEADS,
);
for (let i = 0; i < MAX_BEADS; i++) this.beads.setMatrixAt(i, this.zeroMat);
this.beads.rely = 0;

The pre-zeroing of the entire buffer is value stating. Occasion matrices are in any other case uninitialised rubbish. Increase rely midway by means of a stroke and you may abruptly draw a unit-scale sphere on the origin from no matter occurred to be left in reminiscence.

Zero the buffer as soon as throughout building, and that complete class of bug disappears.

Wrapping up

The factor I’d take from this challenge into the subsequent one isn’t a shader. It’s the constraint.

“No allocation whereas a slider is transferring” seemed like a efficiency rule once I first wrote it down. It turned out to be a design rule.

It pressured each mode right into a form the place its look is only a operate of secure per-instance random values and the present settings. When you’re working that manner, dwell enhancing, deterministic replay, seed-based variation, and undo cease feeling like separate options you need to implement.

They simply turn out to be issues which are true.

A couple of issues are value making an attempt in the event you clone it:

  • Write a mode. Implement createStroke and add one line to the registry. Mushrooms, circuitry, frost, feathers, fungal networks. The samples don’t care.
  • Swap the canvas. Nothing within the portray code is aware of it’s a sphere. Load a mesh, run indexForRaycasts on it, and most issues ought to maintain working. The one exception is the fissure department walker, which presently re-projects onto a sphere with radius |origin| and would wish a correct floor stroll for arbitrary geometry.
  • Push the heart beat additional. The reef’s world-space wave is a helpful template. A shared subject that each mode samples, whether or not that’s wind, tide, or a transferring gentle supply, may tie 4 unrelated results into one world with about thirty strains.

Credit

  • Constructed with three.js (WebGPU renderer + TSL), three-mesh-bvh for accelerated choosing, and lil-gui.
  • Development easing is a variant of the usual easeOutBack from easings.web.
  • The random generator is Tommy Ettinger’s mulberry32.
  • The lighting strategy is borrowed wholesale from product images, which is a a lot older subject than ours and has already solved most of it.

Work with me

Should you’re constructing one thing that should look pretty much as good as it really works, I’d love to listen to about it.

I run Chiro Studio, an award-winning 3D visualization and animation studio centered on cinematic CGI, architectural visualization, product movies, and real-time experiences. I work with manufacturers, structure companies, and tech corporations to show concepts, areas, and merchandise into visuals that really feel tangible earlier than they exist.

When you’ve got a challenge in thoughts, a product that wants a movie, an surroundings that wants bringing to life, or simply an thought you need to push someplace visually fascinating, get in contact.

Tags: ExploringGeometryProceduralThree.jsWebGPU
Admin

Admin

Next Post
Pastime mindset | Seth’s Weblog

Consideration up for public sale | Seth's Weblog

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recommended.

Examine Level Provides AI Utility Protection With Lakera Buy

Examine Level Provides AI Utility Protection With Lakera Buy

September 16, 2025
What 916 Opinions Reveal About AI’s Function

What 916 Opinions Reveal About AI’s Function

May 31, 2026

Trending.

Backrooms director Kane Parsons explains the birds, the portals, and his sensible results

Backrooms director Kane Parsons explains the birds, the portals, and his sensible results

May 31, 2026
Telegram ban in India sparks a rush to VPNs, rival apps

Telegram ban in India sparks a rush to VPNs, rival apps

June 19, 2026
The Full Information to EcoGPT

The Full Information to EcoGPT

June 6, 2026
Authorized DUI PPC Companies in Atlanta

Authorized DUI PPC Companies in Atlanta

June 14, 2026
Customers, Progress, and International Tendencies

Customers, Progress, and International Tendencies

March 18, 2026

AimactGrow

Welcome to AimactGrow, your ultimate source for all things technology! Our mission is to provide insightful, up-to-date content on the latest advancements in technology, coding, gaming, digital marketing, SEO, cybersecurity, and artificial intelligence (AI).

Categories

  • AI
  • Coding
  • Cybersecurity
  • Digital marketing
  • Gaming
  • SEO
  • Technology

Recent News

Crimson Desert Reveals Change 2 Launch Window, However Efficiency Considerations Stays

Crimson Desert Reveals Change 2 Launch Window, However Efficiency Considerations Stays

August 12, 2026
Pastime mindset | Seth’s Weblog

Consideration up for public sale | Seth’s Weblog

August 12, 2026
  • About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved

No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved