一句话总结

GaussiAnimate 提出 Skelebones 系统,将自由形式的变形骨骼(Bones)与拓扑感知的骨架(Skeleton)结合,使 3D 高斯角色既能表达衣物飘动等复杂非刚性形变,又支持直觉式关节控制——这是 4D 重建走向实际动画制作的关键一步。

论文链接:arxiv.org/abs/2604.08547 代码:cookmaker.cn/gaussianimate

为什么这个问题重要?

动画制作的两难困境

3D 角色动画一直面临一个根本矛盾:

  • 传统骨骼绑定(Rigging):直觉可控,但 LBS(Linear Blend Skinning)无法表达衣物、肌肉等非刚性形变
  • 数据驱动变形(NeRF、4D 高斯):拟合度高,但缺乏语义控制,”换个姿势”几乎不可能

现有方法要么用 LBS 硬撑(17-21% 的精度损失),要么训一个 MLP 隐式学形变(泛化到新姿势极差)。

GaussiAnimate 的思路是:用自由形式的骨骼捕捉形变细节,用曲率骨架提供控制语义,两者通过 PartMM 绑定

应用场景

场景 需求 传统方案的痛点
游戏角色动画 高保真非刚性形变 LBS 皮肤穿模
影视虚拟人 任意姿势重新渲染 需要大量人工 K 帧
机器人模拟 软体形变建模 物理引擎计算成本高

背景知识

3D 高斯 Splatting 回顾

每个高斯由 $(\mu, \Sigma, \alpha, \mathbf{c})$ 描述:位置、协方差(形状+朝向)、不透明度、颜色。渲染时投影到 2D,速度远超 NeRF。

4D 高斯在此基础上引入时间维度:每帧的高斯参数都在变化,捕捉动态场景。

骨架绑定基础

经典 LBS 公式:

