Skip to main content

Digital Twin Viewer Reference

How the domain viewer builds its 3D scene: the world frame, both camera modes with their exact mouse and keyboard mappings, the first-person rules, and what each layer loads and draws. Every value here was read from the viewer's source, not from its README — use it when you (or your agent) build a viewer of your own or reason about what's on screen.

Source

Read from domain-viewer at feature/organization-logo @ 8f48c94 plus uncommitted changes, checked 13 Oct 2026 · three 0.182.0 · React Three Fiber 9.5.0 · drei 10.7.7 · Spark 2.0.0. Anything marked in development describes code that isn't committed yet.

Try it​

A live example scene built with the same frame, camera, controls and layer styling as the viewer, drawn from a synthetic shelving maze around a central plaza instead of a real domain. Click the scene to focus it, then use the controls below — press F for first person.

The example draws the Portals, Nav Mesh, Occlusion and Point Cloud layers as the viewer does, plus a Devices layer specific to this example: two robots, a phone and a pair of smart glasses at scripted poses, updated at 10 Hz and drawn as received. Each device shows its camera frustum and a floor ring in its own colour. There's no splat (Rendering) or local model (Model). Auto-rotate runs at the viewer's 60 Hz speed but is time-based here, so it doesn't speed up on high-refresh displays (see Gaps).

Domains​

A domain is the digital representation of a physical space — the thing the viewer loads when you type a domain ID. It's hosted on a domain server and holds the space's portals plus the data built from scans of it: the point cloud, nav mesh, occlusion mesh and splats drawn in Layers. Portals are printed QR markers that act as access points to a domain: a device that sees one can work out where it is in the domain's coordinate frame.

You create and manage domains with the Domain Management Tool (DMT). Follow the DMT user guide → for the full walkthrough. In short:

  1. Install the DMT — iOS 16+ (Android is returning) — and create an account in the app or the posemesh console.
  2. Get portals. Order a portal kit for a durable install, or generate portals in the console or the app and print them yourself at 100% scale, so the printed size matches the size the domain expects.
  3. Place them at likely entry points to the space. Avoid reflective surfaces and sharp shadows, which interfere with detection.
  4. Create the domain in the DMT and give it a name — the name is public to apps built on the Auki Network.
  5. Pick a domain server to host it: your own, a public one provided by Auki, or one run by the community.
  6. Add the portals to the domain, with one of two flows:
    • Manual flow, for smaller spaces: place portals directly. The first one goes on the floor and sets the domain's coordinate system.
    • Capture flow, for larger spaces (iOS only): start on a floor portal to set the ground plane, then record the space while keeping portals in view. For large spaces, record several captures; each must include at least two portals from earlier captures. Upload the captures, send them to a refinement server, and apply the result to the domain.

Once a domain has portals, and refinements if you ran the capture flow, open it in the domain viewer by its domain ID: the Loading section below describes what it fetches, and World describes the frame it draws the domain in.

At a glance​

Units & framemetres, +Y up — right-handed, three.js convention
Cameraperspective, 50° FOV — starts at (15, 15, 15), looking at the origin
ModesMap · First person — press F to switch
Eye height1.6 m, fixed — first person only
Walk / sprint3 m/s · 6 m/s — hold Shift to sprint
Auto-rotateafter 5 s idle — map mode, once a domain is loaded
Floor grid0.2 m cells · 1 m sections — infinite, fades out by 500 m
LayersPortals, Nav Mesh, Occlusion, Point Cloud, Rendering, Model — all on by default

World​

components/3d/Scene.tsx · FloorGrid.tsx · OriginLines.tsx

Domain coordinates map one to one onto scene coordinates: one unit is one metre and +Y points up. The floor is the plane y = 0. The plan below is drawn to scale, looking down the Y axis.

Plan view of the scene origin and camera start pointsTop-down view at 20 pixels per metre. The map camera starts at x 15, z 15, 15 metres up, looking back at the origin. First person starts at x 0, z 3. A dashed circle of 5 metres marks the minimum auto-rotate radius, and an arc of 21.2 metres marks the auto-rotate orbit from the default view.−5−5005510101515x (m)z (m)+X+Z+Y up, toward youFirst-person start (0, 1.6, 3)Map camera start (15, 15, 15)looks at the origin5 m: minimum auto-rotate radius21.2 m: auto-rotate orbit from the default view
Grid lines here are the 1 m sections; the scene also draws 0.2 m cells, left out of this plan for legibility. The axis lines in the scene are 1 m long, as drawn.
Origin axesThree 1 m lines from the origin, 2 px wide: X #dc2626, Y #84cc16, Z #2563eb.
Floor griddrei Grid, infinite, on y = 0. Cells every 0.2 m, sections every 1 m, both #404040 in the dark colour scheme and #c0c0c0 in light. Fades to nothing by 500 m. Drawn first (renderOrder −100) and double-sided, so it stays visible from below the floor.
LightingAmbient light at 0.5 plus a directional light at 0.5 from (10, 100, 10). The point cloud, both meshes and the splats use unlit materials, so lighting only shapes the GLB models: the portal QR markers and the local model.
CanvasTransparent, over the viewer's dark neutral background. preserveDrawingBuffer is on so Share → Screenshot can read the pixels back.
ResolutionWhile a splat is in the scene, Spark's renderer caps the device pixel ratio at 1.0 and restores it when the splat unmounts. On a 2× display the whole scene renders at half resolution whenever a domain has a splat.

