Appearance
Examples
Complete patterns for common scripting tasks. Each example is self-contained – attach to the indicated event and it works.
Entity hierarchy and lookup
Every object in the scene is an Entity (the anchor container). Each entity has one or more Representations (the visuals inside it – models, shapes, images, audio, etc.).
scene
└─ Entity ("MyObject") ← scene.findEntity({ name: "MyObject" })
├─ representation (primary) ← entity.representation
├─ Representation "Audio1" ← entity.findRepresentation({ name: "Audio1" })
└─ Representation "SubModel" ← entity.findRepresentation({ name: "SubModel" })Common Gotcha
scene.findEntity only finds top-level entities – not nested representations inside them. Find the parent entity first, then use findRepresentation to navigate.
Find the parent entity first, then navigate to the representation:
javascript
// WRONG – Audio1 is a representation, not a top-level entity
var audio = scene.findEntity({ name: "Audio1" }); // null!
// RIGHT – find the parent entity, then the nested representation
var parent = scene.findEntity({ name: "MyObject" });
var audio = parent.findRepresentation({ name: "Audio1" });
audio.isEnabled = true;Look up by ID or name:
javascript
// Entity lookup
var entity = scene.findEntity("ABC123"); // by ID
var entity = scene.findEntity({ name: "MyBox" }); // by name
// Representation lookup (from entity)
var rep = entity.representation; // primary (first) representation
var rep = entity.findRepresentation("DEF456"); // by ID
var rep = entity.findRepresentation({ name: "Wheel" }); // by nameExplore a model's internal hierarchy:
javascript
var entity = scene.findEntity({ name: "Robot" });
var rep = entity.representation;
// List all nested children (bones, sub-meshes, etc.)
var names = rep.getChildNames();
console.log(names); // ["torso", "arm_left", "arm_right", ...]
// Access a specific child
var arm = rep.findChild("arm_left");
arm.transform = Transform({ rotation: Rotation(0, 0, Math.PI / 4) });Event context: objectEntity vs representationEntity
When a script runs from an event (tap, collision, etc.), the event data includes both:
javascript
// objectEntity = the top-level entity (anchor container)
var entity = scriptContext.sourceEvent.objectEntity;
// representationEntity = the specific representation that triggered the event
var rep = scriptContext.sourceEvent.representationEntity;
// Common pattern: use representationEntity for visual changes
rep.opacity = 0.5;
rep.animateTo({ scale: Vector3(1.2, 1.2, 1.2) }, 0.3);
// Use objectEntity to find sibling representations
var audio = entity.findRepresentation({ name: "Audio1" });
audio.isEnabled = true;Sequential audio with collision counter
A practical example combining entity navigation with state:
javascript
// Attach to: On Object Collision (on each trigger object)
// Setup: Parent entity "AudioPlayer" contains representations "Audio1", "Audio2", "Audio3"
var count = experience.getVariable("hitCount") ?? 0;
count = count + 1;
experience.setVariable("hitCount", count);
if (count <= 3) {
var player = scene.findEntity({ name: "AudioPlayer" });
var audio = player.findRepresentation({ name: "Audio" + count });
if (audio) {
audio.isEnabled = true;
}
}Create objects dynamically
Spawn a box in front of the camera:
javascript
// Attach to: On Screen Tap
var box = await scene.createEntity(
createBox(0.3, 0.3, 0.3) // width, height, depth in meters
.anchor(Anchor.currentPOV(0, 0, -1)) // x, y, z offset from camera
);
console.log("Created: " + box.id);Create a sphere at a fixed world position with traits:
javascript
var sphere = await scene.createEntity(
createSphere(0.1) // radius in meters
.name("Ball")
.anchor(Anchor.position(0, 1.5, -2)) // x, y, z world position
.opacity(0.8)
);Place an object on a detected floor:
javascript
var marker = await scene.createEntity(
createSphere(0.05)
.name("Marker")
.anchor(Anchor.horizontalPlane("floor"))
);Load a 3D model from a remote URL:
javascript
// Attach to: On Experience Start
var model = await scene.createEntity(
createModel("https://developer.apple.com/augmented-reality/quick-look/models/soccerball/ball_soccerball_realistic.usdz")
.name("RemoteModel")
.anchor(Anchor.position(0, 1, -2))
.fittingBox(0.5) // fit into 0.5m bounding box
);
// Spin on tap
model.on('tap', function() {
model.play(EntityAnimation.spin(1, 0.8, { axis: [0, 1, 0] }));
});Display an image:
javascript
var photo = await scene.createEntity(
createImage("https://picsum.photos/800/600", 0.5, 1.5) // url, width (m), aspect ratio (w/h)
.anchor(Anchor.currentPOV(0, 0, -1.5))
);Adjust an authored object before it appears
findEntity gives you an object that's already in the scene. scene.getObjectDescriptor gives you the recipe of an object you built in the editor – so a script can adjust it before it's displayed, then write it back with scene.setObjectDescriptor. Use it to adapt an authored object to the device, add an interaction the editor can't express per platform, or spawn variations from it.
The edit takes effect the next time the object is displayed, so run the script before the object's "Add to Scene" action, in the same event – at the top level or in a Sequence, not grouped with it (a Group runs its actions at the same time).
Enable a throw gesture only where there's no touchscreen. On Apple Vision Pro there are no screen gestures, so drag‑to‑throw has to live on the object itself – author the ball once, then add the gesture at runtime only where it's needed:
javascript
// Attach to: On Start, before the ball's Add to Scene action
if (!environment.features.has('screenGestures')) {
const ball = scene.getObjectDescriptor({ name: 'ball' });
ball.gestures({ drag: true, releaseBehavior: 'momentum' });
scene.setObjectDescriptor(ball);
}Swap the source asset per device – a lighter model on a phone, a heavier one on a headset. .asset(...) replaces a model's or video's source, routed to the right place for its kind:
javascript
const hero = scene.getObjectDescriptor({ name: 'hero' });
hero.asset(environment.deviceCategory === 'spatial' ? 'hero_hi.usdz' : 'hero_lo.usdz');
scene.setObjectDescriptor(hero);Spawn variations from one authored template. .clone() copies the descriptor with fresh ids, so you can add many:
javascript
const template = scene.getObjectDescriptor({ name: 'can' });
for (const p of positions) {
await scene.createEntity(template.clone().transform({ position: p }));
}Passing the authored descriptor to createEntity without .clone() is rejected – its id already belongs to the authored object, so clone first for a distinct copy.
Replace the whole representation. .representation(...) swaps the visual entirely – the way to change kind, e.g. a simple shape instead of a model on a low‑end device – keeping the object's placement and events:
javascript
const crate = scene.getObjectDescriptor({ name: 'crate' });
crate.representation(createBox(0.3, 0.3, 0.3).material('crate-mat'));
scene.setObjectDescriptor(crate);TIP
Inspect what you fetched with descriptor.kind ('model', 'primitive', 'visualMedia', …) and branch on it, and keep descriptor.id to look the object up later with findEntity.
Session only
These edits live in memory for the running session – they change how the object appears now, not the saved experience.
Animate objects
Simple property animation:
javascript
var box = scene.findEntity({ name: "MyBox" });
// Animate to target values (fire-and-forget, no await needed)
box.animateTo(
{ position: Vector3(0, 2, -1), opacity: 0.5 },
1.0,
{ timingFunction: "easeInOut" }
);
// Available: "linear", "easeIn", "easeOut", "easeInOut"Chained animations with async/await:
javascript
var box = scene.findEntity({ name: "MyBox" });
await box.animateTo({ position: Vector3(0, 2, 0) }, 1.0);
await box.animateTo({ scale: Vector3(2, 2, 2) }, 0.5);
await box.animateTo({ opacity: 0 }, 0.3);Using EntityAnimation with play():
javascript
var box = scene.findEntity({ name: "MyBox" });
// Spin 2 full revolutions over 3 seconds
await box.play(EntityAnimation.spin(2, 3.0, { axis: [0, 1, 0] }));
// Keyframe animation
await box.play(EntityAnimation.keyframes([
{ position: Vector3(0, 1, -2) },
{ position: Vector3(1, 2, -2) },
{ position: Vector3(0, 3, -2) }
], 2.0, { tweenMode: "linear" }));
// Group multiple animations together
await box.play(EntityAnimation.group([
EntityAnimation.to({ opacity: 0.5 }, 1.0),
EntityAnimation.spin(1, 1.0)
]));Value animations
Spring animations
animateTo supports "linear", "easeIn", "easeOut", "easeInOut" timing. For spring dynamics, use animateValue with a spring config instead.
Animate a number and apply it each frame:
javascript
var box = scene.findEntity({ name: "MyBox" });
animateValue({
from: 0,
to: Math.PI * 2,
duration: 3.0,
curve: "easeInOut",
repeatCount: -1,
onUpdate: function(value) {
box.representation.position = Vector3(
Math.sin(value) * 2,
1,
Math.cos(value) * 2
);
}
});Spring-based value animation:
javascript
animateValue({
from: 0.6,
to: 0.08,
spring: { duration: 0.6, bounce: 0 },
onUpdate: function(h) {
scene.setVariable("baseColor", Color.hsl(h, 0.8, 0.55));
}
});Repeating pulse animation:
javascript
animateValue({
from: 0.6,
to: 0.65,
duration: 4,
curve: 'easeInOut',
repeatCount: -1,
reverseOnRepeat: true,
onUpdate: function(h) {
scene.setVariable("baseColor", Color.hsl(h, 0.5, 0.5));
}
});Fetch data from an API
GET request – fetch live weather data:
javascript
// Attach to: On Experience Start
var response = await http.get(
"https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=temperature_2m"
);
var temp = response.data.current.temperature_2m;
console.log("Berlin: " + temp + "°C");POST request with JSON:
javascript
var response = await http.post(
"https://api.example.com/submit",
JSON.stringify({ score: 100, name: "Player" }),
{ headers: { "Content-Type": "application/json" } }
);
console.log("Status: " + response.status);Custom request with auth and headers:
javascript
var response = await http.request("https://api.example.com/data", {
method: "POST",
token: "YOUR_API_KEY",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "test" })
});
console.log(response.data.results);Authorization
Use the token shorthand for Bearer auth, or pass headers directly:
javascript
// These are equivalent:
http.request(url, { token: "sk-..." });
http.request(url, { headers: { "Authorization": "Bearer sk-..." } });Binary response as base64 (e.g. text-to-speech):
javascript
var response = await http.request("https://api.openai.com/v1/audio/speech", {
method: "POST",
token: "sk-...",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "tts-1",
input: "Hello from augmented reality!",
voice: "nova",
response_format: "pcm"
}),
responseType: "base64"
});
if (response.status === 200) {
var entity = scene.findEntity({ name: "Speaker" });
await entity.playAudioBuffer(response.data, { sampleRate: 24000 });
}WebSocket real-time connection
javascript
// Attach to: On Experience Start
var ws = websocket.connect("wss://echo.websocket.org", {
headers: { "Authorization": "Bearer YOUR_TOKEN" }
});
ws.onOpen = function() {
console.log("Connected");
ws.send(JSON.stringify({ type: "hello" }));
};
ws.onMessage = function(event) {
var data = JSON.parse(event.data);
console.log("Received: " + data.type);
};
ws.onClose = function() {
console.log("Disconnected");
};
ws.onError = function(event) {
console.log("Error: " + event.error);
};Shader material parameters
Bind a shader parameter to a variable, then toggle it:
javascript
var box = scene.findEntity({ name: "Box" });
// Bind the ShaderGraphMaterial parameter "isOn" to scene variable "isOn"
box.representation.bindMaterialParameter("isOn", "isOn", { update: "onChange" });
// Initialize the variable
scene.setVariable("isOn", false);
// Toggle on tap
box.on("tap", function() {
var isOn = scene.getVariable("isOn") ?? false;
scene.setVariable("isOn", !isOn);
});Animate a material parameter via variables:
javascript
var entity = scriptContext.sourceEvent.representationEntity;
// Bind material parameters to variables
entity.bindMaterialParameter("baseColor", "baseColor", { update: "onChange" });
entity.bindMaterialParameter("intensity", "intensity", { update: "onChange" });
// Animate color via variable (material auto-updates)
scene.setVariable("baseColor", Color.hsl(0.6, 0.5, 0.5));
scene.setVariable("intensity", 0.15);
// Smoothly ramp intensity to a target – animateValue paces by
// wall-clock so the ramp takes the same time regardless of frame rate.
// Hand-rolled `lerp on render tick` looks similar but converges twice
// as fast at 120 Hz as at 60 Hz.
animateValue({
from: 0.15,
to: 0.5,
duration: 0.6,
curve: "easeOut",
onUpdate: function (v) { scene.setVariable("intensity", v); }
});Toggle behavior on tap
Use a variable to alternate between two actions on each tap:
javascript
// Attach to: On Experience Start
experience.setVariable("isOpen", false);
var lid = scene.findEntity({ name: "Lid" });
lid.on('tap', function() {
var isOpen = experience.getVariable("isOpen");
if (isOpen) {
lid.animateTo({ rotation: Rotation(0, 0, 0) }, 0.4, { timingFunction: "easeInOut" });
} else {
lid.animateTo({ rotation: Rotation(-Math.PI / 2, 0, 0) }, 0.4, { timingFunction: "easeOut" });
}
experience.setVariable("isOpen", !isOpen);
});Or use on/off to swap between different event handlers:
javascript
// Attach to: On Experience Start
var box = scene.findEntity({ name: "MyBox" });
var currentTap = null;
function listenShrink() {
currentTap = box.on('tap', function() {
box.off(currentTap);
box.animateTo({ scale: Vector3(0.5, 0.5, 0.5) }, 0.3, { timingFunction: "easeOut" });
listenGrow();
});
}
function listenGrow() {
currentTap = box.on('tap', function() {
box.off(currentTap);
box.animateTo({ scale: Vector3(1, 1, 1) }, 0.3, { timingFunction: "easeOut" });
listenShrink();
});
}
listenShrink();State management with variables
Variables live on two scopes:
experience– persists across all scenes. Use for global state like scores, settings, or progress.scene– cleared on scene transition. Use for local state like counters, toggles, or material bindings.
Experience-scoped (persists across scenes):
javascript
// Script A (On Experience Start): initialize state
experience.setVariable("score", 0);
experience.setVariable("level", 1);
// Script B (On Tap Gesture): update state
var score = experience.getVariable("score");
experience.setVariable("score", score + 10);
// Script C (On Variable Change): react to changes
var newScore = experience.getVariable("score");
console.log("Score: " + newScore);Scene-scoped (cleared on scene transition):
javascript
scene.setVariable("localCounter", 0);
var count = scene.getVariable("localCounter");
scene.setVariable("localCounter", count + 1);Raycasting
Cast a ray from the screen center to find objects:
javascript
// Attach to: On Render or On Screen Tap
var center = Vector2(0.5, 0.5);
var ray = scene.screenToRay(center);
if (!ray) return;
var hits = scene.raycast(ray.origin, ray.direction);
if (hits.length > 0) {
var hit = hits[0];
console.log("Hit at: " + hit.position.x.toFixed(2) + ", " + hit.position.y.toFixed(2) + ", " + hit.position.z.toFixed(2));
}Place an object aligned to the surface normal:
javascript
function transformFromNormal(position, normal) {
var n = normal.normalize();
var up = Vector3(0, 1, 0);
var dot = up.dot(n);
if (dot > 0.999) return Transform({ position: position });
if (dot < -0.999) return Transform({ position: position, rotation: Rotation(Math.PI, 0, 0) });
var axis = up.cross(n).normalize();
var angle = Math.acos(dot);
var halfAngle = angle / 2;
var s = Math.sin(halfAngle);
return Transform({
position: position,
rotation: Rotation.quaternion(axis.x * s, axis.y * s, axis.z * s, Math.cos(halfAngle))
});
}
var ray = scene.screenToRay(Vector2(0.5, 0.5));
if (ray) {
var hits = scene.raycast(ray.origin, ray.direction);
if (hits.length > 0) {
var entity = scene.findEntity({ name: "Marker" });
entity.representation.worldTransform = transformFromNormal(hits[0].position, hits[0].normal);
}
}Cast against AR planes (real-world surfaces):
javascript
var ray = scene.screenToRay(Vector2(0.5, 0.5));
var arHits = scene.raycastAR(ray.origin, ray.direction, "existingPlaneGeometry", "horizontal");
if (arHits.length > 0) {
var hit = arHits[0];
console.log("Surface: " + hit.targetAlignment);
scene.createEntity(
createSphere(0.05)
.anchor(Anchor.position(hit.worldTransform.position))
);
}Audio
Play spatial audio on an object:
javascript
var speaker = scene.findEntity({ name: "Speaker" });
await speaker.playAudio("https://example.com/sound.mp3", {
gain: 0.8,
loops: true,
inputMode: "spatial"
});Play audio from a generated buffer:
javascript
// Generate a 440Hz sine wave tone (1 second)
var sampleRate = 24000;
var numSamples = Math.floor(sampleRate * 1.0);
var samples = new Int16Array(numSamples);
for (var i = 0; i < numSamples; i++) {
var t = i / sampleRate;
samples[i] = Math.floor(Math.sin(2 * Math.PI * 440 * t) * 32767 * 0.5);
}
// Convert to base64
var bytes = new Uint8Array(samples.buffer);
var binary = '';
for (var j = 0; j < bytes.length; j++) {
binary += String.fromCharCode(bytes[j]);
}
var base64 = btoa(binary);
var entity = scene.findEntity({ name: "Speaker" });
await entity.playAudioBuffer(base64, { sampleRate: sampleRate });Stream audio in real-time:
javascript
var entity = scene.findEntity({ name: "Speaker" });
var stream = entity.createAudioStream({ sampleRate: 24000 });
// Append base64-encoded PCM16 audio chunks as they arrive
stream.append(base64AudioData);
// Check if buffer has finished playing
if (stream.isDrained) {
console.log("Playback finished");
}
// Stop when done
stream.stop();Microphone input
javascript
// Attach to: On Experience Start
var mic = scene.microphone;
mic.configure({
sampleRate: 24000,
silenceThreshold: 0,
highpassFrequency: 0
});
mic.onData = function(base64Chunk) {
// Process audio chunk (e.g. send to server or WebSocket)
ws.send(JSON.stringify({
type: "input_audio_buffer.append",
audio: base64Chunk
}));
};
mic.onError = function(error) {
console.log("Mic error: " + error);
};
mic.start();
// Later: mic.stop();Face tracking (blend shapes)
Drive a 3D face model with live face tracking data:
javascript
// Attach to: On Render
// Requires a model with blend shape support (e.g. a face-tracking-capable head model)
var headModel = scene.findEntity({ name: "Head" }).representation;
var faces = scene.getAnchors("face");
if (faces.length > 0) {
headModel.setBlendShapeWeights(faces[0].blendShapes);
}Hand tracking (visionOS)
Hand tracking is available on Apple Vision Pro. Gate portable scripts with environment.features.has('handTracking').
Attach an object to the user's hand:
javascript
scene.createEntity(
createSphere(0.02)
.name("HandMarker")
.anchor(Anchor.hand("right", "indexFingerTip", "full", 0.5, true))
);Read hand joint data without placing an object – opt into the provider, then poll the live hands each frame:
javascript
// Attach to: On Experience Start
await scene.tracking.hands.enable(); // resolves when tracking is live; rejects if denied / unsupportedjavascript
// Attach to: On Render
var right = scene.tracking.hands.right; // null when that hand isn't tracked
if (right) {
var tip = right.joints.indexFingerTip.worldTransform.position;
console.log("Right index tip: " + tip);
}scene.tracking.hands owns the provider lifecycle (enable() / disable(), isSupported, active) and the live reads (left / right / anchors).
Joint positions are in world space. To use them against an object you built (a mesh, a placed entity), convert into that object's local frame first – see Coordinate spaces.
scene.getAnchors("hand") remains the low-level equivalent:
javascript
// Attach to: On Render
var anchors = scene.getAnchors("hand");
for (var i = 0; i < anchors.length; i++) {
var hand = anchors[i];
if (hand.chirality === "right") {
console.log("Right hand at: " + hand.worldTransform.position);
}
}Inspect model hierarchy for hand mesh rigging:
javascript
var entity = scene.findEntity({ name: "Glove" });
var rep = entity.representation;
var names = rep.getChildNames();
names.forEach(function(name) {
var child = rep.findChild(name);
console.log(name + " - joints: " + child.jointNames.length);
});Cross-platform surface input
scene.createSurfaceInput gives you interaction points on a surface using whatever input the platform offers – fingertips on Apple Vision Pro (hand tracking), screen taps and drags on iPhone, iPad and Mac. Your code reads the same points either way and never branches on platform.
Read points each frame. Each point is in the surface's own local frame and carries a stable id, a phase (began / moved / ended) and a source (hand / screen):
javascript
// Attach to: On Experience Start
var surface = scene.findEntity({ name: "Surface" });
var input = scene.createSurfaceInput({
target: surface.representation, // points are returned in this representation's local frame
plane: { axis: "y", offset: 0 }, // how a screen ray maps to the surface (a flat plane here)
maxDistance: 0.08 // a fingertip counts as touching within 8 cm of the plane
});
scene.on('render', function () {
input.points.forEach(function (p) {
if (p.phase === 'began') {
console.log('touch started via ' + p.source + ' at ' + p.x.toFixed(2) + ', ' + p.z.toFixed(2));
}
// p.x / p.y / p.z are local to the surface — feed them into your own logic
// (deform a mesh, spawn something, move an object …).
});
});
// When you no longer need it (for example before leaving the scene):
// input.stop();TIP
targetis the representation whose local frame you want, usuallyentity.representation. For a composed object with nested representations, pass the specific one that holds the surface.project: 'plane'(the default) needs no collision – ideal for water, tables, or a deforming mesh. Useproject: 'collision'to raycast a solid model's collision geometry instead.- On Apple Vision Pro the points come from fingertips; on iPhone / iPad / Mac they come from taps and drags. Nothing else in your code changes.
Image tracking
Track a physical image and attach content:
javascript
scene.createEntity(
createBox(0.1, 0.1, 0.01)
.name("ImageOverlay")
.anchor(Anchor.image(
"https://example.com/marker.jpg",
0.15, // physical width in meters
"myMarker",
"any",
true, // track continuously
true, // hide if tracking lost
true // show discovery hint
))
);Render loop patterns
Smooth oscillation:
javascript
// Attach to: On Render
var box = scene.findEntity({ name: "MyBox" });
var t = scriptContext.sourceEvent.time;
box.representation.position = Vector3(
Math.sin(t) * 0.5,
1 + Math.sin(t * 2) * 0.2,
-2
);Frame-rate independent movement:
javascript
// Attach to: On Render
var box = scene.findEntity({ name: "MyBox" });
var dt = scriptContext.sourceEvent.deltaTime;
var speed = 2.0;
var pos = box.representation.position;
box.representation.position = Vector3(pos.x + speed * dt, pos.y, pos.z);Throttled updates for expensive operations:
javascript
scene.on('render', { throttle: 0.1 }, function(e) {
// Runs at 10 FPS instead of every frame
var anchors = scene.getAnchors("plane");
console.log("Planes: " + anchors.length);
});Look at camera (billboard)
javascript
// Attach to: On Render
var label = scene.findEntity({ name: "Label" });
var cam = scene.cameraTransform;
var dir = cam.position.subtract(label.representation.worldPosition).normalize();
var yaw = Math.atan2(dir.x, dir.z);
label.representation.rotation = Rotation(0, yaw, 0);Scene transitions
javascript
// Pass data to the next scene via experience variables
experience.setVariable("selectedLevel", 3);
// Transition (use scene ID from editor)
experience.transitionToScene("SCENE_ID");Platform-specific behavior
Use environment to check the current platform and adapt:
javascript
// Attach to: On Experience Start
var platform = environment.hostingPlatform; // "iOS", "macOS", "visionOS", or "web"
if (platform === "visionOS") {
// Add hand-tracked content
scene.createEntity(
createSphere(0.02)
.name("HandMarker")
.anchor(Anchor.hand("right", "indexFingerTip", "full", 0.5, true))
);
} else {
console.log("Running on " + platform + " (" + environment.systemOSVersion + ")");
console.log("Locale: " + environment.locale);
}Arrange objects in a line
On screen tap, find multiple entities and animate them into a row:
javascript
// Attach to: On Screen Tap
var names = ["BoxA", "BoxB", "BoxC", "BoxD", "BoxE"];
var spacing = 0.4;
var startX = -((names.length - 1) * spacing) / 2;
for (var i = 0; i < names.length; i++) {
var entity = scene.findEntity({ name: names[i] });
if (!entity) continue;
var targetPos = Vector3(startX + i * spacing, 1.2, -2);
entity.animateTo(
{ position: targetPos, rotation: Rotation(0, 0, 0), scale: Vector3(1, 1, 1) },
0.6 + i * 0.1,
{ timingFunction: "easeOut" }
);
}Arrange in a circle:
javascript
// Attach to: On Screen Tap
var names = ["Obj1", "Obj2", "Obj3", "Obj4", "Obj5", "Obj6"];
var radius = 1.0;
var center = Vector3(0, 1.2, -2.5);
for (var i = 0; i < names.length; i++) {
var entity = scene.findEntity({ name: names[i] });
if (!entity) continue;
var angle = (i / names.length) * Math.PI * 2;
var targetPos = Vector3(
center.x + Math.sin(angle) * radius,
center.y,
center.z + Math.cos(angle) * radius
);
// Face center
var yaw = Math.atan2(
center.x - targetPos.x,
center.z - targetPos.z
);
entity.animateTo(
{ position: targetPos, rotation: Rotation(0, yaw, 0) },
0.8,
{
timingFunction: "easeOut",
delay: i * 0.08
}
);
}Procedural spiral with tap bounce
Create boxes in a rainbow spiral. Each box gets an inline unlit material with a unique HSL color. Tap any box to bounce it:
javascript
// Attach to: On Experience Start
var count = 24;
var baseRadius = 0.8;
var heightStep = 0.06;
var turns = 2.5;
async function buildSpiral() {
for (var i = 0; i < count; i++) {
var t = i / count;
var angle = t * Math.PI * 2 * turns;
var radius = baseRadius * (0.3 + t * 0.7);
var size = 0.06 + t * 0.06;
var x = Math.sin(angle) * radius;
var y = 1.0 + i * heightStep;
var z = -2.5 + Math.cos(angle) * radius;
var box = await scene.createEntity(
createBox(size, size, size)
.name("Spiral_" + i)
.anchor(Anchor.position(x, y, z))
);
// Face outward from center
var yaw = Math.atan2(x, z - (-2.5));
box.animateTo(
{ rotation: Rotation(0, yaw, 0) },
0.3,
{ delay: i * 0.03 }
);
// Bounce on tap
box.on('tap', function(e) {
e.representationEntity.animateTo(
{ scale: Vector3(1.5, 1.5, 1.5) },
0.3,
{ timingFunction: "easeOut" }
);
});
}
}
buildSpiral();UI panel with bound slider
Build a small control panel, bind a slider to a scene variable, and read the live value back in a label via ${var|fixed(2)} – the label re-renders automatically whenever the variable changes. See Live labels for the full filter reference.
javascript
// Attach to: On Experience Start
scene.setVariable("brightness", 0.5);
var panel = createPanel(
UI.vStack({
spacing: 16,
padding: 24,
children: [
UI.label({ text: "Brightness: ${brightness|fixed(2)}" }),
// Sliders fill the stack's width by default – no `sizing` needed.
UI.slider({
name: "brightness",
minValue: 0,
maxValue: 1,
value: 0.5
})
]
}),
// Panel size in points (~1360 pt/m). Use 'auto', 'fill', or [w, h].
{ panelSize: [400, 200] }
)
.name("controls")
// Static world anchor in front of the user at eye height.
.anchor(Anchor.position(0.5, 1.4, -0.8));
var entity = await scene.createEntity(panel);
entity.representation.findView({ name: "brightness" }).bind("value", "brightness");Dynamic mesh ribbon
Generate a growing ribbon procedurally: each frame, append two new vertices to the strip, then publish the updated draw range. Vertex writes go through a single memcpy from a reused TypedArray.
javascript
// Attach to: On Experience Start
var MAX_SEGMENTS = 256;
var vertexCapacity = MAX_SEGMENTS * 2; // two verts per segment
var ribbon = await scene.createEntity(
createMesh({
vertexCapacity: vertexCapacity,
indexCapacity: vertexCapacity,
attributes: {
position: { format: "float3", storage: "dynamic" },
color: { format: "uchar4Normalized", storage: "dynamic" }
},
parts: [{
indexCount: 0,
topology: "triangleStrip",
bounds: { min: [-1, -1, -1], max: [1, 1, 1] }
}]
})
);
var mesh = ribbon.representation.mesh;
// triangleStrip walks the vertex buffer in order, so the index buffer
// is just identity. Written once; drawCount expands per-frame.
var indices = new Uint32Array(vertexCapacity);
for (var i = 0; i < vertexCapacity; i++) { indices[i] = i; }
mesh.writeIndices(0, indices);
// Reusable scratch buffers – allocated once, extended lazily as the
// ribbon grows.
var positions = new Float32Array(vertexCapacity * 3);
var colors = new Uint8Array(vertexCapacity * 4);
var written = 0; // high-water mark – each segment's geometry is deterministic
function writeSegment(i) {
var t = i * 0.05;
var x = Math.sin(t) * 0.4;
var y = i * 0.01;
var z = Math.cos(t) * 0.4;
positions[(i * 2) * 3 + 0] = x; positions[(i * 2) * 3 + 1] = y; positions[(i * 2) * 3 + 2] = z;
positions[(i * 2 + 1) * 3 + 0] = x; positions[(i * 2 + 1) * 3 + 1] = y + 0.03; positions[(i * 2 + 1) * 3 + 2] = z;
var hue = (i / MAX_SEGMENTS) * 255;
colors[(i * 2) * 4 + 0] = hue; colors[(i * 2) * 4 + 1] = 200; colors[(i * 2) * 4 + 2] = 255 - hue; colors[(i * 2) * 4 + 3] = 255;
colors[(i * 2 + 1) * 4 + 0] = hue; colors[(i * 2 + 1) * 4 + 1] = 200; colors[(i * 2 + 1) * 4 + 2] = 255 - hue; colors[(i * 2 + 1) * 4 + 3] = 255;
}
// Wall-clock paced reveal via animateValue – same duration at 60, 120,
// 240 Hz. Avoid hand-rolled `segments++` per render tick: that ties the
// reveal speed to the frame rate.
animateValue({
from: 0,
to: MAX_SEGMENTS,
duration: 4.0,
curve: "linear",
onUpdate: function (s) {
var target = Math.floor(s);
// Extend the buffer to cover any newly-visible segments.
for (var i = written; i < target; i++) writeSegment(i);
if (target > written) {
mesh.writeVertices("position", 0, positions);
mesh.writeVertices("color", 0, colors);
written = target;
}
mesh.drawCount = target * 2;
mesh.setBounds({ min: [-0.5, 0, -0.5], max: [0.5, Math.max(0.05, target * 0.01), 0.5] });
}
});Follow a path
Build a spline with scene.createPath and drive an object along it with followPath:
javascript
// Attach to: On Experience Start
var pts = [
new Vector3(-0.6, 0, -1.2),
new Vector3(-0.2, 0.4, -1.2),
new Vector3(0.2, -0.2, -1.4),
new Vector3(0.6, 0.3, -1.2)
];
var path = scene.createPath({ points: pts });
var box = await scene.createEntity(
createBox(0.05, 0.05, 0.05).anchor(Anchor.position(0, 0, 0))
);
box.followPath(path, {
duration: 4,
constantSpeed: true, // even speed regardless of control-point spacing
alignsToTangent: true, // nose points along the direction of travel
timing: 'easeInOut',
repeat: true
});Mesh instancing
Render thousands of copies of one object cheaply with setInstances – far lighter than cloning. Here, a sunflower spiral:
javascript
// Attach to: On Experience Start
var base = await scene.createEntity(
createBox(0.03, 0.03, 0.03).anchor(Anchor.position(0, 0, -1.5))
);
var spots = [];
var count = 1000;
var goldenAngle = Math.PI * (3 - Math.sqrt(5));
for (var i = 0; i < count; i++) {
var r = 0.4 * Math.sqrt(i / count);
var a = i * goldenAngle;
spots.push(new Vector3(r * Math.cos(a), 0, r * Math.sin(a)));
}
base.setInstances(Instances.transforms(spots));Read the device location
Ride the base location grant, read a fix, and measure distance to a point of interest:
javascript
// Attach to: On Experience Start
await environment.location.requestAccess();
var here = await environment.location.current();
console.log('You are at', here.latitude, here.longitude, '±' + here.horizontalAccuracy + 'm');
var munich = new GeoCoordinate(48.1372, 11.5756);
console.log('Distance to Munich:', Math.round(here.distanceTo(munich)), 'm');
console.log('Bearing to Munich:', Math.round(here.bearingTo(munich)) + '°');AI voice assistant (OpenAI Realtime)
Full conversational AI with voice input/output and tool calling:
javascript
// Attach to: On Did Appear (on the assistant entity)
var entity = scriptContext.sourceEvent.representationEntity;
var stream = entity.createAudioStream({ sampleRate: 24000 });
var isResponding = false;
var ws = websocket.connect("wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview", {
headers: {
"Authorization": "Bearer YOUR_KEY",
"OpenAI-Beta": "realtime=v1"
}
});
ws.onOpen = function() {
ws.send(JSON.stringify({
type: "session.update",
session: {
modalities: ["text", "audio"],
input_audio_format: "pcm16",
output_audio_format: "pcm16",
voice: "ash",
turn_detection: {
type: "semantic_vad",
eagerness: "medium",
create_response: true,
interrupt_response: true
},
instructions: "You are a helpful AR assistant. Keep responses brief."
}
}));
startListening();
};
ws.onMessage = function(event) {
var msg = JSON.parse(event.data);
if (msg.type === "response.created") {
isResponding = true;
scene.microphone.stop();
}
if (msg.type === "response.audio.delta") {
stream.append(msg.delta);
}
if (msg.type === "response.done") {
isResponding = false;
// Resume mic after audio finishes
waitForDrain(function() { startListening(); });
}
};
function startListening() {
if (isResponding) return;
scene.microphone.configure({ sampleRate: 24000, silenceThreshold: 0, highpassFrequency: 0 });
scene.microphone.onData = function(base64) {
if (!isResponding) {
ws.send(JSON.stringify({ type: "input_audio_buffer.append", audio: base64 }));
}
};
scene.microphone.start();
}
function waitForDrain(callback) {
if (stream.isDrained) { callback(); return; }
setTimeout(function() { waitForDrain(callback); }, 100);
}