Jan 15, 2014

[Intro to Shader] 03. Texture Mapping


Where to buy:
Amazon
iBooks

Source Code: GitHubZip

Chapter 3: Texture Mapping

New HLSL in this chapter

  • sampler2D: a texture sampler data type which is used to get a texel from a texture
  • tex2D(): a HLSL function to sample a texel from a texture
  • swizzling: a way to access the components of a vector in an arbitrary order

What did you think about what we covered in the last chapter? Too easy? It didn't seem that useful for the game you are trying to make? Yeah, you are right. The main goal of the last chapter was learning the basic syntax of HLSL through a simple practice. Just consider it as a hello-world program in other programming languages. Now, you are going to learn something more useful in this chapter. What about wrapping the red sphere with an image? You know this is called Texture Mapping, right?

Texture Mapping and UV Coordinates
As mentioned earlier in this book, the building blocks of a 3D object are triangles. Then, what’s involved to put an image, or a texture, on a triangle? We should order the GPU like this: “Show the pixel at the right-bottom corner of that image on the left vertex of this triangle”[1]  We all know that a triangle is made of three vertices, so all we need to do is mapping each of three vertices to a pixel in a texture. Then how do we specify one pixel on a texture? A texture is an image file after all, so can we just say something like “the pixel at x = 30, y = 101”? But, what happens if we doubles the width and height of the image? We will have to change it to “x = 60, y = 202”. This is not good, at all!

Let’s take a moment and think about a common sense that we learned in the last chapter. We did something very similar with the color representation. To represent a color in a uniform way regardless the number of bits per channel, we used the percentage notation [0~1]. So why don’t we just use the same method? Let’s say x = 0 points to the very left column of a texture, and x = 1 points to the very right column. Similarly, y = 0 is the top row and y = 1 is the bottom row. By the way, the UV notation is normally used instead of XY for texture mapping; there is no special reason, it’s just to avoid any confusion since XY is normally associated with positions. Figure 3.1 shows what we just discussed here:

Figure 3.1 UV layout on a texture

Now let’s see some examples of how different UV coordinates change the visuals. Please look at Figure 3.2.
Figure 3.2 Various examples of texture mapping

(a) 2 triangles with no texture. Vertices v0, v1, v2 and v0, v2, v3 are making up one triangle each.
(b) The range of UV coordinates is [0, 0] ~ [1, 1]. It shows a full texture.
(c) The range of UV coordinates is [0, 0] ~ [0.5, 1]. It shows only the left half of the texture. 0.5 means 50%, so it’s halfway, right?
(d) The range of UV coordinates is [0, 0] ~ [0.5, 0.5]. So it only shows the top left quarter of the image.
(e) The range of UV coordinates is [0, 0] ~ [1, 2]. It repeats the texture twice vertically. [2]
(f) The range of UV coordinates is [0, 0] ~ [2, 2]. It repeats the texture twice vertically and twice horizontally. [3]

Additionally, you can flip the texture horizontally if the range of UV coordinates is set to [1, 0] ~ [0, 1]. I believe it’s enough for you to understand how UV coordinates work. Then, it is about time to write Texture Mapping shader, finally!

Initial Step-by-Step Setup

  1. As we did in Chapter 2, open RenderMonkey to make a new DirectX effect. Then, delete all the code inside the vertex and pixel shaders.
  2. Now, change the name of shader to TextureMapping.
  3. Don’t forget to add gWorldMatrix, gViewMatrix and gProjectionMatrix variables that are needed to transform vertex positions. You still remember how to use variable semantics to pass the data, right?
  4. Next, we will add an image that is going to be used as the texture. Right-click on TextureMapping shader and select Add Texture > Add 2D Texture > [RenderMonkey installation folder]\examples\media\textures\earth.jpg. Now you will see a texture, named Earth, is added.
  5. Change the name of texture to DiffuseMap.
  6. Now, right-click on Pass 0 and select Add Texture Object > DiffuseMap. You should be able to see a newly added texture object, named Texture0.
  7. Change the name from Texture0 to DiffuseSampler.

Once you finish these steps, the Workspace panel should look like Figure 3.3.



Figure 3.3 RenderMonkey project after the initial setup


Vertex Shader
The full source code is listed below, followed by line-by-line explanation.

struct VS_INPUT
{
   float4 mPosition : POSITION;
   float2 mTexCoord : TEXCOORD0;
};

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float2 mTexCoord : TEXCOORD0;
};

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;


VS_OUTPUT vs_main(VS_INPUT Input)
{
   VS_OUTPUT Output;
   
   Output.mPosition = mul(Input.mPosition, gWorldMatrix);
   Output.mPosition = mul(Output.mPosition, gViewMatrix);
   Output.mPosition = mul(Output.mPosition, gProjectionMatrix);
   
   Output.mTexCoord = Input.mTexCoord;
   
   return Output;
}

Before walking through the vertex shader code, let’s take a moment and think about what kind of new data is needed to perform texture mapping. Obviously, we need an image, which is going to be used as the texture. Then where should we perform the actual texture mapping between the vertex and pixel shaders? If you think about where vertex and pixel shaders are executed, you can find the answer easily. A vertex shader is executed for each vertices, but where the texture will be shown? Is it on vertices? No, it’s not. We want to see the texture on all the pixels inside of a triangle, so texture mapping got to be performed inside the pixel shader, which is executed for each pixels. Then, now we know that it’s unnecessary to declare a texture variable inside of vertex shaders.

