This tutorial will teach you how to use the low-level API in Frontend::Graphics to draw custom geometry. This is the most flexible way to draw geometry in Frontend, and the API all the more high-level methods build upon. If you want to maximize the performance of a certain element in your game, writing custom code for setting up vertex buffers and input layouts gives you more control, and allows you to do several optimizations. If you want a simpler method for just getting something on the screen, check out the GeoStream or FGM/FSG-tutorials.
Applies to:
- OpenFrontend 2.0 SDK Beta 2 (all platforms)
Drawing in Frontend::Graphics summarized
Drawing something using the Frontend::Graphics API can be summarized by the following steps.
Load-time steps
These steps should be carried out one time only, typically when your program starts up or when the scene you want to display should load.
- Create vertex buffer(s) on a Device using Device::CreateVertexBuffer()
- Upload data to it with VertexBuffer::SetData()
- Create an InputLayout which defines how the vertex data should be read from the data streams
- Load the shader program you want to use
Run-time steps
The following steps should be carried out each frame when you want to draw your geometry.
- Bind the vertex buffers to data streams on the device using Device::SetVertexBuffer()
- Bind the input layout to the device using Device::SetInputLayout()
- Bind the program you want to use to the device using Device::SetProgram(). Set necessary set program parameters
- Tell the device to draw primitives using Device::Draw()
The rest of this tutorial will go through each of these steps in detail.
Drawing in Frontend::Graphics in detail
Create a vertex buffer
First we create a local vertex buffer in the memory, which we will fill with data and upload to the device-managed vertex buffer later. Its often useful to create a stucture fitting the desired layout of the vertex buffer, as it makes it easier to fill the buffer with data. If you do so, be careful to align the elements on 4-byte boundaries, or what ever alignment setting you are using on your compiler.
{
Vector3 position;
Vector4 color;
Vector2 texcoord;
}
int vertexCount = 3;
OurVertex* vertexData;
vertexData = new OurVertex[vertexCount];
vertexData[0].position = Vector3(0,0.5,0);
vertexData[0].color = Vector4(1,0,0,1);
vertexData[0].texcoord = Vector2(1,0);
vertexData[1].position = Vector3(-0.5,0,0);
vertexData[1].color = Vector4(0,1,0,1);
vertexData[1].texcoord = Vector2(0,1);
vertexData[2].position = Vector3(0,0,0.5);
vertexData[2].color = Vector4(0,0,1,1);
vertexData[2].texcoord = Vector2(1,1);
Next, we create a vertex buffer on the device with the size of sizeof(OurVertex)*vertexCount, so we can upload the data to a buffer managed by the device. The first parameter bufferMode tells frontend what kind of usage the buffer should be optimzied for. The options are:
- BufferModeVolatile - The buffer will be written to once or often but never read back. Use this mode if you will never read data back from the buffer, and/or if you are going to update the buffer often. You can not use Lock() on volatile buffers with read-access. Volatile buffers can also be lost on some devices if the application looses focus (i.e. device lost in d3d).
- BufferModeDynamic - The buffer will be written to, read from and/or partially overwritten frequently. The buffer will be kept in both system and device memory to optimize for this kind of activity. Dynamic buffers are never lost.
- BufferModeStatic - The buffer will be written to once or seldom and never or rarely read back. This mode is practical for most cases where you have static data like models you are never going to modfiy cpu-side. Static buffers are never lost.
Then we upload our local vertex buffer to the vertex buffer on the device.
Setting up an input layout
Each Device object has up to 16 so called streams, indexed from 0 to 15. A stream is a “slot” on the device to which a single vertex buffer can be connected at a time. In this example, we use a single vertex buffer, and we are going to set it to stream 0 prior to drawing. The input layout defines how data should be read from the data streams and put into vertex attributes available in the shader program.
First we need set up some InputElements for each of the attributes. Each input element describes how a certain attribute should be read from the data streams. InputElement has the following fields:
- Attribute - An integer specifying the attribute semantic in the shader program this element should be mapped to. While using the default semantic map, it is convenient to use the symbols enumerated in the DefaultSemantics enumeration. This allows you to set the attribute equal to the name of the semantic you will use in CG or HLSL, and simply the name of the attribute when using GLSL. More on how to customize this will be covered in a later tutorial.
- Components - The number of components in the attribute. For instance or vertex position has 3 components: x, y and z.
- ComponentType - An enum telling Frontend which data type the data is in. All our data is 32-bit floats in this example
- Offset - The offset from the beginning of the buffer to the first value of this element, in bytes
- Stream - The stream index this element should be read from. In this example we use a single vertex buffer bound to stream 0
- Normalize - A bool specifying whether or not each component should be “normalized”. This is useful when using integral types to represent i.e. normals
The first element is the position of each vertex, which is a 3-component float vector we bind to the POSITION attribute.
elm[0].Components = 3;
elm[0].ComponentType = DataTypeFloat32;
elm[0].Offset = 0; // it’s located first in the stream
elm[0].Stream = 0;
elm[0].Normalize = false;
The second element is the color of each vertex, which is a 4 component float vector we bind to the COLOR attribute.
elm[1].Components = 4;
elm[1].ComponentType = DataTypeFloat32;
elm[1].Offset = 12; // Located after the first 3 floats from the position: 4*3=12
elm[1].Stream = 0;
elm[1].Normalize = false;
The third element is the texture coordinate of each vertex, which is a 2 component float vector we bind to the TEXCOORD0 attribute (you’re getting the hang of this now, right?:)
elm[2].Components = 2;
elm[2].ComponentType = DataTypeFloat32;
elm[2].Offset = 28; // Located after the first 7 floats from the position+color: 7*4=28
elm[2].Stream = 0;
elm[2].Normalize = false;
Now we create an InputLayout of the 3 elements we defined
Bind a shader program
Well, there has to be an active shader program running so the graphics device know what to do with the data. The shader program gain access the data in our vertex buffer through the attributes defined for each element in our input layout. In this lesson we won’t go in depth about shader pogramming, but here’s a basic Cg-program source file:
struct VSInput
{
float4 Position: POSITION;
float4 Color: COLOR;
float2 Texcoord: TEXCOORD0;
};
struct VSOutput
{
float4 Position: POSITION;
float4 Color: COLOR;
float2 Texcoord: TEXCOORD0;
};
VSOutput vsMain(VSInput Input)
{
VSOutput Output;
Output.Position = Input.Position;
Output.Position *= ViewportConformFactor;
Output.Color = Input.Color;
Output.Texcoord = Input.Texcoord;
return Output;
}
float4 psMain(VSOutput Output): COLOR
{
return Output.Color;
}
Make a directory called Data in the same directory as your executable and save the shader as shader.cg.
In order to load files, we need a proxy object. We make our proxy read it’s files from the Data directory. You’ll learn more about the proxy object later on.
Utils::Assets::Proxy* proxy = new Utils::Assets::Proxy(folder, device);
We load our Cg program using Frontend2Utils’ shader loader:
Note! The following steps are done every frame (inside our rendering loop)
We bind our program as the current active shader program.
We the device to use our VertexBuffer as stream 0.
We tell the device to render Triangles
Note: Frontend2 has removed the Quad primitive type.
Tell the device to use our InputLayout (tell the device what data in the VertexBuffer should be mapped to which attribute)
At last, we tell the device to draw.