extensions/scene-carriage.md: the bundle, the numbers that cross the mapping, the glTF document (MPEG_media, MPEG_buffer_circular, MPEG_accessor_timed, MPEG_audio_spatial, MPEG_texture_video) and the USD layer each direction writes and reads, the refusals and the corpus. scene_verify.py implements it from the text and agrees with wai-rs on every vector; it also flips bytes of every exported document and requires each copy to be caught. Co-Authored-By: Claude Opus 5.5 <[email protected]>
473 lines
29 KiB
Markdown
473 lines
29 KiB
Markdown
# WAI Extension: Scene Carriage
|
||
|
||
> Status: Draft. Maps WAI's scene content — a world (`WWLD`,
|
||
> [interactive-worlds](interactive-worlds.md)), an audio scene (`WSCN`,
|
||
> [spatial-audio](spatial-audio.md)), the assets a world names, and timed media — onto two
|
||
> scene-description formats, in both directions: ISO/IEC 23090-14 scene description, a set of
|
||
> glTF 2.0 extensions (§4, §5), and a USD layer in the text format of the USD Core
|
||
> Specification (§6, §7). A document WAI writes carries the WAI bytes exactly, beside the
|
||
> presentation a player of that format reads; reading it back gives the same bytes, and is
|
||
> refused unless writing them again gives the document. A document WAI did not write becomes
|
||
> new WAI bytes under stated rules, and is refused wherever a value would not carry exactly.
|
||
> No new capability and no transport. Reference implementation: the `scene_map` module of
|
||
> `wai-rs` (feature `scene_map`); corpus [`live-conformance/scene/`](../live-conformance/scene/),
|
||
> second implementation `live-conformance/scene_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: glTF 2.0 as ISO/IEC 12113:2022; the descriptions and JSON
|
||
> schemas of the extensions of ISO/IEC 23090-14 that the glTF extension registry publishes
|
||
> (`MPEG_media`, `MPEG_buffer_circular`, `MPEG_accessor_timed`, `MPEG_audio_spatial`,
|
||
> `MPEG_texture_video`, `MPEG_animation_timing`, `MPEG_scene_dynamic`, `MPEG_mesh_linking`,
|
||
> `MPEG_viewport_recommended`; each states that it is based on ISO/IEC FDIS 23090-14, and
|
||
> the registry marks their licence as not yet determined, so none is copied here); and the USD
|
||
> Core Specification v1.0.1 (b7bc21a, 2025-12-12), published under CC-BY-ND-4.0. ISO/IEC
|
||
> 23090-14's own text, including its Annex D (the attenuation functions) and the timed
|
||
> accessor information header of its clause 5, was not read.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the bundle a mapping reads and writes (§2); the numbers that cross it (§3); the
|
||
document each direction writes or reads, for glTF (§4, §5) and for USD (§6, §7); the
|
||
refusals (§8); the corpus (§9).
|
||
|
||
Out of scope:
|
||
|
||
- **Presenting a world after tick 0.** A world's op log changes its nodes through the sim
|
||
that owns them; a scene description has no sim. A document presents the world's tick-0
|
||
graph and carries its op log and seal in the WAI bytes, so a WAI sink replays it.
|
||
`MPEG_scene_dynamic` carries scene updates as JSON patches over a scene; an op is a sim
|
||
mutation, not a patch, so this mapping does not write one.
|
||
- **Mesh geometry in glTF.** A world names its assets by content hash and never embeds
|
||
them (interactive-worlds §4.1); glTF 2.0 and the extensions read give a scene no reference
|
||
to a separate glTF asset. So a `wai.mesh.glb` asset, like every asset of a world's table,
|
||
travels in the glTF document by its hash, in the carried bytes, and a glTF player does not
|
||
present it; a USD layer references it (§6.2), which USD composes where a reader for the
|
||
asset exists. Gaussian splats in glTF are `KHR_gaussian_splatting` with
|
||
`EXT_gaussian_splatting_compression_wai` (the exact-splat extension); this mapping does not
|
||
carry them.
|
||
- **Rendering.** The presentation is what each format says a player reads; how a player
|
||
renders it, and how a WAI sink renders the carried bytes, is the renderer's.
|
||
|
||
## 2. The bundle
|
||
|
||
### 2.1 Worlds and audio scenes
|
||
|
||
A **bundle** holds at most one world (a `WWLD` payload of a `wai.world.*` capability), at
|
||
most one audio scene (a `WSCN` payload of `wai.audio.scene`) and a list of timed media items
|
||
(§2.2). The mapping reads each container's section table (interactive-worlds §4: the magic,
|
||
a little-endian u16 count, nine-byte entries of a kind, an offset from the end of the table
|
||
and a length; every section within the payload, no kind twice) and:
|
||
|
||
- of a world: the contract section (0x01), a JSON text whose `sim` is a string; the graph
|
||
section (0x02), canonical state bytes (interactive-worlds §7.1: a u32 count, then each
|
||
node's 16-byte id, a u32 length and its state, ids strictly ascending, nothing after); and
|
||
the asset table (0x03) where present (interactive-worlds §4.1: a u32 count, then each
|
||
entry's 32-byte hash, its role byte, a u32 name length and the UTF-8 name, nothing after).
|
||
A world that breaks one of these, or a node whose sim §3.4 gives a position with a state
|
||
too short to hold it, is `world_malformed`.
|
||
- of an audio scene: the contract section (0x01), a JSON text with `sample_rate` (1 to
|
||
2^32 − 1) and `duration`, each a plain decimal integer; the object table (0x02,
|
||
spatial-audio §2.2, read to its end and nothing after); the asset table (0x03) where
|
||
present; and a seal (0x04) of 104 bytes where present. One that breaks these is
|
||
`audio_malformed`.
|
||
|
||
Other sections are carried and not read. The checks run world, then audio scene, then
|
||
media items in order.
|
||
|
||
### 2.2 Timed media items
|
||
|
||
A **timed media item** is a WAI object named by its content hash — the hash of
|
||
interactive-worlds §4.1, over a complete envelope — with a `name` (a string), its
|
||
`capability`, and for pictures a `width` and a `height`. Its capability is `wai.video.`,
|
||
`wai.audio.` or `wai.image.` followed by the capability grammar of
|
||
[http-delivery](http-delivery.md) §4.1 (segments of `a` to `z` and `0` to `9`, then
|
||
segments that may also hold `_`); a picture's (`wai.video.*`, `wai.image.*`) width and
|
||
height are each 1 to 65535, and an audio item has neither. An item that breaks this is
|
||
`media_malformed`. A document names a WAI object by the URI `urn:wai:asset:` followed by its
|
||
hash in 64 lowercase hexadecimal digits.
|
||
|
||
### 2.3 What a document carries
|
||
|
||
Every document this mapping writes names its profile, `wai-scene/1`, and carries the bundle
|
||
exactly: the world's and the audio scene's payloads in lowercase hexadecimal, and the media
|
||
list as a JSON text — an array of objects, each of exactly `name`, `asset` (64 lowercase
|
||
hexadecimal digits) and `capability`, then for a picture `width` and `height`, in that
|
||
order, written as §4.1 writes JSON. A reader takes the WAI bytes from what is carried and
|
||
nothing else; the rest of the document is presentation, a function of those bytes.
|
||
|
||
## 3. Numbers
|
||
|
||
### 3.1 Conversions
|
||
|
||
A position in WAI is three Q32.32 values (`wai.det.fixed64`: a signed 64-bit integer `r`
|
||
standing for `r / 2^32`). Writing one, the mapping takes the binary32 (for glTF) or binary64
|
||
(for USD) nearest `r / 2^32`, ties to even; every non-zero result is a normal number. A
|
||
time is a sample index `s` of an audio scene at rate `R`; writing it, the mapping takes the
|
||
binary32 nearest `s / R`, ties to even. Reading, a binary32 or binary64 value `v` is a
|
||
Q32.32 value only when `v · 2^32` is an integer from −2^63 to 2^63 − 1 (`position_not_exact`
|
||
otherwise), and a binary32 time `t ≥ 0` is a sample index only when `t · R` is an integer
|
||
of at most 2^64 − 1 (`time_not_exact`).
|
||
|
||
### 3.2 JSON numbers
|
||
|
||
glTF's JSON numbers are written as RFC 8785 §3.2.2.3 writes a binary64 (the shortest digits
|
||
that read back to it, spelt as ECMAScript's `Number.prototype.toString` spells them), the
|
||
binary64 being the binary32 value of §3.1 exactly. Integers are plain decimal. A reader takes
|
||
a JSON number for a glTF float as glTF does: the binary32 nearest its decimal value, ties to
|
||
even; one past the binary32 range is `document_malformed`.
|
||
|
||
### 3.3 USDA numbers
|
||
|
||
USDA numbers are written as the USD Core Specification (*Double-Precision Floating Point
|
||
Representation when Writing USDA content*) writes a binary64: the shortest digits that read
|
||
back to it; scientific notation, one digit before the point and a signed exponent with no
|
||
`+`, when the decimal exponent is below −6 or at least 15; decimal otherwise; no trailing
|
||
point, no trailing zero in a fraction, no leading zero but before the point of a value
|
||
below 1. Integers are plain decimal. A reader takes a USDA number as the binary64 nearest
|
||
its decimal value.
|
||
|
||
### 3.4 Positions by sim
|
||
|
||
A world node has a position when its sim's state begins with one: `wai.world.sim.kinematic0`
|
||
and `wai.world.sim.ballistic0`, the first 24 bytes (three little-endian Q32.32 values);
|
||
`wai.world.sim.truss0`, a particle (state byte 0 is 0), the 24 bytes after that byte. Other
|
||
nodes and sims have none. A node with no position is presented with no translation.
|
||
|
||
## 4. Writing a glTF document
|
||
|
||
### 4.1 The document
|
||
|
||
The document is one JSON text in UTF-8, followed by a line feed. Every object is written
|
||
with the members this section lists, in the order it lists them, and none other; no
|
||
whitespace; strings escaped as RFC 8785 §3.2.2.2 escapes them; numbers as §3.2 writes them.
|
||
A member a step does not produce is not written. The document's members are, in order:
|
||
`asset` (`{"version":"2.0","generator":"wai-scene/1"}`); `extensionsUsed` (the names of the
|
||
extensions the document uses, in ascending byte order); `extensions`, holding `MPEG_media`
|
||
when there are media items (§4.4); `scene` (0); `scenes` (one scene whose `nodes` lists
|
||
every node, `[{}]` when there is none); `nodes`; `cameras`; `buffers`; `bufferViews`;
|
||
`accessors`; `animations`; `textures`; `extras` (§4.5). First the bundle is checked (§2).
|
||
|
||
### 4.2 The world
|
||
|
||
Each node of the world's tick-0 graph, in ascending id order, is a node
|
||
`{"name":"node-<32 lowercase hexadecimal digits of its id>","translation":[x,y,z]}`, its
|
||
translation the binary32 of §3.1 of its position (§3.4), absent when it has none.
|
||
|
||
### 4.3 The audio scene
|
||
|
||
An audio scene adds a camera, `{"type":"perspective","perspective":{"yfov":1,"znear":0.25}}`,
|
||
and after the world's nodes a listener node,
|
||
`{"name":"listener","camera":0,"extensions":{"MPEG_audio_spatial":{"listener":{"id":0}}}}`.
|
||
Then each object `i`, in table order, is a node
|
||
`{"name":"object-<i>","translation":[…],"extensions":{"MPEG_audio_spatial":{"sources":[{"id":i,"type":"Object","attenuation":"custom","accessors":[a]}]}}}`,
|
||
its translation its first key's position (absent with no key), `a` its source's timed
|
||
accessor (§4.4). Its keys MUST be in strictly ascending sample order, and their times (§3.1)
|
||
strictly ascending, or the bundle is `not_presentable`. The attenuation is `custom`, which
|
||
ISO/IEC 23090-14 leaves outside its scope: WAI's distance law and gain are in the carried
|
||
bytes, and this mapping states no equivalence with a function of that standard's Annex D.
|
||
|
||
An object with two or more keys is animated. The document's first buffer holds, object by
|
||
object, its times (binary32, little-endian), then its positions (three binary32 each), and
|
||
is `{"byteLength":n,"uri":"data:application/octet-stream;base64,<RFC 4648 base64 with padding>"}`.
|
||
For each animated object, in order: two buffer views,
|
||
`{"buffer":0,"byteOffset":o,"byteLength":4k}` and `{"buffer":0,"byteOffset":o',"byteLength":12k}`;
|
||
two accessors,
|
||
`{"bufferView":v,"componentType":5126,"count":k,"type":"SCALAR","min":[t0],"max":[tlast]}` and
|
||
`{"bufferView":v+1,"componentType":5126,"count":k,"type":"VEC3"}`; a sampler
|
||
`{"input":a,"interpolation":"LINEAR","output":a+1}`; and a channel
|
||
`{"sampler":s,"target":{"node":n,"path":"translation"}}`. One animation holds them all:
|
||
`{"name":"audio","channels":[…],"samplers":[…]}`. glTF's linear interpolation of positions
|
||
between keys is the presentation; WAI's own interpolation of keys is spatial-audio §2.2's,
|
||
in the carried bytes.
|
||
|
||
### 4.4 Media
|
||
|
||
`MPEG_media`'s `media` lists first the audio scene's sources, each once, in object order,
|
||
then the bundle's other items in list order (an item whose hash is already listed is not
|
||
listed again). Each is
|
||
`{"name":…,"autoplay":true,"alternatives":[{"uri":"urn:wai:asset:<hex>","mimeType":…}]}`:
|
||
for a source, the name of the first bundle item with its hash, else `source-` and the first
|
||
16 digits of its hash; the `mimeType` is `application/wai`
|
||
([http-delivery](http-delivery.md) §3) followed, when the item's capability is known, by
|
||
`; codecs="wai1.<the capability without its leading wai.>"`, the `codecs` grammar of
|
||
http-delivery §4.1.
|
||
|
||
Each source, and each other item of a picture capability, gets in media order a circular
|
||
buffer, `{"byteLength":B,"extensions":{"MPEG_buffer_circular":{"media":m}}}`, a view
|
||
`{"buffer":b,"byteLength":B}`, and a timed accessor: for a source,
|
||
`{"bufferView":v,"componentType":5126,"count":1024,"type":"SCALAR","extensions":{"MPEG_accessor_timed":{"immutable":true}}}`
|
||
with `B` = 4096 (one frame of 1024 mono binary32 samples); for a picture of width `w` and
|
||
height `h`,
|
||
`{"bufferView":v,"componentType":5121,"normalized":true,"count":w·h,"type":"VEC3","extensions":{"MPEG_accessor_timed":{"immutable":true}}}`
|
||
with `B` = 3·w·h, and a texture
|
||
`{"extensions":{"MPEG_texture_video":{"accessor":a,"width":w,"height":h,"format":"RGB"}}}`.
|
||
Buffers, views and accessors follow the animation's. A circular buffer's `byteLength` is one
|
||
frame; how many frames a pipeline keeps is `MPEG_buffer_circular`'s `count`, left at its
|
||
default. An accessor with no header view is `immutable`, as `MPEG_accessor_timed`'s schema
|
||
requires.
|
||
|
||
The extensions used are `MPEG_audio_spatial` with an audio scene, `MPEG_media` with media
|
||
items, `MPEG_buffer_circular` and `MPEG_accessor_timed` with a circular buffer, and
|
||
`MPEG_texture_video` with a texture.
|
||
|
||
### 4.5 What is carried
|
||
|
||
The document's `extras` is `{"wai":{"profile":"wai-scene/1","world":"<hex>","audio":"<hex>","media":"<JSON text>"}}`
|
||
(§2.3), `world`, `audio` and `media` present when the bundle has them.
|
||
|
||
## 5. Reading a glTF document
|
||
|
||
### 5.1 Which reading
|
||
|
||
A document that is not a JSON text (RFC 8259, UTF-8, no member named twice, at most 64
|
||
levels) whose value is an object with `asset.version` `"2.0"` is `document_malformed`. One
|
||
whose `extras` has a member `wai` is read as §5.2; any other, as §5.3.
|
||
|
||
### 5.2 A document WAI wrote
|
||
|
||
`extras.wai` is an object of the members of §4.5 and no other, `profile` being
|
||
`wai-scene/1`, `world` and `audio` lowercase hexadecimal of even length, and `media` a JSON
|
||
text of §2.3's form; otherwise the document is `carried_malformed`. The bundle is what it
|
||
carries. Writing that bundle (§4) MUST give the document's bytes: a bundle §4 refuses is
|
||
`carried_malformed`, and other bytes are `presentation_mismatch`. So a player's view and the
|
||
WAI bytes cannot drift apart unnoticed.
|
||
|
||
### 5.3 A document WAI did not write
|
||
|
||
Reading one needs the key that mints its node ids (an `actor`, 32 bytes) and the contract of
|
||
the world and of the audio scene it yields. The steps run in order, and the first refusal is
|
||
the one reported; each index a step follows MUST be in range, and a member a step reads MUST
|
||
have the type it names, or the document is `document_malformed`.
|
||
|
||
1. **The nodes.** The scene is `scenes[scene]` (`scene` defaulting to 0; no scenes, no
|
||
nodes). Its nodes are visited depth first in preorder: its `nodes` in order, each node's
|
||
`children` in order. A node visited twice is `document_malformed`.
|
||
2. **Positions**, in visiting order. A node with a `matrix`, a `rotation` other than
|
||
(0, 0, 0, 1) or a `scale` other than (1, 1, 1), each read as binary32 (§3.2), is
|
||
`transform_not_carried`: a node of the world has a position and nothing more. Its
|
||
translation (zero when absent) is read as binary32 and then as Q32.32 (§3.1,
|
||
`position_not_exact`), and its position is its parent's plus its translation
|
||
(`position_not_exact` past the Q32.32 range).
|
||
3. **Kinds**, in visiting order: a node whose `extensions.MPEG_audio_spatial` has `sources`
|
||
is an audio node; one with a `camera`, or with `MPEG_audio_spatial` and no sources, is
|
||
skipped; every other node is a world node, and a world node with a `mesh` is reported as
|
||
presentation not carried (`node <i>: mesh`).
|
||
4. **Animations**, in document order, each animation's channels in order: a channel's
|
||
`sampler` is followed first, then its `target`; a target with no `node` is ignored. A
|
||
channel whose node is not an audio node, whose `path` is not `translation`, whose
|
||
sampler's `interpolation` (`LINEAR` by default) is not `LINEAR`, or that is a node's
|
||
second channel, is `animation_not_carried`.
|
||
5. **Audio objects**, audio nodes in visiting order, each node's sources in order. A source
|
||
of `type` other than `Object` is `audio_not_object` (an `HOA` source has no position to
|
||
carry); a `pregain` other than 0 is `gain_not_exact` (a gain in decibels has no exact
|
||
linear value). Its first accessor's buffer view's buffer is a circular buffer whose
|
||
`media` names an `MPEG_media` item; the source is the first of that item's alternatives
|
||
whose `uri` is `urn:wai:asset:<hex>` (`media_not_wai` if none). Its keys: with no channel,
|
||
one key at sample 0 at the node's position; with one, the audio contract is due now
|
||
(`contract_absent` if absent, not JSON, or without a `sample_rate` of 1 to 2^32 − 1), and
|
||
the sampler's input (`FLOAT`, `SCALAR`) and output (`FLOAT`, `VEC3`) accessors, neither
|
||
sparse, read from views with no `byteStride` of a buffer whose `uri` is
|
||
`data:application/octet-stream;base64,` and canonical base64, each within its view and
|
||
buffer, the output three values to each input value, give its keys: each time a sample
|
||
(§3.1, `time_not_exact`), each position the parent's position plus the output as Q32.32
|
||
(`position_not_exact`). Its gain is 1.
|
||
6. **Other media**, the `MPEG_media` items no source uses, in order: its first `urn:wai:asset`
|
||
alternative (`media_not_wai` if none), whose `mimeType` names a capability as §4.4 writes
|
||
it (`media_not_wai` otherwise); its name (empty when absent); for a picture capability,
|
||
the width and height of the first `MPEG_texture_video` texture whose accessor's circular
|
||
buffer is this item. An item that then breaks §2.2 is `media_not_wai`.
|
||
7. **The bytes.** World nodes need the world contract (`contract_absent` if absent or not
|
||
JSON), whose `sim` MUST be `wai.world.sim.kinematic0` (`sim_not_importable`): the `i`-th
|
||
world node, from 1 in visiting order, gets the id interactive-worlds §5.4 mints for the
|
||
actor and lamport `i`, and a kinematic0 state at its position, at rest, with scalar 0.
|
||
The world is the contract given, those nodes in ascending id order, no asset table, and an
|
||
empty op log, laid out as interactive-worlds §4 lays a container out. Audio objects need
|
||
the audio contract (as in step 5); the audio scene is the contract given, the objects in
|
||
order (gain 1), and an asset table of each distinct source in object order, role 0, named
|
||
by its item's name. The media list is step 6's.
|
||
|
||
## 6. Writing a USD layer
|
||
|
||
### 6.1 The layer
|
||
|
||
The layer is UTF-8 text in USD's text format, every line ended by a line feed, indented four
|
||
spaces a level:
|
||
|
||
```
|
||
#usda 1.0
|
||
(
|
||
customLayerData = {
|
||
string "wai:audio" = "<hex>"
|
||
string "wai:media" = <string>
|
||
string "wai:profile" = "wai-scene/1"
|
||
string "wai:world" = "<hex>"
|
||
}
|
||
defaultPrim = "World"
|
||
endTimeCode = <duration>
|
||
startTimeCode = 0
|
||
timeCodesPerSecond = <sample rate>
|
||
)
|
||
|
||
def Xform "World"
|
||
{
|
||
…
|
||
}
|
||
```
|
||
|
||
`wai:audio`, `wai:media` and `wai:world` are written when the bundle has them, and the three
|
||
time fields with an audio scene: time codes are its sample indices. The prims of §6.2 are
|
||
`World`'s children, with one empty line before each but the first. First the bundle is
|
||
checked (§2).
|
||
|
||
### 6.2 Prims
|
||
|
||
- **World nodes**, in ascending id order: `def Xform "node_<32 hex>"`, holding
|
||
`double3 xformOp:translate = (x, y, z)` and `uniform token[] xformOpOrder = ["xformOp:translate"]`
|
||
when the node has a position (§3.4), each value the binary64 of §3.1 written as §3.3; the
|
||
meaning of the translate operation is a domain specification's, which the core
|
||
specification leaves to them, and the core specification fixes its syntax and value
|
||
resolution.
|
||
- **Assets**, when the world has an asset table: `def Scope "Assets"`, and in it, for entry
|
||
`i` in table order, `def "asset_<i>"` with the prim metadata
|
||
`prepend references = @urn:wai:asset:<hex>@` and the properties
|
||
`custom string wai:name = <name>` and `custom int wai:role = <role>`. A reference is a
|
||
composition arc to the asset. Where the reader cannot compute a layer stack for it — no
|
||
reader for a WAI object — the core specification's composition treats it as a composition
|
||
error and ignores that reference, and the rest of the stage composes.
|
||
- **Audio objects**, when the audio scene has any: `def Scope "Audio"`, and in it, for
|
||
object `i`, `def Xform "object_<i>"` with `custom string wai:source = "<hex>"`, then, with
|
||
keys, its first key's position as `xformOp:translate`, with two or more keys
|
||
`double3 xformOp:translate.timeSamples = {` and one line `<sample>: (x, y, z),` per key and
|
||
`}`, and the `xformOpOrder` above. Keys MUST be in strictly ascending sample order, and
|
||
a sample, the duration and the rate at most 2^53, or the bundle is `not_presentable`.
|
||
- **Media**, when the list has items: `def Scope "Media"`, and in it, for item `i`,
|
||
`def "media_<i>"` with `prepend references = @urn:wai:asset:<hex>@` and the properties
|
||
`custom string wai:capability`, for a picture `custom int wai:height`,
|
||
`custom string wai:name`, and for a picture `custom int wai:width`, in that order.
|
||
|
||
Prims inside a scope are separated by one empty line. A prim with metadata is written
|
||
`def "<name>" (`, the metadata line, `)`, then its body `{` … `}` on the following lines;
|
||
an empty body is `{` and `}`.
|
||
|
||
### 6.3 Strings
|
||
|
||
A string is written in double quotes, with `\"`, `\\`, `\n`, `\r` and `\t`, and with `\x`
|
||
and two uppercase hexadecimal digits for any other character below U+0020 and for U+007F. A
|
||
character outside the grammar's `Utf8Character` ranges has no spelling, and a bundle that
|
||
needs one is `not_presentable`.
|
||
|
||
## 7. Reading a USD layer
|
||
|
||
### 7.1 The subset read
|
||
|
||
A layer that is not UTF-8, or does not begin `#usda`, is `document_malformed`; a version on
|
||
that line other than `1.0` is `usd_not_read`. The rest is read as the core specification's
|
||
grammar reads it, for this subset:
|
||
|
||
- **Tokens.** Spaces, tabs and line breaks separate tokens; `#…` and `//…` to the end of a
|
||
line and `/*…*/` are comments. Strings in `"` or `'`, on one line, with the escapes `\n`,
|
||
`\r`, `\t`, `\a`, `\b`, `\f`, `\v`, `\\`, `\'`, `\"` and `\x` with one or two uppercase
|
||
hexadecimal digits below 0x80; asset paths `@…@` on one line; paths `<…>`; numbers (`-`,
|
||
digits, `.`, `e`, `E` and an exponent's sign, or `inf`); identifiers (`A`–`Z`, `a`–`z`,
|
||
`0`–`9`, `_`, and `:` before a letter or `_`); and `( ) [ ] { } = : , ; .`. A
|
||
triple-quoted string, a `@@@` asset path, another escape or character is `usd_not_read`;
|
||
a string, path, asset path or comment that does not end is `document_malformed`.
|
||
- **Values**: numbers, strings, identifiers, asset paths, paths, tuples `( … )`, lists
|
||
`[ … ]` and dictionaries (`{` items `type key = value` or `dictionary key = { … }` `}`).
|
||
- **Layer metadata** `( … )`: a bare string is a comment; `subLayers`, `relocates` and a
|
||
list operation are `usd_not_read`; `customLayerData` is a dictionary and `defaultPrim` a
|
||
string (`document_malformed` otherwise); other items are read and not used.
|
||
- **Prims**: `def`, an optional type name, the name as a string, optional prim metadata, and
|
||
a body. `over` and `class` are `usd_not_read`. In prim metadata, a bare string is a
|
||
comment; `references`, alone or after `prepend` or `append`, is `None`, an asset path or a
|
||
list of them, each without a prim path or parameters (`usd_not_read` otherwise; `delete`
|
||
and other list operations on it too); `payload`, `inherits`, `specializes`, `variants`,
|
||
`variantSets` and `relocates`, and a list operation on anything else, are `usd_not_read`;
|
||
other items are read and not used.
|
||
- **Bodies**: child prims (a name twice is `document_malformed`, as two root prims of one
|
||
name are); `rel`, `variantSet`, `reorder` and list-operation statements are
|
||
`usd_not_read`; and attributes, `[custom] [uniform|varying] type[[]] name` then `= value`,
|
||
`.timeSamples = { time: value, … }` or nothing, then optional metadata. A `.` other than
|
||
`.timeSamples` is `usd_not_read`; an attribute declared twice with two types, or given two
|
||
defaults or two sets of time samples, is `document_malformed`.
|
||
|
||
Anything else is `document_malformed`.
|
||
|
||
### 7.2 A layer WAI wrote
|
||
|
||
A layer whose `customLayerData` has a string key beginning `wai:` is read as WAI wrote it:
|
||
its `wai:` items MUST be the strings of §6.1, `wai:profile` being `wai-scene/1`
|
||
(`carried_malformed` otherwise), and writing the bundle they carry (§6) MUST give the
|
||
layer's bytes (`carried_malformed` for a bundle §6 refuses, `presentation_mismatch` for other
|
||
bytes).
|
||
|
||
### 7.3 A layer WAI did not write
|
||
|
||
The root is the prim `defaultPrim` names (`document_malformed` if no root prim has that
|
||
name), else the first root prim (`document_malformed` if none). Prims are visited depth first
|
||
in preorder from the root, the root included, children in written order. For each, in
|
||
visiting order:
|
||
|
||
1. **Its offset**, from its properties in written order: `xformOpOrder` MUST be a list of
|
||
the one string `xformOp:translate` with no time samples; `xformOp:translate` MUST have a
|
||
default value and no time samples; any other `xformOp:` property is not carried; each
|
||
breach is `transform_not_carried`. The translate's default is a tuple of three numbers
|
||
(`document_malformed`), each read as binary64 (§3.3) and then as Q32.32
|
||
(`position_not_exact`). With no translate the offset is zero. Its position is its
|
||
parent's plus its offset (`position_not_exact` past the range).
|
||
2. **Its kind.** A prim with references is a set of assets: each asset path MUST be
|
||
`urn:wai:asset:<hex>` (`asset_not_wai`), and gives an asset entry of role 0 named by the
|
||
prim. Otherwise an `Xform` prim with an `xformOp:translate` is a world node at its
|
||
position. Otherwise a prim of a type other than none, `Xform` and `Scope` is reported as
|
||
presentation not carried (`<path>: <type>`).
|
||
|
||
Nodes or assets need the world contract (`contract_absent` if absent or not JSON), whose sim
|
||
MUST be `wai.world.sim.kinematic0` (`sim_not_importable`). The world is built as §5.3 step 7
|
||
builds one, with an asset table of the entries in visiting order. A layer gives no audio
|
||
scene and no media: the core specification defines no audio prims, so the audio and media of
|
||
a layer are read only from the bytes a WAI-written layer carries (§7.2).
|
||
|
||
## 8. Refusals
|
||
|
||
| code | where |
|
||
|---|---|
|
||
| `world_malformed` | §2.1: the world does not read |
|
||
| `audio_malformed` | §2.1: the audio scene does not read |
|
||
| `media_malformed` | §2.2: a media item's capability, width or height |
|
||
| `not_presentable` | §4.3, §6.2, §6.3: keys out of order, two keys at one binary32 time, a time code past 2^53, a character with no USDA spelling |
|
||
| `document_malformed` | §5.1, §5.3, §7.1, §7.3: the document does not read as its format, or a value a step needs is not there |
|
||
| `carried_malformed` | §5.2, §7.2: what a WAI-written document carries does not read, or is a bundle §4 or §6 refuses |
|
||
| `presentation_mismatch` | §5.2, §7.2: the document is not the export of the bundle it carries |
|
||
| `usd_not_read` | §7.1: a construct outside the subset |
|
||
| `transform_not_carried` | §5.3 step 2, §7.3 step 1: a rotation, scale, matrix or other transform operation |
|
||
| `position_not_exact` | §3.1, §5.3, §7.3: a value that is not a Q32.32 value |
|
||
| `animation_not_carried` | §5.3 step 4: an animation channel the mapping does not carry |
|
||
| `time_not_exact` | §3.1, §5.3 step 5: a time that is not a whole sample |
|
||
| `gain_not_exact` | §5.3 step 5: a pregain other than 0 |
|
||
| `audio_not_object` | §5.3 step 5: a source that is not an object |
|
||
| `media_not_wai` | §5.3 steps 5 and 6: media that names no WAI object or capability |
|
||
| `asset_not_wai` | §7.3 step 2: a reference to something other than a WAI object |
|
||
| `contract_absent` | §5.3, §7.3: nodes or objects and no contract to give them |
|
||
| `sim_not_importable` | §5.3 step 7, §7.3: a world contract whose sim is not `wai.world.sim.kinematic0` |
|
||
|
||
## 9. Conformance corpus
|
||
|
||
[`live-conformance/scene/`](../live-conformance/scene/) holds bundles — worlds built in code
|
||
(kinematic0, ballistic0 and truss0 nodes, an asset table) and the worlds and audio scenes
|
||
other corpora commit, each pinned by SHA-256 — each written as a glTF document and as a USD
|
||
layer and read back; bundles refused on export; documents WAI wrote, altered and refused;
|
||
and documents WAI did not write, built in code, read to new WAI bytes or refused with each
|
||
code of §8. A vector is an *export* (a bundle to a document's bytes, or a code) or an
|
||
*import* (a document, with the actor and contracts §5.3 needs, to a bundle and its report of
|
||
presentation not carried, or a code). A verifier conforms when it reaches every vector's
|
||
outcome and bytes. `wai_scene_vectors gen|verify` writes and checks it, `tests/scene_map.rs`
|
||
regenerates it, and `live-conformance/scene_verify.py`, written from this text and sharing
|
||
no code with `wai-rs`, is its second implementation.
|
||
|
||
## 10. Reference implementation
|
||
|
||
The `scene_map` module of `wai-rs` (feature `scene_map`, on `world` and `audio_scene`):
|
||
`Bundle`, `MediaItem`, `ImportParams` and `Imported`; `gltf::export` and `gltf::import`;
|
||
`usda::export` and `usda::import`; `num` for §3; `corpus` for §9. Its unit tests check the
|
||
conversions and spellings of §3 against fixed values, and that the containers §5.3 and §7.3
|
||
build are the bytes `world::WorldContainer` and `audio_scene::SpatialScene` write.
|