Refactor depth surface handling and unify depth sampling rules

- Removed the `always_vertex` parameter from `build_surface_camera` to streamline depth sampling for cutouts and ensure consistent behavior across depth plane and panorama.
- Updated depth sampling logic to ensure that all free boundary points, including the original frame of the depth plane, use neighboring face depth instead of their own texture sampling.
- Adjusted the depth mask deletion process to operate on faces rather than points, eliminating the need for additional cleanup of dangling edges.
- Ensured that the cutout rules are consistently applied across all relevant nodes, removing exceptions and simplifying the logic.
- Updated tests to verify the new behavior, ensuring that cutout points follow the depth of adjacent faces and that the original frame maintains its depth sampling.
- Refactored geometry handling in multiple depth-related nodes to improve clarity and maintainability.
This commit is contained in:
2026-09-16 16:30:56 +08:00
parent 7f69630371
commit c62ec2cd48
28 changed files with 880 additions and 173 deletions

View File

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

View File

@@ -0,0 +1,79 @@
## Context
`common/depth_surface.py` 的 `build_surface_camera` 负责把「点的相机采样」换成「面的相机采样」:保持点自身的射线方向,只沿射线把深度缩放到相邻面的采样值(`projected = original × separated.z / original.z`)。`separated` 是 FACE 域的采样,消费到 POINT 域时按该点的保留相邻面取平均。
三个调用点的接入方式不一致:
| 组 | 切口来源 | 面深覆盖范围 |
| --- | --- | --- |
| `O Image Depth Cutout` | `Depth Split` 切边、网格轮廓 | `cut_vertex` + `always_vertex=outline`(全部自由边界点) |
| `O Image Depth Plane` | `Depth Mask` 删点、`Depth Split` 切边 | 仅 `cut_vertex` |
| `O Image Depth Panorama` | `Depth Mask` 删点、`Depth Split` 切边 | 仅 `cut_vertex` |
Plane 的矩形网格另有一条始终存在的自由边界:原画幅外框(UV 边距 0)。它以 `original_boundary` 的形式被 `smooth_boundary_camera` 与 `smooth_cut_boundary` 的 `protected` 参数排除在平滑之外。Panorama 的球面是闭合流形,Depth Mask 打开时唯一的自由边界就是删除产生的切口。
`Depth Mask` 删点后,切口点的深度仍取该点自身的纹理采样。Plane 的深度纹理使用 Linear + EXTEND 采样,外框行或无效区旁的像素被 clamp 或跨断裂混合时,切口点会顶出尖刺;Panorama 的点采样已有 nearest 保护,但切口与 Depth Split 切边仍走两套深度来源。
`common/validity.py` 的删点用 POINT 域的 Delete Geometry:无效顶点连同所有相邻面一起消失,切口因此比阈值轮廓多退一圈,剩下的悬空边还要再删一次。Panorama 的无效判定链先经 `evaluate_field(..., "POINT")` 固定到点域,Plane 的判定链则直接由 UV 属性与纹理采样组成。
## Goals / Non-Goals
**Goals:** Depth Plane 与 Panorama 的 mask 切口与 Depth Split 切边遵循同一条深度规则,并与 Depth Cutout 的既有约定一致;规则内建在共享 helper 里,调用方不再枚举哪些点用面深,切口规则本身不带例外。
**Non-Goals:** Panorama 点采样的 nearest 保护、`Boundary Smooth` 与位置平滑的语义(含原画幅外框在边界带平滑中的保护)、`O Image Depth Cutout` 与 `O Image Relief Plane` 的行为。
## Decisions
### 切口边界内建,删除 `always_vertex` 参数
规则统一后三个调用点都要它,参数只剩「要不要这条规则」一个含义,而三处答案相同,因此把它移进 `build_surface_camera`:helper 内部用 `edge_boundary_field` 求值边界点,再与 `cut_vertex` 一起接入既有的 `OR → Switch`。`always_vertex` 参数删除,不留兼容分支。代价是将来若有调用方需要退出这条规则,得重新引入参数——目前看不出这样的调用方。
### 切口 = 全部自由边界,不设例外
`build_surface_camera` 内部把自由边界直接当作切口,不接受调用方给定的点集合,也不接受排除字段。Plane 的原画幅外框因此与 mask 切口同规则,Depth Mask 关闭时外框也会取相邻面采样。
曾做过一版「外框例外」:以 UV 边距为零排除外框,并把被 mask 判为无效的外框点并回切口。实际结果是外框上仍会剩下一两个自身采样有效、又紧贴 mask 边界的点按点采样,边界上出现个别不合规则的深度;判据还多出「外框 ∧ 自身采样是否有效」这层状态。相比之下,把全部自由边界当作切口既符合规则本身,也少一层例外,因此取消。
外框在边界带平滑(`smooth_boundary_camera`、`smooth_cut_boundary` 的 `protected`)中的保护保持不变,与深度规则互不影响。
### 深度替换沿用射线方向不变的比率法
切口点保持自身的射线方向,只取相邻保留面的深度。替代方案是整点改用面采样(方向与深度都换),那会把切口点拉离它所在的行列,破坏 Plane 的规则网格与 Panorama 的等距经纬投影;比率法只改深度分量,是既有 Depth Split 与 Cutout 轮廓已验证的形式。
### 平滑顺序保持不变
```
删除面(切口成为自由边界)
│
▼
build_surface_camera(切口点取相邻面深)
│
▼
切口带内深度模糊,沿射线重建(Plane)/径向重建(Panorama)
│
▼
切口带内位置平滑
```
面深决定切口点贴在哪张面上,带内模糊决定切口附近怎么过渡,两者职责不同,先锚定再过渡,与 Depth Cutout 一致。可观测后果是切口点的最终深度等于「面深在切口带内与邻点模糊一次」的结果,而不是相邻面深的精确值。要让切口严格等于面深,只能把替换挪到模糊之后或把切口点也排除出模糊,两者都会在切口与第二圈之间留出台阶,不采用。
### Depth Mask 按面删除
删除改用 FACE 域,并去掉随后的残留边清理。理由是节点链按消费域求值:Plane 的判定链没有固定到点域,切到 FACE 后 UV 属性取该面四角平均,等于在面中心采样深度,因此面中心仍有效的面保留下来,切口不再比阈值轮廓多退一圈;点删除遗留的悬空边也随面删除一并消失,不需要第二次 Delete Geometry。
Panorama 的判定链在 `evaluate_field(..., "POINT")` 处已固定到点域,面域消费时按四角平均,得到的是「任一顶点无效即删」,删除范围与改动前相同。要让它也按面心判定,需要改用 `face_u` / `face_v` 那套面中心采样(直接平均点域经度会跨 seam 折返),本轮不做。
## Risks / Trade-offs
- 推翻「Depth Mask 只删点、不改位置」的既有契约 → 重写相关断言为「切口取相邻面深」,并保留 Depth Mask 关闭时结果逐位不变的断言。
- 没有相邻保留面的点参与面深替换时 FACE→POINT 求值为 0,比率会塌到原点 → `edge_boundary_field` 只标记开口边界与残留边端点,不标记孤立点,并加断言兜住。
- 外框改按面深后,Depth Mask 关闭时外框的深度也随之变化 → 由切口规则测试覆盖;外框在边界带平滑中的保护保持不变。
- 源码与 `.blend` 资产不同步 → 修改后运行 `uv run node-group build`,源码、测试与资产一并更新。
## Migration Plan
直接替换构建函数、两个组的接入与相关测试,不保留旧参数或开关;重新构建节点资产。回滚时恢复 `build_surface_camera`、两个组与测试并重建资产。用户工程中已保存的节点实例沿用保存时的接口,不做迁移。
## Open Questions
无。

