Predator/IR vision: reverse-engineered from the original firmware and
confirmed by the build team -- it is the Division board's TEXTURE-VALUE RAMP
mode (a "check your texture maps" diagnostic the devs hijacked), NOT a
grayscale squash or a false-colour palette. Located in VREND.MNG (effect
handler @0xe6c0, wire action 0x1b, type -1 ON / -2 OFF); ramp colours from
VR_DRAW.C. Renderer reworked to match: vrview_gl now does the 4-ramp
lerp(color0,color1,luminance(texel)) in the mesh pass (grayscale+defog
removed). Live-rendered on a new night-clear arena egg; crew A/B verdict
pending.
Firmware-decomp toolchain (emulator/firmware-decomp/), all built from the
project's own artifacts and validated:
- coff860.py i860 COFF reader (symbols/sections), names match AS860 source
- derive860.py derives the i860 opcode map from matched .S<->.O pairs
- dis860.py i860 disassembler (98% on clean ground truth; proven on
VREND.MNG -- velocirender_statistics decodes correctly)
- sigmatch860.py reloc-invariant signature matcher onto the stripped image
- i860-encoding.md / FIRMWARE-SYMBOLS.txt / README.md
PVISION-IMPLEMENTATION-GUIDE.md: self-contained hand-off for the BT411 team.
HARDWARE-ARCHITECTURE.md + hardware-photos/ (15 board shots): the Division
VelociRender card is a 2-board stack driving a 3-processor pipeline --
INMOS IMS T425-J25S (comms/control, runs vrendmon.btl) + Intel i860 XP-50 (FP
geometry, runs vrender.mng) + Division PXPL IGC 5.2 ASIC with ~48x PXPL EMC
5.1 (UNC Pixel-Planes-5 SIMD array; "EMC" = the firmware's configEMCs) +
Analog Devices ADV7150 RAMDAC + NTSC. Plus the VWE Video Distribution Board
(P/N 1404: AMD MACH130 + 3x Brooktree Bt477) for the 3-VGA-head cockpit split.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
194 lines
8.2 KiB
Markdown
194 lines
8.2 KiB
Markdown
# 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.
|