g.a.d. ring — a round brilliant ray-traced against its own facet planes

A one-page site for a solitaire ring. There are no photos and no model files: the ring and the stone are built in code.

Live: g.a.d. — Made to be looked at.

The stone

  • Geometry is a ConvexGeometry over points computed from real round-brilliant proportions: table 56 , crown 34.5°, pavilion 40.75°, star 50 , lower girdle 77 %. The in-between girdle points sit on their facet planes, so the girdle scallops like a real one. Oval and princess cuts come from the same code.
  • After building the hull, coplanar triangles are merged into facet planes (up to 128), which go to the shader as a vec4 array.
  • The fragment shader does the optics: Fresnel reflection off the facet you look at, then refraction inside (IOR 2.42). Because the stone is convex, the exit point is simply the nearest facet plane ahead of the ray, so each bounce is one loop over the planes. Up to 7 bounces (4 on phones), total internal reflection when the angle says so, and a small per-channel IOR spread for the fire.
  • The environment is a CubeCamera render of a small studio scene: softboxes, an overhead strip and a lime kicker.

The ring

  • The band is a custom sweep: a flat-topped, comfort-fit section swept round the finger axis, with the engraving inside. Each claw is a TubeGeometry along a curve from the collar, over the girdle, onto the crown; the gallery rails are tori. Metals are MeshPhysicalMaterial presets for 18k yellow, 18k rose and platinum 950.
  • Scrolling (Lenis) takes it apart: the stone lifts out, the claws open, the band drops, each part labelled. A small configurator swaps metal and cut live.

Happy to answer questions about the stone shader.

Really nice trick, using the convexity so the exit point is just the nearest facet plane ahead of the ray: each bounce stays one loop over the planes.

Two questions: with the per-channel IOR spread, do you trace three separate paths through all the bounces, or only split at the final exit? And how do the facet edges hold up while the ring rotates? On my black hole shader the hard edges only got clean once I accumulated a few jittered frames while the camera is still, which is exactly why it has no free orbit.

Thanks! Neither, actually. Every bounce inside the stone is a mirror reflection, which doesn’t depend on IOR, so there’s one shared path. It splits at every facet where light can leave, not only the last one: three refract() calls (2.375 / 2.42 / 2.4875), one channel from each env lookup, and the reflected part carries on, up to 7 bounces:

vec3 rf = reflect(d, -nh), er = E(rf);
vec3 dr = refract(d, -nh, iv.r), dg = refract(d, -nh, iv.g), db = refract(d, -nh, iv.b);
// past its own critical angle a channel gets a zero vector and takes the reflection
vec3 o = vec3(dot(dr, dr) > 0. ? E(dr).r : er.r, E(dg).g, dot(db, db) > 0. ? E(db).b : er.b);
col += thr * (1. - Fo) * o;  thr *= Fo;  d = rf;

The cheat is at the entry: one refraction with the mean IOR, so colours only separate on the way out.

Edges: nothing clever, just antialias: true and pixel ratio capped at 2. MSAA handles the silhouette and the hull’s real facet edges, but edges inside the pattern, where neighbouring pixels pick a different exit plane, are shader discontinuities, so they crawl a little while it turns. On a diamond that mostly reads as scintillation, and the studio env never stops rotating, so there’s no still frame to accumulate into. If I fix it, I’d add extra rays only where the exit-plane index changes between neighbouring pixels. Reprojection would smear a refracted pattern that doesn’t follow the surface motion.

When the camera moves again, do you hard-reset the history or let it fade?

1 Like

Thanks for the detailed answer, a shared mirror path that splits at every exit facet, with a zero vector past the critical angle, is a neat way to keep it cheap. On your question: I hard reset, but without clearing anything. The blend weight is max(base, 1/(n+1)) driven by a counter, so setting the counter to 0 makes the next frame weigh 1 and overwrite the history in place. While the camera is actually moving I hold the weight at 0.55 instead of the resting 0.18 floor, which avoids ghost trails, and once it stops it ramps back over eight Halton jittered samples. Your idea of extra rays only where the exit plane changes sounds more targeted than my uniform accumulation.