Refactor and enhance geometry node groups for depth plane and material layers

- Updated the depth plane node group to load from an asset and simplified the interface.
- Created a new build script for depth plane geometry nodes.
- Introduced material layer groups for image and depth materials with appropriate sockets.
- Added functionality to connect balloon normal scale to the image material.
- Enhanced tests to validate the new node groups and their connections.
- Ensured that production code does not create node groups directly.
This commit is contained in:
2026-08-31 12:25:23 +08:00
parent 87d7721128
commit 377eee43f5
18 changed files with 602 additions and 405 deletions

View File

@@ -75,25 +75,14 @@ uv lock --refresh
- 新增或修改 Blender 类型时同步检查 `src/anyimage/__init__.py` 的注册与逆序注销,并补充对应测试
- 依赖版本、wheel 或 Manifest 改变时同步检查 `pyproject.toml`、`uv.lock`、`blender_manifest.toml` 和打包测试
## 执行流程与复用
- 按数据依赖安排步骤:先完成会改变后续输入或分析范围的处理,再执行依赖该结果的计算
- 对相同输入和参数的昂贵确定性计算只执行一次,从同一中间结果派生多个产物
- 实现路径由当前功能的输入范围和质量要求决定,不为无实际用途的全局兼容选项增加分支
- 名称、参数和进度文案应反映当前阶段的真实职责,并明确跨进程流程中每一侧负责的最终操作
- 临时资源的创建与清理由同一所有者管理;共享输入解析和生命周期逻辑,不在各消费者中重复实现
## Blender 节点组规范
## 清理、测试与重构验证
资产脚本必须由 Blender 执行;具体入口与验证范围见 [节点资产](docs/internals/node-assets.md),不要用普通 Python 解释器运行。
- 删除前先用 `rg` 查找生产调用方、注册入口、测试、文档和打包清单。只有注册或测试、没有真实功能消费者的入口视为孤立代码,应连同协议一起删除
- 测试应保护当前功能、公开协议、关键算法和已发生过的回归;不要为了保留旧文件结构、无消费者兼容层或实现细节而写测试
- 删除功能时删除对应测试;移动原子能力时把测试迁到新的职责模块。测试名称也要使用当前业务术语,不能继续出现已废弃的模块名或 Job ID
- 打包测试应验证最终产物包含必要文件且不包含已删除结构;文件移动后同步更新
- 结构重构完成后至少执行旧名称与旧路径搜索、差异检查、完整测试和文档构建;涉及 Blender 注册、运行时行为或节点资产时,再使用目标 Blender 验证
- 不混用开发环境、独立运行环境和 Blender 自带 Python;每类验证使用项目声明的对应解释器
- 重构应减少模块、分支、协议和重复计算的数量。新抽象只有在职责稳定或存在真实复用时才成立
`tools/` 中的脚本按维护对象分组,并统一使用能够区分“构建”和“验证”等动作的命名。每个脚本都应有明确入口、文档或测试消费者;没有实际用途的脚本应删除。
## Blender 节点图规范
- 生产运行时使用的可复用节点组必须构建并存储在 `src/anyimage/assets/O_AnyImage.blend`,由统一资产加载入口按名称加载;不得在 `src/anyimage/` 中通过 `bpy.data.node_groups.new()` 动态创建。节点组新增、修改或删除时同步更新资产构建、验证、测试和节点资产文档
- 不修改节点的 `name`,也不使用 `label` 覆盖节点标题;保留 Blender 为节点类型生成的默认名称和外观,避免读图时无法判断节点原型。用途应通过节点类型、参数、空间布局和必要的少量注释表达

View File

