init_code

This commit is contained in:
sky
2025-07-03 17:03:00 +08:00
parent a710c87a2b
commit 89766fe3d1
220 changed files with 479903 additions and 77 deletions

View File

@@ -0,0 +1,31 @@
"""
Package for to simulate garments from patterns in Maya with Qualoth.
Main dependencies:
* Maya 2022+ (uses Python 3.6+)
* Arnold Renderer
* Qualoth (compatible with your Maya version)
To run the package in Maya don't foget to add it to PYTHONPATH!
"""
from importlib import reload
import pygarment.mayaqltools.mayascene as mayascene
reload(mayascene)
from .mayascene import PatternLoadingError
from .mayascene import MayaGarment
from .mayascene import Scene
from .mayascene import MayaGarmentWithUI
import pygarment.mayaqltools.simulation as simulation
import pygarment.mayaqltools.qualothwrapper as qualothwrapper
import pygarment.mayaqltools.garmentUI as garmentUI
import pygarment.mayaqltools.scan_imitation as scan_imitation
import pygarment.mayaqltools.utils as utils
reload(simulation)
reload(qualothwrapper)
reload(garmentUI)
reload(scan_imitation)
reload(utils)

View File

@@ -0,0 +1,569 @@
"""
Maya interface for editing & testing patterns files
* Maya 2022+
* Qualoth
"""
# Basic
from functools import partial
from datetime import datetime
import os
import numpy as np
# Maya
from maya import cmds
import maya.mel as mel
# My modules
from pygarment import mayaqltools as mymaya
from pygarment import data_config
# -------- Main call - Draw the UI -------------
def start_GUI():
"""Initialize interface"""
# Init state
state = State()
# init window
window_width = 450
main_offset = 10
win = cmds.window(
title="Garment Viewer", width=window_width,
closeCommand=win_closed_callback,
topEdge=15
)
cmds.columnLayout(columnAttach=('both', main_offset), rowSpacing=10, adj=1)
# ------ Draw GUI -------
# Pattern load
text_button_group(template_field_callback, state, label='Pattern spec: ', button_label='Load')
# body load
text_button_group(load_body_callback, state, label='Body file: ', button_label='Load')
# props load
text_button_group(load_props_callback, state, label='Properties: ', button_label='Load')
# scene setup load
text_button_group(load_scene_callback, state, label='Scene: ', button_label='Load')
# separate
cmds.separator()
# Pattern description
state.pattern_layout = cmds.columnLayout(
columnAttach=('both', 0), rowSpacing=main_offset, adj=1)
cmds.text(label='<pattern_here>', al='left')
cmds.setParent('..')
# separate
cmds.separator()
# Operations
equal_rowlayout(5, win_width=window_width, offset=(main_offset / 2))
cmds.button(label='Reload Spec', backgroundColor=[255 / 256, 169 / 256, 119 / 256],
command=partial(reload_garment_callback, state))
sim_button = cmds.button(label='Start Sim', backgroundColor=[227 / 256, 255 / 256, 119 / 256])
cmds.button(sim_button, edit=True,
command=partial(start_sim_callback, sim_button, state))
collisions_button = cmds.button(label='Collisions', backgroundColor=[250 / 256, 200 / 256, 119 / 256])
cmds.button(collisions_button, edit=True,
command=partial(check_collisions_callback, collisions_button, state))
segm_button = cmds.button(label='Segmentation', backgroundColor=[150 / 256, 225 / 256, 80 / 256])
cmds.button(segm_button, edit=True,
command=partial(display_segmentation_callback, segm_button, state))
scan_button = cmds.button(label='3D Scan', backgroundColor=[200 / 256, 225 / 256, 80 / 256])
cmds.button(scan_button, edit=True,
command=partial(imitate_3D_scan_callback, scan_button, state))
cmds.setParent('..')
# separate
cmds.separator()
# Saving folder
saving_to_field = text_button_group(saving_folder_callback, state,
label='Saving to: ', button_label='Choose')
# saving requests
equal_rowlayout(2, win_width=window_width, offset=main_offset)
cmds.button(label='Save snapshot', backgroundColor=[227 / 256, 255 / 256, 119 / 256],
command=partial(quick_save_callback, saving_to_field, state),
ann='Quick save with pattern spec and sim config')
cmds.button(label='Save with render', backgroundColor=[255 / 256, 140 / 256, 73 / 256],
command=partial(full_save_callback, saving_to_field, state),
ann='Full save with pattern spec, sim config, garment mesh & rendering')
cmds.setParent('..')
# Last
cmds.text(label='') # offset
# fin
cmds.showWindow(win)
# ----- State -------
class State(object):
def __init__(self):
self.pattern_layout = None # to be set on UI init
self.garment = None
self.scene = None
self.save_to = None
self.saving_prefix = None
self.body_file = None
self.config = data_config.Properties()
self.scenes_path = ''
self.segmented = False
mymaya.simulation.init_sim_props(self.config) # use default setup for simulation -- for now
def reload_garment(self):
"""Reloads garment Geometry & UI with current scene.
JSON is NOT loaded from disk as it's on-demand operation"""
if self.garment is None:
return
if self.scene is not None:
self.garment.load(
shader_group=self.scene.cloth_SG(),
obstacles=[self.scene.body], # self.scene.floor()],
config=self.config['sim']['config']
)
self.scene.reset_garment_color() # in case there was a segmentation display
else:
self.garment.load(config=self.config['sim']['config'])
# calling UI after loading for correct connection of attributes
self.garment.drawUI(self.pattern_layout)
def fetch(self):
"""Update info in deendent object from Maya"""
if self.scene is not None:
self.scene.fetch_props_from_Maya()
garment_conf = self.garment.fetchSimProps()
self.config.set_section_config(
'sim',
material=garment_conf['material'],
body_friction=garment_conf['body_friction'],
collision_thickness=garment_conf['collision_thickness']
)
def serialize(self, directory):
"""Serialize text-like objects"""
self.config.serialize(os.path.join(directory, 'sim_props.json'))
self.garment.serialize(
directory,
to_subfolder=False,
with_3d=False, with_text=False, view_ids=False,
empty_ok=True)
def save_scene(self, directory):
"""Save scene objects"""
self.garment.save_mesh(directory)
self.scene.render(directory, self.garment.name)
# ------- Errors --------
class CustomError(Exception):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return(self.__class__.__name__ + ', {0} '.format(self.message))
else:
return(self.__class__.__name__)
class SceneSavingError(CustomError):
def __init__(self, *args):
super(SceneSavingError, self).__init__(*args)
# --------- UI Drawing ----------
def equal_rowlayout(num_columns, win_width, offset):
"""Create new layout with given number of columns + extra columns for spacing"""
col_width = []
for col in range(1, num_columns + 1):
col_width.append((col, win_width / num_columns - offset))
col_attach = [(col, 'both', offset) for col in range(1, num_columns + 1)]
return cmds.rowLayout(
numberOfColumns=num_columns,
columnWidth=col_width,
columnAttach=col_attach,
)
def text_button_group(callback, state, label='', button_label='Click'):
"""Custom version of textFieldButtonGrp"""
cmds.rowLayout(nc=3, adj=2)
cmds.text(label=label)
text_field = cmds.textField(editable=False)
cmds.button(
label=button_label,
bgc=[0.99, 0.66, 0.46], # backgroundColor=[255 / 256, 169 / 256, 119 / 256],
command=partial(callback, text_field, state))
cmds.setParent('..')
return text_field
# ----- Callbacks -----
# -- Loading --
def sample_callback(text, *args):
print('Called ' + text)
def template_field_callback(view_field, state, *args):
"""Get the file with pattern"""
current_dir = os.path.dirname(cmds.textField(view_field, query=True, text=True))
multipleFilters = "JSON (*.json);;All Files (*.*)"
template_file = cmds.fileDialog2(
fileFilter=multipleFilters,
dialogStyle=2,
fileMode=1,
caption='Choose pattern specification file',
startingDirectory=current_dir
)
if not template_file: # do nothing
return
template_file = template_file[0]
cmds.textField(view_field, edit=True, text=template_file)
# create new grament
if state.garment is not None:
# Cleanup
state.garment.clean(delete=True)
state.garment = mymaya.MayaGarmentWithUI(template_file, True)
state.reload_garment()
def load_body_callback(view_field, state, *args):
"""Get body file & (re)init scene"""
current_dir = os.path.dirname(cmds.textField(view_field, query=True, text=True))
multipleFilters = "OBJ (*.obj);;All Files (*.*)"
file = cmds.fileDialog2(
fileFilter=multipleFilters,
dialogStyle=2,
fileMode=1,
caption='Choose body obj file',
startingDirectory=current_dir
)
if not file: # do nothing
return
file = file[0]
cmds.textField(view_field, edit=True, text=file)
state.config['body'] = os.path.basename(file) # update info
state.body_file = file
state.scene = mymaya.Scene(file, state.config['render'], scenes_path=state.scenes_path, clean_on_die=True) # previous scene will autodelete
state.reload_garment()
def load_props_callback(view_field, state, *args):
"""Load sim & renderign properties from file rather then use defaults"""
current_dir = os.path.dirname(cmds.textField(view_field, query=True, text=True))
multipleFilters = "JSON (*.json);;All Files (*.*)"
file = cmds.fileDialog2(
fileFilter=multipleFilters,
dialogStyle=2,
fileMode=1,
caption='Choose sim & rendering properties file',
startingDirectory=current_dir
)
if not file: # do nothing
return
file = file[0]
cmds.textField(view_field, edit=True, text=file)
# Edit the incoming config to reflect explicit choiced made in other UI elements
in_config = data_config.Properties(file)
# Use current body info instead of one from config
if state.body_file is not None:
in_config['body'] = os.path.basename(state.body_file)
# Use current scene info instead of one from config
if 'scene' not in state.config['render']['config']: # remove entirely
in_config['render']['config'].pop('scene', None)
else:
in_config['render']['config']['scene'] = state.config['render']['config']['scene']
# After the adjustments made, apply the new config to all elements
state.config = in_config
mymaya.simulation.init_sim_props(state.config) # fill the empty parts
if state.scene is not None:
state.scene = mymaya.Scene(
state.body_file, state.config['render'],
scenes_path=state.scenes_path, clean_on_die=True)
if state.garment is not None:
state.reload_garment()
def load_scene_callback(view_field, state, *args):
"""Load sim & renderign properties from file rather then use defaults"""
current_dir = os.path.dirname(cmds.textField(view_field, query=True, text=True))
multipleFilters = "MayaBinary (*.mb);;All Files (*.*)"
file = cmds.fileDialog2(
fileFilter=multipleFilters,
dialogStyle=2,
fileMode=1,
caption='Choose scene setup Maya file',
startingDirectory=current_dir
)
if not file: # do nothing
return
file = file[0]
cmds.textField(view_field, edit=True, text=file)
# Use current scene info instead of one from config
state.config['render']['config']['scene'] = os.path.basename(file)
state.scenes_path = os.path.dirname(file)
# Update scene with new config
if state.scene is not None:
# del state.scene
state.scene = mymaya.Scene(
state.body_file, state.config['render'],
scenes_path=state.scenes_path,
clean_on_die=True)
state.reload_garment()
def reload_garment_callback(state, *args):
"""
(re)loads current garment object to Maya if it exists
"""
if state.garment is not None:
state.garment.reloadJSON()
state.reload_garment()
# -- Operations --
def start_sim_callback(button, state, *args):
""" Start simulation """
if state.garment is None or state.scene is None:
cmds.confirmDialog(title='Error', message='Load pattern specification & body info first')
return
print('Simulating..')
# Reload geometry in case something changed
state.reload_garment()
mymaya.qualothwrapper.start_maya_sim(state.garment, state.config['sim'])
# Update button
cmds.button(button, edit=True,
label='Stop Sim', backgroundColor=[245 / 256, 96 / 256, 66 / 256],
command=partial(stop_sim_callback, button, state))
def stop_sim_callback(button, state, *args):
"""Stop simulation execution"""
# toggle playback
cmds.play(state=False)
print('Simulation::Stopped')
# uppdate button state
cmds.button(button, edit=True,
label='Start Sim', backgroundColor=[227 / 256, 255 / 256, 119 / 256],
command=partial(start_sim_callback, button, state))
cmds.select(state.garment.get_qlcloth_props_obj()) # for props change
def check_collisions_callback(button, state, *args):
"""Run removal of faces that might be invisible to 3D scanner"""
# indicate waiting for imitation finish
cmds.button(button, edit=True,
label='Checking...', backgroundColor=[245 / 256, 96 / 256, 66 / 256],
command=partial(stop_sim_callback, button, state))
cmds.refresh(currentView=True)
cmds.confirmDialog(
title='Simulation quality info:',
message=(
'Simulation quality checks: \n\n'
'Garment intersect colliders: {} \n'
'Garment has self-intersections: {}').format(
'Yes' if state.garment.intersect_colliders_3D() else 'No',
'Yes' if state.garment.self_intersect_3D(verbose=True) else 'No'),
button=['Ok'], defaultButton='Ok', cancelButton='Ok', dismissString='Ok')
cmds.button(button, edit=True,
label='Collisions', backgroundColor=[250 / 256, 200 / 256, 119 / 256],
command=partial(check_collisions_callback, button, state))
def imitate_3D_scan_callback(button, state, *args):
"""Run removal of faces that might be invisible to 3D scanner"""
# indicate waiting for imitation finish
cmds.button(button, edit=True,
label='Scanning...', backgroundColor=[245 / 256, 96 / 256, 66 / 256])
cmds.refresh(currentView=True)
if 'scan_imitation' in state.config:
num_rays = state.config['scan_imitation']['config']['test_rays_num']
vis_rays = state.config['scan_imitation']['config']['visible_rays_num']
mymaya.scan_imitation.remove_invisible(
state.garment.get_qlcloth_geomentry(),
[state.scene.body] if state.scene is not None else [],
num_rays, vis_rays
)
else: # go with function defaults
mymaya.scan_imitation.remove_invisible(
state.garment.get_qlcloth_geomentry(),
[state.scene.body] if state.scene is not None else []
)
cmds.button(button, edit=True,
label='3D Scan', backgroundColor=[200 / 256, 225 / 256, 80 / 256],
command=partial(imitate_3D_scan_callback, button, state))
def display_segmentation_callback(button, state, *args):
"""
Visualize the segmentation labels
"""
if not state.segmented:
# indicate waiting for imitation finish
cmds.button(button, edit=True,
label='Segmenting...', backgroundColor=[245 / 256, 96 / 256, 66 / 256])
cmds.refresh(currentView=True)
state.garment.display_vertex_segmentation(state.scene.scene['cloth_shader'])
print('Segmentation displayed!')
state.segmented = True
cmds.button(button, edit=True,
label='Color', backgroundColor=[150 / 256, 225 / 256, 80 / 256],
command=partial(display_segmentation_callback, button, state))
else:
state.scene.reset_garment_color()
state.segmented = False
cmds.button(button, edit=True,
label='Segmentation', backgroundColor=[150 / 256, 225 / 256, 80 / 256],
command=partial(display_segmentation_callback, button, state))
# -- Saving ---
def win_closed_callback(*args):
"""Clean-up"""
# Remove solver objects from the scene
cmds.delete(cmds.ls('qlSolver*'))
# Other created objects will be automatically deleted through destructors
def saving_folder_callback(view_field, state, *args):
"""Choose folder to save files to"""
current_dir = cmds.textField(view_field, query=True, text=True)
directory = cmds.fileDialog2(
dialogStyle=2,
fileMode=3, # directories
caption='Choose folder to save snapshots and renderings to',
startingDirectory=current_dir
)
if not directory: # do nothing
return
directory = directory[0]
cmds.textField(view_field, edit=True, text=directory)
state.save_to = directory
# request saving prefix
tag_result = cmds.promptDialog(
t='Enter a saving prefix',
m='Enter a saving prefix:',
button=['OK', 'Cancel'],
defaultButton='OK',
cancelButton='Cancel',
dismissString='Cancel'
)
if tag_result == 'OK':
tag = cmds.promptDialog(query=True, text=True)
state.saving_prefix = tag
else:
state.saving_prefix = None
return True
def _new_dir(root_dir, tag='snap'):
"""create fresh directory for saving files"""
folder = tag + '_' + datetime.now().strftime('%y%m%d-%H-%M-%S')
path = os.path.join(root_dir, folder)
os.makedirs(path)
return path
def _create_saving_dir(view_field, state):
"""Create directory to save to """
if state.garment is None:
cmds.confirmDialog(title='Error', message='Load pattern specification first')
raise SceneSavingError('Garment is not loaded before saving')
if state.save_to is None:
if not saving_folder_callback(view_field, state):
raise SceneSavingError('Saving folder not supplied')
if state.saving_prefix is not None:
tag = state.saving_prefix
else:
tag = state.garment.name
new_dir = _new_dir(state.save_to, tag)
return new_dir
def quick_save_callback(view_field, state, *args):
"""Quick save with pattern spec and sim config"""
try:
new_dir = _create_saving_dir(view_field, state)
except SceneSavingError:
return
state.fetch()
state.serialize(new_dir)
state.garment.save_mesh(new_dir)
print('Garment info saved to ' + new_dir)
def full_save_callback(view_field, state, *args):
"""Full save with pattern spec, sim config, garment mesh & rendering"""
if state.garment is None or state.scene is None:
cmds.confirmDialog(title='Error', message='Load pattern specification & body info first')
return
# do the same as for quick save
try:
new_dir = _create_saving_dir(view_field, state)
except SceneSavingError:
return
# save scene objects
state.save_scene(new_dir)
# save text properties
state.fetch()
state.serialize(new_dir)
print('Pattern spec, props, 3D mesh & render saved to ' + new_dir)
cmds.select(state.garment.get_qlcloth_props_obj()) # for props change

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,396 @@
"""
Qualoth scripts are written in MEL.
This module makes a python interface to them
Notes:
* Error checks are sparse to save coding time & lines.
This sould not be a problem during the normal workflow
"""
import time
import sys
from maya import mel
from maya import cmds
def load_plugin():
"""
Forces loading Qualoth plugin into Maya.
Note that plugin should be installed and licensed to use it!
Inquire here: http://www.fxgear.net/vfxpricing
"""
maya_year = int(mel.eval('getApplicationVersionAsFloat'))
plugin_name = 'qualoth_' + str(maya_year) + '_x64'
print('Loading ', plugin_name)
cmds.loadPlugin(plugin_name)
# -------- Wrappers -----------
# Make sure that Qualoth plugin is loaded before running any wrappers!
def qlCreatePattern(curves_group):
"""
Converts given 2D closed curve to a flat geometry piece
"""
objects_before = cmds.ls(assemblies=True)
# run
cmds.select(curves_group)
mel.eval('qlCreatePattern()')
# Identify newly created objects
objects_after = cmds.ls(assemblies=True)
# No need for symmetric difference because we don't care if some objects were deleted
return list(set(objects_after) - set(objects_before))
def qlCreateSeam(curve1, curve2):
"""
Create a seam between two selected curves
"""
cmds.select([curve1, curve2])
# Operates on selection
seam_shape = mel.eval('qlCreateSeam()')
return seam_shape
def qlCreateCollider(cloth, target):
"""
Marks object as a collider object for cloth --
eshures that cloth won't penetrate body when simulated
"""
objects_before = cmds.ls(assemblies=True)
cmds.select([cloth, target])
# Operates on selection
mel.eval('qlCreateCollider()')
objects_after = cmds.ls(assemblies=True)
return list(set(objects_after) - set(objects_before))
def qlCleanCache(cloth):
"""Clean layback cache for given cloth. Accepts qlCloth object"""
cmds.select(cloth)
mel.eval('qlClearCache()')
def qlReinitSolver(cloth, solver):
"""Reinitialize solver
set both cloth and solver to the initial state before simulation was applied
NOTE: useful for correct reload of garments on delete
"""
cmds.select([cloth, solver])
mel.eval('qlReinitializeSolver()')
# ------- Higher-level functions --------
def start_maya_sim(garment, props):
"""Start simulation through Maya defalut playback without checks
Gives Maya user default control over stopping & resuming sim
Current qlCloth material properties from Maya are used (instead of garment config)
"""
config = props['config']
solver = _init_sim(config)
# Allow to assemble without gravity
print('Simulation::Assemble without gravity')
_set_gravity(solver, 0)
for frame in range(1, config['zero_gravity_steps']):
cmds.currentTime(frame) # step
# resume normally
print('Simulation::normal playback.. Use ESC key to stop simulation')
_set_gravity(solver, -980)
cmds.currentTime(frame - 1) # one step back to start from simulated state
cmds.play()
def run_sim(garment, props):
"""
Setup and run cloth simulator untill static equlibrium is achieved.
Note:
* Assumes garment is already properly aligned!
* All of the garments existing in Maya scene will be simulated
because solver is shared!!
"""
config = props['config']
solver = _init_sim(config)
start_time = time.time()
# Allow to assemble without gravity + skip checks for first few frames
print('Simulating {}'.format(garment.name))
_set_gravity(solver, 0)
for frame in range(1, config['zero_gravity_steps']):
cmds.currentTime(frame) # step
garment.cache_if_enabled(frame)
garment.update_verts_info()
_update_progress(frame, config['max_sim_steps']) # progress bar
# resume normally
_set_gravity(solver, -980)
for frame in range(config['zero_gravity_steps'], config['max_sim_steps']):
cmds.currentTime(frame) # step
garment.cache_if_enabled(frame)
garment.update_verts_info()
_update_progress(frame, config['max_sim_steps']) # progress bar
static, non_st_count = garment.is_static(config['static_threshold'], config['non_static_percent'])
if static: # Success!
print('\nAchieved static equilibrium for {}'.format(garment.name))
break
# stats
props['stats']['sim_time'][garment.name] = time.time() - start_time
props['stats']['spf'][garment.name] = props['stats']['sim_time'][garment.name] / frame
props['stats']['fin_frame'][garment.name] = frame
# Fail checks
# static equilibrium never detected -- might have false negs!
if frame == config['max_sim_steps'] - 1:
print('\nFailed to achieve static equilibrium for {} with {} non-static vertices out of {}'.format(
garment.name, non_st_count, len(garment.current_verts)))
_record_fail(props, 'static_equilibrium', garment.name)
# 3D penetrations
if garment.intersect_colliders_3D():
_record_fail(props, 'intersect_colliders', garment.name)
if garment.self_intersect_3D():
_record_fail(props, 'intersect_self', garment.name)
# Finished too fast
if props['stats']['sim_time'][garment.name] < 2: # 2 sec
_record_fail(props, 'fast_finish', garment.name)
def findSolver():
"""
Returns the name of the qlSover existing in the scene
(usully solver is created once per scene)
"""
solver = cmds.ls('*qlSolver*Shape*')
return solver[0] if solver else None
def deleteSolver():
"""deletes all solver objects from the scene"""
cmds.delete(cmds.ls('*qlSolver*'))
def flipPanelNormal(panel_geom):
"""Set flippling normals to True for a given panel geom objects
at least one of the provided objects should a qlPattern object"""
ql_pattern = [obj for obj in panel_geom if 'Pattern' in obj]
ql_pattern = ql_pattern[0]
shape = cmds.listRelatives(ql_pattern, shapes=True, path=True)
cmds.setAttr(shape[0] + '.flipNormal', 1)
def getVertsOnCurve(panel_node, curve, curve_group=None):
"""
Return the list of mesh vertices located on the curve
* panel_node is qlPattern object to which the curve belongs
* curve is a main name of a curve object to get vertex info for
OR any substring of it's full Maya name that would uniquely identify it
* (optional) curve_group is a name of parent group of given curve to uni quely distinguish the curve
"""
# find qlDiscretizer node
if 'Shape' not in panel_node:
shapes = cmds.listRelatives(panel_node, shapes=True)
panel_node = panel_node + '|' + shapes[0]
connections = cmds.listConnections(panel_node)
discretizer = [node for node in connections if 'qlDiscretizer' in node]
discretizer = discretizer[0]
info_array = discretizer + '.curveVeritcesInfoArray'
# iterate over curveVeritcesInfoArray
num_curves = cmds.getAttr(info_array, size=True)
# avoid matching 'curve1' with 'Acurve10' by adding a starting and ending caharacter
if curve[0] != '|':
curve = '|' + curve
if curve[-1] != '|':
curve = curve + '|'
for idx in range(num_curves):
curve_name = cmds.getAttr(info_array + '[%d].curveName' % idx)
if curve in curve_name:
if curve_group is not None and curve_group not in curve_name:
# erroneous match
continue
# found!
vertices = cmds.getAttr(info_array + '[%d].curveVertices' % idx)
return vertices
return None
# ------ Working with props ------
def setColliderFriction(collider_objects, friction_value):
"""Sets the level of friction of the given collider to friction_value"""
main_collider = [obj for obj in collider_objects if 'Offset' not in obj]
collider_shape = cmds.listRelatives(main_collider[0], shapes=True)
cmds.setAttr(collider_shape[0] + '.friction', friction_value)
def setFabricProps(cloth, props):
"""Set given material propertied to qlClothObject"""
if not props:
return
# Simple ones
cmds.setAttr(cloth + '.density', props['density'], clamp=True)
cmds.setAttr(cloth + '.stretch', props['stretch_resistance'], clamp=True)
cmds.setAttr(cloth + '.shear', props['shear_resistance'], clamp=True)
cmds.setAttr(cloth + '.stretchDamp', props['stretch_damp'], clamp=True)
cmds.setAttr(cloth + '.bend', props['bend_resistance'], clamp=True)
cmds.setAttr(cloth + '.bendAngleDropOff', props['bend_angle_dropoff'], clamp=True)
cmds.setAttr(cloth + '.bendDamp', props['bend_damp'], clamp=True)
cmds.setAttr(cloth + '.bendDampDropOff', props['bend_damp_dropoff'], clamp=True)
cmds.setAttr(cloth + '.bendYield', props['bend_yield'], clamp=True)
cmds.setAttr(cloth + '.bendPlasticity', props['bend_plasticity'], clamp=True)
cmds.setAttr(cloth + '.viscousDamp', props['viscous_damp'], clamp=True)
cmds.setAttr(cloth + '.friction', props['friction'], clamp=True)
cmds.setAttr(cloth + '.pressure', props['pressure'], clamp=True)
cmds.setAttr(cloth + '.lengthScale', props['length_scale'], clamp=True)
cmds.setAttr(cloth + '.airDrag', props['air_drag'], clamp=True)
cmds.setAttr(cloth + '.rubber', props['rubber'], clamp=True)
# need setting flags
cmds.setAttr(cloth + '.overrideCompression', 1)
cmds.setAttr(cloth + '.compression', props['compression_resistance'], clamp=True)
cmds.setAttr(cloth + '.anisotropicControl', 1)
cmds.setAttr(cloth + '.uStretchScale', props['weft_resistance_scale'], clamp=True)
cmds.setAttr(cloth + '.vStretchScale', props['warp_resistance_scale'], clamp=True)
cmds.setAttr(cloth + '.rubberU', props['weft_rubber_scale'], clamp=True)
cmds.setAttr(cloth + '.rubberV', props['warp_rubber_scale'], clamp=True)
def setPanelsResolution(scaling):
"""Set resoluiton conroller of all qlPatterns in the scene"""
all_panels = cmds.ls('*qlPattern*', shapes=True)
for panel in all_panels:
cmds.setAttr(panel + '.resolutionScale', scaling)
def fetchFabricProps(cloth):
"""Returns current material properties of the cloth's objects
Requires qlCloth object
"""
props = {}
# Mass density per unit area. (Kg/cm2)
props['density'] = cmds.getAttr(cloth + '.density')
# Resisting force to planar stretching and compression
props['stretch_resistance'] = cmds.getAttr(cloth + '.stretch')
# Resisting force to shearing. (See Figure.) This parameter is
# interpreted as a scale factor to the stretch resistance.
props['shear_resistance'] = cmds.getAttr(cloth + '.shear')
# Damping factor for stretching motion.
props['stretch_damp'] = cmds.getAttr(cloth + '.stretchDamp')
# Resisting force to bending.
props['bend_resistance'] = cmds.getAttr(cloth + '.bend')
props['bend_angle_dropoff'] = cmds.getAttr(cloth + '.bendAngleDropOff')
# Damping factor for bending motion
props['bend_damp'] = cmds.getAttr(cloth + '.bendDamp')
props['bend_damp_dropoff'] = cmds.getAttr(cloth + '.bendDampDropOff')
# creases: elasticity vs plasticity
props['bend_yield'] = cmds.getAttr(cloth + '.bendYield')
props['bend_plasticity'] = cmds.getAttr(cloth + '.bendPlasticity')
# external
# This damping force drags the motion of each cloth vertex in all directions uniformly
# regardless of the directions of normals of each vertex.
props['viscous_damp'] = cmds.getAttr(cloth + '.viscousDamp')
# Controls the friction among cloth objects or colliders. Also self-friction
props['friction'] = cmds.getAttr(cloth + '.friction')
# The amount of pressure force which are applied to the vertex normal directions of each cloth vertex.
props['pressure'] = cmds.getAttr(cloth + '.pressure')
# need setting flags
# need to turn on .overrideCompression
props['compression_resistance'] = cmds.getAttr(cloth + '.compression')
# ------ unlikely to be used ---------
# Scale factor for length unit.
props['length_scale'] = cmds.getAttr(cloth + '.lengthScale')
# value controls the amount of influence from those air fields. In case
# here is no attached field to this cloth, 'Air Drag' simply drags the
# cloth motion in the direction of face normals of each triangle.
props['air_drag'] = cmds.getAttr(cloth + '.airDrag')
# This value scales the area of the cloth in rest state.
props['rubber'] = cmds.getAttr(cloth + '.rubber')
# fine-grained
# The scale factor to the planar stretching/compression resistance in weft (U) direction.
props['weft_resistance_scale'] = cmds.getAttr(cloth + '.uStretchScale')
# The scale factor to the planar stretching/compression resistance in warp (V) direction.
props['warp_resistance_scale'] = cmds.getAttr(cloth + '.vStretchScale')
# The scale factor to the rubber value (rest length scale) in weft (U) direction.
props['weft_rubber_scale'] = cmds.getAttr(cloth + '.rubberU')
# The scale factor to the rubber value (rest length scale) in warp (V) direction.
props['warp_rubber_scale'] = cmds.getAttr(cloth + '.rubberV')
return props
def fetchColliderFriction(collider_objects):
"""Retrieve collider friction info from given collider"""
try:
main_collider = [obj for obj in collider_objects if 'Offset' not in obj]
collider_shape = cmds.listRelatives(main_collider[0], shapes=True)
return cmds.getAttr(collider_shape[0] + '.friction')
except ValueError as e:
# collider doesn't exist any more
return None
def fetchPanelResolution():
some_panels = cmds.ls('*qlPattern*')
shape = cmds.listRelatives(some_panels[0], shapes=True, path=True)
return cmds.getAttr(shape[0] + '.resolutionScale')
# ------- Self-Utils ---------
def _init_sim(config):
"""
Basic simulation settings before starting simulation
"""
solver = findSolver()
cmds.setAttr(solver + '.selfCollision', 1)
cmds.setAttr(solver + '.startTime', 1)
cmds.setAttr(solver + '.solverStatistics', 0) # for easy reading of console output
cmds.playbackOptions(ps=0, max=config['max_sim_steps']) # 0 playback speed = play every frame
return solver
def _set_gravity(solver, gravity):
"""Set a given value of gravity to sim solver"""
cmds.setAttr(solver + '.gravity1', gravity)
def _update_progress(progress, total):
"""Progress bar in console"""
# https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
amtDone = progress / total
num_dash = int(amtDone * 50)
sys.stdout.write('\rProgress: [{0:50s}] {1:.1f}%'.format('#' * num_dash + '-' * (50 - num_dash), amtDone * 100))
sys.stdout.flush()
def _record_fail(props, fail_type, garment_name):
"""add a failure recording to props. Creates nodes if don't exist"""
if 'fails' not in props['stats']:
props['stats']['fails'] = {}
try:
props['stats']['fails'][fail_type].append(garment_name)
except KeyError:
props['stats']['fails'][fail_type] = [garment_name]

