import igl import json import pickle import numpy as np import yaml import warp as wp import warp.sim.render from warp.sim.utils import implicit_laplacian_smoothing import warp.collision.panel_assignment as assign from warp.sim.collide import count_self_intersections, count_body_cloth_intersections from warp.sim.integrator_xpbd import replace_mesh_points # Custom from pygarment.meshgen.sim_config import PathCofig, SimConfig from pygarment.pattern.core import BasicPattern class Cloth: def __init__(self, name, config: SimConfig, paths: PathCofig, caching=False): self.caching = caching # Saves intermediate frames, extra logs, etc. self.paths = paths self.name = name self.config = config self.sim_fps = config.sim_fps self.sim_substeps = config.sim_substeps self.zero_gravity_steps = config.zero_gravity_steps self.sim_dt = (1.0 / self.sim_fps) / self.sim_substeps self.usd_frame_time = 0.0 self.sim_use_graph = wp.get_device().is_cuda self.device = wp.get_device() if wp.get_device().is_cuda else 'cpu' self.frame = -1 self.c_scale = 1.0 self.b_scale = 100.0 self.body_path = paths.in_body_obj # collision resolution options self.enable_body_smoothing = config.enable_body_smoothing self.enable_cloth_reference_drag = config.enable_cloth_reference_drag # Build the stage -- model object, colliders, etc. self.build_stage(config) # -------- Final model settings ---------- # NOTE: global_viscous_damping: (damping_factor, min_vel_damp, max_vel) # apply damping when vel > min_vel_damp, and clamp vel below max_vel after damping # TODO Remove after refactoring Euler integrator self.model.global_viscous_damping = wp.vec3( (config.global_damping_factor, config.global_damping_effective_velocity, config.global_max_velocity)) self.model.particle_max_velocity = config.global_max_velocity self.model.ground = config.ground self.model.global_collision_filter = config.enable_global_collision_filter self.model.cloth_reference_drag = self.enable_cloth_reference_drag self.model.cloth_reference_margin = config.cloth_reference_margin self.model.cloth_reference_k = config.cloth_reference_k self.model.cloth_reference_watertight_whole_shape_index = 0 self.model.enable_particle_particle_collisions = config.enable_particle_particle_collisions self.model.enable_triangle_particle_collisions = config.enable_triangle_particle_collisions self.model.enable_edge_edge_collisions = config.enable_edge_edge_collisions self.model.attachment_constraint = config.enable_attachment_constraint self.model.soft_contact_margin = config.soft_contact_margin self.model.soft_contact_ke = config.soft_contact_ke self.model.soft_contact_kd = config.soft_contact_kd self.model.soft_contact_kf = config.soft_contact_kf self.model.soft_contact_mu = config.soft_contact_mu self.model.particle_ke = config.particle_ke self.model.particle_kd = config.particle_kd self.model.particle_kf = config.particle_kf self.model.particle_mu = config.particle_mu self.model.particle_cohesion = config.particle_cohesion self.model.particle_adhesion = config.particle_adhesion #self.integrator = wp.sim.SemiImplicitIntegrator() #intialize semi-implicit time-integrator self.integrator = wp.sim.XPBDIntegrator() #intialize semi-implicit time-integrator self.state_0 = self.model.state() #returns state object for model (holds all *time-varying* data for a model) self.state_1 = self.model.state() #i.e. body/particle positions and velocities if self.caching: self.renderer = wp.sim.render.SimRenderer(self.model, str(paths.usd), scaling=1.0) if self.sim_use_graph: self.create_graph() self.last_verts = None self.current_verts = wp.array.numpy(self.state_0.particle_q) def build_stage(self, config): builder = wp.sim.ModelBuilder(gravity=0.0) # --------------- Load body info ----------------- body_vertices, body_indices, body_faces = self.load_obj(self.paths.in_body_obj) body_seg = self.read_json(self.paths.body_seg) body_vertices = body_vertices * self.b_scale self.shift_y = self.get_shift_param(body_vertices) if self.shift_y: body_vertices[:, 1] = body_vertices[:, 1] + self.shift_y self.v_body = body_vertices self.f_body = body_faces self.body_indices = body_indices # -------------- Load cloth ------------ cloth_vertices, cloth_indices, cloth_faces = self.load_obj(self.paths.g_box_mesh) cloth_seg_dict = assign.read_segmentation(self.paths.g_mesh_segmentation) self.cloth_seg_dict = cloth_seg_dict stitching_vertices = cloth_seg_dict["stitch"] if 'stitch' in cloth_seg_dict.keys() else [] cloth_vertices = cloth_vertices * self.c_scale if self.shift_y: cloth_vertices[:, 1] = cloth_vertices[:, 1] + self.shift_y self.v_cloth_init = cloth_vertices self.f_cloth = cloth_faces #Load ground truth stitching lengths if not self.paths.g_orig_edge_len.exists(): orig_lens_dict = None print("no original length dict found") else: with open(self.paths.g_orig_edge_len, 'rb') as file: orig_lens_dict = pickle.load(file) cloth_pos = (0.0, 0.0, 0.0) cloth_rot = wp.quat_from_axis_angle(wp.vec3(0.0, 1.0, 0.0), wp.degrees(0.0)) #no rotation, but orientation of cloth in world space builder.add_cloth_mesh_sewing_spring( pos=cloth_pos, rot=cloth_rot, scale=1.0, vel=(0.0, 0.0, 0.0), vertices=cloth_vertices, indices=cloth_indices, resolution_scale=config.resolution_scale, orig_lens=orig_lens_dict, stitching_vertices=stitching_vertices, density=config.garment_density, edge_ke=config.garment_edge_ke, edge_kd=config.garment_edge_kd, tri_ke=config.garment_tri_ke, tri_ka=config.garment_tri_ka, tri_kd=config.garment_tri_kd, tri_drag=config.garment_tri_drag, tri_lift=config.garment_tri_lift, radius=config.garment_radius, add_springs=True, spring_ke=config.spring_ke, spring_kd=config.spring_kd, ) # ------------ Add a body ----------- if self.enable_body_smoothing: # Starts sim from smoothed-out body and slowly restores original details smoothing_total_smoothing_factor = config.smoothing_total_smoothing_factor smoothing_num_steps = config.smoothing_num_steps smoothing_recover_start_frame = config.smoothing_recover_start_frame smoothing_frame_gap_between_steps = config.smoothing_frame_gap_between_steps smoothing_step_size = smoothing_total_smoothing_factor / smoothing_num_steps self.body_smoothing_frames = [smoothing_recover_start_frame + smoothing_frame_gap_between_steps*i for i in range(smoothing_num_steps + 1)] self.body_smoothing_vertices_list = [] self.body_smoothing_vertices_list = implicit_laplacian_smoothing(body_vertices, body_indices.reshape(-1, 3), step_size=smoothing_step_size, iters=smoothing_num_steps) body_vertices = self.body_smoothing_vertices_list.pop() self.body_smoothing_frames.pop() self.body_indices = body_indices self.body_vertices_device_buffer = wp.array(body_vertices, dtype=wp.vec3, device=self.device) self.v_body = body_vertices self.body_mesh = wp.sim.Mesh(body_vertices, body_indices) body_pos = wp.vec3(0.0, 0, 0.0) body_rot = wp.quat_from_axis_angle(wp.vec3(0.0, 1.0, 0.0), wp.degrees(0.0)) # Cloth-body segemntation cloth_reference_labels, body_parts = assign.panel_assignment( cloth_seg_dict, cloth_vertices, cloth_indices, wp.transform(cloth_pos, cloth_rot), body_seg, body_vertices, body_indices, wp.transform(body_pos, body_rot), device=self.device, panel_init_labels=self._load_panel_labels(), strategy='closest', merge_two_legs=True, smpl_body=self.paths.use_smpl_seg ) face_filters, particle_filter = [], [] if config.enable_body_collision_filters: v_connectivity = self._build_vert_connectivity(cloth_vertices, cloth_indices) # Arm filter for the skirts face_filters.append(assign.create_face_filter( body_vertices, body_indices, body_seg, ['left_arm', 'right_arm', 'arms'], smpl_body=self.paths.use_smpl_seg)) particle_filter = assign.assign_face_filter_points( cloth_reference_labels, ['left_leg', 'right_leg', 'legs'], filter_id=0, vert_connectivity=v_connectivity ) # Overall filter that ignored internal geometry face_filters.append(assign.create_face_filter( body_vertices, body_indices, body_seg, ['face_internal'], smpl_body=self.paths.use_smpl_seg)) particle_filter = assign.assign_face_filter_points( cloth_reference_labels, ['body'], filter_id=1, vert_connectivity=v_connectivity, current_vertex_filter=particle_filter ) self.body_shape_index = 0 # Body is the first collider object to be added builder.add_shape_mesh( body=-1, mesh=self.body_mesh, pos=body_pos, rot=body_rot, scale=wp.vec3(1.0,1.0,1.0), #performed body scaling above thickness=config.body_thickness, mu=config.body_friction, face_filters=face_filters if face_filters else [[]], model_particle_filter_ids = particle_filter, ) # ----- Attachment constraint ------- if config.enable_attachment_constraint: self._add_attachment_labels(builder, config) # ----- Global collision resolution error ---- for part in body_parts: part_v, part_inds = assign.extract_submesh(body_vertices, body_indices, body_parts[part]) builder.add_cloth_reference_shape_mesh( mesh = wp.sim.Mesh(part_v, part_inds), name = part, pos = body_pos, rot = body_rot, scale = (1.0,1.0,1.0) #performed body scaling above ) # NOTE: has a side-effect of filling up model.particle_reference_label array self.body_parts_names2index = builder.add_cloth_reference_labels( cloth_reference_labels, [ # NOTE: Not adding drag between legs and the body as it's useless and contradicts attachment ['left_arm', 'body'], ['right_arm', 'body'], ['left_leg', 'right_leg'], ['left_arm', 'left_leg'], ['right_arm', 'left_leg'], ['left_arm', 'right_leg'], ['right_arm', 'right_leg'], ['left_arm', 'legs'], ['right_arm', 'legs'], ] ) # ------- Finalize -------------- self.model: wp.sim.Model = builder.finalize(device = self.device) #data is transferred to warp tensors, object used in simulation def _add_attachment_labels(self, builder, config): with open(self.paths.in_body_mes, 'r') as file: body_dict = yaml.load(file, Loader=yaml.SafeLoader)['body'] with open(self.paths.g_vert_labels, 'r') as f: vertex_labels = yaml.load(f, Loader=yaml.SafeLoader) lables_present = False for i, attach_label in enumerate(config.attachment_labels): if attach_label in vertex_labels.keys() and len(vertex_labels[attach_label]) > 0: constaint_verts = vertex_labels[attach_label] if attach_label == 'lower_interface': lables_present = True if '_waist_level' in body_dict: waist_level = body_dict['_waist_level'] else: waist_level = body_dict['height'] - body_dict['head_l'] - body_dict['waist_line'] builder.add_attachment( constaint_verts, wp.vec3(0, waist_level, 0), wp.vec3(0., 1., 0.), # Vertical attachment stiffness = config.attachment_stiffness[i], damping = config.attachment_damping[i] ) elif attach_label == 'right_collar': lables_present = True neck_w = body_dict['neck_w'] - 2 builder.add_attachment( constaint_verts, wp.vec3(-neck_w / 2, 0, 0), wp.vec3(1., 0., 0.), # Horizontal attachment stiffness = config.attachment_stiffness[i], damping = config.attachment_damping[i] ) elif attach_label == 'left_collar': lables_present = True neck_w = body_dict['neck_w'] - 2 builder.add_attachment( constaint_verts, wp.vec3(neck_w / 2, 0, 0), wp.vec3(-1., 0., 0.), # Horizontal attachment stiffness = config.attachment_stiffness[i], damping = config.attachment_damping[i] ) elif attach_label == 'strapless_top': lables_present = True # Attach under arm level = body_dict['height'] - body_dict['head_l'] - body_dict['armscye_depth'] builder.add_attachment( constaint_verts, wp.vec3(0, level, 0), wp.vec3(0., 1., 0.), # Vertical attachment stiffness = config.attachment_stiffness[i], damping = config.attachment_damping[i] ) else: print(f'{self.name}::WARNING::Requested attachment label {attach_label} ' 'is not supported. Skipped') continue print(f'Using attachment for {attach_label} with {len(constaint_verts)} vertices') if not lables_present: # Loaded garment is not labeled -- update config config.enable_attachment_constraint = False config.update_min_steps() print(f'{self.name}::WARNING::Requested attachment labels {config.attachment_labels} ' 'are not present. Attachment is turned off' ) def _load_panel_labels(self): pattern = BasicPattern(self.paths.g_specs) labels = {} for name, panel in pattern.pattern['panels'].items(): labels[name] = panel['label'] if 'label' in panel else '' return labels def _sim_frame_with_substeps(self): """Basic scheme for simulating a frame update""" wp.sim.collide(self.model, self.state_0, self.sim_dt * self.sim_substeps) # Generates contact points for the particles and rigid bodies # in the model, to be used in the contact dynamics kernel of the integrator # launches kernels for s in range(self.sim_substeps): self.state_0.clear_forces() # set particle and body forces to 0s self.integrator.simulate(self.model, self.state_0, self.state_1, self.sim_dt) # calculate semi-implicit Euler step # launches kernels and calculates new particle (and body) positions and velocities # swap states (self.state_0, self.state_1) = (self.state_1, self.state_0) # swap prev, new state def create_graph(self): # create update graph wp.capture_begin() # Captures all subsequent kernel launches and memory operations on CUDA devices. self._sim_frame_with_substeps() self.graph = wp.capture_end() # returns a handle to a CUDA graph object that can be launched with :func:`~warp.capture_launch()` # do not capture kernel launches anymore def update(self, frame): with wp.ScopedTimer("simulate", print=False, active=True): if self.model.enable_particle_particle_collisions: # FIXME: Produces cuda errors when activated together with "enable_cloth_reference_drag" # Reason is unknown. Or not? self.model.particle_grid.build(self.state_0.particle_q, self.model.particle_max_radius * 2.0) if frame == self.zero_gravity_steps: self.model.gravity = np.array((0.0, -9.81, 0.0)) if self.sim_use_graph: self.create_graph() if self.enable_body_smoothing and frame in self.body_smoothing_frames: self.update_smooth_body_shape() if self.sim_use_graph: self.create_graph() if (self.model.attachment_constraint and frame >= self.config.attachment_frames): self.model.attachment_constraint = False if self.sim_use_graph: self.create_graph() if self.sim_use_graph: #GPU wp.capture_launch(self.graph) else: #CPU: launch kernels without graph self._sim_frame_with_substeps() # Update vertices of last frame self.last_verts = self.current_verts # NOTE Makes a copy if particle_q device is not CPU self.current_verts = wp.array.numpy(self.state_0.particle_q) def update_smooth_body_shape(self): body_vertices = self.body_smoothing_vertices_list.pop() self.v_body = body_vertices wp.copy(self.body_vertices_device_buffer, wp.array(body_vertices, dtype=wp.vec3, device='cpu', copy=False)) # Apply new vertices and refit the sructures wp.launch( kernel=replace_mesh_points, dim = len(body_vertices), inputs=[self.body_mesh.mesh.id, self.body_vertices_device_buffer], device=self.device ) self.body_mesh.mesh.refit() #update render if self.caching: self.renderer.render_mesh( f'shape_{self.body_shape_index}', body_vertices, None, is_template=True, ) def render_usd_frame(self, is_live=False): with wp.ScopedTimer("render", print=False, active=True): start_time = 0.0 if is_live else self.usd_frame_time self.renderer.begin_frame(start_time) self.renderer.render(self.state_0) self.renderer.end_frame() self.usd_frame_time += 1.0 / self.sim_fps if not is_live: self.renderer.save() def run_frame(self): self.update(self.frame) # NOTE: USD Render if self.caching: self.render_usd_frame() def read_json(self, path): with open(path, 'r') as f: data = json.load(f) return data def load_obj(self, path): v, f = igl.read_triangle_mesh(str(path)) return v, f.flatten(), f def get_shift_param(self,body_vertices): v_body_arr = np.array(body_vertices) min_y = (min(v_body_arr[:, 1])) if min_y < 0: return abs(min_y) return 0.0 def calc_norm(self, a, b, c): """ This function calculates the norm based on the three points a, b, and c. Input: * self (BoxMesh object): Instance of BoxMesh class from which the function is called * a (ndarray): first point taking part in norm calculation * b (ndarray): second point taking part in norm calculation * c (ndarray): third point taking part in norm calculation Output: * n_normalized (bool): norm(a,b,c) with length 1 """ # Calculate the vectors AB and AC AB = np.array(b - a) AC = np.array(c - a) # Calculate the cross product of AB and AC n = np.cross(AB, AC) n_normalized = n / np.linalg.norm(n) return n_normalized def calc_vertex_norms(self): vertex_normals = np.zeros((len(self.v_cloth_init), 4)) for face in self.f_cloth: v0, v1, v2 = np.array(self.current_verts)[face] face_norm = list(self.calc_norm(v0, v1, v2)) temp_update = face_norm + [1] vertex_normals[face] += temp_update vertex_normals = vertex_normals[:, :3] / (vertex_normals[:, 3][:, np.newaxis]) return vertex_normals def save_frame(self, save_v_norms=False): """Save current garment state as an obj file, re-using all the information from boxmesh except for vertices and vertex normals (e.g. textures and faces) """ # NOTE: igl routine is not used here because it cannot write any extra info (e.g. texture coords) into obj # stores v, f, vf and vn # Save cloth with texture and normals if save_v_norms: vertex_normals = self.calc_vertex_norms() v_cloth_sim = self.current_verts # Store simulated cloth mesh # Read the boxmesh file with open(self.paths.g_box_mesh, 'r') as obj_file: lines = obj_file.readlines() # Modify the vertex positions and normals, if required with open(self.paths.g_sim, 'w') as obj_file: v_idx = 0 vn_idx = 0 for line in lines: if line.startswith('v '): new_vertex = v_cloth_sim[v_idx] obj_file.write(f'v {new_vertex[0]} {new_vertex[1]} {new_vertex[2]}\n') v_idx += 1 elif line.startswith('vn '): if save_v_norms: new_vertex = vertex_normals[vn_idx] obj_file.write(f'vn {new_vertex[0]} {new_vertex[1]} {new_vertex[2]}\n') vn_idx += 1 else: obj_file.write(line) def is_static(self): """ Checks whether garment is in the static equilibrium Compares current state with the last recorded state """ threshold = self.config.static_threshold non_static_percent = self.config.non_static_percent curr_verts_arr = self.current_verts last_verts_arr = self.last_verts if self.last_verts is None: # first iteration return False, len(curr_verts_arr) # Compare L1 norm per vertex # Checking vertices change is the same as checking if velocity is zero diff = np.abs(curr_verts_arr - last_verts_arr) diff_L1 = np.sum(diff, axis=1) non_static_len = len( diff_L1[diff_L1 > threshold]) # compare vertex-wise to allow accurate control over outliers if non_static_len == 0 or (non_static_len < len(curr_verts_arr) * 0.01 * non_static_percent): print('\nStatic with {} non-static vertices out of {}'.format(non_static_len, len(curr_verts_arr))) # Store last frame return True, non_static_len else: return False, non_static_len def count_self_intersections(self): model = self.model if model.particle_count and model.spring_count: model.particle_self_intersection_count.zero_() wp.launch( kernel=count_self_intersections, dim=model.spring_count, inputs=[ model.spring_indices, model.particle_shape.id, ], outputs=[ model.particle_self_intersection_count ], device=model.device, ) return int(wp.array.numpy(self.model.particle_self_intersection_count)[0]) else: return 0 def count_body_intersections(self): model = self.model if model.particle_count: model.body_cloth_intersection_count.zero_() wp.launch( kernel=count_body_cloth_intersections, dim=model.spring_count, inputs=[ model.spring_indices, model.particle_shape.id, model.shape_geo, self.body_shape_index ], outputs=[ model.body_cloth_intersection_count ], device=model.device, ) return int(wp.array.numpy(self.model.body_cloth_intersection_count)[0]) else: return 0 def _build_vert_connectivity(self, vertices, indices): vert_connectivity = [[] for _ in range(len(vertices))] for face_id in range(int(len(indices) / 3)): v1, v2, v3 = indices[face_id*3 + 0], indices[face_id*3 + 1], indices[face_id*3 + 2] vert_connectivity[v1].append(v2) vert_connectivity[v1].append(v3) vert_connectivity[v2].append(v1) vert_connectivity[v2].append(v3) vert_connectivity[v3].append(v1) vert_connectivity[v3].append(v2) return vert_connectivity