\[\mathbf{p}' = \sum_{j=1}^{B} w_j(\mathbf{p}) \cdot T_j \cdot \mathbf{p}\]

其中 $w_j$ 是蒙皮权重,$T_j$ 是第 $j$ 根骨骼的变换矩阵。问题在于:线性混合会导致”糖果纸扭曲”伪影,而非刚性形变根本不能用单一 $T_j$ 表达。


核心方法:Skelebones 三步流程

直觉解释

4D 高斯序列(动态)
       ↓ Step 1: Bones
  自由骨骼(捕捉每个局部的实际运动轨迹)
       ↓ Step 2: Skeleton
  曲率骨架(提供人类可理解的关节层次)
       ↓ Step 3: Binding (PartMM)
  Skelebones(既可控又表达力强)
       ↓
  新姿势合成 / 重新动画

Step 1:高斯压缩为自由骨骼

核心思想:运动模式相似的高斯点属于同一”骨骼”。用时序轨迹特征做 K-means 聚类,每簇的主运动方向就是一根骨骼。

import torch
import numpy as np
from sklearn.cluster import KMeans

class GaussianBoneCompressor:
    """将时序高斯轨迹压缩为自由形式骨骼"""
    
    def __init__(self, n_bones=32):
        self.n_bones = n_bones
    
    def compress(self, gaussians_seq: torch.Tensor):
        # gaussians_seq: [T, N, 3] — T帧,N个高斯中心
        T, N, _ = gaussians_seq.shape
        mean_pos = gaussians_seq.mean(0)          # 规范姿态 [N, 3]
        displacements = gaussians_seq - mean_pos   # 时序位移 [T, N, 3]
        
        # 每个高斯的运动指纹:T帧位移拼接
        motion_features = displacements.permute(1, 0, 2).reshape(N, -1)  # [N, T*3]
        
        # K-means 聚类:同运动模式 → 同一骨骼
        kmeans = KMeans(n_clusters=self.n_bones, random_state=42, n_init=10)
        labels = kmeans.fit_predict(motion_features.numpy())
        
        bones = []
        for b in range(self.n_bones):
            mask = labels == b
            if mask.sum() < 3:
                continue
            group_pos = mean_pos[mask]             # 该骨骼覆盖的高斯 [n_b, 3]
            center = group_pos.mean(0)
            
            # PCA 提取骨骼主轴(长轴方向)
            cov = torch.cov(group_pos.T)
            _, vecs = torch.linalg.eigh(cov)       # 特征向量按升序排列
            axis = vecs[:, -1]                     # 最大方差方向
            bones.append({'center': center, 'axis': axis, 'mask': mask})
        
        return bones, labels

Step 2:平均曲率骨架提取

从规范高斯点云提取拓扑骨架,使用曲率流收缩(Mean Curvature Flow Contraction):让点云沿法向内缩,直到形成 1D 骨架结构。

import numpy as np
import scipy.sparse as sp

def build_cotangent_laplacian(V: np.ndarray, F: np.ndarray) -> sp.csr_matrix:
    """构建余切 Laplace-Beltrami 算子,用于曲率计算"""
    n = len(V)
    rows, cols, vals = [], [], []
    for tri in F:
        for i in range(3):
            vi, vj, vk = tri[i], tri[(i+1)%3], tri[(i+2)%3]
            e1, e2 = V[vi] - V[vk], V[vj] - V[vk]
            cos_a = np.dot(e1, e2)
            sin_a = np.linalg.norm(np.cross(e1, e2)) + 1e-8
            cot = cos_a / sin_a                    # 余切权重
            rows += [vi, vj, vi, vj]
            cols += [vj, vi, vi, vj]
            vals += [cot/2, cot/2, -cot/2, -cot/2]
    return sp.csr_matrix((vals, (rows, cols)), shape=(n, n))

def mean_curvature_skeleton(V: np.ndarray, F: np.ndarray,
                             n_iter=80, dt=0.05) -> np.ndarray:
    """
    平均曲率流骨架提取(Au et al. 2008)
    V: [N,3] 顶点, F: [M,3] 面片索引
    返回收缩后的骨架点
    """
    V = V.copy().astype(np.float64)
    for it in range(n_iter):
        L = build_cotangent_laplacian(V, F)
        H = L @ V                                  # 平均曲率向量 [N,3]
        # 约束:不要过度收缩(防止退化)
        step = np.clip(dt * H, -0.1, 0.1)
        V -= step
        # 每20步做一次点云稀疏化(合并近邻点)
        if (it + 1) % 20 == 0:
            V = _cluster_nearby_points(V, radius=0.02)
    return V

def _cluster_nearby_points(V, radius=0.02):
    """合并距离小于 radius 的近邻点(简化版体素下采样)"""
    from sklearn.cluster import DBSCAN
    labels = DBSCAN(eps=radius, min_samples=1).fit_predict(V)
    return np.array([V[labels==l].mean(0) for l in np.unique(labels)])

Step 3:分段运动匹配(PartMM)

PartMM 的核心是非参数检索:不训练网络,而是从已有动作库中检索最相似的片段,加权混合合成新动作。这在低数据(~1000 帧)场景下比 GRU/MLP 泛化好 20%+。

import torch
import torch.nn.functional as F

class PartwiseMotionMatching:
    """
    非参数化分段运动匹配
    数据库: motion_db [M, T, B, D] — M个片段,T帧,B根骨骼,D维特征
    """
    
    def __init__(self, motion_db: torch.Tensor, part_labels: torch.Tensor):
        self.db = motion_db                         # [M, T, B, D]
        self.part_labels = part_labels             # [B] 每根骨骼的身体部位标签
        self.n_parts = int(part_labels.max()) + 1
    
    def synthesize(self, ctx: torch.Tensor, k: int = 5, tau: float = 0.1):
        """
        ctx: [T_ctx, B, D] 当前动作上下文
        返回: [T, B, D] 合成的未来动作
        """
        M, T, B, D = self.db.shape
        future = torch.zeros(T, B, D)
        
        for part_id in range(self.n_parts):
            mask = (self.part_labels == part_id)   # 该部位的骨骼
            
            # 查询特征 vs 数据库特征(余弦相似度)
            q = ctx[:, mask].flatten()
            db_part = self.db[:, :, mask].reshape(M, -1)
            sims = F.cosine_similarity(q.unsqueeze(0).expand(M, -1), db_part)
            
            # Top-k 检索 + temperature softmax 混合
            top_sims, top_idx = sims.topk(k)
            weights = F.softmax(top_sims / tau, dim=0)             # [k]
            top_motions = self.db[top_idx][:, :, mask]            # [k, T, n_b, D]
            blended = (weights[:, None, None, None] * top_motions).sum(0)
            future[:, mask] = blended
        
        return future

Pipeline 概览

多视角视频 → 4D 高斯重建
                  ↓
         GaussianBoneCompressor
         (时序轨迹聚类 → 自由骨骼)
                  ↓
         mean_curvature_skeleton
         (点云收缩 → 曲率骨架)
                  ↓
         骨骼-骨架绑定(对齐 + 权重分配)
                  ↓
         PartMiseMotionMatching
         (检索+混合 → 新姿势合成)
                  ↓
         重新渲染(3DGS 渲染器)

实验

数据集说明

数据集 类型 用途 获取难度
ZJU-MoCap 真实人体多视角 训练+评估 公开,需申请
D-NeRF 合成变形物体 消融实验 公开下载
自采 4D 数据 真实非刚性物体 泛化测试 需自建采集装置

定量评估

方法 PSNR ↑ SSIM ↑ RMSE ↓(姿势迁移) 参数量
LBS baseline baseline baseline
Bag-of-Bones +3.2 +0.01 -12%
GRU-based +4.1 +0.02 -18%
Skelebones (PartMM) +17.3% +0.04 -48.4%

低数据场景(~1000帧)下,PartMM 优势更加明显——检索比拟合更抗过拟合。


工程实践

实际部署考虑

推理速度:骨架提取是离线预处理,PartMM 检索在 GPU 上大约 5-15ms/帧(取决于数据库大小),可以接近实时。

内存

  • 运动数据库随片段数线性增长
  • 1000 帧 × 64 骨骼 × 6D 位姿 ≈ 1.5MB(极其轻量)

硬件需求:RTX 3090 可跑完整 pipeline,4D 高斯重建阶段最吃显存(~16GB),PartMM 推理只需 4GB。

数据采集建议

多视角采集时的关键参数:

# 推荐配置(不是命令,是配置建议)
相机数量: 8-16 台(均匀分布在球面上)
帧率: 30-60fps(捕捉快速形变)
分辨率: 1920×1080 以上
同步精度: <1ms(多相机硬件同步)

动态纹理覆盖:纯色/无纹理物体需要投影纹理辅助特征点匹配。

常见坑

坑 1:曲率骨架拓扑错误

问题:薄片状结构(如裙摆)的曲率流会收缩成面而非线。

# 修复:在收缩前做几何检测,对薄片区域增大收缩步长
if is_sheet_region(V, F, thickness_threshold=0.05):
    dt_local = dt * 3.0  # 加速薄片区域收缩

坑 2:PartMM 检索数据库太小导致风格单一

问题:少于 500 帧时,检索结果同质化,混合后姿势不自然。

# 修复:数据增强——对已有片段做时间翻转、速度扰动
def augment_database(db, factor=5):
    augmented = [db]
    for _ in range(factor - 1):
        speed = np.random.uniform(0.8, 1.2)
        flipped = db.flip(dims=[1])            # 时间翻转
        augmented.append(flipped)
    return torch.cat(augmented, dim=0)

坑 3:骨骼-骨架绑定对齐失败

自由骨骼的方向和曲率骨架的关节方向可能不匹配,需要显式的 Procrustes 对齐:

# 用迭代最近点(ICP)对齐骨骼中心和骨架关节
from sklearn.neighbors import NearestNeighbors
def align_bones_to_skeleton(bone_centers, skel_joints):
    nn = NearestNeighbors(n_neighbors=1).fit(skel_joints)
    _, indices = nn.kneighbors(bone_centers)
    return indices.flatten()   # 每根骨骼对应的骨架关节

什么时候用 / 不用?

适用场景 不适用场景
非刚性形变明显(衣物、肌肉) 纯刚体场景(传统 LBS 已够)
需要新姿势泛化 只需重放已有动作
低数据场景(1000帧左右) 海量数据(深度学习更强)
离线预计算可接受 需要端到端实时训练
类别内泛化(同类物体) 跨类别迁移

与其他方法对比

方法 优点 缺点 适用场景
LBS 速度快,控制直觉 无法表达非刚性形变 游戏实时渲染
Bag-of-Bones 比 LBS 更灵活 缺乏拓扑约束 轻度非刚性
NeRF+变形场 拟合度极高 无控制语义,泛化差 纯重建,不需动画
4DGS 速度快,质量高 同 NeRF,缺乏控制 动态场景捕捉
Skelebones 可控+非刚性兼顾 需要预计算骨架 影视/游戏角色动画

我的观点

方向判断是对的:把”可控性”和”表达力”解耦,分别用骨架和自由骨骼处理,这个设计思路很清晰。PartMM 的非参数路线在数据少的情况下确实比神经网络稳定。

离产品化还有距离

  1. 自动骨架提取不稳定:平均曲率骨架对噪声敏感,真实扫描数据里大量三角面质量差,骨架提取失败率不低
  2. 跨类别泛化未验证:论文在同类物体(如所有人体)上验证,不同拓扑结构(四足动物 vs 双足人类)还需要额外工作
  3. 动作数据库冷启动:PartMM 依赖已有动作库,新类别首次使用需要足够的种子数据

值得关注的方向:将 PartMM 的检索思想与扩散模型结合——用 diffusion 生成动作分布,再用 PartMM 做 guided retrieval,可能突破单一数据库的上限。曲率骨架提取的鲁棒性也是一个值得单独研究的子问题。