Bring the graphics-dev collaborator's dpl3-revive into the repo as first-class
project code (they've handed it off; it's ours now). This is the proven
Division renderer that our in-process rt_draw has been trying to be.
What's here:
- parser/ B2Z/V2Z/SVT/SCN/SPL/BGF/BMF/BSL decoders (pure Python).
- spec/ reverse-engineered format + the definitive VelociRender wire
protocol (from the original DIVISION source, matches our live
VPX node/action tables exactly).
- source-ref/ read-only copies of the original DIVISION C (BIZREAD.C,
DPLTYPES.H, DPL.H) that define the formats.
- patha/ the "virtual VelociRender board": vrboard.py (24-action protocol
server), vrview.py (numpy software rasterizer, the reference),
vrview_gl.py (moderngl GPU backend, 832x512@60Hz), plus the
run/replay/regress tooling and evidence renders. Drives FLYK/BLADE/
Star Trek demos AND our btl4opt/rpl4opt game binaries.
- viewer/ WebGL archive generators (.py); prebuilt HTML/data regeneratable.
- samples/ small test models/textures.
- bt*.raw.bin real BTL4OPT arena wire captures (kept for offline renderer
testing/regression against OUR game).
.gitignore keeps the multi-hundred-MB demo capture dumps + debug logs +
regeneratable viewer bundles out of history (they stay on disk).
Phase 0 of the integration is validated: their board decodes our bt8 capture
with zero errors (3748 nodes, 507 instances, 4 mechs) and renders our arena
(terrain/dome/sky, correct Division DAC gamma). Plan + status in memory;
integration continues in emulator/RENDERER-COLLAB.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
147 lines
7.3 KiB
Markdown
147 lines
7.3 KiB
Markdown
# `.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` → `<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 (→ `<map>.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** `<stem>.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 <texname> 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`.
|