chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:17 +08:00
commit 7243d5823b
2201 changed files with 257291 additions and 0 deletions
@@ -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: