Implement PBR anisotropy per KHR_materials_anisotropy. (#13450)

This commit implements support for physically-based anisotropy in Bevy's
`StandardMaterial`, following the specification for the
[`KHR_materials_anisotropy`] glTF extension.

[*Anisotropy*] (not to be confused with [anisotropic filtering]) is a
PBR feature that allows roughness to vary along the tangent and
bitangent directions of a mesh. In effect, this causes the specular
light to stretch out into lines instead of a round lobe. This is useful
for modeling brushed metal, hair, and similar surfaces. Support for
anisotropy is a common feature in major game and graphics engines;
Unity, Unreal, Godot, three.js, and Blender all support it to varying
degrees.

Two new parameters have been added to `StandardMaterial`:
`anisotropy_strength` and `anisotropy_rotation`. Anisotropy strength,
which ranges from 0 to 1, represents how much the roughness differs
between the tangent and the bitangent of the mesh. In effect, it
controls how stretched the specular highlight is. Anisotropy rotation
allows the roughness direction to differ from the tangent of the model.

In addition to these two fixed parameters, an *anisotropy texture* can
be supplied. Such a texture should be a 3-channel RGB texture, where the
red and green values specify a direction vector using the same
conventions as a normal map ([0, 1] color values map to [-1, 1] vector
values), and the the blue value represents the strength. This matches
the format that the [`KHR_materials_anisotropy`] specification requires.
Such textures should be loaded as linear and not sRGB. Note that this
texture does consume one additional texture binding in the standard
material shader.

The glTF loader has been updated to properly parse the
`KHR_materials_anisotropy` extension.

A new example, `anisotropy`, has been added. This example loads and
displays the barn lamp example from the [`glTF-Sample-Assets`]
repository. Note that the textures were rather large, so I shrunk them
down and converted them to a mixture of JPEG and KTX2 format, in the
interests of saving space in the Bevy repository.

[*Anisotropy*]:
https://google.github.io/filament/Filament.md.html#materialsystem/anisotropicmodel

[anisotropic filtering]:
https://en.wikipedia.org/wiki/Anisotropic_filtering

[`KHR_materials_anisotropy`]:
https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md

[`glTF-Sample-Assets`]:
https://github.com/KhronosGroup/glTF-Sample-Assets/

## Changelog

### Added

* Physically-based anisotropy is now available for materials, which
enhances the look of surfaces such as brushed metal or hair. glTF scenes
can use the new feature with the `KHR_materials_anisotropy` extension.

## Screenshots

With anisotropy:
![Screenshot 2024-05-20
233414](https://github.com/bevyengine/bevy/assets/157897/379f1e42-24e9-40b6-a430-f7d1479d0335)

Without anisotropy:
![Screenshot 2024-05-20
233420](https://github.com/bevyengine/bevy/assets/157897/aa220f05-b8e7-417c-9671-b242d4bf9fc4)
This commit is contained in:
Patrick Walton 2024-06-03 16:46:06 -07:00 committed by GitHub
parent 257fec996f
commit df8ccb8735
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1163 additions and 74 deletions

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View file

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

View file

@ -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:
/// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md>
#[derive(Default)]
struct AnisotropyExtension {
anisotropy_strength: Option<f64>,
anisotropy_rotation: Option<f64>,
anisotropy_channel: UvChannel,
anisotropy_texture: Option<Handle<Image>>,
}
impl AnisotropyExtension {
fn parse(
load_context: &mut LoadContext,
document: &Document,
material: &Material,
) -> Option<AnisotropyExtension> {
let extension = material
.extensions()?
.get("KHR_materials_anisotropy")?
.as_object()?;
let (anisotropy_channel, anisotropy_texture) = extension
.get("anisotropyTexture")
.and_then(|value| value::from_value::<json::texture::Info>(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,

View file

@ -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<Handle<Image>>,
@ -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<Handle<Image>>,
@ -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<Handle<Image>>,
@ -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<Handle<Image>>,
@ -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<Handle<Image>>,
@ -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<Handle<Image>>,
/// 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<Handle<Image>>,
/// 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<StandardMaterialUniform> 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<StandardMaterialUniform> 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<StandardMaterialUniform> 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());

View file

@ -15,19 +15,21 @@
@group(2) @binding(10) var normal_map_sampler: sampler;
@group(2) @binding(11) var depth_map_texture: texture_2d<f32>;
@group(2) @binding(12) var depth_map_sampler: sampler;
@group(2) @binding(13) var anisotropy_texture: texture_2d<f32>;
@group(2) @binding(14) var anisotropy_sampler: sampler;
#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED
@group(2) @binding(13) var specular_transmission_texture: texture_2d<f32>;
@group(2) @binding(14) var specular_transmission_sampler: sampler;
@group(2) @binding(15) var thickness_texture: texture_2d<f32>;
@group(2) @binding(16) var thickness_sampler: sampler;
@group(2) @binding(17) var diffuse_transmission_texture: texture_2d<f32>;
@group(2) @binding(18) var diffuse_transmission_sampler: sampler;
@group(2) @binding(15) var specular_transmission_texture: texture_2d<f32>;
@group(2) @binding(16) var specular_transmission_sampler: sampler;
@group(2) @binding(17) var thickness_texture: texture_2d<f32>;
@group(2) @binding(18) var thickness_sampler: sampler;
@group(2) @binding(19) var diffuse_transmission_texture: texture_2d<f32>;
@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<f32>;
@group(2) @binding(20) var clearcoat_sampler: sampler;
@group(2) @binding(21) var clearcoat_roughness_texture: texture_2d<f32>;
@group(2) @binding(22) var clearcoat_roughness_sampler: sampler;
@group(2) @binding(23) var clearcoat_normal_texture: texture_2d<f32>;
@group(2) @binding(24) var clearcoat_normal_sampler: sampler;
@group(2) @binding(21) var clearcoat_texture: texture_2d<f32>;
@group(2) @binding(22) var clearcoat_sampler: sampler;
@group(2) @binding(23) var clearcoat_roughness_texture: texture_2d<f32>;
@group(2) @binding(24) var clearcoat_roughness_sampler: sampler;
@group(2) @binding(25) var clearcoat_normal_texture: texture_2d<f32>;
@group(2) @binding(26) var clearcoat_normal_sampler: sampler;
#endif

View file

@ -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:
// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md#individual-lights>
#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

View file

@ -152,15 +152,11 @@ fn prepare_world_normal(
return output;
}
fn apply_normal_mapping(
standard_material_flags: u32,
world_normal: vec3<f32>,
double_sided: bool,
is_front: bool,
world_tangent: vec4<f32>,
in_Nt: vec3<f32>,
mip_bias: f32,
) -> vec3<f32> {
// 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<f32>, world_tangent: vec4<f32>) -> mat3x3<f32> {
// 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<f32> = world_tangent.xyz;
var B: vec3<f32> = world_tangent.w * cross(N, T);
return mat3x3(T, B, N);
}
fn apply_normal_mapping(
standard_material_flags: u32,
TBN: mat3x3<f32>,
double_sided: bool,
is_front: bool,
in_Nt: vec3<f32>,
mip_bias: f32,
) -> vec3<f32> {
// 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<function, lighting::LightingInput>) {
// 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<f32>(
@ -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

View file

@ -70,7 +70,7 @@ struct LightingInput {
// The world-space position.
P: vec3<f32>,
// The vector to the light.
// The vector to the view.
V: vec3<f32>,
// 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<f32>,
// The bitangent direction, which is the cross product of the normal with
// the tangent direction.
Ba: vec3<f32>,
#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>) -> 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:
// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md#individual-lights>
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 (nv) (nl) }
// 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<f32>, LdotH: f32) -> vec3<f32> {
return F_Schlick_vec(f0, f90, LdotH);
}
// Given distribution, visibility, and Fresnel term, calculates the final
// specular light.
//
// Multiscattering approximation:
// <https://google.github.io/filament/Filament.html#listing_energycompensationimpl>
fn specular_multiscatter(
input: ptr<function, LightingInput>,
D: f32,
V: f32,
F: vec3<f32>,
specular_intensity: f32,
) -> vec3<f32> {
// 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:
// <https://google.github.io/filament/Filament.html#listing_energycompensationimpl>
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<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
L: vec3<f32>,
specular_intensity: f32,
) -> vec3<f32> {
// 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 / { |nv||nl| } Ω D(m,α) G(v,l,m) (vm) (lm) dm
@ -337,6 +449,7 @@ fn point_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32>
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<function, LightingInput>) -> vec3<f32>
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<function, LightingInput>) -> vec3<f32>
// Diffuse.
// Comes after specular since its NL 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<function, LightingInput>) -> 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<function, LightingInput>) -> 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);

View file

@ -17,6 +17,8 @@ struct StandardMaterial {
attenuation_distance: f32,
clearcoat: f32,
clearcoat_perceptual_roughness: f32,
anisotropy_strength: f32,
anisotropy_rotation: vec2<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,
@ -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<f32>,
lightmap_light: vec3<f32>,
clearcoat_N: vec3<f32>,
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<f32>,
anisotropy_B: vec3<f32>,
is_orthographic: bool,
flags: u32,
};
@ -131,6 +139,10 @@ fn pbr_input_new() -> PbrInput {
pbr_input.N = vec3<f32>(0.0, 0.0, 1.0);
pbr_input.V = vec3<f32>(1.0, 0.0, 0.0);
pbr_input.clearcoat_N = vec3<f32>(0.0);
pbr_input.anisotropy_T = vec3<f32>(0.0);
pbr_input.anisotropy_B = vec3<f32>(0.0);
pbr_input.lightmap_light = vec3<f32>(0.0);
pbr_input.flags = 0u;

315
examples/3d/anisotropy.rs Normal file
View file

@ -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<StandardMaterial>,
/// The version of the material with anisotropy removed.
isotropic: Handle<StandardMaterial>,
}
/// The application entry point.
fn main() {
App::new()
.init_resource::<AppStatus>()
.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<AssetServer>, app_status: Res<AppStatus>) {
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<Assets<StandardMaterial>>,
new_meshes: Query<
(Entity, &Handle<StandardMaterial>),
(Added<Handle<StandardMaterial>>, Without<MaterialVariants>),
>,
) {
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<DirectionalLight>, With<PointLight>)>>,
time: Res<Time>,
) {
let now = time.elapsed_seconds();
for mut transform in lights.iter_mut() {
transform.translation = vec3(f32::cos(now), 1.0, f32::sin(now)) * vec3(3.0, 4.0, 3.0);
transform.look_at(Vec3::ZERO, Vec3::Y);
}
}
/// A system that rotates the camera if the environment map is enabled.
fn rotate_camera(
mut camera: Query<&mut Transform, With<Camera>>,
app_status: Res<AppStatus>,
time: Res<Time>,
mut stopwatch: Local<Stopwatch>,
) {
if app_status.light_mode == LightMode::EnvironmentMap {
stopwatch.tick(time.delta());
}
let now = stopwatch.elapsed_secs();
for mut transform in camera.iter_mut() {
*transform = Transform::from_translation(
Quat::from_rotation_y(now).mul_vec3(CAMERA_INITIAL_POSITION),
)
.looking_at(Vec3::ZERO, Vec3::Y);
}
}
/// Handles requests from the user to change the lighting or toggle anisotropy.
fn handle_input(
mut commands: Commands,
asset_server: Res<AssetServer>,
cameras: Query<Entity, With<Camera>>,
lights: Query<Entity, Or<(With<DirectionalLight>, With<PointLight>)>>,
mut meshes: Query<(&mut Handle<StandardMaterial>, &MaterialVariants)>,
keyboard: Res<ButtonInput<KeyCode>>,
mut app_status: ResMut<AppStatus>,
) {
// If Space was pressed, change the lighting.
if keyboard.just_pressed(KeyCode::Space) {
match app_status.light_mode {
LightMode::Directional => {
// Switch to a point light. Despawn all existing lights and
// create the light point.
app_status.light_mode = LightMode::Point;
for light in lights.iter() {
commands.entity(light).despawn();
}
spawn_point_light(&mut commands);
}
LightMode::Point => {
// Switch to the environment map. Despawn all existing lights,
// and create the skybox and environment map.
app_status.light_mode = LightMode::EnvironmentMap;
for light in lights.iter() {
commands.entity(light).despawn();
}
for camera in cameras.iter() {
add_skybox_and_environment_map(&mut commands, &asset_server, camera);
}
}
LightMode::EnvironmentMap => {
// Switch back to a directional light. Despawn the skybox and
// environment map light, and recreate the directional light.
app_status.light_mode = LightMode::Directional;
for camera in cameras.iter() {
commands
.entity(camera)
.remove::<Skybox>()
.remove::<EnvironmentMapLight>();
}
spawn_directional_light(&mut commands);
}
}
}
// If Enter was pressed, toggle anisotropy on and off.
if keyboard.just_pressed(KeyCode::Enter) {
app_status.anisotropy_enabled = !app_status.anisotropy_enabled;
// Go through each mesh and alter its material.
for (mut material_handle, material_variants) in meshes.iter_mut() {
*material_handle = if app_status.anisotropy_enabled {
material_variants.anisotropic.clone()
} else {
material_variants.isotropic.clone()
}
}
}
}
/// A system that updates the help text based on the current app status.
fn update_help_text(
mut text_query: Query<&mut Text>,
app_status: Res<AppStatus>,
asset_server: Res<AssetServer>,
) {
for mut text in text_query.iter_mut() {
*text = app_status.create_help_text(&asset_server);
}
}
/// Adds the skybox and environment map to the scene.
fn add_skybox_and_environment_map(
commands: &mut Commands,
asset_server: &AssetServer,
entity: Entity,
) {
commands
.entity(entity)
.insert(Skybox {
brightness: 5000.0,
image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
})
.insert(EnvironmentMapLight {
diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
intensity: 2500.0,
});
}
/// Spawns a rotating directional light.
fn spawn_directional_light(commands: &mut Commands) {
commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight {
color: WHITE.into(),
illuminance: 3000.0,
..default()
},
..default()
});
}
/// Spawns a rotating point light.
fn spawn_point_light(commands: &mut Commands) {
commands.spawn(PointLightBundle {
point_light: PointLight {
color: WHITE.into(),
intensity: 200000.0,
..default()
},
..default()
});
}
impl AppStatus {
/// Creates the help text as appropriate for the current app status.
fn create_help_text(&self, asset_server: &AssetServer) -> Text {
// Choose the appropriate help text for the anisotropy toggle.
let material_variant_help_text = if self.anisotropy_enabled {
"Press Enter to disable anisotropy"
} else {
"Press Enter to enable anisotropy"
};
// Choose the appropriate help text for the light toggle.
let light_help_text = match self.light_mode {
LightMode::Directional => "Press Space to switch to a point light",
LightMode::Point => "Press Space to switch to an environment map",
LightMode::EnvironmentMap => "Press Space to switch to a directional light",
};
// Build the `Text` object.
Text::from_section(
format!("{}\n{}", material_variant_help_text, light_help_text),
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 24.0,
..default()
},
)
}
}
impl Default for AppStatus {
fn default() -> Self {
Self {
light_mode: default(),
anisotropy_enabled: true,
}
}
}

View file

@ -128,6 +128,7 @@ Example | Description
[3D Shapes](../examples/3d/3d_shapes.rs) | A scene showcasing the built-in 3D shapes
[3D Viewport To World](../examples/3d/3d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world` method
[Animated Material](../examples/3d/animated_material.rs) | Shows how to animate material properties
[Anisotropy](../examples/3d/anisotropy.rs) | Displays an example model with anisotropy
[Anti-aliasing](../examples/3d/anti_aliasing.rs) | Compares different anti-aliasing methods
[Atmospheric Fog](../examples/3d/atmospheric_fog.rs) | A scene showcasing the atmospheric fog effect
[Auto Exposure](../examples/3d/auto_exposure.rs) | A scene showcasing auto exposure

View file

@ -16,6 +16,7 @@ iy = "iy" # Variable name used in bevy_gizmos. Probably stands for "y-axis ind
ser = "ser" # ron::ser - Serializer
SME = "SME" # Subject Matter Expert
Sur = "Sur" # macOS Big Sur - South
Ba = "Ba" # Bitangent for Anisotropy
# Match Inside a Word - Case Insensitive
[default.extend-words]