187 lines
7.5 KiB
Python
187 lines
7.5 KiB
Python
import os
|
|
import math
|
|
|
|
import bpy
|
|
import bmesh
|
|
import mathutils
|
|
|
|
from .lib import Quadwild, QWException
|
|
from .util import bisect, exporter, importer
|
|
|
|
|
|
class QREMESH_OT_Remesh(bpy.types.Operator):
|
|
"""Remesh with Quadwild"""
|
|
bl_idname = "qremeshify.remesh"
|
|
bl_label = "Remesh"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
|
|
def execute(self, ctx):
|
|
props = ctx.scene.quadwild_props
|
|
qr_props = ctx.scene.quadpatches_props
|
|
selected_objs = ctx.selected_objects
|
|
|
|
if len(selected_objs) == 0:
|
|
self.report({'ERROR_INVALID_INPUT'}, "No selected objects")
|
|
return {'CANCELLED'}
|
|
|
|
if len(selected_objs) > 1:
|
|
self.report({'INFO'}, "Multiple objects selected, will only operate on the first selected object")
|
|
|
|
obj = selected_objs[0]
|
|
if obj is None or obj.type != 'MESH':
|
|
self.report({'ERROR_INVALID_INPUT'}, "Object is not a mesh")
|
|
return {'CANCELLED'}
|
|
|
|
if len(obj.data.polygons) == 0:
|
|
self.report({'ERROR_INVALID_INPUT'}, "Mesh has 0 faces")
|
|
return {'CANCELLED'}
|
|
|
|
original_location = obj.location
|
|
|
|
mesh_filename = "".join(c if c not in "\/:*?<>|" else "_" for c in obj.name).strip()
|
|
mesh_filepath = f"{os.path.join(bpy.app.tempdir, mesh_filename)}.obj"
|
|
self.report({'DEBUG'}, f"Remeshing from {mesh_filepath}")
|
|
|
|
# Load lib
|
|
qw = Quadwild(mesh_filepath)
|
|
|
|
try:
|
|
if not props.useCache:
|
|
# Get mesh after modifiers and shapekeys applied
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
evaluated_obj = obj.evaluated_get(depsgraph)
|
|
mesh = evaluated_obj.to_mesh()
|
|
|
|
# Create a bmesh from mesh
|
|
# (won't affect mesh, unless explicitly written back)
|
|
bm = bmesh.new()
|
|
bm.from_mesh(mesh)
|
|
|
|
# Apply only rotation and scale
|
|
if evaluated_obj.rotation_mode == 'QUATERNION':
|
|
matrix = mathutils.Matrix.LocRotScale(None, evaluated_obj.rotation_quaternion, evaluated_obj.scale)
|
|
else:
|
|
matrix = mathutils.Matrix.LocRotScale(None, evaluated_obj.rotation_euler, evaluated_obj.scale)
|
|
bmesh.ops.transform(bm, matrix=matrix, verts=bm.verts)
|
|
|
|
# Bisect to prep for symmetry
|
|
if props.symmetryX or props.symmetryY or props.symmetryZ:
|
|
bisect.bisect_on_axes(bm, props.symmetryX, props.symmetryY, props.symmetryZ)
|
|
|
|
# Find edges to mark as sharp
|
|
if props.enableSharp:
|
|
face_set_data_layer = bm.faces.layers.int.get('.sculpt_face_set')
|
|
bm.edges.ensure_lookup_table()
|
|
for edge in bm.edges:
|
|
is_sharp = math.degrees(edge.calc_face_angle(0)) > props.sharpAngle
|
|
is_material_boundary = len(edge.link_faces) > 1 and edge.link_faces[0].material_index != edge.link_faces[1].material_index
|
|
is_face_set_boundary = (
|
|
face_set_data_layer is not None and
|
|
len(edge.link_faces) > 1 and
|
|
edge.link_faces[0][face_set_data_layer] != edge.link_faces[1][face_set_data_layer]
|
|
)
|
|
|
|
if is_sharp or edge.is_boundary or edge.seam or is_material_boundary or is_face_set_boundary:
|
|
edge.smooth = False
|
|
|
|
# Triangulate mesh
|
|
bmesh.ops.triangulate(bm, faces=bm.faces, quad_method='SHORT_EDGE', ngon_method='BEAUTY')
|
|
|
|
# Export selected object as OBJ
|
|
exporter.export_mesh(bm, mesh_filepath)
|
|
|
|
# Calculate sharp features
|
|
if props.enableSharp:
|
|
num_sharp_features = exporter.export_sharp_features(bm, qw.sharp_path, props.sharpAngle)
|
|
self.report({'DEBUG'}, f"Found {num_sharp_features} sharp edges")
|
|
|
|
# Remesh and calculate field
|
|
qw.remeshAndField(remesh=props.enableRemesh, enableSharp=props.enableSharp, sharpAngle=props.sharpAngle)
|
|
if props.debug:
|
|
new_mesh = importer.import_mesh(qw.remeshed_path)
|
|
new_obj = bpy.data.objects.new(f"{obj.name} remeshAndField", new_mesh)
|
|
bpy.context.collection.objects.link(new_obj)
|
|
new_obj.hide_set(True)
|
|
|
|
# Trace
|
|
qw.trace()
|
|
if props.debug:
|
|
new_mesh = importer.import_mesh(qw.traced_path)
|
|
new_obj = bpy.data.objects.new(f"{obj.name} trace", new_mesh)
|
|
bpy.context.collection.objects.link(new_obj)
|
|
new_obj.hide_set(True)
|
|
|
|
# Convert to quads
|
|
qw.quadrangulate(
|
|
props.enableSmoothing,
|
|
qr_props.scaleFact,
|
|
qr_props.fixedChartClusters,
|
|
|
|
qr_props.alpha,
|
|
qr_props.ilpMethod,
|
|
qr_props.timeLimit,
|
|
qr_props.gapLimit,
|
|
qr_props.minimumGap,
|
|
qr_props.isometry,
|
|
qr_props.regularityQuadrilaterals,
|
|
qr_props.regularityNonQuadrilaterals,
|
|
qr_props.regularityNonQuadrilateralsWeight,
|
|
qr_props.alignSingularities,
|
|
qr_props.alignSingularitiesWeight,
|
|
qr_props.repeatLosingConstraintsIterations,
|
|
qr_props.repeatLosingConstraintsQuads,
|
|
qr_props.repeatLosingConstraintsNonQuads,
|
|
qr_props.repeatLosingConstraintsAlign,
|
|
qr_props.hardParityConstraint,
|
|
|
|
qr_props.flowConfig,
|
|
qr_props.satsumaConfig,
|
|
|
|
qr_props.callbackTimeLimit,
|
|
qr_props.callbackGapLimit,
|
|
)
|
|
if props.debug and props.enableSmoothing:
|
|
new_mesh = importer.import_mesh(qw.output_path)
|
|
new_obj = bpy.data.objects.new(f"{obj.name} quadrangulate", new_mesh)
|
|
bpy.context.collection.objects.link(new_obj)
|
|
new_obj.hide_set(True)
|
|
|
|
# Import final OBJ
|
|
final_mesh_path = qw.output_smoothed_path if props.enableSmoothing else qw.output_path
|
|
final_mesh = importer.import_mesh(final_mesh_path)
|
|
final_obj = bpy.data.objects.new(f"{obj.name} Remeshed", final_mesh)
|
|
bpy.context.collection.objects.link(final_obj)
|
|
bpy.context.view_layer.objects.active = final_obj
|
|
final_obj.select_set(True)
|
|
|
|
# Set object location
|
|
final_obj.location = original_location
|
|
|
|
# Add Mirror modifier for symmetry
|
|
if props.symmetryX or props.symmetryY or props.symmetryZ:
|
|
mirror_modifier = final_obj.modifiers.new("Mirror", "MIRROR")
|
|
|
|
mirror_modifier.use_axis[0] = props.symmetryX
|
|
mirror_modifier.use_axis[1] = props.symmetryY
|
|
mirror_modifier.use_axis[2] = props.symmetryZ
|
|
mirror_modifier.use_clip = True
|
|
mirror_modifier.merge_threshold = 0.001
|
|
|
|
# Hide original
|
|
obj.hide_set(True)
|
|
|
|
except QWException as e:
|
|
self.report({'ERROR'}, repr(e))
|
|
|
|
finally:
|
|
# Cleanup
|
|
del qw
|
|
|
|
if not props.useCache:
|
|
bm.free()
|
|
del bm
|
|
evaluated_obj.to_mesh_clear()
|
|
|
|
return {'FINISHED'}
|