diff --git a/doc/classes/BakedLightmap.xml b/doc/classes/BakedLightmap.xml
new file mode 100644
index 000000000..6bad4a3cc
--- /dev/null
+++ b/doc/classes/BakedLightmap.xml
@@ -0,0 +1,151 @@
+
+
+
+ Prerendered indirect light map for a scene.
+
+
+ Baked lightmaps are an alternative workflow for adding indirect (or baked) lighting to a scene. Unlike the [GIProbe] approach, baked lightmaps work fine on low-end PCs and mobile devices as they consume almost no resources in run-time.
+ [b]Procedural generation:[/b] Lightmap baking functionality is only available in the editor. This means [BakedLightmap] is not suited to procedurally generated or user-built levels. For procedurally generated or user-built levels, use [GIProbe] instead.
+ [b]Note:[/b] Due to how lightmaps work, most properties only have a visible effect once lightmaps are baked again.
+
+
+ $DOCS_URL/tutorials/3d/baked_lightmaps.html
+
+
+
+
+
+
+
+ Bakes the lightmap, scanning from the given [code]from_node[/code] root and saves the resulting [BakedLightmapData] in [code]data_save_path[/code]. If no root node is provided, parent of this node will be used as root instead. If no save path is provided it will try to match the path from the current [member light_data].
+
+
+
+
+
+ If [code]true[/code], the lightmapper will merge the textures for all meshes into one or several large layered textures. If [code]false[/code], every mesh will get its own lightmap texture, which is less efficient.
+ [b]Note:[/b] Atlas lightmap rendering is only supported in GLES3, [i]not[/i] GLES2. Non-atlas lightmap rendering is supported by both GLES3 and GLES2. If [member ProjectSettings.rendering/quality/driver/fallback_to_gles2] is [code]true[/code], consider baking lightmaps with [member atlas_generate] set to [code]false[/code] so that the resulting lightmap is visible in both GLES3 and GLES2.
+
+
+ Maximum size of each lightmap layer, only used when [member atlas_generate] is enabled.
+
+
+ Raycasting bias used during baking to avoid floating point precision issues.
+
+
+ The energy multiplier for each bounce. Higher values will make indirect lighting brighter. A value of [code]1.0[/code] represents physically accurate behavior, but higher values can be used to make indirect lighting propagate more visibly when using a low number of bounces. This can be used to speed up bake times by lowering the number of [member bounces] then increasing [member bounce_indirect_energy]. Unlike [member BakedLightmapData.energy], this property does not affect direct lighting emitted by light nodes, emissive materials and the environment.
+ [b]Note:[/b] [member bounce_indirect_energy] only has an effect if [member bounces] is set to a value greater than or equal to [code]1[/code].
+
+
+ Number of light bounces that are taken into account during baking. See also [member bounce_indirect_energy].
+
+
+ Grid size used for real-time capture information on dynamic objects.
+
+
+ When enabled, an octree containing the scene's lighting information will be computed. This octree will then be used to light dynamic objects in the scene.
+
+
+ Bias value to reduce the amount of light propagation in the captured octree.
+
+
+ Bake quality of the capture data.
+
+
+ If a baked mesh doesn't have a UV2 size hint, this value will be used to roughly compute a suitable lightmap size.
+
+
+ The environment color when [member environment_mode] is set to [constant ENVIRONMENT_MODE_CUSTOM_COLOR].
+
+
+ The energy scaling factor when when [member environment_mode] is set to [constant ENVIRONMENT_MODE_CUSTOM_COLOR] or [constant ENVIRONMENT_MODE_CUSTOM_SKY].
+
+
+ The [Sky] resource to use when [member environment_mode] is set o [constant ENVIRONMENT_MODE_CUSTOM_SKY].
+
+
+ The rotation of the baked custom sky.
+
+
+ Minimum ambient light for all the lightmap texels. This doesn't take into account any occlusion from the scene's geometry, it simply ensures a minimum amount of light on all the lightmap texels. Can be used for artistic control on shadow color.
+
+
+ Decides which environment to use during baking.
+
+
+ Size of the baked lightmap. Only meshes inside this region will be included in the baked lightmap, also used as the bounds of the captured region for dynamic lighting.
+
+
+ Deprecated, in previous versions it determined the location where lightmaps were be saved.
+
+
+ The calculated light data.
+
+
+ Determines the amount of samples per texel used in indirect light baking. The amount of samples for each quality level can be configured in the project settings.
+
+
+ Store full color values in the lightmap textures. When disabled, lightmap textures will store a single brightness channel. Can be disabled to reduce disk usage if the scene contains only white lights or you don't mind losing color information in indirect lighting.
+
+
+ When enabled, a lightmap denoiser will be used to reduce the noise inherent to Monte Carlo based global illumination.
+
+
+ If [code]true[/code], stores the lightmap textures in a high dynamic range format (EXR). If [code]false[/code], stores the lightmap texture in a low dynamic range PNG image. This can be set to [code]false[/code] to reduce disk usage, but light values over 1.0 will be clamped and you may see banding caused by the reduced precision.
+ [b]Note:[/b] Setting [member use_hdr] to [code]true[/code] will decrease lightmap banding even when using the GLES2 backend or if [member ProjectSettings.rendering/quality/depth/hdr] is [code]false[/code].
+
+
+
+
+ The lowest bake quality mode. Fastest to calculate.
+
+
+ The default bake quality mode.
+
+
+ A higher bake quality mode. Takes longer to calculate.
+
+
+ The highest bake quality mode. Takes the longest to calculate.
+
+
+ Baking was successful.
+
+
+ Returns if no viable save path is found. This can happen where an [member image_path] is not specified or when the save location is invalid.
+
+
+ Currently unused.
+
+
+ Returns when the baker cannot save per-mesh textures to file.
+
+
+ The size of the generated lightmaps is too large.
+
+
+ Some mesh contains UV2 values outside the [code][0,1][/code] range.
+
+
+ Returns if user cancels baking.
+
+
+ Returns if lightmapper can't be created. Unless you are using a custom lightmapper, please report this as bug.
+
+
+ There is no root node to start baking from. Either provide [code]from_node[/code] argument or attach this node to a parent that should be used as root.
+
+
+ No environment is used during baking.
+
+
+ The baked environment is automatically picked from the current scene.
+
+
+ A custom sky is used as environment during baking.
+
+
+ A custom solid color is used as environment during baking.
+
+
+
diff --git a/doc/classes/BakedLightmapData.xml b/doc/classes/BakedLightmapData.xml
new file mode 100644
index 000000000..8cce1d0f0
--- /dev/null
+++ b/doc/classes/BakedLightmapData.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Global energy multiplier for baked and dynamic capture objects. This can be changed at run-time without having to bake lightmaps again.
+ To adjust only the energy of indirect lighting (without affecting direct lighting or emissive materials), adjust [member BakedLightmap.bounce_indirect_energy] and bake lightmaps again.
+
+
+ Controls whether dynamic capture objects receive environment lighting or not.
+
+
+
+
+
+
+
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 5b28df111..115d349a5 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -109,6 +109,7 @@
#include "editor/plugins/animation_player_editor_plugin.h"
#include "editor/plugins/animation_tree_editor_plugin.h"
#include "editor/plugins/audio_stream_editor_plugin.h"
+#include "editor/plugins/baked_lightmap_editor_plugin.h"
#include "editor/plugins/bit_map_editor_plugin.h"
#include "editor/plugins/camera_editor_plugin.h"
#include "editor/plugins/canvas_item_editor_plugin.h"
@@ -7171,6 +7172,7 @@ EditorNode::EditorNode() {
add_editor_plugin(memnew(CollisionPolygon2DEditorPlugin(this)));
add_editor_plugin(memnew(SpriteFramesEditorPlugin(this)));
add_editor_plugin(memnew(TextureRegionEditorPlugin(this)));
+ add_editor_plugin(memnew(BakedLightmapEditorPlugin(this)));
add_editor_plugin(memnew(RoomManagerEditorPlugin(this)));
add_editor_plugin(memnew(RoomEditorPlugin(this)));
add_editor_plugin(memnew(OccluderEditorPlugin(this)));
diff --git a/editor/plugins/baked_lightmap_editor_plugin.cpp b/editor/plugins/baked_lightmap_editor_plugin.cpp
new file mode 100644
index 000000000..13ec414b0
--- /dev/null
+++ b/editor/plugins/baked_lightmap_editor_plugin.cpp
@@ -0,0 +1,179 @@
+/**************************************************************************/
+/* baked_lightmap_editor_plugin.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "baked_lightmap_editor_plugin.h"
+
+#include "editor/editor_file_dialog.h"
+
+void BakedLightmapEditorPlugin::_bake_select_file(const String &p_file) {
+ if (lightmap) {
+ BakedLightmap::BakeError err;
+ if (get_tree()->get_edited_scene_root() && get_tree()->get_edited_scene_root() == lightmap) {
+ err = lightmap->bake(lightmap, p_file);
+ } else {
+ err = lightmap->bake(lightmap->get_parent(), p_file);
+ }
+
+ switch (err) {
+ case BakedLightmap::BAKE_ERROR_NO_SAVE_PATH: {
+ String scene_path = lightmap->get_filename();
+ if (scene_path == String()) {
+ scene_path = lightmap->get_owner()->get_filename();
+ }
+ if (scene_path == String()) {
+ EditorNode::get_singleton()->show_warning(TTR("Can't determine a save path for lightmap images.\nSave your scene and try again."));
+ break;
+ }
+ scene_path = scene_path.get_basename() + ".lmbake";
+
+ file_dialog->set_current_path(scene_path);
+ file_dialog->popup_centered_ratio();
+
+ } break;
+ case BakedLightmap::BAKE_ERROR_NO_MESHES:
+ EditorNode::get_singleton()->show_warning(TTR("No meshes to bake. Make sure they contain an UV2 channel and that the 'Use In Baked Light' and 'Generate Lightmap' flags are on."));
+ break;
+ case BakedLightmap::BAKE_ERROR_CANT_CREATE_IMAGE:
+ EditorNode::get_singleton()->show_warning(TTR("Failed creating lightmap images, make sure path is writable."));
+ break;
+ case BakedLightmap::BAKE_ERROR_LIGHTMAP_SIZE:
+ EditorNode::get_singleton()->show_warning(TTR("Failed determining lightmap size. Maximum lightmap size too small?"));
+ break;
+ case BakedLightmap::BAKE_ERROR_INVALID_MESH:
+ EditorNode::get_singleton()->show_warning(TTR("Some mesh is invalid. Make sure the UV2 channel values are contained within the [0.0,1.0] square region."));
+ break;
+ case BakedLightmap::BAKE_ERROR_NO_LIGHTMAPPER:
+ EditorNode::get_singleton()->show_warning(TTR("Pandemonium editor was built without ray tracing support, lightmaps can't be baked."));
+ break;
+ default: {
+ }
+ }
+ }
+}
+
+void BakedLightmapEditorPlugin::_bake() {
+ _bake_select_file("");
+}
+
+void BakedLightmapEditorPlugin::edit(Object *p_object) {
+ BakedLightmap *s = Object::cast_to(p_object);
+ if (!s) {
+ return;
+ }
+
+ lightmap = s;
+}
+
+bool BakedLightmapEditorPlugin::handles(Object *p_object) const {
+ return p_object->is_class("BakedLightmap");
+}
+
+void BakedLightmapEditorPlugin::make_visible(bool p_visible) {
+ if (p_visible) {
+ bake->show();
+ } else {
+ bake->hide();
+ }
+}
+
+EditorProgress *BakedLightmapEditorPlugin::tmp_progress = nullptr;
+EditorProgress *BakedLightmapEditorPlugin::tmp_subprogress = nullptr;
+
+bool BakedLightmapEditorPlugin::bake_func_step(float p_progress, const String &p_description, void *, bool p_force_refresh) {
+ if (!tmp_progress) {
+ tmp_progress = memnew(EditorProgress("bake_lightmaps", TTR("Bake Lightmaps"), 1000, true));
+ ERR_FAIL_COND_V(tmp_progress == nullptr, false);
+ }
+ return tmp_progress->step(p_description, p_progress * 1000, p_force_refresh);
+}
+
+bool BakedLightmapEditorPlugin::bake_func_substep(float p_progress, const String &p_description, void *, bool p_force_refresh) {
+ if (!tmp_subprogress) {
+ tmp_subprogress = memnew(EditorProgress("bake_lightmaps_substep", "", 1000, true));
+ ERR_FAIL_COND_V(tmp_subprogress == nullptr, false);
+ }
+ return tmp_subprogress->step(p_description, p_progress * 1000, p_force_refresh);
+}
+
+void BakedLightmapEditorPlugin::bake_func_end(uint32_t p_time_started) {
+ if (tmp_progress != nullptr) {
+ memdelete(tmp_progress);
+ tmp_progress = nullptr;
+ }
+
+ if (tmp_subprogress != nullptr) {
+ memdelete(tmp_subprogress);
+ tmp_subprogress = nullptr;
+ }
+
+ const int time_taken = (OS::get_singleton()->get_ticks_msec() - p_time_started) * 0.001;
+ if (time_taken >= 1) {
+ // Only print a message and request attention if baking lightmaps took at least 1 second.
+ // Otherwise, attempting to bake in an erroneous situation (e.g. no meshes to bake)
+ // would print the "done baking lightmaps" message and request attention for no good reason.
+ print_line(vformat("Done baking lightmaps in %02d:%02d:%02d.", time_taken / 3600, (time_taken % 3600) / 60, time_taken % 60));
+
+ // Request attention in case the user was doing something else.
+ // Baking lightmaps is likely the editor task that can take the most time,
+ // so only request the attention for baking lightmaps.
+ OS::get_singleton()->request_attention();
+ }
+}
+
+void BakedLightmapEditorPlugin::_bind_methods() {
+ ClassDB::bind_method("_bake", &BakedLightmapEditorPlugin::_bake);
+ ClassDB::bind_method("_bake_select_file", &BakedLightmapEditorPlugin::_bake_select_file);
+}
+
+BakedLightmapEditorPlugin::BakedLightmapEditorPlugin(EditorNode *p_node) {
+ editor = p_node;
+ bake = memnew(ToolButton);
+ bake->set_icon(editor->get_gui_base()->get_theme_icon("Bake", "EditorIcons"));
+ bake->set_tooltip(TTR("Bake Lightmaps"));
+ bake->hide();
+ bake->connect("pressed", this, "_bake");
+
+ file_dialog = memnew(EditorFileDialog);
+ file_dialog->set_mode(EditorFileDialog::MODE_SAVE_FILE);
+ file_dialog->add_filter("*.lmbake ; " + TTR("LightMap Bake"));
+ file_dialog->set_title(TTR("Select lightmap bake file:"));
+ file_dialog->connect("file_selected", this, "_bake_select_file");
+ bake->add_child(file_dialog);
+
+ add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, bake);
+ lightmap = nullptr;
+
+ BakedLightmap::bake_step_function = bake_func_step;
+ BakedLightmap::bake_substep_function = bake_func_substep;
+ BakedLightmap::bake_end_function = bake_func_end;
+}
+
+BakedLightmapEditorPlugin::~BakedLightmapEditorPlugin() {
+}
diff --git a/editor/plugins/baked_lightmap_editor_plugin.h b/editor/plugins/baked_lightmap_editor_plugin.h
new file mode 100644
index 000000000..74e1077b9
--- /dev/null
+++ b/editor/plugins/baked_lightmap_editor_plugin.h
@@ -0,0 +1,74 @@
+/**************************************************************************/
+/* baked_lightmap_editor_plugin.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef BAKED_LIGHTMAP_EDITOR_PLUGIN_H
+#define BAKED_LIGHTMAP_EDITOR_PLUGIN_H
+
+#include "editor/editor_node.h"
+#include "editor/editor_plugin.h"
+#include "scene/3d/baked_lightmap.h"
+#include "scene/resources/material/material.h"
+
+class EditorFileDialog;
+
+class BakedLightmapEditorPlugin : public EditorPlugin {
+ GDCLASS(BakedLightmapEditorPlugin, EditorPlugin);
+
+ BakedLightmap *lightmap;
+
+ ToolButton *bake;
+ EditorNode *editor;
+
+ EditorFileDialog *file_dialog;
+ static EditorProgress *tmp_progress;
+ static EditorProgress *tmp_subprogress;
+
+ static bool bake_func_step(float p_progress, const String &p_description, void *, bool p_force_refresh);
+ static bool bake_func_substep(float p_progress, const String &p_description, void *, bool p_force_refresh);
+ static void bake_func_end(uint32_t p_time_started);
+
+ void _bake_select_file(const String &p_file);
+ void _bake();
+
+protected:
+ static void _bind_methods();
+
+public:
+ virtual String get_name() const { return "BakedLightmap"; }
+ bool has_main_screen() const { return false; }
+ virtual void edit(Object *p_object);
+ virtual bool handles(Object *p_object) const;
+ virtual void make_visible(bool p_visible);
+
+ BakedLightmapEditorPlugin(EditorNode *p_node);
+ ~BakedLightmapEditorPlugin();
+};
+
+#endif // BAKED_LIGHTMAP_EDITOR_PLUGIN_H
diff --git a/editor/plugins/spatial_editor_plugin.cpp b/editor/plugins/spatial_editor_plugin.cpp
index d5bbf2575..9a45b5395 100644
--- a/editor/plugins/spatial_editor_plugin.cpp
+++ b/editor/plugins/spatial_editor_plugin.cpp
@@ -7188,6 +7188,7 @@ void SpatialEditor::_register_all_gizmos() {
add_gizmo_plugin(Ref(memnew(VisibilityNotifierGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(CPUParticlesGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(ReflectionProbeGizmoPlugin)));
+ add_gizmo_plugin(Ref(memnew(BakedIndirectLightGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(CollisionObjectGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(CollisionShapeSpatialGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(CollisionPolygonSpatialGizmoPlugin)));
diff --git a/editor/spatial_editor_gizmos.cpp b/editor/spatial_editor_gizmos.cpp
index dbf8bbf8d..e14c26db5 100644
--- a/editor/spatial_editor_gizmos.cpp
+++ b/editor/spatial_editor_gizmos.cpp
@@ -56,6 +56,7 @@
#include "editor/editor_node.h"
#include "editor/editor_settings.h"
#include "scene/3d/audio_stream_player_3d.h"
+#include "scene/3d/baked_lightmap.h"
#include "scene/3d/camera.h"
#include "scene/3d/collision_object.h"
#include "scene/3d/collision_polygon.h"
@@ -2848,6 +2849,135 @@ void ReflectionProbeGizmoPlugin::redraw(EditorSpatialGizmo *p_gizmo) {
////
+BakedIndirectLightGizmoPlugin::BakedIndirectLightGizmoPlugin() {
+ Color gizmo_color = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/baked_indirect_light", Color(0.5, 0.6, 1));
+
+ create_material("baked_indirect_light_material", gizmo_color);
+
+ gizmo_color.a = 0.1;
+ create_material("baked_indirect_light_internal_material", gizmo_color);
+
+ create_icon_material("baked_indirect_light_icon", SpatialEditor::get_singleton()->get_theme_icon("GizmoBakedLightmap", "EditorIcons"));
+ create_handle_material("handles");
+}
+
+String BakedIndirectLightGizmoPlugin::get_handle_name(const EditorSpatialGizmo *p_gizmo, int p_id, bool p_secondary) const {
+ switch (p_id) {
+ case 0:
+ return "Extents X";
+ case 1:
+ return "Extents Y";
+ case 2:
+ return "Extents Z";
+ }
+
+ return "";
+}
+Variant BakedIndirectLightGizmoPlugin::get_handle_value(EditorSpatialGizmo *p_gizmo, int p_id, bool p_secondary) const {
+ BakedLightmap *baker = Object::cast_to(p_gizmo->get_spatial_node());
+ return baker->get_extents();
+}
+void BakedIndirectLightGizmoPlugin::set_handle(EditorSpatialGizmo *p_gizmo, int p_id, bool p_secondary, Camera *p_camera, const Point2 &p_point) {
+ BakedLightmap *baker = Object::cast_to(p_gizmo->get_spatial_node());
+
+ Transform gt = baker->get_global_transform();
+ Transform gi = gt.affine_inverse();
+
+ Vector3 extents = baker->get_extents();
+
+ Vector3 ray_from = p_camera->project_ray_origin(p_point);
+ Vector3 ray_dir = p_camera->project_ray_normal(p_point);
+
+ Vector3 sg[2] = { gi.xform(ray_from), gi.xform(ray_from + ray_dir * 16384) };
+
+ Vector3 axis;
+ axis[p_id] = 1.0;
+
+ Vector3 ra, rb;
+ Geometry::get_closest_points_between_segments(Vector3(), axis * 16384, sg[0], sg[1], ra, rb);
+ float d = ra[p_id];
+ if (SpatialEditor::get_singleton()->is_snap_enabled()) {
+ d = Math::stepify(d, SpatialEditor::get_singleton()->get_translate_snap());
+ }
+
+ if (d < 0.001) {
+ d = 0.001;
+ }
+
+ extents[p_id] = d;
+ baker->set_extents(extents);
+}
+
+void BakedIndirectLightGizmoPlugin::commit_handle(EditorSpatialGizmo *p_gizmo, int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel) {
+ BakedLightmap *baker = Object::cast_to(p_gizmo->get_spatial_node());
+
+ Vector3 restore = p_restore;
+
+ if (p_cancel) {
+ baker->set_extents(restore);
+ return;
+ }
+
+ UndoRedo *ur = SpatialEditor::get_singleton()->get_undo_redo();
+ ur->create_action(TTR("Change Probe Extents"));
+ ur->add_do_method(baker, "set_extents", baker->get_extents());
+ ur->add_undo_method(baker, "set_extents", restore);
+ ur->commit_action();
+}
+
+bool BakedIndirectLightGizmoPlugin::has_gizmo(Spatial *p_spatial) {
+ return Object::cast_to(p_spatial) != nullptr;
+}
+
+String BakedIndirectLightGizmoPlugin::get_gizmo_name() const {
+ return "BakedLightmap";
+}
+
+int BakedIndirectLightGizmoPlugin::get_priority() const {
+ return -1;
+}
+
+void BakedIndirectLightGizmoPlugin::redraw(EditorSpatialGizmo *p_gizmo) {
+ BakedLightmap *baker = Object::cast_to(p_gizmo->get_spatial_node());
+
+ Ref material = get_material("baked_indirect_light_material", p_gizmo);
+ Ref icon = get_material("baked_indirect_light_icon", p_gizmo);
+ Ref material_internal = get_material("baked_indirect_light_internal_material", p_gizmo);
+
+ p_gizmo->clear();
+
+ Vector lines;
+ Vector3 extents = baker->get_extents();
+
+ AABB aabb = AABB(-extents, extents * 2);
+
+ for (int i = 0; i < 12; i++) {
+ Vector3 a, b;
+ aabb.get_edge(i, a, b);
+ lines.push_back(a);
+ lines.push_back(b);
+ }
+
+ p_gizmo->add_lines(lines, material);
+
+ Vector handles;
+
+ for (int i = 0; i < 3; i++) {
+ Vector3 ax;
+ ax[i] = aabb.position[i] + aabb.size[i];
+ handles.push_back(ax);
+ }
+
+ if (p_gizmo->is_selected()) {
+ p_gizmo->add_solid_box(material_internal, aabb.get_size());
+ }
+
+ p_gizmo->add_unscaled_billboard(icon, 0.05);
+ p_gizmo->add_handles(handles, get_material("handles"));
+}
+
+////
+
CollisionObjectGizmoPlugin::CollisionObjectGizmoPlugin() {
const Color gizmo_color = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/shape", Color(0.5, 0.7, 1));
create_material("shape_material", gizmo_color);
diff --git a/editor/spatial_editor_gizmos.h b/editor/spatial_editor_gizmos.h
index d9404aab8..34245e031 100644
--- a/editor/spatial_editor_gizmos.h
+++ b/editor/spatial_editor_gizmos.h
@@ -453,6 +453,23 @@ public:
ReflectionProbeGizmoPlugin();
};
+class BakedIndirectLightGizmoPlugin : public EditorSpatialGizmoPlugin {
+ GDCLASS(BakedIndirectLightGizmoPlugin, EditorSpatialGizmoPlugin);
+
+public:
+ bool has_gizmo(Spatial *p_spatial);
+ String get_gizmo_name() const;
+ int get_priority() const;
+ void redraw(EditorSpatialGizmo *p_gizmo);
+
+ String get_handle_name(const EditorSpatialGizmo *p_gizmo, int p_id, bool p_secondary) const;
+ Variant get_handle_value(EditorSpatialGizmo *p_gizmo, int p_id, bool p_secondary) const;
+ void set_handle(EditorSpatialGizmo *p_gizmo, int p_id, bool p_secondary, Camera *p_camera, const Point2 &p_point);
+ void commit_handle(EditorSpatialGizmo *p_gizmo, int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel = false);
+
+ BakedIndirectLightGizmoPlugin();
+};
+
class CollisionObjectGizmoPlugin : public EditorSpatialGizmoPlugin {
GDCLASS(CollisionObjectGizmoPlugin, EditorSpatialGizmoPlugin);
diff --git a/scene/3d/baked_lightmap.cpp b/scene/3d/baked_lightmap.cpp
new file mode 100644
index 000000000..7f391cc81
--- /dev/null
+++ b/scene/3d/baked_lightmap.cpp
@@ -0,0 +1,1668 @@
+/**************************************************************************/
+/* baked_lightmap.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "baked_lightmap.h"
+#include "core/io/config_file.h"
+#include "core/io/resource_saver.h"
+#include "core/math/math_defs.h"
+#include "core/os/dir_access.h"
+#include "core/os/os.h"
+#include "scene/resources/material/spatial_material.h"
+#include "voxel_light_baker.h"
+#include "scene/resources/sky.h"
+#include "scene/resources/environment_3d.h"
+#include "scene/resources/material/material.h"
+#include "scene/resources/world_3d.h"
+#include "core/config/project_settings.h"
+
+void BakedLightmapData::set_bounds(const AABB &p_bounds) {
+ bounds = p_bounds;
+ RS::get_singleton()->lightmap_capture_set_bounds(baked_light, p_bounds);
+}
+
+AABB BakedLightmapData::get_bounds() const {
+ return bounds;
+}
+
+void BakedLightmapData::set_octree(const PoolVector &p_octree) {
+ RS::get_singleton()->lightmap_capture_set_octree(baked_light, p_octree);
+}
+
+PoolVector BakedLightmapData::get_octree() const {
+ return RS::get_singleton()->lightmap_capture_get_octree(baked_light);
+}
+
+void BakedLightmapData::set_cell_space_transform(const Transform &p_xform) {
+ cell_space_xform = p_xform;
+ RS::get_singleton()->lightmap_capture_set_octree_cell_transform(baked_light, p_xform);
+}
+
+Transform BakedLightmapData::get_cell_space_transform() const {
+ return cell_space_xform;
+}
+
+void BakedLightmapData::set_cell_subdiv(int p_cell_subdiv) {
+ cell_subdiv = p_cell_subdiv;
+ RS::get_singleton()->lightmap_capture_set_octree_cell_subdiv(baked_light, p_cell_subdiv);
+}
+
+int BakedLightmapData::get_cell_subdiv() const {
+ return cell_subdiv;
+}
+
+void BakedLightmapData::set_energy(float p_energy) {
+ energy = p_energy;
+ RS::get_singleton()->lightmap_capture_set_energy(baked_light, energy);
+}
+
+float BakedLightmapData::get_energy() const {
+ return energy;
+}
+
+void BakedLightmapData::set_interior(bool p_interior) {
+ interior = p_interior;
+ RS::get_singleton()->lightmap_capture_set_interior(baked_light, interior);
+}
+
+bool BakedLightmapData::is_interior() const {
+ return interior;
+}
+
+void BakedLightmapData::add_user(const NodePath &p_path, const Ref &p_lightmap, int p_lightmap_slice, const Rect2 &p_lightmap_uv_rect, int p_instance) {
+ ERR_FAIL_COND_MSG(p_lightmap.is_null(), "It's not a reference to a valid Texture object.");
+ ERR_FAIL_COND(p_lightmap_slice == -1 && !Object::cast_to(p_lightmap.ptr()));
+ ERR_FAIL_COND(p_lightmap_slice != -1 && !Object::cast_to(p_lightmap.ptr()));
+
+ User user;
+ user.path = p_path;
+ if (p_lightmap_slice == -1) {
+ user.lightmap.single = p_lightmap;
+ } else {
+ user.lightmap.layered = p_lightmap;
+ }
+ user.lightmap_slice = p_lightmap_slice;
+ user.lightmap_uv_rect = p_lightmap_uv_rect;
+ user.instance_index = p_instance;
+ users.push_back(user);
+}
+
+int BakedLightmapData::get_user_count() const {
+ return users.size();
+}
+NodePath BakedLightmapData::get_user_path(int p_user) const {
+ ERR_FAIL_INDEX_V(p_user, users.size(), NodePath());
+ return users[p_user].path;
+}
+Ref BakedLightmapData::get_user_lightmap(int p_user) const {
+ ERR_FAIL_INDEX_V(p_user, users.size(), Ref());
+ if (users[p_user].lightmap_slice == -1) {
+ return users[p_user].lightmap.single;
+ } else {
+ return users[p_user].lightmap.layered;
+ }
+}
+
+int BakedLightmapData::get_user_lightmap_slice(int p_user) const {
+ ERR_FAIL_INDEX_V(p_user, users.size(), -1);
+ return users[p_user].lightmap_slice;
+}
+
+Rect2 BakedLightmapData::get_user_lightmap_uv_rect(int p_user) const {
+ ERR_FAIL_INDEX_V(p_user, users.size(), Rect2(0, 0, 1, 1));
+ return users[p_user].lightmap_uv_rect;
+}
+
+int BakedLightmapData::get_user_instance(int p_user) const {
+ ERR_FAIL_INDEX_V(p_user, users.size(), -1);
+ return users[p_user].instance_index;
+}
+
+void BakedLightmapData::clear_users() {
+ users.clear();
+}
+
+void BakedLightmapData::clear_data() {
+ clear_users();
+ if (baked_light.is_valid()) {
+ RS::get_singleton()->free(baked_light);
+ }
+ baked_light = RID_PRIME(RS::get_singleton()->lightmap_capture_create());
+}
+
+void BakedLightmapData::_set_user_data(const Array &p_data) {
+ ERR_FAIL_COND(p_data.size() <= 0);
+
+ // Detect old lightmapper format
+ if (p_data.size() % 3 == 0) {
+ bool is_old_format = true;
+ for (int i = 0; i < p_data.size(); i += 3) {
+ is_old_format = is_old_format && p_data[i + 0].get_type() == Variant::NODE_PATH;
+ is_old_format = is_old_format && p_data[i + 1].is_ref();
+ is_old_format = is_old_format && p_data[i + 2].get_type() == Variant::INT;
+ if (!is_old_format) {
+ break;
+ }
+ }
+ if (is_old_format) {
+#ifdef DEBUG_ENABLED
+ WARN_PRINT("Geometry at path " + String(p_data[0]) + " is using old lightmapper data. Please re-bake.");
+#endif
+ Array adapted_data;
+ adapted_data.resize((p_data.size() / 3) * 5);
+ for (int i = 0; i < p_data.size() / 3; i++) {
+ adapted_data[i * 5 + 0] = p_data[i * 3 + 0];
+ adapted_data[i * 5 + 1] = p_data[i * 3 + 1];
+ adapted_data[i * 5 + 2] = -1;
+ adapted_data[i * 5 + 3] = Rect2(0, 0, 1, 1);
+ adapted_data[i * 5 + 4] = p_data[i * 3 + 2];
+ }
+ _set_user_data(adapted_data);
+ return;
+ }
+ }
+
+ ERR_FAIL_COND((p_data.size() % 5) != 0);
+
+ for (int i = 0; i < p_data.size(); i += 5) {
+ add_user(p_data[i], p_data[i + 1], p_data[i + 2], p_data[i + 3], p_data[i + 4]);
+ }
+}
+
+Array BakedLightmapData::_get_user_data() const {
+ Array ret;
+ for (int i = 0; i < users.size(); i++) {
+ ret.push_back(users[i].path);
+ ret.push_back(users[i].lightmap_slice == -1 ? Ref(users[i].lightmap.single) : Ref(users[i].lightmap.layered));
+ ret.push_back(users[i].lightmap_slice);
+ ret.push_back(users[i].lightmap_uv_rect);
+ ret.push_back(users[i].instance_index);
+ }
+ return ret;
+}
+
+RID BakedLightmapData::get_rid() const {
+ return baked_light;
+}
+void BakedLightmapData::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("_set_user_data", "data"), &BakedLightmapData::_set_user_data);
+ ClassDB::bind_method(D_METHOD("_get_user_data"), &BakedLightmapData::_get_user_data);
+
+ ClassDB::bind_method(D_METHOD("set_bounds", "bounds"), &BakedLightmapData::set_bounds);
+ ClassDB::bind_method(D_METHOD("get_bounds"), &BakedLightmapData::get_bounds);
+
+ ClassDB::bind_method(D_METHOD("set_cell_space_transform", "xform"), &BakedLightmapData::set_cell_space_transform);
+ ClassDB::bind_method(D_METHOD("get_cell_space_transform"), &BakedLightmapData::get_cell_space_transform);
+
+ ClassDB::bind_method(D_METHOD("set_cell_subdiv", "cell_subdiv"), &BakedLightmapData::set_cell_subdiv);
+ ClassDB::bind_method(D_METHOD("get_cell_subdiv"), &BakedLightmapData::get_cell_subdiv);
+
+ ClassDB::bind_method(D_METHOD("set_octree", "octree"), &BakedLightmapData::set_octree);
+ ClassDB::bind_method(D_METHOD("get_octree"), &BakedLightmapData::get_octree);
+
+ ClassDB::bind_method(D_METHOD("set_energy", "energy"), &BakedLightmapData::set_energy);
+ ClassDB::bind_method(D_METHOD("get_energy"), &BakedLightmapData::get_energy);
+
+ ClassDB::bind_method(D_METHOD("set_interior", "interior"), &BakedLightmapData::set_interior);
+ ClassDB::bind_method(D_METHOD("is_interior"), &BakedLightmapData::is_interior);
+
+ ClassDB::bind_method(D_METHOD("add_user", "path", "lightmap", "lightmap_slice", "lightmap_uv_rect", "instance"), &BakedLightmapData::add_user);
+ ClassDB::bind_method(D_METHOD("get_user_count"), &BakedLightmapData::get_user_count);
+ ClassDB::bind_method(D_METHOD("get_user_path", "user_idx"), &BakedLightmapData::get_user_path);
+ ClassDB::bind_method(D_METHOD("get_user_lightmap", "user_idx"), &BakedLightmapData::get_user_lightmap);
+ ClassDB::bind_method(D_METHOD("clear_users"), &BakedLightmapData::clear_users);
+ ClassDB::bind_method(D_METHOD("clear_data"), &BakedLightmapData::clear_data);
+
+ ADD_PROPERTY(PropertyInfo(Variant::AABB, "bounds", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "set_bounds", "get_bounds");
+ ADD_PROPERTY(PropertyInfo(Variant::TRANSFORM, "cell_space_transform", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "set_cell_space_transform", "get_cell_space_transform");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "cell_subdiv", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "set_cell_subdiv", "get_cell_subdiv");
+ ADD_PROPERTY(PropertyInfo(Variant::REAL, "energy", PROPERTY_HINT_RANGE, "0,16,0.01,or_greater"), "set_energy", "get_energy");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "interior"), "set_interior", "is_interior");
+ ADD_PROPERTY(PropertyInfo(Variant::POOL_BYTE_ARRAY, "octree", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "set_octree", "get_octree");
+ ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "user_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL), "_set_user_data", "_get_user_data");
+}
+
+BakedLightmapData::BakedLightmapData() {
+ baked_light = RID_PRIME(RS::get_singleton()->lightmap_capture_create());
+ energy = 1;
+ cell_subdiv = 1;
+ interior = false;
+}
+
+BakedLightmapData::~BakedLightmapData() {
+ RS::get_singleton()->free(baked_light);
+}
+
+///////////////////////////
+
+Lightmapper::BakeStepFunc BakedLightmap::bake_step_function;
+Lightmapper::BakeStepFunc BakedLightmap::bake_substep_function;
+Lightmapper::BakeEndFunc BakedLightmap::bake_end_function;
+
+Size2i BakedLightmap::_compute_lightmap_size(const MeshesFound &p_mesh) {
+ double area = 0;
+ double uv_area = 0;
+ for (int i = 0; i < p_mesh.mesh->get_surface_count(); i++) {
+ Array arrays = p_mesh.mesh->surface_get_arrays(i);
+ PoolVector vertices = arrays[Mesh::ARRAY_VERTEX];
+ PoolVector uv2 = arrays[Mesh::ARRAY_TEX_UV2];
+ PoolVector indices = arrays[Mesh::ARRAY_INDEX];
+
+ ERR_FAIL_COND_V(vertices.size() == 0, Vector2());
+ ERR_FAIL_COND_V(uv2.size() == 0, Vector2());
+
+ int vc = vertices.size();
+ PoolVector::Read vr = vertices.read();
+ PoolVector::Read u2r = uv2.read();
+ PoolVector::Read ir;
+ int ic = 0;
+
+ if (indices.size()) {
+ ic = indices.size();
+ ir = indices.read();
+ }
+
+ int faces = ic ? ic / 3 : vc / 3;
+ for (int j = 0; j < faces; j++) {
+ Vector3 vertex[3];
+ Vector2 uv[3];
+
+ for (int k = 0; k < 3; k++) {
+ int idx = ic ? ir[j * 3 + k] : j * 3 + k;
+ vertex[k] = p_mesh.xform.xform(vr[idx]);
+ uv[k] = u2r[idx];
+ }
+
+ Vector3 p1 = vertex[0];
+ Vector3 p2 = vertex[1];
+ Vector3 p3 = vertex[2];
+ double a = p1.distance_to(p2);
+ double b = p2.distance_to(p3);
+ double c = p3.distance_to(p1);
+ double halfPerimeter = (a + b + c) / 2.0;
+ area += sqrt(halfPerimeter * (halfPerimeter - a) * (halfPerimeter - b) * (halfPerimeter - c));
+
+ Vector2 uv_p1 = uv[0];
+ Vector2 uv_p2 = uv[1];
+ Vector2 uv_p3 = uv[2];
+ double uv_a = uv_p1.distance_to(uv_p2);
+ double uv_b = uv_p2.distance_to(uv_p3);
+ double uv_c = uv_p3.distance_to(uv_p1);
+ double uv_halfPerimeter = (uv_a + uv_b + uv_c) / 2.0;
+ uv_area += sqrt(
+ uv_halfPerimeter * (uv_halfPerimeter - uv_a) * (uv_halfPerimeter - uv_b) * (uv_halfPerimeter - uv_c));
+ }
+ }
+
+ if (uv_area < 0.0001f) {
+ uv_area = 1.0;
+ }
+
+ int pixels = Math::round(ceil((1.0 / sqrt(uv_area)) * sqrt(area * default_texels_per_unit)));
+ int size = CLAMP(pixels, 2, 4096);
+ return Vector2i(size, size);
+}
+
+void BakedLightmap::_find_meshes_and_lights(Node *p_at_node, Vector &meshes, Vector &lights) {
+ AABB bounds = AABB(-extents, extents * 2.0);
+
+ MeshInstance *mi = Object::cast_to(p_at_node);
+ if (mi && mi->get_flag(GeometryInstance::FLAG_USE_BAKED_LIGHT) && mi->is_visible_in_tree()) {
+ Ref mesh = mi->get_mesh();
+ if (mesh.is_valid()) {
+ bool all_have_uv2_and_normal = true;
+ bool surfaces_found = false;
+ for (int i = 0; i < mesh->get_surface_count(); i++) {
+ if (mesh->surface_get_primitive_type(i) != Mesh::PRIMITIVE_TRIANGLES) {
+ continue;
+ }
+ if (!(mesh->surface_get_format(i) & Mesh::ARRAY_FORMAT_TEX_UV2)) {
+ all_have_uv2_and_normal = false;
+ break;
+ }
+ if (!(mesh->surface_get_format(i) & Mesh::ARRAY_FORMAT_NORMAL)) {
+ all_have_uv2_and_normal = false;
+ break;
+ }
+ surfaces_found = true;
+ }
+
+ if (surfaces_found && all_have_uv2_and_normal) {
+ Transform mesh_xform = get_global_transform().affine_inverse() * mi->get_global_transform();
+
+ AABB aabb = mesh_xform.xform(mesh->get_aabb());
+
+ if (bounds.intersects(aabb)) {
+ MeshesFound mf;
+ mf.cast_shadows = mi->get_cast_shadows_setting() != GeometryInstance::SHADOW_CASTING_SETTING_OFF;
+ mf.generate_lightmap = mi->get_generate_lightmap();
+ mf.xform = mesh_xform;
+ mf.node_path = get_path_to(mi);
+ mf.subindex = -1;
+ mf.mesh = mesh;
+
+ static const int lightmap_scale[4] = { 1, 2, 4, 8 }; //GeometryInstance3D::LIGHTMAP_SCALE_MAX = { 1, 2, 4, 8 };
+ mf.lightmap_scale = lightmap_scale[mi->get_lightmap_scale()];
+
+ Ref all_override = mi->get_material_override();
+ for (int i = 0; i < mesh->get_surface_count(); i++) {
+ if (all_override.is_valid()) {
+ mf.overrides.push_back(all_override);
+ } else {
+ mf.overrides.push_back(mi->get_surface_material(i));
+ }
+ }
+
+ meshes.push_back(mf);
+ }
+ }
+ }
+ }
+
+ Spatial *s = Object::cast_to(p_at_node);
+
+ if (!mi && s) {
+ Array bmeshes = p_at_node->call("get_bake_meshes");
+ if (bmeshes.size() && (bmeshes.size() & 1) == 0) {
+ Transform xf = get_global_transform().affine_inverse() * s->get_global_transform();
+ Ref all_override;
+
+ GeometryInstance *gi = Object::cast_to(p_at_node);
+ if (gi) {
+ all_override = gi->get_material_override();
+ }
+
+ for (int i = 0; i < bmeshes.size(); i += 2) {
+ Ref mesh = bmeshes[i];
+ if (!mesh.is_valid()) {
+ continue;
+ }
+
+ Transform bmtf = bmeshes[i + 1];
+ Transform mesh_xform = xf * bmtf;
+
+ AABB aabb = mesh_xform.xform(mesh->get_aabb());
+
+ if (!bounds.intersects(aabb)) {
+ continue;
+ }
+
+ MeshesFound mf;
+ mf.xform = mesh_xform;
+ mf.node_path = get_path_to(s);
+ mf.subindex = i / 2;
+ mf.lightmap_scale = 1;
+ mf.mesh = mesh;
+
+ if (gi) {
+ mf.cast_shadows = gi->get_cast_shadows_setting() != GeometryInstance::SHADOW_CASTING_SETTING_OFF;
+ mf.generate_lightmap = gi->get_generate_lightmap();
+ } else {
+ mf.cast_shadows = true;
+ mf.generate_lightmap = true;
+ }
+
+ for (int j = 0; j < mesh->get_surface_count(); j++) {
+ mf.overrides.push_back(all_override);
+ }
+
+ meshes.push_back(mf);
+ }
+ }
+ }
+
+ Light *light = Object::cast_to(p_at_node);
+
+ if (light && light->get_bake_mode() != Light::BAKE_DISABLED) {
+ LightsFound lf;
+ lf.xform = get_global_transform().affine_inverse() * light->get_global_transform();
+ lf.light = light;
+ lights.push_back(lf);
+ }
+
+ for (int i = 0; i < p_at_node->get_child_count(); i++) {
+ Node *child = p_at_node->get_child(i);
+ if (!child->get_owner()) {
+ continue; //maybe a helper
+ }
+
+ _find_meshes_and_lights(child, meshes, lights);
+ }
+}
+
+void BakedLightmap::_get_material_images(const MeshesFound &p_found_mesh, Lightmapper::MeshData &r_mesh_data, Vector[> &r_albedo_textures, Vector][> &r_emission_textures) {
+ for (int i = 0; i < p_found_mesh.mesh->get_surface_count(); ++i) {
+ Ref mat = p_found_mesh.overrides[i];
+
+ if (mat.is_null()) {
+ mat = p_found_mesh.mesh->surface_get_material(i);
+ }
+
+ Ref albedo_texture;
+ Color albedo_add = Color(1, 1, 1, 1);
+ Color albedo_mul = Color(1, 1, 1, 1);
+
+ Ref emission_texture;
+ Color emission_add = Color(0, 0, 0, 0);
+ Color emission_mul = Color(1, 1, 1, 1);
+
+ if (mat.is_valid()) {
+ albedo_texture = mat->get_texture(SpatialMaterial::TEXTURE_ALBEDO);
+
+ if (albedo_texture.is_valid()) {
+ albedo_mul = mat->get_albedo();
+ albedo_add = Color(0, 0, 0, 0);
+ } else {
+ albedo_add = mat->get_albedo();
+ }
+
+ emission_texture = mat->get_texture(SpatialMaterial::TEXTURE_EMISSION);
+ Color emission_color = mat->get_emission();
+ float emission_energy = mat->get_emission_energy();
+
+ if (mat->get_emission_operator() == SpatialMaterial::EMISSION_OP_ADD) {
+ emission_mul = Color(1, 1, 1) * emission_energy;
+ emission_add = emission_color * emission_energy;
+ } else {
+ emission_mul = emission_color * emission_energy;
+ emission_add = Color(0, 0, 0);
+ }
+ }
+
+ Lightmapper::MeshData::TextureDef albedo;
+ albedo.mul = albedo_mul;
+ albedo.add = albedo_add;
+
+ if (albedo_texture.is_valid()) {
+ albedo.tex_rid = albedo_texture->get_rid();
+ r_albedo_textures.push_back(albedo_texture);
+ }
+
+ r_mesh_data.albedo.push_back(albedo);
+
+ Lightmapper::MeshData::TextureDef emission;
+ emission.mul = emission_mul;
+ emission.add = emission_add;
+
+ if (emission_texture.is_valid()) {
+ emission.tex_rid = emission_texture->get_rid();
+ r_emission_textures.push_back(emission_texture);
+ }
+ r_mesh_data.emission.push_back(emission);
+ }
+}
+
+void BakedLightmap::_save_image(String &r_base_path, Ref r_img, bool p_use_srgb) {
+ if (use_hdr) {
+ r_base_path += ".exr";
+ } else {
+ r_base_path += ".png";
+ }
+
+ String relative_path = r_base_path;
+ if (relative_path.begins_with("res://")) {
+ relative_path = relative_path.substr(6, relative_path.length());
+ }
+
+ bool hdr_grayscale = use_hdr && !use_color;
+
+ r_img->lock();
+ for (int i = 0; i < r_img->get_height(); i++) {
+ for (int j = 0; j < r_img->get_width(); j++) {
+ Color c = r_img->get_pixel(j, i);
+
+ c.r = MAX(c.r, environment_min_light.r);
+ c.g = MAX(c.g, environment_min_light.g);
+ c.b = MAX(c.b, environment_min_light.b);
+
+ if (hdr_grayscale) {
+ c = Color(c.get_v(), 0.0f, 0.0f);
+ }
+
+ if (p_use_srgb) {
+ c = c.to_srgb();
+ }
+
+ r_img->set_pixel(j, i, c);
+ }
+ }
+ r_img->unlock();
+
+ if (!use_color) {
+ if (use_hdr) {
+ r_img->convert(Image::FORMAT_RH);
+ } else {
+ r_img->convert(Image::FORMAT_L8);
+ }
+ }
+
+ if (use_hdr) {
+ r_img->save_exr(relative_path, !use_color);
+ } else {
+ r_img->save_png(relative_path);
+ }
+}
+
+bool BakedLightmap::_lightmap_bake_step_function(float p_completion, const String &p_text, void *ud, bool p_refresh) {
+ BakeStepUD *bsud = (BakeStepUD *)ud;
+ bool ret = false;
+ if (bsud->func) {
+ ret = bsud->func(bsud->from_percent + p_completion * (bsud->to_percent - bsud->from_percent), p_text, bsud->ud, p_refresh);
+ }
+ return ret;
+}
+
+BakedLightmap::BakeError BakedLightmap::bake(Node *p_from_node, String p_data_save_path) {
+ if (!p_from_node && !get_parent()) {
+ return BAKE_ERROR_NO_ROOT;
+ }
+
+ bool no_save_path = false;
+ if (p_data_save_path == "" && (get_light_data().is_null() || !get_light_data()->get_path().is_resource_file())) {
+ no_save_path = true;
+ }
+
+ if (p_data_save_path == "") {
+ if (get_light_data().is_null()) {
+ no_save_path = true;
+ } else {
+ p_data_save_path = get_light_data()->get_path();
+ if (!p_data_save_path.is_resource_file()) {
+ no_save_path = true;
+ }
+ }
+ }
+
+ if (no_save_path) {
+ if (image_path == "") {
+ return BAKE_ERROR_NO_SAVE_PATH;
+ } else {
+ p_data_save_path = image_path;
+ }
+
+ WARN_PRINT("Using the deprecated property \"image_path\" as a save path, consider providing a better save path via the \"data_save_path\" parameter");
+ p_data_save_path = image_path.plus_file("BakedLightmap.lmbake");
+ }
+
+ {
+ //check for valid save path
+ DirAccessRef d = DirAccess::open(p_data_save_path.get_base_dir());
+ if (!d) {
+ ERR_FAIL_V_MSG(BAKE_ERROR_NO_SAVE_PATH, "Invalid save path '" + p_data_save_path + "'.");
+ }
+ }
+
+ uint32_t time_started = OS::get_singleton()->get_ticks_msec();
+
+ if (bake_step_function) {
+ bool cancelled = bake_step_function(0.0, TTR("Finding meshes and lights"), nullptr, true);
+ if (cancelled) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ return BAKE_ERROR_USER_ABORTED;
+ }
+ }
+
+ Ref lightmapper = Lightmapper::create();
+ if (lightmapper.is_null()) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ return BAKE_ERROR_NO_LIGHTMAPPER;
+ }
+
+ Vector lights_found;
+ Vector meshes_found;
+
+ _find_meshes_and_lights(p_from_node ? p_from_node : get_parent(), meshes_found, lights_found);
+
+ if (meshes_found.size() == 0) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ return BAKE_ERROR_NO_MESHES;
+ }
+
+ for (int m_i = 0; m_i < meshes_found.size(); m_i++) {
+ if (bake_step_function) {
+ float p = (float)(m_i) / meshes_found.size();
+ bool cancelled = bake_step_function(p * 0.05, vformat(TTR("Preparing geometry (%d/%d)"), m_i + 1, meshes_found.size()), nullptr, false);
+ if (cancelled) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ return BAKE_ERROR_USER_ABORTED;
+ }
+ }
+
+ MeshesFound &mf = meshes_found.write[m_i];
+
+ Size2i lightmap_size = mf.mesh->get_lightmap_size_hint();
+
+ if (lightmap_size == Vector2i(0, 0)) {
+ lightmap_size = _compute_lightmap_size(mf);
+ }
+ lightmap_size *= mf.lightmap_scale;
+
+ Lightmapper::MeshData md;
+
+ {
+ Dictionary d;
+ d["path"] = mf.node_path;
+ if (mf.subindex >= 0) {
+ d["subindex"] = mf.subindex;
+ }
+ d["cast_shadows"] = mf.cast_shadows;
+ d["generate_lightmap"] = mf.generate_lightmap;
+ d["node_name"] = mf.node_path.get_name(mf.node_path.get_name_count() - 1);
+ md.userdata = d;
+ }
+
+ Basis normal_xform = mf.xform.basis.inverse().transposed();
+
+ for (int i = 0; i < mf.mesh->get_surface_count(); i++) {
+ if (mf.mesh->surface_get_primitive_type(i) != Mesh::PRIMITIVE_TRIANGLES) {
+ continue;
+ }
+ Array a = mf.mesh->surface_get_arrays(i);
+
+ Vector vertices = a[Mesh::ARRAY_VERTEX];
+ const Vector3 *vr = vertices.ptr();
+ Vector uv2 = a[Mesh::ARRAY_TEX_UV2];
+ const Vector2 *uv2r = nullptr;
+ Vector uv = a[Mesh::ARRAY_TEX_UV];
+ const Vector2 *uvr = nullptr;
+ Vector normals = a[Mesh::ARRAY_NORMAL];
+ const Vector3 *nr = nullptr;
+ Vector index = a[Mesh::ARRAY_INDEX];
+
+ ERR_CONTINUE(uv2.size() == 0);
+ ERR_CONTINUE(normals.size() == 0);
+
+ if (!uv.empty()) {
+ uvr = uv.ptr();
+ }
+
+ uv2r = uv2.ptr();
+ nr = normals.ptr();
+
+ int facecount;
+ const int *ir = nullptr;
+
+ if (index.size()) {
+ facecount = index.size() / 3;
+ ir = index.ptr();
+ } else {
+ facecount = vertices.size() / 3;
+ }
+
+ md.surface_facecounts.push_back(facecount);
+
+ for (int j = 0; j < facecount; j++) {
+ uint32_t vidx[3];
+
+ if (ir) {
+ for (int k = 0; k < 3; k++) {
+ vidx[k] = ir[j * 3 + k];
+ }
+ } else {
+ for (int k = 0; k < 3; k++) {
+ vidx[k] = j * 3 + k;
+ }
+ }
+
+ for (int k = 0; k < 3; k++) {
+ Vector3 v = mf.xform.xform(vr[vidx[k]]);
+ md.points.push_back(v);
+
+ md.uv2.push_back(uv2r[vidx[k]]);
+ md.normal.push_back(normal_xform.xform(nr[vidx[k]]).normalized());
+
+ if (uvr != nullptr) {
+ md.uv.push_back(uvr[vidx[k]]);
+ }
+ }
+ }
+ }
+
+ Vector][> albedo_textures;
+ Vector][> emission_textures;
+
+ _get_material_images(mf, md, albedo_textures, emission_textures);
+
+ for (int j = 0; j < albedo_textures.size(); j++) {
+ lightmapper->add_albedo_texture(albedo_textures[j]);
+ }
+
+ for (int j = 0; j < emission_textures.size(); j++) {
+ lightmapper->add_emission_texture(emission_textures[j]);
+ }
+
+ lightmapper->add_mesh(md, lightmap_size);
+ }
+
+ for (int i = 0; i < lights_found.size(); i++) {
+ Light *light = lights_found[i].light;
+ Transform xf = lights_found[i].xform;
+
+ if (Object::cast_to(light)) {
+ DirectionalLight *l = Object::cast_to(light);
+ lightmapper->add_directional_light(light->get_bake_mode() == Light::BAKE_ALL, -xf.basis.get_axis(Vector3::AXIS_Z).normalized(), l->get_color(), l->get_param(Light::PARAM_ENERGY), l->get_param(Light::PARAM_INDIRECT_ENERGY), l->get_param(Light::PARAM_SIZE));
+ } else if (Object::cast_to(light)) {
+ OmniLight *l = Object::cast_to(light);
+ lightmapper->add_omni_light(light->get_bake_mode() == Light::BAKE_ALL, xf.origin, l->get_color(), l->get_param(Light::PARAM_ENERGY), l->get_param(Light::PARAM_INDIRECT_ENERGY), l->get_param(Light::PARAM_RANGE), l->get_param(Light::PARAM_ATTENUATION), l->get_param(Light::PARAM_SIZE));
+ } else if (Object::cast_to(light)) {
+ SpotLight *l = Object::cast_to(light);
+ lightmapper->add_spot_light(light->get_bake_mode() == Light::BAKE_ALL, xf.origin, -xf.basis.get_axis(Vector3::AXIS_Z).normalized(), l->get_color(), l->get_param(Light::PARAM_ENERGY), l->get_param(Light::PARAM_INDIRECT_ENERGY), l->get_param(Light::PARAM_RANGE), l->get_param(Light::PARAM_ATTENUATION), l->get_param(Light::PARAM_SPOT_ANGLE), l->get_param(Light::PARAM_SPOT_ATTENUATION), l->get_param(Light::PARAM_SIZE));
+ }
+ }
+
+ Ref environment_image;
+ Basis environment_xform;
+
+ if (environment_mode != ENVIRONMENT_MODE_DISABLED) {
+ if (bake_step_function) {
+ bake_step_function(0.1, TTR("Preparing environment"), nullptr, true);
+ }
+
+ switch (environment_mode) {
+ case ENVIRONMENT_MODE_DISABLED: {
+ //nothing
+ } break;
+ case ENVIRONMENT_MODE_SCENE: {
+ Ref world = get_world_3d();
+ if (world.is_valid()) {
+ Ref env = world->get_environment();
+ if (env.is_null()) {
+ env = world->get_fallback_environment();
+ }
+
+ if (env.is_valid()) {
+ environment_image = _get_irradiance_map(env, Vector2i(128, 64));
+ environment_xform = get_global_transform().affine_inverse().basis * env->get_sky_orientation();
+
+ float ambient_sky = env->get_ambient_light_sky_contribution();
+ float energy = env->get_ambient_light_energy();
+ if (ambient_sky != 1.0 || energy != 1.0) {
+ Color ambient_light = env->get_ambient_light_color().to_linear() * (1.0 - ambient_sky) * energy;
+ environment_image->lock();
+ for (int i = 0; i < 128; i++) {
+ for (int j = 0; j < 64; j++) {
+ Color c = ambient_light + environment_image->get_pixel(i, j) * ambient_sky * energy;
+ environment_image->set_pixel(i, j, c);
+ }
+ }
+ environment_image->unlock();
+ }
+ }
+ }
+ } break;
+ case ENVIRONMENT_MODE_CUSTOM_SKY: {
+ if (environment_custom_sky.is_valid()) {
+ environment_image = _get_irradiance_from_sky(environment_custom_sky, environment_custom_energy, Vector2i(128, 64));
+ environment_xform.set_euler(environment_custom_sky_rotation_degrees * Math_PI / 180.0);
+ }
+
+ } break;
+ case ENVIRONMENT_MODE_CUSTOM_COLOR: {
+ environment_image.instance();
+ environment_image->create(128, 64, false, Image::FORMAT_RGBF);
+ Color c = environment_custom_color;
+ c.r *= environment_custom_energy;
+ c.g *= environment_custom_energy;
+ c.b *= environment_custom_energy;
+ environment_image->lock();
+ for (int i = 0; i < 128; i++) {
+ for (int j = 0; j < 64; j++) {
+ environment_image->set_pixel(i, j, c);
+ }
+ }
+ environment_image->unlock();
+ } break;
+ }
+ }
+
+ BakeStepUD bsud;
+ bsud.func = bake_step_function;
+ bsud.ud = nullptr;
+ bsud.from_percent = 0.1;
+ bsud.to_percent = 0.9;
+
+ bool gen_atlas = OS::get_singleton()->get_current_video_driver() == OS::VIDEO_DRIVER_GLES2 ? false : generate_atlas;
+
+ Lightmapper::BakeError bake_err = lightmapper->bake(Lightmapper::BakeQuality(bake_quality), use_denoiser, bounces, bounce_indirect_energy, bias, gen_atlas, max_atlas_size, environment_image, environment_xform, _lightmap_bake_step_function, &bsud, bake_substep_function);
+
+ if (bake_err != Lightmapper::BAKE_OK) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ switch (bake_err) {
+ case Lightmapper::BAKE_ERROR_USER_ABORTED: {
+ return BAKE_ERROR_USER_ABORTED;
+ }
+ case Lightmapper::BAKE_ERROR_LIGHTMAP_TOO_SMALL: {
+ return BAKE_ERROR_LIGHTMAP_SIZE;
+ }
+ case Lightmapper::BAKE_ERROR_NO_MESHES: {
+ return BAKE_ERROR_NO_MESHES;
+ }
+ default: {
+ }
+ }
+ return BAKE_ERROR_NO_LIGHTMAPPER;
+ }
+
+ Ref data;
+ if (get_light_data().is_valid()) {
+ data = get_light_data();
+ set_light_data(Ref()); //clear
+ data->clear_data();
+ } else {
+ data.instance();
+ }
+
+ if (capture_enabled) {
+ if (bake_step_function) {
+ bool cancelled = bake_step_function(0.85, TTR("Generating capture"), nullptr, true);
+ if (cancelled) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ return BAKE_ERROR_USER_ABORTED;
+ }
+ }
+
+ VoxelLightBaker voxel_baker;
+
+ int bake_subdiv;
+ int capture_subdiv;
+ AABB bake_bounds;
+ {
+ bake_bounds = AABB(-extents, extents * 2.0);
+ int subdiv = nearest_power_of_2_templated(int(bake_bounds.get_longest_axis_size() / capture_cell_size));
+ bake_bounds.size[bake_bounds.get_longest_axis_index()] = subdiv * capture_cell_size;
+ bake_subdiv = nearest_shift(subdiv) + 1;
+
+ capture_subdiv = bake_subdiv;
+ float css = capture_cell_size;
+ while (css < capture_cell_size && capture_subdiv > 2) {
+ capture_subdiv--;
+ css *= 2.0;
+ }
+ }
+
+ voxel_baker.begin_bake(capture_subdiv + 1, bake_bounds);
+
+ for (int mesh_id = 0; mesh_id < meshes_found.size(); mesh_id++) {
+ MeshesFound &mf = meshes_found.write[mesh_id];
+ voxel_baker.plot_mesh(mf.xform, mf.mesh, mf.overrides, Ref());
+ }
+
+ VoxelLightBaker::BakeQuality capt_quality = VoxelLightBaker::BakeQuality::BAKE_QUALITY_HIGH;
+ if (capture_quality == BakedLightmap::BakeQuality::BAKE_QUALITY_LOW) {
+ capt_quality = VoxelLightBaker::BakeQuality::BAKE_QUALITY_LOW;
+ } else if (capture_quality == BakedLightmap::BakeQuality::BAKE_QUALITY_MEDIUM) {
+ capt_quality = VoxelLightBaker::BakeQuality::BAKE_QUALITY_MEDIUM;
+ }
+
+ voxel_baker.begin_bake_light(capt_quality, capture_propagation);
+
+ for (int i = 0; i < lights_found.size(); i++) {
+ LightsFound &lf = lights_found.write[i];
+ switch (lf.light->get_light_type()) {
+ case RS::LIGHT_DIRECTIONAL: {
+ voxel_baker.plot_light_directional(-lf.xform.basis.get_axis(2), lf.light->get_color(), lf.light->get_param(Light::PARAM_ENERGY), lf.light->get_param(Light::PARAM_INDIRECT_ENERGY), lf.light->get_bake_mode() == Light::BAKE_ALL);
+ } break;
+ case RS::LIGHT_OMNI: {
+ voxel_baker.plot_light_omni(lf.xform.origin, lf.light->get_color(), lf.light->get_param(Light::PARAM_ENERGY), lf.light->get_param(Light::PARAM_INDIRECT_ENERGY), lf.light->get_param(Light::PARAM_RANGE), lf.light->get_param(Light::PARAM_ATTENUATION), lf.light->get_bake_mode() == Light::BAKE_ALL);
+ } break;
+ case RS::LIGHT_SPOT: {
+ voxel_baker.plot_light_spot(lf.xform.origin, lf.xform.basis.get_axis(2), lf.light->get_color(), lf.light->get_param(Light::PARAM_ENERGY), lf.light->get_param(Light::PARAM_INDIRECT_ENERGY), lf.light->get_param(Light::PARAM_RANGE), lf.light->get_param(Light::PARAM_ATTENUATION), lf.light->get_param(Light::PARAM_SPOT_ANGLE), lf.light->get_param(Light::PARAM_SPOT_ATTENUATION), lf.light->get_bake_mode() == Light::BAKE_ALL);
+
+ } break;
+ }
+ }
+
+ voxel_baker.end_bake();
+
+ AABB bounds = AABB(-extents, extents * 2);
+ data->set_cell_subdiv(capture_subdiv);
+ data->set_bounds(bounds);
+ data->set_octree(voxel_baker.create_capture_octree(capture_subdiv));
+
+ {
+ float bake_bound_size = bake_bounds.get_longest_axis_size();
+ Transform to_bounds;
+ to_bounds.basis.scale(Vector3(bake_bound_size, bake_bound_size, bake_bound_size));
+ to_bounds.origin = bounds.position;
+
+ Transform to_grid;
+ to_grid.basis.scale(Vector3(1 << (capture_subdiv - 1), 1 << (capture_subdiv - 1), 1 << (capture_subdiv - 1)));
+
+ Transform to_cell_space = to_grid * to_bounds.affine_inverse();
+ data->set_cell_space_transform(to_cell_space);
+ }
+ }
+
+ if (bake_step_function) {
+ bool cancelled = bake_step_function(0.9, TTR("Saving lightmaps"), nullptr, true);
+ if (cancelled) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ return BAKE_ERROR_USER_ABORTED;
+ }
+ }
+
+ Vector][> images;
+ for (int i = 0; i < lightmapper->get_bake_texture_count(); i++) {
+ images.push_back(lightmapper->get_bake_texture(i));
+ }
+
+ bool use_srgb = use_color && !use_hdr;
+
+ if (gen_atlas) {
+ int slice_count = images.size();
+ int slice_width = images[0]->get_width();
+ int slice_height = images[0]->get_height();
+
+ int slices_per_texture = Image::MAX_HEIGHT / slice_height;
+ int texture_count = Math::ceil(slice_count / (float)slices_per_texture);
+
+ Vector][> textures;
+ textures.resize(texture_count);
+ String base_path = p_data_save_path.get_basename();
+
+ int last_count = slice_count % slices_per_texture;
+ for (int i = 0; i < texture_count; i++) {
+ String texture_path = texture_count > 1 ? base_path + "_" + itos(i) : base_path;
+ int texture_slice_count = (i == texture_count - 1 && last_count != 0) ? last_count : slices_per_texture;
+
+ Ref large_image;
+ large_image.instance();
+ large_image->create(slice_width, slice_height * texture_slice_count, false, images[0]->get_format());
+
+ for (int j = 0; j < texture_slice_count; j++) {
+ large_image->blit_rect(images[i * slices_per_texture + j], Rect2(0, 0, slice_width, slice_height), Point2(0, slice_height * j));
+ }
+ _save_image(texture_path, large_image, use_srgb);
+
+ Ref config;
+ config.instance();
+ if (FileAccess::exists(texture_path + ".import")) {
+ config->load(texture_path + ".import");
+ } else {
+ // Set only if settings don't exist, to keep user choice
+ config->set_value("params", "compress/mode", 0);
+ }
+ config->set_value("remap", "importer", "texture_array");
+ config->set_value("remap", "type", "TextureArray");
+ config->set_value("params", "detect_3d", false);
+ config->set_value("params", "flags/repeat", false);
+ config->set_value("params", "flags/filter", true);
+ config->set_value("params", "flags/mipmaps", false);
+ config->set_value("params", "flags/srgb", use_srgb);
+ config->set_value("params", "slices/horizontal", 1);
+ config->set_value("params", "slices/vertical", texture_slice_count);
+
+ config->save(texture_path + ".import");
+
+ ResourceLoader::import(texture_path);
+ textures.write[i] = ResourceLoader::load(texture_path); //if already loaded, it will be updated on refocus?
+ }
+
+ for (int i = 0; i < lightmapper->get_bake_mesh_count(); i++) {
+ if (!meshes_found[i].generate_lightmap) {
+ continue;
+ }
+ Dictionary d = lightmapper->get_bake_mesh_userdata(i);
+ NodePath np = d["path"];
+ int32_t subindex = -1;
+ if (d.has("subindex")) {
+ subindex = d["subindex"];
+ }
+
+ Rect2 uv_rect = lightmapper->get_bake_mesh_uv_scale(i);
+ int slice_index = lightmapper->get_bake_mesh_texture_slice(i);
+ data->add_user(np, textures[slice_index / slices_per_texture], slice_index % slices_per_texture, uv_rect, subindex);
+ }
+ } else {
+ for (int i = 0; i < lightmapper->get_bake_mesh_count(); i++) {
+ if (!meshes_found[i].generate_lightmap) {
+ continue;
+ }
+
+ Ref texture;
+ String base_path = p_data_save_path.get_base_dir().plus_file(images[i]->get_name());
+
+ if (ResourceLoader::import) {
+ _save_image(base_path, images[i], use_srgb);
+
+ Ref config;
+ config.instance();
+ if (FileAccess::exists(base_path + ".import")) {
+ config->load(base_path + ".import");
+ } else {
+ // Set only if settings don't exist, to keep user choice
+ config->set_value("params", "compress/mode", 0);
+ }
+ config->set_value("remap", "importer", "texture");
+ config->set_value("remap", "type", "StreamTexture");
+ config->set_value("params", "detect_3d", false);
+ config->set_value("params", "flags/repeat", false);
+ config->set_value("params", "flags/filter", true);
+ config->set_value("params", "flags/mipmaps", false);
+ config->set_value("params", "flags/srgb", use_srgb);
+
+ config->save(base_path + ".import");
+
+ ResourceLoader::import(base_path);
+ texture = ResourceLoader::load(base_path); //if already loaded, it will be updated on refocus?
+ } else {
+ base_path += ".tex";
+ Ref tex;
+ bool set_path = true;
+ if (ResourceCache::has(base_path)) {
+ tex = Ref((Resource *)ResourceCache::get(base_path));
+ set_path = false;
+ }
+
+ if (!tex.is_valid()) {
+ tex.instance();
+ }
+
+ tex->create_from_image(images[i], Texture::FLAGS_DEFAULT);
+
+ ResourceSaver::save(base_path, tex, ResourceSaver::FLAG_CHANGE_PATH);
+ if (set_path) {
+ tex->set_path(base_path);
+ }
+ texture = tex;
+ }
+
+ Dictionary d = lightmapper->get_bake_mesh_userdata(i);
+ NodePath np = d["path"];
+ int32_t subindex = -1;
+ if (d.has("subindex")) {
+ subindex = d["subindex"];
+ }
+
+ Rect2 uv_rect = Rect2(0, 0, 1, 1);
+ int slice_index = -1;
+ data->add_user(np, texture, slice_index, uv_rect, subindex);
+ }
+ }
+
+ if (bake_step_function) {
+ bool cancelled = bake_step_function(1.0, TTR("Done"), nullptr, true);
+ if (cancelled) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ return BAKE_ERROR_USER_ABORTED;
+ }
+ }
+
+ Error err = ResourceSaver::save(p_data_save_path, data);
+ data->set_path(p_data_save_path);
+
+ if (err != OK) {
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+ return BAKE_ERROR_CANT_CREATE_IMAGE;
+ }
+
+ set_light_data(data);
+ if (bake_end_function) {
+ bake_end_function(time_started);
+ }
+
+ return BAKE_ERROR_OK;
+}
+
+void BakedLightmap::set_capture_cell_size(float p_cell_size) {
+ capture_cell_size = MAX(0.1, p_cell_size);
+}
+
+float BakedLightmap::get_capture_cell_size() const {
+ return capture_cell_size;
+}
+
+void BakedLightmap::set_extents(const Vector3 &p_extents) {
+ extents = p_extents;
+ update_gizmos();
+ _change_notify("extents");
+}
+
+Vector3 BakedLightmap::get_extents() const {
+ return extents;
+}
+
+void BakedLightmap::set_default_texels_per_unit(const float &p_bake_texels_per_unit) {
+ default_texels_per_unit = MAX(0.0, p_bake_texels_per_unit);
+}
+
+float BakedLightmap::get_default_texels_per_unit() const {
+ return default_texels_per_unit;
+}
+
+void BakedLightmap::set_capture_enabled(bool p_enable) {
+ capture_enabled = p_enable;
+ _change_notify();
+}
+
+bool BakedLightmap::get_capture_enabled() const {
+ return capture_enabled;
+}
+
+void BakedLightmap::_notification(int p_what) {
+ if (p_what == NOTIFICATION_READY) {
+ if (light_data.is_valid()) {
+ _assign_lightmaps();
+ }
+ request_ready(); //will need ready again if re-enters tree
+ }
+
+ if (p_what == NOTIFICATION_EXIT_TREE) {
+ if (light_data.is_valid()) {
+ _clear_lightmaps();
+ }
+ }
+}
+
+void BakedLightmap::_assign_lightmaps() {
+ ERR_FAIL_COND(!light_data.is_valid());
+
+ bool atlassed_on_gles2 = false;
+
+ for (int i = 0; i < light_data->get_user_count(); i++) {
+ Ref lightmap = light_data->get_user_lightmap(i);
+ ERR_CONTINUE(!lightmap.is_valid());
+ ERR_CONTINUE(!Object::cast_to(lightmap.ptr()) && !Object::cast_to(lightmap.ptr()));
+
+ Node *node = get_node(light_data->get_user_path(i));
+ int instance_idx = light_data->get_user_instance(i);
+ if (instance_idx >= 0) {
+ RID instance = node->call("get_bake_mesh_instance", instance_idx);
+ if (instance.is_valid()) {
+ int slice = light_data->get_user_lightmap_slice(i);
+ atlassed_on_gles2 = atlassed_on_gles2 || (slice != -1 && OS::get_singleton()->get_current_video_driver() == OS::VIDEO_DRIVER_GLES2);
+ RS::get_singleton()->instance_set_use_lightmap(instance, get_instance(), lightmap->get_rid(), slice, light_data->get_user_lightmap_uv_rect(i));
+ }
+ } else {
+ VisualInstance *vi = Object::cast_to(node);
+ ERR_CONTINUE(!vi);
+ int slice = light_data->get_user_lightmap_slice(i);
+ atlassed_on_gles2 = atlassed_on_gles2 || (slice != -1 && OS::get_singleton()->get_current_video_driver() == OS::VIDEO_DRIVER_GLES2);
+ RS::get_singleton()->instance_set_use_lightmap(vi->get_instance(), get_instance(), lightmap->get_rid(), slice, light_data->get_user_lightmap_uv_rect(i));
+ }
+ }
+
+ if (atlassed_on_gles2) {
+ ERR_PRINT("GLES2 doesn't support layered textures, so lightmap atlassing is not supported. Please re-bake the lightmap or switch to GLES3.");
+ }
+}
+
+void BakedLightmap::_clear_lightmaps() {
+ ERR_FAIL_COND(!light_data.is_valid());
+ for (int i = 0; i < light_data->get_user_count(); i++) {
+ Node *node = get_node(light_data->get_user_path(i));
+ int instance_idx = light_data->get_user_instance(i);
+ if (instance_idx >= 0) {
+ RID instance = node->call("get_bake_mesh_instance", instance_idx);
+ if (instance.is_valid()) {
+ RS::get_singleton()->instance_set_use_lightmap(instance, get_instance(), RID(), -1, Rect2(0, 0, 1, 1));
+ }
+ } else {
+ VisualInstance *vi = Object::cast_to(node);
+ ERR_CONTINUE(!vi);
+ RS::get_singleton()->instance_set_use_lightmap(vi->get_instance(), get_instance(), RID(), -1, Rect2(0, 0, 1, 1));
+ }
+ }
+}
+
+Ref BakedLightmap::_get_irradiance_from_sky(Ref p_sky, float p_energy, Vector2i p_size) {
+ if (p_sky.is_null()) {
+ return Ref();
+ }
+
+ Ref sky_image;
+ Ref panorama = p_sky;
+ if (panorama.is_valid()) {
+ sky_image = panorama->get_panorama()->get_data();
+ }
+ Ref procedural = p_sky;
+ if (procedural.is_valid()) {
+ sky_image = procedural->get_data();
+ }
+
+ if (sky_image.is_null()) {
+ return Ref();
+ }
+
+ sky_image->convert(Image::FORMAT_RGBF);
+ sky_image->resize(p_size.x, p_size.y, Image::INTERPOLATE_CUBIC);
+
+ if (p_energy != 1.0) {
+ sky_image->lock();
+ for (int i = 0; i < p_size.y; i++) {
+ for (int j = 0; j < p_size.x; j++) {
+ sky_image->set_pixel(j, i, sky_image->get_pixel(j, i) * p_energy);
+ }
+ }
+ sky_image->unlock();
+ }
+
+ return sky_image;
+}
+
+Ref BakedLightmap::_get_irradiance_map(Ref p_env, Vector2i p_size) {
+ Environment3D::BGMode bg_mode = p_env->get_background();
+ switch (bg_mode) {
+ case Environment3D::BG_SKY: {
+ return _get_irradiance_from_sky(p_env->get_sky(), p_env->get_bg_energy(), Vector2i(128, 64));
+ }
+ case Environment3D::BG_CLEAR_COLOR:
+ case Environment3D::BG_COLOR: {
+ Color c = bg_mode == Environment3D::BG_CLEAR_COLOR ? Color(GLOBAL_GET("rendering/environment/default_clear_color")) : p_env->get_bg_color();
+ c.r *= p_env->get_bg_energy();
+ c.g *= p_env->get_bg_energy();
+ c.b *= p_env->get_bg_energy();
+
+ Ref ret;
+ ret.instance();
+ ret->create(p_size.x, p_size.y, false, Image::FORMAT_RGBF);
+ ret->fill(c);
+ return ret;
+ }
+ default: {
+ }
+ }
+ return Ref();
+}
+
+void BakedLightmap::set_light_data(const Ref &p_data) {
+ if (light_data.is_valid()) {
+ if (is_inside_tree()) {
+ _clear_lightmaps();
+ }
+ set_base(RID());
+ }
+ light_data = p_data;
+ _change_notify();
+
+ if (light_data.is_valid()) {
+ set_base(light_data->get_rid());
+ if (is_inside_tree()) {
+ _assign_lightmaps();
+ }
+ }
+}
+
+Ref BakedLightmap::get_light_data() const {
+ return light_data;
+}
+
+void BakedLightmap::set_capture_propagation(float p_propagation) {
+ capture_propagation = p_propagation;
+}
+
+float BakedLightmap::get_capture_propagation() const {
+ return capture_propagation;
+}
+
+void BakedLightmap::set_capture_quality(BakeQuality p_quality) {
+ capture_quality = p_quality;
+}
+
+BakedLightmap::BakeQuality BakedLightmap::get_capture_quality() const {
+ return capture_quality;
+}
+
+void BakedLightmap::set_generate_atlas(bool p_enabled) {
+ generate_atlas = p_enabled;
+}
+
+bool BakedLightmap::is_generate_atlas_enabled() const {
+ return generate_atlas;
+}
+
+void BakedLightmap::set_max_atlas_size(int p_size) {
+ ERR_FAIL_COND(p_size < 2048);
+ max_atlas_size = p_size;
+}
+
+int BakedLightmap::get_max_atlas_size() const {
+ return max_atlas_size;
+}
+
+void BakedLightmap::set_bake_quality(BakeQuality p_quality) {
+ bake_quality = p_quality;
+ _change_notify();
+}
+
+BakedLightmap::BakeQuality BakedLightmap::get_bake_quality() const {
+ return bake_quality;
+}
+
+#ifndef DISABLE_DEPRECATED
+void BakedLightmap::set_image_path(const String &p_path) {
+ image_path = p_path;
+}
+
+String BakedLightmap::get_image_path() const {
+ return image_path;
+}
+#endif
+
+void BakedLightmap::set_use_denoiser(bool p_enable) {
+ use_denoiser = p_enable;
+}
+
+bool BakedLightmap::is_using_denoiser() const {
+ return use_denoiser;
+}
+
+void BakedLightmap::set_use_hdr(bool p_enable) {
+ use_hdr = p_enable;
+}
+
+bool BakedLightmap::is_using_hdr() const {
+ return use_hdr;
+}
+
+void BakedLightmap::set_use_color(bool p_enable) {
+ use_color = p_enable;
+}
+
+bool BakedLightmap::is_using_color() const {
+ return use_color;
+}
+
+void BakedLightmap::set_environment_mode(EnvironmentMode p_mode) {
+ environment_mode = p_mode;
+ _change_notify();
+}
+
+BakedLightmap::EnvironmentMode BakedLightmap::get_environment_mode() const {
+ return environment_mode;
+}
+
+void BakedLightmap::set_environment_custom_sky(const Ref &p_sky) {
+ environment_custom_sky = p_sky;
+}
+
+Ref BakedLightmap::get_environment_custom_sky() const {
+ return environment_custom_sky;
+}
+
+void BakedLightmap::set_environment_custom_sky_rotation_degrees(const Vector3 &p_rotation) {
+ environment_custom_sky_rotation_degrees = p_rotation;
+}
+
+Vector3 BakedLightmap::get_environment_custom_sky_rotation_degrees() const {
+ return environment_custom_sky_rotation_degrees;
+}
+
+void BakedLightmap::set_environment_custom_color(const Color &p_color) {
+ environment_custom_color = p_color;
+}
+Color BakedLightmap::get_environment_custom_color() const {
+ return environment_custom_color;
+}
+
+void BakedLightmap::set_environment_custom_energy(float p_energy) {
+ environment_custom_energy = p_energy;
+}
+float BakedLightmap::get_environment_custom_energy() const {
+ return environment_custom_energy;
+}
+
+void BakedLightmap::set_environment_min_light(Color p_min_light) {
+ environment_min_light = p_min_light;
+}
+
+Color BakedLightmap::get_environment_min_light() const {
+ return environment_min_light;
+}
+
+void BakedLightmap::set_bounces(int p_bounces) {
+ ERR_FAIL_COND(p_bounces < 0 || p_bounces > 16);
+ bounces = p_bounces;
+}
+
+int BakedLightmap::get_bounces() const {
+ return bounces;
+}
+
+void BakedLightmap::set_bounce_indirect_energy(float p_indirect_energy) {
+ ERR_FAIL_COND(p_indirect_energy < 0.0);
+ bounce_indirect_energy = p_indirect_energy;
+}
+
+float BakedLightmap::get_bounce_indirect_energy() const {
+ return bounce_indirect_energy;
+}
+
+void BakedLightmap::set_bias(float p_bias) {
+ ERR_FAIL_COND(p_bias < 0.00001f);
+ bias = p_bias;
+}
+
+float BakedLightmap::get_bias() const {
+ return bias;
+}
+
+AABB BakedLightmap::get_aabb() const {
+ return AABB(-extents, extents * 2);
+}
+PoolVector BakedLightmap::get_faces(uint32_t p_usage_flags) const {
+ return PoolVector();
+}
+
+void BakedLightmap::_validate_property(PropertyInfo &property) const {
+ if (property.name.begins_with("environment_custom_sky") && environment_mode != ENVIRONMENT_MODE_CUSTOM_SKY) {
+ property.usage = 0;
+ }
+
+ if (property.name == "environment_custom_color" && environment_mode != ENVIRONMENT_MODE_CUSTOM_COLOR) {
+ property.usage = 0;
+ }
+
+ if (property.name == "environment_custom_energy" && environment_mode != ENVIRONMENT_MODE_CUSTOM_COLOR && environment_mode != ENVIRONMENT_MODE_CUSTOM_SKY) {
+ property.usage = 0;
+ }
+
+ if (property.name.begins_with("atlas") && OS::get_singleton()->get_current_video_driver() == OS::VIDEO_DRIVER_GLES2) {
+ property.usage = PROPERTY_USAGE_NOEDITOR;
+ }
+
+ if (property.name.begins_with("capture") && property.name != "capture_enabled" && !capture_enabled) {
+ property.usage = 0;
+ }
+}
+
+void BakedLightmap::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_light_data", "data"), &BakedLightmap::set_light_data);
+ ClassDB::bind_method(D_METHOD("get_light_data"), &BakedLightmap::get_light_data);
+
+ ClassDB::bind_method(D_METHOD("set_bake_quality", "quality"), &BakedLightmap::set_bake_quality);
+ ClassDB::bind_method(D_METHOD("get_bake_quality"), &BakedLightmap::get_bake_quality);
+
+ ClassDB::bind_method(D_METHOD("set_bounces", "bounces"), &BakedLightmap::set_bounces);
+ ClassDB::bind_method(D_METHOD("get_bounces"), &BakedLightmap::get_bounces);
+
+ ClassDB::bind_method(D_METHOD("set_bounce_indirect_energy", "bounce_indirect_energy"), &BakedLightmap::set_bounce_indirect_energy);
+ ClassDB::bind_method(D_METHOD("get_bounce_indirect_energy"), &BakedLightmap::get_bounce_indirect_energy);
+
+ ClassDB::bind_method(D_METHOD("set_bias", "bias"), &BakedLightmap::set_bias);
+ ClassDB::bind_method(D_METHOD("get_bias"), &BakedLightmap::get_bias);
+
+ ClassDB::bind_method(D_METHOD("set_environment_mode", "mode"), &BakedLightmap::set_environment_mode);
+ ClassDB::bind_method(D_METHOD("get_environment_mode"), &BakedLightmap::get_environment_mode);
+
+ ClassDB::bind_method(D_METHOD("set_environment_custom_sky", "sky"), &BakedLightmap::set_environment_custom_sky);
+ ClassDB::bind_method(D_METHOD("get_environment_custom_sky"), &BakedLightmap::get_environment_custom_sky);
+
+ ClassDB::bind_method(D_METHOD("set_environment_custom_sky_rotation_degrees", "rotation"), &BakedLightmap::set_environment_custom_sky_rotation_degrees);
+ ClassDB::bind_method(D_METHOD("get_environment_custom_sky_rotation_degrees"), &BakedLightmap::get_environment_custom_sky_rotation_degrees);
+
+ ClassDB::bind_method(D_METHOD("set_environment_custom_color", "color"), &BakedLightmap::set_environment_custom_color);
+ ClassDB::bind_method(D_METHOD("get_environment_custom_color"), &BakedLightmap::get_environment_custom_color);
+
+ ClassDB::bind_method(D_METHOD("set_environment_custom_energy", "energy"), &BakedLightmap::set_environment_custom_energy);
+ ClassDB::bind_method(D_METHOD("get_environment_custom_energy"), &BakedLightmap::get_environment_custom_energy);
+
+ ClassDB::bind_method(D_METHOD("set_environment_min_light", "min_light"), &BakedLightmap::set_environment_min_light);
+ ClassDB::bind_method(D_METHOD("get_environment_min_light"), &BakedLightmap::get_environment_min_light);
+
+ ClassDB::bind_method(D_METHOD("set_use_denoiser", "use_denoiser"), &BakedLightmap::set_use_denoiser);
+ ClassDB::bind_method(D_METHOD("is_using_denoiser"), &BakedLightmap::is_using_denoiser);
+
+ ClassDB::bind_method(D_METHOD("set_use_hdr", "use_denoiser"), &BakedLightmap::set_use_hdr);
+ ClassDB::bind_method(D_METHOD("is_using_hdr"), &BakedLightmap::is_using_hdr);
+
+ ClassDB::bind_method(D_METHOD("set_use_color", "use_denoiser"), &BakedLightmap::set_use_color);
+ ClassDB::bind_method(D_METHOD("is_using_color"), &BakedLightmap::is_using_color);
+
+ ClassDB::bind_method(D_METHOD("set_generate_atlas", "enabled"), &BakedLightmap::set_generate_atlas);
+ ClassDB::bind_method(D_METHOD("is_generate_atlas_enabled"), &BakedLightmap::is_generate_atlas_enabled);
+
+ ClassDB::bind_method(D_METHOD("set_max_atlas_size", "max_atlas_size"), &BakedLightmap::set_max_atlas_size);
+ ClassDB::bind_method(D_METHOD("get_max_atlas_size"), &BakedLightmap::get_max_atlas_size);
+
+ ClassDB::bind_method(D_METHOD("set_capture_quality", "capture_quality"), &BakedLightmap::set_capture_quality);
+ ClassDB::bind_method(D_METHOD("get_capture_quality"), &BakedLightmap::get_capture_quality);
+
+ ClassDB::bind_method(D_METHOD("set_extents", "extents"), &BakedLightmap::set_extents);
+ ClassDB::bind_method(D_METHOD("get_extents"), &BakedLightmap::get_extents);
+
+ ClassDB::bind_method(D_METHOD("set_default_texels_per_unit", "texels"), &BakedLightmap::set_default_texels_per_unit);
+ ClassDB::bind_method(D_METHOD("get_default_texels_per_unit"), &BakedLightmap::get_default_texels_per_unit);
+
+ ClassDB::bind_method(D_METHOD("set_capture_propagation", "propagation"), &BakedLightmap::set_capture_propagation);
+ ClassDB::bind_method(D_METHOD("get_capture_propagation"), &BakedLightmap::get_capture_propagation);
+
+ ClassDB::bind_method(D_METHOD("set_capture_enabled", "enabled"), &BakedLightmap::set_capture_enabled);
+ ClassDB::bind_method(D_METHOD("get_capture_enabled"), &BakedLightmap::get_capture_enabled);
+
+ ClassDB::bind_method(D_METHOD("set_capture_cell_size", "capture_cell_size"), &BakedLightmap::set_capture_cell_size);
+ ClassDB::bind_method(D_METHOD("get_capture_cell_size"), &BakedLightmap::get_capture_cell_size);
+#ifndef DISABLE_DEPRECATED
+ ClassDB::bind_method(D_METHOD("set_image_path", "image_path"), &BakedLightmap::set_image_path);
+ ClassDB::bind_method(D_METHOD("get_image_path"), &BakedLightmap::get_image_path);
+#endif
+ ClassDB::bind_method(D_METHOD("bake", "from_node", "data_save_path"), &BakedLightmap::bake, DEFVAL(Variant()), DEFVAL(""));
+
+ ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "extents"), "set_extents", "get_extents");
+
+ ADD_GROUP("Tweaks", "");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "quality", PROPERTY_HINT_ENUM, "Low,Medium,High,Ultra"), "set_bake_quality", "get_bake_quality");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "bounces", PROPERTY_HINT_RANGE, "0,16,1"), "set_bounces", "get_bounces");
+ ADD_PROPERTY(PropertyInfo(Variant::REAL, "bounce_indirect_energy", PROPERTY_HINT_RANGE, "0,16,0.01"), "set_bounce_indirect_energy", "get_bounce_indirect_energy");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_denoiser"), "set_use_denoiser", "is_using_denoiser");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_hdr"), "set_use_hdr", "is_using_hdr");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_color"), "set_use_color", "is_using_color");
+ ADD_PROPERTY(PropertyInfo(Variant::REAL, "bias", PROPERTY_HINT_RANGE, "0.00001,0.1,0.00001,or_greater"), "set_bias", "get_bias");
+ ADD_PROPERTY(PropertyInfo(Variant::REAL, "default_texels_per_unit", PROPERTY_HINT_RANGE, "0.0,64.0,0.01,or_greater"), "set_default_texels_per_unit", "get_default_texels_per_unit");
+
+ ADD_GROUP("Atlas", "atlas_");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "atlas_generate"), "set_generate_atlas", "is_generate_atlas_enabled");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "atlas_max_size"), "set_max_atlas_size", "get_max_atlas_size");
+
+ ADD_GROUP("Environment", "environment_");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "environment_mode", PROPERTY_HINT_ENUM, "Disabled,Scene,Custom Sky,Custom Color"), "set_environment_mode", "get_environment_mode");
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "environment_custom_sky", PROPERTY_HINT_RESOURCE_TYPE, "Sky"), "set_environment_custom_sky", "get_environment_custom_sky");
+ ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "environment_custom_sky_rotation_degrees", PROPERTY_HINT_NONE), "set_environment_custom_sky_rotation_degrees", "get_environment_custom_sky_rotation_degrees");
+ ADD_PROPERTY(PropertyInfo(Variant::COLOR, "environment_custom_color", PROPERTY_HINT_COLOR_NO_ALPHA), "set_environment_custom_color", "get_environment_custom_color");
+ ADD_PROPERTY(PropertyInfo(Variant::REAL, "environment_custom_energy", PROPERTY_HINT_RANGE, "0,64,0.01"), "set_environment_custom_energy", "get_environment_custom_energy");
+ ADD_PROPERTY(PropertyInfo(Variant::COLOR, "environment_min_light", PROPERTY_HINT_COLOR_NO_ALPHA), "set_environment_min_light", "get_environment_min_light");
+
+ ADD_GROUP("Capture", "capture_");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "capture_enabled"), "set_capture_enabled", "get_capture_enabled");
+ ADD_PROPERTY(PropertyInfo(Variant::REAL, "capture_cell_size", PROPERTY_HINT_RANGE, "0.25,2.0,0.05,or_greater"), "set_capture_cell_size", "get_capture_cell_size");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "capture_quality", PROPERTY_HINT_ENUM, "Low,Medium,High"), "set_capture_quality", "get_capture_quality");
+ ADD_PROPERTY(PropertyInfo(Variant::REAL, "capture_propagation", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_capture_propagation", "get_capture_propagation");
+
+ ADD_GROUP("Data", "");
+#ifndef DISABLE_DEPRECATED
+ ADD_PROPERTY(PropertyInfo(Variant::STRING, "image_path", PROPERTY_HINT_DIR, "", 0), "set_image_path", "get_image_path");
+#endif
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "light_data", PROPERTY_HINT_RESOURCE_TYPE, "BakedLightmapData"), "set_light_data", "get_light_data");
+
+ BIND_ENUM_CONSTANT(BAKE_QUALITY_LOW);
+ BIND_ENUM_CONSTANT(BAKE_QUALITY_MEDIUM);
+ BIND_ENUM_CONSTANT(BAKE_QUALITY_HIGH);
+ BIND_ENUM_CONSTANT(BAKE_QUALITY_ULTRA);
+
+ BIND_ENUM_CONSTANT(BAKE_ERROR_OK);
+ BIND_ENUM_CONSTANT(BAKE_ERROR_NO_SAVE_PATH);
+ BIND_ENUM_CONSTANT(BAKE_ERROR_NO_MESHES);
+ BIND_ENUM_CONSTANT(BAKE_ERROR_CANT_CREATE_IMAGE);
+ BIND_ENUM_CONSTANT(BAKE_ERROR_LIGHTMAP_SIZE);
+ BIND_ENUM_CONSTANT(BAKE_ERROR_INVALID_MESH);
+ BIND_ENUM_CONSTANT(BAKE_ERROR_USER_ABORTED);
+ BIND_ENUM_CONSTANT(BAKE_ERROR_NO_LIGHTMAPPER);
+ BIND_ENUM_CONSTANT(BAKE_ERROR_NO_ROOT);
+
+ BIND_ENUM_CONSTANT(ENVIRONMENT_MODE_DISABLED);
+ BIND_ENUM_CONSTANT(ENVIRONMENT_MODE_SCENE);
+ BIND_ENUM_CONSTANT(ENVIRONMENT_MODE_CUSTOM_SKY);
+ BIND_ENUM_CONSTANT(ENVIRONMENT_MODE_CUSTOM_COLOR);
+}
+
+BakedLightmap::BakedLightmap() {
+ extents = Vector3(10, 10, 10);
+
+ default_texels_per_unit = 16.0f;
+ bake_quality = BAKE_QUALITY_MEDIUM;
+ capture_quality = BAKE_QUALITY_MEDIUM;
+ capture_propagation = 1;
+ capture_enabled = true;
+ bounces = 3;
+ bounce_indirect_energy = 1.0;
+ image_path = "";
+ set_disable_scale(true);
+ capture_cell_size = 0.5;
+
+ environment_mode = ENVIRONMENT_MODE_DISABLED;
+ environment_custom_color = Color(0.2, 0.7, 1.0);
+ environment_custom_energy = 1.0;
+ environment_min_light = Color(0.0, 0.0, 0.0);
+
+ use_denoiser = true;
+ use_hdr = true;
+ use_color = true;
+ bias = 0.005;
+
+ generate_atlas = true;
+ max_atlas_size = 4096;
+}
diff --git a/scene/3d/baked_lightmap.h b/scene/3d/baked_lightmap.h
new file mode 100644
index 000000000..472c458b3
--- /dev/null
+++ b/scene/3d/baked_lightmap.h
@@ -0,0 +1,292 @@
+/**************************************************************************/
+/* baked_lightmap.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef BAKED_LIGHTMAP_H
+#define BAKED_LIGHTMAP_H
+
+#include "core/containers/local_vector.h"
+#include "scene/resources/mesh/mesh.h"
+#include "multimesh_instance.h"
+#include "scene/3d/light.h"
+#include "scene/3d/lightmapper.h"
+#include "scene/3d/visual_instance.h"
+
+class Sky;
+class Environment3D;
+
+class BakedLightmapData : public Resource {
+ GDCLASS(BakedLightmapData, Resource);
+ RES_BASE_EXTENSION("lmbake")
+
+ RID baked_light;
+ AABB bounds;
+ float energy;
+ bool interior;
+ int cell_subdiv;
+ Transform cell_space_xform;
+
+ struct User {
+ NodePath path;
+ struct {
+ Ref single;
+ Ref layered;
+ } lightmap;
+ int lightmap_slice;
+ Rect2 lightmap_uv_rect;
+ int instance_index;
+ };
+
+ Vector users;
+
+ void _set_user_data(const Array &p_data);
+ Array _get_user_data() const;
+
+protected:
+ static void _bind_methods();
+
+public:
+ void set_bounds(const AABB &p_bounds);
+ AABB get_bounds() const;
+
+ void set_octree(const PoolVector &p_octree);
+ PoolVector get_octree() const;
+
+ void set_cell_space_transform(const Transform &p_xform);
+ Transform get_cell_space_transform() const;
+
+ void set_cell_subdiv(int p_cell_subdiv);
+ int get_cell_subdiv() const;
+
+ void set_energy(float p_energy);
+ float get_energy() const;
+
+ void set_interior(bool p_interior);
+ bool is_interior() const;
+
+ void add_user(const NodePath &p_path, const Ref &p_lightmap, int p_lightmap_slice, const Rect2 &p_lightmap_uv_rect, int p_instance);
+ int get_user_count() const;
+ NodePath get_user_path(int p_user) const;
+ Ref get_user_lightmap(int p_user) const;
+ int get_user_lightmap_slice(int p_user) const;
+ Rect2 get_user_lightmap_uv_rect(int p_user) const;
+ int get_user_instance(int p_user) const;
+ void clear_users();
+ void clear_data();
+
+ virtual RID get_rid() const;
+ BakedLightmapData();
+ ~BakedLightmapData();
+};
+
+class BakedLightmap : public VisualInstance {
+ GDCLASS(BakedLightmap, VisualInstance);
+
+public:
+ enum BakeQuality {
+ BAKE_QUALITY_LOW,
+ BAKE_QUALITY_MEDIUM,
+ BAKE_QUALITY_HIGH,
+ BAKE_QUALITY_ULTRA
+ };
+
+ enum BakeError {
+ BAKE_ERROR_OK,
+ BAKE_ERROR_NO_SAVE_PATH,
+ BAKE_ERROR_NO_MESHES,
+ BAKE_ERROR_CANT_CREATE_IMAGE,
+ BAKE_ERROR_LIGHTMAP_SIZE,
+ BAKE_ERROR_INVALID_MESH,
+ BAKE_ERROR_USER_ABORTED,
+ BAKE_ERROR_NO_LIGHTMAPPER,
+ BAKE_ERROR_NO_ROOT,
+ };
+
+ enum EnvironmentMode {
+ ENVIRONMENT_MODE_DISABLED,
+ ENVIRONMENT_MODE_SCENE,
+ ENVIRONMENT_MODE_CUSTOM_SKY,
+ ENVIRONMENT_MODE_CUSTOM_COLOR
+ };
+
+ struct BakeStepUD {
+ Lightmapper::BakeStepFunc func;
+ void *ud;
+ float from_percent;
+ float to_percent;
+ };
+
+ struct LightsFound {
+ Transform xform;
+ Light *light;
+ };
+
+ struct MeshesFound {
+ Transform xform;
+ NodePath node_path;
+ int32_t subindex;
+ Ref mesh;
+ int32_t lightmap_scale;
+ Vector][> overrides;
+ bool cast_shadows;
+ bool generate_lightmap;
+ };
+
+private:
+ Vector3 extents;
+ float default_texels_per_unit;
+ float bias;
+ BakeQuality bake_quality;
+ bool generate_atlas;
+ int max_atlas_size;
+ bool capture_enabled;
+ int bounces;
+ float bounce_indirect_energy;
+ bool use_denoiser;
+ bool use_hdr;
+ bool use_color;
+
+ EnvironmentMode environment_mode;
+ Ref environment_custom_sky;
+ Vector3 environment_custom_sky_rotation_degrees;
+ Color environment_custom_color;
+ float environment_custom_energy;
+ Color environment_min_light;
+
+ BakeQuality capture_quality;
+ float capture_propagation;
+ float capture_cell_size;
+
+ String image_path; // (Deprecated property)
+
+ Ref light_data;
+
+ void _assign_lightmaps();
+ void _clear_lightmaps();
+
+ void _get_material_images(const MeshesFound &p_found_mesh, Lightmapper::MeshData &r_mesh_data, Vector][> &r_albedo_textures, Vector][> &r_emission_textures);
+ Ref _get_irradiance_from_sky(Ref p_sky, float p_energy, Vector2i p_size);
+ Ref _get_irradiance_map(Ref p_env, Vector2i p_size);
+ void _find_meshes_and_lights(Node *p_at_node, Vector &meshes, Vector &lights);
+ Vector2i _compute_lightmap_size(const MeshesFound &p_mesh);
+
+ static bool _lightmap_bake_step_function(float p_completion, const String &p_text, void *ud, bool p_refresh);
+ void _save_image(String &r_base_path, Ref p_img, bool p_use_srgb);
+
+protected:
+ static void _bind_methods();
+ void _validate_property(PropertyInfo &property) const;
+ void _notification(int p_what);
+
+public:
+ static Lightmapper::BakeStepFunc bake_step_function;
+ static Lightmapper::BakeStepFunc bake_substep_function;
+ static Lightmapper::BakeEndFunc bake_end_function;
+
+ void set_light_data(const Ref &p_data);
+ Ref get_light_data() const;
+
+ void set_capture_cell_size(float p_cell_size);
+ float get_capture_cell_size() const;
+
+ void set_extents(const Vector3 &p_extents);
+ Vector3 get_extents() const;
+
+ void set_default_texels_per_unit(const float &p_extents);
+ float get_default_texels_per_unit() const;
+
+ void set_capture_propagation(float p_propagation);
+ float get_capture_propagation() const;
+
+ void set_capture_quality(BakeQuality p_quality);
+ BakeQuality get_capture_quality() const;
+
+ void set_bake_quality(BakeQuality p_quality);
+ BakeQuality get_bake_quality() const;
+
+ void set_generate_atlas(bool p_enabled);
+ bool is_generate_atlas_enabled() const;
+
+ void set_max_atlas_size(int p_size);
+ int get_max_atlas_size() const;
+
+ void set_capture_enabled(bool p_enable);
+ bool get_capture_enabled() const;
+
+ void set_image_path(const String &p_path);
+ String get_image_path() const;
+
+ void set_environment_mode(EnvironmentMode p_mode);
+ EnvironmentMode get_environment_mode() const;
+
+ void set_environment_custom_sky(const Ref &p_sky);
+ Ref get_environment_custom_sky() const;
+
+ void set_environment_custom_sky_rotation_degrees(const Vector3 &p_rotation);
+ Vector3 get_environment_custom_sky_rotation_degrees() const;
+
+ void set_environment_custom_color(const Color &p_color);
+ Color get_environment_custom_color() const;
+
+ void set_environment_custom_energy(float p_energy);
+ float get_environment_custom_energy() const;
+
+ void set_environment_min_light(Color p_min_light);
+ Color get_environment_min_light() const;
+
+ void set_use_denoiser(bool p_enable);
+ bool is_using_denoiser() const;
+
+ void set_use_hdr(bool p_enable);
+ bool is_using_hdr() const;
+
+ void set_use_color(bool p_enable);
+ bool is_using_color() const;
+
+ void set_bounces(int p_bounces);
+ int get_bounces() const;
+
+ void set_bounce_indirect_energy(float p_indirect_energy);
+ float get_bounce_indirect_energy() const;
+
+ void set_bias(float p_bias);
+ float get_bias() const;
+
+ AABB get_aabb() const;
+ PoolVector get_faces(uint32_t p_usage_flags) const;
+
+ BakeError bake(Node *p_from_node, String p_data_save_path = "");
+ BakedLightmap();
+};
+
+VARIANT_ENUM_CAST(BakedLightmap::BakeQuality);
+VARIANT_ENUM_CAST(BakedLightmap::BakeError);
+VARIANT_ENUM_CAST(BakedLightmap::EnvironmentMode);
+
+#endif // BAKED_LIGHTMAP_H
diff --git a/scene/3d/lightmapper.cpp b/scene/3d/lightmapper.cpp
new file mode 100644
index 000000000..0a71a7309
--- /dev/null
+++ b/scene/3d/lightmapper.cpp
@@ -0,0 +1,76 @@
+/**************************************************************************/
+/* lightmapper.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "lightmapper.h"
+
+LightmapDenoiser *(*LightmapDenoiser::create_function)() = nullptr;
+
+Ref LightmapDenoiser::create() {
+ if (create_function) {
+ return Ref(create_function());
+ }
+ return Ref();
+}
+
+LightmapRaycaster *(*LightmapRaycaster::create_function)() = nullptr;
+
+Ref LightmapRaycaster::create() {
+ if (create_function) {
+ return Ref(create_function());
+ }
+ return Ref();
+}
+
+Lightmapper::CreateFunc Lightmapper::create_custom = nullptr;
+Lightmapper::CreateFunc Lightmapper::create_gpu = nullptr;
+Lightmapper::CreateFunc Lightmapper::create_cpu = nullptr;
+
+Ref Lightmapper::create() {
+ Lightmapper *lm = nullptr;
+ if (create_custom) {
+ lm = create_custom();
+ }
+
+ if (!lm && create_gpu) {
+ lm = create_gpu();
+ }
+
+ if (!lm && create_cpu) {
+ lm = create_cpu();
+ }
+ if (!lm) {
+ return Ref();
+ } else {
+ return Ref(lm);
+ }
+}
+
+Lightmapper::Lightmapper() {
+}
diff --git a/scene/3d/lightmapper.h b/scene/3d/lightmapper.h
new file mode 100644
index 000000000..d135d2c64
--- /dev/null
+++ b/scene/3d/lightmapper.h
@@ -0,0 +1,197 @@
+/**************************************************************************/
+/* lightmapper.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef LIGHTMAPPER_H
+#define LIGHTMAPPER_H
+
+#include "scene/resources/mesh/mesh.h"
+
+#if !defined(__aligned)
+
+#if defined(_WIN32) && defined(_MSC_VER)
+#define __aligned(...) __declspec(align(__VA_ARGS__))
+#else
+#define __aligned(...) __attribute__((aligned(__VA_ARGS__)))
+#endif
+
+#endif
+
+class LightmapDenoiser : public Reference {
+ GDCLASS(LightmapDenoiser, Reference)
+protected:
+ static LightmapDenoiser *(*create_function)();
+
+public:
+ virtual Ref denoise_image(const Ref &p_image) = 0;
+ static Ref create();
+};
+
+class LightmapRaycaster : public Reference {
+ GDCLASS(LightmapRaycaster, Reference)
+protected:
+ static LightmapRaycaster *(*create_function)();
+
+public:
+ // compatible with embree3 rays
+ struct __aligned(16) Ray {
+ const static unsigned int INVALID_GEOMETRY_ID = ((unsigned int)-1); // from rtcore_common.h
+
+ /*! Default construction does nothing. */
+ _FORCE_INLINE_ Ray() :
+ geomID(INVALID_GEOMETRY_ID) {}
+
+ /*! Constructs a ray from origin, direction, and ray segment. Near
+ * has to be smaller than far. */
+ _FORCE_INLINE_ Ray(const Vector3 &org,
+ const Vector3 &dir,
+ float tnear = 0.0f,
+ float tfar = INFINITY) :
+ org(org),
+ tnear(tnear),
+ dir(dir),
+ time(0.0f),
+ tfar(tfar),
+ mask(-1),
+ u(0.0),
+ v(0.0),
+ primID(INVALID_GEOMETRY_ID),
+ geomID(INVALID_GEOMETRY_ID),
+ instID(INVALID_GEOMETRY_ID) {}
+
+ /*! Tests if we hit something. */
+ _FORCE_INLINE_ explicit operator bool() const { return geomID != INVALID_GEOMETRY_ID; }
+
+ public:
+ Vector3 org; //!< Ray origin + tnear
+ float tnear; //!< Start of ray segment
+ Vector3 dir; //!< Ray direction + tfar
+ float time; //!< Time of this ray for motion blur.
+ float tfar; //!< End of ray segment
+ unsigned int mask; //!< used to mask out objects during traversal
+ unsigned int id; //!< ray ID
+ unsigned int flags; //!< ray flags
+
+ Vector3 normal; //!< Not normalized geometry normal
+ float u; //!< Barycentric u coordinate of hit
+ float v; //!< Barycentric v coordinate of hit
+ unsigned int primID; //!< primitive ID
+ unsigned int geomID; //!< geometry ID
+ unsigned int instID; //!< instance ID
+ };
+
+ virtual bool intersect(Ray &p_ray) = 0;
+
+ virtual void intersect(Vector &r_rays) = 0;
+
+ virtual void add_mesh(const Vector &p_vertices, const Vector &p_normals, const Vector &p_uv2s, unsigned int p_id) = 0;
+ virtual void set_mesh_alpha_texture(Ref p_alpha_texture, unsigned int p_id) = 0;
+ virtual void commit() = 0;
+
+ virtual void set_mesh_filter(const RBSet &p_mesh_ids) = 0;
+ virtual void clear_mesh_filter() = 0;
+
+ static Ref create();
+};
+
+class Lightmapper : public Reference {
+ GDCLASS(Lightmapper, Reference)
+public:
+ enum LightType {
+ LIGHT_TYPE_DIRECTIONAL,
+ LIGHT_TYPE_OMNI,
+ LIGHT_TYPE_SPOT
+ };
+
+ enum BakeError {
+ BAKE_ERROR_LIGHTMAP_TOO_SMALL,
+ BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES,
+ BAKE_ERROR_NO_MESHES,
+ BAKE_ERROR_USER_ABORTED,
+ BAKE_ERROR_NO_RAYCASTER,
+ BAKE_OK
+ };
+
+ enum BakeQuality {
+ BAKE_QUALITY_LOW,
+ BAKE_QUALITY_MEDIUM,
+ BAKE_QUALITY_HIGH,
+ BAKE_QUALITY_ULTRA,
+ };
+
+ typedef Lightmapper *(*CreateFunc)();
+
+ static CreateFunc create_custom;
+ static CreateFunc create_gpu;
+ static CreateFunc create_cpu;
+
+protected:
+public:
+ typedef bool (*BakeStepFunc)(float, const String &, void *, bool); //progress, step description, userdata, force refresh
+ typedef void (*BakeEndFunc)(uint32_t); // time_started
+
+ struct MeshData {
+ struct TextureDef {
+ RID tex_rid;
+ Color mul;
+ Color add;
+ };
+
+ //triangle data
+ Vector points;
+ Vector uv;
+ Vector uv2;
+ Vector normal;
+ Vector albedo;
+ Vector emission;
+ Vector surface_facecounts;
+ Variant userdata;
+ };
+
+ virtual void add_albedo_texture(Ref p_texture) = 0;
+ virtual void add_emission_texture(Ref p_texture) = 0;
+ virtual void add_mesh(const MeshData &p_mesh, Vector2i p_size) = 0;
+ virtual void add_directional_light(bool p_bake_direct, const Vector3 &p_direction, const Color &p_color, float p_energy, float p_indirect_multiplier, float p_size) = 0;
+ virtual void add_omni_light(bool p_bake_direct, const Vector3 &p_position, const Color &p_color, float p_energy, float p_indirect_multiplier, float p_range, float p_attenuation, float p_size) = 0;
+ virtual void add_spot_light(bool p_bake_direct, const Vector3 &p_position, const Vector3 p_direction, const Color &p_color, float p_energy, float p_indirect_multiplier, float p_range, float p_attenuation, float p_spot_angle, float p_spot_attenuation, float p_size) = 0;
+ virtual BakeError bake(BakeQuality p_quality, bool p_use_denoiser, int p_bounces, float p_bounce_indirect_energy, float p_bias, bool p_generate_atlas, int p_max_texture_size, const Ref &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function = nullptr, void *p_step_userdata = nullptr, BakeStepFunc p_substep_function = nullptr) = 0;
+
+ virtual int get_bake_texture_count() const = 0;
+ virtual Ref get_bake_texture(int p_index) const = 0;
+ virtual int get_bake_mesh_count() const = 0;
+ virtual Variant get_bake_mesh_userdata(int p_index) const = 0;
+ virtual Rect2 get_bake_mesh_uv_scale(int p_index) const = 0;
+ virtual int get_bake_mesh_texture_slice(int p_index) const = 0;
+
+ static Ref create();
+
+ Lightmapper();
+};
+
+#endif // LIGHTMAPPER_H
diff --git a/scene/3d/voxel_light_baker.cpp b/scene/3d/voxel_light_baker.cpp
new file mode 100644
index 000000000..933be7b79
--- /dev/null
+++ b/scene/3d/voxel_light_baker.cpp
@@ -0,0 +1,1600 @@
+/**************************************************************************/
+/* voxel_light_baker.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "voxel_light_baker.h"
+
+#include "core/os/os.h"
+#include "core/os/threaded_array_processor.h"
+#include "scene/resources/material/material.h"
+#include "scene/resources/material/spatial_material.h"
+
+#include
+
+#define FINDMINMAX(x0, x1, x2, min, max) \
+ min = max = x0; \
+ if (x1 < min) \
+ min = x1; \
+ if (x1 > max) \
+ max = x1; \
+ if (x2 < min) \
+ min = x2; \
+ if (x2 > max) \
+ max = x2;
+
+static bool planeBoxOverlap(Vector3 normal, float d, Vector3 maxbox) {
+ int q;
+ Vector3 vmin, vmax;
+ for (q = 0; q <= 2; q++) {
+ if (normal[q] > 0.0f) {
+ vmin[q] = -maxbox[q];
+ vmax[q] = maxbox[q];
+ } else {
+ vmin[q] = maxbox[q];
+ vmax[q] = -maxbox[q];
+ }
+ }
+ if (normal.dot(vmin) + d > 0.0f) {
+ return false;
+ }
+ if (normal.dot(vmax) + d >= 0.0f) {
+ return true;
+ }
+
+ return false;
+}
+
+/*======================== X-tests ========================*/
+#define AXISTEST_X01(a, b, fa, fb) \
+ p0 = a * v0.y - b * v0.z; \
+ p2 = a * v2.y - b * v2.z; \
+ if (p0 < p2) { \
+ min = p0; \
+ max = p2; \
+ } else { \
+ min = p2; \
+ max = p0; \
+ } \
+ rad = fa * boxhalfsize.y + fb * boxhalfsize.z; \
+ if (min > rad || max < -rad) \
+ return false;
+
+#define AXISTEST_X2(a, b, fa, fb) \
+ p0 = a * v0.y - b * v0.z; \
+ p1 = a * v1.y - b * v1.z; \
+ if (p0 < p1) { \
+ min = p0; \
+ max = p1; \
+ } else { \
+ min = p1; \
+ max = p0; \
+ } \
+ rad = fa * boxhalfsize.y + fb * boxhalfsize.z; \
+ if (min > rad || max < -rad) \
+ return false;
+
+/*======================== Y-tests ========================*/
+#define AXISTEST_Y02(a, b, fa, fb) \
+ p0 = -a * v0.x + b * v0.z; \
+ p2 = -a * v2.x + b * v2.z; \
+ if (p0 < p2) { \
+ min = p0; \
+ max = p2; \
+ } else { \
+ min = p2; \
+ max = p0; \
+ } \
+ rad = fa * boxhalfsize.x + fb * boxhalfsize.z; \
+ if (min > rad || max < -rad) \
+ return false;
+
+#define AXISTEST_Y1(a, b, fa, fb) \
+ p0 = -a * v0.x + b * v0.z; \
+ p1 = -a * v1.x + b * v1.z; \
+ if (p0 < p1) { \
+ min = p0; \
+ max = p1; \
+ } else { \
+ min = p1; \
+ max = p0; \
+ } \
+ rad = fa * boxhalfsize.x + fb * boxhalfsize.z; \
+ if (min > rad || max < -rad) \
+ return false;
+
+/*======================== Z-tests ========================*/
+
+#define AXISTEST_Z12(a, b, fa, fb) \
+ p1 = a * v1.x - b * v1.y; \
+ p2 = a * v2.x - b * v2.y; \
+ if (p2 < p1) { \
+ min = p2; \
+ max = p1; \
+ } else { \
+ min = p1; \
+ max = p2; \
+ } \
+ rad = fa * boxhalfsize.x + fb * boxhalfsize.y; \
+ if (min > rad || max < -rad) \
+ return false;
+
+#define AXISTEST_Z0(a, b, fa, fb) \
+ p0 = a * v0.x - b * v0.y; \
+ p1 = a * v1.x - b * v1.y; \
+ if (p0 < p1) { \
+ min = p0; \
+ max = p1; \
+ } else { \
+ min = p1; \
+ max = p0; \
+ } \
+ rad = fa * boxhalfsize.x + fb * boxhalfsize.y; \
+ if (min > rad || max < -rad) \
+ return false;
+
+static bool fast_tri_box_overlap(const Vector3 &boxcenter, const Vector3 boxhalfsize, const Vector3 *triverts) {
+ /* use separating axis theorem to test overlap between triangle and box */
+ /* need to test for overlap in these directions: */
+ /* 1) the {x,y,z}-directions (actually, since we use the AABB of the triangle */
+ /* we do not even need to test these) */
+ /* 2) normal of the triangle */
+ /* 3) crossproduct(edge from tri, {x,y,z}-directin) */
+ /* this gives 3x3=9 more tests */
+ Vector3 v0, v1, v2;
+ float min, max, d, p0, p1, p2, rad, fex, fey, fez;
+ Vector3 normal, e0, e1, e2;
+
+ /* This is the fastest branch on Sun */
+ /* move everything so that the boxcenter is in (0,0,0) */
+
+ v0 = triverts[0] - boxcenter;
+ v1 = triverts[1] - boxcenter;
+ v2 = triverts[2] - boxcenter;
+
+ /* compute triangle edges */
+ e0 = v1 - v0; /* tri edge 0 */
+ e1 = v2 - v1; /* tri edge 1 */
+ e2 = v0 - v2; /* tri edge 2 */
+
+ /* Bullet 3: */
+ /* test the 9 tests first (this was faster) */
+ fex = Math::abs(e0.x);
+ fey = Math::abs(e0.y);
+ fez = Math::abs(e0.z);
+ AXISTEST_X01(e0.z, e0.y, fez, fey);
+ AXISTEST_Y02(e0.z, e0.x, fez, fex);
+ AXISTEST_Z12(e0.y, e0.x, fey, fex);
+
+ fex = Math::abs(e1.x);
+ fey = Math::abs(e1.y);
+ fez = Math::abs(e1.z);
+ AXISTEST_X01(e1.z, e1.y, fez, fey);
+ AXISTEST_Y02(e1.z, e1.x, fez, fex);
+ AXISTEST_Z0(e1.y, e1.x, fey, fex);
+
+ fex = Math::abs(e2.x);
+ fey = Math::abs(e2.y);
+ fez = Math::abs(e2.z);
+ AXISTEST_X2(e2.z, e2.y, fez, fey);
+ AXISTEST_Y1(e2.z, e2.x, fez, fex);
+ AXISTEST_Z12(e2.y, e2.x, fey, fex);
+
+ /* Bullet 1: */
+ /* first test overlap in the {x,y,z}-directions */
+ /* find min, max of the triangle each direction, and test for overlap in */
+ /* that direction -- this is equivalent to testing a minimal AABB around */
+ /* the triangle against the AABB */
+
+ /* test in X-direction */
+ FINDMINMAX(v0.x, v1.x, v2.x, min, max);
+ if (min > boxhalfsize.x || max < -boxhalfsize.x) {
+ return false;
+ }
+
+ /* test in Y-direction */
+ FINDMINMAX(v0.y, v1.y, v2.y, min, max);
+ if (min > boxhalfsize.y || max < -boxhalfsize.y) {
+ return false;
+ }
+
+ /* test in Z-direction */
+ FINDMINMAX(v0.z, v1.z, v2.z, min, max);
+ if (min > boxhalfsize.z || max < -boxhalfsize.z) {
+ return false;
+ }
+
+ /* Bullet 2: */
+ /* test if the box intersects the plane of the triangle */
+ /* compute plane equation of triangle: normal*x+d=0 */
+ normal = e0.cross(e1);
+ d = -normal.dot(v0); /* plane eq: normal.x+d=0 */
+ return planeBoxOverlap(normal, d, boxhalfsize); /* if true, box and triangle overlaps */
+}
+
+static _FORCE_INLINE_ void get_uv_and_normal(const Vector3 &p_pos, const Vector3 *p_vtx, const Vector2 *p_uv, const Vector3 *p_normal, Vector2 &r_uv, Vector3 &r_normal) {
+ if (p_pos.distance_squared_to(p_vtx[0]) < CMP_EPSILON2) {
+ r_uv = p_uv[0];
+ r_normal = p_normal[0];
+ return;
+ }
+ if (p_pos.distance_squared_to(p_vtx[1]) < CMP_EPSILON2) {
+ r_uv = p_uv[1];
+ r_normal = p_normal[1];
+ return;
+ }
+ if (p_pos.distance_squared_to(p_vtx[2]) < CMP_EPSILON2) {
+ r_uv = p_uv[2];
+ r_normal = p_normal[2];
+ return;
+ }
+
+ Vector3 v0 = p_vtx[1] - p_vtx[0];
+ Vector3 v1 = p_vtx[2] - p_vtx[0];
+ Vector3 v2 = p_pos - p_vtx[0];
+
+ float d00 = v0.dot(v0);
+ float d01 = v0.dot(v1);
+ float d11 = v1.dot(v1);
+ float d20 = v2.dot(v0);
+ float d21 = v2.dot(v1);
+ float denom = (d00 * d11 - d01 * d01);
+ if (denom == 0) {
+ r_uv = p_uv[0];
+ r_normal = p_normal[0];
+ return;
+ }
+ float v = (d11 * d20 - d01 * d21) / denom;
+ float w = (d00 * d21 - d01 * d20) / denom;
+ float u = 1.0f - v - w;
+
+ r_uv = p_uv[0] * u + p_uv[1] * v + p_uv[2] * w;
+ r_normal = (p_normal[0] * u + p_normal[1] * v + p_normal[2] * w).normalized();
+}
+
+void VoxelLightBaker::_plot_face(int p_idx, int p_level, int p_x, int p_y, int p_z, const Vector3 *p_vtx, const Vector3 *p_normal, const Vector2 *p_uv, const MaterialCache &p_material, const AABB &p_aabb) {
+ if (p_level == cell_subdiv - 1) {
+ //plot the face by guessing its albedo and emission value
+
+ //find best axis to map to, for scanning values
+ int closest_axis = 0;
+ float closest_dot = 0;
+
+ Plane plane = Plane(p_vtx[0], p_vtx[1], p_vtx[2]);
+ Vector3 normal = plane.normal;
+
+ for (int i = 0; i < 3; i++) {
+ Vector3 axis;
+ axis[i] = 1.0;
+ float dot = ABS(normal.dot(axis));
+ if (i == 0 || dot > closest_dot) {
+ closest_axis = i;
+ closest_dot = dot;
+ }
+ }
+
+ Vector3 axis;
+ axis[closest_axis] = 1.0;
+ Vector3 t1;
+ t1[(closest_axis + 1) % 3] = 1.0;
+ Vector3 t2;
+ t2[(closest_axis + 2) % 3] = 1.0;
+
+ t1 *= p_aabb.size[(closest_axis + 1) % 3] / float(color_scan_cell_width);
+ t2 *= p_aabb.size[(closest_axis + 2) % 3] / float(color_scan_cell_width);
+
+ Color albedo_accum;
+ Color emission_accum;
+ Vector3 normal_accum;
+
+ float alpha = 0.0;
+
+ //map to a grid average in the best axis for this face
+ for (int i = 0; i < color_scan_cell_width; i++) {
+ Vector3 ofs_i = float(i) * t1;
+
+ for (int j = 0; j < color_scan_cell_width; j++) {
+ Vector3 ofs_j = float(j) * t2;
+
+ Vector3 from = p_aabb.position + ofs_i + ofs_j;
+ Vector3 to = from + t1 + t2 + axis * p_aabb.size[closest_axis];
+ Vector3 half = (to - from) * 0.5;
+
+ //is in this cell?
+ if (!fast_tri_box_overlap(from + half, half, p_vtx)) {
+ continue; //face does not span this cell
+ }
+
+ //go from -size to +size*2 to avoid skipping collisions
+ Vector3 ray_from = from + (t1 + t2) * 0.5 - axis * p_aabb.size[closest_axis];
+ Vector3 ray_to = ray_from + axis * p_aabb.size[closest_axis] * 2;
+
+ if (normal.dot(ray_from - ray_to) < 0) {
+ SWAP(ray_from, ray_to);
+ }
+
+ Vector3 intersection;
+
+ if (!plane.intersects_segment(ray_from, ray_to, &intersection)) {
+ if (ABS(plane.distance_to(ray_from)) < ABS(plane.distance_to(ray_to))) {
+ intersection = plane.project(ray_from);
+ } else {
+ intersection = plane.project(ray_to);
+ }
+ }
+
+ intersection = Face3(p_vtx[0], p_vtx[1], p_vtx[2]).get_closest_point_to(intersection);
+
+ Vector2 uv;
+ Vector3 lnormal;
+ get_uv_and_normal(intersection, p_vtx, p_uv, p_normal, uv, lnormal);
+ if (lnormal == Vector3()) { //just in case normal as nor provided
+ lnormal = normal;
+ }
+
+ int uv_x = CLAMP(int(Math::fposmod(uv.x, 1.0f) * bake_texture_size), 0, bake_texture_size - 1);
+ int uv_y = CLAMP(int(Math::fposmod(uv.y, 1.0f) * bake_texture_size), 0, bake_texture_size - 1);
+
+ int ofs = uv_y * bake_texture_size + uv_x;
+ albedo_accum.r += p_material.albedo[ofs].r;
+ albedo_accum.g += p_material.albedo[ofs].g;
+ albedo_accum.b += p_material.albedo[ofs].b;
+ albedo_accum.a += p_material.albedo[ofs].a;
+
+ emission_accum.r += p_material.emission[ofs].r;
+ emission_accum.g += p_material.emission[ofs].g;
+ emission_accum.b += p_material.emission[ofs].b;
+
+ normal_accum += lnormal;
+
+ alpha += 1.0;
+ }
+ }
+
+ if (alpha == 0) {
+ //could not in any way get texture information.. so use closest point to center
+
+ Face3 f(p_vtx[0], p_vtx[1], p_vtx[2]);
+ Vector3 inters = f.get_closest_point_to(p_aabb.position + p_aabb.size * 0.5);
+
+ Vector3 lnormal;
+ Vector2 uv;
+ get_uv_and_normal(inters, p_vtx, p_uv, p_normal, uv, normal);
+ if (lnormal == Vector3()) { //just in case normal as nor provided
+ lnormal = normal;
+ }
+
+ int uv_x = CLAMP(Math::fposmod(uv.x, 1.0f) * bake_texture_size, 0, bake_texture_size - 1);
+ int uv_y = CLAMP(Math::fposmod(uv.y, 1.0f) * bake_texture_size, 0, bake_texture_size - 1);
+
+ int ofs = uv_y * bake_texture_size + uv_x;
+
+ alpha = 1.0 / (color_scan_cell_width * color_scan_cell_width);
+
+ albedo_accum.r = p_material.albedo[ofs].r * alpha;
+ albedo_accum.g = p_material.albedo[ofs].g * alpha;
+ albedo_accum.b = p_material.albedo[ofs].b * alpha;
+ albedo_accum.a = p_material.albedo[ofs].a * alpha;
+
+ emission_accum.r = p_material.emission[ofs].r * alpha;
+ emission_accum.g = p_material.emission[ofs].g * alpha;
+ emission_accum.b = p_material.emission[ofs].b * alpha;
+
+ normal_accum = lnormal * alpha;
+
+ } else {
+ float accdiv = 1.0 / (color_scan_cell_width * color_scan_cell_width);
+ alpha *= accdiv;
+
+ albedo_accum.r *= accdiv;
+ albedo_accum.g *= accdiv;
+ albedo_accum.b *= accdiv;
+ albedo_accum.a *= accdiv;
+
+ emission_accum.r *= accdiv;
+ emission_accum.g *= accdiv;
+ emission_accum.b *= accdiv;
+
+ normal_accum *= accdiv;
+ }
+
+ //put this temporarily here, corrected in a later step
+ bake_cells.write[p_idx].albedo[0] += albedo_accum.r;
+ bake_cells.write[p_idx].albedo[1] += albedo_accum.g;
+ bake_cells.write[p_idx].albedo[2] += albedo_accum.b;
+ bake_cells.write[p_idx].emission[0] += emission_accum.r;
+ bake_cells.write[p_idx].emission[1] += emission_accum.g;
+ bake_cells.write[p_idx].emission[2] += emission_accum.b;
+ bake_cells.write[p_idx].normal[0] += normal_accum.x;
+ bake_cells.write[p_idx].normal[1] += normal_accum.y;
+ bake_cells.write[p_idx].normal[2] += normal_accum.z;
+ bake_cells.write[p_idx].alpha += alpha;
+
+ } else {
+ //go down
+
+ int half = (1 << (cell_subdiv - 1)) >> (p_level + 1);
+ for (int i = 0; i < 8; i++) {
+ AABB aabb = p_aabb;
+ aabb.size *= 0.5;
+
+ int nx = p_x;
+ int ny = p_y;
+ int nz = p_z;
+
+ if (i & 1) {
+ aabb.position.x += aabb.size.x;
+ nx += half;
+ }
+ if (i & 2) {
+ aabb.position.y += aabb.size.y;
+ ny += half;
+ }
+ if (i & 4) {
+ aabb.position.z += aabb.size.z;
+ nz += half;
+ }
+ //make sure to not plot beyond limits
+ if (nx < 0 || nx >= axis_cell_size[0] || ny < 0 || ny >= axis_cell_size[1] || nz < 0 || nz >= axis_cell_size[2]) {
+ continue;
+ }
+
+ {
+ AABB test_aabb = aabb;
+ //test_aabb.grow_by(test_aabb.get_longest_axis_size()*0.05); //grow a bit to avoid numerical error in real-time
+ Vector3 qsize = test_aabb.size * 0.5; //quarter size, for fast aabb test
+
+ if (!fast_tri_box_overlap(test_aabb.position + qsize, qsize, p_vtx)) {
+ //if (!Face3(p_vtx[0],p_vtx[1],p_vtx[2]).intersects_aabb2(aabb)) {
+ //does not fit in child, go on
+ continue;
+ }
+ }
+
+ if (bake_cells[p_idx].children[i] == CHILD_EMPTY) {
+ //sub cell must be created
+
+ uint32_t child_idx = bake_cells.size();
+ bake_cells.write[p_idx].children[i] = child_idx;
+ bake_cells.resize(bake_cells.size() + 1);
+ bake_cells.write[child_idx].level = p_level + 1;
+ }
+
+ _plot_face(bake_cells[p_idx].children[i], p_level + 1, nx, ny, nz, p_vtx, p_normal, p_uv, p_material, aabb);
+ }
+ }
+}
+
+Vector VoxelLightBaker::_get_bake_texture(Ref p_image, const Color &p_color_mul, const Color &p_color_add) {
+ Vector ret;
+
+ if (p_image.is_null() || p_image->empty()) {
+ ret.resize(bake_texture_size * bake_texture_size);
+ for (int i = 0; i < bake_texture_size * bake_texture_size; i++) {
+ ret.write[i] = p_color_add;
+ }
+
+ return ret;
+ }
+ p_image = p_image->duplicate();
+
+ if (p_image->is_compressed()) {
+ p_image->decompress();
+ }
+ p_image->convert(Image::FORMAT_RGBA8);
+ p_image->resize(bake_texture_size, bake_texture_size, Image::INTERPOLATE_CUBIC);
+
+ PoolVector::Read r = p_image->get_data().read();
+ ret.resize(bake_texture_size * bake_texture_size);
+
+ for (int i = 0; i < bake_texture_size * bake_texture_size; i++) {
+ Color c;
+ c.r = (r[i * 4 + 0] / 255.0) * p_color_mul.r + p_color_add.r;
+ c.g = (r[i * 4 + 1] / 255.0) * p_color_mul.g + p_color_add.g;
+ c.b = (r[i * 4 + 2] / 255.0) * p_color_mul.b + p_color_add.b;
+
+ c.a = r[i * 4 + 3] / 255.0;
+
+ ret.write[i] = c;
+ }
+
+ return ret;
+}
+
+VoxelLightBaker::MaterialCache VoxelLightBaker::_get_material_cache(Ref p_material) {
+ //this way of obtaining materials is inaccurate and also does not support some compressed formats very well
+ Ref mat = p_material;
+
+ Ref material = mat; //hack for now
+
+ if (material_cache.has(material)) {
+ return material_cache[material];
+ }
+
+ MaterialCache mc;
+
+ Ref empty;
+
+ if (mat.is_valid()) {
+ Ref albedo_tex = mat->get_texture(SpatialMaterial::TEXTURE_ALBEDO);
+
+ Ref img_albedo;
+ if (albedo_tex.is_valid()) {
+ img_albedo = albedo_tex->get_data();
+ mc.albedo = _get_bake_texture(img_albedo, mat->get_albedo(), Color(0, 0, 0)); // albedo texture, color is multiplicative
+ } else {
+ mc.albedo = _get_bake_texture(img_albedo, Color(1, 1, 1), mat->get_albedo()); // no albedo texture, color is additive
+ }
+
+ if (mat->get_feature(SpatialMaterial::FEATURE_EMISSION)) {
+ Ref emission_tex = mat->get_texture(SpatialMaterial::TEXTURE_EMISSION);
+
+ Color emission_col = mat->get_emission();
+ float emission_energy = mat->get_emission_energy();
+
+ Ref img_emission;
+
+ if (emission_tex.is_valid()) {
+ img_emission = emission_tex->get_data();
+ }
+
+ if (mat->get_emission_operator() == SpatialMaterial::EMISSION_OP_ADD) {
+ mc.emission = _get_bake_texture(img_emission, Color(1, 1, 1) * emission_energy, emission_col * emission_energy);
+ } else {
+ mc.emission = _get_bake_texture(img_emission, emission_col * emission_energy, Color(0, 0, 0));
+ }
+ } else {
+ mc.emission = _get_bake_texture(empty, Color(0, 0, 0), Color(0, 0, 0));
+ }
+
+ } else {
+ mc.albedo = _get_bake_texture(empty, Color(0, 0, 0), Color(1, 1, 1));
+ mc.emission = _get_bake_texture(empty, Color(0, 0, 0), Color(0, 0, 0));
+ }
+
+ material_cache[p_material] = mc;
+ return mc;
+}
+
+void VoxelLightBaker::plot_mesh(const Transform &p_xform, Ref &p_mesh, const Vector][> &p_materials, const Ref &p_override_material) {
+ for (int i = 0; i < p_mesh->get_surface_count(); i++) {
+ if (p_mesh->surface_get_primitive_type(i) != Mesh::PRIMITIVE_TRIANGLES) {
+ continue; //only triangles
+ }
+
+ Ref src_material;
+
+ if (p_override_material.is_valid()) {
+ src_material = p_override_material;
+ } else if (i < p_materials.size() && p_materials[i].is_valid()) {
+ src_material = p_materials[i];
+ } else {
+ src_material = p_mesh->surface_get_material(i);
+ }
+ MaterialCache material = _get_material_cache(src_material);
+
+ Array a = p_mesh->surface_get_arrays(i);
+
+ PoolVector vertices = a[Mesh::ARRAY_VERTEX];
+ PoolVector::Read vr = vertices.read();
+ PoolVector uv = a[Mesh::ARRAY_TEX_UV];
+ PoolVector::Read uvr;
+ PoolVector normals = a[Mesh::ARRAY_NORMAL];
+ PoolVector::Read nr;
+ PoolVector index = a[Mesh::ARRAY_INDEX];
+
+ bool read_uv = false;
+ bool read_normals = false;
+
+ if (uv.size()) {
+ uvr = uv.read();
+ read_uv = true;
+ }
+
+ if (normals.size()) {
+ read_normals = true;
+ nr = normals.read();
+ }
+
+ if (index.size()) {
+ int facecount = index.size() / 3;
+ PoolVector::Read ir = index.read();
+
+ for (int j = 0; j < facecount; j++) {
+ Vector3 vtxs[3];
+ Vector2 uvs[3];
+ Vector3 normal[3];
+
+ for (int k = 0; k < 3; k++) {
+ vtxs[k] = p_xform.xform(vr[ir[j * 3 + k]]);
+ }
+
+ if (read_uv) {
+ for (int k = 0; k < 3; k++) {
+ uvs[k] = uvr[ir[j * 3 + k]];
+ }
+ }
+
+ if (read_normals) {
+ for (int k = 0; k < 3; k++) {
+ normal[k] = nr[ir[j * 3 + k]];
+ }
+ }
+
+ //test against original bounds
+ if (!fast_tri_box_overlap(original_bounds.position + original_bounds.size * 0.5, original_bounds.size * 0.5, vtxs)) {
+ continue;
+ }
+ //plot
+ _plot_face(0, 0, 0, 0, 0, vtxs, normal, uvs, material, po2_bounds);
+ }
+
+ } else {
+ int facecount = vertices.size() / 3;
+
+ for (int j = 0; j < facecount; j++) {
+ Vector3 vtxs[3];
+ Vector2 uvs[3];
+ Vector3 normal[3];
+
+ for (int k = 0; k < 3; k++) {
+ vtxs[k] = p_xform.xform(vr[j * 3 + k]);
+ }
+
+ if (read_uv) {
+ for (int k = 0; k < 3; k++) {
+ uvs[k] = uvr[j * 3 + k];
+ }
+ }
+
+ if (read_normals) {
+ for (int k = 0; k < 3; k++) {
+ normal[k] = nr[j * 3 + k];
+ }
+ }
+
+ //test against original bounds
+ if (!fast_tri_box_overlap(original_bounds.position + original_bounds.size * 0.5, original_bounds.size * 0.5, vtxs)) {
+ continue;
+ }
+ //plot face
+ _plot_face(0, 0, 0, 0, 0, vtxs, normal, uvs, material, po2_bounds);
+ }
+ }
+ }
+
+ max_original_cells = bake_cells.size();
+}
+
+void VoxelLightBaker::_init_light_plot(int p_idx, int p_level, int p_x, int p_y, int p_z, uint32_t p_parent) {
+ bake_light.write[p_idx].x = p_x;
+ bake_light.write[p_idx].y = p_y;
+ bake_light.write[p_idx].z = p_z;
+
+ if (p_level == cell_subdiv - 1) {
+ bake_light.write[p_idx].next_leaf = first_leaf;
+ first_leaf = p_idx;
+ } else {
+ //go down
+ int half = (1 << (cell_subdiv - 1)) >> (p_level + 1);
+ for (int i = 0; i < 8; i++) {
+ uint32_t child = bake_cells[p_idx].children[i];
+
+ if (child == CHILD_EMPTY) {
+ continue;
+ }
+
+ int nx = p_x;
+ int ny = p_y;
+ int nz = p_z;
+
+ if (i & 1) {
+ nx += half;
+ }
+ if (i & 2) {
+ ny += half;
+ }
+ if (i & 4) {
+ nz += half;
+ }
+
+ _init_light_plot(child, p_level + 1, nx, ny, nz, p_idx);
+ }
+ }
+}
+
+void VoxelLightBaker::begin_bake_light(BakeQuality p_quality, float p_propagation) {
+ _check_init_light();
+ propagation = p_propagation;
+ bake_quality = p_quality;
+}
+
+void VoxelLightBaker::_check_init_light() {
+ if (bake_light.size() == 0) {
+ direct_lights_baked = false;
+ leaf_voxel_count = 0;
+ _fixup_plot(0, 0); //pre fixup, so normal, albedo, emission, etc. work for lighting.
+ bake_light.resize(bake_cells.size());
+ //memset(bake_light.ptrw(), 0, bake_light.size() * sizeof(Light));
+ first_leaf = -1;
+ _init_light_plot(0, 0, 0, 0, 0, CHILD_EMPTY);
+ }
+}
+
+static float _get_normal_advance(const Vector3 &p_normal) {
+ Vector3 normal = p_normal;
+ Vector3 unorm = normal.abs();
+
+ if ((unorm.x >= unorm.y) && (unorm.x >= unorm.z)) {
+ // x code
+ unorm = normal.x > 0.0 ? Vector3(1.0, 0.0, 0.0) : Vector3(-1.0, 0.0, 0.0);
+ } else if ((unorm.y > unorm.x) && (unorm.y >= unorm.z)) {
+ // y code
+ unorm = normal.y > 0.0 ? Vector3(0.0, 1.0, 0.0) : Vector3(0.0, -1.0, 0.0);
+ } else if ((unorm.z > unorm.x) && (unorm.z > unorm.y)) {
+ // z code
+ unorm = normal.z > 0.0 ? Vector3(0.0, 0.0, 1.0) : Vector3(0.0, 0.0, -1.0);
+ } else {
+ // oh-no we messed up code
+ // has to be
+ unorm = Vector3(1.0, 0.0, 0.0);
+ }
+
+ return 1.0 / normal.dot(unorm);
+}
+
+static const Vector3 aniso_normal[6] = {
+ Vector3(-1, 0, 0),
+ Vector3(1, 0, 0),
+ Vector3(0, -1, 0),
+ Vector3(0, 1, 0),
+ Vector3(0, 0, -1),
+ Vector3(0, 0, 1)
+};
+
+uint32_t VoxelLightBaker::_find_cell_at_pos(const Cell *cells, int x, int y, int z) {
+ uint32_t cell = 0;
+
+ int ofs_x = 0;
+ int ofs_y = 0;
+ int ofs_z = 0;
+ int size = 1 << (cell_subdiv - 1);
+ int half = size / 2;
+
+ if (x < 0 || x >= size) {
+ return -1;
+ }
+ if (y < 0 || y >= size) {
+ return -1;
+ }
+ if (z < 0 || z >= size) {
+ return -1;
+ }
+
+ for (int i = 0; i < cell_subdiv - 1; i++) {
+ const Cell *bc = &cells[cell];
+
+ int child = 0;
+ if (x >= ofs_x + half) {
+ child |= 1;
+ ofs_x += half;
+ }
+ if (y >= ofs_y + half) {
+ child |= 2;
+ ofs_y += half;
+ }
+ if (z >= ofs_z + half) {
+ child |= 4;
+ ofs_z += half;
+ }
+
+ cell = bc->children[child];
+ if (cell == CHILD_EMPTY) {
+ return CHILD_EMPTY;
+ }
+
+ half >>= 1;
+ }
+
+ return cell;
+}
+void VoxelLightBaker::plot_light_directional(const Vector3 &p_direction, const Color &p_color, float p_energy, float p_indirect_energy, bool p_direct) {
+ _check_init_light();
+
+ float max_len = Vector3(axis_cell_size[0], axis_cell_size[1], axis_cell_size[2]).length() * 1.1;
+
+ if (p_direct) {
+ direct_lights_baked = true;
+ }
+
+ Vector3 light_axis = p_direction;
+ Plane clip[3];
+ int clip_planes = 0;
+
+ Light *light_data = bake_light.ptrw();
+ const Cell *cells = bake_cells.ptr();
+
+ for (int i = 0; i < 3; i++) {
+ if (Math::is_zero_approx(light_axis[i])) {
+ continue;
+ }
+ clip[clip_planes].normal[i] = 1.0;
+
+ if (light_axis[i] < 0) {
+ clip[clip_planes].d = axis_cell_size[i] + 1;
+ } else {
+ clip[clip_planes].d -= 1.0;
+ }
+
+ clip_planes++;
+ }
+
+ float distance_adv = _get_normal_advance(light_axis);
+
+ Vector3 light_energy = Vector3(p_color.r, p_color.g, p_color.b) * p_energy * p_indirect_energy;
+
+ int idx = first_leaf;
+ while (idx >= 0) {
+ Light *light = &light_data[idx];
+
+ Vector3 to(light->x + 0.5, light->y + 0.5, light->z + 0.5);
+ to += -light_axis.sign() * 0.47; //make it more likely to receive a ray
+
+ Vector3 from = to - max_len * light_axis;
+
+ for (int j = 0; j < clip_planes; j++) {
+ clip[j].intersects_segment(from, to, &from);
+ }
+
+ float distance = (to - from).length();
+ distance += distance_adv - Math::fmod(distance, distance_adv); //make it reach the center of the box always
+ from = to - light_axis * distance;
+
+ uint32_t result = 0xFFFFFFFF;
+
+ while (distance > -distance_adv) { //use this to avoid precision errors
+
+ result = _find_cell_at_pos(cells, int(floor(from.x)), int(floor(from.y)), int(floor(from.z)));
+ if (result != 0xFFFFFFFF) {
+ break;
+ }
+
+ from += light_axis * distance_adv;
+ distance -= distance_adv;
+ }
+
+ if (result == (uint32_t)idx) {
+ //cell hit itself! hooray!
+
+ Vector3 normal(cells[idx].normal[0], cells[idx].normal[1], cells[idx].normal[2]);
+ if (normal == Vector3()) {
+ for (int i = 0; i < 6; i++) {
+ light->accum[i][0] += light_energy.x * cells[idx].albedo[0];
+ light->accum[i][1] += light_energy.y * cells[idx].albedo[1];
+ light->accum[i][2] += light_energy.z * cells[idx].albedo[2];
+ }
+
+ } else {
+ for (int i = 0; i < 6; i++) {
+ float s = MAX(0.0, aniso_normal[i].dot(-normal));
+ light->accum[i][0] += light_energy.x * cells[idx].albedo[0] * s;
+ light->accum[i][1] += light_energy.y * cells[idx].albedo[1] * s;
+ light->accum[i][2] += light_energy.z * cells[idx].albedo[2] * s;
+ }
+ }
+
+ if (p_direct) {
+ for (int i = 0; i < 6; i++) {
+ float s = MAX(0.0, aniso_normal[i].dot(-light_axis)); //light depending on normal for direct
+ light->direct_accum[i][0] += light_energy.x * s;
+ light->direct_accum[i][1] += light_energy.y * s;
+ light->direct_accum[i][2] += light_energy.z * s;
+ }
+ }
+ }
+
+ idx = light_data[idx].next_leaf;
+ }
+}
+
+void VoxelLightBaker::plot_light_omni(const Vector3 &p_pos, const Color &p_color, float p_energy, float p_indirect_energy, float p_radius, float p_attenutation, bool p_direct) {
+ _check_init_light();
+
+ if (p_direct) {
+ direct_lights_baked = true;
+ }
+
+ Plane clip[3];
+ int clip_planes = 0;
+
+ // uint64_t us = OS::get_singleton()->get_ticks_usec();
+
+ Vector3 light_pos = to_cell_space.xform(p_pos) + Vector3(0.5, 0.5, 0.5);
+ //Vector3 spot_axis = -light_cache.transform.basis.get_axis(2).normalized();
+
+ float local_radius = to_cell_space.basis.xform(Vector3(0, 0, 1)).length() * p_radius;
+
+ Light *light_data = bake_light.ptrw();
+ const Cell *cells = bake_cells.ptr();
+ Vector3 light_energy = Vector3(p_color.r, p_color.g, p_color.b) * p_energy * p_indirect_energy;
+
+ int idx = first_leaf;
+ while (idx >= 0) {
+ Light *light = &light_data[idx];
+
+ Vector3 to(light->x + 0.5, light->y + 0.5, light->z + 0.5);
+ to += (light_pos - to).sign() * 0.47; //make it more likely to receive a ray
+
+ Vector3 light_axis = (to - light_pos).normalized();
+ float distance_adv = _get_normal_advance(light_axis);
+
+ Vector3 normal(cells[idx].normal[0], cells[idx].normal[1], cells[idx].normal[2]);
+
+ if (normal != Vector3() && normal.dot(-light_axis) < 0.001) {
+ idx = light_data[idx].next_leaf;
+ continue;
+ }
+
+ float att = 1.0;
+ {
+ float d = light_pos.distance_to(to);
+ if (d + distance_adv > local_radius) {
+ idx = light_data[idx].next_leaf;
+ continue; // too far away
+ }
+
+ float dt = CLAMP((d + distance_adv) / local_radius, 0, 1);
+ att *= powf(1.0 - dt, p_attenutation);
+ }
+
+ clip_planes = 0;
+
+ for (int c = 0; c < 3; c++) {
+ if (Math::is_zero_approx(light_axis[c])) {
+ continue;
+ }
+ clip[clip_planes].normal[c] = 1.0;
+
+ if (light_axis[c] < 0) {
+ clip[clip_planes].d = (1 << (cell_subdiv - 1)) + 1;
+ } else {
+ clip[clip_planes].d -= 1.0;
+ }
+
+ clip_planes++;
+ }
+
+ Vector3 from = light_pos;
+
+ for (int j = 0; j < clip_planes; j++) {
+ clip[j].intersects_segment(from, to, &from);
+ }
+
+ float distance = (to - from).length();
+
+ distance -= Math::fmod(distance, distance_adv); //make it reach the center of the box always, but this tame make it closer
+ from = to - light_axis * distance;
+ to += (light_pos - to).sign() * 0.47; //make it more likely to receive a ray
+
+ uint32_t result = 0xFFFFFFFF;
+
+ while (distance > -distance_adv) { //use this to avoid precision errors
+
+ result = _find_cell_at_pos(cells, int(floor(from.x)), int(floor(from.y)), int(floor(from.z)));
+ if (result != 0xFFFFFFFF) {
+ break;
+ }
+
+ from += light_axis * distance_adv;
+ distance -= distance_adv;
+ }
+
+ if (result == (uint32_t)idx) {
+ //cell hit itself! hooray!
+
+ if (normal == Vector3()) {
+ for (int i = 0; i < 6; i++) {
+ light->accum[i][0] += light_energy.x * cells[idx].albedo[0] * att;
+ light->accum[i][1] += light_energy.y * cells[idx].albedo[1] * att;
+ light->accum[i][2] += light_energy.z * cells[idx].albedo[2] * att;
+ }
+
+ } else {
+ for (int i = 0; i < 6; i++) {
+ float s = MAX(0.0, aniso_normal[i].dot(-normal));
+ light->accum[i][0] += light_energy.x * cells[idx].albedo[0] * s * att;
+ light->accum[i][1] += light_energy.y * cells[idx].albedo[1] * s * att;
+ light->accum[i][2] += light_energy.z * cells[idx].albedo[2] * s * att;
+ }
+ }
+
+ if (p_direct) {
+ for (int i = 0; i < 6; i++) {
+ float s = MAX(0.0, aniso_normal[i].dot(-light_axis)); //light depending on normal for direct
+ light->direct_accum[i][0] += light_energy.x * s * att;
+ light->direct_accum[i][1] += light_energy.y * s * att;
+ light->direct_accum[i][2] += light_energy.z * s * att;
+ }
+ }
+ }
+
+ idx = light_data[idx].next_leaf;
+ }
+}
+
+void VoxelLightBaker::plot_light_spot(const Vector3 &p_pos, const Vector3 &p_axis, const Color &p_color, float p_energy, float p_indirect_energy, float p_radius, float p_attenutation, float p_spot_angle, float p_spot_attenuation, bool p_direct) {
+ _check_init_light();
+
+ if (p_direct) {
+ direct_lights_baked = true;
+ }
+
+ Plane clip[3];
+ int clip_planes = 0;
+
+ // uint64_t us = OS::get_singleton()->get_ticks_usec();
+
+ Vector3 light_pos = to_cell_space.xform(p_pos) + Vector3(0.5, 0.5, 0.5);
+ Vector3 spot_axis = to_cell_space.basis.xform(p_axis).normalized();
+
+ float local_radius = to_cell_space.basis.xform(Vector3(0, 0, 1)).length() * p_radius;
+
+ Light *light_data = bake_light.ptrw();
+ const Cell *cells = bake_cells.ptr();
+ Vector3 light_energy = Vector3(p_color.r, p_color.g, p_color.b) * p_energy * p_indirect_energy;
+
+ int idx = first_leaf;
+ while (idx >= 0) {
+ Light *light = &light_data[idx];
+
+ Vector3 to(light->x + 0.5, light->y + 0.5, light->z + 0.5);
+
+ Vector3 light_axis = (to - light_pos).normalized();
+ float distance_adv = _get_normal_advance(light_axis);
+
+ Vector3 normal(cells[idx].normal[0], cells[idx].normal[1], cells[idx].normal[2]);
+
+ if (normal != Vector3() && normal.dot(-light_axis) < 0.001) {
+ idx = light_data[idx].next_leaf;
+ continue;
+ }
+
+ float angle = Math::rad2deg(Math::acos(light_axis.dot(-spot_axis)));
+ if (angle > p_spot_angle) {
+ idx = light_data[idx].next_leaf;
+ continue; // too far away
+ }
+
+ float att = Math::pow(1.0f - angle / p_spot_angle, p_spot_attenuation);
+
+ {
+ float d = light_pos.distance_to(to);
+ if (d + distance_adv > local_radius) {
+ idx = light_data[idx].next_leaf;
+ continue; // too far away
+ }
+
+ float dt = CLAMP((d + distance_adv) / local_radius, 0, 1);
+ att *= powf(1.0 - dt, p_attenutation);
+ }
+
+ clip_planes = 0;
+
+ for (int c = 0; c < 3; c++) {
+ if (Math::is_zero_approx(light_axis[c])) {
+ continue;
+ }
+ clip[clip_planes].normal[c] = 1.0;
+
+ if (light_axis[c] < 0) {
+ clip[clip_planes].d = (1 << (cell_subdiv - 1)) + 1;
+ } else {
+ clip[clip_planes].d -= 1.0;
+ }
+
+ clip_planes++;
+ }
+
+ Vector3 from = light_pos;
+
+ for (int j = 0; j < clip_planes; j++) {
+ clip[j].intersects_segment(from, to, &from);
+ }
+
+ float distance = (to - from).length();
+
+ distance -= Math::fmod(distance, distance_adv); //make it reach the center of the box always, but this tame make it closer
+ from = to - light_axis * distance;
+
+ uint32_t result = 0xFFFFFFFF;
+
+ while (distance > -distance_adv) { //use this to avoid precision errors
+
+ result = _find_cell_at_pos(cells, int(floor(from.x)), int(floor(from.y)), int(floor(from.z)));
+ if (result != 0xFFFFFFFF) {
+ break;
+ }
+
+ from += light_axis * distance_adv;
+ distance -= distance_adv;
+ }
+
+ if (result == (uint32_t)idx) {
+ //cell hit itself! hooray!
+
+ if (normal == Vector3()) {
+ for (int i = 0; i < 6; i++) {
+ light->accum[i][0] += light_energy.x * cells[idx].albedo[0] * att;
+ light->accum[i][1] += light_energy.y * cells[idx].albedo[1] * att;
+ light->accum[i][2] += light_energy.z * cells[idx].albedo[2] * att;
+ }
+
+ } else {
+ for (int i = 0; i < 6; i++) {
+ float s = MAX(0.0, aniso_normal[i].dot(-normal));
+ light->accum[i][0] += light_energy.x * cells[idx].albedo[0] * s * att;
+ light->accum[i][1] += light_energy.y * cells[idx].albedo[1] * s * att;
+ light->accum[i][2] += light_energy.z * cells[idx].albedo[2] * s * att;
+ }
+ }
+
+ if (p_direct) {
+ for (int i = 0; i < 6; i++) {
+ float s = MAX(0.0, aniso_normal[i].dot(-light_axis)); //light depending on normal for direct
+ light->direct_accum[i][0] += light_energy.x * s * att;
+ light->direct_accum[i][1] += light_energy.y * s * att;
+ light->direct_accum[i][2] += light_energy.z * s * att;
+ }
+ }
+ }
+
+ idx = light_data[idx].next_leaf;
+ }
+}
+
+void VoxelLightBaker::_fixup_plot(int p_idx, int p_level) {
+ if (p_level == cell_subdiv - 1) {
+ leaf_voxel_count++;
+ float alpha = bake_cells[p_idx].alpha;
+
+ bake_cells.write[p_idx].albedo[0] /= alpha;
+ bake_cells.write[p_idx].albedo[1] /= alpha;
+ bake_cells.write[p_idx].albedo[2] /= alpha;
+
+ //transfer emission to light
+ bake_cells.write[p_idx].emission[0] /= alpha;
+ bake_cells.write[p_idx].emission[1] /= alpha;
+ bake_cells.write[p_idx].emission[2] /= alpha;
+
+ bake_cells.write[p_idx].normal[0] /= alpha;
+ bake_cells.write[p_idx].normal[1] /= alpha;
+ bake_cells.write[p_idx].normal[2] /= alpha;
+
+ Vector3 n(bake_cells[p_idx].normal[0], bake_cells[p_idx].normal[1], bake_cells[p_idx].normal[2]);
+ if (n.length() < 0.01) {
+ //too much fight over normal, zero it
+ bake_cells.write[p_idx].normal[0] = 0;
+ bake_cells.write[p_idx].normal[1] = 0;
+ bake_cells.write[p_idx].normal[2] = 0;
+ } else {
+ n.normalize();
+ bake_cells.write[p_idx].normal[0] = n.x;
+ bake_cells.write[p_idx].normal[1] = n.y;
+ bake_cells.write[p_idx].normal[2] = n.z;
+ }
+
+ bake_cells.write[p_idx].alpha = 1.0;
+
+ /*if (bake_light.size()) {
+ for(int i=0;i<6;i++) {
+
+ }
+ }*/
+
+ } else {
+ //go down
+
+ bake_cells.write[p_idx].emission[0] = 0;
+ bake_cells.write[p_idx].emission[1] = 0;
+ bake_cells.write[p_idx].emission[2] = 0;
+ bake_cells.write[p_idx].normal[0] = 0;
+ bake_cells.write[p_idx].normal[1] = 0;
+ bake_cells.write[p_idx].normal[2] = 0;
+ bake_cells.write[p_idx].albedo[0] = 0;
+ bake_cells.write[p_idx].albedo[1] = 0;
+ bake_cells.write[p_idx].albedo[2] = 0;
+ if (bake_light.size()) {
+ for (int j = 0; j < 6; j++) {
+ bake_light.write[p_idx].accum[j][0] = 0;
+ bake_light.write[p_idx].accum[j][1] = 0;
+ bake_light.write[p_idx].accum[j][2] = 0;
+ }
+ }
+
+ float alpha_average = 0;
+ int children_found = 0;
+
+ for (int i = 0; i < 8; i++) {
+ uint32_t child = bake_cells[p_idx].children[i];
+
+ if (child == CHILD_EMPTY) {
+ continue;
+ }
+
+ _fixup_plot(child, p_level + 1);
+ alpha_average += bake_cells[child].alpha;
+
+ if (bake_light.size() > 0) {
+ for (int j = 0; j < 6; j++) {
+ bake_light.write[p_idx].accum[j][0] += bake_light[child].accum[j][0];
+ bake_light.write[p_idx].accum[j][1] += bake_light[child].accum[j][1];
+ bake_light.write[p_idx].accum[j][2] += bake_light[child].accum[j][2];
+ }
+ bake_cells.write[p_idx].emission[0] += bake_cells[child].emission[0];
+ bake_cells.write[p_idx].emission[1] += bake_cells[child].emission[1];
+ bake_cells.write[p_idx].emission[2] += bake_cells[child].emission[2];
+ }
+
+ children_found++;
+ }
+
+ bake_cells.write[p_idx].alpha = alpha_average / 8.0;
+ if (bake_light.size() && children_found) {
+ float divisor = Math::lerp(8, children_found, propagation);
+ for (int j = 0; j < 6; j++) {
+ bake_light.write[p_idx].accum[j][0] /= divisor;
+ bake_light.write[p_idx].accum[j][1] /= divisor;
+ bake_light.write[p_idx].accum[j][2] /= divisor;
+ }
+ bake_cells.write[p_idx].emission[0] /= divisor;
+ bake_cells.write[p_idx].emission[1] /= divisor;
+ bake_cells.write[p_idx].emission[2] /= divisor;
+ }
+ }
+}
+
+void VoxelLightBaker::begin_bake(int p_subdiv, const AABB &p_bounds) {
+ original_bounds = p_bounds;
+ cell_subdiv = p_subdiv;
+ bake_cells.resize(1);
+ material_cache.clear();
+
+ //find out the actual real bounds, power of 2, which gets the highest subdivision
+ po2_bounds = p_bounds;
+ int longest_axis = po2_bounds.get_longest_axis_index();
+ axis_cell_size[longest_axis] = (1 << (cell_subdiv - 1));
+ leaf_voxel_count = 0;
+
+ for (int i = 0; i < 3; i++) {
+ if (i == longest_axis) {
+ continue;
+ }
+
+ axis_cell_size[i] = axis_cell_size[longest_axis];
+ float axis_size = po2_bounds.size[longest_axis];
+
+ //shrink until fit subdiv
+ while (axis_size / 2.0 >= po2_bounds.size[i]) {
+ axis_size /= 2.0;
+ axis_cell_size[i] >>= 1;
+ }
+
+ po2_bounds.size[i] = po2_bounds.size[longest_axis];
+ }
+
+ Transform to_bounds;
+ to_bounds.basis.scale(Vector3(po2_bounds.size[longest_axis], po2_bounds.size[longest_axis], po2_bounds.size[longest_axis]));
+ to_bounds.origin = po2_bounds.position;
+
+ Transform to_grid;
+ to_grid.basis.scale(Vector3(axis_cell_size[longest_axis], axis_cell_size[longest_axis], axis_cell_size[longest_axis]));
+
+ to_cell_space = to_grid * to_bounds.affine_inverse();
+
+ cell_size = po2_bounds.size[longest_axis] / axis_cell_size[longest_axis];
+}
+
+void VoxelLightBaker::end_bake() {
+ _fixup_plot(0, 0);
+}
+
+//create the data for visual server
+
+PoolVector VoxelLightBaker::create_gi_probe_data() {
+ PoolVector data;
+
+ data.resize(16 + (8 + 1 + 1 + 1 + 1) * bake_cells.size()); //4 for header, rest for rest.
+
+ {
+ PoolVector::Write w = data.write();
+
+ uint32_t *w32 = (uint32_t *)w.ptr();
+
+ w32[0] = 0; //version
+ w32[1] = cell_subdiv; //subdiv
+ w32[2] = axis_cell_size[0];
+ w32[3] = axis_cell_size[1];
+ w32[4] = axis_cell_size[2];
+ w32[5] = bake_cells.size();
+ w32[6] = leaf_voxel_count;
+
+ int ofs = 16;
+
+ for (int i = 0; i < bake_cells.size(); i++) {
+ for (int j = 0; j < 8; j++) {
+ w32[ofs++] = bake_cells[i].children[j];
+ }
+
+ { //albedo
+ uint32_t rgba = uint32_t(CLAMP(bake_cells[i].albedo[0] * 255.0, 0, 255)) << 16;
+ rgba |= uint32_t(CLAMP(bake_cells[i].albedo[1] * 255.0, 0, 255)) << 8;
+ rgba |= uint32_t(CLAMP(bake_cells[i].albedo[2] * 255.0, 0, 255)) << 0;
+
+ w32[ofs++] = rgba;
+ }
+ { //emission
+
+ Vector3 e(bake_cells[i].emission[0], bake_cells[i].emission[1], bake_cells[i].emission[2]);
+ float l = e.length();
+ if (l > 0) {
+ e.normalize();
+ l = CLAMP(l / 8.0, 0, 1.0);
+ }
+
+ uint32_t em = uint32_t(CLAMP(e[0] * 255, 0, 255)) << 24;
+ em |= uint32_t(CLAMP(e[1] * 255, 0, 255)) << 16;
+ em |= uint32_t(CLAMP(e[2] * 255, 0, 255)) << 8;
+ em |= uint32_t(CLAMP(l * 255, 0, 255));
+
+ w32[ofs++] = em;
+ }
+
+ //w32[ofs++]=bake_cells[i].used_sides;
+ { //normal
+
+ Vector3 n(bake_cells[i].normal[0], bake_cells[i].normal[1], bake_cells[i].normal[2]);
+ n = n * Vector3(0.5, 0.5, 0.5) + Vector3(0.5, 0.5, 0.5);
+ uint32_t norm = 0;
+
+ norm |= uint32_t(CLAMP(n.x * 255.0, 0, 255)) << 16;
+ norm |= uint32_t(CLAMP(n.y * 255.0, 0, 255)) << 8;
+ norm |= uint32_t(CLAMP(n.z * 255.0, 0, 255)) << 0;
+
+ w32[ofs++] = norm;
+ }
+
+ {
+ uint16_t alpha = MIN(uint32_t(bake_cells[i].alpha * 65535.0), 65535);
+ uint16_t level = bake_cells[i].level;
+
+ w32[ofs++] = (uint32_t(level) << 16) | uint32_t(alpha);
+ }
+ }
+ }
+
+ return data;
+}
+
+void VoxelLightBaker::_debug_mesh(int p_idx, int p_level, const AABB &p_aabb, Ref &p_multimesh, int &idx, DebugMode p_mode) {
+ if (p_level == cell_subdiv - 1) {
+ Vector3 center = p_aabb.position + p_aabb.size * 0.5;
+ Transform xform;
+ xform.origin = center;
+ xform.basis.scale(p_aabb.size * 0.5);
+ p_multimesh->set_instance_transform(idx, xform);
+ Color col;
+ if (p_mode == DEBUG_ALBEDO) {
+ col = Color(bake_cells[p_idx].albedo[0], bake_cells[p_idx].albedo[1], bake_cells[p_idx].albedo[2]);
+ } else if (p_mode == DEBUG_LIGHT) {
+ for (int i = 0; i < 6; i++) {
+ col.r += bake_light[p_idx].accum[i][0];
+ col.g += bake_light[p_idx].accum[i][1];
+ col.b += bake_light[p_idx].accum[i][2];
+ col.r += bake_light[p_idx].direct_accum[i][0];
+ col.g += bake_light[p_idx].direct_accum[i][1];
+ col.b += bake_light[p_idx].direct_accum[i][2];
+ }
+ }
+ //Color col = Color(bake_cells[p_idx].emission[0], bake_cells[p_idx].emission[1], bake_cells[p_idx].emission[2]);
+ p_multimesh->set_instance_color(idx, col);
+
+ idx++;
+
+ } else {
+ for (int i = 0; i < 8; i++) {
+ uint32_t child = bake_cells[p_idx].children[i];
+
+ if (child == CHILD_EMPTY || child >= (uint32_t)max_original_cells) {
+ continue;
+ }
+
+ AABB aabb = p_aabb;
+ aabb.size *= 0.5;
+
+ if (i & 1) {
+ aabb.position.x += aabb.size.x;
+ }
+ if (i & 2) {
+ aabb.position.y += aabb.size.y;
+ }
+ if (i & 4) {
+ aabb.position.z += aabb.size.z;
+ }
+
+ _debug_mesh(bake_cells[p_idx].children[i], p_level + 1, aabb, p_multimesh, idx, p_mode);
+ }
+ }
+}
+
+Ref VoxelLightBaker::create_debug_multimesh(DebugMode p_mode) {
+ Ref mm;
+
+ ERR_FAIL_COND_V(p_mode == DEBUG_LIGHT && bake_light.size() == 0, mm);
+ mm.instance();
+
+ mm->set_transform_format(MultiMesh::TRANSFORM_3D);
+ mm->set_color_format(MultiMesh::COLOR_8BIT);
+ mm->set_instance_count(leaf_voxel_count);
+
+ Ref mesh;
+ mesh.instance();
+
+ {
+ Array arr;
+ arr.resize(Mesh::ARRAY_MAX);
+
+ PoolVector vertices;
+ PoolVector colors;
+#define ADD_VTX(m_idx) \
+ ; \
+ vertices.push_back(face_points[m_idx]); \
+ colors.push_back(Color(1, 1, 1, 1));
+
+ for (int i = 0; i < 6; i++) {
+ Vector3 face_points[4];
+
+ for (int j = 0; j < 4; j++) {
+ float v[3];
+ v[0] = 1.0;
+ v[1] = 1 - 2 * ((j >> 1) & 1);
+ v[2] = v[1] * (1 - 2 * (j & 1));
+
+ for (int k = 0; k < 3; k++) {
+ if (i < 3) {
+ face_points[j][(i + k) % 3] = v[k];
+ } else {
+ face_points[3 - j][(i + k) % 3] = -v[k];
+ }
+ }
+ }
+
+ //tri 1
+ ADD_VTX(0);
+ ADD_VTX(1);
+ ADD_VTX(2);
+ //tri 2
+ ADD_VTX(2);
+ ADD_VTX(3);
+ ADD_VTX(0);
+ }
+
+ arr[Mesh::ARRAY_VERTEX] = vertices;
+ arr[Mesh::ARRAY_COLOR] = colors;
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arr);
+ }
+
+ {
+ Ref fsm;
+ fsm.instance();
+ fsm->set_flag(SpatialMaterial::FLAG_SRGB_VERTEX_COLOR, true);
+ fsm->set_flag(SpatialMaterial::FLAG_ALBEDO_FROM_VERTEX_COLOR, true);
+ fsm->set_flag(SpatialMaterial::FLAG_UNSHADED, true);
+ fsm->set_albedo(Color(1, 1, 1, 1));
+
+ mesh->surface_set_material(0, fsm);
+ }
+
+ mm->set_mesh(mesh);
+
+ int idx = 0;
+ _debug_mesh(0, 0, po2_bounds, mm, idx, p_mode);
+
+ return mm;
+}
+
+struct VoxelLightBakerOctree {
+ enum {
+ CHILD_EMPTY = 0xFFFFFFFF
+ };
+
+ uint16_t light[6][3]; //anisotropic light
+ float alpha;
+ uint32_t children[8];
+};
+
+PoolVector VoxelLightBaker::create_capture_octree(int p_subdiv) {
+ p_subdiv = MIN(p_subdiv, cell_subdiv); // use the smaller one
+
+ Vector remap;
+ int bc = bake_cells.size();
+ remap.resize(bc);
+ Vector demap;
+
+ int new_size = 0;
+ for (int i = 0; i < bc; i++) {
+ uint32_t c = CHILD_EMPTY;
+ if (bake_cells[i].level < p_subdiv) {
+ c = new_size;
+ new_size++;
+ demap.push_back(i);
+ }
+ remap.write[i] = c;
+ }
+
+ Vector octree;
+ octree.resize(new_size);
+
+ for (int i = 0; i < new_size; i++) {
+ octree.write[i].alpha = bake_cells[demap[i]].alpha;
+ for (int j = 0; j < 6; j++) {
+ for (int k = 0; k < 3; k++) {
+ float l = bake_light[demap[i]].accum[j][k]; //add anisotropic light
+ l += bake_cells[demap[i]].emission[k]; //add emission
+ octree.write[i].light[j][k] = CLAMP(l * 1024, 0, 65535); //give two more bits to octree
+ }
+ }
+
+ for (int j = 0; j < 8; j++) {
+ uint32_t child = bake_cells[demap[i]].children[j];
+ octree.write[i].children[j] = child == CHILD_EMPTY ? CHILD_EMPTY : remap[child];
+ }
+ }
+
+ PoolVector ret;
+ int ret_bytes = octree.size() * sizeof(VoxelLightBakerOctree);
+ ret.resize(ret_bytes);
+ {
+ PoolVector::Write w = ret.write();
+ memcpy(w.ptr(), octree.ptr(), ret_bytes);
+ }
+
+ return ret;
+}
+
+float VoxelLightBaker::get_cell_size() const {
+ return cell_size;
+}
+
+Transform VoxelLightBaker::get_to_cell_space_xform() const {
+ return to_cell_space;
+}
+VoxelLightBaker::VoxelLightBaker() {
+ color_scan_cell_width = 4;
+ bake_texture_size = 128;
+ propagation = 0.85;
+}
diff --git a/scene/3d/voxel_light_baker.h b/scene/3d/voxel_light_baker.h
new file mode 100644
index 000000000..4aec5f5a8
--- /dev/null
+++ b/scene/3d/voxel_light_baker.h
@@ -0,0 +1,173 @@
+/**************************************************************************/
+/* voxel_light_baker.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef VOXEL_LIGHT_BAKER_H
+#define VOXEL_LIGHT_BAKER_H
+
+#include "scene/3d/mesh_instance.h"
+#include "scene/resources/mesh/multimesh.h"
+
+class Material;
+
+class VoxelLightBaker {
+public:
+ enum DebugMode {
+ DEBUG_ALBEDO,
+ DEBUG_LIGHT
+ };
+
+ enum BakeQuality {
+ BAKE_QUALITY_LOW,
+ BAKE_QUALITY_MEDIUM,
+ BAKE_QUALITY_HIGH
+ };
+
+ enum BakeMode {
+ BAKE_MODE_CONE_TRACE,
+ BAKE_MODE_RAY_TRACE,
+ };
+
+private:
+ enum {
+ CHILD_EMPTY = 0xFFFFFFFF
+
+ };
+
+ struct Cell {
+ uint32_t children[8];
+ float albedo[3]; //albedo in RGB24
+ float emission[3]; //accumulated light in 16:16 fixed point (needs to be integer for moving lights fast)
+ float normal[3];
+ uint32_t used_sides;
+ float alpha; //used for upsampling
+ int level;
+
+ Cell() {
+ for (int i = 0; i < 8; i++) {
+ children[i] = CHILD_EMPTY;
+ }
+
+ for (int i = 0; i < 3; i++) {
+ emission[i] = 0;
+ albedo[i] = 0;
+ normal[i] = 0;
+ }
+ alpha = 0;
+ used_sides = 0;
+ level = 0;
+ }
+ };
+
+ Vector] bake_cells;
+ int cell_subdiv;
+
+ struct Light {
+ int x, y, z;
+ float accum[6][3]; //rgb anisotropic
+ float direct_accum[6][3]; //for direct bake
+ int next_leaf;
+ Light() {
+ x = y = z = 0;
+ for (int i = 0; i < 6; i++) {
+ for (int j = 0; j < 3; j++) {
+ accum[i][j] = 0;
+ direct_accum[i][j] = 0;
+ }
+ }
+ next_leaf = 0;
+ }
+ };
+
+ int first_leaf;
+
+ Vector bake_light;
+
+ struct MaterialCache {
+ //128x128 textures
+ Vector albedo;
+ Vector emission;
+ };
+
+ RBMap[, MaterialCache> material_cache;
+ int leaf_voxel_count;
+ bool direct_lights_baked;
+
+ AABB original_bounds;
+ AABB po2_bounds;
+ int axis_cell_size[3];
+
+ Transform to_cell_space;
+
+ int color_scan_cell_width;
+ int bake_texture_size;
+ float cell_size;
+ float propagation;
+
+ BakeQuality bake_quality;
+
+ int max_original_cells;
+
+ void _init_light_plot(int p_idx, int p_level, int p_x, int p_y, int p_z, uint32_t p_parent);
+
+ Vector _get_bake_texture(Ref p_image, const Color &p_color_mul, const Color &p_color_add);
+ MaterialCache _get_material_cache(Ref p_material);
+
+ void _plot_face(int p_idx, int p_level, int p_x, int p_y, int p_z, const Vector3 *p_vtx, const Vector3 *p_normal, const Vector2 *p_uv, const MaterialCache &p_material, const AABB &p_aabb);
+ void _fixup_plot(int p_idx, int p_level);
+ void _debug_mesh(int p_idx, int p_level, const AABB &p_aabb, Ref &p_multimesh, int &idx, DebugMode p_mode);
+ void _check_init_light();
+
+ uint32_t _find_cell_at_pos(const Cell *cells, int x, int y, int z);
+
+public:
+ void begin_bake(int p_subdiv, const AABB &p_bounds);
+ void plot_mesh(const Transform &p_xform, Ref &p_mesh, const Vector][> &p_materials, const Ref &p_override_material);
+ void begin_bake_light(BakeQuality p_quality = BAKE_QUALITY_MEDIUM, float p_propagation = 0.85);
+ void plot_light_directional(const Vector3 &p_direction, const Color &p_color, float p_energy, float p_indirect_energy, bool p_direct);
+ void plot_light_omni(const Vector3 &p_pos, const Color &p_color, float p_energy, float p_indirect_energy, float p_radius, float p_attenutation, bool p_direct);
+ void plot_light_spot(const Vector3 &p_pos, const Vector3 &p_axis, const Color &p_color, float p_energy, float p_indirect_energy, float p_radius, float p_attenutation, float p_spot_angle, float p_spot_attenuation, bool p_direct);
+ void end_bake();
+
+ struct LightMapData {
+ int width;
+ int height;
+ PoolVector light;
+ };
+
+ PoolVector create_gi_probe_data();
+ Ref create_debug_multimesh(DebugMode p_mode = DEBUG_ALBEDO);
+ PoolVector create_capture_octree(int p_subdiv);
+
+ float get_cell_size() const;
+ Transform get_to_cell_space_xform() const;
+ VoxelLightBaker();
+};
+
+#endif // VOXEL_LIGHT_BAKER_H
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index 2fda29290..adcdea3fc 100644
--- a/scene/register_scene_types.cpp
+++ b/scene/register_scene_types.cpp
@@ -72,6 +72,7 @@
#include "scene/2d/y_sort.h"
#include "scene/3d/label_3d.h"
#include "scene/3d/world_environment_3d.h"
+#include "scene/3d/baked_lightmap.h"
#include "scene/animation/animation_blend_space_1d.h"
#include "scene/animation/animation_blend_space_2d.h"
#include "scene/animation/animation_blend_tree.h"
@@ -459,6 +460,8 @@ void register_scene_types() {
ClassDB::register_class();
ClassDB::register_class();
ClassDB::register_class();
+ ClassDB::register_class();
+ ClassDB::register_class();
ClassDB::register_class();
ClassDB::register_class();
ClassDB::register_class();
] |