Graphics

HDRP Wind Corruption Shader Post-Mortem

Unity HDRP
Decal Projector
Shader Graph
Optimization

Facing the "volumetric corruption" requirement, this post documents why we ruled out heavy solutions like Uber Shaders and RT-driven approaches, pivoting to a lightweight Native Decal Projector with mathematical static/dynamic decoupling.

HDRP Decal Projector Wind Corruption Shader Analysis

Rendering Pipeline Decision: The Journey from Complex R&D to Pragmatic Implementation

When receiving Mike’s requirement for “volumetric corruption with dynamic wind feedback,” the initial instinct was to lean towards complex, cutting-edge techniques. However, during the actual R&D and testing phases, I had to make pragmatic trade-offs between performance and visual fidelity.

This post-mortem not only documents how our final Shader is constructed, but more importantly, why we chose NOT to use those “advanced-sounding” solutions, and how we left the door open for future extensions.


1. The Initial Blueprint: Exploring Heavy Architectures

In the early stages of the project, to completely solve the lack of volumetric depth in 2D decals, I evaluated several highly ambitious technical routes:

  • Route A: Uber Shader Injection Injecting the corruption logic directly into the Master Node of all terrain and prop materials via a Subgraph, utilizing true world-space coordinates for vertex-level influence.

    SCHEME_A::UBER_SHADER_INJECTIONSTATUS::MODIFIED
    Input Assembler[FIXED]
    Vertex Shader[SHADER]
    Tessellation[SHADER]
    Geometry Shader[SHADER]
    Rasterizer[FIXED]
    Fragment Shader[SHADER]
    Output Merger[FIXED]
    MODIFIED
    UNTOUCHED
  • Route B: Mesh Decal Proxy Spawning a real semi-transparent sphere mesh at the corruption site and calculating soft blending at intersections by reading the CameraDepthTexture.

    SCHEME_B::MESH_PROXY_BLENDSTATUS::MODIFIED
    Input Assembler[FIXED]
    Vertex Shader[SHADER]
    Tessellation[SHADER]
    Geometry Shader[SHADER]
    Rasterizer[FIXED]
    Fragment Shader[SHADER]
    Output Merger[FIXED]
    MODIFIED
    UNTOUCHED
  • Route C: Render Texture Channel Packing (Enrico’s Proposal) Discarding global variable parameters in favor of a real-time Render Texture (RT). By packing the corruption shape, wind perturbation, and footprint state into different channels, this would allow for fully independent physical interactions for every corruption ring in the scene.

Theoretically, all these approaches are perfectly valid and represent standard practices for environmental VFX in AAA engines. However, following a rigorous Profiler audit, I decided to put them on hold.


2. The Reality Check: Why We Pivoted

The primary reasons for discarding the above solutions boil down to a poor Performance-to-Visual Ratio and exorbitant maintenance costs.

  • Why we dropped the Uber Shader: The Variant Explosion Risk Adding a corruption macro toggle to a vast library of existing materials would cause Shader Variants to multiply exponentially, severely bloating VRAM and build times. Furthermore, if environment artists updated the base terrain shaders later, these injected connections would easily break, resulting in an unmanageable maintenance nightmare.
  • Why we dropped the Mesh Proxy: The Abyss of Overdraw Proxy spheres are inherently part of the Transparent render queue, meaning they cannot benefit from Early-Z culling. If multiple corruption spheres overlap in a narrow corridor, the fragment shader (frag) overdraw would instantly blow through our performance budget.
  • Why we postponed the RT Solution: Avoiding Premature Over-engineering Enrico’s RT proposal is undeniably the ultimate form for complex environmental VFX. However, our current weather system only provides a “globally unified” wind parameter. Until game design explicitly demands granular interactions like “wind occlusion inside caves,” forcing a dynamic RT pipeline would introduce unnecessary memory bandwidth pressure.

