Unity - 导出的FBX模型,无法将 vector4 保存在 uv 中(使用 Unity Mesh 保存即可)
目的
备忘,便于日后自己索引
问题
为了学习了解大厂项目的效果:
上周为了将 王者荣耀的 杨玉环 的某个皮肤的头发效果还原
所以我想直接抓模型,再还原 shader
我使用的还是以前的老方法: GPA + 夜神模拟器,具体可以查看以前的另一篇教程,具体参考:教你如何使用GPA导出模型,另送一个 GPA CSV2MESH Tool in unity
抓出来的数据,导出 FBX 后,我看不出什么异常
直到,我逐行的 shader
还原效果的时候
发现 vertex input
数据有 float4 uv1 : TEXCOORD1; float4 uv2 : TEXCOORD2;
但是发现 shader 调试发现,uv1, uv2
使用颜色输出都发现了数据不对的 BUG
然后我还想在 unity Game 视图下,使用 RenderDoc 抓帧分析一下
结果 Load RenderDoc
之后,直接导致 unity 闪退
瞄了一下 CSharp 代码,发现我使用的是 Mesh.uv
API,getter and setter 都是 Vector2[]
的,所以 zw
是不可能设置上的
然后瞄了一下 Mesh
是有 void SetUVs(int channel, Vector4[] uvs)
的 API 的
但是经过测试,还是发现 UV的 zw 无法保存下来
最终我问了一下unity 技术官方,结果他们测试是OK的 (因为他们是对 Mesh 内存数据的实时修改)
然后我也试了一下,确实OK,但是经过自己跟进一步测试,发现使用 FBX Exporter 导出之后,UV 还是会丢失的
我将测试总结一下: unity Mesh 中会保存 uv vector4 的数据,到时经过 FBX Exporter 插件导出之后,uv 就不可能保存 Vector4 了
然后我分析了一下 FBX Exporter 插件的代码
发现一丢丢问题:
-
我将 FBX Exporter Local 化后,再按照我下面截图的内容,修改后,还是无法导出 (如何 local 化,可以参考我之前的文章:Unity - 如何修改一个 Package 或是如何将 Package Local化 )
-
发现 Unity 中 AutoDesk 的 package 里面封装的 API
FbxLayerElemetnUV.Create
进入是继承UV2
的
也要先 local,但是这个 package 比较特殊,在 PackageManager 中不显示的,方法可以是先从 Library/PackageCache/com.autodesk.fbx@4.2.0 剪贴到 [项目目录]/Packages/下面,然后使用 Package add from disk 的方式
然后再开始修改代码
从public class FbxLayerElementUV : FbxLayerElementTemplateFbxVector2
修改为
新public class FbxLayerElementUV : FbxLayerElementTemplateFbxVector4
结果发现还是不行
因为之前说的,unity editor 下,无论 game view, 还是 scene view
直接 Load RenderDoc 都会导致unity 闪退
然后我再使用 RenderDoc + 真机 抓帧分析,果然是没有 vertex input TEXCOORD0 zw 分量数据的
解决方案
于是我就有点怀疑 FBX 是不能保存 uv 超过4 分量数据的
然后百度: ‘fbx 文本 file header’ 找到这篇:
- FBX文件结构解读【文本格式】
- 译文原始地址在这:FBX文件结构解读
- 翻译之前的原文在这:A quick tutorial about the FBX ASCII format
google ‘fbx ascii file header’ 找到:
- FBX binary file format specification - blender 的
再 ‘How to save uv data more than 4 components in fbx file’ 找到: - FBX export/import only supports Vector2 in UV (but the uvs can contain upto Vector4 in Unity) - 这个人遇到的问题,和我一模一样,里面的解决方式就是使用 AssetData.CreateAsset(mesh, path) 的方式来解决的
经过前面 (还有很多篇)
看完 ascii 格式的 FBX 头文件后,我就知道,uv 存不了 vector4 了,那我就在猜
王者荣耀 也是使用 unity 开发的,难不成他们 TEXCOORD[N] 保存超过 2 个以上的分量数据都是使用 unity Mesh 的方式来保存的吗?
验证
- 试一下 unity mesh 能否成功 - OK
- 测试一下 *.obj 格式能否将 uv 保存超过 2 个分量以上的数据 - OK,但是AB打包可能不会打进去(目录中注意的部分会有讲到)
保存为 Unity Mesh 结果 - OK
先构建uv数据
然后设置数据
然后 shader 打印
之前的z是全黑色,w全白色,现在都有对应的强度了,OK,说明 unity mesh 还是OK的
想要了解 unity mesh 如何保存数据,我们可以将 AssetDatabase.CreateAsset
之后的 Mesh.asset
文件用文本编辑器打开,瞄一下就好啦
保存为 *.obj 文件结果 - not OK,但是可以 DIY importer
首先我们用 blender 简单整一个 cube,将 uv 展好,如下
然后导出 *.obj 放到 unity 里面瞄一下,如下图
然后我们直接给 obj 里面的 vt 增加 字段数据的分量,看看 unity 有否变化,然后发现是没有变化的
然后我们发现修改不了 原始的 .obj 里面在 library 下的 mesh cache 信息 (.fbx) 同样如此
比如下面的代码,我将问题写在注释了
var assetObj = AssetDatabase.LoadAssetAtPath<Object>(assetPath); // assetObj == null
var modelPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath); // modelPrefab == null
var mesh_filter = modelPrefab.GetComponentInChildren<MeshFilter>(); // 所有导致 modelPrefab 出现空引用的 BUG
完整如下
public class AssetsImporterExt : AssetPostprocessor
{
private void OnPreprocessModel()
{
var mi = assetImporter as ModelImporter;
if (mi == null) return;
// assetPath == "Assets/Test/Test_uv.obj"
var assetPath = assetImporter.assetPath;
var assetObj = AssetDatabase.LoadAssetAtPath<Object>(assetPath); // assetObj == null
var modelPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath); // modelPrefab == null
var mesh_filter = modelPrefab.GetComponentInChildren<MeshFilter>(); // 所有导致 modelPrefab 出现空引用的 BUG
if (mesh_filter == null) return;
var mesh = mesh_filter.sharedMesh;
var uvs = new List<Vector4>();
mesh.GetUVs(0, uvs);
var ext = System.IO.Path.GetExtension(assetPath).ToLower();
if (ext == ".obj")
{
var spliter = new string[] { " " };
var sb = new StringBuilder();
var dirty = false;
using(var reader = new StreamReader(assetPath))
{
var idx = 0;
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (line.StartsWith("vt"))
{
sb.Clear();
var args = line.Split(spliter, System.StringSplitOptions.RemoveEmptyEntries);
if (args.Length > 3) sb.Append(args[3]);
if (args.Length > 4) sb.Append(" " + args[4]);
Debug.Log($"extension uv data zw : {sb}");
var uv_data = uvs[idx]; // get from array
if (args.Length > 3)
{
if (!float.TryParse(args[3], out float val)
|| float.IsNaN(val)
|| float.IsInfinity(val)
)
{
val = 0f;
}
uv_data.z = val; // update z component
}
if (args.Length > 4)
{
if (!float.TryParse(args[4], out float val)
|| float.IsNaN(val)
|| float.IsInfinity(val)
)
{
val = 0f;
}
uv_data.w = val; // update w component
}
uvs[idx] = uv_data; // update to array
++idx;
dirty = true;
} // end of if (line.StartsWith("vt"))
} // end of while (!reader.EndOfStream)
} // end of using(var reader = new StreamReader(assetPath))
if (dirty)
{
EditorUtility.SetDirty(mesh);
EditorUtility.SetDirty(modelPrefab);
AssetDatabase.SaveAssetIfDirty(modelPrefab);
}
} // end of if (ext == ".obj")
}
既然 原始模型的 mesh 修改不了,那么我们可以处理 prefab 里面的 mesh,下面进行尝试一下
其实这帖子 FBX export/import only supports Vector2 in UV (but the uvs can contain upto Vector4 in Unity) 里面也有人是这样的思路,如下图
先来一段代码,看看能否修改成功
private void OnPostprocessPrefab(GameObject gameObject)
{
var mf = gameObject.GetComponentInChildren<MeshFilter>();
if (mf == null) return;
var mesh_path = AssetDatabase.GetAssetPath(mf.sharedMesh);
Debug.Log($"mehs_path : {mesh_path}");
var mesh = mf.sharedMesh;
var uvs = new List<Vector4>();
mesh.GetUVs(0, uvs);
var ext = System.IO.Path.GetExtension(mesh_path).ToLower();
if (ext == ".obj")
{
var spliter = new string[] { " " };
var sb = new StringBuilder();
var dirty = false;
using (var reader = new StreamReader(mesh_path))
{
var idx = 0;
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (line.StartsWith("vt"))
{
sb.Clear();
var args = line.Split(spliter, System.StringSplitOptions.RemoveEmptyEntries);
if (args.Length > 3) sb.Append(args[3]);
if (args.Length > 4) sb.Append(" " + args[4]);
Debug.Log($"extension uv data zw : {sb}");
var uv_data = uvs[idx]; // get from array
if (args.Length > 3)
{
if (!float.TryParse(args[3], out float val)
|| float.IsNaN(val)
|| float.IsInfinity(val)
)
{
val = 0f;
}
uv_data.z = val; // update z component
}
if (args.Length > 4)
{
if (!float.TryParse(args[4], out float val)
|| float.IsNaN(val)
|| float.IsInfinity(val)
)
{
val = 0f;
}
uv_data.w = val; // update w component
}
uvs[idx] = uv_data; // update to array
++idx;
dirty = true;
} // end of if (line.StartsWith("vt"))
} // end of while (!reader.EndOfStream)
} // end of using(var reader = new StreamReader(assetPath))
if (dirty)
{
mesh.SetUVs(0, uvs);
EditorUtility.SetDirty(mesh);
EditorUtility.SetDirty(gameObject);
AssetDatabase.SaveAssetIfDirty(gameObject);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
} // end of if (ext == ".obj")
}
OK,有了上面的 postprocess 代码 + prefab,我们 reimport 测试一下
可以看到 Test_uv.obj 里面的 mesh 的 uv 从 flaot2 变成了 float4 了,如下图
然后我们看一下 测试 shader 的效果,发现是有数据异常的,一部分有设置成功,一部分没有,那么很有可能是 *.obj 的顶点数解析和unity不一样
首先,瞄一下,*.obj 里面有 14 条 uv 信息 xy 分量是原来的,后面的 zw (0.25, 0.5) 都是我后续增加的
然后我们断点发现,unity 解析出来,会有 24 个 uv 信息,如下图
观察了一下规律,可以发现,他将一些多面共点,拆分为分别的三角面的对应的独立点
因此我们可以根据 uv.xy 如果坐标相同,那么我们就将 uv.zw 记录一份,共享这些 uv.xy 的数据的 zw 数据即可
继续修改一下代码
private void OnPostprocessPrefab(GameObject gameObject)
{
var mf = gameObject.GetComponentInChildren<MeshFilter>();
if (mf == null) return;
var mesh_path = AssetDatabase.GetAssetPath(mf.sharedMesh);
Debug.Log($"mehs_path : {mesh_path}");
var mesh = mf.sharedMesh;
var uvs = new List<Vector4>();
mesh.GetUVs(0, uvs);
var ext = System.IO.Path.GetExtension(mesh_path).ToLower();
if (ext == ".obj")
{
var dict = new Dictionary<string, Vector2>();
var spliter = new string[] { " " };
var sb = new StringBuilder();
var dirty = false;
using (var reader = new StreamReader(mesh_path))
{
var idx = 0;
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (line.StartsWith("vt"))
{
sb.Clear();
var args = line.Split(spliter, System.StringSplitOptions.RemoveEmptyEntries);
if (args.Length > 3) sb.Append(args[3]);
if (args.Length > 4) sb.Append(" " + args[4]);
Debug.Log($"extension uv data zw : {sb}");
var uv_data = uvs[idx]; // get from array
var key1 = float.Parse(args[1]).ToString("0.000000");
var key2 = float.Parse(args[2]).ToString("0.000000");
var key = $"{key1},{key2}";
if (!dict.TryGetValue(key, out var zwVec))
{
if (args.Length > 3)
{
if (!float.TryParse(args[3], out float val)
|| float.IsNaN(val)
|| float.IsInfinity(val)
)
{
val = 0f;
}
uv_data.z = val; // update z component
}
if (args.Length > 4)
{
if (!float.TryParse(args[4], out float val)
|| float.IsNaN(val)
|| float.IsInfinity(val)
)
{
val = 0f;
}
uv_data.w = val; // update w component
}
zwVec.x = uv_data.z;
zwVec.y = uv_data.w;
dict[key] = new Vector2(zwVec.x, zwVec.y); // update to dict
}
uvs[idx] = uv_data; // update to array
++idx;
dirty = true;
} // end of if (line.StartsWith("vt"))
} // end of while (!reader.EndOfStream)
} // end of using(var reader = new StreamReader(assetPath))
if (dirty)
{
// 将 xy 相同的都共用 uv.zw 数据
for (int i = 0; i < uvs.Count; i++)
{
var uv = uvs[i];
var key = $"{uv.x.ToString("0.000000")},{uv.y.ToString("0.000000")}";
if (dict.TryGetValue(key, out var zwVec))
{
uv.z = zwVec.x;
uv.w = zwVec.y;
uvs[i] = uv;
}
}
mesh.SetUVs(0, uvs);
EditorUtility.SetDirty(mesh);
EditorUtility.SetDirty(gameObject);
AssetDatabase.SaveAssetIfDirty(gameObject);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
} // end of if (ext == ".obj")
}
查看渲染结果,正常了
然后我们试试修改 *.obj 里面的uv 扩展数据瞄一下效果如何
最后的渲染效果如下
注意
-
*.obj 这种方式暂时没去验证能否将打包出来的 ab 里面的 mesh 修改(因为里头的文件信息是再 library 里面的临时生成的问题,打包不会打包进去)
-
但是使用 *.asset 来保存 unity mesh 的方式肯定可以,因为变成了文件信息