Unity引擎中Blend Tree(混合树)是动画系统中用于实现多动画剪辑平滑混合的关键技术,广泛应用于角色动作的平滑过渡和多维参数驱动的动画控制。下面我详细介绍Unity Blend Tree的混合原理,包括其分类、数据结构、混合算法和底层实现思路。
1. Blend Tree 概述
Blend Tree是一个树形结构,叶子节点是具体的动画剪辑(Animation Clip),中间节点是混合节点(Blend Node),通过参数驱动实现多个动画的加权混合,输出最终的骨骼姿态。
2. Blend Tree 类型
Unity支持多种Blend Tree类型,常见的有:
类型 | 说明 |
---|---|
1D Blend Tree | 以单个参数驱动,沿一维轴线对多个动画进行线性或曲线混合 |
2D Blend Tree | 以两个参数驱动,支持平面内多动画混合,常用插值算法有线性插值和三角插值 |
Direct Blend | 直接对多个动画按权重混合,权重由外部参数直接控制 |
3. Blend Tree 数据结构
struct BlendTreeNode {
enum Type { Clip, Blend1D, Blend2D };
Type type;
// 叶子节点:动画剪辑
AnimationClip* clip;
// 子节点
std::vector<BlendTreeNode*> children;
// 混合参数(1D或2D)
float parameter1D;
Vector2 parameter2D;
// 1D混合时,子节点对应的参数值(如速度)
std::vector<float> childParameterValues1D;
// 2D混合时,子节点对应的二维坐标
std::vector<Vector2> childParameterValues2D;
};
4. Blend Tree 混合原理
4.1 1D Blend Tree 混合原理
- 输入参数
p
(如速度)在子节点参数值区间内定位。 - 找到
p
所在的两个相邻子节点A
和B
。 - 计算权重
w
,通常是线性插值权重:
[
w = \frac{p - p_A}{p_B - p_A}
]
- 最终Pose为:
[
Pose = (1 - w) \times Pose_A + w \times Pose_B
]
4.2 2D Blend Tree 混合原理
- 输入参数为二维向量
P = (x, y)
。 - 子节点在二维参数空间有对应坐标。
- 通过三角形剖分(Triangulation)确定
P
所在的三角形。 - 计算
P
在三角形三个顶点的重心坐标(Barycentric Coordinates)作为权重。 - 最终Pose为三个顶点Pose的加权和。
4.3 直接混合(Direct Blend)
- 输入参数直接作为权重,归一化后对所有子节点Pose加权求和。
5. Blend Tree 混合流程伪代码
Pose BlendTreeNode::Sample(const ParameterSet& params) {
switch (type) {
case Clip:
return clip->Sample(params.time);
case Blend1D: {
float p = params.Get(parameterName);
// 找到p所在区间
int i = FindInterval(childParameterValues1D, p);
float pA = childParameterValues1D[i];
float pB = childParameterValues1D[i+1];
float w = (p - pA) / (pB - pA);
Pose poseA = children[i]->Sample(params);
Pose poseB = children[i+1]->Sample(params);
return BlendPoses(poseA, poseB, w);
}
case Blend2D: {
Vector2 p = params.Get2D(parameterNameX, parameterNameY);
// 三角剖分找到包含p的三角形顶点索引
auto tri = FindTriangleContainingPoint(childParameterValues2D, p);
Vector3 bary = ComputeBarycentricCoordinates(childParameterValues2D[tri[0]], childParameterValues2D[tri[1]], childParameterValues2D[tri[2]], p);
Pose pose0 = children[tri[0]]->Sample(params);
Pose pose1 = children[tri[1]]->Sample(params);
Pose pose2 = children[tri[2]]->Sample(params);
return pose0 * bary.x + pose1 * bary.y + pose2 * bary.z;
}
}
}
6. 关键技术点
-
三角剖分算法
2D Blend Tree需要对子节点参数点集进行三角剖分(如Delaunay三角剖分),保证任意输入参数都能找到对应三角形。 -
权重归一化
保证所有权重和为1,避免动画叠加异常。 -
插值方式
位置使用线性插值,旋转使用球面线性插值(Slerp),保证旋转平滑。 -
性能优化
预计算三角剖分结构,缓存权重计算结果,减少每帧计算开销。
7. Unity Blend Tree的底层实现思路
- Blend Tree作为Animator Controller的一部分,运行时根据参数驱动递归采样。
- 叶子节点采样动画剪辑,返回骨骼局部变换。
- 中间节点根据参数计算权重,递归采样子节点Pose并混合。
- 最终输出骨骼姿态用于渲染。
8. 总结
方面 | 说明 |
---|---|
Blend Tree类型 | 1D、2D、Direct混合 |
1D混合 | 找区间线性插值 |
2D混合 | 三角剖分 + 重心坐标加权 |
旋转插值 | 使用Slerp保证旋转平滑 |
性能优化 | 预计算三角剖分,缓存权重,减少每帧计算 |
好的,接下来我将深入讲解Unity Blend Tree中2D混合的三角剖分算法实现细节、重心坐标计算方法,以及如何高效实现Blend Tree的采样和权重计算。同时,我也会介绍一些实际项目中常用的优化技巧。
9. 2D Blend Tree 三角剖分算法详解
9.1 为什么需要三角剖分?
2D Blend Tree的子节点在二维参数空间(如速度X和速度Y)有对应的点坐标。为了根据输入参数点P
找到对应的混合权重,需要确定P
落在哪个三角形内。
三角剖分将所有子节点点集划分成若干不重叠三角形,保证任意点P
都能定位到唯一三角形。
9.2 常用三角剖分算法
-
Delaunay三角剖分
保证三角形的最小角最大化,避免“瘦长”三角形,适合插值。 -
Bowyer-Watson算法
一种增量式Delaunay三角剖分实现。 -
库支持
Unity底层可能使用自定义或第三方库实现三角剖分。
9.3 三角剖分伪代码示例(Bowyer-Watson)
struct Triangle {
Vector2 v0, v1, v2;
};
std::vector<Triangle> DelaunayTriangulation(std::vector<Vector2> points) {
// 1. 创建超级三角形,包含所有点
std::vector<Triangle> triangles = {superTriangle};
for (auto& p : points) {
std::vector<Triangle> badTriangles;
std::vector<Edge> polygon;
// 2. 找出所有包含p的圆的三角形(坏三角形)
for (auto& t : triangles) {
if (PointInCircumcircle(p, t)) {
badTriangles.push_back(t);
}
}
// 3. 找出坏三角形的边界(多边形边)
polygon = FindPolygonBoundary(badTriangles);
// 4. 删除坏三角形
RemoveTriangles(triangles, badTriangles);
// 5. 用p和边界多边形重建三角形
for (auto& edge : polygon) {
triangles.push_back(Triangle{edge.v0, edge.v1, p});
}
}
// 6. 删除包含超级三角形顶点的三角形
RemoveTrianglesWithSuperTriangleVertices(triangles);
return triangles;
}
10. 重心坐标(Barycentric Coordinates)计算
10.1 作用
重心坐标用于计算点P
在三角形(A, B, C)
内的权重,权重和为1,且权重非负。
10.2 计算公式
给定三角形顶点A, B, C
和点P
,计算权重(u, v, w)
:
[
\begin{cases}
v = \frac{Area(PAC)}{Area(ABC)} \
w = \frac{Area(PAB)}{Area(ABC)} \
u = 1 - v - w
\end{cases}
]
面积可以用向量叉积计算:
[
Area(ABC) = \frac{1}{2} |(B - A) \times (C - A)|
]
10.3 伪代码示例
Vector3 ComputeBarycentricCoordinates(Vector2 A, Vector2 B, Vector2 C, Vector2 P) {
float areaABC = Cross(B - A, C - A);
float areaPBC = Cross(B - P, C - P);
float areaPCA = Cross(C - P, A - P);
float u = areaPBC / areaABC;
float v = areaPCA / areaABC;
float w = 1.0f - u - v;
return Vector3(u, v, w);
}
11. Blend Tree采样与权重计算优化
11.1 预计算三角剖分结构
- 在Blend Tree构建阶段完成三角剖分,缓存三角形顶点索引。
- 运行时只需快速查找包含点
P
的三角形。
11.2 空间加速结构
- 使用四叉树或包围盒层级加速三角形查找。
- 减少每帧遍历所有三角形的开销。
11.3 权重缓存与插值
- 对于参数变化不大的情况,缓存上一次三角形和权重。
- 只有参数超出一定阈值时重新计算。
11.4 多线程与GPU加速
- 动画采样和混合可利用多线程并行计算。
- 骨骼变换上传GPU后,利用GPU实例化和着色器插值减轻CPU负担。
12. Unity Blend Tree调用流程简述
-
参数更新
Animator根据外部输入更新Blend Tree参数。 -
递归采样
Blend Tree节点递归调用Sample()
,叶子节点采样动画剪辑。 -
权重计算
根据参数计算权重(1D线性插值或2D三角剖分+重心坐标)。 -
Pose混合
使用权重对骨骼局部变换插值,旋转用Slerp。 -
输出Pose
返回最终混合Pose给Animator,后续计算全局骨骼变换。
13. 总结
主题 | 说明 |
---|---|
三角剖分算法 | Delaunay三角剖分,Bowyer-Watson实现 |
重心坐标计算 | 利用面积比例计算权重,保证权重和为1 |
优化技巧 | 预计算三角剖分,空间加速结构,权重缓存,多线程加速 |
Unity调用流程 | 参数更新 → 递归采样 → 权重计算 → Pose混合 → 输出 |