Home Adobe Substance


ngon master
Offline / Send Message
Daniel_Swing ngon master

Download this node for free on my gumroad!


I made a tile generator node in Substance Designer that can create tiles of different sizes in both X and Y axises at the same time, without intersecting shapes and can semi-reliably avoid straight seams across the canvas.

The user can customize the number of tiles generated in both axises and change the random seed to generate different patterns.

The node consists of a series of Pixel Processors and functions that render the pattern and will end with different results depending on the graph’s random seed.


Irregular tile patterns like versailles patterns and other similar are not simple to create procedurally in Substance Designer.

There are no native nodes or functions in Substance Designer 2020.1.3 that can create irregular tile sizes in both X and Y axises at the same time while also avoiding intersecting shapes or straight aligning seams across the texture canvas.

Another reason for this project was so I could play with the pixel processor node a bit and learn a thing or two about it.


The goal with this project was to create a way for a user to generate multiple different tile patterns with different sized tiles/shapes in both X and Y axis, with no intersecting shapes, without any straight seams across the canvas, with as little manual user input as possible as fast as possible.

  • Multiple different patterns with sized tiles in both X and Y axises.

  • No intersecting shapes, only solid squares and rectangles.

  • No straight seams across the texture’s canvas or along the edges.

  • User controlled automation, no mandatory manual input.

  • Fast computation.


The node’s structure is explained here - not much details on exact node-to-node interaction, but instead a description of each sequence of what it does and how it helps to achieve the end goal.

The DS_Tile_Random node uses several pixel processors. Because the pixel processor can’t refer to itself or recur onto itself, several different pixel processors had to be sequenced together to incrementally make progress to the final pattern.

The float numbers 1, -1 and 0 were used frequently, so they were made global constants in the main graph, instead of instances in every individual pixel processor.

Random Colors:

Everything starts with random gray-scale colors per pixel.

For every pixel, create a random value between 0 and 1 and add half the sum of the X&Y position of the pixel, then divided by half - this will render a canvas of values randomly spread between 0 and 1 with higher values trending towards the top left and brighter values trending towards bottom right.

The gray-scale color is not completely left to randomness because complete randomness rendered suboptimal results. By letting the position of the pixel also decide the final value it minimizes the number of random cases of the exact same value being generated at several pixels in proximity of each other, which creates problems in later pixel processors.


For every pixel, sample the color of the pixel on position 1/x,1/y that the active pixel is “closest” to, rounded down.

By only sampling the gray-scale color from one pixel over an area of pixels, the pixel processor creates a square grid.

The texture size is not always evenly divided by the number of squares (example; 2048x2048 pixels / 9x9 grid) and so not all squares are exactly the same size - this is important to remember for later stages as this will create impressions.

The number of sampled pixels / squares in the grid is decided by user input in both X and Y.

Initially this node operated on single pixels in small resolution textures, where one pixel was one square. But that restricted the number of tiles too much and this function was created instead.

Expanding Tiles:

For every pixel, compare the active pixel’s value to the adjacent square’s corresponding pixel’s value in the squares above, to the right and diagonally top right and change the active pixel’s values to the highest value among the compared values.

This means that brighter squares spread their color values to the left, down and diagonally bottom left, while darker squares take on new values from the adjacent squares.

This will start to create larger square and rectangle shapes, as well as some undesired L shapes and potentially some other irregular shapes that need to be removed.

Fill L-Shapes:

By identifying non-conflicting L-shapes in the pattern, they can be patched together into a full square.

For every pixel, compare the surrounding squares’ corresponding pixels. If three connected squares share the same color but not with the active pixel, they form an L-shape. If there is a L-shape adjacent to the active pixel’s square, the active pixel takes the color of the L-shape as long as that wouldn’t create a new L-shape.

Cut L-Shapes:

By identifying the remaining L-shapes they can be cut by the tail end. Some L-shapes are in conflict with each other or with an already complete rectangular shape and would not be filled in by the previous method.

