diff --git a/renderer/viewer/three/world/vr.ts b/renderer/viewer/three/world/vr.ts index c2665585..f8c4e998 100644 --- a/renderer/viewer/three/world/vr.ts +++ b/renderer/viewer/three/world/vr.ts @@ -5,6 +5,7 @@ import { buttonMap as standardButtonsMap } from 'contro-max/build/gamepad' import * as THREE from 'three' import { WorldRendererThree } from '../worldrendererThree' import { DocumentRenderer } from '../documentRenderer' +import { VRHud } from './vrHud' export async function initVR (worldRenderer: WorldRendererThree, documentRenderer: DocumentRenderer) { if (!('xr' in navigator) || !worldRenderer.worldRendererConfig.vrSupport) return @@ -15,6 +16,9 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere enableVr() + // Create VR HUD + const vrHud = new VRHud(worldRenderer) + const vrButtonContainer = createVrButtonContainer(renderer) const updateVrButtons = () => { const newHidden = !worldRenderer.worldRendererConfig.vrSupport || !worldRenderer.worldRendererConfig.foreground @@ -37,6 +41,9 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere worldRenderer.reactiveState.preventEscapeMenu = false worldRenderer.scene.remove(user) vrButtonContainer.hidden = true + // Detach HUD when VR is disabled + vrHud.detachFromVRCamera(user) + vrHud.setVisible(false) } function createVrButtonContainer (renderer) { @@ -199,16 +206,28 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere // bot.entity.yaw = Math.atan2(-d.x, -d.z) // bot.entity.pitch = Math.asin(d.y) + // Update VR HUD + vrHud.update() + documentRenderer.frameRender(false) }) renderer.xr.addEventListener('sessionstart', () => { worldRenderer.cameraGroupVr = user + // Attach HUD to VR camera when session starts + vrHud.attachToVRCamera(user) + vrHud.setVisible(true) }) renderer.xr.addEventListener('sessionend', () => { worldRenderer.cameraGroupVr = undefined + // Detach HUD when session ends + vrHud.detachFromVRCamera(user) + vrHud.setVisible(false) }) - worldRenderer.abortController.signal.addEventListener('abort', disableVr) + worldRenderer.abortController.signal.addEventListener('abort', () => { + disableVr() + vrHud.dispose() + }) } const xrStandardRightButtonsMap = [ diff --git a/renderer/viewer/three/world/vrHud.ts b/renderer/viewer/three/world/vrHud.ts new file mode 100644 index 00000000..359b4bcd --- /dev/null +++ b/renderer/viewer/three/world/vrHud.ts @@ -0,0 +1,129 @@ +import * as THREE from 'three' +import { WorldRendererThree } from '../worldrendererThree' + +export class VRHud { + private hudMesh: THREE.Mesh + private hudCanvas: HTMLCanvasElement + private hudContext: CanvasRenderingContext2D + private hudTexture: THREE.CanvasTexture + private hudGroup: THREE.Group + + constructor(private worldRenderer: WorldRendererThree) { + // Create canvas for HUD + this.hudCanvas = document.createElement('canvas') + this.hudCanvas.width = 1024 + this.hudCanvas.height = 512 + + this.hudContext = this.hudCanvas.getContext('2d')! + + // Create texture from canvas + this.hudTexture = new THREE.CanvasTexture(this.hudCanvas) + this.hudTexture.minFilter = THREE.LinearFilter + this.hudTexture.magFilter = THREE.LinearFilter + + // Create HUD geometry - a plane that will display our canvas + // Adjusted size for better VR viewing + const hudGeometry = new THREE.PlaneGeometry(3, 1.5) + const hudMaterial = new THREE.MeshBasicMaterial({ + map: this.hudTexture, + transparent: true, + opacity: 0.8, + side: THREE.DoubleSide, + depthTest: false, + depthWrite: false + }) + + this.hudMesh = new THREE.Mesh(hudGeometry, hudMaterial) + this.hudMesh.renderOrder = 1000 // Render on top + + // Create a group to hold the HUD + this.hudGroup = new THREE.Group() + this.hudGroup.add(this.hudMesh) + + // Position the HUD in front of the camera + // Slightly lower and further for comfortable VR viewing + this.hudMesh.position.set(0, -0.3, -2.5) + + // Initial render to show something + this.update() + } + + attachToVRCamera(vrCameraGroup: THREE.Object3D) { + // Add HUD to the VR camera group so it follows the player's view + vrCameraGroup.add(this.hudGroup) + } + + detachFromVRCamera(vrCameraGroup: THREE.Object3D) { + vrCameraGroup.remove(this.hudGroup) + } + + update() { + // Get player data + const bot = (window as any).bot + const playerState = this.worldRenderer.playerState + + // Clear canvas + this.hudContext.clearRect(0, 0, this.hudCanvas.width, this.hudCanvas.height) + + // Set up text styling + this.hudContext.fillStyle = 'white' + this.hudContext.strokeStyle = 'black' + this.hudContext.lineWidth = 3 + this.hudContext.font = 'bold 32px Arial' + this.hudContext.textAlign = 'left' + this.hudContext.textBaseline = 'top' + + // Top left - FPS and Ping + const fps = Math.round(1000 / this.worldRenderer.renderTimeAvg) || 0 + const ping = bot?._client?.latency || 0 + + this.drawText(`FPS: ${fps}`, 50, 50) + this.drawText(`Ping: ${ping}ms`, 50, 90) + + // Top right - Velocity and Coords + this.hudContext.textAlign = 'right' + const velocity = playerState.getVelocity() + const position = playerState.getPosition() + const vel = Math.sqrt(velocity.x ** 2 + velocity.z ** 2).toFixed(2) + + this.drawText(`Vel: ${vel} m/s`, this.hudCanvas.width - 50, 50) + this.drawText(`X: ${position.x.toFixed(1)}`, this.hudCanvas.width - 50, 90) + this.drawText(`Y: ${position.y.toFixed(1)}`, this.hudCanvas.width - 50, 130) + this.drawText(`Z: ${position.z.toFixed(1)}`, this.hudCanvas.width - 50, 170) + + // Bottom left - Health + this.hudContext.textAlign = 'left' + this.hudContext.textBaseline = 'bottom' + const health = bot?.health || 10 + const maxHealth = 20 + const hearts = health / 2 + const maxHearts = maxHealth / 2 + + this.drawText(`HP: ${hearts}/${maxHearts} ❤`, 50, this.hudCanvas.height - 50) + + // Bottom right - Game mode + this.hudContext.textAlign = 'right' + const gameMode = playerState.reactive.gameMode || 'survival' + this.drawText(`Mode: ${gameMode}`, this.hudCanvas.width - 50, this.hudCanvas.height - 50) + + // Update texture + this.hudTexture.needsUpdate = true + } + + private drawText(text: string, x: number, y: number) { + // Draw text with outline for better visibility + this.hudContext.strokeText(text, x, y) + this.hudContext.fillText(text, x, y) + } + + setVisible(visible: boolean) { + this.hudMesh.visible = visible + } + + dispose() { + this.hudTexture.dispose() + this.hudMesh.geometry.dispose() + ;(this.hudMesh.material as THREE.Material).dispose() + this.hudCanvas.remove() + } +} \ No newline at end of file