mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-24 15:01:45 +02:00
8.4 KiB
8.4 KiB
FramePort room files (mixed reality)
Quest MR games read a room model (floor, ceiling, walls, furniture, a global mesh) through Meta's scene APIs. The
Steam Frame has no such data for apps (see docs/FRAME_RUNTIME.md "Room data"), so FramePort scans the room itself
(fp_roomscan) and FrameBridge (native/adapter/scene_emu.c) serves it to games. This file is the contract between
the scanner, the Frame agent, the PC app and FrameBridge.
Coordinates
- Everything is in STAGE space = SteamVR's standing universe = OpenXR
XR_REFERENCE_SPACE_TYPE_STAGE: metres, right-handed, +Y up, floor at y = 0, -Z = the universe's forward. - A room belongs to one chaperone universe (
universeline). The Frame sometimes starts a new universe without relocalizing; then the stored room may be misaligned (the agent reportsaligned: false, nothing breaks).
room.txt (UTF-8 text, one record per line, # starts a comment, fields separated by spaces)
frameport-room 1 # magic + format version (required, first line)
universe <decimal u64> # chaperone universe id the room was scanned in (0 = unknown)
source scan|boundary|box|manual # how it was made
time <unix seconds>
room <uuid> # the room anchor
floor <uuid> <y> # floor height (normally 0)
ceiling <uuid> <y> # ceiling height
outline <n> x0 z0 x1 z1 ... x(n-1) z(n-1)
# floor polygon, n >= 3, not closed, POSITIVE signed area:
# sum(x[i]*z[i+1] - x[i+1]*z[i]) > 0 (turning from +X towards +Z; seen from
# above with -Z forward that is clockwise). Readers reverse a negative outline.
wall <uuid> x0 z0 x1 z1 <y0> <y1> [LABEL]
# one wall face from (x0,z0) to (x1,z1), bottom y0, top y1.
# Its normal points INTO the room: walls follow the outline's order, and the
# inward normal is (-dz, dx)/|d| for d = (x1-x0, z1-z0).
# LABEL (optional, default WALL_FACE): INVISIBLE_WALL_FACE for an edge the
# scanner closed without seeing a wall (open side of a room, doorway gap).
volume <uuid> <LABEL> cx cz top_y yaw_deg w d h
# furniture box: top-face centre (cx, top_y, cz), yaw about +Y in degrees
# (0 = local X along world X), width w along local X, depth d along the
# horizontal local axis perpendicular to it, height h downwards from the top
plane <uuid> <LABEL> px py pz qx qy qz qw w h
# extra 2D plane (DOOR_FRAME, WINDOW_FRAME, WALL_ART, ...): pose of its centre,
# Meta convention (bounds in local XY, local +Z = the plane's normal)
mesh <uuid> <file name> <vertex count> <index count>
# global mesh sidecar in the same folder (GLOBAL_MESH entity)
<uuid>= 32 lowercase hex digits (no dashes). New ones per scan; FrameBridge uses them as the XrUuidEXT bytes.- Labels are Meta's semantic label strings: FLOOR, CEILING, WALL_FACE, INVISIBLE_WALL_FACE, TABLE, COUCH, BED, STORAGE, SCREEN, LAMP, PLANT, DOOR_FRAME, WINDOW_FRAME, WALL_ART, OTHER, GLOBAL_MESH.
- Readers ignore unknown record types (forward compatibility) and reject a file whose first line isn't
frameport-room <version>with a version they know. Missing ceiling -> 2.5 m above the floor. Missing walls -> walls from the outline edges. Missing outline -> the bounding rectangle of the walls.
How FrameBridge maps records to Meta entities (scene_emu.c)
- ROOM: ROOM_LAYOUT (floor, ceiling, walls in file order) + SPACE_CONTAINER (every other entity) + LOCATABLE/STORABLE.
- floor/ceiling: pose at the outline centroid at height y; local +Z = +Y (floor) / -Y (ceiling); BOUNDED_2D = the outline's bounding rect in local XY; BOUNDARY_2D = the outline polygon in local XY.
- wall: pose at the segment midpoint, height (y0+y1)/2; local +X along the wall (x0,z0)->(x1,z1), local +Y up, local +Z = inward normal; BOUNDED_2D = length x (y1-y0) centred.
- volume: pose at the top-face centre, rotation = yaw about +Y then local +Z pointing up (+Y); BOUNDED_3D offset (-w/2, -d/2, -h), extent (w, d, h) (Meta: volumes hang below their anchor); BOUNDED_2D = the top face (w x d).
- plane: pose as given; BOUNDED_2D = w x h centred.
- mesh: entity at the identity pose labelled GLOBAL_MESH with TRIANGLE_MESH (XR_META_spatial_entity_mesh).
room-mesh.bin (little-endian)
char[4] "FPRM", u32 version = 1, u32 vertex_count, u32 index_count,
float32 xyz[vertex_count], u32 indices[index_count] (triangles; counter-clockwise seen from the room's inside)
Coordinates in STAGE space. Kept below 200k triangles (decimated).
Raw files and scan.json (scanner sidecars)
fp_roomscan refines its output by default (docs/mr/scan.md "Refinement"). room.txt + room-mesh.bin are always
the refined room (what games and the GUI read; the contract above is unchanged). Next to them:
room-raw.txt+room-mesh-raw.bin: the scanner's unrefined output in the same formats (itsmeshrecord namesroom-mesh-raw.bin). Same UUIDs as the refined room.txt, so anchors stay the same when the room is refined again. The refined room differs in: the ceiling height (re-estimated from the scan, wall tops follow it), the volumes (after the label rules) and the mesh. Records the scanner doesn't model (e.g.plane) are copied verbatim.scan.json(UTF-8 JSON,version1):{"version": 1, "heads": [[x, y, z], ...], # head path of the recording (STAGE), <= 2000 points, decimated evenly "refine": {"refine": 1, "walls": "panels", "band": 0.35, ..., "erase": [[x, y, z, r], ...]}, # every refinement setting used (keys = the CLI options, docs/mr/scan.md) "report": {"triangles", "kept", "structural", "near_walls", "outside", "small_pieces", "thin_pieces", "erased", "walked", "tops", "panel_triangles", "truncated", "walked_skipped", "floor_y", "ceiling_y", "boxes", "relabelled", "removed_boxes", "suggestions", "plane_shifts": [{"kind": "wall"|"floor"|"ceiling", "wall": i, "shift": m}, ...]}, "labels": [{"from": "COUCH", "to": "TABLE"|null, "reason": "top 0.69 m", "suggest": "door"|"closet" (optional), "id": "<volume uuid>", "box": {"cx", "cz", "top_y", "yaw_deg", "w", "d", "h"}}, ...], # one per scanner volume, in room-raw.txt order; to = null: removed "suggestions": [{"kind": "door"|"closet", "box": {...}, "wall": i, "along": t, "reason": "..."}, ...]} # tall thin slabs; never applied by the scanner. wall = index of the nearest # wall record in room-raw.txt, along = 0..1 position on it (from x0,z0)reportcounts triangles of the raw mesh per pass (panel_trianglesare added ones),wallinplane_shiftsindexes the wall records. Readers ignore unknown keys.fp_roomscan --refine-only <dir>re-runs only the refinement from these files (an older folder without them: its room.txt + mesh are copied to the raw names first). Publishing order: [raw mesh, raw text,]scan.json,room-mesh.bin,room.txt.
Where the files live
- Frame:
~/.local/share/frameport/rooms/<universe>/room.txt+room-mesh.bin(+scan.log, the raw files andscan.jsonabove).~/.local/share/frameport/rooms/current= symlink to the newest room's folder. - Per game: launch.sh copies the current universe's room (else
current) to<app_dir>/room.txt(+ mesh) before Lepton starts, next tosettings.conf; FrameBridge readsdirname($FRAMEBRIDGE_CONFIG)/room.txt. No room -> the old guardian box. - A game's own saved anchors:
/sdcard/Android/data/<pkg>/files/framebridge-anchors.txt(anchor <uuid> px py pz qx qy qz qwper line, STAGE space), written by FrameBridge on save/erase.
Scan dumps (*.fprs) — recorded RoomView 3D meshes, input of fp_roomscan --offline
char[4] "FPRS", u32 version = 1, then frames until EOF:
f64 time_s, float32 pose[7] (px py pz qx qy qz qw: mesh -> STAGE), u32 vertex_count, u32 index_count,
float32 xyz[vertex_count] (mesh space), u32 indices[index_count]