Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/pr-preview/pr-35/.nojekyll b/pr-preview/pr-35/.nojekyll new file mode 100644 index 0000000..f173110 --- /dev/null +++ b/pr-preview/pr-35/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/pr-preview/pr-35/404.html b/pr-preview/pr-35/404.html new file mode 100644 index 0000000..2d5b3cb --- /dev/null +++ b/pr-preview/pr-35/404.html @@ -0,0 +1,217 @@ + + +
+ + +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +Now that we've covered the basics, let's take a look at some more advanced scripting techniques.
+ +An important concept in Geometry Nodes is attributes. Many trees capture attributes or transfer them from one domain to another.
+When using these methods, the data_type
argument must be correctly specified for the transfer to work as intended.
@tree("Skin")
+def skin():
+ # Create a cube
+ c = cube()
+ # Create a sphere
+ sphere = uv_sphere()
+ # Transfer the position to the sphere
+ transferred_position = c.transfer_attribute(
+ data_type=TransferAttribute.DataType.FLOAT_VECTOR,
+ attribute=position()
+ )
+ # Make the sphere conform to the shape of the cube
+ return sphere.set_position(position=transferred_position)
+
+To improve the usability of these nodes, capture(...)
and transfer(...)
methods are provided on Geometry
that simply take the attribute and any other optional arguments.
@tree("Skin")
+def skin():
+ # Create a cube
+ c = cube()
+ # Create a sphere
+ sphere = uv_sphere()
+ # Make the sphere conform to the shape of the cube
+ return sphere.set_position(position=c.transfer(position()))
+
+The same is available for capture(...)
.
geometry_with_attribute, attribute = c.capture(position())
+
+++You must use the
+Geometry
returned fromcapture(...)
for the anonymous attribute it creates to be usable.
Any additional keyword arguments can be passed as normal.
+c.transfer(position(), mapping=TransferAttribute.Mapping.INDEX)
+
+Custom attributes can be created by name.
+The safest way to use named attributes is with the Attribute
class.
Create a named attribute with a data type and optional domain, then use the store(...)
, exists()
, and __call__(...)
methods to use it.
# Create the attribute
+my_custom_attribute = Attribute(
+ "my_custom_attribute",
+ NamedAttribute.DataType.FLOAT, # declare the data type once
+ StoreNamedAttribute.Domain.INSTANCE # optional
+)
+# Store a value
+geometry = my_custom_attribute.store(geometry, 0.5)
+# Use the value by calling the attribute
+geometry = geometry.set_position(offset=my_custom_attribute())
+
+In Blender 3.4+, transfer attribute was replaced with a few separate nodes: Sample Index, Sample Nearest, and Sample Nearest Surface.
+To avoid inputting data types and geometry manually, you can use the custom Geometry
subscript.
The structure for these subscripts is:
+geometry[value : index or sample position : domain, mode, domain]
+
+Only the value argument is required. Other arguments can be supplied as needed.
+geometry[value]
+geometry[value : sample_position, SampleMode.NEAREST]
+geometry[value : index() + 1 : SampleIndex.Domain.EDGE]
+
+Try passing different arguments and see how the resulting nodes are created.
+ +The Boolean Math node gives access to common boolean operations, such as AND
, NOT
, XOR
, etc.
However, it can be cumbersome to use the boolean_math
function in complex boolean expressions.
# Check if the two values equal, or if the first is true.
+x = False
+y = True
+return boolean_math(
+ operation=BooleanMath.Operation.OR
+ boolean=(
+ boolean_math(
+ operation=BooleanMath.Operation.XNOR # Equal
+ boolean=(x, y)
+ ),
+ x
+ )
+)
+
+A few operators are available to make boolean math easier and more readable.
+# Check if the two values equal, or if the first is true.
+x = False
+y = True
+return (x == y) | x
+
+The operators available are:
+==
- XNOR
!=
- XOR
|
- OR
&
- AND
~
- NOT
++ +You cannot use the built-in Python keywords
+and
,or
, andnot
. You must use the custom operators above to create Boolean Math nodes.
Some nodes, such as Float Curve take a curve as a property. You can create a curve with the Curve
class.
float_curve(
+ mapping=Curve(
+ Point(0, 0),
+ Point(0.5, 0.25),
+ Point(1, 1, HandleType.VECTOR), # Optionally specify a handle type, such as `AUTO`, `VECTOR`, or `AUTO_CLAMPED`.
+ )
+)
+
+
+You can also pass the points as a list to Curve
.
points = [Point(0, 0), Point(1, 1)]
+float_curve(
+ mapping=Curve(points)
+)
+
+If a node has multiple curve properties, such as the Vector Curves node, pass a list of curves to the node.
+vector_curves(
+ mapping=[x_curve, y_curve, z_curve]
+)
+
+
+ Drivers can be used with geometry nodes. To create a scripted expression driver, use the scripted_expression
convenience function.
This can be used to get information like the current frame number in a Geometry Script.
+frame_number = scripted_expression("frame")
+frame_number_doubled = scripted_expression("frame * 2")
+
+
+ Python has support for generators using the yield
keyword.
Geometry Script tree functions can be represented as generators to output multiple values. If every generated value is Geometry
, the values are automatically connected to a Join Geometry node and output as a single mesh.
@tree("Primitive Shapes")
+def primitive_shapes():
+ yield cube()
+ yield uv_sphere()
+ yield cylinder().mesh
+
+
+However, if any of the outputs is not Geometry
, separate sockets are created for each output.
@tree("Primitive Shapes and Integer")
+def primitive_shapes():
+ yield cube()
+ yield uv_sphere()
+ yield cylinder().mesh
+ yield 5 # Not a geometry socket type
+
+
+++ +The first output is always displayed when using a Geometry Nodes modifier. Ensure it is a
+Geometry
socket type, unless you are using the function as a node group.
Some geometry node trees need a lot of arguments.
+@tree("Terrain Generator")
+def terrain_generator(
+ width: Float
+ height: Float
+ resolution: Int
+ scale: Float
+ w: Float
+):
+ ...
+
+There are a couple of problems with this. Firstly, the function signature is getting long. This can make it harder to visually parse the script. And, if we want to use the same arguments in another tree and pass them through to terrain
, we need to make sure to keep everything up to date.
This is where input groups come in. An input group is class that contains properties annotated with valid socket types.
+To create an input group, declare a new class that derives from InputGroup
.
class TerrainInputs(InputGroup):
+ width: Float
+ height: Float
+ resolution: Int
+ scale: Float
+ w: Float
+
+Then annotate an argument in your tree function with this class.
+@tree("Terrain Generator")
+def terrain_generator(
+ inputs: TerrainInputs
+):
+ ...
+
+This will create a node tree with the exact same structure as the original implementation. The inputs can be accessed with dot notation.
+size = combine_xyz(x=input.width, y=input.height)
+
+And now passing the inputs through from another function is much simpler.
+def point_terrain(
+ terrain_inputs: TerrainInputs,
+ radius: Float
+):
+ return terrain_generator(
+ inputs=terrain_inputs
+ ).mesh_to_points(radius=radius)
+
+If you nest calls to tree functions, you can instantiate the InputGroup
subclass to pass the correct inputs.
def point_terrain():
+ return terrain_generator(
+ inputs=TerrainInputs(
+ width=5,
+ height=5,
+ resolution=10,
+ scale=1,
+ w=0
+ )
+ ).mesh_to_points()
+
+If you use the same InputGroup
multiple times, you need to provide a prefix. Otherwise, inputs with duplicate names will be created on your tree.
To do this, use square brackets next to the annotation with a string for the prefix.
+def mountain_or_canyon(
+ mountain_inputs: TerrainInputs["Mountain"], # Prefixed with 'Mountain'
+ canyon_inputs: TerrainInputs["Canyon"], # Prefixed with 'Canyon'
+ is_mountain: Bool
+):
+ return terrain_generator(
+ inputs=switch(switch=is_mountain, true=mountain_inputs, false=canyon_inputs)
+ )
+
+
+ A Geometry Script can have more than one tree function. Each tree function is a node group, and tree functions can be used in other tree functions to create Node Group nodes.
+@tree("Instance Grid")
+def instance_grid(instance: Geometry):
+ """ Instance the input geometry on a grid """
+ return grid().mesh_to_points().instance_on_points(instance=instance)
+
+@tree("Cube Grid")
+def cube_grid():
+ """ Create a grid of cubes """
+ return instance_grid(instance=cube(size=0.2))
+
+The Cube Grid tree uses the Instance Grid node group by calling the instance_grid
function:
The Instance Grid node group uses the passed in instance
argument to create a grid of instances:
This concept can scale to complex interconnected node trees, while keeping everything neatly organized in separate functions.
+You do not have to mark a function with @tree(...)
. If you don't, function calls are treated as normal in Python. No checks are made against the arguments. Any nodes created in the callee will be placed in the caller's tree.
def instance_grid(instance: Geometry): # Not marked with `@tree(...)`
+ return grid().mesh_to_points().instance_on_points(instance=instance)
+
+@tree("Cube Grid")
+def cube_grid(): # Marked with `@tree(...)`
+ return instance_grid(instance=cube(size=0.2))
+
+The above example would place the Grid, Mesh to Points, and Instance on Points nodes in the main "Cube Grid" tree. It could be rewritten as:
+@tree("Cube Grid")
+def cube_grid():
+ return grid().mesh_to_points().instance_on_points(instance=cube(size=0.2))
+
+
+ Blender 4.0 introduced repeat zones.
+Using a Repeat Input and Repeat Output node, you can loop a block of nodes for a specific number of iterations.
+You must use the @repeat_zone
decorator to create these special linked nodes.
from geometry_script import *
+
+@tree
+def test_loop(geometry: Geometry):
+ @repeat_zone
+ def doubler(value: Float):
+ return value * 2
+ return points(count=doubler(5, 1)) # double the input value 5 times.
+
+The function should modify the input values and return them in the same order.
+When calling the repeat zone, pass the Iterations argument first, then any other arguments the function accepts.
+For example:
+def doubler(value: Float) -> Float
+
+would be called as:
+doubler(iteration_count, value)
+
+When a repeat zone has multiple arguments, return a tuple from the zone.
+@repeat_zone
+def multi_doubler(value1: Float, value2: Float):
+ return (value1 * 2, value2 * 2)
+
+
+ Blender 3.6 includes simulation nodes.
+Using a Simulation Input and Simulation Output node, you can create effects that change over time.
+As a convenience, the @simulation_zone
decorator is provided to make simulation node blocks easier to create.
from geometry_script import *
+
+@tree
+def test_sim(geometry: Geometry):
+ @simulation_zone
+ def my_sim(delta_time, geometry: Geometry, value: Float):
+ return (geometry, value)
+ return my_sim(geometry, 0.26).value
+
+The first argument should always be delta_time
. Any other arguments must also be returned as a tuple with their modified values.
+Each frame, the result from the previous frame is passed into the zone's inputs.
+The initial call to my_sim
in test_sim
provides the initial values for the simulation.
A "Skip" argument was added to the Simulation Output node in Blender 4.0.
+Return a boolean value first from any simulation zone to determine whether the step should be skipped.
+The simplest way to migrate existing node trees is by adding False
to the return tuple.
@simulation_zone
+def my_sim(delta_time, geometry: Geometry, value: Float):
+ return (False, geometry, value)
+
+You can pass any boolean value as the skip output.
+ +Creating Geometry Scripts can be as easy or complex as you want for your project. +Throughout this guide, scripts will be displayed alongside the generated nodes to provide context on how a script relates to the underlying nodes.
+Setting up an editor for external editing is recommended when writing scripts, but internal editing inside Blender will suffice for the simple examples shown here.
+ +The first step when writing is script is importing the geometry_script
module. There a are a few ways of doing this:
This will import every type and function available into your script. It can make it easy to discover what's available with code completion, and makes the scripts more terse.
+from geometry_script import *
+
+cube(...) # Available globally
+my_geo: Geometry # All types available as well
+
+This will import only the specified names from the module:
+from geometry_script import cube, Geometry
+
+cube(...) # Available from import
+my_geo: Geometry
+
+This will import every type and function, and place them behind the namespace. You can use the module name, or provide your own.
+import geometry_script
+
+geometry_script.cube(...) # Prefix with the namespace
+my_geo: geometry_script.Geometry
+
+import geometry_script as gs
+
+gs.cube(...) # Prefix with the custom name
+my_geo: gs.Geometry
+
+Now that you have Geometry Script imported in some way, let's create a tree.
+ +Because scripts are converted to Geometry Node trees, you typically cannot use default Python types as arguments. In some cases, they will be automatically converted for you, but in general you will be dealing with socket types.
+A socket is any input or output on a node. Take the Cube node for example:
+ +This node has 4 input sockets, and 1 output socket.
+Vector
Int
Int
Int
Geometry
A socket does not represent a value itself. For example, the Size
socket does not necessarily represent the value (1, 1, 1)
. Instead, it can be connected to another node as an input, giving it a dynamic value.
When we write scripts, we typically deal with socket types, not concrete values like (1, 1, 1)
. Take this script for example:
@tree("Cube Tree")
+def cube_tree(size: Vector):
+ return cube(size=size)
+
+The size
argument creates a input socket with the type Vector
. This is then connected to the size
socket of the Cube node.
Our script does not run every time the node tree is evaluated. It only runs once to create the node tree. Therefore, we have no way of knowing what value size
has when the script runs, because it is dynamic.
Sockets are great for passing values between nodes. A socket type like Geometry
does not represent concrete vertices, edges, and faces. Instead, it represents the input or output socket of a node. This lets us use it to create connections between different nodes, by passing the output of one node to the input of another.
Sockets cannot be read for their concrete value. A Float
socket type does not equal 5
or 10
or 3.14
to our script. It only represents the socket of a node. If you try to print(...)
a socket, you will receive a generic reference type with no underlying value.
You might be wondering, "if you can't access the value of a socket, what can you do with it?"
+Geometry Script provides many helpful additions that make working with sockets about as easy as working with a concrete value.
+Socket types can be used to perform math operations. The proper Math node will be created automatically for you, so you can focus on writing a script and not thinking about sockets. If you use Float
or Int
it will create a Math node, and if you use a Vector
it will create a Vector Math node.
@tree("Cube Tree")
+def cube_tree(size: Vector):
+ doubled = size * (2, 2, 2) # Multiply each component by 2
+ return cube(size=doubled)
+
+
+Several common math operations are available, such as:
+socket + 2
)socket - 2
)socket * 2
)socket / 2
)socket % 2
)Socket types can be compared with Python comparison operators. A Compare node will be created with the correct inputs and options specified.
+@tree("Cube Tree")
+def cube_tree(size: Vector):
+ show_cube = size > (2, 2, 2) # Check if each component is greater than 2
+ return cube(size=show_cube)
+
+
+Several common comparison operators are supported, such as:
+socket == 2
)socket != 2
)socket < 2
)socket <= 2
)socket > 2
)socket >= 2
)While the Vector
type does not equate to three concrete components, such as (1, 2, 3)
, you can still access the x
, y
, and z
components as sockets. A Separate XYZ node will be created with the correct inputs and outputs specified.
@tree("Cube Tree")
+def cube_tree(size: Vector):
+ height = size.z # Access the Z component
+ # Multiply the height by 2 but leave the other components unchanged.
+ return cube(size=combine_xyz(x=size.x, y=size.y, z=height * 2))
+
+For each component access, a Separate XYZ node is created.
+ +Any node function can be called on a socket type. This will automatically connect the socket to the first input of the node.
+@tree("Cube Tree")
+def cube_tree(size: Vector):
+ return cube(size=size).mesh_to_volume()
+
+The output of the Cube node (a Geometry
socket type) is connected to the first input of the Mesh to Volume node.
The same script without chaining calls is written more verbosely as:
+@tree("Cube Tree")
+def cube_tree(size: Vector):
+ return mesh_to_volume(mesh=cube(size=size))
+
+Often times you want each chained calls to be on a separate line. There are a few ways to do this in Python:
+cube(
+ size=size
+).mesh_to_volume()
+
+(cube(size=size)
+ .mesh_to_volume())
+
+cube(size=size) \
+ .mesh_to_volume()
+
+
+ Node trees are created by decorating a function with @tree
. Let's try creating a simple tree function.
++The code samples for the rest of the book assume you are importing all names with
+from geometry_script import *
. However, if you are using a namespaced import, simply prefix the functions and types withgeometry_script
or your custom name.
@tree
+def cube_tree():
+ ...
+
+By default, the name of your function will be used as the name of the generated node tree. However, you can specify a custom name by passing a string to @tree
:
@tree("Cube Tree")
+def cube_tree():
+ ...
+
+Every node tree is required to return Geometry
as the first output. Let's try returning a simple cube.
@tree("Cube Tree")
+def cube_tree():
+ return cube()
+
+Here we call the cube(...)
function, which creates a Cube node and connects it to the Group Output.
You can also return multiple values. However, Geometry
must always be returned first for a tree to be valid.
@tree("Cube Tree")
+def cube_tree():
+ return cube(), 5
+
+
+By default, each output is named 'Result'. To customize the name, return a dictionary.
+@tree("Cube Tree")
+def cube_tree():
+ return {
+ "My Cube": cube(),
+ "Scale Constant": 5
+ }
+
+
+All arguments in a tree function must be annotated with a valid socket type. These types are provided by Geometry Script, and are not equivalent to Python's built-in types. Let's add a size argument to our Cube Tree.
+@tree("Cube Tree")
+def cube_tree(size: Vector):
+ return cube(size=size)
+
+This creates a Size socket on the Group Input node and connects it to our cube.
+ +The option is available on the Geometry Nodes modifier.
+ +The available socket types match those in the UI. Here are some common ones:
+Geometry
Float
Int
Vector
++You cannot use Python's built-in types in place of these socket types.
+
In the next chapter, we'll take a closer look at how socket types work, and what you can and cannot do with them.
+You can specify a default for any argument, and it will be set on the modifier when added:
+@tree("Cube Tree")
+def cube_tree(size: Vector = (1, 1, 1)):
+ return cube(size=size)
+
+
+
+ Node functions are automatically generated for the Blender version you are using. This means every node will be available from geometry script.
+This means that when future versions of Blender add new nodes, they will all be available in Geometry Script without updating the add-on.
+To see all of the node functions available in your Blender version, open the Geometry Script menu in the Text Editor and click Open Documentation.
+ +This will open the automatically generated docs page with a list of every available node and it's inputs and outputs.
+All nodes are mapped to functions in the same way, so even without the documentation you can decifer what a node will equate to. Using an IDE with code completion makes this even easier.
+The general process is:
+++Properties and inputs are different types of argument. A property is a value that cannot be connected to a socket. These are typically enums (displayed in the UI as a dropdown), with specific string values expected. Check the documentation for a node to see what the possible values are for a property.
+
Many nodes have enum properties. For example, the math node lets you choose which operation to perform. You can pass a string to specify the enum case to use. But a safer way to set these values is with the autogenerated enum types. The enums are namespaced to the name of the node in PascalCase:
+# Access it by Node.Enum Name.Case
+math(operation=Math.Operation.ADD)
+math(operation=Math.Operation.SUBTRACT)
+math(operation='MULTIPLY') # Or manually pass a string
+
+Internally, this type is generated as:
+import enum
+class Math:
+ class Operation(enum.Enum):
+ ADD = 'ADD'
+ SUBTRACT = 'SUBTRACT'
+ MULTIPLY = 'MULTIPLY'
+ ...
+ ...
+
+The cases will appear in code completion if you setup an external editor.
+Some nodes use the same input name multiple times. For example, the Math node has three inputs named value
. To specify each value, pass a tuple for the input:
math(operation=Math.Operation.WRAP, value=(0.5, 1, 0)) # Pass all 3
+math(operation=Math.Operation.WRAP, value=(0.5, 1)) # Only pass 2/3
+math(operation=Math.Operation.WRAP, value=0.5) # Only pass 1/3
+
+
+Here are two examples to show how a node maps to a function.
+Cube
-> cube
size: Vector
vertices_x: Int
vertices_y: Int
vertices_z: Int
Geometry
The node can now be used as a function:
+cube() # All arguments are optional
+cube(size=(1, 1, 1), vertices_x=3) # Optionally specify keyword arguments
+cube_geo: Geometry = cube() # Returns a Geometry socket type
+
+The generated documentation will show the signature, result type, and chain syntax.
+cube(
+ size: VectorTranslation,
+ vertices_x: Int,
+ vertices_y: Int,
+ vertices_z: Int
+)
+
+mesh: Geometry
+
+size: VectorTranslation = ...
+size.cube(...)
+
+Capture Attribute
-> capture_attribute
data_type: CaptureAttribute.DataType
domain: CaptureAttribute.Domain
geometry: Geometry
value: Vector | Float | Color | Bool | Int
{ geometry: Geometry, attribute: Int }
The node can now be used as a function:
+result = capture_attribute(data_type=CaptureAttribute.DataType.BOOLEAN, geometry=cube_geo) # Specify a property and an input
+result.geometry # Access the geometry
+result.attribute # Access the attribute
+
+The generated documentation will show the signature, result type, and chain syntax.
+capture_attribute(
+ data_type: CaptureAttribute.DataType,
+ domain: CaptureAttribute.Domain,
+ geometry: Geometry,
+ value: Vector | Float | Color | Bool | Int
+)
+
+{ geometry: Geometry, attribute: Int }
+
+geometry: Geometry = ...
+geometry.capture_attribute(...)
+
+
+ Geometry Script is a scripting API for Blender's Geometry Nodes. +It makes complicated node trees more managable and easy to share.
+Here's a simple example of what's possible with a short script:
+from geometry_script import *
+
+@tree("Repeat Grid")
+def repeat_grid(geometry: Geometry, width: Int, height: Int):
+ g = grid(
+ size_x=width, size_y=height,
+ vertices_x=width, vertices_y=height
+ ).mesh_to_points()
+ return g.instance_on_points(instance=geometry)
+
+