kyletristentran@outlook.com
Commercial Real Estate Analyst · Los Angeles
USC B.S. Real Estate Development, Aug 2026
All projects
Bottle Study · Real-Time Product Viewer · Santa Fe Botanicals

Product Engineering · WebGL · 2026

Bottle Study

A real-time WebGL product viewer for Santa Fe Botanicals, the DTC skincare brand I founded. One glass bottle, rendered in the browser instead of photographed, shipped to a public URL and built to embed in a Shopify storefront without slowing the page that hosts it.

Role
Sole author: 3D pipeline, code, deploy
Stack
three.js r166, WebGL2, glTF, Vercel
Status
Live in production
Scope
One asset replacing a photo shoot

Exhibit 01 · The deployed viewer

studio-bottle-deploy.vercel.app

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

Why the bottle was expensive to photograph

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

Rendering it in the browser instead

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.

In underwriting terms, this converts a recurring operating cost into a one-off capital cost. The build still has to hold up under real load, which is what the rest of this page measures.

03 · Constraints

The budget the viewer was built against

ConstraintWhat it forcedMeasured
Model weightThe glTF binary can never sit on the storefront’s critical path8.4 MB
Environment mapStudio HDR deferred behind a procedural fallback so frame one still has reflections1.6 MB
Fetched before first viewAn embed that is never scrolled to costs the host page the HTML shell and nothing else0 bytes
Pixel ratio ceilingCapped at 1.25 instead of the device value, roughly 30% fewer fragments than 1.51.25×
GPU selectionNo powerPreference hint, so dual-GPU laptops are not forced onto the discrete cardiGPU
Meshes drawnEverything else in the export is stripped at load: studio scenery, duplicates, oversized geometry6

Source: deployed build, measured from the production URL.

04 · Execution

Where the engineering went

01

Pausing the loop when the canvas is off-screen

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.

02

Promoting the materials after import

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.

03

Keeping the label legible

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.

The visibility gate
// 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.

Keeping the brand mark legible
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

What shipped

31KBHTML shell, the entire cost to a storefront page that never scrolls to the embed
1.25×Device pixel ratio ceiling, chosen over 1.5 for roughly 30% fewer fragments
3Camera breakpoints, each with its own field of view and distance, so the bottle frames correctly on a phone
0Build dependencies. Plain ES modules and an import map, deployed as static files

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

Why a bottle is on an analyst’s portfolio

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.

More case studies

Underwriting models, market analyses and the data tools built around them.

© 2026 Kyle Tran