feat: implement separate geometry handling for panorama mask regions

- Replace Delete Geometry with Separate Geometry in O Image Depth Panorama to manage low and high mask areas independently.
- Adjust depth processing to ensure low mask regions are displaced to Dome Radius while high mask regions follow existing depth rules.
- Update tests to validate new behavior, ensuring outputs align with expected results for both mask states.
- Enhance material handling by introducing emission strength in the shadeless group.
This commit is contained in:
2026-09-17 02:23:19 +08:00
parent efd117dd9b
commit ac7f5a7f6d
13 changed files with 391 additions and 42 deletions

View File

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

View File

@@ -0,0 +1,54 @@
## Context
`O Image Depth Panorama` 现在的 Depth Mask 是一个开关包住 Delete Geometry:面中心的 mask 采样低于 Mask Threshold 就删面。被删的面在之后的链路里不复存在,Depth Mask 关闭时只能让无效面按点回退到 `Dome Radius`,整片网格仍以同一个连通体走 Depth Split 与边界平滑。
球面网格在删除后出现的自由边界就是 mask 切口,切口深度规则、边界带深度模糊与位置平滑都建立在「切口是一段自由边界」之上。Separate Geometry 可以把同一个 mask 判定变成两条独立分支:低 mask 面与其余面各自成网格,那么关闭开关时就有条件把两者分开处理。
## Goals / Non-Goals
**Goals:**
- 用 Separate Geometry(Face 域)替代 Panorama 上的 Delete Geometry,低 mask 区域成为可继续处理的几何。
- Depth Mask 打开时输出与按面删除等价的结果;关闭时两个区域各按自己的方式位移后合并。
- 维持切口规则、Depth Split、Depth Scale、UV 与材质协议的既有职责。
**Non-Goals:**
- 不改动 `O Image Depth Plane` 的 Depth Mask 删除行为,也不改动 `validity.delete_invalid_faces`。
- 不新增或改名接口输入,不改 Mask Threshold、Dome Radius、Depth Scale、Depth Split 的语义与默认值。
- 不引入跨分支的焊接、合并或平均。
## Decisions
### 分面判定沿用面心比较
Separate Geometry 的 `Selection` 接当前的 `invalid`(面中心采样 `< Mask Threshold`),`Inverted` 即保留面。判定仍只做一次比较,切口轮廓仍落在面中心上,低 mask 区域就是被旧逻辑删掉的那批面。
### 开关切换的是「低 mask 区域是否并入」
高 mask 分支照原链路处理:`split_depth_surface` → `build_surface_camera` → 深度边界模糊 → 半径 `Dome Radius + Depth Scale × (d − Dome Radius)` → Set Position → 清理 `_o_*` → 切口带位置平滑。低 mask 分支先把单位球面上的位置乘 `Dome Radius` 落到 dome 球面,再走同一套切口带位置平滑;它不参与 Depth Split,也不参与深度边界模糊(半径与采样深度无关)。
最后 `Join Geometry` 合并两条已各自定形的分支,再一起过 Flip Faces、Shade Smooth 与材质槽继承。Depth Mask 开关用一个 Geometry 类型的 Switch 实现:打开时取高 mask 分支,关断时取无 mask 一侧的结果;后者再按「Depth Scale 为 0」分一次,直接取整球。两条分支都要先算出来才能让开关只选结果,Join 因此总是执行;相比「再删一次低 mask 分支来制造空几何」,这条路径少一个节点,且把开关语义放在输出侧。
**为什么不是整片网格加回退**:旧做法把面域判定插值到点域来写回退距离,接缝点的归属取决于相邻面插值,切口处既可能拿到 dome 也可能拿到采样深度;分面后每个点只属于一个区域,判定没有插值空间。
### 关闭开关时不再保留逐位不变的契约
分面后接缝两侧各自持有自己的顶点,合并不会焊接:接缝是两条分支的自由边界,和打开开关时的切口一样。这是分面结构的直接结果,也是有意的取舍,取代 `unify-mask-cut-depth` 里「Depth Mask 关闭时几何逐位不变」的场景。
### 两侧边界都交给 Boundary Smooth
切口带平滑让接缝两侧的过渡一致:高 mask 分支平滑切口带,低 mask 分支平滑自己的自由边界,两者都发生在合并之前,各自只看到自己的拓扑。合并之后再平滑会把两条分支拼成的一张网格当成一个整体,接缝两侧互相牵动,所以平滑必须留在合并前。低 mask 分支不传 Depth Split 边界,它的自由边界全部按轮廓处理。
### Depth Scale 为 0 且关闭 Depth Mask 时旁路
Depth Scale 为 0 时整张表面都落在 dome 球面,再分面只会得到两批共面但接缝顶点不共享的网格,白白引入切口与缝隙;Depth Mask 又关断了排除,分面没有任何输出差异。这一组合因此旁路分面与全部后续处理,直接把球面乘 `Dome Radius` 输出,得到无切分的完整球面。
Depth Mask 打开时不能旁路:低 mask 区域仍要按 mask 排除。旁路条件因此是「Depth Scale 不大于 0」与「Depth Mask 关闭」的合取。
## Risks / Trade-offs
- Separate Geometry 的 `Invert` 与 Delete Geometry 在索引顺序上可能不同 → 断言按坐标、面集合与半径比较,不比较索引编号。
- 关闭开关时接缝顶点复制会让输出的顶点数多于旧的整片网格 → 测试改为按分支语义断言(面数、分区域半径、接缝无共享点);Depth Scale 为 0 的旁路单独断言封闭整球。
- 合并结果在接缝处暴露两条自由边,材质与法线在接缝上不连续 → 这是关闭 Depth Mask 时的既有语义(无效区本就与有效区差一个量级),保持翻转与 Shade Smooth 作用在合并后的整体上。
- 低 mask 分支在 Depth Scale 为 0 时与高 mask 分支重叠在同一球面 → 不焊接,保持两条分支的独立边界,行为与打开开关时的一致。