Then, is there any other information required for texture mapping? It was mentioned earlier in this chapter. Yes, you need the UV coordinates. Do you remember where the UV coordinates are stored? They are stored in vertex data since they can differ across vertices. Therefore, the UV coordinates are passed via vertex data instead of global variables. Now, with this knowledge, let’s take a look at the input and output data of the vertex shader.

Input Data to Vertex Shader
We start from the input data structure used in Chapter 2.

struct VS_INPUT
{
    float4 mPosition : POSITION;
};

We will add the UV coordinates to this structure. The UV coordinates have two components, U and V, so the data type should be float2. Then which semantic must be used to retrieve the UV information from the vertex buffer? Just like how the position information was retrieved via POSITION semantic, UV coordinates have their own semantic: TEXCOORD.[4] After adding the data field for UV to the structure, it looks like below:

struct VS_INPUT
{
    float4 mPosition : POSITION;
    float2 mTexCoord : TEXCOORD0;
};

The reason why the number 0 follows TEXCOORD is because multiple TEXCOORDs are supported by HLSL. There are cases where multiple textures are used in a shader. In those cases, you would use different semantics, such as TEXCOORD0, TEXCOORD1 and so on.

Output Data from Vertex Shader
Again, we start from the output structure used in Chapter 2.

struct VS_OUTPUT 
{
    float4 mPosition : POSITION;
};

Can you guess if we need to add another information here? One of those things that were not explained in Chapter 2 is that a vertex shader can return more than just the vertex position. The reason why a vertex shader must return a vertex position was to allow the rasterizer to find pixels. However, this is not the reason why a vertex shader returns information other than the position. It does so solely for the pixel shader, and a good example is the UV coordinates.

Pixel shaders cannot directly access the vertex buffer data. Therefore, any data that needs to be accessed by pixel shaders (e.g., UV coordinates) must be passed through vertex shaders. Does it feel like an unnecessary restriction? Once you look at Figure 3.4, you will understand why this restriction exists.


Figure 3.4 What would be the UV coordinates of this pixel?

Where the UV coordinates are defined is on each vertices, but as you can see in Figure 3.4, most pixels’ UV coordinates are different from any vertices UV coordinates. [5] Therefore, the right way of finding the correct UV coordinates of a pixel is smoothly blending the UV coordinates defined on three vertices based on the distance from the pixel to each vertices. Luckily, you do not have to do this calculation manually. Just like vertex positions, any other data is automatically handled by a device called interpolator. Let’s add the interpolator to the figure of a GPU pipeline presented in Chapter 1.

Figure 3.5 Still pretty simple 3D pipeline after adding the interpolator

By the way, this device doesn't stop at interpolating[6] the UV coordinates. It interpolates any data that is returned from vertex shaders and pass the result to pixel shaders.

By now, you should know that the UV coordinates need to be returned from this vertex shader. Let’s add the data field.

struct VS_OUTPUT 
{
    float4 mPosition : POSITION;
    float2 mTexCoord : TEXCOORD0;
};

Global Variables
We don’t need any extra global variables other than what we already used in Chapter 2. So, I’ll just show the code again and skip the explanation.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;

Vertex Shader Function
You heard it enough. The most important responsibility of a vertex shader is transforming vertex positions into the projection space. The below code is identical to the one used in Chapter 2.

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldMatrix );
   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );

Now, it’s time to pass through the UV coordinates, but do we need to apply any transformation before assigning the UV coordinates to Output structure? The answer is no. UV coordinates do not exist in any 3D spaces discussed in this book. Therefore, we will simply pass the UV coordinates without any transformation.

   Output.mTexCoord = Input.mTexCoord;

I cannot think of any other data that needs to be handled here, so I’ll finish this function by returning Output.

   return Output;
}

Pixel Shader
As done in Vertex Shader section, the full source code is listed first below:

sampler2D DiffuseSampler;

struct PS_INPUT
{
   float2 mTexCoord : TEXCOORD0;
};

float4 ps_main( PS_INPUT Input ) : COLOR
{
   float4 albedo = tex2D(DiffuseSampler, Input.mTexCoord);
   return albedo.rgba;
}

Input Data to Pixel Shader and Global Variables
It is time to look at the pixel shader. What we need to do here is retrieving a texel [7] from a texture image and output its color on the screen. Then, we need a texture and current pixels’ UV coordinates, right? A texture image is uniform for all the pixels, so it would be a global variable. Unlikely, the UV coordinates are part of the input data sent from the vertex shader and passed through the interpolator. First, let’s declare the input structure of the pixel shader.

struct PS_INPUT
{
   float2 mTexCoord : TEXCOORD0;
};

Wait. We saw something like this before. It is almost identical to the VS_OUTPUT structure except it is missing mPosition. In fact, the input structure of a pixel shader should match the output structure of its counter-part vertex shader. After all, the pixel shader is getting what is returned from the vertex shader, right?

The next step is texture declaration. Do you remember that we made a texture object named DiffuseSampler while setting up the RenderMonkey project earlier in this chapter? This object is the texture sampler and will be used to retrieve a texel. Therefore, the name of the texture sampler in HLSL must be DiffuseSampler, as well.

sampler2D DiffuseSampler;

