ardy-motion-api / ardy /skeleton /kinematics.py
cs686's picture
Deploy ARDY ZeroGPU Blender motion API
c1e2af3 verified
Raw
History Blame Contribute Delete
5.87 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Forward-kinematics primitives for articulated skeletons."""
from typing import List
import einops
import torch
import torch.nn.functional as F
from ..tools import ensure_batched
@ensure_batched(local_joint_rots=4, root_positions=2)
def fk(
local_joint_rots: torch.Tensor,
root_positions: torch.Tensor,
skeleton,
root_positions_is_global: bool = True,
):
"""Compute global joint rotations and positions from local rotations.
Args:
local_joint_rots: Local rotation matrices with shape `(..., J, 3, 3)`.
root_positions: Root translations with shape `(..., 3)`.
skeleton: Skeleton object exposing `neutral_joints`, `joint_parents`, and
`root_idx`.
root_positions_is_global: If `True`, neutral joints are recentered so root
translations are interpreted in world space.
Returns:
Tuple `(global_joint_rots, posed_joints, posed_joints_norootpos)`.
"""
device = local_joint_rots.device
dtype = local_joint_rots.dtype
neutral_joints = skeleton.neutral_joints.to(device=device, dtype=dtype)
if root_positions_is_global is True:
# Removing the pelvis offset from the neutral joints
# as the root positions does not depends on the pelvis offset of the skeleton
pelvis_offset = neutral_joints[skeleton.root_idx]
neutral_joints = neutral_joints - pelvis_offset
# compute joint position and global rotations
joints = einops.repeat(
neutral_joints,
"j k -> b j k",
b=len(local_joint_rots),
)
posed_joints_norootpos, global_joint_rots = batch_rigid_transform(
local_joint_rots,
joints,
skeleton.joint_parents,
skeleton.root_idx,
)
# if root_positions_is_global is True:
# posed_joints_norootpos always start at zero
# otherwise it could start with the pelvis offset
posed_joints = posed_joints_norootpos + root_positions[:, None]
return global_joint_rots, posed_joints, posed_joints_norootpos
def compute_idx_levels(parents):
"""Group joint indices by hierarchy depth for level-wise FK updates.
Args:
parents: Parent index tensor of shape `(J,)` with root parent `-1`.
Returns:
List of index tensors, where each tensor contains joints at one depth.
"""
idx_levs = [[]]
lev_dicts = {0: -1}
for i in range(1, parents.shape[0]):
assert int(parents[i]) in lev_dicts
lev = lev_dicts[int(parents[i])] + 1
if lev + 1 > len(idx_levs):
idx_levs.append([])
idx_levs[lev].append(int(i))
lev_dicts[int(i)] = lev
idx_levs = [torch.tensor(x).long() for x in idx_levs]
return idx_levs
def batch_rigid_transform(rot_mats, joints, parents, root_idx):
"""Perform batch rigid transformation on a skeletal structure.
Args:
rot_mats: Local rotation matrices for each joint: (B, J, 3, 3)
joints: Initial joint positions: (B, J, 3)
parents: Tensor indicating the parent of each joint: (J,)
root_idx (int): index of the root
Returns:
Transformed joint positions after applying forward kinematics.
"""
# Compute the hierarchical levels of joints based on their parent relationships
idx_levs = compute_idx_levels(parents)
# Apply forward kinematics to transform the joints
return forward_kinematics(rot_mats, joints, parents, idx_levs, root_idx)
@torch.jit.script
def transform_mat(R, t):
"""Creates a batch of transformation matrices.
Args:
- R: Bx3x3 array of a batch of rotation matrices
- t: Bx3x1 array of a batch of translation vectors
Returns:
- T: Bx4x4 Transformation matrix
"""
# No padding left or right, only add an extra row
return torch.cat([F.pad(R, [0, 0, 0, 1]), F.pad(t, [0, 0, 0, 1], value=1.0)], dim=2)
@torch.jit.script
def forward_kinematics(
rot_mats,
joints,
parents: torch.Tensor,
idx_levs: List[torch.Tensor],
root_idx: int,
):
"""Perform forward kinematics to compute posed joints and global rotation matrices.
Args:
rot_mats: Local rotation matrices for each joint: (B, J, 3, 3)
joints: Initial joint positions: (B, J, 3)
parents: Tensor indicating the parent of each joint: (J,)
idx_levs: Tensors of joint indices grouped by depth in the kinematic tree.
root_idx (int): index of the root
Returns:
Posed joints: (B, J, 3)
Global rotation matrices: (B, J, 3, 3)
"""
# Add an extra dimension to joints
joints = torch.unsqueeze(joints, dim=-1)
# Compute relative joint positions
rel_joints = joints.clone()
mask_no_root = torch.ones(joints.shape[1], dtype=torch.bool)
mask_no_root[root_idx] = False
rel_joints[:, mask_no_root] -= joints[:, parents[mask_no_root]].clone()
# Compute initial transformation matrices
# (B, J + 1, 4, 4)
transforms_mat = transform_mat(rot_mats.reshape(-1, 3, 3), rel_joints.reshape(-1, 3, 1)).reshape(
-1, joints.shape[1], 4, 4
)
# Initialize the root transformation matrices
transforms = torch.zeros_like(transforms_mat)
transforms[:, root_idx] = transforms_mat[:, root_idx]
# Compute global transformations level by level
for indices in idx_levs:
curr_res = torch.matmul(transforms[:, parents[indices]], transforms_mat[:, indices])
transforms[:, indices] = curr_res
# Extract posed joint positions from the transformation matrices
posed_joints = transforms[:, :, :3, 3]
# Extract global rotation matrices from the transformation matrices
global_rot_mat = transforms[:, :, :3, :3]
return posed_joints, global_rot_mat