Skip to content

Shader Programming Guide — GLSL and HLSL for Game Development

DodaTech Updated 2026-06-23 5 min read

Shader Programming is the art of writing programs that run directly on the GPU — vertex shaders transform 3D geometry into 2D screen coordinates, fragment (pixel) shaders determine the color of every pixel on screen, and compute shaders handle general-purpose GPU work. The two dominant shading languages are GLSL (OpenGL Shading Language, used in Godot, WebGL) and HLSL (High-Level Shader Language, used in Unity, Unreal, DirectX), and mastering them is essential for anyone serious about WebGL or Game Development.

In this tutorial, you'll understand the GPU pipeline, write vertex and fragment shaders in both GLSL and HLSL, pass data from CPU to GPU via uniforms, sample textures, implement the Blinn-Phong lighting model, and create screen-space post-processing effects like bloom and edge detection. By the end, you'll be able to create custom visual effects that set your game apart.

Why Shader Programming Matters

Shaders control the entire visual identity of a game — the metallic shine of a sword, the glow of a neon sign, the heat shimmer over a desert. Without shader knowledge, you are limited to the built-in materials your engine provides. At DodaTech, custom shaders power the GPU-accelerated image processing in Durga Antivirus Pro and the hardware-accelerated UI effects in Doda Browser.

Learning Path

flowchart LR
  A[Game Development] --> B[Shader Programming
You are here] B --> C[Game Optimization] B --> D[Computer Graphics] style B fill:#f90,color:#fff

GLSL Vertex and Fragment Shaders

The vertex shader runs once per vertex, transforming local coordinates to screen space. The fragment shader runs once per rasterized pixel.

// GLSL Vertex Shader
#version 330 core

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;

uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;

out vec3 vNormal;
out vec2 vTexCoord;
out vec3 vWorldPos;

void main()
{
    vec4 worldPos = uModel * vec4(aPosition, 1.0);
    vWorldPos = worldPos.xyz;
    vNormal = mat3(transpose(inverse(uModel))) * aNormal;
    vTexCoord = aTexCoord;
    gl_Position = uProjection * uView * worldPos;
}
// GLSL Fragment Shader
#version 330 core

in vec3 vNormal;
in vec2 vTexCoord;
in vec3 vWorldPos;

uniform vec3 uLightPos;
uniform vec3 uLightColor;
uniform vec3 uViewPos;
uniform sampler2D uTexture;

out vec4 fragColor;

void main()
{
    vec3 normal = normalize(vNormal);
    vec3 lightDir = normalize(uLightPos - vWorldPos);

    // Diffuse
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = diff * uLightColor;

    // Specular (Blinn-Phong)
    vec3 viewDir = normalize(uViewPos - vWorldPos);
    vec3 halfDir = normalize(lightDir + viewDir);
    float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
    vec3 specular = spec * uLightColor * 0.5;

    // Ambient
    vec3 ambient = 0.1 * uLightColor;

    vec4 texColor = texture(uTexture, vTexCoord);
    vec3 final = (ambient + diffuse + specular) * texColor.rgb;
    fragColor = vec4(final, texColor.a);
}

The vertex shader computes world-space position and normal. The fragment shader applies Blinn-Phong lighting with texture sampling. The transpose(inverse(uModel)) pattern transforms normals correctly under non-uniform scaling.

HLSL Unity Shader

Unity uses HLSL with a custom wrapper called ShaderLab. Here is the same Blinn-Phong shader in Unity's syntax.

Shader "Custom/BlinnPhong"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8, 256)) = 32
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float3 normal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float _Gloss;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                float3 halfDir = normalize(lightDir + viewDir);

                float diff = max(dot(i.normal, lightDir), 0);
                float spec = pow(max(dot(i.normal, halfDir), 0), _Gloss);

                float4 tex = tex2D(_MainTex, i.uv);
                float3 final = tex.rgb * _Color.rgb * (_GlossLightColor0.rgb * (diff + 0.1) + spec);
                return float4(final, tex.a);
            }
            ENDCG
        }
    }
}

The UnityObjectToClipPos and UnityObjectToWorldNormal are Unity-provided helper functions. The shader uses the scene's main directional light via _WorldSpaceLightPos0.

Post-Processing Edge Detection (GLSL)

Screen-space post-processing effects run after the scene is rendered. This Sobel edge detection pass highlights object boundaries.

#version 330 core

in vec2 vTexCoord;

uniform sampler2D uSceneTex;
uniform vec2 uScreenSize;

out vec4 fragColor;

void main()
{
    vec2 pixelSize = 1.0 / uScreenSize;

    float tl = length(texture(uSceneTex, vTexCoord + vec2(-1,  1) * pixelSize).rgb);
    float t  = length(texture(uSceneTex, vTexCoord + vec2( 0,  1) * pixelSize).rgb);
    float tr = length(texture(uSceneTex, vTexCoord + vec2( 1,  1) * pixelSize).rgb);
    float l  = length(texture(uSceneTex, vTexCoord + vec2(-1,  0) * pixelSize).rgb);
    float r  = length(texture(uSceneTex, vTexCoord + vec2( 1,  0) * pixelSize).rgb);
    float bl = length(texture(uSceneTex, vTexCoord + vec2(-1, -1) * pixelSize).rgb);
    float b  = length(texture(uSceneTex, vTexCoord + vec2( 0, -1) * pixelSize).rgb);
    float br = length(texture(uSceneTex, vTexCoord + vec2( 1, -1) * pixelSize).rgb);

    float gx = -tl - 2.0 * l - bl + tr + 2.0 * r + br;
    float gy = -tl - 2.0 * t - tr + bl + 2.0 * b + br;
    float edge = sqrt(gx * gx + gy * gy);

    vec4 color = texture(uSceneTex, vTexCoord);
    fragColor = mix(color, vec4(0.0, 0.0, 0.0, 1.0), edge);
}

The Sobel operator convolves the scene with 3x3 kernels to detect horizontal and vertical edges. Areas with high luminance change (edges) are darkened.

Practice Questions

  1. What is the difference between a vertex shader and a fragment shader in the Rendering Pipeline?
  2. Why do normals need to be transformed with the inverse transpose of the model matrix?
  3. How does Blinn-Phong differ from Phong shading in terms of specular calculation?

Frequently Asked Questions

What is the difference between GLSL and HLSL?

GLSL is used with OpenGL, WebGL, and Godot. HLSL is used with DirectX, Unity, and Unreal Engine. Syntactically, GLSL uses in/out qualifiers while HLSL uses semantics like SV_POSITION. The concepts (vertex/fragment stages, uniforms, textures) are identical.

How do I debug a shader that renders incorrectly?

Use fragColor = vec4(1,0,0,1) to force a red output and verify the shader compiles. Then simplify: output UV coordinates as colors (fragColor = vec4(uv, 0, 1)), normals as colors, then add lighting components one at a time.

Are compute shaders better than fragment shaders for post-processing?

Compute shaders offer more flexible memory access (shared memory, atomic operations) and better control over thread groups. For simple screen-space effects, fragment shaders are easier to write. For complex effects like bloom, depth of field, or global illumination, compute shaders are significantly faster.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro