Основа: рендерер, сцена, камера
Кожному застосунку на Three.js потрібні рівно три об'єкти
верхнього рівня: WebGLRenderer, який малює пікселі,
Scene, що містить граф сцени, і Camera,
що визначає точку огляду. Налаштуйте їх один раз і
повторно використовуйте протягом усього життя застосунку.
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js';
// Рендерер
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// Сцена
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0a);
scene.fog = new THREE.Fog(0x0a0a0a, 10, 60);
// Перспективна камера: fov, aspect, near, far
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);
// Ортографічна камера (для 2D-стилю / ізометричних видів)
const frustumSize = 10;
const aspect = window.innerWidth / window.innerHeight;
const orthoCam = new THREE.OrthographicCamera(
(-frustumSize * aspect) / 2, (frustumSize * aspect) / 2,
frustumSize / 2, -frustumSize / 2,
0.1, 1000
);
| Властивість рендерера |
Призначення |
setPixelRatio() |
Відповідність DPR пристрою; обмежте до 2, щоб не перевантажувати GPU на retina-екранах |
outputColorSpace |
THREE.SRGBColorSpace для коректної гами на фінальному виводі |
toneMapping |
ACESFilmic або Reinhard для HDR-освітлених PBR-сцен |
shadowMap.type |
PCFSoftShadowMap для м'яких країв; BasicShadowMap — найдешевший |
setClearColor(color, alpha) |
Колір фону, коли scene.background не задано |
Геометрії
Геометрії — це екземпляри BufferGeometry, що
описують позиції вершин, нормалі та UV-координати. Вбудовані
конструктори покривають найпоширеніші примітиви.
| Конструктор |
Основні аргументи |
BoxGeometry |
width, height, depth, widthSeg, heightSeg, depthSeg |
SphereGeometry |
radius, widthSegments, heightSegments |
PlaneGeometry |
width, height, widthSeg, heightSeg |
CylinderGeometry |
radiusTop, radiusBottom, height, radialSegments |
TorusGeometry |
radius, tube, radialSegments, tubularSegments |
ConeGeometry |
radius, height, radialSegments |
IcosahedronGeometry |
radius, detail (0 = плоский low-poly) |
ExtrudeGeometry |
Shape + { depth, bevelEnabled, steps } |
// Власна BufferGeometry з сирих даних вершин
const geo = new THREE.BufferGeometry();
const positions = new Float32Array([
-1, -1, 0,
1, -1, 0,
0, 1, 0,
]);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.computeVertexNormals();
geo.computeBoundingSphere(); // потрібно для коректного frustum culling
// Об'єднання кількох геометрій в один draw call
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const merged = mergeGeometries([geoA, geoB, geoC]);
Матеріали
Матеріали визначають, як поверхня реагує на світло.
MeshStandardMaterial та
MeshPhysicalMaterial є фізично коректними (PBR) і
є стандартним вибором для реалістичного освітлення.
| Матеріал |
Модель освітлення |
Застосування |
MeshBasicMaterial |
Без освітлення (unlit) |
UI-накладки, каркаси, суто емісивні фігури |
MeshLambertMaterial |
Лише дифузне |
Дешеві матові поверхні, мобільні пристрої |
MeshPhongMaterial |
Дифузне + дзеркальне |
Блискучий пластик без повної вартості PBR |
MeshStandardMaterial |
PBR metallic/roughness |
Стандарт для реалістичних сцен |
MeshPhysicalMaterial |
PBR + clearcoat/transmission |
Скло, автомобільна фарба, покриті поверхні |
ShaderMaterial |
Власний GLSL |
Повний контроль — власні vertex/fragment шейдери |
const mat = new THREE.MeshStandardMaterial({
color: 0x3388ff,
metalness: 0.2,
roughness: 0.6,
map: colorTexture, // альбедо
normalMap: normalTexture,
roughnessMap: roughTexture,
envMapIntensity: 1.0,
transparent: true,
opacity: 0.9,
side: THREE.DoubleSide, // FrontSide | BackSide | DoubleSide
});
// Власний шейдерний матеріал
const shaderMat = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0xff5500) },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(uColor * (0.5 + 0.5 * sin(uTime + vUv.x * 6.28)), 1.0);
}
`,
});
Освітлення
| Джерело світла |
Поведінка |
Вартість |
AmbientLight |
Рівномірне світло без напрямку, без тіней |
Безкоштовно |
HemisphereLight |
Градієнт небо/земля — дешеве наближення зовнішнього освітлення |
Безкоштовно |
DirectionalLight |
Паралельні промені (сонце); тіні через ортографічний frustum |
Дешево |
PointLight |
Випромінює з точки; тінь використовує cube map (6 проходів) |
Дорого з тінями |
SpotLight |
Конус світла з кутом і напівтінню (penumbra) |
Помірно |
RectAreaLight |
М'яке світло від прямокутної панелі (без тіней) |
Помірно, потребує ініціалізації LTC-текстур |
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
const sun = new THREE.DirectionalLight(0xffffff, 3);
sun.position.set(5, 10, 5);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.left = -10;
sun.shadow.camera.right = 10;
sun.shadow.camera.top = 10;
sun.shadow.camera.bottom = -10;
sun.shadow.bias = -0.0005; // зменшує shadow acne
const spot = new THREE.SpotLight(0xffaa33, 5, 20, Math.PI / 6, 0.3);
spot.position.set(0, 8, 0);
scene.add(ambient, sun, spot);
Лише DirectionalLight, PointLight та
SpotLight можуть відкидати тіні. Кожен меш, який
має відкидати або приймати тінь, потребує явного встановлення
mesh.castShadow = true /
mesh.receiveShadow = true — самого лише
shadowMap.enabled на рендерері недостатньо.
Об'єкти та граф сцени
Object3D — базовий клас для всього, що можна
розмістити в сцені: меші, групи, камери й джерела світла
успадковують його трансформацію (позицію, обертання/кватерніон,
масштаб) та можливість вкладеності (parenting).
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(0, 1, 0);
mesh.rotation.set(0, Math.PI / 4, 0); // Euler, радіани
mesh.scale.setScalar(1.5);
mesh.castShadow = true;
mesh.receiveShadow = true;
// Групування об'єктів для спільної трансформації
const group = new THREE.Group();
group.add(mesh, otherMesh);
group.position.y = 2;
scene.add(group);
// Обхід усього графа
scene.traverse((obj) => {
if (obj.isMesh) obj.material.needsUpdate = true;
});
// Трансформації у світових координатах (з урахуванням батьків)
const worldPos = new THREE.Vector3();
mesh.getWorldPosition(worldPos);
// InstancedMesh для тисяч однакових об'єктів в одному draw call
const instanced = new THREE.InstancedMesh(geo, mat, 5000);
const m4 = new THREE.Matrix4();
for (let i = 0; i < 5000; i++) {
m4.makeTranslation(Math.random() * 20 - 10, 0, Math.random() * 20 - 10);
instanced.setMatrixAt(i, m4);
}
instanced.instanceMatrix.needsUpdate = true;
scene.add(instanced);
Контроли
| Контрол |
Типове застосування |
OrbitControls |
Обертання/масштабування/панорама навколо цілі — стандарт для демо й редакторів |
PointerLockControls |
Огляд від першої особи мишею, блокує курсор |
TrackballControls |
Вільне обертання без фіксованого вектора "вгору" |
TransformControls |
Гізмо для переміщення/обертання/масштабування у редакторах |
FlyControls |
Камера вільного польоту (у стилі космічного корабля) |
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 2;
controls.maxDistance = 50;
controls.maxPolarAngle = Math.PI / 2; // не дозволяє камері опускатися під землю
// Потрібно викликати щокадру, коли увімкнено демпфування (damping)
function tick() {
controls.update();
renderer.render(scene, camera);
}
Лоадери
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
// GLTF/GLB моделі, опційно стиснуті Draco
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load('model.glb', (gltf) => {
scene.add(gltf.scene);
gltf.animations; // AnimationClip[]
});
// Текстури
const texLoader = new THREE.TextureLoader();
const colorMap = texLoader.load('albedo.jpg');
colorMap.colorSpace = THREE.SRGBColorSpace; // кольорові текстури потребують sRGB
colorMap.wrapS = colorMap.wrapT = THREE.RepeatWrapping;
colorMap.anisotropy = renderer.capabilities.getMaxAnisotropy();
// HDR карти оточення
new RGBELoader().load('studio.hdr', (hdrTexture) => {
hdrTexture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = hdrTexture; // image-based lighting для PBR-матеріалів
scene.background = hdrTexture;
});
Усі лоадери асинхронні й побудовані на колбеках або Promise.
Використовуйте LoadingManager, щоб відстежувати
загальний прогрес завантаження багатьох ресурсів, і завжди
обробляйте колбек onError — невдале завантаження
ресурсу має деградувати без падіння всієї сцени.
Цикл анімації
const clock = new THREE.Clock();
function tick() {
const dt = clock.getDelta(); // секунди з попереднього кадру
const elapsed = clock.getElapsedTime();
mesh.rotation.y += dt * 0.5;
shaderMat.uniforms.uTime.value = elapsed;
controls.update();
renderer.render(scene, camera);
}
// Сумісний з WebXR цикл анімації (кращий за requestAnimationFrame)
renderer.setAnimationLoop(tick);
// Зупинити цикл
// renderer.setAnimationLoop(null);
// Обробка зміни розміру
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
Рейкастинг і взаємодія
Raycaster пускає промінь від камери через
нормалізовану позицію вказівника й повідомляє про перетини —
стандартний спосіб клікати, наводити курсор або обирати
3D-об'єкти.
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
window.addEventListener('pointermove', (event) => {
// Перетворення в нормалізовані координати пристрою: від -1 до +1
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
window.addEventListener('click', () => {
raycaster.setFromCamera(pointer, camera);
// true = перевіряти нащадків рекурсивно (потрібно для груп)
const hits = raycaster.intersectObjects(scene.children, true);
if (hits.length > 0) {
const { object, point, distance, face } = hits[0];
object.material.color.set(0xff0000);
console.log('Точка перетину:', point, 'відстань:', distance);
}
});
Рейкастинг проти тисяч об'єктів щокадру — витратна операція.
Обмежте список кандидатів (передавайте конкретний масив, а не
scene.children), або використовуйте просторовий
індекс (octree/BVH через three-mesh-bvh) для
великих статичних сцен.
Математичні типи
| Тип |
Поширені методи |
Vector3 |
.set(), .add(),
.sub(), .multiplyScalar(),
.normalize(), .length(),
.lerp(), .cross(),
.dot(), .applyQuaternion()
|
Quaternion |
.setFromAxisAngle(),
.setFromEuler(), .slerp(),
.multiply(), .invert()
|
Euler |
x, y, z (радіани) + порядок обертання, напр. 'XYZ' |
Matrix4 |
.compose(), .decompose(),
.makeTranslation(),
.multiplyMatrices(), .invert()
|
Box3 |
.setFromObject(),
.containsPoint(),
.intersectsBox(), .getCenter()
|
MathUtils |
.degToRad(), .radToDeg(),
.clamp(), .lerp(),
.randFloatSpread(),
.smoothstep()
|
// Усі тимчасові об'єкти варто виносити за межі гарячих циклів, щоб уникнути навантаження на GC
const _v = new THREE.Vector3();
const _q = new THREE.Quaternion();
_q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
_v.set(1, 0, 0).applyQuaternion(_q); // обертання вектора кватерніоном
// Розкладання світової матриці на позицію/кватерніон/масштаб
const pos = new THREE.Vector3();
const quat = new THREE.Quaternion();
const scale = new THREE.Vector3();
mesh.matrixWorld.decompose(pos, quat, scale);
Для інтерполяції та композиції обертань надавайте перевагу
кватерніонам, а не кутам Ейлера — кути Ейлера страждають від
gimbal lock і не інтерполюються лінійно найкоротшим шляхом.
Використовуйте Euler лише для створення/читання
зрозумілих людині обертань.