feat(cutout): implement boundary padding for cutout textures

- Introduced a new `Boundary Padding` preference in the Cutout Tool with a default of 2 px and a configurable range of 1-4 px.
- Updated the Cutout client to establish inward mapping from the final Mask, sampling in a fixed direction and processing independent material RGBA and Depth.
- Enhanced the handling of RGBA and Depth during cutout operations, ensuring proper mapping and preservation of source data.
- Added tests to validate the new boundary padding functionality and its integration with existing cutout processes.

feat(geometry): remove overall mesh smoothing from geometry nodes

- Removed the `Smooth` input from several geometry node groups to eliminate unnecessary overall mesh smoothing.
- Retained `Smooth Weight` for depth nodes while removing it from Cutout and Relief Plane nodes.
- Ensured that boundary, normal, and fill smoothing behaviors remain intact and consistent.
- Updated tests and documentation to reflect the changes in node interfaces and behaviors.

feat(normals): scale tangent normals with depth

- Reused the existing `o_normal_reduction` attribute to adjust Tangent Normal strength based on the geometry's `Depth Scale`.
- Ensured that Tangent Normal strength is consistent with the geometry depth, preserving full strength for materials that do not provide the attribute.
- Updated tests to verify the correct behavior of normal strength adjustments in both Tangent and Object Space.
This commit is contained in:
2026-09-15 00:40:24 +08:00
parent ab282e4fdf
commit 591e62085a
93 changed files with 1915 additions and 2851 deletions

View File

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

View File

@@ -0,0 +1,45 @@
## Context
Convert to Panorama currently validates `width == height * 2` before it exports the source color image. The shared color-input helper already accepts pixel bounds and uses the resulting file for both material color and AI analysis, so the crop can be applied once at the Blender boundary without changing the server job or duplicating image processing.
## Goals / Non-Goals
**Goals:**
- Derive a deterministic, centered, exact 2:1 integer-pixel crop from any usable static source image.
- Use the same cropped pixels for AI analysis and the generated panorama material.
- Warn users whenever the prepared input omits source pixels.
- Keep exact 2:1 inputs unchanged.
**Non-Goals:**
- Padding, stretching, resampling, or content-aware crop positioning.
- Repairing an image that is not actually an equirectangular panorama.
- Changing server inference, panorama fusion, output metadata, or node groups.
## Decisions
### Compute the largest centered exact 2:1 pixel rectangle
Let `crop_height = min(source_height, source_width // 2)` and `crop_width = crop_height * 2`. Center these dimensions independently within the source using integer offsets. This retains the maximum possible source area, handles both wide and tall images, and removes one unavoidable column from odd-width inputs. Integer floor placement gives deterministic bounds when the discarded pixels cannot be split equally.
Padding would preserve all pixels but introduce artificial panorama regions. Resizing would distort the projection. Both are less faithful than an explicit crop.
### Select bounds before preparing the shared color input
Add a small reusable image-bound calculation in the appropriate `common` image module, then pass those bounds to `prepare_material_color_input`. `material_analysis_input` will continue to derive its input from that exported color file. This guarantees that inference and the material see identical framing while retaining existing HDR, color-space, alpha, temporary-file, and cleanup behavior.
Cropping only the analysis preview was rejected because the generated material and inferred geometry would then use different angular content.
### Warn at the public operator boundary
`ConvertToPanorama` will emit one Blender `WARNING` report after determining that the crop differs from the source. The message will state original and cropped pixel dimensions. Exact 2:1 sources will not report a warning. Animated images and inputs for which the computed crop has zero width or height remain errors.
The warning belongs at the public operator boundary because that is where source dimensions and user feedback are both available; it does not need to become part of the job protocol.
## Risks / Trade-offs
- [Centered cropping can remove important edge content] → Make the crop explicit through a warning with dimensions; interactive crop selection remains outside this change.
- [Odd discarded pixel counts make perfect geometric centering impossible] → Use stable integer floor offsets and test the exact bounds.
- [Very narrow images cannot yield a positive 2:1 integer crop] → Keep a clear validation error for widths below two pixels or otherwise invalid dimensions.
- [Warning timing could imply success before setup or submission later fails] → Report the warning as part of input preparation, while preserving normal error reporting for subsequent failures.

View File

@@ -0,0 +1,27 @@
## Why
Convert to Panorama currently rejects any image whose pixel dimensions are not exactly 2:1, so even a nearly compliant panorama cannot be processed. Panorama conversion should accept arbitrary static image aspect ratios by preparing a valid 2:1 input automatically while making the loss of edge content visible to the user.
## What Changes
- Accept static panorama source images regardless of their original aspect ratio.
- Center-crop the source to the largest integer-pixel 2:1 region before preparing both the AI analysis input and panorama color material.
- Report a warning when any rows or columns are removed, including the original and cropped dimensions.
- Preserve the existing behavior without a warning when the source dimensions are already exactly 2:1.
- Continue rejecting animated or unusably small images that cannot produce a positive 2:1 crop.
## Capabilities
### New Capabilities
- `panorama-input-cropping`: Defines validation, centered 2:1 crop selection, prepared panorama inputs, and user warning behavior.
### Modified Capabilities
None.
## Impact
- Affects the Blender-side Convert to Panorama operator and shared material color input preparation used by its job submission.
- Adds focused operator and image-preparation tests for wide, tall, odd-width, already-2:1, animated, and unusably small inputs.
- Does not change the panorama job protocol, server geometry pipeline, model dependencies, or node assets.

View File

