The Technical Art of Ascending Inferno

Making Of / 27 October 2024

Hey everyone! Two years ago I had the opportunity to join Oppolyon Studios to work on their project Ascending inferno. From day one, I could tell that this project would not only push my technical and creative boundaries but would also allow me to collaborate with a team of passionate and talented developers.

In this post I’ll be taking you behind the scenes of my contributions to Ascending Inferno; diving into some of the challenges and features I had the opportunity to work on and showcasing some of my proudest visual effects. But before we jump into the details, let me give you a quick overview of Ascending Inferno.

Ascending Inferno is a 2.5d rage-platformer where you play as Dani, a soccer player, tragically you will die and be sent to the bottom of hell where, fortunately you encounter your brother Vincent. Unfortunately, however Vincent has taken on the form of a ball and needs your help to be kicked up the nine levels of Dante's inferno.

You can find the game here!


The game is set across ten distinct levels: The Earthly Realm (which serves as the tutorial), followed by Limbo, Lust, Gluttony, Greed, Wrath, Heresy, Violence, Fraud, and Treachery. Below, I’ll take you through some of the mechanics, visual effects, optimizations, and world-building elements that helped bring these environments to life.

Contents

  • Visual Effects For Game Mechanics
  • Visual Effect Overlays
  • Vincent Camera - Improving Gameplay
  • Optimising The Project
  • Shaders
  • Project Juice
  • World Building Blueprints
  • Set Dressing and Level Construction Challenges
  • Turntables: Crafting Immersive Character Customisation
  • Character Customiser: Adding Wings, Hats and Trails
  • Automatic Scalability Options
  • Conclusions

Visual Effects For Game Mechanics

To begin, let’s take a closer look at the particle effects and how they play a critical role in world-building and conveying information to the player. In this section, I’ll break down some of the core game mechanics, their visual effects, and the layers we added to achieve the result. I’ll also touch on some of the decorative effects that brought our world to life.

Each of our ten levels features a unique gimmick that players must understand to progress, with one exemption: Limbo. The tutorial is designed to quickly teach players how to control Dani and the soccer ball, guiding them through movement, manipulating the ball, and how the ball affects the world. It was important that we introduced the mechanics progressively as to not overwhelm them with new information and ensure smooth progression. We chose to first introduce a door – something familiar, but which required players to fully understand before they could continue forward.

Once the tutorial is completed, players are thrust into Hell, where the true challenge begins. At this point, there are few areas that feel truly safe, leaving falling between levels as a constant threat. In designing Limbo, we decided that the level itself should be challenging enough without the need for an additional gimmick. Instead, we focused on large, simple jumps and big falls. We also used Limbo as an opportunity to introduce shortcuts—secret routes that would let skilled players bypass some of the harder sections. While these shortcuts were subtle, they played an important role in the player's exploration and learning experience.

Moving on to the third level, Lust, we introduced the player’s first real obstacle: a demon whose hypnotic dance draws Vincent in before kicking him away. To bring this mechanic to life, I created three distinct visual effects: a swirling vortex around the dancer, a screen-space overlay to enhance the hypnotic effect, and a subtle blue swirl around Vincent. These effects worked together to clearly signal the mechanic’s function to the player, adding both visual clarity and thematic consistency to the level.

Next up is Gluttony. This level presented some design challenges, and we went through several iterations before settling on the final mechanic. Initially, we created sticky platforms that were supposed to make it harder to move Vincent, but this inadvertently helped players more than hindered them. To raise the difficulty, we switched to “hot” platforms. These would cause Vincent to bounce uncontrollably, eventually ejecting him if the player didn’t regain control. It was a chaotic mechanic that fit the gluttonous excess of the level perfectly.

The fifth level, Greed, was another one that took a few attempts to get right. Initially, we wanted to punish players who took too many shortcuts, but this design proved problematic as our intended shortcuts took too long to access. By this point, the level’s layout was already locked in, so we pivoted to a wind mechanic. Now, strong gusts of wind push Vincent left or right while inside the wind volume, adding an additional layer of difficulty to the level.

For Wrath, we were torn between two ideas; sinking platforms and hands that pull you through the ground. Both ideas drew inspiration from Dantes Inferno, but both presented their own design challenges. Ultimately, we decided to go with the hands. This is one of the mechanics I’m most proud of, the first iteration I made ran below 20 frames per second with only 8 sets of hands running. After a couple attempts, I was able to offload all the calculations onto the GPU through the shader, then using Unreal Engine’s foliage brush we could have them instanced anywhere. Suddenly we went from approximately 50 hands all the way up to 80,000 before we started seeing framerate issues.

Our next level Heresy is right before the checkpoint, so we wanted it to be a bit of a challenge but not completely unforgiving. We ended up making this one the moving platforms biome. Aside from a few steam and smoke visual effects there weren’t too many particles to be made here, so instead most of my time was focused on the trains.

Once players make it past the checkpoint in between Heresy and Violence it becomes impossible to fall back to the previous levels, this means that we could drastically increase the difficulty of the mechanics without them being too punishing. For Violence we wanted something that could throw Vincent off screen with a large surrounding space to ensure disaster. I was tasked with creating a “lightning” and “sphere” effect, which I interpreted as a plasma ball. Sparks would gather around the outer edge of the sphere and discharge toward the centre, creating a satisfying explosion that propels Vincent off into danger.

Fraud was all about creating the feeling of being out of place, as an impostor. To reinforce this, we designed a stage that our player would run along with a grand audience behind them, watching all their movements. Our mechanic for this one was going to be false platforms driven by what was and was not in light. After some playtesting we found it just wasn’t fun or easy to convey, so instead the platforms now glitch before popping out of visibility.

We also wanted Fraud to have a fake ending—a grand, open, celebratory moment where players think they’ve beaten the game. Little do they know, they are still in Fraud, and there’s more to come. This presented a significant technical challenge, as the entire level above had to remain loaded and visible while the player navigated the fake finale. To solve this we set all the materials in Treachery to invisible, then as the player hits a trigger we fade in the entire level and cover it all with another skybox.


Finally, we reach Treachery, the last level of Ascending Inferno. From the beginning, we knew that this level would be modelled after the frozen lake described in Dante’s Inferno. The key mechanic here was the icy platforms, which were slick and unpredictable, creating one last difficult challenge before the final ascent.

Visual Effect Overlays

In addition to the core particle effects, we added an extra layer of visual polish to Ascending Inferno through screen space overlays. These overlays played an important role in providing key information to the player without breaking immersion. One of the most prominent examples is the title cards that appear when the player enters a new level. For these, our talented pixel artist Fauzi created a series of flipbook textures, and I took on the task of integrating them into the game. I developed the blueprint and shader system that would trigger these title cards, ensuring they appeared smoothly and consistently across each level.

Another notable overlay effect I worked on was what we called "the supernatural effect." This was triggered whenever the player discovered one of the game’s secret rooms. The effect prominently featured the demon’s emblem glitching across the screen before settling in the centre and fading out. I approached this effect like a camera shake, introducing randomness and distortion into the visual to give it an unsettling, otherworldly quality. This was achieved by offsetting the texture by random values, applying a glitch texture to distort the UVs, and adding some chromatic aberration in the shader. These small details gave the effect a chaotic, supernatural feel that fit perfectly with the tone of the game. The supernatural effect was designed to unsettle the player, giving the discovery of secret rooms an eerie, unpredictable feel. By using random distortion and chromatic aberration, I wanted to create a moment that felt visually jarring, amplifying the tension and mystery that players would associate with these hidden areas.

