I'd like to use geometry nodes to put a marker on each face of a mesh. Each marker should be placed at specified weighted average of that face's vertices, and should stay at that weighted average as the mesh is transformed and deformed without changing structure. The desired weights have been precomputed and placed into a per-face-corner attribute on the mesh.
How can I accomplish this?
I see there are some geometry node types that might be relevant: "Corners of Face", "Capture Attribute", but I haven't figured out how to string them together yet.
The script included below sets up a toy case: a cube, with the per-face-corner weights in an attribute called "per_face_corner_weights". These weights specify that all the markers should appear in the centers of the faces, except for the one on the top face, which should be biased towards one corner.
This script also puts a simple geometry nodes modifier on the cube, which just puts a marker at each face center (ignoring the weights).
How can I position each marker using the per-face-corner weights, instead?
import bpy
# Create a cube with per-face-corner weights in a custom attribute.
verts = ((-1,-1,-1), (+1,-1,-1), (-1,+1,-1), (+1,+1,-1),
(-1,-1,+1), (+1,-1,+1), (-1,+1,+1), (+1,+1,+1))
faces = ((0,2,3,1), (0,1,5,4), (1,3,7,5),
(3,2,6,7), (2,0,4,6), (4,5,7,6))
per_face_corner_weights = (
1/4, 1/4, 1/4, 1/4, # uniform - put marker in center of face
1/4, 1/4, 1/4, 1/4, # uniform - put marker in center of face
1/4, 1/4, 1/4, 1/4, # uniform - put marker in center of face
1/4, 1/4, 1/4, 1/4, # uniform - put marker in center of face
1/4, 1/4, 1/4, 1/4, # uniform - put marker in center of face
1/2, 1/6, 1/6, 1/6, # bias this one towards one of this face's corners
)
mesh = bpy.data.meshes.new(name="CubeMesh")
mesh.from_pydata(vertices=verts, edges=[], faces=faces)
attr = mesh.attributes.new(name="per_face_corner_weights",
type="FLOAT",
domain="CORNER")
attr.data.foreach_set("value", per_face_corner_weights)
cube = bpy.data.objects.new(name="Cube", object_data=mesh)
# Link it to the active collection (whose icon is circled in the outliner),
# making it visible in 3D Views.
bpy.context.collection.objects.link(cube)
# And here is some code to put a simple geometry nodes modifier on the cube.
# This puts a marker (a sphere) in the center of each face, ignoring the weights,
# which is *almost* what we want.
# How do we position the markers using the weights, instead?
def PutMarkersOnFaces(mesh_object):
# Create a geometry nodes modifier on the mesh object
modifier = mesh_object.modifiers.new(name="AddFaceMarkersModifier", type="NODES")
# Create the node group with group input and output nodes, and assign it
# to the modifier (like bpy.ops.node.new_geometry_node_group_assign() does)
node_group = bpy.data.node_groups.new(name="AddFaceMarkersNodeGroup", type="GeometryNodeTree")
node_group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
node_group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
modifier.node_group = node_group
nodes = node_group.nodes
group_input_node = nodes.new(type="NodeGroupInput")
group_output_node = nodes.new(type="NodeGroupOutput")
# Add my nodes
mesh_to_points_node = nodes.new(type="GeometryNodeMeshToPoints")
mesh_to_points_node.mode = "FACES" # change from "VERTICES"
uv_sphere_node = nodes.new(type="GeometryNodeMeshUVSphere")
uv_sphere_node.inputs['Radius'].default_value = 0.25 # change from 1
instance_on_points_node = nodes.new(type="GeometryNodeInstanceOnPoints")
join_geometry_node = nodes.new(type="GeometryNodeJoinGeometry")
# Add connections between nodes
links = node_group.links
links.new(group_input_node.outputs['Geometry'], join_geometry_node.inputs['Geometry'])
links.new(join_geometry_node.outputs['Geometry'], group_output_node.inputs['Geometry'])
links.new(group_input_node.outputs['Geometry'], mesh_to_points_node.inputs['Mesh'])
links.new(mesh_to_points_node.outputs['Points'], instance_on_points_node.inputs['Points'])
links.new(uv_sphere_node.outputs['Mesh'], instance_on_points_node.inputs['Instance'])
links.new(instance_on_points_node.outputs['Instances'], join_geometry_node.inputs['Geometry'])
# Set node locations for the geometry nodes editor
group_input_node.location.x = -500.0
group_input_node.location.y = -25.0
group_output_node.location.x = 160.0
group_output_node.location.y = 0.0
mesh_to_points_node.location.x = -320.0
mesh_to_points_node.location.y = -80.0
uv_sphere_node.location.x = -320.0
uv_sphere_node.location.y = -265.0
instance_on_points_node.location.x = -160.0
instance_on_points_node.location.y = -80.0
join_geometry_node.location.x = 0.0
join_geometry_node.location.y = 0.0
# Hide unconnected inputs on all nodes (like ctrl-h which toggles them in ui)
for node in node_group.nodes:
for input in node.inputs: input.hide = True # gets rejected if connected; good
for output in node.outputs: output.hide = True # gets rejected if connected; good
# Then unhide interesting unconnected inputs (ones we've set default values on)
uv_sphere_node.inputs["Radius"].hide = False
# Deselect all the nodes
for node in node_group.nodes: node.select = False
PutMarkersOnFaces(cube)
# Make the cube selected and active (mainly so its geom nodes modifier
# will appear in the geometry nodes editor)
cube.select_set(True)
bpy.context.view_layer.objects.active = cube