@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Panorama input uses an exact centered 2:1 crop
The system SHALL prepare panorama conversion from the largest centered integer-pixel rectangle whose width is exactly twice its height. The prepared color material and AI analysis input MUST use the same cropped pixels without stretching, padding, or aspect-ratio resampling.
#### Scenario: Source is wider than 2:1
- **WHEN** a user converts a usable static image whose width is greater than twice its height
- **THEN** the system crops columns equally where possible from the left and right and prepares an exact 2:1 input at the source height
#### Scenario: Source is narrower than 2:1
- **WHEN** a user converts a usable static image whose width is less than twice its height
- **THEN** the system crops rows equally where possible from the top and bottom and prepares the largest exact 2:1 input at or below the source width
#### Scenario: Source width is odd
- **WHEN** a usable static source has an odd width that cannot be part of an exact integer-pixel 2:1 rectangle
- **THEN** the system omits the unmatched column as part of its deterministic centered crop
#### Scenario: Source is already 2:1
- **WHEN** a user converts a usable static image whose width is exactly twice its height
- **THEN** the system prepares the full source image without removing pixels
### Requirement: Panorama crop is disclosed to the user
The system SHALL issue one warning whenever panorama input preparation removes source pixels, and the warning MUST identify both the original and cropped pixel dimensions.
#### Scenario: Conversion requires cropping
- **WHEN** the computed 2:1 bounds differ from the source bounds
- **THEN** the system reports a warning containing the source width and height and the cropped width and height
#### Scenario: Conversion does not require cropping
- **WHEN** the source bounds already form an exact 2:1 image
- **THEN** the system does not report a crop warning
### Requirement: Unsupported panorama sources remain rejected
The system MUST reject animated images and images too small to produce a positive integer-pixel 2:1 crop before submitting panorama generation.
#### Scenario: Source is animated
- **WHEN** a user attempts to convert an animated image
- **THEN** the system reports an error and does not submit panorama generation
#### Scenario: Source cannot produce a positive crop
- **WHEN** a source image cannot provide at least a 2-pixel-wide by 1-pixel-high crop
- **THEN** the system reports an error and does not submit panorama generation

View File

@@ -0,0 +1,15 @@
## 1. Crop Bounds
- [x] 1.1 Add a common helper that returns the largest deterministic centered exact 2:1 integer-pixel bounds and rejects dimensions that cannot produce a positive crop
- [x] 1.2 Add focused tests for exact, wide, tall, odd-width, uneven-margin, and unusably small dimension cases
## 2. Panorama Conversion
- [x] 2.1 Update Convert to Panorama to keep animated-image validation, compute the crop bounds, and pass them into the shared material color input preparation
- [x] 2.2 Report one crop warning with original and cropped dimensions only when pixels are removed
- [x] 2.3 Update panorama operator tests to verify arbitrary aspect ratios submit cropped color and analysis inputs, exact 2:1 input stays unchanged, and unsupported sources remain rejected
## 3. Verification
- [x] 3.1 Run the related common image and panorama conversion test suites and confirm they pass
- [x] 3.2 Inspect the final diff and search the panorama conversion path to confirm the strict 2:1 rejection and stale expectations are removed

View File

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

View File

@@ -0,0 +1,52 @@
## Context
`O Image Relief Plane` 先由 `O Image Plane` 生成固定底面位于 `Z=0`、可变面位于 `Z=Thickness` 的几何,再按 `Reference Depth - sampled depth` 计算位移。当前有厚度时,负位移最多接近 `-Thickness`,因此可变面会进入基础厚度区域;`Reference Depth` 又是校准值,不适合作为艺术调整用的整体平移控件。
## Goals / Non-Goals
**Goals:**
- 将 `Z=Thickness` 明确定义为 depth 0 和浮雕深度的最低高度。
- 让 `Depth Scale` 以 depth 0 为位移基准改变起伏幅度。
- 提供默认值为 `1` 的 `Depth Offset`,在方向换算和缩放之前进一步矫正 `Reference Depth`。
- 保持固定底面不动,并保持侧壁各层按原有权重插值位移。
- 创建 Relief Plane 时给出基于完整有效深度场的近似 `Depth Direction`。
**Non-Goals:**
- 不改变 `Reference Depth`、`Uniform Scale` 或 `Depth Direction` 的含义。
- 不改变 Depth Plane、Cutout、AI 产物及对象创建流程。
- 不为旧节点图保留兼容接口或迁移节点。
## Decisions
### 使用单一最终位移公式
可变面的完整标量位移定义为:
`corrected_reference_depth = reference_depth + depth_offset`
`relief_offset = max((direction_adjusted(corrected_reference_depth) - sampled_metric_depth) * depth_scale, 0)`
最终可变面高度为 `Thickness + relief_offset`。`Depth Offset` 与 `Reference Depth` 先相加,矫正后的参考深度再参与 `Depth Direction` 的参考平面换算、采样深度求差和 `Depth Scale` 缩放。缩放只产生从 `Thickness` 高度出发的位移,所以 depth 0 始终对应 `Thickness` 高度。
备选方案是把 `Depth Offset` 作为缩放后的几何平移,但这会脱离参考深度标定,并使 `Depth Scale=0` 时仍产生位移。将它作为 Reference Depth 矫正值,可让两者遵循同一套方向和缩放语义。
### Depth Offset 使用主界面距离输入
新增 `Depth Offset`,类型为 Float、subtype 为 `DISTANCE`、默认值为 `1`,允许正负值。`Depth Direction` 与 `Depth Offset` 均不放入 Options,主界面在 `Thickness` 后依次排列 `Depth Direction`、`Depth Offset`、`Depth Scale`。节点图先将 Depth Offset 与 `Reference Depth` 相加,再把结果传入现有 `reference_plane_depth()`。
### 沿现有层权重应用最终位移
最终 `relief_offset` 继续乘以从固定底面到可变面的 `layer_weight`。这样固定底面保持 `Z=0`,可变面应用完整位移,侧壁中间层保持线性插值。`Thickness=0` 时沿用现有权重为 `1` 的单面行为。
### Relief 与 Cutout Symmetry 共用稳健平面拟合
将稳健深度平面拟合下沉到 `common.depth`。Relief 创建时把完整深度图的有效像素中心映射到图片平面的局部 XY,以 `Uniform Scale` 转为本地深度后拟合平面,并用归一化 `(slope_x, slope_y, 1)` 初始化 `Depth Direction`。Cutout Symmetry 改用同一公共函数,继续在其裁切网格采样域上拟合,避免维护两套算法。`Depth Offset` 使用节点组默认值 `1`。
## Risks / Trade-offs
- [已有场景依赖浮雕压入基础厚度] → 这是本次明确要求的行为变化;通过几何求值测试锁定新边界,不增加兼容分支。
- [负 Depth Offset 使更多采样在 depth 0 处被截断,局部细节可能变平] → 接口说明明确 Depth Offset 是参考深度矫正,并用正、负 Depth Offset 场景覆盖预期。
- [节点接口和发布资产不一致] → 修改构建脚本后运行 `uv run node-group build`,同时检查保存资产接口和求值结果。
- [整图内容不是单一平面] → 沿用 Cutout Symmetry 的迭代稳健权重降低离群深度影响,只把结果作为可继续手调的初始值。