View File

@@ -0,0 +1,29 @@
## Why
`O Image Depth Panorama` 现在用 Delete Geometry 删除低 mask 面:接口只能给出「删除」,被删区域无法再作为几何参与后续处理,Depth Mask 关闭时也只能让无效面勉强落在 Dome Radius。改用 Separate Geometry 把两个区域分成独立分支,关闭开关时各走各的位移,再合并回一个输出。
## What Changes
- 低 mask 判定改为 Separate Geometry(Face 域):`Selection` 输出低于 Mask Threshold 的面,`Invert` 输出其余面,取消 `O Image Depth Panorama` 对 Delete Geometry 的依赖。
- Depth Mask 打开时只走 `Invert` 分支:保持当前的排除行为,低 mask 区域不进入输出。
- Depth Mask 关闭时两个分支分别处理:低 mask 区域整片位移到 `Dome Radius` 球面,高 mask 区域照原路径做 Depth Split、径向深度与边界平滑,两者都在自身边界带上按 Boundary Smooth 松弛,最后 Join Geometry 合并输出。
- Depth Scale 为 0 且 Depth Mask 关闭时旁路全部分支:输出直接是 `Dome Radius` 的完整球面,不做分面、切分与平滑。
- 两个分支各自输出独立的网格,接缝点不再共享;输出几何为各分支结果的并集,取消关闭开关时 `O Image Depth Panorama` 的「逐位不变」契约。
- 重建 `O_AnyImage.blend`,并按分支语义重写既有全景断言。
## Capabilities
### New Capabilities
- `panorama-mask-regions`: `O Image Depth Panorama` 如何用 Mask Threshold 分出面区域、Depth Mask 开关如何选择分支,以及关闭时两个区域各自的位移与合并结果。
### Modified Capabilities
无。`openspec/specs` 尚无可修改的已归档规格。
## Impact
- `tools/nodes/groups/image_depth_panorama.py`:Separate Geometry 分面、双分支位移、Join Geometry 合并,不再调用 `validity.delete_invalid_faces`(该函数仍由 `O Image Depth Plane` 使用)。
- `tests/tools/nodes/test_image_depth_panorama.py`:Mask Threshold 分面、开关分支选择、Dome Radius 位移与合并结果的断言。
- `src/anyimage/assets/O_AnyImage.blend`:重新构建节点资产。
- 不改动 `O Image Depth Plane`、`O Image Depth Cutout`、`O Image Relief Plane`、Operator 入口、材质与依赖。

View File

