Skip to content

Commit fe4cae7

Browse files
authored
feat: Add manage_material tool for dedicated material manipulation (#440)
* WIP: Material management tool implementation and tests - Add ManageMaterial tool for creating and modifying materials - Add MaterialOps helper for material property operations - Add comprehensive test suite for material management - Add string parameter parsing support for material properties - Update related tools (ManageGameObject, manage_asset, etc.) - Add test materials and scenes for material testing * refactor: unify material property logic into MaterialOps - Move and logic from to - Update to delegate to - Update to use enhanced for creation and property setting - Add texture path loading support to * Add parameter aliasing support: accept 'name' as alias for 'target' in manage_gameobject modify action * Refactor ManageMaterial and fix code review issues - Fix Python server tools (redundant imports, exception handling, string formatting) - Clean up documentation and error reports - Improve ManageMaterial.cs (overwrite checks, error handling) - Enhance MaterialOps.cs (robustness, logging, dead code removal) - Update tests (assertions, unused imports) - Fix manifest.json relative path - Remove temporary test artifacts and manual setup scripts * Remove test scene * remove extra mat * Remove unnecessary SceneTemplateSettings.json * Remove unnecessary SceneTemplateSettings.json * Fix MaterialOps issues * Fix: Case-insensitive material property lookup and missing HasProperty checks * Rabbit fixes * Improve material ops logging and test coverage * Fix: NormalizePath now handles backslashes correctly using AssetPathUtility * Fix: Address multiple nitpicks (test robustness, shader resolution, HasProperty checks) * Add manage_material tool documentation and fix MaterialOps texture property checks - Add comprehensive ManageMaterial tool documentation to MCPForUnity/README.md - Add manage_material to tools list in README.md and README-zh.md - Fix MaterialOps.cs to check HasProperty before SetTexture calls to prevent Unity warnings - Ensures consistency with other property setters in MaterialOps * Fix ManageMaterial shader reflection for Unity 6 and improve texture logging
1 parent 7f8ca2a commit fe4cae7

28 files changed

+2004
-404
lines changed

MCPForUnity/Editor/Helpers/MaterialOps.cs

Lines changed: 397 additions & 0 deletions
Large diffs are not rendered by default.

MCPForUnity/Editor/Helpers/MaterialOps.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MCPForUnity/Editor/Tools/ManageAsset.cs

Lines changed: 2 additions & 294 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ private static object CreateAsset(JObject @params)
215215

216216
if (propertiesForApply.HasValues)
217217
{
218-
ApplyMaterialProperties(mat, propertiesForApply);
218+
MaterialOps.ApplyProperties(mat, propertiesForApply, ManageGameObject.InputSerializer);
219219
}
220220
}
221221
AssetDatabase.CreateAsset(mat, fullPath);
@@ -443,7 +443,7 @@ prop.Value is JObject componentProperties
443443
{
444444
// Apply properties directly to the material. If this modifies, it sets modified=true.
445445
// Use |= in case the asset was already marked modified by previous logic (though unlikely here)
446-
modified |= ApplyMaterialProperties(material, properties);
446+
modified |= MaterialOps.ApplyProperties(material, properties, ManageGameObject.InputSerializer);
447447
}
448448
// Example: Modifying a ScriptableObject
449449
else if (asset is ScriptableObject so)
@@ -897,299 +897,7 @@ private static void EnsureDirectoryExists(string directoryPath)
897897
}
898898
}
899899

900-
/// <summary>
901-
/// Applies properties from JObject to a Material.
902-
/// </summary>
903-
private static bool ApplyMaterialProperties(Material mat, JObject properties)
904-
{
905-
if (mat == null || properties == null)
906-
return false;
907-
bool modified = false;
908-
909-
// Example: Set shader
910-
if (properties["shader"]?.Type == JTokenType.String)
911-
{
912-
string shaderRequest = properties["shader"].ToString();
913-
Shader newShader = RenderPipelineUtility.ResolveShader(shaderRequest);
914-
if (newShader != null && mat.shader != newShader)
915-
{
916-
mat.shader = newShader;
917-
modified = true;
918-
}
919-
}
920-
// Example: Set color property
921-
if (properties["color"] is JObject colorProps)
922-
{
923-
string propName = colorProps["name"]?.ToString() ?? GetMainColorPropertyName(mat); // Auto-detect if not specified
924-
if (colorProps["value"] is JArray colArr && colArr.Count >= 3)
925-
{
926-
try
927-
{
928-
Color newColor = new Color(
929-
colArr[0].ToObject<float>(),
930-
colArr[1].ToObject<float>(),
931-
colArr[2].ToObject<float>(),
932-
colArr.Count > 3 ? colArr[3].ToObject<float>() : 1.0f
933-
);
934-
if (mat.HasProperty(propName))
935-
{
936-
if (mat.GetColor(propName) != newColor)
937-
{
938-
mat.SetColor(propName, newColor);
939-
modified = true;
940-
}
941-
}
942-
else
943-
{
944-
Debug.LogWarning(
945-
$"Material '{mat.name}' with shader '{mat.shader.name}' does not have color property '{propName}'. " +
946-
$"Color not applied. Common color properties: _BaseColor (URP), _Color (Standard)"
947-
);
948-
}
949-
}
950-
catch (Exception ex)
951-
{
952-
Debug.LogWarning(
953-
$"Error parsing color property '{propName}': {ex.Message}"
954-
);
955-
}
956-
}
957-
}
958-
else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
959-
{
960-
// Auto-detect the main color property for the shader
961-
string propName = GetMainColorPropertyName(mat);
962-
try
963-
{
964-
if (colorArr.Count >= 3)
965-
{
966-
Color newColor = new Color(
967-
colorArr[0].ToObject<float>(),
968-
colorArr[1].ToObject<float>(),
969-
colorArr[2].ToObject<float>(),
970-
colorArr.Count > 3 ? colorArr[3].ToObject<float>() : 1.0f
971-
);
972-
if (mat.HasProperty(propName))
973-
{
974-
if (mat.GetColor(propName) != newColor)
975-
{
976-
mat.SetColor(propName, newColor);
977-
modified = true;
978-
}
979-
}
980-
else
981-
{
982-
Debug.LogWarning(
983-
$"Material '{mat.name}' with shader '{mat.shader.name}' does not have color property '{propName}'. " +
984-
$"Color not applied. Common color properties: _BaseColor (URP), _Color (Standard)"
985-
);
986-
}
987-
}
988-
}
989-
catch (Exception ex)
990-
{
991-
Debug.LogWarning(
992-
$"Error parsing color property '{propName}': {ex.Message}"
993-
);
994-
}
995-
}
996-
// Example: Set float property
997-
if (properties["float"] is JObject floatProps)
998-
{
999-
string propName = floatProps["name"]?.ToString();
1000-
if (
1001-
!string.IsNullOrEmpty(propName) &&
1002-
(floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)
1003-
)
1004-
{
1005-
try
1006-
{
1007-
float newVal = floatProps["value"].ToObject<float>();
1008-
if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal)
1009-
{
1010-
mat.SetFloat(propName, newVal);
1011-
modified = true;
1012-
}
1013-
}
1014-
catch (Exception ex)
1015-
{
1016-
Debug.LogWarning(
1017-
$"Error parsing float property '{propName}': {ex.Message}"
1018-
);
1019-
}
1020-
}
1021-
}
1022-
// Example: Set texture property (case-insensitive key and subkeys)
1023-
{
1024-
JObject texProps = null;
1025-
var direct = properties.Property("texture");
1026-
if (direct != null && direct.Value is JObject t0) texProps = t0;
1027-
if (texProps == null)
1028-
{
1029-
var ci = properties.Properties().FirstOrDefault(
1030-
p => string.Equals(p.Name, "texture", StringComparison.OrdinalIgnoreCase));
1031-
if (ci != null && ci.Value is JObject t1) texProps = t1;
1032-
}
1033-
if (texProps != null)
1034-
{
1035-
string rawName = (texProps["name"] ?? texProps["Name"])?.ToString();
1036-
string texPath = (texProps["path"] ?? texProps["Path"])?.ToString();
1037-
if (!string.IsNullOrEmpty(texPath))
1038-
{
1039-
var newTex = AssetDatabase.LoadAssetAtPath<Texture>(
1040-
AssetPathUtility.SanitizeAssetPath(texPath));
1041-
if (newTex == null)
1042-
{
1043-
Debug.LogWarning($"Texture not found at path: {texPath}");
1044-
}
1045-
else
1046-
{
1047-
// Reuse alias resolver so friendly names like 'albedo' work here too
1048-
string candidateName = string.IsNullOrEmpty(rawName) ? "_BaseMap" : rawName;
1049-
string targetProp = ResolvePropertyName(candidateName);
1050-
if (!string.IsNullOrEmpty(targetProp) && mat.HasProperty(targetProp))
1051-
{
1052-
if (mat.GetTexture(targetProp) != newTex)
1053-
{
1054-
mat.SetTexture(targetProp, newTex);
1055-
modified = true;
1056-
}
1057-
}
1058-
}
1059-
}
1060-
}
1061-
}
1062-
1063-
// --- Flexible direct property assignment ---
1064-
// Allow payloads like: { "_Color": [r,g,b,a] }, { "_Glossiness": 0.5 }, { "_MainTex": "Assets/.." }
1065-
// while retaining backward compatibility with the structured keys above.
1066-
// This iterates all top-level keys except the reserved structured ones and applies them
1067-
// if they match known shader properties.
1068-
var reservedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "shader", "color", "float", "texture" };
1069-
1070-
// Helper resolves common URP/Standard aliasing (e.g., _Color <-> _BaseColor, _MainTex <-> _BaseMap, _Glossiness <-> _Smoothness)
1071-
string ResolvePropertyName(string name)
1072-
{
1073-
if (string.IsNullOrEmpty(name)) return name;
1074-
string[] candidates;
1075-
var lower = name.ToLowerInvariant();
1076-
switch (lower)
1077-
{
1078-
case "_color": candidates = new[] { "_Color", "_BaseColor" }; break;
1079-
case "_basecolor": candidates = new[] { "_BaseColor", "_Color" }; break;
1080-
case "_maintex": candidates = new[] { "_MainTex", "_BaseMap" }; break;
1081-
case "_basemap": candidates = new[] { "_BaseMap", "_MainTex" }; break;
1082-
case "_glossiness": candidates = new[] { "_Glossiness", "_Smoothness" }; break;
1083-
case "_smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break;
1084-
// Friendly names → shader property names
1085-
case "metallic": candidates = new[] { "_Metallic" }; break;
1086-
case "smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break;
1087-
case "albedo": candidates = new[] { "_BaseMap", "_MainTex" }; break;
1088-
default: candidates = new[] { name }; break; // keep original as-is
1089-
}
1090-
foreach (var candidate in candidates)
1091-
{
1092-
if (mat.HasProperty(candidate)) return candidate;
1093-
}
1094-
return name; // fall back to original
1095-
}
1096-
1097-
foreach (var prop in properties.Properties())
1098-
{
1099-
if (reservedKeys.Contains(prop.Name)) continue;
1100-
string shaderProp = ResolvePropertyName(prop.Name);
1101-
JToken v = prop.Value;
1102-
1103-
// Color: numeric array [r,g,b,(a)]
1104-
if (v is JArray arr && arr.Count >= 3 && arr.All(t => t.Type == JTokenType.Float || t.Type == JTokenType.Integer))
1105-
{
1106-
if (mat.HasProperty(shaderProp))
1107-
{
1108-
try
1109-
{
1110-
var c = new Color(
1111-
arr[0].ToObject<float>(),
1112-
arr[1].ToObject<float>(),
1113-
arr[2].ToObject<float>(),
1114-
arr.Count > 3 ? arr[3].ToObject<float>() : 1f
1115-
);
1116-
if (mat.GetColor(shaderProp) != c)
1117-
{
1118-
mat.SetColor(shaderProp, c);
1119-
modified = true;
1120-
}
1121-
}
1122-
catch (Exception ex)
1123-
{
1124-
Debug.LogWarning($"Error setting color '{shaderProp}': {ex.Message}");
1125-
}
1126-
}
1127-
continue;
1128-
}
1129-
1130-
// Float: single number
1131-
if (v.Type == JTokenType.Float || v.Type == JTokenType.Integer)
1132-
{
1133-
if (mat.HasProperty(shaderProp))
1134-
{
1135-
try
1136-
{
1137-
float f = v.ToObject<float>();
1138-
if (!Mathf.Approximately(mat.GetFloat(shaderProp), f))
1139-
{
1140-
mat.SetFloat(shaderProp, f);
1141-
modified = true;
1142-
}
1143-
}
1144-
catch (Exception ex)
1145-
{
1146-
Debug.LogWarning($"Error setting float '{shaderProp}': {ex.Message}");
1147-
}
1148-
}
1149-
continue;
1150-
}
1151900

1152-
// Texture: string path
1153-
if (v.Type == JTokenType.String)
1154-
{
1155-
string texPath = v.ToString();
1156-
if (!string.IsNullOrEmpty(texPath) && mat.HasProperty(shaderProp))
1157-
{
1158-
var tex = AssetDatabase.LoadAssetAtPath<Texture>(AssetPathUtility.SanitizeAssetPath(texPath));
1159-
if (tex != null && mat.GetTexture(shaderProp) != tex)
1160-
{
1161-
mat.SetTexture(shaderProp, tex);
1162-
modified = true;
1163-
}
1164-
}
1165-
continue;
1166-
}
1167-
}
1168-
1169-
// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
1170-
return modified;
1171-
}
1172-
1173-
/// <summary>
1174-
/// Auto-detects the main color property name for a material's shader.
1175-
/// Tries common color property names in order: _BaseColor (URP), _Color (Standard), etc.
1176-
/// </summary>
1177-
private static string GetMainColorPropertyName(Material mat)
1178-
{
1179-
if (mat == null || mat.shader == null)
1180-
return "_Color";
1181-
1182-
// Try common color property names in order of likelihood
1183-
string[] commonColorProps = { "_BaseColor", "_Color", "_MainColor", "_Tint", "_TintColor" };
1184-
foreach (var prop in commonColorProps)
1185-
{
1186-
if (mat.HasProperty(prop))
1187-
return prop;
1188-
}
1189-
1190-
// Fallback to _Color if none found
1191-
return "_Color";
1192-
}
1193901

1194902
/// <summary>
1195903
/// Applies properties from JObject to a PhysicsMaterial.

0 commit comments

Comments
 (0)