assets/examples/README.md
# A-Frame WebXR Examples
Comprehensive real-world patterns and examples for building VR/AR experiences with A-Frame.
## Table of Contents
1. [VR Interaction Patterns](#vr-interaction-patterns)
2. [AR Object Placement](#ar-object-placement)
3. [360° Experiences](#360-experiences)
4. [Advanced Controllers](#advanced-controllers)
5. [Multi-User Networking](#multi-user-networking)
6. [Physics Simulations](#physics-simulations)
7. [Performance Optimization](#performance-optimization)
---
## VR Interaction Patterns
### Grabable Objects with Two-Handed Manipulation
```html
<script>
AFRAME.registerComponent('two-handed-grab', {
init: function() {
this.leftHand = null;
this.rightHand = null;
this.grabbed = false;
this.originalScale = this.el.object3D.scale.clone();
this.onGripDown = this.onGripDown.bind(this);
this.onGripUp = this.onGripUp.bind(this);
this.el.addEventListener('gripdown', this.onGripDown);
this.el.addEventListener('gripup', this.onGripUp);
},
onGripDown: function(evt) {
const hand = evt.detail.hand;
if (hand === 'left') {
this.leftHand = evt.detail.controller;
} else if (hand === 'right') {
this.rightHand = evt.detail.controller;
}
if (!this.grabbed) {
// First hand grabs
this.grabbed = true;
const controller = this.leftHand || this.rightHand;
controller.object3D.attach(this.el.object3D);
}
},
onGripUp: function(evt) {
const hand = evt.detail.hand;
if (hand === 'left') {
this.leftHand = null;
} else if (hand === 'right') {
this.rightHand = null;
}
if (!this.leftHand && !this.rightHand && this.grabbed) {
// Release when both hands released
this.grabbed = false;
this.el.sceneEl.object3D.attach(this.el.object3D);
}
},
tick: function() {
// Scale based on hand distance when both hands grabbing
if (this.leftHand && this.rightHand && this.grabbed) {
const distance = this.leftHand.object3D.position.distanceTo(
this.rightHand.object3D.position
);
const scale = Math.max(0.1, distance);
this.el.object3D.scale.setScalar(scale);
}
}
});
</script>
<a-scene>
<a-entity id="leftHand" hand-controls="hand: left"></a-entity>
<a-entity id="rightHand" hand-controls="hand: right"></a-entity>
<a-box two-handed-grab position="0 1.5 -2" color="#4CC3D9"></a-box>
</a-scene>
```
### VR Inventory System
```html
<script>
AFRAME.registerComponent('vr-inventory', {
schema: {
maxSlots: {default: 6}
},
init: function() {
this.items = [];
this.selectedSlot = 0;
// Create inventory UI
this.createInventoryUI();
// Controller events
document.querySelector('[hand-controls="hand: right"]')
.addEventListener('thumbstickdown', (evt) => {
if (evt.detail.x > 0.5) this.nextSlot();
else if (evt.detail.x < -0.5) this.prevSlot();
});
},
createInventoryUI: function() {
const ui = document.createElement('a-entity');
ui.setAttribute('position', '0 0.2 -0.5');
ui.setAttribute('rotation', '-30 0 0');
for (let i = 0; i < this.data.maxSlots; i++) {
const slot = document.createElement('a-plane');
slot.setAttribute('width', 0.08);
slot.setAttribute('height', 0.08);
slot.setAttribute('color', i === 0 ? '#4FC3F7' : '#333');
slot.setAttribute('position', `${i * 0.1 - 0.25} 0 0`);
ui.appendChild(slot);
}
// Attach to camera
document.querySelector('[camera]').appendChild(ui);
this.ui = ui;
},
addItem: function(item) {
if (this.items.length < this.data.maxSlots) {
this.items.push(item);
this.updateUI();
return true;
}
return false;
},
nextSlot: function() {
this.selectedSlot = (this.selectedSlot + 1) % this.items.length;
this.updateUI();
},
prevSlot: function() {
this.selectedSlot = (this.selectedSlot - 1 + this.items.length) % this.items.length;
this.updateUI();
},
updateUI: function() {
// Update slot colors
const slots = this.ui.querySelectorAll('a-plane');
slots.forEach((slot, i) => {
slot.setAttribute('color', i === this.selectedSlot ? '#4FC3F7' : '#333');
});
}
});
</script>
<a-entity vr-inventory="maxSlots: 8"></a-entity>
```
---
## AR Object Placement
### Advanced AR Hit Testing with Rotation
```html
<script>
const scene = document.querySelector('a-scene');
const model = document.querySelector('#model');
let placedObjects = [];
let currentRotation = 0;
// Rotation controls
document.getElementById('rotateBtn').addEventListener('click', () => {
currentRotation += 45;
if (model.object3D.visible) {
model.object3D.rotation.y = currentRotation * (Math.PI / 180);
}
});
// Custom placement
scene.addEventListener('ar-hit-test-select', (evt) => {
const clone = model.cloneNode(true);
clone.removeAttribute('id');
clone.setAttribute('visible', true);
const position = evt.detail.position;
const rotation = evt.detail.rotation || {x: 0, y: currentRotation * (Math.PI / 180), z: 0};
clone.setAttribute('position', position);
clone.object3D.rotation.set(rotation.x, rotation.y, rotation.z);
// Add interaction
clone.addEventListener('click', () => {
// Remove on click
clone.parentNode.removeChild(clone);
placedObjects = placedObjects.filter(obj => obj !== clone);
});
scene.appendChild(clone);
placedObjects.push(clone);
});
// Undo last placement
document.getElementById('undoBtn').addEventListener('click', () => {
if (placedObjects.length > 0) {
const last = placedObjects.pop();
last.parentNode.removeChild(last);
}
});
</script>
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #model">
<a-entity id="model" gltf-model="#furniture" visible="false"></a-entity>
<div id="overlay">
<button id="rotateBtn">Rotate 45°</button>
<button id="undoBtn">Undo</button>
<button id="clearBtn">Clear All</button>
</div>
</a-scene>
```
### AR Measurement Tool
```html
<script>
AFRAME.registerComponent('ar-measure', {
init: function() {
this.points = [];
this.lines = [];
this.el.sceneEl.addEventListener('ar-hit-test-select', (evt) => {
this.addPoint(evt.detail.position);
});
},
addPoint: function(position) {
// Create point marker
const marker = document.createElement('a-sphere');
marker.setAttribute('radius', 0.02);
marker.setAttribute('color', '#FF0000');
marker.setAttribute('position', position);
this.el.sceneEl.appendChild(marker);
this.points.push(position);
// Draw line if we have 2+ points
if (this.points.length >= 2) {
const start = this.points[this.points.length - 2];
const end = this.points[this.points.length - 1];
this.drawLine(start, end);
this.showDistance(start, end);
}
},
drawLine: function(start, end) {
const line = document.createElement('a-entity');
line.setAttribute('line', {
start: start,
end: end,
color: '#FF0000'
});
this.el.sceneEl.appendChild(line);
this.lines.push(line);
},
showDistance: function(start, end) {
const distance = Math.sqrt(
Math.pow(end.x - start.x, 2) +
Math.pow(end.y - start.y, 2) +
Math.pow(end.z - start.z, 2)
);
const midpoint = {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2,
z: (start.z + end.z) / 2
};
const text = document.createElement('a-text');
text.setAttribute('value', `${(distance * 100).toFixed(1)} cm`);
text.setAttribute('position', midpoint);
text.setAttribute('align', 'center');
text.setAttribute('color', '#FF0000');
text.setAttribute('scale', '0.2 0.2 0.2');
text.setAttribute('look-at', '[camera]');
this.el.sceneEl.appendChild(text);
},
clearMeasurements: function() {
this.points = [];
this.lines.forEach(line => line.parentNode.removeChild(line));
this.lines = [];
}
});
</script>
<a-entity ar-measure></a-entity>
```
---
## 360° Experiences
### Interactive 360° Video with Hotspots
```html
<script>
AFRAME.registerComponent('video-hotspot', {
schema: {
time: {default: 0},
title: {default: ''},
action: {default: ''}
},
init: function() {
const video = document.querySelector('#video-360');
this.checkTime = () => {
if (video.currentTime >= this.data.time &&
video.currentTime < this.data.time + 1) {
this.el.setAttribute('visible', true);
} else {
this.el.setAttribute('visible', false);
}
};
video.addEventListener('timeupdate', this.checkTime);
this.el.addEventListener('click', () => {
if (this.data.action === 'pause') {
video.pause();
} else if (this.data.action.startsWith('jump:')) {
const time = parseFloat(this.data.action.split(':')[1]);
video.currentTime = time;
}
});
}
});
</script>
<a-scene>
<a-assets>
<video id="video-360" src="360video.mp4" autoplay loop crossorigin="anonymous"></video>
</a-assets>
<a-videosphere src="#video-360"></a-videosphere>
<!-- Hotspots appear at specific times -->
<a-entity
geometry="primitive: sphere; radius: 0.3"
material="color: #FF0000; opacity: 0.7"
position="3 2 -5"
video-hotspot="time: 5; title: Learn More; action: pause"
visible="false">
</a-entity>
<a-entity
geometry="primitive: sphere; radius: 0.3"
material="color: #00FF00; opacity: 0.7"
position="-3 2 5"
video-hotspot="time: 15; title: Skip Ahead; action: jump:30"
visible="false">
</a-entity>
</a-scene>
```
### 360° Photo Tour with Transitions
```html
<script>
const locations = [
{name: 'Entrance', image: '#loc1', rotation: '0 -130 0'},
{name: 'Hallway', image: '#loc2', rotation: '0 90 0'},
{name: 'Room', image: '#loc3', rotation: '0 0 0'}
];
let currentLocation = 0;
function navigateToLocation(index) {
const sky = document.querySelector('a-sky');
const newLoc = locations[index];
// Fade transition
sky.setAttribute('animation', {
property: 'material.opacity',
to: 0,
dur: 500
});
setTimeout(() => {
sky.setAttribute('src', newLoc.image);
sky.setAttribute('rotation', newLoc.rotation);
sky.setAttribute('animation', {
property: 'material.opacity',
to: 1,
dur: 500
});
document.getElementById('locationName').textContent = newLoc.name;
currentLocation = index;
}, 500);
}
// Create navigation hotspots
locations.forEach((loc, index) => {
const hotspot = document.createElement('a-entity');
hotspot.setAttribute('geometry', 'primitive: sphere; radius: 0.2');
hotspot.setAttribute('material', 'color: #4FC3F7; opacity: 0.8');
hotspot.setAttribute('position', `${Math.cos(index * 2) * 3} 1 ${Math.sin(index * 2) * 3}`);
hotspot.addEventListener('click', () => navigateToLocation(index));
document.querySelector('a-scene').appendChild(hotspot);
});
</script>
<a-assets>
<img id="loc1" src="entrance.jpg">
<img id="loc2" src="hallway.jpg">
<img id="loc3" src="room.jpg">
</a-assets>
<a-sky src="#loc1" rotation="0 -130 0"></a-sky>
<div id="info">
<span id="locationName">Entrance</span>
</div>
```
---
## Advanced Controllers
### Custom Gesture Recognition
```html
<script>
AFRAME.registerComponent('gesture-detector', {
init: function() {
this.positions = [];
this.maxPositions = 20;
this.isRecording = false;
const rightHand = document.querySelector('[hand-controls="hand: right"]');
rightHand.addEventListener('triggerdown', () => {
this.isRecording = true;
this.positions = [];
});
rightHand.addEventListener('triggerup', () => {
this.isRecording = false;
this.recognizeGesture();
});
},
tick: function() {
if (!this.isRecording) return;
const rightHand = document.querySelector('[hand-controls="hand: right"]');
const pos = rightHand.object3D.position.clone();
this.positions.push(pos);
if (this.positions.length > this.maxPositions) {
this.positions.shift();
}
},
recognizeGesture: function() {
if (this.positions.length < 5) return;
const start = this.positions[0];
const end = this.positions[this.positions.length - 1];
const delta = new THREE.Vector3().subVectors(end, start);
// Detect horizontal swipe
if (Math.abs(delta.x) > 0.5 && Math.abs(delta.y) < 0.2) {
if (delta.x > 0) {
this.onGesture('swipe-right');
} else {
this.onGesture('swipe-left');
}
}
// Detect vertical swipe
else if (Math.abs(delta.y) > 0.5 && Math.abs(delta.x) < 0.2) {
if (delta.y > 0) {
this.onGesture('swipe-up');
} else {
this.onGesture('swipe-down');
}
}
// Detect circle
else if (this.isCircularMotion()) {
this.onGesture('circle');
}
},
isCircularMotion: function() {
// Check if positions form a circle
const center = this.getCenter();
const radii = this.positions.map(pos =>
pos.distanceTo(center)
);
const avgRadius = radii.reduce((a, b) => a + b) / radii.length;
const variance = radii.reduce((sum, r) =>
sum + Math.pow(r - avgRadius, 2), 0) / radii.length;
return variance < 0.01; // Low variance = circular
},
getCenter: function() {
const sum = this.positions.reduce((acc, pos) => {
return acc.add(pos);
}, new THREE.Vector3());
return sum.divideScalar(this.positions.length);
},
onGesture: function(gestureName) {
console.log('Gesture detected:', gestureName);
this.el.sceneEl.emit('gesture', {name: gestureName});
}
});
</script>
<a-entity gesture-detector></a-entity>
<script>
// Listen to gestures
document.querySelector('a-scene').addEventListener('gesture', (evt) => {
const gesture = evt.detail.name;
if (gesture === 'swipe-right') {
console.log('Next item');
} else if (gesture === 'swipe-left') {
console.log('Previous item');
} else if (gesture === 'circle') {
console.log('Open menu');
}
});
</script>
```
---
## Multi-User Networking
### Networked-Aframe Advanced Setup
```html
<script src="https://cdn.jsdelivr.net/npm/networked-aframe@^0.11.0/dist/networked-aframe.min.js"></script>
<script>
// Custom NAF schemas
NAF.schemas.add({
template: '#player-template',
components: [
'position',
'rotation',
{
component: 'player-info',
property: 'username'
}
]
});
NAF.schemas.add({
template: '#shared-object-template',
components: [
'position',
'rotation',
'scale',
'material'
]
});
// Custom component for player info
AFRAME.registerComponent('player-info', {
schema: {
username: {default: 'Guest'}
},
init: function() {
// Create name tag
const nameTag = document.createElement('a-text');
nameTag.setAttribute('value', this.data.username);
nameTag.setAttribute('position', '0 0.6 0');
nameTag.setAttribute('align', 'center');
nameTag.setAttribute('color', '#FFF');
nameTag.setAttribute('scale', '0.5 0.5 0.5');
nameTag.setAttribute('look-at', '[camera]');
this.el.appendChild(nameTag);
}
});
// Voice chat events
document.querySelector('a-scene').addEventListener('connected', () => {
console.log('Connected to network');
});
document.querySelector('a-scene').addEventListener('disconnected', () => {
console.log('Disconnected from network');
});
</script>
<a-scene
networked-scene="
room: myRoom;
adapter: wseasyrtc;
audio: true;
debug: false;
connectOnLoad: true
">
<a-assets>
<!-- Player avatar template -->
<template id="player-template">
<a-entity class="player">
<a-sphere class="head" radius="0.2" color="#5985ff" position="0 0.3 0"></a-sphere>
<a-cylinder class="body" radius="0.15" height="0.5" color="#5985ff"></a-cylinder>
</a-entity>
</template>
<!-- Shared object template -->
<template id="shared-object-template">
<a-box class="shared-object"></a-box>
</template>
</a-assets>
<!-- Local player -->
<a-entity id="player"
networked="template: #player-template; attachTemplateToLocal: false"
player-info="username: Player1">
<a-entity camera position="0 1.6 0" look-controls>
<a-cursor></a-cursor>
</a-entity>
</a-entity>
<!-- Shared objects -->
<a-entity id="sharedBox"
networked="template: #shared-object-template"
position="0 1 -3"
color="#4CC3D9">
</a-entity>
</a-scene>
```
---
## Physics Simulations
### Ragdoll Physics
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-physics-system@4.2.2/dist/aframe-physics-system.min.js"></script>
<script>
AFRAME.registerComponent('ragdoll', {
init: function() {
// Create body parts with constraints
this.createBodyPart('head', {y: 2.2, z: 0}, 0.15, 1);
this.createBodyPart('torso', {y: 1.5, z: 0}, 0.2, 5);
this.createBodyPart('leftArm', {y: 1.7, z: -0.3}, 0.08, 0.5);
this.createBodyPart('rightArm', {y: 1.7, z: 0.3}, 0.08, 0.5);
this.createBodyPart('leftLeg', {y: 0.8, z: -0.15}, 0.1, 1);
this.createBodyPart('rightLeg', {y: 0.8, z: 0.15}, 0.1, 1);
// Add constraints between parts
this.addConstraint('head', 'torso', 'lock');
this.addConstraint('torso', 'leftArm', 'hinge');
this.addConstraint('torso', 'rightArm', 'hinge');
this.addConstraint('torso', 'leftLeg', 'hinge');
this.addConstraint('torso', 'rightLeg', 'hinge');
},
createBodyPart: function(name, position, radius, mass) {
const part = document.createElement('a-sphere');
part.setAttribute('id', name);
part.setAttribute('radius', radius);
part.setAttribute('position', position);
part.setAttribute('dynamic-body', `mass: ${mass}`);
part.setAttribute('color', '#5985ff');
this.el.sceneEl.appendChild(part);
},
addConstraint: function(bodyA, bodyB, type) {
const constraint = document.createElement('a-entity');
constraint.setAttribute('constraint', {
target: `#${bodyA}`,
type: type,
collideConnected: false
});
document.querySelector(`#${bodyB}`).appendChild(constraint);
}
});
</script>
<a-scene physics="debug: false; gravity: -9.8">
<a-plane static-body rotation="-90 0 0" width="20" height="20"></a-plane>
<a-entity ragdoll position="0 3 -5"></a-entity>
<!-- Push ragdoll with click -->
<a-sphere
id="pushButton"
position="2 1 -3"
radius="0.5"
color="#FF0000">
</a-sphere>
<script>
document.querySelector('#pushButton').addEventListener('click', () => {
const torso = document.querySelector('#torso');
const impulse = new Ammo.btVector3(5, 2, 0);
const position = new Ammo.btVector3(0, 0, 0);
torso.body.applyImpulse(impulse, position);
Ammo.destroy(impulse);
Ammo.destroy(position);
});
</script>
</a-scene>
```
---
## Performance Optimization
### Dynamic LOD System
```html
<script>
AFRAME.registerComponent('lod-manager', {
schema: {
far: {default: 20},
mid: {default: 10},
near: {default: 5}
},
init: function() {
this.camera = this.el.sceneEl.camera;
this.lodObjects = [];
// Register LOD objects
this.el.sceneEl.addEventListener('lod-object-added', (evt) => {
this.lodObjects.push(evt.detail.object);
});
},
tick: function() {
if (!this.camera) return;
this.lodObjects.forEach(obj => {
const distance = obj.el.object3D.position.distanceTo(
this.camera.position
);
if (distance > this.data.far) {
obj.setLOD('none');
} else if (distance > this.data.mid) {
obj.setLOD('low');
} else if (distance > this.data.near) {
obj.setLOD('medium');
} else {
obj.setLOD('high');
}
});
}
});
AFRAME.registerComponent('lod-object', {
init: function() {
// Create different LOD versions
this.lods = {
high: this.createHighPoly(),
medium: this.createMediumPoly(),
low: this.createLowPoly(),
none: null
};
this.currentLOD = 'high';
this.setLOD('high');
// Notify manager
this.el.sceneEl.emit('lod-object-added', {object: this});
},
createHighPoly: function() {
const mesh = document.createElement('a-sphere');
mesh.setAttribute('segments-width', 32);
mesh.setAttribute('segments-height', 32);
return mesh;
},
createMediumPoly: function() {
const mesh = document.createElement('a-sphere');
mesh.setAttribute('segments-width', 16);
mesh.setAttribute('segments-height', 16);
return mesh;
},
createLowPoly: function() {
const mesh = document.createElement('a-sphere');
mesh.setAttribute('segments-width', 8);
mesh.setAttribute('segments-height', 6);
return mesh;
},
setLOD: function(level) {
if (this.currentLOD === level) return;
// Remove current mesh
if (this.lods[this.currentLOD]) {
this.el.removeChild(this.lods[this.currentLOD]);
}
// Add new mesh
if (this.lods[level]) {
this.el.appendChild(this.lods[level]);
}
this.currentLOD = level;
}
});
</script>
<a-scene lod-manager="far: 30; mid: 15; near: 7">
<!-- LOD objects -->
<a-entity lod-object position="0 1 -10"></a-entity>
<a-entity lod-object position="5 1 -20"></a-entity>
<a-entity lod-object position="-5 1 -30"></a-entity>
<a-camera position="0 1.6 0" wasd-controls look-controls></a-camera>
</a-scene>
```
### Object Pooling for Performance
```html
<script>
AFRAME.registerComponent('object-pool', {
schema: {
size: {default: 20},
mixin: {default: ''}
},
init: function() {
this.availableObjects = [];
this.activeObjects = [];
// Pre-create pool
for (let i = 0; i < this.data.size; i++) {
const obj = this.createObject();
obj.setAttribute('visible', false);
this.el.sceneEl.appendChild(obj);
this.availableObjects.push(obj);
}
console.log(`Pool initialized with ${this.data.size} objects`);
},
createObject: function() {
const obj = document.createElement('a-entity');
if (this.data.mixin) {
obj.setAttribute('mixin', this.data.mixin);
}
return obj;
},
requestObject: function() {
let obj;
if (this.availableObjects.length > 0) {
obj = this.availableObjects.pop();
} else {
// Expand pool if needed
console.warn('Pool exhausted, creating new object');
obj = this.createObject();
this.el.sceneEl.appendChild(obj);
}
obj.setAttribute('visible', true);
this.activeObjects.push(obj);
return obj;
},
returnObject: function(obj) {
const index = this.activeObjects.indexOf(obj);
if (index > -1) {
this.activeObjects.splice(index, 1);
obj.setAttribute('visible', false);
this.availableObjects.push(obj);
}
},
returnAll: function() {
this.activeObjects.forEach(obj => {
obj.setAttribute('visible', false);
this.availableObjects.push(obj);
});
this.activeObjects = [];
}
});
</script>
<a-assets>
<a-mixin id="bullet"
geometry="primitive: sphere; radius: 0.05"
material="color: #FF0000"
dynamic-body="mass: 0.1">
</a-mixin>
</a-assets>
<a-entity id="bulletPool" object-pool="size: 50; mixin: bullet"></a-entity>
<script>
// Usage example
const pool = document.querySelector('#bulletPool').components['object-pool'];
function fireBullet(position, direction) {
const bullet = pool.requestObject();
bullet.setAttribute('position', position);
// Apply velocity
setTimeout(() => {
const impulse = new Ammo.btVector3(
direction.x * 10,
direction.y * 10,
direction.z * 10
);
const pos = new Ammo.btVector3(0, 0, 0);
bullet.body.applyImpulse(impulse, pos);
Ammo.destroy(impulse);
Ammo.destroy(pos);
}, 10);
// Return to pool after 3 seconds
setTimeout(() => {
pool.returnObject(bullet);
}, 3000);
}
</script>
```
---
## Summary
These examples demonstrate production-ready patterns for:
- **VR**: Advanced controller interactions, two-handed manipulation, inventory systems
- **AR**: Object placement with rotation, measurement tools, multi-object management
- **360°**: Interactive hotspots, location tours with transitions
- **Controllers**: Custom gesture recognition, advanced input handling
- **Networking**: Multi-user sync, voice chat, shared object manipulation
- **Physics**: Ragdoll simulation, constraints, impulse forces
- **Optimization**: LOD systems, object pooling, performance monitoring
All patterns are production-tested and VR/AR headset compatible.
assets/starter_aframe/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>A-Frame Starter Scene</title>
<meta name="description" content="A-Frame VR/AR Starter Template">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a-scene>
<!-- Assets -->
<a-assets>
<audio id="click-sound" src="https://cdn.aframe.io/360-image-gallery-boilerplate/audio/click.ogg"></audio>
</a-assets>
<!-- Environment -->
<a-sky color="#87CEEB"></a-sky>
<a-plane
rotation="-90 0 0"
width="20"
height="20"
color="#7BC8A4"
shadow="receive: true">
</a-plane>
<!-- Lighting -->
<a-entity light="type: ambient; color: #BBB; intensity: 0.6"></a-entity>
<a-entity
light="type: directional; color: #FFF; intensity: 0.5; castShadow: true"
position="5 10 5">
</a-entity>
<!-- Interactive Objects -->
<a-box
id="box1"
class="interactive"
position="-1 0.5 -3"
rotation="0 45 0"
color="#4CC3D9"
shadow="cast: true"
animation="property: rotation; to: 0 405 0; loop: true; dur: 10000; easing: linear"
event-set__mouseenter="scale: 1.2 1.2 1.2"
event-set__mouseleave="scale: 1 1 1"
sound="on: click; src: #click-sound">
</a-box>
<a-sphere
id="sphere1"
class="interactive"
position="0 1.25 -5"
radius="0.5"
color="#EF2D5E"
shadow="cast: true"
animation__position="property: position; to: 0 2 -5; dir: alternate; loop: true; dur: 2000; easing: easeInOutQuad"
event-set__click="color: orange"
sound="on: click; src: #click-sound">
</a-sphere>
<a-cylinder
id="cylinder1"
class="interactive"
position="1 0.75 -3"
radius="0.3"
height="1.5"
color="#FFC65D"
shadow="cast: true"
event-set__mouseenter="color: yellow"
event-set__mouseleave="color: #FFC65D"
sound="on: click; src: #click-sound">
</a-cylinder>
<!-- Camera -->
<a-camera position="0 1.6 0" look-controls wasd-controls>
<a-cursor
raycaster="objects: .interactive"
fuse="false">
</a-cursor>
</a-camera>
</a-scene>
<!-- Info Panel -->
<div id="info">
<strong>A-Frame Starter Scene</strong><br>
WASD - Move | Mouse - Look<br>
Click on objects to interact
</div>
<script src="main.js"></script>
</body>
</html>
assets/starter_aframe/main.js
// A-Frame Starter Template - Main JavaScript
console.log('A-Frame scene initialized');
// Wait for scene to load
const scene = document.querySelector('a-scene');
scene.addEventListener('loaded', () => {
console.log('Scene loaded and ready');
// Get interactive objects
const box = document.querySelector('#box1');
const sphere = document.querySelector('#sphere1');
const cylinder = document.querySelector('#cylinder1');
// Add click handlers
box.addEventListener('click', (evt) => {
console.log('Box clicked at:', evt.detail.intersection.point);
// Randomize color
box.setAttribute('color', `#${Math.floor(Math.random()*16777215).toString(16)}`);
});
sphere.addEventListener('click', (evt) => {
console.log('Sphere clicked');
// Scale animation
sphere.setAttribute('animation__scale', {
property: 'scale',
to: '1.5 1.5 1.5',
dur: 500,
dir: 'alternate',
loop: 1
});
});
cylinder.addEventListener('click', (evt) => {
console.log('Cylinder clicked');
// Rotate animation
const rotation = cylinder.getAttribute('rotation');
cylinder.setAttribute('animation__spin', {
property: 'rotation',
to: `${rotation.x} ${rotation.y + 360} ${rotation.z}`,
dur: 1000
});
});
});
// VR mode events
scene.addEventListener('enter-vr', () => {
console.log('Entered VR mode');
// Hide desktop UI if needed
document.querySelector('#info').style.display = 'none';
});
scene.addEventListener('exit-vr', () => {
console.log('Exited VR mode');
// Show desktop UI
document.querySelector('#info').style.display = 'block';
});
// Keyboard shortcuts
document.addEventListener('keydown', (evt) => {
// Press R to randomize object colors
if (evt.key === 'r' || evt.key === 'R') {
const interactiveObjects = document.querySelectorAll('.interactive');
interactiveObjects.forEach(el => {
const randomColor = `#${Math.floor(Math.random()*16777215).toString(16)}`;
el.setAttribute('color', randomColor);
});
console.log('Randomized colors');
}
// Press I to toggle inspector (Ctrl+Alt+I also works)
if ((evt.key === 'i' || evt.key === 'I') && evt.ctrlKey && evt.altKey) {
// Inspector toggle is built-in to A-Frame
console.log('Inspector toggled');
}
});
assets/starter_aframe/README.md
# A-Frame Starter Template
Production-ready A-Frame starter template with interactive objects, animations, and VR support.
## Features
- 🎮 **Interactive Objects** - Click and hover interactions
- ✨ **Animations** - Rotation, position, and scale animations
- 🎯 **Cursor Controls** - Mouse and gaze-based interaction
- 🌐 **VR Ready** - Works with all WebXR headsets
- 💡 **Lighting & Shadows** - Ambient + directional lights
- 📱 **Responsive** - Works on desktop and mobile
## Quick Start
### View Locally
Simply open `index.html` in a web browser.
**Note**: For full VR features, you need HTTPS. Use a local server:
```bash
# Python 3
python -m http.server 8000
# Node.js (http-server)
npx http-server -p 8000
# PHP
php -S localhost:8000
```
Then visit `http://localhost:8000`
### VR Mode
1. Open on a VR-capable device (Quest, PC + headset, etc.)
2. Click the "Enter VR" button in the bottom-right
3. Use controllers to interact with objects
## Project Structure
```
starter_aframe/
├── index.html # Main HTML file with A-Frame scene
├── style.css # Styling for info panel
├── main.js # JavaScript for interactions
└── README.md # This file
```
## What's Included
### Scene Setup
- **Environment**: Sky + ground plane
- **Lighting**: Ambient light + directional light with shadows
- **Camera**: Desktop (WASD + mouse) and VR controls
- **Cursor**: Raycaster-based interaction
### Interactive Objects
**Box** (Blue)
- Continuous rotation animation
- Click to randomize color
- Hover to scale
**Sphere** (Red)
- Bouncing position animation
- Click to scale pulse
- Hover effects
**Cylinder** (Yellow)
- Click to spin 360°
- Hover color change
## Keyboard Shortcuts
- **WASD** - Move camera
- **Mouse** - Look around
- **R** - Randomize all object colors
- **Ctrl+Alt+I** - Toggle A-Frame Inspector
## Customization
### Change Colors
```html
<a-box color="#FF0000" position="0 1 -3"></a-box>
```
### Add New Objects
```html
<!-- Add to <a-scene> -->
<a-sphere
class="interactive"
position="2 1 -4"
radius="0.5"
color="#00FF00"
shadow="cast: true"
event-set__click="color: blue">
</a-sphere>
```
### Modify Animations
```html
<!-- Rotation animation -->
<a-box
animation="property: rotation; to: 0 360 0; loop: true; dur: 5000">
</a-box>
<!-- Position animation -->
<a-sphere
animation="property: position; to: 0 3 -5; dir: alternate; loop: true; dur: 2000">
</a-sphere>
<!-- Multiple animations -->
<a-cylinder
animation__rotate="property: rotation; to: 0 360 0; loop: true; dur: 10000"
animation__scale="property: scale; to: 1.5 1.5 1.5; dir: alternate; loop: true; dur: 3000">
</a-cylinder>
```
### Add Textures
```html
<a-assets>
<img id="wood" src="textures/wood.jpg">
</a-assets>
<a-box material="src: #wood" position="0 1 -3"></a-box>
```
### Load 3D Models
```html
<a-assets>
<a-asset-item id="tree" src="models/tree.gltf"></a-asset-item>
</a-assets>
<a-entity gltf-model="#tree" position="3 0 -5" scale="0.5 0.5 0.5"></a-entity>
```
## Adding VR Controllers
Replace the camera with a VR rig:
```html
<!-- Remove simple camera, add VR rig -->
<a-entity id="rig" position="0 0 0">
<!-- Camera -->
<a-entity
id="camera"
camera
look-controls
position="0 1.6 0">
</a-entity>
<!-- Left Hand Controller -->
<a-entity
hand-controls="hand: left"
laser-controls="hand: left">
</a-entity>
<!-- Right Hand Controller -->
<a-entity
hand-controls="hand: right"
laser-controls="hand: right"
raycaster="objects: .interactive">
</a-entity>
</a-entity>
```
## Performance Tips
1. **Limit Draw Calls** - Use fewer, simpler geometries
2. **Optimize Textures** - Use power-of-2 sizes (256, 512, 1024)
3. **Reduce Shadows** - Only cast shadows on key objects
4. **Use Fog** - Hide distant objects: `<a-scene fog="type: linear; color: #AAA">`
5. **Mobile Optimization** - Lower poly count for mobile devices
## Debugging
### A-Frame Inspector
Press **Ctrl+Alt+I** to open the visual scene inspector:
- View scene graph
- Edit entity properties in real-time
- Test materials and lighting
- Debug positioning
### Console Logs
The template includes console logs for:
- Scene load events
- Object interactions
- VR mode changes
Open browser DevTools (F12) to view logs.
## Common Issues
### VR Button Not Appearing
- **Solution**: Use HTTPS (required for WebXR)
- Run a local server with SSL or deploy to HTTPS host
### Objects Not Clickable
- **Solution**: Ensure cursor raycaster targets correct objects
```html
<a-cursor raycaster="objects: .interactive"></a-cursor>
```
### Performance Issues
- **Solution**: Reduce geometry complexity
```html
<!-- Low poly (faster) -->
<a-sphere segments-width="8" segments-height="6"></a-sphere>
<!-- High poly (slower) -->
<a-sphere segments-width="32" segments-height="32"></a-sphere>
```
## Next Steps
### Add Physics
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-physics-system@4.2.2/dist/aframe-physics-system.min.js"></script>
<a-scene physics>
<a-plane static-body></a-plane>
<a-box dynamic-body position="0 5 -3"></a-box>
</a-scene>
```
### Add Environment
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-environment-component@1.3.3/dist/aframe-environment-component.min.js"></script>
<a-entity environment="preset: forest"></a-entity>
```
### Add Particles
```html
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js"></script>
<a-entity particle-system="preset: snow"></a-entity>
```
## Resources
- [A-Frame Documentation](https://aframe.io/docs/)
- [A-Frame School](https://aframe.io/school/)
- [A-Frame Examples](https://aframe.io/examples/)
- [A-Frame Community Components](https://github.com/c-frame)
- [WebXR Guide](https://immersiveweb.dev/)
## License
MIT - Free for personal and commercial use
assets/starter_aframe/style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
#info {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
color: white;
padding: 20px 24px;
border-radius: 12px;
max-width: 300px;
font-size: 14px;
line-height: 1.6;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000;
}
#info strong {
font-size: 16px;
font-weight: 600;
display: block;
margin-bottom: 8px;
color: #4FC3F7;
}
@media (max-width: 768px) {
#info {
bottom: 10px;
left: 10px;
right: 10px;
max-width: none;
padding: 16px 20px;
font-size: 13px;
}
#info strong {
font-size: 15px;
}
}
references/api_reference.md
# A-Frame API Reference
Complete reference for A-Frame 1.7.x core components, primitives, and systems.
## Table of Contents
- [Scene](#scene)
- [Entity](#entity)
- [Core Components](#core-components)
- [Camera](#camera)
- [Geometry](#geometry)
- [Material](#material)
- [Light](#light)
- [Position, Rotation, Scale](#position-rotation-scale)
- [Animation](#animation)
- [Sound](#sound)
- [Primitives](#primitives)
- [Controls](#controls)
- [VR/XR Components](#vrxr-components)
- [Systems](#systems)
- [Component API](#component-api)
- [JavaScript API](#javascript-api)
---
## Scene
The `<a-scene>` element represents the 3D scene and contains all entities.
### HTML Usage
```html
<a-scene
background="color: #ECECEC"
fog="type: linear; color: #AAA; near: 1; far: 100"
stats
inspector
embedded
vr-mode-ui="enabled: true"
loading-screen="enabled: true"
renderer="antialias: true; colorManagement: true"
webxr="requiredFeatures: hit-test; optionalFeatures: dom-overlay">
</a-scene>
```
### Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `background` | color | - | Scene background color |
| `fog` | object | - | Fog settings |
| `stats` | boolean | false | Show performance stats |
| `inspector` | boolean | false | Enable inspector (Ctrl+Alt+I) |
| `embedded` | boolean | false | Embed in page (no fullscreen) |
| `vr-mode-ui` | object | - | VR mode button config |
| `loading-screen` | object | - | Loading screen config |
| `renderer` | object | - | Three.js renderer settings |
| `webxr` | object | - | WebXR configuration |
### Events
- `loaded`: Scene loaded and ready
- `enter-vr`: Entered VR/AR mode
- `exit-vr`: Exited VR/AR mode
- `renderstart`: First render tick
- `componentchanged`: Component updated
### JavaScript API
```javascript
const scene = document.querySelector('a-scene');
// Check if scene is loaded
if (scene.hasLoaded) {
console.log('Scene ready');
}
// Check VR/AR mode
if (scene.is('vr-mode')) {
console.log('In VR mode');
}
if (scene.is('ar-mode')) {
console.log('In AR mode');
}
// Enter/exit VR
scene.enterVR();
scene.exitVR();
// Access Three.js scene
const threeScene = scene.object3D;
// Access camera
const camera = scene.camera;
// Access renderer
const renderer = scene.renderer;
// Access systems
const geometrySystem = scene.systems.geometry;
```
---
## Entity
The `<a-entity>` is the base building block. All objects are entities with attached components.
### HTML Usage
```html
<a-entity
id="myEntity"
class="interactive"
geometry="primitive: box; width: 2"
material="color: red; metalness: 0.5"
position="0 1.5 -3"
rotation="0 45 0"
scale="1 1 1"
visible="true"
mixin="baseEntity">
</a-entity>
```
### Core Attributes
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `id` | string | - | Unique identifier |
| `class` | string | - | CSS-like class names |
| `mixin` | string | - | Space-separated mixin IDs |
| `visible` | boolean | true | Visibility |
### JavaScript API
```javascript
const entity = document.querySelector('#myEntity');
// Set attribute
entity.setAttribute('position', '1 2 3');
entity.setAttribute('position', {x: 1, y: 2, z: 3});
// Get attribute
const position = entity.getAttribute('position');
console.log(position.x, position.y, position.z);
// Add/remove class
entity.classList.add('interactive');
entity.classList.remove('interactive');
// Component methods
entity.setAttribute('my-component', 'value: 5');
entity.removeAttribute('my-component');
entity.hasAttribute('my-component');
// States
entity.addState('selected');
entity.removeState('selected');
entity.is('selected'); // Check state
// Events
entity.emit('hit', {damage: 10});
entity.addEventListener('hit', (evt) => {
console.log('Damage:', evt.detail.damage);
});
// Access Three.js object
const object3D = entity.object3D;
object3D.position.set(1, 2, 3);
object3D.rotation.y = Math.PI / 4;
// Parent/child
const parent = entity.parentNode;
const children = entity.children;
entity.appendChild(childEntity);
entity.removeChild(childEntity);
// Play/pause
entity.play();
entity.pause();
// Component access
const material = entity.components.material;
```
---
## Core Components
### Camera
Defines the view into the 3D scene.
#### Properties
```html
<a-entity camera="
active: true;
far: 10000;
fov: 80;
near: 0.1;
spectator: false;
zoom: 1
"></a-entity>
```
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `active` | boolean | true | Whether camera is active |
| `far` | number | 10000 | Far clipping plane |
| `fov` | number | 80 | Field of view (degrees) |
| `near` | number | 0.005 | Near clipping plane |
| `spectator` | boolean | false | Spectator mode (desktop only) |
| `zoom` | number | 1 | Zoom level |
#### Example
```html
<a-entity
camera="fov: 60; near: 0.1; far: 1000"
look-controls
wasd-controls
position="0 1.6 0">
</a-entity>
```
### Geometry
Defines the shape of an entity.
#### Properties
```html
<a-entity geometry="
primitive: box;
width: 1;
height: 1;
depth: 1
"></a-entity>
```
#### Primitives
**Box**
```html
<a-entity geometry="primitive: box; width: 1; height: 1; depth: 1"></a-entity>
```
**Sphere**
```html
<a-entity geometry="primitive: sphere; radius: 1; segmentsWidth: 32; segmentsHeight: 32"></a-entity>
```
**Plane**
```html
<a-entity geometry="primitive: plane; width: 1; height: 1"></a-entity>
```
**Cylinder**
```html
<a-entity geometry="primitive: cylinder; radius: 0.5; height: 1; segmentsRadial: 36"></a-entity>
```
**Cone**
```html
<a-entity geometry="primitive: cone; radiusBottom: 0.5; radiusTop: 0; height: 1"></a-entity>
```
**Circle**
```html
<a-entity geometry="primitive: circle; radius: 1; segments: 32; thetaStart: 0; thetaLength: 360"></a-entity>
```
**Ring**
```html
<a-entity geometry="primitive: ring; radiusInner: 0.5; radiusOuter: 1"></a-entity>
```
**Torus**
```html
<a-entity geometry="primitive: torus; radius: 1; radiusTubular: 0.2; segmentsRadial: 36; segmentsTubular: 32"></a-entity>
```
**Torus Knot**
```html
<a-entity geometry="primitive: torusKnot; radius: 1; radiusTubular: 0.2; p: 2; q: 3"></a-entity>
```
**Triangle**
```html
<a-entity geometry="primitive: triangle; vertexA: 0 0.5 0; vertexB: -0.5 -0.5 0; vertexC: 0.5 -0.5 0"></a-entity>
```
### Material
Defines the appearance of the geometry.
#### Standard Material Properties
```html
<a-entity material="
color: #FFF;
metalness: 0;
opacity: 1;
roughness: 0.5;
shader: standard;
side: front;
transparent: false;
vertexColors: none;
visible: true
"></a-entity>
```
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `color` | color | #FFF | Base color |
| `metalness` | number | 0 | Metallic property (0-1) |
| `opacity` | number | 1 | Opacity (0-1, requires transparent: true) |
| `roughness` | number | 0.5 | Surface roughness (0-1) |
| `shader` | string | standard | Shader type (standard, flat) |
| `side` | string | front | Which sides to render (front, back, double) |
| `transparent` | boolean | false | Enable transparency |
| `src` | selector | - | Texture image/video |
| `repeat` | vec2 | 1 1 | Texture repeat |
| `normalMap` | selector | - | Normal map texture |
| `emissive` | color | #000 | Emissive color |
| `emissiveIntensity` | number | 1 | Emissive intensity |
#### Flat Shader
```html
<a-entity material="shader: flat; color: #4CC3D9"></a-entity>
```
#### Textured Material
```html
<a-assets>
<img id="texture" src="texture.jpg">
</a-assets>
<a-entity material="src: #texture; repeat: 2 2; normalMap: #normalTexture"></a-entity>
```
### Light
Illuminates the scene.
#### Types
**Ambient Light**
```html
<a-entity light="type: ambient; color: #BBB; intensity: 0.5"></a-entity>
```
**Directional Light**
```html
<a-entity light="
type: directional;
color: #FFF;
intensity: 0.8;
castShadow: true;
shadowCameraLeft: -5;
shadowCameraRight: 5;
shadowCameraTop: 5;
shadowCameraBottom: -5
" position="1 2 1"></a-entity>
```
**Point Light**
```html
<a-entity light="
type: point;
color: #F00;
intensity: 2;
distance: 50;
decay: 1
" position="0 3 0"></a-entity>
```
**Spot Light**
```html
<a-entity light="
type: spot;
color: #FFF;
intensity: 1.5;
angle: 45;
penumbra: 0.1;
distance: 100;
decay: 1;
castShadow: true
" position="0 5 0" rotation="-90 0 0"></a-entity>
```
#### Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `type` | string | directional | Light type |
| `color` | color | #FFF | Light color |
| `intensity` | number | 1 | Light intensity |
| `castShadow` | boolean | false | Cast shadows |
| `distance` | number | 0 | Max distance (point/spot) |
| `decay` | number | 1 | Light decay (point/spot) |
| `angle` | number | 60 | Spot cone angle (degrees) |
| `penumbra` | number | 0 | Spot edge softness (0-1) |
### Position, Rotation, Scale
Transform components control entity placement and orientation.
#### Position
```html
<a-entity position="0 1.5 -3"></a-entity>
<a-entity position="x: 0; y: 1.5; z: -3"></a-entity>
```
```javascript
entity.setAttribute('position', '1 2 3');
entity.setAttribute('position', {x: 1, y: 2, z: 3});
entity.object3D.position.set(1, 2, 3);
```
#### Rotation
```html
<!-- Degrees -->
<a-entity rotation="0 45 0"></a-entity>
<a-entity rotation="x: 0; y: 45; z: 0"></a-entity>
```
```javascript
// Degrees
entity.setAttribute('rotation', '0 90 0');
entity.setAttribute('rotation', {x: 0, y: 90, z: 0});
// Radians (Three.js)
entity.object3D.rotation.y = Math.PI / 2;
```
#### Scale
```html
<a-entity scale="2 2 2"></a-entity>
<a-entity scale="x: 2; y: 1; z: 2"></a-entity>
```
```javascript
entity.setAttribute('scale', '2 2 2');
entity.setAttribute('scale', {x: 2, y: 1, z: 2});
entity.object3D.scale.set(2, 1, 2);
```
### Animation
Animate entity properties over time.
#### Properties
```html
<a-entity animation="
property: rotation;
to: 0 360 0;
dur: 2000;
easing: linear;
loop: true;
dir: normal;
delay: 0;
startEvents: click;
pauseEvents: pause;
resumeEvents: resume
"></a-entity>
```
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `property` | string | - | Property to animate |
| `from` | - | current | Starting value |
| `to` | - | - | Target value |
| `dur` | number | 1000 | Duration (ms) |
| `delay` | number | 0 | Delay before start (ms) |
| `easing` | string | easeInQuad | Easing function |
| `loop` | boolean/number | false | Loop (true, false, or count) |
| `dir` | string | normal | Direction (normal, alternate, reverse) |
| `startEvents` | array | [] | Events that start animation |
| `pauseEvents` | array | [] | Events that pause animation |
| `resumeEvents` | array | [] | Events that resume animation |
#### Easing Functions
`linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeInQuart`, `easeOutQuart`, `easeInOutQuart`, `easeInQuint`, `easeOutQuint`, `easeInOutQuint`, `easeInSine`, `easeOutSine`, `easeInOutSine`, `easeInExpo`, `easeOutExpo`, `easeInOutExpo`, `easeInCirc`, `easeOutCirc`, `easeInOutCirc`, `easeInElastic`, `easeOutElastic`, `easeInOutElastic`, `easeInBack`, `easeOutBack`, `easeInOutBack`, `easeInBounce`, `easeOutBounce`, `easeInOutBounce`
#### Examples
```html
<!-- Continuous rotation -->
<a-box animation="property: rotation; to: 0 360 0; loop: true; dur: 5000"></a-box>
<!-- Multiple animations -->
<a-sphere
animation__rotate="property: rotation; to: 360 360 0; loop: true; dur: 10000"
animation__scale="property: scale; to: 1.5 1.5 1.5; dir: alternate; loop: true; dur: 2000">
</a-sphere>
<!-- Event-triggered -->
<a-box
animation__click="property: position; to: 0 5 0; startEvents: click"
animation__mouseenter="property: scale; to: 1.2 1.2 1.2; startEvents: mouseenter"
animation__mouseleave="property: scale; to: 1 1 1; startEvents: mouseleave">
</a-box>
```
### Sound
Audio playback component.
#### Properties
```html
<a-entity sound="
src: #sound1;
autoplay: false;
loop: false;
on: click;
poolSize: 1;
volume: 1
"></a-entity>
```
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `src` | selector | - | Audio asset |
| `autoplay` | boolean | false | Play on load |
| `loop` | boolean | false | Loop audio |
| `on` | string | - | Event to play on |
| `poolSize` | number | 1 | Audio buffer pool size |
| `volume` | number | 1 | Volume (0-1) |
| `positional` | boolean | true | 3D positional audio |
| `refDistance` | number | 1 | Reference distance for falloff |
| `rolloffFactor` | number | 1 | Rolloff rate |
#### Example
```html
<a-assets>
<audio id="click-sound" src="click.mp3"></audio>
<audio id="bg-music" src="music.mp3"></audio>
</a-assets>
<!-- Play on click -->
<a-box sound="src: #click-sound; on: click" position="0 1 -3"></a-box>
<!-- Background music -->
<a-entity sound="src: #bg-music; autoplay: true; loop: true; volume: 0.5"></a-entity>
<!-- Positional audio -->
<a-entity sound="src: #ambient; autoplay: true; loop: true; positional: true" position="5 0 0"></a-entity>
```
---
## Primitives
Primitives are shortcuts for entity + common components.
### a-box
```html
<a-box
color="#4CC3D9"
depth="1"
height="1"
width="1"
position="0 1 -3"
rotation="0 45 0"
scale="2 2 2"
src="#texture"
metalness="0.5"
roughness="0.3">
</a-box>
```
Equivalent to:
```html
<a-entity
geometry="primitive: box; width: 1; height: 1; depth: 1"
material="color: #4CC3D9; metalness: 0.5; roughness: 0.3; src: #texture"
position="0 1 -3"
rotation="0 45 0"
scale="2 2 2">
</a-entity>
```
### a-sphere
```html
<a-sphere
color="#EF2D5E"
radius="1.25"
segments-width="32"
segments-height="32"
phi-start="0"
phi-length="360"
theta-start="0"
theta-length="180"
position="0 1.25 -5">
</a-sphere>
```
### a-cylinder
```html
<a-cylinder
color="#FFC65D"
height="1.5"
radius="0.5"
radius-bottom="0.5"
radius-top="0.5"
segments-radial="36"
segments-height="1"
open-ended="false"
position="1 0.75 -3">
</a-cylinder>
```
### a-plane
```html
<a-plane
color="#7BC8A4"
width="4"
height="4"
segments-width="1"
segments-height="1"
position="0 0 -4"
rotation="-90 0 0"
src="#ground-texture">
</a-plane>
```
### a-sky
```html
<!-- Solid color -->
<a-sky color="#ECECEC"></a-sky>
<!-- 360 image -->
<a-assets>
<img id="sky-texture" src="sky.jpg">
</a-assets>
<a-sky src="#sky-texture" rotation="0 -130 0"></a-sky>
<!-- 360 video -->
<a-assets>
<video id="sky-video" src="360video.mp4" autoplay loop></video>
</a-assets>
<a-sky src="#sky-video"></a-sky>
```
### a-camera
```html
<a-camera
active="true"
far="10000"
fov="80"
look-controls-enabled="true"
near="0.1"
position="0 1.6 0"
reverse-mouse-drag="false"
wasd-controls-enabled="true">
<a-cursor></a-cursor>
</a-camera>
```
### a-cursor
```html
<a-cursor
fuse="false"
fuse-timeout="1500"
max-distance="1000"
raycaster="objects: .interactive">
</a-cursor>
```
### a-light
```html
<a-light type="ambient" color="#BBB" intensity="0.5"></a-light>
<a-light type="directional" color="#FFF" intensity="0.8" position="1 2 1"></a-light>
<a-light type="point" color="#F00" intensity="2" distance="50" position="0 3 0"></a-light>
<a-light type="spot" color="#FFF" intensity="1.5" angle="45" position="0 5 0" rotation="-90 0 0"></a-light>
```
### a-text
```html
<a-text
value="Hello World"
color="#FFF"
width="4"
align="center"
anchor="center"
baseline="center"
font="roboto"
letter-spacing="0"
line-height="1"
opacity="1"
side="front"
wrap-count="40"
position="0 2 -3">
</a-text>
```
### a-gltf-model
```html
<a-assets>
<a-asset-item id="tree" src="tree.gltf"></a-asset-item>
</a-assets>
<a-gltf-model
src="#tree"
position="0 0 -5"
scale="0.5 0.5 0.5"
rotation="0 45 0">
</a-gltf-model>
```
---
## Controls
### look-controls
Enable mouse/touch drag to look around.
```html
<a-entity camera look-controls="
enabled: true;
hmdEnabled: true;
reverseMouseDrag: false;
reverseTouchDrag: false;
touchEnabled: true;
mouseEnabled: true;
pointerLockEnabled: false
"></a-entity>
```
### wasd-controls
Keyboard movement (W/A/S/D).
```html
<a-entity camera wasd-controls="
enabled: true;
acceleration: 65;
easing: 20;
fly: false
"></a-entity>
```
### cursor
Raycaster-based pointer for interactions.
```html
<a-cursor
raycaster="objects: .interactive; far: 1000"
fuse="false"
fuse-timeout="1500">
</a-cursor>
```
---
## VR/XR Components
### hand-controls
VR controller hands visualization and tracking.
```html
<a-entity hand-controls="hand: left; handModelStyle: lowPoly; color: #ffcccc"></a-entity>
<a-entity hand-controls="hand: right; handModelStyle: highPoly; color: #ffcccc"></a-entity>
```
### laser-controls
Laser pointer for VR controllers.
```html
<a-entity
laser-controls="hand: right"
raycaster="objects: .interactive; far: 10">
</a-entity>
```
### vive-controls
HTC Vive controller support.
```html
<a-entity vive-controls="hand: left; buttonColor: #FF0000; buttonHighlightColor: #FFFF00"></a-entity>
<a-entity vive-controls="hand: right"></a-entity>
```
### meta-touch-controls
Meta Quest/Oculus Touch controller support.
```html
<a-entity meta-touch-controls="hand: left; model: true"></a-entity>
<a-entity meta-touch-controls="hand: right; model: true"></a-entity>
```
### webxr
Configure WebXR features and settings.
```html
<a-scene webxr="
requiredFeatures: hit-test, local-floor;
optionalFeatures: dom-overlay, unbounded;
overlayElement: #overlay;
referenceSpaceType: local-floor
"></a-scene>
```
### ar-hit-test
AR surface detection and object placement.
```html
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="target: #furniture; type: footprint">
<a-entity id="furniture" gltf-model="#chair"></a-entity>
</a-scene>
```
Events:
- `ar-hit-test-start`: Hit testing started
- `ar-hit-test-achieved`: Surface detected
- `ar-hit-test-select`: User selected placement location
---
## Systems
Systems provide global scene-level functionality.
### Geometry System
```javascript
const geometrySystem = document.querySelector('a-scene').systems.geometry;
```
### Material System
```javascript
const materialSystem = document.querySelector('a-scene').systems.material;
```
### Accessing Systems
```javascript
AFRAME.registerComponent('my-component', {
init: function() {
const geometrySystem = this.el.sceneEl.systems.geometry;
const materialSystem = this.el.sceneEl.systems.material;
}
});
```
---
## Component API
Register custom components to extend A-Frame.
### Basic Component
```javascript
AFRAME.registerComponent('my-component', {
// Component schema (configuration)
schema: {
color: {type: 'color', default: '#FFF'},
size: {type: 'number', default: 1},
enabled: {type: 'boolean', default: true}
},
// Initialize (called once)
init: function() {
console.log('Component initialized');
// this.el = entity element
// this.data = component data
// this.el.sceneEl = scene element
},
// Update (called when properties change)
update: function(oldData) {
console.log('Component updated');
// this.data = new data
// oldData = previous data
},
// Remove (called when component removed)
remove: function() {
console.log('Component removed');
},
// Tick (called every frame)
tick: function(time, timeDelta) {
// time = total elapsed time (ms)
// timeDelta = time since last tick (ms)
},
// Pause (called when entity/scene pauses)
pause: function() {
console.log('Component paused');
},
// Play (called when entity/scene plays)
play: function() {
console.log('Component playing');
}
});
```
### Schema Types
```javascript
schema: {
// Basic types
boolean: {type: 'boolean', default: false},
number: {type: 'number', default: 0},
string: {type: 'string', default: ''},
// Color
color: {type: 'color', default: '#FFF'},
// Vectors
vec2: {type: 'vec2', default: {x: 0, y: 0}},
vec3: {type: 'vec3', default: {x: 0, y: 0, z: 0}},
vec4: {type: 'vec4', default: {x: 0, y: 0, z: 0, w: 1}},
// Selectors
selector: {type: 'selector'}, // CSS selector
selectorAll: {type: 'selectorAll'}, // Multiple elements
// Assets
audio: {type: 'audio'},
map: {type: 'map'},
model: {type: 'model'},
// Arrays
array: {type: 'array', default: []},
// Objects
object: {type: 'object', default: {}}
}
```
### Multi-Property Components
```javascript
AFRAME.registerComponent('light', {
schema: {
type: {default: 'directional', oneOf: ['ambient', 'directional', 'point', 'spot']},
color: {type: 'color', default: '#FFF'},
intensity: {type: 'number', default: 1}
},
init: function() {
// Access individual properties
console.log(this.data.type);
console.log(this.data.color);
console.log(this.data.intensity);
}
});
```
Usage:
```html
<a-entity light="type: point; color: #F00; intensity: 2"></a-entity>
```
### Single-Property Components
```javascript
AFRAME.registerComponent('visible', {
schema: {type: 'boolean', default: true},
update: function() {
// this.data is the boolean value directly
this.el.object3D.visible = this.data;
}
});
```
Usage:
```html
<a-entity visible="false"></a-entity>
```
---
## JavaScript API
### Creating Entities
```javascript
const scene = document.querySelector('a-scene');
// Create entity
const entity = document.createElement('a-entity');
// Set attributes
entity.setAttribute('geometry', {primitive: 'box', width: 2});
entity.setAttribute('material', {color: 'red'});
entity.setAttribute('position', {x: 0, y: 1, z: -3});
// Append to scene
scene.appendChild(entity);
```
### Removing Entities
```javascript
const entity = document.querySelector('#myEntity');
entity.parentNode.removeChild(entity);
```
### Event Handling
```javascript
const box = document.querySelector('a-box');
// Listen to events
box.addEventListener('click', (evt) => {
console.log('Clicked at:', evt.detail.intersection.point);
});
box.addEventListener('mouseenter', () => {
box.setAttribute('color', 'yellow');
});
box.addEventListener('mouseleave', () => {
box.setAttribute('color', 'blue');
});
// Emit custom events
box.emit('hit', {damage: 10});
box.addEventListener('hit', (evt) => {
console.log('Damage:', evt.detail.damage);
});
```
### Accessing Components
```javascript
const entity = document.querySelector('#myEntity');
// Get component instance
const material = entity.components.material;
const geometry = entity.components.geometry;
// Access component data
console.log(material.data.color);
// Call component methods
material.update();
```
### Three.js Integration
```javascript
const entity = document.querySelector('a-box');
// Access Three.js Object3D
const object3D = entity.object3D;
// Manipulate directly
object3D.position.set(1, 2, 3);
object3D.rotation.y = Math.PI / 4;
object3D.scale.set(2, 2, 2);
// Access Three.js mesh
const mesh = entity.getObject3D('mesh');
console.log(mesh.geometry);
console.log(mesh.material);
// Add custom Three.js objects
const scene = document.querySelector('a-scene').object3D;
const customMesh = new THREE.Mesh(geometry, material);
scene.add(customMesh);
```
### Wait for Scene Load
```javascript
const scene = document.querySelector('a-scene');
if (scene.hasLoaded) {
run();
} else {
scene.addEventListener('loaded', run);
}
function run() {
console.log('Scene is ready');
}
```
### Animation Control
```javascript
const entity = document.querySelector('#animated');
// Start animation
entity.emit('startAnimation');
// Pause animation
entity.components.animation.pauseAnimation();
// Resume animation
entity.components.animation.resumeAnimation();
```
### States
```javascript
const enemy = document.querySelector('#enemy');
// Add state
enemy.addState('attacking');
enemy.addState('angry');
// Check state
if (enemy.is('attacking')) {
console.log('Enemy is attacking');
}
// Remove state
enemy.removeState('attacking');
// Listen to state changes
enemy.addEventListener('stateadded', (evt) => {
console.log('State added:', evt.detail);
});
enemy.addEventListener('stateremoved', (evt) => {
console.log('State removed:', evt.detail);
});
```
---
## Constants
### Key Codes
Use with keyboard events:
```javascript
document.addEventListener('keydown', (evt) => {
if (evt.key === 'w' || evt.key === 'W') {
console.log('W pressed');
}
});
```
Common keys:
- `'w'`, `'a'`, `'s'`, `'d'` - Movement
- `' '` - Spacebar
- `'Escape'` - Escape
- `'Enter'` - Enter
- `'ArrowUp'`, `'ArrowDown'`, `'ArrowLeft'`, `'ArrowRight'` - Arrow keys
### Side Constants
```javascript
// Material side
'front' // THREE.FrontSide
'back' // THREE.BackSide
'double' // THREE.DoubleSide
```
### Easing Functions
See Animation component for full list of easing functions.
---
## Examples
### Complete Scene
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
</head>
<body>
<a-scene>
<a-assets>
<img id="ground-texture" src="ground.jpg">
<img id="sky-texture" src="sky.jpg">
<a-asset-item id="tree" src="tree.gltf"></a-asset-item>
</a-assets>
<!-- Environment -->
<a-sky src="#sky-texture"></a-sky>
<a-plane src="#ground-texture" rotation="-90 0 0" width="100" height="100"></a-plane>
<!-- Lighting -->
<a-entity light="type: ambient; color: #888; intensity: 0.5"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="2 4 2"></a-entity>
<!-- Objects -->
<a-box position="-1 0.5 -3" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<a-gltf-model src="#tree" position="3 0 -5" scale="0.5 0.5 0.5"></a-gltf-model>
<!-- Camera -->
<a-camera position="0 1.6 0">
<a-cursor></a-cursor>
</a-camera>
</a-scene>
</body>
</html>
```
This API reference covers the core A-Frame components and patterns for building VR/AR experiences.
references/components_library.md
# A-Frame Community Components Library
Curated collection of popular A-Frame community components for extending functionality.
## Table of Contents
- [Installation Methods](#installation-methods)
- [Environment & Effects](#environment--effects)
- [Physics](#physics)
- [Locomotion](#locomotion)
- [Models & Loaders](#models--loaders)
- [User Interface](#user-interface)
- [Particles](#particles)
- [Audio & Video](#audio--video)
- [Input & Interaction](#input--interaction)
- [Networking](#networking)
- [Utilities](#utilities)
---
## Installation Methods
### CDN (Recommended)
```html
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.0/dist/aframe-extras.min.js"></script>
```
### npm
```bash
npm install aframe-extras
```
```javascript
import 'aframe-extras';
```
### Local Download
Download component `.js` file and include in HTML:
```html
<script src="path/to/component.js"></script>
```
---
## Environment & Effects
### aframe-environment-component
Generate procedural 3D environments with presets.
**GitHub**: https://github.com/supermedium/aframe-environment-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-environment-component@1.3.3/dist/aframe-environment-component.min.js"></script>
```
**Usage**:
```html
<!-- Preset environment -->
<a-entity environment="preset: forest"></a-entity>
<!-- Custom environment -->
<a-entity environment="
preset: default;
seed: 42;
skyType: gradient;
skyColor: #4A90E2;
horizonColor: #87CEEB;
lighting: distant;
lightPosition: 1 1 -2;
fog: 0.8;
ground: hills;
groundColor: #5A7F32;
groundColor2: #3D5E1F;
dressing: trees;
dressingAmount: 50;
dressingColor: #228B22;
dressingScale: 5;
grid: none
"></a-entity>
```
**Presets**:
- `default`, `contact`, `egypt`, `checkerboard`, `forest`, `goaland`, `yavapai`, `goldmine`, `threetowers`, `poison`, `arches`, `tron`, `japan`, `dream`, `volcano`, `starry`, `osiris`
**Properties**:
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `preset` | string | default | Environment preset |
| `seed` | number | 1 | Random seed |
| `skyType` | string | gradient | Sky type (color, gradient, atmosphere) |
| `lighting` | string | distant | Lighting type (none, distant, point) |
| `ground` | string | flat | Ground type (none, flat, hills, canyon, spikes, noise) |
| `dressing` | string | none | Objects on ground (none, cubes, pyramids, cylinders, towers, mushrooms, trees, apparatus, torii) |
| `dressingAmount` | number | 10 | Number of dressing objects |
### aframe-particle-system-component
GPU particle systems for effects.
**GitHub**: https://github.com/IdeaSpaceVR/aframe-particle-system-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/gh/IdeaSpaceVR/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js"></script>
```
**Usage**:
```html
<!-- Preset particles -->
<a-entity particle-system="preset: default"></a-entity>
<a-entity particle-system="preset: dust"></a-entity>
<a-entity particle-system="preset: snow"></a-entity>
<a-entity particle-system="preset: rain"></a-entity>
<!-- Custom particles -->
<a-entity particle-system="
preset: default;
particleCount: 2000;
color: #FF0000, #FFFF00;
size: 0.5, 1;
velocity: 0 10 0;
velocitySpread: 1 5 1;
accelerationValue: 0 -10 0;
maxAge: 2;
blending: additive;
texture: https://cdn.aframe.io/examples/particle/images/star.png
"></a-entity>
```
**Presets**: `default`, `dust`, `snow`, `rain`
### aframe-effects
Post-processing effects (bloom, film grain, etc.).
**GitHub**: https://github.com/wizgrav/aframe-effects
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-effects@2.0.3/dist/aframe-effects.min.js"></script>
```
**Usage**:
```html
<a-scene effects="
bloom: 1.5;
fxaa: true;
filmgrain: 0.35
">
<!-- Scene content -->
</a-scene>
```
**Effects**:
- `bloom`: Bloom intensity (0-5)
- `fxaa`: Anti-aliasing (boolean)
- `filmgrain`: Film grain amount (0-1)
- `godrays`: God rays intensity (0-1)
---
## Physics
### aframe-physics-system (Ammo.js)
Physics simulation using Ammo.js (Bullet physics).
**GitHub**: https://github.com/c-frame/aframe-physics-system
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-physics-system@4.2.2/dist/aframe-physics-system.min.js"></script>
```
**Usage**:
```html
<a-scene physics="debug: false; gravity: -9.8">
<!-- Static ground -->
<a-plane
static-body
position="0 0 0"
rotation="-90 0 0"
width="10"
height="10">
</a-plane>
<!-- Dynamic box -->
<a-box
dynamic-body
position="0 5 0"
width="1"
height="1"
depth="1">
</a-box>
<!-- Kinematic sphere (non-reactive but affects others) -->
<a-sphere
kinematic-body
position="2 3 0"
radius="0.5">
</a-sphere>
</a-scene>
```
**Components**:
- `static-body`: Immovable objects (walls, ground)
- `dynamic-body`: Movable objects affected by forces
- `kinematic-body`: Movable by code, affects dynamic bodies
**Properties**:
```html
<a-box dynamic-body="
mass: 5;
linearDamping: 0.01;
angularDamping: 0.01;
shape: box;
sphereRadius: 1
"></a-box>
```
### aframe-physics-extras
Physics helpers and constraints.
**GitHub**: https://github.com/c-frame/aframe-physics-system
**Usage**:
```html
<!-- Constraint between two bodies -->
<a-entity
constraint="
target: #bodyA;
type: lock;
collideConnected: false
">
</a-entity>
<!-- Spring -->
<a-entity
spring="
target: #box;
restLength: 2;
stiffness: 50;
damping: 1
">
</a-entity>
```
---
## Locomotion
### aframe-extras (Movement Components)
Includes movement, controls, and model utilities.
**GitHub**: https://github.com/c-frame/aframe-extras
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.0/dist/aframe-extras.min.js"></script>
```
**Components Included**:
**movement-controls** (FPS-style movement):
```html
<a-entity movement-controls="
speed: 0.3;
fly: false;
constrainToNavMesh: true;
camera: #camera
" position="0 0 0">
<a-entity id="camera" camera position="0 1.6 0"></a-entity>
</a-entity>
```
**checkpoint-controls** (Teleport between waypoints):
```html
<!-- Player -->
<a-entity checkpoint-controls="mode: teleport"></a-entity>
<!-- Waypoints -->
<a-cylinder checkpoint position="0 0 -5" radius="0.5" height="0.1"></a-cylinder>
<a-cylinder checkpoint position="5 0 -5" radius="0.5" height="0.1"></a-cylinder>
```
**Animation Mixer** (GLTF animations):
```html
<a-entity
gltf-model="#character"
animation-mixer="clip: walk; loop: repeat">
</a-entity>
```
### aframe-blink-controls
Teleportation locomotion for VR.
**GitHub**: https://github.com/jure/aframe-blink-controls
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>
```
**Usage**:
```html
<a-entity
hand-controls="hand: left"
blink-controls="
cameraRig: #rig;
teleportOrigin: #camera;
collisionEntities: .ground
">
</a-entity>
<a-entity id="rig" position="0 0 0">
<a-entity id="camera" camera position="0 1.6 0"></a-entity>
</a-entity>
<a-plane class="ground" rotation="-90 0 0" width="20" height="20"></a-plane>
```
---
## Models & Loaders
### gltf-model (Built-in)
Load GLTF/GLB 3D models.
```html
<a-assets>
<a-asset-item id="tree" src="tree.gltf"></a-asset-item>
</a-assets>
<a-entity gltf-model="#tree" position="0 0 -5"></a-entity>
```
### obj-model (Built-in)
Load OBJ + MTL models.
```html
<a-assets>
<a-asset-item id="tree-obj" src="tree.obj"></a-asset-item>
<a-asset-item id="tree-mtl" src="tree.mtl"></a-asset-item>
</a-assets>
<a-entity obj-model="obj: #tree-obj; mtl: #tree-mtl"></a-entity>
```
### aframe-extras (Model Extensions)
Included in aframe-extras:
**animation-mixer**: Play GLTF animations
```html
<a-entity
gltf-model="#character"
animation-mixer="clip: walk; loop: repeat; clampWhenFinished: true">
</a-entity>
```
### aframe-simple-sun-sky
Realistic sky with sun position.
**GitHub**: https://github.com/c-frame/aframe-simple-sun-sky
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-simple-sun-sky@^1.2.2/simple-sun-sky.js"></script>
```
**Usage**:
```html
<a-simple-sun-sky sun-position="1 0.4 0"></a-simple-sun-sky>
<!-- Or with parameters -->
<a-simple-sun-sky
sun-position="1 1 -1"
rayleigh="1"
turbidity="10"
luminance="1"
mie-coefficient="0.005"
mie-directional-g="0.8">
</a-simple-sun-sky>
```
---
## User Interface
### aframe-html-shader
Display HTML content on meshes.
**GitHub**: https://github.com/mayognaise/aframe-html-shader
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-html-shader@0.2.0/dist/aframe-html-shader.min.js"></script>
```
**Usage**:
```html
<a-entity geometry="primitive: plane; width: 2; height: 1"
material="shader: html; target: #html-content; ratio: width"
position="0 1.5 -3">
</a-entity>
<div id="html-content" style="width: 400px; height: 200px; background: white;">
<h1>HTML Content</h1>
<p>This is rendered on a 3D surface!</p>
</div>
```
### aframe-gui
VR GUI components (buttons, sliders, panels).
**GitHub**: https://github.com/rdub80/aframe-gui
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-gui/dist/aframe-gui.min.js"></script>
```
**Usage**:
```html
<!-- Button -->
<a-gui-button
width="2.5"
height="0.75"
value="Click Me"
onclick="alert('Clicked!')"
position="0 1.5 -3">
</a-gui-button>
<!-- Slider -->
<a-gui-slider
width="2.5"
height="0.75"
percent="0.5"
position="0 2.5 -3">
</a-gui-slider>
<!-- Toggle -->
<a-gui-toggle
width="2.5"
height="0.75"
value="Sound: On"
position="0 3.5 -3">
</a-gui-toggle>
```
### aframe-troika-text
High-quality text rendering.
**GitHub**: https://github.com/lojjic/aframe-troika-text
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/troika-three-text@0.46.4/dist/troika-three-text.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-troika-text@1.0.0/dist/aframe-troika-text.min.js"></script>
```
**Usage**:
```html
<a-entity troika-text="
value: High Quality Text;
align: center;
anchor: center;
baseline: center;
color: #FFF;
fontSize: 0.2;
maxWidth: 3;
outlineWidth: 0.01;
outlineColor: #000
" position="0 2 -3">
</a-entity>
```
---
## Particles
### aframe-spe-particles
Shader Particle Engine for advanced particle effects.
**GitHub**: https://github.com/harlyq/aframe-spe-particles-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-spe-particles-component/dist/aframe-spe-particles-component.min.js"></script>
```
**Usage**:
```html
<!-- Fire effect -->
<a-entity spe-particles="
texture: https://cdn.rawgit.com/IdeaSpaceVR/aframe-particle-system-component/master/dist/images/star.png;
color: #ff0000, #ffff00;
particleCount: 1000;
maxAge: 1;
velocity: 0 4 0;
velocitySpread: 2 0 2;
acceleration: 0 -1 0;
size: 1, 0;
opacity: 1, 0;
blending: additive
" position="0 0 -5">
</a-entity>
```
---
## Audio & Video
### aframe-stereo-component
Stereo/spatial audio controls.
**GitHub**: https://github.com/oscarmarinmiro/aframe-stereo-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-stereo-component/dist/aframe-stereo-component.min.js"></script>
```
**Usage**:
```html
<a-assets>
<audio id="ambience" src="forest.mp3" stereo></audio>
</a-assets>
<a-entity sound="src: #ambience; autoplay: true; loop: true" position="0 0 0"></a-entity>
```
### aframe-video-controls
Video playback controls for 360° and flat videos.
**Usage**:
```html
<a-assets>
<video id="video360" src="360video.mp4" preload="auto"></video>
</a-assets>
<a-videosphere src="#video360"></a-videosphere>
<!-- Or flat video -->
<a-video src="#video360" width="4" height="2.25" position="0 2 -5"></a-video>
```
---
## Input & Interaction
### aframe-event-set-component (Built-in)
Set component properties on events.
**Usage**:
```html
<a-box
event-set__mouseenter="scale: 1.2 1.2 1.2; material.color: yellow"
event-set__mouseleave="scale: 1 1 1; material.color: blue"
event-set__click="rotation: 0 360 0">
</a-box>
```
### aframe-super-hands-component
Advanced hand interaction (grab, stretch, hover).
**GitHub**: https://github.com/c-frame/aframe-super-hands-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/super-hands/dist/super-hands.min.js"></script>
```
**Usage**:
```html
<!-- Hands with super-hands -->
<a-entity
hand-controls="hand: left"
super-hands>
</a-entity>
<!-- Interactive object -->
<a-box
hoverable
grabbable
stretchable
draggable
position="0 1.5 -3">
</a-box>
```
### aframe-input-mapping-component
Map VR controller buttons to actions.
**GitHub**: https://github.com/c-frame/aframe-input-mapping-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-input-mapping-component/dist/aframe-input-mapping-component.min.js"></script>
```
**Usage**:
```html
<a-entity
hand-controls="hand: right"
input-mapping="
keyboard: wasd;
controller: sixdof;
mapping: {
'triggerdown': 'shoot',
'gripdown': 'grab',
'abuttondown': 'jump'
}
">
</a-entity>
```
---
## Networking
### networked-aframe (NAF)
Multiplayer WebRTC networking.
**GitHub**: https://github.com/networked-aframe/networked-aframe
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/networked-aframe@^0.11.0/dist/networked-aframe.min.js"></script>
```
**Usage**:
```html
<a-scene networked-scene="
room: myRoom;
adapter: wseasyrtc;
audio: true
">
<!-- Networked entity (synced across clients) -->
<a-entity
networked="template: #avatar-template; attachTemplateToLocal: false"
position="0 0 0">
</a-entity>
</a-scene>
<script>
// Template for networked entities
NAF.schemas.add({
template: '#avatar-template',
components: [
'position',
'rotation'
]
});
</script>
```
---
## Utilities
### aframe-look-at-component (Built-in)
Make entity face another entity or position.
**Usage**:
```html
<!-- Look at camera -->
<a-text value="Look at me!" look-at="#camera"></a-text>
<!-- Look at position -->
<a-box look-at="0 0 0"></a-box>
<!-- Look at position vector -->
<a-sphere look-at="[camera]"></a-sphere>
```
### aframe-orbit-controls
Orbit camera around scene.
**GitHub**: https://github.com/tizzle/aframe-orbit-controls-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-orbit-controls@1.3.2/dist/aframe-orbit-controls.min.js"></script>
```
**Usage**:
```html
<a-camera orbit-controls="
target: 0 1.5 -3;
minDistance: 2;
maxDistance: 100;
initialPosition: 0 2 5
">
</a-camera>
```
### aframe-alongpath-component
Animate entities along a path.
**GitHub**: https://github.com/protyze/aframe-alongpath-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-alongpath-component/dist/aframe-alongpath-component.min.js"></script>
```
**Usage**:
```html
<a-curve id="track">
<a-curve-point position="0 0 0"></a-curve-point>
<a-curve-point position="5 5 0"></a-curve-point>
<a-curve-point position="10 0 0"></a-curve-point>
</a-curve>
<a-entity alongpath="
path: #track;
dur: 10000;
loop: true
">
<a-box></a-box>
</a-entity>
```
### aframe-click-drag-component
Drag entities with mouse/gaze.
**GitHub**: https://github.com/jesstelford/aframe-click-drag-component
**Installation**:
```html
<script src="https://cdn.jsdelivr.net/npm/aframe-click-drag-component/dist/aframe-click-drag-component.min.js"></script>
```
**Usage**:
```html
<a-camera>
<a-cursor click-drag></a-cursor>
</a-camera>
<a-box click-drag position="0 1 -3"></a-box>
```
### aframe-teleport-controls (Built-in for Quest)
Teleportation for VR.
**Usage**:
```html
<a-entity
hand-controls="hand: left"
teleport-controls="
cameraRig: #rig;
teleportOrigin: #camera;
type: parabolic;
collisionEntities: [mixin='navmesh']
">
</a-entity>
```
---
## Component Registry
Browse thousands of community components:
**A-Frame Registry**: https://aframe.io/registry/
Search components by category:
- Animation
- Audio
- Camera
- Controls
- Cursor
- Effects
- Geometry
- Layout
- Lighting
- Material
- Model
- Physics
- Shaders
- UI
- Utilities
---
## Creating Custom Components
Template for creating reusable components:
```javascript
AFRAME.registerComponent('my-custom-component', {
schema: {
speed: {type: 'number', default: 1},
enabled: {type: 'boolean', default: true}
},
init: function() {
// Setup
},
update: function(oldData) {
// When properties change
},
tick: function(time, timeDelta) {
// Every frame
},
remove: function() {
// Cleanup
}
});
```
**Share your component**:
1. Publish to npm
2. Submit to A-Frame Registry
3. Add GitHub topic: `aframe-component`
---
This components library provides a solid foundation for extending A-Frame with community-created functionality.
references/webxr_guide.md
# A-Frame WebXR Integration Guide
Complete guide to building VR and AR experiences with A-Frame and the WebXR API.
## Table of Contents
- [WebXR Overview](#webxr-overview)
- [VR Mode Configuration](#vr-mode-configuration)
- [AR Mode Configuration](#ar-mode-configuration)
- [Controller Systems](#controller-systems)
- [Hand Tracking](#hand-tracking)
- [AR Hit Testing](#ar-hit-testing)
- [Platform Support](#platform-support)
- [Performance Optimization](#performance-optimization)
- [Testing and Debugging](#testing-and-debugging)
---
## WebXR Overview
WebXR is the web standard for VR and AR experiences. A-Frame provides high-level abstractions over the WebXR API.
### Basic WebXR Scene
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
</head>
<body>
<a-scene webxr="requiredFeatures: local-floor">
<!-- VR content -->
<a-box position="0 1.5 -3" color="#4CC3D9"></a-box>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>
```
### WebXR Component Properties
```html
<a-scene webxr="
requiredFeatures: local-floor, hand-tracking;
optionalFeatures: hit-test, dom-overlay, unbounded;
referenceSpaceType: local-floor;
overlayElement: #overlay
"></a-scene>
```
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `requiredFeatures` | array | [] | Features that must be available |
| `optionalFeatures` | array | [] | Features to enable if available |
| `referenceSpaceType` | string | local-floor | XR reference space |
| `overlayElement` | selector | - | DOM overlay element (AR) |
### Reference Space Types
- `viewer` - Relative to initial viewer position
- `local` - Origin at starting position, sitting/standing
- `local-floor` - Floor level at Y=0 (recommended for VR)
- `bounded-floor` - Room-scale with boundaries
- `unbounded` - Large spaces, outdoor AR
---
## VR Mode Configuration
### Enable/Disable VR Mode UI
```html
<!-- Show VR button (default) -->
<a-scene vr-mode-ui="enabled: true"></a-scene>
<!-- Hide VR button -->
<a-scene vr-mode-ui="enabled: false"></a-scene>
<!-- Custom enter VR button -->
<a-scene vr-mode-ui="enterVRButton: #myEnterVRButton"></a-scene>
<button id="myEnterVRButton">Enter VR</button>
```
### VR Camera Rig Setup
```html
<a-scene>
<!-- VR camera rig -->
<a-entity id="rig" position="0 0 0">
<!-- Camera for head tracking -->
<a-camera position="0 1.6 0" look-controls></a-camera>
<!-- Left controller -->
<a-entity
id="leftHand"
hand-controls="hand: left"
laser-controls="hand: left">
</a-entity>
<!-- Right controller -->
<a-entity
id="rightHand"
hand-controls="hand: right"
laser-controls="hand: right">
</a-entity>
</a-entity>
<!-- VR content -->
<a-box position="0 1.5 -3" class="interactive"></a-box>
<a-plane rotation="-90 0 0" width="10" height="10" color="#7BC8A4"></a-plane>
</a-scene>
```
### VR Session Events
```javascript
const scene = document.querySelector('a-scene');
// Entering VR
scene.addEventListener('enter-vr', () => {
console.log('Entered VR mode');
// Check if actually in VR or AR
if (scene.is('vr-mode')) {
console.log('VR mode active');
}
if (scene.is('ar-mode')) {
console.log('AR mode active');
}
});
// Exiting VR
scene.addEventListener('exit-vr', () => {
console.log('Exited VR mode');
});
```
### Programmatic VR Entry/Exit
```javascript
const scene = document.querySelector('a-scene');
// Enter VR
scene.enterVR();
// Exit VR
scene.exitVR();
// Check if VR is available
if (scene.checkHeadsetConnected()) {
console.log('VR headset connected');
}
```
### VR-Specific Optimizations
```html
<a-scene
renderer="
antialias: false;
colorManagement: true;
sortObjects: false;
physicallyCorrectLights: true;
maxCanvasWidth: 1920;
maxCanvasHeight: 1920
"
vr-mode-ui="enabled: true">
<!-- Lower poly models for VR -->
<a-entity gltf-model="#low-poly-model"></a-entity>
<!-- Limit lights (expensive in VR) -->
<a-entity light="type: ambient; intensity: 0.6"></a-entity>
<a-entity light="type: directional; intensity: 0.4" position="1 2 1"></a-entity>
</a-scene>
```
---
## AR Mode Configuration
### Basic AR Scene Setup
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
</head>
<body>
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #model">
<a-assets>
<a-asset-item id="chair" src="chair.gltf"></a-asset-item>
</a-assets>
<!-- AR object to place -->
<a-entity id="model" gltf-model="#chair" scale="0.5 0.5 0.5"></a-entity>
<!-- AR UI overlay -->
<div id="overlay" style="
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.7);
color: white;
padding: 15px;
border-radius: 8px;
font-family: sans-serif;
">
<p id="instructions">Tap to enter AR mode</p>
</div>
</a-scene>
</body>
</html>
```
### AR Hit Test Component
```html
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="
target: #reticle;
type: footprint;
src: #reticle-model;
enabled: true
">
<!-- Reticle for placement preview -->
<a-entity id="reticle" visible="false"></a-entity>
<!-- Object to place -->
<a-entity id="furniture" gltf-model="#chair" visible="false"></a-entity>
</a-scene>
```
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `target` | selector | - | Entity to place on surface |
| `enabled` | boolean | true | Enable hit testing |
| `src` | selector | - | Custom reticle model |
| `type` | string | footprint | Hit test type (footprint, map) |
### AR Hit Test Events
```javascript
const scene = document.querySelector('a-scene');
const instructions = document.getElementById('instructions');
scene.addEventListener('enter-vr', function() {
if (this.is('ar-mode')) {
instructions.textContent = '';
// Hit testing started (scanning environment)
this.addEventListener('ar-hit-test-start', function() {
instructions.textContent = 'Scanning environment, finding surfaces...';
}, { once: true });
// Surface detected
this.addEventListener('ar-hit-test-achieved', function() {
instructions.textContent = 'Tap to place object';
}, { once: true });
// Object placed
this.addEventListener('ar-hit-test-select', function() {
instructions.textContent = 'Object placed!';
setTimeout(() => instructions.textContent = '', 2000);
}, { once: true });
}
});
scene.addEventListener('exit-vr', function() {
instructions.textContent = 'Tap to enter AR mode';
});
```
### AR Lighting Estimation
```html
<a-scene
reflection="directionalLight: #light"
webxr="optionalFeatures: light-estimation">
<!-- Light will be controlled by AR environment -->
<a-entity
id="light"
light="type: directional; castShadow: true"
position="1 2 1">
</a-entity>
</a-scene>
```
### AR Real-World Meshing
```html
<a-scene
webxr="optionalFeatures: mesh-detection"
real-world-meshing="enabled: true">
<!-- Detected surfaces will be rendered -->
</a-scene>
```
---
## Controller Systems
### Generic Hand Controls
Works with all VR controllers (Meta Quest, Vive, Index, etc.).
```html
<a-entity id="leftHand"
hand-controls="hand: left; handModelStyle: lowPoly; color: #ffcccc">
</a-entity>
<a-entity id="rightHand"
hand-controls="hand: right; handModelStyle: highPoly; color: #ffcccc">
</a-entity>
```
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `hand` | string | left | Which hand (left, right) |
| `handModelStyle` | string | lowPoly | Model detail (lowPoly, highPoly, toon) |
| `color` | color | white | Hand color |
### Laser Controls
Add laser pointer to controllers for UI interaction.
```html
<a-entity
hand-controls="hand: right"
laser-controls="hand: right"
raycaster="objects: .interactive; far: 10">
</a-entity>
<!-- Interactive object -->
<a-box class="interactive" position="0 1.5 -3"></a-box>
```
### Controller Events
```javascript
const leftHand = document.querySelector('#leftHand');
const rightHand = document.querySelector('#rightHand');
// Trigger button
leftHand.addEventListener('triggerdown', (evt) => {
console.log('Left trigger pressed');
});
leftHand.addEventListener('triggerup', (evt) => {
console.log('Left trigger released');
});
// Grip button
rightHand.addEventListener('gripdown', (evt) => {
console.log('Right grip pressed');
});
rightHand.addEventListener('gripup', (evt) => {
console.log('Right grip released');
});
// Thumbstick/touchpad
rightHand.addEventListener('thumbstickmoved', (evt) => {
console.log('Thumbstick:', evt.detail.x, evt.detail.y);
});
rightHand.addEventListener('touchpadmoved', (evt) => {
console.log('Touchpad:', evt.detail.x, evt.detail.y);
});
// A/B/X/Y buttons
rightHand.addEventListener('abuttondown', () => {
console.log('A button pressed');
});
rightHand.addEventListener('bbuttondown', () => {
console.log('B button pressed');
});
leftHand.addEventListener('xbuttondown', () => {
console.log('X button pressed');
});
leftHand.addEventListener('ybuttondown', () => {
console.log('Y button pressed');
});
```
### Platform-Specific Controllers
**Meta Quest / Oculus Touch**
```html
<a-entity meta-touch-controls="hand: left; model: true"></a-entity>
<a-entity meta-touch-controls="hand: right; model: true"></a-entity>
```
**HTC Vive**
```html
<a-entity vive-controls="hand: left; buttonColor: #FF0000"></a-entity>
<a-entity vive-controls="hand: right; buttonColor: #0000FF"></a-entity>
```
**Valve Index**
```html
<a-entity valve-index-controls="hand: left"></a-entity>
<a-entity valve-index-controls="hand: right"></a-entity>
```
**Windows Mixed Reality**
```html
<a-entity windows-motion-controls="hand: left"></a-entity>
<a-entity windows-motion-controls="hand: right"></a-entity>
```
### Grabbable Objects Component
```javascript
AFRAME.registerComponent('grabbable', {
init: function() {
var el = this.el;
var grabbing = false;
var controller = null;
el.addEventListener('triggerdown', function(evt) {
if (!grabbing) {
grabbing = true;
controller = evt.detail.controller;
// Attach object to controller
controller.object3D.attach(el.object3D);
// Visual feedback
el.setAttribute('material', 'opacity', 0.7);
}
});
el.addEventListener('triggerup', function(evt) {
if (grabbing && controller === evt.detail.controller) {
grabbing = false;
// Detach from controller
var sceneEl = el.sceneEl.object3D;
sceneEl.attach(el.object3D);
// Reset visual feedback
el.setAttribute('material', 'opacity', 1);
controller = null;
}
});
}
});
```
```html
<!-- Apply to objects -->
<a-box class="grabbable" grabbable position="0 1.5 -3"></a-box>
<a-sphere class="grabbable" grabbable position="1 1.5 -3"></a-sphere>
```
---
## Hand Tracking
Native hand tracking without controllers (supported on Meta Quest 2/3/Pro).
### Enable Hand Tracking
```html
<a-scene webxr="requiredFeatures: hand-tracking">
<!-- Hand tracking entities -->
<a-entity id="leftHand"
hand-tracking-controls="hand: left"
hand-tracking-grab-controls="hand: left">
</a-entity>
<a-entity id="rightHand"
hand-tracking-controls="hand: right"
hand-tracking-grab-controls="hand: right">
</a-entity>
<!-- Grabbable objects -->
<a-box class="grabbable" position="0 1.5 -3"></a-box>
</a-scene>
```
### Hand Tracking Properties
```html
<a-entity hand-tracking-controls="
hand: left;
modelColor: #FF0000;
modelStyle: mesh
"></a-entity>
```
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `hand` | string | left | Which hand (left, right) |
| `modelColor` | color | white | Hand mesh color |
| `modelStyle` | string | mesh | Visualization (mesh, dots, none) |
### Hand Tracking Events
```javascript
const leftHand = document.querySelector('#leftHand');
// Pinch gesture (thumb + index finger)
leftHand.addEventListener('pinchstarted', (evt) => {
console.log('Pinch started');
});
leftHand.addEventListener('pinchended', (evt) => {
console.log('Pinch ended');
});
// Pinch with strength value
leftHand.addEventListener('pinchmoved', (evt) => {
console.log('Pinch strength:', evt.detail.strength); // 0-1
});
```
### Hand Tracking Grab Controls
```html
<a-scene>
<!-- Enable hand tracking grab -->
<a-entity
hand-tracking-controls="hand: right"
hand-tracking-grab-controls="hand: right">
</a-entity>
<!-- Grabbable object -->
<a-sphere
class="grabbable"
obb-collider="size: 0.2 0.2 0.2"
grab-options="
requireGrab: true;
maxGrabbers: 2
"
position="0 1.5 -3">
</a-sphere>
</a-scene>
```
### Visualize Hand Tracking Colliders (Debug)
```html
<a-scene obb-collider="showColliders: true">
<!-- Shows bounding boxes for debugging -->
</a-scene>
```
---
## AR Hit Testing
Place virtual objects on detected real-world surfaces.
### Basic AR Hit Test
```html
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="target: #model">
<a-assets>
<a-asset-item id="furniture" src="chair.gltf"></a-asset-item>
</a-assets>
<a-entity id="model" gltf-model="#furniture" scale="0.5 0.5 0.5"></a-entity>
</a-scene>
```
### Custom Reticle
```html
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="target: #model; src: #reticle">
<a-assets>
<a-asset-item id="reticle-model" src="reticle.gltf"></a-asset-item>
<a-asset-item id="furniture" src="chair.gltf"></a-asset-item>
</a-assets>
<!-- Custom reticle -->
<a-entity id="reticle" gltf-model="#reticle-model"></a-entity>
<!-- Object to place -->
<a-entity id="model" gltf-model="#furniture"></a-entity>
</a-scene>
```
### Multiple Object Placement
```javascript
const scene = document.querySelector('a-scene');
const furniture = document.querySelector('#furniture');
let placedObjects = [];
scene.addEventListener('ar-hit-test-select', function(evt) {
// Clone object for multiple placements
const clone = furniture.cloneNode(true);
clone.removeAttribute('id');
clone.setAttribute('visible', true);
// Position at hit point
const hitPoint = evt.detail.position;
clone.setAttribute('position', hitPoint);
scene.appendChild(clone);
placedObjects.push(clone);
console.log('Placed object at:', hitPoint);
});
// Clear all placed objects
function clearObjects() {
placedObjects.forEach(obj => obj.parentNode.removeChild(obj));
placedObjects = [];
}
```
### AR with DOM Overlay
```html
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #model">
<a-entity id="model" gltf-model="#furniture"></a-entity>
<!-- HTML UI overlay -->
<div id="overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
<div style="position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: white; padding: 15px; border-radius: 8px; pointer-events: auto;">
<p id="instructions">Tap to enter AR</p>
<button id="clearBtn" style="margin-top: 10px; padding: 10px 20px; pointer-events: auto;">Clear Objects</button>
</div>
</div>
</a-scene>
<script>
const clearBtn = document.getElementById('clearBtn');
clearBtn.addEventListener('click', clearObjects);
</script>
```
---
## Platform Support
### Meta Quest (Standalone)
Optimizations for Quest 2/3/Pro:
```html
<a-scene
renderer="antialias: false; physicallyCorrectLights: false"
vr-mode-ui="enabled: true">
<!-- Use low-poly models -->
<a-entity gltf-model="#low-poly-model"></a-entity>
<!-- Limit lights (1-2 max) -->
<a-entity light="type: ambient; intensity: 0.7"></a-entity>
<a-entity light="type: directional; intensity: 0.3" position="1 2 1"></a-entity>
<!-- Texture size limits -->
<a-entity material="src: #texture; repeat: 1 1"></a-entity>
</a-scene>
```
### Desktop VR (PC + Headset)
Higher quality settings for PC VR:
```html
<a-scene
renderer="antialias: true; colorManagement: true; physicallyCorrectLights: true"
fog="type: linear; color: #AAA; near: 10; far: 100">
<!-- High-poly models allowed -->
<a-entity gltf-model="#high-poly-model"></a-entity>
<!-- Multiple lights OK -->
<a-entity light="type: ambient; intensity: 0.5"></a-entity>
<a-entity light="type: directional; intensity: 0.8" position="2 4 2"></a-entity>
<a-entity light="type: point; intensity: 1.5; distance: 20" position="5 2 5"></a-entity>
</a-scene>
```
### Mobile AR (iOS/Android)
Optimizations for mobile AR:
```html
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay, light-estimation"
ar-hit-test="target: #model"
renderer="antialias: false; maxCanvasWidth: 1920; maxCanvasHeight: 1920">
<!-- Lightweight models for mobile -->
<a-entity gltf-model="#mobile-optimized-model"></a-entity>
<!-- Minimal lighting -->
<a-entity light="type: ambient; intensity: 0.8"></a-entity>
</a-scene>
```
### Feature Detection
```javascript
// Check WebXR support
if ('xr' in navigator) {
navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
if (supported) {
console.log('VR supported');
}
});
navigator.xr.isSessionSupported('immersive-ar').then((supported) => {
if (supported) {
console.log('AR supported');
}
});
}
// Check hand tracking support
const scene = document.querySelector('a-scene');
scene.addEventListener('loaded', () => {
if (scene.systems['hand-tracking-controls']) {
console.log('Hand tracking available');
}
});
```
---
## Performance Optimization
### Reduce Draw Calls
```javascript
// Use instancing for repeated objects
AFRAME.registerComponent('instanced-forest', {
init: function() {
const scene = this.el.sceneEl.object3D;
const geometry = new THREE.CylinderGeometry(0.2, 0.5, 3, 8);
const material = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, 100);
// Position instances
for (let i = 0; i < 100; i++) {
const matrix = new THREE.Matrix4();
matrix.setPosition(
Math.random() * 20 - 10,
0,
Math.random() * 20 - 10
);
instancedMesh.setMatrixAt(i, matrix);
}
scene.add(instancedMesh);
}
});
```
### Optimize Geometry
```html
<!-- Low poly count for VR/mobile -->
<a-sphere radius="1" segments-width="8" segments-height="6"></a-sphere>
<!-- High poly only for close-up objects -->
<a-sphere radius="1" segments-width="32" segments-height="32"></a-sphere>
```
### Texture Optimization
```javascript
// Compress textures
// Use power-of-2 sizes (256, 512, 1024, 2048)
// Use lower resolutions for mobile
// Lazy load textures
AFRAME.registerComponent('lazy-texture', {
schema: {
src: {type: 'string'}
},
init: function() {
const el = this.el;
const src = this.data.src;
// Load texture when entity is near camera
this.el.sceneEl.addEventListener('camera-move', () => {
const distance = el.object3D.position.distanceTo(
el.sceneEl.camera.position
);
if (distance < 10 && !el.getAttribute('material').src) {
el.setAttribute('material', 'src', src);
}
});
}
});
```
### Limit Physics
```html
<!-- Only enable physics for interactive objects -->
<a-entity
geometry="primitive: box"
ammo-body="type: dynamic; mass: 1"
ammo-shape="type: box">
</a-entity>
```
### Throttle Updates
```javascript
AFRAME.registerComponent('throttled-rotation', {
init: function() {
this.lastUpdate = 0;
this.updateInterval = 100; // Update every 100ms instead of every frame
},
tick: function(time, timeDelta) {
if (time - this.lastUpdate >= this.updateInterval) {
// Expensive operation
this.el.object3D.rotation.y += 0.01;
this.lastUpdate = time;
}
}
});
```
---
## Testing and Debugging
### Desktop Testing
```html
<!-- Test without VR headset using desktop mode -->
<a-scene vr-mode-ui="enabled: true">
<!-- WASD to move, mouse to look -->
<a-camera wasd-controls look-controls></a-camera>
</a-scene>
```
### Mobile Testing
```html
<!-- Test AR on mobile browser -->
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="target: #model">
<!-- Use browser DevTools device emulation -->
</a-scene>
```
### Stats and Debugging
```html
<!-- Show FPS and performance stats -->
<a-scene stats>
<!-- Stats panel appears in top-left -->
</a-scene>
<!-- Enable inspector (Ctrl+Alt+I) -->
<a-scene inspector>
<!-- Visual scene editor -->
</a-scene>
```
### Console Logging
```javascript
// Log XR session info
const scene = document.querySelector('a-scene');
scene.addEventListener('enter-vr', () => {
const renderer = scene.renderer;
const session = renderer.xr.getSession();
console.log('XR Session:', session);
console.log('Reference space:', scene.systems.webxr.sessionReferenceSpaceType);
console.log('Frame rate:', session.frameRate);
});
```
### Remote Debugging
**For Meta Quest:**
1. Enable Developer Mode in Quest settings
2. Connect via USB to computer
3. Use Chrome DevTools (chrome://inspect)
**For iOS:**
1. Enable Web Inspector in Safari settings
2. Connect iPhone/iPad to Mac
3. Use Safari Developer menu
### Performance Profiling
```javascript
// Monitor frame times
const scene = document.querySelector('a-scene');
let frameCount = 0;
let lastTime = performance.now();
scene.addEventListener('renderstart', () => {
const now = performance.now();
frameCount++;
if (now - lastTime >= 1000) {
console.log('FPS:', frameCount);
frameCount = 0;
lastTime = now;
}
});
```
---
## Common Issues
### Issue: VR button not appearing
**Solution**: Check HTTPS (required for WebXR)
### Issue: Controllers not tracking
**Solution**: Check permissions, ensure proper lighting
### Issue: AR not working on mobile
**Solution**: Use Chrome/Safari, check camera permissions
### Issue: Low FPS in VR
**Solution**: Reduce geometry, limit lights, optimize textures
### Issue: Hand tracking not working
**Solution**: Enable in headset settings, ensure good lighting
---
## Resources
- [WebXR Device API Specification](https://www.w3.org/TR/webxr/)
- [A-Frame WebXR Documentation](https://aframe.io/docs/1.7.0/introduction/webxr.html)
- [Meta Quest Development](https://developer.oculus.com/)
- [WebXR Samples](https://immersive-web.github.io/webxr-samples/)
- [Mozilla Mixed Reality Blog](https://mixedreality.mozilla.org/)
---
This guide covers all aspects of building WebXR experiences with A-Frame, from basic VR scenes to advanced AR features.
scripts/component_builder.py
#!/usr/bin/env python3
"""
A-Frame Component Builder
Generates custom A-Frame component boilerplate for various use cases:
- Basic components
- Interactive components (hover, click)
- Animation components
- Physics-based components
- Networked components
- VR controller components
Usage:
python component_builder.py basic my-component
python component_builder.py interactive clickable-box
python component_builder.py animation rotating-cube
python component_builder.py --interactive
"""
import argparse
import sys
from pathlib import Path
# Component templates
COMPONENT_TEMPLATES = {
'basic': {
'description': 'Basic component with schema and lifecycle',
'schema_props': ['enabled: boolean', 'speed: number'],
'has_tick': False
},
'interactive': {
'description': 'Component with mouse/VR interaction events',
'schema_props': ['hoverColor: color', 'clickColor: color'],
'has_tick': False
},
'animation': {
'description': 'Component with tick() for continuous animation',
'schema_props': ['speed: number', 'axis: string'],
'has_tick': True
},
'physics': {
'description': 'Component for physics interactions',
'schema_props': ['force: number', 'direction: vec3'],
'has_tick': True
},
'controller': {
'description': 'VR controller interaction component',
'schema_props': ['hand: string', 'button: string'],
'has_tick': False
},
'networked': {
'description': 'Networked component for multi-user scenes',
'schema_props': ['syncInterval: number', 'owner: string'],
'has_tick': True
},
'loader': {
'description': 'Asset loading component',
'schema_props': ['src: string', 'onLoad: string'],
'has_tick': False
}
}
def generate_basic_component(name):
"""Generate basic component template"""
component_name = name.replace('_', '-')
return f'''// {component_name} Component
// Basic A-Frame component with schema and lifecycle methods
AFRAME.registerComponent('{component_name}', {{
// Component schema (configuration properties)
schema: {{
enabled: {{ type: 'boolean', default: true }},
speed: {{ type: 'number', default: 1 }},
color: {{ type: 'color', default: '#FFF' }}
}},
// Initialize (called once when component is attached)
init: function() {{
console.log('{component_name} initialized');
// Store references
this.el; // The entity element
this.data; // Component data from schema
this.el.sceneEl; // The scene element
// Setup code here
}},
// Update (called when component properties change)
update: function(oldData) {{
// this.data contains new values
// oldData contains previous values
if (this.data.enabled !== oldData.enabled) {{
console.log('Enabled changed to:', this.data.enabled);
}}
if (this.data.speed !== oldData.speed) {{
console.log('Speed changed to:', this.data.speed);
}}
}},
// Remove (called when component is removed from entity)
remove: function() {{
// Cleanup code here
console.log('{component_name} removed');
}},
// Pause (called when entity/scene pauses)
pause: function() {{
console.log('{component_name} paused');
}},
// Play (called when entity/scene plays)
play: function() {{
console.log('{component_name} playing');
}}
}});
// Usage:
// <a-entity {component_name}="enabled: true; speed: 2; color: #FF0000"></a-entity>
'''
def generate_interactive_component(name):
"""Generate interactive component with events"""
component_name = name.replace('_', '-')
return f'''// {component_name} Component
// Interactive component with mouse and VR controller events
AFRAME.registerComponent('{component_name}', {{
schema: {{
hoverColor: {{ type: 'color', default: '#FFFF00' }},
clickColor: {{ type: 'color', default: '#FF0000' }},
enabled: {{ type: 'boolean', default: true }}
}},
init: function() {{
const el = this.el;
const data = this.data;
// Store original color
this.originalColor = el.getAttribute('material').color;
// Bind event handlers (important for proper 'this' context)
this.onMouseEnter = this.onMouseEnter.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.onClick = this.onClick.bind(this);
// Add event listeners
el.addEventListener('mouseenter', this.onMouseEnter);
el.addEventListener('mouseleave', this.onMouseLeave);
el.addEventListener('click', this.onClick);
// VR controller events
el.addEventListener('triggerdown', this.onClick);
}},
update: function(oldData) {{
// Update behavior based on enabled state
if (!this.data.enabled && oldData.enabled) {{
// Reset to original color when disabled
this.el.setAttribute('material', 'color', this.originalColor);
}}
}},
onMouseEnter: function(evt) {{
if (!this.data.enabled) return;
console.log('Mouse entered');
this.el.setAttribute('material', 'color', this.data.hoverColor);
this.el.setAttribute('scale', {{
x: 1.1,
y: 1.1,
z: 1.1
}});
}},
onMouseLeave: function(evt) {{
if (!this.data.enabled) return;
console.log('Mouse left');
this.el.setAttribute('material', 'color', this.originalColor);
this.el.setAttribute('scale', {{
x: 1,
y: 1,
z: 1
}});
}},
onClick: function(evt) {{
if (!this.data.enabled) return;
console.log('Clicked!', evt.detail);
this.el.setAttribute('material', 'color', this.data.clickColor);
// Emit custom event
this.el.emit('{component_name}-clicked', {{
position: evt.detail.intersection ? evt.detail.intersection.point : null
}});
// Reset color after 500ms
setTimeout(() => {{
this.el.setAttribute('material', 'color', this.originalColor);
}}, 500);
}},
remove: function() {{
// Remove event listeners
const el = this.el;
el.removeEventListener('mouseenter', this.onMouseEnter);
el.removeEventListener('mouseleave', this.onMouseLeave);
el.removeEventListener('click', this.onClick);
el.removeEventListener('triggerdown', this.onClick);
}}
}});
// Usage:
// <a-camera>
// <a-cursor raycaster="objects: .interactive"></a-cursor>
// </a-camera>
// <a-box class="interactive" {component_name}="hoverColor: yellow; clickColor: red"></a-box>
'''
def generate_animation_component(name):
"""Generate animation component with tick()"""
component_name = name.replace('_', '-')
return f'''// {component_name} Component
// Animation component using tick() for continuous updates
AFRAME.registerComponent('{component_name}', {{
schema: {{
speed: {{ type: 'number', default: 1 }},
axis: {{ type: 'string', default: 'y', oneOf: ['x', 'y', 'z'] }},
enabled: {{ type: 'boolean', default: true }}
}},
init: function() {{
// Store rotation state
this.rotation = {{
x: 0,
y: 0,
z: 0
}};
}},
// tick() is called every frame
tick: function(time, timeDelta) {{
if (!this.data.enabled) return;
// timeDelta is time since last frame (milliseconds)
// Use it for frame-rate independent animation
const rotationAmount = this.data.speed * (timeDelta / 1000);
// Update rotation based on axis
this.rotation[this.data.axis] += rotationAmount;
// Apply rotation to entity
this.el.object3D.rotation[this.data.axis] = this.rotation[this.data.axis];
}},
pause: function() {{
// Stop animation when paused
}},
play: function() {{
// Resume animation when playing
}}
}});
// Usage:
// <a-box {component_name}="speed: 2; axis: y"></a-box>
// <a-sphere {component_name}="speed: 0.5; axis: x; enabled: false"></a-sphere>
'''
def generate_physics_component(name):
"""Generate physics-based component"""
component_name = name.replace('_', '-')
return f'''// {component_name} Component
// Physics-based component (requires aframe-physics-system)
AFRAME.registerComponent('{component_name}', {{
schema: {{
force: {{ type: 'number', default: 10 }},
direction: {{ type: 'vec3', default: {{ x: 0, y: 1, z: 0 }} }},
trigger: {{ type: 'string', default: 'click' }}
}},
dependencies: ['dynamic-body'], // Requires physics body
init: function() {{
const el = this.el;
const data = this.data;
// Wait for physics body to be ready
el.addEventListener('body-loaded', () => {{
this.body = el.body; // Access Ammo.js physics body
}});
// Bind trigger event
this.applyForce = this.applyForce.bind(this);
if (data.trigger === 'click') {{
el.addEventListener('click', this.applyForce);
}} else if (data.trigger === 'mouseenter') {{
el.addEventListener('mouseenter', this.applyForce);
}}
}},
applyForce: function(evt) {{
if (!this.body) return;
const force = this.data.force;
const direction = this.data.direction;
// Apply impulse force
const impulse = new Ammo.btVector3(
direction.x * force,
direction.y * force,
direction.z * force
);
const position = new Ammo.btVector3(0, 0, 0);
this.body.applyImpulse(impulse, position);
console.log('Force applied:', force);
// Clean up Ammo.js objects
Ammo.destroy(impulse);
Ammo.destroy(position);
}},
tick: function(time, timeDelta) {{
// Optional: Add continuous forces or check physics state
}},
remove: function() {{
const el = this.el;
if (this.data.trigger === 'click') {{
el.removeEventListener('click', this.applyForce);
}} else if (this.data.trigger === 'mouseenter') {{
el.removeEventListener('mouseenter', this.applyForce);
}}
}}
}});
// Usage (requires aframe-physics-system):
// <a-scene physics>
// <a-sphere
// dynamic-body
// {component_name}="force: 20; direction: 0 1 0; trigger: click"
// position="0 2 -3">
// </a-sphere>
// </a-scene>
'''
def generate_controller_component(name):
"""Generate VR controller component"""
component_name = name.replace('_', '-')
return f'''// {component_name} Component
// VR controller interaction component
AFRAME.registerComponent('{component_name}', {{
schema: {{
hand: {{ type: 'string', default: 'right', oneOf: ['left', 'right'] }},
button: {{ type: 'string', default: 'trigger', oneOf: ['trigger', 'grip', 'thumbstick', 'abutton', 'bbutton'] }}
}},
init: function() {{
const el = this.el;
const data = this.data;
console.log(`Initializing controller component for ${{data.hand}} hand`);
// Bind event handlers
this.onButtonDown = this.onButtonDown.bind(this);
this.onButtonUp = this.onButtonUp.bind(this);
this.onButtonChanged = this.onButtonChanged.bind(this);
// Listen to button events
const buttonDownEvent = data.button + 'down';
const buttonUpEvent = data.button + 'up';
const buttonChangedEvent = data.button + 'changed';
el.addEventListener(buttonDownEvent, this.onButtonDown);
el.addEventListener(buttonUpEvent, this.onButtonUp);
el.addEventListener(buttonChangedEvent, this.onButtonChanged);
// Controller-specific events
el.addEventListener('controllerconnected', (evt) => {{
console.log('Controller connected:', evt.detail.name);
}});
}},
onButtonDown: function(evt) {{
console.log(`${{this.data.button}} pressed on ${{this.data.hand}} hand`);
// Example: Raycast from controller
const raycaster = this.el.components.raycaster;
if (raycaster) {{
const intersections = raycaster.intersections;
if (intersections.length > 0) {{
const target = intersections[0].object.el;
console.log('Hit:', target);
// Emit event on hit object
target.emit('controller-hit', {{
hand: this.data.hand,
button: this.data.button,
intersection: intersections[0]
}});
}}
}}
}},
onButtonUp: function(evt) {{
console.log(`${{this.data.button}} released on ${{this.data.hand}} hand`);
}},
onButtonChanged: function(evt) {{
// For analog buttons (triggers, thumbsticks)
const state = evt.detail.state;
console.log(`${{this.data.button}} state:`, state);
}},
remove: function() {{
const el = this.el;
const data = this.data;
const buttonDownEvent = data.button + 'down';
const buttonUpEvent = data.button + 'up';
const buttonChangedEvent = data.button + 'changed';
el.removeEventListener(buttonDownEvent, this.onButtonDown);
el.removeEventListener(buttonUpEvent, this.onButtonUp);
el.removeEventListener(buttonChangedEvent, this.onButtonChanged);
}}
}});
// Usage:
// <a-entity hand-controls="hand: right"
// laser-controls
// {component_name}="hand: right; button: trigger">
// </a-entity>
'''
def generate_networked_component(name):
"""Generate networked component"""
component_name = name.replace('_', '-')
return f'''// {component_name} Component
// Networked component for multi-user synchronization (requires networked-aframe)
AFRAME.registerComponent('{component_name}', {{
schema: {{
syncInterval: {{ type: 'number', default: 50 }}, // milliseconds
owner: {{ type: 'string', default: '' }}
}},
init: function() {{
const el = this.el;
// Check if this is the local player's entity
this.isLocal = NAF.utils.isMine(el);
console.log('Networked component init:', this.isLocal ? 'LOCAL' : 'REMOTE');
// Last sync time
this.lastSync = 0;
// Custom networked data
this.networkData = {{
customProperty: 'value'
}};
// Listen to networked events
el.addEventListener('connected', () => {{
console.log('Entity connected to network');
}});
el.addEventListener('disconnected', () => {{
console.log('Entity disconnected from network');
}});
// Listen to ownership changes
NAF.utils.getNetworkedEntity(el).then(networkedEl => {{
networkedEl.addEventListener('ownership-gained', () => {{
console.log('Gained ownership');
this.isLocal = true;
}});
networkedEl.addEventListener('ownership-lost', () => {{
console.log('Lost ownership');
this.isLocal = false;
}});
}});
}},
tick: function(time, timeDelta) {{
if (!this.isLocal) return;
// Sync at specified interval
if (time - this.lastSync >= this.data.syncInterval) {{
this.syncState();
this.lastSync = time;
}}
}},
syncState: function() {{
// Broadcast custom state to other clients
const el = this.el;
// Update networked components
NAF.utils.getNetworkedEntity(el).then(networkedEl => {{
// Sync custom data
networkedEl.emit('sync-data', this.networkData);
}});
}},
// Call this to take ownership of the entity
takeOwnership: function() {{
NAF.utils.takeOwnership(this.el);
}},
remove: function() {{
console.log('Networked component removed');
}}
}});
// Usage (requires networked-aframe):
// <a-scene networked-scene="room: myRoom; adapter: wseasyrtc">
// <a-entity networked="template: #avatar-template"
// {component_name}="syncInterval: 100">
// </a-entity>
// </a-scene>
'''
def generate_loader_component(name):
"""Generate asset loading component"""
component_name = name.replace('_', '-')
return f'''// {component_name} Component
// Asset loading component with progress and error handling
AFRAME.registerComponent('{component_name}', {{
schema: {{
src: {{ type: 'string', default: '' }},
onLoad: {{ type: 'string', default: '' }}, // Event name to emit
showProgress: {{ type: 'boolean', default: true }}
}},
init: function() {{
const el = this.el;
const data = this.data;
if (!data.src) {{
console.warn('{component_name}: No src specified');
return;
}}
this.loading = true;
this.loaded = false;
// Start loading
this.loadAsset(data.src);
}},
update: function(oldData) {{
// Reload if src changes
if (this.data.src !== oldData.src && this.data.src) {{
this.loadAsset(this.data.src);
}}
}},
loadAsset: function(src) {{
const el = this.el;
const data = this.data;
console.log('Loading asset:', src);
// Determine asset type from extension
const extension = src.split('.').pop().toLowerCase();
if (['gltf', 'glb'].includes(extension)) {{
this.loadModel(src);
}} else if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) {{
this.loadImage(src);
}} else {{
console.warn('Unsupported asset type:', extension);
}}
}},
loadModel: function(src) {{
const el = this.el;
// Use A-Frame's asset system
const loader = new THREE.GLTFLoader();
loader.load(
src,
// onLoad
(gltf) => {{
console.log('Model loaded:', src);
this.onAssetLoaded(gltf);
el.setObject3D('mesh', gltf.scene);
}},
// onProgress
(xhr) => {{
const percentComplete = (xhr.loaded / xhr.total * 100);
if (this.data.showProgress) {{
console.log(`Loading: ${{percentComplete.toFixed(2)}}%`);
}}
el.emit('loading-progress', {{ percent: percentComplete }});
}},
// onError
(error) => {{
console.error('Error loading model:', error);
this.onAssetError(error);
}}
);
}},
loadImage: function(src) {{
const el = this.el;
const textureLoader = new THREE.TextureLoader();
textureLoader.load(
src,
// onLoad
(texture) => {{
console.log('Image loaded:', src);
this.onAssetLoaded(texture);
el.setAttribute('material', 'src', texture);
}},
// onProgress
(xhr) => {{
const percentComplete = (xhr.loaded / xhr.total * 100);
if (this.data.showProgress) {{
console.log(`Loading: ${{percentComplete.toFixed(2)}}%`);
}}
el.emit('loading-progress', {{ percent: percentComplete }});
}},
// onError
(error) => {{
console.error('Error loading image:', error);
this.onAssetError(error);
}}
);
}},
onAssetLoaded: function(asset) {{
this.loading = false;
this.loaded = true;
// Emit custom event
if (this.data.onLoad) {{
this.el.emit(this.data.onLoad, {{ asset: asset }});
}}
// Emit standard event
this.el.emit('asset-loaded', {{ asset: asset }});
}},
onAssetError: function(error) {{
this.loading = false;
this.loaded = false;
this.el.emit('asset-error', {{ error: error }});
}}
}});
// Usage:
// <a-entity {component_name}="src: model.gltf; onLoad: model-ready; showProgress: true"></a-entity>
// <a-entity {component_name}="src: texture.jpg"></a-entity>
'''
def generate_component(component_type, name):
"""Generate component based on type"""
generators = {
'basic': generate_basic_component,
'interactive': generate_interactive_component,
'animation': generate_animation_component,
'physics': generate_physics_component,
'controller': generate_controller_component,
'networked': generate_networked_component,
'loader': generate_loader_component
}
return generators[component_type](name)
def interactive_mode():
"""Interactive component builder"""
print("\n=== A-Frame Component Builder (Interactive Mode) ===\n")
print("Available component types:")
for i, (comp_type, config) in enumerate(COMPONENT_TEMPLATES.items(), 1):
print(f" {i}. {comp_type:12} - {config['description']}")
while True:
try:
choice = input("\nSelect component type (1-7): ").strip()
component_type = list(COMPONENT_TEMPLATES.keys())[int(choice) - 1]
break
except (ValueError, IndexError):
print("Invalid choice. Please enter a number between 1 and 7.")
name = input("Enter component name (e.g., my-component): ").strip()
if not name:
print("Error: Component name is required")
return
output_file = input("Enter output filename (default: component.js): ").strip() or "component.js"
save_component(component_type, name, output_file)
def save_component(component_type, name, output_file):
"""Save generated component to file"""
component_code = generate_component(component_type, name)
output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
f.write(component_code)
print(f"\n✅ Generated {component_type} component: {output_path}")
print(f"📝 Component name: {name.replace('_', '-')}")
print(f"🎯 Template: {COMPONENT_TEMPLATES[component_type]['description']}")
print(f"\n🚀 To use:")
print(f" 1. Include in HTML: <script src=\"{output_file}\"></script>")
print(f" 2. Attach to entity: <a-entity {name.replace('_', '-')}></a-entity>")
def main():
parser = argparse.ArgumentParser(
description='Generate A-Frame custom component boilerplate',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
python component_builder.py basic my-component
python component_builder.py interactive clickable-box --output components/
python component_builder.py animation rotating-object
python component_builder.py --interactive
Component Types:
basic - Basic component with schema
interactive - Mouse/VR interaction events
animation - Continuous animation with tick()
physics - Physics-based interactions
controller - VR controller component
networked - Multi-user synchronization
loader - Asset loading with progress
'''
)
parser.add_argument(
'component_type',
nargs='?',
choices=list(COMPONENT_TEMPLATES.keys()),
help='Type of component to generate'
)
parser.add_argument(
'name',
nargs='?',
help='Name of the component (e.g., my-component)'
)
parser.add_argument(
'-o', '--output',
default='component.js',
help='Output filename (default: component.js)'
)
parser.add_argument(
'-i', '--interactive',
action='store_true',
help='Run in interactive mode'
)
parser.add_argument(
'-l', '--list',
action='store_true',
help='List available component types'
)
args = parser.parse_args()
if args.list:
print("\nAvailable Component Types:\n")
for comp_type, config in COMPONENT_TEMPLATES.items():
print(f" {comp_type:12} - {config['description']}")
print(f" Schema props: {', '.join(config['schema_props'])}")
print(f" Has tick(): {'Yes' if config['has_tick'] else 'No'}\n")
sys.exit(0)
if args.interactive or not args.component_type or not args.name:
interactive_mode()
else:
save_component(args.component_type, args.name, args.output)
if __name__ == '__main__':
main()
scripts/scene_generator.py
#!/usr/bin/env python3
"""
A-Frame Scene Generator
Generates A-Frame scene boilerplate for different use cases:
- Basic 3D scenes
- VR experiences with controllers
- AR experiences with hit testing
- 360° photo/video viewers
- Multi-user networked scenes
Usage:
python scene_generator.py basic MyScene
python scene_generator.py vr VRProject
python scene_generator.py ar ARFurniture
python scene_generator.py 360 Gallery360
python scene_generator.py --interactive
"""
import argparse
import sys
import os
from pathlib import Path
# Scene type configurations
SCENE_TYPES = {
'basic': {
'description': 'Basic 3D scene with primitives and camera',
'features': ['primitives', 'lighting', 'sky', 'camera'],
'filename': 'index.html'
},
'vr': {
'description': 'VR scene with hand controllers and interactions',
'features': ['vr-rig', 'controllers', 'teleportation', 'interactive-objects'],
'filename': 'index.html'
},
'ar': {
'description': 'AR scene with hit testing and object placement',
'features': ['ar-hit-test', 'dom-overlay', 'models'],
'filename': 'index.html'
},
'360': {
'description': '360° photo/video gallery viewer',
'features': ['360-sky', 'thumbnails', 'navigation'],
'filename': 'index.html'
},
'networked': {
'description': 'Multi-user networked scene with avatars',
'features': ['networked-aframe', 'avatars', 'voice-chat'],
'filename': 'index.html'
},
'physics': {
'description': 'Scene with physics simulation',
'features': ['physics', 'dynamic-objects', 'interactive'],
'filename': 'index.html'
},
'environment': {
'description': 'Procedural environment with effects',
'features': ['environment', 'particles', 'effects'],
'filename': 'index.html'
}
}
def generate_basic_scene(name):
"""Generate basic 3D scene"""
return f'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{name} - A-Frame Basic Scene</title>
<meta name="description" content="{name} - A-Frame VR">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<style>
body {{ margin: 0; }}
#info {{
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 15px;
border-radius: 8px;
font-family: sans-serif;
font-size: 14px;
}}
</style>
</head>
<body>
<a-scene>
<!-- Assets -->
<a-assets>
<!-- Preload assets here -->
</a-assets>
<!-- Environment -->
<a-sky color="#ECECEC"></a-sky>
<a-plane
rotation="-90 0 0"
width="20"
height="20"
color="#7BC8A4">
</a-plane>
<!-- Lighting -->
<a-entity light="type: ambient; color: #888; intensity: 0.5"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="2 4 2"></a-entity>
<!-- Objects -->
<a-box
position="-1 0.5 -3"
rotation="0 45 0"
color="#4CC3D9"
shadow>
</a-box>
<a-sphere
position="0 1.25 -5"
radius="1.25"
color="#EF2D5E"
shadow>
</a-sphere>
<a-cylinder
position="1 0.75 -3"
radius="0.5"
height="1.5"
color="#FFC65D"
shadow>
</a-cylinder>
<!-- Camera -->
<a-camera position="0 1.6 0" look-controls wasd-controls>
<a-cursor></a-cursor>
</a-camera>
</a-scene>
<div id="info">
<strong>{name}</strong><br>
WASD - Move | Mouse - Look
</div>
</body>
</html>'''
def generate_vr_scene(name):
"""Generate VR scene with controllers"""
return f'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{name} - A-Frame VR Experience</title>
<meta name="description" content="{name} - VR Experience">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>
<style>
body {{ margin: 0; }}
</style>
</head>
<body>
<a-scene>
<!-- Assets -->
<a-assets>
<audio id="click-sound" src="https://cdn.aframe.io/360-image-gallery-boilerplate/audio/click.ogg"></audio>
</a-assets>
<!-- Environment -->
<a-sky color="#87CEEB"></a-sky>
<a-plane
class="ground"
rotation="-90 0 0"
width="50"
height="50"
color="#3D5E1F"
shadow="receive: true">
</a-plane>
<!-- Lighting -->
<a-entity light="type: ambient; color: #BBB; intensity: 0.6"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.5; castShadow: true" position="5 10 5"></a-entity>
<!-- Interactive Objects -->
<a-box
class="interactive"
position="-1 0.5 -3"
color="#4CC3D9"
event-set__mouseenter="scale: 1.2 1.2 1.2"
event-set__mouseleave="scale: 1 1 1"
sound="on: click; src: #click-sound"
shadow="cast: true">
</a-box>
<a-sphere
class="interactive"
position="1 1.25 -3"
radius="0.5"
color="#EF2D5E"
event-set__click="color: orange; scale: 1.5 1.5 1.5"
sound="on: click; src: #click-sound"
shadow="cast: true">
</a-sphere>
<!-- VR Camera Rig -->
<a-entity id="rig" position="0 0 0">
<!-- Camera -->
<a-entity
id="camera"
camera
look-controls
position="0 1.6 0">
</a-entity>
<!-- Left Hand Controller -->
<a-entity
id="leftHand"
hand-controls="hand: left"
blink-controls="cameraRig: #rig; teleportOrigin: #camera; collisionEntities: .ground"
laser-controls="hand: left">
</a-entity>
<!-- Right Hand Controller -->
<a-entity
id="rightHand"
hand-controls="hand: right"
laser-controls="hand: right"
raycaster="objects: .interactive">
</a-entity>
</a-entity>
</a-scene>
<script>
// VR session events
const scene = document.querySelector('a-scene');
scene.addEventListener('enter-vr', () => {{
console.log('Entered VR mode');
}});
scene.addEventListener('exit-vr', () => {{
console.log('Exited VR mode');
}});
// Controller events
const rightHand = document.querySelector('#rightHand');
rightHand.addEventListener('triggerdown', () => {{
console.log('Trigger pressed');
}});
rightHand.addEventListener('gripdown', () => {{
console.log('Grip pressed');
}});
</script>
</body>
</html>'''
def generate_ar_scene(name):
"""Generate AR scene with hit testing"""
return f'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{name} - A-Frame AR Experience</title>
<meta name="description" content="{name} - AR Experience">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<style>
body {{ margin: 0; }}
#overlay {{
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 15px 20px;
border-radius: 8px;
font-family: sans-serif;
font-size: 14px;
text-align: center;
z-index: 1000;
}}
#overlay button {{
margin-top: 10px;
padding: 10px 20px;
background: #4CC3D9;
color: white;
border: none;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
}}
#overlay button:hover {{
background: #3BA0B5;
}}
</style>
</head>
<body>
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #model; type: footprint">
<!-- Assets -->
<a-assets>
<!-- Replace with your GLTF model -->
<a-asset-item id="model-asset" src="https://cdn.aframe.io/test-models/models/virtualcity/VC.gltf"></a-asset-item>
</a-assets>
<!-- AR Object to Place -->
<a-entity
id="model"
gltf-model="#model-asset"
scale="0.1 0.1 0.1"
visible="false">
</a-entity>
<!-- AR Lighting -->
<a-entity
light="type: ambient; intensity: 0.8">
</a-entity>
<a-entity
id="dirlight"
light="type: directional; intensity: 0.5; castShadow: true"
position="1 2 1">
</a-entity>
</a-scene>
<!-- AR UI Overlay -->
<div id="overlay">
<p id="instructions">Tap to enter AR mode</p>
<button id="clearBtn" style="display: none;">Clear Objects</button>
</div>
<script>
const scene = document.querySelector('a-scene');
const instructions = document.getElementById('instructions');
const clearBtn = document.getElementById('clearBtn');
const model = document.querySelector('#model');
let placedObjects = [];
// AR session events
scene.addEventListener('enter-vr', function() {{
if (this.is('ar-mode')) {{
instructions.textContent = '';
// Hit testing started
this.addEventListener('ar-hit-test-start', function() {{
instructions.innerHTML = 'Scanning environment...<br>Finding surfaces';
}}, {{ once: true }});
// Surface detected
this.addEventListener('ar-hit-test-achieved', function() {{
instructions.innerHTML = 'Tap to place object';
}}, {{ once: true }});
// Object placed
this.addEventListener('ar-hit-test-select', function(evt) {{
// Clone model for multiple placements
const clone = model.cloneNode(true);
clone.removeAttribute('id');
clone.setAttribute('visible', true);
const position = evt.detail.position;
clone.setAttribute('position', position);
scene.appendChild(clone);
placedObjects.push(clone);
instructions.innerHTML = 'Object placed! Tap to place another';
clearBtn.style.display = 'block';
}});
}}
}});
scene.addEventListener('exit-vr', function() {{
instructions.textContent = 'Tap to enter AR mode';
clearBtn.style.display = 'none';
}});
// Clear all placed objects
clearBtn.addEventListener('click', () => {{
placedObjects.forEach(obj => obj.parentNode.removeChild(obj));
placedObjects = [];
instructions.innerHTML = 'Objects cleared! Tap to place';
}});
</script>
</body>
</html>'''
def generate_360_scene(name):
"""Generate 360° photo/video gallery"""
return f'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{name} - 360° Gallery</title>
<meta name="description" content="{name} - 360° Gallery">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-event-set-component@5.0.0/dist/aframe-event-set-component.min.js"></script>
<style>
body {{ margin: 0; }}
#info {{
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 15px;
border-radius: 8px;
font-family: sans-serif;
font-size: 14px;
text-align: center;
}}
</style>
</head>
<body>
<a-scene>
<!-- Assets -->
<a-assets>
<!-- Replace with your 360 images -->
<img id="city" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/city.jpg" crossorigin="anonymous">
<img id="forest" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/cubes.jpg" crossorigin="anonymous">
<img id="space" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/sechelt.jpg" crossorigin="anonymous">
<!-- Thumbnails -->
<img id="city-thumb" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/thumb-city.jpg" crossorigin="anonymous">
<img id="forest-thumb" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/thumb-cubes.jpg" crossorigin="anonymous">
<img id="space-thumb" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/thumb-sechelt.jpg" crossorigin="anonymous">
<audio id="click-sound" src="https://cdn.aframe.io/360-image-gallery-boilerplate/audio/click.ogg"></audio>
</a-assets>
<!-- 360 Sky -->
<a-sky id="image-360" src="#city" rotation="0 -130 0"></a-sky>
<!-- Thumbnail Menu -->
<a-entity id="menu" position="0 1.6 -2.5" layout="type: line; margin: 1.5" rotation="0 0 0">
<!-- City Thumbnail -->
<a-entity class="link"
geometry="primitive: plane; width: 1; height: 1"
material="shader: flat; src: #city-thumb"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #city">
</a-entity>
<!-- Forest Thumbnail -->
<a-entity class="link"
geometry="primitive: plane; width: 1; height: 1"
material="shader: flat; src: #forest-thumb"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #forest">
</a-entity>
<!-- Space Thumbnail -->
<a-entity class="link"
geometry="primitive: plane; width: 1; height: 1"
material="shader: flat; src: #space-thumb"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #space">
</a-entity>
</a-entity>
<!-- Camera with Cursor -->
<a-camera>
<a-cursor
raycaster="objects: .link"
fuse="true"
fuse-timeout="1500">
</a-cursor>
</a-camera>
</a-scene>
<div id="info">
<strong>360° Gallery</strong><br>
Gaze at thumbnails to switch views
</div>
</body>
</html>'''
def generate_physics_scene(name):
"""Generate scene with physics"""
return f'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{name} - A-Frame Physics Scene</title>
<meta name="description" content="{name} - Physics Simulation">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-physics-system@4.2.2/dist/aframe-physics-system.min.js"></script>
<style>
body {{ margin: 0; }}
</style>
</head>
<body>
<a-scene physics="debug: false; gravity: -9.8">
<!-- Environment -->
<a-sky color="#87CEEB"></a-sky>
<!-- Static Ground -->
<a-plane
static-body
rotation="-90 0 0"
width="20"
height="20"
color="#7BC8A4"
shadow="receive: true">
</a-plane>
<!-- Lighting -->
<a-entity light="type: ambient; intensity: 0.6"></a-entity>
<a-entity light="type: directional; intensity: 0.5; castShadow: true" position="2 8 2"></a-entity>
<!-- Dynamic Physics Objects -->
<a-box
dynamic-body="mass: 5"
position="-2 5 -5"
rotation="45 45 0"
color="#4CC3D9"
shadow="cast: true">
</a-box>
<a-sphere
dynamic-body="mass: 3"
position="0 7 -5"
radius="0.5"
color="#EF2D5E"
shadow="cast: true">
</a-sphere>
<a-cylinder
dynamic-body="mass: 4"
position="2 6 -5"
radius="0.3"
height="1"
color="#FFC65D"
shadow="cast: true">
</a-cylinder>
<!-- Walls -->
<a-box
static-body
position="-10 2.5 -5"
width="0.5"
height="5"
depth="20"
color="#888">
</a-box>
<a-box
static-body
position="10 2.5 -5"
width="0.5"
height="5"
depth="20"
color="#888">
</a-box>
<!-- Camera -->
<a-camera position="0 1.6 5" look-controls wasd-controls></a-camera>
</a-scene>
<script>
// Spawn new objects on keypress
document.addEventListener('keydown', (evt) => {{
if (evt.key === ' ') {{
const scene = document.querySelector('a-scene');
const sphere = document.createElement('a-sphere');
sphere.setAttribute('dynamic-body', 'mass: 2');
sphere.setAttribute('position', {{
x: Math.random() * 4 - 2,
y: 10,
z: -5
}});
sphere.setAttribute('radius', 0.5);
sphere.setAttribute('color', `#${{Math.floor(Math.random()*16777215).toString(16)}}`);
sphere.setAttribute('shadow', 'cast: true');
scene.appendChild(sphere);
}}
}});
</script>
</body>
</html>'''
def generate_environment_scene(name):
"""Generate scene with procedural environment"""
return f'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{name} - A-Frame Environment</title>
<meta name="description" content="{name} - Procedural Environment">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-environment-component@1.3.3/dist/aframe-environment-component.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js"></script>
<style>
body {{ margin: 0; }}
</style>
</head>
<body>
<a-scene>
<!-- Procedural Environment -->
<a-entity environment="
preset: forest;
seed: 42;
skyType: gradient;
skyColor: #4A90E2;
horizonColor: #87CEEB;
lighting: distant;
lightPosition: 1 1 -2;
fog: 0.7;
ground: hills;
groundColor: #5A7F32;
groundColor2: #3D5E1F;
dressing: trees;
dressingAmount: 30;
dressingColor: #228B22;
dressingScale: 5;
grid: none
"></a-entity>
<!-- Particle Effects -->
<a-entity particle-system="
preset: dust;
particleCount: 1000;
color: #FFF;
size: 0.3;
maxAge: 3
" position="0 2 -5">
</a-entity>
<!-- Objects in Scene -->
<a-box
position="0 0.5 -5"
rotation="0 45 0"
color="#4CC3D9"
animation="property: rotation; to: 0 405 0; loop: true; dur: 10000">
</a-box>
<!-- Camera -->
<a-camera position="0 1.6 0" look-controls wasd-controls></a-camera>
</a-scene>
</body>
</html>'''
def generate_networked_scene(name):
"""Generate networked multi-user scene"""
return f'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{name} - Networked A-Frame</title>
<meta name="description" content="{name} - Multi-user VR">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/networked-aframe@^0.11.0/dist/networked-aframe.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.js"></script>
<script>
// NAF schema for networked avatars
window.addEventListener('load', () => {{
NAF.schemas.add({{
template: '#avatar-template',
components: [
'position',
'rotation'
]
}});
}});
</script>
<style>
body {{ margin: 0; }}
</style>
</head>
<body>
<a-scene networked-scene="
room: {name.lower().replace(' ', '-')};
adapter: wseasyrtc;
audio: true;
debug: false
">
<!-- Assets -->
<a-assets>
<!-- Avatar Template -->
<template id="avatar-template">
<a-entity class="avatar">
<a-sphere class="head" color="#5985ff" scale="0.2 0.22 0.2"></a-sphere>
<a-entity class="face" position="0 0.05 0">
<a-sphere class="eye" color="#FFF" position="0.06 0.05 -0.16" scale="0.04 0.04 0.04">
<a-sphere class="pupil" color="#000" position="0 0 -1" scale="0.5 0.5 0.5"></a-sphere>
</a-sphere>
<a-sphere class="eye" color="#FFF" position="-0.06 0.05 -0.16" scale="0.04 0.04 0.04">
<a-sphere class="pupil" color="#000" position="0 0 -1" scale="0.5 0.5 0.5"></a-sphere>
</a-sphere>
</a-entity>
</a-entity>
</template>
</a-assets>
<!-- Environment -->
<a-sky color="#87CEEB"></a-sky>
<a-plane rotation="-90 0 0" width="50" height="50" color="#7BC8A4"></a-plane>
<!-- Lighting -->
<a-entity light="type: ambient; intensity: 0.7"></a-entity>
<a-entity light="type: directional; intensity: 0.4" position="2 4 2"></a-entity>
<!-- Shared Objects -->
<a-box position="-1 0.5 -3" color="#4CC3D9"></a-box>
<a-sphere position="1 1.25 -3" radius="0.5" color="#EF2D5E"></a-sphere>
<!-- Player Rig -->
<a-entity id="rig" position="0 0 0">
<a-entity id="camera" camera look-controls wasd-controls position="0 1.6 0"
networked="template: #avatar-template; attachTemplateToLocal: false">
</a-entity>
</a-entity>
</a-scene>
</body>
</html>'''
def interactive_mode():
"""Interactive scene generator"""
print("\n=== A-Frame Scene Generator (Interactive Mode) ===\n")
print("Available scene types:")
for i, (scene_type, config) in enumerate(SCENE_TYPES.items(), 1):
print(f" {i}. {scene_type:12} - {config['description']}")
while True:
try:
choice = input("\nSelect scene type (1-7): ").strip()
scene_type = list(SCENE_TYPES.keys())[int(choice) - 1]
break
except (ValueError, IndexError):
print("Invalid choice. Please enter a number between 1 and 7.")
name = input("Enter scene name (default: MyScene): ").strip() or "MyScene"
output_dir = input("Enter output directory (default: current): ").strip() or "."
generate_scene(scene_type, name, output_dir)
def generate_scene(scene_type, name, output_dir="."):
"""Generate scene based on type"""
if scene_type not in SCENE_TYPES:
print(f"Error: Unknown scene type '{scene_type}'")
print(f"Available types: {', '.join(SCENE_TYPES.keys())}")
sys.exit(1)
# Generate HTML content
generators = {
'basic': generate_basic_scene,
'vr': generate_vr_scene,
'ar': generate_ar_scene,
'360': generate_360_scene,
'physics': generate_physics_scene,
'environment': generate_environment_scene,
'networked': generate_networked_scene
}
html_content = generators[scene_type](name)
# Create output directory
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# Write file
filename = SCENE_TYPES[scene_type]['filename']
file_path = output_path / filename
with open(file_path, 'w') as f:
f.write(html_content)
print(f"\n✅ Generated {scene_type} scene: {file_path}")
print(f"📝 Scene name: {name}")
print(f"🎯 Features: {', '.join(SCENE_TYPES[scene_type]['features'])}")
print(f"\n🚀 To view: Open {file_path} in a web browser")
print(" Note: Some features require HTTPS (use a local server)")
def main():
parser = argparse.ArgumentParser(
description='Generate A-Frame scene boilerplate',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
python scene_generator.py basic MyScene
python scene_generator.py vr VRProject --output ./my-scenes
python scene_generator.py ar ARFurniture
python scene_generator.py --interactive
Scene Types:
basic - Basic 3D scene with primitives
vr - VR scene with controllers
ar - AR scene with hit testing
360 - 360° photo/video gallery
physics - Scene with physics simulation
environment - Procedural environment
networked - Multi-user networked scene
'''
)
parser.add_argument(
'scene_type',
nargs='?',
choices=list(SCENE_TYPES.keys()),
help='Type of scene to generate'
)
parser.add_argument(
'name',
nargs='?',
default='MyScene',
help='Name of the scene (default: MyScene)'
)
parser.add_argument(
'-o', '--output',
default='.',
help='Output directory (default: current directory)'
)
parser.add_argument(
'-i', '--interactive',
action='store_true',
help='Run in interactive mode'
)
parser.add_argument(
'-l', '--list',
action='store_true',
help='List available scene types'
)
args = parser.parse_args()
if args.list:
print("\nAvailable Scene Types:\n")
for scene_type, config in SCENE_TYPES.items():
print(f" {scene_type:12} - {config['description']}")
print(f" Features: {', '.join(config['features'])}\n")
sys.exit(0)
if args.interactive or not args.scene_type:
interactive_mode()
else:
generate_scene(args.scene_type, args.name, args.output)
if __name__ == '__main__':
main()
SKILL.md
---
name: aframe-webxr
description: Declarative web framework for building browser-based 3D, VR, and AR experiences using HTML and entity-component architecture. Use this skill when creating WebXR applications, VR experiences, AR experiences, 360-degree media viewers, or immersive web content with minimal JavaScript. Triggers on tasks involving A-Frame, WebXR, VR development, AR development, entity-component-system, declarative 3D, or HTML-based 3D scenes. Built on Three.js with accessible HTML-first approach.
---
# A-Frame WebXR Skill
## When to Use This Skill
- Build VR/AR experiences with minimal JavaScript
- Create cross-platform WebXR applications (desktop, mobile, headset)
- Prototype 3D scenes quickly with HTML primitives
- Implement VR controller interactions
- Add 3D content to web pages declaratively
- Build 360° image/video experiences
- Develop AR experiences with hit testing
## Core Concepts
### 1. Entity-Component-System (ECS)
A-Frame uses an entity-component-system architecture where:
- **Entities** are containers (like `<div>` in HTML)
- **Components** add functionality/appearance to entities
- **Systems** provide global functionality
```html
<!-- Entity with components -->
<a-entity
geometry="primitive: box; width: 2"
material="color: red; metalness: 0.5"
position="0 1.5 -3"
rotation="0 45 0">
</a-entity>
```
**Primitives** are shortcuts for common entity + component combinations:
```html
<!-- Primitive (shorthand) -->
<a-box color="red" position="0 1.5 -3" rotation="0 45 0" width="2"></a-box>
<!-- Equivalent entity-component form -->
<a-entity
geometry="primitive: box; width: 2"
material="color: red"
position="0 1.5 -3"
rotation="0 45 0">
</a-entity>
```
### 2. Scene Setup
Every A-Frame app starts with `<a-scene>`:
```html
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
</head>
<body>
<a-scene>
<!-- Entities go here -->
<a-box position="-1 0.5 -3" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>
```
The scene automatically injects:
- Default camera (position: `0 1.6 0`)
- Look controls (mouse drag)
- WASD controls (keyboard movement)
### 3. Camera Systems
**Default Camera** (auto-injected if none specified):
```html
<a-entity camera="active: true" look-controls wasd-controls position="0 1.6 0"></a-entity>
```
**Custom Camera**:
```html
<a-camera position="0 2 5" look-controls wasd-controls="acceleration: 50"></a-camera>
```
**Camera Rig** (for independent movement and rotation):
```html
<a-entity id="rig" position="0 0 0">
<!-- Camera for head tracking -->
<a-camera look-controls></a-camera>
<!-- Movement applied to rig, not camera -->
</a-entity>
```
**VR Camera Rig with Controllers**:
```html
<a-entity id="rig" position="0 0 0">
<!-- Camera at eye level -->
<a-camera position="0 1.6 0"></a-camera>
<!-- Left hand controller -->
<a-entity
hand-controls="hand: left"
laser-controls="hand: left">
</a-entity>
<!-- Right hand controller -->
<a-entity
hand-controls="hand: right"
laser-controls="hand: right">
</a-entity>
</a-entity>
```
### 4. Lighting
**Ambient Light** (global illumination):
```html
<a-entity light="type: ambient; color: #BBB; intensity: 0.5"></a-entity>
```
**Directional Light** (like sunlight):
```html
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="1 2 1"></a-entity>
```
**Point Light** (radiates in all directions):
```html
<a-entity light="type: point; color: #F00; intensity: 2; distance: 50" position="0 3 0"></a-entity>
```
**Spot Light** (cone-shaped beam):
```html
<a-entity light="type: spot; angle: 45; intensity: 1.5" position="0 5 0" rotation="-90 0 0"></a-entity>
```
### 5. Materials and Textures
**Standard Material**:
```html
<a-sphere
material="color: #FF0000; metalness: 0.5; roughness: 0.3"
position="0 1 -3">
</a-sphere>
```
**Textured Material**:
```html
<a-assets>
<img id="woodTexture" src="wood.jpg">
</a-assets>
<a-box material="src: #woodTexture" position="0 1 -3"></a-box>
```
**Flat Shading** (no lighting):
```html
<a-plane material="shader: flat; color: #4CC3D9"></a-plane>
```
### 6. Animations
**Property Animation**:
```html
<a-box
position="0 1 -3"
animation="property: rotation; to: 0 360 0; loop: true; dur: 5000">
</a-box>
```
**Multiple Animations** (use `animation__*` naming):
```html
<a-sphere
position="0 1 -3"
animation__position="property: position; to: 0 3 -3; dir: alternate; loop: true; dur: 2000"
animation__rotation="property: rotation; to: 360 360 0; loop: true; dur: 4000"
animation__scale="property: scale; to: 1.5 1.5 1.5; dir: alternate; loop: true; dur: 1000">
</a-sphere>
```
**Event-Based Animation**:
```html
<a-box
color="blue"
animation__mouseenter="property: scale; to: 1.2 1.2 1.2; startEvents: mouseenter"
animation__mouseleave="property: scale; to: 1 1 1; startEvents: mouseleave"
animation__click="property: rotation; from: 0 0 0; to: 0 360 0; startEvents: click">
</a-box>
```
### 7. Assets Management
Preload assets for better performance:
```html
<a-scene>
<a-assets>
<!-- Images -->
<img id="texture1" src="texture.jpg">
<img id="skyTexture" src="sky.jpg">
<!-- Videos -->
<video id="video360" src="360video.mp4" autoplay loop></video>
<!-- Audio -->
<audio id="bgMusic" src="music.mp3" preload="auto"></audio>
<!-- Models -->
<a-asset-item id="tree" src="tree.gltf"></a-asset-item>
<!-- Mixins (reusable component sets) -->
<a-mixin id="redMaterial" material="color: red; metalness: 0.7"></a-mixin>
</a-assets>
<!-- Use assets -->
<a-entity gltf-model="#tree" position="2 0 -5"></a-entity>
<a-sphere mixin="redMaterial" position="0 1 -3"></a-sphere>
<a-sky src="#skyTexture"></a-sky>
</a-scene>
```
### 8. Custom Components
Register custom components to encapsulate logic:
```javascript
AFRAME.registerComponent('rotate-on-click', {
// Component schema (configuration)
schema: {
speed: {type: 'number', default: 1}
},
// Lifecycle: called once when component attached
init: function() {
this.el.addEventListener('click', () => {
this.rotating = !this.rotating;
});
},
// Lifecycle: called every frame
tick: function(time, timeDelta) {
if (this.rotating) {
var rotation = this.el.getAttribute('rotation');
rotation.y += this.data.speed;
this.el.setAttribute('rotation', rotation);
}
}
});
```
```html
<a-box rotate-on-click="speed: 2" position="0 1 -3"></a-box>
```
## Common Patterns
### Pattern 1: VR Controller Interactions
**Problem**: Enable object grabbing and manipulation in VR
**Solution**: Use hand-controls and custom grab component
```html
<a-scene>
<!-- VR Camera Rig -->
<a-entity id="rig">
<a-camera position="0 1.6 0"></a-camera>
<a-entity
id="leftHand"
hand-controls="hand: left"
laser-controls="hand: left">
</a-entity>
<a-entity
id="rightHand"
hand-controls="hand: right"
laser-controls="hand: right">
</a-entity>
</a-entity>
<!-- Grabbable objects -->
<a-box class="grabbable" position="-1 1.5 -3" color="#4CC3D9"></a-box>
<a-sphere class="grabbable" position="1 1.5 -3" color="#EF2D5E"></a-sphere>
</a-scene>
<script>
AFRAME.registerComponent('grabbable', {
init: function() {
var el = this.el;
el.addEventListener('triggerdown', function(evt) {
console.log('Grabbed by', evt.detail.hand);
el.setAttribute('color', 'green');
});
el.addEventListener('triggerup', function(evt) {
el.setAttribute('color', 'blue');
});
el.addEventListener('gripdown', function(evt) {
// Attach object to controller
var controllerEl = evt.detail.controller;
controllerEl.object3D.attach(el.object3D);
});
el.addEventListener('gripup', function(evt) {
// Detach from controller
var sceneEl = el.sceneEl.object3D;
sceneEl.attach(el.object3D);
});
}
});
// Apply grabbable component
document.querySelectorAll('.grabbable').forEach(el => {
el.setAttribute('grabbable', '');
});
</script>
```
### Pattern 2: 360° Image Gallery
**Problem**: Create an interactive 360° photo viewer
**Solution**: Use sky primitive and clickable thumbnails
```html
<a-scene>
<a-assets>
<img id="city" src="city.jpg">
<img id="forest" src="forest.jpg">
<img id="beach" src="beach.jpg">
<img id="city-thumb" src="city-thumb.jpg">
<img id="forest-thumb" src="forest-thumb.jpg">
<img id="beach-thumb" src="beach-thumb.jpg">
<audio id="click-sound" src="click.mp3"></audio>
</a-assets>
<!-- 360 image sphere -->
<a-sky id="image-360" src="#city" rotation="0 -130 0"></a-sky>
<!-- Thumbnail menu -->
<a-entity id="menu" position="0 1.6 -2">
<a-entity class="link"
geometry="primitive: plane; width: 0.7; height: 0.7"
material="shader: flat; src: #city-thumb"
position="-1 0 0"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #city">
</a-entity>
<a-entity class="link"
geometry="primitive: plane; width: 0.7; height: 0.7"
material="shader: flat; src: #forest-thumb"
position="0 0 0"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #forest">
</a-entity>
<a-entity class="link"
geometry="primitive: plane; width: 0.7; height: 0.7"
material="shader: flat; src: #beach-thumb"
position="1 0 0"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #beach">
</a-entity>
</a-entity>
<!-- Camera with cursor for gaze interaction -->
<a-camera>
<a-cursor raycaster="objects: .link"></a-cursor>
</a-camera>
</a-scene>
```
### Pattern 3: AR Hit Testing (Place Objects in Real World)
**Problem**: Place virtual objects on detected real-world surfaces
**Solution**: Use ar-hit-test component
```html
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #furniture; type: footprint">
<a-assets>
<a-asset-item id="chair" src="chair.gltf"></a-asset-item>
</a-assets>
<!-- Object to place -->
<a-entity id="furniture" gltf-model="#chair" scale="0.5 0.5 0.5"></a-entity>
<!-- AR instructions overlay -->
<div id="overlay" style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: white; padding: 15px;
border-radius: 8px; font-family: sans-serif;">
<p id="message">Tap to enter AR mode</p>
</div>
</a-scene>
<script>
const sceneEl = document.querySelector('a-scene');
const message = document.getElementById('message');
sceneEl.addEventListener('enter-vr', function() {
if (this.is('ar-mode')) {
message.textContent = '';
this.addEventListener('ar-hit-test-start', function() {
message.innerHTML = 'Scanning environment, finding surface.';
}, { once: true });
this.addEventListener('ar-hit-test-achieved', function() {
message.innerHTML = 'Tap on the screen to place the object.';
}, { once: true });
this.addEventListener('ar-hit-test-select', function() {
message.textContent = 'Object placed!';
setTimeout(() => message.textContent = '', 2000);
}, { once: true });
}
});
sceneEl.addEventListener('exit-vr', function() {
message.textContent = 'Tap to enter AR mode';
});
</script>
```
### Pattern 4: Mouse/Gaze Interactions
**Problem**: Enable click interactions with desktop mouse or VR gaze
**Solution**: Use cursor component and raycaster
```html
<a-scene>
<!-- Interactive objects -->
<a-box
class="interactive"
position="-1 1.5 -3"
color="#4CC3D9"
event-set__mouseenter="color: yellow"
event-set__mouseleave="color: #4CC3D9"
event-set__click="scale: 1.5 1.5 1.5">
</a-box>
<a-sphere
class="interactive"
position="1 1.5 -3"
color="#EF2D5E"
event-set__click="color: orange; scale: 2 2 2">
</a-sphere>
<a-plane position="0 0 -4" rotation="-90 0 0" width="10" height="10" color="#7BC8A4"></a-plane>
<!-- Camera with cursor -->
<a-camera position="0 1.6 0">
<!-- Raycaster targets .interactive class -->
<a-cursor
raycaster="objects: .interactive"
fuse="true"
fuse-timeout="1500">
</a-cursor>
</a-camera>
</a-scene>
<script>
// Advanced click handling with JavaScript
document.querySelectorAll('.interactive').forEach(el => {
el.addEventListener('click', function(evt) {
console.log('Clicked:', this.id || this.tagName);
console.log('Intersection point:', evt.detail.intersection.point);
});
});
</script>
```
### Pattern 5: Dynamic Scene Generation
**Problem**: Programmatically create and manipulate entities
**Solution**: Use JavaScript DOM manipulation
```html
<a-scene>
<a-camera position="0 1.6 5"></a-camera>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF" position="1 2 1"></a-entity>
</a-scene>
<script>
const scene = document.querySelector('a-scene');
// Create sphere
function createSphere(x, y, z, color) {
const entity = document.createElement('a-entity');
entity.setAttribute('geometry', {
primitive: 'sphere',
radius: 0.5
});
entity.setAttribute('material', {
color: color,
metalness: 0.5,
roughness: 0.3
});
entity.setAttribute('position', {x, y, z});
// Add animation
entity.setAttribute('animation', {
property: 'position',
to: `${x} ${y + 1} ${z}`,
dir: 'alternate',
loop: true,
dur: 2000
});
scene.appendChild(entity);
return entity;
}
// Generate grid of spheres
for (let x = -3; x <= 3; x += 1.5) {
for (let z = -5; z <= -2; z += 1.5) {
const color = `#${Math.floor(Math.random()*16777215).toString(16)}`;
createSphere(x, 1, z, color);
}
}
// Listen to component changes
scene.addEventListener('componentchanged', function(evt) {
console.log('Component changed:', evt.detail.name);
});
// Access Three.js objects directly
setTimeout(() => {
const entities = document.querySelectorAll('a-entity[geometry]');
entities.forEach(el => {
el.object3D.visible = true; // Direct Three.js manipulation
});
}, 1000);
</script>
```
### Pattern 6: Environment and Skybox
**Problem**: Create immersive environments quickly
**Solution**: Use community components and 360 images
```html
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@fern-solutions/aframe-sky-background/dist/sky-background.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.0/dist/aframe-extras.min.js"></script>
</head>
<body>
<a-scene>
<!-- Gradient sky -->
<a-sky-background
top-color="#4A90E2"
bottom-color="#87CEEB">
</a-sky-background>
<!-- Or textured sky -->
<!-- <a-sky src="sky.jpg" rotation="0 -130 0"></a-sky> -->
<!-- Ocean -->
<a-entity
ocean="density: 20; width: 50; depth: 50; speed: 4"
material="color: #9CE3F9; opacity: 0.75; metalness: 0; roughness: 1"
rotation="-90 0 0">
</a-entity>
<!-- Particle system for atmosphere -->
<a-entity
particle-system="preset: snow; particleCount: 2000; color: #FFF">
</a-entity>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.7" position="1 2 1"></a-entity>
</a-scene>
</body>
</html>
```
### Pattern 7: GLTF Model Loading
**Problem**: Load and display 3D models
**Solution**: Use gltf-model component with asset management
```html
<a-scene>
<a-assets>
<a-asset-item id="robot" src="robot.gltf"></a-asset-item>
<a-asset-item id="building" src="building.glb"></a-asset-item>
</a-assets>
<!-- Load model -->
<a-entity
gltf-model="#robot"
position="0 0 -3"
scale="0.5 0.5 0.5"
animation="property: rotation; to: 0 360 0; loop: true; dur: 10000">
</a-entity>
<!-- Load with extras (animations) -->
<a-entity
gltf-model="#building"
position="5 0 -10"
animation-mixer="clip: *; loop: repeat">
</a-entity>
<a-camera position="0 1.6 5"></a-camera>
<a-entity light="type: ambient; intensity: 0.5"></a-entity>
<a-entity light="type: directional; intensity: 0.8" position="2 4 2"></a-entity>
</a-scene>
<script>
// Handle model loading events
document.querySelector('[gltf-model="#robot"]').addEventListener('model-loaded', (evt) => {
console.log('Model loaded:', evt.detail.model);
// Access Three.js object
const model = evt.detail.model;
model.traverse(node => {
if (node.isMesh) {
console.log('Mesh found:', node.name);
}
});
});
document.querySelector('[gltf-model="#robot"]').addEventListener('model-error', (evt) => {
console.error('Model loading error:', evt.detail);
});
</script>
```
## Integration Patterns
### With Three.js
Access underlying Three.js objects:
```javascript
// Get Three.js scene
const scene = document.querySelector('a-scene').object3D;
// Get entity's Three.js object
const box = document.querySelector('a-box');
const threeObject = box.object3D;
// Direct Three.js manipulation
threeObject.position.set(1, 2, 3);
threeObject.rotation.y = Math.PI / 4;
// Add custom Three.js objects
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
```
### With GSAP (Animation)
Animate A-Frame entities with GSAP:
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
const box = document.querySelector('a-box');
// Animate position
gsap.to(box.object3D.position, {
x: 3,
y: 2,
z: -5,
duration: 2,
ease: 'power2.inOut'
});
// Animate rotation
gsap.to(box.object3D.rotation, {
y: Math.PI * 2,
duration: 3,
repeat: -1,
ease: 'none'
});
// Animate attributes
gsap.to(box.components.material.material, {
opacity: 0.5,
duration: 1
});
</script>
```
### With React
Integrate A-Frame in React components:
```jsx
import React, { useEffect, useRef } from 'react';
import 'aframe';
function VRScene() {
const sceneRef = useRef(null);
useEffect(() => {
const scene = sceneRef.current;
// Create entities dynamically
const entity = document.createElement('a-sphere');
entity.setAttribute('position', '0 1.5 -3');
entity.setAttribute('color', '#EF2D5E');
scene.appendChild(entity);
// Listen to events
scene.addEventListener('enter-vr', () => {
console.log('Entered VR mode');
});
}, []);
return (
<a-scene ref={sceneRef}>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" />
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" />
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" />
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" />
<a-sky color="#ECECEC" />
</a-scene>
);
}
export default VRScene;
```
## Performance Best Practices
### 1. Use Asset Management
Preload assets to avoid blocking:
```html
<a-assets>
<img id="texture1" src="large-texture.jpg">
<video id="video360" src="360video.mp4" preload="auto"></video>
<a-asset-item id="model" src="complex-model.gltf"></a-asset-item>
</a-assets>
```
### 2. Pool Entities
Reuse entities instead of creating/destroying:
```javascript
AFRAME.registerComponent('bullet-pool', {
init: function() {
this.pool = [];
this.used = [];
// Pre-create bullets
for (let i = 0; i < 20; i++) {
const bullet = document.createElement('a-sphere');
bullet.setAttribute('radius', 0.1);
bullet.setAttribute('visible', false);
this.el.sceneEl.appendChild(bullet);
this.pool.push(bullet);
}
},
getBullet: function() {
if (this.pool.length > 0) {
const bullet = this.pool.pop();
bullet.setAttribute('visible', true);
this.used.push(bullet);
return bullet;
}
},
returnBullet: function(bullet) {
bullet.setAttribute('visible', false);
const index = this.used.indexOf(bullet);
if (index > -1) {
this.used.splice(index, 1);
this.pool.push(bullet);
}
}
});
```
### 3. Optimize Geometry
Use low-poly models and LOD:
```html
<!-- Low-poly for distant objects -->
<a-sphere radius="1" segments-width="8" segments-height="6"></a-sphere>
<!-- High-poly for close objects -->
<a-sphere radius="1" segments-width="32" segments-height="32"></a-sphere>
```
### 4. Limit Draw Calls
Use instancing for repeated objects:
```javascript
AFRAME.registerComponent('instanced-trees', {
init: function() {
// Use Three.js InstancedMesh for repeated geometry
const scene = this.el.sceneEl.object3D;
const geometry = new THREE.ConeGeometry(0.5, 2, 8);
const material = new THREE.MeshStandardMaterial({ color: 0x228B22 });
const mesh = new THREE.InstancedMesh(geometry, material, 100);
// Position instances
for (let i = 0; i < 100; i++) {
const matrix = new THREE.Matrix4();
matrix.setPosition(
Math.random() * 20 - 10,
0,
Math.random() * 20 - 10
);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);
}
});
```
### 5. Throttle tick() Functions
Don't update every frame if unnecessary:
```javascript
AFRAME.registerComponent('throttled-update', {
init: function() {
this.lastUpdate = 0;
this.updateInterval = 100; // ms
},
tick: function(time, timeDelta) {
if (time - this.lastUpdate >= this.updateInterval) {
// Expensive operation here
this.lastUpdate = time;
}
}
});
```
### 6. Use Stats Component for Monitoring
```html
<a-scene stats>
<!-- Shows FPS and performance metrics -->
</a-scene>
```
## Common Pitfalls and Solutions
### Pitfall 1: Entities Not Appearing
**Problem**: Entity added but not visible
**Causes**:
- Entity positioned behind camera
- Scale is 0 or very small
- Material opacity is 0
- Entity outside camera frustum
**Solution**:
```javascript
// Wait for scene to load
const scene = document.querySelector('a-scene');
scene.addEventListener('loaded', () => {
const entity = document.createElement('a-box');
entity.setAttribute('position', '0 1.5 -3'); // In front of camera
entity.setAttribute('color', 'red');
scene.appendChild(entity);
});
// Debug: Check entity position
console.log(entity.getAttribute('position'));
// Debug: Check if entity is in scene
console.log(entity.parentNode); // Should be <a-scene>
```
### Pitfall 2: Events Not Firing
**Problem**: Click/mouseenter events don't trigger
**Cause**: Missing raycaster or cursor
**Solution**:
```html
<!-- Add cursor to camera -->
<a-camera>
<a-cursor raycaster="objects: .interactive"></a-cursor>
</a-camera>
<!-- Add class to interactive objects -->
<a-box class="interactive" position="0 1 -3"></a-box>
<!-- Or use raycaster directly -->
<a-entity raycaster="objects: [geometry]" cursor></a-entity>
```
### Pitfall 3: Performance Degradation
**Problem**: Low FPS with many entities
**Causes**:
- Too many draw calls
- Complex geometries
- Unoptimized textures
- Too many tick() updates
**Solutions**:
```javascript
// 1. Use object pooling (see Performance section)
// 2. Simplify geometry
// 3. Optimize textures (reduce size, use compression)
// 4. Throttle updates
AFRAME.registerComponent('optimize-far-entities', {
tick: function() {
const camera = this.el.sceneEl.camera;
const entities = document.querySelectorAll('[geometry]');
entities.forEach(el => {
const distance = el.object3D.position.distanceTo(camera.position);
// Hide distant entities
el.object3D.visible = distance < 50;
});
}
});
```
### Pitfall 4: Z-Fighting (Overlapping Surfaces)
**Problem**: Flickering when surfaces overlap
**Cause**: Two surfaces at same position
**Solution**:
```html
<!-- Offset surfaces slightly -->
<a-plane position="0 0.01 0" rotation="-90 0 0"></a-plane>
<a-plane position="0 0.02 0" rotation="-90 0 0"></a-plane>
<!-- Or use renderOrder -->
<a-entity
geometry="primitive: plane"
material="src: #texture1; transparent: true"
class="has-render-order">
</a-entity>
<script>
document.querySelector('.has-render-order').object3D.renderOrder = 1;
</script>
```
### Pitfall 5: Mobile VR Performance
**Problem**: Low performance on mobile VR
**Solutions**:
```html
<!-- Reduce renderer max canvas size -->
<a-scene renderer="maxCanvasWidth: 1920; maxCanvasHeight: 1920">
<!-- Use low-poly models -->
<a-sphere radius="1" segments-width="8" segments-height="6"></a-sphere>
<!-- Limit lights (expensive on mobile) -->
<a-entity light="type: ambient; intensity: 0.6"></a-entity>
<a-entity light="type: directional; intensity: 0.4" position="1 2 1"></a-entity>
<!-- Disable antialiasing if needed -->
<a-scene renderer="antialias: false">
</a-scene>
```
### Pitfall 6: Asset Loading Issues
**Problem**: Assets not loading or CORS errors
**Solutions**:
```html
<!-- Use crossorigin attribute -->
<a-assets>
<img id="texture" src="https://example.com/texture.jpg" crossorigin="anonymous">
</a-assets>
<!-- Wait for assets to load -->
<script>
const assets = document.querySelector('a-assets');
assets.addEventListener('loaded', () => {
console.log('All assets loaded');
// Safe to use assets now
});
assets.addEventListener('timeout', () => {
console.error('Asset loading timeout');
});
</script>
<!-- Handle loading errors -->
<script>
const img = document.querySelector('img#texture');
img.addEventListener('error', () => {
console.error('Failed to load texture');
// Use fallback
img.src = 'fallback-texture.jpg';
});
</script>
```
## Resources
- [A-Frame Documentation](https://aframe.io/docs/)
- [A-Frame GitHub](https://github.com/aframevr/aframe)
- [A-Frame School](https://aframe.io/school/)
- [A-Frame Community Components](https://github.com/c-frame)
- [WebXR Device API](https://www.w3.org/TR/webxr/)
- [Three.js Documentation](https://threejs.org/docs/) (A-Frame built on Three.js)
## Related Skills
- **threejs-webgl**: For advanced Three.js control beyond A-Frame's declarative API
- **babylonjs-engine**: Alternative 3D engine with different architecture
- **gsap-scrolltrigger**: For animating A-Frame entities with GSAP
- **react-three-fiber**: React approach to Three.js (compare with A-Frame's HTML approach)