Skip to content

Commit 78aa5ab

Browse files
committed
feat(material): support direct shader property keys and texture paths; add EditMode tests
1 parent 1bdbeb8 commit 78aa5ab

File tree

3 files changed

+271
-1
lines changed

3 files changed

+271
-1
lines changed

MCPForUnity/Editor/Tools/ManageAsset.cs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,108 @@ private static bool ApplyMaterialProperties(Material mat, JObject properties)
10141014
}
10151015
}
10161016

1017-
// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
1017+
// --- Flexible direct property assignment ---
1018+
// Allow payloads like: { "_Color": [r,g,b,a] }, { "_Glossiness": 0.5 }, { "_MainTex": "Assets/.." }
1019+
// while retaining backward compatibility with the structured keys above.
1020+
// This iterates all top-level keys except the reserved structured ones and applies them
1021+
// if they match known shader properties.
1022+
var reservedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "shader", "color", "float", "texture" };
1023+
1024+
// Helper resolves common URP/Standard aliasing (e.g., _Color <-> _BaseColor, _MainTex <-> _BaseMap, _Glossiness <-> _Smoothness)
1025+
string ResolvePropertyName(string name)
1026+
{
1027+
if (string.IsNullOrEmpty(name)) return name;
1028+
string[] candidates;
1029+
switch (name)
1030+
{
1031+
case "_Color": candidates = new[] { "_Color", "_BaseColor" }; break;
1032+
case "_BaseColor": candidates = new[] { "_BaseColor", "_Color" }; break;
1033+
case "_MainTex": candidates = new[] { "_MainTex", "_BaseMap" }; break;
1034+
case "_BaseMap": candidates = new[] { "_BaseMap", "_MainTex" }; break;
1035+
case "_Glossiness": candidates = new[] { "_Glossiness", "_Smoothness" }; break;
1036+
case "_Smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break;
1037+
default: candidates = new[] { name }; break;
1038+
}
1039+
foreach (var candidate in candidates)
1040+
{
1041+
if (mat.HasProperty(candidate)) return candidate;
1042+
}
1043+
return name; // fall back to original
1044+
}
1045+
1046+
foreach (var prop in properties.Properties())
1047+
{
1048+
if (reservedKeys.Contains(prop.Name)) continue;
1049+
string shaderProp = ResolvePropertyName(prop.Name);
1050+
JToken v = prop.Value;
1051+
1052+
// Color: numeric array [r,g,b,(a)]
1053+
if (v is JArray arr && arr.Count >= 3 && arr.All(t => t.Type == JTokenType.Float || t.Type == JTokenType.Integer))
1054+
{
1055+
if (mat.HasProperty(shaderProp))
1056+
{
1057+
try
1058+
{
1059+
var c = new Color(
1060+
arr[0].ToObject<float>(),
1061+
arr[1].ToObject<float>(),
1062+
arr[2].ToObject<float>(),
1063+
arr.Count > 3 ? arr[3].ToObject<float>() : 1f
1064+
);
1065+
if (mat.GetColor(shaderProp) != c)
1066+
{
1067+
mat.SetColor(shaderProp, c);
1068+
modified = true;
1069+
}
1070+
}
1071+
catch (Exception ex)
1072+
{
1073+
Debug.LogWarning($"Error setting color '{shaderProp}': {ex.Message}");
1074+
}
1075+
}
1076+
continue;
1077+
}
1078+
1079+
// Float: single number
1080+
if (v.Type == JTokenType.Float || v.Type == JTokenType.Integer)
1081+
{
1082+
if (mat.HasProperty(shaderProp))
1083+
{
1084+
try
1085+
{
1086+
float f = v.ToObject<float>();
1087+
if (!Mathf.Approximately(mat.GetFloat(shaderProp), f))
1088+
{
1089+
mat.SetFloat(shaderProp, f);
1090+
modified = true;
1091+
}
1092+
}
1093+
catch (Exception ex)
1094+
{
1095+
Debug.LogWarning($"Error setting float '{shaderProp}': {ex.Message}");
1096+
}
1097+
}
1098+
continue;
1099+
}
1100+
1101+
// Texture: string path
1102+
if (v.Type == JTokenType.String)
1103+
{
1104+
string texPath = v.ToString();
1105+
if (!string.IsNullOrEmpty(texPath) && mat.HasProperty(shaderProp))
1106+
{
1107+
var tex = AssetDatabase.LoadAssetAtPath<Texture>(AssetPathUtility.SanitizeAssetPath(texPath));
1108+
if (tex != null && mat.GetTexture(shaderProp) != tex)
1109+
{
1110+
mat.SetTexture(shaderProp, tex);
1111+
modified = true;
1112+
}
1113+
}
1114+
continue;
1115+
}
1116+
}
1117+
1118+
// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
10181119
return modified;
10191120
}
10201121

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System;
2+
using System.IO;
3+
using Newtonsoft.Json.Linq;
4+
using NUnit.Framework;
5+
using UnityEditor;
6+
using UnityEngine;
7+
using MCPForUnity.Editor.Tools;
8+
9+
namespace MCPForUnityTests.Editor.Tools
10+
{
11+
public class MaterialDirectPropertiesTests
12+
{
13+
private const string TempRoot = "Assets/Temp/MaterialDirectPropertiesTests";
14+
private string _matPath;
15+
private string _baseMapPath;
16+
private string _normalMapPath;
17+
private string _occlusionMapPath;
18+
19+
[SetUp]
20+
public void SetUp()
21+
{
22+
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
23+
{
24+
AssetDatabase.CreateFolder("Assets", "Temp");
25+
}
26+
if (!AssetDatabase.IsValidFolder(TempRoot))
27+
{
28+
AssetDatabase.CreateFolder("Assets/Temp", "MaterialDirectPropertiesTests");
29+
}
30+
31+
string guid = Guid.NewGuid().ToString("N");
32+
_matPath = $"{TempRoot}/DirectProps_{guid}.mat";
33+
_baseMapPath = $"{TempRoot}/TexBase_{guid}.asset";
34+
_normalMapPath = $"{TempRoot}/TexNormal_{guid}.asset";
35+
_occlusionMapPath = $"{TempRoot}/TexOcc_{guid}.asset";
36+
37+
// Clean any leftovers just in case
38+
TryDeleteAsset(_matPath);
39+
TryDeleteAsset(_baseMapPath);
40+
TryDeleteAsset(_normalMapPath);
41+
TryDeleteAsset(_occlusionMapPath);
42+
43+
AssetDatabase.Refresh();
44+
}
45+
46+
[TearDown]
47+
public void TearDown()
48+
{
49+
TryDeleteAsset(_matPath);
50+
TryDeleteAsset(_baseMapPath);
51+
TryDeleteAsset(_normalMapPath);
52+
TryDeleteAsset(_occlusionMapPath);
53+
AssetDatabase.Refresh();
54+
}
55+
56+
private static void TryDeleteAsset(string path)
57+
{
58+
if (string.IsNullOrEmpty(path)) return;
59+
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
60+
{
61+
AssetDatabase.DeleteAsset(path);
62+
}
63+
var abs = Path.Combine(Directory.GetCurrentDirectory(), path);
64+
try
65+
{
66+
if (File.Exists(abs)) File.Delete(abs);
67+
if (File.Exists(abs + ".meta")) File.Delete(abs + ".meta");
68+
}
69+
catch { }
70+
}
71+
72+
private static Texture2D CreateSolidTextureAsset(string path, Color color)
73+
{
74+
var tex = new Texture2D(4, 4, TextureFormat.RGBA32, false);
75+
var pixels = new Color[16];
76+
for (int i = 0; i < pixels.Length; i++) pixels[i] = color;
77+
tex.SetPixels(pixels);
78+
tex.Apply();
79+
AssetDatabase.CreateAsset(tex, path);
80+
AssetDatabase.SaveAssets();
81+
return tex;
82+
}
83+
84+
private static JObject ToJObject(object result)
85+
{
86+
return result as JObject ?? JObject.FromObject(result);
87+
}
88+
89+
[Test]
90+
public void CreateAndModifyMaterial_WithDirectPropertyKeys_Works()
91+
{
92+
// Arrange: create textures as assets
93+
CreateSolidTextureAsset(_baseMapPath, Color.white);
94+
CreateSolidTextureAsset(_normalMapPath, new Color(0.5f, 0.5f, 1f));
95+
CreateSolidTextureAsset(_occlusionMapPath, Color.gray);
96+
97+
// Create material using direct keys via JSON string
98+
var createParams = new JObject
99+
{
100+
["action"] = "create",
101+
["path"] = _matPath,
102+
["assetType"] = "Material",
103+
["properties"] = new JObject
104+
{
105+
["shader"] = "Universal Render Pipeline/Lit",
106+
["_Color"] = new JArray(0f, 1f, 0f, 1f),
107+
["_Glossiness"] = 0.25f
108+
}
109+
};
110+
var createRes = ToJObject(ManageAsset.HandleCommand(createParams));
111+
Assert.IsTrue(createRes.Value<bool>("success"), createRes.ToString());
112+
113+
// Modify with aliases and textures
114+
var modifyParams = new JObject
115+
{
116+
["action"] = "modify",
117+
["path"] = _matPath,
118+
["properties"] = new JObject
119+
{
120+
["_BaseColor"] = new JArray(0f, 0f, 1f, 1f),
121+
["_Smoothness"] = 0.5f,
122+
["_BaseMap"] = _baseMapPath,
123+
["_BumpMap"] = _normalMapPath,
124+
["_OcclusionMap"] = _occlusionMapPath
125+
}
126+
};
127+
var modifyRes = ToJObject(ManageAsset.HandleCommand(modifyParams));
128+
Assert.IsTrue(modifyRes.Value<bool>("success"), modifyRes.ToString());
129+
130+
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
131+
Assert.IsNotNull(mat, "Material should exist at path.");
132+
133+
// Verify color alias applied
134+
if (mat.HasProperty("_BaseColor"))
135+
{
136+
Assert.AreEqual(Color.blue, mat.GetColor("_BaseColor"));
137+
}
138+
else if (mat.HasProperty("_Color"))
139+
{
140+
Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
141+
}
142+
143+
// Verify float
144+
string smoothProp = mat.HasProperty("_Smoothness") ? "_Smoothness" : (mat.HasProperty("_Glossiness") ? "_Glossiness" : null);
145+
Assert.IsNotNull(smoothProp, "Material should expose Smoothness/Glossiness.");
146+
Assert.That(Mathf.Abs(mat.GetFloat(smoothProp) - 0.5f) < 1e-4f);
147+
148+
// Verify textures
149+
string baseMapProp = mat.HasProperty("_BaseMap") ? "_BaseMap" : (mat.HasProperty("_MainTex") ? "_MainTex" : null);
150+
Assert.IsNotNull(baseMapProp, "Material should expose BaseMap/MainTex.");
151+
Assert.IsNotNull(mat.GetTexture(baseMapProp), "BaseMap/MainTex should be assigned.");
152+
if (mat.HasProperty("_BumpMap")) Assert.IsNotNull(mat.GetTexture("_BumpMap"));
153+
if (mat.HasProperty("_OcclusionMap")) Assert.IsNotNull(mat.GetTexture("_OcclusionMap"));
154+
}
155+
}
156+
}
157+
158+

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialDirectPropertiesTests.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.

0 commit comments

Comments
 (0)