diff --git a/Cargo.toml b/Cargo.toml index ed744eac37..593a73af98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -302,6 +302,11 @@ shader_format_spirv = ["bevy_internal/shader_format_spirv"] # 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 pbr_transmission_textures = ["bevy_internal/pbr_transmission_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_multi_layer_material_textures = [ + "bevy_internal/pbr_multi_layer_material_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"] @@ -2994,6 +2999,18 @@ description = "Demonstrates color grading" category = "3D Rendering" wasm = true +[[example]] +name = "clearcoat" +path = "examples/3d/clearcoat.rs" +doc-scrape-examples = true +required-features = ["pbr_multi_layer_material_textures"] + +[package.metadata.example.clearcoat] +name = "Clearcoat" +description = "Demonstrates the clearcoat PBR feature" +category = "3D Rendering" +wasm = false + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/models/GolfBall/GolfBall.glb b/assets/models/GolfBall/GolfBall.glb new file mode 100644 index 0000000000..bd2f5e8d98 Binary files /dev/null and b/assets/models/GolfBall/GolfBall.glb differ diff --git a/assets/shaders/array_texture.wgsl b/assets/shaders/array_texture.wgsl index 7c0216f73e..3fa77933b2 100644 --- a/assets/shaders/array_texture.wgsl +++ b/assets/shaders/array_texture.wgsl @@ -3,6 +3,7 @@ mesh_view_bindings::view, pbr_types::{STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT, PbrInput, pbr_input_new}, pbr_functions as fns, + pbr_bindings, } #import bevy_core_pipeline::tonemapping::tone_mapping @@ -37,19 +38,21 @@ fn fragment( pbr_input.is_orthographic = view.projection[3].w == 1.0; + pbr_input.N = normalize(pbr_input.world_normal); + +#ifdef VERTEX_TANGENTS + let Nt = textureSampleBias(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, mesh.uv, view.mip_bias).rgb; pbr_input.N = fns::apply_normal_mapping( pbr_input.material.flags, mesh.world_normal, double_sided, is_front, -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP mesh.world_tangent, -#endif -#endif - mesh.uv, + Nt, view.mip_bias, ); +#endif + pbr_input.V = fns::calculate_view(mesh.world_position, pbr_input.is_orthographic); return tone_mapping(fns::apply_pbr_lighting(pbr_input), view.color_grading); diff --git a/assets/textures/BlueNoise-Normal.png b/assets/textures/BlueNoise-Normal.png new file mode 100644 index 0000000000..b6e5a996f8 Binary files /dev/null and b/assets/textures/BlueNoise-Normal.png differ diff --git a/assets/textures/ScratchedGold-Normal.png b/assets/textures/ScratchedGold-Normal.png new file mode 100644 index 0000000000..9bb80c0f08 Binary files /dev/null and b/assets/textures/ScratchedGold-Normal.png differ diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 66bb01abe7..b74079b386 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["bevy"] [features] dds = ["bevy_render/dds"] pbr_transmission_textures = ["bevy_pbr/pbr_transmission_textures"] +pbr_multi_layer_material_textures = [] [dependencies] # bevy diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 171c09882f..b34727da16 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -38,13 +38,18 @@ use bevy_tasks::IoTaskPool; use bevy_transform::components::Transform; use bevy_utils::tracing::{error, info_span, warn}; use bevy_utils::{HashMap, HashSet}; +use gltf::image::Source; use gltf::{ accessor::Iter, mesh::{util::ReadIndices, Mode}, texture::{Info, MagFilter, MinFilter, TextureTransform, WrappingMode}, Material, Node, Primitive, Semantic, }; +use gltf::{json, Document}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "pbr_multi_layer_material_textures")] +use serde_json::value; +use serde_json::{Map, Value}; #[cfg(feature = "bevy_animation")] use smallvec::SmallVec; use std::io::Error; @@ -214,6 +219,22 @@ async fn load_gltf<'a, 'b, 'c>( { linear_textures.insert(texture.texture().index()); } + + // None of the clearcoat maps should be loaded as sRGB. + #[cfg(feature = "pbr_multi_layer_material_textures")] + for texture_field_name in [ + "clearcoatTexture", + "clearcoatRoughnessTexture", + "clearcoatNormalTexture", + ] { + if let Some(texture_index) = material_extension_texture_index( + &material, + "KHR_materials_clearcoat", + texture_field_name, + ) { + linear_textures.insert(texture_index); + } + } } #[cfg(feature = "bevy_animation")] @@ -390,7 +411,7 @@ async fn load_gltf<'a, 'b, 'c>( if !settings.load_materials.is_empty() { // NOTE: materials must be loaded after textures because image load() calls will happen before load_with_settings, preventing is_srgb from being set properly for material in gltf.materials() { - let handle = load_material(&material, load_context, false); + let handle = load_material(&material, load_context, &gltf.document, false); if let Some(name) = material.name() { named_materials.insert(name.into(), handle.clone()); } @@ -490,7 +511,7 @@ async fn load_gltf<'a, 'b, 'c>( { mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute); } else if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some() - && primitive.material().normal_texture().is_some() + && material_needs_tangents(&primitive.material()) { bevy_utils::tracing::debug!( "Missing vertex tangents for {}, computing them using the mikktspace algorithm. Consider using a tool such as Blender to pre-compute the tangents.", file_name @@ -609,6 +630,7 @@ async fn load_gltf<'a, 'b, 'c>( &animation_roots, #[cfg(feature = "bevy_animation")] None, + &gltf.document, ); if result.is_err() { err = Some(result); @@ -815,6 +837,7 @@ async fn load_image<'a, 'b>( fn load_material( material: &Material, load_context: &mut LoadContext, + document: &Document, is_scale_inverted: bool, ) -> Handle { let material_label = material_label(material, is_scale_inverted); @@ -918,6 +941,10 @@ fn load_material( let ior = material.ior().unwrap_or(1.5); + // Parse the `KHR_materials_clearcoat` extension data if necessary. + let clearcoat = ClearcoatExtension::parse(load_context, document, material.extensions()) + .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 scaled_emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); @@ -957,6 +984,15 @@ fn load_material( unlit: material.unlit(), alpha_mode: alpha_mode(material), uv_transform, + clearcoat: clearcoat.clearcoat_factor.unwrap_or_default() as f32, + clearcoat_perceptual_roughness: clearcoat.clearcoat_roughness_factor.unwrap_or_default() + as f32, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: clearcoat.clearcoat_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: clearcoat.clearcoat_roughness_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, ..Default::default() } }) @@ -1015,6 +1051,7 @@ fn load_node( parent_transform: &Transform, #[cfg(feature = "bevy_animation")] animation_roots: &HashSet, #[cfg(feature = "bevy_animation")] mut animation_context: Option, + document: &Document, ) -> Result<(), GltfError> { let mut gltf_error = None; let transform = node_transform(gltf_node); @@ -1122,7 +1159,7 @@ fn load_node( if !root_load_context.has_labeled_asset(&material_label) && !load_context.has_labeled_asset(&material_label) { - load_material(&material, load_context, is_scale_inverted); + load_material(&material, load_context, document, is_scale_inverted); } let primitive_label = primitive_label(&mesh, &primitive); @@ -1267,6 +1304,7 @@ fn load_node( animation_roots, #[cfg(feature = "bevy_animation")] animation_context.clone(), + document, ) { gltf_error = Some(err); return; @@ -1337,11 +1375,11 @@ fn texture_label(texture: &gltf::Texture) -> String { fn texture_handle(load_context: &mut LoadContext, texture: &gltf::Texture) -> Handle { match texture.source().source() { - gltf::image::Source::View { .. } => { + Source::View { .. } => { let label = texture_label(texture); load_context.get_label_handle(&label) } - gltf::image::Source::Uri { uri, .. } => { + Source::Uri { uri, .. } => { let uri = percent_encoding::percent_decode_str(uri) .decode_utf8() .unwrap(); @@ -1358,6 +1396,24 @@ fn texture_handle(load_context: &mut LoadContext, texture: &gltf::Texture) -> Ha } } +/// Given a [`json::texture::Info`], returns the handle of the texture that this +/// refers to. +/// +/// This is a low-level function only used when the `gltf` crate has no support +/// for an extension, forcing us to parse its texture references manually. +#[allow(dead_code)] +fn texture_handle_from_info( + load_context: &mut LoadContext, + document: &Document, + texture_info: &json::texture::Info, +) -> Handle { + let texture = document + .textures() + .nth(texture_info.index.value()) + .expect("Texture info references a nonexistent texture"); + texture_handle(load_context, &texture) +} + /// Returns the label for the `node`. fn node_label(node: &Node) -> String { format!("Node{}", node.index()) @@ -1636,6 +1692,104 @@ struct AnimationContext { path: SmallVec<[Name; 8]>, } +/// Parsed data from the `KHR_materials_clearcoat` extension. +/// +/// See the specification: +/// +#[derive(Default)] +struct ClearcoatExtension { + clearcoat_factor: Option, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: Option>, + clearcoat_roughness_factor: Option, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: Option>, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: Option>, +} + +impl ClearcoatExtension { + #[allow(unused_variables)] + fn parse( + load_context: &mut LoadContext, + document: &Document, + material_extensions: Option<&Map>, + ) -> Option { + let extension = material_extensions? + .get("KHR_materials_clearcoat")? + .as_object()?; + + Some(ClearcoatExtension { + clearcoat_factor: extension.get("clearcoatFactor").and_then(Value::as_f64), + clearcoat_roughness_factor: extension + .get("clearcoatRoughnessFactor") + .and_then(Value::as_f64), + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: extension + .get("clearcoatTexture") + .and_then(|value| value::from_value::(value.clone()).ok()) + .map(|json_info| texture_handle_from_info(load_context, document, &json_info)), + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: extension + .get("clearcoatRoughnessTexture") + .and_then(|value| value::from_value::(value.clone()).ok()) + .map(|json_info| texture_handle_from_info(load_context, document, &json_info)), + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: extension + .get("clearcoatNormalTexture") + .and_then(|value| value::from_value::(value.clone()).ok()) + .map(|json_info| texture_handle_from_info(load_context, document, &json_info)), + }) + } +} + +/// 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, + texture_field_name: &str, +) -> Option { + Some( + value::from_value::( + material + .extensions()? + .get(extension_name)? + .as_object()? + .get(texture_field_name)? + .clone(), + ) + .ok()? + .index + .value(), + ) +} + +/// Returns true if the material needs mesh tangents in order to be successfully +/// rendered. +/// +/// We generate them if this function returns true. +fn material_needs_tangents(material: &Material) -> bool { + if material.normal_texture().is_some() { + return true; + } + + #[cfg(feature = "pbr_multi_layer_material_textures")] + if material_extension_texture_index( + material, + "KHR_materials_clearcoat", + "clearcoatNormalTexture", + ) + .is_some() + { + return true; + } + + false +} + #[cfg(test)] mod test { use std::path::PathBuf; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index c5d9c2b0e1..36b63ff193 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -98,6 +98,12 @@ pbr_transmission_textures = [ "bevy_gltf?/pbr_transmission_textures", ] +# Multi-layer material textures in `StandardMaterial`: +pbr_multi_layer_material_textures = [ + "bevy_pbr?/pbr_multi_layer_material_textures", + "bevy_gltf?/pbr_multi_layer_material_textures", +] + # Optimise for WebGL2 webgl = [ "bevy_core_pipeline?/webgl", diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index adcc4beea3..3af9f64348 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["bevy"] webgl = [] webgpu = [] pbr_transmission_textures = [] +pbr_multi_layer_material_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/light_probe/environment_map.wgsl b/crates/bevy_pbr/src/light_probe/environment_map.wgsl index 2c8390f83e..7b9945a8a6 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.wgsl +++ b/crates/bevy_pbr/src/light_probe/environment_map.wgsl @@ -3,6 +3,9 @@ #import bevy_pbr::light_probe::query_light_probe #import bevy_pbr::mesh_view_bindings as bindings #import bevy_pbr::mesh_view_bindings::light_probes +#import bevy_pbr::lighting::{ + F_Schlick_vec, LayerLightingInput, LightingInput, LAYER_BASE, LAYER_CLEARCOAT +} struct EnvironmentMapLight { diffuse: vec3, @@ -21,12 +24,16 @@ struct EnvironmentMapRadiances { #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY fn compute_radiances( - perceptual_roughness: f32, - N: vec3, - R: vec3, + input: ptr, + layer: u32, world_position: vec3, found_diffuse_indirect: bool, ) -> EnvironmentMapRadiances { + // Unpack. + let perceptual_roughness = (*input).layers[layer].perceptual_roughness; + let N = (*input).layers[layer].N; + let R = (*input).layers[layer].R; + var radiances: EnvironmentMapRadiances; // Search for a reflection probe that contains the fragment. @@ -69,12 +76,16 @@ fn compute_radiances( #else // MULTIPLE_LIGHT_PROBES_IN_ARRAY fn compute_radiances( - perceptual_roughness: f32, - N: vec3, - R: vec3, + input: ptr, + layer: u32, world_position: vec3, found_diffuse_indirect: bool, ) -> EnvironmentMapRadiances { + // Unpack. + let perceptual_roughness = (*input).layers[layer].perceptual_roughness; + let N = (*input).layers[layer].N; + let R = (*input).layers[layer].R; + var radiances: EnvironmentMapRadiances; if (light_probes.view_cubemap_index < 0) { @@ -109,26 +120,53 @@ fn compute_radiances( #endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY +#ifdef STANDARD_MATERIAL_CLEARCOAT + +// Adds the environment map light from the clearcoat layer to that of the base +// layer. +fn environment_map_light_clearcoat( + out: ptr, + input: ptr, + found_diffuse_indirect: bool, +) { + // Unpack. + let world_position = (*input).P; + let clearcoat_NdotV = (*input).layers[LAYER_CLEARCOAT].NdotV; + let clearcoat_strength = (*input).clearcoat_strength; + + // Calculate the Fresnel term `Fc` for the clearcoat layer. + // 0.04 is a hardcoded value for F0 from the Filament spec. + let clearcoat_F0 = vec3(0.04); + let Fc = F_Schlick_vec(clearcoat_F0, 1.0, clearcoat_NdotV) * clearcoat_strength; + let inv_Fc = 1.0 - Fc; + + let clearcoat_radiances = compute_radiances( + input, LAYER_CLEARCOAT, world_position, found_diffuse_indirect); + + // Composite the clearcoat layer on top of the existing one. + // These formulas are from Filament: + // + (*out).diffuse *= inv_Fc; + (*out).specular = (*out).specular * inv_Fc * inv_Fc + clearcoat_radiances.radiance * Fc; +} + +#endif // STANDARD_MATERIAL_CLEARCOAT + fn environment_map_light( - perceptual_roughness: f32, - roughness: f32, - diffuse_color: vec3, - NdotV: f32, - f_ab: vec2, - N: vec3, - R: vec3, - F0: vec3, - world_position: vec3, + input: ptr, found_diffuse_indirect: bool, ) -> EnvironmentMapLight { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let diffuse_color = (*input).diffuse_color; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let F_ab = (*input).F_ab; + let F0 = (*input).F0_; + let world_position = (*input).P; + var out: EnvironmentMapLight; - let radiances = compute_radiances( - perceptual_roughness, - N, - R, - world_position, - found_diffuse_indirect); + let radiances = compute_radiances(input, LAYER_BASE, world_position, found_diffuse_indirect); if (all(radiances.irradiance == vec3(0.0)) && all(radiances.radiance == vec3(0.0))) { out.diffuse = vec3(0.0); out.specular = vec3(0.0); @@ -144,7 +182,7 @@ fn environment_map_light( // Useful reference: https://bruop.github.io/ibl let Fr = max(vec3(1.0 - roughness), F0) - F0; let kS = F0 + Fr * pow(1.0 - NdotV, 5.0); - let Ess = f_ab.x + f_ab.y; + let Ess = F_ab.x + F_ab.y; let FssEss = kS * Ess * specular_occlusion; let Ems = 1.0 - Ess; let Favg = F0 + (1.0 - F0) / 21.0; @@ -153,7 +191,6 @@ fn environment_map_light( let Edss = 1.0 - (FssEss + FmsEms); let kD = diffuse_color * Edss; - if (!found_diffuse_indirect) { out.diffuse = (FmsEms + kD) * radiances.irradiance; } else { @@ -161,5 +198,10 @@ fn environment_map_light( } out.specular = FssEss * radiances.radiance; + +#ifdef STANDARD_MATERIAL_CLEARCOAT + environment_map_light_clearcoat(&out, input, found_diffuse_indirect); +#endif // STANDARD_MATERIAL_CLEARCOAT + return out; } diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 59e6d63fff..a8369304ff 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -338,6 +338,60 @@ pub struct StandardMaterial { #[dependency] pub occlusion_texture: Option>, + /// An extra thin translucent layer on top of the main PBR layer. This is + /// typically used for painted surfaces. + /// + /// This value specifies the strength of the layer, which affects how + /// visible the clearcoat layer will be. + /// + /// Defaults to zero, specifying no clearcoat layer. + pub clearcoat: f32, + + /// An image texture that specifies the strength of the clearcoat layer in + /// the red channel. Values sampled from this texture are multiplied by the + /// main [`StandardMaterial::clearcoat`] factor. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[texture(19)] + #[sampler(20)] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_texture: Option>, + + /// The roughness of the clearcoat material. This is specified in exactly + /// the same way as the [`StandardMaterial::perceptual_roughness`]. + /// + /// If the [`StandardMaterial::clearcoat`] value if zero, this has no + /// effect. + /// + /// Defaults to 0.5. + pub clearcoat_perceptual_roughness: f32, + + /// An image texture that specifies the roughness of the clearcoat level in + /// the green channel. Values from this texture are multiplied by the main + /// [`StandardMaterial::clearcoat_perceptual_roughness`] factor. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[texture(21)] + #[sampler(22)] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_roughness_texture: Option>, + + /// An image texture that specifies a normal map that is to be applied to + /// the clearcoat layer. This can be used to simulate, for example, + /// scratches on an outer layer of varnish. Normal maps are in the same + /// format as [`StandardMaterial::normal_map_texture`]. + /// + /// Note that, if a clearcoat normal map isn't specified, the main normal + /// map, if any, won't be applied to the clearcoat. If you want a normal map + /// that applies to both the main materal and to the clearcoat, specify it + /// 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)] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_normal_texture: Option>, + /// Support two-sided lighting by automatically flipping the normals for "back" faces /// within the PBR lighting shader. /// @@ -579,6 +633,14 @@ impl Default for StandardMaterial { attenuation_distance: f32::INFINITY, occlusion_texture: None, normal_map_texture: None, + clearcoat: 0.0, + clearcoat_perceptual_roughness: 0.5, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: None, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: None, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: None, flip_normal_map_y: false, double_sided: false, cull_mode: Some(Face::Back), @@ -641,6 +703,9 @@ bitflags::bitflags! { const THICKNESS_TEXTURE = 1 << 11; const DIFFUSE_TRANSMISSION_TEXTURE = 1 << 12; const ATTENUATION_ENABLED = 1 << 13; + const CLEARCOAT_TEXTURE = 1 << 14; + const CLEARCOAT_ROUGHNESS_TEXTURE = 1 << 15; + const CLEARCOAT_NORMAL_TEXTURE = 1 << 16; 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. @@ -690,6 +755,8 @@ pub struct StandardMaterialUniform { pub ior: f32, /// How far light travels through the volume underneath the material surface before being absorbed pub attenuation_distance: f32, + pub clearcoat: f32, + pub clearcoat_perceptual_roughness: f32, /// 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, @@ -753,6 +820,20 @@ impl AsBindGroupShaderType for StandardMaterial { flags |= StandardMaterialFlags::DIFFUSE_TRANSMISSION_TEXTURE; } } + + #[cfg(feature = "pbr_multi_layer_material_textures")] + { + if self.clearcoat_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_TEXTURE; + } + if self.clearcoat_roughness_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_ROUGHNESS_TEXTURE; + } + if self.clearcoat_normal_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_NORMAL_TEXTURE; + } + } + let has_normal_map = self.normal_map_texture.is_some(); if has_normal_map { let normal_map_id = self.normal_map_texture.as_ref().map(|h| h.id()).unwrap(); @@ -799,6 +880,8 @@ impl AsBindGroupShaderType for StandardMaterial { roughness: self.perceptual_roughness, metallic: self.metallic, reflectance: self.reflectance, + clearcoat: self.clearcoat, + clearcoat_perceptual_roughness: self.clearcoat_perceptual_roughness, diffuse_transmission: self.diffuse_transmission, specular_transmission: self.specular_transmission, thickness: self.thickness, @@ -829,6 +912,8 @@ bitflags! { const RELIEF_MAPPING = 0x08; const DIFFUSE_TRANSMISSION = 0x10; const SPECULAR_TRANSMISSION = 0x20; + const CLEARCOAT = 0x40; + const CLEARCOAT_NORMAL_MAP = 0x80; const DEPTH_BIAS = 0xffffffff_00000000; } } @@ -865,6 +950,15 @@ impl From<&StandardMaterial> for StandardMaterialKey { StandardMaterialKey::SPECULAR_TRANSMISSION, material.specular_transmission > 0.0, ); + + key.set(StandardMaterialKey::CLEARCOAT, material.clearcoat > 0.0); + + #[cfg(feature = "pbr_multi_layer_material_textures")] + key.set( + StandardMaterialKey::CLEARCOAT_NORMAL_MAP, + material.clearcoat > 0.0 && material.clearcoat_normal_texture.is_some(), + ); + key.insert(StandardMaterialKey::from_bits_retain( (material.depth_bias as u64) << STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT, )); @@ -941,38 +1035,37 @@ impl Material for StandardMaterial { if let Some(fragment) = descriptor.fragment.as_mut() { let shader_defs = &mut fragment.shader_defs; - if key - .bind_group_data - .contains(StandardMaterialKey::NORMAL_MAP) - { - shader_defs.push("STANDARD_MATERIAL_NORMAL_MAP".into()); - } - if key - .bind_group_data - .contains(StandardMaterialKey::RELIEF_MAPPING) - { - shader_defs.push("RELIEF_MAPPING".into()); - } - - if key - .bind_group_data - .contains(StandardMaterialKey::DIFFUSE_TRANSMISSION) - { - shader_defs.push("STANDARD_MATERIAL_DIFFUSE_TRANSMISSION".into()); - } - - if key - .bind_group_data - .contains(StandardMaterialKey::SPECULAR_TRANSMISSION) - { - shader_defs.push("STANDARD_MATERIAL_SPECULAR_TRANSMISSION".into()); - } - - if key.bind_group_data.intersects( - StandardMaterialKey::DIFFUSE_TRANSMISSION - | StandardMaterialKey::SPECULAR_TRANSMISSION, - ) { - shader_defs.push("STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION".into()); + for (flags, shader_def) in [ + ( + StandardMaterialKey::NORMAL_MAP, + "STANDARD_MATERIAL_NORMAL_MAP", + ), + (StandardMaterialKey::RELIEF_MAPPING, "RELIEF_MAPPING"), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION, + "STANDARD_MATERIAL_DIFFUSE_TRANSMISSION", + ), + ( + StandardMaterialKey::SPECULAR_TRANSMISSION, + "STANDARD_MATERIAL_SPECULAR_TRANSMISSION", + ), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION + | StandardMaterialKey::SPECULAR_TRANSMISSION, + "STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION", + ), + ( + StandardMaterialKey::CLEARCOAT, + "STANDARD_MATERIAL_CLEARCOAT", + ), + ( + StandardMaterialKey::CLEARCOAT_NORMAL_MAP, + "STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP", + ), + ] { + if key.bind_group_data.intersects(flags) { + shader_defs.push(shader_def.into()); + } } } diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 199f6752cf..3c499be441 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1564,6 +1564,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if cfg!(feature = "pbr_transmission_textures") { shader_defs.push("PBR_TRANSMISSION_TEXTURES_SUPPORTED".into()); } + if cfg!(feature = "pbr_multi_layer_material_textures") { + shader_defs.push("PBR_MULTI_LAYER_MATERIAL_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 2f26e010b0..e51c22a7ea 100644 --- a/crates/bevy_pbr/src/render/pbr_bindings.wgsl +++ b/crates/bevy_pbr/src/render/pbr_bindings.wgsl @@ -23,3 +23,11 @@ @group(2) @binding(17) var diffuse_transmission_texture: texture_2d; @group(2) @binding(18) 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; +#endif diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index d64e78b47e..7a90079994 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -2,6 +2,7 @@ #import bevy_pbr::{ pbr_functions, + pbr_functions::SampleBias, pbr_bindings, pbr_types, prepass_utils, @@ -79,6 +80,15 @@ fn pbr_input_from_standard_material( // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); + // Fill in the sample bias so we can sample from textures. + var bias: SampleBias; +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv = in.ddx_uv; + bias.ddy_uv = in.ddy_uv; +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias = view.mip_bias; +#endif // MESHLET_MESH_MATERIAL_PASS + #ifdef VERTEX_UVS let uv_transform = pbr_bindings::material.uv_transform; var uv = (uv_transform * vec3(in.uv, 1.0)).xy; @@ -105,11 +115,12 @@ fn pbr_input_from_standard_material( #endif // VERTEX_TANGENTS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - pbr_input.material.base_color *= textureSampleGrad(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, in.ddx_uv, in.ddy_uv); -#else - pbr_input.material.base_color *= textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias); -#endif + pbr_input.material.base_color *= pbr_functions::sample_texture( + pbr_bindings::base_color_texture, + pbr_bindings::base_color_sampler, + uv, + bias, + ); #ifdef ALPHA_TO_COVERAGE // Sharpen alpha edges. @@ -142,11 +153,12 @@ fn pbr_input_from_standard_material( var emissive: vec4 = pbr_bindings::material.emissive; #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - emissive = vec4(emissive.rgb * textureSampleGrad(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, in.ddx_uv, in.ddy_uv).rgb, 1.0); -#else - emissive = vec4(emissive.rgb * textureSampleBias(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, view.mip_bias).rgb, 1.0); -#endif + emissive = vec4(pbr_functions::sample_texture( + pbr_bindings::emissive_texture, + pbr_bindings::emissive_sampler, + uv, + bias, + ).rgb, 1.0); } #endif pbr_input.material.emissive = emissive; @@ -157,11 +169,12 @@ fn pbr_input_from_standard_material( let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - let metallic_roughness = textureSampleGrad(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, in.ddx_uv, in.ddy_uv); -#else - let metallic_roughness = textureSampleBias(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, view.mip_bias); -#endif + let metallic_roughness = pbr_functions::sample_texture( + pbr_bindings::metallic_roughness_texture, + pbr_bindings::metallic_roughness_sampler, + uv, + bias, + ); // Sampling from GLTF standard channels for now metallic *= metallic_roughness.b; perceptual_roughness *= metallic_roughness.g; @@ -170,14 +183,45 @@ fn pbr_input_from_standard_material( pbr_input.material.metallic = metallic; pbr_input.material.perceptual_roughness = perceptual_roughness; + // Clearcoat factor + pbr_input.material.clearcoat = pbr_bindings::material.clearcoat; +#ifdef VERTEX_UVS +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT) != 0u) { + pbr_input.material.clearcoat *= pbr_functions::sample_texture( + pbr_bindings::clearcoat_texture, + pbr_bindings::clearcoat_sampler, + uv, + bias, + ).r; + } +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#endif // VERTEX_UVS + + // Clearcoat roughness + pbr_input.material.clearcoat_perceptual_roughness = pbr_bindings::material.clearcoat_perceptual_roughness; +#ifdef VERTEX_UVS +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT) != 0u) { + pbr_input.material.clearcoat_perceptual_roughness *= pbr_functions::sample_texture( + pbr_bindings::clearcoat_roughness_texture, + pbr_bindings::clearcoat_roughness_sampler, + uv, + bias, + ).g; + } +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#endif // VERTEX_UVS + var specular_transmission: f32 = pbr_bindings::material.specular_transmission; #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - specular_transmission *= textureSampleGrad(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv, in.ddx_uv, in.ddy_uv).r; -#else - specular_transmission *= textureSampleBias(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv, view.mip_bias).r; -#endif + specular_transmission *= pbr_functions::sample_texture( + pbr_bindings::specular_transmission_texture, + pbr_bindings::specular_transmission_sampler, + uv, + bias, + ).r; } #endif pbr_input.material.specular_transmission = specular_transmission; @@ -185,11 +229,12 @@ fn pbr_input_from_standard_material( var thickness: f32 = pbr_bindings::material.thickness; #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - thickness *= textureSampleGrad(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv, in.ddx_uv, in.ddy_uv).g; -#else - thickness *= textureSampleBias(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv, view.mip_bias).g; -#endif + thickness *= pbr_functions::sample_texture( + pbr_bindings::thickness_texture, + pbr_bindings::thickness_sampler, + uv, + bias, + ).g; } #endif // scale thickness, accounting for non-uniform scaling (e.g. a “squished” mesh) @@ -204,11 +249,12 @@ fn pbr_input_from_standard_material( var diffuse_transmission = pbr_bindings::material.diffuse_transmission; #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - diffuse_transmission *= textureSampleGrad(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv, in.ddx_uv, in.ddy_uv).a; -#else - diffuse_transmission *= textureSampleBias(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv, view.mip_bias).a; -#endif + diffuse_transmission *= pbr_functions::sample_texture( + pbr_bindings::diffuse_transmission_texture, + pbr_bindings::diffuse_transmission_sampler, + uv, + bias, + ).a; } #endif pbr_input.material.diffuse_transmission = diffuse_transmission; @@ -217,11 +263,12 @@ fn pbr_input_from_standard_material( var specular_occlusion: f32 = 1.0; #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - diffuse_occlusion = vec3(textureSampleGrad(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, in.ddx_uv, in.ddy_uv).r); -#else - diffuse_occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r); -#endif + diffuse_occlusion *= pbr_functions::sample_texture( + pbr_bindings::occlusion_texture, + pbr_bindings::occlusion_sampler, + uv, + bias, + ).r; } #endif #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION @@ -237,26 +284,67 @@ fn pbr_input_from_standard_material( // N (normal vector) #ifndef LOAD_PREPASS_NORMALS + + pbr_input.N = normalize(pbr_input.world_normal); + pbr_input.clearcoat_N = pbr_input.N; + +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS + +#ifdef STANDARD_MATERIAL_NORMAL_MAP + + let Nt = pbr_functions::sample_texture( + pbr_bindings::normal_map_texture, + pbr_bindings::normal_map_sampler, + uv, + bias, + ).rgb; + pbr_input.N = pbr_functions::apply_normal_mapping( pbr_bindings::material.flags, pbr_input.world_normal, double_sided, is_front, -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP in.world_tangent, -#endif -#endif -#ifdef VERTEX_UVS - uv, -#endif + Nt, view.mip_bias, -#ifdef MESHLET_MESH_MATERIAL_PASS - in.ddx_uv, - in.ddy_uv, -#endif ); -#endif + +#endif // STANDARD_MATERIAL_NORMAL_MAP + +#ifdef STANDARD_MATERIAL_CLEARCOAT + + // Note: `KHR_materials_clearcoat` specifies that, if there's no + // clearcoat normal map, we must set the normal to the mesh's normal, + // and not to the main layer's bumped normal. + +#ifdef STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP + + let clearcoat_Nt = pbr_functions::sample_texture( + pbr_bindings::clearcoat_normal_texture, + pbr_bindings::clearcoat_normal_sampler, + uv, + bias, + ).rgb; + + pbr_input.clearcoat_N = pbr_functions::apply_normal_mapping( + pbr_bindings::material.flags, + pbr_input.world_normal, + double_sided, + is_front, + in.world_tangent, + clearcoat_Nt, + view.mip_bias, + ); + +#endif // STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP + +#endif // STANDARD_MATERIAL_CLEARCOAT + +#endif // VERTEX_TANGENTS +#endif // VERTEX_UVS + +#endif // LOAD_PREPASS_NORMALS // TODO: Meshlet support #ifdef LIGHTMAP diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 60db5106f8..d16317e6af 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -6,6 +6,7 @@ mesh_view_bindings as view_bindings, mesh_view_types, lighting, + lighting::{LAYER_BASE, LAYER_CLEARCOAT}, transmission, clustered_forward as clustering, shadows, @@ -13,15 +14,34 @@ irradiance_volume, mesh_types::{MESH_FLAGS_SHADOW_RECEIVER_BIT, MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT}, } - #import bevy_render::maths::E +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else // PREPASS_PIPELINE +#import bevy_pbr::forward_io::VertexOutput +#endif // PREPASS_PIPELINE + #ifdef ENVIRONMENT_MAP #import bevy_pbr::environment_map #endif #import bevy_core_pipeline::tonemapping::{screen_space_dither, powsafe, tone_mapping} +// Biasing info needed to sample from a texture when calling `sample_texture`. +// How this is done depends on whether we're rendering meshlets or regular +// meshes. +struct SampleBias { +#ifdef MESHLET_MESH_MATERIAL_PASS + ddx_uv: vec2, + ddy_uv: vec2, +#else // MESHLET_MESH_MATERIAL_PASS + mip_bias: f32, +#endif // MESHLET_MESH_MATERIAL_PASS +} + // This is the standard 4x4 ordered dithering pattern from [1]. // // We can't use `array, 4>` because they can't be indexed dynamically @@ -98,6 +118,21 @@ fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4) return color; } +// Samples a texture using the appropriate biasing metric for the type of mesh +// in use (mesh vs. meshlet). +fn sample_texture( + texture: texture_2d, + samp: sampler, + uv: vec2, + bias: SampleBias, +) -> vec4 { +#ifdef MESHLET_MESH_MATERIAL_PASS + return textureSampleGrad(texture, samp, uv, bias.ddx_uv, bias.ddy_uv); +#else + return textureSampleBias(texture, samp, uv, bias.mip_bias); +#endif +} + fn prepare_world_normal( world_normal: vec3, double_sided: bool, @@ -119,19 +154,9 @@ fn apply_normal_mapping( world_normal: vec3, double_sided: bool, is_front: bool, -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP world_tangent: vec4, -#endif -#endif -#ifdef VERTEX_UVS - uv: vec2, -#endif + in_Nt: vec3, mip_bias: f32, -#ifdef MESHLET_MESH_MATERIAL_PASS - ddx_uv: vec2, - ddy_uv: vec2, -#endif ) -> vec3 { // 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 @@ -141,26 +166,15 @@ fn apply_normal_mapping( // http://www.mikktspace.com/ var N: vec3 = world_normal; -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP // NOTE: The mikktspace method of normal mapping explicitly requires that these NOT be // normalized nor any Gram-Schmidt applied to ensure the vertex normal is orthogonal to the // vertex tangent! Do not change this code unless you really know what you are doing. // http://www.mikktspace.com/ var T: vec3 = world_tangent.xyz; var B: vec3 = world_tangent.w * cross(N, T); -#endif -#endif -#ifdef VERTEX_TANGENTS -#ifdef VERTEX_UVS -#ifdef STANDARD_MATERIAL_NORMAL_MAP // Nt is the tangent-space normal. -#ifdef MESHLET_MESH_MATERIAL_PASS - var Nt = textureSampleGrad(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, uv, ddx_uv, ddy_uv).rgb; -#else - var Nt = textureSampleBias(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, uv, mip_bias).rgb; -#endif + var Nt = in_Nt; if (standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u { // Only use the xy components and derive z for 2-component normal maps. Nt = vec3(Nt.rg * 2.0 - 1.0, 0.0); @@ -183,9 +197,6 @@ fn apply_normal_mapping( // unless you really know what you are doing. // http://www.mikktspace.com/ N = Nt.x * T + Nt.y * B + Nt.z * N; -#endif -#endif -#endif return normalize(N); } @@ -232,11 +243,18 @@ fn apply_pbr_lighting( // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" let NdotV = max(dot(in.N, in.V), 0.0001); + let R = reflect(-in.V, in.N); - // Remapping [0,1] reflectance to F0 - // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping - let reflectance = in.material.reflectance; - let F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + output_color.rgb * metallic; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Do the above calculations again for the clearcoat layer. Remember that + // the clearcoat can have its own roughness and its own normal. + let clearcoat = in.material.clearcoat; + let clearcoat_perceptual_roughness = in.material.clearcoat_perceptual_roughness; + let clearcoat_roughness = lighting::perceptualRoughnessToRoughness(clearcoat_perceptual_roughness); + let clearcoat_N = in.clearcoat_N; + let clearcoat_NdotV = max(dot(clearcoat_N, in.V), 0.0001); + let clearcoat_R = reflect(-in.V, clearcoat_N); +#endif // STANDARD_MATERIAL_CLEARCOAT // Diffuse strength is inversely related to metallicity, specular and diffuse transmission let diffuse_color = output_color.rgb * (1.0 - metallic) * (1.0 - specular_transmission) * (1.0 - diffuse_transmission); @@ -247,15 +265,58 @@ fn apply_pbr_lighting( // Calculate the world position of the second Lambertian lobe used for diffuse transmission, by subtracting material thickness let diffuse_transmissive_lobe_world_position = in.world_position - vec4(in.world_normal, 0.0) * thickness; - let R = reflect(-in.V, in.N); - - let f_ab = lighting::F_AB(perceptual_roughness, NdotV); + let F0 = lighting::F0(in.material.reflectance, metallic, output_color.rgb); + let F_ab = lighting::F_AB(perceptual_roughness, NdotV); var direct_light: vec3 = vec3(0.0); // Transmitted Light (Specular and Diffuse) var transmitted_light: vec3 = vec3(0.0); + // Pack all the values into a structure. + var lighting_input: lighting::LightingInput; + lighting_input.layers[LAYER_BASE].NdotV = NdotV; + lighting_input.layers[LAYER_BASE].N = in.N; + lighting_input.layers[LAYER_BASE].R = R; + lighting_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + lighting_input.layers[LAYER_BASE].roughness = roughness; + lighting_input.P = in.world_position.xyz; + lighting_input.V = in.V; + lighting_input.diffuse_color = diffuse_color; + lighting_input.F0_ = F0; + lighting_input.F_ab = F_ab; +#ifdef STANDARD_MATERIAL_CLEARCOAT + lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV; + lighting_input.layers[LAYER_CLEARCOAT].N = clearcoat_N; + lighting_input.layers[LAYER_CLEARCOAT].R = clearcoat_R; + lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = clearcoat_perceptual_roughness; + lighting_input.layers[LAYER_CLEARCOAT].roughness = clearcoat_roughness; + lighting_input.clearcoat_strength = clearcoat; +#endif // STANDARD_MATERIAL_CLEARCOAT + + // And do the same for transmissive if we need to. +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmissive_lighting_input: lighting::LightingInput; + transmissive_lighting_input.layers[LAYER_BASE].NdotV = 1.0; + transmissive_lighting_input.layers[LAYER_BASE].N = -in.N; + transmissive_lighting_input.layers[LAYER_BASE].R = vec3(0.0); + transmissive_lighting_input.layers[LAYER_BASE].perceptual_roughness = 1.0; + transmissive_lighting_input.layers[LAYER_BASE].roughness = 1.0; + transmissive_lighting_input.P = diffuse_transmissive_lobe_world_position.xyz; + transmissive_lighting_input.V = -in.V; + transmissive_lighting_input.diffuse_color = diffuse_transmissive_color; + transmissive_lighting_input.F0_ = vec3(0.0); + transmissive_lighting_input.F_ab = vec2(0.0); +#ifdef STANDARD_MATERIAL_CLEARCOAT + transmissive_lighting_input.layers[LAYER_CLEARCOAT].NdotV = 0.0; + transmissive_lighting_input.layers[LAYER_CLEARCOAT].N = vec3(0.0); + transmissive_lighting_input.layers[LAYER_CLEARCOAT].R = vec3(0.0); + transmissive_lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = 0.0; + transmissive_lighting_input.layers[LAYER_CLEARCOAT].roughness = 0.0; + transmissive_lighting_input.clearcoat_strength = 0.0; +#endif // STANDARD_MATERIAL_CLEARCOAT +#endif // STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + let view_z = dot(vec4( view_bindings::view.inverse_view[0].z, view_bindings::view.inverse_view[1].z, @@ -273,7 +334,8 @@ fn apply_pbr_lighting( && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal); } - let light_contrib = lighting::point_light(in.world_position.xyz, light_id, roughness, NdotV, in.N, in.V, R, F0, f_ab, diffuse_color); + + let light_contrib = lighting::point_light(light_id, &lighting_input); direct_light += light_contrib * shadow; #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -284,14 +346,16 @@ fn apply_pbr_lighting( // roughness = 1.0; // NdotV = 1.0; // R = vec3(0.0) // doesn't really matter - // f_ab = vec2(0.1) + // F_ab = vec2(0.1) // F0 = vec3(0.0) var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); } - let transmitted_light_contrib = lighting::point_light(diffuse_transmissive_lobe_world_position.xyz, light_id, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + + let transmitted_light_contrib = + lighting::point_light(light_id, &transmissive_lighting_input); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -305,7 +369,8 @@ fn apply_pbr_lighting( && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = shadows::fetch_spot_shadow(light_id, in.world_position, in.world_normal); } - let light_contrib = lighting::spot_light(in.world_position.xyz, light_id, roughness, NdotV, in.N, in.V, R, F0, f_ab, diffuse_color); + + let light_contrib = lighting::spot_light(light_id, &lighting_input); direct_light += light_contrib * shadow; #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -316,14 +381,16 @@ fn apply_pbr_lighting( // roughness = 1.0; // NdotV = 1.0; // R = vec3(0.0) // doesn't really matter - // f_ab = vec2(0.1) + // F_ab = vec2(0.1) // F0 = vec3(0.0) var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { transmitted_shadow = shadows::fetch_spot_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); } - let transmitted_light_contrib = lighting::spot_light(diffuse_transmissive_lobe_world_position.xyz, light_id, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + + let transmitted_light_contrib = + lighting::spot_light(light_id, &transmissive_lighting_input); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -343,7 +410,9 @@ fn apply_pbr_lighting( && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = shadows::fetch_directional_shadow(i, in.world_position, in.world_normal, view_z); } - var light_contrib = lighting::directional_light(i, roughness, NdotV, in.N, in.V, R, F0, f_ab, diffuse_color); + + var light_contrib = lighting::directional_light(i, &lighting_input); + #ifdef DIRECTIONAL_LIGHT_SHADOW_MAP_DEBUG_CASCADES light_contrib = shadows::cascade_debug_visualization(light_contrib, i, view_z); #endif @@ -357,14 +426,16 @@ fn apply_pbr_lighting( // roughness = 1.0; // NdotV = 1.0; // R = vec3(0.0) // doesn't really matter - // f_ab = vec2(0.1) + // F_ab = vec2(0.1) // F0 = vec3(0.0) var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { transmitted_shadow = shadows::fetch_directional_shadow(i, diffuse_transmissive_lobe_world_position, -in.world_normal, view_z); } - let transmitted_light_contrib = lighting::directional_light(i, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + + let transmitted_light_contrib = + lighting::directional_light(i, &transmissive_lighting_input); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -414,17 +485,8 @@ fn apply_pbr_lighting( // Note that up until this point, we have only accumulated diffuse light. // This call is the first call that can accumulate specular light. #ifdef ENVIRONMENT_MAP - let environment_light = environment_map::environment_map_light( - perceptual_roughness, - roughness, - diffuse_color, - NdotV, - f_ab, - in.N, - R, - F0, - in.world_position.xyz, - any(indirect_light != vec3(0.0f))); + let environment_light = + environment_map::environment_map_light(&lighting_input, any(indirect_light != vec3(0.0f))); indirect_light += environment_light.diffuse * diffuse_occlusion + environment_light.specular * specular_occlusion; @@ -433,7 +495,7 @@ fn apply_pbr_lighting( // light in the call to `specular_transmissive_light()` below var specular_transmitted_environment_light = vec3(0.0); -#ifdef STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION +#ifdef STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION // NOTE: We use the diffuse transmissive color, inverted normal and view vectors, // and the following simplified values for the transmitted environment light contribution // approximation: @@ -452,24 +514,37 @@ fn apply_pbr_lighting( refract(in.V, -in.N, 1.0 / ior) * thickness // add refracted vector scaled by thickness, towards exit point ); // normalize to find exit point view vector - let transmitted_environment_light = bevy_pbr::environment_map::environment_map_light( - perceptual_roughness, - roughness, - vec3(1.0), - 1.0, - f_ab, - -in.N, - T, - vec3(1.0), - in.world_position.xyz, - false); + var transmissive_environment_light_input: lighting::LightingInput; + transmissive_environment_light_input.diffuse_color = vec3(1.0); + transmissive_environment_light_input.layers[LAYER_BASE].NdotV = 1.0; + transmissive_environment_light_input.P = in.world_position.xyz; + transmissive_environment_light_input.layers[LAYER_BASE].N = -in.N; + transmissive_environment_light_input.V = in.V; + transmissive_environment_light_input.layers[LAYER_BASE].R = T; + transmissive_environment_light_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + transmissive_environment_light_input.layers[LAYER_BASE].roughness = roughness; + transmissive_environment_light_input.F0_ = vec3(1.0); + transmissive_environment_light_input.F_ab = vec2(0.1); +#ifdef STANDARD_MATERIAL_CLEARCOAT + // No clearcoat. + transmissive_environment_light_input.clearcoat_strength = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].NdotV = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].N = in.N; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].R = vec3(0.0); + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].perceptual_roughness = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].roughness = 0.0; +#endif // STANDARD_MATERIAL_CLEARCOAT + + let transmitted_environment_light = + environment_map::environment_map_light(&transmissive_environment_light_input, false); + #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color; #endif #ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION specular_transmitted_environment_light = transmitted_environment_light.specular * specular_transmissive_color; #endif -#endif // STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION +#endif // STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION #else // If there's no environment map light, there's no transmitted environment // light specular component, so we can just hardcode it to zero. @@ -479,7 +554,15 @@ fn apply_pbr_lighting( // Ambient light (indirect) indirect_light += ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, diffuse_occlusion); - let emissive_light = emissive.rgb * output_color.a; + var emissive_light = emissive.rgb * output_color.a; + + // "The clearcoat layer is on top of emission in the layering stack. + // Consequently, the emission is darkened by the Fresnel term." + // + // +#ifdef STANDARD_MATERIAL_CLEARCOAT + emissive_light = emissive_light * (0.04 + (1.0 - 0.04) * pow(1.0 - clearcoat_NdotV, 5.0)); +#endif #ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION transmitted_light += transmission::specular_transmissive_light(in.world_position, in.frag_coord.xyz, view_z, in.N, in.V, F0, ior, thickness, perceptual_roughness, specular_transmissive_color, specular_transmitted_environment_light).rgb; diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index cfb43914ce..ae629f6699 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -4,9 +4,11 @@ mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, mesh_view_bindings as view_bindings, } - #import bevy_render::maths::PI +const LAYER_BASE: u32 = 0; +const LAYER_CLEARCOAT: u32 = 1; + // From the Filament design doc // https://google.github.io/filament/Filament.html#table_symbols // Symbol Definition @@ -41,6 +43,69 @@ // // The above integration needs to be approximated. +// Input to a lighting function for a single layer (either the base layer or the +// clearcoat layer). +struct LayerLightingInput { + // The normal vector. + N: vec3, + // The reflected vector. + R: vec3, + // The normal vector ⋅ the view vector. + NdotV: f32, + + // The perceptual roughness of the layer. + perceptual_roughness: f32, + // The roughness of the layer. + roughness: f32, +} + +// Input to a lighting function (`point_light`, `spot_light`, +// `directional_light`). +struct LightingInput { +#ifdef STANDARD_MATERIAL_CLEARCOAT + layers: array, +#else // STANDARD_MATERIAL_CLEARCOAT + layers: array, +#endif // STANDARD_MATERIAL_CLEARCOAT + + // The world-space position. + P: vec3, + // The vector to the light. + V: vec3, + + // The diffuse color of the material. + diffuse_color: vec3, + + // Specular reflectance at the normal incidence angle. + // + // This should be read F₀, but due to Naga limitations we can't name it that. + F0_: vec3, + // Constants for the BRDF approximation. + // + // See `EnvBRDFApprox` in + // . + // What we call `F_ab` they call `AB`. + F_ab: vec2, + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // The strength of the clearcoat layer. + clearcoat_strength: f32, +#endif // STANDARD_MATERIAL_CLEARCOAT +} + +// Values derived from the `LightingInput` for both diffuse and specular lights. +struct DerivedLightingInput { + // The half-vector between L, the incident light vector, and V, the view + // vector. + H: vec3, + // The normal vector ⋅ the incident light vector. + NdotL: f32, + // The normal vector ⋅ the half-vector. + NdotH: f32, + // The incident light vector ⋅ the half-vector. + LdotH: f32, +} + // distanceAttenuation is simply the square falloff of light intensity // combined with a smooth attenuation at the edge of the light radius // @@ -60,10 +125,10 @@ fn getDistanceAttenuation(distanceSquare: f32, inverseRangeSquared: f32) -> f32 // Simple implementation, has precision problems when using fp16 instead of fp32 // see https://google.github.io/filament/Filament.html#listing_speculardfp16 -fn D_GGX(roughness: f32, NoH: f32, h: vec3) -> f32 { - let oneMinusNoHSquared = 1.0 - NoH * NoH; - let a = NoH * roughness; - let k = roughness / (oneMinusNoHSquared + a * a); +fn D_GGX(roughness: f32, NdotH: f32, h: vec3) -> f32 { + let oneMinusNdotHSquared = 1.0 - NdotH * NdotH; + let a = NdotH * roughness; + let k = roughness / (oneMinusNdotHSquared + a * a); let d = k * k * (1.0 / PI); return d; } @@ -75,62 +140,141 @@ fn D_GGX(roughness: f32, NoH: f32, h: vec3) -> f32 { // where // V(v,l,α) = 0.5 / { n⋅l sqrt((n⋅v)^2 (1−α2) + α2) + n⋅v sqrt((n⋅l)^2 (1−α2) + α2) } // Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.html#listing_approximatedspecularv -fn V_SmithGGXCorrelated(roughness: f32, NoV: f32, NoL: f32) -> f32 { +fn V_SmithGGXCorrelated(roughness: f32, NdotV: f32, NdotL: f32) -> f32 { let a2 = roughness * roughness; - let lambdaV = NoL * sqrt((NoV - a2 * NoV) * NoV + a2); - let lambdaL = NoV * sqrt((NoL - a2 * NoL) * NoL + a2); + let lambdaV = NdotL * sqrt((NdotV - a2 * NdotV) * NdotV + a2); + let lambdaL = NdotV * sqrt((NdotL - a2 * NdotL) * NdotL + a2); let v = 0.5 / (lambdaV + lambdaL); return v; } +// A simpler, but nonphysical, alternative to Smith-GGX. We use this for +// clearcoat, per the Filament spec. +// +// https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel#toc4.9.1 +fn V_Kelemen(LdotH: f32) -> f32 { + return 0.25 / (LdotH * LdotH); +} + // Fresnel function // see https://google.github.io/filament/Filament.html#citation-schlick94 // F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 − f_0) (1 − v⋅h)^5 -fn F_Schlick_vec(f0: vec3, f90: f32, VoH: f32) -> vec3 { +fn F_Schlick_vec(f0: vec3, f90: f32, VdotH: f32) -> vec3 { // not using mix to keep the vec3 and float versions identical - return f0 + (f90 - f0) * pow(1.0 - VoH, 5.0); + return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); } -fn F_Schlick(f0: f32, f90: f32, VoH: f32) -> f32 { +fn F_Schlick(f0: f32, f90: f32, VdotH: f32) -> f32 { // not using mix to keep the vec3 and float versions identical - return f0 + (f90 - f0) * pow(1.0 - VoH, 5.0); + return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); } -fn fresnel(f0: vec3, LoH: f32) -> vec3 { +fn fresnel(f0: vec3, LdotH: f32) -> vec3 { // f_90 suitable for ambient occlusion // see https://google.github.io/filament/Filament.html#lighting/occlusion let f90 = saturate(dot(f0, vec3(50.0 * 0.33))); - return F_Schlick_vec(f0, f90, LoH); + return F_Schlick_vec(f0, f90, LdotH); } // Specular BRDF // https://google.github.io/filament/Filament.html#materialsystem/specularbrdf +// N, V, and L must all be normalized. +fn derive_lighting_input(N: vec3, V: vec3, L: vec3) -> DerivedLightingInput { + var input: DerivedLightingInput; + var H: vec3 = normalize(L + V); + input.H = H; + input.NdotL = saturate(dot(N, L)); + input.NdotH = saturate(dot(N, H)); + input.LdotH = saturate(dot(L, H)); + return input; +} + +// Returns L in the `xyz` components and the specular intensity in the `w` component. +fn compute_specular_layer_values_for_point_light( + input: ptr, + layer: u32, + V: vec3, + light_to_frag: vec3, + light_position_radius: f32, +) -> vec4 { + // Unpack. + let R = (*input).layers[layer].R; + let a = (*input).layers[layer].roughness; + + // Representative Point Area Lights. + // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 + let centerToRay = dot(light_to_frag, R) * R - light_to_frag; + let closestPoint = light_to_frag + centerToRay * saturate( + light_position_radius * inverseSqrt(dot(centerToRay, centerToRay))); + let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); + let normalizationFactor = a / saturate(a + (light_position_radius * 0.5 * LspecLengthInverse)); + let intensity = normalizationFactor * normalizationFactor; + + let L: vec3 = closestPoint * LspecLengthInverse; // normalize() equivalent? + return vec4(L, intensity); +} + // Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m // f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) } fn specular( - f0: vec3, - roughness: f32, - h: vec3, - NoV: f32, - NoL: f32, - NoH: f32, - LoH: f32, - specularIntensity: f32, - f_ab: vec2 + input: ptr, + derived_input: ptr, + specular_intensity: f32, ) -> vec3 { - let D = D_GGX(roughness, NoH, h); - let V = V_SmithGGXCorrelated(roughness, NoV, NoL); - let F = fresnel(f0, LoH); + // Unpack. + 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; + let LdotH = (*derived_input).LdotH; - var Fr = (specularIntensity * D * V) * F; - - // Multiscattering approximation: https://google.github.io/filament/Filament.html#listing_energycompensationimpl - Fr *= 1.0 + f0 * (1.0 / f_ab.x - 1.0); + // Calculate distribution. + let D = D_GGX(roughness, NdotH, H); + // Calculate visibility. + let V = V_SmithGGXCorrelated(roughness, NdotV, NdotL); + // Calculate the Fresnel term. + 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); return Fr; } +// Calculates the specular light for the clearcoat layer. Returns Fc, the +// Fresnel term, in the first channel, and Frc, the specular clearcoat light, in +// the second channel. +// +// +fn specular_clearcoat( + input: ptr, + derived_input: ptr, + clearcoat_strength: f32, + specular_intensity: f32, +) -> vec2 { + // Unpack. + let roughness = (*input).layers[LAYER_CLEARCOAT].roughness; + let H = (*derived_input).H; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + // Calculate distribution. + let Dc = D_GGX(roughness, NdotH, H); + // Calculate visibility. + let Vc = V_Kelemen(LdotH); + // Calculate the Fresnel term. + let Fc = F_Schlick(0.04, 1.0, LdotH) * clearcoat_strength; + // Calculate the specular light. + let Frc = (specular_intensity * Dc * Vc) * Fc; + return vec2(Fc, Frc); +} + // 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 @@ -145,26 +289,41 @@ fn specular( // Disney approximation // See https://google.github.io/filament/Filament.html#citation-burley12 // minimal quality difference -fn Fd_Burley(roughness: f32, NoV: f32, NoL: f32, LoH: f32) -> f32 { - let f90 = 0.5 + 2.0 * roughness * LoH * LoH; - let lightScatter = F_Schlick(1.0, f90, NoL); - let viewScatter = F_Schlick(1.0, f90, NoV); +fn Fd_Burley( + input: ptr, + derived_input: ptr, +) -> f32 { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let NdotL = (*derived_input).NdotL; + let LdotH = (*derived_input).LdotH; + + let f90 = 0.5 + 2.0 * roughness * LdotH * LdotH; + let lightScatter = F_Schlick(1.0, f90, NdotL); + let viewScatter = F_Schlick(1.0, f90, NdotV); return lightScatter * viewScatter * (1.0 / PI); } +// Remapping [0,1] reflectance to F0 +// See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping +fn F0(reflectance: f32, metallic: f32, color: vec3) -> vec3 { + return 0.16 * reflectance * reflectance * (1.0 - metallic) + color * metallic; +} + // Scale/bias approximation // https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile // TODO: Use a LUT (more accurate) -fn F_AB(perceptual_roughness: f32, NoV: f32) -> vec2 { +fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2 { let c0 = vec4(-1.0, -0.0275, -0.572, 0.022); let c1 = vec4(1.0, 0.0425, 1.04, -0.04); let r = perceptual_roughness * c0 + c1; - let a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y; + let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; return vec2(-1.04, 1.04) * a004 + r.zw; } -fn EnvBRDFApprox(f0: vec3, f_ab: vec2) -> vec3 { - return f0 * f_ab.x + f_ab.y; +fn EnvBRDFApprox(F0: vec3, F_ab: vec2) -> vec3 { + return F0 * F_ab.x + F_ab.y; } fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { @@ -175,50 +334,69 @@ fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { return clampedPerceptualRoughness * clampedPerceptualRoughness; } -fn point_light( - world_position: vec3, - light_id: u32, - roughness: f32, - NdotV: f32, - N: vec3, - V: vec3, - R: vec3, - F0: vec3, - f_ab: vec2, - diffuseColor: vec3 -) -> vec3 { +fn point_light(light_id: u32, input: ptr) -> vec3 { + // Unpack. + let diffuse_color = (*input).diffuse_color; + let P = (*input).P; + let N = (*input).layers[LAYER_BASE].N; + let V = (*input).V; + let light = &view_bindings::point_lights.data[light_id]; - let light_to_frag = (*light).position_radius.xyz - world_position.xyz; + let light_to_frag = (*light).position_radius.xyz - P; let distance_square = dot(light_to_frag, light_to_frag); let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); - // Specular. - // Representative Point Area Lights. - // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 - let a = roughness; - let centerToRay = dot(light_to_frag, R) * R - light_to_frag; - let closestPoint = light_to_frag + centerToRay * saturate((*light).position_radius.w * inverseSqrt(dot(centerToRay, centerToRay))); - let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); - let normalizationFactor = a / saturate(a + ((*light).position_radius.w * 0.5 * LspecLengthInverse)); - let specularIntensity = normalizationFactor * normalizationFactor; + // Base layer - var L: vec3 = closestPoint * LspecLengthInverse; // normalize() equivalent? - var H: vec3 = normalize(L + V); - var NoL: f32 = saturate(dot(N, L)); - var NoH: f32 = saturate(dot(N, H)); - var LoH: f32 = saturate(dot(L, H)); + let specular_L_intensity = compute_specular_layer_values_for_point_light( + input, + LAYER_BASE, + V, + light_to_frag, + (*light).position_radius.w, + ); + var specular_derived_input = derive_lighting_input(N, V, specular_L_intensity.xyz); - let specular_light = specular(F0, roughness, H, NdotV, NoL, NoH, LoH, specularIntensity, f_ab); + let specular_intensity = specular_L_intensity.w; + let specular_light = specular(input, &specular_derived_input, specular_intensity); + + // Clearcoat + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Unpack. + let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; + let clearcoat_strength = (*input).clearcoat_strength; + + // 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. + let clearcoat_specular_L_intensity = compute_specular_layer_values_for_point_light( + input, + LAYER_CLEARCOAT, + V, + light_to_frag, + (*light).position_radius.w, + ); + var clearcoat_specular_derived_input = + derive_lighting_input(clearcoat_N, V, clearcoat_specular_L_intensity.xyz); + + // Calculate the specular light. + let clearcoat_specular_intensity = clearcoat_specular_L_intensity.w; + let Fc_Frc = specular_clearcoat( + input, + &clearcoat_specular_derived_input, + clearcoat_strength, + clearcoat_specular_intensity + ); + let inv_Fc = 1.0 - Fc_Frc.r; // Inverse Fresnel term. + let Frc = Fc_Frc.g; // Clearcoat light. +#endif // STANDARD_MATERIAL_CLEARCOAT // Diffuse. - // Comes after specular since its NoL is used in the lighting equation. - L = normalize(light_to_frag); - H = normalize(L + V); - NoL = saturate(dot(N, L)); - NoH = saturate(dot(N, H)); - LoH = saturate(dot(L, H)); - - let diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH); + // 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); // See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation // Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩ @@ -233,23 +411,23 @@ fn point_light( // NOTE: (*light).color.rgb is premultiplied with (*light).intensity / 4 π (which would be the luminous intensity) on the CPU - return ((diffuse + specular_light) * (*light).color_inverse_square_range.rgb) * (rangeAttenuation * NoL); + var color: vec3; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Account for the Fresnel term from the clearcoat darkening the main layer. + // + // + color = (diffuse + specular_light * inv_Fc) * inv_Fc + Frc; +#else // STANDARD_MATERIAL_CLEARCOAT + color = diffuse + specular_light; +#endif // STANDARD_MATERIAL_CLEARCOAT + + return color * (*light).color_inverse_square_range.rgb * + (rangeAttenuation * derived_input.NdotL); } -fn spot_light( - world_position: vec3, - light_id: u32, - roughness: f32, - NdotV: f32, - N: vec3, - V: vec3, - R: vec3, - F0: vec3, - f_ab: vec2, - diffuseColor: vec3 -) -> vec3 { +fn spot_light(light_id: u32, input: ptr) -> vec3 { // reuse the point light calculations - let point_light = point_light(world_position, light_id, roughness, NdotV, N, V, R, F0, f_ab, diffuseColor); + let point_light = point_light(light_id, input); let light = &view_bindings::point_lights.data[light_id]; @@ -259,7 +437,7 @@ fn spot_light( if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u { spot_dir.y = -spot_dir.y; } - let light_to_frag = (*light).position_radius.xyz - world_position.xyz; + let light_to_frag = (*light).position_radius.xyz - (*input).P.xyz; // calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight // spot_scale and spot_offset have been precomputed @@ -271,19 +449,48 @@ fn spot_light( return point_light * spot_attenuation; } -fn directional_light(light_id: u32, roughness: f32, NdotV: f32, normal: vec3, view: vec3, R: vec3, F0: vec3, f_ab: vec2, diffuseColor: vec3) -> vec3 { +fn directional_light(light_id: u32, input: ptr) -> vec3 { + // Unpack. + let diffuse_color = (*input).diffuse_color; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let N = (*input).layers[LAYER_BASE].N; + let V = (*input).V; + let roughness = (*input).layers[LAYER_BASE].roughness; + 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 half_vector = normalize(incident_light + view); - let NoL = saturate(dot(normal, incident_light)); - let NoH = saturate(dot(normal, half_vector)); - let LoH = saturate(dot(incident_light, half_vector)); + let diffuse = diffuse_color * Fd_Burley(input, &derived_input); - let diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH); - let specularIntensity = 1.0; - let specular_light = specular(F0, roughness, half_vector, NdotV, NoL, NoH, LoH, specularIntensity, f_ab); + let specular_light = specular(input, &derived_input, 1.0); - return (specular_light + diffuse) * (*light).color.rgb * NoL; +#ifdef STANDARD_MATERIAL_CLEARCOAT + let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; + let clearcoat_strength = (*input).clearcoat_strength; + + // 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); + + let Fc_Frc = + specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, 1.0); + let inv_Fc = 1.0 - Fc_Frc.r; + let Frc = Fc_Frc.g; +#endif // STANDARD_MATERIAL_CLEARCOAT + + var color: vec3; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Account for the Fresnel term from the clearcoat darkening the main layer. + // + // + color = (diffuse + specular_light * inv_Fc) * inv_Fc * derived_input.NdotL + + Frc * derived_clearcoat_input.NdotL; +#else // STANDARD_MATERIAL_CLEARCOAT + color = (diffuse + specular_light) * derived_input.NdotL; +#endif // STANDARD_MATERIAL_CLEARCOAT + + return color * (*light).color.rgb; } diff --git a/crates/bevy_pbr/src/render/pbr_prepass.wgsl b/crates/bevy_pbr/src/render/pbr_prepass.wgsl index c77d71ebca..86840cdb38 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass.wgsl @@ -1,8 +1,10 @@ #import bevy_pbr::{ pbr_prepass_functions, + pbr_bindings, pbr_bindings::material, pbr_types, pbr_functions, + pbr_functions::SampleBias, prepass_io, mesh_view_bindings::view, } @@ -45,26 +47,42 @@ fn fragment( is_front, ); - let normal = pbr_functions::apply_normal_mapping( + var normal = world_normal; + +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS +#ifdef STANDARD_MATERIAL_NORMAL_MAP + + // Fill in the sample bias so we can sample from textures. + var bias: SampleBias; +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv = in.ddx_uv; + bias.ddy_uv = in.ddy_uv; +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias = view.mip_bias; +#endif // MESHLET_MESH_MATERIAL_PASS + + let Nt = pbr_functions::sample_texture( + pbr_bindings::normal_map_texture, + pbr_bindings::normal_map_sampler, + in.uv, + bias, + ).rgb; + + normal = pbr_functions::apply_normal_mapping( material.flags, world_normal, double_sided, is_front, -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP in.world_tangent, -#endif // STANDARD_MATERIAL_NORMAL_MAP -#endif // VERTEX_TANGENTS -#ifdef VERTEX_UVS - in.uv, -#endif // VERTEX_UVS + Nt, view.mip_bias, -#ifdef MESHLET_MESH_MATERIAL_PASS - in.ddx_uv, - in.ddy_uv, -#endif // MESHLET_MESH_MATERIAL_PASS ); +#endif // STANDARD_MATERIAL_NORMAL_MAP +#endif // VERTEX_TANGENTS +#endif // VERTEX_UVS + out.normal = vec4(normal * 0.5 + vec3(0.5), 1.0); } else { out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 98d0e56918..aaa133fa5d 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -15,6 +15,8 @@ struct StandardMaterial { thickness: f32, ior: f32, attenuation_distance: f32, + clearcoat: f32, + clearcoat_perceptual_roughness: 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, @@ -44,6 +46,9 @@ const STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT: u32 = 1024u; const STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT: u32 = 2048u; const STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT: u32 = 4096u; 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_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) @@ -72,6 +77,8 @@ fn standard_material_new() -> StandardMaterial { material.ior = 1.5; material.attenuation_distance = 1.0; material.attenuation_color = vec4(1.0, 1.0, 1.0, 1.0); + material.clearcoat = 0.0; + material.clearcoat_perceptual_roughness = 0.0; material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE; material.alpha_cutoff = 0.5; material.parallax_depth_scale = 0.1; @@ -101,6 +108,7 @@ struct PbrInput { // view world position V: vec3, lightmap_light: vec3, + clearcoat_N: vec3, is_orthographic: bool, flags: u32, }; diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 573d8b6fbe..a4ea90d371 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -68,6 +68,7 @@ The default feature set enables most of the expected features of a game engine, |meshlet_processor|Enables processing meshes into meshlet meshes for bevy_pbr| |minimp3|MP3 audio format support (through minimp3)| |mp3|MP3 audio format support| +|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_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| |serialize|Enable serialization support through serde| diff --git a/examples/3d/clearcoat.rs b/examples/3d/clearcoat.rs new file mode 100644 index 0000000000..742d52ca31 --- /dev/null +++ b/examples/3d/clearcoat.rs @@ -0,0 +1,352 @@ +//! Demonstrates the clearcoat PBR feature. +//! +//! Clearcoat is a separate material layer that represents a thin translucent +//! layer over a material. Examples include (from the Filament spec [1]) car paint, +//! soda cans, and lacquered wood. +//! +//! In glTF, clearcoat is supported via the `KHR_materials_clearcoat` [2] +//! extension. This extension is well supported by tools; in particular, +//! Blender's glTF exporter maps the clearcoat feature of its Principled BSDF +//! node to this extension, allowing it to appear in Bevy. +//! +//! This Bevy example is inspired by the corresponding three.js example [3]. +//! +//! [1]: https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel +//! +//! [2]: https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md +//! +//! [3]: https://threejs.org/examples/webgl_materials_physical_clearcoat.html + +use std::f32::consts::PI; + +use bevy::{ + color::palettes::css::{BLUE, GOLD, WHITE}, + core_pipeline::{tonemapping::Tonemapping::AcesFitted, Skybox}, + math::vec3, + pbr::{CascadeShadowConfig, Cascades, CascadesVisibleEntities}, + prelude::*, + render::{primitives::CascadesFrusta, texture::ImageLoaderSettings}, +}; + +/// The size of each sphere. +const SPHERE_SCALE: f32 = 0.9; + +/// The speed at which the spheres rotate, in radians per second. +const SPHERE_ROTATION_SPEED: f32 = 0.8; + +/// Which type of light we're using: a point light or a directional light. +#[derive(Clone, Copy, PartialEq, Resource, Default)] +enum LightMode { + #[default] + Point, + Directional, +} + +/// Tags the example spheres. +#[derive(Component)] +struct ExampleSphere; + +/// Entry point. +pub fn main() { + App::new() + .init_resource::() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, animate_light) + .add_systems(Update, animate_spheres) + .add_systems(Update, (handle_input, update_help_text).chain()) + .run(); +} + +/// Initializes the scene. +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, + light_mode: Res, +) { + let sphere = create_sphere_mesh(&mut meshes); + spawn_car_paint_sphere(&mut commands, &mut materials, &asset_server, &sphere); + spawn_coated_glass_bubble_sphere(&mut commands, &mut materials, &sphere); + spawn_golf_ball(&mut commands, &asset_server); + spawn_scratched_gold_ball(&mut commands, &mut materials, &asset_server, &sphere); + + spawn_light(&mut commands); + spawn_camera(&mut commands, &asset_server); + spawn_text(&mut commands, &asset_server, &light_mode); +} + +/// Generates a sphere. +fn create_sphere_mesh(meshes: &mut Assets) -> Handle { + // We're going to use normal maps, so make sure we've generated tangents, or + // else the normal maps won't show up. + + let mut sphere_mesh = Sphere::new(1.0).mesh().build(); + sphere_mesh + .generate_tangents() + .expect("Failed to generate tangents"); + meshes.add(sphere_mesh) +} + +/// Spawn a regular object with a clearcoat layer. This looks like car paint. +fn spawn_car_paint_sphere( + commands: &mut Commands, + materials: &mut Assets, + asset_server: &AssetServer, + sphere: &Handle, +) { + commands + .spawn(PbrBundle { + mesh: sphere.clone(), + material: materials.add(StandardMaterial { + clearcoat: 1.0, + clearcoat_perceptual_roughness: 0.1, + normal_map_texture: Some(asset_server.load_with_settings( + "textures/BlueNoise-Normal.png", + |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + )), + metallic: 0.9, + perceptual_roughness: 0.5, + base_color: BLUE.into(), + ..default() + }), + transform: Transform::from_xyz(-1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)), + ..default() + }) + .insert(ExampleSphere); +} + +/// Spawn a semitransparent object with a clearcoat layer. +fn spawn_coated_glass_bubble_sphere( + commands: &mut Commands, + materials: &mut Assets, + sphere: &Handle, +) { + commands + .spawn(PbrBundle { + mesh: sphere.clone(), + material: materials.add(StandardMaterial { + clearcoat: 1.0, + clearcoat_perceptual_roughness: 0.1, + metallic: 0.5, + perceptual_roughness: 0.1, + base_color: Color::srgba(0.9, 0.9, 0.9, 0.3), + alpha_mode: AlphaMode::Blend, + ..default() + }), + transform: Transform::from_xyz(-1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)), + ..default() + }) + .insert(ExampleSphere); +} + +/// Spawns an object with both a clearcoat normal map (a scratched varnish) and +/// a main layer normal map (the golf ball pattern). +/// +/// This object is in glTF format, using the `KHR_materials_clearcoat` +/// extension. +fn spawn_golf_ball(commands: &mut Commands, asset_server: &AssetServer) { + commands + .spawn(SceneBundle { + scene: asset_server.load("models/GolfBall/GolfBall.glb#Scene0"), + transform: Transform::from_xyz(1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)), + ..default() + }) + .insert(ExampleSphere); +} + +/// Spawns an object with only a clearcoat normal map (a scratch pattern) and no +/// main layer normal map. +fn spawn_scratched_gold_ball( + commands: &mut Commands, + materials: &mut Assets, + asset_server: &AssetServer, + sphere: &Handle, +) { + commands + .spawn(PbrBundle { + mesh: sphere.clone(), + material: materials.add(StandardMaterial { + clearcoat: 1.0, + clearcoat_perceptual_roughness: 0.3, + clearcoat_normal_texture: Some(asset_server.load_with_settings( + "textures/ScratchedGold-Normal.png", + |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + )), + metallic: 0.9, + perceptual_roughness: 0.1, + base_color: GOLD.into(), + ..default() + }), + transform: Transform::from_xyz(1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)), + ..default() + }) + .insert(ExampleSphere); +} + +/// Spawns a light. +fn spawn_light(commands: &mut Commands) { + // Add the cascades objects used by the `DirectionalLightBundle`, since the + // user can toggle between a point light and a directional light. + commands + .spawn(PointLightBundle { + point_light: PointLight { + color: WHITE.into(), + intensity: 100000.0, + ..default() + }, + ..default() + }) + .insert(CascadesFrusta::default()) + .insert(Cascades::default()) + .insert(CascadeShadowConfig::default()) + .insert(CascadesVisibleEntities::default()); +} + +/// Spawns a camera with associated skybox and environment map. +fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { + commands + .spawn(Camera3dBundle { + camera: Camera { + hdr: true, + ..default() + }, + projection: Projection::Perspective(PerspectiveProjection { + fov: 27.0 / 180.0 * PI, + ..default() + }), + transform: Transform::from_xyz(0.0, 0.0, 10.0), + tonemapping: AcesFitted, + ..default() + }) + .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: 2000.0, + }); +} + +/// Spawns the help text. +fn spawn_text(commands: &mut Commands, asset_server: &AssetServer, light_mode: &LightMode) { + commands.spawn( + TextBundle { + text: light_mode.create_help_text(asset_server), + ..TextBundle::default() + } + .with_style(Style { + position_type: PositionType::Absolute, + bottom: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ); +} + +/// Moves the light around. +fn animate_light( + mut lights: Query<&mut Transform, Or<(With, With)>>, + time: Res