Refactor image mesh processing and background removal functionality
- Updated NODE_GROUP_NAME from "O Refine DA3 Surface" to "O DA3 Surface". - Introduced `remove_surface_background` function to filter out background faces and compact geometry. - Enhanced `build_image_mesh` to conditionally remove background faces based on a new property. - Added `snapform_remove_background` and `snapform_keep_original` properties to operators for better control over background handling. - Updated UI to reflect new properties and improved resolution scaling logic. - Refactored tests to cover new background removal functionality and ensure proper geometry handling. - Adjusted geometry nodes and validation scripts to remove references to background handling.
This commit is contained in:
17
README.md
17
README.md
@@ -31,8 +31,10 @@ DA3、BEN2 源码包和 uv 已内置在扩展里,安装时直接从本地解
|
|||||||
2. 右键打开 `SnapForm` 子菜单,选择 `Convert to Depth Plane`
|
2. 右键打开 `SnapForm` 子菜单,选择 `Convert to Depth Plane`
|
||||||
3. 在对话框中选择:
|
3. 在对话框中选择:
|
||||||
- `Depth from`:`Original` 直接用原图,`AI Generated` 让 AI 估算深度
|
- `Depth from`:`Original` 直接用原图,`AI Generated` 让 AI 估算深度
|
||||||
- `Background Depth Distance` 为 `0` 时不剔除背景;大于 `0` 时把背景设为固定距离,AI 与 Original 均支持
|
- `Resolution Scale` 使用 `1–100%`,按原图长边计算 Process Res;结果会限制在 `252–1008` 并自动对齐到 14 的倍数
|
||||||
- 其余选项保持默认即可
|
- `Remove Background` 默认关闭;开启后在转换期间使用 BEN2 删除背景
|
||||||
|
- `Unshaded` 保留为直接显示贴图颜色的选项
|
||||||
|
- `Keep Original` 默认关闭,位于分割线下方;开启后保留源 Image Empty
|
||||||
4. 点击确认,稍等片刻就得到带位移的立体面片
|
4. 点击确认,稍等片刻就得到带位移的立体面片
|
||||||
|
|
||||||
### 去除背景
|
### 去除背景
|
||||||
@@ -44,14 +46,17 @@ DA3、BEN2 源码包和 uv 已内置在扩展里,安装时直接从本地解
|
|||||||
|
|
||||||
选中单张图片的 Image Empty,右键打开 `SnapForm` 子菜单并选择 `Convert to Surface`。SnapForm 会按 DA3
|
选中单张图片的 Image Empty,右键打开 `SnapForm` 子菜单并选择 `Convert to Surface`。SnapForm 会按 DA3
|
||||||
相机内参与深度逐像素反投影,并把生成网格匹配到原 Empty 的可见尺寸、Offset 与 Transform。DA3
|
相机内参与深度逐像素反投影,并把生成网格匹配到原 Empty 的可见尺寸、Offset 与 Transform。DA3
|
||||||
confidence、边拉伸与 BEN2 背景结果会保存为 Surface attributes,再由一个可复用的 Geometry Nodes
|
confidence 与边拉伸结果会保存为 Surface attributes,再由一个可复用的 Geometry Nodes
|
||||||
修改器完成剔面、快速重拓扑、Relax 与 UV 重投。匹配相机默认不创建;开启后会作为 Surface 的子级。
|
修改器完成剔面、快速重拓扑、Relax 与 UV 重投。开启 Remove Background 时,背景三角面会在转换期间
|
||||||
|
直接删除,不再交给 Geometry Nodes。匹配相机默认不创建;开启后会作为 Surface 的子级。
|
||||||
当前此功能仅支持单张图片。
|
当前此功能仅支持单张图片。
|
||||||
|
|
||||||
转换对话框只保留模型设置、`Create Matching Camera`、`Unshaded` 与 `Delete Original`。剔除和重拓扑参数直接在
|
转换对话框提供模型与 `Resolution Scale`、`Create Matching Camera`、`Remove Background`、`Unshaded`,
|
||||||
|
以及分割线下方的 `Keep Original`。
|
||||||
|
界面会显示计算所得的实际 Process Res。剔除和重拓扑参数直接在
|
||||||
Geometry Nodes 修改器中调整;`Unshaded` 可使用与 Depth Plane 相同的 Emission 材质。普通材质的
|
Geometry Nodes 修改器中调整;`Unshaded` 可使用与 Depth Plane 相同的 Emission 材质。普通材质的
|
||||||
Bump Scale 会在材质组内乘以 `0.001`,并按透视表面的深度空间缩放。Surface attributes 统一使用 `o_` 前缀;Host 准备过程显示
|
Bump Scale 会在材质组内乘以 `0.001`,并按透视表面的深度空间缩放。Surface attributes 统一使用 `o_` 前缀;Host 准备过程显示
|
||||||
`Post-processing n/6`,可看到当前阶段与总步骤。
|
`Preparing surface data n/6`,可看到当前阶段与总步骤。
|
||||||
|
|
||||||
### 视频与序列帧
|
### 视频与序列帧
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
- 视频生成的序列帧以外部文件引用保留在 job 目录(Blender 无法整体 Pack 序列帧);
|
- 视频生成的序列帧以外部文件引用保留在 job 目录(Blender 无法整体 Pack 序列帧);
|
||||||
Geometry Nodes 与材质 Image Texture 均可按场景帧播放序列帧
|
Geometry Nodes 与材质 Image Texture 均可按场景帧播放序列帧
|
||||||
- 使用浮点深度贴图、`O DA3 Depth Plane` Geometry Nodes 和原图 Bump 构建立体面片
|
- 使用浮点深度贴图、`O DA3 Depth Plane` Geometry Nodes 和原图 Bump 构建立体面片
|
||||||
- 转换对话框选择推理模式和处理分辨率;细分、背景与置换方式在 Geometry Nodes 修改器中调整
|
- 转换对话框选择推理模式、处理分辨率与背景移除;细分和置换方式在 Geometry Nodes 修改器中调整
|
||||||
- Debug 的 DA3 输出或 BEN2 背景剔除完成后只打开对应 job 文件夹,不修改当前场景
|
- Debug 的 DA3 输出或 BEN2 背景剔除完成后只打开对应 job 文件夹,不修改当前场景
|
||||||
- 完整保留每次任务生成的所有官方文件
|
- 完整保留每次任务生成的所有官方文件
|
||||||
- 安装和模型下载使用后台子进程;推理提交给常驻 AI Host,状态栏实时显示阶段、百分比进度条与取消按钮
|
- 安装和模型下载使用后台子进程;推理提交给常驻 AI Host,状态栏实时显示阶段、百分比进度条与取消按钮
|
||||||
@@ -103,18 +103,19 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
|
|||||||
|
|
||||||
1. 在 3D View 选中 `Object Type = Empty`、`Display As = Image` 的对象。
|
1. 在 3D View 选中 `Object Type = Empty`、`Display As = Image` 的对象。
|
||||||
2. 右键打开 `SnapForm` 子菜单并选择 `Convert to Depth Plane`,通过 `Depth from` 选择深度来源:
|
2. 右键打开 `SnapForm` 子菜单并选择 `Convert to Depth Plane`,通过 `Depth from` 选择深度来源:
|
||||||
- `AI Generated`:显示 Model、Use Ray Pose 和 Resolution Scale。实际 Process Res 为图片长边像素数
|
- `AI Generated`:显示 Model、Use Ray Pose 和 Resolution Scale。Process Res 先以原图长边乘以
|
||||||
乘以 Resolution Scale;范围为 `0.1–1.0`,默认 `1.0`。计算后的 Process Res 会直接显示在
|
Resolution Scale,再钳制到 `252–1008`,最后对齐到最接近的 `14` 倍数。Resolution Scale 以百分比
|
||||||
`Resolution Scale (… px)` 标签中。
|
显示,范围为 `1–100%`,默认 `100%`;界面只在标签中实时显示最终 Process Res。
|
||||||
转换导出 `mini_npz`,使用前需要在
|
转换导出 `mini_npz`,使用前需要在
|
||||||
`Host > Models` 下载所选模型,并固定使用 `process_res_method="upper_bound_resize"`;设备和参考视图策略
|
`Host > Models` 下载所选模型,并固定使用 `process_res_method="upper_bound_resize"`;设备和参考视图策略
|
||||||
继续使用场景设置。Model 下拉菜单只列出已经下载的 DA3 模型;若没有可用模型,对话框会引导到
|
继续使用场景设置。Model 下拉菜单只列出已经下载的 DA3 模型;若没有可用模型,对话框会引导到
|
||||||
`Host > Models` 下载。
|
`Host > Models` 下载。
|
||||||
- `Original`:直接把原图作为深度贴图,不显示估算参数。单张图片运行 BEN2 取得前景 Alpha;视频或
|
- `Original`:直接把原图作为深度贴图,不显示估算参数。仅在开启 `Remove Background` 时运行 BEN2
|
||||||
序列帧同时抽取原始帧与前景序列,把原始帧作为 `.depth` 序列。
|
取得前景 Alpha;视频或序列帧按需同时抽取原始帧与前景序列,把原始帧作为 `.depth` 序列。
|
||||||
`Original` 是默认深度来源。选择 `AI Generated` 时,其 Model、Use Ray Pose 与 Resolution Scale
|
`Original` 是默认深度来源。选择 `AI Generated` 时,其 Model、Use Ray Pose 与 Resolution Scale
|
||||||
参数会集中显示在独立边框中。两种模式都有默认折叠的 Options,内部只保留 `Unshaded`;
|
参数会集中显示在独立边框中。`Remove Background` 与 `Unshaded` 直接显示,不再使用 Options 折叠;
|
||||||
`Delete Original` 保持在折叠项外。转换固定需要 `BEN2 Base`,只把 `.foreground` 的 Alpha 作为独立
|
`Keep Original` 默认关闭,并在分割线下方保持为最后一项。仅当开启 `Remove Background` 时才需要
|
||||||
|
`BEN2 Base`,并把 `.foreground` 的 Alpha 作为独立
|
||||||
Mask,不在 Host 中改写深度值。保存 `.foreground` 时保留 BEN2 Alpha,但 RGB 使用未经修改的原始输入;
|
Mask,不在 Host 中改写深度值。保存 `.foreground` 时保留 BEN2 Alpha,但 RGB 使用未经修改的原始输入;
|
||||||
因此材质直接用 `.foreground` 同时提供颜色和 Mask,不再额外加载原图 Image Texture。
|
因此材质直接用 `.foreground` 同时提供颜色和 Mask,不再额外加载原图 Image Texture。
|
||||||
Image Empty 引用视频或序列帧时,AI Generated 会按 Empty 的 Image Frames 数值逐帧估计
|
Image Empty 引用视频或序列帧时,AI Generated 会按 Empty 的 Image Frames 数值逐帧估计
|
||||||
@@ -123,40 +124,39 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
|
|||||||
一致;Original 模式的视频或序列帧使用 `frame.0001.jpg` 等原图序列作为颜色与深度,
|
一致;Original 模式的视频或序列帧使用 `frame.0001.jpg` 等原图序列作为颜色与深度,
|
||||||
`foreground.0001.png` 只提供前景 Alpha。
|
`foreground.0001.png` 只提供前景 Alpha。
|
||||||
AI Generated 的深度仍以 EXR 序列帧(`depth.0001.exr` 起)保存在 job 目录根下。
|
AI Generated 的深度仍以 EXR 序列帧(`depth.0001.exr` 起)保存在 job 目录根下。
|
||||||
`Unshaded` 默认关闭,并默认删除原对象。
|
`Unshaded`、`Remove Background` 与 `Keep Original` 均默认关闭,因此默认删除原对象。
|
||||||
3. AI Generated 完成后会自动创建面片。面片几何先由 `mini_npz` 的相机内参与深度确定,再通过对象
|
3. AI Generated 完成后会自动创建面片。面片几何先由 `mini_npz` 的相机内参与深度确定,再通过对象
|
||||||
Transform 匹配原 Image Empty 的可见尺寸、位置、旋转和 Offset。Original 模式直接使用源 Empty
|
Transform 匹配原 Image Empty 的可见尺寸、位置、旋转和 Offset。Original 模式直接使用源 Empty
|
||||||
的可见尺寸、Offset 与 Transform。AI Generated 会把用于匹配画面尺寸的缩放预乘到 `.depth` 数值,
|
的可见尺寸、Offset 与 Transform。AI Generated 会把用于匹配画面尺寸的缩放预乘到 `.depth` 数值,
|
||||||
XY 缩放烘焙到 Mesh,Z 不参与平面匹配;最终对象 Scale 为 `(1, 1, 1)`,视觉深度与旧逻辑一致。
|
XY 缩放烘焙到 Mesh,Z 不参与平面匹配;最终对象 Scale 为 `(1, 1, 1)`,视觉深度与旧逻辑一致。
|
||||||
开启 `Delete Original` 时,成功生成后
|
关闭 `Keep Original` 时,成功生成后删除源 Empty。生成面片 Object 使用源 Empty 的原名;开启
|
||||||
删除源 Empty。生成面片 Object 使用源 Empty 的原名;关闭 `Delete Original` 时不处理
|
`Keep Original` 时不处理
|
||||||
同名冲突,由 Blender 自动为生成面片追加编号。
|
同名冲突,由 Blender 自动为生成面片追加编号。
|
||||||
4. Blender 只创建保留图像纵横比的最低密度基础网格,再把 `O DA3 Depth Plane` 作为 Geometry Nodes
|
4. Blender 只创建保留图像纵横比的最低密度基础网格,再把 `O DA3 Depth Plane` 作为 Geometry Nodes
|
||||||
修改器添加到对象。节点组与 `O Refine DA3 Surface` 一同保存在 `O_SnapForm.blend`,关闭
|
修改器添加到对象。节点组与 `O DA3 Surface` 一同保存在 `O_SnapForm.blend`,关闭
|
||||||
`show_group_selector`,不为每个对象复制节点组。修改器公开以下参数:
|
`show_group_selector`,不为每个对象复制节点组。修改器公开以下参数:
|
||||||
- `Subdivision`:默认 `5`。节点读取基础平面的 Bounding Box,短边分段数为 `2^Subdivision`,
|
- `Displacement`:位于 Geometry 之后的第一项,菜单包含 `Geometry`(默认)与 `Material`。`Geometry`
|
||||||
|
在节点组内沿法线修改网格,`Material` 保持网格平面并启用材质置换;
|
||||||
|
- `Subdivision`:位于第二项,默认 `5`。节点读取基础平面的 Bounding Box,短边分段数为 `2^Subdivision`,
|
||||||
长边按宽高比计算并四舍五入;例如 16:9 图片在等级 `5` 时生成 `57×32` 个面;
|
长边按宽高比计算并四舍五入;例如 16:9 图片在等级 `5` 时生成 `57×32` 个面;
|
||||||
- `Remove Background`:默认关闭的 Panel Toggle。关闭时折叠隐藏 `Background Depth`,开启后才显示;
|
- `Background Depth`:默认 `0`,允许正负值,Structure Type 为 `Single`,直接显示且不再放在 Panel 中。前景 Mask 外的结构或
|
||||||
它不删除几何,而是通过点域的
|
材质置换使用该值;是否存在背景只取决于转换所得色彩贴图的 `Alpha > 0.5` 二值结果;
|
||||||
`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
|
- `Depth Scale`:默认 `1`,允许正负值,Structure Type 为 `Single`,同时控制 Geometry 与 Material
|
||||||
两条置换路径。
|
两条置换路径。
|
||||||
Grid 重建后通过隐藏的 Material 输入重新赋予原图材质,并对所有面执行 Shade Smooth。
|
Grid 重建后通过隐藏的 Material 输入重新赋予原图材质,并对所有面执行 Shade Smooth。
|
||||||
材质使用显式指向 `UVMap` 的 UV Map 节点连接颜色、前景 Mask 与深度 Image Texture,不依赖活动 UV。
|
材质使用显式指向 `UVMap` 的 UV Map 节点连接颜色、前景 Mask 与深度 Image Texture,不依赖活动 UV。
|
||||||
开启背景移除时,前景深度先与 `Background Depth` 取 Maximum,再按前景 Alpha 与背景深度混合,
|
前景深度先与 `Background Depth` 取 Maximum,再按色彩贴图 Alpha 与背景深度混合,
|
||||||
深度分支在混合前使用 `Alpha > 0.5` 转为二值 Mask,避免半透明前景被背景深度拉扯;
|
深度分支在混合前使用 `Alpha > 0.5` 转为二值 Mask,避免半透明前景被背景深度拉扯;
|
||||||
材质表面 Alpha 仍保留柔和边缘。材质置换使用相同的二值和钳制顺序。
|
材质表面 Alpha 同样使用二值 Mask。材质置换使用相同的二值和钳制顺序。
|
||||||
节点组还会把 `o_material_displacement` 与 `o_background_depth` 写入点域,材质通过
|
节点组还会把 `o_material_displacement` 与 `o_background_depth` 写入点域,材质通过
|
||||||
`Geometry` 类型的 Attribute 节点读取。点域会为顶点保存重复值,但能在应用修改器后继续保留参数;
|
`Geometry` 类型的 Attribute 节点读取。点域会为顶点保存重复值,但能在应用修改器后继续保留参数;
|
||||||
相比自适应网格本身,其占用很小。Depth Image 与 Foreground Image 是修改器隐藏数据输入,
|
相比自适应网格本身,其占用很小。Depth Image 与 Foreground Image 是修改器隐藏数据输入,
|
||||||
由导入流程自动设置。
|
由导入流程自动设置。
|
||||||
视频与序列帧还会为 Depth 与 Foreground 共同写入隐藏的 `Frame Start`、`Frames` 与 `Cyclic`。
|
视频与序列帧还会为 Depth 与 Foreground 共同写入隐藏的 `Frame`、`Frame Start`、`Frames` 与 `Cyclic`。
|
||||||
导入内容已经是截取后的帧,因此 Offset 在节点组内恒定为 `0`,不再作为输入。节点组使用 Scene Time
|
节点组内部不再使用 Scene Time。只有原 Image Empty 是视频或序列时,导入流程才直接给修改器的 `Frame`
|
||||||
计算从 `1` 开始的 Image Texture Frame:场景帧与 Start 都为 `1` 时读取图片第 `1` 帧;非循环输入
|
socket 添加 Scripted Expression Driver `frame`;静态图保持默认值 `1`,不创建 Driver。导入内容已经
|
||||||
|
是截取后的帧,因此 Offset 在节点组内恒定为 `0`。
|
||||||
|
节点组根据 `Frame` 计算从 `1` 开始的 Image Texture Frame:场景帧与 Start 都为 `1` 时读取图片第 `1` 帧;非循环输入
|
||||||
钳制到有效范围;循环输入使用 Floored Modulo,让 `Frame Start` 前后的负、正相对帧都映射到
|
钳制到有效范围;循环输入使用 Floored Modulo,让 `Frame Start` 前后的负、正相对帧都映射到
|
||||||
`1…Frames`,因此开始帧之前也会循环播放。Geometry 置换会像材质 Image Texture 一样随场景帧刷新。
|
`1…Frames`,因此开始帧之前也会循环播放。Geometry 置换会像材质 Image Texture 一样随场景帧刷新。
|
||||||
|
|
||||||
@@ -165,13 +165,17 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
|
|||||||
1. 在 3D View 选中仅包含一帧的 Image Empty,右键打开 `SnapForm` 子菜单并选择
|
1. 在 3D View 选中仅包含一帧的 Image Empty,右键打开 `SnapForm` 子菜单并选择
|
||||||
`Convert to Surface`。该入口独立于
|
`Convert to Surface`。该入口独立于
|
||||||
`Convert to Depth Plane`,当前不处理视频或序列帧。
|
`Convert to Depth Plane`,当前不处理视频或序列帧。
|
||||||
2. Host 请求 DA3 的 `depth`、`conf` 与 `intrinsics`,同时固定运行 BEN2 取得前景 alpha。Depth 不做
|
对话框中的 Resolution Scale 与 Depth Plane 使用相同设计:原图长边乘以 `1–100%` 的比例后,
|
||||||
smoothing,也不在 Host 中剔面或减面;Host 只反投影完整有效拓扑,并计算 confidence percentile、
|
将实际 Process Res 钳制在 `252–1008` 并按 `14` 对齐。
|
||||||
camera-space edge stretch 与 background 标记。
|
2. Host 请求 DA3 的 `depth`、`conf` 与 `intrinsics`。仅在开启 `Remove Background` 时运行 BEN2 取得
|
||||||
3. Blender 把勾选 `Usage > Modifier` 的 `O Refine DA3 Surface` 节点组直接作为 Geometry Nodes 修改器
|
前景 Alpha,并在输出前删除包含背景顶点的三角面;关闭时不加载 BEN2。Depth 不做 smoothing,Host
|
||||||
|
反投影深度网格,建立源三角面,计算 confidence percentile 与 camera-space edge stretch,并写入
|
||||||
|
基于原图像素坐标的源 `UVMap`。这些步骤只准备修改器的输入网格,不执行 Relax、拓扑修复、平滑、
|
||||||
|
小岛清理或最终 UV 重投影。运行时显示的六个 `Preparing surface data` 步骤均指这段 Host 端源数据准备。
|
||||||
|
3. Blender 把勾选 `Usage > Modifier` 的 `O DA3 Surface` 节点组直接作为 Geometry Nodes 修改器
|
||||||
添加到 Surface,不另设修改器名称。节点组内没有子节点组,依次完成:
|
添加到 Surface,不另设修改器名称。节点组内没有子节点组,依次完成:
|
||||||
- 修改器顶层依次显示 `Resolution`、`Relax Iterations`、`Smooth`;默认展开的 `Culling` Panel
|
- 修改器顶层依次显示 `Resolution`、`Relax Iterations`、`Smooth`;默认展开的 `Culling` Panel
|
||||||
包含 `Confidence`、`Stretch`、`Background`。两个百分比输入都使用 `0–100` 的节点值,并在组内
|
只包含 `Confidence` 与 `Stretch`。两个百分比输入都使用 `0–100` 的节点值,并在组内
|
||||||
转换为 `0–1` 阈值;`Stretch = 100%` 明确选择全部面,剔面后同时删除
|
转换为 `0–1` 阈值;`Stretch = 100%` 明确选择全部面,剔面后同时删除
|
||||||
Face Count 为零的孤立顶点;
|
Face Count 为零的孤立顶点;
|
||||||
- 每轮 Remesh Iteration 依次执行 Connected Merge、循环拓扑修复与一次 Smooth,使新拓扑在进入下一轮
|
- 每轮 Remesh Iteration 依次执行 Connected Merge、循环拓扑修复与一次 Smooth,使新拓扑在进入下一轮
|
||||||
@@ -188,8 +192,8 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
|
|||||||
修改器关闭 `show_group_selector`。`Fix Topology` 位于默认折叠的 `Options` Panel。
|
修改器关闭 `show_group_selector`。`Fix Topology` 位于默认折叠的 `Options` Panel。
|
||||||
节点图按 Culling、Remesh、Topology Fix、Island Cleanup 与 UV 分区摆放;各区就近使用独立的
|
节点图按 Culling、Remesh、Topology Fix、Island Cleanup 与 UV 分区摆放;各区就近使用独立的
|
||||||
Group Input,并隐藏没有连线的接口,避免跨越整个节点图的输入连线。
|
Group Input,并隐藏没有连线的接口,避免跨越整个节点图的输入连线。
|
||||||
5. Host 准备过程固定显示 6 个编号阶段:验证 DA3 数据、反投影、准备 DA3/BEN2 attributes、构建原始
|
5. Host 准备过程固定显示 6 个编号阶段:验证 DA3 数据、反投影、准备 DA3 与可选 BEN2 数据、构建原始
|
||||||
拓扑、投影 UV/attributes、写出网格。状态栏显示 `Post-processing n/6: ...`。
|
源三角面、源 UV/attributes、写出网格。状态栏显示 `Preparing surface data n/6: ...`。
|
||||||
6. Host 在 job 根目录写出 `mesh.npz` 与 `texture.png`。NPZ 保存 camera-space `vertices`、
|
6. Host 在 job 根目录写出 `mesh.npz` 与 `texture.png`。NPZ 保存 camera-space `vertices`、
|
||||||
`faces`、`uv`、`intrinsics`、`image_size` 与有效深度中位数 `reference_depth`。Blender 读取这些
|
`faces`、`uv`、`intrinsics`、`image_size` 与有效深度中位数 `reference_depth`。Blender 读取这些
|
||||||
数据后直接创建 Surface 与原图材质,不需要 Open3D 或 OpenCV 进入 Blender Python 环境。
|
数据后直接创建 Surface 与原图材质,不需要 Open3D 或 OpenCV 进入 Blender Python 环境。
|
||||||
@@ -200,14 +204,26 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
|
|||||||
边界重合,主点同时匹配 Empty Offset。网格对象直接沿用源 Empty 的 `matrix_world`,因此位置、旋转、
|
边界重合,主点同时匹配 Empty Offset。网格对象直接沿用源 Empty 的 `matrix_world`,因此位置、旋转、
|
||||||
缩放及父级关系产生的世界变换保持一致。更近的顶点沿 Empty 局部 `+Z` 方向凸出,三角面正面朝向
|
缩放及父级关系产生的世界变换保持一致。更近的顶点沿 Empty 局部 `+Z` 方向凸出,三角面正面朝向
|
||||||
观察相机,启用 Backface Culling 时可从正面看到。
|
观察相机,启用 Backface Culling 时可从正面看到。
|
||||||
8. 转换对话框不再提供后处理参数,只保留模型设置、`Create Matching Camera`、`Unshaded` 与
|
8. 转换对话框不再提供后处理参数,只保留模型与 Resolution Scale 设置、`Create Matching Camera`、
|
||||||
`Delete Original`。`Unshaded` 使用与 Depth Plane 相同的 Emission/Transparent 材质路径;普通材质的
|
`Remove Background`、`Unshaded`,以及分割线下方的 `Keep Original`。背景移除默认关闭;开启后 Host
|
||||||
|
使用 BEN2,并在写出 Surface 前直接删除背景三角面,不再由 Geometry Nodes 的 Background 开关处理。
|
||||||
|
`Unshaded` 使用与 Depth Plane 相同的 Emission/Transparent 材质路径;普通材质的
|
||||||
Bump Scale 按 `abs(object.scale.z) × camera scale_z` 设置,并在材质组内乘以固定基准 `0.001`,
|
Bump Scale 按 `abs(object.scale.z) × camera scale_z` 设置,并在材质组内乘以固定基准 `0.001`,
|
||||||
与透视表面实际深度尺度一致;它允许负数以反转凹凸方向。
|
与透视表面实际深度尺度一致;它允许负数以反转凹凸方向。
|
||||||
相机默认关闭;开启后会在 Empty 局部 `+Z` 侧创建一台朝向局部 `-Z` 的透视相机,并成为生成 Surface
|
相机默认关闭;开启后会在 Empty 局部 `+Z` 侧创建一台朝向局部 `-Z` 的透视相机,并成为生成 Surface
|
||||||
的子级,使二者移动时保持投影关系。开启 `Delete Original` 时,网格成功创建后删除源 Empty。
|
的子级,使二者移动时保持投影关系。关闭 `Keep Original` 时,网格成功创建后删除源 Empty。
|
||||||
表面、材质与相机会归入同一个 `Convert to Surface` Undo 步骤。
|
表面、材质与相机会归入同一个 `Convert to Surface` Undo 步骤。
|
||||||
|
|
||||||
|
Process Res 的设计依据 DA3 官方实现:API 与 CLI 默认使用 `process_res=504` 和
|
||||||
|
`process_res_method="upper_bound_resize"`;输入预处理的 `PATCH_SIZE=14`,会把宽高分别调整到最接近的
|
||||||
|
`14` 倍数。DA3 论文以 `504×504` 为 base resolution,公开的训练尺寸长边为 `504`、`672`、`756` 或
|
||||||
|
`896`。官方 CLI 虽给出 `1024` 示例,但没有声明更高分辨率会提升质量,也没有规定 `1000` 为硬上限。
|
||||||
|
因此转换入口使用相对原图的 Resolution Scale,同时通过上下限避免 Process Res 过低或过高,并把训练范围
|
||||||
|
之外的值明确视为实验设置。参考
|
||||||
|
[DA3 API](https://github.com/ByteDance-Seed/Depth-Anything-3/blob/main/docs/API.md)、
|
||||||
|
[输入预处理实现](https://github.com/ByteDance-Seed/Depth-Anything-3/blob/main/src/depth_anything_3/utils/io/input_processor.py)
|
||||||
|
与[技术报告](https://depth-anything-3.github.io/assets/da3_tech_report_2025.pdf)。
|
||||||
|
|
||||||
Debug 的 Textured Surface GLB 会把 depth、confidence 与 confidence percentile 写入自定义顶点
|
Debug 的 Textured Surface GLB 会把 depth、confidence 与 confidence percentile 写入自定义顶点
|
||||||
attributes。其他 DA3 Debug 三维输出会同时写出未压缩的 `prediction.attributes.npz`,保存完整
|
attributes。其他 DA3 Debug 三维输出会同时写出未压缩的 `prediction.attributes.npz`,保存完整
|
||||||
depth、confidence、intrinsics、extrinsics 与 confidence percentile。当前 trimesh 的 glTF 点图元
|
depth、confidence、intrinsics、extrinsics 与 confidence percentile。当前 trimesh 的 glTF 点图元
|
||||||
@@ -245,24 +261,27 @@ Height = sqrt(Area / Aspect)
|
|||||||
材质始终导入深度 Image Texture,并连接到 Displacement 节点和 Material Output;材质 Surface
|
材质始终导入深度 Image Texture,并连接到 Displacement 节点和 Material Output;材质 Surface
|
||||||
Displacement 设置为 `Displacement Only`,AI Generated 与 Original 都使用 `Midlevel = 0`、`Scale = 1`。
|
Displacement 设置为 `Displacement Only`,AI Generated 与 Original 都使用 `Midlevel = 0`、`Scale = 1`。
|
||||||
`o_material_displacement` 为 `False` 时,材质把 Height 乘以零,置换只在几何节点中生效;为 `True` 时,
|
`o_material_displacement` 为 `False` 时,材质把 Height 乘以零,置换只在几何节点中生效;为 `True` 时,
|
||||||
几何节点关闭结构置换,材质 Height 恢复。开启 `Remove Background` 时,前景 Alpha 同时用于选择
|
几何节点关闭结构置换,材质 Height 恢复。色彩贴图的 Alpha 二值结果同时用于选择原始深度或有符号的
|
||||||
原始深度或有符号的 `Background Depth`,使两种置换路径保持一致;关闭时背景深度不参与计算。
|
`Background Depth`,使两种置换路径保持一致;未移除背景的普通不透明图片因此全部视为前景。
|
||||||
视频与序列帧的颜色、深度与前景 Image Texture
|
视频与序列帧的颜色、深度与前景 Image Texture
|
||||||
通过 image_user 的 Frames / Offset / Start 把第 i 帧映射到场景 `Start+i` 帧,并启用 Auto Refresh。
|
通过 image_user 的 Frames / Offset / Start 把第 i 帧映射到场景 `Start+i` 帧,并启用 Auto Refresh。
|
||||||
颜色贴图到 Principled BSDF 之间使用独立的 Shader Node Group。Surface 使用 `O DA3 Surface`;
|
颜色贴图到 Principled BSDF 之间使用独立的 Shader Node Group。Surface 使用 `O DA3 Surface Layer`;
|
||||||
Depth Plane 使用显示名为 `O DA3 Depth Plane` 的材质组,并把背景剔除、背景深度与材质置换 Attribute
|
Depth Plane 使用 `O DA3 Depth Plane Layer` 材质组,并把背景剔除、背景深度与材质置换 Attribute
|
||||||
读取全部收进组内。两者都依次接收 `Color`、`Alpha`、Float 类型的 `Depth`、`Bump Scale`,输出 `Color`、
|
读取全部收进组内。两者都依次接收 `Color`、`Alpha`、Float 类型的 `Depth`、`Bump Scale`。
|
||||||
`Roughness`、`Alpha`、`Normal`、`Displacement`。Surface 把原图 Color 同时送入 `Color` 与 `Depth`,
|
`O DA3 Surface Layer` 只输出 `Color`、`Roughness`、`Alpha` 与 `Normal`,不创建 Displacement 节点或输出;
|
||||||
|
`O DA3 Depth Plane Layer` 另外输出 `Displacement`。Surface 把原图 Color 同时送入 `Color` 与 `Depth`,
|
||||||
保持图像 Bump 效果。外层材质只保留贴图、材质组、Principled BSDF 与 Material Output,方便继续调整。
|
保持图像 Bump 效果。外层材质只保留贴图、材质组、Principled BSDF 与 Material Output,方便继续调整。
|
||||||
|
Depth Plane 的 Bump Height 直接使用材质组的 `Color` 输入,只受独立的 `Bump Scale` 影响,不使用深度贴图,
|
||||||
|
也不经过 `Displacement` 模式开关、`Depth Scale` 或 `Background Depth`;材质 Displacement 仍使用这些参数处理后的深度。
|
||||||
无论选择哪种置换方式,原始彩色贴图都会连接
|
无论选择哪种置换方式,原始彩色贴图都会连接
|
||||||
Principled BSDF 的 `Base Color`,并通过
|
Principled BSDF 的 `Base Color`,并通过
|
||||||
`Bump.Height → Bump.Normal → Principled.Normal` 提供表面细节。Bump 的基础 Distance 为
|
`Bump.Height → Bump.Normal → Principled.Normal` 提供表面细节。Bump 的基础 Distance 为
|
||||||
`0.001`,会乘以面片 Transform 的统一缩放倍率。原图 Color 还会经过 `Float Curve` 连接到
|
`0.001`,会乘以面片 Transform 的统一缩放倍率。原图 Color 还会经过 `Float Curve` 连接到
|
||||||
Principled BSDF 的 `Roughness`,曲线两端输出分别为 `0.5` 和 `0.1`;材质 IOR 保持
|
Principled BSDF 的 `Roughness`,曲线两端输出分别为 `0.5` 和 `0.1`;材质 IOR 保持
|
||||||
Principled BSDF 默认值 `1.5`。
|
Principled BSDF 默认值 `1.5`。
|
||||||
开启 `Unshaded` 后,原图 Color 连接 Emission,原图 Alpha 作为 Mix Shader 系数,在
|
开启 `Unshaded` 后,色彩贴图 Color 连接 Emission,二值 Alpha 作为 Mix Shader 系数,在
|
||||||
Transparent BSDF 与 Emission 之间混合后连接 Material Output;不创建 Principled BSDF、Bump 或
|
Transparent BSDF 与 Emission 之间混合后连接 Material Output;不创建 Principled BSDF、Bump 或
|
||||||
Float Curve。`Remove Background` 开启时,原图 Alpha 会再乘以前景 Mask Alpha;关闭时仍完整显示原图。
|
Float Curve。开启 `Remove Background` 时色彩贴图使用 BEN2 输出;关闭时使用原图。
|
||||||
|
|
||||||
这里的“原始距离”是 DA3 模型输出的尺度。普通 Small、Base、Large 和 Giant 模型的深度不应
|
这里的“原始距离”是 DA3 模型输出的尺度。普通 Small、Base、Large 和 Giant 模型的深度不应
|
||||||
被解释为现实世界的米;需要现实尺度时应选择带 metric depth 的
|
被解释为现实世界的米;需要现实尺度时应选择带 metric depth 的
|
||||||
|
|||||||
Binary file not shown.
@@ -15,12 +15,14 @@ ORIGINAL_DISPLACEMENT_MIDLEVEL = 0.0
|
|||||||
BUMP_BASE_DISTANCE = 0.001
|
BUMP_BASE_DISTANCE = 0.001
|
||||||
DEPTH_PLANE_NODE_GROUP_NAME = "O DA3 Depth Plane"
|
DEPTH_PLANE_NODE_GROUP_NAME = "O DA3 Depth Plane"
|
||||||
NODE_ASSET_NAME = "O_SnapForm.blend"
|
NODE_ASSET_NAME = "O_SnapForm.blend"
|
||||||
REMOVE_BACKGROUND_ATTRIBUTE = "o_remove_background"
|
|
||||||
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
|
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
|
||||||
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
|
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
|
||||||
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
|
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
|
||||||
SURFACE_MATERIAL_NODE_GROUP_NAME = "O DA3 Surface"
|
SURFACE_MATERIAL_NODE_GROUP_NAME = "O DA3 Surface Layer"
|
||||||
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME = "O DA3 Depth Plane Material"
|
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME = "O DA3 Depth Plane Layer"
|
||||||
|
DA3_PATCH_SIZE = 14
|
||||||
|
DA3_MIN_PROCESS_RES = 252
|
||||||
|
DA3_MAX_PROCESS_RES = 1008
|
||||||
IMAGE_NAME_SUFFIXES = (
|
IMAGE_NAME_SUFFIXES = (
|
||||||
".jpeg",
|
".jpeg",
|
||||||
".jpg",
|
".jpg",
|
||||||
@@ -180,13 +182,45 @@ def removed_background_data_name(source_object):
|
|||||||
return f"{image_base_name(source_object.data)}.foreground"
|
return f"{image_base_name(source_object.data)}.foreground"
|
||||||
|
|
||||||
|
|
||||||
def scaled_process_resolution(image, scale):
|
def scaled_process_resolution(image, scale_percent):
|
||||||
|
"""Return the scaled long edge clamped and aligned for DA3 inference."""
|
||||||
width, height = image.size
|
width, height = image.size
|
||||||
if width <= 0 or height <= 0:
|
if width <= 0 or height <= 0:
|
||||||
raise ValueError("The source image has no readable dimensions")
|
raise ValueError("The source image has no readable dimensions")
|
||||||
if scale < 0.1 or scale > 1.0:
|
if scale_percent < 1.0 or scale_percent > 100.0:
|
||||||
raise ValueError("Resolution Scale must be between 0.1 and 1.0")
|
raise ValueError("Resolution Scale must be between 1% and 100%")
|
||||||
return int(max(width, height) * scale + 0.5)
|
process_res = max(width, height) * scale_percent / 100.0
|
||||||
|
process_res = max(DA3_MIN_PROCESS_RES, process_res)
|
||||||
|
process_res = min(DA3_MAX_PROCESS_RES, process_res)
|
||||||
|
return int(process_res / DA3_PATCH_SIZE + 0.5) * DA3_PATCH_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
def process_resolution_scale_property():
|
||||||
|
return bpy.props.FloatProperty(
|
||||||
|
name="Resolution Scale",
|
||||||
|
description=(
|
||||||
|
"Scale the source image long edge for DA3 processing; the result "
|
||||||
|
"is limited to 252-1008 px and aligned to a multiple of 14"
|
||||||
|
),
|
||||||
|
default=100.0,
|
||||||
|
min=1.0,
|
||||||
|
max=100.0,
|
||||||
|
step=100,
|
||||||
|
precision=0,
|
||||||
|
subtype="PERCENTAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_process_resolution(layout, owner, image):
|
||||||
|
process_res = scaled_process_resolution(
|
||||||
|
image,
|
||||||
|
owner.snapform_process_res_factor,
|
||||||
|
)
|
||||||
|
layout.prop(
|
||||||
|
owner,
|
||||||
|
"snapform_process_res_factor",
|
||||||
|
text=f"Resolution Scale ({process_res} px)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_packed_image(image, scene):
|
def save_packed_image(image, scene):
|
||||||
@@ -880,13 +914,15 @@ def material_node_group(depth_plane=False):
|
|||||||
socket.default_value = 1.0
|
socket.default_value = 1.0
|
||||||
socket.min_value = -10000.0
|
socket.min_value = -10000.0
|
||||||
socket.max_value = 10000.0
|
socket.max_value = 10000.0
|
||||||
for name, socket_type in (
|
output_sockets = [
|
||||||
("Color", "NodeSocketColor"),
|
("Color", "NodeSocketColor"),
|
||||||
("Roughness", "NodeSocketFloat"),
|
("Roughness", "NodeSocketFloat"),
|
||||||
("Alpha", "NodeSocketFloat"),
|
("Alpha", "NodeSocketFloat"),
|
||||||
("Normal", "NodeSocketVector"),
|
("Normal", "NodeSocketVector"),
|
||||||
("Displacement", "NodeSocketVector"),
|
]
|
||||||
):
|
if depth_plane:
|
||||||
|
output_sockets.append(("Displacement", "NodeSocketVector"))
|
||||||
|
for name, socket_type in output_sockets:
|
||||||
interface.new_socket(
|
interface.new_socket(
|
||||||
name=name,
|
name=name,
|
||||||
in_out="OUTPUT",
|
in_out="OUTPUT",
|
||||||
@@ -908,23 +944,20 @@ def material_node_group(depth_plane=False):
|
|||||||
roughness_curve.location = (-250.0, 350.0)
|
roughness_curve.location = (-250.0, 350.0)
|
||||||
bump = nodes.new("ShaderNodeBump")
|
bump = nodes.new("ShaderNodeBump")
|
||||||
bump.location = (-250.0, 0.0)
|
bump.location = (-250.0, 0.0)
|
||||||
displacement = nodes.new("ShaderNodeDisplacement")
|
displacement = None
|
||||||
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_alpha = group_input.outputs["Alpha"]
|
||||||
processed_depth = group_input.outputs["Depth"]
|
processed_depth = group_input.outputs["Depth"]
|
||||||
|
bump_depth = group_input.outputs["Color"]
|
||||||
if depth_plane:
|
if depth_plane:
|
||||||
|
displacement = nodes.new("ShaderNodeDisplacement")
|
||||||
|
displacement.inputs["Midlevel"].default_value = 0.0
|
||||||
|
displacement.inputs["Scale"].default_value = 1.0
|
||||||
group_input.location = (-1200.0, 100.0)
|
group_input.location = (-1200.0, 100.0)
|
||||||
group_output.location = (1050.0, 100.0)
|
group_output.location = (1050.0, 100.0)
|
||||||
roughness_curve.location = (-300.0, 450.0)
|
roughness_curve.location = (-300.0, 450.0)
|
||||||
bump.location = (750.0, 50.0)
|
bump.location = (750.0, 50.0)
|
||||||
displacement.location = (750.0, -300.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 = nodes.new("ShaderNodeAttribute")
|
||||||
background_attribute.attribute_type = "GEOMETRY"
|
background_attribute.attribute_type = "GEOMETRY"
|
||||||
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
|
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
|
||||||
@@ -937,10 +970,6 @@ def material_node_group(depth_plane=False):
|
|||||||
strength_attribute.attribute_type = "GEOMETRY"
|
strength_attribute.attribute_type = "GEOMETRY"
|
||||||
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
|
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
|
||||||
strength_attribute.location = (200.0, -650.0)
|
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 = nodes.new("ShaderNodeMath")
|
||||||
foreground_mask.operation = "GREATER_THAN"
|
foreground_mask.operation = "GREATER_THAN"
|
||||||
foreground_mask.inputs[1].default_value = 0.5
|
foreground_mask.inputs[1].default_value = 0.5
|
||||||
@@ -951,31 +980,23 @@ def material_node_group(depth_plane=False):
|
|||||||
masked_depth = nodes.new("ShaderNodeMix")
|
masked_depth = nodes.new("ShaderNodeMix")
|
||||||
masked_depth.data_type = "FLOAT"
|
masked_depth.data_type = "FLOAT"
|
||||||
masked_depth.location = (-350.0, -350.0)
|
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 = nodes.new("ShaderNodeMath")
|
||||||
enabled_depth.operation = "MULTIPLY"
|
enabled_depth.operation = "MULTIPLY"
|
||||||
enabled_depth.location = (250.0, -350.0)
|
enabled_depth.location = (250.0, -350.0)
|
||||||
scaled_depth = nodes.new("ShaderNodeMath")
|
scaled_depth = nodes.new("ShaderNodeMath")
|
||||||
scaled_depth.operation = "MULTIPLY"
|
scaled_depth.operation = "MULTIPLY"
|
||||||
scaled_depth.location = (450.0, -350.0)
|
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["Alpha"], foreground_mask.inputs[0])
|
||||||
links.new(group_input.outputs["Depth"], clamp_foreground.inputs[0])
|
links.new(group_input.outputs["Depth"], clamp_foreground.inputs[0])
|
||||||
links.new(background_attribute.outputs["Fac"], clamp_foreground.inputs[1])
|
links.new(background_attribute.outputs["Fac"], clamp_foreground.inputs[1])
|
||||||
links.new(foreground_mask.outputs[0], masked_depth.inputs[0])
|
links.new(foreground_mask.outputs[0], masked_depth.inputs[0])
|
||||||
links.new(background_attribute.outputs["Fac"], masked_depth.inputs[2])
|
links.new(background_attribute.outputs["Fac"], masked_depth.inputs[2])
|
||||||
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
|
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
|
||||||
links.new(remove_attribute.outputs["Fac"], background_mix.inputs[0])
|
links.new(masked_depth.outputs["Result"], enabled_depth.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(material_attribute.outputs["Fac"], enabled_depth.inputs[1])
|
||||||
links.new(enabled_depth.outputs[0], scaled_depth.inputs[0])
|
links.new(enabled_depth.outputs[0], scaled_depth.inputs[0])
|
||||||
links.new(strength_attribute.outputs["Fac"], scaled_depth.inputs[1])
|
links.new(strength_attribute.outputs["Fac"], scaled_depth.inputs[1])
|
||||||
processed_alpha = alpha_mix.outputs["Result"]
|
processed_alpha = foreground_mask.outputs[0]
|
||||||
processed_depth = scaled_depth.outputs[0]
|
processed_depth = scaled_depth.outputs[0]
|
||||||
|
|
||||||
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
|
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
|
||||||
@@ -985,7 +1006,7 @@ def material_node_group(depth_plane=False):
|
|||||||
group_output.inputs["Roughness"],
|
group_output.inputs["Roughness"],
|
||||||
)
|
)
|
||||||
links.new(processed_alpha, group_output.inputs["Alpha"])
|
links.new(processed_alpha, group_output.inputs["Alpha"])
|
||||||
links.new(processed_depth, bump.inputs["Height"])
|
links.new(bump_depth, bump.inputs["Height"])
|
||||||
bump_distance = nodes.new("ShaderNodeMath")
|
bump_distance = nodes.new("ShaderNodeMath")
|
||||||
bump_distance.operation = "MULTIPLY"
|
bump_distance.operation = "MULTIPLY"
|
||||||
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
|
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
|
||||||
@@ -993,6 +1014,7 @@ def material_node_group(depth_plane=False):
|
|||||||
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
|
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
|
||||||
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
|
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
|
||||||
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
|
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
|
||||||
|
if displacement is not None:
|
||||||
links.new(processed_depth, displacement.inputs["Height"])
|
links.new(processed_depth, displacement.inputs["Height"])
|
||||||
links.new(
|
links.new(
|
||||||
displacement.outputs["Displacement"],
|
displacement.outputs["Displacement"],
|
||||||
@@ -1081,9 +1103,7 @@ def create_material(
|
|||||||
material_inputs = nodes.new("ShaderNodeGroup")
|
material_inputs = nodes.new("ShaderNodeGroup")
|
||||||
is_depth_plane = displacement_image is not None
|
is_depth_plane = displacement_image is not None
|
||||||
material_inputs.node_tree = material_node_group(is_depth_plane)
|
material_inputs.node_tree = material_node_group(is_depth_plane)
|
||||||
material_inputs.name = (
|
material_inputs.name = material_inputs.node_tree.name
|
||||||
"O DA3 Depth Plane" if is_depth_plane else "O DA3 Surface"
|
|
||||||
)
|
|
||||||
material_inputs.label = material_inputs.name
|
material_inputs.label = material_inputs.name
|
||||||
material_inputs.location = (-300.0, 350.0)
|
material_inputs.location = (-300.0, 350.0)
|
||||||
material_inputs.inputs["Bump Scale"].default_value = depth_scale
|
material_inputs.inputs["Bump Scale"].default_value = depth_scale
|
||||||
@@ -1243,21 +1263,32 @@ def add_depth_plane_processing(
|
|||||||
frame_start=1,
|
frame_start=1,
|
||||||
frame_duration=1,
|
frame_duration=1,
|
||||||
use_cyclic=False,
|
use_cyclic=False,
|
||||||
|
animate_frame=False,
|
||||||
):
|
):
|
||||||
node_group = load_depth_plane_node_group()
|
node_group = load_depth_plane_node_group()
|
||||||
modifier = plane_object.modifiers.new(name=node_group.name, type="NODES")
|
modifier = plane_object.modifiers.new(
|
||||||
|
name=DEPTH_PLANE_NODE_GROUP_NAME,
|
||||||
|
type="NODES",
|
||||||
|
)
|
||||||
modifier.node_group = node_group
|
modifier.node_group = node_group
|
||||||
modifier.show_group_selector = False
|
modifier.show_group_selector = False
|
||||||
for name, value in (
|
for name, value in (
|
||||||
("Depth Image", depth_image),
|
("Depth Image", depth_image),
|
||||||
("Foreground Image", foreground_image),
|
("Foreground Image", foreground_image),
|
||||||
("Material", material),
|
("Material", material),
|
||||||
|
("Frame", 1),
|
||||||
("Frame Start", frame_start),
|
("Frame Start", frame_start),
|
||||||
("Frames", max(int(frame_duration or 1), 1)),
|
("Frames", max(int(frame_duration or 1), 1)),
|
||||||
("Cyclic", use_cyclic),
|
("Cyclic", use_cyclic),
|
||||||
):
|
):
|
||||||
identifier = modifier_input_identifier(node_group, name)
|
identifier = modifier_input_identifier(node_group, name)
|
||||||
modifier[identifier] = value
|
modifier[identifier] = value
|
||||||
|
if animate_frame:
|
||||||
|
frame_identifier = modifier_input_identifier(node_group, "Frame")
|
||||||
|
driver = modifier.driver_add(f'["{frame_identifier}"]')
|
||||||
|
driver.driver.type = "SCRIPTED"
|
||||||
|
driver.driver.expression = "frame"
|
||||||
|
plane_object.update_tag(refresh={"DATA"})
|
||||||
return modifier
|
return modifier
|
||||||
|
|
||||||
|
|
||||||
@@ -1353,6 +1384,7 @@ def build_image_depth_plane(
|
|||||||
frame_start=source_frame_start,
|
frame_start=source_frame_start,
|
||||||
frame_duration=frame_count,
|
frame_duration=frame_count,
|
||||||
use_cyclic=source_use_cyclic,
|
use_cyclic=source_use_cyclic,
|
||||||
|
animate_frame=is_animated_image(source_object.data),
|
||||||
)
|
)
|
||||||
plane_object["snapform_frame_count"] = frame_count
|
plane_object["snapform_frame_count"] = frame_count
|
||||||
return finish_depth_plane(
|
return finish_depth_plane(
|
||||||
@@ -1456,6 +1488,7 @@ def build_depth_plane(
|
|||||||
frame_start=source_frame_start,
|
frame_start=source_frame_start,
|
||||||
frame_duration=frame_count,
|
frame_duration=frame_count,
|
||||||
use_cyclic=source_use_cyclic,
|
use_cyclic=source_use_cyclic,
|
||||||
|
animate_frame=is_animated_image(source_object.data),
|
||||||
)
|
)
|
||||||
plane_object["snapform_reference_depth"] = reference_depth
|
plane_object["snapform_reference_depth"] = reference_depth
|
||||||
plane_object["snapform_camera_plane_width"] = bounds[1] - bounds[0]
|
plane_object["snapform_camera_plane_width"] = bounds[1] - bounds[0]
|
||||||
@@ -1744,6 +1777,11 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
|||||||
description="Display the source color through an Emission shader",
|
description="Display the source color through an Emission shader",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
snapform_remove_background: bpy.props.BoolProperty(
|
||||||
|
name="Remove Background",
|
||||||
|
description="Remove the background during conversion with BEN2",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
snapform_model: bpy.props.EnumProperty(
|
snapform_model: bpy.props.EnumProperty(
|
||||||
name="Model",
|
name="Model",
|
||||||
description="Model used to estimate image depth",
|
description="Model used to estimate image depth",
|
||||||
@@ -1754,33 +1792,24 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
|||||||
description="Use ray-based camera pose estimation",
|
description="Use ray-based camera pose estimation",
|
||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
snapform_process_res_factor: bpy.props.FloatProperty(
|
snapform_process_res_factor: process_resolution_scale_property()
|
||||||
name="Resolution Scale",
|
snapform_keep_original: bpy.props.BoolProperty(
|
||||||
description="Scale applied to the source image long-edge resolution",
|
name="Keep Original",
|
||||||
default=1.0,
|
description="Keep the source Image Empty after conversion",
|
||||||
min=0.1,
|
default=False,
|
||||||
max=1.0,
|
|
||||||
step=10,
|
|
||||||
precision=2,
|
|
||||||
)
|
|
||||||
snapform_delete_original: bpy.props.BoolProperty(
|
|
||||||
name="Delete Original",
|
|
||||||
description="Delete the source Image Empty after conversion",
|
|
||||||
default=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
from .operators import active_job
|
from .operators import active_job
|
||||||
from .runtime import host_busy, model_ready
|
from .runtime import host_busy, model_ready
|
||||||
from .models import BEN2_MODEL, MODELS
|
from .models import MODELS
|
||||||
|
|
||||||
return (
|
return (
|
||||||
is_image_empty(context.object)
|
is_image_empty(context.object)
|
||||||
and active_job() is None
|
and active_job() is None
|
||||||
and not host_busy()
|
and not host_busy()
|
||||||
and any(model_ready(model_key) for model_key in MODELS)
|
and any(model_ready(model_key) for model_key in MODELS)
|
||||||
and model_ready(BEN2_MODEL.key)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def invoke(self, context, _event):
|
def invoke(self, context, _event):
|
||||||
@@ -1810,28 +1839,17 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
|||||||
else:
|
else:
|
||||||
estimate_box.prop(self, "snapform_model")
|
estimate_box.prop(self, "snapform_model")
|
||||||
estimate_box.prop(self, "snapform_use_ray_pose")
|
estimate_box.prop(self, "snapform_use_ray_pose")
|
||||||
process_res = scaled_process_resolution(
|
draw_process_resolution(estimate_box, self, context.object.data)
|
||||||
context.object.data,
|
layout.prop(self, "snapform_remove_background")
|
||||||
self.snapform_process_res_factor,
|
layout.prop(self, "snapform_unshaded")
|
||||||
)
|
layout.separator(type="LINE")
|
||||||
estimate_box.prop(
|
layout.prop(self, "snapform_keep_original")
|
||||||
self,
|
|
||||||
"snapform_process_res_factor",
|
|
||||||
text=f"Resolution Scale ({process_res} px)",
|
|
||||||
)
|
|
||||||
settings_header, settings_body = layout.panel(
|
|
||||||
"snapform_depth_plane_options",
|
|
||||||
default_closed=True,
|
|
||||||
)
|
|
||||||
settings_header.label(text="Options")
|
|
||||||
if settings_body is not None:
|
|
||||||
settings_body.prop(self, "snapform_unshaded")
|
|
||||||
layout.prop(self, "snapform_delete_original")
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
from .operators import (
|
from .operators import (
|
||||||
IMAGE_DEPTH_JOB,
|
IMAGE_DEPTH_JOB,
|
||||||
ORIGINAL_BACKGROUND_JOB,
|
ORIGINAL_BACKGROUND_JOB,
|
||||||
|
ORIGINAL_FRAMES_JOB,
|
||||||
)
|
)
|
||||||
|
|
||||||
source_object = context.object
|
source_object = context.object
|
||||||
@@ -1849,10 +1867,14 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
|||||||
self.report({"ERROR"}, str(error))
|
self.report({"ERROR"}, str(error))
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
result = bpy.ops.snapform.run_job(
|
result = bpy.ops.snapform.run_job(
|
||||||
job_type=ORIGINAL_BACKGROUND_JOB,
|
job_type=(
|
||||||
|
ORIGINAL_BACKGROUND_JOB
|
||||||
|
if self.snapform_remove_background
|
||||||
|
else ORIGINAL_FRAMES_JOB
|
||||||
|
),
|
||||||
snapform_input_path=str(source_path),
|
snapform_input_path=str(source_path),
|
||||||
snapform_source_object_name=source_object.name,
|
snapform_source_object_name=source_object.name,
|
||||||
snapform_delete_source=self.snapform_delete_original,
|
snapform_delete_source=not self.snapform_keep_original,
|
||||||
snapform_delete_input=delete_input,
|
snapform_delete_input=delete_input,
|
||||||
snapform_unshaded=self.snapform_unshaded,
|
snapform_unshaded=self.snapform_unshaded,
|
||||||
)
|
)
|
||||||
@@ -1888,10 +1910,10 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
|||||||
snapform_model_key=self.snapform_model,
|
snapform_model_key=self.snapform_model,
|
||||||
snapform_use_ray_pose=self.snapform_use_ray_pose,
|
snapform_use_ray_pose=self.snapform_use_ray_pose,
|
||||||
snapform_process_res=process_res,
|
snapform_process_res=process_res,
|
||||||
snapform_delete_source=self.snapform_delete_original,
|
snapform_delete_source=not self.snapform_keep_original,
|
||||||
snapform_delete_input=delete_input,
|
snapform_delete_input=delete_input,
|
||||||
snapform_unshaded=self.snapform_unshaded,
|
snapform_unshaded=self.snapform_unshaded,
|
||||||
snapform_remove_background=True,
|
snapform_remove_background=self.snapform_remove_background,
|
||||||
)
|
)
|
||||||
if "CANCELLED" in result:
|
if "CANCELLED" in result:
|
||||||
if delete_input:
|
if delete_input:
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import bpy
|
|||||||
|
|
||||||
from .image_depth import (
|
from .image_depth import (
|
||||||
create_material,
|
create_material,
|
||||||
|
draw_process_resolution,
|
||||||
image_depth_model_items,
|
image_depth_model_items,
|
||||||
is_image_empty,
|
is_image_empty,
|
||||||
plane_bounds,
|
plane_bounds,
|
||||||
prepare_image_input,
|
prepare_image_input,
|
||||||
|
process_resolution_scale_property,
|
||||||
remove_temporary_input,
|
remove_temporary_input,
|
||||||
scaled_process_resolution,
|
scaled_process_resolution,
|
||||||
video_total_frames,
|
video_total_frames,
|
||||||
@@ -33,7 +35,7 @@ FACE_ATTRIBUTES = {
|
|||||||
"o_edge_stretch": ("FLOAT", "face_edge_stretch"),
|
"o_edge_stretch": ("FLOAT", "face_edge_stretch"),
|
||||||
"o_background_face": ("BOOLEAN", "face_background"),
|
"o_background_face": ("BOOLEAN", "face_background"),
|
||||||
}
|
}
|
||||||
NODE_GROUP_NAME = "O Refine DA3 Surface"
|
NODE_GROUP_NAME = "O DA3 Surface"
|
||||||
NODE_ASSET_NAME = "O_SnapForm.blend"
|
NODE_ASSET_NAME = "O_SnapForm.blend"
|
||||||
|
|
||||||
|
|
||||||
@@ -202,6 +204,30 @@ def assign_snapform_attributes(mesh, attributes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_surface_background(vertices, faces, uv, attributes):
|
||||||
|
"""Remove background faces and compact matching point/face data."""
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
background_faces = np.asarray(attributes["face_background"], dtype=bool)
|
||||||
|
kept_faces = faces[~background_faces]
|
||||||
|
if not len(kept_faces):
|
||||||
|
raise ValueError("Background removal deleted every surface face")
|
||||||
|
used_vertices = np.unique(kept_faces)
|
||||||
|
index_map = np.full(len(vertices), -1, dtype=np.int32)
|
||||||
|
index_map[used_vertices] = np.arange(len(used_vertices), dtype=np.int32)
|
||||||
|
filtered_attributes = {}
|
||||||
|
for _name, (_data_type, key) in POINT_ATTRIBUTES.items():
|
||||||
|
filtered_attributes[key] = attributes[key][used_vertices]
|
||||||
|
for _name, (_data_type, key) in FACE_ATTRIBUTES.items():
|
||||||
|
filtered_attributes[key] = attributes[key][~background_faces]
|
||||||
|
return (
|
||||||
|
vertices[used_vertices],
|
||||||
|
index_map[kept_faces],
|
||||||
|
uv[used_vertices],
|
||||||
|
filtered_attributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def node_asset_path():
|
def node_asset_path():
|
||||||
return Path(__file__).with_name("assets") / NODE_ASSET_NAME
|
return Path(__file__).with_name("assets") / NODE_ASSET_NAME
|
||||||
|
|
||||||
@@ -333,6 +359,7 @@ def build_image_mesh(
|
|||||||
delete_source,
|
delete_source,
|
||||||
create_camera,
|
create_camera,
|
||||||
unshaded,
|
unshaded,
|
||||||
|
remove_background=False,
|
||||||
):
|
):
|
||||||
(
|
(
|
||||||
camera_vertices,
|
camera_vertices,
|
||||||
@@ -343,6 +370,13 @@ def build_image_mesh(
|
|||||||
reference_depth,
|
reference_depth,
|
||||||
attributes,
|
attributes,
|
||||||
) = load_image_mesh_data(npz_path)
|
) = load_image_mesh_data(npz_path)
|
||||||
|
if remove_background:
|
||||||
|
camera_vertices, faces, uv, attributes = remove_surface_background(
|
||||||
|
camera_vertices,
|
||||||
|
faces,
|
||||||
|
uv,
|
||||||
|
attributes,
|
||||||
|
)
|
||||||
vertices, camera_parameters = empty_anchored_vertices(
|
vertices, camera_parameters = empty_anchored_vertices(
|
||||||
source_object,
|
source_object,
|
||||||
camera_vertices,
|
camera_vertices,
|
||||||
@@ -415,6 +449,10 @@ class BuildImageMesh(bpy.types.Operator):
|
|||||||
default=False,
|
default=False,
|
||||||
options={"HIDDEN", "SKIP_SAVE"},
|
options={"HIDDEN", "SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
snapform_remove_background: bpy.props.BoolProperty(
|
||||||
|
default=False,
|
||||||
|
options={"HIDDEN", "SKIP_SAVE"},
|
||||||
|
)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
source_object = bpy.data.objects.get(self.snapform_source_object_name)
|
source_object = bpy.data.objects.get(self.snapform_source_object_name)
|
||||||
@@ -429,6 +467,7 @@ class BuildImageMesh(bpy.types.Operator):
|
|||||||
self.snapform_delete_source,
|
self.snapform_delete_source,
|
||||||
self.snapform_create_camera,
|
self.snapform_create_camera,
|
||||||
self.snapform_unshaded,
|
self.snapform_unshaded,
|
||||||
|
self.snapform_remove_background,
|
||||||
)
|
)
|
||||||
except (OSError, RuntimeError, ValueError) as error:
|
except (OSError, RuntimeError, ValueError) as error:
|
||||||
self.report({"ERROR"}, str(error))
|
self.report({"ERROR"}, str(error))
|
||||||
@@ -452,15 +491,7 @@ class ConvertImageToMesh(bpy.types.Operator):
|
|||||||
description="Use ray-based camera pose estimation",
|
description="Use ray-based camera pose estimation",
|
||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
snapform_process_res_factor: bpy.props.FloatProperty(
|
snapform_process_res_factor: process_resolution_scale_property()
|
||||||
name="Resolution Scale",
|
|
||||||
description="Scale applied to the source image long-edge resolution",
|
|
||||||
default=1.0,
|
|
||||||
min=0.1,
|
|
||||||
max=1.0,
|
|
||||||
step=10,
|
|
||||||
precision=2,
|
|
||||||
)
|
|
||||||
snapform_create_camera: bpy.props.BoolProperty(
|
snapform_create_camera: bpy.props.BoolProperty(
|
||||||
name="Create Matching Camera",
|
name="Create Matching Camera",
|
||||||
description="Create a perspective camera matching the source projection",
|
description="Create a perspective camera matching the source projection",
|
||||||
@@ -471,15 +502,20 @@ class ConvertImageToMesh(bpy.types.Operator):
|
|||||||
description="Display the source color through an Emission shader",
|
description="Display the source color through an Emission shader",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
snapform_delete_original: bpy.props.BoolProperty(
|
snapform_remove_background: bpy.props.BoolProperty(
|
||||||
name="Delete Original",
|
name="Remove Background",
|
||||||
description="Delete the source Image Empty after conversion",
|
description="Remove the background during conversion with BEN2",
|
||||||
default=True,
|
default=False,
|
||||||
|
)
|
||||||
|
snapform_keep_original: bpy.props.BoolProperty(
|
||||||
|
name="Keep Original",
|
||||||
|
description="Keep the source Image Empty after conversion",
|
||||||
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
from .models import BEN2_MODEL, MODELS
|
from .models import MODELS
|
||||||
from .operators import active_job
|
from .operators import active_job
|
||||||
from .runtime import host_busy, model_ready
|
from .runtime import host_busy, model_ready
|
||||||
|
|
||||||
@@ -489,7 +525,6 @@ class ConvertImageToMesh(bpy.types.Operator):
|
|||||||
and active_job() is None
|
and active_job() is None
|
||||||
and not host_busy()
|
and not host_busy()
|
||||||
and any(model_ready(model_key) for model_key in MODELS)
|
and any(model_ready(model_key) for model_key in MODELS)
|
||||||
and model_ready(BEN2_MODEL.key)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def invoke(self, context, _event):
|
def invoke(self, context, _event):
|
||||||
@@ -511,18 +546,12 @@ class ConvertImageToMesh(bpy.types.Operator):
|
|||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.prop(self, "snapform_model")
|
layout.prop(self, "snapform_model")
|
||||||
layout.prop(self, "snapform_use_ray_pose")
|
layout.prop(self, "snapform_use_ray_pose")
|
||||||
process_res = scaled_process_resolution(
|
draw_process_resolution(layout, self, context.object.data)
|
||||||
context.object.data,
|
|
||||||
self.snapform_process_res_factor,
|
|
||||||
)
|
|
||||||
layout.prop(
|
|
||||||
self,
|
|
||||||
"snapform_process_res_factor",
|
|
||||||
text=f"Resolution Scale ({process_res} px)",
|
|
||||||
)
|
|
||||||
layout.prop(self, "snapform_create_camera")
|
layout.prop(self, "snapform_create_camera")
|
||||||
|
layout.prop(self, "snapform_remove_background")
|
||||||
layout.prop(self, "snapform_unshaded")
|
layout.prop(self, "snapform_unshaded")
|
||||||
layout.prop(self, "snapform_delete_original")
|
layout.separator(type="LINE")
|
||||||
|
layout.prop(self, "snapform_keep_original")
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
from .operators import IMAGE_MESH_JOB
|
from .operators import IMAGE_MESH_JOB
|
||||||
@@ -550,10 +579,11 @@ class ConvertImageToMesh(bpy.types.Operator):
|
|||||||
snapform_model_key=self.snapform_model,
|
snapform_model_key=self.snapform_model,
|
||||||
snapform_use_ray_pose=self.snapform_use_ray_pose,
|
snapform_use_ray_pose=self.snapform_use_ray_pose,
|
||||||
snapform_process_res=process_res,
|
snapform_process_res=process_res,
|
||||||
snapform_delete_source=self.snapform_delete_original,
|
snapform_delete_source=not self.snapform_keep_original,
|
||||||
snapform_delete_input=delete_input,
|
snapform_delete_input=delete_input,
|
||||||
snapform_mesh_create_camera=self.snapform_create_camera,
|
snapform_mesh_create_camera=self.snapform_create_camera,
|
||||||
snapform_unshaded=self.snapform_unshaded,
|
snapform_unshaded=self.snapform_unshaded,
|
||||||
|
snapform_remove_background=self.snapform_remove_background,
|
||||||
)
|
)
|
||||||
if "CANCELLED" in result:
|
if "CANCELLED" in result:
|
||||||
if delete_input:
|
if delete_input:
|
||||||
|
|||||||
@@ -328,8 +328,6 @@ class RunJob(bpy.types.Operator):
|
|||||||
remove_background = bool(
|
remove_background = bool(
|
||||||
getattr(self, "snapform_remove_background", False)
|
getattr(self, "snapform_remove_background", False)
|
||||||
)
|
)
|
||||||
if self.job_type == IMAGE_MESH_JOB:
|
|
||||||
remove_background = True
|
|
||||||
if self.job_type == "INSTALL_RUNTIME":
|
if self.job_type == "INSTALL_RUNTIME":
|
||||||
self._require_online_access()
|
self._require_online_access()
|
||||||
return runtime.install_runtime_command(paths["status"]), paths
|
return runtime.install_runtime_command(paths["status"]), paths
|
||||||
@@ -723,6 +721,7 @@ class RunJob(bpy.types.Operator):
|
|||||||
snapform_delete_source=job.delete_source,
|
snapform_delete_source=job.delete_source,
|
||||||
snapform_create_camera=job.mesh_create_camera,
|
snapform_create_camera=job.mesh_create_camera,
|
||||||
snapform_unshaded=job.unshaded,
|
snapform_unshaded=job.unshaded,
|
||||||
|
snapform_remove_background=job.remove_background,
|
||||||
)
|
)
|
||||||
if "FINISHED" not in result:
|
if "FINISHED" not in result:
|
||||||
raise RuntimeError("Unable to build the perspective surface")
|
raise RuntimeError("Unable to build the perspective surface")
|
||||||
|
|||||||
@@ -473,6 +473,12 @@ def export_prediction_attributes(prediction, output_directory):
|
|||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def remove_background_faces(faces, foreground_mask):
|
||||||
|
"""Return only triangles whose vertices all belong to the foreground."""
|
||||||
|
foreground_points = foreground_mask.reshape(-1)
|
||||||
|
return faces[foreground_points[faces].all(axis=1)]
|
||||||
|
|
||||||
|
|
||||||
def export_image_mesh_data(
|
def export_image_mesh_data(
|
||||||
prediction,
|
prediction,
|
||||||
image_path,
|
image_path,
|
||||||
@@ -505,9 +511,9 @@ def export_image_mesh_data(
|
|||||||
raw_depth = depth_frames[0]
|
raw_depth = depth_frames[0]
|
||||||
confidence = confidence_frames[0]
|
confidence = confidence_frames[0]
|
||||||
intrinsics = np.asarray(prediction.intrinsics[0], dtype=np.float32)
|
intrinsics = np.asarray(prediction.intrinsics[0], dtype=np.float32)
|
||||||
progress(2, "Unprojecting camera geometry")
|
progress(2, "Unprojecting the depth grid")
|
||||||
points = unproject_camera_depth(raw_depth, intrinsics)
|
points = unproject_camera_depth(raw_depth, intrinsics)
|
||||||
progress(3, "Preparing DA3 and BEN2 attributes")
|
progress(3, "Preparing confidence and background data")
|
||||||
foreground_mask = np.ones(raw_depth.shape, dtype=bool)
|
foreground_mask = np.ones(raw_depth.shape, dtype=bool)
|
||||||
if foreground_path is not None:
|
if foreground_path is not None:
|
||||||
foreground_mask = foreground_mesh_mask(
|
foreground_mask = foreground_mesh_mask(
|
||||||
@@ -518,7 +524,7 @@ def export_image_mesh_data(
|
|||||||
raw_depth,
|
raw_depth,
|
||||||
confidence,
|
confidence,
|
||||||
)
|
)
|
||||||
progress(4, "Building editable source topology")
|
progress(4, "Building source triangles")
|
||||||
faces = mesh_triangle_faces(
|
faces = mesh_triangle_faces(
|
||||||
points,
|
points,
|
||||||
raw_depth,
|
raw_depth,
|
||||||
@@ -527,11 +533,13 @@ def export_image_mesh_data(
|
|||||||
False,
|
False,
|
||||||
0.0,
|
0.0,
|
||||||
)
|
)
|
||||||
|
if foreground_path is not None:
|
||||||
|
faces = remove_background_faces(faces, foreground_mask)
|
||||||
if not len(faces):
|
if not len(faces):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"DA3 output does not contain any valid surface faces"
|
"DA3 output does not contain any valid surface faces"
|
||||||
)
|
)
|
||||||
progress(5, "Projecting UV and structured attributes")
|
progress(5, "Preparing source UV and attributes")
|
||||||
vertices, faces = compact_mesh(
|
vertices, faces = compact_mesh(
|
||||||
points.reshape(-1, 3),
|
points.reshape(-1, 3),
|
||||||
faces,
|
faces,
|
||||||
@@ -551,7 +559,7 @@ def export_image_mesh_data(
|
|||||||
reference_depth = float(np.median(valid_depth))
|
reference_depth = float(np.median(valid_depth))
|
||||||
output_directory = Path(output_directory)
|
output_directory = Path(output_directory)
|
||||||
output_directory.mkdir(parents=True, exist_ok=True)
|
output_directory.mkdir(parents=True, exist_ok=True)
|
||||||
progress(6, "Writing surface data")
|
progress(6, "Writing source surface data")
|
||||||
result_path = output_directory / "mesh.npz"
|
result_path = output_directory / "mesh.npz"
|
||||||
np.savez_compressed(
|
np.savez_compressed(
|
||||||
result_path,
|
result_path,
|
||||||
@@ -564,7 +572,8 @@ def export_image_mesh_data(
|
|||||||
**attributes,
|
**attributes,
|
||||||
)
|
)
|
||||||
texture_path = output_directory / "texture.png"
|
texture_path = output_directory / "texture.png"
|
||||||
with Image.open(image_path) as image:
|
texture_source = foreground_path or image_path
|
||||||
|
with Image.open(texture_source) as image:
|
||||||
image.convert("RGBA").save(texture_path, format="PNG", compress_level=1)
|
image.convert("RGBA").save(texture_path, format="PNG", compress_level=1)
|
||||||
return result_path, texture_path
|
return result_path, texture_path
|
||||||
|
|
||||||
@@ -1650,7 +1659,7 @@ def reconstruct(args, model_cache=None, cancel_check=None):
|
|||||||
write_status(
|
write_status(
|
||||||
args.status,
|
args.status,
|
||||||
0.88 + 0.09 * step / total,
|
0.88 + 0.09 * step / total,
|
||||||
f"Post-processing {step}/{total}: {label}",
|
f"Preparing surface data {step}/{total}: {label}",
|
||||||
)
|
)
|
||||||
|
|
||||||
export_image_mesh_data(
|
export_image_mesh_data(
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ class AddonTest(unittest.TestCase):
|
|||||||
"snapform_edge_culling",
|
"snapform_edge_culling",
|
||||||
"snapform_face_reduction",
|
"snapform_face_reduction",
|
||||||
"snapform_depth_smoothing",
|
"snapform_depth_smoothing",
|
||||||
"snapform_remove_background",
|
|
||||||
"snapform_postprocess_method",
|
"snapform_postprocess_method",
|
||||||
):
|
):
|
||||||
with self.subTest(property_name=property_name):
|
with self.subTest(property_name=property_name):
|
||||||
@@ -222,6 +221,18 @@ class AddonTest(unittest.TestCase):
|
|||||||
self.assertFalse(annotations["snapform_create_camera"]["default"])
|
self.assertFalse(annotations["snapform_create_camera"]["default"])
|
||||||
self.assertFalse(annotations["snapform_unshaded"]["default"])
|
self.assertFalse(annotations["snapform_unshaded"]["default"])
|
||||||
self.assertEqual(annotations["snapform_unshaded"]["name"], "Unshaded")
|
self.assertEqual(annotations["snapform_unshaded"]["name"], "Unshaded")
|
||||||
|
self.assertFalse(annotations["snapform_remove_background"]["default"])
|
||||||
|
self.assertFalse(annotations["snapform_keep_original"]["default"])
|
||||||
|
self.assertEqual(
|
||||||
|
annotations["snapform_process_res_factor"]["default"],
|
||||||
|
100.0,
|
||||||
|
)
|
||||||
|
self.assertEqual(annotations["snapform_process_res_factor"]["min"], 1.0)
|
||||||
|
self.assertEqual(annotations["snapform_process_res_factor"]["max"], 100.0)
|
||||||
|
self.assertEqual(
|
||||||
|
annotations["snapform_process_res_factor"]["subtype"],
|
||||||
|
"PERCENTAGE",
|
||||||
|
)
|
||||||
|
|
||||||
def test_image_mesh_uses_o_prefixed_attributes_and_bundled_nodes(self):
|
def test_image_mesh_uses_o_prefixed_attributes_and_bundled_nodes(self):
|
||||||
image_mesh = self.addon.image_mesh
|
image_mesh = self.addon.image_mesh
|
||||||
@@ -231,7 +242,7 @@ class AddonTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(all(name.startswith("o_") for name in names))
|
self.assertTrue(all(name.startswith("o_") for name in names))
|
||||||
self.assertTrue(image_mesh.node_asset_path().is_file())
|
self.assertTrue(image_mesh.node_asset_path().is_file())
|
||||||
self.assertEqual(image_mesh.NODE_GROUP_NAME, "O Refine DA3 Surface")
|
self.assertEqual(image_mesh.NODE_GROUP_NAME, "O DA3 Surface")
|
||||||
self.assertEqual(image_mesh.NODE_ASSET_NAME, "O_SnapForm.blend")
|
self.assertEqual(image_mesh.NODE_ASSET_NAME, "O_SnapForm.blend")
|
||||||
|
|
||||||
def test_depth_plane_uses_bundled_nodes_and_material_attributes(self):
|
def test_depth_plane_uses_bundled_nodes_and_material_attributes(self):
|
||||||
@@ -242,10 +253,6 @@ class AddonTest(unittest.TestCase):
|
|||||||
image_depth.DEPTH_PLANE_NODE_GROUP_NAME,
|
image_depth.DEPTH_PLANE_NODE_GROUP_NAME,
|
||||||
"O DA3 Depth Plane",
|
"O DA3 Depth Plane",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
|
||||||
image_depth.REMOVE_BACKGROUND_ATTRIBUTE,
|
|
||||||
"o_remove_background",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
image_depth.MATERIAL_DISPLACEMENT_ATTRIBUTE,
|
image_depth.MATERIAL_DISPLACEMENT_ATTRIBUTE,
|
||||||
"o_material_displacement",
|
"o_material_displacement",
|
||||||
@@ -279,6 +286,33 @@ class AddonTest(unittest.TestCase):
|
|||||||
float(np.median(edge_lengths) * np.sqrt(10.0) * 2.0),
|
float(np.median(edge_lengths) * np.sqrt(10.0) * 2.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_surface_background_filter_compacts_geometry_and_attributes(self):
|
||||||
|
vertices = np.arange(18, dtype=np.float32).reshape(6, 3)
|
||||||
|
faces = np.asarray(((0, 1, 2), (2, 3, 4)), dtype=np.int32)
|
||||||
|
uv = np.arange(12, dtype=np.float32).reshape(6, 2)
|
||||||
|
attributes = {
|
||||||
|
key: np.arange(6 if name in self.addon.image_mesh.POINT_ATTRIBUTES else 2)
|
||||||
|
for name, (_data_type, key) in {
|
||||||
|
**self.addon.image_mesh.POINT_ATTRIBUTES,
|
||||||
|
**self.addon.image_mesh.FACE_ATTRIBUTES,
|
||||||
|
}.items()
|
||||||
|
}
|
||||||
|
attributes["face_background"] = np.asarray((True, False))
|
||||||
|
|
||||||
|
result = self.addon.image_mesh.remove_surface_background(
|
||||||
|
vertices,
|
||||||
|
faces,
|
||||||
|
uv,
|
||||||
|
attributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
filtered_vertices, filtered_faces, filtered_uv, filtered_attributes = result
|
||||||
|
np.testing.assert_array_equal(filtered_vertices, vertices[[2, 3, 4]])
|
||||||
|
np.testing.assert_array_equal(filtered_faces, ((0, 1, 2),))
|
||||||
|
np.testing.assert_array_equal(filtered_uv, uv[[2, 3, 4]])
|
||||||
|
self.assertEqual(len(filtered_attributes["point_depth"]), 3)
|
||||||
|
self.assertEqual(len(filtered_attributes["face_background"]), 1)
|
||||||
|
|
||||||
def test_registers_image_background_operators(self):
|
def test_registers_image_background_operators(self):
|
||||||
self.assertIn(self.addon.RemoveImageBackground, self.addon.CLASSES)
|
self.assertIn(self.addon.RemoveImageBackground, self.addon.CLASSES)
|
||||||
self.assertIn(self.addon.ApplyRemovedBackground, self.addon.CLASSES)
|
self.assertIn(self.addon.ApplyRemovedBackground, self.addon.CLASSES)
|
||||||
@@ -481,7 +515,8 @@ class AddonTest(unittest.TestCase):
|
|||||||
operator = self.addon.ConvertImageToDepthPlane()
|
operator = self.addon.ConvertImageToDepthPlane()
|
||||||
operator.snapform_depth_source = "ORIGINAL"
|
operator.snapform_depth_source = "ORIGINAL"
|
||||||
operator.snapform_unshaded = False
|
operator.snapform_unshaded = False
|
||||||
operator.snapform_delete_original = False
|
operator.snapform_remove_background = True
|
||||||
|
operator.snapform_keep_original = True
|
||||||
source_object = SimpleNamespace(
|
source_object = SimpleNamespace(
|
||||||
name="Reference",
|
name="Reference",
|
||||||
data=SimpleNamespace(name="img.png"),
|
data=SimpleNamespace(name="img.png"),
|
||||||
@@ -513,7 +548,8 @@ class AddonTest(unittest.TestCase):
|
|||||||
operator = self.addon.ConvertImageToDepthPlane()
|
operator = self.addon.ConvertImageToDepthPlane()
|
||||||
operator.snapform_depth_source = "ORIGINAL"
|
operator.snapform_depth_source = "ORIGINAL"
|
||||||
operator.snapform_unshaded = False
|
operator.snapform_unshaded = False
|
||||||
operator.snapform_delete_original = False
|
operator.snapform_remove_background = True
|
||||||
|
operator.snapform_keep_original = True
|
||||||
source_object = SimpleNamespace(
|
source_object = SimpleNamespace(
|
||||||
name="Reference",
|
name="Reference",
|
||||||
data=SimpleNamespace(
|
data=SimpleNamespace(
|
||||||
@@ -552,6 +588,39 @@ class AddonTest(unittest.TestCase):
|
|||||||
self.assertFalse(calls[0]["snapform_delete_input"])
|
self.assertFalse(calls[0]["snapform_delete_input"])
|
||||||
self.assertFalse(calls[0]["snapform_delete_source"])
|
self.assertFalse(calls[0]["snapform_delete_source"])
|
||||||
|
|
||||||
|
def test_original_depth_without_background_uses_frame_extraction(self):
|
||||||
|
operator = self.addon.ConvertImageToDepthPlane()
|
||||||
|
operator.snapform_depth_source = "ORIGINAL"
|
||||||
|
operator.snapform_unshaded = False
|
||||||
|
operator.snapform_remove_background = False
|
||||||
|
operator.snapform_keep_original = False
|
||||||
|
source_object = SimpleNamespace(
|
||||||
|
name="Reference",
|
||||||
|
data=SimpleNamespace(name="img.png"),
|
||||||
|
)
|
||||||
|
context = SimpleNamespace(object=source_object, scene=object())
|
||||||
|
calls = []
|
||||||
|
self.fake_bpy.ops = SimpleNamespace(
|
||||||
|
snapform=SimpleNamespace(
|
||||||
|
run_job=lambda **options: calls.append(options)
|
||||||
|
or {"RUNNING_MODAL"},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with TemporaryDirectory() as directory:
|
||||||
|
input_path = Path(directory) / "input.png"
|
||||||
|
input_path.touch()
|
||||||
|
with patch.object(
|
||||||
|
self.addon.image_depth,
|
||||||
|
"prepare_image_input",
|
||||||
|
return_value=(input_path, False),
|
||||||
|
):
|
||||||
|
result = operator.execute(context)
|
||||||
|
|
||||||
|
self.assertEqual(result, {"FINISHED"})
|
||||||
|
self.assertEqual(calls[0]["job_type"], "ORIGINAL_FRAMES")
|
||||||
|
self.assertTrue(calls[0]["snapform_delete_source"])
|
||||||
|
|
||||||
def test_conversion_dialog_exposes_requested_settings(self):
|
def test_conversion_dialog_exposes_requested_settings(self):
|
||||||
operator = self.addon.ConvertImageToDepthPlane
|
operator = self.addon.ConvertImageToDepthPlane
|
||||||
|
|
||||||
@@ -562,10 +631,21 @@ class AddonTest(unittest.TestCase):
|
|||||||
self.assertTrue(operator.__annotations__["snapform_use_ray_pose"]["default"])
|
self.assertTrue(operator.__annotations__["snapform_use_ray_pose"]["default"])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
operator.__annotations__["snapform_process_res_factor"]["default"],
|
operator.__annotations__["snapform_process_res_factor"]["default"],
|
||||||
|
100.0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
operator.__annotations__["snapform_process_res_factor"]["min"],
|
||||||
1.0,
|
1.0,
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertEqual(
|
||||||
operator.__annotations__["snapform_delete_original"]["default"]
|
operator.__annotations__["snapform_process_res_factor"]["max"],
|
||||||
|
100.0,
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
operator.__annotations__["snapform_remove_background"]["default"]
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
operator.__annotations__["snapform_keep_original"]["default"]
|
||||||
)
|
)
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
operator.__annotations__["snapform_unshaded"]["default"]
|
operator.__annotations__["snapform_unshaded"]["default"]
|
||||||
@@ -578,7 +658,8 @@ class AddonTest(unittest.TestCase):
|
|||||||
"snapform_model",
|
"snapform_model",
|
||||||
"snapform_use_ray_pose",
|
"snapform_use_ray_pose",
|
||||||
"snapform_process_res_factor",
|
"snapform_process_res_factor",
|
||||||
"snapform_delete_original",
|
"snapform_remove_background",
|
||||||
|
"snapform_keep_original",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
depth_sources = operator.__annotations__["snapform_depth_source"]
|
depth_sources = operator.__annotations__["snapform_depth_source"]
|
||||||
@@ -690,9 +771,10 @@ class AddonTest(unittest.TestCase):
|
|||||||
operator.snapform_depth_source = "ESTIMATE"
|
operator.snapform_depth_source = "ESTIMATE"
|
||||||
operator.snapform_model = "BASE"
|
operator.snapform_model = "BASE"
|
||||||
operator.snapform_use_ray_pose = True
|
operator.snapform_use_ray_pose = True
|
||||||
operator.snapform_process_res_factor = 1.0
|
operator.snapform_process_res_factor = 50.0
|
||||||
operator.snapform_unshaded = False
|
operator.snapform_unshaded = False
|
||||||
operator.snapform_delete_original = False
|
operator.snapform_remove_background = True
|
||||||
|
operator.snapform_keep_original = True
|
||||||
calls = []
|
calls = []
|
||||||
self.fake_bpy.ops = SimpleNamespace(
|
self.fake_bpy.ops = SimpleNamespace(
|
||||||
snapform=SimpleNamespace(
|
snapform=SimpleNamespace(
|
||||||
@@ -817,9 +899,10 @@ class AddonTest(unittest.TestCase):
|
|||||||
operator.snapform_depth_source = "ESTIMATE"
|
operator.snapform_depth_source = "ESTIMATE"
|
||||||
operator.snapform_model = "GIANT_1_1"
|
operator.snapform_model = "GIANT_1_1"
|
||||||
operator.snapform_use_ray_pose = True
|
operator.snapform_use_ray_pose = True
|
||||||
operator.snapform_process_res_factor = 0.5
|
operator.snapform_process_res_factor = 50.0
|
||||||
operator.snapform_unshaded = True
|
operator.snapform_unshaded = True
|
||||||
operator.snapform_delete_original = True
|
operator.snapform_remove_background = True
|
||||||
|
operator.snapform_keep_original = False
|
||||||
image = SimpleNamespace(
|
image = SimpleNamespace(
|
||||||
name="img.png",
|
name="img.png",
|
||||||
filepath="",
|
filepath="",
|
||||||
@@ -855,7 +938,7 @@ class AddonTest(unittest.TestCase):
|
|||||||
self.assertEqual(result, {"FINISHED"})
|
self.assertEqual(result, {"FINISHED"})
|
||||||
save_packed_image.assert_called_once_with(image, context.scene)
|
save_packed_image.assert_called_once_with(image, context.scene)
|
||||||
self.assertEqual(calls[0]["snapform_input_path"], str(packed_path))
|
self.assertEqual(calls[0]["snapform_input_path"], str(packed_path))
|
||||||
self.assertEqual(calls[0]["snapform_process_res"], 960)
|
self.assertEqual(calls[0]["snapform_process_res"], 966)
|
||||||
self.assertEqual(calls[0]["snapform_model_key"], "GIANT_1_1")
|
self.assertEqual(calls[0]["snapform_model_key"], "GIANT_1_1")
|
||||||
self.assertTrue(calls[0]["snapform_remove_background"])
|
self.assertTrue(calls[0]["snapform_remove_background"])
|
||||||
self.assertTrue(calls[0]["snapform_unshaded"])
|
self.assertTrue(calls[0]["snapform_unshaded"])
|
||||||
|
|||||||
@@ -558,12 +558,29 @@ class ImageDepthTest(unittest.TestCase):
|
|||||||
):
|
):
|
||||||
self.assertAlmostEqual(value, expected)
|
self.assertAlmostEqual(value, expected)
|
||||||
|
|
||||||
def test_process_resolution_scales_the_image_long_edge(self):
|
def test_process_resolution_scales_long_edge_and_aligns_to_patch_size(self):
|
||||||
image = SimpleNamespace(size=(1080, 1920))
|
image = SimpleNamespace(size=(1080, 1920))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.image_depth.scaled_process_resolution(image, 0.5),
|
self.image_depth.scaled_process_resolution(image, 25.0),
|
||||||
960,
|
476,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.image_depth.scaled_process_resolution(image, 50.0),
|
||||||
|
966,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_process_resolution_clamps_to_supported_range(self):
|
||||||
|
large_image = SimpleNamespace(size=(4000, 3000))
|
||||||
|
small_image = SimpleNamespace(size=(200, 100))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.image_depth.scaled_process_resolution(large_image, 100.0),
|
||||||
|
1008,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.image_depth.scaled_process_resolution(small_image, 1.0),
|
||||||
|
252,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_depth_data_uses_source_image_name(self):
|
def test_depth_data_uses_source_image_name(self):
|
||||||
|
|||||||
@@ -1340,6 +1340,20 @@ class WorkerTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
np.testing.assert_array_equal(compact_faces, [[0, 1, 2]])
|
np.testing.assert_array_equal(compact_faces, [[0, 1, 2]])
|
||||||
|
|
||||||
|
def test_surface_background_faces_are_removed_before_export(self):
|
||||||
|
faces = np.asarray(
|
||||||
|
((0, 1, 4), (0, 4, 3), (1, 2, 5), (1, 5, 4)),
|
||||||
|
dtype=np.int32,
|
||||||
|
)
|
||||||
|
foreground = np.asarray(
|
||||||
|
((False, True, True), (False, True, True)),
|
||||||
|
dtype=bool,
|
||||||
|
)
|
||||||
|
|
||||||
|
filtered = self.worker.remove_background_faces(faces, foreground)
|
||||||
|
|
||||||
|
np.testing.assert_array_equal(filtered, faces[2:])
|
||||||
|
|
||||||
def test_image_mesh_data_keeps_camera_geometry_and_uv(self):
|
def test_image_mesh_data_keeps_camera_geometry_and_uv(self):
|
||||||
class FakeImage:
|
class FakeImage:
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ from pathlib import Path
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
GROUP_NAME = "O Refine DA3 Surface"
|
GROUP_NAME = "O DA3 Surface"
|
||||||
DEPTH_PLANE_GROUP_NAME = "O DA3 Depth Plane"
|
DEPTH_PLANE_GROUP_NAME = "O DA3 Depth Plane"
|
||||||
REMOVE_BACKGROUND_ATTRIBUTE = "o_remove_background"
|
|
||||||
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
|
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
|
||||||
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
|
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
|
||||||
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
|
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
|
||||||
@@ -424,14 +423,6 @@ def build_group():
|
|||||||
edge.min_value = 0.0
|
edge.min_value = 0.0
|
||||||
edge.max_value = 100.0
|
edge.max_value = 100.0
|
||||||
edge.subtype = "PERCENTAGE"
|
edge.subtype = "PERCENTAGE"
|
||||||
background = new_socket(
|
|
||||||
interface,
|
|
||||||
"Background",
|
|
||||||
"INPUT",
|
|
||||||
"NodeSocketBool",
|
|
||||||
parent=culling_panel,
|
|
||||||
)
|
|
||||||
background.default_value = True
|
|
||||||
|
|
||||||
options_panel = interface.new_panel(
|
options_panel = interface.new_panel(
|
||||||
name="Options",
|
name="Options",
|
||||||
@@ -454,7 +445,6 @@ def build_group():
|
|||||||
geometry_input = new_group_input(nodes, (-650.0, 50.0), {"Geometry"})
|
geometry_input = new_group_input(nodes, (-650.0, 50.0), {"Geometry"})
|
||||||
confidence_input = new_group_input(nodes, (-1800.0, 800.0), {"Confidence"})
|
confidence_input = new_group_input(nodes, (-1800.0, 800.0), {"Confidence"})
|
||||||
stretch_input = new_group_input(nodes, (-1800.0, 50.0), {"Stretch"})
|
stretch_input = new_group_input(nodes, (-1800.0, 50.0), {"Stretch"})
|
||||||
background_input = new_group_input(nodes, (-1800.0, -350.0), {"Background"})
|
|
||||||
remesh_input = new_group_input(
|
remesh_input = new_group_input(
|
||||||
nodes,
|
nodes,
|
||||||
(-100.0, -500.0),
|
(-100.0, -500.0),
|
||||||
@@ -523,25 +513,11 @@ def build_group():
|
|||||||
links.new(edge_attribute.outputs["Attribute"], input_socket(edge_compare, "A", 0))
|
links.new(edge_attribute.outputs["Attribute"], input_socket(edge_compare, "A", 0))
|
||||||
links.new(edge_reciprocal.outputs[0], input_socket(edge_compare, "B", 1))
|
links.new(edge_reciprocal.outputs[0], input_socket(edge_compare, "B", 1))
|
||||||
|
|
||||||
background_attribute = nodes.new("GeometryNodeInputNamedAttribute")
|
|
||||||
background_attribute.data_type = "BOOLEAN"
|
|
||||||
background_attribute.inputs["Name"].default_value = "o_background_face"
|
|
||||||
background_attribute.location = (-1800.0, -200.0)
|
|
||||||
background_and = nodes.new("FunctionNodeBooleanMath")
|
|
||||||
background_and.operation = "AND"
|
|
||||||
background_and.location = (-1450.0, -200.0)
|
|
||||||
links.new(background_attribute.outputs["Attribute"], background_and.inputs[0])
|
|
||||||
links.new(background_input.outputs["Background"], background_and.inputs[1])
|
|
||||||
first_or = nodes.new("FunctionNodeBooleanMath")
|
first_or = nodes.new("FunctionNodeBooleanMath")
|
||||||
first_or.operation = "OR"
|
first_or.operation = "OR"
|
||||||
first_or.location = (-950.0, 450.0)
|
first_or.location = (-950.0, 450.0)
|
||||||
links.new(confidence_compare.outputs["Result"], first_or.inputs[0])
|
links.new(confidence_compare.outputs["Result"], first_or.inputs[0])
|
||||||
links.new(edge_compare.outputs["Result"], first_or.inputs[1])
|
links.new(edge_compare.outputs["Result"], first_or.inputs[1])
|
||||||
final_or = nodes.new("FunctionNodeBooleanMath")
|
|
||||||
final_or.operation = "OR"
|
|
||||||
final_or.location = (-750.0, 300.0)
|
|
||||||
links.new(first_or.outputs[0], final_or.inputs[0])
|
|
||||||
links.new(background_and.outputs[0], final_or.inputs[1])
|
|
||||||
full_stretch = new_compare(
|
full_stretch = new_compare(
|
||||||
nodes,
|
nodes,
|
||||||
"FLOAT",
|
"FLOAT",
|
||||||
@@ -556,7 +532,7 @@ def build_group():
|
|||||||
culling_selection = nodes.new("FunctionNodeBooleanMath")
|
culling_selection = nodes.new("FunctionNodeBooleanMath")
|
||||||
culling_selection.operation = "OR"
|
culling_selection.operation = "OR"
|
||||||
culling_selection.location = (-550.0, 300.0)
|
culling_selection.location = (-550.0, 300.0)
|
||||||
links.new(final_or.outputs[0], culling_selection.inputs[0])
|
links.new(first_or.outputs[0], culling_selection.inputs[0])
|
||||||
links.new(full_stretch.outputs["Result"], culling_selection.inputs[1])
|
links.new(full_stretch.outputs["Result"], culling_selection.inputs[1])
|
||||||
delete_geometry = nodes.new("GeometryNodeDeleteGeometry")
|
delete_geometry = nodes.new("GeometryNodeDeleteGeometry")
|
||||||
delete_geometry.domain = "FACE"
|
delete_geometry.domain = "FACE"
|
||||||
@@ -638,7 +614,7 @@ def build_group():
|
|||||||
|
|
||||||
group.color_tag = "GEOMETRY"
|
group.color_tag = "GEOMETRY"
|
||||||
group.description = (
|
group.description = (
|
||||||
"Cull DA3/BEN2 faces, repair merged topology, preserve boundaries, "
|
"Cull DA3 faces, repair merged topology, preserve boundaries, "
|
||||||
"and reproject UVs."
|
"and reproject UVs."
|
||||||
)
|
)
|
||||||
group.use_fake_user = True
|
group.use_fake_user = True
|
||||||
@@ -707,6 +683,15 @@ def build_depth_plane_group():
|
|||||||
"INPUT",
|
"INPUT",
|
||||||
"NodeSocketMenu",
|
"NodeSocketMenu",
|
||||||
)
|
)
|
||||||
|
subdivision = new_socket(
|
||||||
|
interface,
|
||||||
|
"Subdivision",
|
||||||
|
"INPUT",
|
||||||
|
"NodeSocketInt",
|
||||||
|
)
|
||||||
|
subdivision.default_value = 5
|
||||||
|
subdivision.min_value = 0
|
||||||
|
subdivision.max_value = 10
|
||||||
displacement_strength = new_socket(
|
displacement_strength = new_socket(
|
||||||
interface,
|
interface,
|
||||||
"Depth Scale",
|
"Depth Scale",
|
||||||
@@ -717,39 +702,17 @@ def build_depth_plane_group():
|
|||||||
displacement_strength.min_value = -10000.0
|
displacement_strength.min_value = -10000.0
|
||||||
displacement_strength.max_value = 10000.0
|
displacement_strength.max_value = 10000.0
|
||||||
displacement_strength.structure_type = "SINGLE"
|
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(
|
background_depth = new_socket(
|
||||||
interface,
|
interface,
|
||||||
"Background Depth",
|
"Background Depth",
|
||||||
"INPUT",
|
"INPUT",
|
||||||
"NodeSocketFloat",
|
"NodeSocketFloat",
|
||||||
parent=background_panel,
|
|
||||||
)
|
)
|
||||||
background_depth.default_value = 0.0
|
background_depth.default_value = 0.0
|
||||||
background_depth.min_value = -10000.0
|
background_depth.min_value = -10000.0
|
||||||
background_depth.max_value = 10000.0
|
background_depth.max_value = 10000.0
|
||||||
background_depth.subtype = "DISTANCE"
|
background_depth.subtype = "DISTANCE"
|
||||||
|
background_depth.structure_type = "SINGLE"
|
||||||
data_panel = interface.new_panel(name="Data", default_closed=True)
|
data_panel = interface.new_panel(name="Data", default_closed=True)
|
||||||
depth_image = new_socket(
|
depth_image = new_socket(
|
||||||
interface,
|
interface,
|
||||||
@@ -774,6 +737,7 @@ def build_depth_plane_group():
|
|||||||
)
|
)
|
||||||
frame_data = []
|
frame_data = []
|
||||||
for name, socket_type, default_value in (
|
for name, socket_type, default_value in (
|
||||||
|
("Frame", "NodeSocketInt", 1),
|
||||||
("Frame Start", "NodeSocketInt", 1),
|
("Frame Start", "NodeSocketInt", 1),
|
||||||
("Frames", "NodeSocketInt", 1),
|
("Frames", "NodeSocketInt", 1),
|
||||||
("Cyclic", "NodeSocketBool", False),
|
("Cyclic", "NodeSocketBool", False),
|
||||||
@@ -786,6 +750,8 @@ def build_depth_plane_group():
|
|||||||
parent=data_panel,
|
parent=data_panel,
|
||||||
)
|
)
|
||||||
socket.default_value = default_value
|
socket.default_value = default_value
|
||||||
|
if name == "Frame":
|
||||||
|
socket.structure_type = "SINGLE"
|
||||||
if name == "Frames":
|
if name == "Frames":
|
||||||
socket.min_value = 1
|
socket.min_value = 1
|
||||||
frame_data.append(socket)
|
frame_data.append(socket)
|
||||||
@@ -809,6 +775,7 @@ def build_depth_plane_group():
|
|||||||
nodes,
|
nodes,
|
||||||
(50.0, 1750.0),
|
(50.0, 1750.0),
|
||||||
{
|
{
|
||||||
|
"Frame",
|
||||||
"Frame Start",
|
"Frame Start",
|
||||||
"Frames",
|
"Frames",
|
||||||
"Cyclic",
|
"Cyclic",
|
||||||
@@ -817,7 +784,7 @@ def build_depth_plane_group():
|
|||||||
background_input = new_group_input(
|
background_input = new_group_input(
|
||||||
nodes,
|
nodes,
|
||||||
(350.0, 250.0),
|
(350.0, 250.0),
|
||||||
{"Remove Background", "Background Depth"},
|
{"Background Depth"},
|
||||||
)
|
)
|
||||||
displacement_input = new_group_input(
|
displacement_input = new_group_input(
|
||||||
nodes,
|
nodes,
|
||||||
@@ -924,12 +891,10 @@ def build_depth_plane_group():
|
|||||||
)
|
)
|
||||||
links.new(grid.outputs["UV Map"], depth_texture.inputs["Vector"])
|
links.new(grid.outputs["UV Map"], depth_texture.inputs["Vector"])
|
||||||
links.new(grid.outputs["UV Map"], foreground_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(
|
sequence_frame = build_sequence_frame(
|
||||||
nodes,
|
nodes,
|
||||||
links,
|
links,
|
||||||
scene_time.outputs["Frame"],
|
frames_input.outputs["Frame"],
|
||||||
frames_input,
|
frames_input,
|
||||||
2200.0,
|
2200.0,
|
||||||
)
|
)
|
||||||
@@ -964,14 +929,6 @@ def build_depth_plane_group():
|
|||||||
masked_depth.inputs[2],
|
masked_depth.inputs[2],
|
||||||
)
|
)
|
||||||
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
|
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 = nodes.new("GeometryNodeMenuSwitch")
|
||||||
displacement_menu.data_type = "FLOAT"
|
displacement_menu.data_type = "FLOAT"
|
||||||
displacement_menu.enum_items.clear()
|
displacement_menu.enum_items.clear()
|
||||||
@@ -995,7 +952,7 @@ def build_depth_plane_group():
|
|||||||
displacement_scale = nodes.new("ShaderNodeMath")
|
displacement_scale = nodes.new("ShaderNodeMath")
|
||||||
displacement_scale.operation = "MULTIPLY"
|
displacement_scale.operation = "MULTIPLY"
|
||||||
displacement_scale.location = (1200.0, 650.0)
|
displacement_scale.location = (1200.0, 650.0)
|
||||||
links.new(selected_depth.outputs["Result"], displacement_scale.inputs[0])
|
links.new(masked_depth.outputs["Result"], displacement_scale.inputs[0])
|
||||||
geometry_strength = nodes.new("ShaderNodeMath")
|
geometry_strength = nodes.new("ShaderNodeMath")
|
||||||
geometry_strength.operation = "MULTIPLY"
|
geometry_strength.operation = "MULTIPLY"
|
||||||
geometry_strength.location = (1150.0, 450.0)
|
geometry_strength.location = (1150.0, 450.0)
|
||||||
@@ -1024,11 +981,6 @@ def build_depth_plane_group():
|
|||||||
shade_smooth.location = (1850.0, 650.0)
|
shade_smooth.location = (1850.0, 650.0)
|
||||||
links.new(set_material.outputs["Geometry"], shade_smooth.inputs["Geometry"])
|
links.new(set_material.outputs["Geometry"], shade_smooth.inputs["Geometry"])
|
||||||
attribute_values = (
|
attribute_values = (
|
||||||
(
|
|
||||||
REMOVE_BACKGROUND_ATTRIBUTE,
|
|
||||||
"BOOLEAN",
|
|
||||||
remove_background_value,
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
MATERIAL_DISPLACEMENT_ATTRIBUTE,
|
MATERIAL_DISPLACEMENT_ATTRIBUTE,
|
||||||
"BOOLEAN",
|
"BOOLEAN",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from snapform.image_depth import (
|
|||||||
load_depth_plane_node_group,
|
load_depth_plane_node_group,
|
||||||
modifier_input_identifier,
|
modifier_input_identifier,
|
||||||
)
|
)
|
||||||
|
from snapform.image_mesh import load_geometry_node_group
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -27,6 +28,15 @@ def main():
|
|||||||
and item.name == "Background Depth"
|
and item.name == "Background Depth"
|
||||||
)
|
)
|
||||||
assert background_depth.subtype == "DISTANCE"
|
assert background_depth.subtype == "DISTANCE"
|
||||||
|
assert background_depth.structure_type == "SINGLE"
|
||||||
|
frame_input = next(
|
||||||
|
item
|
||||||
|
for item in group.interface.items_tree
|
||||||
|
if item.item_type == "SOCKET"
|
||||||
|
and item.in_out == "INPUT"
|
||||||
|
and item.name == "Frame"
|
||||||
|
)
|
||||||
|
assert frame_input.structure_type == "SINGLE"
|
||||||
geometry_input = next(
|
geometry_input = next(
|
||||||
item
|
item
|
||||||
for item in group.interface.items_tree
|
for item in group.interface.items_tree
|
||||||
@@ -45,22 +55,15 @@ def main():
|
|||||||
assert visible_inputs[:6] == [
|
assert visible_inputs[:6] == [
|
||||||
"Geometry",
|
"Geometry",
|
||||||
"Displacement",
|
"Displacement",
|
||||||
"Depth Scale",
|
|
||||||
"Subdivision",
|
"Subdivision",
|
||||||
"Remove Background",
|
"Depth Scale",
|
||||||
"Background Depth",
|
"Background Depth",
|
||||||
]
|
]
|
||||||
remove_background = next(
|
assert not getattr(background_depth.parent, "name", "")
|
||||||
item
|
assert not any(
|
||||||
|
item.name == "Remove Background"
|
||||||
for item in group.interface.items_tree
|
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(
|
depth_scale = next(
|
||||||
item
|
item
|
||||||
for item in group.interface.items_tree
|
for item in group.interface.items_tree
|
||||||
@@ -77,7 +80,6 @@ def main():
|
|||||||
if node.bl_idname == "GeometryNodeStoreNamedAttribute"
|
if node.bl_idname == "GeometryNodeStoreNamedAttribute"
|
||||||
}
|
}
|
||||||
attribute_names = (
|
attribute_names = (
|
||||||
"o_remove_background",
|
|
||||||
"o_material_displacement",
|
"o_material_displacement",
|
||||||
"o_background_depth",
|
"o_background_depth",
|
||||||
"o_depth_scale",
|
"o_depth_scale",
|
||||||
@@ -160,14 +162,31 @@ def main():
|
|||||||
and node.node_tree == material_group
|
and node.node_tree == material_group
|
||||||
]
|
]
|
||||||
assert len(material_group_nodes) == 1
|
assert len(material_group_nodes) == 1
|
||||||
assert material_group_nodes[0].name == "O DA3 Depth Plane"
|
assert material_group_nodes[0].name == "O DA3 Depth Plane Layer"
|
||||||
|
bump = next(
|
||||||
|
node
|
||||||
|
for node in material_group.nodes
|
||||||
|
if node.bl_idname == "ShaderNodeBump"
|
||||||
|
)
|
||||||
|
bump_height_link = next(
|
||||||
|
link
|
||||||
|
for link in material_group.links
|
||||||
|
if link.to_node == bump and link.to_socket == bump.inputs["Height"]
|
||||||
|
)
|
||||||
|
assert bump_height_link.from_node.bl_idname == "NodeGroupInput"
|
||||||
|
assert bump_height_link.from_socket.name == "Color"
|
||||||
attributes = [
|
attributes = [
|
||||||
node
|
node
|
||||||
for node in material_group.nodes
|
for node in material_group.nodes
|
||||||
if node.bl_idname == "ShaderNodeAttribute"
|
if node.bl_idname == "ShaderNodeAttribute"
|
||||||
]
|
]
|
||||||
assert len(attributes) == 4
|
assert len(attributes) == 3
|
||||||
assert all(node.attribute_type == "GEOMETRY" for node in attributes)
|
assert all(node.attribute_type == "GEOMETRY" for node in attributes)
|
||||||
|
assert {node.attribute_name for node in attributes} == {
|
||||||
|
"o_material_displacement",
|
||||||
|
"o_background_depth",
|
||||||
|
"o_depth_scale",
|
||||||
|
}
|
||||||
assert not any(
|
assert not any(
|
||||||
node.bl_idname == "ShaderNodeAttribute"
|
node.bl_idname == "ShaderNodeAttribute"
|
||||||
for node in material.node_tree.nodes
|
for node in material.node_tree.nodes
|
||||||
@@ -193,7 +212,25 @@ def main():
|
|||||||
and node.node_tree == surface_group
|
and node.node_tree == surface_group
|
||||||
]
|
]
|
||||||
assert len(surface_group_nodes) == 1
|
assert len(surface_group_nodes) == 1
|
||||||
assert surface_group_nodes[0].name == "O DA3 Surface"
|
assert surface_group_nodes[0].name == "O DA3 Surface Layer"
|
||||||
|
surface_outputs = [
|
||||||
|
item.name
|
||||||
|
for item in surface_group.interface.items_tree
|
||||||
|
if item.item_type == "SOCKET"
|
||||||
|
and item.in_out == "OUTPUT"
|
||||||
|
]
|
||||||
|
assert surface_outputs == ["Color", "Roughness", "Alpha", "Normal"]
|
||||||
|
assert not any(
|
||||||
|
node.bl_idname == "ShaderNodeDisplacement"
|
||||||
|
for node in surface_group.nodes
|
||||||
|
)
|
||||||
|
refine_group = load_geometry_node_group()
|
||||||
|
assert not any(
|
||||||
|
item.item_type == "SOCKET"
|
||||||
|
and item.in_out == "INPUT"
|
||||||
|
and item.name == "Background"
|
||||||
|
for item in refine_group.interface.items_tree
|
||||||
|
)
|
||||||
surface_depth_input = next(
|
surface_depth_input = next(
|
||||||
item
|
item
|
||||||
for item in surface_group.interface.items_tree
|
for item in surface_group.interface.items_tree
|
||||||
@@ -206,7 +243,13 @@ def main():
|
|||||||
node.bl_idname in forbidden_outer_nodes
|
node.bl_idname in forbidden_outer_nodes
|
||||||
for node in surface_material.node_tree.nodes
|
for node in surface_material.node_tree.nodes
|
||||||
)
|
)
|
||||||
modifier = add_depth_plane_processing(obj, depth, foreground, material)
|
modifier = add_depth_plane_processing(
|
||||||
|
obj,
|
||||||
|
depth,
|
||||||
|
foreground,
|
||||||
|
material,
|
||||||
|
animate_frame=True,
|
||||||
|
)
|
||||||
modifier[modifier_input_identifier(group, "Depth Scale")] = -2.0
|
modifier[modifier_input_identifier(group, "Depth Scale")] = -2.0
|
||||||
for name, expected in (
|
for name, expected in (
|
||||||
("Frame Start", 1),
|
("Frame Start", 1),
|
||||||
@@ -214,6 +257,20 @@ def main():
|
|||||||
("Cyclic", False),
|
("Cyclic", False),
|
||||||
):
|
):
|
||||||
assert modifier[modifier_input_identifier(group, name)] == expected
|
assert modifier[modifier_input_identifier(group, name)] == expected
|
||||||
|
assert not any(
|
||||||
|
node.bl_idname == "GeometryNodeInputSceneTime"
|
||||||
|
for node in group.nodes
|
||||||
|
)
|
||||||
|
frame_identifier = modifier_input_identifier(group, "Frame")
|
||||||
|
frame_driver = next(
|
||||||
|
fcurve
|
||||||
|
for fcurve in obj.animation_data.drivers
|
||||||
|
if fcurve.data_path == (
|
||||||
|
f'modifiers["{modifier.name}"]["{frame_identifier}"]'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert frame_driver.driver.type == "SCRIPTED"
|
||||||
|
assert frame_driver.driver.expression == "frame"
|
||||||
image_textures = [
|
image_textures = [
|
||||||
node
|
node
|
||||||
for node in group.nodes
|
for node in group.nodes
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ def main():
|
|||||||
group_inputs = [
|
group_inputs = [
|
||||||
node for node in group.nodes if node.bl_idname == "NodeGroupInput"
|
node for node in group.nodes if node.bl_idname == "NodeGroupInput"
|
||||||
]
|
]
|
||||||
assert len(group_inputs) >= 8
|
assert len(group_inputs) >= 7
|
||||||
for node in group_inputs:
|
for node in group_inputs:
|
||||||
visible_outputs = [
|
visible_outputs = [
|
||||||
output for output in node.outputs if output.name and not output.hide
|
output for output in node.outputs if output.name and not output.hide
|
||||||
@@ -75,7 +75,6 @@ def main():
|
|||||||
assert [item.name for item in panels["Culling"].interface_items] == [
|
assert [item.name for item in panels["Culling"].interface_items] == [
|
||||||
"Confidence",
|
"Confidence",
|
||||||
"Stretch",
|
"Stretch",
|
||||||
"Background",
|
|
||||||
]
|
]
|
||||||
assert [item.name for item in panels["Options"].interface_items] == [
|
assert [item.name for item in panels["Options"].interface_items] == [
|
||||||
"Fix Topology",
|
"Fix Topology",
|
||||||
@@ -116,7 +115,7 @@ def main():
|
|||||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||||
result = evaluated.to_mesh()
|
result = evaluated.to_mesh()
|
||||||
try:
|
try:
|
||||||
assert len(result.polygons) == 1
|
assert len(result.polygons) == 2
|
||||||
assert "UVMap" in result.attributes
|
assert "UVMap" in result.attributes
|
||||||
assert result.attributes["UVMap"].data_type == "FLOAT2"
|
assert result.attributes["UVMap"].data_type == "FLOAT2"
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
Reference in New Issue
Block a user