Unity下TriLib远程加载FBX的使用与源码解析

最近在开发一个Unity人物形象的SDK,目标是该SDK可以动态下载最新的人物和服装的模型而不用让提前内置在SDK中。

一开始我打算采用的方案是传统的比较成熟的Addressables去打包bundle的方式,但是bundle的划分是个问题,而且这种方式需要每次把模型导入unity然后再出包,再更新catalog,整个流程比较繁琐。

所以我就在想,能不能直接从远程加载FBX和贴图,然后直接使用FBX进行加载就好,这样整个发版流程会简单很多。

经过一番查找,找到了TriLib这个仓库,他可以很方便的从远程加载模型。但是也有一些问题,为了定位这些问题,特意阅读了下源码,这里总结下,其实感觉整个仓库的逻辑是比较简单清晰的。

用法

其实TriLib的用法很简单,它一共就没给开放多少配置的能力,你导入之后,看一眼它的例子就很清晰了,但是还是简单放在这里展示下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using TriLibCore.General;
using UnityEngine;
namespace TriLibCore.Samples
{
/// <summary>
/// Represents a sample that loads a compressed (Zipped) Model.
/// </summary>
public class LoadModelFromURLSample : MonoBehaviour
{
public string modelUrl;
/// <summary>
/// Creates the AssetLoaderOptions instance, configures the Web Request, and downloads the Model.
/// </summary>
/// <remarks>
/// You can create the AssetLoaderOptions by right clicking on the Assets Explorer and selecting "TriLib->Create->AssetLoaderOptions->Pre-Built AssetLoaderOptions".
/// </remarks>
///
private void Start()
{
var assetLoaderOptions = AssetLoader.CreateDefaultLoaderOptions();
var webRequest = AssetDownloader.CreateWebRequest(modelUrl);
AssetDownloader.LoadModelFromUri(webRequest, OnLoad, OnMaterialsLoad, OnProgress, OnError, null, assetLoaderOptions);
}

/// <summary>
/// Called when any error occurs.
/// </summary>
/// <param name="obj">The contextualized error, containing the original exception and the context passed to the method where the error was thrown.</param>
private void OnError(IContextualizedError obj)
{
Debug.LogError($"An error ocurred while loading your Model: {obj.GetInnerException()}");
}

/// <summary>
/// Called when the Model loading progress changes.
/// </summary>
/// <param name="assetLoaderContext">The context used to load the Model.</param>
/// <param name="progress">The loading progress.</param>
private void OnProgress(AssetLoaderContext assetLoaderContext, float progress)
{
Debug.Log($"Loading Model. Progress: {progress:P}");
}

/// <summary>
/// Called when the Model (including Textures and Materials) has been fully loaded, or after any error occurs.
/// </summary>
/// <remarks>The loaded GameObject is available on the assetLoaderContext.RootGameObject field.</remarks>
/// <param name="assetLoaderContext">The context used to load the Model.</param>
private void OnMaterialsLoad(AssetLoaderContext assetLoaderContext)
{
Debug.Log("Materials loaded. Model fully loaded.");
}

/// <summary>
/// Called when the Model Meshes and hierarchy are loaded.
/// </summary>
/// <remarks>The loaded GameObject is available on the assetLoaderContext.RootGameObject field.</remarks>
/// <param name="assetLoaderContext">The context used to load the Model.</param>
private void OnLoad(AssetLoaderContext assetLoaderContext)
{
Debug.Log("Model loaded. Loading materials.");
}
}
}

这段代码很简单,需要注意的是,对应的下载链接下载的内容需要是一个zip文件,内部包含该模型以及模型引用的材质,贴图等等。

使用注意

上面代码是不是看起来很简单,但是你可能会发现并没有用,我就遇到了两个问题

找不到对应的MaterailMapper(v2.0.6)

如果你的Console里面出现了一个waring,大概意思就是找不到MaterialMapper,不要忽略这个错误,因为找不到这个东西,结果就是模型没法正确加载。

我出现这个问题是因为,我的项目是URP渲染管线的。

然后代码里面会判断你是普通渲染管线,URP还是HDRP,然后根据这个找到合适的Material去渲染模型。

其中的URP的判断逻辑如下:

1
2
3
4
5
6
7
public override bool IsCompatible(MaterialMapperContext materialMapperContext)
{
return GraphicsSettingsUtils.IsUsingUniversalPipeline;
}

public static bool IsUsingUniversalPipeline => GraphicsSettings.renderPipelineAsset != null && GraphicsSettings.renderPipelineAsset.name.StartsWith("UniversalRP");

所以这个东西判断成功,你配置的URP Setting使用的文件名字必须是“UniversalRP”,而URP默认生成的叫做“UniversalRenderPipeline”,然后他就没法识别了,就没法给你的FBX添加材质

材质不对,而且多一个材质(v2.0.6)

到目前为止,笔者还是遇到了一个问题,就是材质除了名字,其他都是不对的,不仅属性不对,还会多一个材质,为此,笔者特地去研究了下源码是怎么回事,发现这东西是故意的。

先来解释下为什么会这样:

CreateDefaultLoaderOptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public static AssetLoaderOptions CreateDefaultLoaderOptions(bool generateAssets = false)
{
var assetLoaderOptions = ScriptableObject.CreateInstance<AssetLoaderOptions>();
ByNameRootBoneMapper byNameRootBoneMapper;
#if UNITY_EDITOR
if (generateAssets) {
byNameRootBoneMapper = (ByNameRootBoneMapper)LoadOrCreateScriptableObject("ByNameRootBoneMapper", "RootBone");
} else {
byNameRootBoneMapper = ScriptableObject.CreateInstance<ByNameRootBoneMapper>();
}
#else
byNameRootBoneMapper = ScriptableObject.CreateInstance<ByNameRootBoneMapper>();
#endif
byNameRootBoneMapper.name = "ByNameRootBoneMapper";
assetLoaderOptions.RootBoneMapper = byNameRootBoneMapper;
if (MaterialMapper.RegisteredMappers.Count == 0)
{
Debug.LogWarning("Please add at least one MaterialMapper name to the MaterialMapper.RegisteredMappers static field to create the right MaterialMapper for the Render Pipeline you are using.");
}
else
{
var materialMappers = new List<MaterialMapper>();
foreach (var materialMapperName in MaterialMapper.RegisteredMappers)
{
if (materialMapperName == null)
{
continue;
}
MaterialMapper materialMapper;
#if UNITY_EDITOR
if (generateAssets)
{
materialMapper = (MaterialMapper)LoadOrCreateScriptableObject(materialMapperName, "Material");
} else {
materialMapper = (MaterialMapper)ScriptableObject.CreateInstance(materialMapperName);
}
#else
materialMapper = ScriptableObject.CreateInstance(materialMapperName) as MaterialMapper;
#endif
if (materialMapper != null)
{
materialMapper.name = materialMapperName;
if (materialMapper.IsCompatible(null))
{
materialMappers.Add(materialMapper);
assetLoaderOptions.FixedAllocations.Add(materialMapper);
}
else
{
#if UNITY_EDITOR
var assetPath = AssetDatabase.GetAssetPath(materialMapper);
if (assetPath == null) {
Object.DestroyImmediate(materialMapper);
}
#else
Object.Destroy(materialMapper);
#endif
}
}
}
if (materialMappers.Count == 0)
{
Debug.LogWarning("TriLib could not find any suitable MaterialMapper on the project.");
}
else
{
assetLoaderOptions.MaterialMappers = materialMappers.ToArray();
}
}
//These two animation clip mappers are used to convert legacy to humanoid animations and add a simple generic animation playback component to the model, which will be disabled by default.
LegacyToHumanoidAnimationClipMapper legacyToHumanoidAnimationClipMapper;
SimpleAnimationPlayerAnimationClipMapper simpleAnimationPlayerAnimationClipMapper;
#if UNITY_EDITOR
if (generateAssets)
{
legacyToHumanoidAnimationClipMapper = (LegacyToHumanoidAnimationClipMapper)LoadOrCreateScriptableObject("LegacyToHumanoidAnimationClipMapper", "AnimationClip");
simpleAnimationPlayerAnimationClipMapper = (SimpleAnimationPlayerAnimationClipMapper)LoadOrCreateScriptableObject("SimpleAnimationPlayerAnimationClipMapper", "AnimationClip");
} else {
legacyToHumanoidAnimationClipMapper = ScriptableObject.CreateInstance<LegacyToHumanoidAnimationClipMapper>();
simpleAnimationPlayerAnimationClipMapper = ScriptableObject.CreateInstance<SimpleAnimationPlayerAnimationClipMapper>();
}
#else
legacyToHumanoidAnimationClipMapper = ScriptableObject.CreateInstance<LegacyToHumanoidAnimationClipMapper>();
simpleAnimationPlayerAnimationClipMapper = ScriptableObject.CreateInstance<SimpleAnimationPlayerAnimationClipMapper>();
#endif
legacyToHumanoidAnimationClipMapper.name = "LegacyToHumanoidAnimationClipMapper";
simpleAnimationPlayerAnimationClipMapper.name = "SimpleAnimationPlayerAnimationClipMapper";
assetLoaderOptions.AnimationClipMappers = new AnimationClipMapper[]
{
legacyToHumanoidAnimationClipMapper,
simpleAnimationPlayerAnimationClipMapper
};
assetLoaderOptions.FixedAllocations.Add(assetLoaderOptions);
assetLoaderOptions.FixedAllocations.Add(legacyToHumanoidAnimationClipMapper);
assetLoaderOptions.FixedAllocations.Add(simpleAnimationPlayerAnimationClipMapper);
return assetLoaderOptions;
}

这段代码首先主要干了三件事,分别是找到合适的RootBoneMapper,MaterialMapper,AnimationPlayerAninamtionClipMapper

我们以其中MaterialMapper举例,这个工具导入的模型的时候,是不会导入Material的,那就需要工具帮忙创建一个Material,那么Trilib如何确定使用什么样的Material呢?这就是刚才那个问题上说的。

