3
$\begingroup$

Geometry Nodes with Capture Attributes

In the depicted geometry nodes setup, capture attribute is used with the "position" field added as input, in two nodes. I expect that this gives no new information to the geometry that is passed through capture attribute, since it's simply declaring properties inherent to the geometry.

Yet, when I route the mesh output from split edges directly to set position (and bypass the capture attributes, which still supply data to the mix node), the top viewer node displays nothing. The bottom viewer node displays a cube with disconnected edges and faces of variable size. I would expect these two to be identical.

What are the capture attributes nodes adding to the geometry output that causes the viewer nodes to differ?

$\endgroup$
1
  • 2
    $\begingroup$ Capture Attributes captures attributes in the domain you specify in the currently evaluated geometry. Since you are not capturing attributes in the upper geometry (trace the green line backwards from the Viewer node), they cannot be read. If you want to transfer attributes from one geometry to another, you would have to do it with Sample Index. $\endgroup$
    – quellenform
    May 3, 2023 at 9:44

2 Answers 2

10
$\begingroup$

TLDR: if you skim through and search for a bold text like this paragraph, it might be enough to explain what's going on.

This will be a third post in my series on how to read geonode trees [links at the bottom]. In both previous I recommend reading the node tree ⬅ from right to left, however there is a limitation within which you can read fragments from left ➡ to right: a circular socket 🟣 generates a single value (it's calculated for each tree user once per frame), so it calculates a value based only on inputs from left, regardless of what is connected to the outputs on the right. The only caveat is such a node won't run at all if it's not connected (directly or indirectly) to the Group Output - but it hardly matters, due to the purely functional programming nature of geonodes; there are no side effects (other than performance related: memory usage, execution time, crash risk), so you can reason about your node tree by assuming every such node actually runs.

Since your node tree consists mostly of nodes outputting such 'constants' in the form of 🟢Geometry, it's a good opportunity to try to read it from the left ➡ to right.

I numbered some nodes to represent steps.

1 Cube

"Cube" node generates and outputs mesh data in the form:

Index Vertices Edges Faces
coords xyz vertex indices vertex indices
0 -0.5, -0.5, -0.5 0, 1 0, 2, 3, 1
1 0.5, -0.5, -0.5 0, 2 0, 1, 5, 4
2 -0.5, 0.5, -0.5 2, 3 4, 5, 7, 6
3 0.5, 0.5, -0.5 0, 4 2, 6, 7, 3
4 -0.5, -0.5, 0.5 1, 5 0, 4, 6, 2
5 0.5, -0.5, 0.5 4, 6 1, 3, 7, 5
6 -0.5, 0.5, 0.5 5, 7
7 0.5, 0.5, 0.5 2, 6
8 6, 7
9 1, 3
10 4, 5
11 3, 7

There's more data than that: UV map (that's face corner domain not even mentioned in the table above), edges contain information like sharpness, faces contain information like material slot index etc. This data also is stored a little bit differently, for example in case of vertex coordinates it's just a one-dimensional array:

>>> import numpy as np
>>> d = C.object.data
>>> coords = [0.0]*len(d.vertices)*3
>>> d.vertices.foreach_get('co', coords)
>>> coords
[-0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5]

Still, the table is useful to get the point across: It is this table, not some kind of a pointer, handle or name of the geometry, that is passed around in the 🟢 green geometry links. The entire geometry is copied in each link, that's why the following doubles geometry:

2 Split Edges

"Split Edges" node gets the table, splits all edges and outputs altered mesh:

Index Vertices Edges Faces
coords xyz vertex indices vertex indices
0 -0.5, -0.5, -0.5 8, 10 8, 12, 14, 10
1 0.5, -0.5, -0.5 8, 12 9, 11, 18, 16
2 -0.5, 0.5, -0.5 12, 14 17, 19, 22, 20
3 0.5, 0.5, -0.5 9, 16 13, 21, 23, 15
4 -0.5, -0.5, 0.5 11, 18 0, 4, 6, 2
5 0.5, -0.5, 0.5 17, 20 1, 3, 7, 5
6 -0.5, 0.5, 0.5 19, 22
7 0.5, 0.5, 0.5 13, 21
8 -0.5, -0.5, -0.5 20, 22
9 -0.5, -0.5, -0.5 10, 14
10 0.5, -0.5, -0.5 16, 18
11 0.5, -0.5, -0.5 15, 23
12 -0.5, 0.5, -0.5 9, 11
13 -0.5, 0.5, -0.5 0, 2
14 0.5, 0.5, -0.5 13, 15
15 0.5, 0.5, -0.5 0, 4
16 -0.5, -0.5, 0.5 1, 5
17 -0.5, -0.5, 0.5 4, 6
18 0.5, -0.5, 0.5 5, 7
19 0.5, -0.5, 0.5 2, 6
20 -0.5, 0.5, 0.5 21, 23
21 -0.5, 0.5, 0.5 1, 3
22 0.5, 0.5, 0.5 17, 19
23 0.5, 0.5, 0.5 3, 7

