597 lines
23 KiB
C#
597 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using MCPForUnity.Editor.Helpers;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace MCPForUnity.Editor.Tools
|
|
{
|
|
[McpForUnityTool("manage_material", AutoRegister = false)]
|
|
public static class ManageMaterial
|
|
{
|
|
public static object HandleCommand(JObject @params)
|
|
{
|
|
string action = @params["action"]?.ToString();
|
|
if (string.IsNullOrEmpty(action))
|
|
{
|
|
return new ErrorResponse("Action is required");
|
|
}
|
|
|
|
try
|
|
{
|
|
switch (action)
|
|
{
|
|
case "ping":
|
|
return new SuccessResponse("pong", new { tool = "manage_material" });
|
|
|
|
case "create":
|
|
return CreateMaterial(@params);
|
|
|
|
case "set_material_shader_property":
|
|
return SetMaterialShaderProperty(@params);
|
|
|
|
case "set_material_color":
|
|
return SetMaterialColor(@params);
|
|
|
|
case "assign_material_to_renderer":
|
|
return AssignMaterialToRenderer(@params);
|
|
|
|
case "set_renderer_color":
|
|
return SetRendererColor(@params);
|
|
|
|
case "get_material_info":
|
|
return GetMaterialInfo(@params);
|
|
|
|
default:
|
|
return new ErrorResponse($"Unknown action: {action}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace });
|
|
}
|
|
}
|
|
|
|
private static string NormalizePath(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path)) return path;
|
|
|
|
// Normalize separators and ensure Assets/ root
|
|
path = AssetPathUtility.SanitizeAssetPath(path);
|
|
|
|
// Ensure .mat extension
|
|
if (!path.EndsWith(".mat", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
path += ".mat";
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
private static object SetMaterialShaderProperty(JObject @params)
|
|
{
|
|
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
|
string property = @params["property"]?.ToString();
|
|
JToken value = @params["value"];
|
|
|
|
if (string.IsNullOrEmpty(materialPath) || string.IsNullOrEmpty(property) || value == null)
|
|
{
|
|
return new ErrorResponse("materialPath, property, and value are required");
|
|
}
|
|
|
|
// Find material
|
|
var findInstruction = new JObject { ["find"] = materialPath };
|
|
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
|
|
|
|
if (mat == null)
|
|
{
|
|
return new ErrorResponse($"Could not find material at path: {materialPath}");
|
|
}
|
|
|
|
Undo.RecordObject(mat, "Set Material Property");
|
|
|
|
// Normalize alias/casing once for all code paths
|
|
property = MaterialOps.ResolvePropertyName(mat, property);
|
|
|
|
// 1. Try handling Texture instruction explicitly (ManageMaterial special feature)
|
|
if (value.Type == JTokenType.Object)
|
|
{
|
|
// Check if it looks like an instruction
|
|
if (value is JObject obj && (obj.ContainsKey("find") || obj.ContainsKey("method")))
|
|
{
|
|
Texture tex = ObjectResolver.Resolve(obj, typeof(Texture)) as Texture;
|
|
if (tex != null && mat.HasProperty(property))
|
|
{
|
|
mat.SetTexture(property, tex);
|
|
EditorUtility.SetDirty(mat);
|
|
return new SuccessResponse($"Set texture property {property} on {mat.name}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Fallback to standard logic via MaterialOps (handles Colors, Floats, Strings->Path)
|
|
bool success = MaterialOps.TrySetShaderProperty(mat, property, value, UnityJsonSerializer.Instance);
|
|
|
|
if (success)
|
|
{
|
|
EditorUtility.SetDirty(mat);
|
|
return new SuccessResponse($"Set property {property} on {mat.name}");
|
|
}
|
|
else
|
|
{
|
|
return new ErrorResponse($"Failed to set property {property}. Value format might be unsupported or texture not found.");
|
|
}
|
|
}
|
|
|
|
private static object SetMaterialColor(JObject @params)
|
|
{
|
|
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
|
JToken colorToken = @params["color"];
|
|
string property = @params["property"]?.ToString();
|
|
|
|
if (string.IsNullOrEmpty(materialPath) || colorToken == null)
|
|
{
|
|
return new ErrorResponse("materialPath and color are required");
|
|
}
|
|
|
|
var findInstruction = new JObject { ["find"] = materialPath };
|
|
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
|
|
|
|
if (mat == null)
|
|
{
|
|
return new ErrorResponse($"Could not find material at path: {materialPath}");
|
|
}
|
|
|
|
Color color;
|
|
try
|
|
{
|
|
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return new ErrorResponse($"Invalid color format: {e.Message}");
|
|
}
|
|
|
|
Undo.RecordObject(mat, "Set Material Color");
|
|
|
|
bool foundProp = false;
|
|
if (!string.IsNullOrEmpty(property))
|
|
{
|
|
if (mat.HasProperty(property))
|
|
{
|
|
mat.SetColor(property, color);
|
|
foundProp = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback logic: _BaseColor (URP/HDRP) then _Color (Built-in)
|
|
if (mat.HasProperty("_BaseColor"))
|
|
{
|
|
mat.SetColor("_BaseColor", color);
|
|
foundProp = true;
|
|
property = "_BaseColor";
|
|
}
|
|
else if (mat.HasProperty("_Color"))
|
|
{
|
|
mat.SetColor("_Color", color);
|
|
foundProp = true;
|
|
property = "_Color";
|
|
}
|
|
}
|
|
|
|
if (foundProp)
|
|
{
|
|
EditorUtility.SetDirty(mat);
|
|
return new SuccessResponse($"Set color on {property}");
|
|
}
|
|
else
|
|
{
|
|
return new ErrorResponse("Could not find suitable color property (_BaseColor or _Color) or specified property does not exist.");
|
|
}
|
|
}
|
|
|
|
private static object AssignMaterialToRenderer(JObject @params)
|
|
{
|
|
string target = @params["target"]?.ToString();
|
|
string searchMethod = @params["searchMethod"]?.ToString();
|
|
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
|
int slot = @params["slot"]?.ToObject<int>() ?? 0;
|
|
|
|
if (string.IsNullOrEmpty(target) || string.IsNullOrEmpty(materialPath))
|
|
{
|
|
return new ErrorResponse("target and materialPath are required");
|
|
}
|
|
|
|
var goInstruction = new JObject { ["find"] = target };
|
|
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
|
|
|
|
GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
|
|
if (go == null)
|
|
{
|
|
return new ErrorResponse($"Could not find target GameObject: {target}");
|
|
}
|
|
|
|
Renderer renderer = go.GetComponent<Renderer>();
|
|
if (renderer == null)
|
|
{
|
|
return new ErrorResponse($"GameObject {go.name} has no Renderer component");
|
|
}
|
|
|
|
var matInstruction = new JObject { ["find"] = materialPath };
|
|
Material mat = ObjectResolver.Resolve(matInstruction, typeof(Material)) as Material;
|
|
if (mat == null)
|
|
{
|
|
return new ErrorResponse($"Could not find material: {materialPath}");
|
|
}
|
|
|
|
Undo.RecordObject(renderer, "Assign Material");
|
|
|
|
Material[] sharedMats = renderer.sharedMaterials;
|
|
if (slot < 0 || slot >= sharedMats.Length)
|
|
{
|
|
return new ErrorResponse($"Slot {slot} out of bounds (count: {sharedMats.Length})");
|
|
}
|
|
|
|
sharedMats[slot] = mat;
|
|
renderer.sharedMaterials = sharedMats;
|
|
|
|
EditorUtility.SetDirty(renderer);
|
|
return new SuccessResponse($"Assigned material {mat.name} to {go.name} slot {slot}");
|
|
}
|
|
|
|
private static object SetRendererColor(JObject @params)
|
|
{
|
|
string target = @params["target"]?.ToString();
|
|
string searchMethod = @params["searchMethod"]?.ToString();
|
|
JToken colorToken = @params["color"];
|
|
int slot = @params["slot"]?.ToObject<int>() ?? 0;
|
|
string mode = @params["mode"]?.ToString() ?? "property_block";
|
|
|
|
if (string.IsNullOrEmpty(target) || colorToken == null)
|
|
{
|
|
return new ErrorResponse("target and color are required");
|
|
}
|
|
|
|
Color color;
|
|
try
|
|
{
|
|
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return new ErrorResponse($"Invalid color format: {e.Message}");
|
|
}
|
|
|
|
var goInstruction = new JObject { ["find"] = target };
|
|
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
|
|
|
|
GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
|
|
if (go == null)
|
|
{
|
|
return new ErrorResponse($"Could not find target GameObject: {target}");
|
|
}
|
|
|
|
Renderer renderer = go.GetComponent<Renderer>();
|
|
if (renderer == null)
|
|
{
|
|
return new ErrorResponse($"GameObject {go.name} has no Renderer component");
|
|
}
|
|
|
|
if (mode == "property_block")
|
|
{
|
|
if (slot < 0 || slot >= renderer.sharedMaterials.Length)
|
|
{
|
|
return new ErrorResponse($"Slot {slot} out of bounds (count: {renderer.sharedMaterials.Length})");
|
|
}
|
|
|
|
MaterialPropertyBlock block = new MaterialPropertyBlock();
|
|
renderer.GetPropertyBlock(block, slot);
|
|
|
|
if (renderer.sharedMaterials[slot] != null)
|
|
{
|
|
Material mat = renderer.sharedMaterials[slot];
|
|
if (mat.HasProperty("_BaseColor")) block.SetColor("_BaseColor", color);
|
|
else if (mat.HasProperty("_Color")) block.SetColor("_Color", color);
|
|
else block.SetColor("_Color", color);
|
|
}
|
|
else
|
|
{
|
|
block.SetColor("_Color", color);
|
|
}
|
|
|
|
renderer.SetPropertyBlock(block, slot);
|
|
EditorUtility.SetDirty(renderer);
|
|
return new SuccessResponse($"Set renderer color (PropertyBlock) on slot {slot}");
|
|
}
|
|
else if (mode == "shared")
|
|
{
|
|
if (slot >= 0 && slot < renderer.sharedMaterials.Length)
|
|
{
|
|
Material mat = renderer.sharedMaterials[slot];
|
|
if (mat == null)
|
|
{
|
|
return new ErrorResponse($"No material in slot {slot}");
|
|
}
|
|
Undo.RecordObject(mat, "Set Material Color");
|
|
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
|
|
else mat.SetColor("_Color", color);
|
|
EditorUtility.SetDirty(mat);
|
|
return new SuccessResponse("Set shared material color");
|
|
}
|
|
return new ErrorResponse("Invalid slot");
|
|
}
|
|
else if (mode == "instance")
|
|
{
|
|
if (slot >= 0 && slot < renderer.materials.Length)
|
|
{
|
|
Material mat = renderer.materials[slot];
|
|
if (mat == null)
|
|
{
|
|
return new ErrorResponse($"No material in slot {slot}");
|
|
}
|
|
// Note: Undo cannot fully revert material instantiation
|
|
Undo.RecordObject(mat, "Set Instance Material Color");
|
|
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
|
|
else mat.SetColor("_Color", color);
|
|
return new SuccessResponse("Set instance material color", new { warning = "Material instance created; Undo cannot fully revert instantiation." });
|
|
}
|
|
return new ErrorResponse("Invalid slot");
|
|
}
|
|
|
|
return new ErrorResponse($"Unknown mode: {mode}");
|
|
}
|
|
|
|
private static object GetMaterialInfo(JObject @params)
|
|
{
|
|
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
|
if (string.IsNullOrEmpty(materialPath))
|
|
{
|
|
return new ErrorResponse("materialPath is required");
|
|
}
|
|
|
|
var findInstruction = new JObject { ["find"] = materialPath };
|
|
Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material;
|
|
|
|
if (mat == null)
|
|
{
|
|
return new ErrorResponse($"Could not find material at path: {materialPath}");
|
|
}
|
|
|
|
Shader shader = mat.shader;
|
|
var properties = new List<object>();
|
|
|
|
#if UNITY_6000_0_OR_NEWER
|
|
int propertyCount = shader.GetPropertyCount();
|
|
for (int i = 0; i < propertyCount; i++)
|
|
{
|
|
string name = shader.GetPropertyName(i);
|
|
var type = shader.GetPropertyType(i);
|
|
string description = shader.GetPropertyDescription(i);
|
|
|
|
object currentValue = null;
|
|
try
|
|
{
|
|
if (mat.HasProperty(name))
|
|
{
|
|
switch (type)
|
|
{
|
|
case UnityEngine.Rendering.ShaderPropertyType.Color:
|
|
var c = mat.GetColor(name);
|
|
currentValue = new { r = c.r, g = c.g, b = c.b, a = c.a };
|
|
break;
|
|
case UnityEngine.Rendering.ShaderPropertyType.Vector:
|
|
var v = mat.GetVector(name);
|
|
currentValue = new { x = v.x, y = v.y, z = v.z, w = v.w };
|
|
break;
|
|
case UnityEngine.Rendering.ShaderPropertyType.Float:
|
|
case UnityEngine.Rendering.ShaderPropertyType.Range:
|
|
currentValue = mat.GetFloat(name);
|
|
break;
|
|
case UnityEngine.Rendering.ShaderPropertyType.Texture:
|
|
currentValue = mat.GetTexture(name)?.name ?? "null";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
currentValue = $"<error: {ex.Message}>";
|
|
}
|
|
|
|
properties.Add(new
|
|
{
|
|
name = name,
|
|
type = type.ToString(),
|
|
description = description,
|
|
value = currentValue
|
|
});
|
|
}
|
|
#else
|
|
int propertyCount = ShaderUtil.GetPropertyCount(shader);
|
|
for (int i = 0; i < propertyCount; i++)
|
|
{
|
|
string name = ShaderUtil.GetPropertyName(shader, i);
|
|
ShaderUtil.ShaderPropertyType type = ShaderUtil.GetPropertyType(shader, i);
|
|
string description = ShaderUtil.GetPropertyDescription(shader, i);
|
|
|
|
object currentValue = null;
|
|
try
|
|
{
|
|
if (mat.HasProperty(name))
|
|
{
|
|
switch (type)
|
|
{
|
|
case ShaderUtil.ShaderPropertyType.Color:
|
|
var c = mat.GetColor(name);
|
|
currentValue = new { r = c.r, g = c.g, b = c.b, a = c.a };
|
|
break;
|
|
case ShaderUtil.ShaderPropertyType.Vector:
|
|
var v = mat.GetVector(name);
|
|
currentValue = new { x = v.x, y = v.y, z = v.z, w = v.w };
|
|
break;
|
|
case ShaderUtil.ShaderPropertyType.Float: currentValue = mat.GetFloat(name); break;
|
|
case ShaderUtil.ShaderPropertyType.Range: currentValue = mat.GetFloat(name); break;
|
|
case ShaderUtil.ShaderPropertyType.TexEnv: currentValue = mat.GetTexture(name)?.name ?? "null"; break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
currentValue = $"<error: {ex.Message}>";
|
|
}
|
|
|
|
properties.Add(new
|
|
{
|
|
name = name,
|
|
type = type.ToString(),
|
|
description = description,
|
|
value = currentValue
|
|
});
|
|
}
|
|
#endif
|
|
|
|
return new SuccessResponse($"Retrieved material info for {mat.name}", new
|
|
{
|
|
material = mat.name,
|
|
shader = shader.name,
|
|
properties = properties
|
|
});
|
|
}
|
|
|
|
private static object CreateMaterial(JObject @params)
|
|
{
|
|
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
|
string shaderName = @params["shader"]?.ToString() ?? "Standard";
|
|
JToken colorToken = @params["color"];
|
|
string colorProperty = @params["property"]?.ToString();
|
|
|
|
JObject properties = null;
|
|
JToken propsToken = @params["properties"];
|
|
if (propsToken != null)
|
|
{
|
|
if (propsToken.Type == JTokenType.String)
|
|
{
|
|
try { properties = JObject.Parse(propsToken.ToString()); }
|
|
catch (Exception ex) { return new ErrorResponse($"Invalid JSON in properties: {ex.Message}"); }
|
|
}
|
|
else if (propsToken is JObject obj)
|
|
{
|
|
properties = obj;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(materialPath))
|
|
{
|
|
return new ErrorResponse("materialPath is required");
|
|
}
|
|
|
|
// Safety check: SanitizeAssetPath should guarantee Assets/ prefix
|
|
// This check catches edge cases where normalization might fail
|
|
if (!materialPath.StartsWith("Assets/"))
|
|
{
|
|
return new ErrorResponse($"Invalid path '{materialPath}'. Path must be within Assets/ folder.");
|
|
}
|
|
|
|
Shader shader = RenderPipelineUtility.ResolveShader(shaderName);
|
|
if (shader == null)
|
|
{
|
|
return new ErrorResponse($"Could not find shader: {shaderName}");
|
|
}
|
|
|
|
// Check for existing asset to avoid silent overwrite
|
|
if (AssetDatabase.LoadAssetAtPath<Material>(materialPath) != null)
|
|
{
|
|
return new ErrorResponse($"Material already exists at {materialPath}");
|
|
}
|
|
|
|
Material material = null;
|
|
var shouldDestroyMaterial = true;
|
|
try
|
|
{
|
|
material = new Material(shader);
|
|
|
|
// Apply color param during creation (keeps Python tool signature and C# implementation consistent).
|
|
// If "properties" already contains a color property, let properties win.
|
|
bool shouldApplyColor = false;
|
|
if (colorToken != null)
|
|
{
|
|
if (properties == null)
|
|
{
|
|
shouldApplyColor = true;
|
|
}
|
|
else if (!string.IsNullOrEmpty(colorProperty))
|
|
{
|
|
// If colorProperty is specified, only check that specific property.
|
|
shouldApplyColor = !properties.ContainsKey(colorProperty);
|
|
}
|
|
else
|
|
{
|
|
// If colorProperty is not specified, check fallback properties.
|
|
shouldApplyColor = !properties.ContainsKey("_BaseColor") && !properties.ContainsKey("_Color");
|
|
}
|
|
}
|
|
|
|
if (shouldApplyColor)
|
|
{
|
|
Color color;
|
|
try
|
|
{
|
|
color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return new ErrorResponse($"Invalid color format: {e.Message}");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(colorProperty))
|
|
{
|
|
if (material.HasProperty(colorProperty))
|
|
{
|
|
material.SetColor(colorProperty, color);
|
|
}
|
|
else
|
|
{
|
|
return new ErrorResponse($"Specified color property '{colorProperty}' does not exist on this material.");
|
|
}
|
|
}
|
|
else if (material.HasProperty("_BaseColor"))
|
|
{
|
|
material.SetColor("_BaseColor", color);
|
|
}
|
|
else if (material.HasProperty("_Color"))
|
|
{
|
|
material.SetColor("_Color", color);
|
|
}
|
|
else
|
|
{
|
|
return new ErrorResponse("Could not find suitable color property (_BaseColor or _Color) on this material's shader.");
|
|
}
|
|
}
|
|
|
|
AssetDatabase.CreateAsset(material, materialPath);
|
|
shouldDestroyMaterial = false; // material is now owned by the AssetDatabase
|
|
|
|
if (properties != null)
|
|
{
|
|
MaterialOps.ApplyProperties(material, properties, UnityJsonSerializer.Instance);
|
|
}
|
|
|
|
EditorUtility.SetDirty(material);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return new SuccessResponse($"Created material at {materialPath} with shader {shaderName}");
|
|
}
|
|
finally
|
|
{
|
|
if (shouldDestroyMaterial && material != null)
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(material);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|