- 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.
253 lines
13 KiB
Python
253 lines
13 KiB
Python
"""Build the three directly editable Cutout modifier assets."""
|
|
import bpy
|
|
from types import SimpleNamespace
|
|
from mathutils import Matrix
|
|
|
|
from anyimage.operators.cutout_tool.shape import CUTOUT_NODE_GROUP_NAMES, CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME
|
|
from cutout_shapes import (
|
|
_socket, _float_input, _bool_input, _vector_input, _group_input, _math,
|
|
_island_cleanup, _build_cutout, _build_balloon, _build_depth_balloon,
|
|
_build_depth_surface, _shade_output,
|
|
)
|
|
from cutout_shape_layouts import LAYOUTS
|
|
from depth_cutout_layout import POSITIONS, INPUT_REGIONS, layout_geometry_references
|
|
from cutout_layout import layout_stage, finish_layout
|
|
from cutout_volume import build_volume_fields, shift_stage
|
|
from cutout_rounding import build_volume_rounding
|
|
from cutout_inflation import build_front_fields
|
|
from cutout_shell import build_front_normals, build_shell_position
|
|
from node_graph import expand_smooth
|
|
|
|
|
|
def _menu(group, control, names, kind, location):
|
|
selector = next((node for node in group.nodes if node.bl_idname == "GeometryNodeMenuSwitch" and node.inputs["Menu"].is_linked and node.inputs["Menu"].links[0].from_socket.name == control), None)
|
|
if selector is None:
|
|
selector = group.nodes.new("GeometryNodeMenuSwitch")
|
|
selector.data_type = "BOOLEAN"
|
|
selector.location = (location[0] - 240, location[1] + 450)
|
|
for item, name in zip(selector.enum_items, names):
|
|
item.name = name
|
|
selector.inputs[names[0]].default_value = False
|
|
selector.inputs[names[1]].default_value = True
|
|
local = _group_input(group.nodes, (location[0] - 480, location[1] + 450), {control})
|
|
group.links.new(local.outputs[control], selector.inputs["Menu"])
|
|
node = group.nodes.new("GeometryNodeSwitch")
|
|
node.input_type = kind
|
|
node.location = location
|
|
group.links.new(selector.outputs[0], node.inputs["Switch"])
|
|
return SimpleNamespace(inputs={names[0]: node.inputs["False"], names[1]: node.inputs["True"]}, outputs=node.outputs)
|
|
|
|
|
|
def _read(group, name, location):
|
|
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
|
node.data_type = "FLOAT"
|
|
node.inputs["Name"].default_value = name
|
|
node.location = location
|
|
return node.outputs["Attribute"]
|
|
|
|
|
|
def _store(group, geometry, name, value, location, domain="POINT"):
|
|
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
|
node.domain, node.data_type = domain, "FLOAT"
|
|
node.location = location
|
|
node.inputs["Name"].default_value = name
|
|
group.links.new(geometry, node.inputs["Geometry"])
|
|
if isinstance(value, (int, float)):
|
|
node.inputs["Value"].default_value = value
|
|
else:
|
|
group.links.new(value, node.inputs["Value"])
|
|
return node.outputs["Geometry"]
|
|
|
|
|
|
def _depth_volume(group, stage):
|
|
"""Insert the volume field ahead of identity-based shell construction."""
|
|
links = group.links
|
|
projection = stage["Set Position"].outputs["Geometry"]
|
|
shell_input = stage["Store Named Attribute.005"].inputs["Geometry"]
|
|
for node in group.nodes:
|
|
if node.location.x >= 4150:
|
|
node.location.x += 5100
|
|
projection = build_front_normals(group, projection)
|
|
volume = build_volume_fields(group, projection, (4150, 0))
|
|
shared = build_front_fields(group, volume.node.inputs["Geometry"].links[0].from_socket)
|
|
links.new(shared, volume.node.inputs["Geometry"])
|
|
uniform_control = _group_input(group.nodes, (8210, -330), {"Uniform Thickness"})
|
|
uniform = _store(group, shared, "o_balloon_surface_thickness", uniform_control.outputs["Uniform Thickness"], (8490, -330))
|
|
volume.node.location.x = uniform.node.location.x
|
|
choose = _menu(group, "Mode", ("Balloon", "Uniform"), "GEOMETRY", (9000, 0))
|
|
links.new(volume, choose.inputs["Balloon"])
|
|
links.new(uniform, choose.inputs["Uniform"])
|
|
links.new(choose.outputs[0], shell_input)
|
|
thickness = _read(group, "o_balloon_surface_thickness", (12000, -1100))
|
|
links.new(thickness, stage["Math.018"].inputs[1])
|
|
control = _group_input(group.nodes, (13300, 700), {"Thickness", "Uniform Thickness"})
|
|
amount = _menu(group, "Mode", ("Balloon", "Uniform"), "FLOAT", (13600, 500))
|
|
links.new(control.outputs["Thickness"], amount.inputs["Balloon"])
|
|
links.new(control.outputs["Uniform Thickness"], amount.inputs["Uniform"])
|
|
links.new(amount.outputs[0], stage["Math.019"].inputs[0])
|
|
identity_thickness = _read(group, "o_balloon_surface_thickness", (11100, 1500))
|
|
positive = _math(group.nodes, "GREATER_THAN", (11340, 1500))
|
|
positive.inputs[1].default_value = 1e-12
|
|
links.new(identity_thickness, positive.inputs[0])
|
|
layer = _math(group.nodes, "MULTIPLY", (11570, 1200))
|
|
links.new(positive.outputs[0], layer.inputs[0])
|
|
links.new(stage["Separate XYZ.006"].outputs["Z"], layer.inputs[1])
|
|
links.new(layer.outputs[0], stage["Combine XYZ.002"].inputs["Z"])
|
|
for node in group.nodes:
|
|
if node.location.x >= 12070:
|
|
node.location.x += 400
|
|
for name in ("Flip Faces", "Flip Faces.001"):
|
|
source = stage[name].outputs["Mesh"]
|
|
outgoing = [link for link in links if link.from_socket == source]
|
|
targets = [link.to_socket for link in outgoing]
|
|
for link in outgoing:
|
|
links.remove(link)
|
|
masked = _store(group, source, NORMAL_SCALE_ATTRIBUTE_NAME, 0.0, (12100, stage[name].location.y), domain="FACE")
|
|
for target in targets:
|
|
links.new(masked, target)
|
|
|
|
for node in group.nodes:
|
|
if node.location.x >= 8490:
|
|
node.location.x += 3200
|
|
volume.node.location.x = 8290
|
|
rounded = build_volume_rounding(group, volume, (8600, 0))
|
|
rounded.node.location.x = uniform.node.location.x + 40
|
|
links.new(rounded, choose.inputs["Balloon"])
|
|
restored = stage["Vector Math.004"].outputs[0]
|
|
targets = [link.to_socket for link in restored.links]
|
|
uniform_mode = choose.outputs[0].node.inputs["Switch"].links[0].from_socket
|
|
oriented = build_shell_position(group, restored, uniform_mode, (14100, -2100))
|
|
for target in targets:
|
|
links.new(oriented, target)
|
|
|
|
|
|
def build_cutout_group(shape):
|
|
name = CUTOUT_NODE_GROUP_NAMES[shape]
|
|
existing = bpy.data.node_groups.get(name)
|
|
if existing is not None:
|
|
return existing
|
|
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
|
|
group.is_modifier, group.use_fake_user = True, True
|
|
group.color_tag = "GEOMETRY"
|
|
group["anyimage_asset_revision"] = 11
|
|
depth = shape in {"DEPTH_SOLID", "DEPTH_BALLOON"}
|
|
dual = shape != "DEPTH_BALLOON"
|
|
_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
|
if dual:
|
|
_socket(group, "Mode", "INPUT", "NodeSocketMenu")
|
|
_float_input(group, "Thickness", 1.0).description = "Balloon profile multiplier; zero returns the sheet"
|
|
if dual:
|
|
_float_input(group, "Uniform Thickness", 0.0, subtype="DISTANCE").description = "Distance used only by Uniform thickness mode"
|
|
if depth:
|
|
_float_input(group, "Depth Scale", 1.0)
|
|
if shape == "DEPTH_BALLOON":
|
|
_bool_input(group, "Double Sided", True)
|
|
iterations = _socket(group, "Smooth", "INPUT", "NodeSocketInt")
|
|
iterations.default_value = 0
|
|
iterations.min_value, iterations.max_value = 0, 20
|
|
options = group.interface.new_panel(name="Options", default_closed=True)
|
|
_float_input(group, "Cleanup Threshold", 0.0, parent=options)
|
|
if shape == "DEPTH_SOLID":
|
|
_float_input(group, "Split Threshold", 0.0, maximum=1, subtype="FACTOR", parent=options)
|
|
if depth:
|
|
_float_input(group, "Reference Depth", 1.0, subtype="DISTANCE", parent=options)
|
|
if shape == "DEPTH_SOLID":
|
|
_float_input(group, "Edge Round", 0.2, maximum=1, subtype="FACTOR", parent=options)
|
|
_float_input(group, "Front Inflation", 1.0, maximum=32, parent=options)
|
|
normal_iterations = _socket(group, "Normal Smooth", "INPUT", "NodeSocketInt", options)
|
|
normal_iterations.default_value = 50
|
|
normal_iterations.min_value, normal_iterations.max_value = 0, 50
|
|
_float_input(group, "Smooth Weight", 1.0, maximum=1, subtype="FACTOR", parent=options)
|
|
if shape == "DEPTH_BALLOON":
|
|
_vector_input(group, "Depth Direction", (0, 0, 1), subtype="DIRECTION", parent=options)
|
|
data = group.interface.new_panel(name="Data", default_closed=True)
|
|
_socket(group, "Inward Axis", "INPUT", "NodeSocketMenu", data).hide_in_modifier = True
|
|
if depth:
|
|
_float_input(group, "Uniform Scale", 1.0, parent=data).hide_in_modifier = True
|
|
_socket(group, "Depth Image", "INPUT", "NodeSocketImage", data).hide_in_modifier = True
|
|
_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
|
controls = _group_input(group.nodes, (-5000, 0), set())
|
|
previous = set(group.nodes)
|
|
if shape == "DEPTH_SOLID":
|
|
geometry = _build_depth_surface(group, controls.outputs["Geometry"], controls.outputs["Thickness"], controls.outputs["Depth Scale"], controls.outputs["Split Threshold"], controls.outputs["Uniform Scale"], controls.outputs["Reference Depth"], controls.outputs["Depth Image"])
|
|
stage = layout_stage(group, previous, POSITIONS, INPUT_REGIONS)
|
|
_depth_volume(group, stage)
|
|
end = 18000
|
|
elif shape == "DEPTH_BALLOON":
|
|
geometry = _build_depth_balloon(group, controls.outputs["Geometry"], controls.outputs["Thickness"], controls.outputs["Depth Scale"], controls.outputs["Double Sided"], controls.outputs["Uniform Scale"], controls.outputs["Depth Direction"], controls.outputs["Depth Image"], controls.outputs["Reference Depth"])
|
|
layout_stage(group, previous, *LAYOUTS["DEPTH_BALLOON"])
|
|
end = 3000
|
|
else:
|
|
balloon = _build_balloon(group, controls.outputs["Geometry"], controls.outputs["Thickness"])
|
|
layout_stage(group, previous, *LAYOUTS["BALLOON"])
|
|
previous = set(group.nodes)
|
|
uniform = _build_cutout(group, controls.outputs["Geometry"], controls.outputs["Uniform Thickness"])
|
|
layout_stage(group, previous, *LAYOUTS["UNIFORM"], offset=(0, -2400))
|
|
balloon.node.location.x = uniform.node.location.x
|
|
choose = _menu(group, "Mode", ("Balloon", "Uniform"), "GEOMETRY", (3200, 0))
|
|
group.links.new(balloon, choose.inputs["Balloon"])
|
|
group.links.new(uniform, choose.inputs["Uniform"])
|
|
geometry, end = choose.outputs[0], 3700
|
|
planar_inputs = [link.to_socket for link in group.links if link.from_node.bl_idname == "NodeGroupInput" and link.from_socket.name == "Geometry"]
|
|
base = _group_input(group.nodes, (-4800, 0), {"Geometry", "Cleanup Threshold"})
|
|
area = _math(group.nodes, "MULTIPLY", (-4520, 500))
|
|
area.inputs[1].default_value = 0.01
|
|
group.links.new(base.outputs["Cleanup Threshold"], area.inputs[0])
|
|
cleaned = _island_cleanup(group, base.outputs["Geometry"], area.outputs[0], (-4520, 0))
|
|
if shape == "DEPTH_SOLID":
|
|
cleaned = _store(group, cleaned, NORMAL_SCALE_ATTRIBUTE_NAME, 1.0, (-3540, 0), domain="FACE")
|
|
for target in planar_inputs:
|
|
group.links.new(cleaned, target)
|
|
previous = set(group.nodes)
|
|
geometry = expand_smooth(group, geometry, iterations_name="Smooth", weight_name="Smooth Weight")
|
|
shift_stage(group, previous, (end, -550))
|
|
end += 3450
|
|
if shape == "DEPTH_SOLID":
|
|
remove = group.nodes.new("GeometryNodeRemoveAttribute")
|
|
remove.pattern_mode = "WILDCARD"
|
|
remove.inputs["Name"].default_value = "o_balloon_surface_*"
|
|
remove.location = (end, 0)
|
|
group.links.new(geometry, remove.inputs["Geometry"])
|
|
geometry, end = remove.outputs[0], end + 300
|
|
else:
|
|
profile = _read(group, "o_balloon", (end, 800))
|
|
scale = profile
|
|
if dual:
|
|
amount = _group_input(group.nodes, (end, 1100), {"Thickness"})
|
|
enabled = _math(group.nodes, "GREATER_THAN", (end + 250, 1100))
|
|
enabled.inputs[1].default_value = 1e-6
|
|
group.links.new(amount.outputs["Thickness"], enabled.inputs[0])
|
|
inflated = group.nodes.new("GeometryNodeSwitch")
|
|
inflated.input_type = "FLOAT"
|
|
inflated.location = (end + 480, 800)
|
|
inflated.inputs["False"].default_value = 1
|
|
group.links.new(enabled.outputs[0], inflated.inputs["Switch"])
|
|
group.links.new(profile, inflated.inputs["True"])
|
|
mode = _menu(group, "Mode", ("Balloon", "Uniform"), "FLOAT", (end + 750, 800))
|
|
mode.inputs["Uniform"].default_value = 1
|
|
group.links.new(inflated.outputs[0], mode.inputs["Balloon"])
|
|
scale = mode.outputs[0]
|
|
geometry = _store(group, geometry, NORMAL_SCALE_ATTRIBUTE_NAME, scale, (end + 1050, 0))
|
|
end += 1350
|
|
orient = group.nodes.new("GeometryNodeTransform")
|
|
orient.location = (end, -350)
|
|
orient.inputs["Rotation"].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
|
|
group.links.new(geometry, orient.inputs["Geometry"])
|
|
axis = _menu(group, "Inward Axis", ("-Z", "+X"), "GEOMETRY", (end + 300, 0))
|
|
group.links.new(geometry, axis.inputs["-Z"])
|
|
group.links.new(orient.outputs[0], axis.inputs["+X"])
|
|
_shade_output(group, axis.outputs[0], (end + 600, 0))
|
|
if shape == "DEPTH_SOLID":
|
|
layout_geometry_references(group, stage)
|
|
finish_layout(group)
|
|
for item in group.interface.items_tree:
|
|
if item.item_type != "SOCKET" or item.in_out != "INPUT":
|
|
continue
|
|
if item.name == "Mode":
|
|
item.default_value = "Balloon"
|
|
elif item.name == "Inward Axis":
|
|
item.default_value = "-Z"
|
|
elif item.name == "Uniform Thickness":
|
|
item.name = "Thickness"
|
|
return group
|