Found from the user's live 2-node demo: a Crimson MadCat went GREY in its own view after a V (inside/outside) toggle, with NO new [paint] or MakeMechRenderables line in the log -- so no rebuild, just a re-parse. ROOT CAUSE: the per-pilot colour/badge/patch is applied by rewriting MATERIAL NAMES while a BGF parses, and only while the substitution callback is installed (SetupMaterialSubstitutionList .. TearDown, bgfload.cpp:15-18). BTL4VideoRenderer::ApplyViewSkeleton re-parses every shown segment BGF on each view toggle AND on respawn (that is what its fresh graphic-state read is for), but did so OUTSIDE that bracket -> raw %color% placeholders -> unpainted materials -> grey. Nothing caches the geometry (d3d_OBJECT caches only textures), so every call re-parses. This is #38's mechanism: 'colors not preserved on respawn' was never a replication or teardown bug. FIX: re-install the substitution list around the reload, reusing the serial the mech was BUILT with -- SetupMaterialSubstitutionList advances the global %serno% per call, so a naive re-bracket would stamp a different serial and still resolve the wrong names. MechRenderTree gains paintSerno (captured before Setup at build); ApplyViewSkeleton installs/restores around the loop and logs it. Rig-verified: the crimson MadCat stays crimson (yellow patch intact) across repeated inside/outside toggles; each toggle now logs '[paint] color=red serno=0' + '(paint serno 0)'. ALSO -- corrections to yesterday's #44 name plate from the adversarial pass: - kPlateARGB 0xFF808080 -> 0x80FFFFFF. BMAP.BMF tag 0x0027 is dpfB_MATERIAL_OPACITY_TAG [T0 libDPL/dsys/PFBIZTAG.H], NOT a diffuse colour: the materials carry NO colour tag at all, so the plate is the unmodulated callsign raster at ~50% opacity, not a grey label. (Two independent workflows converged on this.) - the plate's K = 1/2.8 is NOT the hotbox's constant (2.8145) -- de-unified. - the Lock attribute is HUD attr id 10, an int/Logical*, not a Scalar*. - the PNAME pair split is the V half, not U (our per-player texture is one 128x32 cell, so the port quad samples v 0..1). - retracted bgf-format's 'tag 0x0027 likely SPECULAR [T4]'. KB: new reconstruction-gotchas section on the whole bug class (load-time-only state must be re-installed on every RE-load, incl. the serno trap). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
128 lines
8.7 KiB
Markdown
128 lines
8.7 KiB
Markdown
---
|
||
id: bgf-format
|
||
title: "BGF Model Format (DIV-BIZ2 geometry) + the render gotchas"
|
||
status: established
|
||
source_sections: "docs/BGF_FORMAT.md (full spec); CLAUDE.md §5, §10 render-fidelity notes"
|
||
related_topics: [asset-formats, rendering, decomp-reference]
|
||
key_terms: [BGF, DIV-BIZ2, PMESH, CONN, PCONN, LOD, material-ramp, SV_SPECIAL]
|
||
open_questions:
|
||
- "Replacement-LOD selection for NON-additive multi-LOD models (hot-spot/reference semantics)"
|
||
---
|
||
|
||
# BGF Model Format
|
||
|
||
`.BGF` = **`DIV-BIZ2`** little-endian nested Tag-Length-Value geometry. Full byte-level parsing
|
||
spec (verified against all 1275 content `.BGF` files) lives in **`docs/BGF_FORMAT.md`** — this
|
||
topic captures the structure + the hard-won gotchas that cost real debugging. Loader of record:
|
||
`engine/MUNGA_L4/bgfload.cpp` (game-callable via `LoadBgfFile`); standalone reference reader
|
||
`DivLoader/VGCDivLoader.cpp` (archive-only; in-repo reimplementation: `engine/MUNGA_L4/image.cpp`).
|
||
Tag names: `dsys/PFBIZTAG.H`.
|
||
|
||
## Structure
|
||
`OBJECT → LOD → PATCH (geogroup) → PMESH`. Interleaved float32 verts; **QUAD faces (must
|
||
triangulate `[a b c][a c d]`)**; materials referenced by `library:material` name (resolved
|
||
against the map's `.BMF` libs). Damage/articulation are **name-driven via `SV_SPECIAL` (0x2037)
|
||
`dz_*` tokens**, not stored geometry. [T1]
|
||
|
||
## Indices — the CONN/PCONN story (two corrections)
|
||
A PMESH carries faces in `CONNECTION_LIST` (0x0047) and/or `PCONN_LIST`:
|
||
- `CONNECTION_LIST` is a **FLAT TRIANGLE LIST** (3 indices/face) — NOT "one polygon, fan-triangulate"
|
||
(the original doc was wrong; fan-triangulating turned buttec's 115 tris into 341 fan-garbage).
|
||
Corpus-verified: ALL 3488 CONN chunks have n%3==0. [T2]
|
||
- **CONN and PCONN COEXIST** — a pmesh can carry quads/hexes in PCONN PLUS loose triangles in
|
||
CONN. **370 of 841 GEO models** had mixed pmeshes silently dropping their CONN triangles until
|
||
both were processed always (the "turret base missing panels" bug). `ppf=6` hexagons exist. [T2]
|
||
|
||
## LODs — the √3 decode
|
||
Each LOD chunk carries a `0x2046` header = **[near..far) viewing band**. The board selected the
|
||
LOD whose band contains the camera distance.
|
||
- **√3 range decode:** every stored band value is the authored euclidean distance ÷ √3 (92% of
|
||
the corpus is exactly nice-number/√3; hand-conversion typos prove it). Multiply parsed bands by
|
||
`√3` (`kLodRangeScale=1.7320508`). [T2]
|
||
- **Metric = the instance-transformed OBJECT ORIGIN distance**, ONE shared `d` for all LODs of an
|
||
object (a bounding-sphere-SURFACE metric desyncs sibling bands — wrong). [T2]
|
||
- **ADDITIVE_LODS** (object-level `SV_SPECIAL` token, 135 pod BGFs — all arena structures): at eye
|
||
distance d, EVERY LOD with `d < OutDist` draws (near detail ADDS onto coarser massing). Non-additive
|
||
keep first-LOD-only (the 2007 engine's own behavior). [T2]
|
||
- **PUNCH** (patch-level `SV_SPECIAL`, `dpl_Punchize` = FUN_00490308 → VPX board cmd `0x20`, tokens
|
||
`{0x80000003,0x80000017,0x80000004}` — a CONSTANT geogroup-modifier opcode, no color key; the
|
||
sibling `dpl_Damagize` FUN_004902b0 uses the same cmd with a different triple). Two cases: [T1]
|
||
- **Textured** punch (arena walls/catwalks): black texels = HOLES (alpha-test in the opaque
|
||
pass). Coplanar shells inside one LOD z-fight → depth-bias by submission ordinal. [T2]
|
||
- **FINAL mechanism (i860 VREND.MNG firmware, decoded 2026-07-11): a punch geogroup is a
|
||
THREE-CHUNK STENCIL-CUT KIT** -- in file order: aperture MASK (invisible per-pixel cutout),
|
||
visible HULL (the only colour-drawn chunk, drawn where not masked), byte-identical hull TWIN
|
||
(the damage-reset record, never visible). Visible result = hull-minus-apertures. Applies
|
||
to cockpit canopies, arena punch (AR02 etc.) and outside-torso punch (VUX_TOR) alike -- the
|
||
port's arena "black texels = holes" alpha-test path is a visual approximation that predates
|
||
this decode (works; revisit for exactness). Port: bgfload punch-kit roles + the L4D3D
|
||
stencil cut (D24S8). See [[cockpit-view]] + [[decomp-reference]] for firmware addresses.
|
||
[T1 mechanism / T2 render-verified all 8 cockpits]
|
||
|
||
## Shading model (from libDPL DPLTYPES.H)
|
||
Shading is selected PER-GEOMETRY by vertex type:
|
||
- **No-normal geometry** (XYZ_UV / XYZ_RGBA_UV: mesas/ground/sky/buildings/mech, WHITE baked
|
||
verts) is UNLIT, colorized by the material's 2-endpoint **RAMP** (see [[material-ramp]]).
|
||
- **Normal-bearing geometry** (XYZ_N_UV: ~150 vehicle/missile files) is LIT by the map light.
|
||
- **Applying the ramp to lit geometry = the "dusty white blobs" bug** — gate on `hasNormals`. [T2]
|
||
|
||
## Skeleton assembly
|
||
A mech is not one BGF — it's a `.SKL` skeleton (a DCS tree of joints) whose each joint's
|
||
`Object=part.bgf` is loaded + posed, then animated by `.ANI` clips. See [[asset-formats]] /
|
||
[[locomotion]] for skeleton + animation. [T1]
|
||
|
||
## The PNAME/PLACE name-plate atlas (the only TEXT-bearing BGF geometry) [T1/T2]
|
||
`PNAME1-8.BGF` + `PLACE1-8.BGF` (`content/VIDEO/GEO/`, 235/238 bytes) are the MP
|
||
scoreboard's 16 billboard plates — each ONE flat quad (4 verts, `XYZ_UV`, `PCONN` ppf=4,
|
||
**no LOD chunk = 1 implicit LOD**, no normals, no `SV_SPECIAL`). PNAME = 1.0×0.25 m,
|
||
PLACE = 0.5×0.25 m, planar Z=0. Consumers: `FUN_00454a70` (the scoreboard, resource type
|
||
0x46) and `BTReticleRenderable` @004cc40c (PNAME only). See [[open-questions]].
|
||
|
||
**They are the only geometry in the corpus whose texture is TEXT**, which makes them the
|
||
sole asset that can prove BSL row order and slice mapping — and they CONFIRM the port's
|
||
pair-swapped nibble decode (`image.cpp:122-125`): slices 0→3 come out as "PLAYER 1/2",
|
||
"3/4", "5/6", "7/8" in exact index order. [T2 — decoded + read visually]
|
||
|
||
One shared 128×64 texmap: `bmap:` → `VIDEO/MAT/BMAP.BMF` → 6 `TEXTURE` records
|
||
`bmap1_tex..bmap6_tex`, all `TEXTURE_MAP=bmap` with `BITSLICE` 0..5 → `VIDEO/TEX/BMAP.BSL`
|
||
(`DIV-BSL2`, 128×64, depth 4, texels @0x70). Cell map (UVs read straight from each BGF):
|
||
|
||
| material | slice | plates → UV sub-rect |
|
||
|---|---|---|
|
||
| `name12_mtl`→`bmap1_tex` | 0 | PNAME1 `v[.5,1]`, PNAME2 `v[0,.5]` (full U; 128×32 cells) |
|
||
| `name34_mtl`→`bmap2_tex` | 1 | PNAME3 `v[.5,1]`, PNAME4 `v[0,.5]` |
|
||
| `name56_mtl`→`bmap3_tex` | 2 | PNAME5 `v[.5,1]`, PNAME6 `v[0,.5]` |
|
||
| `name78_mtl`→`bmap4_tex` | 3 | PNAME7 `v[.5,1]`, PNAME8 `v[0,.5]` |
|
||
| `place1234_mtl`→`bmap5_tex` | 4 | PLACE1-4 = 2×2 grid of 64×32 cells (`u[0,.5]/[.5,1]` × `v[.5,1]/[0,.5]`) |
|
||
| `place5678_mtl`→`bmap6_tex` | 5 | PLACE5-8 = same 2×2 grid |
|
||
|
||
**The BSL is a RUNTIME COMPOSITING TARGET, not final art [T0 engine source].**
|
||
`DPLRenderer::MakeBitSliceStorage` (L4VIDEO.cpp:9888) allocates `uint32[128*64]`;
|
||
`LoadBitSliceTexture` merges a 1bpp egg BitMap into it; `FlushBitSliceTexture`
|
||
(L4VIDEO.cpp:5682, stubbed in 2007) did `dpl_LookupTexture("bmap:bmap1_tex")` +
|
||
`dpl_TexmapTexels2D(texmap, buf, 128, 64, 4)` — one upload rewrites ALL SIX slices. So the
|
||
1995 engine painted the **egg's callsign + ordinal bitmaps** onto these plates every mission;
|
||
the baked "PLAYER n" text is only the shipped DEFAULT. Two stacked 128×32 large-name bitmaps
|
||
per slice = exactly the PNAME v-halves; two stacked 128×32 ordinal bitmaps ("1st|2nd") = exactly
|
||
the PLACE 2×2 grid. Corollaries:
|
||
- **The shipped BMAP.BSL has only 4 distinct planes** — slice4 aliases slice0 and slice5
|
||
aliases slice3 (nibble3==nibble7, nibble4==nibble6; nibbles 0/1 zero). The ordinal art was
|
||
never baked, so a naive PLACE draw shows "PLAY"/"ER 1"/… fragments. [T1 measured]
|
||
- The placeholder cells are stored **scanline-reversed** vs the compositor's top-down order
|
||
(`LoadBitSliceTexture` writes y=0..31 top-down), so a naive PNAME draw gives the RIGHT
|
||
number upside-down. Don't chase it — implement the compositor instead. [T1 / T3 fix choice]
|
||
- These 6 materials carry **no DIFFUSE/AMBIENT/EMISSIVE/RAMP** — only `NAME`,
|
||
`MATERIAL_TEXTURE` and tag **`0x0027`** (12 B = {0.5,0.5,0.5}), which is
|
||
**`dpfB_MATERIAL_OPACITY_TAG`** — `[T0` `engine/MUNGA_L4/libDPL/dsys/PFBIZTAG.H]`, NOT specular
|
||
(that earlier [T4] guess is retracted). So the material contributes **no colour at all**: the
|
||
authentic plate is the **unmodulated callsign raster at ~50% opacity**. `bgfload` would fall
|
||
back to the grey placeholder `0xFFB0B0B8`; the reticle plate's 2D path therefore draws it as
|
||
`0x80FFFFFF` (white, half alpha) — see [[gauges-hud]] §Lock ring. And `decodeBSL` emits **alpha=255** for mono slices, so the plate
|
||
draws as an OPAQUE black rectangle — a billboard needs black→alpha-0 keying (which is exactly
|
||
what the 2D path's A4R4G4B4 `0xFFFF/0x0000` upload does).
|
||
|
||
## Key Relationships
|
||
- Detailed spec: `docs/BGF_FORMAT.md`.
|
||
- Consumed by: [[rendering]] (the D3D9 draw path), [[locomotion]] (skeleton parts).
|
||
- Related: [[asset-formats]] (BMF/BSL/SKL/ANI), [[decomp-reference]].
|