Skip to content

Commit

Permalink
Compute EvaluatedBlock::color via raytracing.
Browse files Browse the repository at this point in the history
This computes a more accurate color for the exterior of a block,
disregarding interior voxels that are trivially hidden by the outside
surfaces. It does not produce a true average color from all viewing
angles, since only axis-aligned rays are considered, but it should
produce good results for typical content.

Note that render tests' output changes because the color values are
used in light transport calculations.

A further improvement might be to split the color into six separate
face colors. However, that's fraught because it suggests doing the same
to `Evoxel`, which is a can of worms for the volumetric interpretation.
  • Loading branch information
kpreid committed Aug 15, 2023
1 parent 168f659 commit b050a10
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[?25l ▄▄▄▄▄▄▄▄▄
 ▄▄▄▄▄▄▄▄▄
[?25l ▄▄▄▄▄▄▄▄▄
  ▄▄▄▄▄▄▄▄
 
 
 
Expand Down Expand Up @@ -37,5 +37,5 @@
 
 
 ▄ ▄ ▄ ▄ ▄ ▄ 
 ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ 
 ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ 
[?25h[?1006l[?1015l[?1003l[?1002l[?1000l
28 changes: 14 additions & 14 deletions all-is-cubes-port/src/gltf/tests/export_block_defs.gltf
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
"componentType": 5126,
"type": "VEC4",
"min": [
0.008904562331736088,
0.00849095918238163,
0.010273359715938568,
0.042881786823272705,
0.04067595675587654,
0.050832804292440414,
1.0
],
"max": [
0.008904562331736088,
0.00849095918238163,
0.010273359715938568,
0.042881786823272705,
0.04067595675587654,
0.050832804292440414,
1.0
],
"name": "'block0' color"
Expand Down Expand Up @@ -71,15 +71,15 @@
"componentType": 5126,
"type": "VEC4",
"min": [
0.9575070142745972,
0.9570935368537903,
0.9588758945465088,
0.8732726573944092,
0.8710667490959167,
0.8812239170074463,
1.0
],
"max": [
0.9575070142745972,
0.9570935368537903,
0.9588758945465088,
0.8732726573944092,
0.8710667490959167,
0.8812239170074463,
1.0
],
"name": "'block1' color"
Expand All @@ -101,12 +101,12 @@
{
"byteLength": 744,
"name": "'block0' data",
"uri": "data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAceQRPKodCzyYUSg8AACAPwAAAAAAAAAAAACAP3HkETyqHQs8mFEoPAAAgD8AAAAAAACAPwAAAABx5BE8qh0LPJhRKDwAAIA/AAAAAAAAgD8AAIA/ceQRPKodCzyYUSg8AACAPwAAAAAAAAAAAAAAAHHkETyqHQs8mFEoPAAAgD8AAIA/AAAAAAAAAABx5BE8qh0LPJhRKDwAAIA/AAAAAAAAAAAAAIA/ceQRPKodCzyYUSg8AACAPwAAgD8AAAAAAACAP3HkETyqHQs8mFEoPAAAgD8AAAAAAAAAAAAAAABx5BE8qh0LPJhRKDwAAIA/AAAAAAAAgD8AAAAAceQRPKodCzyYUSg8AACAPwAAgD8AAAAAAAAAAHHkETyqHQs8mFEoPAAAgD8AAIA/AACAPwAAAABx5BE8qh0LPJhRKDwAAIA/AACAPwAAgD8AAAAAceQRPKodCzyYUSg8AACAPwAAgD8AAIA/AACAP3HkETyqHQs8mFEoPAAAgD8AAIA/AAAAAAAAAABx5BE8qh0LPJhRKDwAAIA/AACAPwAAAAAAAIA/ceQRPKodCzyYUSg8AACAPwAAgD8AAIA/AAAAAHHkETyqHQs8mFEoPAAAgD8AAAAAAACAPwAAAABx5BE8qh0LPJhRKDwAAIA/AACAPwAAgD8AAIA/ceQRPKodCzyYUSg8AACAPwAAAAAAAIA/AACAP3HkETyqHQs8mFEoPAAAgD8AAAAAAACAPwAAgD9x5BE8qh0LPJhRKDwAAIA/AAAAAAAAAAAAAIA/ceQRPKodCzyYUSg8AACAPwAAgD8AAIA/AACAP3HkETyqHQs8mFEoPAAAgD8AAIA/AAAAAAAAgD9x5BE8qh0LPJhRKDwAAIA/AAABAAIAAgABAAMABAAFAAYABgAFAAcACAAJAAoACgAJAAsADAANAA4ADgANAA8AEAARABIAEgARABMAFAAVABYAFgAVABcA"
"uri": "data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAA0KQvPdWbJj0PNlA9AACAPwAAAAAAAAAAAACAP9CkLz3VmyY9DzZQPQAAgD8AAAAAAACAPwAAAADQpC891ZsmPQ82UD0AAIA/AAAAAAAAgD8AAIA/0KQvPdWbJj0PNlA9AACAPwAAAAAAAAAAAAAAANCkLz3VmyY9DzZQPQAAgD8AAIA/AAAAAAAAAADQpC891ZsmPQ82UD0AAIA/AAAAAAAAAAAAAIA/0KQvPdWbJj0PNlA9AACAPwAAgD8AAAAAAACAP9CkLz3VmyY9DzZQPQAAgD8AAAAAAAAAAAAAAADQpC891ZsmPQ82UD0AAIA/AAAAAAAAgD8AAAAA0KQvPdWbJj0PNlA9AACAPwAAgD8AAAAAAAAAANCkLz3VmyY9DzZQPQAAgD8AAIA/AACAPwAAAADQpC891ZsmPQ82UD0AAIA/AACAPwAAgD8AAAAA0KQvPdWbJj0PNlA9AACAPwAAgD8AAIA/AACAP9CkLz3VmyY9DzZQPQAAgD8AAIA/AAAAAAAAAADQpC891ZsmPQ82UD0AAIA/AACAPwAAAAAAAIA/0KQvPdWbJj0PNlA9AACAPwAAgD8AAIA/AAAAANCkLz3VmyY9DzZQPQAAgD8AAAAAAACAPwAAAADQpC891ZsmPQ82UD0AAIA/AACAPwAAgD8AAIA/0KQvPdWbJj0PNlA9AACAPwAAAAAAAIA/AACAP9CkLz3VmyY9DzZQPQAAgD8AAAAAAACAPwAAgD/QpC891ZsmPQ82UD0AAIA/AAAAAAAAAAAAAIA/0KQvPdWbJj0PNlA9AACAPwAAgD8AAIA/AACAP9CkLz3VmyY9DzZQPQAAgD8AAIA/AAAAAAAAgD/QpC891ZsmPQ82UD0AAIA/AAABAAIAAgABAAMABAAFAAYABgAFAAcACAAJAAoACgAJAAsADAANAA4ADgANAA8AEAARABIAEgARABMAFAAVABYAFgAVABcA"
},
{
"byteLength": 744,
"name": "'block1' data",
"uri": "data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAALh91PxUEdT/keHU/AACAPwAAAAAAAAAAAACAPy4fdT8VBHU/5Hh1PwAAgD8AAAAAAACAPwAAAAAuH3U/FQR1P+R4dT8AAIA/AAAAAAAAgD8AAIA/Lh91PxUEdT/keHU/AACAPwAAAAAAAAAAAAAAAC4fdT8VBHU/5Hh1PwAAgD8AAIA/AAAAAAAAAAAuH3U/FQR1P+R4dT8AAIA/AAAAAAAAAAAAAIA/Lh91PxUEdT/keHU/AACAPwAAgD8AAAAAAACAPy4fdT8VBHU/5Hh1PwAAgD8AAAAAAAAAAAAAAAAuH3U/FQR1P+R4dT8AAIA/AAAAAAAAgD8AAAAALh91PxUEdT/keHU/AACAPwAAgD8AAAAAAAAAAC4fdT8VBHU/5Hh1PwAAgD8AAIA/AACAPwAAAAAuH3U/FQR1P+R4dT8AAIA/AACAPwAAgD8AAAAALh91PxUEdT/keHU/AACAPwAAgD8AAIA/AACAPy4fdT8VBHU/5Hh1PwAAgD8AAIA/AAAAAAAAAAAuH3U/FQR1P+R4dT8AAIA/AACAPwAAAAAAAIA/Lh91PxUEdT/keHU/AACAPwAAgD8AAIA/AAAAAC4fdT8VBHU/5Hh1PwAAgD8AAAAAAACAPwAAAAAuH3U/FQR1P+R4dT8AAIA/AACAPwAAgD8AAIA/Lh91PxUEdT/keHU/AACAPwAAAAAAAIA/AACAPy4fdT8VBHU/5Hh1PwAAgD8AAAAAAACAPwAAgD8uH3U/FQR1P+R4dT8AAIA/AAAAAAAAAAAAAIA/Lh91PxUEdT/keHU/AACAPwAAgD8AAIA/AACAPy4fdT8VBHU/5Hh1PwAAgD8AAIA/AAAAAAAAgD8uH3U/FQR1P+R4dT8AAIA/AAABAAIAAgABAAMABAAFAAYABgAFAAcACAAJAAoACgAJAAsADAANAA4ADgANAA8AEAARABIAEgARABMAFAAVABYAFgAVABcA"
"uri": "data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAzI5fPzv+Xj/kl2E/AACAPwAAAAAAAAAAAACAP8yOXz87/l4/5JdhPwAAgD8AAAAAAACAPwAAAADMjl8/O/5eP+SXYT8AAIA/AAAAAAAAgD8AAIA/zI5fPzv+Xj/kl2E/AACAPwAAAAAAAAAAAAAAAMyOXz87/l4/5JdhPwAAgD8AAIA/AAAAAAAAAADMjl8/O/5eP+SXYT8AAIA/AAAAAAAAAAAAAIA/zI5fPzv+Xj/kl2E/AACAPwAAgD8AAAAAAACAP8yOXz87/l4/5JdhPwAAgD8AAAAAAAAAAAAAAADMjl8/O/5eP+SXYT8AAIA/AAAAAAAAgD8AAAAAzI5fPzv+Xj/kl2E/AACAPwAAgD8AAAAAAAAAAMyOXz87/l4/5JdhPwAAgD8AAIA/AACAPwAAAADMjl8/O/5eP+SXYT8AAIA/AACAPwAAgD8AAAAAzI5fPzv+Xj/kl2E/AACAPwAAgD8AAIA/AACAP8yOXz87/l4/5JdhPwAAgD8AAIA/AAAAAAAAAADMjl8/O/5eP+SXYT8AAIA/AACAPwAAAAAAAIA/zI5fPzv+Xj/kl2E/AACAPwAAgD8AAIA/AAAAAMyOXz87/l4/5JdhPwAAgD8AAAAAAACAPwAAAADMjl8/O/5eP+SXYT8AAIA/AACAPwAAgD8AAIA/zI5fPzv+Xj/kl2E/AACAPwAAAAAAAIA/AACAP8yOXz87/l4/5JdhPwAAgD8AAAAAAACAPwAAgD/Mjl8/O/5eP+SXYT8AAIA/AAAAAAAAAAAAAIA/zI5fPzv+Xj/kl2E/AACAPwAAgD8AAIA/AACAP8yOXz87/l4/5JdhPwAAgD8AAIA/AAAAAAAAgD/Mjl8/O/5eP+SXYT8AAIA/AAABAAIAAgABAAMABAAFAAYABgAFAAcACAAJAAoACgAJAAsADAANAA4ADgANAA8AEAARABIAEgARABMAFAAVABYAFgAVABcA"
}
],
"bufferViews": [
Expand Down
101 changes: 84 additions & 17 deletions all-is-cubes/src/block/evaluated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ use crate::block::{
Resolution::{self, R1},
};
use crate::content::palette;
use crate::math::Face6;
use crate::math::{FaceMap, GridAab, GridArray, GridPoint, OpacityCategory, Rgb, Rgba};
use crate::raytracer;
use crate::universe::RefError;

// Things mentioned in doc comments only
Expand Down Expand Up @@ -139,23 +141,64 @@ impl EvaluatedBlock {
let full_block_bounds = GridAab::for_block(resolution);
let less_than_full = full_block_bounds != voxels.bounds();

// Compute color sum from voxels
// TODO: Give GridArray an iter() or something
// TODO: The color sum actually needs to be weighted by alpha. (Too bad we're not using premultiplied alpha.)
// TODO: Should not be counting interior voxels for the color, only visible surfaces.
let (color, uniform_collision) = {
// Compute color sum from voxels.
// This is actually a sort of mini-raytracer, in that it computes the appearance
// of all six faces by tracing in from the edges, and then averages them.
// TODO: Account for reduced bounds being smaller
let color: Rgba = {
let mut color_sum: Vector4<f32> = Vector4::zero();
let mut count = 0;
// Loop over all face voxels.
// (This is a similar structure to the algorithm we use for mesh generation.)
for face in Face6::ALL {
let transform = face.face_transform(resolution.into());
let rotated_voxel_range = voxels.bounds().transform(transform.inverse()).unwrap();

for v in rotated_voxel_range.y_range() {
for u in rotated_voxel_range.x_range() {
let cube: GridPoint = transform.transform_cube(GridPoint::new(
u,
v,
rotated_voxel_range.z_range().start,
));
debug_assert!(voxels.bounds().contains_cube(cube));

let buf = raytracer::trace_axis_aligned::<raytracer::ColorBuf>(
&voxels,
cube,
face.opposite(),
resolution,
);
color_sum += Rgba::from(buf).into();
count += 1;
}
}
}
if count == 0 {
Rgba::TRANSPARENT
} else {
// Note the divisors —- this adds transparency to compensate for when the
// voxel data doesn't cover the full_block_bounds.
Rgba::try_from(
(color_sum.truncate() / (count as f32))
.extend(color_sum.w / (full_block_bounds.surface_area() as f32)),
)
.expect("Recursive block color computation produced NaN")
}
};

// Compute if the collision is uniform in all voxels.
let uniform_collision = {
let mut collision: Option<BlockCollision> = if less_than_full {
Some(BlockCollision::None)
} else {
None
};
let mut collision_unequal = false;
// TODO: use GridArray iter
for position in voxels.bounds().interior_iter() {
let voxel: Evoxel = voxels[position];

color_sum += voxel.color.into();

match (collision, collision_unequal) {
// Already unequal
(_, true) => {}
Expand All @@ -170,14 +213,8 @@ impl EvaluatedBlock {
}
}
}
(
Rgba::try_from(
(color_sum.truncate() / (voxels.bounds().volume().max(1) as f32))
.extend(color_sum.w / (full_block_bounds.volume() as f32)),
)
.expect("Recursive block color computation produced NaN"),
collision,
)

collision
};

let visible = voxels.bounds().interior_iter().any(
Expand All @@ -198,11 +235,11 @@ impl EvaluatedBlock {

EvaluatedBlock {
attributes,
// The single color is the mean of the actual block colors.
color,
opaque: FaceMap::from_fn(|face| {
// TODO: This test should be refined by flood-filling in from the face,
// so that we can also consider a face opaque if it has hollows/engravings.
// Merge this with the raytracer above.
let surface_volume = full_block_bounds.abut(face, -1).unwrap();
if surface_volume.intersection(voxels.bounds()) == Some(surface_volume) {
surface_volume.interior_iter().all(
Expand Down Expand Up @@ -607,8 +644,38 @@ mod tests {
voxel_opacity_mask: ev_one.voxel_opacity_mask.clone(),
..ev_many
},
ev_one
ev_one,
"Input color {color:?}"
);
}
}

/// Test that interior color is hidden by surface color.
///
/// TODO: This test is irregular because it bypasses constructing a `Block`, but
/// this is convenient, but it doesn't match other tests in `crate::block`. What style
/// should we use?
#[test]
fn overall_color_ignores_interior() {
let resolution = Resolution::R8;
let outer_bounds = GridAab::for_block(resolution);
let inner_bounds = outer_bounds.expand(FaceMap::repeat(-1));
let outer_color = Rgba::new(1.0, 0.0, 0.0, 1.0);
let inner_color = Rgba::new(0.0, 1.0, 0.0, 1.0);
let voxels = Evoxels::Many(
resolution,
GridArray::from_fn(outer_bounds, |p| {
Evoxel::from_color(if inner_bounds.contains_cube(p) {
inner_color
} else {
outer_color
})
}),
);

// The inner_color should be ignored because it is not visible.
let ev = EvaluatedBlock::from_voxels(BlockAttributes::default(), voxels);

assert_eq!(ev.color, outer_color);
}
}
2 changes: 1 addition & 1 deletion all-is-cubes/src/block/modifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ mod tests {
re,
EvaluatedBlock {
attributes: oe.attributes,
color: rgba_const!(0.5, 0.5, 0.5, 0.5),
color: Rgba::new(1. / 3., 0., 1. / 3., 2. / 3.),
voxels: Evoxels::Many(
R2,
GridArray::from_fn(block_bounds, |cube| {
Expand Down
5 changes: 3 additions & 2 deletions all-is-cubes/src/block/modifier/move.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ impl universe::VisitRefs for Move {
#[cfg(test)]
mod tests {
use cgmath::EuclideanSpace;
use ordered_float::NotNan;

use crate::block::{Block, Composite, EvaluatedBlock, Evoxel, Resolution::*};
use crate::content::make_some_blocks;
Expand Down Expand Up @@ -223,7 +224,7 @@ mod tests {
moved.evaluate().unwrap(),
EvaluatedBlock {
attributes: ev_original.attributes.clone(),
color: color.to_rgb().with_alpha(notnan!(0.5)),
color: color.to_rgb().with_alpha(NotNan::new(2. / 3.).unwrap()),
voxels: Evoxels::Many(
R16,
GridArray::repeat(expected_bounds, Evoxel::from_block(&ev_original))
Expand Down Expand Up @@ -262,7 +263,7 @@ mod tests {
moved.evaluate().unwrap(),
EvaluatedBlock {
attributes: ev_original.attributes.clone(),
color: color.to_rgb().with_alpha(notnan!(0.5)),
color: color.to_rgb().with_alpha(NotNan::new(2. / 3.).unwrap()),
voxels: Evoxels::Many(
resolution,
GridArray::repeat(expected_bounds, Evoxel::from_block(&ev_original))
Expand Down
24 changes: 17 additions & 7 deletions all-is-cubes/src/block/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,15 @@ fn evaluate_voxels_checked_individually() {
fn evaluate_transparent_voxels() {
let mut universe = Universe::new();
let resolution = R4;
let alpha = 0.5;
let block = Block::builder()
.voxels_fn(&mut universe, resolution, |point| {
Block::from(Rgba::new(
0.0,
0.0,
0.0,
if point == GridPoint::new(0, 0, 0) {
0.5
if point.x == 0 && point.z == 0 {
alpha
} else {
1.0
},
Expand All @@ -249,9 +250,17 @@ fn evaluate_transparent_voxels() {
.build();

let e = block.evaluate().unwrap();
// Transparency is (currently) computed by an orthographic view through all six
// faces, and only two out of six faces in this test block don't fully cover
// the light paths with opaque surfaces.
assert_eq!(
e.color,
Rgba::new(0.0, 0.0, 0.0, 1.0 - (0.5 / f32::from(resolution).powi(3)))
Rgba::new(
0.0,
0.0,
0.0,
1.0 - (alpha / (f32::from(resolution).powi(2) * 3.0))
)
);
assert_eq!(
e.opaque,
Expand All @@ -260,15 +269,15 @@ fn evaluate_transparent_voxels() {
ny: false,
nz: false,
px: true,
py: true,
py: false,
pz: true,
}
);
assert_eq!(e.visible, true);
}

#[test]
fn evaluate_voxels_not_filling_block() {
fn evaluate_voxels_full_but_transparent() {
let resolution = R4;
let mut universe = Universe::new();
let block = Block::builder()
Expand All @@ -290,7 +299,7 @@ fn evaluate_voxels_not_filling_block() {
let e = block.evaluate().unwrap();
assert_eq!(
e.color,
Rgba::new(0.0, 0.0, 0.0, 1.0 / f32::from(resolution).powi(3))
Rgba::new(0.0, 0.0, 0.0, 1.0 / f32::from(resolution).powi(2))
);
assert_eq!(e.resolution(), resolution);
assert_eq!(e.opaque, FaceMap::repeat(false));
Expand All @@ -313,7 +322,8 @@ fn evaluate_voxels_partial_not_filling() {
.build();

let e = block.evaluate().unwrap();
assert_eq!(e.color, Rgba::new(1.0, 1.0, 1.0, 0.5));
// of 6 faces, 2 are opaque and 2 are half-transparent, thus there are 8 opaque half-faces.
assert_eq!(e.color, Rgba::new(1.0, 1.0, 1.0, 8./12.));
assert_eq!(e.resolution(), resolution);
assert_eq!(e.opaque, FaceMap::repeat(false).with(Face6::NX, true));
assert_eq!(e.visible, true);
Expand Down
7 changes: 7 additions & 0 deletions all-is-cubes/src/math/grid_aab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ impl GridAab {
Self::checked_volume_helper(self.sizes).unwrap()
}

pub(crate) fn surface_area(&self) -> usize {
// can't fail because it would fail the volume check
let size = self.sizes.cast::<usize>().unwrap();

size.x * size.y * 2 + size.x * size.z * 2 + size.y * size.z * 2
}

/// Returns whether the box contains no cubes (its volume is zero).
#[inline]
pub fn is_empty(&self) -> bool {
Expand Down
31 changes: 30 additions & 1 deletion all-is-cubes/src/raytracer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use ordered_float::NotNan;
#[cfg(feature = "threads")]
use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _};

use crate::block::{Evoxels, AIR};
use crate::block::{Evoxels, Resolution, AIR};
use crate::camera::{Camera, GraphicsOptions, TransparencyOption};
use crate::math::{
point_to_enclosing_cube, smoothstep, Face6, Face7, FreeCoordinate, GridAab, GridArray,
Expand Down Expand Up @@ -573,6 +573,35 @@ fn apply_transmittance(color: Rgba, thickness: f32) -> Rgba {
color.to_rgb().with_alpha(alpha)
}

/// Minimal raytracing helper used by block evaluation to compute aggregate properties
/// of voxel blocks. Compared to the regular raytracer, it:
///
/// * Traces through `Evoxel`s instead of a `SpaceRaytracer`.
/// * Follows an axis-aligned ray only.
///
/// `origin` should be the first cube to trace through *within* the grid.
pub(crate) fn trace_axis_aligned<P: PixelBuf<BlockData = ()>>(
voxels: &Evoxels,
origin: GridPoint,
direction: Face6,
resolution: Resolution,
) -> P {
let thickness = f32::from(resolution).recip();
let step = direction.normal_vector();

let mut cube = origin;
let mut buf = P::default();

while let Some(voxel) = voxels.get(cube) {
buf.add(apply_transmittance(voxel.color, thickness), &());
if buf.opaque() {
break;
}
cube += step;
}
buf
}

#[cfg(feature = "threads")]
mod rayon_helper {
use rayon::iter::{IntoParallelIterator, ParallelExtend, ParallelIterator as _};
Expand Down
Binary file modified test-renderers/expected/icons-all.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b050a10

Please sign in to comment.