View File

@@ -0,0 +1,26 @@
## Why
`O Image Relief Plane` 当前允许远于参考深度的采样把浮雕面压到 `Thickness` 高度以下,只在接近固定底面时停止,导致基础厚度同时承担浮雕零点和防穿透边界,深度整体位置也无法独立调节。需要明确浮雕深度零点,使缩放、钳制与整体偏移各自表达单一职责。
## What Changes
- 将 `Thickness` 所在高度定义为浮雕 depth 0,并把采样深度产生的向内位移钳制在该高度,不再让深度结果进入基础厚度区域。
- 为 `O Image Relief Plane` 新增默认值为 `1` 的公开 `Depth Offset` 距离参数,作为 `Reference Depth` 的附加矫正,在方向换算和 `Depth Scale` 之前参与相对深度计算。
- 将 `Depth Direction` 和 `Depth Offset` 移到主界面,并在 `Thickness` 后按 `Depth Direction`、`Depth Offset`、`Depth Scale` 排列。
- 创建 Relief Plane 时从完整深度图拟合近似深度平面,并用其法向初始化 `Depth Direction`。
- `Depth Scale` 以 `Thickness` 所在的 depth 0 为位移基准,并缩放包含 `Depth Offset` 矫正的相对深度;`Thickness` 高度本身不参与缩放。
- 更新节点组行为测试、接口清单与发布 `.blend` 节点资产。
## Capabilities
### New Capabilities
- `relief-plane-depth-controls`: 定义 Relief Plane 的 depth 0、钳制、缩放和整体偏移行为。
### Modified Capabilities
无。
## Impact
影响 `tools/nodes/groups/image_relief_plane.py`、`src/anyimage/common/depth.py`、Plane 创建逻辑、相关测试、节点资产接口清单,以及 `src/anyimage/assets/O_AnyImage.blend`。不增加依赖,也不改变 AI 深度产物;已有 Modifier 会获得新的节点组输入,原先依赖负向深度进入基础厚度区域的结果将发生变化。

View File

@@ -0,0 +1,73 @@
## ADDED Requirements
### Requirement: Relief depth is clamped at the thickness height
`O Image Relief Plane` SHALL 将未置换可变面所在的 `Thickness` 高度定义为 depth 0。经过参考深度矫正、方向换算和 `Depth Scale` 得到的最终浮雕位移 MUST 不小于零,使可变面不进入固定底面与 `Thickness` 高度之间的基础厚度区域。
#### Scenario: Far depth would move below depth zero
- **WHEN** 矫正参考深度与采样深度之差经过方向换算和缩放后小于零
- **THEN** 可变面的最终浮雕位移钳制为零
- **AND** 可变面位于 `Thickness` 高度
#### Scenario: Positive relief depth
- **WHEN** 矫正参考深度与采样深度之差经过方向换算和缩放后大于零
- **THEN** 可变面保留完整的正向浮雕位移
- **AND** 正向位移不受 `Thickness` 大小限制
#### Scenario: Keep the fixed base
- **WHEN** `Thickness` 大于零且浮雕深度发生变化
- **THEN** 固定底面的所有顶点保持在对象空间 `Z=0`
- **AND** 侧壁各层按其从固定底面到可变面的归一化位置线性应用最终浮雕位移
### Requirement: Relief Plane exposes a reference depth correction
`O Image Relief Plane` SHALL 在主界面公开 `Depth Offset` 距离输入,默认值为 `1` 并允许正负值。`Depth Direction` 与 `Depth Offset` SHALL 移出 Options,并在 `Thickness` 后按 `Depth Direction`、`Depth Offset`、`Depth Scale` 排列。系统 SHALL 先按 `Reference Depth + Depth Offset` 得到矫正参考深度,再进行 `Depth Direction` 参考平面换算、与缩放采样深度求差及 `Depth Scale` 缩放,最后把负位移钳制为零。
#### Scenario: Use the default offset
- **WHEN** `Depth Offset` 为默认值 `1`
- **THEN** 矫正参考深度等于 `Reference Depth + 1`
- **AND** 系统按现有方向换算和 `Depth Scale` 计算浮雕位移
- **AND** 最终结果仍钳制在 depth 0
#### Scenario: Increase the reference depth correction
- **WHEN** 用户增加 `Depth Offset`
- **THEN** 系统在方向换算和 `Depth Scale` 之前增加矫正参考深度
- **AND** Depth Offset 对最终位移的影响随 `Depth Scale` 一同缩放
#### Scenario: Decrease the reference depth correction
- **WHEN** 用户减小 `Depth Offset`
- **THEN** 系统在方向换算和 `Depth Scale` 之前减小矫正参考深度
- **AND** 任何结果均不低于 depth 0
#### Scenario: Apply depth direction after correction
- **WHEN** `Depth Offset` 非零且用户改变 `Depth Direction`
- **THEN** 系统使用包含 Depth Offset 的矫正参考深度构建方向调整后的参考平面
- **AND** 随后才计算相对深度和应用 `Depth Scale`
### Requirement: Depth Scale uses depth zero as its invariant origin
`Depth Scale` SHALL 以 depth 0 为几何位移基准,缩放包含 `Depth Offset` 矫正的相对深度差,MUST 不缩放 `Thickness` 高度本身。
#### Scenario: Set depth scale to zero
- **WHEN** `Depth Scale` 为 `0`
- **THEN** 采样深度差、`Reference Depth` 和 `Depth Offset` 均不产生几何位移
- **AND** 可变面位于 `Thickness` 高度
#### Scenario: Change depth scale
- **WHEN** 用户改变 `Depth Scale` 且保持 `Thickness`、`Reference Depth` 和 `Depth Offset` 不变
- **THEN** 系统以 depth 0 为基准缩放包含 Depth Offset 矫正的采样深度差
- **AND** `Thickness` 所在高度保持不变
### Requirement: Relief Plane initializes an approximate depth direction
系统 SHALL 在创建 Relief Plane 时,将完整深度图的有效像素映射到图片平面局部坐标,以 `Uniform Scale` 换算深度并进行稳健平面拟合,再用拟合平面的归一化方向初始化 `Depth Direction`。Relief Plane 与 Cutout Symmetry MUST 共用同一平面拟合实现。
#### Scenario: Create a relief from a sloped depth field
- **WHEN** 有效深度场包含可拟合的整体斜率
- **THEN** 新建 Relief Plane 的 `Depth Direction` 使用拟合斜率对应的归一化方向
- **AND** `Depth Offset` 保持默认值 `1`
#### Scenario: Create a relief from a flat depth field
- **WHEN** 有效深度场整体为平面且没有斜率
- **THEN** 新建 Relief Plane 的 `Depth Direction` 近似为 `(0, 0, 1)`
#### Scenario: Create a relief without usable depth samples
- **WHEN** 深度图没有可用于拟合的正有限深度
- **THEN** Relief Plane 仍然成功创建
- **AND** `Depth Direction` 回退为 `(0, 0, 1)`

