Skip to content
This repository was archived by the owner on May 6, 2026. It is now read-only.

Commit 8203c8f

Browse files
committed
Extended handling for invalid geometry
Fixed variable naming for non-comb variable restore in the combined plugin. Added skipping for geometry-less meshes. Added rotation preprocessing (with Metashape option) to GUI and combined plugin. Added normal transfer from the original to sliced mesh to prevent flipped normals. Wrapped slicing face creation in a try-except block in case the source geometry has multiple faces that share the exact same verticies.
1 parent 5cc1665 commit 8203c8f

6 files changed

Lines changed: 96 additions & 24 deletions

File tree

enviro_lod_tools/addons/ds_blender_baker_plug.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
bl_info = {
1111
"name": "Baker Plugin",
1212
"author": "Nico Breycha",
13-
"version": (0, 0, 2),
13+
"version": (0, 0, 3),
1414
"blender": (4, 0, 0),
1515
"location": "View3D > Sidebar > Tool Tab",
1616
"description": "Bakes the base color of a defined mesh onto one or multiple selected meshes.",
@@ -135,6 +135,10 @@ def execute(self, context):
135135

136136
# Bake the Lowpoly Meshes one by one.
137137
for lowpoly in lowpolys:
138+
# Skip empty meshes
139+
if len(lowpoly.data.polygons) == 0:
140+
continue
141+
138142
print(f"Progress: {progress_cnt}/{lowpoly_cnt}")
139143
bake(highpoly, lowpoly, settings)
140144
progress_cnt += 1

enviro_lod_tools/addons/ds_blender_combined_plugin.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import copy
3+
import math
34

45
import bpy
56

@@ -12,7 +13,7 @@
1213
bl_info = {
1314
"name": "Automated LOD Generation Tool",
1415
"author": "Nico Breycha",
15-
"version": (0, 0, 2),
16+
"version": (0, 0, 3),
1617
"blender": (4, 0, 0),
1718
"location": "View3D > Sidebar > Tool Tab",
1819
"description": "Combines the Operator from all the other plugins.",
@@ -39,6 +40,9 @@ def _select_all_except_original():
3940
import_fp_comb = context.scene.import_fp_comb
4041
export_fp_comb = context.scene.export_fp_comb
4142

43+
# Preprocessing
44+
rot_correction_comb = context.scene.rot_correction_comb
45+
4246
# Cleanup Properties
4347
initial_reduction_comb = context.scene.initial_reduction_comb
4448
loose_threshold_comb = context.scene.loose_threshold_comb
@@ -63,19 +67,19 @@ def _select_all_except_original():
6367

6468
restore_dict = {
6569
"initial_reduction": context.scene.initial_reduction,
66-
"loose_threshold_comb": context.scene.loose_threshold,
70+
"loose_threshold": context.scene.loose_threshold,
6771
"boundary_length": context.scene.boundary_length,
6872
"merge_threshold": context.scene.merge_threshold,
6973
"number_of_modules": context.scene.number_of_modules,
7074
"lod_count": context.scene.lod_count,
7175
"reduction_percentage": context.scene.reduction_percentage,
72-
"baker_settings_comb": context.scene.baker_settings
76+
"baker_settings": context.scene.baker_settings
7377
}
7478

7579
# Preserve the original values of the properties with shallow copy.
7680
restore_dict = copy.copy(restore_dict)
7781

78-
# Override Operator Properties
82+
# Override Operator Properties for non-comb components
7983
context.scene.initial_reduction = initial_reduction_comb
8084
context.scene.loose_threshold = loose_threshold_comb
8185
context.scene.boundary_length = boundary_length_comb
@@ -102,6 +106,20 @@ def _select_all_except_original():
102106
bpy.context.view_layer.objects.active = obj
103107

104108
bpy.ops.object.join()
109+
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
110+
111+
# Apply rotation correction if needed
112+
obj = bpy.context.active_object
113+
114+
if rot_correction_comb[0] != 0:
115+
obj.rotation_euler[0] = math.radians(rot_correction_comb[0])
116+
if rot_correction_comb[1] != 0:
117+
obj.rotation_euler[1] = math.radians(rot_correction_comb[1])
118+
if rot_correction_comb[2] != 0:
119+
obj.rotation_euler[2] = math.radians(rot_correction_comb[2])
120+
121+
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
122+
105123

106124
# Rename the original mesh. We will use it later for baking.
107125
original_mesh = bpy.context.active_object
@@ -128,6 +146,7 @@ def _select_all_except_original():
128146
print("Starting Slicing")
129147
launch_operator_by_name(SLICE_IDNAME)
130148

149+
131150
# Ensure Target Polycount set by the user as intial polycount.
132151
parts = []
133152

@@ -177,18 +196,18 @@ def _select_all_except_original():
177196

178197
# Restore Operator Properties
179198
context.scene.initial_reduction = restore_dict["initial_reduction"]
180-
context.scene.loose_threshold = restore_dict["loose_threshold_comb"]
199+
context.scene.loose_threshold = restore_dict["loose_threshold"]
181200
context.scene.boundary_length = restore_dict["boundary_length"]
182201
context.scene.merge_threshold = restore_dict["merge_threshold"]
183202
context.scene.number_of_modules = restore_dict["number_of_modules"]
184203
context.scene.lod_count = restore_dict["lod_count"]
185204
context.scene.reduction_percentage = restore_dict["reduction_percentage"]
186205

187-
context.scene.baker_settings.highpoly_mesh_name = restore_dict["baker_settings_comb"].highpoly_mesh_name
188-
context.scene.baker_settings.ray_distance = restore_dict["baker_settings_comb"].ray_distance
189-
context.scene.baker_settings.texture_resolution = restore_dict["baker_settings_comb"].texture_resolution
190-
context.scene.baker_settings.texture_margin = restore_dict["baker_settings_comb"].texture_margin
191-
context.scene.baker_settings.save_path = restore_dict["baker_settings_comb"].save_path
206+
context.scene.baker_settings.highpoly_mesh_name = restore_dict["baker_settings"].highpoly_mesh_name
207+
context.scene.baker_settings.ray_distance = restore_dict["baker_settings"].ray_distance
208+
context.scene.baker_settings.texture_resolution = restore_dict["baker_settings"].texture_resolution
209+
context.scene.baker_settings.texture_margin = restore_dict["baker_settings"].texture_margin
210+
context.scene.baker_settings.save_path = restore_dict["baker_settings"].save_path
192211

193212
# Save .blend File
194213
bpy.ops.wm.save_as_mainfile(filepath=blend_file_path, check_existing=False, compress=True)
@@ -211,6 +230,7 @@ def draw(self, context):
211230

212231
io_box = layout.box()
213232
io_box.prop(scene, "import_fp_comb", text="Import Filepath")
233+
io_box.prop(scene, "rot_correction_comb", text="Rotation Correction")
214234
io_box.prop(scene, "export_fp_comb", text="Export Filepath")
215235

216236
# Create a box for Cleanup Properties and add labeled properties
@@ -257,6 +277,10 @@ def register():
257277
for cls in classes:
258278
register_class(cls)
259279

280+
# Preprocessing Properties
281+
bpy.types.Scene.rot_correction_comb = bpy.props.FloatVectorProperty(name="Initial Rotation Correction",
282+
default=(0.0, 0.0, 0.0), subtype="EULER")
283+
260284
# Cleanup Properties
261285
bpy.types.Scene.initial_reduction_comb = bpy.props.IntProperty(name="Initial Reduction", default=1000000)
262286
bpy.types.Scene.loose_threshold_comb = bpy.props.IntProperty(name="Loose Component Vertex Thr", default=1000)
@@ -288,6 +312,7 @@ def unregister():
288312
unregister_class(cls)
289313

290314
# Cleanup Properties
315+
del bpy.types.Scene.rot_correction_comb
291316
del bpy.types.Scene.initial_reduction_comb
292317
del bpy.types.Scene.baker_settings_comb
293318
del bpy.types.Scene.loose_threshold_comb

enviro_lod_tools/addons/ds_blender_lod_plug.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
bl_info = {
77
"name": "LOD Generator",
88
"author": "Nico Breycha",
9-
"version": (0, 0, 3),
9+
"version": (0, 0, 4),
1010
"blender": (4, 0, 0),
1111
"location": "View3D > Sidebar > Tool Tab",
1212
"description": "Generates levels of detail (LODs) for selected mesh objects.",
@@ -45,6 +45,11 @@ def generate_lods(self, context):
4545

4646
for obj in context.selected_objects:
4747
if obj.type == 'MESH':
48+
# Skip objects with no polygons
49+
if len(obj.data.polygons) == 0:
50+
continue
51+
52+
# Create LODs for the object
4853
self._create_lods_for_object(obj, context)
4954

5055
def _create_lods_for_object(self, obj, context):

enviro_lod_tools/addons/ds_blender_slice_plug.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
bl_info = {
1515
"name": "Mesh Slicer",
1616
"author": "Nico Breycha",
17-
"version": (0, 1, 1),
17+
"version": (0, 1, 2),
1818
"blender": (4, 0, 0),
1919
"location": "View3D > Sidebar > Tool Tab",
2020
"description": "Cut's a Mesh into a user-defined amount of square slices, "
@@ -256,15 +256,32 @@ def better_bisect(mesh, cut_pos, direction, middle_point=None):
256256
pos_map = {vert.index: bm_pos.verts.new(vert.co) for vert in verts_pos}
257257
neg_map = {vert.index: bm_neg.verts.new(vert.co) for vert in verts_neg}
258258

259+
# Store original vertex normals
260+
original_normals = {v.index: v.normal.copy() for v in bm.verts}
261+
262+
# Transfer Normals. NOTE: From here on we should not call "recalculate_normals" anymore!!
263+
for old_index, new_vert in pos_map.items():
264+
new_vert.normal = original_normals[old_index]
265+
266+
for old_index, new_vert in neg_map.items():
267+
new_vert.normal = original_normals[old_index]
268+
269+
double_faces = 0
270+
259271
# Recreate the faces in bm_pos and bm_neg
260272
for face in bm.faces:
261-
pos_face_verts = [pos_map[v.index] for v in face.verts if v.index in pos_map]
262-
neg_face_verts = [neg_map[v.index] for v in face.verts if v.index in neg_map]
273+
pos_face_verts = [pos_map[vert.index] for vert in face.verts if vert.index in pos_map]
274+
neg_face_verts = [neg_map[vert.index] for vert in face.verts if vert.index in neg_map]
263275

264-
if len(pos_face_verts) == len(face.verts):
265-
bm_pos.faces.new(pos_face_verts)
266-
elif len(neg_face_verts) == len(face.verts):
267-
bm_neg.faces.new(neg_face_verts)
276+
try:
277+
if len(pos_face_verts) == len(face.verts):
278+
bm_pos.faces.new(pos_face_verts)
279+
elif len(neg_face_verts) == len(face.verts):
280+
bm_neg.faces.new(neg_face_verts)
281+
except ValueError as err:
282+
print(f"Failed to create face {face.index}: {err}")
283+
double_faces += 1
284+
pass
268285

269286
bm.free()
270287
del verts_pos
@@ -397,8 +414,6 @@ def execute(self, context):
397414
suffix = f"_{i + 1:03d}"
398415
part.name = part.name + suffix
399416

400-
self.recalculate_normals(part.data)
401-
402417
self.report({'INFO'}, "Slicing completed")
403418
return {'FINISHED'}
404419

enviro_lod_tools/addons/ds_blender_xatlas_plug.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
bl_info = {
1515
"name": "XAtlas Unwrapper",
1616
"author": "Nico Breycha",
17-
"version": (0, 0, 1),
17+
"version": (0, 0, 2),
1818
"blender": (4, 0, 0),
1919
"location": "View3D > Sidebar > Tool Tab",
2020
"description": "Unwraps the model using xatals.",
@@ -77,8 +77,14 @@ def execute(self, context):
7777
cnt_fail += 1
7878
continue
7979

80-
# Get mesh data from the object
8180
mesh = obj.data
81+
82+
# Skip valid meshes with 0 polygons
83+
if len(mesh.polygons) == 0:
84+
self.report({'INFO'}, f"Skipping {obj.name}: empty mesh")
85+
continue
86+
87+
# Get mesh data from the object
8288
cnt_succ += 1
8389

8490
# Create a bmesh object and load the mesh data into it

enviro_tools_gui.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ def __init__(self):
7777
io_layout.addLayout(highpoly_layout)
7878
io_layout.addWidget(self.polycount_label)
7979

80+
# Rotate Highpoly Input
81+
rotate_layout = QHBoxLayout()
82+
self.rotate_label = QLabel("Rotate Highpoly Input:")
83+
self.rotation_correction = QComboBox()
84+
self.rotation_correction.addItems(["No Rotation", "-90 on X (Metashape)"])
85+
rotate_layout.addWidget(self.rotate_label)
86+
rotate_layout.addWidget(self.rotation_correction)
87+
io_layout.addLayout(rotate_layout)
88+
8089
# Horizontal Line as Separator
8190
h_line = QFrame()
8291
h_line.setFrameShape(QFrame.HLine)
@@ -363,6 +372,7 @@ def setup_blender(self, values):
363372

364373
# Run the pipeline
365374
bpy.types.Scene.import_fp_comb = values["highpoly_model_path"]
375+
bpy.types.Scene.rot_correction_comb = values["rot_correction"]
366376
bpy.types.Scene.export_fp_comb = values["export_path"]
367377

368378
bpy.types.Scene.initial_reduction_comb = values["initial_reduction_polycount"]
@@ -392,6 +402,12 @@ def start_pipeline(self):
392402
return
393403

394404
highpoly_model_path = self.highpoly_model_line_edit.text()
405+
rot_correction = self.rotation_correction.currentText()
406+
if rot_correction == "No Rotation":
407+
rot_correction = [0, 0, 0]
408+
elif rot_correction == "-90 on X (Metashape)":
409+
rot_correction = [-90, 0, 0]
410+
395411
export_path = self.export_path_line_edit.text()
396412

397413
if not os.path.isfile(highpoly_model_path):
@@ -423,7 +439,8 @@ def start_pipeline(self):
423439
"num_lods": num_lods,
424440
"reduction_percentage": reduction_percentage,
425441
"texture_resolution": texture_resolution,
426-
"ray_distance": ray_distance
442+
"ray_distance": ray_distance,
443+
"rot_correction": rot_correction
427444
}
428445

429446
self.setup_blender(values)

0 commit comments

Comments
 (0)