Introduction

This article will introduce you to the Direct3D 12 graphics pipeline and HLSL Shader Model 5. There are 5 main shader types; Vertex, Hull, Domain, Geometry and Pixel. This is the graphics pipeline of Direct3D 12 displaying in what order the shaders are applied.

Simplified Graphics Pipeline

Pipeline State

Before I can start explaining the different stages of the graphics pipeline I need to explain the pipeline state object (PSO). The IA, VS, HS, DS, GS, PS and OM are stored in a PSO. A PSO is a unified pipeline state object that is immutable after creation. The currently selected PSO can be changed quickly and dynamically, and the hardware and drivers can directly convert a PSO into native hardware instructions and state, readying the GPU for graphics processing. To apply a PSO, the hardware copies a minimal amount of pre-computed state directly to the hardware registers. The currently applied PSO defines and connects all of the shaders being used in the rendering pipeline. HLSL shaders can be precompiled into a shader object or compiled at runtime for debugging purposes.

Input Assambler

The input assambler reads the buffers created by the user to create primitives for other stages. The IA can can assemble vertices into several different Primitive Types.

Adjacency information available in the geometry shader. If a geometry shader were invoked with a triangle including adjacency, for instance, the input data would contain 3 vertices for each triangle and 3 vertices for adjacency data per triangle.

The secondary purpose of the Input Assambler is to attach system-generated values to help shaders to be faster. System-generated values are also called semantics. All three shader stages are constructed from a common shader core, and the shader core uses system-generated values (such as a primitive id, an instance id, or a vertex id) so that a shader stage can reduce processing to only those primitives, instances, or vertices that have not already been processed.

Vertex Shader

The vertex shader takes the data received from the IA (vertices and indices) and GPU buffers and modifies the data. We use this to create effects such as transformations, lighting and displacement mapping.

Hull and Domein Shader

The hull and domein shader are both part of the GPU’s Tesselessation process. These shaders are generally used to create high detail geometry from “Patches” (Low detail geometry). The hull shader takes an input patch and returns an output patch. The ouput of the hull shader runs trough the Tessellesation stage which produces domains.

The domein shader is takes those domains in order to compute the actual vertex position.

Geometry Shader

Unlike vertex shaders, which operate on a single vertex, the Geometry shader inputs are the vertices for a full primitive. For example the input primitive can be expanded into one or more other primtives. Earlier I said I won’t go into the Stream Output Stage since that is quite advanced but i’ll mention it here brievly: The geometry shader can stream-out vertex data into a buffer which can later be drawn. A example use can be things like grass, geometry tessellation and volumatric shadows.

Pixel Shader

The pixel shader is the final stage of the pipeline before we merge everything together. This shader is executed for every pixel fragment and is used to determine the color of said pixel. This can return a constant output or something more advanced like per-pixel lighting, reflections and shadows.

Output Merger

This stage may reject some of the pixel fragments from the pixel shader thanks to the depth. stencil and depth tests and render targets. The remaining pixels are drawn to the back buffer. Blending is also done in this stage.

Phong Shading

Phong shading is interpolation tecnique for surface rendering. Specificly it interpolates surface normals. Phong shading improves upon Gouraud Shading by providing better smoothing. But it does come at a performance cost. Phong shading can be stripped down to 3 parts: Ambient diffuse and specular. When we have calculated those 3 parts we can combine them. The result will look something like this:

Phong shading example

Ambient

The ambient light is generally callculated with or without direction. So the ambient value will be quite easy to calculate::

Pixel Shader

float4 ambient_color = float4(1.f, 1.f, 1.f, 1.f);
float ambient_strength = 0.1f;

float4 ambient = ambient_color * ambient_strength;

Diffuse

Diffuse lighting gives the object more brightness the closer the fragments are aligned to the light rays from a light source. This image will explain it better than I can:

Diffuse Lighting Visualization

This images shows a light ray toutching a single fragment of a mesh. As you can see we need to calculate the angle of the light ray. To do this we use a normal vector. This can be calculated with the dot product. So there are 4 things we need for diffuse lighting, A normal vector, diffuse color, The fragmant’s position and a light direction. We can calculate the fragmant’s position by multiplying the model matrix with the vertex position. Now we have the fragment’s position we can also calculate the direction of the light by substracting the fragment’s position from the position of the light and normalizing the output since we want a directional vector.

Now we have all the nessessary calculations we can finally calculate the actual diffuse effect. We do this by taking the dot product of the normal and the light’s direction. Now we need to clamp the diffuse value to a minimum of 0 to prevent the diffuse effect to become negative when both the normal and light direction is greater than 90 degrees.

Pixel Shader

float3 light_dir = normalize(light_pos - input.fragment_pos);  
input.normal = normalize(input.normal);
float diffuse_value = max(dot(norm, light_dir), 0.0);

float4 diffuse = diffuse_value * diffuse_color;

Specular

Just like diffuse lighting specular lighting is based on the light’s direction vector and the object’s normal. But now we will also need a view direction. We can think of specular lighting as a mirror. The light will be reflected in the material.

For the calculation we will use a specuilar strength variable, A shinyness variable which ranges from 2 to 256. The shinyness determines the scattering of the light. This image will show the result when changing the shinyness of the material:

Different Shinyness

Now lets get to the code and calculation. First we need a view direction and a reflection direction as mentioned above.

Now we calculate the specular vector by first calculating the dot product between the view direction and the reflect direction. Clamp it for the same reason as we clamped the diffuse value. And than multiply the outcome by the sinyness value.

And finally we multiply the specular value by the specular strength and light value to get the specular vector3.

float3 view_dir = normalize(frag_pos - view_pos);
float3 reflect_dir = reflect(light_dir, norm);

float specular_value = pow(max(dot(view_dir, reflect_dir), 0.0), shinynessss);
float3 specular = specular_strength * specular_value * light_color;  

Rounding up

Now we just need to combine all the calculated vectors.

// Calculate Phong
float4 phong = ambient + diffuse + specular;
return phong;