# BATEngine **Repository Path**: graphi/batengine ## Basic Information - **Project Name**: BATEngine - **Description**: 基于Unity URP 的 BAT 骨骼矩阵渲染,GPU运行时动态蒙皮框架 - **Primary Language**: Unknown - **License**: MPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-18 - **Last Updated**: 2026-10-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: Unity, URP, BoneTexture, GPUInstancing ## README # BATEngine **GPU 骨骼动画蒙皮引擎** —— 基于 Unity 动画系统与骨骼,通过骨骼矩阵纹理 + GPU Instancing 实现大规模动画实例化渲染。 适合场景:SLG / RTS / 塔防 / 割草 —— 同屏数百到数千个单位的高性能动画渲染。 *** ## 环境要求 | 项 | 版本 | | :----- | :------------------------------------------------------------------------------------------ | | Unity | 2022.3 LTS 及以上 | | 渲染管线 | URP 14.0.12 及以上 | | 图形 API | 支持 `StructuredBuffer` + `DrawMeshInstancedIndirect` 的平台(Vulkan / Metal / GLES3.1+ / D3D11+) | | 目标设备 | 中端及以上移动设备(Mali-G52 / Adreno 6xx / Apple A12+ 级别) | *** ## 一、核心概念 | 概念 | 说明 | | :------------------- | :--------------------------------------- | | **BATAnimationData** | 烘焙数据资产。包含骨骼矩阵纹理、LOD Mesh、动画切片、事件、挂点等信息 | | **BATRenderer** | 场景级渲染器。**每种模型在场景中有且仅有一个**,管理该模型的所有实例 | | **BATInstance** | 实例组件。挂在每个单位上,驱动自身动画,渲染由 BATRenderer 统一提交 | | **BATVisibilityManager** | 全局可见性管理器。多相机 CullingGroup 视锥剔除 + LOD 分级,自动创建,业务层不直接调用 | | **BATEnginer** | 引擎唯一门面。业务层与引擎交互的唯一入口 | | **BATEventSystem** | 动画事件系统。同一模型的所有实例共享注册 | | **BATAttachment** | 挂点组件。把武器/特效等 GameObject 跟随骨骼运动 | **核心工作流**: text ``` [美术] FBX → LODGroup → 挂点(AP_ 前缀) → 烘焙 → BATAnimationData ↓ [TA] Prefab Generator → Instance Prefab + Renderer Prefab ↓ [程序] 场景放 Renderer Prefab → 运行时 Instantiate Instance Prefab → Play / CrossFade / 事件 / 挂点 ``` *** ## 二、Editor 工作流 ### 2.1 源 Prefab 准备(美术) 美术在 Unity 里按标准工作流准备源 Prefab,BAT 只读取不干预: | 项 | 要求 | | :----------- | :------------------------------------------------------------------------- | | **Animator** | 必须挂 `Animator` + `AnimatorController`(clip 从 Controller 里读取) | | **LODGroup** | 有 LOD 时按 Unity 标准方式配置。BAT 直接读 `screenRelativeTransitionHeight` 作为 LOD 切换依据 | | **挂点** | 需要挂点的节点,命名以 `AP_` 开头(可改),放在骨骼层级下即可 | | **动画事件** | 在 FBX 的 Animation 面板里加 Unity 原生 `AnimationEvent`,BAT 会拷贝 | **骨骼数量上限 200**。超出会直接拒绝烘焙。 ### 2.2 烘焙(TA) 打开 `BATEngine / Baker` 窗口: | 参数 | 说明 | | :-------------------- | :----------------------------------------------- | | **Source Prefab** | 源 Prefab | | **Bake FPS** | 烘焙帧率,默认 30。越低数据越小但动画精度越低 | | **Output Path** | 输出目录 | | **Attachment Prefix** | 挂点命名前缀,默认 `AP_` | | **LOD Camera FOV** | LOD 距离换算用的相机 FOV。必须与运行时主相机的 FOV 一致,否则 LOD 切换时机偏移 | **Clip 列表**: - `Loop`:循环动画 - `Bake Root Motion`:仅非循环动画可勾,勾 Loop 时自动禁用 **烘焙产物**: - `{PrefabName}_BATData.asset` —— 主资产 - `{PrefabName}_BATMat.mat` —— 材质(首次自动创建,绑定 `BATEngine/BATLit`;再次烘焙会复用,保留美术调整) - 骨骼矩阵纹理(子资产) - 每级 LOD 的 Mesh(子资产) ### 2.3 Prefab 生成(TA) 打开 `BATEngine / Prefab Generator`: 1. 拖入或 `From Selection` 选择 `BATAnimationData` 2. 配置: - **Conflict**:`Overwrite` / `Skip` / `AutoRename` - **Generate Instance Prefab**:被 spawn 的实例 - **Add Box Collider**:从 LOD0 bounds 生成碰撞盒 - **Generate Renderer Prefab**:场景驻留的渲染器 - **Shadow Casting / Receive Shadows**:默认 `Off` / `false`(性能优先) 3. 点 `Generate` **产物**: | 文件 | 用途 | | :--------------------- | :--------------------------------- | | `{Name}.prefab` | Instance Prefab,运行时被 `Instantiate` | | `{Name}_Render.prefab` | Renderer Prefab,场景里放一个 | **最终完整产物(每模型 4 个文件)**: text ``` {Name}_BATData.asset ← 烘焙数据 {Name}_BATMat.mat ← 材质 {Name}.prefab ← Instance Prefab {Name}_Render.prefab ← Renderer Prefab ``` *** ## 三、Runtime 工作流 ### 3.1 场景初始化(一次性) **每个模型在场景里有且仅有一个 BATRenderer**: - 方式 A:把 `{Name}_Render.prefab` 拖到场景 - 方式 B:Prefab Generator 的 `Scene Check` → `创建` 按钮 多模型时就放多个 Renderer Prefab。 **可选**:设置剔除 / LOD 使用的相机集合(默认 `Camera.main`): csharp ``` BATEnginer.SetTargetCameras(new Camera[] { mainCamera }); ``` **多相机(分屏)**:传入多个相机即可。引擎会为每个相机各建一个 `CullingGroup`,**任意一个相机能看到实例,实例就会渲染**: csharp ``` BATEnginer.SetTargetCameras(new Camera[] { leftCamera, rightCamera }); ``` - `null` 或空数组 → 回落 `Camera.main` - 可在运行时调用,修改后立即重建全部 `CullingGroup`(无需重启) - 只需传游戏相机,不要传 UI / 后处理等辅助相机(每个相机都会增加一份剔除开销) ### 3.2 生成实例 csharp ``` var instancePrefab = Resources.Load("Soldier"); // 你的 Instance Prefab var go = Instantiate(instancePrefab, position, rotation); var inst = go.GetComponent(); inst.Play("idle"); // 按名称播放 // 或 inst.Play(0, 0.5f); // 按索引 + 归一化起始时间 ``` **BATInstance 会自动注册到对应的 BATRenderer**。无需手动管理。 ### 3.3 播放控制 csharp ``` inst.Play("walk"); inst.Pause(); inst.Resume(); inst.SetSpeed(1.5f); ``` ### 3.4 CrossFade 过渡 默认关闭(低端机性能保护)。启用: csharp ``` BATEnginer.EnableCrossFade = true; // 设置后自动刷新所有渲染器 ``` 调用: csharp ``` inst.CrossFade("attack", 0.3f); // 0.3 秒过渡到 attack ``` 关闭后 `CrossFade` 退化为瞬时切换。 ### 3.5 动画事件 注册一次,处理所有同模型实例: csharp ``` BATEventSystem.Register(data, "OnAttackHit", (inst, evt) => { Debug.Log($"{inst.name} 命中,伤害 = {evt.floatParameter}"); // 业务逻辑 }); ``` 在数据资产卸载时反注册: csharp ``` BATEventSystem.Unregister(data, "OnAttackHit", handler); // 或 BATEventSystem.UnregisterAll(data); ``` ### 3.6 挂点 csharp ``` // 场景里的 GameObject 层级: // [BATInstance] // ├─ [BATAttachment] (_attachmentName = "RightHand") // │ └─ 武器模型 ``` `BATAttachment` 会自动查找父级 `BATInstance`,每帧更新自己的 Transform 跟随骨骼。 前提:`BATAnimationData.boneTexture` 必须 CPU 可读(`Read/Write Enabled`)。打包时若纹理被剥离可读性,挂点会失效。 ### 3.7 根运动 仅对 `Bake Root Motion = true` 的 clip 生效。运行时自动驱动 `instance.transform`,业务层不用做任何事。 注意:根运动是叠加模式(`+=`),若与 NavMeshAgent 等控制器混用,需自行控制调用顺序。 ### 3.8 全局 API csharp ``` // CrossFade BATEnginer.EnableCrossFade = true; // 可见性 / LOD 配置 BATEnginer.SetTargetCameras(new Camera[] { camA, camB }); // 多相机;null / 空数组 → Camera.main BATEnginer.InitialCapacity = 4096; // 仅在 BATVisibilityManager 首次创建时生效 BATEnginer.LodUpdateInterval = 0.3f; // LOD 刷新间隔,默认 0.5s,最小 0.01s // 清理(场景切换 / 热更 / 回主菜单) BATEnginer.ResetAll(); // 查询 BATEnginer.ActiveRendererCount; BATEnginer.ActiveInstanceCount; BATEnginer.GetInstanceStats(out var total, out var visible, out var culled, out var farCulled); BATEnginer.GetLodHistogram(buffer, out int maxLod); ``` ### 3.9 调试面板 | 面板 | 位置 | 快捷键 | 说明 | | :--------------- | :--- | :--- | :-------------------------------- | | **RuntimeStats** | 屏幕左中 | `F1` | 通用性能数据(FPS、Batches、内存等) | | **BATStats** | 屏幕右中 | 无 | BAT 专有数据(实例数、LOD 分布、CrossFade 状态) | 两个面板都支持鼠标拖拽。BATStats 可通过 `BATStats.Instance.SetVisible(bool)` 控制。 *** ## 四、关键注意事项 ### 4.1 骨骼与纹理 | 限制 | 值 | 超出后果 | | :--------- | :--- | :----- | | **骨骼数量上限** | 200 | 烘焙直接失败 | | **纹理高度上限** | 4096 | 烘焙直接失败 | **显存估算**:`width = 骨骼数 × 4`,`height = 总帧数向上取 2 的幂`。 显存 = `width × height × 8` 字节(RGBAHalf)。 例:骨骼 100,总帧 512 → `400 × 512 × 8 = 1.6 MB`。 控制帧数的方法: - 降低 Bake FPS(30 → 15) - 精简 clip 数量 - 缩短 clip 时长 ### 4.2 性能约束(Mali-G52 实测) | 指标 | 上限 | | :-------------- | :----------------------------------------------------------- | | **同屏实例数 × 顶点数** | ≤ 50 万 | | **每帧顶点 Pass 数** | 4-5(Forward + Shadow + Depth + DepthNormals + MotionVectors) | **LOD 顶点建议**: | LOD | 顶点数 | | :--- | :---- | | LOD0 | ≤ 500 | | LOD1 | ≤ 200 | | LOD2 | ≤ 100 | **阴影性能开销巨大**:开启 ShadowCaster 会让顶点数翻倍。移动端建议默认关闭,或仅 LOD0 开启。 ### 4.3 场景管理 **每个模型必须有且仅有一个 BATRenderer**。多个 renderer 共用同一个 `modelData` 会互相覆盖 `_rendererMap`,导致只有最后一个生效。 场景切换 / 热更 / 回主菜单时: csharp ``` BATEnginer.ResetAll(); ``` 幂等。可以在任何安全时机反复调用。清理内容: - 实例注册表 - 剔除球 - LOD 缓存 - 事件注册 - Renderer 注册表 不清理:`EnableCrossFade` / `SetTargetCameras` 设置的相机集合 / `InitialCapacity` / `LodUpdateInterval`(业务配置)、各 Manager 的 GameObject 本体。 ### 4.4 LOD 相关 **LODGroup 的 LOD 数量必须与烘焙的 LOD 数量一致**。不一致会打 warning 并回落默认阈值。 LOD 距离的来源: text ``` LODGroup.screenRelativeTransitionHeight(屏幕占比) ↓ 烘焙时用 FOV 换算 BATAnimationData.lodDistances(绝对距离) ``` 修改 FOV 后必须重新烘焙,否则 LOD 切换时机与视觉不符。 **多相机下的 LOD 只有一个全局级别**:距离统一以 `SetTargetCameras` 传入数组的**第一个相机**为基准。 原因是渲染使用 `DrawMeshInstancedIndirect` —— 一个实例每帧只提交一次、只生成一个 DrawCall,GPU 无法让相机 A 用 LOD0、相机 B 同时用 LOD1。 分屏游戏的两个相机通常距离相近,以第一个相机为基准的 LOD 结果基本正常;若两相机观察距离差异很大,需业务层自行取舍(例如拆分实例,或接受第一个相机视角下的 LOD 结果)。 ### 4.5 循环动画与根运动 **循环动画不支持根运动**。UI 层自动禁用,管线层也会强制忽略。设计决策: - 循环动画(走/跑)的位移应由业务层(NavMeshAgent 等)控制 - 根运动主要用于非循环的一次性动作(突进、跳跃、翻滚) ### 4.6 挂点系统 | 要求 | 说明 | | :---------- | :------------------------------------------------------------ | | 纹理必须 CPU 可读 | 在 `BATAnimationData` 里检查 `boneTexture.isReadable`,未勾选会打 error | | 挂点必须在骨骼层级下 | 若挂点不在任何骨骼下,烘焙时打 warning 并忽略 | | 首次访问挂点会构建缓存 | 惰性构建,之后调用零开销;资源卸载时自动释放 | 挂点频率控制:`BATAttachment._updateInterval` 默认 1(每帧)。装饰性挂点可调到 2-5,节省 CPU。 ### 4.7 Shader 关键字 | 关键字 | 用途 | 默认 | | :--------------------- | :------------------ | :-------- | | `_BONE_1` \~ `_BONE_4` | 每顶点骨骼影响数,烘焙时自动计算 | 按 LOD 自动 | | `_BAT_CROSSFADE` | CrossFade 双 Clip 采样 | 关闭(业务层打开) | 两者都是 `multi_compile_local_vertex`,打包时会全部编译进 build。开启 CrossFade 会让变体数量翻倍,但运行时开销只在真正过渡的实例上产生。 ### 4.8 BATInstance 与 BATRenderer 的父子关系 Instance 不应该放在 Renderer 的子节点下。它们是独立的: text ``` Scene ├─ [Soldier_Render] ← BATRenderer ├─ Soldier_1 ← BATInstance ├─ Soldier_2 ← BATInstance └─ ... ``` Instance 的 Transform 会被根运动直接驱动。 ### 4.9 多相机与 CullingGroup 多相机(分屏)支持涉及三个类: | 类 | 行为 | | :--- | :--- | | `BATEnginer` | 对外入口由 `TargetCamera` 改为 `SetTargetCameras(Camera[])`;`null` / 空数组回落 `Camera.main` | | `BATVisibilityManager` | 为每个相机各创建一个 `CullingGroup`,所有 CullingGroup 共享同一份 `BoundingSphere[]`;在 `LateUpdate` 中统一刷新包围球、剔除数量、可见计数与 LOD | | `BATInstance` | 可见性由单一 `bool` 改为**可见相机计数**:`IsVisible => 可见相机数 > 0`,任意一个相机可见即渲染;计数由管理器通过内部方法 `SetVisibleCount(int)` 写入 | 工作方式: - **剔除**:每个相机独立做视锥剔除,管理器汇总所有相机的结果,得到该实例的可见相机数 - **LOD**:仍为全局唯一级别,以相机数组第一个相机的位置计算距离(原因见 [4.4](#44-lod-相关));`LodLevel = -1` 表示超出最后一级 LOD 距离,实例被隐藏 - **主动同步**:可见性与 LOD 状态在 `LateUpdate` 中通过 `CullingGroup.IsVisible` 主动拉取,不依赖 CullingGroup 的回调,规避其异步计算导致的首帧延迟 - **重建**:运行时调用 `SetTargetCameras` 会立即 `Dispose` 旧 CullingGroup 并按新相机集合重建,旧的失效实例不会被重复剔除 注意事项: - 相机越多,剔除开销越大(每个相机一份 CullingGroup),只传真正参与渲染的游戏相机 - `BATVisibilityManager` 组件在首次访问时自动创建(`DontDestroyOnLoad`),无需手动挂载;`BATEnginer.ResetAll()` 只清理运行时数据,不销毁它 - 统计接口 `GetInstanceStats` 中的 `culled` 表示**所有相机都看不到**,`visible` 表示至少一个相机可见且 LOD ≥ 0 *** ## 五、不支持的特性 ### 5.1 半透明动画渲染 **结论**:BAT 不支持半透明材质的动画实例化渲染。 **原因**: 1. **GPU Instancing 无法批次内排序**:同一批次内所有实例共享一次 DrawCall,无法按深度逐实例排序,导致半透明混合的前后关系错误。 2. **正确排序需业务层分桶,DC 倍增**:要实现视觉上接近正确的半透明,需按深度把实例分到 16-32 个桶里逐桶提交,DrawCall 数量增加 16-32 倍,抵消了 GPU Instancing 的性能优势。 3. **Overdraw 成本在低端(PC 或移动端)过高**:半透明渲染需要每个像素多次混合,Overdraw 在移动 GPU(尤其是 Mali 系)上极为昂贵,中低端硬件直接爆帧。即使是 PC 端高性能硬件,大规模半透明也有明显压力。 4. **与 BAT 核心设计冲突**:BAT 的核心价值是"一次提交、批量渲染、顶点吞吐优先",半透明排序需求与之根本冲突。 5. **实际使用率低**:绝大多数动画单位(小兵、建筑、英雄)是不透明的。真正需要半透明动画的场景(隐身单位、幽灵、召唤物)通常是小批量、特定兵种,可用其他方案替代。 **替代方案**: | 场景 | 推荐方案 | | :--------------- | :----------------------------------------------- | | 少量半透明单位(≤ 20) | 用 Unity 原生 `SkinnedMeshRenderer` + 半透明材质,牺牲性能换简单 | | 大规模半透明视觉表达 | 用粒子系统 / VFX Graph / 自定义特效 | | 半透明单位数量大且必须用 BAT | 基于 BAT 源码自行扩展 Transparent 版本(见下方参考) | **自行扩展参考(不推荐,仅提供方向)**: 如确有大量半透明单位需求,可基于 BAT 源码扩展: 1. 新建 `BATLit_Transparent.shader`:复制 `BATLit.shader`,修改 Blend 为 `SrcAlpha OneMinusSrcAlpha`,`ZWrite Off`,`Queue = Transparent` 2. 修改 `BATRenderer`:可见实例按深度分桶(16-32 桶),逐桶 `DrawMeshInstancedIndirect` 3. 材质侧:`BATAnimationData.batMaterial` 换成 Transparent 版本 代价:DrawCall 数量 × 桶数,Overdraw 显著上升,低端硬件不建议开启。 *** ## 六、相关文件速查 | 文件 | 位置 | 说明 | | :------------------------ | :------ | :--------------- | | `BATEnginer.cs` | Runtime | 引擎门面(唯一对外入口) | | `BATAnimationData.cs` | Runtime | 烘焙数据资产 | | `BATRenderer.cs` | Runtime | 场景级渲染器 | | `BATInstance.cs` | Runtime | 实例组件 | | `BATVisibilityManager.cs` | Runtime | 全局剔除 + LOD(内部实现) | | `BATEventSystem.cs` | Runtime | 事件注册与派发 | | `BATRootMotion.cs` | Runtime | 根运动积分 | | `BATAttachment.cs` | Runtime | 挂点组件 | | `BATStats.cs` | Runtime | BAT 专用调试面板 | | `RuntimeStats.cs` | Runtime | 通用性能面板 | | `BATBakePipeline.cs` | Editor | 烘焙核心 | | `BATBakerWindow.cs` | Editor | 烘焙窗口 | | `BATPrefabGenerator.cs` | Editor | Prefab 生成窗口 | | `BATLit.shader` | Shader | PBR + GPU 蒙皮着色器 | *** ## 七、作者 - **Author**: Graphi - **Email**: - **Url**: [https://graphi.cyou](https://graphi.cyou/) - **Gitee**: - **GitHub**: