Jonas Armellini has walked us through the process of creating an oil painting generator in Substance 3D Designer, as well as told us how renders were set with the help of Marmoset Toolbag.
Hello! My name is Jonas Armellini, and I come from Belgium. Currently, I'm working as a Senior Texture Artist at Ubisoft Annecy, where I'm involved in the development of Star Wars Outlaws.
My professional journey is quite traditional. I completed a Bachelor's degree in Game Art at Haute Ecole Albert Jacquard in Belgium, during which I discovered a true passion for a texture creation. The satisfaction of seeing our textures visually enhance the gaming experience is what, in my opinion, makes this profession truly rewarding and precisely is what captured my attention.
At the end of my academic path, I had the opportunity to do an internship at Ubisoft Annecy, where I worked as a Texture Artist on Tom Clancy's The Division 2. This initial immersion in the industry was extremely beneficial. I met incredible people who were passionate and dedicated to what they did, and, above all, I learned the fundamentals of what would become my daily work.
Then, this internship turned into a contract and over the years, I've been lucky enough to contribute to a variety of projects, which are all very different, the main ones probably being Tom Clancy’s The Division 2, Riders Republic, and now Star Wars Outlaws.
Some Words On The Backstory Of The Procedural Oil Painting Generator
In fact, I had already created an oil painting generator very similar to this one a little over a year ago, after a discussion with a colleague on the subject. However, ironically, when I tried to get my hands on it again, I realized that I had probably accidentally deleted it. So, I decided to create a new one.
The initial idea behind the generator was quite simple: to procedurally create a convincing "painting" effect just from a pre-existing image. For this, I drew heavily on the work of Impressionist painters, such as Claude Monet and Gustave Caillebotte.
In this regard, my approach aimed to replicate the methodology of a real painter as closely as possible by superimposing several layers of paint one on top of the other, refining their strokes as they go. Broadly speaking (and this will, of course, depend on the artistic style), they will try to follow the contours and volume of the subject to be represented and change brush sizes according to the level of detail to be reproduced.
The Workflow Behind Creating The Generator
The Brush Strokes Generator
First of all, I began by creating a brushstroke generator. This step is simple and quick. Given their size in the final texture, it is not necessary to over-detail them.
The initial step was to create an interesting shape. To achieve this, I used Circular Splatter with primitive shapes (rectangle and ellipse). Besides achieving the organic, rugged result I was aiming for, this also allowed me to obtain very different shape variations once the generator was instantiated, just by modifying Random Seed.
Then, I used Directional Warp to give the brush a direction and movement effect.
The second step, which was also very quick, involved refining the edges with Multi-Directional Warp and giving it a slope (simulating the thickness of the paint layer) by multiplying it with Linear Gradient 1.
The third step was about adding texture and details to the brush. There is no magic formula for this step because each artist has their own techniques and methods. For my part, I successively added a mask created in Gaussian Spots 2, Anisotropic Noise modified by Directional Warp to match the shape, and Grunge Map 002.
The fourth step allowed me to add some volume around the edges. The pressure applied by the brush on a canvas expelled excess paint outward. Consequently, we ended up with a thicker outline and a thicker point of contact. To achieve this, I started from the result obtained in the first step to create two masks. The first one simply reused the Directional Warp technique from the first step but with a very high-intensity value. The second one was a combination of invert and blur, which isolated a gradient all around the inner edge of the shape.
For the fifth and final step, I began by using Swirl to curve my brush. I exposed its amount so that it could be modified by instance later on. Then, I created a Level, in which I increased the Level In Low to enhance the contrast of my brush and give it its final look.
Later, when I started splattering multiple instances of the generator to create the painting, I realized that it was essential to keep all the generators oriented in the same direction to maintain a continuous flow effect. However, the different Swirl values I had set for each of my instances were causing an unwanted rotation.
To fix this, I created FX Map in which I simply exposed Pattern Rotation. I also took the opportunity to expose Pattern Offset and Pattern Size, in case I needed them later.
Personally, I chose to use FX Map because it allowed me to use only the desired parameters and had a minimal computation cost. However, it was entirely possible to do it with Tile Sampler or something similar.
When my brushstroke generator was finished, I moved on to more serious business and started painting!
The Oil Painting Generator
The method I decided to use was quite straightforward, as it primarily relied on the use of Shape Splatter. This node is very similar to Tile Sampler, the main difference is that it's non-destructive. What I mean is that you can think of it as a PSD file with all its layers uncompressed, whereas Tile Sampler is more like a JPG file, where everything has been flattened.
The Shape Splatter node must be combined with Shape Splatter Blend to reveal all its power. This technique is also quick to set up because once the first layer of paint is created, it will be very easy to iterate and add the various other layers of brushstrokes.
But before starting to generate Shape Splatters, I needed a few things first. As I wanted to get as close as possible to the work of a painter, I needed a Vector Map based on an Input Image to control the orientation of brushstrokes and several masks representing the different levels of detail.
So, I started by creating a Normal Map to use as a Vector Map. To do this, I converted the Input Map to grayscale and then blurred it before converting it to Normal Map. The blur helped control the sharpness of the edges and the overall flow of the brushes. It also affected how faithful the paint would be to the input. Since it was a useful parameter, I decided to expose it for easy access later.
For the mask, I started from the blurred version and applied Quantize Grayscale. This way, where there were a lot of details, represented by color changes, Quantize created a higher density of grayscale steps. From there, I created several Edge Detects with different Edge Width values, starting with thick edges and gradually refining them into something thinner. Each Shape Splatter, except the first one, would need a different one.
When the masks and Vector Map were created, it was time to create the first Shape Splatter. I connected Vector Map and Height Map of four instances of the brushstroke generator, making sure they all aligned in the same direction. Then, I only needed to adjust a handful of parameters, including X and Y Amounts, Scale, Vector Map Multiplier under the Rotation tab (which reoriented the brushstroke relative to the inputted Vector Map), and Height Offset Random to give a slightly random grayscale tint to each scattered instance.
From there, I created several Shape Scatter Blends (Color and grayscale) that I connected to my Shape Splatter using Input Splatter Data 1 and 2. These nodes generated a Normal Map, ID Map, and second Height Map. Note that for ID map, you can simply connect Uniform Color (any color), and in the Shape Splatter Blend settings, adjust the Random Hue values to give each instance a different hue.
Regarding my base color, the process was slightly different since it involved creating Flood Fill to Color and connecting it to both the Input Image and Splatter Data 2.
The initial layer of painting was successfully finished! To facilitate rapid iterations, I opted to duplicate this section of the graph multiple times, ensuring each duplication was connected to the preceding one, forming a chain. This allowed me to create five distinct levels. The only adjustments made involved modifying the X and Y Amounts and incorporating the previously created masks.
From here, 90% of the work was done, and after that, the focus was mainly on refining my base color and Normal Map and creating Roughness.
For my Normal Map, I decided to reduce the intensity of the smallest stroke instances, which I felt were too noticeable compared to the rest. I also added some volume using my second Height Map generated by Shape Splatter Blend and other types of details like scratches.
The work on the base color takes a bit more time. It primarily focuses on improving the blending between the different strokes, softening color transitions (which were initially very sharp), and adding texture to avoid flat colors. To achieve this, I used several techniques based on Slope Blur, Non-Uniform Directional Warp, and Vector Warp.
To smooth out color transitions, I started by using Slope Blur with White Noise. With very low intensity, I obtained transitions that were a little less regular and less precise. Then, I used Non-Uniform Directional Warp with a very fine noise, such as BnW Spots 3.
I added the result to my base color. Next, I used Vector Warps, reusing Vector Map. This allowed me to slightly warp the color following the position of each stroke in order to simulate a mix of colors. I created two Vector Warps with different intensity levels and blended them using a mask created from a contrasted curvature. I finalized the base color by sharpening it with Highpass and Sharpen.
As for Roughness, it's extremely simple, and to be honest, I didn't spend much time on it. I mainly reused Maps created earlier, such as the base color and ID Map, which I converted to grayscale, and a mask derived from the Curvature Map created just above. Additionally, I made the cavities and hollows rougher by using the Ambient Occlusion and Curvature Smooth nodes.
That was it! All Texture Maps were finished, but there was one last step before exporting everything: exposing parameters. Depending on the input image and the desired style, I wanted to make it as accessible and easy as possible to modify certain parameters. These include the Quantize level (which controls the level of definition), the intensity of the blur applied to the input image (which controls the generator's fidelity to the input), and last but not least, the brush size.
For the first two, it's a simple matter of exposing them. However, when it came to brush size, it was inconceivable for me to expose their sizes independently. It would have been extremely painful to use. So, to avoid this, I decided to create a parameter shared by all brushes, which would serve as a multiplier for the X and Y amounts already defined in my Shape Splatters (note that this doesn't actually control their size but rather their numbers, which logically reduces their size when increased).
To accomplish this, I initiated by generating a new exposed parameter of the Integer type. Following that, I established empty functions for both X and Y Amounts within Shape Splatters. Within the Empty Function, I multiplied the predefined amount (integer) by the value of the exposed multiplier (Get Integer). By doing so, each level maintains a consistent scale ratio in relation to the others. This approach enables me to update all Shape Splatters simply by modifying a single value, streamlining the process.
That concludes my oil painting generator!
Setting Up The Final Renders
For renders, I wanted to create a scene representing an art gallery. I aimed for something very neutral, which would allow the paintings to stand out and be highlighted as much as possible.
I chose Marmoset Toolbag 4 because it was a very easy and accessible tool and offered incredible rendering quality very quickly. As for the format, I used a 2:1 ratio, which was longer than the traditional 16:9 and offered a more cinematographic look.
I started with building my scene, importing assets, and creating my materials. Later, I decided to add some plants downloaded from Megascans. This allowed me to introduce some colors and organic/curved shapes to break the horizontal and vertical lines from the canvas and, most importantly, it gave me the opportunity to frame my image by clamping the view and refocusing on the painting.
As for lighting, I primarily relied on spotlights. To achieve this, I created multiple spotlights to emulate the various light sources typically found in art galleries. Initially, I used a wide-range spotlight to provide the main source of light and shadows. Additionally, I positioned a second spotlight above the painting to accentuate it from the top and provide a framing effect. Finally, I incorporated a few more spotlights focused specifically on the painting to create additional areas of reflection.
Speaking of the camera, I significantly reduced its FOV to flatten the image and avoid distortion. I also changed the tone mapping to Hejl and added some post-processing effects, applying them very lightly, including bloom, chromatic aberration, vignette, and sharpen. Then, I made some additional adjustments in Photoshop, enhancing levels, contrast, and brightness of the renders, adding more vignetting, and sharpening the image further.
Thoughts On Working with Substance 3D Designer And Some Final Words
Substance 3D Designer can initially seem a bit intimidating due to its node-based structure and the unfamiliarity with various nodes, functions, and combinations. However, I wouldn't say it's difficult. Personally, I started using it during my internship at Ubisoft. Before that, I had only opened it to create very simple patterns or to damage edges (RIP all the wooden planks and brick textures crushed by Slope Blur and Warp).
For learning or improving in Substance 3D Designer, the best approach, even if it sounds basic, is to practice, practice, practice! Moreover, the internet is full of educational content, from tutorials to breakdowns and even downloadable SBS files, making it accessible and easy for anyone to learn quickly and at their own pace.
If I were to provide some guidance, I would suggest taking your time and progressing step by step, always setting realistic and attainable goals. If you are a beginner, it is crucial not to rush and avoid being discouraged if the outcome falls short of your initial expectations. Quality usually develops over time and practice, so keep honing your skills. Sometimes, achieving the desired result is simply a matter of fine-tuning a few parameters.
As for the time it takes to create a texture in Substance Designer, it varies greatly. It primarily depends on the complexity of the surface you're representing, your personal experience in replicating such surfaces, the level of fidelity and details you want to achieve, and your set objectives. In fact, a texture is never truly finished. There are always elements to improve and new techniques to discover so you could spend an incredible number of hours on each one! The key will be to stop before sinking into a process of infinite iteration.
Also, I would like to thank the entire 80 Level team for giving me the opportunity to share this little glimpse of my work with you.
You may find these articles interesting