ludic/packages/ludic.render3d/camera.ludic
Orkuncakilkaya 2ce2d997be render3d: the fog wall culls on the CPU - nothing past it costs a draw, a vertex or a caster
cam_sphere_visible refuses a sphere wholly past the wall (terrain patches, scatter cells, stream
chunks, grass tiles, water), actors past it are skipped for draws, casters and outlines, the grass
reach and the streams' generate-and-gather reach end at it, and the card shadows cast only from
the wall's share of a layer (fog_casters.ludic, rebuilt every 4 m). r3d_beyond_fog is exported for
the game to skip animating what will not be drawn. Render-only: no query of the ground or the world
changes, and off is the old frame (0 pixels over 8 against 160ce96, twice per side).

Draws per frame at the overlook: 2757 off, 1104 at 175 m, 484 at 30 m.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:38:00 +03:00

98 lines
5.1 KiB
Text

# ============================================================================
# camera.ludic — a first-person fly camera (yaw / pitch, metres) and its view /
# projection matrices. Float bits throughout; see fmath.ludic.
# ============================================================================
# the view frustum's four side planes (a, b, c, d), float bits: left, right, bottom, top;
# from the clean (unjittered) view-projection, column-major m[col * 4 + row]
function cam_init(render3d_st: mut Render3dState, aspect: float) -> void {
render3d_st.cam_pos = v3_new(0.0, 2.0, 0.0)
render3d_st.cam_view = m4_new(); render3d_st.cam_proj = m4_new(); render3d_st.cam_vp = m4_new(); render3d_st.cam_inv_vp = m4_new(); render3d_st.cam_inv_proj = m4_new()
render3d_st.cam_vp_clean = m4_new(); render3d_st.cam_inv_vp_clean = m4_new()
render3d_st.cam_fwd = v3_new(0.0, 0.0, -1.0)
render3d_st.cam_right = v3_new(1.0, 0.0, 0.0)
render3d_st.cam_fov = Math.deg_to_rad(42.0)
render3d_st.cam_near = 0.3
render3d_st.cam_far = 14000.0
render3d_st.cam_aspect = aspect
cam_update(render3d_st)
}
function cam_begin_frame(render3d_st: mut Render3dState, n: int, w: int, h: int) -> void {
gsl_jitter_frame(render3d_st)
cam_update(render3d_st)
}
function cam_set(render3d_st: mut Render3dState, x: float, y: float, z: float, yaw_deg: float, pitch_deg: float) -> void {
v3_set(render3d_st.cam_pos, x, y, z)
render3d_st.cam_yaw = Math.deg_to_rad(yaw_deg)
render3d_st.cam_pitch = Math.deg_to_rad(pitch_deg)
cam_update(render3d_st)
}
function cam_update(render3d_st: mut Render3dState) -> void {
let cy = Math.cos(render3d_st.cam_yaw); let sy = Math.sin(render3d_st.cam_yaw)
let cp = Math.cos(render3d_st.cam_pitch); let sp = Math.sin(render3d_st.cam_pitch)
v3_set(render3d_st.cam_fwd, -(sy * cp), sp, -(cy * cp))
v3_set(render3d_st.cam_right, cy, 0.0, -sy)
let px = render3d_st.cam_pos[0]
let py = render3d_st.cam_pos[1]
let pz = render3d_st.cam_pos[2]
m4_look_at_xyz(render3d_st.cam_view, px, py, pz, px + render3d_st.cam_fwd[0], py + render3d_st.cam_fwd[1], pz + render3d_st.cam_fwd[2], 0.0, 1.0, 0.0)
m4_perspective(render3d_st.cam_proj, render3d_st.cam_fov, render3d_st.cam_aspect, render3d_st.cam_near, r3d_reach(render3d_st, render3d_st.cam_far))
m4_mul(render3d_st.cam_vp_clean, render3d_st.cam_proj, render3d_st.cam_view)
m4_inverse(render3d_st.cam_inv_vp_clean, render3d_st.cam_vp_clean)
# DLSS's sub-pixel jitter (streamline.ludic), in the projection the scene draws with only:
# culling, depth reconstruction and last frame's matrix keep the clean one
if render3d_st.gsl_jitter_x != 0.0 or render3d_st.gsl_jitter_y != 0.0 {
render3d_st.cam_proj[8] = render3d_st.cam_proj[8] - render3d_st.gsl_jitter_x
render3d_st.cam_proj[9] = render3d_st.cam_proj[9] - render3d_st.gsl_jitter_y
}
m4_mul(render3d_st.cam_vp, render3d_st.cam_proj, render3d_st.cam_view)
m4_inverse(render3d_st.cam_inv_vp, render3d_st.cam_vp)
m4_inverse(render3d_st.cam_inv_proj, render3d_st.cam_proj)
cam_planes_update(render3d_st)
}
function cam_planes_update(render3d_st: mut Render3dState) -> void {
let m = render3d_st.cam_vp_clean
for p in 0 .. 4 {
var r = 0
if p >= 2 { r = 1 }
var sg = 1.0
if p == 1 or p == 3 { sg = -1.0 }
var a = m[3] + sg * m[r]
var b = m[7] + sg * m[4 + r]
var c = m[11] + sg * m[8 + r]
var d = m[15] + sg * m[12 + r]
let inv = 1.0 / Math.sqrt(a * a + b * b + c * c)
render3d_st.cam_planes[p * 4] = a * inv; render3d_st.cam_planes[p * 4 + 1] = b * inv
render3d_st.cam_planes[p * 4 + 2] = c * inv; render3d_st.cam_planes[p * 4 + 3] = d * inv
}
}
# is a sphere (float bits) at least partly inside the side planes of the view?
function cam_sphere_visible(render3d_st: Render3dState, x: float, y: float, z: float, r: float) -> bool {
if render3d_st.cam_planes == null { return true }
if r3d_beyond_fog(render3d_st, x, y, z, r) { return false }
let nr = -r
for p in 0 .. 4 {
let o = p * 4
let dist = render3d_st.cam_planes[o] * x + render3d_st.cam_planes[o + 1] * y + render3d_st.cam_planes[o + 2] * z + render3d_st.cam_planes[o + 3]
if dist < nr { return false }
}
return true
}
# is a sphere wholly past the fog wall? Nothing there is seen, so nothing there is drawn or animated
export function r3d_beyond_fog(render3d_st: Render3dState, x: float, y: float, z: float, r: float) -> bool {
let w = render3d_st.r3d_fog_wall
if w <= 0.0 { return false }
let dx = x - render3d_st.cam_pos[0]; let dy = y - render3d_st.cam_pos[1]; let dz = z - render3d_st.cam_pos[2]
let far = w + R3D_FOG_MARGIN + r
return dx * dx + dy * dy + dz * dz > far * far
}
# fly: forward/strafe in metres, turn in radians
function cam_move(render3d_st: mut Render3dState, fwd: float, strafe: float, up: float, dyaw: float, dpitch: float) -> void {
render3d_st.cam_yaw = render3d_st.cam_yaw + dyaw
render3d_st.cam_pitch = Math.clamp(render3d_st.cam_pitch + dpitch, -1.5, 1.5)
v3_madd(render3d_st.cam_pos, render3d_st.cam_pos, render3d_st.cam_fwd, fwd)
v3_madd(render3d_st.cam_pos, render3d_st.cam_pos, render3d_st.cam_right, strafe)
render3d_st.cam_pos[1] = render3d_st.cam_pos[1] + up
cam_update(render3d_st)
}