using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; using MCPForUnity.Runtime.Helpers; namespace MCPForUnity.Editor.Tools.Graphics { internal static class LightBakingOps { // === bake_start === // Params: async (bool, default true) internal static object StartBake(JObject @params) { if (Application.isPlaying) return new ErrorResponse("Light baking requires Edit mode."); var p = new ToolParams(@params); bool async_ = p.GetBool("async", true); if (async_) { Lightmapping.BakeAsync(); return new PendingResponse( "Light bake started (async). Use bake_status to check progress.", pollIntervalSeconds: 2.0, data: new { mode = "async" } ); } Lightmapping.Bake(); return new { success = true, message = "Light bake completed (synchronous).", data = new { mode = "sync", lightmapCount = LightmapSettings.lightmaps.Length } }; } // === bake_cancel === internal static object CancelBake(JObject @params) { Lightmapping.Cancel(); return new { success = true, message = "Light bake cancelled." }; } // === bake_get_status === internal static object GetStatus(JObject @params) { bool running = Lightmapping.isRunning; return new { success = true, message = running ? "Light bake in progress." : "No bake running.", data = new { isRunning = running, bakedGI = Lightmapping.bakedGI, realtimeGI = Lightmapping.realtimeGI, lightmapCount = LightmapSettings.lightmaps.Length } }; } // === bake_clear === internal static object ClearBake(JObject @params) { Lightmapping.Clear(); Lightmapping.ClearLightingDataAsset(); return new { success = true, message = "Cleared all baked lighting data and lighting data asset." }; } // === bake_reflection_probe === // Params: target (name or instanceID of GameObject with ReflectionProbe) internal static object BakeReflectionProbe(JObject @params) { if (Application.isPlaying) return new ErrorResponse("Reflection probe baking requires Edit mode."); var p = new ToolParams(@params); string target = p.Get("target"); if (string.IsNullOrEmpty(target)) return new ErrorResponse("'target' parameter is required (name or instanceID of a GameObject with ReflectionProbe)."); var go = FindGameObject(target); if (go == null) return new ErrorResponse($"GameObject '{target}' not found."); var probe = go.GetComponent(); if (probe == null) return new ErrorResponse($"GameObject '{go.name}' does not have a ReflectionProbe component."); string dir = "Assets/Lightmaps"; if (!AssetDatabase.IsValidFolder(dir)) AssetDatabase.CreateFolder("Assets", "Lightmaps"); string outputPath = $"{dir}/{probe.name}_ReflectionProbe.exr"; bool result = Lightmapping.BakeReflectionProbe(probe, outputPath); if (!result) return new ErrorResponse($"Failed to bake reflection probe '{probe.name}'."); return new { success = true, message = $"Baked reflection probe '{probe.name}' to '{outputPath}'.", data = new { probeName = probe.name, outputPath, instanceID = go.GetInstanceIDCompat() } }; } // === bake_get_settings === internal static object GetSettings(JObject @params) { var settings = EnsureLightingSettings(); if (settings == null) return new ErrorResponse( "Failed to create LightingSettings. Open Window > Rendering > Lighting manually."); var data = new Dictionary { ["name"] = settings.name, ["path"] = AssetDatabase.GetAssetPath(settings), ["bakedGI"] = settings.bakedGI, ["realtimeGI"] = settings.realtimeGI, ["lightmapper"] = settings.lightmapper.ToString(), ["lightmapResolution"] = settings.lightmapResolution, ["lightmapMaxSize"] = settings.lightmapMaxSize, ["directSampleCount"] = settings.directSampleCount, ["indirectSampleCount"] = settings.indirectSampleCount, ["environmentSampleCount"] = settings.environmentSampleCount, ["mixedBakeMode"] = settings.mixedBakeMode.ToString(), ["lightmapCompression"] = settings.lightmapCompression.ToString(), ["ao"] = settings.ao, ["aoMaxDistance"] = settings.aoMaxDistance }; // bounceCount vs maxBounces — name varies by Unity version ReadBounceCount(settings, data); return new { success = true, message = $"Lighting settings: {settings.lightmapper}, resolution {settings.lightmapResolution}.", data }; } // === bake_set_settings === // Params: settings (dict of property name -> value) internal static object SetSettings(JObject @params) { var p = new ToolParams(@params); var settingsToken = p.GetRaw("settings") as JObject; if (settingsToken == null || !settingsToken.HasValues) return new ErrorResponse("'settings' parameter is required (dict of property name to value)."); var lightingSettings = EnsureLightingSettings(); if (lightingSettings == null) return new ErrorResponse( "Failed to create LightingSettings. Open Window > Rendering > Lighting manually."); Undo.RecordObject(lightingSettings, "Modify Lighting Settings"); var changed = new List(); var failed = new List(); foreach (var prop in settingsToken.Properties()) { string name = prop.Name; JToken value = prop.Value; try { if (TrySetLightingSetting(lightingSettings, name, value)) changed.Add(name); else failed.Add(name); } catch (Exception ex) { McpLog.Warn($"[LightBakingOps] Failed to set '{name}': {ex.Message}"); failed.Add(name); } } if (changed.Count == 0 && failed.Count > 0) return new ErrorResponse($"Failed to set any settings. Invalid properties: {string.Join(", ", failed)}"); EditorUtility.SetDirty(lightingSettings); var msg = $"Updated {changed.Count} lighting setting(s)"; if (failed.Count > 0) msg += $". Failed: {string.Join(", ", failed)}"; return new { success = true, message = msg, data = new { changed, failed } }; } // === bake_create_light_probe_group === // Params: name, position, grid_size, spacing internal static object CreateLightProbeGroup(JObject @params) { var p = new ToolParams(@params); string name = p.Get("name") ?? "Light Probes"; float spacing = p.GetFloat("spacing") ?? 2.0f; var posToken = p.GetRaw("position") as JArray; Vector3 position = posToken != null && posToken.Count >= 3 ? new Vector3(posToken[0].Value(), posToken[1].Value(), posToken[2].Value()) : Vector3.zero; var gridToken = p.GetRaw("grid_size") as JArray; int gridX = gridToken != null && gridToken.Count >= 1 ? gridToken[0].Value() : 3; int gridY = gridToken != null && gridToken.Count >= 2 ? gridToken[1].Value() : 2; int gridZ = gridToken != null && gridToken.Count >= 3 ? gridToken[2].Value() : 3; var go = new GameObject(name); go.transform.position = position; Undo.RegisterCreatedObjectUndo(go, $"Create Light Probe Group '{name}'"); var probeGroup = go.AddComponent(); var positions = new List(); float halfX = (gridX - 1) * spacing * 0.5f; float halfY = (gridY - 1) * spacing * 0.5f; float halfZ = (gridZ - 1) * spacing * 0.5f; for (int x = 0; x < gridX; x++) { for (int y = 0; y < gridY; y++) { for (int z = 0; z < gridZ; z++) { positions.Add(new Vector3( x * spacing - halfX, y * spacing - halfY, z * spacing - halfZ )); } } } probeGroup.probePositions = positions.ToArray(); GraphicsHelpers.MarkDirty(probeGroup); return new { success = true, message = $"Created Light Probe Group '{name}' with {positions.Count} probes ({gridX}x{gridY}x{gridZ} grid, spacing {spacing}).", data = new { instanceID = go.GetInstanceIDCompat(), probeCount = positions.Count, gridSize = new[] { gridX, gridY, gridZ }, spacing, position = new[] { position.x, position.y, position.z } } }; } // === bake_create_reflection_probe === // Params: name, position, size, resolution, mode, hdr, box_projection internal static object CreateReflectionProbe(JObject @params) { var p = new ToolParams(@params); string name = p.Get("name") ?? "Reflection Probe"; int resolution = p.GetInt("resolution") ?? 256; bool hdr = p.GetBool("hdr", true); bool boxProjection = p.GetBool("box_projection", false); string modeStr = p.Get("mode") ?? "Baked"; var posToken = p.GetRaw("position") as JArray; Vector3 position = posToken != null && posToken.Count >= 3 ? new Vector3(posToken[0].Value(), posToken[1].Value(), posToken[2].Value()) : Vector3.zero; var sizeToken = p.GetRaw("size") as JArray; Vector3 size = sizeToken != null && sizeToken.Count >= 3 ? new Vector3(sizeToken[0].Value(), sizeToken[1].Value(), sizeToken[2].Value()) : new Vector3(10f, 10f, 10f); if (!Enum.TryParse(modeStr, true, out var mode)) return new ErrorResponse( $"Invalid mode '{modeStr}'. Valid values: Baked, Realtime, Custom."); var go = new GameObject(name); go.transform.position = position; Undo.RegisterCreatedObjectUndo(go, $"Create Reflection Probe '{name}'"); var probe = go.AddComponent(); probe.size = size; probe.resolution = resolution; probe.mode = mode; probe.hdr = hdr; probe.boxProjection = boxProjection; GraphicsHelpers.MarkDirty(probe); return new { success = true, message = $"Created Reflection Probe '{name}' (mode: {mode}, resolution: {resolution}, HDR: {hdr}).", data = new { instanceID = go.GetInstanceIDCompat(), mode = mode.ToString(), resolution, hdr, boxProjection, size = new[] { size.x, size.y, size.z }, position = new[] { position.x, position.y, position.z } } }; } // === bake_set_probe_positions === // Params: target (name/instanceID), positions (array of [x,y,z]) internal static object SetProbePositions(JObject @params) { var p = new ToolParams(@params); string target = p.Get("target"); if (string.IsNullOrEmpty(target)) return new ErrorResponse("'target' parameter is required (name or instanceID of a GameObject with LightProbeGroup)."); var go = FindGameObject(target); if (go == null) return new ErrorResponse($"GameObject '{target}' not found."); var probeGroup = go.GetComponent(); if (probeGroup == null) return new ErrorResponse($"GameObject '{go.name}' does not have a LightProbeGroup component."); var positionsToken = p.GetRaw("positions") as JArray; if (positionsToken == null || positionsToken.Count == 0) return new ErrorResponse("'positions' parameter is required (array of [x,y,z] arrays)."); Undo.RecordObject(probeGroup, "Set Light Probe Positions"); var positions = new Vector3[positionsToken.Count]; for (int i = 0; i < positionsToken.Count; i++) { var arr = positionsToken[i] as JArray; if (arr == null || arr.Count < 3) return new ErrorResponse($"Position at index {i} must be an array of [x, y, z]."); positions[i] = new Vector3( arr[0].Value(), arr[1].Value(), arr[2].Value() ); } probeGroup.probePositions = positions; GraphicsHelpers.MarkDirty(probeGroup); return new { success = true, message = $"Set {positions.Length} probe positions on '{go.name}'.", data = new { instanceID = go.GetInstanceIDCompat(), probeCount = positions.Length } }; } // --- Helper: Ensure a LightingSettings asset exists --- private static LightingSettings EnsureLightingSettings() { try { var settings = Lightmapping.lightingSettings; if (settings != null) return settings; } catch { /* getter throws when no asset exists */ } try { var settings = new LightingSettings { name = "LightingSettings" }; Lightmapping.lightingSettings = settings; return Lightmapping.lightingSettings; } catch { return null; } } // --- Helper: Find a GameObject by name or instanceID --- private static GameObject FindGameObject(string target) { if (string.IsNullOrEmpty(target)) return null; if (int.TryParse(target, out int instanceId)) { var byId = GameObjectLookup.ResolveInstanceID(instanceId) as GameObject; if (byId != null) return byId; } return GameObject.Find(target); } // --- Helper: Read bounceCount with version fallback --- private static void ReadBounceCount(LightingSettings settings, Dictionary data) { var type = typeof(LightingSettings); // Try bounceCount first (Unity 2022+) var prop = type.GetProperty("bounceCount", BindingFlags.Public | BindingFlags.Instance); if (prop != null) { data["bounceCount"] = prop.GetValue(settings); return; } // Fallback to maxBounces (older Unity versions) prop = type.GetProperty("maxBounces", BindingFlags.Public | BindingFlags.Instance); if (prop != null) data["maxBounces"] = prop.GetValue(settings); } // --- Helper: Set a single lighting setting by name --- private static bool TrySetLightingSetting(LightingSettings settings, string name, JToken value) { switch (name.ToLowerInvariant()) { case "bakedgi": case "baked_gi": settings.bakedGI = ParamCoercion.CoerceBool(value, settings.bakedGI); return true; case "realtimegi": case "realtime_gi": settings.realtimeGI = ParamCoercion.CoerceBool(value, settings.realtimeGI); return true; case "lightmapper": if (TryParseEnum(value, out var lm)) { settings.lightmapper = lm; return true; } return false; case "lightmapresolution": case "lightmap_resolution": settings.lightmapResolution = ParamCoercion.CoerceFloat(value, settings.lightmapResolution); return true; case "lightmapmaxsize": case "lightmap_max_size": settings.lightmapMaxSize = ParamCoercion.CoerceInt(value, settings.lightmapMaxSize); return true; case "directsamplecount": case "direct_sample_count": settings.directSampleCount = ParamCoercion.CoerceInt(value, settings.directSampleCount); return true; case "indirectsamplecount": case "indirect_sample_count": settings.indirectSampleCount = ParamCoercion.CoerceInt(value, settings.indirectSampleCount); return true; case "environmentsamplecount": case "environment_sample_count": settings.environmentSampleCount = ParamCoercion.CoerceInt(value, settings.environmentSampleCount); return true; case "bouncecount": case "bounce_count": case "maxbounces": case "max_bounces": return TrySetBounceCount(settings, ParamCoercion.CoerceInt(value, 2)); case "mixedbakemode": case "mixed_bake_mode": if (TryParseEnum(value, out var mlm)) { settings.mixedBakeMode = mlm; return true; } return false; case "compresslightmaps": case "compress_lightmaps": case "lightmapcompression": case "lightmap_compression": var strVal = value?.ToString() ?? ""; if (System.Enum.TryParse(strVal, true, out var compression)) settings.lightmapCompression = compression; else if (bool.TryParse(strVal, out var boolVal)) settings.lightmapCompression = boolVal ? LightmapCompression.NormalQuality : LightmapCompression.None; else if (int.TryParse(strVal, out var intVal)) settings.lightmapCompression = (LightmapCompression)intVal; else return false; return true; case "ao": settings.ao = ParamCoercion.CoerceBool(value, settings.ao); return true; case "aomaxdistance": case "ao_max_distance": settings.aoMaxDistance = ParamCoercion.CoerceFloat(value, settings.aoMaxDistance); return true; default: return false; } } // --- Helper: Set bounceCount with version fallback --- private static bool TrySetBounceCount(LightingSettings settings, int value) { var type = typeof(LightingSettings); // Try bounceCount first (Unity 2022+) var prop = type.GetProperty("bounceCount", BindingFlags.Public | BindingFlags.Instance); if (prop != null && prop.CanWrite) { prop.SetValue(settings, value); return true; } // Fallback to maxBounces (older Unity versions) prop = type.GetProperty("maxBounces", BindingFlags.Public | BindingFlags.Instance); if (prop != null && prop.CanWrite) { prop.SetValue(settings, value); return true; } return false; } // --- Helper: Parse enum from JToken (string name or int value) --- private static bool TryParseEnum(JToken value, out T result) where T : struct, Enum { result = default; if (value == null || value.Type == JTokenType.Null) return false; string str = value.ToString(); // Try parse by name if (Enum.TryParse(str, true, out result)) return true; // Try parse by int value if (int.TryParse(str, out int intVal)) { result = (T)Enum.ToObject(typeof(T), intVal); return true; } return false; } } }