Files
2025-07-03 17:03:00 +08:00

570 lines
19 KiB
Python

"""
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