在三维图形学里,模型变形通常指保持拓扑不变的前提下改变顶点的空间位置。自由变形(Free-Form Deformation,FFD)和笼子变形(Cage Deformation)是两种经典且高效的空间变形技术。它们都不直接操作模型的每个顶点,而是通过一个包围结构或控制点集合来间接驱动顶点,因此对原始模型的三角形或四边形连接关系没有破坏。理解了这两种方法,你就能在角色蒙皮、表情动画、参数化建模等任务中灵活运用。

自由变形FFD的基本原理与实现
自由变形的核心思想是:把待变形的模型嵌入到一个由控制点张成的规则网格中,然后通过移动控制点来改变网格形状,模型顶点作为网格内部的点会随之移动。最常见的实现基于三变量张量积贝塞尔体或B样条体。以最简单的线性格为例,假设控制网格在三个方向上分别有 l、m、n 个控制点,那么任意一个模型顶点 p 可以先通过归一化坐标找到它在这个网格内的局部参数 (s, t, u),再用这些参数对控制点进行加权求和。
更具体地说,先计算模型顶点的轴对齐包围盒,将每个顶点的世界坐标映射到 [0,1] 区间的局部坐标。对于局部坐标 s,用伯恩斯坦基函数 B_{i,d}(s) = C(d,i) * s^i * (1-s)^{d-i} 计算各个控制点的权重。然后顶点变形后的位置就等于所有控制点坐标与对应权重乘积的累加。这种方法的优势在于变形是光滑的,因为伯恩斯坦基函数具有连续性。缺点也很明显:控制网格必须是规则的矩形格子,遇到复杂形状的模型时,包围盒内会产生大量无用的控制点,而且局部细节的控制粒度较粗。
下面给出一段 Python 代码,演示一维情况下的伯恩斯坦多项式计算,并扩展到三维插值。实际工程中可以采用更高效的算法,比如利用张量积分解预先计算基函数值。
import numpy as np
from math import comb
def bernstein_basis(i, n, t):
"""计算伯恩斯坦基函数 B_{i,n}(t)"""
return comb(n, i) * (t ** i) * ((1.0 - t) ** (n - i))
def ffd_deform(vertices, control_points):
"""
简化版FFD:control_points 是一个形状为 (l+1, m+1, n+1, 3) 的数组
vertices 是 (N, 3) 的模型顶点坐标
假设顶点坐标已经归一化到 [0,1] 区间内
"""
l, m, n = control_points.shape[0] - 1, control_points.shape[1] - 1, control_points.shape[2] - 1
deformed = np.zeros_like(vertices)
for idx, p in enumerate(vertices):
s, t, u = p # 局部坐标
new_pos = np.zeros(3)
for i in range(l + 1):
for j in range(m + 1):
for k in range(n + 1):
weight = bernstein_basis(i, l, s) * bernstein_basis(j, m, t) * bernstein_basis(k, n, u)
new_pos += weight * control_points[i, j, k]
deformed[idx] = new_pos
return deformed
# 示例:10x10x10的控制网格,模型顶点随机
ctrl_pts = np.random.rand(11, 11, 11, 3) # 控制点可以任意移动
verts = np.random.rand(100, 3) # 100个顶点,局部坐标在[0,1]
result = ffd_deform(verts, ctrl_pts)
print(result.shape)
上述代码展示了 FFD 的暴力遍历实现,复杂度为 O(N * l * m * n)。当控制点数量较多时性能会急剧下降,因此实际应用中常使用查表法或 GPU 并行计算。另外,局部坐标的获取通常需要求解一个反算过程,对于贝塞尔体有解析式,对于 B 样条体则需要迭代求解。FFD 在早期工业软件如 CATIA、Maya 的 lattice 变形器中十分常见,至今仍在概念设计阶段被广泛使用。
笼子变形的工作机制与优势
笼子变形是另一种思路:用一个封闭的三角网格(即“笼子”)包裹模型,模型顶点位于笼子内部。笼子变形的关键是为每个模型顶点计算一组相对于笼子顶点的广义重心坐标(Generalized Barycentric Coordinates)。最常用的有均值坐标(Mean Value Coordinates)、格林坐标(Green Coordinates)以及调和坐标(Harmonic Coordinates)。这些坐标保证了一个重要性质——当笼子顶点移动时,模型顶点通过相同的坐标权重进行仿射变换,从而产生自然的形变。
以均值坐标为例,给定笼子上的所有三角形,对于一个内部点 p,先计算每个三角形对应的单位法向和球面投影,然后通过积分公式得到各笼子顶点的权重。均值坐标的计算相对简单且对非凸笼子也适用,但可能出现负权重,导致形变结果不够直观。格林坐标在均值坐标的基础上增加了对法向的线性项,能够更好地保持局部几何细节,但实现更复杂。调和坐标是一种特殊的重心坐标,仅适用于凸笼子但全部权重非负,适合蒙皮动画。
下面这段代码演示了计算二维均值坐标的过程,三维情况原理相同但计算量更大。代码中的向量运算使用了 numpy 进行向量化,便于理解和移植。
import numpy as np
def mean_value_coordinates_2d(cage_vertices, p):
"""
cage_vertices: 按顺序排列的二维笼子顶点列表,假设闭合并逆时针
p: 二维查询点
返回各顶点的均值坐标权重
"""
n = len(cage_vertices)
weights = np.zeros(n)
for i in range(n):
v_i = cage_vertices[i] - p
v_next = cage_vertices[(i + 1) % n] - p
d_i = np.linalg.norm(v_i)
d_next = np.linalg.norm(v_next)
if d_i < 1e-12 or d_next < 1e-12:
weights[i] = 1.0
return weights
cos_angle = np.dot(v_i, v_next) / (d_i * d_next)
sin_angle = np.cross(v_i, v_next) / (d_i * d_next)
tan_half = sin_angle / (1.0 + cos_angle)
if abs(tan_half) > 1e-6:
weights[i] = (np.tan(np.arctan(tan_half) / 2.0)) / d_i
weights /= np.sum(weights)
return weights
# 示例:正方形笼子
cage = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
point = np.array([0.3, 0.4])
w = mean_value_coordinates_2d(cage, point)
print("权重:", w)
笼子变形相对于 FFD 最大的优势在于笼子可以贴合模型的外形,控制点更有针对性。例如要弯曲一只手臂,用一个贴合手臂的网格笼子,移动笼子的少数顶点就能实现自然的弯曲,而 FFD 则需要一个较大的矩形控制框,很多控制点与模型无关。此外,广义重心坐标天然支持仿射不变性,即模型整体平移、旋转和缩放时,权重保持不变。在商业动画软件中,笼子变形常用于角色表情控制、肌肉模拟以及模型间形变传递。
实际项目中的选择与优化策略
面对具体需求时,选择 FFD 还是笼子变形并没有绝对标准,但可以从几个维度评估。如果模型外形接近长方体或需要全局统一的缩放、扭曲,FFD 的实现简单且控制直观,是快速原型的好选择。如果模型形状不规则,或者需要局部精细控制,比如面部器官、手指关节,笼子变形因为控制点贴合模型,操作效率更高。另外,笼子变形通常需要预先计算每个顶点的重心坐标,这属于一次性开销,之后每次笼子顶点移动时只需做矩阵乘法,非常高效。FFD 的顶点坐标反算也只需要进行一次,变形时同样是线性组合。
性能优化方面,笼子变形的重心坐标计算是主要瓶颈,尤其是均值坐标的三维版本需要遍历所有三角形,对于大规模模型和复杂笼子,可以借助 GPU 并行化或预先缓存投影几何数据。调和坐标需要求解拉普拉斯方程,通常用有限元或网格参数化方法,计算量更大,但换来了非负权重,变形更稳定。FFD 则可以通过张量积分解把复杂度从 O(l*m*n) 降到 O(l+m+n),在实际工程中配合查表法能得到很好的加速。
近年来,随着深度学习在图形学中的应用,也出现了一些基于神经网络的变形方法,例如用网络直接预测控制点位移或重心坐标,但传统 FFD 和笼子变形依然是众多商业软件和游戏引擎的底层基础。理解这两者的数学本质,能帮你更好地调试变形效果,甚至在自定义工具中混合使用——比如在模型的不同部位分别应用 FFD 和笼子变形,再通过权重融合确保过渡平滑。