3D web case study
ENRU
Case study - Luxury 3D web

One screen of gold.
A luxury 3D site on pure statics.


An image website for a Hong Kong financial company. The whole page is one fullscreen 3D scene: a slowly rotating gold nugget, studio light that follows the visitor's cursor, and two minimal black plaques with the brand and contacts. No backend, no database, no trackers. Real-time 3D instead of a looping video file, shipped as static files.

three.jsReact Three Fiberdrei TypeScript strictViteglTF-TransformCloudflare Pages
530 lines
of live code, the whole product
-61%
GLB size: 56.5 to 22.2 MB
136K
triangles in a single mesh
0
backend: no endpoints, no tables
1
developer: design, 3D and markup in one pair of hands
01 The client's ask

A digital business card at private banking level

The client works in capital raising and deal structuring. Their website did not need pages of text or a blog. It needed one thing: a first impression of quiet wealth. Restraint, gold, air. The kind of page a counterparty opens once before a meeting.

I.Not a corporate site. A statement.

One screen, no scrolling, no menu. A black plaque with the company name on top, contacts below, and a piece of gold in the middle of the screen. Everything else was removed on purpose.

II.Luxury means restraint

Five colors in the whole product. One typeface in light weights. Uppercase with wide letter spacing. The tagline sits on a frosted glass plate over the 3D scene. Nothing blinks, nothing slides in.

III.Wow without a video file

The obvious route is a looped showreel video. Instead the page renders a real-time 3D scene: it reacts to the visitor's cursor, weighs a fraction of a video, never stutters on loop and needs no autoplay permissions.

IV.Cheap to own

Pure static files on Cloudflare Pages. No CMS to update, no server to patch, no hosting bill. The site is a folder of files served from a global CDN.

02 3D engineering

The light rig: a lamp in the visitor's hand

The entire product lives in one Scene.tsx of 345 lines. The centerpiece trick: a point light glides after the cursor, so highlights and shading sweep across the rotating gold as if the visitor carried a lamp around a sculpture.

4 LIGHTFORMER PANELS Environment without an HDRI file environmentIntensity 0.75 BASE FILL directionalLight 0.38 ambientLight 0.45 MATERIAL PASS stock GLB sanitized at runtime: metalness 1.0, roughness 0.42 gold #d4af37 forced in code broken maps switched off sRGB / linear color spaces fixed GOLD NUGGET GLB 136,224 triangles, gold forced in code rotation 0.168 rad/s, gentle Y bob 0.045 STATIC CAMERA fov 40, position 0 / 2.9 / 7.6 POINTLIGHT, INTENSITY 40 follows the cursor with lerp 0.14 per frame: a lamp in the visitor's hand Two motions, one effect: the nugget rotates while the lamp follows the cursor. Highlights and shading sweep across the gold in real time. ACES FILMIC, EXPOSURE 0.85 GLB PRELOADED BEFORE FIRST FRAME DPR CLAMP 1 TO 2
Gold and light Camera and fill light Light paths and notes

I.A point light follows the cursor

A pointLight of intensity 40 chases the pointer with a lerp factor of 0.14 per frame. The easing makes the light feel carried, not glued. Highlights and shading sweep across the rotating gold as the light moves, which reads as depth instantly.

II.A stock material rebuilt at runtime

The stock asset rendered as dirty gray metal: broken ao, light and metalness maps, wrong color spaces. The code switches the bad maps off and forces the physics of gold, metalness 1.0 and base color #d4af37, every time the model loads. Swap the model and the fix still applies.

III.A studio without an HDRI

Instead of loading a multi-megabyte HDR environment file, the scene builds its own softbox: 4 Lightformer rectangles inside a drei Environment, at intensity 0.75. Gold gets its long white highlights and the network transfers zero extra bytes.

IV.Film-grade output

ACES Filmic tonemapping at exposure 0.85 keeps the highlights on metal from clipping. Device pixel ratio is clamped to 1-2 so phones stay smooth, and the model preloads before the first frame. Antialiasing is on, the canvas asks for the high-performance GPU.

Fullscreen scene with the cursor on the left, the nugget lit from the left
Cursor on the left: the lamp lights the nugget from the left, its right side falls into shade.
Fullscreen scene with the cursor on the right, the nugget lit from the right
Cursor on the right: the same frame, mirrored light. Highlights shift right, the left side falls into shade.

A stock asset never ships raw

