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`:  ## 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 <hello@yrns.org> Co-authored-by: Kanabenki <lucien.menassol@gmail.com>
This commit is contained in:
		
							parent
							
								
									37e632145a
								
							
						
					
					
						commit
						8531033b31
					
				@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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<Handle<Image>> =
 | 
			
		||||
            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(
 | 
			
		||||
 | 
			
		||||
@ -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<StandardMaterialUniform> 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,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<f32> {
 | 
			
		||||
    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<f32> {
 | 
			
		||||
    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<f32>, vertex_position: vec4<f32>) -> vec4<f32> {
 | 
			
		||||
 | 
			
		||||
@ -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<f32>,
 | 
			
		||||
    previous_model: mat3x4<f32>,
 | 
			
		||||
    // 3x3 matrix packed in mat2x4 and f32 as:
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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<f32> = 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 <https://www.w3.org/TR/WGSL/#alignment-and-size>.
 | 
			
		||||
struct StandardMaterial {
 | 
			
		||||
    base_color: vec4<f32>,
 | 
			
		||||
    emissive: vec4<f32>,
 | 
			
		||||
    attenuation_color: vec4<f32>,
 | 
			
		||||
    uv_transform: mat3x2<f32>,
 | 
			
		||||
    perceptual_roughness: f32,
 | 
			
		||||
    metallic: f32,
 | 
			
		||||
    reflectance: f32,
 | 
			
		||||
@ -11,7 +15,6 @@ struct StandardMaterial {
 | 
			
		||||
    thickness: f32,
 | 
			
		||||
    ior: f32,
 | 
			
		||||
    attenuation_distance: f32,
 | 
			
		||||
    attenuation_color: vec4<f32>,
 | 
			
		||||
    // '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<f32>(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
 | 
			
		||||
 | 
			
		||||
    return material;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,14 @@
 | 
			
		||||
#define_import_path bevy_render::maths
 | 
			
		||||
 | 
			
		||||
fn affine_to_square(affine: mat3x4<f32>) -> mat4x4<f32> {
 | 
			
		||||
fn affine2_to_square(affine: mat3x2<f32>) -> mat3x3<f32> {
 | 
			
		||||
    return mat3x3<f32>(
 | 
			
		||||
        vec3<f32>(affine[0].xy, 0.0),
 | 
			
		||||
        vec3<f32>(affine[1].xy, 0.0),
 | 
			
		||||
        vec3<f32>(affine[2].xy, 1.0),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn affine3_to_square(affine: mat3x4<f32>) -> mat4x4<f32> {
 | 
			
		||||
    return transpose(mat4x4<f32>(
 | 
			
		||||
        affine[0],
 | 
			
		||||
        affine[1],
 | 
			
		||||
 | 
			
		||||
@ -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<f32> {
 | 
			
		||||
    return affine_to_square(mesh[instance_index].model);
 | 
			
		||||
    return affine3_to_square(mesh[instance_index].model);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn mesh2d_position_local_to_world(model: mat4x4<f32>, vertex_position: vec4<f32>) -> vec4<f32> {
 | 
			
		||||
 | 
			
		||||
@ -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<f32>,
 | 
			
		||||
    // 3x3 matrix packed in mat2x4 and f32 as:
 | 
			
		||||
    // [0].xyz, [1].x,
 | 
			
		||||
 | 
			
		||||
@ -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<f32>(
 | 
			
		||||
    out.clip_position = view.view_proj * affine3_to_square(mat3x4<f32>(
 | 
			
		||||
        in.i_model_transpose_col0,
 | 
			
		||||
        in.i_model_transpose_col1,
 | 
			
		||||
        in.i_model_transpose_col2,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user