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 <hello@yrns.org>
Co-authored-by: Kanabenki <lucien.menassol@gmail.com>
This commit is contained in:
Jan Hohenheim 2024-02-21 02:11:28 +01:00 committed by GitHub
parent 37e632145a
commit 8531033b31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 106 additions and 19 deletions

View file

@ -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",

View file

@ -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(

View file

@ -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,
}
}
}

View file

@ -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> {

View file

@ -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:

View file

@ -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) {

View file

@ -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

View file

@ -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;
}

View file

@ -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],

View file

@ -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> {

View file

@ -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,

View file

@ -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,