View File

@@ -0,0 +1,21 @@
## 1. Relief depth behavior
- [x] 1.1 Add the public `Depth Offset` distance input to `O Image Relief Plane`, with a default of one and signed range; expose `Depth Direction`, `Depth Offset`, and `Depth Scale` directly below `Thickness` in that order.
- [x] 1.2 Add `Depth Offset` to `Reference Depth` before `reference_plane_depth()`, then replace the thickness-dependent inward limit with a zero clamp after direction adjustment and `Depth Scale`, preserving fixed-base and side-layer weighting.
## 2. Behavior verification
- [x] 2.1 Update Relief Plane geometry tests to verify far depths stop at `Thickness`, positive depths remain unrestricted, and the fixed base and side interpolation remain valid.
- [x] 2.2 Add positive, negative, and zero `Depth Offset` tests, including direction adjustment, `Depth Scale = 0`, and checks that Depth Offset is scaled as part of the corrected relative depth while the depth-zero height remains fixed.
- [x] 2.3 Update the node-group interface inventory test for `Depth Direction` and `Depth Offset` names, order, panel placement, and old base-plane clamp behavior.
## 3. Node asset
- [x] 3.1 Run `uv run node-group build` to rebuild and validate `src/anyimage/assets/O_AnyImage.blend` from the updated source and tests.
- [x] 3.2 Run the relevant Relief Plane and plane-object tests, then inspect the final diff for stale inward-limit nodes or old behavior references.
## 4. Initial depth direction
- [x] 4.1 Extract the robust plane-direction fit into `common.depth` and reuse it from Cutout Symmetry calibration.
- [x] 4.2 Fit the complete valid Relief depth image in local plane coordinates and initialize the new Modifier's `Depth Direction`.
- [x] 4.3 Add sloped-depth initialization and shared calibration regression tests.

View File

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

View File

@@ -0,0 +1,52 @@
## Context
Cutout 客户端持有源图裁切结果、Selection Mask、最终 `content_values` 以及对象创建所需的全部图片。服务端目前在 MoGe 推理后按源图 Alpha 做 Cutout 专用深度延伸,但它不知道 Selection Mask,因此无法与最终 Cutout 轮廓一致。材质、Depth 和 Normal 共用 `UVMap`,移动 UV 会让所有采样发生位移。
## Goals / Non-Goals
**Goals:**
- 从最终 Cutout Mask 得到唯一、局部、结构向内的采样方向。
- 用同一 mapping 处理材质 RGBA 与 Depth,而不改变 MoGe 输入或 UV。
- 在客户端完成处理,使 Selection Mask、颜色图和深度结果处于同一数据边界。
- 只保留必要的尺寸、有限值和结构边界保护,不建立 donor 可靠性分级。
**Non-Goals:**
- 不使用 Alpha 0.95、最近可靠 donor、core/candidate/fallback 或跨方向候选比较。
- 不处理 Normal,也不修改源 Image、网格几何、UV 或节点组。
- 不让 padding 重新定义 MoGe 的整体深度标定。
## Decisions
### 从最终 Mask 构造结构 inward mapping
以 `content_values >= alpha_threshold` 得到最终二值 Mask。用有符号距离场的局部梯度取得指向 Mask 内部的单位法线;轮廓外目标通过其边界落点继承该法线,而不是搜索多个内部 donor。对边界位置 `b`,内部采样位置为 `q = b + inward * padding`,边界带像素沿同一法线使用 `q` 的值。
同一 `Boundary Padding` 同时定义向内采样距离与向外写入宽度。Mask 内超过该距离的像素不变;边界内侧带、边界和外侧带使用结构内向样本,从而把 UV 边界内缩的采样结果烘焙到纹理,而不改变 UV。薄结构中若 `q` 沿射线提前离开当前 Mask,则使用该射线最后一个内部位置。角点使用距离场给出的局部法线;零梯度位置保持原值。
该规则没有 Alpha 可靠阈值、最近 donor 或回退层。方向完全由 Cutout 结构决定,像素内容不参与方向选择。
### 客户端统一处理 Color 与 Depth
同步 Cutout 在独立 Color Image 创建后立即处理;AI Cutout 等服务端返回原始产物后,加载临时 Color 与 Depth 数据,在创建材质和对象之前处理。两条路径共用一个纯数组 mapping 入口。mapping 按各目标图片分辨率从同一 Mask 重建,padding 以材质图像素为基准同比缩放到 Depth。
材质对边界带搬运完整 RGBA。Depth 从内部采样 camera Z 与 A validity;目标 R/G 不复制 donor 的相机位置,而是按 Metadata 中的像素单位 intrinsics 和目标像素中心从新 Z 重建 camera X/Y。这样只延伸深度表面,不把 donor 的投影视线横向搬到边界。
处理后的 Blender Image 重新打包。原始 `reference_depth` 和 intrinsics 保持不变:padding 是采样保护,不参与全局尺度校准。Normal 使用服务端原始结果。
### 服务端恢复为原始产物生成器
Cutout Job 不再向 `generate_moge_artifacts` 请求 `extend_edge_depth`。若搜索确认没有其他调用方,移除通用生成器的延伸参数、导入、旧服务端算法和仅覆盖旧行为的测试;否则保留其他真实调用方所需的最小能力。MoGe 仍读取未 padding 的原始 RGB,Depth Metadata 由原始 Frame 生成。
### Preference 只影响新对象
Preferences 的 Cutout Tool 提供 `Boundary Padding` 浮点像素值,默认 2、范围 1–4。操作开始时捕获当前值,保证异步响应不受中途设置变化影响;已有 Cutout 不追溯更新。
## Risks / Trade-offs
- [薄结构不足以容纳完整内部距离] → 沿同一 inward ray 截止在最后一个内部采样点,不转向其他 donor。
- [距离场在尖角或对称轴出现零梯度] → 仅对应位置保持原值,避免引入任意方向。
- [Color 与 Depth 分辨率不同] → 从同一 Mask 按目标尺寸重建 mapping,并按统一比例缩放 padding。
- [Depth XYZ 被错误整体复制] → 只搬运 Z/A,使用目标像素与原 intrinsics 重建 X/Y。
- [修改 packed Image 后保存旧内容] → 在对象消费前 update 并重新 pack,覆盖保存重载与失败清理。

