Реалізація 3D-переглядача (Three.js/Babylon.js) на сайті
3D-переглядач у браузері потрібен для конфігураторів товарів (меблі, автомобілі, ювелірні прикраси), архітектурних порталів, освітніх платформ з 3D-моделями, ігрових вітрин. WebGL рендеринг через Three.js або Babylon.js — стандартний підхід, який працює без плагінів у всіх сучасних браузерах.
Three.js vs Babylon.js
Three.js — мінімалістична бібліотека рендеринга (~600 КБ), величезна спільнота, тисячі прикладів. Вимагає більше ручної роботи для складних сцен (фізика, виявлення колізій).
Babylon.js — повноцінний ігровий рушій у браузері (~2 МБ). Вбудована фізика, PBR-матеріали, Inspector, GUI, підтримка XR. Хороший для складних інтерактивних сцен.
Для перегляду 3D-моделей — Three.js. Для інтерактивних конфігураторів і сцен — Babylon.js.
Three.js: переглядач GLTF-моделі
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
// Сцена
const scene = new THREE.Scene()
scene.background = new THREE.Color(0xf8fafc)
// Камера
const camera = new THREE.PerspectiveCamera(50, width / height, 0.01, 1000)
camera.position.set(2, 1.5, 3)
// Рендерер
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)
// Освітлення
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 завантажувач з Draco стисненням
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/') // Копіюємо у public/draco/
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
let mixer: THREE.AnimationMixer | null = null
loader.load(
modelUrl,
(gltf) => {
const model = gltf.scene
// Центруємо модель
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)
// Анімації
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(`Завантаження: ${progress}%`)
},
(error) => console.error('Помилка завантаження моделі:', error)
)
// Цикл анімації
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()
// Зміна розміру
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"
/>
)
}
Конфігуратор кольору матеріалу
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)
}
})
}
// Використання в 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: альтернатива для складних сцен
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 }) => {
// Автоцентрування
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' }} />
}
Оптимізація продуктивності
- Draco-компресія GLTF зменшує розмір геометрії на 60–90%
-
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))— не рендеримо вище 2x -
LOD(Level of Detail) — більш детальна модель поблизу, спрощена здалеку - Instanced Mesh для множини однакових об'єктів (дерева, крісла)
- Вимикаємо
autoRotateпід час взаємодії користувача
Строк: переглядач з завантаженням GLTF і orbit controls — 2–3 дні. Конфігуратор з вибором кольору/матеріалу та кількома моделями — 5–7 днів.







