Implement clearcoat per the Filament and the KHR_materials_clearcoat specifications. (#13031)

Clearcoat is a separate material layer that represents a thin
translucent layer of a material. Examples include (from the [Filament
spec]) car paint, soda cans, and lacquered wood. This commit implements
support for clearcoat following the Filament and Khronos specifications,
marking the beginnings of support for multiple PBR layers in Bevy.

The [`KHR_materials_clearcoat`] specification describes the clearcoat
support in glTF. In Blender, applying a clearcoat to the Principled BSDF
node causes the clearcoat settings to be exported via this extension. As
of this commit, Bevy parses and reads the extension data when present in
glTF. Note that the `gltf` crate has no support for
`KHR_materials_clearcoat`; this patch therefore implements the JSON
semantics manually.

Clearcoat is integrated with `StandardMaterial`, but the code is behind
a series of `#ifdef`s that only activate when clearcoat is present.
Additionally, the `pbr_feature_layer_material_textures` Cargo feature
must be active in order to enable support for clearcoat factor maps,
clearcoat roughness maps, and clearcoat normal maps. This approach
mirrors the same pattern used by the existing transmission feature and
exists to avoid running out of texture bindings on platforms like WebGL
and WebGPU. Note that constant clearcoat factors and roughness values
*are* supported in the browser; only the relatively-less-common maps are
disabled on those platforms.

This patch refactors the lighting code in `StandardMaterial`
significantly in order to better support multiple layers in a natural
way. That code was due for a refactor in any case, so this is a nice
improvement.

A new demo, `clearcoat`, has been added. It's based on [the
corresponding three.js demo], but all the assets (aside from the skybox
and environment map) are my original work.

[Filament spec]:
https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel

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

[the corresponding three.js demo]:
https://threejs.org/examples/webgl_materials_physical_clearcoat.html