View File

@@ -0,0 +1,28 @@
## Why
Cutout 的材质和深度在网格轮廓处会线性混合轮廓外的不稳定像素。移动共享 UV 会同时扭曲多个纹理消费者;按最近 donor 搜索又会在复杂轮廓处产生不确定方向,因此需要在 Cutout 客户端按唯一的结构内向统一延伸边界纹理。
## What Changes
- 服务端停止 Cutout 专用深度延伸,只输出未经边缘处理的 MoGe Depth、Metadata 与 Normal;MoGe 输入保持原样。
- Cutout 客户端从最终 Mask 建立局部结构 inward mapping,以固定方向从轮廓内采样,并同时处理独立材质 RGBA 与 Depth。
- 材质搬运完整 RGBA;Depth 搬运 camera Z 与 validity,并使用目标像素和原始 intrinsics 重建 camera X/Y。
- Preferences 的 Cutout Tool 增加 `Boundary Padding`,默认 2 px、范围 1–4 px;同一数值同时表示内部采样距离和外部延伸宽度。
- 保持源 Image、网格几何、UV、Normal 与原始 Depth Metadata 标定不变。
## Capabilities
### New Capabilities
- `cutout-boundary-padding`: 规定 Cutout 客户端对材质和深度使用同一结构内向映射的边界延伸、设置与数据隔离。
### Modified Capabilities
## Impact
- `src/anyimage/operators/cutout_tool` 的最终 Mask、颜色图和深度图处理及对象创建路径。
- `src/anyimage/operators/cutout_tool` 中的图片缓冲读写与目标射线 Depth 重建。
- `src/anyimage/preferences.py` 的 Cutout Tool 设置。
- `src/anyimage/server/jobs/cutout.py` 与 `server/geometry/prediction_artifacts.py` 移除 Cutout 深度延伸启用路径;无节点资产变化。
- Cutout padding、深度投影、同步/异步路径、Preferences 和资源生命周期测试。

View File

@@ -0,0 +1,60 @@
## ADDED Requirements
### Requirement: Cutout boundary mapping follows the structural interior
The system SHALL derive one local inward direction from the final Cutout threshold Mask without selecting among content-based donor candidates. For each boundary location it SHALL sample at Boundary Padding pixels along that inward direction and transport the sample through the inner boundary band, boundary and outer padding band.
#### Scenario: Outer and hole boundaries
- **WHEN** the final Cutout Mask contains an outer outline or a hole
- **THEN** its mapping points from that boundary into the adjacent filled Mask region
#### Scenario: Thin structure
- **WHEN** the configured inward distance would leave the current Mask along the same ray
- **THEN** the mapping stops at the last position inside that Mask without changing direction or searching another donor
#### Scenario: Undefined local direction
- **WHEN** a boundary-band location has no finite nonzero local inward direction
- **THEN** that location remains unchanged
### Requirement: Material and depth share the Cutout boundary mapping
The system SHALL apply the same structural mapping to the independent Cutout material Color Image and optional Depth Image after raw AI artifacts return and before material or geometry consumption. It SHALL scale the mapping to each target image resolution.
#### Scenario: Material padding
- **WHEN** Boundary Padding is positive
- **THEN** the material boundary band receives complete RGBA from its mapped internal location
#### Scenario: Depth padding
- **WHEN** a Depth Cutout has positive Boundary Padding
- **THEN** its boundary band receives camera Z and validity from the mapped internal location and camera X/Y are reconstructed for each target pixel from the original intrinsics
#### Scenario: Different resolutions
- **WHEN** material and Depth images have different dimensions
- **THEN** both mappings represent the same normalized Cutout boundary and proportional padding width
### Requirement: Boundary processing is isolated from source and AI data
The system SHALL preserve the source Image, original Cutout Mask, mesh geometry, UV coordinates, MoGe input, Normal output, intrinsics and reference depth. The server SHALL generate raw Cutout Depth without edge extension.
#### Scenario: AI Cutout
- **WHEN** Cutout requests Depth or Normal generation
- **THEN** MoGe processes the unpadded input and only the returned Color and Depth images receive client-side boundary processing
#### Scenario: Existing depth calibration
- **WHEN** client-side Depth padding changes boundary pixels
- **THEN** the original Metadata reference depth remains the Cutout calibration reference
### Requirement: Boundary Padding is configurable
The system SHALL expose `Boundary Padding` in Preferences under Cutout Tool as a pixel distance with a default of 2, a minimum of 1 and a maximum of 4. Each Cutout operation SHALL capture the current value and existing objects SHALL remain unchanged.
#### Scenario: Asynchronous preference change
- **WHEN** the preference changes after an AI Cutout starts
- **THEN** its response uses the value captured when that operation started
### Requirement: Processed images preserve their lifecycle
The system SHALL preserve existing Color and Depth precision, interpretation, ownership and cleanup behavior after client-side boundary processing.
#### Scenario: Successful creation
- **WHEN** a padded Cutout is created successfully
- **THEN** its processed independent Color and optional Depth Images are packed with their updated pixels
#### Scenario: Failed creation
- **WHEN** Cutout creation fails after loading either image
- **THEN** unowned Blender Images and temporary files are released by the existing cleanup path