比如我们使用的是URP,最终选择的MaterialMapper就是UniversalRPMaterialMapper

其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
public class UniversalRPMaterialMapper : MaterialMapper
{
static UniversalRPMaterialMapper()
{
AddToRegisteredMappers();
}

[RuntimeInitializeOnLoadMethod]
private static void AddToRegisteredMappers()
{
if (RegisteredMappers.Contains("UniversalRPMaterialMapper"))
{
return;
}
RegisteredMappers.Add("UniversalRPMaterialMapper");
}

public override Material MaterialPreset => Resources.Load<Material>("Materials/UniversalRP/TriLibUniversalRP");

public override Material AlphaMaterialPreset => Resources.Load<Material>("Materials/UniversalRP/TriLibUniversalRPAlphaCutout");

public override Material AlphaMaterialPreset2 => Resources.Load<Material>("Materials/UniversalRP/TriLibUniversalRPAlpha");

public override Material SpecularMaterialPreset => Resources.Load<Material>("Materials/UniversalRP/TriLibUniversalRPSpecular");

public override Material SpecularAlphaMaterialPreset => Resources.Load<Material>("Materials/UniversalRP/TriLibUniversalRPAlphaCutoutSpecular");

public override Material SpecularAlphaMaterialPreset2 => Resources.Load<Material>("Materials/UniversalRP/TriLibUniversalRPAlphaSpecular");

public override Material LoadingMaterial => Resources.Load<Material>("Materials/UniversalRP/TriLibUniversalRPLoading");

///<inheritdoc />
public override bool IsCompatible(MaterialMapperContext materialMapperContext)
{
return GraphicsSettingsUtils.IsUsingUniversalPipeline;
}

///<inheritdoc />
public override void Map(MaterialMapperContext materialMapperContext)
{
materialMapperContext.VirtualMaterial = new VirtualMaterial();
CheckDiffuseMapTexture(materialMapperContext);
}

private void CheckDiffuseMapTexture(MaterialMapperContext materialMapperContext)
{
var diffuseTexturePropertyName = materialMapperContext.Material.GetGenericPropertyName(GenericMaterialProperty.DiffuseTexture);
if (materialMapperContext.Material.HasProperty(diffuseTexturePropertyName))
{
LoadTexture(materialMapperContext, TextureType.Diffuse, materialMapperContext.Material.GetTextureValue(diffuseTexturePropertyName), ApplyDiffuseMapTexture);
}
else
{
ApplyDiffuseMapTexture(materialMapperContext, TextureType.Diffuse, null);
}
}

private void ApplyDiffuseMapTexture(MaterialMapperContext materialMapperContext, TextureType textureType, Texture texture)
{
materialMapperContext.VirtualMaterial.SetProperty("_BaseMap", texture);
CheckGlossinessValue(materialMapperContext);
}

private void CheckGlossinessValue(MaterialMapperContext materialMapperContext)
{
var value = materialMapperContext.Material.GetGenericPropertyValueMultiplied(GenericMaterialProperty.Glossiness, materialMapperContext.Material.GetGenericFloatValue(GenericMaterialProperty.Glossiness));
materialMapperContext.VirtualMaterial.SetProperty("_Glossiness", value);
CheckMetallicValue(materialMapperContext);
}

private void CheckMetallicValue(MaterialMapperContext materialMapperContext)
{
var value = materialMapperContext.Material.GetGenericFloatValue(GenericMaterialProperty.Metallic);
materialMapperContext.VirtualMaterial.SetProperty("_Metallic", value);
CheckEmissionMapTexture(materialMapperContext);
}

private void CheckEmissionMapTexture(MaterialMapperContext materialMapperContext)
{
var emissionTexturePropertyName = materialMapperContext.Material.GetGenericPropertyName(GenericMaterialProperty.EmissionTexture);
if (materialMapperContext.Material.HasProperty(emissionTexturePropertyName))
{
LoadTexture(materialMapperContext, TextureType.Emission, materialMapperContext.Material.GetTextureValue(emissionTexturePropertyName), ApplyEmissionMapTexture);
}
else
{
ApplyEmissionMapTexture(materialMapperContext, TextureType.Emission, null);
}
}

private void ApplyEmissionMapTexture(MaterialMapperContext materialMapperContext, TextureType textureType, Texture texture)
{
materialMapperContext.VirtualMaterial.SetProperty("_EmissionMap", texture);
if (texture)
{
materialMapperContext.VirtualMaterial.EnableKeyword("_EMISSION");
materialMapperContext.VirtualMaterial.GlobalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
}
else
{
materialMapperContext.VirtualMaterial.DisableKeyword("_EMISSION");
materialMapperContext.VirtualMaterial.GlobalIlluminationFlags = MaterialGlobalIlluminationFlags.EmissiveIsBlack;
}
CheckNormalMapTexture(materialMapperContext);
}

private void CheckNormalMapTexture(MaterialMapperContext materialMapperContext)
{
var normalMapTexturePropertyName = materialMapperContext.Material.GetGenericPropertyName(GenericMaterialProperty.NormalTexture);
if (materialMapperContext.Material.HasProperty(normalMapTexturePropertyName))
{
LoadTexture(materialMapperContext, TextureType.NormalMap, materialMapperContext.Material.GetTextureValue(normalMapTexturePropertyName), ApplyNormalMapTexture);
}
else
{
ApplyNormalMapTexture(materialMapperContext, TextureType.NormalMap, null);
}
}

private void ApplyNormalMapTexture(MaterialMapperContext materialMapperContext, TextureType textureType, Texture texture)
{
materialMapperContext.VirtualMaterial.SetProperty("_BumpMap", texture);
if (texture != null)
{
materialMapperContext.VirtualMaterial.EnableKeyword("_NORMALMAP");
materialMapperContext.VirtualMaterial.SetProperty("_NormalScale", materialMapperContext.Material.GetGenericPropertyValueMultiplied(GenericMaterialProperty.NormalTexture, 1f));
}
else
{
materialMapperContext.VirtualMaterial.DisableKeyword("_NORMALMAP");
}
CheckSpecularTexture(materialMapperContext);
}

private void CheckSpecularTexture(MaterialMapperContext materialMapperContext)
{
var specularTexturePropertyName = materialMapperContext.Material.GetGenericPropertyName(GenericMaterialProperty.SpecularTexture);
if (materialMapperContext.Material.HasProperty(specularTexturePropertyName))
{
LoadTexture(materialMapperContext, TextureType.Specular, materialMapperContext.Material.GetTextureValue(specularTexturePropertyName), ApplySpecGlossMapTexture);
}
else
{
ApplySpecGlossMapTexture(materialMapperContext, TextureType.Specular, null);
}
}

private void ApplySpecGlossMapTexture(MaterialMapperContext materialMapperContext, TextureType textureType, Texture texture)
{
materialMapperContext.VirtualMaterial.SetProperty("_SpecGlossMap", texture);
if (texture != null)
{
materialMapperContext.VirtualMaterial.EnableKeyword("_METALLICSPECGLOSSMAP");
}
else
{
materialMapperContext.VirtualMaterial.DisableKeyword("_METALLICSPECGLOSSMAP");
}
CheckOcclusionMapTexture(materialMapperContext);
}

private void CheckOcclusionMapTexture(MaterialMapperContext materialMapperContext)
{
var occlusionMapTextureName = materialMapperContext.Material.GetGenericPropertyName(GenericMaterialProperty.OcclusionTexture);
if (materialMapperContext.Material.HasProperty(occlusionMapTextureName))
{
LoadTexture(materialMapperContext, TextureType.Occlusion, materialMapperContext.Material.GetTextureValue(occlusionMapTextureName), ApplyOcclusionMapTexture);
}
else
{
ApplyOcclusionMapTexture(materialMapperContext, TextureType.Occlusion, null);
}
}

private void ApplyOcclusionMapTexture(MaterialMapperContext materialMapperContext, TextureType textureType, Texture texture)
{
materialMapperContext.VirtualMaterial.SetProperty("_OcclusionMap", texture);
if (texture != null)
{
materialMapperContext.VirtualMaterial.EnableKeyword("_OCCLUSIONMAP");
}
else
{
materialMapperContext.VirtualMaterial.DisableKeyword("_OCCLUSIONMAP");
}
CheckParallaxMapTexture(materialMapperContext);
}

private void CheckParallaxMapTexture(MaterialMapperContext materialMapperContext)
{
var parallaxMapTextureName = materialMapperContext.Material.GetGenericPropertyName(GenericMaterialProperty.ParallaxMap);
if (materialMapperContext.Material.HasProperty(parallaxMapTextureName))
{
LoadTexture(materialMapperContext, TextureType.Parallax, materialMapperContext.Material.GetTextureValue(parallaxMapTextureName), ApplyParallaxMapTexture);
}
else
{
ApplyParallaxMapTexture(materialMapperContext, TextureType.Parallax, null);
}
}

private void ApplyParallaxMapTexture(MaterialMapperContext materialMapperContext, TextureType textureType, Texture texture)
{
materialMapperContext.VirtualMaterial.SetProperty("_ParallaxMap", texture);
if (texture)
{
materialMapperContext.VirtualMaterial.EnableKeyword("_PARALLAXMAP");
}
else
{
materialMapperContext.VirtualMaterial.DisableKeyword("_PARALLAXMAP");
}
CheckMetallicGlossMapTexture(materialMapperContext);
}

private void CheckMetallicGlossMapTexture(MaterialMapperContext materialMapperContext)
{
var metallicGlossMapTextureName = materialMapperContext.Material.GetGenericPropertyName(GenericMaterialProperty.MetallicGlossMap);
if (materialMapperContext.Material.HasProperty(metallicGlossMapTextureName))
{
LoadTexture(materialMapperContext, TextureType.Metalness, materialMapperContext.Material.GetTextureValue(metallicGlossMapTextureName), ApplyMetallicGlossMapTexture);
}
else
{
ApplyMetallicGlossMapTexture(materialMapperContext, TextureType.Metalness, null);
}
}

private void ApplyMetallicGlossMapTexture(MaterialMapperContext materialMapperContext, TextureType textureType, Texture texture)
{
materialMapperContext.VirtualMaterial.SetProperty("_MetallicGlossMap", texture);
if (texture != null)
{
materialMapperContext.VirtualMaterial.EnableKeyword("_METALLICGLOSSMAP");
}
else
{
materialMapperContext.VirtualMaterial.DisableKeyword("_METALLICGLOSSMAP");
}
CheckEmissionColor(materialMapperContext);
}

private void CheckEmissionColor(MaterialMapperContext materialMapperContext)
{
var value = materialMapperContext.Material.GetGenericColorValue(GenericMaterialProperty.EmissionColor) * materialMapperContext.Material.GetGenericPropertyValueMultiplied(GenericMaterialProperty.EmissionColor, 1f);
materialMapperContext.VirtualMaterial.SetProperty("_EmissionColor", value);
if (value != Color.black)
{
materialMapperContext.VirtualMaterial.EnableKeyword("_EMISSION");
materialMapperContext.VirtualMaterial.GlobalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
}
else
{
materialMapperContext.VirtualMaterial.DisableKeyword("_EMISSION");
materialMapperContext.VirtualMaterial.GlobalIlluminationFlags = MaterialGlobalIlluminationFlags.EmissiveIsBlack;
}
CheckDiffuseColor(materialMapperContext);
}

private void CheckDiffuseColor(MaterialMapperContext materialMapperContext)
{
var value = materialMapperContext.Material.GetGenericColorValue(GenericMaterialProperty.DiffuseColor) * materialMapperContext.Material.GetGenericPropertyValueMultiplied(GenericMaterialProperty.DiffuseColor, 1f);
value.a *= materialMapperContext.Material.GetGenericFloatValue(GenericMaterialProperty.AlphaValue);
if (!materialMapperContext.VirtualMaterial.HasAlpha && value.a < 1f)
{
materialMapperContext.VirtualMaterial.HasAlpha = true;
}
materialMapperContext.VirtualMaterial.SetProperty("_BaseColor", value);
BuildMaterial(materialMapperContext);
}
}

