fs2port/port/include/sceneryProjection.h
2026-05-13 21:32:05 -05:00

172 lines
7.3 KiB
C

// 3D vertex pipeline that mirrors FS2's chunk5 polygon math.
//
// The original 6502 implementation lives in chunk5.s:
//
// L7EBC -- per-vertex coord transform (auto-scale +
// rotation matrix)
// ClassifyVertex1/2 -- 6-plane outcode generation
// ProjectV1ToScreen, -- perspective divide -> screen pixel
// ProjectV2ToScreen
// PerspectiveDivide -- shift-and-subtract 16/16 divide
// EmitPrimaryVertex -- append a vertex to the 60-slot pool
//
// This port keeps the same precision (signed 16-bit world deltas, 8-bit
// rotation matrix, 16/16 perspective divide) and the same outcode bit
// assignments, so the algorithmic results match the 6502 game value-
// for-value modulo the LSB of MultiplyXY's 7x7 truncation.
//
// Outcode bit layout (from ClassifyVertex2 at chunk5 line 2673):
// bit 7 ($80) -- z negative (behind camera)
// bit 6 ($40) -- x + z < 0 (right of frustum)
// bit 5 ($20) -- z - x < 0 (left of frustum)
// bit 4 ($10) -- y + z < 0 (below frustum)
// bit 3 ($08) -- z - y < 0 (above frustum)
#ifndef SCENERY_PROJECTION_H
#define SCENERY_PROJECTION_H
#include <stdint.h>
#include <stdbool.h>
#define SCENERY_OUTCODE_BEHIND 0x80
#define SCENERY_OUTCODE_RIGHT 0x40
#define SCENERY_OUTCODE_LEFT 0x20
#define SCENERY_OUTCODE_BOTTOM 0x10
#define SCENERY_OUTCODE_TOP 0x08
#define SCENERY_VERTEX_POOL_CAP 60
// One slot in the primary vertex pool. Six bytes are stored per vertex
// in chunk5 (six parallel arrays at L0AB8/AF8/B38/B78/BB8/BF8); we
// pack them into a single struct for cache behaviour. Components are
// camera-space x/y/z in 16-bit signed, plus a Cohen-Sutherland outcode.
typedef struct SceneryVertexT {
int16_t x; // L0AB8/L0AF8 = lo/hi of camera-space x
int16_t y; // L0B38/L0B78 = lo/hi of camera-space y
int16_t z; // L0BB8/L0BF8 = lo/hi of camera-space z
uint8_t outcode; // 6-plane mask (see SCENERY_OUTCODE_*)
} SceneryVertexT;
// Per-frame projection state. Mirrors the chunk5 zero-page variables
// the polygon pipeline reads:
// $66/$67 = camera world X (eyepoint)
// $6A/$6B = camera world Z
// $79/$7B/$7D = first row of the 2x3 rotation matrix (XZ -> camX)
// $85/$87/$89 = second row of the 2x3 rotation matrix (XZ -> camY/Z)
// $4A/$4D/$50 = section-base contribution to camX/camY/camZ
// (set by the coord-frame opcode $0D, accounts for the
// vertical/altitude term)
// $2F = current zoom-detail counter (auto-scale shift count)
typedef struct SceneryProjStateT {
int16_t camX; // $66/$67
int16_t camZ; // $6A/$6B
int8_t matRow1[3]; // $79, $7B, $7D
int8_t matRow2[3]; // $85, $87, $89
int16_t baseX; // $4A
int16_t baseY; // $4D
int16_t baseZ; // $50
uint8_t zoomShift; // $2F (init $40, decrements as we shift)
} SceneryProjStateT;
// Two "current" vertex slots, mirroring chunk5's $CB..$D2 (vertex 1)
// and $D4..$DB (vertex 2). The vertex-emit family writes into these
// before deciding whether to project, classify, or push into the pool.
typedef struct SceneryCurrentT {
SceneryVertexT v1; // $CB..$D0 (+ $CA outcode, $D1/$D2 screen)
SceneryVertexT v2; // $D4..$D9 (+ $D3 outcode, $DA/$DB screen)
int16_t v1ScreenX; // $D1
int16_t v1ScreenY; // $D2
int16_t v2ScreenX; // $DA
int16_t v2ScreenY; // $DB
// Running coord accumulators that L7EBC writes into ($18/$1A,
// $1B/$1D, $1E/$20). Three signed 16-bit values plus a sign-
// extension byte each; we collapse to int32 for the
// intermediate sum and snap back to int16 on store.
int16_t accX;
int16_t accY;
int16_t accZ;
// Polygon outcode AND-accumulator at $D3 (zero -> all vertices
// share an offscreen plane, polygon culled).
uint8_t polygonOutcode;
// Vertex-pool head ($B5).
uint8_t poolCount;
} SceneryCurrentT;
typedef struct SceneryPipelineT {
SceneryProjStateT proj;
SceneryCurrentT cur;
SceneryVertexT pool[SCENERY_VERTEX_POOL_CAP];
} SceneryPipelineT;
// Reset the vertex pool and outcode accumulator. Call at the top of
// each scenery frame and whenever opcode $2F (SceneryOpResetState)
// fires.
void sceneryPipelineReset(SceneryPipelineT *pipe);
// Set the camera world position and the 2x3 rotation matrix. Called
// by the world driver once per frame, before sceneryRun walks the
// stream. Matrix entries are signed 8-bit (see chunk5 $79..$89).
void sceneryPipelineSetCamera(SceneryPipelineT *pipe, int16_t worldX, int16_t worldZ);
void sceneryPipelineSetMatrix(SceneryPipelineT *pipe, const int8_t row1[3], const int8_t row2[3]);
void sceneryPipelineSetBase(SceneryPipelineT *pipe, int16_t bx, int16_t by, int16_t bz);
// L7EBC: read 4 stream bytes (XZ pair, signed 16-bit each), subtract
// camera XZ, auto-scale, multiply by the 2x3 rotation matrix, add to
// section base, store into target slot ($CB..$D0 or $D4..$D9).
//
// Returns the number of stream bytes consumed (always 4), so the
// caller can advance.
int sceneryProjectStreamVertex(SceneryPipelineT *pipe, const uint8_t *streamPlus1, SceneryVertexT *outSlot);
// Same math as sceneryProjectStreamVertex but takes the world XZ pair
// directly (caller already has decoded values). Used by the world
// driver to push hardcoded vertex data through the same pipeline a
// real scenery byte stream would.
void sceneryProjectXZ(SceneryPipelineT *pipe, int16_t worldX, int16_t worldZ, SceneryVertexT *outSlot);
// ClassifyVertex2-style outcode for a camera-space vertex. Pure
// function; reads only the vertex itself.
uint8_t sceneryClassifyVertex(const SceneryVertexT *v);
// ProjectV2ToScreen: divide camera x/y by camera z (with chunk5's
// fixed-point shift-and-subtract divide) and bias to screen pixels.
// Returns false if the vertex is behind the camera (cz <= 0).
bool sceneryProjectVertexToScreen(const SceneryVertexT *v, int16_t *outX, int16_t *outY);
// EmitPrimaryVertex: append `slot` to the pool, AND its outcode into
// the polygon accumulator. No-op if the pool is full (chunk5 caps at
// 60 too -- $cpy #$3C / bcs).
void sceneryEmitPrimary(SceneryPipelineT *pipe, const SceneryVertexT *slot);
// 4-pass Sutherland-Hodgman 3D frustum clipper. Mirrors chunk5's
// PolygonScanFillSetup + PolygonClipTopPass + PolygonClipRightPass +
// PolygonClipBottomPass (src/chunk5.s:2884+). Operates on camera-space
// XYZ vertices, intersects each clip plane at the frustum half-spaces
// (Left: Z-X=0, Top: Z-Y=0, Right: Z+X=0, Bottom: Z+Y=0), introducing
// new vertices at the plane crossings. Ping-pongs between two arrays.
//
// On entry: `in`/`out` are arrays of capacity `cap`, `inCount` is the
// initial vertex count.
//
// Returns the final clipped vertex count, with the output in whichever
// of the two arrays the last pass wrote to (signalled via the boolean
// returned in `*outIsIn`: true means the final result is in `in`,
// false means it's in `out`).
//
// Returns 0 if the polygon was fully clipped away.
int sceneryClipPolygon3D(SceneryVertexT *in, SceneryVertexT *out, int inCount, int cap, bool *outIsIn);
#endif