Vincent Camera - Improving Gameplay

One of the core challenges in Ascending Inferno is navigating Vincent—the ball—up through the layers of hell. While getting the player to the top is relatively straightforward, taking no more than 15 minutes of continuous running, the true difficulty lies in managing Vincent’s journey. Given how easily Vincent can fall or be kicked off course, it became essential to provide the player with a way to keep track of him. To address this, we implemented a secondary camera that pops up in a small window whenever Vincent goes off-screen, showing his position as he falls, waits, or gets knocked around.

However, this feature came with a major challenge: rendering a second camera while maintaining good performance. Initially, we thought this feature might be too demanding, especially for lower-spec systems, and considered abandoning it. But after revisiting the problem, I made several optimisations to the camera system. By reducing the secondary camera’s framerate to 24fps and lowering some of its rendering quality, we made it more efficient without compromising the player experience. Our programmer, Ben, also played a pivotal role by developing a chunking system to deactivate unnecessary game objects in the scene. Together, these optimisations allowed us to reintroduce the Vincent camera without seeing any significant performance drop, even on the lowest-spec machines. This was a huge win for our team and really added to the player's connection with Vincent, ensuring they could always keep an eye on him during tricky segments.

Optimising The Project

When I first joined the team, Ascending Inferno was running at a sluggish 20fps, even on relatively high-end computers. It was immediately clear that there were multiple bottlenecks impacting the GPU, CPU, and memory usage, all of which needed to be addressed to improve performance. The first issue I tackled was the game’s texture budget, which was filling up rapidly, even with relatively few assets on screen.

The problem stemmed from the workflow our team was using, which, due to a limited understanding of optimisation, involved creating models in MagicaVoxel, transferring them to Blender to reduce polycount and bake textures from vertex data, and then importing them into Unreal Engine. This process left a lot of wasted texture space, and downscaling from 4K textures was nearly impossible. Additionally, each asset had its own unique shader rather than using material instances, which meant we weren’t taking advantage of shader batching, leading to unnecessary overhead.

To fix this, I revamped our material workflow. We didn’t have an immediate solution for how to remake the models, but I knew we could simplify our materials to reduce both texture usage and shader complexity. I implemented a 2D lookup table to sample the colours based on the voxel UVs. With this system in place, we were able to recolour objects simply by swapping the lookup table, drastically reducing texture sizes and increasing efficiency.

Once the memory issue was under control, I shifted my focus to the CPU and GPU bottlenecks. The CPU problem was relatively easy to identify but incredibly complex to solve. Unreal Engine is optimised for games built on two primary axes (X and Y), but our game had a unique structure: it was 60,000 units tall but only a single chunk wide. Because of this, Unreal’s built-in chunking system wasn’t functioning properly, and the game was keeping objects in memory and processing them even when they were far out of view.

At first, we tried disabling frustum culling, which yielded a slight performance improvement since our game uses forced perspective, but it wasn’t enough. Working closely with Ben, our programmer, we devised a custom chunking system. This allowed us to dynamically load and unload portions of the level as the player progressed, significantly reducing CPU overhead.

With the CPU issue addressed, I moved on to tackling the GPU bottlenecks. Although Unreal Engine 5.0 had introduced powerful new tools like Nanite and Lumen, we quickly discovered that our forced-perspective voxel art style limited our ability to take full advantage of them. Nanite’s clustering wasn’t compatible with our models, and as a result, Lumen’s performance also suffered without Nanite-enabled assets.

Digging deeper, I identified three key issues affecting the GPU:

  1. Material Batching: As mentioned earlier, our use of single-use materials instead of instances was preventing efficient shader batching.
  2. Lighting: Our scenes were overloaded with overlapping lights, creating hotspots and driving up the complexity in lighting calculations.
  3. Geometry Density: Our voxel models were highly detailed with numerous hard edges, making many of the polygons smaller than a single pixel. This sub-pixel anti-aliasing was inflating the cost of rendering the scene, especially on large, dense meshes.

Unfortunately, by the time we discovered these issues, many of the assets for the first three levels had already been created and integrated, and reworking them wasn’t feasible given our production timeline. However, we found a compromise: we simplified the geometry on larger, flat surfaces, replacing complex meshes with simple cubes. We then used normal maps, displacement, and parallax occlusion mapping to preserve the visual detail, all while reducing the rendering cost.

In the end, these optimisations drastically improved performance across the board, making the game playable on lower-end hardware without sacrificing the artistic integrity or visual appeal of the levels. While we couldn’t leverage all of Unreal Engine’s new features, the custom solutions we developed allowed us to bring Ascending Inferno to life, without compromising on performance.

Shaders

In my pursuit to optimize the rendering pipeline for Ascending Inferno, I developed several shaders, each with the following considerations in mind:

  1. Adjustability: Could the shader be adapted for many variations?
  2. Modularity: Could it be used for tiling assets or modular pieces?
  3. Performance: How efficient was it for rendering?
  4. Scalability: Could we simplify it for lower-end platforms?
  5. Transparency: Could we avoid using transparency to optimize performance?

While not every shader could meet all these constraints (such as glass shaders, which require transparency), each was crafted to strike the best balance for the game’s unique needs.

The first shader I developed was meant to optimize prop rendering, particularly for the large number of assets already in development. Initially, it was a basic shader with an albedo colour chart and world-space tri-planar projected roughness, but over time it evolved to include several crucial features.

One major improvement was shifting the UV space from world-space tri-planar to local-space tri-planar, which allowed the roughness to respect object rotation, thus preserving the correct voxel sizes. Although I encountered a bug where scaling distorted the UVs, it remained mostly unnoticeable in the final build.

To provide more control over how objects appeared, I introduced metalness and rust sliders, which allowed artists to adjust the wear and aging effects on props. This flexibility gave the assets a much broader visual range without needing to recreate textures or models. Additionally, to solve visibility issues (when players passed behind large props), I added a screen-space dithered mask cut-out. This feature allowed certain props to become transparent or semi-transparent, improving gameplay clarity without compromising immersion.

Tri-planar

This shader was critical in reducing GPU rendering costs. We needed a simple model that could be used across multiple levels while still looking visually detailed. The solution was a tri-planar shader, which used surface deformation methods to maintain the detailed look of objects without requiring expensive geometry. I also introduced object scale UVs to add further versatility, though this feature was eventually scrapped due to a recurring compilation error when applied to skeletal meshes.

Like the prop shader, this shader supported metalness, rust, and dithered opacity, which helped keep props and walls visually consistent throughout the game. The goal was to ensure that all elements appeared cohesive, even as we shifted the underlying technical complexity for optimization.

Sprites

The sprite shader was an entirely different challenge. Unlike the modular shaders for props and walls, this shader was focused on single-use textures. Initially, the shader supported a flipbook texture, an emission texture, and a few parameters to control timing. However, as the game’s design evolved, so too did the shader's complexity. For example, some textures had 6 frames and non-power-of-two dimensions, so I had to implement cropping to remove unnecessary blank space from the flipbook. New levels also demanded different visual effects, such as rim lighting for Lust and desaturation of blue and green channels for Violence.

