Skip to content

Commit

Permalink
mesh: Add Channels property to texture allocations.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kpreid committed Nov 5, 2023
1 parent 0d7ddd0 commit 2e9f6c6
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 28 deletions.
12 changes: 11 additions & 1 deletion all-is-cubes-gpu/src/in_wgpu/block_texture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,13 @@ impl texture::Allocator for AtlasAllocator {
type Tile = AtlasTile;
type Point = TexPoint;

fn allocate(&self, requested_bounds: GridAab) -> Option<AtlasTile> {
fn allocate(
&self,
requested_bounds: GridAab,
_channels: texture::Channels,
) -> Option<AtlasTile> {
// TODO: implement channels

let mut allocator_backing = self.backing.lock().unwrap();

// If alloctree grows, the next flush() will take care of reallocating the texture.
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions all-is-cubes-mesh/src/block_mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,11 @@ impl<M: MeshTypes + 'static> BlockMesh<M> {
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
}
Expand Down Expand Up @@ -478,7 +482,7 @@ impl<M: MeshTypes + 'static> BlockMesh<M> {
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,
);
Expand Down
19 changes: 12 additions & 7 deletions all-is-cubes-mesh/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl texture::Allocator for Allocator {
type Tile = Tile;
type Point = TexPoint;

fn allocate(&self, bounds: GridAab) -> Option<Self::Tile> {
fn allocate(&self, bounds: GridAab, channels: texture::Channels) -> Option<Self::Tile> {
self.count_allocated
.fetch_update(SeqCst, SeqCst, |count| {
if count < self.capacity {
Expand All @@ -104,7 +104,7 @@ impl texture::Allocator for Allocator {
})
.ok()
.map(|_| ())?;
Some(Tile { bounds })
Some(Tile { bounds, channels })
}
}

Expand All @@ -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 {
Expand All @@ -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()
Expand Down Expand Up @@ -153,7 +158,7 @@ pub type TexPoint = Point3D<texture::TextureCoordinate, texture::TexelUnit>;
#[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`].
Expand All @@ -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());
}
}
89 changes: 74 additions & 15 deletions all-is-cubes-mesh/src/texture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<Self::Tile>;
fn allocate(&self, bounds: GridAab, channels: Channels) -> Option<Self::Tile>;
}

/// 3D texture volume provided by an [`Allocator`] to paint a block's voxels in.
Expand All @@ -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
Expand Down Expand Up @@ -116,24 +129,50 @@ impl<T: Allocator> Allocator for &T {
type Tile = T::Tile;
type Point = T::Point;
#[mutants::skip] // trivial
fn allocate(&self, bounds: GridAab) -> Option<Self::Tile> {
<T as Allocator>::allocate(self, bounds)
fn allocate(&self, bounds: GridAab, channels: Channels) -> Option<Self::Tile> {
<T as Allocator>::allocate(self, bounds, channels)
}
}
impl<T: Allocator> Allocator for std::sync::Arc<T> {
type Tile = T::Tile;
type Point = T::Point;
#[mutants::skip] // trivial
fn allocate(&self, bounds: GridAab) -> Option<Self::Tile> {
<T as Allocator>::allocate(self, bounds)
fn allocate(&self, bounds: GridAab, channels: Channels) -> Option<Self::Tile> {
<T as Allocator>::allocate(self, bounds, channels)
}
}
impl<T: Allocator> Allocator for std::rc::Rc<T> {
type Tile = T::Tile;
type Point = T::Point;
#[mutants::skip] // trivial
fn allocate(&self, bounds: GridAab) -> Option<Self::Tile> {
<T as Allocator>::allocate(self, bounds)
fn allocate(&self, bounds: GridAab, channels: Channels) -> Option<Self::Tile> {
<T as Allocator>::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,
}
}
}

Expand All @@ -145,7 +184,7 @@ impl<T: Allocator> Allocator for std::rc::Rc<T> {
/// (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!(
Expand All @@ -160,18 +199,34 @@ pub fn validate_slice(tile_bounds: GridAab, slice_bounds: GridAab) -> Axis {
}
}

pub(super) fn copy_voxels_to_texture<A: Allocator>(
pub(super) fn copy_voxels_to_new_texture<A: Allocator>(
texture_allocator: &A,
voxels: &Evoxels,
) -> Option<A::Tile> {
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)]
Expand Down Expand Up @@ -209,7 +264,7 @@ impl Allocator for NoTextures {
type Tile = NoTexture;
type Point = NoTexture;

fn allocate(&self, _: GridAab) -> Option<Self::Tile> {
fn allocate(&self, _: GridAab, _: Channels) -> Option<Self::Tile> {
None
}
}
Expand All @@ -230,6 +285,10 @@ impl Tile for NoTexture {
match *self {}
}

fn channels(&self) -> Channels {
match *self {}
}

fn slice(&self, _: GridAab) -> Self::Plane {
match *self {}
}
Expand Down
17 changes: 14 additions & 3 deletions all-is-cubes-port/src/gltf/texture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,16 @@ impl texture::Allocator for GltfTextureAllocator {
type Tile = GltfTile;
type Point = GltfAtlasPoint;

fn allocate(&self, bounds: GridAab) -> Option<GltfTile> {
fn allocate(&self, bounds: GridAab, mut channels: texture::Channels) -> Option<GltfTile> {
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(),
})
Expand All @@ -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,
}
Expand All @@ -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);

Expand Down Expand Up @@ -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.
Expand All @@ -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(),
Expand Down

0 comments on commit 2e9f6c6

Please sign in to comment.