- r3d_open opens the window with nothing baked; r3d_load_count / r3d_load_step run the rest (sky and light, terrain, shadow and screen targets, cover and actors, grass) so a game can present a loading frame between them. r3d_init is the same calls in a row. - sky_set_quality(width): the prefiltered sky light at 256 / 512 / 1024, baked again. - post_set_msaa(samples): multisampling on OpenGL, remade in place (post_msaa_live is false on Vulkan); post_free frees the multisampled framebuffer too. - STREAM_BUDGET_US is a variable; r3d_fog_scale multiplies the fog the day sets. - Vulkan buffers carry UNIFORM_BUFFER usage: the frame's ring is bound as uniform buffers and was created without it (VUID-VkWriteDescriptorSet-descriptorType-00330, found on MoltenVK). OpenGL frames at the five viewpoints unchanged; the game's 59 self-tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
205 lines
11 KiB
Text
205 lines
11 KiB
Text
# ============================================================================
|
|
# daylight.ludic — a time of day over the HDRI sky. The photograph is one late
|
|
# morning; the game wants a whole day. The sun's direction and colour follow a
|
|
# simple solar arc from the time (`daylight_set`), the sky image turns to keep
|
|
# its disc under that sun, the image-based light is scaled toward a deep-blue
|
|
# night (`u_ibl_scale`), and after dusk the light becomes a moon so the world
|
|
# still has shadows and form. A campfire is the one point light (`u_fire_*`).
|
|
# The convolved sky maps are rebuilt only when the sky has turned far from the
|
|
# angle they were baked at; the height-field shadow rebakes as the sun moves.
|
|
# ============================================================================
|
|
|
|
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_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_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 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
|
|
|
|
function daylight_init() -> void {
|
|
day_ibl = v3_new(F_ONE, F_ONE, F_ONE)
|
|
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)
|
|
day_light = F_ONE
|
|
}
|
|
|
|
# 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 {
|
|
day_yaw0 = yaw0; day_hour0 = hour0
|
|
day_az0 = f_atan2(f_neg(sun_dir[0]), f_neg(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) }
|
|
day_on = true
|
|
day_baked_az = fi(1000)
|
|
daylight_set(hour0)
|
|
}
|
|
function day_wrap(a: int) -> int {
|
|
let two_pi = f_mul(F_TWO, F_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) }
|
|
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 daylight_set(hours: int) -> void {
|
|
var h = f_mod(hours, fi(24))
|
|
if f_ls(h, F_ZERO) { h = f_add(h, fi(24)) }
|
|
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))
|
|
day_az = az; day_el = el
|
|
let d = smoothf(f_rad(fi(-8)), f_rad(fi(12)), 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)
|
|
# 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))
|
|
# 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)))
|
|
day_moon = false
|
|
if f_ls(el, f_rad(fi(-7))) {
|
|
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)
|
|
# 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 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)))
|
|
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)))
|
|
# 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)))
|
|
# 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) }
|
|
if day_fog_base == 0 { day_fog_base = r3d_fog_density }
|
|
r3d_fog_density = f_mul(f_mul(day_fog_base, day_fog_mul), r3d_fog_scale)
|
|
# 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)
|
|
# 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))
|
|
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)) {
|
|
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))) {
|
|
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) }
|
|
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)
|
|
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 {
|
|
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 {
|
|
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))
|
|
}
|
|
|
|
# the light in the hand: a point light (cone < -1) or a cone along dir (cone = cos half-angle)
|
|
function daylight_hand(x: int, y: int, z: int, dx: int, dy: int, dz: int, cone: int, r: int, g: int, b: int) -> 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)
|
|
}
|
|
function daylight_bind(prog: int) -> void {
|
|
u_v3(gpu_uniform(prog, "u_hand_pos"), hand_pos)
|
|
u_v3(gpu_uniform(prog, "u_hand_color"), hand_color)
|
|
u_v3(gpu_uniform(prog, "u_hand_dir"), hand_dir)
|
|
u_f(gpu_uniform(prog, "u_hand_cone"), hand_cone)
|
|
u_v3(gpu_uniform(prog, "u_ibl_scale"), day_ibl)
|
|
u_v3(gpu_uniform(prog, "u_moon_dir"), day_moon_dir)
|
|
u_f(gpu_uniform(prog, "u_moon_phase"), day_moon_phase)
|
|
u_f(gpu_uniform(prog, "u_moon_illum"), day_moon_illum)
|
|
u_f(gpu_uniform(prog, "u_moon_haze"), day_overcast)
|
|
u_f(gpu_uniform(prog, "u_daylight"), day_light)
|
|
u_v3(gpu_uniform(prog, "u_fire_pos"), fire_pos)
|
|
u_v3(gpu_uniform(prog, "u_fire_color"), fire_color)
|
|
}
|