Despite the increasing complexity, the shader maintained a consistent rendering cost, which was a relief. However, the numerous static switches led to longer compilation times towards the end of the project. Although we could have further reduced compilation complexity, the time investment wasn't justified given the project’s deadline. Initial builds took a couple of hours to compile, but subsequent builds reduced this time to around 30 minutes.

Glass

Glass presented a unique challenge in terms of both performance and appearance. Transparency is notoriously expensive, and in some areas of the game, we had a significant amount of glass. In our fixed perspective, glass often didn’t look convincing, so I stylized it with two key components:

  1. A white speckled noise texture, which helped ground the glass in the world.
  2. A diagonal streak effect, like what you’d see in cartoon games, adding a subtle shine and vibrancy to the otherwise flat material.

This approach allowed us to maintain the game’s visual style while optimizing the rendering cost of glass objects.

Mirror

A more specific shader developed late in production was the mirror material for Fraud. A standard low-roughness, metallic material didn’t quite fit our world. Like the glass shader, it felt like it was missing something. I reused the glass streak effect but skipped the speckled noise, instead introducing roughness variation across the surface to give the mirrors more life.

To complete the effect, we also made sure the mirrors had something to reflect, avoiding the dull appearance of reflecting an empty void.

Virtual production stage

One of the game's biggest technical hurdles was how to render an entire level while hiding it behind an artificial boundary, maintaining a large open area. I was tasked with creating a virtual production stage, inspired by hexagonal LCD panels.

The shader I developed used an alpha mask that gradually revealed more of the stage in a broken, jittering state. To achieve this, I designed the colour reveal to mimic the behaviour of real RGB pixels, increasing and decreasing their values according to the underlying texture.

Project Juice

With Ascending Inferno now running at a stable 60 frames per second, and shaders finalized, I shifted focus towards adding smaller details and interactive elements that could deepen the player's immersion. The goal was to fill the levels with objects that players could interact with or that would interact with them, creating a world that felt alive and responsive. This part of development focused on adding easter eggs, player-triggered interactions, and various “juice” elements to enhance the overall experience.

Lust: The Staring Eyes

One of the first interactive elements I developed was for Lust, a level filled with hundreds of eerie eyes scattered throughout the environment. These eyes were meant to act like cameras, always watching the player no matter where they moved.

Initially, I implemented this using a blueprint setup, which resulted in each of the 400-500 eye cameras being constantly active. Unfortunately, this tanked the frame rate, dropping it as low as 10 frames per second. To resolve this, I rewrote the blueprint so that the eyes would only activate when the player entered a specific trigger area. While this improved performance, a side effect of chunked level loading caused the eyes to lose track of the player after each load, resulting in periodic lag spikes.

After multiple attempts, I settled on a simpler solution—moving the eye movement to the shader. I had hoped to create a fully functional 3D rotation that would track the player’s movements but ran into limitations in my knowledge of vertex shader math. Instead, I made the eyes sweep back and forth on a set trajectory. Although I initially saw this as a compromise, playtesting revealed it was more engaging: more players noticed the sweeping motion than had noticed the original player-tracking version, so in the end, it was a happy accident.

Fraud: Virtual Production Cameras

Next, I worked on interactive cameras for the virtual production set in Fraud. These cameras, unlike the ones in Lust, were large filming cameras placed on tracks. Since there were only 2-4 cameras per scene, I didn’t have to worry about performance issues. I recycled much of the code from the Lust eyes blueprint and adapted it for this new purpose. With fewer cameras, even with chunked loading, performance remained stable.

Dynamic Lighting: Gradients Based on Player Movement

With Ben’s assistance, I worked on dynamic lighting that could shift based on the player's position. This effect was inspired by the team’s desire for lighting to reflect the emotional and visual tone of each level dynamically.

Using the player’s location as a reference, I sampled a colour curve to change the directional and skylights in real-time. Later, I expanded this feature to modify exponential height fog, allowing us to change its colour, density, and refraction based on the same system. This adaptive lighting helped keep the game visually fresh, adding subtle atmosphere shifts as players traversed each level.

Gluttony: The TV Easter Egg

In Gluttony, I was tasked with creating a working television set for the diner level. Initially, I experimented with smaller TVs scattered around the scene but found they lacked impact. Instead, I placed a single large TV that players could interact with.

Inspired by the chaotic nature of the level, I added a mechanic where kicking Vincent (the character) into the TV would cycle through channels until the television broke. To add a fun easter egg, I inserted a hidden video of my cat. If players hit the broken TV enough times, they would be rewarded with a short clip of my cat on-screen, adding a personal touch to the game.

Gluttony: Conveyor Belts and Trash Physics

While working on Gluttony, I noticed that the conveyor belts running through the lower levels needed more "juice." I experimented with allowing the player to kick objects off the belts. However, due to the way the belts were set up, it was tricky to get the interaction to feel natural.

Eventually, I found that adding a small collider to detect player momentum and applying it to the trash objects worked, but the interaction remained difficult to control. Although it wasn't perfect, the mechanic added a nice touch of interactivity to the level, making it feel more dynamic.

Interactive Platforms: Shimmy Without Collision

To make some of the platforms react to player inputs, we decided to add a shimmying effect when players jumped on them. However, the platforms couldn’t alter their collision or physically move the player or ball, so the effect had to be purely visual. This turned out to be more challenging than simply rotating an object, so I developed a solution using a shader.

The shader rotated the model based on a pivot point using a timeline defined in an external blueprint. While this method worked well and looked great, setting it up for each platform was time-consuming, so it wasn’t as widely implemented as I would have liked by the project’s end.

Violence: The Fighting Game Easter Egg

In Violence, Chris (our pixel artist) and I collaborated on a fun easter egg inspired by discussions about how video games allegedly cause violence. We thought it would be a nice touch to add a pixel-art fighting game in the background of this level.

Chris created pixel art sprites of the fighters, while I handled the shaders for the health bars and the infinite timer. I also scripted a blueprint that would toggle through the various fight states. Initially, the fighters were meant to be Chris and myself, but we later decided to reference all the developers in Ascending Inferno by cycling our names in the fight roster. This led to amusing moments during playtesting, as we’d stop and watch the characters fight to see who was winning that day. It became a fun, light-hearted feature that consistently drew laughs from my fellow developers.

World Building Blueprints

While the “juice” added flair and personality to Ascending Inferno, the backbone of the immersive experience was built on the carefully constructed world-building blueprints. These blueprints ensured that the game world could be created efficiently while maintaining the desired level of detail and atmosphere. Initially, several blueprints were already in place when I joined the project, but as development continued, it became apparent that additional, more specialized blueprints were necessary to meet the creative and technical demands.

Spline Follower Blueprint: Trains, Rats, and More

The first significant blueprint I worked on was a spline follower. This was pivotal for creating animated world elements like planes flying in the sky, rats scurrying along the ground, and trains moving along the tracks in Heresy.

The spline follower allowed us to lay out a path for these elements to follow smoothly. However, as we used this blueprint across various levels, one limitation became clear, there was no way to instance meshes along the same spline. This resulted in us duplicating the spline and manually offsetting objects, which became tedious, especially in Heresy, where both the tracks and the trains needed to follow the same path. Next time I work on a similar feature, I plan to include the ability to instance meshes directly along the spline, saving both time and effort.

Dynamic Lighting Blueprint: Adjusting for Quality Settings

Another critical blueprint I created was for dynamic lighting adjustments. This was born out of an issue we encountered while switching between high and low-quality settings. Our game relied heavily on Lumen (Unreal Engine’s global illumination system) and emissive materials to create the proper lighting ambiance. However, when switching to lower quality settings, the lighting in certain areas would fall flat, leading to a lack of depth.

To address this, I built a small blueprint that took the current quality settings into account. The blueprint would assess whether the emissive materials alone were sufficient to light the area or if additional light sources should be enabled. This was a fascinating technical challenge, and it’s something I hope we can further streamline in future projects to avoid needing such workarounds.

Clothesline Blueprint: Decorating Greed’s Underground

In Greed, we wanted to create a slums-like underground environment filled with clotheslines to help bring the space to life. Hand-placing each clothesline in Unreal Engine would have been a time-consuming task and building them as single-purpose in Blender would lack the flexibility we needed.

To solve this, I created a dynamic clothesline blueprint that allowed us to quickly scale and adjust the lines and the number of clothes hanging from them. This blueprint allowed the artists to scatter dozens of clotheslines throughout the underground area, giving the level a lived-in, chaotic look without requiring manual placement of each item.

Skybox and Layer Transitions: Seamless Vertical Biomes

One of the major visual challenges we faced was the vertical nature of the game. Each biome in Ascending Inferno needed to feel distinct and self-contained, but we also wanted to create the illusion of a continuous world.

To accomplish this, we opted for a fake skybox system that would wrap around and between each biome. The challenge, however, was making sure the transition between biomes appeared smooth. Without an object to block the view, the sky would continue into the camera plane, creating a jarring visual. To solve this, I used a custom depth pass to create cut-outs in the skybox, allowing for clean transitions between biomes. This maintained the illusion of a large, seamless world while allowing players to move vertically between different levels.

Parallax Backgrounds and Large Artwork: Solving Clipping Issues

The last major blueprint I worked on tackled a significant issue we faced with our parallax background art. For many levels, we split the background artwork into several layers to create a parallax effect, where objects at different distances would move at varying speeds as the player moved. These background layers were often hundreds of units tall and wide, sometimes clipping with skyboxes or other levels.

The most noticeable issue occurred in Lust, where a giant demon and a disco ball were part of the background art. As players progressed closer to Gluttony, the level geometry and meshes began covering the background art, breaking the illusion that the demon and disco ball were far away in the distance.

To fix this, I used a clever technique involving the camera position and screen-space UVs. Instead of displaying the demon and disco ball as part of the background layers, I projected them onto the skybox itself. This projection was paired with a slow offset, mimicking the parallax movement we initially desired. This solution allowed the demon and disco ball to remain visible and convincing, regardless of the player's location, without the risk of clipping through the environment. On the other hand, while this technique worked well, it also appeared in the main menu flythrough cameras meaning those had to be curated around the skybox limitation.

Set Dressing & Level Construction Challenges

The set dressing and level construction process for Ascending Inferno was highly collaborative, with each artist contributing to the creation and refinement of the game world. While many contributions were a joint effort and hard to define individually, there were certain aspects of the process that are clearly identifiable as my own work.

Setting the Mood: Initial Look Development

In the early stages of each level’s development sprint, my primary focus was on establishing the general feel and look of the level. This included spending several days working on the blockout, filling it with generic assets such as rocks, walls, windows, and doors. During this time, I also focused on material and texture creation to drive the look-development process and give the levels a clear aesthetic foundation.

While I worked on these foundational elements, the other 3D artists, Ricky and Lachlan, focused on creating unique props to further enrich the environment. These props, ranging from scattered debris to interactable objects, would later fill out the levels and provide them with detail and personality.

Alongside this, I also began the first pass on lighting for each scene. This initial lighting setup mainly involved directional and skylights, as it allowed us to get a rough idea of how the lighting would interact with the environment and assets as the level progressed.

Lighting for Atmosphere: Establishing Distinct Biomes

As the levels became more fleshed out and moved toward their final form, I turned my attention to lighting the levels to create a cohesive atmosphere. My goal was to use distinct colour palettes to reflect the themes and emotional tones of each biome. For example, the clearest colour choices were:

  • Red for Anger
  • Blue for Sadness
  • Yellow for Hunger

These colours not only added to the emotional weight of each area but also helped create a strong visual identity for each biome.

Some biomes, such as Treachery, which featured ice and frozen landscapes, were naturally suited for cool whites, blues, and turquoises. By using lighting that evoked coldness and isolation, we further emphasized the thematic qualities of treachery, drawing players into the oppressive atmosphere of the level.

Secret Rooms and Emblems: Visual Cues for Players

During the development of secret rooms, we faced the challenge of helping players identify and remember which rooms belonged to which biome. To solve this, we developed a set of emblems to serve as visual indicators. Each emblem corresponded to a specific demon and biome, and their colours were matched with the lighting of their respective level.

This colour-coding system provided a clear visual connection between the secret rooms and their respective biomes, making it easier for players to recognize rooms they had missed. These emblems also reinforced the overall mood of each biome, as they were designed to blend harmoniously with the lighting and overall aesthetic.

Set Dressing Challenges: Evolving Environments

One of the main challenges in set dressing was the evolution of the levels over time. As props were created and the levels matured, it often became necessary to revisit earlier set dressing decisions. What seemed like a simple task of placing objects initially became more complex as new assets, textures, and lighting changes were introduced.

For example, there were instances where:

  • Textures created early on clashed with newly developed assets, requiring re-texturing or re-mapping.
  • The addition of new props meant that certain objects had to be rearranged or removed to maintain a consistent look and avoid cluttering the scene.
  • Lighting changes sometimes drastically altered the feel of a level, making certain set dressing elements feel out of place or misaligned with the new atmosphere.

This iterative process meant that set dressing was rarely a “one-and-done” task, but rather something that needed to be continually refined as the game world evolved. The team had to balance creative freedom with the constraints of level design, ensuring that each level felt immersive while still serving the overall gameplay and progression.

Turntables: Crafting Immersive Character Customisation

As we neared the final stages of production, one of the key features we focused on was creating a visually engaging character customizer. Initially, the plan was simple: the player would be teleported to a generic room, customize their character, and then return to the game. However, this approach felt somewhat detached from the game's immersive experience.

I was later assigned the exciting task of designing unique customization environments for each level. Rather than taking the player out of the world, this new approach allowed them to stay connected with their surroundings by transporting them to a customization room that reflected the style and theme of the current level. This added a layer of immersion to the character creation process, making it feel more like a natural part of the game world rather than an isolated feature.

Each customization room was carefully designed with its own distinct visual identity, aligning with the tone, mood, and aesthetic of its respective level. This was a fun and challenging aspect of development, as it required pulling in familiar elements from throughout the level to ensure continuity, while also incorporating visual effects that elevated the overall experience.

If you're interested in seeing these unique rooms, you can check them out [here]!

Character Customiser: Adding Wings, Hats and Trails

After finalizing the customizer rooms, I moved on to creating some fun and thematic cosmetics for Vincent. These included a variety of wings, hats, and trails that players could unlock by achieving certain milestones, such as completing level-specific achievements. The goal was to give players a visual reward tied to their progression, adding a layer of personalization to their character.

Each set of cosmetics was inspired by the levels themselves, reflecting the themes and aesthetics of the world Vincent was exploring. While many of the levels had clear visual motifs to draw from, a few proved to be more challenging in terms of translating their essence into cosmetics—most notably, Greed and Violence. Finding ways to visually represent those abstract ideas in a cohesive and appealing way took some creative experimentation.

