You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
255 lines
9.2 KiB
255 lines
9.2 KiB
2 years ago
|
import sys
|
||
|
import os
|
||
|
import pathlib
|
||
|
import json
|
||
|
import textwrap
|
||
|
import argparse
|
||
|
|
||
|
import numpy
|
||
|
from scipy.spatial.transform import Rotation
|
||
|
|
||
|
import pymesh
|
||
|
|
||
|
sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / "tools" / "fixtures")) # noqa
|
||
|
import fixture_utils
|
||
|
|
||
|
|
||
|
def convert_to_ipc_msh(input_path, output_path, remesh=False):
|
||
|
input_mesh = pymesh.load_mesh(str(input_path))
|
||
|
if input_mesh.num_voxels == 0: # tetrahedralize the mesh
|
||
|
tetgen = pymesh.tetgen()
|
||
|
tetgen.points = input_mesh.vertices
|
||
|
tetgen.triangles = input_mesh.faces
|
||
|
tetgen.split_boundary = remesh
|
||
|
tetgen.max_radius_edge_ratio = 2.0
|
||
|
tetgen.min_dihedral_angle = 0.0
|
||
|
tetgen.merge_coplanar = remesh
|
||
|
tetgen.run()
|
||
|
output_mesh = tetgen.mesh
|
||
|
# Reorient faces
|
||
|
faces = output_mesh.faces.copy()
|
||
|
faces[:, [1, 2]] = faces[:, [2, 1]]
|
||
|
output_mesh = pymesh.form_mesh(
|
||
|
output_mesh.vertices, faces, output_mesh.voxels)
|
||
|
else: # mesh is already a tet mesh, just convert to IPC compatible
|
||
|
output_mesh = input_mesh
|
||
|
|
||
|
if remesh:
|
||
|
pymesh.save_mesh(
|
||
|
str(output_path.with_suffix(".obj")), output_mesh, ascii=True)
|
||
|
|
||
|
with open(output_path, mode='w') as f:
|
||
|
f.write("$MeshFormat\n4 0 8\n$EndMeshFormat\n")
|
||
|
f.write("$Entities\n0 0 0 1\n")
|
||
|
f.write("0 {:g} {:g} {:g} {:g} {:g} {:g} 0 0\n".format(
|
||
|
*output_mesh.nodes.min(axis=0), *output_mesh.nodes.max(axis=0)))
|
||
|
f.write("$EndEntities\n")
|
||
|
f.write("$Nodes\n")
|
||
|
f.write("1 {0:d}\n0 3 0 {0:d}\n".format(output_mesh.num_nodes))
|
||
|
for i, node in enumerate(output_mesh.nodes):
|
||
|
f.write("{:d} {:g} {:g} {:g}\n".format(i + 1, *node))
|
||
|
f.write("$EndNodes\n")
|
||
|
|
||
|
f.write("$Elements\n")
|
||
|
f.write("1 {0:d}\n0 3 4 {0:d}\n".format(output_mesh.num_elements))
|
||
|
for i, element in enumerate(output_mesh.elements):
|
||
|
f.write("{:d} {:d} {:d} {:d} {:d}\n".format(i + 1, *(element + 1)))
|
||
|
f.write("$EndElements\n")
|
||
|
|
||
|
f.write("$Surface\n")
|
||
|
f.write("{:d}\n".format(output_mesh.num_faces))
|
||
|
for face in output_mesh.faces:
|
||
|
f.write("{:d} {:d} {:d}\n".format(*(face + 1)))
|
||
|
f.write("$EndSurface\n")
|
||
|
|
||
|
|
||
|
def convert_to_obj(input_path, output_path):
|
||
|
pymesh.save_mesh(str(output_path), pymesh.load_mesh(str(input_path)))
|
||
|
|
||
|
|
||
|
def fixture_to_ipc_script(fixture, output_path, remesh=False):
|
||
|
timestep = fixture.get("timestep", 0.01)
|
||
|
|
||
|
if "max_time" in fixture:
|
||
|
max_time = fixture["max_time"]
|
||
|
elif "max_iterations" in fixture:
|
||
|
max_time = fixture["max_iterations"] * timestep
|
||
|
else:
|
||
|
max_time = 10
|
||
|
|
||
|
dhat = fixture.get("distance_barrier_constraint", {}).get(
|
||
|
"initial_barrier_activation_distance", 1e-3)
|
||
|
|
||
|
bodies = fixture["rigid_body_problem"]["rigid_bodies"]
|
||
|
|
||
|
is_remeshed = {}
|
||
|
|
||
|
shapes = []
|
||
|
disabled_shapes = []
|
||
|
for body in bodies:
|
||
|
if "type" in body:
|
||
|
is_static = body["type"] == "static"
|
||
|
elif "is_dof_fixed" in body:
|
||
|
is_static = (body["is_dof_fixed"] if isinstance(
|
||
|
body["is_dof_fixed"], bool) else all(body["is_dof_fixed"]))
|
||
|
else:
|
||
|
is_static = False
|
||
|
|
||
|
is_kinematic = body.get("type", "dynamic") == "kinematic"
|
||
|
|
||
|
surface_mesh_path = pathlib.Path(body["mesh"])
|
||
|
if not surface_mesh_path.exists():
|
||
|
surface_mesh_path = (
|
||
|
fixture_utils.get_meshes_dir_path() / surface_mesh_path)
|
||
|
if not is_static:
|
||
|
try:
|
||
|
msh_path = surface_mesh_path.with_suffix('.msh').resolve()
|
||
|
msh_path = (
|
||
|
pathlib.Path(__file__).resolve().parent / "meshes" /
|
||
|
msh_path.relative_to(fixture_utils.get_meshes_dir_path()))
|
||
|
msh_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
except:
|
||
|
msh_path = surface_mesh_path.with_suffix('.msh')
|
||
|
if not msh_path.exists() or is_remeshed.get(surface_mesh_path, remesh):
|
||
|
convert_to_ipc_msh(surface_mesh_path, msh_path, remesh)
|
||
|
is_remeshed[surface_mesh_path] = remesh
|
||
|
mesh_path = msh_path
|
||
|
elif surface_mesh_path.suffix != ".obj":
|
||
|
try:
|
||
|
obj_path = surface_mesh_path.with_suffix('.obj').resolve()
|
||
|
obj_path = (
|
||
|
pathlib.Path(__file__).resolve().parent / "meshes" /
|
||
|
obj_path.relative_to(fixture_utils.get_meshes_dir_path()))
|
||
|
obj_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
except:
|
||
|
obj_path = surface_mesh_path.with_suffix('.obj')
|
||
|
if not obj_path.exists():
|
||
|
convert_to_obj(surface_mesh_path, obj_path)
|
||
|
mesh_path = obj_path
|
||
|
else:
|
||
|
mesh_path = surface_mesh_path
|
||
|
|
||
|
mesh_path = os.path.relpath(
|
||
|
mesh_path.resolve(), output_path.parent.resolve())
|
||
|
|
||
|
if "dimensions" in body:
|
||
|
vertices = pymesh.load_mesh(str(surface_mesh_path)).vertices
|
||
|
initial_dimensions = abs(
|
||
|
vertices.max(axis=0) - vertices.min(axis=0))
|
||
|
initial_dimensions[initial_dimensions == 0] = 1
|
||
|
scale = numpy.array(body["dimensions"], dtype=float)
|
||
|
scale /= initial_dimensions
|
||
|
else:
|
||
|
scale = body.get("scale", [1, 1, 1])
|
||
|
if isinstance(scale, int) or isinstance(scale, float):
|
||
|
scale = [scale] * 3
|
||
|
|
||
|
if "rotation" in body:
|
||
|
rotation = Rotation.from_euler(
|
||
|
"xyz", body["rotation"], degrees=True).as_euler("zyx", degrees=True)[::-1]
|
||
|
else:
|
||
|
rotation = [0, 0, 0]
|
||
|
|
||
|
linear_velocity = body.get("linear_velocity", [0, 0, 0])
|
||
|
angular_velocity = body.get("angular_velocity", [0, 0, 0])
|
||
|
force = body.get("force", [0, 0, 0])
|
||
|
if "force" in body:
|
||
|
nbc = (" NBC -1e300 -1e300 -1e300 1e300 1e300 1e300 "
|
||
|
" {:g} {:g} {:g}".format(*body["force"]))
|
||
|
else:
|
||
|
nbc = ""
|
||
|
if "torque" in body:
|
||
|
print("External torque is not supported in IPC! Dropping it!")
|
||
|
|
||
|
if is_static:
|
||
|
velocity_str = "linearVelocity 0 0 0"
|
||
|
elif is_kinematic:
|
||
|
velocity_str = (
|
||
|
"linearVelocity {:g} {:g} {:g} angularVelocity {:g} {:g} {:g}".format(
|
||
|
*linear_velocity, *angular_velocity))
|
||
|
else:
|
||
|
velocity_str = (
|
||
|
"initVel {:g} {:g} {:g} {:g} {:g} {:g}".format(
|
||
|
*linear_velocity, *angular_velocity))
|
||
|
|
||
|
body_line = "{} {:g} {:g} {:g} {:g} {:g} {:g} {:g} {:g} {:g} material {:g} 2e11 0.3 {}{}".format(
|
||
|
mesh_path, *body.get("position", [0, 0, 0]),
|
||
|
*rotation, *scale, body.get("density", 1000), velocity_str, nbc)
|
||
|
is_enabled = (body.get("enabled", True) or body.get(
|
||
|
"ipc_comparison_enabled", False))
|
||
|
if is_enabled:
|
||
|
shapes.append(body_line)
|
||
|
else:
|
||
|
disabled_shapes.append(f"# {body_line}")
|
||
|
|
||
|
epsv = fixture.get("friction_constraints", {}).get(
|
||
|
"static_friction_speed_bound", 1e-3)
|
||
|
friction_iterations = fixture.get(
|
||
|
"friction_constraints", {}).get("iterations", 1)
|
||
|
|
||
|
gravity = ""
|
||
|
if sum(fixture["rigid_body_problem"].get("gravity", [0, 0, 0])) == 0:
|
||
|
gravity = "turnOffGravity"
|
||
|
|
||
|
# velocity_conv_tol = fixture.get(
|
||
|
# "newton_solver", {}).get("velocity_conv_tol", 1e-2)
|
||
|
velocity_conv_tol = fixture.get(
|
||
|
"ipc_solver", {}).get("velocity_conv_tol", 1e-2)
|
||
|
|
||
|
return textwrap.dedent(f"""\
|
||
|
energy NH
|
||
|
warmStart 0
|
||
|
time {max_time} {timestep}
|
||
|
{gravity}
|
||
|
|
||
|
shapes input {len(shapes)}
|
||
|
{{}}
|
||
|
{{}}
|
||
|
|
||
|
selfCollisionOn
|
||
|
selfFric {fixture["rigid_body_problem"].get("coefficient_friction", 0)}
|
||
|
|
||
|
constraintSolver interiorPoint
|
||
|
dHat {dhat}
|
||
|
epsv {epsv}
|
||
|
useAbsParameters
|
||
|
fricIterAmt {friction_iterations}
|
||
|
tol 1
|
||
|
{velocity_conv_tol}
|
||
|
# useAbsTol
|
||
|
""").format("\n".join(shapes), "\n".join(disabled_shapes))
|
||
|
|
||
|
|
||
|
def main():
|
||
|
parser = argparse.ArgumentParser(
|
||
|
description="Convert a rigid fixture to a IPC script")
|
||
|
parser.add_argument(
|
||
|
"-i", "--input", metavar="path/to/input", type=pathlib.Path,
|
||
|
dest="input", default=None, help="path to input json(s)", nargs="+")
|
||
|
parser.add_argument(
|
||
|
"--remesh", action="store_true", default=False, help="remesh meshes")
|
||
|
args, _ = parser.parse_known_args()
|
||
|
|
||
|
for input in args.input:
|
||
|
print(f"Converting {input}")
|
||
|
try:
|
||
|
output = input.with_suffix(".txt").resolve()
|
||
|
output = (
|
||
|
pathlib.Path(__file__).resolve().parent / "scripts" /
|
||
|
output.relative_to(fixture_utils.get_fixture_dir_path()))
|
||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||
|
except:
|
||
|
output = input.with_suffix(".txt")
|
||
|
|
||
|
with open(input) as input_file:
|
||
|
fixture = json.load(input_file)
|
||
|
|
||
|
ipc_script = fixture_to_ipc_script(fixture, output, args.remesh)
|
||
|
|
||
|
with open(output, 'w') as ipc_script_file:
|
||
|
ipc_script_file.write(ipc_script)
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|