3D Viewer Implementation (Three.js/Babylon.js) on Website
A 3D viewer in the browser is needed for product configurators (furniture, cars, jewelry), architectural portals, educational platforms with 3D models, game storefronts. WebGL rendering via Three.js or Babylon.js is the standard approach that works without plugins in all modern browsers.
Three.js vs Babylon.js
Three.js — a minimalist rendering library (~600 KB), huge community, thousands of examples. Requires more manual work for complex scenes (physics, collision detection).
Babylon.js — a full-featured game engine in the browser (~2 MB). Built-in physics, PBR materials, Inspector, GUI, XR support. Good for complex interactive scenes.
For 3D model viewing — Three.js. For interactive configurators and scenes — Babylon.js.
Three.js: GLTF Model Viewer
npm install three @types/three
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
import { useEffect, useRef } from 'react'
interface ModelViewerProps {
modelUrl: string
envMapUrl?: string
}
export function ModelViewer({ modelUrl, envMapUrl }: ModelViewerProps) {
const mountRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = mountRef.current!
const width = container.clientWidth
const height = container.clientHeight
// Scene
const scene = new THREE.Scene()
scene.background = new THREE.Color(0xf8fafc)
// Camera
const camera = new THREE.PerspectiveCamera(50, width / height, 0.01, 1000)
camera.position.set(2, 1.5, 3)
// Renderer
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
})
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 1.2
renderer.outputColorSpace = THREE.SRGBColorSpace
container.appendChild(renderer.domElement)
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
const dirLight = new THREE.DirectionalLight(0xffffff, 2)
dirLight.position.set(5, 10, 5)
dirLight.castShadow = true
dirLight.shadow.mapSize.set(2048, 2048)
scene.add(dirLight)
const fillLight = new THREE.DirectionalLight(0x8bb8e8, 0.5)
fillLight.position.set(-5, 2, -5)
scene.add(fillLight)
// Orbit Controls
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.08
controls.minDistance = 0.5
controls.maxDistance = 20
controls.autoRotate = true
controls.autoRotateSpeed = 1.5
// GLTF loader with Draco compression
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/') // Copy to public/draco/
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
let mixer: THREE.AnimationMixer | null = null
loader.load(
modelUrl,
(gltf) => {
const model = gltf.scene
// Center model
const box = new THREE.Box3().setFromObject(model)
const center = box.getCenter(new THREE.Vector3())
const size = box.getSize(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z)
model.position.sub(center)
camera.position.multiplyScalar(maxDim * 0.8)
controls.update()
scene.add(model)
// Animations
if (gltf.animations.length > 0) {
mixer = new THREE.AnimationMixer(model)
gltf.animations.forEach((clip) => {
mixer!.clipAction(clip).play()
})
}
},
(xhr) => {
const progress = Math.round((xhr.loaded / xhr.total) * 100)
console.log(`Loading: ${progress}%`)
},
(error) => console.error('Error loading model:', error)
)
// Animation loop
const clock = new THREE.Clock()
let animFrameId: number
function animate() {
animFrameId = requestAnimationFrame(animate)
const delta = clock.getDelta()
mixer?.update(delta)
controls.update()
renderer.render(scene, camera)
}
animate()
// Resize
function handleResize() {
const w = container.clientWidth
const h = container.clientHeight
camera.aspect = w / h
camera.updateProjectionMatrix()
renderer.setSize(w, h)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
cancelAnimationFrame(animFrameId)
controls.dispose()
renderer.dispose()
container.removeChild(renderer.domElement)
}
}, [modelUrl])
return (
<div
ref={mountRef}
style={{ width: '100%', height: '500px' }}
className="rounded-xl overflow-hidden cursor-grab active:cursor-grabbing"
/>
)
}
Material Color Configurator
function changeModelColor(scene: THREE.Scene, meshName: string, color: string) {
scene.traverse((object) => {
if (object instanceof THREE.Mesh && object.name === meshName) {
const material = object.material as THREE.MeshStandardMaterial
material.color.set(color)
}
})
}
// Usage in UI
<div className="flex gap-2">
{['#ef4444', '#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6'].map((color) => (
<button
key={color}
onClick={() => changeModelColor(sceneRef.current!, 'Body', color)}
style={{ background: color }}
className="w-8 h-8 rounded-full border-2 border-white shadow"
/>
))}
</div>
Babylon.js: Alternative for Complex Scenes
npm install @babylonjs/core @babylonjs/loaders
import { Engine, Scene, ArcRotateCamera, HemisphericLight, Vector3 } from '@babylonjs/core'
import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader'
import '@babylonjs/loaders/glTF'
function BabylonViewer({ modelUrl }: { modelUrl: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const engine = new Engine(canvasRef.current!, true, {
preserveDrawingBuffer: true,
stencil: true,
})
const scene = new Scene(engine)
const camera = new ArcRotateCamera('camera', -Math.PI / 2, Math.PI / 4, 5, Vector3.Zero(), scene)
camera.attachControl(canvasRef.current!, true)
camera.lowerRadiusLimit = 1
camera.upperRadiusLimit = 20
new HemisphericLight('light', new Vector3(0, 1, 0), scene)
SceneLoader.ImportMeshAsync('', '', modelUrl, scene).then(({ meshes }) => {
// Auto-center
camera.setTarget(meshes[0])
})
engine.runRenderLoop(() => scene.render())
const handleResize = () => engine.resize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
engine.dispose()
}
}, [modelUrl])
return <canvas ref={canvasRef} style={{ width: '100%', height: '500px' }} />
}
Performance Optimization
- Draco compression reduces GLTF geometry size by 60–90%
-
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))— don't render above 2x -
LOD(Level of Detail) — more detailed model nearby, simplified in distance - Instanced Mesh for multiple identical objects (trees, chairs)
- Disable
autoRotateduring user interaction
Timeline: viewer with GLTF loading and orbit controls — 2–3 days. Configurator with color/material selection and multiple models — 5–7 days.