These L-shapes are identified by comparing the active pixel’s value to the corresponding pixel of three adjacent. connected squares. If the active pixel shares color value with two connected squares but not all three, it is considered an L-shape.

If the active pixel is on one end of the L-shape, it takes the gray-scale color of the second input pattern.

Fix Irregular:

To remove any remaining irregular and undesired shapes, a brute-force function that cuts up any non-square or rectangle shapes.

Similar to the ‘Fill L-Shape’ and ‘Cut L-Shape’ functions, the ‘Fix Irregular’ function identifies pixels in shapes that are not complete squares or rectangles.

The majority of undesired shapes are removed completely by iterating this function three times.

Pair Singles:

A lot of single squares are present in the pattern after fixing all the undesired shapes, this is also an undesired result.

By identifying pixels in single squares and letting them take the color of another adjacent square, as long as that would not create any new L-shapes, the number of single squares are reduced.

This is done twice, once to prioritize vertical pairing and once to prioritize horizontal.

Break Alignment:

At this point the pattern often has seams running straight through the texture’s canvas.

The ‘Break Alignment’ function identifies straight seams by comparing a number of corresponding pixels in squares adjacent to the active pixel’s square. If they don’t share any colors with the neighboring squares across either the X or Y axis, it is treated as a straight seam.

The number of comparisons to find a straight seam depends on the inputted tiling values. A higher tiling value sets a higher number of comparisons in the corresponding axis, because too low of a threshold risks just displacing the straight seam instead of actually breaking it.

If the function finds a straight seam, the active pixel takes the value of one adjacent square’s corresponding pixel that would break the seam. This can create L-shapes and other undesirable shapes.

The ‘Break Alignment’ function is very heavy computation-wise and makes up for about half the computation time, which is why the user is allowed to turn it off if straight seams don’t bother the user.


Some undesirable shapes that are created by the ‘Break Alignment’ function are easily identified and reverted to their old state.

These shapes are identified in a similar fashion to previous fixing functions.

Last Fix:

A series of ‘Fill L-Shapes’ and ‘Cut L-Shapes’ removes most remaining undesirable shapes.

Clean Up:

As mentioned previously, when texture size is not evenly divided by the tiling number in either or both axises (ex; 2048x2048 / 9x9) the squares will not all be exact equal size, meaning that there are some impressions in all the functions that will creates 1 or 2 pixels thick lines on the edges of some squares, possibly intersecting a large square or rectangle - these need to be removed.

For every pixel, compare all adjacent pixels and the pixels a few steps away. If the compared pixels share color, the active pixel assumes that it is in the middle of a square of that color and takes on that compared color.

This is done separately for the X and Y axises.



Re-Calibrate Grid:

Lastly, by running the same function that sampled the initial color values and created the square grid, the generator fixes any last pixel off-sets that may have happened in the removal of the pixel-thin lines, creating a grid-perfect pattern.

This option can be toggled off if the user wishes to.


The generated patterns are reminiscent of a versailles pattern, but where versailles pattern follows a set of rules my tile generator renders a much more random result.

  • Multiple different patterns with sized tiles in both X and Y axises.

The node automatically generates a pattern with different sized tiles in both X and Y axises at the same time. Changing the random seed changes the pattern entirely.

  • No intersecting shapes, only solid squares and rectangles.

L-shapes or other undesired shapes do occur in the final pattern but very seldom and only on specific seeds with specific tiling values.

  • User controlled automation, no mandatory manual input.

The user can customize the pattern by inputting the X and Y tiling number. The user can change the random seed of the node, which will completely change the generated pattern.

  • No straight seams across the texture’s canvas.

The node has trouble avoiding straight seams, even with the ‘Break Alignment’ function, especially on lower tiling values. At 4x4 not one of the 350 first random seeds rendered a valid result.

Below are all the (valid) results between random seeds 0 and 60 that do not contain a straight seam across the texture’s canvas, with tiling values 5x5, 6x6, 7x7, 8x8, 9x9 and 10x10. The leftmost results are with no functions to counteract straight seams, the middle results are with using [redacted] function and the rightmost one is with the ‘Break Alignment’ function.

