Long Châu
Posted on June 10, 2024
Last year, I developed a new simulation game called My Animal Mart. It’s a fantastic game, and I’ve learned a lot from the experience.
Foreword
The reason we decided to create this game was because we needed to generate more revenue. One day, my PO called the team together and said we had to make a new move. After spending a year working on hyper casual games such as Popit Fidget 3D, Kick the Rainbow Friend, and Save the Dog, we decided to venture into the simulation genre.
The Background Story
We took inspiration from My Mini Mart. At first glance, there weren’t any games quite like it, especially with its unique outline shader. Some competitors didn’t use this technique, likely due to performance concerns.
The Technique I Used
At that time, we used Quibli for outline and cartoon shading. Writing an outline shader isn’t particularly difficult; it typically involves two passes:
Pass
{
Name "ForwardLit"
Tags
{
"LightMode" = "UniversalForwardOnly"
}
// ... shader code ...
}
Pass
{
Name "Outline"
Tags
{
//"LightMode" = "SRPDefaultUnlit"
"LightMode"="Outline"
}
Cull Front
// ... shader code ...
}
As you can see, it’s a multi-pass shader that Unity cannot batch. For instance, if it renders 10 apples, it will first render each apple individually, then render the outline for each apple, resulting in 20 draw calls.
Here’s the detailed draw call in the frame debugger:
The game had a GPU bottleneck. With the release date approaching, the optimization task was handed to me. Since we were using URP, I knew we could customize the render pipeline. I decided to try using a Rendering Feature. I created one and called it RenderOutlineFeature
.
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class RenderOutlineFeature : ScriptableRendererFeature
{
private RenderOutlinePass renderOutlinePass;
public Setting featureSetting = new Setting();
[System.Serializable]
public class Setting
{
public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(renderOutlinePass);
}
public override void Create()
{
renderOutlinePass = new RenderOutlinePass();
renderOutlinePass.renderPassEvent = featureSetting.renderPassEvent;
}
class RenderOutlinePass : ScriptableRenderPass
{
ShaderTagId outlineTag = new ShaderTagId("Outline");
FilteringSettings filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
DrawingSettings drawingSettings = CreateDrawingSettings(outlineTag, ref renderingData, SortingCriteria.OptimizeStateChanges);
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings);
}
}
}
We then went to the Forward Renderer, selected Add Renderer Feature, and chose Render Outline Feature.
After applying the Render Outline Feature, we achieved efficient Unity batching.
Result
I was thrilled to solve this issue. The GPU bottleneck was eliminated.
Side Story
I contacted Quibli's publisher, DustyRoom, to ask if there was any way to optimize the shader.
Unfortunately, Unity does not support multipass batching (as I knew).
I tried using an outline image effect, but it worsened the game’s performance.
After finding the solution, I shared my idea with them.
They agreed with what I had done.
Unity Version: 2021.3.11f1
Link to the game: My Animal Mart
Posted on June 10, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.