This commit is contained in:
1
nodes/groups/__init__.py
Normal file
1
nodes/groups/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Named node group definitions."""
|
||||
205
nodes/groups/image_cutout.py
Normal file
205
nodes/groups/image_cutout.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""Build O Image Cutout."""
|
||||
import bpy
|
||||
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
from ..common.nodes import (
|
||||
interface_socket, group_input, compare_node, math_node, float_input,
|
||||
menu_switch, read_float_attribute, store_float_attribute, prepare_node_group,
|
||||
)
|
||||
from ..common.cutout import _extrude_layers, _offset_geometry_y, _set_layer_y, _shade_output
|
||||
|
||||
|
||||
NODE_GROUP_NAME = "O Image Cutout"
|
||||
|
||||
|
||||
def _set_segmented_layer_y(
|
||||
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 = compare_node(group, "EQUAL", edge_neighbors.outputs["Face Count"], 1, data_type="INT")
|
||||
statistics = nodes.new("GeometryNodeAttributeStatistic")
|
||||
statistics.data_type = "FLOAT"
|
||||
statistics.domain = "EDGE"
|
||||
links.new(geometry, statistics.inputs["Geometry"])
|
||||
links.new(boundary, 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_y(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_y(
|
||||
group,
|
||||
repeat_output.outputs["Sides"],
|
||||
front_z,
|
||||
)
|
||||
flip_side = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(positioned_side, flip_side.inputs["Mesh"])
|
||||
|
||||
back = _offset_geometry_y(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_shell(group, geometry, thickness):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
zero = compare_node(group, "LESS_THAN", thickness, 1e-6)
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(zero, choose.inputs["Switch"])
|
||||
links.new(geometry, choose.inputs["True"])
|
||||
front = math_node(nodes, "MULTIPLY")
|
||||
front.inputs[1].default_value = 0.0
|
||||
links.new(thickness, front.inputs[0])
|
||||
back = math_node(nodes, "MULTIPLY")
|
||||
back.inputs[1].default_value = 1.0
|
||||
links.new(thickness, back.inputs[0])
|
||||
shaped = _set_segmented_layer_y(
|
||||
group,
|
||||
geometry,
|
||||
front.outputs[0],
|
||||
back.outputs[0],
|
||||
thickness,
|
||||
)
|
||||
links.new(shaped, choose.inputs["False"])
|
||||
return choose.outputs[0]
|
||||
|
||||
|
||||
def _build_balloon(group, geometry, thickness):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
zero = compare_node(group, "LESS_THAN", thickness, 1e-6)
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(zero, choose.inputs["Switch"])
|
||||
links.new(geometry, choose.inputs["True"])
|
||||
profile = read_float_attribute(group, 'o_balloon')
|
||||
front_scale = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, front_scale.inputs[0])
|
||||
links.new(thickness, front_scale.inputs[1])
|
||||
negative_front = math_node(nodes, "MULTIPLY")
|
||||
negative_front.inputs[1].default_value = -1.0
|
||||
links.new(front_scale.outputs[0], negative_front.inputs[0])
|
||||
extrude = _extrude_layers(group, geometry)
|
||||
shaped = _set_layer_y(
|
||||
group,
|
||||
geometry,
|
||||
extrude,
|
||||
negative_front.outputs[0],
|
||||
front_scale.outputs[0],
|
||||
)
|
||||
links.new(shaped, choose.inputs["False"])
|
||||
return choose.outputs[0]
|
||||
|
||||
|
||||
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'
|
||||
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
||||
interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu')
|
||||
float_input(group, 'Thickness', 1.0, maximum=2.0).description = 'Control how much the surface bulges in Balloon mode. Zero removes the bulge.'
|
||||
float_input(group, 'Shell Thickness', 0.0, maximum=1.0, subtype='DISTANCE').description = 'Add thickness behind the original surface while keeping the front surface in place.'
|
||||
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
||||
controls = group_input(group.nodes, set())
|
||||
balloon = _build_balloon(group, controls.outputs['Geometry'], controls.outputs['Thickness'])
|
||||
shell = _build_shell(group, controls.outputs['Geometry'], controls.outputs['Shell Thickness'])
|
||||
choose = menu_switch(group, 'Mode', ('Balloon', 'Shell'), 'GEOMETRY')
|
||||
group.links.new(balloon, choose.inputs['Balloon'])
|
||||
group.links.new(shell, choose.inputs['Shell'])
|
||||
geometry = choose.outputs[0]
|
||||
profile = read_float_attribute(group, 'o_balloon')
|
||||
amount = group_input(group.nodes, {'Thickness'})
|
||||
enabled = compare_node(group, 'GREATER_THAN', amount.outputs['Thickness'], 1e-6)
|
||||
inflated = group.nodes.new('GeometryNodeSwitch')
|
||||
inflated.input_type = 'FLOAT'
|
||||
inflated.inputs['False'].default_value = 1
|
||||
group.links.new(enabled, inflated.inputs['Switch'])
|
||||
group.links.new(profile, inflated.inputs['True'])
|
||||
mode = menu_switch(group, 'Mode', ('Balloon', 'Shell'), 'FLOAT')
|
||||
mode.inputs['Shell'].default_value = 1
|
||||
group.links.new(inflated.outputs[0], mode.inputs['Balloon'])
|
||||
scale = mode.outputs[0]
|
||||
reduction = math_node(group.nodes, 'SUBTRACT')
|
||||
reduction.inputs[0].default_value = 1.0
|
||||
group.links.new(scale, reduction.inputs[1])
|
||||
geometry = store_float_attribute(group, geometry, NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction.outputs[0])
|
||||
_shade_output(group, geometry)
|
||||
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 == 'Shell Thickness':
|
||||
item.name = 'Thickness'
|
||||
return group
|
||||
416
nodes/groups/image_cutout_symmetry.py
Normal file
416
nodes/groups/image_cutout_symmetry.py
Normal file
@@ -0,0 +1,416 @@
|
||||
"""Mirror completed depth geometry and fill matching boundaries."""
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
|
||||
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
|
||||
from ..common.nodes import (
|
||||
interface_socket, group_input, float_input, bool_input, vector_input,
|
||||
compare_node, boolean_node, evaluate_field,
|
||||
store_boolean_attribute, store_float_attribute, read_float_attribute,
|
||||
remove_attribute_pattern, prepare_node_group,
|
||||
)
|
||||
from ..common.depth_surface import _math
|
||||
from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE
|
||||
from ..common.direction import symmetry_legacy_direction_to_canonical
|
||||
from ..common.smoothing import edge_boundary_field, pinned_smooth
|
||||
|
||||
NODE_GROUP_NAME = "O Image Cutout Symmetry"
|
||||
|
||||
# 切口环与补面衔接带各自向外覆盖的圈数。
|
||||
SMOOTH_RINGS = 4
|
||||
|
||||
# 补面标记:挤出之后按面记下补面,作为补面平滑的范围。
|
||||
WELD_ATTRIBUTE = '_o_symmetry_weld'
|
||||
|
||||
|
||||
# 对称面附近按中位边长的倍数降低法线贴图强度。
|
||||
SEAM_NORMAL_BAND = 4
|
||||
|
||||
|
||||
def _align_front(group, geometry, controls):
|
||||
"""Align existing geometry with the fixed XY symmetry plane."""
|
||||
nodes, links = group.nodes, group.links
|
||||
direction = symmetry_legacy_direction_to_canonical(group, controls['Direction'])
|
||||
align = nodes.new('FunctionNodeAlignEulerToVector')
|
||||
align.axis = 'Y'
|
||||
links.new(direction, align.inputs['Vector'])
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
rotate = nodes.new('ShaderNodeVectorRotate')
|
||||
rotate.rotation_type = 'EULER_XYZ'
|
||||
rotate.invert = True
|
||||
links.new(position.outputs[0], rotate.inputs['Vector'])
|
||||
links.new(align.outputs['Rotation'], rotate.inputs['Rotation'])
|
||||
offset = nodes.new('ShaderNodeCombineXYZ')
|
||||
offset_value = nodes.new('ShaderNodeMath')
|
||||
offset_value.operation = 'MULTIPLY'
|
||||
offset_value.inputs[1].default_value = -1.0
|
||||
links.new(controls['Offset'], offset_value.inputs[0])
|
||||
links.new(offset_value.outputs[0], offset.inputs['Y'])
|
||||
offset_position = nodes.new('ShaderNodeVectorMath')
|
||||
offset_position.operation = 'ADD'
|
||||
links.new(rotate.outputs[0], offset_position.inputs[0])
|
||||
links.new(offset.outputs[0], offset_position.inputs[1])
|
||||
scale_vector = nodes.new('ShaderNodeCombineXYZ')
|
||||
scale_vector.inputs['X'].default_value = 1
|
||||
scale_vector.inputs['Z'].default_value = 1
|
||||
links.new(controls['Scale'], scale_vector.inputs['Y'])
|
||||
scale = nodes.new('ShaderNodeVectorMath')
|
||||
scale.operation = 'MULTIPLY'
|
||||
links.new(offset_position.outputs[0], scale.inputs[0])
|
||||
links.new(scale_vector.outputs[0], scale.inputs[1])
|
||||
place = nodes.new('GeometryNodeSetPosition')
|
||||
links.new(geometry, place.inputs['Geometry'])
|
||||
links.new(scale.outputs[0], place.inputs['Position'])
|
||||
store = nodes.new('GeometryNodeStoreNamedAttribute')
|
||||
store.data_type, store.domain = 'FLOAT_VECTOR', 'FACE'
|
||||
store.inputs['Name'].default_value = ROTATION_ATTRIBUTE
|
||||
links.new(place.outputs[0], store.inputs['Geometry'])
|
||||
links.new(align.outputs['Rotation'], store.inputs['Value'])
|
||||
return store.outputs['Geometry']
|
||||
|
||||
|
||||
def _boundary_edge_field(group):
|
||||
"""Report one on edges that carry a single face."""
|
||||
neighbors = group.nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
||||
return compare_node(group, 'EQUAL', neighbors.outputs['Face Count'], 1, data_type='INT')
|
||||
|
||||
|
||||
def _beyond_plane_field(group, distance):
|
||||
"""Report the points sitting farther than the weld distance past the plane."""
|
||||
nodes = group.nodes
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
group.links.new(position.outputs[0], components.inputs[0])
|
||||
return compare_node(group, 'GREATER_THAN', components.outputs['Y'], distance)
|
||||
|
||||
|
||||
def _on_plane_field(group, distance):
|
||||
"""Report the points sitting within the weld distance of the plane."""
|
||||
nodes = group.nodes
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
group.links.new(position.outputs[0], components.inputs[0])
|
||||
return compare_node(group, 'LESS_THAN', _math(group, 'ABSOLUTE', components.outputs['Y']), distance)
|
||||
|
||||
|
||||
def _merge_points(group, geometry, selection, distance):
|
||||
"""Weld the selected points that share a position."""
|
||||
nodes, links = group.nodes, group.links
|
||||
merge = nodes.new('GeometryNodeMergeByDistance')
|
||||
links.new(geometry, merge.inputs['Geometry'])
|
||||
links.new(selection, merge.inputs['Selection'])
|
||||
if isinstance(distance, (int, float)):
|
||||
merge.inputs['Distance'].default_value = distance
|
||||
else:
|
||||
links.new(distance, merge.inputs['Distance'])
|
||||
return merge.outputs[0]
|
||||
|
||||
|
||||
def _delete_faces(group, geometry, selection):
|
||||
"""Delete the selected faces and the geometry only they used."""
|
||||
nodes, links = group.nodes, group.links
|
||||
delete = nodes.new('GeometryNodeDeleteGeometry')
|
||||
delete.domain, delete.mode = 'FACE', 'ALL'
|
||||
links.new(geometry, delete.inputs['Geometry'])
|
||||
links.new(selection, delete.inputs['Selection'])
|
||||
return delete.outputs[0]
|
||||
|
||||
|
||||
def _delete_beyond_plane(group, geometry, beyond):
|
||||
"""Delete the faces whose points all sit beyond the symmetry plane."""
|
||||
# 点域求值让面域选择按「全部点越界」删除,跨界的面连几何一起留给切口回拉。
|
||||
return _delete_faces(group, geometry, evaluate_field(group, beyond, 'BOOLEAN', 'POINT'))
|
||||
|
||||
|
||||
def _corner_edge(group, corner, offset, value):
|
||||
"""Read an edge value at one corner around a face."""
|
||||
nodes, links = group.nodes, group.links
|
||||
shifted = nodes.new('GeometryNodeOffsetCornerInFace')
|
||||
links.new(corner, shifted.inputs['Corner Index'])
|
||||
shifted.inputs['Offset'].default_value = offset
|
||||
edges = nodes.new('GeometryNodeEdgesOfCorner')
|
||||
links.new(shifted.outputs['Corner Index'], edges.inputs['Corner Index'])
|
||||
sample = nodes.new('GeometryNodeFieldAtIndex')
|
||||
sample.data_type, sample.domain = 'BOOLEAN', 'EDGE'
|
||||
links.new(value, sample.inputs['Value'])
|
||||
links.new(edges.outputs['Next Edge Index'], sample.inputs['Index'])
|
||||
return sample.outputs['Value']
|
||||
|
||||
|
||||
def _boundary_edge_count(group):
|
||||
"""Count the edges carrying a single face around each face."""
|
||||
nodes, links = group.nodes, group.links
|
||||
boundary = _boundary_edge_field(group)
|
||||
index = nodes.new('GeometryNodeInputIndex')
|
||||
corners = nodes.new('GeometryNodeCornersOfFace')
|
||||
links.new(index.outputs[0], corners.inputs['Face Index'])
|
||||
# 第四个偏移在三角形上绕回第一条边,按角数把它归零。
|
||||
quad = compare_node(group, 'GREATER_THAN', corners.outputs['Total'], 3, data_type='INT')
|
||||
count = None
|
||||
for offset in range(4):
|
||||
term = _corner_edge(group, corners.outputs['Corner Index'], offset, boundary)
|
||||
if offset == 3:
|
||||
masked = nodes.new('GeometryNodeSwitch')
|
||||
masked.input_type = 'FLOAT'
|
||||
masked.inputs['False'].default_value = 0.0
|
||||
links.new(quad, masked.inputs['Switch'])
|
||||
links.new(term, masked.inputs['True'])
|
||||
term = masked.outputs['Output']
|
||||
count = term if count is None else _math(group, 'ADD', count, term)
|
||||
return count
|
||||
|
||||
|
||||
def _delete_bridge_faces(group, geometry):
|
||||
"""Delete the faces left bridging two openings of a cut."""
|
||||
return _delete_faces(group, geometry, compare_node(group, 'GREATER_THAN', _boundary_edge_count(group), 1.5))
|
||||
|
||||
|
||||
def _delete_flat_faces(group, geometry, distance):
|
||||
"""Delete the faces lying on the symmetry plane; they mirror onto themselves."""
|
||||
nodes, links = group.nodes, group.links
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(position.outputs[0], components.inputs[0])
|
||||
normal = nodes.new('GeometryNodeInputNormal')
|
||||
normal_axes = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(normal.outputs[0], normal_axes.inputs[0])
|
||||
center = evaluate_field(group, _math(group, 'ABSOLUTE', components.outputs['Y']), 'FLOAT', 'FACE')
|
||||
return _delete_faces(
|
||||
group, geometry,
|
||||
boolean_node(
|
||||
group, 'AND',
|
||||
compare_node(group, 'LESS_THAN', center, distance),
|
||||
compare_node(group, 'GREATER_THAN', _math(group, 'ABSOLUTE', normal_axes.outputs['Y']), 1 - 1e-3),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _delete_crease_faces(group, geometry, distance):
|
||||
"""Delete the faces the fold leaves sharing an interior edge on the plane."""
|
||||
nodes, links = group.nodes, group.links
|
||||
heights = _edge_abs_y(group)
|
||||
plane = nodes.new('FunctionNodeBooleanMath')
|
||||
plane.operation = 'AND'
|
||||
links.new(compare_node(group, 'LESS_THAN', heights[0], distance), plane.inputs[0])
|
||||
links.new(compare_node(group, 'LESS_THAN', heights[1], distance), plane.inputs[1])
|
||||
neighbors = nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
||||
crease = nodes.new('FunctionNodeBooleanMath')
|
||||
crease.operation = 'AND'
|
||||
links.new(plane.outputs[0], crease.inputs[0])
|
||||
links.new(compare_node(group, 'EQUAL', neighbors.outputs['Face Count'], 2, data_type='INT'), crease.inputs[1])
|
||||
index = nodes.new('GeometryNodeInputIndex')
|
||||
corners = nodes.new('GeometryNodeCornersOfFace')
|
||||
links.new(index.outputs[0], corners.inputs['Face Index'])
|
||||
selected = None
|
||||
for offset in range(4):
|
||||
term = _corner_edge(group, corners.outputs['Corner Index'], offset, crease.outputs[0])
|
||||
selected = term if selected is None else boolean_node(group, 'OR', selected, term)
|
||||
return _delete_faces(group, geometry, selected)
|
||||
|
||||
|
||||
def _off_plane_edge_field(group, distance):
|
||||
"""Report one on edges that keep an endpoint farther than the weld distance."""
|
||||
heights = _edge_abs_y(group)
|
||||
return compare_node(group, 'GREATER_THAN', _math(group, 'MAXIMUM', heights[0], heights[1]), distance)
|
||||
|
||||
|
||||
def _edge_abs_y(group):
|
||||
"""Return both endpoint heights on the symmetry axis for an edge."""
|
||||
edge = group.nodes.new('GeometryNodeInputMeshEdgeVertices')
|
||||
heights = []
|
||||
for socket in ('Position 1', 'Position 2'):
|
||||
axis = group.nodes.new('ShaderNodeSeparateXYZ')
|
||||
group.links.new(edge.outputs[socket], axis.inputs[0])
|
||||
heights.append(_math(group, 'ABSOLUTE', axis.outputs['Y']))
|
||||
return heights
|
||||
|
||||
|
||||
def _mirror(group, geometry):
|
||||
mirror = group.nodes.new('GeometryNodeTransform')
|
||||
mirror.inputs['Scale'].default_value = (1, -1, 1)
|
||||
group.links.new(geometry, mirror.inputs['Geometry'])
|
||||
flip = group.nodes.new('GeometryNodeFlipFaces')
|
||||
group.links.new(mirror.outputs[0], flip.inputs['Mesh'])
|
||||
return flip.outputs[0]
|
||||
|
||||
|
||||
def _seam_field(group, distance):
|
||||
"""Report the cut ring the folded front leaves on the symmetry plane."""
|
||||
boundary = evaluate_field(group, edge_boundary_field(group.nodes, group.links), 'BOOLEAN', 'POINT')
|
||||
return boolean_node(group, 'AND', boundary, _on_plane_field(group, distance))
|
||||
|
||||
|
||||
def _falloff(group, center, rings):
|
||||
"""Blur a 0/1 center into a falloff over the requested rings."""
|
||||
blur = group.nodes.new('GeometryNodeBlurAttribute')
|
||||
blur.data_type = 'FLOAT'
|
||||
blur.inputs['Iterations'].default_value = rings
|
||||
group.links.new(center, blur.inputs['Value'])
|
||||
return _math(group, 'MAXIMUM', center, blur.outputs[0])
|
||||
|
||||
|
||||
def _snap_to_plane(group, geometry, selection):
|
||||
"""Move the selected points onto the symmetry plane."""
|
||||
nodes, links = group.nodes, group.links
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(position.outputs[0], components.inputs[0])
|
||||
target = nodes.new('ShaderNodeCombineXYZ')
|
||||
links.new(components.outputs['X'], target.inputs['X'])
|
||||
links.new(components.outputs['Z'], target.inputs['Z'])
|
||||
place = nodes.new('GeometryNodeSetPosition')
|
||||
links.new(geometry, place.inputs['Geometry'])
|
||||
links.new(selection, place.inputs['Selection'])
|
||||
links.new(target.outputs[0], place.inputs['Position'])
|
||||
return place.outputs[0]
|
||||
|
||||
|
||||
def _relax_seam(group, geometry, seam, retract, smooth):
|
||||
"""Relax the cut seam in three dimensions and keep the cut on the plane."""
|
||||
relaxed = pinned_smooth(
|
||||
group, geometry, smooth, _falloff(group, seam, SMOOTH_RINGS),
|
||||
pin_boundary=True,
|
||||
)
|
||||
return _snap_to_plane(group, relaxed, boolean_node(group, 'OR', seam, retract))
|
||||
|
||||
|
||||
def _relax_fill(group, geometry, fill, smooth):
|
||||
"""Relax the fill faces on the welded mesh and fade outward from them."""
|
||||
return pinned_smooth(
|
||||
group, geometry, smooth, _falloff(group, fill, SMOOTH_RINGS),
|
||||
weight=0.2, pin_boundary=False,
|
||||
)
|
||||
|
||||
|
||||
def _build_wall(group, front, boundary):
|
||||
"""Close the selected boundary edges onto the symmetry plane and mark the fill."""
|
||||
nodes, links = group.nodes, group.links
|
||||
extrude = nodes.new('GeometryNodeExtrudeMesh')
|
||||
extrude.mode = 'EDGES'
|
||||
extrude.inputs['Offset Scale'].default_value = 0
|
||||
links.new(front, extrude.inputs['Mesh'])
|
||||
links.new(boundary, extrude.inputs['Selection'])
|
||||
geometry = _snap_to_plane(group, extrude.outputs['Mesh'], extrude.outputs['Top'])
|
||||
# 挤出之后再标记补面:范围就是补面本身连同它的边界线,挤出不会改变它。
|
||||
fill = evaluate_field(group, extrude.outputs['Side'], 'BOOLEAN', 'POINT')
|
||||
geometry, fill = store_boolean_attribute(group, geometry, WELD_ATTRIBUTE, fill, domain='FACE')
|
||||
store = nodes.new('GeometryNodeStoreNamedAttribute')
|
||||
store.data_type, store.domain = 'FLOAT', 'FACE'
|
||||
store.inputs['Name'].default_value = FACE_ATTRIBUTE
|
||||
store.inputs['Value'].default_value = 3
|
||||
links.new(geometry, store.inputs['Geometry'])
|
||||
links.new(extrude.outputs['Side'], store.inputs['Selection'])
|
||||
return store.outputs[0], fill
|
||||
|
||||
|
||||
def build_image_cutout_symmetry_group():
|
||||
existing = bpy.data.node_groups.get(NODE_GROUP_NAME)
|
||||
if existing is not None:
|
||||
return existing
|
||||
group = bpy.data.node_groups.new(NODE_GROUP_NAME, 'GeometryNodeTree')
|
||||
group.is_modifier, group.use_fake_user = True, True
|
||||
group.color_tag = 'GEOMETRY'
|
||||
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
||||
vector_input(group, 'Direction', (0, 0, 1), subtype='DIRECTION').description = 'Align this direction with the local symmetry axis.'
|
||||
float_input(group, 'Offset', 0, minimum=-10, maximum=10, subtype='DISTANCE').description = 'Offset geometry along the aligned symmetry axis before scaling.'
|
||||
float_input(group, 'Scale', 1, maximum=2).description = 'Scale the offset geometry along the aligned symmetry axis.'
|
||||
options = group.interface.new_panel(name='Options', default_closed=True)
|
||||
bool_input(group, 'Fill Sides', True, parent=options).description = 'Connect matching front and mirrored boundaries.'
|
||||
smooth = interface_socket(group, 'Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
smooth.default_value, smooth.min_value, smooth.max_value = 4, 0, 16
|
||||
smooth.description = 'Smooth the cut ring and the fill band around the outline and the fill, each over four rings. Zero keeps both raw.'
|
||||
merge = float_input(group, 'Merge Distance', 0.001, minimum=0, subtype='DISTANCE', parent=options)
|
||||
merge.description = 'Weld mirrored geometry within this distance of the symmetry plane; boundaries farther away get filled.'
|
||||
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
||||
nodes, links = group.nodes, group.links
|
||||
inputs = group_input(nodes, set()).outputs
|
||||
aligned = _align_front(group, inputs['Geometry'], inputs)
|
||||
edge = nodes.new('GeometryNodeInputMeshEdgeVertices')
|
||||
edge_length = nodes.new('ShaderNodeVectorMath')
|
||||
edge_length.operation = 'DISTANCE'
|
||||
links.new(edge.outputs['Position 1'], edge_length.inputs[0])
|
||||
links.new(edge.outputs['Position 2'], edge_length.inputs[1])
|
||||
statistics = nodes.new('GeometryNodeAttributeStatistic')
|
||||
statistics.data_type, statistics.domain = 'FLOAT', 'EDGE'
|
||||
links.new(aligned, statistics.inputs['Geometry'])
|
||||
links.new(edge_length.outputs['Value'], statistics.inputs['Attribute'])
|
||||
# 合并距离同时决定平面的贴合带和补面边界,两者一致才不会留下细小补面。
|
||||
merge_distance = _math(group, 'MAXIMUM', inputs['Merge Distance'], 1e-6)
|
||||
beyond = _beyond_plane_field(group, merge_distance)
|
||||
on_plane = _on_plane_field(group, merge_distance)
|
||||
retract = boolean_node(group, 'OR', on_plane, beyond)
|
||||
is_boundary = _boundary_edge_field(group)
|
||||
front = _delete_beyond_plane(group, aligned, beyond)
|
||||
front = _delete_bridge_faces(group, front)
|
||||
# 贴合带内和跨界保留的面都折回对称面,切口环因此落在平面上。
|
||||
front = _snap_to_plane(group, front, retract)
|
||||
seam_field = _seam_field(group, merge_distance)
|
||||
shade = nodes.new('GeometryNodeSetShadeSmooth')
|
||||
shade.domain = 'FACE'
|
||||
links.new(front, shade.inputs['Geometry'])
|
||||
front = store_float_attribute(group, shade.outputs[0], FACE_ATTRIBUTE, 1., domain='FACE')
|
||||
front = _relax_seam(group, front, seam_field, retract, inputs['Smooth'])
|
||||
# 松弛会把点压回平面,贴平的面和内部贴平边要在松弛之后再清一次。
|
||||
front = _delete_flat_faces(group, front, merge_distance)
|
||||
front = _delete_crease_faces(group, front, merge_distance)
|
||||
fill_iterations = nodes.new('GeometryNodeSwitch')
|
||||
fill_iterations.input_type = 'INT'
|
||||
fill_iterations.inputs['False'].default_value = 0
|
||||
links.new(inputs['Fill Sides'], fill_iterations.inputs['Switch'])
|
||||
links.new(inputs['Smooth'], fill_iterations.inputs['True'])
|
||||
# 切口环本身贴在对称面上并靠镜像焊住,只有不落在这条环上的边界边才需要补面。
|
||||
wall = nodes.new('FunctionNodeBooleanMath')
|
||||
wall.operation = 'AND'
|
||||
links.new(is_boundary, wall.inputs[0])
|
||||
links.new(_off_plane_edge_field(group, merge_distance), wall.inputs[1])
|
||||
walled, fill = _build_wall(group, front, wall.outputs[0])
|
||||
wall_shade = nodes.new('GeometryNodeSetShadeSmooth')
|
||||
wall_shade.domain = 'FACE'
|
||||
links.new(compare_node(group, 'EQUAL', read_float_attribute(group, FACE_ATTRIBUTE), 3), wall_shade.inputs['Selection'])
|
||||
links.new(walled, wall_shade.inputs['Geometry'])
|
||||
body = nodes.new('GeometryNodeSwitch')
|
||||
body.input_type = 'GEOMETRY'
|
||||
links.new(inputs['Fill Sides'], body.inputs['Switch'])
|
||||
links.new(front, body.inputs['False'])
|
||||
links.new(wall_shade.outputs[0], body.inputs['True'])
|
||||
mark_back = nodes.new('GeometryNodeStoreNamedAttribute')
|
||||
mark_back.data_type, mark_back.domain = 'FLOAT', 'FACE'
|
||||
mark_back.inputs['Name'].default_value = FACE_ATTRIBUTE
|
||||
mark_back.inputs['Value'].default_value = 2
|
||||
links.new(_mirror(group, body.outputs[0]), mark_back.inputs['Geometry'])
|
||||
links.new(compare_node(group, 'EQUAL', read_float_attribute(group, FACE_ATTRIBUTE), 1), mark_back.inputs['Selection'])
|
||||
join = nodes.new('GeometryNodeJoinGeometry')
|
||||
for geometry in (body.outputs[0], mark_back.outputs['Geometry']):
|
||||
links.new(geometry, join.inputs[0])
|
||||
# 平面附近的点已经折到平面上,接缝两侧位置重合;合并只作用在这条带内。
|
||||
merged = _merge_points(group, join.outputs[0], on_plane, merge_distance)
|
||||
# 先让镜像焊住接缝,再松弛补面和它外侧的衔接带。
|
||||
geometry = _relax_fill(group, merged, fill, fill_iterations.outputs[0])
|
||||
geometry = store_float_attribute(group, geometry, AXIS_ATTRIBUTE, 1., domain='FACE')
|
||||
orient = nodes.new('GeometryNodeTransform')
|
||||
orient.inputs['Rotation'].default_value = Matrix.Rotation(1.5707963267948966, 4, 'Z').to_euler()
|
||||
links.new(geometry, orient.inputs['Geometry'])
|
||||
# Fade the material normal map near the symmetry plane so the weld shades smoother.
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(position.outputs[0], components.inputs[0])
|
||||
ramp = nodes.new('ShaderNodeMapRange')
|
||||
ramp.interpolation_type = 'SMOOTHSTEP'
|
||||
ramp.clamp = True
|
||||
links.new(_math(group, 'ABSOLUTE', components.outputs['X']), ramp.inputs['Value'])
|
||||
ramp.inputs['From Min'].default_value = 0.0
|
||||
links.new(_math(group, 'MULTIPLY', statistics.outputs['Median'], SEAM_NORMAL_BAND), ramp.inputs['From Max'])
|
||||
ramp.inputs['To Min'].default_value = 1.0
|
||||
ramp.inputs['To Max'].default_value = 0.0
|
||||
reduction = _math(
|
||||
group, 'MAXIMUM',
|
||||
read_float_attribute(group, NORMAL_REDUCTION_ATTRIBUTE_NAME), ramp.outputs['Result'],
|
||||
)
|
||||
geometry = store_float_attribute(
|
||||
group, orient.outputs[0], NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction,
|
||||
)
|
||||
geometry = remove_attribute_pattern(group, geometry, '_o_*')
|
||||
links.new(geometry, nodes.new('NodeGroupOutput').inputs['Geometry'])
|
||||
prepare_node_group(group)
|
||||
return group
|
||||
336
nodes/groups/image_depth_cutout.py
Normal file
336
nodes/groups/image_depth_cutout.py
Normal file
@@ -0,0 +1,336 @@
|
||||
"""Build O Image Depth Cutout."""
|
||||
import bpy
|
||||
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
from ..common.nodes import (
|
||||
interface_socket, group_input, float_input, boolean_node, read_float_attribute, read_vector_attribute,
|
||||
store_float_attribute, store_vector_attribute, remove_attribute_pattern,
|
||||
prepare_node_group, evaluate_field, compare_node,
|
||||
)
|
||||
from ..common.boundary_smoothing import smooth_cut_boundary
|
||||
from ..common.cutout import _position_field, _shade_output
|
||||
from ..common.depth_surface import _math, project_depth_surface, build_surface_camera, split_depth_surface, sample_face_camera
|
||||
from ..common.smoothing import edge_boundary_field
|
||||
from ..common.cutout_boundary import remove_boundary_triangles, taper_split_profile
|
||||
|
||||
|
||||
NODE_GROUP_NAME = "O Image Depth Cutout"
|
||||
FRONT_NORMAL_ATTRIBUTE = "_o_front_normal"
|
||||
BALLOON_THICKNESS_BASE = 1.0
|
||||
NORMAL_SMOOTH_ITERATIONS = 256
|
||||
SOLID_WELD_SCALE = 1e-3
|
||||
|
||||
|
||||
def build_normal_weight(group, is_shell, profile):
|
||||
"""Keep shell smoothing and let the balloon profile drive the blend."""
|
||||
weight = group.nodes.new("GeometryNodeSwitch")
|
||||
weight.input_type = "FLOAT"
|
||||
group.links.new(is_shell, weight.inputs["Switch"])
|
||||
group.links.new(profile, weight.inputs["False"])
|
||||
weight.inputs["True"].default_value = 0.0
|
||||
return weight.outputs[0]
|
||||
|
||||
|
||||
def build_profile_weight(group, geometry):
|
||||
"""Normalize the corrected balloon profile into a zero to one blend weight."""
|
||||
profile = read_float_attribute(group, "o_balloon")
|
||||
statistics = group.nodes.new("GeometryNodeAttributeStatistic")
|
||||
statistics.data_type, statistics.domain = "FLOAT", "POINT"
|
||||
group.links.new(geometry, statistics.inputs["Geometry"])
|
||||
group.links.new(profile, statistics.inputs["Attribute"])
|
||||
return _math(
|
||||
group, "DIVIDE", profile,
|
||||
_math(group, "MAXIMUM", statistics.outputs["Max"], 1e-6),
|
||||
)
|
||||
|
||||
|
||||
def build_island_normal(group, normal):
|
||||
"""Average the smoothed normals of one connected mesh island."""
|
||||
nodes, links = group.nodes, group.links
|
||||
island = nodes.new("GeometryNodeInputMeshIsland")
|
||||
accumulate = nodes.new("GeometryNodeAccumulateField")
|
||||
accumulate.data_type, accumulate.domain = "FLOAT_VECTOR", "POINT"
|
||||
links.new(normal, accumulate.inputs["Value"])
|
||||
links.new(island.outputs["Island Index"], accumulate.inputs["Group Index"])
|
||||
normalize = nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
links.new(accumulate.outputs["Total"], normalize.inputs[0])
|
||||
return normalize.outputs["Vector"]
|
||||
|
||||
|
||||
def build_smoothed_normals(group):
|
||||
"""Return the smoothed normals and the average normal of each mesh island."""
|
||||
nodes, links = group.nodes, group.links
|
||||
normal = group.nodes.new("GeometryNodeInputNormal")
|
||||
blur = group.nodes.new("GeometryNodeBlurAttribute")
|
||||
blur.data_type = "FLOAT_VECTOR"
|
||||
links.new(normal.outputs[0], blur.inputs["Value"])
|
||||
blur.inputs["Iterations"].default_value = NORMAL_SMOOTH_ITERATIONS
|
||||
smoothed = blur.outputs[0]
|
||||
return smoothed, build_island_normal(group, smoothed)
|
||||
|
||||
|
||||
def build_normal_direction(group, controls, smoothed, average, weight):
|
||||
"""Blend the smoothed normal against the island average normal."""
|
||||
nodes, links = group.nodes, group.links
|
||||
difference = group.nodes.new("ShaderNodeVectorMath")
|
||||
difference.operation = "SUBTRACT"
|
||||
links.new(average, difference.inputs[0])
|
||||
links.new(smoothed, difference.inputs[1])
|
||||
lean = _math(
|
||||
group, "MULTIPLY", _math(group, "SUBTRACT", 1.0, weight),
|
||||
controls.outputs["Edge Turn"],
|
||||
)
|
||||
blend = _math(group, "ADD", weight, lean)
|
||||
offset = group.nodes.new("ShaderNodeVectorMath")
|
||||
offset.operation = "SCALE"
|
||||
links.new(difference.outputs["Vector"], offset.inputs[0])
|
||||
links.new(blend, offset.inputs[3])
|
||||
direction = group.nodes.new("ShaderNodeVectorMath")
|
||||
direction.operation = "ADD"
|
||||
links.new(smoothed, direction.inputs[0])
|
||||
links.new(offset.outputs["Vector"], direction.inputs[1])
|
||||
normalize = group.nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
links.new(direction.outputs["Vector"], normalize.inputs[0])
|
||||
return normalize.outputs["Vector"]
|
||||
|
||||
|
||||
def build_inflation_amount(group, controls, is_shell):
|
||||
"""Balloon scales the profile by Depth Scale, Shell moves a fixed distance."""
|
||||
nodes, links = group.nodes, group.links
|
||||
thickness = _math(
|
||||
group, "MULTIPLY", controls.outputs["Thickness"],
|
||||
_math(group, "ADD", BALLOON_THICKNESS_BASE, controls.outputs["Depth Scale"]),
|
||||
)
|
||||
balloon = _math(
|
||||
group, "MULTIPLY", read_float_attribute(group, "o_balloon"), thickness,
|
||||
)
|
||||
amount = nodes.new("GeometryNodeSwitch")
|
||||
amount.input_type = "FLOAT"
|
||||
links.new(is_shell, amount.inputs["Switch"])
|
||||
links.new(balloon, amount.inputs["False"])
|
||||
links.new(controls.outputs["Shell Thickness"], amount.inputs["True"])
|
||||
return amount.outputs[0]
|
||||
|
||||
|
||||
def build_solid(group, surface, amount, normal, is_shell, controls, weld_distance):
|
||||
"""Inflate the projected surface into a front leaf, wall and rear leaf."""
|
||||
nodes, links = group.nodes, group.links
|
||||
|
||||
def offset(distance):
|
||||
result = nodes.new("ShaderNodeVectorMath")
|
||||
result.operation = "SCALE"
|
||||
links.new(normal, result.inputs[0])
|
||||
links.new(distance, result.inputs[3])
|
||||
return result.outputs[0]
|
||||
|
||||
# The front leaf moves by Thickness x o_balloon x Front Inflation, independent of
|
||||
# the Depth Scale that scales the rear recession.
|
||||
inflation = _math(
|
||||
group, "MULTIPLY",
|
||||
_math(
|
||||
group, "MULTIPLY", read_float_attribute(group, "o_balloon"),
|
||||
controls.outputs["Thickness"],
|
||||
),
|
||||
controls.outputs["Front Inflation"],
|
||||
)
|
||||
front_amount = nodes.new("GeometryNodeSwitch")
|
||||
front_amount.input_type = "FLOAT"
|
||||
links.new(is_shell, front_amount.inputs["Switch"])
|
||||
links.new(inflation, front_amount.inputs["False"])
|
||||
front_amount.inputs["True"].default_value = 0.0
|
||||
|
||||
front = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(surface, front.inputs["Geometry"])
|
||||
links.new(offset(front_amount.outputs[0]), front.inputs["Offset"])
|
||||
|
||||
# The rear leaf inflates along the negated front direction, straight from the
|
||||
# untouched surface so the front offset cannot drag it along.
|
||||
span = _math(group, "MULTIPLY", amount, -1.0)
|
||||
extrude = nodes.new("GeometryNodeExtrudeMesh")
|
||||
extrude.mode = "FACES"
|
||||
extrude.inputs["Individual"].default_value = False
|
||||
links.new(surface, extrude.inputs["Mesh"])
|
||||
links.new(offset(span), extrude.inputs["Offset"])
|
||||
|
||||
# The rear surface smooths in proportion to its thickness and the balloon profile,
|
||||
# so thin shells and cut boundaries keep their exact positions.
|
||||
thickness = nodes.new("GeometryNodeSwitch")
|
||||
thickness.input_type = "FLOAT"
|
||||
links.new(is_shell, thickness.inputs["Switch"])
|
||||
links.new(controls.outputs["Thickness"], thickness.inputs["False"])
|
||||
links.new(controls.outputs["Shell Thickness"], thickness.inputs["True"])
|
||||
weight = _math(
|
||||
group, "MULTIPLY", _math(group, "MINIMUM", thickness.outputs[0], 1.0),
|
||||
read_float_attribute(group, "o_balloon"),
|
||||
)
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||
blur.data_type = "FLOAT_VECTOR"
|
||||
blur.inputs["Weight"].default_value = 1.0
|
||||
links.new(position.outputs["Position"], blur.inputs["Value"])
|
||||
links.new(group_input(nodes, {"Back Smooth"}).outputs["Back Smooth"], blur.inputs["Iterations"])
|
||||
difference = nodes.new("ShaderNodeVectorMath")
|
||||
difference.operation = "SUBTRACT"
|
||||
links.new(blur.outputs["Value"], difference.inputs[0])
|
||||
links.new(position.outputs["Position"], difference.inputs[1])
|
||||
smooth_offset = nodes.new("ShaderNodeVectorMath")
|
||||
smooth_offset.operation = "SCALE"
|
||||
links.new(difference.outputs["Vector"], smooth_offset.inputs[0])
|
||||
links.new(weight, smooth_offset.inputs[3])
|
||||
smoothed = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(extrude.outputs["Mesh"], smoothed.inputs["Geometry"])
|
||||
links.new(smooth_offset.outputs["Vector"], smoothed.inputs["Offset"])
|
||||
|
||||
# Extruding against the normal leaves the rear leaf and wall facing inward.
|
||||
flipped = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(smoothed.outputs["Geometry"], flipped.inputs["Mesh"])
|
||||
rear = store_float_attribute(
|
||||
group, flipped.outputs["Mesh"], NORMAL_REDUCTION_ATTRIBUTE_NAME, 1.0,
|
||||
)
|
||||
join = nodes.new("GeometryNodeJoinGeometry")
|
||||
links.new(front.outputs["Geometry"], join.inputs["Geometry"])
|
||||
links.new(rear, join.inputs["Geometry"])
|
||||
merge = nodes.new("GeometryNodeMergeByDistance")
|
||||
merge.mode = "ALL"
|
||||
links.new(weld_distance, merge.inputs["Distance"])
|
||||
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
||||
return merge.outputs["Geometry"]
|
||||
|
||||
|
||||
def _build_depth_surface(group, geometry, controls):
|
||||
"""Project the depth image and inflate the projected surface into a solid."""
|
||||
links = group.links
|
||||
split = controls.outputs["Depth Split"]
|
||||
image = controls.outputs["Depth Image"]
|
||||
scale = controls.outputs["Uniform Scale"]
|
||||
geometry, corner_camera, cut_vertex = split_depth_surface(
|
||||
group, geometry, sample_face_camera(group, image), split, controls.outputs["Reference Depth"],
|
||||
controls.outputs["Depth Scale"], triangles=True,
|
||||
)
|
||||
geometry = remove_boundary_triangles(group, geometry)
|
||||
boundary = edge_boundary_field(group.nodes, group.links)
|
||||
rim = evaluate_field(group, boundary, "FLOAT", "POINT")
|
||||
cut = evaluate_field(
|
||||
group, boolean_node(group, "AND", boundary, cut_vertex), "FLOAT", "POINT",
|
||||
)
|
||||
# Only the Depth Split cuts taper the profile; the outline keeps its thickness.
|
||||
geometry = taper_split_profile(group, geometry, cut)
|
||||
camera = build_surface_camera(
|
||||
group, _position_field(group, image), corner_camera, cut_vertex, split,
|
||||
)
|
||||
projection = project_depth_surface(
|
||||
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"],
|
||||
)
|
||||
projection = smooth_cut_boundary(group, projection, cut_vertex, boundary=rim)
|
||||
# Clamped depth pixels can collapse adjacent outline samples after projection.
|
||||
edge = group.nodes.new("GeometryNodeInputMeshEdgeVertices")
|
||||
length = group.nodes.new("ShaderNodeVectorMath")
|
||||
length.operation = "DISTANCE"
|
||||
links.new(edge.outputs["Position 1"], length.inputs[0])
|
||||
links.new(edge.outputs["Position 2"], length.inputs[1])
|
||||
typical = group.nodes.new("GeometryNodeAttributeStatistic")
|
||||
typical.data_type, typical.domain = "FLOAT", "EDGE"
|
||||
links.new(projection, typical.inputs["Geometry"])
|
||||
links.new(length.outputs["Value"], typical.inputs["Attribute"])
|
||||
merge = group.nodes.new("GeometryNodeMergeByDistance")
|
||||
merge.mode = "CONNECTED"
|
||||
links.new(projection, merge.inputs["Geometry"])
|
||||
links.new(_math(group, "MULTIPLY", typical.outputs["Mean"], 1e-6), merge.inputs["Distance"])
|
||||
projection = merge.outputs["Geometry"]
|
||||
# The mode field is shared by the thickness and direction choices.
|
||||
mode = group.nodes.new("GeometryNodeMenuSwitch")
|
||||
mode.data_type = "BOOLEAN"
|
||||
for item, name in zip(mode.enum_items, ("Balloon", "Shell")):
|
||||
item.name = name
|
||||
links.new(controls.outputs["Mode"], mode.inputs["Menu"])
|
||||
mode.inputs["Balloon"].default_value = False
|
||||
mode.inputs["Shell"].default_value = True
|
||||
is_shell = mode.outputs[0]
|
||||
amount = build_inflation_amount(group, controls, is_shell)
|
||||
statistics = group.nodes.new("GeometryNodeAttributeStatistic")
|
||||
statistics.data_type, statistics.domain = "FLOAT", "POINT"
|
||||
links.new(projection, statistics.inputs["Geometry"])
|
||||
links.new(amount, statistics.inputs["Attribute"])
|
||||
has_thickness = compare_node(group, "GREATER_THAN", statistics.outputs["Max"], 1e-6)
|
||||
profile = build_profile_weight(group, projection)
|
||||
smoothed, average = build_smoothed_normals(group)
|
||||
weight = build_normal_weight(group, is_shell, profile)
|
||||
# Material normal strength: the balloon profile gates it, Depth Scale attenuates it.
|
||||
inflated = group.nodes.new("GeometryNodeSwitch")
|
||||
inflated.input_type = "FLOAT"
|
||||
links.new(is_shell, inflated.inputs["Switch"])
|
||||
links.new(profile, inflated.inputs["False"])
|
||||
inflated.inputs["True"].default_value = 1.0
|
||||
normal_strength = group.nodes.new("GeometryNodeSwitch")
|
||||
normal_strength.input_type = "FLOAT"
|
||||
links.new(has_thickness, normal_strength.inputs["Switch"])
|
||||
normal_strength.inputs["False"].default_value = 1.0
|
||||
links.new(inflated.outputs[0], normal_strength.inputs["True"])
|
||||
reduction = _math(
|
||||
group, "SUBTRACT", 1.0,
|
||||
_math(group, "MULTIPLY", controls.outputs["Depth Scale"], normal_strength.outputs[0]),
|
||||
)
|
||||
projection = store_float_attribute(
|
||||
group, projection, NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction,
|
||||
)
|
||||
# The rear leaf inflates along the negated front direction, so one direction is
|
||||
# carried across the extrusion instead of being re-evaluated on the moved surface.
|
||||
projection = store_vector_attribute(
|
||||
group, projection, FRONT_NORMAL_ATTRIBUTE,
|
||||
build_normal_direction(group, controls, smoothed, average, weight),
|
||||
)
|
||||
solid = build_solid(
|
||||
group, projection, amount,
|
||||
read_vector_attribute(group, FRONT_NORMAL_ATTRIBUTE),
|
||||
is_shell, controls,
|
||||
_math(group, "MULTIPLY", typical.outputs["Mean"], SOLID_WELD_SCALE),
|
||||
)
|
||||
shaped = group.nodes.new("GeometryNodeSwitch")
|
||||
shaped.input_type = "GEOMETRY"
|
||||
links.new(has_thickness, shaped.inputs["Switch"])
|
||||
links.new(projection, shaped.inputs["False"])
|
||||
links.new(solid, shaped.inputs["True"])
|
||||
return remove_attribute_pattern(group, shaped.outputs[0], "_o_*")
|
||||
|
||||
|
||||
def build_image_depth_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'
|
||||
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
||||
interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu')
|
||||
float_input(group, 'Thickness', 0.0, maximum=2.0).description = 'Control how much the surface bulges in Balloon mode. Zero removes the bulge.'
|
||||
float_input(group, 'Shell Thickness', 0.0, maximum=1.0, subtype='DISTANCE').description = 'Add thickness behind the original surface while keeping the front surface in place.'
|
||||
float_input(group, 'Depth Scale', 1.0, maximum=2.0)
|
||||
float_input(group, 'Depth Split', 0.1, maximum=1, subtype='FACTOR').description = 'Split the mesh where depth changes abruptly. Scaled by Depth Scale: a flat projection stays whole, higher values detect smaller depth jumps.'
|
||||
options = group.interface.new_panel(name='Options', default_closed=True)
|
||||
boundary_smooth = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 4, 0, 16
|
||||
boundary_smooth.description = 'Smooth geometry near the original outline and Depth Split edges. Zero disables boundary smoothing.'
|
||||
back_smooth = interface_socket(group, 'Back Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
back_smooth.default_value, back_smooth.min_value, back_smooth.max_value = 8, 0, 16
|
||||
back_smooth.description = 'Soften the rear surface where the balloon is thick. The effect follows Thickness and the balloon profile, so thin geometry and cut boundaries keep their positions.'
|
||||
float_input(group, 'Edge Turn', 2.0, minimum=-3.0, maximum=3.0, parent=options).description = 'Turn the outline direction: zero follows the smoothed normal, one reaches the island average normal, two mirrors the smoothed normal across it, and higher or negative values keep turning either way.'
|
||||
float_input(group, 'Front Inflation', 0.2, maximum=1, subtype='FACTOR', parent=options).description = 'Control how much the front surface bulges in Balloon mode, as a share of the balloon thickness. Zero keeps the original front surface.'
|
||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
||||
data = group.interface.new_panel(name='Data', default_closed=True)
|
||||
float_input(group, 'Uniform Scale', 1.0, parent=data).hide_in_modifier = True
|
||||
interface_socket(group, 'Depth Image', 'INPUT', 'NodeSocketImage', data).hide_in_modifier = True
|
||||
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
||||
controls = group_input(group.nodes, set())
|
||||
geometry = _build_depth_surface(group, controls.outputs['Geometry'], controls)
|
||||
_shade_output(group, geometry)
|
||||
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 == 'Shell Thickness':
|
||||
item.name = 'Thickness'
|
||||
return group
|
||||
90
nodes/groups/image_depth_layer.py
Normal file
90
nodes/groups/image_depth_layer.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Build O Image Depth Layer."""
|
||||
import bpy
|
||||
from anyimage.common.material import DEPTH_MATERIAL_NODE_GROUP_NAME
|
||||
from ..common.shader import build_material_nodes, connect_material_outputs
|
||||
|
||||
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
|
||||
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
|
||||
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
|
||||
|
||||
|
||||
def build_image_depth_layer_group():
|
||||
group_name = DEPTH_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')]
|
||||
input_sockets.insert(2, ('Depth', 'NodeSocketFloat'))
|
||||
options = None
|
||||
option_names = {'Alpha Fix', 'Normal Scale', 'Object Space'}
|
||||
for name, socket_type in input_sockets:
|
||||
socket = interface.new_socket(name=name, in_out='INPUT', socket_type=socket_type, parent=options if name in option_names else None)
|
||||
if name in {'Alpha', 'Alpha Fix'}:
|
||||
socket.default_value = 1.0 if name == 'Alpha' else 0.0
|
||||
socket.min_value = 0.0
|
||||
socket.max_value = 1.0
|
||||
socket.subtype = 'FACTOR'
|
||||
elif name == 'Bump Scale':
|
||||
socket.description = 'Control bump detail derived from image brightness. Negative values reverse the bumps. Zero disables the effect.'
|
||||
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')]
|
||||
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, links = group.nodes, group.links
|
||||
group_input, group_output, roughness_curve, bump = build_material_nodes(group)
|
||||
processed_alpha = group_input.outputs['Alpha']
|
||||
displacement = nodes.new('ShaderNodeDisplacement')
|
||||
displacement.inputs['Midlevel'].default_value = 0.0
|
||||
displacement.inputs['Scale'].default_value = 1.0
|
||||
background_attribute = nodes.new('ShaderNodeAttribute')
|
||||
background_attribute.attribute_type = 'GEOMETRY'
|
||||
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
|
||||
material_attribute = nodes.new('ShaderNodeAttribute')
|
||||
material_attribute.attribute_type = 'GEOMETRY'
|
||||
material_attribute.attribute_name = MATERIAL_DISPLACEMENT_ATTRIBUTE
|
||||
strength_attribute = nodes.new('ShaderNodeAttribute')
|
||||
strength_attribute.attribute_type = 'GEOMETRY'
|
||||
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
|
||||
foreground_mask = nodes.new('ShaderNodeMath')
|
||||
foreground_mask.operation = 'GREATER_THAN'
|
||||
foreground_mask.inputs[1].default_value = 0.5
|
||||
clamp_foreground = nodes.new('ShaderNodeMath')
|
||||
clamp_foreground.operation = 'MAXIMUM'
|
||||
masked_depth = nodes.new('ShaderNodeMix')
|
||||
masked_depth.data_type = 'FLOAT'
|
||||
enabled_depth = nodes.new('ShaderNodeMath')
|
||||
enabled_depth.operation = 'MULTIPLY'
|
||||
scaled_depth = nodes.new('ShaderNodeMath')
|
||||
scaled_depth.operation = 'MULTIPLY'
|
||||
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]
|
||||
connect_material_outputs(group, group_input, group_output, roughness_curve, bump, processed_alpha)
|
||||
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.'
|
||||
group.use_fake_user = True
|
||||
return group
|
||||
212
nodes/groups/image_depth_panorama.py
Normal file
212
nodes/groups/image_depth_panorama.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Build a quad panorama with periodic sampling and radial depth separation."""
|
||||
|
||||
import math
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.depth_surface import (
|
||||
_math, split_depth_surface, build_surface_camera,
|
||||
)
|
||||
from ..common.nodes import (
|
||||
boolean_node, compare_node, float_input, group_input, interface_socket,
|
||||
prepare_node_group, evaluate_field, remove_attribute_pattern,
|
||||
)
|
||||
from ..common.material_slots import inherit_material_slots
|
||||
from ..common.boundary_smoothing import smooth_boundary_depth, smooth_cut_boundary
|
||||
|
||||
|
||||
def _quad_sphere(group, segments):
|
||||
nodes, links = group.nodes, group.links
|
||||
cube = nodes.new("GeometryNodeMeshCube")
|
||||
cube.inputs["Size"].default_value = (2, 2, 2)
|
||||
for axis in "XYZ":
|
||||
links.new(segments, cube.inputs[f"Vertices {axis}"])
|
||||
xyz = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(nodes.new("GeometryNodeInputPosition").outputs[0], xyz.inputs[0])
|
||||
squares = {axis: _math(group, "MULTIPLY", xyz.outputs[axis], xyz.outputs[axis]) for axis in "XYZ"}
|
||||
combine = nodes.new("ShaderNodeCombineXYZ")
|
||||
for axis, a, b in (("X", "Y", "Z"), ("Y", "Z", "X"), ("Z", "X", "Y")):
|
||||
half = _math(group, "DIVIDE", _math(group, "ADD", squares[a], squares[b]), 2)
|
||||
third = _math(group, "DIVIDE", _math(group, "MULTIPLY", squares[a], squares[b]), 3)
|
||||
root = _math(group, "SQRT", _math(group, "ADD", _math(group, "SUBTRACT", 1, half), third))
|
||||
links.new(_math(group, "MULTIPLY", xyz.outputs[axis], root), combine.inputs[axis])
|
||||
position = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(cube.outputs["Mesh"], position.inputs["Geometry"])
|
||||
links.new(combine.outputs[0], position.inputs["Position"])
|
||||
return position.outputs["Geometry"]
|
||||
|
||||
|
||||
def build_image_depth_panorama_group():
|
||||
group = bpy.data.node_groups.new("O Image Depth Panorama", "GeometryNodeTree")
|
||||
group.is_modifier = group.use_fake_user = True
|
||||
interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
||||
subdivide = interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
|
||||
subdivide.description = "Increase mesh density to represent finer shapes. Higher levels create more vertices and require more processing time."
|
||||
subdivide.default_value, subdivide.min_value, subdivide.max_value = 8, 0, 8
|
||||
dome = float_input(group, "Dome Radius", 50, minimum=1, maximum=100, subtype="DISTANCE")
|
||||
dome.description = "Radius of the sphere kept at Depth Scale 0, and where mask areas below Mask Threshold land."
|
||||
float_input(group, "Depth Scale", 1, maximum=1, subtype="FACTOR").description = "Blend between a sphere at zero and the estimated depth surface at one."
|
||||
float_input(group, "Depth Split", 0.1, maximum=1, subtype="FACTOR").description = "Split the mesh where depth changes abruptly. Scaled by Depth Scale: a flat projection stays whole, higher values detect smaller depth jumps."
|
||||
valid_only = interface_socket(group, "Depth Mask", "INPUT", "NodeSocketBool")
|
||||
valid_only.default_value = True
|
||||
valid_only.description = "Remove mesh areas marked invalid by the depth mask; with the mask off, they rest on the dome sphere."
|
||||
options = group.interface.new_panel("Options", default_closed=True)
|
||||
boundary_smooth = interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 2, 0, 16
|
||||
boundary_smooth.description = "Smooth depth and geometry near cut edges on the panorama surface. Zero disables boundary smoothing."
|
||||
float_input(group, "Mask Threshold", 0.9, maximum=0.99, subtype="FACTOR", parent=options).description = "Set the minimum depth mask value treated as valid. Higher values exclude more partially valid boundary areas."
|
||||
data = group.interface.new_panel("Data", default_closed=True)
|
||||
interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage", data).hide_in_modifier = True
|
||||
interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
||||
nodes, links = group.nodes, group.links
|
||||
|
||||
def uv(direction):
|
||||
normalize = nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
links.new(direction, normalize.inputs[0])
|
||||
xyz = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(normalize.outputs[0], xyz.inputs[0])
|
||||
longitude = _math(group, "DIVIDE", _math(group, "ARCTAN2", xyz.outputs["Y"], xyz.outputs["X"]), 2 * math.pi)
|
||||
z = _math(group, "MAXIMUM", -1, _math(group, "MINIMUM", 1, xyz.outputs["Z"]))
|
||||
latitude = _math(group, "SUBTRACT", 1, _math(group, "DIVIDE", _math(group, "ARCCOSINE", z), math.pi))
|
||||
return longitude, latitude, xyz.outputs["Z"]
|
||||
|
||||
def combine(u, v):
|
||||
node = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(u, node.inputs["X"])
|
||||
links.new(v, node.inputs["Y"])
|
||||
return node.outputs[0]
|
||||
|
||||
def texture(coordinates, interpolation):
|
||||
inputs = group_input(nodes, {"Depth Image"})
|
||||
info = nodes.new("GeometryNodeImageInfo")
|
||||
links.new(inputs.outputs["Depth Image"], info.inputs["Image"])
|
||||
axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(coordinates, axes.inputs[0])
|
||||
half_pixel = _math(group, "DIVIDE", 0.5, info.outputs["Height"])
|
||||
v = _math(group, "MAXIMUM", half_pixel,
|
||||
_math(group, "MINIMUM", axes.outputs["Y"], _math(group, "SUBTRACT", 1, half_pixel)))
|
||||
node = nodes.new("GeometryNodeImageTexture")
|
||||
node.interpolation, node.extension = interpolation, "REPEAT"
|
||||
links.new(inputs.outputs["Depth Image"], node.inputs["Image"])
|
||||
links.new(combine(axes.outputs["X"], v), node.inputs["Vector"])
|
||||
return node.outputs["Color"]
|
||||
|
||||
def depth(coordinates):
|
||||
linear, nearest = texture(coordinates, "Linear"), texture(coordinates, "Closest")
|
||||
alphas = []
|
||||
for color in (linear, nearest):
|
||||
separate = nodes.new("FunctionNodeSeparateColor")
|
||||
links.new(color, separate.inputs["Color"])
|
||||
alphas.append(separate.outputs["Alpha"])
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "RGBA"
|
||||
links.new(compare_node(group, "LESS_THAN", alphas[0],
|
||||
_math(group, "SUBTRACT", alphas[1], 1e-6)), choose.inputs["Switch"])
|
||||
links.new(linear, choose.inputs["False"])
|
||||
links.new(nearest, choose.inputs["True"])
|
||||
return choose.outputs[0], alphas[1]
|
||||
|
||||
inputs = group_input(nodes, {"Subdivide"})
|
||||
sphere = _quad_sphere(group, _math(group, "ADD", _math(group, "POWER", 2, inputs.outputs["Subdivide"]), 1))
|
||||
position = nodes.new("GeometryNodeInputPosition").outputs[0]
|
||||
longitude, latitude, z = uv(position)
|
||||
coordinates = evaluate_field(group, combine(longitude, latitude), "FLOAT_VECTOR", "POINT")
|
||||
face = nodes.new("GeometryNodeFieldOnDomain")
|
||||
face.data_type, face.domain = "FLOAT_VECTOR", "FACE"
|
||||
links.new(position, face.inputs["Value"])
|
||||
face_u, face_v, _ = uv(face.outputs["Value"])
|
||||
correction = _math(group, "ROUND", _math(group, "SUBTRACT", face_u, longitude))
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "FLOAT"
|
||||
links.new(compare_node(group, "GREATER_THAN", _math(group, "ABSOLUTE", z), 0.999999), choose.inputs["Switch"])
|
||||
links.new(_math(group, "ADD", longitude, correction), choose.inputs["False"])
|
||||
links.new(face_u, choose.inputs["True"])
|
||||
uv_map = nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
uv_map.data_type, uv_map.domain = "FLOAT2", "CORNER"
|
||||
uv_map.inputs["Name"].default_value = "UVMap"
|
||||
links.new(sphere, uv_map.inputs["Geometry"])
|
||||
links.new(combine(choose.outputs[0], latitude), uv_map.inputs["Value"])
|
||||
pixel, _ = depth(coordinates)
|
||||
pixel = evaluate_field(group, pixel, "FLOAT_VECTOR", "POINT")
|
||||
# Both the mask and the depth of a face are read at its centre, like Depth Plane.
|
||||
face_camera, face_alpha = depth(combine(face_u, face_v))
|
||||
face_alpha = evaluate_field(group, face_alpha, "FLOAT", "FACE")
|
||||
threshold_input = group_input(nodes, {"Mask Threshold"})
|
||||
invalid = compare_node(group, "LESS_THAN", face_alpha, threshold_input.outputs["Mask Threshold"])
|
||||
protected = uv_map.outputs["Geometry"]
|
||||
separate = nodes.new("GeometryNodeSeparateGeometry")
|
||||
separate.domain = "FACE"
|
||||
links.new(protected, separate.inputs["Geometry"])
|
||||
links.new(invalid, separate.inputs["Selection"])
|
||||
controls = group_input(nodes, {"Depth Split", "Depth Scale"})
|
||||
strength = controls.outputs["Depth Split"]
|
||||
radial = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(face_camera, radial.inputs[0])
|
||||
geometry, corner_camera, cut_vertex = split_depth_surface(
|
||||
group, separate.outputs["Inverted"], face_camera, strength, 1.0, controls.outputs["Depth Scale"],
|
||||
face_distance=radial.outputs["X"], face_center=face.outputs["Value"],
|
||||
)
|
||||
distance = build_surface_camera(group, pixel, corner_camera, cut_vertex, strength)
|
||||
axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(distance, axes.inputs[0])
|
||||
distance = smooth_boundary_depth(group, axes.outputs["X"])
|
||||
dome_radius = group_input(nodes, {"Dome Radius"}).outputs["Dome Radius"]
|
||||
radius = _math(
|
||||
group, "ADD", dome_radius,
|
||||
_math(group, "MULTIPLY", controls.outputs["Depth Scale"],
|
||||
_math(group, "SUBTRACT", distance, dome_radius)),
|
||||
)
|
||||
radial = nodes.new("ShaderNodeVectorMath")
|
||||
radial.operation = "SCALE"
|
||||
links.new(position, radial.inputs[0])
|
||||
links.new(radius, radial.inputs[3])
|
||||
deform = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(geometry, deform.inputs["Geometry"])
|
||||
links.new(radial.outputs[0], deform.inputs["Position"])
|
||||
geometry = remove_attribute_pattern(group, deform.outputs[0], "_o_*")
|
||||
geometry = smooth_cut_boundary(group, geometry, cut_vertex, outline_scale=1.0)
|
||||
# The excluded faces rest on the dome sphere and relax along their own boundary.
|
||||
domed = nodes.new("ShaderNodeVectorMath")
|
||||
domed.operation = "SCALE"
|
||||
links.new(position, domed.inputs[0])
|
||||
links.new(dome_radius, domed.inputs[3])
|
||||
resting = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(separate.outputs["Selection"], resting.inputs["Geometry"])
|
||||
links.new(domed.outputs[0], resting.inputs["Position"])
|
||||
resting = smooth_cut_boundary(group, resting.outputs["Geometry"], outline_scale=1.0)
|
||||
joined = nodes.new("GeometryNodeJoinGeometry")
|
||||
links.new(geometry, joined.inputs["Geometry"])
|
||||
links.new(resting, joined.inputs["Geometry"])
|
||||
# A flat projection without the mask is the dome sphere alone: no split, no seam.
|
||||
dome_sphere = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(protected, dome_sphere.inputs["Geometry"])
|
||||
links.new(domed.outputs[0], dome_sphere.inputs["Position"])
|
||||
cull_controls = group_input(nodes, {"Depth Mask"})
|
||||
dome_only = boolean_node(
|
||||
group,
|
||||
"AND",
|
||||
boolean_node(group, "NOT", compare_node(group, "GREATER_THAN", controls.outputs["Depth Scale"], 0.0)),
|
||||
boolean_node(group, "NOT", cull_controls.outputs["Depth Mask"]),
|
||||
)
|
||||
unmasked = nodes.new("GeometryNodeSwitch")
|
||||
unmasked.input_type = "GEOMETRY"
|
||||
links.new(dome_only, unmasked.inputs["Switch"])
|
||||
links.new(joined.outputs["Geometry"], unmasked.inputs["False"])
|
||||
links.new(dome_sphere.outputs["Geometry"], unmasked.inputs["True"])
|
||||
cull = nodes.new("GeometryNodeSwitch")
|
||||
cull.input_type = "GEOMETRY"
|
||||
links.new(cull_controls.outputs["Depth Mask"], cull.inputs["Switch"])
|
||||
links.new(unmasked.outputs["Output"], cull.inputs["False"])
|
||||
links.new(geometry, cull.inputs["True"])
|
||||
flip = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(cull.outputs["Output"], flip.inputs["Mesh"])
|
||||
smooth = nodes.new("GeometryNodeSetShadeSmooth")
|
||||
smooth.domain = "FACE"
|
||||
smooth.inputs["Shade Smooth"].default_value = True
|
||||
links.new(flip.outputs["Mesh"], smooth.inputs["Geometry"])
|
||||
geometry = inherit_material_slots(group, smooth.outputs[0], group_input(nodes, {"Geometry"}).outputs["Geometry"])
|
||||
output = nodes.new("NodeGroupOutput")
|
||||
links.new(geometry, output.inputs["Geometry"])
|
||||
prepare_node_group(group)
|
||||
return group
|
||||
158
nodes/groups/image_depth_plane.py
Normal file
158
nodes/groups/image_depth_plane.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Build the camera-projected Depth Plane Geometry Nodes group."""
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.validity import delete_invalid_faces
|
||||
|
||||
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
|
||||
from ..common.nodes import compare_node, prepare_node_group, float_input, evaluate_field
|
||||
from ..common.boundary_smoothing import smooth_boundary_camera, smooth_cut_boundary
|
||||
from ..common.depth_surface import (
|
||||
_math, project_depth_surface, thicken_depth_surface,
|
||||
split_depth_surface, sample_face_camera, build_surface_camera,
|
||||
)
|
||||
|
||||
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
|
||||
|
||||
|
||||
def build_image_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"
|
||||
subdivide = _interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
|
||||
subdivide.description = "Increase mesh density to represent finer shapes. Higher levels create more vertices and require more processing time."
|
||||
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 = "Add shell thickness inward from the depth surface. Zero keeps a single surface."
|
||||
depth_scale = _interface_socket(group, "Depth Scale", "INPUT", "NodeSocketFloat")
|
||||
depth_scale.default_value = 1.0
|
||||
depth_scale.min_value = 0.0
|
||||
depth_scale.max_value = 2.0
|
||||
float_input(group, "Depth Split", 0.1, maximum=1, subtype="FACTOR").description = "Split the mesh where depth changes abruptly. Scaled by Depth Scale: a flat projection stays whole, higher values detect smaller depth jumps."
|
||||
valid_only = _interface_socket(group, "Depth Mask", "INPUT", "NodeSocketBool")
|
||||
valid_only.description = "Remove mesh areas marked invalid by the depth mask."
|
||||
valid_only.default_value = True
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
boundary_smooth = _interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", parent=options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 4, 0, 16
|
||||
boundary_smooth.description = "Smooth depth and geometry near cut edges while preserving the original image border. Zero disables boundary smoothing."
|
||||
validity_threshold = _interface_socket(group, "Mask Threshold", "INPUT", "NodeSocketFloat", parent=options)
|
||||
validity_threshold.description = "Set the minimum depth mask value treated as valid. Higher values exclude more partially valid boundary areas."
|
||||
validity_threshold.default_value = 0.9
|
||||
validity_threshold.min_value, validity_threshold.max_value = 0.0, 0.99
|
||||
validity_threshold.subtype = "FACTOR"
|
||||
float_input(group, "Reference Depth", 1.0, subtype="DISTANCE", parent=options)
|
||||
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, links = group.nodes, group.links
|
||||
controls = _group_input(nodes, {"Geometry", "Subdivide"})
|
||||
plane = nodes.new("GeometryNodeGroup")
|
||||
plane.node_tree = image_plane_group
|
||||
plane.inputs["Thickness"].default_value = 0
|
||||
links.new(controls.outputs["Geometry"], plane.inputs["Geometry"])
|
||||
links.new(controls.outputs["Subdivide"], plane.inputs["Subdivide"])
|
||||
texture_input = _group_input(nodes, {"Depth Image"})
|
||||
scale_input = _group_input(nodes, {"Uniform Scale"})
|
||||
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
||||
uv.data_type = "FLOAT_VECTOR"
|
||||
uv.inputs["Name"].default_value = "UVMap"
|
||||
texture = nodes.new("GeometryNodeImageTexture")
|
||||
texture.interpolation, texture.extension = "Linear", "EXTEND"
|
||||
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
|
||||
links.new(texture_input.outputs["Depth Image"], texture.inputs["Image"])
|
||||
threshold_input = _group_input(nodes, {"Mask Threshold"})
|
||||
channels = nodes.new("FunctionNodeSeparateColor")
|
||||
channels.mode = "RGB"
|
||||
links.new(texture.outputs["Color"], channels.inputs["Color"])
|
||||
invalid = nodes.new("FunctionNodeCompare")
|
||||
invalid.data_type, invalid.operation = "FLOAT", "LESS_THAN"
|
||||
links.new(channels.outputs["Alpha"], invalid.inputs["A"])
|
||||
links.new(threshold_input.outputs["Mask Threshold"], invalid.inputs["B"])
|
||||
# Image Plane's normalized UV rectangle retains its original outline after cuts.
|
||||
uv_axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(uv.outputs["Attribute"], uv_axes.inputs[0])
|
||||
u, v = uv_axes.outputs["X"], uv_axes.outputs["Y"]
|
||||
margin = _math(group, "MINIMUM", _math(group, "MINIMUM", u, v),
|
||||
_math(group, "MINIMUM", _math(group, "SUBTRACT", 1, u), _math(group, "SUBTRACT", 1, v)))
|
||||
original_boundary = evaluate_field(group, compare_node(group, "LESS_THAN", margin, 1e-6), "BOOLEAN", "POINT")
|
||||
protected = plane.outputs["Geometry"]
|
||||
culled = delete_invalid_faces(group, protected, invalid.outputs["Result"])
|
||||
cull_controls = _group_input(nodes, {"Depth Mask"})
|
||||
cull = nodes.new("GeometryNodeSwitch")
|
||||
cull.input_type = "GEOMETRY"
|
||||
links.new(cull_controls.outputs["Depth Mask"], cull.inputs["Switch"])
|
||||
links.new(culled, cull.inputs["True"])
|
||||
links.new(protected, cull.inputs["False"])
|
||||
split_input = _group_input(nodes, {"Depth Split", "Depth Scale"})
|
||||
reference_input = _group_input(nodes, {"Reference Depth"})
|
||||
geometry, corner_camera, cut_vertex = split_depth_surface(
|
||||
group, cull.outputs["Output"], sample_face_camera(group, texture_input.outputs["Depth Image"]),
|
||||
split_input.outputs["Depth Split"], reference_input.outputs["Reference Depth"],
|
||||
split_input.outputs["Depth Scale"],
|
||||
)
|
||||
camera = build_surface_camera(
|
||||
group, texture.outputs["Color"], corner_camera, cut_vertex,
|
||||
split_input.outputs["Depth Split"],
|
||||
)
|
||||
depth_input = _group_input(nodes, {"Reference Depth", "Depth Scale"})
|
||||
camera = smooth_boundary_camera(group, camera, original_boundary)
|
||||
surface = project_depth_surface(
|
||||
group, geometry, camera,
|
||||
scale_input.outputs["Uniform Scale"], depth_input.outputs["Reference Depth"],
|
||||
depth_input.outputs["Depth Scale"],
|
||||
)
|
||||
surface = smooth_cut_boundary(
|
||||
group, surface, cut_vertex, protected=original_boundary, outline_scale=1.0,
|
||||
)
|
||||
shell_input = _group_input(nodes, {"Thickness"})
|
||||
shell = thicken_depth_surface(
|
||||
group, surface, shell_input.outputs["Thickness"],
|
||||
normal_smooth=100,
|
||||
)
|
||||
positive = compare_node(group, "GREATER_THAN", shell_input.outputs["Thickness"], 0)
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(positive, choose.inputs["Switch"])
|
||||
links.new(surface, choose.inputs["False"])
|
||||
links.new(shell["geometry"], choose.inputs["True"])
|
||||
smooth = nodes.new("GeometryNodeSetShadeSmooth")
|
||||
smooth.domain = "FACE"
|
||||
links.new(choose.outputs["Output"], smooth.inputs["Geometry"])
|
||||
output = nodes.new("NodeGroupOutput")
|
||||
links.new(smooth.outputs["Geometry"], output.inputs["Geometry"])
|
||||
group.color_tag = "GEOMETRY"
|
||||
group.description = "Build a camera-projected image shell."
|
||||
group.use_fake_user = True
|
||||
prepare_node_group(group)
|
||||
return group
|
||||
103
nodes/groups/image_layer.py
Normal file
103
nodes/groups/image_layer.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Build O Image Layer."""
|
||||
import bpy
|
||||
from anyimage.common.material import IMAGE_MATERIAL_NODE_GROUP_NAME
|
||||
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
from ..common.shader import build_material_nodes, connect_material_outputs
|
||||
from ..common.normal_map import transform_object_normal_color
|
||||
|
||||
|
||||
def build_image_layer_group():
|
||||
group_name = 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')]
|
||||
input_sockets.insert(2, ('Normal', 'NodeSocketColor'))
|
||||
input_sockets.insert(3, ('Normal Scale', 'NodeSocketFloat'))
|
||||
input_sockets.insert(4, ('Object Space', 'NodeSocketBool'))
|
||||
input_sockets.insert(2, ('Alpha Fix', 'NodeSocketFloat'))
|
||||
options = interface.new_panel(name='Options', default_closed=True)
|
||||
option_names = {'Alpha Fix', 'Object Space'}
|
||||
for name, socket_type in input_sockets:
|
||||
socket = interface.new_socket(name=name, in_out='INPUT', socket_type=socket_type, parent=options if name in option_names else None)
|
||||
if name in {'Alpha', 'Alpha Fix'}:
|
||||
socket.default_value = 1.0 if name == 'Alpha' else 0.0
|
||||
socket.min_value = 0.0
|
||||
socket.max_value = 1.0
|
||||
socket.subtype = 'FACTOR'
|
||||
elif name == 'Bump Scale':
|
||||
socket.description = 'Control bump detail derived from image brightness. Negative values reverse the bumps. Zero disables the effect.'
|
||||
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.description = 'Control the strength of surface detail from the normal map.'
|
||||
socket.default_value = 1.0
|
||||
socket.min_value = 0.0
|
||||
socket.max_value = 10.0
|
||||
elif name == 'Object Space':
|
||||
socket.description = 'Interpret the normal map in object space when enabled, or tangent space when disabled.'
|
||||
socket.default_value = False
|
||||
if name == 'Alpha Fix':
|
||||
socket.description = 'Reduce semi-transparent fringes around image edges. Higher values remove more faint edge pixels. Zero keeps the original transparency.'
|
||||
output_sockets = [('Color', 'NodeSocketColor'), ('Roughness', 'NodeSocketFloat'), ('Alpha', 'NodeSocketFloat'), ('Normal', 'NodeSocketVector')]
|
||||
for name, socket_type in output_sockets:
|
||||
interface.new_socket(name=name, in_out='OUTPUT', socket_type=socket_type)
|
||||
nodes, links = group.nodes, group.links
|
||||
group_input, group_output, roughness_curve, bump = build_material_nodes(group)
|
||||
normal_attribute = nodes.new('ShaderNodeAttribute')
|
||||
normal_attribute.attribute_type = 'GEOMETRY'
|
||||
normal_attribute.attribute_name = NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
normal_weight = nodes.new('ShaderNodeMath')
|
||||
normal_weight.operation = 'SUBTRACT'
|
||||
normal_weight.inputs[0].default_value = 1.0
|
||||
links.new(normal_attribute.outputs['Fac'], normal_weight.inputs[1])
|
||||
weighted_strength = nodes.new('ShaderNodeMath')
|
||||
weighted_strength.operation = 'MULTIPLY'
|
||||
links.new(group_input.outputs['Normal Scale'], weighted_strength.inputs[0])
|
||||
links.new(normal_weight.outputs[0], weighted_strength.inputs[1])
|
||||
tangent_normal = nodes.new('ShaderNodeNormalMap')
|
||||
tangent_normal.space = 'TANGENT'
|
||||
object_normal = nodes.new('ShaderNodeNormalMap')
|
||||
object_normal.space = 'OBJECT'
|
||||
choose_normal = nodes.new('ShaderNodeMix')
|
||||
choose_normal.data_type = 'VECTOR'
|
||||
links.new(group_input.outputs['Normal'], tangent_normal.inputs['Color'])
|
||||
links.new(weighted_strength.outputs[0], tangent_normal.inputs['Strength'])
|
||||
normal_color, normal_strength = transform_object_normal_color(
|
||||
group, group_input.outputs['Normal'], weighted_strength.outputs[0],
|
||||
)
|
||||
links.new(normal_color, object_normal.inputs['Color'])
|
||||
links.new(normal_strength, object_normal.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'])
|
||||
processed_alpha = group_input.outputs['Alpha']
|
||||
alpha_curve = nodes.new('ShaderNodeFloatCurve')
|
||||
alpha_curve.mapping.initialize()
|
||||
alpha_curve.mapping.extend = 'HORIZONTAL'
|
||||
alpha_curve.mapping.use_clip = True
|
||||
alpha_points = alpha_curve.mapping.curves[0].points
|
||||
alpha_points[0].location = (0.0, 0.0)
|
||||
alpha_points[-1].location = (1.0, 1.0)
|
||||
alpha_points.new(0.8, 0.0)
|
||||
alpha_points.new(0.925, 0.925)
|
||||
for point in alpha_points:
|
||||
point.handle_type = 'AUTO_CLAMPED'
|
||||
alpha_curve.mapping.update()
|
||||
links.new(group_input.outputs['Alpha'], alpha_curve.inputs['Value'])
|
||||
links.new(group_input.outputs['Alpha Fix'], alpha_curve.inputs['Factor'])
|
||||
processed_alpha = alpha_curve.outputs['Value']
|
||||
connect_material_outputs(group, group_input, group_output, roughness_curve, bump, processed_alpha)
|
||||
group.color_tag = 'SHADER'
|
||||
group.description = f'Prepare {group_name} material outputs.'
|
||||
group.use_fake_user = True
|
||||
for node in nodes:
|
||||
for socket in node.outputs:
|
||||
socket.hide = not socket.is_linked
|
||||
return group
|
||||
253
nodes/groups/image_plane.py
Normal file
253
nodes/groups/image_plane.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""Build the Image Plane Geometry Nodes group."""
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
|
||||
from ..common.nodes import prepare_node_group
|
||||
from ..common.material_slots import inherit_material_slots
|
||||
|
||||
|
||||
IMAGE_PLANE_GROUP_NAME = "O Image Plane"
|
||||
|
||||
|
||||
def new_socket(interface, name, in_out, socket_type, parent=None):
|
||||
socket = 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 new_group_input(nodes, visible_names):
|
||||
node = nodes.new("NodeGroupInput")
|
||||
visible = set(visible_names)
|
||||
for output in node.outputs:
|
||||
output.hide = output.name not in visible
|
||||
return node
|
||||
|
||||
|
||||
def build_dimension_fields(group):
|
||||
"""Build independent source bounds for UV and mesh calculations."""
|
||||
sources = [
|
||||
group.nodes[name] for name in ("Bounding Box", "Vector Math", "Separate XYZ")
|
||||
]
|
||||
for targets in (
|
||||
{"Math.011", "Math.013"},
|
||||
{"Grid", "Combine XYZ.001", "Vector Math.001"},
|
||||
):
|
||||
copies = {source: group.nodes.new(source.bl_idname) for source in sources}
|
||||
copies[sources[1]].operation = "SUBTRACT"
|
||||
for link in list(group.links):
|
||||
if link.to_node in copies:
|
||||
source = copies.get(link.from_node, link.from_node)
|
||||
group.links.new(
|
||||
source.outputs[link.from_socket.identifier],
|
||||
copies[link.to_node].inputs[link.to_socket.identifier],
|
||||
)
|
||||
elif link.from_node in copies and link.to_node.name in targets:
|
||||
group.links.new(
|
||||
copies[link.from_node].outputs[link.from_socket.identifier],
|
||||
link.to_socket,
|
||||
)
|
||||
|
||||
|
||||
def build_image_plane_group():
|
||||
group = bpy.data.node_groups.new(IMAGE_PLANE_GROUP_NAME, "GeometryNodeTree")
|
||||
group.is_modifier = True
|
||||
interface = group.interface
|
||||
geometry = new_socket(interface, "Geometry", "INPUT", "NodeSocketGeometry")
|
||||
geometry.structure_type = "SINGLE"
|
||||
subdivide = new_socket(interface, "Subdivide", "INPUT", "NodeSocketInt")
|
||||
subdivide.description = "Increase mesh density to represent finer shapes. Higher levels create more vertices and require more processing time."
|
||||
subdivide.default_value = 0
|
||||
subdivide.min_value = 0
|
||||
subdivide.max_value = 10
|
||||
thickness = new_socket(interface, "Thickness", "INPUT", "NodeSocketFloat")
|
||||
thickness.default_value = 0.0
|
||||
thickness.min_value = 0.0
|
||||
thickness.max_value = 10000.0
|
||||
thickness.subtype = "DISTANCE"
|
||||
new_socket(interface, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
||||
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
geometry_input = new_group_input(nodes, {"Geometry"})
|
||||
subdivide_input = new_group_input(nodes, {"Subdivide"})
|
||||
thickness_input = new_group_input(nodes, {"Thickness"})
|
||||
output = nodes.new("NodeGroupOutput")
|
||||
|
||||
bounds = nodes.new("GeometryNodeBoundBox")
|
||||
links.new(geometry_input.outputs["Geometry"], bounds.inputs["Geometry"])
|
||||
dimensions = nodes.new("ShaderNodeVectorMath")
|
||||
dimensions.operation = "SUBTRACT"
|
||||
links.new(bounds.outputs["Max"], dimensions.inputs[0])
|
||||
links.new(bounds.outputs["Min"], dimensions.inputs[1])
|
||||
separate_dimensions = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(dimensions.outputs["Vector"], separate_dimensions.inputs[0])
|
||||
|
||||
short_segments = nodes.new("ShaderNodeMath")
|
||||
short_segments.operation = "POWER"
|
||||
short_segments.inputs[0].default_value = 2.0
|
||||
links.new(subdivide_input.outputs["Subdivide"], short_segments.inputs[1])
|
||||
|
||||
vertex_counts = []
|
||||
for axis, other_axis in (("X", "Z"), ("Z", "X")):
|
||||
ratio = nodes.new("ShaderNodeMath")
|
||||
ratio.operation = "DIVIDE"
|
||||
links.new(separate_dimensions.outputs[axis], ratio.inputs[0])
|
||||
links.new(separate_dimensions.outputs[other_axis], ratio.inputs[1])
|
||||
at_least_one = nodes.new("ShaderNodeMath")
|
||||
at_least_one.operation = "MAXIMUM"
|
||||
at_least_one.inputs[1].default_value = 1.0
|
||||
links.new(ratio.outputs[0], at_least_one.inputs[0])
|
||||
scaled_segments = nodes.new("ShaderNodeMath")
|
||||
scaled_segments.operation = "MULTIPLY"
|
||||
links.new(short_segments.outputs[0], scaled_segments.inputs[0])
|
||||
links.new(at_least_one.outputs[0], scaled_segments.inputs[1])
|
||||
rounded_segments = nodes.new("ShaderNodeMath")
|
||||
rounded_segments.operation = "ROUND"
|
||||
links.new(scaled_segments.outputs[0], rounded_segments.inputs[0])
|
||||
vertices = nodes.new("ShaderNodeMath")
|
||||
vertices.operation = "ADD"
|
||||
vertices.inputs[1].default_value = 1.0
|
||||
links.new(rounded_segments.outputs[0], vertices.inputs[0])
|
||||
vertex_counts.append(vertices.outputs[0])
|
||||
|
||||
grid = nodes.new("GeometryNodeMeshGrid")
|
||||
links.new(separate_dimensions.outputs["X"], grid.inputs["Size X"])
|
||||
links.new(separate_dimensions.outputs["Z"], grid.inputs["Size Y"])
|
||||
links.new(vertex_counts[0], grid.inputs["Vertices X"])
|
||||
links.new(vertex_counts[1], grid.inputs["Vertices Y"])
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
separate_position = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(position.outputs["Position"], separate_position.inputs[0])
|
||||
uv_components = []
|
||||
for position_axis, dimension_axis in (("X", "X"), ("Y", "Z")):
|
||||
normalize = nodes.new("ShaderNodeMath")
|
||||
normalize.operation = "DIVIDE"
|
||||
links.new(separate_position.outputs[position_axis], normalize.inputs[0])
|
||||
links.new(separate_dimensions.outputs[dimension_axis], normalize.inputs[1])
|
||||
offset_uv = nodes.new("ShaderNodeMath")
|
||||
offset_uv.operation = "ADD"
|
||||
offset_uv.inputs[1].default_value = 0.5
|
||||
links.new(normalize.outputs[0], offset_uv.inputs[0])
|
||||
uv_components.append(offset_uv.outputs[0])
|
||||
uv_coordinates = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(uv_components[0], uv_coordinates.inputs["X"])
|
||||
links.new(uv_components[1], uv_coordinates.inputs["Y"])
|
||||
|
||||
cube_uv_components = []
|
||||
for position_axis, dimension_axis in (("X", "X"), ("Z", "Z")):
|
||||
normalize = nodes.new("ShaderNodeMath")
|
||||
normalize.operation = "DIVIDE"
|
||||
links.new(separate_position.outputs[position_axis], normalize.inputs[0])
|
||||
links.new(separate_dimensions.outputs[dimension_axis], normalize.inputs[1])
|
||||
offset_uv = nodes.new("ShaderNodeMath")
|
||||
offset_uv.operation = "ADD"
|
||||
offset_uv.inputs[1].default_value = 0.5
|
||||
links.new(normalize.outputs[0], offset_uv.inputs[0])
|
||||
cube_uv_components.append(offset_uv.outputs[0])
|
||||
cube_uv_coordinates = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(cube_uv_components[0], cube_uv_coordinates.inputs["X"])
|
||||
links.new(cube_uv_components[1], cube_uv_coordinates.inputs["Y"])
|
||||
|
||||
store_grid_uv = nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
store_grid_uv.data_type = "FLOAT2"
|
||||
store_grid_uv.domain = "CORNER"
|
||||
store_grid_uv.inputs["Name"].default_value = "UVMap"
|
||||
links.new(grid.outputs["Mesh"], store_grid_uv.inputs["Geometry"])
|
||||
links.new(uv_coordinates.outputs["Vector"], store_grid_uv.inputs["Value"])
|
||||
|
||||
rotate_grid = nodes.new("GeometryNodeTransform")
|
||||
rotate_grid.inputs["Rotation"].default_value = Matrix.Rotation(1.5707963267948966, 4, "X").to_euler()
|
||||
links.new(store_grid_uv.outputs["Geometry"], rotate_grid.inputs["Geometry"])
|
||||
|
||||
center_sum = nodes.new("ShaderNodeVectorMath")
|
||||
center_sum.operation = "ADD"
|
||||
links.new(bounds.outputs["Min"], center_sum.inputs[0])
|
||||
links.new(bounds.outputs["Max"], center_sum.inputs[1])
|
||||
center = nodes.new("ShaderNodeVectorMath")
|
||||
center.operation = "SCALE"
|
||||
center.inputs[3].default_value = 0.5
|
||||
links.new(center_sum.outputs["Vector"], center.inputs[0])
|
||||
place_grid = nodes.new("GeometryNodeTransform")
|
||||
links.new(rotate_grid.outputs["Geometry"], place_grid.inputs["Geometry"])
|
||||
links.new(center.outputs["Vector"], place_grid.inputs["Translation"])
|
||||
|
||||
cell_sizes = []
|
||||
for index, axis in enumerate(("X", "Z")):
|
||||
segments = nodes.new("ShaderNodeMath")
|
||||
segments.operation = "SUBTRACT"
|
||||
segments.inputs[1].default_value = 1.0
|
||||
links.new(vertex_counts[index], segments.inputs[0])
|
||||
cell_size = nodes.new("ShaderNodeMath")
|
||||
cell_size.operation = "DIVIDE"
|
||||
links.new(separate_dimensions.outputs[axis], cell_size.inputs[0])
|
||||
links.new(segments.outputs[0], cell_size.inputs[1])
|
||||
cell_sizes.append(cell_size.outputs[0])
|
||||
cell_area = nodes.new("ShaderNodeMath")
|
||||
cell_area.operation = "MULTIPLY"
|
||||
links.new(cell_sizes[0], cell_area.inputs[0])
|
||||
links.new(cell_sizes[1], cell_area.inputs[1])
|
||||
target_step = nodes.new("ShaderNodeMath")
|
||||
target_step.operation = "SQRT"
|
||||
links.new(cell_area.outputs[0], target_step.inputs[0])
|
||||
depth_ratio = nodes.new("ShaderNodeMath")
|
||||
depth_ratio.operation = "DIVIDE"
|
||||
links.new(thickness_input.outputs["Thickness"], depth_ratio.inputs[0])
|
||||
links.new(target_step.outputs[0], depth_ratio.inputs[1])
|
||||
rounded_depth = nodes.new("ShaderNodeMath")
|
||||
rounded_depth.operation = "ROUND"
|
||||
links.new(depth_ratio.outputs[0], rounded_depth.inputs[0])
|
||||
depth_segments = nodes.new("ShaderNodeMath")
|
||||
depth_segments.operation = "MAXIMUM"
|
||||
depth_segments.inputs[1].default_value = 3.0
|
||||
links.new(rounded_depth.outputs[0], depth_segments.inputs[0])
|
||||
depth_vertices = nodes.new("ShaderNodeMath")
|
||||
depth_vertices.operation = "ADD"
|
||||
depth_vertices.inputs[1].default_value = 1.0
|
||||
links.new(depth_segments.outputs[0], depth_vertices.inputs[0])
|
||||
|
||||
cube_size = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(separate_dimensions.outputs["X"], cube_size.inputs["X"])
|
||||
links.new(thickness_input.outputs["Thickness"], cube_size.inputs["Y"])
|
||||
links.new(separate_dimensions.outputs["Z"], cube_size.inputs["Z"])
|
||||
cube = nodes.new("GeometryNodeMeshCube")
|
||||
links.new(cube_size.outputs["Vector"], cube.inputs["Size"])
|
||||
links.new(vertex_counts[0], cube.inputs["Vertices X"])
|
||||
links.new(depth_vertices.outputs[0], cube.inputs["Vertices Y"])
|
||||
links.new(vertex_counts[1], cube.inputs["Vertices Z"])
|
||||
store_cube_uv = nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
store_cube_uv.data_type = "FLOAT2"
|
||||
store_cube_uv.domain = "CORNER"
|
||||
store_cube_uv.inputs["Name"].default_value = "UVMap"
|
||||
links.new(cube.outputs["Mesh"], store_cube_uv.inputs["Geometry"])
|
||||
links.new(cube_uv_coordinates.outputs["Vector"], store_cube_uv.inputs["Value"])
|
||||
place_cube = nodes.new("GeometryNodeTransform")
|
||||
links.new(store_cube_uv.outputs["Geometry"], place_cube.inputs["Geometry"])
|
||||
links.new(center.outputs["Vector"], place_cube.inputs["Translation"])
|
||||
has_thickness = nodes.new("FunctionNodeCompare")
|
||||
has_thickness.data_type = "FLOAT"
|
||||
has_thickness.operation = "GREATER_THAN"
|
||||
links.new(thickness_input.outputs["Thickness"], has_thickness.inputs[0])
|
||||
geometry_switch = nodes.new("GeometryNodeSwitch")
|
||||
geometry_switch.input_type = "GEOMETRY"
|
||||
links.new(has_thickness.outputs["Result"], geometry_switch.inputs["Switch"])
|
||||
links.new(place_grid.outputs["Geometry"], geometry_switch.inputs["False"])
|
||||
links.new(place_cube.outputs["Geometry"], geometry_switch.inputs["True"])
|
||||
|
||||
geometry = inherit_material_slots(group, geometry_switch.outputs["Output"], geometry_input.outputs["Geometry"])
|
||||
links.new(geometry, output.inputs["Geometry"])
|
||||
|
||||
group.color_tag = "GEOMETRY"
|
||||
group.description = (
|
||||
"Build an aspect-adaptive image plane and add optional centered thickness."
|
||||
)
|
||||
group.use_fake_user = True
|
||||
build_dimension_fields(group)
|
||||
prepare_node_group(group)
|
||||
return group
|
||||
234
nodes/groups/image_relief_plane.py
Normal file
234
nodes/groups/image_relief_plane.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""Build the Relief Plane Geometry Nodes group."""
|
||||
|
||||
import bpy
|
||||
|
||||
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
|
||||
from ..common.nodes import store_float_attribute
|
||||
from ..common.nodes import compare_node, prepare_node_group, vector_input
|
||||
from ..common.reference_plane import reference_plane_depth
|
||||
from ..common.boundary_smoothing import boundary_influence
|
||||
|
||||
|
||||
RELIEF_PLANE_NODE_GROUP_NAME = "O Image Relief Plane"
|
||||
|
||||
|
||||
def _math(nodes, operation):
|
||||
node = nodes.new("ShaderNodeMath")
|
||||
node.operation = operation
|
||||
return node
|
||||
|
||||
|
||||
def build_image_relief_plane_group(image_plane_group):
|
||||
group = bpy.data.node_groups.get(RELIEF_PLANE_NODE_GROUP_NAME)
|
||||
if group is not None:
|
||||
return group
|
||||
group = bpy.data.node_groups.new(
|
||||
RELIEF_PLANE_NODE_GROUP_NAME,
|
||||
"GeometryNodeTree",
|
||||
)
|
||||
group.is_modifier = True
|
||||
geometry = _interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
||||
geometry.structure_type = "SINGLE"
|
||||
subdivide = _interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
|
||||
subdivide.description = "Increase mesh density to represent finer shapes. Higher levels create more vertices and require more processing time."
|
||||
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 = "Set the thickness of the flat base beneath the relief."
|
||||
vector_input(group, "Depth Direction", (0, 0, 1), subtype="DIRECTION").description = "Set the direction used to orient the reference depth plane."
|
||||
depth_offset = _interface_socket(group, "Depth Offset", "INPUT", "NodeSocketFloat")
|
||||
depth_offset.default_value = 0.0
|
||||
depth_offset.min_value = -10.0
|
||||
depth_offset.max_value = 10.0
|
||||
depth_offset.subtype = "DISTANCE"
|
||||
depth_offset.description = "Correct the reference depth before direction and scale are applied."
|
||||
depth_scale = _interface_socket(group, "Depth Scale", "INPUT", "NodeSocketFloat")
|
||||
depth_scale.default_value = 0.5
|
||||
depth_scale.min_value = 0.0
|
||||
depth_scale.max_value = 2.0
|
||||
valid_only = _interface_socket(group, "Depth Mask", "INPUT", "NodeSocketBool")
|
||||
valid_only.description = "Treat depth-mask-invalid areas as zero depth instead of deleting them."
|
||||
valid_only.default_value = True
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
validity_threshold = _interface_socket(group, "Mask Threshold", "INPUT", "NodeSocketFloat", options)
|
||||
validity_threshold.description = "Set the minimum depth mask value treated as valid."
|
||||
validity_threshold.default_value = 0.9
|
||||
validity_threshold.min_value, validity_threshold.max_value = 0.0, 0.99
|
||||
validity_threshold.subtype = "FACTOR"
|
||||
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"
|
||||
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,
|
||||
{"Geometry", "Subdivide"},
|
||||
)
|
||||
shape_input = _group_input(
|
||||
nodes,
|
||||
{"Thickness", "Depth Direction", "Depth Offset", "Depth Scale"},
|
||||
)
|
||||
image_input = _group_input(
|
||||
nodes,
|
||||
{"Depth Image", "Uniform Scale", "Depth Mask", "Mask Threshold"},
|
||||
)
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
image_plane = nodes.new("GeometryNodeGroup")
|
||||
image_plane.node_tree = image_plane_group
|
||||
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")
|
||||
positive_half.inputs[1].default_value = 0.5
|
||||
links.new(shape_input.outputs["Thickness"], positive_half.inputs[0])
|
||||
translation = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(positive_half.outputs[0], translation.inputs["Y"])
|
||||
place_block = nodes.new("GeometryNodeTransform")
|
||||
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"
|
||||
texture = nodes.new("GeometryNodeImageTexture")
|
||||
texture.interpolation = "Linear"
|
||||
texture.extension = "EXTEND"
|
||||
links.new(image_input.outputs["Depth Image"], texture.inputs["Image"])
|
||||
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
|
||||
depth_components = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(texture.outputs["Color"], depth_components.inputs["Vector"])
|
||||
invalid = nodes.new("FunctionNodeCompare")
|
||||
invalid.data_type, invalid.operation = "FLOAT", "LESS_THAN"
|
||||
links.new(texture.outputs["Alpha"], invalid.inputs["A"])
|
||||
links.new(image_input.outputs["Mask Threshold"], invalid.inputs["B"])
|
||||
mask_enabled = image_input.outputs["Depth Mask"]
|
||||
valid_mask = nodes.new("GeometryNodeSwitch")
|
||||
valid_mask.input_type = "FLOAT"
|
||||
valid_mask.inputs["False"].default_value = 1.0
|
||||
links.new(invalid.outputs["Result"], valid_mask.inputs["Switch"])
|
||||
mask_switch = nodes.new("GeometryNodeSwitch")
|
||||
mask_switch.input_type = "FLOAT"
|
||||
mask_switch.inputs["False"].default_value = 1.0
|
||||
links.new(mask_enabled, mask_switch.inputs["Switch"])
|
||||
links.new(valid_mask.outputs[0], mask_switch.inputs["True"])
|
||||
metric_depth = _math(nodes, "MULTIPLY")
|
||||
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")
|
||||
reference_input = _group_input(nodes, {"Reference Depth"})
|
||||
corrected_reference = _math(nodes, "ADD")
|
||||
links.new(reference_input.outputs["Reference Depth"], corrected_reference.inputs[0])
|
||||
links.new(shape_input.outputs["Depth Offset"], corrected_reference.inputs[1])
|
||||
plane_depth = reference_plane_depth(
|
||||
group, shape_input.outputs["Depth Direction"], corrected_reference.outputs[0],
|
||||
)
|
||||
links.new(metric_depth.outputs[0], relative_depth.inputs[0])
|
||||
links.new(plane_depth, relative_depth.inputs[1])
|
||||
full_offset = _math(nodes, "MULTIPLY")
|
||||
links.new(relative_depth.outputs[0], full_offset.inputs[0])
|
||||
links.new(shape_input.outputs["Depth Scale"], full_offset.inputs[1])
|
||||
masked_full_offset = _math(nodes, "MULTIPLY")
|
||||
links.new(full_offset.outputs[0], masked_full_offset.inputs[0])
|
||||
links.new(mask_switch.outputs[0], masked_full_offset.inputs[1])
|
||||
|
||||
has_thickness = compare_node(group, "GREATER_THAN", shape_input.outputs["Thickness"], 1e-6)
|
||||
safe_offset = _math(nodes, "MINIMUM")
|
||||
safe_offset.inputs[1].default_value = 0.0
|
||||
links.new(masked_full_offset.outputs[0], safe_offset.inputs[0])
|
||||
edge_influence = boundary_influence(group, protected=False)
|
||||
edge_clamped = nodes.new("ShaderNodeClamp")
|
||||
edge_clamped.inputs["Min"].default_value = 0.0
|
||||
edge_clamped.inputs["Max"].default_value = 1.0
|
||||
links.new(edge_influence, edge_clamped.inputs["Value"])
|
||||
geometry_weight = _math(nodes, "SUBTRACT")
|
||||
geometry_weight.inputs[0].default_value = 1.0
|
||||
links.new(edge_clamped.outputs[0], geometry_weight.inputs[1])
|
||||
smooth_safe_offset = _math(nodes, "MULTIPLY")
|
||||
links.new(safe_offset.outputs[0], smooth_safe_offset.inputs[0])
|
||||
links.new(geometry_weight.outputs[0], smooth_safe_offset.inputs[1])
|
||||
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
position_components = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(position.outputs["Position"], position_components.inputs["Vector"])
|
||||
layer_ratio = _math(nodes, "DIVIDE")
|
||||
links.new(position_components.outputs["Y"], layer_ratio.inputs[0])
|
||||
links.new(shape_input.outputs["Thickness"], layer_ratio.inputs[1])
|
||||
nonnegative_ratio = _math(nodes, "MAXIMUM")
|
||||
nonnegative_ratio.inputs[1].default_value = 0.0
|
||||
links.new(layer_ratio.outputs[0], nonnegative_ratio.inputs[0])
|
||||
clamped_ratio = _math(nodes, "MINIMUM")
|
||||
clamped_ratio.inputs[1].default_value = 1.0
|
||||
links.new(nonnegative_ratio.outputs[0], clamped_ratio.inputs[0])
|
||||
one_minus_ratio = _math(nodes, "SUBTRACT")
|
||||
one_minus_ratio.inputs[0].default_value = 1.0
|
||||
links.new(clamped_ratio.outputs[0], one_minus_ratio.inputs[1])
|
||||
layer_weight = nodes.new("GeometryNodeSwitch")
|
||||
layer_weight.input_type = "FLOAT"
|
||||
layer_weight.inputs["False"].default_value = 1.0
|
||||
links.new(has_thickness, layer_weight.inputs["Switch"])
|
||||
links.new(one_minus_ratio.outputs[0], layer_weight.inputs["True"])
|
||||
|
||||
weighted_offset = _math(nodes, "MULTIPLY")
|
||||
links.new(smooth_safe_offset.outputs[0], weighted_offset.inputs[0])
|
||||
links.new(layer_weight.outputs["Output"], weighted_offset.inputs[1])
|
||||
displacement = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(weighted_offset.outputs[0], displacement.inputs["Y"])
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(place_block.outputs["Geometry"], set_position.inputs["Geometry"])
|
||||
links.new(displacement.outputs["Vector"], set_position.inputs["Offset"])
|
||||
shade_smooth = nodes.new("GeometryNodeSetShadeSmooth")
|
||||
shade_smooth.domain = "FACE"
|
||||
shade_smooth.inputs["Shade Smooth"].default_value = True
|
||||
links.new(set_position.outputs["Geometry"], shade_smooth.inputs["Geometry"])
|
||||
normal_reduction = _math(nodes, "SUBTRACT")
|
||||
normal_reduction.inputs[0].default_value = 1.0
|
||||
scaled_depth = _math(nodes, "MULTIPLY")
|
||||
links.new(shape_input.outputs["Depth Scale"], scaled_depth.inputs[0])
|
||||
links.new(geometry_weight.outputs[0], scaled_depth.inputs[1])
|
||||
links.new(scaled_depth.outputs[0], normal_reduction.inputs[1])
|
||||
output_geometry = store_float_attribute(
|
||||
group, shade_smooth.outputs["Geometry"], NORMAL_REDUCTION_ATTRIBUTE_NAME,
|
||||
normal_reduction.outputs[0],
|
||||
)
|
||||
links.new(output_geometry, group_output.inputs["Geometry"])
|
||||
group.color_tag = "GEOMETRY"
|
||||
group.description = (
|
||||
"Build a fixed-base image relief."
|
||||
)
|
||||
group.use_fake_user = True
|
||||
prepare_node_group(group)
|
||||
return group
|
||||
79
nodes/groups/shadeless.py
Normal file
79
nodes/groups/shadeless.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Build the Shadeless shader node group."""
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
SHADELESS_GROUP_NAME = "O Shadeless"
|
||||
|
||||
|
||||
def build_shadeless_group():
|
||||
group = bpy.data.node_groups.new(SHADELESS_GROUP_NAME, "ShaderNodeTree")
|
||||
group.interface.new_socket(
|
||||
name="Shader", in_out="OUTPUT", socket_type="NodeSocketShader"
|
||||
)
|
||||
group.interface.new_socket(
|
||||
name="Color", in_out="INPUT", socket_type="NodeSocketColor"
|
||||
)
|
||||
alpha = group.interface.new_socket(name="Alpha", in_out="INPUT", socket_type="NodeSocketFloat")
|
||||
alpha.default_value, alpha.min_value, alpha.max_value = 1.0, 0.0, 1.0
|
||||
alpha.subtype = "FACTOR"
|
||||
strength = group.interface.new_socket(
|
||||
name="Strength", in_out="INPUT", socket_type="NodeSocketFloat"
|
||||
)
|
||||
strength.description = "Set the emission strength for camera rays. Zero renders the surface black."
|
||||
strength.default_value, strength.min_value = 1.0, 0.0
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
group_input = nodes.new("NodeGroupInput")
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
|
||||
diffuse = nodes.new("ShaderNodeBsdfDiffuse")
|
||||
emission = nodes.new("ShaderNodeEmission")
|
||||
links.new(group_input.outputs["Color"], diffuse.inputs["Color"])
|
||||
links.new(group_input.outputs["Color"], emission.inputs["Color"])
|
||||
links.new(group_input.outputs["Strength"], emission.inputs["Strength"])
|
||||
|
||||
light_path = nodes.new("ShaderNodeLightPath")
|
||||
bounce_count = nodes.new("ShaderNodeMath")
|
||||
bounce_count.operation = "SUBTRACT"
|
||||
links.new(light_path.outputs["Ray Depth"], bounce_count.inputs[0])
|
||||
links.new(light_path.outputs["Transmission Depth"], bounce_count.inputs[1])
|
||||
refracted = nodes.new("ShaderNodeMath")
|
||||
refracted.operation = "SUBTRACT"
|
||||
refracted.inputs[0].default_value = 1.0
|
||||
links.new(bounce_count.outputs[0], refracted.inputs[1])
|
||||
|
||||
reflection_limit = nodes.new("ShaderNodeMath")
|
||||
reflection_limit.operation = "SUBTRACT"
|
||||
reflection_limit.inputs[0].default_value = 2.0
|
||||
links.new(light_path.outputs["Ray Depth"], reflection_limit.inputs[1])
|
||||
camera_reflected = nodes.new("ShaderNodeMath")
|
||||
camera_reflected.operation = "MULTIPLY"
|
||||
links.new(reflection_limit.outputs[0], camera_reflected.inputs[0])
|
||||
links.new(light_path.outputs["Is Glossy Ray"], camera_reflected.inputs[1])
|
||||
shadow_or_reflect = nodes.new("ShaderNodeMath")
|
||||
shadow_or_reflect.operation = "MAXIMUM"
|
||||
links.new(camera_reflected.outputs[0], shadow_or_reflect.inputs[0])
|
||||
links.new(light_path.outputs["Is Shadow Ray"], shadow_or_reflect.inputs[1])
|
||||
shader_factor = nodes.new("ShaderNodeMath")
|
||||
shader_factor.operation = "MAXIMUM"
|
||||
links.new(shadow_or_reflect.outputs[0], shader_factor.inputs[0])
|
||||
links.new(refracted.outputs[0], shader_factor.inputs[1])
|
||||
|
||||
mix = nodes.new("ShaderNodeMixShader")
|
||||
links.new(shader_factor.outputs[0], mix.inputs["Fac"])
|
||||
links.new(diffuse.outputs["BSDF"], mix.inputs[1])
|
||||
links.new(emission.outputs["Emission"], mix.inputs[2])
|
||||
transparent = nodes.new("ShaderNodeBsdfTransparent")
|
||||
alpha_mix = nodes.new("ShaderNodeMixShader")
|
||||
links.new(group_input.outputs["Alpha"], alpha_mix.inputs["Fac"])
|
||||
links.new(transparent.outputs["BSDF"], alpha_mix.inputs[1])
|
||||
links.new(mix.outputs["Shader"], alpha_mix.inputs[2])
|
||||
links.new(alpha_mix.outputs["Shader"], group_output.inputs["Shader"])
|
||||
group.color_tag = "SHADER"
|
||||
group.description = (
|
||||
"Display color without scene lighting while preserving shadows "
|
||||
"and reflections."
|
||||
)
|
||||
group.use_fake_user = True
|
||||
return group
|
||||
Reference in New Issue
Block a user