Deferred Rendering Pipeline and Custom Post-Processing
Unturned uses Unity's built-in deferred rendering path with a custom post-processing stack built on UnityEngine.Rendering.PostProcessing (the legacy post-processing stack, not the URP/RenderGraph system). The custom stack adds SkyFog volumetric fog, single-render scope overlays, Gaussian blur, scope vignette, per-frame GL overlay rendering for the editor and runtime gizmos, and a dedicated material utility system for wireframe and debug rendering.
This article documents the GLRenderer and GLUtility overlay system, the UnturnedPostProcess integration point, the SkyFog volumetric shader integration with the water volume system, the SrScope (single-render scope) implementation including its Gaussian blur pipeline and vignette alpha compositing, the custom post-processing effect registration, and the overall rendering pipeline architecture.
Source code location: CustomPostProcess/*, Framework/Rendering/*, Unturned/Managers/UnturnedPostProcess.cs
Rendering Pipeline Overview
Unturned's rendering pipeline has three layers that compose into the final frame:
Layer 1: Unity Deferred Rendering
— Unity's built-in deferred shading path
— G-buffer with normals, specular, albedo
— Forward rendering for transparent objects
— SSAO (ambient occlusion) via Unity's post-processing stack
Layer 2: Custom Post-Processing Stack
— Unity.PostProcessing v2 effects
— SkyFog (volumetric fog with water integration)
— SrScope (scope overlay with blur + vignette)
— Registered with PostProcessEvent.AfterStack for scope
Layer 3: GL Overlay (Editor and Debug)
— GLRenderer.OnRenderImage hook
— GLUtility immediate-mode line and triangle rendering
— RuntimeGizmos for debug visualization
— Active in editor and optionally in-gameGLRenderer — The GL Overlay System
The GLRenderer MonoBehaviour is attached to the main camera and intercepts the OnRenderImage event, which Unity calls after all rendering is complete. The GLRenderer does not modify the rendered image — it calls Graphics.Blit(source, destination) immediately to preserve the frame — and then optionally renders GL overlays on top.
csharp
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
// Must always call blit first
Graphics.Blit(source, destination);
bool shouldRenderAny = false;
bool shouldInvokeRenderEvent = false; // Editor render
bool shouldInvokeGameRenderEvent = false; // Game render
bool shouldRenderGizmos = false;
if (Level.isEditor) { ... }
else { ... }
if (shouldRenderAny)
{
RenderTexture.active = destination;
// Push GL matrix, invoke delegates, pop GL matrix
}
}The renderer distinguishes two modes:
- Editor mode: fires the
renderevent when the editor is active and the editor UI window is enabled - Game mode: fires the
OnGameRenderevent when the player UI is active
Both modes also render RuntimeGizmos if any gizmo elements are queued. The GL matrix is wrapped in GL.PushMatrix() / GL.PopMatrix() to isolate overlay rendering from Unity's internal state.
GLUtility — Immediate-Mode Drawing Materials
GLUtility provides static materials and utility methods for immediate-mode GL rendering. It is the rendering backend for the editor's wireframe visualization, spawn point markers, volume boundaries, and debug overlays.
GL Materials
The utility initializes six line materials and six triangle materials on demand (lazy initialization). Each pair differs in depth testing and checkering behavior:
| Material | Purpose |
|---|---|
LINE_FLAT_COLOR | Simple colored lines, no depth test |
LINE_CHECKERED_COLOR | Checkered (stippled) lines for selected objects |
LINE_DEPTH_CHECKERED_COLOR | Depth-tested checkered lines |
LINE_CHECKERED_DEPTH_CUTOFF_COLOR | Checkered with configurable depth cutoff |
LINE_DEPTH_CUTOFF_COLOR | Flat color with depth cutoff |
TRI_FLAT_COLOR | Solid triangles |
TRI_CHECKERED_COLOR | Stippled triangles |
TRI_DEPTH_CHECKERED_COLOR | Depth-tested stippled triangles |
TRI_CHECKERED_DEPTH_CUTOFF_COLOR | Stippled with depth cutoff |
TRI_DEPTH_CUTOFF_COLOR | Solid with depth cutoff |
Each material is created from a shader in the GL/ shader namespace (e.g., GL/LineFlatColor, GL/TriCheckeredColor). Materials are only created on the client — Dedicator.IsDedicatedServer guard prevents material creation on headless server builds.
Drawing Primitives
The utility provides methods that emit vertices directly into GL immediate-mode:
line(Vector3 begin, Vector3 end)— Emits two vertices multiplied by the current matrixboxSolid(Vector3 center, Vector3 size)— Emits 36 vertices (6 faces × 2 triangles × 3 vertices) forming a solid boxcircle(Vector3 center, float radius, Vector3 horizontalAxis, Vector3 verticalAxis)— Emits line segments approximating a circle, with adaptive segment count based on radius (Clamp(4 * radius, 8, 128))circle(..., GLCircleOffsetHandler)— Variant with a per-vertex callback for deforming the circle (used for projecting onto surfaces)
All vertices are transformed through the static matrix field, which allows callers to set a custom transformation matrix for coordinate system conversions.
Custom Post-Processing Effects
Unturned registers custom post-processing effects through Unity's PostProcessEffectSettings / PostProcessEffectRenderer pattern. The effects are applied after the deferred shading pass and before the final image output.
Effect Registration
Each effect uses the [PostProcess] attribute to register with the post-processing stack:
| Effect | Event | Renderer | Purpose |
|---|---|---|---|
SkyFog | BeforeTransparent | SkyFogRenderer | Volumetric fog with water color integration |
SrScope | AfterStack | SrScopeRenderer | Scope overlay with Gaussian blur |
SkyFog — Volumetric Fog
The SkyFog effect renders a fullscreen gradient that blends fog color, sky color, equator color, and ground color from the skybox material. It also integrates with the WaterVolume system to apply underwater visual effects when the camera is submerged.
Shader uniforms set per frame:
| Uniform | Source | Purpose |
|---|---|---|
_FogColor | RenderSettings.fogColor | Base fog tint |
_SkyColor | Skybox material _SkyColor | Upper hemisphere color |
_EquatorColor | Skybox material _EquatorColor | Horizon band color |
_GroundColor | Skybox material _GroundColor | Lower hemisphere color |
_InverseProjectionMatrix | Camera's projection matrix inverse | Ray reconstruction for world-space fog |
_CameraToWorld | Camera's world matrix | Camera position for fog depth |
_WaterColor | LevelLighting.getSeaColor("_BaseColor") | Water tint when underwater |
_IsCameraUnderwater | Boolean (1.0 or 0.0) | Enables underwater fog blending |
_WaterCount | Number of relevant water volumes | 0–3, determines underwater effects |
_WaterMatrices | World-to-local matrices for water volumes | Per-volume distance calculation |
Water volume relevance: The effect finds up to three WaterVolume instances closest to the camera position within a 2-meter radius (sqrMagnitude < 4.0). If more than 3 water volumes exist within range, they are sorted by distance and only the nearest 3 are used. The MAX_WATER_COUNT hard limit prevents shader array overflow.
The underwater effect is only enabled when LevelLighting.enableUnderwaterEffects is true and LevelLighting.isSea is true and at least one water volume is within range. If any condition fails, the effect renders only the above-water fog gradient.
SrScope — Single-Render Scope
The SrScope effect implements Unturned's scope overlay system. It replaces the legacy "dual-render" scope (which rendered the scene a second time with a zoomed camera) with a single-render approach that blits the middle square of the player's view into the scope's render target, then applies a peripheral darkening or blur effect depending on the player's graphics settings.
Scope pipeline:
SrScope.Render()
│
├─ 1. Blit source → renderTarget (square crop)
│ └─ Scale/offset to maintain aspect ratio:
│ smallAxis = min(width, height)
│ scale[bigAxis] = smallAxis / bigAxis
│ offset[bigAxis] = (scale - 1) * -0.5
│
├─ 2. Apply peripheral effect:
│ │
│ ├─ If DarkScopePeripheral:
│ │ └─ ScopeVignette shader (alpha-based darkening)
│ │
│ └─ Else (blur scope):
│ └─ GaussianBlur shader
│ ├─ Horizontal pass (pass 0)
│ └─ Vertical pass (pass 1)
│
└─ 3. If no effect applied (scopeAlpha ≈ 0, stdDeviation ≈ 0):
└─ Simple Blit(source, destination)
(Prevents black frame flicker with TAA disabled)Aspect ratio handling: The render target for scopes is square (equal width and height). The initial blit from the source render texture to the square render target scales the longer axis down so the full vertical/horizontal extent is visible within the square. For a 1920×1080 screen (16:9), the horizontal extent is scaled by 1080/1920 = 0.5625, centered horizontally.
Resolution scaling: The Gaussian blur kernel size scales with screen resolution. The baseline resolution is 1080 pixels on the small axis. For a 4K screen (2160 small axis), the blur scale multiplier is 2160/1080 = 2.0. This prevents higher-resolution displays from appearing less blurry due to finer pixel density.
csharp
float blurScale = screenSize[smallAxis] / 1080f;
float stdDeviation = settings.standardDeviation * blurScale;
float sqrStdDeviation = stdDeviation * stdDeviation;
int halfKernelSize = CeilToInt(stdDeviation * 3.0f);The half-kernel size is 3 standard deviations, which captures 99.7% of the Gaussian distribution.
Vignette peripheral mode: When WantsDarkScopePeripheral is enabled and scopeAlpha > 0.001, the ScopeVignette shader composites a dark circular vignette over the peripheral area of the scope image. The scopeAlpha parameter controls the opacity of the darkening.
UnturnedPostProcess
The UnturnedPostProcess class is the game-level integration point for the custom post-processing stack. It is created in Setup.postProcess and initialized in Setup.Start(). The class configures the post-processing volume component on the main camera, registers custom effects, and manages the day/night and weather-dependent rendering parameters.
Shader Assets
The custom post-processing system uses these shader files:
| Shader file | Hidden name | Used by |
|---|---|---|
SkyFog.shader | Hidden/Custom/SkyFog | SkyFog renderer |
SrScope.cs | (C# effect class) | SrScope effect |
GaussianBlur.shader | Hidden/Custom/GaussianBlur | SrScope blur passes |
ScopeVignette.shader | Hidden/Custom/ScopeVignette | SrScope vignette peripheral |
The shaders follow the standard Unity post-processing effect structure with #include for common post-processing utilities.
Deferred Rendering Path Configuration
Unturned configures Unity's rendering as follows:
| Setting | Value |
|---|---|
| Rendering path | Deferred |
| MSAA | Off (post-process AA used instead) |
| HDR | Enabled |
| Depth buffer | 24-bit with stencil |
| Shadow resolution | Configurable per quality level |
| SSAO | Via post-processing stack |
The deferred G-buffer stores the standard Unity deferred data:
- RT0: Albedo (RGB) + specular mask (A)
- RT1: World-space normals (RGB) + occlusion (A)
- RT2: Material parameters (smoothness, metallic, specular)
- Depth-stencil: 24-bit depth + 8-bit stencil
Night Vision and Post-Process Integration
The ItemGlassesAsset interacts with the post-processing system through the ELightingVision enum:
| Vision type | Post-process effect |
|---|---|
NONE | No effect |
HEADLAMP | Player spot light config; no post-process filter |
CIVILIAN | Grayscale filter with configurable color and fog intensity |
MILITARY | Green-tinted filter with configurable color and fog intensity |
The glasses asset sets nightvisionColor and nightvisionFogIntensity which are consumed by the post-processing stack to render the night vision overlay. These are separate from the SkyFog and scope systems.
Rendering Flow Diagram
Unity Camera.Render()
│
├─ Shadow maps rendering
│
├─ Deferred G-buffer pass
│ ├─ RT0: Albedo + Specular
│ ├─ RT1: Normals + Occlusion
│ └─ Depth-stencil
│
├─ Deferred lighting pass
│ ├─ Directional light (sun/moon)
│ ├─ Point/spot lights (with LightLOD culling)
│ └─ Ambient occlusion (SSAO)
│
├─ Forward rendering pass
│ ├─ Transparent geometry
│ ├─ Water surfaces
│ └─ Weather particles
│
└─ Post-processing stack
│
├─ BeforeTransparent effects
│ └─ SkyFog (fog color, water integration)
│
├─ Unity built-in effects
│ ├─ SSAO
│ ├─ Bloom
│ ├─ Tonemapping
│ └─ Anti-aliasing
│
├─ AfterStack effects
│ └─ SrScope (scope overlay)
│
└─ OnRenderImage (GL overlay)
├─ GLRenderer.render (editor)
├─ GLRenderer.OnGameRender (game)
└─ RuntimeGizmos.Render (debug)GLUtility Matrix Transform System
All GLUtility drawing primitives transform vertices through the static matrix field:
csharp
public static Matrix4x4 matrix;
public static void line(Vector3 begin, Vector3 end)
{
GL.Vertex(matrix.MultiplyPoint3x4(begin));
GL.Vertex(matrix.MultiplyPoint3x4(end));
}Callers set matrix before drawing to define the coordinate system. For example, editor code sets the matrix to the current camera's view-projection matrix to render overlay geometry in world space. The utility does not manage matrix state — callers must push/pop the matrix themselves.
Box Rendering Optimization
The boxSolid method emits 36 vertices (6 faces × 2 triangles × 3 vertices per triangle) to render a solid axis-aligned box. The six faces are rendered in the order -x, +x, -y, +y, -z, +z, with each face emitting two counterclockwise triangles. This ordering ensures correct backface culling regardless of viewing angle.
Circle Adaptive Segmentation
The circle methods use an adaptive segment count based on the radius:
csharp
steps = Mathf.Clamp(4 * radius, 8, 128);A radius of 1 generates 8 segments (octagon). A radius of 10 generates 40 segments. A radius of 32 generates 128 segments. This adaptive approach ensures that small circles (used for spawn point markers) are rendered with minimal geometry while large circles (used for safe zone boundaries) are rendered smoothly.
The GLCircleOffsetHandler delegate variant allows deformation of the circle per-vertex, used for projecting the circle onto uneven terrain surfaces.
Shader Configuration for Custom Effects
The SkyFog and GaussianBlur shaders follow the Hidden/ shader naming convention, meaning they are not listed in the Unity shader dropdown menu and can only be used through script references. The shaders are found at runtime via Shader.Find("Hidden/Custom/...").
SkyFog Shader Inputs
The SkyFog shader receives:
| Uniform | Resolution | Coordinate space |
|---|---|---|
_FogColor | RGB linear | Color |
_SkyColor | RGB linear | Color |
_EquatorColor | RGB linear | Color |
_GroundColor | RGB linear | Color |
_InverseProjectionMatrix | 4×4 | Matrix |
_CameraToWorld | 4×4 | Matrix |
_WaterColor | RGB linear | Color |
_IsCameraUnderwater | float (0 or 1) | Boolean |
_WaterCount | int | Integer (0-3) |
_WaterMatrices | 4×4[3] | Matrix array |
The inverse projection matrix is used to reconstruct world-space positions from each pixel's screen position and depth buffer value. The camera-to-world matrix provides the camera's world-space position and orientation. Together, they allow the shader to compute a ray from the camera through each pixel and apply the fog color based on distance.
GaussianBlur Shader Passes
The Gaussian blur shader has two passes:
| Pass | Direction | Kernel |
|---|---|---|
| 0 | Horizontal | 1D Gaussian kernel along X axis |
| 1 | Vertical | 1D Gaussian kernel along Y axis |
The two-pass approach reduces the computational cost from O(n²) for a 2D kernel to O(2n) for two 1D passes. The kernel size is halfKernelSize × 2 + 1 and is derived from the standard deviation:
kernelSize = ceil(stdDeviation × 3.0) × 2 + 1At 3σ, the kernel covers 99.7% of the Gaussian distribution. With the resolution scaling applied at 4K (blurScale = 2.0), the kernel is approximately twice as large as at 1080p, producing the same perceived blurriness.
Water Volume Distance Calculation
The SkyFog water integration uses the GetClosestWorldPosition method on each WaterVolume to find the nearest point on the volume's surface to the camera. The squared distance is used for sorting:
csharp
Vector3 closestPoint = volume.GetClosestWorldPosition(viewPosition);
float sqrDistance = (viewPosition - closestPoint).sqrMagnitude;Volumes within 2 meters (sqrt(4.0)) of the camera are considered relevant. The VolumeAlphaPair<WaterVolume> struct pairs each volume with its squared distance, used for sorting. The nearest volume has the highest alpha influence on the underwater fog color.
Editor vs Runtime GL Rendering
The GLRenderer distinguishes between editor and runtime rendering with two separate events:
| Event | When fired | Typical subscribers |
|---|---|---|
render | Level editor active, editor UI enabled | Devkit wireframes, selection outlines, volume boundaries |
OnGameRender | Game mode active, player UI enabled | In-game debug overlays, custom plugin rendering |
Both events check that the corresponding UI window is active before firing. If the UI is hidden (e.g., paused game, minimized editor), the GL overlay is suppressed to prevent rendering UI elements that are not visible.
Appendix A: Shader File Reference
| Shader file | Namespace | Path in SDK | Dependencies |
|---|---|---|---|
SkyFog.shader | Custom/SkyFog | CustomPostProcess/SkyFog.shader | Unity PostProcessing v2 |
GaussianBlur.shader | Custom/GaussianBlur | CustomPostProcess/GaussianBlur.shader | None (standalone) |
ScopeVignette.shader | Custom/ScopeVignette | CustomPostProcess/ScopeVignette.shader | None (standalone) |
LineFlatColor.shader | GL/LineFlatColor | (in GL shader collection) | None |
TriFlatColor.shader | GL/TriFlatColor | (in GL shader collection) | None |
Appendix B: GLUtility Lazy Material Initialization
Each material in GLUtility is initialized on first access through a lazy singleton pattern:
csharp
protected static Material _LINE_FLAT_COLOR;
public static Material LINE_FLAT_COLOR
{
get
{
if (_LINE_FLAT_COLOR == null && !Dedicator.IsDedicatedServer)
{
_LINE_FLAT_COLOR = new Material(Shader.Find("GL/LineFlatColor"));
}
return _LINE_FLAT_COLOR;
}
}The Dedicator.IsDedicatedServer check prevents material creation on headless builds, where no rendering occurs. Materials are cached in static fields and never destroyed during the game session — they persist for the lifetime of the AppDomain.
Appendix C: Post-Processing Effect Registration Details
The [PostProcess] attribute on each effect class controls its position in the render pipeline:
csharp
[PostProcess(typeof(SkyFogRenderer), PostProcessEvent.BeforeTransparent, "Custom/SkyFog")]The PostProcessEvent values used:
| Value | Position in pipeline | Used by |
|---|---|---|
BeforeTransparent | After opaque deferred pass, before transparent forward pass | SkyFog |
AfterStack | After all other post-processing, before final output | SrScope |
The BeforeTransparent placement is critical for SkyFog: the fog color is applied to the opaque scene before transparent geometry (water, particles) is rendered, so transparent objects composite correctly over the fog. The AfterStack placement for SrScope ensures the scope overlay is the last thing drawn, directly composited onto the final post-processed frame.
Water Interaction System
The SkyFog underwater effect interacts with the water volume system through a distance-based relevance calculation. The WaterVolume class defines a 3D volume zone:
csharp
public class WaterVolume : VolumeBase
{
// Inherits transform, collider, GameObject from VolumeBase
// Provides GetClosestWorldPosition(Vector3)
}The FindRelevantWaterVolumes method in SkyFogRenderer iterates all water volumes, finds those within 2 meters of the camera, and sorts them by distance. The three nearest volumes contribute to the underwater visual effect.
Underwater Effect Pipeline
When the camera is underwater:
LevelLighting.isSeamust betrue(the level has ocean water)LevelLighting.enableUnderwaterEffectsmust betrue- At least one water volume must be within 2 meters of the camera
- The
_IsCameraUnderwatershader uniform is set to1.0 - The water color is read from
LevelLighting.getSeaColor("_BaseColor") - Up to three water volume world-to-local matrices are passed to the shader
When any condition fails, the underwater effect is disabled and only the above-water fog gradient is rendered.
Scope Scaling and Aspect Ratio Handling
The SrScope system handles non-square screen aspect ratios by calculating a scale factor for the longer axis. The calculation:
csharp
Vector2 screenSize = new Vector2(Screen.width, Screen.height);
int smallAxis = screenSize.x < screenSize.y ? 0 : 1;
int bigAxis = 1 - smallAxis;
Vector2 scale = new Vector2(1.0f, 1.0f);
scale[bigAxis] = screenSize[smallAxis] / screenSize[bigAxis];
Vector2 offset = new Vector2(0.0f, 0.0f);
offset[bigAxis] = (scale[bigAxis] - 1.0f) * -0.5f;For common resolutions:
| Resolution | smallAxis | bigAxis | Scale | Offset (bigAxis) |
|---|---|---|---|---|
| 1920×1080 | Y (1080) | X (1920) | X: 0.5625 | X: 0.21875 |
| 2560×1440 | Y (1440) | X (2560) | X: 0.5625 | X: 0.21875 |
| 1080×1920 (portrait) | X (1080) | Y (1920) | Y: 0.5625 | Y: 0.21875 |
| 3440×1440 | Y (1440) | X (3440) | X: 0.4186 | X: 0.2907 |
The square render target for the scope is populated by blitting the source with the calculated scale and offset. The result is a square image taken from the center of the screen, regardless of aspect ratio.
Post-Processing Volume Configuration
Unturned configures a single PostProcessVolume on the main camera that applies the custom effects. The volume uses:
csharp
VolumeProfile profile = GetComponent<PostProcessVolume>().profile;
// SkyFog effect is added to profile
// Other built-in effects (SSAO, Bloom, Tonemapping) are configured hereThe post-processing volume is set to isGlobal = true, meaning it applies to the entire scene regardless of camera position. Individual effects within the volume can be enabled or disabled at runtime based on game state (e.g., night vision active, scope equipped).
Night Vision Color Configuration
The ItemGlassesAsset sets night vision colors with specific defaults per vision type:
csharp
// Military NVG: green-tinted with moderate fog
nightvisionColor = data.LegacyParseColor32RGB("Nightvision_Color",
defaultValue: LevelLighting.NIGHTVISION_MILITARY); // ≈ green
nightvisionFogIntensity = data.ParseFloat("Nightvision_Fog_Intensity",
defaultValue: 0.25f);
// Civilian NVG: grayscale with heavier fog
nightvisionColor = data.LegacyParseColor32RGB("Nightvision_Color",
defaultValue: LevelLighting.NIGHTVISION_CIVILIAN); // ≈ gray
nightvisionFogIntensity = data.ParseFloat("Nightvision_Fog_Intensity",
defaultValue: 0.5f);The civilian night vision forces the color to grayscale (R = G = B = R) to avoid confusion with colored NVG filters. The military night vision preserves the configured color. The fog intensity controls how much of the scene is obscured by fog when night vision is active — military NVG has lighter fog (0.25) than civilian (0.5).
Post-Processing Performance Considerations
The custom post-processing effects have differing performance costs:
| Effect | GPU cost | Bottleneck | Notes |
|---|---|---|---|
| SkyFog | Low | Fragment shader | Single fullscreen pass, simple color blending |
| SkyFog (underwater) | Low | Fragment shader | Additional water volume distance calculation |
| GaussianBlur scope | Medium-high | Fragment shader | Two-pass blur with adaptive kernel size |
| ScopeVignette | Low | Fragment shader | Single fullscreen pass, alpha compositing |
| GLRenderer overlay | Variable | CPU (GL immediate mode) | Depends on number of GL primitives submitted |
The Gaussian blur scope is the most expensive effect. At 4K resolution with a 2× blur scale, the two-pass blur processes approximately twice as many pixels per frame as at 1080p. The blur quality (standardDeviation parameter) controls the kernel size — a higher value produces more blur but requires more texture samples per pixel.
GLShader Material Instantiation
The GLUtility materials are created lazily on first access. Each material is created by:
csharp
new Material(Shader.Find("GL/LineFlatColor"))The Shader.Find call searches all loaded shaders for one with the given name. The first call triggers a search, but subsequent calls use the cached Material instance stored in the static field. The materials are never destroyed during the game session — they persist in memory as long as the AppDomain is alive.
The Dedicator.IsDedicatedServer guard on each property prevents material creation on headless server builds. On a dedicated server, GLUtility property access returns null for all materials, and no GL rendering occurs.
SrScope Implementation Details
The SrScope system has two code paths determined by GraphicsSettings.WantsDarkScopePeripheral:
DarkScope path (dark peripheral with vignette):
- The scope overlay has a dark vignette composited onto the peripheral area
- Uses the
ScopeVignetteshader with_ScopeAlphacontrolling darkness - No blur pass required — faster than the blur path
Blur path (blurred peripheral):
- The scope overlay is blurred using a two-pass Gaussian blur
- Uses the
GaussianBlurshader with_StdDeviationSquaredcontrolling blur intensity - Requires a temporary render target for the intermediate blur pass
- Uses
GetScreenSpaceTemporaryRT/ReleaseTemporaryRTfor the buffer
The fallback path (when both scopeAlpha and standardDeviation are approximately zero) simply blits the source to destination. This prevents a black frame flicker that occurs with TAA disabled (Nelson, 2025-06-27: "If we don't blit here then there's a black frame flicker with TAA disabled").
FAQ
Why does SkyFog use BeforeTransparent instead of AfterStack?
SkyFog needs to render before transparent geometry because the fog color affects the entire scene, including water and glass surfaces. If SkyFog ran after transparent rendering, transparent objects would not be colored by the fog, producing an unnatural look where water surfaces appear in full color against a foggy background.
What happens to GL rendering on dedicated servers?
GL rendering is completely disabled on dedicated servers. The GLRenderer game object is never created, and the GLUtility material properties all return null because of the Dedicator.IsDedicatedServer guard. No overlay rendering occurs, and the RuntimeGizmos system is not active.
Can I add custom post-processing effects as a mod?
The custom post-processing effects are registered via the [PostProcess] attribute on classes that extend PostProcessEffectSettings. Adding new effects would require modifying the game's assembly or using Harmony patch injection. The effects are hard-coded in the UnturnedPostProcess initialization and are not configurable through the module system.
How does the scope system handle ultrawide monitors?
The scope blit adjusts for aspect ratio by calculating smallAxis and bigAxis. On a 21:9 ultrawide monitor (2560×1080), the big axis is X (2560) and the small axis is Y (1080). The scale is 1080/2560 ≈ 0.422 on the X axis, centered by an offset of (0.422 - 1) × -0.5 = 0.289. This produces a square crop from the center of the ultrawide viewport.
What is the maximum number of water volumes SkyFog can track?
MAX_WATER_COUNT = 3. Up to three water volumes nearest to the camera within 2 meters are tracked. If the camera is not within 2 meters of any water volume, the underwater effect is disabled. The 2-meter threshold is hard-coded and not configurable.
Appendix E: Post-Processing Effect Registration Template
csharp
// Custom post-process effect registration pattern
[Serializable]
[PostProcess(typeof(MyEffectRenderer), PostProcessEvent.BeforeTransparent, "Custom/MyEffect")]
public sealed class MyEffect : PostProcessEffectSettings
{
// Parameters exposed to the volume system
public FloatParameter myParameter = new FloatParameter();
}
public sealed class MyEffectRenderer : PostProcessEffectRenderer<MyEffect>
{
private Shader shader;
private int somePropertyId;
public override void Init()
{
base.Init();
shader = Shader.Find("Hidden/Custom/MyEffect");
somePropertyId = Shader.PropertyToID("_SomeProperty");
}
public override void Render(PostProcessRenderContext context)
{
PropertySheet sheet = context.propertySheets.Get(shader);
sheet.properties.SetFloat(somePropertyId, settings.myParameter);
context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
}
}G-Buffer Layout Details
Unturned's deferred G-buffer layout follows Unity's standard deferred shading convention:
| Render target | Format | Contents |
|---|---|---|
| RT0 (ARGB32) | sRGB | Albedo (RGB) + Specular mask (A) |
| RT1 (ARGB32) | Non-sRGB | World-space normals (RGB) + Ambient occlusion (A) |
| RT2 (ARGBHalf) | Non-sRGB | Surface parameters: smoothness (R), metallic (G), specular color (B), occlusion (A) |
| DepthStencil | 24/8 | Depth (24-bit) + Stencil (8-bit) |
The specular mask in RT0's alpha channel is used by the lighting pass to determine which pixels receive specular highlights. The stencil buffer is used for light stencil culling — each light's volume is rendered into the stencil to limit the lighting pass to affected pixels.
Unity's deferred rendering does not support MSAA. Unturned uses post-process anti-aliasing (TAA or FXAA) instead of hardware MSAA.
Tone Mapping and Color Grading
Unturned applies post-process tone mapping and color grading through Unity's post-processing stack. The tonemapper uses the standard ACES filmic curve:
| Setting | Value |
|---|---|
| Tonemapper | ACES (Filmic) |
| Color grading mode | Low Definition Range (LDR) |
| Post-exposure | 0 (auto) |
| Temperature | Configurable per level via LevelLighting |
Color grading is adjusted at runtime by the day/night cycle and weather system. The LevelLighting class updates the color grading values each frame based on the current time of day and weather conditions.
Water Rendering Interaction
The SkyFog effect interacts with the water rendering system through the _IsCameraUnderwater uniform. When the camera is underwater, additional fog attenuation is applied to simulate light absorption through water:
Above water: fog_color = lerp(scene_color, fog_color, fog_falloff)
Under water: fog_color = lerp(scene_color, water_color, water_attenuation)The waterColor is sourced from LevelLighting.getSeaColor("_BaseColor") which reads the level's sea color configuration. The water color is typically a blue-green tint that blends with the underwater scene.
When LevelLighting.enableUnderwaterEffects is false, the underwater fog is disabled even if the camera is submerged. This allows level authors to create water bodies that behave like normal surfaces (shallow ponds, decorative water) without underwater visual effects.
Scope Rendering Performance Budget
The SrScope effect typically completes within 0.5-1.5ms at 1920×1080 depending on the blur kernel size. The Gaussian blur pass dominates the cost:
| Resolution | Blur scale | Half-kernel | Cost per pass | Total cost |
|---|---|---|---|---|
| 1920×1080 | 1.0 | 6 | ~0.3ms | ~0.6ms (×2 passes) |
| 2560×1440 | 1.33 | 8 | ~0.5ms | ~1.0ms |
| 3840×2160 | 2.0 | 12 | ~1.0ms | ~2.0ms |
The vignette-only path (no blur) completes in ~0.1ms regardless of resolution, making it the preferred path for lower-end hardware.
Appendix F: GLShader Namespace Reference
| Shader path | Material class | Type |
|---|---|---|
GL/LineFlatColor | LINE_FLAT_COLOR | Line (immediate mode) |
GL/LineCheckeredColor | LINE_CHECKERED_COLOR | Line (stippled) |
GL/LineDepthCheckeredColor | LINE_DEPTH_CHECKERED_COLOR | Line (depth-tested stippled) |
GL/LineCheckeredDepthCutoffColor | LINE_CHECKERED_DEPTH_CUTOFF_COLOR | Line (stippled, depth cutoff) |
GL/LineDepthCutoffColor | LINE_DEPTH_CUTOFF_COLOR | Line (depth cutoff) |
GL/TriFlatColor | TRI_FLAT_COLOR | Triangle (solid) |
GL/TriCheckeredColor | TRI_CHECKERED_COLOR | Triangle (stippled) |
GL/TriDepthCheckeredColor | TRI_DEPTH_CHECKERED_COLOR | Triangle (depth-tested stippled) |
GL/TriCheckeredDepthCutoffColor | TRI_CHECKERED_DEPTH_CUTOFF_COLOR | Triangle (stippled, depth cutoff) |
GL/TriDepthCutoffColor | TRI_DEPTH_CUTOFF_COLOR | Triangle (depth cutoff) |
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2026-07-27 | 57 Studios | Initial publication. Custom post-processing stack, GLRenderer, GLUtility, SkyFog, SrScope. |