sampler2D is another data type that is supported in HLSL, and is used to sample a texel from a 2D texture. There are also other samplers, such as sampler1D, sampler3D and samplerCUBE.

Now, we are ready to write the pixel shader function.

Pixel Shader Function
Let’s take a look at the function header first

float4 ps_main( PS_INPUT Input ) : COLOR
{

The only difference from previous pixel shader function headers is that it takes a parameter, and its type is PS_INPUT. This is to receive the UV coordinates the interpolator calculated for us. Equipped with the texture sampler and UV coordinates, we can get the value of the texel. A HLSL built-in function, tex2D() can do the magic. tex2D takes two parameters: a texture sampler and UV coordinates, in order.

    float4 albedo = tex2D(DiffuseSampler, Input.mTexCoord);

The above code reads a texel which is located at the coordinates which Input.mTexCoord specifies from DiffuseSampler. And the value will be stored in a variable named albedo. Now what do we do with this value? Well, we wanted to show the texture as is, so let’s just return it.

   return albedo.rgba;
}

If we press F5 to compile the vertex and pixel shaders and see the preview window…… uh… it’s messed up!
Figure 3.6 Something is messed up here!

Why? It is because that we forgot to map the UV coordinates element in the vertex buffer to the TEXCOORD semantic. To map it properly, go to Workspace panel and left-click on Stream Mapping. There is currently only one entry: POSITION. Now click on Add button to add a new entry, and then change Usage to TEXCOORD. Make sure Index is 0 and Data Type is FLOAT2. You do not need to change Attribute Name. Once you click on OK button, you will see a proper globe as shown in Figure 3.7.


Figure 3.7 A nice looking globe


By the way, did you notice that I used return albedo.rgba; instead of return albedo; while returning the final color? Although it is completely valid to use return albedo;, I intentionally did so to show you something new.

In HLSL, you can attach a postfix, such as .xyzw or .rgba to a vector variable to access the vector’s components with ease. For example, if we are dealing with a float4 variable, which has a four components, you can think it as an array of four floats. So if you add .x (or .r), it accesses the first component. Likewise, .y (or .g), .z (or .b) and .w (or .a) point to the second, third and fourth components, respectively. So, if you want to get only the rgb value from albedo, you would do something like this:

float3 rgb = albedo.rgb;

Neat, right? But it does not stop here. You can even change the order of the postfix to access vector components in an arbitrary order. Below example shows how to create a new vector with the same components, but in reverse order.

float4 newAlbedo = albedo.bgra;

Or you can even repeat only one channel three times like this:

float4 newAlbedo = albedo.rrra;

Pretty rad. We refer this technique, which allows us to access vectors’ components in any arbitrary order, swizzling.

Maybe you can do some practice here. How about switching the red and blue channels of the globe? Go ahead and try it. It should be a piece of cake for you. :-)

(Optional): DirectX Framework
This is an optional section for readers who want to use shaders in a C++ DirectX framework.

First, make a copy of the framework that we used in Chapter 2 into a new directory. Then, save the shader and 3D model into TextureMapping.fx and Sphere.x respectively so that they can be used in the DirectX framework. Also make a copy of earth.jpg texture file that we used in RenderMonkey. You can find this file in \Examples\Media\Textures folder from RenderMonkey installation folder.

First, let’s look at the global variables. In Chapter 2, we used gpColorShader variable for the shader. Change the name to gpTextureMappingShader:

// Shaders
LPD3DXEFFECT gpTextureMappingShader = NULL;

Also, we need to declare a texture pointer, which will be used to store the globe texture.

// Textures
LPDIRECT3DTEXTURE9 gpEarthDM = NULL;

Don’t forget to release D3D resources that we just declared. Go to CleanUp() function to do so. Doing so makes you a good programmer. You know that, right? ;) Also don’t forget to change the name of gpColourShader.

  // release shaders
  if (gpTextureMappingShader)
  {
    gpTextureMappingShader->Release();
    gpTextureMappingShader = NULL;
  }

  // release textures
  if (gpEarthDM)
  {
    gpEarthDM->Release();
    gpEarthDM = NULL;
  }

Now we will load the texture and shader. Of course, we do this in LoadAssets() function.

First, change the name of shader variable and file to gpTextureMappingShader and TextureMapping.fx, respectively.

  // loading shaders
  gpTextureMappingShader = LoadShader("TextureMapping.fx");
  if (!gpTextureMappingShader)
  {
    return false;
  }

Then, load earth.jpg file by using LoadTexture() function that we implemented earlier in this book.

  // loading textures
  gpEarthDM = LoadTexture("Earth.jpg");
  if (!gpEarthDM)
  {
    return false;
  }

Now go to RenderScene() function which takes care of all the drawings. There are multiple places where gpColorShader variable is used. Find and replace them all to gpTextureMappingShader.

There was a newly added global variable in the texture mapping shader, right? Yes, the texture sampler. But we can’t just assign the texture to the sampler directly in the D3D framework; instead, we have to assign it to a texture variable. Do you remember there was something called DiffuseMap? That was the texture variable. Then, you would think we should be able to assign the texture to a shader variable named DiffuseMap, right? Well that’s the most sensible thing to do, but guess what? RenderMonkey changed the texture variable’s name to something else. If you open TextureMapping.fx file in Notepad, you will see there’s only one variable which data type is texture, and apparently RenderMonkey added _Tex postfix to it. Bad, Bad Monkey!

