diff --git a/Cargo.toml b/Cargo.toml index ba1947661a078..81749db14b57c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -389,6 +389,17 @@ description = "Demonstrates how to prevent meshes from casting/receiving shadows category = "3D Rendering" wasm = true +[[example]] +name = "skybox" +path = "examples/3d/skybox.rs" +required-features = ["ktx2", "zstd"] + +[package.metadata.example.skybox] +name = "Skybox" +description = "Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats." +category = "3D Rendering" +wasm = false + [[example]] name = "spherical_area_lights" path = "examples/3d/spherical_area_lights.rs" diff --git a/assets/shaders/cubemap_unlit.wgsl b/assets/shaders/cubemap_unlit.wgsl new file mode 100644 index 0000000000000..6837384dea3ac --- /dev/null +++ b/assets/shaders/cubemap_unlit.wgsl @@ -0,0 +1,24 @@ +#import bevy_pbr::mesh_view_bindings + +#ifdef CUBEMAP_ARRAY +@group(1) @binding(0) +var base_color_texture: texture_cube_array; +#else +@group(1) @binding(0) +var base_color_texture: texture_cube; +#endif + +@group(1) @binding(1) +var base_color_sampler: sampler; + +@fragment +fn fragment( + #import bevy_pbr::mesh_vertex_output +) -> @location(0) vec4 { + let fragment_position_view_lh = world_position.xyz * vec3(1.0, 1.0, -1.0); + return textureSample( + base_color_texture, + base_color_sampler, + fragment_position_view_lh + ); +} diff --git a/assets/textures/Ryfjallet_cubemap.png b/assets/textures/Ryfjallet_cubemap.png new file mode 100644 index 0000000000000..777987b75552a Binary files /dev/null and b/assets/textures/Ryfjallet_cubemap.png differ diff --git a/assets/textures/Ryfjallet_cubemap_astc4x4.ktx2 b/assets/textures/Ryfjallet_cubemap_astc4x4.ktx2 new file mode 100644 index 0000000000000..78696cadca7ee Binary files /dev/null and b/assets/textures/Ryfjallet_cubemap_astc4x4.ktx2 differ diff --git a/assets/textures/Ryfjallet_cubemap_bc7.ktx2 b/assets/textures/Ryfjallet_cubemap_bc7.ktx2 new file mode 100644 index 0000000000000..17e67c7c9ff8b Binary files /dev/null and b/assets/textures/Ryfjallet_cubemap_bc7.ktx2 differ diff --git a/assets/textures/Ryfjallet_cubemap_etc2.ktx2 b/assets/textures/Ryfjallet_cubemap_etc2.ktx2 new file mode 100644 index 0000000000000..22a389cfd90e5 Binary files /dev/null and b/assets/textures/Ryfjallet_cubemap_etc2.ktx2 differ diff --git a/assets/textures/Ryfjallet_cubemap_readme.txt b/assets/textures/Ryfjallet_cubemap_readme.txt new file mode 100644 index 0000000000000..81bed0d91d8f3 --- /dev/null +++ b/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/ diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index ceab85cb99498..8b363dc226a30 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -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>, } /// Used in [`Image`], this determines what image sampler to use when rendering. The default setting, @@ -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, } } } @@ -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, diff --git a/crates/bevy_render/src/texture/ktx2.rs b/crates/bevy_render/src/texture/ktx2.rs index b26c6c1bcebe2..75d61e3a9eda0 100644 --- a/crates/bevy_render/src/texture/ktx2.rs +++ b/crates/bevy_render/src/texture/ktx2.rs @@ -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}; @@ -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(); @@ -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 { @@ -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), + ); + 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 } @@ -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::>(); + image.data = wgpu_data.into_iter().flatten().collect::>(); 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 @@ -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) } diff --git a/examples/3d/skybox.rs b/examples/3d/skybox.rs new file mode 100644 index 0000000000000..13ccfa3d1f3c4 --- /dev/null +++ b/examples/3d/skybox.rs @@ -0,0 +1,424 @@ +//! Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats + +use bevy::{ + asset::LoadState, + input::mouse::MouseMotion, + pbr::{MaterialPipeline, MaterialPipelineKey}, + prelude::*, + reflect::TypeUuid, + render::{ + mesh::MeshVertexBufferLayout, + render_asset::RenderAssets, + render_resource::{ + AsBindGroup, AsBindGroupError, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, + BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, + OwnedBindingResource, PreparedBindGroup, RenderPipelineDescriptor, SamplerBindingType, + ShaderRef, ShaderStages, SpecializedMeshPipelineError, TextureSampleType, + TextureViewDescriptor, TextureViewDimension, + }, + renderer::RenderDevice, + texture::{CompressedImageFormats, FallbackImage}, + }, +}; + +const CUBEMAPS: &[(&str, CompressedImageFormats)] = &[ + ( + "textures/Ryfjallet_cubemap.png", + CompressedImageFormats::NONE, + ), + ( + "textures/Ryfjallet_cubemap_astc4x4.ktx2", + CompressedImageFormats::ASTC_LDR, + ), + ( + "textures/Ryfjallet_cubemap_bc7.ktx2", + CompressedImageFormats::BC, + ), + ( + "textures/Ryfjallet_cubemap_etc2.ktx2", + CompressedImageFormats::ETC2, + ), +]; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(MaterialPlugin::::default()) + .add_startup_system(setup) + .add_system(cycle_cubemap_asset) + .add_system(asset_loaded.after(cycle_cubemap_asset)) + .add_system(camera_controller) + .add_system(animate_light_direction) + .run(); +} + +struct Cubemap { + is_loaded: bool, + index: usize, + image_handle: Handle, +} + +fn setup(mut commands: Commands, asset_server: Res) { + // directional 'sun' light + commands.spawn_bundle(DirectionalLightBundle { + directional_light: DirectionalLight { + illuminance: 32000.0, + ..default() + }, + transform: Transform { + translation: Vec3::new(0.0, 2.0, 0.0), + rotation: Quat::from_rotation_x(-std::f32::consts::FRAC_PI_4), + ..default() + }, + ..default() + }); + + let skybox_handle = asset_server.load(CUBEMAPS[0].0); + // camera + commands + .spawn_bundle(Camera3dBundle { + transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::default(), Vec3::Y), + ..default() + }) + .insert(CameraController::default()); + + // ambient light + // NOTE: The ambient light is used to scale how bright the environment map is so with a bright + // environment map, use an appropriate colour and brightness to match + commands.insert_resource(AmbientLight { + color: Color::rgb_u8(210, 220, 240), + brightness: 1.0, + }); + + commands.insert_resource(Cubemap { + is_loaded: false, + index: 0, + image_handle: skybox_handle, + }); +} + +const CUBEMAP_SWAP_DELAY: f64 = 3.0; + +fn cycle_cubemap_asset( + time: Res