diff --git a/Cargo.toml b/Cargo.toml index e5e600bca4..502f73a17e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -430,6 +430,9 @@ pbr_anisotropy_texture = ["bevy_internal/pbr_anisotropy_texture"] # Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs experimental_pbr_pcss = ["bevy_internal/experimental_pbr_pcss"] +# Enable support for specular textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs +pbr_specular_textures = ["bevy_internal/pbr_specular_textures"] + # Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU. webgl2 = ["bevy_internal/webgl"] @@ -4020,6 +4023,18 @@ description = "Demonstrates how to make materials that use bindless textures" category = "Shaders" wasm = true +[[example]] +name = "specular_tint" +path = "examples/3d/specular_tint.rs" +doc-scrape-examples = true +required-features = ["pbr_specular_textures"] + +[package.metadata.example.specular_tint] +name = "Specular Tint" +description = "Demonstrates specular tints and maps" +category = "3D Rendering" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/textures/AlphaNoise.png b/assets/textures/AlphaNoise.png new file mode 100644 index 0000000000..4fa9518103 Binary files /dev/null and b/assets/textures/AlphaNoise.png differ diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index c979ae47f4..991d0776b8 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -15,6 +15,7 @@ pbr_multi_layer_material_textures = [ "bevy_pbr/pbr_multi_layer_material_textures", ] pbr_anisotropy_texture = ["bevy_pbr/pbr_anisotropy_texture"] +pbr_specular_textures = [] [dependencies] # bevy diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 0a86fa45ef..b4453ab24f 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -51,6 +51,11 @@ use gltf::{ Document, Material, Node, Primitive, Semantic, }; use serde::{Deserialize, Serialize}; +#[cfg(any( + feature = "pbr_specular_textures", + feature = "pbr_multi_layer_material_textures" +))] +use serde_json::Map; use serde_json::{value, Value}; use std::{ io::Error, @@ -1235,6 +1240,10 @@ fn load_material( let anisotropy = AnisotropyExtension::parse(load_context, document, material).unwrap_or_default(); + // Parse the `KHR_materials_specular` extension data if necessary. + let specular = + SpecularExtension::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); @@ -1303,6 +1312,21 @@ fn load_material( anisotropy_channel: anisotropy.anisotropy_channel, #[cfg(feature = "pbr_anisotropy_texture")] anisotropy_texture: anisotropy.anisotropy_texture, + // From the `KHR_materials_specular` spec: + // + reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5, + #[cfg(feature = "pbr_specular_textures")] + specular_channel: specular.specular_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: specular.specular_texture, + specular_tint: match specular.specular_color_factor { + Some(color) => Color::linear_rgb(color[0] as f32, color[1] as f32, color[2] as f32), + None => Color::WHITE, + }, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_channel: specular.specular_color_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_texture: specular.specular_color_texture, ..Default::default() } }) @@ -1731,7 +1755,8 @@ fn texture_handle(load_context: &mut LoadContext, texture: &gltf::Texture) -> Ha /// for an extension, forcing us to parse its texture references manually. #[cfg(any( feature = "pbr_anisotropy_texture", - feature = "pbr_multi_layer_material_textures" + feature = "pbr_multi_layer_material_textures", + feature = "pbr_specular_textures" ))] fn texture_handle_from_info( load_context: &mut LoadContext, @@ -2122,40 +2147,35 @@ impl ClearcoatExtension { .as_object()?; #[cfg(feature = "pbr_multi_layer_material_textures")] - let (clearcoat_channel, clearcoat_texture) = extension - .get("clearcoatTexture") - .and_then(|value| value::from_value::(value.clone()).ok()) - .map(|json_info| { - ( - get_uv_channel(material, "clearcoat", json_info.tex_coord), - texture_handle_from_info(load_context, document, &json_info), - ) - }) - .unzip(); + let (clearcoat_channel, clearcoat_texture) = parse_material_extension_texture( + load_context, + document, + material, + extension, + "clearcoatTexture", + "clearcoat", + ); #[cfg(feature = "pbr_multi_layer_material_textures")] - let (clearcoat_roughness_channel, clearcoat_roughness_texture) = extension - .get("clearcoatRoughnessTexture") - .and_then(|value| value::from_value::(value.clone()).ok()) - .map(|json_info| { - ( - get_uv_channel(material, "clearcoat roughness", json_info.tex_coord), - texture_handle_from_info(load_context, document, &json_info), - ) - }) - .unzip(); + let (clearcoat_roughness_channel, clearcoat_roughness_texture) = + parse_material_extension_texture( + load_context, + document, + material, + extension, + "clearcoatRoughnessTexture", + "clearcoat roughness", + ); #[cfg(feature = "pbr_multi_layer_material_textures")] - let (clearcoat_normal_channel, clearcoat_normal_texture) = extension - .get("clearcoatNormalTexture") - .and_then(|value| value::from_value::(value.clone()).ok()) - .map(|json_info| { - ( - get_uv_channel(material, "clearcoat normal", json_info.tex_coord), - texture_handle_from_info(load_context, document, &json_info), - ) - }) - .unzip(); + let (clearcoat_normal_channel, clearcoat_normal_texture) = parse_material_extension_texture( + load_context, + document, + material, + extension, + "clearcoatNormalTexture", + "clearcoat normal", + ); Some(ClearcoatExtension { clearcoat_factor: extension.get("clearcoatFactor").and_then(Value::as_f64), @@ -2163,15 +2183,15 @@ impl ClearcoatExtension { .get("clearcoatRoughnessFactor") .and_then(Value::as_f64), #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_channel: clearcoat_channel.unwrap_or_default(), + clearcoat_channel, #[cfg(feature = "pbr_multi_layer_material_textures")] clearcoat_texture, #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_channel: clearcoat_roughness_channel.unwrap_or_default(), + clearcoat_roughness_channel, #[cfg(feature = "pbr_multi_layer_material_textures")] clearcoat_roughness_texture, #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_channel: clearcoat_normal_channel.unwrap_or_default(), + clearcoat_normal_channel, #[cfg(feature = "pbr_multi_layer_material_textures")] clearcoat_normal_texture, }) @@ -2234,6 +2254,121 @@ impl AnisotropyExtension { } } +/// Parsed data from the `KHR_materials_specular` extension. +/// +/// We currently don't parse `specularFactor` and `specularTexture`, since +/// they're incompatible with Filament. +/// +/// Note that the map is a *specular map*, not a *reflectance map*. In Bevy and +/// Filament terms, the reflectance values in the specular map range from [0.0, +/// 0.5], rather than [0.0, 1.0]. This is an unfortunate +/// `KHR_materials_specular` specification requirement that stems from the fact +/// that glTF is specified in terms of a specular strength model, not the +/// reflectance model that Filament and Bevy use. A workaround, which is noted +/// in the [`StandardMaterial`] documentation, is to set the reflectance value +/// to 2.0, which spreads the specular map range from [0.0, 1.0] as normal. +/// +/// See the specification: +/// +#[derive(Default)] +struct SpecularExtension { + specular_factor: Option, + #[cfg(feature = "pbr_specular_textures")] + specular_channel: UvChannel, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: Option>, + specular_color_factor: Option<[f64; 3]>, + #[cfg(feature = "pbr_specular_textures")] + specular_color_channel: UvChannel, + #[cfg(feature = "pbr_specular_textures")] + specular_color_texture: Option>, +} + +impl SpecularExtension { + fn parse( + _load_context: &mut LoadContext, + _document: &Document, + material: &Material, + ) -> Option { + let extension = material + .extensions()? + .get("KHR_materials_specular")? + .as_object()?; + + #[cfg(feature = "pbr_specular_textures")] + let (_specular_channel, _specular_texture) = parse_material_extension_texture( + _load_context, + _document, + material, + extension, + "specularTexture", + "specular", + ); + + #[cfg(feature = "pbr_specular_textures")] + let (_specular_color_channel, _specular_color_texture) = parse_material_extension_texture( + _load_context, + _document, + material, + extension, + "specularColorTexture", + "specular color", + ); + + Some(SpecularExtension { + specular_factor: extension.get("specularFactor").and_then(Value::as_f64), + #[cfg(feature = "pbr_specular_textures")] + specular_channel: _specular_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: _specular_texture, + specular_color_factor: extension + .get("specularColorFactor") + .and_then(Value::as_array) + .and_then(|json_array| { + if json_array.len() < 3 { + None + } else { + Some([ + json_array[0].as_f64()?, + json_array[1].as_f64()?, + json_array[2].as_f64()?, + ]) + } + }), + #[cfg(feature = "pbr_specular_textures")] + specular_color_channel: _specular_color_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_color_texture: _specular_color_texture, + }) + } +} + +/// Parses a texture that's part of a material extension block and returns its +/// UV channel and image reference. +#[cfg(any( + feature = "pbr_specular_textures", + feature = "pbr_multi_layer_material_textures" +))] +fn parse_material_extension_texture( + load_context: &mut LoadContext, + document: &Document, + material: &Material, + extension: &Map, + texture_name: &str, + texture_kind: &str, +) -> (UvChannel, Option>) { + match extension + .get(texture_name) + .and_then(|value| value::from_value::(value.clone()).ok()) + { + Some(json_info) => ( + get_uv_channel(material, texture_kind, json_info.tex_coord), + Some(texture_handle_from_info(load_context, document, &json_info)), + ), + None => (UvChannel::default(), None), + } +} + /// 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. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index ee8214ab3d..3bdbb019c7 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -136,6 +136,12 @@ pbr_anisotropy_texture = [ # Percentage-closer soft shadows experimental_pbr_pcss = ["bevy_pbr?/experimental_pbr_pcss"] +# Specular textures in `StandardMaterial`: +pbr_specular_textures = [ + "bevy_pbr?/pbr_specular_textures", + "bevy_gltf?/pbr_specular_textures", +] + # Optimise for WebGL2 webgl = [ "bevy_core_pipeline?/webgl", diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index cb8a22fb8b..bf568885e7 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -15,6 +15,7 @@ pbr_transmission_textures = [] pbr_multi_layer_material_textures = [] pbr_anisotropy_texture = [] experimental_pbr_pcss = [] +pbr_specular_textures = [] shader_format_glsl = ["bevy_render/shader_format_glsl"] trace = ["bevy_render/trace"] ios_simulator = ["bevy_render/ios_simulator"] diff --git a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl index e96de6bded..e6254b1154 100644 --- a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl @@ -23,21 +23,24 @@ // Creates the deferred gbuffer from a PbrInput. fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4 { - // Only monochrome occlusion supported. May not be worth including at all. - // Some models have baked occlusion, GLTF only supports monochrome. - // Real time occlusion is applied in the deferred lighting pass. - // Deriving luminance via Rec. 709. coefficients - // https://en.wikipedia.org/wiki/Rec._709 - let diffuse_occlusion = dot(in.diffuse_occlusion, vec3(0.2126, 0.7152, 0.0722)); + // Only monochrome occlusion supported. May not be worth including at all. + // Some models have baked occlusion, GLTF only supports monochrome. + // Real time occlusion is applied in the deferred lighting pass. + // Deriving luminance via Rec. 709. coefficients + // https://en.wikipedia.org/wiki/Rec._709 + let rec_709_coeffs = vec3(0.2126, 0.7152, 0.0722); + let diffuse_occlusion = dot(in.diffuse_occlusion, rec_709_coeffs); + // Only monochrome specular supported. + let reflectance = dot(in.material.reflectance, rec_709_coeffs); #ifdef WEBGL2 // More crunched for webgl so we can also fit depth. var props = deferred_types::pack_unorm3x4_plus_unorm_20_(vec4( - in.material.reflectance, + reflectance, in.material.metallic, diffuse_occlusion, in.frag_coord.z)); #else var props = deferred_types::pack_unorm4x8_(vec4( - in.material.reflectance, // could be fewer bits + reflectance, // could be fewer bits in.material.metallic, // could be fewer bits diffuse_occlusion, // is this worth including? 0.0)); // spare @@ -100,10 +103,10 @@ fn pbr_input_from_deferred_gbuffer(frag_coord: vec4, gbuffer: vec4) -> #ifdef WEBGL2 // More crunched for webgl so we can also fit depth. let props = deferred_types::unpack_unorm3x4_plus_unorm_20_(gbuffer.b); // Bias to 0.5 since that's the value for almost all materials. - pbr.material.reflectance = saturate(props.r - 0.03333333333); + pbr.material.reflectance = vec3(saturate(props.r - 0.03333333333)); #else let props = deferred_types::unpack_unorm4x8_(gbuffer.b); - pbr.material.reflectance = props.r; + pbr.material.reflectance = vec3(props.r); #endif // WEBGL2 pbr.material.metallic = props.g; pbr.diffuse_occlusion = vec3(props.b); diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 150f58acf9..48353eb67c 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -183,6 +183,19 @@ pub struct StandardMaterial { #[doc(alias = "specular_intensity")] pub reflectance: f32, + /// A color with which to modulate the [`StandardMaterial::reflectance`] for + /// non-metals. + /// + /// The specular highlights and reflection are tinted with this color. Note + /// that it has no effect for non-metals. + /// + /// This feature is currently unsupported in the deferred rendering path, in + /// order to reduce the size of the geometry buffers. + /// + /// Defaults to [`Color::WHITE`]. + #[doc(alias = "specular_color")] + pub specular_tint: Color, + /// The amount of light transmitted _diffusely_ through the material (i.e. “translucency”). /// /// Implemented as a second, flipped [Lambertian diffuse](https://en.wikipedia.org/wiki/Lambertian_reflectance) lobe, @@ -401,6 +414,54 @@ pub struct StandardMaterial { #[dependency] pub occlusion_texture: Option>, + /// The UV channel to use for the [`StandardMaterial::specular_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_specular_textures")] + pub specular_channel: UvChannel, + + /// A map that specifies reflectance for non-metallic materials. + /// + /// Alpha values from [0.0, 1.0] in this texture are linearly mapped to + /// reflectance values of [0.0, 0.5] and multiplied by the constant + /// [`StandardMaterial::reflectance`] value. This follows the + /// `KHR_materials_specular` specification. The map will have no effect if + /// the material is fully metallic. + /// + /// When using this map, you may wish to set the + /// [`StandardMaterial::reflectance`] value to 2.0 so that this map can + /// express the full [0.0, 1.0] range of values. + /// + /// Note that, because the reflectance is stored in the alpha channel, and + /// the [`StandardMaterial::specular_tint_texture`] has no alpha value, it + /// may be desirable to pack the values together and supply the same + /// texture to both fields. + #[texture(27)] + #[sampler(28)] + #[cfg(feature = "pbr_specular_textures")] + pub specular_texture: Option>, + + /// The UV channel to use for the + /// [`StandardMaterial::specular_tint_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_specular_textures")] + pub specular_tint_channel: UvChannel, + + /// A map that specifies color adjustment to be applied to the specular + /// reflection for non-metallic materials. + /// + /// The RGB values of this texture modulate the + /// [`StandardMaterial::specular_tint`] value. See the documentation for + /// that field for more information. + /// + /// Like the fixed specular tint value, this texture map isn't supported in + /// the deferred renderer. + #[cfg(feature = "pbr_specular_textures")] + #[texture(29)] + #[sampler(30)] + pub specular_tint_texture: Option>, + /// An extra thin translucent layer on top of the main PBR layer. This is /// typically used for painted surfaces. /// @@ -801,6 +862,15 @@ impl Default for StandardMaterial { occlusion_texture: None, normal_map_channel: UvChannel::Uv0, normal_map_texture: None, + #[cfg(feature = "pbr_specular_textures")] + specular_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: None, + specular_tint: Color::WHITE, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_texture: None, clearcoat: 0.0, clearcoat_perceptual_roughness: 0.5, #[cfg(feature = "pbr_multi_layer_material_textures")] @@ -887,6 +957,8 @@ bitflags::bitflags! { const CLEARCOAT_ROUGHNESS_TEXTURE = 1 << 15; const CLEARCOAT_NORMAL_TEXTURE = 1 << 16; const ANISOTROPY_TEXTURE = 1 << 17; + const SPECULAR_TEXTURE = 1 << 18; + const SPECULAR_TINT_TEXTURE = 1 << 19; 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. @@ -918,14 +990,14 @@ pub struct StandardMaterialUniform { pub attenuation_color: Vec4, /// The transform applied to the UVs corresponding to `ATTRIBUTE_UV_0` on the mesh before sampling. Default is identity. pub uv_transform: Mat3, + /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] + /// defaults to 0.5 which is mapped to 4% reflectance in the shader + pub reflectance: Vec3, /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader /// Defaults to minimum of 0.089 pub roughness: f32, /// From [0.0, 1.0], dielectric to pure metallic pub metallic: f32, - /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] - /// defaults to 0.5 which is mapped to 4% reflectance in the shader - pub reflectance: f32, /// Amount of diffuse light transmitted through the material pub diffuse_transmission: f32, /// Amount of specular light transmitted through the material @@ -1011,6 +1083,16 @@ impl AsBindGroupShaderType for StandardMaterial { } } + #[cfg(feature = "pbr_specular_textures")] + { + if self.specular_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TEXTURE; + } + if self.specular_tint_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TINT_TEXTURE; + } + } + #[cfg(feature = "pbr_multi_layer_material_textures")] { if self.clearcoat_texture.is_some() { @@ -1075,7 +1157,7 @@ impl AsBindGroupShaderType for StandardMaterial { emissive, roughness: self.perceptual_roughness, metallic: self.metallic, - reflectance: self.reflectance, + reflectance: LinearRgba::from(self.specular_tint).to_vec3() * self.reflectance, clearcoat: self.clearcoat, clearcoat_perceptual_roughness: self.clearcoat_perceptual_roughness, anisotropy_strength: self.anisotropy_strength, @@ -1125,6 +1207,8 @@ bitflags! { const CLEARCOAT_UV = 0x040000; const CLEARCOAT_ROUGHNESS_UV = 0x080000; const CLEARCOAT_NORMAL_UV = 0x100000; + const SPECULAR_UV = 0x200000; + const SPECULAR_TINT_UV = 0x400000; const DEPTH_BIAS = 0xffffffff_00000000; } } @@ -1221,6 +1305,18 @@ impl From<&StandardMaterial> for StandardMaterialKey { ); } + #[cfg(feature = "pbr_specular_textures")] + { + key.set( + StandardMaterialKey::SPECULAR_UV, + material.specular_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::SPECULAR_TINT_UV, + material.specular_tint_channel != UvChannel::Uv0, + ); + } + #[cfg(feature = "pbr_multi_layer_material_textures")] { key.set( @@ -1392,7 +1488,15 @@ impl Material for StandardMaterial { ), ( StandardMaterialKey::ANISOTROPY_UV, - "STANDARD_MATERIAL_ANISOTROPY_UV", + "STANDARD_MATERIAL_ANISOTROPY_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_UV, + "STANDARD_MATERIAL_SPECULAR_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_TINT_UV, + "STANDARD_MATERIAL_SPECULAR_TINT_UV_B", ), ] { if key.bind_group_data.intersects(flags) { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index fcd41b3cb6..96b783ea66 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -2054,6 +2054,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if cfg!(feature = "pbr_anisotropy_texture") { shader_defs.push("PBR_ANISOTROPY_TEXTURE_SUPPORTED".into()); } + if cfg!(feature = "pbr_specular_textures") { + shader_defs.push("PBR_SPECULAR_TEXTURES_SUPPORTED".into()); + } let mut bind_group_layout = vec![self.get_view_layout(key.into()).clone()]; diff --git a/crates/bevy_pbr/src/render/pbr_bindings.wgsl b/crates/bevy_pbr/src/render/pbr_bindings.wgsl index 9b9d9dcc92..d6514acfa9 100644 --- a/crates/bevy_pbr/src/render/pbr_bindings.wgsl +++ b/crates/bevy_pbr/src/render/pbr_bindings.wgsl @@ -77,3 +77,17 @@ @group(2) @binding(26) var clearcoat_normal_sampler: sampler; #endif // BINDLESS #endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + +#ifdef PBR_SPECULAR_TEXTURES_SUPPORTED +#ifdef BINDLESS +@group(2) @binding(27) var specular_texture: binding_array, 16>; +@group(2) @binding(28) var specular_sampler: binding_array; +@group(2) @binding(29) var specular_tint_texture: binding_array, 16>; +@group(2) @binding(30) var specular_tint_sampler: binding_array; +#else +@group(2) @binding(27) var specular_texture: texture_2d; +@group(2) @binding(28) var specular_sampler: sampler; +@group(2) @binding(29) var specular_tint_texture: texture_2d; +@group(2) @binding(30) var specular_tint_sampler: sampler; +#endif // BINDLESS +#endif // PBR_SPECULAR_TEXTURES_SUPPORTED diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index a8a02b3f71..ac68e9f0aa 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -233,19 +233,92 @@ fn pbr_input_from_standard_material( // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { #ifdef BINDLESS - pbr_input.material.reflectance = pbr_bindings::material[slot].reflectance; pbr_input.material.ior = pbr_bindings::material[slot].ior; pbr_input.material.attenuation_color = pbr_bindings::material[slot].attenuation_color; pbr_input.material.attenuation_distance = pbr_bindings::material[slot].attenuation_distance; pbr_input.material.alpha_cutoff = pbr_bindings::material[slot].alpha_cutoff; #else // BINDLESS - pbr_input.material.reflectance = pbr_bindings::material.reflectance; pbr_input.material.ior = pbr_bindings::material.ior; pbr_input.material.attenuation_color = pbr_bindings::material.attenuation_color; pbr_input.material.attenuation_distance = pbr_bindings::material.attenuation_distance; pbr_input.material.alpha_cutoff = pbr_bindings::material.alpha_cutoff; #endif // BINDLESS + // reflectance +#ifdef BINDLESS + pbr_input.material.reflectance = pbr_bindings::material[slot].reflectance; +#else // BINDLESS + pbr_input.material.reflectance = pbr_bindings::material.reflectance; +#endif // BINDLESS + +#ifdef PBR_SPECULAR_TEXTURES_SUPPORTED +#ifdef VERTEX_UVS + + // Specular texture + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TEXTURE_BIT) != 0u) { + let specular = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + pbr_bindings::specular_texture[slot], + pbr_bindings::specular_sampler[slot], +#else // BINDLESS + pbr_bindings::specular_texture, + pbr_bindings::specular_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_SPECULAR_UV_B + uv_b, +#else // STANDARD_MATERIAL_SPECULAR_UV_B + uv, +#endif // STANDARD_MATERIAL_SPECULAR_UV_B +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).a; + // This 0.5 factor is from the `KHR_materials_specular` specification: + // + pbr_input.material.reflectance *= specular * 0.5; + } + + // Specular tint texture + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT) != 0u) { + let specular_tint = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + pbr_bindings::specular_tint_texture[slot], + pbr_bindings::specular_tint_sampler[slot], +#else // BINDLESS + pbr_bindings::specular_tint_texture, + pbr_bindings::specular_tint_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_SPECULAR_TINT_UV_B + uv_b, +#else // STANDARD_MATERIAL_SPECULAR_TINT_UV_B + uv, +#endif // STANDARD_MATERIAL_SPECULAR_TINT_UV_B +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + pbr_input.material.reflectance *= specular_tint; + } + +#endif // VERTEX_UVS +#endif // PBR_SPECULAR_TEXTURES_SUPPORTED + // emissive #ifdef BINDLESS var emissive: vec4 = pbr_bindings::material[slot].emissive; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 04b52f8780..44890b3a65 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -273,7 +273,7 @@ fn calculate_diffuse_color( // Remapping [0,1] reflectance to F0 // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping -fn calculate_F0(base_color: vec3, metallic: f32, reflectance: f32) -> vec3 { +fn calculate_F0(base_color: vec3, metallic: f32, reflectance: vec3) -> vec3 { return 0.16 * reflectance * reflectance * (1.0 - metallic) + base_color * metallic; } diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index d9b600c40b..29d479c4e3 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -7,9 +7,9 @@ struct StandardMaterial { emissive: vec4, attenuation_color: vec4, uv_transform: mat3x3, + reflectance: vec3, perceptual_roughness: f32, metallic: f32, - reflectance: f32, diffuse_transmission: f32, specular_transmission: f32, thickness: f32, @@ -52,6 +52,8 @@ 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_SPECULAR_TEXTURE_BIT: u32 = 262144u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT: u32 = 524288u; 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) @@ -73,7 +75,7 @@ fn standard_material_new() -> StandardMaterial { material.emissive = vec4(0.0, 0.0, 0.0, 1.0); material.perceptual_roughness = 0.5; material.metallic = 0.00; - material.reflectance = 0.5; + material.reflectance = vec3(0.5); material.diffuse_transmission = 0.0; material.specular_transmission = 0.0; material.thickness = 0.0; diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 04a925ad68..5fcca5a600 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -87,6 +87,7 @@ The default feature set enables most of the expected features of a game engine, |mp3|MP3 audio format support| |pbr_anisotropy_texture|Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_multi_layer_material_textures|Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| +|pbr_specular_textures|Enable support for specular textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_transmission_textures|Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pnm|PNM image format support, includes pam, pbm, pgm and ppm| |qoi|QOI image format support| diff --git a/examples/3d/specular_tint.rs b/examples/3d/specular_tint.rs new file mode 100644 index 0000000000..5dc362b9c1 --- /dev/null +++ b/examples/3d/specular_tint.rs @@ -0,0 +1,227 @@ +//! Demonstrates specular tints and maps. + +use std::f32::consts::PI; + +use bevy::{color::palettes::css::WHITE, core_pipeline::Skybox, prelude::*}; + +/// The camera rotation speed in radians per frame. +const ROTATION_SPEED: f32 = 0.005; +/// The rate at which the specular tint hue changes in degrees per frame. +const HUE_SHIFT_SPEED: f32 = 0.2; + +static SWITCH_TO_MAP_HELP_TEXT: &str = "Press Space to switch to a specular map"; +static SWITCH_TO_SOLID_TINT_HELP_TEXT: &str = "Press Space to switch to a solid specular tint"; + +/// The current settings the user has chosen. +#[derive(Resource, Default)] +struct AppStatus { + /// The type of tint (solid or texture map). + tint_type: TintType, + /// The hue of the solid tint in radians. + hue: f32, +} + +/// Assets needed by the demo. +#[derive(Resource)] +struct AppAssets { + /// A color tileable 3D noise texture. + noise_texture: Handle, +} + +impl FromWorld for AppAssets { + fn from_world(world: &mut World) -> Self { + let asset_server = world.resource::(); + Self { + noise_texture: asset_server.load("textures/AlphaNoise.png"), + } + } +} + +/// The type of specular tint that the user has selected. +#[derive(Clone, Copy, PartialEq, Default)] +enum TintType { + /// A solid color. + #[default] + Solid, + /// A Perlin noise texture. + Map, +} + +/// The entry point. +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Specular Tint Example".into(), + ..default() + }), + ..default() + })) + .init_resource::() + .init_resource::() + .insert_resource(AmbientLight { + color: Color::BLACK, + brightness: 0.0, + ..default() + }) + .add_systems(Startup, setup) + .add_systems(Update, rotate_camera) + .add_systems(Update, (toggle_specular_map, update_text).chain()) + .add_systems(Update, shift_hue.after(toggle_specular_map)) + .run(); +} + +/// Creates the scene. +fn setup( + mut commands: Commands, + asset_server: Res, + app_status: Res, + mut meshes: ResMut>, + mut standard_materials: ResMut>, +) { + // Spawns a camera. + commands.spawn(( + Transform::from_xyz(-2.0, 0.0, 3.5).looking_at(Vec3::ZERO, Vec3::Y), + Camera { + hdr: true, + ..default() + }, + Camera3d::default(), + Skybox { + image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + brightness: 3000.0, + ..default() + }, + 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"), + // We want relatively high intensity here in order for the specular + // tint to show up well. + intensity: 25000.0, + ..default() + }, + )); + + // Spawn the sphere. + commands.spawn(( + Transform::from_rotation(Quat::from_rotation_x(PI * 0.5)), + Mesh3d(meshes.add(Sphere::default().mesh().uv(32, 18))), + MeshMaterial3d(standard_materials.add(StandardMaterial { + // We want only reflected specular light here, so we set the base + // color as black. + base_color: Color::BLACK, + reflectance: 1.0, + specular_tint: Color::hsva(app_status.hue, 1.0, 1.0, 1.0), + // The object must not be metallic, or else the reflectance is + // ignored per the Filament spec: + // + // + metallic: 0.0, + perceptual_roughness: 0.0, + ..default() + })), + )); + + // Spawn the help text. + commands.spawn(( + Node { + position_type: PositionType::Absolute, + bottom: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }, + app_status.create_text(), + )); +} + +/// Rotates the camera a bit every frame. +fn rotate_camera(mut cameras: Query<&mut Transform, With>) { + for mut camera_transform in cameras.iter_mut() { + camera_transform.translation = + Quat::from_rotation_y(ROTATION_SPEED) * camera_transform.translation; + camera_transform.look_at(Vec3::ZERO, Vec3::Y); + } +} + +/// Alters the hue of the solid color a bit every frame. +fn shift_hue( + mut app_status: ResMut, + objects_with_materials: Query<&MeshMaterial3d>, + mut standard_materials: ResMut>, +) { + if app_status.tint_type != TintType::Solid { + return; + } + + app_status.hue += HUE_SHIFT_SPEED; + + for material_handle in objects_with_materials.iter() { + let Some(material) = standard_materials.get_mut(material_handle) else { + continue; + }; + material.specular_tint = Color::hsva(app_status.hue, 1.0, 1.0, 1.0); + } +} + +impl AppStatus { + /// Returns appropriate help text that reflects the current app status. + fn create_text(&self) -> Text { + let tint_map_help_text = match self.tint_type { + TintType::Solid => SWITCH_TO_MAP_HELP_TEXT, + TintType::Map => SWITCH_TO_SOLID_TINT_HELP_TEXT, + }; + + Text::new(tint_map_help_text) + } +} + +/// Changes the specular tint to a solid color or map when the user presses +/// Space. +fn toggle_specular_map( + keyboard: Res>, + mut app_status: ResMut, + app_assets: Res, + objects_with_materials: Query<&MeshMaterial3d>, + mut standard_materials: ResMut>, +) { + if !keyboard.just_pressed(KeyCode::Space) { + return; + } + + // Swap tint type. + app_status.tint_type = match app_status.tint_type { + TintType::Solid => TintType::Map, + TintType::Map => TintType::Solid, + }; + + for material_handle in objects_with_materials.iter() { + let Some(material) = standard_materials.get_mut(material_handle) else { + continue; + }; + + // Adjust the tint type. + match app_status.tint_type { + TintType::Solid => { + material.reflectance = 1.0; + material.specular_tint_texture = None; + } + TintType::Map => { + // Set reflectance to 2.0 to spread out the map's reflectance + // range from the default [0.0, 0.5] to [0.0, 1.0]. + material.reflectance = 2.0; + // As the tint map is multiplied by the tint color, we set the + // latter to white so that only the map has an effect. + material.specular_tint = WHITE.into(); + material.specular_tint_texture = Some(app_assets.noise_texture.clone()); + } + }; + } +} + +/// Updates the help text at the bottom of the screen to reflect the current app +/// status. +fn update_text(mut text_query: Query<&mut Text>, app_status: Res) { + for mut text in text_query.iter_mut() { + *text = app_status.create_text(); + } +} diff --git a/examples/README.md b/examples/README.md index a63e4aedb1..8963590365 100644 --- a/examples/README.md +++ b/examples/README.md @@ -180,6 +180,7 @@ Example | Description [Shadow Biases](../examples/3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene [Shadow Caster and Receiver](../examples/3d/shadow_caster_receiver.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene [Skybox](../examples/3d/skybox.rs) | Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats. +[Specular Tint](../examples/3d/specular_tint.rs) | Demonstrates specular tints and maps [Spherical Area Lights](../examples/3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior [Split Screen](../examples/3d/split_screen.rs) | Demonstrates how to render two cameras to the same window to accomplish "split screen" [Spotlight](../examples/3d/spotlight.rs) | Illustrates spot lights