mirror of
https://github.com/Squareville/lu-toolbox.git
synced 2026-01-03 00:49:43 -06:00
386 lines
15 KiB
Python
386 lines
15 KiB
Python
import bpy, bmesh
|
|
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 hidden interior geometry from the model."""
|
|
bl_idname = "lutb.remove_hidden_faces"
|
|
bl_label = "Remove Hidden Faces"
|
|
|
|
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")
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
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")
|
|
|
|
select = np.empty(len(mesh.polygons), dtype=bool)
|
|
mesh.polygons.foreach_get("select", select)
|
|
face_indices = np.where(select)[0]
|
|
|
|
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)
|
|
|
|
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()
|
|
node_diffuse = nodes.new("ShaderNodeBsdfDiffuse")
|
|
node_diffuse.inputs["Color"].default_value = (0, 0, 0, 1)
|
|
node_output = nodes.new("ShaderNodeOutputMaterial")
|
|
material.node_tree.links.new(node_diffuse.outputs[0], node_output.inputs[0])
|
|
|
|
mesh.materials.append(material)
|
|
|
|
return obj
|
|
|
|
def setup_scene_override(self, context):
|
|
scene_override = context.scene.copy()
|
|
|
|
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
|
|
|
|
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(LUTB_HSR_ID, size_pixels, size_pixels)
|
|
|
|
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
|
|
|
|
cycles = scene.cycles
|
|
cycles.samples = self.samples
|
|
cycles.max_bounces = 8
|
|
cycles.diffuse_bounces = 8
|
|
scene.render.bake.target = "IMAGE_TEXTURES"
|
|
|
|
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
|
|
|
|
mesh.uv_layers.remove(uv_layer)
|
|
|
|
return image
|
|
|
|
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)
|
|
sum_per_face = np.reshape(sum_per_face, (size_pixels_sq // quadrant_size, quadrant_size * 3))
|
|
sum_per_face = np.sum(sum_per_face, axis=1)
|
|
sum_per_face = np.reshape(sum_per_face, (size_pixels, size))
|
|
sum_per_face = np.swapaxes(sum_per_face, 0, 1)
|
|
sum_per_face = np.reshape(sum_per_face, (size_sq, quadrant_size))
|
|
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][: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 = sum_per_face / pixels_per_face / 3
|
|
|
|
indices = face_indices[np.where(average_per_face < self.threshold)[0]]
|
|
|
|
return indices
|
|
|
|
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 not material:
|
|
material = bpy.data.materials.new(LUTB_HSR_ID)
|
|
material.use_nodes = True
|
|
nodes = material.node_tree.nodes
|
|
|
|
node_texture = nodes.new("ShaderNodeTexImage")
|
|
node_texture.name = "LUTB_TARGET"
|
|
|
|
nodes = material.node_tree.nodes
|
|
node_texture = nodes["LUTB_TARGET"]
|
|
node_texture.image = image
|
|
|
|
return material
|
|
|
|
def get_overexposed_world():
|
|
world = bpy.data.worlds.get(LUTB_HSR_ID)
|
|
if not world:
|
|
world = bpy.data.worlds.new(LUTB_HSR_ID)
|
|
world.use_nodes = True
|
|
nodes = world.node_tree.nodes
|
|
|
|
node_background = nodes["Background"]
|
|
node_background.inputs["Color"].default_value = (1, 1, 1, 1)
|
|
node_background.inputs["Strength"].default_value = 100000
|
|
|
|
return world
|
|
|
|
def register():
|
|
bpy.utils.register_class(LUTB_OT_remove_hidden_faces)
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(LUTB_OT_remove_hidden_faces)
|