View File

@@ -0,0 +1,116 @@
"""
Maya script for removing faces from 3D garement model that are not visible from the outside cameras
The goal is to imitate scanning artifacts that result in missing geometry
* Maya 2022+
"""
from maya import OpenMaya
from maya import cmds
import numpy as np
from datetime import datetime
# My modules
from pygarment.mayaqltools import utils
def _sample_on_sphere(rad):
"""Uniformly sample a point on a sphere with radious rad. Return as Maya-compatible floating-point vector"""
# Using method of (Muller 1959, Marsaglia 1972)
# see the last one here https://mathworld.wolfram.com/SpherePointPicking.html
uni_array = np.random.normal(size=3)
uni_array = uni_array / np.linalg.norm(uni_array) * rad
return OpenMaya.MFloatVector(uni_array[0], uni_array[1], uni_array[2])
def _camera_surface(target, obstacles=[], vertical_scaling_factor=1.5, ground_scaling_factor=1.2):
"""Generate a (3D scanning) camera surface around provided scene"""
# basically, draw a bounding box around the target
bbox = np.array(cmds.exactWorldBoundingBox(obstacles + [target])) # [xmin, ymin, zmin, xmax, ymax, zmax]
top = bbox[3:]
bottom = bbox[:3]
center = (top + bottom) / 2
dims = top - bottom
dims = [max(dims[0], dims[2]) * ground_scaling_factor, dims[1] * vertical_scaling_factor]
cube = cmds.polyCube(height=dims[1], depth=dims[0], width=dims[0], name='camera_surface')
# align with center
cmds.move(center[0], center[1], center[2], cube, absolute=True)
# remove bottom face -- as if no cameras there
# adding '.f[1]' would also remove the ceiling
cmds.polyDelFacet( cube[0] + '.f[3]') # we know exact structure of default polyCube in Maya2018 & Maya2020
return cube[0], np.max(dims)
def remove_invisible(target, obstacles=[], num_rays=30, visibile_rays=4):
"""Update target 3D mesh: remove faces that are not visible from camera_surface
* due to self-occlusion or occlusion by an obstacle
* Camera surface is generated aroung the target as a small "room" with empty floor and ceiling
In my context, target is usually a garment mesh, and obstacle is a body surface
Noise control:
* num_rays -- number of random rays to emit from each face -- the less rays, the more noisy the output is
* visibile_rays -- number of rays to hit camera surface without obstacles to consider the face to be visible
BUT at least one ray is always required to consider face as visible!
"""
# Follows the idea of self_intersect_3D() checks used in simulation pipeline
print('Performing scanning imitation on {} with obstacles {}'.format(target, obstacles))
# generate apropriate camera surface
camera_surface_obj, ray_dist = _camera_surface(target, obstacles)
start_time = datetime.now()
# get mesh objects as OpenMaya object
target_mesh, target_dag = utils.get_mesh_dag(target)
camera_surface_mesh, _ = utils.get_mesh_dag(camera_surface_obj)
obstacles_meshes = [utils.get_mesh_dag(name)[0] for name in obstacles]
# search for intersections
target_accelerator = target_mesh.autoUniformGridParams()
cam_surface_accelerator = camera_surface_mesh.autoUniformGridParams()
obstacles_accs = [mesh.autoUniformGridParams() for mesh in obstacles_meshes]
to_delete = []
target_face_iterator = OpenMaya.MItMeshPolygon(target_dag)
while not target_face_iterator.isDone(): # https://stackoverflow.com/questions/40422082/how-to-find-face-neighbours-in-maya
# midpoint of the current face -- start of all the rays
face_mean = OpenMaya.MFloatPoint(target_face_iterator.center(OpenMaya.MSpace.kWorld))
face_id = target_face_iterator.index()
visible_count = 0
visible = False
# Send rays in all directions from the currect vertex
for _ in range(num_rays):
rayDir = _sample_on_sphere(ray_dist)
# Case when face is visible from camera surface
if (utils.test_ray_intersect(camera_surface_mesh, face_mean, rayDir, cam_surface_accelerator) # intesection with camera surface
and not any([utils.test_ray_intersect(mesh, face_mean, rayDir, acc,) for mesh, acc in zip(obstacles_meshes, obstacles_accs)]) # intesects any of the obstacles
and not utils.test_ray_intersect(target_mesh, face_mean, rayDir, target_accelerator, hit_tol=1e-5)): # intersects itself
visible_count += 1
if visible_count >= visibile_rays: # enough rays are visible -- no need to test more
visible = True
if not visible:
to_delete.append(face_id)
target_face_iterator.next() # iterate!
cmds.delete(camera_surface_obj) # clean-up the scene
# Remove invisible faces
delete_strs = [target + '.f[{}]'.format(face_id) for face_id in to_delete]
if len(delete_strs) > 0:
cmds.polyDelFacet(tuple(delete_strs)) # as this is the last command to execute, it could be undone with Ctrl-Z once
passed = datetime.now() - start_time
print('{}::Removed {} faces after {}. Press Ctrl-Z to undo the changes'.format(target, len(to_delete), passed))
return len(to_delete), passed.total_seconds()