texture DiffuseMap_Tex

Well, complaining does not solve anything. So we will just use this variable name. In order to pass a texture to a shader, we use SetTexture() function. Like SetMatrix() function, it takes the variable name in the shader as the first parameter.

     gpTextureMappingShader->SetTexture("DiffuseMap_Tex", gpEarthDM);

Now, compile and run the program. You should be able to see the same visual as RenderMonkey showed us. Hey! I have an idea. Why don’t we do something cooler? Let’s make it rotate! After all, it is the earth!

First, add a global variable which will remember the current rotation angle.

// Rotation around UP vector
float gRotationY = 0.0f;

The rotation and position of a 3D object are part of the world matrix. So, let’s go back to RenderScene() function and change the world matrix construction code like this:

  // for each frame, we rotate 0.4 degree
  gRotationY += 0.4f * PI / 180.0f;
  if (gRotationY > 2 * PI)
  {
    gRotationY -= 2 * PI;
  }

  // world matrix
  D3DXMATRIXA16 matWorld;
  D3DXMatrixRotationY(&matWorld, gRotationY);

The above code keeps adding 0.4 degree to the rotation each frame. Depending on the computer you are using, this might make the globe to rotate too fast or slow. Change the value appropriately. [8]

Run the code again. You can see the rotating earth, right?

Summary
A quick summary of what we learned in this chapter:

  • UV coordinates are required for texture mapping.
  • UV coordinates are varying values across vertices, thus defined on each vertex.
  • A pixel shader requires a vertex shader’s help to access vertex data.
  • Any data returned by a vertex shader goes through the interpolator.
  • tex2D() function is a magic function for texture sampling.

I cannot think of an advanced shading technique which doesn’t rely on texture mapping. So, texture mapping is very crucial in shader programming. Fortunately, performing a texture lookup is very easy with HLSL, so please practice it enough so that you can use it anytime!

Congratulations! You just finished texture mapping. Now take some break, and see you in Chapter 4. :D

----------------
Footnotes:

  1. You are basically mapping a pixel to a vertex.
  2. There are different ways of handling the UV coordinates outside 0~1 range. The current explanation is only valid when texture wrapping mode is used. Other modes, such as mirror and clamp, are also available.
  3. Again, this explanation is only correct with wrap mode.
  4. An abbreviation for texture coordinates.
  5. UV coordinates are same only when the positions of pixels are same as the vertices’.
  6. If you are having a hard time understanding this term just think it this way: it blends the values defined on three vertices. But by how much? Based on the distances to the vertices.
  7. As a pixel is the smallest element in a picture, a texel is the smallest element in a texture.
  8. For a real game, you would measure the elapsed time since the last frame and use it to calculate the proper rotation delta. This book’s code is definitely not ready for real-world applications. :P



Jan 7, 2014

[Intro to Shader] 02. Red Shader

Where to buy:
Amazon
iBooks

Source Code: GitHubZip

Chapter 2. It’s Really Easy! Red Shader

New HLSL in this chapter

  • float4: a vector data type with 4 components
  • float4x4: 4 X 4 matrix data type
  • mul(): multiplication built-in function. Can handle almost all data types
  • POSITION: Semantic for vertex position. Useful to read only the position info from vertex data

New math in this chapter

  • 3D space transformation: uses matrix multiplication

In the previous chapter, we defined shaders as functions that calculate the positions and colors of pixels. Then, we should try to write a shader that actually does the job, right? We are going to write a very simple shader here so that even readers with no shader programming experience can follow easily. What about a shader that draws a red sphere? [1] We are going to use RenderMoney for this and it will be your first time seeing any HLSL syntax. Yay! Excited? Once you write a shader in RenderMonkey, you can export it to a .FX file, which can be loaded directly into the DirectX framework we prepared in the last chapter.

Initial Step-by-Step Setup
Please follow these steps in order to start writing this shader

  1. Launch RenderMonkey. A quick scary-looking monkey will welcome you for a moment, and there will be an empty workspace. 
  2. Inside Workspace panel, click the right mouse button on Effect. You will see a pop-up menu.
  3. From the pop-up menu, select Add Default Effect > DirectX > DirectX, in order. Can you see a red sphere in the preview window?
  4. You will also see a new shader named Deafult_DirectX_Effect in Workspace panel. Change the name to ColorShader.

Now the screen should look like Figure 2.1.
Figure 2.1 Our RenderMonkey project after initial setup

Vertex Shader
Now, click on the plus(+) sign right next to ColorShader. Can you see Pass 0 at the very bottom? Again, click on the plus sign next to it and double-click on Vertex Shader. You will see the code for vertex shader in the shader editor window on the right. Well, this code is actually what we want: it draws a red ball! But we really need to practice, so let’s just delete all the code in it.

Did you delete the code? If so, let’s get it started! *plays music* First, I’ll show you the full source code for vertex shader below, and explain it line by line after.

struct VS_INPUT 
{
   float4 mPosition : POSITION;
};

struct VS_OUTPUT 
{
   float4 mPosition : POSITION;
};

float4x4 gWorldMatrix;         
float4x4 gViewMatrix;          
float4x4 gProjectionMatrix;    


VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldMatrix );
   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );
   
   return Output;
}

