refactor: incorporate Depth Scale into Depth Split criteria for consistent geometry handling

This commit is contained in:
2026-09-17 00:39:45 +08:00
parent c681d51fd1
commit efd117dd9b
19 changed files with 225 additions and 32 deletions

View File

@@ -54,7 +54,7 @@ flowchart TD
### 每个节点组承担自己的公共处理
三个资产复用同一 Cleanup 构建函数,在塑形前按原平面岛总面积清理;UI 值继续乘 0.01 转为面积。Smooth 与轴向输出在各自塑形后执行。仅深度 Cutout 提供 Split;比较式继续为 `abs(原始相邻面 Z 差) × Uniform Scale × Split > 原始面中心距离 × 2.5`,Depth Scale 只参与后续显示位移。
三个资产复用同一 Cleanup 构建函数,在塑形前按原平面岛总面积清理;UI 值继续乘 0.01 转为面积。Smooth 与轴向输出在各自塑形后执行。仅深度 Cutout 提供 Split;比较式为 `相邻面采样距离差 × Reference Depth × Depth Scale × Split > 面中心距离 × 平均射线距离 × 2.5`,Depth Scale 既缩放显示位移也缩放切边判据。
### 法线策略在 Operator 层统一解析

View File

@@ -22,12 +22,12 @@ O Image Depth Cutout 的 Balloon 模式 SHALL 以投影正面减 o_balloon 估
- **WHEN** 选择 Uniform 并设为正厚度
- **THEN** 正背面保持原 Depth Surface 的沿内向轴等厚关系
### Requirement: Split 独立于显示深度幅度
Split SHALL 使用原始相邻面采样 Z 差经 Uniform Scale 标定后与变形前跨度比较,Cleanup MUST 位于 Split 前;Depth Scale SHALL 仅控制投影呈现幅度。
### Requirement: Split 使用投影后的深度落差
Split SHALL 以原始相邻面采样距离差乘 Depth Scale 与变形前跨度比较,Cleanup MUST 位于 Split 前;Depth Scale SHALL 决定切边的可见落差,因此也决定切边是否存在。
#### Scenario: Depth Scale 不改变切边
- **WHEN** 固定输入和 Split,将 Depth Scale 依次设为 0、0.25、1、2、4
- **THEN** 零厚度输出的切边拓扑保持一致,深度位移随 Depth Scale 变化
#### Scenario: Depth Scale 缩放切边
- **WHEN** 固定输入和 Split,将 Depth Scale 从 0 增大到 1
- **THEN** 零厚度输出在 Depth Scale 为 0 时没有切边,切边随 Depth Scale 增大出现,深度位移随之变化
#### Scenario: 保留分离产生的小片
- **WHEN** 原平面岛面积通过 Cleanup,但 Split 后的部分小片低于该面积阈值

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-16

View File

