Unity ShaderLab Cheatsheet — HLSL, Pipelines & Optimization

pavelzosim:~/atlas_SYS.ONLINE / UTC+3

01 CGPROGRAM Section

The sections we have examined earlier are composed in the ShaderLab declarative language. The true challenge in graphics programming begins here with the CGPROGRAM or HLSLPROGRAM declaration.

Shader "CustomPath/ShaderName_simpleColor"
{
    // ====================================================
    // 1. MATERIAL PROPERTIES - Inspector Interface
    // ====================================================
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1, 1, 1, 1)
    }

    // ====================================================
    // 2. SUBSHADER - Render Pipeline Configuration
    // ====================================================
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        // ====================================================
        // 3. PASS - Contains the shader program code
        // ====================================================
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            // Global Uniforms
            sampler2D _MainTex;
            fixed4 _Color;

            // ====================================================
            // 4. DATA STRUCTURES (Vertex Input / Output)
            // ====================================================
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
            };

            // ====================================================
            // 5. VERTEX SHADER STAGE
            // ====================================================
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o, o.vertex);
                return o;
            }

            // ====================================================
            // 6. FRAGMENT SHADER STAGE
            // ====================================================
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv) * _Color;
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }

    // ====================================================
    // 7. SHADER FALLBACK
    // ====================================================
    FallBack "Diffuse"
}

02 Shader Architecture Technical Reference

[ 3.0 Component of the shader: Pass ]

A Pass refers to a Render Pass literally. For those who have worked with rendering in 3D software (e.g., Maya or Blender), this concept will be easier to understand since when an image is being processed, it can generate different layers or passes separately (e.g., color Pass, light Pass, occlusion Pass, etc.) and thus obtain a separate composition in different layers.
!Each Pass renders one object at a time: That is, if we have two passes in our shader, the object will be rendered twice on the GPU and the equivalent of that would be two draw calls.

CGPROGRAM / ENDCG

Shader will appear with the declaration of HLSLPROGRAM instead of CGPROGRAM since HLSL is currently the official graphics programming language (in fact, Shader Graph is based on it). Anyway, we can update our shader simply by replacing the word CGPROGRAM for HLSLPROGRAM and ENDCG for ENDHLSL, and then our program will compile both in Built-in RP as in Universal RP and High Definition RP.

HLSL Pragma

The pragma is a directive that is used to give instructions to the compiler. In the case of shaders, it is used to indicate the vertex and fragment functions that we will use in shader. Their function is to help our shader recognize and compile certain functions that could not otherwise be recognized as such.

Multi-compile Pragma Variant Flags:

HLSL Include

The include is a directive that is used to include a file in the shader. This is useful when we want to use a function that is already defined in another file and we do not want to write it again in the current shader. In Unity, the most common include is “UnityCG.cginc”, which contains a series of functions that are used in the shader (e.g. lighting, texture sampling, etc.).

Another defined function that we can find in UnityCG.cginc is UNITY_PI, which equals 3.14159265359f. The latter is not included in our default shader because it is used only in specific cases (e.g. when calculating a triangle or Sphere).

Review Path:
Windows: {unity install path}/Data/CGIncludes/UnityCG.cginc

We can create our own directives using ".cginc" files, which will help us to organize our code and make it more readable. For example, we can create a file called "MyFunctions.cginc" and inside it, we can define the functions that we will use in our shader.

HLSL vertex input & vertex output

A data type that we will use frequently in the creation of our shaders is “struct”. For those who know the C language, a struct is a compound data type declaration, which defines a grouped list of multiple elements of the same type and allows access to different variables through a single pointer. We will use structs to define both inputs and outputs in our shader.

Its syntax is as follows:

struct name
{
    vector[n] name : SEMANTIC[n];
};

A semantic is a chain connected to a shader input or output that transmits usage information of the intended use of a parameter:

System Value (SV) Semantics Matrix

Semantic Description
SV_POSITION Corresponds to the position of the vertices in screen space.
SV_TARGET Corresponds to the target of the vertices (Render Target / Frame Buffer color output).
SV_DEPTH Corresponds to the depth of the vertices.
SV_COVERAGE Corresponds to the coverage of the vertices.
SV_CLIPDISTANCE Corresponds to the clipping distance of the vertices.
SV_STENCILREF Corresponds to the stencil reference of the vertices.
SV_VERTEXID Corresponds to the vertex ID.
SV_PRIMITIVEID Corresponds to the primitive ID.
SV_INSTANCEID Corresponds to the instance ID.
SV_ISFRONTFACE Corresponds to the front face of the vertices.
SV_SAMPLEINDEX Corresponds to the sample index of the vertices.
SV_DISPATCHTHREADID Corresponds to the dispatch thread ID.
SV_GROUPID Corresponds to the group ID.
SV_GROUPINDEX Corresponds to the group index.
SV_GROUPTHREADID Corresponds to the group thread ID.
SV_GROUPTHREADFLATTENED Corresponds to the group thread flattened.
SV_RENDERTARGETARRAYINDEX Corresponds to the render target array index.
SV_VIEWPORTARRAYINDEX Corresponds to the viewport array index.

Cg / HLSL vertex shader stage

The vertex shader corresponds to a rendering pipeline’s programmable stage, where the vertices are transformed from a 3D space to a two-dimensional projection on the screen. Its smallest unit of calculation corresponds to an independent vertex.

"vert" is the name of the function that we will use to transform the vertices which corresponds to the vertex shader stage. Vertex shader was declared as such in the #pragma vertex.

This Shader in an Unlit type means it doesn't have light it includes a function for the vertex shader and another for the fragment shader #pragma fragment frag. It is essential to mention this since Unity provides a quick way to write shaders in the form of “Surface Shader” (surf) that generates Cg code automatically, exclusively for materials that are affected by lighting.

The first operation that occurs within the vertex shader stage is the transformation of the object vertices from object-space to clip-space through the “UnityObjectToClipPos” method. Let’s remember that our objects are within a three-dimensional space in the scene, and we must transform those coordinates into a two-dimensional projection of pixels on the screen. That transformation occurs precisely within the “UnityObjectToClipPos” function.

This function multiplies the matrix of the current model (unity_ObjectToWorld) by the factor of the multiplication between the current view and the projection matrix (UNITY_MATRIX_VP):

$$UnityObjectToClipPos = mul(\text{UNITY\_MATRIX\_VP}, mul(\text{unity\_ObjectToWorld}, float4(\text{pos}, 1.0)))$$

Cg / HLSL fragment shader stage

Our next and last function in the Pass corresponds to the fragment shader stage that appears in our shader with the name “frag”. The reason we can tell that frag is the function of the fragment shader stage is because it has been declared as such in the #pragma fragment.

The word “fragment” refers to a pixel on the screen; to an individual fragment or to a group that together cover an object area. This means that the fragment shader stage will process every pixel on the computer screen concerning the object we are viewing.

This shader is currently written in Cg, so it will only compile in Built-in RP. If we want our shader to compile both in Universal RP and High Definition RP, we will have to change this data type for “half4 or float4”, otherwise our program could generate an error.

// Cg language architecture configuration
fixed4 frag (v2f i) : SV_Target { … }

// HLSL language architecture configuration
half4 frag (v2f i) : SV_Target { … }

Unlike the vertex shader stage, this function has an output called “SV_Target”, which allows us to render our scene in an intermediate buffer (render target) instead of sending the data to the Frame Buffer. In previous versions of Direct3D (version 9 and lower), the color output in the fragment shader appeared with the COLOR semantic. However, in modern GPUs (version 10 onwards), this semantics is updated to SV_Target, which means “system value target”. It can apply additional effects to the image before projecting them on the computer screen.

Inside the fragment shader stage we can find a fixed4 vector type called “col” which is the same as the tex2D function, where, as an argument, it receives the _MainTex texture and UV coordinate input. Basically what this operation does is store a texture within the col vector.

Data Types Precision & Optimization

