Skip to content

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

js
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

js
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ü

js
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)

js
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:

js
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ı

js
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

js
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

js
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

js
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

js
// 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

js
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

Harici Kaynaklar