# `.SCN` scene format Reverse-engineered from `DPL3/EXAMPLES/FLYK.C` (the flythrough viewer) and `DPL3/MATRIX.C`. `.SCN` is **plain text**, one command per line, whitespace- delimited. Comments start with `//` or `#`. It assembles a world by *referencing* models (`.B2Z`) and textures (`.SVT`) by name — it embeds no geometry itself. ## Core commands (the FLYK subset) These cover the overwhelming majority of every scene on the disk (`STATIC` alone is ~5,000 of ~9,000 command lines): | Command | Arguments | Meaning | |---------|-----------|---------| | `FOG` | `z0 z1 r g b` | linear fog: start/end distance + colour | | `BACKGND` | `r g b` | background / clear colour | | `AMBIENT` | `r g b` | ambient light | | `LIGHT` | `r g b ax ay az` | directional light: colour + orientation angles (deg) | | `STATIC` | `name scale x y z ax ay az` | place a model instance | | `DYNAMIC` | `name scale splinefile t0 dt` | model that follows a camera-path spline | | `SSTATIC` | (as `STATIC`) | static variant | | `SCROLL` | `texname u0 v0 du dv` | UV drift on a TEXTURE entity (see below) | Colours are floats in 0..1. Distances/positions are world units. Angles are in **degrees**. `name` is resolved to a geometry file by search path (see Resolution). ## Instance transform From `FLYK.C` + `MATRIX.C`. DPL uses a **row-vector** convention — a point is a row and transforms as `p' = p · M`, with translation in the **bottom row** of the matrix and each op post-multiplied (`M = M · op`). `STATIC` builds: ``` M = Rz(az) · Rx(ax) · Ry(ay) · Scale(s) · Translate(x, y, z) p_world = [x y z 1] · M ``` so a point is rotated (Z, then X, then Y), scaled, then translated into place. Rotation matrices (row-vector form, `c=cos`, `s=sin`): ``` Rz = [ c s 0 0] Rx = [1 0 0 0] Ry = [c 0 -s 0] [-s c 0 0] [0 c s 0] [0 1 0 0] [ 0 0 1 0] [0 -s c 0] [s 0 c 0] [ 0 0 0 1] [0 0 0 1] [0 0 0 1] ``` `LIGHT`'s three angles orient a directional light the same way (`Rz·Rx·Ry`). ## Asset resolution - **Models**: `name` → `.B2Z` (also `.BIZ`/`.V2Z`), found on a search path (originally the `GEOMETRY` env var; here: index of the archive by basename). - **Materials & textures (authoritative)**: a geogroup carries a *material name* (e.g. `redrock_mtl`). The real definition lives in the model's **`.V2Z`** source (the verbose text twin of the binary `.B2Z` — magic `DIV-VIZ2`), which inlines its material library: ``` TEXTURE(NAME=redrock_tex) { MAP {"rocknoiz"} ... } MATERIAL(NAME=redrock_mtl) { TEXTURE {redrock_tex} DIFFUSE {0.743, 0.216, 0.202} } ``` So a material = a **texture (→ `.SVT`) modulated by a DIFFUSE colour**. The final surface colour is `texel × diffuse × lighting`. Material names are **scoped per model** (the same name can differ between themes), so each model's own `.V2Z` is the source of truth — `parser`/`bundle` parse it per model. - Two kinds of texture, handled differently: - **Full-colour painted skins** (vehicle textures like `VTVCAB`) are shown **as-is** (tint = white); their colour lives in the texels. - **Greyscale / intensity "noise" maps** (`CANAL`, `ROCKNOIZ`) are **tinted by the material diffuse** — `techy` (white) × CANAL = grey concrete, `rednoise` (red) × CANAL = red pipes. Some (e.g. `CANAL`, `VTVALL`) have a dead colour channel and are rebuilt as luminance first. The loader auto-classifies by saturation. - Prefer the material's **per-part skin** `.SVT` (e.g. `vtvcab_mtl` → `VTVCAB.SVT`) over any atlas the material names (`vtvall`) — the per-part skins match the model UVs; the atlas tinted by diffuse does not. - **Pair geometry and materials from the same source directory.** The same asset exists in several theme folders (`CONVERT/THOR/`, `CONVERT/CANALS/`, …) and the *same material name is defined with different colours* in each (THOR `rednoise` = vivid red `0.83,0.006,0.006`; CANALS = salmon `0.69,0.41,0.40`). Loading `.B2Z` from one folder but `.V2Z` from another mixes them and yields the wrong colour. `bundle.v2z_for()` resolves the sibling `.V2Z` first. - *Fallback:* only when a model has no `.V2Z` do we fall back to the old stem/keyword heuristics. ### Output gamma (the Division DAC) Material/diffuse values are **linear**. The Division i860/pxpl5 card output through a **10-bit DAC with a gamma-1.7 encode** — from `DPL3/GAMMA.C`, which builds a 256→1023 table `out = 1023 · (i/255)^(1/1.7)`. This brightens midtones and gives the characteristic bright/pastel look. The viewer reproduces it by applying `colour^(1/1.7)` as the final fragment step (and to the background clear). Without it, linear output on an sRGB display looks too dark and over-saturated. ### Surface merging (a bug worth noting) When baking a scene, geometry is merged into surfaces to cut draw calls. Surfaces **must be keyed on `(texture, diffuse-colour)`, not texture alone** — otherwise two materials that share a texture but differ in tint (grey columns vs red pipes, both on `CANAL.SVT`) merge into one and the second colour is lost. This was the cause of "the pipes render grey". ## Scrolling textures (`SCROLL`) SCROLL u0 v0 du dv e.g. SCROLL btfx:firesmoke1_scr_tex 0 0 0.05 -0.331 Animates a texture's UV coordinates — flowing water, drifting smoke, rising fire, laser-beam flow (287 uses across the disk). `texname` addresses a **TEXTURE entity** (not a bitmap): either `lib:name` in a named material library (`.VMF`/`.BMF`) or a plain name from the model's own `.V2Z`/`.VGF` source. `u0 v0` is the initial offset, `du dv` the drift rate — the viewer plays it as UV/second (rate units are inferred; the engine source for this path is not on the disk). The scene command is actually an **override**: the same data is baked into the texture entity itself as a `SPECIAL` hook in the material source — TEXTURE(NAME="firesmoke1_scr_tex";SPECIAL=" SCROLL 0 0 0.05 -0.331 \0x00") { MAP {"bintA"} } so a texture scrolls even in scenes/maps with no `SCROLL` line, and several entities can share one bitmap (`bintA`) at different rates. In the binary `.BMF` form the SPECIAL string is texture sub-block tag `0x037` (see B2Z_FORMAT §7). Scroll is a per-texture-entity property, so the baker keys animated surfaces on the entity, not the bitmap. ## Extended commands (game-tool scenes) Larger scenes authored by the game tools use more keywords. The current loader **recognises and skips** these; they are documented here for completeness: `MORPH` (animated geometry) · `SPECIALFX` (particle/effect hooks) · `TEXTURE` / `GEOMETRY` / `MATERIAL` / `RAMP` (explicit asset declarations) · `ZONE` / `START` / `END` (zone blocks) · `CLIP` (near/far clip) · `VIEWANGLE` (fov) · `TRACKER` / `RETRACE` / `KEYRATE` / `GEOMETRIZE` / `IMMUNE` / `DRAWLAST` / `NOVIEWMATRIX` / `BILLBOARD`. ## Validation `parser/scn.py` parses `CANAL.SCN` → fog, background, ambient, 1 light, 47 instances (5 unique models, all resolved). `viewer/bundle.py` assembles it into world space and renders it: a Red Planet canal — 22 red-rock cliff segments lining a channel, 22 struts, a VTV "mule" vehicle, rock and tech-junk — with reddish fog. See `viewer/preview_scene.png`.