View File

@@ -0,0 +1,26 @@
## 1. Boundary Padding setting
- [x] 1.1 Add the Cutout Tool `Boundary Padding` preference with default 2 px, range 1–4 px and a clamped reader.
- [x] 1.2 Capture the configured value when each Cutout operation starts so synchronous and asynchronous responses use the same operation value.
## 2. Structural inward mapping
- [x] 2.1 Build a pure array mapping from the final threshold Mask that carries each boundary-band location along one local structural inward direction to the configured internal distance.
- [x] 2.2 Bound thin-structure rays to their last same-direction internal position and leave undefined directions unchanged without donor thresholds or fallback searches.
- [x] 2.3 Cover outer outlines, holes, concave corners, thin structures, scaled target resolutions and zero bypass.
## 3. Client-side image processing
- [x] 3.1 Apply the mapping to independent material Color RGBA for synchronous and AI Cutouts while preserving the source Image and AI input.
- [x] 3.2 Apply the mapping to Depth Z and validity, reconstruct target X/Y from Metadata intrinsics, and preserve reference depth.
- [x] 3.3 Update and repack processed Color and Depth Images before object consumption while preserving byte, float/HDR and CHANNEL_PACKED interpretation and cleanup.
## 4. Raw server artifacts
- [x] 4.1 Stop enabling server-side depth edge extension for Cutout and remove the unused extension path if no real caller remains.
- [x] 4.2 Update server tests to require raw MoGe Depth and unchanged Metadata generation.
## 5. Integration and validation
- [x] 5.1 Cover preference capture, synchronous/AI parity, unmodified geometry and UV, correct target-ray Depth projection, Normal isolation, packed ownership and failure cleanup.
- [x] 5.2 Run related tests, validate OpenSpec strictly, and inspect the final diff and stale references without rebuilding node assets or extension packages.

View File

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

View File

@@ -0,0 +1,33 @@
## Context
五个主节点组在业务几何完成后调用共享 `expand_smooth()`,由公开的 `Smooth` 与 `Smooth Weight` 控制整体顶点位置平滑。三个深度节点组还通过同一个 `Smooth Weight` 驱动 `Boundary Smooth`;`O Image Cutout` 与 `O Image Relief Plane` 则没有该权重的其他消费者。项目同时存在 `Boundary Smooth`、`Normal Smooth`、`Fill Smooth` 和 shade smooth,它们分别承担切边修整、位移法线处理、填充墙体构造与着色职责,不属于待移除的末端整体 smooth。
## Goals / Non-Goals
**Goals:**
- 移除五个主节点组输出前的整体 `expand_smooth()` 后处理及其 `Smooth` 接口。
- 按真实消费关系处理 `Smooth Weight`:深度节点组保留,Cutout 与 Relief Plane 删除。
- 保持边界、法线、填充墙体与着色平滑行为不变。
- 使源码、保存的节点资产、接口测试与行为测试保持一致。
**Non-Goals:**
- 不移除或重新调参 `Boundary Smooth`、`Normal Smooth`、`Fill Smooth`。
- 不取消 `GeometryNodeSetShadeSmooth` 或 Blender object polygon 的 smooth shading。
- 不改变生成前的轮廓平滑、深度字段模糊或其他不位于主输出末端的专用算法。
## Decisions
1. 以“主几何构建完成后,由 `Smooth` socket 控制的 `expand_smooth()`”作为删除边界。这样可直接对应用户可见接口和五条真实调用链,避免把名称含 smooth 的不同业务误删。备选方案是删除所有含 smooth 的节点,但会破坏边界、法线和墙体生成能力。
2. 五个节点组均删除 `Smooth` socket,并把原本送入整体平滑的几何直接连接至现有 shade smooth 或 group output。此方式不引入旁路或兼容 socket,符合项目不保留旧路径的约束。
3. `Smooth Weight` 按消费者保留:`O Image Depth Cutout`、`O Image Depth Plane`、`O Image Depth Panorama` 继续用于 `smooth_cut_boundary()`;`O Image Cutout` 与 `O Image Relief Plane` 删除该 socket。共享 `expand_smooth()` 仍由边界平滑实现使用,因此保留公共实现。
4. 测试从“整体 smooth 可配置”改为验证接口不存在、未追加整体平滑、专用平滑仍有效。节点组逻辑与接口变化后执行规定的 `uv run node-group build`,由构建流程更新并验证 `.blend` 资产。
## Risks / Trade-offs
- [已有文件中的 modifier 仍保存旧 socket 值] → 这是明确的接口破坏;不提供兼容层,新资产和新建对象只暴露当前接口。
- [删除整体平滑后表面更忠实但可能更显离散] → 由细分密度及各专用平滑控制负责各自质量,不重新引入通用末端处理。
- [误删共享权重或公共 helper] → 以调用图和节点接口测试确认三个边界平滑消费者仍连接,公共 helper 仍被 `boundary_smoothing.py` 引用。
- [保存资产与源码漂移] → 必须运行节点资产构建,并执行相关节点与 operator 测试后检查旧接口引用。

View File

