ygauravyy's picture
Upload 722 files
d49f7bc verified
raw
history blame
32.9 kB
# Copyright (c) Meta Platforms, Inc. and affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import logging
import ctypes
import heapq
import math
import time
from typing import Dict, List, Tuple, Optional, TypedDict, DefaultDict
from collections import defaultdict
from pathlib import Path
import cv2
import numpy as np
import numpy.typing as npt
from skimage import measure
from shapely import geometry
from OpenGL import GL
from scipy.spatial import Delaunay
from animated_drawings.model.transform import Transform
from animated_drawings.model.time_manager import TimeManager
from animated_drawings.model.retargeter import Retargeter
from animated_drawings.model.arap import ARAP
from animated_drawings.model.joint import Joint
from animated_drawings.model.quaternions import Quaternions
from animated_drawings.model.vectors import Vectors
from animated_drawings.config import CharacterConfig, MotionConfig, RetargetConfig
class AnimatedDrawingMesh(TypedDict):
vertices: npt.NDArray[np.float32]
triangles: List[npt.NDArray[np.int32]]
class AnimatedDrawingsJoint(Joint):
""" Joints within Animated Drawings Rig."""
def __init__(self, name: str, x: float, y: float):
super().__init__(name=name, offset=np.array([x, 1 - y, 0]))
self.starting_theta: float
self.current_theta: float
class AnimatedDrawingRig(Transform):
""" The skeletal rig used to deform the character """
def __init__(self, char_cfg: CharacterConfig):
""" Initializes character rig. """
super().__init__()
# create dictionary populated with joints
joints_d: Dict[str, AnimatedDrawingsJoint]
joints_d = {joint['name']: AnimatedDrawingsJoint(joint['name'], *joint['loc']) for joint in char_cfg.skeleton}
# assign joints within dictionary as childre of their parents
for joint_d in char_cfg.skeleton:
if joint_d['parent'] is None:
continue
joints_d[joint_d['parent']].add_child(joints_d[joint_d['name']])
# updates joint positions to reflect local offsets from their parent joints
def _update_positions(t: Transform):
""" Now that kinematic parent-> child chain is formed, subtract parent world positions to get actual child offsets"""
parent: Optional[Transform] = t.get_parent()
if parent is not None:
offset = np.subtract(t.get_local_position(), parent.get_world_position())
t.set_position(offset)
for c in t.get_children():
_update_positions(c)
_update_positions(joints_d['root'])
# compute the starting rotation (CCW from +Y axis) of each joint
for _, joint in joints_d.items():
parent = joint.get_parent()
if parent is None:
joint.starting_theta = 0
continue
v1_xy = np.array([0.0, 1.0])
v2 = Vectors([np.subtract(joint.get_world_position(), parent.get_world_position())])
v2.norm()
v2_xy: npt.NDArray[np.float32] = v2.vs[0, :2]
theta = np.arctan2(v2_xy[1], v2_xy[0]) - np.arctan2(v1_xy[1], v1_xy[0])
theta = np.degrees(theta)
theta = theta % 360.0
theta = np.where(theta < 0.0, theta + 360, theta)
joint.starting_theta = float(theta)
# attach root joint
self.root_joint = joints_d['root']
self.add_child(self.root_joint)
# cache for later
self.joint_count = joints_d['root'].joint_count()
# set up buffer for visualizing vertices
self.vertices = np.zeros([2 * (self.joint_count - 1), 6], np.float32)
self._is_opengl_initialized: bool = False
self._vertex_buffer_dirty_bit: bool = True
def set_global_orientations(self, bvh_frame_orientations: Dict[str, float]) -> None:
""" Applies orientation from bvh_frame_orientation to the rig. """
self._set_global_orientations(self.root_joint, bvh_frame_orientations)
self._vertex_buffer_dirty_bit = True
def get_joints_2D_positions(self) -> npt.NDArray[np.float32]:
""" Returns array of 2D joints positions for rig. """
return np.array(self.root_joint.get_chain_worldspace_positions()).reshape([-1, 3])[:, :2]
def _compute_buffer_vertices(self, parent: Optional[Transform], pointer: List[int]) -> None:
""" Recomputes values to pass to vertex buffer. Called recursively, pointer is List[int] to emulate pass-by-reference """
if parent is None:
parent = self.root_joint
for c in parent.get_children():
p1 = c.get_world_position()
p2 = parent.get_world_position()
self.vertices[pointer[0], 0:3] = p1
self.vertices[pointer[0] + 1, 0:3] = p2
pointer[0] += 2
self._compute_buffer_vertices(c, pointer)
def _initialize_opengl_resources(self):
self.vao = GL.glGenVertexArrays(1)
self.vbo = GL.glGenBuffers(1)
GL.glBindVertexArray(self.vao)
# buffer vertex data
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vbo)
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vertices, GL.GL_STATIC_DRAW)
vert_bytes: int = 4 * self.vertices.shape[1] # 4 is byte size of np.float32
# position attributes
pos_offset = 4 * 0
GL.glVertexAttribPointer( 0, 3, GL.GL_FLOAT, False, vert_bytes, ctypes.c_void_p(pos_offset))
GL.glEnableVertexAttribArray(0)
# color attributes
color_offset = 4 * 3
GL.glVertexAttribPointer( 1, 3, GL.GL_FLOAT, False, vert_bytes, ctypes.c_void_p(color_offset))
GL.glEnableVertexAttribArray(1)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
GL.glBindVertexArray(0)
self._is_opengl_initialized = True
def _compute_and_buffer_vertex_data(self):
self._compute_buffer_vertices(parent=self.root_joint, pointer=[0])
GL.glBindVertexArray(self.vao)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vbo)
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vertices, GL.GL_STATIC_DRAW)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
GL.glBindVertexArray(0)
self._vertex_buffer_dirty_bit = False
def _set_global_orientations(self, joint: AnimatedDrawingsJoint, bvh_orientations: Dict[str, float]) -> None:
if joint.name in bvh_orientations.keys():
theta: float = bvh_orientations[str(joint.name)] - joint.starting_theta
theta = np.radians(theta)
joint.current_theta = theta
parent = joint.get_parent()
assert isinstance(parent, AnimatedDrawingsJoint)
if hasattr(parent, 'current_theta'):
theta = theta - parent.current_theta
rotation_q = Quaternions.from_angle_axis(np.array([theta]), axes=Vectors([0.0, 0.0, 1.0]))
parent.set_rotation(rotation_q)
parent.update_transforms()
for c in joint.get_children():
if isinstance(c, AnimatedDrawingsJoint):
self._set_global_orientations(c, bvh_orientations)
def _draw(self, **kwargs):
if not kwargs['viewer_cfg'].draw_ad_rig:
return
if not self._is_opengl_initialized:
self._initialize_opengl_resources()
if self._vertex_buffer_dirty_bit:
self._compute_and_buffer_vertex_data()
GL.glDisable(GL.GL_DEPTH_TEST)
GL.glUseProgram(kwargs['shader_ids']['color_shader'])
model_loc = GL.glGetUniformLocation(kwargs['shader_ids']['color_shader'], "model")
GL.glUniformMatrix4fv(model_loc, 1, GL.GL_FALSE, self._world_transform.T)
GL.glBindVertexArray(self.vao)
GL.glDrawArrays(GL.GL_LINES, 0, len(self.vertices))
GL.glEnable(GL.GL_DEPTH_TEST)
class AnimatedDrawing(Transform, TimeManager):
"""
The drawn character to be animated.
An AnimatedDrawings object consists of four main parts:
1. A 2D mesh textured with the original drawing, the 'visual' representation of the character
2. A 2D skeletal rig
3. An ARAP module which uses rig joint positions to deform the mesh
4. A retargeting module which reposes the rig.
After initializing the object, the retarger must be initialized by calling initialize_retarger_bvh().
Afterwars, only the update() method needs to be called.
"""
def __init__(self, char_cfg: CharacterConfig, retarget_cfg: RetargetConfig, motion_cfg: MotionConfig):
super().__init__()
self.char_cfg: CharacterConfig = char_cfg
self.retarget_cfg: RetargetConfig = retarget_cfg
self.img_dim: int = self.char_cfg.img_dim
# load mask and pad to square
self.mask: npt.NDArray[np.uint8] = self._load_mask()
# load texture and pad to square
self.txtr: npt.NDArray[np.uint8] = self._load_txtr()
# generate the mesh
self.mesh: AnimatedDrawingMesh
self._generate_mesh()
self.rig = AnimatedDrawingRig(self.char_cfg)
self.add_child(self.rig)
# perform runtime checks for character pose, modify retarget config accordingly
self._modify_retargeting_cfg_for_character()
self.joint_to_tri_v_idx: Dict[str, npt.NDArray[np.int32]]
self._initialize_joint_to_triangles_dict()
self.indices: npt.NDArray[np.int32] = np.stack(self.mesh['triangles']).flatten() # order in which to render triangles
self.retargeter: Retargeter
self._initialize_retargeter_bvh(motion_cfg, retarget_cfg)
# initialize arap solver with original joint positions
self.arap = ARAP(self.rig.get_joints_2D_positions(), self.mesh['triangles'], self.mesh['vertices'])
self.vertices: npt.NDArray[np.float32]
self._initialize_vertices()
self._is_opengl_initialized: bool = False
self._vertex_buffer_dirty_bit: bool = True
# pose the animated drawing using the first frame of the bvh
self.update()
def _modify_retargeting_cfg_for_character(self):
"""
If the character is drawn in particular poses, the orientation-matching retargeting framework produce poor results.
Therefore, the retargeter config can specify a number of runtime checks and retargeting modifications to make if those checks fail.
"""
for position_test, target_joint_name, joint1_name, joint2_name in self.retarget_cfg.char_runtime_checks:
if position_test == 'above':
""" Checks whether target_joint is 'above' the vector from joint1 to joint2. If it's below, removes it.
This was added to account for head flipping when nose was below shoulders. """
# get joints 1, 2 and target joint
joint1 = self.rig.root_joint.get_transform_by_name(joint1_name)
if joint1 is None:
msg = f'Could not find joint1 in runtime check: {joint1_name}'
logging.critical(msg)
assert False, msg
joint2 = self.rig.root_joint.get_transform_by_name(joint2_name)
if joint2 is None:
msg = f'Could not find joint2 in runtime check: {joint2_name}'
logging.critical(msg)
assert False, msg
target_joint = self.rig.root_joint.get_transform_by_name(target_joint_name)
if target_joint is None:
msg = f'Could not find target_joint in runtime check: {target_joint_name}'
logging.critical(msg)
assert False, msg
# get world positions
joint1_xyz = joint1.get_world_position()
joint2_xyz = joint2.get_world_position()
target_joint_xyz = target_joint.get_world_position()
# rotate target vector by inverse of test_vector angle. If then below x axis discard it.
test_vector = np.subtract(joint2_xyz, joint1_xyz)
target_vector = np.subtract(target_joint_xyz, joint1_xyz)
angle = math.atan2(test_vector[1], test_vector[0])
if (math.sin(-angle) * target_vector[0] + math.cos(-angle) * target_vector[1]) < 0:
logging.info(f'char_runtime_check failed, removing {target_joint_name} from retargeter :{target_joint_name, position_test, joint1_name, joint2_name}')
del self.retarget_cfg.char_joint_bvh_joints_mapping[target_joint_name]
else:
msg = f'Unrecognized char_runtime_checks position_test: {position_test}'
logging.critical(msg)
assert False, msg
def _initialize_retargeter_bvh(self, motion_cfg: MotionConfig, retarget_cfg: RetargetConfig):
""" Initializes the retargeter used to drive the animated character. """
# initialize retargeter
self.retargeter = Retargeter(motion_cfg, retarget_cfg)
# validate the motion and retarget config files, now that we know char/bvh joint names
char_joint_names: List[str] = self.rig.root_joint.get_chain_joint_names()
bvh_joint_names = self.retargeter.bvh_joint_names
motion_cfg.validate_bvh(bvh_joint_names)
retarget_cfg.validate_char_and_bvh_joint_names(char_joint_names, bvh_joint_names)
# a shorter alias
char_bvh_root_offset: RetargetConfig.CharBvhRootOffset = self.retarget_cfg.char_bvh_root_offset
# compute ratio of character's leg length to bvh skel leg length
c_limb_length = 0
c_joint_groups: List[List[str]] = char_bvh_root_offset['char_joints']
for b_joint_group in c_joint_groups:
while len(b_joint_group) >= 2:
c_dist_joint = self.rig.root_joint.get_transform_by_name(b_joint_group[1])
c_prox_joint = self.rig.root_joint.get_transform_by_name(b_joint_group[0])
assert isinstance(c_dist_joint, AnimatedDrawingsJoint)
assert isinstance(c_prox_joint, AnimatedDrawingsJoint)
c_dist_joint_pos = c_dist_joint.get_world_position()
c_prox_joint_pos = c_prox_joint.get_world_position()
c_limb_length += np.linalg.norm(np.subtract(c_dist_joint_pos, c_prox_joint_pos))
b_joint_group.pop(0)
b_limb_length = 0
b_joint_groups: List[List[str]] = char_bvh_root_offset['bvh_joints']
for b_joint_group in b_joint_groups:
while len(b_joint_group) >= 2:
b_dist_joint = self.retargeter.bvh.root_joint.get_transform_by_name(b_joint_group[1])
b_prox_joint = self.retargeter.bvh.root_joint.get_transform_by_name(b_joint_group[0])
assert isinstance(b_dist_joint, Joint)
assert isinstance(b_prox_joint, Joint)
b_dist_joint_pos = b_dist_joint.get_world_position()
b_prox_joint_pos = b_prox_joint.get_world_position()
b_limb_length += np.linalg.norm(np.subtract(b_dist_joint_pos, b_prox_joint_pos))
b_joint_group.pop(0)
# compute character-bvh scale factor and send to retargeter
scale_factor = float(c_limb_length / b_limb_length)
projection_bodypart_group_for_offset = char_bvh_root_offset['bvh_projection_bodypart_group_for_offset']
self.retargeter.scale_root_positions_for_character(scale_factor, projection_bodypart_group_for_offset)
# compute the necessary orienations
for char_joint_name, (bvh_prox_joint_name, bvh_dist_joint_name) in self.retarget_cfg.char_joint_bvh_joints_mapping.items():
self.retargeter.compute_orientations(bvh_prox_joint_name, bvh_dist_joint_name, char_joint_name)
def update(self):
"""
This method receives the delta t, the amount of time to progress the character's internal time keeper.
This method passes its time to the retargeter, which returns bone orientations.
Orientations are passed to rig to calculate new joint positions.
The updated joint positions are passed into the ARAP module, which computes the new vertex locations.
The new vertex locations are stored and the dirty bit is set.
"""
# get retargeted motion data
frame_orientations: Dict[str, float]
joint_depths: Dict[str, float]
root_position: npt.NDArray[np.float32]
frame_orientations, joint_depths, root_position = self.retargeter.get_retargeted_frame_data(self.get_time())
# update the rig's root position and reorient all of its joints
self.rig.root_joint.set_position(root_position)
self.rig.set_global_orientations(frame_orientations)
# using new joint positions, calculate new mesh vertex xy positions
control_points: npt.NDArray[np.float32] = self.rig.get_joints_2D_positions() - root_position[:2]
self.vertices[:, :2] = self.arap.solve(control_points) + root_position[:2]
# use the z position of the rig's root joint for all mesh vertices
self.vertices[:, 2] = self.rig.root_joint.get_world_position()[2]
self._vertex_buffer_dirty_bit = True
# using joint depths, determine the correct order in which to render the character
self._set_draw_indices(joint_depths)
def _set_draw_indices(self, joint_depths: Dict[str, float]):
# sort segmentation groups by decreasing depth_driver's distance to camera
_bodypart_render_order: List[Tuple[int, np.float32]] = []
for idx, bodypart_group_dict in enumerate(self.retarget_cfg.char_bodypart_groups):
bodypart_depth: np.float32 = np.mean([joint_depths[joint_name] for joint_name in bodypart_group_dict['bvh_depth_drivers']])
_bodypart_render_order.append((idx, bodypart_depth))
_bodypart_render_order.sort(key=lambda x: float(x[1]))
# Add vertices belonging to joints in each segment group in the order they will be rendered
indices: List[npt.NDArray[np.int32]] = []
for idx, dist in _bodypart_render_order:
intra_bodypart_render_order = 1 if dist > 0 else -1 # if depth driver is behind plane, render bodyparts in reverse order
for joint_name in self.retarget_cfg.char_bodypart_groups[idx]['char_joints'][::intra_bodypart_render_order]:
indices.append(self.joint_to_tri_v_idx.get(joint_name, np.array([], dtype=np.int32)))
self.indices = np.hstack(indices)
def _initialize_joint_to_triangles_dict(self) -> None: # noqa: C901
"""
Uses BFS to find and return the closest joint bone (line segment between joint and parent) to each triangle centroid.
"""
shortest_distance = np.full(self.mask.shape, 1 << 12, dtype=np.int32) # to nearest joint
closest_joint_idx = np.full(self.mask.shape, -1, dtype=np.int8) # track joint idx nearest each point
# temp dictionary to help with seed generation
joints_d: Dict[str, CharacterConfig.JointDict] = {}
for joint in self.char_cfg.skeleton:
joints_d[joint['name']] = joint
joints_d[joint['name']]['loc'][1] = 1 - joints_d[joint['name']]['loc'][1]
# store joint names and later reference by element location
joint_name_to_idx: List[str] = [joint['name'] for joint in self.char_cfg.skeleton]
# seed generation
heap: List[Tuple[float, Tuple[int, Tuple[int, int]]]] = [] # [(dist, (joint_idx, (x, y))]
for _, joint in joints_d.items():
if joint['parent'] is None: # skip root joint
continue
joint_idx = joint_name_to_idx.index(joint['name'])
dist_joint_xy: List[float] = joint['loc']
prox_joint_xy: List[float] = joints_d[joint['parent']]['loc']
seeds_xy = (self.img_dim * np.linspace(dist_joint_xy, prox_joint_xy, num=20, endpoint=False)).round()
heap.extend([(0, (joint_idx, tuple(seed_xy.astype(np.int32)))) for seed_xy in seeds_xy])
# BFS search
start_time: float = time.time()
logging.info('Starting joint -> mask pixel BFS')
while heap:
distance, (joint_idx, (x, y)) = heapq.heappop(heap)
neighbors = [(x-1, y-1), (x, y-1), (x+1, y-1), (x-1, y), (x+1, y), (x-1, y+1), (x, y+1), (x+1, y+1)]
n_dist = [1.414, 1.0, 1.414, 1.0, 1.0, 1.414, 1.0, 1.414]
for (n_x, n_y), n_dist in zip(neighbors, n_dist):
n_distance = distance + n_dist
if not 0 <= n_x < self.img_dim or not 0 <= n_y < self.img_dim:
continue # neighbor is outside image bounds- ignore
if not self.mask[n_x, n_y]:
continue # outside character mask
if shortest_distance[n_x, n_y] <= n_distance:
continue # a closer joint exists
closest_joint_idx[n_x, n_y] = joint_idx
shortest_distance[n_x, n_y] = n_distance
heapq.heappush(heap, (n_distance, (joint_idx, (n_x, n_y))))
logging.info(f'Finished joint -> mask pixel BFS in {time.time() - start_time} seconds')
# create map between joint name and triangle centroids it is closest to
joint_to_tri_v_idx_and_dist: DefaultDict[str, List[Tuple[npt.NDArray[np.int32], np.int32]]] = defaultdict(list)
for tri_v_idx in self.mesh['triangles']:
tri_verts = np.array([self.mesh['vertices'][v_idx] for v_idx in tri_v_idx])
centroid_x, centroid_y = list((tri_verts.mean(axis=0) * self.img_dim).round().astype(np.int32))
tri_centroid_closest_joint_idx: np.int8 = closest_joint_idx[centroid_x, centroid_y]
dist_from_tri_centroid_to_bone: np.int32 = shortest_distance[centroid_x, centroid_y]
joint_to_tri_v_idx_and_dist[joint_name_to_idx[tri_centroid_closest_joint_idx]].append((tri_v_idx, dist_from_tri_centroid_to_bone))
joint_to_tri_v_idx: Dict[str, npt.NDArray[np.int32]] = {}
for key, val in joint_to_tri_v_idx_and_dist.items():
# sort by distance, descending
val.sort(key=lambda x: float(x[1]), reverse=True)
# retain vertex indices, remove distance info
val = [v[0] for v in val]
# convert to np array and save in dictionary
joint_to_tri_v_idx[key] = np.array(val).flatten() # type: ignore
self.joint_to_tri_v_idx = joint_to_tri_v_idx
def _load_mask(self) -> npt.NDArray[np.uint8]:
""" Load and perform preprocessing upon the mask """
mask_p: Path = self.char_cfg.mask_p
try:
_mask: npt.NDArray[np.uint8] = cv2.imread(str(mask_p), cv2.IMREAD_GRAYSCALE).astype(np.uint8)
if _mask.shape[0] != self.char_cfg.img_height:
raise AssertionError('height in character config and mask height do not match')
if _mask.shape[1] != self.char_cfg.img_width:
raise AssertionError('width in character config and mask height do not match')
except Exception as e:
msg = f'Error loading mask {mask_p}: {str(e)}'
logging.critical(msg)
assert False, msg
_mask = np.rot90(_mask, 3, ) # rotate to upright
# pad to square
mask = np.zeros([self.img_dim, self.img_dim], _mask.dtype)
mask[0:_mask.shape[0], 0:_mask.shape[1]] = _mask
return mask
def _load_txtr(self) -> npt.NDArray[np.uint8]:
""" Load and perform preprocessing upon the drawing image """
txtr_p: Path = self.char_cfg.txtr_p
try:
_txtr: npt.NDArray[np.uint8] = cv2.imread(str(txtr_p), cv2.IMREAD_IGNORE_ORIENTATION | cv2.IMREAD_UNCHANGED).astype(np.uint8)
_txtr = cv2.cvtColor(_txtr, cv2.COLOR_BGRA2RGBA).astype(np.uint8)
if _txtr.shape[-1] != 4:
raise AssertionError('texture must be RGBA')
if _txtr.shape[0] != self.char_cfg.img_height:
raise AssertionError('height in character config and txtr height do not match')
if _txtr.shape[1] != self.char_cfg.img_width:
raise AssertionError('width in character config and txtr height do not match')
except Exception as e:
msg = f'Error loading texture {txtr_p}: {str(e)}'
logging.critical(msg)
assert False, msg
_txtr = np.rot90(_txtr, 3, ) # rotate to upright
# pad to square
txtr = np.zeros([self.img_dim, self.img_dim, _txtr.shape[-1]], _txtr.dtype)
txtr[0:_txtr.shape[0], 0:_txtr.shape[1], :] = _txtr
txtr[np.where(self.mask == 0)][:, 3] = 0 # make pixels outside mask transparent
return txtr
def _generate_mesh(self) -> None:
try:
contours: List[npt.NDArray[np.float64]] = measure.find_contours(self.mask, 128)
except Exception as e:
msg = f'Error finding contours for character mesh: {str(e)}'
logging.critical(msg)
assert False, msg
# if multiple distinct polygons are in the mask, use largest and discard the rest
if len(contours) > 1:
msg = f'{len(contours)} separate polygons found in mask. Using largest.'
logging.info(msg)
contours.sort(key=len, reverse=True)
outside_vertices: npt.NDArray[np.float64] = measure.approximate_polygon(contours[0], tolerance=0.25)
character_outline = geometry.Polygon(contours[0])
# add some internal vertices to ensure a good mesh is created
inside_vertices_xy: List[Tuple[np.float32, np.float32]] = []
_x = np.linspace(0, self.img_dim, 40)
_y = np.linspace(0, self.img_dim, 40)
xv, yv = np.meshgrid(_x, _y)
for x, y in zip(xv.flatten(), yv.flatten()):
if character_outline.contains(geometry.Point(x, y)):
inside_vertices_xy.append((x, y))
inside_vertices: npt.NDArray[np.float64] = np.array(inside_vertices_xy)
vertices: npt.NDArray[np.float32] = np.concatenate([outside_vertices, inside_vertices]).astype(np.float32)
"""
Create a convex hull containing the character.
Then remove unnecessary edges by discarding triangles whose centroid
falls outside the character's outline.
"""
convex_hull_triangles = Delaunay(vertices)
triangles: List[npt.NDArray[np.int32]] = []
for _triangle in convex_hull_triangles.simplices:
tri_vertices = np.array(
[vertices[_triangle[0]], vertices[_triangle[1]], vertices[_triangle[2]]])
tri_centroid = geometry.Point(np.mean(tri_vertices, 0))
if character_outline.contains(tri_centroid):
triangles.append(_triangle)
vertices /= self.img_dim # scale vertices so they lie between 0-1
self.mesh = {'vertices': vertices, 'triangles': triangles}
def _initialize_vertices(self) -> None:
"""
Prepare the ndarray that will be sent to rendering pipeline.
Later, x and y vertex positions will change, but z pos, u v texture, and rgb color won't.
"""
self.vertices = np.zeros((self.mesh['vertices'].shape[0], 8), np.float32)
# initialize xy positions of mesh vertices
self.vertices[:, :2] = self.arap.solve(self.rig.get_joints_2D_positions()).reshape([-1, 2])
# initialize texture coordinates
self.vertices[:, 6] = self.mesh['vertices'][:, 1] # u tex
self.vertices[:, 7] = self.mesh['vertices'][:, 0] # v tex
# set per-joint triangle colors
color_set: set[Tuple[np.float32, np.float32, np.float32]] = set()
r = g = b = np.linspace(0, 1, 4, dtype=np.float32)
while len(color_set) < len(self.joint_to_tri_v_idx):
color = (np.random.choice(r), np.random.choice(g), np.random.choice(b))
color_set.add(color)
colors: npt.NDArray[np.float32] = np.array(list(color_set), np.float32)
for c_idx, v_idxs in enumerate(self.joint_to_tri_v_idx.values()):
self.vertices[v_idxs, 3:6] = colors[c_idx] # rgb colors
def _initialize_opengl_resources(self) -> None:
h, w, _ = self.txtr.shape
# # initialize the texture
self.txtr_id = GL.glGenTextures(1)
GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 4)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.txtr_id)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_BASE_LEVEL, 0)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAX_LEVEL, 0)
GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, w, h,
0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, self.txtr)
self.vao = GL.glGenVertexArrays(1)
self.vbo = GL.glGenBuffers(1)
self.ebo = GL.glGenBuffers(1)
GL.glBindVertexArray(self.vao)
# buffer vertex data
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vbo)
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vertices, GL.GL_DYNAMIC_DRAW)
# buffer element index data
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.ebo)
GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER,
self.indices, GL.GL_STATIC_DRAW)
# position attributes
GL.glVertexAttribPointer(
0, 3, GL.GL_FLOAT, False, 4 * self.vertices.shape[1], None)
GL.glEnableVertexAttribArray(0)
# color attributes
GL.glVertexAttribPointer(
1, 3, GL.GL_FLOAT, False, 4 * self.vertices.shape[1], ctypes.c_void_p(4 * 3))
GL.glEnableVertexAttribArray(1)
# texture attributes
GL.glVertexAttribPointer(
2, 2, GL.GL_FLOAT, False, 4 * self.vertices.shape[1], ctypes.c_void_p(4 * 6))
GL.glEnableVertexAttribArray(2)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
GL.glBindVertexArray(0)
self._is_opengl_initialized = True
def _rebuffer_vertex_data(self):
GL.glBindVertexArray(self.vao)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vbo)
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vertices, GL.GL_STATIC_DRAW)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
# buffer element index data
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.ebo)
GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER,
self.indices, GL.GL_STATIC_DRAW)
GL.glBindVertexArray(0)
self._vertex_buffer_dirty_bit = False
def _draw(self, **kwargs):
if not self._is_opengl_initialized:
self._initialize_opengl_resources()
if self._vertex_buffer_dirty_bit:
self._rebuffer_vertex_data()
GL.glBindVertexArray(self.vao)
if kwargs['viewer_cfg'].draw_ad_txtr:
GL.glActiveTexture(GL.GL_TEXTURE0)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.txtr_id)
GL.glDisable(GL.GL_DEPTH_TEST)
GL.glUseProgram(kwargs['shader_ids']['texture_shader'])
model_loc = GL.glGetUniformLocation(kwargs['shader_ids']['texture_shader'], "model")
GL.glUniformMatrix4fv(model_loc, 1, GL.GL_FALSE, self._world_transform.T)
GL.glDrawElements(GL.GL_TRIANGLES, self.indices.shape[0], GL.GL_UNSIGNED_INT, None)
GL.glEnable(GL.GL_DEPTH_TEST)
if kwargs['viewer_cfg'].draw_ad_color:
GL.glDisable(GL.GL_DEPTH_TEST)
GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL)
GL.glUseProgram(kwargs['shader_ids']['color_shader'])
model_loc = GL.glGetUniformLocation(kwargs['shader_ids']['color_shader'], "model")
GL.glUniformMatrix4fv(model_loc, 1, GL.GL_FALSE, self._world_transform.T)
GL.glDrawElements(GL.GL_TRIANGLES, self.indices.shape[0], GL.GL_UNSIGNED_INT, None)
GL.glEnable(GL.GL_DEPTH_TEST)
if kwargs['viewer_cfg'].draw_ad_mesh_lines:
GL.glDisable(GL.GL_DEPTH_TEST)
GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE)
GL.glUseProgram(kwargs['shader_ids']['color_shader'])
model_loc = GL.glGetUniformLocation(kwargs['shader_ids']['color_shader'], "model")
GL.glUniformMatrix4fv(model_loc, 1, GL.GL_FALSE, self._world_transform.T)
color_black_loc = GL.glGetUniformLocation(kwargs['shader_ids']['color_shader'], "color_black")
GL.glUniform1i(color_black_loc, 1)
GL.glDrawElements(GL.GL_TRIANGLES, self.indices.shape[0], GL.GL_UNSIGNED_INT, None)
GL.glUniform1i(color_black_loc, 0)
GL.glEnable(GL.GL_DEPTH_TEST)
GL.glBindVertexArray(0)