The nugget comes from a 3D stock. Loaded as is, it looked like gray dirty rock: broken maps, wrong color spaces, arbitrary scale. The scene sanitizes the material at runtime, in code, so any future model swap gets the same treatment for free.

  1. Normalize the geometry. The GLTF is cloned, auto-centered and auto-scaled by its bounding box to a target of about 5 units. Any stock model lands in the frame correctly.
  2. Force the physics of gold. Metalness 1.0, roughness 0.42, base color #d4af37, envMapIntensity 0.92 and a touch of clearcoat 0.06. The material is defined by the case, not by the stock file.
  3. Disable the broken maps. The stock aoMap, lightMap, metalnessMap and emissiveMap produced gray dirty patches on the metal. Switched off explicitly.
  4. Fix the color spaces. Base color to sRGB, normal and roughness maps to linear, anisotropy 8 for sharp texture at grazing angles, normalScale 0.55 to soften missing-tangent artifacts.
  5. Recompute and pin. Normals and bounding sphere are rebuilt, frustum culling is disabled so the model never pops out of the frame when its bounds leave the mathematical view cone.
Close-up of the gold nugget after material sanitization
The sanitized material up close: forced gold physics over the stock geometry, ACES highlights.
345lines in Scene.tsx, the whole 3D core
40pointLight intensity, lerp 0.14
1-2dpr clamp, phones stay smooth
4Lightformers instead of an HDRI
0.168rad/s base rotation, drifting axes
03 Asset optimization

56.5 MB will not deploy: the GLB pipeline

Cloudflare Pages enforces a hard limit of 25 MB per file. The stock nugget arrived at 56.5 MB and simply could not be deployed. glTF-Transform compressed it to 22.2 MB, a 61% cut, without touching what the eye sees.

CLOUDFLARE PAGES LIMIT: 25 MB PER FILE SOURCE GLB FROM THE 3D STOCK 56.5 MB AFTER GLTF-TRANSFORM 22.2 MB -61% EXT_meshopt_compression + KHR_mesh_quantization, glTF-Transform 4.2.1 Geometry intact: 69,104 vertices, 136,224 triangles, 3 PBR textures (8.6 + 6.6 + 6.2 MB)
Source asset Deployed asset Platform limit

I.What was compressed

Meshopt compression re-encodes vertex data into a compact GPU-friendly stream, quantization stores positions and normals in fewer bits. Both are visually lossless at this scale. One mesh, one material, all three PBR textures preserved.

II.Why it matters beyond the limit

The GLB is the page's heaviest download by far. Cutting it by 61% is the difference between a luxury first impression and a loading spinner. The platform limit forced the discipline; the visitor gets the benefit on every load.

25 MBhard per-file limit on the host
22.2 MBfinal GLB, fits with margin
69,104vertices, untouched
3PBR textures: normal, color, metal-rough
04 Stack and deploy

Five dependencies, zero servers

The runtime footprint is deliberately tiny: react, react-dom, three, @react-three/fiber and @react-three/drei. Nothing else ships to the browser.

TypeScript runs in strict mode and the build starts with a full type check: tsc first, then Vite. The Vite config is the default one, so the build is trivially reproducible. Deploy is Cloudflare Pages: push, build, done. The site is served from a global CDN on the free tier.

TypeScript ~5.9 strictVite 7React 19 three.js 0.182React Three Fiber 9drei 10
5 runtime dependencies0 endpoints0 database tables0 trackers

What this buys the client

Nothing to maintain. No CMS to update, no server to patch, no admin panel to get hacked, no hosting invoice. The site is a folder of static files that will open the same way years from now. For a company whose product is trust, a page that can not break is a feature, not a compromise.

Mobile version of the site: fullscreen 3D scene with plaques
Mobile: the same scene with dpr clamped to the device, plaques and glass plate scale on clamp().
05 How it was built

Small project, same discipline

A small project built by the same rules as a large one: strict TypeScript, an explicit asset pipeline and a deployed scene to iterate on.

  1. Concept survives iteration. The working name in package.json is still luxury-light-cube. The idea evolved from an abstract light cube to a gold nugget, and the light rig survived the pivot intact.
  2. Type safety even on a one-pager. Strict TypeScript with a tsc gate before every build. A 530-line project earns the same guardrails as a large one.
  3. Platform limits treated as requirements. The 25 MB Cloudflare Pages limit became an explicit asset pipeline step, not a late surprise: a dedicated step compresses the GLB by 61%.
  4. Polish is a separate pass. The final steps are pure craft: a better nugget model, the full material sanitization, then light and background tuning. Visual quality was iterated on the real deployed scene.

Delivery profile

530 lines of project codestrict TypeScript1 developer

Code footprint

530 lines totalScene.tsx 345 linesCSS 143 lines

Honest numbers

Every figure in this case study is verified against the project repository: file sizes, line counts, commit history, scene parameters. Estimates are labeled as estimates.

06 Outcome and contact

Presence, delivered as engineering

The client got a distinctive image site that behaves like a piece of brand identity: quiet, heavy with gold, alive under the cursor. It loads from a CDN, costs nothing to run and has no moving parts to break.

Kirill Sedov

Business analyst and systems builder. Most of my portfolio is ERP and back-office automation: analysis first, then code. This case is the other side of the same craft. When a business needs presence instead of process, I apply the same discipline: clear requirements, platform limits as constraints, honest numbers, clean delivery.

Connect on LinkedIn