Files
2026-07-13 12:49:17 +08:00

578 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using MCPForUnity.Runtime.Helpers;
namespace MCPForUnity.Editor.Tools.Graphics
{
internal static class 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();
}
}
}
}
}