72 Commits

Author SHA1 Message Date
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
d6c137dba0 Increased AO radius to better match LU (5.0m) 2022-03-18 22:28:23 -04:00
bc869eb9a7 Resolves #9 2022-03-17 23:17:58 -05:00
eafde9ac0b Update README.md 2022-03-17 01:11:39 -04:00
30fb77888e Update README.md 2022-03-17 01:07:46 -04:00
479891617b Update README.md
Added Toolbox features description.
2022-03-17 01:01:12 -04:00
26af3e241b actuallu show the option for LOD3 2022-03-14 22:53:30 -05:00
996c0210e9 Dynamic LOD 3 rendering
(god forgive me)
2022-03-14 22:49:34 -05:00
3b8f27ad08 LOD3
linting cleaning
2022-03-14 21:20:54 -05:00
bd5f00bf4b Update resources.blend. Added ItemRender materials. Added ModelRender and ItemRender scenes. 2022-03-14 15:57:29 -04:00
Bobbe
6976646685 Remove ambient world light when baking with AO only 2022-03-12 17:44:22 +01:00
Bobbe
98f34e53c6 Fix not linking HSR ground plane 2022-03-12 17:28:09 +01:00
Bobbe
19c058f038 Fix division by zero if no faces remain after pre pass in HSR 2022-03-12 17:21:40 +01:00
8f761df032 Update resources.blend. Set nif mat emissive value to 0.1 on VertexColor and VertexColorAO materials. Added GlowOnly material. 2022-03-12 00:03:07 -05:00
5c359a6af7 Toggle to use normals when importing
make clear collections variable to be consistent with wording
fix typo in readme
2022-03-11 13:15:15 -06:00
2b5b809f21 Update README.md
Minor spelling edits.
2022-03-11 13:10:07 -05:00
08116d705e Merge pull request #6 from Squareville/issue-4
Update Readme with info about the importer
2022-03-11 13:08:12 -05:00
250341bffa Resolves #3
Have enable autosmooth for the custom normals to be respected
and actually do anything
2022-03-11 11:36:18 -06:00
Bobbe
5be7335ac8 Rework ao only, Add force white option to bake lighting 2022-03-11 17:56:19 +01:00
d5f60af5e6 Update resources.blend. Added ForceWhite material. 2022-03-11 10:54:04 -05:00
e5f9e19dfc Update Readme with info about the importer
Naming consistency/sensibility
2022-03-10 23:16:10 -06:00
de78dcf350 use the correct custom normal setting method 2022-03-10 17:14:31 -06:00
bee8515d6e Removed material 294 from transparent materials. 2022-03-10 13:47:21 -05:00
Bobbe
4a79913ab4 Make pre-pass actually work on per vertex basis 2022-03-10 17:54:47 +01:00
Bobbe
6cef63975a Optimize hsr uv setup, image evaluation 2022-03-10 17:28:46 +01:00
Bobbe
42f5cc87f8 Implement vc pre pass for hsr 2022-03-10 17:12:20 +01:00
Bobbe
3ad5ba95ed Refactor process model, Replace np zeros calls with empty if possible 2022-03-09 20:16:17 +01:00
Bobbe
3a5c46f44a Finish refactoring HSR code, Prepare for vc pre pass 2022-03-09 20:15:21 +01:00
d6384d77d0 Renamed "Clear Colllection" to "Overwrite Scene". 2022-03-06 23:48:33 -05:00
5cdab7f109 option to clear scene during import
resolves #2
2022-03-06 19:45:38 -06:00
c163e9e21f remove hacky normals code 2022-03-06 11:58:02 -06:00
Bobbe
5228e89f0e Refactor HSR code 2022-03-05 20:39:51 +01:00
Bobbe
010d483bee Add/Edit a bunch of tooltips 2022-03-05 18:01:55 +01:00
Bobbe
9421dfbbfd Improve duplicate color correction handling 2022-03-05 17:57:54 +01:00
Bobbe
1f0abd97a3 Disable fast GI and clamping during HSR, Set bounces for HSR 2022-03-04 17:26:53 +01:00
Bobbe
14a6d6348b Use context overrides for baking, Disable denoising for baking 2022-03-04 17:25:43 +01:00
Bobbe
d388a495c6 Simplify dynamic lod distance handling 2022-03-04 16:17:58 +01:00
1782ece2c8 normals!
cleanup
2022-03-03 14:43:02 -06:00
9009add828 cleanup, map missing colors 2022-03-03 10:29:29 -06:00
11 changed files with 1300 additions and 665 deletions

View File

