init_code
This commit is contained in:
31
pygarment/mayaqltools/__init__.py
Normal file
31
pygarment/mayaqltools/__init__.py
Normal 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)
|
||||
569
pygarment/mayaqltools/garmentUI.py
Normal file
569
pygarment/mayaqltools/garmentUI.py
Normal 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
|
||||
1515
pygarment/mayaqltools/mayascene.py
Normal file
1515
pygarment/mayaqltools/mayascene.py
Normal file
File diff suppressed because it is too large
Load Diff
396
pygarment/mayaqltools/qualothwrapper.py
Normal file
396
pygarment/mayaqltools/qualothwrapper.py
Normal 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]
|
||||
116
pygarment/mayaqltools/scan_imitation.py
Normal file
116
pygarment/mayaqltools/scan_imitation.py
Normal 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()
|
||||
|
||||
264
pygarment/mayaqltools/simulation.py
Normal file
264
pygarment/mayaqltools/simulation.py
Normal 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
|
||||
|
||||
167
pygarment/mayaqltools/utils.py
Normal file
167
pygarment/mayaqltools/utils.py
Normal 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))
|
||||
Reference in New Issue
Block a user