From efd117dd9ba69c14e05a05d1ac3c1ecada2007e6 Mon Sep 17 00:00:00 2001 From: nan Date: Thu, 17 Sep 2026 00:39:45 +0800 Subject: [PATCH] refactor: incorporate Depth Scale into Depth Split criteria for consistent geometry handling --- .../reorganize-cutout-shapes/design.md | 2 +- .../specs/depth-cutout-volume/spec.md | 10 ++--- .../.openspec.yaml | 2 + .../design.md | 42 +++++++++++++++++++ .../proposal.md | 29 +++++++++++++ .../specs/depth-split-selection/spec.md | 23 ++++++++++ .../scale-depth-split-by-depth-scale/tasks.md | 16 +++++++ src/anyimage/assets/O_AnyImage.blend | 4 +- tests/tools/nodes/test_boundary_smoothing.py | 6 +-- tests/tools/nodes/test_cutout_boundary.py | 18 +++++++- tests/tools/nodes/test_depth_relief_planes.py | 20 +++++++++ tests/tools/nodes/test_depth_strip_cleanup.py | 1 + tests/tools/nodes/test_depth_surface_split.py | 36 ++++++++++++---- .../tools/nodes/test_image_depth_panorama.py | 15 +++++++ .../tools/nodes/test_temporary_attributes.py | 1 + tools/nodes/common/depth_surface.py | 16 +++++-- tools/nodes/groups/image_depth_cutout.py | 5 ++- tools/nodes/groups/image_depth_panorama.py | 6 +-- tools/nodes/groups/image_depth_plane.py | 5 ++- 19 files changed, 225 insertions(+), 32 deletions(-) create mode 100644 openspec/changes/scale-depth-split-by-depth-scale/.openspec.yaml create mode 100644 openspec/changes/scale-depth-split-by-depth-scale/design.md create mode 100644 openspec/changes/scale-depth-split-by-depth-scale/proposal.md create mode 100644 openspec/changes/scale-depth-split-by-depth-scale/specs/depth-split-selection/spec.md create mode 100644 openspec/changes/scale-depth-split-by-depth-scale/tasks.md diff --git a/openspec/changes/reorganize-cutout-shapes/design.md b/openspec/changes/reorganize-cutout-shapes/design.md index f3ff7b7..4a1b3c6 100644 --- a/openspec/changes/reorganize-cutout-shapes/design.md +++ b/openspec/changes/reorganize-cutout-shapes/design.md @@ -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 层统一解析 diff --git a/openspec/changes/reorganize-cutout-shapes/specs/depth-cutout-volume/spec.md b/openspec/changes/reorganize-cutout-shapes/specs/depth-cutout-volume/spec.md index 3d32380..c23d2fb 100644 --- a/openspec/changes/reorganize-cutout-shapes/specs/depth-cutout-volume/spec.md +++ b/openspec/changes/reorganize-cutout-shapes/specs/depth-cutout-volume/spec.md @@ -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 后的部分小片低于该面积阈值 diff --git a/openspec/changes/scale-depth-split-by-depth-scale/.openspec.yaml b/openspec/changes/scale-depth-split-by-depth-scale/.openspec.yaml new file mode 100644 index 0000000..f080778 --- /dev/null +++ b/openspec/changes/scale-depth-split-by-depth-scale/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-16 diff --git a/openspec/changes/scale-depth-split-by-depth-scale/design.md b/openspec/changes/scale-depth-split-by-depth-scale/design.md new file mode 100644 index 0000000..034bbcf --- /dev/null +++ b/openspec/changes/scale-depth-split-by-depth-scale/design.md @@ -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 独立于显示深度幅度」的条目冲突 → 在同一变更里修订该条目。 diff --git a/openspec/changes/scale-depth-split-by-depth-scale/proposal.md b/openspec/changes/scale-depth-split-by-depth-scale/proposal.md new file mode 100644 index 0000000..06d10d0 --- /dev/null +++ b/openspec/changes/scale-depth-split-by-depth-scale/proposal.md @@ -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 创建入口、材质与依赖。 diff --git a/openspec/changes/scale-depth-split-by-depth-scale/specs/depth-split-selection/spec.md b/openspec/changes/scale-depth-split-by-depth-scale/specs/depth-split-selection/spec.md new file mode 100644 index 0000000..e1bf4a2 --- /dev/null +++ b/openspec/changes/scale-depth-split-by-depth-scale/specs/depth-split-selection/spec.md @@ -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 与改动前一致 diff --git a/openspec/changes/scale-depth-split-by-depth-scale/tasks.md b/openspec/changes/scale-depth-split-by-depth-scale/tasks.md new file mode 100644 index 0000000..c9eb728 --- /dev/null +++ b/openspec/changes/scale-depth-split-by-depth-scale/tasks.md @@ -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 运行相关节点测试并确认通过。 diff --git a/src/anyimage/assets/O_AnyImage.blend b/src/anyimage/assets/O_AnyImage.blend index 43feeb5..86dcae0 100644 --- a/src/anyimage/assets/O_AnyImage.blend +++ b/src/anyimage/assets/O_AnyImage.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95b8dcb1e1dac1fc18c42438b6c4c8506bf3dbb3294177382ab3388b7dfea0d4 -size 8225064 +oid sha256:a37ec3dca7a21366601a66e01ca2bb94e1e8665375c2a311162dc438cebfd37e +size 8278599 diff --git a/tests/tools/nodes/test_boundary_smoothing.py b/tests/tools/nodes/test_boundary_smoothing.py index 6ba1136..ae7e685 100644 --- a/tests/tools/nodes/test_boundary_smoothing.py +++ b/tests/tools/nodes/test_boundary_smoothing.py @@ -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 - diff --git a/tests/tools/nodes/test_cutout_boundary.py b/tests/tools/nodes/test_cutout_boundary.py index 969e22f..9a371a0 100644 --- a/tests/tools/nodes/test_cutout_boundary.py +++ b/tests/tools/nodes/test_cutout_boundary.py @@ -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] diff --git a/tests/tools/nodes/test_depth_relief_planes.py b/tests/tools/nodes/test_depth_relief_planes.py index ad1246d..5f3b8a5 100644 --- a/tests/tools/nodes/test_depth_relief_planes.py +++ b/tests/tools/nodes/test_depth_relief_planes.py @@ -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) diff --git a/tests/tools/nodes/test_depth_strip_cleanup.py b/tests/tools/nodes/test_depth_strip_cleanup.py index 6e91e8f..8d69dd1 100644 --- a/tests/tools/nodes/test_depth_strip_cleanup.py +++ b/tests/tools/nodes/test_depth_strip_cleanup.py @@ -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) diff --git a/tests/tools/nodes/test_depth_surface_split.py b/tests/tools/nodes/test_depth_surface_split.py index 57c361c..3da5015 100644 --- a/tests/tools/nodes/test_depth_surface_split.py +++ b/tests/tools/nodes/test_depth_surface_split.py @@ -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"] diff --git a/tests/tools/nodes/test_image_depth_panorama.py b/tests/tools/nodes/test_image_depth_panorama.py index f1eb6fa..add082f 100644 --- a/tests/tools/nodes/test_image_depth_panorama.py +++ b/tests/tools/nodes/test_image_depth_panorama.py @@ -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 diff --git a/tests/tools/nodes/test_temporary_attributes.py b/tests/tools/nodes/test_temporary_attributes.py index 29e5156..e8f3992 100644 --- a/tests/tools/nodes/test_temporary_attributes.py +++ b/tests/tools/nodes/test_temporary_attributes.py @@ -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] diff --git a/tools/nodes/common/depth_surface.py b/tools/nodes/common/depth_surface.py index 786a809..fcca467 100644 --- a/tools/nodes/common/depth_surface.py +++ b/tools/nodes/common/depth_surface.py @@ -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"]) diff --git a/tools/nodes/groups/image_depth_cutout.py b/tools/nodes/groups/image_depth_cutout.py index 2d504eb..c845abc 100644 --- a/tools/nodes/groups/image_depth_cutout.py +++ b/tools/nodes/groups/image_depth_cutout.py @@ -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 diff --git a/tools/nodes/groups/image_depth_panorama.py b/tools/nodes/groups/image_depth_panorama.py index 5c0a747..e27e4e4 100644 --- a/tools/nodes/groups/image_depth_panorama.py +++ b/tools/nodes/groups/image_depth_panorama.py @@ -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) diff --git a/tools/nodes/groups/image_depth_plane.py b/tools/nodes/groups/image_depth_plane.py index b0c3854..acc9bb3 100644 --- a/tools/nodes/groups/image_depth_plane.py +++ b/tools/nodes/groups/image_depth_plane.py @@ -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,