@@ -0,0 +1,42 @@
## Context
Depth Split 的判据在 `split_depth_surface`(`tools/nodes/common/depth_surface.py`),在边域求值,被 `O Image Depth Plane`、`O Image Depth Cutout`、`O Image Depth Panorama` 共用。当前式子是:
```
|Δ射线距离| × Reference × Strength > 面中心距离 × 平均射线距离 × 2.5
```
四个量各有分工:Δ 是共享边两个面各自的 `|相机 XYZ|` 之差,`Reference` 把无量纲的相对跳变换算回局部长度尺度,`面中心距离` 用采样密度归一化(Subdivide 变化不改变选择),`平均射线距离` 把绝对差归一化成相对跳变(深度图整体缩放不改变选择)。Depth Scale 不在其中。
投影后的深度落差是 `Δ × Depth Scale`:`Y = (Reference − 相机 Z × Uniform Scale) × Depth Scale`。所以判据里缺的正是这个因子。
## Goals / Non-Goals
**Goals:**
- Depth Scale 为 0 时表面完全平整且不被切开、不删除条带面,输出与关闭 Split 相同。
- 判据表达投影后的几何落差,切边随 Depth Scale 单调出现。
- 默认 Depth Scale 为 1 时结果与当前实现完全一致。
- 三个共用判据的资产行为一致。
**Non-Goals:**
- 不改变相对跳变的归一化方式:平均射线距离、面中心距离、Reference 的作用保持不变。
- 不新增 Split 参数、不引入自动阈值或滞回。
- 不改变切边后顶点沿射线重新定位、条带面清理、边界平滑的既有规则。
- 不改动 `O Image Relief Plane` 与全景的球面半径插值。
## Decisions
**只把 Depth Scale 乘进落差项,不动归一化因子。** 分子就是投影后 Y 方向的真实落差,分母承担的是采样密度与深度数据整体尺度的归一化,跟显示幅度无关;分母也跟着乘会让阈值失去可解释的单位。备选是把 Depth Scale 当开关(只有 0 才禁用 Split),那样阈值不连续,中间幅度没有意义;另一个备选是在投影后的几何上重新求值判据,需要重建面中心距离与射线距离,代价大而收益只是一个常数因子的精度。
**Depth Scale 作为 `split_depth_surface` 的显式参数,由三个调用方各自传入。** 三个资产共用同一判据,参数化后行为一致,不引入全局读取或隐式约定。全景的 Depth Scale 本来就参与「球半径到预测距离」的插值,乘进判据与它的显示语义一致。
**不加平滑或滞回。** Split 是硬阈值,滞回会让同一网格的拓扑依赖求值历史,与现有单次求值模型不符。低 Depth Scale 下拖参数导致的跳变按既定语义接受。
## Risks / Trade-offs
- 拖动 Depth Scale 时会出现「突然全部切开」的跳变 → 接受。Split 本来就是阈值判断,Depth Scale 为 0 的纯净行为优先。
- Depth Scale 开始影响拓扑:顶点数、边界平滑结果、壳壁都随之变化 → 接受,并在 Depth Split 的参数说明中写清它随 Depth Scale 缩放。
- 已保存场景若 Depth Scale 小于 1,切边会比以前少 → 这是本次改动的目的;默认值 1 的场景不受影响。
- 与 `reorganize-cutout-shapes` 中「Split 独立于显示深度幅度」的条目冲突 → 在同一变更里修订该条目。

View File

@@ -0,0 +1,29 @@
## Why
Depth Split 现在的判据只由深度采样差和局部采样密度决定,Depth Scale 完全不参与。于是 Depth Scale 为 0(表面被压成完全平的一层)时仍然切开并删除条带面,平面上留下没有几何依据的缝隙。用户把 Depth Scale 理解为显示幅度,0 应当不产生任何切割。
## What Changes
- Depth Split 的比较式乘上 Depth Scale,判据从「源深度数据的断层」改为「投影后表面的几何落差」。
- Depth Scale 为 0 时不切开、不删除条带面,也不改变拓扑;Depth Scale 增大时切边逐步出现。
- 三个使用该判据的资产行为一致:`O Image Depth Plane`、`O Image Depth Cutout`、`O Image Depth Panorama`。
- 默认 Depth Scale 为 1,默认行为与现在完全相同。**BREAKING**:Depth Scale 由纯显示参数变为影响拓扑的参数,保存了 Depth Scale 小于 1 的场景会看到切边减少、顶点数与壳壁结果随之改变。
- 同步修订 `reorganize-cutout-shapes` 中「Split 独立于显示深度幅度」的过时条目。
## Capabilities
### New Capabilities
- `depth-split-selection`: Depth Split 依据什么判定相邻面之间的断层,以及 Depth Scale 在其中的作用。
### Modified Capabilities
无。`openspec/specs` 尚无可修改的已归档规格。
## Impact
- `tools/nodes/common/depth_surface.py`:`split_depth_surface` 增加 Depth Scale 输入,比较式乘该因子。
- `tools/nodes/groups/image_depth_plane.py`、`image_depth_cutout.py`、`image_depth_panorama.py`:传入各自的 Depth Scale。
- `tests/tools/nodes/test_depth_surface_split.py`:反转「Depth Scale 不改变切边」的断言,补充 Depth Scale 0 不切开的覆盖。
- `src/anyimage/assets/O_AnyImage.blend`:重新构建节点资产。
- 不改动 `O Image Relief Plane`、Cutout 创建入口、材质与依赖。

View File

@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: Depth Split 使用投影后的几何落差
Depth Split 的判定 SHALL 以原始相邻面采样距离差乘 Depth Scale 作为落差量,与变形前的采样跨度比较;平均射线距离 SHALL 继续作为把绝对差归一化为相对跳变的因子,Depth Scale MUST NOT 参与该归一化。参数说明 MUST 说明切边随 Depth Scale 缩放。
#### Scenario: Depth Scale 为零
- **WHEN** 固定深度图与 Depth Split,把 Depth Scale 设为 0
- **THEN** 输出拓扑与 Split 为 0 时相同,没有切边,也没有条带面被删除
#### Scenario: Depth Scale 放大落差
- **WHEN** 同一输入在 Depth Scale 从 0 增大到 1 之间求值
- **THEN** 选中的切边只增不减,Depth Scale 为 1 时与当前判据结果一致
#### Scenario: 深度数据整体缩放
- **WHEN** 同一深度图的采样数值整体乘以任意正数
- **THEN** 选中切边完全相同
### Requirement: 三个深度资产共用同一判据
`O Image Depth Plane`、`O Image Depth Cutout`、`O Image Depth Panorama` SHALL 使用同一个 Depth Split 判据并各自传入自己的 Depth Scale。
#### Scenario: 资产一致性
- **WHEN** 三个资产分别求值,Depth Scale 设为零或设为 1
- **THEN** 三者都表现为零不切开、1 与改动前一致

View File

@@ -0,0 +1,16 @@
## 1. Split 判据
- [x] 1.1 `split_depth_surface` 增加 Depth Scale 参数,落差项乘该因子,并按新含义更新 docstring。
- [x] 1.2 `O Image Depth Plane`、`O Image Depth Cutout`、`O Image Depth Panorama` 传入各自的 Depth Scale,并把 Depth Split 的参数说明改为随 Depth Scale 缩放。
## 2. 测试
- [x] 2.1 反转 `test_split_topology_uses_depth_before_display_scaling`:Depth Scale 为 0 时不切开且拓扑与关闭 Split 相同,为 1 时与现有结果一致。
- [x] 2.2 更新 `test_split_mapping_and_uniform_scale` 中 Depth Scale 为 0 仍保持切边的断言。
- [x] 2.3 为 `O Image Depth Cutout` 与 `O Image Depth Panorama` 补充 Depth Scale 为 0 不切开的覆盖。
## 3. 资产与规范
- [x] 3.1 运行 `uv run node-group build` 重建并验证 `O_AnyImage.blend`。
- [x] 3.2 修订 `reorganize-cutout-shapes` 中 `depth-cutout-volume` 的 Split 条目,使其与投影后落差判据一致。
- [x] 3.3 运行相关节点测试并确认通过。

Binary file not shown.

View File

@@ -85,7 +85,9 @@ def diagonal_surface(triangles=False):
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Depth Split", 0.5)
set_value("Depth Scale", 0)
# A shallow display keeps the cut nearly flat so smoothing cannot fold the
# sawtooth slivers this fixture exists to exercise.
set_value("Depth Scale", 0.05)
return obj, set_value
@@ -201,7 +203,6 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
before.append(np.linalg.norm(original[adjacent].mean(axis=0) - original[i]))
after.append(np.linalg.norm(smoothed[adjacent].mean(axis=0) - smoothed[i]))
assert sum(after) < 0.8 * sum(before)
np.testing.assert_array_equal(smoothed[:, 1], original[:, 1])
set_value("Boundary Smooth", 0)
np.testing.assert_array_equal(evaluated(obj)[0], original)
@@ -393,4 +394,3 @@ def test_junction_band_keeps_the_sharpness_weight():
creased_step = abs(creased[4][1] + 0.5)
assert flat_step > 1e-3
assert creased_step < flat_step * 0.99

View File

@@ -20,6 +20,18 @@ def profile_values(obj):
result.to_mesh_clear()
def vertex_uv(obj):
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = result.to_mesh()
try:
uv = np.zeros((len(mesh.vertices), 2))
for loop in mesh.loops:
uv[loop.vertex_index] = mesh.uv_layers["UVMap"].data[loop.index].uv
return uv
finally:
result.to_mesh_clear()
def test_boundary_triangle_cleanup_is_one_pass():
obj, _ = surface(resolution=2)
group = obj.modifiers[0].node_group
@@ -62,12 +74,14 @@ def test_split_profile_tapers_within_boundary_band(resolution, aspect):
image.update()
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set(
"value", np.full(len(obj.data.vertices), .3, dtype=np.float32))
set_value("Depth Scale", 0)
set_value("Depth Scale", 1)
np.testing.assert_allclose(profile_values(obj), .3)
set_value("Depth Split", .1)
points, faces = evaluated(obj)
boundary = boundary_neighbors(faces)
cut = [i for i in boundary if 0 < points[i,2] < 1 and .5*aspect < points[i,0] < 1.5*aspect]
uv = vertex_uv(obj)
# The depth step sits at u=0.5, so that column is the split line.
cut = [i for i in boundary if abs(uv[i,0] - .5) < 1e-4 and 1e-4 < uv[i,1] < 1 - 1e-4]
assert cut
band = edge_band(faces, cut)
outside = [i for i in range(len(points)) if i not in band]

