Fundamentals of Unity Shader Development and GPU Rendering Pipeline

Graphics APIs and Shading Languages

Rendering APIs like OpenGL and DirectX provide the necessary interface for rendering 2D and 3D graphics. GLSL is the shading language tailored for OpenGL, boasting excellent cross-platform compatibility across Windows, Linux, macOS, and mobile platforms. Conversely, HLSL is heavily tied to Microsoft's ecosystem, primarily functioning on Windows and Xbox, lacking compilers for other environments. Cg, developed by Nvidia, bridges this divide by functioning as a truly cross-platform shading language.

GPU Rendering Pipeline Stages

1. Vertex Processing

As the initial pipeline stage, the vertex shader receives input directly from the CPU. It operates on a per-vertex basis, executing once for every vertex provided. Its core responsibilities include coordinate transformations and per-vertex lighting calculations. It can also output custom data required by subsequent stages. The minimal requirement for this stage is transforming vertex coordinates from local model space into homogeneous clip space.

2. Primitive Clipping

Since camera frustums cannot always encompass an entire massive scene, geometry outside the view volume is discarded to save resources. A primitive's relationship to the camera falls into three categories: entirely inside, partially inside, or entirely outside. Primitives partially inside require clipping; for instance, if a line segment has one vertex inside and one outside, the external vertex is replaced by a new vertex generated at the intersection of the line and the frustum boundary.

3. Screen Space Mapping

At this point, coordinates remain in 3D. Screen mapping converts the x and y components of primitives into a 2D screen coordinate system tied to the display resolution. These screen coordinates dictate which specific pixel a vertex corresponds to and its relative depth. Note that OpenGL anchors the screen origin at the bottom-left, whereas DirectX places it at the top-left—a discrepancy that often causes flipped renders if overlooked.

4. Triangle Setup and Traversal

These stages mark the beginning of rasterization. The setup phase computes the necessary boundary equations for a triangle based on vertex endpoints. The traversal phase, sometimes called scan conversion, scans these boundaries to determine which pixels fall inside the triangle. Covered pixels generate fragments, and the data from the three triangle vertices is interpolated across these fragments.

5. Fragment Processing

This programmable stage consumes the interpolated data produced by rasterization. Its primary output is one or more color values. A fundamental technique executed here is texture sampling; UV coordinates passed from the vertex shader are interpolated across the primitive, allowing the fragment shader to map texture pixels to fragments accurately.

6. Fragment Tests and Blending

Referred to as the Output Merger stage in DirectX, this phase dictates final visibility via depth and stencil tests. If a fragment passes all evaluations, its resulting color is blended with the existing color residing in the framebuffer.

Unity Shader Paradigms

  • Surface Shaders: A higher-level abstraction encapsulating Vertex/Fragment logic for simplified lighting interactions.
  • Vertex/Fragment Shaders: Granular control over the rendering pipeline.
  • Fixed Function Shaders: Deprecated legacy syntax used for outdated hardware limitations.

Unity Shader Architecture

Shader "Custom/IntroShader"
{
    Properties { /* Material inputs defined here */ }
    SubShader
    {
        // Multiple SubShaders can exist; Unity evaluates them top-down
        // based on hardware capability.
        Pass
        {
            // Rendering logic implemented using HLSL/CG
            CGPROGRAM
            // Shader code...
            ENDCG
        }
    }
    Fallback "Diffuse" // Default fallback if SubShaders fail
}

Properties and Variable Mapping

Declaration Block

_BaseTint("Main Color", Color) = (1,1,1,1)
_CustomDir("Direction Vector", Vector) = (1,2,3,4)
_SampleInt("Integer Value", Int) = 2
_SampleFloat("Float Value", Float) = 12.3
_SampleRange("Clamped Value", Range(0.0, 10.0)) = 1.0
_MainTex("Albedo Map", 2D) = "white" {}
_EnvCube("Skybox Cube", Cube) = "red" {}
_VolTex("Volume Data", 3D) = "black" {}

Variable Implementation

float4 _BaseTint;
float4 _CustomDir;
float _SampleInt;
float _SampleRange;
sampler2D _MainTex;
samplerCUBE _EnvCube;
sampler3D _VolTex;

Precision Qualifiers

  • float: 32-bit precision, standard for complex calculations.
  • half: 16-bit precision, range of roughly -60,000 to 60,000.
  • fixed: 11-bit precision, ideal for simple color operations on mobile.

Vertex and Fragment Functions

Vertex Program

#pragma vertex VertMain

float4 VertMain(float4 localPos : POSITION) : SV_POSITION
{
    // Transform local model space to homogeneous clip space
    return UnityObjectToClipPos(localPos);
}

Fragment Program

#pragma fragment FragMain

fixed4 FragMain() : SV_Target
{
    // Output a solid white pixel
    return fixed4(1, 1, 1, 1);
}

Data Structures and Inter-Stage Communication

Structs streamline parameter passing between stages. Certain attributes like normals are strictly accessible in the vertex stage; utilizing structs ensures this data propagates correctly to the fragment stage.

Shader "Custom/StructPipelineShader"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex VertMain
            #pragma fragment FragMain

            struct AppData
            {
                float4 pos : POSITION;
                float3 norm : NORMAL;
                float2 uv : TEXCOORD0;
            }; 

            struct Varyings
            {
                float4 clipPos : SV_POSITION;
                float3 interpolatedNorm : COLOR0;
            }; 

            Varyings VertMain(AppData input)
            {
                Varyings output;
                output.clipPos = UnityObjectToClipPos(input.pos);
                output.interpolatedNorm = input.norm;
                return output;
            }

            fixed4 FragMain(Varyings input) : SV_Target
            {
                // Visualize the interpolated normal direction as a color
                return fixed4(input.interpolatedNorm, 1.0);
            }
            ENDCG
        }
    }
    Fallback "VertexLit"
}

Fragment computations yield superior visual fidelity compared to vertex computations because fragments are evaluated per-pixel, producing finer detail despite higher performance overhead.

Common Semantic Keywords

Application to Vertex (AppData)

  • POSITION: Local model space vertex coordinates.
  • NORMAL: Local model space normal direction.
  • TANGENT: Local model space tangent vector.
  • TEXCOORD0: Primary UV mapping coordinates.
  • COLOR: Per-vertex color attribute.

Vertex to Fragment (Varyings)

  • SV_POSITION: Clip space coordinates (consumed internally for rasterization).
  • COLOR0 / COLOR1: Custom interpolated float4 data sets.
  • TEXCOORD0~7: Interpolated UV or general data channels.

Fragment to Framebuffer

  • SV_Target: Final pixel color destination for the render target.

Tags: Unity3D shader GPU Pipeline HLSL CG

Posted on Sun, 07 Jun 2026 17:14:42 +0000 by cwscribner