From f297ccbc7df6ab987755b93457235769cebf33b2 Mon Sep 17 00:00:00 2001 From: Relintai Date: Mon, 15 Jul 2024 21:20:48 +0200 Subject: [PATCH] Added back the scene side lighmapper classes. --- doc/classes/BakedLightmap.xml | 151 ++ doc/classes/BakedLightmapData.xml | 67 + editor/editor_node.cpp | 2 + .../plugins/baked_lightmap_editor_plugin.cpp | 179 ++ editor/plugins/baked_lightmap_editor_plugin.h | 74 + editor/plugins/spatial_editor_plugin.cpp | 1 + editor/spatial_editor_gizmos.cpp | 130 ++ editor/spatial_editor_gizmos.h | 17 + scene/3d/baked_lightmap.cpp | 1668 +++++++++++++++++ scene/3d/baked_lightmap.h | 292 +++ scene/3d/lightmapper.cpp | 76 + scene/3d/lightmapper.h | 197 ++ scene/3d/voxel_light_baker.cpp | 1600 ++++++++++++++++ scene/3d/voxel_light_baker.h | 173 ++ scene/register_scene_types.cpp | 3 + 15 files changed, 4630 insertions(+) create mode 100644 doc/classes/BakedLightmap.xml create mode 100644 doc/classes/BakedLightmapData.xml create mode 100644 editor/plugins/baked_lightmap_editor_plugin.cpp create mode 100644 editor/plugins/baked_lightmap_editor_plugin.h create mode 100644 scene/3d/baked_lightmap.cpp create mode 100644 scene/3d/baked_lightmap.h create mode 100644 scene/3d/lightmapper.cpp create mode 100644 scene/3d/lightmapper.h create mode 100644 scene/3d/voxel_light_baker.cpp create mode 100644 scene/3d/voxel_light_baker.h 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();