Skip to main content
Glama
MaterialOps.cs16.6 kB
using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEngine; using UnityEditor; using MCPForUnity.Editor.Tools; namespace MCPForUnity.Editor.Helpers { public static class MaterialOps { /// <summary> /// Applies a set of properties (JObject) to a material, handling aliases and structured formats. /// </summary> public static bool ApplyProperties(Material mat, JObject properties, JsonSerializer serializer) { if (mat == null || properties == null) return false; bool modified = false; // Helper for case-insensitive lookup JToken GetValue(string key) { return properties.Properties() .FirstOrDefault(p => string.Equals(p.Name, key, StringComparison.OrdinalIgnoreCase))?.Value; } // --- Structured / Legacy Format Handling --- // Example: Set shader var shaderToken = GetValue("shader"); if (shaderToken?.Type == JTokenType.String) { string shaderRequest = shaderToken.ToString(); // Set shader Shader newShader = RenderPipelineUtility.ResolveShader(shaderRequest); if (newShader != null && mat.shader != newShader) { mat.shader = newShader; modified = true; } } // Example: Set color property (structured) var colorToken = GetValue("color"); if (colorToken is JObject colorProps) { string propName = colorProps["name"]?.ToString() ?? GetMainColorPropertyName(mat); if (colorProps["value"] is JArray colArr && colArr.Count >= 3) { try { Color newColor = ParseColor(colArr, serializer); if (mat.HasProperty(propName)) { if (mat.GetColor(propName) != newColor) { mat.SetColor(propName, newColor); modified = true; } } } catch (Exception ex) { Debug.LogWarning($"[MaterialOps] Failed to parse color for property '{propName}': {ex.Message}"); } } } else if (colorToken is JArray colorArr) // Structured shorthand { string propName = GetMainColorPropertyName(mat); try { Color newColor = ParseColor(colorArr, serializer); if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) { mat.SetColor(propName, newColor); modified = true; } } catch (Exception ex) { Debug.LogWarning($"[MaterialOps] Failed to parse color array: {ex.Message}"); } } // Example: Set float property (structured) var floatToken = GetValue("float"); if (floatToken is JObject floatProps) { string propName = floatProps["name"]?.ToString(); if (!string.IsNullOrEmpty(propName) && (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)) { try { float newVal = floatProps["value"].ToObject<float>(); if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal) { mat.SetFloat(propName, newVal); modified = true; } } catch (Exception ex) { Debug.LogWarning($"[MaterialOps] Failed to set float property '{propName}': {ex.Message}"); } } } // Example: Set texture property (structured) { var texToken = GetValue("texture"); if (texToken is JObject texProps) { string rawName = (texProps["name"] ?? texProps["Name"])?.ToString(); string texPath = (texProps["path"] ?? texProps["Path"])?.ToString(); if (!string.IsNullOrEmpty(texPath)) { var sanitizedPath = AssetPathUtility.SanitizeAssetPath(texPath); var newTex = AssetDatabase.LoadAssetAtPath<Texture>(sanitizedPath); // Use ResolvePropertyName to handle aliases even for structured texture names string candidateName = string.IsNullOrEmpty(rawName) ? "_BaseMap" : rawName; string targetProp = ResolvePropertyName(mat, candidateName); if (!string.IsNullOrEmpty(targetProp) && mat.HasProperty(targetProp)) { if (mat.GetTexture(targetProp) != newTex) { mat.SetTexture(targetProp, newTex); modified = true; } } } } } // --- Direct Property Assignment (Flexible) --- var reservedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "shader", "color", "float", "texture" }; foreach (var prop in properties.Properties()) { if (reservedKeys.Contains(prop.Name)) continue; string shaderProp = ResolvePropertyName(mat, prop.Name); JToken v = prop.Value; if (TrySetShaderProperty(mat, shaderProp, v, serializer)) { modified = true; } } return modified; } /// <summary> /// Resolves common property aliases (e.g. "metallic" -> "_Metallic"). /// </summary> public static string ResolvePropertyName(Material mat, string name) { if (mat == null || string.IsNullOrEmpty(name)) return name; string[] candidates; var lower = name.ToLowerInvariant(); switch (lower) { case "_color": candidates = new[] { "_Color", "_BaseColor" }; break; case "_basecolor": candidates = new[] { "_BaseColor", "_Color" }; break; case "_maintex": candidates = new[] { "_MainTex", "_BaseMap" }; break; case "_basemap": candidates = new[] { "_BaseMap", "_MainTex" }; break; case "_glossiness": candidates = new[] { "_Glossiness", "_Smoothness" }; break; case "_smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break; // Friendly names → shader property names case "metallic": candidates = new[] { "_Metallic" }; break; case "smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break; case "albedo": candidates = new[] { "_BaseMap", "_MainTex" }; break; default: candidates = new[] { name }; break; // keep original as-is } foreach (var candidate in candidates) { if (mat.HasProperty(candidate)) return candidate; } return name; } /// <summary> /// Auto-detects the main color property name for a material's shader. /// </summary> public static string GetMainColorPropertyName(Material mat) { if (mat == null || mat.shader == null) return "_Color"; string[] commonColorProps = { "_BaseColor", "_Color", "_MainColor", "_Tint", "_TintColor" }; foreach (var prop in commonColorProps) { if (mat.HasProperty(prop)) return prop; } return "_Color"; } /// <summary> /// Tries to set a shader property on a material based on a JToken value. /// Handles Colors, Vectors, Floats, Ints, Booleans, and Textures. /// </summary> public static bool TrySetShaderProperty(Material material, string propertyName, JToken value, JsonSerializer serializer) { if (material == null || string.IsNullOrEmpty(propertyName) || value == null) return false; // Handle stringified JSON if (value.Type == JTokenType.String) { string s = value.ToString(); if (s.TrimStart().StartsWith("[") || s.TrimStart().StartsWith("{")) { try { JToken parsed = JToken.Parse(s); return TrySetShaderProperty(material, propertyName, parsed, serializer); } catch { } } } // Use the serializer to convert the JToken value first if (value is JArray jArray) { if (jArray.Count == 4) { if (material.HasProperty(propertyName)) { try { material.SetColor(propertyName, ParseColor(value, serializer)); return true; } catch (Exception ex) { // Log at Debug level since we'll try other conversions Debug.Log($"[MaterialOps] SetColor attempt for '{propertyName}' failed: {ex.Message}"); } try { Vector4 vec = value.ToObject<Vector4>(serializer); material.SetVector(propertyName, vec); return true; } catch (Exception ex) { Debug.Log($"[MaterialOps] SetVector (Vec4) attempt for '{propertyName}' failed: {ex.Message}"); } } } else if (jArray.Count == 3) { if (material.HasProperty(propertyName)) { try { material.SetColor(propertyName, ParseColor(value, serializer)); return true; } catch (Exception ex) { Debug.Log($"[MaterialOps] SetColor (Vec3) attempt for '{propertyName}' failed: {ex.Message}"); } } } else if (jArray.Count == 2) { if (material.HasProperty(propertyName)) { try { Vector2 vec = value.ToObject<Vector2>(serializer); material.SetVector(propertyName, vec); return true; } catch (Exception ex) { Debug.Log($"[MaterialOps] SetVector (Vec2) attempt for '{propertyName}' failed: {ex.Message}"); } } } } else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer) { if (!material.HasProperty(propertyName)) return false; try { material.SetFloat(propertyName, value.ToObject<float>(serializer)); return true; } catch (Exception ex) { Debug.Log($"[MaterialOps] SetFloat attempt for '{propertyName}' failed: {ex.Message}"); } } else if (value.Type == JTokenType.Boolean) { if (!material.HasProperty(propertyName)) return false; try { material.SetFloat(propertyName, value.ToObject<bool>(serializer) ? 1f : 0f); return true; } catch (Exception ex) { Debug.Log($"[MaterialOps] SetFloat (bool) attempt for '{propertyName}' failed: {ex.Message}"); } } else if (value.Type == JTokenType.String) { try { // Try loading as asset path first (most common case for strings in this context) string path = value.ToString(); if (!string.IsNullOrEmpty(path) && path.Contains("/")) // Heuristic: paths usually have slashes { // We need to handle texture assignment here. // Since we don't have easy access to AssetDatabase here directly without using UnityEditor namespace (which is imported), // we can try to load it. var sanitizedPath = AssetPathUtility.SanitizeAssetPath(path); Texture tex = AssetDatabase.LoadAssetAtPath<Texture>(sanitizedPath); if (tex != null && material.HasProperty(propertyName)) { material.SetTexture(propertyName, tex); return true; } } } catch (Exception ex) { McpLog.Warn($"SetTexture (string path) for '{propertyName}' failed: {ex.Message}"); } } if (value.Type == JTokenType.Object) { try { Texture texture = value.ToObject<Texture>(serializer); if (texture != null && material.HasProperty(propertyName)) { material.SetTexture(propertyName, texture); return true; } } catch (Exception ex) { McpLog.Warn($"SetTexture (object) for '{propertyName}' failed: {ex.Message}"); } } Debug.LogWarning( $"[MaterialOps] Unsupported or failed conversion for material property '{propertyName}' from value: {value.ToString(Formatting.None)}" ); return false; } /// <summary> /// Helper to parse color from JToken (array or object). /// </summary> public static Color ParseColor(JToken token, JsonSerializer serializer) { if (token.Type == JTokenType.String) { string s = token.ToString(); if (s.TrimStart().StartsWith("[") || s.TrimStart().StartsWith("{")) { try { return ParseColor(JToken.Parse(s), serializer); } catch { } } } if (token is JArray jArray) { if (jArray.Count == 4) { return new Color( (float)jArray[0], (float)jArray[1], (float)jArray[2], (float)jArray[3] ); } else if (jArray.Count == 3) { return new Color( (float)jArray[0], (float)jArray[1], (float)jArray[2], 1f ); } else { throw new ArgumentException("Color array must have 3 or 4 elements."); } } try { return token.ToObject<Color>(serializer); } catch (Exception ex) { Debug.LogWarning($"[MaterialOps] Failed to parse color from token: {ex.Message}"); throw; } } } }

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