At the beginning of the project, I considered using a young girl as the main character to explore anxiety and the pursuit of freedom through her personal experiences.
However, as the project developed, I realised that the girl’s distinct age and identity might lead audiences to interpret the film as a personal coming-of-age story, rather than an exploration of the broader social anxiety I wanted to address.
Therefore, I decided to move away from this design and use a plastic mannequin as the main visual reference.
2. Design Concept
Plastic mannequins have standardised body proportions, smooth surfaces and expressionless faces.
Their lack of individual identity makes them more suitable for representing emotional numbness in urban life, as well as the gradual loss of individuality under social expectations and collective routines.
3. Visual Design
I retained the main characteristics of plastic mannequins, including simplified body structures, smooth surfaces and minimal facial features.
As the film uses stylised black-and-white rendering, I reduced unnecessary surface details and focused on clear silhouettes and body proportions.
I also used clothing and posture to distinguish different characters while maintaining a consistent visual style.
4. Character Movement
Since the characters have limited facial expressions, body movement becomes the primary way of communicating emotion.
I used repetitive walking, running and mechanical movements to represent anxiety and emotional numbness in urban life.
As the film’s pace accelerates, the characters’ movements become increasingly hurried, contrasting with their expressionless faces.
The characters appear emotionless, yet they are constantly moving and unable to stop.
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.
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.
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.
After completing the visual analysis of Return of the Obra Dinn, I began developing the surface material in Unreal Engine 5.
Unlike a conventional Toon Shader, this experiment presented two main challenges: creating a more complex tonal structure and allowing the material to respond to lighting within the scene.
Challenge 01: Five-Level Toon Shading
Traditional Toon Shaders typically use two or three distinct tonal levels, separating illuminated and shadowed surfaces with hard boundaries.
For this experiment, however, I wanted to divide the surface shading into five tonal regions:
Black → Dark Grey → Mid Grey → Light Grey → White
Rather than using the same transition between every region, I wanted to combine smooth gradients with sharp boundaries.
Tonal Transition Expected Effect
Black → Dark Grey Smooth gradient
Dark Grey → Mid Grey Sharp boundary
Mid Grey → Light Grey Smooth gradient
Light Grey → White Sharp boundary
This design was intended to support the subsequent Post Process Material.
Since the final black-and-white dithering effect would use different pixel distributions for different tonal regions, the surface material needed to provide a controllable brightness structure.
If all transitions used hard boundaries, the image would appear too flat. However, using only smooth gradients would weaken the visual impact of large black-and-white shapes.
Therefore, I needed to combine both transition methods within a single material.
Challenge 02: Dynamic Lighting
The second challenge was making the Toon Shader respond to changes in light positions within the scene.
In UE5, when using an Unlit Material for custom toon shading, the material does not automatically respond to conventional scene lighting.
This means that moving a Point Light would not automatically change the shading on the object’s surface.
For Rushing Place, the environment contains multiple light sources and complex spatial structures. Without lighting interaction, the relationship between characters and their surroundings would lack visual consistency.
Therefore, I needed to develop a custom lighting calculation system that could read light positions from the scene and calculate brightness based on the orientation of each surface.
2. Technical Development
To create a five-level Toon Shader that could respond to scene lighting, I divided the development into two stages: first, building the five-level shading system, and then implementing custom lighting through Blueprint and a Material Parameter Collection.
Step 1: Five-Level Toon Shading
First, I created an Unlit Material in UE5 and used Emissive Color to output the final colour.
Unlike traditional Toon Shaders, I wanted different transitions between the five tonal regions. To achieve this, I combined Lerp, Step, Divide, and Saturate nodes to create both smooth gradients and sharp boundaries.
To make the different regions easier to identify during testing, I initially used five colours: blue, red, yellow, green, and white. These would later be converted into grayscale.
1. Setting Up Five Colour Regions
I created five Vector Parameters representing different tonal levels, along with five Scalar Parameters to control the brightness thresholds.
Parameter Value
M1 0.2
M2 0.4
M3 0.6
M4 0.8
M5 0.9
These parameters control the distribution and transition points of the different colour regions.
2. Creating Smooth Gradients
For regions requiring smooth transitions, I used Divide and Saturate to generate blending values between 0 and 1, then used Lerp to blend between two colours.
The first gradient was created using the following node connection:
This allows the gradient to remain within a controlled brightness range without affecting other tonal regions.
3. Creating Sharp Tonal Boundaries
For regions requiring clear boundaries, I used Step nodes to compare LightValue against thresholds such as M2 and M3.
The node connection was:
LightValue + Threshold → Step → Lerp (Alpha)
When the brightness crosses a specified threshold, the Step node switches its output between 0 and 1, creating a sharp transition between two colours.
Finally, I combined the five colour regions using multiple Lerp nodes and connected the final result to Emissive Color.
This allowed me to control both smooth gradients and sharp tonal boundaries within a single material.
Step 2: Custom Lighting System
After completing the five-level Toon Shader, the next step was to make the tonal regions respond to light positions within the scene.
Since I was using an Unlit Material, the shader could not directly respond to UE5’s conventional lighting system. Therefore, I needed to manually calculate the directional relationship between the surface and each light source.
1. Creating a Material Parameter Collection
First, I created a Material Parameter Collection (MPC_ToonLight) to share lighting information between Blueprint and the material.
I added three Vector Parameters:
LightPos0 / LightPos1 / LightPos2
These parameters store the world positions of three Point Lights.
2. Passing Light Positions Through Blueprint
Next, I created BP_ToonLights3 and added three Point Light Components.
In the Construction Script, I used Get World Location to retrieve the position of each light, then used Set Vector Parameter Value to write these coordinates into MPC_ToonLight.
The node connection was:
Point Light → Get World Location → Set Vector Parameter Value
Each light was assigned to its corresponding parameter: LightPos0, LightPos1, or LightPos2.
This allowed the material to access the light positions without manually entering their coordinates.
3. Calculating Surface Brightness
Back in the Material Editor, I used Collection Parameter nodes to retrieve the three light positions.
First, I used Absolute World Position to obtain the world coordinates of the current pixel.
Then, I used Subtract to calculate the direction from the surface towards the light source, followed by Normalize to convert it into a unit vector.
The node connection was:
Light Position – Absolute World Position → Normalize
Next, I used PixelNormalWS to obtain the surface normal in world space and Dot Product to calculate the relationship between the surface orientation and the light direction.
PixelNormalWS + Light Direction → Dot Product
When the surface faces the light source, the Dot Product produces a higher value. When the surface faces away from the light, the value becomes lower.
I repeated this calculation for all three light sources.
4. Combining Multiple Light Sources
To allow the material to respond to three light sources, I used two Max nodes to combine the three Dot Product results.
The node connection was:
Dot 0 + Dot 1 → Max → Max (Dot 2)
This method selects the highest brightness value from the three light sources, preventing excessive brightness caused by directly adding their contributions together.
I then used ConstantBiasScale to adjust the brightness distribution and Saturate to clamp the result between 0 and 1.
The resulting LightValue was connected to the five-level Toon Shader developed earlier, controlling the distribution of the different tonal regions.
This allowed the surface shading to change when the light position data was updated.
It is important to note that this system calculates brightness primarily based on light direction. It does not fully simulate physically accurate lighting effects such as distance attenuation, occlusion, or shadows.
Step 3: Final Material Integration
Finally, I connected the custom lighting system to the five-level Toon Shader.
The overall material workflow was:
Point Light Position → Surface Normal Calculation → LightValue → Five-Level Shading → Emissive Color
Through this process, I developed a Toon Shader with five distinct tonal regions, different transition styles, and the ability to adjust its shading based on multiple light positions.
This material provided a controllable brightness structure for the subsequent Post Process Material, allowing me to further convert different grayscale regions into black-and-white dithering patterns.
3. Result
Through this experiment, I developed a five-level Toon Shader capable of calculating surface brightness based on multiple light positions.
Compared with a conventional Toon Shader, this material allows me to control not only the number of tonal regions but also the transition style between them.
By using a Material Parameter Collection and Blueprint, the material can also update its shading distribution according to the light position data provided by the scene.
The main goal of this stage was not to generate the final black-and-white dithering effect, but to establish a stable and controllable tonal structure for the subsequent Post Process Material.
Following my previous Halftone, Cross-Hatching, and Soft Two-Tone Shading experiments, I began exploring a more complex black-and-white rendering style. This time, my visual reference was the game Return of the Obra Dinn.
What attracted me most to this game was its distinctive monochrome aesthetic. The visuals are primarily composed of black and white, using large areas of solid colour and pixel patterns of varying densities to represent lighting and spatial structure.
When analysing the reference, I identified two key visual characteristics.
First, there is a clear separation between illuminated and shadowed surfaces. Large black-and-white regions emphasise the geometric structure of objects, creating strong visual contrast.
Second, instead of relying on traditional grayscale gradients, the image uses different densities of black-and-white pixels to represent intermediate brightness levels. I divided this visual approach into five main tonal regions:
Tonal Region Visual Representation
Darkest– Black background with a few white noise dots
Dark –Solid black
Mid-tone — Mixed black-and-white dithering
Light– White background with a few black noise dots
Brightest –Solid white
This approach maintains strong black-and-white contrast while preserving tonal variation through changes in pixel density.
Another important feature I wanted to preserve was the clarity of object silhouettes. In complex scenes, clear outlines help distinguish characters, buildings, and background elements, preventing objects with similar brightness values from blending together.
Therefore, I wanted to apply this combination of bold black-and-white shapes, procedural dithering, and clear outlines to Rushing Place.
2. Technical Planning
To achieve this effect, I planned to divide the rendering system into two parts: Surface Material and Post Process Material, responsible for object outlines and tonal representation respectively.
Surface Material | Adaptive Black-and-White Outlines
First, I planned to create adaptive black-and-white outlines using surface materials.
Since the final image would contain only black and white, outlines with a fixed colour could easily blend into the background. To address this, I wanted the outline colour to change automatically based on the surrounding brightness:
* Dark regions: White outlines.
* Light regions: Black outlines.
This approach would help characters and environmental objects maintain clear silhouettes under different lighting conditions.
Post Process Material | Grayscale Quantization & Procedural Dithering
The post-process material would handle three main stages.
Step 1: Grayscale Conversion
First, I would convert the original rendered scene into grayscale, extracting brightness information while preserving the original lighting relationships.
Step 2: Grayscale Quantization
Next, I planned to divide the continuous grayscale values into five stable tonal levels using different brightness thresholds:
Black / Dark Grey / Grey / Light Grey / White
Each tonal level would correspond to a different black-and-white pixel distribution.
Step 3: Procedural Dithering
Finally, I would represent the brightness of each pixel by adjusting the density of black-and-white patterns according to its tonal level.
The darkest regions would use a black background with a few white dots, while the dark regions would remain solid black. Mid-tones would use a mixture of black-and-white dots, lighter regions would have a white background with a few black dots, and the brightest regions would remain solid white.
I planned to use ScreenPosition and mathematical nodes to generate a procedural grid, then control the distribution of black-and-white pixels using pseudo-random values.
Rather than displaying grey directly, this method would create the perception of different brightness levels through the proportion of black and white pixels, producing a dithering effect similar to the visual reference.