Урок · Середній · ~50 хв
WebXR · Three.js · VR · Введення з контролера

WebXR Device API — перші кроки

WebXR — це браузерний стандарт для іммерсивних VR та AR. Він керує трекінгом голови, стереоскопічним рендерингом, позами контролерів і тактильною віддачею — і все це з JavaScript. Цей урок проведе вас через вхід у вбудовану (inline) сесію попереднього перегляду, перехід до immersive-vr, обробку введення з контролера та відмалювання простої інтерактивної VR-сцени за допомогою Three.js.

1Перевірка підтримки XR і показ кнопки

const btn = document.getElementById('vr-btn'); if (!navigator.xr) { btn.textContent = 'WebXR not supported'; btn.disabled = true; } else { // Перевіряємо, чи доступний immersive-vr (потребує HTTPS + гарнітуру) navigator.xr.isSessionSupported('immersive-vr').then(supported => { if (supported) { btn.textContent = 'Enter VR'; btn.addEventListener('click', startXR); } else { btn.textContent = 'VR not available on this device'; btn.disabled = true; } }); } // Завжди показуємо вбудований 3D-перегляд (працює на будь-якому пристрої) navigator.xr?.isSessionSupported('inline').then(ok => { if (ok) startInlineSession(); });
WebXR потребує HTTPS (або localhost). Локально використовуйте serve / vite — звичайні file:// URL не працюватимуть.

2Запуск сесії immersive-vr

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js'; const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.xr.enabled = true; document.body.appendChild(renderer.domElement); let xrSession = null; async function startXR() { xrSession = await navigator.xr.requestSession('immersive-vr', { requiredFeatures: ['local-floor'], // опорний простір відносно підлоги optionalFeatures: ['hand-tracking'], // суглоби рук, якщо доступні }); // Повідомляємо Three.js про сесію — стереоскопічний рендеринг він обробляє внутрішньо await renderer.xr.setSession(xrSession); // Сесія завершується, коли користувач знімає гарнітуру xrSession.addEventListener('end', () => { xrSession = null; btn.textContent = 'Enter VR'; }); btn.textContent = 'Exit VR'; }

3Цикл рендерингу XR

У режимі XR Three.js перебирає на себе requestAnimationFrame. Використовуйте renderer.setAnimationLoop — він працює як на сторінці, так і у VR:

const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(70, innerWidth/innerHeight, 0.01, 100); scene.add(camera); // Додаємо кілька об'єктів на сцену const geo = new THREE.BoxGeometry(0.2, 0.2, 0.2); const mat = new THREE.MeshStandardMaterial({ color: 0x22c55e }); for (let i = 0; i < 10; i++) { const box = new THREE.Mesh(geo, mat); box.position.set( (Math.random() - 0.5) * 4, Math.random() * 1.5, -1 - Math.random() * 3 ); scene.add(box); } // Додаємо світло scene.add(new THREE.AmbientLight(0xffffff, 0.5)); const dirLight = new THREE.DirectionalLight(0xffffff, 1); dirLight.position.set(2, 4, 2); scene.add(dirLight); // Цикл — викликається браузером із частотою 90 Гц у VR renderer.setAnimationLoop((time, frame) => { renderer.render(scene, camera); });
Аргумент frame — це XRFrame; використовуйте його, щоб у тому ж циклі запитувати пози контролерів, суглоби рук та AR-hit-тести.

4Зчитування поз контролерів

// Three.js XRControllerModelFactory автоматично керує мешами контролерів import { XRControllerModelFactory } from 'https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/webxr/XRControllerModelFactory.js'; const cmf = new XRControllerModelFactory(); const controllers = [0, 1].map(i => { // Простір хвата: фізична модель контролера const grip = renderer.xr.getControllerGrip(i); grip.add(cmf.createControllerModel(grip)); scene.add(grip); // Простір променя: промінь вказування const ctrl = renderer.xr.getController(i); ctrl.addEventListener('selectstart', onSelectStart); ctrl.addEventListener('selectend', onSelectEnd); scene.add(ctrl); // Візуалізуємо промінь const ray = new THREE.Line( new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1), ]), new THREE.LineBasicMaterial({ color: 0x22c55e }) ); ctrl.add(ray); return ctrl; }); function onSelectStart(e) { // Контролер натиснув курок const ctrl = e.target; console.log('Select start from', ctrl === controllers[0] ? 'left' : 'right'); }

5Кнопки геймпада та тактильна віддача

renderer.setAnimationLoop((time, frame) => { if (frame) { const session = renderer.xr.getSession(); for (const source of session.inputSources) { const gp = source.gamepad; if (!gp) continue; // Стандартне розкладання кнопок (профіль геймпада OpenXR): // buttons[0] = курок, [1] = хват, [3] = натискання стіка, [4/5] = A/B const trigger = gp.buttons[0]?.value ?? 0; // аналогове 0–1 const grip = gp.buttons[1]?.pressed ?? false; // Аналоговий стік const stickX = gp.axes[2] ?? 0; // -1 вліво, +1 вправо const stickY = gp.axes[3] ?? 0; // -1 вгору, +1 вниз // Тактильний імпульс (якщо доступний) if (trigger > 0.9 && source.gamepad.hapticActuators?.length) { source.gamepad.hapticActuators[0].pulse(trigger * 0.5, 50); } } } renderer.render(scene, camera); });
Тактильна віддача потребує короткого імпульсу < 250 мс — довші імпульси більшість середовищ виконання мовчки ігнорує. Діапазон інтенсивності — 0–1.