Introduce support for mixed lighting by allowing lights to opt out of contributing diffuse light to lightmapped objects. (#16761)
This PR adds support for *mixed lighting* to Bevy, whereby some parts of the scene are lightmapped, while others take part in real-time lighting. (Here *real-time lighting* means lighting at runtime via the PBR shader, as opposed to precomputed light using lightmaps.) It does so by adding a new field, `affects_lightmapped_meshes` to `IrradianceVolume` and `AmbientLight`, and a corresponding field `affects_lightmapped_mesh_diffuse` to `DirectionalLight`, `PointLight`, `SpotLight`, and `EnvironmentMapLight`. By default, this value is set to true; when set to false, the light contributes nothing to the diffuse irradiance component to meshes with lightmaps. Note that specular light is unaffected. This is because the correct way to bake specular lighting is *directional lightmaps*, which we have no support for yet. There are two general ways I expect this field to be used: 1. When diffuse indirect light is baked into lightmaps, irradiance volumes and reflection probes shouldn't contribute any diffuse light to the static geometry that has a lightmap. That's because the baking tool should have already accounted for it, and in a higher-quality fashion, as lightmaps typically offer a higher effective texture resolution than the light probe does. 2. When direct diffuse light is baked into a lightmap, punctual lights shouldn't contribute any diffuse light to static geometry with a lightmap, to avoid double-counting. It may seem odd to bake *direct* light into a lightmap, as opposed to indirect light. But there is a use case: in a scene with many lights, avoiding light leaks requires shadow mapping, which quickly becomes prohibitive when many lights are involved. Baking lightmaps allows light leaks to be eliminated on static geometry. A new example, `mixed_lighting`, has been added. It demonstrates a sofa (model from the [glTF Sample Assets]) that has been lightmapped offline using [Bakery]. It has four modes: 1. In *baked* mode, all objects are locked in place, and all the diffuse direct and indirect light has been calculated ahead of time. Note that the bottom of the sphere has a red tint from the sofa, illustrating that the baking tool captured indirect light for it. 2. In *mixed direct* mode, lightmaps capturing diffuse direct and indirect light have been pre-calculated for the static objects, but the dynamic sphere has real-time lighting. Note that, because the diffuse lighting has been entirely pre-calculated for the scenery, the dynamic sphere casts no shadow. In a real app, you would typically use real-time lighting for the most important light so that dynamic objects can shadow the scenery and relegate baked lighting to the less important lights for which shadows aren't as important. Also note that there is no red tint on the sphere, because there is no global illumination applied to it. In an actual game, you could fix this problem by supplementing the lightmapped objects with an irradiance volume. 3. In *mixed indirect* mode, all direct light is calculated in real-time, and the static objects have pre-calculated indirect lighting. This corresponds to the mode that most applications are expected to use. Because direct light on the scenery is computed dynamically, shadows are fully supported. As in mixed direct mode, there is no global illumination on the sphere; in a real application, irradiance volumes could be used to supplement the lightmaps. 4. In *real-time* mode, no lightmaps are used at all, and all punctual lights are rendered in real-time. No global illumination exists. In the example, you can click around to move the sphere, unless you're in baked mode, in which case the sphere must be locked in place to be lit correctly. ## Showcase Baked mode:  Mixed direct mode:  Mixed indirect mode (default):  Real-time mode:  ## Migration guide * The `AmbientLight` resource, the `IrradianceVolume` component, and the `EnvironmentMapLight` component now have `affects_lightmapped_meshes` fields. If you don't need to use that field (for example, if you aren't using lightmaps), you can safely set the field to true. * `DirectionalLight`, `PointLight`, and `SpotLight` now have `affects_lightmapped_mesh_diffuse` fields. If you don't need to use that field (for example, if you aren't using lightmaps), you can safely set the field to true. [glTF Sample Assets]: https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main [Bakery]: https://geom.io/bakery/wiki/index.php?title=Bakery_-_GPU_Lightmapper
12
Cargo.toml
@ -3849,6 +3849,18 @@ description = "Demonstrates percentage-closer soft shadows (PCSS)"
|
||||
category = "3D Rendering"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "mixed_lighting"
|
||||
path = "examples/3d/mixed_lighting.rs"
|
||||
doc-scrape-examples = true
|
||||
required-features = ["jpeg"]
|
||||
|
||||
[package.metadata.example.mixed_lighting]
|
||||
name = "Mixed lighting"
|
||||
description = "Demonstrates how to combine baked and dynamic lighting"
|
||||
category = "3D Rendering"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "animated_ui"
|
||||
path = "examples/animation/animated_ui.rs"
|
||||
|
BIN
assets/lightmaps/MixedLightingExample-Baked.zstd.ktx2
Normal file
BIN
assets/lightmaps/MixedLightingExample-MixedDirect.zstd.ktx2
Normal file
BIN
assets/lightmaps/MixedLightingExample-MixedIndirect.zstd.ktx2
Normal file
BIN
assets/models/MixedLightingExample/MixedLightingExample.bin
Normal file
1012
assets/models/MixedLightingExample/MixedLightingExample.gltf
Normal file
BIN
assets/models/MixedLightingExample/SheenChairBaseColor.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/models/MixedLightingExample/SheenChairFabricNormal.jpg
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
assets/models/MixedLightingExample/SheenChairFabricOcclusion.jpg
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
assets/models/MixedLightingExample/SheenChairLabelBaseColor.jpg
Normal file
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 43 KiB |
BIN
assets/models/MixedLightingExample/SheenChairOcclusion.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
assets/models/MixedLightingExample/SheenChairWoodBaseColor.jpg
Normal file
After Width: | Height: | Size: 30 KiB |
@ -19,12 +19,21 @@ use super::*;
|
||||
#[reflect(Resource, Debug, Default)]
|
||||
pub struct AmbientLight {
|
||||
pub color: Color,
|
||||
|
||||
/// A direct scale factor multiplied with `color` before being passed to the shader.
|
||||
///
|
||||
/// After applying this multiplier, the resulting value should be in units of [cd/m^2].
|
||||
///
|
||||
/// [cd/m^2]: https://en.wikipedia.org/wiki/Candela_per_square_metre
|
||||
pub brightness: f32,
|
||||
|
||||
/// Whether this ambient light has an effect on meshes with lightmaps.
|
||||
///
|
||||
/// Set this to false if your lightmap baking tool bakes the ambient light
|
||||
/// into the lightmaps, to avoid rendering that light twice.
|
||||
///
|
||||
/// By default, this is set to true.
|
||||
pub affects_lightmapped_meshes: bool,
|
||||
}
|
||||
|
||||
impl Default for AmbientLight {
|
||||
@ -32,6 +41,7 @@ impl Default for AmbientLight {
|
||||
Self {
|
||||
color: Color::WHITE,
|
||||
brightness: 80.0,
|
||||
affects_lightmapped_meshes: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,5 +49,6 @@ impl AmbientLight {
|
||||
pub const NONE: AmbientLight = AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 0.0,
|
||||
affects_lightmapped_meshes: true,
|
||||
};
|
||||
}
|
||||
|
@ -98,6 +98,18 @@ pub struct DirectionalLight {
|
||||
#[cfg(feature = "experimental_pbr_pcss")]
|
||||
pub soft_shadow_size: Option<f32>,
|
||||
|
||||
/// Whether this directional light contributes diffuse lighting to meshes
|
||||
/// with lightmaps.
|
||||
///
|
||||
/// Set this to false if your lightmap baking tool bakes the direct diffuse
|
||||
/// light from this directional light into the lightmaps in order to avoid
|
||||
/// counting the radiance from this light twice. Note that the specular
|
||||
/// portion of the light is always considered, because Bevy currently has no
|
||||
/// means to bake specular light.
|
||||
///
|
||||
/// By default, this is set to true.
|
||||
pub affects_lightmapped_mesh_diffuse: bool,
|
||||
|
||||
/// A value that adjusts the tradeoff between self-shadowing artifacts and
|
||||
/// proximity of shadows to their casters.
|
||||
///
|
||||
@ -123,6 +135,7 @@ impl Default for DirectionalLight {
|
||||
shadows_enabled: false,
|
||||
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
|
||||
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
|
||||
affects_lightmapped_mesh_diffuse: true,
|
||||
#[cfg(feature = "experimental_pbr_pcss")]
|
||||
soft_shadow_size: None,
|
||||
}
|
||||
|
@ -64,6 +64,18 @@ pub struct PointLight {
|
||||
#[cfg(feature = "experimental_pbr_pcss")]
|
||||
pub soft_shadows_enabled: bool,
|
||||
|
||||
/// Whether this point light contributes diffuse lighting to meshes with
|
||||
/// lightmaps.
|
||||
///
|
||||
/// Set this to false if your lightmap baking tool bakes the direct diffuse
|
||||
/// light from this point light into the lightmaps in order to avoid
|
||||
/// counting the radiance from this light twice. Note that the specular
|
||||
/// portion of the light is always considered, because Bevy currently has no
|
||||
/// means to bake specular light.
|
||||
///
|
||||
/// By default, this is set to true.
|
||||
pub affects_lightmapped_mesh_diffuse: bool,
|
||||
|
||||
/// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions
|
||||
/// that happen as a result of shadow-map fragments not mapping 1:1 to screen-space fragments.
|
||||
/// Too high of a depth bias can lead to shadows detaching from their casters, or
|
||||
@ -96,6 +108,7 @@ impl Default for PointLight {
|
||||
range: 20.0,
|
||||
radius: 0.0,
|
||||
shadows_enabled: false,
|
||||
affects_lightmapped_mesh_diffuse: true,
|
||||
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
|
||||
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
|
||||
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
|
||||
|
@ -60,6 +60,18 @@ pub struct SpotLight {
|
||||
#[cfg(feature = "experimental_pbr_pcss")]
|
||||
pub soft_shadows_enabled: bool,
|
||||
|
||||
/// Whether this spot light contributes diffuse lighting to meshes with
|
||||
/// lightmaps.
|
||||
///
|
||||
/// Set this to false if your lightmap baking tool bakes the direct diffuse
|
||||
/// light from this directional light into the lightmaps in order to avoid
|
||||
/// counting the radiance from this light twice. Note that the specular
|
||||
/// portion of the light is always considered, because Bevy currently has no
|
||||
/// means to bake specular light.
|
||||
///
|
||||
/// By default, this is set to true.
|
||||
pub affects_lightmapped_mesh_diffuse: bool,
|
||||
|
||||
/// A value that adjusts the tradeoff between self-shadowing artifacts and
|
||||
/// proximity of shadows to their casters.
|
||||
///
|
||||
@ -116,6 +128,7 @@ impl Default for SpotLight {
|
||||
range: 20.0,
|
||||
radius: 0.0,
|
||||
shadows_enabled: false,
|
||||
affects_lightmapped_mesh_diffuse: true,
|
||||
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
|
||||
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
|
||||
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
|
||||
|
@ -106,6 +106,16 @@ pub struct EnvironmentMapLight {
|
||||
/// This is useful for users who require a different axis, such as the Z-axis, to serve
|
||||
/// as the vertical axis.
|
||||
pub rotation: Quat,
|
||||
|
||||
/// Whether the light from this environment map contributes diffuse lighting
|
||||
/// to meshes with lightmaps.
|
||||
///
|
||||
/// Set this to false if your lightmap baking tool bakes the diffuse light
|
||||
/// from this environment light into the lightmaps in order to avoid
|
||||
/// counting the radiance from this environment map twice.
|
||||
///
|
||||
/// By default, this is set to true.
|
||||
pub affects_lightmapped_mesh_diffuse: bool,
|
||||
}
|
||||
|
||||
impl Default for EnvironmentMapLight {
|
||||
@ -115,6 +125,7 @@ impl Default for EnvironmentMapLight {
|
||||
specular_map: Handle::default(),
|
||||
intensity: 0.0,
|
||||
rotation: Quat::IDENTITY,
|
||||
affects_lightmapped_mesh_diffuse: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -199,6 +210,9 @@ pub struct EnvironmentMapViewLightProbeInfo {
|
||||
/// The scale factor applied to the diffuse and specular light in the
|
||||
/// cubemap. This is in units of cd/m² (candela per square meter).
|
||||
pub(crate) intensity: f32,
|
||||
/// Whether this lightmap affects the diffuse lighting of lightmapped
|
||||
/// meshes.
|
||||
pub(crate) affects_lightmapped_mesh_diffuse: bool,
|
||||
}
|
||||
|
||||
impl ExtractInstance for EnvironmentMapIds {
|
||||
@ -326,6 +340,10 @@ impl LightProbeComponent for EnvironmentMapLight {
|
||||
self.intensity
|
||||
}
|
||||
|
||||
fn affects_lightmapped_mesh_diffuse(&self) -> bool {
|
||||
self.affects_lightmapped_mesh_diffuse
|
||||
}
|
||||
|
||||
fn create_render_view_light_probes(
|
||||
view_component: Option<&EnvironmentMapLight>,
|
||||
image_assets: &RenderAssets<GpuImage>,
|
||||
@ -338,6 +356,7 @@ impl LightProbeComponent for EnvironmentMapLight {
|
||||
diffuse_map: diffuse_map_handle,
|
||||
specular_map: specular_map_handle,
|
||||
intensity,
|
||||
affects_lightmapped_mesh_diffuse,
|
||||
..
|
||||
}) = view_component
|
||||
{
|
||||
@ -354,6 +373,7 @@ impl LightProbeComponent for EnvironmentMapLight {
|
||||
) as i32,
|
||||
smallest_specular_mip_level: specular_map.mip_level_count - 1,
|
||||
intensity: *intensity,
|
||||
affects_lightmapped_mesh_diffuse: *affects_lightmapped_mesh_diffuse,
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -368,6 +388,7 @@ impl Default for EnvironmentMapViewLightProbeInfo {
|
||||
cubemap_index: -1,
|
||||
smallest_specular_mip_level: 0,
|
||||
intensity: 1.0,
|
||||
affects_lightmapped_mesh_diffuse: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,8 @@ fn compute_radiances(
|
||||
if (query_result.texture_index < 0) {
|
||||
query_result.texture_index = light_probes.view_cubemap_index;
|
||||
query_result.intensity = light_probes.intensity_for_view;
|
||||
query_result.affects_lightmapped_mesh_diffuse =
|
||||
light_probes.view_environment_map_affects_lightmapped_mesh_diffuse != 0u;
|
||||
}
|
||||
|
||||
// If there's no cubemap, bail out.
|
||||
@ -62,7 +64,14 @@ fn compute_radiances(
|
||||
let radiance_level = perceptual_roughness * f32(textureNumLevels(
|
||||
bindings::specular_environment_maps[query_result.texture_index]) - 1u);
|
||||
|
||||
if (!found_diffuse_indirect) {
|
||||
// If we're lightmapped, and we shouldn't accumulate diffuse light from the
|
||||
// environment map, note that.
|
||||
var enable_diffuse = !found_diffuse_indirect;
|
||||
#ifdef LIGHTMAP
|
||||
enable_diffuse = enable_diffuse && query_result.affects_lightmapped_mesh_diffuse;
|
||||
#endif // LIGHTMAP
|
||||
|
||||
if (enable_diffuse) {
|
||||
var irradiance_sample_dir = N;
|
||||
// Rotating the world space ray direction by the environment light map transform matrix, it is
|
||||
// equivalent to rotating the diffuse environment cubemap itself.
|
||||
@ -121,7 +130,15 @@ fn compute_radiances(
|
||||
|
||||
let intensity = light_probes.intensity_for_view;
|
||||
|
||||
if (!found_diffuse_indirect) {
|
||||
// If we're lightmapped, and we shouldn't accumulate diffuse light from the
|
||||
// environment map, note that.
|
||||
var enable_diffuse = !found_diffuse_indirect;
|
||||
#ifdef LIGHTMAP
|
||||
enable_diffuse = enable_diffuse &&
|
||||
light_probes.view_environment_map_affects_lightmapped_mesh_diffuse;
|
||||
#endif // LIGHTMAP
|
||||
|
||||
if (enable_diffuse) {
|
||||
var irradiance_sample_dir = N;
|
||||
// Rotating the world space ray direction by the environment light map transform matrix, it is
|
||||
// equivalent to rotating the diffuse environment cubemap itself.
|
||||
|
@ -143,6 +143,7 @@ use bevy_render::{
|
||||
renderer::RenderDevice,
|
||||
texture::{FallbackImage, GpuImage},
|
||||
};
|
||||
use bevy_utils::default;
|
||||
use core::{num::NonZero, ops::Deref};
|
||||
|
||||
use bevy_asset::{AssetId, Handle};
|
||||
@ -166,7 +167,7 @@ pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "w
|
||||
/// The component that defines an irradiance volume.
|
||||
///
|
||||
/// See [`crate::irradiance_volume`] for detailed information.
|
||||
#[derive(Clone, Default, Reflect, Component, Debug)]
|
||||
#[derive(Clone, Reflect, Component, Debug)]
|
||||
#[reflect(Component, Default, Debug)]
|
||||
pub struct IrradianceVolume {
|
||||
/// The 3D texture that represents the ambient cubes, encoded in the format
|
||||
@ -180,6 +181,30 @@ pub struct IrradianceVolume {
|
||||
///
|
||||
/// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
|
||||
pub intensity: f32,
|
||||
|
||||
/// Whether the light from this irradiance volume has an effect on meshes
|
||||
/// with lightmaps.
|
||||
///
|
||||
/// Set this to false if your lightmap baking tool bakes the light from this
|
||||
/// irradiance volume into the lightmaps in order to avoid counting the
|
||||
/// irradiance twice. Frequently, applications use irradiance volumes as a
|
||||
/// lower-quality alternative to lightmaps for capturing indirect
|
||||
/// illumination on dynamic objects, and such applications will want to set
|
||||
/// this value to false.
|
||||
///
|
||||
/// By default, this is set to true.
|
||||
pub affects_lightmapped_meshes: bool,
|
||||
}
|
||||
|
||||
impl Default for IrradianceVolume {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
IrradianceVolume {
|
||||
voxels: default(),
|
||||
intensity: 0.0,
|
||||
affects_lightmapped_meshes: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All the bind group entries necessary for PBR shaders to access the
|
||||
@ -336,6 +361,10 @@ impl LightProbeComponent for IrradianceVolume {
|
||||
self.intensity
|
||||
}
|
||||
|
||||
fn affects_lightmapped_mesh_diffuse(&self) -> bool {
|
||||
self.affects_lightmapped_meshes
|
||||
}
|
||||
|
||||
fn create_render_view_light_probes(
|
||||
_: Option<&Self>,
|
||||
_: &RenderAssets<GpuImage>,
|
||||
|
@ -31,6 +31,14 @@ fn irradiance_volume_light(
|
||||
return vec3(0.0f);
|
||||
}
|
||||
|
||||
// If we're lightmapped, and the irradiance volume contributes no diffuse
|
||||
// light, then bail out.
|
||||
#ifdef LIGHTMAP
|
||||
if (!query_result.affects_lightmapped_mesh_diffuse) {
|
||||
return vec3(0.0f);
|
||||
}
|
||||
#endif // LIGHTMAP
|
||||
|
||||
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
|
||||
let irradiance_volume_texture = irradiance_volumes[query_result.texture_index];
|
||||
#else
|
||||
|
@ -16,6 +16,8 @@ struct LightProbeQueryResult {
|
||||
// Transform from world space to the light probe model space. In light probe
|
||||
// model space, the light probe is a 1×1×1 cube centered on the origin.
|
||||
light_from_world: mat4x4<f32>,
|
||||
// Whether this light probe contributes diffuse light to lightmapped meshes.
|
||||
affects_lightmapped_mesh_diffuse: bool,
|
||||
};
|
||||
|
||||
fn transpose_affine_matrix(matrix: mat3x4<f32>) -> mat4x4<f32> {
|
||||
@ -80,6 +82,8 @@ fn query_light_probe(
|
||||
result.texture_index = light_probe.cubemap_index;
|
||||
result.intensity = light_probe.intensity;
|
||||
result.light_from_world = light_from_world;
|
||||
result.affects_lightmapped_mesh_diffuse =
|
||||
light_probe.affects_lightmapped_mesh_diffuse != 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -132,6 +136,8 @@ fn query_light_probe(
|
||||
result.texture_index = light_probe.cubemap_index;
|
||||
result.intensity = light_probe.intensity;
|
||||
result.light_from_world = light_from_world;
|
||||
result.affects_lightmapped_mesh_diffuse =
|
||||
light_probe.affects_lightmapped_mesh_diffuse != 0u;
|
||||
|
||||
// TODO: Workaround for ICE in DXC https://github.com/microsoft/DirectXShaderCompiler/issues/6183
|
||||
// We can't use `break` here because of the ICE.
|
||||
|
@ -125,6 +125,10 @@ struct RenderLightProbe {
|
||||
///
|
||||
/// See the comment in [`EnvironmentMapLight`] for details.
|
||||
intensity: f32,
|
||||
|
||||
/// Whether this light probe adds to the diffuse contribution of the
|
||||
/// irradiance for meshes with lightmaps.
|
||||
affects_lightmapped_mesh_diffuse: u32,
|
||||
}
|
||||
|
||||
/// A per-view shader uniform that specifies all the light probes that the view
|
||||
@ -158,6 +162,12 @@ pub struct LightProbesUniform {
|
||||
///
|
||||
/// See the comment in [`EnvironmentMapLight`] for details.
|
||||
intensity_for_view: f32,
|
||||
|
||||
/// Whether the environment map attached to the view affects the diffuse
|
||||
/// lighting for lightmapped meshes.
|
||||
///
|
||||
/// This will be 1 if the map does affect lightmapped meshes or 0 otherwise.
|
||||
view_environment_map_affects_lightmapped_mesh_diffuse: u32,
|
||||
}
|
||||
|
||||
/// A GPU buffer that stores information about all light probes.
|
||||
@ -191,6 +201,10 @@ where
|
||||
// See the comment in [`EnvironmentMapLight`] for details.
|
||||
intensity: f32,
|
||||
|
||||
// Whether this light probe adds to the diffuse contribution of the
|
||||
// irradiance for meshes with lightmaps.
|
||||
affects_lightmapped_mesh_diffuse: bool,
|
||||
|
||||
// The IDs of all assets associated with this light probe.
|
||||
//
|
||||
// Because each type of light probe component may reference different types
|
||||
@ -279,6 +293,10 @@ pub trait LightProbeComponent: Send + Sync + Component + Sized {
|
||||
/// sampled from the texture.
|
||||
fn intensity(&self) -> f32;
|
||||
|
||||
/// Returns true if this light probe contributes diffuse lighting to meshes
|
||||
/// with lightmaps or false otherwise.
|
||||
fn affects_lightmapped_mesh_diffuse(&self) -> bool;
|
||||
|
||||
/// Creates an instance of [`RenderViewLightProbes`] containing all the
|
||||
/// information needed to render this light probe.
|
||||
///
|
||||
@ -537,6 +555,9 @@ fn upload_light_probes(
|
||||
intensity_for_view: render_view_environment_maps
|
||||
.map(|maps| maps.view_light_probe_info.intensity)
|
||||
.unwrap_or(1.0),
|
||||
view_environment_map_affects_lightmapped_mesh_diffuse: render_view_environment_maps
|
||||
.map(|maps| maps.view_light_probe_info.affects_lightmapped_mesh_diffuse as u32)
|
||||
.unwrap_or(1),
|
||||
};
|
||||
|
||||
// Add any environment maps that [`gather_light_probes`] found to the
|
||||
@ -576,6 +597,7 @@ impl Default for LightProbesUniform {
|
||||
view_cubemap_index: -1,
|
||||
smallest_specular_mip_level_for_view: 0,
|
||||
intensity_for_view: 1.0,
|
||||
view_environment_map_affects_lightmapped_mesh_diffuse: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -596,6 +618,7 @@ where
|
||||
light_from_world: light_probe_transform.compute_matrix().inverse(),
|
||||
asset_id: id,
|
||||
intensity: environment_map.intensity(),
|
||||
affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -693,6 +716,8 @@ where
|
||||
],
|
||||
texture_index: cubemap_index as i32,
|
||||
intensity: light_probe.intensity,
|
||||
affects_lightmapped_mesh_diffuse: light_probe.affects_lightmapped_mesh_diffuse
|
||||
as u32,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -707,6 +732,7 @@ where
|
||||
light_from_world: self.light_from_world,
|
||||
world_from_light: self.world_from_light,
|
||||
intensity: self.intensity,
|
||||
affects_lightmapped_mesh_diffuse: self.affects_lightmapped_mesh_diffuse,
|
||||
asset_id: self.asset_id.clone(),
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,8 @@ pub struct ExtractedPointLight {
|
||||
pub spot_light_angles: Option<(f32, f32)>,
|
||||
pub volumetric: bool,
|
||||
pub soft_shadows_enabled: bool,
|
||||
/// whether this point light contributes diffuse light to lightmapped meshes
|
||||
pub affects_lightmapped_mesh_diffuse: bool,
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
@ -68,6 +70,9 @@ pub struct ExtractedDirectionalLight {
|
||||
pub transform: GlobalTransform,
|
||||
pub shadows_enabled: bool,
|
||||
pub volumetric: bool,
|
||||
/// whether this directional light contributes diffuse light to lightmapped
|
||||
/// meshes
|
||||
pub affects_lightmapped_mesh_diffuse: bool,
|
||||
pub shadow_depth_bias: f32,
|
||||
pub shadow_normal_bias: f32,
|
||||
pub cascade_shadow_config: CascadeShadowConfig,
|
||||
@ -84,6 +89,7 @@ bitflags::bitflags! {
|
||||
const SHADOWS_ENABLED = 1 << 0;
|
||||
const SPOT_LIGHT_Y_NEGATIVE = 1 << 1;
|
||||
const VOLUMETRIC = 1 << 2;
|
||||
const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 3;
|
||||
const NONE = 0;
|
||||
const UNINITIALIZED = 0xFFFF;
|
||||
}
|
||||
@ -117,6 +123,7 @@ bitflags::bitflags! {
|
||||
struct DirectionalLightFlags: u32 {
|
||||
const SHADOWS_ENABLED = 1 << 0;
|
||||
const VOLUMETRIC = 1 << 1;
|
||||
const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 2;
|
||||
const NONE = 0;
|
||||
const UNINITIALIZED = 0xFFFF;
|
||||
}
|
||||
@ -135,6 +142,7 @@ pub struct GpuLights {
|
||||
n_directional_lights: u32,
|
||||
// offset from spot light's light index to spot light's shadow map index
|
||||
spot_light_shadowmap_offset: i32,
|
||||
ambient_light_affects_lightmapped_meshes: u32,
|
||||
}
|
||||
|
||||
// NOTE: When running bevy on Adreno GPU chipsets in WebGL, any value above 1 will result in a crash
|
||||
@ -311,6 +319,7 @@ pub fn extract_lights(
|
||||
shadow_map_near_z: point_light.shadow_map_near_z,
|
||||
spot_light_angles: None,
|
||||
volumetric: volumetric_light.is_some(),
|
||||
affects_lightmapped_mesh_diffuse: point_light.affects_lightmapped_mesh_diffuse,
|
||||
#[cfg(feature = "experimental_pbr_pcss")]
|
||||
soft_shadows_enabled: point_light.soft_shadows_enabled,
|
||||
#[cfg(not(feature = "experimental_pbr_pcss"))]
|
||||
@ -373,6 +382,8 @@ pub fn extract_lights(
|
||||
shadow_map_near_z: spot_light.shadow_map_near_z,
|
||||
spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)),
|
||||
volumetric: volumetric_light.is_some(),
|
||||
affects_lightmapped_mesh_diffuse: spot_light
|
||||
.affects_lightmapped_mesh_diffuse,
|
||||
#[cfg(feature = "experimental_pbr_pcss")]
|
||||
soft_shadows_enabled: spot_light.soft_shadows_enabled,
|
||||
#[cfg(not(feature = "experimental_pbr_pcss"))]
|
||||
@ -448,6 +459,8 @@ pub fn extract_lights(
|
||||
illuminance: directional_light.illuminance,
|
||||
transform: *transform,
|
||||
volumetric: volumetric_light.is_some(),
|
||||
affects_lightmapped_mesh_diffuse: directional_light
|
||||
.affects_lightmapped_mesh_diffuse,
|
||||
#[cfg(feature = "experimental_pbr_pcss")]
|
||||
soft_shadow_size: directional_light.soft_shadow_size,
|
||||
#[cfg(not(feature = "experimental_pbr_pcss"))]
|
||||
@ -885,6 +898,10 @@ pub fn prepare_lights(
|
||||
flags |= PointLightFlags::VOLUMETRIC;
|
||||
}
|
||||
|
||||
if light.affects_lightmapped_mesh_diffuse {
|
||||
flags |= PointLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE;
|
||||
}
|
||||
|
||||
let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles {
|
||||
Some((inner, outer)) => {
|
||||
let light_direction = light.transform.forward();
|
||||
@ -963,6 +980,10 @@ pub fn prepare_lights(
|
||||
flags |= DirectionalLightFlags::SHADOWS_ENABLED;
|
||||
}
|
||||
|
||||
if light.affects_lightmapped_mesh_diffuse {
|
||||
flags |= DirectionalLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE;
|
||||
}
|
||||
|
||||
let num_cascades = light
|
||||
.cascade_shadow_config
|
||||
.bounds
|
||||
@ -1137,6 +1158,8 @@ pub fn prepare_lights(
|
||||
// index to shadow map index, we need to subtract point light count and add directional shadowmap count.
|
||||
spot_light_shadowmap_offset: num_directional_cascades_enabled as i32
|
||||
- point_light_count as i32,
|
||||
ambient_light_affects_lightmapped_meshes: ambient_light.affects_lightmapped_meshes
|
||||
as u32,
|
||||
};
|
||||
|
||||
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
|
||||
|
@ -20,6 +20,7 @@ struct ClusterableObject {
|
||||
const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
|
||||
const POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u;
|
||||
const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 4u;
|
||||
const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 8u;
|
||||
|
||||
struct DirectionalCascade {
|
||||
clip_from_world: mat4x4<f32>,
|
||||
@ -44,6 +45,7 @@ struct DirectionalLight {
|
||||
|
||||
const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
|
||||
const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 2u;
|
||||
const DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 4u;
|
||||
|
||||
struct Lights {
|
||||
// NOTE: this array size must be kept in sync with the constants defined in bevy_pbr/src/render/light.rs
|
||||
@ -124,6 +126,8 @@ struct LightProbe {
|
||||
light_from_world_transposed: mat3x4<f32>,
|
||||
cubemap_index: i32,
|
||||
intensity: f32,
|
||||
// Whether this light probe contributes diffuse light to lightmapped meshes.
|
||||
affects_lightmapped_mesh_diffuse: u32,
|
||||
};
|
||||
|
||||
struct LightProbes {
|
||||
@ -140,6 +144,9 @@ struct LightProbes {
|
||||
smallest_specular_mip_level_for_view: u32,
|
||||
// The intensity of the environment map associated with the view.
|
||||
intensity_for_view: f32,
|
||||
// Whether the environment map attached to the view affects the diffuse
|
||||
// lighting for lightmapped meshes.
|
||||
view_environment_map_affects_lightmapped_mesh_diffuse: u32,
|
||||
};
|
||||
|
||||
// Settings for screen space reflections.
|
||||
|
@ -406,13 +406,24 @@ fn apply_pbr_lighting(
|
||||
i < clusterable_object_index_ranges.first_spot_light_index_offset;
|
||||
i = i + 1u) {
|
||||
let light_id = clustering::get_clusterable_object_id(i);
|
||||
|
||||
// If we're lightmapped, disable diffuse contribution from the light if
|
||||
// requested, to avoid double-counting light.
|
||||
#ifdef LIGHTMAP
|
||||
let enable_diffuse =
|
||||
(view_bindings::clusterable_objects.data[light_id].flags &
|
||||
mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u;
|
||||
#else // LIGHTMAP
|
||||
let enable_diffuse = true;
|
||||
#endif // LIGHTMAP
|
||||
|
||||
var shadow: f32 = 1.0;
|
||||
if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
|
||||
&& (view_bindings::clusterable_objects.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(light_id, &lighting_input);
|
||||
let light_contrib = lighting::point_light(light_id, &lighting_input, enable_diffuse);
|
||||
direct_light += light_contrib * shadow;
|
||||
|
||||
#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION
|
||||
@ -443,6 +454,16 @@ fn apply_pbr_lighting(
|
||||
i = i + 1u) {
|
||||
let light_id = clustering::get_clusterable_object_id(i);
|
||||
|
||||
// If we're lightmapped, disable diffuse contribution from the light if
|
||||
// requested, to avoid double-counting light.
|
||||
#ifdef LIGHTMAP
|
||||
let enable_diffuse =
|
||||
(view_bindings::clusterable_objects.data[light_id].flags &
|
||||
mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u;
|
||||
#else // LIGHTMAP
|
||||
let enable_diffuse = true;
|
||||
#endif // LIGHTMAP
|
||||
|
||||
var shadow: f32 = 1.0;
|
||||
if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
|
||||
&& (view_bindings::clusterable_objects.data[light_id].flags &
|
||||
@ -455,7 +476,7 @@ fn apply_pbr_lighting(
|
||||
);
|
||||
}
|
||||
|
||||
let light_contrib = lighting::spot_light(light_id, &lighting_input);
|
||||
let light_contrib = lighting::spot_light(light_id, &lighting_input, enable_diffuse);
|
||||
direct_light += light_contrib * shadow;
|
||||
|
||||
#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION
|
||||
@ -495,13 +516,24 @@ fn apply_pbr_lighting(
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're lightmapped, disable diffuse contribution from the light if
|
||||
// requested, to avoid double-counting light.
|
||||
#ifdef LIGHTMAP
|
||||
let enable_diffuse =
|
||||
((*light).flags &
|
||||
mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) !=
|
||||
0u;
|
||||
#else // LIGHTMAP
|
||||
let enable_diffuse = true;
|
||||
#endif // LIGHTMAP
|
||||
|
||||
var shadow: f32 = 1.0;
|
||||
if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
|
||||
&& (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, &lighting_input);
|
||||
var light_contrib = lighting::directional_light(i, &lighting_input, enable_diffuse);
|
||||
|
||||
#ifdef DIRECTIONAL_LIGHT_SHADOW_MAP_DEBUG_CASCADES
|
||||
light_contrib = shadows::cascade_debug_visualization(light_contrib, i, view_z);
|
||||
|
@ -440,7 +440,11 @@ fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 {
|
||||
return clampedPerceptualRoughness * clampedPerceptualRoughness;
|
||||
}
|
||||
|
||||
fn point_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32> {
|
||||
fn point_light(
|
||||
light_id: u32,
|
||||
input: ptr<function, LightingInput>,
|
||||
enable_diffuse: bool
|
||||
) -> vec3<f32> {
|
||||
// Unpack.
|
||||
let diffuse_color = (*input).diffuse_color;
|
||||
let P = (*input).P;
|
||||
@ -507,7 +511,10 @@ fn point_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32>
|
||||
// Diffuse.
|
||||
// Comes after specular since its N⋅L is used in the lighting equation.
|
||||
var derived_input = derive_lighting_input(N, V, L);
|
||||
let diffuse = diffuse_color * Fd_Burley(input, &derived_input);
|
||||
var diffuse = vec3(0.0);
|
||||
if (enable_diffuse) {
|
||||
diffuse = diffuse_color * Fd_Burley(input, &derived_input);
|
||||
}
|
||||
|
||||
// See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation
|
||||
// Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩
|
||||
@ -536,9 +543,13 @@ fn point_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32>
|
||||
(rangeAttenuation * derived_input.NdotL);
|
||||
}
|
||||
|
||||
fn spot_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32> {
|
||||
fn spot_light(
|
||||
light_id: u32,
|
||||
input: ptr<function, LightingInput>,
|
||||
enable_diffuse: bool
|
||||
) -> vec3<f32> {
|
||||
// reuse the point light calculations
|
||||
let point_light = point_light(light_id, input);
|
||||
let point_light = point_light(light_id, input, enable_diffuse);
|
||||
|
||||
let light = &view_bindings::clusterable_objects.data[light_id];
|
||||
|
||||
@ -560,7 +571,11 @@ fn spot_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32> {
|
||||
return point_light * spot_attenuation;
|
||||
}
|
||||
|
||||
fn directional_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3<f32> {
|
||||
fn directional_light(
|
||||
light_id: u32,
|
||||
input: ptr<function, LightingInput>,
|
||||
enable_diffuse: bool
|
||||
) -> vec3<f32> {
|
||||
// Unpack.
|
||||
let diffuse_color = (*input).diffuse_color;
|
||||
let NdotV = (*input).layers[LAYER_BASE].NdotV;
|
||||
@ -573,7 +588,10 @@ fn directional_light(light_id: u32, input: ptr<function, LightingInput>) -> vec3
|
||||
let L = (*light).direction_to_light.xyz;
|
||||
var derived_input = derive_lighting_input(N, V, L);
|
||||
|
||||
let diffuse = diffuse_color * Fd_Burley(input, &derived_input);
|
||||
var diffuse = vec3(0.0);
|
||||
if (enable_diffuse) {
|
||||
diffuse = diffuse_color * Fd_Burley(input, &derived_input);
|
||||
}
|
||||
|
||||
#ifdef STANDARD_MATERIAL_ANISOTROPY
|
||||
let specular_light = specular_anisotropy(input, &derived_input, L, 1.0);
|
||||
|
@ -27,6 +27,7 @@ fn main() {
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 1.0 / 5.0f32,
|
||||
..default()
|
||||
})
|
||||
.add_plugins((
|
||||
DefaultPlugins.set(
|
||||
|
@ -104,6 +104,7 @@ fn setup(
|
||||
commands.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 0.0,
|
||||
..default()
|
||||
});
|
||||
|
||||
commands.spawn((
|
||||
|
@ -160,6 +160,7 @@ fn main() {
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 0.0,
|
||||
..default()
|
||||
})
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(PreUpdate, create_cubes)
|
||||
@ -248,6 +249,7 @@ fn spawn_irradiance_volume(commands: &mut Commands, assets: &ExampleAssets) {
|
||||
IrradianceVolume {
|
||||
voxels: assets.irradiance_volume.clone(),
|
||||
intensity: IRRADIANCE_VOLUME_INTENSITY,
|
||||
..default()
|
||||
},
|
||||
LightProbe,
|
||||
));
|
||||
@ -431,6 +433,7 @@ fn toggle_irradiance_volumes(
|
||||
commands.entity(light_probe).insert(IrradianceVolume {
|
||||
voxels: assets.irradiance_volume.clone(),
|
||||
intensity: IRRADIANCE_VOLUME_INTENSITY,
|
||||
..default()
|
||||
});
|
||||
ambient_light.brightness = 0.0;
|
||||
app_status.irradiance_volume_present = true;
|
||||
|
@ -114,6 +114,7 @@ fn setup(
|
||||
commands.insert_resource(AmbientLight {
|
||||
color: ORANGE_RED.into(),
|
||||
brightness: 0.02,
|
||||
..default()
|
||||
});
|
||||
|
||||
// red point light
|
||||
|
523
examples/3d/mixed_lighting.rs
Normal file
@ -0,0 +1,523 @@
|
||||
//! Demonstrates how to combine baked and dynamic lighting.
|
||||
|
||||
use bevy::{
|
||||
pbr::Lightmap,
|
||||
picking::{backend::HitData, pointer::PointerInteraction},
|
||||
prelude::*,
|
||||
scene::SceneInstanceReady,
|
||||
};
|
||||
|
||||
use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender};
|
||||
|
||||
#[path = "../helpers/widgets.rs"]
|
||||
mod widgets;
|
||||
|
||||
/// How bright the lightmaps are.
|
||||
const LIGHTMAP_EXPOSURE: f32 = 600.0;
|
||||
|
||||
/// How far above the ground the sphere's origin is when moved, in scene units.
|
||||
const SPHERE_OFFSET: f32 = 0.2;
|
||||
|
||||
/// The settings that the user has currently chosen for the app.
|
||||
#[derive(Clone, Default, Resource)]
|
||||
struct AppStatus {
|
||||
/// The lighting mode that the user currently has set: baked, mixed, or
|
||||
/// real-time.
|
||||
lighting_mode: LightingMode,
|
||||
}
|
||||
|
||||
/// The type of lighting to use in the scene.
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
enum LightingMode {
|
||||
/// All light is computed ahead of time; no lighting takes place at runtime.
|
||||
///
|
||||
/// In this mode, the sphere can't be moved, as the light shining on it was
|
||||
/// precomputed. On the plus side, the sphere has indirect lighting in this
|
||||
/// mode, as the red hue on the bottom of the sphere demonstrates.
|
||||
Baked,
|
||||
|
||||
/// All light for the static objects is computed ahead of time, but the
|
||||
/// light for the dynamic sphere is computed at runtime.
|
||||
///
|
||||
/// In this mode, the sphere can be moved, and the light will be computed
|
||||
/// for it as you do so. The sphere loses indirect illumination; notice the
|
||||
/// lack of a red hue at the base of the sphere. However, the rest of the
|
||||
/// scene has indirect illumination. Note also that the sphere doesn't cast
|
||||
/// a shadow on the static objects in this mode, because shadows are part of
|
||||
/// the lighting computation.
|
||||
MixedDirect,
|
||||
|
||||
/// Indirect light for the static objects is computed ahead of time, and
|
||||
/// direct light for all objects is computed at runtime.
|
||||
///
|
||||
/// In this mode, the sphere can be moved, and the light will be computed
|
||||
/// for it as you do so. The sphere loses indirect illumination; notice the
|
||||
/// lack of a red hue at the base of the sphere. However, the rest of the
|
||||
/// scene has indirect illumination. The sphere does cast a shadow on
|
||||
/// objects in this mode, because the direct light for all objects is being
|
||||
/// computed dynamically.
|
||||
#[default]
|
||||
MixedIndirect,
|
||||
|
||||
/// Light is computed at runtime for all objects.
|
||||
///
|
||||
/// In this mode, no lightmaps are used at all. All objects are dynamically
|
||||
/// lit, which provides maximum flexibility. However, the downside is that
|
||||
/// global illumination is lost; note that the base of the sphere isn't red
|
||||
/// as it is in baked mode.
|
||||
RealTime,
|
||||
}
|
||||
|
||||
/// An event that's fired whenever the user changes the lighting mode.
|
||||
///
|
||||
/// This is also fired when the scene loads for the first time.
|
||||
#[derive(Clone, Copy, Default, Event)]
|
||||
struct LightingModeChanged;
|
||||
|
||||
#[derive(Clone, Copy, Component, Debug)]
|
||||
struct HelpText;
|
||||
|
||||
/// The name of every static object in the scene that has a lightmap, as well as
|
||||
/// the UV rect of its lightmap.
|
||||
///
|
||||
/// Storing this as an array and doing a linear search through it is rather
|
||||
/// inefficient, but we do it anyway for clarity's sake.
|
||||
static LIGHTMAPS: [(&str, Rect); 5] = [
|
||||
(
|
||||
"Plane",
|
||||
uv_rect_opengl(Vec2::splat(0.026), Vec2::splat(0.710)),
|
||||
),
|
||||
(
|
||||
"SheenChair_fabric",
|
||||
uv_rect_opengl(vec2(0.7864, 0.02377), vec2(0.1910, 0.1912)),
|
||||
),
|
||||
(
|
||||
"SheenChair_label",
|
||||
uv_rect_opengl(vec2(0.275, -0.016), vec2(0.858, 0.486)),
|
||||
),
|
||||
(
|
||||
"SheenChair_metal",
|
||||
uv_rect_opengl(vec2(0.998, 0.506), vec2(-0.029, -0.067)),
|
||||
),
|
||||
(
|
||||
"SheenChair_wood",
|
||||
uv_rect_opengl(vec2(0.787, 0.257), vec2(0.179, 0.177)),
|
||||
),
|
||||
];
|
||||
|
||||
static SPHERE_UV_RECT: Rect = uv_rect_opengl(vec2(0.788, 0.484), Vec2::splat(0.062));
|
||||
|
||||
/// The initial position of the sphere.
|
||||
///
|
||||
/// When the user sets the light mode to [`LightingMode::Baked`], we reset the
|
||||
/// position to this point.
|
||||
const INITIAL_SPHERE_POSITION: Vec3 = vec3(0.0, 0.5233223, 0.0);
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: "Bevy Mixed Lighting Example".into(),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}))
|
||||
.add_plugins(MeshPickingPlugin)
|
||||
.insert_resource(AmbientLight {
|
||||
color: ClearColor::default().0,
|
||||
brightness: 10000.0,
|
||||
affects_lightmapped_meshes: true,
|
||||
})
|
||||
.init_resource::<AppStatus>()
|
||||
.add_event::<WidgetClickEvent<LightingMode>>()
|
||||
.add_event::<LightingModeChanged>()
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, update_lightmaps)
|
||||
.add_systems(Update, update_directional_light)
|
||||
.add_systems(Update, make_sphere_nonpickable)
|
||||
.add_systems(Update, update_radio_buttons)
|
||||
.add_systems(Update, handle_lighting_mode_change)
|
||||
.add_systems(Update, widgets::handle_ui_interactions::<LightingMode>)
|
||||
.add_systems(Update, reset_sphere_position)
|
||||
.add_systems(Update, move_sphere)
|
||||
.add_systems(Update, adjust_help_text)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Creates the scene.
|
||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_status: Res<AppStatus>) {
|
||||
spawn_camera(&mut commands);
|
||||
spawn_scene(&mut commands, &asset_server);
|
||||
spawn_buttons(&mut commands);
|
||||
spawn_help_text(&mut commands, &app_status);
|
||||
}
|
||||
|
||||
/// Spawns the 3D camera.
|
||||
fn spawn_camera(commands: &mut Commands) {
|
||||
commands
|
||||
.spawn(Camera3d::default())
|
||||
.insert(Transform::from_xyz(-0.7, 0.7, 1.0).looking_at(vec3(0.0, 0.3, 0.0), Vec3::Y));
|
||||
}
|
||||
|
||||
/// Spawns the scene.
|
||||
///
|
||||
/// The scene is loaded from a glTF file.
|
||||
fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) {
|
||||
commands
|
||||
.spawn(SceneRoot(
|
||||
asset_server.load(
|
||||
GltfAssetLabel::Scene(0)
|
||||
.from_asset("models/MixedLightingExample/MixedLightingExample.gltf"),
|
||||
),
|
||||
))
|
||||
.observe(
|
||||
|_: Trigger<SceneInstanceReady>,
|
||||
mut lighting_mode_change_event_writer: EventWriter<LightingModeChanged>| {
|
||||
// When the scene loads, send a `LightingModeChanged` event so
|
||||
// that we set up the lightmaps.
|
||||
lighting_mode_change_event_writer.send(LightingModeChanged);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Spawns the buttons that allow the user to change the lighting mode.
|
||||
fn spawn_buttons(commands: &mut Commands) {
|
||||
commands
|
||||
.spawn(widgets::main_ui_node())
|
||||
.with_children(|parent| {
|
||||
widgets::spawn_option_buttons(
|
||||
parent,
|
||||
"Lighting",
|
||||
&[
|
||||
(LightingMode::Baked, "Baked"),
|
||||
(LightingMode::MixedDirect, "Mixed (Direct)"),
|
||||
(LightingMode::MixedIndirect, "Mixed (Indirect)"),
|
||||
(LightingMode::RealTime, "Real-Time"),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawns the help text at the top of the window.
|
||||
fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
|
||||
commands.spawn((
|
||||
create_help_text(app_status),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(12.0),
|
||||
left: Val::Px(12.0),
|
||||
..default()
|
||||
},
|
||||
HelpText,
|
||||
));
|
||||
}
|
||||
|
||||
/// Adds lightmaps to and/or removes lightmaps from objects in the scene when
|
||||
/// the lighting mode changes.
|
||||
///
|
||||
/// This is also called right after the scene loads in order to set up the
|
||||
/// lightmaps.
|
||||
fn update_lightmaps(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
meshes: Query<(Entity, &Name, &MeshMaterial3d<StandardMaterial>), With<Mesh3d>>,
|
||||
mut lighting_mode_change_event_reader: EventReader<LightingModeChanged>,
|
||||
app_status: Res<AppStatus>,
|
||||
) {
|
||||
// Only run if the lighting mode changed. (Note that a change event is fired
|
||||
// when the scene first loads.)
|
||||
if lighting_mode_change_event_reader.read().next().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the lightmap to use, based on the lighting mode.
|
||||
let lightmap: Option<Handle<Image>> = match app_status.lighting_mode {
|
||||
LightingMode::Baked => {
|
||||
Some(asset_server.load("lightmaps/MixedLightingExample-Baked.zstd.ktx2"))
|
||||
}
|
||||
LightingMode::MixedDirect => {
|
||||
Some(asset_server.load("lightmaps/MixedLightingExample-MixedDirect.zstd.ktx2"))
|
||||
}
|
||||
LightingMode::MixedIndirect => {
|
||||
Some(asset_server.load("lightmaps/MixedLightingExample-MixedIndirect.zstd.ktx2"))
|
||||
}
|
||||
LightingMode::RealTime => None,
|
||||
};
|
||||
|
||||
'outer: for (entity, name, material) in &meshes {
|
||||
// Add lightmaps to or remove lightmaps from the scenery objects in the
|
||||
// scene (all objects but the sphere).
|
||||
//
|
||||
// Note that doing a linear search through the `LIGHTMAPS` array is
|
||||
// inefficient, but we do it anyway in this example to improve clarity.
|
||||
for (lightmap_name, uv_rect) in LIGHTMAPS {
|
||||
if &**name != lightmap_name {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lightmap exposure defaults to zero, so we need to set it.
|
||||
if let Some(ref mut material) = materials.get_mut(material) {
|
||||
material.lightmap_exposure = LIGHTMAP_EXPOSURE;
|
||||
}
|
||||
|
||||
// Add or remove the lightmap.
|
||||
match lightmap {
|
||||
Some(ref lightmap) => {
|
||||
commands.entity(entity).insert(Lightmap {
|
||||
image: (*lightmap).clone(),
|
||||
uv_rect,
|
||||
});
|
||||
}
|
||||
None => {
|
||||
commands.entity(entity).remove::<Lightmap>();
|
||||
}
|
||||
}
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
// Add lightmaps to or remove lightmaps from the sphere.
|
||||
if &**name == "Sphere" {
|
||||
// Lightmap exposure defaults to zero, so we need to set it.
|
||||
if let Some(ref mut material) = materials.get_mut(material) {
|
||||
material.lightmap_exposure = LIGHTMAP_EXPOSURE;
|
||||
}
|
||||
|
||||
// Add or remove the lightmap from the sphere. We only apply the
|
||||
// lightmap in fully-baked mode.
|
||||
match (&lightmap, app_status.lighting_mode) {
|
||||
(Some(lightmap), LightingMode::Baked) => {
|
||||
commands.entity(entity).insert(Lightmap {
|
||||
image: (*lightmap).clone(),
|
||||
uv_rect: SPHERE_UV_RECT,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
commands.entity(entity).remove::<Lightmap>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a uv rectangle from the OpenGL coordinate system (origin in the
|
||||
/// lower left) to the Vulkan coordinate system (origin in the upper left) that
|
||||
/// Bevy uses.
|
||||
///
|
||||
/// For this particular example, the baking tool happened to use the OpenGL
|
||||
/// coordinate system, so it was more convenient to do the conversion at compile
|
||||
/// time than to pre-calculate and hard-code the values.
|
||||
const fn uv_rect_opengl(gl_min: Vec2, size: Vec2) -> Rect {
|
||||
let min = vec2(gl_min.x, 1.0 - gl_min.y - size.y);
|
||||
Rect {
|
||||
min,
|
||||
max: vec2(min.x + size.x, min.y + size.y),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures that clicking on the scene to move the sphere doesn't result in a
|
||||
/// hit on the sphere itself.
|
||||
fn make_sphere_nonpickable(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(Entity, &Name), (With<Mesh3d>, Without<PickingBehavior>)>,
|
||||
) {
|
||||
for (sphere, name) in &mut query {
|
||||
if &**name == "Sphere" {
|
||||
commands.entity(sphere).insert(PickingBehavior::IGNORE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the directional light settings as necessary when the lighting mode
|
||||
/// changes.
|
||||
fn update_directional_light(
|
||||
mut lights: Query<&mut DirectionalLight>,
|
||||
mut lighting_mode_change_event_reader: EventReader<LightingModeChanged>,
|
||||
app_status: Res<AppStatus>,
|
||||
) {
|
||||
// Only run if the lighting mode changed. (Note that a change event is fired
|
||||
// when the scene first loads.)
|
||||
if lighting_mode_change_event_reader.read().next().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Real-time direct light is used on the scenery if we're using mixed
|
||||
// indirect or real-time mode.
|
||||
let scenery_is_lit_in_real_time = matches!(
|
||||
app_status.lighting_mode,
|
||||
LightingMode::MixedIndirect | LightingMode::RealTime
|
||||
);
|
||||
|
||||
for mut light in &mut lights {
|
||||
light.affects_lightmapped_mesh_diffuse = scenery_is_lit_in_real_time;
|
||||
// Don't bother enabling shadows if they won't show up on the scenery.
|
||||
light.shadows_enabled = scenery_is_lit_in_real_time;
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the state of the selection widgets at the bottom of the window when
|
||||
/// the lighting mode changes.
|
||||
fn update_radio_buttons(
|
||||
mut widgets: Query<
|
||||
(
|
||||
Entity,
|
||||
Option<&mut BackgroundColor>,
|
||||
Has<Text>,
|
||||
&WidgetClickSender<LightingMode>,
|
||||
),
|
||||
Or<(With<RadioButton>, With<RadioButtonText>)>,
|
||||
>,
|
||||
app_status: Res<AppStatus>,
|
||||
mut writer: TextUiWriter,
|
||||
) {
|
||||
for (entity, image, has_text, sender) in &mut widgets {
|
||||
let selected = **sender == app_status.lighting_mode;
|
||||
|
||||
if let Some(mut bg_color) = image {
|
||||
widgets::update_ui_radio_button(&mut bg_color, selected);
|
||||
}
|
||||
if has_text {
|
||||
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles clicks on the widgets at the bottom of the screen and fires
|
||||
/// [`LightingModeChanged`] events.
|
||||
fn handle_lighting_mode_change(
|
||||
mut widget_click_event_reader: EventReader<WidgetClickEvent<LightingMode>>,
|
||||
mut lighting_mode_change_event_writer: EventWriter<LightingModeChanged>,
|
||||
mut app_status: ResMut<AppStatus>,
|
||||
) {
|
||||
for event in widget_click_event_reader.read() {
|
||||
app_status.lighting_mode = **event;
|
||||
lighting_mode_change_event_writer.send(LightingModeChanged);
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the sphere to its original position when the user selects the baked
|
||||
/// lighting mode.
|
||||
///
|
||||
/// As the light from the sphere is precomputed and depends on the sphere's
|
||||
/// original position, the sphere must be placed there in order for the lighting
|
||||
/// to be correct.
|
||||
fn reset_sphere_position(
|
||||
mut objects: Query<(&Name, &mut Transform)>,
|
||||
mut lighting_mode_change_event_reader: EventReader<LightingModeChanged>,
|
||||
app_status: Res<AppStatus>,
|
||||
) {
|
||||
// Only run if the lighting mode changed and if the lighting mode is
|
||||
// `LightingMode::Baked`. (Note that a change event is fired when the scene
|
||||
// first loads.)
|
||||
if lighting_mode_change_event_reader.read().next().is_none()
|
||||
|| app_status.lighting_mode != LightingMode::Baked
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (name, mut transform) in &mut objects {
|
||||
if &**name == "Sphere" {
|
||||
transform.translation = INITIAL_SPHERE_POSITION;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the position of the sphere when the user clicks on a spot in the
|
||||
/// scene.
|
||||
///
|
||||
/// Note that the position of the sphere is locked in baked lighting mode.
|
||||
fn move_sphere(
|
||||
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
||||
pointers: Query<&PointerInteraction>,
|
||||
mut meshes: Query<(&Name, &Parent), With<Mesh3d>>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
app_status: Res<AppStatus>,
|
||||
) {
|
||||
// Only run when the left button is clicked and we're not in baked lighting
|
||||
// mode.
|
||||
if app_status.lighting_mode == LightingMode::Baked
|
||||
|| !mouse_button_input.pressed(MouseButton::Left)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the sphere.
|
||||
let Some(parent) = meshes
|
||||
.iter_mut()
|
||||
.filter_map(|(name, parent)| {
|
||||
if &**name == "Sphere" {
|
||||
Some(parent)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Grab its transform.
|
||||
let Ok(mut transform) = transforms.get_mut(**parent) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Set its transform to the appropriate position, as determined by the
|
||||
// picking subsystem.
|
||||
for interaction in pointers.iter() {
|
||||
if let Some(&(
|
||||
_,
|
||||
HitData {
|
||||
position: Some(position),
|
||||
..
|
||||
},
|
||||
)) = interaction.get_nearest_hit()
|
||||
{
|
||||
transform.translation = position + vec3(0.0, SPHERE_OFFSET, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the help text at the top of the screen when the lighting mode
|
||||
/// changes.
|
||||
fn adjust_help_text(
|
||||
mut commands: Commands,
|
||||
help_texts: Query<Entity, With<HelpText>>,
|
||||
app_status: Res<AppStatus>,
|
||||
mut lighting_mode_change_event_reader: EventReader<LightingModeChanged>,
|
||||
) {
|
||||
if lighting_mode_change_event_reader.read().next().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
for help_text in &help_texts {
|
||||
commands
|
||||
.entity(help_text)
|
||||
.insert(create_help_text(&app_status));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns appropriate text to display at the top of the screen.
|
||||
fn create_help_text(app_status: &AppStatus) -> Text {
|
||||
match app_status.lighting_mode {
|
||||
LightingMode::Baked => Text::new(
|
||||
"Scenery: Static, baked direct light, baked indirect light
|
||||
Sphere: Static, baked direct light, baked indirect light",
|
||||
),
|
||||
LightingMode::MixedDirect => Text::new(
|
||||
"Scenery: Static, baked direct light, baked indirect light
|
||||
Sphere: Dynamic, real-time direct light, no indirect light
|
||||
Click in the scene to move the sphere",
|
||||
),
|
||||
LightingMode::MixedIndirect => Text::new(
|
||||
"Scenery: Static, real-time direct light, baked indirect light
|
||||
Sphere: Dynamic, real-time direct light, no indirect light
|
||||
Click in the scene to move the sphere",
|
||||
),
|
||||
LightingMode::RealTime => Text::new(
|
||||
"Scenery: Dynamic, real-time direct light, no indirect light
|
||||
Sphere: Dynamic, real-time direct light, no indirect light
|
||||
Click in the scene to move the sphere",
|
||||
),
|
||||
}
|
||||
}
|
@ -62,6 +62,7 @@ fn setup_scene(
|
||||
commands.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 300.0,
|
||||
..default()
|
||||
});
|
||||
commands.insert_resource(CameraMode::Chase);
|
||||
commands.spawn((
|
||||
|
@ -86,6 +86,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands.insert_resource(AmbientLight {
|
||||
color: Color::srgb_u8(210, 220, 240),
|
||||
brightness: 1.0,
|
||||
..default()
|
||||
});
|
||||
|
||||
commands.insert_resource(Cubemap {
|
||||
|
@ -158,6 +158,7 @@ Example | Description
|
||||
[Load glTF extras](../examples/3d/load_gltf_extras.rs) | Loads and renders a glTF file as a scene, including the gltf extras
|
||||
[Mesh Ray Cast](../examples/3d/mesh_ray_cast.rs) | Demonstrates ray casting with the `MeshRayCast` system parameter
|
||||
[Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental)
|
||||
[Mixed lighting](../examples/3d/mixed_lighting.rs) | Demonstrates how to combine baked and dynamic lighting
|
||||
[Motion Blur](../examples/3d/motion_blur.rs) | Demonstrates per-pixel motion blur
|
||||
[Order Independent Transparency](../examples/3d/order_independent_transparency.rs) | Demonstrates how to use OIT
|
||||
[Orthographic View](../examples/3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look in games or CAD applications)
|
||||
|
@ -18,6 +18,7 @@ fn main() {
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 2000.,
|
||||
..default()
|
||||
})
|
||||
.add_plugins(DefaultPlugins)
|
||||
.init_resource::<ParticleAssets>()
|
||||
|
@ -13,6 +13,7 @@ fn main() {
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 150.0,
|
||||
..default()
|
||||
})
|
||||
.add_systems(Startup, setup)
|
||||
.run();
|
||||
|
@ -91,6 +91,7 @@ fn main() {
|
||||
.insert_resource(AmbientLight {
|
||||
color: WHITE.into(),
|
||||
brightness: 100.0,
|
||||
..default()
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ fn main() {
|
||||
.insert_resource(AmbientLight {
|
||||
color: WHITE.into(),
|
||||
brightness: 100.0,
|
||||
..default()
|
||||
})
|
||||
.init_resource::<AppState>()
|
||||
.run();
|
||||
|
@ -20,6 +20,7 @@ fn main() {
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 2000.,
|
||||
..default()
|
||||
})
|
||||
.add_systems(Startup, setup_assets)
|
||||
.add_systems(Startup, setup_scene)
|
||||
|