Contents
This document is my attempt at providing some documentation on the MDS format used in Return to Castle Wolfenstein. It's totally unofficial, so bear with me if certain areas are a little vague or inaccurate. Most of the information was gleaned by myself using a hex editor and many nights of MAXScripting, but huge thanks go to Ryan Feltrin for providing me with the crucial last bits of the MDS puzzle; Richard Crowder for getting me to continue working on my MDS importer (as well as providing loads of help and advice); and TiCaL for all his help. Cheers, guys!
I'm assuming you're familiar with the basics of 3D, Quake 3 mods, and skeletal animation with smooth mesh deformation, so I won't cover these topics in much detail.
MDS In a Nutshell
So what's in an MDS file? The main things are:
MDS Data Structures
These are the main data structures present in an MDS file:I've used a C/C++ like notation with explicitly sized types for clarity. For example, uint32 is a 32-bit unsigned integer, sint16 is a signed 16-bit integer and so on. floats are assumed to be 32 bits in size, and chars 8-bits. Note that as there are some hard limits on the numbers of certain structures, so the unsigned/signed distinction is probably overkill.
MDSHeader |
struct MDSHeader { uint32 ident; // "MDSW" uint32 version; // 0x0004 char name[64]; // Model name eg. "body.mds" float lodScale; // LOD Scale float lodBias; // LOD Bias uint32 numFrames; // Number of animation frames uint32 numBones; // Number of bones uint32 offsetFrames; // Offset of animation frames (see MDSFrame) uint32 offsetBones; // Offset of bones (see MDSBone) uint32 torsoParent; // Index of torso parent bone uint32 numSurfaces; // Number of surfaces uint32 offsetSurfaces; // Offset of surfaces (numSurfaces * MDS_Surface) uint32 numTags; // Number of tags uint32 offsetTags; // Offset of tags (numTags * MDS_Tag) uint32 offsetEnd; // Offset of end of file }; |
This structure is located at the very start of an MDS file. The torsoParent index probably refers to the bone that controls the overall orientation of a player's torso. |
MDSBone |
struct MDSBone { char name[64]; // Bone name sint32 parentIndex; // Bone parent index (-1 if root) float torsoWeight; // 0.0 to 1.0 float parentDist; // Distance from parent bone to this bone's pivot point uint32 flags; // Bit 0 is set if bone is a tag }; |
Defines a bone used to deform the MDS model's surface meshes during animation. The torsoWeight value is apparently used to scale the torso rotation and torso parent bone. |
MDSTag |
struct MDSTag { char name[64]; // Name of tag float torsoWeight; // 0.0, 0.3 uint32 boneIndex; // Index of bone this tag is attached to }; |
Defines a tag/attachment point for weapons, MD3/MDC head models and particle effects. Each tag has a corresponding bone in the skeleton, with the same name as the tag (eg. tag_head). |
MDSFrame |
struct MDSFrame { float[3] bboxMin; // Bounding box min float[3] bboxMax; // Bounding box max float[3] localOrigin; // Local origin float radius; // Radius of bounding sphere (used for runtime culling) float[3] parentOffset; // Offset of parent bone from origin // Following this structure is a list of compressed bone frames MDSCompressedBoneFrame bones[numBones]; }; |
This structure is present at the start of every animation frame. Following each MDSFrame is a sequence of MDSCompressedBoneFrames, with one entry for each bone in the model's skeleton. The parentOffset vector defines the offset of the parent bone from the world origin. |
MDSCompressedBoneFrame |
struct MDSCompressedBoneFrame { sint16[4] angles; // Defines the bone orientation sint16[2] offsetAngles; // Defines the direction of the bone pivot from this bone's parent }; |
A bone keyframe that defines position and orientation per frame. The first 3 entries in the angles array are stored in (pitch, yaw, roll) order. The 4th entry is just padding and can be safely ignored (it's typically set to zero anyway). The offsetAngles are stored in (pitch, yaw) order. In RTCW, pitch, yaw and roll are defined assuming that +X is forward, -Y is right and +Z is up, with right handed rotations. For example, a positive pitch will tilt down towards -Z. To convert the compressed 16-bit angles into degrees, simply scale them by 360.0 / 65536.0. To obtain the origin of the bone's local coordinate system, create a rotation matrix from offsetAngles using its pitch and yaw values. Construct a 3D point with the parentDist value stored in the bone's MDSBone structure as the x component, with y and z components set to zero (ie. [parentDist, 0, 0]). Transform this point using the offsetAngles rotation matrix and add it to the bone's parent's origin to get the bone's own origin. If the bone has no parent, use the parentOffset coordinate stored in the MDSBoneFrame for this frame in place of the parent's origin. To correctly orient the bone, construct its rotation matrix from angles using the roll, pitch and yaw (in that order) values. Together with the bone origin, this rotation matrix defines the bone's coordinate system for the current frame. Take a look at MDSData.ms and MDSLoader.ms in my MDS Importer to see how I managed to load the bone information into gmax/max. |
MDSSurface |
struct MDSSurface { uint32 ident; // Always 0x0008 char name[64]; // Name of surface char shader[64]; // Name of shader uint32 shaderIndex; // Used in game only sint32 minLod; // Minimum Lod level sint32 offsetHeader; // Offset of this surface header, always negative uint32 numVerts; // Number of vertices uint32 offsetVerts; // Offset of vertices (MDSVertex * numVerts) uint32 numTris; // Number of triangles uint32 offsetTris; // Offset of triangles (MDSTriangle * numTris) uint32 offsetCollapseMap; // Offset of the collapse map (uint32 * numVerts in size) uint32 numBoneRefs; // Number of bone references (bones that influence this surface) uint32 offsetBoneRefs; // Offset of bone refs (uint32 * numBones) uint32 offsetEnd; // Offset of the end of this surface, from start of MDSSurface header. // The next surface (if there are more) can be found at this offset. }; |
A surface is a triangle mesh rendered using a single shader. Each surface has its own collection of vertices (each influenced by one or more bones) and triangles. The bone references aren't indexed by the MDSWeight structures, which instead index into the main bone list. The bone refs provide a list of all the bones referenced by a surface's MDSWeight structures, which is often less than the total number of bones in the model. The collapse map is a list of target vertices for each surface vertex. The mapping from a mesh vertex to a target vertex defines an edge collapse. By successively collapsing edges and removing triangles that become degenerate after these collapses, the surface can be simplified automatically. It's also possible to go the other way, adding triangles right back up to the original full level of detail. This is known as a progressive mesh system and is used by RTCW to dynamically adjust the individual levels of detail for all onscreen MDS models whilst the game is running. See the collapse map section for a few more details on this dead funky technique. The minLod is likely to be the lowest possible detail level. This is the point in the collapse map at which the engine should stop edge collapsing to prevent the mesh becoming noticeably distorted, even when in the distance. |
MDSVertex |
struct MDSVertex { float[3] normal; // Vertex normal vector. In base pose/model space I presume. float[2] texCoords; // Texture (s,t) co-ordinates uint32 numWeights; // Number of bone weights affecting this vertex uint32 fixedParent; // Stay equidistant from this parent float fixedDist; // Fixed distance from parent MDSWeight weights[numWeights] // List of weights follows }; |
A surface vertex, which must have at least one weight. Not 100% sure on the fixedParent and fixedDist entries, but they are probably used to prevent certain vertices straying too far from certain bones (causing visually unacceptable distortion) during animation. |
MDSWeight |
struct MDSWeight { uint32 boneIndex; // Index of bone in the main bone list float weight; // Bone weighting (from 0.0 to 1.0) float[3] xyz; // xyz bone space position of vertex }; |
There is an MDSWeight structure for every bone that has some influence over a vertex. Bones with higher weights have more influence over the final position of a vertex during animation. For example, a weight of 1.0 will mean the corresponding bone (indicated by boneIndex) totally defines the position of the vertex. The weight values stored in each MDSWeight for a single vertex should all sum to 1.0. If you want to know more about how the smooth mesh deformation process works, check out Game Programming Gems I for some good articles. Also, have a look at the function "SkinBaseSurfaces" in the MDSLoader.ms MAXScript from my MDS Importer where I manually 'skin' the initial base pose mesh before handing it over to the Skin modifier to do the rest of the work.
|
MDSTriangle |
struct MDSTriangle { uint32[3] vertexIndices; // 3 indices into the surface's vertex list }; |
A triangle in a surface. |
These are the upper limits imposed by RTCW on the numbers of various data structures in an MDS file:
Here are some ramblings on the collapse map structure that might be of use:
The collapse map (as introduced here) is used for runtime dynamic LOD (level of detail) adjustment. In Quake 3, separate models where switched between according to the estimated on-screen size of each player model. The system used in Wolfenstein automatically adjusts the triangle count of the model up and down as its on-screen size changes without requiring any additional models! Cool, eh?
Models that can have their detail adjusted by edge collapses (and the opposite, vertex splits) are known as progressive meshes. Technically, RTCW uses view-independent progressive meshes or VIPM as it's known to its friends. The particular variant used in RTCW is based on the one described by Stan Melax in his polygon reduction article, which you can find on his web site along with example programs and source.
There has been a lot written on progressive meshes in the last few years, and I'd recommend you consult Game Programming Gems I and II for some great articles by Jan Svarovsky and Tom Forsyth on the topic. Charles Bloom also has some very interesting VIPM notes on his web site. Finally, if you don't mind academic papers, the relevant work by Hugues Hoppe is essential reading.
Creating the collapse map involves picking the edge collapse that causes the least visible difference, doing the collapse and repeating with the remaining edges until the mesh cannot be simplified any more. Once all the collapses are done, the mesh vertices are sorted into collapse order (last one to be collapsed is first) and the collapse map is written out. It would be far too time consuming to manually pick the edge collapses in the best order, so this process is done automatically by the MDS build tool. A fully featured exporter is free to choose edge collapses in any order, but for best results some time should be spent on developing a good error metric.
As a rough guide, here are total poly counts for some MDS models:
The End