deea7d2a56
Addresses review feedback on the Unity 6.5 GetInstanceID migration: - UnityObjectIdCompatExtensions: drop the reflective method lookup in favor of a simple #if UNITY_6000_5_OR_NEWER / #else split calling GetEntityId() or GetInstanceID() directly. Also wrap the class in the MCPForUnity.Runtime.Helpers namespace. - UnityTypeConverters: remove the reflective EntityIdToObject probe and call EditorUtility.EntityIdToObject(EntityId) directly under the same version gate. Serialize entityID as EntityId.ToULong() rather than ToString(), since Unity's docs explicitly warn that the textual form is not a stable serialization contract. - Drop #pragma warning disable 0619 from 22 files that no longer make any direct calls to obsolete APIs. The remaining 7 files still need it (FindObjectsOfType, InstanceIDToObject fallback) and are left as-is — those deprecations are out of scope for this PR. - Add the MCPForUnity.Runtime.Helpers using to every file that calls GetInstanceIDCompat() now that the extension method lives in a namespace.
586 lines
22 KiB
C#
586 lines
22 KiB
C#
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<ReflectionProbe>();
|
|
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<string, object>
|
|
{
|
|
["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<string>();
|
|
var failed = new List<string>();
|
|
|
|
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<float>(), posToken[1].Value<float>(), posToken[2].Value<float>())
|
|
: Vector3.zero;
|
|
|
|
var gridToken = p.GetRaw("grid_size") as JArray;
|
|
int gridX = gridToken != null && gridToken.Count >= 1 ? gridToken[0].Value<int>() : 3;
|
|
int gridY = gridToken != null && gridToken.Count >= 2 ? gridToken[1].Value<int>() : 2;
|
|
int gridZ = gridToken != null && gridToken.Count >= 3 ? gridToken[2].Value<int>() : 3;
|
|
|
|
var go = new GameObject(name);
|
|
go.transform.position = position;
|
|
Undo.RegisterCreatedObjectUndo(go, $"Create Light Probe Group '{name}'");
|
|
|
|
var probeGroup = go.AddComponent<LightProbeGroup>();
|
|
|
|
var positions = new List<Vector3>();
|
|
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<float>(), posToken[1].Value<float>(), posToken[2].Value<float>())
|
|
: Vector3.zero;
|
|
|
|
var sizeToken = p.GetRaw("size") as JArray;
|
|
Vector3 size = sizeToken != null && sizeToken.Count >= 3
|
|
? new Vector3(sizeToken[0].Value<float>(), sizeToken[1].Value<float>(), sizeToken[2].Value<float>())
|
|
: new Vector3(10f, 10f, 10f);
|
|
|
|
if (!Enum.TryParse<ReflectionProbeMode>(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<ReflectionProbe>();
|
|
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<LightProbeGroup>();
|
|
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<float>(),
|
|
arr[1].Value<float>(),
|
|
arr[2].Value<float>()
|
|
);
|
|
}
|
|
|
|
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<string, object> 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<LightingSettings.Lightmapper>(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<MixedLightingMode>(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<LightmapCompression>(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<T>(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;
|
|
}
|
|
}
|
|
}
|