@@ -0,0 +1,91 @@
## ADDED Requirements
### Requirement: Mask Threshold 按面分出两个区域
`O Image Depth Panorama` MUST 用 Separate Geometry(Face 域)把球面网格按 Mask Threshold 分成两个区域:面中心采样低于 Mask Threshold 的面由 `Selection` 输出,其余面由 `Inverted` 输出。判定 MUST 复用同一次 Mask Threshold 比较,MUST NOT 另行采样或按深度通道判断;分面后 MUST NOT 再单独清理残留边。
#### Scenario: 切口落在面中心轮廓
- **WHEN** 无效区与有效区之间存在只跨过部分顶点的面
- **THEN** 该面在面中心仍有效时留在高 mask 区域,轮廓落在面中心的连线上,而不是所有相邻顶点都有效的内圈
#### Scenario: 两个区域互补
- **WHEN** 任意 mask 图下求值
- **THEN** 两个区域的面数之和等于输入球面的面数,各区域输出的边都至少属于一个面
### Requirement: Depth Mask 打开时只输出高 mask 区域
Depth Mask 打开时,`O Image Depth Panorama` MUST 只输出高 mask 区域经过完整深度处理的几何,低 mask 区域 MUST NOT 出现在输出中;输出 MUST 与按面删除低 mask 面后的处理结果等价。
#### Scenario: 打开时与按面删除等价
- **WHEN** Depth Mask 打开
- **THEN** 输出的面、顶点坐标与 UV 同按面删除低 mask 面后处理的结果一致
#### Scenario: 全部低于阈值
- **WHEN** Depth Mask 打开且所有面中心的采样都低于 Mask Threshold
- **THEN** 输出为空几何
### Requirement: Depth Mask 关闭时两区域分别处理再合并
Depth Mask 关闭时,低 mask 区域 MUST 保持自身方向、整片位移到 `Dome Radius` 球面,高 mask 区域 MUST 照常做 Depth Split、径向深度重建与切口带平滑,两者 MUST 在各自处理完成后 Join 成同一输出。低 mask 区域 MUST NOT 参与 Depth Split 与深度边界模糊,但 MUST 与高 mask 区域一样在自身自由边界上按 Boundary Smooth 松弛;输出 MUST NOT 跨区域焊接或平均顶点。
#### Scenario: 两个区域各自落位
- **WHEN** Depth Mask 关闭,且 mask 图同时含高、低两个区域
- **THEN** 高 mask 区域各点半径等于 `Dome Radius + Depth Scale × (d − Dome Radius)`,低 mask 区域各点半径等于 `Dome Radius`
#### Scenario: 低 mask 区域的采样深度不影响输出
- **WHEN** Depth Mask 关闭,且低 mask 区域写入与有效侧相差一个数量级的深度
- **THEN** 该区域仍整片落在 `Dome Radius` 球面上
#### Scenario: Depth Scale 为 0
- **WHEN** Depth Scale 为 0 且 Depth Mask 关闭
- **THEN** 两个区域都落在半径为 `Dome Radius` 的球面上,输出仍是完整球面
#### Scenario: 接缝两侧不共享顶点
- **WHEN** Depth Mask 关闭,且 mask 图同时含高、低两个区域
- **THEN** 接缝两侧各自保留自己的顶点,接缝在两侧都是自由边,输出顶点数多于同一网格不分面时的顶点数
#### Scenario: 两侧边界都随 Boundary Smooth 松弛
- **WHEN** Depth Mask 关闭且 Boundary Smooth 大于 0
- **THEN** 低 mask 区域接缝带内的点与高 mask 区域接缝带内的点都移动,两个区域的内部点保持不变,低 mask 区域的内部点仍落在 `Dome Radius` 球面上
### Requirement: Depth Scale 为 0 且 Depth Mask 关闭时输出整球
Depth Scale 为 0 且 Depth Mask 关闭时,`O Image Depth Panorama` MUST 直接输出半径为 `Dome Radius` 的完整球面,MUST NOT 做分面、Depth Split、切口带平滑,也 MUST NOT 留下未共享的接缝顶点;输出 MUST 是封闭网格。Depth Mask 打开时 MUST NOT 走这条旁路。
#### Scenario: 关闭开关的平坦投影没有切线
- **WHEN** Depth Scale 为 0 且 Depth Mask 关闭
- **THEN** 输出的面数与顶点数与同一球面的完整网格一致,没有自由边,各点半径等于 `Dome Radius`
#### Scenario: 打开开关的平坦投影仍按 mask 排除
- **WHEN** Depth Scale 为 0 且 Depth Mask 打开
- **THEN** 输出只含高 mask 区域,低 mask 区域不出现
### Requirement: 高 mask 区域沿用既有切口与深度语义
Separate Geometry 形成的自由边界 MUST 继续走既有切口规则:深度取相邻面采样,点保持自身射线方向。Depth Split、Depth Scale、UV 与材质槽继承 MUST 保持既有职责,`O Image Depth Plane` 的 Depth Mask 删除行为 MUST 保持不变。
#### Scenario: 打开开关时切口不出现尖刺
- **WHEN** Depth Mask 打开,且低 mask 侧采样与有效侧相差一个数量级
- **THEN** 切口圈各点的径向距离等于相邻有效面中心的采样距离
#### Scenario: 关闭开关时高 mask 区域仍按切口规则处理
- **WHEN** Depth Mask 关闭且 Boundary Smooth 为 0
- **THEN** 高 mask 区域的接缝点深度取相邻面采样,不跟随低 mask 侧的采样
#### Scenario: Plane 不受影响
- **WHEN** `O Image Depth Plane` 用同一深度图求值,Depth Mask 打开或关闭
- **THEN** 输出与本次改动前逐位一致

