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

Expose runtime baking functionality in LightmapGI #91676

Closed
Closed
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
11 changes: 11 additions & 0 deletions doc/classes/LightmapGI.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
<tutorials>
<link title="Using Lightmap global illumination">$DOCS_URL/tutorials/3d/global_illumination/using_lightmap_gi.html</link>
</tutorials>
<methods>
<method name="bake">
<return type="int" enum="LightmapGI.BakeError" />
<param index="0" name="from_node" type="Node" />
<param index="1" name="image_data_path" type="String" default="&quot;&quot;" />
<description>
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.
Comment on lines +23 to +24
Copy link
Member

@Calinou Calinou Jul 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
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.

</description>
</method>
</methods>
<members>
<member name="bias" type="float" setter="set_bias" getter="get_bias" default="0.0005">
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.
Expand Down
4 changes: 2 additions & 2 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);
} else {
err = lightmap->bake(lightmap->get_parent(), p_file, bake_func_step);
err = lightmap->_bake(lightmap->get_parent(), p_file, bake_func_step);
}
}
} else {
Expand Down
15 changes: 13 additions & 2 deletions modules/lightmapper_rd/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
def can_build(env, platform):
return env.editor_build and platform not in ["android", "ios"]
return (env.editor_build and platform not in ["android", "ios"]) or env["module_lightmapper_rd_enabled"]


def configure(env):
pass
from SCons.Script import BoolVariable, Variables, Help

envvars = Variables()
envvars.Add(
BoolVariable(
"lightmapper_rd",
"Enable Lightmapper functionality in export template builds (increases binary size)",
False,
)
)
Comment on lines +8 to +15
Copy link
Member

@Calinou Calinou Jul 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why we need a separate lightmapper_rd option for this, considering we already have module_lightmapper_rd_enabled option that is True by default in editor builds and False by default in export template builds.

See also #73003.

envvars.Update(env)
Help(envvars.GenerateHelpText(env))
7 changes: 6 additions & 1 deletion modules/lightmapper_rd/lightmapper_rd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,9 @@ LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref<RDSh

LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_denoiser, float p_denoiser_strength, 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<Image> &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function, void *p_bake_userdata, float p_exposure_normalization) {
int denoiser = GLOBAL_GET("rendering/lightmapping/denoising/denoiser");
String oidn_path = EDITOR_GET("filesystem/tools/oidn/oidn_denoise_path");

#ifdef TOOLS_ENABLED
String oidn_path = p_use_denoiser ? EDITOR_GET("filesystem/tools/oidn/oidn_denoise_path") : Variant();

if (p_use_denoiser && denoiser == 1) {
// OIDN (external).
Expand All @@ -993,6 +995,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
}
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);
Expand Down Expand Up @@ -1788,8 +1791,10 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
{
BakeError error;
if (denoiser == 1) {
#ifdef TOOLS_ENABLED
// OIDN (external).
error = _denoise_oidn(rd, light_accum_tex, normal_tex, light_accum_tex, atlas_size, atlas_slices, p_bake_sh, oidn_path);
#endif
} else {
// JNLM (built-in).
SWAP(light_accum_tex, light_accum_tex2);
Expand Down
15 changes: 13 additions & 2 deletions modules/xatlas_unwrap/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
def can_build(env, platform):
return env.editor_build and platform not in ["android", "ios"]
return env.editor_build and platform not in ["android", "ios"] or env["module_xatlas_unwrap_enabled"]


def configure(env):
pass
from SCons.Script import BoolVariable, Variables, Help

envvars = Variables()
envvars.Add(
BoolVariable(
"xatlas_unwrap",
"Enable xatlas unwrapping functionality in export template builds (increases binary size)",
False,
)
)
Comment on lines +8 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will work on it! Thanks.

envvars.Update(env)
Help(envvars.GenerateHelpText(env))
105 changes: 76 additions & 29 deletions scene/3d/lightmap_gi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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;
Expand Down Expand Up @@ -728,7 +730,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;
Expand Down Expand Up @@ -1071,13 +1083,32 @@ 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));
Sky::RadianceSize old_radiance_size = Sky::RADIANCE_SIZE_MAX;
if (!Engine::get_singleton()->is_editor_hint()) {
Ref<Sky> 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> 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;
Expand Down Expand Up @@ -1141,34 +1172,47 @@ 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";
if (Engine::get_singleton()->is_editor_hint()) {
String texture_path = texture_count > 1 ? base_path + "_" + itos(i) + ".exr" : base_path + ".exr";
Ref<ConfigFile> config;
config.instantiate();

Ref<ConfigFile> config;
config.instantiate();
if (FileAccess::exists(texture_path + ".import")) {
config->load(texture_path + ".import");
}

if (FileAccess::exists(texture_path + ".import")) {
config->load(texture_path + ".import");
}
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<TextureLayered> 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<Texture2DArray> texs;
texs.instantiate();
texs->create_from_images(images);

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);
Error err = ResourceSaver::save(texs, texture_path);
ERR_FAIL_COND_V(err, BAKE_ERROR_CANT_CREATE_IMAGE);
Ref<TextureLayered> 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<TextureLayered> 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;
}
}

Expand Down Expand Up @@ -1334,7 +1378,10 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa
}

gi_data->set_path(p_image_data_path);
Error err = ResourceSaver::save(gi_data);

String base_path = p_image_data_path.get_basename();
String new_bake_file_path = base_path + ".lmbake";
Error err = ResourceSaver::save(gi_data, new_bake_file_path);

if (err != OK) {
return BAKE_ERROR_CANT_CREATE_IMAGE;
Expand Down Expand Up @@ -1650,7 +1697,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");
Expand Down
6 changes: 5 additions & 1 deletion scene/3d/lightmap_gi.h
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,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;

Expand Down