logo80lv
Articlesclick_arrow
Research
Talentsclick_arrow
Events
Workshops
Aboutclick_arrow
profile_loginLogIn

Breakdown: Substance 3D Designer System To Create Organic Shapes & Fractal Patterns

Marco Vitale, a two-time winner of the Substance 3D Insanity Awards, joined us to discuss how he applied Lindenmeyer's principles in Substance 3D Designer to procedurally create organic shapes and intricate fractal patterns.

Introduction

My name is Marco Vitale, and I'm a Lighting Designer for theater with over 15 years of experience in the field of lighting. Since the theater is quite an old art form, sometimes the methods are too, but it's in my nature to go unconventional ways. In 2014 I started a 3D lighting visualization software called Capture which was mainly used in the show-design Industry for rock and roll shows and festivals or the big musical productions, but didn't have too much of an impact in theater, and I wanted to change that, so I started to use Capture to prepare all my shows and designs. 

To get the best out of the software and to convince the teams and coworkers, I had to aim for the most realistic look on the PC, which meant doing everything from modeling over texturing to lighting. I was fascinated by the huge impact the materials can have on the realism of a 3D render, which is why I started to focus on material creation, first with simple photo textures and hand-drawn normal maps, but this workflow wasn't fast enough to use the time efficiently, so I was looking for a better solution and read about the capabilities of Substance 3D Designer and the procedural workflow. In 2017, I bought a license, took my first steps, and very quickly got addicted to that amazing piece of software.

After a few materials, some of them were quite good and realistic, but some of them looked really procedural; I searched for techniques to enhance realism and get rid of the procedural look. My focus shifted another time, and I really got into surface imperfections, like scratches, cracks, dust, fingerprints, and so on. I think it was 2020 when my friend Andrei Zelenco started a group on Facebook, Inside the Node, which really hit me. I was a big fan of Andrei's work, and it was such a cool resource to get deeper into the Pixel Processor and FX-Map-Nodes.

Soon, I was totally lost and dragged into the technical aspects of 3D Design. I was looking into the Pixel Processor and FX-Map-Nodes, which helped me achieve the goal of non-procedural-looking materials using a procedural workflow. Today these are almost the only nodes I use; even for a simple blend, I use a Pixel Processor.

The MV L-System Project

I was searching for ways to create organic root-like structures and patterns and found the paper The Algorithmic Beauty of Plants. This was the first time I read about the Lindenmayer systems. I used some of the techniques like recursive replacement of data in my tool MV Fractal Tree Node, which got a lot of great feedback because it allows the creation of organic recursive branching in the designer, which is great for all kinds of natural materials. The first prototype of the L-System was basically a byproduct of this tool and was over 4 years old.

Lindenmayer systems were introduced and developed in 1968 by Aristid Lindenmayer, a Hungarian theoretical biologist and botanist. He used L-Systems to describe the growth of plants and cells. For this, there is an initial state, the premise or axiom, and one or more reproduction rules. With every evolution, these production rules are applied to the last state and recursively grow. The fascinating thing about L-Systems is that even very simple rules can produce stunning patterns and fractals after a few iterations.

In 1988 the L-Systems were introduced to the world of CG at SIGGRAPH 1988 as course notes of the course Fractals: Introduction, Basics, and Applications and found a lot of applications in the organic procedural generation of plants, roots, and natural-looking branches as well as flowers or leaves and of course fractal, self-similar patterns. In order to "draw" the result of the L-System, the output of the system can be translated into turtle graphic commands as the programming language LOGO introduced them back then.

Although it's so versatile in its applications, the basics are always the same: Start with an initial state and apply reproduction rules at each generation to produce the next generation. After iterating a few times, draw the result using turtle graphic commands.

Example 001 – Simple Pattern

This example shows how a very complex pattern can be created with 8 Iterations and one simple rule.

Premise: F+F+F+F

Rules: F=FF

Rotation Angle: 90°

The MV L-System in Substance 3D Designer allows the creation of very complex L-Systems by using an extended alphabet, randomness, conditional rules, probabilities, and changeable parameters in a typical 3D Designer style. All this is done without using any parameters or sliders, just by writing commands as text directly into the text input of the node. This makes it possible to create simple L-Systems as well as very big ones with just a few lines of "code".

The Complete Alphabet Of The MV L-System

Drawing Commands

Not all parameters have to be set. Without explicit parameters set, the default Step Length and Line Width are taken.

F(l, w) Move forward a step, drawing a line connecting the previous position to the new position

f(l, w) Move forward without drawing

H(l, w) Move forward half a step, drawing a line connecting the previous position to the new position.

h(l, w) Move forward half a step without drawing.

Rotation Commands