If you would like to see them in more detail, go [here]!

Automatic Scalability Options

The final feature I’d like to discuss is the automatic quality scaling system I developed. Rather than forcing players to manually adjust their settings, we wanted an option that would automatically optimize performance based on the players hardware capabilities.

The system is initiated when the game detects that its running below our minimum target framerate (50fps). When it detects poor framerate a UI element is presented to the player to initiate the automatic quality settings, where if accepted, will cycle through various settings such as texture quality, shadows, global illumination and shader complexity. It will gradually lower each setting until the game achieves a sustainable framerate.

Importantly, I made the design choice to handle this inside the main menu, before the player has an opportunity to play the game. This ensures that players are never distracted or disrupted by performance drops during critical moments as the adjustments can take place before the game even starts. Given our menus are full 3D scenes reflecting that of the game, it gives us a close approximation of the expected framerate while in gameplay.


In this way, the player enters the game world with optimal settings, without the need to tinker or experience frustrating performance drops. We can’t expect the player to know what each individual setting will do, so taking an automatic approach simplifies it a lot for them and creates a better game experience. You can check out how it works in the video below!

Conclusions

Looking back on the journey of working on this project, I can see how each piece I contributed has played a crucial role in shaping the final product. I’ve had the opportunity to collaborate with incredibly talented artists, designers, programmers and musicians, and in the process I’ve both taught and learned so much. Working on Ascending Inferno was both challenging, yet incredibly rewarding, allowing me to further myself as a technical artist.

One of the most fulfilling aspects was seeing how all these elements came together to create a cohesive experience for the player. Whether it was crafting the visual effects that enhanced gameplay or working through the intricacies of world-building, every step helped build a world that players could get lost in. And even though some contributions, like the screen-space overlays, may go unnoticed by the casual player, knowing how much they add to the overall immersion made every hurdle worth it.

As this project draws to a close, I find myself reflecting not just on the technical accomplishments, but also on the creative collaborations that made everything possible. Each shader tweak, lighting adjustment, and prop placement has been a step toward realizing the team's vision — and it’s something I’m incredibly proud to have been a part of.

Moving forward, I’m excited to see how players interact with the world we’ve built and where my next projects will take me. Every game is a new opportunity to learn, create, and push the boundaries of what's possible. I can’t wait for the next adventure, and I hope you’ll be there to experience it with me.


Improving on my guide for Screen-Space Halftones.

Tutorial / 11 February 2024

Why am I writing this?

4 Years ago while I was studying art for games I wrote a guide outlining how to make screen-space halftones using post processing in Unreal engine 4. Between then and now I have learnt so much about games, optimisation, workflows and tech art, so I am here today rewriting my 4 year old guide with a better understanding of halftones and post processing materials in unreal engine. Not a whole lot has changed in the way the code was laid out, however it is now significantly easier to put together, visually cleaner (both in code and game), and I have removed a lot of the redundancies. 


Contents

  • Project Setup
  • Material Setup
  • Outlines (Simplified)
  • Halftones
  • Bokeh's (Stencils/Shapes)

Project Setup

There's a couple dependencies we need to setup before we get started on the materials. I noticed while working on the halftones that I would get a lot of blur as the camera moved around, by setting the project settings anti-aliasing method to FXAA the problem disappeared entirely. I know however that this doesn't work for every project so I will cover an alternative in Material Setup that may suit your needs better. With the project settings complete we also need to setup the scene; 

  1. create a post processing volume (or use one already in your scene). 
  2. With the volume selected search for Infinite Extents (Unbound) and enable it, this will make the volume influence the camera no matter where it is in the scene. 
  3. The last step, with the volume still selected search for the dropdown Render Features, inside this drop down there will be Post Process Materials here we will add an Element --> Asset Reference. This is where we will assign our halftones, outlines or other post processing materials.

Material Setup

Depending on if you can or cannot use FXAA, I have outlined two options for you in the image below. The main points to note are which material domain are we using and what step in the render process is it injected. both of these can be found on the master node of the material.


Outlines (Simplified)

Between my old post and this one, outlines have not changed. We are simply sampling a pixel, checking each adjacent pixel and seeing if the depth has changed, if the answer is yes then we are drawing black (0), otherwise we are drawing PostProcessInput0. I spent the time making a material function in the image after this to reduce the clutter.

Below is the material function for MF_Outline.


Halftones

Compared to my last post, these halftones are a walk in the park. There is two parts behind halftones, the shape and how light masks the output. In unreal engine it is quite simple to get the light value in a post process material. Starting with 2 SceneTexture nodes, we will set one of them to PostProcessInput0 and the other to SceneTexture:BaseColor (for lighting). By dividing our Input0 by the base colour we have now extracted the lighting information in our scene. We then convert this to greyscale and clip/step our shape. This gives us a mask that we then blend between Input0 and black with.    


As games are made for all dimensions of monitors it is important that the shape we are sampling is the correct aspect ratio, by dividing the screen width by the screen height we get a ratio that we can use to square our screen UVs. This will eliminate any stretching we would otherwise see on non-square monitors.

Bokeh's (Stencils / Shapes)

With the way this shader is setup we can input whatever shape we like! I find that circles are still the nicest but that comes down to personal opinion and project art style. Below are a few examples of textures to input and their results. It is important that whichever image we use has a gradient from black to white. These were made using Affinity Photo and the "Outer Glow" layer effect.

The Making of Me, Myself & Die (part 2)

Making Of / 02 December 2020

To read the first part click here. You can also find the game on the Google Store and Itch.io

The brief I was given for our character initially was much like the Borg from Star Trek. A block of metal with light shining through the cracks, however we wanted minimal detail. I made a cube made from smaller cubes with the intent of having them animated to bring life to our character. This proved problematic for a few reasons, our first being it had way too many bones and our second issue being time limited was a lot of work to animate. After a few playtests our designers found players wanted the player speed to be faster, leaving less than 3/10ths of a second for an animation to play. This simply wasn't possible for animating meaning it was time to go back to concepting. I made a few sketches and got the best bits from each to make our second character, but we felt it had lost a lot of its liveliness. I went online and found how others had made their cubes and came across one particularly nice stylised stone looking cube. I got the approval of my team to work on it from here and made my own cube. In the end I wasn't happy with this one as it still wasn't as lively as our first and there is something off about it. However I was under a strict deadline for this character so we had to deal with what we had and continue bringing the rest of the game up. 

As part of the character brief was to have light coming from its centre I used a mask for its emission and had it pulsing from another animated material. Our dead character uses the same animation but with a new emission colour for its three states.  

I looked into a few ways to make decals within Unity such as how Unreal does it, mesh decals and finally sprites. I chose to use sprites as it was less performance impacting than the other two options and it saved us a bunch of time not having to make custom meshes. Using a shader I made we can choose how worn the sprites look, 3 colours based on a mask and where emission comes from on these sprites. All that  was left was making the sprites themselves. I looked into some hieroglyphs and made some rough silhouettes to be placed around the scenes. As part of this there were 12 unique glyphs and two tile-able ones to give variance among our 40 levels. Our tiling sprites use Unity's tiling sprite settings to allow us to scale these to any size. With this in combination of the other 12 images we were able to decorate the scenes in many unique ways. 