Based on these considerations, I decided to pivot to the most lightweight, non-invasive solution available: HDRP Native Decal Projector.

FINAL_SCHEME::HDRP_DBUFFER_PROJECTIONSTATUS::MODIFIED
Input Assembler[FIXED]
Vertex Shader[SHADER]
Tessellation[SHADER]
Geometry Shader[SHADER]
Rasterizer[FIXED]
Fragment Shader[SHADER]
Output Merger[FIXED]
MODIFIED
UNTOUCHED
Note: Although it acts as a decal, under the hood it still renders a Projection Box (ia/vert), calculates inverse projection and internal logic in the fragment stage (frag), and finally blends into the D-Buffer (om).

3. Solving the Decal’s Inherent Visual Flaws

Having committed to Native Decals, the new challenge emerged: Using purely mathematical logic, how do we make a 2D projection feel like an organic fluid torn by the wind, without clipping awkwardly into the environment?

Through a synchronized effort between C# and Shader Graph, we tackled three major pain points:

Pain Point 1: Visual Snapping During Wind Speed Changes

Initially, I multiplied Time * WindSpeed directly inside the Shader to pan the noise. The fatal flaw? Whenever wind speed changed smoothly, the output of Time would jump drastically, causing the VFX to aggressively flicker and snap. The Solution (C# Phase Accumulation): Strip the Shader of its time-calculation duties. Instead, use Time.deltaTime in the C# script to calculate the incremental physical distance traveled each frame. We pass this stabilized, continuously accumulating vector (_CustomWindOffset) to the Shader, while applying modulo operations to prevent floating-point precision loss over long play sessions.

Pain Point 2: The Entire Decal Sliding Across the Ground

If we applied the wind offset to the global UVs directly, the entire corruption pit would slide across the floor like a skateboard. The Solution (Static/Dynamic Decoupling): Strictly isolate the static and dynamic components within the Shader. Static UVs are exclusively used to generate the central black hole mask; meanwhile, the wind perturbation doesn’t shift the UVs but is instead added to the Radius of the Distance node. This locks the pit in place, while allowing only the outer flames to stretch downwind.

Comparison: Left shows incorrect sliding, Right shows correct edge stretching

Visual: Before vs. After Static/Dynamic Decoupling (Left: Incorrect sliding; Right: Correct stretching)

Pain Point 3: The “Tree-Climbing” Artifact (Vertical Stretching)

When a decal is projected near a tree trunk or steep wall, the 2D planar projection inevitably creates hideous vertical stretching.

Decal texture stretching vertically on a tree trunk

The Problem: Severe stretching artifacts caused by Decal projection on vertical surfaces.

We deployed a “Dual Defense Mechanism” to handle this:

  1. Defense A (Physical Culling - Decal Layer Mask): For individual objects that should absolutely never be corrupted (e.g., crucial quest items, specific walls), we exclude them entirely using HDRP’s Decal Layer Mask. This takes effect at the base Culling stage, serving as a zero-cost hard isolation.
  2. Defense B (Visual Blending - Height Fade Mask): For organic transitions over tree roots or slight slopes, a hard Layer cut looks terribly rigid. Therefore, I built a volume height clipping mask inside the Shader based on absolute world height (World Space Y), infused with noise to break up the cut line. It allows flames to naturally “lick” slight elevations, but smoothly fades to transparent if it climbs beyond a set threshold, perfectly hiding the stretching artifact.
Result after applying the height fade mask, stretching is perfectly hidden

The Solution: Vertical stretching is smoothly eliminated after applying the Height Fade Mask.


4. Shader Graph Core Logic Breakdown

To execute the concepts above, the Shader is cleanly modularized into several functional groups:

Shader Graph global overview

Shader Graph Global Overview: Clean functional modularization

Group: WindFlowNoise (Wind Drive Engine)

Receives the accumulated variable _CustomWindOffset from C#.

  • Logic: Multiplies the global offset by an intensity parameter. It doesn’t modify vertices directly but acts as the dynamic “coordinate displacement thrust” for subsequent noise sampling.
Wind Flow Noise node setup

Group: UVDistance Field (Static Distance Field)

This is the anchor for the underlying corruption pit, ensuring the core doesn’t drift.

  • Logic: Uses a pristine local UV coupled with a center point of (0.5, 0.5) fed into a Distance node to calculate a perfectly static radial gradient. Inside this group, introducing any wind variables is strictly prohibited.

Group: Corruption Mask (Multi-dimensional Masking)

This is the heart of the deformation and restriction logic. I split it into two dimensions: planar perturbation and height clamping.

  • Dimension 1: Downwind Surface Stretching (XZ Noise) Handles fluid deformation on the 2D surface.

    XZ plane noise logic
    • Method: I split the wind offset vector and feed it into two separate Simple Noise nodes. By independently adjusting the XZ and Y scales, we artificially create a directional bias—elongated in the downwind direction and compressed on the flanks.
    • Crucial Detail: During this phase, I use a Subtract node to subtract 0.5 from the 0~1 noise. This shifts pure positive numbers into a range containing negatives, ensuring the flames oscillate back and forth across the original boundary, preventing the entire ring from migrating unidirectionally.
  • Dimension 2: Height Clamping (Y Mask) Specifically addresses the “tree-climbing” stretching artifact (Pain Point 3).

    Y-axis height mask logic
    • Method: Extracts the absolute Y-axis height of the pixel using the Position (World) node. This is fed into a Remap node to define the falloff zone, then a Power node controls the harshness of the fade, and finally, a Clamp restricts it between 0 and 1.
    • Result: This computed Float acts as an invisible “horizontal guillotine,” forcing all pixels that stray “too high off the ground” to aggressively fade out right before the final output.

Group: M * (1-M) * 4 (Emission Edge Extraction & Final Blend)

  • Logic: A pure mathematical trick. By adding the computed XZ noise to the static distance field and subtracting our designated corruption radius, we generate a base dynamic mask, M.
  • Final Composite: Utilizing the mathematical property of M * (1 - M), we extract a brilliant peak exclusively along the light-to-dark transition boundary. We take this razor-sharp flame ring, multiply it by our previously calculated Y Mask (to trim excess vertical climbing), and finally multiply it by our target color before feeding it into the Emission channel.
Final emission extraction and height mask blending

Final Composite: Blending the mathematical edge extraction with the height mask

Final in-game visual result

The Final In-Game Result


5. Future Extensions: Leaving the Door Open for RT

Currently, passing parameters via Shader.SetGlobalVector is incredibly performant and offers the best bang for our buck. However, keeping Enrico’s advice in mind, this setup can smoothly transition if environmental interaction demands increase.

Triggers for a Pipeline Upgrade:

  1. Design requires localized wind occlusion (e.g., zero wind inside a cave).
  2. The need for precise footprint feedback, such as flames extinguishing when a character steps on them.

When that day arrives, because our current Shader architecture already solidifies the underlying logic for “Static/Dynamic Decoupling” and “Planar/Height Separation”, we only need to swap out the few nodes receiving C# global variables. By replacing them with nodes that sample specific channels of a global Render Texture (e.g., reading the G channel for wind strength, B channel for footprint masks), we can seamlessly plug into a holistic environmental ecosystem like TVE.

Conclusion

Stepping back from a complex AAA architectural R&D phase to a streamlined mathematical implementation has been the most valuable lesson of this pipeline selection process. As Technical Artists, we must avoid the trap of treating everything like a nail just because we hold a powerful hammer.

By leveraging precise mathematical decoupling and height masking, we traded a handful of cheap ALU instructions for excellent volumetric visuals. More importantly, we clearly defined the boundaries of this solution while actively paving a smooth transition path toward a more robust RT architecture in the future.