From 2e9f6c6c4638992b0839deec5fe6922b65aa991a Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Sat, 4 Nov 2023 09:36:47 -0700 Subject: [PATCH] mesh: Add `Channels` property to texture allocations. This will allow storing and therefore rendering per-voxel light emission, and possibly other properties in the future, without requiring all texture allocations to store it even if zero. For now, none of the mesh users support emission; this is just the logic for asking for it. --- all-is-cubes-gpu/src/in_wgpu/block_texture.rs | 12 ++- all-is-cubes-mesh/src/block_mesh.rs | 8 +- all-is-cubes-mesh/src/testing.rs | 19 ++-- all-is-cubes-mesh/src/texture.rs | 89 +++++++++++++++---- all-is-cubes-port/src/gltf/texture.rs | 17 +++- 5 files changed, 117 insertions(+), 28 deletions(-) diff --git a/all-is-cubes-gpu/src/in_wgpu/block_texture.rs b/all-is-cubes-gpu/src/in_wgpu/block_texture.rs index 9307ecd40..8f5c26b3c 100644 --- a/all-is-cubes-gpu/src/in_wgpu/block_texture.rs +++ b/all-is-cubes-gpu/src/in_wgpu/block_texture.rs @@ -271,7 +271,13 @@ impl texture::Allocator for AtlasAllocator { type Tile = AtlasTile; type Point = TexPoint; - fn allocate(&self, requested_bounds: GridAab) -> Option { + fn allocate( + &self, + requested_bounds: GridAab, + _channels: texture::Channels, + ) -> Option { + // TODO: implement channels + let mut allocator_backing = self.backing.lock().unwrap(); // If alloctree grows, the next flush() will take care of reallocating the texture. @@ -307,6 +313,10 @@ impl texture::Tile for AtlasTile { self.requested_bounds } + fn channels(&self) -> texture::Channels { + texture::Channels::Reflectance + } + fn slice(&self, requested_bounds: GridAab) -> Self::Plane { texture::validate_slice(self.requested_bounds, requested_bounds); AtlasPlane { diff --git a/all-is-cubes-mesh/src/block_mesh.rs b/all-is-cubes-mesh/src/block_mesh.rs index 866ce8a40..0899a9301 100644 --- a/all-is-cubes-mesh/src/block_mesh.rs +++ b/all-is-cubes-mesh/src/block_mesh.rs @@ -206,7 +206,11 @@ impl BlockMesh { voxel_opacity_mask: Some(new_mask), .. }, - ) if old_mask == new_mask => { + ) if old_mask == new_mask + && existing_texture + .channels() + .is_superset_of(texture::needed_channels(&block.voxels)) => + { existing_texture.write(voxels.as_vol_ref()); true } @@ -478,7 +482,7 @@ impl BlockMesh { if texture_plane_if_needed.is_none() { if texture_if_needed.is_none() { // Try to compute texture (might fail) - texture_if_needed = texture::copy_voxels_to_texture( + texture_if_needed = texture::copy_voxels_to_new_texture( texture_allocator, voxels, ); diff --git a/all-is-cubes-mesh/src/testing.rs b/all-is-cubes-mesh/src/testing.rs index dc9582a9a..270ed7997 100644 --- a/all-is-cubes-mesh/src/testing.rs +++ b/all-is-cubes-mesh/src/testing.rs @@ -93,7 +93,7 @@ impl texture::Allocator for Allocator { type Tile = Tile; type Point = TexPoint; - fn allocate(&self, bounds: GridAab) -> Option { + fn allocate(&self, bounds: GridAab, channels: texture::Channels) -> Option { self.count_allocated .fetch_update(SeqCst, SeqCst, |count| { if count < self.capacity { @@ -104,7 +104,7 @@ impl texture::Allocator for Allocator { }) .ok() .map(|_| ())?; - Some(Tile { bounds }) + Some(Tile { bounds, channels }) } } @@ -114,6 +114,7 @@ impl texture::Allocator for Allocator { #[derive(Clone, Debug, Eq, PartialEq)] pub struct Tile { bounds: GridAab, + channels: texture::Channels, } impl texture::Tile for Tile { @@ -125,6 +126,10 @@ impl texture::Tile for Tile { self.bounds } + fn channels(&self) -> texture::Channels { + self.channels + } + fn slice(&self, bounds: GridAab) -> Self::Plane { texture::validate_slice(self.bounds, bounds); self.clone() @@ -153,7 +158,7 @@ pub type TexPoint = Point3D; #[cfg(test)] mod tests { use super::*; - use crate::texture::Allocator as _; + use crate::texture::{Allocator as _, Channels}; use all_is_cubes::block::Resolution::*; /// Test the test [`Allocator`]. @@ -162,11 +167,11 @@ mod tests { let bounds = GridAab::for_block(R8); let mut allocator = Allocator::new(); assert_eq!(allocator.count_allocated(), 0); - assert!(allocator.allocate(bounds).is_some()); - assert!(allocator.allocate(bounds).is_some()); + assert!(allocator.allocate(bounds, Channels::Reflectance).is_some()); + assert!(allocator.allocate(bounds, Channels::Reflectance).is_some()); assert_eq!(allocator.count_allocated(), 2); allocator.set_capacity(3); - assert!(allocator.allocate(bounds).is_some()); - assert!(allocator.allocate(bounds).is_none()); + assert!(allocator.allocate(bounds, Channels::Reflectance).is_some()); + assert!(allocator.allocate(bounds, Channels::Reflectance).is_none()); } } diff --git a/all-is-cubes-mesh/src/texture.rs b/all-is-cubes-mesh/src/texture.rs index 6fa9c008a..8626a00f9 100644 --- a/all-is-cubes-mesh/src/texture.rs +++ b/all-is-cubes-mesh/src/texture.rs @@ -5,7 +5,7 @@ use std::fmt; use all_is_cubes::block::{Evoxel, Evoxels}; use all_is_cubes::content::palette; use all_is_cubes::euclid::Point3D; -use all_is_cubes::math::{Axis, Cube, GridAab, Vol}; +use all_is_cubes::math::{Axis, Cube, GridAab, Rgb, Vol}; use all_is_cubes::util::{ConciseDebug, Fmt}; #[cfg(doc)] @@ -45,12 +45,17 @@ pub trait Allocator { /// Allocate a tile, whose range of texels will be reserved for use as long as the /// [`Tile`] value, and its clones, are not dropped. /// - /// The given [`GridAab`] specifies the desired size of the allocation; - /// its translation does not affect the size but may be used to make the resulting - /// texture coordinate transformation convenient for the caller. + /// * `bounds` specifies the desired size of the allocation; + /// its translation does not affect the size but may be used to make the resulting + /// texture coordinate transformation convenient for the caller. + /// * `channels` specifies what types of data the texture should capture from the + /// [`Evoxel`]s that will be provided later to [`Tile::write()`]. + /// The allocator may choose to ignore some channels if this suits the + /// limitations of the intended rendering; an allocation should not fail due to + /// unsupported channels. /// /// Returns [`None`] if no space is available for another region. - fn allocate(&self, bounds: GridAab) -> Option; + fn allocate(&self, bounds: GridAab, channels: Channels) -> Option; } /// 3D texture volume provided by an [`Allocator`] to paint a block's voxels in. @@ -73,6 +78,14 @@ pub trait Tile: Clone + PartialEq { /// Returns the [`GridAab`] originally passed to the texture allocator for this tile. fn bounds(&self) -> GridAab; + /// Returns the [`Channels`] that this tile is capable of storing or intentionally discarding. + /// This should be equal to, or a superset of, the [`Channels`] requested when the texture was + /// allocated. + /// + /// This will be used by mesh algorithms to avoid reallocating unless necessary when new texel + /// data is to be displayed. + fn channels(&self) -> Channels; + /// Returns a [`Plane`] instance referring to some 2D slice of this 3D texture volume. /// /// `bounds` specifies the region to be sliced and must have a size of 1 in at least @@ -116,24 +129,50 @@ impl Allocator for &T { type Tile = T::Tile; type Point = T::Point; #[mutants::skip] // trivial - fn allocate(&self, bounds: GridAab) -> Option { - ::allocate(self, bounds) + fn allocate(&self, bounds: GridAab, channels: Channels) -> Option { + ::allocate(self, bounds, channels) } } impl Allocator for std::sync::Arc { type Tile = T::Tile; type Point = T::Point; #[mutants::skip] // trivial - fn allocate(&self, bounds: GridAab) -> Option { - ::allocate(self, bounds) + fn allocate(&self, bounds: GridAab, channels: Channels) -> Option { + ::allocate(self, bounds, channels) } } impl Allocator for std::rc::Rc { type Tile = T::Tile; type Point = T::Point; #[mutants::skip] // trivial - fn allocate(&self, bounds: GridAab) -> Option { - ::allocate(self, bounds) + fn allocate(&self, bounds: GridAab, channels: Channels) -> Option { + ::allocate(self, bounds, channels) + } +} + +/// Specifies a combination of data stored per texel that may be requested of an [`Allocator`]. +/// +/// Design note: This is an `enum` rather than a bitmask so that allocators and shaders do not +/// have to support a large number of cases, but only typical ones. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[allow(clippy::exhaustive_enums)] +pub enum Channels { + /// RGBA color (or perhaps RGB if the target does not support transparency) representing + /// reflectance. + Reflectance, + /// Reflectance as defined above and also RGB light emission. + ReflectanceEmission, +} + +impl Channels { + /// Returns whether `self` can store everything that `other` can. + pub(crate) fn is_superset_of(self, other: Self) -> bool { + use Channels::*; + match (self, other) { + (ReflectanceEmission, _) => true, + (Reflectance, Reflectance) => true, + (Reflectance, ReflectanceEmission) => false, + } } } @@ -145,7 +184,7 @@ impl Allocator for std::rc::Rc { /// (i.e. Z is preferred). /// * If invalid, panic. /// -/// This function may be useful to [`Tile`] implementors. +/// This function may be useful to [`Tile::slice()`] implementors. #[track_caller] pub fn validate_slice(tile_bounds: GridAab, slice_bounds: GridAab) -> Axis { assert!( @@ -160,18 +199,34 @@ pub fn validate_slice(tile_bounds: GridAab, slice_bounds: GridAab) -> Axis { } } -pub(super) fn copy_voxels_to_texture( +pub(super) fn copy_voxels_to_new_texture( texture_allocator: &A, voxels: &Evoxels, ) -> Option { texture_allocator - .allocate(voxels.bounds()) + .allocate(voxels.bounds(), needed_channels(voxels)) .map(|mut texture| { texture.write(voxels.as_vol_ref()); texture }) } +/// Determine which [`Channels`] are necessary to store all relevant characteristics of the block. +pub(super) fn needed_channels(voxels: &Evoxels) -> Channels { + // This has false positives because it includes obscured voxels, but that is probably not + // worth fixing with a more complex algorithm. + if voxels + .as_vol_ref() + .as_linear() + .iter() + .any(|voxel| voxel.emission != Rgb::ZERO) + { + Channels::ReflectanceEmission + } else { + Channels::Reflectance + } +} + /// Helper function to implement the typical case of copying voxels into an X-major, sRGB, RGBA /// texture. #[doc(hidden)] @@ -209,7 +264,7 @@ impl Allocator for NoTextures { type Tile = NoTexture; type Point = NoTexture; - fn allocate(&self, _: GridAab) -> Option { + fn allocate(&self, _: GridAab, _: Channels) -> Option { None } } @@ -230,6 +285,10 @@ impl Tile for NoTexture { match *self {} } + fn channels(&self) -> Channels { + match *self {} + } + fn slice(&self, _: GridAab) -> Self::Plane { match *self {} } diff --git a/all-is-cubes-port/src/gltf/texture.rs b/all-is-cubes-port/src/gltf/texture.rs index c2b4c306b..e2b8c6ec6 100644 --- a/all-is-cubes-port/src/gltf/texture.rs +++ b/all-is-cubes-port/src/gltf/texture.rs @@ -69,10 +69,16 @@ impl texture::Allocator for GltfTextureAllocator { type Tile = GltfTile; type Point = GltfAtlasPoint; - fn allocate(&self, bounds: GridAab) -> Option { + fn allocate(&self, bounds: GridAab, mut channels: texture::Channels) -> Option { if self.enable { + // TODO: implement more channels + if true { + channels = texture::Channels::Reflectance; + } + Some(GltfTile { bounds, + channels, texels: internal::TexelsCell::default(), gatherer: self.gatherer.clone(), }) @@ -93,6 +99,7 @@ impl texture::Allocator for GltfTextureAllocator { #[allow(clippy::derive_partial_eq_without_eq)] pub struct GltfTile { bounds: GridAab, + channels: texture::Channels, gatherer: internal::Gatherer, texels: internal::TexelsCell, } @@ -118,6 +125,10 @@ impl texture::Tile for GltfTile { self.bounds } + fn channels(&self) -> texture::Channels { + self.channels + } + fn slice(&self, sliced_bounds: GridAab) -> Self::Plane { let axis = texture::validate_slice(self.bounds, sliced_bounds); @@ -429,7 +440,7 @@ mod tests { use super::*; use all_is_cubes::block; use all_is_cubes::math::Rgba; - use all_is_cubes_mesh::texture::{Allocator, Tile}; + use all_is_cubes_mesh::texture::{Allocator, Channels, Tile}; use std::fs; /// TODO: this is just a smoke-test; add more rigorous tests. @@ -442,7 +453,7 @@ mod tests { let allocator = GltfTextureAllocator::new(GltfDataDestination::new(Some(file_base_path), 0), true); let mut tile = allocator - .allocate(GridAab::ORIGIN_CUBE) + .allocate(GridAab::ORIGIN_CUBE, Channels::Reflectance) .expect("allocation"); tile.write( block::Evoxels::One(Evoxel::from_color(Rgba::from_srgb8([1, 2, 3, 4]))).as_vol_ref(),