Implementing a 3D Product Configurator on a Website
A 3D configurator is an interactive product model in the browser that updates in real-time when parameters change: a customer changes the sofa color and immediately sees the result, selects wood for the tabletop — the texture changes, adds legs of a different shape — the model is rebuilt. Technically, it is a combination of WebGL rendering, 3D scene management, and configurator business logic.
Technology stack
Three.js — base WebGL library for the browser. Works everywhere, maximum flexibility, requires deep understanding of 3D.
React Three Fiber (R3F) — React wrapper over Three.js. Declarative approach, integrates well with existing React applications.
Babylon.js — alternative to Three.js with richer built-in features (physics, PBR materials, GUI). Better documentation, but smaller community.
model-viewer (Google) — web component for viewing GLB/GLTF models with AR support. Suitable for simple cases: view a model, but don't configure it.
For most configurators, the choice is: React Three Fiber if the project is React-based, Babylon.js if a visual scene editor is needed.
3D model formats
| Format | Size | Browser | Description |
|---|---|---|---|
| GLTF/GLB | Compact | Yes | Web standard, PBR support |
| OBJ | Large | Yes | Outdated, no animation |
| FBX | Large | No | Requires conversion |
| USD/USDZ | Medium | Safari/AR | Apple AR Quick Look |
| Draco-compressed GLTF | Minimal | Yes | GLTF with Draco geometry compression |
For web — always GLTF/GLB with Draco compression. This reduces model size by 5–10x without visual loss.
Model preparation: designer exports from Blender/3ds Max/Maya to GLTF, then optimization via gltf-pipeline or gltfpack:
# Draco compression + optimization
npx gltf-pipeline -i chair.gltf -o chair.glb --draco.compressionLevel 7
# Or via gltfpack (more aggressive simplification)
gltfpack -i chair.gltf -o chair.glb -cc
React Three Fiber configurator architecture
import { Canvas, useLoader } from '@react-three/fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
import { OrbitControls, Environment } from '@react-three/drei'
import { Suspense } from 'react'
function ChairModel({ configuration }) {
const gltf = useLoader(GLTFLoader, '/models/chair-base.glb', (loader) => {
const draco = new DRACOLoader()
draco.setDecoderPath('/draco/')
loader.setDRACOLoader(draco)
})
// Apply materials based on configuration
useEffect(() => {
gltf.scene.traverse((node) => {
if (node.isMesh && node.name.startsWith('Seat')) {
node.material = getMaterialForChoice(configuration.material)
}
if (node.isMesh && node.name.startsWith('Frame')) {
node.material.color.setHex(configuration.frameColor)
}
})
}, [configuration])
return <primitive object={gltf.scene} />
}
export function ProductConfigurator({ productId }) {
const [config, setConfig] = useState({ material: 'leather', frameColor: 0x2c2c2c })
return (
<div className="grid grid-cols-2 gap-8">
<Canvas camera={{ position: [2, 1.5, 2], fov: 45 }}>
<Suspense fallback={<LoadingFallback />}>
<ChairModel configuration={config} />
<Environment preset="studio" />
<OrbitControls
enablePan={false}
minDistance={1}
maxDistance={4}
/>
</Suspense>
</Canvas>
<ConfiguratorPanel config={config} onChange={setConfig} />
</div>
)
}
Material and texture management
PBR materials (Physically Based Rendering) — standard for realistic rendering. Each material is described by a set of textures:
- baseColorTexture — color/pattern
- normalMap — surface relief
- roughnessMap — matte/gloss
- metalnessMap — metallicity
- aoMap — shadows in crevices (AO)
To change a material, don't replace the entire model — replace the textures:
async function applyMaterial(
mesh: THREE.Mesh,
materialConfig: MaterialConfig
): Promise<void> {
const loader = new THREE.TextureLoader()
const [baseColor, normal, roughness] = await Promise.all([
loader.loadAsync(`/textures/${materialConfig.id}/base.jpg`),
loader.loadAsync(`/textures/${materialConfig.id}/normal.jpg`),
loader.loadAsync(`/textures/${materialConfig.id}/roughness.jpg`),
])
;[baseColor, normal, roughness].forEach(t => {
t.wrapS = t.wrapT = THREE.RepeatWrapping
t.repeat.set(2, 2) // texture tiling
})
const material = mesh.material as THREE.MeshStandardMaterial
material.map = baseColor
material.normalMap = normal
material.roughnessMap = roughness
material.needsUpdate = true
}
Textures are preloaded at configurator initialization, not on every change — this eliminates delay when switching materials.
Performance
WebGL requires optimization, especially on mobile devices:
LOD (Level of Detail) — different model versions by detail level:
const lod = new THREE.LOD()
lod.addLevel(highDetailMesh, 0) // < 2 meters from camera
lod.addLevel(midDetailMesh, 2) // 2–5 meters
lod.addLevel(lowDetailMesh, 5) // > 5 meters
Instancing — for repeating elements (chair legs × 4):
const geometry = new THREE.CylinderGeometry(0.02, 0.02, 0.45)
const material = new THREE.MeshStandardMaterial()
const instancedMesh = new THREE.InstancedMesh(geometry, material, 4)
// Position 4 legs via transformation matrices
legPositions.forEach((pos, i) => {
const matrix = new THREE.Matrix4()
matrix.setPosition(pos.x, pos.y, pos.z)
instancedMesh.setMatrixAt(i, matrix)
})
Lazy loading models: base model loads immediately, optional components (accessories, attachments) — on demand.
Photorealistic lighting
For quality rendering, you need an HDR environment map (HDRI):
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'
const hdrLoader = new RGBELoader()
const hdrTexture = await hdrLoader.loadAsync('/env/studio.hdr')
hdrTexture.mapping = THREE.EquirectangularReflectionMapping
scene.environment = hdrTexture // lighting from HDR
scene.background = hdrTexture // or null for transparent background
HDRI maps are taken from Poly Haven (free, CC0) or custom maps are created for the brand.
Exporting a configuration screenshot
The customer should be able to save or share the configuration. Screenshot from WebGL canvas:
function captureConfiguration(): string {
const canvas = document.querySelector('canvas')
return canvas.toDataURL('image/png', 0.95)
}
async function saveToServer(dataUrl: string, config: object): Promise<string> {
const response = await fetch('/api/configurator/save', {
method: 'POST',
body: JSON.stringify({ image: dataUrl, configuration: config }),
})
const { shareUrl } = await response.json()
return shareUrl // unique link to configuration
}
A saved configuration can be restored from a link — useful for abandoned carts and social media sharing.
Mobile devices and AR
On mobile devices, an additional option — viewing in AR via model-viewer:
<model-viewer
src="/models/chair.glb"
ios-src="/models/chair.usdz"
ar
ar-modes="webxr scene-viewer quick-look"
camera-controls
alt="Chair in 3D"
>
<button slot="ar-button">View in room</button>
</model-viewer>
AR works on iOS via USDZ (Safari AR Quick Look) and on Android via Scene Viewer (ARCore).
Implementation timeline
- Basic 3D model view with OrbitControls: 2–3 days
- Real-time material/texture switching: 3–5 days
- Complete configurator with business logic + price calculation: 5–8 days
- Performance optimization (LOD, instancing, texture atlas): 2–3 days
- AR viewing on mobile: 2–3 days
- Configuration saving + share links: 1–2 days
3D model preparation (not programming) — separate task for 3D artist: 1–4 weeks depending on product complexity.
Total development: 3–5 weeks.







