Shaders and HLSL Programming Part 1 : Introduction

Part 1: Introduction - Part 2: Semantics - Part 3: Vector Types - Part 4: Intrinsics and Textures - Part 5: Blinn-Phong Shader

This first part of the tutorial will give a basic introduction to HLSL programming.

What does HLSL stand for?

First things first; HLSL stands for High-Level Shading Language. As opposed to Assembly shader programming, HLSL uses a C-like syntax and is compiled by Direct3D to the assembly version you specify. This is a direct advantage because :

  • Assembly is messy and error-prone;
  • The same HLSL code complied in different shader models gives very different Assembly code, so when using HLSL, knowing the specific limitations and capacities of each shader model and version is not necessary.

What is a shader?

A shader is a program written with a shading language. OpenGL uses GLSL, Direct3D uses HLSL, nVidia created Cg, and in all cases Assembly can be used.

The shader is basically a program that’s run by the GPU directly, and that’s executed for every vertex (Vertex Shader, or VS) or for every pixel (Pixel Shader, or PS) that a mesh occupies on the screen.

Because a mesh could occupy the whole screen, and then the Pixel Shader would be run for all the pixels forming the viewport, optimization is a serious issue in shaders. But all in good time... this is a topic by itself.

An simple example shader

// Global world-view-projection matrix
float4x4 matWorldViewProj : WORLDVIEWPROJECTION;
 
// Vertex Shader
float4 VS(float4 inPosition : POSITION) : POSITION {
    return mul(inPosition, matWorldViewProj);
}
 
// Pixel Shader
float4 PS() : COLOR {
    return float4(1, 1, 1, 1);
}
 
technique TSM1 {
    pass P0 {
        VertexShader = compile vs_1_1 VS();
        PixelShader  = compile ps_1_1 PS();
    }
}

First thing you’ll notice is that this is not C code, or Delphi, or any other language. HLSL is a language by itself, its syntax is inspired from C but there are a lot of differences.

Let’s take the different sections of the shader.

Global variables and parameters

The first line of the shader declares a variable that’s global to all functions. Its type is “float4x4”, which denotes a 4×4 matrix composed of floats. Its name is “matWorldViewProj”, this is arbitrary; could’ve been anything. And the last part, “: WORLDVIEWPROJECTION”, is what is called a Semantic. Basically, it’s a parameter that’s mapped by the framework/engine to the shader without having to assign it by hand. In this case, it’s the World-View-Projection matrix of the application. I’ll explain in more depth what is a semantic and how they’re used later on.

Functions

Functions are alot like C functions. They return a single value or a group of values, take a couple of parameters, and have a body which transforms the input into output.

Vertex shader functions

The name of the function is arbitrary as well, but I named it VS for clarity. This function is what will be run for each vertex. The main purpose of a vertex shader is to transform the vertices of the mesh from Object-Space to Clip-Space/Screen-Space. There are usually 3 transformations for that :

  • The first takes the vertex from Object-Space (vertex postion in the model, or at its creation) to World-Space (if the model was moved, rotated and scaled around)
  • The second takes the vertex from World-Space to View-Space (all the camera position and rotation transformations)
  • The final one takes the vertex from View-Space to Clip-Space, or Screen-Space (the perspective transform, which takes the 3D image to a 2D screen)

For convenience, there is a composed transformation which brings all of those together. Then, all that’s needed to do is take the input position, which is in Object-Space, and multiply it by the World-View-Projection matrix to get its Screen-Space position. The vertex shader must at LEAST output that position.

We’ll see how a function can return more than one value later, and why we’d want to do that.

Pixel shader functions

The pixel shader function does not take anything in parameter by “default”, and outputs an RGBA color at all times; this is the only output it can give, too.

In this very simple shader, every pixel forming the mesh is fully opaque and fully white. That is because the return value is constructed out of a 4-length float vector, whose values are r=1, g=1, b=1, a=1.

Technique blocks

The technique blocks are usually used to differenciate the different compilations you make to your HLSL code. For example, you could make a technique that compiles the code to Shader Model (SM) version 3.0, another to 2.0, and then 1.4 and 1.3 down to 1.1, then the video card would “fall-back” to the version it supports.

Passes are sections of a technique which can be used for multi-pass rendering. Some things, like a Bloom shader or an HDR shader, can’t be done in a single code pass to all vertices and pixels. In that case, you can seperate your code in different tasks that will be “chained” in your code to produce a final result. This will as well be explained later on.

The two lines in the pass block define which functions are the entry-point of the Vertex and Pixel shaders, and which shader version the code is compiled to. In this case, I chose to make an “universal” shader since it is awfully simple and compatible with all versions; SM1.1 is supported by all shader-capable video cards. We have to tell what are the functions for VS and PS, because there the VS/PS functions could call other functions for code re-use and separation.

The next part of this on-going tutorial is about semantics and how to use them, it can be found here.

 
tutorialsarticlesandexamples/programming_hlsl_shaders.txt · Last modified: 2013/11/22 13:32