// 3D world fixture: a runway, a tower, a control building, a strip // of water, and a row of mountains. World units are roughly metres. // // `worldRender` drives the chunk5-faithful scenery pipeline // (sceneryProjection.c), so the demo data exercises the same math // real FS2 scenery would. Each line projects via the L7EBC port and // the L7C39 PerspectiveDivide port; output goes to the renderer's // line primitive. #include #include "camera.h" #include "math6502.h" #include "projection.h" #include "sceneryProjection.h" #include "types.h" #include "world.h" // Scratch pipeline shared across worldRender invocations. Each frame // the world driver reseats the camera + matrix + per-altitude base // before projecting vertices. static SceneryPipelineT worldPipe; static int worldPipeInited; static int radarLineToScreen(const CameraT *cam, int16_t wx, int16_t wz, int16_t metresPerPixel_q88, int16_t cx, int16_t cy, int16_t *outX, int16_t *outY); // Each row defines one coloured line segment in world space. // Coordinates: +X east, +Y up, +Z north (forward). static const WorldLineT worldLines[] = { // Runway outline at the origin, oriented along +Z. { -10, 0, 0, 10, 0, 0, COLOR_RUNWAY }, { 10, 0, 0, 10, 0, 200, COLOR_RUNWAY }, { 10, 0, 200, -10, 0, 200, COLOR_RUNWAY }, { -10, 0, 0, -10, 0, 200, COLOR_RUNWAY }, // Centreline dashes { 0, 0, 20, 0, 0, 30, COLOR_WHITE }, { 0, 0, 50, 0, 0, 60, COLOR_WHITE }, { 0, 0, 80, 0, 0, 90, COLOR_WHITE }, { 0, 0, 110, 0, 0, 120, COLOR_WHITE }, { 0, 0, 140, 0, 0, 150, COLOR_WHITE }, { 0, 0, 170, 0, 0, 180, COLOR_WHITE }, // Control tower (a tall thin box) east of the runway. { 35, 0, 30, 35, 25, 30, COLOR_BUILDING }, { 45, 0, 30, 45, 25, 30, COLOR_BUILDING }, { 35, 0, 40, 35, 25, 40, COLOR_BUILDING }, { 45, 0, 40, 45, 25, 40, COLOR_BUILDING }, { 35, 25, 30, 45, 25, 30, COLOR_BUILDING }, { 35, 25, 40, 45, 25, 40, COLOR_BUILDING }, { 35, 25, 30, 35, 25, 40, COLOR_BUILDING }, { 45, 25, 30, 45, 25, 40, COLOR_BUILDING }, // A flat hangar west of the runway. { -50, 0, 25, -50, 12, 25, COLOR_BUILDING }, { -30, 0, 25, -30, 12, 25, COLOR_BUILDING }, { -50, 0, 55, -50, 12, 55, COLOR_BUILDING }, { -30, 0, 55, -30, 12, 55, COLOR_BUILDING }, { -50, 12, 25, -30, 12, 25, COLOR_BUILDING }, { -50, 12, 55, -30, 12, 55, COLOR_BUILDING }, { -50, 12, 25, -50, 12, 55, COLOR_BUILDING }, { -30, 12, 25, -30, 12, 55, COLOR_BUILDING }, // Water strip beyond the runway. { -150, 0, 250, 150, 0, 250, COLOR_WATER }, { -150, 0, 270, 150, 0, 270, COLOR_WATER }, { -150, 0, 290, 150, 0, 290, COLOR_WATER }, // Mountain ridge much further away. { -300, 0, 600, -200, 60, 580, COLOR_MOUNTAIN }, { -200, 60, 580, -100, 30, 600, COLOR_MOUNTAIN }, { -100, 30, 600, 0, 80, 620, COLOR_MOUNTAIN }, { 0, 80, 620, 100, 25, 600, COLOR_MOUNTAIN }, { 100, 25, 600, 220, 70, 580, COLOR_MOUNTAIN }, { 220, 70, 580, 300, 0, 600, COLOR_MOUNTAIN }, // Ground reference grid (so motion is visible). // North-south lines every 40 metres. { -200, 0, -40, -200, 0, 600, COLOR_DIRT }, { -120, 0, -40, -120, 0, 600, COLOR_DIRT }, { -40, 0, -40, -40, 0, 0, COLOR_DIRT }, { -40, 0, 200, -40, 0, 600, COLOR_DIRT }, { 40, 0, -40, 40, 0, 0, COLOR_DIRT }, { 40, 0, 200, 40, 0, 600, COLOR_DIRT }, { 120, 0, -40, 120, 0, 600, COLOR_DIRT }, { 200, 0, -40, 200, 0, 600, COLOR_DIRT }, // East-west lines every 80 metres. { -200, 0, 0, 200, 0, 0, COLOR_DIRT }, { -200, 0, 80, 200, 0, 80, COLOR_DIRT }, { -200, 0, 160, 200, 0, 160, COLOR_DIRT }, { -200, 0, 320, 200, 0, 320, COLOR_DIRT }, { -200, 0, 400, 200, 0, 400, COLOR_DIRT }, { -200, 0, 480, 200, 0, 480, COLOR_DIRT }, { -200, 0, 560, 200, 0, 560, COLOR_DIRT }, }; #define WORLD_LINE_COUNT (sizeof(worldLines) / sizeof(worldLines[0])) // Project a world point (X, Z) onto the radar viewport, accounting // for camera yaw so North-up rotates with the player heading. // `wx`/`wz` are world-unit metres (int16); `metresPerPixel_q88` is // the inverse zoom in Q8.8 metres / pixel. static int radarLineToScreen(const CameraT *cam, int16_t wx, int16_t wz, int16_t metresPerPixel_q88, int16_t cx, int16_t cy, int16_t *outX, int16_t *outY) { // Translate to camera-relative metres (drop the Q16.16 fraction). int32_t dx = wx - (cam->worldX >> CAM_POS_FRACT_BITS); int32_t dz = wz - (cam->worldZ >> CAM_POS_FRACT_BITS); // Rotate by -yaw. sin/cos are Q1.15. int32_t yawSin = math6502Sin(cam->yaw); int32_t yawCos = math6502Cos(cam->yaw); int32_t rx_metres = (dx * yawCos - dz * yawSin) >> 15; int32_t rz_metres = (dx * yawSin + dz * yawCos) >> 15; // Pixels = metres * 256 / metresPerPixel_q88. if (metresPerPixel_q88 <= 0) { return 0; } int32_t sx = (int32_t)cx + (rx_metres << 8) / metresPerPixel_q88; int32_t sy = (int32_t)cy - (rz_metres << 8) / metresPerPixel_q88; if (sx < -1024 || sx > 1024 || sy < -1024 || sy > 1024) { return 0; } *outX = (int16_t)sx; *outY = (int16_t)sy; return 1; } void worldRenderRadar(const CameraT *cam, RenderStateT *renderer, int16_t metresPerPixel_q88) { const int16_t cx = NATIVE_WIDTH / 2; const int16_t cy = VIEWPORT_BOTTOM / 2; for (size_t i = 0; i < WORLD_LINE_COUNT; i++) { const WorldLineT *L = &worldLines[i]; int16_t x1, y1, x2, y2; if (!radarLineToScreen(cam, L->x1, L->z1, metresPerPixel_q88, cx, cy, &x1, &y1)) { continue; } if (!radarLineToScreen(cam, L->x2, L->z2, metresPerPixel_q88, cx, cy, &x2, &y2)) { continue; } rendererSetDrawColor(renderer, L->color); rendererDrawLine(renderer, x1, y1, x2, y2); } // Player aircraft as a "+" at viewport centre. rendererSetDrawColor(renderer, COLOR_WHITE); rendererDrawLine(renderer, (int16_t)(cx - 4), cy, (int16_t)(cx + 4), cy); rendererDrawLine(renderer, cx, (int16_t)(cy - 6), cx, (int16_t)(cy + 2)); } void worldRender(const CameraT *cam, RenderStateT *renderer) { if (!worldPipeInited) { sceneryPipelineReset(&worldPipe); worldPipeInited = 1; } // Push the camera state into the pipeline once per frame. // Camera world coords are Q16.16; chunk5's $66/$67/$6A/$6B are // int16 world units. Drop the fraction and clamp so flying // past 32 km doesn't wrap negative. int32_t wxUnits = cam->worldX >> CAM_POS_FRACT_BITS; int32_t wzUnits = cam->worldZ >> CAM_POS_FRACT_BITS; if (wxUnits > 32767) wxUnits = 32767; if (wxUnits < -32768) wxUnits = -32768; if (wzUnits > 32767) wzUnits = 32767; if (wzUnits < -32768) wzUnits = -32768; sceneryPipelineSetCamera(&worldPipe, (int16_t)wxUnits, (int16_t)wzUnits); int8_t rowX[3]; int8_t rowZ[3]; cameraGet2x3Matrix(cam, rowX, rowZ); sceneryPipelineSetMatrix(&worldPipe, rowX, rowZ); for (size_t i = 0; i < WORLD_LINE_COUNT; i++) { const WorldLineT *L = &worldLines[i]; // Per-line altitude is supplied via the section base // ($4A/$4D/$50 in chunk5). The base contributes // directly to camY in L7EBC's running accumulator, so // an altitude of `y` becomes a baseY of // `(int16_t)(y - cam->worldY)` scaled to match the // camera-space rotation we already applied to the XZ // plane. // // chunk5 expresses base as the *post-rotation* camera- // space contribution -- the section center is fed into // $18/$1B/$1E pre-rotation only when it's already in // camera coords. Since worldLines[] uses world Y // directly, we project the (0, y - camY, 0) vector // through the camera rotation, then take that as the // base. // dy is in metres (drop Q16.16 fraction). rot is Q1.15; // (rot * metres) >> 15 yields metres again. int32_t dy = (int32_t)L->y1 - (cam->worldY >> CAM_POS_FRACT_BITS); int16_t baseX = (int16_t)((cam->rot[0][1] * dy) >> CAM_ROT_FRACT_BITS); int16_t baseY = (int16_t)((cam->rot[1][1] * dy) >> CAM_ROT_FRACT_BITS); int16_t baseZ = (int16_t)((cam->rot[2][1] * dy) >> CAM_ROT_FRACT_BITS); sceneryPipelineSetBase(&worldPipe, baseX, baseY, baseZ); SceneryVertexT a; SceneryVertexT b; sceneryProjectXZ(&worldPipe, (int16_t)L->x1, (int16_t)L->z1, &a); // Re-set base for endpoint b in case y2 != y1 (the // mountain ridge does this). Real scenery wouldn't, // since both endpoints share the section base. if (L->y2 != L->y1) { int32_t dy2 = (int32_t)L->y2 - (cam->worldY >> CAM_POS_FRACT_BITS); sceneryPipelineSetBase(&worldPipe, (int16_t)((cam->rot[0][1] * dy2) >> CAM_ROT_FRACT_BITS), (int16_t)((cam->rot[1][1] * dy2) >> CAM_ROT_FRACT_BITS), (int16_t)((cam->rot[2][1] * dy2) >> CAM_ROT_FRACT_BITS)); } sceneryProjectXZ(&worldPipe, (int16_t)L->x2, (int16_t)L->z2, &b); // Trivial reject: both endpoints share an off-screen // half-space (chunk5 $D3 "polygon outcode" AND test). if ((a.outcode & b.outcode) != 0) { continue; } int16_t x1; int16_t y1; int16_t x2; int16_t y2; if (!sceneryProjectVertexToScreen(&a, &x1, &y1)) { continue; } if (!sceneryProjectVertexToScreen(&b, &x2, &y2)) { continue; } rendererSetDrawColor(renderer, L->color); rendererDrawLine(renderer, x1, y1, x2, y2); } }