@@ -0,0 +1,27 @@
## Why
几何节点组当前在主体几何构建完成后提供一轮可选的整体 mesh smooth,既增加节点接口与实现复杂度,也会软化已经确定的表面细节。应移除这类末端后处理,同时保留边界平滑、法线平滑等独立业务所需的控制与权重。
## What Changes
- **BREAKING**:从所有相关几何节点组公开接口移除 `Smooth` 输入,生成结果不再执行末端整体 mesh smooth。
- 删除仅服务于末端整体 smooth 的节点链、构建调用与测试约束。
- `Smooth Weight` 若仍被 `Boundary Smooth` 等独立处理使用则保留;若只控制已删除的末端 smooth,则连同接口与相关逻辑删除。
- 保留 `Boundary Smooth`、`Normal Smooth`、shade smooth,以及深度/轮廓生成过程中的其他专用平滑行为。
- 重建并验证 `O_AnyImage.blend`,同步节点组接口清单、布局预期及相关内部说明。
## Capabilities
### New Capabilities
- `geometry-node-smoothing`: 规定几何节点输出不得追加整体 mesh smooth,并明确专用平滑控制及共享权重的保留规则。
### Modified Capabilities
无。
## Impact
- 影响 `O Image Cutout`、`O Image Depth Cutout`、`O Image Depth Plane`、`O Image Depth Panorama`、`O Image Relief Plane` 的公开 modifier 接口与生成图。
- 影响 `tools/nodes/groups`、共享 smoothing 构建代码的实际引用、节点资产与节点相关测试。
- 现有依赖 `Smooth` socket identifier 的 modifier 配置或调用方需要停止写入该输入;`Smooth Weight` 只在没有其他消费者的节点组中移除。

View File

@@ -0,0 +1,34 @@
## ADDED Requirements
### Requirement: 主节点组不得执行末端整体 mesh smooth
系统 SHALL 让 `O Image Cutout`、`O Image Depth Cutout`、`O Image Depth Plane`、`O Image Depth Panorama` 与 `O Image Relief Plane` 在主体几何构建完成后直接进入着色或输出阶段,不得追加由通用 `Smooth` 控制的整体顶点位置平滑。
#### Scenario: 节点组接口不再提供整体 smooth
- **WHEN** 检查任一受影响节点组的公开输入接口
- **THEN** 该接口不包含 `Smooth`
#### Scenario: 输出路径不经过整体 smooth
- **WHEN** 构建任一受影响节点组并追踪其最终业务几何到输出的连接
- **THEN** 输出路径不包含原先由 `Smooth` 和 `Smooth Weight` 控制的整体 `expand_smooth()` 后处理
### Requirement: Smooth Weight 按独立消费者保留
系统 MUST 在 `Smooth Weight` 仍控制 `Boundary Smooth` 时保留该输入,并在它仅服务已删除的整体 smooth 时删除该输入。
#### Scenario: 深度节点组保留共享权重
- **WHEN** 检查 `O Image Depth Cutout`、`O Image Depth Plane` 或 `O Image Depth Panorama`
- **THEN** 节点组仍提供 `Smooth Weight`,且该值继续参与边界平滑计算
#### Scenario: 无独立消费者的权重被删除
- **WHEN** 检查 `O Image Cutout` 或 `O Image Relief Plane`
- **THEN** 节点组接口不包含 `Smooth Weight`
### Requirement: 专用平滑行为保持可用
系统 SHALL 保留与末端整体 mesh smooth 不同职责的边界平滑、法线平滑、填充墙体平滑和 smooth shading。
#### Scenario: 深度边界和法线控制仍存在
- **WHEN** 检查受影响深度节点组的接口与计算路径
- **THEN** 既有 `Boundary Smooth` 与适用的 `Normal Smooth` 仍按原职责工作
#### Scenario: 填充与着色平滑不受影响
- **WHEN** 构建相关节点组并检查输出
- **THEN** `Fill Smooth` 仍仅用于生成墙体内部,且既有 smooth shading 仍应用于对应表面

View File

@@ -0,0 +1,17 @@
## 1. 节点组实现
- [x] 1.1 从 Cutout、Depth Cutout、Depth Plane、Depth Panorama 与 Relief Plane 删除 `Smooth` 接口及末端整体 `expand_smooth()` 调用,直接连接原有着色或输出阶段
- [x] 1.2 从 Cutout 与 Relief Plane 删除仅服务整体 smooth 的 `Smooth Weight`,并确认三个深度节点组继续以 `Smooth Weight` 驱动边界平滑
- [x] 1.3 检查共享 smoothing 构建函数的剩余调用,保留边界平滑所需实现并清理仅随末端整体 smooth 产生的无用代码
## 2. 调用方与测试
- [x] 2.1 清理 operator、节点检查工具和测试中对已删除 `Smooth` 及相应 `Smooth Weight` 的赋值与接口预期
- [x] 2.2 更新节点组库存与布局测试,验证五个节点组不再暴露 `Smooth`,且仅三个深度节点组保留 `Smooth Weight`
- [x] 2.3 精简整体 smooth 行为测试,并保留或补充边界平滑、法线平滑、Fill Smooth 与 smooth shading 不变的有效覆盖
## 3. 资产与验证
- [x] 3.1 同步 `tools/nodes` 与 `docs/internals` 中相关接口和实现说明,检查仓库内不存在失效的整体 `Smooth` 引用
- [x] 3.2 运行 `uv run node-group build`,更新并验证 `src/anyimage/assets/O_AnyImage.blend`
- [x] 3.3 运行相关节点与 operator 测试,检查最终差异并确认专用平滑路径仍正常

View File

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

View File

