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

@@ -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"})