You can imagine this table being put inside the socket:

And once this socket is filled with this table, it is sealed, and the table can't be replaced with another, nor can the contents of this table be changed.

3 Capture Attribute

"Capture Attribute" node is set to operate on points, so it creates a new column of data, and on reach row stores a calculated value. So it sets the context to vert#0 and requests data from the field link, which is connected to Position node, sending -0.5, -0.5, -0.5, then it stores this data on the first row of the new columns, sets the context to vert#1, requests data, Position node reads current vertex position and sends 0.5, -0.5, -0.5 and so on until the column is filled:

Index Vertices Vertices Edges Faces
coords xyz anonymous attribute vertex indices vertex indices
ID # 9192631770
0 -0.5, -0.5, -0.5 -0.5, -0.5, -0.5 8, 10 8, 12, 14, 10
1 0.5, -0.5, -0.5 0.5, -0.5, -0.5 8, 12 9, 11, 18, 16
2 -0.5, 0.5, -0.5 -0.5, 0.5, -0.5 12, 14 17, 19, 22, 20
3 0.5, 0.5, -0.5 0.5, 0.5, -0.5 9, 16 13, 21, 23, 15
4 -0.5, -0.5, 0.5 -0.5, -0.5, 0.5 11, 18 0, 4, 6, 2
5 0.5, -0.5, 0.5 0.5, -0.5, 0.5 17, 20 1, 3, 7, 5
6 -0.5, 0.5, 0.5 -0.5, 0.5, 0.5 19, 22
7 0.5, 0.5, 0.5 0.5, 0.5, 0.5 13, 21
8 -0.5, -0.5, -0.5 -0.5, -0.5, -0.5 20, 22
9 -0.5, -0.5, -0.5 -0.5, -0.5, -0.5 10, 14
10 0.5, -0.5, -0.5 0.5, -0.5, -0.5 16, 18
11 0.5, -0.5, -0.5 0.5, -0.5, -0.5 15, 23
12 -0.5, 0.5, -0.5 -0.5, 0.5, -0.5 9, 11
13 -0.5, 0.5, -0.5 -0.5, 0.5, -0.5 0, 2
14 0.5, 0.5, -0.5 0.5, 0.5, -0.5 13, 15
15 0.5, 0.5, -0.5 0.5, 0.5, -0.5 0, 4
16 -0.5, -0.5, 0.5 -0.5, -0.5, 0.5 1, 5
17 -0.5, -0.5, 0.5 -0.5, -0.5, 0.5 4, 6
18 0.5, -0.5, 0.5 0.5, -0.5, 0.5 5, 7
19 0.5, -0.5, 0.5 0.5, -0.5, 0.5 2, 6
20 -0.5, 0.5, 0.5 -0.5, 0.5, 0.5 21, 23
21 -0.5, 0.5, 0.5 -0.5, 0.5, 0.5 1, 3
22 0.5, 0.5, 0.5 0.5, 0.5, 0.5 17, 19
23 0.5, 0.5, 0.5 0.5, 0.5, 0.5 3, 7

4 Another Capture Attribute

Right "Capture Attribute" node works very similarly: it gets the table from above (the one with one anonymous attribute column), and adds yet another column, this time for the face domain. First it sets the context to face#0, asks the field for data, "Position" node hears this request over the field link and given the context is face#0, it sends the position of the face, however it is implemented to obtain it: 0, 0, -0.5. Then the "Capture Attribute" node saves this data, changes context to face#1 and repeats until all faces have been evaluated.

Index Vertices Vertices Edges Faces Faces
coords xyz anonymous attribute vertex indices vertex indices anonymous attribute
ID # 9192631770 ID # 299792456
0 -0.5, -0.5, -0.5 -0.5, -0.5, -0.5 8, 10 8, 12, 14, 10 0.0, 0.0, -0.5
1 0.5, -0.5, -0.5 0.5, -0.5, -0.5 8, 12 9, 11, 18, 16 0.0, -0.5, 0.0
2 -0.5, 0.5, -0.5 -0.5, 0.5, -0.5 12, 14 17, 19, 22, 20 0.0, 0.0, 0.5
3 0.5, 0.5, -0.5 0.5, 0.5, -0.5 9, 16 13, 21, 23, 15 0.0, 0.5, 0.0
4 -0.5, -0.5, 0.5 -0.5, -0.5, 0.5 11, 18 0, 4, 6, 2 -0.5, 0.0, 0.0
5 0.5, -0.5, 0.5 0.5, -0.5, 0.5 17, 20 1, 3, 7, 5 0.5, 0.0, 0.0
Skipping the rest with dedication to mouse wheels