View File

@@ -0,0 +1,31 @@
## Why
`O Image Depth Plane` 与 `O Image Depth Panorama` 上由 Depth Mask 删点形成的切口,深度仍取该点自己的纹理采样;同一场景里 `O Image Depth Cutout` 的轮廓点和 Depth Split 切边早已改为取相邻面的采样。同类切口因此落在不同深度,clamp 或跨深度断裂的像素会把切口顶出尖刺。
## What Changes
- `build_surface_camera` 内建切口规则:自由边界点与 Depth Split 切边一样,深度取相邻面的采样,射线方向不变;删除仅有单一调用方使用的 `always_vertex` 参数。
- `O Image Depth Plane` 的原画幅外框与 mask 切口同规则:所有自由边界点都取相邻面采样,切口规则不带例外,外框也不再保留自身采样。
- `O Image Depth Panorama` 不传排除项:球面闭合,删除后出现的自由边界只有 mask 切口。
- Depth Mask 的删除由点改为面:面中心的采样低于阈值才删除该面,切口因此不再比阈值轮廓多退一圈,残留边也不需要单独清理。
- `build_surface_camera` 不再接受排除字段:切口规则覆盖全部自由边界点,取消上一版「外框例外」。
- 平滑顺序保持不变:切口先取面深,切口带的深度模糊与位置平滑照常在其后作用。
- 重建 `O_AnyImage.blend`,补齐切口面深与实位不变的断言。
## Capabilities
### New Capabilities
- `mask-cut-depth-sampling`: Depth Plane 与 Panorama 上由 Depth Mask 删除产生的切口如何确定深度,以及原画幅外框的例外。
### Modified Capabilities
无。`openspec/specs` 尚无可修改的已归档规格。
## Impact
- `tools/nodes/common/depth_surface.py`:切口边界内建在 `build_surface_camera`,`always_vertex` 参数移除。
- `tools/nodes/groups/image_depth_plane.py`、`tools/nodes/groups/image_depth_panorama.py`:接入切口规则,Plane 额外声明原画幅外框。
- `tests/tools/nodes/`:切口面深断言,以及既有「Depth Mask 只删点、不改位置」断言的重写。
- `src/anyimage/assets/O_AnyImage.blend`:重新构建节点资产。
- 不改动 `O Image Depth Cutout`、`O Image Relief Plane`、Cutout 创建入口、材质与依赖。

View File

@@ -0,0 +1,85 @@
## ADDED Requirements
### Requirement: 自由边界与切边取相邻面的深度采样
`O Image Depth Plane` 与 `O Image Depth Panorama` 上由 Depth Mask 删除产生的切口点,以及 `O Image Depth Plane` 原画幅外框上的点,其深度 MUST 取该点相邻保留面的采样值,而不是该点自己的纹理采样。切口点 MUST 保持自身的射线方向,只改变沿射线的深度分量。
#### Scenario: 无效侧深度尖刺不影响 Plane 切口
- **WHEN** Depth Mask 删除一片区域,且紧邻该区域的无效侧像素深度与有效侧相差一个数量级
- **THEN** 切口点的深度等于相邻有效面中心的采样深度,切口不出现跟随无效侧像素的尖刺
#### Scenario: Panorama 切口跟随有效侧半径
- **WHEN** Panorama 的 Depth Mask 删除一片区域,且被删区域的距离值与有效侧相差一个数量级
- **THEN** 切口圈各点的径向距离等于相邻有效面中心的采样距离
#### Scenario: 射线方向保持
- **WHEN** 切口点取相邻面深度
- **THEN** Plane 切口点的相机横纵坐标不变,Panorama 切口点仍落在原经纬方向上
#### Scenario: 关闭 Depth Split 时仍然生效
- **WHEN** Depth Split 为 0
- **THEN** 切口点仍取相邻面深度,Depth Split 切边不参与深度替换
#### Scenario: 外框与切口同规则
- **WHEN** `O Image Depth Plane` 用同一深度图求值,Depth Mask 打开或关闭
- **THEN** 原画幅外框上的点与外框邻接面的采样深度一致,不保留自身采样
### Requirement: 切口规则内建在共享相机采样
切口规则 MUST 由 `build_surface_camera` 自身求值自由边界并应用,调用方 MUST NOT 传入「哪些点使用面深」的集合,也 MUST NOT 传入排除字段;Depth Split 切边 MUST 与切口点走同一条深度替换路径。
#### Scenario: 三个调用点行为一致
- **WHEN** 比较 `O Image Depth Cutout`、`O Image Depth Plane` 与 `O Image Depth Panorama` 的相机采样接线
- **THEN** 三者都不传入点集合或排除字段,切口点与 Depth Split 切边由同一规则覆盖
### Requirement: Depth Mask 按面删除
Depth Mask 的删除 MUST 按面判定:面中心的采样低于 Mask Threshold 时删除该面,其余面连同其顶点一起保留。删除后 MUST NOT 再单独清理残留边。
#### Scenario: 切口不再多退一圈
- **WHEN** Depth Mask 打开,且无效区与有效区之间存在只跨过部分顶点的面
- **THEN** 该面在面中心仍有效时保留,切口落在面中心轮廓上,而不是所有相邻顶点都有效的内圈
#### Scenario: 删除后没有悬空边
- **WHEN** Depth Mask 删除整片面
- **THEN** 输出的边都至少属于一个保留面,没有只由删除残留形成的边
### Requirement: 原画幅外框的平滑保护独立于切口规则
`O Image Depth Plane` 的原画幅外框(UV 边距为 0 的点)MUST 继续作为 `smooth_boundary_camera` 与 `smooth_cut_boundary` 的保护范围,边界带平滑 MUST NOT 因此改变切口规则的覆盖范围。
#### Scenario: 外框保护不参与深度规则
- **WHEN** Boundary Smooth 大于 0 且存在切口
- **THEN** 原画幅外框保持不被边界带平滑拖入,同时其深度仍取相邻面采样
#### Scenario: 关闭 Depth Mask 时几何逐位不变
- **WHEN** Depth Mask 关闭
- **THEN** `O Image Depth Plane` 与 `O Image Depth Panorama` 的输出与改动前逐位一致
### Requirement: 切口深度先锚定后过渡
切口点的面深替换 MUST 发生在切口带深度模糊与位置平滑之前。切口点的最终几何 MUST 由「面深在切口带内模糊一次,再沿射线重建」得到,带外点的位置与深度 MUST 与改动前一致。
#### Scenario: 切口带保留平滑过渡
- **WHEN** Boundary Smooth 大于 0 且存在 mask 切口
- **THEN** 切口带各点沿各自射线过渡到相邻面深,切口带以外的点与 Boundary Smooth 为 0 时逐位一致
### Requirement: 无相邻面的边界点不参与面深替换
切口规则 MUST 在删除无面边(wire edge)之后求值自由边界。没有相邻保留面的点 MUST NOT 进入面深替换,避免深度塌陷到原点。
#### Scenario: 孤立点保持自身深度
- **WHEN** 几何中存在没有相邻面的自由点
- **THEN** 该点保持自身采样深度,输出坐标有限

