VR oyunları için WebXR temelleri
WebXR, VR ve AR'ı tarayıcıya getirir. Oyuncular hiçbir şey indirmeden oyununuzu bir başlıkla deneyimleyebilir.
1) WebXR desteğini kontrol etme
async function checkXRSupport() {
if (!navigator.xr) {
return { vr: false, ar: false }
}
const vr = await navigator.xr.isSessionSupported('immersive-vr')
const ar = await navigator.xr.isSessionSupported('immersive-ar')
return { vr, ar }
}
// Kullanım
const support = await checkXRSupport()
if (support.vr) {
showVRButton()
}2) VR oturumu başlatma
let xrSession = null
let xrRefSpace = null
async function startVR() {
try {
xrSession = await navigator.xr.requestSession('immersive-vr', {
requiredFeatures: ['local-floor'],
optionalFeatures: ['hand-tracking'],
})
xrSession.addEventListener('end', onSessionEnd)
// Görüntü oluşturmayı ayarla
const gl = canvas.getContext('webgl2', { xrCompatible: true })
await xrSession.updateRenderState({
baseLayer: new XRWebGLLayer(xrSession, gl),
})
// Referans uzayını al
xrRefSpace = await xrSession.requestReferenceSpace('local-floor')
// Görüntü oluşturma döngüsünü başlat
xrSession.requestAnimationFrame(onXRFrame)
} catch (err) {
console.error('VR başlatılamadı:', err)
}
}
function onSessionEnd() {
xrSession = null
xrRefSpace = null
}3) XR görüntü oluşturma döngüsü
function onXRFrame(time, frame) {
const session = frame.session
session.requestAnimationFrame(onXRFrame)
const pose = frame.getViewerPose(xrRefSpace)
if (!pose) return
const glLayer = session.renderState.baseLayer
gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
// Her göz için görüntü oluştur
for (const view of pose.views) {
const viewport = glLayer.getViewport(view)
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height)
// Kamera matrislerini al
const viewMatrix = view.transform.inverse.matrix
const projectionMatrix = view.projectionMatrix
// Sahneyi bu matrislerle oluştur
renderScene(viewMatrix, projectionMatrix)
}
}4) Giriş kaynakları (kontrolcüler)
xrSession.addEventListener('inputsourceschange', (e) => {
for (const source of e.added) {
console.log('Kontrolcü eklendi:', source.handedness) // 'left' veya 'right'
}
for (const source of e.removed) {
console.log('Kontrolcü kaldırıldı:', source.handedness)
}
})
function processInput(frame) {
for (const source of xrSession.inputSources) {
if (!source.gamepad) continue
const gamepad = source.gamepad
// Tetik (genellikle indeks 0)
const triggerPressed = gamepad.buttons[0]?.pressed
const triggerValue = gamepad.buttons[0]?.value || 0
// Kavrama (genellikle indeks 1)
const gripPressed = gamepad.buttons[1]?.pressed
// Başparmak çubuğu
const thumbstickX = gamepad.axes[2] || 0
const thumbstickY = gamepad.axes[3] || 0
// A/B düğmeleri (indeks 4, 5)
const buttonA = gamepad.buttons[4]?.pressed
const buttonB = gamepad.buttons[5]?.pressed
}
}Her cihazda gamepad bulunduğunu varsaymayın
Yukarıdaki kod, tetikler ve başparmak çubukları için source.gamepad değerini okur. Bu, Meta Quest gibi kontrolcülü başlıklarda çalışır ancak birincil girişin bakış ve parmak kıstırma ya da el takibi olduğu Apple Vision Pro'da (WebXR, visionOS 2 Safari'de varsayılan olarak etkindir), Samsung Galaxy XR'da ve diğer Android XR cihazlarında hiçbir şey yapmaz. Bu başlıklarda source.gamepad null değerindedir ve kullanıcı gerçekten parmaklarını kıstırana kadar session.inputSources boş kalır.
Her cihazın garanti ettiği tek etkileşim olan “birincil eylem” için düğmeleri sürekli denetlemek yerine select olaylarını dinleyin. WebXR; bir kontrolcü tetiği, el hareketiyle parmak kıstırma ve Vision Pro'daki bakış ve parmak kıstırma için aynı şekilde selectstart, select ve selectend olaylarını gönderir. Böylece tek bir işleyici bunların tümünü kapsar:
xrSession.addEventListener('selectstart', (e) => {
// e.inputSource kontrolcü, el veya geçici parmak kıstırma girişidir
// e.frame, targetRaySpace pozunu okumanızı sağlar
})
xrSession.addEventListener('select', (e) => {
// birincil eylem tamamlandı (tetik çekildi, hedef üzerindeki parmak kıstırma bırakıldı)
})
xrSession.addEventListener('selectend', (e) => {})Kontrolcülü başlıklarda başparmak çubuğuyla hareket ve ek düğmeler için gamepad kodunu koruyun ancak bunu if (source.gamepad) koşulunun arkasına alın. Temel “seç/kavra/etkinleştir” etkileşiminizi bu olaylar üzerinden yönlendirin; böylece oyun el ve bakışla kontrol edilen cihazlarda da çalışır.
5) Kontrolcü konumları
function getControllerPose(frame, source) {
if (!source.gripSpace) return null
const pose = frame.getPose(source.gripSpace, xrRefSpace)
if (!pose) return null
return {
position: pose.transform.position,
orientation: pose.transform.orientation,
matrix: pose.transform.matrix,
}
}
function renderControllers(frame) {
for (const source of xrSession.inputSources) {
const pose = getControllerPose(frame, source)
if (pose) {
renderControllerModel(pose, source.handedness)
}
}
}6) İşaretleme için ışın gönderimi
function getControllerRay(frame, source) {
if (!source.targetRaySpace) return null
const pose = frame.getPose(source.targetRaySpace, xrRefSpace)
if (!pose) return null
const origin = pose.transform.position
const direction = {
x: -pose.transform.matrix[8],
y: -pose.transform.matrix[9],
z: -pose.transform.matrix[10],
}
return { origin, direction }
}
function checkRayIntersection(ray, objects) {
let closest = null
let closestDist = Infinity
for (const obj of objects) {
const dist = rayIntersectBox(ray, obj.boundingBox)
if (dist !== null && dist < closestDist) {
closest = obj
closestDist = dist
}
}
return closest
}7) Işınlanarak hareket
class TeleportSystem {
constructor() {
this.targetPosition = null
this.isAiming = false
}
update(frame, inputSources) {
for (const source of inputSources) {
if (source.handedness !== 'left') continue
const gamepad = source.gamepad
const thumbstickY = gamepad?.axes[3] || 0
if (thumbstickY < -0.5) {
// Nişan alma
this.isAiming = true
const ray = getControllerRay(frame, source)
this.targetPosition = this.findTeleportTarget(ray)
} else if (this.isAiming) {
// Bırak — ışınlan
if (this.targetPosition) {
player.position.x = this.targetPosition.x
player.position.z = this.targetPosition.z
}
this.isAiming = false
this.targetPosition = null
}
}
}
findTeleportTarget(ray) {
// Zemin düzlemiyle kesiştir
if (ray.direction.y >= 0) return null
const t = -ray.origin.y / ray.direction.y
if (t < 0 || t > 10) return null
return {
x: ray.origin.x + ray.direction.x * t,
y: 0,
z: ray.origin.z + ray.direction.z * t,
}
}
render() {
if (this.isAiming && this.targetPosition) {
renderTeleportMarker(this.targetPosition)
}
}
}8) Akıcı hareket
function updateSmoothLocomotion(frame, inputSources, dt) {
for (const source of inputSources) {
if (source.handedness !== 'left') continue
const gamepad = source.gamepad
if (!gamepad) continue
const moveX = gamepad.axes[2] || 0
const moveY = gamepad.axes[3] || 0
// Ölü bölge uygula
if (Math.abs(moveX) < 0.1) moveX = 0
if (Math.abs(moveY) < 0.1) moveY = 0
// Hareket için baş yönünü al
const pose = frame.getViewerPose(xrRefSpace)
if (!pose) continue
const headMatrix = pose.transform.matrix
const forward = { x: -headMatrix[8], z: -headMatrix[10] }
const right = { x: headMatrix[0], z: headMatrix[2] }
// XZ düzlemine göre normalleştir
const len = Math.sqrt(forward.x ** 2 + forward.z ** 2)
forward.x /= len
forward.z /= len
const speed = 3 * dt
player.position.x += (forward.x * -moveY + right.x * moveX) * speed
player.position.z += (forward.z * -moveY + right.z * moveX) * speed
}
}9) VR konfor yönergeleri
// Hareket hastalığını azaltmak için hareket sırasında vinyet
function renderComfortVignette(movementSpeed) {
const intensity = Math.min(movementSpeed / 5, 0.5)
if (intensity < 0.1) return
// Hızla birlikte koyulaşan kenarları oluştur
renderVignette(intensity)
}
// Kademeli dönüş
let snapTurnCooldown = 0
function handleSnapTurn(gamepad, dt) {
snapTurnCooldown -= dt
const thumbstickX = gamepad.axes[2] || 0
if (Math.abs(thumbstickX) > 0.7 && snapTurnCooldown <= 0) {
const angle = Math.sign(thumbstickX) * (Math.PI / 4) // 45 derece
player.rotation += angle
snapTurnCooldown = 0.3 // Hızlı dönüşleri önle
}
}10) Eksiksiz WebXR kurulumu
class VRGame {
constructor(canvas) {
this.canvas = canvas
this.gl = canvas.getContext('webgl2', { xrCompatible: true })
this.session = null
this.refSpace = null
this.teleport = new TeleportSystem()
}
async checkSupport() {
if (!navigator.xr) return false
return await navigator.xr.isSessionSupported('immersive-vr')
}
async start() {
this.session = await navigator.xr.requestSession('immersive-vr', {
requiredFeatures: ['local-floor'],
})
await this.session.updateRenderState({
baseLayer: new XRWebGLLayer(this.session, this.gl),
})
this.refSpace = await this.session.requestReferenceSpace('local-floor')
this.session.addEventListener('end', () => this.onEnd())
this.session.requestAnimationFrame((t, f) => this.onFrame(t, f))
}
onFrame(time, frame) {
this.session.requestAnimationFrame((t, f) => this.onFrame(t, f))
const dt = (time - this.lastTime) / 1000
this.lastTime = time
// Oyun mantığını güncelle
this.teleport.update(frame, this.session.inputSources)
// Görüntüyü oluştur
const pose = frame.getViewerPose(this.refSpace)
if (!pose) return
const glLayer = this.session.renderState.baseLayer
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, glLayer.framebuffer)
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT)
for (const view of pose.views) {
const vp = glLayer.getViewport(view)
this.gl.viewport(vp.x, vp.y, vp.width, vp.height)
this.render(view.transform.inverse.matrix, view.projectionMatrix)
}
this.teleport.render()
}
render(viewMatrix, projectionMatrix) {
// Görüntü oluşturma kodunuz buraya gelir
}
onEnd() {
this.session = null
}
}İlgili içerikler
- WebGL temelleri
- WebGPU'ya başlangıç
- Oyun girişlerini işleme
- 2026'da Web Oyunları Teknoloji Yığını — WebXR'ın daha geniş teknoloji dünyasındaki yeri
- Tarayıcıda Three.js + USDC — XR sahneleri için 3D varlıkları yükleme
Harici Kaynaklar
- MDN: WebXR Device API — eksiksiz API referansı
- Immersive Web — WebXR örnekleri, araçları ve tarayıcı destek tabloları
- Three.js WebXR rehberi — Three.js ile VR içerikleri