代码中对应的那些预设我们可以直接在文件夹中看到:

而每个Mapper首先构造函数中会将自己的加入到MaterialMapper.RegisteredMappers中,只有在这个变量中,才会进入待选择的列表

而每个Mapper对外暴露的方法只有两个,一个是IsCompatible,也就是使用注意第一个问题中涉及到的变量,另外一个就是Map,在该函数中,会顺序调用所有的私有函数,每个函数分别去检查FBX文件中的某个属性,然后去设置最终使用的Material的属性。

最终会调用到BuildMaterial方法,这个方法的定义很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected void BuildMaterial(MaterialMapperContext materialMapperContext) => materialMapperContext.UnityMaterial = this.InstantiateSuitableMaterial(materialMapperContext);

private Material InstantiateSuitableMaterial(MaterialMapperContext materialMapperContext)
{
Material material;
if (!materialMapperContext.Context.LoadedMaterials.TryGetValue(materialMapperContext.Material, out material))
{
bool flag = materialMapperContext.Context.Options.UseAlphaMaterials && materialMapperContext.VirtualMaterial.HasAlpha && !this.ForceStandardMaterial;
if (materialMapperContext.Material.MaterialShadingSetup == MaterialShadingSetup.Specular)
{
if ((UnityEngine.Object) this.SpecularAlphaMaterialPreset != (UnityEngine.Object) null & flag)
material = UnityEngine.Object.Instantiate<Material>(this.SpecularAlphaMaterialPreset);
else if ((UnityEngine.Object) this.SpecularMaterialPreset != (UnityEngine.Object) null)
material = UnityEngine.Object.Instantiate<Material>(this.SpecularMaterialPreset);
}
if ((UnityEngine.Object) material == (UnityEngine.Object) null)
material = UnityEngine.Object.Instantiate<Material>(flag ? this.AlphaMaterialPreset : this.MaterialPreset);
MaterialMapper.ApplyMaterialProperties(materialMapperContext, material);
materialMapperContext.Context.LoadedMaterials.Add(materialMapperContext.Material, material);
materialMapperContext.Context.Allocations.Add((UnityEngine.Object) material);
}
return material;
}

