From 8531033b31b086f202b881933d1efcabf8fea026 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Wed, 21 Feb 2024 02:11:28 +0100 Subject: [PATCH] Add support for KHR_texture_transform (#11904) Adopted #8266, so copy-pasting the description from there: # Objective Support the KHR_texture_transform extension for the glTF loader. - Fixes #6335 - Fixes #11869 - Implements part of #11350 - Implements the GLTF part of #399 ## Solution As is, this only supports a single transform. Looking at Godot's source, they support one transform with an optional second one for detail, AO, and emission. glTF specifies one per texture. The public domain materials I looked at seem to share the same transform. So maybe having just one is acceptable for now. I tried to include a warning if multiple different transforms exist for the same material. Note the gltf crate doesn't expose the texture transform for the normal and occlusion textures, which it should, so I just ignored those for now. (note by @janhohenheim: this is still the case) Via `cargo run --release --example scene_viewer ~/src/clone/glTF-Sample-Models/2.0/TextureTransformTest/glTF/TextureTransformTest.gltf`: ![texture_transform](https://user-images.githubusercontent.com/283864/228938298-aa2ef524-555b-411d-9637-fd0dac226fb0.png) ## Changelog Support for the [KHR_texture_transform](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform) extension added. Texture UVs that were scaled, rotated, or offset in a GLTF are now properly handled. --------- Co-authored-by: Al McElrath Co-authored-by: Kanabenki --- crates/bevy_gltf/Cargo.toml | 1 + crates/bevy_gltf/src/loader.rs | 59 ++++++++++++++++++- crates/bevy_pbr/src/pbr_material.rs | 21 +++++-- .../bevy_pbr/src/render/mesh_functions.wgsl | 6 +- crates/bevy_pbr/src/render/mesh_types.wgsl | 2 +- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 4 +- .../src/render/pbr_prepass_functions.wgsl | 5 +- crates/bevy_pbr/src/render/pbr_types.wgsl | 7 ++- crates/bevy_render/src/maths.wgsl | 10 +++- .../src/mesh2d/mesh2d_functions.wgsl | 4 +- .../bevy_sprite/src/mesh2d/mesh2d_types.wgsl | 2 +- crates/bevy_sprite/src/render/sprite.wgsl | 4 +- 12 files changed, 106 insertions(+), 19 deletions(-) diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index cc2af18278..e0a8b93916 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -43,6 +43,7 @@ gltf = { version = "1.4.0", default-features = false, features = [ "KHR_materials_volume", "KHR_materials_unlit", "KHR_materials_emissive_strength", + "KHR_texture_transform", "extras", "extensions", "names", diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index d4456c4594..1bc4c8328a 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -9,7 +9,7 @@ use bevy_ecs::entity::EntityHashMap; use bevy_ecs::{entity::Entity, world::World}; use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder}; use bevy_log::{error, info_span, warn}; -use bevy_math::{Mat4, Vec3}; +use bevy_math::{Affine2, Mat4, Vec3}; use bevy_pbr::{ AlphaMode, DirectionalLight, DirectionalLightBundle, PbrBundle, PointLight, PointLightBundle, SpotLight, SpotLightBundle, StandardMaterial, MAX_JOINTS, @@ -42,7 +42,7 @@ use bevy_utils::{ use gltf::{ accessor::Iter, mesh::{util::ReadIndices, Mode}, - texture::{MagFilter, MinFilter, WrappingMode}, + texture::{Info, MagFilter, MinFilter, TextureTransform, WrappingMode}, Material, Node, Primitive, Semantic, }; use serde::{Deserialize, Serialize}; @@ -826,6 +826,14 @@ fn load_material( texture_handle(load_context, &info.texture()) }); + let uv_transform = pbr + .base_color_texture() + .and_then(|info| { + info.texture_transform() + .map(convert_texture_transform_to_affine2) + }) + .unwrap_or_default(); + let normal_map_texture: Option> = material.normal_texture().map(|normal_texture| { // TODO: handle normal_texture.scale @@ -835,6 +843,12 @@ fn load_material( let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { // TODO: handle info.tex_coord() (the *set* index for the right texcoords) + warn_on_differing_texture_transforms( + material, + &info, + uv_transform, + "metallic/roughness", + ); texture_handle(load_context, &info.texture()) }); @@ -848,6 +862,7 @@ fn load_material( let emissive_texture = material.emissive_texture().map(|info| { // TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords) // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) + warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); texture_handle(load_context, &info.texture()) }); @@ -935,11 +950,51 @@ fn load_material( ), unlit: material.unlit(), alpha_mode: alpha_mode(material), + uv_transform, ..Default::default() } }) } +fn convert_texture_transform_to_affine2(texture_transform: TextureTransform) -> Affine2 { + Affine2::from_scale_angle_translation( + texture_transform.scale().into(), + -texture_transform.rotation(), + texture_transform.offset().into(), + ) +} + +fn warn_on_differing_texture_transforms( + material: &Material, + info: &Info, + texture_transform: Affine2, + texture_kind: &str, +) { + let has_differing_texture_transform = info + .texture_transform() + .map(convert_texture_transform_to_affine2) + .is_some_and(|t| t != texture_transform); + if has_differing_texture_transform { + let material_name = material + .name() + .map(|n| format!("the material \"{n}\"")) + .unwrap_or_else(|| "an unnamed material".to_string()); + let texture_name = info + .texture() + .name() + .map(|n| format!("its {texture_kind} texture \"{n}\"")) + .unwrap_or_else(|| format!("its unnamed {texture_kind} texture")); + let material_index = material + .index() + .map(|i| format!("index {i}")) + .unwrap_or_else(|| "default".to_string()); + warn!( + "Only texture transforms on base color textures are supported, but {material_name} ({material_index}) \ + has a texture transform on {texture_name} (index {}), which will be ignored.", info.texture().index() + ); + } +} + /// Loads a glTF node. #[allow(clippy::too_many_arguments, clippy::result_large_err)] fn load_node( diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 25cb24fc14..3c017fbc4b 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1,5 +1,5 @@ use bevy_asset::{Asset, Handle}; -use bevy_math::Vec4; +use bevy_math::{Affine2, Vec2, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ color::Color, mesh::MeshVertexBufferLayout, render_asset::RenderAssets, render_resource::*, @@ -472,6 +472,9 @@ pub struct StandardMaterial { /// Default is [`DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID`] for default /// PBR deferred lighting pass. Ignored in the case of forward materials. pub deferred_lighting_pass_id: u8, + + /// The transform applied to the UVs corresponding to ATTRIBUTE_UV_0 on the mesh before sampling. Default is identity. + pub uv_transform: Affine2, } impl Default for StandardMaterial { @@ -520,6 +523,7 @@ impl Default for StandardMaterial { parallax_mapping_method: ParallaxMappingMethod::Occlusion, opaque_render_method: OpaqueRendererMethod::Auto, deferred_lighting_pass_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID, + uv_transform: Affine2::IDENTITY, } } } @@ -590,9 +594,17 @@ pub struct StandardMaterialUniform { /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// in between. pub base_color: Vec4, - // Use a color for user friendliness even though we technically don't use the alpha channel + // Use a color for user-friendliness even though we technically don't use the alpha channel // Might be used in the future for exposure correction in HDR pub emissive: Vec4, + /// Color white light takes after travelling through the attenuation distance underneath the material surface + pub attenuation_color: Vec4, + /// The x-axis of the mat2 of the transform applied to the UVs corresponding to ATTRIBUTE_UV_0 on the mesh before sampling. Default is [1, 0]. + pub uv_transform_x_axis: Vec2, + /// The y-axis of the mat2 of the transform applied to the UVs corresponding to ATTRIBUTE_UV_0 on the mesh before sampling. Default is [0, 1]. + pub uv_transform_y_axis: Vec2, + /// The translation of the transform applied to the UVs corresponding to ATTRIBUTE_UV_0 on the mesh before sampling. Default is [0, 0]. + pub uv_transform_translation: Vec2, /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader /// Defaults to minimum of 0.089 pub roughness: f32, @@ -611,8 +623,6 @@ pub struct StandardMaterialUniform { pub ior: f32, /// How far light travels through the volume underneath the material surface before being absorbed pub attenuation_distance: f32, - /// Color white light takes after travelling through the attenuation distance underneath the material surface - pub attenuation_color: Vec4, /// The [`StandardMaterialFlags`] accessible in the `wgsl` shader. pub flags: u32, /// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque, @@ -729,6 +739,9 @@ impl AsBindGroupShaderType for StandardMaterial { lightmap_exposure: self.lightmap_exposure, max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(), deferred_lighting_pass_id: self.deferred_lighting_pass_id as u32, + uv_transform_x_axis: self.uv_transform.matrix2.x_axis, + uv_transform_y_axis: self.uv_transform.matrix2.y_axis, + uv_transform_translation: self.uv_transform.translation, } } } diff --git a/crates/bevy_pbr/src/render/mesh_functions.wgsl b/crates/bevy_pbr/src/render/mesh_functions.wgsl index 170c2f916a..e94b4f7633 100644 --- a/crates/bevy_pbr/src/render/mesh_functions.wgsl +++ b/crates/bevy_pbr/src/render/mesh_functions.wgsl @@ -6,15 +6,15 @@ mesh_types::MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT, view_transformations::position_world_to_clip, } -#import bevy_render::maths::{affine_to_square, mat2x4_f32_to_mat3x3_unpack} +#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack} fn get_model_matrix(instance_index: u32) -> mat4x4 { - return affine_to_square(mesh[instance_index].model); + return affine3_to_square(mesh[instance_index].model); } fn get_previous_model_matrix(instance_index: u32) -> mat4x4 { - return affine_to_square(mesh[instance_index].previous_model); + return affine3_to_square(mesh[instance_index].previous_model); } fn mesh_position_local_to_world(model: mat4x4, vertex_position: vec4) -> vec4 { diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index a3aec0b9eb..258b6ceef9 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -2,7 +2,7 @@ struct Mesh { // Affine 4x3 matrices transposed to 3x4 - // Use bevy_render::maths::affine_to_square to unpack + // Use bevy_render::maths::affine3_to_square to unpack model: mat3x4, previous_model: mat3x4, // 3x3 matrix packed in mat2x4 and f32 as: diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index be759d38b5..b484e9691a 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -11,6 +11,7 @@ parallax_mapping::parallaxed_uv, lightmap::lightmap, } +#import bevy_render::maths::affine2_to_square #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture @@ -73,7 +74,8 @@ fn pbr_input_from_standard_material( let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); #ifdef VERTEX_UVS - var uv = in.uv; + let uv_transform = affine2_to_square(pbr_bindings::material.uv_transform); + var uv = (uv_transform * vec3(in.uv, 1.0)).xy; #ifdef VERTEX_TANGENTS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) { diff --git a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl index 176c56aa1a..7a8dbb8a19 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl @@ -7,6 +7,7 @@ pbr_bindings, pbr_types, } +#import bevy_render::maths::affine2_to_square // Cutoff used for the premultiplied alpha modes BLEND and ADD. const PREMULTIPLIED_ALPHA_CUTOFF = 0.05; @@ -18,8 +19,10 @@ fn prepass_alpha_discard(in: VertexOutput) { var output_color: vec4 = pbr_bindings::material.base_color; #ifdef VERTEX_UVS + let uv_transform = affine2_to_square(pbr_bindings::material.uv_transform); + let uv = (uv_transform * vec3(in.uv, 1.0)).xy; if (pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { - output_color = output_color * textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, in.uv, view.mip_bias); + output_color = output_color * textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias); } #endif // VERTEX_UVS diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 72a70e45c8..c08245034c 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -1,8 +1,12 @@ #define_import_path bevy_pbr::pbr_types +// Since this is a hot path, try to keep the alignment and size of the struct members in mind. +// You can find the alignment and sizes at . struct StandardMaterial { base_color: vec4, emissive: vec4, + attenuation_color: vec4, + uv_transform: mat3x2, perceptual_roughness: f32, metallic: f32, reflectance: f32, @@ -11,7 +15,6 @@ struct StandardMaterial { thickness: f32, ior: f32, attenuation_distance: f32, - attenuation_color: vec4, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, alpha_cutoff: f32, @@ -74,6 +77,8 @@ fn standard_material_new() -> StandardMaterial { material.max_parallax_layer_count = 16.0; material.max_relief_mapping_search_steps = 5u; material.deferred_lighting_pass_id = 1u; + // scale 1, translation 0, rotation 0 + material.uv_transform = mat3x2(1.0, 0.0, 0.0, 1.0, 0.0, 0.0); return material; } diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index af79c88c9a..17d045154a 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -1,6 +1,14 @@ #define_import_path bevy_render::maths -fn affine_to_square(affine: mat3x4) -> mat4x4 { +fn affine2_to_square(affine: mat3x2) -> mat3x3 { + return mat3x3( + vec3(affine[0].xy, 0.0), + vec3(affine[1].xy, 0.0), + vec3(affine[2].xy, 1.0), + ); +} + +fn affine3_to_square(affine: mat3x4) -> mat4x4 { return transpose(mat4x4( affine[0], affine[1], diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl index b66b34e888..d76a88ab03 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl +++ b/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl @@ -4,10 +4,10 @@ mesh2d_view_bindings::view, mesh2d_bindings::mesh, } -#import bevy_render::maths::{affine_to_square, mat2x4_f32_to_mat3x3_unpack} +#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack} fn get_model_matrix(instance_index: u32) -> mat4x4 { - return affine_to_square(mesh[instance_index].model); + return affine3_to_square(mesh[instance_index].model); } fn mesh2d_position_local_to_world(model: mat4x4, vertex_position: vec4) -> vec4 { diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl index f855707790..4b14b919f3 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl +++ b/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl @@ -2,7 +2,7 @@ struct Mesh2d { // Affine 4x3 matrix transposed to 3x4 - // Use bevy_render::maths::affine_to_square to unpack + // Use bevy_render::maths::affine3_to_square to unpack model: mat3x4, // 3x3 matrix packed in mat2x4 and f32 as: // [0].xyz, [1].x, diff --git a/crates/bevy_sprite/src/render/sprite.wgsl b/crates/bevy_sprite/src/render/sprite.wgsl index 1f5c0125e1..48f0235155 100644 --- a/crates/bevy_sprite/src/render/sprite.wgsl +++ b/crates/bevy_sprite/src/render/sprite.wgsl @@ -3,7 +3,7 @@ #endif #import bevy_render::{ - maths::affine_to_square, + maths::affine3_to_square, view::View, } @@ -37,7 +37,7 @@ fn vertex(in: VertexInput) -> VertexOutput { 0.0 ); - out.clip_position = view.view_proj * affine_to_square(mat3x4( + out.clip_position = view.view_proj * affine3_to_square(mat3x4( in.i_model_transpose_col0, in.i_model_transpose_col1, in.i_model_transpose_col2,