View File

@@ -0,0 +1,264 @@
"""Routines to run cloth simulation in Maya + Qualoth"""
# Basic
import time
import os
# Maya
from maya import cmds
# My modules
from pygarment.pattern.core import BasicPattern
import pygarment.mayaqltools as mymaya
from pygarment.mayaqltools import qualothwrapper as qw
# ----------- High-level requests --------------
# TODO Deprecated
def single_file_sim(resources, props, caching=False):
"""
Simulates the given template and puts the results in original template folder,
including config and statistics
"""
try:
# ----- Init -----
init_sim_props(props, True)
qw.load_plugin()
scene = mymaya.Scene(
os.path.join(resources['bodies_path'], props['body']),
props['render'],
scenes_path=resources['scenes_path'])
# Main part
template_simulation(os.path.join(resources['templates_path'], props['templates']),
scene, props['sim'], caching=caching)
# Fin
print('\nFinished experiment')
try:
# remove unnecessaty field
del props['sim']['stats']['processed']
except KeyError:
pass
props.serialize(os.path.join(resources['templates_path'], 'props.json'))
except Exception as e:
print(e)
def batch_sim(resources, data_path, dataset_props,
num_samples=None, caching=False, force_restart=False):
"""
Performs pattern simulation for each example in the dataset
given by dataset_props.
Batch processing is automatically resumed
from the last unporcessed datapoint if restart is not forced. The last
example on the processes list is assumed to cause the failure, so it can be later found in failure cases.
Parameters:
* resources -- dict of paths to needed resoursed:
* body_path -- path to folder with body meshes
* data_path -- path to folder with the dataset
* scenes_path -- path to folder with rendering scenes
* dataset_props -- dataset properties. Properties has to be of custom data_config.Properties() class and contain
* dataset folder (inside data_path)
* name of pattern template
* name of body .obj file
* type of dataset structure (with/without subfolders for patterns)
* list of processed samples if processing of dataset was allready attempted
Other needed properties will be filles with default values if the corresponding sections
are not found in props object
* num_samples -- number of (unprocessed) samples from dataset to process with this run. If None, runs over all unprocessed samples
* caching -- enables caching of every frame of simulation (disabled by default)
* force_restart -- force restarting the batch processing even if resume conditions are met.
"""
# ----- Init -----
if 'frozen' in dataset_props and dataset_props['frozen']:
# avoid accidential re-runs of data
print('WARNING: dataset is frozen, processing is skipped')
return True
resume = init_sim_props(dataset_props, batch_run=True, force_restart=force_restart)
qw.load_plugin()
scene = mymaya.Scene(
os.path.join(resources['bodies_default_path'], dataset_props['body']),
dataset_props['render'],
scenes_path=resources['scenes_path'])
pattern_specs = _get_pattern_files(data_path, dataset_props)
data_props_file = os.path.join(data_path, 'dataset_properties.json')
# Simulate every template
count = 0
for pattern_spec in pattern_specs:
# skip processed cases -- in case of resume. First condition needed to skip checking second one on False =)
pattern_spec_norm = os.path.normpath(pattern_spec)
pattern_name = BasicPattern.name_from_path(pattern_spec_norm)
if resume and pattern_name in dataset_props['sim']['stats']['processed']:
print('Skipped as already processed {}'.format(pattern_spec_norm))
continue
dataset_props['sim']['stats']['processed'].append(pattern_name)
_serialize_props_with_sim_stats(dataset_props, data_props_file) # save info of processed files before potential crash
template_simulation(pattern_spec_norm,
scene,
dataset_props['sim'],
delete_on_clean=True, # delete geometry after sim as we don't need it any more
caching=caching,
save_maya_scene=False)
if pattern_name in dataset_props['sim']['stats']['fails']['crashes']:
# if we successfully finished simulating crashed example -- it's not a crash any more!
print('Crash successfully resimulated!')
dataset_props['sim']['stats']['fails']['crashes'].remove(pattern_name)
count += 1 # count actively processed cases
if num_samples is not None and count >= num_samples: # only process requested number of samples
break
# Fin
print('\nFinished batch of ' + os.path.basename(data_path))
try:
if len(dataset_props['sim']['stats']['processed']) >= len(pattern_specs):
# processing successfully finished -- no need to resume later
del dataset_props['sim']['stats']['processed']
dataset_props['frozen'] = True
process_finished = True
else:
process_finished = False
except KeyError:
print('KeyError -processed-')
process_finished = True
pass
# Logs
_serialize_props_with_sim_stats(dataset_props, data_props_file)
return process_finished
# ------- Utils -------
def init_sim_props(props, batch_run=False, force_restart=False):
"""
Add default config values if not given in props & clean-up stats if not resuming previous processing
Returns a flag wheter current simulation is a resumed last one
"""
if 'sim' not in props:
props.set_section_config(
'sim',
max_sim_steps=500,
zero_gravity_steps=5, # time to assembly
static_threshold=0.05, # 0.01 # depends on the units used,
non_static_percent=1,
material={},
body_friction=0.5,
resolution_scale=5
)
if 'material' not in props['sim']['config']:
props['sim']['config']['material'] = {}
if 'render' not in props:
# init with defaults
props.set_section_config(
'render',
resolution=[800, 800]
)
if batch_run and 'processed' in props['sim']['stats'] and not force_restart:
# resuming existing batch processing -- do not clean stats
# Assuming the last example processed example caused the failure
last_processed = props['sim']['stats']['processed'][-1]
props['sim']['stats']['stop_over'].append(last_processed) # indicate resuming dataset simulation
if not any([(name in last_processed) or (last_processed in name) for name in props['render']['stats']['render_time']]):
# crash detected -- the last example does not appear in the stats
if last_processed not in props['sim']['stats']['fails']['crashes']:
# first time to crash here -- try to re-do this example => remove from visited
props['sim']['stats']['processed'].pop()
props['sim']['stats']['fails']['crashes'].append(last_processed)
# else we crashed here before -- do not re-try + leave in crashed list
return True
# else new life
# Prepare commulative stats
props.set_section_stats('sim', fails={}, sim_time={}, spf={}, fin_frame={})
props['sim']['stats']['fails'] = {
'crashes': [],
'intersect_colliders': [],
'intersect_self': [],
'static_equilibrium': [],
'fast_finish': [],
'pattern_loading': []
}
props.set_section_stats('render', render_time={})
if batch_run: # track batch processing
props.set_section_stats('sim', processed=[], stop_over=[])
return False
def template_simulation(spec, scene, sim_props, delete_on_clean=False, caching=False, save_maya_scene=False):
"""
Simulate given template within given scene & save log files
"""
print('\nGarment load')
garment = mymaya.MayaGarment(spec)
try:
garment.load(
shader_group=scene.cloth_SG(),
obstacles=[scene.body], # I don't add floor s.t. garment falls infinitely if falls
config=sim_props['config']
)
except mymaya.PatternLoadingError as e:
# record error and skip subequent processing
sim_props['stats']['fails']['pattern_loading'].append(garment.name)
else:
# garment.save_mesh(tag='stitched') # Saving the geometry before eny forces were applied
garment.sim_caching(caching)
qw.run_sim(garment, sim_props)
# save even if sim failed -- to see what happened!
garment.save_mesh(tag='sim')
scene.render(garment.path, garment.name)
if save_maya_scene:
# save current Maya scene
cmds.file(rename=os.path.join(garment.path, garment.name + '_scene'))
cmds.file(save=True, type='mayaBinary', force=True, defaultExtensions=True)
garment.clean(delete_on_clean)
def _serialize_props_with_sim_stats(dataset_props, filename):
"""Compute data processing statistics and serialize props to file"""
dataset_props.stats_summary()
dataset_props.serialize(filename)
def _get_pattern_files(data_path, dataset_props):
""" Collects paths to all the pattern files in given folder"""
to_ignore = ['renders'] # special dirs not to include in the pattern list
pattern_specs = []
root, dirs, files = next(os.walk(data_path))
if dataset_props['to_subfolders']:
# https://stackoverflow.com/questions/800197/how-to-get-all-of-the-immediate-subdirectories-in-python
for directory in dirs:
if directory not in to_ignore:
pattern_specs.append(os.path.join(root, directory, 'specification.json')) # cereful for file name changes ^^
else:
for file in files:
# NOTE filtering might not be very robust
if ('.json' in file
and 'specification' in file
and 'template' not in file):
pattern_specs.append(os.path.normpath(os.path.join(root, file)))
return pattern_specs

