Skip to content

VR games के लिए WebXR की बुनियादी बातें

WebXR, VR और AR को ब्राउज़र में लाता है। खिलाड़ी आपके game को बिना कुछ डाउनलोड किए headset में अनुभव कर सकते हैं।

1) WebXR support की जाँच करें

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 }
}

// Usage
const support = await checkXRSupport()
if (support.vr) {
  showVRButton()
}

2) एक VR session शुरू करना

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)
    
    // Set up rendering
    const gl = canvas.getContext('webgl2', { xrCompatible: true })
    await xrSession.updateRenderState({
      baseLayer: new XRWebGLLayer(xrSession, gl),
    })
    
    // Get reference space
    xrRefSpace = await xrSession.requestReferenceSpace('local-floor')
    
    // Start render loop
    xrSession.requestAnimationFrame(onXRFrame)
  } catch (err) {
    console.error('Failed to start VR:', err)
  }
}

function onSessionEnd() {
  xrSession = null
  xrRefSpace = null
}

3) XR render loop

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)
  
  // Render for each eye
  for (const view of pose.views) {
    const viewport = glLayer.getViewport(view)
    gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height)
    
    // Get camera matrices
    const viewMatrix = view.transform.inverse.matrix
    const projectionMatrix = view.projectionMatrix
    
    // Render scene with these matrices
    renderScene(viewMatrix, projectionMatrix)
  }
}

4) Input sources (controllers)

js
xrSession.addEventListener('inputsourceschange', (e) => {
  for (const source of e.added) {
    console.log('Controller added:', source.handedness) // 'left' or 'right'
  }
  for (const source of e.removed) {
    console.log('Controller removed:', source.handedness)
  }
})

function processInput(frame) {
  for (const source of xrSession.inputSources) {
    if (!source.gamepad) continue
    
    const gamepad = source.gamepad
    
    // Trigger (usually index 0)
    const triggerPressed = gamepad.buttons[0]?.pressed
    const triggerValue = gamepad.buttons[0]?.value || 0
    
    // Grip (usually index 1)
    const gripPressed = gamepad.buttons[1]?.pressed
    
    // Thumbstick
    const thumbstickX = gamepad.axes[2] || 0
    const thumbstickY = gamepad.axes[3] || 0
    
    // A/B buttons (index 4, 5)
    const buttonA = gamepad.buttons[4]?.pressed
    const buttonB = gamepad.buttons[5]?.pressed
  }
}

यह न मानें कि हर डिवाइस में gamepad होता है

ऊपर दिया गया code triggers और thumbsticks के लिए source.gamepad पढ़ता है। यह Meta Quest जैसे controller वाले headsets पर काम करता है, लेकिन Apple Vision Pro (visionOS 2 के Safari में WebXR डिफ़ॉल्ट रूप से चालू है) या Samsung Galaxy XR और दूसरे Android XR डिवाइसों पर कुछ नहीं करता, जहाँ प्राथमिक input gaze-and-pinch या hand tracking होता है। उन headsets पर source.gamepad null रहता है और session.inputSources तब तक खाली रहता है जब तक उपयोगकर्ता वाकई pinch नहीं करता।

जो एक interaction हर डिवाइस की गारंटी देता है, यानी "primary action", उसके लिए buttons को पोल करने के बजाय select events सुनें। WebXR एक controller trigger, एक hand pinch और एक Vision Pro gaze-and-pinch सबके लिए एक जैसे selectstart, select और selectend देता है, इसलिए एक ही handler इन सबको कवर कर लेता है:

js
xrSession.addEventListener('selectstart', (e) => {
  // e.inputSource is the controller, hand, or transient pinch
  // e.frame lets you read its targetRaySpace pose
})
xrSession.addEventListener('select', (e) => {
  // primary action completed (trigger pulled, pinch released on target)
})
xrSession.addEventListener('selectend', (e) => {})

controller वाले headsets पर thumbstick locomotion और अतिरिक्त buttons के लिए gamepad code रखें, लेकिन उसे if (source.gamepad) के पीछे रखें और अपने मुख्य "select/grab/activate" interaction को इन events के ज़रिए चलाएँ ताकि game hand- और gaze-driven डिवाइसों पर भी काम करता रहे।

5) Controller positions

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) पॉइंटिंग के लिए Ray casting

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) Teleportation locomotion

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) {
        // Aiming
        this.isAiming = true
        const ray = getControllerRay(frame, source)
        this.targetPosition = this.findTeleportTarget(ray)
      } else if (this.isAiming) {
        // Release - teleport
        if (this.targetPosition) {
          player.position.x = this.targetPosition.x
          player.position.z = this.targetPosition.z
        }
        this.isAiming = false
        this.targetPosition = null
      }
    }
  }
  
  findTeleportTarget(ray) {
    // Intersect with ground plane
    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) Smooth locomotion

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
    
    // Apply deadzone
    if (Math.abs(moveX) < 0.1) moveX = 0
    if (Math.abs(moveY) < 0.1) moveY = 0
    
    // Get head direction for movement
    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] }
    
    // Normalize to XZ plane
    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 comfort के दिशानिर्देश

js
// Vignette during movement to reduce motion sickness
function renderComfortVignette(movementSpeed) {
  const intensity = Math.min(movementSpeed / 5, 0.5)
  if (intensity < 0.1) return
  
  // Render dark edges that increase with speed
  renderVignette(intensity)
}

// Snap turning
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 degrees
    player.rotation += angle
    snapTurnCooldown = 0.3 // Prevent rapid turns
  }
}

10) पूरा WebXR सेटअप

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
    
    // Update game logic
    this.teleport.update(frame, this.session.inputSources)
    
    // Render
    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) {
    // Your rendering code here
  }
  
  onEnd() {
    this.session = null
  }
}

संबंधित

बाहरी संसाधन