Skip to content

Commit

Permalink
Implement lightmaps. (#10231)
Browse files Browse the repository at this point in the history
![Screenshot](https://i.imgur.com/A4KzWFq.png)

# Objective

Lightmaps, textures that store baked global illumination, have been a
mainstay of real-time graphics for decades. Bevy currently has no
support for them, so this pull request implements them.

## Solution

The new `Lightmap` component can be attached to any entity that contains
a `Handle<Mesh>` and a `StandardMaterial`. When present, it will be
applied in the PBR shader. Because multiple lightmaps are frequently
packed into atlases, each lightmap may have its own UV boundaries within
its texture. An `exposure` field is also provided, to control the
brightness of the lightmap.

Note that this PR doesn't provide any way to bake the lightmaps. That
can be done with [The Lightmapper] or another solution, such as Unity's
Bakery.

---

## Changelog

### Added
* A new component, `Lightmap`, is available, for baked global
illumination. If your mesh has a second UV channel (UV1), and you attach
this component to the entity with that mesh, Bevy will apply the texture
referenced in the lightmap.

[The Lightmapper]: https://github.com/Naxela/The_Lightmapper

---------

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
  • Loading branch information
pcwalton and cart authored Jan 2, 2024
1 parent 2440aa8 commit dd14f3a
Show file tree
Hide file tree
Showing 23 changed files with 536 additions and 45 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,17 @@ description = "Showcases wireframe rendering"
category = "3D Rendering"
wasm = false

[[example]]
name = "lightmaps"
path = "examples/3d/lightmaps.rs"
doc-scrape-examples = true

[package.metadata.example.lightmaps]
name = "Lightmaps"
description = "Rendering a scene with baked lightmaps"
category = "3D Rendering"
wasm = false

[[example]]
name = "no_prepass"
path = "tests/3d/no_prepass.rs"
Expand Down
Binary file added assets/lightmaps/CornellBox-Box.zstd.ktx2
Binary file not shown.
Binary file added assets/lightmaps/CornellBox-Large.zstd.ktx2
Binary file not shown.
Binary file added assets/lightmaps/CornellBox-Small.zstd.ktx2
Binary file not shown.
Binary file added assets/models/CornellBox/CornellBox.glb
Binary file not shown.
3 changes: 3 additions & 0 deletions crates/bevy_pbr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod environment_map;
mod extended_material;
mod fog;
mod light;
mod lightmap;
mod material;
mod parallax;
mod pbr_material;
Expand All @@ -20,6 +21,7 @@ pub use environment_map::EnvironmentMapLight;
pub use extended_material::*;
pub use fog::*;
pub use light::*;
pub use lightmap::*;
pub use material::*;
pub use parallax::*;
pub use pbr_material::*;
Expand Down Expand Up @@ -258,6 +260,7 @@ impl Plugin for PbrPlugin {
FogPlugin,
ExtractResourcePlugin::<DefaultOpaqueRendererMethod>::default(),
ExtractComponentPlugin::<ShadowFilteringMethod>::default(),
LightmapPlugin,
))
.configure_sets(
PostUpdate,
Expand Down
29 changes: 29 additions & 0 deletions crates/bevy_pbr/src/lightmap/lightmap.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#define_import_path bevy_pbr::lightmap

#import bevy_pbr::mesh_bindings::mesh

@group(1) @binding(4) var lightmaps_texture: texture_2d<f32>;
@group(1) @binding(5) var lightmaps_sampler: sampler;

// Samples the lightmap, if any, and returns indirect illumination from it.
fn lightmap(uv: vec2<f32>, exposure: f32, instance_index: u32) -> vec3<f32> {
let packed_uv_rect = mesh[instance_index].lightmap_uv_rect;
let uv_rect = vec4<f32>(vec4<u32>(
packed_uv_rect.x & 0xffffu,
packed_uv_rect.x >> 16u,
packed_uv_rect.y & 0xffffu,
packed_uv_rect.y >> 16u)) / 65535.0;

let lightmap_uv = mix(uv_rect.xy, uv_rect.zw, uv);

// Mipmapping lightmaps is usually a bad idea due to leaking across UV
// islands, so there's no harm in using mip level 0 and it lets us avoid
// control flow uniformity problems.
//
// TODO(pcwalton): Consider bicubic filtering.
return textureSampleLevel(
lightmaps_texture,
lightmaps_sampler,
lightmap_uv,
0.0).rgb * exposure;
}
210 changes: 210 additions & 0 deletions crates/bevy_pbr/src/lightmap/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
//! Lightmaps, baked lighting textures that can be applied at runtime to provide
//! diffuse global illumination.
//!
//! Bevy doesn't currently have any way to actually bake lightmaps, but they can
//! be baked in an external tool like [Blender](http://blender.org), for example
//! with an addon like [The Lightmapper]. The tools in the [`bevy-baked-gi`]
//! project support other lightmap baking methods.
//!
//! When a [`Lightmap`] component is added to an entity with a [`Mesh`] and a
//! [`StandardMaterial`](crate::StandardMaterial), Bevy applies the lightmap when rendering. The brightness
//! of the lightmap may be controlled with the `lightmap_exposure` field on
//! `StandardMaterial`.
//!
//! During the rendering extraction phase, we extract all lightmaps into the
//! [`RenderLightmaps`] table, which lives in the render world. Mesh bindgroup
//! and mesh uniform creation consults this table to determine which lightmap to
//! supply to the shader. Essentially, the lightmap is a special type of texture
//! that is part of the mesh instance rather than part of the material (because
//! multiple meshes can share the same material, whereas sharing lightmaps is
//! nonsensical).
//!
//! Note that meshes can't be instanced if they use different lightmap textures.
//! If you want to instance a lightmapped mesh, combine the lightmap textures
//! into a single atlas, and set the `uv_rect` field on [`Lightmap`]
//! appropriately.
//!
//! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper
//!
//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi

use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, AssetId, Handle};
use bevy_ecs::{
component::Component,
entity::Entity,
reflect::ReflectComponent,
schedule::IntoSystemConfigs,
system::{Query, Res, ResMut, Resource},
};
use bevy_math::{uvec2, vec4, Rect, UVec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
mesh::Mesh, render_asset::RenderAssets, render_resource::Shader, texture::Image,
view::ViewVisibility, Extract, ExtractSchedule, RenderApp,
};
use bevy_utils::{EntityHashMap, HashSet};

use crate::RenderMeshInstances;

/// The ID of the lightmap shader.
pub const LIGHTMAP_SHADER_HANDLE: Handle<Shader> =
Handle::weak_from_u128(285484768317531991932943596447919767152);

/// A plugin that provides an implementation of lightmaps.
pub struct LightmapPlugin;

/// A component that applies baked indirect diffuse global illumination from a
/// lightmap.
///
/// When assigned to an entity that contains a [`Mesh`] and a
/// [`StandardMaterial`](crate::StandardMaterial), if the mesh has a second UV
/// layer ([`ATTRIBUTE_UV_1`](bevy_render::mesh::Mesh::ATTRIBUTE_UV_1)), then
/// the lightmap will render using those UVs.
#[derive(Component, Clone, Reflect)]
#[reflect(Component, Default)]
pub struct Lightmap {
/// The lightmap texture.
pub image: Handle<Image>,

/// The rectangle within the lightmap texture that the UVs are relative to.
///
/// The top left coordinate is the `min` part of the rect, and the bottom
/// right coordinate is the `max` part of the rect. The rect ranges from (0,
/// 0) to (1, 1).
///
/// This field allows lightmaps for a variety of meshes to be packed into a
/// single atlas.
pub uv_rect: Rect,
}

/// Lightmap data stored in the render world.
///
/// There is one of these per visible lightmapped mesh instance.
#[derive(Debug)]
pub(crate) struct RenderLightmap {
/// The ID of the lightmap texture.
pub(crate) image: AssetId<Image>,

/// The rectangle within the lightmap texture that the UVs are relative to.
///
/// The top left coordinate is the `min` part of the rect, and the bottom
/// right coordinate is the `max` part of the rect. The rect ranges from (0,
/// 0) to (1, 1).
pub(crate) uv_rect: Rect,
}

/// Stores data for all lightmaps in the render world.
///
/// This is cleared and repopulated each frame during the `extract_lightmaps`
/// system.
#[derive(Default, Resource)]
pub struct RenderLightmaps {
/// The mapping from every lightmapped entity to its lightmap info.
///
/// Entities without lightmaps, or for which the mesh or lightmap isn't
/// loaded, won't have entries in this table.
pub(crate) render_lightmaps: EntityHashMap<Entity, RenderLightmap>,

/// All active lightmap images in the scene.
///
/// Gathering all lightmap images into a set makes mesh bindgroup
/// preparation slightly more efficient, because only one bindgroup needs to
/// be created per lightmap texture.
pub(crate) all_lightmap_images: HashSet<AssetId<Image>>,
}

impl Plugin for LightmapPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
LIGHTMAP_SHADER_HANDLE,
"lightmap.wgsl",
Shader::from_wgsl
);
}

