# Implementing IR / "Predator" Vision in a BattleTech (VelociRender) renderer **Audience:** the BT411 renderer + whoever's Claude is implementing it. **Status:** reverse-engineered from the original VelociRender i860 firmware (`VREND.MNG`) and the DPL3/VRENDER board source, and confirmed by original-team recollection. This document is self-contained — the key code is quoted inline so you don't need our tree. --- ## TL;DR Predator/IR vision is **not** a grayscale squash, **not** a palette swap, and **not** a scene-luminance heat map. It is the Division board's **texture-value ramp mode** — a diagnostic the board provided for *checking texture maps* that the game developers hijacked as the in-game IR effect. Each **textured** surface is drawn as: ``` out_rgb = lerp(color0, color1, luminance(texel)) ``` where `luminance(texel)` is the brightness of the sampled **texel** (the texture map's value, *not* the lit/shaded pixel), and `(color0, color1)` are the two RGB endpoints of the **ramp assigned to that surface's material**. There are four ramps (gray / red / green / blue → white). Because different materials use different ramps, the scene comes out **multicolor**. --- ## 1. How the game turns it on (host side) The game's renderer object (`DPLRenderer`, shared by BattleTech and Red Planet) has a toggle. From the original source (`MUNGA_L4/L4VIDEO.CPP`, `DPLRenderer::DPLTogglePVision`): ```c static Logical pvision_on = 0; dpl_EXPLOSION_EFFECT_INFO sfx_info; sfx_info.x = sfx_info.y = sfx_info.z = 0; if ((pvision_on ^= 1) != 0) sfx_info.type = -1; // pvision ON else sfx_info.type = -2; // pvision OFF dpl_Effect(dpl_effect_type_explosion, NULL, &sfx_info); ``` Key point: **it sends no colour.** It fires a bogus "explosion" effect at the origin whose `type` field is a magic flag: `-1` = ON, `-2` = OFF. The `dpl_EXPLOSION_EFFECT_INFO` struct is `{float x,y,z; int32 type; dpl_TEXTURE*}` — there is no colour to carry. So the palette is **entirely board-side**; the host only flips a switch. On the VelociRender wire this arrives as the effect action (in our decode, action `0x1b`), payload first word = `type`. If you drive a board/emulator, the handler is: `type >= 0` → normal explosion; `type == -1/-2` → set/clear the pvision mode flag. --- ## 2. What the board does (firmware) The board firmware selects a **texture display mode** per frame (`dvpx_eoftexmode`, from the end-of-frame code `VRENDER/PXPL5SUP/EOF.C`): | mode | meaning | |------|---------| | `0` | **texture RAMP** — texel value → interpolated colour ramp (this is IR vision) | | `0x4` | 8-bit monochrome | | `0x6` | 8-bit 3-3-2 full colour | | `0x7` | 12-bit 4-4-4 full colour | Normal rendering uses the full-colour modes. `pvision ON` flips the whole scene into **mode 0 (ramp)**. The end-of-frame code literally linear-interpolates each channel across the texel value: ``` /* now access the texture colour map table (8 32-bit entries) */ /* now linterp from r0 to r1 */ // Red across texel value /* now linterp from g0 to g1 */ // Green /* now linterp from b0 to b1 */ // Blue ``` The "texel value" is a 6-bit luminance of the 24-bit texel (`VRENDER/DNC.C::luminize` — "returns a 6-bit luminance value from a 24-bit texel"). Think of it as `value = luminance(texel)`, `value ∈ [0,1]`. --- ## 3. The ramps A ramp is two RGB endpoints (`dpl_RAMP { float32 color0[3]; float32 color1[3]; }`). `color0` = the colour for **dark** texels (value 0), `color1` = the colour for **bright** texels (value 1). Four ramps are installed once at render init (`VRENDER/VR_DRAW.C`, `setRampEntry(ramp, index, r0,g0,b0, r1,g1,b1)`): | ramp | color0 (dark) | color1 (bright) | look | |------|---------------|-----------------|------| | 0 | `0.0, 0.0, 0.0` | `1.0, 1.0, 1.0` | black → white (grayscale) | | 1 | `0.3, 0.0, 0.0` | `1.0, 1.0, 0.9` | dark red → warm white | | 2 | `0.0, 0.5, 0.0` | `1.0, 1.0, 1.0` | green → white | | 3 | `0.0, 0.0, 0.4` | `0.9, 0.9, 1.0` | blue → cool white | Each **material** references one of these by an integer `ramp_entry` (0–3), so a mech built from several materials shows several colour families at once — that's the multicolor IR look, and it's why the tool was useful for *checking* which material/ramp a surface used. > **Provenance caveat:** those RGBs are the source defaults. The shipped 1996 > `VREND.MNG` build tweaked the exact floats (the constant `0.9` is absent from > the binary — the bright endpoints were likely rounded toward pure white). The > *structure* (four gradients from a dark saturated colour to ~white, indexed by > texel value) is unchanged. Use the table above; nudge the two `0.9`s to `1.0` > if you want to match the shipped build more closely. --- ## 4. How to implement it in your renderer The recolour must happen **at texturing time in the scene pass**, on the sampled texel — *not* in a post/present pass on the final pixel, because you need the raw texel value, not the lit result. Per textured fragment, when pvision is active: ```glsl // u_pv : 1 when IR/predator vision is on // u_ramp0/1 : the two RGB endpoints of THIS surface's ramp (from ramp_entry) if (u_pv == 1) { float value = clamp(dot(texel.rgb, vec3(0.299, 0.587, 0.114)), 0.0, 1.0); frag_rgb = mix(u_ramp0, u_ramp1, value); } // else: normal shading (frag_rgb = texel * lighting, etc.) ``` Wiring notes: 1. **Ramp selection = the material's `ramp_entry` (0–3).** Set `u_ramp0`/`u_ramp1` from `PV_RAMP[ramp_entry]` per draw call. If your pipeline doesn't yet track `ramp_entry`, pick a **stable** ramp per surface (e.g. `hash(material_id) & 3`) so different objects get different families — this reproduces the multicolor look until you can plumb the real per-material index. 2. **Apply to all textured world geometry** — mechs, terrain, buildings, sky. 3. **Untextured surfaces:** use the shaded base colour's luminance as the value (there's no texel), or leave them as-is; they're a minor case. 4. **HUD / MFDs are separate** and must *not* be ramped — they're a different display path (the cockpit's mono/color panels), not the out-the-window scene. 5. **Fog is orthogonal.** The ramp mode is a texture-colour stage; it does not touch the fog/haze stage. Do **not** disable fog for pvision unless you get specific evidence otherwise. (We initially guessed "see through fog" and it was wrong — the mode is purely about colour.) 6. **DAC gamma** (the board applies ~`pow(c, 1/1.25)` at scan-out) is unrelated to pvision; keep whatever gamma you already do. ### Host trigger If you emulate/interpret the wire, treat the effect action carrying `type == -1` as "pvision on" and `type == -2` as "off"; `type >= 0` is a real explosion. If you drive rendering directly from game state, hook `DPLTogglePVision`. --- ## 5. What NOT to do (things we tried that are wrong) - ❌ Grayscale squash of the frame. - ❌ False-colour heat/green/amber palette applied to **scene** luminance. - ❌ Recolouring in a present/post pass using final-pixel luminance. - ❌ Disabling fog as part of the effect. - ❌ Expecting the game to send colours — it sends only the on/off flag. The correct model is the four-ramp, per-material, **texel-value** mapping above. --- ## 6. How to verify against the originals Everything here is checkable in the VelociRender materials: - Host trigger: `MUNGA_L4/L4VIDEO.CPP` `DPLTogglePVision` (BT and RP both link the same `DPLRenderer`). - Ramp mechanism & modes: `DPL3/VRENDER/PXPL5SUP/EOF.C` ("lerp texture ramp", `dvpx_eoftexmode`), `DPL3/VRENDER/DNC.C::luminize`, `DPL3/VRENDER/DPLTYPES.H` (`dpl_RAMP`, `dpl_draw_luminance 0x04`). - Ramp colours: `DPL3/VRENDER/VR_DRAW.C` (the four `setRampEntry` calls at render init). - Firmware (if you want ground truth from the shipped board image): in `RPLIVE/VREND.MNG` / `BTLIVE/VREND.MNG` the effect handler is reached via the wire-action jump table; it reads `msg[0]` as `type`, tests sign, and branches `type==-1`/`type==-2`. Code loads at ~`0xf0400000`, globals low (~`0x10000`). (Reversing that binary needs an i860 disassembler; the source above is easier.) If your Claude has the shared dump, those paths are under `sda4/DPL3/VRENDER/`. The Tesla preservation project (this repo) is the authoritative source for these findings.