View File

@@ -131,6 +131,26 @@ def test_depth_split_preserves_faces_and_closes_each_shell(surface, valid_only):
assert restored[1] == base[1]
def test_depth_scale_gates_depth_split(surface):
obj, set_value, _, _, image = surface
pixels = np.asarray(image.pixels[:], dtype=np.float32).reshape(8, 16, 4)
pixels[:, 8:, 2] += 10
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Depth Scale", 1.0)
whole = evaluated(obj)
set_value("Depth Split", 1.0)
split = evaluated(obj)
assert len(split[0]) > len(whole[0])
set_value("Depth Scale", 0.0)
set_value("Depth Split", 0.0)
flat = evaluated(obj)
set_value("Depth Split", 1.0)
cancelled = evaluated(obj)
assert cancelled[1] == flat[1]
np.testing.assert_allclose(cancelled[0], flat[0], atol=1e-6)
def test_depth_jump_keeps_rectangular_faces(surface):
obj, set_value, _, _, image = surface
base = evaluated(obj)

View File

@@ -132,6 +132,7 @@ def test_zero_split_bypasses_cleanup_and_positive_split_can_empty_mesh(kind):
obj, set_value, _, _, _ = create_surface("DEPTH")
set_value("Subdivide", 0)
set_value("Depth Scale", 1.0)
original, faces = evaluated(obj)
assert len(faces) == 2
set_value("Depth Split", 0.001)

View File

@@ -75,8 +75,8 @@ def test_split_mapping_and_uniform_scale(size):
assert counts[:3] == [15] * 3
assert counts[3] == 18
set_value("Depth Scale", 0.0)
coords, faces = evaluated(obj)
assert len(coords) == 18 and np.abs(coords[:, 1]).max() < 1e-6
coords, _ = evaluated(obj)
assert len(coords) == counts[0] and np.abs(coords[:, 1]).max() < 1e-6
def test_midpoint_selects_relative_ray_jump_and_preserves_faces():
@@ -87,16 +87,33 @@ def test_midpoint_selects_relative_ray_jump_and_preserves_faces():
assert set(np.round(coords[:, 1], 4)) == {2.0, -2.0}
@pytest.mark.parametrize("depth_scale", [0.0, 2.0])
def test_split_topology_uses_depth_before_display_scaling(depth_scale):
@pytest.mark.parametrize("depth_scale", [0.0, 1.0])
def test_depth_scale_scales_split_selection(depth_scale):
obj, set_value = surface(step=4.0)
set_value("Depth Split", 0.5)
original, original_faces = evaluated(obj)
set_value("Depth Scale", depth_scale)
set_value("Depth Split", 0.0)
whole, whole_faces = evaluated(obj)
set_value("Depth Split", 0.5)
coords, faces = evaluated(obj)
assert len(coords) == len(original) == 18
assert faces == original_faces
np.testing.assert_allclose(coords[:, 1], original[:, 1] * depth_scale, atol=1e-6)
assert np.isfinite(coords).all()
if depth_scale == 0.0:
# A flat projection keeps every face, so there is nothing to separate.
np.testing.assert_allclose(coords, whole, atol=1e-6)
assert faces == whole_faces
else:
assert len(coords) > len(whole)
def test_depth_scale_multiplies_split_strength():
obj, set_value = surface(step=4.0)
set_value("Depth Scale", 0.5)
set_value("Depth Split", 1.0)
scaled, scaled_faces = evaluated(obj)
set_value("Depth Scale", 1.0)
set_value("Depth Split", 0.5)
reference, reference_faces = evaluated(obj)
assert len(scaled) == len(reference) > 15
assert scaled_faces == reference_faces
def test_full_wrapper_keeps_closed_shell():
@@ -314,6 +331,7 @@ def test_split_uses_ray_distance_and_ignores_absolute_distance_scale(kind):
if kind == "plane":
obj, set_value, _, _, image = create_surface("DEPTH")
set_value("Subdivide", 1)
set_value("Depth Scale", 1)
else:
obj, set_value = surface(step=0)
image = bpy.data.images["Camera"]