如果我们加载FBX的时候,也加载进了material,也就是能直接从LoadedMaterials变量中获取到,那就直接使用,如果不行,那就从预设选择一个创建Material。

我们最终模型中的第一个材质就是这么来的,至于该方法何时调用下面再讲。

CreateWebRequest(modelUrl);

这个方法没啥好讲的,就是创建UnityWebRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static UnityWebRequest CreateWebRequest(string uri, HttpRequestMethod httpRequestMethod = HttpRequestMethod.Get, string data = null, int timeout = 2000)
{
UnityWebRequest unityWebRequest;
switch (httpRequestMethod)
{
case HttpRequestMethod.Post:
unityWebRequest = UnityWebRequest.Post(uri, data);
break;
case HttpRequestMethod.Put:
unityWebRequest = UnityWebRequest.Put(uri, data);
break;
case HttpRequestMethod.Delete:
unityWebRequest = UnityWebRequest.Delete($"{uri}?{data}");
break;
case HttpRequestMethod.Head:
unityWebRequest = UnityWebRequest.Head($"{uri}?{data}");
break;
default:
unityWebRequest = UnityWebRequest.Get($"{uri}?{data}");
break;
}
unityWebRequest.timeout = timeout;
return unityWebRequest;
}

LoadModelFromUri

