Spaces:
Building
Building
File size: 32,851 Bytes
d49f7bc |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 |
# 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)
|