Note: A high-precision data type has more decimals, therefore, the GPU will take longer to calculate it, increasing times and generating heat. It is essential to use vectors and/or variables in their required data type, thus we can optimize our program, reducing the graphic load on the GPU.

“Sampler” refers to the sampling state of a texture. Within this type of data, we can store a texture and its UV coordinates. Scalar values are real number integers or floating-point numbers (float, half, fixed, etc.) and vectors are a set of scalar values (float2, half3, fixed4, etc.). Matrix types are used mainly for shearing, rotation and changes of vertex positions:

// three rows and three columns matrix execution layout
float3x3 name = float3x3(
    1, 0, 0,
    0, 1, 0,
    0, 0, 1
);

// two rows and two columns matrix execution layout
half2x2 name = half2x2(
    1, 0,
    0, 1
);

// four rows and four columns matrix execution layout
fixed4x4 name = fixed4x4(
    1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1
);

The data type “sampler” allows us to store both the texture and the sampling status in a single variable:

// declare the _MainTex texture as a global variable
Texture2D _MainTex;
// declare the _MainTex sampler as a global variable
SamplerState sampler_MainTex;

half4 frag(v2f i) : SV_Target {
    // inside the col vector sample the texture in UV coordinates.
    half4 col = _MainTex.Sample(sampler_MainTex, i.uv);
    return col;
}

The above process can be optimized by simply using a unified legacy structural macro assignment statement:

// declare the sampler for _MainTex
sampler2D _MainTex;

half4 frag(v2f i) : SV_Target {
    // sample the texture in UV coordinates using the function tex2D().
    half4 col = tex2D(_MainTex, i.uv);
    return col;
}

Intrinsic Functions Reference Matrix

Function Description Evaluation
abs(x) returns the absolute value of a scalar or vector.
ceil(x) returns the smallest integer value greater than or equal to the input value.
clamp(x, min, max) restricts the value of a scalar or vector to a range.
cos(x) returns the cosine of a scalar or vector.
sin(x) returns the sine of a scalar or vector.
tan(x) returns the tangent of a scalar or vector.
exp(x) returns the exponential value of a scalar or vector.
exp2(x) returns the exponential value of a scalar or vector in base 2.
floor(x) returns the largest integer value less than or equal to the input value.
step(edge, x) returns 0.0 if the value is less than the edge, and 1.0 if the value is greater than or equal to the edge.
smoothstep(min, max, x) returns a smooth interpolation between 0.0 and 1.0 when the value is between the minimum and maximum values.
frac(x) returns the fractional part of a scalar or vector.
length(v) returns the length of a vector.
lerp(a, b, t) returns the linear interpolation between two values.
min(a, b) returns the minimum value between two values.
max(a, b) returns the maximum value between two values.
pow(x, y) returns the value of the first parameter raised to the power of the second parameter.

03 Unity Render Pipeline Fundamentals

Unity utilizes three distinct rendering architectures, each tailored to different performance requirements, hardware targets, and pipeline capabilities.

Key Unity Rendering Architectures

Pipeline Comparison Table

Feature BIRP URP HDRP
Shader Language Cg / HLSL HLSL HLSL
Base Include File UnityCG.cginc Core.hlsl Common.hlsl
Lightmap Support
Deferred Rendering Limited
Shader Graph Support No

04 GPU Optimization Essentials for Shaders

Writing efficient shader code requires understanding how modern GPU hardware executes logic at the silicon level. Minor syntax adjustments can drastically alter instruction latency and register pressure.

Math Operations & Instruction Latency

GPUs are optimized for parallel multiply-accumulate (MAC) operations, which form the structural building blocks of linear algebra:

// BAD: Division is expensive and stalls the ALU pipeline
float result = value / 2.0;

// GOOD: Use multiplication by a precalculated reciprocal literal
float result = value * 0.5;

When to Actually Use Division

While multiplication should always be favored, division is structurally unavoidable under the following mathematical conditions:

Data Type Precision Optimization

Choosing the correct variable precision directly influences register allocation and memory bandwidth usage on mobile and embedded architectures:

Texture Sampling Optimizations

Branching Efficiency & Conditional Overhead

Dynamic branching in a fragment shader causes divergent execution paths within a warp/wavefront, forcing the GPU to execute both paths sequentially while masking off threads.

// BAD: Dynamic branch causing execution divergence in fragment stages
if (distance > threshold) {
    // Code path A
} else {
    // Code path B
}

// GOOD: Mathematical interpolation eliminating physical branch execution
float lerpFactor = saturate(distance / threshold);
return lerp(a, b, lerpFactor);

SRP Batcher Compatibility

To leverage the Scriptable Render Pipeline (SRP) Batcher and eliminate CPU-side draw call overhead, all material properties must be grouped within a standardized constant buffer block named UnityPerMaterial.

// Proper constant buffer declaration for SRP Batcher compliance
CBUFFER_START(UnityPerMaterial)
    float4 _MainTex_ST;
    half4 _Color;
CBUFFER_END

Vectorization Tips

Modern GPUs run vectorized instructions efficiently. Consolidate your linear algebra into parallel vector calculations rather than component-by-component scalar operations.

// GOOD: Unified parallel vector math (processed simultaneously by SIMD hardware)
float3 result = a * b + c;

// Swizzling: Reconstructing component layouts efficiently via native routing
float4 color = texture.rrrg; // Maps channels natively to create float4(r, r, r, g)

Manual Mipmap Calculations

When implementing custom screen-space mappings or procedural patterns, automatic mipmap generation fails due to missing texture coordinates. Screen-space partial derivatives must be evaluated manually:

// Manual mip selection based on screen-space coordinate differentials
float2 derivatives = ddx(uv) + ddy(uv);
float mip = 0.5 * log2(max(dot(derivatives, derivatives), 1e-6));

// Sample texture with explicitly resolved mip level
color = tex2Dlod(_MainTex, float4(uv, 0, mip));

Shader Variant Reduction

Shaders generate massive keyword combinations (permutations) that can explode compile times and memory footprints. Differentiate keyword behavior strategically:

Debugging & Profiling

05 Cross-Pipeline Shader Adaptation (Built-in, URP, HDRP)

Porting legacy shaders into Scriptable Render Pipelines (SRP) requires rewriting architectural layout blocks. Modern pipelines abandon old Cg compiler abstractions in favor of direct, cross-platform HLSL structures.

URP Shader Implementation Example

Shader "URPExamples/SimpleColor"
{
    Properties
    {
        _BaseColor("Color", Color) = (1,1,1,1)
        _BaseMap("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            // Data structure for Vertex Input
            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv           : TEXCOORD0;
            };

            // Data structure for Vertex Output (Rasterizer Input)
            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv           : TEXCOORD0;
            };

            // Textures and Samplers API macros
            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            // SRP Batcher constant buffer configuration
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _BaseColor;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                // Transform positions from object space to homogenous clip space
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                // Core macro-based sampling
                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                return color * _BaseColor;
            }
            ENDHLSL
        }
    }
}

06 Key Pipeline Differences & Architectural Mappings

When migrating shaders across pipelines, use the following structural conversions for include files, space transformations, and sampling macros:

Structural Include Files

Modern render pipelines break up global dependencies into target-specific shader libraries to avoid bloating memory allocations:

Matrix & Space Operations

Modern hardware pipelines standardize vertex operations using specific, explicitly named functions rather than combined utility macros:

Texture Sampling Architectures

To preserve compile-time API compatibility across platform backends (such as Vulkan, Metal, and Direct3D), explicit texture sampling macros must replace legacy sampler2D layouts:

Pipeline Stage Legacy System (BIRP) Modern SRP Architecture (URP / HDRP)
Global Allocation sampler2D _BaseMap; TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
Fragment Sample fixed4 col = tex2D(_BaseMap, uv); half4 col = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);

07 References & Technical Sources

Below is the verified verification index of external engineering resources, documentation modules, and research papers utilized throughout this technical pipeline review:

// END OF PART // UNITY_SHADERLAB // EOF