Multi-scene experiences
Real 3D apps rarely show one fixed scene. A game moves the player between levels; a configurator or product viewer swaps models in and out; an architectural walkthrough crosses rooms or floors. Uptimizr treats all of these as scenes within one session — you keep a single tracker running and tell it when the active scene changes.
Why one session, not many
Section titled “Why one session, not many”Stopping and restarting tracking on every level/model change would fragment the visit into
disconnected sessions and break replay continuity (the timeline must stay ordered and
sessionId-keyed). Instead, keep one trackScene(...) client alive for the whole visit and mark
the transitions. Each scene becomes a segment of the same session, so you can:
- compare heatmaps, dwell, and performance per level / per model, and
- still replay the entire visit end to end as one ordered stream.
The two calls
Section titled “The two calls”Set the initial scene id when you start, then call setScene(id) every time the active scene
changes.
const client = trackScene(scene, { projectId, endpoint, meta: { sceneId: "level-1" }, // initial scene / area / model});
// Later, when the next scene loads:client.setScene("level-2");setScene emits an ordered scene_change marker and stamps the new sceneId onto every
subsequent event, so gaze/click heatmaps, mesh interactions, performance, and custom events are
all attributable to the scene that was active when they happened. Filter any read endpoint by the
scene query param to get per-scene results.
Patterns
Section titled “Patterns”Call setScene from your level-loaded hook, after the new level is live:
async function loadLevel(id: string) { await engine.loadLevel(id); client.setScene(id); // e.g. "level-3", "boss-arena"}Use stable, low-cardinality ids (the level’s slug), not a per-playthrough guid — that keeps aggregates meaningful across visitors.
When the visitor switches the displayed model, treat each model as a scene:
function showModel(sku: string) { viewer.load(sku); client.setScene(sku); // e.g. "sku-chair-walnut"}Now each model has its own view-direction and click heatmaps even though they share one canvas and one session.
For a continuous walkthrough, fire setScene on the trigger that marks entering a new area
(portal volume, teleport, floor change):
onRoomEnter((room) => client.setScene(room.id)); // "lobby", "gallery-2", "roof"Querying per scene
Section titled “Querying per scene”Most read endpoints accept a scene filter, so you can scope an aggregate to a single
level/model/area. Reads are scoped to the project behind your x-api-key — there is no
projectId query param:
curl "$ENDPOINT/api/v1/meshes/dwell?scene=level-2" \ -H "x-api-key: $KEY"Omit the filter to see the whole session across all scenes, and use /api/v1/scenes to list the
distinct scenes that have activity. See the query endpoints for the full
parameter list.
Naming places inside a scene: regions
Section titled “Naming places inside a scene: regions”A sceneId names a whole area; a region names a place inside one — “the entrance”, “the
checkout counter”. A region is a labelled axis-aligned box in the scene’s world space, stored next
to the scene proxy, and it does two things: it gives dashboards, summaries and agents a shared
vocabulary instead of raw coordinates, and it lets any spatial endpoint be drilled into that place
by name with ?region=<id>.
Declare a scene’s regions once (they replace the stored set, so leaving one out removes it):
import { registerRegions } from "@uptimizr/sdk-core";
await registerRegions( "lobby", [ { id: "entrance", label: "Entrance", bounds: [-5, 0, -5, 5, 3, 0] }, { id: "counter", label: "Checkout counter", bounds: [-1, 0, 1, 1, 2, 3] }, ], { endpoint: ENDPOINT, apiKey: process.env.UPTIMIZR_API_KEY! },);Then read any spatial aggregate for just that place:
curl "$ENDPOINT/api/v1/heatmaps/world?scene=lobby®ion=counter" \ -H "x-api-key: $KEY"Regions may overlap (an enclosing hall and a counter within it), so a point can belong to several. Where they do, membership is every containing region and the smallest by volume is the one reported — a click on the till is “in the counter”, not “in the hall”.
Why it is worth the five minutes
Section titled “Why it is worth the five minutes”Regions are the vocabulary an agent answers in. Ask for a summarised spatial query
(format=summary) on a scene that has regions and a proxy, and every hotspot comes back labelled
with the place it is in and the object it sits on, instead of a voxel index:
// GET /api/v1/heatmaps/world?scene=lobby&format=summary — one cluster{ "centroid": [7, 2, 11], "region": "counter", "regions": ["counter", "shop-floor"], "nearestMesh": "checkout_button", "distance": 0, "drill": { "region": "counter" },}The reading sentence uses them too — “the densest spans 3x2x3 voxels on checkout_button in
region counter” — and drill.region hands back the region id, so the follow-up query (“show
me just that place”) is one parameter away. Without regions the same answer is a coordinate;
without a proxy nearestMesh is null and the summary says so in its caveats. The dashboard’s 3D
panels use the same pair to name a voxel on hover. See
Labelled spatial clusters.
Registering regions is an authenticated write — like the scene-proxy upload it uses your project
API key, which must hold the annotate capability (uptimizr new-key <projectId> --capabilities annotate), so do it from a build script, a server-side route, or a developer-only path, never from
a public bundle. There is also a CLI, which writes straight to the store and needs no key:
uptimizr regions set lobby --file regions.json. See
Scene regions.
In the dashboard
Section titled “In the dashboard”The 3D panels (world heatmap, gaze heatmap, click rays) draw your scene geometry as a backdrop. By default — with the Scene filter on “All scenes” — the dashboard shows the whole building: it merges every active section’s scoped proxy into one backdrop, so every level and far-flung area is present at once. Pick the Scene dropdown in the filter bar to focus a single section — that anchors the backdrop to just that area’s geometry and scopes the heatmap data to it.
Showing the whole building is deterministic: the full multi-level world renders immediately, rather than sections appearing one at a time as a live visitor crosses boundaries. When the live layer is on, crossing into a newly-active section simply refreshes the merge so the new area joins the backdrop promptly; the heatmap data stays across all scenes, so the aggregate isn’t re-scoped underneath you. Session replay follows the same rule — it renders the whole building for the session’s time window so the entire walkthrough plays against full geometry. A mesh that bridges two areas (a ramp or stairway) belongs to one section’s proxy but is always visible in the merged whole-building view.
Replay across scenes
Section titled “Replay across scenes”Because the transitions are recorded as ordered scene_change markers in the same stream, session
replay re-drives the visit through every scene change in order — your replay
target can switch levels/models in lockstep by reacting to the scene_change events.
Related
Section titled “Related”- Custom events & input — the rest of the client API (
track,trackInput). - Sessions & lifecycle — how a session starts, flushes, and ends.
- Query endpoints — filtering aggregates by
sceneId.