Dynamic Objects
Dynamic Objects are objects in your scene that move, or objects that you want to collect heat map data on. The SDK records their position, rotation, and scale over time so you can track them on your dashboard.
The Three-Step Workflow
Whichever engine you're on, the conceptual flow is the same:
-
Upload the object's 3D model. The dashboard needs geometry for the object so it can visualize data against it. Export
scene.gltf/scene.bin/settings.json/ a thumbnail for the object (via your adapter'sexportObjecthelper or your own pipeline) and upload through the Cognitive3D Upload Web App using your Developer Key. This is a one-time step per mesh. -
Identify the object in your scene. Attach a stable ID to the object — either as engine-specific metadata (e.g. Three.js
userData.c3dId) so the adapter can find it automatically, or by tracking the ID yourself in application state. -
Register the object at runtime. Call
c3d.dynamicObject.registerObjectCustomId(...)when the object enters the scene (on spawn, on level load, on scene activate). Registration tells the backend what this object is and where it starts, and links the live instance to the uploaded model.
Once registered, snapshots (addSnapshot) keep the backend up to date as the object moves. Adapters with a full integration run this automatically; without an adapter you call it yourself each time the object's transform changes.
Core API
All of the following live on c3d.dynamicObject. They are framework-agnostic.
Register an Object with a Known ID
Use this when the mesh is already uploaded to the dashboard and has a known ID you want to track against.
c3d.dynamicObject.registerObjectCustomId(
name, // Display name, e.g. "PlayerCar"
meshname, // Mesh identifier that matches the upload, e.g. "car_mesh"
customid, // Unique ID that matches the uploaded object id
position, // [x, y, z] initial world position
rotation, // [x, y, z, w] initial world rotation (quaternion)
scale, // Optional. [x, y, z] initial scale
fileType // Optional. "gltf" by default
);
This registers the object and records its initial snapshot, so you only need to add a second snapshot once the transform actually changes.
Register an Object with a Generated ID
If you don't have a stable dashboard ID for the object, use registerObject. It generates a deterministic UUID (v5) using the format sceneId::name::meshname within a specific namespace. This ensures that the same logical objects receive identical IDs across different sessions, allowing for cross-session behavioral analytics out-of-the-box without needing to supply custom IDs.
const generatedId = c3d.dynamicObject.registerObject(
"Enemy_orc_01",
"orc_mesh",
position,
rotation,
scale // optional
);
Add a Snapshot
Tell the backend the object has moved:
c3d.dynamicObject.addSnapshot(objectId, position, rotation, scale, properties);
scale and properties are optional. Adapters with a full integration (Three.js, Mattercraft) call this automatically every frame for any object whose position / rotation / scale crosses a configured threshold; without an adapter, call it when you know the object changed.
Remove an Object
When an object leaves the scene for good (destroyed, despawned, collected):
c3d.dynamicObject.removeObject(objectId, position, rotation);
Refresh the Manifest
Re-sends the registration information for every tracked object. Called automatically by c3d.setScene(...); call it yourself if you need to force a resync.
c3d.dynamicObject.refreshObjectManifest();
Standard (Automatic) Dynamic Objects
The SDK automatically registers left/right controllers and hands as dynamic objects. You do not need to register these manually.
This automatic tracking provides:
- Per-Frame Snapshots: Captures position, rotation, and per-frame button state snapshots.
- Controller Typing: Includes
controllerTypeand hardware properties. - Fallback Profiles: Automatically assigns a fallback profile for any unrecognized WebXR controllers.
- Reliable Swapping: Uses per-frame polling to reliably catch Quest controller ↔ hand swaps without relying on event-driven input detection dropping state.
Object Engagements
Engagements describe how a participant interacts with a dynamic object over a period of time — grabbing, pointing at, being near. They show up on the dashboard as timed states attached to the object, which is more informative than a one-off custom event when the interaction has real duration.
c3d.dynamicObject.beginEngagement(objectId, engagementTypeName, parentObjectId);
c3d.dynamicObject.endEngagement(objectId, engagementTypeName, parentObjectId);
- objectId — the ID of the object being engaged with.
- engagementTypeName — a name of your choosing:
"grab","point","proximity", etc. - parentObjectId — the object doing the engaging. For a grab this is usually the controller or hand ID.
Example:
function onGrabStart(obj) {
// your engine's grab handling...
c3d.dynamicObject.beginEngagement(obj.id, "grab", "right_hand");
}
function onGrabEnd(obj) {
// your engine's release handling...
c3d.dynamicObject.endEngagement(obj.id, "grab", "right_hand");
}
You can run multiple engagements on the same object concurrently (a participant can point at an object they are also near). Each begin needs a matching end with the same engagementTypeName and parentObjectId.
Three.js-Specific: Tagging Objects
The Three.js adapter looks for two keys on userData:
const myObject = new THREE.Mesh(geometry, material);
myObject.name = "MyTrackedObject";
myObject.userData.isDynamic = true; // flags the object for auto-tracking
myObject.userData.c3dId = "your-unique-id"; // matches the uploaded object id
// Optional movement thresholds that control snapshot frequency
myObject.userData.positionThreshold = 0.01; // metres
myObject.userData.rotationThreshold = 0.5; // degrees
myObject.userData.scaleThreshold = 0.05; // scale units
Once tagged, tell the adapter where to look by passing a root (a THREE.Scene, a THREE.Group, a single Object3D, or an array) as the third argument to c3dAdapter.startTracking(renderer, camera, interactableGroup). Objects spawned after startTracking can be added with c3dAdapter.addInteractable(object). Full walkthrough in the Three.js Integration page.
Raycast Behavior (Adapter-Driven)
When the adapter's gaze raycaster hits an object that's part of your tracked set:
- If the object has a dynamic object ID, the hit is recorded in the object's local coordinates, which is what powers per-object heatmaps.
- If the object doesn't have an ID (static scenery), the hit is recorded in world coordinates against the scene.
This happens automatically on Three.js and Mattercraft. The other adapters record gaze against the world but don't currently raycast against individual dynamic objects.
If you have a question or any feedback about our documentation please use the Intercom button (purple circle) in the lower right corner of any web page or join our Discord.