Merge branch 'lang/chunks' into lang/uifree - the ground's heights by chunk and its Jolt ground per chunk; both new render checks kept (steady, chunks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
08a6fc8a3f
10 changed files with 216 additions and 12 deletions
|
|
@ -5,6 +5,7 @@ numbers float
|
|||
import "ludic.base"
|
||||
import "native.ludic"
|
||||
import "state.ludic"
|
||||
import "shape_ids.ludic"
|
||||
import "shapes.ludic"
|
||||
import "bodies.ludic"
|
||||
import "queries.ludic"
|
||||
|
|
|
|||
33
packages/ludic.physics/shape_ids.ludic
Normal file
33
packages/ludic.physics/shape_ids.ludic
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# shape_ids.ludic - a shape id is its place in ph_shapes. A shape let go leaves its place to the next
|
||||
# one made, so a ground that comes and goes by chunk grows nothing
|
||||
|
||||
function ph_keep(physics_st: mut PhysicsState, s: pointer) -> int {
|
||||
if s == null { return -1 }
|
||||
let n = len(physics_st.ph_spare)
|
||||
if n > 0 {
|
||||
let id = physics_st.ph_spare[n - 1]
|
||||
List.pop(physics_st.ph_spare)
|
||||
physics_st.ph_shapes[id] = s
|
||||
return id
|
||||
}
|
||||
push(physics_st.ph_shapes, s)
|
||||
return len(physics_st.ph_shapes) - 1
|
||||
}
|
||||
|
||||
function ph_shape(physics_st: PhysicsState, id: int) -> pointer {
|
||||
if physics_st.ph_shapes == null or id < 0 or id >= len(physics_st.ph_shapes) { return null }
|
||||
return physics_st.ph_shapes[id]
|
||||
}
|
||||
|
||||
# a shape let go: a body made with it keeps its own hold until it is removed, and the id may be
|
||||
# handed to the next shape made
|
||||
export function phys_shape_free(physics_st: mut PhysicsState, id: int) -> void {
|
||||
let s = ph_shape(physics_st, id)
|
||||
if s == null { return }
|
||||
jph_shape_free(s)
|
||||
physics_st.ph_shapes[id] = null
|
||||
push(physics_st.ph_spare, id)
|
||||
}
|
||||
|
||||
# how many shapes are held: a ground that comes and goes must hold this steady
|
||||
export function phys_shapes(physics_st: PhysicsState) -> int { return len(physics_st.ph_shapes) - len(physics_st.ph_spare) }
|
||||
|
|
@ -32,23 +32,12 @@ export function phys_close(physics_st: mut PhysicsState) -> void {
|
|||
}
|
||||
}
|
||||
physics_st.ph_shapes = new []pointer
|
||||
List.clear(physics_st.ph_spare)
|
||||
List.clear(physics_st.ph_floats)
|
||||
List.clear(physics_st.ph_float_k)
|
||||
if physics_st.ph_facts != null { q_clear(physics_st.ph_facts) }
|
||||
}
|
||||
|
||||
function ph_keep(physics_st: mut PhysicsState, s: pointer) -> int {
|
||||
if s == null { return -1 }
|
||||
ph_buffers()
|
||||
push(physics_st.ph_shapes, s)
|
||||
return len(physics_st.ph_shapes) - 1
|
||||
}
|
||||
|
||||
function ph_shape(physics_st: PhysicsState, id: int) -> pointer {
|
||||
if physics_st.ph_shapes == null or id < 0 or id >= len(physics_st.ph_shapes) { return null }
|
||||
return physics_st.ph_shapes[id]
|
||||
}
|
||||
|
||||
# a crate: half extents in metres
|
||||
export function phys_box(physics_st: mut PhysicsState, hx: float, hy: float, hz: float) -> int { return ph_keep(physics_st, jph_shape_box(hx, hy, hz)) }
|
||||
export function phys_sphere(physics_st: mut PhysicsState, r: float) -> int { return ph_keep(physics_st, jph_shape_sphere(r)) }
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export const PH_CAP: int = 512 # contacts drained a step, queries' buffer
|
|||
export state PhysicsState {
|
||||
ph_world: pointer = null
|
||||
ph_shapes: []pointer = new []pointer # a shape id is its place here; null once freed
|
||||
ph_spare: []int = new []int # the freed places, handed out again (shape_ids.ludic)
|
||||
ph_step: float = 0.016666668 # seconds a step
|
||||
ph_sub: int = 1 # collision steps in each
|
||||
ph_max_steps: int = 4 # a slow frame catches up this far and no further
|
||||
|
|
|
|||
48
packages/ludic.physics/tests/shape_free_test.ludic
Normal file
48
packages/ludic.physics/tests/shape_free_test.ludic
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# shape_free_test.ludic - a shape let go: its body stands until removed, its id goes to the next
|
||||
# shape made, and a ground that comes and goes a thousand times holds no more shapes than at once
|
||||
import "ludic.physics"
|
||||
import "ludic.base"
|
||||
program ShapeFreeTest {
|
||||
numbers float
|
||||
function down(physics_st: mut PhysicsState, x: float, z: float) -> PhysHit {
|
||||
let h = new PhysHit
|
||||
phys_ray(physics_st, x, 50.0, z, 0.0, -100.0, 0.0, PHYS_M_ALL, h)
|
||||
return h
|
||||
}
|
||||
# a flat 33 x 33 field at height y, 2 m cells, its first sample at (ox, 0)
|
||||
function field(physics_st: mut PhysicsState, h: []float, ox: float, y: float) -> int {
|
||||
for i in 0 .. 33 * 33 { h[i] = y }
|
||||
return phys_heightfield(physics_st, h, 33, ox, 0.0, 2.0)
|
||||
}
|
||||
|
||||
test "a body outlives its shape's id, and the id is handed out again" (physics_st: mut PhysicsState) {
|
||||
expect(phys_open(physics_st, 64, 1))
|
||||
let h = floats(33 * 33)
|
||||
let s = field(physics_st, h, 0.0, 3.0)
|
||||
let b = phys_ground_add(physics_st, s)
|
||||
phys_shape_free(physics_st, s)
|
||||
phys_settle(physics_st)
|
||||
expect(Math.abs(down(physics_st, 10.0, 10.0).y - 3.0) < 0.01)
|
||||
expect_eq(field(physics_st, h, 100.0, 5.0), s)
|
||||
phys_remove(physics_st, b)
|
||||
expect(down(physics_st, 10.0, 10.0).body < 0)
|
||||
phys_close(physics_st)
|
||||
}
|
||||
|
||||
test "a thousand chunks in and out hold two shapes and grow nothing" (physics_st: mut PhysicsState) {
|
||||
expect(phys_open(physics_st, 64, 1))
|
||||
let h = floats(33 * 33)
|
||||
let keep = phys_box(physics_st, 1.0, 1.0, 1.0)
|
||||
for k in 0 .. 1000 {
|
||||
let s = field(physics_st, h, float(k % 7) * 64.0, 1.0)
|
||||
let b = phys_ground_add(physics_st, s)
|
||||
phys_remove(physics_st, b)
|
||||
phys_shape_free(physics_st, s)
|
||||
}
|
||||
expect_eq(phys_shapes(physics_st), 1)
|
||||
expect(phys_box(physics_st, 1.0, 1.0, 1.0) != keep)
|
||||
expect_eq(phys_shapes(physics_st), 2)
|
||||
phys_close(physics_st)
|
||||
expect_eq(phys_shapes(physics_st), 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import "camera.ludic"
|
|||
import "sky.ludic"
|
||||
import "daylight.ludic"
|
||||
import "terrain.ludic"
|
||||
import "terrain_chunks.ludic"
|
||||
import "overlay.ludic"
|
||||
import "shadow.ludic"
|
||||
import "post.ludic"
|
||||
|
|
|
|||
40
packages/ludic.render3d/terrain_chunks.ludic
Normal file
40
packages/ludic.render3d/terrain_chunks.ludic
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# terrain_chunks.ludic - the height field a chunk at a time, for whatever is built per chunk (the
|
||||
# ground's physics, plan 23.5 of maroon-lake): the texels the renderer read back, as they are.
|
||||
|
||||
# the ground's height at texel (tx, tz) as the renderer draws it - the cubic B-spline, which at a
|
||||
# texel's centre is the separable (1 4 1) / 6 filter of its neighbours, clamped at the map's edge:
|
||||
# the same numbers ludic.physics' jph_shape_heightfield_bspline makes of the whole map
|
||||
function ter_spline_at(render3d_st: Render3dState, tx: int, tz: int) -> float {
|
||||
var sum = 0.0
|
||||
for d in -1 .. 2 {
|
||||
let z = min(max(tz + d, 0), TERRAIN_RES - 1)
|
||||
let r = z * TERRAIN_RES
|
||||
let row = (render3d_st.ter_heights[r + max(tx - 1, 0)] + 4.0 * render3d_st.ter_heights[r + tx] + render3d_st.ter_heights[r + min(tx + 1, TERRAIN_RES - 1)]) / 6.0
|
||||
if d == 0 { sum = sum + 4.0 * row } else { sum = sum + row }
|
||||
}
|
||||
return sum / 6.0
|
||||
}
|
||||
|
||||
# The heights of chunk (i, j) of a size_m grid laid from the terrain's corner (ter_ox - TERRAIN_HALF,
|
||||
# ter_oz - TERRAIN_HALF): n x n samples, row-major (z rows, x across), sample (a, b) at corner +
|
||||
# (i * size_m + a * size_m / (n - 1), j * size_m + b * size_m / (n - 1)), each the ground's
|
||||
# B-spline height at the texel under it (ter_spline_at: what the physics ground is built from).
|
||||
# Neighbours share their edge row and column, so seams agree to the bit. Into `out` (n * n floats,
|
||||
# the caller's, reused), nothing allocated. The read-back lives from terrain_generate to
|
||||
# terrain_unload - the whole of a map - so a chunk can be read any time between; false when there
|
||||
# is none (a plate, or before the map is made).
|
||||
function terrain_chunk_heights(render3d_st: Render3dState, i: int, j: int, size_m: float, n: int, out: floats) -> bool {
|
||||
if render3d_st.ter_heights == null or n < 2 or len(out) < n * n { return false }
|
||||
let texel = 2.0 * float(render3d_st.TERRAIN_HALF) / float(TERRAIN_RES)
|
||||
let step = size_m / float(n - 1)
|
||||
let x0 = float(i) * size_m
|
||||
let z0 = float(j) * size_m
|
||||
for b in 0 .. n {
|
||||
let tz = min(max(int((z0 + float(b) * step) / texel + 0.5), 0), TERRAIN_RES - 1)
|
||||
for a in 0 .. n {
|
||||
let tx = min(max(int((x0 + float(a) * step) / texel + 0.5), 0), TERRAIN_RES - 1)
|
||||
out[b * n + a] = ter_spline_at(render3d_st, tx, tz)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue