# ============================================================================ # 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) } # ---- 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, # so three things move together with its elevation: how much air there is, how hard that air # scatters the sun forward, and how low in the valley it lies. `lowsun` peaks at the horizon # and falls away on both sides - it has to fall away BELOW it too, or the middle of the night # 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) # 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))) # 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))) # and distance takes colour away at every hour, harder under cloud r3d_fog_desat = f_add(fl(1.9), f_mul(fl(0.6), 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 # sun is warm-white and the ONLY thing filling a midday shadow is a blue sky, so a midday # frame wants a cool, lifted shadow and a hard contrast between it and the lit ground. Dawn # and dusk are the opposite - a warm low sun, warm shadow fill off the whole sky, and less # contrast because the light is coming through so much air. Overcast flattens and drains both. # `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 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))) # 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))) # 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))) # 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))) # ---- what is IN the air ------------------------------------------------------------- # The volumetric march (post.ludic) needs the same story the analytic fog tells, or the # shafts and the haze disagree. Density rides the sun's elevation like everything else; the # MIST is separate and is the thing a valley actually does - a shallow layer that forms in # 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)) # 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 } # 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) }