Skip to content

Commit

Permalink
Expose runtime baking functionality in LightmapGI
Browse files Browse the repository at this point in the history
Co-authored-by: Rising Thumb <[email protected]>
  • Loading branch information
sourcelocation and RisingThumb committed Sep 30, 2024
1 parent e3213aa commit 87469db
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 47 deletions.
12 changes: 12 additions & 0 deletions doc/classes/LightmapGI.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@
<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. 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.
</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
2 changes: 1 addition & 1 deletion modules/lightmapper_rd/config.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
39 changes: 27 additions & 12 deletions modules/lightmapper_rd/lightmapper_rd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -881,6 +883,7 @@ Ref<Image> 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<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);

Expand Down Expand Up @@ -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<RDShaderFile> &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));
Expand Down Expand Up @@ -1024,22 +1028,28 @@ 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_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<Image> &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<DirAccess> 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<DirAccess> 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);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion modules/xatlas_unwrap/config.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
100 changes: 70 additions & 30 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 @@ -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;
Expand Down Expand Up @@ -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> 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 @@ -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<ConfigFile> 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<ConfigFile> 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<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);

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 @@ -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");
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 @@ -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;

Expand Down

0 comments on commit 87469db

Please sign in to comment.