Without explicit parameters set, the default Rotation Angle is taken.

+(a) Turn right [a] turns

-(a) Turn left [a] turns

&(a) Pitch up [a] turns

^(a) Pitch down [a] turns

\(a) Roll clockwise [a] turns

/(a) Roll counter-clockwise [a] turns

|(a) Turn 180°

*(a) Roll 180°

~(a) Pitch / Roll / Turn random up to a given number of turns [a], the default angle is 180°

Due to the Substance 3D Designer convention, all rotations are in turns. To convert angles in degrees to turns, simply use a division by 360 in the argument of the command, e.g., +(30/360).

Rotation Commands

Without explicit parameters set, the corresponding default Scale Parameter is taken.

"(s) Multiply current length by Step Size Scale

!(s) Multiply current thickness by Thickness Scale

;(s) Multiply current angle by Angle Scale

_(s) Divide current length (underscore) Step Size Scale

?(s) Divides current width by Thickness Scale

@(s) Divide current angle by Angle Scale

Input Commands

Without explicit parameters set, the current Step Length and Turtle Rotation are taken.

I (s, r) Adds the Image from Input Slot (I) at the turtle's position with size [s] and rotation [r]

J (s, r) Adds the Image from Input Slot (J) at the turtle's position with size [s] and rotation [r]

K (s, r) Adds the Image from Input Slot (K) at the turtle's position with size [s] and rotation [r]

L (s, r) Adds the Image from Input Slot (L) at the turtle's position with size [s] and rotation [r]

M (s, r) Adds the Image from Input Slot (M) at the turtle's position with size [s] and rotation [r]

N (s, r) Adds the Image from Input Slot (N) at the turtle's position with size [s] and rotation [r]

Value Variables

Custom Variables can't be used in the calculation because these are set at the drawing stage and, therefore, aren't numbers to compute during the generation of the L-System.

a This variable represents the value, set by the Draw - Nodes Custom Variable a

b This variable represents the value, set by the Draw - Nodes Custom Variable b

c This variable represents the value, set by the Draw - Nodes Custom Variable c

d This variable represents the value, set by the Draw - Nodes Custom Variable d

e This variable represents the value, set by the Draw - Nodes Custom Variable e

k This variable represents the value, set by the Draw - Nodes Custom Variable k

l This variable represents the value, set by the Draw - Nodes Custom Variable l

m This variable represents the value, set by the Draw - Nodes Custom Variable m

i, j These variables represent the arguments, which can be set or calculated for every step

r Represents the System Variable Random and generates a random number between 0 and 1 at each generation

t Represents the System Variable Current Generation and is set to the current generation of the L-System Step

Node Variables

All Node Variables can have up to 2 arguments, which can be passed from one generation to the next, these arguments can be accessed by the rule using the argument variables i and j. The expression A(0.1, 1.3) using this rule A(i,j) = F(i)F(j) results in this expression F(0.1)F(1.3).

A(i,j) This Variable can be used to rewrite a complete Node

B(i,j) This Variable can be used to rewrite a complete Node

C(i,j) This Variable can be used to rewrite a complete Node

D(i,j) This Variable can be used to rewrite a complete Node

E(i,j) This Variable can be used to rewrite a complete Node

X(i,j) This Variable can be used to rewrite a complete Node

Y(i,j) This Variable can be used to rewrite a complete Node

Z(i,j) This Variable can be used to rewrite a complete Node

Behavioural Commands

% Cut off the remainder of the branch

[ Push turtle state (start a branch)

] Pop turtle state (end a branch)

:{} Condition, e.g., A:{t<=10) = F (this Rule only gets applied when the current generation is lower or equal to 10). The condition has to be placed after the Character to replace and before the Equality Sign, it can handle simple conditionals Equal(==), Greater(>), Greater or Equal(>=), Lower(<), Lower or Equal(<=), Not Equal (!=).

:0.33 Probability, e.g. A = -F:0.33 (this Rule is applied randomly in about 33% of the generations). The probability has to be at the last position of the Rules, otherwise, it just gets ignored

= Replace left with right

Simple Math Operators

For now, this is limited to one operation (I will expand these in future versions). It is possible to perform simple math operations like Add (+), Subtract(-), Multiply(*), Division(/), and Pow(^) inside arguments of the Premise or Rules. This makes it possible to calculate values depending on conditions, arguments, or the current generation, e.g., A(r*0.1, t-1).

Example 002 – Fuzzy Weed

This example shows how a second rule and a probability value produce variations in the same style but with different branching patterns.

Regular Ruleset

Premise: X

Rules: F=FF

X=!F-[[X]+X]+F[+FX]-X

Rotation Angle: 22.5°