fn finish(&self, app: &mut App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};

render_app.init_resource::<RenderLightmaps>().add_systems(
ExtractSchedule,
extract_lightmaps.after(crate::extract_meshes),
);
}
}

/// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`]
/// resource.
fn extract_lightmaps(
mut render_lightmaps: ResMut<RenderLightmaps>,
lightmaps: Extract<Query<(Entity, &ViewVisibility, &Lightmap)>>,
render_mesh_instances: Res<RenderMeshInstances>,
images: Res<RenderAssets<Image>>,
meshes: Res<RenderAssets<Mesh>>,
) {
// Clear out the old frame's data.
render_lightmaps.render_lightmaps.clear();
render_lightmaps.all_lightmap_images.clear();

// Loop over each entity.
for (entity, view_visibility, lightmap) in lightmaps.iter() {
// Only process visible entities for which the mesh and lightmap are
// both loaded.
if !view_visibility.get()
|| images.get(&lightmap.image).is_none()
|| !render_mesh_instances
.get(&entity)
.and_then(|mesh_instance| meshes.get(mesh_instance.mesh_asset_id))
.is_some_and(|mesh| mesh.layout.contains(Mesh::ATTRIBUTE_UV_1.id))
{
continue;
}

// Store information about the lightmap in the render world.
render_lightmaps.render_lightmaps.insert(
entity,
RenderLightmap::new(lightmap.image.id(), lightmap.uv_rect),
);

// Make a note of the loaded lightmap image so we can efficiently
// process them later during mesh bindgroup creation.
render_lightmaps
.all_lightmap_images
.insert(lightmap.image.id());
}
}

