Unity - 导出的FBX模型,无法将 vector4 保存在 uv 中(使用 Unity Mesh 保存即可)
文章目录
- 目的
- 问题
- 解决方案
- 验证
- 保存为 Unity Mesh 结果 - OK
- 保存为 *.obj 文件结果 - not OK,但是可以 DIY importer
- 注意
- References
目的
备忘,便于日后自己索引
问题
为了学习了解大厂项目的效果:
上周为了将 王者荣耀的 杨玉环 的某个皮肤的头发效果还原
所以我想直接抓模型,再还原 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 == nullvar modelPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath); // modelPrefab == nullvar 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 == nullvar modelPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath); // modelPrefab == nullvar mesh_filter = modelPrefab.GetComponentInChildren<MeshFilter>(); // 所有导致 modelPrefab 出现空引用的 BUGif (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 arrayif (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 arrayif (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 arrayvar 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 的方式肯定可以,因为变成了文件信息
References
- FBX export/import only supports Vector2 in UV (but the uvs can contain upto Vector4 in Unity)
相关文章:
Unity - 导出的FBX模型,无法将 vector4 保存在 uv 中(使用 Unity Mesh 保存即可)
文章目录 目的问题解决方案验证保存为 Unity Mesh 结果 - OK保存为 *.obj 文件结果 - not OK,但是可以 DIY importer注意References 目的 备忘,便于日后自己索引 问题 为了学习了解大厂项目的效果: 上周为了将 王者荣耀的 杨玉环 的某个皮肤…...
【疯狂Java】数组
1、一维数组 (1)初始化 ①静态初始化:只指定元素,不指定长度 new 类型[] {元素1,元素2,...} int[] intArr; intArr new int[] {5,6,7,8}; ②动态初始化:只指定长度,不指定元素 new 类型[数组长度] int[] princes new in…...
leetcode 503. 下一个更大元素 II、42. 接雨水
下一个更大元素 II 给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数&…...
【德哥说库系列】-PostgreSQL跨版本升级
📢📢📢📣📣📣 哈喽!大家好,我是【IT邦德】,江湖人称jeames007,10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】!😜&am…...
rust学习——智能指针
智能指针 在各个编程语言中,指针的概念几乎都是相同的:指针是一个包含了内存地址的变量,该内存地址引用或者指向了另外的数据。 在 Rust 中,最常见的指针类型是引用,引用通过 & 符号表示。不同于其它语言…...
系列一、Spring Framework
一、谈谈你对Spring的理解 Spring是一个生态,是一个轻量级的开源容器框架,可以构建Java应用所需要的一切基础设施,它的出现是为了解决企业级应用开发中业务逻辑层和其他各层对象与对象之间耦合的问题,通常情况下所说的Spring是指S…...
PULP Ubuntu18.04
1. 安装eda工具:questasim_10.7_linux64,网上有教程和方法,如有问题,可私信我 2. 代码下载: git clone https://github.com/pulp-platform/pulp 编译代码 cd pulp source setup/vsim.sh make checkout make scripts …...
Docker harbor私有仓库部与管理
目录 搭建本地私有仓库 Docker容器的重启策略 Harbor 简介 什么是Harbor Harbor的特性 Harbor的构成 Docker harbor私有仓库部署 Harbor.cfg配置文件中的参数 维护管理Harbor 总结 搭建本地私有仓库 #首先下载 registry 镜像 docker pull registry#在 daemon.json …...
解决虚拟机联网问题
虚拟机开机后发现右上角缺少联网标志(下面有正常联网标志),这样就是连不上网的 不信你可以打开Ubuntu里面的浏览器或ping www.baidu.com 1.编辑虚拟机设置-->网络适配器-->如图所示 2.选择编辑-->虚拟网络编辑器 3.更改设置 4此处可以选择还原默认设置&am…...
Unity 自定义小地图
最近工作做了个小地图,再此记录下思路。 1、准备所需素材 显示为地图(我们取顶视图)。创建一个Cube,缩放到可以把实际地图包住。实际地图的尺寸和偏移量 。我这里长宽都是25,偏移量(1,0&…...
力扣每日一题66:加一
题目描述: 给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。 最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。 你可以假设除了整数 0 之外,这个整数不会以零开头。 示例 1: 输…...
项目管理工具ConceptDraw PROJECT mac中文版自定义列功能
ConceptDraw PROJECT Mac是一款专业的项目管理工具,适用于MacOS平台。它提供了成功规划和执行项目所需的完整功能,包括任务和资源管理、报告和变更控制。 这款软件可以与ConceptDraw office集成,利用思维导图和数据可视化的强大功能来改进项目…...
Kafka-Java二:Spring实现kafka消息发送的ack机制
写在前面 如果只有一个kafka实例的话,那么文章中提到kafka集群kafka实例 一、什么是消息发送者端的ack机制 ack机制:消息确认发送成功的标识 由谁发起该标识:kafka集群 发起该标识的场景:kafka集群确认已经收到了消息。 由谁接收…...
Go代码解密:理解byte和int8的边界行为
今天看到一个很有意思的 Golang 代码,最后的输出结果为 4: func main() {count : 0for i : range [256]struct{}{} {m, n : byte(i), int8(i)if n -n {count}if m -m {count}}fmt.Println(count) // 输出为 4 }原因如下 当 i 0 时,n -n …...
Mac M1下使用Colima替代docker desktop搭建云原生环境
文章目录 为什么不使用docker desktop1.docker desktop卸载2.docker、docker compose安装3.colima安装3.1获取镜像地址3.2将下载好的iso文件放到colima指定路径3.3重新执行colima start 4.minikukekubernetes安装5.关闭minikube Mac M1下使用Colima替代docker desktop搭建云原生…...
Non-constant range: argument must be an integer literal
更新 Xcode IDE 后 ForEach 方法抛出了如下异常 Non-constant range: argument must be an integer literal 新增了指向性 id 参数 init(_:content:) 原始方法 ForEach(0 ..< pickerTitleColors.count) {Text(self.pickerTitleColors[$0]).tag($0).foregroundColor(self.…...
TCP网络通信
TCP通信的 实现发1收1 package TCP1;//完成TCP通信的 实现发1收1import java.io.DataOutputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket;public class Client {public static void main(S…...
echarts中,X轴名称过长隐藏,鼠标hove显示全称
echarts中,X轴名称过长隐藏,鼠标hove显示全称: <div id"main" :style"{ width: 100%, height: 100% }"></div>option: {title: {text: 重点物料库存预警,left: center},tooltip: {trigger: axis,axisPointer…...
laravel框架介绍(二) 打开站点:autoload.php报错
Laravel:require..../vendor/autoload.php错误的解决办法 打开站点:http://laraveltest.com:8188/set_api-master/public/ set_api-master\public\index.php文件内容为: 解决办法: 1. cd 到该引用的根目录,删除 compo…...
reactNative导入excel文件
组件内导入 import {TouchableOpacity,PermissionsAndroid} from react-native; import RNFS from react-native-fs; import XLSX from xlsx; import DocumentPicker from react-native-document-picker; import {Buffer} from buffer;// 需要安装一下三个,Buffer和react-nati…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...
OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...
LeetCode - 199. 二叉树的右视图
题目 199. 二叉树的右视图 - 力扣(LeetCode) 思路 右视图是指从树的右侧看,对于每一层,只能看到该层最右边的节点。实现思路是: 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...
