Vertex Buffer in Direct3D 10
This short
article gives a brief overview of how a simple, yet typical, vertex buffer
creation process differs between Direct3D 9 and Direct3D 10. The aim is to show
some simple examples of how the API has changed and what these changes mean to
you.
A vertex
buffer, as you are probably aware, contains the raw geometric data (a 1D array
of vertices) that builds
up a scene. Vertex buffers can be arranged in a variety of different ways –
depending on how you want the final object to be displayed. If used in
conjunction with index buffers then there is no requirement for specific
ordering of data in a vertex buffer.
The first
step is to define a way of describing the data for each vertex that you wish to
store in a vertex buffer. With C/C++ this is typically done using a
struct:
Direct3D 9 and Direct3D 10:
struct
SimpleVertex
{
D3DXVECTOR3 Pos;
// Position
DWORD
Color;
// Color
};
As you can
see, the definition remains the same between 9 and 10. Because there is no
fixed-function pipeline in Direct3D 10 the vertex format is implicitly more
flexible - although you can mimic much of this via Direct3D 9's programmable
route.
The next step is the basic definition of a
vertex buffer object. Once created it’s this variable that you use whenever you
want to reference/use the data contained within it.
Direct3D 9:
IDirect3DVertexBuffer9 *pVertexBuffer
= NULL;
Direct3D 10:
ID3D10Buffer *pVertexBuffer =
NULL;
It’s worth
noting that there isn’t a specific vertex buffer object in Direct3D 10 – it’s
just a different configuration of a generic ‘buffer’ object.
The first
difference to note is that Direct3D 9 has an additional, optional, declaration:
The Flexible Vertex Format (FVF) which is important mainly for the legacy
fixed function pipeline. Direct3D 10 has no concept of a fixed function
pipeline, so you’re not going to see any FVF’s. The
previously defined
SimpleVertex would look like either of these
(depending on your coding style):
Direct3D 9:
static const DWORD
FVF_SIMPLEVERTEX = D3DFVF_XYZ | D3DFVF_DIFFUSE;
#define
FVF_SIMPLEVERTEX ( D3DFVF_XYZ | D3DFVF_DIFFUSE )
Now that we
have a method by which we can describe the vertex data that will be stored in
our vertex buffer object, we should define some data to use.
Direct3D
10, as we shall see shortly, can accept the
initial
data as an input into the creation process. If we provide the data at create
time then as soon as the call returns we would have an already filled buffer.
With Direct3D 9 you must allocate the necessary storage space for the vertex
data before you copy any data into
it. If you prefer the Direct3D 9 method, then you can still use it with
Direct3D 10.
Direct3D 9 and Direct3D 10:
SimpleVertex
VertexArray[] =
{
// SimpleVertex::Pos , SimpleVertex::Color
//
( x, y,
z ) ( r,
g, b )
{ D3DXVECTOR3(
1.0f, 1.0f, 1.0f ), D3DCOLOR_XRGB( 255, 255, 255 ) },
{ D3DXVECTOR3( 1.0f,
1.0f, -1.0f ), D3DCOLOR_XRGB( 255, 255,
0 ) },
{ D3DXVECTOR3( 1.0f, -1.0f,
1.0f ), D3DCOLOR_XRGB( 255, 0,
255 ) },
{ D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DCOLOR_XRGB(
255, 0, 0 ) },
{ D3DXVECTOR3( -1.0f, 1.0f,
1.0f ), D3DCOLOR_XRGB( 0, 255,
255 ) },
{ D3DXVECTOR3( -1.0f, 1.0f, -1.0f ), D3DCOLOR_XRGB( 0, 255,
0 ) },
{ D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DCOLOR_XRGB( 0,
0, 255 ) },
{
D3DXVECTOR3(
-1.0f, -1.0f, -1.0f ), D3DCOLOR_XRGB(
0, 0, 0 ) }
};
At this
point we have the necessary components (for either API) to get started. The
next step is where things start to get noticeably different. As hinted at
earlier, Direct3D10 doesn’t have a specific vertex buffer object – it’s just a
specialization of a generic buffer (basically a block of binary data). This
means that we must describe a vertex buffer for Direct3D. In Direct3D 9 there is
a specific method used to create a vertex buffer (and
only a vertex buffer).
The
following two fragments allocate a vertex buffer that will contain a simple
cube using 8 vertices (one for each corner!).
Direct3D 9:
if( FAILED( device->CreateVertexBuffer(
8
* sizeof( SimpleVertex ),
0,
FVF_SIMPLEVERTEX,
D3DPOOL_DEFAULT,
&pVertexBuffer,
NULL
) )
)
{
// Handle the error condition here
}
Direct3D 10:
D3D10_BUFFER_DESC
BufferDescription;
BufferDescription.Usage = D3D10_USAGE_DEFAULT;
BufferDescription.ByteWidth = sizeof(
SimpleVertex ) * 8;
BufferDescription.BindFlags = D3D10_BIND_VERTEX_BUFFER;
BufferDescription.CPUAccessFlags = 0;
BufferDescription.MiscFlags = 0;
D3D10_SUBRESOURCE_UP
InitialData;
InitialData.pSysMem = VertexArray;
InitialData.SysMemPitch = sizeof(
VertexArray );
InitialData.SysMemSlicePitch = sizeof(
VertexArray );
if( FAILED( device->CreateBuffer(
&BufferDescription,
&InitialData,
&pVertexBuffer
) ) )
{
// Handle the error condition here
}
There are
three things of interest in the Direct3D 9 code.
- There
is a “usage” parameter (set to 0/default in this case) that is very critical to
set correctly should you wish to manipulate your vertex buffer. Many
applications modify the data by reading and/or writing once it’s been created,
which can have severe performance penalties if done without the correct usage
flag.
- The
“pool” that the resource is created in (D3DPOOL_DEFAULT in this example). This has various
implications many of which are connected with the previous point.
- More
a curiosity – the final parameter (NULL
in the above example) is the
pSharedHandle. This, according to
the documentation, should always be
NULL. If you look up the Direct3D 9 revisions
for Windows Vista you’ll see references to shared resources and shared handles –
which is obviously what this (currently useless) parameter was included for.
With
regards to the Direct3D 10 fragment the following points are worth noting:
- The
definition obviously has to precede the act of creating the resource. In doing
so it also requires that the vertex data already exists. In the above fragment
this is represented by the
VertexArray
variable which will be discussed in the next paragraph.
- To
define the usage patterns (previously via the
D3DUSAGE enumeration) is a little more
involved.
D3D10_BUFFER_DESC::Usage is the obvious one to look at, and its options
should be fairly recognizable (with the addition of
D3D10_USAGE_IMMUTABLE, which will be useful).
Additionally there is the
D3D10_BUFFER_DESC::CPUAccessFlags field that you can use to restrict
how your application code will be able to interact with the buffer (none, read,
write, read and write).
-
The
only part of the code that implies what the contents of the buffer is, and how
they can be used is the
D3D10_BUFFER_DESC::BindFlags field. If you study the new Direct3D
10 pipeline, there are many more places where a generic resource can be either
an input or an output to a given stage. By specifying
D3D10_BIND_VERTEX_BUFFER you are effectively telling
Direct3D 10 that you can only ‘attach’ it to the pipeline in places that vertex
data is a valid input or a valid destination for output.
Now,
provided that the respective
Create*( )
calls didn’t fail then we should
have a vertex buffer ready to use. This is where a big difference from Direct3D
9 can be found. If you chose to provide the
VertexArray
as initial data then you’re ready to go. If you didn’t (or you’re using
Direct3D 9) then you will have to modify the resource and specify the data that
belongs in the allocated memory:
Direct3D 9:
SimpleVertex *pData
= NULL;
if( SUCCEEDED(
pVertexBuffer->Lock( 0, 0,
reinterpret_cast< void**
>( &pData ), 0 ) ) )
{
memcpy( pData, VertexArray,
sizeof(
SimpleVertex ) * 8 );
pVertexBuffer->Unlock(
);
}
Direct3D 10:
SimpleVertex
*pData = NULL;
if(
SUCCEEDED( pVertexBuffer->Map( D3D10_MAP_WRITE, 0, reinterpret_cast<
void** >( &pData ) ) ) )
{
memcpy( pData, VertexArray,
sizeof( SimpleVertex ) * 8 );
g_pVertexBuffer->Unmap( );
}
Both of the
above fragments are pretty much the same – substituting the terms “lock” with
“map” and “Unlock” with “Unmap”. However, what is not
immediately obvious is that you must be more specific about how you wish to map
the data in Direct3D 10 – the first parameter (set to
D3D10_MAP_WRITE
in the above example) is used to indicate this. Whilst it’s not relevant in the
fragments shown in this example, the choices made when creating the resource
(e.g. the D3D10_BUFFER_DESC::Usage
and D3D10_BUFFER_DESC::CPUAccessUsage
fields) can restrict what operations you’re allowed to perform later on.
Both pieces
of code are about as concise as it gets – many examples will actually fill the
pData pointer in a loop whilst the resource is locked (usually not good for
performance!). To try and keep the Direct3D 9 and Direct3D 10 code synchronized
the concise form has been such that they can both share the
VertexArray declaration.
Direct3D 10:
pVertexBuffer->UpdateSubresource( 0,
NULL,
VertexArray,
sizeof( VertexArray ),
sizeof( VertexArray )
);
The above
fragment is a convenient way of doing the same map/unmap operation in a single call. The obvious difference is
that it will be write-only access as you don’t get given a pointer to read
from. It’s worth noting that the last three parameters in the function call are
essentially the same as the three fields in the
D3D10_SUBRESOURCE_UP struct.
At this point in the article
we have covered two of the three main parts when it comes to rendering with
vertex buffers. Firstly we covered how to define the data that will exist
inside the buffer, and secondly we covered how to actually create the buffer
and get the data from our application into the buffer.
The third
part is concerned with actually using the data stored in the vertex buffer –
this will primarily be the process of rendering the geometry to the screen. It
can be conveniently broken down into two sub-parts which will allow us to look
at some more differences between Direct3D 9 and Direct3D 10.
Firstly it
is necessary to configure Direct3D to use the data stored in the buffer. In a
real-world application it is very likely that a large number of different
vertex buffers will be used – many of them with data stored in different
formats. When you tell Direct3D you want to render from a vertex buffer it has
to know how to interpret the raw binary data that it finds in the vertex buffer
you provide it.
In Direct3D
9 this could be done quite simply by specifying the Flexible Vertex Format
(FVF) discussed earlier, pointing Direct3D to the correct vertex buffer object
and issuing a draw call. If this method was chosen then you would be using the
fixed-function pipeline – a feature that has now been superseded by the
programmable pipeline. The advantages of this transition are huge and
consequently far beyond the scope of this short article; however it is worth
noting that because of the programmable pipeline the equivalent code has
changed quite significantly.
Direct3D 9:
device->SetStreamSource(
0, pVertexBuffer, 0,
sizeof( SimpleVertex
) );
device->SetFVF(
FVF_SIMPLEVERTEX );
device->DrawPrimitive(
D3DPT_TRIANGLELIST, 0, 12 );
With
Direct3D 10 it is necessary to configure the Input Assembler (IA) stage of the
pipeline before you can actually render anything. The IA stage is a refinement
on various pieces that existed for the programmable pipeline found in Direct3D
9, and is effectively the first part of the geometry pipeline. Quite simply the
IA will take all of the data that the application provides and merge it
together into a stream of primitives and vertices that the Vertex
Shader (VS) and Geometry Shader
(GS) units can use directly.
A simple
example of this is taking the vertex buffer and “chopping” it directly into
triangles, an extension would be to use an index buffer to format the vertex
buffer into a series of primitives. When appropriate hardware was available,
Direct3D 9 allowed applications to use a feature called “Geometry Instancing”
as well as pulling in data from multiple vertex buffers – this advanced feature
now resides in the IA stage.
Direct3D 10:
D3D10_INPUT_ELEMENT_DESC
layout[] =
{
{ L"POSITION", 0,
DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D10_INPUT_PER_VERTEX_DATA, 0 },
{ L"COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 },
};
The above
fragment should be immediately familiar to anyone who has done any work with
the Direct3D 9 programmable pipeline. A
D3D10_INPUT_ELEMENT_DESC struct is
used to fully describe the contents of a single element within the vertex
buffer – what format a range of bytes are to be interpreted as (via the
DXGI_FORMAT
value) and the semantic used to map it to the desired input of a vertex
shader. There are two slightly subtle differences to the
Direct3D 10 declaration:
Because of
the fixed-caps nature of Direct3D 10, there are a lot of possible formats that
you can use in a vertex – especially when supported by the more flexible
semantic naming.
For
reference purposes, the above fragment in Direct3D 9 form would look like this:
Direct3D 9:
D3DVERTEXELEMENT9
decl[] =
{
{ 0, 0,
D3DDECLTYPE_FLOAT3,
D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },
{ 0, 12, D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT,
D3DDECLUSAGE_COLOR, 0 },
D3DDECL_END( )
};
The
D3DDECLTYPE and
D3DDECLUSAGE
enumerations being the equivalent of the more free-form constructs described
above.
Once the
initial declaration/layout is defined, it is necessary to create a usable
‘input layout’ – An
ID3D10InputLayout for Direct3D 10 or an
IDirect3DVertexDeclaration9 for Direct3D 9. This object is used when you need to tell the pipeline
how to interpret the data that you’re about to send it. A key conceptual
difference here is that under Direct3D 9 you created an internal representation of the
D3DVERTEXELEMENT9 array that was just created. There
is no verification involved. If you then configure your device to render using the
returned
IDirect3DVertexDeclaration9 and it isn’t compatible with the current
Vertex Shader then you’ll get a silent error (nothing
rendered, and a lot of debug output!). This changes under Direct3D 10 because
it verifies the layout you provide against the layout of the vertex
shader. If they don’t match, the call will fail. This
validation step is pretty much essential given that we now have much more
freedom in how we can describe individual vertex elements – and it should also
allow the runtime to avoid some checks.
Direct3D 9:
if( FAILED( device->CreateVertexDeclaration( decl,
&vertexDeclaration ) ) )
{
// Handle the error
condition here.
}
Direct3D 10:
D3D10_PASS_DESC
PassDesc;
pTechnique->GetPassByIndex(
0 )->GetDesc( &PassDesc );
if( FAILED(
device->CreateInputLayout( layout, 2, PassDesc.pIAInputSignature,
&pVertexLayout ) ) )
{
//
Handle the error condition here..
}
It is worth
noting that, because of the programmable pipeline, that the effect framework is
more extensively (although, if desired, direct access to a shader is possible) used. The finer details of the new effects framework for
Direct3D 10 are beyond the scope of this article - the above fragment assumes
that an effect has been loaded and that a technique has been extracted. It also
works on the assumption that there is only one pass in the current technique –
which is not a safe assumption for a data-driven architecture. It would make
for better code to build a layout for each pass in the effect:
Direct3D 10:
D3D10_TECHNIQUE_DESC
pDesc;
pTechnique->GetDesc(
&pDesc );
ID3D10InputLayout
**pVertexLayout = new ID3D10InputLayout*[
pDesc.Passes ];
for( UINT idx
= 0; idx < pDesc.Passes; idx ++ )
{
D3D10_PASS_DESC PassDesc;
pTechnique->GetPassByIndex( idx
)->GetDesc( &PassDesc );
if(
FAILED( device->CreateInputLayout( layout, 2, PassDesc.pIAInputSignature,
&pVertexLayout[idx] ) ) )
{
// Handle the error
condition here...
}
}
At this
point the application will have the two main sources of information that are
required in order to render from a vertex buffer: The vertex buffer itself, and
the vertex layout. Typically these two objects will be stored in a suitable
data structure and when it comes to rendering them they’ll be assigned to the
relevant part of the pipeline.
Direct3D 9:
device->SetVertexDeclaration(
vertexDeclaration );
device->SetStreamSource( 0,
pVertexBuffer, 0,
sizeof( SimpleVertex
) );
Direct3D 10:
device->IASetInputLayout( pVertexLayout );
UINT stride
= sizeof( SimpleVertex );
UINT offset
= 0;
device->IASetVertexBuffers( 0, 1, &pVertexBuffer,
&stride, &offset );
device->IASetPrimitiveTopology(
D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
With
Direct3D 10 a single
IASetVertexBuffers call can assign many vertex buffers
to the device (hinted at by the name!). This fits in with the previously
mentioned concept that the Input Assembler now takes care of reading data from
various buffers and formatting any streams based on any instancing-related
rendering. Additionally it’s worth noting that the Input Assembler also needs
to know the topology of the data its being given – for the most part this is
going to be the same as the first parameter for a typical Direct3D 9
Draw*( ) call.
The only obvious exception from the
D3D10_PRIMITIVE_TOPOLOGY enumeration is for triangle fans
(which was
D3DPT_TRIANGLEFAN).
Once the
Input Assembler has been configured with the vertex buffer, layout and topology
the device should be ready. There are several other factors that need to be
setup (such as the current effect technique and effect parameters) but these
are beyond the scope of this article.
Direct3D 9:
device->DrawPrimitive(
D3DPT_TRIANGLELIST, 0, 12 );
Direct3D 10:
device->Draw( 12, 0 );
That concludes the comparison of vertex buffers in Direct3D 10 and Direct3D 9. There are many more aspects to both the Direct3D 9 and 10 uses for vertex buffers, the majority of these were dropped so as to keep this article short and to the point. The following is a brief overview of some features that weren't covered:
Instancing and multi-stream inputs are common to both versions and allow
for some very clever implementations. For example, it is possible to store
different parts of a vertex in different vertex buffers and have them
"composed" into a single vertex that is fed into the Vertex Shader.
Advanced features of the Direct3D 10 pipeline that don't have an equivalent in Direct3D 9. This article is primarily a comparison, so aspects such as Geometry Shader usage has been ignored.
Summary
Much of what has been covered here will be familiar to any experienced Direct3D 9 programmer, and any bigger changes should be a manageable step-up from Direct3D 9. As a summary of some of the key points, specifically with regards to geometry storage - Direct3D 10…
has no
fixed function pipeline so you won't be seeing any FVF-related code in
Direct3D 10. You'll also see a lot more application code that is similar to
the programmable route under Direct3D9.
has no
specific vertex buffer object meaning that a Direct3D 10 "vertex buffer"
is simply a specialization of a generic data-buffer.
is more
strict about creating the input layout as it requires the layout to be
created with reference to a set of vertex shader input semantics.
is more verbose than Direct3D 9. Using the classic fixed-function pipeline, the code presented in this article takes approximately 40 lines. Using the programmable pipeline in Direct3D 9 results in 50 lines. The Direct3D 10 pipeline uses 60 lines.