145 lines
4.8 KiB
Lua
145 lines
4.8 KiB
Lua
-- Minimal wireframe 3D projection engine
|
|
-- Flat ground plane, 60-degree horizontal FOV, near-plane clipping.
|
|
local Projection = {}
|
|
|
|
local cam = { x = 0, y = 9.7, z = 0, angle = 0, cosA = 1, sinA = 0 }
|
|
local CAM_HEIGHT = 9.7 -- camera eye at the tank's barrel height
|
|
local vp = { x = 0, y = 0, w = 1024, h = 600, halfW = 512, halfH = 300, horizonY = 0 }
|
|
local NEAR = 2.0
|
|
local SQRT3 = math.sqrt(3)
|
|
local focalLength = 512
|
|
|
|
function Projection.init(viewX, viewY, viewW, viewH)
|
|
vp.x = viewX
|
|
vp.y = viewY
|
|
vp.w = viewW
|
|
vp.h = viewH
|
|
vp.halfW = viewW / 2
|
|
vp.halfH = viewH / 2
|
|
-- 60° total horizontal FOV: focal = halfW * sqrt(3). Half-angle = atan(1/sqrt(3)) = 30°.
|
|
focalLength = vp.halfW * SQRT3
|
|
vp.horizonY = viewY + viewH * 0.5 -- horizon at exact centre; projection math treats cy=0 as horizon
|
|
end
|
|
|
|
function Projection.setCamera(x, z, angle)
|
|
-- World convention: at angle=0, player faces +z (into screen).
|
|
-- Forward unit vector = (sin(angle), cos(angle)). Camera rotates world by +angle
|
|
-- so that the player's forward direction maps to camera-space +z. Camera sits
|
|
-- at CAM_HEIGHT above the ground so the ground plane renders below horizon.
|
|
cam.x = x
|
|
cam.y = CAM_HEIGHT
|
|
cam.z = z
|
|
cam.angle = angle
|
|
cam.cosA = math.cos(angle)
|
|
cam.sinA = math.sin(angle)
|
|
end
|
|
|
|
function Projection.getViewport()
|
|
return vp
|
|
end
|
|
|
|
-- Transform world point to camera space
|
|
function Projection.worldToCamera(wx, wy, wz)
|
|
local dx = wx - cam.x
|
|
local dy = wy - cam.y
|
|
local dz = wz - cam.z
|
|
local cx = dx * cam.cosA - dz * cam.sinA
|
|
local cz = dx * cam.sinA + dz * cam.cosA
|
|
return cx, dy, cz
|
|
end
|
|
|
|
-- Project camera-space point to screen
|
|
function Projection.cameraToScreen(cx, cy, cz)
|
|
if cz <= NEAR then return nil, nil end
|
|
local sx = vp.x + vp.halfW + (cx / cz) * focalLength
|
|
local sy = vp.horizonY - (cy / cz) * focalLength
|
|
return sx, sy
|
|
end
|
|
|
|
-- Combined: world to screen
|
|
function Projection.projectPoint(wx, wy, wz)
|
|
local cx, cy, cz = Projection.worldToCamera(wx, wy, wz)
|
|
if cz <= NEAR then return nil, nil, cz end
|
|
local sx, sy = Projection.cameraToScreen(cx, cy, cz)
|
|
return sx, sy, cz
|
|
end
|
|
|
|
-- Clip and draw a 3D line segment
|
|
function Projection.drawLine3D(x1, y1, z1, x2, y2, z2)
|
|
local cx1, cy1, cz1 = Projection.worldToCamera(x1, y1, z1)
|
|
local cx2, cy2, cz2 = Projection.worldToCamera(x2, y2, z2)
|
|
|
|
-- Both behind camera
|
|
if cz1 <= NEAR and cz2 <= NEAR then return end
|
|
|
|
-- Clip against near plane
|
|
if cz1 <= NEAR then
|
|
local t = (NEAR - cz1) / (cz2 - cz1)
|
|
cx1 = cx1 + t * (cx2 - cx1)
|
|
cy1 = cy1 + t * (cy2 - cy1)
|
|
cz1 = NEAR
|
|
elseif cz2 <= NEAR then
|
|
local t = (NEAR - cz2) / (cz1 - cz2)
|
|
cx2 = cx2 + t * (cx1 - cx2)
|
|
cy2 = cy2 + t * (cy1 - cy2)
|
|
cz2 = NEAR
|
|
end
|
|
|
|
local sx1 = vp.x + vp.halfW + (cx1 / cz1) * focalLength
|
|
local sy1 = vp.horizonY - (cy1 / cz1) * focalLength
|
|
local sx2 = vp.x + vp.halfW + (cx2 / cz2) * focalLength
|
|
local sy2 = vp.horizonY - (cy2 / cz2) * focalLength
|
|
|
|
love.graphics.line(sx1, sy1, sx2, sy2)
|
|
end
|
|
|
|
-- Draw a wireframe model at a world position with Y-axis rotation
|
|
function Projection.drawModel(model, wx, wy, wz, rotY)
|
|
rotY = rotY or 0
|
|
local cosR = math.cos(rotY)
|
|
local sinR = math.sin(rotY)
|
|
|
|
for _, edge in ipairs(model) do
|
|
-- Rotate edge vertices around Y axis
|
|
local lx1 = edge[1] * cosR - edge[3] * sinR
|
|
local lz1 = edge[1] * sinR + edge[3] * cosR
|
|
local lx2 = edge[4] * cosR - edge[6] * sinR
|
|
local lz2 = edge[4] * sinR + edge[6] * cosR
|
|
|
|
Projection.drawLine3D(
|
|
wx + lx1, wy + edge[2], wz + lz1,
|
|
wx + lx2, wy + edge[5], wz + lz2
|
|
)
|
|
end
|
|
end
|
|
|
|
-- Rotate (x, y, z) through Euler angles (rx, ry, rz) applied in order X→Y→Z.
|
|
-- Used by tumbling debris.
|
|
local function rotateXYZ(x, y, z, cx, sx, cy, sy, cz, sz)
|
|
-- X-axis rotation
|
|
local y1 = y * cx - z * sx
|
|
local z1 = y * sx + z * cx
|
|
-- Y-axis rotation
|
|
local x2 = x * cy + z1 * sy
|
|
local z2 = -x * sy + z1 * cy
|
|
-- Z-axis rotation
|
|
local x3 = x2 * cz - y1 * sz
|
|
local y3 = x2 * sz + y1 * cz
|
|
return x3, y3, z2
|
|
end
|
|
|
|
-- Draw a wireframe model with full Euler-angle rotation (rx, ry, rz).
|
|
function Projection.drawModelXYZ(model, wx, wy, wz, rx, ry, rz)
|
|
rx = rx or 0; ry = ry or 0; rz = rz or 0
|
|
local cx, sx = math.cos(rx), math.sin(rx)
|
|
local cy, sy = math.cos(ry), math.sin(ry)
|
|
local cz, sz = math.cos(rz), math.sin(rz)
|
|
|
|
for _, edge in ipairs(model) do
|
|
local x1, y1, z1 = rotateXYZ(edge[1], edge[2], edge[3], cx, sx, cy, sy, cz, sz)
|
|
local x2, y2, z2 = rotateXYZ(edge[4], edge[5], edge[6], cx, sx, cy, sy, cz, sz)
|
|
Projection.drawLine3D(wx + x1, wy + y1, wz + z1, wx + x2, wy + y2, wz + z2)
|
|
end
|
|
end
|
|
|
|
return Projection
|