- Implemented `image_plane.py` to build the Image Plane Geometry Nodes group with customizable parameters for geometry, subdivide, and thickness. - Created `image_plane_validation.py` to validate the functionality of the image plane, ensuring correct geometry generation and material assignment. - Developed `material_layers.py` to construct shader node groups for image materials, including depth and normal handling. - Authored `plane_layouts.py` to define node arrangements for image geometry assets. - Introduced `shadeless.py` to create a shadeless shader node group that displays color without scene lighting while preserving shadows and reflections.
255 lines
11 KiB
Python
255 lines
11 KiB
Python
"""Build the Depth Plane Geometry Nodes group."""
|
|
|
|
import bpy
|
|
|
|
from plane_layouts import layout_geometry
|
|
from cutout_layout import finish_layout
|
|
from depth_plane_camera import build_camera_plane
|
|
from depth_plane_layout import layout_camera_plane
|
|
|
|
|
|
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
|
|
|
|
|
|
def _interface_socket(group, name, in_out, socket_type, parent=None):
|
|
socket = group.interface.new_socket(
|
|
name=name,
|
|
in_out=in_out,
|
|
socket_type=socket_type,
|
|
parent=parent,
|
|
)
|
|
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 _math(nodes, operation, location):
|
|
node = nodes.new("ShaderNodeMath")
|
|
node.operation = operation
|
|
node.location = location
|
|
return node
|
|
|
|
|
|
def build_depth_plane_group(image_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
|
|
geometry = _interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
|
geometry.structure_type = "SINGLE"
|
|
mode = _interface_socket(group, "Mode", "INPUT", "NodeSocketMenu")
|
|
subdivide = _interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
|
|
subdivide.default_value = 6
|
|
subdivide.min_value = 0
|
|
subdivide.max_value = 10
|
|
thickness = _interface_socket(group, "Thickness", "INPUT", "NodeSocketFloat")
|
|
thickness.default_value = 0.0
|
|
thickness.min_value = 0.0
|
|
thickness.subtype = "DISTANCE"
|
|
thickness.description = "Base thickness used only by Relief mode"
|
|
camera_thickness = _interface_socket(group, "Camera Thickness", "INPUT", "NodeSocketFloat")
|
|
camera_thickness.default_value = 0.0
|
|
camera_thickness.min_value = 0.0
|
|
camera_thickness.subtype = "DISTANCE"
|
|
camera_thickness.description = "Inward shell distance used only by Camera mode"
|
|
depth_scale = _interface_socket(group, "Depth Scale", "INPUT", "NodeSocketFloat")
|
|
depth_scale.default_value = 1.0
|
|
depth_scale.min_value = 0.0
|
|
options = group.interface.new_panel(name="Options", default_closed=True)
|
|
base_plane_depth = _interface_socket(
|
|
group,
|
|
"Reference Depth",
|
|
"INPUT",
|
|
"NodeSocketFloat",
|
|
parent=options,
|
|
)
|
|
base_plane_depth.default_value = 1.0
|
|
base_plane_depth.min_value = 0.0
|
|
base_plane_depth.subtype = "DISTANCE"
|
|
normal_smooth = _interface_socket(
|
|
group, "Normal Smooth", "INPUT", "NodeSocketInt", parent=options,
|
|
)
|
|
normal_smooth.default_value, normal_smooth.min_value, normal_smooth.max_value = 50, 0, 50
|
|
normal_smooth.description = "Smooth the Camera shell thickness direction"
|
|
data = group.interface.new_panel(name="Data", default_closed=True)
|
|
uniform_scale = _interface_socket(
|
|
group,
|
|
"Uniform Scale",
|
|
"INPUT",
|
|
"NodeSocketFloat",
|
|
parent=data,
|
|
)
|
|
uniform_scale.default_value = 1.0
|
|
uniform_scale.min_value = 0.0
|
|
uniform_scale.hide_in_modifier = True
|
|
depth_image = _interface_socket(
|
|
group,
|
|
"Depth Image",
|
|
"INPUT",
|
|
"NodeSocketImage",
|
|
parent=data,
|
|
)
|
|
depth_image.hide_in_modifier = True
|
|
_interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
|
|
|
nodes = group.nodes
|
|
links = group.links
|
|
geometry_input = _group_input(
|
|
nodes,
|
|
(-1900.0, 500.0),
|
|
{"Geometry", "Subdivide"},
|
|
)
|
|
shape_input = _group_input(
|
|
nodes,
|
|
(-1550.0, 950.0),
|
|
{"Thickness", "Depth Scale", "Reference Depth"},
|
|
)
|
|
image_input = _group_input(
|
|
nodes,
|
|
(-1200.0, -350.0),
|
|
{"Depth Image", "Uniform Scale"},
|
|
)
|
|
group_output = nodes.new("NodeGroupOutput")
|
|
group_output.location = (1550.0, 500.0)
|
|
image_plane = nodes.new("GeometryNodeGroup")
|
|
image_plane.node_tree = image_plane_group
|
|
image_plane.location = (-1600.0, 500.0)
|
|
links.new(geometry_input.outputs["Geometry"], image_plane.inputs["Geometry"])
|
|
links.new(geometry_input.outputs["Subdivide"], image_plane.inputs["Subdivide"])
|
|
links.new(shape_input.outputs["Thickness"], image_plane.inputs["Thickness"])
|
|
|
|
positive_half = _math(nodes, "MULTIPLY", (-1300.0, 300.0))
|
|
positive_half.inputs[1].default_value = 0.5
|
|
links.new(shape_input.outputs["Thickness"], positive_half.inputs[0])
|
|
translation = nodes.new("ShaderNodeCombineXYZ")
|
|
translation.location = (-1100.0, 300.0)
|
|
links.new(positive_half.outputs[0], translation.inputs["Z"])
|
|
place_block = nodes.new("GeometryNodeTransform")
|
|
place_block.location = (-900.0, 500.0)
|
|
links.new(image_plane.outputs["Geometry"], place_block.inputs["Geometry"])
|
|
links.new(translation.outputs["Vector"], place_block.inputs["Translation"])
|
|
|
|
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
|
uv.data_type = "FLOAT_VECTOR"
|
|
uv.inputs["Name"].default_value = "UVMap"
|
|
uv.location = (-950.0, -350.0)
|
|
texture = nodes.new("GeometryNodeImageTexture")
|
|
texture.interpolation = "Linear"
|
|
texture.extension = "EXTEND"
|
|
texture.location = (-700.0, -350.0)
|
|
links.new(image_input.outputs["Depth Image"], texture.inputs["Image"])
|
|
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
|
|
depth_components = nodes.new("ShaderNodeSeparateXYZ")
|
|
depth_components.location = (-450.0, -350.0)
|
|
links.new(texture.outputs["Color"], depth_components.inputs["Vector"])
|
|
metric_depth = _math(nodes, "MULTIPLY", (-200.0, -350.0))
|
|
links.new(depth_components.outputs["Z"], metric_depth.inputs[0])
|
|
links.new(image_input.outputs["Uniform Scale"], metric_depth.inputs[1])
|
|
relative_depth = _math(nodes, "SUBTRACT", (50.0, -350.0))
|
|
links.new(shape_input.outputs["Reference Depth"], relative_depth.inputs[0])
|
|
links.new(metric_depth.outputs[0], relative_depth.inputs[1])
|
|
full_offset = _math(nodes, "MULTIPLY", (300.0, -350.0))
|
|
links.new(relative_depth.outputs[0], full_offset.inputs[0])
|
|
links.new(shape_input.outputs["Depth Scale"], full_offset.inputs[1])
|
|
|
|
inward_limit = _math(nodes, "MULTIPLY", (50.0, -600.0))
|
|
inward_limit.inputs[1].default_value = -0.999
|
|
links.new(shape_input.outputs["Thickness"], inward_limit.inputs[0])
|
|
limited_offset = _math(nodes, "MAXIMUM", (300.0, -600.0))
|
|
links.new(full_offset.outputs[0], limited_offset.inputs[0])
|
|
links.new(inward_limit.outputs[0], limited_offset.inputs[1])
|
|
has_thickness = _math(nodes, "GREATER_THAN", (50.0, -850.0))
|
|
has_thickness.inputs[1].default_value = 1e-6
|
|
links.new(shape_input.outputs["Thickness"], has_thickness.inputs[0])
|
|
safe_offset = nodes.new("GeometryNodeSwitch")
|
|
safe_offset.input_type = "FLOAT"
|
|
safe_offset.location = (550.0, -450.0)
|
|
links.new(has_thickness.outputs[0], safe_offset.inputs["Switch"])
|
|
links.new(full_offset.outputs[0], safe_offset.inputs["False"])
|
|
links.new(limited_offset.outputs[0], safe_offset.inputs["True"])
|
|
|
|
position = nodes.new("GeometryNodeInputPosition")
|
|
position.location = (-650.0, 50.0)
|
|
position_components = nodes.new("ShaderNodeSeparateXYZ")
|
|
position_components.location = (-400.0, 50.0)
|
|
links.new(position.outputs["Position"], position_components.inputs["Vector"])
|
|
layer_ratio = _math(nodes, "DIVIDE", (-150.0, 50.0))
|
|
links.new(position_components.outputs["Z"], layer_ratio.inputs[0])
|
|
links.new(shape_input.outputs["Thickness"], layer_ratio.inputs[1])
|
|
nonnegative_ratio = _math(nodes, "MAXIMUM", (350.0, 50.0))
|
|
nonnegative_ratio.inputs[1].default_value = 0.0
|
|
links.new(layer_ratio.outputs[0], nonnegative_ratio.inputs[0])
|
|
clamped_ratio = _math(nodes, "MINIMUM", (600.0, 50.0))
|
|
clamped_ratio.inputs[1].default_value = 1.0
|
|
links.new(nonnegative_ratio.outputs[0], clamped_ratio.inputs[0])
|
|
layer_weight = nodes.new("GeometryNodeSwitch")
|
|
layer_weight.input_type = "FLOAT"
|
|
layer_weight.location = (850.0, 50.0)
|
|
layer_weight.inputs["False"].default_value = 1.0
|
|
links.new(has_thickness.outputs[0], layer_weight.inputs["Switch"])
|
|
links.new(clamped_ratio.outputs[0], layer_weight.inputs["True"])
|
|
|
|
weighted_offset = _math(nodes, "MULTIPLY", (850.0, -350.0))
|
|
links.new(safe_offset.outputs["Output"], weighted_offset.inputs[0])
|
|
links.new(layer_weight.outputs["Output"], weighted_offset.inputs[1])
|
|
offset = nodes.new("ShaderNodeCombineXYZ")
|
|
offset.location = (1050.0, -200.0)
|
|
links.new(weighted_offset.outputs[0], offset.inputs["Z"])
|
|
set_position = nodes.new("GeometryNodeSetPosition")
|
|
set_position.location = (1050.0, 500.0)
|
|
links.new(place_block.outputs["Geometry"], set_position.inputs["Geometry"])
|
|
links.new(offset.outputs["Vector"], set_position.inputs["Offset"])
|
|
shade_smooth = nodes.new("GeometryNodeSetShadeSmooth")
|
|
shade_smooth.domain = "FACE"
|
|
shade_smooth.inputs["Shade Smooth"].default_value = True
|
|
shade_smooth.location = (1300.0, 500.0)
|
|
links.new(set_position.outputs["Geometry"], shade_smooth.inputs["Geometry"])
|
|
links.new(shade_smooth.outputs["Geometry"], group_output.inputs["Geometry"])
|
|
group.color_tag = "GEOMETRY"
|
|
group.description = (
|
|
"Build a fixed-base relief or camera-projected image shell."
|
|
)
|
|
group.use_fake_user = True
|
|
layout_geometry(group)
|
|
for node in nodes:
|
|
node.location.x += 3750
|
|
node.location.y += 2400
|
|
previous = set(nodes)
|
|
camera = build_camera_plane(group, image_plane_group)
|
|
layout_camera_plane(group, previous)
|
|
nodes.remove(shade_smooth)
|
|
# Align the Relief and Camera branch ends in menu order.
|
|
set_position.location.x = camera.node.location.x + 40
|
|
selector = nodes.new("GeometryNodeMenuSwitch")
|
|
selector.data_type = "GEOMETRY"
|
|
for item, name in zip(selector.enum_items, ("Relief", "Camera")):
|
|
item.name = name
|
|
selector.location = (6500, 1300)
|
|
controls = _group_input(nodes, (6250, 1550), {"Mode"})
|
|
links.new(controls.outputs["Mode"], selector.inputs["Menu"])
|
|
links.new(set_position.outputs["Geometry"], selector.inputs["Relief"])
|
|
links.new(camera, selector.inputs["Camera"])
|
|
mode.default_value = "Relief"
|
|
smooth = nodes.new("GeometryNodeSetShadeSmooth")
|
|
smooth.domain = "FACE"
|
|
smooth.location = (6750, 1300)
|
|
links.new(selector.outputs[0], smooth.inputs["Geometry"])
|
|
group_output.location = (7000, 1300)
|
|
links.new(smooth.outputs["Geometry"], group_output.inputs["Geometry"])
|
|
finish_layout(group)
|
|
camera_thickness.name = "Thickness"
|
|
return group
|