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;
|
|
}
|
|
}
|
|
}
|