Stochastic Ruleset with a 35% probability

Premise: X

Rules: F = FF

X=!F-[[X]+X]+F[+FX]-X:0.35

X=!F+[[X]-X]-F[-FX]+X

Rotation Angle: 22.5°

Example 003 – Random Ranks

This example shows how random rotation and step length produce absolutely different but similar results.

Premise: +(r)F(r)

Rules: F=F[-(r*0.2)FI]+(r*0.1)F:0.5

F=F-F

Rotation Angle: 22.5°

Workflow

The Top Level of the MV L-System always consists of a chain of the following nodes: Input, Iterate, and Draw. To keep it procedural and versatile, these nodes are separated and can be reused in various combinations.

Let's take a look at the Input Node. This Node has two main components. The first component is the String to Array Node, which converts the input text to an array of readable values. The second component is an FX-Map, which acts as a lexer for the input text.

To keep it as optimized as possible, the Premise and the Rules have two different Input Nodes to produce an array of values from the input text. The function of these nodes is the same, but the output array size is different. The Premise Input outputs one line with a maximum of 128 characters, whereas the Rules Input outputs an array of 256 lines with 256 characters. The exact size of these arrays is important for the lexer to know the limits of the input.

A lexer (lexical analyzer) is a component of a compiler or interpreter that processes input text and converts it into tokens. These tokens are meaningful units such as keywords, numbers, identifiers, or operators the parser or interpreter can understand.

In this case, the FX-Map-Node creates an array of pixels with three components stored in the RGB channels. The red channel contains the token code (an integer number) encoding the valid command, and the green and blue channels contain arguments for the command. After that stage, the output is an encoded version of the input text stored in an RGB texture.

For example, the input text F(0.1, 0.2) gets converted into a single pixel with the value RGB(70,0.1,0.2), which means that the input shrinks from eleven characters to one.

The lexer for the Premise Input:

The lexer for the Rules Input:

The next node in that chain is the Iterate Node. This node is the core of the L-System and performs the replacement of tokens by applying the reproduction rules recursively. To achieve a recursive system, a utility node Utility Iteration Step gets chained together and applies the same rules to the output of the previous step up to 256 times. Due to the recursive nature of the L-System it grows very fast, so the 256 iterations are usually enough, in the rare case that more iterations are needed, a second Iterate node can be connected to the first one, adding 256 iterations to the system.

Example

The recursive replacement of the premise after 4 iterations.

Premise: X

Rules: F=FF

X=F[+X][-X]FX

1st Iteration F[+X][-X]FX

2nd Iteration FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX

3rd Iteration
FFFF[+FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX][-FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX]FFFFFF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX

4th Iteration FFFFFFFF[+FFFF[+FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX][-FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX]FFFFFF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX][-FFFF[+FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX][-FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX]FFFFFF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX]FFFFFFFFFFFF[+FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX][-FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX]FFFFFF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX

The Utility Iteration Step is just another FX-Map, which loops through the whole array and replaces the tokens from the previous step by applying the rules of the system. Theoretically, it would be possible to use multiple Iterate Nodes and input different rules to a number of steps, but this can also be put into one ruleset with a condition e.g.:{t<=3} this rule gets only applied for the first three iterations, after the third iteration, this rule gets ignored, and an alternative rule can be applied.

The FX-Map iterates over the input array (internally a Data Table). At every position in the input, it iterates over the ruleset and searches for a rule to apply. If a rule fits, it replaces the current input data with the rule. This gets repeated until the last valid input.

The last node in the chain is the Draw Node. It interprets the input array from the Iterate Node using the turtle graphics commands. To create a useful node for Substance 3D Designer, it decided to add more functionality to the draw node, such as 3D transformations and input textures, which are called "leaves" in the Node. To optimize performance, the branches and the leaves get drawn in different nodes.

The leaves get drawn in the Pixel Processor because this node is very performant when it comes to drawing whole input images at once. However, it has its drawbacks when drawing thin lines and doing a lot of iterations. The whole texture gets calculated multiple times, but only a small portion of the image is needed. That's why the branches are drawn inside the FX-Map Node.

To keep the performance as good as possible, I decided to do the interpretation of the commands in another FX-Map node, this first FX-Map splits the Array into branch- and leaves commands, which can be read by the two draw nodes.

To draw a leaf, 4 pixels are needed; these 4 pixels store the X, Y, and Z coordinates of all 4 corners of the input texture. To draw a branch, theoretically, only 2 pixels are needed, the X, Y, and Z coordinates of the start- and end of the line as well as the line thickness at the start and end, but since the alpha channel in an FX-Map scales down the other 3 channels as well, I decided to use a third pixel to store information for the line-thickness.

