Реализация 3D-просмотрщика (Three.js/Babylon.js) на сайте
3D-просмотрщик в браузере нужен для конфигураторов продуктов (мебель, автомобили, ювелирные украшения), архитектурных порталов, образовательных платформ с 3D-моделями, игровых витрин. WebGL рендеринг через Three.js или Babylon.js — стандартный подход, работающий без плагинов во всех современных браузерах.
Three.js vs Babylon.js
Three.js — минималистичная библиотека рендеринга (~600 КБ), огромное сообщество, тысячи примеров. Требует больше ручной работы для сложных сцен (физика, collision detection).
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(`Loading: ${progress}%`)
},
(error) => console.error('Error loading model:', 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()
// 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"
/>
)
}
Конфигуратор цвета материала
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 дней.







