Implement lightmaps. (#10231)

![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>
This commit is contained in:
Patrick Walton 2024-01-02 12:38:47 -08:00 committed by GitHub
parent 2440aa8475
commit dd14f3a477
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 536 additions and 45 deletions

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -7,6 +7,7 @@ mod environment_map;
mod extended_material;
mod fog;
mod light;
mod lightmap;
mod material;
mod parallax;
mod pbr_material;
@ -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::*;
@ -258,6 +260,7 @@ impl Plugin for PbrPlugin {
FogPlugin,
ExtractResourcePlugin::<DefaultOpaqueRendererMethod>::default(),
ExtractComponentPlugin::<ShadowFilteringMethod>::default(),
LightmapPlugin,
))
.configure_sets(
PostUpdate,

View file

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

View file

@ -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),
}
}
}

View file

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

View file

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

View file

@ -366,6 +366,11 @@ where
vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(1));
}
if layout.contains(Mesh::ATTRIBUTE_UV_1) {
shader_defs.push("VERTEX_UVS_B".into());
vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(2));
}
if key.mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) {
shader_defs.push("NORMAL_PREPASS".into());
}
@ -374,11 +379,11 @@ where
.mesh_key
.intersects(MeshPipelineKey::NORMAL_PREPASS | MeshPipelineKey::DEFERRED_PREPASS)
{
vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(2));
vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(3));
shader_defs.push("NORMAL_PREPASS_OR_DEFERRED_PREPASS".into());
if layout.contains(Mesh::ATTRIBUTE_TANGENT) {
shader_defs.push("VERTEX_TANGENTS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3));
vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4));
}
}
@ -395,7 +400,7 @@ where
if layout.contains(Mesh::ATTRIBUTE_COLOR) {
shader_defs.push("VERTEX_COLORS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(6));
vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(7));
}
if key
@ -681,6 +686,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
render_mesh_instances: Res<RenderMeshInstances>,
render_materials: Res<RenderMaterials<M>>,
render_material_instances: Res<RenderMaterialInstances<M>>,
render_lightmaps: Res<RenderLightmaps>,
mut views: Query<
(
&ExtractedView,
@ -793,6 +799,18 @@ pub fn queue_prepass_material_meshes<M: Material>(
mesh_key |= MeshPipelineKey::DEFERRED_PREPASS;
}
// Even though we don't use the lightmap in the prepass, the
// `SetMeshBindGroup` render command will bind the data for it. So
// we need to include the appropriate flag in the mesh pipeline key
// to ensure that the necessary bind group layout entries are
// present.
if render_lightmaps
.render_lightmaps
.contains_key(visible_entity)
{
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
}
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&prepass_pipeline,

View file

@ -62,6 +62,10 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
out.uv = vertex.uv;
#endif // VERTEX_UVS
#ifdef VERTEX_UVS_B
out.uv_b = vertex.uv_b;
#endif // VERTEX_UVS_B
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
#ifdef SKINNED
out.world_normal = skinning::skin_normals(model, vertex.normal);

View file

@ -10,20 +10,24 @@ struct Vertex {
@location(1) uv: vec2<f32>,
#endif
#ifdef VERTEX_UVS_B
@location(2) uv_b: vec2<f32>,
#endif
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
@location(2) normal: vec3<f32>,
@location(3) normal: vec3<f32>,
#ifdef VERTEX_TANGENTS
@location(3) tangent: vec4<f32>,
@location(4) tangent: vec4<f32>,
#endif
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
#ifdef SKINNED
@location(4) joint_indices: vec4<u32>,
@location(5) joint_weights: vec4<f32>,
@location(5) joint_indices: vec4<u32>,
@location(6) joint_weights: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(6) color: vec4<f32>,
@location(7) color: vec4<f32>,
#endif
#ifdef MORPH_TARGETS
@ -40,27 +44,31 @@ struct VertexOutput {
@location(0) uv: vec2<f32>,
#endif
#ifdef VERTEX_UVS_B
@location(1) uv_b: vec2<f32>,
#endif
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
@location(1) world_normal: vec3<f32>,
@location(2) world_normal: vec3<f32>,
#ifdef VERTEX_TANGENTS
@location(2) world_tangent: vec4<f32>,
@location(3) world_tangent: vec4<f32>,
#endif
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
@location(3) world_position: vec4<f32>,
@location(4) world_position: vec4<f32>,
#ifdef MOTION_VECTOR_PREPASS
@location(4) previous_world_position: vec4<f32>,
@location(5) previous_world_position: vec4<f32>,
#endif
#ifdef DEPTH_CLAMP_ORTHO
@location(5) clip_position_unclamped: vec4<f32>,
@location(6) clip_position_unclamped: vec4<f32>,
#endif // DEPTH_CLAMP_ORTHO
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
@location(6) instance_index: u32,
@location(7) instance_index: u32,
#endif
#ifdef VERTEX_COLORS
@location(7) color: vec4<f32>,
@location(8) color: vec4<f32>,
#endif
}

View file

@ -11,7 +11,9 @@ struct Vertex {
#ifdef VERTEX_UVS
@location(2) uv: vec2<f32>,
#endif
// (Alternate UVs are at location 3, but they're currently unused here.)
#ifdef VERTEX_UVS_B
@location(3) uv_b: vec2<f32>,
#endif
#ifdef VERTEX_TANGENTS
@location(4) tangent: vec4<f32>,
#endif
@ -36,14 +38,17 @@ struct VertexOutput {
#ifdef VERTEX_UVS
@location(2) uv: vec2<f32>,
#endif
#ifdef VERTEX_UVS_B
@location(3) uv_b: vec2<f32>,
#endif
#ifdef VERTEX_TANGENTS
@location(3) world_tangent: vec4<f32>,
@location(4) world_tangent: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(4) color: vec4<f32>,
@location(5) color: vec4<f32>,
#endif
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
@location(5) @interpolate(flat) instance_index: u32,
@location(6) @interpolate(flat) instance_index: u32,
#endif
}

View file

@ -10,7 +10,7 @@ use bevy_ecs::{
query::{QueryItem, ROQueryItem},
system::{lifetimeless::*, SystemParamItem, SystemState},
};
use bevy_math::{Affine3, Vec4};
use bevy_math::{Affine3, Rect, UVec2, Vec4};
use bevy_render::{
batching::{
batch_and_prepare_render_phase, write_batched_instance_buffer, GetBatchData,
@ -26,7 +26,7 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::GlobalTransform;
use bevy_utils::{tracing::error, EntityHashMap, HashMap, Hashed};
use bevy_utils::{tracing::error, EntityHashMap, Entry, HashMap, Hashed};
use std::cell::Cell;
use thread_local::ThreadLocal;
@ -195,6 +195,16 @@ pub struct MeshUniform {
// Affine 4x3 matrices transposed to 3x4
pub transform: [Vec4; 3],
pub previous_transform: [Vec4; 3],
// Four 16-bit unsigned normalized UV values packed into a `UVec2`:
//
// <--- MSB LSB --->
// +---- min v ----+ +---- min u ----+
// lightmap_uv_rect.x: vvvvvvvv vvvvvvvv uuuuuuuu uuuuuuuu,
// +---- max v ----+ +---- max u ----+
// lightmap_uv_rect.y: VVVVVVVV VVVVVVVV UUUUUUUU UUUUUUUU,
//
// (MSB: most significant bit; LSB: least significant bit.)
pub lightmap_uv_rect: UVec2,
// 3x3 matrix packed in mat2x4 and f32 as:
// [0].xyz, [1].x,
// [1].yz, [2].xy
@ -204,13 +214,14 @@ pub struct MeshUniform {
pub flags: u32,
}
impl From<&MeshTransforms> for MeshUniform {
fn from(mesh_transforms: &MeshTransforms) -> Self {
impl MeshUniform {
fn new(mesh_transforms: &MeshTransforms, maybe_lightmap_uv_rect: Option<Rect>) -> Self {
let (inverse_transpose_model_a, inverse_transpose_model_b) =
mesh_transforms.transform.inverse_transpose_3x3();
Self {
transform: mesh_transforms.transform.to_transpose(),
previous_transform: mesh_transforms.previous_transform.to_transpose(),
lightmap_uv_rect: lightmap::pack_lightmap_uv_rect(maybe_lightmap_uv_rect),
inverse_transpose_model_a,
inverse_transpose_model_b,
flags: mesh_transforms.flags,
@ -447,24 +458,34 @@ impl MeshPipeline {
}
impl GetBatchData for MeshPipeline {
type Param = SRes<RenderMeshInstances>;
type Param = (SRes<RenderMeshInstances>, SRes<RenderLightmaps>);
type Data = Entity;
type Filter = With<Mesh3d>;
type CompareData = (MaterialBindGroupId, AssetId<Mesh>);
// The material bind group ID, the mesh ID, and the lightmap ID,
// respectively.
type CompareData = (MaterialBindGroupId, AssetId<Mesh>, Option<AssetId<Image>>);
type BufferData = MeshUniform;
fn get_batch_data(
mesh_instances: &SystemParamItem<Self::Param>,
(mesh_instances, lightmaps): &SystemParamItem<Self::Param>,
entity: &QueryItem<Self::Data>,
) -> (Self::BufferData, Option<Self::CompareData>) {
let mesh_instance = mesh_instances
.get(entity)
.expect("Failed to find render mesh instance");
let maybe_lightmap = lightmaps.render_lightmaps.get(entity);
(
(&mesh_instance.transforms).into(),
MeshUniform::new(
&mesh_instance.transforms,
maybe_lightmap.map(|lightmap| lightmap.uv_rect),
),
mesh_instance.automatic_batching.then_some((
mesh_instance.material_bind_group_id,
mesh_instance.mesh_asset_id,
maybe_lightmap.map(|lightmap| lightmap.image),
)),
)
}
@ -492,6 +513,7 @@ bitflags::bitflags! {
const TEMPORAL_JITTER = 1 << 11;
const MORPH_TARGETS = 1 << 12;
const READS_VIEW_TRANSMISSION_TEXTURE = 1 << 13;
const LIGHTMAPPED = 1 << 14;
const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state
const BLEND_OPAQUE = 0 << Self::BLEND_SHIFT_BITS; // ← Values are just sequential within the mask, and can range from 0 to 3
const BLEND_PREMULTIPLIED_ALPHA = 1 << Self::BLEND_SHIFT_BITS; //
@ -609,21 +631,23 @@ pub fn setup_morph_and_skinning_defs(
vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(offset + 1));
};
let is_morphed = key.intersects(MeshPipelineKey::MORPH_TARGETS);
match (is_skinned(layout), is_morphed) {
(true, false) => {
let is_lightmapped = key.intersects(MeshPipelineKey::LIGHTMAPPED);
match (is_skinned(layout), is_morphed, is_lightmapped) {
(true, false, _) => {
add_skin_data();
mesh_layouts.skinned.clone()
}
(true, true) => {
(true, true, _) => {
add_skin_data();
shader_defs.push("MORPH_TARGETS".into());
mesh_layouts.morphed_skinned.clone()
}
(false, true) => {
(false, true, _) => {
shader_defs.push("MORPH_TARGETS".into());
mesh_layouts.morphed.clone()
}
(false, false) => mesh_layouts.model_only.clone(),
(false, false, true) => mesh_layouts.lightmapped.clone(),
(false, false, false) => mesh_layouts.model_only.clone(),
}
}
@ -659,7 +683,7 @@ impl SpecializedMeshPipeline for MeshPipeline {
}
if layout.contains(Mesh::ATTRIBUTE_UV_1) {
shader_defs.push("VERTEX_UVS_1".into());
shader_defs.push("VERTEX_UVS_B".into());
vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(3));
}
@ -810,6 +834,10 @@ impl SpecializedMeshPipeline for MeshPipeline {
shader_defs.push("ENVIRONMENT_MAP".into());
}
if key.contains(MeshPipelineKey::LIGHTMAPPED) {
shader_defs.push("LIGHTMAP".into());
}
if key.contains(MeshPipelineKey::TEMPORAL_JITTER) {
shader_defs.push("TEMPORAL_JITTER".into());
}
@ -922,36 +950,44 @@ pub struct MeshBindGroups {
model_only: Option<BindGroup>,
skinned: Option<BindGroup>,
morph_targets: HashMap<AssetId<Mesh>, BindGroup>,
lightmaps: HashMap<AssetId<Image>, BindGroup>,
}
impl MeshBindGroups {
pub fn reset(&mut self) {
self.model_only = None;
self.skinned = None;
self.morph_targets.clear();
self.lightmaps.clear();
}
/// Get the `BindGroup` for `GpuMesh` with given `handle_id`.
/// Get the `BindGroup` for `GpuMesh` with given `handle_id` and lightmap
/// key `lightmap`.
pub fn get(
&self,
asset_id: AssetId<Mesh>,
lightmap: Option<AssetId<Image>>,
is_skinned: bool,
morph: bool,
) -> Option<&BindGroup> {
match (is_skinned, morph) {
(_, true) => self.morph_targets.get(&asset_id),
(true, false) => self.skinned.as_ref(),
(false, false) => self.model_only.as_ref(),
match (is_skinned, morph, lightmap) {
(_, true, _) => self.morph_targets.get(&asset_id),
(true, false, _) => self.skinned.as_ref(),
(false, false, Some(lightmap)) => self.lightmaps.get(&lightmap),
(false, false, None) => self.model_only.as_ref(),
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_mesh_bind_group(
meshes: Res<RenderAssets<Mesh>>,
images: Res<RenderAssets<Image>>,
mut groups: ResMut<MeshBindGroups>,
mesh_pipeline: Res<MeshPipeline>,
render_device: Res<RenderDevice>,
mesh_uniforms: Res<GpuArrayBuffer<MeshUniform>>,
skins_uniform: Res<SkinUniform>,
weights_uniform: Res<MorphUniform>,
render_lightmaps: Res<RenderLightmaps>,
) {
groups.reset();
let layouts = &mesh_pipeline.mesh_layouts;
@ -977,6 +1013,15 @@ pub fn prepare_mesh_bind_group(
}
}
}
// Create lightmap bindgroups.
for &image_id in &render_lightmaps.all_lightmap_images {
if let (Entry::Vacant(entry), Some(image)) =
(groups.lightmaps.entry(image_id), images.get(image_id))
{
entry.insert(layouts.lightmapped(&render_device, &model, image));
}
}
}
pub struct SetMeshViewBindGroup<const I: usize>;
@ -1018,6 +1063,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
SRes<RenderMeshInstances>,
SRes<SkinIndices>,
SRes<MorphIndices>,
SRes<RenderLightmaps>,
);
type ViewData = ();
type ItemData = ();
@ -1027,7 +1073,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
item: &P,
_view: (),
_item_query: (),
(bind_groups, mesh_instances, skin_indices, morph_indices): SystemParamItem<
(bind_groups, mesh_instances, skin_indices, morph_indices, lightmaps): SystemParamItem<
'w,
'_,
Self::Param,
@ -1050,7 +1096,14 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
let is_skinned = skin_index.is_some();
let is_morphed = morph_index.is_some();
let Some(bind_group) = bind_groups.get(mesh.mesh_asset_id, is_skinned, is_morphed) else {
let lightmap = lightmaps
.render_lightmaps
.get(entity)
.map(|render_lightmap| render_lightmap.image);
let Some(bind_group) =
bind_groups.get(mesh.mesh_asset_id, lightmap, is_skinned, is_morphed)
else {
error!(
"The MeshBindGroups resource wasn't set in the render phase. \
It should be set by the queue_mesh_bind_group system.\n\

View file

@ -68,6 +68,10 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
out.uv = vertex.uv;
#endif
#ifdef VERTEX_UVS_B
out.uv_b = vertex.uv_b;
#endif
#ifdef VERTEX_TANGENTS
out.world_tangent = mesh_functions::mesh_tangent_local_to_world(
model,

View file

@ -1,7 +1,9 @@
//! Bind group layout related definitions for the mesh pipeline.
use bevy_math::Mat4;
use bevy_render::{mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice};
use bevy_render::{
mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice, texture::GpuImage,
};
use crate::render::skin::MAX_JOINTS;
@ -17,9 +19,9 @@ mod layout_entry {
use crate::MeshUniform;
use bevy_render::{
render_resource::{
binding_types::{texture_3d, uniform_buffer_sized},
BindGroupLayoutEntryBuilder, BufferSize, GpuArrayBuffer, ShaderStages,
TextureSampleType,
binding_types::{sampler, texture_2d, texture_3d, uniform_buffer_sized},
BindGroupLayoutEntryBuilder, BufferSize, GpuArrayBuffer, SamplerBindingType,
ShaderStages, TextureSampleType,
},
renderer::RenderDevice,
};
@ -37,6 +39,12 @@ mod layout_entry {
pub(super) fn targets() -> BindGroupLayoutEntryBuilder {
texture_3d(TextureSampleType::Float { filterable: false })
}
pub(super) fn lightmaps_texture_view() -> BindGroupLayoutEntryBuilder {
texture_2d(TextureSampleType::Float { filterable: true }).visibility(ShaderStages::FRAGMENT)
}
pub(super) fn lightmaps_sampler() -> BindGroupLayoutEntryBuilder {
sampler(SamplerBindingType::Filtering).visibility(ShaderStages::FRAGMENT)
}
}
/// Individual [`BindGroupEntry`]
@ -44,7 +52,7 @@ mod layout_entry {
mod entry {
use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE};
use bevy_render::render_resource::{
BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, TextureView,
BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, Sampler, TextureView,
};
fn entry(binding: u32, size: u64, buffer: &Buffer) -> BindGroupEntry {
@ -72,6 +80,18 @@ mod entry {
resource: BindingResource::TextureView(texture),
}
}
pub(super) fn lightmaps_texture_view(binding: u32, texture: &TextureView) -> BindGroupEntry {
BindGroupEntry {
binding,
resource: BindingResource::TextureView(texture),
}
}
pub(super) fn lightmaps_sampler(binding: u32, sampler: &Sampler) -> BindGroupEntry {
BindGroupEntry {
binding,
resource: BindingResource::Sampler(sampler),
}
}
}
/// All possible [`BindGroupLayout`]s in bevy's default mesh shader (`mesh.wgsl`).
@ -80,6 +100,9 @@ pub struct MeshLayouts {
/// The mesh model uniform (transform) and nothing else.
pub model_only: BindGroupLayout,
/// Includes the lightmap texture and uniform.
pub lightmapped: BindGroupLayout,
/// Also includes the uniform for skinning
pub skinned: BindGroupLayout,
@ -102,6 +125,7 @@ impl MeshLayouts {
pub fn new(render_device: &RenderDevice) -> Self {
MeshLayouts {
model_only: Self::model_only_layout(render_device),
lightmapped: Self::lightmapped_layout(render_device),
skinned: Self::skinned_layout(render_device),
morphed: Self::morphed_layout(render_device),
morphed_skinned: Self::morphed_skinned_layout(render_device),
@ -158,6 +182,19 @@ impl MeshLayouts {
),
)
}
fn lightmapped_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"lightmapped_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
(4, layout_entry::lightmaps_texture_view()),
(5, layout_entry::lightmaps_sampler()),
),
),
)
}
// ---------- BindGroup methods ----------
@ -168,6 +205,22 @@ impl MeshLayouts {
&[entry::model(0, model.clone())],
)
}
pub fn lightmapped(
&self,
render_device: &RenderDevice,
model: &BindingResource,
lightmap: &GpuImage,
) -> BindGroup {
render_device.create_bind_group(
"lightmapped_mesh_bind_group",
&self.lightmapped,
&[
entry::model(0, model.clone()),
entry::lightmaps_texture_view(4, &lightmap.texture_view),
entry::lightmaps_sampler(5, &lightmap.sampler),
],
)
}
pub fn skinned(
&self,
render_device: &RenderDevice,

View file

@ -5,6 +5,7 @@ struct Mesh {
// Use bevy_render::maths::affine_to_square to unpack
model: mat3x4<f32>,
previous_model: mat3x4<f32>,
lightmap_uv_rect: vec2<u32>,
// 3x3 matrix packed in mat2x4 and f32 as:
// [0].xyz, [1].x,
// [1].yz, [2].xy

View file

@ -8,6 +8,7 @@
mesh_bindings::mesh,
mesh_view_bindings::view,
parallax_mapping::parallaxed_uv,
lightmap::lightmap,
}
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
@ -191,6 +192,13 @@ fn pbr_input_from_standard_material(
view.mip_bias,
);
#endif
#ifdef LIGHTMAP
pbr_input.lightmap_light = lightmap(
in.uv_b,
pbr_bindings::material.lightmap_exposure,
in.instance_index);
#endif
}
return pbr_input;

View file

@ -358,6 +358,10 @@ fn apply_pbr_lighting(
let specular_transmitted_environment_light = vec3<f32>(0.0);
#endif
#ifdef LIGHTMAP
indirect_light += in.lightmap_light * diffuse_color;
#endif
let emissive_light = emissive.rgb * output_color.a;
if specular_transmission > 0.0 {

View file

@ -17,6 +17,7 @@ struct StandardMaterial {
alpha_cutoff: f32,
parallax_depth_scale: f32,
max_parallax_layer_count: f32,
lightmap_exposure: f32,
max_relief_mapping_search_steps: u32,
/// ID for specifying which deferred lighting pass should be used for rendering this material, if any.
deferred_lighting_pass_id: u32,
@ -90,6 +91,7 @@ struct PbrInput {
// Normalized view vector in world space, pointing from the fragment world position toward the
// view world position
V: vec3<f32>,
lightmap_light: vec3<f32>,
is_orthographic: bool,
flags: u32,
};
@ -110,6 +112,8 @@ fn pbr_input_new() -> PbrInput {
pbr_input.N = vec3<f32>(0.0, 0.0, 1.0);
pbr_input.V = vec3<f32>(1.0, 0.0, 0.0);
pbr_input.lightmap_light = vec3<f32>(0.0);
pbr_input.flags = 0u;
return pbr_input;

60
examples/3d/lightmaps.rs Normal file
View file

@ -0,0 +1,60 @@
//! Rendering a scene with baked lightmaps.
use bevy::pbr::Lightmap;
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 0.2,
})
.add_systems(Startup, setup)
.add_systems(Update, add_lightmaps_to_meshes)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(SceneBundle {
scene: asset_server.load("models/CornellBox/CornellBox.glb#Scene0"),
..default()
});
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-278.0, 273.0, 800.0),
..default()
});
}
fn add_lightmaps_to_meshes(
mut commands: Commands,
asset_server: Res<AssetServer>,
meshes: Query<(Entity, &Name), (With<Handle<Mesh>>, Without<Lightmap>)>,
) {
for (entity, name) in meshes.iter() {
if &**name == "large_box" {
commands.entity(entity).insert(Lightmap {
image: asset_server.load("lightmaps/CornellBox-Large.zstd.ktx2"),
..default()
});
continue;
}
if &**name == "small_box" {
commands.entity(entity).insert(Lightmap {
image: asset_server.load("lightmaps/CornellBox-Small.zstd.ktx2"),
..default()
});
continue;
}
if name.starts_with("cornell_box") {
commands.entity(entity).insert(Lightmap {
image: asset_server.load("lightmaps/CornellBox-Box.zstd.ktx2"),
..default()
});
continue;
}
}
}

View file

@ -126,6 +126,7 @@ Example | Description
[Fog](../examples/3d/fog.rs) | A scene showcasing the distance fog effect
[Generate Custom Mesh](../examples/3d/generate_custom_mesh.rs) | Simple showcase of how to generate a custom mesh with a custom texture
[Lighting](../examples/3d/lighting.rs) | Illustrates various lighting options in a simple scene
[Lightmaps](../examples/3d/lightmaps.rs) | Rendering a scene with baked lightmaps
[Lines](../examples/3d/lines.rs) | Create a custom material to draw 3d lines
[Load glTF](../examples/3d/load_gltf.rs) | Loads and renders a glTF file as a scene
[Orthographic View](../examples/3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look in games or CAD applications)