chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Graphics
|
||||
{
|
||||
internal static class GraphicsHelpers
|
||||
{
|
||||
private static bool? _hasVolumeSystem;
|
||||
private static Type _volumeType;
|
||||
private static Type _volumeProfileType;
|
||||
private static Type _volumeComponentType;
|
||||
private static Type _volumeParameterType;
|
||||
|
||||
internal static bool HasVolumeSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hasVolumeSystem == null) DetectPackages();
|
||||
return _hasVolumeSystem.Value;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool HasURP =>
|
||||
RenderPipelineUtility.GetActivePipeline() == RenderPipelineUtility.PipelineKind.Universal;
|
||||
|
||||
internal static bool HasHDRP =>
|
||||
RenderPipelineUtility.GetActivePipeline() == RenderPipelineUtility.PipelineKind.HighDefinition;
|
||||
|
||||
internal static Type VolumeType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hasVolumeSystem == null) DetectPackages();
|
||||
return _volumeType;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Type VolumeProfileType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hasVolumeSystem == null) DetectPackages();
|
||||
return _volumeProfileType;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Type VolumeComponentType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hasVolumeSystem == null) DetectPackages();
|
||||
return _volumeComponentType;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Type VolumeParameterType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hasVolumeSystem == null) DetectPackages();
|
||||
return _volumeParameterType;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DetectPackages()
|
||||
{
|
||||
_volumeType = Type.GetType("UnityEngine.Rendering.Volume, Unity.RenderPipelines.Core.Runtime");
|
||||
_volumeProfileType = Type.GetType("UnityEngine.Rendering.VolumeProfile, Unity.RenderPipelines.Core.Runtime");
|
||||
_volumeComponentType = Type.GetType("UnityEngine.Rendering.VolumeComponent, Unity.RenderPipelines.Core.Runtime");
|
||||
_volumeParameterType = Type.GetType("UnityEngine.Rendering.VolumeParameter, Unity.RenderPipelines.Core.Runtime");
|
||||
_hasVolumeSystem = _volumeType != null && _volumeProfileType != null;
|
||||
}
|
||||
|
||||
internal static Type ResolveVolumeComponentType(string effectName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(effectName) || VolumeComponentType == null)
|
||||
return null;
|
||||
|
||||
var derivedTypes = TypeCache.GetTypesDerivedFrom(VolumeComponentType);
|
||||
foreach (var t in derivedTypes)
|
||||
{
|
||||
if (t.IsAbstract) continue;
|
||||
if (string.Equals(t.Name, effectName, StringComparison.OrdinalIgnoreCase))
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static List<Type> GetAvailableEffectTypes()
|
||||
{
|
||||
if (VolumeComponentType == null)
|
||||
return new List<Type>();
|
||||
var derivedTypes = TypeCache.GetTypesDerivedFrom(VolumeComponentType);
|
||||
return derivedTypes
|
||||
.Where(t => !t.IsAbstract && !t.IsGenericType)
|
||||
.OrderBy(t => t.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
internal static Component FindVolume(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string target = p.Get("target");
|
||||
if (string.IsNullOrEmpty(target))
|
||||
{
|
||||
var allVolumes = UnityFindObjectsCompat.FindAll(VolumeType);
|
||||
return allVolumes.Length > 0 ? allVolumes[0] as Component : null;
|
||||
}
|
||||
|
||||
if (int.TryParse(target, out int instanceId))
|
||||
{
|
||||
var byId = GameObjectLookup.ResolveInstanceID(instanceId) as GameObject;
|
||||
if (byId != null) return byId.GetComponent(VolumeType);
|
||||
}
|
||||
|
||||
var go = GameObject.Find(target);
|
||||
if (go != null) return go.GetComponent(VolumeType);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static string GetPipelineName()
|
||||
{
|
||||
return RenderPipelineUtility.GetActivePipeline() switch
|
||||
{
|
||||
RenderPipelineUtility.PipelineKind.Universal => "Universal (URP)",
|
||||
RenderPipelineUtility.PipelineKind.HighDefinition => "High Definition (HDRP)",
|
||||
RenderPipelineUtility.PipelineKind.BuiltIn => "Built-in",
|
||||
RenderPipelineUtility.PipelineKind.Custom => "Custom",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
|
||||
internal static object ReadSerializedValue(SerializedProperty prop)
|
||||
{
|
||||
return prop.propertyType switch
|
||||
{
|
||||
SerializedPropertyType.Boolean => prop.boolValue,
|
||||
SerializedPropertyType.Integer => prop.type == "long" ? prop.longValue : (object)prop.intValue,
|
||||
SerializedPropertyType.Float => prop.floatValue,
|
||||
SerializedPropertyType.String => prop.stringValue,
|
||||
SerializedPropertyType.Enum => prop.enumValueIndex < prop.enumNames.Length
|
||||
? prop.enumNames[prop.enumValueIndex]
|
||||
: (object)prop.enumValueIndex,
|
||||
SerializedPropertyType.ObjectReference => prop.objectReferenceValue != null
|
||||
? (object)new
|
||||
{
|
||||
name = prop.objectReferenceValue.name,
|
||||
path = AssetDatabase.GetAssetPath(prop.objectReferenceValue)
|
||||
}
|
||||
: null,
|
||||
SerializedPropertyType.Color => new[] { prop.colorValue.r, prop.colorValue.g, prop.colorValue.b, prop.colorValue.a },
|
||||
SerializedPropertyType.Vector2 => new[] { prop.vector2Value.x, prop.vector2Value.y },
|
||||
SerializedPropertyType.Vector3 => new[] { prop.vector3Value.x, prop.vector3Value.y, prop.vector3Value.z },
|
||||
SerializedPropertyType.LayerMask => prop.intValue,
|
||||
_ => prop.propertyType.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
internal static bool SetSerializedValue(SerializedProperty prop, JToken value)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (prop.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.Boolean:
|
||||
prop.boolValue = ParamCoercion.CoerceBool(value, false);
|
||||
return true;
|
||||
case SerializedPropertyType.Integer:
|
||||
if (prop.type == "long")
|
||||
prop.longValue = ParamCoercion.CoerceLong(value, 0);
|
||||
else
|
||||
prop.intValue = ParamCoercion.CoerceInt(value, 0);
|
||||
return true;
|
||||
case SerializedPropertyType.Float:
|
||||
prop.floatValue = ParamCoercion.CoerceFloat(value, 0f);
|
||||
return true;
|
||||
case SerializedPropertyType.String:
|
||||
prop.stringValue = value.ToString();
|
||||
return true;
|
||||
case SerializedPropertyType.Enum:
|
||||
if (value.Type == JTokenType.String)
|
||||
{
|
||||
for (int i = 0; i < prop.enumNames.Length; i++)
|
||||
{
|
||||
if (string.Equals(prop.enumNames[i], value.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{ prop.enumValueIndex = i; return true; }
|
||||
}
|
||||
}
|
||||
prop.enumValueIndex = ParamCoercion.CoerceInt(value, 0);
|
||||
return true;
|
||||
case SerializedPropertyType.ObjectReference:
|
||||
if (value.Type == JTokenType.String)
|
||||
{
|
||||
string path = value.ToString();
|
||||
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
|
||||
if (asset != null) { prop.objectReferenceValue = asset; return true; }
|
||||
}
|
||||
else if (value.Type == JTokenType.Object)
|
||||
{
|
||||
string path = value["path"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
|
||||
if (asset != null) { prop.objectReferenceValue = asset; return true; }
|
||||
}
|
||||
}
|
||||
else if (value.Type == JTokenType.Null)
|
||||
{
|
||||
prop.objectReferenceValue = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case SerializedPropertyType.Color:
|
||||
if (value is JArray colorArr && colorArr.Count >= 3)
|
||||
{
|
||||
prop.colorValue = new Color(
|
||||
(float)colorArr[0], (float)colorArr[1], (float)colorArr[2],
|
||||
colorArr.Count >= 4 ? (float)colorArr[3] : 1f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case SerializedPropertyType.Vector2:
|
||||
if (value is JArray v2Arr && v2Arr.Count >= 2)
|
||||
{
|
||||
prop.vector2Value = new Vector2((float)v2Arr[0], (float)v2Arr[1]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case SerializedPropertyType.Vector3:
|
||||
if (value is JArray v3Arr && v3Arr.Count >= 3)
|
||||
{
|
||||
prop.vector3Value = new Vector3((float)v3Arr[0], (float)v3Arr[1], (float)v3Arr[2]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case SerializedPropertyType.LayerMask:
|
||||
prop.intValue = ParamCoercion.CoerceInt(value, 0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
internal static void MarkDirty(UnityEngine.Object obj)
|
||||
{
|
||||
if (obj == null) return;
|
||||
EditorUtility.SetDirty(obj);
|
||||
if (obj is Component comp)
|
||||
{
|
||||
var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if (prefabStage != null)
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||||
else
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(comp.gameObject.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99e4b1a4aa03465ea2d55e2794537155
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bf5a93fc3954f3e880e60794fa968de
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Graphics
|
||||
{
|
||||
[McpForUnityTool("manage_graphics", AutoRegister = false, Group = "core")]
|
||||
public static class ManageGraphics
|
||||
{
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
|
||||
var p = new ToolParams(@params);
|
||||
string action = p.Get("action")?.ToLowerInvariant();
|
||||
|
||||
if (string.IsNullOrEmpty(action))
|
||||
return new ErrorResponse("'action' parameter is required.");
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
// --- Health check ---
|
||||
case "ping":
|
||||
var pipeName = GraphicsHelpers.GetPipelineName();
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Graphics tool ready. Pipeline: {pipeName}",
|
||||
data = new
|
||||
{
|
||||
pipeline = RenderPipelineUtility.GetActivePipeline().ToString(),
|
||||
pipelineName = pipeName,
|
||||
hasVolumeSystem = GraphicsHelpers.HasVolumeSystem,
|
||||
hasURP = GraphicsHelpers.HasURP,
|
||||
hasHDRP = GraphicsHelpers.HasHDRP,
|
||||
availableEffects = GraphicsHelpers.HasVolumeSystem
|
||||
? GraphicsHelpers.GetAvailableEffectTypes().Count : 0
|
||||
}
|
||||
};
|
||||
|
||||
// --- Volume actions (require Volume system = URP or HDRP) ---
|
||||
case "volume_create":
|
||||
case "volume_add_effect":
|
||||
case "volume_set_effect":
|
||||
case "volume_remove_effect":
|
||||
case "volume_get_info":
|
||||
case "volume_set_properties":
|
||||
case "volume_list_effects":
|
||||
case "volume_create_profile":
|
||||
{
|
||||
if (!GraphicsHelpers.HasVolumeSystem)
|
||||
return new ErrorResponse(
|
||||
"Volume system not available. Requires URP or HDRP (com.unity.render-pipelines.core).");
|
||||
|
||||
return action switch
|
||||
{
|
||||
"volume_create" => VolumeOps.CreateVolume(@params),
|
||||
"volume_add_effect" => VolumeOps.AddEffect(@params),
|
||||
"volume_set_effect" => VolumeOps.SetEffect(@params),
|
||||
"volume_remove_effect" => VolumeOps.RemoveEffect(@params),
|
||||
"volume_get_info" => VolumeOps.GetInfo(@params),
|
||||
"volume_set_properties" => VolumeOps.SetProperties(@params),
|
||||
"volume_list_effects" => VolumeOps.ListEffects(@params),
|
||||
"volume_create_profile" => VolumeOps.CreateProfile(@params),
|
||||
_ => new ErrorResponse($"Unknown volume action: '{action}'")
|
||||
};
|
||||
}
|
||||
|
||||
// --- Bake actions (always available, Edit mode only) ---
|
||||
case "bake_start":
|
||||
return LightBakingOps.StartBake(@params);
|
||||
case "bake_cancel":
|
||||
return LightBakingOps.CancelBake(@params);
|
||||
case "bake_status":
|
||||
return LightBakingOps.GetStatus(@params);
|
||||
case "bake_clear":
|
||||
return LightBakingOps.ClearBake(@params);
|
||||
case "bake_reflection_probe":
|
||||
return LightBakingOps.BakeReflectionProbe(@params);
|
||||
case "bake_get_settings":
|
||||
return LightBakingOps.GetSettings(@params);
|
||||
case "bake_set_settings":
|
||||
return LightBakingOps.SetSettings(@params);
|
||||
case "bake_create_light_probe_group":
|
||||
return LightBakingOps.CreateLightProbeGroup(@params);
|
||||
case "bake_create_reflection_probe":
|
||||
return LightBakingOps.CreateReflectionProbe(@params);
|
||||
case "bake_set_probe_positions":
|
||||
return LightBakingOps.SetProbePositions(@params);
|
||||
|
||||
// --- Stats actions (always available) ---
|
||||
case "stats_get":
|
||||
return RenderingStatsOps.GetStats(@params);
|
||||
case "stats_list_counters":
|
||||
return RenderingStatsOps.ListCounters(@params);
|
||||
case "stats_set_scene_debug":
|
||||
return RenderingStatsOps.SetSceneDebugMode(@params);
|
||||
case "stats_get_memory":
|
||||
return RenderingStatsOps.GetMemory(@params);
|
||||
|
||||
// --- Pipeline actions (always available) ---
|
||||
case "pipeline_get_info":
|
||||
return RenderPipelineOps.GetInfo(@params);
|
||||
case "pipeline_set_quality":
|
||||
return RenderPipelineOps.SetQuality(@params);
|
||||
case "pipeline_get_settings":
|
||||
return RenderPipelineOps.GetSettings(@params);
|
||||
case "pipeline_set_settings":
|
||||
return RenderPipelineOps.SetSettings(@params);
|
||||
|
||||
// --- Renderer feature actions (URP only) ---
|
||||
case "feature_list":
|
||||
case "feature_add":
|
||||
case "feature_remove":
|
||||
case "feature_configure":
|
||||
case "feature_toggle":
|
||||
case "feature_reorder":
|
||||
{
|
||||
if (!GraphicsHelpers.HasURP)
|
||||
return new ErrorResponse("Renderer features require URP (Universal Render Pipeline).");
|
||||
|
||||
return action switch
|
||||
{
|
||||
"feature_list" => RendererFeatureOps.ListFeatures(@params),
|
||||
"feature_add" => RendererFeatureOps.AddFeature(@params),
|
||||
"feature_remove" => RendererFeatureOps.RemoveFeature(@params),
|
||||
"feature_configure" => RendererFeatureOps.ConfigureFeature(@params),
|
||||
"feature_toggle" => RendererFeatureOps.ToggleFeature(@params),
|
||||
"feature_reorder" => RendererFeatureOps.ReorderFeatures(@params),
|
||||
_ => new ErrorResponse($"Unknown feature action: '{action}'")
|
||||
};
|
||||
}
|
||||
|
||||
// --- Skybox / Environment actions (always available) ---
|
||||
case "skybox_get":
|
||||
return SkyboxOps.GetEnvironment(@params);
|
||||
case "skybox_set_material":
|
||||
return SkyboxOps.SetMaterial(@params);
|
||||
case "skybox_set_properties":
|
||||
return SkyboxOps.SetMaterialProperties(@params);
|
||||
case "skybox_set_ambient":
|
||||
return SkyboxOps.SetAmbient(@params);
|
||||
case "skybox_set_fog":
|
||||
return SkyboxOps.SetFog(@params);
|
||||
case "skybox_set_reflection":
|
||||
return SkyboxOps.SetReflection(@params);
|
||||
case "skybox_set_sun":
|
||||
return SkyboxOps.SetSun(@params);
|
||||
|
||||
default:
|
||||
return new ErrorResponse(
|
||||
$"Unknown action: '{action}'. Valid actions: ping, "
|
||||
+ "volume_create, volume_add_effect, volume_set_effect, volume_remove_effect, "
|
||||
+ "volume_get_info, volume_set_properties, volume_list_effects, volume_create_profile, "
|
||||
+ "bake_start, bake_cancel, bake_status, bake_clear, bake_reflection_probe, "
|
||||
+ "bake_get_settings, bake_set_settings, bake_create_light_probe_group, "
|
||||
+ "bake_create_reflection_probe, bake_set_probe_positions, "
|
||||
+ "stats_get, stats_list_counters, stats_set_scene_debug, stats_get_memory, "
|
||||
+ "pipeline_get_info, pipeline_set_quality, pipeline_get_settings, pipeline_set_settings, "
|
||||
+ "feature_list, feature_add, feature_remove, feature_configure, feature_toggle, feature_reorder, "
|
||||
+ "skybox_get, skybox_set_material, skybox_set_properties, skybox_set_ambient, "
|
||||
+ "skybox_set_fog, skybox_set_reflection, skybox_set_sun.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"[ManageGraphics] Action '{action}' failed: {ex}");
|
||||
return new ErrorResponse($"Error in action '{action}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dafb0cedb22e465b8f7b19e68a636415
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,286 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Graphics
|
||||
{
|
||||
internal static class RenderPipelineOps
|
||||
{
|
||||
// === pipeline_get_info ===
|
||||
// Returns: active pipeline, quality level, renderer info, key settings
|
||||
internal static object GetInfo(JObject @params)
|
||||
{
|
||||
var pipeline = RenderPipelineUtility.GetActivePipeline();
|
||||
var pipelineAsset = GraphicsSettings.currentRenderPipeline;
|
||||
|
||||
// Quality level info
|
||||
int currentQuality = QualitySettings.GetQualityLevel();
|
||||
string[] qualityNames = QualitySettings.names;
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["pipeline"] = pipeline.ToString(),
|
||||
["pipelineName"] = GraphicsHelpers.GetPipelineName(),
|
||||
["qualityLevel"] = currentQuality,
|
||||
["qualityLevelName"] = currentQuality < qualityNames.Length ? qualityNames[currentQuality] : "Unknown",
|
||||
["qualityLevels"] = qualityNames,
|
||||
["colorSpace"] = QualitySettings.activeColorSpace.ToString(),
|
||||
["hasVolumeSystem"] = GraphicsHelpers.HasVolumeSystem,
|
||||
};
|
||||
|
||||
// If SRP, add pipeline asset info
|
||||
if (pipelineAsset != null)
|
||||
{
|
||||
data["pipelineAsset"] = pipelineAsset.name;
|
||||
data["pipelineAssetPath"] = AssetDatabase.GetAssetPath(pipelineAsset);
|
||||
data["pipelineAssetType"] = pipelineAsset.GetType().Name;
|
||||
|
||||
// Read common public properties via reflection
|
||||
var settings = new Dictionary<string, object>();
|
||||
TryReadProperty(pipelineAsset, "renderScale", settings);
|
||||
TryReadProperty(pipelineAsset, "supportsHDR", settings);
|
||||
TryReadProperty(pipelineAsset, "msaaSampleCount", settings);
|
||||
TryReadProperty(pipelineAsset, "shadowDistance", settings);
|
||||
TryReadProperty(pipelineAsset, "shadowCascadeCount", settings);
|
||||
TryReadProperty(pipelineAsset, "maxAdditionalLightsCount", settings);
|
||||
TryReadProperty(pipelineAsset, "supportsSoftShadows", settings);
|
||||
TryReadProperty(pipelineAsset, "colorGradingMode", settings);
|
||||
|
||||
if (settings.Count > 0)
|
||||
data["settings"] = settings;
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Pipeline: {GraphicsHelpers.GetPipelineName()}, Quality: {(currentQuality < qualityNames.Length ? qualityNames[currentQuality] : "?")}",
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
// === pipeline_set_quality ===
|
||||
// Params: level (int or string name)
|
||||
internal static object SetQuality(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string levelName = p.Get("level");
|
||||
int? levelIndex = p.GetInt("level");
|
||||
|
||||
string[] names = QualitySettings.names;
|
||||
int targetIndex = -1;
|
||||
|
||||
if (levelIndex.HasValue)
|
||||
{
|
||||
targetIndex = levelIndex.Value;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(levelName))
|
||||
{
|
||||
// Try exact match first
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
if (string.Equals(names[i], levelName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Try parse as int
|
||||
if (targetIndex < 0 && int.TryParse(levelName, out int parsed))
|
||||
targetIndex = parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"'level' parameter required. Available: {string.Join(", ", names)}");
|
||||
}
|
||||
|
||||
if (targetIndex < 0 || targetIndex >= names.Length)
|
||||
return new ErrorResponse(
|
||||
$"Invalid quality level. Available: {string.Join(", ", names)} (0-{names.Length - 1})");
|
||||
|
||||
QualitySettings.SetQualityLevel(targetIndex, true);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Quality level set to '{names[targetIndex]}' (index {targetIndex}).",
|
||||
data = new
|
||||
{
|
||||
level = targetIndex,
|
||||
name = names[targetIndex],
|
||||
allLevels = names
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === pipeline_get_settings ===
|
||||
// Detailed read of pipeline asset settings via public properties + SerializedObject fallback
|
||||
internal static object GetSettings(JObject @params)
|
||||
{
|
||||
var pipelineAsset = GraphicsSettings.currentRenderPipeline;
|
||||
if (pipelineAsset == null)
|
||||
return new ErrorResponse("No render pipeline asset found (Built-in pipeline has no asset).");
|
||||
|
||||
var settings = new Dictionary<string, object>();
|
||||
|
||||
// Public properties (URP)
|
||||
string[] publicProps = {
|
||||
"renderScale", "supportsHDR", "msaaSampleCount", "shadowDistance",
|
||||
"shadowCascadeCount", "mainLightShadowmapResolution",
|
||||
"additionalLightsShadowmapResolution", "maxAdditionalLightsCount",
|
||||
"supportsSoftShadows", "colorGradingMode", "colorGradingLutSize"
|
||||
};
|
||||
foreach (var propName in publicProps)
|
||||
TryReadProperty(pipelineAsset, propName, settings);
|
||||
|
||||
// SerializedObject for non-public settings
|
||||
var serializedSettings = new Dictionary<string, object>();
|
||||
string[] serializedPaths = {
|
||||
"m_DefaultRendererIndex", "m_MainLightRenderingMode",
|
||||
"m_AdditionalLightsRenderingMode", "m_SupportsOpaqueTexture",
|
||||
"m_SupportsDepthTexture"
|
||||
};
|
||||
|
||||
using (var so = new SerializedObject(pipelineAsset))
|
||||
{
|
||||
foreach (var path in serializedPaths)
|
||||
{
|
||||
var prop = so.FindProperty(path);
|
||||
if (prop != null)
|
||||
serializedSettings[path] = GraphicsHelpers.ReadSerializedValue(prop);
|
||||
}
|
||||
}
|
||||
|
||||
if (serializedSettings.Count > 0)
|
||||
settings["_serialized"] = serializedSettings;
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Pipeline settings for '{pipelineAsset.name}'.",
|
||||
data = new
|
||||
{
|
||||
assetName = pipelineAsset.name,
|
||||
assetPath = AssetDatabase.GetAssetPath(pipelineAsset),
|
||||
assetType = pipelineAsset.GetType().Name,
|
||||
settings
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === pipeline_set_settings ===
|
||||
// Write pipeline asset settings via public properties + SerializedObject fallback
|
||||
internal static object SetSettings(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
var settingsToken = p.GetRaw("settings") as JObject;
|
||||
if (settingsToken == null)
|
||||
return new ErrorResponse("'settings' dict is required.");
|
||||
|
||||
var pipelineAsset = GraphicsSettings.currentRenderPipeline;
|
||||
if (pipelineAsset == null)
|
||||
return new ErrorResponse("No render pipeline asset found.");
|
||||
|
||||
var changed = new List<string>();
|
||||
var failed = new List<string>();
|
||||
|
||||
using (var so = new SerializedObject(pipelineAsset))
|
||||
{
|
||||
foreach (var prop in settingsToken.Properties())
|
||||
{
|
||||
string propName = prop.Name;
|
||||
JToken value = prop.Value;
|
||||
|
||||
// Try public property first
|
||||
var publicProp = pipelineAsset.GetType().GetProperty(propName,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
if (publicProp != null && publicProp.CanWrite)
|
||||
{
|
||||
try
|
||||
{
|
||||
object converted = ConvertPropertyValue(value, publicProp.PropertyType);
|
||||
publicProp.SetValue(pipelineAsset, converted);
|
||||
changed.Add(propName);
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[RenderPipelineOps] Failed to set '{propName}' via property: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Try SerializedObject fallback (for m_ prefixed properties)
|
||||
string serializedPath = propName.StartsWith("m_") ? propName : $"m_{char.ToUpper(propName[0])}{propName.Substring(1)}";
|
||||
var sProp = so.FindProperty(serializedPath);
|
||||
if (sProp == null && !propName.StartsWith("m_"))
|
||||
sProp = so.FindProperty(propName);
|
||||
|
||||
if (sProp != null)
|
||||
{
|
||||
if (GraphicsHelpers.SetSerializedValue(sProp, value))
|
||||
{
|
||||
changed.Add(propName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
failed.Add(propName);
|
||||
}
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(pipelineAsset);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
var msg = $"Updated {changed.Count} pipeline setting(s)";
|
||||
if (failed.Count > 0)
|
||||
msg += $". Failed: {string.Join(", ", failed)}";
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = msg,
|
||||
data = new { changed, failed }
|
||||
};
|
||||
}
|
||||
|
||||
// --- Helper: Convert JToken to target property type ---
|
||||
private static object ConvertPropertyValue(JToken value, Type targetType)
|
||||
{
|
||||
if (targetType == typeof(bool)) return ParamCoercion.CoerceBool(value, false);
|
||||
if (targetType == typeof(int)) return ParamCoercion.CoerceInt(value, 0);
|
||||
if (targetType == typeof(float)) return ParamCoercion.CoerceFloat(value, 0f);
|
||||
if (targetType == typeof(string)) return value.ToString();
|
||||
if (targetType.IsEnum)
|
||||
{
|
||||
string str = value.ToString();
|
||||
if (Enum.TryParse(targetType, str, true, out object enumVal))
|
||||
return enumVal;
|
||||
if (int.TryParse(str, out int intVal))
|
||||
return Enum.ToObject(targetType, intVal);
|
||||
}
|
||||
return Convert.ChangeType(value.ToObject<object>(), targetType);
|
||||
}
|
||||
|
||||
// --- Helper: Try to read a property value via reflection ---
|
||||
private static void TryReadProperty(object obj, string propertyName, Dictionary<string, object> target)
|
||||
{
|
||||
if (obj == null) return;
|
||||
var prop = obj.GetType().GetProperty(propertyName,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var val = prop.GetValue(obj);
|
||||
target[propertyName] = val is Enum e ? e.ToString() : val;
|
||||
}
|
||||
catch { /* skip unreadable properties */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e637fccb0c440e4b8cdb3bc6040c7c9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,577 @@
|
||||
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 RendererFeatureOps
|
||||
{
|
||||
// Cached URP types (resolved via reflection to avoid hard dependency)
|
||||
private static Type _scriptableRendererDataType;
|
||||
private static Type _scriptableRendererFeatureType;
|
||||
private static Type _universalRenderPipelineAssetType;
|
||||
private static bool _typesResolved;
|
||||
|
||||
private static void EnsureTypes()
|
||||
{
|
||||
if (_typesResolved) return;
|
||||
_typesResolved = true;
|
||||
|
||||
_scriptableRendererDataType = Type.GetType(
|
||||
"UnityEngine.Rendering.Universal.ScriptableRendererData, Unity.RenderPipelines.Universal.Runtime");
|
||||
_scriptableRendererFeatureType = Type.GetType(
|
||||
"UnityEngine.Rendering.Universal.ScriptableRendererFeature, Unity.RenderPipelines.Universal.Runtime");
|
||||
_universalRenderPipelineAssetType = Type.GetType(
|
||||
"UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset, Unity.RenderPipelines.Universal.Runtime");
|
||||
}
|
||||
|
||||
// === feature_list ===
|
||||
internal static object ListFeatures(JObject @params)
|
||||
{
|
||||
var rendererData = GetRendererData(@params);
|
||||
if (rendererData == null)
|
||||
return new ErrorResponse("Could not find URP ScriptableRendererData. Ensure URP is active.");
|
||||
|
||||
var featuresProp = rendererData.GetType().GetProperty("rendererFeatures",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
if (featuresProp == null)
|
||||
return new ErrorResponse("rendererFeatures property not found on renderer data.");
|
||||
|
||||
var featuresList = featuresProp.GetValue(rendererData) as System.Collections.IList;
|
||||
if (featuresList == null)
|
||||
return new { success = true, message = "No renderer features.", data = new { features = new object[0] } };
|
||||
|
||||
var features = new List<object>();
|
||||
for (int i = 0; i < featuresList.Count; i++)
|
||||
{
|
||||
var feature = featuresList[i] as ScriptableObject;
|
||||
if (feature == null) continue;
|
||||
|
||||
var isActiveProp = feature.GetType().GetProperty("isActive",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
features.Add(new
|
||||
{
|
||||
index = i,
|
||||
name = feature.name,
|
||||
type = feature.GetType().Name,
|
||||
isActive = isActiveProp != null ? (bool)isActiveProp.GetValue(feature) : true,
|
||||
properties = GetFeatureProperties(feature)
|
||||
});
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Found {features.Count} renderer feature(s).",
|
||||
data = new
|
||||
{
|
||||
rendererDataName = (rendererData as ScriptableObject)?.name,
|
||||
features
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === feature_add ===
|
||||
internal static object AddFeature(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string typeName = p.Get("type");
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
return new ErrorResponse("'type' parameter required (e.g., 'FullScreenPassRendererFeature', 'RenderObjects').");
|
||||
|
||||
var rendererData = GetRendererData(@params);
|
||||
if (rendererData == null)
|
||||
return new ErrorResponse("Could not find URP ScriptableRendererData.");
|
||||
|
||||
EnsureTypes();
|
||||
if (_scriptableRendererFeatureType == null)
|
||||
return new ErrorResponse("ScriptableRendererFeature type not found. Is URP installed?");
|
||||
|
||||
// Resolve the feature type
|
||||
var featureType = ResolveFeatureType(typeName);
|
||||
if (featureType == null)
|
||||
{
|
||||
var available = GetAvailableFeatureTypes();
|
||||
return new ErrorResponse(
|
||||
$"Feature type '{typeName}' not found. Available: {string.Join(", ", available.Select(t => t.Name))}");
|
||||
}
|
||||
|
||||
// Create the feature instance
|
||||
var feature = ScriptableObject.CreateInstance(featureType);
|
||||
if (feature == null)
|
||||
return new ErrorResponse($"Failed to create instance of '{featureType.Name}'.");
|
||||
|
||||
string displayName = p.Get("name") ?? featureType.Name;
|
||||
feature.name = displayName;
|
||||
|
||||
// Add to the renderer data asset
|
||||
Undo.RecordObject(rendererData as UnityEngine.Object, "Add Renderer Feature");
|
||||
AssetDatabase.AddObjectToAsset(feature, rendererData as UnityEngine.Object);
|
||||
|
||||
// Add to the features list via SerializedObject
|
||||
using (var so = new SerializedObject(rendererData as UnityEngine.Object))
|
||||
{
|
||||
var rendererFeaturesProp = so.FindProperty("m_RendererFeatures");
|
||||
if (rendererFeaturesProp != null)
|
||||
{
|
||||
rendererFeaturesProp.arraySize++;
|
||||
var element = rendererFeaturesProp.GetArrayElementAtIndex(rendererFeaturesProp.arraySize - 1);
|
||||
element.objectReferenceValue = feature;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
// Also update the map (m_RendererFeatureMap) if it exists
|
||||
// Map stores persistent local file IDs, not transient instance IDs
|
||||
var mapProp = so.FindProperty("m_RendererFeatureMap");
|
||||
if (mapProp != null)
|
||||
{
|
||||
long localId = 0;
|
||||
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(feature, out _, out localId);
|
||||
mapProp.arraySize++;
|
||||
var mapElement = mapProp.GetArrayElementAtIndex(mapProp.arraySize - 1);
|
||||
mapElement.longValue = localId;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
|
||||
// Configure initial properties if provided
|
||||
var propertiesToken = p.GetRaw("properties") as JObject;
|
||||
if (propertiesToken != null)
|
||||
ApplyFeatureProperties(feature, propertiesToken);
|
||||
|
||||
// Set material if provided (common for FullScreenPass)
|
||||
string materialPath = p.Get("material");
|
||||
if (!string.IsNullOrEmpty(materialPath))
|
||||
TrySetMaterial(feature, materialPath);
|
||||
|
||||
EditorUtility.SetDirty(rendererData as UnityEngine.Object);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added renderer feature '{displayName}' ({featureType.Name}).",
|
||||
data = new
|
||||
{
|
||||
name = displayName,
|
||||
type = featureType.Name,
|
||||
instanceId = feature.GetInstanceIDCompat()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === feature_remove ===
|
||||
internal static object RemoveFeature(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
int? index = p.GetInt("index");
|
||||
string name = p.Get("name");
|
||||
|
||||
var rendererData = GetRendererData(@params);
|
||||
if (rendererData == null)
|
||||
return new ErrorResponse("Could not find URP ScriptableRendererData.");
|
||||
|
||||
var featuresProp = rendererData.GetType().GetProperty("rendererFeatures",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
if (featuresProp == null)
|
||||
return new ErrorResponse("rendererFeatures property not found.");
|
||||
|
||||
var featuresList = featuresProp.GetValue(rendererData) as System.Collections.IList;
|
||||
if (featuresList == null || featuresList.Count == 0)
|
||||
return new ErrorResponse("No renderer features to remove.");
|
||||
|
||||
int targetIndex = ResolveFeatureIndex(featuresList, index, name);
|
||||
if (targetIndex < 0)
|
||||
return new ErrorResponse($"Feature not found. Specify 'index' (0-{featuresList.Count - 1}) or 'name'.");
|
||||
|
||||
var feature = featuresList[targetIndex] as ScriptableObject;
|
||||
string featureName = feature?.name ?? "Unknown";
|
||||
|
||||
Undo.RecordObject(rendererData as UnityEngine.Object, "Remove Renderer Feature");
|
||||
|
||||
// Remove from the list via SerializedObject
|
||||
using (var so = new SerializedObject(rendererData as UnityEngine.Object))
|
||||
{
|
||||
var rendererFeaturesPropSo = so.FindProperty("m_RendererFeatures");
|
||||
if (rendererFeaturesPropSo != null)
|
||||
{
|
||||
rendererFeaturesPropSo.DeleteArrayElementAtIndex(targetIndex);
|
||||
// SerializedProperty.DeleteArrayElementAtIndex sets to null first for ObjectReference
|
||||
if (rendererFeaturesPropSo.arraySize > targetIndex)
|
||||
{
|
||||
var element = rendererFeaturesPropSo.GetArrayElementAtIndex(targetIndex);
|
||||
if (element.objectReferenceValue == null)
|
||||
rendererFeaturesPropSo.DeleteArrayElementAtIndex(targetIndex);
|
||||
}
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
// Clean up the map
|
||||
var mapProp = so.FindProperty("m_RendererFeatureMap");
|
||||
if (mapProp != null && targetIndex < mapProp.arraySize)
|
||||
{
|
||||
mapProp.DeleteArrayElementAtIndex(targetIndex);
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the sub-asset
|
||||
if (feature != null)
|
||||
AssetDatabase.RemoveObjectFromAsset(feature);
|
||||
|
||||
EditorUtility.SetDirty(rendererData as UnityEngine.Object);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Removed renderer feature '{featureName}' at index {targetIndex}."
|
||||
};
|
||||
}
|
||||
|
||||
// === feature_configure ===
|
||||
internal static object ConfigureFeature(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
int? index = p.GetInt("index");
|
||||
string name = p.Get("name");
|
||||
var propertiesToken = (p.GetRaw("properties") ?? p.GetRaw("settings")) as JObject;
|
||||
|
||||
if (propertiesToken == null)
|
||||
return new ErrorResponse("'properties' (or 'settings') dict is required.");
|
||||
|
||||
var rendererData = GetRendererData(@params);
|
||||
if (rendererData == null)
|
||||
return new ErrorResponse("Could not find URP ScriptableRendererData.");
|
||||
|
||||
var featuresProp = rendererData.GetType().GetProperty("rendererFeatures",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
var featuresList = featuresProp?.GetValue(rendererData) as System.Collections.IList;
|
||||
if (featuresList == null || featuresList.Count == 0)
|
||||
return new ErrorResponse("No renderer features to configure.");
|
||||
|
||||
int targetIndex = ResolveFeatureIndex(featuresList, index, name);
|
||||
if (targetIndex < 0)
|
||||
return new ErrorResponse($"Feature not found. Specify 'index' (0-{featuresList.Count - 1}) or 'name'.");
|
||||
|
||||
var feature = featuresList[targetIndex] as ScriptableObject;
|
||||
if (feature == null)
|
||||
return new ErrorResponse($"Feature at index {targetIndex} is null.");
|
||||
|
||||
Undo.RecordObject(feature, "Configure Renderer Feature");
|
||||
var result = ApplyFeatureProperties(feature, propertiesToken);
|
||||
EditorUtility.SetDirty(feature);
|
||||
EditorUtility.SetDirty(rendererData as UnityEngine.Object);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Configured '{feature.name}': {result.changed.Count} set, {result.failed.Count} failed.",
|
||||
data = new { result.changed, result.failed }
|
||||
};
|
||||
}
|
||||
|
||||
// === feature_toggle ===
|
||||
internal static object ToggleFeature(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
int? index = p.GetInt("index");
|
||||
string name = p.Get("name");
|
||||
bool? active = p.GetBool("active");
|
||||
|
||||
var rendererData = GetRendererData(@params);
|
||||
if (rendererData == null)
|
||||
return new ErrorResponse("Could not find URP ScriptableRendererData.");
|
||||
|
||||
var featuresProp = rendererData.GetType().GetProperty("rendererFeatures",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
var featuresList = featuresProp?.GetValue(rendererData) as System.Collections.IList;
|
||||
if (featuresList == null || featuresList.Count == 0)
|
||||
return new ErrorResponse("No renderer features.");
|
||||
|
||||
int targetIndex = ResolveFeatureIndex(featuresList, index, name);
|
||||
if (targetIndex < 0)
|
||||
return new ErrorResponse($"Feature not found. Specify 'index' or 'name'.");
|
||||
|
||||
var feature = featuresList[targetIndex] as ScriptableObject;
|
||||
if (feature == null)
|
||||
return new ErrorResponse($"Feature at index {targetIndex} is null.");
|
||||
|
||||
// ScriptableRendererFeature.SetActive(bool) is public
|
||||
var setActiveMethod = feature.GetType().GetMethod("SetActive",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
if (setActiveMethod == null)
|
||||
return new ErrorResponse("SetActive method not found on feature.");
|
||||
|
||||
bool newState = active ?? true;
|
||||
Undo.RecordObject(feature, "Toggle Renderer Feature");
|
||||
setActiveMethod.Invoke(feature, new object[] { newState });
|
||||
EditorUtility.SetDirty(feature);
|
||||
EditorUtility.SetDirty(rendererData as UnityEngine.Object);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Feature '{feature.name}' {(newState ? "enabled" : "disabled")}."
|
||||
};
|
||||
}
|
||||
|
||||
// === feature_reorder ===
|
||||
internal static object ReorderFeatures(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
var orderToken = p.GetRaw("order") as JArray;
|
||||
if (orderToken == null)
|
||||
return new ErrorResponse("'order' parameter required (array of indices, e.g. [2, 0, 1]).");
|
||||
|
||||
var rendererData = GetRendererData(@params);
|
||||
if (rendererData == null)
|
||||
return new ErrorResponse("Could not find URP ScriptableRendererData.");
|
||||
|
||||
var featuresProp = rendererData.GetType().GetProperty("rendererFeatures",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
var featuresList = featuresProp?.GetValue(rendererData) as System.Collections.IList;
|
||||
if (featuresList == null || featuresList.Count == 0)
|
||||
return new ErrorResponse("No renderer features to reorder.");
|
||||
|
||||
var newOrder = orderToken.Select(t => (int)t).ToList();
|
||||
if (newOrder.Count != featuresList.Count)
|
||||
return new ErrorResponse(
|
||||
$"Order array length ({newOrder.Count}) must match feature count ({featuresList.Count}).");
|
||||
|
||||
// Validate all indices are present
|
||||
var sorted = newOrder.OrderBy(x => x).ToList();
|
||||
for (int i = 0; i < sorted.Count; i++)
|
||||
{
|
||||
if (sorted[i] != i)
|
||||
return new ErrorResponse("Order array must contain each index exactly once (0 to N-1).");
|
||||
}
|
||||
|
||||
Undo.RecordObject(rendererData as UnityEngine.Object, "Reorder Renderer Features");
|
||||
|
||||
using (var so = new SerializedObject(rendererData as UnityEngine.Object))
|
||||
{
|
||||
var rendererFeaturesPropSo = so.FindProperty("m_RendererFeatures");
|
||||
if (rendererFeaturesPropSo == null)
|
||||
return new ErrorResponse("m_RendererFeatures property not found.");
|
||||
|
||||
// Read current features
|
||||
var current = new UnityEngine.Object[featuresList.Count];
|
||||
for (int i = 0; i < featuresList.Count; i++)
|
||||
current[i] = rendererFeaturesPropSo.GetArrayElementAtIndex(i).objectReferenceValue;
|
||||
|
||||
// Apply new order
|
||||
for (int i = 0; i < newOrder.Count; i++)
|
||||
rendererFeaturesPropSo.GetArrayElementAtIndex(i).objectReferenceValue = current[newOrder[i]];
|
||||
|
||||
// Also reorder the feature map to keep it in sync
|
||||
var mapProp = so.FindProperty("m_RendererFeatureMap");
|
||||
if (mapProp != null && mapProp.arraySize == featuresList.Count)
|
||||
{
|
||||
var currentMap = new long[featuresList.Count];
|
||||
for (int i = 0; i < featuresList.Count; i++)
|
||||
currentMap[i] = mapProp.GetArrayElementAtIndex(i).longValue;
|
||||
for (int i = 0; i < newOrder.Count; i++)
|
||||
mapProp.GetArrayElementAtIndex(i).longValue = currentMap[newOrder[i]];
|
||||
}
|
||||
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(rendererData as UnityEngine.Object);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Reordered {featuresList.Count} renderer features."
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private static object GetRendererData(JObject @params)
|
||||
{
|
||||
EnsureTypes();
|
||||
if (_universalRenderPipelineAssetType == null || _scriptableRendererDataType == null)
|
||||
return null;
|
||||
|
||||
var pipelineAsset = GraphicsSettings.currentRenderPipeline;
|
||||
if (pipelineAsset == null || !_universalRenderPipelineAssetType.IsInstanceOfType(pipelineAsset))
|
||||
return null;
|
||||
|
||||
var p = new ToolParams(@params);
|
||||
int rendererIndex = p.GetInt("renderer_index") ?? -1;
|
||||
|
||||
// Get renderer data from the URP asset
|
||||
// Try scriptableRendererData property or GetRenderer method
|
||||
if (rendererIndex >= 0)
|
||||
{
|
||||
// Use SerializedObject to get specific renderer
|
||||
using (var so = new SerializedObject(pipelineAsset))
|
||||
{
|
||||
var renderersProp = so.FindProperty("m_RendererDataList");
|
||||
if (renderersProp == null || rendererIndex >= renderersProp.arraySize)
|
||||
return null;
|
||||
|
||||
var element = renderersProp.GetArrayElementAtIndex(rendererIndex);
|
||||
return element.objectReferenceValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: get the active renderer (index from m_DefaultRendererIndex)
|
||||
using (var so = new SerializedObject(pipelineAsset))
|
||||
{
|
||||
var defaultIndex = so.FindProperty("m_DefaultRendererIndex");
|
||||
int idx = defaultIndex != null ? defaultIndex.intValue : 0;
|
||||
|
||||
var renderersProp = so.FindProperty("m_RendererDataList");
|
||||
if (renderersProp != null && idx < renderersProp.arraySize)
|
||||
{
|
||||
var element = renderersProp.GetArrayElementAtIndex(idx);
|
||||
return element.objectReferenceValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Type ResolveFeatureType(string typeName)
|
||||
{
|
||||
EnsureTypes();
|
||||
if (_scriptableRendererFeatureType == null) return null;
|
||||
|
||||
var derivedTypes = TypeCache.GetTypesDerivedFrom(_scriptableRendererFeatureType);
|
||||
foreach (var t in derivedTypes)
|
||||
{
|
||||
if (t.IsAbstract) continue;
|
||||
if (string.Equals(t.Name, typeName, StringComparison.OrdinalIgnoreCase))
|
||||
return t;
|
||||
}
|
||||
|
||||
// Try partial match (e.g., "FullScreenPass" matches "FullScreenPassRendererFeature")
|
||||
foreach (var t in derivedTypes)
|
||||
{
|
||||
if (t.IsAbstract) continue;
|
||||
if (t.Name.StartsWith(typeName, StringComparison.OrdinalIgnoreCase))
|
||||
return t;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<Type> GetAvailableFeatureTypes()
|
||||
{
|
||||
EnsureTypes();
|
||||
if (_scriptableRendererFeatureType == null) return new List<Type>();
|
||||
|
||||
return TypeCache.GetTypesDerivedFrom(_scriptableRendererFeatureType)
|
||||
.Where(t => !t.IsAbstract && !t.IsGenericType)
|
||||
.OrderBy(t => t.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static int ResolveFeatureIndex(System.Collections.IList featuresList, int? index, string name)
|
||||
{
|
||||
if (index.HasValue && index.Value >= 0 && index.Value < featuresList.Count)
|
||||
return index.Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
for (int i = 0; i < featuresList.Count; i++)
|
||||
{
|
||||
var feature = featuresList[i] as ScriptableObject;
|
||||
if (feature == null) continue;
|
||||
if (string.Equals(feature.name, name, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(feature.GetType().Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> GetFeatureProperties(ScriptableObject feature)
|
||||
{
|
||||
var props = new Dictionary<string, object>();
|
||||
using (var so = new SerializedObject(feature))
|
||||
{
|
||||
var iterator = so.GetIterator();
|
||||
if (iterator.NextVisible(true)) // Enter children
|
||||
{
|
||||
do
|
||||
{
|
||||
// Skip Unity internal properties
|
||||
if (iterator.name == "m_Script" || iterator.name == "m_ObjectHideFlags" || iterator.name == "m_Name")
|
||||
continue;
|
||||
|
||||
props[iterator.name] = GraphicsHelpers.ReadSerializedValue(iterator);
|
||||
} while (iterator.NextVisible(false));
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
private static (List<string> changed, List<string> failed) ApplyFeatureProperties(
|
||||
ScriptableObject feature, JObject propertiesToken)
|
||||
{
|
||||
var changed = new List<string>();
|
||||
var failed = new List<string>();
|
||||
|
||||
using (var so = new SerializedObject(feature))
|
||||
{
|
||||
foreach (var prop in propertiesToken.Properties())
|
||||
{
|
||||
var sProp = so.FindProperty(prop.Name);
|
||||
if (sProp != null)
|
||||
{
|
||||
if (GraphicsHelpers.SetSerializedValue(sProp, prop.Value))
|
||||
changed.Add(prop.Name);
|
||||
else
|
||||
failed.Add(prop.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try nested: "settings.fieldName"
|
||||
string nested = $"settings.{prop.Name}";
|
||||
sProp = so.FindProperty(nested);
|
||||
if (sProp != null && GraphicsHelpers.SetSerializedValue(sProp, prop.Value))
|
||||
changed.Add(prop.Name);
|
||||
else
|
||||
failed.Add(prop.Name);
|
||||
}
|
||||
}
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
return (changed, failed);
|
||||
}
|
||||
|
||||
private static void TrySetMaterial(ScriptableObject feature, string materialPath)
|
||||
{
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
|
||||
if (mat == null) return;
|
||||
|
||||
using (var so = new SerializedObject(feature))
|
||||
{
|
||||
// FullScreenPassRendererFeature uses "m_PassMaterial" or "passMaterial"
|
||||
var matProp = so.FindProperty("m_PassMaterial") ?? so.FindProperty("passMaterial");
|
||||
if (matProp != null && matProp.propertyType == SerializedPropertyType.ObjectReference)
|
||||
{
|
||||
matProp.objectReferenceValue = mat;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a75368a2f6b478fbaa88a36c677b5af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Unity.Profiling;
|
||||
using Unity.Profiling.LowLevel.Unsafe;
|
||||
using UnityEngine.Profiling;
|
||||
using UProfiler = UnityEngine.Profiling.Profiler;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Graphics
|
||||
{
|
||||
internal static class RenderingStatsOps
|
||||
{
|
||||
private static readonly (string counterName, string jsonKey)[] COUNTER_MAP = new[]
|
||||
{
|
||||
("Draw Calls Count", "draw_calls"),
|
||||
("Batches Count", "batches"),
|
||||
("SetPass Calls Count", "set_pass_calls"),
|
||||
("Triangles Count", "triangles"),
|
||||
("Vertices Count", "vertices"),
|
||||
("Dynamic Batches Count", "dynamic_batches"),
|
||||
("Dynamic Batched Draw Calls Count", "dynamic_batched_draw_calls"),
|
||||
("Static Batches Count", "static_batches"),
|
||||
("Static Batched Draw Calls Count", "static_batched_draw_calls"),
|
||||
("Instanced Batches Count", "instanced_batches"),
|
||||
("Instanced Batched Draw Calls Count", "instanced_batched_draw_calls"),
|
||||
("Shadow Casters Count", "shadow_casters"),
|
||||
("Render Textures Count", "render_textures"),
|
||||
("Render Textures Bytes", "render_textures_bytes"),
|
||||
("Used Textures Count", "used_textures"),
|
||||
("Used Textures Bytes", "used_textures_bytes"),
|
||||
("Render Textures Changes Count", "render_target_changes"),
|
||||
("Visible Skinned Meshes Count", "visible_skinned_meshes"),
|
||||
};
|
||||
|
||||
// === stats_get ===
|
||||
internal static object GetStats(JObject @params)
|
||||
{
|
||||
var stats = new Dictionary<string, object>();
|
||||
|
||||
foreach (var (counterName, jsonKey) in COUNTER_MAP)
|
||||
{
|
||||
using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, counterName);
|
||||
stats[jsonKey] = recorder.Valid ? recorder.CurrentValue : 0;
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Rendering stats captured.",
|
||||
data = stats
|
||||
};
|
||||
}
|
||||
|
||||
// === stats_list_counters ===
|
||||
internal static object ListCounters(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string categoryName = p.Get("category");
|
||||
|
||||
// Default to "Render" category to avoid massive payloads (all categories = 300K+ chars)
|
||||
ProfilerCategory category = ProfilerCategory.Render;
|
||||
if (!string.IsNullOrEmpty(categoryName))
|
||||
{
|
||||
category = TryResolveCategory(categoryName);
|
||||
}
|
||||
|
||||
var allHandles = new List<ProfilerRecorderHandle>();
|
||||
ProfilerRecorderHandle.GetAvailable(allHandles);
|
||||
var counters = allHandles
|
||||
.Select(h => ProfilerRecorderHandle.GetDescription(h))
|
||||
.Where(d => string.Equals(d.Category.Name, category.Name, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(d => new
|
||||
{
|
||||
name = d.Name,
|
||||
category = d.Category.Name,
|
||||
unit = d.UnitType.ToString()
|
||||
})
|
||||
.OrderBy(c => c.name).ToList();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Found {counters.Count} counters in category '{category.Name}'.",
|
||||
data = new { counters }
|
||||
};
|
||||
}
|
||||
|
||||
// === stats_set_scene_debug_mode ===
|
||||
internal static object SetSceneDebugMode(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string modeName = p.Get("mode");
|
||||
if (string.IsNullOrEmpty(modeName))
|
||||
{
|
||||
var validModes = string.Join(", ", Enum.GetNames(typeof(DrawCameraMode)).Take(20));
|
||||
return new ErrorResponse(
|
||||
$"'mode' parameter required. Options: {validModes}");
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<DrawCameraMode>(modeName, true, out var drawMode))
|
||||
{
|
||||
var validModes = string.Join(", ", Enum.GetNames(typeof(DrawCameraMode)).Take(20));
|
||||
return new ErrorResponse($"Unknown mode '{modeName}'. Valid: {validModes}");
|
||||
}
|
||||
|
||||
var sceneView = SceneView.lastActiveSceneView;
|
||||
if (sceneView == null)
|
||||
return new ErrorResponse("No active Scene View found.");
|
||||
|
||||
sceneView.cameraMode = SceneView.GetBuiltinCameraMode(drawMode);
|
||||
|
||||
sceneView.Repaint();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Scene debug mode set to '{drawMode}'."
|
||||
};
|
||||
}
|
||||
|
||||
// === stats_get_memory ===
|
||||
internal static object GetMemory(JObject @params)
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["totalAllocatedMB"] = Math.Round(UProfiler.GetTotalAllocatedMemoryLong() / (1024.0 * 1024.0), 2),
|
||||
["totalReservedMB"] = Math.Round(UProfiler.GetTotalReservedMemoryLong() / (1024.0 * 1024.0), 2),
|
||||
["totalUnusedReservedMB"] = Math.Round(UProfiler.GetTotalUnusedReservedMemoryLong() / (1024.0 * 1024.0), 2),
|
||||
["monoUsedMB"] = Math.Round(UProfiler.GetMonoUsedSizeLong() / (1024.0 * 1024.0), 2),
|
||||
["monoHeapMB"] = Math.Round(UProfiler.GetMonoHeapSizeLong() / (1024.0 * 1024.0), 2),
|
||||
["graphicsDriverMB"] = Math.Round(UProfiler.GetAllocatedMemoryForGraphicsDriver() / (1024.0 * 1024.0), 2),
|
||||
};
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Memory stats captured.",
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
// --- Helper: Try to resolve a ProfilerCategory by name ---
|
||||
private static ProfilerCategory TryResolveCategory(string name)
|
||||
{
|
||||
// ProfilerCategory has static properties for well-known categories
|
||||
switch (name.ToLowerInvariant())
|
||||
{
|
||||
case "render": return ProfilerCategory.Render;
|
||||
case "scripts": return ProfilerCategory.Scripts;
|
||||
case "memory": return ProfilerCategory.Memory;
|
||||
case "physics": return ProfilerCategory.Physics;
|
||||
case "animation": return ProfilerCategory.Animation;
|
||||
case "audio": return ProfilerCategory.Audio;
|
||||
case "lighting": return ProfilerCategory.Lighting;
|
||||
case "network": return ProfilerCategory.Network;
|
||||
case "gui": return ProfilerCategory.Gui;
|
||||
case "ai": return ProfilerCategory.Ai;
|
||||
case "video": return ProfilerCategory.Video;
|
||||
case "loading": return ProfilerCategory.Loading;
|
||||
case "input": return ProfilerCategory.Input;
|
||||
case "vr": return ProfilerCategory.Vr;
|
||||
case "internal": return ProfilerCategory.Internal;
|
||||
default: return ProfilerCategory.Render;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6a02f2bb19e450a9ddca18ee8d211bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,497 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Graphics
|
||||
{
|
||||
internal static class SkyboxOps
|
||||
{
|
||||
static Texture CustomReflectionTexture
|
||||
{
|
||||
get =>
|
||||
#if UNITY_2022_1_OR_NEWER
|
||||
RenderSettings.customReflectionTexture;
|
||||
#else
|
||||
RenderSettings.customReflection;
|
||||
#endif
|
||||
set {
|
||||
#if UNITY_2022_1_OR_NEWER
|
||||
RenderSettings.customReflectionTexture = value;
|
||||
#else
|
||||
RenderSettings.customReflection = value;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------
|
||||
// skybox_get — read all environment settings
|
||||
// ---------------------------------------------------------------
|
||||
public static object GetEnvironment(JObject @params)
|
||||
{
|
||||
var skyMat = RenderSettings.skybox;
|
||||
var sun = RenderSettings.sun;
|
||||
|
||||
object matInfo = null;
|
||||
if (skyMat != null)
|
||||
{
|
||||
var props = new List<object>();
|
||||
int count = skyMat.shader.GetPropertyCount();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string propName = skyMat.shader.GetPropertyName(i);
|
||||
var propType = skyMat.shader.GetPropertyType(i);
|
||||
object val = ReadMaterialProperty(skyMat, propName, propType);
|
||||
props.Add(new { name = propName, type = propType.ToString(), value = val });
|
||||
}
|
||||
matInfo = new
|
||||
{
|
||||
name = skyMat.name,
|
||||
shader = skyMat.shader.name,
|
||||
path = AssetDatabase.GetAssetPath(skyMat),
|
||||
properties = props
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Environment settings retrieved.",
|
||||
data = new
|
||||
{
|
||||
skybox = matInfo,
|
||||
ambient = new
|
||||
{
|
||||
mode = RenderSettings.ambientMode.ToString(),
|
||||
skyColor = ColorToArray(RenderSettings.ambientSkyColor),
|
||||
equatorColor = ColorToArray(RenderSettings.ambientEquatorColor),
|
||||
groundColor = ColorToArray(RenderSettings.ambientGroundColor),
|
||||
ambientLight = ColorToArray(RenderSettings.ambientLight),
|
||||
intensity = RenderSettings.ambientIntensity
|
||||
},
|
||||
fog = new
|
||||
{
|
||||
enabled = RenderSettings.fog,
|
||||
mode = RenderSettings.fogMode.ToString(),
|
||||
color = ColorToArray(RenderSettings.fogColor),
|
||||
density = RenderSettings.fogDensity,
|
||||
startDistance = RenderSettings.fogStartDistance,
|
||||
endDistance = RenderSettings.fogEndDistance
|
||||
},
|
||||
reflection = new
|
||||
{
|
||||
intensity = RenderSettings.reflectionIntensity,
|
||||
bounces = RenderSettings.reflectionBounces,
|
||||
mode = RenderSettings.defaultReflectionMode.ToString(),
|
||||
resolution = RenderSettings.defaultReflectionResolution,
|
||||
customCubemap = CustomReflectionTexture != null
|
||||
? AssetDatabase.GetAssetPath(CustomReflectionTexture)
|
||||
: null
|
||||
},
|
||||
sun = sun != null
|
||||
? (object)new { name = sun.gameObject.name, instanceID = sun.gameObject.GetInstanceIDCompat() }
|
||||
: null,
|
||||
subtractiveShadowColor = ColorToArray(RenderSettings.subtractiveShadowColor)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// skybox_set_material — assign a skybox material
|
||||
// ---------------------------------------------------------------
|
||||
public static object SetMaterial(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string materialPath = p.Get("material") ?? p.Get("path") ?? p.Get("material_path");
|
||||
if (string.IsNullOrEmpty(materialPath))
|
||||
return new ErrorResponse("'material' (asset path) is required.");
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
|
||||
if (mat == null)
|
||||
return new ErrorResponse($"Material not found at '{materialPath}'.");
|
||||
|
||||
RenderSettings.skybox = mat;
|
||||
MarkSceneDirty();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Skybox set to '{mat.name}' (shader: {mat.shader.name}).",
|
||||
data = new
|
||||
{
|
||||
material = mat.name,
|
||||
shader = mat.shader.name,
|
||||
path = materialPath
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// skybox_set_properties — modify properties on the current skybox material
|
||||
// ---------------------------------------------------------------
|
||||
public static object SetMaterialProperties(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
var skyMat = RenderSettings.skybox;
|
||||
if (skyMat == null)
|
||||
return new ErrorResponse("No skybox material is set.");
|
||||
|
||||
var propsRaw = p.GetRaw("properties") ?? p.GetRaw("parameters");
|
||||
if (propsRaw == null || propsRaw.Type != JTokenType.Object)
|
||||
return new ErrorResponse("'properties' dict is required.");
|
||||
|
||||
var set = new List<string>();
|
||||
var failed = new List<string>();
|
||||
|
||||
foreach (var kvp in (JObject)propsRaw)
|
||||
{
|
||||
string propName = kvp.Key;
|
||||
if (!skyMat.HasProperty(propName))
|
||||
{
|
||||
string altName = "_" + propName;
|
||||
if (skyMat.HasProperty(altName))
|
||||
propName = altName;
|
||||
else
|
||||
{
|
||||
failed.Add(kvp.Key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (SetMaterialProperty(skyMat, propName, kvp.Value))
|
||||
set.Add(kvp.Key);
|
||||
else
|
||||
failed.Add(kvp.Key);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(skyMat);
|
||||
AssetDatabase.SaveAssets();
|
||||
MarkSceneDirty();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Set {set.Count} property(ies) on skybox material '{skyMat.name}'.",
|
||||
data = new { material = skyMat.name, set, failed }
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// skybox_set_ambient — set ambient lighting mode and colors
|
||||
// ---------------------------------------------------------------
|
||||
public static object SetAmbient(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
string modeStr = p.Get("ambient_mode") ?? p.Get("mode");
|
||||
if (!string.IsNullOrEmpty(modeStr))
|
||||
{
|
||||
if (Enum.TryParse<AmbientMode>(modeStr, true, out var mode))
|
||||
RenderSettings.ambientMode = mode;
|
||||
else
|
||||
return new ErrorResponse(
|
||||
$"Invalid ambient mode '{modeStr}'. Valid: Skybox, Trilight, Flat, Custom.");
|
||||
}
|
||||
|
||||
var skyColor = ParseColorToken(p.GetRaw("color") ?? p.GetRaw("sky_color"));
|
||||
if (skyColor.HasValue)
|
||||
RenderSettings.ambientSkyColor = skyColor.Value;
|
||||
|
||||
var equatorColor = ParseColorToken(p.GetRaw("equator_color"));
|
||||
if (equatorColor.HasValue)
|
||||
RenderSettings.ambientEquatorColor = equatorColor.Value;
|
||||
|
||||
var groundColor = ParseColorToken(p.GetRaw("ground_color"));
|
||||
if (groundColor.HasValue)
|
||||
RenderSettings.ambientGroundColor = groundColor.Value;
|
||||
|
||||
var intensity = p.GetFloat("intensity");
|
||||
if (intensity.HasValue)
|
||||
RenderSettings.ambientIntensity = intensity.Value;
|
||||
|
||||
MarkSceneDirty();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Ambient lighting updated (mode: {RenderSettings.ambientMode}).",
|
||||
data = new
|
||||
{
|
||||
mode = RenderSettings.ambientMode.ToString(),
|
||||
skyColor = ColorToArray(RenderSettings.ambientSkyColor),
|
||||
equatorColor = ColorToArray(RenderSettings.ambientEquatorColor),
|
||||
groundColor = ColorToArray(RenderSettings.ambientGroundColor),
|
||||
intensity = RenderSettings.ambientIntensity
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// skybox_set_fog — enable/configure fog
|
||||
// ---------------------------------------------------------------
|
||||
public static object SetFog(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var enabledToken = p.GetRaw("fog_enabled") ?? p.GetRaw("enabled");
|
||||
if (enabledToken != null && enabledToken.Type != JTokenType.Null)
|
||||
RenderSettings.fog = ParamCoercion.CoerceBool(enabledToken, RenderSettings.fog);
|
||||
|
||||
string modeStr = p.Get("fog_mode") ?? p.Get("mode");
|
||||
if (!string.IsNullOrEmpty(modeStr))
|
||||
{
|
||||
if (Enum.TryParse<FogMode>(modeStr, true, out var fogMode))
|
||||
RenderSettings.fogMode = fogMode;
|
||||
else
|
||||
return new ErrorResponse(
|
||||
$"Invalid fog mode '{modeStr}'. Valid: Linear, Exponential, ExponentialSquared.");
|
||||
}
|
||||
|
||||
var fogColor = ParseColorToken(p.GetRaw("fog_color") ?? p.GetRaw("color"));
|
||||
if (fogColor.HasValue)
|
||||
RenderSettings.fogColor = fogColor.Value;
|
||||
|
||||
var density = p.GetFloat("fog_density") ?? p.GetFloat("density");
|
||||
if (density.HasValue)
|
||||
RenderSettings.fogDensity = density.Value;
|
||||
|
||||
var start = p.GetFloat("fog_start") ?? p.GetFloat("start");
|
||||
if (start.HasValue)
|
||||
RenderSettings.fogStartDistance = start.Value;
|
||||
|
||||
var end = p.GetFloat("fog_end") ?? p.GetFloat("end");
|
||||
if (end.HasValue)
|
||||
RenderSettings.fogEndDistance = end.Value;
|
||||
|
||||
MarkSceneDirty();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Fog settings updated (enabled: {RenderSettings.fog}, mode: {RenderSettings.fogMode}).",
|
||||
data = new
|
||||
{
|
||||
enabled = RenderSettings.fog,
|
||||
mode = RenderSettings.fogMode.ToString(),
|
||||
color = ColorToArray(RenderSettings.fogColor),
|
||||
density = RenderSettings.fogDensity,
|
||||
startDistance = RenderSettings.fogStartDistance,
|
||||
endDistance = RenderSettings.fogEndDistance
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// skybox_set_reflection — configure environment reflections
|
||||
// ---------------------------------------------------------------
|
||||
public static object SetReflection(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var intensity = p.GetFloat("intensity");
|
||||
if (intensity.HasValue)
|
||||
RenderSettings.reflectionIntensity = intensity.Value;
|
||||
|
||||
var bounces = p.GetInt("bounces");
|
||||
if (bounces.HasValue)
|
||||
RenderSettings.reflectionBounces = bounces.Value;
|
||||
|
||||
string modeStr = p.Get("reflection_mode") ?? p.Get("mode");
|
||||
if (!string.IsNullOrEmpty(modeStr))
|
||||
{
|
||||
if (Enum.TryParse<DefaultReflectionMode>(modeStr, true, out var mode))
|
||||
RenderSettings.defaultReflectionMode = mode;
|
||||
else
|
||||
return new ErrorResponse(
|
||||
$"Invalid reflection mode '{modeStr}'. Valid: Skybox, Custom.");
|
||||
}
|
||||
|
||||
var resolution = p.GetInt("resolution");
|
||||
if (resolution.HasValue)
|
||||
RenderSettings.defaultReflectionResolution = resolution.Value;
|
||||
|
||||
string cubemapPath = p.Get("path") ?? p.Get("cubemap_path");
|
||||
if (!string.IsNullOrEmpty(cubemapPath))
|
||||
{
|
||||
var cubemap = AssetDatabase.LoadAssetAtPath<Texture>(cubemapPath);
|
||||
if (cubemap != null)
|
||||
CustomReflectionTexture = cubemap;
|
||||
else
|
||||
return new ErrorResponse($"Cubemap not found at '{cubemapPath}'.");
|
||||
}
|
||||
|
||||
MarkSceneDirty();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Reflection settings updated (intensity: {RenderSettings.reflectionIntensity}, bounces: {RenderSettings.reflectionBounces}).",
|
||||
data = new
|
||||
{
|
||||
intensity = RenderSettings.reflectionIntensity,
|
||||
bounces = RenderSettings.reflectionBounces,
|
||||
mode = RenderSettings.defaultReflectionMode.ToString(),
|
||||
resolution = RenderSettings.defaultReflectionResolution,
|
||||
customCubemap = CustomReflectionTexture != null
|
||||
? AssetDatabase.GetAssetPath(CustomReflectionTexture)
|
||||
: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// skybox_set_sun — set the sun source light
|
||||
// ---------------------------------------------------------------
|
||||
public static object SetSun(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string target = p.Get("target") ?? p.Get("name");
|
||||
if (string.IsNullOrEmpty(target))
|
||||
return new ErrorResponse("'target' (light GameObject name or instance ID) is required.");
|
||||
|
||||
GameObject go = null;
|
||||
if (int.TryParse(target, out int instanceId))
|
||||
go = GameObjectLookup.ResolveInstanceID(instanceId) as GameObject;
|
||||
if (go == null)
|
||||
go = GameObject.Find(target);
|
||||
if (go == null)
|
||||
return new ErrorResponse($"GameObject '{target}' not found.");
|
||||
|
||||
var light = go.GetComponent<Light>();
|
||||
if (light == null)
|
||||
return new ErrorResponse($"'{go.name}' does not have a Light component.");
|
||||
|
||||
RenderSettings.sun = light;
|
||||
MarkSceneDirty();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Sun source set to '{go.name}'.",
|
||||
data = new
|
||||
{
|
||||
name = go.name,
|
||||
instanceID = go.GetInstanceIDCompat(),
|
||||
lightType = light.type.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private static float[] ColorToArray(Color c)
|
||||
{
|
||||
return new[] { c.r, c.g, c.b, c.a };
|
||||
}
|
||||
|
||||
private static Color ArrayToColor(float[] arr)
|
||||
{
|
||||
return new Color(
|
||||
arr[0], arr[1], arr[2],
|
||||
arr.Length >= 4 ? arr[3] : 1f);
|
||||
}
|
||||
|
||||
private static Color? ParseColorToken(JToken token)
|
||||
{
|
||||
if (token == null || token.Type == JTokenType.Null) return null;
|
||||
if (token is JArray arr && arr.Count >= 3)
|
||||
{
|
||||
return new Color(
|
||||
(float)arr[0], (float)arr[1], (float)arr[2],
|
||||
arr.Count >= 4 ? (float)arr[3] : 1f);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object ReadMaterialProperty(Material mat, string propName, ShaderPropertyType propType)
|
||||
{
|
||||
switch (propType)
|
||||
{
|
||||
case ShaderPropertyType.Color:
|
||||
return ColorToArray(mat.GetColor(propName));
|
||||
case ShaderPropertyType.Float:
|
||||
case ShaderPropertyType.Range:
|
||||
return mat.GetFloat(propName);
|
||||
case ShaderPropertyType.Int:
|
||||
return mat.GetInt(propName);
|
||||
case ShaderPropertyType.Vector:
|
||||
var v = mat.GetVector(propName);
|
||||
return new[] { v.x, v.y, v.z, v.w };
|
||||
case ShaderPropertyType.Texture:
|
||||
var tex = mat.GetTexture(propName);
|
||||
return tex != null ? AssetDatabase.GetAssetPath(tex) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool SetMaterialProperty(Material mat, string propName, JToken value)
|
||||
{
|
||||
int propIdx = mat.shader.FindPropertyIndex(propName);
|
||||
if (propIdx < 0) return false;
|
||||
|
||||
var propType = mat.shader.GetPropertyType(propIdx);
|
||||
try
|
||||
{
|
||||
switch (propType)
|
||||
{
|
||||
case ShaderPropertyType.Color:
|
||||
if (value is JArray colorArr && colorArr.Count >= 3)
|
||||
{
|
||||
mat.SetColor(propName, new Color(
|
||||
(float)colorArr[0], (float)colorArr[1], (float)colorArr[2],
|
||||
colorArr.Count >= 4 ? (float)colorArr[3] : 1f));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case ShaderPropertyType.Float:
|
||||
case ShaderPropertyType.Range:
|
||||
mat.SetFloat(propName, (float)value);
|
||||
return true;
|
||||
case ShaderPropertyType.Int:
|
||||
mat.SetInt(propName, (int)value);
|
||||
return true;
|
||||
case ShaderPropertyType.Vector:
|
||||
if (value is JArray vecArr && vecArr.Count >= 2)
|
||||
{
|
||||
mat.SetVector(propName, new Vector4(
|
||||
(float)vecArr[0], (float)vecArr[1],
|
||||
vecArr.Count >= 3 ? (float)vecArr[2] : 0f,
|
||||
vecArr.Count >= 4 ? (float)vecArr[3] : 0f));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case ShaderPropertyType.Texture:
|
||||
if (value.Type == JTokenType.String)
|
||||
{
|
||||
var tex = AssetDatabase.LoadAssetAtPath<Texture>(value.ToString());
|
||||
if (tex != null) { mat.SetTexture(propName, tex); return true; }
|
||||
}
|
||||
else if (value.Type == JTokenType.Null)
|
||||
{
|
||||
mat.SetTexture(propName, null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void MarkSceneDirty()
|
||||
{
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(
|
||||
UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 93b73ba8237dee84088417864958084a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,705 @@
|
||||
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 MCPForUnity.Runtime.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Graphics
|
||||
{
|
||||
internal static class VolumeOps
|
||||
{
|
||||
// === volume_create ===
|
||||
// Params: name (string), is_global (bool, default true), weight (float, default 1),
|
||||
// priority (float, default 0), profile_path (string, optional - path to save VolumeProfile asset),
|
||||
// effects (array of {type, ...params}, optional - effects to add immediately)
|
||||
internal static object CreateVolume(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string name = p.Get("name") ?? "Volume";
|
||||
bool isGlobal = p.GetBool("is_global", true);
|
||||
float weight = p.GetFloat("weight") ?? 1.0f;
|
||||
float priority = p.GetFloat("priority") ?? 0f;
|
||||
string profilePath = p.Get("profile_path");
|
||||
if (!string.IsNullOrEmpty(profilePath))
|
||||
{
|
||||
if (!profilePath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) &&
|
||||
!profilePath.StartsWith("Assets\\", StringComparison.OrdinalIgnoreCase))
|
||||
profilePath = "Assets/" + profilePath;
|
||||
if (!profilePath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase))
|
||||
profilePath += ".asset";
|
||||
}
|
||||
|
||||
var go = new GameObject(name);
|
||||
Undo.RegisterCreatedObjectUndo(go, $"Create Volume '{name}'");
|
||||
|
||||
// Add Volume component via reflection
|
||||
var volumeComp = go.AddComponent(GraphicsHelpers.VolumeType);
|
||||
|
||||
// Set properties via reflection
|
||||
SetProperty(volumeComp, "isGlobal", isGlobal);
|
||||
SetProperty(volumeComp, "weight", weight);
|
||||
SetProperty(volumeComp, "priority", priority);
|
||||
|
||||
// Create or load VolumeProfile
|
||||
object profile;
|
||||
if (!string.IsNullOrEmpty(profilePath))
|
||||
{
|
||||
// Load existing or create new profile asset
|
||||
profile = AssetDatabase.LoadAssetAtPath(profilePath, GraphicsHelpers.VolumeProfileType);
|
||||
if (profile == null)
|
||||
{
|
||||
profile = ScriptableObject.CreateInstance(GraphicsHelpers.VolumeProfileType);
|
||||
// Ensure directory exists
|
||||
var dir = System.IO.Path.GetDirectoryName(profilePath);
|
||||
if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir))
|
||||
System.IO.Directory.CreateDirectory(dir);
|
||||
AssetDatabase.CreateAsset((UnityEngine.Object)profile, profilePath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create embedded profile (not saved as asset)
|
||||
profile = ScriptableObject.CreateInstance(GraphicsHelpers.VolumeProfileType);
|
||||
}
|
||||
|
||||
// Assign profile (sharedProfile is a public field, handled by SetProperty's field fallback)
|
||||
SetProperty(volumeComp, "sharedProfile", profile);
|
||||
|
||||
// Add initial effects if provided
|
||||
var effectsToken = p.GetRaw("effects") as JArray;
|
||||
var addedEffects = new List<string>();
|
||||
if (effectsToken != null)
|
||||
{
|
||||
foreach (var effectDef in effectsToken)
|
||||
{
|
||||
if (effectDef is JObject effectObj)
|
||||
{
|
||||
string effectType = ParamCoercion.CoerceString(effectObj["type"], null);
|
||||
if (string.IsNullOrEmpty(effectType)) continue;
|
||||
|
||||
var type = GraphicsHelpers.ResolveVolumeComponentType(effectType);
|
||||
if (type == null) continue;
|
||||
|
||||
// profile.Add(type, true)
|
||||
var addMethod = GraphicsHelpers.VolumeProfileType.GetMethod("Add",
|
||||
new[] { typeof(Type), typeof(bool) });
|
||||
if (addMethod != null)
|
||||
{
|
||||
var component = addMethod.Invoke(profile, new object[] { type, true });
|
||||
if (component != null)
|
||||
{
|
||||
// Set parameters — support both nested {"parameters": {...}} and flat fields
|
||||
var paramObj = effectObj["parameters"] as JObject;
|
||||
if (paramObj != null)
|
||||
{
|
||||
foreach (var pp in paramObj.Properties())
|
||||
SetVolumeParameter(component, pp.Name, pp.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var prop in effectObj.Properties())
|
||||
{
|
||||
if (prop.Name == "type") continue;
|
||||
SetVolumeParameter(component, prop.Name, prop.Value);
|
||||
}
|
||||
}
|
||||
addedEffects.Add(effectType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (profile is UnityEngine.Object profileObj)
|
||||
EditorUtility.SetDirty(profileObj);
|
||||
}
|
||||
|
||||
GraphicsHelpers.MarkDirty(volumeComp);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created {(isGlobal ? "global" : "local")} Volume '{name}'" +
|
||||
(addedEffects.Count > 0 ? $" with effects: {string.Join(", ", addedEffects)}" : ""),
|
||||
data = new
|
||||
{
|
||||
instanceID = go.GetInstanceIDCompat(),
|
||||
isGlobal,
|
||||
weight,
|
||||
priority,
|
||||
profilePath = profilePath ?? "(embedded)",
|
||||
effects = addedEffects
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === volume_add_effect ===
|
||||
// Params: target (string/int), effect (string - type name like "Bloom")
|
||||
internal static object AddEffect(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string effectName = p.Get("effect");
|
||||
if (string.IsNullOrEmpty(effectName))
|
||||
return new ErrorResponse("'effect' parameter is required (e.g., 'Bloom', 'Vignette').");
|
||||
|
||||
var volume = GraphicsHelpers.FindVolume(@params);
|
||||
if (volume == null)
|
||||
return new ErrorResponse("Volume not found. Specify 'target' (name or instance ID).");
|
||||
|
||||
var effectType = GraphicsHelpers.ResolveVolumeComponentType(effectName);
|
||||
if (effectType == null)
|
||||
{
|
||||
var available = GraphicsHelpers.GetAvailableEffectTypes()
|
||||
.Select(t => t.Name).ToList();
|
||||
return new ErrorResponse(
|
||||
$"Effect type '{effectName}' not found. Available: {string.Join(", ", available.Take(20))}");
|
||||
}
|
||||
|
||||
var profile = GetProperty(volume, "sharedProfile");
|
||||
if (profile == null)
|
||||
return new ErrorResponse("Volume has no profile assigned.");
|
||||
|
||||
// Check if effect already exists
|
||||
var components = GetProperty(profile, "components") as System.Collections.IList;
|
||||
if (components != null)
|
||||
{
|
||||
foreach (var comp in components)
|
||||
{
|
||||
if (comp != null && comp.GetType() == effectType)
|
||||
return new ErrorResponse($"Effect '{effectName}' already exists on this Volume. Use volume_set_effect to modify it.");
|
||||
}
|
||||
}
|
||||
|
||||
// profile.Add(effectType, true) -- 'true' means override all params
|
||||
var addMethod = GraphicsHelpers.VolumeProfileType.GetMethod("Add",
|
||||
new[] { typeof(Type), typeof(bool) });
|
||||
if (addMethod == null)
|
||||
return new ErrorResponse("Could not find VolumeProfile.Add method.");
|
||||
|
||||
var component = addMethod.Invoke(profile, new object[] { effectType, true });
|
||||
if (component == null)
|
||||
return new ErrorResponse($"Failed to add effect '{effectName}'.");
|
||||
|
||||
if (profile is UnityEngine.Object profileObj)
|
||||
EditorUtility.SetDirty(profileObj);
|
||||
GraphicsHelpers.MarkDirty(volume);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added '{effectName}' to Volume '{(volume as Component)?.gameObject.name}'.",
|
||||
data = new { effect = effectName, volumeInstanceID = (volume as Component)?.gameObject.GetInstanceIDCompat() }
|
||||
};
|
||||
}
|
||||
|
||||
// === volume_set_effect ===
|
||||
// Params: target (string/int), effect (string), parameters (dict of field->value)
|
||||
internal static object SetEffect(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string effectName = p.Get("effect");
|
||||
if (string.IsNullOrEmpty(effectName))
|
||||
return new ErrorResponse("'effect' parameter is required.");
|
||||
|
||||
var volume = GraphicsHelpers.FindVolume(@params);
|
||||
if (volume == null)
|
||||
return new ErrorResponse("Volume not found. Specify 'target'.");
|
||||
|
||||
var profile = GetProperty(volume, "sharedProfile");
|
||||
if (profile == null)
|
||||
return new ErrorResponse("Volume has no profile assigned.");
|
||||
|
||||
var effectType = GraphicsHelpers.ResolveVolumeComponentType(effectName);
|
||||
if (effectType == null)
|
||||
return new ErrorResponse($"Effect type '{effectName}' not found.");
|
||||
|
||||
// Find the effect component in the profile
|
||||
var components = GetProperty(profile, "components") as System.Collections.IList;
|
||||
if (components == null)
|
||||
return new ErrorResponse("Could not read profile components.");
|
||||
|
||||
object targetComponent = null;
|
||||
foreach (var comp in components)
|
||||
{
|
||||
if (comp != null && comp.GetType() == effectType)
|
||||
{
|
||||
targetComponent = comp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetComponent == null)
|
||||
return new ErrorResponse($"Effect '{effectName}' not found on this Volume. Use volume_add_effect first.");
|
||||
|
||||
// Set parameters
|
||||
var parameters = p.GetRaw("parameters") as JObject;
|
||||
if (parameters == null)
|
||||
return new ErrorResponse("'parameters' dict is required.");
|
||||
|
||||
var setParams = new List<string>();
|
||||
var failedParams = new List<string>();
|
||||
foreach (var prop in parameters.Properties())
|
||||
{
|
||||
if (SetVolumeParameter(targetComponent, prop.Name, prop.Value))
|
||||
setParams.Add(prop.Name);
|
||||
else
|
||||
failedParams.Add(prop.Name);
|
||||
}
|
||||
|
||||
if (profile is UnityEngine.Object profileObj)
|
||||
EditorUtility.SetDirty(profileObj);
|
||||
|
||||
var msg = $"Set {setParams.Count} parameter(s) on '{effectName}'";
|
||||
if (failedParams.Count > 0)
|
||||
msg += $". Failed: {string.Join(", ", failedParams)}";
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = msg,
|
||||
data = new { effect = effectName, set = setParams, failed = failedParams }
|
||||
};
|
||||
}
|
||||
|
||||
// === volume_remove_effect ===
|
||||
// Params: target, effect
|
||||
internal static object RemoveEffect(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string effectName = p.Get("effect");
|
||||
if (string.IsNullOrEmpty(effectName))
|
||||
return new ErrorResponse("'effect' parameter is required.");
|
||||
|
||||
var volume = GraphicsHelpers.FindVolume(@params);
|
||||
if (volume == null)
|
||||
return new ErrorResponse("Volume not found.");
|
||||
|
||||
var effectType = GraphicsHelpers.ResolveVolumeComponentType(effectName);
|
||||
if (effectType == null)
|
||||
return new ErrorResponse($"Effect type '{effectName}' not found.");
|
||||
|
||||
var profile = GetProperty(volume, "sharedProfile");
|
||||
if (profile == null)
|
||||
return new ErrorResponse("Volume has no profile.");
|
||||
|
||||
// Check if effect exists before removing
|
||||
bool found = false;
|
||||
var components = GetProperty(profile, "components") as System.Collections.IList;
|
||||
if (components != null)
|
||||
{
|
||||
foreach (var comp in components)
|
||||
{
|
||||
if (comp != null && comp.GetType() == effectType)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
return new ErrorResponse($"Effect '{effectName}' not found on this Volume.");
|
||||
|
||||
var removeMethod = GraphicsHelpers.VolumeProfileType.GetMethod("Remove",
|
||||
new[] { typeof(Type) });
|
||||
if (removeMethod == null)
|
||||
return new ErrorResponse("Could not find VolumeProfile.Remove method.");
|
||||
|
||||
removeMethod.Invoke(profile, new object[] { effectType });
|
||||
|
||||
if (profile is UnityEngine.Object profileObj)
|
||||
EditorUtility.SetDirty(profileObj);
|
||||
GraphicsHelpers.MarkDirty(volume);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Removed '{effectName}' from Volume.",
|
||||
data = new { effect = effectName }
|
||||
};
|
||||
}
|
||||
|
||||
// === volume_get_info ===
|
||||
// Params: target (optional -- if omitted, returns info for all volumes)
|
||||
internal static object GetInfo(JObject @params)
|
||||
{
|
||||
var volume = GraphicsHelpers.FindVolume(@params);
|
||||
if (volume == null)
|
||||
return new ErrorResponse("Volume not found.");
|
||||
|
||||
var info = BuildVolumeInfo(volume);
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Volume info for '{(volume as Component)?.gameObject.name}'.",
|
||||
data = info
|
||||
};
|
||||
}
|
||||
|
||||
// === volume_set_properties ===
|
||||
// Params: target, weight, priority, is_global, blend_distance
|
||||
// OR properties dict with those keys
|
||||
internal static object SetProperties(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
var volume = GraphicsHelpers.FindVolume(@params);
|
||||
if (volume == null)
|
||||
return new ErrorResponse("Volume not found.");
|
||||
|
||||
// Unpack "properties" dict into top-level params so callers can use either style
|
||||
var propsDict = p.GetRaw("properties") as JObject;
|
||||
if (propsDict != null)
|
||||
{
|
||||
foreach (var prop in propsDict.Properties())
|
||||
{
|
||||
if (@params[prop.Name] == null)
|
||||
@params[prop.Name] = prop.Value;
|
||||
}
|
||||
p = new ToolParams(@params);
|
||||
}
|
||||
|
||||
var changed = new List<string>();
|
||||
|
||||
var weight = p.GetFloat("weight");
|
||||
if (weight.HasValue) { SetProperty(volume, "weight", weight.Value); changed.Add("weight"); }
|
||||
|
||||
var priority = p.GetFloat("priority");
|
||||
if (priority.HasValue) { SetProperty(volume, "priority", priority.Value); changed.Add("priority"); }
|
||||
|
||||
if (p.Has("is_global")) { SetProperty(volume, "isGlobal", p.GetBool("is_global")); changed.Add("isGlobal"); }
|
||||
|
||||
var blendDist = p.GetFloat("blend_distance");
|
||||
if (blendDist.HasValue) { SetProperty(volume, "blendDistance", blendDist.Value); changed.Add("blendDistance"); }
|
||||
|
||||
if (changed.Count == 0)
|
||||
return new ErrorResponse("No properties specified. Use: weight, priority, is_global, blend_distance.");
|
||||
|
||||
GraphicsHelpers.MarkDirty(volume);
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Updated Volume properties: {string.Join(", ", changed)}",
|
||||
data = new { changed }
|
||||
};
|
||||
}
|
||||
|
||||
// === volume_list_effects ===
|
||||
// No params needed -- lists all available VolumeComponent types
|
||||
internal static object ListEffects(JObject @params)
|
||||
{
|
||||
var types = GraphicsHelpers.GetAvailableEffectTypes();
|
||||
var effectList = types.Select(t => new
|
||||
{
|
||||
name = t.Name,
|
||||
fullName = t.FullName,
|
||||
ns = t.Namespace
|
||||
}).ToList();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Found {effectList.Count} available volume effects.",
|
||||
data = new { pipeline = GraphicsHelpers.GetPipelineName(), effects = effectList }
|
||||
};
|
||||
}
|
||||
|
||||
// === volume_create_profile ===
|
||||
// Params: path (string -- asset path like "Assets/Settings/MyProfile.asset")
|
||||
internal static object CreateProfile(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string path = p.Get("path");
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return new ErrorResponse("'path' parameter is required (e.g., 'Settings/MyProfile' or 'Assets/Settings/MyProfile.asset').");
|
||||
|
||||
// Auto-prepend Assets/ if missing (paths are relative to Assets/ by convention)
|
||||
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) &&
|
||||
!path.StartsWith("Assets\\", StringComparison.OrdinalIgnoreCase))
|
||||
path = "Assets/" + path;
|
||||
|
||||
if (!path.EndsWith(".asset", StringComparison.OrdinalIgnoreCase))
|
||||
path += ".asset";
|
||||
|
||||
// Ensure directory exists
|
||||
var dir = System.IO.Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(dir) && !AssetDatabase.IsValidFolder(dir))
|
||||
{
|
||||
// Create folders recursively
|
||||
var parts = dir.Replace("\\", "/").Split('/');
|
||||
string current = parts[0];
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = current + "/" + parts[i];
|
||||
if (!AssetDatabase.IsValidFolder(next))
|
||||
AssetDatabase.CreateFolder(current, parts[i]);
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
var profile = ScriptableObject.CreateInstance(GraphicsHelpers.VolumeProfileType);
|
||||
AssetDatabase.CreateAsset(profile, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created VolumeProfile at '{path}'.",
|
||||
data = new { path }
|
||||
};
|
||||
}
|
||||
|
||||
// === ListVolumes (used by VolumesResource) ===
|
||||
internal static object ListVolumes(JObject @params)
|
||||
{
|
||||
if (!GraphicsHelpers.HasVolumeSystem)
|
||||
return new { success = true, message = "Volume system not available.", data = new { volumes = new List<object>() } };
|
||||
|
||||
var allVolumes = UnityFindObjectsCompat.FindAll(GraphicsHelpers.VolumeType);
|
||||
var volumeList = new List<object>();
|
||||
|
||||
foreach (Component vol in allVolumes)
|
||||
{
|
||||
volumeList.Add(BuildVolumeInfo(vol));
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Found {volumeList.Count} volume(s).",
|
||||
data = new { pipeline = GraphicsHelpers.GetPipelineName(), volumes = volumeList }
|
||||
};
|
||||
}
|
||||
|
||||
// --- Helper: Build info object for a single Volume ---
|
||||
private static object BuildVolumeInfo(object volumeComponent)
|
||||
{
|
||||
var comp = volumeComponent as Component;
|
||||
if (comp == null) return null;
|
||||
|
||||
bool isGlobal = GetPropertyValue<bool>(volumeComponent, "isGlobal", true);
|
||||
float weight = GetPropertyValue<float>(volumeComponent, "weight", 1f);
|
||||
float priority = GetPropertyValue<float>(volumeComponent, "priority", 0f);
|
||||
float blendDistance = GetPropertyValue<float>(volumeComponent, "blendDistance", 0f);
|
||||
|
||||
var profile = GetProperty(volumeComponent, "sharedProfile");
|
||||
string profileName = profile is UnityEngine.Object profileObj2 ? profileObj2.name : null;
|
||||
string profilePath = profile is UnityEngine.Object po ? AssetDatabase.GetAssetPath(po) : null;
|
||||
|
||||
var effectsList = new List<object>();
|
||||
if (profile != null)
|
||||
{
|
||||
var components = GetProperty(profile, "components") as System.Collections.IList;
|
||||
if (components != null)
|
||||
{
|
||||
foreach (var effect in components)
|
||||
{
|
||||
if (effect == null) continue;
|
||||
var effectType = effect.GetType();
|
||||
bool active = GetPropertyValue<bool>(effect, "active", true);
|
||||
|
||||
// Collect overridden parameters
|
||||
var overriddenParams = new List<string>();
|
||||
foreach (var field in effectType.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var fieldValue = field.GetValue(effect);
|
||||
if (fieldValue == null) continue;
|
||||
var overrideProp = fieldValue.GetType().GetProperty("overrideState");
|
||||
if (overrideProp != null)
|
||||
{
|
||||
bool overridden = (bool)overrideProp.GetValue(fieldValue);
|
||||
if (overridden)
|
||||
overriddenParams.Add(field.Name);
|
||||
}
|
||||
}
|
||||
|
||||
effectsList.Add(new
|
||||
{
|
||||
type = effectType.Name,
|
||||
active,
|
||||
overridden_params = overriddenParams
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
name = comp.gameObject.name,
|
||||
instance_id = comp.gameObject.GetInstanceIDCompat(),
|
||||
is_global = isGlobal,
|
||||
weight,
|
||||
priority,
|
||||
blend_distance = blendDistance,
|
||||
profile = profileName,
|
||||
profile_path = profilePath ?? "",
|
||||
effects = effectsList
|
||||
};
|
||||
}
|
||||
|
||||
// --- Helper: Set a VolumeParameter field value via reflection ---
|
||||
// VolumeParameter<T> has: overrideState (bool), value (T)
|
||||
internal static bool SetVolumeParameter(object component, string fieldName, JToken value)
|
||||
{
|
||||
if (component == null || string.IsNullOrEmpty(fieldName)) return false;
|
||||
|
||||
var field = component.GetType().GetField(fieldName,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
if (field == null)
|
||||
{
|
||||
// Try camelCase conversion from snake_case
|
||||
string camelCase = StringCaseUtility.ToCamelCase(fieldName);
|
||||
field = component.GetType().GetField(camelCase,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
}
|
||||
if (field == null) return false;
|
||||
|
||||
var param = field.GetValue(component);
|
||||
if (param == null) return false;
|
||||
|
||||
// Set value with type conversion, then enable override on success
|
||||
var valueProp = param.GetType().GetProperty("value");
|
||||
if (valueProp == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
object converted = ConvertToParameterType(value, valueProp.PropertyType);
|
||||
valueProp.SetValue(param, converted);
|
||||
|
||||
var overrideProp = param.GetType().GetProperty("overrideState");
|
||||
if (overrideProp != null)
|
||||
overrideProp.SetValue(param, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[VolumeOps] Failed to set '{fieldName}': {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper: Convert JToken to target parameter type ---
|
||||
private static object ConvertToParameterType(JToken value, Type targetType)
|
||||
{
|
||||
if (value == null || value.Type == JTokenType.Null) return null;
|
||||
|
||||
// Handle Color
|
||||
if (targetType == typeof(Color))
|
||||
{
|
||||
if (value is JArray arr && arr.Count >= 3)
|
||||
{
|
||||
float r = arr[0].Value<float>();
|
||||
float g = arr[1].Value<float>();
|
||||
float b = arr[2].Value<float>();
|
||||
float a = arr.Count >= 4 ? arr[3].Value<float>() : 1f;
|
||||
return new Color(r, g, b, a);
|
||||
}
|
||||
// Try hex string
|
||||
if (value.Type == JTokenType.String)
|
||||
{
|
||||
if (ColorUtility.TryParseHtmlString(value.ToString(), out Color c))
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Vector2
|
||||
if (targetType == typeof(Vector2))
|
||||
{
|
||||
if (value is JArray arr && arr.Count >= 2)
|
||||
return new Vector2(arr[0].Value<float>(), arr[1].Value<float>());
|
||||
}
|
||||
|
||||
// Handle Vector3
|
||||
if (targetType == typeof(Vector3))
|
||||
{
|
||||
if (value is JArray arr && arr.Count >= 3)
|
||||
return new Vector3(arr[0].Value<float>(), arr[1].Value<float>(), arr[2].Value<float>());
|
||||
}
|
||||
|
||||
// Handle Vector4
|
||||
if (targetType == typeof(Vector4))
|
||||
{
|
||||
if (value is JArray arr && arr.Count >= 4)
|
||||
return new Vector4(arr[0].Value<float>(), arr[1].Value<float>(),
|
||||
arr[2].Value<float>(), arr[3].Value<float>());
|
||||
}
|
||||
|
||||
// Handle enums
|
||||
if (targetType.IsEnum)
|
||||
{
|
||||
string str = value.ToString();
|
||||
if (Enum.TryParse(targetType, str, true, out object enumVal))
|
||||
return enumVal;
|
||||
// Try as int
|
||||
if (int.TryParse(str, out int intVal))
|
||||
return Enum.ToObject(targetType, intVal);
|
||||
}
|
||||
|
||||
// Handle bool
|
||||
if (targetType == typeof(bool))
|
||||
return ParamCoercion.CoerceBool(value, false);
|
||||
|
||||
// Handle float
|
||||
if (targetType == typeof(float))
|
||||
return ParamCoercion.CoerceFloat(value, 0f);
|
||||
|
||||
// Handle int
|
||||
if (targetType == typeof(int))
|
||||
return ParamCoercion.CoerceInt(value, 0);
|
||||
|
||||
// Handle Texture2D (by asset path)
|
||||
if (targetType == typeof(Texture2D) || targetType == typeof(Texture))
|
||||
{
|
||||
string path = value.ToString();
|
||||
return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
||||
}
|
||||
|
||||
// Fallback: try Convert
|
||||
try
|
||||
{
|
||||
return Convert.ChangeType(value.ToObject<object>(), targetType);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value.ToObject<object>();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reflection helpers (with field fallback for Volume.sharedProfile etc.) ---
|
||||
private static object GetProperty(object obj, string name)
|
||||
{
|
||||
if (obj == null) return null;
|
||||
var prop = obj.GetType().GetProperty(name,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop != null) return prop.GetValue(obj);
|
||||
// Fallback: try as a field (e.g., Volume.sharedProfile is a public field, not a property)
|
||||
var field = obj.GetType().GetField(name,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
return field?.GetValue(obj);
|
||||
}
|
||||
|
||||
private static T GetPropertyValue<T>(object obj, string name, T defaultValue)
|
||||
{
|
||||
var val = GetProperty(obj, name);
|
||||
if (val is T typed) return typed;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static void SetProperty(object obj, string name, object value)
|
||||
{
|
||||
if (obj == null) return;
|
||||
var prop = obj.GetType().GetProperty(name,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop != null && prop.CanWrite)
|
||||
{
|
||||
prop.SetValue(obj, value);
|
||||
return;
|
||||
}
|
||||
// Fallback: try as a field (e.g., Volume.sharedProfile is a public field, not a property)
|
||||
var field = obj.GetType().GetField(name,
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
field?.SetValue(obj, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9cc9cf9a8664f5bbefa277a02e020fc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user