diff --git a/Cargo.toml b/Cargo.toml index 3dfdf3ec77..72e3559548 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3143,6 +3143,18 @@ description = "Demonstrates volumetric fog and lighting" category = "3D Rendering" wasm = true +[[example]] +name = "anisotropy" +path = "examples/3d/anisotropy.rs" +doc-scrape-examples = true +required-features = ["jpeg"] + +[package.metadata.example.anisotropy] +name = "Anisotropy" +description = "Displays an example model with anisotropy" +category = "3D Rendering" +wasm = false + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp.bin b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp.bin new file mode 100644 index 0000000000..f0237209b9 Binary files /dev/null and b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp.bin differ diff --git a/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp.gltf b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp.gltf new file mode 100644 index 0000000000..40646ed8c4 --- /dev/null +++ b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp.gltf @@ -0,0 +1,350 @@ +{ + "asset": { + "version": "2.0", + "generator": "3ds Max, Max2Babylon, Visual Studio Code, glTF Tools", + "copyright": "(c) 2023 Wayfair, model and textures by Eric Chadwick, CC BY 4.0." + }, + "extensionsUsed": [ + "KHR_materials_anisotropy", + "KHR_materials_clearcoat", + "KHR_materials_emissive_strength", + "KHR_materials_transmission", + "KHR_materials_volume" + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0, + 1, + 2 + ] + } + ], + "nodes": [ + { + "mesh": 0, + "name": "Lamp Metal" + }, + { + "mesh": 1, + "name": "Lamp Filament" + }, + { + "mesh": 2, + "name": "Lamp Glass" + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "POSITION": 1, + "TANGENT": 2, + "NORMAL": 3, + "TEXCOORD_0": 4 + }, + "indices": 0, + "material": 0 + } + ], + "name": "Lamp Metal" + }, + { + "primitives": [ + { + "attributes": { + "POSITION": 6, + "NORMAL": 7 + }, + "indices": 5, + "material": 1 + } + ], + "name": "Lamp Filament" + }, + { + "primitives": [ + { + "attributes": { + "POSITION": 9, + "NORMAL": 10 + }, + "indices": 8, + "material": 2 + } + ], + "name": "Lamp Glass" + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5123, + "count": 25257, + "type": "SCALAR", + "name": "accessorIndices" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 6803, + "max": [ + 0.08619007, + 0.056946706, + 0.226538792 + ], + "min": [ + -0.104619145, + -0.172301471, + -2.43595832E-06 + ], + "type": "VEC3", + "name": "accessorPositions" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 6803, + "type": "VEC4", + "name": "accessorTangents" + }, + { + "bufferView": 1, + "byteOffset": 81636, + "componentType": 5126, + "count": 6803, + "type": "VEC3", + "name": "accessorNormals" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 6803, + "type": "VEC2", + "name": "accessorUVs" + }, + { + "bufferView": 0, + "byteOffset": 50516, + "componentType": 5123, + "count": 840, + "type": "SCALAR", + "name": "accessorIndices" + }, + { + "bufferView": 1, + "byteOffset": 163272, + "componentType": 5126, + "count": 140, + "max": [ + 8.701398E-05, + -0.12128906, + 0.142439723 + ], + "min": [ + -0.0193775147, + -0.170719177, + 0.122564256 + ], + "type": "VEC3", + "name": "accessorPositions" + }, + { + "bufferView": 1, + "byteOffset": 164952, + "componentType": 5126, + "count": 140, + "type": "VEC3", + "name": "accessorNormals" + }, + { + "bufferView": 0, + "byteOffset": 52196, + "componentType": 5123, + "count": 4512, + "type": "SCALAR", + "name": "accessorIndices" + }, + { + "bufferView": 1, + "byteOffset": 166632, + "componentType": 5126, + "count": 769, + "max": [ + 0.01725974, + -0.0980222151, + 0.15841879 + ], + "min": [ + -0.03568878, + -0.198292032, + 0.106095724 + ], + "type": "VEC3", + "name": "accessorPositions" + }, + { + "bufferView": 1, + "byteOffset": 175860, + "componentType": 5126, + "count": 769, + "type": "VEC3", + "name": "accessorNormals" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 61220, + "name": "bufferViewScalar", + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 61220, + "byteLength": 185088, + "byteStride": 12, + "name": "bufferViewFloatVec3", + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 246308, + "byteLength": 108848, + "byteStride": 16, + "name": "bufferViewFloatVec4", + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 355156, + "byteLength": 54424, + "byteStride": 8, + "name": "bufferViewFloatVec2", + "target": 34962 + } + ], + "buffers": [ + { + "uri": "AnisotropyBarnLamp.bin", + "byteLength": 409580 + } + ], + "materials": [ + { + "name": "Lamp Metal", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 2 + }, + "metallicRoughnessTexture": { + "index": 1 + } + }, + "normalTexture": { + "index": 0 + }, + "occlusionTexture": { + "index": 1 + }, + "extensions": { + "KHR_materials_anisotropy": { + "anisotropyStrength": 1, + "anisotropyRotation": 0, + "anisotropyTexture": { + "index": 3 + } + }, + "KHR_materials_clearcoat": { + "clearcoatFactor": 0.25, + "clearcoatRoughnessFactor": 0.15, + "clearcoatNormalTexture": { + "index": 0 + } + } + } + }, + { + "name": "Lamp Filament", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.09, + 0.09, + 0.09, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 0.7 + }, + "emissiveFactor": [ + 1, + 0.5, + 0.25 + ], + "extensions": { + "KHR_materials_emissive_strength": { + "emissiveStrength": 25 + } + } + }, + { + "name": "Lamp Glass", + "pbrMetallicRoughness": { + "metallicFactor": 0, + "roughnessFactor": 0 + }, + "extensions": { + "KHR_materials_transmission": { + "transmissionFactor": 1 + }, + "KHR_materials_volume": { + "thicknessFactor": 0.01 + } + } + } + ], + "textures": [ + { + "sampler": 0, + "source": 0, + "name": "AnisotropyBarnLamp_normalbump.ktx2" + }, + { + "sampler": 0, + "source": 1, + "name": "AnisotropyBarnLamp_occlusionroughnessmetal.ktx2" + }, + { + "sampler": 0, + "source": 2, + "name": "AnisotropyBarnLamp_basecolor.jpeg" + }, + { + "sampler": 0, + "source": 3, + "name": "AnisotropyBarnLamp_anisotropy.ktx2" + } + ], + "images": [ + { + "uri": "AnisotropyBarnLamp_normalbump.ktx2" + }, + { + "uri": "AnisotropyBarnLamp_occlusionroughnessmetal.ktx2" + }, + { + "uri": "AnisotropyBarnLamp_basecolor.jpeg" + }, + { + "uri": "AnisotropyBarnLamp_anisotropy.ktx2" + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987 + } + ] +} \ No newline at end of file diff --git a/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_anisotropy.ktx2 b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_anisotropy.ktx2 new file mode 100644 index 0000000000..32ef2d17d3 Binary files /dev/null and b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_anisotropy.ktx2 differ diff --git a/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_basecolor.jpeg b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_basecolor.jpeg new file mode 100644 index 0000000000..715f082f18 Binary files /dev/null and b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_basecolor.jpeg differ diff --git a/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_normalbump.ktx2 b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_normalbump.ktx2 new file mode 100644 index 0000000000..8099744a8a Binary files /dev/null and b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_normalbump.ktx2 differ diff --git a/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_occlusionroughnessmetal.ktx2 b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_occlusionroughnessmetal.ktx2 new file mode 100644 index 0000000000..af0ae73cd4 Binary files /dev/null and b/assets/models/AnisotropyBarnLamp/AnisotropyBarnLamp_occlusionroughnessmetal.ktx2 differ diff --git a/assets/shaders/array_texture.wgsl b/assets/shaders/array_texture.wgsl index 21c8fb2741..1be9064570 100644 --- a/assets/shaders/array_texture.wgsl +++ b/assets/shaders/array_texture.wgsl @@ -42,12 +42,12 @@ fn fragment( #ifdef VERTEX_TANGENTS let Nt = textureSampleBias(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, mesh.uv, view.mip_bias).rgb; + let TBN = fns::calculate_tbn_mikktspace(mesh.world_normal, mesh.world_tangent); pbr_input.N = fns::apply_normal_mapping( pbr_input.material.flags, - mesh.world_normal, + TBN, double_sided, is_front, - mesh.world_tangent, Nt, view.mip_bias, ); diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index a50a99c6d6..c2026ce67f 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -51,9 +51,7 @@ use gltf::{ }; use gltf::{json, Document}; use serde::{Deserialize, Serialize}; -#[cfg(feature = "pbr_multi_layer_material_textures")] -use serde_json::value; -use serde_json::Value; +use serde_json::{value, Value}; #[cfg(feature = "bevy_animation")] use smallvec::SmallVec; use std::io::Error; @@ -223,6 +221,13 @@ async fn load_gltf<'a, 'b, 'c>( { linear_textures.insert(texture.texture().index()); } + if let Some(texture_index) = material_extension_texture_index( + &material, + "KHR_materials_anisotropy", + "anisotropyTexture", + ) { + linear_textures.insert(texture_index); + } // None of the clearcoat maps should be loaded as sRGB. #[cfg(feature = "pbr_multi_layer_material_textures")] @@ -1016,6 +1021,10 @@ fn load_material( let clearcoat = ClearcoatExtension::parse(load_context, document, material).unwrap_or_default(); + // Parse the `KHR_materials_anisotropy` extension data if necessary. + let anisotropy = + AnisotropyExtension::parse(load_context, document, material).unwrap_or_default(); + // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); let emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); @@ -1078,6 +1087,10 @@ fn load_material( clearcoat_normal_channel: clearcoat.clearcoat_normal_channel, #[cfg(feature = "pbr_multi_layer_material_textures")] clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, + anisotropy_strength: anisotropy.anisotropy_strength.unwrap_or_default() as f32, + anisotropy_rotation: anisotropy.anisotropy_rotation.unwrap_or_default() as f32, + anisotropy_channel: anisotropy.anisotropy_channel, + anisotropy_texture: anisotropy.anisotropy_texture, ..Default::default() } }) @@ -1886,10 +1899,52 @@ impl ClearcoatExtension { } } +/// Parsed data from the `KHR_materials_anisotropy` extension. +/// +/// See the specification: +/// +#[derive(Default)] +struct AnisotropyExtension { + anisotropy_strength: Option, + anisotropy_rotation: Option, + anisotropy_channel: UvChannel, + anisotropy_texture: Option>, +} + +impl AnisotropyExtension { + fn parse( + load_context: &mut LoadContext, + document: &Document, + material: &Material, + ) -> Option { + let extension = material + .extensions()? + .get("KHR_materials_anisotropy")? + .as_object()?; + + let (anisotropy_channel, anisotropy_texture) = extension + .get("anisotropyTexture") + .and_then(|value| value::from_value::(value.clone()).ok()) + .map(|json_info| { + ( + get_uv_channel(material, "anisotropy", json_info.tex_coord), + texture_handle_from_info(load_context, document, &json_info), + ) + }) + .unzip(); + + Some(AnisotropyExtension { + anisotropy_strength: extension.get("anisotropyStrength").and_then(Value::as_f64), + anisotropy_rotation: extension.get("anisotropyRotation").and_then(Value::as_f64), + anisotropy_channel: anisotropy_channel.unwrap_or_default(), + anisotropy_texture, + }) + } +} + /// Returns the index (within the `textures` array) of the texture with the /// given field name in the data for the material extension with the given name, /// if there is one. -#[cfg(feature = "pbr_multi_layer_material_textures")] fn material_extension_texture_index( material: &Material, extension_name: &str, diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 61f8ddc144..59a6792dff 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1,6 +1,6 @@ use bevy_asset::Asset; use bevy_color::{Alpha, ColorToComponents}; -use bevy_math::{Affine2, Affine3, Mat2, Mat3, Vec2, Vec3, Vec4}; +use bevy_math::{vec2, Affine2, Affine3, Mat2, Mat3, Vec2, Vec3, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ mesh::MeshVertexBufferLayoutRef, render_asset::RenderAssets, render_resource::*, @@ -215,8 +215,8 @@ pub struct StandardMaterial { /// /// **Important:** The [`StandardMaterial::diffuse_transmission`] property must be set to a value higher than 0.0, /// or this texture won't have any effect. - #[texture(17)] - #[sampler(18)] + #[texture(19)] + #[sampler(20)] #[cfg(feature = "pbr_transmission_textures")] pub diffuse_transmission_texture: Option>, @@ -256,8 +256,8 @@ pub struct StandardMaterial { /// /// **Important:** The [`StandardMaterial::specular_transmission`] property must be set to a value higher than 0.0, /// or this texture won't have any effect. - #[texture(13)] - #[sampler(14)] + #[texture(15)] + #[sampler(16)] #[cfg(feature = "pbr_transmission_textures")] pub specular_transmission_texture: Option>, @@ -285,8 +285,8 @@ pub struct StandardMaterial { /// /// **Important:** The [`StandardMaterial::thickness`] property must be set to a value higher than 0.0, /// or this texture won't have any effect. - #[texture(15)] - #[sampler(16)] + #[texture(17)] + #[sampler(18)] #[cfg(feature = "pbr_transmission_textures")] pub thickness_texture: Option>, @@ -420,8 +420,8 @@ pub struct StandardMaterial { /// main [`StandardMaterial::clearcoat`] factor. /// /// As this is a non-color map, it must not be loaded as sRGB. - #[texture(19)] - #[sampler(20)] + #[texture(21)] + #[sampler(22)] #[cfg(feature = "pbr_multi_layer_material_textures")] pub clearcoat_texture: Option>, @@ -445,8 +445,8 @@ pub struct StandardMaterial { /// [`StandardMaterial::clearcoat_perceptual_roughness`] factor. /// /// As this is a non-color map, it must not be loaded as sRGB. - #[texture(21)] - #[sampler(22)] + #[texture(23)] + #[sampler(24)] #[cfg(feature = "pbr_multi_layer_material_textures")] pub clearcoat_roughness_texture: Option>, @@ -467,11 +467,79 @@ pub struct StandardMaterial { /// in both [`StandardMaterial::normal_map_texture`] and this field. /// /// As this is a non-color map, it must not be loaded as sRGB. - #[texture(23)] - #[sampler(24)] + #[texture(25)] + #[sampler(26)] #[cfg(feature = "pbr_multi_layer_material_textures")] pub clearcoat_normal_texture: Option>, + /// Increases the roughness along a specific direction, so that the specular + /// highlight will be stretched instead of being a circular lobe. + /// + /// This value ranges from 0 (perfectly circular) to 1 (maximally + /// stretched). The default direction (corresponding to a + /// [`StandardMaterial::anisotropy_rotation`] of 0) aligns with the + /// *tangent* of the mesh; thus mesh tangents must be specified in order for + /// this parameter to have any meaning. The direction can be changed using + /// the [`StandardMaterial::anisotropy_rotation`] parameter. + /// + /// This is typically used for modeling surfaces such as brushed metal and + /// hair, in which one direction of the surface but not the other is smooth. + /// + /// See the [`KHR_materials_anisotropy` specification] for more details. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + pub anisotropy_strength: f32, + + /// The direction of increased roughness, in radians relative to the mesh + /// tangent. + /// + /// This parameter causes the roughness to vary according to the + /// [`StandardMaterial::anisotropy_strength`]. The rotation is applied in + /// tangent-bitangent space; thus, mesh tangents must be present for this + /// parameter to have any meaning. + /// + /// This parameter has no effect if + /// [`StandardMaterial::anisotropy_strength`] is zero. Its value can + /// optionally be adjusted across the mesh with the + /// [`StandardMaterial::anisotropy_texture`]. + /// + /// See the [`KHR_materials_anisotropy` specification] for more details. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + pub anisotropy_rotation: f32, + + /// The UV channel to use for the [`StandardMaterial::anisotropy_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub anisotropy_channel: UvChannel, + + /// An image texture that allows the + /// [`StandardMaterial::anisotropy_strength`] and + /// [`StandardMaterial::anisotropy_rotation`] to vary across the mesh. + /// + /// The [`KHR_materials_anisotropy` specification] defines the format that + /// this texture must take. To summarize: The direction vector is encoded in + /// the red and green channels, while the strength is encoded in the blue + /// channels. For the direction vector, the red and green channels map the + /// color range [0, 1] to the vector range [-1, 1]. The direction vector + /// encoded in this texture modifies the default rotation direction in + /// tangent-bitangent space, before the + /// [`StandardMaterial::anisotropy_rotation`] parameter is applied. The + /// value in the blue channel is multiplied by the + /// [`StandardMaterial::anisotropy_strength`] value to produce the final + /// anisotropy strength. + /// + /// As the texel values don't represent colors, this texture must be in + /// linear color space, not sRGB. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + #[texture(13)] + #[sampler(14)] + pub anisotropy_texture: Option>, + /// Support two-sided lighting by automatically flipping the normals for "back" faces /// within the PBR lighting shader. /// @@ -739,6 +807,10 @@ impl Default for StandardMaterial { clearcoat_normal_channel: UvChannel::Uv0, #[cfg(feature = "pbr_multi_layer_material_textures")] clearcoat_normal_texture: None, + anisotropy_strength: 0.0, + anisotropy_rotation: 0.0, + anisotropy_channel: UvChannel::Uv0, + anisotropy_texture: None, flip_normal_map_y: false, double_sided: false, cull_mode: Some(Face::Back), @@ -804,6 +876,7 @@ bitflags::bitflags! { const CLEARCOAT_TEXTURE = 1 << 14; const CLEARCOAT_ROUGHNESS_TEXTURE = 1 << 15; const CLEARCOAT_NORMAL_TEXTURE = 1 << 16; + const ANISOTROPY_TEXTURE = 1 << 17; const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS; // ← Bitmask reserving bits for the `AlphaMode` const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS; // ← Values are just sequential values bitshifted into const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS; // the bitmask, and can range from 0 to 7. @@ -855,6 +928,8 @@ pub struct StandardMaterialUniform { pub attenuation_distance: f32, pub clearcoat: f32, pub clearcoat_perceptual_roughness: f32, + pub anisotropy_strength: f32, + pub anisotropy_rotation: Vec2, /// 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, @@ -919,6 +994,10 @@ impl AsBindGroupShaderType for StandardMaterial { } } + if self.anisotropy_texture.is_some() { + flags |= StandardMaterialFlags::ANISOTROPY_TEXTURE; + } + #[cfg(feature = "pbr_multi_layer_material_textures")] { if self.clearcoat_texture.is_some() { @@ -975,6 +1054,12 @@ impl AsBindGroupShaderType for StandardMaterial { let mut emissive = self.emissive.to_vec4(); emissive[3] = self.emissive_exposure_weight; + // Doing this up front saves having to do this repeatedly in the fragment shader. + let anisotropy_rotation = vec2( + self.anisotropy_rotation.cos(), + self.anisotropy_rotation.sin(), + ); + StandardMaterialUniform { base_color: LinearRgba::from(self.base_color).to_vec4(), emissive, @@ -983,6 +1068,8 @@ impl AsBindGroupShaderType for StandardMaterial { reflectance: self.reflectance, clearcoat: self.clearcoat, clearcoat_perceptual_roughness: self.clearcoat_perceptual_roughness, + anisotropy_strength: self.anisotropy_strength, + anisotropy_rotation, diffuse_transmission: self.diffuse_transmission, specular_transmission: self.specular_transmission, thickness: self.thickness, @@ -1007,26 +1094,28 @@ bitflags! { /// The pipeline key for `StandardMaterial`, packed into 64 bits. #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct StandardMaterialKey: u64 { - const CULL_FRONT = 0x01; - const CULL_BACK = 0x02; - const NORMAL_MAP = 0x04; - const RELIEF_MAPPING = 0x08; - const DIFFUSE_TRANSMISSION = 0x10; - const SPECULAR_TRANSMISSION = 0x20; - const CLEARCOAT = 0x40; - const CLEARCOAT_NORMAL_MAP = 0x80; - const BASE_COLOR_UV = 0x00100; - const EMISSIVE_UV = 0x00200; - const METALLIC_ROUGHNESS_UV = 0x00400; - const OCCLUSION_UV = 0x00800; - const SPECULAR_TRANSMISSION_UV = 0x01000; - const THICKNESS_UV = 0x02000; - const DIFFUSE_TRANSMISSION_UV = 0x04000; - const NORMAL_MAP_UV = 0x08000; - const CLEARCOAT_UV = 0x10000; - const CLEARCOAT_ROUGHNESS_UV = 0x20000; - const CLEARCOAT_NORMAL_UV = 0x40000; - const DEPTH_BIAS = 0xffffffff_00000000; + const CULL_FRONT = 0x000001; + const CULL_BACK = 0x000002; + const NORMAL_MAP = 0x000004; + const RELIEF_MAPPING = 0x000008; + const DIFFUSE_TRANSMISSION = 0x000010; + const SPECULAR_TRANSMISSION = 0x000020; + const CLEARCOAT = 0x000040; + const CLEARCOAT_NORMAL_MAP = 0x000080; + const ANISOTROPY = 0x000100; + const BASE_COLOR_UV = 0x000200; + const EMISSIVE_UV = 0x000400; + const METALLIC_ROUGHNESS_UV = 0x000800; + const OCCLUSION_UV = 0x001000; + const SPECULAR_TRANSMISSION_UV = 0x002000; + const THICKNESS_UV = 0x004000; + const DIFFUSE_TRANSMISSION_UV = 0x008000; + const NORMAL_MAP_UV = 0x010000; + const ANISOTROPY_UV = 0x020000; + const CLEARCOAT_UV = 0x040000; + const CLEARCOAT_ROUGHNESS_UV = 0x080000; + const CLEARCOAT_NORMAL_UV = 0x100000; + const DEPTH_BIAS = 0xffffffff_00000000; } } @@ -1071,6 +1160,11 @@ impl From<&StandardMaterial> for StandardMaterialKey { material.clearcoat > 0.0 && material.clearcoat_normal_texture.is_some(), ); + key.set( + StandardMaterialKey::ANISOTROPY, + material.anisotropy_strength > 0.0, + ); + key.set( StandardMaterialKey::BASE_COLOR_UV, material.base_color_channel != UvChannel::Uv0, @@ -1103,10 +1197,16 @@ impl From<&StandardMaterial> for StandardMaterialKey { material.diffuse_transmission_channel != UvChannel::Uv0, ); } + key.set( StandardMaterialKey::NORMAL_MAP_UV, material.normal_map_channel != UvChannel::Uv0, ); + key.set( + StandardMaterialKey::ANISOTROPY_UV, + material.anisotropy_channel != UvChannel::Uv0, + ); + #[cfg(feature = "pbr_multi_layer_material_textures")] { key.set( @@ -1226,6 +1326,10 @@ impl Material for StandardMaterial { StandardMaterialKey::CLEARCOAT_NORMAL_MAP, "STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP", ), + ( + StandardMaterialKey::ANISOTROPY, + "STANDARD_MATERIAL_ANISOTROPY", + ), ( StandardMaterialKey::BASE_COLOR_UV, "STANDARD_MATERIAL_BASE_COLOR_UV_B", @@ -1270,6 +1374,10 @@ impl Material for StandardMaterial { StandardMaterialKey::CLEARCOAT_NORMAL_UV, "STANDARD_MATERIAL_CLEARCOAT_NORMAL_UV_B", ), + ( + StandardMaterialKey::ANISOTROPY_UV, + "STANDARD_MATERIAL_ANISOTROPY_UV", + ), ] { if key.bind_group_data.intersects(flags) { shader_defs.push(shader_def.into()); diff --git a/crates/bevy_pbr/src/render/pbr_bindings.wgsl b/crates/bevy_pbr/src/render/pbr_bindings.wgsl index e51c22a7ea..f4a02f512b 100644 --- a/crates/bevy_pbr/src/render/pbr_bindings.wgsl +++ b/crates/bevy_pbr/src/render/pbr_bindings.wgsl @@ -15,19 +15,21 @@ @group(2) @binding(10) var normal_map_sampler: sampler; @group(2) @binding(11) var depth_map_texture: texture_2d; @group(2) @binding(12) var depth_map_sampler: sampler; +@group(2) @binding(13) var anisotropy_texture: texture_2d; +@group(2) @binding(14) var anisotropy_sampler: sampler; #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED -@group(2) @binding(13) var specular_transmission_texture: texture_2d; -@group(2) @binding(14) var specular_transmission_sampler: sampler; -@group(2) @binding(15) var thickness_texture: texture_2d; -@group(2) @binding(16) var thickness_sampler: sampler; -@group(2) @binding(17) var diffuse_transmission_texture: texture_2d; -@group(2) @binding(18) var diffuse_transmission_sampler: sampler; +@group(2) @binding(15) var specular_transmission_texture: texture_2d; +@group(2) @binding(16) var specular_transmission_sampler: sampler; +@group(2) @binding(17) var thickness_texture: texture_2d; +@group(2) @binding(18) var thickness_sampler: sampler; +@group(2) @binding(19) var diffuse_transmission_texture: texture_2d; +@group(2) @binding(20) var diffuse_transmission_sampler: sampler; #endif #ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED -@group(2) @binding(19) var clearcoat_texture: texture_2d; -@group(2) @binding(20) var clearcoat_sampler: sampler; -@group(2) @binding(21) var clearcoat_roughness_texture: texture_2d; -@group(2) @binding(22) var clearcoat_roughness_sampler: sampler; -@group(2) @binding(23) var clearcoat_normal_texture: texture_2d; -@group(2) @binding(24) var clearcoat_normal_sampler: sampler; +@group(2) @binding(21) var clearcoat_texture: texture_2d; +@group(2) @binding(22) var clearcoat_sampler: sampler; +@group(2) @binding(23) var clearcoat_roughness_texture: texture_2d; +@group(2) @binding(24) var clearcoat_roughness_sampler: sampler; +@group(2) @binding(25) var clearcoat_normal_texture: texture_2d; +@group(2) @binding(26) var clearcoat_normal_sampler: sampler; #endif diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 9f1a6349b3..7bfc4738de 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -357,6 +357,8 @@ fn pbr_input_from_standard_material( #ifdef VERTEX_UVS #ifdef VERTEX_TANGENTS + let TBN = pbr_functions::calculate_tbn_mikktspace(pbr_input.world_normal, in.world_tangent); + #ifdef STANDARD_MATERIAL_NORMAL_MAP let Nt = pbr_functions::sample_texture( @@ -372,10 +374,9 @@ fn pbr_input_from_standard_material( pbr_input.N = pbr_functions::apply_normal_mapping( pbr_bindings::material.flags, - pbr_input.world_normal, + TBN, double_sided, is_front, - in.world_tangent, Nt, view.mip_bias, ); @@ -403,10 +404,9 @@ fn pbr_input_from_standard_material( pbr_input.clearcoat_N = pbr_functions::apply_normal_mapping( pbr_bindings::material.flags, - pbr_input.world_normal, + TBN, double_sided, is_front, - in.world_tangent, clearcoat_Nt, view.mip_bias, ); @@ -418,6 +418,47 @@ fn pbr_input_from_standard_material( #endif // VERTEX_TANGENTS #endif // VERTEX_UVS + // Take anisotropy into account. + // + // This code comes from the `KHR_materials_anisotropy` spec: + // +#ifdef VERTEX_TANGENTS +#ifdef STANDARD_MATERIAL_ANISOTROPY + + var anisotropy_strength = pbr_bindings::material.anisotropy_strength; + var anisotropy_direction = pbr_bindings::material.anisotropy_rotation; + + // Adjust based on the anisotropy map if there is one. + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT) != 0u) { + let anisotropy_texel = pbr_functions::sample_texture( + pbr_bindings::anisotropy_texture, + pbr_bindings::anisotropy_sampler, +#ifdef STANDARD_MATERIAL_ANISOTROPY_UV_B + uv_b, +#else // STANDARD_MATERIAL_ANISOTROPY_UV_B + uv, +#endif // STANDARD_MATERIAL_ANISOTROPY_UV_B + bias, + ).rgb; + + let anisotropy_direction_from_texture = normalize(anisotropy_texel.rg * 2.0 - 1.0); + // Rotate by the anisotropy direction. + anisotropy_direction = + mat2x2(anisotropy_direction.xy, anisotropy_direction.yx * vec2(-1.0, 1.0)) * + anisotropy_direction_from_texture; + anisotropy_strength *= anisotropy_texel.b; + } + + pbr_input.anisotropy_strength = anisotropy_strength; + + let anisotropy_T = normalize(TBN * vec3(anisotropy_direction, 0.0)); + let anisotropy_B = normalize(cross(pbr_input.world_normal, anisotropy_T)); + pbr_input.anisotropy_T = anisotropy_T; + pbr_input.anisotropy_B = anisotropy_B; + +#endif // STANDARD_MATERIAL_ANISOTROPY +#endif // VERTEX_TANGENTS + #endif // LOAD_PREPASS_NORMALS // TODO: Meshlet support diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index e2cac858c8..51b4dc4c83 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -152,15 +152,11 @@ fn prepare_world_normal( return output; } -fn apply_normal_mapping( - standard_material_flags: u32, - world_normal: vec3, - double_sided: bool, - is_front: bool, - world_tangent: vec4, - in_Nt: vec3, - mip_bias: f32, -) -> vec3 { +// Calculates the three TBN vectors according to [mikktspace]. Returns a matrix +// with T, B, N columns in that order. +// +// [mikktspace]: http://www.mikktspace.com/ +fn calculate_tbn_mikktspace(world_normal: vec3, world_tangent: vec4) -> mat3x3 { // NOTE: The mikktspace method of normal mapping explicitly requires that the world normal NOT // be re-normalized in the fragment shader. This is primarily to match the way mikktspace // bakes vertex tangents and normal maps so that this is the exact inverse. Blender, Unity, @@ -176,6 +172,22 @@ fn apply_normal_mapping( var T: vec3 = world_tangent.xyz; var B: vec3 = world_tangent.w * cross(N, T); + return mat3x3(T, B, N); +} + +fn apply_normal_mapping( + standard_material_flags: u32, + TBN: mat3x3, + double_sided: bool, + is_front: bool, + in_Nt: vec3, + mip_bias: f32, +) -> vec3 { + // Unpack the TBN vectors. + var T = TBN[0]; + var B = TBN[1]; + var N = TBN[2]; + // Nt is the tangent-space normal. var Nt = in_Nt; if (standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u { @@ -204,6 +216,42 @@ fn apply_normal_mapping( return normalize(N); } +#ifdef STANDARD_MATERIAL_ANISOTROPY + +// Modifies the normal to achieve a better approximate direction from the +// environment map when using anisotropy. +// +// This follows the suggested implementation in the `KHR_materials_anisotropy` specification: +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md#image-based-lighting +fn bend_normal_for_anisotropy(lighting_input: ptr) { + // Unpack. + let N = (*lighting_input).layers[LAYER_BASE].N; + let roughness = (*lighting_input).layers[LAYER_BASE].roughness; + let V = (*lighting_input).V; + let anisotropy = (*lighting_input).anisotropy; + let Ba = (*lighting_input).Ba; + + var bent_normal = normalize(cross(cross(Ba, V), Ba)); + + // The `KHR_materials_anisotropy` spec states: + // + // > This heuristic can probably be improved upon + let a = pow(2.0, pow(2.0, 1.0 - anisotropy * (1.0 - roughness))); + bent_normal = normalize(mix(bent_normal, N, a)); + + // The `KHR_materials_anisotropy` spec states: + // + // > Mixing the reflection with the normal is more accurate both with and + // > without anisotropy and keeps rough objects from gathering light from + // > behind their tangent plane. + let R = normalize(mix(reflect(-V, bent_normal), bent_normal, roughness * roughness)); + + (*lighting_input).layers[LAYER_BASE].N = bent_normal; + (*lighting_input).layers[LAYER_BASE].R = R; +} + +#endif // STANDARD_MATERIAL_ANISTROPY + // NOTE: Correctly calculates the view vector depending on whether // the projection is orthographic or perspective. fn calculate_view( @@ -317,6 +365,11 @@ fn apply_pbr_lighting( lighting_input.layers[LAYER_CLEARCOAT].roughness = clearcoat_roughness; lighting_input.clearcoat_strength = clearcoat; #endif // STANDARD_MATERIAL_CLEARCOAT +#ifdef STANDARD_MATERIAL_ANISOTROPY + lighting_input.anisotropy = in.anisotropy_strength; + lighting_input.Ta = in.anisotropy_T; + lighting_input.Ba = in.anisotropy_B; +#endif // STANDARD_MATERIAL_ANISOTROPY // And do the same for transmissive if we need to. #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -339,6 +392,11 @@ fn apply_pbr_lighting( transmissive_lighting_input.layers[LAYER_CLEARCOAT].roughness = 0.0; transmissive_lighting_input.clearcoat_strength = 0.0; #endif // STANDARD_MATERIAL_CLEARCOAT +#ifdef STANDARD_MATERIAL_ANISOTROPY + lighting_input.anisotropy = in.anisotropy_strength; + lighting_input.Ta = in.anisotropy_T; + lighting_input.Ba = in.anisotropy_B; +#endif // STANDARD_MATERIAL_ANISOTROPY #endif // STANDARD_MATERIAL_DIFFUSE_TRANSMISSION let view_z = dot(vec4( @@ -507,6 +565,19 @@ fn apply_pbr_lighting( // Environment map light (indirect) #ifdef ENVIRONMENT_MAP +#ifdef STANDARD_MATERIAL_ANISOTROPY + var bent_normal_lighting_input = lighting_input; + bend_normal_for_anisotropy(&bent_normal_lighting_input); + let environment_map_lighting_input = &bent_normal_lighting_input; +#else // STANDARD_MATERIAL_ANISOTROPY + let environment_map_lighting_input = &lighting_input; +#endif // STANDARD_MATERIAL_ANISOTROPY + + let environment_light = environment_map::environment_map_light( + environment_map_lighting_input, + any(indirect_light != vec3(0.0f)) + ); + // If screen space reflections are going to be used for this material, don't // accumulate environment map light yet. The SSR shader will do it. #ifdef SCREEN_SPACE_REFLECTIONS diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index 7b0c108c1b..727721acfc 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -70,7 +70,7 @@ struct LightingInput { // The world-space position. P: vec3, - // The vector to the light. + // The vector to the view. V: vec3, // The diffuse color of the material. @@ -91,6 +91,18 @@ struct LightingInput { // The strength of the clearcoat layer. clearcoat_strength: f32, #endif // STANDARD_MATERIAL_CLEARCOAT + +#ifdef STANDARD_MATERIAL_ANISOTROPY + // The anisotropy strength, reflecting the amount of increased roughness in + // the tangent direction. + anisotropy: f32, + // The tangent direction for anisotropy: i.e. the direction in which + // roughness increases. + Ta: vec3, + // The bitangent direction, which is the cross product of the normal with + // the tangent direction. + Ba: vec3, +#endif // STANDARD_MATERIAL_ANISOTROPY } // Values derived from the `LightingInput` for both diffuse and specular lights. @@ -133,6 +145,30 @@ fn D_GGX(roughness: f32, NdotH: f32, h: vec3) -> f32 { return d; } +// An approximation of the anisotropic GGX distribution function. +// +// 1 +// D(𝐡) = ─────────────────────────────────────────────────── +// παₜα_b((𝐡 ⋅ 𝐭)² / αₜ²) + (𝐡 ⋅ 𝐛)² / α_b² + (𝐡 ⋅ 𝐧)²)² +// +// * `T` = 𝐭 = the tangent direction = the direction of increased roughness. +// +// * `B` = 𝐛 = the bitangent direction = the direction of decreased roughness. +// +// * `at` = αₜ = the alpha-roughness in the tangent direction. +// +// * `ab` = α_b = the alpha-roughness in the bitangent direction. +// +// This is from the `KHR_materials_anisotropy` spec: +// +fn D_GGX_anisotropic(at: f32, ab: f32, NdotH: f32, TdotH: f32, BdotH: f32) -> f32 { + let a2 = at * ab; + let f = vec3(ab * TdotH, at * BdotH, a2 * NdotH); + let w2 = a2 / dot(f, f); + let d = a2 * w2 * w2 * (1.0 / PI); + return d; +} + // Visibility function (Specular G) // V(v,l,a) = G(v,l,α) / { 4 (n⋅v) (n⋅l) } // such that f_r becomes @@ -148,6 +184,23 @@ fn V_SmithGGXCorrelated(roughness: f32, NdotV: f32, NdotL: f32) -> f32 { return v; } +// The visibility function, anisotropic variant. +fn V_GGX_anisotropic( + at: f32, + ab: f32, + NdotL: f32, + NdotV: f32, + BdotV: f32, + TdotV: f32, + TdotL: f32, + BdotL: f32, +) -> f32 { + let GGX_V = NdotL * length(vec3(at * TdotV, ab * BdotV, NdotV)); + let GGX_L = NdotV * length(vec3(at * TdotL, ab * BdotL, NdotL)); + let v = 0.5 / (GGX_V + GGX_L); + return saturate(v); +} + // A simpler, but nonphysical, alternative to Smith-GGX. We use this for // clearcoat, per the Filament spec. // @@ -176,6 +229,27 @@ fn fresnel(f0: vec3, LdotH: f32) -> vec3 { return F_Schlick_vec(f0, f90, LdotH); } +// Given distribution, visibility, and Fresnel term, calculates the final +// specular light. +// +// Multiscattering approximation: +// +fn specular_multiscatter( + input: ptr, + D: f32, + V: f32, + F: vec3, + specular_intensity: f32, +) -> vec3 { + // Unpack. + let F0 = (*input).F0_; + let F_ab = (*input).F_ab; + + var Fr = (specular_intensity * D * V) * F; + Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0); + return Fr; +} + // Specular BRDF // https://google.github.io/filament/Filament.html#materialsystem/specularbrdf @@ -226,7 +300,6 @@ fn specular( let roughness = (*input).layers[LAYER_BASE].roughness; let NdotV = (*input).layers[LAYER_BASE].NdotV; let F0 = (*input).F0_; - let F_ab = (*input).F_ab; let H = (*derived_input).H; let NdotL = (*derived_input).NdotL; let NdotH = (*derived_input).NdotH; @@ -240,10 +313,7 @@ fn specular( let F = fresnel(F0, LdotH); // Calculate the specular light. - // Multiscattering approximation: - // - var Fr = (specular_intensity * D * V) * F; - Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0); + let Fr = specular_multiscatter(input, D, V, F, specular_intensity); return Fr; } @@ -275,6 +345,48 @@ fn specular_clearcoat( return vec2(Fc, Frc); } +#ifdef STANDARD_MATERIAL_ANISOTROPY + +fn specular_anisotropy( + input: ptr, + derived_input: ptr, + L: vec3, + specular_intensity: f32, +) -> vec3 { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let V = (*input).V; + let F0 = (*input).F0_; + let anisotropy = (*input).anisotropy; + let Ta = (*input).Ta; + let Ba = (*input).Ba; + let H = (*derived_input).H; + let NdotL = (*derived_input).NdotL; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + let TdotL = dot(Ta, L); + let BdotL = dot(Ba, L); + let TdotH = dot(Ta, H); + let BdotH = dot(Ba, H); + let TdotV = dot(Ta, V); + let BdotV = dot(Ba, V); + + let ab = roughness * roughness; + let at = mix(ab, 1.0, anisotropy * anisotropy); + + let Da = D_GGX_anisotropic(at, ab, NdotH, TdotH, BdotH); + let Va = V_GGX_anisotropic(at, ab, NdotL, NdotV, BdotV, TdotV, TdotL, BdotL); + let Fa = fresnel(F0, LdotH); + + // Calculate the specular light. + let Fr = specular_multiscatter(input, Da, Va, Fa, specular_intensity); + return Fr; +} + +#endif // STANDARD_MATERIAL_ANISOTROPY + // Diffuse BRDF // https://google.github.io/filament/Filament.html#materialsystem/diffusebrdf // fd(v,l) = σ/π * 1 / { |n⋅v||n⋅l| } ∫Ω D(m,α) G(v,l,m) (v⋅m) (l⋅m) dm @@ -337,6 +449,7 @@ fn point_light(light_id: u32, input: ptr) -> vec3 let light = &view_bindings::point_lights.data[light_id]; let light_to_frag = (*light).position_radius.xyz - P; + let L = normalize(light_to_frag); let distance_square = dot(light_to_frag, light_to_frag); let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); @@ -352,7 +465,12 @@ fn point_light(light_id: u32, input: ptr) -> vec3 var specular_derived_input = derive_lighting_input(N, V, specular_L_intensity.xyz); let specular_intensity = specular_L_intensity.w; + +#ifdef STANDARD_MATERIAL_ANISOTROPY + let specular_light = specular_anisotropy(input, &specular_derived_input, L, specular_intensity); +#else // STANDARD_MATERIAL_ANISOTROPY let specular_light = specular(input, &specular_derived_input, specular_intensity); +#endif // STANDARD_MATERIAL_ANISOTROPY // Clearcoat @@ -388,7 +506,6 @@ fn point_light(light_id: u32, input: ptr) -> vec3 // Diffuse. // Comes after specular since its N⋅L is used in the lighting equation. - let L = normalize(light_to_frag); var derived_input = derive_lighting_input(N, V, L); let diffuse = diffuse_color * Fd_Burley(input, &derived_input); @@ -453,12 +570,16 @@ fn directional_light(light_id: u32, input: ptr) -> vec3 let light = &view_bindings::lights.directional_lights[light_id]; - let incident_light = (*light).direction_to_light.xyz; - var derived_input = derive_lighting_input(N, V, incident_light); + let L = (*light).direction_to_light.xyz; + var derived_input = derive_lighting_input(N, V, L); let diffuse = diffuse_color * Fd_Burley(input, &derived_input); +#ifdef STANDARD_MATERIAL_ANISOTROPY + let specular_light = specular_anisotropy(input, &derived_input, L, 1.0); +#else // STANDARD_MATERIAL_ANISOTROPY let specular_light = specular(input, &derived_input, 1.0); +#endif // STANDARD_MATERIAL_ANISOTROPY #ifdef STANDARD_MATERIAL_CLEARCOAT let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; @@ -467,7 +588,7 @@ fn directional_light(light_id: u32, input: ptr) -> vec3 // Perform specular input calculations again for the clearcoat layer. We // can't reuse the above because the clearcoat normal might be different // from the main layer normal. - var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, incident_light); + var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, L); let Fc_Frc = specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, 1.0); diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 27a22dc094..d9b600c40b 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -17,6 +17,8 @@ struct StandardMaterial { attenuation_distance: f32, clearcoat: f32, clearcoat_perceptual_roughness: f32, + anisotropy_strength: f32, + anisotropy_rotation: vec2, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, alpha_cutoff: f32, @@ -49,6 +51,7 @@ const STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT: u32 = 8192u; const STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT: u32 = 16384u; const STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT: u32 = 32768u; const STANDARD_MATERIAL_FLAGS_CLEARCOAT_NORMAL_TEXTURE_BIT: u32 = 65536u; +const STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT: u32 = 131072u; const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3758096384u; // (0b111u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 536870912u; // (1u32 << 29) @@ -109,6 +112,11 @@ struct PbrInput { V: vec3, lightmap_light: vec3, clearcoat_N: vec3, + anisotropy_strength: f32, + // These two aren't specific to anisotropy, but we only fill them in if + // we're doing anisotropy, so they're prefixed with `anisotropy_`. + anisotropy_T: vec3, + anisotropy_B: vec3, is_orthographic: bool, flags: u32, }; @@ -131,6 +139,10 @@ fn pbr_input_new() -> PbrInput { pbr_input.N = vec3(0.0, 0.0, 1.0); pbr_input.V = vec3(1.0, 0.0, 0.0); + pbr_input.clearcoat_N = vec3(0.0); + pbr_input.anisotropy_T = vec3(0.0); + pbr_input.anisotropy_B = vec3(0.0); + pbr_input.lightmap_light = vec3(0.0); pbr_input.flags = 0u; diff --git a/examples/3d/anisotropy.rs b/examples/3d/anisotropy.rs new file mode 100644 index 0000000000..fb69c7ddbb --- /dev/null +++ b/examples/3d/anisotropy.rs @@ -0,0 +1,315 @@ +//! Demonstrates anisotropy with the glTF sample barn lamp model. + +use bevy::{ + color::palettes::css::WHITE, core_pipeline::Skybox, math::vec3, prelude::*, time::Stopwatch, +}; + +/// The initial position of the camera. +const CAMERA_INITIAL_POSITION: Vec3 = vec3(-0.4, 0.0, 0.0); + +/// The current settings of the app, as chosen by the user. +#[derive(Resource)] +struct AppStatus { + /// Which type of light is in the scene. + light_mode: LightMode, + /// Whether anisotropy is enabled. + anisotropy_enabled: bool, +} + +/// Which type of light we're using: a directional light, a point light, or an +/// environment map. +#[derive(Clone, Copy, PartialEq, Default)] +enum LightMode { + /// A rotating directional light. + #[default] + Directional, + /// A rotating point light. + Point, + /// An environment map (image-based lighting, including skybox). + EnvironmentMap, +} + +/// A component that stores the version of the material with anisotropy and the +/// version of the material without it. +/// +/// This is placed on each mesh with a material. It exists so that the +/// appropriate system can replace the materials when the user presses Enter to +/// turn anisotropy on and off. +#[derive(Component)] +struct MaterialVariants { + /// The version of the material in the glTF file, with anisotropy. + anisotropic: Handle, + /// The version of the material with anisotropy removed. + isotropic: Handle, +} + +/// The application entry point. +fn main() { + App::new() + .init_resource::() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Anisotropy Example".into(), + ..default() + }), + ..default() + })) + .add_systems(Startup, setup) + .add_systems(Update, create_material_variants) + .add_systems(Update, animate_light) + .add_systems(Update, rotate_camera) + .add_systems(Update, (handle_input, update_help_text).chain()) + .run(); +} + +/// Creates the initial scene. +fn setup(mut commands: Commands, asset_server: Res, app_status: Res) { + commands.spawn(Camera3dBundle { + transform: Transform::from_translation(CAMERA_INITIAL_POSITION) + .looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + spawn_directional_light(&mut commands); + + commands.spawn(SceneBundle { + scene: asset_server.load("models/AnisotropyBarnLamp/AnisotropyBarnLamp.gltf#Scene0"), + transform: Transform::from_xyz(0.0, 0.07, -0.13), + ..default() + }); + + spawn_text(&mut commands, &asset_server, &app_status); +} + +/// Spawns the help text. +fn spawn_text(commands: &mut Commands, asset_server: &AssetServer, app_status: &AppStatus) { + commands.spawn( + TextBundle { + text: app_status.create_help_text(asset_server), + ..default() + } + .with_style(Style { + position_type: PositionType::Absolute, + bottom: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ); +} + +/// For each material, creates a version with the anisotropy removed. +/// +/// This allows the user to press Enter to toggle anisotropy on and off. +fn create_material_variants( + mut commands: Commands, + mut materials: ResMut>, + new_meshes: Query< + (Entity, &Handle), + (Added>, Without), + >, +) { + for (entity, anisotropic_material_handle) in new_meshes.iter() { + let Some(anisotropic_material) = materials.get(anisotropic_material_handle).cloned() else { + continue; + }; + + commands.entity(entity).insert(MaterialVariants { + anisotropic: anisotropic_material_handle.clone(), + isotropic: materials.add(StandardMaterial { + anisotropy_texture: None, + anisotropy_strength: 0.0, + anisotropy_rotation: 0.0, + ..anisotropic_material + }), + }); + } +} + +/// A system that animates the light every frame, if there is one. +fn animate_light( + mut lights: Query<&mut Transform, Or<(With, With)>>, + time: Res