Add tests for foreground preservation and refactor geometry node asset

- Implement test to ensure foreground preserves source RGB and Ben2 alpha in WorkerTest.
- Remove outdated tests related to background distance and original background depth.
- Update geometry node asset to reflect new naming conventions and structure.
- Introduce validation for depth plane asset, ensuring correct input/output configurations and attribute handling.
- Adjust confidence and edge parameters in geometry node asset to support a wider range of values.
This commit is contained in:
2026-08-11 23:25:51 +08:00
parent 07d836d9ad
commit 7cd5681a8b
15 changed files with 1296 additions and 660 deletions

View File

@@ -42,15 +42,15 @@ DA3、BEN2 源码包和 uv 已内置在扩展里,安装时直接从本地解
### 转换为透视网格
选中单张图片的 Image Empty,右键打开 `SnapForm` 子菜单并选择 `Convert to Mesh`。SnapForm 会按 DA3
选中单张图片的 Image Empty,右键打开 `SnapForm` 子菜单并选择 `Convert to Surface`。SnapForm 会按 DA3
相机内参与深度逐像素反投影,并把生成网格匹配到原 Empty 的可见尺寸、Offset 与 Transform。DA3
confidence、边拉伸与 BEN2 背景结果会保存为 Mesh attributes,再由一个可复用的 Geometry Nodes
修改器完成剔面、快速重拓扑、Relax 与 UV 重投。匹配相机默认不创建;开启后会作为 Mesh 的子级。
confidence、边拉伸与 BEN2 背景结果会保存为 Surface attributes,再由一个可复用的 Geometry Nodes
修改器完成剔面、快速重拓扑、Relax 与 UV 重投。匹配相机默认不创建;开启后会作为 Surface 的子级。
当前此功能仅支持单张图片。
转换对话框只保留模型设置、`Create Matching Camera`、`Unshaded` 与 `Delete Original`。剔除和重拓扑参数直接在
Geometry Nodes 修改器中调整;`Unshaded` 可使用与 Depth Plane 相同的 Emission 材质。普通材质的
Bump Distance 会按透视网格的深度空间缩放。Mesh attributes 统一使用 `o_` 前缀;Host 准备过程显示
Bump Scale 会在材质组内乘以 `0.001`,并按透视表面的深度空间缩放。Surface attributes 统一使用 `o_` 前缀;Host 准备过程显示
`Post-processing n/6`,可看到当前阶段与总步骤。
### 视频与序列帧

View File

@@ -30,17 +30,16 @@
- Debug 与 Convert to Depth Plane 的模型选项直接显示下载状态;未下载模型带
`(Missing)`,已下载模型保持原名
- 分别调用 DA3 的 `glb`、`gs_ply`、`depth_vis` 和 `mini_npz` exporter
- `Textured Mesh (GLB)` 直接使用 DA3 Prediction 中的深度、置信度和相机参数,把相邻像素
- `Textured Surface (GLB)` 直接使用 DA3 Prediction 中的深度、置信度和相机参数,把相邻像素
组成三角面并写入 `mesh.glb`;每个输入帧生成一张独立纹理网格,不做多视图融合
- Image Empty 右键菜单可直接运行 DA3,根据 `mini_npz` 深度生成面片
- Image Empty 右键菜单可直接运行 BEN2,保留原图并换用新建的透明背景图片
- Image Empty 支持视频与序列帧输入:`Convert to Depth Plane` 逐帧估计深度,
`Remove Background` 逐帧抠图,入口保持不变
- 视频生成的序列帧以外部文件引用保留在 job 目录(Blender 无法整体 Pack 序列帧);
Displace 修改器与材质 Image Texture 均可按场景帧播放序列帧
- 使用浮点深度贴图、Simple Subdivision、Displace Modifier 和原图 Bump
构建立体面片
- 转换对话框可选择推理模式、处理分辨率和面片网格细分
Geometry Nodes 与材质 Image Texture 均可按场景帧播放序列帧
- 使用浮点深度贴图、`O DA3 Depth Plane` Geometry Nodes 和原图 Bump 构建立体面片
- 转换对话框选择推理模式和处理分辨率;细分、背景与置换方式在 Geometry Nodes 修改器中调整
- Debug 的 DA3 输出或 BEN2 背景剔除完成后只打开对应 job 文件夹,不修改当前场景
- 完整保留每次任务生成的所有官方文件
- 安装和模型下载使用后台子进程;推理提交给常驻 AI Host,状态栏实时显示阶段、百分比进度条与取消按钮
@@ -111,27 +110,20 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
`Host > Models` 下载所选模型,并固定使用 `process_res_method="upper_bound_resize"`;设备和参考视图策略
继续使用场景设置。Model 下拉菜单只列出已经下载的 DA3 模型;若没有可用模型,对话框会引导到
`Host > Models` 下载。
- `Original`:直接把原图作为深度贴图,不显示估算参数。单张图片不启动 Runtime;视频或
序列帧会启动轻量抽帧任务,把帧抽出来作为 `.depth` 序列(比直接引用视频稳定)。
- `Original`:直接把原图作为深度贴图,不显示估算参数。单张图片运行 BEN2 取得前景 Alpha;视频或
序列帧同时抽取原始帧与前景序列,把原始帧作为 `.depth` 序列。
`Original` 是默认深度来源。选择 `AI Generated` 时,其 Model、Use Ray Pose 与 Resolution Scale
参数会集中显示在独立边框中。两种模式都有默认折叠的 Options,内部依次为 Displace via、
Subdivision、Background Depth Distance 和 Unshaded;Delete Original 保持在折叠项外。
Background Depth Distance 默认 `0`,表示不剔除背景。大于 `0` 时需要 `BEN2 Base`,并使用固定值
处理 alpha 背景。AI Generated 的 DA3 仍完全使用原图估算深度,推理后先把全部 depth 转换为
`1 - DA3 depth`,再只把全透明区域设为 `-Background Depth Distance`;平面尺寸仍使用转换前的深度参考值,
同时把比背景更远的前景值钳制到背景平面,并强制最外侧 1 像素为背景距离,以减少边缘墙面和毛刺;
因此背景赋值不会缩放前景。Original 则把原图前景转为灰度深度,并把全透明区域设为该距离的负值,
生成浮点 `.depth` 图片或 EXR 序列。两种模式都会使用 `.foreground` 作为材质颜色,不修改源
Image Empty 的 Image datablock,也不根据每帧前景深度动态调整背景值。
参数会集中显示在独立边框中。两种模式都有默认折叠的 Options,内部只保留 `Unshaded`;
`Delete Original` 保持在折叠项外。转换固定需要 `BEN2 Base`,只把 `.foreground` 的 Alpha 作为独立
Mask,不在 Host 中改写深度值。保存 `.foreground` 时保留 BEN2 Alpha,但 RGB 使用未经修改的原始输入;
因此材质直接用 `.foreground` 同时提供颜色和 Mask,不再额外加载原图 Image Texture。
Image Empty 引用视频或序列帧时,AI Generated 会按 Empty 的 Image Frames 数值逐帧估计
深度(未设置时为完整帧数,显式设为 1 时只处理 1 帧),起始帧跟随 Empty 的 Offset,与背景蒙版范围一致;
Offset 超出视频长度时同样把窗口收进视频内。材质节点与置换贴图的 Start 保持与原 Empty
一致;Original 模式在 Distance 为 `0` 时,单帧复制原图作为 `.depth`,视频或序列帧则使用
`frame.0001.jpg` 等原图序列。Distance 大于 `0` 时改为生成包含固定负背景值的浮点 `.depth` 或
`depth.0001.exr` 序列。材质颜色在启用固定背景距离时使用 `foreground.0001.png` 序列。
一致;Original 模式的视频或序列帧使用 `frame.0001.jpg` 等原图序列作为颜色与深度,
`foreground.0001.png` 只提供前景 Alpha。
AI Generated 的深度仍以 EXR 序列帧(`depth.0001.exr` 起)保存在 job 目录根下。
Displace via 可选 `Modifier`(默认)或 `Material`;
Unshaded 默认关闭;Subdivision 默认为 `5`,并默认删除原对象。
`Unshaded` 默认关闭,并默认删除原对象。
3. AI Generated 完成后会自动创建面片。面片几何先由 `mini_npz` 的相机内参与深度确定,再通过对象
Transform 匹配原 Image Empty 的可见尺寸、位置、旋转和 Offset。Original 模式直接使用源 Empty
的可见尺寸、Offset 与 Transform。AI Generated 会把用于匹配画面尺寸的缩放预乘到 `.depth` 数值,
@@ -139,23 +131,48 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
开启 `Delete Original` 时,成功生成后
删除源 Empty。生成面片 Object 使用源 Empty 的原名;关闭 `Delete Original` 时不处理
同名冲突,由 Blender 自动为生成面片追加编号。
4. 短边分段数为 `2^Subdivision`,长边分段数根据图像宽高比取最接近的整数。例如高:宽为
`2:3`、Subdivision 为 `1` 时,直接生成 `2×3` 个面;Subdivision 为 `3` 时生成
`8×12` 个面。随后的 Simple Subdivision 修改器使用原生名称,Viewport Levels 固定为
`1`,Render Levels 固定为 `2`,不再随 Resolution Scale 或 Subdivision 动态变化。
4. Blender 只创建保留图像纵横比的最低密度基础网格,再把 `O DA3 Depth Plane` 作为 Geometry Nodes
修改器添加到对象。节点组与 `O Refine DA3 Surface` 一同保存在 `O_SnapForm.blend`,关闭
`show_group_selector`,不为每个对象复制节点组。修改器公开以下参数:
- `Subdivision`:默认 `5`。节点读取基础平面的 Bounding Box,短边分段数为 `2^Subdivision`,
长边按宽高比计算并四舍五入;例如 16:9 图片在等级 `5` 时生成 `57×32` 个面;
- `Remove Background`:默认关闭的 Panel Toggle。关闭时折叠隐藏 `Background Depth`,开启后才显示;
它不删除几何,而是通过点域的
`o_remove_background` Attribute 控制材质 Alpha 是否乘以前景 Mask;
- `Background Depth`:默认 `0`,允许正负值。只有开启 `Remove Background` 时,前景 Mask 外的
结构或材质置换才使用该值;关闭时所有像素继续使用原始深度;
- `Displacement`:菜单包含 `Geometry`(默认)与 `Material`。`Geometry` 在节点组内沿法线修改网格,
`Material` 保持网格平面并启用材质置换。
- `Depth Scale`:默认 `1`,允许正负值,Structure Type 为 `Single`,同时控制 Geometry 与 Material
两条置换路径。
Grid 重建后通过隐藏的 Material 输入重新赋予原图材质,并对所有面执行 Shade Smooth。
材质使用显式指向 `UVMap` 的 UV Map 节点连接颜色、前景 Mask 与深度 Image Texture,不依赖活动 UV。
开启背景移除时,前景深度先与 `Background Depth` 取 Maximum,再按前景 Alpha 与背景深度混合,
深度分支在混合前使用 `Alpha > 0.5` 转为二值 Mask,避免半透明前景被背景深度拉扯;
材质表面 Alpha 仍保留柔和边缘。材质置换使用相同的二值和钳制顺序。
节点组还会把 `o_material_displacement` 与 `o_background_depth` 写入点域,材质通过
`Geometry` 类型的 Attribute 节点读取。点域会为顶点保存重复值,但能在应用修改器后继续保留参数;
相比自适应网格本身,其占用很小。Depth Image 与 Foreground Image 是修改器隐藏数据输入,
由导入流程自动设置。
视频与序列帧还会为 Depth 与 Foreground 共同写入隐藏的 `Frame Start`、`Frames` 与 `Cyclic`。
导入内容已经是截取后的帧,因此 Offset 在节点组内恒定为 `0`,不再作为输入。节点组使用 Scene Time
计算从 `1` 开始的 Image Texture Frame:场景帧与 Start 都为 `1` 时读取图片第 `1` 帧;非循环输入
钳制到有效范围;循环输入使用 Floored Modulo,让 `Frame Start` 前后的负、正相对帧都映射到
`1…Frames`,因此开始帧之前也会循环播放。Geometry 置换会像材质 Image Texture 一样随场景帧刷新。
## Image Empty 转透视网格
1. 在 3D View 选中仅包含一帧的 Image Empty,右键打开 `SnapForm` 子菜单并选择
`Convert to Mesh`。该入口独立于
`Convert to Surface`。该入口独立于
`Convert to Depth Plane`,当前不处理视频或序列帧。
2. Host 请求 DA3 的 `depth`、`conf` 与 `intrinsics`,同时固定运行 BEN2 取得前景 alpha。Depth 不做
smoothing,也不在 Host 中剔面或减面;Host 只反投影完整有效拓扑,并计算 confidence percentile、
camera-space edge stretch 与 background 标记。
3. Blender 把勾选 `Usage > Modifier` 的 `O Refine DA3 Mesh` 节点组直接作为 Geometry Nodes 修改器
添加到 Mesh,不另设修改器名称。节点组内没有子节点组,依次完成:
3. Blender 把勾选 `Usage > Modifier` 的 `O Refine DA3 Surface` 节点组直接作为 Geometry Nodes 修改器
添加到 Surface,不另设修改器名称。节点组内没有子节点组,依次完成:
- 修改器顶层依次显示 `Resolution`、`Relax Iterations`、`Smooth`;默认展开的 `Culling` Panel
包含 `Confidence`、`Stretch`、`Background`。`Stretch = 100%` 明确选择全部面,剔面后同时删除
包含 `Confidence`、`Stretch`、`Background`。两个百分比输入都使用 `0–100` 的节点值,并在组内
转换为 `0–1` 阈值;`Stretch = 100%` 明确选择全部面,剔面后同时删除
Face Count 为零的孤立顶点;
- 每轮 Remesh Iteration 依次执行 Connected Merge、循环拓扑修复与一次 Smooth,使新拓扑在进入下一轮
Merge 前先稳定;拓扑修复删除非流形面、单面拓扑和小面积 mesh islands;
@@ -166,7 +183,7 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
`Smooth` 程度系数;节点组不再额外执行 Flip Faces;
- 从修改前的原始表面以 nearest-surface 重新采样 `UVMap`,最终明确写为 Corner 域 `2D Vector`。
4. 节点组随插件放在 `O_SnapForm.blend`。Blender 4.5 首次 Append 后复用同一 datablock;
Blender 5.0 以 linked ID 载入后调用 Pack,后续 Mesh 继续复用。插件不会为每个 Mesh 新建节点组,
Blender 5.0 以 linked ID 载入后调用 Pack,后续 Surface 继续复用。插件不会为每个 Surface 新建节点组,
也不会在添加后重命名 Geometry Nodes 修改器。节点组只启用 `Usage > Modifier`,不 Mark as Asset;
修改器关闭 `show_group_selector`。`Fix Topology` 位于默认折叠的 `Options` Panel。
节点图按 Culling、Remesh、Topology Fix、Island Cleanup 与 UV 分区摆放;各区就近使用独立的
@@ -175,8 +192,8 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
拓扑、投影 UV/attributes、写出网格。状态栏显示 `Post-processing n/6: ...`。
6. Host 在 job 根目录写出 `mesh.npz` 与 `texture.png`。NPZ 保存 camera-space `vertices`、
`faces`、`uv`、`intrinsics`、`image_size` 与有效深度中位数 `reference_depth`。Blender 读取这些
数据后直接创建 Mesh 与原图材质,不需要 Open3D 或 OpenCV 进入 Blender Python 环境。
Mesh 保留下列结构化 attributes:
数据后直接创建 Surface 与原图材质,不需要 Open3D 或 OpenCV 进入 Blender Python 环境。
Surface 保留下列结构化 attributes:
- 点域:`o_depth`、`o_confidence`、`o_confidence_percentile`、`o_background`;
- 面域:`o_face_confidence`、`o_edge_stretch`、`o_background_face`。
7. 网格以 `reference_depth` 截面匹配 Image Empty 的显示矩形:该截面的投影四角与 Empty 的可见
@@ -185,12 +202,13 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
观察相机,启用 Backface Culling 时可从正面看到。
8. 转换对话框不再提供后处理参数,只保留模型设置、`Create Matching Camera`、`Unshaded` 与
`Delete Original`。`Unshaded` 使用与 Depth Plane 相同的 Emission/Transparent 材质路径;普通材质的
Bump Distance 按 `abs(object.scale.z) × camera scale_z` 缩放,与透视网格实际深度尺度一致。
相机默认关闭;开启后会在 Empty 局部 `+Z` 侧创建一台朝向局部 `-Z` 的透视相机,并成为生成 Mesh
Bump Scale 按 `abs(object.scale.z) × camera scale_z` 设置,并在材质组内乘以固定基准 `0.001`,
与透视表面实际深度尺度一致;它允许负数以反转凹凸方向。
相机默认关闭;开启后会在 Empty 局部 `+Z` 侧创建一台朝向局部 `-Z` 的透视相机,并成为生成 Surface
的子级,使二者移动时保持投影关系。开启 `Delete Original` 时,网格成功创建后删除源 Empty。
网格、材质与相机会归入同一个 `Convert to Mesh` Undo 步骤。
表面、材质与相机会归入同一个 `Convert to Surface` Undo 步骤。
Debug 的 Textured Mesh GLB 会把 depth、confidence 与 confidence percentile 写入自定义顶点
Debug 的 Textured Surface GLB 会把 depth、confidence 与 confidence percentile 写入自定义顶点
attributes。其他 DA3 Debug 三维输出会同时写出未压缩的 `prediction.attributes.npz`,保存完整
depth、confidence、intrinsics、extrinsics 与 confidence percentile。当前 trimesh 的 glTF 点图元
导出器只写 Position/Color,无法可靠把任意 attribute 嵌入点云 GLB,因此点云先以 companion NPZ
@@ -224,16 +242,19 @@ Height = sqrt(Area / Aspect)
`snapform_camera_plane_height`,便于检查实际采用的
参考深度和尺寸。
选择 `Modifier` 时,AI Generated 的 Displace Modifier 深度贴图使用 `Extension = Extend`,
`Strength` 固定为 `1`、`Midlevel` 固定为 `0`。Original 把原图指定给 Displace Texture,并设置
`Texture Coordinates = UV`、`Midlevel = 0`;其余修改器设置保留 Blender 默认值。
视频与序列帧生成的深度贴图是 SEQUENCE Image;Displace 修改器的 Texture 与材质的 Image Texture
节点都支持序列帧,通过 image_user 的 Frames / Offset / Start 设置把第 i 帧映射到场景
`Start+i` 帧(Start 沿用 Empty 的 Start),并启用 Auto Refresh,播放时随场景帧逐帧更新。
选择 `Material` 时,不创建 Displace Modifier;深度 Image Texture 的 Color 连接到 Displacement
节点的 Height,再连接 Material Output 的 Displacement,并把材质 Surface Displacement 设置为
`Displacement Only`。AI Generated 与 Original 都使用 `Midlevel = 0`、`Scale = 1`,与对应修改器值一致。
无论选择哪种呈现方式,原始彩色贴图都会连接
材质始终导入深度 Image Texture,并连接到 Displacement 节点和 Material Output;材质 Surface
Displacement 设置为 `Displacement Only`,AI Generated 与 Original 都使用 `Midlevel = 0`、`Scale = 1`。
`o_material_displacement` 为 `False` 时,材质把 Height 乘以零,置换只在几何节点中生效;为 `True` 时,
几何节点关闭结构置换,材质 Height 恢复。开启 `Remove Background` 时,前景 Alpha 同时用于选择
原始深度或有符号的 `Background Depth`,使两种置换路径保持一致;关闭时背景深度不参与计算。
视频与序列帧的颜色、深度与前景 Image Texture
通过 image_user 的 Frames / Offset / Start 把第 i 帧映射到场景 `Start+i` 帧,并启用 Auto Refresh。
颜色贴图到 Principled BSDF 之间使用独立的 Shader Node Group。Surface 使用 `O DA3 Surface`;
Depth Plane 使用显示名为 `O DA3 Depth Plane` 的材质组,并把背景剔除、背景深度与材质置换 Attribute
读取全部收进组内。两者都依次接收 `Color`、`Alpha`、Float 类型的 `Depth`、`Bump Scale`,输出 `Color`、
`Roughness`、`Alpha`、`Normal`、`Displacement`。Surface 把原图 Color 同时送入 `Color` 与 `Depth`,
保持图像 Bump 效果。外层材质只保留贴图、材质组、Principled BSDF 与 Material Output,方便继续调整。
无论选择哪种置换方式,原始彩色贴图都会连接
Principled BSDF 的 `Base Color`,并通过
`Bump.Height → Bump.Normal → Principled.Normal` 提供表面细节。Bump 的基础 Distance 为
`0.001`,会乘以面片 Transform 的统一缩放倍率。原图 Color 还会经过 `Float Curve` 连接到
@@ -241,21 +262,18 @@ Principled BSDF 的 `Roughness`,曲线两端输出分别为 `0.5` 和 `0.1`;
Principled BSDF 默认值 `1.5`。
开启 `Unshaded` 后,原图 Color 连接 Emission,原图 Alpha 作为 Mix Shader 系数,在
Transparent BSDF 与 Emission 之间混合后连接 Material Output;不创建 Principled BSDF、Bump 或
Float Curve。若 Displace via 选择 `Material`,深度贴图与 Displacement
节点仍会保留并连接 Material Output 的 Displacement。
Float Curve。`Remove Background` 开启时,原图 Alpha 会再乘以前景 Mask Alpha;关闭时仍完整显示原图。
这里的“原始距离”是 DA3 模型输出的尺度。普通 Small、Base、Large 和 Giant 模型的深度不应
被解释为现实世界的米;需要现实尺度时应选择带 metric depth 的
`DA3 Nested Giant Large 1.1`。此外,当前结果仍是固定 XY 平面上的深度位移面,不是使用
相机内参逐像素反投影得到的完整 camera-space 网格;中位深度截面为现有 Displace Modifier
工作流提供正确的整体高宽尺度。
相机内参逐像素反投影得到的完整 camera-space 网格;中位深度截面为 Geometry Nodes
深度面片提供正确的整体高宽尺度。
生成完成后会直接创建面片,不提供缓存重建入口或单独的导入参数。重新选择
`Convert to Depth Plane` 会按当前 Depth from 再次转换。
面片、深度 Image、Texture 与 Material 会在同一个 Blender Undo 步骤中创建;转换完成后执行
面片、深度 Image、前景 Image、Material 与 Geometry Nodes 修改器会在同一个 Blender Undo 步骤中创建;转换完成后执行
`Ctrl+Z` 会完整撤销本次转换,不会越过转换去回退更早的 Image Empty 操作。
Background Depth Distance 大于 `0` 时,`.foreground` Image、Original 浮点深度、Pack 和深度面片生成也属于
同一个 Undo 步骤。
Image Empty 使用 Packed Image 时,SnapForm 会通过 Blender 写出临时 PNG 交给 DA3,
不会 Unpack 或修改源 Image;任务结束后自动删除临时文件。普通图片仍直接读取源文件。DA3 的原始
@@ -290,7 +308,6 @@ Blender 为 Packed Image 或序列帧生成的输入副本也归档到对应 job
- `remove-background`
- `remove-image-background`
- `generate-original-depth-plane`
- `extract-original-frames`
DA3 官方 exporter 产生的 `gs_ply/`、`depth_vis/`、`exports/mini_npz/` 会在完成后自动平铺,
不会保留只含一层产物的子目录:
@@ -336,10 +353,10 @@ CC BY-NC 4.0,仅限非商业用途。
- Gaussian Splats (PLY):DA3 `gs_ply`,同时传入 `infer_gs=True`
- Depth Preview (Images):DA3 `depth_vis`
- Depth Data (NPZ):DA3 `mini_npz`
- Textured Mesh (GLB):SnapForm 根据 DA3 Prediction 构建并输出 `mesh.glb`
- Textured Surface (GLB):SnapForm 根据 DA3 Prediction 构建并输出 `mesh.glb`
Point Cloud 专属设置仅在对应类型下显示,`gs_views_interval` 仅用于 Gaussian Splats。
Textured Mesh 使用 0–100% 的 Confidence Threshold 删除置信度最低的一定比例:
Textured Surface 使用 0–100% 的 Confidence Threshold 删除置信度最低的一定比例:
- `process_res=504`
- `process_res_method="upper_bound_resize"`

