Add cutout shell and volume node assets for depth volume processing

- Implemented `cutout_shell.py` to build front normals and shell positions for balloon geometry.
- Created `cutout_volume.py` to establish smooth center fields and manage balloon surface attributes.
- Introduced `pisa_sr_export.py` for exporting PiSA-SR components with dependencies and asset finalization.
This commit is contained in:
2026-09-08 17:18:03 +08:00
parent 7dec3b7ca4
commit 196b964efc
71 changed files with 3440 additions and 1988 deletions

View File

@@ -563,6 +563,11 @@ def triangulate(contours, spine, path_edges, free, spacing):
edges.extend(((boundary_count + a, boundary_count + b) for a, b in path_edges))
result = delaunay_2d_cdt([Vector(v) for v in p], edges, [], 0, 1e-07, True)
points = np.asarray([tuple(v) for v in result[0]])
# Restore input precision before testing area and containment. Float32 CDT
# coordinates can turn collinear outline samples into artificial slivers.
for vertex, originals in enumerate(result[3]):
if originals:
points[vertex] = p[originals].mean(axis=0)
faces = np.asarray(result[2], dtype=np.int32)
faces = faces[g.points_inside_contours(points[faces].mean(axis=1), contours)]
area = np.abs(

View File

@@ -1238,6 +1238,35 @@ class BaseShape:
balloon: object
def refine_balloon_triangles(points, faces, boundary):
"""Add interior samples to boundary ears and interior boundary chords."""
from collections import Counter
import numpy as np
edges = Counter(tuple(sorted((face[i], face[(i + 1) % 3]))) for face in faces for i in range(3))
values, midpoints = list(points), {}
for edge, count in edges.items():
if count == 2 and all(boundary[index] for index in edge):
midpoints[edge] = len(values)
values.append(points[list(edge)].mean(axis=0))
refined = []
for face in faces:
ring = []
for index, vertex in enumerate(face):
ring.append(vertex)
midpoint = midpoints.get(tuple(sorted((vertex, face[(index + 1) % 3]))))
if midpoint is not None:
ring.append(midpoint)
if len(ring) == 3 and not all(boundary[vertex] for vertex in face):
refined.append(tuple(face))
continue
center = len(values)
values.append(points[list(face)].mean(axis=0))
refined.extend((center, ring[i], ring[(i + 1) % len(ring)]) for i in range(len(ring)))
points = np.asarray(values)
return points, refined, boundary_vertices_from_faces(len(points), refined)
def cutout_domain(
selection_contours,
bounds,
@@ -1276,6 +1305,17 @@ def cutout_domain(
(start, end, faces.tolist(), pixel_to_planar(contour))
for start, end, faces, contour in pixel_components
]
refined_points, refined_faces, refined_components = [], [], []
offset = 0
for start, end, component_faces, contour in components:
points, faces, _ = refine_balloon_triangles(planar[start:end], component_faces, boundary[start:end])
refined_points.append(points)
refined_faces.extend(tuple(offset + index for index in face) for face in faces)
refined_components.append((offset, offset + len(points), faces, contour))
offset += len(points)
planar = np.concatenate(refined_points)
planar_faces, components = refined_faces, refined_components
boundary = boundary_vertices_from_faces(len(planar), planar_faces)
profile = balloon_profile(
balloon_algorithm, planar, planar_faces, boundary, components
)

View File

@@ -4,10 +4,10 @@ from ...properties import ai_ready
from .shape import CUTOUT_SHAPE_LABELS
CUTOUT_SHAPE_BUTTONS = (
("SURFACE", "MESH_GRID"),
("BALLOON", "MESH_UVSPHERE"),
("FLAT", "MESH_GRID"),
("SOLID", "MESH_UVSPHERE"),
("DEPTH_BALLOON", "META_BALL"),
("DEPTH_SURFACE", "SURFACE_NCURVE"),
("DEPTH_SOLID", "SURFACE_NCURVE"),
)
CUTOUT_MESH_DETAIL_SPACING = {

View File

@@ -73,15 +73,17 @@ _HIDDEN = {"HIDDEN", "SKIP_SAVE"}
def normal_mode_for_shape(shape, generate_normal):
if shape == "DEPTH_SOLID":
return NormalMode.OBJECT
if shape == "DEPTH_BALLOON":
return NormalMode.TANGENT
if not generate_normal:
return NormalMode.NONE
if shape == "DEPTH_SURFACE":
return NormalMode.OBJECT
return NormalMode.TANGENT
def depth_mode_for_shape(shape):
if shape == "DEPTH_SURFACE":
if shape == "DEPTH_SOLID":
return "VECTOR"
if shape == "DEPTH_BALLOON":
return "Z"
@@ -200,7 +202,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
selection_path_json: bpy.props.StringProperty(options=_HIDDEN)
selection_bounds_json: bpy.props.StringProperty(options=_HIDDEN)
shape: bpy.props.StringProperty(options=_HIDDEN)
shape: bpy.props.StringProperty(default="SOLID", options=_HIDDEN)
point_spacing: bpy.props.FloatProperty(
default=32.0,
min=0.01,

View File

@@ -31,10 +31,9 @@ from .geometry import (
)
from .shape import (
BALLOON_ATTRIBUTE_NAME,
BALLOON_CUTOUT_SHAPES,
NORMAL_SCALE_ATTRIBUTE_NAME,
CUTOUT_X_TO_Z,
CUTOUT_NODE_GROUP_NAME,
CUTOUT_SHAPE_INDICES,
CUTOUT_NODE_GROUP_NAMES,
DEPTH_CUTOUT_SHAPES,
)
@@ -131,11 +130,11 @@ def create_cropped_color_image(source_object, crop_bounds, source_rgba):
return image
def load_cutout_node_group():
return load_node_group(CUTOUT_NODE_GROUP_NAME)
def load_cutout_node_group(shape):
return load_node_group(CUTOUT_NODE_GROUP_NAMES[shape], asset_revision=11)
def _connect_balloon_normal_scale(material):
def _connect_cutout_normal_scale(material):
nodes = material.node_tree.nodes
image_layer = next(
node
@@ -146,7 +145,7 @@ def _connect_balloon_normal_scale(material):
)
attribute = nodes.new("ShaderNodeAttribute")
attribute.attribute_type = "GEOMETRY"
attribute.attribute_name = BALLOON_ATTRIBUTE_NAME
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"]
@@ -206,8 +205,7 @@ def create_shape_object(
scene=context.scene,
)
depth_reference = None
if shape in BALLOON_CUTOUT_SHAPES:
_connect_balloon_normal_scale(material)
_connect_cutout_normal_scale(material)
uniform_scale = None
depth_direction = None
if shape in DEPTH_CUTOUT_SHAPES:
@@ -227,7 +225,7 @@ def create_shape_object(
depth_reference = DEFAULT_DEPTH_BASELINE
if shape == "DEPTH_BALLOON":
depth_direction = DEFAULT_DEPTH_DIRECTION
elif shape == "DEPTH_SURFACE":
elif shape == "DEPTH_SOLID":
depth_reference = metric_depth_reference(
camera_depth,
calibration_valid,
@@ -268,22 +266,19 @@ def create_shape_object(
inward_axis,
)
mesh_object["anyimage_mesh_shape"] = shape
node_group = load_cutout_node_group()
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 (
("Shape", CUTOUT_SHAPE_INDICES[shape]),
(
"Inward Axis",
0 if inward_axis == CutoutInwardAxis.NEGATIVE_Z else 1,
),
("Cleanup", cleanup_value),
("Smooth Iterations", 0),
("Smooth", 1.0),
("Cleanup Threshold", cleanup_value),
(
"Thickness",
1.0 if shape in {"BALLOON", "DEPTH_BALLOON"} else 0.0,
0.0 if shape == "FLAT" else 1.0,
),
):
set_modifier_input(

View File

@@ -1,7 +1,7 @@
"""Cutout Shape identifiers shared by runtime and node-asset tools."""
BALLOON_ATTRIBUTE_NAME = "o_balloon"
CUTOUT_NODE_GROUP_NAME = "O Image Cutout"
NORMAL_SCALE_ATTRIBUTE_NAME = "o_normal_scale"
CUTOUT_X_TO_Z = (
(0.0, -1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
@@ -15,20 +15,16 @@ CUTOUT_Z_TO_X = (
(0.0, 0.0, 0.0, 1.0),
)
CUTOUT_NODE_GROUP_NAMES = {
"SURFACE": "O Image Surface",
"BALLOON": "O Image Balloon",
"FLAT": "O Image Cutout",
"SOLID": "O Image Cutout",
"DEPTH_BALLOON": "O Image Depth Balloon",
"DEPTH_SURFACE": "O Image Depth Surface",
"DEPTH_SOLID": "O Image Depth Cutout",
}
CUTOUT_SHAPE_LABELS = {
"SURFACE": "Surface",
"BALLOON": "Balloon",
"FLAT": "Flat",
"SOLID": "Solid",
"DEPTH_BALLOON": "Depth Balloon",
"DEPTH_SURFACE": "Depth Surface",
}
CUTOUT_SHAPE_INDICES = {
shape: index for index, shape in enumerate(CUTOUT_NODE_GROUP_NAMES)
"DEPTH_SOLID": "Depth Solid",
}
CUTOUT_SHAPES = frozenset(CUTOUT_NODE_GROUP_NAMES)
DEPTH_CUTOUT_SHAPES = frozenset({"DEPTH_BALLOON", "DEPTH_SURFACE"})
BALLOON_CUTOUT_SHAPES = frozenset({"BALLOON", "DEPTH_BALLOON"})
DEPTH_CUTOUT_SHAPES = frozenset({"DEPTH_BALLOON", "DEPTH_SOLID"})

View File

@@ -29,7 +29,7 @@ from ..common.image import (
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
UPSCALE_FACTOR = 4
UPSCALE_FACTOR = 2
def upscale_output_size(image):
@@ -49,7 +49,7 @@ def upscale_input_size_allowed(image, max_input_size):
class UpscaleImage(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.upscale_image"
bl_label = "Upscale"
bl_description = "Upscale this Image Empty by 4x with an AI model"
bl_description = "Upscale this Image Empty by 2x with an AI model"
bl_options = {"UNDO"}
job_type = "upscale-image"
starting_message = "Starting Image Empty upscale"