Our game makes use of post processing in a few ways. I looked into post processing and found that bloom can make scenes look "hot" as it has a radiance around the emissive parts. This not only made our levels look hot but also our emissive objects look like they were glowing properly. To draw our players eye to the centre of the screen I added a vignette. The final use of our post processing was for colour correction and checking our values. By converting it to greyscale we were able to see how easy it is to distinguish the scene from mechanics. 

When I was first implementing our post processing stack nothing was rendering. When using the lightweight render pipeline there are a few extra settings to enable before the stack will start rendering.

The lighting of our scene is quite simple, it uses one directional light which is baked into the scene. At the start of production we were going to use real time lighting to save on file size but after some research into ambient occlusion decided to bake it into the scene. When I first baked the lighting I expected the scene to start mostly the same but with some shading in the corners. This is not what happened, but instead we had a much better result. The scene became a lot more desaturated and "dry" feeling as well as having the ambient occlusion we wanted. Although there were a few errors these were easily fixed by enabling "generate lightmap UVs," increasing the Texel density for the lightmaps and finally increasing the padding between shells. This made our scenes look really nice at the expense of 12mb per scene, way too much for a mobile game aimed at less than 100mb. Enabling compression left banding across the scene which was also undesirable but I found that using crunch compression removed this at the expense of overall quality. As we are targeting mobile phones we figured it would be hard to see the pixel sized artefacts and went with it. Now we had got the lightmaps to 1mb per scene which was still a bit too much for us. Knowing that our camera never got closer or further from our scene I disabled mipmaps for our textures, reducing the file size to just over 200kb which was a much more manageable file size.

Although our lightmaps were compressed we still had a very large game for such a simple project. Our total download size was over 90mb and although this was in our budget, we thought we could do better. I went through each of our assets and changed their import settings to have no animations, rig or imported materials. I also removed blend shapes, visibility, cameras and lights from the mesh imports. From here the last setting we changed on the meshes was the compression type. 

After changing the models I addressed the textures. When we were making them we decided to start at a higher resolution so we could down-sample them later. Doing many of the same settings as the lightmaps I set our textures to have no mipmaps and high quality compression. As many of our textures were quite simple I was able to down-sample most of them to 128x128 textures. This reduced the size of the game a bit but we were still sitting around 80mb, there was something still keeping our file size up. After looking around our project a bit I discovered that we were using sprites for our level selection screen. When we were taking the screenshots we didn't take them at a power of two number meaning they couldn't be compressed in Unity. After using David's screenshot tool to retake the images at 2048x2048 I applied the same compression settings as before. After these settings were applied I managed to take our game from 90mb down to 30mb.

Our team was made up of 6 people, our project lead and designer Jason Marsh, Level designers Mateusz Solle and Dean Subtrizky, programming by David Flintoft and art by Sie McEachern and myself. Our team was assisted by two audio engineers Lachlan Willacy and Yan Parums.

You can find the game on the Google Store and Itch.io

The Making of Me, Myself & Die (part 1)

Making Of / 28 November 2020

Me, Myself & Die is an isometric puzzle game for android. You are a mystic alien cube making your way through an Egyptian tomb. Orchestrate your death to reach new areas.

You can find the game on the Google Store and Itch.io

To get the feeling of our game right we started off concepting. Looking at real world places we initially looked toward Mayan temples and its grey brick walls, foliage and water. We knew we were on the right track but some of our mechanics had some sci-fi and futuristic elements to them. We decided to swap over to the pyramids of Egypt as there is a common association of aliens building the pyramids. Using a basic level block out I drew over the meshes to get an idea of unique pieces we would need, decorative pieces and finally some of our mechanics. While doing this I was looking at our colour pallet to try and make our mechanics stand out while still feeling as part of the environment. 

As we were targeting a mobile audience we thought it was best to use the Lightweight Render Pipeline in Unity. The Lightweight Render Pipeline is a stripped version of Unity's standard render pipeline, giving only what the user wants and nothing more. As part of this it comes with Shader-graph, a visual coding to create our own materials. In our early stages I did a lot of research into how we can use shader graph to our advantage. We chose early on to use only diffuse textures as our game was quite simple and having roughness and normal maps would detract from that. 

In my research I also discovered a way to tile object textures by their scale. This shader meant we could use one mesh to build entire levels at the sacrifice of details. We thought this would be a good compromise as it meant the artists could focus on other more intricate models without bottlenecking our designers and their work. 

The tiling shader works by getting the object scale and converts the three dimensions down to two for our UVs. We use the normal vector of the object as a mask for how it tiles, this means the shader will work well for cubes and not really much else, but in our case its not a problem. Another thing we wanted from our shader was the control of how lighting influenced the scene. As our lighting contained one directional light we were able to use a simple bit of code to get its direction, attenuation (falloff) and colour. From here we converted it to cell shading and had a few sliders to control both the shadow falloff and highlight amount. Our final controls in our tiling shader allow for both a world space and local space gradient. We wanted to have a gradient covering our scene but to do it locally would mean a lot of custom meshes, on the other hand however if we had it all world space then some would have little to no gradient which was also undesirable. We had a switch to flip between each dependent on the material used which proved quite handy to add some depth to our scene.

Our second shader was for our water. Using the same maths as the tiling shader, I plugged this into the UVs of a Voronoi noise node. I also used time for the angle offset variable to have the material animate like water. from here it was a matter of adding it to our lighting and giving it some colour.

The third shader I worked on was one for our sprites. Using the same lighting we were able to have sprites act as if they were part of the scene, however there was still an issue with them blending with the surroundings. I used some noise to break up the uniformity of the sprites and make them look worn down and old. Another feature of this shader was that we wanted some hieroglyphs to glow. As we were already using the alpha channel of the diffuse as our transparency we needed to use a second texture. This texture is broken into individual channels where we can choose the colour of certain elements in engine as well as mask where we want emission and its colour.

Our particle systems didn't require any special shaders but we made one anyway so that we could have a little more control over the inputs. We used this shader to assist both the repulsion beam and laser. For our repulsion beam it had the transparency we needed and wasn't affected by lighting like our other shaders. For our laser I used simple noise and time to distort the UV's and give a semi-straight line some waviness. The last issue we had was with our repulsion beam. We are using a cylinder mesh for where the lines render, but the transparency means we see the back faces, thus rendering nothing. The final step was going to the unlit master node and allowing two sided material. Using the blue channel of the normal vector in view space we can figure out whether we see the back or front of a face. I used this as a mask to differentiate the inside and outside lines and give them variance. 


Our game makes use of a fully modular kit. Our walls are made from 4 variants of mesh, a basic flat wall that can be scaled to any size, a window piece and two walls with insets. I made our modular kit in mind of our tiling shader to allow our designers to quickly build scenes without needing to worry about anything other than the puzzle. The issue with this is that if we want anything like chips and cracks in the walls we need to come up with a new solution. Sie our other artist made 7 variants of cracks that we could lay over the  walls as mesh decals. Using an unlit material and colour matching to our background we could cut away pieces of wall. Our other cracks have the same material as our walls which allows them to blend in and receive lighting in a similar manner. Our final modular pieces were our leaves. Sie created some mesh variants for them and in engine created some prefabs. From here I placed these over our levels using the same technique as the cracks. 

Our game has a few mechanics that have very unique properties to them. Our laser needed to be scalable but the mesh could not be larger than one unit. Using a line renderer we were able to scale the length of the laser but this was unique scene to scene. Our programmer David used a ray cast to determine the length of the laser at runtime. From here was a few particles to add some variance and feel more impactful. 