该方法是核心,其定义为:

1
2
3
4
5
6
public static Coroutine LoadModelFromUri(UnityWebRequest unityWebRequest, Action<AssetLoaderContext> onLoad, Action<AssetLoaderContext> onMaterialsLoad, Action<AssetLoaderContext, float> onProgress, Action<IContextualizedError> onError = null, GameObject wrapperGameObject = null, AssetLoaderOptions assetLoaderOptions = null, object customContextData = null, string fileExtension = null, bool? isZipFile = null)
{
var assetDownloader = new GameObject("Asset Downloader").AddComponent<AssetDownloaderBehaviour>();
return assetDownloader.StartCoroutine(assetDownloader.DownloadAsset(unityWebRequest, onLoad, onMaterialsLoad, onProgress, wrapperGameObject, onError, assetLoaderOptions, customContextData, fileExtension, isZipFile));
}

该方法主要是调用了DownloadAsset:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public IEnumerator DownloadAsset(UnityWebRequest unityWebRequest, Action<AssetLoaderContext> onLoad, Action<AssetLoaderContext> onMaterialsLoad, Action<AssetLoaderContext, float> onProgress, GameObject wrapperGameObject, Action<IContextualizedError> onError, AssetLoaderOptions assetLoaderOptions, object customContextData, string fileExtension, bool? isZipFile = null)
{
_unityWebRequest = unityWebRequest;
_onProgress = onProgress;
yield return unityWebRequest.SendWebRequest();
if (unityWebRequest.responseCode < 400)
{
var memoryStream = new MemoryStream(_unityWebRequest.downloadHandler.data);
var uriLoadCustomContextData = new UriLoadCustomContextData
{
UnityWebRequest = _unityWebRequest,
CustomData = customContextData
};
var contentType = unityWebRequest.GetResponseHeader("Content-Type");
if (contentType != null && isZipFile == null)
{
isZipFile = contentType.Contains("application/zip") || contentType.Contains("application/x-zip-compressed") || contentType.Contains("multipart/x-zip");
}
if (isZipFile.GetValueOrDefault())
{
_assetLoaderContext = AssetLoaderZip.LoadModelFromZipStream(memoryStream, onLoad, onMaterialsLoad, delegate (AssetLoaderContext assetLoaderContext, float progress) { onProgress?.Invoke(assetLoaderContext, 0.5f + progress * 0.5f); }, onError, wrapperGameObject, assetLoaderOptions, uriLoadCustomContextData, fileExtension);
}
else
{
_assetLoaderContext = AssetLoader.LoadModelFromStream(memoryStream, null, fileExtension, onLoad, onMaterialsLoad, delegate (AssetLoaderContext assetLoaderContext, float progress) { onProgress?.Invoke(assetLoaderContext, 0.5f + progress * 0.5f); }, onError, wrapperGameObject, assetLoaderOptions, uriLoadCustomContextData);
}
}
else
{
var exception = new Exception($"UnityWebRequest error:{unityWebRequest.error}, code:{unityWebRequest.responseCode}");
if (onError != null)
{
var contextualizedError = exception as IContextualizedError;
onError(contextualizedError ?? new ContextualizedError<AssetLoaderContext>(exception, null));
}
else
{
throw exception;
}
}
Destroy(gameObject);
}

因为我们的是zip文件,所以走进逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static AssetLoaderContext LoadModelFromZipStream(Stream stream, Action<AssetLoaderContext> onLoad, Action<AssetLoaderContext> onMaterialsLoad, Action<AssetLoaderContext, float> onProgress, Action<IContextualizedError> onError = null, GameObject wrapperGameObject = null, AssetLoaderOptions assetLoaderOptions = null, object customContextData = null, string fileExtension = null)
{
var memoryStream = SetupZipModelLoading(ref stream, null, onError, assetLoaderOptions, ref fileExtension, out var zipFile);
return AssetLoader.LoadModelFromStream(memoryStream, null, fileExtension, onLoad, delegate (AssetLoaderContext assetLoaderContext)
{
var zipLoadCustomContextData = assetLoaderContext.CustomData as ZipLoadCustomContextData;
zipLoadCustomContextData?.Stream.Close();
onMaterialsLoad?.Invoke(assetLoaderContext);
}, onProgress, OnError, wrapperGameObject, assetLoaderOptions, new ZipLoadCustomContextData
{
ZipFile = zipFile,
Stream = stream,
CustomData = customContextData
});
}

继续调用LoadModelFromStream:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static AssetLoaderContext LoadModelFromStream(Stream stream, string filename = null, string fileExtension = null, Action<AssetLoaderContext> onLoad = null, Action<AssetLoaderContext> onMaterialsLoad = null, Action<AssetLoaderContext, float> onProgress = null, Action<IContextualizedError> onError = null, GameObject wrapperGameObject = null, AssetLoaderOptions assetLoaderOptions = null, object customContextData = null)
{
#if UNITY_WEBGL || UNITY_UWP
AssetLoaderContext assetLoaderContext = null;
try
{
assetLoaderContext = LoadModelFromStreamNoThread(stream, filename, fileExtension, onError, wrapperGameObject, assetLoaderOptions, customContextData);
onLoad(assetLoaderContext);
onMaterialsLoad(assetLoaderContext);
}
catch (Exception exception)
{
if (exception is IContextualizedError contextualizedError)
{
HandleError(contextualizedError);
}
else
{
HandleError(new ContextualizedError<AssetLoaderContext>(exception, null));
}
}
return assetLoaderContext;
#else
var assetLoaderContext = new AssetLoaderContext
{
Options = assetLoaderOptions == null ? CreateDefaultLoaderOptions() : assetLoaderOptions,
Stream = stream,
FileExtension = fileExtension ?? FileUtils.GetFileExtension(filename, false),
BasePath = FileUtils.GetFileDirectory(filename),
WrapperGameObject = wrapperGameObject,
OnMaterialsLoad = onMaterialsLoad,
OnLoad = onLoad,
HandleError = HandleError,
OnError = onError,
CustomData = customContextData
};
assetLoaderContext.Tasks.Add(ThreadUtils.RunThread(assetLoaderContext, ref assetLoaderContext.CancellationToken, LoadModel, ProcessRootModel, HandleError, assetLoaderContext.Options.Timeout));
return assetLoaderContext;
#endif
}

这里主要是开了个Thread去分别调用LoadModel以及ProcessRootModel

LoadModel内容比较容易理解,就是读取FBX内容,不过这一步读出的很多属性对后续的处理有很大的影响,也是为什么我会有第二个材质,并且决定了我的第二个材质是怎么选择的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static void LoadModel(AssetLoaderContext assetLoaderContext)
{
if (assetLoaderContext.Options.MaterialMappers != null)
{
Array.Sort(assetLoaderContext.Options.MaterialMappers, (a, b) => a.CheckingOrder > b.CheckingOrder ? -1 : 1);
}
else if (assetLoaderContext.Options.ShowLoadingWarnings)
{
Debug.LogWarning("Your AssetLoaderOptions instance has no MaterialMappers. TriLib can't process materials without them.");
}
#if TRILIB_DRACO
GltfReader.DracoDecompressorCallback = DracoMeshLoader.DracoDecompressorCallback;
#endif
var fileExtension = assetLoaderContext.FileExtension ?? FileUtils.GetFileExtension(assetLoaderContext.Filename, false).ToLowerInvariant();
if (assetLoaderContext.Stream == null)
{
var fileStream = new FileStream(assetLoaderContext.Filename, FileMode.Open);
assetLoaderContext.Stream = fileStream;
var reader = Readers.FindReaderForExtension(fileExtension);
if (reader != null)
{
assetLoaderContext.RootModel = reader.ReadStream(fileStream, assetLoaderContext, assetLoaderContext.Filename, assetLoaderContext.OnProgress);
}
}
else
{
var reader = Readers.FindReaderForExtension(fileExtension);
if (reader != null)
{
assetLoaderContext.RootModel = reader.ReadStream(assetLoaderContext.Stream, assetLoaderContext, null, assetLoaderContext.OnProgress);
}
}
}

然后是ProcessRootModel,该函数首先是调用ProcessModel递归引入Model,然后ProcessTextures再处理贴图

1
2
3
4
5
6
7
8
9
10
11
12
private static void ProcessRootModel(AssetLoaderContext assetLoaderContext)
{
ProcessModel(assetLoaderContext);
if (assetLoaderContext.Async)
{
ThreadUtils.RunThread(assetLoaderContext, ref assetLoaderContext.CancellationToken, ProcessTextures, null, assetLoaderContext.HandleError ?? assetLoaderContext.OnError, assetLoaderContext.Options.Timeout);
}
else
{
ProcessTextures(assetLoaderContext);
}
}

ProcessTextures的最后一步是ProcessMaterial,在这里就调用了我们之前选择出来的MaterialMapper的Map方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private static void ProcessMaterials(AssetLoaderContext assetLoaderContext)
{
if (assetLoaderContext.Options.MaterialMappers != null)
{
foreach (var material in assetLoaderContext.RootModel.AllMaterials)
{
MaterialMapper usedMaterialMapper = null;
var materialMapperContext = new MaterialMapperContext
{
Context = assetLoaderContext,
Material = material
};
foreach (var materialMapper in assetLoaderContext.Options.MaterialMappers)
{
if (materialMapper == null || !materialMapper.IsCompatible(materialMapperContext))
{
continue;
}
materialMapper.Map(materialMapperContext);
usedMaterialMapper = materialMapper;
break;
}
if (usedMaterialMapper != null)
{
if (assetLoaderContext.MaterialRenderers.TryGetValue(material, out var materialRendererList))
{
foreach (var materialRendererContext in materialRendererList)
{
usedMaterialMapper.ApplyMaterialToRenderer(materialRendererContext, materialMapperContext);
}
}
}
}
}
else if (assetLoaderContext.Options.ShowLoadingWarnings)
{
Debug.LogWarning("The given AssetLoaderOptions contains no MaterialMapper. Materials will not be created.");
}
}

同时注意这里还调用了ApplyMaterialToRenderer方法,这个方法又创建了一个新的Material出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void ApplyMaterialToRenderer(
MaterialRendererContext materialRendererContext,
MaterialMapperContext materialMapperContext)
{
Material[] sharedMaterials = materialRendererContext.Renderer.sharedMaterials;
sharedMaterials[materialRendererContext.MaterialIndex] = materialMapperContext.UnityMaterial;
materialRendererContext.Renderer.sharedMaterials = sharedMaterials;
if ((!materialMapperContext.Context.Options.UseAlphaMaterials || !materialMapperContext.VirtualMaterial.HasAlpha ? 0 : (!this.ForceStandardMaterial ? 1 : 0)) == 0 || !materialMapperContext.Context.Options.AddSecondAlphaMaterial)
return;
MeshFilter componentInChildren1 = materialRendererContext.Renderer.GetComponentInChildren<MeshFilter>();
SkinnedMeshRenderer componentInChildren2 = materialRendererContext.Renderer.GetComponentInChildren<SkinnedMeshRenderer>();
Mesh mesh = (UnityEngine.Object) componentInChildren1 != (UnityEngine.Object) null ? componentInChildren1.sharedMesh : componentInChildren2?.sharedMesh;
if (!((UnityEngine.Object) mesh != (UnityEngine.Object) null))
return;
Material material = this.InstantiateSuitableSecondPassMaterial(materialMapperContext);
if ((UnityEngine.Object) material == (UnityEngine.Object) null)
return;
int[] triangles = mesh.GetTriangles(materialRendererContext.MaterialIndex);
++mesh.subMeshCount;
mesh.SetTriangles(triangles, mesh.subMeshCount - 1);
List<Material> m = new List<Material>();
materialRendererContext.Renderer.GetSharedMaterials(m);
m.Add(material);
materialRendererContext.Renderer.materials = m.ToArray();
}

最后三行,先为m添加了sharedMaterilas,然后又Add了一个创建的Material,最终一起赋值给Render.materials

总结与解决方案

两个材质出现的简单调用流程可以这样总结:

  • CreateDefaultLoaderOptions的时候选择了一个MaterialMapper
  • 模型下载成功后,通过LoadModel方法导入
  • LoadModel成功后,ProcessRootModel调用ProcessModel来处理Model的属性,使用ProcessTexture来处理贴图
  • ProcessTextures最终会调用ProcessMaterial
  • ProcessMaterial首先会调用第一步选择出来的MaterialMapper的Map方法,创建一个材质出来,然后会调用MaterialMapper的ApplyMaterialToRenderer又创建处一个材质。这两步创建的材质都是根据从事先设置好的模版中选择了一个,而选择的依据是LoadModel导入时自带的那些配置。

解决方案是,在第一步创建assetOptions的时候,把useAlpha那个属性设置为false(默认为true)

loadFbxFromZipFile丢失贴图(v2.1.6)

当你成功将fbx导入后,你会发现,你的材质中可能没有贴图,这个时候需要检查zip包中的贴图名称是否正确,即你的fbx中指定的贴图名称是什么,你的实际的贴图名称就要是什么,trilib是通过文件名进行映射的。

贴图的isReadable为false(v2.1.6)

在升级成2.1.6版本之后,trilib的导入的材质中的贴图是无法读取的,也就是说Read/Write Enable是没有勾选的那种。

这个可以看到官方文档上说,这个是故意的,而且没有选项可以修改这个配置,那么我们就只能去修改它的底层代码了。

最后我选择的是改变MaterialMapper中的ApplyDiffuseMapTexture方法,因为我的项目时URP项目,所以修改的是UniversalUniversalRPMaterialMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
private void ApplyDiffuseMapTexture(TextureLoadingContext textureLoadingContext)
{
if (textureLoadingContext.UnityTexture != null)
{
Texture2D unityTexture = new Texture2D(textureLoadingContext.UnityTexture.width, textureLoadingContext.UnityTexture.height, textureLoadingContext.UnityTexture.graphicsFormat, textureLoadingContext.UnityTexture.mipmapCount, TextureCreationFlags.None);

Graphics.CopyTexture(textureLoadingContext.UnityTexture, unityTexture);
DestroyImmediate(textureLoadingContext.UnityTexture);
textureLoadingContext.UnityTexture = unityTexture;
textureLoadingContext.Context.AddUsedTexture(textureLoadingContext.UnityTexture);
}
textureLoadingContext.MaterialMapperContext.VirtualMaterial.SetProperty("_BaseMap", textureLoadingContext.UnityTexture, GenericMaterialProperty.DiffuseMap);
}