// 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 #include #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