- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity. - Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending. - Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised. - Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry. - Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity. - Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles. - Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
287 lines
8.9 KiB
Python
287 lines
8.9 KiB
Python
"""Create Blender data for a Cutout Shape."""
|
|
|
|
from enum import StrEnum
|
|
|
|
import bpy
|
|
|
|
from ...common.depth import camera_depth_values, depth_uniform_scale
|
|
from ...common.material import (
|
|
IMAGE_MATERIAL_NODE_GROUP_NAME,
|
|
create_image_material,
|
|
)
|
|
from ...common.object import (
|
|
finalize_object_result,
|
|
modifier_input_identifier,
|
|
set_modifier_input,
|
|
)
|
|
from ...common.node import load_node_group
|
|
from .geometry import (
|
|
CUTOUT_ALPHA_THRESHOLD,
|
|
DEFAULT_DEPTH_BASELINE,
|
|
DEFAULT_DEPTH_DIRECTION,
|
|
crop_plane_bounds,
|
|
cutout_cleanup_value,
|
|
cutout_sampling_spacing,
|
|
depth_balloon_projection_parameters,
|
|
metric_depth_reference,
|
|
selection_calibration,
|
|
use_adaptive_depth_calibration,
|
|
world_plane_metric,
|
|
)
|
|
from .shape import (
|
|
BALLOON_ATTRIBUTE_NAME,
|
|
NORMAL_SCALE_ATTRIBUTE_NAME,
|
|
CUTOUT_X_TO_Z,
|
|
CUTOUT_NODE_GROUP_NAMES,
|
|
DEPTH_CUTOUT_SHAPES,
|
|
)
|
|
|
|
|
|
class NormalMode(StrEnum):
|
|
NONE = "NONE"
|
|
TANGENT = "TANGENT"
|
|
OBJECT = "OBJECT"
|
|
|
|
|
|
class CutoutInwardAxis(StrEnum):
|
|
NEGATIVE_Z = "NEGATIVE_Z"
|
|
POSITIVE_X = "POSITIVE_X"
|
|
|
|
|
|
def _cutout_world_matrix(source_matrix, crop_center, inward_axis):
|
|
from mathutils import Matrix
|
|
|
|
center_x, center_y = (float(value) for value in crop_center)
|
|
matrix = source_matrix @ Matrix.Translation((center_x, center_y, 0.0))
|
|
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.POSITIVE_X:
|
|
matrix @= Matrix(CUTOUT_X_TO_Z)
|
|
return matrix
|
|
|
|
|
|
def orient_object_normal_image(image, inward_axis):
|
|
"""Convert native Z-axis Cutout normals to the selected object axes."""
|
|
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.NEGATIVE_Z:
|
|
return image
|
|
import numpy as np
|
|
|
|
pixels = np.empty(int(image.size[0]) * int(image.size[1]) * 4, dtype=np.float32)
|
|
image.pixels.foreach_get(pixels)
|
|
rgba = pixels.reshape((-1, 4))
|
|
normal = rgba[:, :3] * 2.0 - 1.0
|
|
rgba[:, :3] = np.column_stack(
|
|
(-normal[:, 2], -normal[:, 0], normal[:, 1])
|
|
) * 0.5 + 0.5
|
|
image.pixels.foreach_set(pixels)
|
|
image.update()
|
|
image.pack()
|
|
return image
|
|
|
|
|
|
def _assign_uv(mesh, uv):
|
|
import numpy as np
|
|
|
|
for layer in list(mesh.uv_layers):
|
|
mesh.uv_layers.remove(layer)
|
|
uv_layer = mesh.uv_layers.new(name="UVMap")
|
|
loop_vertices = np.empty(len(mesh.loops), dtype=np.int32)
|
|
mesh.loops.foreach_get("vertex_index", loop_vertices)
|
|
loop_uv = np.asarray(uv, dtype=np.float32)[loop_vertices]
|
|
uv_layer.data.foreach_set("uv", loop_uv.ravel())
|
|
|
|
|
|
def _assign_float_point_attribute(mesh, name, values):
|
|
import numpy as np
|
|
|
|
attribute = mesh.attributes.new(name=name, type="FLOAT", domain="POINT")
|
|
attribute.data.foreach_set("value", np.asarray(values, dtype=np.float32))
|
|
|
|
|
|
|
|
|
|
def load_cutout_node_group(shape):
|
|
return load_node_group(CUTOUT_NODE_GROUP_NAMES[shape], asset_revision=18 if shape == "DEPTH_SOLID" else 11)
|
|
|
|
|
|
def _connect_cutout_normal_scale(material):
|
|
nodes = material.node_tree.nodes
|
|
image_layer = next(
|
|
node
|
|
for node in nodes
|
|
if node.bl_idname == "ShaderNodeGroup"
|
|
and node.node_tree is not None
|
|
and node.node_tree.name == IMAGE_MATERIAL_NODE_GROUP_NAME
|
|
)
|
|
attribute = nodes.new("ShaderNodeAttribute")
|
|
attribute.attribute_type = "GEOMETRY"
|
|
attribute.attribute_name = NORMAL_SCALE_ATTRIBUTE_NAME
|
|
attribute.location = (-550.0, -250.0)
|
|
material.node_tree.links.new(
|
|
attribute.outputs["Fac"], image_layer.inputs["Normal Scale"]
|
|
)
|
|
|
|
|
|
def create_shape_object(
|
|
context,
|
|
source_object,
|
|
shape,
|
|
point_spacing,
|
|
content_values,
|
|
content_bounds,
|
|
base_shape,
|
|
*,
|
|
alpha_threshold=CUTOUT_ALPHA_THRESHOLD,
|
|
depth_image=None,
|
|
depth_metadata=None,
|
|
normal_image=None,
|
|
normal_mode=NormalMode.NONE,
|
|
inward_axis=CutoutInwardAxis.NEGATIVE_Z,
|
|
color_image,
|
|
):
|
|
import numpy as np
|
|
|
|
normal_mode = NormalMode(normal_mode)
|
|
inward_axis = CutoutInwardAxis(inward_axis)
|
|
bounds = crop_plane_bounds(
|
|
source_object,
|
|
content_bounds,
|
|
tuple(source_object.data.size),
|
|
)
|
|
crop_center = (
|
|
(bounds[0] + bounds[1]) * 0.5,
|
|
(bounds[2] + bounds[3]) * 0.5,
|
|
)
|
|
world_metric, _world_z_scale = world_plane_metric(source_object.matrix_world)
|
|
cleanup_value = cutout_cleanup_value(
|
|
cutout_sampling_spacing(source_object, point_spacing),
|
|
world_metric,
|
|
)
|
|
material = None
|
|
mesh = None
|
|
mesh_object = None
|
|
try:
|
|
material = create_image_material(
|
|
source_object.data,
|
|
color_image,
|
|
normal_image=normal_image,
|
|
normal_space=(
|
|
NormalMode.TANGENT.value
|
|
if normal_mode == NormalMode.NONE
|
|
else normal_mode.value
|
|
),
|
|
scene=context.scene,
|
|
)
|
|
depth_reference = None
|
|
_connect_cutout_normal_scale(material)
|
|
uniform_scale = None
|
|
depth_direction = None
|
|
if shape in DEPTH_CUTOUT_SHAPES:
|
|
if depth_image is None or depth_metadata is None:
|
|
raise ValueError("Depth Shape requires a Depth texture and metadata")
|
|
camera_depth, valid = camera_depth_values(depth_image)
|
|
calibration_valid, transparent_fraction = selection_calibration(
|
|
content_values, valid.shape, threshold=alpha_threshold
|
|
)
|
|
calibration_valid &= valid
|
|
uniform_scale = depth_uniform_scale(
|
|
source_object,
|
|
depth_metadata,
|
|
bounds,
|
|
)
|
|
if not use_adaptive_depth_calibration(transparent_fraction):
|
|
depth_reference = DEFAULT_DEPTH_BASELINE
|
|
if shape == "DEPTH_BALLOON":
|
|
depth_direction = DEFAULT_DEPTH_DIRECTION
|
|
elif shape == "DEPTH_SOLID":
|
|
depth_reference = metric_depth_reference(
|
|
camera_depth,
|
|
calibration_valid,
|
|
uniform_scale,
|
|
)
|
|
else:
|
|
depth_reference, depth_direction = depth_balloon_projection_parameters(
|
|
base_shape.vertices,
|
|
base_shape.uv,
|
|
camera_depth,
|
|
calibration_valid,
|
|
uniform_scale,
|
|
)
|
|
|
|
source_name = source_object.name
|
|
mesh = bpy.data.meshes.new(source_name)
|
|
mesh.from_pydata(
|
|
np.asarray(base_shape.vertices).tolist(),
|
|
[],
|
|
base_shape.faces,
|
|
)
|
|
for face in mesh.polygons:
|
|
face.use_smooth = True
|
|
_assign_uv(mesh, base_shape.uv)
|
|
_assign_float_point_attribute(
|
|
mesh,
|
|
BALLOON_ATTRIBUTE_NAME,
|
|
base_shape.balloon,
|
|
)
|
|
mesh.materials.append(material)
|
|
mesh.update()
|
|
|
|
mesh_object = bpy.data.objects.new(source_name, mesh)
|
|
context.collection.objects.link(mesh_object)
|
|
mesh_object.matrix_world = _cutout_world_matrix(
|
|
source_object.matrix_world,
|
|
crop_center,
|
|
inward_axis,
|
|
)
|
|
mesh_object["anyimage_mesh_shape"] = shape
|
|
node_group = load_cutout_node_group(shape)
|
|
modifier = mesh_object.modifiers.new(name=node_group.name, type="NODES")
|
|
modifier.node_group = node_group
|
|
modifier.show_group_selector = False
|
|
for name, value in (
|
|
(
|
|
"Inward Axis",
|
|
0 if inward_axis == CutoutInwardAxis.NEGATIVE_Z else 1,
|
|
),
|
|
("Cleanup Threshold", cleanup_value),
|
|
(
|
|
"Thickness",
|
|
0.0 if shape == "FLAT" else 1.0,
|
|
),
|
|
):
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(node_group, name),
|
|
value,
|
|
)
|
|
if shape in DEPTH_CUTOUT_SHAPES:
|
|
for name, value in (
|
|
("Depth Scale", 1.0),
|
|
("Uniform Scale", uniform_scale),
|
|
("Reference Depth", depth_reference),
|
|
("Depth Image", depth_image),
|
|
):
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(node_group, name),
|
|
value,
|
|
)
|
|
if shape == "DEPTH_BALLOON":
|
|
set_modifier_input(
|
|
modifier,
|
|
modifier_input_identifier(node_group, "Depth Direction"),
|
|
depth_direction,
|
|
)
|
|
return finalize_object_result(
|
|
context,
|
|
source_object,
|
|
mesh_object,
|
|
keep_source=True,
|
|
)
|
|
except Exception:
|
|
if mesh_object is not None:
|
|
bpy.data.objects.remove(mesh_object, do_unlink=True)
|
|
if mesh is not None and mesh.users == 0:
|
|
bpy.data.meshes.remove(mesh)
|
|
if material is not None and material.users == 0:
|
|
bpy.data.materials.remove(material)
|
|
raise
|