View File

@@ -0,0 +1,167 @@
"""Shares utils to work with Maya"""
import ctypes
import os
import numpy as np
from maya import OpenMaya
from maya import cmds
# ----- Working with files -----
def load_file(filepath, name='object'):
"""Load mesh to the scene"""
if not os.path.isfile(filepath):
raise RuntimeError('Loading Object from file to Maya::Missing file {}'.format(filepath))
obj = cmds.file(filepath, i=True, rnn=True)[0]
obj = cmds.rename(obj, name + '#')
return obj
def save_mesh(target, to_file):
"""Save given object to file as a mesh"""
# Make sure to only select requested mesh
cmds.select(clear=True)
cmds.select(target)
cmds.file(
to_file,
type='OBJExport',
exportSelectedStrict=True, # export selected -- only explicitely selected
options='groups=0;ptgroups=0;materials=0;smoothing=0;normals=1', # very simple obj
force=True, # force override if file exists
defaultExtensions=False
)
cmds.select(clear=True)
# ----- Mesh info -----
def get_dag(object_name):
"""Return DAG for requested object"""
selectionList = OpenMaya.MSelectionList()
selectionList.add(object_name)
dag = OpenMaya.MDagPath()
selectionList.getDagPath(0, dag)
return dag
def get_mesh_dag(object_name):
"""Return MFnMesh object by the object name"""
# get object as OpenMaya object -- though DAG
dag = get_dag(object_name)
# as mesh
mesh = OpenMaya.MFnMesh(dag) # reference https://help.autodesk.com/view/MAYAUL/2017/ENU/?guid=__py_ref_class_open_maya_1_1_m_fn_mesh_html
return mesh, dag
def get_vertices_np(mesh):
"""
Retreive vertex info as np array for given mesh object
"""
maya_vertices = OpenMaya.MPointArray()
mesh.getPoints(maya_vertices, OpenMaya.MSpace.kWorld)
vertices = np.empty((maya_vertices.length(), 3))
for i in range(maya_vertices.length()):
for j in range(3):
vertices[i, j] = maya_vertices[i][j]
return vertices
def match_vert_lists(short_list, long_list):
"""
Find the vertices from long list that correspond to verts in short_list
Both lists are numpy arrays
NOTE: Assuming order is matching => O(len(long_list)) complexity:
order of vertices in short list is the same as in long list (for those that are left)
"""
match_list = []
idx_short = 0
for idx_long in range(len(long_list)):
long_vertex = long_list[idx_long]
short_vertex = short_list[idx_short]
if all(np.isclose(short_vertex, long_vertex, atol=1e-5)):
match_list.append(idx_long)
idx_short += 1 # advance the short list indexing
if idx_short >= len(short_list): # short list finished before the long one
break
if len(match_list) != len(short_list):
raise ValueError('Vertex matching unsuccessfull: matched {} of {} vertices in short list'.format(
len(match_list), len(short_list)
))
return match_list
# ---- Mesh operations ----
def test_ray_intersect(mesh, raySource, rayVector, accelerator=None, hit_tol=None, return_info=False):
"""Check if given ray intersect given mesh
* hit_tol ignores intersections that are within hit_tol from the ray source (as % of ray length) -- usefull when checking self-intersect
* mesh is expected to be of MFnMesh type
* accelrator is a stucture for speeding-up calculations.
It can be initialized from MFnMesh object and should be supplied with every call to this function
"""
# 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
# follow structure https://stackoverflow.com/questions/58390664/how-to-fix-typeerror-in-method-mfnmesh-anyintersection-argument-4-of-type
maxParam = 1 # only search for intersections within given vector
testBothDirections = False # only in the given direction
sortHits = False # no need to waste time on sorting
hitPoints = OpenMaya.MFloatPointArray()
hitRayParams = OpenMaya.MFloatArray()
hitFaces = OpenMaya.MIntArray()
hit = mesh.allIntersections(
raySource, rayVector, None, None, False, OpenMaya.MSpace.kWorld, maxParam, testBothDirections, accelerator, sortHits,
hitPoints, hitRayParams, hitFaces, None, None, None, 1e-6)
if hit and hit_tol is not None:
hit = any([dist > hit_tol for dist in hitRayParams])
if return_info:
return hit, hitFaces, hitPoints, hitRayParams
return hit
def edge_vert_ids(mesh, edge_id):
"""Return vertex ids for a given edge in given mesh"""
# Have to go through the C++ wrappers
# Vertices that comprise an edge
script_util = OpenMaya.MScriptUtil(0.0)
v_ids_cptr = script_util.asInt2Ptr() # https://forums.cgsociety.org/t/mfnmesh-getedgevertices-error-on-2011/1652362
mesh.getEdgeVertices(edge_id, v_ids_cptr)
# get values from SWIG pointer https://stackoverflow.com/questions/39344039/python-cast-swigpythonobject-to-python-object
ty = ctypes.c_uint * 2
v_ids_list = ty.from_address(int(v_ids_cptr))
return v_ids_list[0], v_ids_list[1]
def scale_to_cm(target, max_height_cm=220):
"""Heuristically check the target units and scale to cantimeters if other units are detected
* default value of max_height_cm is for meshes of humans
"""
# check for througth height (Y axis)
# NOTE prone to fails if non-meter units are used for body
bb = cmds.polyEvaluate(target, boundingBox=True) # ((xmin,xmax), (ymin,ymax), (zmin,zmax))
height = bb[1][1] - bb[1][0]
if height < max_height_cm * 0.01: # meters
cmds.scale(100, 100, 100, target, centerPivot=True, absolute=True)
print('WARNING: {} is found to use meters as units. Scaled up by 100 for cm'.format(target))
elif height < max_height_cm * 0.1: # decimeters
cmds.scale(10, 10, 10, target, centerPivot=True, absolute=True)
print('WARNING: {} is found to use decimeters as units. Scaled up by 10 for cm'.format(target))
elif height > max_height_cm: # millimiters or something strange
cmds.scale(0.1, 0.1, 0.1, target, centerPivot=True, absolute=True)
print('WARNING: {} is found to use millimiters as units. Scaled down by 0.1 for cm'.format(target))