View File

@@ -0,0 +1,28 @@
## 1. 共享相机采样
- [x] 1.1 在 `build_surface_camera` 内部求值 `edge_boundary_field` 得到切口边界,删除 `always_vertex` 参数,让切口点与 Depth Split 切边走同一条深度替换路径
- [x] 1.2 为 `build_surface_camera` 增加可选排除字段并施加在切口边界上:被排除的点保持自身采样
- [x] 1.3 更新 `build_surface_camera` 的 docstring,说明切口取相邻面深、射线方向不变、排除字段的语义
## 2. 三个组接入
- [x] 2.1 `O Image Depth Plane`:去掉 `always_vertex` 用法,外框与切口同规则,不再传排除字段
- [x] 2.2 `O Image Depth Panorama`:去掉 `always_vertex` 用法,不传排除字段
- [x] 2.3 `O Image Depth Cutout`:去掉局部 `outline` 变量与 `always_vertex` 传参,确认轮廓与切边仍由内建规则覆盖
- [x] 2.4 确认 `O Image Relief Plane`、Cutout 创建入口与材质接线未受影响
- [x] 2.5 `delete_invalid_points` 改为 `delete_invalid_faces`:Delete Geometry 用 FACE 域,去掉残留边的第二次删除
- [x] 2.6 `O Image Depth Plane`:取消外框例外,`build_surface_camera` 去掉排除字段,全部自由边界点取相邻面采样
## 3. 测试
- [ ] 3.1 Plane:在无效侧写入与有效侧相差一个数量级的深度,断言 mask 切口点跟随相邻面深、不出现尖刺
- [ ] 3.2 Panorama:在无效侧写入差异距离,断言切口圈径向距离跟随相邻有效面
- [ ] 3.3 断言切口点保持自身射线方向(Plane 相机横纵坐标不变、Panorama 经纬方向不变)
- [ ] 3.4 重写 `test_valid_only_deletes_invalid_points_and_wire_edges` 中「删剩点是原几何子集」的断言,改为「切口取相邻面深」,保留 Depth Mask 关闭时逐位不变的对比
- [ ] 3.5 断言 Plane 原画幅外框保持自身采样深度,以及不存在相邻保留面的自由点输出坐标有限
- [ ] 3.6 运行 `uv run pytest`(至少覆盖 `tests/tools/nodes/test_depth_relief_planes.py`、`test_boundary_depth.py`、`test_depth_surface_split.py`、`test_image_depth_panorama.py`)
## 4. 资产与收尾
- [x] 4.1 运行 `uv run node-group build`,确认节点资产构建与校验通过、Capture Attribute 为零
- [x] 4.2 检查源码与测试中不再有 `always_vertex` 及旧路径引用,确认最终差异只涉及本次范围