chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user