-- 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