Camera​

components/Viewer3D.tsx · PersistedMapControls.tsx · 3d/controllers/CameraController.tsx

ProjectionPerspective, 50° vertical field of view. Near 0.1 m and far 1000 m, the React Three Fiber defaults, so anything beyond 1 km is clipped.
Start(15, 15, 15), looking at the origin, which is also the starting orbit target.
Map controlsthree.js MapControls with damping 0.05, so motion eases out after you let go. Elevation is limited to 0–90°: the camera can look straight down but can't orbit below the floor. Zoom has no minimum or maximum distance.
Pose memoryAbout every half second (every 30 frames) map mode saves position, rotation, zoom and target into an in-memory store. When map controls mount again, for example coming back from first person, they restore the saved target. Nothing is written to browser storage, so a reload starts from the default view.
Auto-rotateAfter 5 s without a pointer press, wheel or key press, the camera circles the current target at its current height. The radius is the current horizontal distance, never less than 5 m; from the default view that's 21.2 m. It's off in first person and waits until a domain has loaded.
Rotation speedThe camera advances a fixed 0.0015 rad per rendered frame, so the speed follows the display's refresh rate: about 70 s per turn at 60 Hz and 35 s at 120 Hz.
Turning it offRendering panel → Auto-rotate. Off stops the drift immediately. Recommended when watching live data, where a moving camera makes it hard to tell scene motion from viewpoint motion. In development.

Controls​

There are two camera modes. Map is the default; F switches between them.

InputWhat it does
Left dragPan. The view slides across the floor plane, not the screen plane, so dragging up moves you forward over the ground.
Right dragOrbit around the target. Stops at the horizon and at straight down.
WheelZoom (dolly) toward the orbit target. It doesn't zoom toward the cursor.
Middle dragZoom, same as the wheel.
Ctrl / ⌘ / Shift + left dragOrbit instead of pan. Handy on a trackpad without a right button.
TrackpadTwo-finger scroll zooms. A two-finger click-drag acts as a right drag and orbits.
TouchOne finger pans. Two fingers zoom and orbit together.
FSwitch to first person. Ignored when the viewer runs inside an iframe.
EscClose the open panel. In the domain ID field, clear the field.
Arrow keysNothing in map mode. Keyboard panning isn't wired up.

First person​

components/FPSControls.tsx · drei PointerLockControls

Where you startEntering first person always moves the camera to (0, 1.6, 3): 3 m in front of the origin at standing eye height. It keeps the map camera's orientation, so you start facing wherever the map view pointed.
Eye heightY is reset to 1.6 m every frame. There's no jumping, crouching, flying, gravity or collision. You walk through walls, meshes and splats.
Looking up or downForward and back follow the view direction with the vertical part thrown away, so ground speed is the walk speed × cos(pitch): full speed looking level, about 71% at 45°, nothing at all looking straight down. Stepping left and right isn't affected.
Speed3 m/s walking, 6 m/s with Shift, scaled by frame time, so it's the same at any refresh rate. Diagonal movement is normalised and isn't faster.
While in itAuto-rotate is off. Mouse look comes from drei's PointerLockControls at its default sensitivity.
LeavingMap controls take over from where you stand. The orbit target is set 10 m ahead along your view and the camera's up vector is reset, so the view doesn't tilt or jump back to the old map pose.
The overlayThe click-to-enter overlay lists W A S D, Mouse and Shift. The arrow keys work too but aren't mentioned there.

Layers​

Layers panel · store/visualizationStore.ts · components/3d/renderers/

Each layer reads its own visibility switch, so toggling one never redraws another. The coloured dot beside each row in the Layers panel only identifies the row — it isn't the colour the layer renders in, so the table lists both.

LayerDefaultSourceDrawn as
PortalsOnGET …/lighthousesOne copy of QR.glb per portal, placed at the portal's pose and scaled to its printed size (reported_size in cm ÷ 100).
Nav MeshOnOBJ named navmesh_v1Unlit #2b4d2b at 90% opacity, double-sided, pulled slightly forward in depth so it sits on top of floor surfaces it shares.
OcclusionOnOBJ named occlusionmesh_v1Unlit #808080 faces at 80% opacity plus #303030 wireframe edges.
Point CloudOnPLY refined_pointcloud_{refinement}Points coloured from the PLY, 0.09 m across in world space and smaller with distance. Placed through the canonical refinement's alignment matrix.
RenderingOnSplat tiles or one splat file for the canonical refinementGaussian splats through Spark 2.0. Listed only when the domain has a splat. See Splats.
ModelOn · in developmentLocal file /models/camera_test.glbOne GLB at a fixed transform, movable with a gizmo. See Model.

