241 lines
11 KiB
C
241 lines
11 KiB
C
// 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 <stddef.h>
|
|
#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);
|
|
}
|
|
}
|