Files
AnyImage/src/anyimage/operators/cutout_tool/object.py
nan ab282e4fdf feat: Implement depth cutout symmetry and associated functionalities
- Introduced a new proposal for depth cutout symmetry, allowing for the reuse of depth replacement and geometry operations.
- Added requirements for maintaining default thickness values for cutout nodes and ensuring proper initialization during object creation.
- Enhanced symmetry node to handle existing geometry, including alignment, mirroring, and boundary filling.
- Implemented fill smoothing to control the geometry of generated walls while preserving original surfaces.
- Created tests to validate the functionality of the new symmetry features against baseline geometry.
- Updated existing assets and node groups to support the new symmetry capabilities without introducing additional dependencies.
2026-09-14 14:58:48 +08:00

258 lines
8.2 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 create_image_material
from ...common.object import (
finalize_object_result,
modifier_input_identifier,
set_modifier_input,
)
from ...common.node import load_node_group
from .depth_calibration import (
DEFAULT_DEPTH_BASELINE,
DEFAULT_DEPTH_DIRECTION,
depth_symmetry_projection_parameters,
metric_depth_reference,
use_adaptive_depth_calibration,
)
from .geometry import (
CUTOUT_ALPHA_THRESHOLD,
crop_plane_bounds,
cutout_cleanup_value,
selection_calibration,
world_plane_metric,
)
from .shape import (
BALLOON_ATTRIBUTE_NAME,
CUTOUT_NODE_GROUP_NAMES,
DEPTH_CUTOUT_SHAPES,
)
class NormalMode(StrEnum):
NONE = "NONE"
TANGENT = "TANGENT"
OBJECT = "OBJECT"
def _cutout_world_matrix(source_matrix, crop_center):
from mathutils import Matrix
center_x, center_y = (float(value) for value in crop_center)
return source_matrix @ Matrix.Translation((center_x, center_y, 0.0))
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])
def create_shape_object(
context,
source_object,
shape,
edge_length,
content_values,
content_bounds,
base_shape,
*,
alpha_threshold=CUTOUT_ALPHA_THRESHOLD,
depth_image=None,
depth_metadata=None,
normal_image=None,
normal_mode=NormalMode.NONE,
color_image,
gesture="LASSO",
):
import numpy as np
normal_mode = NormalMode.OBJECT if shape in DEPTH_CUTOUT_SHAPES else NormalMode(normal_mode)
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(
edge_length,
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
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_SYMMETRY":
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_symmetry_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,
)
mesh_object["anyimage_mesh_shape"] = shape
node_group = load_cutout_node_group("DEPTH_SOLID" if shape == "DEPTH_SYMMETRY" else 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 (
("Cleanup Threshold", cleanup_value),
(
"Thickness",
0.0 if shape in {"FLAT", "DEPTH_SYMMETRY"} else 1.0,
),
):
if name == "Thickness" and shape == "DEPTH_SYMMETRY":
continue
set_modifier_input(
modifier,
modifier_input_identifier(
node_group, name,
subtype="NONE" if name == "Thickness" and shape in DEPTH_CUTOUT_SHAPES else None,
),
value,
)
if shape == "DEPTH_SOLID":
set_modifier_input(
modifier,
modifier_input_identifier(node_group, "Thickness", subtype="DISTANCE"),
0.2,
)
if gesture == "POLYLINE":
set_modifier_input(
modifier,
modifier_input_identifier(node_group, "Mode"),
1,
)
if shape == "SOLID":
set_modifier_input(
modifier,
modifier_input_identifier(node_group, "Thickness", subtype="DISTANCE"),
0.5,
)
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_SYMMETRY":
symmetry_group = load_cutout_node_group("DEPTH_SYMMETRY")
symmetry = mesh_object.modifiers.new(name=symmetry_group.name, type="NODES")
symmetry.node_group = symmetry_group
symmetry.show_group_selector = False
set_modifier_input(
symmetry,
modifier_input_identifier(symmetry_group, "Symmetry 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