36 Commits

Author SHA1 Message Date
Terrev
fd02e3240e disable subsurfacescattering on opaque as it doesnt play nicely with the scaling 2025-01-14 21:05:45 -05:00
Terrev
bbdfafc2b6 Merge pull request #23 from Squareville/lod-color
Coloring vertices per lod group, and repeating the state/seed so each…
2024-11-26 02:43:07 -05:00
473879e900 Coloring vertices per lod group, and repeating the state/seed so each lod is the same color. 2024-11-25 20:27:24 -05:00
Terrev
5c0c04e4a0 Merge pull request #22 from Squareville/reset-orientation
Option to apply a 90° rotation transform, fixes smashable vfx.
2024-11-12 02:04:53 -05:00
9a9b5fe58e Option to apply a 90° rotation transform, fixes smashable vfx. 2024-11-12 01:24:52 -05:00
Terrev
f9ce932bed version 2.0.0 -> 2.1.0 2024-02-19 01:21:29 -05:00
Terrev
1b3f6672e9 only increase sample count if we need to 2024-02-19 01:20:51 -05:00
Terrev
7a9a8e2cbb silly fix for transparent bricks in icons being opaque 2024-02-18 02:35:54 -05:00
Terrev
61ba3c69dd improve transparent colors 2023-12-20 01:31:53 -05:00
Terrev
88e6159f40 fix transparent colors and 111 tbrown (lol), may adjust some values next 2023-12-19 22:30:17 -05:00
6ac59d4be3 Update README.md 2022-12-27 18:18:13 -05:00
569761f972 Update version number to 2.0.0 2022-12-27 13:07:24 -05:00
Terrev
223e607628 transparent color rgb updates 2022-11-08 22:59:50 -05:00
Terrev
8138542fe0 black color variation strength 2022-11-08 13:42:03 -05:00
5fb6338ca9 fix normals 2022-11-07 21:38:28 -06:00
Bobbe
24d5708d7a Fix icon material color space 2022-05-08 19:58:34 +02:00
Bobbe
22df835784 Move color conversion functions to their own file 2022-05-08 19:58:22 +02:00
db5b172f95 Added icon render materials for black and white. 2022-05-08 00:22:57 -04:00
ba9beb1212 Updated resources.blend. Some art polish for icon render scene. 2022-05-08 00:22:38 -04:00
081c6aac0f Updated resources.blend ItemRender camera FOV and render resolution. 2022-05-07 11:37:00 -04:00
Bobbe
713620a30e Remove leftover print 2022-05-05 16:08:58 +02:00
Bobbe
fe1ea064a8 Automatically center and scale model for icon_render 2022-05-04 17:10:45 +02:00
Bobbe
0cc478b5e4 Add set to disable subdivision for certain bricks in icon render 2022-05-03 20:06:17 +02:00
Bobbe
52a40f3d9f Get rid of the now deprecated premerging code in process model 2022-05-03 19:56:47 +02:00
Bobbe
67aa93348a Remove brick empties, premerge bricks and their materials 2022-05-03 19:56:29 +02:00
Bobbe
d8ad70de8c Fix importer error handling 2022-05-03 18:52:56 +02:00
Bobbe
5d41b3e199 Add semi working icon render operator 2022-05-03 18:51:26 +02:00
Bobbe
79d4bf454b Optimize/Simplify precombining bricks/materials 2022-03-25 21:46:30 +01:00
Bobbe
bbabeac716 Add "Ignore Lights" option to HSR 2022-03-25 17:08:46 +01:00
Bobbe
30e3c78bde Fix potential edge cases that could lead to errors in bake lighting 2022-03-25 16:51:49 +01:00
Bobbe
a961554aeb Fix #18 HSR info print not working if no hidden faces were detected 2022-03-25 16:30:45 +01:00
Bobbe
66b5f24d9d Add more shader types, remove leading "S" 2022-03-25 16:13:34 +01:00
Bobbe
34ebba2ff5 Retriangulate faces after HSR (only when autoremoving) 2022-03-25 16:01:34 +01:00
Bobbe
4009b815c7 Reorganize bake lighting UI 2022-03-24 14:29:57 +01:00
Bobbe
33018f7d12 Add "selected only" option to bake lighting 2022-03-24 14:29:31 +01:00
8860921d66 Update resources.blend. Added materials for colliders. 2022-03-21 13:01:43 -04:00
10 changed files with 524 additions and 216 deletions

View File

