413 lines
15 KiB
Python
413 lines
15 KiB
Python
"""Routines to run cloth simulation"""
|
|
|
|
# Basic
|
|
import time
|
|
import multiprocessing
|
|
import platform
|
|
import signal
|
|
from pathlib import Path
|
|
|
|
# BoxMeshGen
|
|
import pygarment.meshgen.boxmeshgen as bmg
|
|
from pygarment.meshgen.boxmeshgen import BoxMesh
|
|
from pygarment.meshgen.sim_config import PathCofig
|
|
|
|
# Warp simulation
|
|
from pygarment.meshgen.simulation import run_sim
|
|
|
|
|
|
def batch_sim(data_path, output_path, dataset_props,
|
|
run_default_body=False, 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:
|
|
* data_path -- path to folder with patterns (for given body type)
|
|
* output_path -- path to folder with the sumulated dataset
|
|
* dataset_props -- dataset properties. Properties has to be of custom data_config.Properties() class and contain
|
|
* dataset folder (inside data_path)
|
|
* type of dataset structure (with/without subfolders for patterns)
|
|
* list of processed samples if processing of dataset was already attempted
|
|
* Simulation parameters
|
|
* Rendering parameters
|
|
Other needed properties will be files with default values if the corresponding sections
|
|
are not found in props object
|
|
* run_default_body -- runs the dataset on the default body (disabled by default)
|
|
* 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)
|
|
body_type = 'default_body' if run_default_body else 'random_body'
|
|
data_props_file = output_path / f'dataset_properties_{body_type}.yaml'
|
|
pattern_names = _get_pattern_names(data_path)
|
|
|
|
# Simulate every template
|
|
count = 0
|
|
for pattern_name in pattern_names:
|
|
# skip processed cases -- in case of resume. First condition needed to skip checking second one on False =)
|
|
if resume and pattern_name in dataset_props['sim']['stats']['processed']:
|
|
print(f'Skipped as already processed {pattern_name}')
|
|
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
|
|
|
|
try:
|
|
paths = PathCofig(
|
|
in_element_path=data_path / pattern_name,
|
|
out_path=output_path,
|
|
in_name=pattern_name,
|
|
body_name=dataset_props['body_default'],
|
|
samples_name=dataset_props['body_samples'],
|
|
default_body=run_default_body
|
|
)
|
|
except BaseException as e:
|
|
# Not all files available
|
|
print("***Pattern loading failed (paths)***")
|
|
dataset_props.add_fail('sim', 'crashes', pattern_name)
|
|
else:
|
|
template_simulation(paths, dataset_props, caching=caching)
|
|
|
|
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(f'\nFinished batch of {data_path}')
|
|
try:
|
|
if len(dataset_props['sim']['stats']['processed']) >= len(pattern_names):
|
|
# 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
|
|
|
|
|
|
def resim_fails(data_path, output_path, dataset_props,
|
|
run_default_body=False, caching=False):
|
|
"""Resimulate failure cases -- maybe some of them would get fixed"""
|
|
|
|
print('************** RESIMULATING FAILS ****************')
|
|
|
|
sim_stats = dataset_props['sim']['stats']
|
|
|
|
# Collect fails and remove them from fails list if any
|
|
fails = sim_stats['fails']
|
|
to_resim = set()
|
|
for key in fails:
|
|
if key not in ['cloth_body_intersection', 'cloth_self_intersection']:
|
|
for el in fails[key]:
|
|
to_resim.add(el)
|
|
fails[key] = [] # NOTE: If nothing to be added in this key, it was already an empty array (and nothing changed)
|
|
|
|
if not len(to_resim):
|
|
# Return previous finished state
|
|
return dataset_props['frozen'] if 'frozen' in dataset_props else False
|
|
|
|
if 'processed' not in sim_stats:
|
|
sim_stats['processed'] = _get_pattern_names(data_path)
|
|
dataset_props['frozen'] = False
|
|
|
|
# Remove fails from processed to trigger re-simulation
|
|
for sample in to_resim:
|
|
sim_stats['processed'].remove(sample)
|
|
|
|
# Start simulation again
|
|
finished = batch_sim(
|
|
data_path, output_path, dataset_props,
|
|
run_default_body=run_default_body,
|
|
num_samples=len(to_resim)+1,
|
|
caching=caching,
|
|
force_restart=False
|
|
)
|
|
|
|
return 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 whether current simulation is a resumed last one
|
|
"""
|
|
if 'sim' not in props:
|
|
props.set_section_config(
|
|
'sim',
|
|
max_sim_steps=1000, #affects speed
|
|
max_meshgen_time=20, #in seconds, affects speed
|
|
max_frame_time= 15, #in seconds, affects speed
|
|
max_sim_time= 1500, #in seconds, affects speed
|
|
zero_gravity_steps=10, # 0.01 # depends on the units used, #affects speed
|
|
static_threshold=0.03, #affects speed
|
|
non_static_percent=1.5, #affects speed
|
|
max_body_collisions=0,
|
|
max_self_collisions=0,
|
|
resolution_scale=1.0, #affects speed
|
|
ground=False, # Do not add floor s.t. garment falls infinitely if falls
|
|
)
|
|
|
|
if 'material' not in props['sim']['config']:
|
|
props['sim']['config']['material'] = {
|
|
'garment_tri_ka': 10000.0,
|
|
|
|
'garment_edge_ke': 1.0,
|
|
'garment_tri_ke': 10000.0,
|
|
'spring_ke': 50000.0,
|
|
|
|
'garment_edge_kd': 10.0,
|
|
'garment_tri_kd': 1.0,
|
|
'spring_kd': 10.0,
|
|
|
|
'fabric_density': 1.0,
|
|
'fabric_thickness': 0.1,
|
|
'fabric_friction': 0.5
|
|
|
|
}
|
|
|
|
if 'options' not in props['sim']['config']:
|
|
props['sim']['config']['options'] = {
|
|
'enable_particle_particle_collisions': False,
|
|
'enable_triangle_particle_collisions': True,
|
|
'enable_edge_edge_collisions': True,
|
|
'enable_body_collision_filters': True,
|
|
|
|
'enable_attachment_constraint': True,
|
|
'attachment_frames': 400,
|
|
'attachment_label_names': ['lower_interface'],
|
|
'attachment_stiffness': [1000.],
|
|
'attachment_damping': [10.],
|
|
|
|
'global_damping_factor': 0.25,
|
|
'global_damping_effective_velocity': 0.0,
|
|
'global_max_velocity': 25.0,
|
|
|
|
'enable_global_collision_filter': True,
|
|
'enable_cloth_reference_drag': False,
|
|
'cloth_reference_margin': 0.1,
|
|
|
|
# FIXME Re-writes mesh references causing occasional CUDA errors when referencing meshes other than the body
|
|
'enable_body_smoothing': False,
|
|
'smoothing_total_smoothing_factor': 1.0,
|
|
'smoothing_recover_start_frame': 150,
|
|
'smoothing_num_steps': 100,
|
|
'smoothing_frame_gap_between_steps': 1,
|
|
|
|
'body_collision_thickness': 0.25,
|
|
'body_friction': 0.5
|
|
}
|
|
|
|
if 'render' not in props:
|
|
# init with defaults
|
|
props.set_section_config(
|
|
'render',
|
|
resolution=[800, 800],
|
|
sides=['front','back'],
|
|
front_camera_location=None,
|
|
uv_texture={
|
|
'seam_width': 0.5,
|
|
'dpi': 1500,
|
|
'fabric_grain_texture_path': None,
|
|
'fabric_grain_resolution': 5,
|
|
}
|
|
)
|
|
|
|
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]
|
|
|
|
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']:
|
|
# add to simulation failures
|
|
# Remove last from processed if it did not crash
|
|
if last_processed not in props['sim']['stats']['stop_over']:
|
|
props['sim']['stats']['processed'].pop()
|
|
else:
|
|
# Already passed here once -> add as crash
|
|
props['sim']['stats']['fails']['crashes'].append(last_processed)
|
|
|
|
props['sim']['stats']['stop_over'].append(last_processed) # indicate resuming dataset simulation
|
|
|
|
|
|
return True
|
|
|
|
# else new life
|
|
# Prepare commulative stats
|
|
props.set_section_stats('sim',
|
|
fails={},
|
|
meshgen_time={},
|
|
sim_time={},
|
|
spf={},
|
|
fin_frame={},
|
|
face_count={},
|
|
body_collisions={},
|
|
self_collisions={})
|
|
props['sim']['stats']['fails'] = {
|
|
'crashes': [],
|
|
'cloth_body_intersection': [],
|
|
'cloth_self_intersection': [],
|
|
'static_equilibrium': [],
|
|
'fast_finish': [],
|
|
'pattern_loading': [],
|
|
'multi_stitching': [],
|
|
'gt_edges_creation': []
|
|
|
|
}
|
|
|
|
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(paths: PathCofig, props, caching=False):
|
|
"""
|
|
Simulate given template within given scene & save log files
|
|
"""
|
|
sim_props = props['sim']
|
|
res = sim_props['config']['resolution_scale']
|
|
|
|
garment = BoxMesh(paths.in_g_spec, res)
|
|
|
|
print('\n-----------------------------'
|
|
'\nLoading garment: ', garment.name)
|
|
|
|
meshgen_start_time = time.time()
|
|
timeout_after = int(get_dict_default_value(sim_props['config'], 'max_meshgen_time', 20))
|
|
|
|
try:
|
|
_load_boxmesh_timeout(garment, timeout_after)
|
|
except TimeoutError as e:
|
|
print(e)
|
|
failure_case = 'meshgen-timeout'
|
|
props.add_fail('sim', failure_case, garment.name)
|
|
except bmg.PatternLoadingError as e:
|
|
# record error and skip subequent processing
|
|
print(e)
|
|
failure_case = 'pattern_loading'
|
|
props.add_fail('sim', failure_case, garment.name)
|
|
except bmg.DegenerateTrianglesError as e:
|
|
print(e)
|
|
failure_case = 'degenerate_triangles'
|
|
props.add_fail('sim', failure_case, garment.name)
|
|
except bmg.MultiStitchingError as e:
|
|
print(e)
|
|
failure_case = 'multi_stitching'
|
|
props.add_fail('sim', failure_case, garment.name)
|
|
except bmg.NormError as e:
|
|
print(e)
|
|
failure_case = 'norm_error'
|
|
props.add_fail('sim', failure_case, garment.name)
|
|
except bmg.StitchingError as e:
|
|
print(e)
|
|
failure_case = 'stitching_error'
|
|
props.add_fail('sim', failure_case, garment.name)
|
|
except BaseException as e: # Catch the rest of exceptions
|
|
print("***Pattern loading failed due to unknown error***")
|
|
print(e)
|
|
failure_case = 'crashes'
|
|
props.add_fail('sim', failure_case, garment.name)
|
|
else:
|
|
# garment.save_mesh(tag='stitched') # Saving the geometry before eny forces were applied
|
|
sim_props['stats']['meshgen_time'][garment.name] = time.time() - meshgen_start_time
|
|
sim_props['stats']['face_count'][garment.name] = len(garment.faces)
|
|
sim_props_option = sim_props['config']['options']
|
|
|
|
vertex_normals = get_dict_default_value(sim_props_option,'store_vertex_normals',False)
|
|
store_panels = get_dict_default_value(sim_props_option,'store_panels',False)
|
|
garment.serialize(
|
|
paths,
|
|
with_v_norms=vertex_normals,
|
|
store_panels=store_panels,
|
|
uv_config=props['render']['config']['uv_texture']
|
|
)
|
|
|
|
run_sim(
|
|
garment.name,
|
|
props,
|
|
paths,
|
|
save_v_norms=vertex_normals,
|
|
store_usd=caching, # NOTE: False for fast simulation!,
|
|
optimize_storage=sim_props['config']['optimize_storage'],
|
|
verbose=False
|
|
)
|
|
|
|
def _load_boxmesh_timeout(garment, timeout_after):
|
|
if platform.system() == "Windows":
|
|
"""https://stackoverflow.com/a/14920854"""
|
|
p = multiprocessing.Process(target=garment.load(), name="GarmentGeneration")
|
|
p.start()
|
|
|
|
# Wait timeout_after seconds for garment.load()
|
|
time.sleep(timeout_after)
|
|
|
|
# If thread is active
|
|
if p.is_alive():
|
|
# Terminate the process
|
|
p.terminate()
|
|
p.join()
|
|
raise TimeoutError
|
|
|
|
elif platform.system() in ["Linux", "OSX"]:
|
|
"""https://code-maven.com/python-timeout"""
|
|
def alarm_handler(signum, frame):
|
|
raise TimeoutError
|
|
|
|
signal.signal(signal.SIGALRM, alarm_handler)
|
|
signal.alarm(timeout_after)
|
|
s_time = time.time()
|
|
try:
|
|
garment.load()
|
|
except TimeoutError as ex:
|
|
raise TimeoutError
|
|
else:
|
|
e_time = time.time() - s_time
|
|
# print("No timeout error with time: ",e_time)
|
|
signal.alarm(0)
|
|
|
|
|
|
def get_dict_default_value(props, name, default_value):
|
|
if name in props:
|
|
return props[name]
|
|
return default_value
|
|
|
|
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_names(data_path: Path):
|
|
names = []
|
|
to_ignore = ['renders'] # special dirs not to include in the pattern list
|
|
for el in data_path.iterdir():
|
|
if el.is_dir() and el.stem not in to_ignore:
|
|
names.append(el.stem)
|
|
|
|
return names |