quiverteam / Engine

Modified Version of Source 2007
104 stars 29 forks source link

Improved Mesh Interfaces #72

Open lachbr opened 4 years ago

lachbr commented 4 years ago

Right now, mesh functionality must be implemented by each Shader API. There is no reason for this to be the case because meshes have no relevance to any graphics API. All a graphics API cares about is vertex buffers, index buffers, and input layouts (AKA vertex declarations in DX9, vertex specifications in OpenGL). While implementing DirectX 11, I have essentially copied and pasted ALL of the mesh related code from DX9 into DX11, with almost no changes needed.

All mesh functionality should be made part of the Material System, which is essentially the abstraction layer over Shader APIs. A more well designed and flexible mesh system will make it less work to implement new Shader APIs and easier for users to create custom vertex formats. Here is the system that I want to implement, which is based on the system implemented in the Panda3D game engine:

Vertex Formats

The current system for vertex formats is simply bit flags. Each bit flag represents a vertex element, such as position, normal, texcoord, etc, which contain no information about the name of the element, size of the element, number of components, etc. There is not a lot of room for user customization.

The new system will allow users to create custom vertex formats, defining any kind or number of vertex elements they desire. It will also allow users to easily create vertex formats that contain multiple vertex buffers/streams.

Defining Vertex Formats

CMeshVertexColumn

This class represents a single element in a vertex format. It contains the name as a string, the number of components, and the data type of each component. The column automatically computes its size based on number of components * datatype size.

Example:

// name | number of components | datatype of component
CMeshVertexColumn position( "POSITION", 3, NUMERICTYPE_FLOAT32 );

CMeshVertexArrayFormat

This class represents the vertex format of a single vertex buffer/stream. It simply contains a list of CMeshVertexColumns, and the byte offset into a single row of vertex data is computed for each column.

Example:

CMeshVertexColumn position( "POSITION", 3, NUMERICTYPE_FLOAT32 );
CMeshVertexColumn texcoord( "TEXCOORD", 2, NUMERICTYPE_FLOAT32 );

CMeshVertexArrayFormat arrayFormat;
arrayFormat.AddColumn( position );
arrayFormat.AddColumn( texcoord );

CMeshVertexFormat

This class represents the complete vertex format of a mesh. It simply contains a list of CMeshVertexArrayFormats, which is the format of each vertex buffer/stream that the mesh uses.

Example:

...
CMeshVertexFormat format;
format.AddArray( arrayFormat );

Registering Vertex Formats

After defining a vertex format, the user must register the vertex format with the Material System. This is done so the same pointer can be used for identical vertex formats. You must use the pointer returned from the Material System when creating vertex data.

Example:

...
const CMeshVertexFormat *pFormat = materials->RegisterVertexFormat( format );

Vertex Data

IVertexBufferContext

This interface is implemented by each Shader API. It's only job is to create/destroy the actual graphics API vertex buffer, and upload data sent to it by IMeshVertexArrayData (explained below).

IMeshVertexArrayData

This interface is implemented by the Material System itself. It represents the actual vertex data of a single vertex buffer/stream used to render the mesh. The data is stored in a contiguous block of memory in the format specified by CMeshVertexArrayFormat. It instructs the shader API to create/destroy an IVertexBufferContext, handles locking the memory for modification, and sends its data to the IVertexBufferContext for uploading to the graphics API.

IMeshVertexData

This interface is implemented by the Material System as well. It uses the CMeshVertexFormat set on it to create the IMeshVertexArrayDatas that the mesh uses to render. Each IMeshVertexArrayData is created to hold the number of vertices that the IMeshVertexData was created with. This is the actual object that is binded to a mesh.

Example:

...
// CMeshVertexFormat to use | number of vertices | buffer type
IMeshVertexData *pVData = materials->CreateVertexData( pFormat, 3, SHADER_BUFFER_TYPE_STATIC );

Each array is created using the specified buffer type. You can access the individual arrays on the vertex data using pVData->GetArray( n );. If you want to use a different array from what the vertex data created, you can use pVData->SetArray( n, pArray );. Just make sure that the new array uses the same array format. It is also possible to share arrays between multiple vertex datas.

Modifying Vertex Data

After you create a vertex data, the data is initialized to all 0s. You will probably want to fill it in with actual data.

CMeshVertexWriter

This class is used to modify the data in a single column of an IMeshVertexArrayData. Before using this class, you will need to lock the array that you want to modify.

Example:

...
int nActualFirstVertex;
// array to lock | requested vertex offset | number of vertices we are going to write | actual vertex offset
pVData->LockArray( 0, 0, 3, nActualFirstVertex );
// Write data to the position column
CMeshVertexWriter positionWriter( pVData->GetArray( 0 ), "POSITION", nActualFirstVertex );
positionWriter.SetData3f( 0.0f, 1.0f, 0.0f );
positionWriter.SetData3f( 1.0f, 1.0f, 0.0f );
positionWriter.SetData3f( 1.0f, 0.0f, 0.0f );
// Write data to the texcoord column
CMeshVertexWriter texcoordWriter( pVData->GetArray( 0 ), "TEXCOORD", nActualFirstVertex );
texcoordWriter.SetData2f( 0.0f, 1.0f );
texcoordWriter.SetData2f( 1.0f, 1.0f );
texcoordWriter.SetData2f( 1.0f, 0.0f );
pVData->UnlockArray( 0 );

To append to an array, pass -1 for the second parameter of LockArray(). nActualFirstVertex will be the first vertex in the array you will append from.

CMeshVertexReader

I have not decided if this class is necessary, but if it is, it would work the same as CMeshVertexWriter, but instead of SetDataX(), it would be GetDataX(), returning the data.

Indices

IIndexBufferContext

Similar to IVertexBufferContext, but for index buffers. It is implemented by each Shader API and does the job of creating/destroying the graphics API index buffer, and uploading data sent to it by IMeshIndexData (explained below).

IMeshIndexData

Similar to IMeshVertexArrayData, but for index buffers. Represents the indices into the IMeshVertexData for drawing triangles. Can use 16-bit or 32-bit indices. This is the actual object that is binded to the mesh.

Example:

// index format | number of indices | buffer type
IMeshIndexData *pIData = materials->CreateIndexData( MATERIAL_INDEX_FORMAT_16BIT, 3, SHADER_BUFFER_TYPE_STATIC );

CMeshIndexWriter

Similar to CMeshVertexWriter, but for writing to IMeshIndexDatas.

Example:

int nActualFirstIndex;
pIData->Lock( 0, 3, nActualFirstIndex );
CMeshIndexWriter writer( pIData, nActualFirstIndex );
writer.SetData1i( 0 );
writer.SetData1i( 1 );
writer.SetData1i( 2 );
pIData->Unlock();

Meshes

IMesh

An interface implemented by the Material System. Simply contains an IMeshVertexData, IMeshIndexData, and a primitive type (triangles, triangle strip, etc). It handles binding the vertex and index datas and telling the Shader API to draw.

Example:

...
IMesh *pMesh = materials->CreateMesh();
pMesh->SetPrimitiveType( MATERIAL_TRIANGLES );
pMesh->SetVertexData( pVData );
pMesh->SetIndexData( pIData );
// draw the whole thing
pMesh->Draw();

I'm currently developing this system on the DX11 branch. It will completely break the DX9 Shader API, though I don't know if it will matter since there is no point in keeping DX9 once DX11 is finished.