@@ -1,4 +1,4 @@
[![blender](https://img.shields.io/badge/blender-2.93.7-success)](https://download.blender.org/release/Blender2.93/)
[![blender](https://img.shields.io/badge/blender-3.1.0-success)](https://download.blender.org/release/Blender3.1/)
[![gpl](https://img.shields.io/github/license/30350n/lu-toolbox)](https://github.com/30350n/lu-toolbox/blob/master/LICENSE)
# LEGO Universe Toolbox
A Blender Add-on which adds a bunch of useful tools to prepare models for use in LEGO Universe.

View File

@@ -1,20 +1,24 @@
bl_info = {
"name": "LU Toolbox",
"author": "Bobbe",
"version": (1, 7, 0),
"version": (2, 4, 0),
"blender": (2, 93, 0),
"location": "3D View -> Sidebar -> LU Toolbox",
"category": "Import-Export",
"support": "COMMUNITY",
}
import bpy
import importlib
module_names = ("process_model", "bake_lighting", "remove_hidden_faces", "importldd")
module_names = (
"process_model",
"icon_render",
"bake_lighting",
"remove_hidden_faces",
"importldd"
)
modules = []
for module_name in module_names:
if module_name in locals():
modules.append(importlib.reload(locals()[module_name]))

View File

@@ -25,24 +25,43 @@ class LUTB_PT_bake_lighting(bpy.types.Panel):
layout.separator()
layout.prop(scene, "lutb_bake_samples")
layout.prop(scene, "lutb_bake_fast_gi_bounces")
layout.prop(scene, "lutb_bake_glow_strength")
layout.prop(scene, "lutb_bake_use_gpu")
layout.prop(scene, "lutb_bake_selected_only")
col = layout.column()
col.prop(scene, "lutb_bake_use_white_ambient")
col.active = not scene.lutb_bake_ao_only
layout.prop(scene, "lutb_bake_smooth_lit")
layout.prop(scene, "lutb_bake_ao_only")
col = layout.column()
col.prop(scene, "lutb_bake_glow_multiplier")
col.prop(scene, "lutb_bake_ao_samples")
col.active = scene.lutb_bake_ao_only
col = layout.column()
col.prop(scene, "lutb_bake_force_to_white")
col.active = not scene.lutb_bake_use_mat_override
class LUTB_PT_mat_override(bpy.types.Panel):
layout.prop(scene, "lutb_bake_samples")
layout.prop(scene, "lutb_bake_fast_gi_bounces")
layout.prop(scene, "lutb_bake_glow_strength")
class LUTB_PT_bake_ao_only(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "LU Toolbox"
bl_label = "AO Only"
bl_parent_id = "LUTB_PT_bake_lighting"
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
self.layout.prop(context.scene, "lutb_bake_ao_only", text="")
def draw(self, context):
scene = context.scene
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.active = scene.lutb_bake_ao_only
layout.prop(scene, "lutb_bake_glow_multiplier")
layout.prop(scene, "lutb_bake_ao_samples")
class LUTB_PT_bake_mat_override(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "LU Toolbox"
@@ -127,22 +146,18 @@ class LUTB_OT_bake_lighting(bpy.types.Operator):
obj.hide_render = True
hidden_objects.append(obj)
target_objects = scene.collection.all_objects
if scene.lutb_bake_selected_only:
target_objects = context.selected_objects
old_active_obj = context.object
for obj in (selected := list(context.selected_objects)):
old_selected_objects = context.selected_objects
for obj in list(target_objects):
if obj.type != "MESH" or obj.get(IS_TRANSPARENT):
continue
other_lod_colls = set()
for lod_collection in obj.users_collection:
for collection in bpy.data.collections:
if lod_collection.name in collection.children:
other_lod_colls |= set(collection.children) - {lod_collection,}
for other_lod_coll in list(other_lod_colls):
if other_lod_coll.hide_render:
other_lod_colls.remove(other_lod_coll)
else:
other_lod_coll.hide_render = True
if not obj.name in context.view_layer.objects:
self.report({"WARNING"}, f"Skipping \"{obj.name}\". (not in viewlayer)")
continue
mesh = obj.data
@@ -160,9 +175,17 @@ class LUTB_OT_bake_lighting(bpy.types.Operator):
if vc_lit := mesh.vertex_colors.get("Lit"):
mesh.vertex_colors.active_index = mesh.vertex_colors.keys().index(vc_lit.name)
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
context.view_layer.objects.active = obj
other_lod_colls = set()
for lod_collection in obj.users_collection:
for collection in bpy.data.collections:
if lod_collection.name in collection.children:
other_lod_colls |= set(collection.children) - {lod_collection,}
for other_lod_coll in list(other_lod_colls):
if other_lod_coll.hide_render:
other_lod_colls.remove(other_lod_coll)
else:
other_lod_coll.hide_render = True
old_material = mesh.materials[0]
if scene.lutb_bake_use_mat_override:
@@ -176,11 +199,25 @@ class LUTB_OT_bake_lighting(bpy.types.Operator):
if node.type == "BSDF_PRINCIPLED":
node.inputs['Emission Strength'].default_value = emission_strength
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
context.view_layer.objects.active = obj
context_override = context.copy()
context_override["scene"] = scene_override
bpy.ops.object.bake(context_override)
try:
bpy.ops.object.bake(context_override)
except RuntimeError as e:
if "is not enabled for rendering" in str(e):
self.report({"WARNING"}, f"Skipping \"{obj.name}\". (not enabled for rendering)")
continue
else:
raise
finally:
mesh.materials[0] = old_material
mesh.materials[0] = old_material
for other_lod_coll in other_lod_colls:
other_lod_coll.hide_render = False
has_edge_split_modifier = "EDGE_SPLIT" in {mod.type for mod in obj.modifiers}
if scene.lutb_bake_smooth_lit and not has_edge_split_modifier:
@@ -202,20 +239,17 @@ class LUTB_OT_bake_lighting(bpy.types.Operator):
lit_data[:, 3] = alpha_data.reshape((n_loops, 4))[:, 0]
vc_lit.data.foreach_set("color", lit_data.flatten())
for other_lod_coll in other_lod_colls:
other_lod_coll.hide_render = False
bpy.data.scenes.remove(scene_override)
if ao_only_world_override:
bpy.data.worlds.remove(ao_only_world_override)
for obj in selected:
obj.select_set(True)
for obj in hidden_objects:
obj.hide_render = False
bpy.ops.object.select_all(action="DESELECT")
for obj in old_selected_objects:
obj.select_set(True)
context.view_layer.objects.active = old_active_obj
end = timer()
@@ -226,9 +260,11 @@ class LUTB_OT_bake_lighting(bpy.types.Operator):
def register():
bpy.utils.register_class(LUTB_OT_bake_lighting)
bpy.utils.register_class(LUTB_PT_bake_lighting)
bpy.utils.register_class(LUTB_PT_mat_override)
bpy.utils.register_class(LUTB_PT_bake_ao_only)
bpy.utils.register_class(LUTB_PT_bake_mat_override)
bpy.types.Scene.lutb_bake_use_gpu = BoolProperty(name="Use GPU", default=True)
bpy.types.Scene.lutb_bake_selected_only = BoolProperty(name="Selected Only")
bpy.types.Scene.lutb_bake_smooth_lit = BoolProperty(name="Smooth Vertex Colors", default=True)
bpy.types.Scene.lutb_bake_samples = IntProperty(name="Samples", default=256, min=1, description=""\
"Number of samples to render for each vertex")
@@ -245,6 +281,7 @@ def register():
def unregister():
del bpy.types.Scene.lutb_bake_use_gpu
del bpy.types.Scene.lutb_bake_selected_only
del bpy.types.Scene.lutb_bake_smooth_lit
del bpy.types.Scene.lutb_bake_samples
del bpy.types.Scene.lutb_bake_fast_gi_bounces
@@ -257,6 +294,7 @@ def unregister():
del bpy.types.Scene.lutb_bake_use_mat_override
del bpy.types.Scene.lutb_bake_mat_override
bpy.utils.unregister_class(LUTB_PT_mat_override)
bpy.utils.unregister_class(LUTB_PT_bake_mat_override)
bpy.utils.unregister_class(LUTB_PT_bake_ao_only)
bpy.utils.unregister_class(LUTB_PT_bake_lighting)
bpy.utils.unregister_class(LUTB_OT_bake_lighting)

209
lu_toolbox/icon_render.py Normal file
View File

@@ -0,0 +1,209 @@
import bpy, bmesh
from bpy.props import BoolProperty
from mathutils import Vector, Matrix
from math import radians
import numpy as np
from .process_model import LOD_SUFFIXES
from .materials import *
class LUTB_OT_setup_icon_render(bpy.types.Operator):
"""Setup Icon Render for LU Model"""
bl_idname = "lutb.setup_icon_render"
bl_label = "Setup Icon Render"
@classmethod
def poll(cls, context):
return context.mode == "OBJECT"
def execute(self, context):
# we will only increase the sample count for rendering if we need to (currently only if there are any transparent pieces) - jamie
increase_samples = False
scene = context.scene
for collection in scene.collection.children:
for lod_collection in collection.children[:]:
if lod_collection.name[-5:] in LOD_SUFFIXES[1:]:
collection.children.unlink(lod_collection)
combine_objects_before = scene.lutb_combine_objects
scene.lutb_combine_objects = False
apply_vertex_colors_before = scene.lutb_apply_vertex_colors
scene.lutb_apply_vertex_colors = True
correct_colors_before = scene.lutb_correct_colors
scene.lutb_correct_colors = scene.lutb_ir_correct_colors
color_variation_before = scene.lutb_color_variation
scene.lutb_color_variation = scene.lutb_ir_color_variation
setup_bake_mat_before = scene.lutb_setup_bake_mat
scene.lutb_setup_bake_mat = False
remove_hidden_faces_before = scene.lutb_remove_hidden_faces
scene.lutb_remove_hidden_faces = False
# hacky way to inject modified color corrections
if scene.lutb_ir_correct_colors:
color_corrections = (
[MATERIALS_OPAQUE, ICON_MATERIALS_OPAQUE, None],
[MATERIALS_TRANSPARENT, ICON_MATERIALS_TRANSPARENT, None],
[MATERIALS_GLOW, ICON_MATERIALS_GLOW, None],
[MATERIALS_METALLIC, ICON_MATERIALS_METALLIC, None],
)
for color_correction in color_corrections:
target, updates, _ = color_correction
color_correction[2] = target.copy()
target.update(updates)
bpy.ops.lutb.process_model()
if scene.lutb_ir_correct_colors:
for target, _, original in color_corrections:
target.update(original)
scene.lutb_combine_objects = combine_objects_before
scene.lutb_apply_vertex_colors = apply_vertex_colors_before
scene.lutb_correct_colors = correct_colors_before
scene.lutb_color_variation = color_variation_before
scene.lutb_setup_bake_mat = setup_bake_mat_before
scene.lutb_remove_hidden_faces = remove_hidden_faces_before
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.tris_convert_to_quads(shape_threshold=radians(50))
for obj in context.selected_objects:
if obj.type != "MESH":
continue
bm = bmesh.from_edit_mesh(obj.data)
bevel_weight = bm.edges.layers.bevel_weight.new("Bevel Weight")
for edge in bm.edges:
if len(edge.link_faces) == 1:
edge[bevel_weight] = 1.0
bmesh.update_edit_mesh(obj.data, loop_triangles=False, destructive=False)
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.remove_doubles()
bpy.ops.object.mode_set(mode="OBJECT")
for obj in context.selected_objects:
if obj.type != "MESH":
continue
if scene.lutb_ir_bevel_edges:
bevel_mod = obj.modifiers.new("Bevel", "BEVEL")
bevel_mod.width = 0.02
bevel_mod.segments = 4
bevel_mod.limit_method = "WEIGHT"
bevel_mod.harden_normals = True
if scene.lutb_ir_subdivide:
brick_id = obj.name.split("brick_")[-1].split("_")[1]
if not brick_id in ICON_RENDER_DISABLE_SUBDIV:
subdiv_mod = obj.modifiers.new("Subdivision", "SUBSURF")
subdiv_mod.levels = 1
subdiv_mod.render_levels = 2
mesh = obj.data
obj.data.use_auto_smooth = True
obj.data.auto_smooth_angle = radians(180)
for i, material in enumerate(mesh.materials):
name = material.name.rsplit(".", 1)[0]
if name in MATERIALS_OPAQUE:
mesh.materials[i] = get_lutb_ir_opaque_mat(self)
# another hack to change a material setting, for the same reasons as the comment below...
# anyway, it was deemed that having this enabled was making the plastic look too soft and washed-out in many scenarios - jamie
if mesh.materials[i].node_tree:
for node in mesh.materials[i].node_tree.nodes:
if node.type == "BSDF_PRINCIPLED":
node.inputs["Subsurface"].default_value = 0
elif name in MATERIALS_TRANSPARENT:
mesh.materials[i] = get_lutb_ir_transparent_mat(self)
# next two lines are a silly hacky fix cause i dont wanna mess with the magical mystery box that is resources.blend, and hollis didnt know why it was broken anyway - jamie
mesh.materials[i].blend_method = "HASHED"
mesh.materials[i].shadow_method = "HASHED"
increase_samples = True
elif name in MATERIALS_METALLIC:
mesh.materials[i] = get_lutb_ir_metal_mat(self)
ir_scene = get_lutb_ir_scene(self)
for collection in scene.collection.children[:]:
for obj in collection.objects:
if obj.type == "EMPTY" and obj.name.startswith("SceneNode_"):
break
else:
continue
lod_collection = collection.children[0]
obj_bounds = np.empty((len(lod_collection.objects) * 2, 3))
for i, obj in enumerate(lod_collection.objects):
obj_bounds[i * 2 + 0] = obj.matrix_world @ Vector(obj.bound_box[0])
obj_bounds[i * 2 + 1] = obj.matrix_world @ Vector(obj.bound_box[6])
dimensions = obj_bounds.max(0) - obj_bounds.min(0)
offset = Matrix.Translation(-(obj_bounds.min(0) + dimensions * 0.5))
scale = Matrix.Scale(1 / np.abs(dimensions).max(), 4)
for obj in lod_collection.objects:
obj.matrix_world = scale @ offset @ obj.matrix_world
scene.collection.children.unlink(collection)
ir_scene.collection.children.link(collection)
context.window.scene = ir_scene
for area in context.screen.areas:
if area.type == "VIEW_3D":
area.spaces[0].shading.type = "RENDERED"
if increase_samples:
bpy.context.scene.eevee.taa_render_samples = 1024
return {"FINISHED"}
class LUTB_PT_icon_render(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "LU Icon Render"
bl_label = "Icon Render"
def draw(self, context):
scene = context.scene
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.operator(LUTB_OT_setup_icon_render.bl_idname)
layout.separator(factor=0.5)
layout.prop(scene, "lutb_ir_correct_colors")
layout.prop(scene, "lutb_ir_color_variation")
layout.prop(scene, "lutb_ir_bevel_edges")
layout.prop(scene, "lutb_ir_subdivide")
def register():
bpy.utils.register_class(LUTB_OT_setup_icon_render)
bpy.utils.register_class(LUTB_PT_icon_render)
bpy.types.Scene.lutb_ir_correct_colors = BoolProperty(name="Correct Colors", default=True,
description=bpy.types.Scene.lutb_correct_colors.keywords["description"])
bpy.types.Scene.lutb_ir_color_variation = BoolProperty(name="Apply Color Variation", default=False,
description=bpy.types.Scene.lutb_use_color_variation.keywords["description"])
bpy.types.Scene.lutb_ir_bevel_edges = BoolProperty(name="Bevel Edges", default=True)
bpy.types.Scene.lutb_ir_subdivide = BoolProperty(name="Subdivide", default=True)
def unregister():
del bpy.types.Scene.lutb_ir_correct_colors
del bpy.types.Scene.lutb_ir_color_variation
del bpy.types.Scene.lutb_ir_bevel_edges
del bpy.types.Scene.lutb_ir_subdivide
bpy.utils.unregister_class(LUTB_PT_icon_render)
bpy.utils.unregister_class(LUTB_OT_setup_icon_render)

View File

@@ -1,6 +1,6 @@
# based on pyldd2obj by jonnysp and lxfml import plugin by sttng
# modified by aronwk-aaron to work better with LU-Toolbox
import bpy
import bpy, bmesh
import mathutils
from bpy_extras.io_utils import (
ImportHelper,
@@ -16,6 +16,7 @@ import zipfile
from xml.dom import minidom
import uuid
import random
import numpy as np
from .materials import (
MATERIALS_OPAQUE,
@@ -209,7 +210,7 @@ def convertldd_data(self, context, filepath, importLOD0, importLOD1, importLOD2,
else:
self.report({'INFO'}, f'LOD3 does not exist, skipping')
except Exception as e:
self.report({'ERROR'}, e)
self.report({'ERROR'}, str(e))
return {'FINISHED'}
@@ -1004,17 +1005,13 @@ class Converter:
uniqueId = str(uuid.uuid4().hex)
material_string = '_' + '_'.join(pa.materials)
written_obj = geo.designID + material_string
brick_name = f"brick_{currentpart}_{written_obj}"
if (len(pa.Bones) > flexflag):
# Flex parts are "unique". Ensure they get a unique filename
written_obj = written_obj + "_" + uniqueId
brick_object = bpy.data.objects.new("brick{0}_{1}".format(currentpart, written_obj), None)
col.objects.link(brick_object)
brick_object.empty_display_size = 1.25
brick_object.empty_display_type = 'PLAIN_AXES'
if not (len(pa.Bones) > flexflag):
part_matrix = global_matrix
else:
# Flex parts don't need to be moved, but non-flex parts need
transform_matrix = mathutils.Matrix(
(
@@ -1028,11 +1025,15 @@ class Converter:
# Random Scale for brick seams
scalefact = (geo.maxGeoBounding - 0.000 * random.uniform(0.0, 1.000)) / geo.maxGeoBounding
scale_matrix = mathutils.Matrix.Scale(scalefact, 4)
part_matrix = global_matrix @ transform_matrix @ scale_matrix
# miny used for floor plane later
if miny > float(n42):
miny = n42
last_color = 0
geo_meshes = []
for part in geo.Parts:
written_geo = str(geo.designID) + '_' + str(part)
@@ -1085,15 +1086,13 @@ class Converter:
mesh.normals_split_custom_set_from_vertices(normals)
mesh.use_auto_smooth = True
geometriecache["geo{0}".format(written_geo)] = mesh
geometriecache["geo{0}".format(written_geo)] = mesh.copy()
else:
mesh = geometriecache["geo{0}".format(written_geo)].copy()
mesh.materials.clear()
geo_obj = bpy.data.objects.new(mesh.name, mesh)
geo_obj.parent = brick_object
col.objects.link(geo_obj)
geo_meshes.append(mesh)
# try catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file
try:
@@ -1123,14 +1122,44 @@ class Converter:
for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total):
uv_layer[loop_index].uv = uvs[mesh.loops[loop_index].vertex_index]
if not (len(pa.Bones) > flexflag):
# Transform (move) only non-flex parts
brick_object.matrix_world = global_matrix @ transform_matrix
brick_object.scale = (scalefact, scalefact, scalefact)
used_materials = []
used_material_indices = {}
bm = bmesh.new()
for mesh in geo_meshes:
index_remapping = np.empty(len(mesh.materials), dtype=int)
for i, material in enumerate(mesh.materials):
mat_name = material.name.rsplit(".", 1)[0]
if (index := used_material_indices.get(mat_name)) is not None:
index_remapping[i] = index
bpy.data.materials.remove(material)
else:
index_remapping[i] = len(used_material_indices)
used_material_indices[mat_name] = index_remapping[i]
used_materials.append(material)
else:
# Flex parts need only to be aligned the Blender coordinate system
brick_object.matrix_world = global_matrix
material_indices = np.empty(len(mesh.polygons), dtype=int)
mesh.polygons.foreach_get("material_index", material_indices)
remapped_indices = index_remapping[material_indices]
mesh.polygons.foreach_set("material_index", remapped_indices)
bm.from_mesh(mesh)
bpy.data.meshes.remove(mesh)
brick_mesh = bpy.data.meshes.new(brick_name)
bm.to_mesh(brick_mesh)
for material in used_materials:
brick_mesh.materials.append(material)
if useNormals:
brick_mesh.use_auto_smooth = True
brick_obj = bpy.data.objects.new(brick_name, brick_mesh)
brick_obj.matrix_world = part_matrix
col.objects.link(brick_obj)
for mesh in geometriecache.values():
if type(mesh) == bpy.types.Mesh:
bpy.data.meshes.remove(mesh)
useplane = True
if useplane is True: # write the floor plane in case True

View File

@@ -1,10 +1,24 @@
from pathlib import Path
import bpy
from .color_conversions import *
LUTB_BAKE_MAT = "VertexColor"
LUTB_TRANSPARENT_MAT = "VertexColorTransparent"
LUTB_FORCE_WHITE_MAT = "ForceWhite"
LUTB_OTHER_MATS = ["VertexColorAO"]
LUTB_BAKE_MATS = (LUTB_BAKE_MAT, LUTB_TRANSPARENT_MAT, LUTB_FORCE_WHITE_MAT, *LUTB_OTHER_MATS)
LUTB_IR_OPAQUE_MAT = "ItemRender_Opaque"
LUTB_IR_TRANSPARENT_MAT = "ItemRender_Transparent"
LUTB_IR_METAL_MAT = "ItemRender_Metal"
LUTB_IR_MATS = (LUTB_IR_OPAQUE_MAT, LUTB_IR_TRANSPARENT_MAT, LUTB_IR_METAL_MAT)
LUTB_IR_SCENE = "ItemRender"
# COLORS HERE ARE EXPECTED TO BE IN LINEAR COLOR SPACE
# IF YOU INPUT AN SRGB COLOR LIKE LU/LDD USE IT MUST BE CONVERTED TO LINEAR EITHER MANUALLY OR BY CALLING SRGB2LIN AS YOU WILL OCCASIONALLY SEE BELOW
# SIGNED, JAMIE (WHO WAS A SMIDGE ANNOYED AT THIS BUT ULTIMATELY PASSES NO JUDGEMENT ON THE AUTHORS OF THIS TOOL)
# Solid/Opaque
MATERIALS_OPAQUE = {
@@ -112,21 +126,21 @@ MATERIALS_OPAQUE["325"] = MATERIALS_OPAQUE["222"]
# Transparent
MATERIALS_TRANSPARENT = {
"20" : (0.930111, 0.672443, 0.250158, 1.0),
"40" : (0.854993, 0.854993, 0.854993, 1.0),
"41" : (0.745405, 0.023153, 0.022174, 1.0),
"42" : (0.467784, 0.745404, 0.863157, 1.0),
"43" : (0.08022, 0.439657, 0.806952, 1.0),
"44" : (0.947307, 0.863157, 0.141263, 1.0),
"47" : (0.791298, 0.132868, 0.059511, 1.0),
"48" : (0.119539, 0.450786, 0.155927, 1.0),
"49" : (0.930111, 0.83077, 0.099899, 1.0),
"111" : (0.496933, 0.445201, 0.341914, 1.0),
"113" : (0.854993, 0.337164, 0.545725, 1.0),
"126" : (0.332452, 0.296138, 0.571125, 1.0),
"143" : (0.623961, 0.760525, 0.930111, 1.0),
"182" : (0.8388, 0.181164, 0.004391, 1.0),
"311" : (0.42869, 0.64448, 0.061246, 1.0),
"20" : (0.930111, 0.672443, 0.250158, 1.0),
"40" : (0.854993, 0.854993, 0.854993, 1.0),
"41" : srgb2lin((0.674509, 0.0, 0.0, 1.0)),
"42" : srgb2lin((0.244106, 0.720966, 0.772058, 1.0)),
"43" : srgb2lin((0.031372, 0.285668, 0.643137, 1.0)),
"44" : srgb2lin((0.858, 0.771375, 0.0, 1.0)),
"47" : srgb2lin((0.986, 0.336526, 0.120035, 1.0)),
"48" : srgb2lin((0.0, 0.391, 0.0, 1.0)),
"49" : srgb2lin((0.697, 1.0, 0.0, 1.0)),
"111" : srgb2lin((0.741177, 0.670588, 0.639216, 1.0)),
"113" : srgb2lin((0.754717, 0.060520, 0.541647, 1.0)),
"126" : srgb2lin((0.267974, 0.196078, 0.627451, 1.0)),
"143" : srgb2lin((0.325985, 0.551358, 0.821, 1.0)),
"182" : srgb2lin((0.913726, 0.524575, 0.0156863, 1.0)),
"311" : srgb2lin((0.454640, 0.788235, 0.0980392, 1.0)),
}
# Duplicate Transparent
@@ -189,6 +203,7 @@ CUSTOM_VARIATION = {
"21" : 1.4,
"23" : 1.25,
"24" : 1.5,
"26" : 0.4,
"28" : 0.8,
"37" : 0.8,
"135" : 0.85,
@@ -204,7 +219,33 @@ CUSTOM_VARIATION = {
"326" : 1.75,
}
for dictionary in (MATERIALS_OPAQUE, MATERIALS_TRANSPARENT, MATERIALS_GLOW, MATERIALS_METALLIC, CUSTOM_VARIATION):
ICON_MATERIALS_OPAQUE = {
"1" : srgb2lin((0.7, 0.7, 0.7, 1.0)),
"26" : srgb2lin((0.01, 0.01, 0.01, 1.0)),
}
ICON_MATERIALS_TRANSPARENT = {
}
ICON_MATERIALS_GLOW = {
}
ICON_MATERIALS_METALLIC = {
}
ICON_RENDER_DISABLE_SUBDIV = {
}
dicts = (
MATERIALS_OPAQUE, MATERIALS_TRANSPARENT, MATERIALS_GLOW, MATERIALS_METALLIC,
ICON_MATERIALS_OPAQUE, ICON_MATERIALS_TRANSPARENT, ICON_MATERIALS_GLOW,
ICON_MATERIALS_METALLIC, CUSTOM_VARIATION,
)
for dictionary in dicts:
for keys, value in list(dictionary.items()):
if not type(keys) == str:
dictionary.pop(keys)
@@ -212,49 +253,48 @@ for dictionary in (MATERIALS_OPAQUE, MATERIALS_TRANSPARENT, MATERIALS_GLOW, MATE
dictionary[key] = value
def get_lutb_bake_mat(parent_op=None):
if not LUTB_BAKE_MAT in bpy.data.materials:
append_resources(parent_op)
append_resources(parent_op)
return bpy.data.materials.get(LUTB_BAKE_MAT)
def get_lutb_transparent_mat(parent_op=None):
if not LUTB_TRANSPARENT_MAT in bpy.data.materials:
append_resources(parent_op)
append_resources(parent_op)
return bpy.data.materials.get(LUTB_TRANSPARENT_MAT)
def get_lutb_force_white_mat(parent_op=None):
if not LUTB_FORCE_WHITE_MAT in bpy.data.materials:
append_resources(parent_op)
append_resources(parent_op)
return bpy.data.materials.get(LUTB_FORCE_WHITE_MAT)
def get_lutb_ir_opaque_mat(parent_op=None):
append_resources(parent_op)
return bpy.data.materials.get(LUTB_IR_OPAQUE_MAT)
def get_lutb_ir_transparent_mat(parent_op=None):
append_resources(parent_op)
return bpy.data.materials.get(LUTB_IR_TRANSPARENT_MAT)
def get_lutb_ir_metal_mat(parent_op=None):
append_resources(parent_op)
return bpy.data.materials.get(LUTB_IR_METAL_MAT)
def get_lutb_ir_scene(parent_op=None, copy=True):
append_resources(parent_op)
return bpy.data.scenes.get(LUTB_IR_SCENE).copy()
def append_resources(parent_op=None):
blend_file = Path(__file__).parent / "resources.blend"
for mat_name in (LUTB_BAKE_MAT, LUTB_TRANSPARENT_MAT, LUTB_FORCE_WHITE_MAT, *LUTB_OTHER_MATS):
for mat_name in (*LUTB_BAKE_MATS, *LUTB_IR_MATS):
if not mat_name in bpy.data.materials:
bpy.ops.wm.append(directory=str(blend_file / "Material"), filename=mat_name)
if not mat_name in bpy.data.materials and parent_op:
parent_op.report({"WARNING"},
f"Failed to append \"{mat_name}\" from \"{blend_file}\"."
)
def srgb2lin(color):
result = []
for srgb in color:
if srgb <= 0.0404482362771082:
lin = srgb / 12.92
else:
lin = pow(((srgb + 0.055) / 1.055), 2.4)
result.append(lin)
return result
def lin2srgb(color):
result = []
for lin in color:
if lin > 0.0031308:
srgb = 1.055 * (pow(lin, (1.0 / 2.4))) - 0.055
else:
srgb = 12.92 * lin
result.append(srgb)
return result
scene_name = LUTB_IR_SCENE
if not scene_name in bpy.data.scenes:
bpy.ops.wm.append(directory=str(blend_file / "Scene"), filename=scene_name)
if not scene_name in bpy.data.scenes and parent_op:
parent_op.report({"WARNING"},
f"Failed to append \"{scene_name}\" from \"{blend_file}\"."
)

View File

@@ -0,0 +1,19 @@
def srgb2lin(color):
result = []
for srgb in color:
if srgb <= 0.0404482362771082:
lin = srgb / 12.92
else:
lin = pow(((srgb + 0.055) / 1.055), 2.4)
result.append(lin)
return result
def lin2srgb(color):
result = []
for lin in color:
if lin > 0.0031308:
srgb = 1.055 * (pow(lin, (1.0 / 2.4))) - 0.055
else:
srgb = 12.92 * lin
result.append(srgb)
return result

Binary file not shown.

View File

@@ -31,8 +31,6 @@ class LUTB_OT_process_model(bpy.types.Operator):
scene.render.engine = "CYCLES"
scene.cycles.device = "GPU" if scene.lutb_process_use_gpu else "CPU"
self.precombine_bricks(context, scene.collection.children)
for obj in scene.collection.all_objects:
if not obj.type == "MESH":
continue
@@ -77,12 +75,15 @@ class LUTB_OT_process_model(bpy.types.Operator):
if not scene.lutb_keep_uvs:
self.clear_uvs(all_objects)
if scene.lutb_reset_orientation:
self.reset_orientation(all_objects)
if scene.lutb_apply_vertex_colors:
if scene.lutb_correct_colors:
self.correct_colors(context, all_objects)
if scene.lutb_use_color_variation:
self.apply_color_variation(context, all_objects)
self.apply_color_variation(context, scene.collection.children)
self.apply_vertex_colors(context, all_objects)
@@ -107,98 +108,27 @@ class LUTB_OT_process_model(bpy.types.Operator):
bpy.ops.object.select_all(action="DESELECT")
for obj in all_objects:
obj.select_set(True)
context.view_layer.objects.active = all_objects[0]
end = timer()
print(f"finished process model in {end - start:.2f}s")
return {"FINISHED"}
def precombine_bricks(self, context, collections):
bricks = {}
for collection in collections:
if not collection.children:
continue
for lod_collection in collection.children:
if not lod_collection.name[-5:] in LOD_SUFFIXES:
continue
for obj in lod_collection.all_objects:
if not (obj.type == "MESH" and obj.parent):
continue
if brick := bricks.get(obj.parent):
brick.append(obj)
else:
bricks[obj.parent] = [obj]
if not bricks:
return
combined_bricks = {}
for parent_empty, children in bricks.items():
bm = bmesh.new()
materials = {}
for child in children:
mesh = child.data
for old_mat_index, material in enumerate(mesh.materials):
mat_name = material.name.rsplit(".", 1)[0]
if mat := materials.get(mat_name):
new_mat_index = mat[1]
else:
new_mat_index = len(materials)
materials[mat_name] = (material, new_mat_index)
if old_mat_index != new_mat_index:
for polygon in mesh.polygons:
if polygon.material_index == old_mat_index:
polygon.material_index = new_mat_index
bm.from_mesh(mesh)
combined = children[0]
combined.name = parent_empty.name
combined.parent = None
combined.matrix_world = parent_empty.matrix_world.copy()
bm.to_mesh(combined.data)
combined.data.materials.clear()
# dictionaries are guaranteed to be ordered in 3.7+ (see PEP 468)
for material, _ in materials.values():
combined.data.materials.append(material)
bpy.data.objects.remove(parent_empty)
for obj in children[1:]:
bpy.data.objects.remove(obj)
combined_bricks[combined.name] = combined
brick_base_mats = {}
for name, obj in combined_bricks.items():
if name[-4:-3] == ".":
base_name = name.rsplit(".", 1)[0]
if not (base_mats := brick_base_mats.get(base_name)):
if not (obj_base := combined_bricks.get(base_name)):
continue
mats = obj_base.data.materials.values()
base_mats = {mat.name.rsplit(".", 1)[0]: mat for mat in mats}
brick_base_mats[base_name] = base_mats
for i, mat in enumerate(obj.data.materials):
if (base_mat := base_mats.get(mat.name.rsplit(".", 1)[0])):
obj.data.materials[i] = base_mat
for obj in list(collection.all_objects):
if obj.type == "EMPTY":
bpy.data.objects.remove(obj)
def clear_uvs(self, objects):
for obj in objects:
for uv_layer in reversed(obj.data.uv_layers):
obj.data.uv_layers.remove(uv_layer)
def reset_orientation(self, objects):
for obj in objects:
obj.select_set(True)
bpy.ops.object.transform_apply()
obj.rotation_euler = (radians(-90), 0, 0)
bpy.ops.object.transform_apply()
obj.rotation_euler = (radians(90), 0, 0)
obj.select_set(False)
def combine_objects(self, context, collections):
scene = context.scene
@@ -250,19 +180,25 @@ class LUTB_OT_process_model(bpy.types.Operator):
elif color := MATERIALS_TRANSPARENT.get(name):
material.diffuse_color = color
def apply_color_variation(self, context, objects):
def apply_color_variation(self, context, collections):
initial_state = random.getstate()
variation = context.scene.lutb_color_variation
for obj in objects:
for material in obj.data.materials:
color = Color(material.diffuse_color[:3])
gamma = color.v ** (1 / 2.224)
custom_variation = CUSTOM_VARIATION.get(material.name.rsplit(".", 1)[0])
var = variation if custom_variation is None else variation * custom_variation
gamma += random.uniform(-var / 200, var / 200)
for collection in collections:
for lod_collection in collection.children:
random.setstate(initial_state)
for obj in list(lod_collection.objects):
if obj.type == "MESH":
for material in obj.data.materials:
color = Color(material.diffuse_color[:3])
gamma = color.v ** (1 / 2.224)
color.v = min(max(0, gamma), 1) ** 2.224
material.diffuse_color = (*color, 1.0)
custom_variation = CUSTOM_VARIATION.get(material.name.rsplit(".", 1)[0])
var = variation if custom_variation is None else variation * custom_variation
gamma += random.uniform(-var / 200, var / 200)
color.v = min(max(0, gamma), 1) ** 2.224
material.diffuse_color = (*color, 1.0)
def apply_vertex_colors(self, context, objects):
scene = context.scene
@@ -374,6 +310,7 @@ class LUTB_OT_process_model(bpy.types.Operator):
autoremove=scene.lutb_hsr_autoremove,
vc_pre_pass=scene.lutb_hsr_vc_pre_pass,
vc_pre_pass_samples=scene.lutb_hsr_vc_pre_pass_samples,
ignore_lights=scene.lutb_hsr_ignore_lights,
tris_to_quads=scene.lutb_hsr_tris_to_quads,
pixels_between_verts=scene.lutb_hsr_pixels_between_verts,
samples=scene.lutb_hsr_samples,
@@ -418,10 +355,10 @@ class LUTB_OT_process_model(bpy.types.Operator):
for obj in list(lod_collection.all_objects):
is_transparent = bool(obj.get(IS_TRANSPARENT))
shader_prefix = "S01" if is_transparent else scene.lutb_shader_prefix
shader_prefix = "01" if is_transparent else scene.lutb_shader_opaque
type_prefix = "Alpha" if is_transparent else "Opaque"
obj_name = obj.name.rsplit(".", 1)[0]
name = f"{shader_prefix}_{type_prefix}_{obj_name}"[:60]
name = f"S{shader_prefix}_{type_prefix}_{obj_name}"[:60]
obj.name = name
if (node := ni_nodes.get(name)):
@@ -534,6 +471,8 @@ class LUTB_PT_process_model(LUToolboxPanel, bpy.types.Panel):
layout.prop(scene, "lutb_keep_uvs")
layout.prop(scene, "lutb_reset_orientation")
class LUTB_PT_apply_vertex_colors(LUToolboxPanel, bpy.types.Panel):
bl_label = "Apply Vertex Colors"
bl_parent_id = "LUTB_PT_process_model"
@@ -600,6 +539,7 @@ class LUTB_PT_remove_hidden_faces(LUToolboxPanel, bpy.types.Panel):
row.prop(scene, "lutb_hsr_vc_pre_pass_samples", slider=True)
row.enabled = scene.lutb_hsr_vc_pre_pass
layout.prop(scene, "lutb_hsr_ignore_lights")
layout.prop(scene, "lutb_hsr_tris_to_quads")
layout.prop(scene, "lutb_hsr_use_ground_plane")
layout.prop(scene, "lutb_hsr_pixels_between_verts", slider=True)
@@ -622,7 +562,10 @@ class LUTB_PT_setup_metadata(LUToolboxPanel, bpy.types.Panel):
layout.active = scene.lutb_setup_lod_data
layout.prop(scene, "lutb_correct_orientation")
layout.prop(scene, "lutb_shader_prefix")
layout.prop(scene, "lutb_shader_opaque")
layout.prop(scene, "lutb_shader_glow")
layout.prop(scene, "lutb_shader_metal")
layout.prop(scene, "lutb_shader_superemissive")
layout.prop(scene, "lutb_lod0")
layout.prop(scene, "lutb_lod1")
layout.prop(scene, "lutb_lod2")
@@ -644,6 +587,8 @@ def register():
"Combine transparent bricks")
bpy.types.Scene.lutb_keep_uvs = BoolProperty(name="Keep UVs", default=False, description=""\
"Keep the original mesh UVs. Disabling this results in a model with no UVs")
bpy.types.Scene.lutb_reset_orientation = BoolProperty(name="Reset Orientation", default=True, description=""\
"Reset the orientation so the model is properly upright for visual effects.")
bpy.types.Scene.lutb_correct_colors = BoolProperty(name="Correct Colors", default=False, description=""\
"Remap model colors to LU color palette. "\
@@ -674,6 +619,8 @@ def register():
description=LUTB_OT_remove_hidden_faces.__annotations__["vc_pre_pass"].keywords["description"])
bpy.types.Scene.lutb_hsr_vc_pre_pass_samples = IntProperty(name="Pre-Pass Samples", min=0, default=32, soft_max=64,
description=LUTB_OT_remove_hidden_faces.__annotations__["vc_pre_pass_samples"].keywords["description"])
bpy.types.Scene.lutb_hsr_ignore_lights = BoolProperty(name="Ignore Lights", default=True,
description=LUTB_OT_remove_hidden_faces.__annotations__["ignore_lights"].keywords["description"])
bpy.types.Scene.lutb_hsr_tris_to_quads = BoolProperty(name="Tris to Quads", default=True,
description=LUTB_OT_remove_hidden_faces.__annotations__["tris_to_quads"].keywords["description"])
bpy.types.Scene.lutb_hsr_pixels_between_verts = IntProperty(name="Pixels Between Vertices", min=0, default=5, soft_max=15,
@@ -685,7 +632,10 @@ def register():
bpy.types.Scene.lutb_setup_lod_data = BoolProperty(name="Setup LOD Data", default=True)
bpy.types.Scene.lutb_correct_orientation = BoolProperty(name="Correct Orientation", default=True)
bpy.types.Scene.lutb_shader_prefix = StringProperty(name="Shader Prefix", default="S01")
bpy.types.Scene.lutb_shader_opaque = StringProperty(name="Opaque Shader", default="01")
bpy.types.Scene.lutb_shader_glow = StringProperty(name="Glow Shader", default="72")
bpy.types.Scene.lutb_shader_metal = StringProperty(name="Metal Shader", default="88")
bpy.types.Scene.lutb_shader_superemissive = StringProperty(name="SuperEmissive Shader", default="19")
bpy.types.Scene.lutb_lod0 = FloatProperty(name="LOD 0", soft_min=0.0, default=0.0, soft_max=25.0)
bpy.types.Scene.lutb_lod1 = FloatProperty(name="LOD 1", soft_min=0.0, default=50.0, soft_max=100.0)
bpy.types.Scene.lutb_lod2 = FloatProperty(name="LOD 2", soft_min=0.0, default=100.0, soft_max=280.0)
@@ -697,6 +647,7 @@ def unregister():
del bpy.types.Scene.lutb_combine_objects
del bpy.types.Scene.lutb_combine_transparent
del bpy.types.Scene.lutb_keep_uvs
del bpy.types.Scene.lutb_reset_orientation
del bpy.types.Scene.lutb_correct_colors
del bpy.types.Scene.lutb_use_color_variation
@@ -712,6 +663,7 @@ def unregister():
del bpy.types.Scene.lutb_hsr_autoremove
del bpy.types.Scene.lutb_hsr_vc_pre_pass
del bpy.types.Scene.lutb_hsr_vc_pre_pass_samples
del bpy.types.Scene.lutb_hsr_ignore_lights
del bpy.types.Scene.lutb_hsr_tris_to_quads
del bpy.types.Scene.lutb_hsr_pixels_between_verts
del bpy.types.Scene.lutb_hsr_samples
@@ -719,7 +671,10 @@ def unregister():
del bpy.types.Scene.lutb_setup_lod_data
del bpy.types.Scene.lutb_correct_orientation
del bpy.types.Scene.lutb_shader_prefix
del bpy.types.Scene.lutb_shader_opaque
del bpy.types.Scene.lutb_shader_glow
del bpy.types.Scene.lutb_shader_metal
del bpy.types.Scene.lutb_shader_superemissive
del bpy.types.Scene.lutb_lod0
del bpy.types.Scene.lutb_lod1
del bpy.types.Scene.lutb_lod2

View File

@@ -22,6 +22,9 @@ class LUTB_OT_remove_hidden_faces(bpy.types.Operator):
"definitely visible.")
vc_pre_pass_samples : IntProperty(min=1, default=32, description=""\
"Number of samples to render for vertex color pre-pass")
ignore_lights : BoolProperty(default=True, description=""\
"Hide all custom lights while processing."\
"Disable this if you want to manually affect the lighting.")
tris_to_quads : BoolProperty(default=True, description=""\
"Convert models triangles to quads for faster, more efficient HSR. "\
"Quads are then converted back to tris afterwards. "\
@@ -63,9 +66,15 @@ class LUTB_OT_remove_hidden_faces(bpy.types.Operator):
hidden_objects = []
for obj in list(scene.collection.all_objects):
if obj not in {target_obj, ground_plane} and not obj.hide_render:
obj.hide_render = True
hidden_objects.append(obj)
if obj.hide_render:
continue
if obj in {target_obj, ground_plane}:
continue
if not self.ignore_lights and obj.type == "LIGHT":
continue
obj.hide_render = True
hidden_objects.append(obj)
scene_override = self.setup_scene_override(context)
@@ -105,8 +114,22 @@ class LUTB_OT_remove_hidden_faces(bpy.types.Operator):
if self.autoremove:
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.delete(type="FACE")
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.quads_convert_to_tris(quad_method="FIXED")
bpy.ops.object.mode_set(mode="OBJECT")
end = timer()
n = len(hidden_indices)
total = len(select)
operation = "removed" if self.autoremove else "found"
print(
f"hsr info: {operation} {n}/{total} hidden faces ({n / total:.2%}) "\
f"in {end - start:.2f}s"
)
else:
print("hsr info: found no hidden faces")
bpy.data.scenes.remove(scene_override)
for obj in hidden_objects:
@@ -115,15 +138,6 @@ class LUTB_OT_remove_hidden_faces(bpy.types.Operator):
if ground_plane:
bpy.data.objects.remove(ground_plane)
end = timer()
n = len(hidden_indices)
total = len(select)
operation = "removed" if self.autoremove else "found"
print(
f"hsr info: {operation} {n}/{total} hidden faces ({n / total:.2%}) "\
f"in {end - start:.2f}s"
)
return {"FINISHED"}
def add_ground_plane(self, context):