Files
BT411/context/bgf-format.md
T
arcattackandClaude Opus 5 6c3fca2f67 Gitea #38 ROOT CAUSE + FIX: mech paint lost on geometry re-load (view toggle / respawn)
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
2026-07-24 19:06:04 -05:00

128 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]].