From 7aab55c89aea6e9863542e8566b266eff036b529 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Fri, 16 Aug 2024 19:44:21 -0700 Subject: [PATCH] Correctly compute emission from semi-transparent voxels. --- .../src/in_wgpu/shaders/blocks-and-lines.wgsl | 28 +++++++--- all-is-cubes/src/raytracer.rs | 51 +++++++++++++------ 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/all-is-cubes-gpu/src/in_wgpu/shaders/blocks-and-lines.wgsl b/all-is-cubes-gpu/src/in_wgpu/shaders/blocks-and-lines.wgsl index d1077aafd..901ac9c7a 100644 --- a/all-is-cubes-gpu/src/in_wgpu/shaders/blocks-and-lines.wgsl +++ b/all-is-cubes-gpu/src/in_wgpu/shaders/blocks-and-lines.wgsl @@ -518,16 +518,28 @@ fn block_fragment_transparent_volumetric(in: BlockFragmentInput) -> @location(0) if material.reflectance.a < 1.0 { // Apply volumetric opacity adjusgment. // Convert alpha to transmittance (light transmitted / light received). - let transmittance = 1.0 - material.reflectance.a; + let unit_transmittance = 1.0 - material.reflectance.a; // Adjust transmittance for the thickness relative to an assumed 1.0 thickness. - let adj_transmittance = pow(transmittance, thickness); + let depth_transmittance = pow(unit_transmittance, thickness); // Convert back to alpha. - material.reflectance.a = 1.0 - adj_transmittance; - - // Also scale the emission based on depth. - // TODO: This abrupt change is not actually appropriate, but it's not clear what is. - // Define rules for volumetric emission that apply to all materials and all complex blocks. - material.emission = material.emission * thickness; + material.reflectance.a = 1.0 - depth_transmittance; + + // Compute how the emission should be scaled to account for internal absorption and thickness. + // Since voxel emission is defined as “emitted from the surface of a unit-thickness layer”, + // the emission per length must be *greater* the more opaque the material is, + // and yet also it is reduced the deeper we go. + // This formula is the integral of that process. + if unit_transmittance == 1.0 { + // This is the integral + // ∫{0..thickness} unit_transmittance^x dx + // = ∫{0..thickness} 1 dx + material.emission *= thickness; + } else { + // This is the integral + // ∫{0..thickness} unit_transmittance^x dx + // in the case where `unit_transmittance` is not equal to 1. + material.emission *= (depth_transmittance - 1.0) / (unit_transmittance - 1.0); + } } let light_from_lit_surface: vec3f = diff --git a/all-is-cubes/src/raytracer.rs b/all-is-cubes/src/raytracer.rs index 08ed1d6ed..2343d3346 100644 --- a/all-is-cubes/src/raytracer.rs +++ b/all-is-cubes/src/raytracer.rs @@ -583,21 +583,20 @@ impl TracingState

{ ((exit_t_distance - surface.t_distance) * self.t_to_absolute_distance) as f32; // Adjust colors for the thickness - surface.diffuse_color = apply_transmittance(surface.diffuse_color, thickness); - // TODO: This abrupt change is not actually appropriate, but it's not clear what is. - // Define rules for volumetric emission. - if surface.diffuse_color.alpha() != 1.0 { - surface.emission = surface.emission * thickness; - } + let (adjusted_color, emission_coeff) = + apply_transmittance(surface.diffuse_color, thickness); + surface.diffuse_color = adjusted_color; + surface.emission = surface.emission * emission_coeff; self.trace_through_surface(&surface, rt); } } -/// Given the alpha of a voxel color, and the thickness of that material passed through, -/// return the alpha that should be used for blending. +/// Given an `Atom`/`Evoxel` color, and the thickness of that material passed through, +/// return the effective alpha that should replace the original, and the coefficient for +/// scaling the light emission. #[inline] -fn apply_transmittance(color: Rgba, thickness: f32) -> Rgba { +fn apply_transmittance(color: Rgba, thickness: f32) -> (Rgba, f32) { // Convert alpha to transmittance (light transmitted / light received). let unit_transmittance = 1.0 - color.clamp().alpha().into_inner(); // Adjust transmittance for the thickness relative to an assumed 1.0 thickness. @@ -606,8 +605,26 @@ fn apply_transmittance(color: Rgba, thickness: f32) -> Rgba { // TODO: skip NaN check ... this may require refactoring Surface usage. // We might also benefit from an "UncheckedRgba" concept. let alpha = NotNan::new(1.0 - depth_transmittance).unwrap(); - - color.to_rgb().with_alpha(alpha) + let modified_color = color.to_rgb().with_alpha(alpha); + + // Compute how the emission should be scaled to account for internal absorption and thickness. + // Since voxel emission is defined as “emitted from the surface of a unit-thickness layer”, + // the emission per length must be *greater* the more opaque the material is, + // and yet also it is reduced the deeper we go. + // This formula is the integral of that process. + let emission_coeff = if unit_transmittance == 1.0 { + // This is the integral + // ∫{0..thickness} unit_transmittance^x dx + // = ∫{0..thickness} 1 dx + thickness + } else { + // This is the integral + // ∫{0..thickness} unit_transmittance^x dx + // in the case where `unit_transmittance` is not equal to 1. + (depth_transmittance - 1.) / (unit_transmittance - 1.) + }; + + (modified_color, emission_coeff) } /// Minimal raytracing helper used by block evaluation to compute aggregate properties @@ -631,8 +648,9 @@ pub(crate) fn trace_for_eval( let mut emission = Vector3D::zero(); while let Some(voxel) = voxels.get(cube) { - emission += Vector3D::from(voxel.emission) * color_buf.transmittance; - color_buf.add(apply_transmittance(voxel.color, thickness).into(), &()); + let (adjusted_color, emission_coeff) = apply_transmittance(voxel.color, thickness); + emission += Vector3D::from(voxel.emission * emission_coeff) * color_buf.transmittance; + color_buf.add(adjusted_color.into(), &()); if color_buf.opaque() { break; @@ -691,15 +709,18 @@ mod tests { #[test] fn apply_transmittance_identity() { let color = rgba_const!(1.0, 0.5, 0.0, 0.5); - assert_eq!(apply_transmittance(color, 1.0), color); + assert_eq!(apply_transmittance(color, 1.0), (color, 1.0)); } /// `apply_transmittance` + `ColorBuf` accumulation should add up to the identity function for /// any unit thickness (except for rounding error, which we are avoiding for this test case). + /// + /// TODO: test emission equivalence too #[test] fn apply_transmittance_equivalence() { fn case(color: Rgba, count: usize) { - let modified_color = apply_transmittance(color, (count as f32).recip()); + let (modified_color, _emission_coeff) = + apply_transmittance(color, (count as f32).recip()); let mut color_buf = ColorBuf::default(); for _ in 0..count { color_buf.add(modified_color.into(), &());