Files
design2garmentcode-impl/pygarment/mayaqltools/qualothwrapper.py

397 lines
15 KiB
Python
Raw Normal View History

2025-07-03 17:03:00 +08:00
"""
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]