From c568c476f77265192d6dd00369cee3bbe7a22723 Mon Sep 17 00:00:00 2001 From: Hugo Locurcio Date: Tue, 14 May 2024 00:23:25 +0200 Subject: [PATCH] Add an option to downsample on bake in LightmapGI This provides increased lightmap quality with less noise, smoother shadows and better small-scale shadow detail. The downside is that this significantly increases bake times and memory usage while baking lightmaps, so this option is disabled by default. --- doc/classes/LightmapGI.xml | 6 +++++- modules/lightmapper_rd/lightmapper_rd.cpp | 13 ++++++++----- modules/lightmapper_rd/lightmapper_rd.h | 4 ++-- scene/3d/lightmap_gi.cpp | 22 +++++++++++++++++++++- scene/3d/lightmap_gi.h | 4 ++++ scene/3d/lightmapper.h | 2 +- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/doc/classes/LightmapGI.xml b/doc/classes/LightmapGI.xml index e7d44411efd4..23873c8606aa 100644 --- a/doc/classes/LightmapGI.xml +++ b/doc/classes/LightmapGI.xml @@ -38,6 +38,9 @@ If [code]true[/code], bakes lightmaps to contain directional information as spherical harmonics. This results in more realistic lighting appearance, especially with normal mapped materials and for lights that have their direct light baked ([member Light3D.light_bake_mode] set to [constant Light3D.BAKE_STATIC] and with [member Light3D.editor_only] set to [code]false[/code]). The directional information is also used to provide rough reflections for static and dynamic objects. This has a small run-time performance cost as the shader has to perform more work to interpret the direction information from the lightmap. Directional lightmaps also take longer to bake and result in larger file sizes. [b]Note:[/b] The property's name has no relationship with [DirectionalLight3D]. [member directional] works with all light types. + + If [code]true[/code], bakes lightmaps with the texel scale doubled and halves the texture size before saving the lightmap (so the effective texel density is identical to having downsampling disabled). Downsampling provides increased lightmap quality with less noise, smoother shadows and better shadowing of small-scale features in objects. However, downsampling results in significantly increased bake times and memory usage while baking lightmaps. Padding is automatically adjusted to avoid increasing light leaking. Downsampling generally does not affect output file size significantly. + The color to use for environment lighting. Only effective if [member environment_mode] is [constant ENVIRONMENT_MODE_CUSTOM_COLOR]. @@ -66,7 +69,8 @@ The quality preset to use when baking lightmaps. This affects bake times, but output file sizes remain mostly identical across quality levels. - To further speed up bake times, decrease [member bounces], disable [member use_denoiser] and increase the lightmap texel size on 3D scenes in the Import doc. + To further speed up bake times, decrease [member bounces], disable [member use_denoiser] and/or decrease [member texel_scale]. + To further increase quality, enable [member downsample] and/or increase [member texel_scale]. Scales the lightmap texel density of all meshes for the current bake. This is a multiplier that builds upon the existing lightmap texel size defined in each imported 3D scene, along with the per-mesh density multiplier (which is designed to be used when the same mesh is used at different scales). Lower values will result in faster bake times. diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp index 33b0b0d01526..0196770ec725 100644 --- a/modules/lightmapper_rd/lightmapper_rd.cpp +++ b/modules/lightmapper_rd/lightmapper_rd.cpp @@ -226,14 +226,14 @@ void LightmapperRD::_sort_triangle_clusters(uint32_t p_cluster_size, uint32_t p_ } } -Lightmapper::BakeError LightmapperRD::_blit_meshes_into_atlas(int p_max_texture_size, int p_denoiser_range, Vector> &albedo_images, Vector> &emission_images, AABB &bounds, Size2i &atlas_size, int &atlas_slices, BakeStepFunc p_step_function, void *p_bake_userdata) { +Lightmapper::BakeError LightmapperRD::_blit_meshes_into_atlas(int p_max_texture_size, int p_denoiser_range, Vector> &albedo_images, Vector> &emission_images, AABB &bounds, Size2i &atlas_size, int &atlas_slices, bool p_downsample, BakeStepFunc p_step_function, void *p_bake_userdata) { Vector sizes; for (int m_i = 0; m_i < mesh_instances.size(); m_i++) { MeshInstance &mi = mesh_instances.write[m_i]; Size2i s = Size2i(mi.data.albedo_on_uv2->get_width(), mi.data.albedo_on_uv2->get_height()); sizes.push_back(s); - atlas_size = atlas_size.max(s + Size2i(2, 2).maxi(p_denoiser_range)); + atlas_size = atlas_size.max(s + Size2i(2, 2).maxi(p_denoiser_range) * (p_downsample ? 2 : 1)); } int max = nearest_power_of_2_templated(atlas_size.width); @@ -261,7 +261,10 @@ Lightmapper::BakeError LightmapperRD::_blit_meshes_into_atlas(int p_max_texture_ source_sizes.resize(sizes.size()); source_indices.resize(sizes.size()); for (int i = 0; i < source_indices.size(); i++) { - source_sizes.write[i] = sizes[i] + Vector2i(2, 2).maxi(p_denoiser_range); // Add padding between lightmaps. + // Add padding between lightmaps. + // Double the padding if the lightmap will be downsampled at the end of the baking process; + // otherwise, the padding would be insufficient. + source_sizes.write[i] = sizes[i] + Vector2i(2, 2).maxi(p_denoiser_range) * (p_downsample ? 2 : 1); source_indices.write[i] = i; } Vector atlas_offsets; @@ -986,7 +989,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) { +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_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, bool p_downsample) { int denoiser = GLOBAL_GET("rendering/lightmapping/denoising/denoiser"); String oidn_path = EDITOR_GET("filesystem/tools/oidn/oidn_denoise_path"); @@ -1018,7 +1021,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d Vector> albedo_images; Vector> emission_images; - BakeError bake_error = _blit_meshes_into_atlas(p_max_texture_size, p_denoiser_range, albedo_images, emission_images, bounds, atlas_size, atlas_slices, p_step_function, p_bake_userdata); + BakeError bake_error = _blit_meshes_into_atlas(p_max_texture_size, p_denoiser_range, albedo_images, emission_images, bounds, atlas_size, atlas_slices, p_downsample, p_step_function, p_bake_userdata); if (bake_error != BAKE_OK) { return bake_error; } diff --git a/modules/lightmapper_rd/lightmapper_rd.h b/modules/lightmapper_rd/lightmapper_rd.h index 487c44a480a3..e82240e729d2 100644 --- a/modules/lightmapper_rd/lightmapper_rd.h +++ b/modules/lightmapper_rd/lightmapper_rd.h @@ -267,7 +267,7 @@ class LightmapperRD : public Lightmapper { float pad[2]; }; - BakeError _blit_meshes_into_atlas(int p_max_texture_size, int p_denoiser_range, Vector> &albedo_images, Vector> &emission_images, AABB &bounds, Size2i &atlas_size, int &atlas_slices, BakeStepFunc p_step_function, void *p_bake_userdata); + BakeError _blit_meshes_into_atlas(int p_max_texture_size, int p_denoiser_range, Vector> &albedo_images, Vector> &emission_images, AABB &bounds, Size2i &atlas_size, int &atlas_slices, bool p_downsample, BakeStepFunc p_step_function, void *p_bake_userdata); void _create_acceleration_structures(RenderingDevice *rd, Size2i atlas_size, int atlas_slices, AABB &bounds, int grid_size, uint32_t p_cluster_size, Vector &probe_positions, GenerateProbes p_generate_probes, Vector &slice_triangle_count, Vector &slice_seam_count, RID &vertex_buffer, RID &triangle_buffer, RID &lights_buffer, RID &r_triangle_indices_buffer, RID &r_cluster_indices_buffer, RID &r_cluster_aabbs_buffer, RID &probe_positions_buffer, RID &grid_texture, RID &seams_buffer, BakeStepFunc p_step_function, void *p_bake_userdata); void _raster_geometry(RenderingDevice *rd, Size2i atlas_size, int atlas_slices, int grid_size, AABB bounds, float p_bias, Vector slice_triangle_count, RID position_tex, RID unocclude_tex, RID normal_tex, RID raster_depth_buffer, RID rasterize_shader, RID raster_base_uniform); @@ -284,7 +284,7 @@ class LightmapperRD : public Lightmapper { virtual void add_omni_light(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(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_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) 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_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, bool p_downsample = 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 3f8b0dfb8ee8..a49af837e09c 100644 --- a/scene/3d/lightmap_gi.cpp +++ b/scene/3d/lightmap_gi.cpp @@ -781,7 +781,8 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa // For now set to basic size to avoid crash. mesh_lightmap_size = Size2i(64, 64); } - Size2i lightmap_size = Size2i(Size2(mesh_lightmap_size) * mf.lightmap_scale * texel_scale); + // Double lightmap texel density if downsampling is enabled, as the final texture size will be halved before saving lightmaps. + Size2i lightmap_size = Size2i(Size2(mesh_lightmap_size) * mf.lightmap_scale * texel_scale) * (downsample ? 2 : 1); ERR_FAIL_COND_V(lightmap_size.x == 0 || lightmap_size.y == 0, BAKE_ERROR_LIGHTMAP_TOO_SMALL); TypedArray overrides; @@ -1166,6 +1167,13 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa config->save(texture_path + ".import"); + if (downsample) { + // Texture was baked with doubled resolution on each axis, so halve the texture size on each axis to act as downsampling. + // The default bilinear interpolation gives good results while avoiding oversharpening + // (which tends to happen with Cubic or Lanczos resize modes). + texture_image->resize(texture_image->get_width() * 0.5, texture_image->get_height() * 0.5); + } + Error err = texture_image->save_exr(texture_path, false); ERR_FAIL_COND_V(err, BAKE_ERROR_CANT_CREATE_IMAGE); ResourceLoader::import(texture_path); @@ -1564,6 +1572,14 @@ int LightmapGI::get_max_texture_size() const { return max_texture_size; } +void LightmapGI::set_downsample(bool p_enable) { + downsample = p_enable; +} + +bool LightmapGI::is_downsampling() const { + return downsample; +} + void LightmapGI::set_generate_probes(GenerateProbes p_generate_probes) { gen_probes = p_generate_probes; } @@ -1646,6 +1662,9 @@ void LightmapGI::_bind_methods() { ClassDB::bind_method(D_METHOD("set_max_texture_size", "max_texture_size"), &LightmapGI::set_max_texture_size); ClassDB::bind_method(D_METHOD("get_max_texture_size"), &LightmapGI::get_max_texture_size); + ClassDB::bind_method(D_METHOD("set_downsample", "downsample"), &LightmapGI::set_downsample); + ClassDB::bind_method(D_METHOD("is_downsampling"), &LightmapGI::is_downsampling); + ClassDB::bind_method(D_METHOD("set_use_denoiser", "use_denoiser"), &LightmapGI::set_use_denoiser); ClassDB::bind_method(D_METHOD("is_using_denoiser"), &LightmapGI::is_using_denoiser); @@ -1671,6 +1690,7 @@ void LightmapGI::_bind_methods() { ADD_GROUP("Tweaks", ""); ADD_PROPERTY(PropertyInfo(Variant::INT, "quality", PROPERTY_HINT_ENUM, "Low,Medium,High,Ultra"), "set_bake_quality", "get_bake_quality"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "downsample"), "set_downsample", "is_downsampling"); ADD_PROPERTY(PropertyInfo(Variant::INT, "bounces", PROPERTY_HINT_RANGE, "0,6,1,or_greater"), "set_bounces", "get_bounces"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bounce_indirect_energy", PROPERTY_HINT_RANGE, "0,2,0.01"), "set_bounce_indirect_energy", "get_bounce_indirect_energy"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "directional"), "set_directional", "is_directional"); diff --git a/scene/3d/lightmap_gi.h b/scene/3d/lightmap_gi.h index 67480132b6fa..d1e51a6fc7f8 100644 --- a/scene/3d/lightmap_gi.h +++ b/scene/3d/lightmap_gi.h @@ -163,6 +163,7 @@ class LightmapGI : public VisualInstance3D { float bias = 0.0005; float texel_scale = 1.0; int max_texture_size = 16384; + bool downsample = false; bool interior = false; EnvironmentMode environment_mode = ENVIRONMENT_MODE_SCENE; Ref environment_custom_sky; @@ -297,6 +298,9 @@ class LightmapGI : public VisualInstance3D { void set_max_texture_size(int p_size); int get_max_texture_size() const; + void set_downsample(bool p_enable); + bool is_downsampling() const; + void set_generate_probes(GenerateProbes p_generate_probes); GenerateProbes get_generate_probes() const; diff --git a/scene/3d/lightmapper.h b/scene/3d/lightmapper.h index 9aa8ef8ccbc9..f46805f3951d 100644 --- a/scene/3d/lightmapper.h +++ b/scene/3d/lightmapper.h @@ -181,7 +181,7 @@ class Lightmapper : public RefCounted { virtual void add_omni_light(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(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_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) = 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_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, bool p_downsample = false) = 0; virtual int get_bake_texture_count() const = 0; virtual Ref get_bake_texture(int p_index) const = 0;