The repulsion beam was much like the laser but this time we needed a cylindrical mesh. With the pivot point at the start of the beam we scaled the repulsion beam to its desired length. The final touch was adding small flares at the end of the beam to give it directionality and feel a little more part of the world. The flares use an animated material rotating at a constant speed relative to the repulsion beam.

For the longest time I struggled to make our end platform and static hazard look good in both our scene as well as on a mobile device. There was issues with theming and screen resolution but in the end I'm happy with how it turned out. From my initial concept we wanted either a fire or spikes as our static hazard. As a team we decided spikes was the way to go, but we quickly found out that these were hard to read on the screen regardless of colours or silhouette due to our working resolution. From here we started concepting some more ideas and stumbled across an icon that looked like a sun. I mocked it up in photoshop to see how it would look and was happy with the outcome. From here I made my own concepts for the sprite and finally placed it in engine. 

Having the same visibility issues for our end and start platform we used the same technique as the static hazard. Sie worked on a few iterations and got the final product in engine. 


To continue reading click here and to find the game on the Google Store and Itch.io.

Interior mapping and building tile-able rooms - UE4

Tutorial / 13 July 2020

Contents

  • Creating a cube map
  • Creating a basic material for interior mapping
  • Tiling Rooms by object scale and randomised light values
  • Pixel depth offset
  • Adding windows and curtains
  • Applying the material and uses
  • Shader complexity
  • Limitations
  • Articles and resources

Creating a cube map

To make interior mapping is a simple process in unreal engine that revolves around distorting a texture; in particular a cube-map. To make our cube map we can place a scene capture cube inside 6 textured planes. We can then detail our little room with other objects and furniture, but be careful with prop placement, angle and distance from the walls as some shapes can distort incorrectly. 

Creating a material for interior mapping

For a very simple room we can simply have a texture distorted by the interior cube map node. If we have many rooms we may want to randomise the rotation which can be done by adding a static boolean into the randomise rotation input. 


Tiling rooms by object scale and randomised light values

A super handy trick to quickly make an apartment building is to tile our texture by the scale of the object. To do this we first need to convert our 3-dimensional coordinates into 2 dimensional. 


From here we can plug our vertex interpolator into the UV's of our cube-map. This on its own is enough to make an apartment building but its not fully believable as not all lights will be on or off at the same time. To fix this we can take our same UV co-ordinates and plug them into a Sobol noise node. This creates a grid of cells with values between 0 and 1 which we can then multiply and combine together to give rooms of varying light levels.

Pixel depth offset

Pixel depth offset is the act of changing the render order on a per pixel level by "pushing" selected pixels a certain distance "behind" other objects. In our case we can use it to place objects into our rooms. To achieve this we can take our UV's from our albedo cube-map and plug them into a black-to-white map. From here we are multiplying our pixel depth value by black (0) to white (1), thereby creating a mask that hides objects close to the window edges but fully encapsulates the objects in the centre. 


Adding windows and curtains

There are many ways of adding window frames to our rooms. For a very detailed window we could have a mesh placed over the top, another route which i chose was to layer a few textures on top of each other. By using linear interpolators (Lerps) we can have our cube-map as our base layer and then layer curtains and window frames over the top. This uses black where we want our cubemap and white where we don't. Anywhere we have grey is a blend of both which is nice for achieving things like transparent curtains. For my curtains and windows I wanted a few variations, on one texture i had 4 curtains and 4 window frames that i could shift the UV's on to swap out which pairing I had. 


Applying the material and uses

Using the material is quite simple, we can use any cube or plane and scale it to whatever size we choose. To place objects further back into the room we simply adjust the pixel depth offset and to place them off to the side we can change our mask.  



Shader complexity

Overall the shader isn't too complicated, we could have many of these in scene before seeing a huge performance impact and for the amount of textures and materials it could save using and the time it saves making huge buildings is well worth having in large city projects. 


Limitations

This shader is great but comes with a couple limitations. Rooms can become quite repetitive on large scale projects and creating rooms is limited to time and reducing distorted objects. 


Articles and resources

https://forums.unrealengine.com/unreal-engine/events/1670169-unreal-engine-livestream-tech-art-with-chris-murphy-october-3-live-from-hq 

https://www.youtube.com/watch?v=dUjNoIxQXAA 

https://80.lv/articles/interior-mapping-rendering-real-rooms-without-geometry/ 

https://www.gamasutra.com/view/news/332409/Game_Tech_Deep_Dive_A_window_into_Playground_Games_latest_shader_development.php 

Replacing block-out meshes, texturing and updates - Small environment creation Part 2

General / 04 June 2020

This is just a short progression update on the little restaurant I've been working on. some of the assets are still the block-out pieces and some have had very basic textures placed on them and will be revised later. 

Interior mapping: 

This window doesn't seem all that impressive, but whats happening here is that a room is being projected on a plane with overlaid curtains and window frames. For a little more variation I can swap out the room, the curtain texture and the window frame and using pixel depth offset I can place objects in the rooms. 


Asset updates:



  

Anatomy studies

General / 16 May 2020

Over the past couple weeks I've been studying some anatomy and sculpting with Zbrush. These were a few of the works that I made in those weeks. 





My pipeline for the creation of a small environment - Part 1

Work In Progress / 16 May 2020

Before we get started i would like to say that this is my approach to breaking down and rebuilding an environment, there are many ways of doing this and this is by no means a guide or how to make something like this. In this post I will go over my process from gathering reference, blocking out, lighting and creating a basic material to get the general feeling for the scene before any major modelling begins. 


As with all projects they all begin with an idea, but the idea is only as good as the research put into it. To start off with this project i looked at everything from New York alley ways and have slowly progressed to more of a southern European feeling scene. Small condensed buildings in tight quarters with paths and streets large enough for the exact traffic flow they need with no more or less space. Then narrowing down my search field further to specific objects, foliage and architecture. 

Once i was happy with the general idea i progressed to the block-out stages of my project. This stage helps to define the general aesthetic of the scene and helps to narrow down an asset list. Using primitive shapes I was able to quickly build a small environment and figure out all the major pieces that are necessary for the environment to be a whole, this of course doesn't cover every asset in detail and there are going to be some that get missed. This is why research is important and looking at reference throughout the creation of the scene will help bring realism to it. Another part to the block-out stage is to help define the silhouette and points of interest with breaks of flat, large shapes with smaller shapes breaking up the surface.


After importing my block-out meshes to Unreal Engine 4 I can quickly reconstruct my environment for a very basic lighting pass. The lighting doesn't need to be perfect in any way, it just has to be good enough that we can get a general understanding of where the scene is headed. This pass in the creation of the scene can further help develop object placement and will further guide where the points of interest will be at the end of production. Furthermore to make this final stage of lighting really shine through i like to give all assets a flat grey material as it helps show problematic areas that could otherwise be hidden in the default checker pattern. 



A guide to Screen-Space Halftones, Cel Shading and Toon Shading for post processing in Unreal Engine 4.20.3

Tutorial / 20 February 2020

!! Important !!

This tutorial has been revised in a new post here. If you are after Toon or Cell shading continue reading here.

Contents

  • Post processing materials for UE4
  • Introduction to Cel shading
  • Cel Shading in UE4
  • Introduction to Toon shading
  • Toon Shading in UE4
  • Introduction to Halftones
  • Halftones in UE4
  • Dependencies for Halftones
  • Known issues
  • Links and resources used

