diff --git a/doc/classes/LightmapGI.xml b/doc/classes/LightmapGI.xml
index e7d44411efd..c96bc41c068 100644
--- a/doc/classes/LightmapGI.xml
+++ b/doc/classes/LightmapGI.xml
@@ -14,6 +14,18 @@
 	
 		$DOCS_URL/tutorials/3d/global_illumination/using_lightmap_gi.html
 	
+	
+		
+			
+			
+			
+			
+				Bakes lightmaps (requires meshes to have UV2 unwrapped) for [param from_node] and its children to [param image_data_path]. [param image_data_path] must end with an [code].exr[/code] or [code].lmbake[/code] file extension. If [param from_node] is [code]null[/code], lightmaps are baked from the [LightmapGI] node's parent. Baking lightmaps can take from a few seconds to several dozen minutes depending on the GPU speed and quality settings chosen.
+				[b]Note:[/b] [method bake] only works within the editor, and when running a project from the editor. [method bake] will do nothing when called in a project exported in either debug or release mode. This limitation is in place to reduce the binary size of exported projects. You can [url=$DOCS_URL/contributing/development/compiling/index.html]compile custom export templates[/url] with the [code]module_lightmapper_rd_enabled=yes module_xatlas_unwrap_enabled=yes[/code] SCons options to remove this limitation.
+				[b]Additional Note:[/b] Baking lightmaps from a headless editor instance is not supported. If you attempt to bake lightmaps in this manner, the images returned will be null.
+			
+		
+	
 	
 		
 			The bias to use when computing shadows. Increasing [member bias] can fix shadow acne on the resulting baked lightmap, but can introduce peter-panning (shadows not connecting to their casters). Real-time [Light3D] shadows are not affected by this [member bias] property.
