Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Support array / cubemap / cubemap array textures in KTX2 #5325

Closed
wants to merge 13 commits into from
Closed
10 changes: 10 additions & 0 deletions Cargo.toml
Expand Up @@ -389,6 +389,16 @@ description = "Demonstrates how to prevent meshes from casting/receiving shadows
category = "3D Rendering"
wasm = true

[[example]]
name = "skybox"
path = "examples/3d/skybox.rs"

superdump marked this conversation as resolved.
Show resolved Hide resolved
[package.metadata.example.skybox]
name = "Skybox"
description = "Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats. Note that the `ktx2` and `zstd` features are needed to be able to load the KTX2 textures."
superdump marked this conversation as resolved.
Show resolved Hide resolved
category = "3D Rendering"
wasm = false

[[example]]
name = "spherical_area_lights"
path = "examples/3d/spherical_area_lights.rs"
Expand Down
36 changes: 36 additions & 0 deletions assets/shaders/cubemap_unlit.wgsl
@@ -0,0 +1,36 @@
#import bevy_pbr::mesh_view_bindings

#ifdef CUBEMAP_ARRAY
@group(1) @binding(0)
var base_color_texture: texture_cube_array<f32>;

// struct CubemapLayer {
// layer: u32,
// };
// @group(1) @binding(1)
// var layer: Layer;
cart marked this conversation as resolved.
Show resolved Hide resolved

#else
@group(1) @binding(0)
var base_color_texture: texture_cube<f32>;
#endif

@group(1) @binding(1)
var base_color_sampler: sampler;

@fragment
fn fragment(
#import bevy_pbr::mesh_vertex_output
) -> @location(0) vec4<f32> {
let fragment_position_view_lh = world_position.xyz * vec3<f32>(1.0, 1.0, -1.0);
return textureSample(
base_color_texture,
base_color_sampler,
#ifdef CUBEMAP_ARRAY
fragment_position_view_lh,
2
#else
fragment_position_view_lh
cart marked this conversation as resolved.
Show resolved Hide resolved
#endif
);
}
Binary file added assets/textures/Ryfjallet_cubemap.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/textures/Ryfjallet_cubemap_astc4x4.ktx2
Binary file not shown.
Binary file added assets/textures/Ryfjallet_cubemap_bc7.ktx2
Binary file not shown.
Binary file added assets/textures/Ryfjallet_cubemap_etc2.ktx2
Binary file not shown.
21 changes: 21 additions & 0 deletions assets/textures/Ryfjallet_cubemap_readme.txt
@@ -0,0 +1,21 @@
Modifications
=============

The original work, as attributed below, has been modified as follows using the ImageMagick tool:

mogrify -resize 256x256 -format png *.jpg
convert posx.png negx.png posy.png negy.png posz.png negz.png -gravity center -append cubemap.png

Author
======

This is the work of Emil Persson, aka Humus.
http://www.humus.name



License
=======

This work is licensed under a Creative Commons Attribution 3.0 Unported License.
http://creativecommons.org/licenses/by/3.0/
10 changes: 9 additions & 1 deletion crates/bevy_render/src/texture/image.rs
Expand Up @@ -110,6 +110,7 @@ pub struct Image {
pub texture_descriptor: wgpu::TextureDescriptor<'static>,
/// The [`ImageSampler`] to use during rendering.
pub sampler_descriptor: ImageSampler,
pub texture_view_descriptor: Option<wgpu::TextureViewDescriptor<'static>>,
}

/// Used in [`Image`], this determines what image sampler to use when rendering. The default setting,
Expand Down Expand Up @@ -202,6 +203,7 @@ impl Default for Image {
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
},
sampler_descriptor: ImageSampler::Default,
texture_view_descriptor: None,
}
}
}
Expand Down Expand Up @@ -670,7 +672,13 @@ impl RenderAsset for Image {
texture
};

