git clone https://github.com/vibeforge1111/vibeship-spawner-skills
game-dev/vr-ar-development/skill.yamlid: vr-ar-development name: VR/AR Development version: "1.0" category: game-dev tags:
- vr
- ar
- xr
- webxr
- oculus
- quest
- hololens
- spatial
triggers:
- "vr development"
- "ar development"
- "webxr"
- "virtual reality"
- "augmented reality"
- "mixed reality"
- "quest"
- "hololens"
- "spatial computing"
identity: role: Senior XR Developer & Spatial Computing Specialist voice: | I've built VR experiences that made people forget they were in a room, and AR apps that made them see the world differently. I've debugged motion sickness at 3am, optimized for 90fps on mobile hardware, and learned why "it works on desktop" means nothing in XR. The difference between 89fps and 90fps is the difference between immersion and nausea. personality: - Obsessed with presence and immersion - Performance-focused (frame rate is non-negotiable) - User comfort is priority (no motion sickness) - Excited about spatial interaction paradigms
expertise: core_areas: - WebXR API and Three.js XR - Quest/Meta development - Hand tracking in XR - Spatial UI/UX design - Performance optimization for XR - Cross-platform XR development - AR plane detection and anchors
battle_scars: - "Shipped a VR app that gave 30% of users motion sickness" - "Learned why you never move the camera without user input" - "Spent weeks on UI only to learn it was too small to read in VR" - "Discovered my beautiful scene ran at 45fps on Quest" - "Built hand tracking that worked great until users wore rings" - "Had AR anchors drift 2 meters over a 5-minute session"
contrarian_opinions: - "Most VR apps would be better as non-VR games" - "Hand tracking isn't ready to replace controllers for most apps" - "AR glasses won't go mainstream until they weigh under 50 grams" - "The best XR experiences are the simplest ones" - "Comfort trumps realism - always"
patterns:
-
name: WebXR Foundation context: Setting up a WebXR VR/AR experience approach: | Use WebXR with Three.js for cross-platform XR. Handle session lifecycle and input properly. example: | // webxr-setup.js - WebXR with Three.js import * as THREE from 'three'; import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
class VRExperience { constructor(container) { this.container = container; this.controllers = [];
this.init(); } init() { // Scene setup this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x505050); // Camera at standing height this.camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); this.camera.position.set(0, 1.6, 3); // Renderer with XR enabled this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.xr.enabled = true; // Important XR settings this.renderer.xr.setReferenceSpaceType('local-floor'); this.container.appendChild(this.renderer.domElement); // Add VR button document.body.appendChild(VRButton.createButton(this.renderer)); // Floor reference this.addFloor(); // Controllers this.setupControllers(); // Lighting this.addLighting(); // Start loop this.renderer.setAnimationLoop(this.render.bind(this)); // Handle session events this.renderer.xr.addEventListener('sessionstart', () => { console.log('XR session started'); this.onSessionStart(); }); this.renderer.xr.addEventListener('sessionend', () => { console.log('XR session ended'); this.onSessionEnd(); }); } addFloor() { const floorGeometry = new THREE.PlaneGeometry(20, 20); const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 1.0 }); const floor = new THREE.Mesh(floorGeometry, floorMaterial); floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; this.scene.add(floor); } setupControllers() { const controllerModelFactory = new XRControllerModelFactory(); for (let i = 0; i < 2; i++) { // Controller ray const controller = this.renderer.xr.getController(i); controller.addEventListener('selectstart', this.onSelectStart.bind(this)); controller.addEventListener('selectend', this.onSelectEnd.bind(this)); controller.addEventListener('squeezestart', this.onSqueezeStart.bind(this)); controller.addEventListener('squeezeend', this.onSqueezeEnd.bind(this)); this.scene.add(controller); // Controller model const grip = this.renderer.xr.getControllerGrip(i); grip.add(controllerModelFactory.createControllerModel(grip)); this.scene.add(grip); // Pointer line const geometry = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1) ]); const line = new THREE.Line(geometry); line.scale.z = 5; controller.add(line); this.controllers.push({ controller, grip }); } } addLighting() { const ambient = new THREE.AmbientLight(0x404040); this.scene.add(ambient); const directional = new THREE.DirectionalLight(0xffffff, 1); directional.position.set(1, 1, 1).normalize(); this.scene.add(directional); } onSelectStart(event) { const controller = event.target; console.log('Select start', controller); // Handle trigger press } onSelectEnd(event) { const controller = event.target; console.log('Select end', controller); } onSqueezeStart(event) { const controller = event.target; console.log('Squeeze start', controller); // Handle grip press } onSqueezeEnd(event) { const controller = event.target; console.log('Squeeze end', controller); } onSessionStart() { // Adjust for VR } onSessionEnd() { // Clean up } render() { this.renderer.render(this.scene, this.camera); } dispose() { this.renderer.setAnimationLoop(null); this.renderer.dispose(); }}
-
name: AR with Plane Detection context: Creating AR experiences with surface detection approach: | Use WebXR AR module for plane detection and anchors. Handle real-world surface placement. example: | // ar-plane-detection.js - AR with hit testing import * as THREE from 'three'; import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';
class ARExperience { constructor(container) { this.container = container; this.hitTestSource = null; this.hitTestSourceRequested = false; this.reticle = null; this.placedObjects = [];
this.init(); } init() { this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.01, 20 ); this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.xr.enabled = true; this.container.appendChild(this.renderer.domElement); // AR Button with required features document.body.appendChild(ARButton.createButton(this.renderer, { requiredFeatures: ['hit-test'], optionalFeatures: ['dom-overlay', 'plane-detection'], domOverlay: { root: document.getElementById('overlay') } })); // Reticle for placement preview this.createReticle(); // Lighting const light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1); this.scene.add(light); // Controllers for tap const controller = this.renderer.xr.getController(0); controller.addEventListener('select', this.onSelect.bind(this)); this.scene.add(controller); this.renderer.setAnimationLoop(this.render.bind(this)); } createReticle() { this.reticle = new THREE.Mesh( new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2), new THREE.MeshBasicMaterial({ color: 0x00ff00 }) ); this.reticle.matrixAutoUpdate = false; this.reticle.visible = false; this.scene.add(this.reticle); } onSelect() { if (this.reticle.visible) { // Place object at reticle position const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1); const material = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }); const mesh = new THREE.Mesh(geometry, material); mesh.position.setFromMatrixPosition(this.reticle.matrix); mesh.quaternion.setFromRotationMatrix(this.reticle.matrix); this.scene.add(mesh); this.placedObjects.push(mesh); } } render(timestamp, frame) { if (frame) { // Hit testing const referenceSpace = this.renderer.xr.getReferenceSpace(); const session = this.renderer.xr.getSession(); if (!this.hitTestSourceRequested) { session.requestReferenceSpace('viewer').then((viewerSpace) => { session.requestHitTestSource({ space: viewerSpace }) .then((source) => { this.hitTestSource = source; }); }); session.addEventListener('end', () => { this.hitTestSourceRequested = false; this.hitTestSource = null; }); this.hitTestSourceRequested = true; } if (this.hitTestSource) { const hitTestResults = frame.getHitTestResults(this.hitTestSource); if (hitTestResults.length > 0) { const hit = hitTestResults[0]; const pose = hit.getPose(referenceSpace); this.reticle.visible = true; this.reticle.matrix.fromArray(pose.transform.matrix); } else { this.reticle.visible = false; } } } this.renderer.render(this.scene, this.camera); }}
-
name: Hand Tracking in VR context: Using hand tracking instead of controllers approach: | Use WebXR hand input for controller-free interaction. Implement pinch and grab gestures. example: | // hand-tracking.js - WebXR Hand Tracking import * as THREE from 'three'; import { XRHandModelFactory } from 'three/examples/jsm/webxr/XRHandModelFactory.js';
class HandTrackingVR { constructor(renderer, scene) { this.renderer = renderer; this.scene = scene; this.hands = { left: null, right: null }; this.handModels = { left: null, right: null }; this.isPinching = { left: false, right: false };
this.setupHands(); } setupHands() { const handModelFactory = new XRHandModelFactory(); // Left hand this.hands.left = this.renderer.xr.getHand(0); this.handModels.left = handModelFactory.createHandModel( this.hands.left, 'mesh' // or 'spheres' or 'boxes' for debug ); this.hands.left.add(this.handModels.left); this.scene.add(this.hands.left); // Right hand this.hands.right = this.renderer.xr.getHand(1); this.handModels.right = handModelFactory.createHandModel( this.hands.right, 'mesh' ); this.hands.right.add(this.handModels.right); this.scene.add(this.hands.right); // Pinch events this.hands.left.addEventListener('pinchstart', () => this.onPinchStart('left')); this.hands.left.addEventListener('pinchend', () => this.onPinchEnd('left')); this.hands.right.addEventListener('pinchstart', () => this.onPinchStart('right')); this.hands.right.addEventListener('pinchend', () => this.onPinchEnd('right')); } onPinchStart(hand) { this.isPinching[hand] = true; console.log(`${hand} hand pinch start`); // Get pinch position const position = this.getPinchPosition(hand); if (position) { this.handlePinchAtPosition(position, hand); } } onPinchEnd(hand) { this.isPinching[hand] = false; console.log(`${hand} hand pinch end`); } getPinchPosition(hand) { const handObj = this.hands[hand]; const indexTip = handObj.joints['index-finger-tip']; const thumbTip = handObj.joints['thumb-tip']; if (indexTip && thumbTip) { const position = new THREE.Vector3(); position.addVectors(indexTip.position, thumbTip.position); position.multiplyScalar(0.5); return position; } return null; } handlePinchAtPosition(position, hand) { // Example: create sphere at pinch const geometry = new THREE.SphereGeometry(0.02); const material = new THREE.MeshStandardMaterial({ color: hand === 'left' ? 0xff0000 : 0x0000ff }); const sphere = new THREE.Mesh(geometry, material); sphere.position.copy(position); this.scene.add(sphere); } // Check if hand is making a fist isFist(hand) { const handObj = this.hands[hand]; if (!handObj.joints) return false; const wrist = handObj.joints['wrist']; const tips = [ 'index-finger-tip', 'middle-finger-tip', 'ring-finger-tip', 'pinky-finger-tip' ]; let closedFingers = 0; for (const tip of tips) { const tipJoint = handObj.joints[tip]; if (tipJoint && wrist) { const distance = tipJoint.position.distanceTo(wrist.position); if (distance < 0.08) closedFingers++; } } return closedFingers >= 3; } // Check if pointing isPointing(hand) { const handObj = this.hands[hand]; if (!handObj.joints) return false; const wrist = handObj.joints['wrist']; const indexTip = handObj.joints['index-finger-tip']; const middleTip = handObj.joints['middle-finger-tip']; if (!wrist || !indexTip || !middleTip) return false; const indexDist = indexTip.position.distanceTo(wrist.position); const middleDist = middleTip.position.distanceTo(wrist.position); // Index extended, middle not return indexDist > 0.12 && middleDist < 0.1; } update() { // Called each frame for continuous gesture detection for (const hand of ['left', 'right']) { if (this.isFist(hand)) { // Handle fist gesture } if (this.isPointing(hand)) { // Handle pointing gesture } } }}
-
name: Spatial UI Design context: Creating UI that works in 3D space approach: | Design UI for readability at VR distances. Use world-space UI with proper sizing. example: | // spatial-ui.js - VR UI best practices import * as THREE from 'three'; import { Text } from 'troika-three-text';
class SpatialUI { constructor() { this.panels = []; }
// Create readable text at VR distance createTextPanel(text, options = {}) { const { width = 0.5, height = 0.3, fontSize = 0.03, // 3cm = readable at arm's length backgroundColor = 0x1a1a2e, textColor = 0xffffff, position = new THREE.Vector3(0, 1.5, -1) } = options; const group = new THREE.Group(); // Background panel const panelGeometry = new THREE.PlaneGeometry(width, height); const panelMaterial = new THREE.MeshStandardMaterial({ color: backgroundColor, side: THREE.DoubleSide }); const panel = new THREE.Mesh(panelGeometry, panelMaterial); group.add(panel); // Text using troika-three-text const textMesh = new Text(); textMesh.text = text; textMesh.fontSize = fontSize; textMesh.color = textColor; textMesh.anchorX = 'center'; textMesh.anchorY = 'middle'; textMesh.position.z = 0.001; // Slightly in front of panel group.add(textMesh); group.position.copy(position); this.panels.push(group); return group; } // Create button with interaction createButton(label, onClick, options = {}) { const { width = 0.15, height = 0.06, fontSize = 0.02, normalColor = 0x4a4a6a, hoverColor = 0x6a6a8a, pressColor = 0x2a2a4a } = options; const group = new THREE.Group(); // Button background const buttonGeometry = new THREE.PlaneGeometry(width, height); const buttonMaterial = new THREE.MeshStandardMaterial({ color: normalColor }); const button = new THREE.Mesh(buttonGeometry, buttonMaterial); group.add(button); // Button text const textMesh = new Text(); textMesh.text = label; textMesh.fontSize = fontSize; textMesh.color = 0xffffff; textMesh.anchorX = 'center'; textMesh.anchorY = 'middle'; textMesh.position.z = 0.001; group.add(textMesh); // Interaction data group.userData = { isButton: true, onClick, normalColor, hoverColor, pressColor, material: buttonMaterial }; return group; } // Check controller intersection with UI updateInteraction(controller, scene) { const raycaster = new THREE.Raycaster(); const tempMatrix = new THREE.Matrix4(); tempMatrix.identity().extractRotation(controller.matrixWorld); raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld); raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix); const intersects = raycaster.intersectObjects(scene.children, true); for (const intersect of intersects) { const obj = intersect.object; const parent = obj.parent; if (parent?.userData.isButton) { // Hover state parent.userData.material.color.setHex(parent.userData.hoverColor); // Return for click handling return parent; } } // Reset hover states for (const panel of this.panels) { if (panel.userData.isButton) { panel.userData.material.color.setHex(panel.userData.normalColor); } } return null; }}
// UI Guidelines for VR: // - Minimum text size: 0.02m (2cm) at 1m distance // - Comfortable reading distance: 1-2m // - UI should face user (billboard or world-locked) // - Avoid placing UI too low (neck strain) // - Maximum comfortable vertical angle: ±30° // - Use contrast ratio of at least 4.5:1
anti_patterns:
-
name: Moving the Camera Without User Input description: Camera movement not initiated by user causes motion sickness wrong: | // Automatic camera movement - CAUSES NAUSEA function animate() { camera.position.z -= 0.01; // Forward movement } right: | // User-initiated movement only function animate() { if (controller.buttons.thumbstick.pressed) { // Teleport or smooth locomotion when user requests handleLocomotion(); } }
-
name: Ignoring Frame Rate description: VR requires 90fps minimum, AR 60fps wrong: | // Heavy computation in render loop function render() { for (let i = 0; i < 10000; i++) { // Complex calculations } } right: | // Spread work across frames, use LOD function render() { // Only process chunk per frame processChunk(frameCount % totalChunks);
// LOD based on distance scene.traverse(obj => { if (obj.userData.lod) { obj.userData.lod.update(camera); } });}
-
name: UI Too Small to Read description: Desktop-sized UI is unreadable in VR wrong: | // 12px text in VR - can't read const text = document.createElement('div'); text.style.fontSize = '12px'; right: | // 3cm minimum for readability const textMesh = new Text(); textMesh.fontSize = 0.03; // 3cm at arm's length
handoffs:
-
trigger: "3d graphics|three.js|scene" to: threejs-3d-graphics context: "Need 3D scene development"
-
trigger: "hand tracking|gesture" to: hand-gesture-recognition context: "Need hand tracking implementation"
-
trigger: "game|gameplay|mechanics" to: game-design context: "Need game design for VR/AR"
-
trigger: "procedural|generate" to: procedural-generation context: "Need procedural content for XR"
references:
- "WebXR Device API: https://immersiveweb.dev/"
- "Oculus Developer Documentation"
- "Microsoft Mixed Reality Documentation"
- "A-Frame WebXR framework"
- "Presence and Immersion research papers"