The ‘Break Alignment’ function proved to increase the number of valid random seeds, compared to no function. The [redacted] function proved less reliable than no function, so it was removed from the graph.

  • Fast computation.

The node has relatively fast computation time, sub 100ms computation time at 2k resolution. The user can disable the ‘Break Alignment’ function to decrease computation time.


To summaries the results, the node achieves what it set out to do, with a margin of error and a lot of room for improvement.

The strongest aspect of the node is that it can very easily generate a great variety of patterns by just changing the random seed - this is possible since the method uses deterministic functions based on an initial random input.

The fact that the node has trouble avoiding straight seams through the canvas at lower tiling values is the biggest flaw in the method - this could probably be fixed with alternate sequencing or functions specific for low tiling values to create a more robust method.

One of the alternate functions that was used earlier was to create a herring-bone like pattern before the Expanding Tiles function, to remove any straight seams at an early stage. But this function proved counterproductive and rendered less reliable results and thus was removed.

Another flaw with this method is that the user has very limited control over the end result.

The final pattern is very reliant on the initial random seed and will not always render a desired result, sometimes completely invalid or just aesthetically displeasing. There is not much a user can do to remedy this except changing the random seed or the tiling values.

I wish that the methods could offer more control over the generated pattern to the user, such as tile placements, pattern directions, min and max shape dimensions, shape size distribution, expanding sub-square size randomness, slanted edges and utilizing texture inputs.

Further Customization

While the DS_Tile_Random node doesn’t offer much customizability in itself, some  of it can be added by using the flood-fill node. For example, inserting random sub-square sizes, slanted tile edges or changing the generated shapes to something other than a square.

To avoid having to construct this constellation in every graph every time one wishes to add further customization to the tile pattern, I made a companion node, the DS_Tile_Random_Customizer.

It is a separate node, and not a part of the DS_Tile_Random base node, to keep the base node as fast and clean as possible. If they were the same, every DS_Tile_Random node would come with a built-in flood-fill node (which takes more time to compute than the entire DS_Tile_Random node by itself), even if the user doesn’t want to utilize the flood-fill functions.

Separating the nodes also means that the DS_Tile_Random_Customizer can be used in other contexts and not only with the DS_Tile_Random.

Hot tip: If you want to decrease the computation time, try lowering the resolution on the DS_Tile_Random_Customizer, as it won’t impact the visual results in a major way.

This node accepts a Pattern input, like the one generated by the DS_Tile_Random node, as well as an optional Scale Map, Slant Map and Grayscale input. The DS_Tile_Random_Customizer has nine parameters, many mimicking the parameters that can be found in native nodes:

Size Parameters:

Size - Allows the user to control the size of each tile on both X and Y individually.

Size Random - Allows the user to add random size differences between each tile in both X and Y individually.

Scale - Allows the user to control the uniform scale of each tile.

Scale Random - Allows the user to add random scale differences between each tile.

Scale Map Multiplier - Allows the user to customize the influence of the Scale Map Input has over each tile’s scale.

Slant Parameters:

Slant Angle - Allows the user to customize the global angle of all tiles / the global angle of the inset created by lowering the size parameters.

Slant Variation - Allows the user to customize the variation of slant angle between each tile.

Slant Input Multiplier - Allows the user to customize the influence the Slant Map Input has over each tile’s slant angle.

Color Parameters:

Grayscale Input - allows the user to customize the influence of the Grayscale Input has over the grayscale color in each tile.

Input Pattern Grayscale - allows the user to cutomize the influence of the original Pattern Input has over the grayscale color in each tile.

This has been a fun project and I wouldn't be surprised if I return to it every so often to add some more functionality to either nodes.


I would like to give special thanks to Bruno Afonseca, Jonas Olesen and Vincent Gault for sharing a lot of technical nodes and workflows on Substance Share, for free - this helped me a lot in the beginning of this project. Check out their stuff if you want to learn!

Download this node for free on my gumroad!

Thank you for reading!


Sign In or Register to comment.