![Screenshot 2024-04-19
101143](https://github.com/bevyengine/bevy/assets/157897/3444bcb5-5c20-490c-b0ad-53759bd47ae2)

![Screenshot 2024-04-19
102054](https://github.com/bevyengine/bevy/assets/157897/6e953944-75b8-49ef-bc71-97b0a53b3a27)

## Changelog

### Added

* `StandardMaterial` now supports a clearcoat layer, which represents a
thin translucent layer over an underlying material.
* The glTF loader now supports the `KHR_materials_clearcoat` extension,
representing materials with clearcoat layers.

## Migration Guide

* The lighting functions in the `pbr_lighting` WGSL module now have
clearcoat parameters, if `STANDARD_MATERIAL_CLEARCOAT` is defined.

* The `R` reflection vector parameter has been removed from some
lighting functions, as it was unused.
This commit is contained in:
Patrick Walton 2024-05-05 17:57:05 -05:00 committed by GitHub
parent 89cd5f54f8
commit 77ed72bc16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1376 additions and 290 deletions

View file

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

Binary file not shown.

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

View file

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

View file

@ -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<StandardMaterial> {
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<usize>,
#[cfg(feature = "bevy_animation")] mut animation_context: Option<AnimationContext>,
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<Image> {
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<Image> {
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:
/// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md>
#[derive(Default)]
struct ClearcoatExtension {
clearcoat_factor: Option<f64>,
#[cfg(feature = "pbr_multi_layer_material_textures")]
clearcoat_texture: Option<Handle<Image>>,
clearcoat_roughness_factor: Option<f64>,
#[cfg(feature = "pbr_multi_layer_material_textures")]
clearcoat_roughness_texture: Option<Handle<Image>>,
#[cfg(feature = "pbr_multi_layer_material_textures")]
clearcoat_normal_texture: Option<Handle<Image>>,
}
impl ClearcoatExtension {
#[allow(unused_variables)]
fn parse(
load_context: &mut LoadContext,
document: &Document,
material_extensions: Option<&Map<String, Value>>,
) -> Option<ClearcoatExtension> {
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::<json::texture::Info>(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::<json::texture::Info>(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::<json::texture::Info>(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<usize> {
Some(
value::from_value::<json::texture::Info>(
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;

View file

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

View file

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

View file

@ -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<f32>,
@ -21,12 +24,16 @@ struct EnvironmentMapRadiances {
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
fn compute_radiances(
perceptual_roughness: f32,
N: vec3<f32>,
R: vec3<f32>,
input: ptr<function, LightingInput>,
layer: u32,
world_position: vec3<f32>,
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<f32>,
R: vec3<f32>,
input: ptr<function, LightingInput>,
layer: u32,
world_position: vec3<f32>,
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<function, EnvironmentMapLight>,
input: ptr<function, LightingInput>,
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<f32>(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:
// <https://google.github.io/filament/Filament.md.html#lighting/imagebasedlights/clearcoat>
(*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<f32>,
NdotV: f32,
f_ab: vec2<f32>,
N: vec3<f32>,
R: vec3<f32>,
F0: vec3<f32>,
world_position: vec3<f32>,
input: ptr<function, LightingInput>,
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;
}

View file

@ -338,6 +338,60 @@ pub struct StandardMaterial {
#[dependency]
pub occlusion_texture: Option<Handle<Image>>,
/// 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<Handle<Image>>,
/// 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<Handle<Image>>,
/// 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<Handle<Image>>,
/// 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<StandardMaterialUniform> 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<StandardMaterialUniform> 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());
}
}
}

View file

@ -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()];

View file

@ -23,3 +23,11 @@
@group(2) @binding(17) var diffuse_transmission_texture: texture_2d<f32>;
@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<f32>;
@group(2) @binding(20) var clearcoat_sampler: sampler;
@group(2) @binding(21) var clearcoat_roughness_texture: texture_2d<f32>;
@group(2) @binding(22) var clearcoat_roughness_sampler: sampler;
@group(2) @binding(23) var clearcoat_normal_texture: texture_2d<f32>;
@group(2) @binding(24) var clearcoat_normal_sampler: sampler;
#endif

View file

@ -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<f32> = 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<f32>(emissive.rgb * textureSampleGrad(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, in.ddx_uv, in.ddy_uv).rgb, 1.0);
#else
emissive = vec4<f32>(emissive.rgb * textureSampleBias(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, view.mip_bias).rgb, 1.0);
#endif
emissive = vec4<f32>(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

View file

@ -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<f32>,
ddy_uv: vec2<f32>,
#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<vec4<u32>, 4>` because they can't be indexed dynamically
@ -98,6 +118,21 @@ fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4<f32>)
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<f32>,
samp: sampler,
uv: vec2<f32>,
bias: SampleBias,
) -> vec4<f32> {
#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<f32>,
double_sided: bool,
@ -119,19 +154,9 @@ fn apply_normal_mapping(
world_normal: vec3<f32>,
double_sided: bool,
is_front: bool,
#ifdef VERTEX_TANGENTS
#ifdef STANDARD_MATERIAL_NORMAL_MAP
world_tangent: vec4<f32>,
#endif
#endif
#ifdef VERTEX_UVS
uv: vec2<f32>,
#endif
in_Nt: vec3<f32>,
mip_bias: f32,
#ifdef MESHLET_MESH_MATERIAL_PASS
ddx_uv: vec2<f32>,
ddy_uv: vec2<f32>,
#endif
) -> vec3<f32> {
// NOTE: The mikktspace method of normal mapping explicitly requires that the world normal NOT
// be re-normalized in the fragment shader. This is primarily to match the way mikktspace
@ -141,26 +166,15 @@ fn apply_normal_mapping(
// http://www.mikktspace.com/
var N: vec3<f32> = 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<f32> = world_tangent.xyz;
var B: vec3<f32> = 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<f32>(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<f32>(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<f32> = vec3<f32>(0.0);
// Transmitted Light (Specular and Diffuse)
var transmitted_light: vec3<f32> = vec3<f32>(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<f32>(
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<f32>(0.0) // doesn't really matter
// f_ab = vec2<f32>(0.1)
// F_ab = vec2<f32>(0.1)
// F0 = vec3<f32>(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<f32>(0.0), vec3<f32>(0.0), vec2<f32>(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<f32>(0.0) // doesn't really matter
// f_ab = vec2<f32>(0.1)
// F_ab = vec2<f32>(0.1)
// F0 = vec3<f32>(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<f32>(0.0), vec3<f32>(0.0), vec2<f32>(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<f32>(0.0) // doesn't really matter
// f_ab = vec2<f32>(0.1)
// F_ab = vec2<f32>(0.1)
// F0 = vec3<f32>(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<f32>(0.0), vec3<f32>(0.0), vec2<f32>(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<f32>(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<f32>(1.0),
1.0,
f_ab,
-in.N,
T,
vec3<f32>(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<f32>(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."
//
// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md#emission>
#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;

View file

@ -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<f32>,
// The reflected vector.
R: vec3<f32>,
// 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<LayerLightingInput, 2>,
#else // STANDARD_MATERIAL_CLEARCOAT
layers: array<LayerLightingInput, 1>,
#endif // STANDARD_MATERIAL_CLEARCOAT
// The world-space position.
P: vec3<f32>,
// The vector to the light.
V: vec3<f32>,
// The diffuse color of the material.
diffuse_color: vec3<f32>,
// 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<f32>,
// Constants for the BRDF approximation.
//
// See `EnvBRDFApprox` in
// <https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile>.
// What we call `F_ab` they call `AB`.
F_ab: vec2<f32>,
#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<f32>,
// 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>) -> 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>) -> 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>) -> f32 {
// where
// V(v,l,α) = 0.5 / { nl sqrt((nv)^2 (1α2) + α2) + nv sqrt((nl)^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 vh)^5
fn F_Schlick_vec(f0: vec3<f32>, f90: f32, VoH: f32) -> vec3<f32> {
fn F_Schlick_vec(f0: vec3<f32>, f90: f32, VdotH: f32) -> vec3<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 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<f32>, LoH: f32) -> vec3<f32> {
fn fresnel(f0: vec3<f32>, LdotH: f32) -> vec3<f32> {
// f_90 suitable for ambient occlusion
// see https://google.github.io/filament/Filament.html#lighting/occlusion
let f90 = saturate(dot(f0, vec3<f32>(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<f32>, V: vec3<f32>, L: vec3<f32>) -> DerivedLightingInput {
var input: DerivedLightingInput;
var H: vec3<f32> = 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<function, LightingInput>,
layer: u32,
V: vec3<f32>,
light_to_frag: vec3<f32>,
light_position_radius: f32,
) -> vec4<f32> {
// 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<f32> = 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 (nv) (nl) }
fn specular(
f0: vec3<f32>,
roughness: f32,
h: vec3<f32>,
NoV: f32,
NoL: f32,
NoH: f32,
LoH: f32,
specularIntensity: f32,
f_ab: vec2<f32>
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
specular_intensity: f32,
) -> vec3<f32> {
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:
// <https://google.github.io/filament/Filament.html#listing_energycompensationimpl>
var Fr = (specular_intensity * D * V) * F;
Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0);
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.
//
// <https://google.github.io/filament/Filament.html#listing_clearcoatbrdf>
fn specular_clearcoat(
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
clearcoat_strength: f32,
specular_intensity: f32,
) -> vec2<f32> {
// 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 / { |nv||nl| } Ω D(m,α) G(v,l,m) (vm) (lm) 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<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
) -> 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<f32>) -> vec3<f32> {
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<f32> {
fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2<f32> {
let c0 = vec4<f32>(-1.0, -0.0275, -0.572, 0.022);
let c1 = vec4<f32>(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<f32>(-1.04, 1.04) * a004 + r.zw;
}
fn EnvBRDFApprox(f0: vec3<f32>, f_ab: vec2<f32>) -> vec3<f32> {
return f0 * f_ab.x + f_ab.y;
fn EnvBRDFApprox(F0: vec3<f32>, F_ab: vec2<f32>) -> vec3<f32> {
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<f32>,
light_id: u32,
roughness: f32,
NdotV: f32,
N: vec3<f32>,
V: vec3<f32>,
R: vec3<f32>,
F0: vec3<f32>,
f_ab: vec2<f32>,
diffuseColor: vec3<f32>
) -> vec3<f32> {
fn point_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32> {
// 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<f32> = closestPoint * LspecLengthInverse; // normalize() equivalent?
var H: vec3<f32> = 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 NL is used in the lighting equation.
let L = normalize(light_to_frag);
var derived_input = derive_lighting_input(N, V, L);
let diffuse = diffuse_color * Fd_Burley(input, &derived_input);
// See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation
// Lout = f(v,l) Φ / { 4 π d^2 }nl
@ -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<f32>;
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Account for the Fresnel term from the clearcoat darkening the main layer.
//
// <https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
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<f32>,
light_id: u32,
roughness: f32,
NdotV: f32,
N: vec3<f32>,
V: vec3<f32>,
R: vec3<f32>,
F0: vec3<f32>,
f_ab: vec2<f32>,
diffuseColor: vec3<f32>
) -> vec3<f32> {
fn spot_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32> {
// 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<f32>, view: vec3<f32>, R: vec3<f32>, F0: vec3<f32>, f_ab: vec2<f32>, diffuseColor: vec3<f32>) -> vec3<f32> {
fn directional_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32> {
// 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<f32>;
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Account for the Fresnel term from the clearcoat darkening the main layer.
//
// <https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
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;
}

View file

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

View file

@ -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<f32>(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<f32>,
lightmap_light: vec3<f32>,
clearcoat_N: vec3<f32>,
is_orthographic: bool,
flags: u32,
};

View file

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

352
examples/3d/clearcoat.rs Normal file
View file

@ -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::<LightMode>()
.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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
light_mode: Res<LightMode>,
) {
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<Mesh>) -> Handle<Mesh> {
// 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<StandardMaterial>,
asset_server: &AssetServer,
sphere: &Handle<Mesh>,
) {
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<StandardMaterial>,
sphere: &Handle<Mesh>,
) {
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<StandardMaterial>,
asset_server: &AssetServer,
sphere: &Handle<Mesh>,
) {
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<PointLight>, With<DirectionalLight>)>>,
time: Res<Time>,
) {
let now = time.elapsed_seconds();
for mut transform in lights.iter_mut() {
transform.translation = vec3(
f32::sin(now * 1.4),
f32::cos(now * 1.0),
f32::cos(now * 0.6),
) * vec3(3.0, 4.0, 3.0);
transform.look_at(Vec3::ZERO, Vec3::Y);
}
}
/// Rotates the spheres.
fn animate_spheres(mut spheres: Query<&mut Transform, With<ExampleSphere>>, time: Res<Time>) {
let now = time.elapsed_seconds();
for mut transform in spheres.iter_mut() {
transform.rotation = Quat::from_rotation_y(SPHERE_ROTATION_SPEED * now);
}
}
/// Handles the user pressing Space to change the type of light from point to
/// directional and vice versa.
fn handle_input(
mut commands: Commands,
mut light_query: Query<Entity, Or<(With<PointLight>, With<DirectionalLight>)>>,
keyboard: Res<ButtonInput<KeyCode>>,
mut light_mode: ResMut<LightMode>,
) {
if !keyboard.just_pressed(KeyCode::Space) {
return;
}
for light in light_query.iter_mut() {
match *light_mode {
LightMode::Point => {
*light_mode = LightMode::Directional;
commands
.entity(light)
.remove::<PointLight>()
.insert(create_directional_light());
}
LightMode::Directional => {
*light_mode = LightMode::Point;
commands
.entity(light)
.remove::<DirectionalLight>()
.insert(create_point_light());
}
}
}
}
/// Updates the help text at the bottom of the screen.
fn update_help_text(
mut text_query: Query<&mut Text>,
light_mode: Res<LightMode>,
asset_server: Res<AssetServer>,
) {
for mut text in text_query.iter_mut() {
*text = light_mode.create_help_text(&asset_server);
}
}
/// Creates or recreates the moving point light.
fn create_point_light() -> PointLight {
PointLight {
color: WHITE.into(),
intensity: 100000.0,
..default()
}
}
/// Creates or recreates the moving directional light.
fn create_directional_light() -> DirectionalLight {
DirectionalLight {
color: WHITE.into(),
illuminance: 1000.0,
..default()
}
}
impl LightMode {
/// Creates the help text at the bottom of the screen.
fn create_help_text(&self, asset_server: &AssetServer) -> Text {
let help_text = match *self {
LightMode::Point => "Press Space to switch to a directional light",
LightMode::Directional => "Press Space to switch to a point light",
};
Text::from_section(
help_text,
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 24.0,
..default()
},
)
}
}

View file

@ -130,6 +130,7 @@ Example | Description
[Atmospheric Fog](../examples/3d/atmospheric_fog.rs) | A scene showcasing the atmospheric fog effect
[Auto Exposure](../examples/3d/auto_exposure.rs) | A scene showcasing auto exposure
[Blend Modes](../examples/3d/blend_modes.rs) | Showcases different blend modes
[Clearcoat](../examples/3d/clearcoat.rs) | Demonstrates the clearcoat PBR feature
[Color grading](../examples/3d/color_grading.rs) | Demonstrates color grading
[Deferred Rendering](../examples/3d/deferred_rendering.rs) | Renders meshes with both forward and deferred pipelines
[Fog](../examples/3d/fog.rs) | A scene showcasing the distance fog effect