@@ -1,35 +1,64 @@
[![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 Addon which adds a bunch of useful tools to prepare models for use in LEGO Universe.
A Blender Add-on which adds a bunch of useful tools to prepare models for use in LEGO Universe.
^ edit this / add more information here
## Features
![banner](https://raw.githubusercontent.com/30350n/lu-toolbox/master/images/banner.png)
* Custom LEGO Exchange Format (.lxf/.lxfml) Importer.
* Automatic model preparation with many useful processes.
* Custom workflow for baking materials, lighting, ambient occlusion, alpha, and more to vertex colors.
* Automatic pathtraced hidden surface removal to clean out model interiors of unseen geometry.
* Many options and toggles to fit a variety of artist workflows.
<hr>
![banner](images/banner.png)
## Installation
1. Download the latest release from [here](https://github.com/30350n/lu-toolbox/releases/latest).
1. Download the latest release from [here](https://github.com/Squareville/lu-toolbox/releases/latest).
2. Start Blender and navigate to "Edit -> Preferences -> Add-ons"
3. (optional) If you already have an older version of the addon installed, disable it, remove it and restart blender.
3. (optional) If you already have an older version of the add-on installed, disable it, remove it and restart blender.
4. Hit "Install" and select the zip archive you downloaded.
5. In the Add-on's preferences, set `Brick DB` to your LU client res folder or extracted Brick DB
After installation you can find the addon in the right sidepanel (N-panel) of the 3D Viewport.
After installation you can find the add-on in the right sidepanel (N-panel) of the 3D Viewport.
#### Blender Version
The minimum compatible Blender version is 2.93.
The addon is generally compatible with Blender 3.0+ but there are a few breaking bugs with vertex color baking that make it almost completely unusable.
The minimum compatible Blender version for LU Toolbox v2.0 is Blender 3.1.
## Documentation
- [DLU - Asset Creation Guide](https://docs.google.com/document/d/15YDtHg3-i3Pn6HTEFkpAjKsvUF6u49ZsQam5b614YRw) by [cdmpants](https://github.com/cdmpants)
### LEGO Exchange Format Importer (.lxf/.lxfml)
This importer is derived from [sttng's ImportLDD Add-on](https://github.com/sttng/ImportLDD) with a few changes:
* Support for importing one or multiple LODs at a time
* Enchanced Brick DB handling:
* Support for defining a path to any Brick DB via Add-on Preferences
* Direct support for using LU's brick db without needing to extract it manually
* You can use the `client/res/` folder directly as a Brick DB source
* the `brickdb.zip` will automatically be unzipped for you if it's not already
* Dropped support for using LDD's `db.lif` directly since it doesn't provide LODs
* Consolidated color support for LU's color palette:
* Colors outside of LU's supported palette will be coerced to the closest color
* Please report any instances of missing colors so that they can be added
* Missing data handling:
* Bricks missing from brick database will be skipped
* Completely missing colors will be set to black (Color ID 26)
* Color missing from secondary brick geo is handled correctly
* Overwrite Scene Option:
* Delete all objects and collections from Blender scene before importing.
## Screenshots
<div float="left">
<img src="https://raw.githubusercontent.com/30350n/lu-toolbox/master/images/blender.PNG" width="52.5%" />
<img src="https://raw.githubusercontent.com/30350n/lu-toolbox/master/images/windmill.png" width="35%" />
<img src="https://raw.githubusercontent.com/30350n/lu-toolbox/master/images/castle.png" width="35%" />
<img src="https://raw.githubusercontent.com/30350n/lu-toolbox/master/images/mech.png" width="23.33%" />
<img src="images/blender.PNG" width="52.5%" />
<img src="images/windmill.png" width="35%" />
<img src="images/castle.png" width="35%" />
<img src="images/mech.png" width="23.33%" />
</div>

View File

@@ -1,20 +1,24 @@
bl_info = {
"name": "LU Toolbox",
"author": "Bobbe",
"version": (1, 7, 0),
"version": (2, 3, 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

@@ -4,7 +4,7 @@ import numpy as np
from timeit import default_timer as timer
from .process_model import IS_TRANSPARENT
from .materials import get_lutb_ao_only_mat
from .materials import get_lutb_force_white_mat
WHITE_AMBIENT = "LUTB_WHITE_AMBIENT"
@@ -25,16 +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_use_gpu")
layout.prop(scene, "lutb_bake_use_white_ambient")
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")
col = layout.column()
col.prop(scene, "lutb_bake_ao_only")
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"
@@ -68,39 +95,50 @@ class LUTB_OT_bake_lighting(bpy.types.Operator):
start = timer()
scene = context.scene
scene_override = scene.copy()
render = scene_override.render
cycles = scene_override.cycles
render.engine = "CYCLES"
cycles.use_denoising = False
cycles.bake_type = "COMBINED"
cycles.caustics_reflective = False
cycles.caustics_refractive = False
render.bake.use_pass_direct = True
render.bake.use_pass_indirect = True
render.bake.use_pass_diffuse = True
render.bake.use_pass_glossy = False
render.bake.use_pass_transmission = True
render.bake.use_pass_emit = True
render.bake.target = "VERTEX_COLORS"
cycles.use_fast_gi = True
cycles.ao_bounces_render = scene.lutb_bake_fast_gi_bounces
cycles.device = "GPU" if scene.lutb_process_use_gpu else "CPU"
cycles.samples = scene.lutb_bake_samples
old_world = scene.world
if scene.lutb_bake_use_white_ambient:
if not (world := bpy.data.worlds.get(WHITE_AMBIENT)):
world = bpy.data.worlds.new(WHITE_AMBIENT)
world.color = (1.0, 1.0, 1.0)
scene.world = world
scene_override.world = world
scene.render.engine = "CYCLES"
scene.cycles.bake_type = "COMBINED"
scene.cycles.caustics_reflective = False
scene.cycles.caustics_refractive = False
scene.render.bake.use_pass_direct = True
scene.render.bake.use_pass_indirect = True
scene.render.bake.use_pass_diffuse = True
scene.render.bake.use_pass_glossy = False
scene.render.bake.use_pass_transmission = True
scene.render.bake.use_pass_emit = True
emission_strength = scene.lutb_bake_glow_strength
scene.cycles.use_fast_gi = True
scene.cycles.ao_bounces = scene.lutb_bake_fast_gi_bounces
scene.cycles.ao_bounces_render = scene.lutb_bake_fast_gi_bounces
scene.cycles.device = "GPU" if scene.lutb_process_use_gpu else "CPU"
old_samples = scene.cycles.samples
scene.cycles.samples = scene.lutb_bake_samples
old_max_bounces = scene.cycles.max_bounces
ao_only_world_override = None
if scene.lutb_bake_ao_only:
scene.cycles.max_bounces = 0
cycles.max_bounces = 0
cycles.fast_gi_method = "ADD"
cycles.samples = scene.lutb_bake_ao_samples
old_active_obj = bpy.context.object
ao_only_world_override = bpy.data.worlds.new("AO_ONLY")
ao_only_world_override.color = (0.0, 0.0, 0.0)
ao_only_world_override.light_settings.ao_factor = 1.0
ao_only_world_override.light_settings.distance = 5.0
scene_override.world = ao_only_world_override
emission_strength *= scene.lutb_bake_glow_multiplier
hidden_objects = []
for obj in list(scene.collection.all_objects):
@@ -108,20 +146,18 @@ class LUTB_OT_bake_lighting(bpy.types.Operator):
obj.hide_render = True
hidden_objects.append(obj)
for obj in (selected := list(context.selected_objects)):
target_objects = scene.collection.all_objects
if scene.lutb_bake_selected_only:
target_objects = context.selected_objects
old_active_obj = context.object
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 parent_collection in bpy.data.collections:
other_lod_colls |= set(parent_collection.children) - set((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
@@ -139,22 +175,49 @@ 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)
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:
mesh.materials[0] = scene.lutb_bake_mat_override
elif scene.lutb_bake_force_to_white:
if material := get_lutb_force_white_mat(self):
mesh.materials[0] = material
if mesh.materials[0].use_nodes:
for node in mesh.materials[0].node_tree.nodes:
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
old_material = mesh.materials[0]
context_override = context.copy()
context_override["scene"] = scene_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
if scene.lutb_bake_ao_only and not scene.lutb_bake_use_mat_override:
if material := get_lutb_ao_only_mat(self):
mesh.materials[0] = material
if scene.lutb_bake_use_mat_override:
mesh.materials[0] = scene.lutb_bake_mat_override
bpy.ops.object.bake(target="VERTEX_COLORS")
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:
@@ -167,8 +230,8 @@ class LUTB_OT_bake_lighting(bpy.types.Operator):
if vc_lit and (vc_alpha := mesh.vertex_colors.get("Alpha")):
n_loops = len(mesh.loops)
lit_data = np.zeros(n_loops * 4)
alpha_data = np.zeros(n_loops * 4)
lit_data = np.empty(n_loops * 4)
alpha_data = np.empty(n_loops * 4)
vc_lit.data.foreach_get("color", lit_data)
vc_alpha.data.foreach_get("color", alpha_data)
@@ -176,53 +239,62 @@ 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)
for obj in selected:
obj.select_set(True)
if ao_only_world_override:
bpy.data.worlds.remove(ao_only_world_override)
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
scene.cycles.samples = old_samples
scene.cycles.max_bounces = old_max_bounces
scene.world = old_world
end = timer()
print(f"finished bake lighting in {end - start:.2f}s")
return {"FINISHED"}
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.")
bpy.types.Scene.lutb_bake_fast_gi_bounces = IntProperty(name="Fast GI Bounces", default=3, min=0,
description="Number of samples to render for each vertex.")
bpy.types.Scene.lutb_bake_use_white_ambient = BoolProperty(name="White Ambient", default=True,
description="Sets ambient light to pure white while baking.")
bpy.types.Scene.lutb_bake_ao_only = BoolProperty(name="AO Only", default=False)
bpy.types.Scene.lutb_bake_samples = IntProperty(name="Samples", default=256, min=1, description=""\
"Number of samples to render for each vertex")
bpy.types.Scene.lutb_bake_fast_gi_bounces = IntProperty(name="Fast GI Bounces", default=3, min=0)
bpy.types.Scene.lutb_bake_glow_strength = FloatProperty(name="Glow Strength Global", default=3.0, min=0, soft_min=0.5, soft_max=5.0)
bpy.types.Scene.lutb_bake_use_white_ambient = BoolProperty(name="White Ambient", default=True, description=""\
"Sets ambient light to pure white while baking")
bpy.types.Scene.lutb_bake_ao_only = BoolProperty(name="AO Only", default=True)
bpy.types.Scene.lutb_bake_glow_multiplier = FloatProperty(name="Glow Multiplier Global", default=2.0, min=0, soft_min=0.5, soft_max=5.0)
bpy.types.Scene.lutb_bake_ao_samples = IntProperty(name="AO Samples", default=64, min=1)
bpy.types.Scene.lutb_bake_use_mat_override = BoolProperty(name="Material Override")
bpy.types.Scene.lutb_bake_force_to_white = BoolProperty(name="Force to White")
bpy.types.Scene.lutb_bake_mat_override = PointerProperty(name="Override Material", type=bpy.types.Material)
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
del bpy.types.Scene.lutb_bake_glow_strength
del bpy.types.Scene.lutb_bake_use_white_ambient
del bpy.types.Scene.lutb_bake_ao_only
del bpy.types.Scene.lutb_bake_force_to_white
del bpy.types.Scene.lutb_bake_glow_multiplier
del bpy.types.Scene.lutb_bake_ao_samples
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)

View File

@@ -9,7 +9,7 @@ def divide_mesh(context, mesh_obj, max_verts=65536, min_div_rate=0.1):
if n_verts < max_verts:
return []
buffer_co = np.zeros(n_verts * 3)
buffer_co = np.empty(n_verts * 3)
mesh.vertices.foreach_get("co", buffer_co)
vecs = buffer_co.reshape((n_verts, 3))
mean = np.sum(vecs, axis=0) / n_verts

203
lu_toolbox/icon_render.py Normal file
View File

@@ -0,0 +1,203 @@
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)
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,15 +1,13 @@
# 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,
orientation_helper,
axis_conversion,
)
ImportHelper,
axis_conversion,
)
import os
import platform
import sys
import math
import time
@@ -18,31 +16,35 @@ import zipfile
from xml.dom import minidom
import uuid
import random
import time
import numpy as np
from .materials import *
from .materials import (
MATERIALS_OPAQUE,
MATERIALS_TRANSPARENT,
MATERIALS_METALLIC,
MATERIALS_GLOW
)
# ImportHelper is a helper class, defines filename and
# invoke() function which calls the file selector.
from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.props import StringProperty, BoolProperty
from bpy.types import Operator, AddonPreferences
class ImportLDDPreferences(AddonPreferences):
bl_idname = __package__
lufilepath: StringProperty(
name="res folder",
subtype='FILE_PATH')
brickdbpath: StringProperty(
name="Brick DB",
subtype='FILE_PATH'
)
def draw(self, context):
self.layout.label(text="Path to LU res Folder")
self.layout.prop(self, "lufilepath")
self.layout.label(text="Path to Brick DB (or luclient/res/)")
self.layout.prop(self, "brickdbpath")
class ImportLDDOps(Operator, ImportHelper):
"""This appears in the tooltip of the operator and in the generated docs"""
bl_description = "Import LEGO Digital Designer scenes (.lxf/.lxfml)"
bl_idname = "import_scene.importldd" # important since its how bpy.ops.import_test.some_data is constructed
bl_description = "Import LEGO Digital Designer scenes (.lxf/.lxfml)"
bl_idname = "import_scene.importldd"
bl_label = "Import LDD scene"
# ImportHelper mixin class uses this
@@ -54,21 +56,39 @@ class ImportLDDOps(Operator, ImportHelper):
maxlen=255, # Max internal buffer length, longer would be clamped.
)
renderLOD0: BoolProperty(
importLOD0: BoolProperty(
name="LOD0",
description="Render LOD0",
description="Import LOD0",
default=True,
)
renderLOD1: BoolProperty(
importLOD1: BoolProperty(
name="LOD1",
description="Render LOD1",
description="Import LOD1",
default=False,
)
importLOD2: BoolProperty(
name="LOD2",
description="Import LOD2",
default=True,
)
renderLOD2: BoolProperty(
name="LOD2",
description="Render LOD2",
importLOD3: BoolProperty(
name="LOD3",
description="Import LOD3",
default=True,
)
overwriteScene: BoolProperty(
name="Overwrite Scene",
description="Delete all objects and collections from Blender scene before importing.",
default=True,
)
useNormals: BoolProperty(
name="Use Normals",
description="Use normals when importing geometry",
default=True,
)
@@ -77,16 +97,21 @@ class ImportLDDOps(Operator, ImportHelper):
self,
context,
self.filepath,
self.renderLOD0,
self.renderLOD1,
self.renderLOD2
self.importLOD0,
self.importLOD1,
self.importLOD2,
self.importLOD3,
self.overwriteScene,
self.useNormals
)
def register():
bpy.utils.register_class(ImportLDDOps)
bpy.utils.register_class(ImportLDDPreferences)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.utils.unregister_class(ImportLDDOps)
bpy.utils.unregister_class(ImportLDDPreferences)
@@ -97,16 +122,17 @@ def unregister():
def menu_func_import(self, context):
self.layout.operator(ImportLDDOps.bl_idname, text="LEGO Exchange Format (.lxf/.lxfml)")
def convertldd_data(self, context, filepath, renderLOD0, renderLOD1, renderLOD2):
def convertldd_data(self, context, filepath, importLOD0, importLOD1, importLOD2, importLOD3, overwriteScene, useNormals):
preferences = context.preferences
addon_prefs = preferences.addons[__package__].preferences
lufilepath = addon_prefs.lufilepath
brickdbpath = addon_prefs.brickdbpath
primaryBrickDBPath = None
if lufilepath:
primaryBrickDBPath = lufilepath
if brickdbpath:
primaryBrickDBPath = brickdbpath
else:
self.report({'ERROR'}, 'ERROR: Please define a Brick DB Path in the Addon Preferences')
return {'FINISHED'}
@@ -116,34 +142,75 @@ def convertldd_data(self, context, filepath, renderLOD0, renderLOD1, renderLOD2)
if os.path.isdir(primaryBrickDBPath):
self.report({'INFO'}, 'Found DB folder.')
start = time.process_time()
setDBFolderVars(dbfolderlocation = primaryBrickDBPath)
converter.LoadDBFolder(dbfolderlocation = primaryBrickDBPath)
setDBFolderVars(dbfolderlocation=primaryBrickDBPath)
converter.LoadDBFolder(dbfolderlocation=primaryBrickDBPath)
end = time.process_time()
self.report({'INFO'}, f'Time taken to load Brick DB: {end - start} seconds')
# Try to use LU's LODS
try:
if overwriteScene:
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
bpy.ops.outliner.orphans_purge()
for c in context.scene.collection.children:
context.scene.collection.children.unlink(c)
converter.LoadScene(filename=filepath)
col = bpy.data.collections.new(converter.scene.Name)
bpy.context.scene.collection.children.link(col)
if renderLOD0:
if importLOD0:
start = time.process_time()
converter.Export(filename=filepath, lod='0', parent_collection=col)
converter.Export(
filename=filepath,
lod='0',
parent_collection=col,
useNormals=useNormals
)
end = time.process_time()
self.report({'INFO'}, f'Time taken to Load LOD0: {end - start} seconds')
if renderLOD1:
if importLOD1:
start = time.process_time()
converter.Export(filename=filepath, lod='1', parent_collection=col)
converter.Export(
filename=filepath,
lod='1',
parent_collection=col,
useNormals=useNormals
)
end = time.process_time()
self.report({'INFO'}, f'Time taken to Load LOD1: {end - start} seconds')
if renderLOD2:
if importLOD2:
start = time.process_time()
converter.Export(filename=filepath, lod='2', parent_collection=col)
converter.Export(
filename=filepath,
lod='2',
parent_collection=col,
useNormals=useNormals
)
end = time.process_time()
self.report({'INFO'}, f'Time taken to Load LOD2: {end - start} seconds')
LOD3_exists = False
if importLOD3:
for dirpath, dirnames, filenames in os.walk(primaryBrickDBPath):
for dirname in dirnames:
if dirname == "lod3":
LOD3_exists = True
if LOD3_exists:
start = time.process_time()
converter.Export(
filename=filepath,
lod='3',
parent_collection=col,
useNormals=useNormals
)
end = time.process_time()
self.report({'INFO'}, f'Time taken to Load LOD3: {end - start} seconds')
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'}
@@ -151,8 +218,14 @@ def convertldd_data(self, context, filepath, renderLOD0, renderLOD1, renderLOD2)
PRIMITIVEPATH = '/Primitives/'
GEOMETRIEPATH = PRIMITIVEPATH + 'LOD0/'
class Matrix3D:
def __init__(self, n11=1,n12=0,n13=0,n14=0,n21=0,n22=1,n23=0,n24=0,n31=0,n32=0,n33=1,n34=0,n41=0,n42=0,n43=0,n44=1):
def __init__(
self,
n11=1, n12=0, n13=0, n14=0,
n21=0, n22=1, n23=0, n24=0,
n31=0, n32=0, n33=1, n34=0,
n41=0, n42=0, n43=0, n44=1):
self.n11 = n11
self.n12 = n12
self.n13 = n13
@@ -176,7 +249,7 @@ class Matrix3D:
{self.n31}, {self.n32}, {self.n33}, {self.n34}, \
{self.n41}, {self.n42}, {self.n43}, {self.n44}]"
def rotate(self,angle=0,axis=0):
def rotate(self, angle=0, axis=0):
c = math.cos(angle)
s = math.sin(angle)
t = 1 - c
@@ -229,19 +302,20 @@ class Matrix3D:
self.n14 * other.n41 + self.n24 * other.n42 + self.n34 * other.n43 + self.n44 * other.n44
)
class Point3D:
def __init__(self, x=0,y=0,z=0):
def __init__(self, x=0, y=0, z=0):
self.x = x
self.y = y
self.z = z
def __str__(self):
return '[{0},{1},{2}]'.format(self.x, self.y,self.z)
return '[{0},{1},{2}]'.format(self.x, self.y, self.z)
def string(self,prefix = "v"):
return '{0} {1:f} {2:f} {3:f}\n'.format(prefix ,self.x , self.y, self.z)
def string(self, prefix="v"):
return '{0} {1:f} {2:f} {3:f}\n'.format(prefix, self.x, self.y, self.z)
def transformW(self,matrix):
def transformW(self, matrix):
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z
@@ -249,7 +323,7 @@ class Point3D:
self.y = y
self.z = z
def transform(self,matrix):
def transform(self, matrix):
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z + matrix.n41
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z + matrix.n42
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z + matrix.n43
@@ -258,73 +332,88 @@ class Point3D:
self.z = z
def copy(self):
return Point3D(x=self.x,y=self.y,z=self.z)
return Point3D(x=self.x, y=self.y, z=self.z)
class Point2D:
def __init__(self, x=0,y=0):
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return '[{0},{1}]'.format(self.x, self.y * -1)
def string(self,prefix="t"):
return '{0} {1:f} {2:f}\n'.format(prefix , self.x, self.y * -1 )
def string(self, prefix="t"):
return '{0} {1:f} {2:f}\n'.format(prefix, self.x, self.y * -1)
def copy(self):
return Point2D(x=self.x,y=self.y)
return Point2D(x=self.x, y=self.y)
class Face:
def __init__(self,a=0,b=0,c=0):
def __init__(self, a=0, b=0, c=0):
self.a = a
self.b = b
self.c = c
def string(self,prefix="f", indexOffset=0 ,textureoffset=0):
def string(self, prefix="f", indexOffset=0, textureoffset=0):
if textureoffset == 0:
return prefix + ' {0}//{0} {1}//{1} {2}//{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset)
return prefix + ' {0}//{0} {1}//{1} {2}//{2}\n'.format(
self.a + indexOffset,
self.b + indexOffset,
self.c + indexOffset
)
else:
return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset,self.a + textureoffset, self.b + textureoffset, self.c + textureoffset)
return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(
self.a + indexOffset,
self.b + indexOffset,
self.c + indexOffset,
self.a + textureoffset,
self.b + textureoffset,
self.c + textureoffset
)
def __str__(self):
return '[{0},{1},{2}]'.format(self.a, self.b, self.c)
class Group:
def __init__(self, node):
self.partRefs = node.getAttribute('partRefs').split(',')
class Bone:
def __init__(self, node):
self.refID = node.getAttribute('refID')
if node.hasAttribute('transformation'):
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
elif node.hasAttribute('angle'):
raise Exception("Cannot Properly import Old LDD Save formats")
new_matrix = mathutils.Quaternion(
(
float(node.getAttribute('ax')),
float(node.getAttribute('ay')),
float(node.getAttribute('az'))
),
math.radians(
float(node.getAttribute('angle'))
)
).to_matrix().to_4x4()
new_matrix[3].xyz = float(node.getAttribute('tx')), float(node.getAttribute('ty')), float(node.getAttribute('tz'))
self.matrix = Matrix3D(
n11=new_matrix[0][0],
n12=new_matrix[0][1],
n13=new_matrix[0][2],
n14=new_matrix[0][3],
n21=new_matrix[1][0],
n22=new_matrix[1][1],
n23=new_matrix[1][2],
n24=new_matrix[1][3],
n31=new_matrix[2][0],
n32=new_matrix[2][1],
n33=new_matrix[2][2],
n34=new_matrix[2][3],
n41=new_matrix[3][0],
n42=new_matrix[3][1],
n43=new_matrix[3][2],
n44=new_matrix[3][3],
n11=a, n12=b, n13=c, n14=0,
n21=d, n22=e, n23=f, n24=0,
n31=g, n32=h, n33=i, n34=0,
n41=x, n42=y, n43=z, n44=1
)
elif node.hasAttribute('angle'):
# raise Exception("Cannot Properly import Old LDD Save formats")
rotationMatrix = Matrix3D()
rotationMatrix.rotate(
angle=float(node.getAttribute('angle')) * math.pi / 180.0,
axis=Point3D(
x=float(node.getAttribute('ax')),
y=float(node.getAttribute('ay')),
z=float(node.getAttribute('az'))
)
)
p = Point3D(
x=float(node.getAttribute('tx')),
y=float(node.getAttribute('ty')),
z=float(node.getAttribute('tz'))
)
p.transformW(rotationMatrix)
rotationMatrix.n41 = p.x
rotationMatrix.n42 = p.y
rotationMatrix.n43 = p.z
self.matrix = rotationMatrix
else:
raise Exception(f"Bone/Part {self.refID} transformation not supported")
@@ -341,13 +430,10 @@ class Part:
for childnode in node.childNodes:
if childnode.nodeName == 'Bone':
self.Bones.append(Bone(node=childnode))
lastm = '0'
for i, m in enumerate(self.materials):
if (m == '0'):
# self.materials[i] = lastm
self.materials[i] = self.materials[0] #in case of 0 choose the 'base' material
else:
lastm = m
self.materials[i] = self.materials[0] # in case of 0 choose the 'base' material
elif node.hasAttribute('materialID'):
self.materials = [str(node.getAttribute('materialID'))]
self.Bones.append(Bone(node=node))
@@ -418,7 +504,6 @@ class Scene:
part.isGrouped = True
part.GroupIDX = i
# print(f'Scene "{self.Name}" Brickversion: {self.Version}')
class GeometryReader:
def __init__(self, data):
@@ -440,10 +525,14 @@ class GeometryReader:
options = self.readInt()
for i in range(0, self.valueCount):
self.positions.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
self.positions.append(
Point3D(x=self.readFloat(), y=self.readFloat(), z=self.readFloat())
)
for i in range(0, self.valueCount):
self.normals.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
self.normals.append(
Point3D(x=self.readFloat(), y=self.readFloat(), z=self.readFloat())
)
if (options & 3) == 3:
self.texCount = self.valueCount
@@ -451,7 +540,7 @@ class GeometryReader:
self.textures.append(Point2D(x=self.readFloat(), y=self.readFloat()))
for i in range(0, self.faceCount):
self.faces.append(Face(a=self.readInt(),b=self.readInt(),c=self.readInt()))
self.faces.append(Face(a=self.readInt(), b=self.readInt(), c=self.readInt()))
if (options & 48) == 48:
num = self.readInt()
@@ -469,7 +558,7 @@ class GeometryReader:
boneoffset = self.readInt() + 4
self.bonemap[i] = self.read_Int(datastart + boneoffset)
def read_Int(self,_offset):
def read_Int(self, _offset):
if sys.version_info < (3, 0):
return int(struct.unpack_from('i', self.data, _offset)[0])
else:
@@ -488,32 +577,37 @@ class GeometryReader:
self.offset += 4
return ret
class Geometry:
def __init__(self, designID, database, lod):
self.designID = designID
self.Parts = {}
self.maxGeoBounding = -1
if lod == None:
if lod is None:
geompath = GEOMETRIEPATH
else:
geompath = os.path.join(database.location, 'brickprimitives', 'lod' + lod + '/')
GeometryLocation = os.path.normpath('{0}{1}{2}'.format(geompath, designID,'.g'))
GeometryLocation = os.path.normpath('{0}{1}{2}'.format(geompath, designID, '.g'))
GeometryCount = 0
while str(GeometryLocation) in database.filelist:
self.Parts[GeometryCount] = GeometryReader(data=database.filelist[GeometryLocation].read())
GeometryCount += 1
GeometryLocation = os.path.normpath(f'{geompath}{designID}.g{GeometryCount}')
primitive = Primitive(data = database.filelist[os.path.normpath(PRIMITIVEPATH + designID + '.xml')].read())
primitive = Primitive(data=database.filelist[os.path.normpath(PRIMITIVEPATH + designID + '.xml')].read())
self.Partname = primitive.Designname
try:
geoBoundingList = [abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])), abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])), abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))]
geoBoundingList = [
abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])),
abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])),
abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))
]
geoBoundingList.sort()
self.maxGeoBounding = geoBoundingList[-1]
except KeyError as e:
print('\nBounding errror in part {0}: {1}\n'.format(designID, e))
print(f'\nBounding errror in part {designID}: {e}\n')
# preflex
for part in self.Parts:
@@ -546,25 +640,33 @@ class Geometry:
count += self.Parts[part].texCount
return count
class Bone2:
def __init__(self,boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
def __init__(self, boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
self.boneId = boneId
rotationMatrix = Matrix3D()
rotationMatrix.rotate(angle = -angle * math.pi / 180.0,axis = Point3D(x=ax,y=ay,z=az))
p = Point3D(x=tx,y=ty,z=tz)
rotationMatrix.rotate(
angle=(-angle * math.pi / 180.0),
axis=Point3D(x=ax, y=ay, z=az)
)
p = Point3D(x=tx, y=ty, z=tz)
p.transformW(rotationMatrix)
rotationMatrix.n41 -= p.x
rotationMatrix.n42 -= p.y
rotationMatrix.n43 -= p.z
self.matrix = rotationMatrix
class Field2D:
def __init__(self, type=0, width=0, height=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0, field2DRawData='none'):
self.type = type
self.field2DRawData = field2DRawData
rotationMatrix = Matrix3D()
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
p = Point3D(x=tx,y=ty,z=tz)
rotationMatrix.rotate(
angle=(-angle * math.pi / 180.0),
axis=Point3D(x=ax, y=ay, z=az)
)
p = Point3D(x=tx, y=ty, z=tz)
p.transformW(rotationMatrix)
rotationMatrix.n41 -= p.x
rotationMatrix.n42 -= p.y
@@ -573,7 +675,8 @@ class Field2D:
self.matrix = rotationMatrix
self.custom2DField = []
#The height and width are always double the number of studs. The contained text is a 2D array that is always height + 1 and width + 1.
# The height and width are always double the number of studs.
# The contained text is a 2D array that is always height + 1 and width + 1.
rows_count = height + 1
cols_count = width + 1
# creation looks reverse
@@ -592,18 +695,22 @@ class Field2D:
def __str__(self):
return f'[type="{self.type}" transform="{self.matrix}" custom2DField="{self.custom2DField}"]'
class CollisionBox:
def __init__(self, sX=0, sY=0, sZ=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
rotationMatrix = Matrix3D()
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
p = Point3D(x=tx,y=ty,z=tz)
rotationMatrix.rotate(
angle=(-angle * math.pi / 180.0),
axis=Point3D(x=ax, y=ay, z=az)
)
p = Point3D(x=tx, y=ty, z=tz)
p.transformW(rotationMatrix)
rotationMatrix.n41 -= p.x
rotationMatrix.n42 -= p.y
rotationMatrix.n43 -= p.z
self.matrix = rotationMatrix
self.corner = Point3D(x=sX,y=sY,z=sZ)
self.corner = Point3D(x=sX, y=sY, z=sZ)
self.positions = []
self.positions.append(Point3D(x=0, y=0, z=0))
@@ -612,8 +719,8 @@ class CollisionBox:
self.positions.append(Point3D(x=sX, y=sY, z=0))
self.positions.append(Point3D(x=0, y=0, z=sZ))
self.positions.append(Point3D(x=0, y=sY, z=sZ))
self.positions.append(Point3D(x=sX ,y=0, z=sZ))
self.positions.append(Point3D(x=sX ,y=sY, z=sZ))
self.positions.append(Point3D(x=sX, y=0, z=sZ))
self.positions.append(Point3D(x=sX, y=sY, z=sZ))
def __str__(self):
return f'[0,0,0] \
@@ -625,6 +732,7 @@ class CollisionBox:
[{self.corner.x},0,{2}] \
[{self.corner.x},{1},{2}]'
class Primitive:
def __init__(self, data):
self.Designname = ''
@@ -764,16 +872,12 @@ class MaterialRi:
self.a = a
def string(self, decorationId):
texture_strg = ''
ref_strg = ''
rgb_or_dec_str = '({0}, {1}, {2})'.format(self.r, self.g, self.b)
matId_or_decId = self.materialId
material = bpy.data.materials.new(matId_or_decId)
material.diffuse_color = (self.r, self.g, self.b, self.a)
#return bxdf_mat_str
return material
@@ -800,7 +904,7 @@ class DBFolderReader:
try:
os.path.isdir(self.location)
except Exception as e:
except Exception:
self.initok = False
print("db folder read FAIL")
return
@@ -816,10 +920,10 @@ class DBFolderReader:
return filename in self.filelist
def parse(self):
if not os.path.exists(os.path.join(self.location,"Assemblies")) and \
os.path.exists(os.path.join(self.location,"brickdb.zip")):
if not os.path.exists(os.path.join(self.location, "Assemblies")) and \
os.path.exists(os.path.join(self.location, "brickdb.zip")):
print("Found brickdb.zip without uzipped files")
with zipfile.ZipFile(os.path.join(self.location,"brickdb.zip"), 'r') as zip_ref:
with zipfile.ZipFile(os.path.join(self.location, "brickdb.zip"), 'r') as zip_ref:
print("Extracting brickdb.zip")
zip_ref.extractall(self.location)
extentions = ('.g', '.g1', '.g2', '.g3', '.g4', '.xml')
@@ -838,30 +942,21 @@ class Converter:
if self.database.initok:
self.allMaterials = Materials()
def LoadScene(self,filename):
def LoadScene(self, filename):
if self.database.initok:
self.scene = Scene(file=filename)
def Export(self, filename, lod=None, parent_collection=None):
def Export(self, filename, lod=None, parent_collection=None, useNormals=True):
invert = Matrix3D()
indexOffset = 1
textOffset = 1
usedmaterials = []
geometriecache = {}
writtenribs = []
start_time = time.time()
total = len(self.scene.Bricks)
current = 0
currentpart = 0
miny = 1000
global_matrix = axis_conversion(from_forward='-Z', from_up='Y', to_forward='Y',to_up='Z').to_4x4()
if lod != None:
global_matrix = axis_conversion(from_forward='-Z', from_up='Y', to_forward='Y', to_up='Z').to_4x4()
if lod is not None:
col = bpy.data.collections.new(self.scene.Name + '_LOD_' + lod)
else:
col = bpy.data.collections.new(self.scene.Name)
@@ -882,7 +977,7 @@ class Converter:
geometriecache[pa.designID] = geo
else:
geo = geometriecache[pa.designID]
except Exception as e:
except Exception:
print(f'WARNING: Missing geo for {pa.designID}')
continue
@@ -910,32 +1005,35 @@ 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):
# Flex parts don't need to be moved, but non-flex parts need
transform_matrix = mathutils.Matrix(((n11, n21, n31, n41),(n12, n22, n32, n42),(n13, n23, n33, n43),(n14, n24, n34, n44)))
part_matrix = global_matrix
else:
# Flex parts don't need to be moved, but non-flex parts need
transform_matrix = mathutils.Matrix(
(
(n11, n21, n31, n41),
(n12, n22, n32, n42),
(n13, n23, n33, n43),
(n14, n24, n34, n44)
)
)
# 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
# transform -------------------------------------------------------
last_color = 0
geo_meshes = []
for part in geo.Parts:
written_geo = str(geo.designID) + '_' + str(part)
@@ -951,12 +1049,12 @@ class Converter:
# positions
for j, p in enumerate(geo.Parts[part].outpositions):
if (geo.Parts[part].bonemap[j] == i):
p.transform( invert * b.matrix)
p.transform(invert * b.matrix)
# normals
for k, n in enumerate(geo.Parts[part].outnormals):
if (geo.Parts[part].bonemap[k] == i):
n.transformW( invert * b.matrix)
n.transformW(invert * b.matrix)
if "geo{0}".format(written_geo) not in geometriecache:
@@ -967,46 +1065,46 @@ class Converter:
single_vert = mathutils.Vector([point.x, point.y, point.z])
verts.append(single_vert)
usenormal = False
if usenormal == True: # write normals in case flag True
# WARNING: SOME PARTS MAY HAVE BAD NORMALS. FOR EXAMPLE MAYBE PART: (85861) PL.ROUND 1X1 W. THROUGHG. HOLE
for normal in geo.Parts[part].outnormals:
i = 0
normals = []
for point in geo.Parts[part].outnormals:
single_norm = mathutils.Vector([point.x, point.y, point.z])
normals.append(single_norm)
faces = []
for face in geo.Parts[part].faces:
single_face = [face.a , face.b, face.c]
single_face = [face.a, face.b, face.c]
faces.append(single_face)
edges = []
mesh.from_pydata(verts, edges, faces)
for f in mesh.polygons:
f.use_smooth = True
geometriecache["geo{0}".format(written_geo)] = mesh
if useNormals:
mesh.calc_normals_split()
mesh.normals_split_custom_set_from_vertices(normals)
mesh.use_auto_smooth = True
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 catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file
try:
materialCurrentPart = pa.materials[part]
last_color = pa.materials[part]
except IndexError:
# print(
# f'WARNING: {pa.designID}.g{part} has NO material assignment in lxf. \
# Replaced with color {last_color}. Fix {pa.designID}.xml faces values.'
# )
materialCurrentPart = last_color
lddmatri = self.allMaterials.getMaterialRibyId(materialCurrentPart)
matname = materialCurrentPart
if not matname in usedmaterials:
if matname not in usedmaterials:
mesh.materials.append(lddmatri.string(None))
if len(geo.Parts[part].textures) > 0:
@@ -1024,28 +1122,49 @@ 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)
# Reset index for each part
indexOffset = 1
textOffset = 1
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 == True: # write the floor plane in case True
if useplane is True: # write the floor plane in case True
i = 0
sys.stdout.write('%s\r' % (' '))
print("--- %s seconds ---" % (time.time() - start_time))
def setDBFolderVars(dbfolderlocation):
global PRIMITIVEPATH

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_AO_ONLY_MAT = "VertexColor"
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 = {
@@ -23,7 +37,7 @@ MATERIALS_OPAQUE = {
"106" : (0.799103, 0.124772, 0.009134, 1.0),
"107" : (0.002732, 0.462077, 0.462077, 1.0),
"119" : (0.296138, 0.47932, 0.003035, 1.0),
"120" : (0.672444, 0.768151, 0.262251, 1.0),
"120" : (0.672444, 0.768151, 0.262251, 1.0),
"124" : (0.332452, 0.0, 0.147027, 1.0),
"135" : (0.111932, 0.174647, 0.262251, 1.0),
"138" : (0.262251, 0.177888, 0.084376, 1.0),
@@ -101,6 +115,7 @@ MATERIALS_OPAQUE["218"] = MATERIALS_OPAQUE["124"]
MATERIALS_OPAQUE["219"] = MATERIALS_OPAQUE["268"]
MATERIALS_OPAQUE["223"] = MATERIALS_OPAQUE["222"]
MATERIALS_OPAQUE["232"] = MATERIALS_OPAQUE["212"]
MATERIALS_OPAQUE["233"] = MATERIALS_OPAQUE["37"]
MATERIALS_OPAQUE["295"] = MATERIALS_OPAQUE["222"]
MATERIALS_OPAQUE["312"] = MATERIALS_OPAQUE["138"]
MATERIALS_OPAQUE["321"] = MATERIALS_OPAQUE["102"]
@@ -111,25 +126,25 @@ 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),
"294" : (0.846873, 0.341914, 0.53948, 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
MATERIALS_TRANSPARENT["157"] = MATERIALS_TRANSPARENT["44"]
MATERIALS_TRANSPARENT["230"] = MATERIALS_TRANSPARENT["113"]
MATERIALS_TRANSPARENT["231"] = MATERIALS_TRANSPARENT["182"]
MATERIALS_TRANSPARENT["234"] = MATERIALS_TRANSPARENT["44"]
@@ -171,36 +186,24 @@ MATERIALS_GLOW["9027"] = MATERIALS_GLOW["329"]
# Metallic
MATERIALS_METALLIC = {
"131" : (0.262251, 0.296138, 0.296138, 1.0),
"139" : (0.174648, 0.066626, 0.029557, 1.0),
"148" : (0.06301, 0.051269, 0.043735, 1.0),
"149" : (0.006, 0.006, 0.006, 1.0),
"184" : (0.238095, 0.00907, 0.00907, 1.0),
"186" : (0.081104, 0.252379, 0.045668, 1.0),
"145" : (0.104617, 0.177888, 0.278894, 1.0),
"309" : (0.617207, 0.617207, 0.617207, 1.0),
"297" : (0.401978, 0.212231, 0.027321, 1.0),
"310" : (0.737911, 0.533276, 0.181164, 1.0),
("131", "150", "179", "298", "315") : (0.262251, 0.296138, 0.296138, 1.0),
("139", "187", "300") : (0.174648, 0.066626, 0.029557, 1.0),
"148" : (0.06301, 0.051269, 0.043735, 1.0),
"149" : (0.006, 0.006, 0.006, 1.0),
"184" : (0.238095, 0.00907, 0.00907, 1.0),
("186", "200") : (0.081104, 0.252379, 0.045668, 1.0),
("145", "185") : (0.104617, 0.177888, 0.278894, 1.0),
("309", "183") : (0.617207, 0.617207, 0.617207, 1.0),
("297", "147", "189") : (0.401978, 0.212231, 0.027321, 1.0),
("310", "127", ) : (0.737911, 0.533276, 0.181164, 1.0),
}
# Duplicate Metallic
MATERIALS_METALLIC["127"] = MATERIALS_METALLIC["310"]
MATERIALS_METALLIC["147"] = MATERIALS_METALLIC["297"]
MATERIALS_METALLIC["150"] = MATERIALS_METALLIC["131"]
MATERIALS_METALLIC["179"] = MATERIALS_METALLIC["131"]
MATERIALS_METALLIC["183"] = MATERIALS_METALLIC["309"]
MATERIALS_METALLIC["185"] = MATERIALS_METALLIC["145"]
MATERIALS_METALLIC["187"] = MATERIALS_METALLIC["139"]
MATERIALS_METALLIC["189"] = MATERIALS_METALLIC["297"]
MATERIALS_METALLIC["200"] = MATERIALS_METALLIC["186"]
MATERIALS_METALLIC["298"] = MATERIALS_METALLIC["131"]
MATERIALS_METALLIC["315"] = MATERIALS_METALLIC["131"]
CUSTOM_VARIATION = {
"1" : 1.3,
"21" : 1.4,
"23" : 1.25,
"24" : 1.5,
"26" : 0.4,
"28" : 0.8,
"37" : 0.8,
"135" : 0.85,
@@ -216,50 +219,82 @@ CUSTOM_VARIATION = {
"326" : 1.75,
}
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)
for key in keys:
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_ao_only_mat(parent_op=None):
if not LUTB_AO_ONLY_MAT in bpy.data.materials:
append_resources(parent_op)
return bpy.data.materials.get(LUTB_AO_ONLY_MAT)
def get_lutb_force_white_mat(parent_op=None):
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_AO_ONLY_MAT, LUTB_TRANSPARENT_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:
self.report({"WARNING"},
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

@@ -12,7 +12,7 @@ from .divide_mesh import divide_mesh
IS_TRANSPARENT = "lu_toolbox_is_transparent"
LOD_SUFFIXES = ("LOD_0", "LOD_1", "LOD_2")
LOD_SUFFIXES = ("LOD_0", "LOD_1", "LOD_2", "LOD_3")
class LUTB_OT_process_model(bpy.types.Operator):
"""Process LU model"""
@@ -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
@@ -281,16 +217,20 @@ class LUTB_OT_process_model(bpy.types.Operator):
if not (vc_col := mesh.vertex_colors.get("Col")):
vc_col = mesh.vertex_colors.new(name="Col")
materials = mesh.materials
n_materials = len(materials)
if n_materials < 2:
color = lin2srgb(materials[0].diffuse_color) if materials else (0.8, 0.8, 0.8, 1.0)
if len(mesh.materials) < 2:
if mesh.materials:
color = lin2srgb(mesh.materials[0].diffuse_color)
else:
color = (0.8, 0.8, 0.8, 1.0)
if is_transparent:
color[3] = scene.lutb_transparent_opacity / 100.0
color_data = np.tile(color, n_loops)
else:
colors = np.zeros((n_materials, 4))
for i, material in enumerate(materials):
colors = np.empty((len(mesh.materials), 4))
for i, material in enumerate(mesh.materials):
colors[i] = lin2srgb(material.diffuse_color)
if is_transparent:
@@ -316,8 +256,8 @@ class LUTB_OT_process_model(bpy.types.Operator):
mat_names = [mat.name.rsplit(".", 1)[0] for mat in mesh.materials]
if set(mat_names) & set(MATERIALS_GLOW):
colors = np.zeros((n_materials, 4))
for i, (name, material) in enumerate(zip(mat_names, materials)):
colors = np.empty((len(mesh.materials), 4))
for i, (name, material) in enumerate(zip(mat_names, mesh.materials)):
color = MATERIALS_GLOW.get(name)
colors[i] = lin2srgb(color) if color else (0.0, 0.0, 0.0, 1.0)
@@ -367,11 +307,14 @@ class LUTB_OT_process_model(bpy.types.Operator):
obj.select_set(True)
bpy.ops.lutb.remove_hidden_faces(
autoremove=scene.lutb_autoremove_hidden_faces,
tris_to_quads=scene.lutb_hidden_surfaces_tris_to_quads,
pixels_between_verts=scene.lutb_pixels_between_verts,
samples=scene.lutb_hidden_surfaces_samples,
use_ground_plane=scene.lutb_use_ground_plane,
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,
use_ground_plane=scene.lutb_hsr_use_ground_plane,
)
def split_objects(self, context, collections):
@@ -399,13 +342,10 @@ class LUTB_OT_process_model(bpy.types.Operator):
ni_nodes = {}
lods_in_use = set()
# make list of lods so that we can better decide what
for lod_collection in list(collection.children):
suffix = lod_collection.name[-5:]
if not suffix in LOD_SUFFIXES:
continue
lods_in_use.add(suffix)
used_lods = set()
for lod_collection in collection.children:
if (suffix := lod_collection.name[-5:]) in LOD_SUFFIXES:
used_lods.add(suffix)
for lod_collection in list(collection.children):
suffix = lod_collection.name[-5:]
@@ -415,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)):
@@ -440,46 +380,69 @@ class LUTB_OT_process_model(bpy.types.Operator):
lod_obj.parent = node_obj
collection.objects.link(lod_obj)
if suffix == LOD_SUFFIXES[0]:
if len(lods_in_use) == 1:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_cull
elif len(lods_in_use) == 2:
if "LOD_1" in lods_in_use:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_lod1
elif "LOD_2" in lods_in_use:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_lod2
elif len(lods_in_use) == 3:
lod_obj["near_extent"] = scene.lutb_lod0
# DYNAMIC LOD HELL
if len(used_lods) == 1:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_cull
elif suffix == LOD_SUFFIXES[0]:
lod_obj["near_extent"] = scene.lutb_lod0
if used_lods in [
{suffix, LOD_SUFFIXES[2]},
{suffix, LOD_SUFFIXES[2], LOD_SUFFIXES[3]}]:
lod_obj["far_extent"] = scene.lutb_lod2
elif used_lods == {suffix, LOD_SUFFIXES[3]}:
lod_obj["far_extent"] = scene.lutb_lod3
else:
lod_obj["far_extent"] = scene.lutb_lod1
elif suffix == LOD_SUFFIXES[1]:
if len(lods_in_use) == 1:
lod_obj["near_extent"] = scene.lutb_lod0
if used_lods == {LOD_SUFFIXES[0], suffix}:
lod_obj["near_extent"] = scene.lutb_lod1
lod_obj["far_extent"] = scene.lutb_cull
elif len(lods_in_use) == 2:
if "LOD_0" in lods_in_use:
lod_obj["near_extent"] = scene.lutb_lod1
lod_obj["far_extent"] = scene.lutb_cull
elif "LOD_2" in lods_in_use:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_lod2
elif len(lods_in_use) == 3:
elif used_lods in [
{suffix, LOD_SUFFIXES[2]},
{suffix, LOD_SUFFIXES[2], LOD_SUFFIXES[3]}]:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_lod2
elif used_lods == {LOD_SUFFIXES[0], suffix, LOD_SUFFIXES[3]}:
lod_obj["near_extent"] = scene.lutb_lod1
lod_obj["far_extent"] = scene.lutb_lod3
elif used_lods == {suffix, LOD_SUFFIXES[3]}:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_lod3
elif used_lods in [
{LOD_SUFFIXES[0], suffix, LOD_SUFFIXES[2]},
{LOD_SUFFIXES[0], suffix, LOD_SUFFIXES[2], LOD_SUFFIXES[3]}]:
lod_obj["near_extent"] = scene.lutb_lod1
lod_obj["far_extent"] = scene.lutb_lod2
elif suffix == LOD_SUFFIXES[2]:
if len(lods_in_use) == 1:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_cull
elif len(lods_in_use) > 1:
if used_lods in [
{LOD_SUFFIXES[0], suffix},
{LOD_SUFFIXES[1], suffix},
{LOD_SUFFIXES[0], LOD_SUFFIXES[1], suffix}]:
lod_obj["near_extent"] = scene.lutb_lod2
lod_obj["far_extent"] = scene.lutb_cull
elif used_lods == {suffix, LOD_SUFFIXES[3]}:
lod_obj["near_extent"] = scene.lutb_lod0
lod_obj["far_extent"] = scene.lutb_lod3
elif used_lods in [
{LOD_SUFFIXES[0], suffix, LOD_SUFFIXES[3]},
{LOD_SUFFIXES[1], suffix, LOD_SUFFIXES[3]},
{LOD_SUFFIXES[0], LOD_SUFFIXES[1], suffix, LOD_SUFFIXES[3]}]:
lod_obj["near_extent"] = scene.lutb_lod2
lod_obj["far_extent"] = scene.lutb_lod3
elif suffix == LOD_SUFFIXES[3]:
lod_obj["near_extent"] = scene.lutb_lod3
lod_obj["far_extent"] = scene.lutb_cull
node_lods[suffix] = lod_obj
obj.parent = lod_obj
class LUToolboxPanel:
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@@ -508,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"
@@ -567,14 +532,21 @@ class LUTB_PT_remove_hidden_faces(LUToolboxPanel, bpy.types.Panel):
layout.use_property_decorate = False
layout.active = scene.lutb_remove_hidden_faces
layout.prop(scene, "lutb_autoremove_hidden_faces")
layout.prop(scene, "lutb_hidden_surfaces_tris_to_quads")
layout.prop(scene, "lutb_use_ground_plane")
layout.prop(scene, "lutb_pixels_between_verts", slider=True)
layout.prop(scene, "lutb_hidden_surfaces_samples", slider=True)
layout.prop(scene, "lutb_hsr_autoremove")
class LUTB_PT_setup_lod_data(LUToolboxPanel, bpy.types.Panel):
bl_label = "Setup LOD Data"
layout.prop(scene, "lutb_hsr_vc_pre_pass")
row = layout.row()
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)
layout.prop(scene, "lutb_hsr_samples", slider=True)
class LUTB_PT_setup_metadata(LUToolboxPanel, bpy.types.Panel):
bl_label = "Setup Metadata"
bl_parent_id = "LUTB_PT_process_model"
bl_options = {"DEFAULT_CLOSED"}
@@ -590,10 +562,14 @@ class LUTB_PT_setup_lod_data(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")
layout.prop(scene, "lutb_lod3")
layout.prop(scene, "lutb_cull")
def register():
@@ -602,38 +578,68 @@ def register():
bpy.utils.register_class(LUTB_PT_apply_vertex_colors)
bpy.utils.register_class(LUTB_PT_setup_bake_mat)
bpy.utils.register_class(LUTB_PT_remove_hidden_faces)
bpy.utils.register_class(LUTB_PT_setup_lod_data)
bpy.utils.register_class(LUTB_PT_setup_metadata)
bpy.types.Scene.lutb_process_use_gpu = BoolProperty(name="Use GPU", default=True)
bpy.types.Scene.lutb_combine_objects = BoolProperty(name="Combine Objects", default=True)
bpy.types.Scene.lutb_combine_transparent = BoolProperty(name="Combine Transparent", default=False)
bpy.types.Scene.lutb_keep_uvs = BoolProperty(name="Keep UVs", default=False)
bpy.types.Scene.lutb_combine_objects = BoolProperty(name="Combine Objects", default=True, description=""\
"Combine opaque bricks")
bpy.types.Scene.lutb_combine_transparent = BoolProperty(name="Combine Transparent", default=False, description=""\
"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=True)
bpy.types.Scene.lutb_use_color_variation = BoolProperty(name="Apply Color Variation", default=True)
bpy.types.Scene.lutb_color_variation = FloatProperty(name="Color Variation", subtype="PERCENTAGE", min=0.0, soft_max=15.0, max=100.0, default=5.0)
bpy.types.Scene.lutb_correct_colors = BoolProperty(name="Correct Colors", default=False, description=""\
"Remap model colors to LU color palette. "\
"Note: Models imported with Toolbox import with the LU color palette natively")
bpy.types.Scene.lutb_use_color_variation = BoolProperty(name="Apply Color Variation", default=True, description=""\
"Randomly shift the brightness value of each brick")
bpy.types.Scene.lutb_color_variation = FloatProperty(name="Color Variation", subtype="PERCENTAGE", min=0.0, soft_max=15.0, max=100.0, default=5.0, description=""\
"Percentage of brightness value shift. Higher values result in more variation")
bpy.types.Scene.lutb_transparent_opacity = FloatProperty(name="Transparent Opacity", subtype="PERCENTAGE", min=0.0, max=100.0, default=58.82)
bpy.types.Scene.lutb_apply_vertex_colors = BoolProperty(name="Apply Vertex Colors", default=True)
bpy.types.Scene.lutb_transparent_opacity = FloatProperty(name="Transparent Opacity", subtype="PERCENTAGE", min=0.0, max=100.0, default=58.82, description=""\
"Percentage of transparent brick opacity. "\
"This controls how see-through the models transparent bricks appear in LU. "\
"Lower values result in more transparency")
bpy.types.Scene.lutb_apply_vertex_colors = BoolProperty(name="Apply Vertex Colors", default=True, description=""\
"Apply vertex colors to the model")
bpy.types.Scene.lutb_setup_bake_mat = BoolProperty(name="Setup Bake Material", default=True)
bpy.types.Scene.lutb_bake_mat = PointerProperty(name="Bake Material", type=bpy.types.Material)
bpy.types.Scene.lutb_setup_bake_mat = BoolProperty(name="Setup Bake Material", default=True, description=""\
"Apply new material to opaque bricks")
bpy.types.Scene.lutb_bake_mat = PointerProperty(name="Bake Material", type=bpy.types.Material, description=""\
"Choose the material that gets added to opaque bricks. "\
"If left blank, defaults to VertexColor material")
bpy.types.Scene.lutb_remove_hidden_faces = BoolProperty(name="Remove Hidden Faces", default=True,
description=LUTB_OT_remove_hidden_faces.__doc__)
bpy.types.Scene.lutb_autoremove_hidden_faces = BoolProperty(name="Autoremove", default=True)
bpy.types.Scene.lutb_hidden_surfaces_tris_to_quads = BoolProperty(name="Tris to Quads", default=True)
bpy.types.Scene.lutb_pixels_between_verts = IntProperty(name="Pixels Between Vertices", min=0, default=5, soft_max=15)
bpy.types.Scene.lutb_hidden_surfaces_samples = IntProperty(name="Samples", min=0, default=8, soft_max=32)
bpy.types.Scene.lutb_use_ground_plane = BoolProperty(name="Use Ground Plane", default=False,
bpy.types.Scene.lutb_hsr_autoremove = BoolProperty(name="Autoremove", default=True,
description=LUTB_OT_remove_hidden_faces.__annotations__["autoremove"].keywords["description"])
bpy.types.Scene.lutb_hsr_vc_pre_pass = BoolProperty(name="Vertex Color Pre-Pass", default=True,
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,
description=LUTB_OT_remove_hidden_faces.__annotations__["pixels_between_verts"].keywords["description"])
bpy.types.Scene.lutb_hsr_samples = IntProperty(name="Samples", min=0, default=8, soft_max=32,
description=LUTB_OT_remove_hidden_faces.__annotations__["samples"].keywords["description"])
bpy.types.Scene.lutb_hsr_use_ground_plane = BoolProperty(name="Use Ground Plane", default=False,
description=LUTB_OT_remove_hidden_faces.__annotations__["use_ground_plane"].keywords["description"])
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=120.0, soft_max=500.0)
bpy.types.Scene.lutb_lod2 = FloatProperty(name="LOD 2", soft_min=0.0, default=100.0, soft_max=280.0)
bpy.types.Scene.lutb_lod3 = FloatProperty(name="LOD 3", soft_min=0.0, default=280.0, soft_max=500.0)
bpy.types.Scene.lutb_cull = FloatProperty(name="Cull", soft_min=1000.0, default=10000.0, soft_max=50000.0)
def unregister():
@@ -641,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
@@ -653,21 +660,27 @@ def unregister():
del bpy.types.Scene.lutb_bake_mat
del bpy.types.Scene.lutb_remove_hidden_faces
del bpy.types.Scene.lutb_autoremove_hidden_faces
del bpy.types.Scene.lutb_hidden_surfaces_tris_to_quads
del bpy.types.Scene.lutb_pixels_between_verts
del bpy.types.Scene.lutb_hidden_surfaces_samples
del bpy.types.Scene.lutb_use_ground_plane
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
del bpy.types.Scene.lutb_hsr_use_ground_plane
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
del bpy.types.Scene.lutb_cull
bpy.utils.unregister_class(LUTB_PT_setup_lod_data)
bpy.utils.unregister_class(LUTB_PT_setup_metadata)
bpy.utils.unregister_class(LUTB_PT_remove_hidden_faces)
bpy.utils.unregister_class(LUTB_PT_setup_bake_mat)
bpy.utils.unregister_class(LUTB_PT_apply_vertex_colors)

View File

@@ -1,52 +1,156 @@
import bpy, bmesh
from mathutils import Vector
from mathutils import Vector, Matrix
from bpy.props import IntProperty, FloatProperty, BoolProperty
import math
import numpy as np
from timeit import default_timer as timer
LUTB_HSR_ID = "LUTB_HSR"
class LUTB_OT_remove_hidden_faces(bpy.types.Operator):
"""Remove faces hidden inside the model (using Cycles baking)"""
"""Remove hidden interior geometry from the model."""
bl_idname = "lutb.remove_hidden_faces"
bl_label = "Remove Hidden Faces"
autoremove: BoolProperty(default=True)
tris_to_quads: BoolProperty(default=True)
pixels_between_verts: IntProperty(min=0, default=5)
samples: IntProperty(min=0, default=8)
threshold: FloatProperty(min=0, default=0.01, max=1)
use_ground_plane: BoolProperty(default=False, description=""\
autoremove : BoolProperty(default=True, description=""\
"Automatically remove hidden polygons. "\
"Disabling this results in hidden polygons being assigned to the objects Face Maps"
)
vc_pre_pass : BoolProperty(default=True, description=""\
"Use vertex color baking based pre-pass to quickly sort out faces that are"\
"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. "\
"Disabling this may result in slower HSR processing")
pixels_between_verts : IntProperty(min=0, default=5, description="")
samples : IntProperty(min=1, default=8, description=""\
"Number of samples to render for HSR")
threshold : FloatProperty(min=0, default=0.01, max=1)
use_ground_plane : BoolProperty(default=False, description=""\
"Add a ground plane that contributes occlusion to the model during HSR so that "\
"the underside of the model gets removed. Before enabling this option, make "\
"sure your model does not extend below the default ground plane in LDD."
)
"sure your model does not extend below the default ground plane in LDD")
@classmethod
def poll(cls, context):
return context.object and context.object.type == "MESH" and context.mode == "OBJECT" and context.scene.render.engine == "CYCLES"
return (
context.object
and context.object.type == "MESH"
and context.mode == "OBJECT"
and context.scene.render.engine == "CYCLES"
)
def execute(self, context):
start = timer()
scene = context.scene
target_obj = context.object
mesh = target_obj.data
loop_counts = np.empty(len(mesh.polygons), dtype=int)
mesh.polygons.foreach_get("loop_total", loop_counts)
if loop_counts.max() > 4:
self.report({"ERROR"}, "Mesh needs to consist of tris or quads only!")
return {"CANCELLED"}
ground_plane = None
if self.use_ground_plane:
ground_plane = self.add_ground_plane(context)
hidden_objects = []
for obj in list(scene.collection.all_objects):
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)
if self.vc_pre_pass:
visible = self.compute_vc_pre_pass(context, scene_override)
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.object.mode_set(mode="OBJECT")
mesh.polygons.foreach_set("select", ~visible)
else:
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.object.mode_set(mode="OBJECT")
if self.tris_to_quads:
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.tris_convert_to_quads()
bpy.ops.object.mode_set(mode="OBJECT")
ground_plane = None
if self.use_ground_plane:
bpy.ops.mesh.primitive_cube_add(
size=1, location=(0, 0, -50), scale=(1000, 1000, 100))
bpy.ops.object.transform_apply()
ground_plane = context.object
select = np.empty(len(mesh.polygons), dtype=bool)
mesh.polygons.foreach_get("select", select)
face_indices = np.where(select)[0]
bpy.ops.object.select_all(action="DESELECT")
target_obj.select_set(True)
context.view_layer.objects.active = target_obj
if len(face_indices) > 0:
image = self.bake_to_image(context, scene_override, mesh, face_indices)
hidden_indices = self.get_hidden_from_image(image, mesh, face_indices)
material = bpy.data.materials.new("LUTB_GROUND_PLANE")
ground_plane.data.materials.append(material)
bpy.ops.object.mode_set(mode="EDIT")
context.tool_settings.mesh_select_mode = (False, False, True)
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.object.mode_set(mode="OBJECT")
select = np.zeros(len(mesh.polygons), dtype=bool)
select[hidden_indices] = True
mesh.polygons.foreach_set("select", select)
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:
obj.hide_render = False
if ground_plane:
bpy.data.objects.remove(ground_plane)
return {"FINISHED"}
def add_ground_plane(self, context):
bm = bmesh.new()
matrix = Matrix.Diagonal((1000, 1000, 100, 1)) @ Matrix.Translation((0, 0, -0.5))
bmesh.ops.create_cube(bm, size=1.0, matrix=matrix)
mesh = bpy.data.meshes.new(LUTB_HSR_ID)
obj = bpy.data.objects.new(LUTB_HSR_ID, mesh)
context.scene.collection.objects.link(obj)
bm.to_mesh(mesh)
if not (material := bpy.data.materials.get(LUTB_HSR_ID + "_GP")):
material = bpy.data.materials.new(LUTB_HSR_ID + "_GP")
material.use_nodes = True
nodes = material.node_tree.nodes
nodes.clear()
@@ -55,101 +159,166 @@ class LUTB_OT_remove_hidden_faces(bpy.types.Operator):
node_output = nodes.new("ShaderNodeOutputMaterial")
material.node_tree.links.new(node_diffuse.outputs[0], node_output.inputs[0])
bm = bmesh.new(use_operators=False)
bm.from_mesh(mesh)
mesh.materials.append(material)
faceCount = len(bm.faces)
return obj
for face in bm.faces:
if len(face.verts) > 4:
self.report({"ERROR"}, "Mesh needs to consist of tris or quads only!")
return {"CANCELLED"}
def setup_scene_override(self, context):
scene_override = context.scene.copy()
size = math.ceil(math.sqrt(faceCount))
scene_override.world = get_overexposed_world()
cycles = scene_override.cycles
cycles.bake_type = "DIFFUSE"
cycles.use_denoising = False
cycles.use_fast_gi = False
cycles.sample_clamp_direct = 0.0
cycles.sample_clamp_indirect = 0.0
bake_settings = scene_override.render.bake
bake_settings.use_pass_direct = True
bake_settings.use_pass_indirect = True
bake_settings.use_pass_diffuse = True
bake_settings.margin = 0
bake_settings.use_clear = True
return scene_override
def compute_vc_pre_pass(self, context, scene):
start = timer()
obj = context.object
mesh = obj.data
material = bpy.data.materials.new(LUTB_HSR_ID)
original_materials = []
for i, material_slot in enumerate(obj.material_slots):
original_materials.append(material_slot.material)
obj.material_slots[i].material = material
vc = mesh.vertex_colors.new(name=LUTB_HSR_ID)
old_active_index = mesh.vertex_colors.active_index
mesh.vertex_colors.active_index = mesh.vertex_colors.keys().index(vc.name)
cycles = scene.cycles
cycles.samples = self.vc_pre_pass_samples
cycles.max_bounces = 12
cycles.diffuse_bounces = 12
scene.render.bake.target = "VERTEX_COLORS"
context_override = context.copy()
context_override["scene"] = scene
bpy.ops.object.bake(context_override)
for i, material in enumerate(original_materials):
obj.material_slots[i].material = material
vc_data = np.empty(len(mesh.loops) * 4)
vc.data.foreach_get("color", vc_data)
mesh.vertex_colors.remove(vc)
mesh.vertex_colors.active_index = old_active_index
loop_values = (vc_data.reshape(len(mesh.loops), 4)[:,:3].sum(1) / 3) > self.threshold
loop_starts = np.empty(len(mesh.polygons), dtype=int)
mesh.polygons.foreach_get("loop_start", loop_starts)
loop_totals = np.empty(len(mesh.polygons), dtype=int)
mesh.polygons.foreach_get("loop_total", loop_totals)
face_loop_values = np.zeros((len(mesh.polygons), 4), dtype=bool)
for i, (loop_start, loop_total) in enumerate(zip(loop_starts, loop_totals)):
face_loop_values[i,:loop_total] = loop_values[loop_start:loop_start + loop_total]
visible = face_loop_values.max(axis=1)
end = timer()
n = visible.sum()
total = len(mesh.polygons)
print(
f"hsr info: vc pre-pass sorted out {n}/{total} faces ({n / total:.2%}) "\
f"in {end - start:.2f}s"
)
return visible
def setup_uv_layer(self, context, mesh, face_indices, size, size_pixels):
uv_layer = mesh.uv_layers.new(name=LUTB_HSR_ID)
uv_layer.active = True
pbv_p_1 = self.pixels_between_verts + 1
offsets = np.array((
np.array((0, 0)) + np.array((-0.01, 0.00)) * pbv_p_1,
np.array((1, 0)) + np.array(( 1.00, 0.00)) * pbv_p_1,
np.array((1, 1)) + np.array(( 1.00, 1.01)) * pbv_p_1,
np.array((0, 1)) + np.array((-0.01, 1.01)) * pbv_p_1,
)) / size_pixels
size_inv = 1 / size
uv_data = np.zeros((len(mesh.loops), 2))
loop_starts = np.empty(len(mesh.polygons), dtype=int)
mesh.polygons.foreach_get("loop_start", loop_starts)
loop_starts = loop_starts[face_indices]
loop_totals = np.empty(len(mesh.polygons), dtype=int)
mesh.polygons.foreach_get("loop_total", loop_totals)
loop_totals = loop_totals[face_indices]
for i, (loop_start, loop_total) in enumerate(zip(loop_starts, loop_totals)):
target = np.array((i % size, i // size)) * size_inv
uv_data[loop_start:loop_start+loop_total] = target + offsets[:loop_total]
uv_layer.data.foreach_set("uv", uv_data.flatten())
return uv_layer
def bake_to_image(self, context, scene, mesh, face_indices):
obj = context.object
mesh = context.object.data
size = math.ceil(math.sqrt(len(face_indices)))
quadrant_size = 2 + self.pixels_between_verts
size_pixels = size * quadrant_size
imageName = "LUTB_OVEREXPOSED_TARGET"
image = bpy.data.images.get(imageName)
if image and (image.size[0] != size_pixels or image.size[1] != size_pixels):
uv_layer = self.setup_uv_layer(context, mesh, face_indices, size, size_pixels)
image = bpy.data.images.get(LUTB_HSR_ID)
if image and tuple(image.size) != (size_pixels, size_pixels):
bpy.data.images.remove(image)
image = None
if not image:
image = bpy.data.images.new(imageName, size_pixels, size_pixels, alpha=False, float_buffer=False)
image = bpy.data.images.new(LUTB_HSR_ID, size_pixels, size_pixels)
uvlayer = bm.loops.layers.uv.new("LUTB_HSR")
material = get_overexposed_material(image)
original_materials = []
for i, material_slot in enumerate(obj.material_slots):
original_materials.append(material_slot.material)
obj.material_slots[i].material = material
pixelSize = 1 / size_pixels
pbv_p_1 = self.pixels_between_verts + 1
offsets = (
pixelSize * Vector((0 - 0.01 * pbv_p_1, 0 + 0.00 * pbv_p_1)),
pixelSize * Vector((1 + 1.00 * pbv_p_1, 0 + 0.00 * pbv_p_1)),
pixelSize * Vector((1 + 1.00 * pbv_p_1, 1 + 1.01 * pbv_p_1)),
pixelSize * Vector((0 - 0.01 * pbv_p_1, 1 + 1.01 * pbv_p_1)),
)
bm.faces.ensure_lookup_table()
for i, face in enumerate(bm.faces):
target = Vector((i % size, i // size)) * quadrant_size / size_pixels
for j, loop in enumerate(face.loops):
loop[uvlayer].uv = target + offsets[j]
bm.to_mesh(mesh)
mesh.uv_layers["LUTB_HSR"].active = True
# baking
hidden_objects = []
for obj in list(scene.collection.all_objects):
if obj != target_obj and obj != ground_plane and not obj.hide_render:
obj.hide_render = True
hidden_objects.append(obj)
originalMaterials = []
for i, material_slot in enumerate(target_obj.material_slots):
originalMaterials.append(material_slot.material)
target_obj.material_slots[i].material = getOverexposedMaterial(image)
originalWorld = scene.world
scene.world = getOverexposedWorld()
originalSamples = scene.cycles.samples
scene.cycles.samples = self.samples
originalTarget = scene.render.bake.target
cycles = scene.cycles
cycles.samples = self.samples
cycles.max_bounces = 8
cycles.diffuse_bounces = 8
scene.render.bake.target = "IMAGE_TEXTURES"
passes = ("use_pass_direct", "use_pass_indirect", "use_pass_diffuse")
originalPasses = []
for p in passes:
originalPasses.append(getattr(scene.render.bake, p))
setattr(scene.render.bake, p, True)
context_override = context.copy()
context_override["scene"] = scene
bpy.ops.object.bake(context_override)
context.view_layer.update()
bpy.ops.object.bake(type="DIFFUSE", margin=0, use_clear=True)
for i, material in enumerate(original_materials):
obj.material_slots[i].material = material
for obj in hidden_objects:
obj.hide_render = False
mesh.uv_layers.remove(uv_layer)
for i, material in enumerate(originalMaterials):
target_obj.material_slots[i].material = material
return image
scene.world = originalWorld
scene.cycles.samples = originalSamples
scene.render.bake.target = originalTarget
for p, originalValue in zip(passes, originalPasses):
setattr(scene.render.bake, p, originalValue)
bm.clear()
bm.from_mesh(mesh)
pixels = np.array(image.pixels)
def get_hidden_from_image(self, image, mesh, face_indices):
face_count = len(face_indices)
size = math.ceil(math.sqrt(face_count))
quadrant_size = 2 + self.pixels_between_verts
size_pixels = size * quadrant_size
size_sq = size ** 2
size_pixels_sq = size_pixels ** 2
pixels = np.empty(size_pixels_sq * 4, dtype=np.float32)
image.pixels.foreach_get(pixels)
sum_per_face = pixels.copy()
sum_per_face = np.reshape(sum_per_face, (size_pixels_sq, 4))
sum_per_face = np.delete(sum_per_face, 3, 1)
@@ -161,55 +330,29 @@ class LUTB_OT_remove_hidden_faces(bpy.types.Operator):
sum_per_face = np.sum(sum_per_face, axis=1)
sum_per_face = np.reshape(sum_per_face, (size, size))
sum_per_face = np.swapaxes(sum_per_face, 0, 1)
sum_per_face = np.reshape(sum_per_face, (1, size_sq))[0][:len(bm.faces)]
sum_per_face = np.reshape(sum_per_face, (1, size_sq))[0][:face_count]
pixels_per_quad = quadrant_size ** 2
pixels_per_tri = (pixels_per_quad + quadrant_size) / 2
loop_totals = np.empty(len(mesh.polygons), dtype=int)
mesh.polygons.foreach_get("loop_total", loop_totals)
loops_per_face = loop_totals[face_indices]
pixels_per_face = np.array((pixels_per_tri, pixels_per_quad))[loops_per_face - 3]
average_per_face = np.zeros(sum_per_face.shape)
for i, face in enumerate(bm.faces):
if len(face.verts) == 4:
average_per_face[i] = sum_per_face[i] / pixels_per_quad
else:
average_per_face[i] = sum_per_face[i] / pixels_per_tri
average_per_face = sum_per_face / pixels_per_face / 3
average_per_face = average_per_face / 3
indices = face_indices[np.where(average_per_face < self.threshold)[0]]
if self.autoremove:
for face, value in reversed(list(zip(bm.faces, average_per_face))):
if value < self.threshold:
bm.faces.remove(face)
return indices
bm.loops.layers.uv.remove(bm.loops.layers.uv["LUTB_HSR"])
bm.to_mesh(mesh)
bm.free()
def get_overexposed_material(image):
material = bpy.data.materials.get(LUTB_HSR_ID)
if material and (not material.use_nodes or not "LUTB_TARGET" in material.node_tree.nodes):
bpy.data.materials.remove(material)
material = None
if self.autoremove:
bpy.ops.object.mode_set(mode="EDIT")
context.tool_settings.mesh_select_mode = (True, False, False)
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.delete_loose(use_verts=True, use_edges=True, use_faces=False)
bpy.ops.object.mode_set(mode="OBJECT")
else:
bpy.ops.object.mode_set(mode="EDIT")
context.tool_settings.mesh_select_mode = (False, False, True)
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.object.mode_set(mode="OBJECT")
for polygon, value in zip(mesh.polygons, average_per_face):
polygon.select = value < self.threshold
if ground_plane:
bpy.data.objects.remove(ground_plane)
return {"FINISHED"}
def getOverexposedMaterial(image):
name = "LUTB_overexposed"
material = bpy.data.materials.get(name)
if not material:
material = bpy.data.materials.new(name)
material = bpy.data.materials.new(LUTB_HSR_ID)
material.use_nodes = True
nodes = material.node_tree.nodes
@@ -222,12 +365,10 @@ def getOverexposedMaterial(image):
return material
def getOverexposedWorld():
name = "LUTB_overexposed"
world = bpy.data.worlds.get(name)
def get_overexposed_world():
world = bpy.data.worlds.get(LUTB_HSR_ID)
if not world:
world = bpy.data.worlds.new(name)
world = bpy.data.worlds.new(LUTB_HSR_ID)
world.use_nodes = True
nodes = world.node_tree.nodes