Skip to content

Commit 9a77b13

Browse files
author
Christophe Dehais
committed
Use IOR to parameterize dielectrics specular reflectance
1 parent 9a35b6a commit 9a77b13

13 files changed

Lines changed: 154 additions & 99 deletions

File tree

crates/bevy_gltf/src/loader/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,9 +1500,7 @@ fn load_material(
15001500
anisotropy_channel: anisotropy.anisotropy_channel,
15011501
#[cfg(feature = "pbr_anisotropy_texture")]
15021502
anisotropy_texture: anisotropy.anisotropy_texture,
1503-
// From the `KHR_materials_specular` spec:
1504-
// <https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_specular#materials-with-reflectance-parameter>
1505-
reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5,
1503+
specular: specular.specular_factor.unwrap_or(1.0) as f32,
15061504
#[cfg(feature = "pbr_specular_textures")]
15071505
specular_channel: specular.specular_channel,
15081506
#[cfg(feature = "pbr_specular_textures")]

crates/bevy_gltf/src/material.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ pub struct GltfMaterial {
4343
/// Metallic and roughness maps, stored as a single texture.
4444
pub metallic_roughness_texture: Option<Handle<Image>>,
4545

46-
/// Specular intensity for non-metals on a linear scale of `[0.0, 1.0]`.
47-
pub reflectance: f32,
46+
/// Specular strength for non-metals on a linear scale of `[0.0, 1.0]`.
47+
pub specular: f32,
4848

4949
/// The UV channel to use for the [`GltfMaterial::specular_texture`].
5050
#[cfg(feature = "pbr_specular_textures")]
@@ -54,8 +54,8 @@ pub struct GltfMaterial {
5454
#[cfg(feature = "pbr_specular_textures")]
5555
pub specular_texture: Option<Handle<Image>>,
5656

57-
/// A color with which to modulate the [`GltfMaterial::reflectance`] for
58-
/// non-metals.
57+
/// A color with which to modulate the default reflectance at normal incidence (which is computed from [`GltfMaterial::ior`]).
58+
/// Only affects non-metals.
5959
pub specular_tint: Color,
6060

6161
/// The UV channel to use for the
@@ -206,10 +206,6 @@ impl Default for GltfMaterial {
206206
metallic: 0.0,
207207
metallic_roughness_channel: UvChannel::Uv0,
208208
metallic_roughness_texture: None,
209-
// Minimum real-world reflectance is 2%, most materials between 2-5%
210-
// Expressed in a linear scale and equivalent to 4% reflectance see
211-
// <https://google.github.io/filament/Material%20Properties.pdf>
212-
reflectance: 0.5,
213209
specular_transmission: 0.0,
214210
#[cfg(feature = "pbr_transmission_textures")]
215211
specular_transmission_channel: UvChannel::Uv0,
@@ -227,6 +223,7 @@ impl Default for GltfMaterial {
227223
occlusion_texture: None,
228224
normal_map_channel: UvChannel::Uv0,
229225
normal_map_texture: None,
226+
specular: 1.0,
230227
#[cfg(feature = "pbr_specular_textures")]
231228
specular_channel: UvChannel::Uv0,
232229
#[cfg(feature = "pbr_specular_textures")]

crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,23 @@ fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4<u32> {
3030
// https://en.wikipedia.org/wiki/Rec._709
3131
let rec_709_coeffs = vec3<f32>(0.2126, 0.7152, 0.0722);
3232
let diffuse_occlusion = dot(in.diffuse_occlusion, rec_709_coeffs);
33-
// Only monochrome specular supported.
34-
let reflectance = dot(in.material.reflectance, rec_709_coeffs);
3533
#ifdef WEBGL2 // More crunched for webgl so we can also fit depth.
36-
var props = deferred_types::pack_unorm3x4_plus_unorm_20_(vec4(
37-
reflectance,
34+
let ior_specular = deferred_types::pack_4bit_ior_specular(in.material.ior, in.material.specular_weight);
35+
let ior_specular_props = f32(ior_specular) / 15.0;
36+
var props = deferred_types::pack_unorm3x4_unorm_20_(vec4(
37+
ior_specular_props,
3838
in.material.metallic,
3939
diffuse_occlusion,
4040
in.frag_coord.z));
4141
#else
42+
let ior_specular = deferred_types::pack_8bit_ior_specular(in.material.ior, in.material.specular_weight);
43+
let ior_specular_props = f32(ior_specular) / 255.0;
4244
let clearcoat = u32(round(saturate(in.material.clearcoat) * 15.0));
4345
let clearcoat_perceptual_roughness =
4446
u32(round(clamp(in.material.clearcoat_perceptual_roughness, 0.0, 0.5) * 30.0));
4547
let clearcoat_props = f32(clearcoat | (clearcoat_perceptual_roughness << 4u)) / 255.0;
4648
var props = deferred_types::pack_unorm4x8_(vec4(
47-
reflectance, // could be fewer bits
49+
ior_specular_props,
4850
in.material.metallic, // could be fewer bits
4951
diffuse_occlusion, // is this worth including?
5052
clearcoat_props));
@@ -106,15 +108,18 @@ fn pbr_input_from_deferred_gbuffer(frag_coord: vec4<f32>, gbuffer: vec4<u32>) ->
106108
}
107109
#ifdef WEBGL2 // More crunched for webgl so we can also fit depth.
108110
let props = deferred_types::unpack_unorm3x4_plus_unorm_20_(gbuffer.b);
109-
// Bias to 0.5 since that's the value for almost all materials.
110-
pbr.material.reflectance = vec3(saturate(props.r - 0.03333333333));
111+
let ior_specular_props = u32(rounds(props.g * 15.0));
112+
let ior_specular = deferred_types::unpack_4bit_ior_specular(ior_specular_props);
111113
#else
112114
let props = deferred_types::unpack_unorm4x8_(gbuffer.b);
115+
let ior_specular_props = u32(round(props.r * 255.0));
116+
let ior_specular = deferred_types::unpack_8bit_ior_specular(ior_specular_props);
113117
let clearcoat_props = u32(round(props.a * 255.0));
114-
pbr.material.reflectance = vec3(props.r);
115118
pbr.material.clearcoat = f32(clearcoat_props & 0x0fu) / 15.0;
116119
pbr.material.clearcoat_perceptual_roughness = f32(clearcoat_props >> 4u) / 30.0;
117120
#endif // WEBGL2
121+
pbr.material.ior = ior_specular.x;
122+
pbr.material.specular_weight = ior_specular.y;
118123
pbr.material.metallic = props.g;
119124
pbr.diffuse_occlusion = vec3(props.b);
120125
let octahedral_normal = deferred_types::unpack_24bit_normal(gbuffer.a);

crates/bevy_pbr/src/deferred/pbr_deferred_types.wgsl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,45 @@ fn unpack_flags(packed: u32) -> u32 {
5252
return (packed >> 24u) & 0xFFu;
5353
}
5454

55+
56+
#ifdef WEBGL2
57+
// Pack ior and specular strength into 4 bits.
58+
// With so little space, specular is made binary, and all values except specular = 0.0 are mapped to 1
59+
// IOR values outside [1.33, 2.33] are clamped.
60+
fn pack_4bit_ior_specular(ior: f32, specular: f32) -> u32 {
61+
var ior_remapped = saturate(ior - 1.33);
62+
// give more range to lower values
63+
ior_remapped = sqrt(ior_remapped);
64+
let ior3 = u32(ior_remapped * 7.0 + 0.5);
65+
return (u32(specular > 0.0) << 3u) | ior3;
66+
}
67+
68+
fn unpack_4bit_ior_specular(v: u32) -> vec2<f32> {
69+
let specular = f32(v >> 3u);
70+
var ior = f32(v & 0x7u) / 7.0;
71+
ior = ior * ior + 1.33;
72+
return vec2<f32>(ior, specular);
73+
}
74+
#endif // WEBGL2
75+
76+
// Pack ior and specular strength into 8 bits. Allocation is IOR: 5 bits / specular: 3 bits
77+
// IOR values outside [1.0. 3.0] are clamped
78+
fn pack_8bit_ior_specular(ior: f32, specular: f32) -> u32 {
79+
var ior_remapped = saturate((ior - 1.0) / 2.0);
80+
// give more range to lower values
81+
ior_remapped = sqrt(ior_remapped);
82+
let ior5 = u32(ior_remapped * 31.0 + 0.5);
83+
let specular3 = u32(saturate(specular) * 7.0 + 0.5);
84+
return (specular3 << 5u) | ior5;
85+
}
86+
87+
fn unpack_8bit_ior_specular(v: u32) -> vec2<f32> {
88+
let specular = f32(v >> 5u) / 7.0;
89+
var ior = f32(v & 0x1Fu) / 31.0;
90+
ior = ior * ior * 2.0 + 1.0;
91+
return vec2<f32>(ior, specular);
92+
}
93+
5594
// The builtin one didn't work in webgl.
5695
// "'unpackUnorm4x8' : no matching overloaded function found"
5796
// https://github.com/gfx-rs/naga/issues/2006

crates/bevy_pbr/src/gltf.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub fn standard_material_from_gltf_material(material: &GltfMaterial) -> Standard
4242
metallic: material.metallic,
4343
metallic_roughness_channel: material.metallic_roughness_channel.clone(),
4444
metallic_roughness_texture: material.metallic_roughness_texture.clone(),
45-
reflectance: material.reflectance,
45+
specular: material.specular,
4646
specular_tint: material.specular_tint,
4747
specular_transmission: material.specular_transmission,
4848
#[cfg(feature = "pbr_transmission_textures")]

crates/bevy_pbr/src/light_probe/environment_map.wgsl

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,8 @@ fn compute_multiscatter(
329329
F0: vec3<f32>,
330330
F_ab: vec2<f32>,
331331
Ems: f32,
332-
specular_occlusion: f32,
333332
) -> MultiscatterResult {
334-
let FssEss = (F0 * F_ab.x + F_ab.y) * specular_occlusion;
333+
let FssEss = (F0 * F_ab.x + F_ab.y);
335334
let Favg = F0 + (1.0 - F0) / 21.0;
336335
let FmsEms = FssEss * Favg / (1.0 - Ems * Favg) * Ems;
337336
let Edss = 1.0 - (FssEss + FmsEms);
@@ -352,6 +351,7 @@ fn environment_map_light(
352351
let F_ab = (*input).F_ab;
353352
let F0_dielectric = (*input).F0_dielectric;
354353
let F0_metallic = (*input).F0_metallic;
354+
let specular_weight = (*input).specular_weight;
355355
let world_position = (*input).P;
356356

357357
var out: EnvironmentMapLight;
@@ -369,18 +369,14 @@ fn environment_map_light(
369369
return out;
370370
}
371371

372-
// No real world material has specular values under 0.02, so we use this range as a
373-
// "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control.
374-
// See: https://google.github.io/filament/Filament.md.html#specularocclusion
375372
let F0_surface = mix(F0_dielectric, F0_metallic, metallic);
376-
let specular_occlusion = saturate(dot(F0_surface, vec3(50.0 * 0.33)));
377373

378374
// Compute per-material (dielectric and metallic separately) then mix the results.
379375
// We can't use F0 directly as the multiscattering term is nonlinear.
380376
let Ems = 1.0 - (F_ab.x + F_ab.y);
381377

382-
let ms_dielectric = compute_multiscatter(F0_dielectric, F_ab, Ems, specular_occlusion);
383-
let ms_metallic = compute_multiscatter(F0_metallic, F_ab, Ems, specular_occlusion);
378+
let ms_dielectric = compute_multiscatter(F0_dielectric, F_ab, Ems);
379+
let ms_metallic = compute_multiscatter(F0_metallic, F_ab, Ems);
384380

385381
let FssEss = mix(ms_dielectric.FssEss, ms_metallic.FssEss, metallic);
386382
let FmsEms = mix(ms_dielectric.FmsEms, ms_metallic.FmsEms, metallic);
@@ -392,7 +388,7 @@ fn environment_map_light(
392388
out.diffuse = vec3(0.0);
393389
}
394390

395-
out.specular = FssEss * radiances.radiance;
391+
out.specular = specular_weight * FssEss * radiances.radiance;
396392

397393
#ifdef STANDARD_MATERIAL_CLEARCOAT
398394
environment_map_light_clearcoat(

crates/bevy_pbr/src/pbr_material.rs

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,22 @@ pub struct StandardMaterial {
169169
#[dependency]
170170
pub metallic_roughness_texture: Option<Handle<Image>>,
171171

172-
/// Specular intensity for non-metals on a linear scale of `[0.0, 1.0]`.
172+
/// Specular strength for non-metals on a linear scale of `[0.0, 1.0]`.
173173
///
174-
/// Use the value as a way to control the intensity of the
175-
/// specular highlight of the material, i.e. how reflective is the material,
176-
/// rather than the physical property "reflectance."
174+
/// For non metals the specular reflectance at normal incidence is governed by the [`StandardMaterial::`ior`] parameter.
177175
///
178-
/// Set to `0.0`, no specular highlight is visible, the highlight is strongest
179-
/// when `reflectance` is set to `1.0`.
176+
/// Use this value as a way to scale down this default reflectance, and thus the intensity of the
177+
/// specular highlight of the material, i.e. how reflective the material ultimately is.
180178
///
181-
/// Defaults to `0.5` which is mapped to 4% reflectance in the shader.
179+
/// Set to `0.0`, no specular highlight is visible. The highlight is strongest when `specular` is set to `1.0`.
180+
///
181+
/// Defaults to `1.0`, which will compute the normal incidence reflectance according to the material's IOR.
182182
#[doc(alias = "specular_intensity")]
183-
pub reflectance: f32,
183+
pub specular: f32,
184184

185-
/// A color with which to modulate the [`StandardMaterial::reflectance`] for
186-
/// non-metals.
185+
/// A color with which to modulate the specular reflectance for non-metals.
187186
///
188-
/// The specular highlights and reflection are tinted with this color. Note
189-
/// that it has no effect for non-metals.
187+
/// The specular highlights and reflection are tinted with this color.
190188
///
191189
/// This feature is currently unsupported in the deferred rendering path, in
192190
/// order to reduce the size of the geometry buffers.
@@ -444,19 +442,15 @@ pub struct StandardMaterial {
444442
#[cfg(feature = "pbr_specular_textures")]
445443
pub specular_channel: UvChannel,
446444

447-
/// A map that specifies reflectance for non-metallic materials.
445+
/// A map that adjusts the strength of the highlights and reflection for non-metallic materials.
448446
///
449-
/// Alpha values from [0.0, 1.0] in this texture are linearly mapped to
450-
/// reflectance values of [0.0, 0.5] and multiplied by the constant
451-
/// [`StandardMaterial::reflectance`] value. This follows the
452-
/// `KHR_materials_specular` specification. The map will have no effect if
447+
/// Alpha values from [0.0, 1.0] in this texture will be multiplied with the constant
448+
/// [`StandardMaterial::specular`] value, to obtain a strength factor that will linearly
449+
/// linearly scale the default specular reflectance of the material.
450+
/// This follows the `KHR_materials_specular` specification. The map will have no effect if
453451
/// the material is fully metallic.
454452
///
455-
/// When using this map, you may wish to set the
456-
/// [`StandardMaterial::reflectance`] value to 2.0 so that this map can
457-
/// express the full [0.0, 1.0] range of values.
458-
///
459-
/// Note that, because the reflectance is stored in the alpha channel, and
453+
/// Note that, because the specular strength is stored in the alpha channel, and
460454
/// the [`StandardMaterial::specular_tint_texture`] has no alpha value, it
461455
/// may be desirable to pack the values together and supply the same
462456
/// texture to both fields.
@@ -866,10 +860,6 @@ impl Default for StandardMaterial {
866860
metallic: 0.0,
867861
metallic_roughness_channel: UvChannel::Uv0,
868862
metallic_roughness_texture: None,
869-
// Minimum real-world reflectance is 2%, most materials between 2-5%
870-
// Expressed in a linear scale and equivalent to 4% reflectance see
871-
// <https://google.github.io/filament/Material%20Properties.pdf>
872-
reflectance: 0.5,
873863
diffuse_transmission: 0.0,
874864
#[cfg(feature = "pbr_transmission_textures")]
875865
diffuse_transmission_channel: UvChannel::Uv0,
@@ -892,6 +882,7 @@ impl Default for StandardMaterial {
892882
occlusion_texture: None,
893883
normal_map_channel: UvChannel::Uv0,
894884
normal_map_texture: None,
885+
specular: 1.0,
895886
#[cfg(feature = "pbr_specular_textures")]
896887
specular_channel: UvChannel::Uv0,
897888
#[cfg(feature = "pbr_specular_textures")]
@@ -1020,9 +1011,10 @@ pub struct StandardMaterialUniform {
10201011
pub attenuation_color: Vec4,
10211012
/// The transform applied to the UVs corresponding to `ATTRIBUTE_UV_0` on the mesh before sampling. Default is identity.
10221013
pub uv_transform: Mat3,
1023-
/// Specular intensity for non-metals on a linear scale of [0.0, 1.0]
1024-
/// defaults to 0.5 which is mapped to 4% reflectance in the shader
1025-
pub reflectance: Vec3,
1014+
/// Specular tint modulating non-metals specular reflectance.
1015+
pub specular_tint: Vec3,
1016+
/// Specular strength for non-metals on a linear scale of [0.0, 1.0]. Default is 1.0.
1017+
pub specular_weight: f32,
10261018
/// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader
10271019
/// Defaults to minimum of 0.089
10281020
pub roughness: f32,
@@ -1187,7 +1179,8 @@ impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
11871179
emissive,
11881180
roughness: self.perceptual_roughness,
11891181
metallic: self.metallic,
1190-
reflectance: LinearRgba::from(self.specular_tint).to_vec3() * self.reflectance,
1182+
specular_tint: LinearRgba::from(self.specular_tint).to_vec3(),
1183+
specular_weight: self.specular,
11911184
clearcoat: self.clearcoat,
11921185
clearcoat_perceptual_roughness: self.clearcoat_perceptual_roughness,
11931186
anisotropy_strength: self.anisotropy_strength,

crates/bevy_pbr/src/render/pbr_fragment.wgsl

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,15 @@ pbr_input.material.uv_transform = uv_transform;
257257
pbr_input.material.attenuation_distance = pbr_bindings::material.attenuation_distance;
258258
#endif // BINDLESS
259259

260-
// reflectance
260+
// Specular reflectance
261261
#ifdef BINDLESS
262-
pbr_input.material.reflectance =
263-
pbr_bindings::material_array[material_indices[slot].material].reflectance;
262+
pbr_input.material.specular_tint =
263+
pbr_bindings::material_array[material_indices[slot].material].specular_tint;
264+
pbr_input.material.specular_weight =
265+
pbr_bindings::material_array[material_indices[slot].material].specular_weight;
264266
#else // BINDLESS
265-
pbr_input.material.reflectance = pbr_bindings::material.reflectance;
267+
pbr_input.material.specular_tint = pbr_bindings::material.specular_tint;
268+
pbr_input.material.specular_weight = pbr_bindings::material.specular_weight;
266269
#endif // BINDLESS
267270

268271
#ifdef PBR_SPECULAR_TEXTURES_SUPPORTED
@@ -295,9 +298,7 @@ pbr_input.material.uv_transform = uv_transform;
295298
bias.mip_bias,
296299
#endif // MESHLET_MESH_MATERIAL_PASS
297300
).a;
298-
// This 0.5 factor is from the `KHR_materials_specular` specification:
299-
// <https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_specular#materials-with-reflectance-parameter>
300-
pbr_input.material.reflectance *= specular * 0.5;
301+
pbr_input.material.specular_weight *= specular;
301302
}
302303

303304
// Specular tint texture
@@ -327,7 +328,7 @@ pbr_input.material.uv_transform = uv_transform;
327328
bias.mip_bias,
328329
#endif // MESHLET_MESH_MATERIAL_PASS
329330
).rgb;
330-
pbr_input.material.reflectance *= specular_tint;
331+
pbr_input.material.specular_tint *= specular_tint;
331332
}
332333

333334
#endif // VERTEX_UVS

0 commit comments

Comments
 (0)