From 27f792ea877c16100e32183b7ef33e3c2c0ede5f Mon Sep 17 00:00:00 2001 From: Hugo Locurcio Date: Fri, 11 Aug 2023 16:24:46 +0200 Subject: [PATCH] Add a Preview Bake button for quick iteration with LightmapGI When using the Preview Bake button instead of Bake Lightmaps, low quality settings will be used instead of settings from the LightmapGI node. A node configuration warning will be displayed besides the LightmapGI node to inform the user that the bake was done in preview mode. This preview bake warning is persisted to storage to make teamwork easier. This helps iterate quicker on a scene's lighting since you don't need to wait as much time to see results. On a simple test scene, Preview Bake is about 3 times faster than a "final" bake with the default LightmapGI settings (if denoising is not the bottleneck). This metric will vary depending on CPU and GPU speed, as the baking process is performed on the GPU but denoising is performedon the CPU. The preview bake settings can be adjusted in the Project Settings. This also prints a message with the effective quality settings before beginning the lightmapping process. --- doc/classes/LightmapGI.xml | 1 + doc/classes/LightmapGIData.xml | 15 +++ doc/classes/ProjectSettings.xml | 14 ++- editor/icons/BakePreview.svg | 1 + editor/plugins/lightmap_gi_editor_plugin.cpp | 27 +++-- editor/plugins/lightmap_gi_editor_plugin.h | 6 +- modules/lightmapper_rd/register_types.cpp | 6 ++ scene/3d/lightmap_gi.cpp | 108 ++++++++++++++++++- scene/3d/lightmap_gi.h | 8 +- 9 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 editor/icons/BakePreview.svg diff --git a/doc/classes/LightmapGI.xml b/doc/classes/LightmapGI.xml index 837da1794fd1..9a8d729b79bc 100644 --- a/doc/classes/LightmapGI.xml +++ b/doc/classes/LightmapGI.xml @@ -7,6 +7,7 @@ The [LightmapGI] node is used to compute and store baked lightmaps. Lightmaps are used to provide high-quality indirect lighting with very little light leaking. [LightmapGI] can also provide rough reflections using spherical harmonics if [member directional] is enabled. Dynamic objects can receive indirect lighting thanks to [i]light probes[/i], which can be automatically placed by setting [member generate_probes_subdiv] to a value other than [constant GENERATE_PROBES_DISABLED]. Additional lightmap probes can also be added by creating [LightmapProbe] nodes. The downside is that lightmaps are fully static and cannot be baked in an exported project. Baking a [LightmapGI] node is also slower compared to [VoxelGI]. [b]Procedural generation:[/b] Lightmap baking functionality is only available in the editor. This means [LightmapGI] is not suited to procedurally generated or user-built levels. For procedurally generated or user-built levels, use [VoxelGI] or SDFGI instead (see [member Environment.sdfgi_enabled]). [b]Performance:[/b] [LightmapGI] provides the best possible run-time performance for global illumination. It is suitable for low-end hardware including integrated graphics and mobile devices. + [b]Bake modes:[/b] In the editor, after selecting a [LightmapGI] node, you can choose between performing a [b]Preview Bake[/b] or a "final" bake using the [b]Bake Lightmaps[/b] button. Preview bakes use lower quality settings but are significantly faster to bake (run-time performance is identical). Using preview bakes during development allows you to iterate on your 3D scene's lighting faster. You can change the preview bake quality settings by adjusting the [ProjectSettings]' [code]rendering/cpu_lightmapper/*[/code] properties. [b]Note:[/b] Due to how lightmaps work, most properties only have a visible effect once lightmaps are baked again. [b]Note:[/b] Lightmap baking on [CSGShape3D]s and [PrimitiveMesh]es is not supported, as these cannot store UV2 data required for baking. [b]Note:[/b] If no custom lightmappers are installed, [LightmapGI] can only be baked from devices that support the Forward+ or Mobile renderers. diff --git a/doc/classes/LightmapGIData.xml b/doc/classes/LightmapGIData.xml index 76824f84a081..1113b4b1cfd9 100644 --- a/doc/classes/LightmapGIData.xml +++ b/doc/classes/LightmapGIData.xml @@ -38,12 +38,27 @@ Returns the [NodePath] of the baked object at index [param user_idx]. + + + + Returns [code]true[/code] if this baked lightmap is the result of a [i]preview bake[/i], [code]false[/code] otherwise. See also [method set_preview_bake]. + See the [LightmapGI] class description for more information. + + If [code]true[/code], lightmaps were baked with directional information. See also [member LightmapGI.directional]. + + + + + If [code]true[/code], the baked lightmap is considered to be a [i]preview bake[/i]. This is set to [code]true[/code] by the editor when using the [b]Preview Bake[/b] button at the top of the 3D editor, and [code]false[/code] when using the [b]Bake Lightmaps[/b] button. This method can also be used by plugins. See also [method is_preview_bake]. + See the [LightmapGI] class description for more information. + + diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 1da578167cf4..8d18128665c9 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -2632,12 +2632,24 @@ - Intel GPUs: SYCL libraries If no GPU acceleration is configured on the system, multi-threaded CPU-based denoising will be performed instead. This CPU-based denoising is significantly slower than the JNLM denoiser in most cases. + + Clamps the maximum value of [member LightmapGI.generate_probes_subdiv] when using the [b]Preview Bake[/b] button after selecting a [LightmapGI] node in the editor. + + + Clamps the maximum value of [member LightmapGI.bounces] when using the [b]Preview Bake[/b] button after selecting a [LightmapGI] node in the editor. If the number of bounces defined in the [LightmapGI] node is lower than this setting, the number of bounces defined in the [LightmapGI] will be used instead. + + + Clamps the maximum value of [member LightmapGI.quality] when using the [b]Preview Bake[/b] button after selecting a [LightmapGI] node in the editor. + + + Multiplier for the [member LightmapGI.texel_scale] when using the [b]Preview Bake[/b] button after selecting a [LightmapGI] node in the editor. Lower values result in significantly faster baking and smaller file sizes at the cost of blurrier and less precise lightmaps. The default value of [code]0.5[/code] halves the resolution on each axis, resulting in 4× fewer pixels needing to be baked on average. + If [code]true[/code], applies a bicubic filter during lightmap sampling. This makes lightmaps look much smoother, at a moderate performance cost. [b]Note:[/b] The bicubic filter exaggerates the 'bleeding' effect that occurs when a lightmap's resolution is low enough. - The texel_size that is used to calculate the [member Mesh.lightmap_size_hint] on [PrimitiveMesh] resources if [member PrimitiveMesh.add_uv2] is enabled. + The texel size that is used to calculate the [member Mesh.lightmap_size_hint] on [PrimitiveMesh] resources if [member PrimitiveMesh.add_uv2] is enabled. Lower values result in more precise lightmaps on primitive meshes, at the cost of longer bake times and larger file sizes. The framerate-independent update speed when representing dynamic object lighting from [LightmapProbe]s. Higher values make dynamic object lighting update faster. Higher values can prevent fast-moving objects from having "outdated" indirect lighting displayed on them, at the cost of possible flickering when an object moves from a bright area to a shaded area. diff --git a/editor/icons/BakePreview.svg b/editor/icons/BakePreview.svg new file mode 100644 index 000000000000..defef7a121fc --- /dev/null +++ b/editor/icons/BakePreview.svg @@ -0,0 +1 @@ + diff --git a/editor/plugins/lightmap_gi_editor_plugin.cpp b/editor/plugins/lightmap_gi_editor_plugin.cpp index 3f21d5d11c1e..c82caa9c2d57 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, nullptr, preview_mode); } else { - err = lightmap->bake(lightmap->get_parent(), p_file, bake_func_step); + err = lightmap->bake(lightmap->get_parent(), p_file, bake_func_step, nullptr, preview_mode); } } } else { @@ -123,7 +123,8 @@ void LightmapGIEditorPlugin::_bake_select_file(const String &p_file) { } } -void LightmapGIEditorPlugin::_bake() { +void LightmapGIEditorPlugin::_bake(bool p_preview_mode) { + preview_mode = p_preview_mode; _bake_select_file(""); } @@ -142,8 +143,10 @@ bool LightmapGIEditorPlugin::handles(Object *p_object) const { void LightmapGIEditorPlugin::make_visible(bool p_visible) { if (p_visible) { + bake_preview->show(); bake->show(); } else { + bake_preview->hide(); bake->hide(); } } @@ -173,15 +176,25 @@ void LightmapGIEditorPlugin::bake_func_end(uint64_t p_time_started) { } void LightmapGIEditorPlugin::_bind_methods() { - ClassDB::bind_method("_bake", &LightmapGIEditorPlugin::_bake); + ClassDB::bind_method("_bake", &LightmapGIEditorPlugin::_bake, DEFVAL(false)); } LightmapGIEditorPlugin::LightmapGIEditorPlugin() { - bake = memnew(Button); - bake->set_theme_type_variation("FlatButton"); // TODO: Rework this as a dedicated toolbar control so we can hook into theme changes and update it // when the editor theme updates. + bake_preview = memnew(Button); + bake_preview->set_theme_type_variation("FlatButton"); + bake_preview->set_button_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("BakePreview"), EditorStringName(EditorIcons))); + bake_preview->set_tooltip_text(TTR("Bakes lightmaps with low-quality settings for quick iteration.\nPreview bake quality can be changed in the Rendering > Lightmapping > Preview Bake section of the Project Settings.")); + bake_preview->set_text(TTR("Preview Bake")); + bake_preview->hide(); + bake_preview->connect("pressed", callable_mp(this, &LightmapGIEditorPlugin::_bake).bind(true)); + add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, bake_preview); + + bake = memnew(Button); + bake->set_theme_type_variation("FlatButton"); bake->set_button_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Bake"), EditorStringName(EditorIcons))); + bake->set_tooltip_text(TTR("Bakes lightmaps with the settings specified in the LightmapGI node.")); bake->set_text(TTR("Bake Lightmaps")); #ifdef MODULE_LIGHTMAPPER_RD_ENABLED @@ -201,7 +214,7 @@ LightmapGIEditorPlugin::LightmapGIEditorPlugin() { #endif // MODULE_LIGHTMAPPER_RD_ENABLED bake->hide(); - bake->connect(SceneStringName(pressed), Callable(this, "_bake")); + bake->connect("pressed", callable_mp(this, &LightmapGIEditorPlugin::_bake).bind(false)); add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, bake); lightmap = nullptr; diff --git a/editor/plugins/lightmap_gi_editor_plugin.h b/editor/plugins/lightmap_gi_editor_plugin.h index 3e739adf9e66..30f32008aba5 100644 --- a/editor/plugins/lightmap_gi_editor_plugin.h +++ b/editor/plugins/lightmap_gi_editor_plugin.h @@ -43,15 +43,19 @@ class LightmapGIEditorPlugin : public EditorPlugin { LightmapGI *lightmap = nullptr; + Button *bake_preview = nullptr; Button *bake = nullptr; + // If `true`, low-quality bake settings will be used for the next bake. + bool preview_mode = false; + EditorFileDialog *file_dialog = nullptr; static EditorProgress *tmp_progress; static bool bake_func_step(float p_progress, const String &p_description, void *, bool p_refresh); static void bake_func_end(uint64_t p_time_started); void _bake_select_file(const String &p_file); - void _bake(); + void _bake(bool p_preview_mode = false); protected: static void _bind_methods(); diff --git a/modules/lightmapper_rd/register_types.cpp b/modules/lightmapper_rd/register_types.cpp index 4fe8f207237d..7fa1a142de2e 100644 --- a/modules/lightmapper_rd/register_types.cpp +++ b/modules/lightmapper_rd/register_types.cpp @@ -60,6 +60,12 @@ void initialize_lightmapper_rd_module(ModuleInitializationLevel p_level) { GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_performance/max_rays_per_probe_pass", PROPERTY_HINT_RANGE, "1,256,1,or_greater"), 64); GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/denoising/denoiser", PROPERTY_HINT_ENUM, "JNLM,OIDN"), 0); + + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/preview_bake/max_quality", PROPERTY_HINT_ENUM, "Low,Medium,High,Ultra"), 0); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/preview_bake/max_bounces", PROPERTY_HINT_RANGE, "0,16,1"), 2); + GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "rendering/lightmapping/preview_bake/texel_scale_factor", PROPERTY_HINT_RANGE, "0.05,1.0,0.001"), 0.5); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/preview_bake/generate_probes_max_subdiv", PROPERTY_HINT_ENUM, "Disabled,4,8,16,32"), 0); + #ifndef _3D_DISABLED GDREGISTER_CLASS(LightmapperRD); Lightmapper::create_gpu = create_lightmapper_rd; diff --git a/scene/3d/lightmap_gi.cpp b/scene/3d/lightmap_gi.cpp index aa4445a7ba84..f3e920b067f0 100644 --- a/scene/3d/lightmap_gi.cpp +++ b/scene/3d/lightmap_gi.cpp @@ -207,6 +207,14 @@ float LightmapGIData::get_baked_exposure() const { return baked_exposure; } +void LightmapGIData::set_preview_bake(bool p_preview_bake) { + preview_bake = p_preview_bake; +} + +bool LightmapGIData::is_preview_bake() const { + return preview_bake; +} + void LightmapGIData::_set_probe_data(const Dictionary &p_data) { ERR_FAIL_COND(!p_data.has("bounds")); ERR_FAIL_COND(!p_data.has("points")); @@ -274,12 +282,17 @@ void LightmapGIData::_bind_methods() { ClassDB::bind_method(D_METHOD("_set_probe_data", "data"), &LightmapGIData::_set_probe_data); ClassDB::bind_method(D_METHOD("_get_probe_data"), &LightmapGIData::_get_probe_data); + ClassDB::bind_method(D_METHOD("set_preview_bake", "preview_bake"), &LightmapGIData::set_preview_bake); + ClassDB::bind_method(D_METHOD("is_preview_bake"), &LightmapGIData::is_preview_bake); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "lightmap_textures", PROPERTY_HINT_ARRAY_TYPE, "TextureLayered", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY), "set_lightmap_textures", "get_lightmap_textures"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "uses_spherical_harmonics", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "set_uses_spherical_harmonics", "is_using_spherical_harmonics"); ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "user_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_user_data", "_get_user_data"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "probe_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_probe_data", "_get_probe_data"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "preview_bake", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "set_preview_bake", "is_preview_bake"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "_uses_packed_directional", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_uses_packed_directional", "_is_using_packed_directional"); + #ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("set_light_texture", "light_texture"), &LightmapGIData::set_light_texture); ClassDB::bind_method(D_METHOD("get_light_texture"), &LightmapGIData::get_light_texture); @@ -806,7 +819,40 @@ LightmapGI::BakeError LightmapGI::_save_and_reimport_atlas_textures(const Ref
  • overrides; @@ -1001,7 +1052,19 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa bounds.grow_by(bounds.size.length() * 0.001); - if (gen_probes == GENERATE_PROBES_DISABLED) { + LightmapGI::BakeQuality effective_bake_quality = bake_quality; + int effective_bounces = bounces; + float effective_texel_scale = texel_scale; + LightmapGI::GenerateProbes effective_gen_probes = gen_probes; + if (p_preview_bake) { + // Use lower-quality settings for quick iteration. + effective_texel_scale *= float(GLOBAL_GET("rendering/lightmapping/preview_bake/texel_scale_factor")); + effective_bake_quality = MIN(bake_quality, LightmapGI::BakeQuality(int(GLOBAL_GET("rendering/lightmapping/preview_bake/max_quality")))); + effective_bounces = MIN(bounces, int(GLOBAL_GET("rendering/lightmapping/preview_bake/max_bounces"))); + effective_gen_probes = MIN(gen_probes, LightmapGI::GenerateProbes(int(GLOBAL_GET("rendering/lightmapping/preview_bake/generate_probes_max_subdiv")))); + } + + if (effective_gen_probes == GENERATE_PROBES_DISABLED) { // generate 8 probes on bound endpoints for (int i = 0; i < 8; i++) { probes_found.push_back(bounds.get_endpoint(i)); @@ -1009,7 +1072,7 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa } else { // detect probes from geometry static const int subdiv_values[6] = { 0, 4, 8, 16, 32 }; - int subdiv = subdiv_values[gen_probes]; + int subdiv = subdiv_values[effective_gen_probes]; float subdiv_cell_size; Vector3i bound_limit; @@ -1181,7 +1244,33 @@ 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, use_texture_for_bounces, Lightmapper::GenerateProbes(gen_probes), environment_image, environment_transform, _lightmap_bake_step_function, &bsud, exposure_normalization); + print_line(vformat(U"Baking %slightmaps: %s quality, %d bounces, %s probe subdivision, %.2f× texel scale%s%s", + p_preview_bake ? "preview " : "", + _get_bake_quality_string(effective_bake_quality), + effective_bounces, + _get_gen_probes_string(effective_gen_probes), + effective_texel_scale, + directional ? ", directional" : "", + use_denoiser ? ", with denoiser" : "")); + + Lightmapper::BakeError bake_err; + bake_err = lightmapper->bake( + Lightmapper::BakeQuality(effective_bake_quality), + use_denoiser, + denoiser_strength, + denoiser_range, + effective_bounces, + bounce_indirect_energy, + bias, + max_texture_size, + directional, + use_texture_for_bounces, + Lightmapper::GenerateProbes(effective_gen_probes), + environment_image, + environment_transform, + _lightmap_bake_step_function, + &bsud, + exposure_normalization); if (bake_err == Lightmapper::BAKE_ERROR_TEXTURE_EXCEEDS_MAX_SIZE) { return BAKE_ERROR_TEXTURE_SIZE_TOO_SMALL; @@ -1374,7 +1463,12 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa return BAKE_ERROR_CANT_CREATE_IMAGE; } + // Only change the preview bake status when everything is completed. + // If the user cancels the bake, the preview bake status shouldn't change. + gi_data->set_preview_bake(p_preview_bake); + set_light_data(gi_data); + update_configuration_warnings(); return BAKE_ERROR_OK; } @@ -1625,6 +1719,10 @@ PackedStringArray LightmapGI::get_configuration_warnings() const { warnings.push_back(vformat(RTR("Lightmaps can only be baked from a GPU that supports the RenderingDevice backends.\nYour GPU (%s) does not support RenderingDevice, as it does not support Vulkan, Direct3D 12, or Metal.\nLightmap baking will not be available on this device, although rendering existing baked lightmaps will work."), RenderingServer::get_singleton()->get_video_adapter_name())); return warnings; } + + if (get_light_data().is_valid() && get_light_data()->is_preview_bake()) { + warnings.push_back(RTR("This LightmapGI's data uses a preview bake which has lower quality than a \"final\" bake.\nRemember to use the Bake Lightmaps button at the top of the 3D editor viewport (instead of Preview Bake) before distributing your project.")); + } #elif defined(ANDROID_ENABLED) || defined(IOS_ENABLED) warnings.push_back(vformat(RTR("Lightmaps cannot be baked on %s. Rendering existing baked lightmaps will still work."), OS::get_singleton()->get_name())); #else diff --git a/scene/3d/lightmap_gi.h b/scene/3d/lightmap_gi.h index faa8b84fa177..32f7c272a9b4 100644 --- a/scene/3d/lightmap_gi.h +++ b/scene/3d/lightmap_gi.h @@ -58,6 +58,7 @@ class LightmapGIData : public Resource { RID lightmap; AABB bounds; float baked_exposure = 1.0; + bool preview_bake = false; struct User { NodePath path; @@ -104,6 +105,9 @@ class LightmapGIData : public Resource { bool is_interior() const; float get_baked_exposure() const; + void set_preview_bake(bool p_preview_bake); + bool is_preview_bake() const; + void set_capture_data(const AABB &p_bounds, bool p_interior, const PackedVector3Array &p_points, const PackedColorArray &p_point_sh, const PackedInt32Array &p_tetrahedra, const PackedInt32Array &p_bsp_tree, float p_baked_exposure); PackedVector3Array get_capture_points() const; PackedColorArray get_capture_sh() const; @@ -231,6 +235,8 @@ class LightmapGI : public VisualInstance3D { float to_percent = 0.0; }; + String _get_bake_quality_string(BakeQuality p_quality) const; + String _get_gen_probes_string(GenerateProbes p_gen_probes) const; static bool _lightmap_bake_step_function(float p_completion, const String &p_text, void *ud, bool p_refresh); struct GenProbesOctree { @@ -316,7 +322,7 @@ 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); + BakeError bake(Node *p_from_node, String p_image_data_path = "", Lightmapper::BakeStepFunc p_bake_step = nullptr, void *p_bake_userdata = nullptr, bool p_preview_bake = false); virtual PackedStringArray get_configuration_warnings() const override;