M2s have arrived! Behold the glorious trees!
Well that time is finally here. We are going to get the last remaining bit of data (at least right now) into the application. There may be other things we need later, and off the top of my head I can say for sure bounding boxes are needed which we don’t have in there all the way just yet, but this will give you all the geometry that you need in your scene.
Well after a bit of toying I got M2’s working in the new codebase. The only thing I didn’t do yet with them is normal vectors. I don’t feel it’s needed at this time because you can pretty well see M2’s without them. Like with any new object we are going to make a new folder called M2 and put it under the ADT folder. We are then going to create an M2.cs file that’ll be the object to represent our actual M2’s that we are pulling. You’re M2.cs file should look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Net; using Microsoft.Xna.Framework.Storage; namespace MPQNav.ADT { class M2 { } } |
Pretty basic stuff there just like most of our others. Make sure the using directives are setup properly and you may have to change the namespace. That’s about it really.
First two variables for this class are going to be for bounding boxes. We don’t fill them in this code segment, but we will another time. We put them there for when we need them.
Next we are going to have two variables that will hold the vectors for this M2. You may ask why we have two of these and the answer is one is for the pre-transformed vectors and the second is for the transformed vectors. Holding the vectors before transformation in the object is probably a waste of time and space but I haven’t decided 100% if they will be needed for anything else yet. Hence they are there.
1 2 | public List<Vector3> verticiesList = new List<Vector3>(); public List<VertexPositionNormalColored> finalVertexList = new List<VertexPositionNormalColored>(); |
The final variable is a list for our indices (we are again building a triangle list):
Tada! Your M2.cs file is complete!
That was easy enough right? Right!
Now it’s time to go back to ADT.cs. M2’s are a lot like WMO’s, we are going to have a list of strings that end with a 0 specifying what M2 is used, as well as a bunch of chunks that tell us the placement for those M2s. Those chunks are called MDDF chunks. So let’s make a class to hold that information for us:
1 2 3 4 5 6 7 8 9 10 11 12 | public class MDDF { // Placement information for M2s public String name; public String filePath; public UInt32 uniqid; public Vector3 position; public float orientation_a; public float orientation_b; public float orientation_c; public float scale = 1; } |
Almost identical to the previous placement information for WMO’s with the exception of the scale value. M2’s can be scaled whereas WMO’s are never scaled. Next we add a few variables for the ADT to hold the M2 information for us:
1 2 3 | public List<String> m2FileNameList = new List<string>(); public List<MDDF> MDDFList = new List<MDDF>(); public List<M2> M2List = new List<M2>(); |
These are again pretty basic and just like our WMO’s. We have a list of strings for the filenames, a list of the MODF (placement information) chunks, and a list of actual M2 objects. Nothing too special here and that’s it for our ADT.cs file.
Now with that all taken care of we’re ready to get the data into our classes. For that we go back to our ADTManager.cs file. There’s two lines of code we already have that will be useful to us:
1 2 | UInt32 offsModels = this.br.ReadUInt32(); /// 20 Bytes behind MDDX (filenames for M2s) UInt32 offsDoodsDef = this.br.ReadUInt32(); // 20 Bytes Behind MDDF (Placement for M2s) |
That’s right! We were smart enough to already get the offsets that we need for the bits of M2 information! If you scroll down a bit in the processADT() method you’ll see these lines:
1 2 | this.buildWMONamesList((int)offsMapObjects, br, currentADT); this.processWMOs((int)offsObjectsDef, br, currentADT); |
We’re going to do the same thing for m2’s so add:
1 2 | this.buildm2NamesList((int)offsModels, br, currentADT); this.processM2s((int)offsDoodsDef, br, currentADT); |
Now it’s time to actually write those methods! Let’s start with the buildm2NamesList method:
1 2 3 4 | public void buildm2NamesList(int Offset, BinaryReader br, ADT currentADT) { } |
Just like our WMO names method. In fact it’s almost a direct copy/paste of said method so I am not going to walk through the code step by step again. Please review the WMO post if you want details:
Now that we have that code it’s time to write the processM2s method which is very similar to the one we already wrote for WMOs:
Now just like before we are going to move the position of our BinaryReader a bit, and get the size of the chunk. We’re then going to setup an integer that tracks how many byte’s we’ve read so we know when we’re at the end of the chunk. Remember that this is the MDDF chunks which happen to be the placement information for all the M2’s in the current ADT:
1 2 3 | br.BaseStream.Position = offset + 24; // 20 to compensate and 4 to get off the header UInt32 chunkSize = br.ReadUInt32(); int bytesRead = 0; |
Next we start a little loop to get the data:
Within this loop we’re going to once again setup a temporary object for the chunk, get all the information we want, and then loop back through. MDDF chunks are 36 bytes in size (nothing we haven’t done before):
1 2 3 4 5 6 7 8 9 10 | ADT.MDDF currentMDDF = new ADT.MDDF(); currentMDDF.filePath = currentADT.m2FileNameList[(int)br.ReadUInt32()]; // 4 bytes currentMDDF.uniqid = br.ReadUInt32(); // 4 bytes currentMDDF.position = new Vector3((float)br.ReadSingle(), (float)br.ReadSingle(), (float)br.ReadSingle()); // 12 Bytes currentMDDF.orientation_a = (float)br.ReadSingle(); // 4 Bytes currentMDDF.orientation_b = (float)br.ReadSingle(); // 4 Bytes currentMDDF.orientation_c = (float)br.ReadSingle(); // 4 Bytes currentMDDF.scale = (float)(br.ReadUInt32() / 1024f); // 4 bytes bytesRead += 36; // 36 total bytes currentADT.MDDFList.Add(currentMDDF); |
The only thing to note here is that we divide the value for the scale by 1024. For some reason blizzard went with an integer for the scale instead of a floating point number, but dividing it by 1024 gives us the value that works.
That’s it for that loop! We now should have all the MDDF data. Now let’s go ahead and loop through them:
Next we are going to check to make sure the file exists. We should really throw an error here if it doesn’t, but my laziness is really shining lately and I didn’t bother. There is one interesting thing here though. The files we extract from the MPQ’s have the extension of m2 whereas they are referred to with an extension of mdx so we must fix that when we check for the file:
1 2 3 | if (File.Exists(this.basePath + currentADT.MDDFList[i].filePath.Replace(".mdx", ".m2"))) { } |
Once we know the file exists, we should probably open it up so we can read it eh?
1 2 3 | String currentFilePath = currentADT.MDDFList[i].filePath; FileStream fs = File.OpenRead(this.basePath + currentADT.MDDFList[i].filePath.Replace(".mdx",".m2")); BinaryReader m2Reader = new BinaryReader(fs); |
And why not create a new instance of our M2 class to put all the data we’re going to get in?
Got that? Good! Now like everything else M2 files have a header. Without boring you about the details of the header, I can tell you 68 bytes in is how far you must go for the data we want. This is all given on http://wowdev.org/wiki/index.php/M2 if you are curious. As such we skip those bytes, and then read in some variables.
1 2 3 4 5 | m2Reader.ReadBytes(68); UInt32 numberOfVerts = m2Reader.ReadUInt32(); UInt32 vertsOffset = m2Reader.ReadUInt32(); UInt32 numberOfViews = m2Reader.ReadUInt32(); UInt32 viewsOffset = m2Reader.ReadUInt32(); |
- numberOfVerts - Number of vertices in this M2
- vertsOffset - Offset to the vertices information
- numberOfViews - Not really used, thought it might be so I took it anyway.
- viewsOffset - Going to help us get to the indices information.
First thing we are going to get is the vertices. Let’s move the reader to their offset and setup a loop to get them all.
1 2 3 4 5 6 7 8 9 | m2Reader.BaseStream.Position = vertsOffset; for (int v = 0; v < (int)numberOfVerts; v++) { float vert_x = (float)m2Reader.ReadSingle() * -1; float vert_z = (float)m2Reader.ReadSingle(); float vert_y = (float)m2Reader.ReadSingle(); currentM2.verticiesList.Add(new Vector3(vert_x,vert_y,vert_z)); m2Reader.ReadBytes(36); // Extra Junk for now } |
Pretty basic stuff. We loop through once for each vertex, and we get the X,Y, and Z for each one (again in the weird format with Z being up so we have to adjust it). We then add it to the list for our M2 (the one that has non-transformed vertices) and then we read the extra 36 bytes of data we don’t need (normals are in there when/if we decide to use them).
After that is all done it’s time to get the indices information. So we move the reader again to that offset, and we get some data:
1 2 3 4 5 | m2Reader.BaseStream.Position = viewsOffset; UInt32 numElementsIndex = m2Reader.ReadUInt32(); UInt32 offsElementsIndex = m2Reader.ReadUInt32(); UInt32 numElementsTriangle = m2Reader.ReadUInt32(); UInt32 offsTriangleList = m2Reader.ReadUInt32(); |
Now the way this is done is a bit odd. There’s one list of indices that point to the list of vertices. Then the actual indices list points to the first one. The first one is almost like a middle-man.
As you can see it’s not made to make much sense, but it’s there so we simply work around it. Back to the variables:
- numElementsIndex - Number of elements in our Jump indices list
- offsElementsIndex - Offset to our Jump indices list
- numElementsTriangle - Number of elements in our final indices list
- offsTriangleList - Offset to that final list
With that in hand we first get the jump list. We create a local variable to hold this information (we don’t need to store it) and then loop through getting the data:
1 2 3 4 5 | List<int> jumpList = new List<int>(); for(int v = 0; v < (int)numElementsIndex; v++) { jumpList.Add((int)m2Reader.ReadUInt16()); } |
After that we go to the real list, and get the data. Remember the diagram when reading this code:
1 2 3 4 5 | m2Reader.BaseStream.Position = offsTriangleList; for (int v = 0; v < (int)numElementsTriangle; v++) { currentM2.finalIndiciesList.Add(jumpList[(int)m2Reader.ReadUInt16()]); } |
That lets us go through our "jump" just like we planned! Now the final step is to transform the vectors (scale them, put them in real world coordinates, and rotate them). To do this I did what I should have done earlier and created a separate method called transformVector. It looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public Vector3 transformVector(Vector3 baseVector, float x, float y, float z, float orientation_a, float orientation_b, float orientation_c, float scale) { // Real work positions for a transform float pos_x = (x - 17066.666666666656f) * -1; float pos_y = y; float pos_z = (z - 17066.666666666656f) * -1; Vector3 origin = new Vector3(pos_x, pos_y, pos_z); float my_x = (float)baseVector.X + pos_x; float my_y = (float)baseVector.Y + pos_y; float my_z = (float)baseVector.Z + pos_z; Vector3 baseVertex = new Vector3(my_x, my_y, my_z); //1 degrees = 0.0174532925 radians float rad = 0.0174532925f; // Creation the rotations float a = orientation_a * -1 * rad; float b = (orientation_b - 90) * rad; float c = orientation_c * rad; // Fancy things to rotate our model Matrix rotateY = Matrix.CreateRotationY(b); Matrix rotateZ = Matrix.CreateRotationZ(a); Matrix rotateX = Matrix.CreateRotationX(c); Matrix scaleMatrix = Matrix.CreateScale(scale); Vector3 rotatedVector = Vector3.Transform(baseVertex - origin, rotateY); Vector3 scaledVector = Vector3.Transform(rotatedVector, scaleMatrix); Vector3 finalVector = scaledVector + origin; return finalVector; } |
I already discussed this in the WMO section (identical code here with the exception of the scale which should be simple enough to read and understand, so I’m not going to do so again. Back to our previous method (processM2s) we were about to use that new method of ours:
1 2 3 4 5 6 7 | for (int v = 0; v < currentM2.verticiesList.Count; v++) { Vector3 modifiedVector = transformVector(currentM2.verticiesList[v], currentADT.MDDFList[i].position.X,currentADT.MDDFList[i].position.Y,currentADT.MDDFList[i].position.Z, currentADT.MDDFList[i].orientation_a, currentADT.MDDFList[i].orientation_b, currentADT.MDDFList[i].orientation_c, currentADT.MDDFList[i].scale); currentM2.finalVertexList.Add(new VertexPositionNormalColored(modifiedVector, Color.Red, Vector3.Up)); } currentADT.M2List.Add(currentM2); |
We loop through each vector, use our new method to transform it, and then we put it into our final list. Notice I went with red for the color here and just set the normal vector to a basic vector that is pointing straight up. That does it for our processM2s method!
Now there’s only two other methods we need in our manager. Identical to WMO’s we need a method to get the indices for all the M2’s used by an ADT as well as the vertices. Once again review the WMO post for more information on how these two methods work:
Because I’m so nice here’s the entire ADTManger.cs file:
