Skip to content

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 name

Explore 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
        .traits(function(t) {
            return t.opacity(0.8).build();
        })
);

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))
        .traits(function(t) {
            return t.fittingBox(0.5).build(); // 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, height in meters
        .anchor(Anchor.currentPOV(0, 0, -1.5))
);

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&current=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. ARKit-compatible face mesh)
var headModel = scene.findEntity({ name: "Head" }).representation;
var faces = scene.getAnchors("face");

if (faces.length > 0) {
    headModel.setBlendShapeWeights(faces[0].blendShapes);
}

Hand tracking (visionOS)

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:

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

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] });
    }
});

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