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:
19
AGENTS.md
19
AGENTS.md
@@ -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 为节点类型生成的默认名称和外观,避免读图时无法判断节点原型。用途应通过节点类型、参数、空间布局和必要的少量注释表达
|
||||
|
||||
|
||||
@@ -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 重复内部算法细节。
|
||||
|
||||
## 开发
|
||||
|
||||
@@ -18,7 +18,7 @@ Depth Plane 将一张静态图片转换为带深度位移的网格,适合快
|
||||
|
||||
## 调整结果
|
||||
|
||||
生成对象使用运行时创建的 `O Image Depth Plane` Geometry Nodes 修改器:
|
||||
生成对象使用扩展资产中的 `O Image Depth Plane` Geometry Nodes 修改器:
|
||||
|
||||
- **Subdivide**:控制基础平面细分级别;
|
||||
- **Thickness**:增加背面和侧壁,`0` 输出单面;
|
||||
|
||||
@@ -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 菜单和四个内部组的依赖关系;
|
||||
|
||||
@@ -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 不建立这条连接。
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
31
mkdocs.yml
31
mkdocs.yml
@@ -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.
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
32
tests/test_node_assets.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
190
tools/node_assets/build_depth_plane.py
Normal file
190
tools/node_assets/build_depth_plane.py
Normal 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
|
||||
|
||||
195
tools/node_assets/build_material_layers.py
Normal file
195
tools/node_assets/build_material_layers.py
Normal 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
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user