Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Preview Bake button for quick iteration with LightmapGI #80518

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/classes/LightmapGI.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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 when using the Vulkan backend (Forward+ or Mobile), not OpenGL.
Expand Down
15 changes: 15 additions & 0 deletions doc/classes/LightmapGIData.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,27 @@
Returns the [NodePath] of the baked object at index [param user_idx].
</description>
</method>
<method name="is_preview_bake" qualifiers="const">
<return type="bool" />
<description>
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.
</description>
</method>
<method name="is_using_spherical_harmonics" qualifiers="const">
<return type="bool" />
<description>
If [code]true[/code], lightmaps were baked with directional information. See also [member LightmapGI.directional].
</description>
</method>
<method name="set_preview_bake">
<return type="void" />
<param index="0" name="preview_bake" type="bool" />
<description>
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.
</description>
</method>
<method name="set_uses_spherical_harmonics">
<return type="void" />
<param index="0" name="uses_spherical_harmonics" type="bool" />
Expand Down
14 changes: 13 additions & 1 deletion doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2478,8 +2478,20 @@
- 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.
</member>
<member name="rendering/lightmapping/preview_bake/generate_probes_max_subdiv" type="int" setter="" getter="" default="0">
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.
</member>
<member name="rendering/lightmapping/preview_bake/max_bounces" type="int" setter="" getter="" default="2">
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.
</member>
<member name="rendering/lightmapping/preview_bake/max_quality" type="int" setter="" getter="" default="0">
Clamps the maximum value of [member LightmapGI.quality] when using the [b]Preview Bake[/b] button after selecting a [LightmapGI] node in the editor.
</member>
<member name="rendering/lightmapping/preview_bake/texel_scale_factor" type="float" setter="" getter="" default="0.5">
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.
</member>
<member name="rendering/lightmapping/primitive_meshes/texel_size" type="float" setter="" getter="" default="0.2">
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.
</member>
<member name="rendering/lightmapping/probe_capture/update_speed" type="float" setter="" getter="" default="15">
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.
Expand Down
1 change: 1 addition & 0 deletions editor/icons/BakePreview.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 21 additions & 7 deletions editor/plugins/lightmap_gi_editor_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -116,7 +116,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("");
}

Expand All @@ -135,8 +136,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();
}
}
Expand Down Expand Up @@ -166,19 +169,30 @@ 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_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_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"));
bake->hide();
bake->connect("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;

file_dialog = memnew(EditorFileDialog);
Expand Down
6 changes: 5 additions & 1 deletion editor/plugins/lightmap_gi_editor_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions modules/lightmapper_rd/register_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
105 changes: 100 additions & 5 deletions scene/3d/lightmap_gi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,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"));
Expand Down Expand Up @@ -263,10 +271,14 @@ 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_NO_EDITOR), "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");

#ifndef DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("set_light_texture", "light_texture"), &LightmapGIData::set_light_texture);
Expand Down Expand Up @@ -687,7 +699,39 @@ 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) {
String LightmapGI::_get_bake_quality_string(LightmapGI::BakeQuality p_quality) const {
switch (p_quality) {
case BAKE_QUALITY_LOW:
return "Low";
case BAKE_QUALITY_MEDIUM:
return "Medium";
case BAKE_QUALITY_HIGH:
return "High";
case BAKE_QUALITY_ULTRA:
return "Ultra";
default:
return "Unknown";
}
}

String LightmapGI::_get_gen_probes_string(LightmapGI::GenerateProbes p_gen_probes) const {
switch (p_gen_probes) {
case GENERATE_PROBES_DISABLED:
return "Disabled";
case GENERATE_PROBES_SUBDIV_4:
return "4";
case GENERATE_PROBES_SUBDIV_8:
return "8";
case GENERATE_PROBES_SUBDIV_16:
return "16";
case GENERATE_PROBES_SUBDIV_32:
return "32";
default:
return "Unknown";
}
}

LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_path, Lightmapper::BakeStepFunc p_bake_step, void *p_bake_userdata, bool p_preview_bake) {
if (p_image_data_path.is_empty()) {
if (get_light_data().is_null()) {
return BAKE_ERROR_NO_SAVE_PATH;
Expand Down Expand Up @@ -725,6 +769,11 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa
}
// create mesh data for insert

float effective_texel_scale = texel_scale;
if (p_preview_bake) {
effective_texel_scale *= float(GLOBAL_GET("rendering/lightmapping/preview_bake/texel_scale_factor"));
}

//get the base material textures, help compute atlas size and bounds
for (int m_i = 0; m_i < meshes_found.size(); m_i++) {
if (p_bake_step) {
Expand All @@ -740,7 +789,7 @@ 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);
Size2i lightmap_size = Size2i(Size2(mesh_lightmap_size) * mf.lightmap_scale * effective_texel_scale);
ERR_FAIL_COND_V(lightmap_size.x == 0 || lightmap_size.y == 0, BAKE_ERROR_LIGHTMAP_TOO_SMALL);

TypedArray<RID> overrides;
Expand Down Expand Up @@ -882,15 +931,27 @@ 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));
}
} 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;
Expand Down Expand Up @@ -1061,7 +1122,32 @@ 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, 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,
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_LIGHTMAP_TOO_SMALL) {
return BAKE_ERROR_TEXTURE_SIZE_TOO_SMALL;
Expand Down Expand Up @@ -1299,7 +1385,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;
}
Expand Down Expand Up @@ -1536,6 +1627,10 @@ PackedStringArray LightmapGI::get_configuration_warnings() const {
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."));
}

return warnings;
}

Expand Down
Loading
Loading