Skip to main content
Glama
ManageMaterial.cs21.7 kB
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"; 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}" }; } Material material = new Material(shader); // Check for existing asset to avoid silent overwrite if (AssetDatabase.LoadAssetAtPath<Material>(materialPath) != null) { return new { status = "error", message = $"Material already exists at {materialPath}" }; } AssetDatabase.CreateAsset(material, materialPath); 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}" }; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CoplayDev/unity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server