diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fdb3bc..6b56161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to Lumenwright are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Changed + +- On the client's day plateau (a vanilla sky, or a stated sky at a tick, whose + `visual/sky_light_factor` is at its day value) HQ mode's default emitter intensity is 0, + `DAY_EMITTER_INTENSITY`: Chunky's default scene, a daylight scene, has its emitters off. + A lantern in full daylight is drawn as its texture lit by the sun and the sky, as the + block beside it is, not as a bright point. Every other frame takes the intensity it took + before, bit for bit (`default_emitter_intensity`); a stated `--emitter-intensity` is used + as given. The default steps at the day plateau's edges + (`docs/research/daylight-emitters.md`). + +### Added + +- `demo-world --daylight-emitters`: a lit and an unlit furnace, a lantern, glowstone beside + sandstone and a torch on a sunlit yard. + ## [0.4.0] - 2026-11-10 ### Added diff --git a/README.md b/README.md index 7c75e14..91aa4f8 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ Both modes run on several native backends: **CPU** (all cores), **Metal**, **Vul ![The Treehouse Camp by day, path traced](docs/renders/treehouse-camp-day-hq.png) -*hq · `--time 4000` · 4096 spp · 8 bounces · CUDA on an NVIDIA GeForce RTX 4070 Ti SUPER · 91 s end to end* +*hq · `--time 4000` · 4096 spp · 8 bounces · Metal on an Apple M2 Pro · 449 s end to end* | | quick (vanilla look) | hq (path traced) | |---|---|---| -| **The whole camp, day** | ![camp, day, quick](docs/renders/treehouse-camp-day-quick.png) quick · `--time 4000` · Metal · Apple M2 Pro | ![camp, day, hq](docs/renders/treehouse-camp-day-hq.png) hq · `--time 4000` · 4096 spp · 8 bounces · CUDA · RTX 4070 Ti SUPER | +| **The whole camp, day** | ![camp, day, quick](docs/renders/treehouse-camp-day-quick.png) quick · `--time 4000` · Metal · Apple M2 Pro | ![camp, day, hq](docs/renders/treehouse-camp-day-hq.png) hq · `--time 4000` · 4096 spp · 8 bounces · Metal · Apple M2 Pro | | **The whole camp, dusk** | ![camp, dusk, quick](docs/renders/treehouse-camp-dusk-quick.png) quick · `--time 12800` · Metal · Apple M2 Pro | ![camp, dusk, hq](docs/renders/treehouse-camp-dusk-hq.png) hq · `--time 12800` · 16384 spp · 8 bounces · CUDA · RTX 4070 Ti SUPER | -| **The Long Bridge, day** | ![bridge, day, quick](docs/renders/treehouse-bridge-day-quick.png) quick · `--time 4000` · Metal · Apple M2 Pro | ![bridge, day, hq](docs/renders/treehouse-bridge-day-hq.png) hq · `--time 4000` · 4096 spp · 8 bounces · CUDA · RTX 4070 Ti SUPER | +| **The Long Bridge, day** | ![bridge, day, quick](docs/renders/treehouse-bridge-day-quick.png) quick · `--time 4000` · Metal · Apple M2 Pro | ![bridge, day, hq](docs/renders/treehouse-bridge-day-hq.png) hq · `--time 4000` · 4096 spp · 8 bounces · Metal · Apple M2 Pro | | **The Long Bridge, dusk** | ![bridge, dusk, quick](docs/renders/treehouse-bridge-dusk-quick.png) quick · `--time 12800` · Metal · Apple M2 Pro | ![bridge, dusk, hq](docs/renders/treehouse-bridge-dusk-hq.png) hq · `--time 12800` · 16384 spp · 8 bounces · CUDA · RTX 4070 Ti SUPER | *The Treehouse Camp* is a small adventure map: a forest clan's houses on four giant trees, @@ -123,7 +123,7 @@ The Treehouse Camp save, more commands and generated worlds: [docs/renders.md](d * [The Treehouse Camp renders](docs/renders.md): each image's command and time, how HQ mode lights it, getting the camp, more commands. * [Benchmark](docs/benchmark.md): Lumenwright against Chunky on one machine, the backends against each other, the cost of samples; design, tables and instruments. -* [The game's sky](docs/research/game-sky.md), [the sea at the horizon](docs/research/sea-horizon.md) and [HQ mode against Chunky](docs/research/chunky-parity.md): what HQ mode ports and how it compares. +* [The game's sky](docs/research/game-sky.md), [the sea at the horizon](docs/research/sea-horizon.md), [emitters in a clear daytime frame](docs/research/daylight-emitters.md) and [HQ mode against Chunky](docs/research/chunky-parity.md): what HQ mode ports and how it compares. * [Command line](docs/cli.md): every `render` flag. * [Library](docs/library.md): using the crate and the `render` API. * [Textures](docs/textures.md): the client jar, the Unifont and resource packs. diff --git a/crates/lumenwright-cli/src/fixture.rs b/crates/lumenwright-cli/src/fixture.rs index 53c7201..e79b873 100644 --- a/crates/lumenwright-cli/src/fixture.rs +++ b/crates/lumenwright-cli/src/fixture.rs @@ -18,7 +18,10 @@ pub const DATA_VERSION: i32 = 4671; pub const VERSION_NAME: &str = "1.21.11"; mod emitters; -pub use emitters::{CANDLE_ROOM_VIEW, FLOOR_Y, TORCH_ROOM_VIEW, candle_room, torch_room}; +pub use emitters::{ + CANDLE_ROOM_VIEW, DAYLIGHT_EMITTERS_VIEW, FLOOR_Y, TORCH_ROOM_VIEW, candle_room, + daylight_emitters, torch_room, +}; mod campaign_entities; pub use campaign_entities::{ ENTITY_CASES, ENTITY_GALLERY_VIEW, EntityCase, OCEAN_VIEW, entity_gallery, ocean, diff --git a/crates/lumenwright-cli/src/fixture/emitters.rs b/crates/lumenwright-cli/src/fixture/emitters.rs index 3747fee..c1eee42 100644 --- a/crates/lumenwright-cli/src/fixture/emitters.rs +++ b/crates/lumenwright-cli/src/fixture/emitters.rs @@ -89,3 +89,59 @@ pub const CANDLE_ROOM_VIEW: [&str; 6] = [ "--fov", "75", ]; + +/// Emitters beside sunlit blocks in the open: on a smooth stone yard 21 × 21 blocks, a lit +/// furnace at (−3, 65, 0) and an unlit one at (3, 65, 0), whose tops are the same texture; +/// a lantern on a cobblestone wall block at (0, 66, −3), glowstone beside sandstone at +/// (−3, 65, −4) and (−2, 65, −4), and a torch at (3, 65, −3). Noon unless rendered at a +/// stated time. +pub fn daylight_emitters() -> FixtureWorld { + let mut w = FixtureWorld::new(); + for x in -10..=10 { + for z in -10..=10 { + w.set(x, FLOOR_Y, z, bs("smooth_stone")); + } + } + let furnace = |lit: &str| props("furnace", &[("facing", "south"), ("lit", lit)]); + w.set(-3, FLOOR_Y + 1, 0, furnace("true")); + w.set(3, FLOOR_Y + 1, 0, furnace("false")); + w.set( + 0, + FLOOR_Y + 1, + -3, + props( + "cobblestone_wall", + &[ + ("up", "true"), + ("north", "none"), + ("south", "none"), + ("east", "none"), + ("west", "none"), + ("waterlogged", "false"), + ], + ), + ); + w.set( + 0, + FLOOR_Y + 2, + -3, + props("lantern", &[("hanging", "false"), ("waterlogged", "false")]), + ); + w.set(-3, FLOOR_Y + 1, -4, bs("glowstone")); + w.set(-2, FLOOR_Y + 1, -4, bs("sandstone")); + w.set(3, FLOOR_Y + 1, -3, bs("torch")); + w.player = Some(([0.5, 72.0, 7.5], 180.0, 40.0)); + w.day_time = 6000; + w +} + +/// Where the daylight emitters are seen from: south of the yard and above it, looking down +/// on both furnaces' tops. +pub const DAYLIGHT_EMITTERS_VIEW: [&str; 6] = [ + "--camera", + "0.5,72,7.5", + "--look-at", + "0.5,65,-0.5", + "--fov", + "55", +]; diff --git a/crates/lumenwright-cli/src/job.rs b/crates/lumenwright-cli/src/job.rs index 56099e4..129cee0 100644 --- a/crates/lumenwright-cli/src/job.rs +++ b/crates/lumenwright-cli/src/job.rs @@ -93,8 +93,9 @@ pub struct RenderArgs { #[arg(long)] pub sky_intensity: Option, /// HQ: the emitter intensity (an emitting surface emits its texel colour squared times - /// its block's emittance times this; 0 turns emitters off). Default: each look's own, - /// mixed as the sky is: Chunky's 13, and the game's look's, authored by Lumenwright. + /// its block's emittance times this; 0 turns emitters off). Default: 0 on the client's + /// day plateau (Chunky's default scene has its emitters off); elsewhere each look's + /// own, mixed as the sky is: Chunky's 13, and the game's look's, authored by Lumenwright. #[arg(long)] pub emitter_intensity: Option, /// HQ: how a diffuse surface samples the emitters near it (Chunky's strategies). diff --git a/crates/lumenwright-cli/src/main.rs b/crates/lumenwright-cli/src/main.rs index 26ce431..291f80b 100644 --- a/crates/lumenwright-cli/src/main.rs +++ b/crates/lumenwright-cli/src/main.rs @@ -68,6 +68,9 @@ enum Cmd { /// Instead of the showcase, an ocean to the edge of 17 × 17 chunks with an island. #[arg(long, conflicts_with_all = ["lineup", "block_entities", "items", "torch_room", "candle_room", "entity_gallery"])] ocean: bool, + /// Instead of the showcase, emitters beside sunlit blocks in an open yard. + #[arg(long, conflicts_with_all = ["lineup", "block_entities", "items", "torch_room", "candle_room", "entity_gallery", "ocean"])] + daylight_emitters: bool, }, } @@ -154,8 +157,14 @@ fn main() -> Result<()> { candle_room, entity_gallery, ocean, + daylight_emitters, } => { - let (what, world) = if ocean { + let (what, world) = if daylight_emitters { + ( + "daylight emitters", + lumenwright_cli::fixture::daylight_emitters(), + ) + } else if ocean { ("ocean", lumenwright_cli::fixture::ocean()) } else if entity_gallery { ( diff --git a/crates/lumenwright-cli/tests/render.rs b/crates/lumenwright-cli/tests/render.rs index ad604d4..fdd82ae 100644 --- a/crates/lumenwright-cli/tests/render.rs +++ b/crates/lumenwright-cli/tests/render.rs @@ -787,7 +787,9 @@ fn chunky_scales_are_refused_under_the_game_sky() { /// The default sky goes by the time of day: on the client's day plateau it is Chunky's look /// byte for byte, on the night plateau the game's, and across the evening ramp of /// `visual/sky_light_factor` a frame one tick later differs by as little as one tick -/// within either look does. +/// within either look does, with the emitters held at one intensity. The default emitter +/// intensity steps at the day plateau's edge, from the plateau's 0 to the mixed looks' +/// (`request::default_emitter_intensity`); the step is printed. #[test] fn the_default_sky_follows_the_day_without_a_jump() { let jar = require_jar!(); @@ -833,14 +835,23 @@ fn the_default_sky_follows_the_day_without_a_jump() { frame("night-game", "18000", &["--sky", "game"]).into_raw() ); // Across the edges of the ramp (11270 and 13140), one tick moves a frame no more than - // one tick does inside a look. - let inside = mean(&frame("a", "11269", &[]), &frame("b", "11270", &[])); - let (e0, e1) = (frame("c", "11270", &[]), frame("d", "11271", &[])); + // one tick does inside a look. The emitters are held at Chunky's intensity across the + // day plateau's edge, where the default intensity steps. + let held: &[&str] = &["--emitter-intensity", "13"]; + let inside = mean(&frame("a", "11269", held), &frame("b", "11270", held)); + let (e0, e1) = (frame("c", "11270", held), frame("d", "11271", held)); let (n0, n1) = (frame("e", "13139", &[]), frame("f", "13140", &[])); let (day_edge, night_edge) = (mean(&e0, &e1), mean(&n0, &n1)); + let (d0, d1) = (frame("g", "11270", &[]), frame("h", "11271", &[])); + assert_eq!( + d0.as_raw(), + frame("g-off", "11270", &["--emitter-intensity", "0"]).as_raw() + ); + let emitter_step = mean(&d0, &d1); eprintln!( "sky by time: one tick moves a frame by {inside:.3}/255 inside Chunky's look, \ - {day_edge:.3} at the day plateau's edge and {night_edge:.3} at the night's" + {day_edge:.3} at the day plateau's edge and {night_edge:.3} at the night's; with \ + the default emitter intensity {emitter_step:.3} at the day plateau's edge" ); assert!(day_edge <= 2.0 * inside.max(0.05), "{day_edge} vs {inside}"); assert!( @@ -1555,3 +1566,128 @@ fn the_water_plane_carries_the_sea_to_the_horizon() { "the edge stays with the water plane: {ja:.2} against {jb:.2}" ); } + +fn daylight_emitters() -> &'static Path { + static DIR: OnceLock = OnceLock::new(); + DIR.get_or_init(|| { + let d = tmp("daylight-emitters"); + fixture::daylight_emitters().write(&d).unwrap(); + d + }) +} + +/// The mean display colour of the `(2 radius + 1)²` pixels around `at`. +fn mean_near(img: &image::RgbaImage, at: (i32, i32), radius: i32) -> [f64; 3] { + let mut sum = [0.0f64; 3]; + let mut n = 0.0; + for y in at.1 - radius..=at.1 + radius { + for x in at.0 - radius..=at.0 + radius { + let p = img.get_pixel(x as u32, y as u32); + for (i, s) in sum.iter_mut().enumerate() { + *s += p.0[i] as f64; + } + n += 1.0; + } + } + sum.map(|s| s / n) +} + +/// The daylight emitters' frame at a tick, HQ on the CPU, with `extra` arguments. +fn daylight_frame(name: &str, tick: &str, extra: &[&str]) -> image::RgbaImage { + let jar = client_jar().unwrap(); + let dir = tmp(&format!("daylight-{name}")); + let out = dir.join("frame.png"); + let mut args = vec![ + "--mode", + "hq", + "--backend", + "cpu", + "--spp", + "64", + "--width", + "192", + "--height", + "108", + "--time", + tick, + ]; + args.extend_from_slice(&fixture::DAYLIGHT_EMITTERS_VIEW); + args.extend_from_slice(extra); + render(daylight_emitters(), &jar, &out, &args).unwrap(); + load(&out) +} + +/// Where a world point of the daylight emitters' frame falls (192 × 108). +fn daylight_pixel(p: [f64; 3]) -> (i32, i32) { + use lumenwright::glam::DVec3; + use lumenwright::scene::camera::Camera; + let pos = DVec3::new(0.5, 72.0, 7.5); + let (yaw, pitch) = Camera::look_at(pos, DVec3::new(0.5, 65.0, -0.5)); + let cam = Camera { + pos, + yaw, + pitch, + fov_y: 55.0, + }; + project(&cam, p, 192, 108).unwrap() +} + +/// The most any channel's mean differs between two regions. +fn channel_gap(a: [f64; 3], b: [f64; 3]) -> f64 { + (0..3).map(|i| (a[i] - b[i]).abs()).fold(0.0, f64::max) +} + +/// At noon under the default sky a lit furnace's top, an emitter, is drawn as the unlit +/// furnace's top beside it in the same sun: the same texture, within 3/255 per channel of +/// the regions' means. At the intensity the day plateau used to take (Chunky's 13) the +/// emitter's top is far brighter, so the comparison tells the two apart. +#[test] +fn a_daylight_emitter_is_drawn_as_its_sunlit_neighbour() { + let _jar = require_jar!(); + let lit = daylight_pixel([-2.5, 66.0, 0.5]); + let unlit = daylight_pixel([3.5, 66.0, 0.5]); + let gap = + |img: &image::RgbaImage| channel_gap(mean_near(img, lit, 3), mean_near(img, unlit, 3)); + let noon = daylight_frame("noon", "6000", &[]); + let at_13 = daylight_frame("noon-13", "6000", &["--emitter-intensity", "13"]); + let (now, then) = (gap(&noon), gap(&at_13)); + eprintln!( + "daylight emitter: lit vs unlit furnace top differ by {now:.2}/255 at noon by default, \ + {then:.2}/255 at emitter intensity 13; lit {:?} unlit {:?} (default)", + mean_near(&noon, lit, 3), + mean_near(&noon, unlit, 3) + ); + assert!( + now <= 3.0, + "the emitter's top differs from its neighbour's by {now:.2}/255" + ); + assert!( + then > 3.0 * 4.0, + "the comparison does not tell 13 apart: {then:.2}/255" + ); +} + +/// At midnight the default frame is the one the game look's emitter intensity draws, byte +/// for byte, and the lit furnace lights the floor in front of it: brighter than the floor +/// in front of the unlit one by more than 20/255 in some channel. +#[test] +fn a_night_emitter_still_lights_its_surroundings() { + let _jar = require_jar!(); + let night = daylight_frame("midnight", "18000", &[]); + let stated = daylight_frame( + "midnight-stated", + "18000", + &[ + "--emitter-intensity", + &lumenwright::request::GAME_EMITTER_INTENSITY.to_string(), + ], + ); + assert_eq!(night.as_raw(), stated.as_raw()); + let by_lit = mean_near(&night, daylight_pixel([-2.5, 65.0, 1.5]), 3); + let by_unlit = mean_near(&night, daylight_pixel([3.5, 65.0, 1.5]), 3); + eprintln!("night emitter: floor by the lit furnace {by_lit:?}, by the unlit one {by_unlit:?}"); + assert!( + (0..3).any(|i| by_lit[i] - by_unlit[i] > 20.0), + "{by_lit:?} vs {by_unlit:?}" + ); +} diff --git a/crates/lumenwright/src/request.rs b/crates/lumenwright/src/request.rs index b1072fb..27619d4 100644 --- a/crates/lumenwright/src/request.rs +++ b/crates/lumenwright/src/request.rs @@ -137,6 +137,30 @@ pub const DEFAULT_EMITTER_INTENSITY: f32 = 13.0; /// the engine's Chunky render of the frame, 41.993 (`docs/research/game-sky.md`, *Lanterns*). pub const GAME_EMITTER_INTENSITY: f32 = 137.9628; +/// The emitter intensity of a frame on the client's day plateau, 0: Chunky's default +/// scene has its emitters off (`Scene.DEFAULT_EMITTERS_ENABLED = false`, +/// chunky-dev/chunky 156e2bb), and it is a daylight scene. A lantern in full daylight is +/// then drawn as its texture lit by the sun and the sky, as a block beside it is. +pub const DAY_EMITTER_INTENSITY: f32 = 0.0; + +/// The emitter intensity HQ mode draws a frame at when the request states none. +/// +/// On the client's day plateau (`on_day_plateau`: the frame stands at a tick where the +/// client's `visual/sky_light_factor` holds its day value) it is +/// [`DAY_EMITTER_INTENSITY`]. Everywhere else it is each look's own mixed by the look's +/// day weight `weight`: `weight × `[`DEFAULT_EMITTER_INTENSITY`]` + (1 − weight) × ` +/// [`GAME_EMITTER_INTENSITY`]; so a frame off the plateau (the evening and morning ramps, +/// the night) and a stated sky at no tick (Chunky's look, as a rain or thunder frame is +/// stated) keep the intensity they were tuned at. The step at the plateau's edges (ticks +/// 730 and 11270 on the 1.21.11 day timeline) is the emitters' alone. +pub fn default_emitter_intensity(on_day_plateau: bool, weight: f32) -> f32 { + if on_day_plateau { + DAY_EMITTER_INTENSITY + } else { + weight * DEFAULT_EMITTER_INTENSITY + (1.0 - weight) * GAME_EMITTER_INTENSITY + } +} + /// Chunky's emitter sampling strategies (`EmitterSamplingStrategy`): which emitters a /// diffuse surface samples, among those its emitter-grid cell lists. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -161,9 +185,10 @@ pub struct HqOptions { /// Random seed; the same seed gives the same image. pub seed: u32, /// Chunky's emitter intensity: an emitting surface emits its texel colour squared - /// times its block's emittance times this. 0 turns emitters off. `None` takes each - /// look's own, mixed by the day weight: Chunky's [`DEFAULT_EMITTER_INTENSITY`] and, - /// under the game's sky, [`GAME_EMITTER_INTENSITY`]. + /// times its block's emittance times this. 0 turns emitters off. `None` takes + /// [`default_emitter_intensity`]: [`DAY_EMITTER_INTENSITY`] on the client's day + /// plateau, elsewhere each look's own, mixed by the day weight: Chunky's + /// [`DEFAULT_EMITTER_INTENSITY`] and, under the game's sky, [`GAME_EMITTER_INTENSITY`]. pub emitter_intensity: Option, /// How emitters are sampled at a diffuse surface (Chunky's emitter sampling /// strategy). @@ -744,9 +769,23 @@ pub fn render(req: &Request) -> Result { `--sky chunky`)" ); } - let emitter_intensity = req.hq.emitter_intensity.unwrap_or( - weight * DEFAULT_EMITTER_INTENSITY + (1.0 - weight) * GAME_EMITTER_INTENSITY, - ); + // The frame stands at a clock when the sky is the vanilla one or a stated sky at + // a tick; a stated sky at no tick names no time of day. + let clocked = match req.sky { + Sky::Vanilla { .. } => true, + Sky::Stated(s) => s.tick.is_some(), + }; + let on_day_plateau = clocked + && match (env.as_ref(), day_range) { + (Some(e), Some((night, day))) => { + day > night && sky::day_weight(e.sky_light_factor, night, day) >= 1.0 + } + _ => false, + }; + let emitter_intensity = req + .hq + .emitter_intensity + .unwrap_or_else(|| default_emitter_intensity(on_day_plateau, weight)); // Chunky's sun and sky, for the Chunky part of the frame. let chunky_sky = match req.sky { Sky::Stated(s) => s.hq, @@ -1189,14 +1228,35 @@ pub fn hq_params( mod tests { use super::*; - /// The default emitter intensity at the camp's dusk is the fitted 115.2423, and each - /// plateau takes its look's own. + /// The default emitter intensity at the camp's dusk is the fitted 115.2423; the night + /// plateau takes the game look's, a frame of Chunky's look off the day plateau (a stated + /// sky at no tick) Chunky's 13, and the day plateau Chunky's emitters off. #[test] fn the_lantern_default_is_the_fit_at_dusk() { - let at = |w: f32| w * DEFAULT_EMITTER_INTENSITY + (1.0 - w) * GAME_EMITTER_INTENSITY; + let at = |w: f32| default_emitter_intensity(false, w); assert!((at(0.181818) - 115.2423).abs() < 1e-3, "{}", at(0.181818)); assert_eq!(at(1.0), 13.0); assert_eq!(at(0.0), GAME_EMITTER_INTENSITY); + assert_eq!(default_emitter_intensity(true, 1.0), 0.0); + assert_eq!(default_emitter_intensity(true, 0.0), DAY_EMITTER_INTENSITY); + } + + /// Off the day plateau the default is the mix it was before the day plateau took its + /// own, bit for bit, at every weight one of the ramp's 1870 ticks gives. + #[test] + fn off_the_day_plateau_the_default_is_unchanged() { + let before = |w: f32| w * DEFAULT_EMITTER_INTENSITY + (1.0 - w) * GAME_EMITTER_INTENSITY; + let mut n = 0; + for k in 0..=1870u32 { + let w = k as f32 / 1870.0; + assert_eq!( + default_emitter_intensity(false, w).to_bits(), + before(w).to_bits(), + "{w}" + ); + n += 1; + } + assert_eq!(n, 1871); } /// `AtmosphericFogEnvironment.getBaseColor` in the 1.21.11 client lerps the clear colour diff --git a/docs/benchmark.md b/docs/benchmark.md index ad65976..6b83690 100644 --- a/docs/benchmark.md +++ b/docs/benchmark.md @@ -38,7 +38,8 @@ a cell that cannot be measured is marked n/a with its reason. `rayDepth` 5), Chunky's Preetham sky and a sun at altitude 45° from the south-east (Chunky altitude π/4, azimuth π/4), emitters at intensity 13 sampled `ONE`, gamma tone map, 250 samples per pixel. Lumenwright: `--sky chunky --tonemap gamma --time 4000 - --sun-altitude 45 --sun-azimuth 135 --spp 250 --max-bounces 4`. Chunky: + --sun-altitude 45 --sun-azimuth 135 --emitter-intensity 13 --spp 250 --max-bounces 4` + (the scene file turns Chunky's emitters on). Chunky: `docs/bench/treehouse-camp-bench.json`. It is the one frame both renderers draw by the same algorithm (Chunky cannot draw the game's sky with two maps). - **F-game**: the same camera, resolution, chunks and bounces under Lumenwright's defaults @@ -154,7 +155,7 @@ measured at `0b39f8f`, whose Chunky sky `--sky chunky` draws unchanged. ```sh # Lumenwright (--backend metal on the M2 Pro, cuda on the 4070) target/release/lumenwright render -w the-treehouse-camp --camera 140,118,14 --look-at 56,82,58 --fov 60 --render-distance 12 \ - --sky chunky --tonemap gamma --time 4000 --sun-altitude 45 --sun-azimuth 135 --mode hq --spp 250 --max-bounces 4 --backend metal -o lw.png + --sky chunky --tonemap gamma --time 4000 --sun-altitude 45 --sun-azimuth 135 --emitter-intensity 13 --mode hq --spp 250 --max-bounces 4 --backend metal -o lw.png # Chunky: the pinned core on its own classpath, as Delvewright's validation/chunky.sh runs it, without its denoise step mkdir -p scenes && cp docs/bench/treehouse-camp-bench.json scenes/ java -Djava.awt.headless=true -Dchunky.home=$HOME/.chunky -cp "" se.llbit.chunky.main.Chunky \ diff --git a/docs/cli.md b/docs/cli.md index 2be782a..a4fbcd7 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -23,7 +23,7 @@ | `--sky auto\|game\|chunky` | auto | HQ: by the time of day (Chunky's sky and sun on the client's day plateau, the game's sky on the night plateau, mixed between by the client's `visual/sky_light_factor`), or either look at every hour | | `--exposure`, `--tonemap gamma\|aces\|filmic\|filmic-mid-grey` | 1.0, each look's | HQ post-processing, Chunky's filters (each ends in the display gamma 2.2); `filmic-mid-grey` is Hable read brighter so linear 0.18 shows as the game shows it. Unstated, each look takes its own (`gamma` for Chunky's, `filmic-mid-grey` for the game's), mixed in display values under `auto` | | `--sun-intensity`, `--sky-intensity` | 1.25, 1 | HQ, Chunky's sky (`chunky`, `auto`): Chunky's sun intensity (irradiance intensity^2.2) and sky light; refused under `--sky game` | -| `--emitter-intensity` | each look's | HQ: the emitter intensity; an emitting surface emits its texel colour squared times its block's emittance times this (0 turns emitters off). Unstated: 13 (Chunky's) for Chunky's look and 137.9628 for the game's, authored by Lumenwright (fitted to Delvewright's engine's render at dusk), mixed linearly under `auto` | +| `--emitter-intensity` | each look's | HQ: the emitter intensity; an emitting surface emits its texel colour squared times its block's emittance times this (0 turns emitters off). Unstated: 0 when the frame stands at a tick on the client's day plateau (ticks 730 to 11270: Chunky's default scene has its emitters off); elsewhere 13 (Chunky's) for Chunky's look and 137.9628 for the game's, authored by Lumenwright (fitted to Delvewright's engine's render at dusk), mixed linearly under `auto` | | `--emitter-sampling none\|one\|one-block\|all`, `--prevent-normal-emitter` | one | HQ: Chunky's emitter sampling strategies, and its "prevent normal emitter with sampling" | | `--water-plane [SEA_LEVEL]` | off; 63 when given alone | HQ: Chunky's water plane, an unbounded sea outside the loaded chunks, level with the loaded sea, so a frame over an ocean shows sea to the horizon ([docs/research/sea-horizon.md](research/sea-horizon.md)) | | `--ssaa N` | 1 | quick-mode supersampling | diff --git a/docs/limitations.md b/docs/limitations.md index d0591de..1e925d8 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -48,5 +48,8 @@ See [docs/SCOPE.md §3](SCOPE.md). The main gaps: of the sun's disc is above the horizon; below it, the sky falls to a night's 3% at the end of civil twilight (the sun 6° below the horizon), and the sun gives no light from below the horizon. Those two rules are Lumenwright's own. The default game sky has none. +* On the client's day plateau HQ mode's emitters are off unless `--emitter-intensity` states + one, as in Chunky's default scene: a room lit only by lanterns is dark at noon + ([docs/research/daylight-emitters.md](research/daylight-emitters.md)). * The game's sky draws no clouds and no weather, and HQ mode's lanterns are Chunky's emitters, not the game's block light ([docs/research/game-sky.md](research/game-sky.md)). diff --git a/docs/renders.md b/docs/renders.md index ce0c834..7f4ff53 100644 --- a/docs/renders.md +++ b/docs/renders.md @@ -5,9 +5,10 @@ ## How they are drawn HQ mode runs with its defaults, which go by the time of day. At `--time 4000` the day -frames are Chunky's look: the Preetham sky, a sun of intensity 1.25 that lights and shadows, -lanterns and campfires as emitters at intensity 13 with one emitter sampled per diffuse -bounce, and the display gamma 2.2. +frames are Chunky's look with Chunky's default scene settings: the Preetham sky, a sun of +intensity 1.25 that lights and shadows, emitters off, so lanterns and campfires are their +textures in the sun ([docs/research/daylight-emitters.md](research/daylight-emitters.md)), +and the display gamma 2.2. At `--time 12800` the sun stands just below the horizon. Quick mode draws the client's sunset: the sun, the orange glow and the light the game stored in the save. HQ mode draws mostly the @@ -17,7 +18,7 @@ intensity 137.96, fitted to Delvewright's engine's render of this frame, and the that shows mid-grey as the game does (see [docs/research/game-sky.md](research/game-sky.md)). Each image's command, run from the repository root with the save from -[Get the camp](#get-the-camp) in `the-treehouse-camp`. The HQ commands need the CUDA build +[Get the camp](#get-the-camp) in `the-treehouse-camp`. The dusk HQ commands need the CUDA build (see [Backends](backends.md#backends)); on another backend the same flags give the same image within the parity the tests hold (`gpu_matches_cpu_in_hq_mode`, `cuda_matches_cpu_in_hq_mode`). @@ -26,7 +27,7 @@ parity the tests hold (`gpu_matches_cpu_in_hq_mode`, `cuda_matches_cpu_in_hq_mod target/release/lumenwright render -w the-treehouse-camp --camera 140,118,14 --look-at 56,82,58 --fov 60 --render-distance 12 \ --time 4000 --backend metal -o treehouse-camp-day-quick.png target/release/lumenwright render -w the-treehouse-camp --camera 140,118,14 --look-at 56,82,58 --fov 60 --render-distance 12 \ - --time 4000 --backend cuda --mode hq --spp 4096 --max-bounces 8 -o treehouse-camp-day-hq.png + --time 4000 --backend metal --mode hq --spp 4096 --max-bounces 8 -o treehouse-camp-day-hq.png target/release/lumenwright render -w the-treehouse-camp --camera 140,118,14 --look-at 56,82,58 --fov 60 --render-distance 12 \ --time 12800 --backend metal -o treehouse-camp-dusk-quick.png target/release/lumenwright render -w the-treehouse-camp --camera 140,118,14 --look-at 56,82,58 --fov 60 --render-distance 12 \ @@ -36,7 +37,7 @@ target/release/lumenwright render -w the-treehouse-camp --camera 140,118,14 --lo target/release/lumenwright render -w the-treehouse-camp --camera 60,93,22 --look-at 60,80,42 --fov 70 \ --time 4000 --backend metal -o treehouse-bridge-day-quick.png target/release/lumenwright render -w the-treehouse-camp --camera 60,93,22 --look-at 60,80,42 --fov 70 \ - --time 4000 --backend cuda --mode hq --spp 4096 --max-bounces 8 -o treehouse-bridge-day-hq.png + --time 4000 --backend metal --mode hq --spp 4096 --max-bounces 8 -o treehouse-bridge-day-hq.png target/release/lumenwright render -w the-treehouse-camp --camera 60,93,22 --look-at 60,80,42 --fov 70 \ --time 12800 --backend metal -o treehouse-bridge-dusk-quick.png target/release/lumenwright render -w the-treehouse-camp --camera 60,93,22 --look-at 60,80,42 --fov 70 \ @@ -46,15 +47,17 @@ target/release/lumenwright render -w the-treehouse-camp --camera 60,93,22 --look | image | Apple M2 Pro, Metal (end to end) | RTX 4070 Ti SUPER, CUDA (end to end / in the kernel) | |---|---|---| | camp, quick, day / dusk | 0.27 s / 0.27 s | | -| camp, hq, day (4096 spp) / dusk (16384 spp) | | 91 s / 333 s (90 s / 332 s) | +| camp, hq, day (4096 spp) | 449 s | | +| camp, hq, dusk (16384 spp) | | 333 s (332 s) | | bridge, quick, day / dusk | 0.23 s / 0.23 s | | -| bridge, hq, day (4096 spp) / dusk (16384 spp) | | 122 s / 447 s (121 s / 445 s) | +| bridge, hq, day (4096 spp) | 554 s | | +| bridge, hq, dusk (16384 spp) | | 447 s (445 s) | These are the renders' own times at their quality budgets, not a speed comparison (that is [benchmark.md](benchmark.md)). The quick times are the second of two runs, measured at `v0.1.0`; the quick frames are byte-identical at this revision. The 4070 Ti SUPER runs under WSL2 on a 16-thread PC. -Each HQ image rendered twice with the same command on the CUDA backend gave byte-identical -PNGs, as each quick image did on Metal. +Each dusk HQ image rendered twice with the same command on the CUDA backend gave +byte-identical PNGs, as each quick image did on Metal. ### Get the camp diff --git a/docs/renders/daylight-emitters-midnight.png b/docs/renders/daylight-emitters-midnight.png new file mode 100644 index 0000000..7faf2c5 Binary files /dev/null and b/docs/renders/daylight-emitters-midnight.png differ diff --git a/docs/renders/daylight-emitters-noon-before.png b/docs/renders/daylight-emitters-noon-before.png new file mode 100644 index 0000000..ce9ddd7 Binary files /dev/null and b/docs/renders/daylight-emitters-noon-before.png differ diff --git a/docs/renders/daylight-emitters-noon.png b/docs/renders/daylight-emitters-noon.png new file mode 100644 index 0000000..dd0115b Binary files /dev/null and b/docs/renders/daylight-emitters-noon.png differ diff --git a/docs/renders/treehouse-bridge-day-hq.png b/docs/renders/treehouse-bridge-day-hq.png index 30f1531..3198831 100644 Binary files a/docs/renders/treehouse-bridge-day-hq.png and b/docs/renders/treehouse-bridge-day-hq.png differ diff --git a/docs/renders/treehouse-camp-day-hq.png b/docs/renders/treehouse-camp-day-hq.png index c325af4..9f3b5c6 100644 Binary files a/docs/renders/treehouse-camp-day-hq.png and b/docs/renders/treehouse-camp-day-hq.png differ diff --git a/docs/research/chunky-parity.md b/docs/research/chunky-parity.md index 405a1fa..17b7d08 100644 --- a/docs/research/chunky-parity.md +++ b/docs/research/chunky-parity.md @@ -25,7 +25,7 @@ Each statement is marked **[cited]** (the Chunky source or the paper it cites sa | Sky: `chunky/renderer/scene/sky/PreethamSky.java`, `SkyCache.java`, `Sky.java` (`SIMULATED` mode), `math/ColorUtil.java` (`RGBtoHSL`, `RGBfromHSL`) | kernel `preetham_radiance`, `build_sky_cache`, `sky_radiance`; `sky.rs` (`Preetham`, `sky_cache`) | Lumenwright's sky was its own zenith/horizon gradient with a glow around the sun; later the Preetham sky evaluated per direction. | **Ported**: the Preetham sky (Preetham, Shirley and Smits, "A Practical Analytic Model for Daylight", SIGGRAPH 1999, as Chunky implements it at turbidity 2.5) [cited]; below the horizon it holds the horizon value as Chunky's does. Read as Chunky reads it, through its sky cache: a 129 × 129 table of the sky in HSL at resolution 128 (`SkyCache.precalculateSky`), built on the host from the kernel's Preetham function, interpolated bilinearly in HSL and turned back into RGB in the kernel (`calcIncidentLight`, `getColorInterpolated`). `RGBtoHSL` limits each channel to 1 first, so the sky is never brighter than 1 near the sun [cited]; before this port Lumenwright's silhouettes against a setting sun were lighter than Chunky's [measured: see Dusk]. **Lumenwright's own**, because Chunky has no time of day and its Preetham sky keeps the sunset sky at any altitude below the horizon (`PreethamSky.updateSun` clamps the altitude to `[0, π]`): the sky is Chunky's at full light while any part of the sun's disc is above the horizon, then falls linearly to 3% of its light (the night's) at the end of civil twilight, the sun's centre 6° below the horizon, and holds it (`sky.rs`, `twilight`). Chunky's documentation names its Nishita sky as the model that "can also simulate twilight" (`docs/reference/user_interface/chunky/render_controls/sky_and_fog.md` in chunky-dev/docs at `2e94084`); it is not ported (see Not yet at parity). **Not ported**: the Nishita sky, gradients, sky maps, clouds. | | Fog: `chunky/renderer/scene/Fog.java`, `FogLayer.java` | none | — | **Not ported**: the benchmark scene's fog has `uniformDensity` 0, under which `addGroundFog` scales by `exp(0) = 1` and adds `(1 − 1) × …` [cited], so Chunky's benchmark frame has no fog. The distance haze seen in Chunky's frame is its sky light on far faces: after the sky and emitter ports the far hills match it (see the measurements). | | Emitters: `PathTracer.sampleEmitter`, `sampleEmitterFace`; `math/Grid.java`; `chunky/renderer/EmitterSamplingStrategy.java` | kernel `sample_emitters`, `sample_emitter`, `sample_emitter_face`, `emitter_cell`; `lumenwright/src/scene/voxel.rs` (`emitter_list`) | Lumenwright gave each block a power of 600 × (level/15)² shared by its glowing quads, picked lights by power over the whole scene (2169 light quads in the benchmark), sampled texel cells, and combined with BSDF hits by MIS. In the before frame the lantern light floods the camp orange and the stone hills, tens of blocks from any lantern, take an orange cast [measured: the before frame and its crops]. The benchmark holds 148 emitters, 146 lanterns and 2 campfires [measured: the build log]. | **Ported**: the emitter grid (cells of 10 blocks, each listing the emitters in it and its 26 neighbours) and the four strategies (`NONE`, `ONE`, `ONE_BLOCK`, `ALL`; `ONE` is Lumenwright's default, Chunky's default is `NONE`); a uniform point on one face; the face counts when the first surface met towards it is at its distance; `|cos| / max(d², 1) × area × emittance × intensity × colour met`, times π; one emitter of the cell picked uniformly with no division by its probability (as Chunky's); `preventNormalEmitterWithSampling`. Cells start at the grid origin, where Chunky's start at its octree origin, the same point when the scene's lowest height is the world's. | -| Emitter materials: `chunky/chunk/BlockPalette.java` (`getDefaultMaterialProperties`), `Scene.DEFAULT_EMITTER_INTENSITY` | `lumenwright/src/assets/blockinfo.rs` (`emittance`); `request.rs` (`DEFAULT_EMITTER_INTENSITY`) | Lumenwright tied emitted light to the block's light level and drew a separate look (`--emitter-glow`). | **Ported**: one emittance per block material, on every face, entry for entry (keys as Chunky has them, so `magma_block` emits nothing as in Chunky, whose key is `minecraft:magma`); emitter intensity 13. **Not ported**: candle flames and the campfire's fire quads, which Chunky draws as particle entities that are its emitters (Lumenwright draws the client's block models; a campfire's whole model is its emitter); the `light` block, which has no faces here. **Same as Chunky, not a gap**: every texel of an emitting block emits its colour squared, so a lantern's dark blue-grey cap glows blue in Chunky's frame as in Lumenwright's [measured: the dusk crops]. Chunky has no per-texel emitter mapping at `156e2bb` nor at its master of 2026-10-20 (`f10ef3f`); a colour-based mapping (`EmitterMappingType`: reference colours, brightest channel, independent channels) exists only in the unmerged pull request chunky-dev/chunky#1627 [cited]. | +| Emitter materials: `chunky/chunk/BlockPalette.java` (`getDefaultMaterialProperties`), `Scene.DEFAULT_EMITTER_INTENSITY` | `lumenwright/src/assets/blockinfo.rs` (`emittance`); `request.rs` (`DEFAULT_EMITTER_INTENSITY`) | Lumenwright tied emitted light to the block's light level and drew a separate look (`--emitter-glow`). | **Ported**: one emittance per block material, on every face, entry for entry (keys as Chunky has them, so `magma_block` emits nothing as in Chunky, whose key is `minecraft:magma`); emitter intensity 13. **Preset**: Chunky's default scene, a daylight scene, has its emitters off (`DEFAULT_EMITTERS_ENABLED`), so on the client's day plateau the default intensity is 0; off it, and for a stated sky at no tick, 13 ([daylight-emitters.md](daylight-emitters.md)). A scene file that turns Chunky's emitters on is matched with `--emitter-intensity 13`. **Not ported**: candle flames and the campfire's fire quads, which Chunky draws as particle entities that are its emitters (Lumenwright draws the client's block models; a campfire's whole model is its emitter); the `light` block, which has no faces here. **Same as Chunky, not a gap**: every texel of an emitting block emits its colour squared, so a lantern's dark blue-grey cap glows blue in Chunky's frame as in Lumenwright's [measured: the dusk crops]. Chunky has no per-texel emitter mapping at `156e2bb` nor at its master of 2026-10-20 (`f10ef3f`); a colour-based mapping (`EmitterMappingType`: reference colours, brightest channel, independent channels) exists only in the unmerged pull request chunky-dev/chunky#1627 [cited]. | | Tone mapping: `chunky/renderer/postprocessing/GammaCorrectionFilter.java`, `ACESFilmicFilter.java`, `HableToneMappingFilter.java`, `SimplePixelPostProcessingFilter.java` | `lumenwright/src/render/post.rs` | Lumenwright's default was ACES with the sRGB curve. | **Ported**: the gamma filter (`v^(1/2.2)`) as the default; ACES and Hable ending in `v^(1/2.2)`; Chunky's rounding (`ColorUtil.getRGB`). **Not ported**: `TONEMAP1`, the UE4 filter. | | Texture decoding: `math/ColorUtil.java` (`toLinearLut`) | kernel `math::gamma_to_linear` | Lumenwright decoded with a polynomial fit of the sRGB curve. | **Ported**: `c^2.2`, computed in `f32` alone (`exp(2.2 ln c)`) so CPU and CUDA agree; within 4.2e-7 relative of `f64` at every byte value [measured]. | | One-sided block faces: `math/Quad.java` (`intersect`), `math/AABB.java`, `chunky/model/AABBModel.java` | `lumenwright-kernel/src/trace.rs` (`test_quad`), `voxel.rs` (`FLAG_FRONT_ONLY` on block-model quads) | Lumenwright met every quad from both sides, so a ray through a hole in a leaf face met the far face of the same block from behind: two layers of leaf texture per block, which read as a flat solid block [inferred from the code; the leaves' measured difference fell after the port]. | **Ported**: block-model faces are met from the front only; water surfaces stay two-sided (the kernel refracts out of water through them). | diff --git a/docs/research/daylight-emitters.md b/docs/research/daylight-emitters.md new file mode 100644 index 0000000..4751f91 --- /dev/null +++ b/docs/research/daylight-emitters.md @@ -0,0 +1,128 @@ +# Emitters in a clear daytime frame + +A clear daytime HQ frame of a town drew every lantern and torch as a saturated bright +point, so the frame read as dotted with lights in broad day; more samples converge to the +same points. This note records what Chunky, the reference HQ mode is ported from, sets for +a scene's emitters, how Lumenwright's day look departed from it, and the preset it now +takes. Each statement is marked **[cited]** (Chunky's source or documentation says it), +**[measured]** or **[authored]** (Lumenwright's own rule). Chunky is read at revision +`156e2bba2526`, the source of Chunky 2.5.0-SNAPSHOT.474 (GPL-3.0, the licence its ports +here carry; see `ACKNOWLEDGEMENTS.md`). + +## What Chunky sets for a new scene + +| setting | Chunky's default | read at | +|---|---|---| +| emitters | **off** (`DEFAULT_EMITTERS_ENABLED = false`) | `chunky/renderer/scene/Scene.java`, the constant and the field `emittersEnabled` | +| emitter intensity | 13 (`DEFAULT_EMITTER_INTENSITY`), applied only while emitters are on | `Scene.java` | +| emitter sampling | `NONE` | `Scene.java`, the field `emitterSamplingStrategy` | +| sun | intensity 1.25 (`Sun.DEFAULT_INTENSITY`), altitude π/3, azimuth π/2.5 | `chunky/renderer/scene/sky/Sun.java` | +| sky light | 1 (`Sky.DEFAULT_INTENSITY`, for the sky's exposure and its light modifiers) | `chunky/renderer/scene/sky/Sky.java` | +| exposure | 1 (`DEFAULT_EXPOSURE`) | `Scene.java` | +| post-processing | gamma correction, gamma 2.2 (`DEFAULT_POSTPROCESSING_FILTER`, `DEFAULT_GAMMA`) | `Scene.java` | + +[cited, all rows] So Chunky's default scene is a daylight scene, the sun 60° high, with its +emitters off. In `PathTracer.doDiffuseReflection` a surface adds emitted light, and emitter +sampling runs, only under `scene.emittersEnabled`; the emitted light is the texel colour +squared times the material's emittance times `emitterIntensity` [cited]. Chunky's lighting +reference (chunky-dev.github.io, *Render Controls – Lighting*) says emitters, when enabled, +"contribute lighting to the scene" and that the intensity "is a multiplier of the base +emittance value of each material"; it gives no default and no advice on day or night use +[cited]. A Chunky scene with emitters on is the scene's own choice: the benchmark scene of +[benchmark.md](../benchmark.md) states `"emittersEnabled": true` [cited, +`docs/bench/treehouse-camp-bench.json`]. + +## How Lumenwright's day look departed + +HQ mode's day look, Chunky's look on the client's day plateau (ticks 730 to 11270, where the +client's `visual/sky_light_factor` holds its day value), took Chunky's sun 1.25, sky 1, +exposure 1 and gamma tone map, all Chunky's defaults, and drew emitters at intensity 13 +with sampling `ONE` [cited, `request.rs` and `sky.rs` at `c3a8500`]. The emitters were the +one setting of the day look that was not Chunky's default. A lantern texel of linear colour +`c` emitted `13 c²` against a sunlit face's `1.25^2.2 c cos θ ≈ 1.63 c cos θ`, so a bright +texel was some eight times a sunlit face of its colour; at a distance a lantern smaller than +a pixel still carried its pixel [authored arithmetic, from the cited values]. + +The low-light presets were tuned with emitters on: the game look's intensity 137.9628 was +fitted at dusk ([game-sky.md](game-sky.md), *Lanterns*), and a rain or thunder frame is a +stated sky at no tick, Chunky's look at 13 under a sky dimmed to the weather's light. The +clear daytime presets were never tuned for emitters [cited, the record of the fit]. + +## The preset + +[authored] HQ mode's default emitter intensity (`request::default_emitter_intensity`) is +0, `DAY_EMITTER_INTENSITY`, Chunky's emitters off, when the frame stands at a tick on the +client's day plateau: a vanilla sky, or a stated sky at a tick, whose +`visual/sky_light_factor` is at its day value. Everywhere else it is what it was, bit for +bit: each look's own intensity mixed by the day weight, and 13 for a stated sky at no tick. +A stated `--emitter-intensity` (`HqOptions::emitter_intensity`) is used as given. No +algorithm changed; the sun, the sky, the exposure and the tone map are untouched. + +With emitters off a lantern in full daylight is drawn as its texture lit by the sun and the +sky, as a block beside it is; that is also what the game draws there, where the lightmap +clamps block light and sky light together and a sunlit face is already at full brightness +[cited: the 1.21.11 client's lightmap; recorded here only as agreement, not as the rule]. + +### What the preset gives up + +* **An interior on the day plateau is lit by the sky alone.** A lantern in a closed room at + noon lights nothing, as in Chunky's default scene; the game lights such a room by block + light at any hour [authored consequence]. A frame that needs it states + `--emitter-intensity`. +* **The default intensity steps at the day plateau's edges.** Off the plateau the intensity + is the low-light presets' own, unchanged, so at ticks 730 and 11270 it steps between 0 and + the mixed value (about 13 there). The sky has no step; the emitters do. One tick across + 11270 moves the showcase frame (CPU, 8 spp, 96×54) by 4.043/255 with default emitters, + and by 0.057/255 with the emitters held at 13; one tick inside Chunky's look moves it by + 0.040/255 [measured, the test `the_default_sky_follows_the_day_without_a_jump`]. A + continuous preset, `weight × 0 + (1 − weight) × 137.9628`, has no step but changes every + frame off the plateau: at tick 12000 (day weight 0.6096) the intensity would fall from + 61.78 to 53.86, 12.8% lower, and at ticks 13000 and 23000 (weight 0.0749) by 0.76% + [authored arithmetic from the timeline's 1870-tick ramps]. + +## Measurements + +The daylight-emitters fixture (`demo-world --daylight-emitters`): a lit and an unlit +furnace, whose tops are one texture, a lantern, glowstone beside sandstone and a torch on +smooth stone in the open. + +* At noon (`--time 6000`, CPU, 64 spp, 192×108), the largest channel gap between the 7×7 + means of the two furnace tops: 70.14/255 at intensity 13 (the old default; the lit top + (236.7, 239.1, 242.6) against (166.6, 176.9, 189.4)), 2.57/255 by default now [measured, + the test `a_daylight_emitter_is_drawn_as_its_sunlit_neighbour`, tolerance 3/255]. +* At midnight the default frame is the frame at 137.9628 byte for byte, and the floor by the + lit furnace (237.7, 229.6, 215.3) is far above the floor by the unlit one (89.1, 85.4, + 101.0), on the revision before the change and after it [measured, the test + `a_night_emitter_still_lights_its_surroundings`]. + +The comparison frames are in `docs/renders/`: `daylight-emitters-noon-before.png` (emitter +intensity 13) and `daylight-emitters-noon.png` (the default), and +`daylight-emitters-midnight.png`, each drawn from the fixture by + +```sh +target/release/lumenwright demo-world --daylight-emitters yard +target/release/lumenwright render -w yard --mode hq --backend cpu --spp 256 --width 384 --height 216 \ + --camera 0.5,72,7.5 --look-at 0.5,65,-0.5 --fov 55 --time 6000 -o daylight-emitters-noon.png +``` + +with `--emitter-intensity 13` for the frame before, and `--time 18000` for midnight. + +### Eighteen stated skies, before and after + +Delvewright's engine states a frame's sky from a time and a weather: six ticks (1000, 6000, +12000, 13000, 18000, 23000) under a clear sky as a stated sky at that tick by the time of +day, and under rain or thunder as a stated sky at no tick (Chunky's look, its sun dark, its +sky scaled to the weather's light). Each of the 18 was drawn through that engine's frame +path (Metal, Apple M2 Pro) with Lumenwright 0.4.0 and with this change, on two scenes: the +daylight-emitters yard (384×216, 256 spp) and a lantern-lit town seen from the +sea (640×360, 256 spp) [measured; SHA-256 of the RGBA bytes]: + +* The 16 skies off the clear day plateau (clear at 12000, 13000, 18000 and 23000; rain and + thunder at all six ticks) are byte-identical before and after, on both scenes: 32 of 32 + frames. A second run before the change reproduced every one of the 36 frames' bytes. +* The 2 clear skies on the plateau (1000, 6000) differ, on both scenes: the lanterns, + torches and glowstone are drawn as their textures in the sun, and the lit furnace's top as + the unlit one's. +* Rain and thunder at ticks 1000 and 6000 keep emitters at 13 under a daytime sky dimmed to + the weather's light; there a lantern is still a bright point and the lit furnace's top + clips to white. They are left as they were tuned. diff --git a/docs/research/game-sky.md b/docs/research/game-sky.md index 7103a0d..69d16be 100644 --- a/docs/research/game-sky.md +++ b/docs/research/game-sky.md @@ -79,7 +79,10 @@ display values and the intensities (13, 137.9628) linearly. On the day plateau t `--sky chunky`'s byte for byte; on the night plateau `--sky game`'s [measured, the test `the_default_sky_follows_the_day_without_a_jump`]. One tick across either edge of the ramp moves the camp frame by 0.067/255 (Metal, 64 spp, at `4d1f6bd`), as one tick inside Chunky's look does -(0.059); a hard switch at the plateau's edge moves it by 28.7 [measured]. +(0.059); a hard switch at the plateau's edge moves it by 28.7 [measured]. The default emitter +intensity is the exception: on the day plateau it is 0, Chunky's emitters off, and off it the +mix above, so it steps at the day plateau's edges; the test holds the sky's continuity there +with the emitters at one intensity ([daylight-emitters.md](daylight-emitters.md)). ## Lanterns diff --git a/docs/testing.md b/docs/testing.md index a7b087e..5b707ad 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -25,7 +25,8 @@ cargo test --workspace --release holders empty), the entity gallery in both modes (every body, layer and variant counted and drawn at its spot against the empty platform, the colours its layers and variants show, a woolly sheep larger than a sheared one, and at midnight the glowing eyes and - bodies bright beside a dark one), that HQ mode's water plane carries an ocean's sea to + bodies bright beside a dark one), that at noon a lit furnace's top is drawn within 3/255 of an + unlit one's beside it while at midnight it lights the floor, that HQ mode's water plane carries an ocean's sea to the horizon without the loaded sea's edge, that every item definition in the jar resolves, HQ sanity, CPU-vs-GPU parity in both modes (showcase, block-entity gallery and item gallery; in HQ mode also the entity gallery and the ocean under the water plane) and CPU-vs-CUDA