impl RenderLightmap {
/// Creates a new lightmap from a texture and a UV rect.
fn new(image: AssetId<Image>, uv_rect: Rect) -> Self {
Self { image, uv_rect }
}
}

/// Packs the lightmap UV rect into 64 bits (4 16-bit unsigned integers).
pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option<Rect>) -> UVec2 {
match maybe_rect {
Some(rect) => {
let rect_uvec4 = (vec4(rect.min.x, rect.min.y, rect.max.x, rect.max.y) * 65535.0)
.round()
.as_uvec4();
uvec2(
rect_uvec4.x | (rect_uvec4.y << 16),
rect_uvec4.z | (rect_uvec4.w << 16),
)
}
None => UVec2::ZERO,
}
}

impl Default for Lightmap {
fn default() -> Self {
Self {
image: Default::default(),
uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0),
}
}
}
8 changes: 8 additions & 0 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ pub fn queue_material_meshes<M: Material>(
mut render_mesh_instances: ResMut<RenderMeshInstances>,
render_material_instances: Res<RenderMaterialInstances<M>>,
images: Res<RenderAssets<Image>>,
render_lightmaps: Res<RenderLightmaps>,
mut views: Query<(
&ExtractedView,
&VisibleEntities,
Expand Down Expand Up @@ -613,6 +614,13 @@ pub fn queue_material_meshes<M: Material>(

mesh_key |= alpha_mode_pipeline_key(material.properties.alpha_mode);

if render_lightmaps
.render_lightmaps
.contains_key(visible_entity)
{
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
}

let pipeline_id = pipelines.specialize(
&pipeline_cache,
&material_pipeline,
Expand Down
7 changes: 7 additions & 0 deletions crates/bevy_pbr/src/pbr_material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,9 @@ pub struct StandardMaterial {
/// Default is `16.0`.
pub max_parallax_layer_count: f32,

/// The exposure (brightness) level of the lightmap, if present.
pub lightmap_exposure: f32,

/// Render method used for opaque materials. (Where `alpha_mode` is [`AlphaMode::Opaque`] or [`AlphaMode::Mask`])
pub opaque_render_method: OpaqueRendererMethod,

Expand Down Expand Up @@ -513,6 +516,7 @@ impl Default for StandardMaterial {
depth_map: None,
parallax_depth_scale: 0.1,
max_parallax_layer_count: 16.0,
lightmap_exposure: 1.0,
parallax_mapping_method: ParallaxMappingMethod::Occlusion,
opaque_render_method: OpaqueRendererMethod::Auto,
deferred_lighting_pass_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID,
Expand Down Expand Up @@ -621,6 +625,8 @@ pub struct StandardMaterialUniform {
/// If your `parallax_depth_scale` is >0.1 and you are seeing jaggy edges,
/// increase this value. However, this incurs a performance cost.
pub max_parallax_layer_count: f32,
/// The exposure (brightness) level of the lightmap, if present.
pub lightmap_exposure: f32,
/// Using [`ParallaxMappingMethod::Relief`], how many additional
/// steps to use at most to find the depth value.
pub max_relief_mapping_search_steps: u32,
Expand Down Expand Up @@ -720,6 +726,7 @@ impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
alpha_cutoff,
parallax_depth_scale: self.parallax_depth_scale,
max_parallax_layer_count: self.max_parallax_layer_count,
lightmap_exposure: self.lightmap_exposure,
max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(),
deferred_lighting_pass_id: self.deferred_lighting_pass_id as u32,
}
Expand Down
Loading

0 comments on commit dd14f3a

Please sign in to comment.