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