diff --git a/editor/plugins/lightmap_gi_editor_plugin.cpp b/editor/plugins/lightmap_gi_editor_plugin.cpp
index 1c17d99d0dc..df866c27134 100644
--- a/editor/plugins/lightmap_gi_editor_plugin.cpp
+++ b/editor/plugins/lightmap_gi_editor_plugin.cpp
@@ -66,9 +66,9 @@ void LightmapGIEditorPlugin::_bake_select_file(const String &p_file) {
 
 			if (err == LightmapGI::BAKE_ERROR_OK) {
 				if (get_tree()->get_edited_scene_root() == lightmap) {
-					err = lightmap->bake(lightmap, p_file, bake_func_step);
+					err = lightmap->_bake(lightmap, p_file, bake_func_step);
 				} else {
-					err = lightmap->bake(lightmap->get_parent(), p_file, bake_func_step);
+					err = lightmap->_bake(lightmap->get_parent(), p_file, bake_func_step);
 				}
 			}
 		} else {
diff --git a/modules/lightmapper_rd/config.py b/modules/lightmapper_rd/config.py
index ecc61c2d7eb..e7da6185475 100644
--- a/modules/lightmapper_rd/config.py
+++ b/modules/lightmapper_rd/config.py
@@ -1,5 +1,5 @@
 def can_build(env, platform):
-    return env.editor_build and platform not in ["android", "ios"]
+    return (env.editor_build or env["module_lightmapper_rd_enabled"]) and platform not in ["android", "ios"]
 
 
 def configure(env):
diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp
index 8ba6f9e2ba0..dcfa0d9252a 100644
--- a/modules/lightmapper_rd/lightmapper_rd.cpp
+++ b/modules/lightmapper_rd/lightmapper_rd.cpp
@@ -37,8 +37,10 @@
 #include "core/config/project_settings.h"
 #include "core/io/dir_access.h"
 #include "core/math/geometry_2d.h"
+#ifdef TOOLS_ENABLED
 #include "editor/editor_paths.h"
 #include "editor/editor_settings.h"
+#endif
 #include "servers/rendering/rendering_device_binds.h"
 
 #if defined(VULKAN_ENABLED)
@@ -881,6 +883,7 @@ Ref LightmapperRD::_read_pfm(const String &p_name) {
 	return img;
 }
 
+#ifdef TOOLS_ENABLED
 LightmapperRD::BakeError LightmapperRD::_denoise_oidn(RenderingDevice *p_rd, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, const String &p_exe) {
 	Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
 
@@ -946,6 +949,7 @@ LightmapperRD::BakeError LightmapperRD::_denoise_oidn(RenderingDevice *p_rd, RID
 	}
 	return BAKE_OK;
 }
+#endif
 
 LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref &p_compute_shader, const RID &p_compute_base_uniform_set, PushConstant &p_push_constant, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, float p_denoiser_strength, int p_denoiser_range, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, BakeStepFunc p_step_function, void *p_bake_userdata) {
 	RID denoise_params_buffer = p_rd->uniform_buffer_create(sizeof(DenoiseParams));
@@ -1024,22 +1028,28 @@ LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref &p_environment_panorama, const Basis &p_environment_transform, BakeStepFunc p_step_function, void *p_bake_userdata, float p_exposure_normalization) {
+#ifdef TOOLS_ENABLED
 	int denoiser = GLOBAL_GET("rendering/lightmapping/denoising/denoiser");
-	String oidn_path = EDITOR_GET("filesystem/tools/oidn/oidn_denoise_path");
-
-	if (p_use_denoiser && denoiser == 1) {
-		// OIDN (external).
-		Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
-
-		if (da->dir_exists(oidn_path)) {
-			if (OS::get_singleton()->get_name() == "Windows") {
-				oidn_path = oidn_path.path_join("oidnDenoise.exe");
-			} else {
-				oidn_path = oidn_path.path_join("oidnDenoise");
+	// TODO: Implement oidnDenoise for non-editor
+	String oidn_path;
+	if (Engine::get_singleton()->is_editor_hint()) {
+		oidn_path = p_use_denoiser ? EDITOR_GET("filesystem/tools/oidn/oidn_denoise_path") : Variant();
+
+		if (p_use_denoiser && denoiser == 1) {
+			// OIDN (external).
+			Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+
+			if (da->dir_exists(oidn_path)) {
+				if (OS::get_singleton()->get_name() == "Windows") {
+					oidn_path = oidn_path.path_join("oidnDenoise.exe");
+				} else {
+					oidn_path = oidn_path.path_join("oidnDenoise");
+				}
 			}
+			ERR_FAIL_COND_V_MSG(oidn_path.is_empty() || !da->file_exists(oidn_path), BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES, "OIDN denoiser is selected in the project settings, but no or invalid OIDN executable path is configured in the editor settings.");
 		}
-		ERR_FAIL_COND_V_MSG(oidn_path.is_empty() || !da->file_exists(oidn_path), BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES, "OIDN denoiser is selected in the project settings, but no or invalid OIDN executable path is configured in the editor settings.");
 	}
+#endif
 
 	if (p_step_function) {
 		p_step_function(0.0, RTR("Begin Bake"), p_bake_userdata, true);
@@ -1849,6 +1859,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
 
 		{
 			BakeError error;
+#ifdef TOOLS_ENABLED
 			if (denoiser == 1) {
 				// OIDN (external).
 				error = _denoise_oidn(rd, light_accum_tex, normal_tex, light_accum_tex, atlas_size, atlas_slices, p_bake_sh, oidn_path);
@@ -1857,6 +1868,10 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
 				SWAP(light_accum_tex, light_accum_tex2);
 				error = _denoise(rd, compute_shader, compute_base_uniform_set, push_constant, light_accum_tex2, normal_tex, light_accum_tex, p_denoiser_strength, p_denoiser_range, atlas_size, atlas_slices, p_bake_sh, p_step_function, p_bake_userdata);
 			}
+#else
+			SWAP(light_accum_tex, light_accum_tex2);
+			error = _denoise(rd, compute_shader, compute_base_uniform_set, push_constant, light_accum_tex2, normal_tex, light_accum_tex, p_denoiser_strength, p_denoiser_range, atlas_size, atlas_slices, p_bake_sh, p_step_function, p_bake_userdata);
+#endif
 			if (unlikely(error != BAKE_OK)) {
 				return error;
 			}
diff --git a/modules/xatlas_unwrap/config.py b/modules/xatlas_unwrap/config.py
index ecc61c2d7eb..9e8eeca0e8c 100644
--- a/modules/xatlas_unwrap/config.py
+++ b/modules/xatlas_unwrap/config.py
@@ -1,5 +1,5 @@
 def can_build(env, platform):
-    return env.editor_build and platform not in ["android", "ios"]
+    return (env.editor_build or env["module_xatlas_unwrap_enabled"]) and platform not in ["android", "ios"]
 
 
 def configure(env):
diff --git a/scene/3d/lightmap_gi.cpp b/scene/3d/lightmap_gi.cpp
index 26a574cd26c..a22096383a1 100644
--- a/scene/3d/lightmap_gi.cpp
+++ b/scene/3d/lightmap_gi.cpp
@@ -30,6 +30,7 @@
 
 #include "lightmap_gi.h"
 
+#include "core/config/engine.h"
 #include "core/config/project_settings.h"
 #include "core/io/config_file.h"
 #include "core/math/delaunay_3d.h"
@@ -39,6 +40,7 @@
 #include "scene/resources/environment.h"
 #include "scene/resources/image_texture.h"
 #include "scene/resources/sky.h"
+#include "scene/resources/texture.h"
 
 void LightmapGIData::add_user(const NodePath &p_path, const Rect2 &p_uv_scale, int p_slice_index, int32_t p_sub_instance) {
 	User user;
@@ -740,7 +742,17 @@ void LightmapGI::_gen_new_positions_from_octree(const GenProbesOctree *p_cell, f
 	}
 }
 
-LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_path, Lightmapper::BakeStepFunc p_bake_step, void *p_bake_userdata) {
+bool LightmapGI::_dummy_bake_func_step(float p_progress, const String &p_description, void *, bool p_refresh) {
+	// No reporting needed, but baking logic is identical
+	return true;
+}
+
+LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_path) {
+	// is dummy bake func needed?
+	return _bake(p_from_node, p_image_data_path, _dummy_bake_func_step, nullptr);
+}
+
+LightmapGI::BakeError LightmapGI::_bake(Node *p_from_node, String p_image_data_path, Lightmapper::BakeStepFunc p_bake_step, void *p_bake_userdata) {
 	if (p_image_data_path.is_empty()) {
 		if (get_light_data().is_null()) {
 			return BAKE_ERROR_NO_SAVE_PATH;
@@ -1083,14 +1095,34 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa
 					}
 
 					if (env.is_valid()) {
-						environment_image = RS::get_singleton()->environment_bake_panorama(env->get_rid(), true, Size2i(128, 64));
 						environment_transform = Basis::from_euler(env->get_sky_rotation()).inverse();
+
+						Sky::RadianceSize old_radiance_size = Sky::RADIANCE_SIZE_MAX;
+						if (!Engine::get_singleton()->is_editor_hint()) {
+							Ref sky = env->get_sky();
+							if (sky.is_valid()) {
+								old_radiance_size = sky->get_radiance_size();
+								sky->set_radiance_size(Sky::RADIANCE_SIZE_128);
+							}
+						}
+						environment_image = RS::get_singleton()->environment_bake_panorama(env->get_rid(), true, Size2i(128, 128));
+						if (old_radiance_size != Sky::RADIANCE_SIZE_MAX) { // If it's not max, it's been set and needs resetting
+							Ref sky = env->get_sky();
+							if (sky.is_valid()) {
+								sky->set_radiance_size(old_radiance_size);
+							}
+						}
 					}
 				}
 			} break;
 			case ENVIRONMENT_MODE_CUSTOM_SKY: {
 				if (environment_custom_sky.is_valid()) {
-					environment_image = RS::get_singleton()->sky_bake_panorama(environment_custom_sky->get_rid(), environment_custom_energy, true, Size2i(128, 64));
+					Sky::RadianceSize old_radiance_size = environment_custom_sky->get_radiance_size();
+					if (!Engine::get_singleton()->is_editor_hint()) {
+						environment_custom_sky->set_radiance_size(Sky::RADIANCE_SIZE_128);
+					}
+					environment_image = RS::get_singleton()->sky_bake_panorama(environment_custom_sky->get_rid(), environment_custom_energy, true, Size2i(128, 128));
+					environment_custom_sky->set_radiance_size(old_radiance_size);
 				}
 
 			} break;
@@ -1156,34 +1188,42 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa
 				texture_image->blit_rect(images[i * slices_per_texture + j], Rect2i(0, 0, slice_width, slice_height), Point2i(0, slice_height * j));
 			}
 
-			String texture_path = texture_count > 1 ? base_path + "_" + itos(i) + ".exr" : base_path + ".exr";
-
-			Ref config;
-			config.instantiate();
-
-			if (FileAccess::exists(texture_path + ".import")) {
-				config->load(texture_path + ".import");
-			}
+			if (Engine::get_singleton()->is_editor_hint()) {
+				String texture_path = texture_count > 1 ? base_path + "_" + itos(i) + ".exr" : base_path + ".exr";
+				Ref config;
+				config.instantiate();
 
-			config->set_value("remap", "importer", "2d_array_texture");
-			config->set_value("remap", "type", "CompressedTexture2DArray");
-			if (!config->has_section_key("params", "compress/mode")) {
-				// User may want another compression, so leave it be, but default to VRAM uncompressed.
-				config->set_value("params", "compress/mode", 3);
+				config->set_value("remap", "importer", "2d_array_texture");
+				config->set_value("remap", "type", "CompressedTexture2DArray");
+				if (!config->has_section_key("params", "compress/mode")) {
+					// User may want another compression, so leave it be, but default to VRAM uncompressed.
+					config->set_value("params", "compress/mode", 3);
+				}
+				config->set_value("params", "compress/channel_pack", 1);
+				config->set_value("params", "mipmaps/generate", false);
+				config->set_value("params", "slices/horizontal", 1);
+				config->set_value("params", "slices/vertical", texture_slice_count);
+
+				config->save(texture_path + ".import");
+
+				Error err = texture_image->save_exr(texture_path, false);
+				ERR_FAIL_COND_V(err, BAKE_ERROR_CANT_CREATE_IMAGE);
+				ResourceLoader::import(texture_path);
+				Ref t = ResourceLoader::load(texture_path); // If already loaded, it will be updated on refocus?
+				ERR_FAIL_COND_V(t.is_null(), BAKE_ERROR_CANT_CREATE_IMAGE);
+				textures[i] = t;
+			} else {
+				String texture_path = texture_count > 1 ? base_path + "_" + itos(i) + ".res" : base_path + ".res";
+				Ref texs;
+				texs.instantiate();
+				texs->create_from_images(images);
+
+				Error err = ResourceSaver::save(texs, texture_path);
+				ERR_FAIL_COND_V(err, BAKE_ERROR_CANT_CREATE_IMAGE);
+				Ref t = ResourceLoader::load(texture_path);
+				ERR_FAIL_COND_V(t.is_null(), BAKE_ERROR_CANT_CREATE_IMAGE);
+				textures[i] = t;
 			}
-			config->set_value("params", "compress/channel_pack", 1);
-			config->set_value("params", "mipmaps/generate", false);
-			config->set_value("params", "slices/horizontal", 1);
-			config->set_value("params", "slices/vertical", texture_slice_count);
-
-			config->save(texture_path + ".import");
-
-			Error err = texture_image->save_exr(texture_path, false);
-			ERR_FAIL_COND_V(err, BAKE_ERROR_CANT_CREATE_IMAGE);
-			ResourceLoader::import(texture_path);
-			Ref t = ResourceLoader::load(texture_path); // If already loaded, it will be updated on refocus?
-			ERR_FAIL_COND_V(t.is_null(), BAKE_ERROR_CANT_CREATE_IMAGE);
-			textures[i] = t;
 		}
 	}
 
@@ -1686,7 +1726,7 @@ void LightmapGI::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_camera_attributes", "camera_attributes"), &LightmapGI::set_camera_attributes);
 	ClassDB::bind_method(D_METHOD("get_camera_attributes"), &LightmapGI::get_camera_attributes);
 
-	//	ClassDB::bind_method(D_METHOD("bake", "from_node"), &LightmapGI::bake, DEFVAL(Variant()));
+	ClassDB::bind_method(D_METHOD("bake", "from_node", "image_data_path"), &LightmapGI::bake, DEFVAL(""));
 
 	ADD_GROUP("Tweaks", "");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "quality", PROPERTY_HINT_ENUM, "Low,Medium,High,Ultra"), "set_bake_quality", "get_bake_quality");
diff --git a/scene/3d/lightmap_gi.h b/scene/3d/lightmap_gi.h
index 6377c420d16..bb39cda88dd 100644
--- a/scene/3d/lightmap_gi.h
+++ b/scene/3d/lightmap_gi.h
@@ -310,7 +310,11 @@ class LightmapGI : public VisualInstance3D {
 
 	AABB get_aabb() const override;
 
-	BakeError bake(Node *p_from_node, String p_image_data_path = "", Lightmapper::BakeStepFunc p_bake_step = nullptr, void *p_bake_userdata = nullptr);
+	static bool _dummy_bake_func_step(float p_progress, const String &p_description, void *, bool p_refresh);
+
+	BakeError bake(Node *p_from_node, String p_image_data_path = "");
+
+	BakeError _bake(Node *p_from_node, String p_image_data_path = "", Lightmapper::BakeStepFunc p_bake_step = nullptr, void *p_bake_userdata = nullptr);
 
 	virtual PackedStringArray get_configuration_warnings() const override;