Post Processing Material Setup for UE4



In Unreal Engine we have access to post processing, it allows us to adjust the look of our game through certain elements like bloom, vignette and more. We will be using the post processing volume to recalculate shading. To create a new post processing volume, under the modes window find the visual effects tab and drag in the post processing volume into your scene. The settings I have used for the volume are as follows: 

Exposure min AND max brightness: 1.0

Infinite Extent (unbound): True (ticked), this setting is not necessary for the shader to work as the post process volume only influences cameras within it. With this setting ticked it acts as if you scaled the volume up. 

Introduction to Cel Shading

So what is Cel Shading? Its a type of rendering used to imitate the looks of comic books or cartoons. Unlike conventional lighting with smooth gradients, Cel shading has hard edges and a step gradient for displaying light. To make Cel shading you can use two methods: Look up tables (Luts) or you can use maths to iterate the steps in gradient. For this tutorial i will cover only how the second method works. 


Cel Shading for UE4

Cel shading for Unreal Engine is quite simple. The method I have chose involves using a value instead of a look up table for a little more modularity. This works by taking the scene colour including lighting (post process input 0) and desaturates the scene. We then multiply the output by the amount of Cels we want in our lighting, reduce it to a whole number, and then divide back to the 0-1 range. Finally we can take this output and add it back to out base colour to reintroduce lighting for our scene. I have included a 3 vector which has a default of white, with a purpose of colour correction if the scene happens to need it at any point. The last step to the graph is to bring back ambient occlusion by multiplying it by our previous results. 


Introduction to Toon Shading

Toon shading is very similar to Cel shading with the only difference being added outlines that help to define the shapes of your objects. For our Toon shader we will use what we made in Cel Shading, then add detection for both outlines and normals.


Toon Shading for UE4

 In Unreal Engine the process of calculating the outlines is to find the scene depth of the object and compare it to the background depth, at this particular point it will find the edge of the objects but nothing will be drawn on the screen. We can further determine the thickness of what we want drawn and finally our bias of what is drawn through this method: 


The node network to create outlines is great for lines on the outer sides of objects, however it fails to draw any lines inside the object. To compensate for this we can duplicate our network for the outlines and convert all the scene textures from scene depth to world normal. For normals it is important to add an "addComponents" node and connect the output of the outlines into the float 3 input, then output from the float 3. From here we can take the maximum value of both graphs and use this as an alpha for a linear interpolation (lerp). For our lerp our alpha is our outlines, our B is the line colour and A will use what we made from Cel Shading.


Of course the outlines are now rendering all objects in scene, including our sky box. This is undesirable as the entire sky box becomes a grid of lines. To fix this we can make a culling mask to stop the lines from rendering beyond a certain threshold.

Introduction to Halftones

What are Halftones? Halftones are a method of printing that creates a gradient by the use of dots with various size or spacing. From a certain distance the dots that make up images become blurred with each other and create a pleasing image to look at, while being efficient at saving ink. 

We can replicate this in Unreal Engine with a few methods, the first is a per-texture instance, this is not a dynamic solution and shading will be static across the objects. The second method uses post processing to overlay a pattern across the screen. This allows the camera to move and the dots to follow, but can be effected by the objects, their materials and the lighting on them, making for a fully dynamic solution. 



This is a small scene made by Dris Hunt ( https://twitter.com/DrisHunt ) using assets from the Unreal store: Soul: Cave, Medieval Dungeon, Paragon: Revenant and Infinity blade. To find out more there are links at the end. Special thanks to Dris for helping find bugs, bug fix and create this shader.



Halftones for UE4

Using the components from both Toon shading and Cel shading we can develop a halftone shader. To achieve this I have used the values of light as steps in radius size for a spherical mask component. The input A is the position on screen for where the dots are placed and finally input B is the centre of where that dot is. The last input hardness does not need to be changed and can be left at 100, this simply changes the falloff or gradient for the dots. A small adjustment i made to Cel shading means that the shadows will be fully black, this was done by taking the Floor node and multiplying it to the base colour, then adding back in the cel shading afterwards.



From here we can use the output of the sphere mask as an alpha and reintroduce it into our graph. If we want the dot colour to be changed we can use a Lerp with input A as our output of cel shading. Input B will be a colour parameter which allows us to change the colour to whatever we like. As an optional step we can introduce the scene light and multiply it by our new dot colour parameter to bring in some light colours in the scene. This optional step can be a little problematic in some versions of unreal engine where the scene being divided outputs undesirable results.



The final step is to connect out last Lerp node into our outline lerp. Depending on where you would like halftones to influence you can connect either the line colour Lerp, the dot Lerp or the output of the Cel Shader into the A>B, A==B and A<B Statements of the render mask. 


Dependencies for Halftones

There are a few dependencies for this shader, the first being materials. They are allowed to be as complicated as you like with only one exception, for the halftones to work correctly the RGB Channels must contain a minimum of 0.05 if you are to have any particular colour. For example if i wanted Red i would do R: 1, G: 0.05 and B: 0.05. However doing blacks and anything grey can be done regularly. The cause of this is due to taking out the scene colour through the cel shader, then multiplying the red and green channels of the screen UVs by the scene colour, thereby influencing the halftones.

Can i do metal and roughness? Metallic and Roughness are interesting, yes you may do both but be weary with metal as it reacts incorrectly.

Another interesting effect to play with is emissive. It reacts to the halftones on the emissive object, but not the areas surrounding it, furthermore the emissive colour is based on the diffuse of the surface rather than the emissive colour. 

The final dependency for the shader is translucency. Any material like glass can be rendered, but is dependent on static or movable lights. With baked lighting the shading behind glass becomes completely black when in shadow. Otherwise in the same scenario with movable lighting the shading is as expected.

Known issues

There have been a few issues noticed with the shader. The first one is how shader complexity increases due to transulcency. This reduces the amount of glass able to be used within the scene. 

The second issue is due to how light is extracted for the dot colours, this can cause shaded areas to appear the wrong colour and does similarly to some materials and colours.

The third issue is how the halftones are calculated from the cel shading. The cel shading comes in RGB and the red and green channels influence both spacing and size of the dots, not fully desirable but it can be worked around. 

The final known issue is with alphas and complexity. As always when overlapping many alphas the shader has more to compute, increasing the overall performance impact. 

Links and resources used

http://blog.theoroy.com/2017/04/27/anime-look-cel-shading-in-ue4/ 

https://www.raywenderlich.com/146-unreal-engine-4-cel-shading-tutorial

https://www.raywenderlich.com/92-unreal-engine-4-toon-outlines-tutorial  

https://www.youtube.com/watch?v=0UBNXneL1oo Underscore's youtube Channel

https://www.youtube.com/watch?v=cQw1CL0xYBE Offical Unreal Engine Youtube

https://forums.unrealengine.com/development-discussion/rendering/42507-lighting-in-post-process-material

Unreal Assets: 

https://www.unrealengine.com/marketplace/en-US/product/soul-cave  

https://www.unrealengine.com/marketplace/en-US/product/a5b6a73fea5340bda9b8ac33d877c9e2 

https://www.unrealengine.com/marketplace/en-US/product/paragon-revenant 

https://www.unrealengine.com/en-US/blog/free-infinity-blade-collection-marketplace-release