From 87469dbbff47ed2eb4c1ad2deef35ae2b6b37b40 Mon Sep 17 00:00:00 2001 From: sourcelocation Date: Mon, 30 Sep 2024 21:02:27 +0300 Subject: [PATCH] Expose runtime baking functionality in LightmapGI Co-authored-by: Rising Thumb --- doc/classes/LightmapGI.xml | 12 +++ editor/plugins/lightmap_gi_editor_plugin.cpp | 4 +- modules/lightmapper_rd/config.py | 2 +- modules/lightmapper_rd/lightmapper_rd.cpp | 39 +++++--- modules/xatlas_unwrap/config.py | 2 +- scene/3d/lightmap_gi.cpp | 100 +++++++++++++------ scene/3d/lightmap_gi.h | 6 +- 7 files changed, 118 insertions(+), 47 deletions(-) diff --git a/doc/classes/LightmapGI.xml b/doc/classes/LightmapGI.xml index e7d44411ef..c96bc41c06 100644 --- a/doc/classes/LightmapGI.xml +++ b/doc/classes/LightmapGI.xml @@ -14,6 +14,18 @@ $DOCS_URL/tutorials/3d/global_illumination/using_lightmap_gi.html + + + + + + + Bakes lightmaps (requires meshes to have UV2 unwrapped) for [param from_node] and its children to [param image_data_path]. [param image_data_path] must end with an [code].exr[/code] or [code].lmbake[/code] file extension. If [param from_node] is [code]null[/code], lightmaps are baked from the [LightmapGI] node's parent. Baking lightmaps can take from a few seconds to several dozen minutes depending on the GPU speed and quality settings chosen. + [b]Note:[/b] [method bake] only works within the editor, and when running a project from the editor. [method bake] will do nothing when called in a project exported in either debug or release mode. This limitation is in place to reduce the binary size of exported projects. You can [url=$DOCS_URL/contributing/development/compiling/index.html]compile custom export templates[/url] with the [code]module_lightmapper_rd_enabled=yes module_xatlas_unwrap_enabled=yes[/code] SCons options to remove this limitation. + [b]Additional Note:[/b] Baking lightmaps from a headless editor instance is not supported. If you attempt to bake lightmaps in this manner, the images returned will be null. + + + The bias to use when computing shadows. Increasing [member bias] can fix shadow acne on the resulting baked lightmap, but can introduce peter-panning (shadows not connecting to their casters). Real-time [Light3D] shadows are not affected by this [member bias] property. diff --git a/editor/plugins/lightmap_gi_editor_plugin.cpp b/editor/plugins/lightmap_gi_editor_plugin.cpp index 1c17d99d0d..df866c2713 100644 --- a/editor/plugins/lightmap_gi_editor_plugin.cpp +++ b/editor/plugins/lightmap_gi_editor_plugin.cpp @@ -66,9 +66,9 @@ void LightmapGIEditorPlugin::_bake_select_file(const String &p_file) { if (err == LightmapGI::BAKE_ERROR_OK) { if (get_tree()->get_edited_scene_root() == lightmap) { - err = lightmap->bake(lightmap, p_file, bake_func_step); + err = lightmap->_bake(lightmap, p_file, bake_func_step); } else { - err = lightmap->bake(lightmap->get_parent(), p_file, bake_func_step); + err = lightmap->_bake(lightmap->get_parent(), p_file, bake_func_step); } } } else { diff --git a/modules/lightmapper_rd/config.py b/modules/lightmapper_rd/config.py index ecc61c2d7e..e7da618547 100644 --- a/modules/lightmapper_rd/config.py +++ b/modules/lightmapper_rd/config.py @@ -1,5 +1,5 @@ def can_build(env, platform): - return env.editor_build and platform not in ["android", "ios"] + return (env.editor_build or env["module_lightmapper_rd_enabled"]) and platform not in ["android", "ios"] def configure(env): diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp index 8ba6f9e2ba..dcfa0d9252 100644 --- a/modules/lightmapper_rd/lightmapper_rd.cpp +++ b/modules/lightmapper_rd/lightmapper_rd.cpp @@ -37,8 +37,10 @@ #include "core/config/project_settings.h" #include "core/io/dir_access.h" #include "core/math/geometry_2d.h" +#ifdef TOOLS_ENABLED #include "editor/editor_paths.h" #include "editor/editor_settings.h" +#endif #include "servers/rendering/rendering_device_binds.h" #if defined(VULKAN_ENABLED) @@ -881,6 +883,7 @@ Ref LightmapperRD::_read_pfm(const String &p_name) { return img; } +#ifdef TOOLS_ENABLED LightmapperRD::BakeError LightmapperRD::_denoise_oidn(RenderingDevice *p_rd, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, const String &p_exe) { Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); @@ -946,6 +949,7 @@ LightmapperRD::BakeError LightmapperRD::_denoise_oidn(RenderingDevice *p_rd, RID } return BAKE_OK; } +#endif LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref &p_compute_shader, const RID &p_compute_base_uniform_set, PushConstant &p_push_constant, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, float p_denoiser_strength, int p_denoiser_range, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, BakeStepFunc p_step_function, void *p_bake_userdata) { RID denoise_params_buffer = p_rd->uniform_buffer_create(sizeof(DenoiseParams)); @@ -1024,22 +1028,28 @@ 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) { +#ifdef TOOLS_ENABLED int denoiser = GLOBAL_GET("rendering/lightmapping/denoising/denoiser"); - String oidn_path = EDITOR_GET("filesystem/tools/oidn/oidn_denoise_path"); - - if (p_use_denoiser && denoiser == 1) { - // OIDN (external). - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - - if (da->dir_exists(oidn_path)) { - if (OS::get_singleton()->get_name() == "Windows") { - oidn_path = oidn_path.path_join("oidnDenoise.exe"); - } else { - oidn_path = oidn_path.path_join("oidnDenoise"); + // TODO: Implement oidnDenoise for non-editor + String oidn_path; + if (Engine::get_singleton()->is_editor_hint()) { + oidn_path = p_use_denoiser ? EDITOR_GET("filesystem/tools/oidn/oidn_denoise_path") : Variant(); + + if (p_use_denoiser && denoiser == 1) { + // OIDN (external). + Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + + if (da->dir_exists(oidn_path)) { + if (OS::get_singleton()->get_name() == "Windows") { + oidn_path = oidn_path.path_join("oidnDenoise.exe"); + } else { + oidn_path = oidn_path.path_join("oidnDenoise"); + } } + ERR_FAIL_COND_V_MSG(oidn_path.is_empty() || !da->file_exists(oidn_path), BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES, "OIDN denoiser is selected in the project settings, but no or invalid OIDN executable path is configured in the editor settings."); } - ERR_FAIL_COND_V_MSG(oidn_path.is_empty() || !da->file_exists(oidn_path), BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES, "OIDN denoiser is selected in the project settings, but no or invalid OIDN executable path is configured in the editor settings."); } +#endif if (p_step_function) { p_step_function(0.0, RTR("Begin Bake"), p_bake_userdata, true); @@ -1849,6 +1859,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d { BakeError error; +#ifdef TOOLS_ENABLED if (denoiser == 1) { // OIDN (external). error = _denoise_oidn(rd, light_accum_tex, normal_tex, light_accum_tex, atlas_size, atlas_slices, p_bake_sh, oidn_path); @@ -1857,6 +1868,10 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d SWAP(light_accum_tex, light_accum_tex2); error = _denoise(rd, compute_shader, compute_base_uniform_set, push_constant, light_accum_tex2, normal_tex, light_accum_tex, p_denoiser_strength, p_denoiser_range, atlas_size, atlas_slices, p_bake_sh, p_step_function, p_bake_userdata); } +#else + SWAP(light_accum_tex, light_accum_tex2); + error = _denoise(rd, compute_shader, compute_base_uniform_set, push_constant, light_accum_tex2, normal_tex, light_accum_tex, p_denoiser_strength, p_denoiser_range, atlas_size, atlas_slices, p_bake_sh, p_step_function, p_bake_userdata); +#endif if (unlikely(error != BAKE_OK)) { return error; } diff --git a/modules/xatlas_unwrap/config.py b/modules/xatlas_unwrap/config.py index ecc61c2d7e..9e8eeca0e8 100644 --- a/modules/xatlas_unwrap/config.py +++ b/modules/xatlas_unwrap/config.py @@ -1,5 +1,5 @@ def can_build(env, platform): - return env.editor_build and platform not in ["android", "ios"] + return (env.editor_build or env["module_xatlas_unwrap_enabled"]) and platform not in ["android", "ios"] def configure(env): diff --git a/scene/3d/lightmap_gi.cpp b/scene/3d/lightmap_gi.cpp index 26a574cd26..a22096383a 100644 --- a/scene/3d/lightmap_gi.cpp +++ b/scene/3d/lightmap_gi.cpp @@ -30,6 +30,7 @@ #include "lightmap_gi.h" +#include "core/config/engine.h" #include "core/config/project_settings.h" #include "core/io/config_file.h" #include "core/math/delaunay_3d.h" @@ -39,6 +40,7 @@ #include "scene/resources/environment.h" #include "scene/resources/image_texture.h" #include "scene/resources/sky.h" +#include "scene/resources/texture.h" void LightmapGIData::add_user(const NodePath &p_path, const Rect2 &p_uv_scale, int p_slice_index, int32_t p_sub_instance) { User user; @@ -740,7 +742,17 @@ void LightmapGI::_gen_new_positions_from_octree(const GenProbesOctree *p_cell, f } } -LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_path, Lightmapper::BakeStepFunc p_bake_step, void *p_bake_userdata) { +bool LightmapGI::_dummy_bake_func_step(float p_progress, const String &p_description, void *, bool p_refresh) { + // No reporting needed, but baking logic is identical + return true; +} + +LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_path) { + // is dummy bake func needed? + return _bake(p_from_node, p_image_data_path, _dummy_bake_func_step, nullptr); +} + +LightmapGI::BakeError LightmapGI::_bake(Node *p_from_node, String p_image_data_path, Lightmapper::BakeStepFunc p_bake_step, void *p_bake_userdata) { if (p_image_data_path.is_empty()) { if (get_light_data().is_null()) { return BAKE_ERROR_NO_SAVE_PATH; @@ -1083,14 +1095,34 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa } if (env.is_valid()) { - environment_image = RS::get_singleton()->environment_bake_panorama(env->get_rid(), true, Size2i(128, 64)); environment_transform = Basis::from_euler(env->get_sky_rotation()).inverse(); + + Sky::RadianceSize old_radiance_size = Sky::RADIANCE_SIZE_MAX; + if (!Engine::get_singleton()->is_editor_hint()) { + Ref sky = env->get_sky(); + if (sky.is_valid()) { + old_radiance_size = sky->get_radiance_size(); + sky->set_radiance_size(Sky::RADIANCE_SIZE_128); + } + } + environment_image = RS::get_singleton()->environment_bake_panorama(env->get_rid(), true, Size2i(128, 128)); + if (old_radiance_size != Sky::RADIANCE_SIZE_MAX) { // If it's not max, it's been set and needs resetting + Ref sky = env->get_sky(); + if (sky.is_valid()) { + sky->set_radiance_size(old_radiance_size); + } + } } } } break; case ENVIRONMENT_MODE_CUSTOM_SKY: { if (environment_custom_sky.is_valid()) { - environment_image = RS::get_singleton()->sky_bake_panorama(environment_custom_sky->get_rid(), environment_custom_energy, true, Size2i(128, 64)); + Sky::RadianceSize old_radiance_size = environment_custom_sky->get_radiance_size(); + if (!Engine::get_singleton()->is_editor_hint()) { + environment_custom_sky->set_radiance_size(Sky::RADIANCE_SIZE_128); + } + environment_image = RS::get_singleton()->sky_bake_panorama(environment_custom_sky->get_rid(), environment_custom_energy, true, Size2i(128, 128)); + environment_custom_sky->set_radiance_size(old_radiance_size); } } break; @@ -1156,34 +1188,42 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa texture_image->blit_rect(images[i * slices_per_texture + j], Rect2i(0, 0, slice_width, slice_height), Point2i(0, slice_height * j)); } - String texture_path = texture_count > 1 ? base_path + "_" + itos(i) + ".exr" : base_path + ".exr"; - - Ref config; - config.instantiate(); - - if (FileAccess::exists(texture_path + ".import")) { - config->load(texture_path + ".import"); - } + if (Engine::get_singleton()->is_editor_hint()) { + String texture_path = texture_count > 1 ? base_path + "_" + itos(i) + ".exr" : base_path + ".exr"; + Ref config; + config.instantiate(); - config->set_value("remap", "importer", "2d_array_texture"); - config->set_value("remap", "type", "CompressedTexture2DArray"); - if (!config->has_section_key("params", "compress/mode")) { - // User may want another compression, so leave it be, but default to VRAM uncompressed. - config->set_value("params", "compress/mode", 3); + config->set_value("remap", "importer", "2d_array_texture"); + config->set_value("remap", "type", "CompressedTexture2DArray"); + if (!config->has_section_key("params", "compress/mode")) { + // User may want another compression, so leave it be, but default to VRAM uncompressed. + config->set_value("params", "compress/mode", 3); + } + config->set_value("params", "compress/channel_pack", 1); + config->set_value("params", "mipmaps/generate", false); + config->set_value("params", "slices/horizontal", 1); + config->set_value("params", "slices/vertical", texture_slice_count); + + config->save(texture_path + ".import"); + + Error err = texture_image->save_exr(texture_path, false); + ERR_FAIL_COND_V(err, BAKE_ERROR_CANT_CREATE_IMAGE); + ResourceLoader::import(texture_path); + Ref t = ResourceLoader::load(texture_path); // If already loaded, it will be updated on refocus? + ERR_FAIL_COND_V(t.is_null(), BAKE_ERROR_CANT_CREATE_IMAGE); + textures[i] = t; + } else { + String texture_path = texture_count > 1 ? base_path + "_" + itos(i) + ".res" : base_path + ".res"; + Ref texs; + texs.instantiate(); + texs->create_from_images(images); + + Error err = ResourceSaver::save(texs, texture_path); + ERR_FAIL_COND_V(err, BAKE_ERROR_CANT_CREATE_IMAGE); + Ref t = ResourceLoader::load(texture_path); + ERR_FAIL_COND_V(t.is_null(), BAKE_ERROR_CANT_CREATE_IMAGE); + textures[i] = t; } - config->set_value("params", "compress/channel_pack", 1); - config->set_value("params", "mipmaps/generate", false); - config->set_value("params", "slices/horizontal", 1); - config->set_value("params", "slices/vertical", texture_slice_count); - - config->save(texture_path + ".import"); - - Error err = texture_image->save_exr(texture_path, false); - ERR_FAIL_COND_V(err, BAKE_ERROR_CANT_CREATE_IMAGE); - ResourceLoader::import(texture_path); - Ref t = ResourceLoader::load(texture_path); // If already loaded, it will be updated on refocus? - ERR_FAIL_COND_V(t.is_null(), BAKE_ERROR_CANT_CREATE_IMAGE); - textures[i] = t; } } @@ -1686,7 +1726,7 @@ void LightmapGI::_bind_methods() { ClassDB::bind_method(D_METHOD("set_camera_attributes", "camera_attributes"), &LightmapGI::set_camera_attributes); ClassDB::bind_method(D_METHOD("get_camera_attributes"), &LightmapGI::get_camera_attributes); - // ClassDB::bind_method(D_METHOD("bake", "from_node"), &LightmapGI::bake, DEFVAL(Variant())); + ClassDB::bind_method(D_METHOD("bake", "from_node", "image_data_path"), &LightmapGI::bake, DEFVAL("")); ADD_GROUP("Tweaks", ""); ADD_PROPERTY(PropertyInfo(Variant::INT, "quality", PROPERTY_HINT_ENUM, "Low,Medium,High,Ultra"), "set_bake_quality", "get_bake_quality"); diff --git a/scene/3d/lightmap_gi.h b/scene/3d/lightmap_gi.h index 6377c420d1..bb39cda88d 100644 --- a/scene/3d/lightmap_gi.h +++ b/scene/3d/lightmap_gi.h @@ -310,7 +310,11 @@ class LightmapGI : public VisualInstance3D { AABB get_aabb() const override; - BakeError bake(Node *p_from_node, String p_image_data_path = "", Lightmapper::BakeStepFunc p_bake_step = nullptr, void *p_bake_userdata = nullptr); + static bool _dummy_bake_func_step(float p_progress, const String &p_description, void *, bool p_refresh); + + BakeError bake(Node *p_from_node, String p_image_data_path = ""); + + BakeError _bake(Node *p_from_node, String p_image_data_path = "", Lightmapper::BakeStepFunc p_bake_step = nullptr, void *p_bake_userdata = nullptr); virtual PackedStringArray get_configuration_warnings() const override;