let texture_view = texture.create_view(&TextureViewDescriptor::default());
let texture_view = texture.create_view(
image
.texture_view_descriptor
.or_else(|| Some(TextureViewDescriptor::default()))
.as_ref()
.unwrap(),
);
let size = Vec2::new(
image.texture_descriptor.size.width as f32,
image.texture_descriptor.size.height as f32,
Expand Down
175 changes: 125 additions & 50 deletions crates/bevy_render/src/texture/ktx2.rs
Expand Up @@ -5,13 +5,17 @@ use std::io::Read;
use basis_universal::{
DecodeFlags, LowLevelUastcTranscoder, SliceParametersUastc, TranscoderBlockFormat,
};
use bevy_utils::default;
#[cfg(any(feature = "flate2", feature = "ruzstd"))]
use ktx2::SupercompressionScheme;
use ktx2::{
BasicDataFormatDescriptor, ChannelTypeQualifiers, ColorModel, DataFormatDescriptorHeader,
Header, SampleInformation,
};
use wgpu::{AstcBlock, AstcChannel, Extent3d, TextureDimension, TextureFormat};
use wgpu::{
AstcBlock, AstcChannel, Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor,
TextureViewDimension,
};

use super::{CompressedImageFormats, DataFormat, Image, TextureError, TranscodeFormat};

Expand All @@ -28,10 +32,14 @@ pub fn ktx2_buffer_to_image(
pixel_height: height,
pixel_depth: depth,
layer_count,
face_count,
level_count,
supercompression_scheme,
..
} = ktx2.header();
let layer_count = layer_count.max(1);
let face_count = face_count.max(1);
let depth = depth.max(1);

// Handle supercompression
let mut levels = Vec::new();
Expand Down Expand Up @@ -80,25 +88,25 @@ pub fn ktx2_buffer_to_image(
let texture_format = ktx2_get_texture_format(&ktx2, is_srgb).or_else(|error| match error {
// Transcode if needed and supported
TextureError::FormatRequiresTranscodingError(transcode_format) => {
let mut transcoded = Vec::new();
let mut transcoded = vec![Vec::default(); levels.len()];
let texture_format = match transcode_format {
TranscodeFormat::Rgb8 => {
let (mut original_width, mut original_height) = (width, height);

for level_data in &levels {
let n_pixels = (original_width * original_height) as usize;
let mut rgba = vec![255u8; width as usize * height as usize * 4];
for (level, level_data) in levels.iter().enumerate() {
let n_pixels = (width as usize >> level).max(1) * (height as usize >> level).max(1);

let mut rgba = vec![255u8; n_pixels * 4];
for i in 0..n_pixels {
rgba[i * 4] = level_data[i * 3];
rgba[i * 4 + 1] = level_data[i * 3 + 1];
rgba[i * 4 + 2] = level_data[i * 3 + 2];
let mut offset = 0;
for _layer in 0..layer_count {
for _face in 0..face_count {
for i in 0..n_pixels {
rgba[i * 4] = level_data[offset];
rgba[i * 4 + 1] = level_data[offset + 1];
rgba[i * 4 + 2] = level_data[offset + 2];
offset += 3;
}
transcoded[level].extend_from_slice(&rgba[0..n_pixels]);
}
}
transcoded.push(rgba);

// Next mip dimensions are half the current, minimum 1x1
original_width = (original_width / 2).max(1);
original_height = (original_height / 2).max(1);
}

if is_srgb {
Expand All @@ -111,41 +119,54 @@ pub fn ktx2_buffer_to_image(
TranscodeFormat::Uastc(data_format) => {
let (transcode_block_format, texture_format) =
get_transcoded_formats(supported_compressed_formats, data_format, is_srgb);
let (mut original_width, mut original_height) = (width, height);
let (block_width_pixels, block_height_pixels) = (4, 4);
let texture_format_info = texture_format.describe();
let (block_width_pixels, block_height_pixels) = (
texture_format_info.block_dimensions.0 as u32,
texture_format_info.block_dimensions.1 as u32,
);
let block_bytes = texture_format_info.block_size as u32;

let transcoder = LowLevelUastcTranscoder::new();
for (level, level_data) in levels.iter().enumerate() {
let slice_parameters = SliceParametersUastc {
num_blocks_x: ((original_width + block_width_pixels - 1)
/ block_width_pixels)
.max(1),
num_blocks_y: ((original_height + block_height_pixels - 1)
/ block_height_pixels)
.max(1),
has_alpha: false,
original_width,
original_height,
};

transcoder
.transcode_slice(
level_data,
slice_parameters,
DecodeFlags::HIGH_QUALITY,
transcode_block_format,
)
.map(|transcoded_level| transcoded.push(transcoded_level))
.map_err(|error| {
TextureError::SuperDecompressionError(format!(
"Failed to transcode mip level {} from UASTC to {:?}: {:?}",
level, transcode_block_format, error
))
})?;
let (level_width, level_height) = (
(width >> level as u32).max(1),
(height >> level as u32).max(1),
);
let (num_blocks_x, num_blocks_y) = (
((level_width + block_width_pixels - 1) / block_width_pixels) .max(1),
((level_height + block_height_pixels - 1) / block_height_pixels) .max(1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Division rounding up is used often. Could we abstract this into a local definition of div_ceil until it's available?

Also, maybe NonZeroUsize could help to avoid most of the .max(1) suffixes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I was thinking of extracting the rounding code. Some functionality is in wgpu’s texture format description but there are other cases. I’m going to be out today so won’t have time to address this unfortunately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we're shotgunning this into 0.8 (and rob isn't available to make changes right now), I think this should be done later. Good idea though!

);
let level_bytes = (num_blocks_x * num_blocks_y * block_bytes) as usize;

// Next mip dimensions are half the current, minimum 1x1
original_width = (original_width / 2).max(1);
original_height = (original_height / 2).max(1);
let mut offset = 0;
for _layer in 0..layer_count {
for _face in 0..face_count {
// NOTE: SliceParametersUastc does not implement Clone nor Copy so
// it has to be created per use
let slice_parameters = SliceParametersUastc {
num_blocks_x,
num_blocks_y,
has_alpha: false,
original_width: level_width,
original_height: level_height,
};
transcoder
.transcode_slice(
&level_data[offset..(offset + level_bytes)],
slice_parameters,
DecodeFlags::HIGH_QUALITY,
transcode_block_format,
)
.map(|mut transcoded_level| transcoded[level].append(&mut transcoded_level))
.map_err(|error| {
TextureError::SuperDecompressionError(format!(
"Failed to transcode mip level {} from UASTC to {:?}: {:?}",
level, transcode_block_format, error
))
})?;
offset += level_bytes;
}
}
}
texture_format
}
Expand Down Expand Up @@ -178,16 +199,52 @@ pub fn ktx2_buffer_to_image(
)));
}

// Reorder data from KTX2 MipXLayerYFaceZ to wgpu LayerYFaceZMipX
let texture_format_info = texture_format.describe();
let (block_width_pixels, block_height_pixels) = (
texture_format_info.block_dimensions.0 as usize,
texture_format_info.block_dimensions.1 as usize,
);
let block_bytes = texture_format_info.block_size as usize;

let mut wgpu_data = vec![Vec::default(); (layer_count * face_count) as usize];
for (level, level_data) in levels.iter().enumerate() {
let (level_width, level_height) = (
(width as usize >> level).max(1),
(height as usize >> level).max(1),
);
let (num_blocks_x, num_blocks_y) = (
((level_width + block_width_pixels - 1) / block_width_pixels).max(1),
((level_height + block_height_pixels - 1) / block_height_pixels).max(1),
);
let level_bytes = num_blocks_x * num_blocks_y * block_bytes;

let mut index = 0;
for _layer in 0..layer_count {
for _face in 0..face_count {
let offset = index * level_bytes;
wgpu_data[index].extend_from_slice(&level_data[offset..(offset + level_bytes)]);
index += 1;
}
}
}

// Assign the data and fill in the rest of the metadata now the possible
// error cases have been handled
let mut image = Image::default();
image.texture_descriptor.format = texture_format;
image.data = levels.into_iter().flatten().collect::<Vec<_>>();
image.data = wgpu_data.into_iter().flatten().collect::<Vec<_>>();
image.texture_descriptor.size = Extent3d {
width,
height,
depth_or_array_layers: if layer_count > 1 { layer_count } else { depth }.max(1),
};
depth_or_array_layers: if layer_count > 1 || face_count > 1 {
layer_count * face_count
} else {
depth
}
.max(1),
}
.physical_size(texture_format);
image.texture_descriptor.mip_level_count = level_count;
image.texture_descriptor.dimension = if depth > 1 {
TextureDimension::D3
Expand All @@ -196,6 +253,24 @@ pub fn ktx2_buffer_to_image(
} else {
TextureDimension::D1
};
let mut dimension = None;
if face_count == 6 {
dimension = Some(if layer_count > 1 {
TextureViewDimension::CubeArray
} else {
TextureViewDimension::Cube
});
} else if layer_count > 1 {
dimension = Some(TextureViewDimension::D2Array);
} else if depth > 1 {
dimension = Some(TextureViewDimension::D3);
}
if dimension.is_some() {
image.texture_view_descriptor = Some(TextureViewDescriptor {
dimension,
..default()
});
}
Ok(image)
}

Expand Down