2025-12-01
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
bl_info = {
|
||||
"name": "QRemeshify",
|
||||
"description": "Remesher with good-quality quad topology",
|
||||
"author": "ksami",
|
||||
"version": (1, 1, 0),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "View3D",
|
||||
"category": "Mesh",
|
||||
}
|
||||
|
||||
|
||||
import bpy
|
||||
from .operator import QREMESH_OT_Remesh
|
||||
from .props import QWPropertyGroup, QRPropertyGroup
|
||||
from .ui import QREMESH_PT_UIPanel, QREMESH_PT_UIAdvancedPanel, QREMESH_PT_UICallbackPanel
|
||||
|
||||
|
||||
classes = [
|
||||
QWPropertyGroup,
|
||||
QRPropertyGroup,
|
||||
QREMESH_PT_UIPanel,
|
||||
QREMESH_PT_UIAdvancedPanel,
|
||||
QREMESH_PT_UICallbackPanel,
|
||||
QREMESH_OT_Remesh,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.Scene.quadwild_props = bpy.props.PointerProperty(type=QWPropertyGroup)
|
||||
bpy.types.Scene.quadpatches_props = bpy.props.PointerProperty(type=QRPropertyGroup)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
del bpy.types.Scene.quadwild_props
|
||||
del bpy.types.Scene.quadpatches_props
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
@@ -0,0 +1,70 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
# Example of manifest file for a Blender extension
|
||||
# Change the values according to your extension
|
||||
id = "qremeshify"
|
||||
version = "1.1.0"
|
||||
name = "QRemeshify"
|
||||
tagline = "Remesher with good-quality quad topology"
|
||||
maintainer = "ksami"
|
||||
# Supported types: "add-on", "theme"
|
||||
type = "add-on"
|
||||
|
||||
# Optional link to documentation, support, source files, etc
|
||||
website = "https://github.com/ksami/QRemeshify"
|
||||
|
||||
# Optional list defined by Blender and server, see:
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
||||
tags = ["Mesh"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
# # Optional: Blender version that the extension does not support, earlier versions are supported.
|
||||
# # This can be omitted and defined later on the extensions platform if an issue is found.
|
||||
# blender_version_max = "5.1.0"
|
||||
|
||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
||||
license = [
|
||||
"SPDX:GPL-3.0-or-later",
|
||||
]
|
||||
# Optional: required by some licenses.
|
||||
# copyright = [
|
||||
# "2002-2024 Developer Name",
|
||||
# "1998 Company Name",
|
||||
# ]
|
||||
|
||||
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
||||
platforms = ["windows-x64", "macos-arm64", "linux-x64"]
|
||||
# Other supported platforms: "windows-arm64", "macos-x64"
|
||||
|
||||
# Optional: bundle 3rd party Python modules.
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
|
||||
# wheels = []
|
||||
|
||||
# # Optional: add-ons can list which resources they will require:
|
||||
# # * files (for access of any filesystem operations)
|
||||
# # * network (for internet access)
|
||||
# # * clipboard (to read and/or write the system clipboard)
|
||||
# # * camera (to capture photos and videos)
|
||||
# # * microphone (to capture audio)
|
||||
# #
|
||||
# # If using network, remember to also check `bpy.app.online_access`
|
||||
# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
|
||||
# #
|
||||
# # For each permission it is important to also specify the reason why it is required.
|
||||
# # Keep this a single short sentence without a period (.) at the end.
|
||||
# # For longer explanations use the documentation or detail page.
|
||||
#
|
||||
[permissions]
|
||||
files = "Import/export OBJ from/to disk"
|
||||
# network = "Need to sync motion-capture data to server"
|
||||
# clipboard = "Copy and paste bone transforms"
|
||||
|
||||
# Optional: build settings.
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
|
||||
[build]
|
||||
paths_exclude_pattern = [
|
||||
"__pycache__/",
|
||||
"/.git/",
|
||||
"/*.zip",
|
||||
]
|
||||
@@ -0,0 +1,150 @@
|
||||
import platform
|
||||
from ctypes import *
|
||||
from os import path
|
||||
from .data import Parameters, QRParameters, create_string, create_default_QRParameters
|
||||
|
||||
ilp_methods = {
|
||||
"LEASTSQUARES": 1,
|
||||
"ABS": 2,
|
||||
}
|
||||
|
||||
flow_config_files = {
|
||||
"SIMPLE": "config/main_config/flow_virtual_simple.json",
|
||||
"HALF": "config/main_config/flow_virtual_half.json",
|
||||
}
|
||||
|
||||
satsuma_config_files = {
|
||||
"DEFAULT": "config/satsuma/default.json",
|
||||
"MST": "config/satsuma/approx-mst.json",
|
||||
"ROUND2EVEN": "config/satsuma/approx-round2even.json",
|
||||
"SYMMDC": "config/satsuma/approx-symmdc.json",
|
||||
"EDGETHRU": "config/satsuma/edgethru.json",
|
||||
"LEMON": "config/satsuma/lemon.json",
|
||||
"NODETHRU": "config/satsuma/nodethru.json",
|
||||
}
|
||||
|
||||
class QWException(Exception):
|
||||
pass
|
||||
|
||||
class Quadwild():
|
||||
def __init__(self, mesh_path: str) -> None:
|
||||
if mesh_path is None or len(mesh_path) == 0:
|
||||
raise QWException("mesh_path is empty")
|
||||
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
quadwild_lib_filename = 'lib_quadwild.dll'
|
||||
quadpatches_lib_filename = 'lib_quadpatches.dll'
|
||||
elif system == "Darwin":
|
||||
quadwild_lib_filename = 'liblib_quadwild.dylib'
|
||||
quadpatches_lib_filename = 'liblib_quadpatches.dylib'
|
||||
else:
|
||||
quadwild_lib_filename = 'liblib_quadwild.so'
|
||||
quadpatches_lib_filename = 'liblib_quadpatches.so'
|
||||
|
||||
quadwild_lib_path = path.join(path.dirname(path.abspath(__file__)), quadwild_lib_filename)
|
||||
quadpatches_lib_path = path.join(path.dirname(path.abspath(__file__)), quadpatches_lib_filename)
|
||||
|
||||
self.quadwild = cdll.LoadLibrary(quadwild_lib_path)
|
||||
self.quadpatches = cdll.LoadLibrary(quadpatches_lib_path)
|
||||
|
||||
self.quadwild.remeshAndField2.argtypes = [POINTER(Parameters), c_char_p, c_char_p, c_char_p]
|
||||
self.quadwild.remeshAndField2.restype = None
|
||||
|
||||
self.quadwild.trace2.argtypes = [c_char_p]
|
||||
self.quadwild.trace2.restype = c_bool
|
||||
|
||||
self.quadpatches.quadPatches.argtypes = [c_char_p, POINTER(QRParameters), c_float, c_int, c_bool]
|
||||
self.quadpatches.quadPatches.restype = c_int
|
||||
|
||||
self.mesh_path = mesh_path
|
||||
self.mesh_path_without_ext, _ = path.splitext(mesh_path)
|
||||
self.sharp_path = f'{self.mesh_path_without_ext}_rem.sharp'
|
||||
self.field_path = f'{self.mesh_path_without_ext}_rem.rosy'
|
||||
self.remeshed_path = f'{self.mesh_path_without_ext}_rem.obj'
|
||||
self.traced_path = f'{self.mesh_path_without_ext}_rem_p0.obj'
|
||||
self.output_path = f'{self.mesh_path_without_ext}_rem_p0_0_quadrangulation.obj'
|
||||
self.output_smoothed_path = f'{self.mesh_path_without_ext}_rem_p0_0_quadrangulation_smooth.obj'
|
||||
|
||||
|
||||
def remeshAndField(self, remesh: bool, enableSharp: bool, sharpAngle: float) -> None:
|
||||
params = Parameters(
|
||||
remesh=remesh,
|
||||
sharpAngle=sharpAngle if enableSharp else -1,
|
||||
hasFeature=enableSharp,
|
||||
hasField=False,
|
||||
alpha=0.01, # Unused
|
||||
scaleFact=1, # Unused
|
||||
)
|
||||
mesh_filename_c = create_string(self.mesh_path)
|
||||
sharp_filename_c = create_string(self.sharp_path)
|
||||
field_filename_c = create_string(self.field_path)
|
||||
try:
|
||||
self.quadwild.remeshAndField2(byref(params), mesh_filename_c, sharp_filename_c, field_filename_c)
|
||||
except Exception as e:
|
||||
raise QWException("remeshAndField failed") from e
|
||||
|
||||
def trace(self) -> bool:
|
||||
remeshed_path_without_ext, _ = path.splitext(self.remeshed_path)
|
||||
filename_prefix_c = create_string(remeshed_path_without_ext)
|
||||
try:
|
||||
return self.quadwild.trace2(filename_prefix_c)
|
||||
except Exception as e:
|
||||
raise QWException("trace failed") from e
|
||||
|
||||
def quadrangulate(
|
||||
self,
|
||||
enableSmoothing: bool,
|
||||
scaleFact: float,
|
||||
fixedChartClusters: int,
|
||||
alpha: float,
|
||||
ilpMethod: str,
|
||||
timeLimit: int,
|
||||
gapLimit: float,
|
||||
minimumGap: float,
|
||||
isometry: bool,
|
||||
regularityQuadrilaterals: bool,
|
||||
regularityNonQuadrilaterals: bool,
|
||||
regularityNonQuadrilateralsWeight: float,
|
||||
alignSingularities: bool,
|
||||
alignSingularitiesWeight: float,
|
||||
repeatLosingConstraintsIterations: bool,
|
||||
repeatLosingConstraintsQuads: bool,
|
||||
repeatLosingConstraintsNonQuads: bool,
|
||||
repeatLosingConstraintsAlign: bool,
|
||||
hardParityConstraint: bool,
|
||||
flowConfig: str,
|
||||
satsumaConfig: str,
|
||||
callbackTimeLimit: list[float],
|
||||
callbackGapLimit: list[float],
|
||||
) -> int:
|
||||
params = create_default_QRParameters()
|
||||
|
||||
params.alpha = alpha
|
||||
params.ilpMethod = ilp_methods[ilpMethod]
|
||||
params.timeLimit = timeLimit
|
||||
params.gapLimit = gapLimit
|
||||
params.minimumGap = minimumGap
|
||||
params.isometry = isometry
|
||||
params.regularityQuadrilaterals = regularityQuadrilaterals
|
||||
params.regularityNonQuadrilaterals = regularityNonQuadrilaterals
|
||||
params.regularityNonQuadrilateralsWeight = regularityNonQuadrilateralsWeight
|
||||
params.alignSingularities = alignSingularities
|
||||
params.alignSingularitiesWeight = alignSingularitiesWeight
|
||||
params.repeatLosingConstraintsIterations = repeatLosingConstraintsIterations
|
||||
params.repeatLosingConstraintsQuads = repeatLosingConstraintsQuads
|
||||
params.repeatLosingConstraintsNonQuads = repeatLosingConstraintsNonQuads
|
||||
params.repeatLosingConstraintsAlign = repeatLosingConstraintsAlign
|
||||
params.hardParityConstraint = hardParityConstraint
|
||||
|
||||
params.flow_config_filename = path.join(path.dirname(path.abspath(__file__)), flow_config_files[flowConfig]).encode()
|
||||
params.satsuma_config_filename = path.join(path.dirname(path.abspath(__file__)), satsuma_config_files[satsumaConfig]).encode()
|
||||
|
||||
params.callbackTimeLimit = (c_float * len(callbackTimeLimit))(*callbackTimeLimit)
|
||||
params.callbackGapLimit = (c_float * len(callbackGapLimit))(*callbackGapLimit)
|
||||
|
||||
mesh_path_c = self.traced_path.encode()
|
||||
try:
|
||||
return self.quadpatches.quadPatches(mesh_path_c, byref(params), scaleFact, fixedChartClusters, enableSmoothing)
|
||||
except Exception as e:
|
||||
raise QWException("quadPatches failed") from e
|
||||
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 1
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 1
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_simple.json"
|
||||
satsuma_config_filename "config/satsuma/default.json"
|
||||
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_simple.json"
|
||||
satsuma_config_filename "config/satsuma/default.json"
|
||||
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_simple.json"
|
||||
satsuma_config_filename "config/satsuma/approx-mst.json"
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_simple.json"
|
||||
satsuma_config_filename "config/satsuma/approx-round2even.json"
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_simple.json"
|
||||
satsuma_config_filename "config/satsuma/approx-symmdc.json"
|
||||
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_simple.json"
|
||||
satsuma_config_filename "config/satsuma/edgethru.json"
|
||||
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_simple.json"
|
||||
satsuma_config_filename "config/satsuma/lemon.json"
|
||||
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_simple.json"
|
||||
satsuma_config_filename "config/satsuma/nodethru.json"
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"paired_half_target": "half",
|
||||
"paired_resolve_new_targets": false,
|
||||
"paired_initial": {
|
||||
"iso_weight": 0.5,
|
||||
"iso_objective": "abs",
|
||||
"unalign_weight": 1.0
|
||||
},
|
||||
"paired_resolve": {
|
||||
"iso_weight": 0.5,
|
||||
"iso_objective": "abs",
|
||||
"unalign_weight": 1.0
|
||||
},
|
||||
"paired_iso_weight": 0.5,
|
||||
"paired_iso_weight_resolve": 0.5
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 0.0
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 1
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 1
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 1
|
||||
flow_config_filename "config/main_config/flow_virtual_half.json"
|
||||
satsuma_config_filename "config/satsuma/default.json"
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"paired_half_target": "simple",
|
||||
"paired_resolve_new_targets": true,
|
||||
"paired_initial": {
|
||||
"iso_weight": 1,
|
||||
"iso_objective": "quad",
|
||||
"unalign_weight": 2.0
|
||||
},
|
||||
"paired_resolve": {
|
||||
"iso_weight": 1,
|
||||
"iso_objective": "abs",
|
||||
"unalign_weight": 4.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 1.1e-9
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 1
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 1
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 300
|
||||
useFlowSolver 0
|
||||
@@ -0,0 +1,21 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 200
|
||||
gapLimit 1.1e-9
|
||||
callbackTimeLimit 8 3.00 5.000 10.0 20.0 30.0 60.0 90.0 120.0
|
||||
callbackGapLimit 8 0.005 0.02 0.05 0.10 0.15 0.20 0.25 0.3
|
||||
minimumGap 0.4
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 300
|
||||
useFlowSolver 0
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
alpha 0.005
|
||||
ilpMethod 1
|
||||
timeLimit 86400
|
||||
gapLimit 1.1e-9
|
||||
callbackTimeLimit 0
|
||||
callbackGapLimit 0
|
||||
minimumGap 100
|
||||
isometry 1
|
||||
regularityQuadrilaterals 1
|
||||
regularityNonQuadrilaterals 1
|
||||
regularityNonQuadrilateralsWeight 0.9
|
||||
alignSingularities 0
|
||||
alignSingularitiesWeight 0.1
|
||||
repeatLosingConstraintsIterations 1
|
||||
repeatLosingConstraintsQuads 0
|
||||
repeatLosingConstraintsNonQuads 0
|
||||
repeatLosingConstraintsAlign 0
|
||||
hardParityConstraint 1
|
||||
scaleFact 1
|
||||
fixedChartClusters 0
|
||||
useFlowSolver 0
|
||||
@@ -0,0 +1,4 @@
|
||||
do_remesh 1
|
||||
sharp_feature_thr 35
|
||||
alpha 0.01
|
||||
scaleFact 1
|
||||
@@ -0,0 +1,4 @@
|
||||
do_remesh 1
|
||||
sharp_feature_thr 35
|
||||
alpha 0.01
|
||||
scaleFact 1
|
||||
@@ -0,0 +1,4 @@
|
||||
do_remesh 1
|
||||
sharp_feature_thr -1
|
||||
alpha 0.02
|
||||
scaleFact 1
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"double_cover": {
|
||||
"max_deviation": 5,
|
||||
"matching_solver": "Lemon",
|
||||
"evening_mode": "MST",
|
||||
"method": "HalfAsymmetric",
|
||||
"verbosity": 1
|
||||
},
|
||||
"refine_with_matching": false,
|
||||
"matching_solver": "Lemon",
|
||||
"refinement_maxdev_min": 2,
|
||||
"refinement_maxdev_max": 2,
|
||||
"deviation_limit": "NodeThroughflow",
|
||||
"verbosity": 2
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"double_cover": {
|
||||
"max_deviation": 5,
|
||||
"matching_solver": "Lemon",
|
||||
"evening_mode": "RoundToEven",
|
||||
"method": "HalfAsymmetric",
|
||||
"verbosity": 1
|
||||
},
|
||||
"refine_with_matching": false,
|
||||
"matching_solver": "Lemon",
|
||||
"refinement_maxdev_min": 2,
|
||||
"refinement_maxdev_max": 2,
|
||||
"deviation_limit": "NodeThroughflow",
|
||||
"verbosity": 2
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"double_cover": {
|
||||
"max_deviation": 5,
|
||||
"matching_solver": "Lemon",
|
||||
"evening_mode": "MST",
|
||||
"method": "HalfSymmetric",
|
||||
"verbosity": 1
|
||||
},
|
||||
"refine_with_matching": false,
|
||||
"matching_solver": "Lemon",
|
||||
"refinement_maxdev_min": 2,
|
||||
"refinement_maxdev_max": 2,
|
||||
"deviation_limit": "NodeThroughflow",
|
||||
"verbosity": 2
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"double_cover": {
|
||||
"max_deviation": 5,
|
||||
"matching_solver": "Lemon",
|
||||
"evening_mode": "MST",
|
||||
"method": "HalfAsymmetric",
|
||||
"verbosity": 1
|
||||
},
|
||||
"refine_with_matching": true,
|
||||
"matching_solver": "lemon",
|
||||
"refinement_maxdev_min": 2,
|
||||
"refinement_maxdev_max": 2,
|
||||
"deviation_limit": "NodeThroughflow",
|
||||
"verbosity": 2
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"double_cover": {
|
||||
"max_deviation": 5,
|
||||
"matching_solver": "Lemon",
|
||||
"evening_mode": "MST",
|
||||
"method": "HalfAsymmetric",
|
||||
"verbosity": 1
|
||||
},
|
||||
"refine_with_matching": true,
|
||||
"matching_solver": "Lemon",
|
||||
"refinement_maxdev_min": 2,
|
||||
"refinement_maxdev_max": 2,
|
||||
"deviation_limit": "NodeThroughflow",
|
||||
"verbosity": 2
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"double_cover": {
|
||||
"max_deviation": 5,
|
||||
"matching_solver": "Lemon",
|
||||
"evening_mode": "MST",
|
||||
"method": "HalfAsymmetric",
|
||||
"verbosity": 1
|
||||
},
|
||||
"refine_with_matching": true,
|
||||
"matching_solver": "Lemon",
|
||||
"refinement_maxdev_min": 2,
|
||||
"refinement_maxdev_max": 2,
|
||||
"deviation_limit": "EdgeFlow",
|
||||
"verbosity": 2
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"double_cover": {
|
||||
"max_deviation": 5,
|
||||
"matching_solver": "Lemon",
|
||||
"evening_mode": "MST",
|
||||
"method": "HalfAsymmetric",
|
||||
"verbosity": 1
|
||||
},
|
||||
"refine_with_matching": true,
|
||||
"matching_solver": "Lemon",
|
||||
"refinement_maxdev_min": 2,
|
||||
"refinement_maxdev_max": 2,
|
||||
"deviation_limit": "NodeThroughflow",
|
||||
"verbosity": 2
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"double_cover": {
|
||||
"max_deviation": 5,
|
||||
"matching_solver": "Lemon",
|
||||
"evening_mode": "MST",
|
||||
"method": "HalfAsymmetric",
|
||||
"verbosity": 1
|
||||
},
|
||||
"refine_with_matching": true,
|
||||
"matching_solver": "Lemon",
|
||||
"refinement_maxdev_min": 2,
|
||||
"refinement_maxdev_max": 2,
|
||||
"deviation_limit": "NodeThroughflow",
|
||||
"verbosity": 2
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
from ctypes import *
|
||||
|
||||
class Parameters(Structure):
|
||||
_fields_ = [
|
||||
('remesh', c_bool),
|
||||
('sharpAngle', c_float),
|
||||
('alpha', c_float), # Unused
|
||||
('scaleFact', c_float), #Unused
|
||||
('hasFeature', c_bool),
|
||||
('hasField', c_bool),
|
||||
]
|
||||
|
||||
|
||||
class QRParameters(Structure):
|
||||
_fields_ = [
|
||||
("useFlowSolver", c_bool),
|
||||
("flow_config_filename", c_char_p),
|
||||
("satsuma_config_filename", c_char_p),
|
||||
("initialRemeshing", c_bool),
|
||||
("initialRemeshingEdgeFactor", c_double),
|
||||
("reproject", c_bool),
|
||||
("splitConcaves", c_bool),
|
||||
("finalSmoothing", c_bool),
|
||||
|
||||
("ilpMethod", c_int),
|
||||
("alpha", c_double),
|
||||
("isometry", c_bool),
|
||||
("regularityQuadrilaterals", c_bool),
|
||||
("regularityNonQuadrilaterals", c_bool),
|
||||
("regularityNonQuadrilateralsWeight", c_double),
|
||||
("alignSingularities", c_bool),
|
||||
("alignSingularitiesWeight", c_double),
|
||||
("repeatLosingConstraintsIterations", c_bool),
|
||||
("repeatLosingConstraintsQuads", c_bool),
|
||||
("repeatLosingConstraintsNonQuads", c_bool),
|
||||
("repeatLosingConstraintsAlign", c_bool),
|
||||
("feasibilityFix", c_bool),
|
||||
("hardParityConstraint", c_bool),
|
||||
|
||||
("timeLimit", c_double),
|
||||
("gapLimit", c_double),
|
||||
("minimumGap", c_double),
|
||||
("callbackTimeLimit", POINTER(c_float)),
|
||||
("callbackGapLimit", POINTER(c_float)),
|
||||
|
||||
("chartSmoothingIterations", c_int),
|
||||
("quadrangulationFixedSmoothingIterations", c_int),
|
||||
("quadrangulationNonFixedSmoothingIterations", c_int),
|
||||
("doubletRemoval", c_bool),
|
||||
|
||||
("resultSmoothingIterations", c_int),
|
||||
("resultSmoothingNRing", c_double),
|
||||
("resultSmoothingLaplacianIterations", c_int),
|
||||
("resultSmoothingLaplacianNRing", c_double),
|
||||
]
|
||||
|
||||
|
||||
def create_string(input):
|
||||
return create_string_buffer(str.encode(input, encoding='utf-8'))
|
||||
|
||||
|
||||
def create_default_QRParameters():
|
||||
callbackTimeLimitDefault = [3.00, 5.000, 10.0, 20.0, 30.0, 60.0, 90.0, 120.0]
|
||||
callbackGapLimitDefault = [0.005, 0.02, 0.05, 0.10, 0.15, 0.20, 0.25, 0.3]
|
||||
|
||||
params = QRParameters()
|
||||
|
||||
# Possibly unused
|
||||
params.initialRemeshing = True
|
||||
params.initialRemeshingEdgeFactor = 1
|
||||
params.reproject = True
|
||||
params.splitConcaves = False
|
||||
params.finalSmoothing = True
|
||||
params.doubletRemoval = True
|
||||
params.resultSmoothingIterations = 5
|
||||
params.resultSmoothingNRing = 3
|
||||
params.resultSmoothingLaplacianIterations = 2
|
||||
params.resultSmoothingLaplacianNRing = 3
|
||||
|
||||
# From configs
|
||||
params.alpha = 0.005
|
||||
params.ilpMethod = 1
|
||||
params.timeLimit = 200
|
||||
params.gapLimit = 0.0
|
||||
params.callbackTimeLimit = (c_float * len(callbackTimeLimitDefault))(*callbackTimeLimitDefault)
|
||||
params.callbackGapLimit = (c_float * len(callbackGapLimitDefault))(*callbackGapLimitDefault)
|
||||
params.minimumGap = 0.4
|
||||
params.isometry = 1
|
||||
params.regularityQuadrilaterals = 1
|
||||
params.regularityNonQuadrilaterals = 1
|
||||
params.regularityNonQuadrilateralsWeight = 0.9
|
||||
params.alignSingularities = 1
|
||||
params.alignSingularitiesWeight = 0.1
|
||||
params.repeatLosingConstraintsIterations = 1
|
||||
params.repeatLosingConstraintsQuads = 0
|
||||
params.repeatLosingConstraintsNonQuads = 0
|
||||
params.repeatLosingConstraintsAlign = 1
|
||||
params.hardParityConstraint = 1
|
||||
params.useFlowSolver = 1
|
||||
params.flow_config_filename = "config/main_config/flow_virtual_simple.json".encode()
|
||||
params.satsuma_config_filename = "config/satsuma/default.json".encode()
|
||||
|
||||
# Hardcoded in lib
|
||||
params.chartSmoothingIterations = 0
|
||||
params.quadrangulationFixedSmoothingIterations = 0
|
||||
params.quadrangulationNonFixedSmoothingIterations = 0
|
||||
params.feasibilityFix = False
|
||||
|
||||
return params
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,186 @@
|
||||
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'}
|
||||
@@ -0,0 +1,187 @@
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import *
|
||||
|
||||
|
||||
class QWPropertyGroup(PropertyGroup):
|
||||
debug: BoolProperty(name="Debug Mode", description="Show meshes from intermediate steps", default=False)
|
||||
useCache: BoolProperty(name="Use Cache", description="Reuses previously calculated features and only runs quadrangulate step. Must run all steps at least once before enabling this.\n(May be out of sync if mesh has been modified)", default=False)
|
||||
enableRemesh: BoolProperty(name="Preprocess", description="Decimates, triangulates, and tries to fix common geometry issues", default=True)
|
||||
enableSmoothing: BoolProperty(name="Smoothing", description="Performs smoothing after quadrangulation", default=True)
|
||||
enableSharp: BoolProperty(name="Sharp Detection", description="Enable detection of sharp features from edges marked sharp, seams, and from angle threshold", default=True)
|
||||
sharpAngle: FloatProperty(name="Angle Threshold", description="Angle threshold for sharp edges", min=0, soft_min=0.1, max=180, soft_max=179.9, default=35, precision=1, step=10, subtype="UNSIGNED")
|
||||
symmetryX: BoolProperty(name="X", description="Enable symmetry in X-axis", default=False)
|
||||
symmetryY: BoolProperty(name="Y", description="Enable symmetry in Y-axis", default=False)
|
||||
symmetryZ: BoolProperty(name="Z", description="Enable symmetry in Z-axis", default=False)
|
||||
|
||||
|
||||
class QRPropertyGroup(PropertyGroup):
|
||||
|
||||
scaleFact: FloatProperty(
|
||||
name="Scale Factor",
|
||||
description="Values > 1 for larger quads, < 1 to preserve more detail",
|
||||
min=0.01,
|
||||
max=10,
|
||||
default=1,
|
||||
subtype="FACTOR"
|
||||
)
|
||||
|
||||
fixedChartClusters: IntProperty(
|
||||
name="Fixed Chart Clusters",
|
||||
description="Fixed chart clusters",
|
||||
min=0,
|
||||
default=0
|
||||
)
|
||||
|
||||
### QRParameters ###
|
||||
|
||||
alpha: FloatProperty(
|
||||
name="Alpha",
|
||||
description="Blends between isometry (alpha) and regularity (1-alpha)",
|
||||
default=0.005,
|
||||
min=0.0,
|
||||
max=0.999,
|
||||
precision=3,
|
||||
step=0.5,
|
||||
subtype="FACTOR"
|
||||
)
|
||||
|
||||
ilpMethod: EnumProperty(
|
||||
name="ILP Method",
|
||||
description="ILP method for solving the ILP problem",
|
||||
items=[
|
||||
('LEASTSQUARES', 'Least Squares', 'Use least squares ILP method', 1),
|
||||
('ABS', 'Absolute', 'Use absolute ILP method', 2),
|
||||
],
|
||||
default='LEASTSQUARES'
|
||||
)
|
||||
|
||||
timeLimit: IntProperty(
|
||||
name="Time Limit",
|
||||
description="Time limit for optimization in seconds",
|
||||
default=200,
|
||||
min=1
|
||||
)
|
||||
|
||||
gapLimit: FloatProperty(
|
||||
name="Gap Limit",
|
||||
description="Optimization stops when gap value reaches this limit",
|
||||
default=0.0,
|
||||
min=0.0
|
||||
)
|
||||
|
||||
minimumGap: FloatProperty(
|
||||
name="Minimum Gap",
|
||||
description="Optimization must reach at least this gap value",
|
||||
default=0.4,
|
||||
min=0.0
|
||||
)
|
||||
|
||||
isometry: BoolProperty(
|
||||
name="Isometry",
|
||||
description="Enable isometry",
|
||||
default=True
|
||||
)
|
||||
|
||||
regularityQuadrilaterals: BoolProperty(
|
||||
name="Regularity Quadrilaterals",
|
||||
description="Enable regularity for quadrilaterals",
|
||||
default=True
|
||||
)
|
||||
|
||||
regularityNonQuadrilaterals: BoolProperty(
|
||||
name="Regularity Non-Quadrilaterals",
|
||||
description="Enable regularity for non-quadrilaterals",
|
||||
default=True
|
||||
)
|
||||
|
||||
regularityNonQuadrilateralsWeight: FloatProperty(
|
||||
name="Regularity Non-Quadrilaterals Weight",
|
||||
description="Weight for regularity of non-quadrilaterals",
|
||||
default=0.9,
|
||||
min=0.0,
|
||||
max=1.0
|
||||
)
|
||||
|
||||
alignSingularities: BoolProperty(
|
||||
name="Align Singularities",
|
||||
description="Enable singularity alignment",
|
||||
default=True
|
||||
)
|
||||
|
||||
alignSingularitiesWeight: FloatProperty(
|
||||
name="Singularity Alignment Weight",
|
||||
description="Weight for singularity alignment",
|
||||
default=0.1,
|
||||
min=0.0,
|
||||
max=1.0
|
||||
)
|
||||
|
||||
repeatLosingConstraintsIterations: BoolProperty(
|
||||
name="Repeat Losing Constraints Iterations",
|
||||
description="Repeat losing constraints for iterations",
|
||||
default=True
|
||||
)
|
||||
|
||||
repeatLosingConstraintsQuads: BoolProperty(
|
||||
name="Repeat Losing Constraints Quadrilaterals",
|
||||
description="Repeat losing constraints for quadrilaterals",
|
||||
default=False
|
||||
)
|
||||
|
||||
repeatLosingConstraintsNonQuads: BoolProperty(
|
||||
name="Repeat Losing Constraints Non-Quadrilaterals",
|
||||
description="Repeat losing constraints for non-quadrilaterals",
|
||||
default=False
|
||||
)
|
||||
|
||||
repeatLosingConstraintsAlign: BoolProperty(
|
||||
name="Repeat Losing Constraints Alignment",
|
||||
description="Repeat losing constraints for alignment",
|
||||
default=True
|
||||
)
|
||||
|
||||
hardParityConstraint: BoolProperty(
|
||||
name="Hard Parity Constraint",
|
||||
description="Use hard parity constraint",
|
||||
default=True
|
||||
)
|
||||
|
||||
flowConfig: EnumProperty(
|
||||
name="Flow Config",
|
||||
description="Flow config to use",
|
||||
items=[
|
||||
("SIMPLE", "Simple", "", 1),
|
||||
("HALF", "Half", "", 2),
|
||||
],
|
||||
default="SIMPLE"
|
||||
)
|
||||
|
||||
satsumaConfig: EnumProperty(
|
||||
name="Satsuma Config",
|
||||
description="Satsuma config to use",
|
||||
items=[
|
||||
("DEFAULT", "Default", "", 1),
|
||||
("MST", "Approx-MST", "", 2),
|
||||
("ROUND2EVEN", "Approx-Round2Even", "", 3),
|
||||
("SYMMDC", "Approx-Symmdc", "", 4),
|
||||
("EDGETHRU", "Edgethru", "", 5),
|
||||
("LEMON", "Lemon", "", 6),
|
||||
("NODETHRU", "Nodethru", "", 7),
|
||||
],
|
||||
default="DEFAULT"
|
||||
)
|
||||
|
||||
callbackTimeLimit: FloatVectorProperty(
|
||||
name="Callback Time Limit",
|
||||
description="Callback time limit",
|
||||
size=8,
|
||||
default=[3.00, 5.000, 10.0, 20.0, 30.0, 60.0, 90.0, 120.0]
|
||||
)
|
||||
|
||||
callbackGapLimit: FloatVectorProperty(
|
||||
name="Callback Gap Limit",
|
||||
description="Callback gap limit",
|
||||
size=8,
|
||||
precision=3,
|
||||
default=[0.005, 0.02, 0.05, 0.10, 0.15, 0.20, 0.25, 0.3]
|
||||
)
|
||||
@@ -0,0 +1,136 @@
|
||||
from bpy.types import Context, Panel
|
||||
from .operator import QREMESH_OT_Remesh
|
||||
|
||||
|
||||
class BasePanel:
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "QRemeshify"
|
||||
bl_context = 'objectmode'
|
||||
|
||||
|
||||
class QREMESH_PT_UIPanel(BasePanel, Panel):
|
||||
bl_idname = "QREMESH_PT_UIPanel"
|
||||
bl_label = "QRemeshify"
|
||||
|
||||
def draw(self, ctx: Context):
|
||||
props = ctx.scene.quadwild_props
|
||||
qr_props = ctx.scene.quadpatches_props
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False # No animation.
|
||||
|
||||
row = layout.row()
|
||||
col = layout.column(heading="Enable")
|
||||
col.prop(props, "enableRemesh")
|
||||
col.prop(props, "enableSmoothing")
|
||||
|
||||
layout.separator(factor=0.1)
|
||||
|
||||
row = layout.row()
|
||||
col = row.column(heading="Sharp Detect")
|
||||
row = col.row()
|
||||
row.prop(props, "enableSharp", text="")
|
||||
row.prop(props, "sharpAngle", text="Angle")
|
||||
|
||||
layout.separator(factor=0.1)
|
||||
|
||||
row = layout.row(align=True, heading="Symmetry")
|
||||
row.prop(props, "symmetryX", expand=True, toggle=1)
|
||||
row.prop(props, "symmetryY", expand=True, toggle=1)
|
||||
row.prop(props, "symmetryZ", expand=True, toggle=1)
|
||||
|
||||
layout.separator(factor=0.1)
|
||||
|
||||
row = layout.row()
|
||||
row.prop(qr_props, "scaleFact", text="Density")
|
||||
|
||||
layout.separator()
|
||||
|
||||
layout.label(icon="ERROR", text="Please save first, remesh may be slow")
|
||||
layout.operator(QREMESH_OT_Remesh.bl_idname, icon="MESH_GRID")
|
||||
|
||||
|
||||
class QREMESH_PT_UIAdvancedPanel(BasePanel, Panel):
|
||||
bl_parent_id = "QREMESH_PT_UIPanel"
|
||||
bl_label = "Advanced"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, ctx: Context):
|
||||
props = ctx.scene.quadwild_props
|
||||
qr_props = ctx.scene.quadpatches_props
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False # No animation.
|
||||
|
||||
row = layout.row()
|
||||
col = row.column()
|
||||
col.prop(props, "debug")
|
||||
col.prop(props, "useCache")
|
||||
|
||||
layout.separator(type="LINE")
|
||||
|
||||
row = layout.row()
|
||||
col = row.column()
|
||||
col.prop(qr_props, "flowConfig")
|
||||
col.prop(qr_props, "satsumaConfig")
|
||||
|
||||
layout.separator(factor=0.1)
|
||||
|
||||
row = layout.row()
|
||||
col = row.column()
|
||||
col.prop(qr_props, "alpha")
|
||||
col.prop(qr_props, "ilpMethod")
|
||||
|
||||
layout.separator(type="LINE")
|
||||
|
||||
row = layout.row()
|
||||
col = row.column(heading="Regularity")
|
||||
col.prop(qr_props, "regularityQuadrilaterals", text="Quadrilaterals")
|
||||
col.prop(qr_props, "regularityNonQuadrilaterals", text="Non Quadrilaterals")
|
||||
col.prop(qr_props, "regularityNonQuadrilateralsWeight")
|
||||
|
||||
layout.separator(factor=0.1)
|
||||
|
||||
row = layout.row()
|
||||
col = row.column(heading="Align")
|
||||
col.prop(qr_props, "alignSingularities", text="Singularities")
|
||||
col.prop(qr_props, "alignSingularitiesWeight")
|
||||
|
||||
layout.separator(factor=0.1)
|
||||
|
||||
row = layout.row()
|
||||
col = row.column(heading="Repeat Losing Constraints")
|
||||
col.prop(qr_props, "repeatLosingConstraintsIterations", text="Iterations")
|
||||
col.prop(qr_props, "repeatLosingConstraintsQuads", text="Quads")
|
||||
col.prop(qr_props, "repeatLosingConstraintsNonQuads", text="NonQuads")
|
||||
col.prop(qr_props, "repeatLosingConstraintsAlign", text="Align")
|
||||
|
||||
layout.separator(type="LINE")
|
||||
|
||||
row = layout.row()
|
||||
col = row.column()
|
||||
col.prop(qr_props, "fixedChartClusters")
|
||||
col.prop(qr_props, "timeLimit")
|
||||
col.prop(qr_props, "gapLimit")
|
||||
col.prop(qr_props, "minimumGap")
|
||||
col.prop(qr_props, "isometry")
|
||||
col.prop(qr_props, "hardParityConstraint")
|
||||
|
||||
class QREMESH_PT_UICallbackPanel(BasePanel, Panel):
|
||||
bl_parent_id = "QREMESH_PT_UIAdvancedPanel"
|
||||
bl_label = "Callback Limits"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, ctx: Context):
|
||||
qr_props = ctx.scene.quadpatches_props
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False # No animation.
|
||||
|
||||
col = layout.column()
|
||||
col.prop(qr_props, "callbackTimeLimit", text="Time Limit")
|
||||
col.prop(qr_props, "callbackGapLimit", text="Gap Limit")
|
||||
@@ -0,0 +1,41 @@
|
||||
import bmesh
|
||||
|
||||
|
||||
def bisect_on_axes(bm: bmesh.types.BMesh, xaxis: bool, yaxis: bool, zaxis: bool):
|
||||
"""Bisect once for each axis specified"""
|
||||
|
||||
if xaxis:
|
||||
bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=[v for v in bm.verts] + [e for e in bm.edges] + [f for f in bm.faces],
|
||||
dist=0.0001,
|
||||
plane_co=(0, 0, 0),
|
||||
plane_no=(1, 0, 0),
|
||||
use_snap_center=False,
|
||||
clear_outer=False,
|
||||
clear_inner=True # Remove geometry on negative side of plane
|
||||
)
|
||||
|
||||
if yaxis:
|
||||
bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=[v for v in bm.verts] + [e for e in bm.edges] + [f for f in bm.faces],
|
||||
dist=0.0001,
|
||||
plane_co=(0, 0, 0),
|
||||
plane_no=(0, 1, 0),
|
||||
use_snap_center=False,
|
||||
clear_outer=False,
|
||||
clear_inner=True # Remove geometry on negative side of plane
|
||||
)
|
||||
|
||||
if zaxis:
|
||||
bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=[v for v in bm.verts] + [e for e in bm.edges] + [f for f in bm.faces],
|
||||
dist=0.0001,
|
||||
plane_co=(0, 0, 0),
|
||||
plane_no=(0, 0, 1),
|
||||
use_snap_center=False,
|
||||
clear_outer=False,
|
||||
clear_inner=True # Remove geometry on negative side of plane
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
import bmesh
|
||||
|
||||
|
||||
def export_sharp_features(bm: bmesh.types.BMesh, sharp_filepath: str, sharp_angle: float=35) -> int:
|
||||
"""Export edges marked sharp, boundary, and seams as sharp features as OBJ format"""
|
||||
|
||||
sharp_edges = []
|
||||
bm.edges.index_update()
|
||||
bm.edges.ensure_lookup_table()
|
||||
|
||||
for edge in bm.edges:
|
||||
if edge.is_wire:
|
||||
continue
|
||||
if not edge.smooth:
|
||||
convexity = 1 if edge.is_convex else 0
|
||||
face = edge.link_faces[0]
|
||||
face_index = face.index
|
||||
for ei, e in enumerate(face.edges):
|
||||
if e.index == edge.index:
|
||||
edge_index = ei
|
||||
break
|
||||
sharp_edges.append(f"{convexity},{face_index},{edge_index}")
|
||||
|
||||
num_sharp_features = len(sharp_edges)
|
||||
with open(sharp_filepath, 'w') as f:
|
||||
f.write(f"{num_sharp_features}\n")
|
||||
for edge in sharp_edges:
|
||||
f.write(f"{edge}\n")
|
||||
f.close()
|
||||
|
||||
return num_sharp_features
|
||||
|
||||
|
||||
def export_mesh(bm: bmesh.types.BMesh, mesh_filepath: str) -> None:
|
||||
"""Export mesh as OBJ format"""
|
||||
|
||||
verts = []
|
||||
vert_normals = []
|
||||
faces = []
|
||||
|
||||
for v in bm.verts:
|
||||
verts.append(f"v {v.co.x:.6f} {v.co.y:.6f} {v.co.z:.6f}")
|
||||
|
||||
for fid, f in enumerate(bm.faces):
|
||||
# NOTE: Blender will export per face normals if flat-shaded, per face per loop normals if smooth-shaded
|
||||
vert_normals.append(f"vn {f.normal.x:.4f} {f.normal.y:.4f} {f.normal.z:.4f}")
|
||||
|
||||
face_verts = []
|
||||
for v in f.verts:
|
||||
# NOTE: OBJ indices start at 1
|
||||
face_verts.append(f"{v.index + 1}//{fid + 1}")
|
||||
|
||||
faces.append(f"f {' '.join(face_verts)}")
|
||||
|
||||
|
||||
with open(mesh_filepath, 'w') as f:
|
||||
f.write("# OBJ file\n")
|
||||
f.write('\n'.join(verts))
|
||||
f.write('\n')
|
||||
f.write('\n'.join(vert_normals))
|
||||
f.write('\n')
|
||||
f.write('\n'.join(faces))
|
||||
f.write('\n')
|
||||
f.close()
|
||||
@@ -0,0 +1,33 @@
|
||||
import bpy
|
||||
import os
|
||||
|
||||
|
||||
def import_mesh(mesh_filepath: str) -> bpy.types.Mesh:
|
||||
if not os.path.isfile(mesh_filepath):
|
||||
raise Exception(f"File does not exist at {mesh_filepath}")
|
||||
|
||||
with open(mesh_filepath, 'r') as f:
|
||||
lines = f.read().splitlines()
|
||||
f.close()
|
||||
|
||||
verts = []
|
||||
edges = []
|
||||
faces = []
|
||||
|
||||
for line in lines:
|
||||
tokens = line.split(' ')
|
||||
element = tokens[0]
|
||||
|
||||
if element == 'v':
|
||||
verts.append(tuple([float(coord) for coord in tokens[1:]]))
|
||||
elif element == 'f':
|
||||
# NOTE: OBJ indices start at 1
|
||||
faces.append(tuple([int(vertex_id) - 1 for vertex_id in tokens[1:]]))
|
||||
else:
|
||||
continue
|
||||
|
||||
new_mesh = bpy.data.meshes.new('Mesh')
|
||||
new_mesh.from_pydata(verts, edges, faces)
|
||||
new_mesh.update()
|
||||
|
||||
return new_mesh
|
||||
Reference in New Issue
Block a user