Global Variables vs Vertex Data
There are two types of input values for shaders: global variables and vertex data. Where to put your input values between these two totally depends on whether all vertices in a mesh can use a same value or not. If a same value is used, you should pass it through a global variable. Otherwise, you cannot use a global variable: you have to pass it as part of the vertex data. (i.e. vertex buffer). [2]

Good candidates for global variables include world matrix and camera position. On the other hand, the position and UV coordinates of each vertex are good examples of vertex data.

Input Data to Vertex Shader
First, we are going to declare vertex input data as VS_INPUT structure.

struct VS_INPUT
{
    float4 mPosition : POSITION;
};

Do you remember that the most important role of a vertex shader is transforming each vertex’s position from one space to another? It was mentioned in Chapter 1: What is Shader. To do so, you need the vertex position as your input. That is why the above structure retrieves the vertex position via the member variable, mPosition. The reason why this variable is able to retrieve the position info from a vertex buffer is because of POSITION semantic.[3] Often a vertex buffer contains many different data, such as the position, UV coordinates and normal vector, for each vertex. Semantics help you to extract only attributes that are meaningful to you.

So when you see float4 mPosition : POSITION;, it is actually an order to your graphics hardware saying  “extract the position information from my vertex data and assign it to mPosition!”

Oh, right. I almost forgot. Then what is float4? This is the variable’s data type. float4, a built-in type supported by HLSL, is nothing more than a vector that has four components: x, y, z and w. Of course, each component is a floating-point type as the type name suggests. HLSL also supports other data types, such as float, float2, float3. [4]

Output Data from Vertex Shader
Now that the input data for vertex shader is declared, we need to turn our eyes to output data. When something goes in, something else should come out, right? Do you still remember the super simplified diagram of a GPU pipeline from <<<Chapter 1? In that picture, vertex shader had to output transformed vertex positions so that rasterizer can figure out each pixel’s position from them. A key point here: a vertex shader must return a transformed vertex position! Okay, that was enough nagging. Now let’s declare the output data as a structure named VS_OUTPUT.

struct VS_OUTPUT 
{
    float4 mPosition : POSITION;
};

It looks very familiar, right? We are returning the position data as float4 type. How do you(and your GPU) know its position? You see the semantic, POSITION? Yes! That’s how.

Global Variables
We need to declare a number of global variables that will be used in the vertex shader, but doing so without understanding what space transformation is sounds dumb to me.

3D Space Transformation
I said that we need to transform vertex positions into different spaces to draw a 3D object on the monitor. Then, what kind of spaces should we go through to show it? Do you like apples? Let’s use an apple as an example.

Object Space
Let’s assume we have an apple in our left hand. The center of the apple is the origin point and from this point we can make 3 axes: one to the right direction (+x), one to the up direction (+y), and the other to the forward direction (+z). If you measure every single points on the surface of the apple, you can represent each points in (x, y, z) coordinates, right? Now you group every 3 points to build triangles. The result is an apple model!

Now we move the arm here and there while holding the apple. Even if the hand’s position is different, the distance from the origin to each points on the surface of the apple is same, right? This is the object space, or local space. In the object space, every objects (3D models) has its own coordinate system. If you think about it, it’s kind of neat. But if you want to handle different objects in a same manner, it’s a bit challenging because everyone has its own space! That is where the world space comes in.

Figure 2.2 An example of object space

World Space
Now why don’t you leave the apple right next to your monitor? The monitor is also an object, so it should have its own object space, too. Now we want to handle these two objects in a same manner. So what should we do? It’s simple. We just need to bring those two objects into a same space. To do so, we need to make a new space. Do you have a door in the room you are in now? (I really hope you do! :-P ) Let’s put an origin at the door and build 3 axes, +x, +y and +z, to right, up and forward directions, respectively. Now, from this origin, can you build new (x, y, z) coordinates for each vertices on the surface of the monitor? You should be able to do the same with the apple, too. We can call this space the world space.

Figure 2.3 An example of world space

View Space
Now, bring out your camera and take two pictures. Make sure the first picture has all these two objects in it, and the second picture doesn’t have any of those in it. These two pictures are totally different, right? In the first space, you can see the objects, while you can’t at all in the second picture. It means that there got to be positional difference, but positions of the objects in the world space didn’t change at all. A-ha! The camera must be using another space! We call this space view space. The origin of the view space is at the center of camera lens, and you can again make 3 axes to the right, up and forward directions.

Figure 2.4 An example of view space. Objects are inside of the camera’s view

Figure 2.5 An example of view space. Objects are outside of camera’s view.

Projection Space
When you see a picture that’s taken by your everyday cameras, far-away objects look smaller than ones close by the lens. Just like how you see the world through your eyes. Do you know why our eyes work this way? It’s because we humans have a field of view of roughly 100 degree horizontally and 75 degrees vertically. So you get to see more stuff as the distance increases but you are squeezing the “more stuff” into your fixed-size retinas. Your everyday camera does exactly same thing, but there is a special type of cameras called orthogonal cameras. These cameras don’t have field of views: instead, they always look straight forward. So if you use these cameras, you can get the consistent object sizes regardless of the distance.

Well, then we should be able to break down these photo-taking steps into two. First step is transforming objects from the world space to the camera space by applying scale, rotation and translation. Second step is projecting these objects onto a 2D image. (i.e., retina in the previous example) So can you distinguish the spaces we used for each of these two steps? Yes, they are view and projection spaces, respectively. With this separation, your view space is independent from the types of projections, such as orthogonal and perspective projections, you are using.

