Entry 160
The shadow volumes: ccShadowModel, ccShadowPacket and the VU1 programs
Documented in The shadow volumes.
Filescrates/piney-data/src/shadow.rscrates/piney-desktop/src/shadow.rscrates/piney-gs/src/shadow.rscrates/piney-desktop/src/soft.rscrates/piney-world/src/draw.rscrates/piney-stream/src/draw.rstools/test_shadow_rs.py
Until now no shadow was drawn anywhere. GAPS listed TOBJ's and the streams'
shadows, but the gap was wider: every character body carries a shadow
model on each of its objects (a PC has 15 to 31 of them), and so do the
weapons and the dogs. Kite, the party, the NPCs and the enemies all stood
on the ground without one. docs/engine/shadow.md has the whole of it.
The game draws stencil shadow volumes. A shadow model is a closed mesh.
DecodeShadowModel works out each triangle's direction, merging those
within 0.996 of each other, and the edges between faces, matched by vertex
index. Each frame ccShadowModel::Draw sends VU1 the light in the model's
axes, the stretch (the light times the shadow's length), and a matrix to a
small buffer's pixels. VU1 then:
- lights each direction (
mc_DrawShadow1); - draws the faces, moving the unlit ones by the stretch (
mc_DrawShadow2); - draws a quad for each edge between a lit and an unlit face
(
mc_DrawShadow3).
The screen winding picks the add or subtract blend. At the frame's end,
each ccShadowPacket builds a short GS program, run backwards because a
layer draws what it was sent last first:
- The frame's Z is copied, point sampled, into the buffer's Z.
- The buffer is filled with grey.
- The volumes are counted into red against that Z (GREATER, no write).
- 0x80 is subtracted, and each group's alpha is written where the count is left above 0.
- The buffer is laid over the frame, black, bilinear, at the darkness
(0x30 of 0x80 for the characters). TOBJ's 128 x 128 packet is laid
twice, 0.875 pixels either way, by
zureTbl.
WORLD_MAN::GO makes the two packets in every area, towns included (a
field-game.md Unknown had it that towns made none). They sit at priority
2, so the Z the volumes meet is only the ground and the objects. The
light is the area's distant light, from SetLightDirection.
ccChar::Draw sets the alpha from the transparency and the length to
three times the height, then puts the alpha back to 128. In a stream, each
shadow layer with a Shadow chunk has its own packet, and F_Shadow
records turn its light and alpha frame by frame. 17 of the 266 stream
files have one, 10 of them story cutscenes.
The port follows those pieces:
piney_data::shadow::ShadowMeshis the decode.piney_desktop::shadow::volumeis the draw and the VU1 programs, done as polygons in buffer pixels with GS Z. It cuts at the near plane the waymc_DrawShadowClipdoes: the part behind the plane is put on it, the near cap.ShadowPacketandShadowsare the packets and the draw environment. They live inLayers, andLayers::flattenputs each packet'sCmd::Shadowpass on its layer.piney-gsruns the pass as four kinds of render pass: the Z copy, the count (R16Float, additive), the resolve, and the composite. The CPU GS does the same.- In piney-world,
go_shadowssets the packets up andchar_shadowwraps eachccChar::Draw.Bodydraws and the enemies' draws cast their nodes' shadow models, and TOBJ casts into the second packet. - piney-stream reads the Shadow chunks and shadow layers, decodes
F_Shadow, and casts each draw-list node's shadow model.
Checks:
tools/test_shadow_rs.pyruns the game'sDecodeShadowModelon all 1,957 distinct shadow mmats: every direction, triangle and edge matches.- The same test runs
ccShadowModel::Drawin eemu and the packet through a VIF1 model andtools/vu.py's VU1. Two things had to be added to the harness: FSSET/FSAND, and reading each XGKICK's GIF when it is sent, since the programs reuse their buffers. Both sides are counted into the buffer against two depth planes; 300 random draws match pixel for pixel. - Two harness mistakes cost time. The test models first came out sub-pixel because they did not use their real vertex scale (512). Then pixel corners on fan edges were counted twice: the game's clipped vertices sit exactly on the corners.
- A piney-gs test renders a pass over a floor.
- The session test
kite_casts_his_shadowchecks the pass in area 14's field and in Mac Anu, and that the CPU GS darkens the ground. stream_shot --no-shadowsshows stream 7's long shadow of Kite across the floor.
The console's choice between the direct and the clipped path for a polygon. It turns on the status flag's pipelined sticky sign. The two differ only where Z passes 0x7fffffff. The shadow bounding box check reads a register (vf21) the program never sets. An open edge's second face reads VU1 memory below the directions. Modes 2 and 3 are never made in Infection and are not ported. EVENTAREA02 and 07 reset the light after GO.