Files
lu-toolbox/lu_toolbox/remove_hidden_faces.py
2022-03-25 17:08:46 +01:00

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)