Once you apply the final projection transformation, the transformed result is the final image shown on the screen.

Summary
A usual way of transforming a vertex’s space in 3D graphics is matrix multiplication. The number of spaces that an object goes through in order to be displayed on the screen is three: world, view and projection spaces. So you need three matrices, as well. By the way, if you know any space’s origin and three axes, you can easily make a matrix that represents that space. [5]

K, now let’s sum it up. These are the space transformations that an object is going through:

Object --------> World --------> View ------> Projection
       ⅹWorld Mat      ⅹView Matrix    ⅹProjection Matrix

Since all these matrices are uniform for all the vertices in an object, global variables should be used.

Global Variable Declarations
Now you should have a very clear idea which global variables are needed, right? We need world, view and projection matrices in order to transform vertices. Let’s insert add the following three lines in the vertex shader code.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix; 

Hey, this is something new! float4x4! This is another data type supported by HLSL. It’s very straight forward, right? Yup, this represents a 4 X 4 matrix. As you can guess, there are also similar data types like float2x2 and float3x3.

Now we have these matrices declared. But, who is in charge of passing the values to these variables? Usually the graphics engine in a game takes care of this. But we are using RenderMonkey, so we should follow this monkey’s rule. RenderMonkey uses something called variable semantics to pass values to global variables.

Please follow these steps to set the values to the globals:

  1. In Workspace panel, find ColorShader and right-click on it.
  2. From the pop-up menu, select Add Variable > Matrix > Float(4x4) in order. It will add a new variable named f4x4Matrix.
  3. Change this variable’s name to gWorldMatrix.
  4. Now, right-click on gWorldMatrix to select Variable Semantic > World. This is how you pass a value to a variable in RenderMonkey.
  5. Repeat above steps to make view and projection matrices too. Name the variables as gViewMatrixand and gProjectionMatrix, respectively. Also don’t forget to assign variables semantics of View and Projection.
  6. Lastly, delete matViewProjection variable. This was added by default when we were adding the effect. We do not need this now because we are use gViewMatrix and gProjectionMatrix, instead.

If you finished all the above steps, your Workspace should look like Figure 2.6:

Figure 2.6 Workspace panel after assigning variable semantics

Vertex Shader Function
Finally, all prep works are done. It’s time to write the vertex shader function! First up! The function header!

VS_OUTPUT vs_main( VS_INPUT Input )
{

What the function header means are:

  • This function’s name is vs_main.
  • The name of the input parameter is Input and its type is VS_INPUT structure.
  • The return type of this function is VS_OUPUT structure.

This is not different from how you define a function in C, right? As mentioned before, HLSL uses a C-like syntax. Now, let’s look at the next line.

  VS_OUTPUT Output;

This is nothing more than declaring a variable of VS_OUTPUT type that we are going to return at the end of the function. Do you remember what the member of VS_OUTPUT structure was? There was only one member: mPosition, which is in the projection space. That means we are finally apply space transformations! First, we transform the object-space position, stored in Input.mPosition, to the world space. How do you transform a vertex? Yes! You multiply a matrix to it. Since the position vector is a float4, we should multiply a float4x4 matrix, right? Wait. You don’t need to flip through your math book to find a way to do this. HLSL already provides an almighty built-in mul() function that can multiply so many different types together. So, you can simply transform the position by calling this function like below:

  Output.mPosition = mul( Input.mPosition, gWorldMatrix );

Above code multiplies the world matrix, gWorldMatrix, to an object-space vertex position, Input.mPosition, and assign the result, which is the world-space position, to Output.mPosition. And you do almost exactly same thing to transform the position into the view and projection spaces.

  Output.mPosition = mul( Output.mPosition, gViewMatrix );
  Output.mPosition = mul( Output.mPosition, gProjectionMatrix );

Nothing complicated, right? Then what do we need to do now? Well, the most important role of a vertex shader is transforming a vertex’s position, which is originally in the object-space, to the projection space…. Um…. I think we just did it, right? Then, let’s just return the result to finish this vertex shader section.

  return Output;
}

Take a moment and press F5 key to compile the vertex shader. You see a red sphere, right? This means we finished the vertex shader section with a great success! If you see any compiler error, please review the code again to see if there is any mistake.

Tip: Got a Shader Compile Error?
If RenderMonkey fails at compiling your code because of typo or invalid syntax, you will see the error messages in the preview window. To see the details about the error, take a look at the output window at the very bottom of RenderMonkey. It should display detailed error messages as well as exact line and column numbers of where the problems are.

Pixel Shader
Now it’s time to write the pixel shader. As we did in the Vertex Shader section, find Workspace window and double-click on Pixel Shader. Now, please delete all the code in it. You should type code with your fingers to learn how to code, so please delete all the code in it.

Let’s take a look at the full source code, which is only 4-lines long, and then I’ll explain it line by line.

float4 ps_main() : COLOR
{   
  return float4( 1.0f, 0.0f, 0.0f, 1.0f );
}

The most important role of a pixel shader is returning a color value and we want to draw a red sphere in this chapter. So we can just return red here. But, here’s a question: how do you represent red in RGB values? If you are thinking RGB(255, 0, 0), you need to read the following section before writing any pixel shader code.