@@ -0,0 +1,54 @@
## Context
`O Image Layer` currently reads `o_normal_reduction` and calculates Tangent and Object Normal Map strengths from `1 - attribute`. Its absence evaluates as zero, preserving full strength. `O Image Relief Plane` changes surface displacement with `Depth Scale` but supplies no matching material attribute, so its normal map remains visually over-strong at lower values.
The current Depth Plane and Depth Cutout conversion paths request Object Space normals. The panorama material is shadeless. Relief Plane is the only current Depth Scale geometry path that supplies a reduction attribute, and the shared material already applies that attribute in both normal spaces.
## Goals / Non-Goals
**Goals:**
- Reuse the existing `o_normal_reduction` attribute contract for Tangent Normal strength.
- Keep Tangent Normal strength numerically equal to the geometry's current `Depth Scale` on Relief Plane.
- Preserve full strength for material users and geometry that do not provide the attribute.
- Rebuild and verify the node asset and its rendered behavior.
**Non-Goals:**
- Alter Object Space normal-color transformation.
- Change Depth Scale ranges, geometry projection, normal-map generation, or user-facing sockets.
- Add a new normal-scale attribute or alter the existing material attribute lookup.
## Decisions
### Reuse the reduction attribute
`O Image Layer` already calculates the normal multiplier as `1 - o_normal_reduction`. Relief Plane therefore writes `1 - Depth Scale` to its final geometry. This produces a normal multiplier equal to `Depth Scale`, including zero and values greater than one, without changing the established shader contract.
Alternative: introduce a second scale attribute and alter the shader to use it. This duplicates an existing contract and would require migration of current geometry producers without improving the resulting calculation.
### Preserve the existing missing-attribute fallback
An absent float attribute evaluates as zero, and the existing `1 - o_normal_reduction` expression therefore evaluates to one. No material-node change is required to preserve ordinary image-material strength.
Alternative: add an existence switch in the material. It would replicate behavior that the current expression already provides.
### Apply the reduction in both normal spaces
`O Image Layer` already supplies the same `Normal Scale × (1 - o_normal_reduction)` strength field to both Tangent and Object Normal Map nodes. Relief Plane will use that established shared behavior, so both normal spaces follow its displayed geometry depth.
Alternative: split the material paths and limit the factor to Tangent Space. This would introduce an inconsistent interpretation of the existing shared attribute and require a separate Object Space policy.
## Risks / Trade-offs
- [Relief geometry writes the reduction on an inappropriate domain] → Store the field where the material evaluates it and add rendered POINT/FACE-domain coverage.
- [Missing attributes disable unrelated normal maps] → Test the explicit missing-attribute fallback on both Tangent and Object paths.
- [A later Tangent Depth Scale group omits the contract] → Treat `o_normal_reduction = 1 - Depth Scale` as the required output attribute whenever a Tangent-normal geometry group exposes `Depth Scale`.
- [Object Space visual response differs from Tangent Space] → Render and compare both paths at zero, fractional, unit, and extrapolated scales.
## Migration Plan
1. Update relevant Tangent-normal geometry builders to write `1 - Depth Scale` as `o_normal_reduction`.
2. Extend rendered node tests with Relief Plane depth-scale coverage.
3. Run focused node tests, then `uv run node-group build` to regenerate and verify `src/anyimage/assets/O_AnyImage.blend`.
4. Roll back by restoring the previous asset and source together.

View File

@@ -0,0 +1,26 @@
## Why
Tangent-space normal maps describe the full-depth surface. When a geometry node reduces its `Depth Scale`, its shape becomes flatter but the material normal remains at full strength, making surface detail appear too strong.
## What Changes
- Reuse the existing `o_normal_reduction` geometry attribute as the material-to-geometry contract for tangent-space normal strength.
- Make tangent-normal depth geometry write `1 - Depth Scale` to that attribute, so the material normal strength follows the displayed geometric depth.
- Extend the existing `O Image Layer` behavior without changing its attribute lookup or fallback semantics.
- Make Object Space normal strength follow the same geometry reduction contract.
- Reuse the existing `o_normal_reduction` contract for additional Tangent-normal depth geometry.
## Capabilities
### New Capabilities
- `depth-scaled-tangent-normals`: Keep tangent-space material normal strength consistent with geometry depth scaling.
### Modified Capabilities
None.
## Impact
- Tangent-normal geometry-group builders, especially Relief Plane.
- Normal-scale node tests; saved node assets must be rebuilt with `uv run node-group build`.
- No server inference format, user-facing modifier socket, or dependency changes.

View File

@@ -0,0 +1,27 @@
## ADDED Requirements
### Requirement: Normal strength follows geometry depth scale
When a Geometry Nodes group both exposes `Depth Scale` and is rendered with a normal map, it SHALL store `1 - Depth Scale` as the `o_normal_reduction` float attribute on its final geometry. `O Image Layer` SHALL calculate Tangent and Object Normal Map Strength as its `Normal Scale` input multiplied by `1 - o_normal_reduction`.
#### Scenario: Reduced relief depth reduces Tangent normal strength
- **WHEN** an O Image Relief Plane uses a Tangent normal map and `Depth Scale` is set to 0.5
- **THEN** the Tangent Normal Map Strength is one half of the value produced at `Depth Scale` 1 with the same `Normal Scale` input
#### Scenario: Zero relief depth disables Tangent normal detail
- **WHEN** an O Image Relief Plane uses a Tangent normal map and `Depth Scale` is set to 0
- **THEN** the Tangent Normal Map Strength is zero
#### Scenario: Depth extrapolation is preserved
- **WHEN** an eligible Tangent-normal geometry group uses `Depth Scale` greater than 1
- **THEN** `o_normal_reduction` is negative by the corresponding amount and the Tangent Normal Map Strength retains the extrapolated value without clamping
### Requirement: Reduction attribute retains its material fallback
`O Image Layer` SHALL use a factor of 1 for its Tangent Normal Map Strength when `o_normal_reduction` is absent. The existing `o_normal_reduction` material contract SHALL remain unchanged.
#### Scenario: Ordinary image material has no normal-scale attribute
- **WHEN** an object using O Image Layer has no `o_normal_reduction` attribute
- **THEN** its Tangent Normal Map Strength equals the `Normal Scale` input
#### Scenario: Object Space normal strength follows the reduction
- **WHEN** O Image Layer is configured for an Object Space normal map and its geometry has any `o_normal_reduction` value
- **THEN** the Object Space Normal Map Strength equals the `Normal Scale` input multiplied by `1 - o_normal_reduction`

View File

@@ -0,0 +1,11 @@
## 1. Reuse the geometry-to-material contract
- [x] 1.1 Update O Image Relief Plane to store `1 - Depth Scale` as `o_normal_reduction` on final geometry.
- [x] 1.2 Confirm O Image Layer's existing `1 - o_normal_reduction` Tangent and Object-strength paths and its absent-attribute fallback need no code change.
- [x] 1.3 Confirm existing O Image Cutout and O Image Depth Cutout attribute uses retain their current geometry and material behavior.
## 2. Verification and assets
- [x] 2.1 Update definition and rendered node tests for absent, zero, fractional, and greater-than-one `o_normal_reduction` values in both normal spaces.
- [x] 2.2 Run the relevant node tests and fix regressions.
- [x] 2.3 Run `uv run node-group build` and verify the regenerated `O_AnyImage.blend` asset and inventory tests.