In the last 4 years, I opened the L-System-Prototype uncountable times and worked on it, but I hit a lot of walls, limitations, or new features in Substance 3D Designer, which led to other tools I needed first to get on with the L-System. I called it finished several times, but then I had another idea to improve it, or found ways to optimize the system, or simply had to rebuild one of the utilities because of a Designer update and new features, which could do things better or faster. Some of the utilities I had to build made it into stand-alone tools, which I published and had to invest some time into, so in total, it took a little over 4 years from the first prototype to the latest working version. But in the end, I could finally implement this beautiful, clever algorithm in 3В Designer, and I hope that a lot of users create fantastic materials with this tool.

I think the biggest challenge was to find the time to work on it since I work full-time in the theater and try to find a balance between work, family, and this hobby called Substance 3D Designer. I sometimes haven't had the time to work on a specific problem for weeks. But I think I found a very healthy way for me to combine these three conditions. I try to work in Substance 3D Designer every day for at least an hour but not more than three hours before I leave for work so I have time for my family after work.

The other big challenge was finding a way to work with text in Designer. The L-System relies on the input and replacement of strings, which is why it is so easy to implement in most programming languages. String manipulation is a fundamental functionality. Writing a recursive function to replace strings is just a few lines of code.

Here is a little example of how to create the basic L-System functionality in Python in just 5 lines of code:

But Substance 3D Designer is not a programming language; it's a node-based material authoring software, so working with text is quite challenging. In the first attempt, I used numbers, which are way better to handle in Designer. But the Data Types are limited to Integer4 or Float4, so every replacement could have a maximum of 4 values, which is okay but far from great.

So, I had to find a way to handle text in Substance 3D Designer, and I created a utility node TextToASCIICode to transform every character in a string input into the corresponding 8-bit ASCII Code (e.g., A: 65). The core idea with this utility was to use a Text Node with the String Input and an 8-bit encoded font for every character in the 8-bit ASCII table.

Example – "80LVL" 8-Bit Encoded

This example shows how the input text 80lv gets encoded into 4 textures 16x16 pixels. These textures can then easily get compressed into a single pixel with the grayscale value of the corresponding ASCII code.

The whole text 80lv in four single pixels:

After this conversion worked, I developed a Data System with Arrays, Data Tables, and some utility Nodes to translate a table of ASCII values back into readable characters for debugging purposes and so on. Now that the Text Input is a table of values in 3D Designer, I could use the Pixel Processor and the FX map Nodes to manipulate the data.

To draw the final output I use a FX-Map Node, which reads the output from the last iteration and translates every value in the data-table into a simple turtle command basically using the technique from 1988.

Example – 80lv

This example shows how to draw the "80lv" logo with turtle commands. The Node Variables A, B, C, and D represent each letter and get replaced in the first iteration.

Premise: ABCD

Rules:

A = ----[F++F++F++F++F++F++F++F][F--F--F--F--F--F--F--F]

B = --------fh----HF++F++F++F++FHHF++F++F++F++FH

C = ++++fffh[----FFH][++++FF--H--H]

D = [fh+++FFH------FFH]

Rotation Angle: 22.5°

Conclusion

Always try to get out of your comfort zone, enhance your technique, and think out of the box. Use the software you have in unconventional ways and never accept sentences like "That doesn't work," "This is not how it should be," or "This is wrong." Usually there is no right or wrong in Substance 3D Designer, there are of course some best practices and workflows which work better than others. But I’m pretty sure that no one at Adobe ever thought of using the Text-Node as an Input for an L-System rather than using it to add Text to a material.

Here is a little blog post where I explain how you can use the Text-Node in another unconventional way, creating a large variety of patterns with it.

Officially being awarded as Insane is a big honor for me personally, and I'm glad that Adobe appreciates the hard work, even if the Convolutional Neural Network from last year was not useful at all. This time, I made a utility that is actually useful for material artists using Substance 3D Designer, and it gained a lot of feedback, especially the technical aspect.

Many users asked if I could do some tutorials on technical art in Substance 3D Designer using the Pixel Processor or FX-Map nodes, which is great. As soon as I have some time, I'll dive into a tutorial series on these two nodes. That is the plan for this year.

Maybe one of the examples I will do in these tutorials has the potential to get nominated for next year's Insanity Award but I don’t have anything specific in mind. I have so many unfinished Ideas and experiments sitting on my hard drive, which may or may not be finished this year, but I have nothing specific planned. I am always open to suggestions.

Marco Vitale, Lighting Designer

Interview conducted by Gloria Levine

Join discussion

Comments 0

    You might also like

    We need your consent

    We use cookies on this website to make your browsing experience better. By using the site you agree to our use of cookies.Learn more