diff --git a/doc/classes/LightmapGI.xml b/doc/classes/LightmapGI.xml
index 16604aabf084..d80e8089390c 100644
--- a/doc/classes/LightmapGI.xml
+++ b/doc/classes/LightmapGI.xml
@@ -57,7 +57,7 @@
[b]Note:[/b] Regardless of [member generate_probes_subdiv], direct lighting on dynamic objects is always applied using [Light3D] nodes in real-time.
- If [code]true[/code], ignore environment lighting when baking lightmaps.
+ If [code]true[/code], considers this [LightmapGI] node to be used for indoor lighting. This means it will not consider environment lighting when blending multiple [LightmapGI] nodes at the same position. Additionally, baking lightmaps will make the lightmap probes located near the edges of the [LightmapGI] black regardless of how much lighting they receive. This prevent light leaks that make dynamic objects too bright due to the lighting being interpolated between those probes.
The [LightmapGIData] associated to this [LightmapGI] node. This resource is automatically created after baking, and is not meant to be created manually.
diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp
index 1e96efd8b957..829e70a986d8 100644
--- a/modules/lightmapper_rd/lightmapper_rd.cpp
+++ b/modules/lightmapper_rd/lightmapper_rd.cpp
@@ -53,6 +53,8 @@
//uncomment this if you want to see textures from all the process saved
//#define DEBUG_TEXTURES
+static constexpr float BOUNDS_GROW = 0.1f;
+
void LightmapperRD::add_mesh(const MeshData &p_mesh) {
ERR_FAIL_COND(p_mesh.albedo_on_uv2.is_null() || p_mesh.albedo_on_uv2->is_empty());
ERR_FAIL_COND(p_mesh.emission_on_uv2.is_null() || p_mesh.emission_on_uv2->is_empty());
@@ -500,7 +502,7 @@ void LightmapperRD::_create_acceleration_structures(RenderingDevice *rd, Size2i
Vector3 pp(p_probe_positions[i].position[0], p_probe_positions[i].position[1], p_probe_positions[i].position[2]);
bounds.expand_to(pp);
}
- bounds.grow_by(0.1); //grow a bit to avoid numerical error
+ bounds.grow_by(BOUNDS_GROW); //grow a bit to avoid numerical error
triangles.sort(); //sort by slice
seams.sort();
@@ -1045,7 +1047,7 @@ LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function, void *p_bake_userdata, float p_exposure_normalization, float p_supersampling_factor) {
+LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_denoiser, float p_denoiser_strength, int p_denoiser_range, int p_bounces, float p_bounce_indirect_energy, float p_bias, int p_max_texture_size, bool p_bake_sh, bool p_bake_shadowmask, bool p_texture_for_bounces, GenerateProbes p_generate_probes, const Ref &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function, void *p_bake_userdata, float p_exposure_normalization, float p_supersampling_factor, bool p_interior) {
int denoiser = GLOBAL_GET("rendering/lightmapping/denoising/denoiser");
String oidn_path = EDITOR_GET("filesystem/tools/oidn/oidn_denoise_path");
@@ -2297,11 +2299,39 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
}
if (probe_positions.size() > 0) {
- probe_values.resize(probe_positions.size() * 9);
+ // 9 colors are stored per probe for spherical harmonics.
+ constexpr int PROBE_COLOR_SIZE = 9;
+ probe_values.resize(probe_positions.size() * PROBE_COLOR_SIZE);
Vector probe_data = rd->buffer_get_data(light_probe_buffer);
memcpy(probe_values.ptrw(), probe_data.ptr(), probe_data.size());
rd->free(light_probe_buffer);
+ if (p_interior) {
+ // If baking lightmaps with Interior enabled, black out probes that are located near the bounds.
+ // This applies to the 8 bounds that are always generated, as well as the probes added by Generate Probes Subdiv
+ // (some of which are located on the edge of the AABB).
+ // This ensures environment lighting doesn't leak out to dynamic objects and make them too bright compared to the lightmap.
+ const Vector3 start = bounds.get_position();
+ const Vector3 end = bounds.get_end();
+
+ for (int i = 0; i < probe_values.size(); i += PROBE_COLOR_SIZE) {
+ constexpr float THRESHOLD = BOUNDS_GROW + CMP_EPSILON;
+ const Vector3 position = Vector3(
+ probe_positions[i / PROBE_COLOR_SIZE].position[0],
+ probe_positions[i / PROBE_COLOR_SIZE].position[1],
+ probe_positions[i / PROBE_COLOR_SIZE].position[2]);
+
+ if (Math::abs(position.x - start.x) <= THRESHOLD || Math::abs(position.x - end.x) <= THRESHOLD ||
+ Math::abs(position.y - start.y) <= THRESHOLD || Math::abs(position.y - end.y) <= THRESHOLD ||
+ Math::abs(position.z - start.z) <= THRESHOLD || Math::abs(position.z - end.z) <= THRESHOLD) {
+ // Black out the probe if it's located near the AABB's edge.
+ for (int j = 0; j < PROBE_COLOR_SIZE; j++) {
+ probe_values.write[i + j] = Color(0, 0, 0);
+ }
+ }
+ }
+ }
+
#ifdef DEBUG_TEXTURES
{
Ref img2 = Image::create_from_data(probe_values.size(), 1, false, Image::FORMAT_RGBAF, probe_data);
diff --git a/modules/lightmapper_rd/lightmapper_rd.h b/modules/lightmapper_rd/lightmapper_rd.h
index c856e21b9aa7..61440a70f0d5 100644
--- a/modules/lightmapper_rd/lightmapper_rd.h
+++ b/modules/lightmapper_rd/lightmapper_rd.h
@@ -294,7 +294,7 @@ class LightmapperRD : public Lightmapper {
virtual void add_omni_light(const String &p_name, bool p_static, const Vector3 &p_position, const Color &p_color, float p_energy, float p_indirect_energy, float p_range, float p_attenuation, float p_size, float p_shadow_blur) override;
virtual void add_spot_light(const String &p_name, bool p_static, const Vector3 &p_position, const Vector3 p_direction, const Color &p_color, float p_energy, float p_indirect_energy, float p_range, float p_attenuation, float p_spot_angle, float p_spot_attenuation, float p_size, float p_shadow_blur) override;
virtual void add_probe(const Vector3 &p_position) override;
- virtual BakeError bake(BakeQuality p_quality, bool p_use_denoiser, float p_denoiser_strength, int p_denoiser_range, int p_bounces, float p_bounce_indirect_energy, float p_bias, int p_max_texture_size, bool p_bake_sh, bool p_bake_shadowmask, bool p_texture_for_bounces, GenerateProbes p_generate_probes, const Ref &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function = nullptr, void *p_bake_userdata = nullptr, float p_exposure_normalization = 1.0, float p_supersampling_factor = 1.0f) override;
+ virtual BakeError bake(BakeQuality p_quality, bool p_use_denoiser, float p_denoiser_strength, int p_denoiser_range, int p_bounces, float p_bounce_indirect_energy, float p_bias, int p_max_texture_size, bool p_bake_sh, bool p_bake_shadowmask, bool p_texture_for_bounces, GenerateProbes p_generate_probes, const Ref &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function = nullptr, void *p_bake_userdata = nullptr, float p_exposure_normalization = 1.0, float p_supersampling_factor = 1.0f, bool p_interior = false) override;
int get_bake_texture_count() const override;
Ref get_bake_texture(int p_index) const override;
diff --git a/scene/3d/lightmap_gi.cpp b/scene/3d/lightmap_gi.cpp
index 8f21260d573b..59f5482c683f 100644
--- a/scene/3d/lightmap_gi.cpp
+++ b/scene/3d/lightmap_gi.cpp
@@ -1268,7 +1268,7 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa
Lightmapper::BakeError bake_err = lightmapper->bake(Lightmapper::BakeQuality(bake_quality), use_denoiser, denoiser_strength, denoiser_range, bounces,
bounce_indirect_energy, bias, max_texture_size, directional, shadowmask_mode != LightmapGIData::SHADOWMASK_MODE_NONE, use_texture_for_bounces,
- Lightmapper::GenerateProbes(gen_probes), environment_image, environment_transform, _lightmap_bake_step_function, &bsud, exposure_normalization, (supersampling_enabled ? supersampling_factor : 1));
+ Lightmapper::GenerateProbes(gen_probes), environment_image, environment_transform, _lightmap_bake_step_function, &bsud, exposure_normalization, (supersampling_enabled ? supersampling_factor : 1), interior);
if (bake_err == Lightmapper::BAKE_ERROR_TEXTURE_EXCEEDS_MAX_SIZE) {
return BAKE_ERROR_TEXTURE_SIZE_TOO_SMALL;
diff --git a/scene/3d/lightmapper.h b/scene/3d/lightmapper.h
index 46858873edd7..2b069d315d80 100644
--- a/scene/3d/lightmapper.h
+++ b/scene/3d/lightmapper.h
@@ -182,7 +182,7 @@ class Lightmapper : public RefCounted {
virtual void add_omni_light(const String &p_name, bool p_static, const Vector3 &p_position, const Color &p_color, float p_energy, float p_indirect_energy, float p_range, float p_attenuation, float p_size, float p_shadow_blur) = 0;
virtual void add_spot_light(const String &p_name, bool p_static, const Vector3 &p_position, const Vector3 p_direction, const Color &p_color, float p_energy, float p_indirect_energy, float p_range, float p_attenuation, float p_spot_angle, float p_spot_attenuation, float p_size, float p_shadow_blur) = 0;
virtual void add_probe(const Vector3 &p_position) = 0;
- virtual BakeError bake(BakeQuality p_quality, bool p_use_denoiser, float p_denoiser_strength, int p_denoiser_range, int p_bounces, float p_bounce_indirect_energy, float p_bias, int p_max_texture_size, bool p_bake_sh, bool p_bake_shadowmask, bool p_texture_for_bounces, GenerateProbes p_generate_probes, const Ref &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function = nullptr, void *p_step_userdata = nullptr, float p_exposure_normalization = 1.0, float p_supersampling_factor = 1.0) = 0;
+ virtual BakeError bake(BakeQuality p_quality, bool p_use_denoiser, float p_denoiser_strength, int p_denoiser_range, int p_bounces, float p_bounce_indirect_energy, float p_bias, int p_max_texture_size, bool p_bake_sh, bool p_bake_shadowmask, bool p_texture_for_bounces, GenerateProbes p_generate_probes, const Ref &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function = nullptr, void *p_step_userdata = nullptr, float p_exposure_normalization = 1.0, float p_supersampling_factor = 1.0, bool p_interior = false) = 0;
virtual int get_bake_texture_count() const = 0;
virtual Ref get_bake_texture(int p_index) const = 0;