Binary file not shown.

View File

@@ -13,6 +13,14 @@ REFERENCE_DEPTH_KEY = "snapform_reference_depth"
ORIGINAL_DISPLACEMENT_STRENGTH = 1.0
ORIGINAL_DISPLACEMENT_MIDLEVEL = 0.0
BUMP_BASE_DISTANCE = 0.001
DEPTH_PLANE_NODE_GROUP_NAME = "O DA3 Depth Plane"
NODE_ASSET_NAME = "O_SnapForm.blend"
REMOVE_BACKGROUND_ATTRIBUTE = "o_remove_background"
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
SURFACE_MATERIAL_NODE_GROUP_NAME = "O DA3 Surface"
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME = "O DA3 Depth Plane Material"
IMAGE_NAME_SUFFIXES = (
".jpeg",
".jpg",
@@ -430,6 +438,7 @@ def create_removed_background_image(
str(foreground_paths[0]),
check_existing=False,
)
result_image.alpha_mode = "CHANNEL_PACKED"
force_sequence_source(result_image)
except (OSError, RuntimeError) as error:
if "result_image" in locals():
@@ -471,6 +480,7 @@ def create_removed_background_image(
str(output_path),
check_existing=False,
)
result_image.alpha_mode = "CHANNEL_PACKED"
except RuntimeError as error:
raise RuntimeError("Unable to load the background result") from error
source_image = source_object.data
@@ -794,10 +804,13 @@ def create_plane_mesh(source_object, bounds=None, subdivision_level=5):
x_min, x_max, y_min, y_max = (
bounds if bounds is not None else plane_bounds(source_object)
)
x_segments, y_segments = grid_segments(
(x_min, x_max, y_min, y_max),
subdivision_level,
)
if subdivision_level is None:
x_segments, y_segments = 1, 1
else:
x_segments, y_segments = grid_segments(
(x_min, x_max, y_min, y_max),
subdivision_level,
)
vertices = []
uv_coordinates = []
for y_index in range(y_segments + 1):
@@ -836,11 +849,171 @@ def create_plane_mesh(source_object, bounds=None, subdivision_level=5):
return mesh
def material_node_group(depth_plane=False):
group_name = (
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME
if depth_plane
else SURFACE_MATERIAL_NODE_GROUP_NAME
)
existing = bpy.data.node_groups.get(group_name)
if existing is not None:
return existing
group = bpy.data.node_groups.new(group_name, "ShaderNodeTree")
interface = group.interface
for name, socket_type in (
("Color", "NodeSocketColor"),
("Alpha", "NodeSocketFloat"),
("Depth", "NodeSocketFloat"),
("Bump Scale", "NodeSocketFloat"),
):
socket = interface.new_socket(
name=name,
in_out="INPUT",
socket_type=socket_type,
)
if name == "Alpha":
socket.default_value = 1.0
socket.min_value = 0.0
socket.max_value = 1.0
socket.subtype = "FACTOR"
elif name == "Bump Scale":
socket.default_value = 1.0
socket.min_value = -10000.0
socket.max_value = 10000.0
for name, socket_type in (
("Color", "NodeSocketColor"),
("Roughness", "NodeSocketFloat"),
("Alpha", "NodeSocketFloat"),
("Normal", "NodeSocketVector"),
("Displacement", "NodeSocketVector"),
):
interface.new_socket(
name=name,
in_out="OUTPUT",
socket_type=socket_type,
)
nodes = group.nodes
links = group.links
group_input = nodes.new("NodeGroupInput")
group_input.location = (-500.0, 100.0)
group_output = nodes.new("NodeGroupOutput")
group_output.location = (300.0, 100.0)
roughness_curve = nodes.new("ShaderNodeFloatCurve")
roughness_curve.mapping.initialize()
curve_points = roughness_curve.mapping.curves[0].points
curve_points[0].location = (0.0, 0.5)
curve_points[-1].location = (1.0, 0.1)
roughness_curve.mapping.update()
roughness_curve.location = (-250.0, 350.0)
bump = nodes.new("ShaderNodeBump")
bump.location = (-250.0, 0.0)
displacement = nodes.new("ShaderNodeDisplacement")
displacement.inputs["Midlevel"].default_value = 0.0
displacement.inputs["Scale"].default_value = 1.0
displacement.location = (-250.0, -300.0)
processed_alpha = group_input.outputs["Alpha"]
processed_depth = group_input.outputs["Depth"]
if depth_plane:
group_input.location = (-1200.0, 100.0)
group_output.location = (1050.0, 100.0)
roughness_curve.location = (-300.0, 450.0)
bump.location = (750.0, 50.0)
displacement.location = (750.0, -300.0)
remove_attribute = nodes.new("ShaderNodeAttribute")
remove_attribute.attribute_type = "GEOMETRY"
remove_attribute.attribute_name = REMOVE_BACKGROUND_ATTRIBUTE
remove_attribute.location = (-950.0, 0.0)
background_attribute = nodes.new("ShaderNodeAttribute")
background_attribute.attribute_type = "GEOMETRY"
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
background_attribute.location = (-950.0, -500.0)
material_attribute = nodes.new("ShaderNodeAttribute")
material_attribute.attribute_type = "GEOMETRY"
material_attribute.attribute_name = MATERIAL_DISPLACEMENT_ATTRIBUTE
material_attribute.location = (-50.0, -650.0)
strength_attribute = nodes.new("ShaderNodeAttribute")
strength_attribute.attribute_type = "GEOMETRY"
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
strength_attribute.location = (200.0, -650.0)
alpha_mix = nodes.new("ShaderNodeMix")
alpha_mix.data_type = "FLOAT"
alpha_mix.inputs[2].default_value = 1.0
alpha_mix.location = (-650.0, 50.0)
foreground_mask = nodes.new("ShaderNodeMath")
foreground_mask.operation = "GREATER_THAN"
foreground_mask.inputs[1].default_value = 0.5
foreground_mask.location = (-950.0, -250.0)
clamp_foreground = nodes.new("ShaderNodeMath")
clamp_foreground.operation = "MAXIMUM"
clamp_foreground.location = (-650.0, -400.0)
masked_depth = nodes.new("ShaderNodeMix")
masked_depth.data_type = "FLOAT"
masked_depth.location = (-350.0, -350.0)
background_mix = nodes.new("ShaderNodeMix")
background_mix.data_type = "FLOAT"
background_mix.location = (-50.0, -350.0)
enabled_depth = nodes.new("ShaderNodeMath")
enabled_depth.operation = "MULTIPLY"
enabled_depth.location = (250.0, -350.0)
scaled_depth = nodes.new("ShaderNodeMath")
scaled_depth.operation = "MULTIPLY"
scaled_depth.location = (450.0, -350.0)
links.new(remove_attribute.outputs["Fac"], alpha_mix.inputs[0])
links.new(group_input.outputs["Alpha"], alpha_mix.inputs[3])
links.new(group_input.outputs["Alpha"], foreground_mask.inputs[0])
links.new(group_input.outputs["Depth"], clamp_foreground.inputs[0])
links.new(background_attribute.outputs["Fac"], clamp_foreground.inputs[1])
links.new(foreground_mask.outputs[0], masked_depth.inputs[0])
links.new(background_attribute.outputs["Fac"], masked_depth.inputs[2])
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
links.new(remove_attribute.outputs["Fac"], background_mix.inputs[0])
links.new(group_input.outputs["Depth"], background_mix.inputs[2])
links.new(masked_depth.outputs["Result"], background_mix.inputs[3])
links.new(background_mix.outputs["Result"], enabled_depth.inputs[0])
links.new(material_attribute.outputs["Fac"], enabled_depth.inputs[1])
links.new(enabled_depth.outputs[0], scaled_depth.inputs[0])
links.new(strength_attribute.outputs["Fac"], scaled_depth.inputs[1])
processed_alpha = alpha_mix.outputs["Result"]
processed_depth = scaled_depth.outputs[0]
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
links.new(group_input.outputs["Color"], roughness_curve.inputs["Value"])
links.new(
roughness_curve.outputs["Value"],
group_output.inputs["Roughness"],
)
links.new(processed_alpha, group_output.inputs["Alpha"])
links.new(processed_depth, bump.inputs["Height"])
bump_distance = nodes.new("ShaderNodeMath")
bump_distance.operation = "MULTIPLY"
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
bump_distance.location = (500.0, 100.0) if depth_plane else (-250.0, 150.0)
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
links.new(processed_depth, displacement.inputs["Height"])
links.new(
displacement.outputs["Displacement"],
group_output.inputs["Displacement"],
)
group.color_tag = "SHADER"
group.description = (
"Prepare DA3 depth-plane material outputs."
if depth_plane
else "Prepare DA3 surface material outputs."
)
group.use_fake_user = True
return group
def create_material(
name,
color_image,
depth_scale,
displacement_image=None,
foreground_image=None,
displacement_midlevel=0.0,
displacement_scale=1.0,
unshaded=False,
@@ -848,6 +1021,8 @@ def create_material(
frame_start=1,
color_frame_offset=0,
color_frame_duration=None,
foreground_frame_offset=0,
foreground_frame_duration=None,
depth_frame_offset=0,
depth_frame_duration=None,
use_cyclic=None,
@@ -860,13 +1035,17 @@ def create_material(
nodes.clear()
color_texture = nodes.new("ShaderNodeTexImage")
uv_map = nodes.new("ShaderNodeUVMap")
output = nodes.new("ShaderNodeOutputMaterial")
color_texture.image = color_image
uv_map.uv_map = "UVMap"
uv_map.location = (-1250.0, 350.0)
material_image = foreground_image or color_image
color_texture.image = material_image
color_texture.interpolation = "Linear"
color_texture.extension = "EXTEND"
color_texture.location = (-640.0, 180.0)
output.location = (460.0, 180.0)
color_texture.location = (-1000.0, 350.0)
output.location = (850.0, 350.0)
extracted_frames = frame_count if frame_count > 1 else None
color_frames = (
color_frame_duration
@@ -878,29 +1057,51 @@ def create_material(
if depth_frame_duration is not None
else extracted_frames
)
if frame_count > 1 or is_animated_image(color_image):
material_frames = color_frames
material_frame_offset = color_frame_offset
if foreground_image is not None:
material_frames = (
foreground_frame_duration
if foreground_frame_duration is not None
else extracted_frames
)
material_frame_offset = foreground_frame_offset
if frame_count > 1 or is_animated_image(material_image):
configure_image_user(
color_texture.image_user,
color_frames,
material_frames,
frame_start=frame_start,
frame_offset=color_frame_offset,
frame_offset=material_frame_offset,
use_cyclic=use_cyclic,
)
links = material.node_tree.links
links.new(uv_map.outputs["UV"], color_texture.inputs["Vector"])
alpha = color_texture.outputs["Alpha"]
material_inputs = nodes.new("ShaderNodeGroup")
is_depth_plane = displacement_image is not None
material_inputs.node_tree = material_node_group(is_depth_plane)
material_inputs.name = (
"O DA3 Depth Plane" if is_depth_plane else "O DA3 Surface"
)
material_inputs.label = material_inputs.name
material_inputs.location = (-300.0, 350.0)
material_inputs.inputs["Bump Scale"].default_value = depth_scale
links.new(color_texture.outputs["Color"], material_inputs.inputs["Color"])
links.new(alpha, material_inputs.inputs["Alpha"])
if unshaded:
emission = nodes.new("ShaderNodeEmission")
transparent = nodes.new("ShaderNodeBsdfTransparent")
mix_shader = nodes.new("ShaderNodeMixShader")
emission.location = (-100.0, 260.0)
transparent.location = (-100.0, 20.0)
mix_shader.location = (200.0, 180.0)
emission.location = (0.0, 550.0)
transparent.location = (0.0, 150.0)
mix_shader.location = (300.0, 350.0)
links.new(
color_texture.outputs["Color"],
material_inputs.outputs["Color"],
emission.inputs["Color"],
)
links.new(
color_texture.outputs["Alpha"],
material_inputs.outputs["Alpha"],
mix_shader.inputs["Fac"],
)
links.new(
@@ -917,39 +1118,22 @@ def create_material(
)
else:
shader = nodes.new("ShaderNodeBsdfPrincipled")
bump = nodes.new("ShaderNodeBump")
roughness_curve = nodes.new("ShaderNodeFloatCurve")
bump.inputs["Distance"].default_value = BUMP_BASE_DISTANCE * depth_scale
roughness_curve.mapping.initialize()
curve_points = roughness_curve.mapping.curves[0].points
curve_points[0].location = (0.0, 0.5)
curve_points[-1].location = (1.0, 0.1)
roughness_curve.mapping.update()
roughness_curve.location = (-300.0, 320.0)
bump.location = (-300.0, -80.0)
shader.location = (80.0, 180.0)
shader.location = (250.0, 400.0)
links.new(
color_texture.outputs["Color"],
material_inputs.outputs["Color"],
shader.inputs["Base Color"],
)
links.new(color_texture.outputs["Alpha"], shader.inputs["Alpha"])
links.new(color_texture.outputs["Color"], bump.inputs["Height"])
links.new(bump.outputs["Normal"], shader.inputs["Normal"])
links.new(material_inputs.outputs["Alpha"], shader.inputs["Alpha"])
links.new(material_inputs.outputs["Normal"], shader.inputs["Normal"])
links.new(
color_texture.outputs["Color"],
roughness_curve.inputs["Value"],
)
links.new(
roughness_curve.outputs["Value"],
material_inputs.outputs["Roughness"],
shader.inputs["Roughness"],
)
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
if displacement_image is not None:
depth_texture = nodes.new("ShaderNodeTexImage")
displacement = nodes.new("ShaderNodeDisplacement")
depth_texture.image = displacement_image
depth_texture.interpolation = "Linear"
depth_texture.extension = "EXTEND"
@@ -961,21 +1145,30 @@ def create_material(
frame_offset=depth_frame_offset,
use_cyclic=use_cyclic,
)
displacement.inputs["Midlevel"].default_value = (
displacement_midlevel
)
displacement.inputs["Scale"].default_value = displacement_scale
depth_texture.location = (-300.0, -360.0)
displacement.location = (80.0, -260.0)
depth_texture.location = (-1000.0, -400.0)
links.new(uv_map.outputs["UV"], depth_texture.inputs["Vector"])
displacement_height = depth_texture.outputs["Color"]
if displacement_midlevel != 0.0 or displacement_scale != 1.0:
depth_adjust = nodes.new("ShaderNodeMath")
depth_adjust.operation = "SUBTRACT"
depth_adjust.inputs[1].default_value = displacement_midlevel
depth_adjust.location = (500.0, -550.0)
depth_scale_node = nodes.new("ShaderNodeMath")
depth_scale_node.operation = "MULTIPLY"
depth_scale_node.inputs[1].default_value = displacement_scale
depth_scale_node.location = (700.0, -550.0)
links.new(displacement_height, depth_adjust.inputs[0])
links.new(depth_adjust.outputs[0], depth_scale_node.inputs[0])
links.new(depth_scale_node.outputs[0], material_inputs.inputs["Depth"])
else:
links.new(displacement_height, material_inputs.inputs["Depth"])
links.new(
depth_texture.outputs["Color"],
displacement.inputs["Height"],
)
links.new(
displacement.outputs["Displacement"],
material_inputs.outputs["Displacement"],
output.inputs["Displacement"],
)
set_displacement_only(material)
else:
links.new(color_texture.outputs["Color"], material_inputs.inputs["Depth"])
return material
@@ -990,6 +1183,84 @@ def set_displacement_only(material):
return
def node_asset_path():
return Path(__file__).with_name("assets") / NODE_ASSET_NAME
def load_depth_plane_node_group():
existing = bpy.data.node_groups.get(DEPTH_PLANE_NODE_GROUP_NAME)
if existing is not None:
return existing
asset_path = node_asset_path()
if not asset_path.is_file():
raise RuntimeError(f"Missing Geometry Nodes asset: {asset_path}")
use_pack = bpy.app.version >= (5, 0, 0)
if not use_pack:
directory = str(asset_path) + "/NodeTree/"
result = bpy.ops.wm.append(
filepath=directory + DEPTH_PLANE_NODE_GROUP_NAME,
directory=directory,
filename=DEPTH_PLANE_NODE_GROUP_NAME,
link=False,
do_reuse_local_id=True,
)
if "FINISHED" not in result:
raise RuntimeError("Unable to append the Geometry Nodes asset")
node_group = bpy.data.node_groups.get(DEPTH_PLANE_NODE_GROUP_NAME)
if node_group is None:
raise RuntimeError("Appended Geometry Nodes asset is unavailable")
node_group.use_fake_user = True
return node_group
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
if DEPTH_PLANE_NODE_GROUP_NAME not in source.node_groups:
raise RuntimeError(
"Geometry Nodes asset is missing "
f"{DEPTH_PLANE_NODE_GROUP_NAME}"
)
target.node_groups = [DEPTH_PLANE_NODE_GROUP_NAME]
linked_group = target.node_groups[0]
node_group = bpy.data.pack_linked_ids_hierarchy(root_id=linked_group)
if linked_group != node_group and linked_group.users == 0:
bpy.data.node_groups.remove(linked_group)
node_group.use_fake_user = True
return node_group
def modifier_input_identifier(node_group, name):
for item in node_group.interface.items_tree:
if item.item_type != "SOCKET" or item.in_out != "INPUT":
continue
if item.name == name:
return item.identifier
raise RuntimeError(f"Geometry Nodes input is missing: {name}")
def add_depth_plane_processing(
plane_object,
depth_image,
foreground_image,
material,
frame_start=1,
frame_duration=1,
use_cyclic=False,
):
node_group = load_depth_plane_node_group()
modifier = plane_object.modifiers.new(name=node_group.name, type="NODES")
modifier.node_group = node_group
modifier.show_group_selector = False
for name, value in (
("Depth Image", depth_image),
("Foreground Image", foreground_image),
("Material", material),
("Frame Start", frame_start),
("Frames", max(int(frame_duration or 1), 1)),
("Cyclic", use_cyclic),
):
identifier = modifier_input_identifier(node_group, name)
modifier[identifier] = value
return modifier
def finish_depth_plane(
context,
source_object,
@@ -1010,11 +1281,10 @@ def finish_depth_plane(
def build_image_depth_plane(
context,
source_object,
subdivision_level,
delete_source,
displacement_via="MODIFIER",
unshaded=False,
color_image=None,
foreground_image=None,
depth_frames_directory=None,
depth_image=None,
):
@@ -1046,48 +1316,21 @@ def build_image_depth_plane(
depth_source_image = frames_image
elif not is_animated_image(source_object.data):
depth_source_image = copy_depth_image(source_object)
mesh = create_plane_mesh(
source_object,
subdivision_level=subdivision_level,
foreground_image = (
foreground_image or color_image or source_object.data
)
mesh = create_plane_mesh(source_object, subdivision_level=None)
plane_object = bpy.data.objects.new(source_name, mesh)
context.collection.objects.link(plane_object)
plane_object.matrix_world = source_object.matrix_world.copy()
subdivision = plane_object.modifiers.new("Subdivision", "SUBSURF")
subdivision.subdivision_type = "SIMPLE"
subdivision.levels = 1
subdivision.render_levels = 2
if displacement_via == "MODIFIER":
texture = bpy.data.textures.new(
depth_data_name(source_object),
type="IMAGE",
)
texture.image = depth_source_image
texture.extension = "EXTEND"
if uses_extracted or is_animated_image(source_object.data):
configure_image_user(
texture.image_user,
texture_frame_count,
frame_start=source_frame_start,
frame_offset=texture_frame_offset,
use_cyclic=source_use_cyclic,
)
displace = plane_object.modifiers.new("Displace", "DISPLACE")
displace.texture = texture
displace.texture_coords = "UV"
displace.mid_level = ORIGINAL_DISPLACEMENT_MIDLEVEL
depth_scale = abs(float(plane_object.scale.z))
displacement_image = (
depth_source_image if displacement_via == "MATERIAL" else None
)
material = create_material(
source_name,
color_image if color_image is not None else source_object.data,
depth_scale,
displacement_image=displacement_image,
displacement_image=depth_source_image,
foreground_image=foreground_image,
displacement_midlevel=ORIGINAL_DISPLACEMENT_MIDLEVEL,
displacement_scale=ORIGINAL_DISPLACEMENT_STRENGTH,
unshaded=unshaded,
@@ -1095,11 +1338,22 @@ def build_image_depth_plane(
frame_start=source_frame_start,
color_frame_offset=texture_frame_offset,
color_frame_duration=texture_frame_count,
foreground_frame_offset=0,
foreground_frame_duration=frame_count,
depth_frame_offset=texture_frame_offset,
depth_frame_duration=texture_frame_count,
use_cyclic=source_use_cyclic,
)
mesh.materials.append(material)
add_depth_plane_processing(
plane_object,
depth_source_image,
foreground_image,
material,
frame_start=source_frame_start,
frame_duration=frame_count,
use_cyclic=source_use_cyclic,
)
plane_object["snapform_frame_count"] = frame_count
return finish_depth_plane(
context,
@@ -1113,12 +1367,11 @@ def build_depth_plane(
context,
source_object,
npz_path,
subdivision_level,
delete_source,
displacement_via="MODIFIER",
unshaded=False,
depth_name="",
color_image=None,
foreground_image=None,
depth_frames_directory=None,
):
source_name = source_object.name
@@ -1136,13 +1389,16 @@ def build_depth_plane(
)
depth_name = depth_name or depth_data_name(source_object)
frame_count = len(depth_arrays)
foreground_image = (
foreground_image or color_image or source_object.data
)
(
source_frame_start,
source_frame_offset,
source_frame_duration,
source_use_cyclic,
) = image_user_settings(source_object)
mesh = create_plane_mesh(source_object, bounds, subdivision_level)
mesh = create_plane_mesh(source_object, bounds, None)
plane_object = bpy.data.objects.new(
source_name,
mesh,
@@ -1170,39 +1426,12 @@ def build_depth_plane(
depth_frames_directory,
)
subdivision = plane_object.modifiers.new("Subdivision", "SUBSURF")
subdivision.subdivision_type = "SIMPLE"
subdivision.levels = 1
subdivision.render_levels = 2
if displacement_via == "MODIFIER":
texture = bpy.data.textures.new(
depth_name,
type="IMAGE",
)
texture.image = depth_image
texture.extension = "EXTEND"
if frame_count > 1:
configure_image_user(
texture.image_user,
frame_count,
frame_start=source_frame_start,
use_cyclic=source_use_cyclic,
)
displace = plane_object.modifiers.new("Displace", "DISPLACE")
displace.texture = texture
displace.texture_coords = "UV"
displace.direction = "NORMAL"
displace.strength = DISPLACEMENT_STRENGTH
displace.mid_level = DISPLACEMENT_MIDLEVEL
material = create_material(
source_name,
color_image if color_image is not None else source_object.data,
depth_scale,
displacement_image=(
depth_image if displacement_via == "MATERIAL" else None
),
displacement_image=depth_image,
foreground_image=foreground_image,
displacement_midlevel=DISPLACEMENT_MIDLEVEL,
displacement_scale=DISPLACEMENT_STRENGTH,
unshaded=unshaded,
@@ -1212,11 +1441,22 @@ def build_depth_plane(
color_frame_duration=(
frame_count if color_image is not None else source_frame_duration
),
foreground_frame_offset=0,
foreground_frame_duration=frame_count,
depth_frame_offset=0,
depth_frame_duration=frame_count,
use_cyclic=source_use_cyclic,
)
mesh.materials.append(material)
add_depth_plane_processing(
plane_object,
depth_image,
foreground_image,
material,
frame_start=source_frame_start,
frame_duration=frame_count,
use_cyclic=source_use_cyclic,
)
plane_object["snapform_reference_depth"] = reference_depth
plane_object["snapform_camera_plane_width"] = bounds[1] - bounds[0]
plane_object["snapform_camera_plane_height"] = bounds[3] - bounds[2]
@@ -1254,24 +1494,10 @@ class BuildDepthPlane(bpy.types.Operator):
snapform_depth_frames_dir: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_subdivision_level: bpy.props.IntProperty(
default=5,
min=0,
max=10,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_delete_source: bpy.props.BoolProperty(
default=True,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_displacement_via: bpy.props.EnumProperty(
items=(
("MODIFIER", "Modifier", "Use a Displace modifier"),
("MATERIAL", "Material", "Use material displacement"),
),
default="MODIFIER",
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_unshaded: bpy.props.BoolProperty(
default=False,
options={"HIDDEN", "SKIP_SAVE"},
@@ -1295,14 +1521,15 @@ class BuildDepthPlane(bpy.types.Operator):
return {"CANCELLED"}
try:
color_image = None
foreground_image = None
if self.snapform_background_path:
color_image = create_removed_background_image(
foreground_image = create_removed_background_image(
source_object,
self.snapform_background_path,
assign_to_source=False,
)
elif self.snapform_color_frames_dir:
color_image = None
if self.snapform_color_frames_dir:
color_image = create_sequence_image(
f"{image_base_name(source_object.data)}.frame",
self.snapform_color_frames_dir,
@@ -1311,12 +1538,11 @@ class BuildDepthPlane(bpy.types.Operator):
context,
source_object,
npz_path,
self.snapform_subdivision_level,
self.snapform_delete_source,
displacement_via=self.snapform_displacement_via,
unshaded=self.snapform_unshaded,
depth_name=self.snapform_depth_data_name,
color_image=color_image,
foreground_image=foreground_image,
depth_frames_directory=(
self.snapform_depth_frames_dir or None
),
@@ -1347,24 +1573,10 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
snapform_depth_npz_path: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_subdivision_level: bpy.props.IntProperty(
default=5,
min=0,
max=10,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_delete_source: bpy.props.BoolProperty(
default=True,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_displacement_via: bpy.props.EnumProperty(
items=(
("MODIFIER", "Modifier", "Use a Displace modifier"),
("MATERIAL", "Material", "Use material displacement"),
),
default="MODIFIER",
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_unshaded: bpy.props.BoolProperty(
default=False,
options={"HIDDEN", "SKIP_SAVE"},
@@ -1379,14 +1591,15 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
)
return {"CANCELLED"}
try:
color_image = None
foreground_image = None
if self.snapform_background_path:
color_image = create_removed_background_image(
foreground_image = create_removed_background_image(
source_object,
self.snapform_background_path,
assign_to_source=False,
)
elif self.snapform_color_frames_dir:
color_image = None
if self.snapform_color_frames_dir:
color_image = create_sequence_image(
f"{image_base_name(source_object.data)}.frame",
self.snapform_color_frames_dir,
@@ -1411,11 +1624,10 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
build_image_depth_plane(
context,
source_object,
self.snapform_subdivision_level,
self.snapform_delete_source,
displacement_via=self.snapform_displacement_via,
unshaded=self.snapform_unshaded,
color_image=color_image,
foreground_image=foreground_image,
depth_frames_directory=(
self.snapform_depth_frames_dir or None
),
@@ -1527,27 +1739,11 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
),
default="ORIGINAL",
)
snapform_displacement_via: bpy.props.EnumProperty(
name="Displace via",
description="How the depth displacement is presented",
items=(
("MODIFIER", "Modifier", "Use a Displace modifier"),
("MATERIAL", "Material", "Use material displacement"),
),
default="MODIFIER",
)
snapform_unshaded: bpy.props.BoolProperty(
name="Unshaded",
description="Display the source color through an Emission shader",
default=False,
)
snapform_background_depth_distance: bpy.props.FloatProperty(
name="Background Depth Distance",
description="Set excluded background to this fixed depth distance; zero disables background exclusion",
default=0.0,
min=0.0,
soft_max=10.0,
)
snapform_model: bpy.props.EnumProperty(
name="Model",
description="Model used to estimate image depth",
@@ -1572,24 +1768,19 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
description="Delete the source Image Empty after conversion",
default=True,
)
snapform_subdivision_level: bpy.props.IntProperty(
name="Subdivision",
description="Power of two for the short-edge segment count; the long edge follows the image aspect ratio",
default=5,
min=0,
max=10,
)
@classmethod
def poll(cls, context):
from .operators import active_job
from .runtime import host_busy, model_ready
from .models import MODELS
from .models import BEN2_MODEL, MODELS
return (
is_image_empty(context.object)
and active_job() is None
and not host_busy()
and any(model_ready(model_key) for model_key in MODELS)
and model_ready(BEN2_MODEL.key)
)
def invoke(self, context, _event):
@@ -1608,9 +1799,6 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self, width=340)
def draw(self, context):
from .models import BEN2_MODEL
from .runtime import model_ready
layout = self.layout
layout.prop(self, "snapform_depth_source")
if self.snapform_depth_source == "ESTIMATE":
@@ -1637,14 +1825,6 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
)
settings_header.label(text="Options")
if settings_body is not None:
settings_body.prop(self, "snapform_displacement_via")
settings_body.prop(self, "snapform_subdivision_level")
remove_background_row = settings_body.row()
remove_background_row.enabled = model_ready(BEN2_MODEL.key)
remove_background_row.prop(
self,
"snapform_background_depth_distance",
)
settings_body.prop(self, "snapform_unshaded")
layout.prop(self, "snapform_delete_original")
@@ -1652,7 +1832,6 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
from .operators import (
IMAGE_DEPTH_JOB,
ORIGINAL_BACKGROUND_JOB,
ORIGINAL_FRAMES_JOB,
)
source_object = context.object
@@ -1660,57 +1839,6 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
context.scene.snapform_settings.video_frames = (
default_extract_frames(source_object)
)
if (
self.snapform_depth_source == "ORIGINAL"
and self.snapform_background_depth_distance <= 0.0
):
if video_total_frames(source_object.data) > 1:
try:
source_path, delete_input = prepare_image_input(
source_object.data,
context.scene,
)
except RuntimeError as error:
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
result = bpy.ops.snapform.run_job(
job_type=ORIGINAL_FRAMES_JOB,
snapform_input_path=str(source_path),
snapform_source_object_name=source_object.name,
snapform_subdivision_level=self.snapform_subdivision_level,
snapform_delete_source=self.snapform_delete_original,
snapform_delete_input=delete_input,
snapform_displacement_via=self.snapform_displacement_via,
snapform_unshaded=self.snapform_unshaded,
)
if "CANCELLED" not in result:
return {"FINISHED"}
if delete_input:
remove_temporary_input(source_path)
return {"CANCELLED"}
try:
build_image_depth_plane(
context,
source_object,
self.snapform_subdivision_level,
self.snapform_delete_original,
self.snapform_displacement_via,
self.snapform_unshaded,
)
except (OSError, RuntimeError, ValueError) as error:
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
undo_result = bpy.ops.ed.undo_push(
message="Convert to Depth Plane",
)
if "FINISHED" not in undo_result:
self.report(
{"ERROR"},
"Unable to create the depth plane Undo step",
)
return {"CANCELLED"}
return {"FINISHED"}
if self.snapform_depth_source == "ORIGINAL":
try:
source_path, delete_input = prepare_image_input(
@@ -1724,14 +1852,9 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
job_type=ORIGINAL_BACKGROUND_JOB,
snapform_input_path=str(source_path),
snapform_source_object_name=source_object.name,
snapform_subdivision_level=self.snapform_subdivision_level,
snapform_delete_source=self.snapform_delete_original,
snapform_delete_input=delete_input,
snapform_displacement_via=self.snapform_displacement_via,
snapform_unshaded=self.snapform_unshaded,
snapform_background_depth_distance=(
self.snapform_background_depth_distance
),
)
if "CANCELLED" not in result:
return {"FINISHED"}
@@ -1765,14 +1888,10 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
snapform_model_key=self.snapform_model,
snapform_use_ray_pose=self.snapform_use_ray_pose,
snapform_process_res=process_res,
snapform_subdivision_level=self.snapform_subdivision_level,
snapform_delete_source=self.snapform_delete_original,
snapform_delete_input=delete_input,
snapform_displacement_via=self.snapform_displacement_via,
snapform_unshaded=self.snapform_unshaded,
snapform_background_depth_distance=(
self.snapform_background_depth_distance
),
snapform_remove_background=True,
)
if "CANCELLED" in result:
if delete_input:
@@ -1797,7 +1916,7 @@ class SnapFormImageMenu(bpy.types.Menu):
)
layout.operator(
"snapform.convert_image_to_mesh",
text="Convert to Mesh",
text="Convert to Surface",
icon="MESH_GRID",
)
layout.separator()

View File

@@ -33,7 +33,7 @@ FACE_ATTRIBUTES = {
"o_edge_stretch": ("FLOAT", "face_edge_stretch"),
"o_background_face": ("BOOLEAN", "face_background"),
}
NODE_GROUP_NAME = "O Refine DA3 Mesh"
NODE_GROUP_NAME = "O Refine DA3 Surface"
NODE_ASSET_NAME = "O_SnapForm.blend"
@@ -60,7 +60,7 @@ def load_image_mesh_data(npz_path):
missing = [key for key in required if key not in archive.files]
if missing:
raise ValueError(
f"Mesh data is missing: {', '.join(missing)}"
f"Surface data is missing: {', '.join(missing)}"
)
vertices = np.asarray(archive["vertices"], dtype=np.float32)
faces = np.asarray(archive["faces"], dtype=np.int32)
@@ -81,19 +81,19 @@ def load_image_mesh_data(npz_path):
}.items()
}
except OSError as error:
raise ValueError(f"Unable to read mesh data: {npz_path}") from error
raise ValueError(f"Unable to read surface data: {npz_path}") from error
if vertices.ndim != 2 or vertices.shape[1] != 3:
raise ValueError(f"Unsupported vertices shape: {vertices.shape}")
if faces.ndim != 2 or faces.shape[1] != 3:
raise ValueError(f"Unsupported faces shape: {faces.shape}")
if uv.shape != (len(vertices), 2):
raise ValueError("Mesh UV and vertex counts do not match")
raise ValueError("Surface UV and vertex counts do not match")
if intrinsics.shape != (3, 3):
raise ValueError(f"Unsupported intrinsics shape: {intrinsics.shape}")
if len(image_size) != 2 or min(image_size) <= 0:
raise ValueError(f"Unsupported image size: {image_size}")
if not reference_depth > 0.0:
raise ValueError("Mesh reference depth must be positive")
raise ValueError("Surface reference depth must be positive")
for _name, (_data_type, key) in POINT_ATTRIBUTES.items():
if len(attributes[key]) != len(vertices):
raise ValueError(f"Point attribute has the wrong length: {key}")
@@ -394,7 +394,7 @@ def build_image_mesh(
class BuildImageMesh(bpy.types.Operator):
bl_idname = "snapform.build_image_mesh"
bl_label = "Build Image Mesh"
bl_label = "Build Image Surface"
bl_options = {"INTERNAL"}
snapform_source_object_name: bpy.props.StringProperty(
@@ -438,8 +438,8 @@ class BuildImageMesh(bpy.types.Operator):
class ConvertImageToMesh(bpy.types.Operator):
bl_idname = "snapform.convert_image_to_mesh"
bl_label = "Convert to Mesh"
bl_description = "Convert an Image Empty into a perspective textured mesh"
bl_label = "Convert to Surface"
bl_description = "Convert an Image Empty into a perspective textured surface"
bl_options = {"REGISTER"}
snapform_model: bpy.props.EnumProperty(
@@ -529,7 +529,7 @@ class ConvertImageToMesh(bpy.types.Operator):
source_object = context.object
if video_total_frames(source_object.data) != 1:
self.report({"ERROR"}, "Convert to Mesh currently supports still images")
self.report({"ERROR"}, "Convert to Surface currently supports still images")
return {"CANCELLED"}
try:
process_res = scaled_process_resolution(

View File

@@ -73,12 +73,9 @@ class ActiveJob:
source_object_name: str = ""
depth_data_name: str = ""
process_res: int = IMAGE_DEPTH_DEFAULT_PROCESS_RES
subdivision_level: int = 5
delete_source: bool = True
displacement_via: str = "MODIFIER"
unshaded: bool = False
remove_background: bool = False
background_depth_distance: float = 0.0
mesh_create_camera: bool = False
temporary_input_path: Path | None = None
cancelled: bool = False
@@ -141,7 +138,7 @@ class RunJob(bpy.types.Operator):
("DOWNLOAD_MODEL", "Download Model", ""),
(RECONSTRUCTION_JOB, "Export DA3 Result", ""),
(IMAGE_DEPTH_JOB, "Convert Image to Depth Plane", ""),
(IMAGE_MESH_JOB, "Convert Image to Mesh", ""),
(IMAGE_MESH_JOB, "Convert Image to Surface", ""),
(BACKGROUND_REMOVAL_JOB, "Test BEN2 Background Removal", ""),
(IMAGE_BACKGROUND_JOB, "Remove Image Empty Background", ""),
(ORIGINAL_BACKGROUND_JOB, "Build Original Background Plane", ""),
@@ -183,29 +180,10 @@ class RunJob(bpy.types.Operator):
default=False,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_subdivision_level: bpy.props.IntProperty(
default=5,
min=0,
max=10,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_displacement_via: bpy.props.EnumProperty(
items=(
("MODIFIER", "Modifier", ""),
("MATERIAL", "Material", ""),
),
default="MODIFIER",
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_unshaded: bpy.props.BoolProperty(
default=False,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_background_depth_distance: bpy.props.FloatProperty(
default=0.0,
min=0.0,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_mesh_create_camera: bpy.props.BoolProperty(
default=False,
options={"HIDDEN", "SKIP_SAVE"},
@@ -214,6 +192,7 @@ class RunJob(bpy.types.Operator):
default=False,
options={"HIDDEN", "SKIP_SAVE"},
)
def execute(self, context):
global _active_job
@@ -251,15 +230,11 @@ class RunJob(bpy.types.Operator):
source_object_name=self.snapform_source_object_name,
depth_data_name=self.snapform_depth_data_name,
process_res=self.snapform_process_res,
subdivision_level=self.snapform_subdivision_level,
delete_source=self.snapform_delete_source,
displacement_via=self.snapform_displacement_via,
unshaded=self.snapform_unshaded,
remove_background=(
self.snapform_background_depth_distance > 0.0
or bool(getattr(self, "snapform_remove_background", False))
remove_background=bool(
getattr(self, "snapform_remove_background", False)
),
background_depth_distance=self.snapform_background_depth_distance,
mesh_create_camera=getattr(
self,
"snapform_mesh_create_camera",
@@ -350,10 +325,9 @@ class RunJob(bpy.types.Operator):
)
self.snapform_input_path = str(archived_input)
self.snapform_delete_input = False
background_depth_distance = float(
getattr(self, "snapform_background_depth_distance", 0.0)
remove_background = bool(
getattr(self, "snapform_remove_background", False)
)
remove_background = background_depth_distance > 0.0
if self.job_type == IMAGE_MESH_JOB:
remove_background = True
if self.job_type == "INSTALL_RUNTIME":
@@ -413,11 +387,6 @@ class RunJob(bpy.types.Operator):
BEN2_MODEL.key,
start_frame=start_frame,
device=self._task_device(settings),
background_depth_distance=(
background_depth_distance
if self.job_type == ORIGINAL_BACKGROUND_JOB
else 0.0
),
),
paths,
)
@@ -483,11 +452,6 @@ class RunJob(bpy.types.Operator):
self.job_type in {IMAGE_DEPTH_JOB, IMAGE_MESH_JOB}
and remove_background
),
background_depth_distance=(
background_depth_distance
if self.job_type == IMAGE_DEPTH_JOB
else 0.0
),
device=self._task_device(settings),
),
paths,
@@ -520,7 +484,7 @@ class RunJob(bpy.types.Operator):
if self.job_type == IMAGE_DEPTH_JOB:
return "Starting depth estimation"
if self.job_type == IMAGE_MESH_JOB:
return "Starting perspective mesh generation"
return "Starting perspective surface generation"
if self.job_type == BACKGROUND_REMOVAL_JOB:
return "Starting BEN2 background removal"
if self.job_type == IMAGE_BACKGROUND_JOB:
@@ -696,19 +660,19 @@ class RunJob(bpy.types.Operator):
status["message"] = "Depth plane is ready"
if job.kind == IMAGE_MESH_JOB:
try:
settings.status = "Building perspective mesh"
settings.status = "Building perspective surface"
settings.progress = 0.96
self._redraw_ui(context, force=True)
self._build_image_mesh(job)
except (OSError, RuntimeError, ValueError) as import_error:
settings.status = "Mesh import failed"
settings.status = "Surface import failed"
settings.progress = 0.0
self.report({"ERROR"}, str(import_error))
runtime.mark_host_job_complete(job.status_path.stem)
close_active_job()
self._redraw_ui(context, force=True)
return {"CANCELLED"}
status["message"] = "Perspective mesh is ready"
status["message"] = "Perspective surface is ready"
if job.kind in {ORIGINAL_BACKGROUND_JOB, ORIGINAL_FRAMES_JOB}:
try:
settings.status = "Building depth plane"
@@ -761,10 +725,10 @@ class RunJob(bpy.types.Operator):
snapform_unshaded=job.unshaded,
)
if "FINISHED" not in result:
raise RuntimeError("Unable to build the perspective mesh")
undo_result = bpy.ops.ed.undo_push(message="Convert to Mesh")
raise RuntimeError("Unable to build the perspective surface")
undo_result = bpy.ops.ed.undo_push(message="Convert to Surface")
if "FINISHED" not in undo_result:
raise RuntimeError("Unable to create the mesh Undo step")
raise RuntimeError("Unable to create the surface Undo step")
def _build_depth_plane(self, context, job):
result_directory = job.result_directory
@@ -784,18 +748,14 @@ class RunJob(bpy.types.Operator):
snapform_depth_data_name=job.depth_data_name,
snapform_background_path=background_path,
snapform_color_frames_dir=(
str(result_directory)
if is_sequence and not foreground_paths
else ""
str(result_directory) if is_sequence else ""
),
snapform_depth_frames_dir=(
str(result_directory)
if is_sequence
else ""
),
snapform_subdivision_level=job.subdivision_level,
snapform_delete_source=job.delete_source,
snapform_displacement_via=job.displacement_via,
snapform_unshaded=job.unshaded,
)
if "FINISHED" not in result:
@@ -815,7 +775,7 @@ class RunJob(bpy.types.Operator):
if is_sequence
else result_directory / "foreground.png"
)
elif is_sequence:
if is_sequence:
color_frames_dir = str(result_directory)
result = bpy.ops.snapform.build_original_depth_plane(
snapform_source_object_name=job.source_object_name,
@@ -824,14 +784,8 @@ class RunJob(bpy.types.Operator):
snapform_depth_frames_dir=(
str(result_directory) if is_sequence else ""
),
snapform_depth_npz_path=(
str(result_directory / "original-depth.npz")
if job.kind == ORIGINAL_BACKGROUND_JOB
else ""
),
snapform_subdivision_level=job.subdivision_level,
snapform_depth_npz_path="",
snapform_delete_source=job.delete_source,
snapform_displacement_via=job.displacement_via,
snapform_unshaded=job.unshaded,
)
if "FINISHED" not in result:

View File

@@ -216,7 +216,7 @@ class Da3OutputDebugPanel(bpy.types.Panel):
ply_box.prop(settings, "gs_views_interval")
elif settings.export_format == "mesh":
mesh_box = layout.box()
mesh_box.label(text="Mesh Settings")
mesh_box.label(text="Surface Settings")
mesh_box.prop(settings, "conf_thresh_percentile")
mesh_box.prop(settings, "mesh_filter_edges")
mesh_box.prop(settings, "mesh_max_edge_length")

View File

@@ -67,7 +67,7 @@ class SnapFormSettings(bpy.types.PropertyGroup):
)
conf_thresh_percentile: bpy.props.FloatProperty(
name="Confidence Threshold",
description="Remove the lowest-confidence percentile of points or mesh faces",
description="Remove the lowest-confidence percentile of points or surface faces",
default=40.0,
min=0.0,
max=100.0,
@@ -135,8 +135,8 @@ class SnapFormSettings(bpy.types.PropertyGroup):
),
(
"mesh",
"Textured Mesh (GLB)",
"Generate one filtered textured depth mesh per input image",
"Textured Surface (GLB)",
"Generate one filtered textured depth surface per input image",
),
),
default="glb",

View File

@@ -748,7 +748,6 @@ def remove_background_request(
video_frames=None,
start_frame=1,
device=None,
background_depth_distance=0.0,
):
return HostJobRequest(
{
@@ -766,9 +765,6 @@ def remove_background_request(
else video_frames
),
"start_frame": start_frame,
"background_depth_distance": float(
background_depth_distance
),
},
}
)
@@ -812,7 +808,6 @@ def reconstruct_request(
process_res_method=None,
use_ray_pose=None,
remove_background=False,
background_depth_distance=0.0,
mesh_filter_edges=None,
start_frame=1,
device=None,
@@ -851,7 +846,6 @@ def reconstruct_request(
"use_ray_pose": 1 if snapform_selected_ray_pose else 0,
"ref_view_strategy": settings.ref_view_strategy,
"remove_background_model_dir": "",
"background_depth_distance": float(background_depth_distance),
"mesh_filter_edges": bool(
getattr(settings, "mesh_filter_edges", True)
if mesh_filter_edges is None
@@ -861,7 +855,7 @@ def reconstruct_request(
getattr(settings, "mesh_max_edge_length", 0.1)
),
}
if remove_background or background_depth_distance > 0.0:
if remove_background:
parameters["remove_background_model_dir"] = str(
model_directory(BEN2_MODEL.key)
)

View File

@@ -48,64 +48,6 @@ def check_cancelled(cancel_check):
raise TaskCancelled("Task cancelled")
def apply_foreground_depth_distance(
prediction,
foreground_paths,
background_depth_distance,
):
"""Set fully transparent pixels to a fixed negative displacement."""
import numpy as np
from PIL import Image
if len(foreground_paths) != len(prediction.depth):
raise RuntimeError("Foreground mask count does not match depth frames")
masks = []
height, width = prediction.depth.shape[-2:]
for foreground_path in foreground_paths:
with Image.open(foreground_path) as source:
alpha = source.convert("RGBA").getchannel("A")
alpha = alpha.resize((width, height), resample=Image.Resampling.NEAREST)
masks.append(np.asarray(alpha) > 0)
apply_depth_background_array(
prediction,
np.stack(masks),
background_depth_distance,
)
def apply_depth_background_array(
prediction,
foreground_mask,
background_depth_distance,
):
import numpy as np
depth = np.asarray(prediction.depth)
masked_depth = depth.copy()
background_depth = -background_depth_distance
for frame_index in range(len(masked_depth)):
np.maximum(
masked_depth[frame_index],
background_depth,
out=masked_depth[frame_index],
)
masked_depth[frame_index][~foreground_mask[frame_index]] = (
background_depth
)
set_depth_border(masked_depth[frame_index], background_depth)
prediction.depth = masked_depth
def set_depth_border(depth, background_depth):
"""Set the outermost pixel ring to the fixed background depth."""
if depth.ndim != 2 or not depth.size:
raise ValueError("Depth frame must be a non-empty 2D array")
depth[0, :] = background_depth
depth[-1, :] = background_depth
depth[:, 0] = background_depth
depth[:, -1] = background_depth
def convert_ai_depth_for_displacement(prediction):
"""Convert DA3 distance into Blender's positive displacement direction."""
import numpy as np
@@ -137,55 +79,6 @@ def write_npz_reference_depth(npz_path, reference_depth):
temporary_path.replace(npz_path)
def write_original_background_depth(
image_paths,
foreground_paths,
background_depth_distance,
output_directory,
):
"""Write original-image luminance with a fixed negative background depth."""
import numpy as np
from PIL import Image
if len(image_paths) != len(foreground_paths):
raise RuntimeError("Foreground mask count does not match source frames")
depth_frames = []
for image_path, foreground_path in zip(image_paths, foreground_paths):
with Image.open(image_path) as source:
luminance = np.asarray(source.convert("L"), dtype=np.float32) / 255.0
with Image.open(foreground_path) as foreground:
alpha = np.asarray(
foreground.convert("RGBA").getchannel("A"),
dtype=np.uint8,
)
if luminance.shape != alpha.shape:
raise RuntimeError("Foreground mask size does not match source frame")
depth_frames.append(
compose_original_background_depth(
luminance,
alpha,
background_depth_distance,
)
)
np.savez_compressed(
Path(output_directory) / "original-depth.npz",
depth=np.stack(depth_frames),
)
def compose_original_background_depth(
luminance,
alpha,
background_depth_distance,
):
import numpy as np
background_depth = -background_depth_distance
depth = np.where(alpha > 0, luminance, background_depth)
set_depth_border(depth, background_depth)
return depth
def write_status(
path,
progress,
@@ -608,7 +501,7 @@ def export_image_mesh_data(
depth_frames = np.asarray(prediction.depth, dtype=np.float32)
confidence_frames = np.asarray(prediction.conf, dtype=np.float32)
if depth_frames.shape[0] != 1:
raise ValueError("Convert to Mesh currently supports one image")
raise ValueError("Convert to Surface currently supports one image")
raw_depth = depth_frames[0]
confidence = confidence_frames[0]
intrinsics = np.asarray(prediction.intrinsics[0], dtype=np.float32)
@@ -636,7 +529,7 @@ def export_image_mesh_data(
)
if not len(faces):
raise ValueError(
"DA3 output does not contain any valid mesh faces"
"DA3 output does not contain any valid surface faces"
)
progress(5, "Projecting UV and structured attributes")
vertices, faces = compact_mesh(
@@ -658,7 +551,7 @@ def export_image_mesh_data(
reference_depth = float(np.median(valid_depth))
output_directory = Path(output_directory)
output_directory.mkdir(parents=True, exist_ok=True)
progress(6, "Writing mesh data")
progress(6, "Writing surface data")
result_path = output_directory / "mesh.npz"
np.savez_compressed(
result_path,
@@ -876,7 +769,7 @@ def export_textured_mesh_glb(
referenced_points.append(mesh.vertices)
if not meshes:
raise ValueError(
"Mesh filters removed every face; lower Confidence Threshold or disable filters"
"Surface filters removed every face; lower Confidence Threshold or disable filters"
)
world_to_camera = np.asarray(prediction.extrinsics[0], dtype=np.float64)
@@ -1343,6 +1236,7 @@ def infer_foreground(
image,
refine_foreground=False,
)
foreground = preserve_source_rgb(image, foreground)
if device.type == "cuda":
torch.cuda.synchronize()
inference_ms = (time.perf_counter() - inference_started) * 1000.0
@@ -1354,6 +1248,18 @@ def infer_foreground(
return foreground, load_ms, inference_ms
def preserve_source_rgb(source, foreground):
"""Combine the untouched source RGB with BEN2's foreground alpha."""
source_rgba = source.convert("RGBA")
alpha = foreground.convert("RGBA").getchannel("A")
if alpha.size != source_rgba.size:
from PIL import Image
alpha = alpha.resize(source_rgba.size, Image.Resampling.LANCZOS)
source_rgba.putalpha(alpha)
return source_rgba
def infer_foreground_frames(
images,
model_dir,
@@ -1456,21 +1362,6 @@ def remove_background(args, model_cache=None, cancel_check=None):
cancel_check=cancel_check,
)
copy_frames(images, output_directory)
background_depth_distance = float(
getattr(args, "background_depth_distance", 0.0)
)
if background_depth_distance > 0.0:
foreground_paths = (
[output_directory / "foreground.png"]
if len(images) == 1
else sorted(output_directory.glob("foreground.*.png"))
)
write_original_background_depth(
images,
foreground_paths,
background_depth_distance,
output_directory,
)
finally:
if temporary_directory is not None:
shutil.rmtree(temporary_directory, ignore_errors=True)
@@ -1662,9 +1553,6 @@ def reconstruct(args, model_cache=None, cancel_check=None):
"remove_background_model_dir",
"",
)
background_depth_distance = float(
getattr(args, "background_depth_distance", 0.0)
)
foreground_images = []
if remove_background_model_dir:
write_status(args.status, 0.12, "Removing image background")
@@ -1747,12 +1635,6 @@ def reconstruct(args, model_cache=None, cancel_check=None):
if args.export_format == "mini_npz":
reference_depth = depth_reference_value(prediction.depth[0])
convert_ai_depth_for_displacement(prediction)
if background_depth_distance > 0.0:
apply_foreground_depth_distance(
prediction,
foreground_images,
background_depth_distance,
)
write_status(args.status, 0.88, f"Exporting {args.export_format}")
if args.export_format == "mesh":
export_textured_mesh_glb(

View File

@@ -231,9 +231,30 @@ class AddonTest(unittest.TestCase):
self.assertTrue(all(name.startswith("o_") for name in names))
self.assertTrue(image_mesh.node_asset_path().is_file())
self.assertEqual(image_mesh.NODE_GROUP_NAME, "O Refine DA3 Mesh")
self.assertEqual(image_mesh.NODE_GROUP_NAME, "O Refine DA3 Surface")
self.assertEqual(image_mesh.NODE_ASSET_NAME, "O_SnapForm.blend")
def test_depth_plane_uses_bundled_nodes_and_material_attributes(self):
image_depth = self.addon.image_depth
self.assertTrue(image_depth.node_asset_path().is_file())
self.assertEqual(
image_depth.DEPTH_PLANE_NODE_GROUP_NAME,
"O DA3 Depth Plane",
)
self.assertEqual(
image_depth.REMOVE_BACKGROUND_ATTRIBUTE,
"o_remove_background",
)
self.assertEqual(
image_depth.MATERIAL_DISPLACEMENT_ATTRIBUTE,
"o_material_displacement",
)
self.assertEqual(
image_depth.BACKGROUND_DEPTH_ATTRIBUTE,
"o_background_depth",
)
def test_image_mesh_material_depth_scale_matches_fitted_geometry(self):
mesh_object = SimpleNamespace(scale=SimpleNamespace(z=-2.0))
@@ -315,6 +336,7 @@ class AddonTest(unittest.TestCase):
(-0.25, -0.75),
)
self.assertEqual(result_image.name, "img.foreground")
self.assertEqual(result_image.alpha_mode, "CHANNEL_PACKED")
self.assertEqual(pack_calls, [True])
self.assertEqual(remove_calls, [])
self.assertEqual(
@@ -365,6 +387,7 @@ class AddonTest(unittest.TestCase):
self.assertIs(source_object.data, original_image)
self.assertFalse(original_image.use_fake_user)
self.assertEqual(result_image.name, "img.foreground")
self.assertEqual(result_image.alpha_mode, "CHANNEL_PACKED")
self.assertEqual(pack_calls, [True])
def test_sequence_background_result_preserves_empty_image_start(self):
@@ -408,6 +431,7 @@ class AddonTest(unittest.TestCase):
self.assertIs(created, result_image)
self.assertIs(source_object.data, result_image)
self.assertEqual(result_image.alpha_mode, "CHANNEL_PACKED")
self.assertEqual(image_user.frame_start, 4)
self.assertEqual(image_user.frame_duration, 2)
self.assertEqual(image_user.frame_offset, 0)
@@ -456,10 +480,7 @@ class AddonTest(unittest.TestCase):
def test_original_depth_background_starts_independent_job(self):
operator = self.addon.ConvertImageToDepthPlane()
operator.snapform_depth_source = "ORIGINAL"
operator.snapform_background_depth_distance = 2.0
operator.snapform_displacement_via = "MODIFIER"
operator.snapform_unshaded = False
operator.snapform_subdivision_level = 5
operator.snapform_delete_original = False
source_object = SimpleNamespace(
name="Reference",
@@ -486,15 +507,12 @@ class AddonTest(unittest.TestCase):
self.assertEqual(result, {"FINISHED"})
self.assertEqual(calls[0]["job_type"], "ORIGINAL_BACKGROUND")
self.assertEqual(calls[0]["snapform_background_depth_distance"], 2.0)
self.assertNotIn("snapform_background_depth_distance", calls[0])
def test_original_depth_animation_extracts_frames_without_background(self):
def test_original_depth_animation_extracts_frames_and_background(self):
operator = self.addon.ConvertImageToDepthPlane()
operator.snapform_depth_source = "ORIGINAL"
operator.snapform_background_depth_distance = 0.0
operator.snapform_displacement_via = "MODIFIER"
operator.snapform_unshaded = False
operator.snapform_subdivision_level = 5
operator.snapform_delete_original = False
source_object = SimpleNamespace(
name="Reference",
@@ -528,7 +546,7 @@ class AddonTest(unittest.TestCase):
self.assertEqual(result, {"FINISHED"})
self.assertEqual(scene.snapform_settings.video_frames, 97)
self.assertEqual(calls[0]["job_type"], "ORIGINAL_FRAMES")
self.assertEqual(calls[0]["job_type"], "ORIGINAL_BACKGROUND")
self.assertEqual(calls[0]["snapform_input_path"], str(input_path))
self.assertEqual(calls[0]["snapform_source_object_name"], "Reference")
self.assertFalse(calls[0]["snapform_delete_input"])
@@ -552,27 +570,15 @@ class AddonTest(unittest.TestCase):
self.assertFalse(
operator.__annotations__["snapform_unshaded"]["default"]
)
background_offset = operator.__annotations__[
"snapform_background_depth_distance"
]
self.assertEqual(background_offset["default"], 0.0)
self.assertEqual(background_offset["min"], 0.0)
self.assertEqual(
operator.__annotations__["snapform_subdivision_level"]["default"],
5,
)
self.assertEqual(
set(operator.__annotations__),
{
"snapform_depth_source",
"snapform_displacement_via",
"snapform_unshaded",
"snapform_background_depth_distance",
"snapform_model",
"snapform_use_ray_pose",
"snapform_process_res_factor",
"snapform_delete_original",
"snapform_subdivision_level",
},
)
depth_sources = operator.__annotations__["snapform_depth_source"]
@@ -583,15 +589,6 @@ class AddonTest(unittest.TestCase):
)
self.assertEqual(depth_sources["items"][0][1], "AI Generated")
self.assertEqual(depth_sources["name"], "Depth from")
displacement_via = operator.__annotations__[
"snapform_displacement_via"
]
self.assertEqual(displacement_via["default"], "MODIFIER")
self.assertEqual(displacement_via["name"], "Displace via")
self.assertEqual(
[item[0] for item in displacement_via["items"]],
["MODIFIER", "MATERIAL"],
)
self.assertEqual(
operator.__annotations__["snapform_unshaded"]["name"],
"Unshaded",
@@ -674,7 +671,7 @@ class AddonTest(unittest.TestCase):
self.assertEqual(result, {"FINISHED"})
self.assertEqual(scene.snapform_settings.video_frames, 24)
def test_conversion_execute_uses_background_offset_frame_range(self):
def test_conversion_execute_uses_background_mask_frame_range(self):
source_object = SimpleNamespace(
name="Reference",
data=SimpleNamespace(
@@ -694,10 +691,7 @@ class AddonTest(unittest.TestCase):
operator.snapform_model = "BASE"
operator.snapform_use_ray_pose = True
operator.snapform_process_res_factor = 1.0
operator.snapform_displacement_via = "MODIFIER"
operator.snapform_unshaded = False
operator.snapform_background_depth_distance = 1.0
operator.snapform_subdivision_level = 5
operator.snapform_delete_original = False
calls = []
self.fake_bpy.ops = SimpleNamespace(
@@ -720,7 +714,8 @@ class AddonTest(unittest.TestCase):
self.assertEqual(result, {"FINISHED"})
self.assertEqual(scene.snapform_settings.video_frames, 24)
self.assertEqual(calls[0]["job_type"], "IMAGE_DEPTH")
self.assertEqual(calls[0]["snapform_background_depth_distance"], 1.0)
self.assertTrue(calls[0]["snapform_remove_background"])
self.assertNotIn("snapform_background_depth_distance", calls[0])
def test_conversion_dialog_only_lists_installed_models(self):
items_callback = self.addon.ConvertImageToDepthPlane.__annotations__[
@@ -823,10 +818,7 @@ class AddonTest(unittest.TestCase):
operator.snapform_model = "GIANT_1_1"
operator.snapform_use_ray_pose = True
operator.snapform_process_res_factor = 0.5
operator.snapform_displacement_via = "MATERIAL"
operator.snapform_unshaded = True
operator.snapform_background_depth_distance = 2.5
operator.snapform_subdivision_level = 5
operator.snapform_delete_original = True
image = SimpleNamespace(
name="img.png",
@@ -865,12 +857,8 @@ class AddonTest(unittest.TestCase):
self.assertEqual(calls[0]["snapform_input_path"], str(packed_path))
self.assertEqual(calls[0]["snapform_process_res"], 960)
self.assertEqual(calls[0]["snapform_model_key"], "GIANT_1_1")
self.assertEqual(
calls[0]["snapform_displacement_via"],
"MATERIAL",
)
self.assertTrue(calls[0]["snapform_remove_background"])
self.assertTrue(calls[0]["snapform_unshaded"])
self.assertEqual(calls[0]["snapform_background_depth_distance"], 2.5)
self.assertEqual(calls[0]["snapform_depth_data_name"], "img.depth")
self.assertTrue(calls[0]["snapform_delete_input"])
self.assertEqual(
@@ -1070,7 +1058,7 @@ class AddonTest(unittest.TestCase):
snapform_ray_pose,
)
def test_image_depth_background_option_requires_ben2(self):
def test_image_depth_background_mask_requires_ben2(self):
settings = SimpleNamespace(
input_path="debug-input.png",
export_format="mini_npz",
@@ -1082,7 +1070,7 @@ class AddonTest(unittest.TestCase):
operator.snapform_model_key = "BASE"
operator.snapform_use_ray_pose = True
operator.snapform_process_res = 1024
operator.snapform_background_depth_distance = 1.0
operator.snapform_remove_background = True
with TemporaryDirectory() as directory:
input_path = Path(directory) / "input.png"
@@ -1140,16 +1128,15 @@ class AddonTest(unittest.TestCase):
Path("status.json"),
Path("result"),
remove_background=True,
background_depth_distance=2.5,
)
self.assertEqual(
request.payload["parameters"]["remove_background_model_dir"],
str(Path("ben2_base")),
)
self.assertEqual(
request.payload["parameters"]["background_depth_distance"],
2.5,
self.assertNotIn(
"background_depth_distance",
request.payload["parameters"],
)
def test_host_busy_snapshot_is_released_for_completed_local_job(self):
@@ -1174,12 +1161,9 @@ class AddonTest(unittest.TestCase):
operator.snapform_source_object_name = "Reference"
operator.snapform_depth_data_name = "img.depth"
operator.snapform_process_res = 1024
operator.snapform_subdivision_level = 5
operator.snapform_delete_source = False
operator.snapform_delete_input = False
operator.snapform_displacement_via = "MODIFIER"
operator.snapform_unshaded = False
operator.snapform_background_depth_distance = 0.0
started = threading.Event()
release = threading.Event()
@@ -1368,13 +1352,12 @@ class AddonTest(unittest.TestCase):
Path("result"),
"BEN2_BASE",
start_frame=70,
background_depth_distance=3.0,
)
parameters = request.payload["parameters"]
self.assertEqual(parameters["video_frames"], 24)
self.assertEqual(parameters["start_frame"], 70)
self.assertEqual(parameters["background_depth_distance"], 3.0)
self.assertNotIn("background_depth_distance", parameters)
def test_extract_frames_request_passes_frame_range(self):
runtime = importlib.import_module("snapform.runtime")
@@ -1683,7 +1666,7 @@ class AddonTest(unittest.TestCase):
"Gaussian Splats (PLY)",
"Depth Preview (Images)",
"Depth Data (NPZ)",
"Textured Mesh (GLB)",
"Textured Surface (GLB)",
],
)
@@ -1826,7 +1809,7 @@ class AddonTest(unittest.TestCase):
):
self.assertTrue(self.addon.RemoveImageBackground.poll(context))
def test_convert_to_depth_plane_poll_requires_da3_model(self):
def test_convert_to_depth_plane_poll_requires_da3_and_ben2_models(self):
context = SimpleNamespace(
object=SimpleNamespace(
type="EMPTY",
@@ -1841,7 +1824,7 @@ class AddonTest(unittest.TestCase):
), patch.object(
self.addon.runtime,
"model_ready",
side_effect=lambda key: key == "BASE",
side_effect=lambda key: key in {"BASE", "BEN2_BASE"},
):
self.assertTrue(
self.addon.ConvertImageToDepthPlane.poll(context)
@@ -2321,7 +2304,7 @@ class AddonTest(unittest.TestCase):
self.assertFalse(calls[0][1]["snapform_delete_source"])
self.assertEqual(
calls[0][1]["snapform_depth_npz_path"],
str(Path("result") / "original-depth.npz"),
"",
)
self.assertEqual(
calls[1],

View File

@@ -30,6 +30,25 @@ class WorkerTest(unittest.TestCase):
def setUpClass(cls):
cls.worker = load_worker()
def test_foreground_preserves_source_rgb_and_ben2_alpha(self):
alpha = SimpleNamespace(size=(2, 1))
source_rgba = SimpleNamespace(size=(2, 1), applied_alpha=None)
source_rgba.putalpha = lambda value: setattr(
source_rgba,
"applied_alpha",
value,
)
source = SimpleNamespace(convert=lambda _mode: source_rgba)
foreground_rgba = SimpleNamespace(
getchannel=lambda _name: alpha,
)
foreground = SimpleNamespace(convert=lambda _mode: foreground_rgba)
result = self.worker.preserve_source_rgb(source, foreground)
self.assertIs(result, source_rgba)
self.assertIs(result.applied_alpha, alpha)
def test_collect_images_is_sorted_and_filters_suffixes(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
@@ -138,49 +157,6 @@ class WorkerTest(unittest.TestCase):
)
self.assertEqual(args.remove_background_model_dir, "ben2")
def test_background_distance_sets_fixed_background_depth(self):
prediction = SimpleNamespace(
depth=np.array(
[[
[1.0, 2.0, 3.0, 4.0],
[5.0, -9.0, -2.0, 8.0],
[9.0, -3.0, 4.0, 12.0],
[13.0, 14.0, 15.0, 16.0],
]]
),
conf=np.arange(16, dtype=np.float32).reshape(1, 4, 4),
)
foreground_mask = np.array(
[[
[True, True, True, True],
[True, True, False, True],
[True, True, True, True],
[True, True, True, True],
]],
dtype=bool,
)
original_confidence = prediction.conf.copy()
self.worker.apply_depth_background_array(
prediction,
foreground_mask,
8.0,
)
np.testing.assert_array_equal(
prediction.depth,
[[
[-8.0, -8.0, -8.0, -8.0],
[-8.0, -8.0, -8.0, -8.0],
[-8.0, -3.0, 4.0, -8.0],
[-8.0, -8.0, -8.0, -8.0],
]],
)
np.testing.assert_array_equal(
prediction.conf,
original_confidence,
)
def test_ai_depth_is_converted_for_positive_displacement(self):
prediction = SimpleNamespace(
depth=np.array([[[0.25, 1.0], [1.5, 2.0]]]),
@@ -215,26 +191,6 @@ class WorkerTest(unittest.TestCase):
1.5,
)
def test_original_background_uses_fixed_negative_depth(self):
luminance = np.full((3, 3), 0.5, dtype=np.float32)
alpha = np.full((3, 3), 255, dtype=np.uint8)
alpha[1, 0] = 0
depth = self.worker.compose_original_background_depth(
luminance,
alpha,
3.0,
)
np.testing.assert_allclose(
depth,
[
[-3.0, -3.0, -3.0],
[-3.0, 0.5, -3.0],
[-3.0, -3.0, -3.0],
],
)
def test_prepares_a_single_image_file(self):
with tempfile.TemporaryDirectory() as directory:
image_path = Path(directory) / "input.png"
@@ -646,6 +602,8 @@ class WorkerTest(unittest.TestCase):
save_options = []
class FakeImage:
size = (1, 1)
def __enter__(self):
return self
@@ -656,6 +614,12 @@ class WorkerTest(unittest.TestCase):
self.mode = mode
return self
def getchannel(self, _name):
return self
def putalpha(self, _alpha):
return None
def save(self, path, **options):
save_options.append(options)
Path(path).touch()
@@ -726,6 +690,8 @@ class WorkerTest(unittest.TestCase):
inference_options = []
class FakeImage:
size = (1, 1)
def __enter__(self):
return self
@@ -736,6 +702,12 @@ class WorkerTest(unittest.TestCase):
self.mode = mode
return self
def getchannel(self, _name):
return self
def putalpha(self, _alpha):
return None
def save(self, path, **options):
Path(path).touch()

View File

@@ -3,7 +3,12 @@ from pathlib import Path
import bpy
GROUP_NAME = "O Refine DA3 Mesh"
GROUP_NAME = "O Refine DA3 Surface"
DEPTH_PLANE_GROUP_NAME = "O DA3 Depth Plane"
REMOVE_BACKGROUND_ATTRIBUTE = "o_remove_background"
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
OUTPUT_PATH = (
Path(__file__).parents[1]
/ "src"
@@ -356,7 +361,13 @@ def build_group():
group = bpy.data.node_groups.new(GROUP_NAME, "GeometryNodeTree")
group.is_modifier = True
interface = group.interface
new_socket(interface, "Geometry", "INPUT", "NodeSocketGeometry")
geometry_input_socket = new_socket(
interface,
"Geometry",
"INPUT",
"NodeSocketGeometry",
)
geometry_input_socket.structure_type = "SINGLE"
resolution = new_socket(
interface,
@@ -400,7 +411,7 @@ def build_group():
)
confidence.default_value = 0.0
confidence.min_value = 0.0
confidence.max_value = 1.0
confidence.max_value = 100.0
confidence.subtype = "PERCENTAGE"
edge = new_socket(
interface,
@@ -409,9 +420,9 @@ def build_group():
"NodeSocketFloat",
parent=culling_panel,
)
edge.default_value = 0.1
edge.default_value = 10.0
edge.min_value = 0.0
edge.max_value = 1.0
edge.max_value = 100.0
edge.subtype = "PERCENTAGE"
background = new_socket(
interface,
@@ -459,6 +470,10 @@ def build_group():
confidence_attribute.data_type = "FLOAT"
confidence_attribute.inputs["Name"].default_value = "o_face_confidence"
confidence_attribute.location = (-1800.0, 600.0)
confidence_scale = nodes.new("ShaderNodeMath")
confidence_scale.operation = "MULTIPLY"
confidence_scale.inputs[1].default_value = 0.01
confidence_scale.location = (-1600.0, 800.0)
confidence_compare = new_compare(
nodes,
"FLOAT",
@@ -472,6 +487,10 @@ def build_group():
)
links.new(
confidence_input.outputs["Confidence"],
confidence_scale.inputs[0],
)
links.new(
confidence_scale.outputs[0],
input_socket(confidence_compare, "B", 1),
)
@@ -479,6 +498,10 @@ def build_group():
edge_attribute.data_type = "FLOAT"
edge_attribute.inputs["Name"].default_value = "o_edge_stretch"
edge_attribute.location = (-1800.0, 350.0)
edge_scale = nodes.new("ShaderNodeMath")
edge_scale.operation = "MULTIPLY"
edge_scale.inputs[1].default_value = 0.01
edge_scale.location = (-1600.0, -50.0)
edge_maximum = nodes.new("ShaderNodeMath")
edge_maximum.operation = "MAXIMUM"
edge_maximum.inputs[1].default_value = 0.0001
@@ -494,7 +517,8 @@ def build_group():
(-1200.0, 350.0),
1.0,
)
links.new(stretch_input.outputs["Stretch"], edge_maximum.inputs[0])
links.new(stretch_input.outputs["Stretch"], edge_scale.inputs[0])
links.new(edge_scale.outputs[0], edge_maximum.inputs[0])
links.new(edge_maximum.outputs[0], edge_reciprocal.inputs[1])
links.new(edge_attribute.outputs["Attribute"], input_socket(edge_compare, "A", 0))
links.new(edge_reciprocal.outputs[0], input_socket(edge_compare, "B", 1))
@@ -526,7 +550,7 @@ def build_group():
0.999999,
)
links.new(
stretch_input.outputs["Stretch"],
edge_scale.outputs[0],
input_socket(full_stretch, "A", 0),
)
culling_selection = nodes.new("FunctionNodeBooleanMath")
@@ -621,10 +645,434 @@ def build_group():
return group
def build_sequence_frame(nodes, links, scene_frame, frames_input, y):
relative_frame = nodes.new("ShaderNodeMath")
relative_frame.operation = "SUBTRACT"
relative_frame.location = (300.0, y)
links.new(scene_frame, relative_frame.inputs[0])
links.new(frames_input.outputs["Frame Start"], relative_frame.inputs[1])
safe_frames = nodes.new("ShaderNodeMath")
safe_frames.operation = "MAXIMUM"
safe_frames.inputs[1].default_value = 1.0
safe_frames.location = (300.0, y - 200.0)
links.new(frames_input.outputs["Frames"], safe_frames.inputs[0])
first_frame = nodes.new("ShaderNodeMath")
first_frame.operation = "ADD"
first_frame.inputs[1].default_value = 1.0
first_frame.location = (500.0, y)
links.new(relative_frame.outputs[0], first_frame.inputs[0])
clamp_minimum = nodes.new("ShaderNodeMath")
clamp_minimum.operation = "MAXIMUM"
clamp_minimum.inputs[1].default_value = 1.0
clamp_minimum.location = (700.0, y)
links.new(first_frame.outputs[0], clamp_minimum.inputs[0])
clamp_maximum = nodes.new("ShaderNodeMath")
clamp_maximum.operation = "MINIMUM"
clamp_maximum.location = (900.0, y)
links.new(clamp_minimum.outputs[0], clamp_maximum.inputs[0])
links.new(safe_frames.outputs[0], clamp_maximum.inputs[1])
cyclic_frame = nodes.new("ShaderNodeMath")
cyclic_frame.operation = "FLOORED_MODULO"
cyclic_frame.location = (700.0, y - 200.0)
links.new(relative_frame.outputs[0], cyclic_frame.inputs[0])
links.new(safe_frames.outputs[0], cyclic_frame.inputs[1])
cyclic_first_frame = nodes.new("ShaderNodeMath")
cyclic_first_frame.operation = "ADD"
cyclic_first_frame.inputs[1].default_value = 1.0
cyclic_first_frame.location = (900.0, y - 200.0)
links.new(cyclic_frame.outputs[0], cyclic_first_frame.inputs[0])
frame_switch = nodes.new("GeometryNodeSwitch")
frame_switch.input_type = "INT"
frame_switch.location = (1100.0, y)
links.new(frames_input.outputs["Cyclic"], frame_switch.inputs["Switch"])
links.new(clamp_maximum.outputs[0], frame_switch.inputs["False"])
links.new(cyclic_first_frame.outputs[0], frame_switch.inputs["True"])
return frame_switch.outputs["Output"]
def build_depth_plane_group():
group = bpy.data.node_groups.new(DEPTH_PLANE_GROUP_NAME, "GeometryNodeTree")
group.is_modifier = True
interface = group.interface
geometry_input_socket = new_socket(
interface,
"Geometry",
"INPUT",
"NodeSocketGeometry",
)
geometry_input_socket.structure_type = "SINGLE"
displacement = new_socket(
interface,
"Displacement",
"INPUT",
"NodeSocketMenu",
)
displacement_strength = new_socket(
interface,
"Depth Scale",
"INPUT",
"NodeSocketFloat",
)
displacement_strength.default_value = 1.0
displacement_strength.min_value = -10000.0
displacement_strength.max_value = 10000.0
displacement_strength.structure_type = "SINGLE"
subdivision = new_socket(
interface,
"Subdivision",
"INPUT",
"NodeSocketInt",
)
subdivision.default_value = 5
subdivision.min_value = 0
subdivision.max_value = 10
background_panel = interface.new_panel(
name="Remove Background",
default_closed=True,
)
remove_background = new_socket(
interface,
"Remove Background",
"INPUT",
"NodeSocketBool",
parent=background_panel,
)
remove_background.default_value = False
remove_background.is_panel_toggle = True
background_depth = new_socket(
interface,
"Background Depth",
"INPUT",
"NodeSocketFloat",
parent=background_panel,
)
background_depth.default_value = 0.0
background_depth.min_value = -10000.0
background_depth.max_value = 10000.0
background_depth.subtype = "DISTANCE"
data_panel = interface.new_panel(name="Data", default_closed=True)
depth_image = new_socket(
interface,
"Depth Image",
"INPUT",
"NodeSocketImage",
parent=data_panel,
)
foreground_image = new_socket(
interface,
"Foreground Image",
"INPUT",
"NodeSocketImage",
parent=data_panel,
)
material = new_socket(
interface,
"Material",
"INPUT",
"NodeSocketMaterial",
parent=data_panel,
)
frame_data = []
for name, socket_type, default_value in (
("Frame Start", "NodeSocketInt", 1),
("Frames", "NodeSocketInt", 1),
("Cyclic", "NodeSocketBool", False),
):
socket = new_socket(
interface,
name,
"INPUT",
socket_type,
parent=data_panel,
)
socket.default_value = default_value
if name == "Frames":
socket.min_value = 1
frame_data.append(socket)
depth_image.hide_in_modifier = True
foreground_image.hide_in_modifier = True
material.hide_in_modifier = True
for socket in frame_data:
socket.hide_in_modifier = True
new_socket(interface, "Geometry", "OUTPUT", "NodeSocketGeometry")
nodes = group.nodes
links = group.links
bounds_input = new_group_input(nodes, (-2200.0, 700.0), {"Geometry"})
grid_input = new_group_input(nodes, (-2200.0, 150.0), {"Subdivision"})
images_input = new_group_input(
nodes,
(100.0, 1250.0),
{"Depth Image", "Foreground Image"},
)
frames_input = new_group_input(
nodes,
(50.0, 1750.0),
{
"Frame Start",
"Frames",
"Cyclic",
},
)
background_input = new_group_input(
nodes,
(350.0, 250.0),
{"Remove Background", "Background Depth"},
)
displacement_input = new_group_input(
nodes,
(950.0, -150.0),
{"Displacement", "Depth Scale"},
)
material_input = new_group_input(nodes, (1450.0, 250.0), {"Material"})
attributes_input = new_group_input(
nodes,
(2150.0, 50.0),
{"Background Depth"},
)
group_output = nodes.new("NodeGroupOutput")
group_output.location = (3050.0, 650.0)
bounds = nodes.new("GeometryNodeBoundBox")
bounds.location = (-1950.0, 700.0)
links.new(bounds_input.outputs["Geometry"], bounds.inputs["Geometry"])
dimensions = nodes.new("ShaderNodeVectorMath")
dimensions.operation = "SUBTRACT"
dimensions.location = (-1750.0, 700.0)
links.new(bounds.outputs["Max"], dimensions.inputs[0])
links.new(bounds.outputs["Min"], dimensions.inputs[1])
separate_dimensions = nodes.new("ShaderNodeSeparateXYZ")
separate_dimensions.location = (-1550.0, 700.0)
links.new(dimensions.outputs["Vector"], separate_dimensions.inputs[0])
short_segments = nodes.new("ShaderNodeMath")
short_segments.operation = "POWER"
short_segments.inputs[0].default_value = 2.0
short_segments.location = (-1950.0, 150.0)
links.new(grid_input.outputs["Subdivision"], short_segments.inputs[1])
vertex_counts = []
for index, (axis, other_axis) in enumerate((("X", "Y"), ("Y", "X"))):
y = 500.0 - index * 300.0
ratio = nodes.new("ShaderNodeMath")
ratio.operation = "DIVIDE"
ratio.location = (-1350.0, y)
links.new(separate_dimensions.outputs[axis], ratio.inputs[0])
links.new(separate_dimensions.outputs[other_axis], ratio.inputs[1])
at_least_one = nodes.new("ShaderNodeMath")
at_least_one.operation = "MAXIMUM"
at_least_one.inputs[1].default_value = 1.0
at_least_one.location = (-1150.0, y)
links.new(ratio.outputs[0], at_least_one.inputs[0])
scaled_segments = nodes.new("ShaderNodeMath")
scaled_segments.operation = "MULTIPLY"
scaled_segments.location = (-950.0, y)
links.new(short_segments.outputs[0], scaled_segments.inputs[0])
links.new(at_least_one.outputs[0], scaled_segments.inputs[1])
rounded_segments = nodes.new("ShaderNodeMath")
rounded_segments.operation = "ROUND"
rounded_segments.location = (-750.0, y)
links.new(scaled_segments.outputs[0], rounded_segments.inputs[0])
vertices = nodes.new("ShaderNodeMath")
vertices.operation = "ADD"
vertices.inputs[1].default_value = 1.0
vertices.location = (-550.0, y)
links.new(rounded_segments.outputs[0], vertices.inputs[0])
vertex_counts.append(vertices.outputs[0])
grid = nodes.new("GeometryNodeMeshGrid")
grid.location = (-300.0, 650.0)
links.new(separate_dimensions.outputs["X"], grid.inputs["Size X"])
links.new(separate_dimensions.outputs["Y"], grid.inputs["Size Y"])
links.new(vertex_counts[0], grid.inputs["Vertices X"])
links.new(vertex_counts[1], grid.inputs["Vertices Y"])
store_uv = nodes.new("GeometryNodeStoreNamedAttribute")
store_uv.data_type = "FLOAT2"
store_uv.domain = "CORNER"
store_uv.inputs["Name"].default_value = "UVMap"
store_uv.location = (-50.0, 650.0)
links.new(grid.outputs["Mesh"], store_uv.inputs["Geometry"])
links.new(grid.outputs["UV Map"], store_uv.inputs["Value"])
center = nodes.new("ShaderNodeVectorMath")
center.operation = "ADD"
center.location = (-550.0, 950.0)
links.new(bounds.outputs["Min"], center.inputs[0])
links.new(bounds.outputs["Max"], center.inputs[1])
half_center = nodes.new("ShaderNodeVectorMath")
half_center.operation = "SCALE"
half_center.inputs[3].default_value = 0.5
half_center.location = (-300.0, 950.0)
links.new(center.outputs["Vector"], half_center.inputs[0])
transform_grid = nodes.new("GeometryNodeTransform")
transform_grid.location = (200.0, 650.0)
links.new(store_uv.outputs["Geometry"], transform_grid.inputs["Geometry"])
links.new(half_center.outputs["Vector"], transform_grid.inputs["Translation"])
depth_texture = nodes.new("GeometryNodeImageTexture")
depth_texture.interpolation = "Linear"
depth_texture.extension = "EXTEND"
depth_texture.location = (350.0, 1150.0)
foreground_texture = nodes.new("GeometryNodeImageTexture")
foreground_texture.interpolation = "Closest"
foreground_texture.extension = "EXTEND"
foreground_texture.location = (350.0, 900.0)
links.new(images_input.outputs["Depth Image"], depth_texture.inputs["Image"])
links.new(
images_input.outputs["Foreground Image"],
foreground_texture.inputs["Image"],
)
links.new(grid.outputs["UV Map"], depth_texture.inputs["Vector"])
links.new(grid.outputs["UV Map"], foreground_texture.inputs["Vector"])
scene_time = nodes.new("GeometryNodeInputSceneTime")
scene_time.location = (50.0, 2200.0)
sequence_frame = build_sequence_frame(
nodes,
links,
scene_time.outputs["Frame"],
frames_input,
2200.0,
)
links.new(sequence_frame, depth_texture.inputs["Frame"])
links.new(sequence_frame, foreground_texture.inputs["Frame"])
clamp_foreground = nodes.new("ShaderNodeMath")
clamp_foreground.operation = "MAXIMUM"
clamp_foreground.location = (600.0, 650.0)
links.new(depth_texture.outputs["Color"], clamp_foreground.inputs[0])
links.new(
background_input.outputs["Background Depth"],
clamp_foreground.inputs[1],
)
foreground_mask = new_compare(
nodes,
"FLOAT",
"GREATER_THAN",
(600.0, 900.0),
0.5,
)
links.new(
foreground_texture.outputs["Alpha"],
input_socket(foreground_mask, "A", 0),
)
masked_depth = nodes.new("ShaderNodeMix")
masked_depth.data_type = "FLOAT"
masked_depth.location = (800.0, 650.0)
links.new(foreground_mask.outputs["Result"], masked_depth.inputs[0])
links.new(
background_input.outputs["Background Depth"],
masked_depth.inputs[2],
)
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
selected_depth = nodes.new("ShaderNodeMix")
selected_depth.data_type = "FLOAT"
selected_depth.location = (1000.0, 650.0)
remove_background_value = background_input.outputs["Remove Background"]
links.new(remove_background_value, selected_depth.inputs[0])
links.new(depth_texture.outputs["Color"], selected_depth.inputs[2])
links.new(masked_depth.outputs["Result"], selected_depth.inputs[3])
displacement_menu = nodes.new("GeometryNodeMenuSwitch")
displacement_menu.data_type = "FLOAT"
displacement_menu.enum_items.clear()
displacement_menu.enum_items.new("Geometry")
displacement_menu.enum_items.new("Material")
displacement_menu.inputs["Geometry"].default_value = 1.0
displacement_menu.inputs["Material"].default_value = 0.0
displacement_menu.location = (1150.0, 200.0)
links.new(
displacement_input.outputs["Displacement"],
displacement_menu.inputs["Menu"],
)
displacement.default_value = "Geometry"
geometry_mode = displacement_menu.outputs[0]
material_mode = nodes.new("ShaderNodeMath")
material_mode.operation = "SUBTRACT"
material_mode.inputs[0].default_value = 1.0
material_mode.location = (1350.0, 100.0)
links.new(geometry_mode, material_mode.inputs[1])
displacement_scale = nodes.new("ShaderNodeMath")
displacement_scale.operation = "MULTIPLY"
displacement_scale.location = (1200.0, 650.0)
links.new(selected_depth.outputs["Result"], displacement_scale.inputs[0])
geometry_strength = nodes.new("ShaderNodeMath")
geometry_strength.operation = "MULTIPLY"
geometry_strength.location = (1150.0, 450.0)
links.new(geometry_mode, geometry_strength.inputs[0])
links.new(displacement_input.outputs["Depth Scale"], geometry_strength.inputs[1])
links.new(geometry_strength.outputs[0], displacement_scale.inputs[1])
normal = nodes.new("GeometryNodeInputNormal")
normal.location = (1200.0, 950.0)
offset = nodes.new("ShaderNodeVectorMath")
offset.operation = "SCALE"
offset.location = (1400.0, 750.0)
links.new(normal.outputs["Normal"], offset.inputs[0])
links.new(displacement_scale.outputs[0], offset.inputs[3])
set_position = nodes.new("GeometryNodeSetPosition")
set_position.location = (1450.0, 650.0)
links.new(transform_grid.outputs["Geometry"], set_position.inputs["Geometry"])
links.new(offset.outputs["Vector"], set_position.inputs["Offset"])
set_material = nodes.new("GeometryNodeSetMaterial")
set_material.location = (1650.0, 650.0)
links.new(set_position.outputs["Geometry"], set_material.inputs["Geometry"])
links.new(material_input.outputs["Material"], set_material.inputs["Material"])
shade_smooth = nodes.new("GeometryNodeSetShadeSmooth")
shade_smooth.domain = "FACE"
shade_smooth.inputs["Shade Smooth"].default_value = True
shade_smooth.location = (1850.0, 650.0)
links.new(set_material.outputs["Geometry"], shade_smooth.inputs["Geometry"])
attribute_values = (
(
REMOVE_BACKGROUND_ATTRIBUTE,
"BOOLEAN",
remove_background_value,
),
(
MATERIAL_DISPLACEMENT_ATTRIBUTE,
"BOOLEAN",
material_mode.outputs[0],
),
(
BACKGROUND_DEPTH_ATTRIBUTE,
"FLOAT",
attributes_input.outputs["Background Depth"],
),
(
DEPTH_SCALE_ATTRIBUTE,
"FLOAT",
displacement_input.outputs["Depth Scale"],
),
)
geometry = shade_smooth.outputs["Geometry"]
x = 2100.0
for name, data_type, value in attribute_values:
store = nodes.new("GeometryNodeStoreNamedAttribute")
store.data_type = data_type
store.domain = "POINT"
store.inputs["Name"].default_value = name
store.location = (x, 650.0)
links.new(geometry, store.inputs["Geometry"])
links.new(value, store.inputs["Value"])
geometry = store.outputs["Geometry"]
x += 200.0
links.new(geometry, group_output.inputs["Geometry"])
group.color_tag = "GEOMETRY"
group.description = (
"Build an aspect-adaptive DA3 depth grid, clamp its foreground, "
"and pass background and displacement controls to the material."
)
group.use_fake_user = True
return group
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.context.preferences.filepaths.save_version = 0
build_group()
build_depth_plane_group()
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=str(OUTPUT_PATH), check_existing=False)

View File

@@ -0,0 +1,249 @@
from pathlib import Path
import sys
import bpy
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from snapform.image_depth import (
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME,
SURFACE_MATERIAL_NODE_GROUP_NAME,
add_depth_plane_processing,
create_material,
load_depth_plane_node_group,
modifier_input_identifier,
)
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
group = load_depth_plane_node_group()
background_depth = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Background Depth"
)
assert background_depth.subtype == "DISTANCE"
geometry_input = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Geometry"
)
assert geometry_input.structure_type == "SINGLE"
all_inputs = [
(item.name, getattr(item.parent, "name", None), item.hide_in_modifier)
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
]
visible_inputs = [name for name, parent, hidden in all_inputs if not hidden]
assert visible_inputs[:6] == [
"Geometry",
"Displacement",
"Depth Scale",
"Subdivision",
"Remove Background",
"Background Depth",
]
remove_background = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Remove Background"
)
assert remove_background.socket_type == "NodeSocketBool"
assert remove_background.is_panel_toggle
assert remove_background.parent.name == "Remove Background"
assert background_depth.parent == remove_background.parent
depth_scale = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Depth Scale"
)
assert depth_scale.default_value == 1.0
assert depth_scale.min_value < 0.0
assert depth_scale.structure_type == "SINGLE"
stores = {
node.inputs["Name"].default_value: node
for node in group.nodes
if node.bl_idname == "GeometryNodeStoreNamedAttribute"
}
attribute_names = (
"o_remove_background",
"o_material_displacement",
"o_background_depth",
"o_depth_scale",
)
for name in attribute_names:
assert stores[name].domain == "POINT"
assert not any(
node.bl_idname == "GeometryNodeGeometryToInstance"
for node in group.nodes
)
mesh = bpy.data.meshes.new("Depth Plane")
mesh.from_pydata(
[(-1, -1, 0), (1, -1, 0), (-1, 1, 0), (1, 1, 0)],
[],
[(0, 1, 3, 2)],
)
obj = bpy.data.objects.new("Depth Plane", mesh)
bpy.context.scene.collection.objects.link(obj)
depth = bpy.data.images.new("Depth", width=2, height=2, float_buffer=True)
depth.pixels[:] = [0.25, 0.25, 0.25, 1.0] * 4
foreground = bpy.data.images.new("Foreground", width=2, height=2)
foreground.pixels[:] = [0.8, 0.2, 0.1, 1.0] * 4
foreground.alpha_mode = "CHANNEL_PACKED"
material = create_material(
"Depth Plane",
foreground,
1.0,
displacement_image=depth,
foreground_image=foreground,
)
mesh.materials.append(material)
material_group = bpy.data.node_groups[DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME]
interface_inputs = [
item.name
for item in material_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
]
interface_outputs = [
item.name
for item in material_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "OUTPUT"
]
assert interface_inputs == [
"Color",
"Alpha",
"Depth",
"Bump Scale",
]
assert interface_outputs == [
"Color",
"Roughness",
"Alpha",
"Normal",
"Displacement",
]
depth_input = next(
item
for item in material_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Depth"
)
assert depth_input.socket_type == "NodeSocketFloat"
bump_scale = next(
item
for item in material_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Bump Scale"
)
assert bump_scale.default_value == 1.0
assert bump_scale.min_value < 0.0
material_group_nodes = [
node
for node in material.node_tree.nodes
if node.bl_idname == "ShaderNodeGroup"
and node.node_tree == material_group
]
assert len(material_group_nodes) == 1
assert material_group_nodes[0].name == "O DA3 Depth Plane"
attributes = [
node
for node in material_group.nodes
if node.bl_idname == "ShaderNodeAttribute"
]
assert len(attributes) == 4
assert all(node.attribute_type == "GEOMETRY" for node in attributes)
assert not any(
node.bl_idname == "ShaderNodeAttribute"
for node in material.node_tree.nodes
)
forbidden_outer_nodes = {
"ShaderNodeAttribute",
"ShaderNodeBump",
"ShaderNodeDisplacement",
"ShaderNodeFloatCurve",
"ShaderNodeMath",
"ShaderNodeMix",
}
assert not any(
node.bl_idname in forbidden_outer_nodes
for node in material.node_tree.nodes
)
surface_material = create_material("Surface", foreground, 1.0)
surface_group = bpy.data.node_groups[SURFACE_MATERIAL_NODE_GROUP_NAME]
surface_group_nodes = [
node
for node in surface_material.node_tree.nodes
if node.bl_idname == "ShaderNodeGroup"
and node.node_tree == surface_group
]
assert len(surface_group_nodes) == 1
assert surface_group_nodes[0].name == "O DA3 Surface"
surface_depth_input = next(
item
for item in surface_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Depth"
)
assert surface_depth_input.socket_type == "NodeSocketFloat"
assert not any(
node.bl_idname in forbidden_outer_nodes
for node in surface_material.node_tree.nodes
)
modifier = add_depth_plane_processing(obj, depth, foreground, material)
modifier[modifier_input_identifier(group, "Depth Scale")] = -2.0
for name, expected in (
("Frame Start", 1),
("Frames", 1),
("Cyclic", False),
):
assert modifier[modifier_input_identifier(group, name)] == expected
image_textures = [
node
for node in group.nodes
if node.bl_idname == "GeometryNodeImageTexture"
]
assert len(image_textures) == 2
assert all(node.inputs["Frame"].is_linked for node in image_textures)
assert image_textures[0].inputs["Frame"].links[0].from_socket == (
image_textures[1].inputs["Frame"].links[0].from_socket
)
floored_modulo = [
node
for node in group.nodes
if node.bl_idname == "ShaderNodeMath"
and node.operation == "FLOORED_MODULO"
]
assert len(floored_modulo) == 1
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
result = bpy.ops.object.modifier_apply(modifier=modifier.name)
assert result == {"FINISHED"}
assert not obj.modifiers
assert obj.type == "MESH"
assert len(obj.data.vertices) > 4
for name in attribute_names:
assert obj.data.attributes[name].domain == "POINT"
assert all(polygon.use_smooth for polygon in obj.data.polygons)
assert material in obj.data.materials[:]
print("Depth plane asset validation passed")
if __name__ == "__main__":
main()

View File

@@ -37,6 +37,14 @@ def main():
group = load_geometry_node_group()
assert load_geometry_node_group() == group
assert group.is_modifier
geometry_input = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Geometry"
)
assert geometry_input.structure_type == "SINGLE"
assert group.asset_data is None
assert not any(node.bl_idname == "GeometryNodeGroup" for node in group.nodes)
assert not any(node.bl_idname == "GeometryNodeTriangulate" for node in group.nodes)
@@ -72,6 +80,16 @@ def main():
assert [item.name for item in panels["Options"].interface_items] == [
"Fix Topology",
]
for name, default_value in (("Confidence", 0.0), ("Stretch", 10.0)):
item = next(
item
for item in panels["Culling"].interface_items
if item.name == name
)
assert item.min_value == 0.0
assert item.max_value == 100.0
assert item.default_value == default_value
assert item.subtype == "PERCENTAGE"
if bpy.app.version >= (5, 0, 0):
assert group.library is not None
@@ -104,13 +122,13 @@ def main():
finally:
evaluated.to_mesh_clear()
set_input(modifier, group, "Stretch", 1.0)
set_input(modifier, group, "Stretch", 100.0)
stretch_item = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.name == "Stretch"
)
assert modifier[stretch_item.identifier] == 1.0
assert modifier[stretch_item.identifier] == 100.0
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
culled = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())