feat(lang): strict numbers in float files; render3d on float

A numbers float file adapts decimal literals to a fixed operand or slot, and refuses to
promote a computed int to a float implicitly: there it is almost always float bits. Explicit
float(x) is always allowed.

render3d's numbers are float, converted by tools/migrate/floatbits.py - a whole-program
inference of which ints carried IEEE bits (union-find over flows, calls, returns, buffers,
nested buffers and lexical scopes) and a rewriter to operators, Math.* and float literals,
with float_bits / float_from_bits left only where bits really cross (runtime scratch
buffers, mixed buffers). Seed regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-23 17:13:25 +03:00
parent 3ac0d8d5cb
commit cc89fc37a4
35 changed files with 57282 additions and 54382 deletions

View file

@ -13,14 +13,14 @@
property Actor {
model: Model,
x: int = 0, # float bits, metres
y: int = 0,
z: int = 0,
yaw: int = 0, # radians; 0 faces -z like the camera
scale: int = 0,
tint: words,
rough: int = 0,
mat: words, # the model matrix
x: float = 0.0, # float bits, metres
y: float = 0.0,
z: float = 0.0,
yaw: float = 0.0, # radians; 0 faces -z like the camera
scale: float = 0.0,
tint: floats,
rough: float = 0.0,
mat: floats, # the model matrix
visible: bool = true,
# Drawn and CASTING are not the same question. An actor hidden because the camera is inside
# its head still stands in the sun, and a body that stops casting the moment you look out of
@ -31,14 +31,14 @@ property Actor {
casts: bool = true,
id: int = 0,
cutout: bool = false, # alpha-tested (a flame's cards)
emissive: int = 0, # float bits: self-lit strength
emissive: float = 0.0, # float bits: self-lit strength
skin: Skin, # this instance's own pose (skin_clone); null: the model's
ptint: words, # 4 per primitive: on flag, r, g, b (a part's own colour)
hide: words, # 1 per primitive: skip it (a cap taken off)
radius: int = 0, # float bits: bounding radius for culling (0 = never culled)
cull: int = 0, # float bits: not drawn beyond this distance (0 = always)
outline: int = 0, # float bits: metres of rim drawn around it (0 = none)
ocol: words # the rim's colour (null = white)
radius: float = 0.0, # float bits: bounding radius for culling (0 = never culled)
cull: float = 0.0, # float bits: not drawn beyond this distance (0 = always)
outline: float = 0.0, # float bits: metres of rim drawn around it (0 = none)
ocol: floats # the rim's colour (null = white)
}
# a program and its uniform locations
property AcProg {
@ -98,11 +98,11 @@ function actor_remove(a: Actor) -> void {
ac_actors = keep
}
# colour one named part of the model (a material name from the file)
function actor_tint_part(a: Actor, name: string, r: int, g: int, b: int) -> void {
function actor_tint_part(a: Actor, name: string, r: float, g: float, b: float) -> void {
if a.model == null { return }
let n = len(a.model.prims)
if a.ptint == null { a.ptint = words(n * 4); for i in 0 .. n * 4 { a.ptint[i] = 0 } }
for i in 0 .. n { if a.model.prims[i].name == name { a.ptint[i * 4] = 1; a.ptint[i * 4 + 1] = r; a.ptint[i * 4 + 2] = g; a.ptint[i * 4 + 3] = b } }
for i in 0 .. n { if a.model.prims[i].name == name { a.ptint[i * 4] = 1; a.ptint[i * 4 + 1] = float_bits(r); a.ptint[i * 4 + 2] = float_bits(g); a.ptint[i * 4 + 3] = float_bits(b) } }
}
function actor_hide_part(a: Actor, name: string, hidden: bool) -> void {
if a.model == null { return }
@ -116,19 +116,19 @@ function actor_hide_part(a: Actor, name: string, hidden: bool) -> void {
function actor_new(model: Model) -> Actor {
let a = new Actor
a.model = model
a.scale = F_ONE
a.tint = v3_new(F_ONE, F_ONE, F_ONE)
a.rough = F_ONE
a.scale = 1.0
a.tint = v3_new(1.0, 1.0, 1.0)
a.rough = 1.0
a.mat = m4_new()
ac_next_id += 1; a.id = ac_next_id
if model != null { a.radius = f_add(f_max(model.radius, model.height), F_ONE) }
a.cull = fi(450)
if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 }
a.cull = 450.0
if ac_actors == null { ac_actors = new []Actor }
push(ac_actors, a)
return a
}
function actor_place(a: Actor, x: int, y: int, z: int, yaw: int) -> void {
function actor_place(a: Actor, x: float, y: float, z: float, yaw: float) -> void {
a.x = x; a.y = y; a.z = z; a.yaw = yaw
m4_trs(a.mat, x, y, z, yaw, a.scale)
}
@ -141,7 +141,7 @@ function ac_bind_scene(ap: AcProg) -> void {
gpu_use_program(p)
u_mat4(ap.l_view, cam_view)
u_mat4(ap.l_proj, cam_proj)
u_f(ap.l_mh, F_ZERO)
u_f(ap.l_mh, 0.0)
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
@ -150,22 +150,22 @@ function ac_visible(a: Actor, shadow: bool) -> bool {
if a.model == null { return false }
if not a.visible and not (shadow and a.cast_hidden) { return false }
if shadow and not a.casts { return false }
if a.cull != 0 {
let dx = f_sub(a.x, cam_pos[0]); let dz = f_sub(a.z, cam_pos[2])
let d2 = f_add(f_mul(dx, dx), f_mul(dz, dz))
if a.cull != 0.0 {
let dx = a.x - cam_pos[0]; let dz = a.z - cam_pos[2]
let d2 = dx * dx + dz * dz
var c = a.cull
if shadow { c = f_min(c, fi(300)) }
if f_gt(d2, f_mul(c, c)) { return false }
if shadow { c = Math.min(c, 300.0) }
if d2 > c * c { return false }
}
if not shadow and a.radius != 0 {
let r = f_mul(a.radius, a.scale)
var cy = f_add(a.y, r)
if not shadow and a.radius != 0.0 {
let r = a.radius * a.scale
var cy = a.y + r
# In the water reflection what reaches the picture is the actor's mirror image, 2L - y. The pass
# keeps the main camera's frustum planes, so the image is what has to be tested against them: the
# actor itself was, and the town's people - far above the lake, their images far below the frame -
# all drew again into the reflection.
if ter_reflect { cy = f_sub(f_mul(F_TWO, water_level), cy) }
if not cam_sphere_visible(a.x, cy, a.z, f_mul(r, fl(1.5))) { return false }
if ter_reflect { cy = 2.0 * water_level - cy }
if not cam_sphere_visible(a.x, cy, a.z, r * 1.5) { return false }
}
return true
}
@ -173,9 +173,9 @@ function actor_draw_one(a: Actor, ap: AcProg, shadow: bool) -> void {
let p = ap.prog
gpu_use_program(p)
u_mat4(ap.l_model, a.mat)
var skinned = F_ZERO
if a.skin != null { skinned = F_ONE; u_mat4n(ap.l_bones, a.skin.n_joints, a.skin.bones) }
else if a.model.skin != null { skinned = F_ONE; u_mat4n(ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
var skinned = 0.0
if a.skin != null { skinned = 1.0; u_mat4n(ap.l_bones, a.skin.n_joints, a.skin.bones) }
else if a.model.skin != null { skinned = 1.0; u_mat4n(ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
u_f(ap.l_skin, skinned)
if not shadow {
u_f(ap.l_emis, a.emissive)
@ -187,7 +187,7 @@ function actor_draw_one(a: Actor, ap: AcProg, shadow: bool) -> void {
let pr = model.prims[i]
if a.hide != null and a.hide[i] != 0 { continue }
if not shadow {
if a.ptint != null and a.ptint[i * 4] != 0 { u_f3(ap.l_tint, a.ptint[i * 4 + 1], a.ptint[i * 4 + 2], a.ptint[i * 4 + 3]) }
if a.ptint != null and a.ptint[i * 4] != 0 { u_f3(ap.l_tint, float_from_bits(a.ptint[i * 4 + 1]), float_from_bits(a.ptint[i * 4 + 2]), float_from_bits(a.ptint[i * 4 + 3])) }
else { u_v3(ap.l_tint, a.tint) }
}
gpu_tex_unit(0); gpu_tex_bind(GPU_TEX2D, pr.diff)
@ -218,11 +218,11 @@ function actor_draw_one(a: Actor, ap: AcProg, shadow: bool) -> void {
# would have arrived at (for a grounded layer, the terrain height at that instance).
property OutlineReq {
model: Model,
mat: words,
width: int = 0,
r: int = 0,
g: int = 0,
b: int = 0
mat: floats,
width: float = 0.0,
r: float = 0.0,
g: float = 0.0,
b: float = 0.0
}
var ac_oq: []OutlineReq = null
var ac_oq_n: int = 0 # live entries; the array is kept and reused
@ -232,8 +232,8 @@ var ac_oq_n: int = 0 # live entries; the array is kept and reuse
# batch rather than clearing it, and the next add opens a new one: every pass in a frame
# sees the same requests, and the caller needs no frame hook.
var ac_oq_closed: bool = true
function outline_model(m: Model, mat: words, width: int, r: int, g: int, b: int) -> void {
if m == null or mat == null or width == 0 { return }
function outline_model(m: Model, mat: floats, width: float, r: float, g: float, b: float) -> void {
if m == null or mat == null or width == 0.0 { return }
if ac_oq_closed { ac_oq_n = 0; ac_oq_closed = false }
if ac_oq == null { ac_oq = new []OutlineReq }
var q: OutlineReq = null
@ -300,19 +300,19 @@ function actor_draw() -> void {
# against the scene, so anything in front of the actor hides its rim too.
function actor_draw_outlines() -> void {
var any = ac_oq_n > 0
for i in 0 .. len(ac_actors) { if ac_actors[i].outline != 0 and ac_visible(ac_actors[i], false) { any = true; break } }
for i in 0 .. len(ac_actors) { if ac_actors[i].outline != 0.0 and ac_visible(ac_actors[i], false) { any = true; break } }
if not any { return }
gpu_cull(true)
gpu_cull_face(GL_FRONT)
for i in 0 .. len(ac_actors) {
let a = ac_actors[i]
if a.outline == 0 or not ac_visible(a, false) { continue }
if a.outline == 0.0 or not ac_visible(a, false) { continue }
var ap = ac_out
if a.cutout { ap = ac_out_cut }
gpu_use_program(ap.prog)
u_mat4(ap.l_view, cam_view); u_mat4(ap.l_proj, cam_proj)
u_f(ap.l_out, f_mul(a.outline, a.scale))
if a.ocol != null { u_v3(ap.l_ocol, a.ocol) } else { u_f3(ap.l_ocol, F_ONE, F_ONE, F_ONE) }
u_f(ap.l_out, a.outline * a.scale)
if a.ocol != null { u_v3(ap.l_ocol, a.ocol) } else { u_f3(ap.l_ocol, 1.0, 1.0, 1.0) }
actor_draw_outline_one(a, ap)
}
# and whatever asked for a rim without being an actor
@ -324,7 +324,7 @@ function actor_draw_outlines() -> void {
u_f(ap.l_out, q.width)
u_f3(ap.l_ocol, q.r, q.g, q.b)
u_mat4(ap.l_model, q.mat)
u_f(ap.l_skin, F_ZERO)
u_f(ap.l_skin, 0.0)
for k in 0 .. len(q.model.prims) { mesh_draw(q.model.prims[k].mesh) }
}
ac_oq_closed = true
@ -332,9 +332,9 @@ function actor_draw_outlines() -> void {
}
function actor_draw_outline_one(a: Actor, ap: AcProg) -> void {
u_mat4(ap.l_model, a.mat)
var skinned = F_ZERO
if a.skin != null { skinned = F_ONE; u_mat4n(ap.l_bones, a.skin.n_joints, a.skin.bones) }
else if a.model.skin != null { skinned = F_ONE; u_mat4n(ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
var skinned = 0.0
if a.skin != null { skinned = 1.0; u_mat4n(ap.l_bones, a.skin.n_joints, a.skin.bones) }
else if a.model.skin != null { skinned = 1.0; u_mat4n(ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
u_f(ap.l_skin, skinned)
let model = a.model
for i in 0 .. len(model.prims) {
@ -349,47 +349,47 @@ function actor_draw_outline_one(a: Actor, ap: AcProg) -> void {
# three axis offsets. An actor outside the box casts nothing into that layer: skipping it changes no
# depth, only the draw. (Each actor used to draw into every cascade out to 300 m - in town that was
# about 1500 skinned shadow draws a frame against 69 in the lit pass.)
var ac_lp0: words = null
var ac_lpx: words = null
var ac_lpy: words = null
var ac_lpz: words = null
function ac_in_light(vp: words, x: int, y: int, z: int, r: int) -> bool {
if ac_lp0 == null { ac_lp0 = words(3); ac_lpx = words(3); ac_lpy = words(3); ac_lpz = words(3) }
var ac_lp0: floats = null
var ac_lpx: floats = null
var ac_lpy: floats = null
var ac_lpz: floats = null
function ac_in_light(vp: floats, x: float, y: float, z: float, r: float) -> bool {
if ac_lp0 == null { ac_lp0 = floats(3); ac_lpx = floats(3); ac_lpy = floats(3); ac_lpz = floats(3) }
m4_xform_point(ac_lp0, vp, x, y, z)
m4_xform_point(ac_lpx, vp, f_add(x, r), y, z)
m4_xform_point(ac_lpy, vp, x, f_add(y, r), z)
m4_xform_point(ac_lpz, vp, x, y, f_add(z, r))
m4_xform_point(ac_lpx, vp, x + r, y, z)
m4_xform_point(ac_lpy, vp, x, y + r, z)
m4_xform_point(ac_lpz, vp, x, y, z + r)
for k in 0 .. 2 {
let c = ac_lp0[k]
let e = f_add(f_add(ac_abs(f_sub(ac_lpx[k], c)), ac_abs(f_sub(ac_lpy[k], c))), ac_abs(f_sub(ac_lpz[k], c)))
if f_gt(f_sub(ac_abs(c), e), F_ONE) { return false }
let e = ac_abs(ac_lpx[k] - c) + ac_abs(ac_lpy[k] - c) + ac_abs(ac_lpz[k] - c)
if ac_abs(c) - e > 1.0 { return false }
}
return true
}
function ac_abs(v: int) -> int { return f_max(v, f_neg(v)) }
function ac_abs(v: float) -> float { return Math.max(v, -v) }
function actor_draw_casters(light_vp: words) -> void {
function actor_draw_casters(light_vp: floats) -> void {
if ac_actors == null { return }
gpu_use_program(ac_sh.prog); u_mat4(ac_sh.l_lvp, light_vp)
gpu_use_program(ac_sh_cut.prog); u_mat4(ac_sh_cut.l_lvp, light_vp)
for i in 0 .. len(ac_actors) {
let a = ac_actors[i]
if not ac_visible(a, true) { continue }
if a.radius != 0 {
if a.radius != 0.0 {
# the radius is the horizontal extent: a person is far taller than wide, so the sphere is
# centred at half the model's height and reaches the larger of the two, with a margin for
# the pose (a first version centred it at the radius and dropped a head at a cascade's edge)
let hh = f_mul(f_mul(a.model.height, F_HALF), a.scale)
let r = f_max(f_mul(a.radius, a.scale), hh)
if not ac_in_light(light_vp, a.x, f_add(a.y, hh), a.z, f_mul(r, fl(1.5))) { continue }
let hh = a.model.height * 0.5 * a.scale
let r = Math.max(a.radius * a.scale, hh)
if not ac_in_light(light_vp, a.x, a.y + hh, a.z, r * 1.5) { continue }
}
# Inside the light box is not the same as able to shade anything this cascade covers: every actor
# within 300 m sits inside the far cascades' boxes, whose receivers start 250 and 1100 m out - at
# the camp 160 actors cast 300 draws into each of those, against 41 into the nearest.
let dxa = f_sub(a.x, cam_pos[0]); let dza = f_sub(a.z, cam_pos[2])
let da = f_sqrt(f_add(f_mul(dxa, dxa), f_mul(dza, dza)))
let ra = f_mul(a.radius, a.scale)
if not cast_band_reaches(f_max(f_sub(da, ra), F_ZERO), f_add(da, ra), f_mul(a.model.height, a.scale)) { continue }
let dxa = a.x - cam_pos[0]; let dza = a.z - cam_pos[2]
let da = Math.sqrt(dxa * dxa + dza * dza)
let ra = a.radius * a.scale
if not cast_band_reaches(Math.max(da - ra, 0.0), da + ra, a.model.height * a.scale) { continue }
var ap = ac_sh
if a.cutout { ap = ac_sh_cut }
if ac_census_at > 0 and ac_frame == ac_census_at { ac_caster_count(a) }

View file

@ -3,35 +3,35 @@
# projection matrices. Float bits throughout; see fmath.ludic.
# ============================================================================
var cam_pos: words = null # x, y, z
var cam_yaw: int = 0 # radians, 0 = looking down -z
var cam_pitch: int = 0
var cam_fov: int = 0 # vertical, radians
var cam_near: int = 0
var cam_far: int = 0
var cam_aspect: int = 0
var cam_view: words = null
var cam_proj: words = null
var cam_vp: words = null
var cam_inv_vp: words = null
var cam_inv_proj: words = null
var cam_vp_clean: words = null # view-projection, kept for depth reconstruction
var cam_inv_vp_clean: words = null
var cam_fwd: words = null
var cam_right: words = null
var cam_pos: floats = null # x, y, z
var cam_yaw: float = 0.0 # radians, 0 = looking down -z
var cam_pitch: float = 0.0
var cam_fov: float = 0.0 # vertical, radians
var cam_near: float = 0.0
var cam_far: float = 0.0
var cam_aspect: float = 0.0
var cam_view: floats = null
var cam_proj: floats = null
var cam_vp: floats = null
var cam_inv_vp: floats = null
var cam_inv_proj: floats = null
var cam_vp_clean: floats = null # view-projection, kept for depth reconstruction
var cam_inv_vp_clean: floats = null
var cam_fwd: floats = null
var cam_right: floats = null
# 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]
var cam_planes: words = null
var cam_planes: floats = null
function cam_init(aspect: int) -> void {
cam_pos = v3_new(F_ZERO, fi(2), F_ZERO)
function cam_init(aspect: float) -> void {
cam_pos = v3_new(0.0, 2.0, 0.0)
cam_view = m4_new(); cam_proj = m4_new(); cam_vp = m4_new(); cam_inv_vp = m4_new(); cam_inv_proj = m4_new()
cam_vp_clean = m4_new(); cam_inv_vp_clean = m4_new()
cam_fwd = v3_new(F_ZERO, F_ZERO, f_neg1())
cam_right = v3_new(F_ONE, F_ZERO, F_ZERO)
cam_fov = f_rad(fi(42))
cam_near = fl(0.3)
cam_far = fi(14000)
cam_fwd = v3_new(0.0, 0.0, -1.0)
cam_right = v3_new(1.0, 0.0, 0.0)
cam_fov = Math.deg_to_rad(42.0)
cam_near = 0.3
cam_far = 14000.0
cam_aspect = aspect
cam_update()
}
@ -39,29 +39,29 @@ function cam_begin_frame(n: int, w: int, h: int) -> void {
gsl_jitter_frame()
cam_update()
}
function cam_set(x: int, y: int, z: int, yaw_deg: int, pitch_deg: int) -> void {
function cam_set(x: float, y: float, z: float, yaw_deg: float, pitch_deg: float) -> void {
v3_set(cam_pos, x, y, z)
cam_yaw = f_rad(yaw_deg)
cam_pitch = f_rad(pitch_deg)
cam_yaw = Math.deg_to_rad(yaw_deg)
cam_pitch = Math.deg_to_rad(pitch_deg)
cam_update()
}
function cam_update() -> void {
let cy = f_cos(cam_yaw); let sy = f_sin(cam_yaw)
let cp = f_cos(cam_pitch); let sp = f_sin(cam_pitch)
v3_set(cam_fwd, f_neg(f_mul(sy, cp)), sp, f_neg(f_mul(cy, cp)))
v3_set(cam_right, cy, F_ZERO, f_neg(sy))
let at = words(3)
let cy = Math.cos(cam_yaw); let sy = Math.sin(cam_yaw)
let cp = Math.cos(cam_pitch); let sp = Math.sin(cam_pitch)
v3_set(cam_fwd, -(sy * cp), sp, -(cy * cp))
v3_set(cam_right, cy, 0.0, -sy)
let at = floats(3)
v3_add(at, cam_pos, cam_fwd)
let up = v3_new(F_ZERO, F_ONE, F_ZERO)
let up = v3_new(0.0, 1.0, 0.0)
m4_look_at(cam_view, cam_pos, at, up)
m4_perspective(cam_proj, cam_fov, cam_aspect, cam_near, cam_far)
m4_mul(cam_vp_clean, cam_proj, cam_view)
m4_inverse(cam_inv_vp_clean, 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 gsl_jitter_x != 0 or gsl_jitter_y != 0 {
cam_proj[8] = f_sub(cam_proj[8], gsl_jitter_x)
cam_proj[9] = f_sub(cam_proj[9], gsl_jitter_y)
if gsl_jitter_x != 0.0 or gsl_jitter_y != 0.0 {
cam_proj[8] = cam_proj[8] - gsl_jitter_x
cam_proj[9] = cam_proj[9] - gsl_jitter_y
}
m4_mul(cam_vp, cam_proj, cam_view)
m4_inverse(cam_inv_vp, cam_vp)
@ -70,39 +70,39 @@ function cam_update() -> void {
cam_planes_update()
}
function cam_planes_update() -> void {
if cam_planes == null { cam_planes = words(16) }
if cam_planes == null { cam_planes = floats(16) }
let m = cam_vp_clean
for p in 0 .. 4 {
var r = 0
if p >= 2 { r = 1 }
var sg = F_ONE
if p == 1 or p == 3 { sg = f_neg(F_ONE) }
var a = f_add(m[3], f_mul(sg, m[r]))
var b = f_add(m[7], f_mul(sg, m[4 + r]))
var c = f_add(m[11], f_mul(sg, m[8 + r]))
var d = f_add(m[15], f_mul(sg, m[12 + r]))
let inv = f_div(F_ONE, f_sqrt(f_add(f_add(f_mul(a, a), f_mul(b, b)), f_mul(c, c))))
cam_planes[p * 4] = f_mul(a, inv); cam_planes[p * 4 + 1] = f_mul(b, inv)
cam_planes[p * 4 + 2] = f_mul(c, inv); cam_planes[p * 4 + 3] = f_mul(d, inv)
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)
cam_planes[p * 4] = a * inv; cam_planes[p * 4 + 1] = b * inv
cam_planes[p * 4 + 2] = c * inv; 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(x: int, y: int, z: int, r: int) -> bool {
function cam_sphere_visible(x: float, y: float, z: float, r: float) -> bool {
if cam_planes == null { return true }
let nr = f_neg(r)
let nr = -r
for p in 0 .. 4 {
let o = p * 4
let dist = f_add(f_add(f_add(f_mul(cam_planes[o], x), f_mul(cam_planes[o + 1], y)), f_mul(cam_planes[o + 2], z)), cam_planes[o + 3])
if f_ls(dist, nr) { return false }
let dist = cam_planes[o] * x + cam_planes[o + 1] * y + cam_planes[o + 2] * z + cam_planes[o + 3]
if dist < nr { return false }
}
return true
}
# fly: forward/strafe in metres, turn in radians
function cam_move(fwd: int, strafe: int, up: int, dyaw: int, dpitch: int) -> void {
cam_yaw = f_add(cam_yaw, dyaw)
cam_pitch = f_clamp(f_add(cam_pitch, dpitch), f_neg(fl(1.5)), fl(1.5))
function cam_move(fwd: float, strafe: float, up: float, dyaw: float, dpitch: float) -> void {
cam_yaw = cam_yaw + dyaw
cam_pitch = Math.clamp(cam_pitch + dpitch, -1.5, 1.5)
v3_madd(cam_pos, cam_pos, cam_fwd, fwd)
v3_madd(cam_pos, cam_pos, cam_right, strafe)
cam_pos[1] = f_add(cam_pos[1], up)
cam_pos[1] = cam_pos[1] + up
cam_update()
}

View file

@ -9,31 +9,31 @@
const COL_CELL: int = 16
const COL_CAP: int = 120000
var col_x: words = null
var col_z: words = null
var col_r: words = null
var col_x: floats = null
var col_z: floats = null
var col_r: floats = null
# What a collider occupies VERTICALLY: y0 its base, y1 its top, metres, float bits. A circle used
# to be an infinite pillar - you could not climb a boulder, and a knee-high rock stopped you dead,
# because there was no height to compare against. col_add keeps that shape (a span from far below
# to far above) so every existing caller behaves exactly as it did; col_add_h gives a real one.
var col_y0: words = null
var col_y1: words = null
var col_y0: floats = null
var col_y1: floats = null
var col_n: int = 0
var col_side: int = 0 # cells per side
var col_start: words = null # per cell: first index into col_sorted (side*side + 1)
var col_sorted: words = null
var col_built: bool = false
var col_out: words = null # the resolved position (x, z)
var col_out: floats = null # the resolved position (x, z)
const COL_LOW: int = 0xCB800000 # -16777216.0: below any ground
const COL_HIGH: int = 0x4B800000 # 16777216.0: above any sky
function col_add(x: int, z: int, r: int) -> void { col_add_h(x, z, r, COL_LOW, COL_HIGH) }
const COL_LOW: float = -16777216.0 # -16777216.0: below any ground
const COL_HIGH: float = 16777216.0 # 16777216.0: above any sky
function col_add(x: float, z: float, r: float) -> void { col_add_h(x, z, r, COL_LOW, COL_HIGH) }
# a collider that occupies only y0 .. y1: a body above its top walks over it, a body below its base
# passes under, and col_top_at reports it as something to stand on
function col_add_h(x: int, z: int, r: int, y0: int, y1: int) -> void {
function col_add_h(x: float, z: float, r: float, y0: float, y1: float) -> void {
if col_x == null {
col_x = words(COL_CAP); col_z = words(COL_CAP); col_r = words(COL_CAP)
col_y0 = words(COL_CAP); col_y1 = words(COL_CAP); col_out = words(2)
col_x = floats(COL_CAP); col_z = floats(COL_CAP); col_r = floats(COL_CAP)
col_y0 = floats(COL_CAP); col_y1 = floats(COL_CAP); col_out = floats(2)
}
if col_n >= COL_CAP { return }
col_x[col_n] = x; col_z[col_n] = z; col_r[col_n] = r
@ -41,8 +41,8 @@ function col_add_h(x: int, z: int, r: int, y0: int, y1: int) -> void {
col_n += 1
col_built = false
}
function col_cell_of(v: int, origin: int) -> int {
var c = f_to_int(f_floor(f_div(f_add(f_sub(v, origin), fi(TERRAIN_HALF)), fi(COL_CELL))))
function col_cell_of(v: float, origin: float) -> int {
var c = int(Math.floor((v - origin + float(TERRAIN_HALF)) / float(COL_CELL)))
if c < 0 { c = 0 }
if c > col_side - 1 { c = col_side - 1 }
return c
@ -67,10 +67,10 @@ function col_build() -> void {
}
# push (px, pz) with radius pr out of every circle it overlaps; the result is in col_out
function col_resolve(px: int, pz: int, pr: int) -> bool { return col_resolve_at(px, pz, pr, COL_LOW, COL_HIGH) }
function col_resolve(px: float, pz: float, pr: float) -> bool { return col_resolve_at(px, pz, pr, COL_LOW, COL_HIGH) }
# the same, for a body that occupies feet .. head: a collider whose span misses that is not in the
# way at all. This is what lets a hiker stand on top of a boulder rather than inside it.
function col_resolve_at(px: int, pz: int, pr: int, feet: int, head: int) -> bool {
function col_resolve_at(px: float, pz: float, pr: float, feet: float, head: float) -> bool {
col_out[0] = px; col_out[1] = pz
if not col_built or col_n == 0 { return false }
var x = px; var z = pz
@ -86,14 +86,14 @@ function col_resolve_at(px: int, pz: int, pr: int, feet: int, head: int) -> bool
let c = zc * col_side + xc
for k in col_start[c] .. col_start[c + 1] {
let i = col_sorted[k]
let ex = f_sub(x, col_x[i]); let ez = f_sub(z, col_z[i])
let d2 = f_add(f_mul(ex, ex), f_mul(ez, ez))
let rr = f_add(col_r[i], pr)
let ex = x - col_x[i]; let ez = z - col_z[i]
let d2 = ex * ex + ez * ez
let rr = col_r[i] + pr
# nothing to push out of if the body is wholly above its top or below its base
if not f_ls(feet, col_y1[i]) { continue }
if not f_gt(head, col_y0[i]) { continue }
if f_ls(d2, f_mul(rr, rr)) {
let d = f_sqrt(d2)
if not (feet < col_y1[i]) { continue }
if not (head > col_y0[i]) { continue }
if d2 < rr * rr {
let d = Math.sqrt(d2)
# The UNIT normal out of this circle. (ex, ez) / d is always unit for d > 0,
# because d is its own length - there is nothing to clamp and nothing that can
# grow. Exactly at the centre there is no direction to be had, so any one will
@ -105,11 +105,11 @@ function col_resolve_at(px: int, pz: int, pr: int, feet: int, head: int) -> bool
# 0.5 m trunk was thrown roughly 800 m across the map instead of 0.85 m clear
# of it. Off-centre - which is how anything actually arrives at a trunk - the
# arithmetic was right, so it never showed up in play.
var ux = F_ONE; var uz = F_ZERO
if f_gt(d, F_ZERO) { ux = f_div(ex, d); uz = f_div(ez, d) }
let push = f_sub(rr, d)
x = f_add(x, f_mul(ux, push))
z = f_add(z, f_mul(uz, push))
var ux = 1.0; var uz = 0.0
if d > 0.0 { ux = ex / d; uz = ez / d }
let push = rr - d
x = x + ux * push
z = z + uz * push
moved = true
}
}
@ -122,11 +122,11 @@ function col_resolve_at(px: int, pz: int, pr: int, feet: int, head: int) -> bool
# The highest collider top under (px, pz) that a body at `feet` could be standing on or step up to:
# tops above `reach` are a wall, not a step. F_ZERO-safe: returns `floor` when there is nothing, so
# a caller can pass the terrain height and use the answer directly as the ground.
function col_top_at(px: int, pz: int, pr: int, feet: int, reach: int, floor: int) -> int {
function col_top_at(px: float, pz: float, pr: float, feet: float, reach: float, floor: float) -> float {
var top = floor
if not col_built or col_n == 0 { return top }
let cx = col_cell_of(px, ter_ox); let cz = col_cell_of(pz, ter_oz)
let limit = f_add(feet, reach)
let limit = feet + reach
for dz in 0 .. 3 {
let zc = cz + dz - 1
if zc < 0 or zc >= col_side { continue }
@ -136,24 +136,24 @@ function col_top_at(px: int, pz: int, pr: int, feet: int, reach: int, floor: int
let c = zc * col_side + xc
for k in col_start[c] .. col_start[c + 1] {
let i = col_sorted[k]
let ex = f_sub(px, col_x[i]); let ez = f_sub(pz, col_z[i])
let d2 = f_add(f_mul(ex, ex), f_mul(ez, ez))
let rr = f_add(col_r[i], pr)
if not f_ls(d2, f_mul(rr, rr)) { continue }
let ex = px - col_x[i]; let ez = pz - col_z[i]
let d2 = ex * ex + ez * ez
let rr = col_r[i] + pr
if not (d2 < rr * rr) { continue }
let t = col_y1[i]
if f_gt(t, limit) { continue } # too tall to step onto: it is a wall
if f_gt(t, top) { top = t }
if t > limit { continue } # too tall to step onto: it is a wall
if t > top { top = t }
}
}
}
return top
}
# is the segment from (x0,z0) to (x1,z1) clear of every circle (a camera line of sight)?
function col_clear(x0: int, z0: int, x1: int, z1: int, r: int) -> bool {
function col_clear(x0: float, z0: float, x1: float, z1: float, r: float) -> bool {
let steps = 6
for s in 0 .. steps + 1 {
let t = fr(s, steps)
let x = f_lerp(x0, x1, t); let z = f_lerp(z0, z1, t)
let t = float(s) / float(steps)
let x = Math.lerp(x0, x1, t); let z = Math.lerp(z0, z1, t)
if col_resolve(x, z, r) { return false }
}
return true

View file

@ -10,139 +10,139 @@
# ============================================================================
var day_on: bool = false
var day_hours: int = 0 # float bits, 0 .. 24
var day_light: int = 0 # 0 night .. 1 full day (float bits)
var day_ibl: words = null # rgb scale on the sky's light
var day_sun_base: words = null # the HDRI's sun radiance, kept
var day_az0: int = 0 # the sun's azimuth at the reference hour (radians, yaw convention)
var day_yaw0: int = 0 # the sky yaw the scene was tuned at
var day_hour0: int = 0 # the hour the photograph was taken (10.5)
var day_dir: int = 0 # +1 / -1: which way the sun travels in yaw
var day_az: int = 0
var day_el: int = 0
var day_hours: float = 0.0 # float bits, 0 .. 24
var day_light: float = 0.0 # 0 night .. 1 full day (float bits)
var day_ibl: floats = null # rgb scale on the sky's light
var day_sun_base: floats = null # the HDRI's sun radiance, kept
var day_az0: float = 0.0 # the sun's azimuth at the reference hour (radians, yaw convention)
var day_yaw0: float = 0.0 # the sky yaw the scene was tuned at
var day_hour0: float = 0.0 # the hour the photograph was taken (10.5)
var day_dir: float = 0.0 # +1 / -1: which way the sun travels in yaw
var day_az: float = 0.0
var day_el: float = 0.0
var day_gen: int = 0 # bumps when the light moved enough to rebake the terrain shadow
var day_baked_az: int = 0
var day_baked_el: int = 0
var day_sky_baked: int = 0 # the sky yaw the convolutions were baked at
var fire_pos: words = null
var fire_color: words = null
var day_baked_az: float = 0.0
var day_baked_el: float = 0.0
var day_sky_baked: float = 0.0 # the sky yaw the convolutions were baked at
var fire_pos: floats = null
var fire_color: floats = null
var day_moon: bool = false
# The moon's place in its month, 0 new .. 0.5 full .. 1 new again. It decides the disc's
# terminator, how much light reaches the ground, and where in the sky it rides: a full
# moon is opposite the sun and rises at sunset, a new one travels with it.
var day_moon_phase: int = 0x3F000000 # 0.5: full, which is where the game used to be
var day_moon_illum: int = 0x3F800000 # the lit fraction, derived from the phase
var day_moon_dir: words = null # toward the moon, whether or not it is up
var hand_pos: words = null
var hand_color: words = null
var hand_dir: words = null
var hand_cone: int = 0
var hand_reach: int = 0 # metres the hand light reaches (float bits)
var day_overcast: int = 0 # 0 clear .. 1 a low grey sky (float bits): dims the sun and the sky's light
var day_flash: int = 0 # a lightning flash this frame (0..1): the sun brightens for it
var day_fog_mul: int = 0x3F800000 # multiplies the base fog density (rain and snow thicken the air)
var day_fog_base: int = 0
var day_moon_phase: float = 0.5 # 0.5: full, which is where the game used to be
var day_moon_illum: float = 1.0 # the lit fraction, derived from the phase
var day_moon_dir: floats = null # toward the moon, whether or not it is up
var hand_pos: floats = null
var hand_color: floats = null
var hand_dir: floats = null
var hand_cone: float = 0.0
var hand_reach: float = 0.0 # metres the hand light reaches (float bits)
var day_overcast: float = 0.0 # 0 clear .. 1 a low grey sky (float bits): dims the sun and the sky's light
var day_flash: float = 0.0 # a lightning flash this frame (0..1): the sun brightens for it
var day_fog_mul: float = 1.0 # multiplies the base fog density (rain and snow thicken the air)
var day_fog_base: float = 0.0
function daylight_init() -> void {
day_ibl = v3_new(F_ONE, F_ONE, F_ONE)
day_ibl = v3_new(1.0, 1.0, 1.0)
day_sun_base = v3_new(sun_color[0], sun_color[1], sun_color[2])
fire_pos = v3_new(F_ZERO, fi(-1000), F_ZERO)
fire_color = v3_new(F_ZERO, F_ZERO, F_ZERO)
day_moon_dir = v3_new(F_ZERO, F_ONE, F_ZERO)
hand_pos = v3_new(F_ZERO, fi(-1000), F_ZERO); hand_color = v3_new(F_ZERO, F_ZERO, F_ZERO); hand_dir = v3_new(F_ZERO, F_ZERO, f_neg(F_ONE)); hand_cone = f_neg(F_TWO); hand_reach = fi(12)
day_light = F_ONE
fire_pos = v3_new(0.0, -1000.0, 0.0)
fire_color = v3_new(0.0, 0.0, 0.0)
day_moon_dir = v3_new(0.0, 1.0, 0.0)
hand_pos = v3_new(0.0, -1000.0, 0.0); hand_color = v3_new(0.0, 0.0, 0.0); hand_dir = v3_new(0.0, 0.0, -1.0); hand_cone = -2.0; hand_reach = 12.0
day_light = 1.0
}
# Start the clock: the scene as tuned (sky yaw `yaw0`) is the photograph's hour `hour0`;
# the sun rises and sets toward `set_yaw` (the direction it should be in at 19:00).
function daylight_start(yaw0: int, hour0: int, set_yaw: int) -> void {
function daylight_start(yaw0: float, hour0: float, set_yaw: float) -> void {
day_yaw0 = yaw0; day_hour0 = hour0
day_az0 = f_atan2(f_neg(sun_dir[0]), f_neg(sun_dir[2]))
day_az0 = Math.atan2(-sun_dir[0], -sun_dir[2])
day_sky_baked = yaw0
# which way does the sun travel? the way that puts it nearest `set_yaw` at 19:00
let step = f_mul(f_sub(fl(19.0), hour0), f_rad(fi(15)))
let da = f_abs(day_wrap(f_sub(f_add(day_az0, step), set_yaw)))
let db = f_abs(day_wrap(f_sub(f_sub(day_az0, step), set_yaw)))
day_dir = F_ONE
if f_ls(db, da) { day_dir = f_neg(F_ONE) }
let step = (19.0 - hour0) * Math.deg_to_rad(15.0)
let da = Math.abs(day_wrap(day_az0 + step - set_yaw))
let db = Math.abs(day_wrap(day_az0 - step - set_yaw))
day_dir = 1.0
if db < da { day_dir = -1.0 }
day_on = true
day_baked_az = fi(1000)
day_baked_az = 1000.0
daylight_set(hour0)
}
function day_wrap(a: int) -> int {
let two_pi = f_mul(F_TWO, F_PI)
function day_wrap(a: float) -> float {
let two_pi = 2.0 * PI
var d = a
while f_gt(d, F_PI) { d = f_sub(d, two_pi) }
while f_ls(d, f_neg(F_PI)) { d = f_add(d, two_pi) }
while d > PI { d = d - two_pi }
while d < -PI { d = d + two_pi }
return d
}
function smoothf(a: int, b: int, x: int) -> int {
let t = f_clamp(f_div(f_sub(x, a), f_sub(b, a)), F_ZERO, F_ONE)
return f_mul(f_mul(t, t), f_sub(fi(3), f_mul(F_TWO, t)))
function smoothf(a: float, b: float, x: float) -> float {
let t = Math.clamp((x - a) / (b - a), 0.0, 1.0)
return t * t * (3.0 - 2.0 * t)
}
function daylight_set(hours: int) -> void {
var h = f_mod(hours, fi(24))
if f_ls(h, F_ZERO) { h = f_add(h, fi(24)) }
function daylight_set(hours: float) -> void {
var h = hours % 24.0
if h < 0.0 { h = h + 24.0 }
day_hours = h
# the arc: up at 5:30, highest (about 57 degrees) at 12:45, down at 20:00
let t = f_mul(f_div(f_sub(h, fl(5.5)), fl(14.5)), F_PI)
let el = f_rad(f_add(fi(-6), f_mul(fi(63), f_sin(t))))
let az = f_add(day_az0, f_mul(f_mul(f_sub(h, day_hour0), f_rad(fi(15))), day_dir))
let t = (h - 5.5) / 14.5 * PI
let el = Math.deg_to_rad(-6.0 + 63.0 * Math.sin(t))
let az = day_az0 + (h - day_hour0) * Math.deg_to_rad(15.0) * day_dir
day_az = az; day_el = el
let d = smoothf(f_rad(fi(-8)), f_rad(fi(12)), el)
let d = smoothf(Math.deg_to_rad(-8.0), Math.deg_to_rad(12.0), el)
day_light = d
# the sun, warm and dim near the horizon; past dusk, the moon from across the sky
var laz = az; var lel = f_max(el, f_rad(fi(3)))
var warm_r = F_ONE; var warm_g = F_ONE; var warm_b = F_ONE
let low = smoothf(F_ZERO, f_rad(fi(24)), el)
warm_g = f_lerp(fl(0.55), F_ONE, low); warm_b = f_lerp(fl(0.28), F_ONE, low)
var laz = az; var lel = Math.max(el, Math.deg_to_rad(3.0))
var warm_r = 1.0; var warm_g = 1.0; var warm_b = 1.0
let low = smoothf(0.0, Math.deg_to_rad(24.0), el)
warm_g = Math.lerp(0.55, 1.0, low); warm_b = Math.lerp(0.28, 1.0, low)
# an overcast sky: the sun goes diffuse and grey, a lightning flash brings it back white
let oc = f_clamp(day_overcast, F_ZERO, F_ONE)
let sunk = f_add(f_sub(F_ONE, f_mul(fl(0.92), oc)), f_mul(fl(2.5), day_flash))
var sr = f_mul(day_sun_base[0], f_mul(f_mul(d, warm_r), sunk))
var sg = f_mul(day_sun_base[1], f_mul(f_mul(d, warm_g), sunk))
var sb = f_mul(day_sun_base[2], f_mul(f_mul(d, warm_b), sunk))
let oc = Math.clamp(day_overcast, 0.0, 1.0)
let sunk = 1.0 - 0.92 * oc + 2.5 * day_flash
var sr = day_sun_base[0] * (d * warm_r * sunk)
var sg = day_sun_base[1] * (d * warm_g * sunk)
var sb = day_sun_base[2] * (d * warm_b * sunk)
# The moon rides a lag behind the sun that is its phase: full is opposite (half a turn),
# new is alongside. Its elevation follows the same arc, offset by the same amount, so a
# full moon rises as the sun sets and a new moon is up all day and invisible.
# The moon is where the sun was `lag` of a day ago: at full that is half a day, so it
# rises as the sun sets; at new it is alongside the sun and up all day, invisible. The
# sign matters — a waxing crescent has to set AFTER the sun, not before it.
let moon_lag = f_mul(f_mul(F_TWO, F_PI), day_moon_phase)
let maz = f_sub(az, f_mul(moon_lag, day_dir))
let mel = f_rad(f_add(fi(-6), f_mul(fi(63), f_sin(f_sub(t, moon_lag)))))
let mce = f_cos(mel)
v3_set(day_moon_dir, f_neg(f_mul(f_sin(maz), mce)), f_sin(mel), f_neg(f_mul(f_cos(maz), mce)))
let moon_lag = 2.0 * PI * day_moon_phase
let maz = az - moon_lag * day_dir
let mel = Math.deg_to_rad(-6.0 + 63.0 * Math.sin(t - moon_lag))
let mce = Math.cos(mel)
v3_set(day_moon_dir, -(Math.sin(maz) * mce), Math.sin(mel), -(Math.cos(maz) * mce))
day_moon = false
if f_ls(el, f_rad(fi(-7))) {
if el < Math.deg_to_rad(-7.0) {
day_moon = true
laz = maz
lel = f_clamp(mel, f_rad(fi(6)), f_rad(fi(70)))
let m = smoothf(f_rad(fi(-7)), f_rad(fi(-16)), el)
lel = Math.clamp(mel, Math.deg_to_rad(6.0), Math.deg_to_rad(70.0))
let m = smoothf(Math.deg_to_rad(-7.0), Math.deg_to_rad(-16.0), el)
# what the moon is worth on the ground, by how much of it is lit. A new moon is a
# properly dark night, which is what makes a torch and a lantern matter.
let up = smoothf(f_rad(fi(-4)), f_rad(fi(8)), mel)
let lit = f_mul(f_mul(m, up), f_add(fl(0.06), f_mul(fl(0.94), day_moon_illum)))
sr = f_mul(day_sun_base[0], f_mul(fl(0.0130), lit))
sg = f_mul(day_sun_base[1], f_mul(fl(0.0165), lit))
sb = f_mul(day_sun_base[2], f_mul(fl(0.0250), lit))
let up = smoothf(Math.deg_to_rad(-4.0), Math.deg_to_rad(8.0), mel)
let lit = m * up * (0.06 + 0.94 * day_moon_illum)
sr = day_sun_base[0] * (0.0130 * lit)
sg = day_sun_base[1] * (0.0165 * lit)
sb = day_sun_base[2] * (0.0250 * lit)
}
let ce = f_cos(lel)
v3_set(sun_dir, f_neg(f_mul(f_sin(laz), ce)), f_sin(lel), f_neg(f_mul(f_cos(laz), ce)))
let ce = Math.cos(lel)
v3_set(sun_dir, -(Math.sin(laz) * ce), Math.sin(lel), -(Math.cos(laz) * ce))
v3_set(sun_color, sr, sg, sb)
# the sky's light: full by day, a deep blue by night, amber through the dusk
let dusk = f_mul(smoothf(f_rad(fi(-10)), f_rad(fi(2)), el), f_sub(F_ONE, smoothf(f_rad(fi(2)), f_rad(fi(18)), el)))
let dusk = smoothf(Math.deg_to_rad(-10.0), Math.deg_to_rad(2.0), el) * (1.0 - smoothf(Math.deg_to_rad(2.0), Math.deg_to_rad(18.0), el))
# a full moon lifts the night's own ambient nearly threefold; a new moon leaves it alone
var moonlit = F_ONE
if day_moon { moonlit = f_add(F_ONE, f_mul(fl(1.8), day_moon_illum)) }
v3_set(day_ibl, f_lerp(f_mul(fl(0.020), moonlit), F_ONE, d), f_lerp(f_mul(fl(0.026), moonlit), F_ONE, d), f_lerp(f_mul(fl(0.045), moonlit), F_ONE, d))
day_ibl[0] = f_mul(day_ibl[0], f_add(F_ONE, f_mul(fl(0.35), dusk)))
day_ibl[2] = f_mul(day_ibl[2], f_sub(F_ONE, f_mul(fl(0.25), dusk)))
var moonlit = 1.0
if day_moon { moonlit = 1.0 + 1.8 * day_moon_illum }
v3_set(day_ibl, Math.lerp(0.020 * moonlit, 1.0, d), Math.lerp(0.026 * moonlit, 1.0, d), Math.lerp(0.045 * moonlit, 1.0, d))
day_ibl[0] = day_ibl[0] * (1.0 + 0.35 * dusk)
day_ibl[2] = day_ibl[2] * (1.0 - 0.25 * dusk)
# clouds: less light, and greyer (the blue and the warmth both fade)
let grey = f_add(f_mul(f_add(day_ibl[0], f_add(day_ibl[1], day_ibl[2])), fl(0.3333)), f_mul(fl(0.6), day_flash))
let dim = f_sub(F_ONE, f_mul(fl(0.55), oc))
for i in 0 .. 3 { day_ibl[i] = f_mul(f_lerp(day_ibl[i], grey, f_mul(fl(0.7), oc)), dim) }
let grey = (day_ibl[0] + (day_ibl[1] + day_ibl[2])) * 0.3333 + 0.6 * day_flash
let dim = 1.0 - 0.55 * oc
for i in 0 .. 3 { day_ibl[i] = Math.lerp(day_ibl[i], grey, 0.7 * oc) * dim }
# ---- the air ---------------------------------------------------------------------------
# Aerial perspective is a CURVE, not the one constant it was. A low sun is shining through
# far more air than a high one, and it is shining ALONG the ground rather than down onto it,
@ -152,25 +152,25 @@ function daylight_set(hours: int) -> void {
# gets a dawn's haze with no dawn to justify it. Overcast thickens the air and flattens the
# scatter, because a grey sky has no disc to scatter from. The player's slider still
# multiplies the result, so nobody loses the setting they chose.
if day_fog_base == 0 { day_fog_base = r3d_fog_density }
let fog_rise = smoothf(f_rad(fi(-12)), f_rad(fi(1)), el)
let fog_high = smoothf(f_rad(fi(2)), f_rad(fi(26)), el)
let lowsun = f_mul(fog_rise, f_sub(F_ONE, fog_high))
var dens = f_mul(day_fog_base, f_add(F_ONE, f_mul(fl(1.5), lowsun)))
dens = f_mul(dens, f_add(F_ONE, f_mul(fl(1.2), oc)))
r3d_fog_density = f_mul(f_mul(dens, day_fog_mul), r3d_fog_scale)
if day_fog_base == 0.0 { day_fog_base = r3d_fog_density }
let fog_rise = smoothf(Math.deg_to_rad(-12.0), Math.deg_to_rad(1.0), el)
let fog_high = smoothf(Math.deg_to_rad(2.0), Math.deg_to_rad(26.0), el)
let lowsun = fog_rise * (1.0 - fog_high)
var dens = day_fog_base * (1.0 + 1.5 * lowsun)
dens = dens * (1.0 + 1.2 * oc)
r3d_fog_density = dens * day_fog_mul * r3d_fog_scale
# how fast the haze thins with height: a settled morning's lies IN the valley, a noon sky is
# thin all the way up, so the falloff rises as the sun drops
r3d_fog_falloff = f_mul(fl(0.002), f_add(F_ONE, f_mul(fl(1.6), lowsun)))
r3d_fog_falloff = 0.002 * (1.0 + 1.6 * lowsun)
# the glow a ridge is silhouetted against when you look into a low sun
r3d_fog_inscatter = f_mul(f_add(fl(0.012), f_mul(fl(0.16), lowsun)), f_sub(F_ONE, f_mul(fl(0.7), oc)))
r3d_fog_inscatter = (0.012 + 0.16 * lowsun) * (1.0 - 0.7 * oc)
# and distance takes colour away at every hour, harder under cloud
# 1.9 was tuned on a valley whose rock was grey anyway. Maroon's air is thin, dry and at
# 2900 m, and the Bells are only a couple of kilometres from the lake - in the photograph
# everybody knows, they are still plainly RED at that distance. Desaturating them to a pale
# grey-pink is physically defensible and loses the one thing the range is named for, so the
# basin's own air gets a gentler figure and overcast still takes colour away faster.
r3d_fog_desat = f_add(fl(1.15), f_mul(fl(0.7), oc))
r3d_fog_desat = 1.15 + 0.7 * oc
# ---- the grade -------------------------------------------------------------------------
# The look of an hour is not only how much air is in front of the mountain; it is what colour
# the light is and what the shadows are filled with. Noon is the case worth naming: direct
@ -181,31 +181,31 @@ function daylight_set(hours: int) -> void {
# `night` is deliberately NOT `1 - d`: d is already falling while the sun is still on the
# horizon, so the two curves would fight and the warmest minute of the day would come out
# half-cooled. It only begins under the horizon.
let night = f_sub(F_ONE, smoothf(f_rad(fi(-14)), f_rad(fi(-4)), el))
let night = 1.0 - smoothf(Math.deg_to_rad(-14.0), Math.deg_to_rad(-4.0), el)
let hi = fog_high
post_wb_r = f_add(fl(1.02), f_sub(f_mul(fl(0.10), lowsun), f_add(f_mul(fl(0.03), hi), f_mul(fl(0.06), night))))
post_wb_g = f_add(F_ONE, f_mul(fl(0.012), lowsun))
post_wb_b = f_add(fl(0.97), f_sub(f_add(f_mul(fl(0.055), hi), f_mul(fl(0.13), night)), f_mul(fl(0.09), lowsun)))
post_wb_r = 1.02 + (0.10 * lowsun - (0.03 * hi + 0.06 * night))
post_wb_g = 1.0 + 0.012 * lowsun
post_wb_b = 0.97 + (0.055 * hi + 0.13 * night - 0.09 * lowsun)
# the shadows' floor: warm and open into a low sun, blue at noon, cold and crushed at night
post_lift_r = f_sub(f_add(fl(0.004), f_mul(fl(0.013), lowsun)), f_mul(fl(0.002), f_add(hi, night)))
post_lift_g = f_sub(f_add(fl(0.004), f_mul(fl(0.008), lowsun)), f_mul(fl(0.001), night))
post_lift_b = f_add(fl(0.012), f_add(f_mul(fl(0.004), lowsun), f_mul(fl(0.013), hi)))
post_lift_r = 0.004 + 0.013 * lowsun - 0.002 * (hi + night)
post_lift_g = 0.004 + 0.008 * lowsun - 0.001 * night
post_lift_b = 0.012 + (0.004 * lowsun + 0.013 * hi)
# Gain is left alone on purpose. It looks like a highlight control and is not: the grade is
# `c * gain + lift * (1 - c)`, so warming the gain warms the WHOLE frame, and warming it at
# noon undid the cool white balance above and turned one o'clock yellower than seven in the
# morning - the opposite of the thing this grade exists to do. Midday's punch comes from
# contrast, and midday's colour from a cool balance over a blue shadow.
post_gain_r = fl(0.99)
post_gain_g = fl(0.995)
post_gain_b = F_ONE
post_contrast = f_sub(f_add(fl(1.12), f_mul(fl(0.11), hi)), f_add(f_mul(fl(0.17), oc), f_mul(fl(0.06), night)))
post_saturation = f_sub(f_add(fl(1.04), f_mul(fl(0.12), lowsun)), f_add(f_mul(fl(0.20), oc), f_mul(fl(0.12), night)))
post_gain_r = 0.99
post_gain_g = 0.995
post_gain_b = 1.0
post_contrast = 1.12 + 0.11 * hi - (0.17 * oc + 0.06 * night)
post_saturation = 1.04 + 0.12 * lowsun - (0.20 * oc + 0.12 * night)
# The visible sky is relit by the hour (sky.frag). It fades out under the horizon, where the
# night's own tint, stars and moon take over, and it eases off under heavy cloud - a relit
# overcast is still overcast, and driving a clear-sky model hard through one paints a blue
# zenith onto a grey day.
r3d_sky_relight = f_mul(smoothf(f_rad(fi(-10)), f_rad(fi(1)), el), f_sub(F_ONE, f_mul(fl(0.75), oc)))
r3d_sky_relight = smoothf(Math.deg_to_rad(-10.0), Math.deg_to_rad(1.0), el) * (1.0 - 0.75 * oc)
# ---- what is IN the air -------------------------------------------------------------
# The volumetric march (post.ludic) needs the same story the analytic fog tells, or the
@ -214,64 +214,64 @@ function daylight_set(hours: int) -> void {
# the cold at either end of the day, lies ON the ground rather than filling the basin, and
# burns off by mid-morning. Cloud thickens the air and kills the shafts, because a shaft
# needs a disc to come from.
post_vol_density = f_mul(f_add(fl(0.00030), f_mul(fl(0.00070), lowsun)), f_sub(F_ONE, f_mul(fl(0.55), oc)))
post_vol_mist = f_mul(f_mul(fl(0.40), lowsun), f_sub(F_ONE, f_mul(fl(0.4), oc)))
post_vol_falloff = f_add(fl(0.006), f_mul(fl(0.004), lowsun))
post_vol_density = (0.00030 + 0.00070 * lowsun) * (1.0 - 0.55 * oc)
post_vol_mist = 0.40 * lowsun * (1.0 - 0.4 * oc)
post_vol_falloff = 0.006 + 0.004 * lowsun
# R3D_NOAIR=1: the air as it was before any of the above - one density, one falloff, the
# inscatter the shader used to hard-code, and no distance desaturation at all. It is here
# so a before-and-after can be shot from ONE binary at one hour, which is the only kind of
# comparison worth looking at, and it joins R3D_NOCLOUD / R3D_NOSHADOW / R3D_NOGI.
if r3d_env_has("R3D_NOAIR") {
r3d_fog_density = f_mul(f_mul(day_fog_base, day_fog_mul), r3d_fog_scale)
r3d_fog_falloff = fl(0.002)
r3d_fog_inscatter = fl(0.02)
r3d_fog_desat = F_ZERO
post_wb_r = fl(1.02); post_wb_g = F_ONE; post_wb_b = fl(0.97)
post_lift_r = fl(0.004); post_lift_g = fl(0.004); post_lift_b = fl(0.012)
post_gain_r = fl(0.99); post_gain_g = fl(0.995); post_gain_b = F_ONE
post_contrast = fl(1.12); post_saturation = fl(1.04)
r3d_sky_relight = F_ZERO
post_vol_density = F_ZERO; post_vol_mist = F_ZERO
r3d_fog_density = day_fog_base * day_fog_mul * r3d_fog_scale
r3d_fog_falloff = 0.002
r3d_fog_inscatter = 0.02
r3d_fog_desat = 0.0
post_wb_r = 1.02; post_wb_g = 1.0; post_wb_b = 0.97
post_lift_r = 0.004; post_lift_g = 0.004; post_lift_b = 0.012
post_gain_r = 0.99; post_gain_g = 0.995; post_gain_b = 1.0
post_contrast = 1.12; post_saturation = 1.04
r3d_sky_relight = 0.0
post_vol_density = 0.0; post_vol_mist = 0.0
}
# exposure: auto-exposure must not turn the night into day
# the ceiling has to move with the moon or auto-exposure eats the difference between a
# full-moon night and a new-moon one
var night_max = fl(4.5)
if day_moon { night_max = f_add(fl(4.5), f_mul(fl(3.5), day_moon_illum)) }
post_exposure_max = f_lerp(night_max, fi(20), d)
var night_max = 4.5
if day_moon { night_max = 4.5 + 3.5 * day_moon_illum }
post_exposure_max = Math.lerp(night_max, 20.0, d)
# the visible sky turns with the sun (cheap); its convolutions rebake when far off
let sky_yaw_now = f_add(day_yaw0, f_mul(f_mul(f_sub(h, day_hour0), f_rad(fi(15))), day_dir))
let sky_yaw_now = day_yaw0 + (h - day_hour0) * Math.deg_to_rad(15.0) * day_dir
sky_set_rot(sky_yaw_now)
if f_gt(f_abs(day_wrap(f_sub(sky_yaw_now, day_sky_baked))), f_rad(fi(35))) and f_gt(d, fl(0.05)) {
if Math.abs(day_wrap(sky_yaw_now - day_sky_baked)) > Math.deg_to_rad(35.0) and d > 0.05 {
day_sky_baked = sky_yaw_now
sky_precompute()
}
# the terrain's baked shadow follows the light in steps
if f_gt(f_abs(day_wrap(f_sub(laz, day_baked_az))), f_rad(fi(4))) or f_gt(f_abs(f_sub(lel, day_baked_el)), f_rad(fi(3))) {
if Math.abs(day_wrap(laz - day_baked_az)) > Math.deg_to_rad(4.0) or Math.abs(lel - day_baked_el) > Math.deg_to_rad(3.0) {
day_baked_az = laz; day_baked_el = lel
day_gen += 1
}
}
# where the moon is in its month; the game advances this each morning
function daylight_moon(phase: int) -> void {
var p = f_mod(phase, F_ONE)
if f_ls(p, F_ZERO) { p = f_add(p, F_ONE) }
function daylight_moon(phase: float) -> void {
var p = phase % 1.0
if p < 0.0 { p = p + 1.0 }
day_moon_phase = p
# illuminated fraction: (1 - cos(2 pi p)) / 2, which is 0 at new and 1 at full
day_moon_illum = f_mul(f_sub(F_ONE, f_cos(f_mul(f_mul(F_TWO, F_PI), p))), F_HALF)
day_moon_illum = (1.0 - Math.cos(2.0 * PI * p)) * 0.5
daylight_set(day_hours)
}
# the weather over the valley: overcast 0..1, a fog multiplier, a lightning flash 0..1
function daylight_weather(overcast: int, fog_mul: int, flash: int) -> void {
function daylight_weather(overcast: float, fog_mul: float, flash: float) -> void {
day_overcast = overcast; day_fog_mul = fog_mul; day_flash = flash
}
# the campfire: a point light at (x, y, z) of `strength` (0 = out)
function daylight_fire(x: int, y: int, z: int, strength: int) -> void {
function daylight_fire(x: float, y: float, z: float, strength: float) -> void {
v3_set(fire_pos, x, y, z)
v3_set(fire_color, f_mul(fl(9.0), strength), f_mul(fl(4.6), strength), f_mul(fl(1.4), strength))
v3_set(fire_color, 9.0 * strength, 4.6 * strength, 1.4 * strength)
}
# the light in the hand: a point light (cone < -1) or a cone along dir (cone = cos half-angle)
@ -280,7 +280,7 @@ function daylight_fire(x: int, y: int, z: int, strength: int) -> void {
# is what a pool of firelight looks like. It used to be a windowed inverse square with the
# window and the scale both hard-coded, so every hand light in every game had the reach of
# a candle whatever it was meant to be.
function daylight_hand(x: int, y: int, z: int, dx: int, dy: int, dz: int, cone: int, reach: int, r: int, g: int, b: int) -> void {
function daylight_hand(x: float, y: float, z: float, dx: float, dy: float, dz: float, cone: float, reach: float, r: float, g: float, b: float) -> void {
v3_set(hand_pos, x, y, z); v3_set(hand_dir, dx, dy, dz); hand_cone = cone
v3_set(hand_color, r, g, b)
hand_reach = reach

View file

@ -16,59 +16,60 @@ const F_ZERO: int = 0x00000000
const F_ONE: int = 0x3F800000
const F_TWO: int = 0x40000000
const F_HALF: int = 0x3F000000
const PI: float = 3.1415927
const F_PI: int = 0x40490FDB
function fl(x: fixed) -> int { return fx_to_f32(x) }
function fi(n: int) -> int { return f_from_int(n) }
function fr(n: int, d: int) -> int { return f_div(f_from_int(n), f_from_int(d)) }
function f_neg1() -> int { return f_neg(F_ONE) }
function f_clamp(x: int, lo: int, hi: int) -> int { return f_min(f_max(x, lo), hi) }
function f_lerp(a: int, b: int, t: int) -> int { return f_add(a, f_mul(f_sub(b, a), t)) }
function f_gt(a: int, b: int) -> bool { return f_lt(b, a) != 0 }
function f_ls(a: int, b: int) -> bool { return f_lt(a, b) != 0 }
function f_rad(deg: int) -> int { return f_mul(deg, f_div(F_PI, fi(180))) }
function f_fx(x: int) -> fixed { return f32_to_fx(x) }
function fl(x: fixed) -> float { return float(x) }
function fi(n: int) -> float { return float(n) }
function fr(n: int, d: int) -> float { return float(n) / float(d) }
function f_neg1() -> float { return -1.0 }
function f_clamp(x: float, lo: float, hi: float) -> float { return Math.min(Math.max(x, lo), hi) }
function f_lerp(a: float, b: float, t: float) -> float { return a + (b - a) * t }
function f_gt(a: float, b: float) -> bool { return f_lt(b, a) != 0 }
function f_ls(a: float, b: float) -> bool { return f_lt(a, b) != 0 }
function f_rad(deg: float) -> float { return deg * (PI / 180.0) }
function f_fx(x: float) -> fixed { return fixed(x) }
# ---- vectors ----------------------------------------------------------------
function v3_new(x: int, y: int, z: int) -> words {
let v = words(3)
function v3_new(x: float, y: float, z: float) -> floats {
let v = floats(3)
v[0] = x
v[1] = y
v[2] = z
return v
}
function v3_set(v: words, x: int, y: int, z: int) -> void { v[0] = x; v[1] = y; v[2] = z }
function v3_copy(o: words, a: words) -> void { o[0] = a[0]; o[1] = a[1]; o[2] = a[2] }
function v3_add(o: words, a: words, b: words) -> void {
o[0] = f_add(a[0], b[0]); o[1] = f_add(a[1], b[1]); o[2] = f_add(a[2], b[2])
function v3_set(v: floats, x: float, y: float, z: float) -> void { v[0] = x; v[1] = y; v[2] = z }
function v3_copy(o: floats, a: floats) -> void { o[0] = a[0]; o[1] = a[1]; o[2] = a[2] }
function v3_add(o: floats, a: floats, b: floats) -> void {
o[0] = a[0] + b[0]; o[1] = a[1] + b[1]; o[2] = a[2] + b[2]
}
function v3_sub(o: words, a: words, b: words) -> void {
o[0] = f_sub(a[0], b[0]); o[1] = f_sub(a[1], b[1]); o[2] = f_sub(a[2], b[2])
function v3_sub(o: floats, a: floats, b: floats) -> void {
o[0] = a[0] - b[0]; o[1] = a[1] - b[1]; o[2] = a[2] - b[2]
}
function v3_scale(o: words, a: words, s: int) -> void {
o[0] = f_mul(a[0], s); o[1] = f_mul(a[1], s); o[2] = f_mul(a[2], s)
function v3_scale(o: floats, a: floats, s: float) -> void {
o[0] = a[0] * s; o[1] = a[1] * s; o[2] = a[2] * s
}
function v3_madd(o: words, a: words, b: words, s: int) -> void { # o = a + b*s
o[0] = f_add(a[0], f_mul(b[0], s)); o[1] = f_add(a[1], f_mul(b[1], s)); o[2] = f_add(a[2], f_mul(b[2], s))
function v3_madd(o: floats, a: floats, b: floats, s: float) -> void { # o = a + b*s
o[0] = a[0] + b[0] * s; o[1] = a[1] + b[1] * s; o[2] = a[2] + b[2] * s
}
function v3_dot(a: words, b: words) -> int {
return f_add(f_add(f_mul(a[0], b[0]), f_mul(a[1], b[1])), f_mul(a[2], b[2]))
function v3_dot(a: floats, b: floats) -> float {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
function v3_cross(o: words, a: words, b: words) -> void {
let x = f_sub(f_mul(a[1], b[2]), f_mul(a[2], b[1]))
let y = f_sub(f_mul(a[2], b[0]), f_mul(a[0], b[2]))
let z = f_sub(f_mul(a[0], b[1]), f_mul(a[1], b[0]))
function v3_cross(o: floats, a: floats, b: floats) -> void {
let x = a[1] * b[2] - a[2] * b[1]
let y = a[2] * b[0] - a[0] * b[2]
let z = a[0] * b[1] - a[1] * b[0]
o[0] = x; o[1] = y; o[2] = z
}
function v3_len(a: words) -> int { return f_sqrt(v3_dot(a, a)) }
function v3_normalize(o: words, a: words) -> void {
function v3_len(a: floats) -> float { return Math.sqrt(v3_dot(a, a)) }
function v3_normalize(o: floats, a: floats) -> void {
let l = v3_len(a)
if l == 0 { o[0] = 0; o[1] = 0; o[2] = 0; return }
let inv = f_div(F_ONE, l)
if l == 0.0 { o[0] = 0.0; o[1] = 0.0; o[2] = 0.0; return }
let inv = 1.0 / l
v3_scale(o, a, inv)
}
function v3_dist(a: words, b: words) -> int {
let t = words(3)
function v3_dist(a: floats, b: floats) -> float {
let t = floats(3)
v3_sub(t, a, b)
let d = v3_len(t)
free(t)
@ -76,78 +77,78 @@ function v3_dist(a: words, b: words) -> int {
}
# ---- matrices (column-major, m[col*4 + row]) ------------------------------------
function m4_new() -> words { let m = words(16); m4_identity(m); return m }
function m4_identity(m: words) -> void {
for i in 0 .. 16 { m[i] = F_ZERO }
m[0] = F_ONE; m[5] = F_ONE; m[10] = F_ONE; m[15] = F_ONE
function m4_new() -> floats { let m = floats(16); m4_identity(m); return m }
function m4_identity(m: floats) -> void {
for i in 0 .. 16 { m[i] = 0.0 }
m[0] = 1.0; m[5] = 1.0; m[10] = 1.0; m[15] = 1.0
}
function m4_copy(o: words, a: words) -> void { for i in 0 .. 16 { o[i] = a[i] } }
function m4_copy(o: floats, a: floats) -> void { for i in 0 .. 16 { o[i] = a[i] } }
# o = a * b (o may not alias a or b)
function m4_mul(o: words, a: words, b: words) -> void {
function m4_mul(o: floats, a: floats, b: floats) -> void {
for c in 0 .. 4 {
for r in 0 .. 4 {
var s = F_ZERO
for k in 0 .. 4 { s = f_add(s, f_mul(a[k * 4 + r], b[c * 4 + k])) }
var s = 0.0
for k in 0 .. 4 { s = s + a[k * 4 + r] * b[c * 4 + k] }
o[c * 4 + r] = s
}
}
}
function m4_mul_into(a: words, b: words) -> void { # a = a * b
let t = words(16)
function m4_mul_into(a: floats, b: floats) -> void { # a = a * b
let t = floats(16)
m4_mul(t, a, b)
m4_copy(a, t)
free(t)
}
function m4_translation(m: words, x: int, y: int, z: int) -> void {
function m4_translation(m: floats, x: float, y: float, z: float) -> void {
m4_identity(m)
m[12] = x; m[13] = y; m[14] = z
}
function m4_scaling(m: words, x: int, y: int, z: int) -> void {
function m4_scaling(m: floats, x: float, y: float, z: float) -> void {
m4_identity(m)
m[0] = x; m[5] = y; m[10] = z
}
function m4_rotation_y(m: words, angle: int) -> void {
function m4_rotation_y(m: floats, angle: float) -> void {
m4_identity(m)
let c = f_cos(angle); let s = f_sin(angle)
m[0] = c; m[2] = f_neg(s); m[8] = s; m[10] = c
let c = Math.cos(angle); let s = Math.sin(angle)
m[0] = c; m[2] = -s; m[8] = s; m[10] = c
}
function m4_rotation_x(m: words, angle: int) -> void {
function m4_rotation_x(m: floats, angle: float) -> void {
m4_identity(m)
let c = f_cos(angle); let s = f_sin(angle)
m[5] = c; m[6] = s; m[9] = f_neg(s); m[10] = c
let c = Math.cos(angle); let s = Math.sin(angle)
m[5] = c; m[6] = s; m[9] = -s; m[10] = c
}
function m4_rotation_z(m: words, angle: int) -> void {
function m4_rotation_z(m: floats, angle: float) -> void {
m4_identity(m)
let c = f_cos(angle); let s = f_sin(angle)
m[0] = c; m[1] = s; m[4] = f_neg(s); m[5] = c
let c = Math.cos(angle); let s = Math.sin(angle)
m[0] = c; m[1] = s; m[4] = -s; m[5] = c
}
# a model matrix: translate * rotate_y * uniform scale
function m4_trs(m: words, x: int, y: int, z: int, yaw: int, s: int) -> void {
function m4_trs(m: floats, x: float, y: float, z: float, yaw: float, s: float) -> void {
m4_rotation_y(m, yaw)
for i in 0 .. 12 { m[i] = f_mul(m[i], s) }
for i in 0 .. 12 { m[i] = m[i] * s }
m[12] = x; m[13] = y; m[14] = z
}
# OpenGL clip space (z in [-1, 1]); fovy in radians
function m4_perspective(m: words, fovy: int, aspect: int, near: int, far: int) -> void {
for i in 0 .. 16 { m[i] = F_ZERO }
let f = f_div(F_ONE, f_tan(f_mul(fovy, F_HALF)))
m[0] = f_div(f, aspect)
function m4_perspective(m: floats, fovy: float, aspect: float, near: float, far: float) -> void {
for i in 0 .. 16 { m[i] = 0.0 }
let f = 1.0 / Math.tan(fovy * 0.5)
m[0] = f / aspect
m[5] = f
m[10] = f_div(f_add(far, near), f_sub(near, far))
m[11] = f_neg(F_ONE)
m[14] = f_div(f_mul(f_mul(F_TWO, far), near), f_sub(near, far))
m[10] = (far + near) / (near - far)
m[11] = -1.0
m[14] = 2.0 * far * near / (near - far)
}
function m4_ortho(m: words, l: int, r: int, b: int, t: int, n: int, f: int) -> void {
function m4_ortho(m: floats, l: float, r: float, b: float, t: float, n: float, f: float) -> void {
m4_identity(m)
m[0] = f_div(F_TWO, f_sub(r, l))
m[5] = f_div(F_TWO, f_sub(t, b))
m[10] = f_div(f_neg(F_TWO), f_sub(f, n))
m[12] = f_neg(f_div(f_add(r, l), f_sub(r, l)))
m[13] = f_neg(f_div(f_add(t, b), f_sub(t, b)))
m[14] = f_neg(f_div(f_add(f, n), f_sub(f, n)))
m[0] = 2.0 / (r - l)
m[5] = 2.0 / (t - b)
m[10] = -2.0 / (f - n)
m[12] = -((r + l) / (r - l))
m[13] = -((t + b) / (t - b))
m[14] = -((f + n) / (f - n))
}
function m4_look_at(m: words, eye: words, at: words, up: words) -> void {
let fwd = words(3); let side = words(3); let u = words(3); let t = words(3)
function m4_look_at(m: floats, eye: floats, at: floats, up: floats) -> void {
let fwd = floats(3); let side = floats(3); let u = floats(3); let t = floats(3)
v3_sub(t, at, eye)
v3_normalize(fwd, t)
v3_cross(t, fwd, up)
@ -156,44 +157,44 @@ function m4_look_at(m: words, eye: words, at: words, up: words) -> void {
m4_identity(m)
m[0] = side[0]; m[4] = side[1]; m[8] = side[2]
m[1] = u[0]; m[5] = u[1]; m[9] = u[2]
m[2] = f_neg(fwd[0]); m[6] = f_neg(fwd[1]); m[10] = f_neg(fwd[2])
m[12] = f_neg(v3_dot(side, eye))
m[13] = f_neg(v3_dot(u, eye))
m[2] = -fwd[0]; m[6] = -fwd[1]; m[10] = -fwd[2]
m[12] = -v3_dot(side, eye)
m[13] = -v3_dot(u, eye)
m[14] = v3_dot(fwd, eye)
free(fwd); free(side); free(u); free(t)
}
# general 4x4 inverse (cofactor expansion); o may not alias a
function m4_inverse(o: words, a: words) -> bool {
let inv = words(16)
inv[0] = f_add(f_sub(f_add(f_mul(a[5], f_mul(a[10], a[15])), f_neg(f_mul(a[5], f_mul(a[11], a[14])))), f_mul(a[9], f_mul(a[6], a[15]))), f_add(f_mul(a[9], f_mul(a[7], a[14])), f_sub(f_mul(a[13], f_mul(a[6], a[11])), f_mul(a[13], f_mul(a[7], a[10])))))
inv[4] = f_add(f_sub(f_add(f_neg(f_mul(a[4], f_mul(a[10], a[15]))), f_mul(a[4], f_mul(a[11], a[14]))), f_mul(a[8], f_mul(a[7], a[14]))), f_add(f_mul(a[8], f_mul(a[6], a[15])), f_sub(f_mul(a[12], f_mul(a[7], a[10])), f_mul(a[12], f_mul(a[6], a[11])))))
inv[8] = f_add(f_sub(f_add(f_mul(a[4], f_mul(a[9], a[15])), f_neg(f_mul(a[4], f_mul(a[11], a[13])))), f_mul(a[8], f_mul(a[5], a[15]))), f_add(f_mul(a[8], f_mul(a[7], a[13])), f_sub(f_mul(a[12], f_mul(a[5], a[11])), f_mul(a[12], f_mul(a[7], a[9])))))
inv[12] = f_add(f_sub(f_add(f_neg(f_mul(a[4], f_mul(a[9], a[14]))), f_mul(a[4], f_mul(a[10], a[13]))), f_mul(a[8], f_mul(a[6], a[13]))), f_add(f_mul(a[8], f_mul(a[5], a[14])), f_sub(f_mul(a[12], f_mul(a[6], a[9])), f_mul(a[12], f_mul(a[5], a[10])))))
inv[1] = f_add(f_sub(f_add(f_neg(f_mul(a[1], f_mul(a[10], a[15]))), f_mul(a[1], f_mul(a[11], a[14]))), f_mul(a[9], f_mul(a[3], a[14]))), f_add(f_mul(a[9], f_mul(a[2], a[15])), f_sub(f_mul(a[13], f_mul(a[3], a[10])), f_mul(a[13], f_mul(a[2], a[11])))))
inv[5] = f_add(f_sub(f_add(f_mul(a[0], f_mul(a[10], a[15])), f_neg(f_mul(a[0], f_mul(a[11], a[14])))), f_mul(a[8], f_mul(a[2], a[15]))), f_add(f_mul(a[8], f_mul(a[3], a[14])), f_sub(f_mul(a[12], f_mul(a[2], a[11])), f_mul(a[12], f_mul(a[3], a[10])))))
inv[9] = f_add(f_sub(f_add(f_neg(f_mul(a[0], f_mul(a[9], a[15]))), f_mul(a[0], f_mul(a[11], a[13]))), f_mul(a[8], f_mul(a[3], a[13]))), f_add(f_mul(a[8], f_mul(a[1], a[15])), f_sub(f_mul(a[12], f_mul(a[3], a[9])), f_mul(a[12], f_mul(a[1], a[11])))))
inv[13] = f_add(f_sub(f_add(f_mul(a[0], f_mul(a[9], a[14])), f_neg(f_mul(a[0], f_mul(a[10], a[13])))), f_mul(a[8], f_mul(a[1], a[14]))), f_add(f_mul(a[8], f_mul(a[2], a[13])), f_sub(f_mul(a[12], f_mul(a[1], a[10])), f_mul(a[12], f_mul(a[2], a[9])))))
inv[2] = f_add(f_sub(f_add(f_mul(a[1], f_mul(a[6], a[15])), f_neg(f_mul(a[1], f_mul(a[7], a[14])))), f_mul(a[5], f_mul(a[2], a[15]))), f_add(f_mul(a[5], f_mul(a[3], a[14])), f_sub(f_mul(a[13], f_mul(a[2], a[7])), f_mul(a[13], f_mul(a[3], a[6])))))
inv[6] = f_add(f_sub(f_add(f_neg(f_mul(a[0], f_mul(a[6], a[15]))), f_mul(a[0], f_mul(a[7], a[14]))), f_mul(a[4], f_mul(a[3], a[14]))), f_add(f_mul(a[4], f_mul(a[2], a[15])), f_sub(f_mul(a[12], f_mul(a[3], a[6])), f_mul(a[12], f_mul(a[2], a[7])))))
inv[10] = f_add(f_sub(f_add(f_mul(a[0], f_mul(a[5], a[15])), f_neg(f_mul(a[0], f_mul(a[7], a[13])))), f_mul(a[4], f_mul(a[1], a[15]))), f_add(f_mul(a[4], f_mul(a[3], a[13])), f_sub(f_mul(a[12], f_mul(a[1], a[7])), f_mul(a[12], f_mul(a[3], a[5])))))
inv[14] = f_add(f_sub(f_add(f_neg(f_mul(a[0], f_mul(a[5], a[14]))), f_mul(a[0], f_mul(a[6], a[13]))), f_mul(a[4], f_mul(a[2], a[13]))), f_add(f_mul(a[4], f_mul(a[1], a[14])), f_sub(f_mul(a[12], f_mul(a[2], a[5])), f_mul(a[12], f_mul(a[1], a[6])))))
inv[3] = f_add(f_sub(f_add(f_neg(f_mul(a[1], f_mul(a[6], a[11]))), f_mul(a[1], f_mul(a[7], a[10]))), f_mul(a[5], f_mul(a[3], a[10]))), f_add(f_mul(a[5], f_mul(a[2], a[11])), f_sub(f_mul(a[9], f_mul(a[3], a[6])), f_mul(a[9], f_mul(a[2], a[7])))))
inv[7] = f_add(f_sub(f_add(f_mul(a[0], f_mul(a[6], a[11])), f_neg(f_mul(a[0], f_mul(a[7], a[10])))), f_mul(a[4], f_mul(a[2], a[11]))), f_add(f_mul(a[4], f_mul(a[3], a[10])), f_sub(f_mul(a[8], f_mul(a[2], a[7])), f_mul(a[8], f_mul(a[3], a[6])))))
inv[11] = f_add(f_sub(f_add(f_neg(f_mul(a[0], f_mul(a[5], a[11]))), f_mul(a[0], f_mul(a[7], a[9]))), f_mul(a[4], f_mul(a[3], a[9]))), f_add(f_mul(a[4], f_mul(a[1], a[11])), f_sub(f_mul(a[8], f_mul(a[3], a[5])), f_mul(a[8], f_mul(a[1], a[7])))))
inv[15] = f_add(f_sub(f_add(f_mul(a[0], f_mul(a[5], a[10])), f_neg(f_mul(a[0], f_mul(a[6], a[9])))), f_mul(a[4], f_mul(a[1], a[10]))), f_add(f_mul(a[4], f_mul(a[2], a[9])), f_sub(f_mul(a[8], f_mul(a[1], a[6])), f_mul(a[8], f_mul(a[2], a[5])))))
let det = f_add(f_add(f_mul(a[0], inv[0]), f_mul(a[1], inv[4])), f_add(f_mul(a[2], inv[8]), f_mul(a[3], inv[12])))
if det == 0 { free(inv); return false }
let id = f_div(F_ONE, det)
for i in 0 .. 16 { o[i] = f_mul(inv[i], id) }
function m4_inverse(o: floats, a: floats) -> bool {
let inv = floats(16)
inv[0] = a[5] * (a[10] * a[15]) + -(a[5] * (a[11] * a[14])) - a[9] * (a[6] * a[15]) + (a[9] * (a[7] * a[14]) + (a[13] * (a[6] * a[11]) - a[13] * (a[7] * a[10])))
inv[4] = -(a[4] * (a[10] * a[15])) + a[4] * (a[11] * a[14]) - a[8] * (a[7] * a[14]) + (a[8] * (a[6] * a[15]) + (a[12] * (a[7] * a[10]) - a[12] * (a[6] * a[11])))
inv[8] = a[4] * (a[9] * a[15]) + -(a[4] * (a[11] * a[13])) - a[8] * (a[5] * a[15]) + (a[8] * (a[7] * a[13]) + (a[12] * (a[5] * a[11]) - a[12] * (a[7] * a[9])))
inv[12] = -(a[4] * (a[9] * a[14])) + a[4] * (a[10] * a[13]) - a[8] * (a[6] * a[13]) + (a[8] * (a[5] * a[14]) + (a[12] * (a[6] * a[9]) - a[12] * (a[5] * a[10])))
inv[1] = -(a[1] * (a[10] * a[15])) + a[1] * (a[11] * a[14]) - a[9] * (a[3] * a[14]) + (a[9] * (a[2] * a[15]) + (a[13] * (a[3] * a[10]) - a[13] * (a[2] * a[11])))
inv[5] = a[0] * (a[10] * a[15]) + -(a[0] * (a[11] * a[14])) - a[8] * (a[2] * a[15]) + (a[8] * (a[3] * a[14]) + (a[12] * (a[2] * a[11]) - a[12] * (a[3] * a[10])))
inv[9] = -(a[0] * (a[9] * a[15])) + a[0] * (a[11] * a[13]) - a[8] * (a[3] * a[13]) + (a[8] * (a[1] * a[15]) + (a[12] * (a[3] * a[9]) - a[12] * (a[1] * a[11])))
inv[13] = a[0] * (a[9] * a[14]) + -(a[0] * (a[10] * a[13])) - a[8] * (a[1] * a[14]) + (a[8] * (a[2] * a[13]) + (a[12] * (a[1] * a[10]) - a[12] * (a[2] * a[9])))
inv[2] = a[1] * (a[6] * a[15]) + -(a[1] * (a[7] * a[14])) - a[5] * (a[2] * a[15]) + (a[5] * (a[3] * a[14]) + (a[13] * (a[2] * a[7]) - a[13] * (a[3] * a[6])))
inv[6] = -(a[0] * (a[6] * a[15])) + a[0] * (a[7] * a[14]) - a[4] * (a[3] * a[14]) + (a[4] * (a[2] * a[15]) + (a[12] * (a[3] * a[6]) - a[12] * (a[2] * a[7])))
inv[10] = a[0] * (a[5] * a[15]) + -(a[0] * (a[7] * a[13])) - a[4] * (a[1] * a[15]) + (a[4] * (a[3] * a[13]) + (a[12] * (a[1] * a[7]) - a[12] * (a[3] * a[5])))
inv[14] = -(a[0] * (a[5] * a[14])) + a[0] * (a[6] * a[13]) - a[4] * (a[2] * a[13]) + (a[4] * (a[1] * a[14]) + (a[12] * (a[2] * a[5]) - a[12] * (a[1] * a[6])))
inv[3] = -(a[1] * (a[6] * a[11])) + a[1] * (a[7] * a[10]) - a[5] * (a[3] * a[10]) + (a[5] * (a[2] * a[11]) + (a[9] * (a[3] * a[6]) - a[9] * (a[2] * a[7])))
inv[7] = a[0] * (a[6] * a[11]) + -(a[0] * (a[7] * a[10])) - a[4] * (a[2] * a[11]) + (a[4] * (a[3] * a[10]) + (a[8] * (a[2] * a[7]) - a[8] * (a[3] * a[6])))
inv[11] = -(a[0] * (a[5] * a[11])) + a[0] * (a[7] * a[9]) - a[4] * (a[3] * a[9]) + (a[4] * (a[1] * a[11]) + (a[8] * (a[3] * a[5]) - a[8] * (a[1] * a[7])))
inv[15] = a[0] * (a[5] * a[10]) + -(a[0] * (a[6] * a[9])) - a[4] * (a[1] * a[10]) + (a[4] * (a[2] * a[9]) + (a[8] * (a[1] * a[6]) - a[8] * (a[2] * a[5])))
let det = a[0] * inv[0] + a[1] * inv[4] + (a[2] * inv[8] + a[3] * inv[12])
if det == 0.0 { free(inv); return false }
let id = 1.0 / det
for i in 0 .. 16 { o[i] = inv[i] * id }
free(inv)
return true
}
# transform a point (w = 1) by m: o = m * (x, y, z, 1), returns w
function m4_xform_point(o: words, m: words, x: int, y: int, z: int) -> int {
o[0] = f_add(f_add(f_mul(m[0], x), f_mul(m[4], y)), f_add(f_mul(m[8], z), m[12]))
o[1] = f_add(f_add(f_mul(m[1], x), f_mul(m[5], y)), f_add(f_mul(m[9], z), m[13]))
o[2] = f_add(f_add(f_mul(m[2], x), f_mul(m[6], y)), f_add(f_mul(m[10], z), m[14]))
return f_add(f_add(f_mul(m[3], x), f_mul(m[7], y)), f_add(f_mul(m[11], z), m[15]))
function m4_xform_point(o: floats, m: floats, x: float, y: float, z: float) -> float {
o[0] = m[0] * x + m[4] * y + (m[8] * z + m[12])
o[1] = m[1] * x + m[5] * y + (m[9] * z + m[13])
o[2] = m[2] * x + m[6] * y + (m[10] * z + m[14])
return m[3] * x + m[7] * y + (m[11] * z + m[15])
}
# ---- uniforms: see gpu.ludic (gpu_uniform and the u_* setters) ----

View file

@ -15,9 +15,9 @@ property Prim {
}
property Model {
prims: []Prim,
radius: int = 0, # float bits: max horizontal extent from the origin
height: int = 0, # float bits: y extent above ymin
ymin: int = 0,
radius: float = 0.0, # float bits: max horizontal extent from the origin
height: float = 0.0, # float bits: y extent above ymin
ymin: float = 0.0,
tris: int = 0,
skin: Skin # the skeleton, for a skinned node (skin.ludic); null for a rigid model
}
@ -35,9 +35,9 @@ var gltf_white: int = 0
var gltf_flat: int = 0
# a JSON number as float bits (ints and fixed-point decimals both)
function jnum(v: Val) -> int {
if v.tag == 2 { return fx_to_f32(v.num) }
return f_from_int(v.num)
function jnum(v: Val) -> float {
if v.tag == 2 { return float(v.num) / 65536.0 }
return float(v.num)
}
function jint(v: Val, key: pointer, fallback: int) -> int {
if value_has(v, key) == 0 { return fallback }
@ -197,7 +197,7 @@ function gltf_load(dir: string, file: string, node_name: string) -> Model {
model.prims = new []Prim
let mesh = value_at(value_get(gltf_doc, "meshes"), mesh_idx)
let prims = value_get(mesh, "primitives")
var r2 = F_ZERO; var ymin = fi(1000); var ymax = fi(-1000)
var r2 = 0.0; var ymin = 1000.0; var ymax = -1000.0
for i in 0 .. value_count(prims) {
let p = value_at(prims, i)
push(model.prims, gltf_prim(p))
@ -205,20 +205,20 @@ function gltf_load(dir: string, file: string, node_name: string) -> Model {
# bounds from the accessor min/max
let acc = value_at(value_get(gltf_doc, "accessors"), value_as_int(value_get(value_get(p, "attributes"), "POSITION")))
let mn = value_get(acc, "min"); let mx = value_get(acc, "max")
let x0 = f_abs(jnum(value_at(mn, 0))); let x1 = f_abs(jnum(value_at(mx, 0)))
let z0 = f_abs(jnum(value_at(mn, 2))); let z1 = f_abs(jnum(value_at(mx, 2)))
let rx = f_max(x0, x1); let rz = f_max(z0, z1)
let rr = f_add(f_mul(rx, rx), f_mul(rz, rz))
if f_gt(rr, r2) { r2 = rr }
let x0 = Math.abs(jnum(value_at(mn, 0))); let x1 = Math.abs(jnum(value_at(mx, 0)))
let z0 = Math.abs(jnum(value_at(mn, 2))); let z1 = Math.abs(jnum(value_at(mx, 2)))
let rx = Math.max(x0, x1); let rz = Math.max(z0, z1)
let rr = rx * rx + rz * rz
if rr > r2 { r2 = rr }
let y0 = jnum(value_at(mn, 1)); let y1 = jnum(value_at(mx, 1))
if f_ls(y0, ymin) { ymin = y0 }
if f_gt(y1, ymax) { ymax = y1 }
if y0 < ymin { ymin = y0 }
if y1 > ymax { ymax = y1 }
}
if skin_idx >= 0 { model.skin = skin_load(skin_idx) }
file_close(gltf_bin)
model.radius = f_sqrt(r2)
model.radius = Math.sqrt(r2)
model.ymin = ymin
model.height = f_sub(ymax, ymin)
model.height = ymax - ymin
print(`gltf: {node_name}: {len(model.prims)} prims, {model.tris} tris`)
return model
}

View file

@ -149,13 +149,13 @@ function gpu_alpha_to_coverage(on: bool) -> void {
# Depth bias for the shadow casters; (0, 0) turns it off. factor/units are fixed, as
# gl_polygon_offset takes them. A pipeline API bakes the bias into the pipeline.
var gpu_s_bias: int = -1
var gpu_s_bias_f: int = 0 # float bits, for a backend that bakes the bias into a pipeline
var gpu_s_bias_u: int = 0
var gpu_s_bias_f: float = 0.0 # float bits, for a backend that bakes the bias into a pipeline
var gpu_s_bias_u: float = 0.0
function gpu_depth_bias(factor: fixed, units: fixed) -> void {
var v = 1
if factor == 0.0 and units == 0.0 { v = 0 }
if v != gpu_s_bias { gpu_s_bias = v; gpu_gl_cap(GL_POLYGON_OFFSET_FILL, v) }
gpu_s_bias_f = fx_to_f32(factor); gpu_s_bias_u = fx_to_f32(units)
gpu_s_bias_f = float(factor); gpu_s_bias_u = float(units)
if v == 1 and gpu_kind != GPU_VK { gl_polygon_offset(factor, units) }
}
@ -241,18 +241,18 @@ function gpu_screenshot(path: string) -> bool { if gpu_kind == GPU_VK { return g
var gpu_u_tmp: words = null
function gpu_tmp() -> words { if gpu_u_tmp == null { gpu_u_tmp = words(4) }; return gpu_u_tmp }
# float bits (IEEE singles in an int), like every other number in the renderer
function u_f(loc: int, v: int) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = v; gvk_u_set(loc, t, 4, 1); return }; let t = gpu_tmp(); t[0] = v; gl_uniform1fv(loc, 1, t) }
function u_f2(loc: int, x: int, y: int) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = x; t[1] = y; gvk_u_set(loc, t, 8, 1); return }; let t = gpu_tmp(); t[0] = x; t[1] = y; gl_uniform2fv(loc, 1, t) }
function u_f3(loc: int, x: int, y: int, z: int) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = x; t[1] = y; t[2] = z; gvk_u_set(loc, t, 12, 1); return }; let t = gpu_tmp(); t[0] = x; t[1] = y; t[2] = z; gl_uniform3fv(loc, 1, t) }
function u_f4(loc: int, x: int, y: int, z: int, w: int) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = x; t[1] = y; t[2] = z; t[3] = w; gvk_u_set(loc, t, 16, 1); return }; let t = gpu_tmp(); t[0] = x; t[1] = y; t[2] = z; t[3] = w; gl_uniform4fv(loc, 1, t) }
function u_v3(loc: int, v: words) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 12, 1); return }; gl_uniform3fv(loc, 1, v) }
function u_fv(loc: int, n: int, v: words) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 4, n); return }; gl_uniform1fv(loc, n, v) }
function u_f(loc: int, v: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(v); gvk_u_set(loc, t, 4, 1); return }; let t = gpu_tmp(); t[0] = float_bits(v); gl_uniform1fv(loc, 1, t) }
function u_f2(loc: int, x: float, y: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); gvk_u_set(loc, t, 8, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); gl_uniform2fv(loc, 1, t) }
function u_f3(loc: int, x: float, y: float, z: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gvk_u_set(loc, t, 12, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gl_uniform3fv(loc, 1, t) }
function u_f4(loc: int, x: float, y: float, z: float, w: float) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gvk_u_set(loc, t, 16, 1); return }; let t = gpu_tmp(); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gl_uniform4fv(loc, 1, t) }
function u_v3(loc: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 12, 1); return }; gl_uniform3fv(loc, 1, v) }
function u_fv(loc: int, n: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 4, n); return }; gl_uniform1fv(loc, n, v) }
# n vec4s from 4n float bits. Not u_fv with 4n: on Vulkan an array element is copied at the size
# given and placed at the array's stride, so a vec4 array fed floats got one float per element -
# which drew the chunked grass with every tile at a nonsense corner and zero blades a cell.
function u_f4v(loc: int, n: int, v: words) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 16, n); return }; gl_uniform4fv(loc, n, v) }
function u_mat4(loc: int, m: words) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, m, 64, 1); return }; gl_uniform_matrix4fv(loc, 1, 0, m) }
function u_mat4n(loc: int, n: int, m: words) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, m, 64, n); return }; gl_uniform_matrix4fv(loc, n, 0, m) }
function u_f4v(loc: int, n: int, v: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, v, 16, n); return }; gl_uniform4fv(loc, n, v) }
function u_mat4(loc: int, m: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, m, 64, 1); return }; gl_uniform_matrix4fv(loc, 1, 0, m) }
function u_mat4n(loc: int, n: int, m: floats) -> void { if gpu_kind == GPU_VK { gvk_u_set(loc, m, 64, n); return }; gl_uniform_matrix4fv(loc, n, 0, m) }
function u_i(loc: int, v: int) -> void { if gpu_kind == GPU_VK { let t = gpu_tmp(); t[0] = v; gvk_u_set(loc, t, 4, 1); return }; gl_uniform1i(loc, v) }
# ---- what this machine can do ----------------------------------------------------
@ -702,7 +702,7 @@ function gpu_tex_param(kind: int, pname: int, value: int) -> void {
function gpu_tex_paramf(kind: int, pname: int, value: fixed) -> void {
if gpu_kind != GPU_VK { gl_tex_parameterf(gpu_gl_target(kind), pname, value) }
let o = gpu_tx_at(gpu_bound(kind))
if o >= 0 and pname == 0x84FE { gpu_tx[o + 11] = fx_to_f32(value) }
if o >= 0 and pname == 0x84FE { gpu_tx[o + 11] = float_bits(float(value)) }
gpu_glcheck_after("tex paramf")
}
# the border colour clamp-to-border reads (four fixed values in `rgba`)
@ -939,7 +939,7 @@ function gpu_glcheck_after(what: string) -> void {
let e = gl_get_error()
if e != 0 { gpu_glcheck_say(`gpu: error {e} from {what} into {gpu_fb_describe(gpu_fb_cur)}`) }
}
function gpu_clear_color(r: fixed, g: fixed, b: fixed, a: fixed) -> void { if gpu_kind == GPU_VK { gvk_clear_color(fx_to_f32(r), fx_to_f32(g), fx_to_f32(b), fx_to_f32(a)); return }; gl_clear_color(r, g, b, a) }
function gpu_clear_color(r: fixed, g: fixed, b: fixed, a: fixed) -> void { if gpu_kind == GPU_VK { gvk_clear_color(float(r), float(g), float(b), float(a)); return }; gl_clear_color(r, g, b, a) }
function gpu_clear(mask: int) -> void {
if gpu_kind == GPU_VK { gvk_clear(mask, gpu_fb, gpu_fb_at(gvk_fb_cur)); return }
gpu_glcheck_before("a clear")

View file

@ -194,7 +194,7 @@ function gvk_init() -> bool {
gvk_has_hqr = Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_hostQueryReset) == 1 and Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampComputeAndGraphics) == 1
if gvk_has_hqr {
Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_hostQueryReset, 1)
gvk_ts_period = Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampPeriod)
gvk_ts_period = float_from_bits(Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_timestampPeriod))
}
Vk.put_i32(cnt, 0, 0)
@ -581,7 +581,7 @@ function gvk_shutdown() -> void {
# again. On a device without host query reset or timestamps, every read says "not yet" and the report
# stays empty.
var gvk_has_hqr: bool = false
var gvk_ts_period: int = 0 # float bits: nanoseconds per timestamp tick
var gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick
var gvk_qpool: long = 0
var gvk_q_active: int = -1
function gvk_query_new(n: int, ids: words) -> void {
@ -616,6 +616,6 @@ function gvk_query_result(id: int, out: words) -> bool {
if Vk.get_query_pool_results(gvk_dev, gvk_qpool, id * 2, 2, size, data, stride, VK_QUERY_RESULT_64_BIT) != VK_SUCCESS { return false }
let ticks = Text.to_int(string(Vk.get_i64(data, 8) - Vk.get_i64(data, 0)))
if ticks < 0 { return false }
out[0] = f_to_int(f_mul(fi(ticks), gvk_ts_period))
out[0] = int(float(ticks) * gvk_ts_period)
return true
}

View file

@ -245,19 +245,19 @@ function gvk_compute_probe() -> void {
let n = 1000
let b = gvk_buf_new()
let data = bytes(n * 4)
for i in 0 .. n { Vk.put_i32(data, i * 4, fi(i)) }
for i in 0 .. n { Vk.put_i32(data, i * 4, float_bits(float(i))) }
gvk_buf_upload(b, n * 4, data)
gvk_buf_gpu_owned(b)
let pr = bytes(8)
Vk.put_i32(pr, 0, n)
Vk.put_i32(pr, 4, fi(3))
Vk.put_i32(pr, 4, float_bits(3.0))
let bufs = words(1)
bufs[0] = b
gvk_dispatch(c, pr, 8, bufs, (n + 63) / 64, 1, 1)
gvk_flush()
var bad = 0
let mp = gvk_buf_map[b]
for i in 0 .. n { if Vk.get_i32(mp, i * 4) != fi(i * 3) { bad += 1 } }
for i in 0 .. n { if float_from_bits(Vk.get_i32(mp, i * 4)) != float(i * 3) { bad += 1 } }
if bad == 0 { print(`r3d: vulkan compute probe OK ({n} values)`) } else { print(`r3d: vulkan compute probe: FAILED ({bad} of {n} wrong)`) }
}
@ -943,7 +943,7 @@ var gvk_pass_samples: int = 1 # the attachments' sample count, which eve
var gvk_pass_col: words = null # the pass's colour attachments: texture, layer + 1
var gvk_pass_dep: words = null # its depth attachment: texture, layer + 1
var gvk_clear_bits: int = 0 # GL_COLOR_BUFFER_BIT / GL_DEPTH_BUFFER_BIT waiting for the pass
var gvk_clear_rgba: words = null # float bits
var gvk_clear_rgba: floats = null # float bits
var gvk_vp: words = null # x, y, w, h
var gvk_sc: words = null # scissor on, x, y (OpenGL rows), w, h
var gvk_screen_color: int = 0
@ -977,17 +977,17 @@ function gvk_screen_fmt() -> int { if gvk_hdr_on { return GL_RGB10_A2 }; return
# picture's and the interface's white sit, and how far the darkest shade is lifted. Displays differ by
# an order of magnitude - a 400-nit monitor and a 2000-nit television - and one fixed curve either
# clips the first's highlights flat or leaves the second dim.
var r3d_hdr_peak: int = 0 # 0 until r3d_hdr_calibrate: 1000
var r3d_hdr_paper: int = 0 # 200
var r3d_hdr_black: int = 0 # 0
function r3d_hdr_peak_nits() -> int { if r3d_hdr_peak == 0 { return fi(1000) }; return r3d_hdr_peak }
function r3d_hdr_paper_nits() -> int { if r3d_hdr_paper == 0 { return fi(200) }; return r3d_hdr_paper }
function r3d_hdr_black_nits() -> int { return r3d_hdr_black }
function r3d_hdr_calibrate(peak: int, paper: int, black: int) -> void {
var pk = f_clamp(peak, fi(100), fi(10000))
let pp = f_clamp(paper, fi(80), fi(1000))
if f_ls(pk, pp) { pk = pp }
let bl = f_clamp(black, F_ZERO, fi(5))
var r3d_hdr_peak: float = 0.0 # 0 until r3d_hdr_calibrate: 1000
var r3d_hdr_paper: float = 0.0 # 200
var r3d_hdr_black: float = 0.0 # 0
function r3d_hdr_peak_nits() -> float { if r3d_hdr_peak == 0.0 { return 1000.0 }; return r3d_hdr_peak }
function r3d_hdr_paper_nits() -> float { if r3d_hdr_paper == 0.0 { return 200.0 }; return r3d_hdr_paper }
function r3d_hdr_black_nits() -> float { return r3d_hdr_black }
function r3d_hdr_calibrate(peak: float, paper: float, black: float) -> void {
var pk = Math.clamp(peak, 100.0, 10000.0)
let pp = Math.clamp(paper, 80.0, 1000.0)
if pk < pp { pk = pp }
let bl = Math.clamp(black, 0.0, 5.0)
if pk == r3d_hdr_peak and pp == r3d_hdr_paper and bl == r3d_hdr_black { return }
r3d_hdr_peak = pk; r3d_hdr_paper = pp; r3d_hdr_black = bl
# the display is told what the picture now reaches
@ -999,14 +999,14 @@ function gvk_hdr_metadata() -> void {
let md = bytes(VkHdrMetadataEXT_sizeof)
Vk.zero(md, VkHdrMetadataEXT_sizeof)
Vk.put_i32(md, VkHdrMetadataEXT_sType, VK_STRUCTURE_TYPE_HDR_METADATA_EXT)
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryRed + VkXYColorEXT_x, fl(0.64)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryRed + VkXYColorEXT_y, fl(0.33))
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_x, fl(0.30)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_y, fl(0.60))
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_x, fl(0.15)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_y, fl(0.06))
Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_x, fl(0.3127)); Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_y, fl(0.3290))
Vk.put_i32(md, VkHdrMetadataEXT_maxLuminance, r3d_hdr_peak_nits())
Vk.put_i32(md, VkHdrMetadataEXT_minLuminance, fl(0.001))
Vk.put_i32(md, VkHdrMetadataEXT_maxContentLightLevel, r3d_hdr_peak_nits())
Vk.put_i32(md, VkHdrMetadataEXT_maxFrameAverageLightLevel, r3d_hdr_paper_nits())
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryRed + VkXYColorEXT_x, float_bits(0.64)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryRed + VkXYColorEXT_y, float_bits(0.33))
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_x, float_bits(0.30)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_y, float_bits(0.60))
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_x, float_bits(0.15)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_y, float_bits(0.06))
Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_x, float_bits(0.3127)); Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_y, float_bits(0.3290))
Vk.put_i32(md, VkHdrMetadataEXT_maxLuminance, float_bits(r3d_hdr_peak_nits()))
Vk.put_i32(md, VkHdrMetadataEXT_minLuminance, float_bits(0.001))
Vk.put_i32(md, VkHdrMetadataEXT_maxContentLightLevel, float_bits(r3d_hdr_peak_nits()))
Vk.put_i32(md, VkHdrMetadataEXT_maxFrameAverageLightLevel, float_bits(r3d_hdr_paper_nits()))
let chains = bytes(8)
Vk.put_i64(chains, 0, gvk_swap)
Vk.set_hdr_metadata_ext(gvk_dev, 1, chains, md)
@ -1014,7 +1014,7 @@ function gvk_hdr_metadata() -> void {
function gvk_screen_make(w: int, h: int) -> bool {
if gvk_vp == null {
gvk_vp = words(4); gvk_sc = words(5); gvk_clear_rgba = words(4)
gvk_vp = words(4); gvk_sc = words(5); gvk_clear_rgba = floats(4)
gvk_pass_col = words(4); gvk_pass_dep = words(2)
gvk_fb_ncolor = words(4096)
for i in 0 .. 4096 { gvk_fb_ncolor[i] = 1 }
@ -1131,7 +1131,7 @@ function gvk_pass_begin(rec: words, rec_o: int) -> void {
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE)
if (gvk_clear_bits & GL_COLOR_BUFFER_BIT) != 0 {
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_CLEAR)
for k in 0 .. 4 { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_clearValue + k * 4, gvk_clear_rgba[k]) }
for k in 0 .. 4 { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_clearValue + k * 4, float_bits(gvk_clear_rgba[k])) }
} else { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD) }
gvk_pass_cfmt = gvk_tex_vkfmt[tex]
gvk_pass_samples = gvk_tex_samples_of(tex)
@ -1193,7 +1193,7 @@ function gvk_clear(mask: int, rec: words, rec_o: int) -> void {
for c in 0 .. gvk_pass_ncolor {
Vk.put_i32(atts, n * caw + VkClearAttachment_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
Vk.put_i32(atts, n * caw + VkClearAttachment_colorAttachment, c)
for k in 0 .. 4 { Vk.put_i32(atts, n * caw + VkClearAttachment_clearValue + k * 4, gvk_clear_rgba[k]) }
for k in 0 .. 4 { Vk.put_i32(atts, n * caw + VkClearAttachment_clearValue + k * 4, float_bits(gvk_clear_rgba[k])) }
n += 1
}
}
@ -1219,10 +1219,10 @@ function gvk_tex_h(tex: int) -> int { return gvk_tex_dims_h[tex] }
function gvk_set_view(cb: pointer) -> void {
let vp = bytes(VkViewport_sizeof)
Vk.zero(vp, VkViewport_sizeof)
Vk.put_i32(vp, VkViewport_x, fi(gvk_vp[0]))
Vk.put_i32(vp, VkViewport_y, fi(gvk_vp[1]))
Vk.put_i32(vp, VkViewport_width, fi(gvk_vp[2]))
Vk.put_i32(vp, VkViewport_height, fi(gvk_vp[3]))
Vk.put_i32(vp, VkViewport_x, float_bits(float(gvk_vp[0])))
Vk.put_i32(vp, VkViewport_y, float_bits(float(gvk_vp[1])))
Vk.put_i32(vp, VkViewport_width, float_bits(float(gvk_vp[2])))
Vk.put_i32(vp, VkViewport_height, float_bits(float(gvk_vp[3])))
Vk.put_i32(vp, VkViewport_maxDepth, 0x3F800000)
Vk.cmd_set_viewport(cb, 0, 1, vp)
let sc = bytes(VkRect2D_sizeof)
@ -1454,7 +1454,7 @@ function gvk_viewport(x: int, y: int, w: int, h: int) -> void {
if gvk_vp == null { return }
gvk_vp[0] = x; gvk_vp[1] = y; gvk_vp[2] = w; gvk_vp[3] = h
}
function gvk_clear_color(r: int, g: int, b: int, a: int) -> void {
function gvk_clear_color(r: float, g: float, b: float, a: float) -> void {
if gvk_clear_rgba == null { return }
gvk_clear_rgba[0] = r; gvk_clear_rgba[1] = g; gvk_clear_rgba[2] = b; gvk_clear_rgba[3] = a
}
@ -1487,8 +1487,8 @@ function gvk_state_now() -> GvkState {
st.color_write = 1; if gpu_s_color_write == 0 { st.color_write = 0 }
st.a2c = 0; if gpu_s_a2c == 1 { st.a2c = 1 }
st.bias = 0; if gpu_s_bias == 1 { st.bias = 1 }
st.bias_factor = gpu_s_bias_f
st.bias_units = gpu_s_bias_u
st.bias_factor = float_bits(gpu_s_bias_f)
st.bias_units = float_bits(gpu_s_bias_u)
st.wireframe = gvk_wireframe
return st
}

View file

@ -480,19 +480,19 @@ function gvk_compare_op(f: int) -> int {
# which is -2.0 at Performance and about -1.6 at Quality. It is applied ONLY while DLSS is live: a
# plain spatial upscale has no temporal accumulation to hide the aliasing a negative bias brings,
# so biasing there would trade blur for shimmer.
function gvk_mip_bias() -> int {
function gvk_mip_bias() -> float {
# R3D_NO_MIPBIAS=1 puts it back the way it was, so one build can be compared against itself
if r3d_env_has("R3D_NO_MIPBIAS") { return F_ZERO }
if not r3d_dlss_live() { return F_ZERO }
if r3d_env_has("R3D_NO_MIPBIAS") { return 0.0 }
if not r3d_dlss_live() { return 0.0 }
let rw = r3d_dlss_render_w()
if rw <= 0 or gl_w <= 0 or rw >= gl_w { return F_ZERO }
if rw <= 0 or gl_w <= 0 or rw >= gl_w { return 0.0 }
# log2 from the natural log the runtime has: log2(x) = ln(x) * 1/ln(2)
return f_sub(f_mul(f_log(f_div(fi(rw), fi(gl_w))), fl(1.4426950408889634)), F_ONE)
return Math.log(float(rw) / float(gl_w)) * 1.4426950408889634 - 1.0
}
# the sampler for texture tex's parameters, from the texture's own one-entry cache when they have
# not changed since it last asked - a draw asks for every texture it binds
function gvk_tex_sampler(tex: int, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
let bias = gvk_mip_bias()
let bias = float_bits(gvk_mip_bias())
var sig = min_f * 31 + mag_f
sig = sig * 31 + wrap_s
sig = sig * 31 + wrap_t
@ -507,7 +507,7 @@ function gvk_tex_sampler(tex: int, min_f: int, mag_f: int, wrap_s: int, wrap_t:
return s
}
function gvk_sampler(min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
let bias = gvk_mip_bias()
let bias = float_bits(gvk_mip_bias())
let key = `{min_f}/{mag_f}/{wrap_s}/{wrap_t}/{compare}/{aniso}/{bias}`
if gvk_smp_keys == null { gvk_smp_keys = new []string; gvk_smp = new []long }
for i in 0 .. len(gvk_smp_keys) { if gvk_smp_keys[i] == key { return gvk_smp[i] } }

View file

@ -14,7 +14,7 @@ const GRASS_CELL: int = 16
var grass_prog: int = 0
var grass_mesh: Mesh = null
var grass_on: bool = true
var grass_wind: int = 0
var grass_wind: float = 0.0
# A photographed blade, as an atlas of straightened blades side by side (the game sets
# this; the renderer does not name a game asset). 0 = the procedural gradient, which is
# what this was for a year: a two-tone ramp with a hard edge, and every blade in the
@ -27,12 +27,12 @@ var grass_blade_cols: int = 8
# in it: you walked through a meadow and every blade ignored you, which is the single most
# noticeable thing missing from every step the game asks you to take. The game sets this each
# frame; radius 0 means nobody is there.
var grass_push_x: int = 0
var grass_push_z: int = 0
var grass_push_r: int = 0
var grass_s0: int = 0 # float bits: blade spacing at the camera (m)
var grass_d0: int = 0 # the distance at which the spacing has doubled (m)
var grass_radius: int = 0 # no blades past this (m)
var grass_push_x: float = 0.0
var grass_push_z: float = 0.0
var grass_push_r: float = 0.0
var grass_s0: float = 0.0 # float bits: blade spacing at the camera (m)
var grass_d0: float = 0.0 # the distance at which the spacing has doubled (m)
var grass_radius: float = 0.0 # no blades past this (m)
var grass_draws: int = 0
var grass_dbg: int = 0
# Vulkan with multi-draw indirect: every visible tile is a record in one buffer, uploaded once a
@ -43,8 +43,8 @@ const GRASS_CHUNK: int = 256
const GRASS_RECS: int = 8192
var grass_merge: bool = false
var grass_rec: words = null # VkDrawIndexedIndirectCommand records, 5 words each
var grass_tv: words = null # per record: corner x, corner z, indices per cell, 0 (float bits)
var grass_chunk_tv: words = null
var grass_tv: floats = null # per record: corner x, corner z, indices per cell, 0 (float bits)
var grass_chunk_tv: floats = null
var grass_n: int = 0
var grass_cmds: int = 0
var grass_band_start: words = null # per band: its first record
@ -81,16 +81,16 @@ function grass_blade_mesh(rows: int) -> Mesh {
# grass.mesh carries A COPY of these two lines for the mesh-shader path; change both or
# the Windows blades stop matching the ones everywhere else.
for r in 0 .. rows {
let t = fr(r, rows - 1)
let grow = f_min(f_div(t, fl(0.22)), F_ONE)
let wide = f_mul(f_add(fl(0.50), f_mul(fl(0.33), grow)), f_sub(F_ONE, f_mul(t, f_mul(t, t))))
let taper = f_max(wide, fl(0.05))
let bend = f_mul(f_mul(t, t), fl(0.52))
let t = float(r) / float(rows - 1)
let grow = Math.min(t / 0.22, 1.0)
let wide = (0.50 + 0.33 * grow) * (1.0 - t * (t * t))
let taper = Math.max(wide, 0.05)
let bend = t * t * 0.52
for sd in 0 .. 2 {
var x = f_neg(F_HALF)
if sd == 1 { x = F_HALF }
gl_put_bits(v, k, f_mul(x, taper)); gl_put_bits(v, k + 1, t); gl_put_bits(v, k + 2, bend)
gl_put_bits(v, k + 3, fi(sd)); gl_put_bits(v, k + 4, t)
var x = -0.5
if sd == 1 { x = 0.5 }
gl_put_bits(v, k, float_bits(x * taper)); gl_put_bits(v, k + 1, float_bits(t)); gl_put_bits(v, k + 2, float_bits(bend))
gl_put_bits(v, k + 3, float_bits(float(sd))); gl_put_bits(v, k + 4, float_bits(t))
k += 5
}
}
@ -117,7 +117,7 @@ function grass_init() -> void {
var defs = "#define FOLIAGE\n#define BLADE\n"
if grass_merge {
defs = defs + "#define TILES\n"
grass_rec = words(GRASS_RECS * 5); grass_tv = words(GRASS_RECS * 4); grass_chunk_tv = words(GRASS_CHUNK * 4)
grass_rec = words(GRASS_RECS * 5); grass_tv = floats(GRASS_RECS * 4); grass_chunk_tv = floats(GRASS_CHUNK * 4)
grass_band_start = words(4); grass_band_cells = words(4)
grass_cmds = gpu_buffer_new()
}
@ -126,30 +126,30 @@ function grass_init() -> void {
# five rows, four quads: the arch above needs somewhere to bend, and at four rows a
# blade that leans over is three straight segments and shows every join
grass_mesh = grass_blade_mesh(5)
grass_wind = fl(2.4)
grass_wind = 2.4
# Matched to the blade's width: a 1 cm blade at 0.11 m spacing covers a third of what a
# 2.8 cm blade did, and the meadow goes bare. The game's graphics settings override this
# (gfx_grass_spacing), but only once game_init has run - a plain headless render never
# gets there, so the two have to agree or a shot shows something no player will see.
# That is exactly how the last change measured as "no effect": the render was identical
# because this line, not the settings, was deciding.
grass_s0 = fl(0.066)
grass_d0 = fi(45)
grass_radius = fi(1600)
grass_s0 = 0.066
grass_d0 = 45.0
grass_radius = 1600.0
if r3d_env_has("R3D_NOBLADES") { grass_on = false }
if r3d_env_has("R3D_GRASS_R") { grass_radius = fi(Text.to_int(r3d_env("R3D_GRASS_R"))) }
if r3d_env_has("R3D_GRASS_R") { grass_radius = float(Text.to_int(r3d_env("R3D_GRASS_R"))) }
if r3d_env_has("R3D_GRASS_DBG") { grass_dbg = Text.to_int(r3d_env("R3D_GRASS_DBG")) }
}
# indices per 16 m cell that could exist at distance d (the count the shader computes)
function grass_count_at(d: int) -> int {
let spacing = f_mul(grass_s0, f_add(F_ONE, f_div(d, grass_d0)))
let n = f_div(fi(GRASS_CELL * GRASS_CELL), f_mul(spacing, spacing))
return f_to_int(n) + 1
function grass_count_at(d: float) -> int {
let spacing = grass_s0 * (1.0 + d / grass_d0)
let n = float(GRASS_CELL * GRASS_CELL) / (spacing * spacing)
return int(n) + 1
}
# one tile size over one distance band
function grass_tiles(size: int, d_min: int, d_max: int) -> void {
function grass_tiles(size: int, d_min: float, d_max: float) -> void {
let p = grass_prog
let cells = size / GRASS_CELL
if grass_merge {
@ -157,27 +157,27 @@ function grass_tiles(size: int, d_min: int, d_max: int) -> void {
} else {
u_i(gpu_uniform(p, "u_tile_cells"), cells)
}
let sz = fi(size)
let half = f_mul(sz, F_HALF)
let reach = f_add(d_max, f_mul(half, fl(1.5)))
let tx0 = f_to_int(f_floor(f_div(f_sub(cam_pos[0], reach), sz)))
let tx1 = f_to_int(f_floor(f_div(f_add(cam_pos[0], reach), sz)))
let tz0 = f_to_int(f_floor(f_div(f_sub(cam_pos[2], reach), sz)))
let tz1 = f_to_int(f_floor(f_div(f_add(cam_pos[2], reach), sz)))
let corner_r = f_mul(half, fl(1.42))
let sz = float(size)
let half = sz * 0.5
let reach = d_max + half * 1.5
let tx0 = int(Math.floor((cam_pos[0] - reach) / sz))
let tx1 = int(Math.floor((cam_pos[0] + reach) / sz))
let tz0 = int(Math.floor((cam_pos[2] - reach) / sz))
let tz1 = int(Math.floor((cam_pos[2] + reach) / sz))
let corner_r = half * 1.42
var tz = tz0
while tz <= tz1 {
var tx = tx0
while tx <= tx1 {
let ox = f_mul(fi(tx), sz); let oz = f_mul(fi(tz), sz)
let cx = f_add(ox, half); let cz = f_add(oz, half)
let dx = f_sub(cx, cam_pos[0]); let dz = f_sub(cz, cam_pos[2])
let dc = f_sqrt(f_add(f_mul(dx, dx), f_mul(dz, dz)))
let ox = float(tx) * sz; let oz = float(tz) * sz
let cx = ox + half; let cz = oz + half
let dx = cx - cam_pos[0]; let dz = cz - cam_pos[2]
let dc = Math.sqrt(dx * dx + dz * dz)
# the tile's nearest and farthest points decide which band it belongs to
let dnear = f_max(f_sub(dc, corner_r), F_ZERO)
if f_ls(dc, d_min) or not f_ls(dnear, d_max) { tx += 1; continue }
let dnear = Math.max(dc - corner_r, 0.0)
if dc < d_min or not (dnear < d_max) { tx += 1; continue }
let cy = terrain_height(cx, cz)
if cam_sphere_visible(cx, cy, cz, f_add(corner_r, fi(6))) {
if cam_sphere_visible(cx, cy, cz, corner_r + 6.0) {
let per = grass_count_at(dnear)
if per > 0 and grass_merge {
if grass_n < GRASS_RECS {
@ -187,7 +187,7 @@ function grass_tiles(size: int, d_min: int, d_max: int) -> void {
grass_rec[r] = grass_mesh.count; grass_rec[r + 1] = inst; grass_rec[r + 2] = 0; grass_rec[r + 3] = 0
grass_rec[r + 4] = ((grass_n - grass_band_start[grass_band_n - 1]) % GRASS_CHUNK) * 65536
let t = grass_n * 4
grass_tv[t] = ox; grass_tv[t + 1] = oz; grass_tv[t + 2] = fi(per); grass_tv[t + 3] = F_ZERO
grass_tv[t] = ox; grass_tv[t + 1] = oz; grass_tv[t + 2] = float(per); grass_tv[t + 3] = 0.0
grass_n += 1
}
} else if per > 0 {
@ -216,28 +216,28 @@ function grass_draw() -> void {
# CLAUDE.md records against u_wade and u_flutter, and it costs a day every time
let btex = grass_blade_tex
let bcols = grass_blade_cols
u_f(gpu_uniform(p, "u_blade_cols"), fi(bcols))
var bon = F_ZERO
if btex != 0 { bon = F_ONE; r3d_bind_2d(p, "u_blade_tex", 12, btex) }
u_f(gpu_uniform(p, "u_blade_cols"), float(bcols))
var bon = 0.0
if btex != 0 { bon = 1.0; r3d_bind_2d(p, "u_blade_tex", 12, btex) }
u_f(gpu_uniform(p, "u_blade_tex_on"), bon)
u_f3(gpu_uniform(p, "u_push"), grass_push_x, grass_push_z, grass_push_r)
u_f(gpu_uniform(p, "u_rough_scale"), F_ONE)
u_f(gpu_uniform(p, "u_rough_scale"), 1.0)
u_v3(gpu_uniform(p, "u_tint"), sc_blade_tint)
u_v3(gpu_uniform(p, "u_blade_base"), sc_blade_base)
u_v3(gpu_uniform(p, "u_blade_tip"), sc_blade_tip)
u_f(gpu_uniform(p, "u_cull"), grass_radius)
u_f(gpu_uniform(p, "u_model_h"), F_ZERO)
u_f(gpu_uniform(p, "u_model_h"), 0.0)
u_f(gpu_uniform(p, "u_s0"), grass_s0)
u_f(gpu_uniform(p, "u_d0"), grass_d0)
u_f(gpu_uniform(p, "u_radius"), grass_radius)
u_i(gpu_uniform(p, "u_dbg"), grass_dbg)
var orthotex = ter_ortho_tex
var oon = F_ONE
if orthotex == 0 { orthotex = ter_height_tex; oon = F_ZERO }
var oon = 1.0
if orthotex == 0 { orthotex = ter_height_tex; oon = 0.0 }
r3d_bind_2d(p, "u_ortho", 4, orthotex)
u_f(gpu_uniform(p, "u_ortho_on"), oon)
var lake = fl(-100000.0)
if ter_lake_ex != 0 { lake = ter_lake_level }
var lake = -100000.0
if ter_lake_ex != 0.0 { lake = ter_lake_level }
u_f(gpu_uniform(p, "u_lake_level"), lake)
var sea = lake
if ter_sea_set { sea = ter_sea_level }
@ -252,15 +252,15 @@ function grass_draw() -> void {
# at 0.15, 0.70% of a near-ground frame was over 210 of 255; at 0.0 it is 0.05%. A blade
# does have a faint sheen, so this is small rather than nothing - the foliage layers have
# used 0.05 all along and never showed the fault.
u_f(gpu_uniform(p, "u_spec_scale"), fl(0.008))
u_f(gpu_uniform(p, "u_spec_scale"), 0.008)
gpu_cull(false)
grass_draws = 0
grass_n = 0
grass_band_n = 0
gpu_mesh_bind(grass_mesh)
grass_tiles(16, F_ZERO, fi(300))
grass_tiles(64, fi(300), fi(1200))
grass_tiles(256, fi(1200), grass_radius)
grass_tiles(16, 0.0, 300.0)
grass_tiles(64, 300.0, 1200.0)
grass_tiles(256, 1200.0, grass_radius)
if grass_merge { grass_flush() }
gpu_cull(true)
}
@ -287,7 +287,7 @@ function grass_flush() -> void {
# chunked path's 2 at 4K on an RTX 3070 Ti)
let cells = grass_band_cells[b]
for q in 0 .. m {
var total = f_to_int(grass_tv[(k + q) * 4 + 2]) * cells * cells
var total = int(grass_tv[(k + q) * 4 + 2]) * cells * cells
if total > 65535 { total = 65535 }
if total > 0 {
for c in 0 .. 4 { grass_chunk_tv[c] = grass_tv[(k + q) * 4 + c] }

View file

@ -25,16 +25,16 @@ function mesh_draw_instanced(m: Mesh, n: int) -> void { gpu_draw_mesh_instanced(
# A flat n x n vertex grid over [-half, half]^2 in x/z, y = 0. Attribute 0 = (x, z).
# The terrain vertex shader lifts it with the height map.
function mesh_grid(n: int, half: int) -> Mesh {
function mesh_grid(n: int, half: float) -> Mesh {
let m = gpu_mesh_new()
let nv = n * n
let v = gl_floats(nv * 2)
var k = 0
for j in 0 .. n {
for i in 0 .. n {
let x = f_sub(f_mul(f_mul(fr(i, n - 1), F_TWO), half), half)
let z = f_sub(f_mul(f_mul(fr(j, n - 1), F_TWO), half), half)
gl_put_bits(v, k, x); gl_put_bits(v, k + 1, z)
let x = float(i) / float(n - 1) * 2.0 * half - half
let z = float(j) / float(n - 1) * 2.0 * half - half
gl_put_bits(v, k, float_bits(x)); gl_put_bits(v, k + 1, float_bits(z))
k += 2
}
}

View file

@ -18,10 +18,10 @@ var ov_buf: pointer = null
var ov_n: int = 0
var ov_tex: int = 0
var ov_mode: int = 2 # what the next quads read: 0 the image texture, 1 the font, 2 a flat colour
var ov_voff: int = 0 # float bits: 4 x ov_mode, added to v (overlay.frag)
var ov_voff: float = 0.0 # float bits: 4 x ov_mode, added to v (overlay.frag)
var ov_white: int = 0
var ov_font: int = 0
var ov_font_adv: words = null # float bits, em units, one per glyph in the atlas
var ov_font_adv: floats = null # float bits, em units, one per glyph in the atlas
var ov_font_n: int = 0 # glyphs in the atlas
# Text is UTF-8, and the atlas says which code points it holds. font.json's "codes" lists
# them in atlas order; an older atlas without it is ASCII from "first" (32) on, which is
@ -38,8 +38,8 @@ var ov_font_cols: int = 16
var ov_font_rows: int = 6
var ov_font_cell: int = 128 # px per cell in the atlas
var ov_font_em: int = 100 # px per em in the atlas
var ov_pad_x: int = 0 # float bits, em
var ov_base_y: int = 0
var ov_pad_x: float = 0.0 # float bits, em
var ov_base_y: float = 0.0
var ov_ready: bool = false
var ov_open: bool = false
var ov_dbg: bool = false
@ -61,16 +61,16 @@ var ov_prog_hdr: int = 0
# How bright the interface's white is while HDR10 is on, as a multiple of paper white. It is 1 for the
# interface; a calibration screen draws its test patches at a number of nits with ov_hdr_nits, which
# closes the batch so far - the multiple is one uniform per flush. On an SDR frame it does nothing.
var ov_hdr_scale: int = 0 # float bits; 0 until ov_begin sets 1
function ov_hdr_nits(nits: int) -> void {
var s = F_ONE
if nits != 0 { s = f_div(nits, r3d_hdr_paper_nits()) }
var ov_hdr_scale: float = 0.0 # float bits; 0 until ov_begin sets 1
function ov_hdr_nits(nits: float) -> void {
var s = 1.0
if nits != 0.0 { s = nits / r3d_hdr_paper_nits() }
if s == ov_hdr_scale { return }
if ov_open { ov_flush() }
ov_hdr_scale = s
}
# back to the interface's own white
function ov_hdr_paper() -> void { ov_hdr_nits(0) }
function ov_hdr_paper() -> void { ov_hdr_nits(0.0) }
function ov_pick_prog() -> void {
if gpu_hdr_active() {
if ov_prog_hdr == 0 {
@ -113,8 +113,8 @@ function overlay_font(font_dir: string) -> bool {
if tex == 0 { return false }
let j = Json.parse(meta)
ov_font_cols = jint(j, "cols", 16); ov_font_rows = jint(j, "rows", 6)
ov_font_cell = f_to_int(jnum(value_get(j, "cell"))); ov_font_em = f_to_int(jnum(value_get(j, "em")))
ov_pad_x = fl(0.14); ov_base_y = fl(0.30)
ov_font_cell = int(jnum(value_get(j, "cell"))); ov_font_em = int(jnum(value_get(j, "em")))
ov_pad_x = 0.14; ov_base_y = 0.30
if value_has(j, "pad_x") != 0 { ov_pad_x = jnum(value_get(j, "pad_x")) }
if value_has(j, "base_y") != 0 { ov_base_y = jnum(value_get(j, "base_y")) }
let adv = value_get(j, "adv")
@ -123,14 +123,14 @@ function overlay_font(font_dir: string) -> bool {
var codes: Val = null
if value_has(j, "codes") != 0 { codes = value_get(j, "codes") }
ov_font_n = n
ov_font_adv = words(n + 1)
ov_font_adv = floats(n + 1)
for i in 0 .. n { ov_font_adv[i] = jnum(value_at(adv, i)) }
ov_font_map = words(OV_MAP_N)
for c in 0 .. OV_MAP_N { ov_font_map[c] = 0 }
ov_font_hi_cp = words(n + 1); ov_font_hi_g = words(n + 1); ov_font_hi_n = 0
for i in 0 .. n {
var cp = first + i
if codes != null and i < value_count(codes) { cp = f_to_int(jnum(value_at(codes, i))) }
if codes != null and i < value_count(codes) { cp = int(jnum(value_at(codes, i))) }
if cp >= 0 and cp < OV_MAP_N { ov_font_map[cp] = i + 1 }
else if cp >= OV_MAP_N {
# kept ascending: the builder writes code points in order, so this is an append
@ -195,12 +195,12 @@ function ov_begin() -> void {
gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
ov_pick_prog()
gpu_use_program(ov_prog)
u_f2(gpu_uniform(ov_prog, "u_screen"), fi(gl_w), fi(gl_h))
u_f2(gpu_uniform(ov_prog, "u_screen"), float(gl_w), float(gl_h))
ov_n = 0; ov_nr = 0; ov_range_start = 0
ov_tex = ov_white
ov_mode = 2; ov_voff = fi(8)
ov_mode = 2; ov_voff = 8.0
ov_clip_w = 0
ov_hdr_scale = F_ONE
ov_hdr_scale = 1.0
ov_open = true
}
# everything drawn until ov_unclip stays inside this rectangle (a scrolling list)
@ -238,7 +238,7 @@ function ov_flush() -> void {
if font == 0 { font = ov_white }
r3d_bind_2d(ov_prog, "u_font", 1, font)
var hs = ov_hdr_scale
if hs == 0 { hs = F_ONE }
if hs == 0.0 { hs = 1.0 }
u_f(gpu_uniform(ov_prog, "u_hdr_paper"), r3d_hdr_paper_nits())
u_f(gpu_uniform(ov_prog, "u_hdr_scale"), hs)
var last = -1
@ -268,21 +268,21 @@ function ov_end() -> void {
# vertex says which it reads (ov_voff). Only a different image texture closes the range. A panel and
# its label used to alternate the white and font textures, and the HUD was 77-91 draws a frame.
function ov_use_tex(t: int) -> void {
if t != 0 and t == ov_font { ov_mode = 1; ov_voff = fi(4); return }
if t == ov_white { ov_mode = 2; ov_voff = fi(8); return }
ov_mode = 0; ov_voff = F_ZERO
if t != 0 and t == ov_font { ov_mode = 1; ov_voff = 4.0; return }
if t == ov_white { ov_mode = 2; ov_voff = 8.0; return }
ov_mode = 0; ov_voff = 0.0
if t != ov_tex { ov_close_range(); ov_tex = t }
}
# one vertex into the batch
function ov_vert(k: int, x: int, y: int, u: int, v: int, r: int, g: int, b: int, a: int) -> void {
function ov_vert(k: int, x: float, y: float, u: float, v: float, r: float, g: float, b: float, a: float) -> void {
let o = k * OV_FLOATS
gl_put_bits(ov_buf, o, x); gl_put_bits(ov_buf, o + 1, y)
gl_put_bits(ov_buf, o + 2, u); gl_put_bits(ov_buf, o + 3, f_add(v, ov_voff))
gl_put_bits(ov_buf, o + 4, r); gl_put_bits(ov_buf, o + 5, g); gl_put_bits(ov_buf, o + 6, b); gl_put_bits(ov_buf, o + 7, a)
gl_put_bits(ov_buf, o, float_bits(x)); gl_put_bits(ov_buf, o + 1, float_bits(y))
gl_put_bits(ov_buf, o + 2, float_bits(u)); gl_put_bits(ov_buf, o + 3, float_bits(v + ov_voff))
gl_put_bits(ov_buf, o + 4, float_bits(r)); gl_put_bits(ov_buf, o + 5, float_bits(g)); gl_put_bits(ov_buf, o + 6, float_bits(b)); gl_put_bits(ov_buf, o + 7, float_bits(a))
}
# a textured quad, float-bit pixel corners and uvs
function ov_quad(x0: int, y0: int, x1: int, y1: int, u0: int, v0: int, u1: int, v1: int, r: int, g: int, b: int, a: int) -> void {
function ov_quad(x0: float, y0: float, x1: float, y1: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
if ov_n >= OV_MAX_QUADS { ov_flush() }
let k = ov_n * 6
ov_vert(k, x0, y0, u0, v0, r, g, b, a)
@ -295,46 +295,46 @@ function ov_quad(x0: int, y0: int, x1: int, y1: int, u0: int, v0: int, u1: int,
}
# a filled rectangle at integer pixels; colour as float bits 0..1
function ov_rect(x: int, y: int, w: int, h: int, r: int, g: int, b: int, a: int) -> void {
function ov_rect(x: int, y: int, w: int, h: int, r: float, g: float, b: float, a: float) -> void {
ov_use_tex(ov_white)
ov_quad(fi(x), fi(y), fi(x + w), fi(y + h), F_ZERO, F_ZERO, F_ONE, F_ONE, r, g, b, a)
ov_quad(float(x), float(y), float(x + w), float(y + h), 0.0, 0.0, 1.0, 1.0, r, g, b, a)
}
function ov_frame(x: int, y: int, w: int, h: int, t: int, r: int, g: int, b: int, a: int) -> void {
function ov_frame(x: int, y: int, w: int, h: int, t: int, r: float, g: float, b: float, a: float) -> void {
ov_rect(x, y, w, t, r, g, b, a)
ov_rect(x, y + h - t, w, t, r, g, b, a)
ov_rect(x, y, t, h, r, g, b, a)
ov_rect(x + w - t, y, t, h, r, g, b, a)
}
# a whole texture at integer pixels
function ov_image(tex: int, x: int, y: int, w: int, h: int, a: int) -> void {
function ov_image(tex: int, x: int, y: int, w: int, h: int, a: float) -> void {
ov_use_tex(tex)
ov_quad(fi(x), fi(y), fi(x + w), fi(y + h), F_ZERO, F_ZERO, F_ONE, F_ONE, F_ONE, F_ONE, F_ONE, a)
ov_quad(float(x), float(y), float(x + w), float(y + h), 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, a)
}
# the width in pixels of `s` at `size` pixels per em
function ov_text_w(size: int, s: string) -> int {
if ov_font_adv == null { return 0 }
var w = F_ZERO
var w = 0.0
let sp: pointer = s # UTF-8 bytes, not one-character strings
let n = len(sp)
var i = 0
while i < n {
let g = ov_glyph(ov_u8(sp, i, n))
i += ov_u8_len
w = f_add(w, f_mul(ov_font_adv[g], fi(size)))
w = w + ov_font_adv[g] * float(size)
}
return f_to_int(w)
return int(w)
}
# text with its top-left at (x, y); returns the pen x after it
function ov_text(x: int, y: int, size: int, s: string, r: int, g: int, b: int, a: int) -> int {
function ov_text(x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> int {
if ov_font == 0 { return x }
ov_use_tex(ov_font)
let k = fr(size, ov_font_em) # atlas px -> screen px
let cell = f_mul(fi(ov_font_cell), k)
var pen = fi(x)
let base = f_add(fi(y), f_mul(fi(size), fl(0.80)))
let px = f_mul(f_mul(ov_pad_x, fi(ov_font_em)), k)
let py = f_mul(f_mul(ov_base_y, fi(ov_font_em)), k)
let k = float(size) / float(ov_font_em) # atlas px -> screen px
let cell = float(ov_font_cell) * k
var pen = float(x)
let base = float(y) + float(size) * 0.80
let px = ov_pad_x * float(ov_font_em) * k
let py = ov_base_y * float(ov_font_em) * k
let sp: pointer = s
let n = len(sp)
var i = 0
@ -344,27 +344,27 @@ function ov_text(x: int, y: int, size: int, s: string, r: int, g: int, b: int, a
if c != ov_font_space {
let cx = c - (c / ov_font_cols) * ov_font_cols
let cy = c / ov_font_cols
let u0 = fr(cx, ov_font_cols); let u1 = fr(cx + 1, ov_font_cols)
let v0 = fr(cy, ov_font_rows); let v1 = fr(cy + 1, ov_font_rows)
let x0 = f_sub(pen, px); let y1 = f_add(base, py)
ov_quad(x0, f_sub(y1, cell), f_add(x0, cell), y1, u0, v0, u1, v1, r, g, b, a)
let u0 = float(cx) / float(ov_font_cols); let u1 = float(cx + 1) / float(ov_font_cols)
let v0 = float(cy) / float(ov_font_rows); let v1 = float(cy + 1) / float(ov_font_rows)
let x0 = pen - px; let y1 = base + py
ov_quad(x0, y1 - cell, x0 + cell, y1, u0, v0, u1, v1, r, g, b, a)
}
pen = f_add(pen, f_mul(ov_font_adv[c], fi(size)))
pen = pen + ov_font_adv[c] * float(size)
}
return f_to_int(pen)
return int(pen)
}
# text with a soft dark shadow under it (HUD over a bright meadow)
function ov_text_sh(x: int, y: int, size: int, s: string, r: int, g: int, b: int, a: int) -> int {
function ov_text_sh(x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> int {
let d = size / 18 + 1
ov_text(x + d, y + d, size, s, F_ZERO, F_ZERO, F_ZERO, f_mul(a, fl(0.7)))
ov_text(x + d, y + d, size, s, 0.0, 0.0, 0.0, a * 0.7)
return ov_text(x, y, size, s, r, g, b, a)
}
function ov_text_center(cx: int, y: int, size: int, s: string, r: int, g: int, b: int, a: int) -> void {
function ov_text_center(cx: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> void {
ov_text(cx - ov_text_w(size, s) / 2, y, size, s, r, g, b, a)
}
# text wrapped at `maxw` pixels on spaces; returns the y after the last line
function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: int, g: int, b: int, a: int) -> int {
function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: float, g: float, b: float, a: float) -> int {
let sp: pointer = s
let n = len(sp)
var first = 0
@ -391,27 +391,27 @@ function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: int, g
# ---- more shapes for a game's interface -------------------------------------------------
# a sub-rectangle of a texture (uv corners as float bits) tinted, at integer pixels
function ov_sub(tex: int, x: int, y: int, w: int, h: int, u0: int, v0: int, u1: int, v1: int, r: int, g: int, b: int, a: int) -> void {
function ov_sub(tex: int, x: int, y: int, w: int, h: int, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
ov_use_tex(tex)
ov_quad(fi(x), fi(y), fi(x + w), fi(y + h), u0, v0, u1, v1, r, g, b, a)
ov_quad(float(x), float(y), float(x + w), float(y + h), u0, v0, u1, v1, r, g, b, a)
}
# a whole texture stretched by nine slices: corners `src` texels wide in a `tw` px square
# texture, drawn `dst` pixels wide, so rounded corners keep their shape at any size
function ov_nine(tex: int, tw: int, src: int, x: int, y: int, w: int, h: int, dst: int, r: int, g: int, b: int, a: int) -> void {
function ov_nine(tex: int, tw: int, src: int, x: int, y: int, w: int, h: int, dst: int, r: float, g: float, b: float, a: float) -> void {
ov_use_tex(tex)
let s = fr(src, tw)
let xs = words(4); let ys = words(4); let us = words(4); let vs = words(4)
xs[0] = fi(x); xs[1] = fi(x + dst); xs[2] = fi(x + w - dst); xs[3] = fi(x + w)
ys[0] = fi(y); ys[1] = fi(y + dst); ys[2] = fi(y + h - dst); ys[3] = fi(y + h)
us[0] = F_ZERO; us[1] = s; us[2] = f_sub(F_ONE, s); us[3] = F_ONE
vs[0] = F_ZERO; vs[1] = s; vs[2] = f_sub(F_ONE, s); vs[3] = F_ONE
let s = float(src) / float(tw)
let xs = floats(4); let ys = floats(4); let us = floats(4); let vs = floats(4)
xs[0] = float(x); xs[1] = float(x + dst); xs[2] = float(x + w - dst); xs[3] = float(x + w)
ys[0] = float(y); ys[1] = float(y + dst); ys[2] = float(y + h - dst); ys[3] = float(y + h)
us[0] = 0.0; us[1] = s; us[2] = 1.0 - s; us[3] = 1.0
vs[0] = 0.0; vs[1] = s; vs[2] = 1.0 - s; vs[3] = 1.0
for j in 0 .. 3 {
for i in 0 .. 3 { ov_quad(xs[i], ys[j], xs[i + 1], ys[j + 1], us[i], vs[j], us[i + 1], vs[j + 1], r, g, b, a) }
}
free(xs); free(ys); free(us); free(vs)
}
# an arbitrary quad (float-bit pixel corners, clockwise from top-left) of a texture
function ov_quad4(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, x3: int, y3: int, u0: int, v0: int, u1: int, v1: int, r: int, g: int, b: int, a: int) -> void {
function ov_quad4(x0: float, y0: float, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
if ov_n >= OV_MAX_QUADS { ov_flush() }
let k = ov_n * 6
ov_vert(k, x0, y0, u0, v0, r, g, b, a)
@ -423,42 +423,42 @@ function ov_quad4(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, x3: int,
ov_n += 1
}
# a line of thickness `t` pixels between two points (float-bit pixels)
function ov_line(x0: int, y0: int, x1: int, y1: int, t: int, r: int, g: int, b: int, a: int) -> void {
function ov_line(x0: float, y0: float, x1: float, y1: float, t: float, r: float, g: float, b: float, a: float) -> void {
ov_use_tex(ov_white)
let dx = f_sub(x1, x0); let dy = f_sub(y1, y0)
let l = f_max(f_sqrt(f_add(f_mul(dx, dx), f_mul(dy, dy))), fl(0.001))
let nx = f_mul(f_div(f_neg(dy), l), f_mul(t, F_HALF)); let ny = f_mul(f_div(dx, l), f_mul(t, F_HALF))
ov_quad4(f_add(x0, nx), f_add(y0, ny), f_add(x1, nx), f_add(y1, ny), f_sub(x1, nx), f_sub(y1, ny), f_sub(x0, nx), f_sub(y0, ny), F_ZERO, F_ZERO, F_ONE, F_ONE, r, g, b, a)
let dx = x1 - x0; let dy = y1 - y0
let l = Math.max(Math.sqrt(dx * dx + dy * dy), 0.001)
let nx = -dy / l * (t * 0.5); let ny = dx / l * (t * 0.5)
ov_quad4(x0 + nx, y0 + ny, x1 + nx, y1 + ny, x1 - nx, y1 - ny, x0 - nx, y0 - ny, 0.0, 0.0, 1.0, 1.0, r, g, b, a)
}
# a sub-rectangle of a texture rotated by `ang` radians about its centre (cx, cy), `w` x `h` pixels
function ov_sub_rot(tex: int, cx: int, cy: int, w: int, h: int, ang: int, u0: int, v0: int, u1: int, v1: int, r: int, g: int, b: int, a: int) -> void {
function ov_sub_rot(tex: int, cx: int, cy: int, w: int, h: int, ang: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> void {
ov_use_tex(tex)
let c = f_cos(ang); let s = f_sin(ang)
let hw = f_mul(fi(w), F_HALF); let hh = f_mul(fi(h), F_HALF)
let fx = fi(cx); let fy = fi(cy)
let c = Math.cos(ang); let s = Math.sin(ang)
let hw = float(w) * 0.5; let hh = float(h) * 0.5
let fx = float(cx); let fy = float(cy)
# corners: (-hw,-hh) (hw,-hh) (hw,hh) (-hw,hh) rotated
let x0 = f_add(fx, f_sub(f_mul(f_neg(hw), c), f_mul(f_neg(hh), s))); let y0 = f_add(fy, f_add(f_mul(f_neg(hw), s), f_mul(f_neg(hh), c)))
let x1 = f_add(fx, f_sub(f_mul(hw, c), f_mul(f_neg(hh), s))); let y1 = f_add(fy, f_add(f_mul(hw, s), f_mul(f_neg(hh), c)))
let x2 = f_add(fx, f_sub(f_mul(hw, c), f_mul(hh, s))); let y2 = f_add(fy, f_add(f_mul(hw, s), f_mul(hh, c)))
let x3 = f_add(fx, f_sub(f_mul(f_neg(hw), c), f_mul(hh, s))); let y3 = f_add(fy, f_add(f_mul(f_neg(hw), s), f_mul(hh, c)))
let x0 = fx + (-hw * c - -hh * s); let y0 = fy + (-hw * s + -hh * c)
let x1 = fx + (hw * c - -hh * s); let y1 = fy + (hw * s + -hh * c)
let x2 = fx + (hw * c - hh * s); let y2 = fy + (hw * s + hh * c)
let x3 = fx + (-hw * c - hh * s); let y3 = fy + (-hw * s + hh * c)
ov_quad4(x0, y0, x1, y1, x2, y2, x3, y3, u0, v0, u1, v1, r, g, b, a)
}
# a filled circle approximated by `n` wedges (float-bit centre and radius)
function ov_disc(cx: int, cy: int, rad: int, n: int, r: int, g: int, b: int, a: int) -> void {
function ov_disc(cx: float, cy: float, rad: float, n: int, r: float, g: float, b: float, a: float) -> void {
ov_use_tex(ov_white)
let step = f_div(f_mul(F_TWO, F_PI), fi(n))
let step = 2.0 * PI / float(n)
for i in 0 .. n {
let a0 = f_mul(fi(i), step); let a1 = f_add(a0, step)
let ax = f_add(cx, f_mul(f_cos(a0), rad)); let ay = f_add(cy, f_mul(f_sin(a0), rad))
let bx = f_add(cx, f_mul(f_cos(a1), rad)); let by = f_add(cy, f_mul(f_sin(a1), rad))
ov_quad4(cx, cy, ax, ay, bx, by, cx, cy, F_ZERO, F_ZERO, F_ONE, F_ONE, r, g, b, a)
let a0 = float(i) * step; let a1 = a0 + step
let ax = cx + Math.cos(a0) * rad; let ay = cy + Math.sin(a0) * rad
let bx = cx + Math.cos(a1) * rad; let by = cy + Math.sin(a1) * rad
ov_quad4(cx, cy, ax, ay, bx, by, cx, cy, 0.0, 0.0, 1.0, 1.0, r, g, b, a)
}
}
# a ring: `n` segments of thickness `t`, from angle a0 for `span` radians (float bits)
function ov_arc(cx: int, cy: int, rad: int, t: int, a0: int, span: int, n: int, r: int, g: int, b: int, a: int) -> void {
let step = f_div(span, fi(n))
function ov_arc(cx: float, cy: float, rad: float, t: float, a0: float, span: float, n: int, r: float, g: float, b: float, a: float) -> void {
let step = span / float(n)
for i in 0 .. n {
let b0 = f_add(a0, f_mul(fi(i), step)); let b1 = f_add(b0, step)
ov_line(f_add(cx, f_mul(f_cos(b0), rad)), f_add(cy, f_mul(f_sin(b0), rad)), f_add(cx, f_mul(f_cos(b1), rad)), f_add(cy, f_mul(f_sin(b1), rad)), t, r, g, b, a)
let b0 = a0 + float(i) * step; let b1 = b0 + step
ov_line(cx + Math.cos(b0) * rad, cy + Math.sin(b0) * rad, cx + Math.cos(b1) * rad, cy + Math.sin(b1) * rad, t, r, g, b, a)
}
}

View file

@ -14,19 +14,19 @@ var post_p_down: int = 0
var post_p_up: int = 0
var post_p_tone: int = 0
var post_fs: Mesh = null
var post_exposure: int = 0
var post_exposure: float = 0.0
var post_bloom_strength: int = 0
var post_vignette: int = 0
var post_saturation: int = 0
var post_contrast: int = 0
var post_vignette: float = 0.0
var post_saturation: float = 0.0
var post_contrast: float = 0.0
var post_w: int = 0
var post_h: int = 0
var post_auto: bool = true
var post_key: int = 0 # target mean luminance after exposure (float bits)
var post_key: float = 0.0 # target mean luminance after exposure (float bits)
var post_lum: words = null
var post_mips: int = 0
var post_adapt: int = 0 # smoothed exposure (float bits)
var post_exposure_max: int = 0x41A00000 # 20: the ceiling auto-exposure may reach (night lowers it)
var post_adapt: float = 0.0 # smoothed exposure (float bits)
var post_exposure_max: float = 20.0 # 20: the ceiling auto-exposure may reach (night lowers it)
# ---- the grade ------------------------------------------------------------------------------
# White balance, the shadows' floor and the highlights' gain. These were nine literals bound at
# the draw, so the game had the same colour at seven in the morning as at one in the afternoon -
@ -35,25 +35,25 @@ var post_exposure_max: int = 0x41A00000 # 20: the ceiling auto-exposure may re
# program that never starts a day looks exactly as it did.
# They are plain ints rather than a v3 on purpose: a grade is written by daylight_set, which can
# run before post_init has allocated anything, and nine ints cannot be null.
var post_wb_r: int = 0x3F828F5C # 1.02
var post_wb_g: int = 0x3F800000 # 1.0
var post_wb_b: int = 0x3F7851EC # 0.97
var post_lift_r: int = 0x3B83126F # 0.004
var post_lift_g: int = 0x3B83126F # 0.004
var post_lift_b: int = 0x3C449BA6 # 0.012
var post_gain_r: int = 0x3F7D70A4 # 0.99
var post_gain_g: int = 0x3F7EB852 # 0.995
var post_gain_b: int = 0x3F800000 # 1.0
var post_wb_r: float = 1.02 # 1.02
var post_wb_g: float = 1.0 # 1.0
var post_wb_b: float = 0.97 # 0.97
var post_lift_r: float = 0.004 # 0.004
var post_lift_g: float = 0.004 # 0.004
var post_lift_b: float = 0.012 # 0.012
var post_gain_r: float = 0.99 # 0.99
var post_gain_g: float = 0.995 # 0.995
var post_gain_b: float = 1.0 # 1.0
var post_ao: Target = null
var post_ao_blur: Target = null
var post_p_ao: int = 0
var post_p_ao_blur: int = 0
var post_ao_radius: int = 0
var post_contact: int = 0x3FA00000 # 1.25: how hard a thing is darkened where it meets the ground
var post_ao_intensity: int = 0
var post_ao_strength: int = 0
var post_gi_strength: int = 0x3ECCCCCD # 0.4
var post_ao_radius: float = 0.0
var post_contact: float = 1.25 # 1.25: how hard a thing is darkened where it meets the ground
var post_ao_intensity: float = 0.0
var post_ao_strength: float = 0.0
var post_gi_strength: float = 0.4 # 0.4
var post_no_gi: bool = false
var post_ldr: Target = null
var post_depth_copy: Target = null
@ -63,13 +63,13 @@ var post_depth_copy: Target = null
# and the pass is skipped entirely rather than run at one step.
var post_vol: Target = null
var post_p_vol: int = 0
var post_vol_steps: int = 0x41C00000 # 24
var post_vol_density: int = 0x3B03126F # 0.002
var post_vol_falloff: int = 0x3BA3D70A # 0.005
var post_vol_far: int = 0x44480000 # 800 m
var post_vol_g: int = 0x3F19999A # 0.6: air throws light forward
var post_vol_mist: int = 0 # the day sets these two
var post_vol_mist_h: int = 0x42200000 # 40 m
var post_vol_steps: float = 24.0 # 24
var post_vol_density: float = 0.002 # 0.002
var post_vol_falloff: float = 0.005 # 0.005
var post_vol_far: float = 800.0 # 800 m
var post_vol_g: float = 0.6 # 0.6: air throws light forward
var post_vol_mist: float = 0.0 # the day sets these two
var post_vol_mist_h: float = 40.0 # 40 m
var post_prev: Target = null # last frame's scene colour, for the SSGI bounce only
var post_scene: Target = null # this frame's scene colour before the water, for refraction
var post_frame: int = 0
@ -79,20 +79,20 @@ var post_color: int = 0 # the HDR colour the rest of post reads
var post_p_sharp: int = 0
# Spatial anti-aliasing, in the sharpen pass because that pass already reads this pixel's
# neighbourhood and runs last on the LDR image. 1 on, 0 off; the game's setting drives it.
var post_fxaa: int = 0x3F800000
var post_fxaa: float = 1.0
# ---- depth of field ------------------------------------------------------------------------
# Off in ordinary play - the pass is skipped whole, not run at zero radius. The game turns it on
# behind the viewfinder and says what to focus on.
var post_p_dof: int = 0
var post_dof: Target = null
var post_dof_focus: int = 0x41200000 # 10 m
var post_dof_aperture: int = 0 # 0 = no lens, and no pass
var post_dof_max: int = 0x41400000 # 12 px
var post_dof_focus: float = 10.0 # 10 m
var post_dof_aperture: float = 0.0 # 0 = no lens, and no pass
var post_dof_max: float = 12.0 # 12 px
var post_p_tone_hdr: int = 0 # the tonemap's HDR10 variant, made the first time HDR is on
var post_ldr_hdr: bool = false # post_ldr was made for HDR10 output (10-bit)
# the LDR image is 10-bit while the output is HDR10: PQ in 8 bits bands
function post_ldr_fmt() -> int { if gpu_hdr_active() { return GL_RGB10_A2 }; return GL_RGBA8 }
var post_sharpen: int = 0
var post_sharpen: float = 0.0
var post_grain: int = 0
# the screen-sized targets go away before post_init makes them at a new size
@ -168,23 +168,23 @@ function post_init(w: int, h: int) -> void {
post_prev = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
post_scene = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
if post_p_sharp == 0 { post_p_sharp = r3d_program("fullscreen.vert", "sharpen.frag", "") }
post_sharpen = fl(1.2)
post_grain = fl(0.025)
post_ao_radius = fl(0.7)
post_ao_intensity = fl(1.4)
post_ao_strength = fl(0.8)
post_sharpen = 1.2
post_grain = float_bits(0.025)
post_ao_radius = 0.7
post_ao_intensity = 1.4
post_ao_strength = 0.8
post_fs = mesh_fullscreen()
post_exposure = fl(0.36)
post_bloom_strength = fl(0.06)
post_vignette = fl(0.35)
post_saturation = fl(1.04)
post_contrast = fl(1.12)
post_key = fl(0.19)
post_exposure = 0.36
post_bloom_strength = float_bits(0.06)
post_vignette = 0.35
post_saturation = 1.04
post_contrast = 1.12
post_key = 0.19
post_lum = words(4)
var m = 1; var sz = max(w, h)
while sz > 1 { sz = sz / 2; m += 1 }
post_mips = m
post_adapt = F_ZERO
post_adapt = 0.0
}
# Mean scene luminance from the HDR mip chain -> exposure = key / mean, eased over
@ -217,12 +217,12 @@ function post_measure() -> void {
gpu_use_program(post_p_adapt)
r3d_bind_2d(post_p_adapt, "u_scene", 0, post_hdr.color)
r3d_bind_2d(post_p_adapt, "u_prev", 1, post_adapt_t[post_adapt_i].color)
u_f(gpu_uniform(post_p_adapt, "u_lod"), fi(post_mips - 1))
u_f(gpu_uniform(post_p_adapt, "u_lod"), float(post_mips - 1))
u_f(gpu_uniform(post_p_adapt, "u_key"), post_key)
u_f(gpu_uniform(post_p_adapt, "u_max"), post_exposure_max)
u_f(gpu_uniform(post_p_adapt, "u_rate"), fl(0.08))
var reset = F_ZERO
if post_adapt_reset { reset = F_ONE; post_adapt_reset = false }
u_f(gpu_uniform(post_p_adapt, "u_rate"), 0.08)
var reset = 0.0
if post_adapt_reset { reset = 1.0; post_adapt_reset = false }
u_f(gpu_uniform(post_p_adapt, "u_reset"), reset)
mesh_draw(post_fs)
post_adapt_i = next
@ -292,21 +292,21 @@ function post_ssao_pass() -> void {
gpu_use_program(post_p_ao)
r3d_bind_2d(post_p_ao, "u_depth", 0, post_hdr.depth)
r3d_bind_2d(post_p_ao, "u_prev_color", 1, post_prev.color)
u_f(gpu_uniform(post_p_ao, "u_frame"), fi(post_frame % 64))
u_f(gpu_uniform(post_p_ao, "u_frame"), float(post_frame % 64))
u_mat4(gpu_uniform(post_p_ao, "u_inv_proj"), cam_inv_proj)
u_mat4(gpu_uniform(post_p_ao, "u_proj"), cam_proj)
u_f2(gpu_uniform(post_p_ao, "u_texel"), fr(1, post_w), fr(1, post_h))
u_f2(gpu_uniform(post_p_ao, "u_texel"), 1.0 / float(post_w), 1.0 / float(post_h))
u_f(gpu_uniform(post_p_ao, "u_radius"), post_ao_radius)
u_f(gpu_uniform(post_p_ao, "u_intensity"), post_ao_intensity)
var contact = post_contact
if r3d_env_has("R3D_NOCONTACT") { contact = F_ZERO }
if r3d_env_has("R3D_NOCONTACT") { contact = 0.0 }
u_f(gpu_uniform(post_p_ao, "u_contact"), contact)
mesh_draw(post_fs)
target_bind(post_ao_blur)
gpu_use_program(post_p_ao_blur)
r3d_bind_2d(post_p_ao_blur, "u_ao", 0, post_ao.color)
r3d_bind_2d(post_p_ao_blur, "u_depth", 1, post_hdr.depth)
u_f2(gpu_uniform(post_p_ao_blur, "u_texel"), fr(1, post_ao.w), fr(1, post_ao.h))
u_f2(gpu_uniform(post_p_ao_blur, "u_texel"), 1.0 / float(post_ao.w), 1.0 / float(post_ao.h))
mesh_draw(post_fs)
}
@ -314,7 +314,7 @@ function post_ssao_pass() -> void {
# light is a bright thing in the air and should bloom like one, and compositing it after the
# bloom pyramid would give hard-edged rays with no glow at all.
function post_volumetric_pass() -> void {
if post_vol_steps <= 0 { return }
if post_vol_steps <= 0.0 { return }
if r3d_env_has("R3D_NOVOL") { return }
gpu_depth_test(false)
gpu_blend(false)
@ -346,8 +346,8 @@ function post_volumetric_pass() -> void {
gpu_blend_func(GL_ONE, GL_ONE)
gpu_use_program(post_p_up)
r3d_bind_2d(post_p_up, "u_src", 0, post_vol.color)
u_f2(gpu_uniform(post_p_up, "u_texel"), fr(1, post_vol.w), fr(1, post_vol.h))
u_f(gpu_uniform(post_p_up, "u_radius"), F_ONE)
u_f2(gpu_uniform(post_p_up, "u_texel"), 1.0 / float(post_vol.w), 1.0 / float(post_vol.h))
u_f(gpu_uniform(post_p_up, "u_radius"), 1.0)
mesh_draw(post_fs)
gpu_blend(false)
}
@ -355,7 +355,7 @@ function post_volumetric_pass() -> void {
# The lens, between the scene and the bloom: a blurred highlight should still bloom, and a
# bloom smeared by the lens afterwards would be a halo round nothing.
function post_dof_pass() -> void {
if post_dof_aperture == 0 { return }
if post_dof_aperture == 0.0 { return }
gpu_depth_test(false)
gpu_blend(false)
target_bind(post_dof)
@ -363,7 +363,7 @@ function post_dof_pass() -> void {
r3d_bind_2d(post_p_dof, "u_src", 0, post_color)
r3d_bind_2d(post_p_dof, "u_depth", 1, post_hdr.depth)
u_mat4(gpu_uniform(post_p_dof, "u_inv_proj"), cam_inv_proj)
u_f2(gpu_uniform(post_p_dof, "u_texel"), fr(1, post_w), fr(1, post_h))
u_f2(gpu_uniform(post_p_dof, "u_texel"), 1.0 / float(post_w), 1.0 / float(post_h))
u_f(gpu_uniform(post_p_dof, "u_focus"), post_dof_focus)
u_f(gpu_uniform(post_p_dof, "u_aperture"), post_dof_aperture)
u_f(gpu_uniform(post_p_dof, "u_max_coc"), post_dof_max)
@ -381,9 +381,9 @@ function post_bloom_pass() -> void {
let t = post_bloom[i]
target_bind(t)
r3d_bind_2d(post_p_down, "u_src", 0, src)
u_f2(gpu_uniform(post_p_down, "u_texel"), fr(1, sw), fr(1, sh))
var th = f_neg1()
if i == 0 { th = fl(1.2) }
u_f2(gpu_uniform(post_p_down, "u_texel"), 1.0 / float(sw), 1.0 / float(sh))
var th = -1.0
if i == 0 { th = 1.2 }
u_f(gpu_uniform(post_p_down, "u_threshold"), th)
mesh_draw(post_fs)
src = t.color; sw = t.w; sh = t.h
@ -397,8 +397,8 @@ function post_bloom_pass() -> void {
let to = post_bloom[i - 1]
target_bind(to)
r3d_bind_2d(post_p_up, "u_src", 0, from.color)
u_f2(gpu_uniform(post_p_up, "u_texel"), fr(1, from.w), fr(1, from.h))
u_f(gpu_uniform(post_p_up, "u_radius"), F_ONE)
u_f2(gpu_uniform(post_p_up, "u_texel"), 1.0 / float(from.w), 1.0 / float(from.h))
u_f(gpu_uniform(post_p_up, "u_radius"), 1.0)
mesh_draw(post_fs)
i -= 1
}
@ -427,10 +427,10 @@ function post_tonemap(color_tex: int) -> void {
u_f(gpu_uniform(prog, "u_ao_strength"), post_ao_strength)
u_f(gpu_uniform(prog, "u_gi_strength"), post_gi_strength)
u_f(gpu_uniform(prog, "u_exposure"), post_exposure)
var auto = F_ZERO
if post_auto and post_adapt_t != null { auto = F_ONE; r3d_bind_2d(prog, "u_adapt", 3, post_adapt_t[post_adapt_i].color) }
var auto = 0.0
if post_auto and post_adapt_t != null { auto = 1.0; r3d_bind_2d(prog, "u_adapt", 3, post_adapt_t[post_adapt_i].color) }
u_f(gpu_uniform(prog, "u_auto"), auto)
u_f(gpu_uniform(prog, "u_bloom_strength"), post_bloom_strength)
u_f(gpu_uniform(prog, "u_bloom_strength"), float_from_bits(post_bloom_strength))
u_f(gpu_uniform(prog, "u_vignette"), post_vignette)
u_f(gpu_uniform(prog, "u_saturation"), post_saturation)
u_f(gpu_uniform(prog, "u_contrast"), post_contrast)
@ -447,15 +447,15 @@ function post_tonemap(color_tex: int) -> void {
gpu_viewport(0, 0, gl_w, gl_h)
gpu_use_program(post_p_sharp)
r3d_bind_2d(post_p_sharp, "u_src", 0, post_ldr.color)
u_f2(gpu_uniform(post_p_sharp, "u_texel"), fr(1, post_ldr.w), fr(1, post_ldr.h))
u_f2(gpu_uniform(post_p_sharp, "u_texel"), 1.0 / float(post_ldr.w), 1.0 / float(post_ldr.h))
u_f(gpu_uniform(post_p_sharp, "u_amount"), post_sharpen)
var fx = post_fxaa
if r3d_env_has("R3D_NOFXAA") { fx = F_ZERO }
if r3d_env_has("R3D_NOFXAA") { fx = 0.0 }
u_f(gpu_uniform(post_p_sharp, "u_fxaa"), fx)
# R3D_NOGRAIN=1: no film grain, so two frames of a still camera can be compared for what else moves
var grain = post_grain
if r3d_env_has("R3D_NOGRAIN") { grain = F_ZERO }
u_f(gpu_uniform(post_p_sharp, "u_grain"), grain)
if r3d_env_has("R3D_NOGRAIN") { grain = float_bits(0.0) }
u_f(gpu_uniform(post_p_sharp, "u_grain"), float_from_bits(grain))
u_f(gpu_uniform(post_p_sharp, "u_time"), r3d_time)
mesh_draw(post_fs)
}

View file

@ -4,92 +4,92 @@
# major matrices, o may alias its inputs unless stated.
# ============================================================================
function q_new() -> words { let q = words(4); q_identity(q); return q }
function q_identity(q: words) -> void { q[0] = F_ZERO; q[1] = F_ZERO; q[2] = F_ZERO; q[3] = F_ONE }
function q_new() -> floats { let q = floats(4); q_identity(q); return q }
function q_identity(q: floats) -> void { q[0] = 0.0; q[1] = 0.0; q[2] = 0.0; q[3] = 1.0 }
function q_set(q: words, x: int, y: int, z: int, w: int) -> void { q[0] = x; q[1] = y; q[2] = z; q[3] = w }
function q_copy(o: words, a: words) -> void { o[0] = a[0]; o[1] = a[1]; o[2] = a[2]; o[3] = a[3] }
function q_copy(o: floats, a: floats) -> void { o[0] = a[0]; o[1] = a[1]; o[2] = a[2]; o[3] = a[3] }
# the idx-th quaternion of a packed buffer
function q_load(o: words, src: words, idx: int) -> void { for i in 0 .. 4 { o[i] = src[idx * 4 + i] } }
function q_store(dst: words, idx: int, a: words) -> void { for i in 0 .. 4 { dst[idx * 4 + i] = a[i] } }
function q_load(o: floats, src: floats, idx: int) -> void { for i in 0 .. 4 { o[i] = src[idx * 4 + i] } }
function q_store(dst: floats, idx: int, a: floats) -> void { for i in 0 .. 4 { dst[idx * 4 + i] = a[i] } }
# a rotation of `angle` radians about the unit axis (ax, ay, az)
function q_axis_angle(o: words, ax: int, ay: int, az: int, angle: int) -> void {
let h = f_mul(angle, F_HALF)
let s = f_sin(h)
o[0] = f_mul(ax, s); o[1] = f_mul(ay, s); o[2] = f_mul(az, s); o[3] = f_cos(h)
function q_axis_angle(o: floats, ax: float, ay: float, az: float, angle: float) -> void {
let h = angle * 0.5
let s = Math.sin(h)
o[0] = ax * s; o[1] = ay * s; o[2] = az * s; o[3] = Math.cos(h)
}
# o = a * b (apply b first, then a); o may alias a or b
function q_mul(o: words, a: words, b: words) -> void {
function q_mul(o: floats, a: floats, b: floats) -> void {
let ax = a[0]; let ay = a[1]; let az = a[2]; let aw = a[3]
let bx = b[0]; let by = b[1]; let bz = b[2]; let bw = b[3]
let x = f_sub(f_add(f_add(f_mul(aw, bx), f_mul(ax, bw)), f_mul(ay, bz)), f_mul(az, by))
let y = f_add(f_add(f_sub(f_mul(aw, by), f_mul(ax, bz)), f_mul(ay, bw)), f_mul(az, bx))
let z = f_add(f_sub(f_add(f_mul(aw, bz), f_mul(ax, by)), f_mul(ay, bx)), f_mul(az, bw))
let w = f_sub(f_sub(f_sub(f_mul(aw, bw), f_mul(ax, bx)), f_mul(ay, by)), f_mul(az, bz))
let x = aw * bx + ax * bw + ay * bz - az * by
let y = aw * by - ax * bz + ay * bw + az * bx
let z = aw * bz + ax * by - ay * bx + az * bw
let w = aw * bw - ax * bx - ay * by - az * bz
o[0] = x; o[1] = y; o[2] = z; o[3] = w
}
function q_conj(o: words, a: words) -> void { o[0] = f_neg(a[0]); o[1] = f_neg(a[1]); o[2] = f_neg(a[2]); o[3] = a[3] }
function q_normalize(q: words) -> void {
let l = f_sqrt(f_add(f_add(f_mul(q[0], q[0]), f_mul(q[1], q[1])), f_add(f_mul(q[2], q[2]), f_mul(q[3], q[3]))))
if l == 0 { q_identity(q); return }
let inv = f_div(F_ONE, l)
for i in 0 .. 4 { q[i] = f_mul(q[i], inv) }
function q_conj(o: floats, a: floats) -> void { o[0] = -a[0]; o[1] = -a[1]; o[2] = -a[2]; o[3] = a[3] }
function q_normalize(q: floats) -> void {
let l = Math.sqrt(q[0] * q[0] + q[1] * q[1] + (q[2] * q[2] + q[3] * q[3]))
if l == 0.0 { q_identity(q); return }
let inv = 1.0 / l
for i in 0 .. 4 { q[i] = q[i] * inv }
}
# normalised linear blend from a to b (shortest arc), fine for the small steps a pose takes
function q_nlerp(o: words, a: words, b: words, t: int) -> void {
var d = f_add(f_add(f_mul(a[0], b[0]), f_mul(a[1], b[1])), f_add(f_mul(a[2], b[2]), f_mul(a[3], b[3])))
var sg = F_ONE
if f_ls(d, F_ZERO) { sg = f_neg(F_ONE) }
for i in 0 .. 4 { o[i] = f_lerp(a[i], f_mul(b[i], sg), t) }
function q_nlerp(o: floats, a: floats, b: floats, t: float) -> void {
var d = a[0] * b[0] + a[1] * b[1] + (a[2] * b[2] + a[3] * b[3])
var sg = 1.0
if d < 0.0 { sg = -1.0 }
for i in 0 .. 4 { o[i] = Math.lerp(a[i], b[i] * sg, t) }
q_normalize(o)
}
# rotate the vector v by q: o = q v q*
function q_rotate(o: words, q: words, v: words) -> void {
function q_rotate(o: floats, q: floats, v: floats) -> void {
let qx = q[0]; let qy = q[1]; let qz = q[2]; let qw = q[3]
# t = 2 * cross(q.xyz, v)
let tx = f_mul(F_TWO, f_sub(f_mul(qy, v[2]), f_mul(qz, v[1])))
let ty = f_mul(F_TWO, f_sub(f_mul(qz, v[0]), f_mul(qx, v[2])))
let tz = f_mul(F_TWO, f_sub(f_mul(qx, v[1]), f_mul(qy, v[0])))
let tx = 2.0 * (qy * v[2] - qz * v[1])
let ty = 2.0 * (qz * v[0] - qx * v[2])
let tz = 2.0 * (qx * v[1] - qy * v[0])
# o = v + w t + cross(q.xyz, t)
let x = f_add(f_add(v[0], f_mul(qw, tx)), f_sub(f_mul(qy, tz), f_mul(qz, ty)))
let y = f_add(f_add(v[1], f_mul(qw, ty)), f_sub(f_mul(qz, tx), f_mul(qx, tz)))
let z = f_add(f_add(v[2], f_mul(qw, tz)), f_sub(f_mul(qx, ty), f_mul(qy, tx)))
let x = v[0] + qw * tx + (qy * tz - qz * ty)
let y = v[1] + qw * ty + (qz * tx - qx * tz)
let z = v[2] + qw * tz + (qx * ty - qy * tx)
o[0] = x; o[1] = y; o[2] = z
}
# pitch about X, yaw about Y, roll about Z, composed as yaw * pitch * roll
var q_scratch: words = null
function q_euler(o: words, pitch: int, yaw: int, roll: int) -> void {
if q_scratch == null { q_scratch = words(16) }
var q_scratch: floats = null
function q_euler(o: floats, pitch: float, yaw: float, roll: float) -> void {
if q_scratch == null { q_scratch = floats(16) }
let qx = q_scratch; let qy = mem_off(q_scratch, 16); let qz = mem_off(q_scratch, 32); let t = mem_off(q_scratch, 48)
q_axis_angle(qx, F_ONE, F_ZERO, F_ZERO, pitch)
q_axis_angle(qy, F_ZERO, F_ONE, F_ZERO, yaw)
q_axis_angle(qz, F_ZERO, F_ZERO, F_ONE, roll)
q_axis_angle(qx, 1.0, 0.0, 0.0, pitch)
q_axis_angle(qy, 0.0, 1.0, 0.0, yaw)
q_axis_angle(qz, 0.0, 0.0, 1.0, roll)
q_mul(t, qy, qx)
q_mul(o, t, qz)
}
# the rotation matrix of q (column-major, translation cleared)
function q_to_m4(m: words, q: words) -> void {
function q_to_m4(m: floats, q: floats) -> void {
let x = q[0]; let y = q[1]; let z = q[2]; let w = q[3]
let xx = f_mul(x, x); let yy = f_mul(y, y); let zz = f_mul(z, z)
let xy = f_mul(x, y); let xz = f_mul(x, z); let yz = f_mul(y, z)
let wx = f_mul(w, x); let wy = f_mul(w, y); let wz = f_mul(w, z)
m[0] = f_sub(F_ONE, f_mul(F_TWO, f_add(yy, zz)))
m[1] = f_mul(F_TWO, f_add(xy, wz))
m[2] = f_mul(F_TWO, f_sub(xz, wy))
m[3] = F_ZERO
m[4] = f_mul(F_TWO, f_sub(xy, wz))
m[5] = f_sub(F_ONE, f_mul(F_TWO, f_add(xx, zz)))
m[6] = f_mul(F_TWO, f_add(yz, wx))
m[7] = F_ZERO
m[8] = f_mul(F_TWO, f_add(xz, wy))
m[9] = f_mul(F_TWO, f_sub(yz, wx))
m[10] = f_sub(F_ONE, f_mul(F_TWO, f_add(xx, yy)))
m[11] = F_ZERO
m[12] = F_ZERO; m[13] = F_ZERO; m[14] = F_ZERO; m[15] = F_ONE
let xx = x * x; let yy = y * y; let zz = z * z
let xy = x * y; let xz = x * z; let yz = y * z
let wx = w * x; let wy = w * y; let wz = w * z
m[0] = 1.0 - 2.0 * (yy + zz)
m[1] = 2.0 * (xy + wz)
m[2] = 2.0 * (xz - wy)
m[3] = 0.0
m[4] = 2.0 * (xy - wz)
m[5] = 1.0 - 2.0 * (xx + zz)
m[6] = 2.0 * (yz + wx)
m[7] = 0.0
m[8] = 2.0 * (xz + wy)
m[9] = 2.0 * (yz - wx)
m[10] = 1.0 - 2.0 * (xx + yy)
m[11] = 0.0
m[12] = 0.0; m[13] = 0.0; m[14] = 0.0; m[15] = 1.0
}
# m = translate(t) * rotate(q) * scale(s)
function m4_trs_q(m: words, tx: int, ty: int, tz: int, q: words, sx: int, sy: int, sz: int) -> void {
function m4_trs_q(m: floats, tx: float, ty: float, tz: float, q: floats, sx: float, sy: float, sz: float) -> void {
q_to_m4(m, q)
for r in 0 .. 3 { m[r] = f_mul(m[r], sx); m[4 + r] = f_mul(m[4 + r], sy); m[8 + r] = f_mul(m[8 + r], sz) }
for r in 0 .. 3 { m[r] = m[r] * sx; m[4 + r] = m[4 + r] * sy; m[8 + r] = m[8 + r] * sz }
m[12] = tx; m[13] = ty; m[14] = tz
}

View file

@ -6,44 +6,44 @@
# ============================================================================
var r3d_sky_prog: int = 0
var r3d_fog_density: int = 0
var r3d_fog_scale: int = 0x3F800000 # float bits: a setting's multiplier over the density the day sets
var r3d_fog_falloff: int = 0
var r3d_fog_base: int = 0 # the height fog is measured from (float bits); 0 = y = 0
var r3d_fog_density: float = 0.0
var r3d_fog_scale: float = 1.0 # float bits: a setting's multiplier over the density the day sets
var r3d_fog_falloff: float = 0.0
var r3d_fog_base: float = 0.0 # the height fog is measured from (float bits); 0 = y = 0
# Both of these are the DAY's, set by daylight_set from the sun's own elevation. The inscatter
# is how hard the air scatters the sun forward - nearly nothing at noon, and the glow a ridge
# is silhouetted against at dusk. The desaturation is how fast distance takes a surface's own
# colour away, which is the term that was missing entirely and is most of why a midday frame
# had no depth in it at all.
var r3d_fog_inscatter: int = 0x3CA3D70A # 0.02, what the shader used to hard-code
var r3d_fog_desat: int = 0x3FACCCCD # 1.35
var r3d_fog_inscatter: float = 0.02 # 0.02, what the shader used to hard-code
var r3d_fog_desat: float = 1.35 # 1.35
# How much of the visible sky's colour comes from the analytic model rather than from the
# photograph (sky.frag). The day fades it in: at night the sky has its own tint, a star field
# and a moon, and relighting on top of those would only wash them out.
var r3d_sky_relight: int = 0
var r3d_sky_relight: float = 0.0
# What the ground reflects back up, low and high, and the height they cross at. The defaults
# are Maroon Lake's - a green basin under a grey-rock treeline at about 480 m over the datum.
var r3d_ground_alb_r: int = 0x3E99999A; var r3d_ground_alb_g: int = 0x3EAE147B; var r3d_ground_alb_b: int = 0x3E0F5C29
var r3d_ground_hi_r: int = 0x3E93F7CF; var r3d_ground_hi_g: int = 0x3E8F5C29; var r3d_ground_hi_b: int = 0x3E851EB8
var r3d_ground_hi_y: int = 0x43F00000 # 480
var r3d_ground_hi_w: int = 0x43160000 # 150
var r3d_time: int = 0
var r3d_ground_alb_r: float = 0.3; var r3d_ground_alb_g: float = 0.34; var r3d_ground_alb_b: float = 0.14
var r3d_ground_hi_r: float = 0.289; var r3d_ground_hi_g: float = 0.28; var r3d_ground_hi_b: float = 0.26
var r3d_ground_hi_y: float = 480.0 # 480
var r3d_ground_hi_w: float = 150.0 # 150
var r3d_time: float = 0.0
var r3d_ready: bool = false
# set before r3d_init to build the landscape from a real height map
var r3d_dem_path: string = null
var r3d_dem_min: int = 0
var r3d_dem_max: int = 0
var r3d_dem_base: int = 0
var r3d_dem_ox: int = 0
var r3d_dem_oz: int = 0
var r3d_dem_min: float = 0.0
var r3d_dem_max: float = 0.0
var r3d_dem_base: float = 0.0
var r3d_dem_ox: float = 0.0
var r3d_dem_oz: float = 0.0
var r3d_ortho_path: string = null
var r3d_debug: bool = false
var r3d_debug_shadow: bool = false
var r3d_debug_max: bool = false
var r3d_cloud_shadow: int = 0x3F000000 # 0.5
var r3d_cloud_shadow: float = 0.5 # 0.5
var r3d_clip_y: int = 0xCF000000 # -2^31: no clipping
var r3d_clip_y: float = -2147483600.0 # -2^31: no clipping
# R3D_NOPREPASS=1: light the foliage the old way, every card behind the front one included
var r3d_prepass_env: int = -1
function r3d_prepass_off() -> bool {
@ -52,7 +52,7 @@ function r3d_prepass_off() -> bool {
}
function fog_bind(prog: int) -> void {
u_f(gpu_uniform(prog, "u_clip_y"), r3d_clip_y)
u_f(gpu_uniform(prog, "u_spec_scale"), F_ONE)
u_f(gpu_uniform(prog, "u_spec_scale"), 1.0)
u_f(gpu_uniform(prog, "u_fog_density"), r3d_fog_density)
u_f(gpu_uniform(prog, "u_fog_height_falloff"), r3d_fog_falloff)
u_f(gpu_uniform(prog, "u_fog_base"), r3d_fog_base)
@ -63,7 +63,7 @@ function fog_bind(prog: int) -> void {
u_f(gpu_uniform(prog, "u_ground_hi_y"), r3d_ground_hi_y)
u_f(gpu_uniform(prog, "u_ground_hi_w"), r3d_ground_hi_w)
var cs = r3d_cloud_shadow
if r3d_env_has("R3D_NOCLOUD") { cs = F_ZERO }
if r3d_env_has("R3D_NOCLOUD") { cs = 0.0 }
u_f(gpu_uniform(prog, "u_cloud_shadow"), cs)
u_f(gpu_uniform(prog, "u_time"), r3d_time)
}
@ -80,7 +80,7 @@ function r3d_env_flags() -> void {
r3d_no_refl = r3d_env_has("R3D_NOREFL")
if r3d_env_has("R3D_DEBUG") { r3d_debug = true }
if r3d_env_has("R3D_DBGSHADOW") { r3d_debug_shadow = true }
if r3d_env_has("R3D_NOGI") { post_gi_strength = F_ZERO; post_ao_strength = F_ZERO; post_no_gi = true }
if r3d_env_has("R3D_NOGI") { post_gi_strength = 0.0; post_ao_strength = 0.0; post_no_gi = true }
if r3d_env_has("R3D_MSAA") { post_ms_samples = Text.to_int(r3d_env("R3D_MSAA")) }
sc_skip_blade = r3d_env_has("R3D_NOBLADES")
sc_skip_card = r3d_env_has("R3D_NOCARDS")
@ -111,7 +111,7 @@ function r3d_open(w: int, h: int, title: string) -> bool {
var renderer: string = gpu_renderer_name()
print(`r3d: {gl_w}x{gl_h} on {renderer}`)
prof_init()
cam_init(fr(gl_w, gl_h))
cam_init(float(gl_w) / float(gl_h))
return true
}
@ -144,8 +144,8 @@ function r3d_load_step(i: int) -> bool {
# was doing its whole job through colour and could not be raised without the valley going
# blue. With the desaturation term carrying the depth, the air can be as thick as real air
# at 2900 m and the far ridge recedes instead of tinting.
r3d_fog_density = fl(0.00024)
r3d_fog_falloff = fl(0.002)
r3d_fog_density = 0.00024
r3d_fog_falloff = 0.002
r3d_ready = true
gpu_check("r3d init")
}
@ -162,9 +162,9 @@ function r3d_draw_sky() -> void {
sky_bind_rot(p)
sky_bind_lighting(p)
u_mat4(gpu_uniform(p, "u_inv_vp"), cam_inv_vp)
u_f(gpu_uniform(p, "u_sky_gain"), fl(0.95))
u_f(gpu_uniform(p, "u_sky_gain"), 0.95)
u_f(gpu_uniform(p, "u_sky_relight"), r3d_sky_relight)
u_f(gpu_uniform(p, "u_sky_sat"), fl(1.35))
u_f(gpu_uniform(p, "u_sky_sat"), 1.35)
u_f(gpu_uniform(p, "u_time"), r3d_time)
mesh_draw(sky_fullscreen)
gpu_depth_write(true)
@ -178,7 +178,7 @@ function r3d_present() -> void { gpu_present() }
# the drawable changed size: the camera's aspect and every screen-sized target follow
function r3d_resize() -> void {
cam_aspect = fr(gl_w, gl_h)
cam_aspect = float(gl_w) / float(gl_h)
cam_update()
post_free()
post_init(gl_w, gl_h)
@ -186,7 +186,7 @@ function r3d_resize() -> void {
print(`r3d: resized to {gl_w}x{gl_h}`)
}
var r3d_cam_log: int = -1
function r3d_frame(time: int) -> void {
function r3d_frame(time: float) -> void {
gpu_glcheck_after("the time between frames")
outline_frame()
if not r3d_ready { return }
@ -218,7 +218,7 @@ function r3d_frame(time: int) -> void {
# camera that moves while the hiker stands still from a picture that shakes on its own
if r3d_cam_log < 0 { r3d_cam_log = 0; if r3d_env_has("R3D_CAM_LOG") { r3d_cam_log = 1 } }
if r3d_cam_log == 1 {
print(`cam {r3d_test_frame} pos {f_to_int(f_mul(cam_pos[0], fi(1000)))} {f_to_int(f_mul(cam_pos[1], fi(1000)))} {f_to_int(f_mul(cam_pos[2], fi(1000)))} yaw {f_to_int(f_mul(cam_yaw, fi(1000)))} pitch {f_to_int(f_mul(cam_pitch, fi(1000)))} jitter {f_to_int(f_mul(gsl_jitter_x, fi(1000000)))} {f_to_int(f_mul(gsl_jitter_y, fi(1000000)))} render {post_w}x{post_h} reset {gsl_reset} evalok {gsl_eval_ok} fresh {gsl_fresh}`)
print(`cam {r3d_test_frame} pos {int(cam_pos[0] * 1000.0)} {int(cam_pos[1] * 1000.0)} {int(cam_pos[2] * 1000.0)} yaw {int(cam_yaw * 1000.0)} pitch {int(cam_pitch * 1000.0)} jitter {int(gsl_jitter_x * 1000000.0)} {int(gsl_jitter_y * 1000000.0)} render {post_w}x{post_h} reset {gsl_reset} evalok {gsl_eval_ok} fresh {gsl_fresh}`)
}
# the height-field shadow rebakes as the light moves in steps (daylight), or with the sky yaw when there is no clock
if (not day_on and ter_shadow_yaw != sky_yaw) or ter_shadow_gen != day_gen {

View file

@ -11,30 +11,30 @@ property Impostor {
albedo: int = 0,
normal: int = 0,
tiles: int = 16,
radius: int = 0,
height: int = 0
radius: float = 0.0,
height: float = 0.0
}
property Layer {
model: Model,
imp: Impostor,
foliage: bool = false,
wind: int = 0, # float bits
flutter: int = 0, # float bits; per-leaf tremble (aspen), 0 = none
tint: words,
inst: words, # INST_FLOATS per instance
wind: float = 0.0, # float bits
flutter: float = 0.0, # float bits; per-leaf tremble (aspen), 0 = none
tint: floats,
inst: floats, # INST_FLOATS per instance
count: int = 0,
cap: int = 0,
near: int = 0, # float bits; instances beyond it draw as impostors (or not at all)
cull: int = 0, # float bits; instances beyond it are skipped (0 = never)
near: float = 0.0, # float bits; instances beyond it draw as impostors (or not at all)
cull: float = 0.0, # float bits; instances beyond it are skipped (0 = never)
buf: int = 0,
n_near: int = 0,
imp_buf: int = 0,
sh_buf: int = 0, # every instance, for shadow casting (no cull, no LOD split)
n_sh: int = 0,
n_far: int = 0,
scratch: words,
last_cam: words,
rough: int = 0,
scratch: floats,
last_cam: floats,
rough: float = 0.0,
blade: bool = false,
flower: bool = false,
card: bool = false,
@ -45,16 +45,16 @@ property Layer {
view_gen: int = -1, # sc_view_gen this layer's partition was built for
# static layers with many instances are sorted into a cell grid once, and only the
# cells inside the view frustum (and within cull) are partitioned each frame
gcell: int = 0, # cell size (float bits); 0 = no grid
gx0: int = 0,
gz0: int = 0,
gcell: float = 0.0, # cell size (float bits); 0 = no grid
gx0: float = 0.0,
gz0: float = 0.0,
gnx: int = 0,
gnz: int = 0,
gstart: words, # per cell: first index into gsorted (ncell + 1 entries)
gsorted: words, # the instances, grouped by cell
gymin: words, # per cell height range (float bits)
gymax: words,
vis: words, # the instances gathered from visible cells this frame
gsorted: floats, # the instances, grouped by cell
gymin: floats, # per cell height range (float bits)
gymax: floats,
vis: floats, # the instances gathered from visible cells this frame
n_vis: int = 0,
# A LOD chain: lods[k] is drawn for instances within lod_dist[k] (and beyond lod_dist[k-1]);
# past the last level the impostor takes over (or, if the last distance is 0, the last
@ -76,7 +76,7 @@ property Layer {
g_base: words, # ... and its first vertex
g_model: Model, # the merged meshes as a model the draws take (level 0's height)
g_model_sh: Model, # the same with level 2's height, for the shadow LOD
lod_dist: words,
lod_dist: floats,
lod_card: words,
lod_buf: words,
n_lod: words,
@ -101,12 +101,12 @@ function model_cross_card() -> Model {
var k = 0
for q in 0 .. 2 {
for c in 0 .. 4 {
var sx = f_neg(F_HALF); var sy = F_ZERO; var u = F_ZERO; var vv = F_ZERO
if c == 1 or c == 2 { sx = F_HALF; u = F_ONE }
if c == 2 or c == 3 { sy = F_ONE; vv = F_ONE }
if q == 0 { gl_put_bits(v, k, sx); gl_put_bits(v, k + 1, sy); gl_put_bits(v, k + 2, F_ZERO); gl_put_bits(v, k + 3, F_ZERO); gl_put_bits(v, k + 4, F_ZERO); gl_put_bits(v, k + 5, f_neg1()) }
else { gl_put_bits(v, k, F_ZERO); gl_put_bits(v, k + 1, sy); gl_put_bits(v, k + 2, sx); gl_put_bits(v, k + 3, f_neg1()); gl_put_bits(v, k + 4, F_ZERO); gl_put_bits(v, k + 5, F_ZERO) }
gl_put_bits(v, k + 6, u); gl_put_bits(v, k + 7, vv)
var sx = -0.5; var sy = 0.0; var u = 0.0; var vv = 0.0
if c == 1 or c == 2 { sx = 0.5; u = 1.0 }
if c == 2 or c == 3 { sy = 1.0; vv = 1.0 }
if q == 0 { gl_put_bits(v, k, float_bits(sx)); gl_put_bits(v, k + 1, float_bits(sy)); gl_put_bits(v, k + 2, float_bits(0.0)); gl_put_bits(v, k + 3, float_bits(0.0)); gl_put_bits(v, k + 4, float_bits(0.0)); gl_put_bits(v, k + 5, float_bits(-1.0)) }
else { gl_put_bits(v, k, float_bits(0.0)); gl_put_bits(v, k + 1, float_bits(sy)); gl_put_bits(v, k + 2, float_bits(sx)); gl_put_bits(v, k + 3, float_bits(-1.0)); gl_put_bits(v, k + 4, float_bits(0.0)); gl_put_bits(v, k + 5, float_bits(0.0)) }
gl_put_bits(v, k + 6, float_bits(u)); gl_put_bits(v, k + 7, float_bits(vv))
k += 8
}
}
@ -124,7 +124,7 @@ function model_cross_card() -> Model {
if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
push(model.prims, pr)
model.radius = F_HALF; model.height = F_ONE; model.tris = 4
model.radius = 0.5; model.height = 1.0; model.tris = 4
return model
}
@ -133,9 +133,9 @@ function model_cross_card() -> Model {
# layer a flutter and its leaves tremble and flash their pale undersides; everything else
# leaves it at zero. It is per layer rather than per instance because a species quakes or
# it does not.
function layer_flutter(l: Layer, v: int) -> void { l.flutter = v }
function layer_cards(scan: Model, cap: int, wind: int, cull: int) -> Layer {
let l = layer_new(model_cross_card(), cap, true, wind, F_ZERO, cull)
function layer_flutter(l: Layer, v: float) -> void { l.flutter = v }
function layer_cards(scan: Model, cap: int, wind: float, cull: float) -> Layer {
let l = layer_new(model_cross_card(), cap, true, wind, 0.0, cull)
l.card = true
l.atlas = impostor_bake(scan, 1, 512, 512)
return l
@ -155,36 +155,36 @@ function model_lupine() -> Model {
var k = 0
var qi = 0
for q in 0 .. nq {
var w = fl(0.012); var y0 = F_ZERO; var y1 = fl(0.62); var ukind = F_ZERO
var ang = F_ZERO
var w = 0.012; var y0 = 0.0; var y1 = 0.62; var ukind = 0.0
var ang = 0.0
if q >= 2 and q < 26 {
let tier = (q - 2) / 2
let t = fr(tier, 12)
w = f_mul(fl(0.05), f_sub(fl(1.1), t))
y0 = f_add(fl(0.27), f_mul(t, fl(0.36)))
y1 = f_add(y0, fl(0.045))
ukind = F_ONE
ang = f_mul(fr(tier, 12), fl(2.1))
if (q & 1) == 1 { ang = f_add(ang, f_mul(F_PI, F_HALF)) }
let t = float(tier) / 12.0
w = 0.05 * (1.1 - t)
y0 = 0.27 + t * 0.36
y1 = y0 + 0.045
ukind = 1.0
ang = float(tier) / 12.0 * 2.1
if (q & 1) == 1 { ang = ang + PI * 0.5 }
} else if q >= 26 {
# a rosette of three leaves near the ground
w = fl(0.09); y0 = fl(0.02); y1 = fl(0.2); ukind = F_TWO
ang = f_mul(fr(q - 26, 3), f_mul(F_TWO, F_PI))
w = 0.09; y0 = 0.02; y1 = 0.2; ukind = 2.0
ang = float(q - 26) / 3.0 * (2.0 * PI)
} else {
if (q & 1) == 1 { ang = f_add(ang, f_mul(F_PI, F_HALF)) }
if (q & 1) == 1 { ang = ang + PI * 0.5 }
}
let cx = f_mul(f_cos(ang), w); let cz = f_mul(f_sin(ang), w)
let cx = Math.cos(ang) * w; let cz = Math.sin(ang) * w
for c in 0 .. 4 {
var sx = f_neg1(); var sy = y0; var u = F_ZERO
if c == 1 or c == 2 { sx = F_ONE; u = F_ONE }
var sx = -1.0; var sy = y0; var u = 0.0
if c == 1 or c == 2 { sx = 1.0; u = 1.0 }
if c == 2 or c == 3 { sy = y1 }
gl_put_bits(v, k, f_mul(cx, sx)); gl_put_bits(v, k + 1, sy); gl_put_bits(v, k + 2, f_mul(cz, sx))
gl_put_bits(v, k + 3, f_neg(cz)); gl_put_bits(v, k + 4, fl(0.2)); gl_put_bits(v, k + 5, cx)
gl_put_bits(v, k + 6, f_add(ukind, u))
gl_put_bits(v, k, float_bits(cx * sx)); gl_put_bits(v, k + 1, float_bits(sy)); gl_put_bits(v, k + 2, float_bits(cz * sx))
gl_put_bits(v, k + 3, float_bits(-cz)); gl_put_bits(v, k + 4, float_bits(0.2)); gl_put_bits(v, k + 5, float_bits(cx))
gl_put_bits(v, k + 6, float_bits(ukind + u))
var vv = sy
if ukind == F_ONE { vv = f_div(f_sub(sy, fl(0.27)), fl(0.4)) }
if ukind == F_TWO { vv = f_div(f_sub(sy, fl(0.02)), fl(0.18)) }
gl_put_bits(v, k + 7, vv)
if ukind == 1.0 { vv = (sy - 0.27) / 0.4 }
if ukind == 2.0 { vv = (sy - 0.02) / 0.18 }
gl_put_bits(v, k + 7, float_bits(vv))
k += 8
}
let b = q * 4
@ -202,7 +202,7 @@ function model_lupine() -> Model {
if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
push(model.prims, pr)
model.radius = fl(0.08); model.height = fl(0.65); model.tris = nq * 2
model.radius = 0.08; model.height = 0.65; model.tris = nq * 2
return model
}
@ -221,43 +221,43 @@ function model_lupine_dense() -> Model {
var qi = 0
seed(5)
for q in 0 .. nq {
var w = fl(0.008); var y0 = F_ZERO; var y1 = fl(0.66); var ukind = F_ZERO
var ang = F_ZERO; var ox = F_ZERO; var oz = F_ZERO; var tilt = F_ZERO
var w = 0.008; var y0 = 0.0; var y1 = 0.66; var ukind = 0.0
var ang = 0.0; var ox = 0.0; var oz = 0.0; var tilt = 0.0
if q >= 2 and q < 2 + nfl {
let t = fr(q - 2, nfl)
let yy = f_add(fl(0.28), f_mul(t, fl(0.4)))
ang = f_mul(fi(q), fl(2.39996)) # golden angle spiral
let rad = f_mul(fl(0.055), f_sub(fl(1.05), t))
ox = f_mul(f_cos(ang), rad); oz = f_mul(f_sin(ang), rad)
w = f_mul(fl(0.028), f_sub(fl(1.1), f_mul(t, fl(0.5))))
y0 = f_sub(yy, fl(0.016)); y1 = f_add(yy, fl(0.016))
ukind = F_ONE
tilt = fl(0.6)
let t = float(q - 2) / float(nfl)
let yy = 0.28 + t * 0.4
ang = float(q) * 2.39996 # golden angle spiral
let rad = 0.055 * (1.05 - t)
ox = Math.cos(ang) * rad; oz = Math.sin(ang) * rad
w = 0.028 * (1.1 - t * 0.5)
y0 = yy - 0.016; y1 = yy + 0.016
ukind = 1.0
tilt = 0.6
} else if q >= 2 + nfl {
w = fl(0.05); y0 = fl(0.03); y1 = fl(0.16); ukind = F_TWO
ang = f_mul(fr(q - 2 - nfl, 5), f_mul(F_TWO, F_PI))
ox = f_mul(f_cos(ang), fl(0.05)); oz = f_mul(f_sin(ang), fl(0.05))
w = 0.05; y0 = 0.03; y1 = 0.16; ukind = 2.0
ang = float(q - 2 - nfl) / 5.0 * (2.0 * PI)
ox = Math.cos(ang) * 0.05; oz = Math.sin(ang) * 0.05
} else {
if (q & 1) == 1 { ang = f_mul(F_PI, F_HALF) }
if (q & 1) == 1 { ang = PI * 0.5 }
}
# the quad faces outward (its normal along the spiral radius), leaning out by `tilt`
let nx = f_cos(ang); let nz = f_sin(ang)
let tx = f_neg(nz); let tz = nx # tangent (quad width direction)
let nx = Math.cos(ang); let nz = Math.sin(ang)
let tx = -nz; let tz = nx # tangent (quad width direction)
for c in 0 .. 4 {
var sx = f_neg1(); var sy = y0; var u = F_ZERO
if c == 1 or c == 2 { sx = F_ONE; u = F_ONE }
var sx = -1.0; var sy = y0; var u = 0.0
if c == 1 or c == 2 { sx = 1.0; u = 1.0 }
if c == 2 or c == 3 { sy = y1 }
var lean = F_ZERO
if c == 2 or c == 3 { lean = f_mul(tilt, w) }
gl_put_bits(v, k, f_add(f_add(ox, f_mul(tx, f_mul(sx, w))), f_mul(nx, lean)))
gl_put_bits(v, k + 1, sy)
gl_put_bits(v, k + 2, f_add(f_add(oz, f_mul(tz, f_mul(sx, w))), f_mul(nz, lean)))
gl_put_bits(v, k + 3, nx); gl_put_bits(v, k + 4, fl(0.35)); gl_put_bits(v, k + 5, nz)
gl_put_bits(v, k + 6, f_add(ukind, u))
var lean = 0.0
if c == 2 or c == 3 { lean = tilt * w }
gl_put_bits(v, k, float_bits(ox + tx * (sx * w) + nx * lean))
gl_put_bits(v, k + 1, float_bits(sy))
gl_put_bits(v, k + 2, float_bits(oz + tz * (sx * w) + nz * lean))
gl_put_bits(v, k + 3, float_bits(nx)); gl_put_bits(v, k + 4, float_bits(0.35)); gl_put_bits(v, k + 5, float_bits(nz))
gl_put_bits(v, k + 6, float_bits(ukind + u))
var vv = sy
if ukind == F_ONE { vv = f_div(f_sub(sy, fl(0.27)), fl(0.42)) }
if ukind == F_TWO { vv = f_div(f_sub(sy, fl(0.03)), fl(0.19)) }
gl_put_bits(v, k + 7, vv)
if ukind == 1.0 { vv = (sy - 0.27) / 0.42 }
if ukind == 2.0 { vv = (sy - 0.03) / 0.19 }
gl_put_bits(v, k + 7, float_bits(vv))
k += 8
}
let b = q * 4
@ -275,7 +275,7 @@ function model_lupine_dense() -> Model {
if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
push(model.prims, pr)
model.radius = fl(0.11); model.height = fl(0.68); model.tris = nq * 2
model.radius = 0.11; model.height = 0.68; model.tris = nq * 2
return model
}
@ -289,17 +289,17 @@ function model_blade() -> Model {
let v = gl_floats(rows * 2 * 8)
var k = 0
for r in 0 .. rows {
let t = fr(r, rows - 1)
let t = float(r) / float(rows - 1)
# never a zero-width tip: a sliver triangle extrapolates its attributes wildly
let taper = f_max(f_sub(F_ONE, f_mul(t, f_mul(t, f_sqrt(t)))), fl(0.12))
let hw = f_mul(fl(0.05), taper)
let bend = f_mul(f_mul(t, t), fl(0.28))
let taper = Math.max(1.0 - t * (t * Math.sqrt(t)), 0.12)
let hw = 0.05 * taper
let bend = t * t * 0.28
for sd in 0 .. 2 {
var x = f_neg(hw)
var x = -hw
if sd == 1 { x = hw }
gl_put_bits(v, k, x); gl_put_bits(v, k + 1, t); gl_put_bits(v, k + 2, bend)
gl_put_bits(v, k + 3, F_ZERO); gl_put_bits(v, k + 4, fl(0.3)); gl_put_bits(v, k + 5, F_ONE)
gl_put_bits(v, k + 6, fi(sd)); gl_put_bits(v, k + 7, t)
gl_put_bits(v, k, float_bits(x)); gl_put_bits(v, k + 1, float_bits(t)); gl_put_bits(v, k + 2, float_bits(bend))
gl_put_bits(v, k + 3, float_bits(0.0)); gl_put_bits(v, k + 4, float_bits(0.3)); gl_put_bits(v, k + 5, float_bits(1.0))
gl_put_bits(v, k + 6, float_bits(float(sd))); gl_put_bits(v, k + 7, float_bits(t))
k += 8
}
}
@ -323,7 +323,7 @@ function model_blade() -> Model {
if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white
push(model.prims, pr)
model.radius = fl(0.05); model.height = F_ONE; model.tris = ni / 3
model.radius = 0.05; model.height = 1.0; model.tris = ni / 3
return model
}
@ -335,9 +335,9 @@ var sc_prog_flower: int = 0
var sc_prog_card: int = 0
var sc_prog_card_shadow: int = 0
var sc_prog_card_cheap: int = 0
var sc_blade_base: words = null
var sc_blade_tip: words = null
var sc_blade_tint: words = null
var sc_blade_base: floats = null
var sc_blade_tip: floats = null
var sc_blade_tint: floats = null
var sc_prog_shadow: int = 0
var sc_prog_shadow_wind: int = 0
var sc_prog_shadow_fol: int = 0 # foliage meshes: alpha-tested casters
@ -374,9 +374,9 @@ function scatter_init() -> void {
sc_prog_card_shadow = r3d_program("model.vert", "model.frag", "#define SHADOW_PASS\n#define WIND\n#define CARD\n")
sc_prog_card_cheap = r3d_program("model.vert", "model.frag", "#define CARD\n#define CHEAP\n")
# a dry alpine meadow: brown-olive roots, straw with a little green at the tips
sc_blade_base = v3_new(fl(0.045), fl(0.06), fl(0.025))
sc_blade_tip = v3_new(fl(0.22), fl(0.27), fl(0.13))
sc_blade_tint = v3_new(F_ONE, F_ONE, F_ONE)
sc_blade_base = v3_new(0.045, 0.06, 0.025)
sc_blade_tip = v3_new(0.22, 0.27, 0.13)
sc_blade_tint = v3_new(1.0, 1.0, 1.0)
sc_prog_shadow = r3d_program("model.vert", "shadow.frag", "#define SHADOW_PASS\n")
sc_prog_shadow_wind = r3d_program("model.vert", "shadow.frag", "#define SHADOW_PASS\n#define WIND\n")
sc_prog_shadow_fol = r3d_program("model.vert", "shadow.frag", "#define SHADOW_PASS\n#define WIND\n#define ALPHA_TEST\n")
@ -388,8 +388,8 @@ function scatter_init() -> void {
sc_card = mesh_card()
# a single identity instance, for baking
let one = gl_floats(INST_FLOATS)
for i in 0 .. INST_FLOATS { gl_put_bits(one, i, F_ZERO) }
gl_put_bits(one, 3, F_ONE); gl_put_bits(one, 5, F_ONE)
for i in 0 .. INST_FLOATS { gl_put_bits(one, i, float_bits(0.0)) }
gl_put_bits(one, 3, float_bits(1.0)); gl_put_bits(one, 5, float_bits(1.0))
sc_ident_buf = gpu_buffer_new()
gpu_buffer_upload(sc_ident_buf, INST_FLOATS * 4, one, GPU_STATIC)
free(one)
@ -404,7 +404,7 @@ function scatter_attach(m: Mesh, buf: int) -> void {
gpu_mesh_done(m)
}
function layer_new(model: Model, cap: int, foliage: bool, wind: int, near: int, cull: int) -> Layer {
function layer_new(model: Model, cap: int, foliage: bool, wind: float, near: float, cull: float) -> Layer {
let l = new Layer
l.model = model
l.cap = cap
@ -412,24 +412,24 @@ function layer_new(model: Model, cap: int, foliage: bool, wind: int, near: int,
l.wind = wind
l.near = near
l.cull = cull
l.tint = v3_new(F_ONE, F_ONE, F_ONE)
l.inst = words(cap * INST_FLOATS)
l.scratch = words(cap * INST_FLOATS)
l.last_cam = v3_new(fi(100000), F_ZERO, F_ZERO)
l.tint = v3_new(1.0, 1.0, 1.0)
l.inst = floats(cap * INST_FLOATS)
l.scratch = floats(cap * INST_FLOATS)
l.last_cam = v3_new(100000.0, 0.0, 0.0)
l.buf = gpu_buffer_new()
l.imp_buf = gpu_buffer_new()
l.sh_buf = gpu_buffer_new()
l.rough = F_ONE
l.rough = 1.0
for i in 0 .. len(model.prims) { scatter_attach(model.prims[i].mesh, l.buf) }
push(sc_layers, l)
return l
}
function layer_add(l: Layer, x: int, y: int, z: int, scale: int, yaw: int, seed: int, wind: int) -> void {
function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float) -> void {
if l.count >= l.cap { return }
let o = l.count * INST_FLOATS
l.inst[o] = x; l.inst[o + 1] = y; l.inst[o + 2] = z; l.inst[o + 3] = scale
l.inst[o + 4] = f_sin(yaw); l.inst[o + 5] = f_cos(yaw); l.inst[o + 6] = seed; l.inst[o + 7] = wind
l.inst[o + 4] = Math.sin(yaw); l.inst[o + 5] = Math.cos(yaw); l.inst[o + 6] = seed; l.inst[o + 7] = wind
l.count += 1
}
@ -439,7 +439,7 @@ var sc_dump_n: int = 0
function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor {
let im = new Impostor
im.tiles = tiles
im.radius = f_mul(model.radius, fl(1.02))
im.radius = model.radius * 1.02
im.height = model.height
let aw = tiles * tw
im.albedo = tex_target(aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
@ -462,24 +462,24 @@ function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor {
# the model's prims temporarily take the identity instance
for i in 0 .. len(model.prims) { scatter_attach(model.prims[i].mesh, sc_ident_buf) }
let view = m4_new(); let proj = m4_new()
let eye = words(3); let at = words(3); let up = v3_new(F_ZERO, F_ONE, F_ZERO)
let cy = f_add(model.ymin, f_mul(model.height, F_HALF))
let eye = floats(3); let at = floats(3); let up = v3_new(0.0, 1.0, 0.0)
let cy = model.ymin + model.height * 0.5
let r = im.radius
let hh = f_mul(model.height, F_HALF)
let hh = model.height * 0.5
var bake = sc_bake_prog
if sc_bake_flower { bake = sc_bake_flower_prog }
gpu_use_program(bake)
for t in 0 .. tiles {
let a = f_mul(f_mul(F_TWO, F_PI), fr(t, tiles))
v3_set(at, F_ZERO, cy, F_ZERO)
let a = 2.0 * PI * (float(t) / float(tiles))
v3_set(at, 0.0, cy, 0.0)
# a touch of elevation (the viewer usually looks slightly down at a tree)
v3_set(eye, f_mul(f_sin(a), f_mul(r, fi(4))), f_add(cy, f_mul(r, fl(0.5))), f_neg(f_mul(f_cos(a), f_mul(r, fi(4)))))
v3_set(eye, Math.sin(a) * (r * 4.0), cy + r * 0.5, -(Math.cos(a) * (r * 4.0)))
m4_look_at(view, eye, at, up)
m4_ortho(proj, f_neg(r), r, f_neg(hh), hh, fl(0.1), f_mul(r, fi(9)))
m4_ortho(proj, -r, r, -hh, hh, 0.1, r * 9.0)
u_mat4(gpu_uniform(bake, "u_view"), view)
u_mat4(gpu_uniform(bake, "u_proj"), proj)
u_f(gpu_uniform(bake, "u_wind"), F_ZERO)
u_f(gpu_uniform(bake, "u_flutter"), F_ZERO)
u_f(gpu_uniform(bake, "u_wind"), 0.0)
u_f(gpu_uniform(bake, "u_flutter"), 0.0)
gpu_viewport(t * tw, 0, tw, th)
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
@ -510,10 +510,10 @@ function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor {
# Give a layer a LOD chain. `dists` (float bits) are the outer distances of each level;
# the last one becomes the layer's `near` so the impostor (if any) starts there.
function layer_set_lods(l: Layer, models: []Model, dists: words) -> void {
function layer_set_lods(l: Layer, models: []Model, dists: floats) -> void {
l.lods = models
l.n_lods = len(models)
l.lod_dist = words(l.n_lods); l.lod_card = words(l.n_lods); l.lod_buf = words(l.n_lods); l.n_lod = words(l.n_lods)
l.lod_dist = floats(l.n_lods); l.lod_card = words(l.n_lods); l.lod_buf = words(l.n_lods); l.n_lod = words(l.n_lods)
for k in 0 .. l.n_lods {
l.lod_dist[k] = dists[k]; l.lod_card[k] = 0; l.n_lod[k] = 0
l.lod_buf[k] = gpu_buffer_new()
@ -537,11 +537,11 @@ function layer_set_impostor(l: Layer, im: Impostor) -> void {
# camera moved 1.5 m or turned about 2.5 degrees. Everything culled by the frustum keys
# off this one counter, so a turn re-gathers the streams and the grids together.
var sc_view_gen: int = 1
var sc_view_pos: words = null
var sc_view_fwd: words = null
var sc_view_pos: floats = null
var sc_view_fwd: floats = null
function scatter_begin_frame() -> void {
if sc_view_pos == null { sc_view_pos = v3_new(fi(100000), F_ZERO, F_ZERO); sc_view_fwd = v3_new(F_ZERO, F_ZERO, f_neg(F_ONE)) }
if f_gt(v3_dist(sc_view_pos, cam_pos), fl(1.5)) or f_ls(v3_dot(sc_view_fwd, cam_fwd), fl(0.999)) {
if sc_view_pos == null { sc_view_pos = v3_new(100000.0, 0.0, 0.0); sc_view_fwd = v3_new(0.0, 0.0, -1.0) }
if v3_dist(sc_view_pos, cam_pos) > 1.5 or v3_dot(sc_view_fwd, cam_fwd) < 0.999 {
sc_view_gen += 1
v3_copy(sc_view_pos, cam_pos)
v3_copy(sc_view_fwd, cam_fwd)
@ -707,12 +707,12 @@ function layer_gpu_prepare(l: Layer) -> bool {
function layer_gpu_cull(l: Layer) -> void {
let pr = words(36)
for i in 0 .. 36 { pr[i] = 0 }
if cam_planes != null { for i in 0 .. 16 { pr[i] = cam_planes[i] } }
pr[16] = cam_pos[0]; pr[17] = cam_pos[1]; pr[18] = cam_pos[2]; pr[19] = l.cull
for k in 0 .. l.n_lods { pr[20 + k] = l.lod_dist[k]; pr[24 + k] = len(l.lods[k].prims) }
if cam_planes != null { for i in 0 .. 16 { pr[i] = float_bits(cam_planes[i]) } }
pr[16] = float_bits(cam_pos[0]); pr[17] = float_bits(cam_pos[1]); pr[18] = float_bits(cam_pos[2]); pr[19] = float_bits(l.cull)
for k in 0 .. l.n_lods { pr[20 + k] = float_bits(l.lod_dist[k]); pr[24 + k] = len(l.lods[k].prims) }
pr[28] = l.count; pr[29] = l.count; pr[30] = l.n_lods; pr[31] = 1
# as layer_grid_gather pads a cell: the tallest instance, plus a margin
pr[32] = f_mul(l.lods[0].height, F_TWO); pr[33] = fi(4)
pr[32] = float_bits(l.lods[0].height * 2.0); pr[33] = float_bits(4.0)
let bufs = words(4)
bufs[0] = l.g_src; bufs[1] = l.g_dst; bufs[2] = l.g_cmds; bufs[3] = l.g_counts
gpu_dispatch(sc_cull_prog, pr, 144, bufs, 1)
@ -722,36 +722,36 @@ function layer_gpu_cull(l: Layer) -> void {
# Sort a static layer's instances into square cells (call once, after placement; a
# large layer that was never gridded gets a 96 m grid on its first update). The
# shadow buffer is uploaded here once — casters are never culled by the view.
function layer_grid_build(l: Layer, cell: int) -> void {
function layer_grid_build(l: Layer, cell: float) -> void {
if l.count == 0 { return }
var minx = l.inst[0]; var maxx = minx; var minz = l.inst[2]; var maxz = minz
for i in 0 .. l.count {
let o = i * INST_FLOATS
minx = f_min(minx, l.inst[o]); maxx = f_max(maxx, l.inst[o])
minz = f_min(minz, l.inst[o + 2]); maxz = f_max(maxz, l.inst[o + 2])
minx = Math.min(minx, l.inst[o]); maxx = Math.max(maxx, l.inst[o])
minz = Math.min(minz, l.inst[o + 2]); maxz = Math.max(maxz, l.inst[o + 2])
}
l.gcell = cell; l.gx0 = minx; l.gz0 = minz
l.gnx = f_to_int(f_div(f_sub(maxx, minx), cell)) + 1
l.gnz = f_to_int(f_div(f_sub(maxz, minz), cell)) + 1
l.gnx = int((maxx - minx) / cell) + 1
l.gnz = int((maxz - minz) / cell) + 1
let ncell = l.gnx * l.gnz
l.gstart = words(ncell + 1)
l.gymin = words(ncell); l.gymax = words(ncell)
l.gymin = floats(ncell); l.gymax = floats(ncell)
let cellof = words(l.count)
for c in 0 .. ncell + 1 { l.gstart[c] = 0 }
for i in 0 .. l.count {
let o = i * INST_FLOATS
let ix = f_to_int(f_div(f_sub(l.inst[o], minx), cell))
let iz = f_to_int(f_div(f_sub(l.inst[o + 2], minz), cell))
let ix = int((l.inst[o] - minx) / cell)
let iz = int((l.inst[o + 2] - minz) / cell)
let c = iz * l.gnx + ix
cellof[i] = c
if l.gstart[c + 1] == 0 { l.gymin[c] = l.inst[o + 1]; l.gymax[c] = l.inst[o + 1] }
else { l.gymin[c] = f_min(l.gymin[c], l.inst[o + 1]); l.gymax[c] = f_max(l.gymax[c], l.inst[o + 1]) }
else { l.gymin[c] = Math.min(l.gymin[c], l.inst[o + 1]); l.gymax[c] = Math.max(l.gymax[c], l.inst[o + 1]) }
l.gstart[c + 1] += 1
}
for c in 0 .. ncell { l.gstart[c + 1] += l.gstart[c] }
let fill = words(ncell)
for c in 0 .. ncell { fill[c] = l.gstart[c] }
l.gsorted = words(l.count * INST_FLOATS)
l.gsorted = floats(l.count * INST_FLOATS)
for i in 0 .. l.count {
let c = cellof[i]
let q = fill[c] * INST_FLOATS
@ -760,7 +760,7 @@ function layer_grid_build(l: Layer, cell: int) -> void {
for k in 0 .. INST_FLOATS { l.gsorted[q + k] = l.inst[o + k] }
}
free(cellof); free(fill)
if l.vis == null { l.vis = words(l.cap * INST_FLOATS) }
if l.vis == null { l.vis = floats(l.cap * INST_FLOATS) }
l.n_sh = l.count
gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, l.inst, GPU_STATIC)
}
@ -768,24 +768,24 @@ function layer_grid_build(l: Layer, cell: int) -> void {
# gather the instances of the cells the camera can see (and that are within cull)
function layer_grid_gather(l: Layer) -> void {
let cell = l.gcell
let half = f_mul(cell, F_HALF)
let reach = f_add(l.cull, f_mul(cell, fl(0.71)))
let half = cell * 0.5
let reach = l.cull + cell * 0.71
var n = 0
for iz in 0 .. l.gnz {
let wz = f_add(f_add(l.gz0, f_mul(fi(iz), cell)), half)
let wz = l.gz0 + float(iz) * cell + half
for ix in 0 .. l.gnx {
let c = iz * l.gnx + ix
let cnt = l.gstart[c + 1] - l.gstart[c]
if cnt == 0 { continue }
let wx = f_add(f_add(l.gx0, f_mul(fi(ix), cell)), half)
if l.cull != 0 {
let dx = f_sub(wx, cam_pos[0]); let dz = f_sub(wz, cam_pos[2])
if f_gt(f_sqrt(f_add(f_mul(dx, dx), f_mul(dz, dz))), reach) { continue }
let wx = l.gx0 + float(ix) * cell + half
if l.cull != 0.0 {
let dx = wx - cam_pos[0]; let dz = wz - cam_pos[2]
if Math.sqrt(dx * dx + dz * dz) > reach { continue }
}
let hy = f_mul(f_sub(l.gymax[c], l.gymin[c]), F_HALF)
let cy = f_add(l.gymin[c], hy)
let hy = (l.gymax[c] - l.gymin[c]) * 0.5
let cy = l.gymin[c] + hy
# pad by the tallest instance (scale 2 of the model's height) so crowns at the frame's edge stay
let r = f_add(f_sqrt(f_add(f_mul(f_mul(half, half), F_TWO), f_mul(hy, hy))), f_add(f_mul(l.model.height, F_TWO), fi(4)))
let r = Math.sqrt(half * half * 2.0 + hy * hy) + (l.model.height * 2.0 + 4.0)
if not cam_sphere_visible(wx, cy, wz, r) { continue }
mem_copy(mem_off(l.vis, n * INST_FLOATS * 4), mem_off(l.gsorted, l.gstart[c] * INST_FLOATS * 4), cnt * INST_FLOATS * 4)
n += cnt
@ -796,22 +796,22 @@ function layer_grid_gather(l: Layer) -> void {
# Sort the gathered instances into their LOD levels (counting sort into the scratch),
# the impostor bucket last, and upload one buffer per level.
function layer_partition_lods(l: Layer, src: words, total: int) -> void {
function layer_partition_lods(l: Layer, src: floats, total: int) -> void {
let n = l.n_lods
let counts = words(n + 2)
for k in 0 .. n + 2 { counts[k] = 0 }
let cull2 = f_mul(l.cull, l.cull)
let open = l.lod_dist[n - 1] == 0 # the last level runs out to the cull distance
let cull2 = l.cull * l.cull
let open = l.lod_dist[n - 1] == 0.0 # the last level runs out to the cull distance
for i in 0 .. total {
let o = i * INST_FLOATS
let dx = f_sub(src[o], cam_pos[0]); let dz = f_sub(src[o + 2], cam_pos[2])
let d2 = f_add(f_mul(dx, dx), f_mul(dz, dz))
let dx = src[o] - cam_pos[0]; let dz = src[o + 2] - cam_pos[2]
let d2 = dx * dx + dz * dz
var lv = n + 1 # n + 1 = dropped
if l.cull == 0 or not f_gt(d2, cull2) {
let d = f_sqrt(d2)
if l.cull == 0.0 or not (d2 > cull2) {
let d = Math.sqrt(d2)
lv = n # n = the impostor bucket
var k = 0
while k < n { if l.lod_dist[k] != 0 and f_ls(d, l.lod_dist[k]) { lv = k; k = n } else { k += 1 } }
while k < n { if l.lod_dist[k] != 0.0 and d < l.lod_dist[k] { lv = k; k = n } else { k += 1 } }
if lv == n and open { lv = n - 1 }
if lv == n and l.imp == null { lv = n + 1 }
}
@ -841,12 +841,12 @@ function layer_partition_lods(l: Layer, src: words, total: int) -> void {
}
l.n_near = counts[0]
l.n_far = counts[n]
if sc_dbg_lod and total > 1000 { print(`lod partition: total {total} dropped {counts[n + 1]} far {counts[n]} l0 {counts[0]} l1 {counts[1]} l2 {counts[2]} l3 {counts[3]} dist0 {f_fx(l.lod_dist[0])} dist3 {f_fx(l.lod_dist[n - 1])} cull {f_fx(l.cull)} cam {f_fx(cam_pos[0])} {f_fx(cam_pos[2])} first {f_fx(src[0])} {f_fx(src[2])}`) }
if sc_dbg_lod and total > 1000 { print(`lod partition: total {total} dropped {counts[n + 1]} far {counts[n]} l0 {counts[0]} l1 {counts[1]} l2 {counts[2]} l3 {counts[3]} dist0 {fixed(l.lod_dist[0])} dist3 {fixed(l.lod_dist[n - 1])} cull {fixed(l.cull)} cam {fixed(cam_pos[0])} {fixed(cam_pos[2])} first {fixed(src[0])} {fixed(src[2])}`) }
if l.n_far > 0 {
gpu_buffer_upload(l.imp_buf, l.n_far * INST_FLOATS * 4, mem_off(tmp, start[n] * INST_FLOATS * 4), GPU_DYNAMIC)
}
# casters: the whole (gathered) set from the shadow buffer, unless the impostor casts
if l.gcell == 0 {
if l.gcell == 0.0 {
l.n_sh = total
if total > 0 { gpu_buffer_upload(l.sh_buf, total * INST_FLOATS * 4, src, GPU_DYNAMIC) }
}
@ -864,18 +864,18 @@ function layer_update(l: Layer) -> void {
let n_lu = l.count
# A streamed layer's instances were already gathered per visible chunk: no split, no
# per-instance loop — one upload, and the same buffer casts its shadows.
if l.streamed and l.imp == null and l.near == 0 and l.n_lods <= 1 {
if l.streamed and l.imp == null and l.near == 0.0 and l.n_lods <= 1 {
l.n_near = l.count; l.n_far = 0; l.n_sh = l.count
gpu_buffer_upload(l.buf, l.count * INST_FLOATS * 4, l.inst, GPU_DYNAMIC)
prof_layer_add(gl_now_us() - t_lu, l.count * INST_FLOATS * 4)
return
}
if l.gcell == 0 and not l.streamed and l.count > 2000 { layer_grid_build(l, fi(96)) }
if l.gcell == 0.0 and not l.streamed and l.count > 2000 { layer_grid_build(l, 96.0) }
var src = l.inst
var total = l.count
if l.gcell != 0 { layer_grid_gather(l); src = l.vis; total = l.n_vis }
let near2 = f_mul(l.near, l.near)
let cull2 = f_mul(l.cull, l.cull)
if l.gcell != 0.0 { layer_grid_gather(l); src = l.vis; total = l.n_vis }
let near2 = l.near * l.near
let cull2 = l.cull * l.cull
var nn = 0
var nf = 0
let far_off = l.cap * INST_FLOATS # far instances fill the scratch from its end backwards
@ -888,11 +888,11 @@ function layer_update(l: Layer) -> void {
var i = 0
while i < total {
let o = i * INST_FLOATS
let dx = f_sub(src[o], cam_pos[0])
let dz = f_sub(src[o + 2], cam_pos[2])
let d2 = f_add(f_mul(dx, dx), f_mul(dz, dz))
if l.cull != 0 and f_gt(d2, cull2) { i += 1; continue }
if l.near == 0 or f_ls(d2, near2) {
let dx = src[o] - cam_pos[0]
let dz = src[o + 2] - cam_pos[2]
let d2 = dx * dx + dz * dz
if l.cull != 0.0 and d2 > cull2 { i += 1; continue }
if l.near == 0.0 or d2 < near2 {
let q = nn * INST_FLOATS
for k in 0 .. INST_FLOATS { tmp[q + k] = src[o + k] }
nn += 1
@ -905,15 +905,15 @@ function layer_update(l: Layer) -> void {
}
l.n_near = nn
l.n_far = nf
if l.imp != null and nn > 0 and sc_debug_dump { print(`near full-mesh instances: {nn} (first at {f_fx(tmp[0])} {f_fx(tmp[1])} {f_fx(tmp[2])})`) }
if l.imp != null and nn > 0 and sc_debug_dump { print(`near full-mesh instances: {nn} (first at {fixed(tmp[0])} {fixed(tmp[1])} {fixed(tmp[2])})`) }
if sc_debug_dump and l.imp != null {
print(`layer: near {nn} far {nf}`)
for k in 0 .. nn { let q = k * INST_FLOATS; print(` near {f_fx(tmp[q])} {f_fx(tmp[q + 1])} {f_fx(tmp[q + 2])} s {f_fx(tmp[q + 3])}`) }
for k in 0 .. nn { let q = k * INST_FLOATS; print(` near {fixed(tmp[q])} {fixed(tmp[q + 1])} {fixed(tmp[q + 2])} s {fixed(tmp[q + 3])}`) }
}
# Every instance, unculled and unsplit, for the shadow pass. What the camera draws is
# allowed to change with distance; what casts must not, or shadows blink in and out as
# you walk. This is the whole set, drawn one way, into every cascade.
if l.gcell == 0 {
if l.gcell == 0.0 {
l.n_sh = l.count
if l.count > 0 {
gpu_buffer_upload(l.sh_buf, l.count * INST_FLOATS * 4, l.inst, GPU_DYNAMIC)
@ -934,7 +934,7 @@ function layer_program(l: Layer, shadow: bool, card: bool) -> int {
}
if shadow {
if l.foliage and not l.blade and not l.flower { return sc_prog_shadow_fol }
if l.wind != 0 { return sc_prog_shadow_wind }
if l.wind != 0.0 { return sc_prog_shadow_wind }
return sc_prog_shadow
}
if l.blade { return sc_prog_blade }
@ -943,7 +943,7 @@ function layer_program(l: Layer, shadow: bool, card: bool) -> int {
if sc_prepass and sc_prog_fol_eq != 0 { return sc_prog_fol_eq }
return sc_prog_fol
}
if l.wind != 0 { return sc_prog_wind }
if l.wind != 0.0 { return sc_prog_wind }
return sc_prog
}
@ -951,7 +951,7 @@ var sc_dbg_blade: int = 0
# R3D_LODDBG=1 tints each LOD level (red, green, blue, yellow) and impostors magenta
var sc_dbg_level: int = -1
var sc_dbg_lod: bool = false
var sc_dbg_tint: words = null
var sc_dbg_tint: floats = null
# Can level k's casters (its instances lie between the previous level's distance and its own) put a
# shadow on anything the cascade being rendered covers? A receiver in that slice of view depth
# [near, far] stands between near - dy and far * K metres away on the ground: dy is the camera's height
@ -963,37 +963,37 @@ var sc_dbg_tint: words = null
# level into every cascade, for comparing.
var sc_cast_all: int = -1
var sc_cast_gen: int = -1
var sc_cast_dy: int = 0
var sc_cast_k: int = 0
var sc_cast_dy: float = 0.0
var sc_cast_k: float = 0.0
function layer_level_casts_here(l: Layer, k: int) -> bool {
if l.lod_dist == null { return true }
var dmin = F_ZERO
var dmin = 0.0
if k > 0 { dmin = l.lod_dist[k - 1] }
return cast_band_reaches(dmin, l.lod_dist[k], l.lods[k].height)
}
# Can something standing between dmin and dmax metres from the camera (dmax 0: no outer limit), this
# tall, put a shadow on anything the cascade being rendered covers? The flowers' levels ask it
# (layer_level_casts_here), and so does every actor (actor_draw_casters).
function cast_band_reaches(dmin: int, dmax: int, height: int) -> bool {
function cast_band_reaches(dmin: float, dmax: float, height: float) -> bool {
if sc_cast_all < 0 { sc_cast_all = 0; if r3d_env_has("R3D_CAST_ALL") { sc_cast_all = 1 } }
if sc_cast_all == 1 or sh_split == null { return true }
if sc_cast_gen != sc_view_gen {
sc_cast_gen = sc_view_gen
sc_cast_dy = f_add(f_abs(f_sub(cam_pos[1], terrain_height(cam_pos[0], cam_pos[2]))), fi(5))
let half = f_mul(cam_fov, F_HALF)
let t = f_div(f_sin(half), f_cos(half))
let ta = f_mul(t, cam_aspect)
sc_cast_k = f_mul(f_sqrt(f_add(F_ONE, f_add(f_mul(t, t), f_mul(ta, ta)))), fl(1.1))
sc_cast_dy = Math.abs(cam_pos[1] - terrain_height(cam_pos[0], cam_pos[2])) + 5.0
let half = cam_fov * 0.5
let t = Math.sin(half) / Math.cos(half)
let ta = t * cam_aspect
sc_cast_k = Math.sqrt(1.0 + (t * t + ta * ta)) * 1.1
}
let c = sh_cascade
var near = cam_near
# sunShadow cross-fades into this cascade from 0.85 of the previous one's split (lighting.glsl), so
# its receivers start there, not at the split: starting at the split changed 19 pixels in town
if c > 0 { near = f_mul(sh_split[c - 1], fl(0.85)) }
if c > 0 { near = sh_split[c - 1] * 0.85 }
let far = sh_split[c]
let reach = f_max(f_min(f_mul(height, fi(6)), fi(40)), fi(8))
if dmax != 0 and f_ls(f_add(f_add(dmax, reach), sc_cast_dy), near) { return false }
if f_gt(f_sub(dmin, reach), f_mul(far, sc_cast_k)) { return false }
let reach = Math.max(Math.min(height * 6.0, 40.0), 8.0)
if dmax != 0.0 and dmax + reach + sc_cast_dy < near { return false }
if dmin - reach > far * sc_cast_k { return false }
return true
}
@ -1001,7 +1001,7 @@ function cast_band_reaches(dmin: int, dmax: int, height: int) -> bool {
# partition out of l.buf. A layer with no impostor (the tree crowns' branch cards) has
# no cheap stand-in to cast from, so without this its shadow simply began at the near
# distance — which is the crown shadow that appeared as you walked up to a tree.
function layer_draw_near(l: Layer, shadow: bool, light_vp: words, full: bool) -> void {
function layer_draw_near(l: Layer, shadow: bool, light_vp: floats, full: bool) -> void {
if l.n_lods > 1 and l.g_on and not shadow {
# one draw per material covering all of its levels (the merged meshes)
sc_ind_base = 0; sc_ind_n = l.n_lods
@ -1026,26 +1026,26 @@ function layer_draw_near(l: Layer, shadow: bool, light_vp: words, full: bool) ->
}
# draw `cnt` instances of `model` out of instance buffer `vb`, as a mesh or as the layer's card
function layer_draw_model(l: Layer, model: Model, vb: int, cnt: int, card: bool, shadow: bool, light_vp: words) -> void {
function layer_draw_model(l: Layer, model: Model, vb: int, cnt: int, card: bool, shadow: bool, light_vp: floats) -> void {
if cnt == 0 { return }
let p = layer_program(l, shadow, card)
gpu_use_program(p)
# over the prepass: only the fragment the prepass kept, at exactly its depth (a texel
# it cut would otherwise pass LEQUAL over the terrain behind and draw the quad solid)
if p == sc_prog_fol_eq { gpu_depth_func(GL_EQUAL) }
var ground = F_ZERO
if l.grounded { ground = F_ONE }
var ground = 0.0
if l.grounded { ground = 1.0 }
u_f(gpu_uniform(p, "u_ground"), ground)
if l.grounded { terrain_bind_height(p) }
u_f(gpu_uniform(p, "u_time"), r3d_time)
u_f(gpu_uniform(p, "u_wind"), l.wind)
u_f(gpu_uniform(p, "u_flutter"), l.flutter)
var mh = F_ZERO
var mh = 0.0
if not card and l.foliage { mh = model.height }
u_f(gpu_uniform(p, "u_model_h"), mh)
if not card and l.foliage and not shadow and sc_a2c { gpu_alpha_to_coverage(true) }
if card {
u_f(gpu_uniform(p, "u_card_w"), f_mul(l.atlas.radius, F_TWO)); u_f(gpu_uniform(p, "u_card_h"), l.atlas.height)
u_f(gpu_uniform(p, "u_card_w"), l.atlas.radius * 2.0); u_f(gpu_uniform(p, "u_card_h"), l.atlas.height)
r3d_bind_2d(p, "u_diff", 0, l.atlas.albedo)
r3d_bind_2d(p, "u_nrm", 1, l.atlas.normal)
gpu_alpha_to_coverage(true)
@ -1056,10 +1056,10 @@ function layer_draw_model(l: Layer, model: Model, vb: int, cnt: int, card: bool,
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_v3(gpu_uniform(p, "u_tint"), l.tint)
if sc_dbg_lod and sc_dbg_level >= 0 {
if sc_dbg_tint == null { sc_dbg_tint = v3_new(F_ONE, F_ONE, F_ONE) }
if sc_dbg_tint == null { sc_dbg_tint = v3_new(1.0, 1.0, 1.0) }
let k = sc_dbg_level
var r = F_ZERO; var g = F_ZERO; var b = F_ZERO
if k == 0 { r = fi(3) } else if k == 1 { g = fi(3) } else if k == 2 { b = fi(3) } else { r = fi(3); g = fi(3) }
var r = 0.0; var g = 0.0; var b = 0.0
if k == 0 { r = 3.0 } else if k == 1 { g = 3.0 } else if k == 2 { b = 3.0 } else { r = 3.0; g = 3.0 }
v3_set(sc_dbg_tint, r, g, b)
u_v3(gpu_uniform(p, "u_tint"), sc_dbg_tint)
}
@ -1069,7 +1069,7 @@ function layer_draw_model(l: Layer, model: Model, vb: int, cnt: int, card: bool,
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
if l.foliage { u_f(gpu_uniform(p, "u_spec_scale"), fl(0.05)) }
if l.foliage { u_f(gpu_uniform(p, "u_spec_scale"), 0.05) }
}
gpu_cull(false)
for i in 0 .. len(model.prims) {
@ -1086,7 +1086,7 @@ function layer_draw_model(l: Layer, model: Model, vb: int, cnt: int, card: bool,
if p == sc_prog_fol_eq { gpu_depth_func(GL_LESS) }
}
function layer_draw_far(l: Layer, shadow: bool, light_vp: words) -> void {
function layer_draw_far(l: Layer, shadow: bool, light_vp: floats) -> void {
if l.imp == null or (l.n_far == 0 and not l.g_on) { return }
var p = sc_imp_prog
if shadow { p = sc_imp_prog_shadow }
@ -1094,23 +1094,23 @@ function layer_draw_far(l: Layer, shadow: bool, light_vp: words) -> void {
let im = l.imp
u_f(gpu_uniform(p, "u_radius"), im.radius)
u_f(gpu_uniform(p, "u_height"), im.height)
u_f(gpu_uniform(p, "u_tiles"), fi(im.tiles))
u_f(gpu_uniform(p, "u_tiles"), float(im.tiles))
r3d_bind_2d(p, "u_atlas_albedo", 0, im.albedo)
if shadow {
u_mat4(gpu_uniform(p, "u_light_vp"), light_vp)
u_v3(gpu_uniform(p, "u_face_dir"), sun_dir)
if r3d_debug_shadow and not sc_printed { sc_printed = true; print(`imp shadow prog {p} face_dir loc {gpu_uniform(p, "u_face_dir")} sun {f_fx(sun_dir[0])} {f_fx(sun_dir[1])} {f_fx(sun_dir[2])} cam {f_fx(cam_pos[0])} {f_fx(cam_pos[1])} {f_fx(cam_pos[2])} n_far {l.n_far}`) }
if r3d_debug_shadow and not sc_printed { sc_printed = true; print(`imp shadow prog {p} face_dir loc {gpu_uniform(p, "u_face_dir")} sun {fixed(sun_dir[0])} {fixed(sun_dir[1])} {fixed(sun_dir[2])} cam {fixed(cam_pos[0])} {fixed(cam_pos[1])} {fixed(cam_pos[2])} n_far {l.n_far}`) }
u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
} else {
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_v3(gpu_uniform(p, "u_tint"), l.tint)
if sc_dbg_lod { if sc_dbg_tint == null { sc_dbg_tint = v3_new(F_ONE, F_ONE, F_ONE) }; v3_set(sc_dbg_tint, fi(3), F_ZERO, fi(3)); u_v3(gpu_uniform(p, "u_tint"), sc_dbg_tint) }
if sc_dbg_lod { if sc_dbg_tint == null { sc_dbg_tint = v3_new(1.0, 1.0, 1.0) }; v3_set(sc_dbg_tint, 3.0, 0.0, 3.0); u_v3(gpu_uniform(p, "u_tint"), sc_dbg_tint) }
r3d_bind_2d(p, "u_atlas_normal", 1, im.normal)
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
if l.foliage { u_f(gpu_uniform(p, "u_spec_scale"), fl(0.05)) }
if l.foliage { u_f(gpu_uniform(p, "u_spec_scale"), 0.05) }
}
gpu_cull(false)
if not shadow and sc_a2c { gpu_alpha_to_coverage(true) }
@ -1128,14 +1128,14 @@ function layer_draw_far(l: Layer, shadow: bool, light_vp: words) -> void {
# swap a scanned mesh in up close; the shadow must not, or a tree's shadow changes shape
# as you approach it. The card is also far the cheaper of the two, which is what pays for
# casting the whole set into all five cascades.
function layer_draw_shadow(l: Layer, light_vp: words) -> void {
function layer_draw_shadow(l: Layer, light_vp: floats) -> void {
if l.n_sh == 0 or l.imp == null { return }
let p = sc_imp_prog_shadow
gpu_use_program(p)
let im = l.imp
u_f(gpu_uniform(p, "u_radius"), im.radius)
u_f(gpu_uniform(p, "u_height"), im.height)
u_f(gpu_uniform(p, "u_tiles"), fi(im.tiles))
u_f(gpu_uniform(p, "u_tiles"), float(im.tiles))
r3d_bind_2d(p, "u_atlas_albedo", 0, im.albedo)
u_mat4(gpu_uniform(p, "u_light_vp"), light_vp)
u_v3(gpu_uniform(p, "u_face_dir"), sun_dir)
@ -1156,8 +1156,8 @@ function layer_draw_depth(l: Layer, model: Model, vb: int, cnt: int) -> void {
if cnt == 0 or model == null or vb == 0 { return }
let p = sc_prog_fol_depth
gpu_use_program(p)
var ground = F_ZERO
if l.grounded { ground = F_ONE }
var ground = 0.0
if l.grounded { ground = 1.0 }
u_f(gpu_uniform(p, "u_ground"), ground)
if l.grounded { terrain_bind_height(p) }
u_f(gpu_uniform(p, "u_time"), r3d_time)
@ -1217,7 +1217,7 @@ function scatter_draw() -> void {
# whole class of caster vanish at a fixed distance, which is exactly the popping. A layer
# with an impostor now casts its entire instance list from the card in every cascade;
# only layers that have no impostor at all fall back to the mesh.
function scatter_draw_casters(light_vp: words) -> void {
function scatter_draw_casters(light_vp: floats) -> void {
for i in 0 .. len(sc_layers) {
let l = sc_layers[i]
if sc_skip_blade and l.blade { continue }
@ -1252,11 +1252,11 @@ function scatter_draw_casters(light_vp: words) -> void {
# worlds use: geometry up close, an authored ground texture that matches it beyond).
# Returns an RGBA8 texture (alpha = coverage), repeat-wrapped and mipmapped.
var cb_state: int = 12345
function cb_rnd() -> int {
function cb_rnd() -> float {
cb_state = (cb_state * 1103515245 + 12345) & 0x7FFFFFFF
return fr((cb_state >> 8) & 0xFFFF, 65536)
return float((cb_state >> 8) & 0xFFFF) / 65536.0
}
function carpet_bake(layers: []Layer, count: int, tile: int, res: int) -> int {
function carpet_bake(layers: []Layer, count: int, tile: float, res: int) -> int {
let tex = tex_target(res, res, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
let fbo = gpu_fb_new()
gpu_fb_bind(fbo)
@ -1273,23 +1273,23 @@ function carpet_bake(layers: []Layer, count: int, tile: int, res: int) -> int {
gpu_cull(false)
gpu_blend(false)
# the clumps, and eight wrapped copies so the tile's edges continue
let half = f_mul(tile, F_HALF)
let half = tile * 0.5
let n9 = count * 9
let inst = gl_floats(n9 * INST_FLOATS)
cb_state = 977
var k = 0
for i in 0 .. count {
let x = f_sub(f_mul(cb_rnd(), tile), half)
let z = f_sub(f_mul(cb_rnd(), tile), half)
let sc = f_add(fl(1.5), cb_rnd())
let yaw = f_mul(cb_rnd(), f_mul(F_TWO, F_PI))
let x = cb_rnd() * tile - half
let z = cb_rnd() * tile - half
let sc = 1.5 + cb_rnd()
let yaw = cb_rnd() * (2.0 * PI)
let sd = cb_rnd()
for oz in 0 .. 3 {
for ox in 0 .. 3 {
let px = f_add(x, f_mul(fi(ox - 1), tile))
let pz = f_add(z, f_mul(fi(oz - 1), tile))
gl_put_bits(inst, k, px); gl_put_bits(inst, k + 1, F_ZERO); gl_put_bits(inst, k + 2, pz); gl_put_bits(inst, k + 3, sc)
gl_put_bits(inst, k + 4, f_sin(yaw)); gl_put_bits(inst, k + 5, f_cos(yaw)); gl_put_bits(inst, k + 6, sd); gl_put_bits(inst, k + 7, F_ZERO)
let px = x + float(ox - 1) * tile
let pz = z + float(oz - 1) * tile
gl_put_bits(inst, k, float_bits(px)); gl_put_bits(inst, k + 1, float_bits(0.0)); gl_put_bits(inst, k + 2, float_bits(pz)); gl_put_bits(inst, k + 3, float_bits(sc))
gl_put_bits(inst, k + 4, float_bits(Math.sin(yaw))); gl_put_bits(inst, k + 5, float_bits(Math.cos(yaw))); gl_put_bits(inst, k + 6, float_bits(sd)); gl_put_bits(inst, k + 7, float_bits(0.0))
k += INST_FLOATS
}
}
@ -1299,20 +1299,20 @@ function carpet_bake(layers: []Layer, count: int, tile: int, res: int) -> int {
free(inst)
# straight down: the window is exactly one tile
let view = m4_new(); let proj = m4_new()
let eye = v3_new(F_ZERO, fi(6), F_ZERO); let at = v3_new(F_ZERO, F_ZERO, F_ZERO); let up = v3_new(F_ZERO, F_ZERO, f_neg1())
let eye = v3_new(0.0, 6.0, 0.0); let at = v3_new(0.0, 0.0, 0.0); let up = v3_new(0.0, 0.0, -1.0)
m4_look_at(view, eye, at, up)
m4_ortho(proj, f_neg(half), half, f_neg(half), half, fl(0.1), fi(12))
m4_ortho(proj, -half, half, -half, half, 0.1, 12.0)
let bake = sc_bake_card_prog
gpu_use_program(bake)
u_mat4(gpu_uniform(bake, "u_view"), view)
u_mat4(gpu_uniform(bake, "u_proj"), proj)
u_f(gpu_uniform(bake, "u_wind"), F_ZERO)
u_f(gpu_uniform(bake, "u_flutter"), F_ZERO)
u_f(gpu_uniform(bake, "u_time"), F_ZERO)
u_f(gpu_uniform(bake, "u_wind"), 0.0)
u_f(gpu_uniform(bake, "u_flutter"), 0.0)
u_f(gpu_uniform(bake, "u_time"), 0.0)
for li in 0 .. len(layers) {
let l = layers[li]
if l.atlas == null { continue }
u_f(gpu_uniform(bake, "u_card_w"), f_mul(l.atlas.radius, F_TWO)); u_f(gpu_uniform(bake, "u_card_h"), l.atlas.height)
u_f(gpu_uniform(bake, "u_card_w"), l.atlas.radius * 2.0); u_f(gpu_uniform(bake, "u_card_h"), l.atlas.height)
r3d_bind_2d(bake, "u_diff", 0, l.atlas.albedo)
r3d_bind_2d(bake, "u_arm", 2, l.atlas.normal)
for i in 0 .. len(l.model.prims) {

View file

@ -11,15 +11,15 @@ const SHADOW_CASCADES: int = 5
var sh_tex: int = 0
var sh_fbo: int = 0
var sh_vp: words = null # 4 x 16 float bits
var sh_split: words = null # view-space far distance of each cascade
var sh_range: words = null # 4 light-frustum depth extents (metres)
var sh_texel: words = null # 4 shadow texel sizes (metres)
var sh_tmp_proj: words = null
var sh_tmp_vp: words = null
var sh_tmp_inv: words = null
var sh_tmp_view: words = null
var sh_corner: words = null
var sh_vp: floats = null # 4 x 16 float bits
var sh_split: floats = null # view-space far distance of each cascade
var sh_range: floats = null # 4 light-frustum depth extents (metres)
var sh_texel: floats = null # 4 shadow texel sizes (metres)
var sh_tmp_proj: floats = null
var sh_tmp_vp: floats = null
var sh_tmp_inv: floats = null
var sh_tmp_view: floats = null
var sh_corner: floats = null
var sh_cascade: int = 0 # the cascade being rendered (for casters that skip far ones)
function shadow_init() -> void {
@ -28,14 +28,14 @@ function shadow_init() -> void {
gpu_fb_bind(sh_fbo)
gpu_fb_no_color()
gpu_fb_bind(0)
sh_vp = words(16 * SHADOW_CASCADES)
sh_split = words(SHADOW_CASCADES)
sh_range = words(SHADOW_CASCADES)
sh_texel = words(SHADOW_CASCADES)
sh_vp = floats(16 * SHADOW_CASCADES)
sh_split = floats(SHADOW_CASCADES)
sh_range = floats(SHADOW_CASCADES)
sh_texel = floats(SHADOW_CASCADES)
# the fourth slice keeps a tree-sized texel out to a kilometre; only the massif uses the last
sh_split[0] = fi(16); sh_split[1] = fi(60); sh_split[2] = fi(250); sh_split[3] = fi(1100); sh_split[4] = fi(6000)
sh_split[0] = 16.0; sh_split[1] = 60.0; sh_split[2] = 250.0; sh_split[3] = 1100.0; sh_split[4] = 6000.0
sh_tmp_proj = m4_new(); sh_tmp_vp = m4_new(); sh_tmp_inv = m4_new(); sh_tmp_view = m4_new()
sh_corner = words(3)
sh_corner = floats(3)
}
# A shadow resolution setting: 1024, 2048 or 4096 per cascade. The depth layers are made again at
@ -83,7 +83,7 @@ function shadow_make_tex() -> void {
}
# light view-projection for the camera-frustum slice [near, far]
function shadow_fit(c: int, near: int, far: int) -> void {
function shadow_fit(c: int, near: float, far: float) -> void {
# Fit the slice in VIEW space, not world space. The bounding sphere of a frustum
# slice depends only on near/far/fov/aspect — never on where the camera is pointing —
# so computing it here makes the radius a constant per cascade. Doing it in world
@ -92,56 +92,56 @@ function shadow_fit(c: int, near: int, far: int) -> void {
# map resampled every frame: that is the crawl and flicker seen while moving.
m4_perspective(sh_tmp_proj, cam_fov, cam_aspect, near, far)
m4_inverse(sh_tmp_inv, sh_tmp_proj) # NDC -> view space
let cview = v3_new(F_ZERO, F_ZERO, F_ZERO)
let corners = words(24)
let cview = v3_new(0.0, 0.0, 0.0)
let corners = floats(24)
for i in 0 .. 8 {
var x = f_neg1(); var y = f_neg1(); var z = f_neg1()
if (i & 1) != 0 { x = F_ONE }
if (i & 2) != 0 { y = F_ONE }
if (i & 4) != 0 { z = F_ONE }
var x = -1.0; var y = -1.0; var z = -1.0
if (i & 1) != 0 { x = 1.0 }
if (i & 2) != 0 { y = 1.0 }
if (i & 4) != 0 { z = 1.0 }
let w = m4_xform_point(sh_corner, sh_tmp_inv, x, y, z)
let iw = f_div(F_ONE, w)
corners[i * 3] = f_mul(sh_corner[0], iw); corners[i * 3 + 1] = f_mul(sh_corner[1], iw); corners[i * 3 + 2] = f_mul(sh_corner[2], iw)
cview[0] = f_add(cview[0], corners[i * 3]); cview[1] = f_add(cview[1], corners[i * 3 + 1]); cview[2] = f_add(cview[2], corners[i * 3 + 2])
let iw = 1.0 / w
corners[i * 3] = sh_corner[0] * iw; corners[i * 3 + 1] = sh_corner[1] * iw; corners[i * 3 + 2] = sh_corner[2] * iw
cview[0] = cview[0] + corners[i * 3]; cview[1] = cview[1] + corners[i * 3 + 1]; cview[2] = cview[2] + corners[i * 3 + 2]
}
v3_scale(cview, cview, fr(1, 8))
var radius = F_ZERO
v3_scale(cview, cview, 1.0 / 8.0)
var radius = 0.0
for i in 0 .. 8 {
v3_set(sh_corner, corners[i * 3], corners[i * 3 + 1], corners[i * 3 + 2])
let d = v3_dist(sh_corner, cview)
if f_gt(d, radius) { radius = d }
if d > radius { radius = d }
}
radius = f_mul(radius, fl(1.05))
radius = radius * 1.05
# the slice centre back into world space
m4_inverse(sh_tmp_vp, cam_view)
let center = words(3)
let center = floats(3)
m4_xform_point(center, sh_tmp_vp, cview[0], cview[1], cview[2])
free(cview)
# light view: from far along the sun direction, looking at the centre
let eye = words(3)
let eye = floats(3)
# casters up to ~900 m toward the sun (a mountain across the valley), and the
# slice itself behind the centre: a tight depth range keeps the bias small
let back = f_add(radius, fi(900))
let back = radius + 900.0
v3_madd(eye, center, sun_dir, back)
let up = v3_new(F_ZERO, F_ONE, F_ZERO)
let up = v3_new(0.0, 1.0, 0.0)
m4_look_at(sh_tmp_view, eye, center, up)
# snap the ortho window to the shadow texel grid
let texel = f_div(f_mul(radius, F_TWO), fi(shadow_res))
let texel = radius * 2.0 / float(shadow_res)
m4_xform_point(sh_corner, sh_tmp_view, center[0], center[1], center[2])
let ox = f_sub(f_mul(f_floor(f_div(sh_corner[0], texel)), texel), sh_corner[0])
let oy = f_sub(f_mul(f_floor(f_div(sh_corner[1], texel)), texel), sh_corner[1])
let nr = f_neg(radius)
let zfar = f_add(f_add(back, radius), fi(100))
m4_ortho(sh_tmp_proj, f_add(nr, ox), f_add(radius, ox), f_add(nr, oy), f_add(radius, oy), F_ONE, zfar)
sh_range[c] = f_sub(zfar, F_ONE)
let ox = Math.floor(sh_corner[0] / texel) * texel - sh_corner[0]
let oy = Math.floor(sh_corner[1] / texel) * texel - sh_corner[1]
let nr = -radius
let zfar = back + radius + 100.0
m4_ortho(sh_tmp_proj, nr + ox, radius + ox, nr + oy, radius + oy, 1.0, zfar)
sh_range[c] = zfar - 1.0
sh_texel[c] = texel
let out = words(16)
let out = floats(16)
m4_mul(out, sh_tmp_proj, sh_tmp_view)
for i in 0 .. 16 { sh_vp[c * 16 + i] = out[i] }
free(out); free(eye); free(up); free(center); free(corners)
}
function shadow_cascade_vp(c: int) -> words { return mem_off(sh_vp, c * 64) }
function shadow_cascade_vp(c: int) -> floats { return mem_off(sh_vp, c * 64) }
# render every cascade; `draw` happens through terrain_draw_shadow + the scene's casters
@ -173,25 +173,25 @@ function shadow_pass() -> void {
if r3d_debug_shadow { shadow_dump() }
if r3d_debug_shadow and not sh_printed2 {
sh_printed2 = true
let q = words(3)
if sh_probe_x != 0 {
let q = floats(3)
if sh_probe_x != 0.0 {
let vp = shadow_cascade_vp(2)
m4_xform_point(q, vp, sh_probe_x, sh_probe_y, sh_probe_z)
print(`probe base ndc {f_fx(q[0])} {f_fx(q[1])} {f_fx(q[2])}`)
m4_xform_point(q, vp, sh_probe_x, f_add(sh_probe_y, fi(15)), sh_probe_z)
print(`probe top ndc {f_fx(q[0])} {f_fx(q[1])} {f_fx(q[2])} -> map texel {f_to_int(f_mul(f_add(f_mul(q[0], F_HALF), F_HALF), fi(shadow_res)))} {f_to_int(f_mul(f_add(f_mul(q[1], F_HALF), F_HALF), fi(shadow_res)))}`)
print(`probe base ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])}`)
m4_xform_point(q, vp, sh_probe_x, sh_probe_y + 15.0, sh_probe_z)
print(`probe top ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} -> map texel {int((q[0] * 0.5 + 0.5) * float(shadow_res))} {int((q[1] * 0.5 + 0.5) * float(shadow_res))}`)
# where the top's shadow lands on the ground: walk down the sun ray
let gx = f_sub(f_add(sh_probe_x, F_ZERO), f_mul(sun_dir[0], f_div(fi(15), sun_dir[1])))
let gz = f_sub(sh_probe_z, f_mul(sun_dir[2], f_div(fi(15), sun_dir[1])))
let gx = sh_probe_x + 0.0 - sun_dir[0] * (15.0 / sun_dir[1])
let gz = sh_probe_z - sun_dir[2] * (15.0 / sun_dir[1])
m4_xform_point(q, vp, gx, terrain_height(gx, gz), gz)
print(`shadow-of-top ground ndc {f_fx(q[0])} {f_fx(q[1])} {f_fx(q[2])} at {f_fx(gx)} {f_fx(gz)}`)
print(`shadow-of-top ground ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} at {fixed(gx)} {fixed(gz)}`)
}
for c in 0 .. SHADOW_CASCADES {
let vp = shadow_cascade_vp(c)
# a point 5 m ahead of the camera on the ground
let px = f_add(cam_pos[0], f_mul(cam_fwd[0], fi(5))); let pz = f_add(cam_pos[2], f_mul(cam_fwd[2], fi(5)))
let px = cam_pos[0] + cam_fwd[0] * 5.0; let pz = cam_pos[2] + cam_fwd[2] * 5.0
let w = m4_xform_point(q, vp, px, terrain_height(px, pz), pz)
print(`cascade {c}: ndc {f_fx(q[0])} {f_fx(q[1])} {f_fx(q[2])} w {f_fx(w)} m0 {f_fx(vp[0])} m5 {f_fx(vp[5])} m14 {f_fx(vp[14])}`)
print(`cascade {c}: ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} w {fixed(w)} m0 {fixed(vp[0])} m5 {fixed(vp[5])} m14 {fixed(vp[14])}`)
}
free(q)
}
@ -201,29 +201,29 @@ var sh_printed3: bool = false
var sh_enabled: bool = true
var sh_force: int = -1 # R3D_FORCE=<c> pins every pixel to cascade c (debug)
var sh_skip_terrain: bool = false
var sh_probe_x: int = 0
var sh_probe_y: int = 0
var sh_probe_z: int = 0
var sh_probe_x: float = 0.0
var sh_probe_y: float = 0.0
var sh_probe_z: float = 0.0
# Debug: cascade depths as grey PPMs (build/dbg_shadow_<c>.ppm)
function shadow_dump() -> void {
let n = shadow_res * shadow_res
let buf = words(n * SHADOW_CASCADES)
let buf = floats(n * SHADOW_CASCADES)
gpu_tex_bind(GPU_TEX2D_ARRAY, sh_tex)
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE)
gpu_tex_read(GPU_TEX2D_ARRAY, GL_DEPTH_COMPONENT, GL_FLOAT, buf)
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE)
if sh_probe_x != 0 {
if sh_probe_x != 0.0 {
let vp = shadow_cascade_vp(2)
let q = words(3)
m4_xform_point(q, vp, sh_probe_x, f_add(sh_probe_y, fi(12)), sh_probe_z)
let tx = f_to_int(f_mul(f_add(f_mul(q[0], F_HALF), F_HALF), fi(shadow_res)))
let ty = f_to_int(f_mul(f_add(f_mul(q[1], F_HALF), F_HALF), fi(shadow_res)))
let want = f_add(f_mul(q[2], F_HALF), F_HALF)
print(`probe (12 m up) texel {tx} {ty} card depth {f_fx(f_mul(want, fi(1000)))}/1000`)
let q = floats(3)
m4_xform_point(q, vp, sh_probe_x, sh_probe_y + 12.0, sh_probe_z)
let tx = int((q[0] * 0.5 + 0.5) * float(shadow_res))
let ty = int((q[1] * 0.5 + 0.5) * float(shadow_res))
let want = q[2] * 0.5 + 0.5
print(`probe (12 m up) texel {tx} {ty} card depth {fixed(want * 1000.0)}/1000`)
for dy in 0 .. 5 {
let yy = ty - 40 + dy * 20
print(` row {yy}: {f_fx(f_mul(buf[2 * n + yy * shadow_res + tx - 20], fi(1000)))} {f_fx(f_mul(buf[2 * n + yy * shadow_res + tx], fi(1000)))} {f_fx(f_mul(buf[2 * n + yy * shadow_res + tx + 20], fi(1000)))} /1000`)
print(` row {yy}: {fixed(buf[2 * n + yy * shadow_res + tx - 20] * 1000.0)} {fixed(buf[2 * n + yy * shadow_res + tx] * 1000.0)} {fixed(buf[2 * n + yy * shadow_res + tx + 20] * 1000.0)} /1000`)
}
free(q)
}
@ -231,10 +231,10 @@ function shadow_dump() -> void {
let row = bytes(sm * 3)
for c in 0 .. SHADOW_CASCADES {
# stretch between the map's own min and max (ignoring the far plane)
var lo = F_ONE; var hi = F_ZERO
var lo = 1.0; var hi = 0.0
var i = 0
while i < n { let d = buf[c * n + i]; if f_ls(d, fl(0.999)) { if f_ls(d, lo) { lo = d }; if f_gt(d, hi) { hi = d } }; i += 97 }
print(`cascade {c} depth range {f_fx(lo)} .. {f_fx(hi)}`)
while i < n { let d = buf[c * n + i]; if d < 0.999 { if d < lo { lo = d }; if d > hi { hi = d } }; i += 97 }
print(`cascade {c} depth range {fixed(lo)} .. {fixed(hi)}`)
let f = file_open(`build/dbg_shadow_{c}.ppm`, "wb")
let hdr = `P6\n{sm} {sm}\n255\n`
file_write(f, hdr, len(hdr))
@ -242,7 +242,7 @@ function shadow_dump() -> void {
for y in 0 .. sm {
for x in 0 .. sm {
let d = buf[c * n + (y * st) * shadow_res + x * st]
let g = f_to_int(f_mul(f_clamp(f_div(f_sub(d, lo), f_max(f_sub(hi, lo), fl(0.0001))), F_ZERO, F_ONE), fi(255)))
let g = int(Math.clamp((d - lo) / Math.max(hi - lo, 0.0001), 0.0, 1.0) * 255.0)
row[x * 3] = g; row[x * 3 + 1] = g; row[x * 3 + 2] = g
}
file_write(f, row, sm * 3)
@ -263,19 +263,19 @@ function shadow_bind(prog: int) -> void {
r3d_bind_tex(prog, "u_shadow", 15, GPU_TEX2D_ARRAY, sh_tex)
# the height-field shadow (terrain.ludic); a stand-in texture keeps the unit valid before the bake
var ts = ter_shadow_tex
var ts_on = F_ONE
if ts == 0 { ts = ter_height_tex; ts_on = F_ZERO }
var ts_on = 1.0
if ts == 0 { ts = ter_height_tex; ts_on = 0.0 }
r3d_bind_2d(prog, "u_tershadow", 6, ts)
terrain_bind_height(prog)
u_f(gpu_uniform(prog, "u_ts_on"), ts_on)
u_f(gpu_uniform(prog, "u_ts_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(prog, "u_ts_half"), float(TERRAIN_HALF))
u_f2(gpu_uniform(prog, "u_ts_origin"), ter_ox, ter_oz)
var loc = gpu_uniform(prog, "u_cascade_vp[0]")
if loc < 0 { loc = gpu_uniform(prog, "u_cascade_vp") }
if r3d_debug_shadow and not sh_printed { sh_printed = true; print(`cascade vp loc {loc} / {gpu_uniform(prog, "u_cascade_vp")} split loc {gpu_uniform(prog, "u_cascade_split")} shadow loc {gpu_uniform(prog, "u_shadow")}`) }
u_mat4n(loc, SHADOW_CASCADES, sh_vp)
u_fv(sh_loc(prog, "u_cascade_split"), SHADOW_CASCADES, sh_split)
if r3d_debug_shadow and not sh_printed3 { sh_printed3 = true; print(`range {f_fx(sh_range[0])} {f_fx(sh_range[1])} {f_fx(sh_range[2])} {f_fx(sh_range[3])} texel*1000 {f_fx(f_mul(sh_texel[0], fi(1000)))} {f_fx(f_mul(sh_texel[1], fi(1000)))} {f_fx(f_mul(sh_texel[2], fi(1000)))} {f_fx(f_mul(sh_texel[3], fi(1000)))} locs {gpu_uniform(prog, "u_cascade_range")} {gpu_uniform(prog, "u_cascade_texel")}`) }
if r3d_debug_shadow and not sh_printed3 { sh_printed3 = true; print(`range {fixed(sh_range[0])} {fixed(sh_range[1])} {fixed(sh_range[2])} {fixed(sh_range[3])} texel*1000 {fixed(sh_texel[0] * 1000.0)} {fixed(sh_texel[1] * 1000.0)} {fixed(sh_texel[2] * 1000.0)} {fixed(sh_texel[3] * 1000.0)} locs {gpu_uniform(prog, "u_cascade_range")} {gpu_uniform(prog, "u_cascade_texel")}`) }
u_fv(sh_loc(prog, "u_cascade_range"), SHADOW_CASCADES, sh_range)
if r3d_env_has("R3D_FORCE") { sh_force = Text.to_int(r3d_env("R3D_FORCE")) }
u_i(gpu_uniform(prog, "u_force_cascade"), sh_force)

View file

@ -19,28 +19,28 @@ property Skin {
n_nodes: int = 0,
par: words, # parent node per node, -1 at a root
walk: words, # the nodes ordered parents-first
rest_t: words, # 3 per node
rest_r: words, # 4 per node (x, y, z, w)
rest_s: words, # 3 per node
rest_g: words, # 4 per node: the global rest rotation
rest_t: floats, # 3 per node
rest_r: floats, # 4 per node (x, y, z, w)
rest_s: floats, # 3 per node
rest_g: floats, # 4 per node: the global rest rotation
names: []string,
pose_r: words, # 4 per node: the pose rotation, model frame
pose_t: words, # 3 per node: the pose offset, model frame (metres)
pose_r: floats, # 4 per node: the pose rotation, model frame
pose_t: floats, # 3 per node: the pose offset, model frame (metres)
gmat: words, # 16 per node: global matrix this pose
n_joints: int = 0,
joints: words, # node index per joint
inv_bind: words, # 16 per joint
bones: words, # 16 per joint: what the vertex shader skins with
tmp_l: words,
tmp_q: words,
tmp_a: words,
tmp_b: words,
tmp_c: words,
tmp_v: words
bones: floats, # 16 per joint: what the vertex shader skins with
tmp_l: floats,
tmp_q: floats,
tmp_a: floats,
tmp_b: floats,
tmp_c: floats,
tmp_v: floats
}
# a 3-vector of a JSON array (float bits), or a default
function skin_jv3(o: words, at: int, nd: Val, key: pointer, dx: int, dy: int, dz: int) -> void {
function skin_jv3(o: floats, at: int, nd: Val, key: pointer, dx: float, dy: float, dz: float) -> void {
if value_has(nd, key) == 0 { o[at] = dx; o[at + 1] = dy; o[at + 2] = dz; return }
let arr = value_get(nd, key)
for i in 0 .. 3 { o[at + i] = jnum(value_at(arr, i)) }
@ -75,22 +75,22 @@ function skin_load(idx: int) -> Skin {
let n = value_count(nodes)
sk.n_nodes = n
sk.par = words(n); sk.walk = words(n)
sk.rest_t = words(n * 3); sk.rest_r = words(n * 4); sk.rest_s = words(n * 3); sk.rest_g = words(n * 4)
sk.pose_r = words(n * 4); sk.pose_t = words(n * 3); sk.gmat = words(n * 16)
sk.rest_t = floats(n * 3); sk.rest_r = floats(n * 4); sk.rest_s = floats(n * 3); sk.rest_g = floats(n * 4)
sk.pose_r = floats(n * 4); sk.pose_t = floats(n * 3); sk.gmat = words(n * 16)
sk.names = new []string
sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = words(3)
sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = floats(3)
for i in 0 .. n { sk.par[i] = -1 }
for i in 0 .. n {
let nd = value_at(nodes, i)
var nm: string = ""
if value_has(nd, "name") != 0 { nm = value_as_str(value_get(nd, "name")) }
push(sk.names, nm)
skin_jv3(sk.rest_t, i * 3, nd, "translation", F_ZERO, F_ZERO, F_ZERO)
skin_jv3(sk.rest_s, i * 3, nd, "scale", F_ONE, F_ONE, F_ONE)
skin_jv3(sk.rest_t, i * 3, nd, "translation", 0.0, 0.0, 0.0)
skin_jv3(sk.rest_s, i * 3, nd, "scale", 1.0, 1.0, 1.0)
if value_has(nd, "rotation") != 0 {
let r = value_get(nd, "rotation")
for k in 0 .. 4 { sk.rest_r[i * 4 + k] = jnum(value_at(r, k)) }
} else { sk.rest_r[i * 4] = F_ZERO; sk.rest_r[i * 4 + 1] = F_ZERO; sk.rest_r[i * 4 + 2] = F_ZERO; sk.rest_r[i * 4 + 3] = F_ONE }
} else { sk.rest_r[i * 4] = 0.0; sk.rest_r[i * 4 + 1] = 0.0; sk.rest_r[i * 4 + 2] = 0.0; sk.rest_r[i * 4 + 3] = 1.0 }
if value_has(nd, "matrix") != 0 { print(`skin: node {nm} uses a matrix transform (unsupported, treated as identity)`) }
if value_has(nd, "children") != 0 {
let ch = value_get(nd, "children")
@ -124,7 +124,7 @@ function skin_load(idx: int) -> Skin {
sk.n_joints = nj
sk.joints = words(nj)
sk.inv_bind = words(nj * 16)
sk.bones = words(nj * 16)
sk.bones = floats(nj * 16)
for j in 0 .. nj { sk.joints[j] = value_as_int(value_at(jl, j)) }
if value_has(skv, "inverseBindMatrices") != 0 {
let ib = gltf_accessor(value_as_int(value_get(skv, "inverseBindMatrices")))
@ -144,24 +144,24 @@ function skin_find(sk: Skin, name: string) -> int {
print(`skin: no node {name}`)
return -1
}
function skin_mat(sk: Skin, node: int) -> words { return mem_off(sk.gmat, node * 64) }
function skin_mat(sk: Skin, node: int) -> floats { return mem_off(sk.gmat, node * 64) }
# back to the rest pose
function skin_reset(sk: Skin) -> void {
for i in 0 .. sk.n_nodes {
sk.pose_r[i * 4] = F_ZERO; sk.pose_r[i * 4 + 1] = F_ZERO; sk.pose_r[i * 4 + 2] = F_ZERO; sk.pose_r[i * 4 + 3] = F_ONE
sk.pose_t[i * 3] = F_ZERO; sk.pose_t[i * 3 + 1] = F_ZERO; sk.pose_t[i * 3 + 2] = F_ZERO
sk.pose_r[i * 4] = 0.0; sk.pose_r[i * 4 + 1] = 0.0; sk.pose_r[i * 4 + 2] = 0.0; sk.pose_r[i * 4 + 3] = 1.0
sk.pose_t[i * 3] = 0.0; sk.pose_t[i * 3 + 1] = 0.0; sk.pose_t[i * 3 + 2] = 0.0
}
}
# a node's pose rotation in the model frame: pitch about X, yaw about Y, roll about Z (radians)
function skin_set_rot(sk: Skin, node: int, pitch: int, yaw: int, roll: int) -> void {
function skin_set_rot(sk: Skin, node: int, pitch: float, yaw: float, roll: float) -> void {
if node < 0 { return }
q_euler(sk.tmp_q, pitch, yaw, roll)
q_store(sk.pose_r, node, sk.tmp_q)
}
function skin_set_quat(sk: Skin, node: int, q: words) -> void { if node >= 0 { q_store(sk.pose_r, node, q) } }
function skin_set_quat(sk: Skin, node: int, q: floats) -> void { if node >= 0 { q_store(sk.pose_r, node, q) } }
# a node's pose offset in the model frame (metres)
function skin_set_offset(sk: Skin, node: int, x: int, y: int, z: int) -> void {
function skin_set_offset(sk: Skin, node: int, x: float, y: float, z: float) -> void {
if node < 0 { return }
sk.pose_t[node * 3] = x; sk.pose_t[node * 3 + 1] = y; sk.pose_t[node * 3 + 2] = z
}
@ -179,12 +179,12 @@ function skin_pose(sk: Skin) -> void {
q_conj(sk.tmp_c, sk.tmp_b) # G_p^-1
q_mul(sk.tmp_q, sk.tmp_c, sk.tmp_a)
q_mul(sk.tmp_a, sk.tmp_q, sk.tmp_b) # G_p^-1 D G_p
if ox != 0 or oy != 0 or oz != 0 {
if ox != 0.0 or oy != 0.0 or oz != 0.0 {
v3_set(sk.tmp_v, ox, oy, oz)
q_rotate(sk.tmp_v, sk.tmp_c, sk.tmp_v) # the offset in the parent's frame
tx = f_add(tx, sk.tmp_v[0]); ty = f_add(ty, sk.tmp_v[1]); tz = f_add(tz, sk.tmp_v[2])
tx = tx + sk.tmp_v[0]; ty = ty + sk.tmp_v[1]; tz = tz + sk.tmp_v[2]
}
} else { tx = f_add(tx, ox); ty = f_add(ty, oy); tz = f_add(tz, oz) }
} else { tx = tx + ox; ty = ty + oy; tz = tz + oz }
q_load(sk.tmp_b, sk.rest_r, i)
q_mul(sk.tmp_q, sk.tmp_a, sk.tmp_b) # local rotation
m4_trs_q(sk.tmp_l, tx, ty, tz, sk.tmp_q, sk.rest_s[i * 3], sk.rest_s[i * 3 + 1], sk.rest_s[i * 3 + 2])
@ -211,9 +211,9 @@ function skin_clone(src: Skin) -> Skin {
sk.names = src.names
sk.n_joints = src.n_joints; sk.joints = src.joints; sk.inv_bind = src.inv_bind
let n = src.n_nodes
sk.pose_r = words(n * 4); sk.pose_t = words(n * 3); sk.gmat = words(n * 16)
sk.bones = words(src.n_joints * 16)
sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = words(3)
sk.pose_r = floats(n * 4); sk.pose_t = floats(n * 3); sk.gmat = words(n * 16)
sk.bones = floats(src.n_joints * 16)
sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = floats(3)
skin_reset(sk)
skin_pose(sk)
return sk

View file

@ -13,70 +13,70 @@ var sky_h: int = 0
var sky_irradiance: int = 0
var sky_prefilter: int = 0 # GL_TEXTURE_2D_ARRAY
var sky_brdf: int = 0
var sun_dir: words = null # toward the sun (float bits)
var sun_color: words = null # radiance (float bits)
var sun_dir: floats = null # toward the sun (float bits)
var sun_color: floats = null # radiance (float bits)
var sky_fullscreen: Mesh = null
var sky_yaw: int = 0 # radians: the HDRI is turned by this about y
var sky_sun_boost: int = 0x40133333 # 2.3: the photograph's thin cloud dims its sun; a crisper day wants more
var sky_rot_s: int = 0
var sky_rot_c: int = 0
var sun_hdri: words = null # the sun direction as found in the file
var sky_yaw: float = 0.0 # radians: the HDRI is turned by this about y
var sky_sun_boost: float = 2.3 # 2.3: the photograph's thin cloud dims its sun; a crisper day wants more
var sky_rot_s: float = 0.0
var sky_rot_c: float = 0.0
var sun_hdri: floats = null # the sun direction as found in the file
# The yaw to bake the light at the first time (radians, float bits). A game that turns the sky at
# start sets it before r3d_init, so the image-based light is baked once at that yaw rather than at
# 0 and then again - which is what sky_set_yaw at boot used to cost.
var sky_start_yaw: int = 0
var sky_start_yaw: float = 0.0
var sky_baked: bool = false # the IBL set exists, baked at sky_baked_yaw
var sky_baked_yaw: int = 0
var sky_baked_yaw: float = 0.0
var sky_p_irr: int = 0
var sky_p_pre: int = 0
var sky_p_brdf: int = 0
# turn the HDRI so its sun sits at world azimuth `yaw` (radians, 0 = toward -z)
function sky_set_yaw(yaw: int) -> void {
function sky_set_yaw(yaw: float) -> void {
sky_set_rot(yaw)
# world sun = rotY(sun_hdri, -yaw): the lookup rotates a world direction by +yaw
let s = sun_hdri
v3_set(sun_dir, f_sub(f_mul(sky_rot_c, s[0]), f_mul(sky_rot_s, s[2])), s[1], f_add(f_mul(sky_rot_s, s[0]), f_mul(sky_rot_c, s[2])))
v3_set(sun_dir, sky_rot_c * s[0] - sky_rot_s * s[2], s[1], sky_rot_s * s[0] + sky_rot_c * s[2])
# the light is already baked at this yaw: turning to it again bakes nothing
if sky_baked and yaw == sky_baked_yaw { return }
sky_precompute()
}
# turn only the visible sky image (cheap, per frame): the light and the convolved
# maps stay where they are — daylight.ludic moves those on its own terms
function sky_set_rot(yaw: int) -> void {
function sky_set_rot(yaw: float) -> void {
sky_yaw = yaw
sky_rot_s = f_sin(yaw); sky_rot_c = f_cos(yaw)
sky_rot_s = Math.sin(yaw); sky_rot_c = Math.cos(yaw)
}
function sky_bind_rot(prog: int) -> void { u_f2(gpu_uniform(prog, "u_sky_rot"), sky_rot_s, sky_rot_c) }
# direction for an equirect uv (matches equirectUV in lighting.glsl)
function sky_dir_from_uv(o: words, u: int, v: int) -> void {
let phi = f_mul(f_sub(u, F_HALF), f_mul(F_TWO, F_PI))
let theta = f_mul(v, F_PI)
let st = f_sin(theta)
v3_set(o, f_mul(st, f_sin(phi)), f_cos(theta), f_neg(f_mul(st, f_cos(phi))))
function sky_dir_from_uv(o: floats, u: float, v: float) -> void {
let phi = (u - 0.5) * (2.0 * PI)
let theta = v * PI
let st = Math.sin(theta)
v3_set(o, st * Math.sin(phi), Math.cos(theta), -(st * Math.cos(phi)))
}
function sky_load(path: string) -> bool {
sky_tex = tex_load_hdr(path)
if sky_tex == 0 { return false }
sky_w = tex_w; sky_h = tex_h
sun_dir = words(3)
sun_hdri = words(3)
sky_dir_from_uv(sun_hdri, fr(hdr_max_x * 2 + 1, sky_w * 2), fr(hdr_max_y * 2 + 1, sky_h * 2))
sun_dir = floats(3)
sun_hdri = floats(3)
sky_dir_from_uv(sun_hdri, float(hdr_max_x * 2 + 1) / float(sky_w * 2), float(hdr_max_y * 2 + 1) / float(sky_h * 2))
v3_copy(sun_dir, sun_hdri)
sky_rot_c = F_ONE
sky_rot_c = 1.0
# the sun's irradiance is what the IBL clip leaves out of the map; lighting it
# directly with that keeps sun and sky in the photograph's own proportion
sun_color = v3_new(f_mul(hdr_sun_r, sky_sun_boost), f_mul(hdr_sun_g, sky_sun_boost), f_mul(hdr_sun_b, sky_sun_boost))
print(`sun irradiance: {f_fx(hdr_sun_r)} {f_fx(hdr_sun_g)} {f_fx(hdr_sun_b)} (Q16.16), clip {f_fx(hdr_clip)}`)
sun_color = v3_new(hdr_sun_r * sky_sun_boost, hdr_sun_g * sky_sun_boost, hdr_sun_b * sky_sun_boost)
print(`sun irradiance: {fixed(hdr_sun_r)} {fixed(hdr_sun_g)} {fixed(hdr_sun_b)} (Q16.16), clip {fixed(hdr_clip)}`)
print(`sky: {sky_w}x{sky_h}, sun at texel {hdr_max_x},{hdr_max_y}`)
sky_fullscreen = mesh_fullscreen()
if sky_start_yaw != 0 { sky_set_yaw(sky_start_yaw) } else { sky_precompute() }
if sky_start_yaw != 0.0 { sky_set_yaw(sky_start_yaw) } else { sky_precompute() }
return true
}
function sky_convolve(prog: int, target_tex: int, layer: int, w: int, h: int, rough: int) -> void {
function sky_convolve(prog: int, target_tex: int, layer: int, w: int, h: int, rough: float) -> void {
let fbo = gpu_fb_new()
gpu_fb_bind(fbo)
if layer < 0 { gpu_fb_color(0, target_tex) }
@ -87,7 +87,7 @@ function sky_convolve(prog: int, target_tex: int, layer: int, w: int, h: int, ro
sky_bind_rot(prog)
u_f(gpu_uniform(prog, "u_sun_clip"), hdr_clip)
u_f(gpu_uniform(prog, "u_rough"), rough)
u_f(gpu_uniform(prog, "u_sky_w"), fi(sky_w))
u_f(gpu_uniform(prog, "u_sky_w"), float(sky_w))
mesh_draw(sky_fullscreen)
gpu_fb_bind(0)
gpu_fb_free(fbo)
@ -115,7 +115,7 @@ function sky_precompute() -> void {
sky_irradiance = tex_target(128, 64, GL_RGB16F, GL_RGB, GL_FLOAT, GL_LINEAR)
gpu_tex_bind(GPU_TEX2D, sky_irradiance)
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
sky_convolve(p_irr, sky_irradiance, -1, 128, 64, F_ZERO)
sky_convolve(p_irr, sky_irradiance, -1, 128, 64, 0.0)
# prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array
let p_pre = sky_p_pre
sky_prefilter = gpu_tex_new()
@ -126,12 +126,12 @@ function sky_precompute() -> void {
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
for l in 0 .. SKY_PREFILTER_LEVELS {
sky_convolve(p_pre, sky_prefilter, l, sky_prefilter_w, sky_prefilter_w / 2, fr(l, SKY_PREFILTER_LEVELS - 1))
sky_convolve(p_pre, sky_prefilter, l, sky_prefilter_w, sky_prefilter_w / 2, float(l) / float(SKY_PREFILTER_LEVELS - 1))
}
# BRDF LUT
let p_brdf = sky_p_brdf
sky_brdf = tex_target(256, 256, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
sky_convolve(p_brdf, sky_brdf, -1, 256, 256, F_ZERO)
sky_convolve(p_brdf, sky_brdf, -1, 256, 256, 0.0)
sky_baked = true
sky_baked_yaw = sky_yaw
gpu_check("sky precompute")
@ -145,6 +145,6 @@ function sky_bind_lighting(prog: int) -> void {
u_v3(gpu_uniform(prog, "u_sun_dir"), sun_dir)
u_v3(gpu_uniform(prog, "u_sun_color"), sun_color)
u_v3(gpu_uniform(prog, "u_cam_pos"), cam_pos)
u_f(gpu_uniform(prog, "u_prefilter_levels"), fi(SKY_PREFILTER_LEVELS))
u_f(gpu_uniform(prog, "u_prefilter_levels"), float(SKY_PREFILTER_LEVELS))
daylight_bind(prog)
}

View file

@ -19,14 +19,14 @@ property Chunk {
used: int = 0, # the walk that last wanted it (for eviction)
data: words, # INST_FLOATS per instance
count: int = 0,
ymin: int = 0, # height range of its instances (float bits), for the frustum test
ymax: int = 0
ymin: float = 0.0, # height range of its instances (float bits), for the frustum test
ymax: float = 0.0
}
property Stream {
layer: Layer,
size: int = 0, # chunk size (metres, float bits)
reach: int = 0, # radius (metres, float bits)
bands: words, # band outer radii (float bits), ascending; 4 of them
size: float = 0.0, # chunk size (metres, float bits)
reach: float = 0.0, # radius (metres, float bits)
bands: floats, # band outer radii (float bits), ascending; 4 of them
chunks: []Chunk,
keys: words, # parallel to chunks for lookup
n: int = 0,
@ -51,7 +51,7 @@ var stream_us_gather: long = 0 # copying cached chunks into the layer buffer
var stream_us_walk: long = 0 # the ring walk itself
var stream_walks: int = 0 # streams that walked their whole ring this frame
function stream_new(layer: Layer, size: int, reach: int, b0: int, b1: int, b2: int, b3: int) -> Stream {
function stream_new(layer: Layer, size: float, reach: float, b0: float, b1: float, b2: float, b3: float) -> Stream {
if not stream_cap_read {
stream_cap_read = true
if r3d_env_has("R3D_STREAM_CAP") { STREAM_MAX_CHUNKS = Text.to_int(r3d_env("R3D_STREAM_CAP")) }
@ -61,7 +61,7 @@ function stream_new(layer: Layer, size: int, reach: int, b0: int, b1: int, b2: i
s.layer = layer; s.size = size; s.reach = reach
layer.streamed = true
layer.grounded = true
s.bands = words(4)
s.bands = floats(4)
s.bands[0] = b0; s.bands[1] = b1; s.bands[2] = b2; s.bands[3] = b3
s.chunks = new []Chunk
s.keys = words(STREAM_MAX_CHUNKS)
@ -108,23 +108,23 @@ function stream_remember(s: Stream, key: int, idx: int) -> void {
# chunk gets an exactly-sized copy when the fill ends)
const STREAM_CHUNK_MAX: int = 262144
var stream_debug_n: int = 0
var stream_scratch: words = null
function stream_emit(s: Stream, x: int, y: int, z: int, scale: int, yaw: int, seed: int, wind: int) -> void {
var stream_scratch: floats = null
function stream_emit(s: Stream, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float) -> void {
let c = s.cur
if c.count >= STREAM_CHUNK_MAX { return }
if stream_scratch == null { stream_scratch = words(STREAM_CHUNK_MAX * INST_FLOATS) }
if c.count == 0 { c.ymin = y; c.ymax = y } else { c.ymin = f_min(c.ymin, y); c.ymax = f_max(c.ymax, y) }
if stream_scratch == null { stream_scratch = floats(STREAM_CHUNK_MAX * INST_FLOATS) }
if c.count == 0 { c.ymin = y; c.ymax = y } else { c.ymin = Math.min(c.ymin, y); c.ymax = Math.max(c.ymax, y) }
let o = c.count * INST_FLOATS
stream_scratch[o] = x; stream_scratch[o + 1] = y; stream_scratch[o + 2] = z; stream_scratch[o + 3] = scale
stream_scratch[o + 4] = f_sin(yaw); stream_scratch[o + 5] = f_cos(yaw); stream_scratch[o + 6] = seed; stream_scratch[o + 7] = wind
stream_scratch[o + 4] = Math.sin(yaw); stream_scratch[o + 5] = Math.cos(yaw); stream_scratch[o + 6] = seed; stream_scratch[o + 7] = wind
c.count += 1
}
function stream_band(s: Stream, d: int) -> int {
if f_ls(d, s.bands[0]) { return 0 }
if f_ls(d, s.bands[1]) { return 1 }
if f_ls(d, s.bands[2]) { return 2 }
if f_ls(d, s.bands[3]) { return 3 }
function stream_band(s: Stream, d: float) -> int {
if d < s.bands[0] { return 0 }
if d < s.bands[1] { return 1 }
if d < s.bands[2] { return 2 }
if d < s.bands[3] { return 3 }
return 4
}
@ -193,9 +193,9 @@ function stream_evict(s: Stream) -> void {
stream_evictions += 1
}
function stream_update(s: Stream, cam_x: int, cam_z: int) -> void {
let ccx = f_to_int(f_floor(f_div(cam_x, s.size)))
let ccz = f_to_int(f_floor(f_div(cam_z, s.size)))
function stream_update(s: Stream, cam_x: float, cam_z: float) -> void {
let ccx = int(Math.floor(cam_x / s.size))
let ccz = int(Math.floor(cam_z / s.size))
if ccx == s.last_cx and ccz == s.last_cz and not s.pending and s.view_gen == sc_view_gen { return }
let first = s.last_cx == 999999
s.view_gen = sc_view_gen
@ -206,7 +206,7 @@ function stream_update(s: Stream, cam_x: int, cam_z: int) -> void {
stream_walks += 1
stream_walk_no += 1
let tw = gl_now_us()
let r = f_to_int(f_div(s.reach, s.size)) + 1
let r = int(s.reach / s.size) + 1
# rings outward from the camera's cell: the nearest chunks are generated first
var ring = 0
while ring <= r {
@ -216,12 +216,12 @@ function stream_update(s: Stream, cam_x: int, cam_z: int) -> void {
while cx <= ccx + ring {
let edge = (cz == ccz - ring) or (cz == ccz + ring) or (cx == ccx - ring) or (cx == ccx + ring)
if edge {
let wx = f_mul(f_add(fi(cx), F_HALF), s.size)
let wz = f_mul(f_add(fi(cz), F_HALF), s.size)
let dx = f_sub(wx, cam_x); let dz = f_sub(wz, cam_z)
let d = f_sqrt(f_add(f_mul(dx, dx), f_mul(dz, dz)))
let wx = (float(cx) + 0.5) * s.size
let wz = (float(cz) + 0.5) * s.size
let dx = wx - cam_x; let dz = wz - cam_z
let d = Math.sqrt(dx * dx + dz * dz)
let band = stream_band(s, d)
if band < 4 and band >= s.min_band and f_ls(d, f_add(s.reach, s.size)) {
if band < 4 and band >= s.min_band and d < s.reach + s.size {
let key = stream_key(cx, cz, band)
var c = stream_find(s, key)
if c != null { c.used = stream_walk_no }
@ -283,12 +283,12 @@ function stream_update(s: Stream, cam_x: int, cam_z: int) -> void {
# height range, padded for the tallest cover and for casters just outside the frame
# whose short shadows still fall inside it.
function stream_chunk_visible(s: Stream, cx: int, cz: int, c: Chunk) -> bool {
let half = f_mul(s.size, F_HALF)
let wx = f_add(f_mul(fi(cx), s.size), half)
let wz = f_add(f_mul(fi(cz), s.size), half)
let hy = f_mul(f_sub(c.ymax, c.ymin), F_HALF)
let cy = f_add(c.ymin, hy)
let r = f_add(f_sqrt(f_add(f_mul(f_mul(half, half), F_TWO), f_mul(hy, hy))), fi(8))
let half = s.size * 0.5
let wx = float(cx) * s.size + half
let wz = float(cz) * s.size + half
let hy = (c.ymax - c.ymin) * 0.5
let cy = c.ymin + hy
let r = Math.sqrt(half * half * 2.0 + hy * hy) + 8.0
return cam_sphere_visible(wx, cy, wz, r)
}

View file

@ -237,12 +237,12 @@ var gsl_consts: bytes = null
var gsl_tags: bytes = null
var gsl_res: bytes = null
var gsl_inputs: bytes = null
var gsl_prev_vp: words = null
var gsl_prev_vp: floats = null
var gsl_reset: bool = true
var gsl_jitter_x: int = 0 # float bits, NDC offsets the projection carries this frame
var gsl_jitter_y: int = 0
var gsl_jpx: int = 0 # float bits, the same in pixels
var gsl_jpy: int = 0
var gsl_jitter_x: float = 0.0 # float bits, NDC offsets the projection carries this frame
var gsl_jitter_y: float = 0.0
var gsl_jpx: float = 0.0 # float bits, the same in pixels
var gsl_jpy: float = 0.0
var gsl_eval_ok: bool = true # the last evaluate worked: only then is the next frame jittered
# R3D_DLSS=0..4 overrides the setting, for a headless take
@ -271,8 +271,8 @@ function gsl_fill_options(w: int, h: int) -> void {
Vk.put_i32(gsl_opts, 32, gsl_sl_mode(gsl_dlss_mode))
Vk.put_i32(gsl_opts, 36, w)
Vk.put_i32(gsl_opts, 40, h)
Vk.put_i32(gsl_opts, 48, F_ONE) # preExposure
Vk.put_i32(gsl_opts, 52, F_ONE) # exposureScale
Vk.put_i32(gsl_opts, 48, float_bits(1.0)) # preExposure
Vk.put_i32(gsl_opts, 52, float_bits(1.0)) # exposureScale
Vk.put_i32(gsl_opts, 56, 1) # colorBuffersHDR eTrue
# The model: preset K (the transformer NVIDIA calls its best image quality) in every mode. The
# defaults put Performance on preset M, which on an RTX 3070 Ti at 4K evaluated in 18 ms against
@ -303,14 +303,14 @@ function r3d_dlss_render_w() -> int { if not r3d_dlss_live() { return gl_w }; g
function r3d_dlss_render_h() -> int { if not r3d_dlss_live() { return gl_h }; gsl_optimal(); return gsl_rh }
# a radical-inverse sample in [0, 1), float bits
function gsl_halton(i: int, b: int) -> int {
var f = F_ONE
var r = F_ZERO
function gsl_halton(i: int, b: int) -> float {
var f = 1.0
var r = 0.0
var k = i
let fb = fi(b)
let fb = float(b)
while k > 0 {
f = f_div(f, fb)
r = f_add(r, f_mul(f, fi(k % b)))
f = f / fb
r = r + f * float(k % b)
k = k / b
}
return r
@ -318,17 +318,17 @@ function gsl_halton(i: int, b: int) -> int {
# cam_begin_frame: this frame's sub-pixel offset, before the camera builds its matrices
function gsl_jitter_frame() -> void {
gsl_jitter_x = F_ZERO; gsl_jitter_y = F_ZERO; gsl_jpx = F_ZERO; gsl_jpy = F_ZERO
gsl_jitter_x = 0.0; gsl_jitter_y = 0.0; gsl_jpx = 0.0; gsl_jpy = 0.0
# a jittered frame nobody resolves shakes on screen however still the camera is: jitter only while
# this frame holds a DLSS token and the last evaluate worked. (Not gsl_fresh: gsl_frame_start has
# already taken the token and cleared it by the time the camera asks, which turned jitter off.)
if not r3d_dlss_live() or not gsl_eval_ok or gsl_token == null or post_w <= 0 or post_h <= 0 { return }
# DLSS wants at least 8 x (display / render)^2 phases; 32 covers performance mode
let i = (gsl_frame_n % 32) + 1
gsl_jpx = f_sub(gsl_halton(i, 2), F_HALF)
gsl_jpy = f_sub(gsl_halton(i, 3), F_HALF)
gsl_jitter_x = f_div(f_mul(F_TWO, gsl_jpx), fi(post_w))
gsl_jitter_y = f_div(f_mul(F_TWO, gsl_jpy), fi(post_h))
gsl_jpx = gsl_halton(i, 2) - 0.5
gsl_jpy = gsl_halton(i, 3) - 0.5
gsl_jitter_x = 2.0 * gsl_jpx / float(post_w)
gsl_jitter_y = 2.0 * gsl_jpy / float(post_h)
}
# sl::Resource for one of the renderer's textures, in the layout every pass leaves them in
@ -370,21 +370,21 @@ function gsl_tag(i: int, buffer: int, w: int, h: int) -> void {
}
# a column-major matrix into a row-major sl::float4x4
function gsl_put_m4(p: pointer, at: int, m: words) -> void {
for r in 0 .. 4 { for c in 0 .. 4 { Vk.put_i32(p, at + (r * 4 + c) * 4, m[c * 4 + r]) } }
function gsl_put_m4(p: pointer, at: int, m: floats) -> void {
for r in 0 .. 4 { for c in 0 .. 4 { Vk.put_i32(p, at + (r * 4 + c) * 4, float_bits(m[c * 4 + r])) } }
}
function gsl_put_v3(p: pointer, at: int, v: words) -> void {
Vk.put_i32(p, at, v[0]); Vk.put_i32(p, at + 4, v[1]); Vk.put_i32(p, at + 8, v[2])
function gsl_put_v3(p: pointer, at: int, v: floats) -> void {
Vk.put_i32(p, at, float_bits(v[0])); Vk.put_i32(p, at + 4, float_bits(v[1])); Vk.put_i32(p, at + 8, float_bits(v[2]))
}
# The renderer's clip space is OpenGL's; what Vulkan stores is depth remapped to [0, 1] and
# row 0 at NDC y = -1. Streamline reads images with row 0 at the top, so the matrices it is
# given carry both: y flipped, z' = (z + w) / 2.
function gsl_clip_fix(m: words) -> void {
function gsl_clip_fix(m: floats) -> void {
m4_identity(m)
m[5] = f_neg(F_ONE)
m[10] = F_HALF
m[14] = F_HALF
m[5] = -1.0
m[10] = 0.5
m[14] = 0.5
}
function gsl_constants() -> void {
@ -414,30 +414,30 @@ function gsl_constants() -> void {
# the sample's offset from the pixel centre, in the image Streamline sees: row 0 at the top, so the
# vertical offset flips with it. With jy unflipped DLSS resolved the ground into concentric
# rings; with jx flipped too, thin stems doubled sideways (PC shots, 2026-09-15).
var jx = f_neg(gsl_jpx)
var jy = f_neg(gsl_jpy)
var jx = -gsl_jpx
var jy = -gsl_jpy
# R3D_DLSS_JX / R3D_DLSS_JY = -1 flip a sign, to check the convention against the picture
if r3d_env_has("R3D_DLSS_JX") and Text.to_int(r3d_env("R3D_DLSS_JX")) < 0 { jx = f_neg(jx) }
if r3d_env_has("R3D_DLSS_JY") and Text.to_int(r3d_env("R3D_DLSS_JY")) < 0 { jy = f_neg(jy) }
Vk.put_i32(k, 352, jx)
Vk.put_i32(k, 356, jy)
Vk.put_i32(k, 360, F_ONE) # mvecScale
Vk.put_i32(k, 364, F_ONE)
if r3d_env_has("R3D_DLSS_JX") and Text.to_int(r3d_env("R3D_DLSS_JX")) < 0 { jx = -jx }
if r3d_env_has("R3D_DLSS_JY") and Text.to_int(r3d_env("R3D_DLSS_JY")) < 0 { jy = -jy }
Vk.put_i32(k, 352, float_bits(jx))
Vk.put_i32(k, 356, float_bits(jy))
Vk.put_i32(k, 360, float_bits(1.0)) # mvecScale
Vk.put_i32(k, 364, float_bits(1.0))
gsl_put_v3(k, 376, cam_pos)
let up = words(3)
let up = floats(3)
v3_cross(up, cam_right, cam_fwd)
gsl_put_v3(k, 388, up)
gsl_put_v3(k, 400, cam_right)
gsl_put_v3(k, 412, cam_fwd)
Vk.put_i32(k, 424, cam_near)
Vk.put_i32(k, 428, cam_far)
Vk.put_i32(k, 432, cam_fov)
Vk.put_i32(k, 436, cam_aspect)
Vk.put_i32(k, 440, F_ZERO) # motionVectorsInvalidValue
Vk.put_i32(k, 424, float_bits(cam_near))
Vk.put_i32(k, 428, float_bits(cam_far))
Vk.put_i32(k, 432, float_bits(cam_fov))
Vk.put_i32(k, 436, float_bits(cam_aspect))
Vk.put_i32(k, 440, float_bits(0.0)) # motionVectorsInvalidValue
# depthInverted, cameraMotionIncluded, motionVectors3D false; reset on a cut; not orthographic,
# not dilated, not jittered
if gsl_reset { Vk.put_i32(k, 444, 256 * 256 * 256) }
Vk.put_i32(k, 452, fi(40)) # minRelativeLinearDepthObjectSeparation
Vk.put_i32(k, 452, float_bits(40.0)) # minRelativeLinearDepthObjectSeparation
free(fix); free(proj); free(v2c); free(c2v); free(cur); free(prev); free(inv_cur); free(c2p); free(p2c); free(ident); free(up)
}

View file

@ -12,9 +12,9 @@ const CD_G: int = 32 # cells per patch side
const CD_LEVELS: int = 9 # 32 m leaves .. 8192 m root
const CD_LEAVES: int = 256 # leaf patches per side (8192 / 32)
var cd_mesh: Mesh = null
var cd_range: words = null # float bits: how far each level is drawn
var cd_min: []words = null # per level: min height of each patch (float bits)
var cd_max: []words = null
var cd_range: floats = null # float bits: how far each level is drawn
var cd_min: []floats = null # per level: min height of each patch (float bits)
var cd_max: []floats = null
var cd_draws: int = 0
var cd_far_draws: int = 0
var cd_near_draws: int = 0
@ -24,7 +24,7 @@ var ter_force_near: bool = false
var ter_skip: bool = false
var ter_height_tex: int = 0
var ter_heights: words = null # CPU copy, float bits, TERRAIN_RES^2
var ter_heights: floats = null # CPU copy, float bits, TERRAIN_RES^2
var ter_reflect: bool = false # drawing the reflection: the mid mesh is plenty
var ter_prog: int = 0
# The far tier compiled on its own (FAR_ONLY). A patch that lies entirely beyond the
@ -42,33 +42,33 @@ var ter_sun_pass: bool = false # selection is drawing the visibility pass
var ter_sun_tex: int = 0
var ter_prog_cur: int = 0 # the program currently bound during selection
var ter_smooth: bool = false # generate the analytic test ground instead of a survey
var ter_far_split: int = 0 # metres: beyond this the terrain takes its cheap far path (R3D_TFAR)
var ter_far_band: int = 0 # half-width of the near/far blend (R3D_TBAND)
var ter_snow_line: int = 0
var ter_far_split: float = 0.0 # metres: beyond this the terrain takes its cheap far path (R3D_TFAR)
var ter_far_band: float = 0.0 # half-width of the near/far blend (R3D_TBAND)
var ter_snow_line: float = 0.0
# 0 dry .. 1 soaked. The game sets it from the weather and lets it dry out.
var ter_wet: int = 0
var ter_wet: float = 0.0
var ter_tex: words = null # 11 material textures (see terrain_bind)
var ter_ox: int = 0 # world x/z of the terrain centre (float bits)
var ter_oz: int = 0
var ter_ox: float = 0.0 # world x/z of the terrain centre (float bits)
var ter_oz: float = 0.0
var ter_dem_tex: int = 0 # a real height map (16-bit), or 0 for the procedural valley
var ter_dem_blur: int = 0 # gaussian texels applied to the survey (0 for lidar; ~3 for 30 m data)
var ter_dem_min: int = 0
var ter_dem_max: int = 0
var ter_dem_base: int = 0
var ter_dem_blur: float = 0.0 # gaussian texels applied to the survey (0 for lidar; ~3 for 30 m data)
var ter_dem_min: float = 0.0
var ter_dem_max: float = 0.0
var ter_dem_base: float = 0.0
var ter_ortho_tex: int = 0 # a photograph of the same window, draped with distance
var ter_carpet: int = 0 # the distant-grass carpet (carpet_bake), 0 = none
var ter_shadow_tex: int = 0 # height-field sun shadow: RG32F (lowest lit height, occluder distance)
var ter_shadow_yaw: int = 0x7fffffff # the sky yaw it was baked for
var ter_shadow_yaw: float = 1000000000.0 # the sky yaw it was baked for
var ter_shadow_gen: int = -1 # the daylight generation it was baked for
var ter_shadow_prog: int = 0
function terrain_set_carpet(tex: int) -> void { ter_carpet = tex }
var ter_lake_level: int = 0 # a lake carved into the height map (float bits; ex = 0 → none)
var ter_lake_cx: int = 0
var ter_lake_cz: int = 0
var ter_lake_ex: int = 0
var ter_lake_ez: int = 0
var ter_lake_level: float = 0.0 # a lake carved into the height map (float bits; ex = 0 → none)
var ter_lake_cx: float = 0.0
var ter_lake_cz: float = 0.0
var ter_lake_ex: float = 0.0
var ter_lake_ez: float = 0.0
# Carve a lake bed below `level` inside the ellipse (cx, cz) ± (ex, ez); call before r3d_init.
function terrain_lake(level: int, cx: int, cz: int, ex: int, ez: int) -> void {
function terrain_lake(level: float, cx: float, cz: float, ex: float, ez: float) -> void {
ter_lake_level = level; ter_lake_cx = cx; ter_lake_cz = cz; ter_lake_ex = ex; ter_lake_ez = ez
}
# Whether the generator carves that bed. A height map that already carries a shaped bed
@ -81,11 +81,11 @@ function terrain_lake_carve(on: bool) -> void { ter_lake_carve = on }
# carved lake's. They were one number, which holds only while the lake is at sea level: a
# lake eighty metres up the valley drowned the whole coast toward its own surface. Unset,
# the sea is the lake exactly as before, so a map with one water line needs no call.
var ter_sea_level: int = 0
var ter_sea_level: float = 0.0
var ter_sea_set: bool = false
function terrain_sea(level: int) -> void { ter_sea_level = level; ter_sea_set = true }
function terrain_sea(level: float) -> void { ter_sea_level = level; ter_sea_set = true }
# the level the generator scales the coast toward (it always read the lake's, set or not)
function ter_sea_gen() -> int { if ter_sea_set { return ter_sea_level }; return ter_lake_level }
function ter_sea_gen() -> float { if ter_sea_set { return ter_sea_level }; return ter_lake_level }
# An island: land out to `r` from (cx, cz), then the terrain scaled down into the water
# over `fall` metres and on down to a shelf. Scaling rather than blending to a fixed bed is
# what makes the coastline come out of the terrain that is already there — low ground turns
@ -93,15 +93,15 @@ function ter_sea_gen() -> int { if ter_sea_set { return ter_sea_level }; return
const TER_ISLE_NONE: int = 0
const TER_ISLE_RADIAL: int = 1
const TER_ISLE_COAST: int = 2
var ter_isle_cx: int = 0
var ter_isle_cz: int = 0
var ter_isle_r: int = 0
var ter_isle_fall: int = 0
var ter_isle_cx: float = 0.0
var ter_isle_cz: float = 0.0
var ter_isle_r: float = 0.0
var ter_isle_fall: float = 0.0
var ter_isle_mode: int = 0
function terrain_island(cx: int, cz: int, r: int, fall: int) -> void {
function terrain_island(cx: float, cz: float, r: float, fall: float) -> void {
ter_isle_cx = cx; ter_isle_cz = cz; ter_isle_r = r; ter_isle_fall = fall
ter_isle_mode = TER_ISLE_RADIAL
if r == 0 { ter_isle_mode = TER_ISLE_NONE }
if r == 0.0 { ter_isle_mode = TER_ISLE_NONE }
}
# The other way to make an island, and the one a real survey usually wants: put the sea
# around the survey's OWN EDGE rather than cutting a circle out of the middle of it.
@ -116,10 +116,10 @@ function terrain_island(cx: int, cz: int, r: int, fall: int) -> void {
#
# The band is wobbled by low-frequency noise so the coastline is headlands and bays rather
# than the square the data arrived in. `margin = 0` leaves the survey alone.
function terrain_coast(cx: int, cz: int, margin: int, fall: int) -> void {
function terrain_coast(cx: float, cz: float, margin: float, fall: float) -> void {
ter_isle_cx = cx; ter_isle_cz = cz; ter_isle_r = margin; ter_isle_fall = fall
ter_isle_mode = TER_ISLE_COAST
if margin == 0 { ter_isle_mode = TER_ISLE_NONE }
if margin == 0.0 { ter_isle_mode = TER_ISLE_NONE }
}
# Use a real place: a 16-bit PNG height map plus its elevation range (metres). The
@ -129,12 +129,12 @@ var ter_ortho_w: int = 0
var ter_ortho_c: int = 3
# the photograph's colour at world (x, z): packed 0xRRGGBB (0 outside the map)
function terrain_ortho(x: int, z: int) -> int {
function terrain_ortho(x: float, z: float) -> int {
if ter_ortho_px == null { return 0 }
if ter_o_scale == 0 { ter_o_scale = fr(ter_ortho_w, TERRAIN_HALF * 2) }
if ter_o_scale == 0.0 { ter_o_scale = float(ter_ortho_w) / float(TERRAIN_HALF * 2) }
let scale = ter_o_scale
var ix = f_to_int(f_floor(f_mul(f_add(f_sub(x, ter_ox), fi(TERRAIN_HALF)), scale)))
var iz = f_to_int(f_floor(f_mul(f_add(f_sub(z, ter_oz), fi(TERRAIN_HALF)), scale)))
var ix = int(Math.floor((x - ter_ox + float(TERRAIN_HALF)) * scale))
var iz = int(Math.floor((z - ter_oz + float(TERRAIN_HALF)) * scale))
if ix < 0 { ix = 0 }; if iz < 0 { iz = 0 }
if ix > ter_ortho_w - 1 { ix = ter_ortho_w - 1 }; if iz > ter_ortho_w - 1 { iz = ter_ortho_w - 1 }
let o = (iz * ter_ortho_w + ix) * ter_ortho_c
@ -144,65 +144,65 @@ function terrain_ortho(x: int, z: int) -> int {
# one should fetch the colour once with terrain_ortho() and use the *_of forms — the
# cover generator tests all three per candidate, so this is three fetches saved out of
# every four in the hottest loop in the program.
function ortho_green_of(c: int) -> int {
function ortho_green_of(c: int) -> float {
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
var v = g - max(r, b)
if v < 0 { v = 0 }
return f_min(fr(v, 22), F_ONE)
return Math.min(float(v) / 22.0, 1.0)
}
function ortho_scree_of(c: int) -> int {
if c == 0 { return F_ZERO }
function ortho_scree_of(c: int) -> float {
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
if g - max(r, b) > 2 or mx < 60 { return F_ZERO }
return F_ONE
if g - max(r, b) > 2 or mx < 60 { return 0.0 }
return 1.0
}
function ortho_forest_of(c: int) -> int {
if c == 0 { return F_ZERO }
function ortho_forest_of(c: int) -> float {
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
if g - max(r, b) < 3 { return F_ZERO }
if mx <= 80 { return F_ONE }
if mx <= 105 { return F_HALF }
return F_ZERO
if g - max(r, b) < 3 { return 0.0 }
if mx <= 80 { return 1.0 }
if mx <= 105 { return 0.5 }
return 0.0
}
# how green the ground is in the photograph (0..1 float bits): meadow / forest vs rock, scree, water
function terrain_ortho_green(x: int, z: int) -> int {
function terrain_ortho_green(x: float, z: float) -> float {
let c = terrain_ortho(x, z)
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
var v = g - max(r, b) # green excess
if v < 0 { v = 0 }
return f_min(fr(v, 22), F_ONE)
return Math.min(float(v) / 22.0, 1.0)
}
# how grey and mid-bright (scree / pebbles / bare rock) the photograph is there (0..1)
# Bare ground only where most of a 50 m neighbourhood is bare: a single 10 m trail pixel
# must not place a boulder or bar a tree.
function terrain_ortho_scree(x: int, z: int) -> int {
function terrain_ortho_scree(x: float, z: float) -> float {
var votes = 0
for j in 0 .. 5 { for i in 0 .. 5 { if ortho_scree_of(terrain_ortho(f_add(x, fi((i - 2) * 10)), f_add(z, fi((j - 2) * 10)))) != F_ZERO { votes += 1 } } }
if votes >= 15 { return F_ONE }
return F_ZERO
for j in 0 .. 5 { for i in 0 .. 5 { if ortho_scree_of(terrain_ortho(x + float((i - 2) * 10), z + float((j - 2) * 10))) != 0.0 { votes += 1 } } }
if votes >= 15 { return 1.0 }
return 0.0
}
function terrain_ortho_scree_pixel(x: int, z: int) -> int {
function terrain_ortho_scree_pixel(x: float, z: float) -> float {
let c = terrain_ortho(x, z)
if c == 0 { return F_ZERO }
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
# bare ground: not green-dominant (grey scree, the maroon rock, the moraine's tan gravel), lit enough not to be water
if g - max(r, b) > 2 or mx < 60 { return F_ZERO }
return F_ONE
if g - max(r, b) > 2 or mx < 60 { return 0.0 }
return 1.0
}
# dense conifer forest in the photograph: green-dominant and dark (the meadows are brighter)
function terrain_ortho_forest(x: int, z: int) -> int {
function terrain_ortho_forest(x: float, z: float) -> float {
let c = terrain_ortho(x, z)
if c == 0 { return F_ZERO }
if c == 0 { return 0.0 }
let r = (c >> 16) & 255; let g = (c >> 8) & 255; let b = c & 255
let mx = max(r, max(g, b))
if g - max(r, b) < 3 { return F_ZERO }
if mx <= 80 { return F_ONE }
if mx <= 105 { return F_HALF }
return F_ZERO
if g - max(r, b) < 3 { return 0.0 }
if mx <= 80 { return 1.0 }
if mx <= 105 { return 0.5 }
return 0.0
}
function terrain_use_ortho(path: string) -> void {
@ -216,7 +216,7 @@ function terrain_use_ortho(path: string) -> void {
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
}
function terrain_use_dem(path: string, emin: int, emax: int, base: int, ox: int, oz: int) -> void {
function terrain_use_dem(path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void {
ter_dem_tex = tex_load(path, false)
ter_dem_min = emin; ter_dem_max = emax; ter_dem_base = base
ter_ox = ox; ter_oz = oz
@ -237,7 +237,7 @@ function terrain_generate() -> void {
gpu_viewport(0, 0, TERRAIN_RES, TERRAIN_RES)
gpu_depth_test(false)
gpu_use_program(p)
u_f(gpu_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(p, "u_half"), float(TERRAIN_HALF))
if ter_dem_tex != 0 {
r3d_bind_2d(p, "u_dem", 0, ter_dem_tex)
u_f(gpu_uniform(p, "u_dem_min"), ter_dem_min)
@ -246,13 +246,13 @@ function terrain_generate() -> void {
u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)
u_f4(gpu_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_f(gpu_uniform(p, "u_lake_level"), ter_lake_level)
var carve = F_ONE
if not ter_lake_carve { carve = F_ZERO }
var carve = 1.0
if not ter_lake_carve { carve = 0.0 }
u_f(gpu_uniform(p, "u_lake_carve"), carve)
u_f(gpu_uniform(p, "u_sea_level"), ter_sea_gen())
u_f4(gpu_uniform(p, "u_isle"), ter_isle_cx, ter_isle_cz, ter_isle_r, ter_isle_fall)
u_f(gpu_uniform(p, "u_isle_mode"), fi(ter_isle_mode))
u_f(gpu_uniform(p, "u_dem_blur"), ter_dem_blur)
u_f(gpu_uniform(p, "u_isle_mode"), float(ter_isle_mode))
u_f(gpu_uniform(p, "u_dem_blur"), float(ter_dem_blur))
}
mesh_draw(sky_fullscreen)
# second pass: R = height, GBA = the smooth surface normal, baked once (ternormal.frag)
@ -262,12 +262,12 @@ function terrain_generate() -> void {
let pn = r3d_program("fullscreen.vert", "ternormal.frag", "")
gpu_use_program(pn)
r3d_bind_2d(pn, "u_src", 0, raw)
u_f(gpu_uniform(pn, "u_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(pn, "u_half"), float(TERRAIN_HALF))
mesh_draw(sky_fullscreen)
gpu_program_free(pn)
gpu_tex_free(raw)
# read the heights back for placement
ter_heights = words(TERRAIN_RES * TERRAIN_RES)
ter_heights = floats(TERRAIN_RES * TERRAIN_RES)
gpu_tex_bind(GPU_TEX2D, ter_height_tex)
gpu_pixel_store(GL_PACK_ALIGNMENT, 4)
gpu_tex_read(GPU_TEX2D, GL_RED, GL_FLOAT, ter_heights)
@ -280,7 +280,7 @@ function terrain_generate() -> void {
# The height field for shaders that place things on the ground (model.vert's u_ground)
function terrain_bind_height(p: int) -> void {
r3d_bind_2d(p, "u_ts_height", 5, ter_height_tex)
u_f(gpu_uniform(p, "u_ts_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(p, "u_ts_half"), float(TERRAIN_HALF))
u_f2(gpu_uniform(p, "u_ts_origin"), ter_ox, ter_oz)
}
@ -299,7 +299,7 @@ function terrain_bake_shadow() -> void {
gpu_blend(false)
gpu_use_program(p)
r3d_bind_2d(p, "u_height", 0, ter_height_tex)
u_f(gpu_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(p, "u_half"), float(TERRAIN_HALF))
u_v3(gpu_uniform(p, "u_sun"), sun_dir)
mesh_draw(sky_fullscreen)
gpu_fb_bind(0)
@ -313,66 +313,66 @@ function terrain_bake_shadow() -> void {
# a fixed-point divide — on every call, and the cover generator calls terrain_height
# five times per candidate (once directly, four more inside slope_at) across hundreds
# of thousands of candidates per chunk. Hoisted, they cost nothing.
var ter_h_scale: int = 0
var ter_o_scale: int = 0
var ter_h_scale: float = 0.0
var ter_o_scale: float = 0.0
function terrain_height(x: int, z: int) -> int {
if ter_h_scale == 0 { ter_h_scale = fr(TERRAIN_RES, TERRAIN_HALF * 2) }
function terrain_height(x: float, z: float) -> float {
if ter_h_scale == 0.0 { ter_h_scale = float(TERRAIN_RES) / float(TERRAIN_HALF * 2) }
let scale = ter_h_scale
let fx = f_mul(f_add(f_sub(x, ter_ox), fi(TERRAIN_HALF)), scale)
let fz = f_mul(f_add(f_sub(z, ter_oz), fi(TERRAIN_HALF)), scale)
var ix = f_to_int(f_floor(fx)); var iz = f_to_int(f_floor(fz))
let fx = (x - ter_ox + float(TERRAIN_HALF)) * scale
let fz = (z - ter_oz + float(TERRAIN_HALF)) * scale
var ix = int(Math.floor(fx)); var iz = int(Math.floor(fz))
if ix < 0 { ix = 0 }; if iz < 0 { iz = 0 }
if ix > TERRAIN_RES - 2 { ix = TERRAIN_RES - 2 }; if iz > TERRAIN_RES - 2 { iz = TERRAIN_RES - 2 }
let tx = f_clamp(f_sub(fx, fi(ix)), F_ZERO, F_ONE)
let tz = f_clamp(f_sub(fz, fi(iz)), F_ZERO, F_ONE)
let tx = Math.clamp(fx - float(ix), 0.0, 1.0)
let tz = Math.clamp(fz - float(iz), 0.0, 1.0)
let h00 = ter_heights[iz * TERRAIN_RES + ix]
let h10 = ter_heights[iz * TERRAIN_RES + ix + 1]
let h01 = ter_heights[(iz + 1) * TERRAIN_RES + ix]
let h11 = ter_heights[(iz + 1) * TERRAIN_RES + ix + 1]
return f_lerp(f_lerp(h00, h10, tx), f_lerp(h01, h11, tx), tz)
return Math.lerp(Math.lerp(h00, h10, tx), Math.lerp(h01, h11, tx), tz)
}
# the same for Q16.16 callers
function terrain_height_fx(x: fixed, z: fixed) -> fixed { return f_fx(terrain_height(fl(x), fl(z))) }
function terrain_height_fx(x: fixed, z: fixed) -> fixed { return fixed(terrain_height(float(x), float(z))) }
# The height the terrain is DRAWN at: the cubic B-spline of the texels (heightSmooth in
# terrain.vert), not the bilinear read above. The two differ by up to half a metre on
# rough ground, which is the difference between a character standing on the meadow
# and one buried to the knee in it. Sixteen taps; for things that move, not for the
# thousands of placement queries a chunk makes.
var ter_bw: words = null
function terrain_height_smooth(x: int, z: int) -> int {
if ter_h_scale == 0 { ter_h_scale = fr(TERRAIN_RES, TERRAIN_HALF * 2) }
if ter_bw == null { ter_bw = words(8) }
var ter_bw: floats = null
function terrain_height_smooth(x: float, z: float) -> float {
if ter_h_scale == 0.0 { ter_h_scale = float(TERRAIN_RES) / float(TERRAIN_HALF * 2) }
if ter_bw == null { ter_bw = floats(8) }
let scale = ter_h_scale
let fx = f_sub(f_mul(f_add(f_sub(x, ter_ox), fi(TERRAIN_HALF)), scale), F_HALF)
let fz = f_sub(f_mul(f_add(f_sub(z, ter_oz), fi(TERRAIN_HALF)), scale), F_HALF)
let ix = f_to_int(f_floor(fx)); let iz = f_to_int(f_floor(fz))
let tx = f_clamp(f_sub(fx, fi(ix)), F_ZERO, F_ONE)
let tz = f_clamp(f_sub(fz, fi(iz)), F_ZERO, F_ONE)
let fx = (x - ter_ox + float(TERRAIN_HALF)) * scale - 0.5
let fz = (z - ter_oz + float(TERRAIN_HALF)) * scale - 0.5
let ix = int(Math.floor(fx)); let iz = int(Math.floor(fz))
let tx = Math.clamp(fx - float(ix), 0.0, 1.0)
let tz = Math.clamp(fz - float(iz), 0.0, 1.0)
# the four cubic B-spline weights of a fraction, over texels i-1 .. i+2
for a in 0 .. 2 {
var t = tx
if a == 1 { t = tz }
let t2 = f_mul(t, t); let t3 = f_mul(t2, t)
let one_t = f_sub(F_ONE, t)
let w0 = f_div(f_mul(f_mul(one_t, one_t), one_t), fi(6))
let w1 = f_div(f_add(f_sub(fi(4), f_mul(fi(6), t2)), f_mul(fi(3), t3)), fi(6))
let w3 = f_div(t3, fi(6))
let w2 = f_sub(f_sub(f_sub(F_ONE, w0), w1), w3)
let t2 = t * t; let t3 = t2 * t
let one_t = 1.0 - t
let w0 = one_t * one_t * one_t / 6.0
let w1 = (4.0 - 6.0 * t2 + 3.0 * t3) / 6.0
let w3 = t3 / 6.0
let w2 = 1.0 - w0 - w1 - w3
ter_bw[a * 4] = w0; ter_bw[a * 4 + 1] = w1; ter_bw[a * 4 + 2] = w2; ter_bw[a * 4 + 3] = w3
}
var h = F_ZERO
var h = 0.0
for j in 0 .. 4 {
var rz = iz - 1 + j
if rz < 0 { rz = 0 }; if rz > TERRAIN_RES - 1 { rz = TERRAIN_RES - 1 }
var row = F_ZERO
var row = 0.0
for i in 0 .. 4 {
var rx = ix - 1 + i
if rx < 0 { rx = 0 }; if rx > TERRAIN_RES - 1 { rx = TERRAIN_RES - 1 }
row = f_add(row, f_mul(ter_bw[i], ter_heights[rz * TERRAIN_RES + rx]))
row = row + ter_bw[i] * ter_heights[rz * TERRAIN_RES + rx]
}
h = f_add(h, f_mul(ter_bw[4 + j], row))
h = h + ter_bw[4 + j] * row
}
return h
}
@ -445,11 +445,11 @@ function terrain_init_finish() -> void {
ter_prog_far = r3d_program("terrain.vert", "terrain.frag", defs + "#define FAR_ONLY\n")
ter_prog_near = r3d_program("terrain.vert", "terrain.frag", defs + "#define NEAR_ONLY\n")
ter_sun_prog = r3d_program("terrain.vert", "tersun.frag", "")
ter_far_split = fi(200)
if r3d_env_has("R3D_TFAR") { ter_far_split = fi(Text.to_int(r3d_env("R3D_TFAR"))) }
ter_far_band = fi(60)
if r3d_env_has("R3D_TBAND") { ter_far_band = fi(Text.to_int(r3d_env("R3D_TBAND"))) }
ter_snow_line = fi(880)
ter_far_split = 200.0
if r3d_env_has("R3D_TFAR") { ter_far_split = float(Text.to_int(r3d_env("R3D_TFAR"))) }
ter_far_band = 60.0
if r3d_env_has("R3D_TBAND") { ter_far_band = float(Text.to_int(r3d_env("R3D_TBAND"))) }
ter_snow_line = 880.0
gpu_check("terrain init")
}
@ -457,7 +457,7 @@ function terrain_init_finish() -> void {
# The terrain no longer casts into the shadow map: it shadows itself by marching its
# own height field in terrain.frag, which cannot produce the self-shadow grid a depth
# map does, and it saves drawing the whole grid five times a frame.
function terrain_draw_shadow(light_vp: words) -> void {
function terrain_draw_shadow(light_vp: floats) -> void {
}
# Every per-frame uniform of one terrain program. Both tiers are bound up front so
@ -475,19 +475,19 @@ function terrain_bind_prog(p: int) -> void {
var orthotex = ter_ortho_tex
if orthotex == 0 { orthotex = ter_tex[0] }
r3d_bind_2d(p, "u_ortho", 4, orthotex)
var oon = F_ZERO
if ter_ortho_tex != 0 { oon = F_ONE }
var oon = 0.0
if ter_ortho_tex != 0 { oon = 1.0 }
u_f(gpu_uniform(p, "u_ortho_on"), oon)
r3d_bind_2d(p, "u_rock_d", 7, ter_tex[6]); r3d_bind_2d(p, "u_rock_n", 8, ter_tex[7]); r3d_bind_2d(p, "u_rock_a", 9, ter_tex[8])
r3d_bind_2d(p, "u_snow_d", 10, ter_tex[9])
if ter_carpet != 0 { r3d_bind_2d(p, "u_carpet", 11, ter_carpet); u_f(gpu_uniform(p, "u_carpet_on"), F_ONE) }
else { r3d_bind_2d(p, "u_carpet", 11, ter_tex[0]); u_f(gpu_uniform(p, "u_carpet_on"), F_ZERO) }
u_f(gpu_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(p, "u_texel"), fr(1, TERRAIN_RES))
if ter_carpet != 0 { r3d_bind_2d(p, "u_carpet", 11, ter_carpet); u_f(gpu_uniform(p, "u_carpet_on"), 1.0) }
else { r3d_bind_2d(p, "u_carpet", 11, ter_tex[0]); u_f(gpu_uniform(p, "u_carpet_on"), 0.0) }
u_f(gpu_uniform(p, "u_half"), float(TERRAIN_HALF))
u_f(gpu_uniform(p, "u_texel"), 1.0 / float(TERRAIN_RES))
u_f(gpu_uniform(p, "u_snow_line"), ter_snow_line)
u_f(gpu_uniform(p, "u_wet"), ter_wet)
var lake = fl(-100000.0)
if ter_lake_ex != 0 { lake = ter_lake_level }
var lake = -100000.0
if ter_lake_ex != 0.0 { lake = ter_lake_level }
u_f(gpu_uniform(p, "u_lake_level"), lake)
# the shoreline, forest and scree gates read the sea; unset it is what they always read
var sea = lake
@ -512,9 +512,9 @@ function terrain_bind_prog(p: int) -> void {
# Set before fog_bind it measured as EXACTLY zero pixels changed, which is the same
# shape of mistake as setting r3d_fog_scale before gfx_apply: the value was right and
# something downstream put it back.
u_f(gpu_uniform(p, "u_spec_scale"), fl(0.22))
u_f(gpu_uniform(p, "u_spec_scale"), 0.22)
u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
u_f(gpu_uniform(p, "u_grid"), fi(CD_G))
u_f(gpu_uniform(p, "u_grid"), float(CD_G))
# The ground reads its sun visibility out of the buffer tersun.frag filled, and has no
# use for the cascade array shadow_bind just put on this unit; leaving both bound under
# one unit is undefined ground, so the array comes off first.
@ -573,12 +573,12 @@ function terrain_sun_pass(w: int, h: int, depth: int) -> Target {
let p = ter_sun_prog
gpu_use_program(p)
r3d_bind_2d(p, "u_height", 0, ter_height_tex)
u_f(gpu_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(p, "u_half"), float(TERRAIN_HALF))
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)
u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
u_f(gpu_uniform(p, "u_grid"), fi(CD_G))
u_f(gpu_uniform(p, "u_grid"), float(CD_G))
u_f(gpu_uniform(p, "u_far_split"), ter_far_split)
u_f(gpu_uniform(p, "u_far_band"), ter_far_band)
u_f(gpu_uniform(p, "u_clip_y"), r3d_clip_y)
@ -648,7 +648,7 @@ function cdlod_init() -> void {
let n = CD_G + 1
let v = gl_floats(n * n * 2)
var k = 0
for j in 0 .. n { for i in 0 .. n { gl_put_bits(v, k, fr(i, CD_G)); gl_put_bits(v, k + 1, fr(j, CD_G)); k += 2 } }
for j in 0 .. n { for i in 0 .. n { gl_put_bits(v, k, float_bits(float(i) / float(CD_G))); gl_put_bits(v, k + 1, float_bits(float(j) / float(CD_G))); k += 2 } }
gpu_mesh_vertices(m, v, gl_bytes_of(n * n * 2), GPU_STATIC)
gpu_mesh_attr(m, 0, 2, GPU_F32, 8, 0, false)
free(v)
@ -668,28 +668,28 @@ function cdlod_init() -> void {
m.count = ni
gpu_mesh_done(m)
cd_mesh = m
cd_range = words(CD_LEVELS)
var r = fi(48)
if r3d_env_has("R3D_CD_R0") { r = fi(Text.to_int(r3d_env("R3D_CD_R0"))) }
for l in 0 .. CD_LEVELS { cd_range[l] = r; r = f_mul(r, F_TWO) }
cd_range = floats(CD_LEVELS)
var r = 48.0
if r3d_env_has("R3D_CD_R0") { r = float(Text.to_int(r3d_env("R3D_CD_R0"))) }
for l in 0 .. CD_LEVELS { cd_range[l] = r; r = r * 2.0 }
cdlod_bounds()
}
# min/max height per patch at every level, from the CPU copy of the height field
function cdlod_bounds() -> void {
cd_min = new []words; cd_max = new []words
cd_min = new []floats; cd_max = new []floats
let t = TERRAIN_RES / CD_LEAVES # texels per leaf patch side
var n = CD_LEAVES
var lo = words(n * n); var hi = words(n * n)
var lo = floats(n * n); var hi = floats(n * n)
for j in 0 .. n {
for i in 0 .. n {
var mn = fi(100000); var mx = fi(-100000)
var mn = 100000.0; var mx = -100000.0
for y in 0 .. t + 1 {
let ty = min(j * t + y, TERRAIN_RES - 1)
for x in 0 .. t + 1 {
let tx = min(i * t + x, TERRAIN_RES - 1)
let h = ter_heights[ty * TERRAIN_RES + tx]
mn = f_min(mn, h); mx = f_max(mx, h)
mn = Math.min(mn, h); mx = Math.max(mx, h)
}
}
lo[j * n + i] = mn; hi[j * n + i] = mx
@ -698,12 +698,12 @@ function cdlod_bounds() -> void {
push(cd_min, lo); push(cd_max, hi)
while n > 1 {
let m = n / 2
let plo = words(m * m); let phi = words(m * m)
let plo = floats(m * m); let phi = floats(m * m)
for j in 0 .. m {
for i in 0 .. m {
let a = (2 * j) * n + 2 * i
plo[j * m + i] = f_min(f_min(lo[a], lo[a + 1]), f_min(lo[a + n], lo[a + n + 1]))
phi[j * m + i] = f_max(f_max(hi[a], hi[a + 1]), f_max(hi[a + n], hi[a + n + 1]))
plo[j * m + i] = Math.min(Math.min(lo[a], lo[a + 1]), Math.min(lo[a + n], lo[a + n + 1]))
phi[j * m + i] = Math.max(Math.max(hi[a], hi[a + 1]), Math.max(hi[a + n], hi[a + n + 1]))
}
}
push(cd_min, plo); push(cd_max, phi)
@ -715,41 +715,41 @@ function cdlod_bounds() -> void {
# map, so a leaf is (2 * TERRAIN_HALF) / CD_LEAVES - 32 m only on the 8192 m map the numbers
# were chosen for. Placing patches at a fixed 32 m put every bound in the wrong place on any
# other TERRAIN_HALF.
function cd_size(level: int) -> int { return f_mul(fi(32 << level), fr(TERRAIN_HALF * 2, 8192)) }
function cd_size(level: int) -> float { return float(32 << level) * (float(TERRAIN_HALF * 2) / 8192.0) }
# does the patch's box come within r of the camera?
function cd_within(x0: int, z0: int, size: int, ymin: int, ymax: int, r: int) -> bool {
let dx = f_max(f_max(f_sub(x0, cam_pos[0]), f_sub(cam_pos[0], f_add(x0, size))), F_ZERO)
let dz = f_max(f_max(f_sub(z0, cam_pos[2]), f_sub(cam_pos[2], f_add(z0, size))), F_ZERO)
let dy = f_max(f_max(f_sub(ymin, cam_pos[1]), f_sub(cam_pos[1], ymax)), F_ZERO)
return f_ls(f_add(f_add(f_mul(dx, dx), f_mul(dz, dz)), f_mul(dy, dy)), f_mul(r, r))
function cd_within(x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool {
let dx = Math.max(Math.max(x0 - cam_pos[0], cam_pos[0] - (x0 + size)), 0.0)
let dz = Math.max(Math.max(z0 - cam_pos[2], cam_pos[2] - (z0 + size)), 0.0)
let dy = Math.max(Math.max(ymin - cam_pos[1], cam_pos[1] - ymax), 0.0)
return dx * dx + dz * dz + dy * dy < r * r
}
# is the patch's box entirely inside r of the camera? (its farthest corner is within r)
function cd_inside(x0: int, z0: int, size: int, ymin: int, ymax: int, r: int) -> bool {
let x1 = f_add(x0, size)
let z1 = f_add(z0, size)
let dx = f_max(f_abs(f_sub(cam_pos[0], x0)), f_abs(f_sub(cam_pos[0], x1)))
let dz = f_max(f_abs(f_sub(cam_pos[2], z0)), f_abs(f_sub(cam_pos[2], z1)))
let dy = f_max(f_abs(f_sub(cam_pos[1], ymin)), f_abs(f_sub(cam_pos[1], ymax)))
return f_ls(f_add(f_add(f_mul(dx, dx), f_mul(dz, dz)), f_mul(dy, dy)), f_mul(r, r))
function cd_inside(x0: float, z0: float, size: float, ymin: float, ymax: float, r: float) -> bool {
let x1 = x0 + size
let z1 = z0 + size
let dx = Math.max(Math.abs(cam_pos[0] - x0), Math.abs(cam_pos[0] - x1))
let dz = Math.max(Math.abs(cam_pos[2] - z0), Math.abs(cam_pos[2] - z1))
let dy = Math.max(Math.abs(cam_pos[1] - ymin), Math.abs(cam_pos[1] - ymax))
return dx * dx + dz * dz + dy * dy < r * r
}
function cdlod_draw(level: int, ix: int, iz: int) -> void {
let size = cd_size(level)
let x0 = f_add(f_sub(ter_ox, fi(TERRAIN_HALF)), f_mul(fi(ix), size))
let z0 = f_add(f_sub(ter_oz, fi(TERRAIN_HALF)), f_mul(fi(iz), size))
let x0 = ter_ox - float(TERRAIN_HALF) + float(ix) * size
let z0 = ter_oz - float(TERRAIN_HALF) + float(iz) * size
# Which tier can run inside this patch. A patch that never comes within the split takes
# the cheap tier at every pixel; one that lies wholly inside it takes the detailed tier
# at every pixel. Only a patch that straddles the band needs the program that holds both
# and cross-fades between them — and there are few of those, one ring of them.
if ter_sun_pass {
let t = gl_scratch()
let t = ter_scratch()
t[0] = x0; t[1] = z0; t[2] = size
u_v3(gpu_uniform(ter_sun_prog, "u_node"), t)
var st0 = F_ZERO
var st0 = 0.0
if level > 0 { st0 = cd_range[level - 1] }
u_f2(gpu_uniform(ter_sun_prog, "u_morph"), f_lerp(st0, cd_range[level], fl(0.7)), cd_range[level])
u_f2(gpu_uniform(ter_sun_prog, "u_morph"), Math.lerp(st0, cd_range[level], 0.7), cd_range[level])
gpu_draw_bound_elements(cd_mesh)
return
}
@ -757,20 +757,20 @@ function cdlod_draw(level: int, ix: int, iz: int) -> void {
let ymin = cd_min[level][iz * n + ix]
let ymax = cd_max[level][iz * n + ix]
var p = ter_prog
if not cd_within(x0, z0, size, ymin, ymax, f_add(ter_far_split, ter_far_band)) { p = ter_prog_far }
else if cd_inside(x0, z0, size, ymin, ymax, f_sub(ter_far_split, ter_far_band)) { p = ter_prog_near }
if not cd_within(x0, z0, size, ymin, ymax, ter_far_split + ter_far_band) { p = ter_prog_far }
else if cd_inside(x0, z0, size, ymin, ymax, ter_far_split - ter_far_band) { p = ter_prog_near }
if ter_force_far { p = ter_prog_far }
if ter_no_split { p = ter_prog }
if ter_force_near { p = ter_prog_near }
if p == ter_prog_far { cd_far_draws += 1 }
if p == ter_prog_near { cd_near_draws += 1 }
if p != ter_prog_cur { gpu_use_program(p); ter_prog_cur = p }
let t = gl_scratch()
let t = ter_scratch()
t[0] = x0; t[1] = z0; t[2] = size
u_v3(gpu_uniform(p, "u_node"), t)
var start = F_ZERO
var start = 0.0
if level > 0 { start = cd_range[level - 1] }
start = f_lerp(start, cd_range[level], fl(0.7))
start = Math.lerp(start, cd_range[level], 0.7)
u_f2(gpu_uniform(p, "u_morph"), start, cd_range[level])
gpu_draw_bound_elements(cd_mesh)
cd_draws += 1
@ -781,15 +781,15 @@ function cdlod_draw(level: int, ix: int, iz: int) -> void {
function cdlod_select(level: int, ix: int, iz: int) -> bool {
let n = CD_LEAVES >> level
let size = cd_size(level)
let x0 = f_add(f_sub(ter_ox, fi(TERRAIN_HALF)), f_mul(fi(ix), size))
let z0 = f_add(f_sub(ter_oz, fi(TERRAIN_HALF)), f_mul(fi(iz), size))
let x0 = ter_ox - float(TERRAIN_HALF) + float(ix) * size
let z0 = ter_oz - float(TERRAIN_HALF) + float(iz) * size
let ymin = cd_min[level][iz * n + ix]
let ymax = cd_max[level][iz * n + ix]
if not cd_within(x0, z0, size, ymin, ymax, cd_range[level]) { return false }
let half = f_mul(size, F_HALF)
let cy = f_mul(f_add(ymin, ymax), F_HALF)
let rad = f_sqrt(f_add(f_mul(f_mul(half, half), F_TWO), f_mul(f_mul(f_sub(ymax, cy), f_sub(ymax, cy)), F_ONE)))
if not cam_sphere_visible(f_add(x0, half), cy, f_add(z0, half), f_add(rad, fi(2))) { return true }
let half = size * 0.5
let cy = (ymin + ymax) * 0.5
let rad = Math.sqrt(half * half * 2.0 + (ymax - cy) * (ymax - cy) * 1.0)
if not cam_sphere_visible(x0 + half, cy, z0 + half, rad + 2.0) { return true }
if level == 0 { cdlod_draw(0, ix, iz); return true }
if not cd_within(x0, z0, size, ymin, ymax, cd_range[level - 1]) { cdlod_draw(level, ix, iz); return true }
for c in 0 .. 4 {
@ -817,15 +817,15 @@ function terrain_unload() -> void {
cd_min = null; cd_max = null
}
# derived from TERRAIN_HALF and cached on first use: stale ones would keep the old size
ter_h_scale = 0; ter_o_scale = 0
ter_h_scale = 0.0; ter_o_scale = 0.0
ter_carpet = 0
ter_shadow_gen = -1; ter_shadow_yaw = 0x7fffffff
ter_shadow_gen = -1; ter_shadow_yaw = 1000000000.0
}
# Generate another map in place. Call terrain_lake / terrain_coast / terrain_island for it
# first - the generator reads them - then this. `half` is the new TERRAIN_HALF in metres.
# An empty `dem` generates the analytic ground (with ter_smooth) and an empty `ortho` drapes
# no photograph. The shadow is rebaked and the patch bounds rebuilt before it returns.
function terrain_reload(dem: string, emin: int, emax: int, base: int, ox: int, oz: int, ortho: string, half: int) -> void {
function terrain_reload(dem: string, emin: float, emax: float, base: float, ox: float, oz: float, ortho: string, half: int) -> void {
terrain_unload()
TERRAIN_HALF = half
ter_ox = ox; ter_oz = oz
@ -836,3 +836,9 @@ function terrain_reload(dem: string, emin: int, emax: int, base: int, ox: int, o
cdlod_bounds()
gpu_check("terrain reload")
}
var ter_scr: floats = null
function ter_scratch() -> floats {
if ter_scr == null { ter_scr = floats(16) }
return ter_scr
}

View file

@ -288,15 +288,15 @@ function tex_solid(r: int, g: int, b: int, a: int) -> int {
}
# ---- Radiance .hdr (RGBE, new-style RLE) -> RGB float bits -------------------------
var hdr_max_lum: int = 0 # float bits of the brightest texel (sun finding)
var hdr_max_lum: float = 0.0 # float bits of the brightest texel (sun finding)
var hdr_max_x: int = 0
var hdr_max_y: int = 0
var hdr_sun_r: int = 0 # irradiance (float bits) of everything above the IBL clip: the sun
var hdr_sun_g: int = 0
var hdr_sun_b: int = 0
var hdr_clip: int = 0 # float bits; texels above this (per channel) feed the sun, not the IBL
var hdr_sun_r: float = 0.0 # irradiance (float bits) of everything above the IBL clip: the sun
var hdr_sun_g: float = 0.0
var hdr_sun_b: float = 0.0
var hdr_clip: float = 0.0 # float bits; texels above this (per channel) feed the sun, not the IBL
function hdr_decode(path: pointer) -> words {
function hdr_decode(path: pointer) -> floats {
let d = r3d_read_file(path)
if d == null { print(`hdr: cannot read {path}`); return null }
let size = tex_file_len
@ -315,14 +315,14 @@ function hdr_decode(path: pointer) -> words {
while d[i] >= '0' and d[i] <= '9' { w = w * 10 + (d[i] - 48); i += 1 }
i += 1
if w <= 0 or h <= 0 { free(d); print(`hdr: bad header {path}`); return null }
let out = words(w * h * 3)
let out = floats(w * h * 3)
let line = bytes(w * 4)
var maxl = 0
if hdr_clip == 0 { hdr_clip = fi(20) }
var sr = F_ZERO; var sg = F_ZERO; var sb = F_ZERO
var skye = F_ZERO # sky irradiance on an upward face (clipped part only)
let dphi = f_div(f_mul(F_TWO, F_PI), fi(w))
let dth = f_div(F_PI, fi(h))
var maxl = 0.0
if hdr_clip == 0.0 { hdr_clip = 20.0 }
var sr = 0.0; var sg = 0.0; var sb = 0.0
var skye = 0.0 # sky irradiance on an upward face (clipped part only)
let dphi = 2.0 * PI / float(w)
let dth = PI / float(h)
var y = 0
while y < h {
if d[i] == 2 and d[i + 1] == 2 and (d[i + 2] & 128) == 0 {
@ -347,28 +347,28 @@ function hdr_decode(path: pointer) -> words {
for x in 0 .. w { for c in 0 .. 4 { line[x * 4 + c] = d[i + x * 4 + c] } }
i += w * 4
}
let sinth = f_sin(f_mul(f_add(fi(y), F_HALF), dth))
let domega = f_mul(f_mul(dphi, dth), sinth)
let sinth = Math.sin((float(y) + 0.5) * dth)
let domega = dphi * dth * sinth
for x in 0 .. w {
let e = line[x * 4 + 3]
let o = (y * w + x) * 3
if e == 0 { out[o] = 0; out[o + 1] = 0; out[o + 2] = 0 }
if e == 0 { out[o] = 0.0; out[o + 1] = 0.0; out[o + 2] = 0.0 }
else {
let sh = e - 136
let vr = f_ldexp(f_from_int(line[x * 4]), sh)
let vg = f_ldexp(f_from_int(line[x * 4 + 1]), sh)
let vb = f_ldexp(f_from_int(line[x * 4 + 2]), sh)
let vr = float_from_bits(f_ldexp(float_bits(float(line[x * 4])), sh))
let vg = float_from_bits(f_ldexp(float_bits(float(line[x * 4 + 1])), sh))
let vb = float_from_bits(f_ldexp(float_bits(float(line[x * 4 + 2])), sh))
# the texture is capped at what a half-float holds; the sun is integrated uncapped
out[o] = f_min(vr, fi(60000))
out[o + 1] = f_min(vg, fi(60000))
out[o + 2] = f_min(vb, fi(60000))
let lum = f_add(f_add(vr, vg), vb)
out[o] = Math.min(vr, 60000.0)
out[o + 1] = Math.min(vg, 60000.0)
out[o + 2] = Math.min(vb, 60000.0)
let lum = vr + vg + vb
if f_lt(maxl, lum) != 0 { maxl = lum; hdr_max_x = x; hdr_max_y = y }
if y < h / 2 { skye = f_add(skye, f_mul(f_mul(f_min(vg, hdr_clip), f_cos(f_mul(f_add(fi(y), F_HALF), dth))), domega)) }
if y < h / 2 { skye = skye + Math.min(vg, hdr_clip) * Math.cos((float(y) + 0.5) * dth) * domega }
if f_lt(hdr_clip, vg) != 0 or f_lt(hdr_clip, vr) != 0 {
sr = f_add(sr, f_mul(f_max(f_sub(vr, hdr_clip), F_ZERO), domega))
sg = f_add(sg, f_mul(f_max(f_sub(vg, hdr_clip), F_ZERO), domega))
sb = f_add(sb, f_mul(f_max(f_sub(vb, hdr_clip), F_ZERO), domega))
sr = sr + Math.max(vr - hdr_clip, 0.0) * domega
sg = sg + Math.max(vg - hdr_clip, 0.0) * domega
sb = sb + Math.max(vb - hdr_clip, 0.0) * domega
}
}
}
@ -378,7 +378,7 @@ function hdr_decode(path: pointer) -> words {
tex_w = w; tex_h = h; tex_channels = 3; tex_depth = 32
hdr_max_lum = maxl
hdr_sun_r = sr; hdr_sun_g = sg; hdr_sun_b = sb
print(`hdr: peak/1000 {f_fx(f_div(maxl, fi(1000)))} sky irradiance(up) {f_fx(skye)} sun irradiance {f_fx(sg)} (Q16.16 = /65536)`)
print(`hdr: peak/1000 {fixed(maxl / 1000.0)} sky irradiance(up) {fixed(skye)} sun irradiance {fixed(sg)} (Q16.16 = /65536)`)
return out
}
@ -414,22 +414,22 @@ function tex_target(w: int, h: int, ifmt: int, fmt: int, ty: int, filter: int) -
var tex_dump_alpha: bool = false
# Debug: the brightest texel of an RGBA float texture and where it is.
function tex_max(tex: int, w: int, h: int, tag: pointer) -> void {
let buf = words(w * h * 4)
let buf = floats(w * h * 4)
gpu_tex_bind(GPU_TEX2D, tex)
gpu_pixel_store(GL_PACK_ALIGNMENT, 4)
gpu_tex_read(GPU_TEX2D, GL_RGBA, GL_FLOAT, buf)
var best = F_ZERO; var bx = 0; var by = 0
var best = 0.0; var bx = 0; var by = 0
var i = 0
while i < w * h {
let v = f_max(buf[i * 4], f_max(buf[i * 4 + 1], buf[i * 4 + 2]))
if f_gt(v, best) { best = v; bx = i % w; by = i / w }
let v = Math.max(buf[i * 4], Math.max(buf[i * 4 + 1], buf[i * 4 + 2]))
if v > best { best = v; bx = i % w; by = i / w }
i += 1
}
print(`{tag}: max {f_fx(f_div(best, fi(100)))}/100 at {bx} {h - 1 - by} (top-down)`)
print(`{tag}: max {fixed(best / 100.0)}/100 at {bx} {h - 1 - by} (top-down)`)
let o = (by * w + bx) * 4
let big = fi(65000)
let finite = f_ls(best, big)
print(` rgba (clamped/100): {f_fx(f_div(f_min(buf[o], big), fi(100)))} {f_fx(f_div(f_min(buf[o + 1], big), fi(100)))} {f_fx(f_div(f_min(buf[o + 2], big), fi(100)))} {f_fx(f_min(buf[o + 3], big))} finite {finite} bits {buf[o]}`)
let big = 65000.0
let finite = best < big
print(` rgba (clamped/100): {fixed(Math.min(buf[o], big) / 100.0)} {fixed(Math.min(buf[o + 1], big) / 100.0)} {fixed(Math.min(buf[o + 2], big) / 100.0)} {fixed(Math.min(buf[o + 3], big))} finite {finite} bits {buf[o]}`)
free(buf)
}
# Debug: write a 2D texture's level 0 (RGBA8, alpha dropped) as a binary PPM.

View file

@ -7,20 +7,20 @@
var water_mesh: Mesh = null
var water_prog: int = 0
var water_level: int = 0
var water_cx: int = 0
var water_cz: int = 0
var water_ex: int = 0
var water_ez: int = 0
var water_level: float = 0.0
var water_cx: float = 0.0
var water_cz: float = 0.0
var water_ex: float = 0.0
var water_ez: float = 0.0
var water_on: bool = false
# Where a body is standing in the water and how hard it is disturbing it. The game sets it;
# strength 0 means nobody is in the water and the whole term is skipped.
var wt_wade_x: int = 0
var wt_wade_z: int = 0
var wt_wade_s: int = 0
var wt_wade_x: float = 0.0
var wt_wade_z: float = 0.0
var wt_wade_s: float = 0.0
var water_refl: Target = null # the world mirrored in the surface, half resolution
var water_refl_div: int = 2 # R3D_REFLDIV overrides: 2 = half res, 4 = quarter
var water_saved: words = null # the real camera's matrices, restored after the pass
var water_saved: floats = null # the real camera's matrices, restored after the pass
var water_dumped: bool = false
# Several still-water planes, each at its own level over its own bounds: the sea round an
# island and a lake a hundred metres above it cannot be one surface. Each draws the same way;
@ -29,11 +29,11 @@ var water_dumped: bool = false
# code written against a single plane still reads the surface that reflects.
const WATER_MAX: int = 8
var wb_n: int = 0
var wb_level: words = null
var wb_cx: words = null
var wb_cz: words = null
var wb_ex: words = null
var wb_ez: words = null
var wb_level: floats = null
var wb_cx: floats = null
var wb_cz: floats = null
var wb_ex: floats = null
var wb_ez: floats = null
var wb_reflect: words = null
var wb_primary: int = -1 # the body the reflection pass mirrors, or -1
@ -48,7 +48,7 @@ function water_reflection_pass() -> void {
# this reflection is composited into is smaller than the drawable, and a reflection
# rendered at the window's size would be paying for pixels the water never samples
water_refl = target_new(post_w / water_refl_div, post_h / water_refl_div, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
water_saved = words(16 * 4 + 3)
water_saved = floats(16 * 4 + 3)
}
# save the camera
m4_copy(water_saved, cam_view)
@ -58,11 +58,11 @@ function water_reflection_pass() -> void {
# the mirrored camera: view' = view * R, R reflecting y about the surface (y' = 2L - y).
# R has determinant -1, so the winding flips (front faces culled below) and the image
# lands exactly where the main camera's pixels expect the reflection.
let eye = v3_new(sx, f_sub(f_mul(F_TWO, water_level), sy), sz)
let eye = v3_new(sx, 2.0 * water_level - sy, sz)
let refl = m4_new()
refl[5] = f_neg1()
refl[13] = f_mul(F_TWO, water_level)
let mv = words(16)
refl[5] = -1.0
refl[13] = 2.0 * water_level
let mv = floats(16)
m4_mul(mv, water_saved, refl)
m4_copy(cam_view, mv)
free(mv); free(refl)
@ -70,7 +70,7 @@ function water_reflection_pass() -> void {
m4_mul(cam_vp, cam_proj, cam_view)
m4_inverse(cam_inv_vp, cam_vp)
v3_copy(cam_pos, eye)
r3d_clip_y = f_sub(water_level, fl(0.05))
r3d_clip_y = water_level - 0.05
target_bind(water_refl)
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
@ -96,7 +96,7 @@ function water_reflection_pass() -> void {
gpu_cull_face(GL_BACK)
if r3d_env_has("R3D_DUMP_REFL") and not water_dumped { water_dumped = true; tex_dump(water_refl.color, water_refl.w, water_refl.h, "build/dbg_refl.ppm") }
# restore
r3d_clip_y = 0xCF000000
r3d_clip_y = -2147483600.0
m4_copy(cam_view, water_saved)
m4_copy(cam_vp, mem_off(water_saved, 64))
m4_copy(cam_inv_vp, mem_off(water_saved, 128))
@ -114,7 +114,7 @@ function water_reflection_pass() -> void {
# asking, so it errs on the side of running. R3D_REFL_ALWAYS=1 runs it on every frame, for comparing.
# Mac, the five shot viewpoints: looking down at the meadow (c) 225 reflection draws -> none; every view
# with the lake in it unchanged, byte-identical frames.
var wb_cells: words = null # x0, z0, x1, z1 of each rectangle with water showing
var wb_cells: floats = null # x0, z0, x1, z1 of each rectangle with water showing
var wb_ncells: int = -1 # -1: not built for the current mirrored body
var wb_blocks: words = null # x0, z0, x1, z1, first cell, cells past the last - of each block
var wb_nblocks: int = 0
@ -141,7 +141,7 @@ function water_reflect_visible() -> bool {
var checked = 0
for b in 0 .. wb_nblocks {
let o = b * 6
if not wb_rect_visible(wb_blocks[o], wb_blocks[o + 1], wb_blocks[o + 2], wb_blocks[o + 3]) { continue }
if not wb_rect_visible(float_from_bits(wb_blocks[o]), float_from_bits(wb_blocks[o + 1]), float_from_bits(wb_blocks[o + 2]), float_from_bits(wb_blocks[o + 3])) { continue }
for i in wb_blocks[o + 4] .. wb_blocks[o + 5] {
if not wb_cell_visible(i) { continue }
if checked >= 16 { return true }
@ -154,14 +154,14 @@ function water_reflect_visible() -> bool {
# A flat rectangle at the water's level against the view's side planes: hidden when all four corners
# lie outside one plane. (A bounding sphere was the first version: a 156 m cell's sphere reached into
# the view from a lake the camera had its back to, and the pass never skipped.)
function wb_rect_visible(x0: int, z0: int, x1: int, z1: int) -> bool {
function wb_rect_visible(x0: float, z0: float, x1: float, z1: float) -> bool {
if cam_planes == null { return true }
for p in 0 .. 4 {
let q = p * 4
let a = cam_planes[q]; let c = cam_planes[q + 2]
let by = f_add(f_mul(cam_planes[q + 1], water_level), cam_planes[q + 3])
let ax0 = f_mul(a, x0); let ax1 = f_mul(a, x1); let cz0 = f_mul(c, z0); let cz1 = f_mul(c, z1)
if f_ls(f_add(f_add(ax0, cz0), by), F_ZERO) and f_ls(f_add(f_add(ax1, cz0), by), F_ZERO) and f_ls(f_add(f_add(ax0, cz1), by), F_ZERO) and f_ls(f_add(f_add(ax1, cz1), by), F_ZERO) { return false }
let by = cam_planes[q + 1] * water_level + cam_planes[q + 3]
let ax0 = a * x0; let ax1 = a * x1; let cz0 = c * z0; let cz1 = c * z1
if ax0 + cz0 + by < 0.0 and ax1 + cz0 + by < 0.0 and ax0 + cz1 + by < 0.0 and ax1 + cz1 + by < 0.0 { return false }
}
return true
}
@ -173,17 +173,17 @@ function wb_cell_visible(i: int) -> bool {
# sight line is sampled at seven points short of both ends, with half a metre (and a little more with
# distance) of margin, so a far ridge the drawn terrain rounds off never hides real water. A camera at
# or under the surface hides nothing behind the ground.
function wb_point_hidden(x: int, z: int) -> bool {
if f_gt(terrain_height(x, z), f_add(water_level, fl(0.05))) { return true }
if not cam_sphere_visible(x, water_level, z, F_HALF) { return true }
function wb_point_hidden(x: float, z: float) -> bool {
if terrain_height(x, z) > water_level + 0.05 { return true }
if not cam_sphere_visible(x, water_level, z, 0.5) { return true }
let ey = cam_pos[1]
if not f_gt(ey, water_level) { return false }
let dx = f_sub(x, cam_pos[0]); let dz = f_sub(z, cam_pos[2])
let margin = f_add(F_HALF, f_mul(f_sqrt(f_add(f_mul(dx, dx), f_mul(dz, dz))), fl(0.004)))
if not (ey > water_level) { return false }
let dx = x - cam_pos[0]; let dz = z - cam_pos[2]
let margin = 0.5 + Math.sqrt(dx * dx + dz * dz) * 0.004
for k in 1 .. 8 {
let t = fr(k, 8)
let sy = f_add(ey, f_mul(f_sub(water_level, ey), t))
if f_gt(terrain_height(f_add(cam_pos[0], f_mul(dx, t)), f_add(cam_pos[2], f_mul(dz, t))), f_add(sy, margin)) { return true }
let t = float(k) / 8.0
let sy = ey + (water_level - ey) * t
if terrain_height(cam_pos[0] + dx * t, cam_pos[2] + dz * t) > sy + margin { return true }
}
return false
}
@ -191,33 +191,33 @@ function wb_point_hidden(x: int, z: int) -> bool {
function wb_cell_occluded(i: int) -> bool {
let o = i * 4
let x0 = wb_cells[o]; let z0 = wb_cells[o + 1]; let x1 = wb_cells[o + 2]; let z1 = wb_cells[o + 3]
let cx = f_mul(f_add(x0, x1), F_HALF); let cz = f_mul(f_add(z0, z1), F_HALF)
let ox = f_mul(f_sub(x1, x0), fl(0.45)); let oz = f_mul(f_sub(z1, z0), fl(0.45))
let cx = (x0 + x1) * 0.5; let cz = (z0 + z1) * 0.5
let ox = (x1 - x0) * 0.45; let oz = (z1 - z0) * 0.45
if not wb_point_hidden(cx, cz) { return false }
if not wb_point_hidden(f_sub(cx, ox), f_sub(cz, oz)) { return false }
if not wb_point_hidden(f_add(cx, ox), f_sub(cz, oz)) { return false }
if not wb_point_hidden(f_sub(cx, ox), f_add(cz, oz)) { return false }
return wb_point_hidden(f_add(cx, ox), f_add(cz, oz))
if not wb_point_hidden(cx - ox, cz - oz) { return false }
if not wb_point_hidden(cx + ox, cz - oz) { return false }
if not wb_point_hidden(cx - ox, cz + oz) { return false }
return wb_point_hidden(cx + ox, cz + oz)
}
function wb_cell_add(x0: int, z0: int, x1: int, z1: int) -> void {
function wb_cell_add(x0: float, z0: float, x1: float, z1: float) -> void {
let o = wb_ncells * 4
wb_cells[o] = x0; wb_cells[o + 1] = z0; wb_cells[o + 2] = x1; wb_cells[o + 3] = z1
wb_ncells += 1
}
function wb_block_add(x0: int, z0: int, x1: int, z1: int, first: int) -> void {
function wb_block_add(x0: float, z0: float, x1: float, z1: float, first: int) -> void {
if wb_ncells <= first { return }
let o = wb_nblocks * 6
wb_blocks[o] = x0; wb_blocks[o + 1] = z0; wb_blocks[o + 2] = x1; wb_blocks[o + 3] = z1
wb_blocks[o] = float_bits(x0); wb_blocks[o + 1] = float_bits(z0); wb_blocks[o + 2] = float_bits(x1); wb_blocks[o + 3] = float_bits(z1)
wb_blocks[o + 4] = first; wb_blocks[o + 5] = wb_ncells
wb_nblocks += 1
}
function wb_wet(x: int, z: int, off: int) -> bool {
if f_ls(terrain_height(x, z), water_level) { return true }
if off == 0 { return false }
if f_ls(terrain_height(f_sub(x, off), f_sub(z, off)), water_level) { return true }
if f_ls(terrain_height(f_add(x, off), f_sub(z, off)), water_level) { return true }
if f_ls(terrain_height(f_sub(x, off), f_add(z, off)), water_level) { return true }
return f_ls(terrain_height(f_add(x, off), f_add(z, off)), water_level)
function wb_wet(x: float, z: float, off: float) -> bool {
if terrain_height(x, z) < water_level { return true }
if off == 0.0 { return false }
if terrain_height(x - off, z - off) < water_level { return true }
if terrain_height(x + off, z - off) < water_level { return true }
if terrain_height(x - off, z + off) < water_level { return true }
return terrain_height(x + off, z + off) < water_level
}
# Over the height map, 32 m rectangles (at most 512 a side) tested at five points and kept in blocks of
# 8 x 8, so a block out of view skips its rectangles in one test - looking away from the water was a
@ -225,59 +225,59 @@ function wb_wet(x: int, z: int, off: int) -> bool {
# coarse rectangles tested at their centre, in one last block, which only ever matter kilometres away.
const WB_BLOCK: int = 8
function water_cells_build() -> void {
let bx0 = f_sub(water_cx, water_ex); let bx1 = f_add(water_cx, water_ex)
let bz0 = f_sub(water_cz, water_ez); let bz1 = f_add(water_cz, water_ez)
let th = fi(TERRAIN_HALF)
let tx0 = f_max(bx0, f_sub(ter_ox, th)); let tx1 = f_min(bx1, f_add(ter_ox, th))
let tz0 = f_max(bz0, f_sub(ter_oz, th)); let tz1 = f_min(bz1, f_add(ter_oz, th))
let inside = f_gt(tx1, tx0) and f_gt(tz1, tz0)
var cell = fi(32)
let bx0 = water_cx - water_ex; let bx1 = water_cx + water_ex
let bz0 = water_cz - water_ez; let bz1 = water_cz + water_ez
let th = float(TERRAIN_HALF)
let tx0 = Math.max(bx0, ter_ox - th); let tx1 = Math.min(bx1, ter_ox + th)
let tz0 = Math.max(bz0, ter_oz - th); let tz1 = Math.min(bz1, ter_oz + th)
let inside = tx1 > tx0 and tz1 > tz0
var cell = 32.0
var nx = 0
var nz = 0
if inside {
let span = f_max(f_sub(tx1, tx0), f_sub(tz1, tz0))
if f_gt(f_div(span, cell), fi(512)) { cell = f_div(span, fi(512)) }
nx = f_to_int(f_div(f_sub(tx1, tx0), cell)) + 1
nz = f_to_int(f_div(f_sub(tz1, tz0), cell)) + 1
let span = Math.max(tx1 - tx0, tz1 - tz0)
if span / cell > 512.0 { cell = span / 512.0 }
nx = int((tx1 - tx0) / cell) + 1
nz = int((tz1 - tz0) / cell) + 1
}
if wb_cells != null { free(wb_cells) }
if wb_blocks != null { free(wb_blocks) }
wb_cells = words((nx * nz + 64 * 64) * 4)
wb_cells = floats((nx * nz + 64 * 64) * 4)
let nbx = (nx + WB_BLOCK - 1) / WB_BLOCK
let nbz = (nz + WB_BLOCK - 1) / WB_BLOCK
wb_blocks = words((nbx * nbz + 1) * 6)
wb_ncells = 0
wb_nblocks = 0
let off = f_mul(cell, fl(0.45))
let half = f_mul(cell, F_HALF)
let off = cell * 0.45
let half = cell * 0.5
for bz in 0 .. nbz {
for bx in 0 .. nbx {
let first = wb_ncells
var iz = bz * WB_BLOCK
while iz < (bz + 1) * WB_BLOCK and iz < nz {
let z0 = f_add(tz0, f_mul(fi(iz), cell))
let z0 = tz0 + float(iz) * cell
var ix = bx * WB_BLOCK
while ix < (bx + 1) * WB_BLOCK and ix < nx {
let x0 = f_add(tx0, f_mul(fi(ix), cell))
if wb_wet(f_add(x0, half), f_add(z0, half), off) { wb_cell_add(x0, z0, f_add(x0, cell), f_add(z0, cell)) }
let x0 = tx0 + float(ix) * cell
if wb_wet(x0 + half, z0 + half, off) { wb_cell_add(x0, z0, x0 + cell, z0 + cell) }
ix += 1
}
iz += 1
}
let x0 = f_add(tx0, f_mul(fi(bx * WB_BLOCK), cell)); let z0 = f_add(tz0, f_mul(fi(bz * WB_BLOCK), cell))
wb_block_add(x0, z0, f_add(x0, f_mul(fi(WB_BLOCK), cell)), f_add(z0, f_mul(fi(WB_BLOCK), cell)), first)
let x0 = tx0 + float(bx * WB_BLOCK) * cell; let z0 = tz0 + float(bz * WB_BLOCK) * cell
wb_block_add(x0, z0, x0 + float(WB_BLOCK) * cell, z0 + float(WB_BLOCK) * cell, first)
}
}
let first = wb_ncells
let cw = f_div(f_sub(bx1, bx0), fi(64))
let ch = f_div(f_sub(bz1, bz0), fi(64))
let cw = (bx1 - bx0) / 64.0
let ch = (bz1 - bz0) / 64.0
for iz in 0 .. 64 {
let z0 = f_add(bz0, f_mul(fi(iz), ch)); let z1 = f_add(z0, ch)
let z0 = bz0 + float(iz) * ch; let z1 = z0 + ch
for ix in 0 .. 64 {
let x0 = f_add(bx0, f_mul(fi(ix), cw)); let x1 = f_add(x0, cw)
let x0 = bx0 + float(ix) * cw; let x1 = x0 + cw
# inside the height map's rectangle: the fine cells above cover it
if inside and not f_ls(x0, tx0) and not f_gt(x1, tx1) and not f_ls(z0, tz0) and not f_gt(z1, tz1) { continue }
if wb_wet(f_add(x0, f_mul(cw, F_HALF)), f_add(z0, f_mul(ch, F_HALF)), 0) { wb_cell_add(x0, z0, x1, z1) }
if inside and not (x0 < tx0) and not (x1 > tx1) and not (z0 < tz0) and not (z1 > tz1) { continue }
if wb_wet(x0 + cw * 0.5, z0 + ch * 0.5, 0.0) { wb_cell_add(x0, z0, x1, z1) }
}
}
wb_block_add(bx0, bz0, bx1, bz1, first)
@ -286,10 +286,10 @@ function water_cells_build() -> void {
# the program and the plane are the process's, built once; the bodies are the map's
function water_setup() -> void {
if water_prog != 0 { return }
water_mesh = mesh_grid(2, F_HALF)
water_mesh = mesh_grid(2, 0.5)
water_prog = r3d_program("water.vert", "water.frag", "")
wb_level = words(WATER_MAX); wb_cx = words(WATER_MAX); wb_cz = words(WATER_MAX)
wb_ex = words(WATER_MAX); wb_ez = words(WATER_MAX); wb_reflect = words(WATER_MAX)
wb_level = floats(WATER_MAX); wb_cx = floats(WATER_MAX); wb_cz = floats(WATER_MAX)
wb_ex = floats(WATER_MAX); wb_ez = floats(WATER_MAX); wb_reflect = words(WATER_MAX)
}
function water_bodies_clear() -> void {
wb_n = 0
@ -299,7 +299,7 @@ function water_bodies_clear() -> void {
}
# Add a plane at `level` over (cx, cz) +- (ex, ez); true `reflect` makes it the mirrored one
# if none is yet. Returns its index, or -1 once WATER_MAX are in use.
function water_body_add(level: int, cx: int, cz: int, ex: int, ez: int, reflect: bool) -> int {
function water_body_add(level: float, cx: float, cz: float, ex: float, ez: float, reflect: bool) -> int {
water_setup()
if wb_n >= WATER_MAX { return -1 }
let i = wb_n
@ -317,7 +317,7 @@ function water_body_add(level: int, cx: int, cz: int, ex: int, ez: int, reflect:
}
# one reflecting plane: what this function always meant, without a new mesh and program
# every time it is called
function water_init(level: int, cx: int, cz: int, ex: int, ez: int) -> void {
function water_init(level: float, cx: float, cz: float, ex: float, ez: float) -> void {
water_bodies_clear()
water_body_add(level, cx, cz, ex, ez, true)
}
@ -334,7 +334,7 @@ function water_draw(depth_tex: int) -> void {
# window. They are the same size only at a render scale of 1; at anything less, taking
# the window's size sent the refraction and depth reads into the wrong corner of the
# frame, and the lake showed a squashed copy of it instead of its own bed.
u_f2(gpu_uniform(p, "u_screen"), fi(post_w), fi(post_h))
u_f2(gpu_uniform(p, "u_screen"), float(post_w), float(post_h))
# Read into locals first. Passing these three globals straight into the call gives the
# shader wrong values - the whole lake churns instead of a patch of it - and a single dead
# `let junk = wt_wade_x` above the same unchanged call is enough to make it correct again.
@ -347,11 +347,11 @@ function water_draw(depth_tex: int) -> void {
let wz = wt_wade_z
let ws = wt_wade_s
u_f3(gpu_uniform(p, "u_wade"), wx, wz, ws)
var ron = F_ZERO
var ron = 0.0
if water_refl != null and wb_primary >= 0 {
# bind on its own unit first: generating the mip chain re-binds the texture on the active unit,
# and it must not displace the depth texture the shader reads for the shore
r3d_bind_2d(p, "u_refl", 1, water_refl.color); ron = F_ONE
r3d_bind_2d(p, "u_refl", 1, water_refl.color); ron = 1.0
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
gpu_tex_mips(GPU_TEX2D)
}
@ -375,15 +375,15 @@ function water_draw(depth_tex: int) -> void {
u_f(gpu_uniform(p, "u_level"), wb_level[i])
u_f2(gpu_uniform(p, "u_center"), wb_cx[i], wb_cz[i])
u_f2(gpu_uniform(p, "u_extent"), wb_ex[i], wb_ez[i])
var r = F_ZERO
var r = 0.0
if i == wb_primary { r = ron }
u_f(gpu_uniform(p, "u_refl_on"), r)
# Every body is clipped to the ellipse inside its bounds but an unbounded mirrored sea,
# whose rectangle is the point. A mirrored LAKE is clipped like any other: a map whose
# reflection belongs to its lake (the Bells in Maroon Lake) would otherwise draw that
# lake's level over every hollow in the survey.
var clip = F_ONE
if i == wb_primary and f_gt(f_max(wb_ex[i], wb_ez[i]), fi(10000)) { clip = F_ZERO }
var clip = 1.0
if i == wb_primary and Math.max(wb_ex[i], wb_ez[i]) > 10000.0 { clip = 0.0 }
u_f(gpu_uniform(p, "u_clip_ellipse"), clip)
mesh_draw(water_mesh)
}