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