Loading​

app/actions.ts · services/domainService.ts · hooks/useRefinementSplat.ts

What happens between typing a domain ID and seeing the scene. These steps run in order.

  1. A server action authenticates the app with the API server, then asks DDS for a domain token (POST /api/v1/domains/{id}/auth). DDS answers with the URL of the domain server that hosts the domain. The app secret never reaches the browser.
  2. The browser fetches the portals (…/lighthouses) and the domain's data manifest (…/data) from that domain server.
  3. It downloads domain_metadata, which names the canonical refinement and carries its alignment matrix. The point cloud and splats are looked up by that refinement ID.
  4. Each layer's bytes come from …/data/{fileId}?raw=true and are parsed in the browser: PLY for the point cloud, OBJ for the meshes.
  5. Splat files download last, one tile after another. The "Acquiring domain" overlay stays up until they have all arrived, then fades.

See the API Reference for the DDS and domain server endpoints.

Splats​

components/3d/RefinementSplat.tsx · spark-r3f/ · hooks/useRefinementSplat.ts

Tiled splatsData types splat_partition or splat_partition_sog, named splat_partition_{full|coarse|fine}_{size}_{x}_{z}_{refinement}. If any coarse or fine tiles exist the viewer loads coarse and fine and ignores full; otherwise it loads full.
Single fileUsed when there are no tiles. Data type is one of splat_data, splat_data_sog, refined_splat, splat or gaussian_splat, named refined_splat_, splat_ or gaussian_splat_ followed by the refinement ID.
FormatTypes ending in _sog load as SOG (Spark's pcsogszip). Everything else loads as .splat.
PlacementEach tile sits at ((x + 0.5) × size, 0, (z − 0.5) × size), and the splat is rotated 180° about X, which flips Y and Z. No alignment matrix is applied.
RevealThe "Magic" shader effect plays over 3 s, once per load. Hiding and showing the layer doesn't replay it.
RendererA Spark SparkRenderer is added to the scene explicitly, with level of detail on, maxStdDev √5 and minPixelRadius 2.
Detail sliderRendering panel → Detail, 0.25× to 4× in steps of 0.05, default 1×. It sets Spark's splat budget (lodSplatScale, which only matters above the platform budget, about 2.5 M splats on desktop) and the inverse minimum pixel size (lodRenderScale = 1 ÷ detail), so it has a visible effect on small scenes too. Changes apply live.

Model​

components/3d/renderers/ModelRenderer.tsx · modelConfig.ts · ModelSettings.tsx · In development

Filepublic/models/camera_test.glb, served from the app, not the domain. If the file is missing or broken, the layer logs a warning and the rest of the scene keeps rendering.
Start transformPosition (0.3962, 1.3418, 2.8246) m, rotation (0, 0, 0), scale 1.
Model panelGizmo on or off, with Move, Rotate and Scale modes. Readouts show position in metres, rotation in degrees and scale. The panel hides itself when there's no model loaded.
Keeping a placementCopy puts the current transform on the clipboard as the TypeScript constants in modelConfig.ts. Paste them there to keep the placement across reloads. Reset returns the model to the configured transform.
While draggingMap controls pause while a gizmo handle is held, so dragging the model doesn't also move the camera.

Embedding​

app/[id]/preview/page.tsx · components/DomainShare.tsx

Preview route/{domainId}/preview shows the same scene with no control bar. Share → Copy Embed produces a 1200 × 630 iframe pointing at it.
Inside an iframeF is ignored, so embeds stay in map mode. Mouse and touch controls still work.
Opened directlyThe preview route outside an iframe still hides the control bar, but F works.

Gaps​

Where the docs and the code disagree, and what isn't verified.

README · mouse buttons are reversed

The README says left drag rotates and right drag pans. The viewer uses three.js MapControls, where left drag pans and right drag orbits.

README · camera state isn't persisted across reloads

"Persisted camera state" means an in-memory store. It survives switching modes, not reloading the page.

UI · layer dots aren't the render colours

The Nav Mesh dot is amber but the mesh is dark green; Occlusion shows red but draws grey. Someone matching the panel to the scene by colour will be misled.

UI · first-person hint leaves out the arrow keys

The overlay lists W A S D, but the arrow keys work identically.

Behaviour · auto-rotate speed depends on refresh rate

The step is per frame, not per second, so a 120 Hz display turns twice as fast as a 60 Hz one.

Unverified · layer alignment

The point cloud goes through the canonical refinement's alignment matrix. The nav mesh, occlusion mesh and splats are drawn without it. That's correct if those assets are already in the domain frame, but it hasn't been checked on a domain whose matrix isn't the identity.

References​