View File

@@ -0,0 +1,37 @@
## 1. 分面
- [x] 1.1 `build_image_depth_panorama_group` 用 `GeometryNodeSeparateGeometry`(domain FACE,selection 为面心 `invalid`)替换 `validity.delete_invalid_faces`,取出 `Selection` 低 mask 区域与 `Invert` 高 mask 区域
- [x] 1.2 去掉 Panorama 对 `validity` 的导入,确认 `O Image Depth Plane` 仍调用该函数
## 2. 两条分支
- [x] 2.1 高 mask 分支保持原链路:`split_depth_surface` → `build_surface_camera` → `smooth_boundary_depth` → 半径表达式 → Set Position → `remove_attribute_pattern` → `smooth_cut_boundary`
- [x] 2.2 去掉按点回退的 `fallback` Switch,无效判定只保留 Separate Geometry 那一处比较
- [x] 2.3 低 mask 分支加独立 Set Position,位置为单位方向乘 `Dome Radius`;不接 Depth Split、深度模糊与切口带平滑
- [x] 2.4 `Join Geometry` 合并两条分支,Depth Mask Switch 关闭时取合并结果、打开时取高 mask 分支
- [x] 2.5 Flip Faces、Shade Smooth 与 `inherit_material_slots` 保持在合并之后,两条分支共用同一套输出处理
- [x] 2.6 核对 `Depth Mask`、`Dome Radius`、`Mask Threshold` 的 description 与新语义一致
## 3. 测试
- [x] 3.1 `test_mask_threshold_culls_faces` 改为分面语义:打开时只输出高 mask 区域,关闭时输出完整球面且低 mask 区域落在 `Dome Radius`
- [x] 3.2 重写 `test_masked_area_falls_back_to_the_dome_radius`:关闭时按区域断言半径、`Dome Radius` 改变后的两个落点、`Depth Scale` 为 0 的整球结果
- [x] 3.3 补断言:关闭时低 mask 区域写入与有效侧相差一个数量级的深度,其半径仍为 `Dome Radius`
- [x] 3.4 补断言:关闭时接缝两侧各自持有顶点,接缝在两侧都是自由边,顶点数多于不分面时的输出
- [x] 3.5 补断言:关闭且 Boundary Smooth 大于 0 时低 mask 区域仍落在 `Dome Radius` 球面
- [x] 3.6 确认 `test_alpha_cut_keeps_the_ring_sharing_the_face_centre_contour`、`test_invalid_depth_does_not_bleed_into_alpha_boundary`、`test_panorama_validity_cut_smoothing_stays_in_two_ring_band` 在分面后仍通过
- [x] 3.7 运行 `uv run pytest`,确认全景与共享切口规则的既有测试全部通过
## 4. 资产与收尾
- [x] 4.1 运行 `uv run node-group build`,确认节点资产构建与校验通过、Capture Attribute 为零
- [x] 4.2 检查源码、测试与资产中不再有 Panorama 走 Delete Geometry 的旧路径,确认最终差异只涉及本次范围
- [x] 4.3 简要报告调整内容与验证结果
## 5. 追加调整
- [x] 5.1 低 mask 分支在合并前接入 `smooth_cut_boundary`,由 Boundary Smooth 控制,不传 Depth Split 边界
- [x] 5.2 Depth Scale 为 0 且 Depth Mask 关闭时旁路分面与后续处理,直接输出 `Dome Radius` 的完整球面;Depth Mask 打开时仍按 mask 排除
- [x] 5.3 `test_boundary_smooth_relaxes_both_masked_region_boundaries`:两个区域的接缝带都移动、内部不动,低 mask 区域内部仍在 `Dome Radius`
- [x] 5.4 `test_flat_scale_without_mask_returns_a_whole_sphere`:面数与顶点数同完整球面一致、无自由边、半径等于 `Dome Radius`
- [x] 5.5 重跑 `uv run node-group build` 与 `uv run pytest`,确认资产与测试通过