KB + handoff: searchlight reconstruction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
412053d5af
commit
d44fae2ae3
@@ -1,385 +0,0 @@
|
||||
---
|
||||
id: rendering
|
||||
title: "Rendering — the D3D9 pipeline, materials, sky, beams, performance"
|
||||
status: established
|
||||
source_sections: "PROGRESS_LOG.md §10 (render-fidelity notes); docs/BGF_FORMAT.md"
|
||||
related_topics: [bgf-format, asset-formats, wintesla-port, locomotion, pod-hardware, cockpit-view]
|
||||
key_terms: [L4D3D, material-ramp, BSL, LOD, DPL, beam]
|
||||
open_questions:
|
||||
- "Replacement-LOD for NON-additive multi-LOD models"
|
||||
---
|
||||
|
||||
# Rendering
|
||||
|
||||
The port renders via the WinTesla `L4D3D`/`L4VIDEO` Direct3D9 path ([[wintesla-port]]), carrying our
|
||||
BT loader (`bgfload.cpp`) + fidelity work. The goal: match the authentic pod footage. Geometry/LOD
|
||||
mechanics are in [[bgf-format]]; this topic covers the D3D9 draw path + the fidelity fixes. Full
|
||||
detail: `docs/PROGRESS_LOG.md §10`.
|
||||
|
||||
## The IG-board shading model (from libDPL DPLTYPES.H)
|
||||
Shading is selected PER-GEOMETRY by vertex type: **no-normal geometry** (terrain/mesas/sky/buildings/
|
||||
mech — WHITE baked verts) is UNLIT, colorized by the material's 2-endpoint **[[material-ramp]]**
|
||||
(`dpl_SetMaterialRamp` — texture luminance indexes a low→high gradient); **normal-bearing geometry**
|
||||
(~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]
|
||||
Exception: cockpit-frame (`*_cop`) batches are pure-emissive constant colour (0.13,0.12,0.15),
|
||||
`BT_COP_FRAME` overrides — see [[cockpit-view]] (task #55; the old `BT_COP_RAMP_L` ramp-index
|
||||
approach is retired). [T2]
|
||||
**VERTEX-ALPHA EFFECT CARDS (2026-07-12) [T1 corpus-swept]:** exactly 14 shipped BGFs carry
|
||||
per-vertex float RGBA with alpha < 1 (tags 0x0082/0x008A, RGBA at +12, values OUTSIDE [0,1] —
|
||||
clamp): FLAMEBIG/FIRE5 (flames), MUZFLASH, EXDISK_A/B/C, TMST_A/B/C, BLUEBEAM/GREEBEAM/RED_BEAM/
|
||||
PPC, DECLOUDS — the EFFECT-CARD family. Their authored gradient (white-hot base → dark tip fading
|
||||
to alpha −0.2) IS the shading: drawn opaque with a flat batch colour they render as solid shapes
|
||||
(the wreck flame read as a "twisted drill bit"). The loader now keeps the authored per-vertex
|
||||
RGBA for any batch with vertex alpha < 1 (`BgfDrawBatch.vertexAlpha`) and L4D3D routes it to the
|
||||
ALPHA-BLEND pass, unlit, colour = texture × gradient, alpha = vertex fade (SRCALPHA/INVSRCALPHA).
|
||||
SKY objects are excluded from the routing (drawAsSky + alphaTest passes NEITHER pass filter —
|
||||
DECLOUDS must stay in the sky pass). Authored TEXTURE **SCROLL** (BMF TEXTURE tag 0x2037
|
||||
`" SCROLL u0 v0 du dv"`, e.g. firesmoke1 = 0.05,−0.331 uv/s) is now parsed and propagated to
|
||||
`L4TEXOP::doScroll` — scrolling materials (firesmoke flame cards) animate in the model path. [T2]
|
||||
|
||||
## The big render-fidelity fixes (dbase/desert matched to pod footage)
|
||||
- **Geometry:** CONN is a flat trilist (not fan); CONN+PCONN coexist — see [[bgf-format]]. Malformed
|
||||
fan-garbage was the "faceted mountains" bug. [T2]
|
||||
- **RAMP colorize** (default-on `BT_RAMP`) + **day/night material priority** (default-on `BT_MATPRI` for
|
||||
the ramps): the `mat\day` libs carry the warm ramps; the generic GEO libs are gray. Terrain/mesas
|
||||
render warm tan; the sky's `dsky_mtl` ramp (0→0.99) gives bright clouds. `hasNormals` gate keeps
|
||||
vehicles LIT (colored trucks). Ramp tint rule: explicit diffuse + NEUTRAL ramp ⇒ tint; coloured ramp
|
||||
⇒ white. [T2]
|
||||
- **Sky:** a `*sky.bgf` dome/plane drawn in a dedicated fullbright **PASS_SKY** (LIGHTING off, extended
|
||||
far=9000 + world fog); tagged `drawAsSky` at load (the `.x` sidecar path doesn't exist). [T2]
|
||||
- **The env pipeline** (`DPLReadEnvironment`→`DPLReadINIPage`, reads **`content/BTDPL.INI`** via
|
||||
`L4DPLCFG`): resolves map+time → clip/fog/ambient + a directional sun. Bring-up stubs
|
||||
(EnsureValidProjection / per-frame ambient / uninit cloud colours) had clobbered it — now they
|
||||
RE-ASSERT the env values. Debug: copy BTDPL.INI → *DBG.INI with `debug=True`. [T2]
|
||||
- **Fog — per map/time/WEATHER (task #63).** `BTDPL.INI` authors `fog=<near> <far> <r> <g> <b>` (+ often
|
||||
`nosearchlightfog=`) on EVERY map/time/weather leaf. WEATHER is a fixed authored egg field
|
||||
(`clear`/`fog`/`soup`, `mission->GetMissionWeather()`; missing = fatal in the arcade); the env pipeline
|
||||
branches location→time→weather to the leaf. Shipped eggs pin `cavern/night/clear` → page `dsnitclear`
|
||||
(near 90, far 1100, dark blue 0.1,0.1,0.12); `fog`→`dsnitfog` (500), `soup`→`dsnitsoup` (300). The
|
||||
arcade fog model is per-PIXEL linear (`dpl_fog_type_pixel_lin`, dpl.h:265). **It rendered INVISIBLE in
|
||||
the port:** `D3DRS_FOGTABLEMODE=D3DFOG_LINEAR` (table fog) reads the perspective-NONLINEAR z-buffer, so
|
||||
without WFOG the authored 90..1100 range collapsed to fog-factor≈1 (no fog). FIX (L4VIDEO.cpp world
|
||||
pass ~8194): auto-detect `D3DPRASTERCAPS_WFOG` → per-pixel W-fog (smooth, == the arcade); else VERTEX
|
||||
fog (eye-space but per-vertex → splotchy on coarse terrain tris); FOGENABLE + mode re-asserted each
|
||||
world-pass frame (the old one-time set got clobbered per-pass). **Task #20's "shipped maps define no
|
||||
fog" was WRONG** — it checked `.MAP`/`.RES`, but fog lives in BTDPL.INI (the DPL env INI), as in the
|
||||
arcade. Night fog is dark BY DESIGN (distance→murk, not light haze); day pages author light haze. Env:
|
||||
`BT_FOGMODE=table|vertex|off`, `BT_WEATHER=clear|fog|soup`, `BT_FOG="near far r g b"` (all branches);
|
||||
`[fog] resolved`/`[fog] model`/`[fogstyle]` (BT_FOG_LOG) log lines. [T1 pipeline / T0 arcade model / T2 fix]
|
||||
**⚠ SEARCHLIGHT SWAP IS INERT — and it was inert in the ARCADE too (task #63 workflow, 2026-07-13).**
|
||||
The arcade swaps the active fog between `fog=` (headlight ON) and `nosearchlightfog=` (OFF,
|
||||
darker/tighter) via `PullFogRenderable::Execute` (L4VIDRND.cpp:4601, edge-triggered on two light attrs →
|
||||
`SetFogStyle(searchLightOn/OffFogStyle)`). TWO independent reasons it never fires:
|
||||
(1) **Port:** `PullFogRenderable` is NEVER CONSTRUCTED (grep: only its .h decl + .cpp def; no `new`);
|
||||
`searchLightOn/Off` is called from nowhere else (BT_FOG_LOG: over 22s only `SetFogStyle(0)`, zero 2/3).
|
||||
(2) ~~**Original 1995 LATENT BUG**~~ — ❌ **RETRACTED 2026-07-25 (#61). There is no 1995 bug.** The
|
||||
claim rested on a **swapped table attribution**: @004b860c is **ThermalSight's** `ToggleLamp`
|
||||
(table @0x51120C), not Searchlight's, so pitting its `0x1DC` write against Searchlight's
|
||||
`SearchlightSimulation` (@004b841c, reads `requestedOn`@0x1E0) compared two different classes and
|
||||
manufactured a missing "0x1DC→0x1E0 bridge". Searchlight's own handler is **@004b838c** (table
|
||||
@0x51117C) and toggles the field its own Performance reads, `0x1E0` — exactly as self-consistently as
|
||||
ThermalSight does with `0x1DC`. [T1 attribution: un-pooled `"ToggleLamp"` strings sit adjacent to
|
||||
their own class names, `section_dump.txt:69661-69711`. Body: @004b838c sits in a Ghidra export gap
|
||||
(#60) but was **recovered by raw disasm** (`scratchpad/dis838c.py`) — it toggles 0x1E0. All T1.]
|
||||
**The searchlight DID light in the arcade, and the fog swap was NOT inert there.** Searchlight's
|
||||
handler is now wired (#61) and **verified live**: pad `0x14` → `requestedOn 0→1` → `lightState 0→1`.
|
||||
So the ONLY remaining blocker is (1), the un-constructed `PullFogRenderable` — and building it is
|
||||
**FAITHFUL, not a designer-intent deviation** (the old "DECISION 2026-07-13: left as-is" is void;
|
||||
the sim needs NO repair). Remaining work: construct `PullFogRenderable` at btl4vid.cpp
|
||||
`MakeMechRenderables` (the reticle-build/inside pass, == arcade part_014.c:5173, ExecutionType
|
||||
Dynamic, bound to each Searchlight `lightState`). See [[open-questions]]. Core per-map/time/weather
|
||||
fog is unaffected.
|
||||
- **Projection / stencil (task #55):** the BTDPL.INI `viewangle=60` is the **HORIZONTAL** FOV —
|
||||
authentic 60×47 frustum at 4:3; fovY derived via `BTFovYFromHorizontal` (L4VIDEO.cpp), applied at
|
||||
all 5 projection sites (it was mis-applied as vertical). The device depth-stencil is now
|
||||
**`D3DFMT_D24S8`** for the cockpit punch stencil-cut (L4VIDEO.cpp:2799; sequence
|
||||
L4D3D.cpp:1199-1248) — see [[cockpit-view]]. [T2]
|
||||
|
||||
## Backface culling (default CW, `BT_CULL`)
|
||||
The bring-up `D3DCULL_NONE` drew interior/back faces (the "dark wedge" shapes = mesh insides). CW is
|
||||
correct for the BGF quad triangulation `[a b c][a c d]`. [T2] Exception: cockpit `*_cop` batches
|
||||
default DOUBLE-SIDED (`BT_COP_SINGLE=1` restores single-siding, diag) — see [[cockpit-view]]. [T2]
|
||||
|
||||
## LODs (default: shipping-engine first-LOD + ADDITIVE_LODS; `BT_ADDLOD`/`BT_LOD_SCALE`/`BT_PUNCH`)
|
||||
See [[bgf-format]] for the √3 range decode, the ADDITIVE_LODS cumulative rule, PUNCH cutouts, and the
|
||||
coplanar-shell submission-order depth bias. Replacement LOD selection for NON-additive multi-LOD
|
||||
models is deferred (needs the board's hot-spot semantics). [T2]
|
||||
|
||||
## Mech skins (BSL bit-slice — the rainbow-mech fix)
|
||||
`.BSL` is a bit-sliced container (6 grayscale sub-images per file); the BMF `0x18 BITSLICE` tag picks
|
||||
the slice. Reading it as RGB overlaid 2-3 gray sheets = the "rainbow/graffiti" bug. Fixed:
|
||||
`image.cpp decodeBSL` slice-decodes; `bgfload` threads the 0x18 channel; a cross-library RAMP registry
|
||||
resolves the mech-skin `softer` ramp (defined in OTHER libs). Every mech renders the authentic BASE
|
||||
gray-metal skin now. See [[asset-formats]]. [T2]
|
||||
|
||||
## Per-pilot mech PAINT (color/badge/patch substitution) — NOT WIRED (2026-07 audit)
|
||||
The "missing warning stripes / coloration" report is the per-pilot paint layer, fully authored but
|
||||
never engaged by the port. Mechanics (all [T1] from the BMF/BGF/vehicletable content + FUN_004d0cc0):
|
||||
- Mech BGFs reference the BASE lib only (`blhskin:gen2_dz_rfoot_mtl` …). The binary rewrites lib
|
||||
prefixes at load via the `vehicletable` resource `[substitute]` templates: `skin:` → `_%color%%serno%:`
|
||||
(`blhskin:` → `blh_wht0:`) and `skin:lgo` → `<<<emblem%serno%:%badge%_%patch%>` (emblem pages →
|
||||
`EMBLEM0.BMF` `lgoN_<patch>_dz_*` materials). Pilot egg values (color/badge/patch) map through the
|
||||
`[color]/[badge]/[patch]` sections to the codes.
|
||||
- The COLOR libs (e.g. `BLH_WHT0.BMF`) are byte-identical to the base lib EXCEPT they add tags
|
||||
`0x23`(diffuse RGB) + `0x24`(ambient RGB) = the pilot color to the ~35 paintable `gen*/mech*_dz_*`
|
||||
materials (base leaves them untinted). 7 colors: wht(.99,.99,.99) red(.90,.11,.10) grn(.63,.83,.52)
|
||||
gry(.65,.60,.65) brn(.60,.44,.28) tan(.65,.55,.43) blk(0,0,0). All 8 sernos are byte-identical
|
||||
(verified: 0 of 193 groups differ) — `%serno%` was future variety, never used. `<mech>SKIN.BMF`
|
||||
(bare) == `<mech>SKIN0.BMF`.
|
||||
- WARNING STRIPES are grayscale hazard-stripe art in the shared `GEN.BSL` pages (slice 1 = `gen2_tex`,
|
||||
top-left), shown through the paintable-zone materials and tinted by the pilot color. `EMBLEM0.BMF`
|
||||
carries `lgoN_<patchcolor>_dz_ld/rdleg` materials (8 patch tints incl. yellow .89,.73,0) over
|
||||
`LGO.BSL` slices 0-5 (slice 5 = VGL winged logo).
|
||||
- **WIRED 2026-07-17 (same-day fix), verified live**: MP MadCat renders crimson with hip hazard
|
||||
stripes + yellow VGL leg emblems; Blackhawk white + emblems; replicants painted on both nodes.
|
||||
The reconstruction: (a) `Mech::resourceNameA/B/C` are real `CString` members (the binary's exact
|
||||
16-byte `CStringRepresentation` {size,len,text@+8,refs@+0xc}; ctor = part_012.c:9841-71, deep-copy
|
||||
bind from the MakeMessage = `CString::operator=(const char*)` == FUN_00402a98 @10427-41, implicit
|
||||
member dtors = 11388-403) with paint-name accessors; (b) `SetupMaterialSubstitutionList` reads them
|
||||
(real egg names; `[paint]` log line); (c) `dpl_SetMaterialNameCallback` is REAL now
|
||||
(L4VIDEO.cpp registry, cleared first in TearDown as FUN_004d11e8 does) and
|
||||
`bgfload MaterialResolver::resolve()` runs every name through `dpl_ApplyMaterialNameCallback`;
|
||||
no mesh-cache issue (LoadObject/LoadBgfFile don't cache — every load resolves fresh). [T2]
|
||||
- DEVIATION (documented, btl4vid.cpp): on a vehicletable miss the binary Fail()ed ("Exiting"
|
||||
@BTL4VID.CPP:0xbeb); the port warns + drops that placeholder (RP analogue). Real case: dev eggs
|
||||
ship `color=Red` but the `[color]` table only has **Crimson** — MP_BHMC.EGG fixed to Crimson;
|
||||
LAST.EGG pilot-2 still says Red (unused slot). The 7 valid egg colors:
|
||||
Black/Brown/Crimson/Green/Grey/Tan/White; badges Davion/Kurita/Liao/Marik/None/Steiner/VGL;
|
||||
patches Black/Blue/Green/Grey/Red/Violet/White/Yellow. [T1]
|
||||
|
||||
## The OLD-STYLE SPECIALFX layer (the weapon-impact action) — RECONSTRUCTED 2026-07-12 (firmware-decoded)
|
||||
The per-weapon impact visuals are fully AUTHORED in `content/BTDPL.INI` `[effects_to_load]`:
|
||||
`specialfx3..15` = Chunks1/Chunks2/Sparks/Explosion3/MachineGun/PPCHit/LaserHit/MissileHit/
|
||||
CannonHit/GroundHit/MinorHit/MajordHit/Fireball — "old-style" (version-1) descriptor sections
|
||||
(`texture/type/size/velocity/v_bias/y_off/cook(3f)/varience/gravity/cool(2f)/count/repeats`),
|
||||
most on `btfx:firesmoke1_scr_tex` (MissileHit size 12 ×7; Fireball size 14, gravity −1.5 rises;
|
||||
Chunks1/2 = ballistic debris vel 40-55 grav 34-35; Sparks NOTEX). [T1: the 1995 binary parses
|
||||
them — part_008.c:1546-1590, field strings @004f25e4-004f2663 → installer FUN_00490210 packs a
|
||||
0x44-byte board command via FUN_0048e2c0.] **Simulation semantics (type/cook/cool) live in the
|
||||
i860 firmware** `content/VREND.MNG` (same decode workflow as the task-#55 'damageize' handler).
|
||||
- **EFFECT-NUMBER ROUTING (corrects the old claim):** `effect_number < 100` = the dpl board
|
||||
EFFECT TABLE = this specialfx list (1996 case-4 comments match: 3=Chunks, 12=GroundHit,
|
||||
15=Fireball), **NOT** a psfx slot; `>= 1000` = psfx slot n−1000 (idam/ddam PFX); 100-999 =
|
||||
ExplosionScripts. The port's `<100 → psfx slot` mapping renders PPCHit(8) as ddam5 smoke and
|
||||
Chunks(3)/Sparks(5) as ddamN smoke — the "hits are just a little smoke" report. The 2007
|
||||
WinTesla loader SKIPS all version-1 descriptors (`if (version < 2) continue;` — it only reads
|
||||
the 1998 INDIE format), which is why the layer was never seen. [T1]
|
||||
- **Live hit census (mp logs):** weapon hits dispatch idam bands 1009-1013 (30×1009) + raw
|
||||
8/9 (PPCHit/LaserHit, mis-rendered); **MissileHit(10)/CannonHit(11) never appeared** — the
|
||||
projectile/salvo damage path likely doesn't thread the firing launcher's subsystemID into the
|
||||
damage records, so messmgr never queues its explosion. [T2 census; T4 cause]
|
||||
- **RECONSTRUCTION (2026-07-12, firmware-first):** the i860 sim decoded from VREND.MNG
|
||||
(dispatch table at data+0xdd0c, VA base: code 0xf0400000, data 0x1000; handlers: sfx trigger
|
||||
0xf040cda0→ca48, install 0xf040cdc0→cba0, step near 0xf0413698, spawn near 0xf0414afc —
|
||||
string-anchored via "attempt to step a NULL sfx object"; disassembler REBUILT at
|
||||
`scratchpad/i860dis.py` from binutils i860 opcodes, `scratchpad/i860_opcodes.h`).
|
||||
**The heat model [T1]:** per 30Hz board frame `h *= cool_a`; `RGB_ch = h_old*(h_new*cook_ch
|
||||
− 0.25) + 0.25` (K=0.25 @VA 0xF080 = the ember floor); kill at h ≤ 1e-4 (@0xF0A0); alpha −=
|
||||
cool_b per frame; `pos += vel`, gravity as accel. y_off = the particle KILL PLANE
|
||||
(Chunks −1 = die at the ground) [T3 data-indicated]. Burst frame randomly spun (quadratic
|
||||
small-angle rot, θ from the host-fed random table cmd 0x21). Implemented in L4VIDEO.cpp:
|
||||
`BTInstallSfx`/`BTStartSfx` + sfxMode particles in the BTPfx layer; parser beside the psfx
|
||||
page walk (`[sfx] page 'effects_to_load': 13 old-style specialfx installed`). Effect-number
|
||||
routing fixed in BOTH consumers (ExplosionClassID + DPLIndependantEffect): <100 → specialfx,
|
||||
≥1000 → psfx slot n−1000. **Missile/cannon hits now bundle** [T2]: BTProjectile carries
|
||||
shooter + weaponSubsys; contact routes through the SHOOTER's SubsystemMessageManager
|
||||
(AddDamageMessage w/ subsystemID → mslhit/acanhit explosion at the impact), mirroring the
|
||||
laser path's SendDamageMessage. Cone shape (v_bias as up-bias) + per-type draw weights are
|
||||
[T3]; the full type-0/1/2 draw-geometry decode in the firmware draw fn (0xf0416160) remains
|
||||
open.
|
||||
|
||||
## Weapon-hit forensics closeout (2026-07-12, workflow-verified) [T1]
|
||||
- **The HIT PACKAGES (BTL4.RES model lists, byte-verified):** ppchit=[8], lzrhit=[9],
|
||||
mghit=[7,11], canhit=[11] (no "acanhit"), mslhit=**[10, 12, 1023]** (MissileHit + GroundHit +
|
||||
psfx23 SHKWAVE), gndhit=[12], minhit=[1002], majhit=[1003], explode=[6]; dam1-4hit=[1002-1005],
|
||||
desthit/stephit=[1008]; cockpit "i" variants (lzrhiti etc.) = same sfx + idam 1009-1013; ALL
|
||||
eight mech death lists = ["104","3","4","5","1007","15","1001"].
|
||||
- **"Silver mist exactly 5×, perfectly timed" ROOT CAUSE:** SHKWAVE.PFX authors maxIssue=5,
|
||||
releasePeriod=0.2, **rate=1** — the ONE shipped .PFX where rate ≠ maxIssue/releasePeriod; the
|
||||
emitter trusted 'rate' → 5 huge white shells (exp 350/s) at exactly 1Hz. **Emission-window
|
||||
rule hardened:** effective rate = maxIssue/releasePeriod always (BTDrawPfx emitter).
|
||||
- **SpecialFX palette [T1 firmware]:** instance init @0xf04128d8: inst[0x19c]=cool_a (per
|
||||
board-frame heat decay), inst[0x1a4]=cool_b (× raw dt in the step = PER-SECOND fade),
|
||||
inst[0x188]=7.0s cap, heat=1.0. The heat channels are BRIGHTNESS over an inherently
|
||||
FIRE-coloured texture (firesmoke through its material ramp): sfx batch draws the FIERY-ramped
|
||||
sheet, psfx keeps GRAYSCALE (authored colours decide — DNBOOM orange, DDAM gray). Drawing sfx
|
||||
grayscale = silver mist; scaling fade ×30 = 0.1s pops ("nothing happens on laser hits").
|
||||
- **Damage-band .PFX shapes are authored GENTLE (puffy clouds):** bands 1-4 of ndam/idam/ddam
|
||||
rise at 1-4 u/s with buoyant accel; ONLY band 5 + the BOOM/SHKWAVE files are explosive-omni.
|
||||
idam = scaled-down WORLD venting smoke (half radius/counts of ddam), not cockpit-only.
|
||||
N-set = night = same physics as D-set with fewer particles. NSRM/DSRM trails = 0.1s overbright
|
||||
ORANGE directional puffs (never silver).
|
||||
- **messmgr explosion DEDUP CONFIRMED [T1, decomp-read @0049b784]:** `Find` (weaponExplosions
|
||||
vtable slot 0xC) gates `AddValue` (slot 8) — ONE explosion per unique weapon resource per
|
||||
frame, exactly as reconstructed. (A workflow agent claimed "one per record, staggered
|
||||
0.1s" — REFUTED; the stagger applies to the deduped queue.) Missile multiplicity comes from
|
||||
the ORDNANCE side instead: Missile::MoveAndCollide @004bef78 detonates per round — ported as
|
||||
BTSpawnRoundDetonation (mech4) + rack-tube launch spread [T3]. Alpha-strike drama = distinct
|
||||
per-weapon-TYPE packages (lzrhit+ppchit) + the victim's band fire, not explosion count.
|
||||
- Remaining [T3]: sfx cone shape (v_bias as up-bias), size ×0.5 + 1.25m textured floor +
|
||||
hot-phase occlusion 0.45..0.9, burst scatter ×0.2; trail
|
||||
density is frame-rate-dependent (2/frame @60fps = 2× pod density); .PFX 'repeats' unconsumed
|
||||
(all shipped files author 1); replicant visual salvo FIXED (2026-08-02, #84): the
|
||||
update record now carries the locked target's EntityID; the mirror resolves it
|
||||
locally (HostManager::GetEntityPointer, engine T0) and homes at the LIVE
|
||||
replicant, as the binary's GHOST missiles did (CLASSMAP: Projectile
|
||||
PTR_LAB_005129e8 authoritative / 005129f4 ghost performances) -- peers now see
|
||||
detonations at the true position. Point fire / local-only targets (wrecks,
|
||||
hostID -1) still fly to the frozen aim, which is correct for them.
|
||||
|
||||
## Weapon beams (the unported dpl_* layer)
|
||||
The `dpl_*` beam renderable was never ported → beams are drawn by `BTPushBeam`/`BTDrawBeams`
|
||||
(L4VIDEO). The real look is the `ermlaser.bgf` TUBE (native Z 0..−2000, UVs tiled) with the scrolling
|
||||
`bexp` grit texture (from the `.VMF` `SCROLL`). ER lasers red, PPC blue, standard yellow. The beam
|
||||
origin re-anchors to the live muzzle every frame (can't run out from under your own beam). See
|
||||
[[combat-damage]] for fire-rate/muzzle. [T2]
|
||||
|
||||
## Translocation warp (the respawn vortex) — see [[translocation-warp]]
|
||||
The death/respawn "blue whirlwind" is `tsphere.bgf` (a 12-facet bicone) spun on-axis with the `bintA`
|
||||
cloud through a wide lavender ramp, drawn as SKY. Full geometry/material/lifecycle/env in
|
||||
[[translocation-warp]]. ⚠ **Rendering-wide fix that came out of it:** `L4D3D::SetTextureScrolling`
|
||||
computed its texture-matrix offset as `scrollDelta × absolute_time`, which grew unbounded and collapsed
|
||||
UV float precision → grainy/spoked scrolled textures the longer a session ran. Wrapped with `fmodf(…,
|
||||
1.0f)`. This also cleans up the scrolling `bexp` beam grit and any other `SCROLL` material. [T2]
|
||||
(Bug class: [[reconstruction-gotchas]] §13.)
|
||||
|
||||
## Performance — the 10fps→60fps fix (~50-80× render speedup)
|
||||
The 10fps baseline had THREE stacked causes, all fixed (engine tree): (1) **`D3DCREATE_SOFTWARE_
|
||||
VERTEXPROCESSING`** — every vertex transformed+lit on the CPU → HARDWARE first; (2) D3DX `DrawSubset`
|
||||
attribute-table scan (our double-sided tris broke OptimizeInplace) → explicit `SetAttributeTable` +
|
||||
direct `DrawIndexedPrimitive`; (3) no frustum culling → bounding-sphere culling per frame. ⚠ LESSON:
|
||||
the user plays the DEBUG build, but the 10fps was NOT the debug build (RelWithDebInfo identical) —
|
||||
split draw-vs-present + check the DEVICE flags before blaming the compiler. Diagnostics kept:
|
||||
`[spike]/[loadobj]/[rstat]`. [T2]
|
||||
|
||||
## Cultural-icon (map prop) DESTRUCTION — revived 2026-07-19 (Gitea issue #3) [T2]
|
||||
Trucks/props/buildings are **CulturalIcon/Landmark** entities (`engine/MUNGA/cultural.h`, map-
|
||||
streamed via INTEREST.cpp; ARENA1 = 124 icons, each with a damage zone, `ExplosionModelFile`,
|
||||
`removeOnDeath`). The SIM was always complete [T0]: ram/weapon damage → `CulturalIcon::
|
||||
TakeDamageMessageHandler` → zone `damageLevel ≥ 1` → **Explosion entity** (Crunch variant for
|
||||
collision) + `WaitingToBurn` (+`timeDelay`) → `BurningState` + `UpdateCollisionVolumes` (boxes
|
||||
DELETED on `removeOnDeath`, else sunk by `destroyedYTranslation`). The mech-side crunch dispatch
|
||||
(`mech4.cpp ProcessCollision` → `BTDispatchCollisionDamage`, RAW since the 2026-07-31 pricing
|
||||
audit — the ×0.001 era needed ≈19 walking-speed bumps to kill the spawn-yard truck; raw is the
|
||||
binary's own dispatch [T1], so a mech contact now crunches a truck-scale icon outright, per the
|
||||
authored icon armor). What was BROKEN (the "no state change"
|
||||
report): the 2007 port fully stubbed **`StateInstanceSwitchRenderable`** ("STUBBED: DPL RB
|
||||
1/14/07") — the intact→rubble visual swap NEVER fired. Revived D3D9-native: the switch now
|
||||
controls the draw component via the **`SetDrawObj` in-place drawable swap** (the mech
|
||||
RemakeEntity mechanism); `[video] Object` hides at BurningState, `Rubble` shows; watcher =
|
||||
`StateIndicator::AddVideoWatcher` (SetState DOES fire video watchers). ⚠ REQUIRED gotcha:
|
||||
cultural objects must stay **OUT of static-mesh consolidation** (`RecurseStaticObject` — same
|
||||
exclusion class as banded LODs/shadows), else the merged mesh draws the intact model forever
|
||||
regardless of DrawObj. **RUBBLE PLACEMENT FIX (2026-07-19, pixel-verified via BT_SHOT):** the
|
||||
rubble originally never appeared because additional cultural objects were built as
|
||||
DPLStaticChildRenderable/DCSInstanceRenderable at TOP level — those read the bare matrix stack
|
||||
(identity), so the rubble drew at the WORLD ORIGIN. Every cultural video object is now a
|
||||
`RootRenderable` (places from `myEntity->localToWorld`). Screenshot timeline confirms: intact
|
||||
MECHMOVR truck → ram death → MECHMD wreck debris scattered at the truck's spot.
|
||||
**(a) SCORCH QUAD FIXED (2026-07-19, pixel-verified, awaiting human confirm):** the scorch base
|
||||
(MECHMD patch 0, 4-vtx no-normal quad, `basev:bvx9_mtl` → bexp9_tex = BEXP.BSL **RGBA4444
|
||||
slice 8**, ramp `cdusty`) drew as a hard dark SQUARE for TWO stacked reasons: (1) the blanket
|
||||
"truecolor slice (channel ≥ 6) never ramps" gate blocked its cdusty ramp — but only 4 shipped
|
||||
textures use truecolor slices, and bexp9/bdet9 are GRAYSCALE in RGB (100% / 98.8% r==g==b
|
||||
texels; only bdam8 is truly coloured) — `bgfload rampableSlice()` now probes the decoded slice
|
||||
(a truecolor slice ramps iff effectively gray, ≥95%; bdam8's colour stays protected); (2) the
|
||||
RGBA4444 **authored alpha** (a binary 0/240 splat CUTOUT mask, 78.5% transparent) was never
|
||||
alpha-tested — new `BgfDrawBatch.texAlpha` (channel ≥ 8) routes through the PUNCH alpha-test
|
||||
draw states WITHOUT the black-texel keying (which would hole the near-black charred centre).
|
||||
Side benefit: tree9 (GRASS tree/leaf cards) + bdet9 (trans-rail lattice) get their authored
|
||||
cutouts too. Post-death shot: irregular char splat, dark brown (71,44,34) lifting to
|
||||
near-terrain tan (115,100,91 vs terrain 131,119,108), no rectangle; pre-death frames
|
||||
unregressed (vehicles stay lit/diffuse — the `hasNormals` gate is untouched). [T2]
|
||||
**(b)+(c) CLOSED (2026-07-19, pixel-verified, awaiting human confirm).** The icon death
|
||||
EXPLOSION packages are authored per KILL TYPE [T1, BTL4.RES byte-dump]: the CRUNCH
|
||||
res 31 = `stephit` (ONE video object, effect **1008** = ddam5 damage smoke) — the ram/step
|
||||
squash is SMALL **BY DESIGN**; the full res 32 = `trkdead` = **1015 dtrkboom.pfx** (fiery
|
||||
omni burst: vel 150, rad 3.5 exp 10, orange 2.0/0.7/0.2→1.0/0.3/0, ~1.5s) + **1016
|
||||
dtrkburn.pfx** (the BURNING-WRECK FIRE: 30 rising fire-card particles over a 5s emission,
|
||||
4±1s each ≈ a ~9s fire at the wreck). The whole icon family shares the pattern:
|
||||
bigdead=[1017 dbigboom,1016], meddead=[1018,1016], msldead/twrdead=[1017,1016],
|
||||
smldead=[1020,1022,1021]. **Item (c) verdict: the burning fire is the dtrkburn ONE-SHOT
|
||||
authored INTO the death package** — there is NO looping BurningState fire: the
|
||||
damage-zone effect watcher ctor @0042a984 has exactly ONE call site binary-wide (the
|
||||
Mech ctor, part_012.c:10405), so icon ExplosionTables are inert, and CulturalIcon has no
|
||||
Performance (AlwaysExecute is replication flush). Documented as authentic. **Item (b):
|
||||
the res-32 path is reached by WEAPON kills, which the binary DOES dispatch [T1]:**
|
||||
`MechWeapon::SendDamageMessage` @004b9728 (part_013.c:6765) gates only "target NOT
|
||||
derived from Mech@0x50bdb4 OR aimed zone set" — a non-Mech target under the boresight is
|
||||
damaged UNCONDITIONALLY (zone −1; the icon handler maps −1→0) — and the Missile contact
|
||||
branch @004be078 dispatches a zone=−1 TakeDamageMessage at the struck solid's OWNER with
|
||||
no class test at all. The port never delivered this because the structure pick designated
|
||||
the `gBTTerrainEntity` SENTINEL; now `Mech::WorldStructurePick` returns the struck
|
||||
solid's owning entity (`BoxedSolid::GetOwningSimulation` — every static solid is built
|
||||
with its Terrain/CulturalIcon/Door entity as owner, TERRAIN.cpp:107/246) and mech4.cpp
|
||||
designates IT (mech.cpp `WorldStructurePick`, mech4.cpp pick block + projectile contact
|
||||
else-branch). Truck armor is `WeaponDamagePoints=1` (TRK.DMG) — one laser kills it.
|
||||
Verified live: 26 laser kills `[cult] TakeDamage type=3` → `DYING … res=32` →
|
||||
`DPLIndependantEffect 1015+1016` at the icon origin → BT_SHOT frames show the orange
|
||||
dtrkboom fireball, dtrkburn flames on the fresh wreck, and the rubble aftermath; ram
|
||||
harness re-run still fires res 31 (crunch unregressed). All 24 psfx slots (incl. 15/16)
|
||||
load from the BTDPL.INI pages. Diag: `BT_FIRE_AT_ICON` (designate nearest ahead icon),
|
||||
`BT_FX_TEST="1015,1016"`. LESSON: `[cultvis]` log lines verify
|
||||
the STATE MACHINE, not pixels — only a screen check (BT_SHOT / human) verifies rendering.
|
||||
Diags: `BT_CULT_LOG` (census/damage/death + `[cultvis]` + `[cultobj]` per-object file/type/op
|
||||
flags). MP: replicants transition via `ReadUpdateRecord` → same SetState watcher path (untested
|
||||
cross-pod).
|
||||
|
||||
## Mech ARMOUR DARKENING (the .DZM material-damage system) — RECONSTRUCTED 2026-07-31 (Gitea #87) [T2]
|
||||
A mech's armour panels **darken as their damage zone takes damage** — a per-zone, per-material
|
||||
colour ramp-down, NOT a texture swap, a filter, or geometry. Players reported the port showing only
|
||||
the limb swap ("the actual enemy mech in external view is not showing darkened armor panels").
|
||||
|
||||
**The 1995 chain** [T1]:
|
||||
- **`.DZM`** (`VIDEO\<mech>SKIN.DZM`, 40 shipped) lists, per damage zone, the materials that zone
|
||||
paints: `[dz_ltorso] material=avaskin:avat2_dz_ltorso_mtl`. One file per SKIN VARIANT — the
|
||||
`dzm`/`dzms`/`dzmt`/`dzmo`/`dzma`/`dzmb`/`dzmc`/`dzmd` key family (same suffix set as the
|
||||
`destroyed`…`destroyedd` video variants); the res-compiler address is UNKNOWN (the old
|
||||
`FUN_0041e4e0` citation was WRONG — that is `DamageZone::TakeDamage`; corrected 2026-08-02,
|
||||
swept with [[decomp-reference]]).
|
||||
- Compiled into BTL4.RES and parsed by **MUNGA's own `DamageZone` stream ctor** into
|
||||
`materialTable`, keyed by `EntitySegment::SkeletonType` (`engine/MUNGA/DAMAGE.cpp:311-336`,
|
||||
`GetMaterialList(skl_type)`) — real engine source [T0]. **This data was loaded and unused in the
|
||||
port all along**; only the consumer was missing.
|
||||
- **`BTL4VideoRenderer::MakeMechRenderables` (`FUN_004cef28`)** walks the zones and builds, per
|
||||
(zone, material), a 232-byte watcher **`FUN_004573e4(material, &zone->damageLevel, 0.1f)`**
|
||||
(a 60-byte per-zone parent first, factor 0.04). Error string: *"BTL4VideoRenderer: Material name
|
||||
<X> could not be found"* @0x51d6f8.
|
||||
- The watcher snapshots the material's **16 authored colour floats** — Ambient(3), Emissive(3),
|
||||
Diffuse(3), Specular+shininess(4), Opacity(3) — precomputes a DAMAGED set = **pristine x 0.1**
|
||||
(`0x3dcccccd`), and **`FUN_00457784`** re-pushes `lerp(pristine, damaged, damageLevel)` whenever
|
||||
the level changes (constant `@0x4579a4` = **1.0**). So a zone at full damage renders at **10%**
|
||||
of authored brightness, continuously interpolated.
|
||||
- **Faithful quirk:** Opacity is read and lerped but **never written back** — only 13 of the 16
|
||||
values are pushed (`dpl_GetMaterialOpacity` has no matching Set call in the watcher).
|
||||
|
||||
**NOT the mechanism** [T2]: the engine also has `SV_SPECIAL DAMAGE <material>` /
|
||||
**`dpl_Damagize` `FUN_004902b0`** (punch's sibling — same VPX cmd 0x20, tokens
|
||||
`f_t_dam(0x92)/sca(0x05)/undamage(0x04)`, dispatch @0x4599ae; error *"SPECIAL GEOGROUP DAMAGE
|
||||
couldn't find material"*). A scan of **all 879 shipped BGFs finds ZERO `DAMAGE` tokens** — the path
|
||||
exists in the engine but no shipped asset uses it. Don't chase it.
|
||||
|
||||
**The port** (`game/reconstructed/btl4vid.cpp` `BindArmourDamage`/`TickArmourDamage`,
|
||||
`engine/MUNGA_L4/L4D3D.cpp`): batches now carry their AUTHORED (pre-substitution) material name
|
||||
(`BgfDrawBatch::matName` -> `L4DRAWOP::dzMatName`); binding resolves the .DZM lists onto real draw
|
||||
ops once per tree build, and the per-frame tick (driven from `Mech::PerformAndWatch`, EVERY mech)
|
||||
copies `zone->damageLevel` onto them. Two hard-won details:
|
||||
- **Key the material list on `viewSkeleton`, not the build `skeletonType`.** Each skeleton variant
|
||||
is painted from its own skin library (Black Hawk N = `blhskin:`, X = `blxskin:`) and
|
||||
`ApplyViewSkeleton` reloads every segment mesh; keying on the build skeleton matched a list
|
||||
belonging to geometry that is not on screen -> **zero bindings, no darkening**.
|
||||
- **Scaling the `D3DMATERIAL9` is inert** — see gotcha **§23** (`DIFFUSEMATERIALSOURCE` defaults to
|
||||
vertex colour). The darkening is applied as a `TEXTUREFACTOR` modulate on texture stage 1.
|
||||
Rebinding runs after every geometry swap (`RemakeEntityRenderables`, `ApplyViewSkeleton`), which
|
||||
also keeps `segPick` (the #73 aimed pick) off discarded meshes.
|
||||
Bench: `scratchpad/night7/armor_darken.sh` (self/enemy/ext legs) + `armor_ab.sh` (forced-level
|
||||
pixel A/B). Gates `BT_ARMOR_LOG` / `BT_ARMOR_FORCE=<0..1>`. Verified: 57 bindings on the enemy
|
||||
mech, brightness tracking `1 - 0.9*level` exactly, hull 0.35x vs terrain control 0.98x at level 1.
|
||||
The `damcolor_`/`gen*gry_` arm+gun names go unmatched until the DESTROYED limb variant loads —
|
||||
correct: those materials live on the `*D*.BGF` damaged meshes.
|
||||
|
||||
## Key Relationships
|
||||
- Geometry/LOD: [[bgf-format]]. Base: [[wintesla-port]] (L4D3D). Shadow/visual-conform: [[locomotion]].
|
||||
- Renders on: [[pod-hardware]] (main 3D view).
|
||||
- Prop destruction feeds: [[combat-damage]] (crunch dispatch) · uses the psfx layer.
|
||||
|
||||
Reference in New Issue
Block a user