Exhibit 01 · The deployed viewer
Rendered in real time in the browser. Nothing here is a photograph or a pre-rendered video. Drag to rotate; zoom and pan are disabled so the product cannot be framed badly.
01 · Situation
Santa Fe Botanicals sells a cuticle serum in a clear glass bottle with a dropper. Glass is the expensive case in product photography: it reads as a set of reflections rather than a surface, so it needs a controlled light box, and every change to the label or the fill level means booking the shoot again. For a brand running one SKU at a time, the recurring cost is the reshoot rather than the original photograph.
The storefront also needed the product to move. A still image cannot show a dropper bottle from the back, and a video loop is a fixed camera path that goes stale the moment the packaging changes.
I had already modelled the bottle in Blender. The question was whether one 3D asset could replace the photography pipeline outright and still run fast enough to sit on a live storefront.
02 · Decision
The build renders the bottle in the visitor’s own browser with three.js. That moves the cost from a recurring studio booking to a one-off asset plus the code around it, and it makes relighting or swapping a label a file change instead of a shoot.
It also creates a real constraint. A storefront cannot afford a heavy 3D scene on the critical path, so the viewer had to be built to an explicit budget: it loads nothing until the canvas is visible, and stops rendering when it is not.
03 · Constraints
| Constraint | What it forced | Measured |
|---|---|---|
| Model weight | The glTF binary can never sit on the storefront’s critical path | 8.4 MB |
| Environment map | Studio HDR deferred behind a procedural fallback so frame one still has reflections | 1.6 MB |
| Fetched before first view | An embed that is never scrolled to costs the host page the HTML shell and nothing else | 0 bytes |
| Pixel ratio ceiling | Capped at 1.25 instead of the device value, roughly 30% fewer fragments than 1.5 | 1.25× |
| GPU selection | No powerPreference hint, so dual-GPU laptops are not forced onto the discrete card | iGPU |
| Meshes drawn | Everything else in the export is stripped at load: studio scenery, duplicates, oversized geometry | 6 |
Source: deployed build, measured from the production URL.
04 · Execution
Three independent signals gate the render loop: the document’s own visibility, an IntersectionObserver on the canvas, and a postMessage channel the host storefront can use to say the iframe has scrolled out. All three must agree before a frame is drawn.
The loader walks every material and promotes smooth dielectrics to a physical material with transmission, index of refraction and thickness, so light bends through the bottle instead of reflecting off it. Anisotropic filtering and correct colour space go on every texture.
ACES filmic tone mapping improves the glass and washes out the label type. The label material opts out of tone mapping, drops its paper-grain normal map and takes no environment contribution, so the brand mark keeps the contrast it was designed with.
// all three must agree before a frame is drawn const isActive = () => docVisible && tabVisible && inViewport; new IntersectionObserver((entries) => { inViewport = entries[0].isIntersecting; if (isActive()) start(); else stop(); }, { threshold: 0.01 }).observe(canvas); function stop() { running = false; renderer.setAnimationLoop(null); }
Why setAnimationLoop(null) and not an early return. A guard inside the tick still costs a callback every frame, on the same main thread as the storefront it is embedded in. Unscheduling the loop means the browser stops calling the viewer at all, so an off-screen embed contributes nothing to the host page’s frame budget.
The same gate defers the 8.4 MB model and the HDR. A visitor who never scrolls to the viewer never pays for it.
if (/label/i.test(mesh.name)) { mat.normalMap = null; // drop paper grain mat.roughness = 0.85; // matte stock mat.envMapIntensity = 0.0; // no softbox glare mat.toneMapped = false; // bypass ACES curve mat.color.multiplyScalar(0.92); mat.needsUpdate = true; }
The label needed different treatment from the glass. ACES filmic tone mapping rolls off the highlights, which is exactly what makes the glass read as glass and exactly what washes the label’s type toward the background. Rather than compromise both, the label is excluded from the curve and lit separately.
The liquid ribbons get the opposite treatment: a physical material with iridescence and chromatic dispersion, drawn before the bottle so the transmission pass samples a frame that already contains them.
05 · Result
The viewer runs at studio-bottle-deploy.vercel.app and respects prefers-reduced-motion. It also exposes a pause API, so the storefront can tell the iframe it has scrolled out of view rather than leaving it to guess.
06 · Transfer
This is not real estate work. It is here because the discipline is the same one I bring to underwriting tooling, and it is easier to verify: the URL is public, the source is readable, and every number on this page can be measured from the deployed build.
The pattern repeats in the work that is CRE. At Rexford Industrial I reconciled 269 underwriting workbooks and 193 capital approval records into a single reporting database under the same rule that governs this viewer: state the budget first, then measure against it. At MRK Partners the T12 automation and Power BI dashboards behind LP reporting were built the same way.
In practice that means I can build the tool myself instead of waiting on an engineering queue.
Underwriting models, market analyses and the data tools built around them.