diff --git a/crates/examples/src/animation.rs b/crates/examples/src/animation.rs new file mode 100644 index 00000000..4048d7a6 --- /dev/null +++ b/crates/examples/src/animation.rs @@ -0,0 +1,196 @@ +//! Animation examples: playback, skinning and morph targets. + +use crate::{cwd_to_manual_assets_dir, workspace_dir}; + +#[tokio::test] +async fn manual_animation() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("animation").unwrap(); + + // ANCHOR: setup + use renderling::{ + camera, + context::Context, + glam::{Vec3, Vec4}, + stage::Stage, + }; + + let ctx = Context::headless(512, 512).await; + let stage: Stage = ctx + .new_stage() + .with_lighting(false) + .with_bloom(false) + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let projection = camera::perspective(512.0, 512.0); + let view = camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + + // Load a GLTF file containing an animation. + let doc = stage + .load_gltf_document_from_path(workspace_dir().join("gltf/animated_triangle.gltf")) + .unwrap(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_frame_0 = frame.read_image().await.unwrap(); + img_frame_0.save("animation/frame-0.png").unwrap(); + frame.present(); + // ANCHOR_END: setup + + // ANCHOR: animator + use renderling::gltf::Animator; + + // Collect every node in the scene, and build an `Animator` for the + // document's first animation clip. + let nodes = doc + .recursive_nodes_in_scene(doc.default_scene.unwrap_or_default()) + .collect::>(); + let mut animator = Animator::new(nodes, doc.animations.first().unwrap().clone()); + // ANCHOR_END: animator + + // ANCHOR: progress + // Advance the animation and render frames. In an application this + // would happen every frame, with `dt` the time since the last one. + let dt = 1.0 / 8.0; + let mut img_frame_2 = None; + let mut img_frame_5 = None; + for i in 1..=8 { + animator.progress(dt).unwrap(); + + if i == 2 || i == 5 { + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame.read_image().await.unwrap(); + img.save(format!("animation/frame-{i}.png")).unwrap(); + if i == 2 { + img_frame_2 = Some(img); + } else { + img_frame_5 = Some(img); + } + frame.present(); + } + } + let img_frame_2 = img_frame_2.unwrap(); + let img_frame_5 = img_frame_5.unwrap(); + // ANCHOR_END: progress + + // The animation should have moved the triangle. + let pixel_diff = |a: &[u8], b: &[u8]| { + a.chunks_exact(4) + .zip(b.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count() + }; + for (name, before, after) in [ + ("frame 0 to 2", img_frame_0.as_raw(), img_frame_2.as_raw()), + ("frame 2 to 5", img_frame_2.as_raw(), img_frame_5.as_raw()), + ] { + let changed = pixel_diff(before, after); + println!("pixels changed from {name}: {changed}"); + assert!( + changed > 500, + "the animation did not change the rendering between {name} ({changed} pixels changed)" + ); + } +} + +#[tokio::test] +async fn manual_morph_targets() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("animation").unwrap(); + + // ANCHOR: morph + use renderling::{ + camera, + context::Context, + geometry::{MorphTarget, Vertex}, + glam::{Vec3, Vec4}, + stage::Stage, + }; + + let ctx = Context::headless(512, 512).await; + let stage: Stage = ctx + .new_stage() + .with_lighting(false) + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let projection = camera::perspective(512.0, 512.0); + let view = camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + + // A quad. + let quad = stage + .new_primitive() + .with_vertices(stage.new_vertices([ + Vertex::default().with_position([-1.0, -1.0, 0.0]), + Vertex::default().with_position([1.0, -1.0, 0.0]), + Vertex::default().with_position([1.0, 1.0, 0.0]), + Vertex::default().with_position([-1.0, 1.0, 0.0]), + ])) + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(0.95, 0.85, 0.1, 1.0)) + .with_has_lighting(false), + ); + + // One morph target per vertex, describing where each vertex moves at + // full weight. Here the quad twists: the left edge comes toward the + // camera, the right edge moves away. + let twist = stage.new_morph_targets([vec![ + MorphTarget { + position: Vec3::new(-1.0, -1.0, 1.0), + ..Default::default() + }, + MorphTarget { + position: Vec3::new(1.0, -1.0, -1.0), + ..Default::default() + }, + MorphTarget { + position: Vec3::new(1.0, 1.0, -1.0), + ..Default::default() + }, + MorphTarget { + position: Vec3::new(-1.0, 1.0, 1.0), + ..Default::default() + }, + ]]); + let weights = stage.new_morph_target_weights([0.0f32]); + quad.set_morph_targets(twist, weights.clone()); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_flat = frame.read_image().await.unwrap(); + img_flat.save("animation/morph-0.png").unwrap(); + frame.present(); + + // Morph weights can be changed at runtime. + weights.set_item(0, 1.0); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_twisted = frame.read_image().await.unwrap(); + img_twisted.save("animation/morph-1.png").unwrap(); + frame.present(); + // ANCHOR_END: morph + + let pixel_diff = |a: &[u8], b: &[u8]| { + a.chunks_exact(4) + .zip(b.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count() + }; + let changed = pixel_diff(img_flat.as_raw(), img_twisted.as_raw()); + println!("pixels changed by morphing: {changed}"); + assert!( + changed > 500, + "changing the morph weight did not change the rendering ({changed} pixels changed)" + ); +} diff --git a/crates/examples/src/debug.rs b/crates/examples/src/debug.rs new file mode 100644 index 00000000..7b432a3f --- /dev/null +++ b/crates/examples/src/debug.rs @@ -0,0 +1,157 @@ +//! Debug mode examples. + +use crate::{cwd_to_manual_assets_dir, workspace_dir}; + +#[tokio::test] +async fn manual_debug() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("debug").unwrap(); + + // ANCHOR: setup + // We'll debug the shadow mapping scene from the previous chapters. + use renderling::{ + camera::Camera, + context::Context, + geometry::Vertex, + glam::{Mat4, UVec2, Vec3, Vec4}, + gltf::GltfDocument, + light::{AnalyticalLight, DirectionalLight, Lux}, + primitive::Primitive, + stage::Stage, + types::GpuOnlyArray, + }; + + let ctx = Context::headless(512, 512).await; + let stage: Stage = ctx + .new_stage() + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let _camera: Camera = { + let aspect = 1.0; + let fovy = core::f32::consts::PI / 4.0; + let znear = 0.1; + let zfar = 10.0; + let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar); + let eye = Vec3::new(0.5, 0.5, 0.8); + let target = Vec3::new(0.0, 0.3, 0.0); + let up = Vec3::Y; + let view = Mat4::look_at_rh(eye, target, up); + + stage + .new_camera() + .with_projection_and_view(projection, view) + }; + + let model: GltfDocument = stage + .load_gltf_document_from_path(workspace_dir().join("gltf/marble_bust_1k.glb")) + .unwrap() + .into_gpu_only(); + + let y = -0.01; + let s = 2.0; + let floor: Primitive = stage + .new_primitive() + .with_vertices( + stage.new_vertices([ + Vertex::default() + .with_position([s, y, s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([s, y, s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, s]) + .with_normal(Vec3::Y), + ]), + ) + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(0.85, 0.85, 0.85, 1.0)), + ); + + let sun: AnalyticalLight = stage + .new_directional_light() + .with_direction(Vec3::new(-0.4, -1.0, -0.6).normalize()) + .with_color(Vec4::ONE) + .with_intensity(Lux::OUTDOOR_OVERCAST_HIGH); + + let shadow_map = stage + .new_shadow_map(&sun, UVec2::splat(1024), 0.1, 10.0) + .unwrap(); + shadow_map + .update( + &stage, + model.renderlets_iter().chain(std::iter::once(&floor)), + ) + .unwrap(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_none = frame.read_image().await.unwrap(); + img_none.save("debug/none.png").unwrap(); + frame.present(); + // ANCHOR_END: setup + + // ANCHOR: channel + use renderling::pbr::debug::DebugChannel; + + // Display the world-space normals, after normal mapping. + stage.set_debug_mode(DebugChannel::Normals); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_normals = frame.read_image().await.unwrap(); + img_normals.save("debug/normals.png").unwrap(); + frame.present(); + + // Display the first set of UV coordinates. + stage.set_debug_mode(DebugChannel::UvCoords0); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_uv = frame.read_image().await.unwrap(); + img_uv.save("debug/uv-coords.png").unwrap(); + frame.present(); + + // Display just the albedo color. + stage.set_debug_mode(DebugChannel::Albedo); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_albedo = frame.read_image().await.unwrap(); + img_albedo.save("debug/albedo.png").unwrap(); + frame.present(); + // ANCHOR_END: channel + + // Each step should have changed the rendering. + let pixel_diff = |a: &[u8], b: &[u8]| { + a.chunks_exact(4) + .zip(b.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count() + }; + let steps = [ + ("normals channel", img_none.as_raw(), img_normals.as_raw()), + ("uv channel", img_none.as_raw(), img_uv.as_raw()), + ("albedo channel", img_none.as_raw(), img_albedo.as_raw()), + ]; + for (name, before, after) in steps { + let changed = pixel_diff(before, after); + println!("pixels changed by {name}: {changed}"); + assert!( + changed > 500, + "changing {name} did not change the rendering ({changed} pixels changed)" + ); + } +} diff --git a/crates/examples/src/lib.rs b/crates/examples/src/lib.rs index 07cf1f0c..f41070f1 100644 --- a/crates/examples/src/lib.rs +++ b/crates/examples/src/lib.rs @@ -18,6 +18,27 @@ mod skybox; #[cfg(test)] mod lighting; +#[cfg(test)] +mod shadow; + +#[cfg(test)] +mod material; + +#[cfg(test)] +mod postprocessing; + +#[cfg(test)] +mod debug; + +#[cfg(test)] +mod scene; + +#[cfg(test)] +mod animation; + +#[cfg(test)] +mod performance; + pub fn cwd_to_manual_assets_dir() -> std::path::PathBuf { let current_dir = std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")).join("manual/src/assets"); diff --git a/crates/examples/src/material.rs b/crates/examples/src/material.rs new file mode 100644 index 00000000..6ae5dd04 --- /dev/null +++ b/crates/examples/src/material.rs @@ -0,0 +1,302 @@ +//! Materials and textures examples. + +use crate::{cwd_to_manual_assets_dir, workspace_dir}; +use renderling::{ + geometry::Vertex, + glam::{Vec2, Vec3}, +}; + +/// A unit cube with UV coordinates, centered on the origin. +fn uv_unit_cube() -> Vec { + let p: [Vec3; 8] = renderling::math::UNIT_POINTS; + let tl = Vec2::new(0.0, 0.0); + let tr = Vec2::new(1.0, 0.0); + let bl = Vec2::new(0.0, 1.0); + let br = Vec2::new(1.0, 1.0); + + vec![ + // top + Vertex::default().with_position(p[0]).with_uv0(bl), + Vertex::default().with_position(p[2]).with_uv0(tr), + Vertex::default().with_position(p[1]).with_uv0(tl), + Vertex::default().with_position(p[0]).with_uv0(bl), + Vertex::default().with_position(p[3]).with_uv0(br), + Vertex::default().with_position(p[2]).with_uv0(tr), + // bottom + Vertex::default().with_position(p[4]).with_uv0(bl), + Vertex::default().with_position(p[6]).with_uv0(tr), + Vertex::default().with_position(p[5]).with_uv0(tl), + Vertex::default().with_position(p[4]).with_uv0(bl), + Vertex::default().with_position(p[7]).with_uv0(br), + Vertex::default().with_position(p[6]).with_uv0(tr), + // left + Vertex::default().with_position(p[7]).with_uv0(bl), + Vertex::default().with_position(p[0]).with_uv0(tr), + Vertex::default().with_position(p[1]).with_uv0(tl), + Vertex::default().with_position(p[7]).with_uv0(bl), + Vertex::default().with_position(p[4]).with_uv0(br), + Vertex::default().with_position(p[0]).with_uv0(tr), + // right + Vertex::default().with_position(p[5]).with_uv0(bl), + Vertex::default().with_position(p[2]).with_uv0(tr), + Vertex::default().with_position(p[3]).with_uv0(tl), + Vertex::default().with_position(p[5]).with_uv0(bl), + Vertex::default().with_position(p[6]).with_uv0(br), + Vertex::default().with_position(p[2]).with_uv0(tr), + // front + Vertex::default().with_position(p[4]).with_uv0(bl), + Vertex::default().with_position(p[3]).with_uv0(tr), + Vertex::default().with_position(p[0]).with_uv0(tl), + Vertex::default().with_position(p[4]).with_uv0(bl), + Vertex::default().with_position(p[5]).with_uv0(br), + Vertex::default().with_position(p[3]).with_uv0(tr), + ] +} + +/// Returns the number of pixels that differ between two raw RGBA images. +fn pixel_diff(a: &[u8], b: &[u8]) -> usize { + a.chunks_exact(4) + .zip(b.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count() +} + +#[tokio::test] +async fn manual_materials() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("material").unwrap(); + + // ANCHOR: setup + use renderling::{ + camera::Camera, + context::Context, + glam::{Mat4, Vec3, Vec4}, + primitive::Primitive, + stage::Stage, + }; + + let ctx = Context::headless(512, 512).await; + let stage: Stage = ctx + .new_stage() + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let _camera: Camera = { + let aspect = 1.0; + let fovy = core::f32::consts::PI / 4.0; + let znear = 0.1; + let zfar = 100.0; + let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar); + let eye = Vec3::new(0.0, 0.5, 4.2); + let target = Vec3::ZERO; + let up = Vec3::Y; + let view = Mat4::look_at_rh(eye, target, up); + + stage + .new_camera() + .with_projection_and_view(projection, view) + }; + + // Two cubes sharing the same vertices, each with its own transform. + let geometry = stage.new_vertices(uv_unit_cube()); + let offset = 0.9; + let left: Primitive = stage + .new_primitive() + .with_vertices(&geometry) + .with_transform( + stage + .new_transform() + .with_translation(Vec3::new(-offset, 0.0, 0.0)), + ); + let right: Primitive = stage + .new_primitive() + .with_vertices(&geometry) + .with_transform( + stage + .new_transform() + .with_translation(Vec3::new(offset, 0.0, 0.0)), + ); + // ANCHOR_END: setup + + // ANCHOR: albedo_texture + use renderling::atlas::AtlasImage; + + // Load two images and stage them in the stage's texture atlas. + // + // `set_images` returns one `AtlasTexture` handle per image. Note that + // calling it again repacks the atlas and invalidates any previous + // handles, so stage all the images you need up front. + let sandstone = AtlasImage::from_path(workspace_dir().join("img/sandstone.png")).unwrap(); + let dirt = AtlasImage::from_path(workspace_dir().join("img/dirt.jpg")).unwrap(); + let entries = stage.set_images([sandstone, dirt]).unwrap(); + // ANCHOR_END: albedo_texture + + // ANCHOR: albedo_factor + use renderling::color::css_srgb_color_to_linear; + + // Create an unlit material with a teal albedo factor. + let material = stage + .new_material() + .with_albedo_factor(css_srgb_color_to_linear(0, 128, 128)) + .with_has_lighting(false); + + // Both primitives share the same material. + left.set_material(&material); + right.set_material(&material); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_teal = frame.read_image().await.unwrap(); + img_teal.save("material/albedo-factor.png").unwrap(); + frame.present(); + // ANCHOR_END: albedo_factor + + // ANCHOR: use_texture + // The albedo factor multiplies the albedo texture, so reset it to + // white and use the first image as the albedo texture. + material + .set_albedo_factor(Vec4::ONE) + .set_albedo_texture(&entries[0]); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_sandstone = frame.read_image().await.unwrap(); + img_sandstone.save("material/albedo-texture.png").unwrap(); + frame.present(); + // ANCHOR_END: use_texture + + // ANCHOR: update + // Updating a material at runtime is just as easy. Here we switch the + // albedo texture of both cubes to the second image with a single call. + material.set_albedo_texture(&entries[1]); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_dirt = frame.read_image().await.unwrap(); + img_dirt.save("material/albedo-updated.png").unwrap(); + frame.present(); + // ANCHOR_END: update + + // ANCHOR: pbr + use renderling::light::{AnalyticalLight, DirectionalLight, Lux}; + + // Turn on lighting and give the scene an environment so the material + // has something to reflect. (See the skybox and image-based lighting + // chapters.) + let skybox = stage + .new_skybox_from_path(workspace_dir().join("img/hdr/helipad.hdr")) + .unwrap(); + stage.use_skybox(&skybox); + let ibl = stage.new_ibl(&skybox); + stage.use_ibl(&ibl); + + let _sun: AnalyticalLight = stage + .new_directional_light() + .with_direction(Vec3::new(-0.4, -1.0, -0.5).normalize()) + .with_color(Vec4::ONE) + .with_intensity(Lux::OUTDOOR_OVERCAST_HIGH); + + // Give the material PBR parameters: make our sandstone cubes metallic + // with low roughness, like polished gold. + material + .set_has_lighting(true) + .set_albedo_texture(&entries[0]) + .set_metallic_factor(1.0) + .set_roughness_factor(0.2); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_pbr = frame.read_image().await.unwrap(); + img_pbr.save("material/pbr.png").unwrap(); + frame.present(); + // ANCHOR_END: pbr + + // ANCHOR: roughness + // Increasing the roughness blurs the reflections, making the surface + // appear matte. + material.set_roughness_factor(1.0); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_rough = frame.read_image().await.unwrap(); + img_rough.save("material/pbr-rough.png").unwrap(); + frame.present(); + // ANCHOR_END: roughness + + // ANCHOR: emissive + // Emissive color is added directly to the final color, regardless of + // lighting. Combined with bloom it makes objects glow. + material + .set_emissive_factor(Vec3::new(1.0, 0.25, 0.1)) + .set_emissive_strength_multiplier(2.0); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_emissive = frame.read_image().await.unwrap(); + img_emissive.save("material/emissive.png").unwrap(); + frame.present(); + // ANCHOR_END: emissive + + // Each step should have changed the rendering. + let steps = [ + ("albedo factor", img_teal.as_raw(), img_sandstone.as_raw()), + ("albedo texture", img_sandstone.as_raw(), img_dirt.as_raw()), + ("texture update", img_dirt.as_raw(), img_pbr.as_raw()), + ("metallic", img_pbr.as_raw(), img_rough.as_raw()), + ("roughness", img_rough.as_raw(), img_emissive.as_raw()), + ]; + for (name, before, after) in steps { + let changed = pixel_diff(before, after); + println!("pixels changed by {name}: {changed}"); + assert!( + changed > 500, + "changing {name} did not change the rendering ({changed} pixels changed)" + ); + } + + // The textured render should contain far more distinct colors than a + // flat, untextured render. + let unique_colors = |raw: &[u8]| { + raw.chunks_exact(4) + .map(|p| [p[0], p[1], p[2], p[3]]) + .collect::>() + .len() + }; + let teal_colors = unique_colors(img_teal.as_raw()); + let textured_colors = unique_colors(img_sandstone.as_raw()); + println!("unique colors: teal={teal_colors}, textured={textured_colors}"); + assert!( + textured_colors > teal_colors + 50, + "expected the albedo texture to render ({textured_colors} unique colors)" + ); +} + +#[tokio::test] +async fn manual_materials_normal_map() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("material").unwrap(); + + // ANCHOR: normal_map + use renderling::{context::Context, glam::Vec4, stage::Stage}; + + // GLTF files can carry textures of their own, including normal maps. + // This brick sphere has lighting and a normal map baked in, and the + // loader wires them into the stage and material for us. + let ctx = Context::headless(800, 450).await; + let stage: Stage = ctx + .new_stage() + .with_lighting(true) + .with_background_color(Vec4::new(0.01, 0.01, 0.01, 1.0)); + + let _doc = stage + .load_gltf_document_from_path(workspace_dir().join("gltf/normal_mapping_brick_sphere.glb")) + .unwrap(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame.read_image().await.unwrap(); + img.save("material/normal-map.png").unwrap(); + frame.present(); + // ANCHOR_END: normal_map +} diff --git a/crates/examples/src/performance.rs b/crates/examples/src/performance.rs new file mode 100644 index 00000000..89ff78bc --- /dev/null +++ b/crates/examples/src/performance.rs @@ -0,0 +1,254 @@ +//! Performance examples: culling, light tiling and MSAA. + +use crate::cwd_to_manual_assets_dir; +use renderling::{geometry::Vertex, glam::Vec3}; + +/// A unit cube of vertices, centered on the origin. +fn unit_cube() -> Vec { + let points: [Vec3; 8] = renderling::math::UNIT_POINTS; + renderling::math::UNIT_INDICES + .iter() + .map(|i| Vertex::default().with_position(points[*i])) + .collect() +} + +/// A horizontal quad at the given height, with the given half extent. +fn floor(y: f32, s: f32) -> Vec { + [ + Vertex::default() + .with_position([s, y, s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([s, y, s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, s]) + .with_normal(Vec3::Y), + ] + .into() +} + +#[tokio::test] +async fn manual_culling() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("performance").unwrap(); + + // ANCHOR: culling + use renderling::{ + camera, + context::Context, + glam::{Vec3, Vec4}, + stage::Stage, + }; + + let ctx = Context::headless(512, 512).await; + let stage: Stage = ctx + .new_stage() + .with_lighting(false) + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let projection = camera::perspective(512.0, 512.0); + let view = camera::look_at(Vec3::new(0.0, 2.5, 5.5), Vec3::ZERO, Vec3::Y); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + + // A grid of cubes, some of which are outside the camera's view. + let geometry = stage.new_vertices(unit_cube()); + for x in 0..7 { + for z in 0..7 { + stage + .new_primitive() + .with_vertices(&geometry) + .with_transform( + stage + .new_transform() + .with_translation(Vec3::new( + x as f32 * 1.2 - 3.6, + 0.0, + z as f32 * 1.2 - 3.6, + )) + .with_scale(Vec3::splat(0.25)), + ) + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(0.4, 0.7, 0.9, 1.0)) + .with_has_lighting(false), + ); + } + } + + // Frustum culling is on by default. Objects outside the camera's + // view are skipped by the GPU. + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_culled = frame.read_image().await.unwrap(); + img_culled.save("performance/culling.png").unwrap(); + frame.present(); + + // Turning frustum culling off must not change the picture - the + // same objects are visible either way, culling only skips work. + stage.set_use_frustum_culling(false); + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_unculled = frame.read_image().await.unwrap(); + frame.present(); + // ANCHOR_END: culling + + let pixel_diff = |a: &[u8], b: &[u8]| { + a.chunks_exact(4) + .zip(b.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count() + }; + let changed = pixel_diff(img_culled.as_raw(), img_unculled.as_raw()); + println!("pixels changed by disabling frustum culling: {changed}"); + assert!( + changed == 0, + "frustum culling changed the rendered output ({changed} pixels changed)" + ); + + // ANCHOR: occlusion + // Occlusion culling also skips objects hidden behind other objects. + // It is off by default and still a feature in development. + stage.set_use_frustum_culling(true); + stage.set_use_occlusion_culling(true); + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + frame + .read_image() + .await + .unwrap() + .save("performance/occlusion.png") + .unwrap(); + frame.present(); + // ANCHOR_END: occlusion + + // ANCHOR: msaa + // Multisample anti-aliasing smooths the edges of geometry. It is set + // with a sample count - here we go from 1 (no MSAA) to 4. + stage.set_msaa_sample_count(4); + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_msaa = frame.read_image().await.unwrap(); + img_msaa.save("performance/msaa-4.png").unwrap(); + frame.present(); + // ANCHOR_END: msaa + + let changed = pixel_diff(img_culled.as_raw(), img_msaa.as_raw()); + println!("pixels changed by msaa: {changed}"); + assert!( + changed > 500, + "enabling msaa did not change the rendering ({changed} pixels changed)" + ); +} + +#[tokio::test] +async fn manual_light_tiling() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("performance").unwrap(); + + // ANCHOR: many_lights + use renderling::{ + camera, + color::css_srgb_color_to_linear, + context::Context, + glam::{Vec3, Vec4}, + light::{AnalyticalLight, Candela, PointLight}, + stage::Stage, + }; + + let ctx = Context::headless(512, 512).await; + let stage: Stage = ctx + .new_stage() + .with_bloom(false) + .with_background_color(Vec4::new(0.02, 0.02, 0.02, 1.0)); + + let projection = camera::perspective(512.0, 512.0); + let view = camera::look_at(Vec3::new(4.0, 4.0, 8.0), Vec3::ZERO, Vec3::Y); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + + // A ground plane to receive the light. + stage + .new_primitive() + .with_vertices(stage.new_vertices(floor(0.0, 3.0))) + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(0.85, 0.85, 0.85, 1.0)), + ); + + // A grid of colorful point lights. Without light tiling the shader + // iterates every light for every fragment. + let mut lights: Vec> = vec![]; + for i in 0..6 { + for j in 0..6 { + let light = stage + .new_point_light() + .with_position(Vec3::new(i as f32 * 1.2 - 3.0, 2.5, j as f32 * 1.2 - 3.0)) + .with_color(css_srgb_color_to_linear( + (60 + i * 35) as u8, + (60 + j * 35) as u8, + 128, + )) + .with_intensity(Candela(20.0)); + lights.push(light); + } + } + println!("lights: {}", lights.len()); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_untiled = frame.read_image().await.unwrap(); + img_untiled.save("performance/no-tiling.png").unwrap(); + frame.present(); + // ANCHOR_END: many_lights + + // ANCHOR: tiling + use renderling::light::LightTilingConfig; + + // Light tiling bins lights into screen tiles so each fragment only + // considers the lights near it. Run it before rendering - in an + // application, every frame. + let tiling = stage.new_light_tiling(LightTilingConfig::default()); + tiling.run(&stage); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_tiled = frame.read_image().await.unwrap(); + img_tiled.save("performance/tiling.png").unwrap(); + frame.present(); + // ANCHOR_END: tiling + + // Tiling should produce nearly the same picture for much less work. + let pixel_diff = |a: &[u8], b: &[u8]| { + a.chunks_exact(4) + .zip(b.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count() + }; + let changed = pixel_diff(img_untiled.as_raw(), img_tiled.as_raw()); + let total = img_untiled.as_raw().len() / 4; + println!( + "pixels changed by light tiling: {changed} of {total} ({:.1}%)", + 100.0 * changed as f32 / total as f32 + ); + assert!( + changed * 10 < total, + "light tiling changed the rendering too much ({changed} of {total} pixels changed)" + ); +} diff --git a/crates/examples/src/postprocessing.rs b/crates/examples/src/postprocessing.rs new file mode 100644 index 00000000..026b0380 --- /dev/null +++ b/crates/examples/src/postprocessing.rs @@ -0,0 +1,124 @@ +//! Post-processing examples: bloom and tonemapping. + +use crate::{cwd_to_manual_assets_dir, workspace_dir}; + +#[tokio::test] +async fn manual_postprocessing() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("postprocessing").unwrap(); + + // ANCHOR: setup + use renderling::{ + camera, + context::Context, + glam::{Vec3, Vec4}, + stage::Stage, + }; + + let width = 512; + let height = 256; + let ctx = Context::headless(width, height).await; + // Bloom is on by default - we turn it off so we can show the + // difference. + let stage: Stage = ctx + .new_stage() + .with_bloom(false) + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let projection = camera::perspective(width as f32, height as f32); + let view = camera::look_at(Vec3::new(0.0, 2.0, 18.0), Vec3::ZERO, Vec3::Y); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + + // A night sky and an emissive-strength test model full of glowing + // objects, so there are bright areas for the bloom effect to pick up. + let skybox = stage + .new_skybox_from_path(workspace_dir().join("img/hdr/night.hdr")) + .unwrap(); + stage.use_skybox(&skybox); + let ibl = stage.new_ibl(&skybox); + stage.use_ibl(&ibl); + + let _model = stage + .load_gltf_document_from_path(workspace_dir().join("gltf/EmissiveStrengthTest.glb")) + .unwrap(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_no_bloom = frame.read_image().await.unwrap(); + img_no_bloom.save("postprocessing/bloom-none.png").unwrap(); + frame.present(); + // ANCHOR_END: setup + + // ANCHOR: bloom + // Turn bloom on, either at stage creation with `.with_bloom(true)` or + // at runtime. + stage.set_has_bloom(true); + // How much of the blurred bright areas to mix back into the image. + stage.set_bloom_mix_strength(0.1); + // The radius of the blur, in texels. + stage.set_bloom_filter_radius(2.0); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_bloom = frame.read_image().await.unwrap(); + img_bloom.save("postprocessing/bloom.png").unwrap(); + frame.present(); + // ANCHOR_END: bloom + + // ANCHOR: exposure + // The stage renders in high dynamic range (HDR). Before the image is + // shown it is multiplied by the exposure and run through the tone + // mapping algorithm, which maps the HDR values into the display's + // range. + let mut config = stage.tonemapping().get_tonemapping_config(); + config.exposure = 3.0; + stage.tonemapping().set_tonemapping_config(config); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_exposure = frame.read_image().await.unwrap(); + img_exposure.save("postprocessing/exposure.png").unwrap(); + frame.present(); + // ANCHOR_END: exposure + + // ANCHOR: tonemap + use renderling::tonemapping::Tonemap; + + // By default no tone mapping algorithm is used (`Tonemap::NONE`), and + // HDR values outside the display range simply clip. A filmic operator + // like ACES compresses the highlights instead. + config.tonemap = Tonemap::ACES_HILL_EXPOSURE_BOOST; + config.exposure = 1.0; + stage.tonemapping().set_tonemapping_config(config); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_aces = frame.read_image().await.unwrap(); + img_aces.save("postprocessing/tonemap-aces.png").unwrap(); + frame.present(); + // ANCHOR_END: tonemap + + // Each step should have changed the rendering. + let pixel_diff = |a: &[u8], b: &[u8]| { + a.chunks_exact(4) + .zip(b.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count() + }; + let steps = [ + ("bloom", img_no_bloom.as_raw(), img_bloom.as_raw()), + ("exposure", img_bloom.as_raw(), img_exposure.as_raw()), + ("tone mapping", img_exposure.as_raw(), img_aces.as_raw()), + ]; + for (name, before, after) in steps { + let changed = pixel_diff(before, after); + println!("pixels changed by {name}: {changed}"); + assert!( + changed > 500, + "changing {name} did not change the rendering ({changed} pixels changed)" + ); + } +} diff --git a/crates/examples/src/scene.rs b/crates/examples/src/scene.rs new file mode 100644 index 00000000..4c477f75 --- /dev/null +++ b/crates/examples/src/scene.rs @@ -0,0 +1,132 @@ +//! Scene hierarchy examples. + +use crate::cwd_to_manual_assets_dir; +use renderling::{geometry::Vertex, glam::Vec3}; + +/// A unit cube of vertices, centered on the origin. +fn unit_cube() -> Vec { + let points: [Vec3; 8] = renderling::math::UNIT_POINTS; + renderling::math::UNIT_INDICES + .iter() + .map(|i| Vertex::default().with_position(points[*i])) + .collect() +} + +#[tokio::test] +async fn manual_scene() { + let _ = env_logger::builder().try_init(); + cwd_to_manual_assets_dir(); + std::fs::create_dir_all("scene").unwrap(); + + // ANCHOR: setup + use renderling::{ + camera::Camera, + context::Context, + glam::{Mat4, Quat, Vec3, Vec4}, + stage::Stage, + }; + + let ctx = Context::headless(512, 512).await; + let stage: Stage = ctx + .new_stage() + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let _camera: Camera = { + let aspect = 1.0; + let fovy = core::f32::consts::PI / 4.0; + let projection = Mat4::perspective_rh(fovy, aspect, 0.1, 100.0); + let eye = Vec3::new(0.0, 2.0, 6.0); + let target = Vec3::ZERO; + let up = Vec3::Y; + let view = Mat4::look_at_rh(eye, target, up); + + stage + .new_camera() + .with_projection_and_view(projection, view) + }; + + // Build a three-level hierarchy: root -> child -> grandchild. + // + // Each node has a _local_ transform, relative to its parent. + let root = stage.new_nested_transform(); + let child = stage + .new_nested_transform() + .with_local_translation(Vec3::X * 1.2); + let grandchild = stage + .new_nested_transform() + .with_local_translation(Vec3::X * 1.2); + root.add_child(&child); + child.add_child(&grandchild); + + // Attach a colored cube to each node. Primitives take the node's + // _global_ transform, which is composed from the whole hierarchy. + let cube = |color: Vec4| { + stage + .new_primitive() + .with_vertices(stage.new_vertices(unit_cube())) + .with_material( + stage + .new_material() + .with_albedo_factor(color) + .with_has_lighting(false), + ) + }; + cube(Vec4::new(0.6, 0.6, 0.6, 1.0)).with_transform(&root); + cube(Vec4::new(0.0, 0.85, 0.85, 1.0)).with_transform(&child); + cube(Vec4::new(0.95, 0.85, 0.1, 1.0)).with_transform(&grandchild); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_before = frame.read_image().await.unwrap(); + img_before.save("scene/hierarchy.png").unwrap(); + frame.present(); + // ANCHOR_END: setup + + // ANCHOR: parent + // Rotate the root - the entire hierarchy rotates with it, because + // the child and grandchild are positioned relative to the root. + root.set_local_rotation(Quat::from_axis_angle(Vec3::Y, core::f32::consts::FRAC_PI_4)); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_parent = frame.read_image().await.unwrap(); + img_parent.save("scene/moved-parent.png").unwrap(); + frame.present(); + // ANCHOR_END: parent + + // ANCHOR: child + // Move the child - the root stays put, and the grandchild follows the + // child. + child.set_local_translation(Vec3::new(1.2, 1.2, 0.0)); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_child = frame.read_image().await.unwrap(); + img_child.save("scene/moved-child.png").unwrap(); + frame.present(); + // ANCHOR_END: child + + // Each step should have changed the rendering. + let pixel_diff = |a: &[u8], b: &[u8]| { + a.chunks_exact(4) + .zip(b.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count() + }; + let steps = [ + ( + "rotating the parent", + img_before.as_raw(), + img_parent.as_raw(), + ), + ("moving the child", img_parent.as_raw(), img_child.as_raw()), + ]; + for (name, before, after) in steps { + let changed = pixel_diff(before, after); + println!("pixels changed by {name}: {changed}"); + assert!( + changed > 500, + "changing {name} did not change the rendering ({changed} pixels changed)" + ); + } +} diff --git a/crates/examples/src/shadow.rs b/crates/examples/src/shadow.rs new file mode 100644 index 00000000..3dc27031 --- /dev/null +++ b/crates/examples/src/shadow.rs @@ -0,0 +1,184 @@ +//! Shadow mapping examples. + +use crate::{cwd_to_manual_assets_dir, workspace_dir}; + +#[tokio::test] +async fn manual_shadow_mapping() { + let _ = env_logger::builder().try_init(); + + // ANCHOR: setup + use renderling::{ + camera::Camera, + context::Context, + geometry::Vertex, + glam::{Mat4, Vec3, Vec4}, + gltf::GltfDocument, + light::{AnalyticalLight, DirectionalLight, Lux}, + primitive::Primitive, + stage::Stage, + types::GpuOnlyArray, + }; + + let ctx = Context::headless(512, 512).await; + let stage: Stage = ctx + .new_stage() + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let _camera: Camera = { + let aspect = 1.0; + let fovy = core::f32::consts::PI / 4.0; + let znear = 0.1; + let zfar = 10.0; + let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar); + let eye = Vec3::new(0.5, 0.5, 0.8); + let target = Vec3::new(0.0, 0.3, 0.0); + let up = Vec3::Y; + let view = Mat4::look_at_rh(eye, target, up); + + stage + .new_camera() + .with_projection_and_view(projection, view) + }; + + // Load the marble bust, as in the lighting section. + let model: GltfDocument = stage + .load_gltf_document_from_path(workspace_dir().join("gltf/marble_bust_1k.glb")) + .unwrap() + .into_gpu_only(); + + // Add a ground plane for the bust's shadow to land on. + let y = -0.01; + let s = 2.0; + let floor: Primitive = stage + .new_primitive() + .with_vertices( + stage.new_vertices([ + Vertex::default() + .with_position([s, y, s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([s, y, s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, -s]) + .with_normal(Vec3::Y), + Vertex::default() + .with_position([-s, y, s]) + .with_normal(Vec3::Y), + ]), + ) + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(0.85, 0.85, 0.85, 1.0)), + ); + + // Create a directional light. + let sun: AnalyticalLight = stage + .new_directional_light() + .with_direction(Vec3::new(-0.4, -1.0, -0.6).normalize()) + .with_color(Vec4::ONE) + .with_intensity(Lux::OUTDOOR_OVERCAST_HIGH); + // ANCHOR_END: setup + + cwd_to_manual_assets_dir(); + + // Render the scene once without any shadow mapping. + // ANCHOR: render_without_shadow + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_without_shadow = frame.read_image().await.unwrap(); + img_without_shadow.save("lighting/shadow-none.png").unwrap(); + frame.present(); + // ANCHOR_END: render_without_shadow + + // ANCHOR: shadowmap + use renderling::glam::UVec2; + + // Create a shadow map for the sun. + // + // The first argument is the light to cast shadows from. The `size` + // determines the resolution of the shadow map - bigger maps give + // crisper shadows but use more memory. `z_near` and `z_far` bound + // the light's frustum - only objects within the frustum cast + // shadows. + // + // Shadow maps are stored in a texture atlas shared by the whole + // stage. The size of the atlas can be configured with + // `Context::with_shadow_mapping_atlas_texture_size`. + let shadow_map = stage + .new_shadow_map(&sun, UVec2::splat(1024), 0.1, 10.0) + .unwrap(); + // ANCHOR_END: shadowmap + + // ANCHOR: update + // Update the shadow map by rendering the given primitives as shadow + // casters, from the light's point of view. + // + // In a typical application this is done every frame, before + // `Stage::render`. + shadow_map + .update( + &stage, + model.renderlets_iter().chain(std::iter::once(&floor)), + ) + .unwrap(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img_with_shadow = frame.read_image().await.unwrap(); + img_with_shadow.save("lighting/shadow.png").unwrap(); + frame.present(); + // ANCHOR_END: update + + // ANCHOR: tuning + // Tune the shadow map by modifying its descriptor. + // + // The bias values compensate for the limited precision of the + // shadow map. Too little bias causes "shadow acne", where surfaces + // shadow themselves. Too much bias causes "peter panning", where + // shadows detach from their casters. + // + // `pcf_samples` controls the softness of shadow edges through + // percentage-closer filtering. Higher values are more expensive. + { + let mut desc = shadow_map.descriptor_lock(); + desc.bias_min = 0.0005; + desc.bias_max = 0.005; + desc.pcf_samples = 4; + } + + // Update and render again to see the difference. + shadow_map + .update( + &stage, + model.renderlets_iter().chain(std::iter::once(&floor)), + ) + .unwrap(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + frame.present(); + // ANCHOR_END: tuning + + // The shadow map should have changed the rendering. + let without = img_without_shadow.as_raw(); + let with = img_with_shadow.as_raw(); + assert_eq!(without.len(), with.len()); + let changed = without + .chunks_exact(4) + .zip(with.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count(); + println!("pixels changed by shadow mapping: {changed}"); + assert!( + changed > 500, + "shadow mapping did not change the rendering ({changed} pixels changed)" + ); +} diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index 5e09c9cb..45fa297c 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -179,7 +179,8 @@ //! # Next steps //! //! For further introduction to what renderling can do, take a tour of the -//! [`Stage`] type, or get started with [the manual](#todo). +//! [`Stage`] type, or get started with +//! [the manual](https://renderling.xyz/manual/index.html). //! //! # WARNING //! diff --git a/crates/renderling/src/stage/cpu.rs b/crates/renderling/src/stage/cpu.rs index a285b78a..ae3e530b 100644 --- a/crates/renderling/src/stage/cpu.rs +++ b/crates/renderling/src/stage/cpu.rs @@ -1523,6 +1523,15 @@ impl Stage { self } + /// Returns the stage's [`Tonemapping`], used to configure the tone + /// mapping algorithm and exposure of the final rendered image. + /// + /// Use [`Tonemapping::set_tonemapping_config`] with a + /// `TonemapConstants` to change the settings. + pub fn tonemapping(&self) -> &Tonemapping { + &self.tonemapping + } + /// Turn the bloom effect on or off. pub fn set_has_bloom(&self, has_bloom: bool) { self.has_bloom diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 1f6f5f30..56b1d5f3 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -60,7 +60,7 @@ pub struct Manual { impl Manual { async fn install_deps() { - const DEPS: &[&str] = &["mdbook", "mdbook-environment"]; + const DEPS: &[&str] = &["mdbook", "mdbook-variables"]; for dep in DEPS { if !deps::has_binary(dep).await { deps::cargo_install(dep).await; diff --git a/manual/book.toml b/manual/book.toml index 2379f046..cf4c1bbc 100644 --- a/manual/book.toml +++ b/manual/book.toml @@ -5,14 +5,20 @@ src = "src" title = "The Renderling Manual" description = "Operations manual for the Renderling real-time renderer" -# The "environment" preprocessor, provided by `mdbook-environment`, -# references variables from here and the environment that can then be -# interpolated in the book with `{{VARIABLE}}`. +# The "variables" preprocessor, provided by `mdbook-variables`, +# interpolates `{{VARIABLE}}` in the book, resolving variables from here +# and from the environment. # # Setting variables here overrides any set in the environment, so for # variables that must change based on environment we add them here # commented out. -[preprocessor.environment] +[preprocessor.variables] +# Look up variables not defined here in the environment. +use_env = true + +[preprocessor.variables.variables] +# Variables can be pinned here; values from the environment are used as a +# fallback for anything not defined in this table. # when deploying the manual # DOCS_URL = "https://docs.rs/renderling/latest" diff --git a/manual/src/SUMMARY.md b/manual/src/SUMMARY.md index b1f1b29d..a8e32a0c 100644 --- a/manual/src/SUMMARY.md +++ b/manual/src/SUMMARY.md @@ -9,3 +9,10 @@ - [Lighting](./lighting.md) - [Analytical lights](./lighting/analytical.md) - [Image based lighting](./lighting/ibl.md) + - [Shadow mapping](./lighting/shadow-mapping.md) +- [Materials and textures](./material.md) +- [Post-processing](./postprocessing.md) +- [Debug modes](./debug.md) +- [Scene hierarchy](./scene.md) +- [Animation](./animation.md) +- [Performance](./performance.md) diff --git a/manual/src/animation.md b/manual/src/animation.md new file mode 100644 index 00000000..bec672da --- /dev/null +++ b/manual/src/animation.md @@ -0,0 +1,84 @@ +# Animation 🎬 + +GLTF files can carry animation data, and `renderling` can play it back. There +are three related mechanisms: + +1. **Animation clips** - keyframed translations, rotations and scales for the + nodes in a scene, played back with the [`Animator`]. +2. **Morph targets** - per-vertex "shapes" blended by weight. +3. **Skins** - vertices bound to a skeleton of joint transforms. + +## Playing an animation clip + +We'll start with a GLTF file containing an animated triangle. As usual, load +it through the stage, then render the starting frame: + +```rust,ignore +{{#include ../../crates/examples/src/animation.rs:setup}} +``` + +To play the document's animation, collect the scene's nodes and hand them to +the [`Animator`] along with a clip: + +```rust,ignore +{{#include ../../crates/examples/src/animation.rs:animator}} +``` + +Then advance the animation and render. In an application this happens every +frame, with `dt` the time since the previous one: + +```rust,ignore +{{#include ../../crates/examples/src/animation.rs:progress}} +``` + +![a triangle at the start of its animation](assets/animation/frame-0.png) + +![the same triangle, further along its animation](assets/animation/frame-2.png) + +![the same triangle, further still](assets/animation/frame-5.png) + +The animation wraps around automatically once it runs past its last keyframe. + +## Morph targets + +Morph targets are alternative positions (and normals/tangents) for a mesh's +vertices, blended in by weight. This is how faces emote, how lips sync, and +how simple effects like this twisted quad are done: + +```rust,ignore +{{#include ../../crates/examples/src/animation.rs:morph}} +``` + +![a flat quad at morph weight zero](assets/animation/morph-0.png) + +![the same quad twisted, at morph weight one](assets/animation/morph-1.png) + +Here we built the morph target by hand with +[`Stage::new_morph_targets`] and [`Stage::new_morph_target_weights`], attached +them with [`Primitive::set_morph_targets`], and updated the weight at +runtime. For GLTF files, the loader stages morph targets for you, and +animation clips can drive the weights automatically through the +[`Animator`]. + +## Skins + +"Skinned" meshes have their vertices bound to a skeleton: each vertex follows +a weighted combination of "joint" transforms, so moving a joint drags the +mesh along like skin over bone. Loading a rigged GLTF file stages the skin, +and rendering with skinning is enabled with +[`Stage::set_has_vertex_skinning`] (or `with_vertex_skinning` on the stage): + +```rust,ignore +stage.set_has_vertex_skinning(true); +``` + +> **Caveat:** vertex skinning currently produces no visible effect, even with +> the skeleton posed - see +> [issue #244](https://github.com/schell/renderling/issues/244). The manual +> will cover skinning in depth once that is fixed. + +[`Animator`]: {{DOCS_URL}}/renderling/gltf/anime/struct.Animator.html +[`Stage::set_has_vertex_skinning`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_has_vertex_skinning +[`Stage::new_morph_targets`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_morph_targets +[`Stage::new_morph_target_weights`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_morph_target_weights +[`Primitive::set_morph_targets`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html#method.set_morph_targets \ No newline at end of file diff --git a/manual/src/assets/animation/frame-0.png b/manual/src/assets/animation/frame-0.png new file mode 100644 index 00000000..4a496832 Binary files /dev/null and b/manual/src/assets/animation/frame-0.png differ diff --git a/manual/src/assets/animation/frame-2.png b/manual/src/assets/animation/frame-2.png new file mode 100644 index 00000000..51ad8ad6 Binary files /dev/null and b/manual/src/assets/animation/frame-2.png differ diff --git a/manual/src/assets/animation/frame-5.png b/manual/src/assets/animation/frame-5.png new file mode 100644 index 00000000..a2ae8a31 Binary files /dev/null and b/manual/src/assets/animation/frame-5.png differ diff --git a/manual/src/assets/animation/morph-0.png b/manual/src/assets/animation/morph-0.png new file mode 100644 index 00000000..3424c50f Binary files /dev/null and b/manual/src/assets/animation/morph-0.png differ diff --git a/manual/src/assets/animation/morph-1.png b/manual/src/assets/animation/morph-1.png new file mode 100644 index 00000000..c85a7e59 Binary files /dev/null and b/manual/src/assets/animation/morph-1.png differ diff --git a/manual/src/assets/debug/albedo.png b/manual/src/assets/debug/albedo.png new file mode 100644 index 00000000..9d122e8c Binary files /dev/null and b/manual/src/assets/debug/albedo.png differ diff --git a/manual/src/assets/debug/none.png b/manual/src/assets/debug/none.png new file mode 100644 index 00000000..23b5be61 Binary files /dev/null and b/manual/src/assets/debug/none.png differ diff --git a/manual/src/assets/debug/normals.png b/manual/src/assets/debug/normals.png new file mode 100644 index 00000000..823c3a1b Binary files /dev/null and b/manual/src/assets/debug/normals.png differ diff --git a/manual/src/assets/debug/uv-coords.png b/manual/src/assets/debug/uv-coords.png new file mode 100644 index 00000000..a968d315 Binary files /dev/null and b/manual/src/assets/debug/uv-coords.png differ diff --git a/manual/src/assets/lighting/shadow-none.png b/manual/src/assets/lighting/shadow-none.png new file mode 100644 index 00000000..6efdd6fe Binary files /dev/null and b/manual/src/assets/lighting/shadow-none.png differ diff --git a/manual/src/assets/lighting/shadow.png b/manual/src/assets/lighting/shadow.png new file mode 100644 index 00000000..23b5be61 Binary files /dev/null and b/manual/src/assets/lighting/shadow.png differ diff --git a/manual/src/assets/material/albedo-factor.png b/manual/src/assets/material/albedo-factor.png new file mode 100644 index 00000000..665b4db2 Binary files /dev/null and b/manual/src/assets/material/albedo-factor.png differ diff --git a/manual/src/assets/material/albedo-texture.png b/manual/src/assets/material/albedo-texture.png new file mode 100644 index 00000000..4e30dbbf Binary files /dev/null and b/manual/src/assets/material/albedo-texture.png differ diff --git a/manual/src/assets/material/albedo-updated.png b/manual/src/assets/material/albedo-updated.png new file mode 100644 index 00000000..adc10f74 Binary files /dev/null and b/manual/src/assets/material/albedo-updated.png differ diff --git a/manual/src/assets/material/emissive.png b/manual/src/assets/material/emissive.png new file mode 100644 index 00000000..e055cc50 Binary files /dev/null and b/manual/src/assets/material/emissive.png differ diff --git a/manual/src/assets/material/normal-map.png b/manual/src/assets/material/normal-map.png new file mode 100644 index 00000000..5c473014 Binary files /dev/null and b/manual/src/assets/material/normal-map.png differ diff --git a/manual/src/assets/material/pbr-rough.png b/manual/src/assets/material/pbr-rough.png new file mode 100644 index 00000000..d6f370b6 Binary files /dev/null and b/manual/src/assets/material/pbr-rough.png differ diff --git a/manual/src/assets/material/pbr.png b/manual/src/assets/material/pbr.png new file mode 100644 index 00000000..d38ebe9e Binary files /dev/null and b/manual/src/assets/material/pbr.png differ diff --git a/manual/src/assets/performance/culling.png b/manual/src/assets/performance/culling.png new file mode 100644 index 00000000..c5fbd61a Binary files /dev/null and b/manual/src/assets/performance/culling.png differ diff --git a/manual/src/assets/performance/msaa-4.png b/manual/src/assets/performance/msaa-4.png new file mode 100644 index 00000000..af698bbd Binary files /dev/null and b/manual/src/assets/performance/msaa-4.png differ diff --git a/manual/src/assets/performance/no-tiling.png b/manual/src/assets/performance/no-tiling.png new file mode 100644 index 00000000..bae1603f Binary files /dev/null and b/manual/src/assets/performance/no-tiling.png differ diff --git a/manual/src/assets/performance/occlusion.png b/manual/src/assets/performance/occlusion.png new file mode 100644 index 00000000..c5fbd61a Binary files /dev/null and b/manual/src/assets/performance/occlusion.png differ diff --git a/manual/src/assets/performance/tiling.png b/manual/src/assets/performance/tiling.png new file mode 100644 index 00000000..bae1603f Binary files /dev/null and b/manual/src/assets/performance/tiling.png differ diff --git a/manual/src/assets/postprocessing/bloom-none.png b/manual/src/assets/postprocessing/bloom-none.png new file mode 100644 index 00000000..a013f328 Binary files /dev/null and b/manual/src/assets/postprocessing/bloom-none.png differ diff --git a/manual/src/assets/postprocessing/bloom.png b/manual/src/assets/postprocessing/bloom.png new file mode 100644 index 00000000..6d30e34b Binary files /dev/null and b/manual/src/assets/postprocessing/bloom.png differ diff --git a/manual/src/assets/postprocessing/exposure.png b/manual/src/assets/postprocessing/exposure.png new file mode 100644 index 00000000..069dd570 Binary files /dev/null and b/manual/src/assets/postprocessing/exposure.png differ diff --git a/manual/src/assets/postprocessing/tonemap-aces.png b/manual/src/assets/postprocessing/tonemap-aces.png new file mode 100644 index 00000000..c9efc676 Binary files /dev/null and b/manual/src/assets/postprocessing/tonemap-aces.png differ diff --git a/manual/src/assets/scene/hierarchy.png b/manual/src/assets/scene/hierarchy.png new file mode 100644 index 00000000..e2700442 Binary files /dev/null and b/manual/src/assets/scene/hierarchy.png differ diff --git a/manual/src/assets/scene/moved-child.png b/manual/src/assets/scene/moved-child.png new file mode 100644 index 00000000..17015dc0 Binary files /dev/null and b/manual/src/assets/scene/moved-child.png differ diff --git a/manual/src/assets/scene/moved-parent.png b/manual/src/assets/scene/moved-parent.png new file mode 100644 index 00000000..3d341dca Binary files /dev/null and b/manual/src/assets/scene/moved-parent.png differ diff --git a/manual/src/debug.md b/manual/src/debug.md new file mode 100644 index 00000000..9329851e --- /dev/null +++ b/manual/src/debug.md @@ -0,0 +1,72 @@ +# Debug modes 🔍 + +When shading goes wrong - shadow acne, seams, surfaces lit from the wrong +direction - it helps to see the raw values the shader is working with. +`renderling` has two built-in debugging tools: + +1. **Debug channels** - visualize an intermediate value of the fragment shader + as colors, one channel at a time. +2. **The debug overlay** - draw the projected bounding volumes of every drawn + primitive, useful when debugging culling. (Currently not rendering - see the + caveat below.) + +## Example setup + +We'll debug the shadow mapping scene from the previous chapters: + +```rust,ignore +{{#include ../../crates/examples/src/debug.rs:setup}} +``` + +![a marble bust casting a shadow onto a ground plane](assets/debug/none.png) + +## Debug channels + +Set the stage's debug channel with [`Stage::set_debug_mode`] (or +[`Stage::with_debug_mode`] at stage creation). The scene then renders through +the given [`DebugChannel`], which early-exits the fragment shader and displays +an intermediate value as colors: + +```rust,ignore +{{#include ../../crates/examples/src/debug.rs:channel}} +``` + +![the bust and floor displaying world-space normals](assets/debug/normals.png) + +![the bust displaying its UV coordinates](assets/debug/uv-coords.png) + +![the bust and floor displaying albedo colors](assets/debug/albedo.png) + +The full set of channels, grouped by what they show: + +| group | channels | reach for when | +|---|---|---| +| geometry | `UvCoords0`, `UvCoords1`, `VertexColor` | texture mapping looks wrong, colors are off | +| normals | `VertexNormals`, `Normals`, `UvNormals`, `Tangents`, `Bitangents` | lighting/shadows come from the wrong direction; normal maps misbehaving | +| material | `Albedo`, `Roughness`, `Metallic`, `Occlusion` | a material parameter is not what you think it is | +| emissive | `Emissive`, `UvEmissive`, `EmissiveFactor`, `EmissiveStrength` | glowing too much or not at all | +| lighting | `DiffuseIrradiance`, `SpecularReflection`, `Brdf` | isolating what the lights and IBL actually contribute | + +For example, `Normals` is the first thing to check when a shadow appears on a +surface that should be lit: a wrong normal explains most "shadows where there +shouldn't be any" bugs, including shadow-mapping acne. + +Set the channel back to `DebugChannel::None` to render normally again. + +## The debug overlay + +[`Stage::set_use_debug_overlay`] (or `with_debug_overlay`) draws the projected +bounding volume of every drawn primitive as outlines on top of the final +image. It is meant for debugging why things draw or don't draw - for example +when frustum culling is more aggressive than expected. + +> **Caveat:** the debug overlay currently produces no visible output, even on +> the GPU-driven indirect drawing path it requires - see +> [issue #243](https://github.com/schell/renderling/issues/243). It also +> silently does nothing under direct drawing, since the overlay visualizes the +> indirect draw calls. + +[`DebugChannel`]: {{DOCS_URL}}/renderling/pbr/debug/enum.DebugChannel.html +[`Stage::set_debug_mode`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_debug_mode +[`Stage::with_debug_mode`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.with_debug_mode +[`Stage::set_use_debug_overlay`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_use_debug_overlay \ No newline at end of file diff --git a/manual/src/lighting.md b/manual/src/lighting.md index 0d813381..cc9c2aa9 100644 --- a/manual/src/lighting.md +++ b/manual/src/lighting.md @@ -31,4 +31,5 @@ HDR image into a skybox, and then rendered: ![renderling skybox](assets/skybox.png) -Now let's learn about analytical lights, and then image based lighting. +Now let's learn about analytical lights, then image based lighting, and +finally shadow mapping. diff --git a/manual/src/lighting/analytical.md b/manual/src/lighting/analytical.md index aea2b7a3..6ca63a3d 100644 --- a/manual/src/lighting/analytical.md +++ b/manual/src/lighting/analytical.md @@ -84,4 +84,22 @@ you can see the effect: ![image of a marble bust lit by a single spot light](../assets/lighting/spot.png) +## Ambient light + +In addition to the three light types above, the stage has a global **ambient** +color - a constant light contribution added to every fragment, modulated by +the surface albedo. It defaults to zero. + +Ambient light fills in shadowed areas, so shadows read as the ambient color +instead of pure black: + +```rust,ignore +// An orange ambient glow, at 30% intensity. +stage.set_ambient_color(Vec4::new(1.0, 0.5, 0.0, 0.3)); +``` + +The XYZ components are the color, and W is the intensity. It can also be set +at stage creation with `.with_ambient_color(...)`, and the current value read +back with `Stage::ambient_color`. + Good enough! Now on to image-based lighting, which uses environment maps to simulate complex lighting scenarios. This technique captures real-world lighting conditions and applies them to the scene, providing more realistic reflections and ambient lighting. diff --git a/manual/src/lighting/shadow-mapping.md b/manual/src/lighting/shadow-mapping.md new file mode 100644 index 00000000..3df1d2d9 --- /dev/null +++ b/manual/src/lighting/shadow-mapping.md @@ -0,0 +1,110 @@ +# Shadow mapping 🌑 + +Shadow mapping is the technique used to render shadows cast by lights. + +It works by rendering the scene one more time, but from the point of view of +the light. Instead of colors, this rendering stores the _depth_ of each +fragment - how far away the closest surface is. Then, when the scene is shaded +for the camera, each fragment can compare its own depth from the light's +perspective to the stored depth. If the fragment is farther away, something is +in front of it, and the fragment is in shadow. + +In `renderling` shadow maps are rendered into textures stored in a shared +"shadow map atlas". The size of the atlas can be configured with +[`Context::with_shadow_mapping_atlas_texture_size`]. + +## Example setup + +We'll continue with our marble bust scene, but this time we'll add a ground +plane for the bust's shadow to land on, and a directional light to cast it: + +```rust,ignore +{{#include ../../../crates/examples/src/shadow.rs:setup}} +``` + +## Without shadows + +Rendering the scene now gives us a well-lit bust, but no shadow: + +```rust,ignore +{{#include ../../../crates/examples/src/shadow.rs:render_without_shadow}} +``` + +![a marble bust on a ground plane, lit by a directional light, with no shadows](../assets/lighting/shadow-none.png) + +Notice how the ground is just as bright beneath the bust as it is everywhere +else. The bust does not block the light at all. + +## Creating a shadow map + +To cast shadows, we create a [`ShadowMap`] for our light using +[`Stage::new_shadow_map`]: + +```rust,ignore +{{#include ../../../crates/examples/src/shadow.rs:shadowmap}} +``` + +The `size` argument determines the resolution of the shadow map - bigger maps +give crisper shadows but use more memory. The `z_near` and `z_far` arguments +bound the light's frustum - only objects within the frustum will cast shadows. + +## Updating the shadow map + +A shadow map is a rendering of the scene, which means it has to be kept up to +date. Updating is done with [`ShadowMap::update`], which takes the primitives +to render as shadow casters: + +```rust,ignore +{{#include ../../../crates/examples/src/shadow.rs:update}} +``` + +In a typical application you will call [`ShadowMap::update`] every frame, +before [`Stage::render`]. + +The [`ShadowMap`] holds a weak reference to the light it was created with, so +changes made to the light - like moving the sun across the sky - automatically +propagate to the shadow map on the next update. + +![a marble bust casting a shadow onto a ground plane](../assets/lighting/shadow.png) + +And just like that, our bust casts a shadow! + +## Tuning + +Shadow maps have limited precision, which shows up as visual artifacts. The +two most common are "shadow acne", where surfaces end up shadowing themselves, +and "peter panning", where shadows detach from their casters. Both are +controlled with the bias values on the shadow map's descriptor: + +```rust,ignore +{{#include ../../../crates/examples/src/shadow.rs:tuning}} +``` + +Increasing the bias values lifts surfaces out of their own shadow, fixing +acne. Too much bias, though, and shadows start to detach from the objects +casting them. + +The `pcf_samples` field controls the softness of the shadow edges through +"percentage-closer filtering". Higher values produce softer edges but cost +more texture samples. + +## Tips for making a good shadow map + +1. **Make sure the map is big enough.** A bigger map gives cleaner shadows, + and can fix some peter panning issues even before playing with bias. +2. **Don't set `pcf_samples` too high.** A high sample count can actually + _cause_ peter panning, and costs more. +3. **Ensure `z_near` and `z_far` make sense for your scene.** If you find that + shadows are cut off in a straight line, it's likely one of them needs + adjustment. +4. **Expect point lights to be more expensive.** A point light shadow map + renders the scene from six points of view, one for each face of a cube + around the light. To compensate for the lower per-face resolution, the + number of percentage-closer filtering samples is forced to 16 for point + lights. + +[`Context::with_shadow_mapping_atlas_texture_size`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.with_shadow_mapping_atlas_texture_size +[`ShadowMap`]: {{DOCS_URL}}/renderling/light/struct.ShadowMap.html +[`ShadowMap::update`]: {{DOCS_URL}}/renderling/light/struct.ShadowMap.html#method.update +[`Stage::new_shadow_map`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_shadow_map +[`Stage::render`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.render \ No newline at end of file diff --git a/manual/src/material.md b/manual/src/material.md new file mode 100644 index 00000000..b9193f69 --- /dev/null +++ b/manual/src/material.md @@ -0,0 +1,148 @@ +# Materials and textures 🎨 + +Materials describe how a surface looks and how it responds to light: its base +color, how metallic and rough it is, whether it glows on its own, and which +textures to sample for all of the above. + +We met [`Material`] briefly in [the staging chapter](/stage.html) - now let's +give it a proper tour. + +## Example setup + +We'll use two cubes that share the same vertices, each with its own transform. +Materials can be shared by any number of primitives, and by the end of this +chapter you'll see why that's useful: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:setup}} +``` + +## The albedo factor + +The simplest material parameter is the "albedo factor" - the base color of the +surface. Here we create an unlit material (lighting turned off on the material +itself) with a teal albedo factor, and assign it to both cubes: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:albedo_factor}} +``` + +![two teal cubes](assets/material/albedo-factor.png) + +Note that one material instance covers both cubes. Materials are staged +resources - assigning the same [`Material`] to many primitives costs nothing +extra, and any later change to the material updates every primitive using it. + +## Textures and the atlas + +Flat colors only get you so far. To use image textures, first stage them in +the stage's texture atlas with [`Stage::set_images`], which takes +[`AtlasImage`]s and returns one [`AtlasTexture`] handle per image: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:albedo_texture}} +``` + +Two things to know about `set_images`: + +1. It **resets the atlas**, repacking it with just the images given. Any + [`AtlasTexture`] handles from a previous call are invalidated. +2. Because of that, stage all the images you'll need up front, in one call. + +## Using a texture + +With handles in hand, we can set the albedo texture of our material: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:use_texture}} +``` + +![two cubes textured with sandstone](assets/material/albedo-texture.png) + +The albedo factor _multiplies_ the albedo texture, which is why we reset it to +white first. Multiplying by a non-white factor is a cheap way to tint a +texture. + +## Updating materials at runtime + +Materials are staged, so updates are cheap and automatic. One call to +[`Material::set_albedo_texture`] switches _both_ cubes to the second image: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:update}} +``` + +![two cubes textured with dirt](assets/material/albedo-updated.png) + +## PBR parameters + +So far our material has been unlit. With lighting on, materials become +"physically based" (PBR), with a few more knobs: + +* `metallic_factor` - how metal-like the surface is. Metals tint their + reflections with the albedo color. +* `roughness_factor` - how rough the surface is. Rough surfaces blur + reflections. + +For this demo we'll also give the scene a skybox, image-based lighting and a +sun, so the metallic surface has an environment to reflect. Those are covered +in [the skybox](/skybox.html) and [lighting](/lighting.html) chapters: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:pbr}} +``` + +![two metallic gold sandstone cubes](assets/material/pbr.png) + +Crank the roughness up and the reflections smear into a matte surface: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:roughness}} +``` + +![two rough metallic cubes](assets/material/pbr-rough.png) + +## Emissive color + +Emissive color is added directly to the final color, regardless of lighting, +and takes an optional strength multiplier. Combined with bloom, emissive +materials glow: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:emissive}} +``` + +![two metallic cubes glowing red](assets/material/emissive.png) + +## Normal maps + +Normal maps fake small-scale surface detail by perturbing the surface normal +per-pixel, which changes how light interacts with the surface without adding +any geometry. + +GLTF files can carry normal maps, and the loader wires them into the material +for you. This brick sphere has lighting and a normal map baked in: + +```rust,ignore +{{#include ../../crates/examples/src/material.rs:normal_map}} +``` + +![a sphere textured with bricks and a normal map](assets/material/normal-map.png) + +When building materials by hand, the same slots exist as builders: +[`Material::with_normal_texture`], +[`Material::with_metallic_roughness_texture`], +[`Material::with_ambient_occlusion_texture`] and +[`Material::with_emissive_texture`], along with their `set_*` counterparts. +Each texture slot can also pick which UV set of the vertex to sample with +(`with_albedo_tex_coord` and friends) when a mesh carries more than one. + +[`Material`]: {{DOCS_URL}}/renderling/material/struct.Material.html +[`Material::set_albedo_texture`]: {{DOCS_URL}}/renderling/material/struct.Material.html#method.set_albedo_texture +[`Material::with_normal_texture`]: {{DOCS_URL}}/renderling/material/struct.Material.html#method.with_normal_texture +[`Material::with_metallic_roughness_texture`]: {{DOCS_URL}}/renderling/material/struct.Material.html#method.with_metallic_roughness_texture +[`Material::with_ambient_occlusion_texture`]: {{DOCS_URL}}/renderling/material/struct.Material.html#method.with_ambient_occlusion_texture +[`Material::with_emissive_texture`]: {{DOCS_URL}}/renderling/material/struct.Material.html#method.with_emissive_texture +[`Stage::set_images`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_images +[`AtlasImage`]: {{DOCS_URL}}/renderling/atlas/struct.AtlasImage.html +[`AtlasTexture`]: {{DOCS_URL}}/renderling/atlas/struct.AtlasTexture.html \ No newline at end of file diff --git a/manual/src/performance.md b/manual/src/performance.md new file mode 100644 index 00000000..b268a3b4 --- /dev/null +++ b/manual/src/performance.md @@ -0,0 +1,87 @@ +# Performance ⚡ + +`renderling` is a GPU-driven renderer: several features move work from your +code (and from wasted GPU work) into efficient pre-passes. None of them change +the picture - they change how much work it takes to produce it. + +## Frustum culling + +Objects outside the camera's view cannot be seen, so there is no reason to +draw them. Frustum culling skips them in a GPU step before drawing, and **is +on by default**: + +```rust,ignore +{{#include ../../crates/examples/src/performance.rs:culling}} +``` + +![a grid of cubes](assets/performance/culling.png) + +The example renders a grid of cubes with culling on and off - the two +renderings are byte-for-byte identical. That is the whole point: culling +skips invisible work without changing the output. Turn it off only for +debugging: + +```rust,ignore +stage.set_use_frustum_culling(false); +``` + +## Occlusion culling + +Occlusion culling goes further and skips objects that are *hidden behind* +other objects. It is **off by default**, and is still a feature in +development: + +```rust,ignore +{{#include ../../crates/examples/src/performance.rs:occlusion}} +``` + +## Light tiling + +Analytical lights are how scenes get their shine, but a naive renderer makes +every fragment consider *every* light. With a grid of point lights the cost +adds up fast: + +```rust,ignore +{{#include ../../crates/examples/src/performance.rs:many_lights}} +``` + +![a floor lit by a grid of colorful point lights](assets/performance/no-tiling.png) + +Light tiling bins lights into screen-space tiles, so each fragment only +considers the lights that overlap its tile. Create it with +[`Stage::new_light_tiling`] and run it before rendering - in an application, +every frame: + +```rust,ignore +{{#include ../../crates/examples/src/performance.rs:tiling}} +``` + +![the same floor, rendered with light tiling](assets/performance/tiling.png) + +The two images above are identical - tiling is pure savings. The +[`LightTilingConfig`] has three knobs: + +* `tile_size` - the size of each screen tile, in pixels. Defaults to `16`. +* `max_lights_per_tile` - the maximum number of lights binned per tile. + Defaults to `32`. +* `minimum_illuminance` - lights dimmer than this, in lux, are skipped. + Defaults to `0.1`. (For reference: moonlight is under 1 lux, indoor + lighting runs 100-300, detailed work needs 1000 or more.) + +## MSAA + +Multisample anti-aliasing smooths the jagged edges of geometry by taking +multiple samples per pixel. Set the sample count on the stage - `4` is a good +default: + +```rust,ignore +{{#include ../../crates/examples/src/performance.rs:msaa}} +``` + +![the same cube grid with 4x msaa, smooth edges](assets/performance/msaa-4.png) + +[`Stage::set_use_frustum_culling`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_use_frustum_culling +[`Stage::set_use_occlusion_culling`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_use_occlusion_culling +[`Stage::new_light_tiling`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_light_tiling +[`LightTilingConfig`]: {{DOCS_URL}}/renderling/light/struct.LightTilingConfig.html +[`Stage::set_msaa_sample_count`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_msaa_sample_count \ No newline at end of file diff --git a/manual/src/postprocessing.md b/manual/src/postprocessing.md new file mode 100644 index 00000000..b2163179 --- /dev/null +++ b/manual/src/postprocessing.md @@ -0,0 +1,86 @@ +# Post-processing ✨ + +`renderling` renders scenes in **high dynamic range** (HDR), where colors can be +far brighter than a display can show. After the scene is rendered, two +post-processing passes turn that HDR image into the final picture: + +1. **Bloom** - bright areas glow, by mixing a blurred copy of the bright parts + back into the image. +2. **Tonemapping** - HDR values are mapped into the range the display can + show, using an exposure value and a tone mapping algorithm. + +## Example setup + +We'll use a night sky and a model full of glowing emissive objects, so there +are bright areas for the effects to work on. Note that bloom is on by default - +we turn it off here so we can show the difference: + +```rust,ignore +{{#include ../../crates/examples/src/postprocessing.rs:setup}} +``` + +![a night scene with glowing objects and no bloom](assets/postprocessing/bloom-none.png) + +## Bloom + +Bloom can be toggled at stage creation with [`Stage::with_bloom`], or at +runtime with [`Stage::set_has_bloom`]. Two parameters control the look: + +* [`Stage::set_bloom_mix_strength`] - how much of the blurred bright areas to + mix back into the image. Defaults to `0.04`. +* [`Stage::set_bloom_filter_radius`] - the radius of the blur, in texels. + +```rust,ignore +{{#include ../../crates/examples/src/postprocessing.rs:bloom}} +``` + +![the same night scene with bloom, glowing objects smeared with light](assets/postprocessing/bloom.png) + +## Tonemapping + +Before the image is shown, it is multiplied by the **exposure** and passed +through the **tone mapping algorithm**, which compresses HDR values into the +display's range. + +The current configuration is available through the stage's +[`Tonemapping`] as a [`TonemapConstants`] - a small struct with two fields: + +```rust,ignore +{{#include ../../crates/examples/src/postprocessing.rs:exposure}} +``` + +![the same night scene with 3x exposure, much brighter](assets/postprocessing/exposure.png) + +By default no tone mapping algorithm is used (`Tonemap::NONE`) - HDR values +outside the display range simply clip to white. The available algorithms are: + +* [`Tonemap::NONE`] - no tone mapping, values clip (default) +* [`Tonemap::ACES_NARKOWICZ`] - the popular ACES approximation +* [`Tonemap::ACES_HILL`] - the ACES fit by Stephen Hill +* [`Tonemap::ACES_HILL_EXPOSURE_BOOST`] - `ACES_HILL` with a built-in + exposure boost, the "filmic" look popularized by three.js +* [`Tonemap::REINHARD`] - the simple Reinhard operator + +```rust,ignore +{{#include ../../crates/examples/src/postprocessing.rs:tonemap}} +``` + +![the same night scene tone mapped with ACES, softer highlights](assets/postprocessing/tonemap-aces.png) + +Filmic operators like ACES trade a little saturation for smoother highlights - +notice how the bright objects keep more of their color, instead of blowing out +to white. + +[`Stage::with_bloom`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.with_bloom +[`Stage::set_has_bloom`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_has_bloom +[`Stage::set_bloom_mix_strength`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_bloom_mix_strength +[`Stage::set_bloom_filter_radius`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_bloom_filter_radius +[`Tonemapping`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemapping.html +[`Tonemapping::set_tonemapping_config`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemapping.html#method.set_tonemapping_config +[`TonemapConstants`]: {{DOCS_URL}}/renderling/tonemapping/struct.TonemapConstants.html +[`Tonemap`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemap.html +[`Tonemap::NONE`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemap.html#associatedconstant.NONE +[`Tonemap::ACES_NARKOWICZ`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemap.html#associatedconstant.ACES_NARKOWICZ +[`Tonemap::ACES_HILL`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemap.html#associatedconstant.ACES_HILL +[`Tonemap::ACES_HILL_EXPOSURE_BOOST`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemap.html#associatedconstant.ACES_HILL_EXPOSURE_BOOST +[`Tonemap::REINHARD`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemap.html#associatedconstant.REINHARD \ No newline at end of file diff --git a/manual/src/reflinks.md b/manual/src/reflinks.md index a1206c98..7f52fe9b 100644 --- a/manual/src/reflinks.md +++ b/manual/src/reflinks.md @@ -15,6 +15,40 @@ [`Primitive`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html [`Material`]: {{DOCS_URL}}/renderling/material/struct.Material.html +[`Material::set_albedo_texture`]: {{DOCS_URL}}/renderling/material/struct.Material.html#method.set_albedo_texture +[`Material::with_normal_texture`]: {{DOCS_URL}}/renderling/material/struct.Material.html#method.with_normal_texture + +[`AtlasImage`]: {{DOCS_URL}}/renderling/atlas/struct.AtlasImage.html +[`AtlasTexture`]: {{DOCS_URL}}/renderling/atlas/struct.AtlasTexture.html +[`Stage::set_images`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_images + +[`Stage::tonemapping`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.tonemapping +[`Tonemapping`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemapping.html +[`TonemapConstants`]: {{DOCS_URL}}/renderling/tonemapping/struct.TonemapConstants.html +[`Tonemap`]: {{DOCS_URL}}/renderling/tonemapping/struct.Tonemap.html + +[`DebugChannel`]: {{DOCS_URL}}/renderling/pbr/debug/enum.DebugChannel.html +[`Stage::set_debug_mode`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_debug_mode +[`Stage::set_use_debug_overlay`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_use_debug_overlay + +[`NestedTransform`]: {{DOCS_URL}}/renderling/transform/struct.NestedTransform.html +[`NestedTransform::add_child`]: {{DOCS_URL}}/renderling/transform/struct.NestedTransform.html#method.add_child +[`Stage::new_nested_transform`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_nested_transform + +[`Animator`]: {{DOCS_URL}}/renderling/gltf/anime/struct.Animator.html +[`Stage::new_morph_targets`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_morph_targets +[`Stage::new_morph_target_weights`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_morph_target_weights +[`Primitive::set_morph_targets`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html#method.set_morph_targets + +[`Stage::set_use_frustum_culling`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_use_frustum_culling +[`Stage::set_use_occlusion_culling`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_use_occlusion_culling +[`Stage::new_light_tiling`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_light_tiling +[`LightTilingConfig`]: {{DOCS_URL}}/renderling/light/struct.LightTilingConfig.html +[`Stage::set_msaa_sample_count`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_msaa_sample_count + +[`Stage::set_ambient_color`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_ambient_color +[`Stage::with_ambient_color`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.with_ambient_color +[`Stage::ambient_color`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.ambient_color [`Mat4`]: https://docs.rs/glam/latest/glam/f32/struct.Mat4.html @@ -22,6 +56,12 @@ [`Skybox`]: {{DOCS_URL}}/renderling/skybox/struct.Skybox.html +[`ShadowMap`]: {{DOCS_URL}}/renderling/light/struct.ShadowMap.html +[`ShadowMap::update`]: {{DOCS_URL}}/renderling/light/struct.ShadowMap.html#method.update +[`ShadowMap::descriptor_lock`]: {{DOCS_URL}}/renderling/light/struct.ShadowMap.html#method.descriptor_lock +[`Stage::new_shadow_map`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_shadow_map +[`Context::with_shadow_mapping_atlas_texture_size`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.with_shadow_mapping_atlas_texture_size + [`Stage`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html [`Stage::new_camera`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_camera diff --git a/manual/src/scene.md b/manual/src/scene.md new file mode 100644 index 00000000..2ed02842 --- /dev/null +++ b/manual/src/scene.md @@ -0,0 +1,76 @@ +# Scene hierarchy 🌳 + +When objects must move together - wheels on a car, segments of a robot arm, +planets around a sun - you want a **hierarchy**: change the parent, and the +children come along for the ride. + +In `renderling` that is the [`NestedTransform`]. Each node has a **local** +transform, relative to its parent. The node's **global** transform is the +composition of every local transform up the chain, and it is recalculated +automatically whenever anything in the chain changes. + +## Building a hierarchy + +Create nodes with [`Stage::new_nested_transform`], position them with their +`local_*` builder methods, and link them with +[`NestedTransform::add_child`]. Then attach primitives with +[`Primitive::with_transform`], handing the node a reference: + +```rust,ignore +{{#include ../../crates/examples/src/scene.rs:setup}} +``` + +![a chain of three colored cubes: gray root, cyan child, yellow grandchild](assets/scene/hierarchy.png) + +Note the cubes are all attached to *nodes*, not positioned directly. The cyan +cube sits at `root * child`, and the yellow one at `root * child * grandchild`. + +## Moving the parent moves the subtree + +Because children are positioned relative to their parent, changing the parent +moves everything below it: + +```rust,ignore +{{#include ../../crates/examples/src/scene.rs:parent}} +``` + +![the whole chain rotated by 45 degrees](assets/scene/moved-parent.png) + +One rotation of the root carried the entire chain. + +## Moving a child moves only its subtree + +Change a node's local transform, and its parent stays put: + +```rust,ignore +{{#include ../../crates/examples/src/scene.rs:child}} +``` + +![the child cube and grandchild have moved up, the gray root stays put](assets/scene/moved-child.png) + +## Notes + +- **Local vs global:** the `local_*` getters and setters work in the parent's + space. To inspect the composed result, use + [`NestedTransform::global_descriptor`], or walk the whole chain with + [`NestedTransform::hierarchy`]. +- **Updates are immediate:** changing a local transform (or the graph itself) + recomputes the globals of the whole subtree right away, and the new globals + sync to the GPU on the next render. +- **Detaching:** [`NestedTransform::remove_child`] unlinks a node, which then + keeps its own transform; [`NestedTransform::parent`] returns the parent, if + any. +- **Standalone objects** that never need to follow anything can use the + simpler [`Transform`] from [`Stage::new_transform`] instead - that is what + the earlier chapters used. + +[`NestedTransform`]: {{DOCS_URL}}/renderling/transform/struct.NestedTransform.html +[`NestedTransform::add_child`]: {{DOCS_URL}}/renderling/transform/struct.NestedTransform.html#method.add_child +[`NestedTransform::remove_child`]: {{DOCS_URL}}/renderling/transform/struct.NestedTransform.html#method.remove_child +[`NestedTransform::parent`]: {{DOCS_URL}}/renderling/transform/struct.NestedTransform.html#method.parent +[`NestedTransform::global_descriptor`]: {{DOCS_URL}}/renderling/transform/struct.NestedTransform.html#method.global_descriptor +[`NestedTransform::hierarchy`]: {{DOCS_URL}}/renderling/transform/struct.NestedTransform.html#method.hierarchy +[`Primitive::with_transform`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html#method.with_transform +[`Stage::new_nested_transform`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_nested_transform +[`Stage::new_transform`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_transform +[`Transform`]: {{DOCS_URL}}/renderling/transform/struct.Transform.html \ No newline at end of file