从零手写 FBX ASCII 解析器:Unity-Runtime-Animation-Recorder 的 FbxDataNode 递归下降解析原理

发布时间:2026/8/25 10:04:00
从零手写 FBX ASCII 解析器:Unity-Runtime-Animation-Recorder 的 FbxDataNode 递归下降解析原理 从零手写 FBX ASCII 解析器Unity-Runtime-Animation-Recorder 的 FbxDataNode 递归下降解析原理【免费下载链接】Unity-Runtime-Animation-RecorderRecord animations in Unity runtime. Can save to .anim, maya, or FBX ASCII format.项目地址: https://gitcode.com/gh_mirrors/un/Unity-Runtime-Animation-RecorderUnity-Runtime-Animation-Recorder 是一款可在 Unity 运行时录制动画的开源工具支持把录制结果保存为.anim、Maya.ma或FBX ASCII三种格式。其中最硬核的部分是 FBX 导出功能作者没有依赖任何官方 SDK而是从零手写了FBX ASCII 解析器——用FbxDataNode类以递归下降的方式把整个 FBX 文件解析成一棵可编辑的节点树改完再序列化回文件。本文带你完整拆解这套原理。为什么不用 SDK而要手写解析器很多开发者听到 FBX 第一反应是去找官方 SDK但那个 SDK 体积大、依赖重只想改几个动画数据时显得杀鸡用牛刀。作者的思路很巧妙FBX ASCII 是纯文本导出 3D 软件里的 FBX 时可选 ASCII 或 Binary 格式ASCII 版本用肉眼就能读懂结构高度规则整个文件就是名称: 数据 花括号嵌套天然适合递归下降零依赖只用 .NET 标准库StringReader 正则几百分行代码搞定换句话说解析一个自己看得懂的小格式比接入一个庞大的 SDK 更快。FBX ASCII 文件长什么样理解解析器之前先看一份典型结构示意global: { Version: 7308 ObjectDefinitions: { Count: 10 } } Objects: { Geometry: 12345, Cube, Mesh { Vertices: *24 { a: 0,0,0, 1,0,0, 1,1,0, ... } } }仔细观察所有行其实只有三种行类型特征例子节点头含{Objects: {属性行含:Version: 7308数据续行什么都没有a: 0,0,0, ...的后续内容FbxDataNode的整个解析策略就是围绕这三种行做状态判断。节点类设计FbxDataNode解析器本体在Unity Runtime Recorder/Scripts/FbxExporter/FbxDataNode.cs中。每个节点就是一个极简的数据类核心字段只有 5 个字段含义nodeName节点名如Objects、VersionnodeData节点头部数据如12345, Cube, Meshlevel嵌套深度决定序列化时的缩进subNodes子节点列表构成树的骨架hasSubNode是否有子节点决定是否输出花括号没有复杂的继承体系一个类同时扮演语法树节点 文本序列化器两个角色——简单是这套设计最大的优点。核心FetchNodes 的递归下降过程整个解析的入口是静态方法FbxDataNode.FetchNodes(inputData, level)它的工作流程可以概括为 4 步第 1 步逐行扫描用StringReader逐行读取输入文本。第 2 步遇到{→ 发现一个新节点开始下降这一行是节点头。先用正则抽出节点名和头部数据然后进入括号计数循环——这是递归下降的灵魂int bracketNum 1; // 节点头的 { 先计 1 while (reader.Peek() ! -1) { string line reader.ReadLine(); if (line.IndexOf({) ! -1) bracketNum; // 更深一层 else if (line.IndexOf(}) ! -1) { --bracketNum; if (bracketNum 0) break; // 配对闭合本节点结束 } contentInsideBracket line \n; }bracketNum就像一根指针只关心花括号是否配平遇到{就往下钻一层遇到}就退一层直到归零这个节点的内容就完整收进contentInsideBracket了。第 3 步递归处理子内容真正的下降拿到节点内容后用同一函数递归解析它FbxDataNode[] subNodes FbxDataNode.FetchNodes(contentInsideBracket, level 1);注意level 1每下降一层深度加 1最终用于序列化时输出正确数量的缩进 Tab。这就是递归下降的全部奥秘——函数自己调用自己用调用栈代替显式的深度管理。第 4 步遇到:或什么都不含的行含:的行是属性行如Version: 7308直接建一个叶子节点加入当前层什么都没有的行视为上一个节点的数据续行当level 1时把该行追加到最后一个节点的nodeData上。这个设计解决了长数组数据跨越多行的问题比如顶点坐标列表递归的出口很自然子内容里不再有{递归返回空列表树就长好了。GetNodeData用正则抽离 名称: 数据解析头部的辅助函数GetNodeData按行类型切换两套正则行类型正则模式说明节点头含{([^:]*):\s([^{]*)\{捕获冒号前的名称和冒号后、{前的数据属性行不含{([^:]*):\s([^\n]*)\n捕获冒号前的名称和整行数据匹配成功后还会清理 Tab 和行尾空格。两个分组分别写入nodeName和nodeData。反向操作getResultData 把树变回文本解析不是目的解析 → 修改 → 写回才是完整闭环。getResultData()是解析的逆操作按level生成缩进 Tab输出nodeName : nodeData若有子节点输出{递归拼接所有子节点的序列化结果最后输出}因为解析和序列化是严格互逆的一对所以修改 FBX 文件被转化成了简单的树操作找到目标节点、addSubNode挂上新节点、重新序列化即可。内存优化把大节点卸载到磁盘FBX 里Objects块动辄包含数万行曲线数据全挂在内存里会非常吃资源。FbxDataNode内置了一个磁盘卸载机制saveDataOnDisk(folder)把子节点序列化后写入磁盘临时文件然后清空内存中的子节点列表只保留文件路径最终序列化时如果标记了isDataInFile就从磁盘读回内容拼进结果clearSavedData()导出结束后递归删除所有临时文件项目 README 也提到 FBX 导出器存在内存问题、不能导出过于复杂的对象——这正是该设计的取舍点读到这里你基本能理解原因了。完整导出流程串起来Unity Runtime Recorder/Scripts/FbxExporter/FbxExporter.cs的ExportToFile协程展示了这套解析器的完整用法复制源文件逐行拷贝源 FBX顺便把所有PreRotation行替换为 0避免 Unity 与 3D 软件的坐标系差异整体解析FetchNodes(File.ReadAllText(...), 0)得到根节点数组定位目标找到Objects节点交给FbxObjectsManager见FbxObjectsManager.cs逐个挂入录制的位置/旋转/缩放动画曲线节点修改定义ModifyDefinitions重新解析文件向Definitions块追加动画曲线类型声明写回文件EditTargetFile再次用括号计数法定位Objects块的起止位置把修改后的树序列化结果拼接替换进原文件——头部、尾部原样保留只换中间Connections 同理最后clearSavedData清理磁盘临时数据可以看到括号计数bracket balancer贯穿了解析与写回的始终它是这套轻量方案里出现频率最高的技巧。从这套实现里能学到什么 三个可迁移的经验递归下降 括号计数是处理花括号嵌套文本最简单可靠的方案不必上 parser 生成器解析与序列化严格互逆改文件就变成改树代码意图一目了然大节点磁盘卸载是纯 C# 实现中对抗内存的有效手段同时它的局限也值得留意假设每个节点头独占一行、未处理字符串内部出现花括号的情况、静态计数器nowDataId不适合多线程——如果你要把它用于生产环境这些边界情况需要加固。从零手写解析器看似重复造轮子但当目标格式简单且文本可读时它换来的轻量、可控和易调试远比接入一个重量级 SDK 划算。这套FbxDataNode的递归下降实现正是一个很好的教学级范例。【免费下载链接】Unity-Runtime-Animation-RecorderRecord animations in Unity runtime. Can save to .anim, maya, or FBX ASCII format.项目地址: https://gitcode.com/gh_mirrors/un/Unity-Runtime-Animation-Recorder创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考