How to Represent a Color
The reason why most beginners think (255, 0, 0) for the RGB values of the red color is because we are so used to a 8-bit-per-channel format to store an image. An 8-bit integer can represent 256 distinct values. (2^8 = 256) So if you start from 0, you can make 256 integer numbers ranging from 0 to 255. Then, what happens if 5 bits are used per channel instead of 8? 2^5 equals to 32, so 31 will be the maximum value this time. This means that the red color is (255, 0, 0) in a 8-bit format, while it is (31, 0, 0) in a 5-bit format. What a bummer!

So now we know what the problem is. Then, is there any way to represent colors uniformly regardless how many bits are used per channel? If you have played with HDR images in an image editing software, such as Adobe Photoshop, you probably know the answer already. Yes. You can use percentage (%) notation. With this notation, the RGB values of the red color always become (100%, 0%, 0%). This is how shaders represent colors, too. Well almost. You know 0~100% is same as 0.0~1.0 right? So, shaders represent this color as RGB (1.0, 0.0, 0.0).

Pixel Shader Function
Now we know what RGB values need to be output from the pixel shader. So let’s write the function now. First is the function header:

float4 ps_main() : COLOR
{

What this line of code means are:

  • the function’s name is ps_main
  • this function doesn’t take any parameters
  • this function returns a float4
  • the return value will be treated as COLOR

One thing to note here is that float4 is used for the return type instead of float3. 4th component is the alpha channel, which is normally used for transparency effect. [6]

By the way, what did we say we need to do in this function? Oh right, we need to return red. The code should be as simple as this:

  return float4( 1.0f, 0.0f, 0.0f, 1.0f );
}

Two things worth mentioning here:

  • a color is encoded in a float4 vector in float4(r, g, b, a) form; and
  • the value of the alpha channel is 1.0, or 100%, so the pixel is completely opaque.

Now press the F5 key inside the shader editing window to compile vertex and pixel shaders. You will have to do it twice. Once for the vertex shader, and once for the pixel shader. Then as shown in Figure 2.7, you will see a red sphere in the preview window.

Tip: How to Compile a Shader in RenderMonkey
You need to compile vertex and pixel shaders separately in RenderMonkey. Open up each shaders in the shader editor and press F5. When the preview window is about to open, both shaders get compiled, too.

Figure 2.7 Our very first craft! So bloody red!

It was really simple, right? What if you want to show a blue ball instead of a bloody one? Returning float4(0.0, 0.0, 1.0, 1.0) would do it, right? What about green? What about yellow? Yellow is basically a mix of green and red, so…. Oh well, you should be smart enough to know. So, I will stop bothering you here. :)

Now, make sure to save this RenderMonkey project somewhere safe. Actually, save your RenderMonkey project at the end of every chapter because you will re-use them in the following chapters.

(Optional) DirectX Framework
This is an optional section for readers who want to use shaders in a C++ DirectX framework.

First, make a copy of the framework that we made in Chapter 1: What is Shader into a new directory. The reason why we make a copy of the framework for each chapter is because we will extend this framework for each chapter.

Next, it is time to save the shader and 3D model we used in RenderMonkey into files so that they can be used in the DirectX framework.

  1. From Workspace panel, find ColorShader and right-click on it.
  2. From the pop-up menu, select Export > FX Exporter.
  3. Find the folder where we saved the DirectX framework, and save the shader as ColorShader.fx.
  4. Again, from Workspace panel, right-click on Model.
  5. From the pop-up menu, select Save > Geometry Saver.
  6. Again, find the DirectX framework folder, and save it as Sphere.x.

Okay, now go ahead and open the framework’s solution file in Visual C++. We are going to add the following code in ShaderFramework.cpp file.

First, we will #define some constants that will be used for the projection matrix.

#define PI           3.14159265f
// Field of View
#define FOV          (PI/4.0f)
// aspect ratio of screen
#define ASPECT_RATIO (WIN_WIDTH/(float)WIN_HEIGHT)
#define NEAR_PLANE   1
#define FAR_PLANE    10000

Then we declare two pointers that will store Sphere.x and ColorShader.fx files after loading.

// Models
LPD3DXMESH gpSphere = NULL;

// Shaders
LPD3DXEFFECT gpColorShader = NULL;

Don’t you think it’s time to load the model and shader files now? We will add some code to LoadAssets() function that we left empty in <<<Chapter 1.

  // loading shaders
  gpColorShader = LoadShader("ColorShader.fx");
  if (!gpColorShader)
  {
    return false;
  }

  // loading models
  gpSphere = LoadModel("sphere.x");
  if (!gpSphere)
  {
    return false;
  }

To load files, the above code calls LoadShader() and LoadModel() functions, which were implemented in Chapter 1: What is Shader. If any of these results in a NULL pointer, it returns false, meaning “fail to load.” When this happens, there should be some error messages in the output window of Visual C++, so please take a look.

Whenever you load new resources, don’t forget to add code to release D3D resources, too. It is to prevent GPU memory leaks. Go to CleanUp() function and insert the following code right before releasing D3D.

  // release models
  if (gpSphere)
  {
    gpSphere->Release();
    gpSphere = NULL;
  }

  // release shaders
  if (gpColorShader)
  {
    gpColorShader->Release();
    gpColorShader = NULL;
  }

Alright,  it’s almost done. The last step is drawing the 3D object with our shader. We said we will put 3D drawing code inside RenderScene() function, right? Let’s go to RenderScene() function.

