advanced-shader-graph

Advanced Shader Programming in Unity: Crafting Stunning Custom Effects with HLSL and Shader Graph

Shader programming is one of the most important elements in creating eye-candy games. Control lighting and textures, as well as visual effects is achieved by it. Advanced Shader Programming for Unity allows the creation of custom effects which will certainly increase the graphical fidelity of your game. This article has covered shader programming both in HLSL and in Unity’s Shader Graph, exploring techniques such as realistic water shaders, dynamic cel-shading, per-stage optimization.

Table of Contents

  • Introduction to Shader Programming
  • Basics of HLSL in Unity
  • Creating Custom Water Shaders
  • Dynamically Cel-Shaded with Stepwise Lighting
  • Optimizing Shaders for Fast Performance
  • Conclusion

Introduction to Shader Programming

    Shaders in Unity Shaders are basically scripts that an author may write to control how the GPU will render each pixel of a game object. They give developers control over visual effects, allowing them to produce great surfaces, lighting effects, and reflections. An advanced form of shader programming uses either Unity’s Shader Graph or HLSL, both of which offer different ways of how visual effects should be produced.

    • Shader Graph: A node-based interface that simplifies the creation of shaders. Developers are able to create shaders visually rather than through code.
    • HLSL: More classical approach where one writes his own code to have control of the rendering of an object precisely.

    Both of these techniques can be used alone or in combination to produce very complex and visually luscious effects. This tutorial covers important techniques for advanced shader programming in Unity.

    The Core HLSL in Unity

      HLSL is widely used in Unity to write custom shaders. Before discussing the advanced shaders, let’s see how a simple HLSL shader works in Unity. Here is a very simple HLSL shader that changes an object’s color based on lighting:

      Shader "Custom/SimpleShader" {
          Properties {
              _Color ("Color", Color) = (1,1,1,1)
          }
          SubShader {
              Pass {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
      
                  fixed4 _Color;
      
                  struct appdata {
                      float4 vertex : POSITION;
                      float3 normal : NORMAL;
                  };
      
                  struct v2f {
                      float4 pos : SV_POSITION;
                      float3 norm : TEXCOORD0;
                  };
      
                  v2f vert(appdata v) {
                      v2f o;
                      o.pos = UnityObjectToClipPos(v.vertex);
                      o.norm = normalize(mul(v.normal, (float3x3)unity_ObjectToWorld));
                      return o;
                  }
      
                  fixed4 frag(v2f i) : SV_Target {
                      float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                      float diff = max(0, dot(i.norm, lightDir));
                      return _Color * diff;
                  }
                  ENDCG
              }
          }
      }

      This shader performs the following operations:

      • Vertex Shader: Takes each vertex of the object and calculates the position in screen space.
      • Fragment Shader: Calculates the lighting of each pixel, creating the effect of shading based on the angle of the light source.

      Now, let’s explore more advanced shaders, starting with water.

      Crafting Custom Water Shaders

      Creating realistic water shaders involves multiple visual techniques such as vertex displacement for waves, reflection, and refraction effects. In this section, we’ll build an advanced water shader that simulates waves and reflects the environment.

      Step 1: Setting Up Shader Graph

      1. Create a new Unlit Shader Graph in Unity.
      2. Vertex Displacement for Waves:
        • Use a noise texture to drive the vertex displacement for waves.
        • Apply the displacement only on the y-axis to simulate vertical wave motion.
      Shader "Custom/AdvancedWaterShader" {
          Properties {
              _MainTex ("Base Texture", 2D) = "white" {}
              _ReflectionTex ("Reflection Texture", 2D) = "black" {}
          }
          SubShader {
              Pass {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
                  sampler2D _MainTex;
                  sampler2D _ReflectionTex;
                  float4 _WaveParams;
      
                  struct appdata {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                  };
      
                  struct v2f {
                      float2 uv : TEXCOORD0;
                      float4 pos : SV_POSITION;
                  };
      
                  v2f vert (appdata v) {
                      v2f o;
                      float3 waveMotion = sin(v.vertex.y + _Time.y * _WaveParams.x) * _WaveParams.yz;
                      o.pos = UnityObjectToClipPos(v.vertex + waveMotion);
                      o.uv = v.uv;
                      return o;
                  }
      
                  fixed4 frag (v2f i) : SV_Target {
                      fixed4 reflection = tex2D(_ReflectionTex, i.uv);
                      return lerp(tex2D(_MainTex, i.uv), reflection, _WaveParams.w);
                  }
                  ENDCG
              }
          }
      }

      Step 2: Adding Reflections and Refractions

      • Reflections: Use a reflection texture sampled from the scene.
      • Refraction: Use the Fresnel effect to distort the scene based on the view angle.

      In the example above, the vertex shader creates the wave motion by displacing vertices. The fragment shader blends between the water texture and the reflection for realistic water effects.

      4. Dynamic Cel-Shading with Stepwise Lighting

      Dynamic Cel-Shading with Stepwise Lighting Cel-shading is among the oft-used styles of games that create a two-dimensional look, similar to hand-drawn animation. The fundamental element behind cel-shading is that it simplifies lighting considerably, hence the strong transitions between light and shadow. Here’s how to do a cel-shaded effect with HLSL:

      Cel Shading Effect

      Step 1: Sobel Edge Detection for Outlines

      1. Apply a Sobel filter to the scene to detect object edges.
      2. Render these edges as black outlines to simulate a cartoon look.
      float4 SobelEdgeDetection(float2 uv) {
          float edge = tex2D(_MainTex, uv).r; // Use red channel for simplicity
          float2 sobelKernel[9] = { ... }; // Sobel filter values
          float sobel = 0.0;
          for (int i = 0; i < 9; i++) {
              sobel += tex2D(_MainTex, uv + sobelKernel[i]).r;
          }
          return sobel > 1.0 ? float4(0, 0, 0, 1) : float4(1, 1, 1, 1);
      }

      Step 2: Multi-Step Lighting

      In cel-shading, light intensity is divided into steps to create a hard transition between lit and shadowed areas.

      fixed4 frag(v2f i) : SV_Target {
          float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
          float diff = max(0, dot(i.norm, lightDir));
          float stepShade = ceil(diff * 3.0) / 3.0; // 3 steps of lighting
          return stepShade * _Color;
      }

      Step 3: Combining Cel-Shading and Edge Detection

      Finally, blend the cel-shaded lighting with the Sobel edge detection to complete the look.

      5. Optimizing Shaders for High Performance

      High-quality shaders can be performance-intensive. Optimizing them ensures smooth gameplay even on lower-end devices. Here are key optimization techniques for shaders:

      • Apply Mipmaps

      Mipmaps are downsampling versions of a texture, that appear when an object is far from the camera and again saves some cost of sampling the textures and hence improving performance.

      • Overdraft decline

      Use very few transparent objects since it builds up to overdraw, and it is very GPU-intensive. Always use the least complex blend mode possible that gets the job done.

      • Multi Shader Variants

      Let us now create shader variants, depending on the quality levels. The same can remove complex reflections or lighting effects for low-end devices while retaining it in high-end machines.

      • Shader LOD (Level of Detail)

      The technique used here achieves dynamic control over the complexity of shaders based on proximity of cameras to objects and automatically and considerably reduces complexity without detrimental qualitative features.

      Conclusion

      Advanced shader programming in Unity represents an open gateway toward really unmatched, striking visual effects which really improve the graphical fidelity of your game. It can realize realistic water, dynamic cel-shading, or optimize performance with shader-programmed visuals. You have some really powerful tools at your disposal through Shader Graph or HLSL. As you learn about shader principles and practice these optimization techniques, your visual effects can truly impress your audiences.

      Leave a Reply

      Shopping cart

      0
      image/svg+xml

      No products in the cart.

      Continue Shopping