Categories
FMP

FMP | Visual Style Test 04: Return of the Obra Dinn 3-Post Process Material Development

1. Technical Challenges

After completing the five-level Toon Shader, I began developing the Post Process Material to convert the scene’s grayscale information into black-and-white dithering patterns.

The goal of this stage was to recreate a visual effect similar to Return of the Obra Dinn, where different brightness levels are represented through the density of black-and-white pixels rather than conventional grayscale values.

Compared with my previous Halftone experiments, this approach was more complex because different tonal regions required different pixel distribution rules while maintaining clear lighting and shadow structures.

I divided the image into five tonal regions:

Tonal Region Visual Representation

Black– Black background with sparse white dots

Dark Grey– Solid black

Mid Grey –Mixed black-and-white dithering

Light Grey –White background with sparse black dots

White –Solid white

To achieve this, I needed to address three main challenges.

First, I needed to accurately extract the scene’s brightness information and divide the continuous grayscale values into stable tonal regions.

Second, I wanted to generate procedural dot patterns and random pixel distributions using mathematical nodes rather than external textures.Finally, I needed to control the pixel density independently for each tonal region instead of applying the same noise pattern across the entire image.

2. Technical Development

I created a Post Process Material in UE5 and used Emissive Color to output the final black-and-white image.

The development process was divided into four main stages.

Step 1: Grayscale Conversion

First, I used SceneTexture: PostProcessInput0 to retrieve the rendered scene colour.

To convert the RGB values into grayscale, I used a Dot Product node to combine the scene colour with a fixed luminance weight vector.

Node connection:

SceneTexture → Dot Product → Grayscale Value

The second input of the Dot Product was:

(0.299, 0.587, 0.114)

These values represent the luminance weights of the red, green, and blue channels.

The grayscale calculation can be expressed as:

[

Gray = 0.299R + 0.587G + 0.114B

]

This converts the original colour information into a single brightness value.

At this stage, the material only extracts brightness information without changing the black-and-white pixel distribution.

Step 2: Tonal Separation

After obtaining the grayscale value, I used several Scalar Parameters to define the boundaries between different tonal regions.

The main parameters included:

* M2: Controls the darker tonal regions.

* M3: Controls the middle tonal regions.

* M4: Controls the brighter tonal regions.

These parameters determine which dithering pattern is applied to each brightness range.

I used If, Power, Subtract, Divide, and Saturate nodes to process the grayscale information.

1. Controlling the Grayscale Range

First, I used Divide and Saturate to normalise the brightness values.

Node connection:

Gray → Divide → Saturate

This maps the brightness values into a controlled range between 0 and 1, making it easier to adjust the dithering density.

2. Creating Tonal Masks

Next, I used multiple If nodes to compare the grayscale values against different thresholds.

For example:

Gray + M2 → If → Mask_M2

Gray + M3 → If → Mask_M3

Gray + M4 → If → Mask_M4

These masks determine which tonal region each pixel belongs to and which dithering pattern should be applied.

3. Adjusting Brightness Distribution

I also used Power nodes to adjust the brightness response curves.

By changing the exponent, I could make the transitions in pixel density more concentrated or gradual.

This allowed me to control the distribution of different tonal regions without directly modifying the lighting in the scene.

Step 3: Procedural Dithering

The next stage was generating black-and-white dot patterns using mathematical nodes.

Unlike applying a noise texture directly, I wanted the patterns to be generated procedurally so that I could control their size, spacing, and randomness.

This process involved three main components: a screen-space grid, circular dots, and pseudo-random values.

1. Creating a Screen-Space Grid

First, I used ScreenPosition to obtain the screen UV coordinates and multiplied them by a GridDensity parameter.

Node connection:

ScreenPosition → Multiply (GridDensity)

GridDensity controls the number of grid cells across the screen.

I then connected the result to both Frac and Floor nodes.

* Frac: Extracts the local coordinates within each grid cell.

* Floor: Generates integer coordinates identifying individual grid cells.

This creates a regular two-dimensional grid across the screen.

2. Creating Circular Dots

To generate circular dots within each grid cell, I used Frac, Subtract, and Length nodes.

Node connection:

Frac → Subtract (0.5, 0.5) → Length

The Frac node produces local coordinates between 0 and 1.

Subtracting (0.5, 0.5) shifts the centre of each grid cell to the origin.

The Length node then calculates the distance between the current pixel and the centre of the cell.

I used the DotSize parameter to control the radius of each dot and an If node to determine whether a pixel falls inside the circular area.

Node connection:

Distance + DotSize → If → Dot Mask

If the distance is smaller than the specified radius, the pixel is included in the dot. Otherwise, it belongs to the background.

This produces a regular pattern of circular dots across the screen.

3. Generating Pseudo-Random Values

To prevent every grid cell from producing an identical pattern, I created a pseudo-random number generator.

First, I used Floor to obtain the integer coordinates of each grid cell.

I then used Dot Product to combine these coordinates with a fixed vector:

Cell ID → Dot Product (12.9898, 78.233)

Next, I passed the result through Sine and multiplied it by a large constant:

Dot → Sine → Multiply (43758.546875) → Frac

This generates a pseudo-random value between 0 and 1 for each grid cell.

Because the random values are calculated from the grid coordinates, the pattern remains stable when the screen coordinates remain unchanged.

I used these values to control whether individual dots appeared, creating a more irregular black-and-white pixel distribution.

Step 4: Pixel Density Control

After generating the dot patterns, the next step was to assign different pixel densities to different tonal regions.

I developed three main dithering patterns.

Pattern T1: White Dots on Black

This pattern was designed for darker regions.

I used the grayscale value to control the probability of white dots appearing, while BlackDotMaxDensity controlled the maximum density of the pattern.

The main nodes included:

Gray → Power → OneMinus → Multiply → If

The Power node adjusts the brightness response curve, OneMinus reverses the value, and Multiply controls the resulting density.

The calculated density is then compared with the procedural random value to determine which grid cells display white dots.

This allows darker regions to remain predominantly black while containing a small number of white pixels.

Pattern T2: Mixed Dithering

The middle tonal region required a more balanced distribution of black and white.

I used an If node to compare the random values against a brightness-dependent threshold, then combined the result with the circular Dot Mask generated earlier.

The two results were combined using Multiply.

Node connection:

Dot Mask × Random Distribution → Pattern T2

This creates a mixture of black-and-white dots in the middle tonal region.

Pattern T3: Black Dots on White

For brighter regions, I wanted the background to remain predominantly white, with a small number of black dots.

I used a similar procedural pattern and applied OneMinus to invert the black-and-white values.

Node connection:

Dot Pattern → Multiply → OneMinus → Pattern T3

This reverses the original pattern, producing black dots on a white background.

I also used WhiteDotMaxDensity to control the maximum density of dots in the brighter regions.

Step 5: Combining Tonal Regions

Finally, I used multiple Lerp nodes to combine the different dithering patterns with solid black and white regions.

The main outputs included:

* Solid Black

* Pattern T1

* Pattern T2

* Pattern T3

* Solid White

The transitions between these outputs were controlled by the tonal masks generated earlier, including Mask_M2, Mask_M3, and Mask_M4.

The overall workflow was:

Grayscale → Tonal Masks → Dithering Patterns → Lerp → Emissive Color

As a result, the grayscale values were no longer displayed directly. Instead, they were represented through different densities of black-and-white pixels.

3. Result & Reflection

Through this experiment, I was able to convert the tonal structure generated by the five-level Toon Shader into a black-and-white dithering effect.

Compared with my previous Halftone experiments, this material allowed different tonal regions to use independent pixel distribution rules, producing a visual style closer to Return of the Obra Dinn.

However, I encountered several challenges during development.

The first challenge was balancing the grayscale thresholds and dithering density.

Because each tonal region had its own control parameters, adjusting one threshold could affect the distribution of neighbouring regions. In particular, excessive dot density between the middle and lighter tonal regions made the image appear visually cluttered, while insufficient density caused some details to disappear.

To address this, I repeatedly adjusted parameters such as M2, M3, M4, BlackDotMaxDensity, and WhiteDotMaxDensity to find a suitable balance.

The second challenge was balancing dot size and image detail.

When GridDensity was low, the dots appeared larger, strengthening the retro pixel aesthetic but reducing surface detail.

When GridDensity was high, the dots became smaller and more closely packed, preserving more structural information. However, when viewed from a distance, the dense patterns could visually blend together and appear almost grey.

Therefore, I needed to adjust both GridDensity and DotSize to balance the visual style with image readability.

Overall, this experiment helped me better understand how procedural materials can control the tonal structure of an image rather than simply changing its surface colours.

By combining five-level toon shading, custom lighting calculations, and procedural dithering, I developed a stylised rendering system in which lighting, tonal thresholds, and pixel density could be adjusted independently.

This experiment also provided further possibilities for exploring the visual direction of Rushing Place.

Leave a Reply

Your email address will not be published. Required fields are marked *