diff --git a/assets/alpha0_etc1s.basisu_ktx2 b/assets/alpha0_etc1s.basisu_ktx2 new file mode 100644 index 0000000..8a28378 --- /dev/null +++ b/assets/alpha0_etc1s.basisu_ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f465f87625313a395c196e3e4401609b0503f2ae4c9a0900a5724c88cfa44d4 +size 895 diff --git a/crates/basisu_sys/build.rs b/crates/basisu_sys/build.rs index 606e3d5..f816784 100644 --- a/crates/basisu_sys/build.rs +++ b/crates/basisu_sys/build.rs @@ -70,6 +70,7 @@ fn bindgen() { .opaque_type("Transcoder") .bitfield_enum("TextureCompressionMethod") .newtype_enum("TextureTranscodedFormat") + .newtype_enum("ChannelType") .generate() .expect("Unable to generate bindings") .write_to_file(binding_file) diff --git a/crates/basisu_sys/src/lib.rs b/crates/basisu_sys/src/lib.rs index 74dbc71..9ab281f 100644 --- a/crates/basisu_sys/src/lib.rs +++ b/crates/basisu_sys/src/lib.rs @@ -17,6 +17,8 @@ mod transcoding { include!(concat!(env!("OUT_DIR"), "/transcoding.rs")); } +pub use transcoding::{ChannelType, TextureCompressionMethod, TextureTranscodedFormat, Transcoder}; + #[cfg(not(all( target_arch = "wasm32", target_vendor = "unknown", diff --git a/crates/basisu_sys/src/native.rs b/crates/basisu_sys/src/native.rs index 5650b36..7ae1e42 100644 --- a/crates/basisu_sys/src/native.rs +++ b/crates/basisu_sys/src/native.rs @@ -1,6 +1,9 @@ #![expect(clippy::missing_safety_doc, reason = "TODO")] -pub use crate::transcoding::{TextureCompressionMethod, TextureTranscodedFormat, Transcoder}; +use crate::ChannelType; +use crate::TextureCompressionMethod; +use crate::TextureTranscodedFormat; +use crate::Transcoder; pub use crate::transcoding::c_basisu_transcoder_init as basisu_transcoder_init; pub use crate::transcoding::c_ktx2_transcoder_delete as ktx2_transcoder_delete; @@ -17,6 +20,8 @@ pub unsafe fn ktx2_transcoder_transcode_image( transcoder: *mut Transcoder, data: Vec, supported_compressed_formats: TextureCompressionMethod, + channel_type_hint: ChannelType, + force_transcode_target: TextureTranscodedFormat, ) -> bool { unsafe { crate::transcoding::c_ktx2_transcoder_transcode_image( @@ -24,6 +29,8 @@ pub unsafe fn ktx2_transcoder_transcode_image( data.as_ptr(), u32::try_from(data.len()).unwrap(), supported_compressed_formats, + channel_type_hint, + force_transcode_target, ) } } diff --git a/crates/basisu_sys/src/web.rs b/crates/basisu_sys/src/web.rs index cfe387e..ce15da2 100644 --- a/crates/basisu_sys/src/web.rs +++ b/crates/basisu_sys/src/web.rs @@ -2,18 +2,22 @@ use std::cell::OnceCell; -pub use crate::transcoding::{TextureCompressionMethod, TextureTranscodedFormat, Transcoder}; - use js_sys::Object; use js_sys::Reflect; use js_sys::Uint8Array; +use crate::ChannelType; +use crate::TextureCompressionMethod; +use crate::TextureTranscodedFormat; +use crate::Transcoder; + mod bindings_sys { use super::Transcoder; use js_sys::Uint8Array; use wasm_bindgen::prelude::wasm_bindgen; type TextureCompressionMethodRepr = u8; type TextureTranscodedFormatRepr = u32; + type ChannelTypeRepr = u8; #[wasm_bindgen] extern "C" { @@ -40,6 +44,8 @@ mod bindings_sys { data: usize, data_len: u32, supported_compressed_formats: TextureCompressionMethodRepr, + channel_type_hint: ChannelTypeRepr, + force_transcode_target: TextureTranscodedFormatRepr, ) -> bool; #[wasm_bindgen(method,js_name=_c_ktx2_transcoder_get_r_dst_buf)] pub fn js_ktx2_transcoder_get_r_dst_buf( @@ -184,6 +190,8 @@ pub unsafe fn ktx2_transcoder_transcode_image( transcoder: *mut Transcoder, data: Vec, supported_compressed_formats: TextureCompressionMethod, + channel_type_hint: ChannelType, + force_transcode_target: TextureTranscodedFormat, ) -> bool { BASISU_VENDOR_INSTANCE.with(|inst| { let inst = inst.get().unwrap(); @@ -196,6 +204,8 @@ pub unsafe fn ktx2_transcoder_transcode_image( ptr, len, supported_compressed_formats.0, + channel_type_hint.0, + force_transcode_target.0, ); inst.js_basisu_free(ptr); result diff --git a/crates/basisu_sys/wasm/basisu_vendor.js b/crates/basisu_sys/wasm/basisu_vendor.js index 9468506..db75c4b 100644 --- a/crates/basisu_sys/wasm/basisu_vendor.js +++ b/crates/basisu_sys/wasm/basisu_vendor.js @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df40ceac9421453492be5b377a6a0835c1d5d10bb131315d7d5978d141532bbf -size 10397 +oid sha256:c89d06319e37edc5b8a737328e5818958e82cdc6a0e7a92e60fd79feeefbb372 +size 10378 diff --git a/crates/basisu_sys/wasm/basisu_vendor.wasm b/crates/basisu_sys/wasm/basisu_vendor.wasm index 5a6cbb9..7b67c85 100755 --- a/crates/basisu_sys/wasm/basisu_vendor.wasm +++ b/crates/basisu_sys/wasm/basisu_vendor.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b4c8ad5ee6f9fb91dbfbfb7382965a0fd9de7ee19358eaee49fe86204f65b74 -size 376542 +oid sha256:b377e8fd04c91d4089e529c762815690d4396c22ef3ad288c53acbbcde6dd0dc +size 377252 diff --git a/examples/test_scene/src/lib.rs b/examples/test_scene/src/lib.rs index 8c366d8..ccd790a 100644 --- a/examples/test_scene/src/lib.rs +++ b/examples/test_scene/src/lib.rs @@ -3,7 +3,7 @@ use bevy::{ log::LogPlugin, math::Affine2, prelude::*, - render::view::Hdr, + render::{render_resource::TextureFormat, view::Hdr}, }; use bevy_basisu_loader::{BasisuLoaderPlugin, BasisuLoaderSettings}; @@ -80,6 +80,7 @@ fn setup( base_color_texture: Some(asset_server.load_with_settings( "desk_uastc_hdr_4x4_mips_10.basisu_ktx2", |s: &mut BasisuLoaderSettings| { + s.force_transcode_target = Some(TextureFormat::Rgb9e5Ufloat); s.sampler = bevy::image::ImageSampler::Descriptor(bevy::image::ImageSamplerDescriptor { address_mode_u: bevy::image::ImageAddressMode::Repeat, @@ -95,9 +96,26 @@ fn setup( * Transform::from_rotation(Quat::from_rotation_y(-45.0)), )); + commands.spawn(( + Mesh3d(meshes.add(Rectangle::new(1.0, 1.0).mesh().build())), + MeshMaterial3d(materials.add(StandardMaterial { + base_color_texture: Some(asset_server.load_with_settings( + "alpha0_etc1s.basisu_ktx2", + |s: &mut BasisuLoaderSettings| { + s.channel_type_hint = bevy_basisu_loader::ChannelType::Rg; + }, + )), + alpha_mode: AlphaMode::Blend, + unlit: true, + ..Default::default() + })), + Transform::from_xyz(-2.0, 0.0, -2.0) + * Transform::from_rotation(Quat::from_rotation_y(45.0)), + )); + // UI commands.spawn(( - Text::new("Press Q, E to rotate camera."), + Text::new("Press Q, E (or ArrowLeft, ArrowRight) to rotate camera."), Node { position_type: PositionType::Absolute, top: px(12), @@ -111,9 +129,11 @@ fn rotate_camera( mut query: Query<&mut Transform, With>, keyboard_input: Res>, ) { - let rotate = if keyboard_input.pressed(KeyCode::KeyQ) { + let rotate = if keyboard_input.pressed(KeyCode::KeyQ) + || keyboard_input.pressed(KeyCode::ArrowLeft) + { 0.05 - } else if keyboard_input.pressed(KeyCode::KeyE) { + } else if keyboard_input.pressed(KeyCode::KeyE) || keyboard_input.pressed(KeyCode::ArrowRight) { -0.05 } else { 0.0 diff --git a/src/lib.rs b/src/lib.rs index a4b1cce..14eb9f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,14 @@ mod loader; pub use loader::*; +/// Provides a loader for Basis Universal KTX2 textures. +/// +/// The file extension must be `.basisu_ktx2` to use this loader. Supports KTX2 UASTC/ETC1S format. Zstd supercompression is supported even if bevy's zstd feature is disabled. No support for `.basis` files. +/// +/// Transcode Target Selection: +/// - ETC1S: Bc7Rgba/Bc5Rg/Bc4R > Etc2Rgba8/Etc2Rgb8/EacRg11/EacR11 > Rgba8 +/// - UASTC LDR: Astc > Bc7Rgba > Etc2Rgba8/Etc2Rgb8/EacRg11/EacR11 > Rgba8 +/// - UASTC HDR: Astc > Bc6hRgbUfloat > Rgba16Float pub struct BasisuLoaderPlugin; impl Plugin for BasisuLoaderPlugin { diff --git a/src/loader.rs b/src/loader.rs index a0b3d7e..29556a2 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -35,6 +35,17 @@ impl BasisuLoader { } } +#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy)] +#[repr(u8)] +pub enum ChannelType { + #[default] + Auto, + Rgba, + Rgb, + Rg, + R, +} + /// Settings for loading an [`Image`] using an [`BasisuLoader`]. #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub struct BasisuLoaderSettings { @@ -44,6 +55,21 @@ pub struct BasisuLoaderSettings { /// Where the asset will be used - see the docs on /// [`RenderAssetUsages`] for details. pub asset_usage: RenderAssetUsages, + /// Whether the texture should be created as sRGB format. + /// + /// If `None`, it will be determined by the KTX2 data format descriptor transfer function. + pub is_srgb: Option, + /// The channel type hint for transcode target selection. + /// + /// If [`ChannelType::Auto`], it will be determined by the KTX2 data format descriptor channel type. + /// Note: This will be ignored when the transcode target format is not ETC2 or BC4/BC5 and usually has no effect for UASTC textures. See [`BasisuLoaderPlugin`](crate::BasisuLoaderPlugin) for more information about the transcode targets. + pub channel_type_hint: ChannelType, + /// Forcibly transcode to a specific [`TextureFormat`] if it's not `None`. Otherwise the format will be selected automatically. + /// + /// Only set this if you know what you're doing and use this with caution, it will fail if the transcode target is not supported by Basis Universal or the texture format is not supported by the device. + /// Srgb-ness is ignored and will be determined by `is_srgb`. + #[serde(skip)] + pub force_transcode_target: Option, } /// An error when loading an image using [`BasisuLoader`]. @@ -91,13 +117,20 @@ impl AssetLoader for BasisuLoader { transcoder, data, self.supported_compressed_formats, + bevy_basisu_loader_sys::ChannelType(settings.channel_type_hint as u8), + texture_bevy_format_to_transcode_format(settings.force_transcode_target), ) { return Err(BasisuLoaderError::TranscodingError( "ktx2_transcoder_transcode_image", )); } - let is_srgb = bevy_basisu_loader_sys::ktx2_transcoder_get_r_is_srgb(transcoder); + let is_srgb = + settings + .is_srgb + .unwrap_or(bevy_basisu_loader_sys::ktx2_transcoder_get_r_is_srgb( + transcoder, + )); let target_format = bevy_basisu_loader_sys::ktx2_transcoder_get_r_target_format(transcoder); @@ -238,3 +271,40 @@ fn texture_transcode_format_to_bevy_format( } fmt } + +fn texture_bevy_format_to_transcode_format( + format: Option, +) -> TextureTranscodedFormat { + let Some(format) = format else { + return TextureTranscodedFormat::cTFTotalTextureFormats; + }; + let format = format.remove_srgb_suffix(); + match format { + TextureFormat::Etc2Rgb8Unorm => TextureTranscodedFormat::cTFETC1_RGB, + TextureFormat::Etc2Rgba8Unorm => TextureTranscodedFormat::cTFETC2_RGBA, + TextureFormat::Bc1RgbaUnorm => TextureTranscodedFormat::cTFBC1_RGB, + TextureFormat::Bc3RgbaUnorm => TextureTranscodedFormat::cTFBC3_RGBA, + TextureFormat::Bc4RUnorm => TextureTranscodedFormat::cTFBC4_R, + TextureFormat::Bc5RgUnorm => TextureTranscodedFormat::cTFBC5_RG, + TextureFormat::Bc7RgbaUnorm => TextureTranscodedFormat::cTFBC7_RGBA, + TextureFormat::Astc { + block: AstcBlock::B4x4, + channel: AstcChannel::Unorm, + } => TextureTranscodedFormat::cTFASTC_4x4_RGBA, + TextureFormat::EacR11Unorm => TextureTranscodedFormat::cTFETC2_EAC_R11, + TextureFormat::EacRg11Unorm => TextureTranscodedFormat::cTFETC2_EAC_RG11, + TextureFormat::Bc6hRgbUfloat => TextureTranscodedFormat::cTFBC6H, + TextureFormat::Astc { + block: AstcBlock::B4x4, + channel: AstcChannel::Hdr, + } => TextureTranscodedFormat::cTFASTC_HDR_4x4_RGBA, + TextureFormat::Rgba8Unorm => TextureTranscodedFormat::cTFRGBA32, + TextureFormat::Rgba16Float => TextureTranscodedFormat::cTFRGBA_HALF, + TextureFormat::Rgb9e5Ufloat => TextureTranscodedFormat::cTFRGB_9E5, + TextureFormat::Astc { + block: AstcBlock::B6x6, + channel: AstcChannel::Hdr, + } => TextureTranscodedFormat::cTFASTC_HDR_6x6_RGBA, + _ => unreachable!(), + } +} diff --git a/vendor/transcoding_wrapper.cpp b/vendor/transcoding_wrapper.cpp index 83a591d..66077a9 100644 --- a/vendor/transcoding_wrapper.cpp +++ b/vendor/transcoding_wrapper.cpp @@ -3,13 +3,6 @@ enum TextureCompressionMethod : unsigned char; -enum ChannelType { - CHANNEL_RGB, - CHANNEL_RGBA, - CHANNEL_R, - CHANNEL_RG, -}; - static ChannelType channel_id_to_type(bool is_uastc, basist::ktx2_df_channel_id channel_id0, basist::ktx2_df_channel_id channel_id1); @@ -69,13 +62,13 @@ static bool c_ktx2_transcoder_get_texture_info(Transcoder *transcoder, TextureTr return true; } -static void c_ktx2_transcoder_get_target_format(Transcoder *transcoder, TextureCompressionMethod supported_compressed_formats, bool *r_is_srgb, TextureTranscodedFormat *r_format) { +static void c_ktx2_transcoder_get_target_format(Transcoder *transcoder, TextureCompressionMethod supported_compressed_formats, ChannelType channel_type_hint, bool *r_is_srgb, TextureTranscodedFormat *r_format) { basist::ktx2_transcoder *inner = transcoder->inner; basist::ktx2_df_channel_id channel_id0 = inner->get_dfd_channel_id0(); basist::ktx2_df_channel_id channel_id1 = inner->get_dfd_channel_id1(); basist::basis_tex_format basis_format = inner->get_basis_tex_format(); - ChannelType channel_type = channel_id_to_type(inner->is_uastc(), channel_id0, channel_id1); + ChannelType channel_type = channel_type_hint != CHANNEL_UNDEFINED ? channel_type_hint : channel_id_to_type(inner->is_uastc(), channel_id0, channel_id1); basist::transcoder_texture_format target_format = get_target_texture_format(basis_format, channel_type, supported_compressed_formats); *r_is_srgb = inner->get_dfd_transfer_func() == basist::KTX2_KHR_DF_TRANSFER_SRGB; *r_format = static_cast(static_cast(target_format)); @@ -83,13 +76,16 @@ static void c_ktx2_transcoder_get_target_format(Transcoder *transcoder, TextureC bool c_ktx2_transcoder_transcode_image( Transcoder *transcoder, const unsigned char *data, unsigned int data_size, - TextureCompressionMethod supported_compressed_formats) { + TextureCompressionMethod supported_compressed_formats, ChannelType channel_type_hint, TextureTranscodedFormat force_transcode_target) { basist::ktx2_transcoder *inner = transcoder->inner; inner->init(data, data_size); inner->start_transcoding(); - c_ktx2_transcoder_get_target_format(transcoder, supported_compressed_formats, &transcoder->r_is_srgb, &transcoder->r_target_format); - const basist::transcoder_texture_format transcode_format = static_cast(static_cast(transcoder->r_target_format)); + c_ktx2_transcoder_get_target_format(transcoder, supported_compressed_formats, channel_type_hint, &transcoder->r_is_srgb, &transcoder->r_target_format); + if (force_transcode_target != TextureTranscodedFormat::cTFTotalTextureFormats) { + transcoder->r_target_format = force_transcode_target; + } + basist::transcoder_texture_format transcode_format = static_cast(static_cast(transcoder->r_target_format)); if (!c_ktx2_transcoder_get_texture_info(transcoder, transcoder->r_target_format, &transcoder->r_width, &transcoder->r_height, &transcoder->r_levels, &transcoder->r_layers, &transcoder->r_faces, &transcoder->r_dst_buf_len)) { return false; @@ -212,7 +208,8 @@ static basist::transcoder_texture_format get_target_texture_format( case CHANNEL_RGB: { return basist::transcoder_texture_format::cTFETC1_RGB; } break; - case CHANNEL_RGBA: { + case CHANNEL_RGBA: + case CHANNEL_UNDEFINED: { return basist::transcoder_texture_format::cTFETC2_RGBA; } break; case CHANNEL_R: { @@ -242,6 +239,9 @@ static basist::transcoder_texture_format get_target_texture_format( case CHANNEL_RG: { return basist::transcoder_texture_format::cTFBC5_RG; } break; + case CHANNEL_UNDEFINED: { + abort(); + } } } else if (supported_compressed_formats & TextureCompressionMethod::ETC2) { switch (channel_type) { @@ -257,6 +257,9 @@ static basist::transcoder_texture_format get_target_texture_format( case CHANNEL_RG: { return basist::transcoder_texture_format::cTFETC2_EAC_RG11; } break; + case CHANNEL_UNDEFINED: { + abort(); + } } } else { return basist::transcoder_texture_format::cTFRGBA32; diff --git a/vendor/transcoding_wrapper.hpp b/vendor/transcoding_wrapper.hpp index 80f8499..3d17c1d 100644 --- a/vendor/transcoding_wrapper.hpp +++ b/vendor/transcoding_wrapper.hpp @@ -5,6 +5,14 @@ class ktx2_transcoder; extern "C" { +enum ChannelType : unsigned char { + CHANNEL_UNDEFINED = 0, + CHANNEL_RGBA, + CHANNEL_RGB, + CHANNEL_RG, + CHANNEL_R, +}; + // This enum must be in sync with the `basist::transcoder_texture_format`. enum TextureTranscodedFormat : unsigned int { // Compressed formats @@ -112,7 +120,7 @@ Transcoder *c_ktx2_transcoder_new(); void c_ktx2_transcoder_delete(Transcoder *transcoder); bool c_ktx2_transcoder_transcode_image(Transcoder *transcoder, const unsigned char *data, unsigned int data_size, - TextureCompressionMethod supported_compressed_formats); + TextureCompressionMethod supported_compressed_formats, ChannelType channel_type_hint, TextureTranscodedFormat force_transcode_target); unsigned char *c_ktx2_transcoder_get_r_dst_buf(Transcoder *transcoder); unsigned int c_ktx2_transcoder_get_r_dst_buf_len(Transcoder *transcoder);