Skip to content
Learni
View all tutorials
Graphisme 3D

How to Master HLSL for Unity Shaders in 2026

18 minINTERMEDIATE
Lire en français

Introduction

HLSL is Microsoft's shader language used in Unity through the Built-in or HDRP pipelines. At an intermediate level, it enables custom visual effects such as advanced lighting or distortions. Understanding the vertex/fragment separation and semantic usage is essential for optimal GPU performance. This tutorial guides you step by step with concrete, directly compilable examples in Unity 2023+.

Prerequisites

  • Unity 2023.3 or higher
  • Basic knowledge of C# and shaders
  • Visual Studio or Rider for editing
  • A 3D project with a simple model

Basic Shader Structure

CustomLighting.shader
Shader "Custom/IntermediateLighting" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1,1,1,1)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
            struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; };
            sampler2D _MainTex; float4 _Color;
            v2f vert (appdata v) {
                v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o;
            }
            fixed4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv) * _Color; }
            ENDCG
        }
    }
}

This basic shader defines properties, the vertex shader, and the fragment shader. The POSITION and SV_POSITION semantics are required for the graphics pipeline.

Adding Normals and Simple Lighting

CustomLighting.shader
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 normal : TEXCOORD1; };
v2f vert (appdata v) {
    v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv;
    o.normal = UnityObjectToWorldNormal(v.normal); return o;
}
fixed4 frag (v2f i) : SV_Target {
    float3 lightDir = _WorldSpaceLightPos0.xyz;
    float diff = max(0, dot(i.normal, lightDir));
    fixed4 col = tex2D(_MainTex, i.uv) * _Color;
    return col * diff;
}

Normals are added to compute basic diffuse lighting. This demonstrates transforming normals from object space to world space.

Shader with Texture and Attenuation

CustomLighting.shader
fixed4 frag (v2f i) : SV_Target {
    float3 lightDir = _WorldSpaceLightPos0.xyz;
    float diff = max(0, dot(normalize(i.normal), normalize(lightDir)));
    fixed4 tex = tex2D(_MainTex, i.uv);
    float atten = 1.0 - length(_WorldSpaceLightPos0.xyz - i.vertex.xyz) * 0.1;
    return tex * _Color * diff * atten;
}

Linear attenuation based on distance to the light is added. Avoid expensive calculations in the fragment shader when possible.

Multiple Passes for Outline

CustomLighting.shader
Pass { /* Main lighting pass */ }
Pass {
    Cull Front
    CGPROGRAM
    #pragma vertex vertOutline
    #pragma fragment fragOutline
    v2f vertOutline(appdata v) {
        v2f o; float3 norm = normalize(v.normal);
        v.vertex.xyz += norm * 0.02; o.vertex = UnityObjectToClipPos(v.vertex); return o;
    }
    fixed4 fragOutline(v2f i) : SV_Target { return fixed4(0,0,0,1); }
    ENDCG
}

A second pass with Cull Front creates a simple outline by offsetting vertices along the normals.

Including a Custom .cginc File

LightingUtils.cginc
#ifndef LIGHTING_UTILS_INCLUDED
#define LIGHTING_UTILS_INCLUDED
float3 CalculateDiffuse(float3 normal, float3 lightDir) {
    return max(0, dot(normalize(normal), normalize(lightDir)));
}
#endif

Creating a reusable .cginc file allows sharing code across multiple shaders and improves maintainability.

Best Practices

  • Prefer calculations in the vertex shader when possible to save GPU resources
  • Use correct semantics (SV_Target, TEXCOORDn)
  • Always test on multiple platforms (DirectX, Vulkan)
  • Avoid dynamic branches in the fragment shader
  • Profile with Unity's Frame Debugger

Common Errors

  • Forgetting to normalize vectors before the dot product
  • Incorrect normal transformation (use UnityObjectToWorldNormal)
  • Exceeding the limit of 8 TEXCOORD interpolators without optimization
  • Not handling shader variants for different graphics quality levels

Going Further

Deepen your skills with our advanced shader courses.