diff --git a/Cargo.toml b/Cargo.toml index 49ec8ddcf8..d531177230 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/assets/lightmaps/CornellBox-Box.zstd.ktx2 b/assets/lightmaps/CornellBox-Box.zstd.ktx2 new file mode 100644 index 0000000000..0a31dd4854 Binary files /dev/null and b/assets/lightmaps/CornellBox-Box.zstd.ktx2 differ diff --git a/assets/lightmaps/CornellBox-Large.zstd.ktx2 b/assets/lightmaps/CornellBox-Large.zstd.ktx2 new file mode 100644 index 0000000000..c41ef9b032 Binary files /dev/null and b/assets/lightmaps/CornellBox-Large.zstd.ktx2 differ diff --git a/assets/lightmaps/CornellBox-Small.zstd.ktx2 b/assets/lightmaps/CornellBox-Small.zstd.ktx2 new file mode 100644 index 0000000000..9b1b72cd13 Binary files /dev/null and b/assets/lightmaps/CornellBox-Small.zstd.ktx2 differ diff --git a/assets/models/CornellBox/CornellBox.glb b/assets/models/CornellBox/CornellBox.glb new file mode 100644 index 0000000000..9cb6661be3 Binary files /dev/null and b/assets/models/CornellBox/CornellBox.glb differ diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index ce104502c8..ddf95a2c9d 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -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::::default(), ExtractComponentPlugin::::default(), + LightmapPlugin, )) .configure_sets( PostUpdate, diff --git a/crates/bevy_pbr/src/lightmap/lightmap.wgsl b/crates/bevy_pbr/src/lightmap/lightmap.wgsl new file mode 100644 index 0000000000..cf3c2275c9 --- /dev/null +++ b/crates/bevy_pbr/src/lightmap/lightmap.wgsl @@ -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; +@group(1) @binding(5) var lightmaps_sampler: sampler; + +// Samples the lightmap, if any, and returns indirect illumination from it. +fn lightmap(uv: vec2, exposure: f32, instance_index: u32) -> vec3 { + let packed_uv_rect = mesh[instance_index].lightmap_uv_rect; + let uv_rect = vec4(vec4( + 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; +} diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs new file mode 100644 index 0000000000..3185507fbb --- /dev/null +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -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 = + 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, + + /// 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, + + /// 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, + + /// 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>, +} + +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::().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, + lightmaps: Extract>, + render_mesh_instances: Res, + images: Res>, + meshes: Res>, +) { + // 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, 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) -> 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), + } + } +} diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 779ee158e0..2a68125ddc 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -465,6 +465,7 @@ pub fn queue_material_meshes( mut render_mesh_instances: ResMut, render_material_instances: Res>, images: Res>, + render_lightmaps: Res, mut views: Query<( &ExtractedView, &VisibleEntities, @@ -613,6 +614,13 @@ pub fn queue_material_meshes( 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, diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index d65eb12afa..ef106c010e 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -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 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, } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 827f458982..a255afe818 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -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( render_mesh_instances: Res, render_materials: Res>, render_material_instances: Res>, + render_lightmaps: Res, mut views: Query< ( &ExtractedView, @@ -793,6 +799,18 @@ pub fn queue_prepass_material_meshes( 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, diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl index 08b5155de3..98795db5ac 100644 --- a/crates/bevy_pbr/src/prepass/prepass.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -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); diff --git a/crates/bevy_pbr/src/prepass/prepass_io.wgsl b/crates/bevy_pbr/src/prepass/prepass_io.wgsl index b66952f5bd..5abcfc1aed 100644 --- a/crates/bevy_pbr/src/prepass/prepass_io.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_io.wgsl @@ -10,20 +10,24 @@ struct Vertex { @location(1) uv: vec2, #endif +#ifdef VERTEX_UVS_B + @location(2) uv_b: vec2, +#endif + #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS - @location(2) normal: vec3, + @location(3) normal: vec3, #ifdef VERTEX_TANGENTS - @location(3) tangent: vec4, + @location(4) tangent: vec4, #endif #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef SKINNED - @location(4) joint_indices: vec4, - @location(5) joint_weights: vec4, + @location(5) joint_indices: vec4, + @location(6) joint_weights: vec4, #endif #ifdef VERTEX_COLORS - @location(6) color: vec4, + @location(7) color: vec4, #endif #ifdef MORPH_TARGETS @@ -40,27 +44,31 @@ struct VertexOutput { @location(0) uv: vec2, #endif +#ifdef VERTEX_UVS_B + @location(1) uv_b: vec2, +#endif + #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS - @location(1) world_normal: vec3, + @location(2) world_normal: vec3, #ifdef VERTEX_TANGENTS - @location(2) world_tangent: vec4, + @location(3) world_tangent: vec4, #endif #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS - @location(3) world_position: vec4, + @location(4) world_position: vec4, #ifdef MOTION_VECTOR_PREPASS - @location(4) previous_world_position: vec4, + @location(5) previous_world_position: vec4, #endif #ifdef DEPTH_CLAMP_ORTHO - @location(5) clip_position_unclamped: vec4, + @location(6) clip_position_unclamped: vec4, #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, + @location(8) color: vec4, #endif } diff --git a/crates/bevy_pbr/src/render/forward_io.wgsl b/crates/bevy_pbr/src/render/forward_io.wgsl index 97567486de..2c861784c1 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -11,7 +11,9 @@ struct Vertex { #ifdef VERTEX_UVS @location(2) uv: vec2, #endif -// (Alternate UVs are at location 3, but they're currently unused here.) +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif #ifdef VERTEX_TANGENTS @location(4) tangent: vec4, #endif @@ -36,14 +38,17 @@ struct VertexOutput { #ifdef VERTEX_UVS @location(2) uv: vec2, #endif +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif #ifdef VERTEX_TANGENTS - @location(3) world_tangent: vec4, + @location(4) world_tangent: vec4, #endif #ifdef VERTEX_COLORS - @location(4) color: vec4, + @location(5) color: vec4, #endif #ifdef VERTEX_OUTPUT_INSTANCE_INDEX - @location(5) @interpolate(flat) instance_index: u32, + @location(6) @interpolate(flat) instance_index: u32, #endif } diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index cb1579b376..c38c2445f2 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -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) -> 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; + type Param = (SRes, SRes); type Data = Entity; type Filter = With; - type CompareData = (MaterialBindGroupId, AssetId); + + // The material bind group ID, the mesh ID, and the lightmap ID, + // respectively. + type CompareData = (MaterialBindGroupId, AssetId, Option>); + type BufferData = MeshUniform; fn get_batch_data( - mesh_instances: &SystemParamItem, + (mesh_instances, lightmaps): &SystemParamItem, entity: &QueryItem, ) -> (Self::BufferData, Option) { 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, skinned: Option, morph_targets: HashMap, BindGroup>, + lightmaps: HashMap, 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, + lightmap: Option>, 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>, + images: Res>, mut groups: ResMut, mesh_pipeline: Res, render_device: Res, mesh_uniforms: Res>, skins_uniform: Res, weights_uniform: Res, + render_lightmaps: Res, ) { 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; @@ -1018,6 +1063,7 @@ impl RenderCommand

for SetMeshBindGroup { SRes, SRes, SRes, + SRes, ); type ViewData = (); type ItemData = (); @@ -1027,7 +1073,7 @@ impl RenderCommand

for SetMeshBindGroup { 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 RenderCommand

for SetMeshBindGroup { 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\ diff --git a/crates/bevy_pbr/src/render/mesh.wgsl b/crates/bevy_pbr/src/render/mesh.wgsl index e2a8041433..651de128cd 100644 --- a/crates/bevy_pbr/src/render/mesh.wgsl +++ b/crates/bevy_pbr/src/render/mesh.wgsl @@ -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, diff --git a/crates/bevy_pbr/src/render/mesh_bindings.rs b/crates/bevy_pbr/src/render/mesh_bindings.rs index f273da7bcb..60beb99115 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_bindings.rs @@ -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, diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 0da870acfe..89b73be2bd 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -5,6 +5,7 @@ struct Mesh { // Use bevy_render::maths::affine_to_square to unpack model: mat3x4, previous_model: mat3x4, + lightmap_uv_rect: vec2, // 3x3 matrix packed in mat2x4 and f32 as: // [0].xyz, [1].x, // [1].yz, [2].xy diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 6472f80d81..acc1b59421 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -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; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 66d815db7e..3f2e8c661f 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -358,6 +358,10 @@ fn apply_pbr_lighting( let specular_transmitted_environment_light = vec3(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 { diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index ff2fe88013..a4bde63333 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -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, + lightmap_light: vec3, is_orthographic: bool, flags: u32, }; @@ -110,6 +112,8 @@ fn pbr_input_new() -> PbrInput { pbr_input.N = vec3(0.0, 0.0, 1.0); pbr_input.V = vec3(1.0, 0.0, 0.0); + pbr_input.lightmap_light = vec3(0.0); + pbr_input.flags = 0u; return pbr_input; diff --git a/examples/3d/lightmaps.rs b/examples/3d/lightmaps.rs new file mode 100644 index 0000000000..ee4f0a1bbf --- /dev/null +++ b/examples/3d/lightmaps.rs @@ -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) { + 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, + meshes: Query<(Entity, &Name), (With>, Without)>, +) { + 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; + } + } +} diff --git a/examples/README.md b/examples/README.md index db5ce7f96c..7a0e2fa88d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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)