""" Module contains classes needed to simulate garments from patterns in Maya. """ # Basic from functools import partial import copy import errno import numpy as np import os import time from importlib import reload # Maya from maya import cmds from maya import OpenMaya # Arnold import mtoa.utils as mutils from mtoa.cmds.arnoldRender import arnoldRender import mtoa.core # My modules import pygarment.pattern.core as core import pygarment.pattern.wrappers as wrappers from pygarment.pattern.utils import vector_angle, rel_to_abs_2d from pygarment.mayaqltools import qualothwrapper as qw from pygarment.mayaqltools import utils reload(core) reload(wrappers) reload(qw) reload(utils) class PatternLoadingError(BaseException): """To be rised when a pattern cannot be loaded correctly to 3D""" pass class MayaGarment(wrappers.VisPattern): """ Extends a pattern specification in custom JSON format to work with Maya Input: * Pattern template in custom JSON format * import panel to Maya scene TODO * cleaning imported stuff TODO * Basic operations on panels in Maya TODO """ def __init__(self, pattern_file, clean_on_die=False): super(MayaGarment, self).__init__(pattern_file) self.self_clean = clean_on_die self.last_verts = None self.current_verts = None self.loaded_to_maya = False self.obstacles = [] self.shader_group = None self.MayaObjects = {} self.config = { 'material': {}, 'body_friction': 0.5, 'resolution_scale': 5 } def __del__(self): """Remove Maya objects when dying""" if self.self_clean: self.clean(True) # ------ Basic operations ------ def load(self, obstacles=[], shader_group=None, config={}, parent_group=None): """ Loads current pattern to Maya as simulatable garment. If already loaded, cleans previous geometry & reloads config should contain info on fabric matereials & body_friction (collider friction) if provided """ if self.is_self_intersecting(): # supplied pattern with self-intersecting panels -- it's likely to crash Maya raise PatternLoadingError('{}::{}::Provided pattern has self-intersecting panels. Nothing is loaded'.format( self.__class__.__name__, self.name)) if self.loaded_to_maya: # save the latest sim info self.fetchSimProps() self.clean(True) # Normal flow produces garbage warnings of parenting from Maya. Solution suggestion didn't work, so I just live with them self.load_panels(parent_group) self.stitch_panels() self.loaded_to_maya = True self.setShaderGroup(shader_group) self.add_colliders(obstacles) self._setSimProps(config) # should be done on the mesh after stitching, res adjustment, but before sim & clean-up self._eval_vertex_segmentation() # remove the junk after garment was stitched and labeled self._clean_mesh() print('Garment ' + self.name + ' is loaded to Maya') def load_panels(self, parent_group=None): """Load panels to Maya as curve collection & geometry objects. Groups them by panel and by pattern""" # top group group_name = cmds.group(em=True, n=self.name) # emrty at first if parent_group is not None: group_name = cmds.parent(group_name, parent_group) self.MayaObjects['pattern'] = group_name # Load panels as curves self.MayaObjects['panels'] = {} for panel_name in self.pattern['panels']: panel_maya = self._load_panel(panel_name, group_name) def stitch_panels(self): """ Create seams between qualoth panels. Assumes that panels are already loadeded (as curves). Assumes that after stitching every pattern becomes a single piece of geometry Returns Qulaoth cloth object name """ self.MayaObjects['stitches'] = [] for stitch in self.pattern['stitches']: stitch_id = qw.qlCreateSeam( self._maya_curve_name(stitch[0]), self._maya_curve_name(stitch[1])) stitch_id = cmds.parent(stitch_id, self.MayaObjects['pattern']) # organization self.MayaObjects['stitches'].append(stitch_id[0]) # after stitching, only one cloth\cloth shape object per pattern is left -- move up the hierarechy children = cmds.listRelatives(self.MayaObjects['pattern'], ad=True) cloths = [obj for obj in children if 'qlCloth' in obj] cmds.parent(cloths, self.MayaObjects['pattern']) def setShaderGroup(self, shader_group=None): """ Sets material properties for the cloth object created from current panel """ if not self.loaded_to_maya: raise RuntimeError( 'MayaGarmentError::Pattern is not yet loaded. Cannot set shader') if shader_group is not None: # use previous othervise self.shader_group = shader_group if self.shader_group is not None: cmds.sets(self.get_qlcloth_geomentry(), forceElement=self.shader_group) def save_mesh(self, folder='', tag='sim'): """ Saves cloth as obj file and its per vertex segmentation to a given folder or to the folder with the pattern if not given. """ if not self.loaded_to_maya: print('MayaGarmentWARNING::Pattern is not yet loaded. Nothing saved') return if folder: filepath = folder else: filepath = self.path self._save_to_path(filepath, self.name + '_' + tag) def sim_caching(self, caching=True): """Toggles the caching of simulation steps to garment folder""" if caching: # create folder self.cache_path = os.path.join(self.path, self.name + '_simcache') try: os.makedirs(self.cache_path) except OSError as exc: if exc.errno != errno.EEXIST: # ok if directory exists raise pass else: # disable caching self.cache_path = '' def clean(self, delete=False): """ Hides/removes the garment from Maya scene NOTE all of the maya ids assosiated with the garment become invalidated, if delete flag is True """ if self.loaded_to_maya: # Remove from simulation cmds.setAttr(self.get_qlcloth_props_obj() + '.active', 0) if delete: print('MayaGarment::Deleting {}'.format(self.MayaObjects['pattern'])) # Clean solver cache properly solver = qw.findSolver() if solver: qw.qlReinitSolver(self.get_qlcloth_props_obj(), solver) cmds.delete(self.MayaObjects['pattern']) qw.deleteSolver() if 'segm_node' in self.MayaObjects and self.MayaObjects['segm_node']: cmds.delete(self.MayaObjects['segm_node']) self.loaded_to_maya = False self.MayaObjects = {} # clean else: cmds.hide(self.MayaObjects['pattern']) # do nothing if not loaded -- already clean =) def display_vertex_segmentation(self, shader): """ Color every vertes of the garment according to the panel is belongs to (as indicated in self.vertex_labels) """ # group vertices by label (it's faster then coloring one-by-one) vertex_select_lists = dict.fromkeys(self.panel_order() + ['stitch', 'Other']) for key in vertex_select_lists: vertex_select_lists[key] = [] for vert_idx in range(len(self.current_verts)): str_label = self.vertex_labels[vert_idx] if str_label not in self.panel_order() and str_label != 'stitch': str_label = 'Other' vert_addr = '{}.vtx[{}]'.format(self.get_qlcloth_geomentry(), vert_idx) vertex_select_lists[str_label].append(vert_addr) # Contrasting Panel Coloring for visualization # https://www.schemecolor.com/bright-rainbow-colors.php # color_hex = ['FF0900', 'FF7F00', 'FFEF00', '00F11D', '0079FF', 'A800FF'] # color_hex = ['FCE6DB', 'FADBD4', 'F5C4C7', 'EDB2C9', 'B7A9CC', 'E6CCE8'] # teenage fever + 0.65 #color_hex = ['FAD2FC', 'CCCCFC', 'E0ECFF', 'FFFEED', 'FFEBC9', 'FFC4CD'] # love you mom # color_hex = ['F6D9E2', 'CACADD', 'FEEDE4', 'F8D6CB', 'D2C7C2'] # Winter season #color_hex = ['95DBD9', '8CB1D8', 'A186BD', 'E698C2', 'FBE7A1'] # Love and hope # color_hex = ['FCEFE3', 'C0C8D5', '9DA9C4', '72638F', 'BA869F', 'D1ABB6'] # Pastel Lust + 0.85 color_hex = ['8594AB', '9FBBBA', 'FACDA7', 'EDA19D', 'CF8299', '8B6B96', 'A4DE87'] # CRAZY SITUATION + 0.85 https://www.schemecolor.com/crazy-situation.php color_list = np.empty((len(color_hex), 3)) for idx in range(len(color_hex)): color_list[idx] = np.array([int(color_hex[idx][i:i + 2], 16) for i in (0, 2, 4)]) / 255.0 start_time = time.time() for label, str_label in enumerate(vertex_select_lists.keys()): if len(vertex_select_lists[str_label]) > 0: # 'Other' may not be present at all if str_label == 'Other': # non-segmented becomes white color = np.ones(3) elif str_label == 'stitch': # stitches are black color = np.zeros(3) else: # color selection with expansion if the list is too small factor, color_id = (label // len(color_list)) + 1, label % len(color_list) color = color_list[color_id] * 0.9 # / factor # gets darker the more labels there are # color corresponding vertices cmds.select(clear=True) cmds.select(vertex_select_lists[str_label]) cmds.polyColorPerVertex(rgb=color.tolist()) cmds.select(clear=True) cmds.setAttr(self.get_qlcloth_geomentry() + '.displayColors', 1) cmds.setAttr(self.get_qlcloth_geomentry() + '.aiExportColors', 1) # Add a color node to allow rendering in Arnold cmds.select(self.get_qlcloth_geomentry()) color_set = cmds.polyColorSet( query=True, currentColorSet=True ) data_node = cmds.createNode('aiUserDataColor') self.MayaObjects['segm_node'] = data_node # Connect with shader color = cmds.getAttr(shader + '.color')[0] cmds.setAttr((data_node + '.default'), color[0], color[1], color[2], type='double3') print('INFO::Segmentation Color Set!') cmds.connectAttr((data_node + '.outColor'), (shader + '.color')) # Assume Lambert shader cmds.setAttr((data_node + '.attribute'), color_set[0], type="string") cmds.refresh() # ------ Simulation ------ def add_colliders(self, obstacles=[]): """ Adds given Maya objects as colliders of the garment """ if not self.loaded_to_maya: raise RuntimeError( 'MayaGarmentError::Pattern is not yet loaded. Cannot load colliders') if obstacles: # if not given, use previous ones self.obstacles = obstacles if 'colliders' not in self.MayaObjects: self.MayaObjects['colliders'] = [] for obj in self.obstacles: collider = qw.qlCreateCollider( self.get_qlcloth_geomentry(), obj ) # apply current friction settings qw.setColliderFriction(collider, self.config['body_friction']) # organize object tree collider = cmds.parent(collider, self.MayaObjects['pattern']) self.MayaObjects['colliders'].append(collider) def fetchSimProps(self): """Fetch garment material & body friction from Maya settings""" if not self.loaded_to_maya: raise RuntimeError('MayaGarmentError::Pattern is not yet loaded.') self.config['material'] = qw.fetchFabricProps(self.get_qlcloth_props_obj()) if 'colliders' in self.MayaObjects and self.MayaObjects['colliders']: # assuming all colliders have the same value friction = qw.fetchColliderFriction(self.MayaObjects['colliders'][0]) if friction: self.config['body_friction'] = friction self.config['collision_thickness'] = cmds.getAttr(self.get_qlcloth_props_obj() + '.thickness') # take resolution scale from any of the panels assuming all the same self.config['resolution_scale'] = qw.fetchPanelResolution() return self.config def update_verts_info(self): """ Retrieves current vertex positions from Maya & updates the last state. For best performance, should be called on each iteration of simulation Assumes the object is already loaded & stitched """ if not self.loaded_to_maya: raise RuntimeError( 'MayaGarmentError::Pattern is not yet loaded. Cannot update verts info') # working with meshes http://www.fevrierdorian.com/blog/post/2011/09/27/Quickly-retrieve-vertex-positions-of-a-Maya-mesh-%28English-Translation%29 cloth_dag = self.get_qlcloth_geom_dag() mesh = OpenMaya.MFnMesh(cloth_dag) vertices = utils.get_vertices_np(mesh) self.last_verts = self.current_verts self.current_verts = vertices def cache_if_enabled(self, frame): """If caching is enabled -> saves current geometry to cache folder Does nothing otherwise """ if not self.loaded_to_maya: print('MayaGarmentWARNING::Pattern is not yet loaded. Nothing cached') return if hasattr(self, 'cache_path') and self.cache_path: self._save_to_path(self.cache_path, self.name + '_{:04d}'.format(frame)) # ------ Qualoth objects ------ def get_qlcloth_geomentry(self): """ Find the first Qualoth cloth geometry object belonging to current pattern """ if not self.loaded_to_maya: raise RuntimeError('MayaGarmentError::Pattern is not yet loaded.') if 'qlClothOut' not in self.MayaObjects: children = cmds.listRelatives(self.MayaObjects['pattern'], ad=True) cloths = [obj for obj in children if 'qlCloth' in obj and 'Out' in obj and 'Shape' not in obj] self.MayaObjects['qlClothOut'] = cloths[0] return self.MayaObjects['qlClothOut'] def get_qlcloth_props_obj(self): """ Find the first qlCloth object belonging to current pattern """ if not self.loaded_to_maya: raise RuntimeError('MayaGarmentError::Pattern is not yet loaded.') if 'qlCloth' not in self.MayaObjects: children = cmds.listRelatives(self.MayaObjects['pattern'], ad=True) cloths = [obj for obj in children if 'qlCloth' in obj and 'Out' not in obj and 'Shape' in obj] self.MayaObjects['qlCloth'] = cloths[0] return self.MayaObjects['qlCloth'] def get_qlcloth_geom_dag(self): """ returns DAG reference to cloth shape object """ if not self.loaded_to_maya: raise RuntimeError('MayaGarmentError::Pattern is not yet loaded.') if 'shapeDAG' not in self.MayaObjects: self.MayaObjects['shapeDAG'] = utils.get_dag(self.get_qlcloth_geomentry()) return self.MayaObjects['shapeDAG'] # ------ Geometry Checks ------ def is_static(self, threshold, allowed_non_static_percent=0): """ Checks wether garment is in the static equilibrium Compares current state with the last recorded state """ if not self.loaded_to_maya: raise RuntimeError( 'MayaGarmentError::Pattern is not yet loaded. Cannot check static') if self.last_verts is None: # first iteration return False # Compare L1 norm per vertex # Checking vertices change is the same as checking if velocity is zero diff = np.abs(self.current_verts - self.last_verts) diff_L1 = np.sum(diff, axis=1) non_static_len = len(diff_L1[diff_L1 > threshold]) # compare vertex-wize to allow accurate control over outliers if non_static_len == 0 or non_static_len < len(self.current_verts) * 0.01 * allowed_non_static_percent: print('\nStatic with {} non-static vertices out of {}'.format(non_static_len, len(self.current_verts))) return True, non_static_len else: return False, non_static_len def intersect_colliders_3D(self, obstacles=[]): """Checks wheter garment intersects given obstacles or its colliders if obstacles are not given Returns True if intersections found Having intersections may disrupt simulation result although it seems to recover from some of those """ if not self.loaded_to_maya: raise RuntimeError('Garment is not yet loaded: cannot check for intersections') if not obstacles: obstacles = self.obstacles print('Garment::3D Penetration checks') # check intersection with colliders for obj in obstacles: intersecting = self._intersect_object(obj) if intersecting: return True return False def self_intersect_3D(self, verbose=False): """Checks wheter currently loaded garment geometry intersects itself Unline boolOp, check is non-invasive and do not require garment reload or copy. Having intersections may disrupt simulation result although it seems to recover from some of those """ if not self.loaded_to_maya: raise RuntimeError( 'MayaGarmentError::Pattern is not yet loaded. Cannot check geometry self-intersection') # It turns out that OpenMaya python reference has nothing to do with reality of passing argument: # most of the functions I use below are to be treated as wrappers of c++ API # https://help.autodesk.com/view/MAYAUL/2018//ENU/?guid=__cpp_ref_class_m_fn_mesh_html mesh, cloth_dag = utils.get_mesh_dag(self.get_qlcloth_geomentry()) vertices = OpenMaya.MPointArray() mesh.getPoints(vertices, OpenMaya.MSpace.kWorld) # use ray intersect with all edges of current mesh & the mesh itself num_edges = mesh.numEdges() accelerator = mesh.autoUniformGridParams() num_hits = 0 for edge_id in range(num_edges): # Vertices that comprise an edge vtx1, vtx2 = utils.edge_vert_ids(mesh, edge_id) # test intersection raySource = OpenMaya.MFloatPoint(vertices[vtx1]) rayDir = OpenMaya.MFloatVector(vertices[vtx2] - vertices[vtx1]) hit, hitFaces, hitPoints, _ = utils.test_ray_intersect(mesh, raySource, rayDir, accelerator, return_info=True) if not hit: continue # Since edge is on the mesh, we have tons of false hits # => check if current edge is adjusent to hit faces: if shares a vertex for face_id in range(hitFaces.length()): face_verts = OpenMaya.MIntArray() mesh.getPolygonVertices(hitFaces[face_id], face_verts) face_verts = [face_verts[j] for j in range(face_verts.length())] if vtx1 not in face_verts and vtx2 not in face_verts: # hit face is not adjacent to the edge => real hit if verbose: print('Hit point: {}, {}, {}'.format(hitPoints[face_id][0], hitPoints[face_id][1], hitPoints[face_id][2])) num_hits += 1 if num_hits == 0: # no intersections -- no need for threshold check print('{} is not self-intersecting'.format(self.name)) return False if ('self_intersect_hit_threshold' in self.config and num_hits > self.config['self_intersect_hit_threshold'] or num_hits > 0 and 'self_intersect_hit_threshold' not in self.config): # non-zero hit if no threshold provided print('{} is self-intersecting with {} intersect edges -- above threshold {}'.format( self.name, num_hits, self.config['self_intersect_hit_threshold'] if 'self_intersect_hit_threshold' in self.config else 0)) return True else: print('{} is self-intersecting with {} intersect edges -- ignored by threshold {}'.format( self.name, num_hits, self.config['self_intersect_hit_threshold'] if 'self_intersect_hit_threshold' in self.config else 0)) # no need to reload -- non-invasive checks return False # ------ ~Private ------- def _load_panel(self, panel_name, pattern_group=None): """ Loads curves constituting given panel to Maya. Goups them per panel """ panel = self.pattern['panels'][panel_name] vertices = np.asarray(panel['vertices']) self.MayaObjects['panels'][panel_name] = {} self.MayaObjects['panels'][panel_name]['edges'] = [] # top panel group panel_group = cmds.group(n=panel_name, em=True) if pattern_group is not None: panel_group = cmds.parent(panel_group, pattern_group)[0] self.MayaObjects['panels'][panel_name]['group'] = panel_group # draw edges curve_names = [] for edge in panel['edges']: if ('curvature' not in edge or isinstance(edge['curvature'], list) or edge['curvature']['type'] != 'circle'): # NOTE Legacy curvature representation curve_points = self._edge_as_3d_tuple_list(edge, vertices) curve = cmds.curve(p=curve_points, d=min(len(curve_points) - 1, 3)) else: curve = self._draw_circle_arc(edge, vertices) curve_names.append(curve) self.MayaObjects['panels'][panel_name]['edges'].append(curve) # Group curve_group = cmds.group(curve_names, n=panel_name + '_curves') curve_group = cmds.parent(curve_group, panel_group)[0] self.MayaObjects['panels'][panel_name]['curve_group'] = curve_group # 3D placemement self._apply_panel_3d_placement(panel_name) # Create geometry panel_geom = qw.qlCreatePattern(curve_group) # take out the solver node -- created only once per scene, no need to store solvers = [obj for obj in panel_geom if 'Solver' in obj] panel_geom = list(set(panel_geom) - set(solvers)) panel_geom = cmds.parent(panel_geom, panel_group) # organize # TODO Compare the norm of loaded pattern with our custom # evaluated norm (see GarmentCode) and flip if different pattern_object = [node for node in panel_geom if 'Pattern' in node] self.MayaObjects['panels'][panel_name]['qlPattern'] = ( pattern_object[0] if panel_group in pattern_object[0] else panel_group + '|' + pattern_object[0] ) return panel_group def _setSimProps(self, config={}): """Pass material properties for cloth & colliders to Qualoth""" if not self.loaded_to_maya: raise RuntimeError('MayaGarmentError::Pattern is not yet loaded.') if config: self.config = config qw.setFabricProps( self.get_qlcloth_props_obj(), self.config['material'] ) if 'colliders' in self.MayaObjects: for collider in self.MayaObjects['colliders']: qw.setColliderFriction(collider, self.config['body_friction']) if 'collision_thickness' in self.config: # if not provided, use default auto-calculated value cmds.setAttr(self.get_qlcloth_props_obj() + '.overrideThickness', 1) cmds.setAttr(self.get_qlcloth_props_obj() + '.thickness', self.config['collision_thickness']) # update resolution properties qw.setPanelsResolution(self.config['resolution_scale']) def _eval_vertex_segmentation(self): """ Evalute which vertex belongs to which panel NOTE: only applicable to the mesh that was JUST loaded and stitched -- Before the mesh was cleaned up (because the info from Qualoth is dependent on original topology) -- before the sim started (need planarity checks) Hence fuction is only called once on garment load NOTE: if garment resolution was changed from Maya tools, the segmentation is not guranteed to be consistent with the change, (reload garment geometry to get correct segmentation) """ if not self.loaded_to_maya: raise RuntimeError('Garment should be loaded when evaluating vertex segmentation') self.update_verts_info() self.vertex_labels = [None] * len(self.current_verts) # -- Stitches (provided in qualoth objects directly) --- on_stitches = self._verts_on_stitches() # TODOLOW I can even distinguish stitches from each other! for idx in on_stitches: self.vertex_labels[idx] = 'stitch' # --- vertices --- vertices = self.current_verts # BBoxes give fast results for most vertices bboxes = self._all_panel_bboxes() vertices_multi_match = [] for i in range(len(vertices)): if i in on_stitches: # already labeled continue vertex = vertices[i] # check which panel is the closest one in_bboxes = [] for panel in bboxes: if self._point_in_bbox(vertex, bboxes[panel]): in_bboxes.append(panel) if len(in_bboxes) == 1: self.vertex_labels[i] = in_bboxes[0] else: # multiple or zero matches -- handle later vertices_multi_match.append((i, in_bboxes)) # eval for confusing cases neighbour_checks = 0 while len(vertices_multi_match) > 0: unlabeled_vert_id, matched_panels = vertices_multi_match.pop(0) # check if vert in on the plane of any of the panels on_panel_planes = [] for panel in matched_panels: if self._point_on_plane(vertices[unlabeled_vert_id], panel): on_panel_planes.append(panel) # plane might not be the only option if len(on_panel_planes) == 1: # found! self.vertex_labels[unlabeled_vert_id] = on_panel_planes[0] else: # by this time, many vertices already have labels, so let's just borrow from neigbours neighbors = self._get_vert_neighbours(unlabeled_vert_id) neighbour_checks += 1 if len(neighbors) == 0: # print('Skipped Vertex {} with zero neigbors'.format(unlabeled_vert_id)) continue unlabelled = [unl[0] for unl in vertices_multi_match] # check only labeled neigbors that are not on stitches neighbors = [vert_id for vert_id in neighbors if vert_id not in unlabelled and vert_id not in on_stitches] if len(neighbors) > 0: neighbour_labels = [self.vertex_labels[vert_id] for vert_id in neighbors] # https://www.geeksforgeeks.org/python-find-most-frequent-element-in-a-list frequent_label = max(set(neighbour_labels), key=neighbour_labels.count) self.vertex_labels[unlabeled_vert_id] = frequent_label else: # put back # NOTE! There is a ponetial for infinite loop here, but it shoulf not occur # if the garment is freshly loaded before sim print('Garment::Labelling::vertex {} needs revisit'.format(unlabeled_vert_id)) vertices_multi_match.append((unlabeled_vert_id, on_panel_planes)) def _clean_mesh(self): """ Clean mesh from incosistencies introduces by stitching, and update vertex-dependednt info accordingly """ # remove the junk after garment was stitched and labeled cmds.polyClean(self.get_qlcloth_geomentry()) # fix labeling self.update_verts_info() match_verts = utils.match_vert_lists(self.current_verts, self.last_verts) self.vertex_labels = [self.vertex_labels[i] for i in match_verts] def _edge_as_3d_tuple_list(self, edge, vertices): """ Represents given edge object as list of control points suitable for drawing in Maya """ points = vertices[edge['endpoints'], :] # NOTE: Legacy curvature representation if 'curvature' in edge: if isinstance(edge['curvature'], list): abs_points = rel_to_abs_2d( points[0], points[1], edge['curvature'] ) abs_points = [abs_points] points = np.r_[ [points[0]], abs_points, [points[1]] ] elif edge['curvature']['type'] == 'cubic' or edge['curvature']['type'] == 'quadratic': abs_points = [] for p in edge['curvature']['params']: abs_points.append(rel_to_abs_2d( points[0], points[1], p)) points = np.r_[ [points[0]], abs_points, [points[1]] ] else: pass # Ignore for circle arcs # to 3D points = np.c_[points, np.zeros(len(points))] return list(map(tuple, points)) def _draw_circle_arc(self, edge, vertices, resolution=20): """Draw a circle arc as specified by an edge""" radius, large_arc, right = edge['curvature']['params'] edge_3d = np.array(self._edge_as_3d_tuple_list(edge, vertices)) angle = 2 * np.arcsin(np.linalg.norm(edge_3d[1] - edge_3d[0]) / radius / 2) if large_arc: angle = 2 * np.pi - angle circle_name = cmds.circle( radius=radius, sections=resolution, sweep=np.rad2deg(angle)) # Move the circle arc to a new position circle_center = np.array([0, 0, 0]) # default arc placement in Maya top_point = np.array([0, radius, 0]) sec_point = np.array([- radius * np.sin(angle), radius * np.cos(angle), 0]) if not right: top_point, sec_point = sec_point, top_point # translation new_arc_position = edge_3d[0] shift = (new_arc_position - top_point).tolist() # Rotation curr_dir = sec_point - top_point target_dir = edge_3d[1] - edge_3d[0] rot_angle = np.rad2deg(vector_angle(curr_dir[:2], target_dir[:2])) if not right: rot_angle = rot_angle - 360 cmds.move(shift[0], shift[1], shift[2], circle_name[0], relative=True) cmds.rotate( 0, 0, rot_angle, circle_name[0], relative=True, pivot=new_arc_position.tolist()) return circle_name[0] def _applyEuler(self, vector, eulerRot): """Applies Euler angles (in degrees) to provided 3D vector""" # https://www.cs.utexas.edu/~theshark/courses/cs354/lectures/cs354-14.pdf eulerRot_rad = np.deg2rad(eulerRot) # X vector_x = np.copy(vector) vector_x[1] = vector[1] * np.cos(eulerRot_rad[0]) - vector[2] * np.sin(eulerRot_rad[0]) vector_x[2] = vector[1] * np.sin(eulerRot_rad[0]) + vector[2] * np.cos(eulerRot_rad[0]) # Y vector_y = np.copy(vector_x) vector_y[0] = vector_x[0] * np.cos(eulerRot_rad[1]) + vector_x[2] * np.sin(eulerRot_rad[1]) vector_y[2] = -vector_x[0] * np.sin(eulerRot_rad[1]) + vector_x[2] * np.cos(eulerRot_rad[1]) # Z vector_z = np.copy(vector_y) vector_z[0] = vector_y[0] * np.cos(eulerRot_rad[2]) - vector_y[1] * np.sin(eulerRot_rad[2]) vector_z[1] = vector_y[0] * np.sin(eulerRot_rad[2]) + vector_y[1] * np.cos(eulerRot_rad[2]) return vector_z def _set_panel_3D_attr(self, panel_dict, panel_group, attribute, maya_attr): """Set recuested attribute to value from the spec""" if attribute in panel_dict: values = panel_dict[attribute] else: values = [0, 0, 0] cmds.setAttr( panel_group + '.' + maya_attr, values[0], values[1], values[2], type='double3') def _apply_panel_3d_placement(self, panel_name): """Apply transform from spec to given panel""" panel = self.pattern['panels'][panel_name] panel_group = self.MayaObjects['panels'][panel_name]['curve_group'] # set pivot to origin relative to currently loaded curves cmds.xform(panel_group, pivots=[0, 0, 0], worldSpace=True) # now place correctly self._set_panel_3D_attr(panel, panel_group, 'translation', 'translate') self._set_panel_3D_attr(panel, panel_group, 'rotation', 'rotate') def _maya_curve_name(self, address): """ Shortcut to retrieve the name of curve corresponding to the edge""" panel_name = address['panel'] edge_id = address['edge'] return self.MayaObjects['panels'][panel_name]['edges'][edge_id] def _save_to_path(self, path, filename): """Save current state of cloth object to given path with given filename""" # geometry filepath = os.path.join(path, filename + '.obj') utils.save_mesh(self.get_qlcloth_geomentry(), filepath) # segmentation filepath = os.path.join(path, filename + '_segmentation.txt') with open(filepath, 'w') as f: for panel_name in self.vertex_labels: f.write("%s\n" % panel_name) # eval num_verts = cmds.polyEvaluate(self.get_qlcloth_geomentry(), v=True) if num_verts != len(self.vertex_labels): print('MayaGarment::WARNING::Segmentation list does not match mesh topology in save {}'.format(self.name)) def _intersect_object(self, geometry): """Check if given object intersects current cloth geometry Function does not have side-effects on input geometry""" # ray-based intersection test cloth_mesh, cloth_dag = utils.get_mesh_dag(self.get_qlcloth_geomentry()) obstacle_mesh, obstacle_dag = utils.get_mesh_dag(geometry) # use obstacle verts as a base for testing # Assuming that the obstacle geometry has a lower resolution then the garment obs_vertices = OpenMaya.MPointArray() obstacle_mesh.getPoints(obs_vertices, OpenMaya.MSpace.kWorld) # use ray intersect of all edges of obstacle mesh with the garment mesh num_edges = obstacle_mesh.numEdges() accelerator = cloth_mesh.autoUniformGridParams() hit_border_length = 0 # those are edges along the border of intersecting area on the geometry for edge_id in range(num_edges): # Vertices that comprise an edge vtx1, vtx2 = utils.edge_vert_ids(obstacle_mesh, edge_id) # test intersection raySource = OpenMaya.MFloatPoint(obs_vertices[vtx1]) rayDir = OpenMaya.MFloatVector(obs_vertices[vtx2] - obs_vertices[vtx1]) hit = utils.test_ray_intersect(cloth_mesh, raySource, rayDir, accelerator) if hit: # A very naive approximation of total border length of areas of intersection hit_border_length += rayDir.length() if hit_border_length < 1e-5: # no intersections -- no need for threshold check print('{} with {} do not intersect'.format(geometry, self.name)) return False if ('object_intersect_border_threshold' in self.config and hit_border_length > self.config['object_intersect_border_threshold'] or (hit_border_length > 1e-5 and 'object_intersect_border_threshold' not in self.config)): # non-zero hit if no threshold provided print('{} with {} intersect::Approximate intersection border length {:.2f} cm is above threshold {:.2f} cm'.format( geometry, self.name, hit_border_length, self.config['object_intersect_border_threshold'] if 'object_intersect_border_threshold' in self.config else 0)) return True print('{} with {} intersect::Approximate intersection border length {:.2f} cm is ignored by threshold {:.2f} cm'.format( geometry, self.name, hit_border_length, self.config['object_intersect_border_threshold'] if 'object_intersect_border_threshold' in self.config else 0)) return False def _verts_on_stitches(self): """ List all the vertices in garment mesh located on stitches NOTE: it does not output vertices correctly on is the mesh topology was changed (e.g. after cmds.polyClean())!! """ on_stitches = [] for stitch in self.pattern['stitches']: # querying one side is enough since they share the vertices for side in [0, 1]: stitch_curve = self._maya_curve_name(stitch[side]) panel_name = stitch[side]['panel'] panel_node = self.MayaObjects['panels'][panel_name]['qlPattern'] verts_on_curve = qw.getVertsOnCurve(panel_node, stitch_curve) on_stitches += verts_on_curve return on_stitches def _verts_on_curves(self): """ List all the vertices of garment mesh that are located on panel curves """ all_edge_verts = [] for panel in self.panel_order(): panel_info = self.MayaObjects['panels'][panel] panel_node = panel_info['qlPattern'] # curves for curve in panel_info['edges']: verts_on_curve = qw.getVertsOnCurve(panel_node, curve) all_edge_verts += verts_on_curve print(min(verts_on_curve), max(verts_on_curve)) # might contain duplicates return all_edge_verts def _all_panel_bboxes(self): """ Evaluate 3D bounding boxes for all panels (as original curve loops) """ panel_curves = self.MayaObjects['panels'] bboxes = {} for panel in panel_curves: box = cmds.exactWorldBoundingBox(panel_curves[panel]['curve_group']) bboxes[panel] = box return bboxes @staticmethod def _point_in_bbox(point, bbox, tol=0.01): """ Check if point is within bbox bbbox given in maya format (float[] xmin, ymin, zmin, xmax, ymax, zmax.) NOTE: tol value is needed for cases when BBox collapses to 2D """ if (point[0] < (bbox[0] - tol) or point[0] > (bbox[3] + tol) or point[1] < (bbox[1] - tol) or point[1] > (bbox[4] + tol) or point[2] < (bbox[2] - tol) or point[2] > (bbox[5] + tol)): return False return True def _point_on_plane(self, point, panel, tol=0.001): """ Check if a point belongs to the same plane as given in the curve group """ # I could check by panel rotation and translation!! rot = self.pattern['panels'][panel]['rotation'] transl = np.array(self.pattern['panels'][panel]['translation']) # default panel normal upon load, sign doesn't matter here normal = np.array([0., 0., 1.]) rotated_normal = self._applyEuler(normal, rot) dot_prod = np.dot(np.array(point) - transl, rotated_normal) return np.isclose(dot_prod, 0., atol=tol) def _point_in_curve(self, point, curve_group, tol=0.01): """ Check if a point is inside a given closed curve region Assuming that the point is roughly in the same plane as the curve """ # closed curve mid-point # shoot a ray (new linear curve) and check if it intersects with any of pass def _get_vert_neighbours(self, vert_id): """ List the neigbours of given vertex in current cloth mesh """ mesh_name = self.get_qlcloth_geomentry() edges = cmds.polyListComponentConversion( mesh_name + '.vtx[%d]' % vert_id, fromVertex=True, toEdge=True) neighbors = [] for edge in edges: neighbor_verts_str = cmds.polyListComponentConversion(edge, toVertex=True) for neighbor_str in neighbor_verts_str: values = neighbor_str.split(']')[0].split('[')[-1] if ':' in values: neighbors += [int(x) for x in values.split(':')] else: neighbors.append(int(values)) return list(set(neighbors)) # leave only unique def _panel_to_id(self, panel): """ Panel label as integer given the name of the panel """ return int(self.panel_order().index(panel) + 1) class MayaGarmentWithUI(MayaGarment): """Extension of MayaGarment that can generate GUI for controlling the pattern""" def __init__(self, pattern_file, clean_on_die=False): super(MayaGarmentWithUI, self).__init__(pattern_file, clean_on_die) self.ui_top_layout = None self.ui_controls = {} def __del__(self): super(MayaGarmentWithUI, self).__del__() # looks like UI now contains links to garment instance (callbacks, most probably) # If destructor is called, the UI is already clean # if self.ui_top_layout is not None: # self._clean_layout(self.ui_top_layout) # ------- UI Drawing routines -------- def drawUI(self, top_layout=None): """ Draw pattern controls in the given layout For correct connection with Maya attributes, it's recommended to call for drawing AFTER garment.load() """ if top_layout is not None: self.ui_top_layout = top_layout if self.ui_top_layout is None: raise ValueError('GarmentDrawUI::top level layout not found') self._clean_layout(self.ui_top_layout) cmds.setParent(self.ui_top_layout) # Pattern name cmds.textFieldGrp(label='Pattern:', text=self.name, editable=False, cal=[1, 'left'], cw=[1, 50]) # load panels info cmds.frameLayout( label='Panel Placement', collapsable=True, borderVisible=True, collapse=True, mh=10, mw=10 ) if not self.loaded_to_maya: cmds.text(label='') else: for panel in self.pattern['panels']: panel_layout = cmds.frameLayout( label=panel, collapsable=True, collapse=True, borderVisible=True, mh=10, mw=10, expandCommand=partial(cmds.select, self.MayaObjects['panels'][panel]['curve_group']), collapseCommand=partial(cmds.select, self.MayaObjects['panels'][panel]['curve_group']) ) self._ui_3d_placement(panel) cmds.setParent('..') cmds.setParent('..') # Parameters cmds.frameLayout( label='Parameters', collapsable=True, borderVisible=True, collapse=True, mh=10, mw=10 ) self._ui_params(self.parameters, self.spec['parameter_order']) cmds.setParent('..') # constraints if 'constraints' in self.spec: cmds.frameLayout( label='Constraints', collapsable=True, borderVisible=True, collapse=True, mh=10, mw=10 ) self._ui_constraints(self.spec['constraints'], self.spec['constraint_order']) cmds.setParent('..') # fin cmds.setParent('..') def _clean_layout(self, layout): """Removes all of the childer from layout""" children = cmds.layout(layout, query=True, childArray=True) if children: cmds.deleteUI(children) def _ui_3d_placement(self, panel_name): """Panel 3D placement""" if not self.loaded_to_maya: cmds.text(label='') # Position cmds.attrControlGrp( attribute=self.MayaObjects['panels'][panel_name]['curve_group'] + '.translate', changeCommand=partial(self._panel_placement_callback, panel_name, 'translation', 'translate') ) # Rotation cmds.attrControlGrp( attribute=self.MayaObjects['panels'][panel_name]['curve_group'] + '.rotate', changeCommand=partial(self._panel_placement_callback, panel_name, 'rotation', 'rotate') ) def _ui_param_value(self, param_name, param_range, value, idx=None, tag=''): """Create UI elements to display range and control the param value""" # range cmds.rowLayout(numberOfColumns=3) cmds.text(label='Range ' + tag + ':') cmds.floatField(value=param_range[0], editable=False) cmds.floatField(value=param_range[1], editable=False) cmds.setParent('..') # value value_field = cmds.floatSliderGrp( label='Value ' + tag + ':', field=True, value=value, minValue=param_range[0], maxValue=param_range[1], cal=[1, 'left'], cw=[1, 45], step=0.01 ) # add command with reference to current field cmds.floatSliderGrp(value_field, edit=True, changeCommand=partial(self._param_value_callback, param_name, idx, value_field)) def _ui_params(self, params, order): """draw params UI""" # control cmds.button(label='To template state', backgroundColor=[227 / 256, 255 / 256, 119 / 256], command=self._to_template_callback, ann='Snap all parameters to default values') cmds.button(label='Randomize', backgroundColor=[227 / 256, 186 / 256, 119 / 256], command=self._param_randomization_callback, ann='Randomize all parameter values') # Parameters themselves for param_name in order: cmds.frameLayout( label=param_name, collapsable=True, collapse=True, mh=10, mw=10 ) # type cmds.textFieldGrp(label='Type:', text=params[param_name]['type'], editable=False, cal=[1, 'left'], cw=[1, 30]) # parameters might have multiple values values = params[param_name]['value'] param_ranges = params[param_name]['range'] if isinstance(values, list): ui_tags = ['X', 'Y', 'Z', 'W'] for idx, (value, param_range) in enumerate(zip(values, param_ranges)): self._ui_param_value(param_name, param_range, value, idx, ui_tags[idx]) else: self._ui_param_value(param_name, param_ranges, values) # fin cmds.setParent('..') def _ui_constraints(self, constraints, order): """View basic info about specified constraints""" for constraint_name in order: cmds.textFieldGrp( label=constraint_name + ':', text=constraints[constraint_name]['type'], editable=False, cal=[1, 'left'], cw=[1, 90]) def _quick_dropdown(self, options, chosen='', label=''): """Add a dropdown with given options""" menu = cmds.optionMenu(label=label) for option in options: cmds.menuItem(label=option) if chosen: cmds.optionMenu(menu, e=True, value=chosen) return menu # -------- Callbacks ----------- def _to_template_callback(self, *args): """Returns current pattern to template state and updates UI accordingly""" # update print('Pattern returns to origins..') self._restore_template() # update geometry in lazy manner if self.loaded_to_maya: self.load() # update UI in lazy manner self.drawUI() def _param_randomization_callback(self, *args): """Randomize parameter values & update everything""" self._randomize_pattern() # update geometry in lazy manner if self.loaded_to_maya: self.load() # update UI in lazy manner self.drawUI() def _param_value_callback(self, param_name, value_idx, value_field, *args): """Update pattern with new value""" # in case the try failes spec_backup = copy.deepcopy(self.spec) if isinstance(self.parameters[param_name]['value'], list): old_value = self.parameters[param_name]['value'][value_idx] else: old_value = self.parameters[param_name]['value'] # restore template state -- params are interdependent # change cannot be applied independently by but should follow specified param order self._restore_template(params_to_default=False) # get value new_value = args[0] # save new value. No need to check ranges -- correct by UI if isinstance(self.parameters[param_name]['value'], list): self.parameters[param_name]['value'][value_idx] = new_value else: self.parameters[param_name]['value'] = new_value # reapply all parameters self._update_pattern_by_param_values() if self.is_self_intersecting(): result = cmds.confirmDialog( title='Restore from broken state', message=('WARNING: Some of the panels contain intersected edges after applying value {} to {}.' '\nDo you want to revert to previous state?' '\n\nNote: simulation in broken state might result in Maya crashing').format(new_value, param_name), button=['Yes', 'No'], defaultButton='Yes', cancelButton='No', dismissString='No') if result == 'Yes': self._restore(spec_backup) cmds.floatSliderGrp(value_field, edit=True, value=old_value) return # No need to reload geometry -- nothing changed # update geometry in lazy manner if self.loaded_to_maya: self.load() # NOTE updating values in UI in this callback causes Maya crashes! # Without update, the 3D placement UI gets disconnected from geometry but that's minor # self.drawUI() def _panel_placement_callback(self, panel_name, attribute, maya_attr): """Update pattern spec with tranlation/rotation info from Maya""" # get values values = cmds.getAttr(self.MayaObjects['panels'][panel_name]['curve_group'] + '.' + maya_attr) values = values[0] # only one attribute requested # set values self.pattern['panels'][panel_name][attribute] = list(values) class Scene(object): """ Decribes scene setup that includes: * body object * floor * light(s) & camera(s) Assumes * body the scene revolved aroung faces z+ direction """ def __init__(self, body_obj, props, scenes_path='', clean_on_die=False): """ Set up scene for rendering using loaded body as a reference """ self.self_clean = clean_on_die self.props = props self.config = props['config'] self.stats = props['stats'] # load body to be used as a translation reference self._load_body(body_obj) # scene self._init_arnold() self.scene = {} if 'scene' in self.config: self._load_maya_scene(os.path.join(scenes_path, self.config['scene'])) else: self._simple_scene_setup() def __del__(self): """Remove all objects related to current scene if requested on creation""" if self.self_clean: cmds.delete(self.body) cmds.delete(self.cameras) # Shaders & other stuff for key in self.scene: # garment color migh become invalid cmds.delete(self.scene[key]) def _init_arnold(self): """Ensure Arnold objects are launched in Maya & init GPU rendering settings""" objects = cmds.ls('defaultArnoldDriver') if not objects: # Arnold objects not found # https://arnoldsupport.com/2015/12/09/mtoa-creating-the-defaultarnold-nodes-in-scripting/ print('Initialized Arnold') mtoa.core.createOptions() cmds.setAttr('defaultArnoldRenderOptions.renderDevice', 1) # turn on GPPU rendering cmds.setAttr('defaultArnoldRenderOptions.render_device_fallback', 1) # switch to CPU in case of failure cmds.setAttr('defaultArnoldRenderOptions.AASamples', 10) # increase sampling for clean results def floor(self): return self.scene['floor'] def cloth_SG(self): return self.scene['cloth_SG'] def render(self, save_to, name='last'): """ Makes a rendering of a current scene, and saves it to a given path """ # https://forums.autodesk.com/t5/maya-programming/rendering-with-arnold-in-a-python-script/td-p/7710875 # NOTE that attribute names depend on Maya version. These are for Maya2018-Maya2020 im_size = self.config['resolution'] # image setup old_setup = self._set_image_size(im_size, im_size[0]/im_size[1], im_size[0]/im_size[1]) cmds.setAttr("defaultArnoldDriver.aiTranslator", "tif", type="string") # fixing dark rendering problem # https://forums.autodesk.com/t5/maya-shading-lighting-and/output-render-w-color-management-is-darker-than-render-view/td-p/7207081 cmds.colorManagementPrefs(e=True, outputTransformEnabled=True, outputUseViewTransform=True) # render all the cameras curr_frame = cmds.currentTime(query=True) start_time = time.time() for camera in self.cameras: print('Rendering from camera {}'.format(camera)) camera_name = camera.split(':')[-1] # list of one element if ':' is not found local_name = (name + '_' + camera_name) if name else camera_name filename = os.path.join(save_to, local_name) cmds.setAttr("defaultArnoldDriver.prefix", filename, type="string") cmds.arnoldRender(width=im_size[0], height=im_size[1], batch=True, frameSequence=curr_frame, camera=camera) self._set_image_size(*old_setup) # restore settings self.stats['render_time'][name] = time.time() - start_time def fetch_props_from_Maya(self): """Get properties records from Maya Note: it updates global config!""" # Update color settings self.config['garment_color'] = self._fetch_color(self.scene['cloth_shader']) def reset_garment_color(self): """Force the current garment shader color""" color_plug = self.scene['cloth_shader'] + '.color' if cmds.connectionInfo(color_plug, isDestination=True): connected_color_plug = cmds.connectionInfo(color_plug, sourceFromDestination=True) cmds.disconnectAttr(connected_color_plug, color_plug) color = self.config['garment_color'] cmds.setAttr((color_plug), color[0], color[1], color[2], type='double3') # ------- Private ----------- def _load_body(self, bodyfilename): """Load body object and scale it to cm units""" # load self.body_filepath = bodyfilename self.body = utils.load_file(bodyfilename, 'body') utils.scale_to_cm(self.body) def _fetch_color(self, shader): """Return current color of a given shader node""" color_plug = shader + '.color' if cmds.connectionInfo(shader + '.color', isDestination=True): color_plug = cmds.connectionInfo(color_plug, sourceFromDestination=True) return cmds.getAttr(color_plug)[0] def _simple_scene_setup(self): """setup very simple scene & materials""" colors = { "body_color": [0.5, 0.5, 0.7], "cloth_color": [0.8, 0.2, 0.2] if 'garment_color' not in self.config else self.config['garment_color'], "floor_color": [0.8, 0.8, 0.8] } self.scene = { 'floor': self._add_floor(self.body) } # materials self.scene['body_shader'], self.scene['body_SG'] = self._new_lambert(colors['body_color'], self.body) self.scene['cloth_shader'], self.scene['cloth_SG'] = self._new_lambert(colors['cloth_color'], self.body) self.scene['floor_shader'], self.scene['floor_SG'] = self._new_lambert(colors['floor_color'], self.body) self.scene['light'] = mutils.createLocator('aiSkyDomeLight', asLight=True) # Put camera self.cameras = [self._add_simple_camera()] # save config self.config['garment_color'] = colors['cloth_color'] def _load_maya_scene(self, scenefile): """Load scene from external file. NOTE Assumes certain naming of nodes in the scene!""" before = set(cmds.ls()) cmds.file(scenefile, i=True, namespace='imported') new_objects = set(cmds.ls()) - before # Maya may modify namespace for uniquness scene_namespace = new_objects.pop().split(':')[0] + '::' self.scene = { 'scene_group': cmds.ls(scene_namespace + '*scene*', transforms=True)[0], 'floor': cmds.ls(scene_namespace + '*floor*', geometry=True)[0], 'body_shader': cmds.ls(scene_namespace + '*body*', materials=True)[0], 'cloth_shader': cmds.ls(scene_namespace + '*garment*', materials=True, )[0] } # shader groups (to be used in cmds.sets()) self.scene['body_SG'] = self._create_shader_group(self.scene['body_shader'], 'bodySG') self.scene['cloth_SG'] = self._create_shader_group(self.scene['cloth_shader'], 'garmentSG') if 'garment_color' in self.config: # if given, use it self.reset_garment_color() else: # save garment color to config self.config['garment_color'] = self._fetch_color(self.scene['cloth_shader']) # apply coloring to body object if self.body: cmds.sets(self.body, forceElement=self.scene['body_SG']) # collect cameras self.cameras = cmds.ls(scene_namespace + '*camera*', transforms=True) # adjust scene position s.t. body is standing in the middle body_low_center = self._get_object_lower_center(self.body) floor_low_center = self._get_object_lower_center(self.scene['floor']) old_translation = cmds.getAttr(self.scene['scene_group'] + '.translate')[0] cmds.setAttr( self.scene['scene_group'] + '.translate', old_translation[0] + body_low_center[0] - floor_low_center[0], old_translation[1] + body_low_center[1] - floor_low_center[1], old_translation[2] + body_low_center[2] - floor_low_center[2], type='double3') # apply to whole group s.t. lights positions were adjusted too def _add_simple_camera(self, rotation=[-23.2, 16, 0]): """Puts camera in the scene NOTE Assumes body is facing +z direction""" camera = cmds.camera(aspectRatio=self.config['resolution'][0] / self.config['resolution'][1])[0] cmds.setAttr(camera + '.rotate', rotation[0], rotation[1], rotation[2], type='double3') # to view the target body fitFactor = self.config['resolution'][1] / self.config['resolution'][0] cmds.viewFit(camera, self.body, f=fitFactor) return camera def _get_object_lower_center(self, object): """return 3D position of the center of the lower side of bounding box""" bb = cmds.exactWorldBoundingBox(object) return [ (bb[3] + bb[0]) / 2, bb[1], (bb[5] + bb[2]) / 2 ] def _add_floor(self, target): """ adds a floor under a given object """ target_bb = cmds.exactWorldBoundingBox(target) size = 50 * (target_bb[4] - target_bb[1]) floor = cmds.polyPlane(n='floor', w=size, h=size) # place under the body floor_level = target_bb[1] cmds.move((target_bb[3] + target_bb[0]) / 2, # bbox center floor_level, (target_bb[5] + target_bb[2]) / 2, # bbox center floor, a=1) # Make the floor non-renderable shape = cmds.listRelatives(floor[0], shapes=True) cmds.setAttr(shape[0] + '.primaryVisibility', 0) return floor[0] def _new_lambert(self, color, target=None): """created a new shader node with given color""" shader = cmds.shadingNode('lambert', asShader=True) cmds.setAttr((shader + '.color'), color[0], color[1], color[2], type='double3') shader_group = self._create_shader_group(shader) if target is not None: cmds.sets(target, forceElement=shader_group) return shader, shader_group def _create_shader_group(self, material, name='shader'): """Create a shader group set for a given material (to be used in cmds.sets())""" shader_group = cmds.sets(renderable=True, noSurfaceShader=True, empty=True, name=name) cmds.connectAttr(material + '.outColor', shader_group + '.surfaceShader') return shader_group def _set_image_size(self, im_size, ar_pix, ar_device): """Set image size for rendering""" # remember the old settings to allow restoration old_ar_pix = cmds.getAttr("defaultResolution.pixelAspect") old_ar_device = cmds.getAttr("defaultResolution.deviceAspectRatio") old_size = [0, 0] old_size[0] = cmds.getAttr("defaultResolution.width") old_size[1] = cmds.getAttr("defaultResolution.height") cmds.setAttr("defaultResolution.width", im_size[0]) cmds.setAttr("defaultResolution.height", im_size[1]) cmds.setAttr("defaultResolution.pixelAspect", ar_pix) cmds.setAttr("defaultResolution.deviceAspectRatio", ar_device) return old_size, old_ar_pix, old_ar_device