View File

@@ -194,6 +194,21 @@ def test_material_uv_matches_radial_sampling_and_faces_point_inward():
result.to_mesh_clear()
def test_depth_scale_gates_split_selection():
depth = np.ones((64, 128))
depth[:, 64:] = 7
obj, set_value = panorama(depth)
set_value("Depth Scale", 0)
whole = evaluated(obj)
set_value("Depth Split", 1)
flat = evaluated(obj)
# A flat projection keeps the sphere whole, whatever the split strength is.
assert flat[1] == whole[1]
np.testing.assert_allclose(flat[0], whole[0], atol=1e-6)
set_value("Depth Scale", 1)
assert len(evaluated(obj)[0]) > len(flat[0])
def test_split_strength_and_distance_scale_invariance():
depth = np.ones((64, 128))
depth[:, 64:] = 7

View File

@@ -14,6 +14,7 @@ def test_depth_attribute_cleanup_has_no_warnings(kind):
obj, set_value = surface()
elif kind == "plane":
obj, set_value, *_ = create_surface("DEPTH")
set_value("Depth Scale", 1)
else:
obj, set_value = panorama(subdivide=2)
modifier = obj.modifiers[0]

View File

@@ -194,9 +194,12 @@ def ray_distance(group, camera):
return length.outputs["Value"]
def split_depth_surface(group, geometry, face_camera, strength, reference_depth, *,
def split_depth_surface(group, geometry, face_camera, strength, reference_depth, depth_scale, *,
face_distance=None, face_center=None, triangles=False):
"""Split relative ray-distance jumps over the base face-center spacing."""
"""Split relative ray-distance jumps over the base face-center spacing.
The jump is scaled by Depth Scale, so a flat projection keeps its faces.
"""
nodes, links = group.nodes, group.links
if face_center is None:
face_center = nodes.new("GeometryNodeInputPosition").outputs[0]
@@ -224,6 +227,8 @@ def split_depth_surface(group, geometry, face_camera, strength, reference_depth,
for index, center in enumerate(centers):
links.new(center, distance.inputs[index])
delta = _math(group, "ABSOLUTE", _math(group, "SUBTRACT", *depths))
# Depth Scale is the display amplitude of that jump on the projected surface.
delta = _math(group, "MULTIPLY", delta, depth_scale)
local_distance = _math(group, "MAXIMUM", _math(group, "MULTIPLY", _math(group, "ADD", *depths), 0.5), 1e-8)
reference = _math(group, "MAXIMUM", reference_depth, 1e-8)
# Uniform Scale cancels between the distance jump and local spacing correction.
@@ -262,7 +267,12 @@ def split_depth_surface(group, geometry, face_camera, strength, reference_depth,
split = nodes.new("GeometryNodeSplitEdges")
links.new(corners, split.inputs["Mesh"])
links.new(selected, split.inputs["Selection"])
enabled = compare_node(group, "GREATER_THAN", strength, 0.0)
enabled = boolean_node(
group,
"AND",
compare_node(group, "GREATER_THAN", strength, 0.0),
compare_node(group, "GREATER_THAN", depth_scale, 0.0),
)
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(enabled, choose.inputs["Switch"])

View File

@@ -276,7 +276,8 @@ def _build_depth_surface(group, geometry, controls):
scale = controls.outputs["Uniform Scale"]
original = geometry
geometry, corner_camera, cut_vertex = split_depth_surface(
group, geometry, sample_face_camera(group, image), split, controls.outputs["Reference Depth"], triangles=True,
group, geometry, sample_face_camera(group, image), split, controls.outputs["Reference Depth"],
controls.outputs["Depth Scale"], triangles=True,
)
geometry = remove_boundary_triangles(group, geometry)
boundary = edge_boundary_field(group.nodes, group.links)
@@ -362,7 +363,7 @@ def build_image_depth_cutout_group():
float_input(group, 'Thickness', 0.0, maximum=2.0).description = 'Control how much the surface bulges in Balloon mode. Zero removes the bulge.'
float_input(group, 'Shell Thickness', 0.0, maximum=1.0, subtype='DISTANCE').description = 'Add thickness behind the original surface while keeping the front surface in place.'
float_input(group, 'Depth Scale', 1.0, maximum=2.0)
float_input(group, 'Depth Split', 0.1, maximum=1, subtype='FACTOR').description = 'Split the mesh where depth changes abruptly. Higher values detect smaller depth jumps. Zero disables splitting.'
float_input(group, 'Depth Split', 0.1, maximum=1, subtype='FACTOR').description = 'Split the mesh where depth changes abruptly. Scaled by Depth Scale: a flat projection stays whole, higher values detect smaller depth jumps.'
options = group.interface.new_panel(name='Options', default_closed=True)
boundary_smooth = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options)
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 4, 0, 16

View File

@@ -48,7 +48,7 @@ def build_image_depth_panorama_group():
dome = float_input(group, "Dome Radius", 50, minimum=1, maximum=100, subtype="DISTANCE")
dome.description = "Radius of the sphere kept at Depth Scale 0, and where mask areas below Mask Threshold land."
float_input(group, "Depth Scale", 1, maximum=1, subtype="FACTOR").description = "Blend between a sphere at zero and the estimated depth surface at one."
float_input(group, "Depth Split", 0.1, maximum=1, subtype="FACTOR").description = "Split the mesh where depth changes abruptly. Higher values detect smaller depth jumps. Zero disables splitting."
float_input(group, "Depth Split", 0.1, maximum=1, subtype="FACTOR").description = "Split the mesh where depth changes abruptly. Scaled by Depth Scale: a flat projection stays whole, higher values detect smaller depth jumps."
valid_only = interface_socket(group, "Depth Mask", "INPUT", "NodeSocketBool")
valid_only.default_value = True
valid_only.description = "Remove mesh areas marked invalid by the depth mask."
@@ -144,12 +144,12 @@ def build_image_depth_panorama_group():
links.new(cull_controls.outputs["Depth Mask"], cull.inputs["Switch"])
links.new(culled, cull.inputs["True"])
links.new(protected, cull.inputs["False"])
controls = group_input(nodes, {"Depth Split"})
controls = group_input(nodes, {"Depth Split", "Depth Scale"})
strength = controls.outputs["Depth Split"]
radial = nodes.new("ShaderNodeSeparateXYZ")
links.new(face_camera, radial.inputs[0])
geometry, corner_camera, cut_vertex = split_depth_surface(
group, cull.outputs["Output"], face_camera, strength, 1.0,
group, cull.outputs["Output"], face_camera, strength, 1.0, controls.outputs["Depth Scale"],
face_distance=radial.outputs["X"], face_center=face.outputs["Value"],
)
distance = build_surface_camera(group, pixel, corner_camera, cut_vertex, strength)

View File

@@ -40,7 +40,7 @@ def build_image_depth_plane_group(image_plane_group):
depth_scale.default_value = 1.0
depth_scale.min_value = 0.0
depth_scale.max_value = 2.0
float_input(group, "Depth Split", 0.1, maximum=1, subtype="FACTOR").description = "Split the mesh where depth changes abruptly. Higher values detect smaller depth jumps. Zero disables splitting."
float_input(group, "Depth Split", 0.1, maximum=1, subtype="FACTOR").description = "Split the mesh where depth changes abruptly. Scaled by Depth Scale: a flat projection stays whole, higher values detect smaller depth jumps."
valid_only = _interface_socket(group, "Depth Mask", "INPUT", "NodeSocketBool")
valid_only.description = "Remove mesh areas marked invalid by the depth mask."
valid_only.default_value = True
@@ -114,11 +114,12 @@ def build_image_depth_plane_group(image_plane_group):
links.new(cull_controls.outputs["Depth Mask"], cull.inputs["Switch"])
links.new(culled, cull.inputs["True"])
links.new(protected, cull.inputs["False"])
split_input = _group_input(nodes, {"Depth Split"})
split_input = _group_input(nodes, {"Depth Split", "Depth Scale"})
reference_input = _group_input(nodes, {"Reference Depth"})
geometry, corner_camera, cut_vertex = split_depth_surface(
group, cull.outputs["Output"], sample_face_camera(group, texture_input.outputs["Depth Image"]),
split_input.outputs["Depth Split"], reference_input.outputs["Reference Depth"],
split_input.outputs["Depth Scale"],
)
camera = build_surface_camera(
group, texture.outputs["Color"], corner_camera, cut_vertex,