Hello all, I'm looking for some feedback regrading a model format and loader I've been working on over the past few days. I'm transitioning to C++, coming from C# and this has been a great exercise thus far, but I feel this post might end up a bit lengthy.
Some background as to what I wanted to accomplish with the file format:
- The model can contain a large number of meshes.
- Each mesh can have an arbitrary number of vertex buffers; not that a mesh should have 8, 16, or even 65,535 vertex streams, just that it could.
- Each mesh can have an arbitrary number of textures; again, see above.
- Each mesh and the model itself and should have some sort of bounding volume.
- Fast loading; the model should use local pointers and require minimal live processing.
- Possibility for compression
After some research, it appears that loading directly into memory and then adjusting local pointers seems to be one of the fastest ways to load an object. So the entire format reflects the objects that make up my model.
A model contains: a pointer to some ModelTextures, a pointer to the MeshHeaders, a pointer to the MeshCullDatas and a pointer to the MeshDrawDatas.
I've tried to implement some Data Oriented Design – a very different concept coming from C#. I've split up the meshes into arrays of data needed for different operations: Culling and Drawing.
Furthermore, I'm attempting to implement this as part of my content manager, so a ModelTexture, is really just a wrapper around a shared_ptr<Texture2D> that is retrieved from another content cache.
All right, so here is what the model format looks like, I made a diagram!
*sorry it's so tall...
The actual files are exported from a tool I've written in C#. I'm loading Collada files via Assimp, calculating any user requested data and displaying the model via SharpDX in a WinForms app.
In the end, the model gets exported to the file by the exporter first writing each mesh data object to “virtual memory,” adjusting all of the pointers and finally using a binary writer to spit out the finished file.
Pretty straight forward for me as I'm used to C#, but the scary stuff happens when we get to actually loading the file in C++.
First I load the entire file with an ifstream into a char[]. Then I cast the char[] to a Model. Now I need to offset the local pointers so that the model will work in memory; however! I read somewhere that you can't add pointers in C++, only subtract them, but to offset local pointers you needed to add!
After much internet searching, I finally found an object ptrdiff_t that I could retrieve from a pointer, add to, and then cast back to a pointer. The question then became, “Is this legal, what I'm doing?” For a full day I pondered before quizzically deciding that it should? be legal. I mean how else would you offset pointers when you shouldn't just cast to an int?
The next problem arrived when I realized that I needed to somehow delete the model from memory as well. Again not sure, as I had casted a char[] to a model, if I could delete the model. I pretended I could and wrote the destructor. Miraculously it seemed to work! The “Memory” window in Visual Studio seemed to show that the object had successfully been deleted, although I'm still not sure if I need to call delete on the model's pointers as they weren't created with new.
So now, I have all this code for loading a model, but I'm not sure if it's legal, safe, or even sensible!
Enough talk though, here's the code for loading the model:
std::shared_ptr<Ruined::Graphics::Model> ModelLoader::Load(const std::string &name)
{
Ruined::Graphics::Model * model;
std::ifstream file (m_BaseDirectory + name, std::ios::in|std::ios::binary|std::ios::ate);
if (file.is_open())
{
// Get the file's total size
unsigned int size = file.tellg();
// Create a char[] of the size to load the file into
char* memblock = new char [size];
// Seek to the beginning and read the file
file.seekg (0, std::ios::beg);
file.read (memblock, size);
// Finally close the file
file.close();
// Cast the char[] to a Ruined::Graphics::Model pointer
model = static_cast<Ruined::Graphics::Model *>((void*)memblock);
// The location of the model in memory
ptrdiff_t memOffset = (ptrdiff_t)model;
// Offset the model's local pointers
// Mesh Headers
ptrdiff_t intOffset;
model->MeshHeaders = (Ruined::Graphics::MeshHeader*)(memOffset + (ptrdiff_t)model->MeshHeaders);
// Mesh Culling Datas
// intOffset = (ptrdiff_t)model->MeshCullDatas;
model->MeshCullDatas = (Ruined::Graphics::MeshCullData*)(memOffset + (ptrdiff_t)model->MeshCullDatas);
// Mesh Drawing Datas
// intOffset = (ptrdiff_t)model->MeshDrawDatas;
model->MeshDrawDatas = (Ruined::Graphics::MeshDrawData*)(memOffset + (ptrdiff_t)model->MeshDrawDatas);
// Model's Ruined::Graphics::ModelTexture pointer
// intOffset = (ptrdiff_t)model->Textures;
model->Textures = (Ruined::Graphics::ModelTexture*)(memOffset + (ptrdiff_t)model->Textures);
// Load the model's textures
for(int t = 0; t < model->TextureCount; t++)
{
// Offset TextureName pointers
// intOffset = (ptrdiff_t)(model->Textures[t].TextureName);
model->Textures[t].TextureName = (char*)(memOffset + (ptrdiff_t)(model->Textures[t].TextureName));
// Load the texture
model->Textures[t].TextureContent = p_TextureCache->Load(model->Textures[t].TextureName);
}
HRESULT hresult;
Ruined::Graphics::MeshDrawData * tempMeshD = nullptr;
for(int m = 0; m < model->MeshCount; m++)
{
// Build the buffers
tempMeshD = &model->MeshDrawDatas[m];
// Offset Index Buffer
// intOffset = (ptrdiff_t)tempMeshD->IndexBuffer;
tempMeshD->IndexBuffer = (ID3D11Buffer*)(memOffset + (ptrdiff_t)tempMeshD->IndexBuffer);
// Offset Vertex Buffer
// intOffset = (ptrdiff_t)tempMeshD->VertexBuffer;
tempMeshD->VertexBuffers = (ID3D11Buffer**)(memOffset + (ptrdiff_t)tempMeshD->VertexBuffers);
// Offset Strides
// intOffset = (ptrdiff_t)tempMeshD->Strides;
tempMeshD->Strides = (unsigned int*)(memOffset + (ptrdiff_t)tempMeshD->Strides);
// Offset Resources
intOffset = (ptrdiff_t)tempMeshD->Resources;
tempMeshD->Resources = (ID3D11ShaderResourceView**)(memOffset + intOffset);
// Convert Resources * to unsigned int *
unsigned int * index = (unsigned int*)(memOffset + intOffset);
// Assign to the poingters from the model's textures
for(int t = 0; t < model->MeshHeaders[m].ResourceCount; t++)
tempMeshD->Resources[t] = model->Textures[index[t]].TextureContent.get()->p_shaderResourceView;
// Desc for the index buffer
D3D11_BUFFER_DESC indexBufferDesc;
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
indexBufferDesc.ByteWidth = tempMeshD->IndexCount * (tempMeshD->IndexFormat == DXGI_FORMAT_R16_UINT ? sizeof(unsigned short) : sizeof(unsigned int));
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
indexBufferDesc.CPUAccessFlags = 0;
indexBufferDesc.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA indexData;
indexData.pSysMem = tempMeshD->IndexBuffer;
indexData.SysMemPitch = 0;
indexData.SysMemSlicePitch = 0;
hresult = p_Graphics->GetDevice()->CreateBuffer(&indexBufferDesc, &indexData, &tempMeshD->IndexBuffer);
if(FAILED(hresult))
{
OutputDebugStringA("Failed to create Index Buffer");
}
// Create each vertex buffer
Ruined::Graphics::MeshBufferDesc * tempDesc = (Ruined::Graphics::MeshBufferDesc*)(tempMeshD->VertexBuffers);
for(unsigned int b = 0; b < tempMeshD->VertexBufferCount; b++)
{
// Each buffer gets a desc
D3D11_BUFFER_DESC bufferDesc;
bufferDesc.Usage = D3D11_USAGE_DEFAULT;
bufferDesc.ByteWidth = tempDesc[b].BufferWidth;
bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bufferDesc.CPUAccessFlags = 0;
bufferDesc.MiscFlags = 0;
// Each buffer needs a subresource data
D3D11_SUBRESOURCE_DATA subData;
subData.pSysMem = (void*)((ptrdiff_t)tempDesc[b].Data + memOffset);
subData.SysMemPitch = 0;
subData.SysMemSlicePitch = 0;
hresult = p_Graphics->GetDevice()->CreateBuffer(&bufferDesc, &subData, &(tempMeshD->VertexBuffers[b]));
if(FAILED(hresult))
{
OutputDebugStringA("Failed to create Vertex Buffer");
}
}
}
}
else
{
std::string errorMsg = "Failed to load Model: ";
errorMsg += m_BaseDirectory + name + "\n";
OutputDebugStringA(errorMsg.c_str());
// Set model equal to something
model = new Ruined::Graphics::Model();
}
std::shared_ptr<Ruined::Graphics::Model> sModel(model);
return sModel;
}
So that makes a little more sense, here is Model.h:
#include "MeshDrawData.h"
#include "ModelTexture.h"
#include <memory>
namespace Ruined
{
namespace Graphics
{
// Combine Model Header and Model Pointers
struct __declspec(dllexport) Model
{
public:
// Header
unsigned short _FILETYPE;
unsigned short _FILEVERSION;
unsigned int ModelSize;
unsigned short TextureCount;
unsigned short MeshCount;
DirectX::BoundingBox BoundingBox;
// Pointers
ModelTexture * Textures;
MeshHeader * MeshHeaders;
MeshCullData * MeshCullDatas;
MeshDrawData * MeshDrawDatas;
public:
Model(void);
~Model(void);
};
}
}
#endif
Here is ModelTexture.h:
#pragma once
#ifndef _MODELTEXTURE_H
#define _MODELTEXTURE_H_
// Includes //
#include "Texture2D.h"
#include <memory>
namespace Ruined
{
namespace Graphics
{
struct __declspec(dllexport) ModelTexture
{
public:
char* TextureName;
std::shared_ptr<Texture2D> TextureContent;
};
}
}
#endif
Here are the mesh objects:
#ifndef _MESHCULLDATA_H_
#define _MESHCULLDATA_H_
#include <DirectXCollision.h>
namespace Ruined
{
namespace Graphics
{
struct __declspec(dllexport) MeshCullData
{
public:
DirectX::BoundingBox BoundingBox;
};
}
}
#endif
#ifndef _MESHHEADER_H_
#define _MESHHEADER_H_
#include "MeshBufferDesc.h"
namespace Ruined
{
namespace Graphics
{
enum MeshMask : unsigned short
{
Undefined = 0x0000,
Texture = 0x0001,
UVCoord = 0x0002,
Color = 0x0004,
Normal = 0x0008,
Tangent = 0x0010,
Binormal = 0x0020,
BoneIndices = 0x0040,
BoneWeights = 0x0080,
SplitBuffers = 0x0100,
AlphaBlend = 0x4000
};
struct __declspec(dllexport) MeshHeader
{
MeshMask Mask;
unsigned char UVStreamCount;
unsigned char ColorStreamCount;
unsigned short ResourceCount;
};
}
}
#endif
#ifndef _MESHDRAWDATA_H_
#define _MESHDRAWDATA_H_
#include <d3d11.h>
namespace Ruined
{
namespace Graphics
{
struct __declspec(dllexport) MeshDrawData
{
public:
unsigned int VertexBufferCount;
ID3D11Buffer ** VertexBuffers;
unsigned int * Strides;
ID3D11Buffer * IndexBuffer;
DXGI_FORMAT IndexFormat;
ID3D11ShaderResourceView ** Resources;
unsigned int IndexCount;
};
}
}
#endif
#ifndef _MESHBUFFERDESC_H_
#define _MESHBUFFERDESC_H_
namespace Ruined
{
namespace Graphics
{
// Used for creating vertex buffers.
// Only accessed at load time.
struct __declspec(dllexport) MeshBufferDesc
{
public:
unsigned int BufferWidth;
void * Data;
};
}
}
#endif
Lastly here is the Model destructor:
Model::~Model(void)
{
if(Textures != nullptr)
{
for(int t = 0; t < TextureCount; t++)
{
Textures[t].TextureContent.reset();
Textures[t].TextureName = nullptr;
}
}
if(MeshDrawDatas != nullptr)
{
for(int m = 0; m < MeshCount; m++)
{
if(MeshDrawDatas[m].IndexBuffer != nullptr)
{
MeshDrawDatas[m].IndexBuffer->Release();
MeshDrawDatas[m].IndexBuffer = nullptr;
}
for(int v = 0; v < MeshDrawDatas[m].VertexBufferCount; v++)
{
if(MeshDrawDatas[m].VertexBuffers[v] != nullptr)
{
MeshDrawDatas[m].VertexBuffers[v]->Release();
MeshDrawDatas[m].VertexBuffers[v] = nullptr;
}
}
}
}
}
Holly cow! That's one long post.
If anyone could take the time to read this, even just part of it, and lend me a hand, I would be very thankful.