Source code for kimodo.viz.playback

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Playback and motion editing: CharacterMotion."""

from typing import Callable, Literal, Optional

import numpy as np
import torch

import viser.transforms as tf
from kimodo.skeleton import (
    G1Skeleton34,
    SOMASkeleton30,
    SOMASkeleton77,
    batch_rigid_transform,
    global_rots_to_local_rots,
)
from kimodo.tools import to_numpy, to_torch

from .g1_rig import (
    _get_g1_joint_axis_indices,
    _get_g1_joint_limits,
    get_g1_joint_f2q_data,
)
from .scene import Character


[docs] class CharacterMotion:
[docs] def __init__( self, character: Character, joints_pos: torch.Tensor, joints_rot: torch.Tensor, foot_contacts: Optional[torch.Tensor] = None, ): self.character = character self.server = character.server self.skeleton = character.skeleton self.name = character.name # [T, J, 3] global joint positions self.joints_pos = joints_pos # [T, J, 3, 3] global joint rotation matrices self.joints_rot = joints_rot assert joints_pos.shape[0] == joints_rot.shape[0] # keep track of local rots as well for convenience during pose editing self.joints_local_rot = global_rots_to_local_rots(joints_rot, self.skeleton) self.length = joints_pos.shape[0] self.cur_frame_idx = None self.foot_contacts = foot_contacts if foot_contacts is not None: assert foot_contacts.shape[0] == self.length self.precompute_mesh_info() # gizmos for pose editing self.root_translation_gizmo = None self.updating_root_translation_gizmo = False self.joint_gizmos = None self.updating_joint_gizmos = False self.gizmo_space: Literal["world", "local"] = "local" self._drag_start_world_rot: list = [] self._joint_gizmo_dragging: list[bool] = []
[docs] def precompute_mesh_info(self): if self.character.skeleton_mesh is not None: print("Caching skeleton mesh info...") self.character.skeleton_mesh.precompute_mesh_info(self.joints_pos) if self.character.skinned_mesh is not None: print("Caching skinning info...") self.character.precompute_skinning(self.joints_pos, self.joints_rot)
[docs] def set_frame(self, idx: int): """Sets the pose of the character to the given frame index.""" idx = min(idx, self.length - 1) # clamp to last frame cur_foot_contacts = self.foot_contacts[idx] if self.foot_contacts is not None else None self.character.set_pose( self.joints_pos[idx], self.joints_rot[idx], frame_idx=idx, foot_contacts=cur_foot_contacts, ) self.cur_frame_idx = idx # update gizmos if frame has changed due to playback cur_root_pos = self.joints_pos[self.cur_frame_idx, self.skeleton.root_idx].clone() cur_root_pos[1] = 0.0 if self.root_translation_gizmo is not None and not self.updating_root_translation_gizmo: self.root_translation_gizmo.position = cur_root_pos.cpu().numpy() if self.joint_gizmos is not None: for i, joint_gizmo in enumerate(self.joint_gizmos): # Do not push wxyz/position while this gizmo is being dragged; # otherwise the client receives e.g. identity and the gizmo snaps back. if not self.updating_joint_gizmos and not self._joint_gizmo_dragging[i]: joint_gizmo.position = self.joints_pos[self.cur_frame_idx, i].cpu().numpy() if self.gizmo_space == "world": joint_gizmo.wxyz = (1.0, 0.0, 0.0, 0.0) else: joint_gizmo.wxyz = tf.SO3.from_matrix(self.joints_rot[self.cur_frame_idx, i].cpu().numpy()).wxyz
[docs] def update_pose_at_frame( self, frame_idx: int, joints_pos: Optional[torch.Tensor] = None, joints_rot: Optional[torch.Tensor] = None, joints_local_rot: Optional[torch.Tensor] = None, foot_contacts: Optional[torch.Tensor] = None, ): """Overwrites one or more of the pose components at the given frame. If only a subset of joints_pos, joints_rot, or joints_local_rot are provided, the other components will be updated with FK. """ if joints_pos is not None: joints_pos = to_torch(joints_pos, device=self.joints_pos.device, dtype=self.joints_pos.dtype) self.joints_pos[frame_idx] = joints_pos if joints_local_rot is None and joints_rot is None: raise NotImplementedError("No IK to update joint rotations accordingly.") if joints_rot is not None: joints_rot = to_torch(joints_rot, device=self.joints_rot.device, dtype=self.joints_rot.dtype) self.joints_rot[frame_idx] = joints_rot if joints_local_rot is None: # update local rots from global rots self.joints_local_rot[frame_idx] = global_rots_to_local_rots(joints_rot, self.skeleton) if joints_pos is None: # need to update with FK new_posed_joints, _ = batch_rigid_transform( self.joints_local_rot[frame_idx : frame_idx + 1], self.skeleton.neutral_joints[None].to(self.joints_local_rot.device), self.skeleton.joint_parents.to(self.joints_local_rot.device), self.skeleton.root_idx, ) new_posed_joints = ( new_posed_joints[0] + self.joints_pos[frame_idx, self.skeleton.root_idx : self.skeleton.root_idx + 1] - self.skeleton.neutral_joints[[self.skeleton.root_idx]] ) self.joints_pos[frame_idx] = new_posed_joints if joints_local_rot is not None: joints_local_rot = to_torch(joints_local_rot, device=self.joints_local_rot.device).to( dtype=self.joints_local_rot.dtype ) self.joints_local_rot[frame_idx] = joints_local_rot if joints_rot is None or joints_pos is None: # need to update with FK new_posed_joints, new_global_rots = batch_rigid_transform( self.joints_local_rot[frame_idx : frame_idx + 1], self.skeleton.neutral_joints[None].to(self.joints_local_rot.device), self.skeleton.joint_parents.to(self.joints_local_rot.device), self.skeleton.root_idx, ) new_posed_joints = ( new_posed_joints[0] + self.joints_pos[frame_idx, self.skeleton.root_idx : self.skeleton.root_idx + 1] - self.skeleton.neutral_joints[[self.skeleton.root_idx]] ) if joints_rot is None: self.joints_rot[frame_idx] = new_global_rots[0] if joints_pos is None: self.joints_pos[frame_idx] = new_posed_joints if foot_contacts is not None: foot_contacts = to_torch(foot_contacts, device=self.foot_contacts.device).to(dtype=self.foot_contacts.dtype) self.foot_contacts[frame_idx] = foot_contacts if self.character.skeleton_mesh is not None: self.character.skeleton_mesh.update_mesh_info_cache(self.joints_pos[frame_idx], frame_idx) if self.character.skinned_mesh is not None: self.character.update_skinning_cache(self.joints_pos[frame_idx], self.joints_rot[frame_idx], frame_idx)
[docs] def clear(self): self.character.clear()
# # Editing helpers #
[docs] def get_current_projected_root_pos(self) -> np.ndarray: """Get the projected root position on the ground at the current frame.""" root_pos = self.joints_pos[self.cur_frame_idx, self.skeleton.root_idx].clone() root_pos[1] = 0.0 return to_numpy(root_pos)
[docs] def get_projected_root_pos(self, start_frame_idx: int, end_frame_idx: int = None) -> np.ndarray: """If requested frames are out of range, simply pads with the last frame to get expected length.""" if end_frame_idx is None: expected_len = 1 else: expected_len = end_frame_idx - start_frame_idx + 1 if start_frame_idx >= self.length: start_frame_idx = self.length - 1 if end_frame_idx is None or expected_len == 1: root_pos = self.joints_pos[start_frame_idx, self.skeleton.root_idx].clone() root_pos[1] = 0.0 return to_numpy(root_pos) else: if end_frame_idx >= self.length: end_frame_idx = self.length - 1 root_pos = self.joints_pos[start_frame_idx : end_frame_idx + 1, self.skeleton.root_idx].clone() root_pos[:, 1] = 0.0 if root_pos.shape[0] < expected_len: # pad with the last root position root_pos = torch.cat( [ root_pos, root_pos[-1:].repeat(expected_len - root_pos.shape[0], 1), ], dim=0, ) return to_numpy(root_pos)
[docs] def set_projected_root_pos_path( self, root_pos_path: np.ndarray | torch.Tensor, min_frame_idx: int = None, max_frame_idx: int = None, ): """Sets the projected root position path for the character motion. Can set only a subset of the path by providing min_frame_idx and max_frame_idx. If not provided, will set the full path. Args: root_pos_path: torch.Tensor, [T, 2] projected root positions min_frame_idx: int, optional, minimum frame index to set the path at max_frame_idx: int, optional, maximum frame index to set the path at """ if min_frame_idx is not None or max_frame_idx is not None: assert ( min_frame_idx is not None and max_frame_idx is not None ), "min_frame_idx and max_frame_idx must be provided if setting path at specific frames" if min_frame_idx >= self.length: # both are out of bounds return max_frame_idx = min(max_frame_idx, self.length - 1) root_pos_path = root_pos_path[min_frame_idx : max_frame_idx + 1] else: assert root_pos_path.shape[0] == self.length min_frame_idx = 0 max_frame_idx = self.length - 1 cur_joints_pos = self.joints_pos.clone()[min_frame_idx : max_frame_idx + 1] root_pos_tensor = to_torch(root_pos_path, device=cur_joints_pos.device, dtype=cur_joints_pos.dtype) diff = root_pos_tensor - cur_joints_pos[:, self.skeleton.root_idx, [0, 2]] cur_joints_pos[:, :, [0, 2]] += diff.unsqueeze(1) for frame_idx in range(min_frame_idx, max_frame_idx + 1): rel_idx = frame_idx - min_frame_idx self.update_pose_at_frame( frame_idx, joints_pos=cur_joints_pos[rel_idx], joints_rot=self.joints_rot[frame_idx], joints_local_rot=self.joints_local_rot[frame_idx], ) # update immediately to show changes self.set_frame(self.cur_frame_idx)
[docs] def get_joints_pos(self, start_frame_idx: int, end_frame_idx: int = None) -> np.ndarray: """If requested frames are out of range, simply pads with the last frame to get expected length.""" if end_frame_idx is None: expected_len = 1 else: expected_len = end_frame_idx - start_frame_idx + 1 if start_frame_idx >= self.length: start_frame_idx = self.length - 1 if end_frame_idx is None or expected_len == 1: return to_numpy(self.joints_pos[start_frame_idx].clone()) else: if end_frame_idx >= self.length: end_frame_idx = self.length - 1 return_joints_pos = self.joints_pos[start_frame_idx : end_frame_idx + 1].clone() if return_joints_pos.shape[0] < expected_len: # pad with the last pose return_joints_pos = torch.cat( [ return_joints_pos, return_joints_pos[-1:].repeat(expected_len - return_joints_pos.shape[0], 1, 1), ], dim=0, ) return to_numpy(return_joints_pos)
[docs] def get_joints_rot(self, start_frame_idx: int, end_frame_idx: int = None) -> np.ndarray: """If requested frames are out of range, simply pads with the last frame to get expected length.""" if end_frame_idx is None: expected_len = 1 else: expected_len = end_frame_idx - start_frame_idx + 1 if start_frame_idx >= self.length: start_frame_idx = self.length - 1 if end_frame_idx is None or expected_len == 1: return to_numpy(self.joints_rot[start_frame_idx].clone()) else: if end_frame_idx >= self.length: end_frame_idx = self.length - 1 return_joints_rot = self.joints_rot[start_frame_idx : end_frame_idx + 1].clone() if return_joints_rot.shape[0] < expected_len: # pad with the last pose return_joints_rot = torch.cat( [ return_joints_rot, return_joints_rot[-1:].repeat(expected_len - return_joints_rot.shape[0], 1, 1, 1), ], dim=0, ) return to_numpy(return_joints_rot)
[docs] def get_current_joints_pos(self) -> torch.Tensor: return self.joints_pos[self.cur_frame_idx].clone()
[docs] def get_current_joints_rot(self) -> torch.Tensor: return self.joints_rot[self.cur_frame_idx].clone()
[docs] def add_root_translation_gizmo( self, constraints: dict, on_2d_root_drag_end: Optional[Callable[[], None]] = None, on_drag_start: Optional[Callable[[], None]] = None, ): """Create and initialize gizmo to control the root translation. When the user drags the root 2D gizmo, path updates are skipped until release. Optional on_2d_root_drag_end is called when the drag ends (e.g. to refresh dense path). on_drag_start is called when the drag begins (e.g. to snapshot state for undo). """ # TODO: could also allow rotation around y-axis self.root_translation_gizmo = self.server.scene.add_transform_controls( f"/{self.name}/gizmo_root_translation", scale=0.5, line_width=2.5, active_axes=(True, False, True), # only allow translation on xz plane disable_axes=False, disable_sliders=False, disable_rotations=True, depth_test=False, # render even when occluded ) init_position = self.get_current_projected_root_pos() self.root_translation_gizmo.position = init_position @self.root_translation_gizmo.on_drag_start def _(_): if on_drag_start is not None: on_drag_start() @self.root_translation_gizmo.on_update def _(_): self.updating_root_translation_gizmo = True # translate to gizmo position new_root_pos = to_torch( self.root_translation_gizmo.position, device=self.joints_pos.device, ).to(dtype=self.joints_pos.dtype) cur_joints_pos = self.joints_pos[self.cur_frame_idx].clone() root_diff = new_root_pos - cur_joints_pos[self.skeleton.root_idx] root_diff[1] = 0.0 # don't change height cur_joints_pos += root_diff[None] self.update_pose_at_frame( self.cur_frame_idx, joints_pos=cur_joints_pos, joints_rot=self.joints_rot[self.cur_frame_idx], joints_local_rot=self.joints_local_rot[self.cur_frame_idx], ) self.updating_root_translation_gizmo = False # update immediately to show user changes self.set_frame(self.cur_frame_idx) # update the 2D waypoint constraints as well if there is one if "2D Root" in constraints: root_2d_contraints = constraints["2D Root"] # if there is a constraint at that frame, we want to update it frame_idx = self.cur_frame_idx if frame_idx in root_2d_contraints.keyframes: for keyframe_id in root_2d_contraints.frame2keyid[frame_idx]: # add will modify the existing constraint # update_path=False during drag to avoid lag; path refreshes on_drag_end root_2d_contraints.add_keyframe( keyframe_id, frame_idx, root_pos=new_root_pos, exists_ok=True, update_path=False, ) if "Full-Body" in constraints: full_body_constraints = constraints["Full-Body"] # if there is a constraint at that frame, we want to update it frame_idx = self.cur_frame_idx if frame_idx in full_body_constraints.keyframes: current_dict = full_body_constraints.keyframes[frame_idx] for keyframe_id in full_body_constraints.frame2keyid[frame_idx]: # add will modify the existing constraint full_body_constraints.add_keyframe( keyframe_id, frame_idx, joints_pos=cur_joints_pos, joints_rot=current_dict["joints_rot"], exists_ok=True, ) if "End-Effectors" in constraints: end_effector_constraints = constraints["End-Effectors"] # if there is a constraint at that frame, we want to update it frame_idx = self.cur_frame_idx if frame_idx in end_effector_constraints.keyframes: current_dict = end_effector_constraints.keyframes[frame_idx] for keyframe_id, _ in end_effector_constraints.frame2keyid[frame_idx]: # add will modify the existing constraint end_effector_constraints.add_keyframe( keyframe_id, frame_idx, joints_pos=cur_joints_pos, joints_rot=current_dict["joints_rot"], joint_names=current_dict["joint_names"], end_effector_type=current_dict["end_effector_type"], exists_ok=True, ) @self.root_translation_gizmo.on_drag_end def _on_drag_end(_): # Refresh path visualization and dense path after release. if "2D Root" in constraints: root_2d = constraints["2D Root"] if root_2d.line_segments is not None: root_2d.update_line_segments() if on_2d_root_drag_end is not None: on_2d_root_drag_end()
[docs] def add_joint_gizmos( self, constraints: dict, space: Literal["world", "local"] = "local", on_drag_start: Optional[Callable[[], None]] = None, ): # Remove existing joint gizmos first so the client gets remove then add, # avoiding in-place update that can briefly show duplicate gizmos. if self.joint_gizmos is not None: for joint_gizmo in self.joint_gizmos: self.server.scene.remove_by_name(joint_gizmo.name) self.joint_gizmos = None self.joint_gizmos = [] self.gizmo_space = space # For world mode: store joint world rotation at drag start to compose with # PivotControls' cumulative-from-identity drag rotation. self._drag_start_world_rot = [None] * self.skeleton.nbjoints # Skip pushing wxyz/position in set_frame while a gizmo is being dragged, # so the client does not receive "snap back" (e.g. identity for world mode). self._joint_gizmo_dragging = [False] * self.skeleton.nbjoints joint_axis_indices = None joint_limits = None joint_f2q_data = None hidden_gizmo_joints = None if isinstance(self.skeleton, G1Skeleton34): joint_axis_indices = _get_g1_joint_axis_indices() joint_limits = _get_g1_joint_limits() joint_f2q_data = get_g1_joint_f2q_data(self.skeleton) hidden_gizmo_joints = { "left_hand_roll_skel", "right_hand_roll_skel", "left_toe_base", "right_toe_base", } elif isinstance(self.skeleton, SOMASkeleton77): skel30_names = {name for name, _ in SOMASkeleton30.bone_order_names_with_parents} hidden_gizmo_joints = {name for name in self.skeleton.bone_order_names if name not in skel30_names} hidden_gizmo_joints |= { "RightHandThumbEnd", "RightHandMiddleEnd", "LeftHandThumbEnd", "LeftHandMiddleEnd", "LeftEye", "RightEye", "Jaw", } elif isinstance(self.skeleton, SOMASkeleton30): hidden_gizmo_joints = { "RightHandThumbEnd", "RightHandMiddleEnd", "LeftHandThumbEnd", "LeftHandMiddleEnd", "LeftEye", "RightEye", "Jaw", } if space == "world": # World mode: gizmo rings stay scene-axis-aligned (identity). joints_wxyzs = np.tile( np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float64), (self.skeleton.nbjoints, 1), ) else: # Local mode: gizmo shows joint world rotation so rings follow the joint. joints_wxyzs = tf.SO3.from_matrix(self.joints_rot[self.cur_frame_idx].cpu().numpy()).wxyz for joint_idx in range(self.skeleton.nbjoints): disable_axes = True # by default, only rotation controls disable_sliders = True if joint_idx == self.skeleton.root_idx: disable_axes = False # allow translation for root disable_sliders = False active_axes = (True, True, True) if joint_axis_indices is not None: joint_name = self.skeleton.bone_order_names[joint_idx] axis_idx = joint_axis_indices.get(joint_name) if axis_idx is not None: # PivotControls shows rotation handles when a plane is active. # To allow rotation about one axis, enable the other two axes. active_axes = ( axis_idx != 0, axis_idx != 1, axis_idx != 2, ) joint_visible = True if hidden_gizmo_joints is not None: joint_name = self.skeleton.bone_order_names[joint_idx] joint_visible = joint_name not in hidden_gizmo_joints cur_joint_gizmo = self.server.scene.add_transform_controls( f"/{self.name}/gizmo_joint_{joint_idx}", scale=0.075, line_width=4.0, active_axes=active_axes, disable_axes=disable_axes, disable_sliders=disable_sliders, disable_rotations=False, depth_test=False, # render even when occluded position=self.joints_pos[self.cur_frame_idx, joint_idx].cpu().numpy(), wxyz=joints_wxyzs[joint_idx], visible=joint_visible, space=space, ) self.joint_gizmos.append(cur_joint_gizmo) def set_callback_in_closure(i: int) -> None: @cur_joint_gizmo.on_drag_start def _on_drag_start(_) -> None: if on_drag_start is not None: on_drag_start() self._joint_gizmo_dragging[i] = True if self.gizmo_space == "world": self._drag_start_world_rot[i] = self.joints_rot[self.cur_frame_idx, i].clone().cpu().numpy() @cur_joint_gizmo.on_drag_end def _on_drag_end(_) -> None: self._joint_gizmo_dragging[i] = False # Force-sync so the client always receives the reset (viser setter skips on allclose). # Use self.joint_gizmos[i] (not cur_joint_gizmo) to avoid the # closure-in-loop bug: cur_joint_gizmo would point to the last handle. gizmo = self.joint_gizmos[i] gizmo.sync_position(self.joints_pos[self.cur_frame_idx, i].cpu().numpy()) if self.gizmo_space == "world": gizmo.sync_wxyz((1.0, 0.0, 0.0, 0.0)) else: gizmo.sync_wxyz(tf.SO3.from_matrix(self.joints_rot[self.cur_frame_idx, i].cpu().numpy()).wxyz) self.set_frame(self.cur_frame_idx) @cur_joint_gizmo.on_update def _(_) -> None: self.updating_joint_gizmos = True new_local_joint_rots = self.joints_local_rot[self.cur_frame_idx].clone() # Gizmo parent is identity; client sends rotation as wxyz. # World mode: wxyz is cumulative from identity, compose with # stored initial world rotation. Local mode: wxyz is new world rotation. gizmo_rot_mat = tf.SO3(self.joint_gizmos[i].wxyz).as_matrix() if self.gizmo_space == "world" and self._drag_start_world_rot[i] is not None: new_world_rot_mat = gizmo_rot_mat @ self._drag_start_world_rot[i] else: new_world_rot_mat = gizmo_rot_mat parent_idx = self.skeleton.joint_parents[i].item() if parent_idx >= 0: R_parent_world = self.joints_rot[self.cur_frame_idx, parent_idx].detach().cpu().numpy() new_local_rot_mat_np = (R_parent_world.T @ new_world_rot_mat).astype(np.float32) else: new_local_rot_mat_np = new_world_rot_mat.astype(np.float32) new_local_rot = tf.SO3.from_matrix(new_local_rot_mat_np) joint_name = self.skeleton.bone_order_names[i] if joint_f2q_data is not None and joint_name in joint_f2q_data: # G1 hinge: use offset (f2q) space so 1-DoF and limits match the robot. # R_f2q = offset_f2q @ R_local; angle_f2q = dot(axis_angle(R_f2q), axis_f2q); # MuJoCo q = angle_f2q - rest_dof; limits apply to q. f2q = joint_f2q_data[joint_name] offset_f2q = f2q["offset_f2q"] axis_f2q = f2q["axis_f2q"] rest_dof = f2q["rest_dof_axis_angle"] R_local = new_local_rot_mat_np.astype(np.float64) R_f2q = offset_f2q @ R_local rotvec = tf.SO3.from_matrix(R_f2q).log() angle_f2q = float(np.dot(rotvec, axis_f2q)) # Keep angle continuous relative to current pose. current_R_f2q = offset_f2q @ ( self.joints_local_rot[self.cur_frame_idx, i].detach().cpu().numpy().astype(np.float64) ) current_angle_f2q = float(np.dot(tf.SO3.from_matrix(current_R_f2q).log(), axis_f2q)) two_pi = 2.0 * np.pi angle_f2q = angle_f2q + two_pi * np.round((current_angle_f2q - angle_f2q) / two_pi) q = angle_f2q - rest_dof if joint_limits is not None: joint_limit = joint_limits.get(joint_name) if joint_limit is not None: q = float(np.clip(q, joint_limit[0], joint_limit[1])) angle_f2q = q + rest_dof R_f2q_new = tf.SO3.exp(angle_f2q * axis_f2q).as_matrix() new_local_rot_mat_np = (offset_f2q.T @ R_f2q_new).astype(np.float32) elif joint_axis_indices is not None: axis_idx = joint_axis_indices.get(joint_name) if axis_idx is not None: rotvec = new_local_rot.log() axis = np.zeros(3, dtype=np.float64) axis[axis_idx] = 1.0 angle = float(rotvec[axis_idx]) # Keep angle continuous relative to current pose. current_rot = tf.SO3.from_matrix( self.joints_local_rot[self.cur_frame_idx, i].detach().cpu().numpy() ) current_angle = float(current_rot.log()[axis_idx]) two_pi = 2.0 * np.pi angle = angle + two_pi * np.round((current_angle - angle) / two_pi) if joint_limits is not None: joint_limit = joint_limits.get(joint_name) if joint_limit is not None: angle = float(np.clip(angle, joint_limit[0], joint_limit[1])) new_local_rot_mat_np = tf.SO3.exp(angle * axis).as_matrix() new_local_rot_mat = torch.tensor(new_local_rot_mat_np).to(new_local_joint_rots.device) new_local_joint_rots[i] = new_local_rot_mat self.update_pose_at_frame( self.cur_frame_idx, joints_local_rot=new_local_joint_rots, ) # handle root translation separately cur_joints_pos = self.joints_pos[self.cur_frame_idx].clone() if i == self.skeleton.root_idx: new_root_pos = to_torch( self.joint_gizmos[i].position, device=self.joints_pos.device, ).to(dtype=self.joints_pos.dtype) root_diff = new_root_pos - self.joints_pos[self.cur_frame_idx, i] if torch.norm(root_diff) > 1e-3: # the root translation has been changed # translate to gizmo position cur_joints_pos += root_diff[None] self.update_pose_at_frame( self.cur_frame_idx, joints_pos=cur_joints_pos, joints_rot=self.joints_rot[self.cur_frame_idx], joints_local_rot=self.joints_local_rot[self.cur_frame_idx], ) # update immediately to show user changes. Keep updating_joint_gizmos # True so set_frame does not overwrite gizmo wxyz mid-drag. self.set_frame(self.cur_frame_idx) self.updating_joint_gizmos = False if i == self.skeleton.root_idx: # update the 2D waypoint constraints as well if there is one if "2D Root" in constraints: root_2d_contraints = constraints["2D Root"] # if there is a constraint at that frame, we want to update it frame_idx = self.cur_frame_idx if frame_idx in root_2d_contraints.keyframes: new_root_pos[1] = 0.0 # force y to 0 for keyframe_id in root_2d_contraints.frame2keyid[frame_idx]: # add will modify the existing constraint root_2d_contraints.add_keyframe( keyframe_id, frame_idx, root_pos=new_root_pos, exists_ok=True, update_path=False, ) if "Full-Body" in constraints: full_body_constraints = constraints["Full-Body"] # if there is a constraint at that frame, we want to update it frame_idx = self.cur_frame_idx if frame_idx in full_body_constraints.keyframes: for keyframe_id in full_body_constraints.frame2keyid[frame_idx]: # add will modify the existing constraint full_body_constraints.add_keyframe( keyframe_id, frame_idx, joints_pos=self.joints_pos[frame_idx], joints_rot=self.joints_rot[frame_idx], exists_ok=True, ) if "End-Effectors" in constraints: end_effector_constraints = constraints["End-Effectors"] # if there is a constraint at that frame, we want to update it frame_idx = self.cur_frame_idx if frame_idx in end_effector_constraints.keyframes: current_dict = end_effector_constraints.keyframes[frame_idx] for keyframe_id, _ in end_effector_constraints.frame2keyid[frame_idx]: # add will modify the existing constraint end_effector_constraints.add_keyframe( keyframe_id, frame_idx, joints_pos=self.joints_pos[frame_idx], joints_rot=self.joints_rot[frame_idx], joint_names=current_dict["joint_names"], end_effector_type=current_dict["end_effector_type"], exists_ok=True, ) set_callback_in_closure(joint_idx)
[docs] def clear_all_gizmos(self): self.updating_root_translation_gizmo = True self.updating_joint_gizmos = True if self.root_translation_gizmo is not None: self.server.scene.remove_by_name(self.root_translation_gizmo.name) self.root_translation_gizmo = None if self.joint_gizmos is not None: for joint_gizmo in self.joint_gizmos: self.server.scene.remove_by_name(joint_gizmo.name) self.joint_gizmos = None self._drag_start_world_rot = [] self._joint_gizmo_dragging = [] self.updating_root_translation_gizmo = False self.updating_joint_gizmos = False