// draw 3D objects and so on
void RenderScene()
{

Do you remember we used some global variables in the shader? RenderMonkey used something called variable semantics to assign the values to these variables, but we don’t have that luxury in our framework. Instead, we have to construct those values and manually pass them to the shader. K, construction time! View matrix is first!

  // make the view matrix
  D3DXMATRIXA16 matView;
  D3DXVECTOR3 vEyePt(0.0f, 0.0f, -200.0f);
  D3DXVECTOR3 vLookatPt(0.0f, 0.0f, 0.0f);
  D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f);
  D3DXMatrixLookAtLH(&matView, &vEyePt, &vLookatPt, &vUpVec);

As shown in the above code snippet, a view matrix can be constructed with D3DXMatrixLookAtLH() function once we have these three information:

  • The position of a camera,
  • The position where the camera is looking at, and
  • The upward direction of the camera. (also known as the up vector)

In this chapter, we assume the camera’s current position is at (0, 0, -200) and is looking at the origin (0, 0, 0). In a real game, you would retrieve these information from your camera class.

Next is the projection matrix. Depending on projection techniques being used, we need to call different functions with different parameters. Remember there are two different projection techniques? Yes, perspective and orthogonal. We will use perspective projection here, so the function of choice is D3DXMatrixPerspectiveFOVLH(). [7]

  // projection matrix
  D3DXMATRIXA16 matProjection;
  D3DXMatrixPerspectiveFovLH(&matProjection, FOV, ASPECT_RATIO, NEAR_PLANE, FAR_PLANE);

Yay! One more matrix to go! World Matrix! A world matrix is combination of the following three properties of an object:

  • position,
  • orientation, and
  • scale

What this means is that each object should have its own world matrix. For this example, we assume the object is at the origin (0, 0, 0) without any rotation or scale, so we will just leave our matrix as an identity matrix.

  // world matrix
  D3DXMATRIXA16 matWorld;
  D3DXMatrixIdentity(&matWorld);

Now that we constructed all three global variables, we can pass these values to the shader. You can do this very easily by using the shader class’ SetMatrix() function. First parameter of SetMatrix() function is the name of variable in the shader, and the second is a D3DXMATRIXA16 variable declared above in the framework.

  // set shader global variables
  gpColorShader->SetMatrix("gWorldMatrix", &matWorld);
  gpColorShader->SetMatrix("gViewMatrix", &matView);
  gpColorShader->SetMatrix("gProjectionMatrix", &matProjection);

Once all necessary values are passed to the shader, it is time to order the GPU: “Use this shader for anything being drawn from now on!” To give this order, use Begin() / BeginPass() and EndPass() / End() functions. Any meshes drawn between BeginPass() and EndPass() calls will use the shader. Let’s look at the below code first.

  // start a shader
  UINT numPasses = 0;
  gpColorShader->Begin(&numPasses, NULL);
  {
    for (UINT i = 0; i < numPasses; ++i)
    {
      gpColorShader->BeginPass(i);
      {
        // draw a sphere
        gpSphere->DrawSubset(0);
      }
      gpColorShader->EndPass();
    }
  }
  gpColorShader->End();

Do you see DrawSubset() call which is wrapped by BeginPass() and EndPass() calls, which are again wrapped by Begin() and End() calls? This will make the GPU to draw gpSphere object with gpColorShader shader.

Some readers might wonder “Why is there BeginPass() function call after Begin()? What is a pass?” Confusing, right? Well, here’s a good news; you do not need to worry about it. Passes are only useful when you draw a same object multiple times at once, but we barely do this in real world, so let’s just ignore it for now. Just remember that the address of numPasses variable is passed to Begin() function to get the number of passes that exist in the shader. Most of the time, the number is 1. If there are two or more passes in the shader, that means there are more than one vertex/pixel shader pairs, too. So you just need to call BeginPass() / EndPass() as many times as the number of passes.

Now, compile and run the program. You will see the exact same red sphere that you saw in RenderMonkey.

Summary
A quick summary of what we learned in this chapter:

  • Per-vertex data is passed as member variables in vertex data.
  • Shared data between all vertices is passed as global variables.
  • HLSL provides vector-operation-friendly data types, such as float4 and float4x4.
  • When transforming vertices into different spaces, matrices are used. To multiply a matrix to a vector, use HLSL intrinsic function, mul().
  • HLSL represents a color in a normalized form. [0 ~ 1]


What we learned here is the basics of basics. So unless you can write this simple shader effortlessly, you shouldn’t attempt to write other shaders. While I was teaching at a college, I saw some students who did not bother to write this red shader because they thought it was too easy, but later they had hard times with other shaders. It was not because the other shaders were hard, but because those students failed to learn very basic HLSL syntax with this red shader. Therefore, please take your time to write this simple shader once or twice before moving to the next chapter.

------
Footnotes:


  1. You will be surprised to see how often graphics programmers use this one-color shader for debugging purposes.
  2. Not entirely true. Textures can be used for this, too.
  3. What the heck is a semantic? Just think it as a tag.
  4. By the way, GPUs are optimized to handle floating-pointing vectors. So, don’t worry about using floats over ints. floats are often faster than ints on GPUs.
  5. For more details on how to manually construct these matrices, please refer to your 3D math book.
  6. If this value is 1, the pixel is opaque, and 0 means the pixel is completely transparent.
  7. Please use D3DXMatrixOrthoLH() for orthogonal projection.