This tutorial, a follow-up to Creative Coding in Blender, focuses on creating patterns on materials in Blender’s Cycles renderer. There are a few reasons why patterning can be challenging at first. Tutorials for OpenGL Shading Language (GLSL) abound, but the node-based visual programming in Blender has a slightly different toolset. While Open Shading Language provides an alternative, it has a few drawbacks:
- OSL requires that a render be done on the CPU, not the GPU.
- There’s a potential dependency, where .
oslfiles may have to be tracked along with the
- Documentation is segmented — and inconsistent — between the OSL language specification and Blender’s documentation.
- OSL may encourage a ‘kitchen sink’ or ‘black box’ style, in which we don’t see how the logic of an OSL script node works and cannot quickly revise it. We have to switch back and forth between the node and text editor (internal or external) to change the pattern.
- OSL can be problematic in collaborative scenarios, where one or more collaborators focus less on the scripting and more on aesthetics.
For these reasons, we’ll group built-in nodes into utility functions that facilitate translating shader patterns from other languages into Blender. There are several add-ons, such as Jacques Lucke’s Animation Nodes and Skorupa & Zaal’s Node Wrangler, which include functionality developed here. This article aims not provide a recipe to simulate a realistic texture or be comprehensive, but to illustrate how a tool set can be developed.
Knowledge of OSL is not necessary to follow along, but code snippets are included to bridge the gap between visual and script-based programming. After we create a node-based tool set, we’ll look into how to generate nodes and materials with Python, then append them to generated objects.
This was written for Blender v. 2.79.
Creating Node Groups
We create the node groups below by selecting nodes in the node editor and pressing
Ctrl-G. To edit a group that has already been created we either click on the linked white boxes in the upper right corner of a node group instance or press
Tab. To return to the main material from editing the group, we either press
Tab again or click on the bent arrow in the node editor header, labeled ‘Go to parent node tree.’
The inputs on the left side of a group node viewed from the outside are represented by a
Group Input node inside the group; the outputs on the right, by the
Group Output node. The interface between inside and outside is in the properties region, which can be opened by going to
View > Properties or by pressing
N. To add new inputs or output sockets, we link nodes to the clear unlinked socket at the bottom of either the
Group Input or
Group Output node. The
+ buttons on the bottom of the interface do the same.
The interface will let us delete sockets, switch their order with the down and up arrows, change their names and default values. Depending on the data type of the input or output, the minimum and maximum bound of the value can also be set. Take care when adding new values that minimums and maximums are suitable.
We can assign a fake user to the node group by clicking on the
F on the right side of the drop-down menu. The number to the left will tell us the number of users the group node has. If we create these groups in the startup Blender file (to which we can return with
Ctrl-N), then press
Ctrl-U to update it, we can store custom group nodes on a long-term basis.
The comparisons ≥ and ≤ are not included in the math node, but can be derived by subtracting the return value of > or < from 1. The
step function common to shader languages is, in effect, a less-than comparison.
Not Equals (neq)
We can use
v >= lower if we want the lower bound to be inclusive;
v <= upper if we want the upper bound to be inclusive.
Because very large and very small input numbers are not anticipated, this simplifies a more robust approximation function, which can be found here.
These logic gates follow Python’s example when converting from a real number to a boolean: any non-zero value is true.
and depends on
or depends on
Not And (nand)
nand depends on
Not Or (nor)
nor depends on
Exclusive Or (xor)
xor depends on
Real Number Operations
This could be parlayed into a
Hyperbolic Cosine (cosh)
cosh depends on
The hyperbolic secant,
Hyperbolic Sine (sinh)
sinh depends on
The hyperbolic cosecant,
Hyperbolic Tangent (tanh)
tanh depends on
The hyperbolic cotangent,
While not a function in shader languages,
map allows a value in one range to be converted to to the equivalent value in another range.
Mod / Floor Mod
mod depends on
modulo function is
fmod, represented in some programming languages by the
% operator. The distinction between a truncation modulo and floor modulo is relevant when working with periodic ranges: when
b is positive and
a is negative,
fmod will return a negative value;
mod, a positive value. For example,
mod is handy when traveling counter-clockwise on the color wheel from orange to magenta.
quantize depends on
Quantizing a value reduces it from a larger to a smaller set, in effect converting smoother transitions to sharper, more distinct ones.
The above example illustrates, using Vermeer’s View of Delft, the outcome of applying
quantize to the components of a
vector. A quantized color has a reduced color palette (seen on the top row). A quantized texture coordinate crenelates the image; not pictured, a vector could also be treated as a direction, where its polar or spherical coordinates are quantized.
This multiplies the input degrees by
pi / 180.0 (
fract depends on
Returns the fractional part of a real number. In OSL,
fract is derived from
threshold depends on
clamp, this returns
0 when the value is out-of-bounds.
ceil depends on
ceil biases toward the greater value, so
floor depends on
floor biases toward the lesser value, so
trunc depends on
There are many useful vector operations — such as
fract — which can be described as the separation of a vector into its components, application of real number operations above, and recombination. We omit most of them below, save for uniform and nonuniform scaling.
Minkowski distance is a generalization of Euclidean and Manhattan distance, where
minkowski(a, b, 2.0) = euclidean(a, b) and
minkowski(a, b, 1.0) = manhattan(a, b). (The result of raising a negative base to an even exponent is positive, so the Pythagorean theorem can dispense with
Multiply by a Uniform Scale (mult)
Multiply by a Nonuniform Scale (scale)
Multiplying two vectors component-wise is undefined, but is often supported as a shortcut for multiplying a scale matrix and a spatial coordinate. In the above, height is assigned to the y axis, but could be assigned to z instead.
reflect depends on
refract depends on
rescale depends on
rotate depends on
Vector rotation is handled by the vector mapping node; that node, however, is unwieldy to use, as
scale are not input sockets. Furthermore, we may need to rotate texture coordinates in the range
(0, 0) ..
(1, 1) around a pivot
tile depends on
With volume shader nodes in particular, we don’t always need the entire volume of an object to be filled with scattering and/or absorption. For this reason we mix the
volume shader with a
holdout by a
Shaped Absorb Volume
Shaped Scatter Volume
Scripting Nodes With Python
Python is not needed to create the above, but if we invest the time to write some utility functions, they can help automate the process. A first step is to create an empty group with input and output nodes. (We can reference the info editor’s report console any time we want to find out the name of a property in Python.)
Appending a group to
bpy.data.node_groups is a different procedure than appending an instance of a group node to the node tree of a particular material. Node groups can be thought of as a repository for custom function definitions, which are then called by various materials.
Courtesy the NodeSocketStandard class, scripted nodes may use a greater variety of data types than are seen when creating nodes manually. These include integer and Boolean data types. To maintain consistency with out-of-the-box nodes, however, we avoid using them here.
Example 1. In-Bounds
Because the math node is the cornerstone of so many groups above, it’s helpful to dedicate a function to that. Operations included in the math node can be found here.
Math nodes can be color-coded for further clarity, or parented to a frame. If no name for the node is provide, the function defaults to the operation. With this function in place, we can create the
inbounds operation from above.
For input and output sockets, we create a list of dictionaries. Each entry in a dictionary is a key-value pair, where the key is usually a data-type that is easy to sort and check for equivalence. In this case, the key is a string. When creating a new link between nodes, the input socket receiving the noodle is supplied first, then the output socket.
Example 2. Shaped Scatter Volume
The shaped scatter group depends on
inbounds. To create a group node which depends on others, we need another helper function.
This function checks to see if the prerequisite node group exists, and, as a backup, creates it if not. Since attempting to find a node group by name could raise an error, we enclose this in a
try block. Earlier, we used the dictionary
get function to supply a default alternative when an item could not be located. In this case, however, the alternative requires a function to create the node group. More on exception handling in Python can be read here.
With that in place, we can create the the shaped absorption volume.
Here and with
threshold above, we reference outputs and inputs by number rather than by name. Accessing an element by index can be helpful when two node sockets have the same name; for example, a mix shader labels both inputs as
If we look in the node editor at operations generated through Python, we find a clump of nodes on top of each other, making them hard to manually adjust. We can create a script to automatically arrange them.
This depends on functions to calculate a node’s depth or priority and functions to find the width and height of nodes at a given depth. When nodes have been grouped into depths, they are sorted, then eased from the left edge to the right edge. For positioning, positive on the y-axis is up.
There are many ways to write the heuristic we supply as the second parameter to
calc_priority, this will generate the key for each entry in the
depth_nodes dictionary. We’ll look at two candidates, one in which we arrange nodes by their type, another in which we look at the nodes input and output sockets.
Arranging By Type
Checking Strings for equivalence is a crude solution, as typically we have to deal with mismatching cases (
'WEDNESDAY'), partial matches (
'Wednesday'), trimmable characters (
'VECT MATH') and so on. Since we are using Strings internal to Blender, we keep our checks simple.
The above does not include all possible node types.
Arranging By Connected Sockets
Alternatively, we could sift through each node’s input and output sockets and assign a score based on whether the socket is connected and, in the case of outputs, how many connections it has.
Not all links are valid, as when the output of a node eventually feeds into one of its inputs, creating an undesirable loop. We may also wish to consider adding to or subtracting from
result if a node is unlinked.
An approach not featured here would be to use a function that begins at a pole of the node tree (the material output, for example), and crawls link by link through the nodes, judging priority by distance from that pole.
Putting It All Together
As an example of how these node groups can be strung together and animated, we can modulate the Minkowski distance of a texture coordinate from the center. The Manhattan distance will generate grid-like patterns; the Euclidean, curvilinear patterns.
That modulated distance is supplied to a color gradient. To convert from a smooth gradient to a distinct pattern, we quantize the value. To add variety, the texture coordinate is rotated on the y-axis and tiled before it is measured.
Tiled Ring Pattern
We can also lump everything into a monolithic group node. Here we create a circle which can be shown as stroke only, fill only or stroke and fill.
While the volumes of all four are similar, the geometries that intersect that volume are different. A challenge when creating patterns, as this quote from Edwin A. Abbott’s Flatland reminds us,
You cannot indeed see more than one of my sections, or Circles, at a time; for you have no power to raise your eye out of the plane of Flatland; but you can at least see that, as I rise in Space, so my sections become smaller. See now, I will rise; and the effect upon your eye will be that my Circle will become smaller and smaller till it dwindles to a point and finally vanishes.
is that we must consider the play of volume against surface.
To see how we can apply this pattern to objects created with Python, we next generate a ring of material samples.
To facilitate animation within our material we create a function that inserts a keyframe for an input socket’s default value.
Because we don’t want a keyframe inserted when the value is constant, we only do so if there is more than one entry in
For now, the base color of the principled shader will be linked to a combine HSV node. The
colorsys module and Blender’s Color class in the
mathutils module may also assist in the setting of colors. A geometry node provides the tangent and normal to the principled shader.
Before we can append nodes to the material, we have to ensure that it uses nodes. We then delete the diffuse shader node. For the sampler itself, we create an empty object that will serve as a parent to the others. We then arrange UV spheres in a ring.
By switching into edit mode upon creating each sphere, we can set UV coordinates according to a spherical projection. We toggle back into object mode and set the sphere’s shading to smooth. Next, we add the pattern.
So as to rotate the pattern based on a swatch’s position in the ring, we add a
rotatey group node. The higher a principled shader’s
Transmission the closer its appearance will be to that of glass. The
stroke of the ring pattern is characterized by a rough metallic look. By supplying this value to the material output’s
displacement as well, we also give the stroke a raised edge.
A shaped-absorption and shaped-light node are thrown into the mix. A
map node governs the temperature range, which in turn changes the light’s color based on the horizontal position of the input vector.
The absorption volume is shaped by the fill of the ring pattern, while the light is governed by the stroke.
The full Python script for this sampler can be found here.
Many possibilities open up from here. We could translate techniques from Vivo and Lowe’s Book of Shaders, draw inspiration from GenerateMe’s Folds, or develop signed distance fields based on Inigo Quilez’s research. Hundreds of procedurally generated textures have been shared on Blender Artists; groups like Blender procedural textures provide forums to ask questions and share experiments. The concept of generating material samplers has recently been combined with artificial intelligence in a recent paper, “Gaussian Material Synthesis” by Zsolnai-Fehér, Wonka and Wimmer.