最近在开发一个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 { public class LoadModelFromURLSample : MonoBehaviour { public string modelUrl; private void Start () { var assetLoaderOptions = AssetLoader.CreateDefaultLoaderOptions(); var webRequest = AssetDownloader.CreateWebRequest(modelUrl); AssetDownloader.LoadModelFromUri(webRequest, OnLoad, OnMaterialsLoad, OnProgress, OnError, null , assetLoaderOptions); } private void OnError (IContextualizedError obj ) { Debug.LogError($"An error ocurred while loading your Model: {obj.GetInnerException()} " ); } private void OnProgress (AssetLoaderContext assetLoaderContext, float progress ) { Debug.Log($"Loading Model. Progress: {progress:P} " ); } private void OnMaterialsLoad (AssetLoaderContext assetLoaderContext ) { Debug.Log("Materials loaded. Model fully loaded." ); } 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)到目前为止,笔者还是遇到了一个问题,就是材质除了名字,其他都是不对的,不仅属性不对,还会多一个材质,为此,笔者特地去研究了下源码是怎么回事,发现这东西是故意的。
先来解释下为什么会这样:
CreateDefaultLoaderOptions1 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(); } } 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" ); public override bool IsCompatible (MaterialMapperContext materialMapperContext ) { return GraphicsSettingsUtils.IsUsingUniversalPipeline; } 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); }