August 14, 2026 · 14 min read
Building a room that behaves like an interface
A practical Blender-to-Three.js workflow for turning a composed 3D scene into a precise, expressive, and genuinely interactive website.- Blender
- Three.js
- Interaction design
A good interactive 3D website is not a model with links taped onto it. It is an interface whose controls happen to be objects in a space.
That distinction changes the whole build. If the room is treated as decoration, the usual approach is to export one large model, place invisible hotspots over it, enlarge whatever the pointer touches, and open a generic panel. It works in a demo. It also feels disconnected almost immediately: hit areas drift away from objects, a bicycle behaves like a button, the camera arrives at the wrong side of a record, and a monitor opens a terminal somewhere other than on its own screen.
The more convincing approach is to carry meaning through the entire pipeline. A book is a complete assembly in Blender, a named interaction target in the GLB, a particular kind of movement in Three.js, and the entrance to a specific piece of content in the interface. The same identity survives every layer.
This article describes that workflow from an empty Blender file to a deployed interactive room. It is not tied to one portfolio or one collection of objects. The methods apply to product showrooms, spatial archives, virtual studios, museum interfaces, and any website in which the scene itself is the navigation.
Begin with an interaction map
Before modeling, decide what the room means.
List the objects that can be selected, the content each one represents, what should happen on hover, what should happen on click, and whether the camera should move. This is more important than choosing materials. A room can be visually finished and still require major reconstruction if its object hierarchy does not match its intended interactions.
The interaction map should use object verbs rather than a universal effect. A record spins. A book leans out from a shelf. A bicycle wheel rolls. A desk lamp illuminates its own pool of light. A monitor wakes and displays an interface. These verbs become requirements for pivots, mesh membership, materials, lights, and runtime state.
It also helps to separate three kinds of behavior:
| Layer | Responsibility | Example | Best home |
|---|---|---|---|
| Authored object | Shape, hierarchy, scale, materials, semantic names | A book with its cover and pages under one root | Blender |
| Spatial response | Hover motion, physical reactions, camera framing | A wheel rotating around its axle | Three.js |
| Information interface | Articles, project details, controls, accessible navigation | A writing archive opened from paper and pen | React and HTML |
This division keeps the 3D file editable, makes interactions responsive, and preserves the accessibility and typography of ordinary web content.
Treat Blender as the source of truth
The canonical scene should be a real Blender file, not a GLB that is repeatedly edited after export. Keep downloaded source assets unchanged in a reference directory, record their origin and license, and build a separate working file from them.
When a base scene is imported, inventory it before changing anything. Record every object name, type, parent, transform, dimensions, material, polygon count, UV layer, image, and animation. An inventory exposes the structure that is otherwise easy to misread in the viewport. It tells you whether a chair is twelve meshes under a transformed empty, whether the pages and covers of a book are separate objects, and whether an apparently complete bicycle depends on unapplied mirror modifiers.
Use metric scene units and establish one unit as one metre. Then create a small number of stable collections: the working room, each externally sourced prop, runtime-relevant fixtures, and optional helpers. Put every imported prop beneath a single clearly named root empty. Moving, rotating, scaling, hiding, or deleting that root should affect the complete prop.
Do not flatten a hierarchy merely to make the Outliner shorter. Imported assets often contain non-uniform scale and nested rotation. Reparenting every mesh directly to a new root can introduce shear or alter the result during glTF serialization. Preserve the native hierarchy unless there is a specific reason to rebuild it, and preserve each object's world matrix whenever it is reparented.
Naming is part of the interface contract. Prefix imported objects, materials, and images by asset or component, while retaining the original source name in custom properties. Names such as Bike__FrontWheel or Workstation__MainMonitor are far more reliable than Cube.042. Add custom properties for interaction roles when naming alone is insufficient: a wall record might carry an external-link role and a platform identifier; a poster might carry an internal-navigation role and a destination.
Calibrate scale in the scene, not in isolation
Asset marketplaces rarely agree on units. A chair can arrive in centimetres, a bicycle in arbitrary units, and a drum kit at a plausible numerical scale that still looks miniature beside the desk.
Do not preserve an imported asset's original scale out of respect for the source file. The only meaningful scale is the one it has in the composed room.
Choose familiar anchors: desk height, chair seat height, monitor width, keyboard width, a bicycle wheel diameter, a twelve-inch record, and the reach of a seated person. Measure the world-space bounding box of each assembly and convert those dimensions to metres. A compact audit table is useful, but numerical plausibility is not enough. Render the whole scene from the audience's likely viewpoint and compare objects visually.
This matters because perspective changes the judgment. A correctly measured object close to the camera can dominate the room; a slightly undersized chair behind a desk can make every workstation object seem small. Adjust the composition as a system. If the workstation becomes wider, the desk may need to grow. If the chair grows, its clearance and the camera's home framing may need to change. Scale is relational, not a property to approve one asset at a time.
Placement should also be described from a human viewpoint. “Parallel to the right edge of the desk” is ambiguous in world coordinates. Define where the seated person is, which way they face, which edge is therefore their right, and which local axis of the prop must align to it. Apply rotations around the prop's own visual centre so a 180-degree turn swaps its facing without orbiting it across the room.
Prepare assets as editable assemblies
For a large downloaded scene, first extract only the components that will actually be used. A workstation package might contain two monitors, a tower, keyboard, mouse, desk pad, and mount alongside dozens of irrelevant meshes. Create a clean appendable Blender file containing the selected objects beneath one root, pack the images, retain provenance, and delete the unused scene furniture, cameras, and lights.
Complex props need another level of organization. A drum kit should retain logical groups for the snare, each tom, each cymbal, each stand, and the throne. A bicycle needs distinguishable front and rear wheel assemblies, frame, handlebar, drivetrain, and derailleur. These groups are not merely tidy—they determine what can animate independently.
Use visual centres deliberately. A complete prop that rocks from the floor should have a floor-centred interaction pivot. A wall record should rotate around the centre of its label. A cymbal should rotate around its own mounting point, not the origin of an imported bolt several centimetres away. When necessary, create runtime pivots later from world-space bounds, but fix obviously wrong authored origins in Blender first.
Modifiers deserve special attention. A model can look complete in Blender while half its geometry exists only as a Mirror modifier. If the web export does not apply modifiers, the handlebar or half the frame will disappear. Bake geometry that must survive the chosen export settings, then validate the exported model rather than assuming the viewport is representative.
Design materials for glTF, not only for Blender
Blender can render shader graphs that glTF cannot describe. Older assets frequently use layered Glossy and Diffuse nodes, procedural networks, or renderer-specific features. The result can be a colourful object in Blender and a white object on the website.
Create an explicit web-material pass. Convert unsupported materials to Principled BSDF graphs with portable base colour, metallic, roughness, texture, alpha, and emission inputs. Pack every required image. For important legacy assets, maintain a small audited material map rather than relying on a guess made by the exporter.
Materials should communicate physical difference. Rubber, painted metal, paper, glass, fabric, and an unlit screen should not share the same roughness. A carpet benefits from woven variation, low-frequency colour mottling, subtle fibre bump, a softened edge, and a distinct border. The web version can bake or generate a tileable weave texture that preserves this character in glTF.
Two common display defects are geometry problems rather than texture problems. Coplanar faces cause z-fighting: a monitor screen placed at exactly the same depth as its housing, or a decorative strip laid directly over a desk edge, can flicker when the camera moves. Give the intended front surface a small, measured clearance. Likewise, transparent covers require an intentional alpha mode and depth behaviour; a material that merely has a low alpha value is not necessarily configured for transparent rendering.
Lighting also needs both a visible fixture and a real contribution to the scene. An emissive tube that turns white is not the same as a light. Pair its emissive material with a point or spot light located at the fixture, then bind both to the same control. The user should be able to identify where every pool of light comes from.
Export a semantic GLB
The web asset is a delivery format, not the editable source. Export only visible, relevant meshes, curves, surfaces, fonts, and empties. Exclude cameras, Blender lights if the web recreates them, hidden helpers, and removable render tricks.
Preserve object names and export custom properties as glTF extras. Preserve hierarchy. Export normals and texture coordinates. Choose whether modifiers are applied as an explicit pipeline decision, then bake any geometry that depends on the opposite choice. A single binary GLB is convenient because geometry, hierarchy, materials, textures, and metadata travel together.
After export, validate the binary itself. Confirm that it is a real GLB rather than a source-control pointer, that required root names exist, that interactive meshes and custom properties survived, that materials have textures, and that the model loads through the same decoder used in production. The website should never discover a broken asset only after deployment.
Reconstruct the room runtime in Three.js
Load the GLB with GLTFLoader and the matching Draco decoder, add it to a Three.js scene, and derive the initial camera from the room's world-space bounds. Exclude the enormous floor or environment enclosure when calculating the furniture cluster; otherwise the useful room becomes microscopic in the frame.
A practical baseline is a perspective camera, orbit controls with damping, a restrained polar range, a hemisphere light, two directional lights, a neutral background, and light fog. The authored materials remain responsible for local surface character; the runtime rig provides a coherent environment.
Do not identify interactions by screen coordinates. Traverse the loaded scene, locate objects by semantic root names or custom properties, and register them as targets. Each target should contain its semantic ID, content section, family, interaction kind, motion root, member meshes, initial transform, animated subparts, hit meshes, and focus bounds.
The family field is important. Several meshes may constitute one perceived object, and several targets may share one hover response. A book's pages and both covers must move together. A record's label and backing disc must spin together. A whiteboard's artwork, frame, visible light tube, black housing, and actual light source should share one motion family.
Build precise hit testing
Use a Three.js Raycaster against actual interactive meshes. Map each mesh back to its target, and resolve only the nearest hit. This naturally respects camera perspective and object occlusion.
Invisible box hotspots are useful only when they are derived carefully. Thin or fragmented objects such as books can be frustrating to point at, so a stationary proxy built from the complete assembly bounds can stabilize hover. But an oversized proxy makes the pointer select a chair, bicycle, or drum kit while it is visibly far away. Keep proxies close to their visible geometry, use actual meshes for large irregular props, and do not let animated hit geometry chase the pointer.
Separate clicking from dragging. Record the target and pointer position on pointer-down, cancel the click if movement exceeds a small threshold, and resolve the target again on pointer-up. Disable hover while the camera is moving or orbit controls are being dragged. These details prevent accidental panels from opening during exploration.
Give every object its own motion language
A universal scale-up animation makes a scene feel like a collection of product cards. Motion should reveal the object's function or material instead.
| Object | Hover response | Click response | Pivot rule |
|---|---|---|---|
| Workstation | Mouse glides slightly; monitor, keyboard, mouse, and tower lights wake | Activate the interface on the physical monitor | Preserve the workstation root; target the display separately |
| Books | Complete volume lifts, enlarges slightly, and leans | Open the archive represented by the shelf | Bottom-centre for each full book assembly |
| Bicycle | Frame shifts subtly; only complete wheel assemblies rotate | Open its related story or view | Wheels rotate around their own axles |
| Records | One centred revolution around the logo | Open the associated destination | Centre of disc and label |
| Whiteboard | Board, artwork, light, and housing move as one fixture | Open the research view | Centre of the complete wall assembly |
| Drum kit | Independent cymbals answer with small phased wobble | Play a physical strike sequence | One mounting pivot per cymbal; sticks begin at the kit |
Smooth hover with a damped value rather than starting a new tween on every pointer event. Store hover position and velocity per target. Each frame, accelerate the value toward zero or one and apply exponential damping. The result can reverse direction smoothly when the pointer leaves midway through an animation.
Always restore transforms from recorded initial values before applying the current animation. Adding offsets to last frame's transform creates drift, overshoot, and the familiar bug in which an object slowly rotates into the floor.
Subpart membership must be explicit. A bicycle wheel animation should include the rim, tyre, hub, spokes, and brake rotor, but not the derailleur. Normalize exported mesh names, define the membership set, find an anchor mesh, construct a pivot at its world-space centre, and attach only those members while preserving their world transforms.
For cymbals, treat wobble as a small spring around each cymbal's own axis. Give every cymbal independent angle and velocity. A strike adds an impulse to one cymbal; damping returns it to rest. Rotating all cymbals under one drum root produces the unmistakable effect of the entire kit flexing as a single cheap object.
Drumsticks should be a tiny procedural rig, not two rods falling from above. Determine real strike points and surface normals from the snare, tom, and cymbal geometry. Find the drummer side from the throne. Define resting tips and grip guides there, aim each stick from tip to grip, and choreograph preparation, downstroke, rebound, and settle phases. Replays must reset to those grounded rest transforms before the first frame.
Make the camera understand faces
Clicking an object should not mean “zoom in a little.” It should mean “compose a clear view of this object.”
For every target, calculate a focus bounding box and define a view direction, padding, centre offset, and optional field of view. Fit the box in the camera frustum, then animate both camera position and orbit target with the same easing curve. A record needs a direction normal to its logo face. A monitor needs a direct view of its screen. A bicycle may need the unobstructed side rather than the mathematically nearest side.
Not every object needs focus. A chair can respond under the pointer without taking over the camera. A drum performance is often better observed from the user's current view. Camera movement is a narrative tool, not a mandatory confirmation animation.
The initial hero composition and the entered-room composition can also be two camera poses. Offset the room to leave space for introductory typography, then interpolate toward the centred home pose when the visitor enters. Derive both poses from the same bounds so the transition remains stable across viewport sizes.
Put live interfaces on real surfaces
A monitor should display its interface on the monitor mesh, including a curved one. Create a high-resolution canvas, draw the interface into it, turn it into a CanvasTexture, and assign it to the display material as both colour and emission. If the source screen lacks useful UVs, generate them from local vertex bounds along the screen's vertical and horizontal axes. The existing curved geometry then bends the texture naturally.
The canvas can be redrawn only when state changes: input, output, scrolling, focus, or cursor blink. Route keyboard, paste, and wheel events to a browser-safe command model while the screen is active. The terminal may look native, but it should operate on an intentional virtual information space rather than expose the deployment host.
The same method works for whiteboards, posters, dashboards, and labels. Draw information with the browser's typography and data, map the result onto the physical surface, and include that runtime mesh in the same interaction family as its frame.
Let HTML carry the long-form content
Three.js is excellent at spatial selection and poor at long-form reading. After an object is selected, let React render the article, project case study, archive, or contact interface in semantic HTML. Keep the relationship clear: paper and pen open writing; a camera opens experience; a monitor activates project navigation.
The panel should feel like the continuation of the object rather than a generic sticker placed over the room. Use object-specific composition, typography, and interaction, but retain ordinary web affordances: focus states, keyboard scrolling, Escape to close, a text-only index, and reduced-motion behaviour.
An optional guided tour can point to the same semantic targets used by pointer interaction. Because the tour references target IDs rather than fixed screen positions, it can focus the correct object and describe its content without duplicating the scene map.
Join the conventional site and the room
For a portfolio or editorial site, the 3D room does not have to be the first thing every visitor loads. A conventional text site can remain the primary document while a visual gateway reveals the spatial version on its own route.
The transition should have geometric continuity. If the static gateway is a triangular folded corner, the animated reveal must begin from that exact triangle and continue along the same diagonal. A curved wipe beginning at the top edge will feel wrong even if both individual frames look polished. The reverse transition should retrace the visual logic in the opposite direction.
Keep the room at a distinct URL so it can be linked, refreshed, and returned from normally. Mount the heavy scene before or during the reveal as appropriate, but do not let its canvas cover the readable page until the transition is genuinely underway. Honour reduced-motion preferences with a clean, immediate route change.
Validate contracts, not just screenshots
Visual inspection remains essential. Render the overall room, scale comparisons, material close-ups, monitor surfaces, carpet edges, and any interaction whose axis is hard to judge. Then inspect the exported GLB in the actual web renderer from the same camera directions a visitor will use.
But screenshots cannot protect the semantic contract. Add small automated checks for the assumptions that make the experience work: both wheel assemblies have the correct members; mirrored geometry is baked; each book includes pages and cover; records use centred pivots; hit areas are not oversized; visible fixtures have real lights; the monitor screen survives export; required custom properties exist; deployed assets are real binaries; and each content object opens a valid section.
This turns hard-won corrections into permanent constraints. A later asset edit can change the room without silently reintroducing a missing handlebar, rotating derailleur, flickering screen, detached book cover, or off-axis cymbal.
The durable pattern
The deepest lesson is that the Blender file and the website should not be separate projects connected by a one-time export. They are two editors for the same semantic scene.
Blender defines what each object is, how large it is, what belongs to it, where its meaningful axes are, and how it looks. The GLB carries those decisions across a stable boundary. Three.js interprets them as space, response, light, and camera. React interprets the same targets as readable information and accessible controls.
Once those layers agree, the room stops feeling like a novelty landing page. It becomes an interface: spatial, legible, and specific to the things inside it.