Files
2026-03-17 14:58:51 -06:00

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'}