DX 10 TUTORIAL Tutorials The following tutorials demonstrate the basic functionality of Direct3D 10: Basic Tutorials DXUT Tutorials Shader Tutorials S...
19 downloads
28 Views
330KB Size
DX 10 TUTORIAL Tutorials The following tutorials demonstrate the basic functionality of Direct3D 10: Basic Tutorials DXUT Tutorials Shader Tutorials State Tutorials The sample code in these tutorials is from source projects whose location is provided in each tutorial. The sample files in these tutorials are written in C++. If you are using a C compiler, you must make the appropriate changes to the files for them to successfully compile. At the very least, you need to add the vtable and pointers to the interface methods. Basic Tutorials The following tutorials were designed to teach a programmer the basics of the Direct3D 10 API. They cover basic setup and procedures to accomplish the fundamental tasks, such as rendering and transformations. The intended audience is those familiar with programming, but relatively new to 3D graphics programming. The beginning tutorials will be a walkthrough to create the bare framework for Direct3D, as well as serve to explain simple concepts behind 3D rendering, such as vertices, transformations, and animations. Experienced users migrating to Direct3D 10 can skip over the explanations and observe the changes made to this iteration of the API. Tutorial 0: Win32 Basics Tutorial 1: Direct3D 10 Basics Tutorial 2: Rendering a Triangle Tutorial 3: Shaders and Effect System Tutorial 4: 3D Spaces Tutorial 5: 3D Transformation Tutorial 06: Lighting Tutorial 7: Texture Mapping and Constant Buffers
Tutorial 0: Win32 Basics
Summary In this preliminary tutorial, we will go through the elements necessary to create a Win32 application. We will be setting up an empty window to prepare for Direct3D 10. Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial00 Setting Up The Window Every Windows application requires at least one window object. Before even getting to the Direct3D 10 specifics, our application must have a working window object. Three things are involved: 1. Register a window class. // // Register class // WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_TUTORIAL1); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = NULL; wcex.lpszClassName = szWindowClass; wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_TUTORIAL1); if( !RegisterClassEx(&wcex) ) return FALSE; 2. Create a window object. // // Create window // g_hInst = hInstance; // Store instance handle in our global variable RECT rc = { 0, 0, 640, 480 }; AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE ); g_hWnd = CreateWindow( szWindowClass, L"Direct3D 10 Tutorial 0: Setting Up Window", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hInstance, NULL); if( !g_hWnd ) return FALSE; ShowWindow( g_hWnd, nCmdShow ); 3. Retrieve and dispatch messages for this window. // // Main message loop // MSG msg = {0}; while( GetMessage( &msg, NULL, 0, 0 ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ) { PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_PAINT: hdc = BeginPaint(hWnd, &ps); EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } These are the minimal necessary steps to set up a window object that are required by every Windows application. If we compile and run this code, we will see a window with a blank white background.
Tutorial 1: Direct3D 10 Basics
Summary In this first tutorial, we will go through the elements necessary to create a minimal Direct3D 10 application. Every Direct3D 10 application must have these elements to function properly. The elements include setting up a window and a device object then displaying a color on the window. Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial01 Setting Up The Direct3D 10 Device Now that we have a window displaying, we can continue to set up a Direct3D 10 device, which is necessary if we are going to render any 3D scene. The first thing to do is to create two objects: a device and a swap chain. The device object is used by the application to perform rendering onto a buffer. The device also contains methods to create resources. The swap chain then is responsible for taking the buffer that the device renders to and displaying the content on the actual monitor screen. The swap chain contains two or more buffers, mainly the front and the back. These are textures that the device renders to, for displaying on the monitor. The front buffer is what the user currently is looking at. This buffer is read-only and cannot be modified. The back buffer is the render target that the device will draw to. Once it finishes the drawing operation, the swap chain will present the backbuffer, by swapping the two buffers. The back buffer becomes the front buffer, and vice versa. To create the swap chain, we fill out a DXGI_SWAPCHAIN_DESC structure that describes the swap chain we are about to create. A few fields are worth mentioning. BackBufferUsage is a flag that tells the application how the back buffer will be used. In this case, we want to render to the back buffer, so we'll set BackBufferUsage to DXGI_USAGE_RENDER_TARGET_OUTPUT. The OutputWindow field represents the window that the swap chain will use to present images on the screen. SampleDesc is used to enable multi-sampling. Since this tutorial does not use multisampling, SampleDesc's Count is set to 1 and Quality to 0 to have multi-sampling disabled. Once the description has been filled out, we can call the function to create both the device and the swap chain for us. The code to create a device and a swap chain is listed below: DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory( &sd, sizeof(sd) ); sd.BufferCount = 1; sd.BufferDesc.Width = 640; sd.BufferDesc.Height = 480; sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferDesc.RefreshRate.Numerator = 60; sd.BufferDesc.RefreshRate.Denominator = 1; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow = g_hWnd; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.Windowed = TRUE; if( FAILED( D3D10CreateDeviceAndSwapChain( NULL, D3D10_DRIVER_TYPE_REFERENCE, NULL, 0, D3D10_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice ) ) ) { return FALSE; } The next thing that we need to do is to create a render target view. A render target view is a type of resource view in Direct3D 10. A resource view allows a resource to be bound to the graphics pipeline at a specific stage. Think of resource views as typecast in C. A chunk of raw memory in C can be cast to any data type. We can cast that chunk of memory to an array of integers, an array of floats, a structure, an array of structures, so on. The raw memory itself is not very useful to us if we don't know its type. Direct3D 10 resource views act in a similar way. For instance a 2D texture, analogous to the raw memory chunk, is the raw underlying resource. Once we have that resource we can create different resource views to bind that texture to different stages in the graphics pipeline with different formats: as a render target to render to, as a depth stencil buffer that will receive depth information, or as a texture resource. Where C typecasts allow a memory chunk to be used in different manner, so do Direct3D 10 resource views. We need to create a render target view because we would like to bind the back buffer of our swap chain as a render target, so that Direct3D 10 can render onto it. We first call GetBuffer() to obtain the back buffer object. Optionally, we can fill in a D3D10_RENDERTARGETVIEW_DESC structure that describes the render target view to be created. This description is normally the second parameter to CreateRenderTargetView. However, for these tutorials, the default render target view will suffice. The default render target view can be obtained by passing NULL as the second parameter. Once we have created the render target view, we can call OMSetRenderTargets() to bind it to the pipeline so that the output that the pipeline renders gets written to the back buffer. The code to create and set the render target view is listed below: // Create a render target view ID3D10Texture2D *pBackBuffer; if( FAILED( g_pSwapChain->GetBuffer( 0, __uuidof( ID3D10Texture2D ), (LPVOID*)&pBackBuffer ) ) ) return FALSE; hr = g_pd3dDevice->CreateRenderTargetView( pBackBuffer, NULL, &g_pRenderTargetView ); pBackBuffer->Release(); if( FAILED( hr ) ) return FALSE; g_pd3dDevice->OMSetRenderTargets( 1, &g_pRenderTargetView,
NULL ); The last thing that we need to set up before Direct3D 10 can render is initialize viewport. The viewport maps clip space coordinates, where X and Y range from -1 to 1 and Z ranges from 0 to 1, to render target space, sometimes known as pixel space. In Direct3D 9, a default viewport is set up to be the same size as the render target if the application does not set up any. In Direct3D 10, no viewport is set by default. Therefore, we must do so before we can see anything on the screen. Since we would like to use the entire render target for the output, we set the top left point to (0, 0) and width and height to be identical to the render target's size. The code to do so is shown below: D3D10_VIEWPORT vp; vp.Width = 640; vp.Height = 480; vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; vp.TopLeftX = 0; vp.TopLeftY = 0; g_pd3dDevice->RSSetViewports( 1, &vp ); Modifying the Message Loop We have set up the window and Direct3D 10 device, and we are ready to render. However, there is still a problem with our message loop: it uses GetMessage() to obtain messages. The problem with GetMessage() is that if there is no message in the queue for the application window, GetMessage() blocks and does not return until a message is available. Thus, instead of doing something like rendering, our application is waiting within GetMessage() when the message queue is empty. We can solve this problem by using PeekMessage() instead of GetMessage(). PeekMessage() can retrieve a message like GetMessage() does, but when there is no message waiting, PeekMessage() returns immediately instead of blocking. We can then take this time to do some rendering. The modified message loop, which uses PeekMessage(), looks like this: MSG msg = {0}; while( WM_QUIT != msg.message ) { if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } else { Render(); // Do some rendering } } The Rendering Code Rendering is done in the Render() function. In this tutorial, we will render the simplest scene possible, which is to fill the screen with a single color. In Direct3D 10, an easy way to fill the render target with a single color is to use the device's ClearRenderTargetView() method. We first define a D3D10_COLOR structure that describes the color we would like to fill the screen with, then pass it to ClearRenderTargetView(). In this example, a shader of blue is chosen. Once we have filled our back buffer, we call the swap chain's Present() method to complete the rendering. Present() is
responsible for displaying the swap chain's back buffer content onto the screen so that the user can see it. The Render() function looks like below: void Render() { // // Clear the backbuffer // float ClearColor[4] = { 0.0f, 0.125f, 0.6f, 1.0f }; // RGBA g_pd3dDevice->ClearRenderTargetView( g_pRenderTargetView, ClearColor ); g_pSwapChain->Present( 0, 0 ); }
Tutorial 2: Rendering a Triangle
Summary In the previous tutorial, we built a minimal bare bone Direct3D 10 application that outputs a single color to the window. In this tutorial, we will extend the application to render a single triangle on the screen. We will go through the process to setup the data structures associated with a triangle. The outcome of this tutorial is a window with a triangle rendered to the center of it. Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial02 • • •
Elements of a Triangle Input Layout Rendering the Triangle
Elements of a Triangle A triangle is defined by its three points, also called vertices. A set of three vertices with unique positions define a unique triangle. In order for GPUs to render a triangle, we must tell it about the position of three vertices. For a 2D example, let's say we wish to render a triangle such as that in figure 1. We would pass three vertices with the positions (0, 0) (0, 1) and (1, 0) to the GPU, and then the GPU has enough information to render the triangle that we want. Figure 1. A triangle in 2D defined by its three vertices
So now we know that we must pass three positions to the GPU in order to render a triangle. How do we pass this information to the GPU? In Direct3D 10, vertex information such as position is stored in a buffer resource. A buffer that is used to store vertex information is called, not surprisingly, vertex buffer. We must create a vertex buffer large enough for three vertices and fill it with the vertex positions. In Direct3D 10, the application must specify a buffer size in bytes when creating a buffer resource. We know the buffer has to be large enough for three vertices, but how many bytes does each vertex need? To answer that question requires an understanding of vertex layout. Input Layout A vertex has a position. More often than not, it also has other attributes as well, such as a normal, one or more colors, texture coordinates (used for texture mapping), and so on. Vertex layout defines how these attributes lie in memory: what data type each attribute uses, what size each attribute has, and the order of the attributes in memory. Because the attributes usually have different types, similar to the fields in a C structure, a vertex is usually represented by a structure. The size of the vertex is conveniently obtained from the size of the structure. In this tutorial, we are only working with the position of the vertices. Therefore, we define our vertex structure with a single field of the type D3DXVECTOR3. This type is a vector of 3 floatingpoints components, which is typically the data type used for position in 3D. struct SimpleVertex { D3DXVECTOR3 Pos; };
// Position
We now have a structure that represents our vertex. That takes care of storing vertex information in system memory in our application. However, when we feed the GPU the vertex buffer containing our vertices, we are just feeding it a chunk of memory. The GPU must also know about the vertex layout in order to extract correct attributes out from the buffer. To accomplish this requires the use of input layout. In Direct3D 10, an input layout is a Direct3D object that describes the structure of vertices in a way that can be understood by the GPU. Each vertex attribute can be described with the D3D10_INPUT_ELEMENT_DESC structure. An application defines one or more D3D10_INPUT_ELEMENT_DESC, then uses that array to create the input layout object which describes the vertex as a whole. We will now look at the fields of D3D10_INPUT_ELEMENT_DESC in details.
SemanticName
SemanticIndex
Format
Semantic name is a string containing a word that describes the nature or purpose (or semantics) of this element. The word can be in any form that a C identifier can, and can be anything that we choose. For instance, a good semantic name for the vertex's position is POSITION. Semantic names are not case-sensitive. Semantic index supplements semantic name. A vertex may have multiple attributes of the same nature. For example, it may have 2 sets of texture coordinates or 2 sets of colors. Instead of using semantic names that have numbers appended, such as "COLOR0" and "COLOR1", the two elements can share a single semantic name, "COLOR", with different semantic indices 0 and 1. Format defines the data type to be used for this element. For instance, a format of DXGI_FORMAT_R32G32B32_FLOAT has three 32-bit floating
point numbers, making the element 12-byte long. A format of DXGI_FORMAT_R16G16B16A16_UINT has four 16-bit unsigned integers, making the element 8-byte long. As mentioned previously, a Direct3D 10 application passes vertex data to GPU via the use of vertex buffer. In Direct3D 10, multiple vertex buffers InputSlot can be fed to the GPU simultaneously, 16 to be exact. Each vertex buffer is bound to an input slot number ranging from 0 to 15. The InputSlot field tells the GPU which vertex buffer it should fetch for this element. A vertex is stored in a vertex buffer, which is simply a chunk of memory. AlignedByteOffset The AlignedByteOffset field tells the GPU the memory location to start fetching the data for this element. This field usually has the value D3D10_INPUT_PER_VERTEX_DATA. When an application uses instancing, it can set an input layout's InputSlotClass to D3D10_INPUT_PER_INSTANCE_DATA to work with InputSlotClass vertex buffer containing instance data. Instancing is an advanced Direct3D topic and will not be discussed here. For our tutorial, we will use D3D10_INPUT_PER_VERTEX_DATA exclusively. This field is used for instancing. Since we are not using instancing, this field InstanceDataStepRate is not used and must be set to 0. Now we can define our D3D10_INPUT_ELEMENT_DESC array and create the input layout: // Define the input layout D3D10_INPUT_ELEMENT_DESC layout[] = { { L"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 }, }; UINT numElements = sizeof(layout)/sizeof(layout[0]); Vertex Layout In the next tutorial, we will explain the technique object and the associated shaders. For now, we will just concentrate on creating the Direct3D 10 vertex layout object for the technique. However, we will learn that the technique and shaders are tightly coupled with this vertex layout. The reason is that creating a vertex layout object requires the vertex shader's input signature. We first call the technique's GetPassByIndex() method to obtain an effect pass object that represents the first pass of the technique. Then, we call the pass object's GetDesc() method to obtain a pass description structure. Within this structure is a field named pIAInputSignature that points to the binary data that represents the input signature of the vertex shader used in this pass. Once we have this data, we can call ID3D10Device::CreateInputLayout() to create a vertex layout object, and ID3D10Device::IASetInputLayout() to set it as the active vertex layout. The code to do all of that is shown below: // Create the input layout D3D10_PASS_DESC PassDesc; g_pTechnique->GetPassByIndex( 0 )->GetDesc( &PassDesc ); if( FAILED( g_pd3dDevice->CreateInputLayout( layout, numElements, PassDesc.pIAInputSignature, PassDesc.IAInputSignatureSize, &g_pVertexLayout ) ) ) return FALSE; // Set the input layout g_pd3dDevice->IASetInputLayout( g_pVertexLayout );
Creating Vertex Buffer One thing that we will also need to do during initialization is to create the vertex buffer that holds the vertex data. To create a vertex buffer in Direct3D 10, we fill in two structures, D3D10_BUFFER_DESC and D3D10_SUBRESOURCE_DATA, and then call ID3D10Device::CreateBuffer(). D3D10_BUFFER_DESC describes the vertex buffer object to be created, and D3D10_SUBRESOURCE_DATA describes the actual data that will be copied to the vertex buffer during creation. The creation and initialization of the vertex buffer is done at once so that we don't need to initialize the buffer later. The data that will be copied to the vertex buffer is vertices, array of 3 SimpleVertex. The coordinates in the vertices array are chosen so that we see a triangle in the middle of our application window when rendered with our shaders. After the vertex buffer is created, we can call ID3D10Device::IASetVertexBuffers() to bind it to the device. The complete code is shown here: // Create vertex buffer SimpleVertex vertices[] = { D3DXVECTOR3( 0.0f, 0.5f, 0.5f ), D3DXVECTOR3( 0.5f, -0.5f, 0.5f ), D3DXVECTOR3( -0.5f, -0.5f, 0.5f ), }; D3D10_BUFFER_DESC bd; bd.Usage = D3D10_USAGE_DEFAULT; bd.ByteWidth = sizeof( SimpleVertex ) * 3; bd.BindFlags = D3D10_BIND_VERTEX_BUFFER; bd.CPUAccessFlags = 0; bd.MiscFlags = 0; D3D10_SUBRESOURCE_DATA InitData; InitData.pSysMem = vertices; if( FAILED( g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer ) ) ) return FALSE; // Set vertex buffer UINT stride = sizeof( SimpleVertex ); UINT offset = 0; g_pd3dDevice->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset ); Primitive Topology Primitive topology refers to how the GPU obtains the three vertices it requires to render a triangle. We discussed above that in order to render a single triangle, the application needs to send three vertices to the GPU. Therefore, the vertex buffer has three vertices in it. What if we want to render two triangles? One way is to send 6 vertices to the GPU. The first 3 vertices define the first triangle and the second 3 vertices define the second triangle. This topology is called a triangle list. Triangle lists have the advantage of being easy to understand, but in certain cases they are very inefficient. Such cases occur when successively rendered triangles share vertices. For instance, figure 3a left shows a square made up of two triangles: A B C and C B D. (By convention, triangles are typically defined by listing their vertices in clockwise order.) If we send these two triangles to the GPU using a triangle list, our vertex buffer would like this: A B C C B D
Notice that B and C appear twice in the vertex buffer because they are shared by both triangles.
Figure 3a) contains a square made up of two triangles; figure 3b contains a pentagonal shape made up of three triangles. We can make the vertex buffer smaller if we can tell the GPU that when rendering the second triangle, instead of fetching all 3 vertices from the vertex buffer, use 2 of the vertices from the previous triangle and fetch only 1 vertex from the vertex buffer. As it turns out, this is supported by Direct3D, and the topology is called triangle strip. When rendering a triangle strip, the very first triangle is defined by the first three vertices in the vertex buffer. The next triangle is defined by the last two vertices of the previous triangle plus the next vertex in the vertex buffer. Taking the square in figure 3a as an example, using triangle strip, the vertex buffer would look like: A B C D The first 3 vertices, A B C, define the first triangle. The second triangle is defined by B and C, the last two vertices of the first triangle, plus D. Thus, by using the triangle strip topology, the vertex buffer size has gone from 6 vertices to 4 vertices. Similarly, for three triangles such as those in figure 3b, using triangle list would require a vertex buffer such as: A B C C B D C D E Using triangle strip, the size of the vertex buffer is dramatically reduced: A B C D E You may have noticed that in the triangle strip example, the second triangle is defined as B C D. These 3 vertices do not form a clockwise order. This is a natural phenomenon from using triangle strips. To overcome this, the GPU automatically swaps the order of the two vertices coming from the previous triangle. It only does this for the second triangle, fourth triangle, sixth triangle, eighth triangle, and so on. This ensures that every triangle is defined by vertices in the correct winding order (clockwise, in this case). Besides triangle list and triangle strip, Direct3D 10 supports many other types of primitive topology. We will not discuss them in this tutorial. In our code, we have one triangle, so it doesn't really matter what we specify. However, we must specify something, so we chose a triangle list. // Set primitive topology g_pd3dDevice>IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST ); Rendering the Triangle The final item missing is the code that does the actual rendering of the triangle. As mentioned earlier, this tutorial will use the effect system. We start by calling ID3D10EffectTechnique::GetDesc() on the technique object obtained earlier to receive a D3D10FX_TECHNIQUE_DESC structure which describes the technique. One of the members of D3D10FX_TECHNIQUE_DESC, Passes, indicates the number of passes the technique contains. To correctly render using this technique, the application should loop the same number of times as there are passes. Within the loop, we must first call the technique's GetPassByIndex() method to obtain the pass object, then call its Apply() method to have the effect system bind the associated shaders and render states to the graphics pipeline. The next thing that we do is call ID3D10Device::Draw(), which commands the GPU to render using the current vertex buffer, vertex layout, and primitive
topology. The first parameter to Draw() is the number of vertices to send to the GPU, and the second parameter is the index of the first vertex to begin sending. Because we are rendering one triangle and we are rendering from the beginning of the vertex buffer, we use 3 and 0 for the two parameters, respectively. The entire triangle-rendering code looks like the following: // Render a triangle D3D10_TECHNIQUE_DESC techDesc; g_pTechnique->GetDesc( &techDesc ); for( UINT p = 0; p < techDesc.Passes; ++p ) { g_pTechnique->GetPassByIndex( p )->Apply(0); g_pd3dDevice->Draw( 3, 0 ); }
Tutorial 3: Shaders and Effect System
Summary In the previous tutorial, we setup a vertex buffer and passed one triangle to the GPU. Now, we will actually step through the graphics pipeline and look at how each stage works. The concept of shaders and the effect system will be explained. Note that this tutorial shares the same source code as the previous one, but will emphasize a different section. Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial03
Navigation • • • •
The Graphics Pipeline Shaders Effect System Putting It Together
The Graphics Pipeline In the previous tutorial, we setup the vertex buffer, and then we associated a vertex layout with a technique object. Now, we will explain the technique object and the shaders that compose it. To fully understand the individual shaders, we will take a step back and look at the whole graphical pipeline. In Tutorial 2, when we called Apply from the technique, we actually bound our shader to a stage in the pipeline. Then, when we called Draw, we start processing the vertex data passed into the graphics pipeline. The following sections describe in detail what happens after the Draw command. Shaders In Direct3D 10, shaders reside in different stages of the graphics pipeline. They are short programs that, executed by the GPU, take certain input data, process that data, and then output the result to the next stage of the pipeline. Direct3D 10 supports 3 types of shaders: vertex shader, geometry shader, and pixel shader. A vertex shader takes a vertex as input. It is run once for every vertex passed to the GPU via vertex buffers. A geometry shader takes a primitive as input, and is run once for every primitive passed to the GPU. A primitive is a point, a line, or a triangle. A pixel shader takes a pixel (or sometimes called a fragment) as input, and is run once for each pixel of a primitive that we wish to render. Together, vertex, geometry, and pixel shaders are where the meat of the action occurs. When rendering with Direct3D 10, the GPU must have a valid vertex shader and pixel shader. Geometry shader is an advanced feature in Direct3D 10 and is optional, so we will not discuss geometry shaders in this tutorial. Vertex Shaders Vertex shaders are short programs to be executed by GPU on vertices. Think of vertex shaders as C functions that take each vertex as input, process the input, and then output the modified vertex. After the application passes vertex data to the GPU in the form of a vertex buffer, the GPU iterates through the vertices in the vertex buffer, and executes the active vertex shader once for each vertex, passing the vertex's data to the vertex shader as input parameters. While a vertex shader can be used to carry out many tasks, the most important job of a vertex shader is transformation. Transformation is the process of converting vectors from one coordinate system to another. For example, a triangle in a 3D scene may have its vertices at the positions (0, 0, 0) (1, 0, 0) (0, 1, 0). When the triangle is drawn on a 2D texture buffer, the GPU has to know the 2D coordinates of the points on the buffer that the vertices should be drawn at. It is transformation that helps us accomplish this. Transformation will be discussed in detail in the next tutorial. For this tutorial, we will be using a simple vertex shader that does nothing except passing the input data through as output. In the Direct3D 10 tutorials, we will write our shaders in High-Level Shading Language (HLSL), and the applications will use these shaders with the effect system. Recall that our vertex data has a 3D position element, and the vertex shader will do no processing on the input at all. The resulting vertex shader looks like the following: float4 VS( float4 Pos : POSITION ) : SV_POSITION {
return Pos; } This vertex shader looks a lot like a C function. HLSL uses C-like syntax to make learning easier for C/C++ programmers. We can see that this vertex shader, named VS, takes a parameter of float4 type and returns a float4 value. In HLSL, a float4 is a 4-component vector where each component is a floating-point number. The colons define the semantics of the parameter as well as the return value. As mentioned above, the semantics in HLSL describe the nature of the data. In our shader above, we choose POSITION as the semantics of the Pos input parameter because this parameter will contain the vertex position. The return value's semantics, SV_POSITION, is a pre-defined semantics with special meaning. This semantics tells the graphics pipeline that the data associated with the semantics defines the clip-space position. This position is needed by the GPU in order to drawn pixels on the screen. (We will discuss clip-space in the next tutorial.) In our shader, we take the input position data and output the exact same data back to the pipeline. Pixel Shaders Modern computer monitors are commonly raster display, which means the screen is actually a twodimensional grid of small dots called pixels. Each pixel contains a color independent of other pixels. When we render a triangle on the screen, we don't really render a triangle as one entity. Rather, we light up the group of pixels that are covered by the triangle's area. Figure 2 shows an illustration of this. Figure 2. Left: What we would like to draw. Right: What is actually on the screen. The process of converting a triangle defined by three vertices to a bunch of pixels covered by the triangle is called rasterization. The GPU first determines what pixels are covered by the triangle being rendered. Then it invokes the active pixel shader for each of these pixels. A pixel shader's primary purpose is to compute the color that each pixel should have. The shader takes certain input about the pixel being colored, computes the pixel's color, then outputs that color back to the pipeline. The input that it takes comes from the active geometry shader, or, if a geometry shader is not present, such as the case in this tutorial, the input comes directly from the vertex shader. The vertex shader we created above outputs a float4 with the semantics SV_POSITION. This will be the input of our pixel shader. Since pixel shaders output color values, the output of our pixel shader will be a float4. We give the output the semantics SV_TARGET to signify outputting to the render target format. The pixel shader looks like the following: float4 PS( float4 Pos : SV_POSITION ) : SV_Target { return float4( 1.0f, 1.0f, 0.0f, 1.0f ); // Yellow, with Alpha = 1 } Effect System Our effect file consists of the two shaders, vertex shader and pixel shader, and the technique definition. The technique definition will set the corresponding shaders to each section. In addition, there is also the semantic to compile the shader. Note that the geometry shader is left NULL because it's not required and will be covered later. // Technique Definition technique10 Render {
pass P0 { SetVertexShader( CompileShader( vs_4_0, VS() ) ); SetGeometryShader( NULL ); SetPixelShader( CompileShader( ps_4_0, PS() ) ); } } Creating the Effect and Effect Technique In the application code, we will need to create an effect object. This effect object represents our effect file, and is created by calling D3D10CreateEffectFromFile(). Once we have created the effect object, we can call the ID3D10Effect::GetTechniqueByName() method, passing in "Render" as the name, to obtain the technique object that we will be using to do the actual rendering. The code is demonstrated below: // Create the effect if( FAILED( D3DX10CreateEffectFromFile( L"Tutorial03.fx", NULL, NULL, D3D10_SHADER_ENABLE_STRICTNESS, 0, g_pd3dDevice, NULL, NULL, &g_pEffect, NULL ) ) ) return FALSE; // Obtain the technique g_pTechnique = g_pEffect->GetTechniqueByName( "Render" ); Putting It Together After walking through the graphics pipeline, we can start to understand the process of rendering the triangle we created at the beginning of Tutorial 2. Creating Direct3D applications requires two distinct steps. The first would be creating the source data in vertex data, as we've done in Tutorial 2; the second stage would be to create the shaders which would transform that data for rendering, which we showed in this Tutorial.
Tutorial 4: 3D Spaces
Summary In the previous tutorial, we have successfully rendered a triangle in the center of our application window. We haven't paid much attention to the vertex positions that we have picked in our vertex buffer. In this tutorial, we will delve into the details of 3D positions and transformation. The outcome of this tutorial will be a 3D object rendered to screen. Whereas previous tutorials focused on rendering a 2D object onto a 3D world, here we show a 3D object.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial04
3D Spaces In the previous tutorial, the vertices of the triangle were placed strategically to perfectly align itself on the screen. However, this will not always be the case, and thus, we need a system to denote objects in 3D space and a system to display them. In the real world, objects exist in 3D space. This means that to place an object in a particular position in the world, we would need to use a coordinate system and define three coordinates that correspond to the position. In computer graphics, 3D spaces are most commonly in Cartesian coordinate system. In this coordinate system, three axes, X, Y, and Z, perpendicular to each other, dictate the coordinate that each point in the space has. This coordinate system is further divided into left-handed and right-handed systems. In a left-handed system, when X axis points to the right and Y axis points to up, Z axis points forward. In a right-handed system, with the same X and Y axes, Z axis points backward. Figure 1. Left-handed versus right-handed coordinate systems
Now that we have talked about the coordinate system, consider 3D spaces. A point has different coordinates in different spaces. As an example in 1D, suppose we have a ruler and we note the point, P, at the 5-inch mark of the ruler. Now, if we move the ruler 1 inch to the right, the same point lies on the 4-inch mark. By moving the ruler, the frame of reference has changed, and therefore, while the point hasn't moved, it has a new coordinate. Figure 2. Spaces illustration in 1D
In 3D, a space is typically defined by an origin and three unique axes from the origin: X, Y and Z. There are several spaces commonly used in computer graphics: object space, world space, view space, projection space, and screen space. Figure 3. A cube defined in object space
Object Space Notice that the cube is centered on the origin. Object space, also called model space, refers to the space used by artists when they create the 3D models. Usually, artists create models that are centered around the origin so that it is easier to perform transformations such as rotations to the models, as we will see when we discuss transformation. The 8 vertices have the following coordinates: (-1, ( 1, (-1, ( 1, (-1, ( 1, (-1, ( 1,
1, 1, -1, -1, 1, 1, -1, -1,
-1) -1) -1) -1) 1) 1) 1) 1)
Because object space is what artists typically use when they design and create models, the models that are stored on disk are also in object space. An application can create a vertex buffer to represent such a model and initialize the buffer with the model data. Therefore, the vertices in the vertex buffer will usually be in object space as well. This also means that the vertex shader receives input vertex data in object space.
World Space World space is a space shared by every object in the scene. It is used to define spatial relationship between objects that we wish to render. To visualize world space, we could imagine that we are standing in the south-western corner of a rectangular room facing north. We define the corner that our feet are standing at to be the origin, (0, 0, 0). The X axis goes to our right; the Y axis goes up; and the Z axis goes forward, the same direction as we are facing. When we do this, every position in the room can be identified with a set of XYZ coordinates. For instance, there may be a chair 5 feet in front and 2 feet to the right of us. There may be a light on the 8-foot-high ceiling directly on top of the chair. We can then refer to the position of the chair as (2, 0, 5) and the position of the light as (2, 8, 5). As we see, world space is so-called because they tell us where objects are in relation to each other in the world.
View Space View space, sometimes called camera space, is similar to world space in that it is typically used for the entire scene. However, in view space, the origin is at the viewer or camera. The view direction (where the viewer is looking) defines the positive Z axis. An "up" direction defined by the application becomes the positive Y axis as shown below. Figure 4. The same object in world space (left) and in view space (right)
The left image shows a scene that consists of a human-like object and a viewer (camera) looking at the object. The origin and axes that are used by world space are shown in red. The right image shows the view space in relation to world space. The view space axes are shown in blue. For clearer illustration, the view space does not have the same orientation as the world space in the left image to readers. Note that in view space, the viewer is looking in the Z direction.
Projection Space Projection space refers to the space after applying projection transformation from view space. In this space, visible content has X and Y coordinates ranging from -1 to 1, and Z coordinate ranging from 0 to 1.
Screen Space Screen space is often used to refer to locations in the frame buffer. Because frame buffer is usually a 2D texture, screen space is a 2D space. The top-left corner is the origin with coordinates (0, 0). The positive X goes to right and positive Y goes down. For a buffer that is w pixels wide and h pixels high, the most lower-right pixel has the coordinates (w - 1, h - 1).
Space-To-Space Transformation Transformation is most commonly used to convert vertices from one space to another. In 3D computer graphics, there are logically 3 such transformations in the pipeline: world, view, and projection transformation. Individual transformation operations such as translation, rotation, and scaling are covered in the next Tutorial.
World Transformation World transformation, as the name suggests, converts vertices from object space to world space. It usually consists of one or more scaling, rotation, and translation, based on the size, orientation, and position we would like to give to the object. Every object in the scene has its own world transformation matrix. This is because each object has its own size, orientation, and position.
View Transformation After vertices are converted to world space, view transformation converts those vertices from world space to view space. Recall from earlier discussion that view space is what the world appears from the viewer's (or camera's) perspective. In view space, the viewer is located at origin looking out along the positive Z axis. It is worth noting that although view space is the world from the viewer's frame of reference, view transformation matrix is applied to vertices, not the viewer. Therefore, the view matrix must perform the opposite transformation that we apply to our viewer or camera. For example, if we want to move the camera 5 units towards the -Z direction, we would need to compute a view matrix that translates vertices for 5 units along the +Z direction. Although the camera has moved backward, the vertices, from the camera's point of view, have moved forward. In Direct3D a convenient API call D3DXMatrixLookAtLH() is often used to compute a view matrix. We would simply need to tell it where the viewer is, where it's looking at, and the direction representing the viewer's top, also called the up-vector, to obtain a corresponding view matrix.
Projection Transformation Projection transformation converts vertices from 3D spaces such as world and view spaces to projection space. In projection space, X and Y coordinates of a vertex are obtained from the X/Z and Y/Z ratios of this vertex in 3D space. Figure 5. Projection
In 3D space, things appear in perspective. That is, the closer an object is, the larger it appears. As shown, the tip of a tree that is h units tall at d units away from the viewer's eye will appear at the same point as the tip of another tree 2h units tall and 2d units away. Therefore, where a vertex appears on a 2D screen is directly related to its X/Z and Y/Z ratios. One of the parameters that defines a 3D space is called the field-of-view. Field-of-view (FOV) denotes which objects are visible from a particular position, while looking in a particular direction. Humans have a field-of-view that is forward-looking (we can't see what is behind us) and we can't see objects that are too close or too far away. In computer graphics, the field-of-view is contained in a view frustum. The view frustum is defined by 6 planes in 3D. Two of these planes are parallel to the XY plane. These are called the near-Z and far-Z planes. The other 4 planes are defined by the viewer's horizontal and vertical field of view. The wider the FOV is, the wider the frustum volume is, and the more objects the viewer sees. The GPU filters out objects that are outside the view frustum so that it does not have to spend time rendering something that will not be displayed. This process is called clipping. The view frustum is a 4-sided pyramid with its top cut off. Clipping against this volume is complicated because to clip against one view frustum plane, the GPU must compare every vertex to the plane's equation. Instead, the GPU generally performs projection transformation first, and then clips against the view frustum volume. The effect of projection transformation on the view frustum is that the pyramid shaped view frustum becomes a box in projection space. This is because, as mentioned above, in projection space the X and Y coordinates are based on the X/Z and Y/Z in 3D space. Therefore, point a and point b will have the same X and Y coordinates in projection space, which is why the view frustum becomes a box. Figure 6. View Frustum
Suppose that the tips of the two trees lie exactly on the top view frustum edge. Further suppose that d = 2h. The Y coordinate along the top edge in projection space will then be 0.5 (because h/d = 0.5). Therefore, any Y values post-projection that are greater than 0.5 will be clipped by the GPU. The problem here is that 0.5 is determined by the vertical field of view chosen by the program, and different FOV values result in different values that the GPU has to clip against. To make the process more convenient, 3D programs generally scale the projected X and Y values of vertices so that the visible X and Y values range from -1 to 1. In other words, anything with X or Y coordinate that's outside the [-1 1] range will be clipped out. To make this clipping scheme work, the projection matrix must scale the X and Y coordinates of projected vertices by the inverse of h/d, or d/h. d/h is also the cotangent of half of FOV. With scaling, the top of the view frustum becomes h/d * d/h = 1. Anything greater than 1 will be clipped by the GPU. This is what we want. A similar tweak is generally done for the Z coordinate in projection space as well. We would like the near and far Z planes to be at 0 and 1 in projection space, respectively. When Z = near-Z value in 3D space, Z should be 0 in projection space; when Z = far-Z in 3D space, Z should be 1 in projection space. After this is done, any Z values outside [0 1] will be clipped out by the GPU. In Direct3D 10, the easiest way to obtain a projection matrix is to call the D3DXMatrixPerspectiveFovLH() method. We simply supply 4 parameters--FOVy, Aspect, Zn, and Zf--and get back a matrix that does everything necessary as mentioned above. FOVy is the field of view in Y direction. Aspect is the aspect ratio, which is ratio of view space width to height. From FOVy and Aspect, FOVx can be computed. This aspect ratio is usually obtained from the ratio of the render target width to height. Zn and Zf are the near and far Z values in view space, respectively.
Using Transformation In the previous tutorial, we wrote a program that renders a single triangle to screen. When we create the vertex buffer, the vertex positions that we use are directly in projection space so that we don't have to perform any transformation. Now that we have an understanding of 3D space and transformation, we are going to modify the program so that the vertex buffer is defined in object space, as it should be. Then, we will modify our vertex shader to transform the vertices from object space to projection space.
Modifying the Vertex Buffer Since we started representing things in 3 dimensions, we have changed the flat triangle from the previous tutorial to a cube. This will allow us to demonstrate these concepts much clearer. SimpleVertex vertices[] = { { D3DXVECTOR3( -1.0f, 1.0f ) }, { D3DXVECTOR3( 1.0f, 1.0f ) }, { D3DXVECTOR3( 1.0f, 1.0f ) }, { D3DXVECTOR3( -1.0f, 1.0f ) }, { D3DXVECTOR3( -1.0f, 1.0f ) }, { D3DXVECTOR3( 1.0f, 1.0f ) }, { D3DXVECTOR3( 1.0f, 1.0f ) }, { D3DXVECTOR3( -1.0f, 1.0f ) }, };
1.0f, -1.0f ), D3DXVECTOR4( 0.0f, 0.0f, 1.0f, 1.0f, -1.0f ), D3DXVECTOR4( 0.0f, 1.0f, 0.0f, 1.0f,
1.0f ), D3DXVECTOR4( 0.0f, 1.0f, 1.0f,
1.0f,
1.0f ), D3DXVECTOR4( 1.0f, 0.0f, 0.0f,
-1.0f, -1.0f ), D3DXVECTOR4( 1.0f, 0.0f, 1.0f, -1.0f, -1.0f ), D3DXVECTOR4( 1.0f, 1.0f, 0.0f, -1.0f,
1.0f ), D3DXVECTOR4( 1.0f, 1.0f, 1.0f,
-1.0f,
1.0f ), D3DXVECTOR4( 0.0f, 0.0f, 0.0f,
If you notice, all we did was specify the eight points on the cube, but we didn't actually describe the individual triangles. If we passed this in as is, the output would not be what we expect. We will need to specify the triangles that form the cube through these eight points. On a cube, many triangles will be sharing the same vertex and it would be a waste of space to redefine the same points over and over again. As such, there is a method to specify just the eight points, and then let Direct3D know which points to pick for a triangle. This is done through an index buffer. An index buffer will contain a list, which will refer to the index of vertices in the buffer, to specify which points to use in each triangle. The code below shows which points make up each of our triangles. // Create index buffer DWORD indices[] = { 3,1,0, 2,1,3, 0,5,4, 1,5,0, 3,4,7, 0,4,3, 1,6,5, 2,6,1,
2,7,6, 3,7,2,
};
6,4,5, 7,4,6,
As you can see, the first triangle is defined by points 3, 1, and 0. This means that the first triangle has vertices at: ( -1.0f, 1.0f, 1.0f ),( 1.0f, 1.0f, -1.0f ), and ( -1.0f, 1.0f, -1.0f ) respectively. There are six faces on the cube, and each face is comprised of two triangles, thus, you see twelve total triangles defined here. Since each vertex is explicitly listed, and no two triangles are sharing edges (at least, in the way it has been defined), this is considered a triangle list. In total, for 12 triangles in a triangle list, we will require a total of 36 vertices. The creation of the index buffer is very similar to the vertex buffer, where we specified parameters such as size and type in a structure, and called CreateBuffer. The type is D3D10_BIND_INDEX_BUFFER, and since we declared our array using DWORD, we will use sizeof(DWORD). bd.Usage = D3D10_USAGE_DEFAULT; bd.ByteWidth = sizeof( DWORD ) * 36; // 36 vertices needed for 12 triangles in a triangle list bd.BindFlags = D3D10_BIND_INDEX_BUFFER; bd.CPUAccessFlags = 0; bd.MiscFlags = 0; InitData.pSysMem = indices; if( FAILED( g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer ) ) ) return FALSE;
Once we created this buffer, we will need to set it so that Direct3D knows to refer to this index buffer when generating the triangles. We specify the pointer to the buffer, the format, and the offset in the buffer to start referencing from. // Set index buffer g_pd3dDevice->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0 );
Modifying the Vertex Shader In our vertex shader from the previous tutorial, we take the input vertex position and output the same position without any modification. We can do this because the input vertex position is already defined in projection space. Now, because the input vertex position is defined in object space, we must transform it before outputting from the vertex shader. We do this with 3 steps: transform from object to world space, transform from world to view space, and transform from view to projection space. The first thing that we need to do is declare 3 constant buffer variables. Constant buffers are used to store data that the application needs to pass to shaders. Before rendering, the application usually writes important data to constant buffers, and then during rendering the data can be read from within the shaders. In an FX file, constant buffer variables are declared like global variables in C++. The 3 variables that we will use are the world, view, and projection transformation matrices of the HLSL type "matrix". Once we have declared the matrices that we will need, we update our vertex shader to transform the input position by using the matrices. A vector is transformed by multiplying the vector by a matrix. In HLSL, this is done using the mul() intrinsic function. Our variable declaration and new vertex shader are shown below: matrix World;
matrix View; matrix Projection; // // Vertex Shader // VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR ) { VS_OUTPUT output = (VS_OUTPUT)0; output.Pos = mul( Pos, World ); output.Pos = mul( output.Pos, View ); output.Pos = mul( output.Pos, Projection ); output.Color = Color; return output; }
In the vertex shader, each mul() applies one transformation to the input position. The world, view, and projection transformations are applied in that order sequentially. This is necessary because vector and matrix multiplication is not commutative.
Setting up the Matrices We have updated our vertex shader to transform using matrices, but we also need to define 3 matrices in our program. These three matrices will store the transformation to be used when we render. Before rendering, we copy the values of these matrices to the shader constant buffer. Then when we initiate the rendering by calling Draw(), our vertex shader reads the matrices stored in the constant buffer. In addition to the matrices, we also need to define 3 ID3D10EffectMatrixVariable pointers. ID3D10EffectMatrixVariable is an object that represents a matrix in the constant buffer. We can use these 3 objects to copy the matrices to the constant buffer. Therefore, our global variables have the following addition: ID3D10EffectMatrixVariable* ID3D10EffectMatrixVariable* ID3D10EffectMatrixVariable* D3DXMATRIX D3DXMATRIX D3DXMATRIX
g_pWorldVariable = NULL; g_pViewVariable = NULL; g_pProjectionVariable = NULL; g_World; g_View; g_Projection;
To initialize the 3 ID3D10EffectMatrixVariable objects, we call ID3D10Effect::GetVariableByName() after creating the effect, passing the name of the variable we are interested in. GetVariableByName() returns an ID3D10EffectVariable interface, even though the underlying object is specific to the variable type defined in the FX file. In this case, the underlying objects are ID3D10EffectMatrixVariable. To get an ID3D10EffectMatrixVariable interface from an ID3D10EffectVariable, we can call its AsMatrix() method: g_pWorldVariable = g_pEffect->GetVariableByName( "World" )->AsMatrix(); g_pViewVariable = g_pEffect->GetVariableByName( "View" )->AsMatrix(); g_pProjectionVariable = g_pEffect->GetVariableByName( "Projection" )>AsMatrix();
The next thing that we need to do is coming up with 3 matrices which we will use to do the transformation. We want the triangle to be sitting on origin, parallel to the XY plane. This is exactly how it is stored in the vertex buffer in object space. Therefore, the world transformation needs to do nothing, and we initialize the world matrix to an identity matrix. We would like to set up our camera so that it is situated at [0 1 -5], looking at the point [0 1 0]. We can call D3DXMatrixLookAtLH() to conveniently compute a view matrix for us using the up vector [0 1 0] since we would like the +Y direction to always stay at top. Finally, to come up with a projection matrix, we call D3DXMatrixPerspectiveFovLH(), with a 90 degree vertical field of view (pi/2), an aspect ratio of
640/480 which is from our back buffer size, and near and far Z at 0.1 and 100, respectively. This means that anything closer than 0.1 or further than 100 will not be visible on the screen. These three matrices are stored in the global variables g_World, g_View, and g_Projection.
Updating Constant Buffers We have the matrices, and now we must write them to the constant buffer when rendering so that the GPU can read them. Constant buffers are covered in depth in a later tutorial. For now, consider them the containers for constants passed to shaders. Because we are writing matrices to the constant buffer, we call ID3D10EffectMatrixVariable's SetMatrix() method, passing to it the matrix to be written to the buffer. This has to be done before we issue the Draw() call. // // Update variables // g_pWorldVariable->SetMatrix( (float*)&g_World ); g_pViewVariable->SetMatrix( (float*)&g_View ); g_pProjectionVariable->SetMatrix( (float*)&g_Projection );
Tutorial 5: 3D Transformation
Summary In the previous tutorial, we rendered a cube from model space to the screen. In this tutorial, we will extend the concept of transformations and demonstrate simple animation that can be achieved with these transformations. The outcome of this tutorial will be an object that orbits around another. It would be useful to demonstrate the transformations and how they can be combined to achieve the desired effect. Future tutorials will be building on this foundation as we introduce new concepts.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial05
Transformation In 3D graphics, transformation is often used to operate on vertices and vectors. It is also used to convert them in one space to another. Transformation is performed via multiplication with a matrix. There are typically three types of primitive transformation that can be performed on vertices: translation (where it lies in space relative to the origin), rotation (its direction in relation to the x, y, z frame), and scaling (its distance from origin). In addition to those, projection transformation is used to go from view space to projection space. The D3DX library contains APIs that can conveniently construct a matrix for many purposes such as translation, rotation, scaling, world-toview transformation, view-to-projection transformation, etc. An application can then use these matrices to transform vertices in its scene. A basic understanding of matrix transformations is required. We will briefly look at some examples below.
Translation Translation refers to moving or displacing for a certain distance in space. In 3D, the matrix used for translation has the form 1 0 0 a
0 1 0 b
0 0 1 c
0 0 0 1
where (a, b, c) is the vector that defines the direction and distance to move. For example, to move a vertex -5 unit along the X axis (negative X direction), we can multiply it with this matrix: 1 0 0 -5
0 1 0 0
0 0 1 0
0 0 0 1
If we apply this to a cube object centered at origin, the result is that the box is moved 5 units towards the negative X axis, as figure 5 shows, after translation is applied. Figure 1. The effect of translation
In 3D, a space is typically defined by an origin and three unique axes from the origin: X, Y and Z. There are several spaces commonly used in computer graphics: object space, world space, view space, projection space, and screen space. Figure 2. A cube defined in object space
Rotation Rotation refers to rotating vertices about an axis going through the origin. Three such axes are the X, Y, and Z axes in the space. An example in 2D would be rotating the vector [1 0] 90 degrees counter-clockwise. The result from the rotation is the vector [0 1]. The matrix used for rotating ? clockwise about the Y axis looks like this: cos? 0 sin? 0
0 1 0 0
-sin? 0 cos? 0
0 0 0
1
Figure 6 shows the effect of rotating a cube centered at origin for 45 degrees about the Y axis. Figure 3. The effect of rotation about the Y axis
Scaling Scaling refers to enlarge to shrink the size of vector components along axis directions. For example, a vector can be scaled up along all directions or scaled down along the X axis only. To scale, we usually apply the scaling matrix below: p 0 0 0
0 q 0 0
0 0 r 0
0 0 0 1
where p, q, and r are the scaling factor along the X, Y, and Z direction, respectively. Figure 7 shows the effect of scaling by 2 along the X direction and scaling by 0.5 along the Y direction. Figure 4. The effect of Scaling
Multiple Transformations To apply multiple transformations to a vector, we can simply multiply the vector to the first transformation matrix, then multiply the resulting vector to the second transformation matrix, and so on. Because vector and matrix multiplication is associative, we can also multiply all of the matrices first, then multiply the vector to the product matrix and obtain identical result. Figure 8 shows how the cube would end up if we combine a rotation and a translation transformation
together. Figure 5. The effect of rotation and translation
Creating the Orbit In this tutorial, we will be transforming two cubes. The first one will rotate in place, while the second one will rotate around the first, while spinning on its own axis. The two cubes will have its own world transformation matrix associated with it, and this matrix will be reapplied to it in every frame rendered. There are functions within D3DX that will assist in the creation of the rotation, translation, and scaling matrices. •
• •
Rotations performed around the X, Y and Z axis are accomplished with the functions D3DXMatrixRotationX, D3DXMatrixRotationY, and D3DXMatrixRotationZ respectively. They create basic rotation matrices which rotate around one of the primary axis. Complex rotations around other axis can be done by multiplying together several of them. Translations can be performed in by invoking the D3DXMatrixTranslation function. This function will create a matrix that will translate points specified by the parameters. Scaling is done with D3DXMatrixScaling. It scales only along the primary axes. If scaling along arbitrary axis is desired, then the scaling matrix can be multiplied with an appropriate rotation matrix to achieve the effect.
The first cube will be spinning in place and act as the center for the orbit. The cube has a rotation along the Y axis applied to the associated world matrix. This is done by calling the D3DXMatrixRotationY function shown below. The cube is rotated by a set amount each frame. Since the cubes are suppose to continuously rotate, the value in which the rotation matrix is based on gets incremented with every frame. // 1st Cube: Rotate around the origin D3DXMatrixRotationY( &g_World1, t );
The second cube will be orbiting around the first one. To demonstrate multiple transformations, a scaling factor, and its own axis spin will be added. The formula used is shown right below the code (in comments). First the cube will be scale down to 30% size, and then it will be rotated along its spin axis (the Z axis in this case). To simulate the orbit, it will get translated away from the origin, and then rotated along the Y axis. The desired effect can be achieved by utilizing four separate matrices with its individual transformation (mScale, mSpin, mTranslate, mOrbit), then multiplied together. // 2nd Cube: Rotate around origin D3DXMATRIX mTranslate; D3DXMATRIX mOrbit; D3DXMATRIX mSpin; D3DXMATRIX mScale; D3DXMatrixRotationZ( &mSpin, -t ); D3DXMatrixRotationY( &mOrbit, -t*2.0f ); D3DXMatrixTranslation( &mTranslate, -4.0f, 0.0f, 0.0f ); D3DXMatrixScaling( &mScale, 0.3f, 0.3f, 0.3f ); D3DXMatrixMultiply( D3DXMatrixMultiply( D3DXMatrixMultiply( //g_World2 = mScale
&g_World2, &mScale, &mSpin ); &g_World2, &g_World2, &mTranslate ); &g_World2, &g_World2, &mOrbit ); * mSpin * mTranslate * mOrbit;
An important point to note is that these operations are not commutative. The order in which the transformations are applied matter. Experiment with the order of transformation and observe the results. Since all the transformation functions will create a new matrix from the parameters, the amount at which they rotate has to be incremented. This is done by updating the "time" variable. // Update our time t += D3DX_PI * 0.0125f;
Before the rendering calls are made, the technique must collect the variables for the shaders. Here, the world, view, and projection matrices are bound to the technique. Note that the world matrix is unique to each cube, and thus, changes for every object that gets passed into it. // // Render the first cube // D3D10_TECHNIQUE_DESC techDesc; g_pTechnique->GetDesc( &techDesc ); for( UINT p = 0; p < techDesc.Passes; ++p ) { g_pTechnique->GetPassByIndex( p )->Apply(0); g_pd3dDevice->DrawIndexed( 36, 0, 0 ); } // // Update variables for the second cube // g_pWorldVariable->SetMatrix( (float*)&g_World2 ); g_pViewVariable->SetMatrix( (float*)&g_View ); g_pProjectionVariable->SetMatrix( (float*)&g_Projection ); // // Render the second cube // for( UINT p = 0; p < techDesc.Passes; ++p ) { g_pTechnique->GetPassByIndex( p )->Apply(0); g_pd3dDevice->DrawIndexed( 36, 0, 0 ); }
The Depth Buffer There is one other important addition to this tutorial, and that is the depth buffer. Without it, the smaller orbiting cube would still be drawn on top of the larger center cube when it went around the back of the latter. The depth buffer allows Direct3D to keep track of the depth of every pixel drawn to the screen. The default behavior of the depth buffer in Direct3D 10 is to check every pixel being drawn to the screen against the value stored in the depth buffer for that screen-space pixel. If the depth of the pixel being rendered is less than or equal to the value already in the depth buffer, the pixel is drawn and the value in the depth buffer is updated to the depth of the newly drawn pixel. On the other hand, if the pixel being draw has a depth greater than the value already in the depth buffer, the pixel is discarded and the depth value in the depth buffer remains unchanged. The following code in the sample creates a depth buffer (a DepthStencil texture). It also creates a DepthStencilView of the depth buffer so that Direct3D 10 knows to use it as a Depth Stencil texture. // Create depth stencil texture D3D10_TEXTURE2D_DESC descDepth; descDepth.Width = width;
descDepth.Height = height; descDepth.MipLevels = 1; descDepth.ArraySize = 1; descDepth.Format = DXGI_FORMAT_D32_FLOAT; descDepth.SampleDesc.Count = 1; descDepth.SampleDesc.Quality = 0; descDepth.Usage = D3D10_USAGE_DEFAULT; descDepth.BindFlags = D3D10_BIND_DEPTH_STENCIL; descDepth.CPUAccessFlags = 0; descDepth.MiscFlags = 0; hr = g_pd3dDevice->CreateTexture2D( &descDepth, NULL, &g_pDepthStencil ); if( FAILED(hr) ) return hr; // Create the depth stencil view D3D10_DEPTH_STENCIL_VIEW_DESC descDSV; descDSV.Format = descDepth.Format; descDSV.ViewDimension = D3D10_DSV_DIMENSION_TEXTURE2D; descDSV.Texture2D.MipSlice = 0; hr = g_pd3dDevice->CreateDepthStencilView( g_pDepthStencil, &descDSV, &g_pDepthStencilView ); if( FAILED(hr) ) return hr;
In order to use this newly created depth stencil buffer, the tutorial must bind it to the device. This is done by passing the depth stencil view to the third parameter of the OMSetRenderTargets function. g_pd3dDevice->OMSetRenderTargets( 1, &g_pRenderTargetView, g_pDepthStencilView );
As with the render target, we must also clear the depth buffer before rendering. This ensures that depth values from previous frames do not incorrectly discard pixels in the current frame. In this the tutorials is actually setting the depth buffer to be the maximum amount (1.0). // // Clear the depth buffer to 1.0 (max depth) // g_pd3dDevice->ClearDepthStencilView( g_pDepthStencilView, D3D10_CLEAR_DEPTH, 1.0f, 0 );
Tutorial 06: Lighting
Summary In the previous tutorials, the world looks boring because all the objects are lit in the same way. This tutorial will introduce the concept of simple lighting and how it can be applied. The technique use will be lambertian lighting. The outcome of this tutorial will modify the previous example to include a light source. This light source will be attached to the cube in orbit. The effects of the light can be seen on the center cube.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial06
Lighting In this tutorial, the most basic type of light will be introduced: lambertian lighting. Lambertian lighting has uniform intensity irrespective of the distance away from the light. When the light hits the surface, the amount of light reflected is calculated by the angle of incidence the light has on the surface. When a light is shined directly on a surface, it is shown to reflect all the light back, with maximum intensity. However, as the angle of the light is increased, the intensity of the light will fade away. To calculate the intensity that a light has on a surface, the angle between the light direction and the normal of the surface has to be calculated. The normal for a surface is defined as a vector that is perpendicular to the surface. The calculation of the angle can be done with a simple dot product, which will return the projection of the light direction vector onto the normal. The wider the angle, the smaller the projection will be. Thus, this gives us the correct function to modulate the diffused light with.
The light source used in this tutorial is an approximation of directional lighting. The vector which describes the light source determines the direction of the light. Since it's an approximation, no matter where an object is, the direction in which the light shines towards it is the same. An example of this light source is the sun; the sun is always seen to be shining in the same direction for all objects in a scene. In addition, the intensity of the light on individual objects is not taken into consideration. Other types of light include point lights, which radiate uniform light from its center, and spot lights, which are directional but not uniform across all objects.
Initializing the Lights In this tutorial, there will be two light sources. One will be statically placed above and behind the cube, and another one will be orbiting the center cube. Note that the orbiting cube in the previous tutorial has been replaced with this light source. Since lighting is computed by the shaders, the variables would have to be declared and then bound to the variables within the technique. In this sample, we just require the direction of the light source,
as well as its color value. The first light is grey and not moving, while the second one is an orbiting red light. // Setup our lighting parameters D3DXVECTOR4 vLightDirs[2] = { D3DXVECTOR4( -0.577f, 0.577f, -0.577f, 1.0f ), D3DXVECTOR4( 0.0f, 0.0f, -1.0f, 1.0f ), }; D3DXVECTOR4 vLightColors[2] = { D3DXVECTOR4( 0.5f, 0.5f, 0.5f, 0.0f ), D3DXVECTOR4( 0.5f, 0.0f, 0.0f, 0.0f ) };
The orbiting light is rotated just like the cube in the last tutorial. The rotation matrix applied will change the direction of the light, to show the effect that it is always shining towards the center. Note that function D3DXVec3Transform is utilized to multiply a matrix with a vector. In the previous tutorial we were multiplying just the transformation matrices into the world matrix, then passed into the shader for transformation; but in this case, we're actually doing the world transform of the light in the CPU, for simplicity's sake. //rotate the second light around the origin D3DXMATRIX mRotate; D3DXVECTOR4 vOutDir; D3DXMatrixRotationY( &mRotate, -2.0f*t ); D3DXVec3Transform( &vLightDirs[1], (D3DXVECTOR3*)&vLightDirs[1], &mRotate );
The lights' direction and color are both passed into the shader just like the matrices. The associated variable is called to set, and the parameter is passed in. // // Update lighting variables // g_pLightDirVariable->SetFloatVectorArray( (float*)vLightDirs, 0, 2 ); g_pLightColorVariable->SetFloatVectorArray( (float*)vLightColors, 0, 2 );
Rendering the Lights in the Pixel Shader Once we have all the data setup and the shader properly fed with data, we can compute the lambertian lighting term on each pixel from the light sources. We'll be using the dot product rule discussed above. Once we've taken the dot product of the light versus the normal, it can then be multiplied with the color of the light to calculate the effect of that light. That value is passed through the saturate function, which converts the range to [0, 1]. Finally, the results from the two separate lights are summed together to create the final pixel color. Consider that the material of the surface itself is not factored into this light calculation; the final color of the surface is a result of the light's colors. // // Pixel Shader // float4 PS( PS_INPUT input) : SV_Target { float4 finalColor = 0; //do NdotL lighting for 2 lights for(int i=0; i<2; i++)
{
finalColor += saturate( dot( (float3)vLightDir[i],input.Norm) * vLightColor[i] ); } return finalColor; }
Once through the pixel shader, the pixels will have been modulated by the lights and you can see the effect of each light on the cube surface. Note that the light in this case looks flat because pixels on the same surface will have the same normal. Diffuse is a very simple and easy lighting model to compute; more complex lighting models can be used to achieve richer and more realistic materials.
Tutorial 7: Texture Mapping and Constant Buffers
Summary In the previous tutorial we introduced lighting to our project; now we will build on top of that by adding textures to our cube. In addition, we will be introducing the concept of constant buffers and how they can be used to speed up processing by minimizing bandwidth usage. The outcome of this tutorial will modify the center cube to have a texture mapped onto it. This tutorial concludes the introduction of the basic concepts in Direct3D 10. The tutorials following will build upon these concepts by introducing DXUT, mesh loading, as well an example of each shader.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial07
Texture mapping Texture mapping refers to the projection of a 2D image onto 3D geometry. We can think of it as wrapping a present, by placing decorative paper over an otherwise bland box. To do this, we have to specify how the points on the surface of the geometry correspond with the 2D image. The trick is to properly align the coordinates of the model with the texture. For complex models, it is difficult to determine the coordinates for the textures by hand. Thus, 3D modeling packages generally will export models with corresponding texture coordinates. Since our example is a cube, it is easy to determine the coordinates needed to match the texture. Texture coordinates are defined at the vertices and then interpolated for individual pixels on a surface.
Creating a Shader Resource from the Texture The texture is a 2D image that is retrieved from file and used to create a shader-resource view, so that it can be read from a shader. hr = D3DX10CreateShaderResourceViewFromFile( g_pd3dDevice, L"seafloor.dds", NULL, NULL, &g_pTextureRV, NULL );
Defining the Coordinates Before we can map the image onto our cube, we must first define the texture coordinates on each of the vertices of the cube. Since images can be of any size, the coordinate system used has been normalized to [0, 1]. The top left corner of the texture corresponds to (0,0) and the bottom right corner maps to (1,1). In this example, we're having the whole texture spread across each side of the cube. This simplifies the definition of the coordinates, without confusion. However, it is entirely possible to specify the texture to stretch across all 6 faces, although it's more difficult to define the points, and will appear stretched and distorted. First, we updated the structure used to define our vertices to include the texture coordinates.
struct SimpleVertex { D3DXVECTOR3 Pos; // Position D3DXVECTOR2 Tex; // Texture Coordinate };
Next, we updated the input layout to the shaders to also include these coordinates. // Define the input layout D3D10_INPUT_ELEMENT_DESC layout[] = { { L"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 }, { L"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 }, };
Since the input layout changed, the corresponding vertex shader input must also be modified to match the addition. struct VS_INPUT { float4 Pos : POSITION; float2 Tex : TEXCOORD; };
Finally, we are ready to include texture coordinates in our vertices we defined back in tutorial 4. Note the second parameter input is a D3DXVECTOR2 containing the texture coordinates. Each vertex on the cube will correspond to a corner of the texture. This creates a simple mapping where each vertex gets (0,0) (0,1) (1,0) or (1,1) as the coordinate. // Create vertex buffer SimpleVertex vertices[] = { { D3DXVECTOR3( -1.0f, { D3DXVECTOR3( 1.0f, { D3DXVECTOR3( 1.0f, { D3DXVECTOR3( -1.0f,
1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f, 1.0f ), D3DXVECTOR2( 1.0f, 1.0f, 1.0f ), D3DXVECTOR2( 0.0f,
0.0f 0.0f 1.0f 1.0f
) ) ) )
}, }, }, },
{ { { {
D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 0.0f, D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 1.0f, D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 0.0f,
0.0f 0.0f 1.0f 1.0f
) ) ) )
}, }, }, },
{ { { {
D3DXVECTOR3( D3DXVECTOR3( D3DXVECTOR3( D3DXVECTOR3(
-1.0f, -1.0f, 1.0f ), D3DXVECTOR2( -1.0f, -1.0f, -1.0f ), D3DXVECTOR2( -1.0f, 1.0f, -1.0f ), D3DXVECTOR2( -1.0f, 1.0f, 1.0f ), D3DXVECTOR2(
0.0f 0.0f 1.0f 1.0f
) ) ) )
}, }, }, },
{ { { {
D3DXVECTOR3( D3DXVECTOR3( D3DXVECTOR3( D3DXVECTOR3(
1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f, 1.0f ), D3DXVECTOR2(
{ { { {
D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR3( 1.0f, 1.0f, -1.0f ), D3DXVECTOR3( -1.0f, 1.0f, -1.0f ),
{ { { {
D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DXVECTOR3( 1.0f, 1.0f, 1.0f ), D3DXVECTOR3( -1.0f, 1.0f, 1.0f ),
0.0f, 1.0f, 1.0f, 0.0f,
D3DXVECTOR2( D3DXVECTOR2( D3DXVECTOR2( D3DXVECTOR2(
0.0f, 1.0f, 1.0f, 0.0f,
D3DXVECTOR2( D3DXVECTOR2( D3DXVECTOR2( D3DXVECTOR2(
0.0f, 1.0f, 1.0f, 0.0f,
0.0f 0.0f 1.0f 1.0f
0.0f, 1.0f, 1.0f, 0.0f,
) ) ) )
0.0f 0.0f 1.0f 1.0f
}, }, }, },
0.0f 0.0f 1.0f 1.0f
) ) ) ) ) ) ) )
}, }, }, }, }, }, }, },
};
When we sample the texture, we will need to modulate it with a material color for the geometry underneath.
Bind Texture as Shader Resource A texture is an object like the matrices and vectors that we have seen in previous tutorials. Before they can be used by the shader, they need to be set to the Effect. This can be done by getting a pointer to the variable, and then setting it as a shader resource. g_pDiffuseVariable = g_pEffect->GetVariableByName("txDiffuse")->AsShaderResource();
After the resource pointer has been retrieved, it can be used to hook the 2D texture resource view that we had initialized earlier. g_pDiffuseVariable->SetResource( g_pTextureRV );
There we go, now we're ready to use the texture within the shader.
Applying the Texture (fx) To actually map the texture on top of the geometry, we will be calling a texture lookup function within the pixel shader. The function, Sample will perform a texture lookup of a 2D texture, then return the sampled color. The pixel shader shown below calls this function and multiplies by the underlying mesh color (or material color) and then outputs the final color. • • •
txDiffuse is the object storing our texture that we passed in from the code above, when we bound the resource view g_pTextureRV to it. samLinear will be described below, it is the sampler specifications for the texture lookup. input.Tex is the coordinates of the texture that we have specified in the source
// Pixel Shader float4 PS( PS_INPUT input) : SV_Target { return txDiffuse.Sample( samLinear, input.Tex ) * vMeshColor; }
The variable samLinear is a structure that contains the information to tell the pixel shader how to sample the texture provided. In our case, we have a linear filter, and both our addresses wrap. These settings would be useful for simple textures, and an explanation of filters is beyond the scope of this tutorial. SamplerState { Filter = AddressU AddressV };
samLinear MIN_MAG_MIP_LINEAR; = Wrap; = Wrap;
Another thing we must remember to do is to pass the texture coordinates through the vertex shader, or else the data is lost when it gets to the pixel shader. Here, we just copy the input's coordinates to the output, and let the hardware handle the rest. // Vertex Shader PS_INPUT VS( VS_INPUT input ) { PS_INPUT output = (PS_INPUT)0;
output.Pos output.Pos output.Pos output.Tex
= = = =
mul( input.Pos, World ); mul( output.Pos, View ); mul( output.Pos, Projection ); input.Tex;
return output; }
Constant Buffers Starting with Direct3D 10, an application can use a constant buffer to set shader constants (shader variables). Constant buffers are declared using a syntax similar to C-style structs. The best way to efficiently use constant buffers is to organize shader variables into constant buffers based on their frequency of update. This allows an application to minimize the bandwidth required for updating shader constants. As an example, this tutorial groups constants into three structures: one for variables that change every frame, one for variables that change only when a window size is changed, and one for variables that are set once and then do not change. cbuffer cbNeverChanges { matrix View; }; cbuffer cbChangeOnResize { matrix Projection; }; cbuffer cbChangesEveryFrame { matrix World; float4 vMeshColor; };
DXUT Tutorials This section serves as an introduction to DXUT. DXUT is a robust framework to create their own custom applications in, and lets developers forego the tedious process behind setting up the environment. DXUT provides users with a simplified process of creating of a window, selection and creation of a Direct3D device, and handling Windows messages. This section picks up right after Tutorial 7 in the Basic Tutorials section. It converts the work created up to Tutorial 7 to DXUT, and starts from there. Tutorial 8: Introduction to DXUT Tutorial 9: Meshes in DXUT Tutorial 10: Advanced DXUT
Tutorial 8: Introduction to DXUT
Summary This tutorial introduces DXUT. DXUT is a layer that is built on top of Direct3D to help make samples, prototypes, tools, and professional games more robust and easier to build. It simplifies Windows and Direct3D APIs, based on typical usage. The result of this tutorial is a sample that looks just like the previous tutorial. However, it is implemented differently.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial08
DXUT DXUT provides a simplified process for creating a window, creating (or selecting) a Direct3D device, and handling Windows messages. This allows you to spend less time worrying about how to perform these standard tasks. DXUT for Direct3D 10 is also highly componentized. The core component of DXUT contains the standard window creation, device creation, and management functions. Optional components of DXUT include functions such as camera manipulation, a GUI system, and mesh handling. The present tutorial explains the DXUT core component. Optional components are explained in the following tutorials. Features introduced in this tutorial include device creation, the main loop, and simple keyboard input. DXUT exposes a wide range of callback functions that the user can hook into. The callbacks are called by DXUT at logical points during program execution. You can insert custom code into the callbacks to build the application, while DXUT manages the window and device management implementation requirements. DXUT supports applications that contain Direct3D 9 and/or Direct3D 10 code paths. This allows DXUT to pick the best code path for the system the application is running on. However, this tutorial focuses only on Direct3D 10 rendering. For an example of a dual Direct3D 9 and Direct3D 10 application built on DXUT, see the BasicHLSL10 sample in the DirectX SDK.
Setting Callback Functions Modifying the WinMain function of an application is the first step. In previous tutorials, WinMain invoked the initialization function, and then entered the message loop. In the DXUT framework, WinMain behaves similarly. First, the callback functions are set by the application. These are the functions that DXUT calls during specific events when the application is run. Notable events include device creation, swap chain creation, keyboard entry, frame move, and frame rendering. // Direct3D10 callbacks DXUTSetCallbackD3D10DeviceAcceptable( IsD3D10DeviceAcceptable ); DXUTSetCallbackD3D10DeviceCreated( OnD3D10CreateDevice ); DXUTSetCallbackD3D10SwapChainResized( OnD3D10ResizedSwapChain ); DXUTSetCallbackD3D10SwapChainReleasing( OnD3D10ReleasingSwapChain ); DXUTSetCallbackD3D10DeviceDestroyed( OnD3D10DestroyDevice );
DXUTSetCallbackD3D10FrameRender( OnD3D10FrameRender ); DXUTSetCallbackMsgProc( MsgProc ); DXUTSetCallbackKeyboard( KeyboardProc ); DXUTSetCallbackFrameMove( OnFrameMove ); DXUTSetCallbackDeviceChanging( ModifyDeviceSettings );
Then the application sets any additional DXUT settings, as in the following example: // Show the cursor and clip it when in full screen DXUTSetCursorSettings( true, true );
Finally, the initialization functions are called. The difference between this tutorial and the basic tutorials is that you have to deal only with application specific code during initialization. This is because the device and window creation is handled by DXUT. Application specific code can be inserted during each of the associated callback functions. DXUT invokes these functions as it progresses through the initialization process. // Initialize DXUT and create the desired Win32 window and Direct3D // device for the application. Calling each of these functions is optional, but they // allow you to set options that control the behavior of the framework. DXUTInit( true, true ); // Parse the command line, handle the default hotkeys, and show msgboxes DXUTCreateWindow( L"Tutorial8" ); DXUTCreateDevice( true, 640, 480 );
Debugging with DXUTTrace DXUTTrace is a macro that can be called to create debug output for the application. It functions much like the standard debug output functions, but it allows a variable number of arguments. For example: DXUTTRACE( L"Hit points left: %d\n", nHitPoints );
In this tutorial, it is placed next to function entry, to report the state of the application. For instance, at the beginning of OnD3D10ResizedSwapChain(), the debugger reports "SwapChain Reset called". DXUTTRACE( L"SwapChain Reset called\n" );
Device Management and Initialization DXUT provides various methods for creating and configuring the window and the Direct3D device. The methods that are used in this tutorial are listed below. They are sufficient for a moderately complex Direct3D application. The procedure from the previous tutorial inside InitDevice() and CleanupDevice() has been properly managed by splitting it into the following functions. • • • •
IsD3D10DeviceAcceptable Callback ModifyDeviceSettings Callback OnD3D10CreateDevice Callback OnD3D10DestroyDevice Callback
Because not all resources are created at the same time, we can minimize overhead by reducing the number of repeated calls. We achieve this goal by recreating only resources that are context dependent. For the sake of simplicity, previous tutorials recreated everything whenever the screen was resized.
IsD3D10DeviceAcceptable Callback This function is invoked for each combination of valid device settings that is found on the system. It allows the application to accept or reject each combination. For example, the application can reject all REF devices, or it can reject full screen device setting combinations. In this tutorial, all devices are acceptable, because we do not need any advanced functionality.
ModifyDeviceSettings Callback This callback function allows the application to change any device settings immediately before DXUT creates the device. In this tutorial, nothing needs to be done in the callback, because we do not need any advanced functionality.
OnD3D10CreateDevice Callback This function is called after Direct3D10 device is created. After the device is created, an application can use this callback to allocate resources, set buffers, and handle other necessary tasks. In this tutorial, most of the InitDevice() function from tutorial 7 is copied into this callback function. The code for device and swap chain creation is omitted, because these functions are handled by DXUT. OnD3D10CreateDevice creates the effect, the vertex/index buffers, the textures, and the transformation matrices. The code has been copied from tutorial 7 with minimal alterations.
OnD3D10DestroyDevice Callback This callback function is called immediately before the ID3D10Device is released by DXUT. This callback is used to release resources that were used by the device. In this tutorial, OnD3D10DestroyDevice releases the resources that were created by the OnD3D10CreateDevice function. These resources include the vertex/index buffers, the layout, the textures, and the effect. The code is copied from the CleanupDevice() function, but it has been changed to use the SAFE_RELEASE macro.
Rendering For rendering, DXUT provides two callback functions to initiate rendering for your application. The first, OnFrameMove, is called before each frame is rendered. It advances time in the application. The second, OnD3D10FrameRender, provides rendering for DXUT. These two functions split the work of the Render() function into logical steps. OnFrameMove updates all the matrices for animation. OnD3D10FrameRender contains the rendering calls.
OnFrameMove Callback This function is called before a frame is rendered. It is used to process the world state. However, its update frequency depends on the speed of the system. On faster systems, it is called more often per second. This means that any state update code must be regulated by time. Otherwise, it performs differently on a slower system than on a faster system. Every time the function is called in the tutorial, the world is updated once, and the mesh color is adjusted. This code is copied directly from the Render() function in tutorial 7. Note that the rendering calls are not included.
OnD3D10FrameRender Callback This function is called whenever a frame is redrawn. Within this function, effects are applied, resources are associated, and the drawing for the scene is called. In this tutorial, this function contains the calls to clear the back buffer and stencil buffer, set up the matrices, and draw the cubes.
Message Processing Message processing is an intrinsic property of all window applications. DXUT exposes these messages for advanced applications.
MsgProc Callback DXUT invokes this function when window messages are received. The function allows the application to handle messages as it sees fit. In this tutorial, no additional messages are handled.
KeyboardProc Callback This callback function is invoked by DXUT when a key is pressed. It can be used as a simple function to process keyboard commands. It is not used in this tutorial, because keyboard interaction is not necessary. However, there is a skeleton case for F1, which you can experiment with. Try to insert code to toggle the rotation of the cubes.
Summary The functions covered in this tutorial can get you started with a project on DXUT. Tutorials 9 and 10 cover more advanced DXUT features. To start a new project using DXUT, you can use the Sample Browser to copy and rename any sample project in the DirectX SDK. The EmptyProject sample is a good blank starting point for DXUT applications, or you can choose another sample.
Tutorial 9: Meshes in DXUT
Summary This tutorial introduces the use of meshes to import source art. It demonstrates meshes by using DXUT. However, meshes can also be used without DXUT. In the tutorial, you replace a cube in the center of the window with a model that is imported from a file. The model already contains a texture with mapped coordinates.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial09
Meshes Listing each vertex within the source code is a tedious and crude method to define source art. Instead, there are methods to import already constructed models into your application. A mesh is a collection of predefined vertex data which often includes additional information such as normal vectors, colors, materials, and texture coordinates. As long the format of the mesh file is known, the mesh file can be imported and rendered. Meshes are used to maintain source art separately from application code, so that the art can be reused. Therefore, meshes are stored in separate files. They are rarely generated in an application. Instead, they are created in 3D authoring software. The cube that was generated in previous tutorials can be saved into a separate file, and then re-loaded. However, importing can be much more efficient, especially if the mesh scales in complexity. One of the biggest benefits is the ability to import the texture coordinates, so that they match up perfectly. These can easily be specified in authoring applications. DXUT handles meshes by using the CDXUTMesh10 class, which is a wrapper for the D3DX Mesh class. This class includes functions to import, render, and destroy. The file format we use for importing is the .X file. There are many freely available converters that convert other model formats to the .X format.
Creating the Mesh To import the model, we first create a CDXUTMesh10 object. It is called g_Mesh in this case, but in general the object name and the source art name should match, for easier association. CDXUTMesh10 g_Mesh;
Next, we call the Create function to read the X file and store it into the object. The Create function requires that we pass in the D3D10Device, the name of the X file holding our mesh, the layout, and the number of elements in the layout. There is also an optional parameter called optimize, which calls D3DX functions to reorder the faces and vertices of a given mesh. This can improve rendering performance. In this case, we import a file that is called "tiny.x". The format for the vertices that define this mesh contains vertex coordinates, normals, and texture coordinates. We must specify our input layout to match, as shown in the following example.
// Define the input layout const D3D10_INPUT_ELEMENT_DESC layout[] = { { L"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 }, { L"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 }, { L"TEXCOORD0", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D10_INPUT_PER_VERTEX_DATA, 0 }, };
After the layout is defined, we call the Create function to import the model. The last parameter is the number of elements in the vertex format. Because we have coordinates, normal, and texture coordinates, we specify 3. // Load the mesh V_RETURN( g_Mesh.Create( pd3dDevice, L"tiny.x", (D3D10_INPUT_ELEMENT_DESC*)layout, 3 ) );
If there are no errors, g_Mesh now contains the vertex and index buffer for our newly imported mesh, as well as textures and materials. Next we begin rendering.
Rendering the Mesh In the previous tutorials, because we had explicit control of the vertex buffer and the index buffers, we had to set them up properly before every frame. However, with a mesh, these elements are abstracted. Therefore, we have to provide only the effects technique to use to render the mesh, and it does all the work. The difference from the previous tutorial is that we removed the portion of OnD3DFrameRender where it began rendering the cube. We replaced it with the mesh rendering call. It is always good practice to set the correct input layout before any mesh is rendered. This ensures that the layout of the mesh matches the input assembler. // // Set the Vertex Layout // pd3dDevice->IASetInputLayout( g_pVertexLayout );
The actual rendering is done by calling the Render function inside the CDXUTMesh10 class. After the technique's buffers are correctly associated, it can be rendered. To render, we pass in the D3D10Device, the Effect, and the Technique within that effect. // // Render the mesh // g_Mesh.Render( pd3dDevice, g_pEffect, g_pTechnique );
If the world matrix is properly set for the model, we can now see that the cube in our previous example is replaced by this more complicated mesh. Notice that the mesh is much bigger than the cube. Try applying a scale to the world matrix to get the mesh to fit the screen better.
Destroying the Mesh Like all objects, the CDXUTMesh10 object must be destroyed after usage. This is done by calling the Destroy function. g_Mesh.Destroy();
Tutorial 10: Advanced DXUT
Summary This tutorial covers the advanced concepts with DXUT. Most of the functionality that is demonstrated in this tutorial is optional. However, it can be used to enhance your application with minimal cost. DXUT provides a simple sprite based GUI system and a device settings dialog. In addition, it provides a few types of camera classes. In this tutorial, you create a fully functional GUI to modify settings by using the device and scene. There will be buttons, sliders, and text to demonstrate these capabilities.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial10
DXUT Camera The CModelViewerCamera class within DXUT is provided to simplify the management of the view and projection transformations. It also provides GUI functionality. CModelViewerCamera g_Camera;
// A model viewing camera
The first function that the camera provides is the creation of the view and projection matrices. With the camera, there is no need to worry about these matrices. Instead, specify the viewer location, the view itself, and the size of the window. Then, pass these parameters to the camera object, which creates the matrices behind the scene. The following example sets the view portion of the camera. This includes location and view. // Initialize the camera D3DXVECTOR3 Eye( 0.0f, 0.0f, -800.0f ); D3DXVECTOR3 At( 0.0f, 0.0f, 0.0f ); g_Camera.SetViewParams( &Eye, &At );
Next, specify the projection portion of the camera. We need to provide the viewing angle, the aspect ratio, and the near and far clipping planes for the view frustum. This is the same information that was required in previous tutorials. However, in this tutorial we do not worry about creating the matrices themselves. // Setup the camera's projection parameters float fAspectRatio = pBackBufferSurfaceDesc->Width / (FLOAT)pBackBufferSurfaceDesc->Height; g_Camera.SetProjParams( D3DX_PI/4, fAspectRatio, 0.1f, 5000.0f ); g_Camera.SetWindow( pBackBufferSurfaceDesc->Width, pBackBufferSurfaceDesc>Height );
The camera also creates masks for simple mouse feedback. Here, we specify three mouse buttons to utilize for the mouse operations that are provided—model rotation, zooming, and camera rotation. Try compiling the project and playing with each button to understand what each operation does. g_Camera.SetButtonMasks( MOUSE_LEFT_BUTTON, MOUSE_WHEEL, MOUSE_MIDDLE_BUTTON );
After the buttons are set, the camera listens for mouse inputs and acts accordingly. To respond to user input, add a listener to the MsgProc callback function. This is the function that DXUT routes messages to. // Pass all remaining windows messages to camera so it can respond to user input g_Camera.HandleMessages( hWnd, uMsg, wParam, lParam );
Finally, after all data is entered into the camera, we extract the actual matrices for the transformations. We grab the projection matrix and the view matrix, together with the associated functions. The camera object is responsible for computing the matrices themselves. g_pProjectionVariable->SetMatrix( (float*)g_Camera.GetProjMatrix() ); g_pViewVariable->SetMatrix( (float*)g_Camera.GetViewMatrix() );
DXUT Dialogs User interaction can be accomplished by using the class CDXUTDialog. This contains controls in a dialog that accepts user input, and passes it to the application to handle. First, the dialog class is instantiated. Then, individual controls can be added.
Declarations In this tutorial, two dialogs are added. One is called g_HUD, and it contains the same code as the Direct3D 10 samples. The other is called g_SampleUI, and it demonstrates functions that are specific to this tutorial. The second dialog is used to control the "puffiness" of the model. It sets a variable that is passed into the shaders. CDXUTDialog g_HUD; // manages the 3D UI CDXUTDialog g_SampleUI; // dialog for sample specific controls
The dialogs are controlled by a class called CDXUTDialogResourceManager. It passes messages and handles resources that are shared by the dialogs. CDXUTDialogResourceManager g_DialogResourceManager; // manager for shared resources of dialogs
Finally, a new callback function is associated with the events that are processed by the GUI. This function is used to handle the interaction between the controls. void CALLBACK OnGUIEvent( UINT nEvent, int nControlID, CDXUTControl* pControl, void* pUserContext );
Dialog Initialization Because more utilities have been introduced, and need to be initialized, this tutorial moves the initialization of these modules to a separate function, called InitApp(). The controls for each dialog are initialized inside this function. Each dialog calls its Init function, and passes in the resource manager to specify where control should be placed. It also sets the callback function to process the GUI responses. In this case, the associated callback function is OnGUIEvent. g_HUD.Init( &g_DialogResourceManager ); g_SampleUI.Init( &g_DialogResourceManager ); g_HUD.SetCallback( OnGUIEvent ); g_SampleUI.SetCallback( OnGUIEvent );
After each dialog is initialized, it can insert the controls to use. The HUD adds three buttons for the basic functionality: toggle fullscreen, toggle reference (software) renderer, and change device. To add a button, specify the IDC identifier to use, a string to display, the coordinates, the width and length, and optionally a keyboard shortcut to associate with the button. Note that the coordinates are relative to the anchor of the dialog. int iY = 10; g_HUD.AddButton( IDC_TOGGLEFULLSCREEN, L"Toggle full screen", 35, iY, 125, 22 ); g_HUD.AddButton( IDC_TOGGLEREF, L"Toggle REF (F3)", 35, iY += 24, 125, 22 ); g_HUD.AddButton( IDC_CHANGEDEVICE, L"Change device (F2)", 35, iY += 24, 125, 22, VK_F2 );
Similarly for the sample UI, three controls are added—one static text, one slider, and one checkbox. The static text parameters are the IDC identifier, the string, the coordinates, and the width and height. The slider parameters are the IDC identifier, the coordinates, the width and height, then the min and max values of the slider, and finally the variable to store the result. The checkbox parameters are the IDC identifier, a string label, the coordinates, the width and height, and the Boolean variable to store the result. iY = 10; WCHAR sz[100]; iY += 24; StringCchPrintf( sz, 100, L"Puffiness: %0.2f", g_fModelPuffiness ); g_SampleUI.AddStatic( IDC_PUFF_STATIC, sz, 35, iY += 24, 125, 22 ); g_SampleUI.AddSlider( IDC_PUFF_SCALE, 50, iY += 24, 100, 22, 0, 2000, (int) (g_fModelPuffiness*100.0f) ); iY += 24; g_SampleUI.AddCheckBox( IDC_TOGGLESPIN, L"Toggle Spinning", 35, iY += 24, 125, 22, g_bSpinning );
After the dialogs are initialized, they must be placed on the screen. This is done during the OnD3D10ResizedSwapChain call, because the screen coordinates can change every time the swap chain is recreated—for example, if the window is resized. g_HUD.SetLocation( pBackBufferSurfaceDesc->Width-170, 0 ); g_HUD.SetSize( 170, 170 ); g_SampleUI.SetLocation( pBackBufferSurfaceDesc->Width-170, pBackBufferSurfaceDesc->Height-300 ); g_SampleUI.SetSize( 170, 300 );
Finally, the dialogs must be identified in the OnD3D10FrameRender function. This allows the dialogs to be drawn so that the user can actually see them. // // Render the UI // g_HUD.OnRender( fElapsedTime ); g_SampleUI.OnRender( fElapsedTime );
Resource Manager Initialization The resource manager must be initialized at every callback that is associated with initialization and destruction. This is because the GUI must be recreated whenever a device is created, or a swap
chain is recreated. The CDXUTDialogResourceManager class contains functions that correspond to each callback. Each function has the same name as its corresponding callback. All we need to do to is insert the code to call them in the appropriate places. V_RETURN( g_DialogResourceManager.OnD3D10CreateDevice( pd3dDevice ) ); V_RETURN( g_DialogResourceManager.OnD3D10ResizedSwapChain( pd3dDevice, pBackBufferSurfaceDesc ) ); g_DialogResourceManager.OnD3D10ReleasingSwapChain(); g_DialogResourceManager.OnD3D10DestroyDevice();
Responding to GUI Events After everything is initialized, we can write code to handle the GUI interaction. During initialization of the dialogs, we set the callback function to OnGUIEvent. Now we create the OnGUIEvent function, which listens for events that are related to the GUI and then processes them. (The GUI is invoked by the framework.) OnGUIEvent is a simple function that contains a case statement for each IDC identifier that was listed when the dialogs were created. Each case statement contains the handler code, with the assumption that the user interacts with the control. The code here is very similar to the Win32 code that handles controls. The controls that are related to the HUD call functions that are built into DXUT. There is a DXUT function to switch between fullscreen and windowed mode, to switch the reference software renderer on and off, and to change the device settings. The SampleUI dialog contains custom code to manipulate the variables that are associated with the slider. It gathers the value, updates the associated text, and passes the value to the shader. void CALLBACK OnGUIEvent( UINT nEvent, int nControlID, CDXUTControl* pControl, void* pUserContext ) { switch( nControlID ) { case IDC_TOGGLEFULLSCREEN: DXUTToggleFullScreen(); break; case IDC_TOGGLEREF: DXUTToggleREF(); break; case IDC_CHANGEDEVICE: g_D3DSettingsDlg.SetActive( ! g_D3DSettingsDlg.IsActive() ); break; case IDC_TOGGLESPIN: { g_bSpinning = g_SampleUI.GetCheckBox( IDC_TOGGLESPIN )>GetChecked(); break; } case IDC_PUFF_SCALE: { g_fModelPuffiness = (float) (g_SampleUI.GetSlider( IDC_PUFF_SCALE )->GetValue() * 0.01f); WCHAR sz[100]; StringCchPrintf( sz, 100, L"Puffiness: %0.2f", g_fModelPuffiness
);
g_SampleUI.GetStatic( IDC_PUFF_STATIC )->SetText( sz ); g_pPuffiness->SetFloat( g_fModelPuffiness ); break; }
}
}
Updating Message Processing Now we have dialog messages and user interactions. Messages that are passed to the application must be handled by the dialogs. The relevant code is handled within the MsgProc callback that is provided by DXUT. In previous tutorials, this section is empty, because there are no messages to be processed. Now, we must make sure that messages intended for the resource manager and dialogs are properly routed. No special message processing code is required. We just call the MsgProcs for each dialog, to ensure that the message is handled. This is done by calling the MsgProc function that corresponds to each class. Note that the function provides a flag to notify the framework that no further processing of the message is required, and therefore the framework can exit. LRESULT CALLBACK MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, bool* pbNoFurtherProcessing, void* pUserContext ) { // Always allow dialog resource manager calls to handle global messages // so GUI state is updated correctly *pbNoFurtherProcessing = g_DialogResourceManager.MsgProc( hWnd, uMsg, wParam, lParam ); if( *pbNoFurtherProcessing ) return 0; if( g_D3DSettingsDlg.IsActive() ) { g_D3DSettingsDlg.MsgProc( hWnd, uMsg, wParam, lParam ); return 0; } // Give the dialogs a chance to handle the message first *pbNoFurtherProcessing = g_HUD.MsgProc( hWnd, uMsg, wParam, lParam ); if( *pbNoFurtherProcessing ) return 0; *pbNoFurtherProcessing = g_SampleUI.MsgProc( hWnd, uMsg, wParam, lParam ); if( *pbNoFurtherProcessing ) return 0; if( uMsg == WM_CHAR && wParam == '1' ) DXUTToggleFullScreen(); return 0; }
3D Settings Dialog There is a special built-in dialog that controls the settings of the Direct3D device. This dialog is provided by DXUT as CD3DSettingsDlg. It functions like a custom dialog, but it provides all the options that users need to modify settings. CD3DSettingsDlg
g_D3DSettingsDlg;
// Device settings dialog
Initialization is much like other dialogs. Simply call the Init function. However, every time Direct3D changes its swap chain or device, the dialog must also be updated. Therefore, it must include an appropriately named call within OnD3D10CreateDevice and
OnD3D10ResizedSwapChain. Likewise, changes to destroyed objects must also be notified. Therefore, we need the appropriate calls within OnD3D10DestroyDevice. g_D3DSettingsDlg.Init( &g_DialogResourceManager ); V_RETURN( g_D3DSettingsDlg.OnD3D10CreateDevice( pd3dDevice ) ); V_RETURN( g_D3DSettingsDlg.OnD3D10ResizedSwapChain( pd3dDevice, pBackBufferSurfaceDesc ) ); g_D3DSettingsDlg.OnD3D10DestroyDevice();
On the rendering side, to switch the appearance of the dialog, we use a flag called IsActive(). If this flag is set to false, then the panel is not rendered. Switching the panel is handled by the HUD dialog. The IDC_CHANGEDEVICE that is associated with the HUD controls this flag. if( g_D3DSettingsDlg.IsActive() ) { g_D3DSettingsDlg.MsgProc( hWnd, uMsg, wParam, lParam ); return 0; }
After the initialization steps are complete, you can include the dialog in your application. Try compiling the tutorial and interacting with the Change Settings panel to see its effect. The reconstruction of the Direct3D device or swap chain is done internally by DXUT.
Text Rendering An application is not very interesting if the user has no idea what to do. DXUT includes a utility class to draw 2D text onto the screen, for feedback to the user. This class, CDXUTD3D10TextHelper, allow you to draw lines of text anywhere on the screen, by using simple string inputs. First, we instantiate the class. Because text rendering can be isolated from most of the initialization procedures, we keep most of the code within RenderText10. CDXUTD3D10TextHelper txtHelper( g_pFont, g_pSprite, 15 );
Initialization The first parameter passed in is the font to draw. This font is of the type ID3DXFont, which is provided by D3DX. To initialize the font, call D3DX10CreateFont, and pass in the device, height, width, weight, mipmap levels (generally 1), italics, character set, precision, quality, pitch and family, font face name, and the pointer to the object. Only the first four and the last two of these are really significant. V_RETURN( D3DX10CreateFont( pd3dDevice, 15, 0, FW_BOLD, 1, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Arial", &g_pFont ) );
The second parameter requires that we initialize an ID3DXSprite class. To do this, we call D3DX10CreateSprite. The only things the function requires as parameters are the device, the maximum number of sprites ever drawn in one frame, and the pointer to the object. // Initialize the sprite V_RETURN( D3DX10CreateSprite( pd3dDevice, MAX_SPRITES, &g_pSprite ) );
As with all other objects, the font and sprite must be destroyed after we have finished with them. This can be accomplished by using the SAFE_RELEASE macro.
SAFE_RELEASE( g_pFont ); SAFE_RELEASE( g_pSprite );
Rendering The text in this sample includes statistics on the rendering. There is also a help section that explains how to manipulate the model by using the mouse. The rendering calls must be done within OnD3D10FrameRender. Here, we call RenderText10 within the frame render call. The first section is always rendered first. The first call to text rendering is Begin(). This notifies the engine to start sending text to the screen. Next, we set the position of the cursor and the color of the text. Now we can draw. Text string output is performed by calling DrawTextLine. Pass in the string, and output that corresponds to the string is provided at the current position. The cursor is incremented while text is written. For example, if the string contains "\n", the cursor is automatically moved to the next line. // Output statistics txtHelper.Begin(); txtHelper.SetInsertionPos( 2, 0 ); txtHelper.SetForegroundColor( D3DXCOLOR( 1.0f, 1.0f, 0.0f, 1.0f ) ); txtHelper.DrawTextLine( DXUTGetFrameStats() ); txtHelper.DrawTextLine( DXUTGetDeviceStats() );
There is another method to provide text output, which is similar to printf. Format the string by using special characters, and then yinsert variables into the string. Use DrawFormattedTextLine for this purpose. txtHelper.SetForegroundColor( D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f ) ); txtHelper.DrawFormattedTextLine( L"fTime: %0.1f sin(fTime): %0.4f", fTime, sin(fTime) );
Because the help is drawn in the same way, we do not need to review its code. You can reposition the pointer at any time by calling SetInsertionPos. When you are satisfied with the text output, call End() to notify the engine.
Shader Tutorials This section will focus on the shader stages within the graphics pipeline. Programmable shaders have become the emphasis for Direct3D 10. The tutorials in this section are meant to show the users the capabilities of each stage, by implementing a simple technique in each of those. They serve as a purpose to familiarize a developer with the concepts. Specific techniques are not demonstrated, rather, they can be explored in the Direct3D 10 Samples included in the SDK. Tutorial 11: Vertex Shaders Tutorial 12: Pixel Shaders Tutorial 13: Geometry Shaders
Tutorial 11: Vertex Shaders
Summary This tutorial will emphasize on the capabilities of the vertex shader. It is meant to show the users the possibilities of allowing manipulation of vertices. The outcome of this tutorial will be a wave effect imposed on the character model from the previous tutorial. This effect is done entirely by the GPU with no CPU interaction with the data.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial11
Vertex Shader While previous tutorials already have shown the use of the vertex shader, this tutorial is meant to emphasize this stage in the pipeline. Vertices contain various types of information such as coordinates, normals, texture coordinates, materials, colors, and even custom data. For example, if you look under the ParticlesGS sample, you will see that each vertex passed into the pipeline will also contain a variable called Timer, which can vary the appearance of the particle over time. The purpose of the vertex shader is to offload intensive calculations from the CPU, onto the GPU. This frees up the CPU to accomplish other tasks within the application. For example, games often offload the graphics processing to the GPU, while the AI and physics is done on the CPU itself. This tutorial is meant to show the possibilities of the vertex shader, rather than to teach specific techniques. It is encouraged that you try new effects and experiment with the results. For specific methods to achieve effects, refer to the samples included with the SDK. However, do note that they are more complex in nature and apply much more advanced concepts.
Creating the Wave Effect The wave effect on the model is created by modulating the x position of each point by a sine wave. The sine wave is controlled by the y position, thus creating a wave along the model. However, to make the wave move and appear animated, the sine wave is also modulated by a time variable. Finally, there is a variable to control the amount of displacement by the vertex shader, and that is the waviness variable specified. This variable is passed into the shader and controlled by a GUI slider. output.Pos.x += sin( output.Pos.y*0.1f + Time )*Waviness;
After this manipulation is done, the vertex shader will still have to prepare the vertices for display. Thus, there is still the transformation of the world, view, and projection matrices. However, we chose to do the world transformation before the wave effect, as it would be easier to determine the effects on the screen, since the x and y axis are apparent. output.Pos = mul( output.Pos, View ); output.Pos = mul( output.Pos, Projection );
Tutorial 12: Pixel Shaders
Summary This tutorial focuses on the pixel shader and its capabilities. There are many things that can be accomplished with the pixel shader, and some of the more common functions are listed. The pixel shader will apply an environment map to the object. The outcome of the tutorial is that the model will become shiny, and it will reflect its surrounding environment.
Source SDK root\Samples\C++\Direct3D10\Tutorials\Tutorial12
Pixel Shaders Pixel shaders are used to manipulate the final color of a pixel before it reaches the output merger. Each pixel that is rendered will be passed through this shader before output. Once the pixel has passed through the pixel shader, the only operations that can be performed are those performed by the output merger, such as alpha blending, depth testing, stencil testing, and so on. The pixel shader evolved from the texture mapping found in early hardware. Instead of just a simple texture lookup, it is possible to compute the final color from multiple sources, as well as alter it according to the vertex data. For general applications, however, a pixel shader does multiple lookups on different textures.
Environment Map On surfaces which are reflective, it is not possible to just do a texture lookup on fixed texture coordinates to determine the final color. This is because whenever the object or viewer moves, the reflection alters. Thus, we must update reflection every time something moves. An effective method to trick the observer to believe it's a dynamic reflection of the environment is to generate a special texture that can wrap around the object. This texture is referred as a cube map. A cube map is effectively placing an object in the middle of a cube, with each face of the cube being a texture. An environment map is a cube map which has the textures which correspond to the view of the environment on that face. If the environment is static, these environment maps can be pre-generated. If the environment is dynamic, the cubemap needs to be updated on-the-fly. See the CubeMapGS sample for an illustration of how to do this. From this environment map, we can calculate what a camera will see as the reflection. First we can find the direction the camera is viewing the object, and from that, reflect it off the normal of each pixel and perform a lookup based on that reflected vector.
Setting up the Environment Map The setup of the environment map is not the emphasis of this tutorial. The procedure to follow is very similar to that of a normal texture map. Please refer to Tutorial 7, Texture Mapping and Constant Buffers, for an explanation of how to properly initialize a texture map and its associated
resource view. // Load the Environment Map ID3D10Resource *pResource = NULL; V_RETURN( DXUTFindDXSDKMediaFileCch( str, MAX_PATH, L"Lobby\\LobbyCube.dds" ) ); V_RETURN( D3DX10CreateTextureFromFile( pd3dDevice, str, NULL, NULL, &pResource ) ); if(pResource) { g_pEnvMap = (ID3D10Texture2D*)pResource; pResource->Release(); D3D10_TEXTURECUBE_DESC desc; g_pEnvMap->GetDesc( &desc ); D3D10_SHADER_RESOURCE_VIEW_DESC SRVDesc; ZeroMemory( &SRVDesc, sizeof(SRVDesc) ); SRVDesc.Format = desc.Format; SRVDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURECUBE; SRVDesc.TextureCube.MipLevels = desc.MipLevels; SRVDesc.TextureCube.MostDetailedMip = 0; V_RETURN(pd3dDevice->CreateShaderResourceView( g_pEnvMap, &SRVDesc, &g_pEnvMapSRV )); } // Set the Environment Map g_pEnvMapVariable->SetResource( g_pEnvMapSRV );
Implementing the Lookup We will step through the simple algorithm described above and perform a proper lookup of an environment map. The calculations are done in the vertex shader and then interpolated to the pixel shader for the lookup. It is better to compute it in the vertex shader and interpolate to the pixel shader since there are fewer computations necessary. To compute the reflected vector for the lookup, we require two pieces of information. First is the normal of the pixel in question, second is the direction of the eye. Since the operation is done in view space, we must transform the pixel's normal to proper viewing space. float3 viewNorm = mul( input.Norm, (float3x3)View );
Next we need to find the direction of the camera. Note, however, that we are already in view space, and thus, the camera's direction is that of the Z-axis (0,0,-1.0), since we are staring directly at the object. Now that we have our two pieces of information, we can calculate the reflection of the vector with the reflect command. The variable ViewR is used to store the resulting reflection for processing in the pixel shader. output.ViewR = reflect( viewNorm, float3(0,0,-1.0) );
Once the correct vector has been calculated in the vertex shader, we can process the environment map in the pixel shader. This is done by calling a built-in function to sample the environment map and return the color value from that texture. // Load the environment map texture float4 cReflect = g_txEnvMap.Sample( samLinearClamp, viewR );
Since it is all normalized (and texture coordinates range from 0 to 1), the x and y coordinates will
correspond directly to the texture coordinates that it needs to look up. As a bonus, we included the code to do the original flat texture lookup, and computed that as the diffuse term. To play with the amount of reflection, you can modulate cReflect by a scaling factor against cDiffuse and experiment with the results. You can have a very reflective character or a somewhat dull character.
Tutorial 13: Geometry Shaders
Summary This tutorial will explore a part of the graphics pipeline that has not been touched in the previous tutorials. We will be touching upon some basic geometry shader functionality. The outcome of this tutorial is that the model will have a second layer extruded from the model. Note that the original model is still preserved at the center.
Source SDK root\Samples\C++\Direct3D10\Tutorials\Tutorial13
Geometry Shader The benefit of the geometry shader (GS) is that it allows manipulation of meshes on a per-primitive basis. Instead of running a computation on each vertex individually, there is the option to operate on a per-primitive basis. That is, vertices can be passed in as a single vertex, a line segment (two vertices), or as a triangle (three vertices). By allowing manipulation on a per-primitive level, new ideas can be approached, and there is more access to data to allow for that. In the tutorial, you will see that we have calculated the normal for the face. By knowing the position of all three vertices, we can find the face normal. In addition to allowing access to whole primitives, the geometry shader can create new primitives on the fly. Previously in Direct3D, the graphics pipeline was only able to manipulate existing content, and it could amplify or deamplify data. The geometry shader in Direct3D 10 can read in a single primitive (with optional edge-adjacent primitives) and emit zero, one, or multiple primitives based on that. It is possible to emit a different type of geometry than the input source. For instance, it is possible to read in individual vertices, and generate multiple triangles based on those. This allows a wide range of new ideas to be executed on the graphics pipeline without CPU intervention. The geometry shader exists between the vertex and the pixel shaders in the graphics pipeline. Since new geometry can potentially be created by the geometry shader, we must ensure that they are also properly transformed to projection space before we pass them off to the pixel shader (PS). This can either be done by the vertex shader (VS) before it enters the geometry shader, or it can be done within the geometry shader itself. Finally, the output of the GS can be rerouted to a buffer. You can read in a bunch of triangles, generate new geometry, and store them in a new buffer. However, the concept of stream output is beyond the scope of tutorials, and it is best shown in the samples found in the SDK. Many of the samples found in the Direct3D 10 SDK illustrate specific techniques that can be achieved with the geometry shader. If you want a basic sample to get started, you can try ParticlesGS. ParticlesGS simulates and renders a dynamic particle system (creating, exploding, and destroying particles) entirely on the GPU.
Formatting a Geometry Shader Unlike the VS and the PS, the geometry shader does not necessarily produce a static number of outputs per input. As such, the format to declare the shader is different from the other two. The first parameter is maxvertexcount. This parameter describes the maximum number of vertices that can be output each time that the shader is run. This is followed by the name of the geometry shader, which has been aptly named GS. The function name is followed by the parameters passed into the function. The first contains the keyword triangle, which specifies that the GS will operate on triangles as input. Following that is the vertex format, and the identifier (with the number signifying the size of the array, 3 for triangles, 2 for line segments). The second parameter is the output format and stream. TriangleStream signifies that the output will be in triangles (triangle strips to be exact), then the format is specified in the angled brackets. Finally, the identifier for the stream is denoted. [maxvertexcount(3)] void GS( triangle GSPS_INPUT input[3], inout TriangleStream TriStream )
If vertices start being emitted to a TriangleStream, it will assume that they are all linked together as a strip. To end a strip, call RestartStrip within the stream. To create a TriangleList, you have to make sure that you call RestartStrip after every triangle.
Exploding the Model In this tutorial, we cover the basic functions of the geometry shader. To illustrate this, we will create an explosion effect on our model. This effect is created by extruding each vertex in the direction of that triangle's normal. Note that a similar effect has been implemented in previous tutorials, whereby we extrude each vertex by its normal, controlled by the puffiness slider. This tutorial demonstrates the usage of the full triangle's information to generate the face normal. The difference of using the face normal is that you will see gaps between the exploded triangles. Because vertices on different triangles are shared, they will actually be passed twice into the geometry shader. Moreover, since each time it will extrude it in the normal of the triangle, as opposed to the vertex, the two final vertices may end up in different positions.
Calculating the face normal To calculate the normal for any plane, we first need two vectors that reside on the plane. Since we are given a triangle, we can subtract any two vertices of the triangle to get the relative vectors. Once we have the vectors, we take the cross product to get the normal. We must also normalize the normal, since we will be scaling it later. // // Calculate the face normal // float3 faceEdgeA = input[1].Pos - input[0].Pos; float3 faceEdgeB = input[2].Pos - input[0].Pos; float3 faceNormal = normalize( cross(faceEdgeA, faceEdgeB) );
Once we have the face normal, we can extrude each point of the triangle in that direction. To do so, we use a loop, which will step through three times and operate on each vertex. The position of the vertex is extruded by the normal, multiplied by a factor. Then, since the vertex shader has not transformed the vertices to proper projection space, we must also do that in the geometry shader. Finally, once we package the rest of the data, we can append this new vertex to our TriangleStream.
for( int v=0; v<3; v++ ) { output.Pos = input[v].Pos + float4(faceNormal*Explode,0); output.Pos = mul( output.Pos, View ); output.Pos = mul( output.Pos, Projection ); output.Norm = input[v].Norm; output.Tex = input[v].Tex; }
TriStream.Append( output );
Once the three vertices have been emitted, we can cut the strip and restart. In this tutorial, we want to extrude each triangle separately, so we end up with a triangle list. TriStream.RestartStrip();
This new triangle stream is then sent to the pixel shader, which will operate on this data and draw it to the render target.
State Tutorials This section will focus on the state settings and management within the graphics pipeline. State changes affect how rendered primitives interact with the render target, the depth stencil buffer, and with each other. Tutorial 14: State Management
Tutorial 14: State Management
Summary This tutorial will explore a very important, but often overlooked aspect of Direct3D 10 programming, state. While not as glamorous or as flashy as shaders, state changes are indispensibly important when it comes to graphics programming. In fact, the same shader can have drastically different visual results base solely upon the state of the device at the time of rendering. In this tutorial we will explore 3 main types of state objects, BlendStates, DepthStencilStates, and RasterizerStates. At the end of the tutorial, you should have a much better understanding of the way in which states interact to produce different representations of the same scene.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial14
Blend States Have you ever heard the term Alpha Blending? Alpha Blending involves modifying color of a pixel being drawn to a certain screen location using the color of the pixel that already exists in that location. Blend States (ID3D10BlendState when used directly from the API and BlendState when used with FX) allow the developer to specify this interaction between new and old pixels. With the Blend State set to default, pixels just overwrite any pixels that already exist at the given screen coordinates during rasterization. Any information about the previous pixel that was drawn there is lost. To give an example, pretend that your application is drawing the view of a city from inside a taxi cab. You've drawn thousands of buildings, sidewalks, telephone poles, trash cans, and other objects that make up a city. You have even drawn the interior of the cab. Now, as your last step, you want to draw the windows of the cab to make it look like as though you're actually peering through glass. Unfortunately, the pixels draw during rasterization of the glass mesh completely overwrite the pixels alread at those locations on the screen. This includes your beautiful city scene along with the sidewalks, telephone poles, and all of your other city objects. Wouldn't it be great if we could include the information that was already on the screen when drawing the glass windows of the cab? Wouldn't it also be great if we could do this with minimal changes in our current pixel shader code? This is exactly what Blend States give you. To demonstrate this, our scene consists of a model and a quad. // Load the mesh V_RETURN( g_Mesh.Create( pd3dDevice, L"Tiny\\tiny.x", (D3D10_INPUT_ELEMENT_DESC*)layout, 3 ) );
Note that our quad has a different vertex format and therefore needs a different input layout description // Create a screen quad const D3D10_INPUT_ELEMENT_DESC quadlayout[] = { { L"POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 }, { L"TEXCOORD0", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 16,
D3D10_INPUT_PER_VERTEX_DATA, 0 }, }; g_pTechniqueQuad[0]->GetPassByIndex( 0 )->GetDesc( &PassDesc ); V_RETURN( pd3dDevice->CreateInputLayout( quadlayout, 2, PassDesc.pIAInputSignature, &g_pQuadLayout ) ); ... D3D10_SUBRESOURCE_DATA InitData; InitData.pSysMem = svQuad; InitData.SysMemPitch = 0; InitData.SysMemSlicePitch = 0; V_RETURN( pd3dDevice->CreateBuffer( &vbdesc, &InitData, &g_pScreenQuadVB ) );
When we render our scene we see the model of a person, with a quad drawn over the top.
Imagine that this person represents our city and the quad is really a pane of glass. When we launch the application, the situation is exactly as we described it above, we can't see through the glass. However, by selecting a different Quad Render Mode from the drop down box, we can suddenly see through the quad. It's as if it's a plane of glass.
In fact, all that the application is doing when we select a different Quad Render Mode is changing which technique in the FX file we're using to render the quad. For example, RenderQuadSolid in the combo box refers to the following technique in the FX file. technique10 RenderQuadSolid { pass P0 { SetVertexShader( CompileShader( vs_4_0, QuadVS() ) ); SetGeometryShader( NULL ); SetPixelShader( CompileShader( ps_4_0, QuadPS() ) ); SetBlendState( NoBlending, float4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xFFFFFFFF ); } }
The SetVertexShader, SetGeometryShader, and SetPixelShader functions should all look familiar at this point. If not, please review tutorials 10 through 12. The last line is what we're focusing on for now. In an FX file, this is how you set the Blend State. NoBlending actually refers to a state structure defined at the top of the FX file. The structure disables blending for the first (0) render target. BlendState NoBlending { BlendEnable[0] = FALSE; };
As we said before, this Blend State does nothing interesting. Because alpha blending is disabled, the pixels of the quad simply overwrite the existing pixels placed on the screen when rendering the person. However, things get more interesting when we select RenderQuadSrcAlphaAdd from the Quad Render Mode combo box. This changes the quad to be rendered with the
RenderQuadSrcAlphaAdd technique. technique10 RenderQuadSrcAlphaAdd { pass P0 { SetVertexShader( CompileShader( vs_4_0, QuadVS() ) ); SetGeometryShader( NULL ); SetPixelShader( CompileShader( ps_4_0, QuadPS() ) ); SetBlendState( SrcAlphaBlendingAdd, float4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xFFFFFFFF ); } }
A close look at this technique reveals two differences between it and the RenderQuadSolid technique. The first is the name. The second, and vitally important, difference is the Blend State passed into SetBlendState. Instead of passing in the NoBlending Blend State, we're passing in the SrcAlphBlendingAdd Blend State which is defined at the top of the FX file. BlendState SrcAlphaBlendingAdd { BlendEnable[0] = TRUE; SrcBlend = SRC_ALPHA; DestBlend = ONE; BlendOp = ADD; SrcBlendAlpha = ZERO; DestBlendAlpha = ZERO; BlendOpAlpha = ADD; RenderTargetWriteMask[0] = 0x0F; };
This blend state is a little more complex than the last one. Blending is enabled on the first (0) render target. For a thorough description of all Blend State parameters, refer to the documentation for D3D10_BLEND_DESC. Here is a quick overview of what this function is telling Direct3D 10 to do. When rendering the quad using this technique, the pixel about to be rendered to the framebuffer does not simply replace the existing pixel. Instead its color is modified using the following formula. outputPixel = ( SourceColor*SourceBlendFactor ) BlendOp ( DestColor*DestinationBlendFactor ) SourceColor is a pixel being rendered when we draw the quad. DestColor is the pixel that already exists in the framebuffer (from drawing the person) BlendOp is the BlendOp from the SrcAlphaBlendingAdd blend structure SourceBlendFactor is SrcBlend in the SrcAlphaBlendingAdd blend structure DestinationBlendFactor is DestBlend in the SrcAlphaBlendingAdd blend structure
By using the SrcAlphaBlendingAdd structure above, we can translate the equation into the following OutputPixel = ( SourceColor.rgba * SourceColor.aaaa ) ADD ( DestColor.rgba * (1,1,1,1) )
Graphically this can be displayed by the following diagram.
Thus, the output color is dependent on the color that was already in the framebuffer at the time of drawing. Different selections in the combo box will result in the quad being draw with different
techniques, and hence, different Blend States. Each Blend State changes the variables to the equation, so be sure to experiment with them all. See if you can predict what will happen just by looking at the Blend State.
Rasterizer states You may have noticed that no matter which Blend State was selected, the figure in the middle looked a little off. Some polygons were showing up where they shouldn't have been. Sometimes the legs looked as though you were looking at an upright tube cut in half. This brings us to our next state object, Rasterizer State. The Rasterizer State ( ID3D10RasterizerState when used through the API and RasterizerState when used through the FX framework ) controls how the triangles are actually rendered on the screen. Unlike the previous section on Blend States, we are not going to control Rasterizer States through the FX interface. This does not mean that they cannot be used through the FX system. In fact, it's just as easy to use Rasterizer States through FX as it is to use Blend States through FX. However, we're going to take this opportunity to learn about non-FX state management using the Direct3D 10 APIs. As the name implies, Rasterizer State controls how polygons are rasterized on the screen. When the tutorial starts up, the polygons are not culled. This means that where the polygons are pointing in your direction or pointing away from you, they all get draw equally. This is why some dark polygons are "poking" through in the figure. In order to change this behavior we must first create a Rasterizer State that defines the behavior we want. Since we're doing this through the API and not the FX interface the process is a little different than for Blend States. The following code is from the LoadRasterizerStates function. D3D10_FILL_MODE fill[MAX_RASTERIZER_MODES] = { D3D10_FILL_SOLID, D3D10_FILL_SOLID, D3D10_FILL_SOLID, D3D10_FILL_WIREFRAME, D3D10_FILL_WIREFRAME, D3D10_FILL_WIREFRAME }; D3D10_CULL_MODE cull[MAX_RASTERIZER_MODES] = { D3D10_CULL_NONE, D3D10_CULL_FRONT, D3D10_CULL_BACK, D3D10_CULL_NONE, D3D10_CULL_FRONT, D3D10_CULL_BACK }; for( UINT i=0; iCreateRasterizerState( &rasterizerState, &g_pRasterStates[i]
); g_SampleUI.GetComboBox(IDC_SCENERASTERIZER_MODE)>AddItem( g_szRasterizerModes[i], (void*)(UINT64)i ); }
All this code is doing is filling in a D3D10_RASTERIZER_DESC structure with the necessary information and then calling ID3D10Device::CreateRasterizerState to get a pointer to an ID3D10RasterizerState object. The loop creates one state for each type of Rasterizer State we want to show off. Notice that the first state, which uses D3D10_FILL_SOLID as the fill mode and D3D10_CULL_NONE as the cull mode is what is causing our figure to look weird. To change this, we can select the second rasterizer state from Scene Rasterizer Mode combo box. This object can then be used to set the state before we render. This is how the currently selected Rasterizer State is set in OnD3D10FrameRender. // // Update the Cull Mode (non-FX method) // pd3dDevice->RSSetState(g_pRasterStates[ g_eSceneRasterizerMode ]);
It must be noted any state set "persists" through all subsequent drawing operations until a different state is set or the device is destroyed. This means that when we set the rasterizer state in OnD3D10FrameRender, that same Rasterizer State is applied to any and all drawing operations that come after until a different Rasterizer State is set. When we selected the second option from the Scene Rasterizer Mode combo box our figure got a lot worse and the quad just dissappeared.
This is because we selected a cull mode that told Direct3D 10 not to draw triangles that are facing forward. The next Rasterizer State ( D3D10_CULL_BACK and D3D10_FILL_SOLID ) should give us the results we want ( with the exception of some stray black triangles around the head which we'll discuss in the next section ).
In addition to which triangles to draw and not draw, Rasterizer States can control many other aspects of rendering. A more thorough explanation of the function of each part of the state structure can be found in the Direct3D 10 documentation under Feel free to experiment with the different Scene Rasterizer Mode selections or to even change the code in LoadRasterizerStates to see what happens.
Depth Stencil states In the last section, setting the cull mode to D3D10_CULL_BACK and the fill mode to D3D10_FILL_SOLID gave a decent looking figure in the middle. However, there were still some black polygons showing up around the face. This is because those polygons were still classified as pointing to the front even though they were on the other side of the head. This is where the Depth Stencil State comes into play. This state object actually controls two different parts of the Direct3D 10 pipeline. The first is the Depth Buffer. On a basic level the depth buffer stores values representation of the distance of the pixel from the view plane. The greater the value, the greater the distance. The depth buffer is the same resolution (height and width) as the backbuffer. By itself, this doesn't seem very useful to our situation.
However, the Depth Stencil State lets us modify the current pixel based upon the depth of the pixel already at that location in the framebuffer. The Depth Stencil States are created in the LoadDepthStencilStates function. This function is similar to the LoadRasterizerStates. We will discuss some of the parameters of the D3D10_DEPTH_STENCIL_DESC structure. The most important of which is the DepthEnable. This determines whether the Depth Test is even functioning. As we can see, when the app starts up, the Depth Test is disabled. This is why the front facing hair spikes on the far side of the head still show up. Setting the Depth Stencil State is also very similar to setting the Rasterizer State with one exception. There is an extra parameter. This parameter is the StencilRef which will be explained in the next section. // // Update the Depth Stencil States (non-FX method) // pd3dDevice>OMSetDepthStencilState(g_pDepthStencilStates[ g_eSceneDepthStencilMode ], 0);
The second Depth Stencil State in the Scene Depth/Stencil Mode combo enables the Depth Test. When you enable the Depth Test, you need to tell Direct3D 10 what type of test to do. The equation boils down to the following. DrawThePixel? = DepthOfCurrentPixel D3D10_COMPARISON CurrentDepthInDepthBuffer D3D10_COMPARISON is the comparison function in the DepthFunc parameter of the D3D10_DEPTH_STENCIL_DESC
By substituting the value of the DepthFunc from the second Depth Stencil State, we get the following for the equation. DrawThePixel? = DepthOfCurrentPixel D3D10_COMPARISON_LESS CurrentDepthInDepthBuffer
In English, this means "Draw the current pixel only if it's depth is LESS than the depth already stored at this location." Additionally, we set the DepthWriteMask to D3D10_DEPTH_WRITE_MASK_ALL to ensure that our depth is put into the depth buffer at this location (should we pass the test) so that any subsequent pixels will have to be closer than our depth value to be drawn. This results in the figure being drawn without any black marks. The forward facing hair spikes in the rear never show through. They are either overwritten by closer triangles in the near side of the face, or they are never draw because the near triangles have a closer depth value. You will also notice that this reveals something about the quad. All this time it looked like it was in front of the figure. Now, we realize that it is actually intersecting the figure.
Try playing around with the DepthFunc values and see what happens.
Rendering Using the Stencil Buffer The Stencil parameters are part of the Depth Stencil State, but, because of their complexity, they deserve their own section. The Stencil buffer can be thought of as a buffer like the Depth Buffer. Like the depth buffer, it has
the same dimensions as the Render Target. However, instead of storing depth values, the stencil buffer stores integer values. These values have meaning only to the application. The Stencil part of the Depth Stencil State determines how these values get there and what they mean to the application. Like the Depth Buffer's Depth Test, the Stencil Buffer has a Stencil Test. This Stencil Test can compare the incoming value of the pixel to the current value in the Stencil Buffer. However, it can also augment this comparison with results from the Depth Test. Let's go through one of the Stencil Test setups to see what this means. The following information is from the fifth iteration of the loop in the LoadDepthStencilStates funciton. This reflects the 5th Depth Stencil State in the Scene Depth/Stencil Mode combo box. D3D10_DEPTH_STENCIL_DESC dsDesc; dsDesc.DepthEnable = TRUE; dsDesc.DepthWriteMask = D3D10_DEPTH_WRITE_MASK_ALL; dsDesc.DepthFunc = D3D10_COMPARISON_LESS; dsDesc.StencilEnable = TRUE dsDesc.StencilReadMask = 0xFF; dsDesc.StencilWriteMask = 0xFF; // Stencil operations if pixel is front-facing dsDesc.FrontFace.StencilFailOp = D3D10_STENCIL_OP_KEEP; dsDesc.FrontFace.StencilDepthFailOp = D3D10_STENCIL_OP_INCR; dsDesc.FrontFace.StencilPassOp = D3D10_STENCIL_OP_KEEP; dsDesc.FrontFace.StencilFunc = D3D10_COMPARISON_ALWAYS; // Stencil operations if pixel is back-facing dsDesc.BackFace.StencilFailOp = D3D10_STENCIL_OP_KEEP; dsDesc.BackFace.StencilDepthFailOp = D3D10_STENCIL_OP_INC; dsDesc.BackFace.StencilPassOp = D3D10_STENCIL_OP_KEEP; dsDesc.BackFace.StencilFunc = D3D10_COMPARISON_ALWAYS;
From the previous section, you should know that the Depth Test is set to let any pixel through that is closer to the plane than the previous depth value at that location. However, now, we've enabled the Stencil Test by setting StencilEnable to TRUE. We've also set the StencilReadMask and StencilWriteMask to values that ensure we read from and write to all bits of the stencil buffer. Now comes the meat of the Stencil settings. The stencil operation can have different effects depending on whether the triangle being rasterized is front facing or back facing. (Remember that we can select to draw only front or back facing polygons using the Rasterizer State.) For simplicity, we're going to use the same operations for both front and back facing polygons, so we'll only discuss the FrontFace operations. The FrontFace.StencilFailOp is set to D3D10_STENCILL_OP_KEEP. This means that if the Stencil Test fails, we keep the current value in the stencil buffer. The FrontFace.StencilDepthFailOp is set to D3D10_STENCIL_OP_INC. This means that if we fail the depth test, we increment the value in the stencil buffer by 1. The FrontFace.STencilPassOp is set to D3D10_STENCIL_OP_KEEP, meaning that on passing the Stencil Test we keep the current value. Finally FrontFace.StencilFunc represents the comparison used to determine if we actually pass the Stencil Test (not the Depth Test). This can be any of the D3D10_COMPARISON values. For example, D3D10_COMPARISON_LESS would only pass the Stencil Test if the current stencil value was less than the incumbant stencil value in the Stencil Buffer. But where does this current test value come from? It's the second parameter to the OMSetDepthStencilState function (the StencilRef parameter). // // Update the Depth Stencil States (non-FX method) // pd3dDevice>OMSetDepthStencilState(g_pDepthStencilStates[ g_eSceneDepthStencilMode ], 0);
For this example, the StencilRef parameter doesn't matter in the stencil comparison function. Why? Because we've set the StencilFunc to D3D10_COMPARISON_ALWAYS, meaning we ALWAYS pass the test. What you may have already determined from looking at this Depth Stencil State is that any pixel that FAILS the Depth Test will cause the value in the Stencil Buffer to increment (for that location in the buffer). If we fill the stencil buffer with zeroes (which we do at the beginning of OnD3D10FrameRender), then any non-zero value in the stencil buffer is a place where a pixel failed the depth test. If we could actually visualize this Stencil Buffer, we could determine all of the places in the scene where we attempted to draw something, but something else was closer. In essence, we could tell where we tried to draw something behind and object already there. Unfortunately, there is no Direct3D 10 function called ShowMeTheStencilBuffer. Fortunately, we can create an FX Technique that does the same thing. To show that the Depth Stencil State can be just as easily set through the FX system as it can be through the Direct3D 10 APIs, we're show how to use a Depth Stencil State in FX to view the contents of the stencil buffer. After both the figure and the quad have been drawn, we render another quad that covers the entire viewport. Because we cover the entire viewport, we can access every pixel that could possibly be touched by our previous rendering operations. THe technique used to render this quad is RenderWithStencil in the FX file. technique10 RenderWithStencil { pass P0 { SetVertexShader( CompileShader( vs_4_0, ScreenQuadVS() ) ); SetGeometryShader( NULL ); SetPixelShader( CompileShader( ps_4_0, QuadPS() ) ); SetBlendState( NoBlending, float4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xFFFFFFFF ); SetDepthStencilState( RenderWithStencilState, 0 ); } }
Notice that in addition to setting the Blend State, we also set the Depth Stencil State to RenderWithStencilState. The second parameter sets the StencilRef value to 0. Let's take a look at the RenderWithStencilState as defined at the top of the FX file. DepthStencilState RenderWithStencilState { DepthEnable = false; DepthWriteMask = ZERO; DepthFunc = Less; // Setup stencil states StencilEnable = true; StencilReadMask = 0xFF; StencilWriteMask = 0x00; FrontFaceStencilFunc = Not_Equal; FrontFaceStencilPass = Keep; FrontFaceStencilFail = Zero; BackFaceStencilFunc = Not_Equal; BackFaceStencilPass = Keep; BackFaceStencilFail = Zero; };
The first thing you should notice is that this is similar to the D3D10_DEPTH_STENCIL_DESC you'd find in the LoadDepthStencilStates function in the cpp file. The values are different, but the members are the same. First, we disable the depth test. Secondly, we enable the Stencil Test, BUT we set our StencilWriteMask to 0. We only want to read the stencil value. Last we set the StencilFunc to Not_Equal, the StencilPass to Keep, and the StencilFail to Zero for both front and back faces. Remember that the Stencil Test tests the current incoming value with the value already stored in the Stencil Buffer. In this case, we set the incoming value (the StencilRef) to 0 when we called SetDepthStencilState( RenderWithStencilState, 0 ); Thus, we can translate this state to mean that the stencil test passes where the Stencil Buffer is not equal to 0 (StencilRef). On pass we get to Keep the incoming pixel from the pixel shader and write it out to the frame buffer. On a fail, we discard the incoming pixel, and it never gets drawn. Thus, our screen covering quad is only drawn in places where the Stencil Buffer is non-zero.
Selecting different items in the Scene Depth/Stencil Mode combo box change how the Depth and Stencil buffers are filled, and thus how much of the Screen Sized quad that's drawn last actually makes it onto the screen. Some of these combinations won't produce any output. Some will produce bizarre effects. Try to see which ones will do what before you apply them.
Workshops This section will focus on Direct3D 10 related workshops and presentations. Direct3D 10 Shader Model 4.0 Workshop Advanced Shader Authoring Workshop GDC 2007 Performance Workshop GDC 2007
Direct3D 10 Shader Model 4.0 Workshop
Summary This workshop is based upon the Direct3D 10 Shader Model 4.0 Workshop presented at GDC 2006. The workshop guides the student through 7 exercises aimed at teaching the basics of Shader Model 4.0 HLSL. Students are expected to have a working knowledge of the Direct3D 10 graphics pipeline and HLSL. Exercise05 will require knowledge of C programming.
Source (SDK root)\Samples\C++\Direct3D10\Tutorials\Direct3D10Workshop
General Exercise Guidelines While the exercises do build upon each other, they are meant to be self-contained. With the exception of Exercise05, the student will be working with .fx files to complete each exercise. Comments are located throughout the code to guide the student. Comments are highlighted by a collection of ascii characters known as "Breakdancin Bob" and take the following form: //----------------------------------------------------------------// o/__ <-- BreakdancinBob TODO: Todos are areas where you need // | (\ to change or implement code. This is where // the actual exercise work will happen. //----------------------------------------------------------------//----------------------------------------------------------------// o/__ <-- BreakdancinBob NOTE: Notes are code snippets of // | (\ interest. No work needs to be done. Just // look. //----------------------------------------------------------------//----------------------------------------------------------------// o/__ <-- BreakdancinBob HINT: Hints may be found near Todo // | (\ comments. Hints can help you if you are // stuck with the exercise. //-----------------------------------------------------------------
Solutions to the exercises are found in the folders with _Solved appended to the end.
Exercise00 - Debugging Practice The goal of Exercise00 is to familiarize the user with the act of debugging shader problems. The solution will compile, but during run time, an error box will appear telling the user that the shader could not be compiled. Look in the Visual Studio output window for debug spew from the shader compiler to determine the cause of the error.
Exercise01 - Introduction to the Geometry Shader The goal of Exercise01 is to teach basic usage of the Geometry Shader, a new shader type for Direct3d 10. The exercise teaches the student to use the Geometry Shader to append any elements needed by the pixel shader to the output vertex stream.
Exercise02 - Finding Silhouette Edges This exercise teaches the principle of finding silhouette edges. When the student opens the exercise, the shader calculates and displays a silhouette with respect to the viewer. Only edges with one face facing the viewer and one face facing away from the viewer are drawn. The goal of Exercise02 is to change this calculation to happen with respect to the light source. This is an essential step for doing stencil shadow volumes. The following diagram illustrates this more clearly.
Exercise03 - Shadow Volumes Exercise03 builds upon the silhouette edges found in Exercise02 to create extruded shadow volumes. While this exercise does not do capping and does not use the stencil buffer to actually do shadowing, it illustrates the second essential step for implementing stencil shadow volumes. The following diagram illustrates how to extrude a shadow volume from a selected edge.
Exercise04 - Creating New Geometry The previous exercises showed off one of the main strengths of the Geometry Shader, operating on whole primitives with adjacency information. The remaining exercises illustrate two other strengths of the Geometry Shader. These are geometry amplification and destruction, and stream output. In Exercise04, the geometry shader is used to create new triangles based upon information contained
in the vertex stream. At specific faces on the mesh, a new triangle is created and moved away from the original face in the direction of the normal.
Exercise05 - Feedback and Stream Out This exercise demonstrates procedural geometry generation using geometry amplification through the Geometry Shader. The amplified geometry is then fed back through the Geometry Shader during subsequent passes to grow an entire tree from a small piece of seed geometry. This exercise deals only with the cpp file and requires knowledge of C to complete.
Exercise06 Exercise06 is a very simple exercise showing how to add some randomness and variation to tree created in Exercise05. A buffer of random numbers is generated and then sampled in the shader to control the growth of the tree.