5 Set Position

"Set Position" node iterates over points (here: vertices). For each point it evaluates "Selection" - if it evaluates to *True", the two vector sockets are evaluated and added together, the position of the currently evaluated point is set to the result and the context is moved to the next point, until all points have been evaluated.

⚠ This is the moment when you have to again read the nodes ⬅ from right to left❗

  1. Set the context to vert#0.
  2. "Selection" by default is True.
  3. Evaluate "Position" field.
  4. "Position" field leads to "Mix" node, so execution goes there. ⬅
  5. "Mix" node evaluates the "Fac" to be the default 0.322.
  6. "Mix" then evaluates "A" connected to "Capture Attribute" (3) node, so execution goes there ⬅
  7. This is the crucial part. "Capture Attribute" node is not a sampling node, it doesn't change context when asked for a value of the attribute. It simply shares the ID of the anonymous [it's like on the Internet, you're never truly anonymous] attribute: 91926317701.
  8. The execution returns to "Mix" ➡
  9. "Mix" accesses the anonymous attribute column #91926317701 for the first row (because currently vertex 0 is evaluated). But there is no such column! The mesh data being evaluated is the mesh data passed to the upper "Set Position" (5), which never had this column created. Here are some examples how Blender devs could implement resolving such a conflict:
  • crashing Blender,
  • displaying a warning ⚠ sign on the top of the "Set Position" (5) node with a tooltip "Can't evaluate nonexistent attribute",
  • have AI rewire the nodes internally in hopes it achieves the result the user intended,
  • set the value to NaN or 3 or 4 NaNs for vectors or RGBA colors and maybe -1 for integers,
  • set all values to 0; and they decided to use the last option.
  1. "Mix" treats "B" similarly to "A" in points 6-9 which this time tries to read from also non-existent column ID#299792458, and also evaluates to <0, 0, 0>.
  2. "Mix" makes a calculation $(1-\mathtt{Fac})×A + \mathtt{Fac}×B = (1-0.322)×0 + 0.322×0 = 0 + 0 = 0$ giving the same result for each of 3 vector components.
  3. The result <0.0, 0.0, 0.0> is sent back through the field ➡
  4. "Set Position" receives the vector <0.0, 0.0, 0.0>, adds default <0.0, 0.0, 0.0> offset vector, which results again with <0.0, 0.0, 0.0> - that's where the currently evaluated vert#0 is positioned. Everything is repeated from point 1. except now for vert#1, then for vert#2, vert#3 and so on until #vert23 is evaluated.
Index Vertices Edges Faces
coords xyz vertex indices vertex indices
0 0.0, 0.0, 0.0 0, 1 0, 2, 3, 1
1 0.0, 0.0, 0.0 0, 2 0, 1, 5, 4
2 0.0, 0.0, 0.0 2, 3 4, 5, 7, 6
3 0.0, 0.0, 0.0 0, 4 2, 6, 7, 3

6 Another Set Position

This answer's length got out of hand so let's just mention how this time the context goes through a table that actually has two anonymous columns ID#9192631770 and ID#299792458, and in the latter case has to interpolate from faces to vertices, which is simply an average of all faces connected to (built upon) currently evaluated vertex. since non-zero values reside in this column, the "Mix" produces various non-zero colors and the lower "Set Position" (6) node therefore positions the vertices differently


More reading:

$\endgroup$
2
  • 1
    $\begingroup$ This answer is very illuminating. The concept of the table that's passed between nodes with a specific, anonymous, additional column is something that I had entirely misunderstood. $\endgroup$
    – libcrypt
    May 4, 2023 at 3:09
  • 1
    $\begingroup$ I would give you an award if I could. Thank you! $\endgroup$ Jun 21, 2023 at 11:23
4
$\begingroup$

The Capture Attribute node passes the attribute values stored within the geometry and they are assigned to the index of the vertices or faces. And though the geometry entering the first Set Position node has 24 vertices indexed 0 to 23 and the geometry entering the second Set Position also has 24 vertices indexed 0 to 23, they are not the same. Those are not global indices. Each geometry output creates geometry with its own index starting at 0 (but in case of successive geometries they are passed on from one to the next).

But altogether, for example if you use a Join Geometry node, you have 48 vertices and joined they have indices 0 to 47. The problem is, the Capture Attribute node assigns the result of the Mix node to the second 24 vertices. For the first 24 it holds no results, so they all get a default 0. Hence you see nothing from the first Viewer node.

If you use Join Geometry and the Spreadsheet to see the values assigned to the vertices, you can see that one half of the vertices carries results and the others are all 0. If indices 0-23 are carrying the results or indices 24-47 depends on the order in which you plug them into the Join Geometry node.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .