cs686's picture
Deploy ARDY ZeroGPU Blender motion API
c1e2af3 verified
Raw
History Blame Contribute Delete
7.66 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from typing import List, Optional, Union
import einops
import torch
from ..geometry import cont6d_to_matrix, matrix_to_cont6d
from ..tools import ensure_batched
def diff_angles(angles, fps: float):
"""Computes differences between angles.
Args:
angles (Tensor): [..., T] the batched sequences of rotation angles in radians.
Returns:
Tensor: [..., T-1] the difference between consecutive angles
"""
cos = torch.cos(angles)
sin = torch.sin(angles)
cos_diff = cos[..., 1:] * cos[..., :-1] + sin[..., 1:] * sin[..., :-1]
sin_diff = sin[..., 1:] * cos[..., :-1] - cos[..., 1:] * sin[..., :-1]
# should be close to angles.diff() but more robust
# multiply by fps = 1 / dt
angles_diff = fps * torch.arctan2(sin_diff, cos_diff)
return angles_diff
@ensure_batched(positions=4, lengths=1)
def compute_vel_xyz(
positions: torch.Tensor,
fps: float,
lengths: Optional[torch.Tensor] = None,
):
"""Compute the velocities from positions: dx/dt.
Works with batches. The last velocity is duplicated to keep the same size.
Args:
positions (torch.Tensor): [..., T, J, 3] xyz positions of a human skeleton
fps (float): frame per seconds
lengths (Optional[torch.Tensor]): [...] size of each input batched. If not provided, positions should not be batched
Returns:
velocity (torch.Tensor): [..., T, J, 3] velocities computed from the positions
"""
device = positions.device
if lengths is None:
assert positions.shape[0] == 1, "If lenghts is not provided, the input should not be batched."
lengths = torch.tensor([len(positions)], device=device)
# compute velocities with fps
velocity = fps * (positions[:, 1:] - positions[:, :-1])
# pading the velocity vector
vel_pad = torch.zeros_like(velocity[:, 0])
velocity, _ = einops.pack([velocity, vel_pad], "batch * nbjoints dim")
# repeat the last velocities
# with special care for different lengths with batches
# Use gather/scatter instead of in-place advanced indexing (TRT-compatible)
nj, nd = velocity.shape[2], velocity.shape[3]
# clamp: for length-1 sequences src would be -1 (invalid for gather);
# index 0 is the zero pad, so the copy is a harmless no-op there
src_idx = (lengths - 2).clamp(min=0).long().view(-1, 1, 1, 1).expand(-1, 1, nj, nd)
dst_idx = (lengths - 1).long().view(-1, 1, 1, 1).expand(-1, 1, nj, nd)
velocity = velocity.scatter(1, dst_idx, torch.gather(velocity, 1, src_idx))
return velocity
@ensure_batched(root_rot_angles=2, lengths=1)
def compute_vel_angle(
root_rot_angles: torch.Tensor,
fps: float,
lengths: Optional[torch.Tensor] = None,
):
"""Compute the local root rotation velocity: dtheta/dt.
Args:
root_rot_angles (torch.Tensor): [..., T] rotation angle (in radian)
fps (float): frame per seconds
lengths (Optional[torch.Tensor]): [...] size of each input batched. If not provided, root_rot_angles should not be batched
Returns:
local_root_rot_vel (torch.Tensor): [..., T] local root rotation velocity (in radian/s)
"""
device = root_rot_angles.device
if lengths is None:
assert root_rot_angles.shape[0] == 1, "If lenghts is not provided, the input should not be batched."
lengths = torch.tensor([len(root_rot_angles)], device=device)
local_root_rot_vel = diff_angles(root_rot_angles, fps)
pad_rot_vel_angles = torch.zeros_like(root_rot_angles[:, 0])
local_root_rot_vel, _ = einops.pack(
[local_root_rot_vel, pad_rot_vel_angles],
"batch *",
)
# repeat the last rotation angle
# with special care for different lengths with batches
# Use gather/scatter instead of in-place advanced indexing (TRT-compatible)
# clamp: for length-1 sequences src would be -1 (invalid for gather);
# index 0 is the zero pad, so the copy is a harmless no-op there
src_idx = (lengths - 2).clamp(min=0).unsqueeze(-1).long() # [B, 1]
dst_idx = (lengths - 1).unsqueeze(-1).long() # [B, 1]
local_root_rot_vel = local_root_rot_vel.scatter(1, dst_idx, torch.gather(local_root_rot_vel, 1, src_idx))
return local_root_rot_vel
@ensure_batched(posed_joints=4)
def compute_heading_angle(posed_joints: torch.Tensor, skeleton):
"""Compute the heading direction from the joint positions, by looking at the hip vector.
Args:
posed_joints (torch.Tensor): [B, T, J, 3] global positions
skeleton (SkeletonBase): skeleton of the human, used to find location of hips
Returns:
heading (torch.Tensor): [B] heading angle
"""
# compute root heading for the sequence from hip positions
r_hip, l_hip = skeleton.hip_joint_idx
diff = posed_joints[:, :, r_hip] - posed_joints[:, :, l_hip]
heading_angle = torch.atan2(diff[..., 2], -diff[..., 0])
return heading_angle
def length_to_mask(
length: Union[torch.Tensor, List],
max_len: Optional[int] = None,
device=None,
) -> torch.Tensor:
if isinstance(length, list):
if device is None:
device = "cpu"
length = torch.tensor(length, device=device)
if device is not None:
assert device == length.device
device = length.device
if max_len is None:
max_len = max(length)
mask = torch.arange(max_len, device=device).expand(len(length), max_len) < length.unsqueeze(1)
return mask
class RotateFeatures:
"""Helper that applies a global heading rotation to motion features."""
def __init__(self, angle: torch.Tensor):
"""Precompute 2D and 3D rotation matrices for a batch of angles.
Args:
angle: Rotation angle(s) in radians, shaped ``[B]``.
"""
self.angle = angle
cos, sin = torch.cos(angle), torch.sin(angle)
one, zero = torch.ones_like(angle), torch.zeros_like(angle)
# 2D rotation transposed (sin are -sin)
self.corrective_mat_2d_T = torch.stack((cos, sin, -sin, cos), -1).reshape(angle.shape + (2, 2))
# 3D rotation on Y axis
self.corrective_mat_Y = torch.stack((cos, zero, sin, zero, one, zero, -sin, zero, cos), -1).reshape(
angle.shape + (3, 3)
)
self.corrective_mat_Y_T = self.corrective_mat_Y.transpose(-2, -1).contiguous()
def rotate_positions(self, positions: torch.Tensor):
"""Rotate 3D positions around the Y axis."""
return positions @ self.corrective_mat_Y_T
def rotate_2d_positions(self, positions_2d: torch.Tensor):
"""Rotate 2D ``(x, z)`` vectors in the ground plane."""
return positions_2d @ self.corrective_mat_2d_T
def rotate_rotations(self, rotations: torch.Tensor):
"""Left-multiply global rotation matrices by the heading correction."""
# "Rotate" the global rotations
# which means add an extra Y rotation after the transform
# so at the left R' = R_y R
# (since we use the convention x' = R x)
# "bik,btdkj->btdij"
B, T, J = rotations.shape[:3]
BTJ = B * T * J
return (
self.corrective_mat_Y[:, None, None].expand(B, T, J, 3, 3).reshape(BTJ, 3, 3) @ rotations.reshape(BTJ, 3, 3)
).reshape(B, T, J, 3, 3)
def rotate_6d_rotations(self, rotations_6d: torch.Tensor):
"""Rotate 6D rotation features via matrix conversion."""
return matrix_to_cont6d(self.rotate_rotations(cont6d_to_matrix(rotations_6d)))