feat: Implement weight cutout boundary smoothing for image depth cutout
- Added a new proposal for boundary smoothing in the weight cutout process, differentiating between Split edges and Outline smoothing requirements. - Introduced a new capability `cutout-boundary-smoothing` to control smoothing strength and default values. - Updated specifications to define the behavior of boundary-specific smoothing strength and default settings. - Created tasks to verify the implementation of boundary behavior and asset updates. - Developed comprehensive tests for depth symmetry and normal scaling to ensure expected behavior and performance. - Introduced new attributes for normal mapping to facilitate persistent face attributes sharing between depth symmetry and image materials. - Built a new node group for O Image Depth Symmetry, integrating the new smoothing capabilities and ensuring compatibility with existing workflows.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Materialize the joint calculation and connection plan in geometry node trees."""
|
||||
"""Materialize the joint calculation and connection plan in node trees."""
|
||||
|
||||
from .branches import calculation_blocks
|
||||
from .dimensions import estimate_node_dimensions, visible_socket
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Identify connected field calculations by their nearest geometry consumers."""
|
||||
"""Identify connected calculations by their nearest geometry or shader consumers."""
|
||||
|
||||
from collections import defaultdict
|
||||
from graphlib import TopologicalSorter
|
||||
@@ -17,7 +17,8 @@ def calculation_blocks(nodes, edges):
|
||||
i
|
||||
for i, n in enumerate(nodes)
|
||||
if n.bl_idname == "NodeGroupOutput"
|
||||
or any(s.type == "GEOMETRY" and s.is_linked for s in n.outputs)
|
||||
or n.bl_idname in {"ShaderNodeNormalMap", "ShaderNodeBump", "ShaderNodeDisplacement"}
|
||||
or any(s.type in {"GEOMETRY", "SHADER"} and s.is_linked for s in n.outputs)
|
||||
or getattr(n, "paired_output", None) is not None
|
||||
}
|
||||
inputs = {i for i, n in enumerate(nodes) if n.bl_idname == "NodeGroupInput"}
|
||||
|
||||
@@ -74,6 +74,19 @@
|
||||
"ShaderNodeMix": 3,
|
||||
"ShaderNodeSeparateXYZ": 0,
|
||||
"ShaderNodeValue": 1,
|
||||
"ShaderNodeVectorMath": 1
|
||||
"ShaderNodeVectorMath": 1,
|
||||
"FunctionNodeAlignEulerToVector": 2,
|
||||
"ShaderNodeVectorRotate": 2,
|
||||
"ShaderNodeAttribute": 2,
|
||||
"ShaderNodeFloatCurve": 12,
|
||||
"ShaderNodeBump": 2,
|
||||
"ShaderNodeNormalMap": 2,
|
||||
"ShaderNodeDisplacement": 1,
|
||||
"ShaderNodeBsdfDiffuse": 1,
|
||||
"ShaderNodeBsdfTransparent": 0,
|
||||
"ShaderNodeEmission": 0,
|
||||
"ShaderNodeLightPath": 0,
|
||||
"ShaderNodeMixShader": 0,
|
||||
"ShaderNodeGroup": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ def estimate_node_dimensions(node):
|
||||
inputs = [s for s in node.inputs if visible_socket(s)]
|
||||
outputs = [s for s in node.outputs if visible_socket(s)]
|
||||
width = RULES["wide_width"] if (
|
||||
"Attribute" in node.bl_idname or node.bl_idname == "GeometryNodeGroup"
|
||||
"Attribute" in node.bl_idname or node.bl_idname in {"GeometryNodeGroup", "ShaderNodeGroup", "ShaderNodeFloatCurve"}
|
||||
) else RULES["width"]
|
||||
if node.hide:
|
||||
height = max(RULES["collapsed_height"], 16 + 12 * max(len(inputs), len(outputs)))
|
||||
@@ -44,7 +44,7 @@ def estimate_node_dimensions(node):
|
||||
controls = RULES["controls"][node.bl_idname]
|
||||
for collection in ("repeat_items", "state_items", "input_items", "main_items", "generation_items", "enum_items"):
|
||||
controls += len(getattr(node, collection, ()))
|
||||
if node.bl_idname == "GeometryNodeGroup" and node.node_tree:
|
||||
if node.bl_idname in {"GeometryNodeGroup", "ShaderNodeGroup"} and node.node_tree:
|
||||
controls += sum(item.item_type == "PANEL" for item in node.node_tree.interface.items_tree)
|
||||
cursor = RULES["header"] + RULES["padding"]
|
||||
sockets = {}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from .branches import calculation_blocks
|
||||
|
||||
|
||||
SHARED_GEOMETRY_USES = 3
|
||||
|
||||
@@ -84,5 +86,9 @@ def geometry_paths(node_group):
|
||||
|
||||
def node_roles(node_group):
|
||||
"""Classify by contribution to the result, not by available socket types."""
|
||||
if node_group.bl_idname == "ShaderNodeTree":
|
||||
nodes = list(node_group.nodes)
|
||||
_, core = calculation_blocks(nodes, list(node_group.links))
|
||||
return {node: "shader" if i in core else "field" for i, node in enumerate(nodes)}
|
||||
core, _ = geometry_paths(node_group)
|
||||
return {node: "geometry" if node in core else "field" for node in node_group.nodes}
|
||||
|
||||
@@ -90,7 +90,7 @@ def plan_layout(nodes, edges, size, blocks, core):
|
||||
continue
|
||||
sy = items[u]["outputs"][e][1] if a in members else 16.0
|
||||
ty = items[v]["inputs"][e][1] if b in members else 16.0
|
||||
weight = 3.0 if edge_types[e] == "GEOMETRY" else 1.0
|
||||
weight = 3.0 if edge_types[e] in {"GEOMETRY", "SHADER"} else 1.0
|
||||
links.append((e, u, v, sy, ty, weight))
|
||||
parents = {k: set() for k in items}
|
||||
children = {k: set() for k in items}
|
||||
@@ -360,7 +360,7 @@ def plan_layout(nodes, edges, size, blocks, core):
|
||||
u, v = node_atom[a], node_atom[b]
|
||||
sy = seed[a][1] + size[a].sockets[edges[e].from_socket] - atoms[u]["preferred"]
|
||||
ty = seed[b][1] + size[b].sockets[edges[e].to_socket] - atoms[v]["preferred"]
|
||||
weight = 3.0 if edge_types[e] == "GEOMETRY" else 1.0
|
||||
weight = 3.0 if edge_types[e] in {"GEOMETRY", "SHADER"} else 1.0
|
||||
if e in edge_bus:
|
||||
weight *= 0.15
|
||||
sequence = [(u, sy), (v, ty)] if e not in edge_bus else [(u, sy), (edge_bus[e], 12.0), (v, ty)]
|
||||
|
||||
@@ -9,7 +9,7 @@ from .groups.image_depth_plane import build_image_depth_plane_group
|
||||
from .groups.image_relief_plane import build_image_relief_plane_group
|
||||
from .groups.image_cutout import build_image_cutout_group
|
||||
from .groups.image_depth_cutout import build_image_depth_cutout_group
|
||||
from .groups.image_depth_balloon import build_image_depth_balloon_group
|
||||
from .groups.image_depth_symmetry import build_image_depth_symmetry_group
|
||||
from .groups.image_layer import build_image_layer_group
|
||||
from .groups.image_depth_layer import build_image_depth_layer_group
|
||||
from .groups.shadeless import build_shadeless_group
|
||||
@@ -29,7 +29,7 @@ def build_node_groups():
|
||||
build_image_relief_plane_group(image_plane),
|
||||
build_image_cutout_group(),
|
||||
build_image_depth_cutout_group(),
|
||||
build_image_depth_balloon_group(),
|
||||
build_image_depth_symmetry_group(),
|
||||
build_image_layer_group(),
|
||||
build_image_depth_layer_group(),
|
||||
build_shadeless_group(),
|
||||
@@ -40,8 +40,7 @@ def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.context.preferences.filepaths.save_version = 0
|
||||
for node_group in build_node_groups():
|
||||
if node_group.bl_idname == "GeometryNodeTree":
|
||||
arrange_nodes(node_group)
|
||||
arrange_nodes(node_group)
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(OUTPUT_PATH), check_existing=False)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ ASSET_PATH = Path(__file__).resolve().parents[2] / "src/anyimage/assets/O_AnyIma
|
||||
GEOMETRY_NAMES = {
|
||||
"O Image Depth Panorama",
|
||||
"O Image Plane", "O Image Depth Plane", "O Image Relief Plane",
|
||||
"O Image Cutout", "O Image Depth Cutout", "O Image Depth Balloon",
|
||||
"O Image Cutout", "O Image Depth Cutout", "O Image Depth Symmetry",
|
||||
}
|
||||
SHADER_NAMES = {"O Image Layer", "O Image Depth Layer", "O Shadeless"}
|
||||
|
||||
@@ -60,6 +60,8 @@ def check_asset_library(asset_path):
|
||||
values = {"Subdivide": 0, "Thickness": 0.2, "Reference Depth": 2.0,
|
||||
"Depth Image": depth_image, "Uniform Scale": 1.0, "Normal Smooth": 0,
|
||||
"Depth Split": 0.0}
|
||||
if node_group.name == "O Image Depth Symmetry":
|
||||
values["Reference Depth"] = 2.5
|
||||
for item in node_group.interface.items_tree:
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name in values:
|
||||
modifier[item.identifier] = values[item.name]
|
||||
|
||||
@@ -5,10 +5,12 @@ from .depth_surface import _math
|
||||
from .smoothing import edge_boundary_field, expand_smooth
|
||||
|
||||
|
||||
def boundary_influence(group, protected=False):
|
||||
def boundary_influence(group, protected=False, *, boundary=None):
|
||||
"""Fade a boundary field through two rings while excluding protected points."""
|
||||
nodes, links = group.nodes, group.links
|
||||
boundary = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
||||
if boundary is None:
|
||||
boundary = edge_boundary_field(nodes, links)
|
||||
boundary = evaluate_field(group, boundary, "FLOAT", "POINT")
|
||||
unprotected = _math(group, "SUBTRACT", 1, protected)
|
||||
cut = _math(group, "MULTIPLY", boundary, unprotected)
|
||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||
@@ -75,9 +77,20 @@ def reconstruct_camera_depth(group, camera, depth, smoothed):
|
||||
return choose.outputs[0]
|
||||
|
||||
|
||||
def smooth_cut_boundary(group, geometry, original_boundary=False):
|
||||
"""Blend O Smooth's pinned-boundary result through a two-ring edge mask."""
|
||||
influence = boundary_influence(group, original_boundary)
|
||||
def cut_boundary_influence(group, split_boundary):
|
||||
"""Give split edges full influence and the original outline one tenth."""
|
||||
nodes, links = group.nodes, group.links
|
||||
boundary = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
||||
split = _math(group, "MULTIPLY", boundary, split_boundary)
|
||||
outline = _math(group, "MAXIMUM", _math(group, "SUBTRACT", boundary, split), 0)
|
||||
split_influence = boundary_influence(group, boundary=split)
|
||||
outline_influence = _math(group, "MULTIPLY", boundary_influence(group, boundary=outline), 0.1)
|
||||
return _math(group, "MAXIMUM", split_influence, outline_influence)
|
||||
|
||||
|
||||
def smooth_cut_boundary(group, geometry, split_boundary=False):
|
||||
"""Blend O Smooth through split and outline masks with distinct weights."""
|
||||
influence = cut_boundary_influence(group, split_boundary)
|
||||
# The support band depends on topology, which stays unchanged in the loop.
|
||||
return expand_smooth(
|
||||
group, geometry, iterations_name="Boundary Smooth", weight_name="Smooth Weight",
|
||||
|
||||
@@ -327,7 +327,7 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
|
||||
links.new(rim, back_parts.inputs["Geometry"])
|
||||
rear = back_parts.outputs["Geometry"]
|
||||
if back_mask is not None:
|
||||
rear = store_float_attribute(group, rear, back_mask, 0.0, domain="FACE")
|
||||
rear = store_float_attribute(group, rear, back_mask, 1.0, domain="FACE")
|
||||
for value in (
|
||||
saved,
|
||||
rear,
|
||||
|
||||
78
tools/nodes/common/normal_map.py
Normal file
78
tools/nodes/common/normal_map.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Share persistent face attributes between depth symmetry and image materials.
|
||||
|
||||
Rotation stores pre-inversion Euler angles, face kind is 1/2/3 for front/back/wall,
|
||||
and axis is 1 for the final +X orientation. Missing attributes mean identity.
|
||||
These FACE attributes stay on the output mesh for the material consumer.
|
||||
"""
|
||||
from mathutils import Matrix
|
||||
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X
|
||||
|
||||
ROTATION_ATTRIBUTE = 'o_depth_rotation'
|
||||
FACE_ATTRIBUTE = 'o_depth_face'
|
||||
AXIS_ATTRIBUTE = 'o_depth_axis'
|
||||
|
||||
|
||||
def transform_object_normal_color(group, color, strength):
|
||||
"""Rotate encoded object normals, reflect backs and disable wall normal maps."""
|
||||
nodes, links = group.nodes, group.links
|
||||
|
||||
def attribute(name):
|
||||
node = nodes.new('ShaderNodeAttribute')
|
||||
node.attribute_type = 'GEOMETRY'
|
||||
node.attribute_name = name
|
||||
return node
|
||||
|
||||
def math(operation, value, constant):
|
||||
node = nodes.new('ShaderNodeMath')
|
||||
node.operation = operation
|
||||
links.new(value, node.inputs[0])
|
||||
node.inputs[1].default_value = constant
|
||||
return node.outputs[0]
|
||||
|
||||
rotation = attribute(ROTATION_ATTRIBUTE)
|
||||
face = attribute(FACE_ATTRIBUTE)
|
||||
axis = attribute(AXIS_ATTRIBUTE)
|
||||
decode = nodes.new('ShaderNodeVectorMath')
|
||||
decode.operation = 'MULTIPLY_ADD'
|
||||
decode.inputs[1].default_value = (2., 2., 2.)
|
||||
decode.inputs[2].default_value = (-1., -1., -1.)
|
||||
links.new(color, decode.inputs[0])
|
||||
rotate = nodes.new('ShaderNodeVectorRotate')
|
||||
rotate.rotation_type = 'EULER_XYZ'
|
||||
rotate.invert = True
|
||||
links.new(decode.outputs[0], rotate.inputs['Vector'])
|
||||
links.new(rotation.outputs['Vector'], rotate.inputs['Rotation'])
|
||||
separate = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(rotate.outputs[0], separate.inputs[0])
|
||||
back = math('COMPARE', face.outputs['Fac'], 2.)
|
||||
# Reflect the normal's Z component on the back.
|
||||
difference = math('MULTIPLY', separate.outputs['Z'], -2.)
|
||||
correction = nodes.new('ShaderNodeMath')
|
||||
correction.operation = 'MULTIPLY_ADD'
|
||||
links.new(back, correction.inputs[0])
|
||||
links.new(difference, correction.inputs[1])
|
||||
links.new(separate.outputs['Z'], correction.inputs[2])
|
||||
combine = nodes.new('ShaderNodeCombineXYZ')
|
||||
links.new(separate.outputs['X'], combine.inputs['X'])
|
||||
links.new(separate.outputs['Y'], combine.inputs['Y'])
|
||||
links.new(correction.outputs[0], combine.inputs['Z'])
|
||||
axis_rotation = nodes.new('ShaderNodeVectorMath')
|
||||
axis_rotation.operation = 'SCALE'
|
||||
axis_rotation.inputs[0].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
|
||||
links.new(axis.outputs['Fac'], axis_rotation.inputs['Scale'])
|
||||
orient = nodes.new('ShaderNodeVectorRotate')
|
||||
orient.rotation_type = 'EULER_XYZ'
|
||||
links.new(combine.outputs[0], orient.inputs['Vector'])
|
||||
links.new(axis_rotation.outputs[0], orient.inputs['Rotation'])
|
||||
encode = nodes.new('ShaderNodeVectorMath')
|
||||
encode.operation = 'MULTIPLY_ADD'
|
||||
encode.inputs[1].default_value = (.5, .5, .5)
|
||||
encode.inputs[2].default_value = (.5, .5, .5)
|
||||
links.new(orient.outputs[0], encode.inputs[0])
|
||||
mapped = math('LESS_THAN', face.outputs['Fac'], 2.5)
|
||||
weight = nodes.new('ShaderNodeMath')
|
||||
weight.operation = 'MULTIPLY'
|
||||
links.new(mapped, weight.inputs[0])
|
||||
links.new(strength, weight.inputs[1])
|
||||
return encode.outputs[0], weight.outputs[0]
|
||||
@@ -5,22 +5,18 @@ BUMP_BASE_DISTANCE = 0.001
|
||||
def build_material_nodes(group):
|
||||
nodes = group.nodes
|
||||
group_input = nodes.new("NodeGroupInput")
|
||||
group_input.location = (-500.0, 100.0)
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
group_output.location = (300.0, 100.0)
|
||||
roughness_curve = nodes.new("ShaderNodeFloatCurve")
|
||||
roughness_curve.mapping.initialize()
|
||||
curve_points = roughness_curve.mapping.curves[0].points
|
||||
curve_points[0].location = (0.0, 0.5)
|
||||
curve_points[-1].location = (1.0, 0.1)
|
||||
roughness_curve.mapping.update()
|
||||
roughness_curve.location = (-250.0, 350.0)
|
||||
bump = nodes.new("ShaderNodeBump")
|
||||
bump.location = (100.0, 0.0)
|
||||
return group_input, group_output, roughness_curve, bump
|
||||
|
||||
|
||||
def connect_material_outputs(group, group_input, group_output, roughness_curve, bump, processed_alpha, bump_location):
|
||||
def connect_material_outputs(group, group_input, group_output, roughness_curve, bump, processed_alpha):
|
||||
nodes, links = group.nodes, group.links
|
||||
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
|
||||
links.new(group_input.outputs["Color"], roughness_curve.inputs["Value"])
|
||||
@@ -33,7 +29,6 @@ def connect_material_outputs(group, group_input, group_output, roughness_curve,
|
||||
bump_distance = nodes.new("ShaderNodeMath")
|
||||
bump_distance.operation = "MULTIPLY"
|
||||
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
|
||||
bump_distance.location = bump_location
|
||||
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
|
||||
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
|
||||
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Build O Image Cutout."""
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.cutout import _extrude_layers, _island_cleanup, _offset_geometry_z, _remove_zero_thickness, _set_layer_z, _shade_output
|
||||
@@ -198,7 +198,10 @@ def build_image_cutout_group():
|
||||
mode.inputs['Shell'].default_value = 1
|
||||
group.links.new(inflated.outputs[0], mode.inputs['Balloon'])
|
||||
scale = mode.outputs[0]
|
||||
geometry = store_float_attribute(group, geometry, NORMAL_SCALE_ATTRIBUTE_NAME, scale)
|
||||
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])
|
||||
orient = group.nodes.new('GeometryNodeTransform')
|
||||
orient.inputs['Rotation'].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
|
||||
group.links.new(geometry, orient.inputs['Geometry'])
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
"""Build O Image Depth Balloon."""
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME
|
||||
from ..common.nodes import (
|
||||
interface_socket, group_input, math_node, float_input, bool_input, vector_input,
|
||||
menu_switch, read_float_attribute, store_float_attribute, prepare_node_group,
|
||||
)
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.reference_plane import reference_plane_depth
|
||||
from ..common.cutout import _extrude_layers, _island_cleanup, _position_field, _profile_field, _remove_zero_thickness, _set_layer_z, _shade_output
|
||||
|
||||
|
||||
NODE_GROUP_NAME = "O Image Depth Balloon"
|
||||
|
||||
|
||||
def _set_geometry_z(group, geometry, z_value):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
separate = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(position.outputs["Position"], separate.inputs["Vector"])
|
||||
combine = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(separate.outputs["X"], combine.inputs["X"])
|
||||
links.new(separate.outputs["Y"], combine.inputs["Y"])
|
||||
links.new(z_value, combine.inputs["Z"])
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(geometry, set_position.inputs["Geometry"])
|
||||
links.new(combine.outputs["Vector"], set_position.inputs["Position"])
|
||||
return set_position.outputs["Geometry"]
|
||||
|
||||
|
||||
def _build_depth_balloon(
|
||||
group,
|
||||
geometry,
|
||||
thickness,
|
||||
depth_scale,
|
||||
double_sided,
|
||||
uniform_scale,
|
||||
depth_direction,
|
||||
depth_image,
|
||||
depth_reference,
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
profile, profile_factor = _profile_field(group, geometry)
|
||||
camera_position = _position_field(group, depth_image)
|
||||
separate_camera = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(camera_position, separate_camera.inputs["Vector"])
|
||||
metric_depth = math_node(nodes, "MULTIPLY")
|
||||
links.new(separate_camera.outputs["Z"], metric_depth.inputs[0])
|
||||
links.new(uniform_scale, metric_depth.inputs[1])
|
||||
plane_depth = reference_plane_depth(
|
||||
group,
|
||||
depth_direction,
|
||||
depth_reference,
|
||||
)
|
||||
corrected_depth = math_node(nodes, "SUBTRACT")
|
||||
links.new(plane_depth, corrected_depth.inputs[0])
|
||||
links.new(metric_depth.outputs[0], corrected_depth.inputs[1])
|
||||
nonnegative_depth = math_node(nodes, "MAXIMUM")
|
||||
nonnegative_depth.inputs[1].default_value = 0.0
|
||||
links.new(corrected_depth.outputs[0], nonnegative_depth.inputs[0])
|
||||
scaled_depth = math_node(nodes, "MULTIPLY")
|
||||
links.new(nonnegative_depth.outputs[0], scaled_depth.inputs[0])
|
||||
links.new(depth_scale, scaled_depth.inputs[1])
|
||||
front_depth = scaled_depth.outputs[0]
|
||||
base_front = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, base_front.inputs[0])
|
||||
links.new(thickness, base_front.inputs[1])
|
||||
negate_back = math_node(nodes, "MULTIPLY")
|
||||
negate_back.inputs[1].default_value = -1.0
|
||||
links.new(base_front.outputs[0], negate_back.inputs[0])
|
||||
front_mix = nodes.new("ShaderNodeMix")
|
||||
front_mix.data_type = "FLOAT"
|
||||
front_mix.clamp_factor = True
|
||||
links.new(profile_factor, front_mix.inputs["Factor"])
|
||||
links.new(base_front.outputs[0], front_mix.inputs["A"])
|
||||
links.new(front_depth, front_mix.inputs["B"])
|
||||
mirrored_front = math_node(nodes, "MULTIPLY")
|
||||
mirrored_front.inputs[1].default_value = -1.0
|
||||
links.new(front_mix.outputs["Result"], mirrored_front.inputs[0])
|
||||
back_choice = nodes.new("GeometryNodeSwitch")
|
||||
back_choice.input_type = "FLOAT"
|
||||
links.new(double_sided, back_choice.inputs["Switch"])
|
||||
links.new(negate_back.outputs[0], back_choice.inputs["False"])
|
||||
links.new(mirrored_front.outputs[0], back_choice.inputs["True"])
|
||||
extrude = _extrude_layers(group, geometry)
|
||||
shaped = _set_layer_z(
|
||||
group,
|
||||
geometry,
|
||||
extrude,
|
||||
front_mix.outputs["Result"],
|
||||
back_choice.outputs["Output"],
|
||||
)
|
||||
front_only = _set_geometry_z(
|
||||
group, geometry, front_mix.outputs["Result"]
|
||||
)
|
||||
return _remove_zero_thickness(group, front_only, shaped, thickness)
|
||||
|
||||
|
||||
def build_image_depth_balloon_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')
|
||||
float_input(group, 'Thickness', 1.0).description = 'Control how much the surface bulges. Zero removes the bulge.'
|
||||
float_input(group, 'Depth Scale', 1.0)
|
||||
bool_input(group, 'Double Sided', True)
|
||||
iterations = interface_socket(group, 'Smooth', 'INPUT', 'NodeSocketInt')
|
||||
iterations.description = 'Set the number of mesh smoothing passes. Higher values smooth the surface more but may soften details. Zero disables smoothing.'
|
||||
iterations.default_value = 0
|
||||
iterations.min_value, iterations.max_value = (0, 20)
|
||||
options = group.interface.new_panel(name='Options', default_closed=True)
|
||||
float_input(group, 'Cleanup Threshold', 0.0, parent=options)
|
||||
float_input(group, 'Smooth Weight', 1.0, maximum=1, subtype='FACTOR', parent=options).description = 'Control the strength of each mesh smoothing pass. Higher values move vertices more.'
|
||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
||||
interface_socket(group, 'Depth Axis', 'INPUT', 'NodeSocketMenu', options).menu_expanded = True
|
||||
vector_input(group, 'Depth Direction', (0, 0, 1), subtype='DIRECTION', 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_balloon(group, controls.outputs['Geometry'], controls.outputs['Thickness'], controls.outputs['Depth Scale'], controls.outputs['Double Sided'], controls.outputs['Uniform Scale'], controls.outputs['Depth Direction'], controls.outputs['Depth Image'], controls.outputs['Reference Depth'])
|
||||
planar_inputs = [link.to_socket for link in group.links if link.from_node.bl_idname == 'NodeGroupInput' and link.from_socket.name == 'Geometry']
|
||||
base = group_input(group.nodes, {'Geometry', 'Cleanup Threshold'})
|
||||
area = math_node(group.nodes, 'MULTIPLY')
|
||||
area.inputs[1].default_value = 0.01
|
||||
group.links.new(base.outputs['Cleanup Threshold'], area.inputs[0])
|
||||
cleaned = _island_cleanup(group, base.outputs['Geometry'], area.outputs[0])
|
||||
for target in planar_inputs:
|
||||
group.links.new(cleaned, target)
|
||||
geometry = expand_smooth(group, geometry, iterations_name='Smooth', weight_name='Smooth Weight')
|
||||
profile = read_float_attribute(group, 'o_balloon')
|
||||
scale = profile
|
||||
geometry = store_float_attribute(group, geometry, NORMAL_SCALE_ATTRIBUTE_NAME, scale)
|
||||
orient = group.nodes.new('GeometryNodeTransform')
|
||||
orient.inputs['Rotation'].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
|
||||
group.links.new(geometry, orient.inputs['Geometry'])
|
||||
axis = menu_switch(group, 'Depth Axis', ('-Z', '+X'), 'GEOMETRY')
|
||||
group.links.new(geometry, axis.inputs['-Z'])
|
||||
group.links.new(orient.outputs[0], axis.inputs['+X'])
|
||||
_shade_output(group, axis.outputs[0])
|
||||
prepare_node_group(group)
|
||||
for item in group.interface.items_tree:
|
||||
if item.item_type != 'SOCKET' or item.in_out != 'INPUT':
|
||||
continue
|
||||
if item.name == 'Mode':
|
||||
item.default_value = 'Balloon'
|
||||
elif item.name == 'Depth Axis':
|
||||
item.default_value = '-Z'
|
||||
return group
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Build O Image Depth Cutout."""
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group, sample_field, evaluate_field
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.boundary_smoothing import smooth_cut_boundary
|
||||
@@ -235,7 +235,7 @@ def build_shell_position(group, position, front, is_shell, fields):
|
||||
links.new(control.outputs["Thickness"], clamp.inputs["Value"])
|
||||
t = clamp.outputs[0]
|
||||
radius = _math(group, "MULTIPLY", _math(group, "SUBTRACT", front_z, fields["center"]), t)
|
||||
rear = _math(group, "SUBTRACT", 1, read_float_attribute(group, "o_normal_scale"))
|
||||
rear = read_float_attribute(group, NORMAL_REDUCTION_ATTRIBUTE_NAME)
|
||||
rear_radius = _math(group, "MULTIPLY", rear, _math(group, "ADD", radius, distance))
|
||||
direction = vector("SUBTRACT", fields["normal"], (0, 0, 1))
|
||||
rear_shift = vector("SCALE", direction, rear_radius)
|
||||
@@ -277,7 +277,7 @@ def _build_depth_surface(group, geometry, controls):
|
||||
projection = project_depth_surface(
|
||||
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"], split,
|
||||
)
|
||||
projection = smooth_cut_boundary(group, projection)
|
||||
projection = smooth_cut_boundary(group, projection, cut_vertex)
|
||||
# Clamped depth pixels can collapse adjacent outline samples after projection.
|
||||
edge = group.nodes.new("GeometryNodeInputMeshEdgeVertices")
|
||||
length = group.nodes.new("ShaderNodeVectorMath")
|
||||
@@ -309,7 +309,7 @@ def _build_depth_surface(group, geometry, controls):
|
||||
front = build_front_fields(group, volume, normal, normal_weight)
|
||||
shell_fields = build_shell_fields(group, projection, volume, is_shell, normal_weight)
|
||||
shell = thicken_depth_surface(
|
||||
group, projection, shell_fields["thickness"], back_mask=NORMAL_SCALE_ATTRIBUTE_NAME, weld_zero_thickness=True,
|
||||
group, projection, shell_fields["thickness"], back_mask=NORMAL_REDUCTION_ATTRIBUTE_NAME, weld_zero_thickness=True,
|
||||
)
|
||||
fields = {
|
||||
name: sample_field(group, projection, value, data_type, index=shell["source_index"])
|
||||
@@ -357,7 +357,7 @@ def build_image_depth_cutout_group():
|
||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
||||
interface_socket(group, 'Depth Axis', 'INPUT', 'NodeSocketMenu', options).menu_expanded = True
|
||||
boundary_smooth = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 0, 0, 16
|
||||
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.'
|
||||
normal_smooth = interface_socket(group, 'Normal Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
normal_smooth.default_value, normal_smooth.min_value, normal_smooth.max_value = 64, 16, 512
|
||||
@@ -372,7 +372,7 @@ def build_image_depth_cutout_group():
|
||||
area.inputs[1].default_value = 0.01
|
||||
group.links.new(base.outputs['Cleanup Threshold'], area.inputs[0])
|
||||
cleaned = _island_cleanup(group, base.outputs['Geometry'], area.outputs[0])
|
||||
cleaned = store_float_attribute(group, cleaned, NORMAL_SCALE_ATTRIBUTE_NAME, 1.0, domain='FACE')
|
||||
cleaned = store_float_attribute(group, cleaned, NORMAL_REDUCTION_ATTRIBUTE_NAME, 0.0, domain='FACE')
|
||||
controls = group_input(group.nodes, set())
|
||||
geometry = _build_depth_surface(group, cleaned, controls)
|
||||
geometry = expand_smooth(group, geometry, iterations_name='Smooth', weight_name='Smooth Weight')
|
||||
|
||||
@@ -49,39 +49,26 @@ def build_image_depth_layer_group():
|
||||
displacement = nodes.new('ShaderNodeDisplacement')
|
||||
displacement.inputs['Midlevel'].default_value = 0.0
|
||||
displacement.inputs['Scale'].default_value = 1.0
|
||||
group_input.location = (-1200.0, 100.0)
|
||||
group_output.location = (1050.0, 100.0)
|
||||
roughness_curve.location = (-300.0, 450.0)
|
||||
bump.location = (750.0, 50.0)
|
||||
displacement.location = (750.0, -300.0)
|
||||
background_attribute = nodes.new('ShaderNodeAttribute')
|
||||
background_attribute.attribute_type = 'GEOMETRY'
|
||||
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
|
||||
background_attribute.location = (-950.0, -500.0)
|
||||
material_attribute = nodes.new('ShaderNodeAttribute')
|
||||
material_attribute.attribute_type = 'GEOMETRY'
|
||||
material_attribute.attribute_name = MATERIAL_DISPLACEMENT_ATTRIBUTE
|
||||
material_attribute.location = (-50.0, -650.0)
|
||||
strength_attribute = nodes.new('ShaderNodeAttribute')
|
||||
strength_attribute.attribute_type = 'GEOMETRY'
|
||||
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
|
||||
strength_attribute.location = (200.0, -650.0)
|
||||
foreground_mask = nodes.new('ShaderNodeMath')
|
||||
foreground_mask.operation = 'GREATER_THAN'
|
||||
foreground_mask.inputs[1].default_value = 0.5
|
||||
foreground_mask.location = (-950.0, -250.0)
|
||||
clamp_foreground = nodes.new('ShaderNodeMath')
|
||||
clamp_foreground.operation = 'MAXIMUM'
|
||||
clamp_foreground.location = (-650.0, -400.0)
|
||||
masked_depth = nodes.new('ShaderNodeMix')
|
||||
masked_depth.data_type = 'FLOAT'
|
||||
masked_depth.location = (-350.0, -350.0)
|
||||
enabled_depth = nodes.new('ShaderNodeMath')
|
||||
enabled_depth.operation = 'MULTIPLY'
|
||||
enabled_depth.location = (250.0, -350.0)
|
||||
scaled_depth = nodes.new('ShaderNodeMath')
|
||||
scaled_depth.operation = 'MULTIPLY'
|
||||
scaled_depth.location = (450.0, -350.0)
|
||||
links.new(group_input.outputs['Alpha'], foreground_mask.inputs[0])
|
||||
links.new(group_input.outputs['Depth'], clamp_foreground.inputs[0])
|
||||
links.new(background_attribute.outputs['Fac'], clamp_foreground.inputs[1])
|
||||
@@ -94,7 +81,7 @@ def build_image_depth_layer_group():
|
||||
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, (500.0, 100.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'
|
||||
|
||||
289
tools/nodes/groups/image_depth_symmetry.py
Normal file
289
tools/nodes/groups/image_depth_symmetry.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""Build O Image Depth Symmetry."""
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||
from ..common.nodes import (
|
||||
interface_socket, group_input, math_node, float_input, bool_input, vector_input,
|
||||
menu_switch, evaluate_field, read_float_attribute, store_float_attribute, prepare_node_group,
|
||||
)
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.boundary_smoothing import smooth_cut_boundary
|
||||
from ..common.depth_surface import _math, project_depth_surface
|
||||
from ..common.reference_plane import reference_plane_depth
|
||||
from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE
|
||||
from ..common.cutout import _extrude_layers, _island_cleanup, _position_field, _profile_field, _remove_zero_thickness, _set_layer_z
|
||||
|
||||
|
||||
NODE_GROUP_NAME = "O Image Depth Symmetry"
|
||||
|
||||
|
||||
def _set_geometry_z(group, geometry, z_value):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
separate = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(position.outputs["Position"], separate.inputs["Vector"])
|
||||
combine = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(separate.outputs["X"], combine.inputs["X"])
|
||||
links.new(separate.outputs["Y"], combine.inputs["Y"])
|
||||
links.new(z_value, combine.inputs["Z"])
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(geometry, set_position.inputs["Geometry"])
|
||||
links.new(combine.outputs["Vector"], set_position.inputs["Position"])
|
||||
return set_position.outputs["Geometry"]
|
||||
|
||||
|
||||
def _build_balloon(
|
||||
group,
|
||||
geometry,
|
||||
thickness,
|
||||
depth_scale,
|
||||
double_sided,
|
||||
uniform_scale,
|
||||
depth_direction,
|
||||
depth_image,
|
||||
depth_reference,
|
||||
depth_offset,
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
profile, profile_factor = _profile_field(group, geometry)
|
||||
camera_position = _position_field(group, depth_image)
|
||||
separate_camera = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(camera_position, separate_camera.inputs["Vector"])
|
||||
metric_depth = math_node(nodes, "MULTIPLY")
|
||||
links.new(separate_camera.outputs["Z"], metric_depth.inputs[0])
|
||||
links.new(uniform_scale, metric_depth.inputs[1])
|
||||
plane_depth = reference_plane_depth(
|
||||
group,
|
||||
depth_direction,
|
||||
depth_reference,
|
||||
)
|
||||
corrected_depth = math_node(nodes, "SUBTRACT")
|
||||
links.new(plane_depth, corrected_depth.inputs[0])
|
||||
links.new(metric_depth.outputs[0], corrected_depth.inputs[1])
|
||||
nonnegative_depth = math_node(nodes, "MAXIMUM")
|
||||
nonnegative_depth.inputs[1].default_value = 0.0
|
||||
links.new(corrected_depth.outputs[0], nonnegative_depth.inputs[0])
|
||||
scaled_depth = math_node(nodes, "MULTIPLY")
|
||||
links.new(nonnegative_depth.outputs[0], scaled_depth.inputs[0])
|
||||
links.new(depth_scale, scaled_depth.inputs[1])
|
||||
front_depth = _math(group, 'MAXIMUM', _math(group, 'ADD', scaled_depth.outputs[0], depth_offset), 0.0)
|
||||
base_front = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, base_front.inputs[0])
|
||||
links.new(thickness, base_front.inputs[1])
|
||||
negate_back = math_node(nodes, "MULTIPLY")
|
||||
negate_back.inputs[1].default_value = -1.0
|
||||
links.new(base_front.outputs[0], negate_back.inputs[0])
|
||||
front_mix = nodes.new("ShaderNodeMix")
|
||||
front_mix.data_type = "FLOAT"
|
||||
front_mix.clamp_factor = True
|
||||
links.new(profile_factor, front_mix.inputs["Factor"])
|
||||
links.new(base_front.outputs[0], front_mix.inputs["A"])
|
||||
links.new(front_depth, front_mix.inputs["B"])
|
||||
mirrored_front = math_node(nodes, "MULTIPLY")
|
||||
mirrored_front.inputs[1].default_value = -1.0
|
||||
links.new(front_mix.outputs["Result"], mirrored_front.inputs[0])
|
||||
back_choice = nodes.new("GeometryNodeSwitch")
|
||||
back_choice.input_type = "FLOAT"
|
||||
links.new(double_sided, back_choice.inputs["Switch"])
|
||||
links.new(negate_back.outputs[0], back_choice.inputs["False"])
|
||||
links.new(mirrored_front.outputs[0], back_choice.inputs["True"])
|
||||
extrude = _extrude_layers(group, geometry)
|
||||
shaped = _set_layer_z(
|
||||
group,
|
||||
geometry,
|
||||
extrude,
|
||||
front_mix.outputs["Result"],
|
||||
back_choice.outputs["Output"],
|
||||
)
|
||||
front_only = _set_geometry_z(
|
||||
group, geometry, front_mix.outputs["Result"]
|
||||
)
|
||||
return _remove_zero_thickness(group, front_only, shaped, thickness)
|
||||
|
||||
|
||||
def _build_projected_front(group, geometry, controls):
|
||||
"""Project camera depth and align its direction with the fixed XY plane."""
|
||||
nodes, links = group.nodes, group.links
|
||||
camera = _position_field(group, controls['Depth Image'])
|
||||
projected = project_depth_surface(
|
||||
group, geometry, camera, controls['Uniform Scale'],
|
||||
controls['Reference Depth'], controls['Depth Scale'], 0.0,
|
||||
)
|
||||
align = nodes.new('FunctionNodeAlignEulerToVector')
|
||||
align.axis = 'Z'
|
||||
links.new(controls['Depth 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'])
|
||||
place = nodes.new('GeometryNodeSetPosition')
|
||||
links.new(projected, place.inputs['Geometry'])
|
||||
links.new(rotate.outputs[0], place.inputs['Position'])
|
||||
offset = nodes.new('ShaderNodeCombineXYZ')
|
||||
links.new(controls['Depth Offset'], offset.inputs['Z'])
|
||||
links.new(offset.outputs[0], place.inputs['Offset'])
|
||||
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 _delete_negative_faces(group, front, tolerance):
|
||||
"""Discard faces touching negative Z without leaving wires or isolated vertices."""
|
||||
position = group.nodes.new('GeometryNodeInputPosition')
|
||||
components = group.nodes.new('ShaderNodeSeparateXYZ')
|
||||
group.links.new(position.outputs[0], components.inputs[0])
|
||||
negative = _math(group, 'LESS_THAN', components.outputs['Z'], tolerance)
|
||||
point_negative = evaluate_field(group, negative, 'FLOAT', 'POINT')
|
||||
face_negative = evaluate_field(group, point_negative, 'FLOAT', 'FACE')
|
||||
delete = group.nodes.new('GeometryNodeDeleteGeometry')
|
||||
delete.domain = 'FACE'
|
||||
delete.mode = 'ALL'
|
||||
group.links.new(front, delete.inputs['Geometry'])
|
||||
group.links.new(_math(group, 'GREATER_THAN', face_negative, 0), delete.inputs['Selection'])
|
||||
return delete.outputs[0]
|
||||
|
||||
|
||||
def _build_straight_volume(group, front, double_sided, fill_sides, tolerance):
|
||||
"""Mirror the positive surface and join matching boundary vertices with straight walls."""
|
||||
nodes, links = group.nodes, group.links
|
||||
shade = nodes.new('GeometryNodeSetShadeSmooth')
|
||||
shade.domain = 'FACE'
|
||||
links.new(front, shade.inputs['Geometry'])
|
||||
front = shade.outputs[0]
|
||||
front = store_float_attribute(group, front, FACE_ATTRIBUTE, 1., domain='FACE')
|
||||
count = nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
||||
boundary = _math(group, 'COMPARE', count.outputs['Face Count'], 1)
|
||||
extrude = nodes.new('GeometryNodeExtrudeMesh')
|
||||
extrude.mode = 'EDGES'
|
||||
extrude.inputs['Offset'].default_value = (0, 0, 0)
|
||||
extrude.inputs['Offset Scale'].default_value = 0
|
||||
links.new(front, extrude.inputs['Mesh'])
|
||||
links.new(boundary, extrude.inputs['Selection'])
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(position.outputs[0], components.inputs[0])
|
||||
mirror_point = nodes.new('ShaderNodeCombineXYZ')
|
||||
links.new(components.outputs['X'], mirror_point.inputs['X'])
|
||||
links.new(components.outputs['Y'], mirror_point.inputs['Y'])
|
||||
links.new(_math(group, 'MULTIPLY', components.outputs['Z'], -1), mirror_point.inputs['Z'])
|
||||
place = nodes.new('GeometryNodeSetPosition')
|
||||
links.new(extrude.outputs['Mesh'], place.inputs['Geometry'])
|
||||
links.new(extrude.outputs['Top'], place.inputs['Selection'])
|
||||
links.new(mirror_point.outputs[0], place.inputs['Position'])
|
||||
sides = nodes.new('GeometryNodeSeparateGeometry')
|
||||
sides.domain = 'FACE'
|
||||
links.new(place.outputs[0], sides.inputs['Geometry'])
|
||||
links.new(extrude.outputs['Side'], sides.inputs['Selection'])
|
||||
flat = nodes.new('GeometryNodeSetShadeSmooth')
|
||||
flat.domain = 'FACE'
|
||||
flat.inputs['Shade Smooth'].default_value = False
|
||||
links.new(sides.outputs['Selection'], flat.inputs['Geometry'])
|
||||
mirror = nodes.new('GeometryNodeTransform')
|
||||
mirror.inputs['Scale'].default_value = (1, 1, -1)
|
||||
links.new(front, mirror.inputs['Geometry'])
|
||||
flip = nodes.new('GeometryNodeFlipFaces')
|
||||
links.new(mirror.outputs[0], flip.inputs['Mesh'])
|
||||
join = nodes.new('GeometryNodeJoinGeometry')
|
||||
back = store_float_attribute(group, flip.outputs[0], FACE_ATTRIBUTE, 2., domain='FACE')
|
||||
walls = store_float_attribute(group, flat.outputs[0], FACE_ATTRIBUTE, 3., domain='FACE')
|
||||
wall_switch = nodes.new('GeometryNodeSwitch')
|
||||
wall_switch.input_type = 'GEOMETRY'
|
||||
links.new(fill_sides, wall_switch.inputs['Switch'])
|
||||
links.new(walls, wall_switch.inputs['True'])
|
||||
for geometry in (front, back, wall_switch.outputs[0]):
|
||||
links.new(geometry, join.inputs[0])
|
||||
merge = nodes.new('GeometryNodeMergeByDistance')
|
||||
links.new(tolerance, merge.inputs['Distance'])
|
||||
links.new(join.outputs[0], merge.inputs[0])
|
||||
switch = nodes.new('GeometryNodeSwitch')
|
||||
switch.input_type = 'GEOMETRY'
|
||||
links.new(double_sided, switch.inputs['Switch'])
|
||||
links.new(front, switch.inputs['False'])
|
||||
links.new(merge.outputs[0], switch.inputs['True'])
|
||||
return switch.outputs[0]
|
||||
|
||||
|
||||
def build_image_depth_symmetry_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).description = 'Control the bulge in Balloon mode. Zero keeps only the front surface.'
|
||||
float_input(group, 'Depth Scale', 1.0)
|
||||
vector_input(group, 'Depth Direction', (0, 0, 1), subtype='DIRECTION')
|
||||
float_input(group, 'Depth Offset', 0.0, minimum=-1e10, subtype='DISTANCE').description = 'Offset the front depth along the symmetry axis after direction adjustment. Positive values move it away from the symmetry plane.'
|
||||
iterations = interface_socket(group, 'Smooth', 'INPUT', 'NodeSocketInt')
|
||||
iterations.description = 'Set the number of mesh smoothing passes. Higher values smooth the surface more but may soften details. Zero disables smoothing.'
|
||||
iterations.default_value = 0
|
||||
iterations.min_value, iterations.max_value = (0, 20)
|
||||
options = group.interface.new_panel(name='Options', default_closed=True)
|
||||
bool_input(group, 'Double Sided', True, parent=options)
|
||||
bool_input(group, 'Fill Sides', True, parent=options).description = 'Close the gap between the front and mirrored back with straight walls in Projected mode.'
|
||||
float_input(group, 'Cleanup Threshold', 0.0, parent=options)
|
||||
float_input(group, 'Smooth Weight', 1.0, maximum=1, subtype='FACTOR', parent=options).description = 'Control the strength of each mesh smoothing pass. Higher values move vertices more.'
|
||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options).description = 'Calibrate the source depth before direction adjustment.'
|
||||
interface_socket(group, 'Depth Axis', 'INPUT', 'NodeSocketMenu', options).menu_expanded = True
|
||||
boundary = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
boundary.default_value, boundary.min_value, boundary.max_value = 1, 0, 16
|
||||
boundary.description = 'Smooth the projected outline before clipping and closing the surface.'
|
||||
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())
|
||||
inputs = controls.outputs
|
||||
cleaned = _island_cleanup(
|
||||
group, inputs['Geometry'], _math(group, 'MULTIPLY', inputs['Cleanup Threshold'], 0.01),
|
||||
)
|
||||
balloon = _build_balloon(
|
||||
group, cleaned, inputs['Thickness'], inputs['Depth Scale'], inputs['Double Sided'],
|
||||
inputs['Uniform Scale'], inputs['Depth Direction'], inputs['Depth Image'], inputs['Reference Depth'],
|
||||
inputs['Depth Offset'],
|
||||
)
|
||||
balloon = expand_smooth(group, balloon, iterations_name='Smooth', weight_name='Smooth Weight')
|
||||
shade = group.nodes.new('GeometryNodeSetShadeSmooth')
|
||||
shade.domain = 'FACE'
|
||||
group.links.new(balloon, shade.inputs['Geometry'])
|
||||
tolerance = _math(group, 'MULTIPLY', _math(group, 'MAXIMUM', inputs['Uniform Scale'], 1e-8), 1e-7)
|
||||
front = smooth_cut_boundary(group, _build_projected_front(group, cleaned, inputs))
|
||||
front = _delete_negative_faces(group, front, tolerance)
|
||||
front = expand_smooth(group, front, iterations_name='Smooth', weight_name='Smooth Weight')
|
||||
projected = _build_straight_volume(group, front, inputs['Double Sided'], inputs['Fill Sides'], tolerance)
|
||||
modes = menu_switch(group, 'Mode', ('Balloon', 'Projected'), 'GEOMETRY')
|
||||
group.links.new(shade.outputs[0], modes.inputs['Balloon'])
|
||||
group.links.new(projected, modes.inputs['Projected'])
|
||||
geometry = modes.outputs[0]
|
||||
profile = read_float_attribute(group, 'o_balloon')
|
||||
reduction = _math(group, 'SUBTRACT', 1.0, profile)
|
||||
geometry = store_float_attribute(group, geometry, NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction)
|
||||
geometry = store_float_attribute(group, geometry, AXIS_ATTRIBUTE, 0., domain='FACE')
|
||||
orient = group.nodes.new('GeometryNodeTransform')
|
||||
orient.inputs['Rotation'].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
|
||||
group.links.new(geometry, orient.inputs['Geometry'])
|
||||
axis = menu_switch(group, 'Depth Axis', ('-Z', '+X'), 'GEOMETRY')
|
||||
group.links.new(geometry, axis.inputs['-Z'])
|
||||
oriented = store_float_attribute(group, orient.outputs[0], AXIS_ATTRIBUTE, 1., domain='FACE')
|
||||
group.links.new(oriented, axis.inputs['+X'])
|
||||
output = group.nodes.new('NodeGroupOutput')
|
||||
group.links.new(axis.outputs[0], output.inputs['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 = 'Projected'
|
||||
elif item.name == 'Depth Axis':
|
||||
item.default_value = '-Z'
|
||||
return group
|
||||
@@ -1,7 +1,9 @@
|
||||
"""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():
|
||||
@@ -47,18 +49,30 @@ def build_image_layer_group():
|
||||
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'
|
||||
tangent_normal.location = (-500.0, -100.0)
|
||||
object_normal = nodes.new('ShaderNodeNormalMap')
|
||||
object_normal.space = 'OBJECT'
|
||||
object_normal.location = (-500.0, -350.0)
|
||||
choose_normal = nodes.new('ShaderNodeMix')
|
||||
choose_normal.data_type = 'VECTOR'
|
||||
choose_normal.location = (-150.0, -150.0)
|
||||
for normal_map in (tangent_normal, object_normal):
|
||||
links.new(group_input.outputs['Normal'], normal_map.inputs['Color'])
|
||||
links.new(group_input.outputs['Normal Scale'], normal_map.inputs['Strength'])
|
||||
links.new(group_input.outputs['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])
|
||||
@@ -76,14 +90,10 @@ def build_image_layer_group():
|
||||
for point in alpha_points:
|
||||
point.handle_type = 'AUTO_CLAMPED'
|
||||
alpha_curve.mapping.update()
|
||||
group_input.location = (-750.0, 150.0)
|
||||
group_output.location = (350.0, 450.0)
|
||||
alpha_curve.location = (-350.0, 1100.0)
|
||||
roughness_curve.location = (-350.0, 650.0)
|
||||
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, (-250.0, 150.0))
|
||||
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
|
||||
|
||||
@@ -20,61 +20,46 @@ def build_shadeless_group():
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
group_input = nodes.new("NodeGroupInput")
|
||||
group_input.location = (-900.0, -350.0)
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
group_output.location = (850.0, 100.0)
|
||||
|
||||
diffuse = nodes.new("ShaderNodeBsdfDiffuse")
|
||||
diffuse.location = (250.0, -150.0)
|
||||
emission = nodes.new("ShaderNodeEmission")
|
||||
emission.location = (250.0, -400.0)
|
||||
links.new(group_input.outputs["Color"], diffuse.inputs["Color"])
|
||||
links.new(group_input.outputs["Color"], emission.inputs["Color"])
|
||||
|
||||
light_path = nodes.new("ShaderNodeLightPath")
|
||||
light_path.location = (-900.0, 150.0)
|
||||
bounce_count = nodes.new("ShaderNodeMath")
|
||||
bounce_count.operation = "SUBTRACT"
|
||||
bounce_count.location = (-500.0, -50.0)
|
||||
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
|
||||
refracted.location = (-250.0, -50.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
|
||||
reflection_limit.location = (-650.0, 450.0)
|
||||
links.new(light_path.outputs["Ray Depth"], reflection_limit.inputs[1])
|
||||
camera_reflected = nodes.new("ShaderNodeMath")
|
||||
camera_reflected.operation = "MULTIPLY"
|
||||
camera_reflected.location = (-400.0, 400.0)
|
||||
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"
|
||||
shadow_or_reflect.location = (-100.0, 350.0)
|
||||
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"
|
||||
shader_factor.location = (150.0, 300.0)
|
||||
links.new(shadow_or_reflect.outputs[0], shader_factor.inputs[0])
|
||||
links.new(refracted.outputs[0], shader_factor.inputs[1])
|
||||
|
||||
mix = nodes.new("ShaderNodeMixShader")
|
||||
mix.location = (550.0, 100.0)
|
||||
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")
|
||||
transparent.location = (550.0, -200.0)
|
||||
alpha_mix = nodes.new("ShaderNodeMixShader")
|
||||
alpha_mix.location = (850.0, 100.0)
|
||||
group_output.location = (1100.0, 100.0)
|
||||
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])
|
||||
|
||||
@@ -46,7 +46,7 @@ def _node_group_data(node_group):
|
||||
label = prop.enum_items[value].name if prop.type == "ENUM" and value in prop.enum_items else str(value)
|
||||
settings.append(label)
|
||||
title = node.bl_label
|
||||
if node.bl_idname == "GeometryNodeGroup":
|
||||
if node.bl_idname in {"GeometryNodeGroup", "ShaderNodeGroup"}:
|
||||
title = node.node_tree.name
|
||||
elif hasattr(node, "operation") and settings:
|
||||
title = settings[0]
|
||||
@@ -120,7 +120,7 @@ button,select { padding: 8px; font: inherit; max-width: 100%; }
|
||||
|
||||
|
||||
def main():
|
||||
"""Build and arrange geometry assets in memory for preview."""
|
||||
"""Build and arrange node assets in memory for preview."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True, help="Output HTML path")
|
||||
parser.add_argument("--fragment", action="store_true", help="Write a fragment for an inline preview")
|
||||
@@ -130,7 +130,7 @@ def main():
|
||||
from ..arrangement.arrange import arrange_nodes
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
node_groups = [node_group for node_group in build_node_groups() if node_group.bl_idname == "GeometryNodeTree"]
|
||||
node_groups = build_node_groups()
|
||||
for node_group in node_groups:
|
||||
arrange_nodes(node_group)
|
||||
print(write_node_preview(node_groups, output_path, fragment=args.fragment))
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<canvas id="layout-canvas" role="img" aria-label="按实际排列坐标绘制的节点组,尺寸和曲线使用估算模型"></canvas>
|
||||
<div class="viz-row text-small" id="layout-legend">
|
||||
<span><i class="geometry-key"></i>几何连接</span>
|
||||
<span><i class="geometry-key"></i>几何与着色连接</span>
|
||||
<span><i class="field-key"></i>字段与参数连接</span>
|
||||
<span id="layout-count"></span>
|
||||
</div>
|
||||
@@ -113,15 +113,15 @@
|
||||
return;
|
||||
}
|
||||
if (view.value === 'middle') {
|
||||
const geometryNodes = current.nodes.filter(n => n.type !== 'NodeReroute' && n.role === 'geometry');
|
||||
const midX = (Math.min(...geometryNodes.map(n => n.x)) + Math.max(...geometryNodes.map(n => n.x))) / 2;
|
||||
const target = geometryNodes.sort((a, b) => Math.abs(a.x - midX) - Math.abs(b.x - midX))[0];
|
||||
const mainNodes = current.nodes.filter(n => n.type !== 'NodeReroute' && n.role !== 'field');
|
||||
const midX = (Math.min(...mainNodes.map(n => n.x)) + Math.max(...mainNodes.map(n => n.x))) / 2;
|
||||
const target = mainNodes.sort((a, b) => Math.abs(a.x - midX) - Math.abs(b.x - midX))[0];
|
||||
fit([target.x - 850, target.y - 560, target.x + 670, target.y + 600]);
|
||||
detail.textContent = `${current.name} · 中段计算局部`;
|
||||
return;
|
||||
}
|
||||
if (view.value === 'input') {
|
||||
const target = current.nodes.filter(n => n.role === 'geometry' && n.type !== 'NodeReroute').sort((a, b) => a.x - b.x)[0];
|
||||
const target = current.nodes.filter(n => n.role !== 'field' && n.type !== 'NodeReroute').sort((a, b) => a.x - b.x)[0];
|
||||
fit([target.x - 850, target.y - 650, target.x + 750, target.y + 650]);
|
||||
detail.textContent = `${current.name} · 起点输入附近`; return;
|
||||
}
|
||||
@@ -192,9 +192,9 @@
|
||||
const x2 = wx2 * scale + dx, y2 = wy2 * scale + dy;
|
||||
const bend = Math.max(40, Math.abs(wx2 - wx1) * 0.42) * scale;
|
||||
const active = selected === source || selected === target;
|
||||
context.globalAlpha = selected !== null && !active ? 0.18 : type === 'GEOMETRY' ? 0.85 : 0.46;
|
||||
context.strokeStyle = type === 'GEOMETRY' ? geometry : fields;
|
||||
context.lineWidth = active ? 2.5 : type === 'GEOMETRY' ? 1.35 : 0.8;
|
||||
context.globalAlpha = selected !== null && !active ? 0.18 : ['GEOMETRY', 'SHADER'].includes(type) ? 0.85 : 0.46;
|
||||
context.strokeStyle = ['GEOMETRY', 'SHADER'].includes(type) ? geometry : fields;
|
||||
context.lineWidth = active ? 2.5 : ['GEOMETRY', 'SHADER'].includes(type) ? 1.35 : 0.8;
|
||||
context.beginPath(); context.moveTo(x1, y1);
|
||||
context.bezierCurveTo(x1 + bend, y1, x2 - bend, y2, x2, y2); context.stroke();
|
||||
}
|
||||
@@ -202,9 +202,9 @@
|
||||
for (const node of current.nodes) {
|
||||
const x = node.x * scale + dx, y = node.y * scale + dy, w = node.w * scale, h = node.h * scale;
|
||||
if (x + w < 0 || x > width || y + h < 0 || y > height) continue;
|
||||
const geometryNode = node.type === 'NodeReroute'
|
||||
? node.sockets.some(socket => socket[3] === 'GEOMETRY') : node.role === 'geometry';
|
||||
const hue = geometryNode ? geometry : other;
|
||||
const mainNode = node.type === 'NodeReroute'
|
||||
? node.sockets.some(socket => ['GEOMETRY', 'SHADER'].includes(socket[3])) : node.role !== 'field';
|
||||
const hue = mainNode ? geometry : other;
|
||||
if (node.type === 'NodeReroute') {
|
||||
context.fillStyle = hue; context.beginPath();
|
||||
context.arc(x + w / 2, y + h / 2, Math.max(1.5, w / 3), 0, Math.PI * 2); context.fill(); continue;
|
||||
@@ -241,7 +241,7 @@
|
||||
}
|
||||
if (scale >= 0.22) {
|
||||
for (const socket of node.sockets) {
|
||||
context.fillStyle = socket[3] === 'GEOMETRY' ? geometry : fields;
|
||||
context.fillStyle = ['GEOMETRY', 'SHADER'].includes(socket[3]) ? geometry : fields;
|
||||
context.beginPath(); context.arc(socket[2] ? x + w : x, y + socket[1] * scale,
|
||||
Math.max(1.4, 3.6 * scale), 0, Math.PI * 2); context.fill();
|
||||
}
|
||||
@@ -285,7 +285,7 @@
|
||||
const x = (event.clientX - rect.left - dx) / scale, y = (event.clientY - rect.top - dy) / scale;
|
||||
const node = current.nodes.findLast(n => x >= n.x && x <= n.x + n.w && y >= n.y && y <= n.y + n.h);
|
||||
selected = node ? node.id : null;
|
||||
detail.textContent = node ? `${node.name} · ${node.title} · ${node.role === 'geometry' ? '几何主线' : '计算或输入'}${node.settings.length ? ' · ' + node.settings.join(' / ') : ''}` : current.name;
|
||||
detail.textContent = node ? `${node.name} · ${node.title} · ${node.role !== 'field' ? '结果主线' : '计算或输入'}${node.settings.length ? ' · ' + node.settings.join(' / ') : ''}` : current.name;
|
||||
draw();
|
||||
}
|
||||
dragging = null;
|
||||
|
||||
Reference in New Issue
Block a user