Files
BT412/engine/MUNGA_L4/bgfload.h
T
arcattackandClaude Opus 4.8 0bfb3d4ab3 Warp: restore the translocation-vortex look + fix texture-scroll precision collapse (task #52)
The death/respawn "blue whirlwind" (tsphere) now matches the original cabinet
photo (capture.png): a smooth spinning lavender vortex with a bright core.

Root cause of the long-standing "radial spokes" artifact was NOT the warp code
but a general engine bug: L4D3D::SetTextureScrolling computed its texture-matrix
offset as scrollDelta * absolute_time, which grows unbounded and collapses UV
float precision -> a smooth scrolled cloud shatters into grainy radial steps.
Wrapped with fmodf(..., 1.0f) (identical under REPEAT tiling, full precision).
This also cleans the scrolling bexp beam grit and any other SCROLL material.

Visual reconstruction (verified against the real 45-vtx TSPHERE.BGF bicone,
offline-rasterized then ported):
  - view ON-AXIS (eye centred on the throat) + spin in place -> concentric rings
    (decomp FUN_00453dc4 does spin-about-local-Z + submit; the port had stubbed it)
  - bintA cloud through a WIDE lavender ramp at full contrast (drawn as SKY);
    no geometry "bands", no log-polar twist, no off-axis tornado (all discarded)
  - tessellate the 12-facet bicone smooth; isotropic + trilinear; ramp baked into
    the texture and drawn SELECTARG1(TEXTURE) to avoid double-tinting

Env knobs (BT_WARP_*) default to the verified values; BT_WARP_SELFTEST/SELFSHOT
are an off-by-default visual-verification harness (backbuffer frame dump).

Docs: new context/translocation-warp.md (geometry/material/visual/lifecycle/env);
reconstruction-gotchas.md gains the accumulated-time precision-collapse bug class;
rendering.md / multiplayer.md / decomp-reference.md / CLAUDE.md cross-linked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 14:53:50 -05:00

102 lines
5.5 KiB
C++

// bgfload.h - C++14 loader for VWE .BGF (DIV-BIZ2) geometry, adapted from the
// proven standalone port loader (port/src/bgf.cpp). Resolves the model under the
// pod content tree (VIDEO\ + VIDEO\GEO\...) and its .BMF material libraries +
// texture image basenames. Produces an L4VERTEX-compatible vertex/index/batch set
// ready to be turned into an ID3DXMesh by d3d_OBJECT.
//
// Self-contained: uses only the Win32 API for directory scanning (no <filesystem>,
// so it builds under the engine's C++14 target).
#pragma once
#include <cstdint>
#include <string>
#include <vector>
// Layout MUST match L4VERTEX (L4D3D.h): XYZ | NORMAL | DIFFUSE | TEX1.
struct BgfVtx {
float x, y, z;
float nx, ny, nz;
uint32_t color; // ARGB 0xAARRGGBB (unused once material diffuse drives lighting)
float u, v;
};
// One draw range sharing a material.
struct BgfDrawBatch {
uint32_t indexStart = 0;
uint32_t indexCount = 0;
uint32_t color = 0xFFB0B0B8u; // ARGB fallback / resolved diffuse
std::string texPath; // resolved texture image file (.vtx/.bsl/.tga), or empty
// BSL BIT-SLICE (BMF TEXTURE tag 0x18, absent = 0): which 4-bit slice of the
// .bsl container this texture samples (0..5 mono; 7 = RGB444, 8 = RGBA4444).
// BSLs pack up to SIX grayscale sub-images per file (mech skins: BLKHWK.BSL
// holds blkhwk1..4); decoding the packed word as one RGBA image overlaid
// 2-3 different sheets as color channels = the rainbow "graffiti" mechs.
int texChannel = 0;
// RAMP (task #20): colorize the grayscale texture by luminance across
// rampLo->rampHi (the authentic IG-board terrain colouring). When set, the
// renderer bakes lerp(rampLo, rampHi, texLum) into the texture and draws with
// a WHITE material so the ramp colour shows directly.
bool hasRamp = false;
float rampLo[3] = {0,0,0};
float rampHi[3] = {1,1,1};
// TRANSLOCATION WARP band count: the IG board realised a ramp as a QUANTISED
// shade colour-map (~16 discrete levels; the material 'dither' field exists to
// soften the band seams), which turns a smooth intensity into clean concentric
// CONTOUR BANDS. Our generic ramp bake is a continuous lerp (right for terrain/
// sky). For tsphere_mtl only, snap the ramp index to this many levels so the
// warp reads as bands, not a blotchy cloud. 0 = continuous (all other ramps).
int warpBands = 0;
// EMISSIVE (tag 0x26): self-lit material colour. Pure-emissive materials
// (diffuse black + emissive set, e.g. btpolar:pintBIceEmit_mtl -- the polar
// ice mounds) render as an UNLIT glow: tex x emissive, no sun/ambient.
bool hasEmissive = false;
float emissive[3] = {0,0,0};
// LOD band (the LOD chunk's 0x2046 header = [near..far) viewing range). The
// authentic renderer selects the LOD whose band contains the camera distance;
// "take the first LOD" broke composite structures (arena buildings/ad signs)
// whose NEAR band is a detail subset and whose massing lives in the FAR bands.
float lodNear = 0.0f;
float lodFar = 1.0e9f; // default: always drawn (single-LOD models)
// PATCH-level SV_SPECIAL "PUNCH" (the 1995 L4VIDEO TestSpecialCallBack ->
// dpl_Punchize): per-texel punch-through -- black texels are HOLES (cutout
// lattice: arena scaffold layers AR01-04, gratings). Drawn SOLID they
// shingle near-coplanar over the massing = depth-shimmer "noisy surfaces".
bool punch = false;
// LAYER DEPTH BIAS (additive objects): the layers contain EXACTLY-COPLANAR
// duplicated surfaces (flush wall patches between detail + massing). The
// IG board resolved those deterministically (exact per-polygon plane
// equations -> identical depth -> submission order wins); D3D9 interpolates
// per-vertex, so two tessellations of one plane differ by rounding per
// pixel = venetian-blind z-fight stripes, at ANY distance. Each finer
// layer gets a slightly more negative normalized-depth bias so coplanar
// overlaps always resolve to the detail layer. 0 = no bias (massing/
// non-additive).
float lodBias = 0.0f;
// SHADOW MATERIAL (e.g. basev:shadow_mtl): baked ground-shadow quads
// (MECHMOVS.BGF = two coplanar quads at y=0.1 under the wreck props).
// Drawn opaque they z-fight each other/the floor; an all-shadow object is
// routed through the mech-shadow pipeline (translucent dark, depth-biased,
// no z-write) instead.
bool shadowMat = false;
};
struct BgfData {
std::vector<BgfVtx> verts;
std::vector<uint32_t> indices; // triangle list (already double-sided)
std::vector<BgfDrawBatch> batches;
bool ok = false;
std::string error;
int tris = 0; // unique (single-sided) triangle count
int matTotal = 0, matResolved = 0;
};
// Resolve `name` (e.g. "blx_cop.bgf", basename only) under the pod content tree and
// load it. Returns false (with .error set) if not found / no geometry.
bool LoadBgfFile(const std::string& name, BgfData& out);
// DAY/NIGHT path priority (task #20): the renderer (DPLReadEnvironment) passes the
// INI's objectpath/materialpath/texmappath search dirs, MOST-SPECIFIC-FIRST, so the
// loader picks the correct time-of-day variant of a colliding stem instead of
// first-match-wins. cacheKey is "bgf" (objects), "bmf" (material libs), or "img"
// (textures). Invalidates that index so it rebuilds with the new priority.
void SetVideoPathPriority(const std::string& cacheKey, const std::vector<std::string>& dirs);