Scene Bundle format

vrata v1 does not read Unity .unity scenes directly. A room can optionally point to a scene bundle manifest URL, and the runtime loads the bundle as a web scene.

For product-quality scene authoring and validator thresholds, see docs/scene-technical-requirements.md. This document describes the runtime compatibility contract; the requirements document is intentionally stricter for new scenes.

Room manifest field

Bundle layout

scenes/the-hall-v1/
  scene.json
  scene.glb
  preview.jpg

Local Validation

Scene authors can validate a bundle directory, scene.json file, or .zip archive before publishing:

pnpm --filter @vrata/asset-pipeline build
pnpm exec vrata scenes validate apps/runtime-web/public/assets/scenes/the-hall-v1
pnpm exec vrata scenes validate apps/runtime-web/public/assets/scenes/the-hall-v1 --json

The validator checks scene.json schema basics, relative path safety, spawn points, referenced main scene assets, optional preview files, optional material override maps, zip entry safety, and basic file size budgets. Human output is intended for local authoring; --json returns { ok, issues, stats } with stable code, path, and message fields for CI or upload workflows.

Example CI gate:

pnpm --filter @vrata/asset-pipeline build
pnpm exec vrata scenes validate path/to/scene-bundle --json

Upload And Publish Flow

Control-plane admins can publish .zip scene bundles from /control-plane:

The upload endpoint is POST /api/scene-bundles/uploads with multipart/form-data fields:

Successful responses return the stored SceneBundleRecord plus validation warnings/stats. Invalid bundles return 400 with error: "scene_bundle_validation_failed" and validator issues containing stable code, path, and message fields. Uploads require the scene-bundle.write control-plane permission.

Storage modes:

scene.json

Required fields:

Optional fields:

baked-pbr-v1 material transport

renderProfile: "baked-pbr-v1" combines baked diffuse lighting with standard glTF PBR materials and environment reflections. The GLB transports an sRGB lightmap as emissiveTexture using texCoord: 1; every affected mesh must have TEXCOORD_1. Material extras identify the conversion:

The last field describes the atlas encoding, not a scene-specific visual override. It must be boolean when present. An incompatible Three.js physical shader is rejected during loading before materials are changed. If a separate custom material hook removes the physical shader include, the existing shader is retained and the material reports fallback instead of claiming the correction applied.

sceneDebug.materialSamples reports lightMapIntensity and bakedEnvironmentMode (legacy, pending-compile, specular-only, fallback). New complete-irradiance candidates must demonstrate specular-only after their visible materials compile and must still pass actual source/browser image review. Preserve the atlas dynamic range and record its encoding/decoding convention; neither a large intensity nor this metadata repairs clipped lightmap pixels.

Logical state for scene-owned media surfaces

When issuing a room session token, the API reads media surface identities from the room's stored scene bundle binding. It signs a bounded sceneMediaSurfaces claim containing IDs, labels and allowed object types. Join-request fields cannot supply this claim. Room-state verifies the token and registers missing logical surfaces before its first snapshot, so a custom ID such as workspace-main supports shared objects on the normal room path.

Registration is additive: existing surfaces and active objects survive reconnects; historical default surfaces are retained. This does not change existing surface policy, delete old objects, or replace transforms from the client-side manifest. The normal command permission and object-type checks still apply to new surfaces. Both legacy surfaceId and F3 id definitions are supported. If a manifest cannot be read or its definitions are invalid, no new surfaces are granted and historical defaults remain available. The runtime's physical/logical mismatch diagnostics expose any resulting missing binding.

attributions

Scene bundles can declare visible credit records for source assets used by the scene. The runtime shows them in a separate HUD block after the scene bundle loads.

Each entry describes one credited source work:

mediaSurfaces

Scene bundles can override the built-in fallback media surface meshes without rebuilding the runtime image. The scene owns only physical visual surfaces: plane identity, placement, size, pixel projection, front face, and physical input reachability. Room/template state owns logical purpose, labels, visibility policy, active objects, and allowed object types.

Each entry describes one runtime-visible surface:

All numeric values must be finite. Physical and pixel dimensions must be positive. A new F3 mediaSurfaces array must be non-empty and contain exactly one surface with surfaceId: "debug-main" for current runtime compatibility. Duplicate ids are invalid in every format.

If mediaSurfaces is omitted, scene loading fails, or scene bundles are disabled, the runtime keeps the built-in fallback layout with the main wall screen, whiteboard wall, and laptop screen. If mediaSurfaces is present, only those scene-declared physical meshes are selectable, visible in media-surface diagnostics, and eligible for surface input. A logical room-state surface without corresponding scene geometry does not alias to debug-main.

Deprecated legacy v1 form

Runtime and asset validation continue to accept the visual mediaSurfaces form shipped in Scene Bundle v1 before the F3 projection. Existing bundles can use surfaceId, widthM, heightM, optional widthPx/heightPx, label, kind, visible, allowedObjectTypes, and transform: { x, y, z, yaw?, pitch?, roll? }.

Example:

{
  "schemaVersion": 1,
  "sceneId": "sense-hall-v1",
  "label": "Sense Hall",
  "source": "sensetowervr",
  "glbPath": "scene.glb",
  "renderMode": "clean",
  "renderProfile": "neutral-pbr",
  "spawnPoints": [
    {
      "id": "main",
      "position": { "x": 0, "y": 0, "z": 6 },
      "yaw": 3.141592653589793
    }
  ],
  "mediaSurfaces": [
    {
      "surfaceId": "debug-main",
      "representation": "platform-runtime-plane",
      "position": { "x": -3.4, "y": 1.55, "z": 0.15 },
      "yaw": 1.5707963267948966,
      "widthM": 3.2,
      "heightM": 1.8,
      "pixelDimensions": { "width": 1920, "height": 1080 },
      "frontFace": "local-positive-z",
      "input": { "enabled": true, "maxDistanceM": 0.05 }
    },
    {
      "surfaceId": "whiteboard-wall",
      "representation": "platform-runtime-plane",
      "position": { "x": 3.4, "y": 1.5, "z": 0.5 },
      "yaw": -1.5707963267948966,
      "widthM": 2.4,
      "heightM": 1.25,
      "pixelDimensions": { "width": 1920, "height": 1000 },
      "frontFace": "local-positive-z",
      "input": { "enabled": true, "maxDistanceM": 0.05 }
    }
  ],
  "bounds": { "width": 24, "height": 8, "depth": 24 },
  "preview": "preview.jpg",
  "attributions": [
    {
      "title": "Old Room",
      "author": "Hansalex",
      "authorUrl": "https://sketchfab.com/Hansalex",
      "source": "https://sketchfab.com/3d-models/old-room-6173a3c88c384f768dfc80967b6527b4",
      "license": "CC-BY-4.0",
      "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
      "changes": "Normalized to meters and optimized for Vrata runtime."
    }
  ],
  "notes": "v1 export without Unity behaviors"
}

Runtime behavior

Source: docs/scene-bundle-contract.md in vrata-labs/platform at revision 380b2e7ff1dd, license Apache-2.0. GitHub