@@ -30,6 +30,8 @@ AI 推理运行在 Blender 之外的独立 Python 3.12 Environment 中。Blender
普通 Plane、基础 Image 编辑,以及未启用 **Fit to Foreground** / **Generate Normal Map** 的 Cutout Surface 与 Balloon 无需 AI Environment。Cutout 的 Depth 与 Normal Map 统一使用 MoGe-2;两者同时启用时共享同一次 Prediction,不会重复运行模型。Cutout 的 **Generate Normal Map** 是启用开关;开启后,Depth Surface 自动使用 Object Space,其余 Shape 自动使用 Tangent Space。
Balloon 与 Depth Balloon 会用 `o_balloon` 驱动 `O Image Layer` 已有的 **Normal Scale**,使 Normal Map 强度在轮廓处回落。
## 快速使用
选中 Image Empty 后,右键 **AnyImage** 菜单提供:
@@ -52,6 +54,8 @@ src/anyimage/
└── __init__.py # Blender 注册入口
```
生产功能使用的可复用节点组统一构建到 `assets/O_AnyImage.blend`,运行时只按名称加载,不动态创建节点组。
程序边界、功能文件结构和调用流程见[内部实现总览](docs/internals/index.md);开发新功能前先查看[Common 公共函数](docs/internals/common.md),复用已经存在的 Image、Depth 产物、Material 和对象替换入口。文档按开始使用、功能指南、参考和内部实现分层,不在 README 重复内部算法细节。
## 开发

View File

@@ -18,7 +18,7 @@ Depth Plane 将一张静态图片转换为带深度位移的网格,适合快
## 调整结果
生成对象使用运行时创建的 `O Image Depth Plane` Geometry Nodes 修改器:
生成对象使用扩展资产中的 `O Image Depth Plane` Geometry Nodes 修改器:
- **Subdivide**:控制基础平面细分级别;
- **Thickness**:增加背面和侧壁,`0` 输出单面;

View File

@@ -1,19 +1,21 @@
# 节点资产
AnyImage 将稳定、可复用的 Geometry Nodes 和 Shader 节点组构建到 `O_AnyImage.blend`,运行时按名称加载。Depth Plane 节点图是例外:它由 Python 在当前 `.blend` 中按需创建。
AnyImage 将生产功能使用的可复用 Geometry Nodes 和 Shader 节点组统一构建到 `O_AnyImage.blend`,运行时只按名称加载,不动态创建节点组。
## 对应文件
```text
src/anyimage/
├── assets/O_AnyImage.blend
├── operators/common/mesh_result.py # 运行时资产加载与 Modifier 输入
└── operators/convert_to_plane/nodes.py # Depth Plane 运行时节点图
├── operators/common/mesh_result.py # 运行时资产加载与 Modifier 输入
└── operators/convert_to_plane/nodes.py # Plane 节点资产名称与加载入口
tools/node_assets/
├── build.py # 统一构建入口
├── node_graph.py # Cutout 资产构建专用节点图函数
├── build_mesh_plane.py
├── build_depth_plane.py
├── build_material_layers.py
├── build_shadeless.py
├── build_cutout.py
├── validate.py # 资产清单与基础接口
@@ -42,11 +44,11 @@ BLENDER_BIN="/c/Program Files/Blender Foundation/Blender 4.5/blender.exe"
"$BLENDER_BIN" --background --python tools/node_assets/validate_cutout.py
```
`build.py` 从空 Factory 文件开始,依次构建 `O Mesh Plane`、`O Shadeless`、四个 Cutout 内部节点组与公开修改器节点组 `O Image Cutout`,再保存唯一资产文件。`O Image Cutout` 用 Menu Switch 包装四个内部组;运行时 Loader 只加载这个稳定入口,Blender 4.5 根据 Shape 菜单依赖只显示当前分支实际使用的输入。
`build.py` 从空 Factory 文件开始,依次构建 `O Mesh Plane`、`O Image Depth Plane`、`O Image Layer`、`O Image Depth Layer`、`O Shadeless`、四个 Cutout 内部节点组与公开修改器节点组 `O Image Cutout`,再保存唯一资产文件。`O Image Cutout` 用 Menu Switch 包装四个内部组;运行时 Loader 只加载这个稳定入口,Blender 4.5 根据 Shape 菜单依赖只显示当前分支实际使用的输入。
## 验证范围
基础验证检查发布资产清单、节点树类型、Modifier 标记、接口顺序和 Fake User。专项验证继续检查:
基础验证检查完整发布资产清单、节点树类型、Modifier 标记、接口顺序和 Fake User。专项验证继续检查:
- Mesh Plane 的接口、细分、Thickness 和几何求值;
- `O Image Cutout` 的公开接口、Shape 菜单和四个内部组的依赖关系;

View File

@@ -89,3 +89,5 @@ Job 完成后,`main.py` 在 Blender 主线程读取可选的 RegionMask,直
Depth Shape 的 Base Plane Depth,以及 Depth Balloon 的 Direction,都由 `object.py` 调用 `geometry.py` 的标定函数,根据最终 Region Alpha 计算。透明像素达到 `5%` 时使用有效 Region 自适应标定,否则使用稳定的固定基准。
Normal Map 是材质细节,不改变上述四种 Shape 的几何分类。关闭时材质使用几何法线;开启时,`object.py` 把生成的图片和已经确定的 Normal Space 交给材质节点。最后 `main.py` 提交 Undo,整条 Cutout 流程结束。
Balloon 与 Depth Balloon 的材质会创建 Geometry Attribute 节点,把 `o_balloon` 的 Float 输出直接连接到 `O Image Layer` 已有的 **Normal Scale**;Surface 与 Depth Surface 不建立这条连接。

View File

@@ -21,7 +21,7 @@
### Plane
普通 Plane 使用内置 `O Mesh Plane` Geometry Nodes,提供 Subdivide、Thickness 和 Material。Depth Plane 使用运行时生成的 `O Image Depth Plane`,增加 Depth Amount 与内部 Depth Image。
普通 Plane 使用资产中的 `O Mesh Plane` Geometry Nodes,提供 Subdivide、Thickness 和 Material。Depth Plane 使用同一资产文件中的 `O Image Depth Plane`,增加 Depth Amount 与内部 Depth Image。
### Cutout Shape

View File

@@ -50,28 +50,35 @@ nav:
theme:
name: material
favicon: assets/logo.png
logo: assets/logo.png
language: zh
font:
text: Open Sans
code: Cascadia Code
palette:
# Palette toggle for automatic mode
- media: "(prefers-color-scheme)"
primary: black
toggle:
icon: fontawesome/solid/circle-half-stroke
name: 切换到浅色模式
name: Switch to light mode
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: black
toggle:
icon: fontawesome/regular/moon
name: 切换到深色模式
name: Switch to dark mode
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
toggle:
icon: fontawesome/solid/moon
name: 跟随系统外观
name: Switch to system preference
features:
- header.autohide
- navigation.footer
@@ -79,10 +86,8 @@ theme:
- navigation.sections
- navigation.tabs
- navigation.tabs.sticky
- navigation.top
- search.highlight
- search.suggest
- content.code.copy
- content.code.select
extra:
version:
@@ -99,12 +104,16 @@ plugins:
markdown_extensions:
- attr_list
- md_in_html
- mkdocs-click
- admonition
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.details
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true

Binary file not shown.

View File

@@ -84,10 +84,6 @@ def set_modifier_input(modifier, identifier, value):
DISPLACEMENT_STRENGTH = 1.0
DISPLACEMENT_MIDLEVEL = 0.0
BUMP_BASE_DISTANCE = 0.001
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
IMAGE_MATERIAL_NODE_GROUP_NAME = "O Image Layer"
DEPTH_MATERIAL_NODE_GROUP_NAME = "O Image Depth Layer"
SHADELESS_NODE_GROUP_NAME = "O Shadeless"
@@ -143,186 +139,7 @@ def material_node_group(depth_plane=False):
if depth_plane
else IMAGE_MATERIAL_NODE_GROUP_NAME
)
existing = bpy.data.node_groups.get(group_name)
if existing is not None:
for item in existing.interface.items_tree:
if (
item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Normal"
):
item.default_value = (0.5, 0.5, 1.0, 1.0)
return existing
group = bpy.data.node_groups.new(group_name, "ShaderNodeTree")
interface = group.interface
input_sockets = [
("Color", "NodeSocketColor"),
("Alpha", "NodeSocketFloat"),
("Bump Scale", "NodeSocketFloat"),
]
normal_group = not depth_plane
if normal_group:
input_sockets.insert(2, ("Normal", "NodeSocketColor"))
input_sockets.insert(3, ("Normal Scale", "NodeSocketFloat"))
input_sockets.insert(4, ("Object Space", "NodeSocketBool"))
if depth_plane:
input_sockets.insert(2, ("Depth", "NodeSocketFloat"))
for name, socket_type in input_sockets:
socket = interface.new_socket(
name=name,
in_out="INPUT",
socket_type=socket_type,
)
if name == "Alpha":
socket.default_value = 1.0
socket.min_value = 0.0
socket.max_value = 1.0
socket.subtype = "FACTOR"
elif name == "Bump Scale":
socket.default_value = 0.0
socket.min_value = -10000.0
socket.max_value = 10000.0
elif name == "Normal":
socket.default_value = (0.5, 0.5, 1.0, 1.0)
elif name == "Normal Scale":
socket.default_value = 1.0
socket.min_value = 0.0
socket.max_value = 10.0
elif name == "Object Space":
socket.default_value = False
output_sockets = [
("Color", "NodeSocketColor"),
("Roughness", "NodeSocketFloat"),
("Alpha", "NodeSocketFloat"),
("Normal", "NodeSocketVector"),
]
if depth_plane:
output_sockets.append(("Displacement", "NodeSocketVector"))
for name, socket_type in output_sockets:
interface.new_socket(
name=name,
in_out="OUTPUT",
socket_type=socket_type,
)
nodes = group.nodes
links = group.links
group_input = nodes.new("NodeGroupInput")
group_input.location = (-500.0, 100.0)
group_output = nodes.new("NodeGroupOutput")
group_output.location = (300.0, 100.0)
roughness_curve = nodes.new("ShaderNodeFloatCurve")
roughness_curve.mapping.initialize()
curve_points = roughness_curve.mapping.curves[0].points
curve_points[0].location = (0.0, 0.5)
curve_points[-1].location = (1.0, 0.1)
roughness_curve.mapping.update()
roughness_curve.location = (-250.0, 350.0)
bump = nodes.new("ShaderNodeBump")
bump.location = (100.0, 0.0)
if normal_group:
tangent_normal = nodes.new("ShaderNodeNormalMap")
tangent_normal.space = "TANGENT"
tangent_normal.location = (-500.0, -100.0)
object_normal = nodes.new("ShaderNodeNormalMap")
object_normal.space = "OBJECT"
object_normal.location = (-500.0, -350.0)
choose_normal = nodes.new("ShaderNodeMix")
choose_normal.data_type = "VECTOR"
choose_normal.location = (-150.0, -150.0)
for normal_map in (tangent_normal, object_normal):
links.new(group_input.outputs["Normal"], normal_map.inputs["Color"])
links.new(
group_input.outputs["Normal Scale"],
normal_map.inputs["Strength"],
)
links.new(group_input.outputs["Object Space"], choose_normal.inputs[0])
links.new(tangent_normal.outputs["Normal"], choose_normal.inputs[4])
links.new(object_normal.outputs["Normal"], choose_normal.inputs[5])
links.new(choose_normal.outputs["Result"], bump.inputs["Normal"])
displacement = None
processed_alpha = group_input.outputs["Alpha"]
bump_depth = group_input.outputs["Color"]
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_output.location = (1050.0, 100.0)
roughness_curve.location = (-300.0, 450.0)
bump.location = (750.0, 50.0)
displacement.location = (750.0, -300.0)
background_attribute = nodes.new("ShaderNodeAttribute")
background_attribute.attribute_type = "GEOMETRY"
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
background_attribute.location = (-950.0, -500.0)
material_attribute = nodes.new("ShaderNodeAttribute")
material_attribute.attribute_type = "GEOMETRY"
material_attribute.attribute_name = MATERIAL_DISPLACEMENT_ATTRIBUTE
material_attribute.location = (-50.0, -650.0)
strength_attribute = nodes.new("ShaderNodeAttribute")
strength_attribute.attribute_type = "GEOMETRY"
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
strength_attribute.location = (200.0, -650.0)
foreground_mask = nodes.new("ShaderNodeMath")
foreground_mask.operation = "GREATER_THAN"
foreground_mask.inputs[1].default_value = 0.5
foreground_mask.location = (-950.0, -250.0)
clamp_foreground = nodes.new("ShaderNodeMath")
clamp_foreground.operation = "MAXIMUM"
clamp_foreground.location = (-650.0, -400.0)
masked_depth = nodes.new("ShaderNodeMix")
masked_depth.data_type = "FLOAT"
masked_depth.location = (-350.0, -350.0)
enabled_depth = nodes.new("ShaderNodeMath")
enabled_depth.operation = "MULTIPLY"
enabled_depth.location = (250.0, -350.0)
scaled_depth = nodes.new("ShaderNodeMath")
scaled_depth.operation = "MULTIPLY"
scaled_depth.location = (450.0, -350.0)
links.new(group_input.outputs["Alpha"], foreground_mask.inputs[0])
links.new(group_input.outputs["Depth"], clamp_foreground.inputs[0])
links.new(background_attribute.outputs["Fac"], clamp_foreground.inputs[1])
links.new(foreground_mask.outputs[0], masked_depth.inputs[0])
links.new(background_attribute.outputs["Fac"], masked_depth.inputs[2])
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
links.new(masked_depth.outputs["Result"], enabled_depth.inputs[0])
links.new(material_attribute.outputs["Fac"], enabled_depth.inputs[1])
links.new(enabled_depth.outputs[0], scaled_depth.inputs[0])
links.new(strength_attribute.outputs["Fac"], scaled_depth.inputs[1])
processed_alpha = foreground_mask.outputs[0]
processed_depth = scaled_depth.outputs[0]
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
links.new(group_input.outputs["Color"], roughness_curve.inputs["Value"])
links.new(
roughness_curve.outputs["Value"],
group_output.inputs["Roughness"],
)
links.new(processed_alpha, group_output.inputs["Alpha"])
links.new(bump_depth, bump.inputs["Height"])
bump_distance = nodes.new("ShaderNodeMath")
bump_distance.operation = "MULTIPLY"
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
bump_distance.location = (500.0, 100.0) if depth_plane else (-250.0, 150.0)
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
if displacement is not None:
links.new(processed_depth, displacement.inputs["Height"])
links.new(
displacement.outputs["Displacement"],
group_output.inputs["Displacement"],
)
group.color_tag = "SHADER"
group.description = (
"Prepare image depth material outputs."
if depth_plane
else f"Prepare {group_name} material outputs."
)
group.use_fake_user = True
return group
return load_asset_node_group(group_name)
def create_image_material(

View File

@@ -1,198 +1,18 @@
"""Runtime Geometry Nodes for Depth Plane."""
import bpy
"""Asset-backed Geometry Nodes for Plane conversion."""
from ..common.mesh_result import load_asset_node_group
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
def mesh_plane_node_group():
return load_asset_node_group(MESH_PLANE_NODE_GROUP_NAME)
def _interface_socket(group, name, in_out, socket_type):
socket = group.interface.new_socket(
name=name,
in_out=in_out,
socket_type=socket_type,
)
if in_out == "INPUT" and hasattr(socket, "structure_type"):
socket.structure_type = "SINGLE"
return socket
def _group_input(nodes, location, visible_names):
node = nodes.new("NodeGroupInput")
node.location = location
visible = set(visible_names)
for output in node.outputs:
output.hide = output.name not in visible
return node
def depth_plane_node_group():
group_name = "O Image Depth Plane"
group = bpy.data.node_groups.get(group_name)
if group is not None:
return group
if group is None:
group = bpy.data.node_groups.new(group_name, "GeometryNodeTree")
else:
group.nodes.clear()
group.interface.clear()
group.is_modifier = True
_interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
subdivide = _interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
subdivide.default_value = 7
subdivide.min_value = 0
subdivide.max_value = 10
thickness = _interface_socket(group, "Thickness", "INPUT", "NodeSocketFloat")
thickness.default_value = 0.1
thickness.min_value = 0.0
thickness.subtype = "DISTANCE"
depth_amount = _interface_socket(group, "Depth Amount", "INPUT", "NodeSocketFloat")
depth_amount.default_value = 1.0
depth_amount.min_value = 0.0
reference_depth = _interface_socket(
group, "Reference Depth", "INPUT", "NodeSocketFloat"
)
reference_depth.hide_in_modifier = True
uniform_scale = _interface_socket(
group, "Uniform Scale", "INPUT", "NodeSocketFloat"
)
uniform_scale.hide_in_modifier = True
depth_image = _interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage")
depth_image.hide_in_modifier = True
material = _interface_socket(group, "Material", "INPUT", "NodeSocketMaterial")
material.hide_in_modifier = True
_interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
nodes = group.nodes
links = group.links
geometry_input = _group_input(
nodes,
(-1850.0, 450.0),
{"Geometry", "Subdivide", "Material"},
)
shape_input = _group_input(
nodes,
(-1250.0, 850.0),
{"Thickness", "Depth Amount"},
)
image_input = _group_input(
nodes,
(-1050.0, 50.0),
{"Depth Image", "Reference Depth", "Uniform Scale"},
)
cleanup_input = _group_input(nodes, (650.0, -200.0), {"Thickness"})
group_output = nodes.new("NodeGroupOutput")
group_output.location = (1850.0, 450.0)
mesh_plane = nodes.new("GeometryNodeGroup")
mesh_plane.node_tree = mesh_plane_node_group()
mesh_plane.inputs["Thickness"].default_value = 0.0
mesh_plane.location = (-1550.0, 450.0)
links.new(geometry_input.outputs["Geometry"], mesh_plane.inputs["Geometry"])
links.new(geometry_input.outputs["Subdivide"], mesh_plane.inputs["Subdivide"])
links.new(geometry_input.outputs["Material"], mesh_plane.inputs["Material"])
extrude = nodes.new("GeometryNodeExtrudeMesh")
extrude.mode = "FACES"
extrude.inputs["Offset"].default_value = (0.0, 0.0, -1.0)
extrude.inputs["Individual"].default_value = False
extrude.location = (-1200.0, 450.0)
links.new(mesh_plane.outputs["Geometry"], extrude.inputs["Mesh"])
links.new(shape_input.outputs["Thickness"], extrude.inputs["Offset Scale"])
geometry = extrude.outputs["Mesh"]
uv = nodes.new("GeometryNodeInputNamedAttribute")
uv.data_type = "FLOAT_VECTOR"
uv.inputs["Name"].default_value = "UVMap"
uv.location = (-1000.0, 50.0)
texture = nodes.new("GeometryNodeImageTexture")
texture.location = (-800.0, 50.0)
links.new(image_input.outputs["Depth Image"], texture.inputs["Image"])
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
separate = nodes.new("ShaderNodeSeparateXYZ")
separate.location = (-600.0, 50.0)
links.new(texture.outputs["Color"], separate.inputs["Vector"])
relative_depth = nodes.new("ShaderNodeMath")
relative_depth.operation = "SUBTRACT"
relative_depth.location = (-600.0, -150.0)
links.new(image_input.outputs["Reference Depth"], relative_depth.inputs[0])
links.new(separate.outputs["X"], relative_depth.inputs[1])
local_depth = nodes.new("ShaderNodeMath")
local_depth.operation = "MULTIPLY"
local_depth.location = (-350.0, -150.0)
links.new(relative_depth.outputs[0], local_depth.inputs[0])
links.new(image_input.outputs["Uniform Scale"], local_depth.inputs[1])
raw_depth = nodes.new("ShaderNodeMath")
raw_depth.operation = "MULTIPLY"
raw_depth.location = (-100.0, 150.0)
links.new(local_depth.outputs[0], raw_depth.inputs[0])
links.new(shape_input.outputs["Depth Amount"], raw_depth.inputs[1])
inward_limit = nodes.new("ShaderNodeMath")
inward_limit.operation = "MULTIPLY"
inward_limit.inputs[1].default_value = -0.98
inward_limit.location = (-100.0, -50.0)
links.new(shape_input.outputs["Thickness"], inward_limit.inputs[0])
clamp = nodes.new("ShaderNodeMath")
clamp.operation = "MAXIMUM"
clamp.location = (150.0, 50.0)
links.new(raw_depth.outputs[0], clamp.inputs[0])
links.new(inward_limit.outputs[0], clamp.inputs[1])
has_thickness = nodes.new("ShaderNodeMath")
has_thickness.operation = "GREATER_THAN"
has_thickness.inputs[1].default_value = 1e-6
has_thickness.location = (-100.0, -350.0)
links.new(shape_input.outputs["Thickness"], has_thickness.inputs[0])
choose_depth = nodes.new("GeometryNodeSwitch")
choose_depth.input_type = "FLOAT"
choose_depth.location = (400.0, 50.0)
links.new(has_thickness.outputs[0], choose_depth.inputs["Switch"])
links.new(raw_depth.outputs[0], choose_depth.inputs["False"])
links.new(clamp.outputs[0], choose_depth.inputs["True"])
combine = nodes.new("ShaderNodeCombineXYZ")
combine.location = (600.0, 50.0)
links.new(choose_depth.outputs["Output"], combine.inputs["Z"])
front = nodes.new("FunctionNodeBooleanMath")
front.operation = "NOT"
front.location = (400.0, 300.0)
links.new(extrude.outputs["Top"], front.inputs[0])
set_position = nodes.new("GeometryNodeSetPosition")
set_position.location = (800.0, 450.0)
links.new(geometry, set_position.inputs["Geometry"])
links.new(front.outputs[0], set_position.inputs["Selection"])
links.new(combine.outputs["Vector"], set_position.inputs["Offset"])
geometry = set_position.outputs["Geometry"]
zero_thickness = nodes.new("ShaderNodeMath")
zero_thickness.operation = "LESS_THAN"
zero_thickness.inputs[1].default_value = 1e-6
zero_thickness.location = (1000.0, -150.0)
links.new(cleanup_input.outputs["Thickness"], zero_thickness.inputs[0])
back_or_side = nodes.new("FunctionNodeBooleanMath")
back_or_side.operation = "OR"
back_or_side.location = (1000.0, 150.0)
links.new(extrude.outputs["Top"], back_or_side.inputs[0])
links.new(extrude.outputs["Side"], back_or_side.inputs[1])
delete_selection = nodes.new("FunctionNodeBooleanMath")
delete_selection.operation = "AND"
delete_selection.location = (1200.0, 50.0)
links.new(zero_thickness.outputs[0], delete_selection.inputs[0])
links.new(back_or_side.outputs[0], delete_selection.inputs[1])
delete_geometry = nodes.new("GeometryNodeDeleteGeometry")
delete_geometry.domain = "FACE"
delete_geometry.location = (1350.0, 450.0)
links.new(geometry, delete_geometry.inputs["Geometry"])
links.new(delete_selection.outputs[0], delete_geometry.inputs["Selection"])
shade_smooth = nodes.new("GeometryNodeSetShadeSmooth")
shade_smooth.domain = "FACE"
shade_smooth.inputs["Shade Smooth"].default_value = True
shade_smooth.location = (1600.0, 450.0)
links.new(delete_geometry.outputs["Geometry"], shade_smooth.inputs["Geometry"])
links.new(shade_smooth.outputs["Geometry"], group_output.inputs["Geometry"])
group.use_fake_user = True
return group
return load_asset_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
def plane_node_group(shape):

View File

@@ -10,6 +10,7 @@ from ..common.image_data import (
)
from ..common.depth_data import camera_depth_values, depth_uniform_scale
from ..common.mesh_result import (
IMAGE_MATERIAL_NODE_GROUP_NAME,
create_image_material,
finalize_object_result,
load_asset_node_group,
@@ -30,6 +31,7 @@ from .geometry import (
)
from .shape import (
BALLOON_ATTRIBUTE_NAME,
BALLOON_CUTOUT_SHAPES,
CUTOUT_NODE_GROUP_NAME,
CUTOUT_SHAPE_INDICES,
DEPTH_CUTOUT_SHAPES,
@@ -124,6 +126,24 @@ def load_cutout_node_group():
return load_asset_node_group(CUTOUT_NODE_GROUP_NAME)
def _connect_balloon_normal_scale(material):
nodes = material.node_tree.nodes
image_layer = next(
node
for node in nodes
if node.bl_idname == "ShaderNodeGroup"
and node.node_tree is not None
and node.node_tree.name == IMAGE_MATERIAL_NODE_GROUP_NAME
)
attribute = nodes.new("ShaderNodeAttribute")
attribute.attribute_type = "GEOMETRY"
attribute.attribute_name = BALLOON_ATTRIBUTE_NAME
attribute.location = (-550.0, -250.0)
material.node_tree.links.new(
attribute.outputs["Fac"], image_layer.inputs["Normal Scale"]
)
def create_shape_object(
context,
source_object,
@@ -178,6 +198,8 @@ def create_shape_object(
scene=context.scene,
)
depth_reference = None
if shape in BALLOON_CUTOUT_SHAPES:
_connect_balloon_normal_scale(material)
uniform_scale = None
depth_direction = None
if shape in DEPTH_CUTOUT_SHAPES:

View File

@@ -19,3 +19,4 @@ CUTOUT_SHAPE_INDICES = {
}
CUTOUT_SHAPES = frozenset(CUTOUT_NODE_GROUP_NAMES)
DEPTH_CUTOUT_SHAPES = frozenset({"DEPTH_BALLOON", "DEPTH_SURFACE"})
BALLOON_CUTOUT_SHAPES = frozenset({"BALLOON", "DEPTH_BALLOON"})

View File

@@ -495,6 +495,23 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
profile = np.empty(len(result.data.vertices), dtype=np.float32)
profile_attribute.data.foreach_get("value", profile)
assert profile.min() == 0.0 and profile.max() > 0.0
material_tree = result.data.materials[0].node_tree
normal_scale_attribute = next(
node
for node in material_tree.nodes
if node.bl_idname == "ShaderNodeAttribute"
)
assert normal_scale_attribute.attribute_type == "GEOMETRY"
assert normal_scale_attribute.attribute_name == shapes.BALLOON_ATTRIBUTE_NAME
image_layer = _group_node(material.material_node_group(), material_tree)
normal_scale_link = next(
link
for link in material_tree.links
if link.to_node == image_layer
and link.to_socket == image_layer.inputs["Normal Scale"]
)
assert normal_scale_link.from_node == normal_scale_attribute
assert normal_scale_link.from_socket == normal_scale_attribute.outputs["Fac"]
assert result.modifiers[0].node_group.name == shapes.CUTOUT_NODE_GROUP_NAME
inward = result.matrix_world.to_3x3() @ Vector((1.0, 0.0, 0.0))
assert np.allclose(tuple(inward), (0.0, 0.0, -1.0), atol=1e-6)
@@ -523,6 +540,10 @@ def test_teddy_preference_writes_the_shared_balloon_attribute():
profile = np.empty(len(result.data.vertices), dtype=np.float32)
attribute.data.foreach_get("value", profile)
assert profile.min() == 0.0 and profile.max() > 0.0
assert not any(
node.bl_idname == "ShaderNodeAttribute"
for node in result.data.materials[0].node_tree.nodes
)
def test_cutout_material_uses_ior_1_2():
@@ -1228,3 +1249,38 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
assert np.allclose(
copied[:, :, 3], np.flipud(rgba[top:bottom, left:right, 3])
)
def test_balloon_normal_scale_uses_existing_image_layer_input():
bpy.ops.wm.read_factory_settings(use_empty=True)
assert shapes.BALLOON_CUTOUT_SHAPES == frozenset(
{"BALLOON", "DEPTH_BALLOON"}
)
group = bpy.data.node_groups.new(
material.IMAGE_MATERIAL_NODE_GROUP_NAME,
"ShaderNodeTree",
)
group.interface.new_socket(
name="Normal Scale",
in_out="INPUT",
socket_type="NodeSocketFloat",
)
image_material = bpy.data.materials.new("Balloon")
image_material.use_nodes = True
image_material.node_tree.nodes.clear()
image_layer = image_material.node_tree.nodes.new("ShaderNodeGroup")
image_layer.node_tree = group
cutout_object._connect_balloon_normal_scale(image_material)
assert _inputs(group) == ["Normal Scale"]
attribute = next(
node
for node in image_material.node_tree.nodes
if node != image_layer
)
assert attribute.bl_idname == "ShaderNodeAttribute"
assert attribute.attribute_name == shapes.BALLOON_ATTRIBUTE_NAME
link = next(iter(image_material.node_tree.links))
assert link.from_socket == attribute.outputs["Fac"]
assert link.to_socket == image_layer.inputs["Normal Scale"]

32
tests/test_node_assets.py Normal file
View File

@@ -0,0 +1,32 @@
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
class NodeAssetTest(unittest.TestCase):
def test_production_code_does_not_create_node_groups(self):
offenders = []
for path in SOURCE_ROOT.rglob("*.py"):
if "bpy.data.node_groups.new" in path.read_text(encoding="utf-8"):
offenders.append(path.relative_to(PROJECT_ROOT).as_posix())
self.assertEqual(offenders, [])
def test_runtime_node_group_entries_load_assets(self):
material_source = (
SOURCE_ROOT / "operators" / "common" / "mesh_result.py"
).read_text(encoding="utf-8")
plane_source = (
SOURCE_ROOT / "operators" / "convert_to_plane" / "nodes.py"
).read_text(encoding="utf-8")
self.assertIn("return load_asset_node_group(group_name)", material_source)
self.assertIn(
"return load_asset_node_group(DEPTH_PLANE_NODE_GROUP_NAME)",
plane_source,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -17,6 +17,8 @@ from build_cutout import (
build_cutout_modifier_group,
build_node_group,
)
from build_depth_plane import build_depth_plane_group
from build_material_layers import build_material_layer_group
from build_mesh_plane import build_mesh_plane_group
from build_shadeless import build_shadeless_group
OUTPUT_PATH = (
@@ -31,7 +33,10 @@ OUTPUT_PATH = (
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.context.preferences.filepaths.save_version = 0
build_mesh_plane_group()
mesh_plane = build_mesh_plane_group()
build_depth_plane_group(mesh_plane)
build_material_layer_group()
build_material_layer_group(depth_plane=True)
build_shadeless_group()
shape_groups = {
shape: build_node_group(shape)

View File

@@ -0,0 +1,190 @@
"""Build the Depth Plane Geometry Nodes group."""
import bpy
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
def _interface_socket(group, name, in_out, socket_type):
socket = group.interface.new_socket(
name=name,
in_out=in_out,
socket_type=socket_type,
)
if in_out == "INPUT" and hasattr(socket, "structure_type"):
socket.structure_type = "SINGLE"
return socket
def _group_input(nodes, location, visible_names):
node = nodes.new("NodeGroupInput")
node.location = location
visible = set(visible_names)
for output in node.outputs:
output.hide = output.name not in visible
return node
def build_depth_plane_group(mesh_plane_group):
group = bpy.data.node_groups.get(DEPTH_PLANE_NODE_GROUP_NAME)
if group is not None:
return group
group = bpy.data.node_groups.new(
DEPTH_PLANE_NODE_GROUP_NAME,
"GeometryNodeTree",
)
group.is_modifier = True
_interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
subdivide = _interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
subdivide.default_value = 7
subdivide.min_value = 0
subdivide.max_value = 10
thickness = _interface_socket(group, "Thickness", "INPUT", "NodeSocketFloat")
thickness.default_value = 0.1
thickness.min_value = 0.0
thickness.subtype = "DISTANCE"
depth_amount = _interface_socket(group, "Depth Amount", "INPUT", "NodeSocketFloat")
depth_amount.default_value = 1.0
depth_amount.min_value = 0.0
reference_depth = _interface_socket(
group, "Reference Depth", "INPUT", "NodeSocketFloat"
)
reference_depth.hide_in_modifier = True
uniform_scale = _interface_socket(
group, "Uniform Scale", "INPUT", "NodeSocketFloat"
)
uniform_scale.hide_in_modifier = True
depth_image = _interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage")
depth_image.hide_in_modifier = True
material = _interface_socket(group, "Material", "INPUT", "NodeSocketMaterial")
material.hide_in_modifier = True
_interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
nodes = group.nodes
links = group.links
geometry_input = _group_input(
nodes,
(-1850.0, 450.0),
{"Geometry", "Subdivide", "Material"},
)
shape_input = _group_input(
nodes,
(-1250.0, 850.0),
{"Thickness", "Depth Amount"},
)
image_input = _group_input(
nodes,
(-1050.0, 50.0),
{"Depth Image", "Reference Depth", "Uniform Scale"},
)
cleanup_input = _group_input(nodes, (650.0, -200.0), {"Thickness"})
group_output = nodes.new("NodeGroupOutput")
group_output.location = (1850.0, 450.0)
mesh_plane = nodes.new("GeometryNodeGroup")
mesh_plane.node_tree = mesh_plane_group
mesh_plane.inputs["Thickness"].default_value = 0.0
mesh_plane.location = (-1550.0, 450.0)
links.new(geometry_input.outputs["Geometry"], mesh_plane.inputs["Geometry"])
links.new(geometry_input.outputs["Subdivide"], mesh_plane.inputs["Subdivide"])
links.new(geometry_input.outputs["Material"], mesh_plane.inputs["Material"])
extrude = nodes.new("GeometryNodeExtrudeMesh")
extrude.mode = "FACES"
extrude.inputs["Offset"].default_value = (0.0, 0.0, -1.0)
extrude.inputs["Individual"].default_value = False
extrude.location = (-1200.0, 450.0)
links.new(mesh_plane.outputs["Geometry"], extrude.inputs["Mesh"])
links.new(shape_input.outputs["Thickness"], extrude.inputs["Offset Scale"])
geometry = extrude.outputs["Mesh"]
uv = nodes.new("GeometryNodeInputNamedAttribute")
uv.data_type = "FLOAT_VECTOR"
uv.inputs["Name"].default_value = "UVMap"
uv.location = (-1000.0, 50.0)
texture = nodes.new("GeometryNodeImageTexture")
texture.location = (-800.0, 50.0)
links.new(image_input.outputs["Depth Image"], texture.inputs["Image"])
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
separate = nodes.new("ShaderNodeSeparateXYZ")
separate.location = (-600.0, 50.0)
links.new(texture.outputs["Color"], separate.inputs["Vector"])
relative_depth = nodes.new("ShaderNodeMath")
relative_depth.operation = "SUBTRACT"
relative_depth.location = (-600.0, -150.0)
links.new(image_input.outputs["Reference Depth"], relative_depth.inputs[0])
links.new(separate.outputs["X"], relative_depth.inputs[1])
local_depth = nodes.new("ShaderNodeMath")
local_depth.operation = "MULTIPLY"
local_depth.location = (-350.0, -150.0)
links.new(relative_depth.outputs[0], local_depth.inputs[0])
links.new(image_input.outputs["Uniform Scale"], local_depth.inputs[1])
raw_depth = nodes.new("ShaderNodeMath")
raw_depth.operation = "MULTIPLY"
raw_depth.location = (-100.0, 150.0)
links.new(local_depth.outputs[0], raw_depth.inputs[0])
links.new(shape_input.outputs["Depth Amount"], raw_depth.inputs[1])
inward_limit = nodes.new("ShaderNodeMath")
inward_limit.operation = "MULTIPLY"
inward_limit.inputs[1].default_value = -0.98
inward_limit.location = (-100.0, -50.0)
links.new(shape_input.outputs["Thickness"], inward_limit.inputs[0])
clamp = nodes.new("ShaderNodeMath")
clamp.operation = "MAXIMUM"
clamp.location = (150.0, 50.0)
links.new(raw_depth.outputs[0], clamp.inputs[0])
links.new(inward_limit.outputs[0], clamp.inputs[1])
has_thickness = nodes.new("ShaderNodeMath")
has_thickness.operation = "GREATER_THAN"
has_thickness.inputs[1].default_value = 1e-6
has_thickness.location = (-100.0, -350.0)
links.new(shape_input.outputs["Thickness"], has_thickness.inputs[0])
choose_depth = nodes.new("GeometryNodeSwitch")
choose_depth.input_type = "FLOAT"
choose_depth.location = (400.0, 50.0)
links.new(has_thickness.outputs[0], choose_depth.inputs["Switch"])
links.new(raw_depth.outputs[0], choose_depth.inputs["False"])
links.new(clamp.outputs[0], choose_depth.inputs["True"])
combine = nodes.new("ShaderNodeCombineXYZ")
combine.location = (600.0, 50.0)
links.new(choose_depth.outputs["Output"], combine.inputs["Z"])
front = nodes.new("FunctionNodeBooleanMath")
front.operation = "NOT"
front.location = (400.0, 300.0)
links.new(extrude.outputs["Top"], front.inputs[0])
set_position = nodes.new("GeometryNodeSetPosition")
set_position.location = (800.0, 450.0)
links.new(geometry, set_position.inputs["Geometry"])
links.new(front.outputs[0], set_position.inputs["Selection"])
links.new(combine.outputs["Vector"], set_position.inputs["Offset"])
geometry = set_position.outputs["Geometry"]
zero_thickness = nodes.new("ShaderNodeMath")
zero_thickness.operation = "LESS_THAN"
zero_thickness.inputs[1].default_value = 1e-6
zero_thickness.location = (1000.0, -150.0)
links.new(cleanup_input.outputs["Thickness"], zero_thickness.inputs[0])
back_or_side = nodes.new("FunctionNodeBooleanMath")
back_or_side.operation = "OR"
back_or_side.location = (1000.0, 150.0)
links.new(extrude.outputs["Top"], back_or_side.inputs[0])
links.new(extrude.outputs["Side"], back_or_side.inputs[1])
delete_selection = nodes.new("FunctionNodeBooleanMath")
delete_selection.operation = "AND"
delete_selection.location = (1200.0, 50.0)
links.new(zero_thickness.outputs[0], delete_selection.inputs[0])
links.new(back_or_side.outputs[0], delete_selection.inputs[1])
delete_geometry = nodes.new("GeometryNodeDeleteGeometry")
delete_geometry.domain = "FACE"
delete_geometry.location = (1350.0, 450.0)
links.new(geometry, delete_geometry.inputs["Geometry"])
links.new(delete_selection.outputs[0], delete_geometry.inputs["Selection"])
shade_smooth = nodes.new("GeometryNodeSetShadeSmooth")
shade_smooth.domain = "FACE"
shade_smooth.inputs["Shade Smooth"].default_value = True
shade_smooth.location = (1600.0, 450.0)
links.new(delete_geometry.outputs["Geometry"], shade_smooth.inputs["Geometry"])
links.new(shade_smooth.outputs["Geometry"], group_output.inputs["Geometry"])
group.use_fake_user = True
return group

View File

@@ -0,0 +1,195 @@
"""Build the image material Shader node groups."""
import bpy
from anyimage.operators.common.mesh_result import (
DEPTH_MATERIAL_NODE_GROUP_NAME,
IMAGE_MATERIAL_NODE_GROUP_NAME,
)
BUMP_BASE_DISTANCE = 0.001
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
def build_material_layer_group(depth_plane=False):
group_name = (
DEPTH_MATERIAL_NODE_GROUP_NAME
if depth_plane
else IMAGE_MATERIAL_NODE_GROUP_NAME
)
existing = bpy.data.node_groups.get(group_name)
if existing is not None:
return existing
group = bpy.data.node_groups.new(group_name, "ShaderNodeTree")
interface = group.interface
input_sockets = [
("Color", "NodeSocketColor"),
("Alpha", "NodeSocketFloat"),
("Bump Scale", "NodeSocketFloat"),
]
normal_group = not depth_plane
if normal_group:
input_sockets.insert(2, ("Normal", "NodeSocketColor"))
input_sockets.insert(3, ("Normal Scale", "NodeSocketFloat"))
input_sockets.insert(4, ("Object Space", "NodeSocketBool"))
if depth_plane:
input_sockets.insert(2, ("Depth", "NodeSocketFloat"))
for name, socket_type in input_sockets:
socket = interface.new_socket(
name=name,
in_out="INPUT",
socket_type=socket_type,
)
if name == "Alpha":
socket.default_value = 1.0
socket.min_value = 0.0
socket.max_value = 1.0
socket.subtype = "FACTOR"
elif name == "Bump Scale":
socket.default_value = 0.0
socket.min_value = -10000.0
socket.max_value = 10000.0
elif name == "Normal":
socket.default_value = (0.5, 0.5, 1.0, 1.0)
elif name == "Normal Scale":
socket.default_value = 1.0
socket.min_value = 0.0
socket.max_value = 10.0
elif name == "Object Space":
socket.default_value = False
output_sockets = [
("Color", "NodeSocketColor"),
("Roughness", "NodeSocketFloat"),
("Alpha", "NodeSocketFloat"),
("Normal", "NodeSocketVector"),
]
if depth_plane:
output_sockets.append(("Displacement", "NodeSocketVector"))
for name, socket_type in output_sockets:
interface.new_socket(
name=name,
in_out="OUTPUT",
socket_type=socket_type,
)
nodes = group.nodes
links = group.links
group_input = nodes.new("NodeGroupInput")
group_input.location = (-500.0, 100.0)
group_output = nodes.new("NodeGroupOutput")
group_output.location = (300.0, 100.0)
roughness_curve = nodes.new("ShaderNodeFloatCurve")
roughness_curve.mapping.initialize()
curve_points = roughness_curve.mapping.curves[0].points
curve_points[0].location = (0.0, 0.5)
curve_points[-1].location = (1.0, 0.1)
roughness_curve.mapping.update()
roughness_curve.location = (-250.0, 350.0)
bump = nodes.new("ShaderNodeBump")
bump.location = (100.0, 0.0)
if normal_group:
tangent_normal = nodes.new("ShaderNodeNormalMap")
tangent_normal.space = "TANGENT"
tangent_normal.location = (-500.0, -100.0)
object_normal = nodes.new("ShaderNodeNormalMap")
object_normal.space = "OBJECT"
object_normal.location = (-500.0, -350.0)
choose_normal = nodes.new("ShaderNodeMix")
choose_normal.data_type = "VECTOR"
choose_normal.location = (-150.0, -150.0)
for normal_map in (tangent_normal, object_normal):
links.new(group_input.outputs["Normal"], normal_map.inputs["Color"])
links.new(
group_input.outputs["Normal Scale"],
normal_map.inputs["Strength"],
)
links.new(group_input.outputs["Object Space"], choose_normal.inputs[0])
links.new(tangent_normal.outputs["Normal"], choose_normal.inputs[4])
links.new(object_normal.outputs["Normal"], choose_normal.inputs[5])
links.new(choose_normal.outputs["Result"], bump.inputs["Normal"])
displacement = None
processed_alpha = group_input.outputs["Alpha"]
bump_depth = group_input.outputs["Color"]
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_output.location = (1050.0, 100.0)
roughness_curve.location = (-300.0, 450.0)
bump.location = (750.0, 50.0)
displacement.location = (750.0, -300.0)
background_attribute = nodes.new("ShaderNodeAttribute")
background_attribute.attribute_type = "GEOMETRY"
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
background_attribute.location = (-950.0, -500.0)
material_attribute = nodes.new("ShaderNodeAttribute")
material_attribute.attribute_type = "GEOMETRY"
material_attribute.attribute_name = MATERIAL_DISPLACEMENT_ATTRIBUTE
material_attribute.location = (-50.0, -650.0)
strength_attribute = nodes.new("ShaderNodeAttribute")
strength_attribute.attribute_type = "GEOMETRY"
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
strength_attribute.location = (200.0, -650.0)
foreground_mask = nodes.new("ShaderNodeMath")
foreground_mask.operation = "GREATER_THAN"
foreground_mask.inputs[1].default_value = 0.5
foreground_mask.location = (-950.0, -250.0)
clamp_foreground = nodes.new("ShaderNodeMath")
clamp_foreground.operation = "MAXIMUM"
clamp_foreground.location = (-650.0, -400.0)
masked_depth = nodes.new("ShaderNodeMix")
masked_depth.data_type = "FLOAT"
masked_depth.location = (-350.0, -350.0)
enabled_depth = nodes.new("ShaderNodeMath")
enabled_depth.operation = "MULTIPLY"
enabled_depth.location = (250.0, -350.0)
scaled_depth = nodes.new("ShaderNodeMath")
scaled_depth.operation = "MULTIPLY"
scaled_depth.location = (450.0, -350.0)
links.new(group_input.outputs["Alpha"], foreground_mask.inputs[0])
links.new(group_input.outputs["Depth"], clamp_foreground.inputs[0])
links.new(background_attribute.outputs["Fac"], clamp_foreground.inputs[1])
links.new(foreground_mask.outputs[0], masked_depth.inputs[0])
links.new(background_attribute.outputs["Fac"], masked_depth.inputs[2])
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
links.new(masked_depth.outputs["Result"], enabled_depth.inputs[0])
links.new(material_attribute.outputs["Fac"], enabled_depth.inputs[1])
links.new(enabled_depth.outputs[0], scaled_depth.inputs[0])
links.new(strength_attribute.outputs["Fac"], scaled_depth.inputs[1])
processed_alpha = foreground_mask.outputs[0]
processed_depth = scaled_depth.outputs[0]
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
links.new(group_input.outputs["Color"], roughness_curve.inputs["Value"])
links.new(
roughness_curve.outputs["Value"],
group_output.inputs["Roughness"],
)
links.new(processed_alpha, group_output.inputs["Alpha"])
links.new(bump_depth, bump.inputs["Height"])
bump_distance = nodes.new("ShaderNodeMath")
bump_distance.operation = "MULTIPLY"
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
bump_distance.location = (500.0, 100.0) if depth_plane else (-250.0, 150.0)
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
if displacement is not None:
links.new(processed_depth, displacement.inputs["Height"])
links.new(
displacement.outputs["Displacement"],
group_output.inputs["Displacement"],
)
group.color_tag = "SHADER"
group.description = (
"Prepare image depth material outputs."
if depth_plane
else f"Prepare {group_name} material outputs."
)
group.use_fake_user = True
return group

View File

@@ -11,8 +11,12 @@ sys.path.insert(
)
from anyimage.operators.common.mesh_result import (
DEPTH_MATERIAL_NODE_GROUP_NAME,
IMAGE_MATERIAL_NODE_GROUP_NAME,
load_asset_node_group,
node_asset_path,
)
from anyimage.operators.convert_to_plane.nodes import DEPTH_PLANE_NODE_GROUP_NAME
from anyimage.operators.cutout_tool.shape import (
CUTOUT_NODE_GROUP_NAME,
CUTOUT_NODE_GROUP_NAMES,
@@ -30,11 +34,25 @@ def input_names(group):
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh_plane = load_asset_node_group("O Mesh Plane")
depth_plane = load_asset_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
image_layer = load_asset_node_group(IMAGE_MATERIAL_NODE_GROUP_NAME)
depth_layer = load_asset_node_group(DEPTH_MATERIAL_NODE_GROUP_NAME)
shadeless = load_asset_node_group("O Shadeless")
cutout = load_asset_node_group(CUTOUT_NODE_GROUP_NAME)
cutout_groups = {
name: bpy.data.node_groups[name] for name in CUTOUT_NODE_GROUP_NAMES.values()
}
expected_names = {
"O Mesh Plane",
DEPTH_PLANE_NODE_GROUP_NAME,
IMAGE_MATERIAL_NODE_GROUP_NAME,
DEPTH_MATERIAL_NODE_GROUP_NAME,
"O Shadeless",
CUTOUT_NODE_GROUP_NAME,
*CUTOUT_NODE_GROUP_NAMES.values(),
}
with bpy.data.libraries.load(str(node_asset_path()), link=False) as (source, _):
assert set(source.node_groups) == expected_names
assert mesh_plane.name == "O Mesh Plane"
assert mesh_plane.bl_idname == "GeometryNodeTree"
@@ -45,6 +63,34 @@ def main():
"Thickness",
"Material",
]
assert depth_plane.bl_idname == "GeometryNodeTree"
assert depth_plane.is_modifier
assert input_names(depth_plane) == [
"Geometry",
"Subdivide",
"Thickness",
"Depth Amount",
"Reference Depth",
"Uniform Scale",
"Depth Image",
"Material",
]
assert image_layer.bl_idname == "ShaderNodeTree"
assert input_names(image_layer) == [
"Color",
"Alpha",
"Normal",
"Normal Scale",
"Object Space",
"Bump Scale",
]
assert depth_layer.bl_idname == "ShaderNodeTree"
assert input_names(depth_layer) == [
"Color",
"Alpha",
"Depth",
"Bump Scale",
]
assert shadeless.name == "O Shadeless"
assert shadeless.bl_idname == "ShaderNodeTree"
assert input_names(shadeless) == ["Color"]
@@ -59,7 +105,14 @@ def main():
assert not group.is_modifier
assert input_names(group)[0] == "Geometry"
public_groups = {mesh_plane, shadeless, cutout}
public_groups = {
mesh_plane,
depth_plane,
image_layer,
depth_layer,
shadeless,
cutout,
}
loaded = {*public_groups, *cutout_groups.values()}
assert all(group.use_fake_user for group in public_groups)
assert all(group.users > 0 for group in cutout_groups.values())