Unity – Exported FBX model cannot save vector4 in uv (just use Unity Mesh to save it)

Article directory

  • Purpose
  • question
  • solution
  • verify
  • Save as Unity Mesh result – OK
  • Save as *.obj file result – not OK, but DIY importer
  • Notice
  • References

Purpose

Memo for easy indexing in the future

Question

In order to learn and understand the effects of large factory projects:
Last week, in order to restore the hair effect of a certain skin of Yang Yuhuan in Glory of Kings
So I want to grab the model directly and then restore the shader
I still use the old method: GPA + Yeshen simulator. For details, you can check out another previous tutorial. For specific reference: teach you how to use GPA to export models, and give you a GPA CSV2MESH Tool in unity.

After exporting the captured data to FBX, I can’t see any abnormalities.

Until, I restored the effect line by line with shader
Found that vertex input data has float4 uv1 : TEXCOORD1; float4 uv2 : TEXCOORD2;

However, I found that shader debugging found that uv1, uv2 used color output and found a bug with incorrect data.

Then I also want to use RenderDoc to capture frames and analyze them in the unity Game view.
As a result, after Load RenderDoc, it directly causes unity to crash.

I took a look at the CSharp code and found that I was using the Mesh.uv API. The getters and setters are both Vector2[], so zw is Impossible to set

Then I took a look and saw that Mesh has the API of void SetUVs(int channel, Vector4[] uvs)

However, after testing, it was found that the UV zw could not be saved.

Finally, I asked the unity technical official, and their test was OK (because they modified the Mesh memory data in real time)
Then I tried it, and it was indeed OK. However, after further testing myself, I found that after exporting using FBX Exporter, the UVs would still be lost.
I will summarize the test: Unity Mesh will save uv vector4 data. After exporting through the FBX Exporter plug-in, uv will not be able to save Vector4

Then I analyzed the code of the FBX Exporter plug-in
Found a few problems:

  1. After I localized the FBX Exporter, and then modified it according to the screenshot below, it still cannot be exported (for how to localize, you can refer to my previous article: Unity – How to modify a Package or how to localize a Package)

  2. It is found that the API FbxLayerElemetnUV.Create encapsulated in the AutoDesk package in Unity inherits UV2
    You also need to go local first, but this package is special and is not displayed in the PackageManager. You can cut and paste it from Library/PackageCache/[email protected] to [Project Directory]/Packages/, and then use Package add from disk method
    Then start modifying the code
    Modified from public class FbxLayerElementUV : FbxLayerElementTemplateFbxVector2 to
    New public class FbxLayerElementUV : FbxLayerElementTemplateFbxVector4
    It turned out that it still didn’t work.


Because as mentioned before, under the unity editor, no matter game view or scene view
Directly loading RenderDoc will cause unity to crash.
Then I used RenderDoc + real machine frame capture analysis, and sure enough, there was no vertex input TEXCOORD0 zw component data.

Solution

So I had some doubts that FBX cannot save UV data with more than 4 components.
Then Baidu: fbx text file header’ found this article:

  • Interpretation of FBX file structure [text format]
    • The original address of the translation is here: Interpretation of FBX file structure
    • The original text before translation is here: A quick tutorial about the FBX ASCII format

Google fbx ascii file header’ found:

  • FBX binary file format specification – blender
    Then How to save uv data more than 4 components in fbx file’ found:
  • FBX export/import only supports Vector2 in UV (but the uvs can contain upto Vector4 in Unity) – This person encountered the same problem as me, and the solution was to use AssetData.CreateAsset(mesh, path) of

After going through the previous section (there are many more articles)

After reading the FBX header file in ascii format, I knew that uv could not save vector4, so I was guessing

Honor of Kings is also developed using unity. Is it possible that they use unity Mesh to save more than 2 component data in TEXCOORD[N]?

Verification

  • Try unity mesh to see if it works – OK
  • Test whether the *.obj format can save uv data with more than 2 components – OK, but the AB package may not be included (it will be mentioned in the Note part of the directory)

Save as Unity Mesh result – OK

Build uv data first

Then set the data

Then the shader prints

Previously, z was all black and w was all white. Now they have corresponding intensities. OK, indicating that the unity mesh is still OK.
To understand how unity mesh saves data, we can open the Mesh.asset file after AssetDatabase.CreateAsset with a text editor and take a look.

Save as *.obj file result – not OK, but DIY importer

First, we use blender to simply create a cube and expand the uv, as follows

Then export *.obj and put it in unity to take a look, as shown below

Then we directly add the field data component to vt in obj to see if there is any change in unity, and then we find that there is no change.

Then we found that the mesh cache information (.fbx) in the original .obj under library cannot be modified. The same is true.

For example, in the following code, I wrote the question in the comments

 var assetObj = AssetDatabase.LoadAssetAtPath<Object>(assetPath); // assetObj == null
        var modelPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath); // modelPrefab == null
        var mesh_filter = modelPrefab.GetComponentInChildren<MeshFilter>(); // All bugs that cause null references to modelPfab

Complete as follows

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>(); // All bugs that cause null references to modelPfab
        if (mesh_filter == null) return;
        var mesh = mesh_filter.sharedMesh;
        var uvs = new List();
        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))
            {
                varidx = 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")
    }

Since the mesh of the original model cannot be modified, we can process the mesh in the prefab. Let’s try it below.
In fact, someone in this post FBX export/import only supports Vector2 in UV (but the uvs can contain upto Vector4 in Unity) has this idea, as shown below

Let’s start with a piece of code to see if it can be modified successfully.

 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))
            {<!-- -->
                varidx = 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, with the above postprocess code + prefab, let’s reimport and test it

You can see that the uv of the mesh in Test_uv.obj has changed from flaot2 to float4, as shown below

Then we take a look at the effect of testing the shader and find that there are data anomalies. Some of them are set successfully and some are not. Then it is very likely that the vertex number analysis of *.obj is different from that of Unity.

First, take a look. There are 14 pieces of uv information in *.obj. The xy components are the original ones, and the following zw (0.25, 0.5) are all added by me later.

Then we breakpointed and found that after unity parsed it, there would be 24 uv information, as shown below

After observing the pattern, we can find that he split some polygonal faces into corresponding independent points of respective triangular faces.
Therefore, if the coordinates of uv.xy are the same, we can record a copy of uv.zw and share the zw data of these uv.xy data.
Continue to modify the code

 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))
            {<!-- -->
                varidx = 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)
            {<!-- -->
                // Share uv.zw data with the same xy
                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")
    }

Check the rendering result, it’s normal

Then let’s try modifying the uv extension data in *.obj and see how it works.

The final rendering effect is as follows

Note

  • *.obj This method has not yet been used to verify whether the mesh in the packaged ab can be modified (because the file information inside is temporarily generated in the library and will not be included in the package)

  • But it is definitely possible to use *.asset to save unity mesh, because it becomes file information

References

  • FBX export/import only supports Vector2 in UV (but the uvs can contain upto Vector4 in Unity)
syntaxbug.com © 2021 All Rights Reserved.