feat: Add new Geometry Nodes groups for image relief and shadeless shader
- Implemented the "O Image Relief Plane" Geometry Nodes group to create a fixed-base image relief using depth images and various parameters. - Created the "O Shadeless" shader node group to display color without scene lighting while preserving shadows and reflections. - Introduced a preview feature for node groups, allowing for interactive visualization of node arrangements in an HTML format. - Added a template HTML file for rendering node previews with customizable views and controls.
This commit is contained in:
223
tools/nodes/groups/image_cutout.py
Normal file
223
tools/nodes/groups/image_cutout.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""Build O Image Cutout."""
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME
|
||||
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.cutout import _extrude_layers, _island_cleanup, _offset_geometry_z, _profile_field, _remove_zero_thickness, _set_layer_z, _shade_output
|
||||
|
||||
|
||||
NODE_GROUP_NAME = "O Image Cutout"
|
||||
|
||||
|
||||
def _set_segmented_layer_z(
|
||||
group,
|
||||
geometry,
|
||||
front_z,
|
||||
back_z,
|
||||
thickness,
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
|
||||
edge_vertices = nodes.new("GeometryNodeInputMeshEdgeVertices")
|
||||
edge_length = nodes.new("ShaderNodeVectorMath")
|
||||
edge_length.operation = "DISTANCE"
|
||||
links.new(edge_vertices.outputs["Position 1"], edge_length.inputs[0])
|
||||
links.new(edge_vertices.outputs["Position 2"], edge_length.inputs[1])
|
||||
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
boundary = math_node(nodes, "COMPARE")
|
||||
boundary.inputs[1].default_value = 1.0
|
||||
boundary.inputs[2].default_value = 0.1
|
||||
links.new(edge_neighbors.outputs["Face Count"], boundary.inputs[0])
|
||||
statistics = nodes.new("GeometryNodeAttributeStatistic")
|
||||
statistics.data_type = "FLOAT"
|
||||
statistics.domain = "EDGE"
|
||||
links.new(geometry, statistics.inputs["Geometry"])
|
||||
links.new(boundary.outputs[0], statistics.inputs["Selection"])
|
||||
links.new(edge_length.outputs["Value"], statistics.inputs["Attribute"])
|
||||
safe_edge = math_node(nodes, "MAXIMUM")
|
||||
safe_edge.inputs[1].default_value = 1e-6
|
||||
links.new(statistics.outputs["Median"], safe_edge.inputs[0])
|
||||
segment_ratio = math_node(nodes, "DIVIDE")
|
||||
links.new(thickness, segment_ratio.inputs[0])
|
||||
links.new(safe_edge.outputs[0], segment_ratio.inputs[1])
|
||||
segment_count = nodes.new("FunctionNodeFloatToInt")
|
||||
segment_count.rounding_mode = "CEILING"
|
||||
links.new(segment_ratio.outputs[0], segment_count.inputs["Float"])
|
||||
at_least_one = nodes.new("FunctionNodeIntegerMath")
|
||||
at_least_one.operation = "MAXIMUM"
|
||||
at_least_one.inputs[1].default_value = 1
|
||||
links.new(segment_count.outputs["Integer"], at_least_one.inputs[0])
|
||||
segment_limit = nodes.new("FunctionNodeIntegerMath")
|
||||
segment_limit.operation = "MINIMUM"
|
||||
segment_limit.inputs[1].default_value = 128
|
||||
links.new(at_least_one.outputs[0], segment_limit.inputs[0])
|
||||
step = math_node(nodes, "DIVIDE")
|
||||
links.new(thickness, step.inputs[0])
|
||||
links.new(segment_limit.outputs[0], step.inputs[1])
|
||||
|
||||
front = _offset_geometry_z(group, geometry, front_z)
|
||||
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
||||
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
||||
repeat_input.pair_with_output(repeat_output)
|
||||
repeat_output.repeat_items.new("GEOMETRY", "Geometry")
|
||||
repeat_output.repeat_items.new("GEOMETRY", "Sides")
|
||||
links.new(segment_limit.outputs[0], repeat_input.inputs["Iterations"])
|
||||
links.new(geometry, repeat_input.inputs["Geometry"])
|
||||
negative_step = math_node(nodes, "MULTIPLY")
|
||||
negative_step.inputs[1].default_value = -1.0
|
||||
links.new(step.outputs[0], negative_step.inputs[0])
|
||||
extrude = nodes.new("GeometryNodeExtrudeMesh")
|
||||
extrude.mode = "FACES"
|
||||
extrude.inputs["Individual"].default_value = False
|
||||
extrude.inputs["Selection"].default_value = True
|
||||
extrude.inputs["Offset"].default_value = (0.0, 0.0, 0.0)
|
||||
links.new(negative_step.outputs[0], extrude.inputs["Offset Scale"])
|
||||
links.new(repeat_input.outputs["Geometry"], extrude.inputs["Mesh"])
|
||||
split_extrude = nodes.new("GeometryNodeSeparateGeometry")
|
||||
split_extrude.domain = "FACE"
|
||||
links.new(extrude.outputs["Mesh"], split_extrude.inputs["Geometry"])
|
||||
links.new(extrude.outputs["Side"], split_extrude.inputs["Selection"])
|
||||
join_sides = nodes.new("GeometryNodeJoinGeometry")
|
||||
links.new(repeat_input.outputs["Sides"], join_sides.inputs["Geometry"])
|
||||
links.new(split_extrude.outputs["Selection"], join_sides.inputs["Geometry"])
|
||||
links.new(split_extrude.outputs["Inverted"], repeat_output.inputs["Geometry"])
|
||||
links.new(join_sides.outputs["Geometry"], repeat_output.inputs["Sides"])
|
||||
|
||||
positioned_side = _offset_geometry_z(
|
||||
group,
|
||||
repeat_output.outputs["Sides"],
|
||||
front_z,
|
||||
)
|
||||
flip_side = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(positioned_side, flip_side.inputs["Mesh"])
|
||||
|
||||
back = _offset_geometry_z(group, geometry, back_z)
|
||||
flip_back = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(back, flip_back.inputs["Mesh"])
|
||||
join = nodes.new("GeometryNodeJoinGeometry")
|
||||
links.new(front, join.inputs["Geometry"])
|
||||
links.new(flip_back.outputs["Mesh"], join.inputs["Geometry"])
|
||||
links.new(flip_side.outputs["Mesh"], join.inputs["Geometry"])
|
||||
merge = nodes.new("GeometryNodeMergeByDistance")
|
||||
merge.mode = "ALL"
|
||||
merge.inputs["Distance"].default_value = 1e-6
|
||||
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
||||
return merge.outputs["Geometry"]
|
||||
|
||||
|
||||
def _build_cutout(group, geometry, thickness):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
half = math_node(nodes, "MULTIPLY")
|
||||
half.inputs[1].default_value = 0.5
|
||||
links.new(thickness, half.inputs[0])
|
||||
back = math_node(nodes, "MULTIPLY")
|
||||
back.inputs[1].default_value = -1.0
|
||||
links.new(half.outputs[0], back.inputs[0])
|
||||
shaped = _set_segmented_layer_z(
|
||||
group,
|
||||
geometry,
|
||||
half.outputs[0],
|
||||
back.outputs[0],
|
||||
thickness,
|
||||
)
|
||||
return _remove_zero_thickness(group, geometry, shaped, thickness)
|
||||
|
||||
|
||||
def _build_balloon(group, geometry, thickness):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
profile, _profile_factor = _profile_field(group, geometry)
|
||||
front_scale = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, front_scale.inputs[0])
|
||||
links.new(thickness, front_scale.inputs[1])
|
||||
back = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, back.inputs[0])
|
||||
links.new(thickness, back.inputs[1])
|
||||
negative_back = math_node(nodes, "MULTIPLY")
|
||||
negative_back.inputs[1].default_value = -1.0
|
||||
links.new(back.outputs[0], negative_back.inputs[0])
|
||||
extrude = _extrude_layers(group, geometry)
|
||||
shaped = _set_layer_z(
|
||||
group,
|
||||
geometry,
|
||||
extrude,
|
||||
front_scale.outputs[0],
|
||||
negative_back.outputs[0],
|
||||
)
|
||||
return _remove_zero_thickness(group, geometry, shaped, thickness)
|
||||
|
||||
|
||||
def build_image_cutout_group():
|
||||
name = NODE_GROUP_NAME
|
||||
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
|
||||
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
||||
interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu')
|
||||
float_input(group, 'Thickness', 1.0).description = 'Balloon profile multiplier; zero returns the sheet'
|
||||
float_input(group, 'Uniform Thickness', 0.0, subtype='DISTANCE').description = 'Distance used only by Uniform thickness mode'
|
||||
iterations = interface_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)
|
||||
float_input(group, 'Smooth Weight', 1.0, maximum=1, subtype='FACTOR', parent=options)
|
||||
data = group.interface.new_panel(name='Data', default_closed=True)
|
||||
interface_socket(group, 'Inward Axis', 'INPUT', 'NodeSocketMenu', data).hide_in_modifier = True
|
||||
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
||||
controls = group_input(group.nodes, set())
|
||||
balloon = _build_balloon(group, controls.outputs['Geometry'], controls.outputs['Thickness'])
|
||||
uniform = _build_cutout(group, controls.outputs['Geometry'], controls.outputs['Uniform Thickness'])
|
||||
choose = menu_switch(group, 'Mode', ('Balloon', 'Uniform'), 'GEOMETRY')
|
||||
group.links.new(balloon, choose.inputs['Balloon'])
|
||||
group.links.new(uniform, choose.inputs['Uniform'])
|
||||
geometry = choose.outputs[0]
|
||||
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, {'Geometry', 'Cleanup Threshold'})
|
||||
area = math_node(group.nodes, 'MULTIPLY')
|
||||
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])
|
||||
for target in planar_inputs:
|
||||
group.links.new(cleaned, target)
|
||||
geometry = expand_smooth(group, geometry, iterations_name='Smooth', weight_name='Smooth Weight')
|
||||
profile = read_float_attribute(group, 'o_balloon')
|
||||
amount = group_input(group.nodes, {'Thickness'})
|
||||
enabled = math_node(group.nodes, 'GREATER_THAN')
|
||||
enabled.inputs[1].default_value = 1e-06
|
||||
group.links.new(amount.outputs['Thickness'], enabled.inputs[0])
|
||||
inflated = group.nodes.new('GeometryNodeSwitch')
|
||||
inflated.input_type = 'FLOAT'
|
||||
inflated.inputs['False'].default_value = 1
|
||||
group.links.new(enabled.outputs[0], inflated.inputs['Switch'])
|
||||
group.links.new(profile, inflated.inputs['True'])
|
||||
mode = menu_switch(group, 'Mode', ('Balloon', 'Uniform'), 'FLOAT')
|
||||
mode.inputs['Uniform'].default_value = 1
|
||||
group.links.new(inflated.outputs[0], mode.inputs['Balloon'])
|
||||
scale = mode.outputs[0]
|
||||
geometry = store_float_attribute(group, geometry, NORMAL_SCALE_ATTRIBUTE_NAME, scale)
|
||||
orient = group.nodes.new('GeometryNodeTransform')
|
||||
orient.inputs['Rotation'].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
|
||||
group.links.new(geometry, orient.inputs['Geometry'])
|
||||
axis = menu_switch(group, 'Inward Axis', ('-Z', '+X'), 'GEOMETRY')
|
||||
group.links.new(geometry, axis.inputs['-Z'])
|
||||
group.links.new(orient.outputs[0], axis.inputs['+X'])
|
||||
_shade_output(group, axis.outputs[0])
|
||||
prepare_node_group(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
|
||||
Reference in New Issue
Block a user