XNA provides a means of obtaining a bounding sphere from a model but not a bounding box. Depending on the shape of the 3D object a bounding box can be much more useful for collisions than a sphere. A bounding box can be created quite easily for most models using the methods described below but there are times when a model structure may mean this does not work. These cases are explained below. The ultimate solution to handle all cases is to create your own content processor however the methods here will work for 99% of 3D models.
To obtain a bounding box around our model in model space (the space the model is created in i.e. with 0,0,0 normally at the centre of the model in your art package, for more information see : XNA Matrix) we need to go through all the vertices in the model keeping a track of the minimum and maximum x, y and z positions. This gives us two corners of the box from which all other corners can be calculated since the box is aligned along the axis (known as an Axis Aligned Bounding Box or AABB for short).
Since each model is made from a number of mesh we need to calculate minimum and maximum values from the vertex positions for each mesh. The ModelMesh object in XNA provides access to the buffer holding the vertex (VertexBuffer) from which we can obtain a copy of the vertices using the GetData call. The GetData call is a generic function which allows us to provide a vertex type.
We may not actually know the type of vertex used in the model and this is an issue I will address in a minute. However here I am assuming the model uses a vertex of type VertexPositionNormalTexture which is the most common vertex type for a model. To know how many vertices there are in the vertex buffer we have to divide the size of the buffer in bytes by the provided VertexStride (the number of bytes needed for one vertex used in this mesh).
Example code for finding minimum and maximum bounds per mesh is shown below:
Matrix []m_transforms = new Matrix[m_model.Bones.Count];
m_model.CopyAbsoluteBoneTransformsTo(m_transforms);
foreach (ModelMesh mesh in m_model.Meshes)
{
VertexPositionNormalTexture[] vertices=
new VertexPositionNormalTexture[mesh.VertexBuffer.SizeInBytes / mesh.MeshParts[0].VertexStride;
mesh.VertexBuffer.GetData<VertexPositionNormalTexture>(vertices);
// Find min, max xyz for this mesh - assumes will be centred on 0,0,0 as BB is initialised to 0,0,0
Vector3 min = vertices[0].Position;
Vector3 max = vertices[0].Position;
for (int i = 1; i < vertices.Length; i++)
{
min = Vector3.Min(min, vertices[i].Position);
max = Vector3.Max(max, vertices[i].Position);
}
// We need to take into account the fact that the mesh may have a bone transform
min = Vector3.Transform(min, m_transforms[mesh.ParentBone.Index]);
max = Vector3.Transform(max, m_transforms[mesh.ParentBone.Index]);
// Now expand main bb by this mesh's box
modelBoundingBox.Min = Vector3.Min(modelBoundingBox.Min, min);
modelBoundingBox.Max = Vector3.Max(modelBoundingBox.Max, max);
}
Note that after calculating the minimum and maximum vertex position values for the mesh we also need to take into account any bone transformations that may need to be applied. The final two lines takes the bounding box just calculated for this mesh and expand the main model bounding box accordingly. XNA provides a bounding box type so prior to the function above (and probably stored as a member of your 3D model entity class) you would declare it like so:
BoundingBox modelBoundingBox;
For more accurate collisions you may also want to store the individual mesh bounding boxes. A first collision check would then check the model bounding box and if this proved true go through each mesh bounding box. This would also have the benefit of giving you the part of the model where the collision occurred.
If we don't know the type of vertex used in the model mesh things become more difficult. One way forward is to consider that every vertex type always has a position element and that position element is always first in the structure. This means that the first three floats of each vertex will be the position. With this knowledge we can extract the vertex buffer data as floats and therefore we change our per mesh code to:
// Get vertex data as floats
float []vbData=new float[mesh.VertexBuffer.SizeInBytes/sizeof(float)];
mesh.VertexBuffer.GetData<float>(vbData);
Vector3 min = new Vector3(vbData[0], vbData[1], vbData[2]);
Vector3 max = min;
for (int i = 0; i < mesh.VertexBuffer.SizeInBytes / sizeof(float); i += mesh.MeshParts[0].VertexStride / sizeof(float))
{
Vector3 pos=new Vector3(vbData[i], vbData[i+1], vbData[i+2]);
min = Vector3.Min(min, pos);
max = Vector3.Max(max, pos);
}
There are a couple of other issues though. Firstly are we sure that every vertex in the vertex buffer is used by the mesh? You might think so however it is not guaranteed. Secondly what if there are multiple mesh parts per mesh with different vertex strides? The first issue could be solved by using the index buffer to look up the vertices. The second could be solved by processing per mesh part. However for the vast majority of situations the above will work just fine and if you want a system that works for every possible model you would probably be better to look at implementing some custom content processor code instead.
This is currently beyond the scope of these notes but is ultimately the best way to calculate your bounding box. By writing code that hooks into the actual model loading we can deal with the vertex data prior to it being put into a vertex buffer and optimised and reorganised by XNA.
It takes some time to go through all the vertices finding min and max values therefore you should only do this once at model load time and not during the game loop. For maximum speed you would do this in advance of the game running.
If our model is animated then we need to modify our bounding boxes at run time. We do this by storing each mesh min and max values we calculated and at game loop time transforming them using the bone matrix. Note that we don't need to extract the vertex again to do this. A bounding box for the whole model can then be created by looping through all the transformed mesh bounding boxes finding the min and max values.