mirror of
https://github.com/Squareville/lu-toolbox.git
synced 2026-02-13 03:18:50 -06:00
Compare commits
74 Commits
v2.0-alpha
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd02e3240e | ||
|
|
bbdfafc2b6 | ||
| 473879e900 | |||
|
|
5c0c04e4a0 | ||
| 9a9b5fe58e | |||
|
|
f9ce932bed | ||
|
|
1b3f6672e9 | ||
|
|
7a9a8e2cbb | ||
|
|
61ba3c69dd | ||
|
|
88e6159f40 | ||
| 6ac59d4be3 | |||
| 569761f972 | |||
|
|
223e607628 | ||
|
|
8138542fe0 | ||
| 5fb6338ca9 | |||
|
|
24d5708d7a | ||
|
|
22df835784 | ||
| db5b172f95 | |||
| ba9beb1212 | |||
| 081c6aac0f | |||
|
|
713620a30e | ||
|
|
fe1ea064a8 | ||
|
|
0cc478b5e4 | ||
|
|
52a40f3d9f | ||
|
|
67aa93348a | ||
|
|
d8ad70de8c | ||
|
|
5d41b3e199 | ||
|
|
79d4bf454b | ||
|
|
bbabeac716 | ||
|
|
30e3c78bde | ||
|
|
a961554aeb | ||
|
|
66b5f24d9d | ||
|
|
34ebba2ff5 | ||
|
|
4009b815c7 | ||
|
|
33018f7d12 | ||
| 8860921d66 | |||
| d6c137dba0 | |||
| bc869eb9a7 | |||
| eafde9ac0b | |||
| 30fb77888e | |||
| 479891617b | |||
| 26af3e241b | |||
| 996c0210e9 | |||
| 3b8f27ad08 | |||
| bd5f00bf4b | |||
|
|
6976646685 | ||
|
|
98f34e53c6 | ||
|
|
19c058f038 | ||
| 8f761df032 | |||
| 5c359a6af7 | |||
| 2b5b809f21 | |||
| 08116d705e | |||
| 250341bffa | |||
|
|
5be7335ac8 | ||
| d5f60af5e6 | |||
| e5f9e19dfc | |||
| de78dcf350 | |||
| bee8515d6e | |||
|
|
4a79913ab4 | ||
|
|
6cef63975a | ||
|
|
42f5cc87f8 | ||
|
|
3ad5ba95ed | ||
|
|
3a5c46f44a | ||
| d6384d77d0 | |||
| 5cdab7f109 | |||
| c163e9e21f | |||
|
|
5228e89f0e | ||
|
|
010d483bee | ||
|
|
9421dfbbfd | ||
|
|
1f0abd97a3 | ||
|
|
14a6d6348b | ||
|
|
d388a495c6 | ||
| 1782ece2c8 | |||
| 9009add828 |
55
README.md
55
README.md
@@ -1,35 +1,64 @@
|
||||
[](https://download.blender.org/release/Blender2.93/)
|
||||
[](https://download.blender.org/release/Blender3.1/)
|
||||
[](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
|
||||
|
||||

|
||||
* 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>
|
||||
|
||||

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