2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -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"
@@ -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"
@@ -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
@@ -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'}
+187
View File
@@ -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]
)
+136
View File
@@ -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