Skip to content

Commit dd4299b

Browse files
JMS55IceSentryjames7132rparrett
committed
EnvironmentMapLight, BRDF Improvements (#7051)
(Before) ![image](https://user-images.githubusercontent.com/47158642/213946111-15ec758f-1f1d-443c-b196-1fdcd4ae49da.png) (After) ![image](https://user-images.githubusercontent.com/47158642/217051179-67381e73-dd44-461b-a2c7-87b0440ef8de.png) ![image](https://user-images.githubusercontent.com/47158642/212492404-524e4ad3-7837-4ed4-8b20-2abc276aa8e8.png) # Objective - Improve lighting; especially reflections. - Closes #4581. ## Solution - Implement environment maps, providing better ambient light. - Add microfacet multibounce approximation for specular highlights from Filament. - Occlusion is no longer incorrectly applied to direct lighting. It now only applies to diffuse indirect light. Unsure if it's also supposed to apply to specular indirect light - the glTF specification just says "indirect light". In the case of ambient occlusion, for instance, that's usually only calculated as diffuse though. For now, I'm choosing to apply this just to indirect diffuse light, and not specular. - Modified the PBR example to use an environment map, and have labels. - Added `FallbackImageCubemap`. ## Implementation - IBL technique references can be found in environment_map.wgsl. - It's more accurate to use a LUT for the scale/bias. Filament has a good reference on generating this LUT. For now, I just used an analytic approximation. - For now, environment maps must first be prefiltered outside of bevy using a 3rd party tool. See the `EnvironmentMap` documentation. - Eventually, we should have our own prefiltering code, so that we can have dynamically changing environment maps, as well as let users drop in an HDR image and use asset preprocessing to create the needed textures using only bevy. --- ## Changelog - Added an `EnvironmentMapLight` camera component that adds additional ambient light to a scene. - StandardMaterials will now appear brighter and more saturated at high roughness, due to internal material changes. This is more physically correct. - Fixed StandardMaterial occlusion being incorrectly applied to direct lighting. - Added `FallbackImageCubemap`. Co-authored-by: IceSentry <[email protected]> Co-authored-by: James Liu <[email protected]> Co-authored-by: Rob Parrett <[email protected]>
1 parent 1ca8755 commit dd4299b

File tree

20 files changed

+522
-80
lines changed

20 files changed

+522
-80
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,14 @@ jobs:
190190
- name: Build bevy
191191
# this uses the same command as when running the example to ensure build is reused
192192
run: |
193-
TRACE_CHROME=trace-alien_cake_addict.json CI_TESTING_CONFIG=.github/example-run/alien_cake_addict.ron cargo build --example alien_cake_addict --features "bevy_ci_testing,trace,trace_chrome"
193+
TRACE_CHROME=trace-alien_cake_addict.json CI_TESTING_CONFIG=.github/example-run/alien_cake_addict.ron cargo build --example alien_cake_addict --features "bevy_ci_testing,trace,trace_chrome,ktx2,zstd"
194194
- name: Run examples
195195
run: |
196196
for example in .github/example-run/*.ron; do
197197
example_name=`basename $example .ron`
198198
echo -n $example_name > last_example_run
199199
echo "running $example_name - "`date`
200-
time TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example xvfb-run cargo run --example $example_name --features "bevy_ci_testing,trace,trace_chrome"
200+
time TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example xvfb-run cargo run --example $example_name --features "bevy_ci_testing,trace,trace_chrome,ktx2,zstd"
201201
sleep 10
202202
done
203203
zip traces.zip trace*.json

.github/workflows/validation-jobs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,15 @@ jobs:
8080
shell: bash
8181
# this uses the same command as when running the example to ensure build is reused
8282
run: |
83-
WGPU_BACKEND=dx12 CI_TESTING_CONFIG=.github/example-run/alien_cake_addict.ron cargo build --example alien_cake_addict --features "bevy_ci_testing"
83+
WGPU_BACKEND=dx12 CI_TESTING_CONFIG=.github/example-run/alien_cake_addict.ron cargo build --example alien_cake_addict --features "bevy_ci_testing,ktx2,zstd"
8484
8585
- name: Run examples
8686
shell: bash
8787
run: |
8888
for example in .github/example-run/*.ron; do
8989
example_name=`basename $example .ron`
9090
echo "running $example_name - "`date`
91-
time WGPU_BACKEND=dx12 CI_TESTING_CONFIG=$example cargo run --example $example_name --features "bevy_ci_testing"
91+
time WGPU_BACKEND=dx12 CI_TESTING_CONFIG=$example cargo run --example $example_name --features "bevy_ci_testing,ktx2,zstd"
9292
sleep 10
9393
done
9494

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ wasm = false
372372
[[example]]
373373
name = "load_gltf"
374374
path = "examples/3d/load_gltf.rs"
375+
required-features = ["ktx2", "zstd"]
375376

376377
[package.metadata.example.load_gltf]
377378
name = "Load glTF"
@@ -422,6 +423,7 @@ wasm = true
422423
[[example]]
423424
name = "pbr"
424425
path = "examples/3d/pbr.rs"
426+
required-features = ["ktx2", "zstd"]
425427

426428
[package.metadata.example.pbr]
427429
name = "Physically Based Rendering"
@@ -1430,6 +1432,7 @@ wasm = true
14301432
[[example]]
14311433
name = "scene_viewer"
14321434
path = "examples/tools/scene_viewer/main.rs"
1435+
required-features = ["ktx2", "zstd"]
14331436

14341437
[package.metadata.example.scene_viewer]
14351438
name = "Scene Viewer"

assets/environment_maps/info.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
The pisa_*.ktx2 files were generated from https://github.com/KhronosGroup/glTF-Sample-Environments/blob/master/pisa.hdr using the following tools and commands:
2+
- IBL environment map prefiltering to cubemaps: https://github.com/KhronosGroup/glTF-IBL-Sampler
3+
- Diffuse: ./cli -inputPath pisa.hdr -outCubeMap pisa_diffuse.ktx2 -distribution Lambertian -cubeMapResolution 32
4+
- Specular: ./cli -inputPath pisa.hdr -outCubeMap pisa_specular.ktx2 -distribution GGX -cubeMapResolution 512
5+
- Converting to rgb9e5 format with zstd 'supercompression': https://github.com/DGriffin91/bevy_mod_environment_map_tools
6+
- cargo run --release -- --inputs pisa_diffuse.ktx2,pisa_specular.ktx2 --outputs pisa_diffuse_rgb9e5_zstd.ktx2,pisa_specular_rgb9e5_zstd.ktx2
Binary file not shown.
Binary file not shown.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#define_import_path bevy_pbr::environment_map
2+
3+
4+
struct EnvironmentMapLight {
5+
diffuse: vec3<f32>,
6+
specular: vec3<f32>,
7+
};
8+
9+
fn environment_map_light(
10+
perceptual_roughness: f32,
11+
roughness: f32,
12+
diffuse_color: vec3<f32>,
13+
NdotV: f32,
14+
f_ab: vec2<f32>,
15+
N: vec3<f32>,
16+
R: vec3<f32>,
17+
F0: vec3<f32>,
18+
) -> EnvironmentMapLight {
19+
20+
// Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
21+
let smallest_specular_mip_level = textureNumLevels(environment_map_specular) - 1i;
22+
let radiance_level = perceptual_roughness * f32(smallest_specular_mip_level);
23+
let irradiance = textureSample(environment_map_diffuse, environment_map_sampler, N).rgb;
24+
let radiance = textureSampleLevel(environment_map_specular, environment_map_sampler, R, radiance_level).rgb;
25+
26+
// Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf
27+
// Useful reference: https://bruop.github.io/ibl
28+
let Fr = max(vec3(1.0 - roughness), F0) - F0;
29+
let kS = F0 + Fr * pow(1.0 - NdotV, 5.0);
30+
let FssEss = kS * f_ab.x + f_ab.y;
31+
let Ess = f_ab.x + f_ab.y;
32+
let Ems = 1.0 - Ess;
33+
let Favg = F0 + (1.0 - F0) / 21.0;
34+
let Fms = FssEss * Favg / (1.0 - Ems * Favg);
35+
let FmsEms = Fms * Ems;
36+
let Edss = 1.0 - (FssEss + FmsEms);
37+
let kD = diffuse_color * Edss;
38+
39+
var out: EnvironmentMapLight;
40+
out.diffuse = (FmsEms + kD) * irradiance;
41+
out.specular = FssEss * radiance;
42+
return out;
43+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use bevy_app::{App, Plugin};
2+
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
3+
use bevy_core_pipeline::prelude::Camera3d;
4+
use bevy_ecs::{prelude::Component, query::With};
5+
use bevy_reflect::{Reflect, TypeUuid};
6+
use bevy_render::{
7+
extract_component::{ExtractComponent, ExtractComponentPlugin},
8+
render_asset::RenderAssets,
9+
render_resource::{
10+
BindGroupEntry, BindGroupLayoutEntry, BindingResource, BindingType, SamplerBindingType,
11+
Shader, ShaderStages, TextureSampleType, TextureViewDimension,
12+
},
13+
texture::{FallbackImageCubemap, Image},
14+
};
15+
16+
pub const ENVIRONMENT_MAP_SHADER_HANDLE: HandleUntyped =
17+
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 154476556247605696);
18+
19+
pub struct EnvironmentMapPlugin;
20+
21+
impl Plugin for EnvironmentMapPlugin {
22+
fn build(&self, app: &mut App) {
23+
load_internal_asset!(
24+
app,
25+
ENVIRONMENT_MAP_SHADER_HANDLE,
26+
"environment_map.wgsl",
27+
Shader::from_wgsl
28+
);
29+
30+
app.register_type::<EnvironmentMapLight>()
31+
.add_plugin(ExtractComponentPlugin::<EnvironmentMapLight>::default());
32+
}
33+
}
34+
35+
/// Environment map based ambient lighting representing light from distant scenery.
36+
///
37+
/// When added to a 3D camera, this component adds indirect light
38+
/// to every point of the scene (including inside, enclosed areas) based on
39+
/// an environment cubemap texture. This is similiar to [`crate::AmbientLight`], but
40+
/// higher quality, and is intended for outdoor scenes.
41+
///
42+
/// The environment map must be prefiltered into a diffuse and specular cubemap based on the
43+
/// [split-sum approximation](https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf).
44+
///
45+
/// To prefilter your environment map, you can use `KhronosGroup`'s [glTF-IBL-Sampler](https://github.com/KhronosGroup/glTF-IBL-Sampler).
46+
/// The diffuse map uses the Lambertian distribution, and the specular map uses the GGX distribution.
47+
///
48+
/// `KhronosGroup` also has several prefiltered environment maps that can be found [here](https://github.com/KhronosGroup/glTF-Sample-Environments).
49+
#[derive(Component, Reflect, Clone)]
50+
pub struct EnvironmentMapLight {
51+
pub diffuse_map: Handle<Image>,
52+
pub specular_map: Handle<Image>,
53+
}
54+
55+
impl EnvironmentMapLight {
56+
/// Whether or not all textures neccesary to use the environment map
57+
/// have been loaded by the asset server.
58+
pub fn is_loaded(&self, images: &RenderAssets<Image>) -> bool {
59+
images.get(&self.diffuse_map).is_some() && images.get(&self.specular_map).is_some()
60+
}
61+
}
62+
63+
impl ExtractComponent for EnvironmentMapLight {
64+
type Query = &'static Self;
65+
type Filter = With<Camera3d>;
66+
type Out = Self;
67+
68+
fn extract_component(item: bevy_ecs::query::QueryItem<'_, Self::Query>) -> Option<Self::Out> {
69+
Some(item.clone())
70+
}
71+
}
72+
73+
pub fn get_bindings<'a>(
74+
environment_map_light: Option<&EnvironmentMapLight>,
75+
images: &'a RenderAssets<Image>,
76+
fallback_image_cubemap: &'a FallbackImageCubemap,
77+
bindings: [u32; 3],
78+
) -> [BindGroupEntry<'a>; 3] {
79+
let (diffuse_map, specular_map) = match (
80+
environment_map_light.and_then(|env_map| images.get(&env_map.diffuse_map)),
81+
environment_map_light.and_then(|env_map| images.get(&env_map.specular_map)),
82+
) {
83+
(Some(diffuse_map), Some(specular_map)) => {
84+
(&diffuse_map.texture_view, &specular_map.texture_view)
85+
}
86+
_ => (
87+
&fallback_image_cubemap.texture_view,
88+
&fallback_image_cubemap.texture_view,
89+
),
90+
};
91+
92+
[
93+
BindGroupEntry {
94+
binding: bindings[0],
95+
resource: BindingResource::TextureView(diffuse_map),
96+
},
97+
BindGroupEntry {
98+
binding: bindings[1],
99+
resource: BindingResource::TextureView(specular_map),
100+
},
101+
BindGroupEntry {
102+
binding: bindings[2],
103+
resource: BindingResource::Sampler(&fallback_image_cubemap.sampler),
104+
},
105+
]
106+
}
107+
108+
pub fn get_bind_group_layout_entries(bindings: [u32; 3]) -> [BindGroupLayoutEntry; 3] {
109+
[
110+
BindGroupLayoutEntry {
111+
binding: bindings[0],
112+
visibility: ShaderStages::FRAGMENT,
113+
ty: BindingType::Texture {
114+
sample_type: TextureSampleType::Float { filterable: true },
115+
view_dimension: TextureViewDimension::Cube,
116+
multisampled: false,
117+
},
118+
count: None,
119+
},
120+
BindGroupLayoutEntry {
121+
binding: bindings[1],
122+
visibility: ShaderStages::FRAGMENT,
123+
ty: BindingType::Texture {
124+
sample_type: TextureSampleType::Float { filterable: true },
125+
view_dimension: TextureViewDimension::Cube,
126+
multisampled: false,
127+
},
128+
count: None,
129+
},
130+
BindGroupLayoutEntry {
131+
binding: bindings[2],
132+
visibility: ShaderStages::FRAGMENT,
133+
ty: BindingType::Sampler(SamplerBindingType::Filtering),
134+
count: None,
135+
},
136+
]
137+
}

crates/bevy_pbr/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod wireframe;
22

33
mod alpha;
44
mod bundle;
5+
mod environment_map;
56
mod fog;
67
mod light;
78
mod material;
@@ -10,8 +11,8 @@ mod prepass;
1011
mod render;
1112

1213
pub use alpha::*;
13-
use bevy_transform::TransformSystem;
1414
pub use bundle::*;
15+
pub use environment_map::EnvironmentMapLight;
1516
pub use fog::*;
1617
pub use light::*;
1718
pub use material::*;
@@ -27,6 +28,7 @@ pub mod prelude {
2728
DirectionalLightBundle, MaterialMeshBundle, PbrBundle, PointLightBundle,
2829
SpotLightBundle,
2930
},
31+
environment_map::EnvironmentMapLight,
3032
fog::{FogFalloff, FogSettings},
3133
light::{AmbientLight, DirectionalLight, PointLight, SpotLight},
3234
material::{Material, MaterialPlugin},
@@ -55,6 +57,8 @@ use bevy_render::{
5557
view::{ViewSet, VisibilitySystems},
5658
ExtractSchedule, RenderApp, RenderSet,
5759
};
60+
use bevy_transform::TransformSystem;
61+
use environment_map::EnvironmentMapPlugin;
5862

5963
pub const PBR_TYPES_SHADER_HANDLE: HandleUntyped =
6064
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 1708015359337029744);
@@ -172,6 +176,7 @@ impl Plugin for PbrPlugin {
172176
prepass_enabled: self.prepass_enabled,
173177
..Default::default()
174178
})
179+
.add_plugin(EnvironmentMapPlugin)
175180
.init_resource::<AmbientLight>()
176181
.init_resource::<GlobalVisiblePointLights>()
177182
.init_resource::<DirectionalLightShadowMap>()

crates/bevy_pbr/src/material.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
2-
AlphaMode, DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, PrepassPlugin,
3-
SetMeshBindGroup, SetMeshViewBindGroup,
2+
AlphaMode, DrawMesh, EnvironmentMapLight, MeshPipeline, MeshPipelineKey, MeshUniform,
3+
PrepassPlugin, SetMeshBindGroup, SetMeshViewBindGroup,
44
};
55
use bevy_app::{App, Plugin};
66
use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle};
@@ -361,10 +361,12 @@ pub fn queue_material_meshes<M: Material>(
361361
render_meshes: Res<RenderAssets<Mesh>>,
362362
render_materials: Res<RenderMaterials<M>>,
363363
material_meshes: Query<(&Handle<M>, &Handle<Mesh>, &MeshUniform)>,
364+
images: Res<RenderAssets<Image>>,
364365
mut views: Query<(
365366
&ExtractedView,
366367
&VisibleEntities,
367368
Option<&Tonemapping>,
369+
Option<&EnvironmentMapLight>,
368370
&mut RenderPhase<Opaque3d>,
369371
&mut RenderPhase<AlphaMask3d>,
370372
&mut RenderPhase<Transparent3d>,
@@ -376,6 +378,7 @@ pub fn queue_material_meshes<M: Material>(
376378
view,
377379
visible_entities,
378380
tonemapping,
381+
environment_map,
379382
mut opaque_phase,
380383
mut alpha_mask_phase,
381384
mut transparent_phase,
@@ -388,6 +391,14 @@ pub fn queue_material_meshes<M: Material>(
388391
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
389392
| MeshPipelineKey::from_hdr(view.hdr);
390393

394+
let environment_map_loaded = match environment_map {
395+
Some(environment_map) => environment_map.is_loaded(&images),
396+
None => false,
397+
};
398+
if environment_map_loaded {
399+
view_key |= MeshPipelineKey::ENVIRONMENT_MAP;
400+
}
401+
391402
if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping {
392403
if !view.hdr {
393404
view_key |= MeshPipelineKey::TONEMAP_IN_SHADER;
@@ -397,8 +408,8 @@ pub fn queue_material_meshes<M: Material>(
397408
}
398409
}
399410
}
400-
let rangefinder = view.rangefinder3d();
401411

412+
let rangefinder = view.rangefinder3d();
402413
for visible_entity in &visible_entities.entities {
403414
if let Ok((material_handle, mesh_handle, mesh_uniform)) =
404415
material_meshes.get(*visible_entity)

0 commit comments

Comments
 (0)