600 lines
24 KiB
C#
600 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using MCPForUnity.Editor.Helpers;
|
|
using UnityEngine;
|
|
using UnityEditor;
|
|
|
|
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 { status = "error", message = "Action is required" };
|
|
}
|
|
|
|
try
|
|
{
|
|
switch (action)
|
|
{
|
|
case "ping":
|
|
return new { status = "success", 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 { status = "error", message = $"Unknown action: {action}" };
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { status = "error", message = ex.Message, 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 { status = "error", message = "materialPath, property, and value are required" };
|
|
}
|
|
|
|
// Find material
|
|
var findInstruction = new JObject { ["find"] = materialPath };
|
|
Material mat = ManageGameObject.FindObjectByInstruction(findInstruction, typeof(Material)) as Material;
|
|
|
|
if (mat == null)
|
|
{
|
|
return new { status = "error", message = $"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 = ManageGameObject.FindObjectByInstruction(obj, typeof(Texture)) as Texture;
|
|
if (tex != null && mat.HasProperty(property))
|
|
{
|
|
mat.SetTexture(property, tex);
|
|
EditorUtility.SetDirty(mat);
|
|
return new { status = "success", message = $"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, ManageGameObject.InputSerializer);
|
|
|
|
if (success)
|
|
{
|
|
EditorUtility.SetDirty(mat);
|
|
return new { status = "success", message = $"Set property {property} on {mat.name}" };
|
|
}
|
|
else
|
|
{
|
|
return new { status = "error", message = $"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 { status = "error", message = "materialPath and color are required" };
|
|
}
|
|
|
|
var findInstruction = new JObject { ["find"] = materialPath };
|
|
Material mat = ManageGameObject.FindObjectByInstruction(findInstruction, typeof(Material)) as Material;
|
|
|
|
if (mat == null)
|
|
{
|
|
return new { status = "error", message = $"Could not find material at path: {materialPath}" };
|
|
}
|
|
|
|
Color color;
|
|
try
|
|
{
|
|
color = MaterialOps.ParseColor(colorToken, ManageGameObject.InputSerializer);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return new { status = "error", message = $"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 { status = "success", message = $"Set color on {property}" };
|
|
}
|
|
else
|
|
{
|
|
return new { status = "error", message = "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 { status = "error", message = "target and materialPath are required" };
|
|
}
|
|
|
|
var goInstruction = new JObject { ["find"] = target };
|
|
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
|
|
|
|
GameObject go = ManageGameObject.FindObjectByInstruction(goInstruction, typeof(GameObject)) as GameObject;
|
|
if (go == null)
|
|
{
|
|
return new { status = "error", message = $"Could not find target GameObject: {target}" };
|
|
}
|
|
|
|
Renderer renderer = go.GetComponent<Renderer>();
|
|
if (renderer == null)
|
|
{
|
|
return new { status = "error", message = $"GameObject {go.name} has no Renderer component" };
|
|
}
|
|
|
|
var matInstruction = new JObject { ["find"] = materialPath };
|
|
Material mat = ManageGameObject.FindObjectByInstruction(matInstruction, typeof(Material)) as Material;
|
|
if (mat == null)
|
|
{
|
|
return new { status = "error", message = $"Could not find material: {materialPath}" };
|
|
}
|
|
|
|
Undo.RecordObject(renderer, "Assign Material");
|
|
|
|
Material[] sharedMats = renderer.sharedMaterials;
|
|
if (slot < 0 || slot >= sharedMats.Length)
|
|
{
|
|
return new { status = "error", message = $"Slot {slot} out of bounds (count: {sharedMats.Length})" };
|
|
}
|
|
|
|
sharedMats[slot] = mat;
|
|
renderer.sharedMaterials = sharedMats;
|
|
|
|
EditorUtility.SetDirty(renderer);
|
|
return new { status = "success", message = $"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 { status = "error", message = "target and color are required" };
|
|
}
|
|
|
|
Color color;
|
|
try
|
|
{
|
|
color = MaterialOps.ParseColor(colorToken, ManageGameObject.InputSerializer);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return new { status = "error", message = $"Invalid color format: {e.Message}" };
|
|
}
|
|
|
|
var goInstruction = new JObject { ["find"] = target };
|
|
if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod;
|
|
|
|
GameObject go = ManageGameObject.FindObjectByInstruction(goInstruction, typeof(GameObject)) as GameObject;
|
|
if (go == null)
|
|
{
|
|
return new { status = "error", message = $"Could not find target GameObject: {target}" };
|
|
}
|
|
|
|
Renderer renderer = go.GetComponent<Renderer>();
|
|
if (renderer == null)
|
|
{
|
|
return new { status = "error", message = $"GameObject {go.name} has no Renderer component" };
|
|
}
|
|
|
|
if (mode == "property_block")
|
|
{
|
|
if (slot < 0 || slot >= renderer.sharedMaterials.Length)
|
|
{
|
|
return new { status = "error", message = $"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 { status = "success", message = $"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 { status = "error", message = $"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 { status = "success", message = "Set shared material color" };
|
|
}
|
|
return new { status = "error", message = "Invalid slot" };
|
|
}
|
|
else if (mode == "instance")
|
|
{
|
|
if (slot >= 0 && slot < renderer.materials.Length)
|
|
{
|
|
Material mat = renderer.materials[slot];
|
|
if (mat == null)
|
|
{
|
|
return new { status = "error", message = $"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 { status = "success", message = "Set instance material color", warning = "Material instance created; Undo cannot fully revert instantiation." };
|
|
}
|
|
return new { status = "error", message = "Invalid slot" };
|
|
}
|
|
|
|
return new { status = "error", message = $"Unknown mode: {mode}" };
|
|
}
|
|
|
|
private static object GetMaterialInfo(JObject @params)
|
|
{
|
|
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
|
if (string.IsNullOrEmpty(materialPath))
|
|
{
|
|
return new { status = "error", message = "materialPath is required" };
|
|
}
|
|
|
|
var findInstruction = new JObject { ["find"] = materialPath };
|
|
Material mat = ManageGameObject.FindObjectByInstruction(findInstruction, typeof(Material)) as Material;
|
|
|
|
if (mat == null)
|
|
{
|
|
return new { status = "error", message = $"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 {
|
|
status = "success",
|
|
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 { status = "error", message = $"Invalid JSON in properties: {ex.Message}" }; }
|
|
}
|
|
else if (propsToken is JObject obj)
|
|
{
|
|
properties = obj;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(materialPath))
|
|
{
|
|
return new { status = "error", message = "materialPath is required" };
|
|
}
|
|
|
|
// Path normalization handled by helper above, explicit check removed
|
|
// but we ensure it's valid for CreateAsset
|
|
if (!materialPath.StartsWith("Assets/"))
|
|
{
|
|
return new { status = "error", message = "Path must start with Assets/ (normalization failed)" };
|
|
}
|
|
|
|
Shader shader = RenderPipelineUtility.ResolveShader(shaderName);
|
|
if (shader == null)
|
|
{
|
|
return new { status = "error", message = $"Could not find shader: {shaderName}" };
|
|
}
|
|
|
|
// Check for existing asset to avoid silent overwrite
|
|
if (AssetDatabase.LoadAssetAtPath<Material>(materialPath) != null)
|
|
{
|
|
return new { status = "error", message = $"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, ManageGameObject.InputSerializer);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return new { status = "error", message = $"Invalid color format: {e.Message}" };
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(colorProperty))
|
|
{
|
|
if (material.HasProperty(colorProperty))
|
|
{
|
|
material.SetColor(colorProperty, color);
|
|
}
|
|
else
|
|
{
|
|
return new
|
|
{
|
|
status = "error",
|
|
message = $"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
|
|
{
|
|
status = "error",
|
|
message = "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, ManageGameObject.InputSerializer);
|
|
}
|
|
|
|
EditorUtility.SetDirty(material);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return new { status = "success", message = $"Created material at {materialPath} with shader {shaderName}" };
|
|
}
|
|
finally
|
|
{
|
|
if (shouldDestroyMaterial && material != null)
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(material);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|