oma-tank/rendering/projection.lua
2026-04-18 11:35:06 +01:00

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