3Д БУМ

3Д принтеры и всё что с ними связано

The 3D5 MODEL5

A

s I browse various Internet forums, read mailing lists, and even just talk to various people about 3D models, this format seems to come up a lot. The questions “How do I load 3ds models?” and “How do I use 3ds models in my program?” show up all over the place.

Unfortunately, until now, there was not a very good reference on this format available. The references out there fell into one of three categories: libraries that offered you little control over what you could do, code and programs that are impossible to decipher, and confusing, very technical documents. Although this writing by no means covers everything to do with the 3ds format, my goal here is to get you started in the right direction.

This chapter guides you through loading and displaying a 3ds model exported directly from AutoDesk’s 3ds format (https://www. autodesk. com). There is a ton of information contained in the 3ds files. This chapter extracts the data that will be most useful to you in your games or other 3D programs. This data includes the mesh data and material informa­tion, neglecting editor flags, lights, and other pieces. However, if you do feel you need these extras, it’s a simple matter to add them after you get through this chapter.

Understanding Chunky 3ds Files

The format of a 3ds file is set up in a very interesting way. Each section of the file is its own “chunk”. Each chunk contains an identifier, a length, and a group of bytes that holds the data for that chunk. Al­though other files had chunks, they either included a single header telling you where you would find each chunk or had the chunks set up in a specific order. A 3ds file does neither of these. The 3ds files can have the chunks in any order and there is no header telling you where they are located in the file or even how many of each chunk there is. The idea of this is shown in Figure 7.1, which illustrates a single file, made of up several chunks.

This is fine and dandy until you look a little more in depth. The setup of a 3ds file can be very confusing at first glance. Not only is it split into many small chunks, but each chunk can have smaller sub-chunks, which may have even more sub-chunks. Worst of all, for the most part, the chunks need not be in any specific order in the file.

The 3D5 MODEL5

The 3D5 MODEL5

Figure 7.1 A file made of chunks of data.

The structure of a 3ds file is set up more like Figure 7.2, rather than the format shown in Figure 7.1.

The 3D5 MODEL5

Figure 7.2 A graphics representation of a file using a structure similar to 3ds.

So just how do you handle that? The easiest way I have found is to first read in the header of the chunk. Each chunk in the 3ds file has a six-byte header that holds the identifier and length of the chunk. Then, based on the type of chunk and the length, a certain section of the program can be called to manipulate and process the data. A chunk can represent the meshes or triangles of an object, whereas sub-chunks of this chunk hold vertices, texture coordinates, and material information for that mesh. Other chunks can hold materials with sub-chunks for diffuse, specular, and ambient materials, with sub-chunks for each of the colors.

There are many benefits to handling it this way. First of all, it allows you to only process certain chunks. If a chunk contains data that is unneeded, you can simply read right past it. In the 3ds data files, for instance, you will probably want to skip over chunks that contain data that is relevant to the editor. Second, it makes it very easy to work with files such as 3ds models where the chunks are not in a set order. Because the header is read first, the type of chunk can be determined, and the appropriate actions can be taken by the code.

Last, this approach leads to a very modular program. If you need to change the specifications of a chunk or you add code for new chunks, it is a fairly simple process. It is very easy to find the section dealing with certain parts of the code. Adding code to deal with new types of chunks entails only tacking extra code onto the end.

This example program uses STL vectors quite a bit. If you need infor­mation on vectors, check out Appendix B, “STL Vector Primer,” for a brief introduction or refresher.

3ds Chunk Headers: The Start

Each chunk in the 3ds file starts with a S3dsChunkHeader structure.

//————————————————————————-

//- S3dsChunkHeader //- Header for each chunk struct S3dsChunkHeader {

unsigned short m_usID; unsigned int m_uiLength;

};

The first variable is the two-byte chunk identifier. This tells you what type of data the chunk contains. It can be vertex data, face data, animation data, even useless information data. The length variable is the length of the whole chunk, including the header. This can be a bit misleading however; the length value encompasses the current chunk plus the lengths of all the sub-chunks it contains, so be careful. Table 7.1 lists the most common chunk IDs along with their uses.

The 3ds Data File

The 3ds file is made up of many chunks, some of which are important for games, many which are not. In the following sections, you will learn about the most important chunks within a 3ds file. Using these chunks you can load, render, and texture a 3ds file. Although the first few chunks are guaranteed to be at the beginning of the file, the rest may pop up anywhere in the file, most of them any number of times.

Header 0x4D4D

Now you are ready to delve into the file itself. The first six bytes of the file should contain the main chunk’s header. The ID of this header is 0x4d4d and the length variable should contain the total length of the file. This chunk contains all of the sub-chunks that make up the file. No action is necessary at this point; you can just move past the header and start reading chunks. Each 3ds model contains only one of these chunks.

The Version Chunk 0x0002

Somewhere in the file, generally the first sub-chunk (the 0x4D4D chunk), is this chunk. A total of 10 bytes in length, the data consists of a single four — byte integer containing the format version. It should be greater than 3. Older versions of 3ds Max differ from the newer versions in the way the file is set up, meaning that this program might not load them correctly.

Objects in the 0x4000 Chunk

The 0x4000 chunk contains data about objects. An object can be a triangle mesh, light, camera, or even window settings for the editor. For game programming, you generally want to neglect everything but the

IV~^

Table 7.1 Common 3ds Chunks and Their Uses

ID Number Use

0x4D4D Used at the start of the file to signify that the file is a 3ds file.

0x0002 Holds the version number of the file.

0x4000 Contains an “object” such as a mesh, camera, or light.

Each 0x4000 chunk contains sub-chunks with vertex, texture coordinate, and other information.

0x4100 A sub-chunk of 0x4000. A 0x4100 chunk contains every­

thing needed to build a mesh of triangles.

0x4110 Contains the vertices for the object. It is a sub-chunk of

0x4100.

0x4120 Also a sub-chunk of 0x4100, a 0x4120 chunk contains the

face information, including the vertex indexes that tell which vertices make up each face.

0x4130 Another sub-chunk of 0x4100, 0x4130 contains informa­

tion on which materials are to be applied to which faces.

0x4140 Even another sub-chunk of 0x4100, this chunk contains the

texture coordinates that allow a face to be texture mapped.

0xAFFF A material definition is found inside 0xAFFF, colors for

ambient, diffuse, and specular materials, as well as shininess and transparency are in this chunk.

0xA000 A sub-chunk of 0xAFFF that contains the material’s name.

0xA010 A sub-chunk of 0xAFFF as well, contains the ambient

color for the material.

0xA020 Another sub-chunk of 0xAFF, 0xA020 contains the diffuse

color of the material.

0xA030 A fourth sub-chunk of 0xAFF, this contains the specular

highlight colors for the specific material.

0xA040 Yet another 0xAFFF sub-chunk, this time contains the

shininess of the material.

0xA050 Again, a sub-chunk of 0xAFFF that controls how transpar­

ent or opaque a material is.

0xA200 0xA200 is also a sub-chunk of the 0xAFFF chunk. This

chunk stores the filename of the texture map or skin for the current material.

meshes. Loading lights and cameras could interfere with other parts of your game and lead to some very strange looking visual artifacts. How­ever, you might want to look into loading and using the lights if you are interested in using the 3ds format for a level or world format.

The 0x4000 chunk contains a little bit of data by itself; the rest is wrapped up within sub-chunks. It contains a null-terminated string that stores the mesh name. Because the string’s length is not set, it must be null terminated. Using strcpy on the data buffer can extract this name. However, if you are using a temporary pointer to move through the array, make sure you move it strlen(m_cName) + 1 bytes to account for the null terminator on the end of the string. After the string is read, the rest of the data length is full of sub-chunks. Keep in mind there may be a lot of meshes in the same file, so be sure to keep track of where you are.

//pseudocode to begin reading the 0x4000 chunk

if chunkID is 0x4000

read null terminated string (strcpy)

advance pointer past string and null pointer (strlen+1)

Triangular Mesh 0x4100

This is where the data you really want is. A triangular mesh chunk holds just that, a mesh containing triangles. A mesh is a group of polygons that make up a surface; a triangular mesh is the same except it consists of only triangles. In the 0x4100 chunk are the vertices, face, and texture coordinates, as well as the face material information.

Before you worry about reading in this data, you need a place to store it. That is where the S3dsMesh structure comes in. Each S3dsMesh struc­ture holds one triangular mesh. Because the file does not reveal how many meshes there are total, you can call on the power of STL and std::vector to create a place to store any number of meshes.

Here is the mesh structure:

//—————————————————————————-

//- S3dsMesh //- Group Mesh Data struct S3dsMesh {

char m_cName[256];

};

The first variable (m_cName) is the mesh name. This should be set when you first enter the 0x4000 chunk, because that is where the name string is located. You need to be careful here. The string within the 3ds model is unrestricted; it can be any length. However, in your structure there is only 256 bytes of storage. Hopefully, this will be enough to hold any of the mesh names, but be sure to check first. If the length of the string in the file exceeds 256 characters, you will need to truncate it before storing.

The rest of the data is contained in sub-chunks of the 0x4100 chunk. (Yes, you can have sub-chunks of sub-chunks!) Because you do not know how many vertices, faces, or materials there will be, you may want to use a std::vector. This is essentially a resizable array. Using std::vectors, you can add as many objects to your array as you like without allocating or resizing it. If you aren’t sure about how to use std::vectors, I suggest you skip to Appendix B.

In the next section, you learn about the sub-chunks of the 0x4100 chunk. These chunks hold information about vertices, texture coordi­nates, faces, and materials. A particular object can have all of these sub-chunks, or just a few. An object doesn’t have to contain texture coordinate and material information.

Vertices 0x4110

The 0x4110 sub-chunk of the 0x4100 chunk contains all of the vertex information for the mesh. This includes the X, Y, and Z coordinates for each vertex. (Thankfully, it does not contain another level of sub­chunks.) The vertices are stored in the mesh structure in the m_vVertices vector array.

Each vector is simply three floating-point values, wrapped in a structure for clarity. Here is the very simple, fairly boring S3dsVertex structure. You may notice that the vertex class here contains three floats, rather than a CVector3 class like the MS3D vertices did. I chose to do this for a reason. Because the vertices do not need to be transformed like those from

The 3D5 MODEL5

MS3D, there is no point in burdening your program with the extra files. In this case, a simple array of three floats will do well.

/—————————————————————————

//- S3dsVertex

//- Vertex structure for 3ds models struct S3dsVertex {

float m_fVert[3];

};

The first two bytes inside the 0x4110 chunk dictate the number of vertices in the mesh. Immediately following these two bytes are many sets of floating-point triples—an X, Y, and Z position for each vertex, which are read and stored as S3dsVertex structures.

Now, if you would like to check your progress, you can use the render­ing function (C3ds::Render()) to render all the vertices as points. To do this, you must loop through the meshes one by one, and for each mesh, draw each vertex as a point in space.

Faces 0x4120

Another very important chunk is the 0x4120 chunk, or faces chunk. This chunk contains the vertex indexes for all the triangles in the current mesh; pretty important if you want a solid model. The faces are stored in the current mesh structure as well.

The S3dsFace structure is another fairly simple structure:

//—————————————————————————-

//- S3dsFace //- Face of a 3ds model struct S3dsFace {

unsigned short m_usIndices[3]; //Vertex indices CVector3 m_vecNormal; //Face Normal

};

Now, you have to get these face components out of the file and into memory. As with the vertices, the first two bytes of the 0x4120 chunk are dedicated to holding the number of faces for the mesh (in this case, three). After that, there are the vertex indexes. There are three indexes for each triangle, one for each corner. That means there are three times

the number of faces indexes. The m_vecNormal part of the face structure is not stored in the file; it must be calculated. This is done using the CalcFaceNormal function defined in model. h. This calculation will give you a unit vector perpendicular to the plane the triangle lies in. This value is required for lighting and materials when rendering.

The CalcFaceNormal function generates the face normal using the points of the triangle. First, two vectors are created from the points using an initial point of the first vertex and a final point as the second vertex for the first vector, and the third vertex for the second vector. The cross product of these vectors is then calculated. The resulting vector is perpendicular to the triangles, just like a normal vector. The only thing left to do is normalize the resulting vector so it can be sent to the rendering API.

These normals are only per face. To do smooth shading you will need per-vertex normals. A vertex normal can be calculated for a particular vertex by averaging the face normals of all of the faces that share that particular vertex.

You can now render the model as a solid object using just the vertex indexes. You can even send the normal vectors you calculated for each triangle and light the model as well.

Face Material Info 0x4130

Yet another level of sub-chunks. The face material information is a sub­chunk of the faces chunk (0x4120). One nice thing about the meshes in the 3ds files is that not only can you put different materials on differ­ent meshes like you could when using the MilkShape 3D format, but you can even put different materials on different parts of the same mesh. For example, a space ship made of a single mesh might need a different texture on the top of the ship than on the bottom. That’s what the 0x4130 chunk is all about.

The S3dsObjMat stores information that defines which faces are to be covered with which materials. The 0x4130 chunk contains only indexes into the material array; it does not store the material information.

Again, there is a vector of these structures in the mesh structure due to the fact that there could be more than one of them. The S3dsObjMesh structure contains an index into the materials array (which you will get to in a minute), as well as a list of face indexes that use this material.

};

The data in the 0x4130 chunk is in the following order:

1. First, you see a null-terminated string that contains the name of the material to use. You can compare this string with the names of the materials loaded from the material chunks (0xAFFF, de­fined next). When the string matches, you can set the m_uiMatIdx variable to the index of that material. This will save a lot of time during rendering because you will not have to search for the proper material.

2. The now-familiar two-byte integer that tells you the number of faces that use this material is up next.

3. The number of two-byte integers, each representing a face, is last. Each number is an index into the faces array. A “0” corre­sponds with face 0, a “1” with face 1, and so on.

Now, when rendering the model, you must first set the material properties of the first material, render all the faces that use that material, switch to the next material, and repeat the render­ing process until all the faces are drawn.

Texture Coordinates 0x4140 Chunk

The last important part of the mesh and the 0x4000 chunk is the 0x4140 sub-chunk. This sub-chunk holds the u and v texture mapping coordi­nates for each mesh vertex. These values position the texture on the triangle. They generally range from 0.0 to 1.0, but can go higher if the texture is to be repeated or tiled on the triangle. The first two bytes of the data give you the number of vertices. There are then two floating­point values for each vertex. These are stored in S3dsTexCoord structures,

which are similar to the S3dsVertex structures, but contain only two floats rather than three.

There, all the geometry is now loaded. A relief isn’t it? Wait, that is only half the battle. You still need to load all the material and texture information so you can make your models look much more interesting than a white, flat-shaded picture.

Materials 0xAFFF

To make your model more interesting and give it colors, highlights, and textures, you must load in the materials chunk. The materials chunk holds information about ambient, diffuse, and specular colors, as well as texture information and shininess. The mesh face chunks contain an index into this array of materials that specifies what materi­als are used for which model faces. The materials chunk is another conglomerate chunk, much like the 0x4000 mesh chunk. Even though it has lots of sub-chunks, I promise it’s not as bad as the mesh chunk.

Let’s take a look at the material structure right away:

//—————————————————————————-

//- S3dsMaterial //- Material structure struct S3dsMaterial {

char m_cName[256]; //Name of material float m_fAmbient[4]; //Ambient color float m_fDiffuse[4]; //Diffuse color float m_fSpecular[4]; //Specular color float m_fShininess; //Matl shininess CImage m_Texture; //Texturemap

};

If you look in the C3ds class, there is a vector of S3dsMaterial structures, the same way there is a vector of meshes. This means there can be more than one material in the 3ds file, so again, as with the meshes, be sure to keep track of which material you are on.

The 0xAFFF material chunk does not contain any data of its own, only sub-chunks, which are defined in the following sections.

Material Name 0xA000

The 0xA000 chunk contains the material’s name. This is just a null — terminated string, same as the mesh name. It can be copied into the m_cName variable of the current material. Again, be careful that your material name does not exceed 256 characters. If it does, you must cut it off before you store it or you will overwrite the array and risk crash­ing your program.

Ambient Color 0xA010

This chunk contains the ambient color of the current material. A sub­chunk 0x0011 contains the color in the form RGB, with one byte for each color value, for a total of three bytes. Before being stored in the m_fAmbient array, each element of the RGB color must be converted to a floating-point value between 0.0 and 1.0 so that they can be sent directly to the rendering API. This can be easily done using the for­mula (255 — R)/255, where R is the value for the red, green, or blue component of the color. The fourth value is always set to 1.0f in this case, because the value is not specified otherwise. The fourth value is included in the array so that it can be sent directly to the renderer, such as OpenGL. The following pseudo-code reads the ambient color, as well as the specular and diffuse colors.

//pseudocode to read colors for materials

read in three bytes of information, one for each red, green, and blue convert each value to a floating point value between 0 and 1 store the new values in the first three appropriate array (m_fAmbient for ambient color)

set the fourth value of the array to 1.0 so that the whole array can be sent to the rendering API

Diffuse 0xA020 and Specular 0xA030 Colors

The diffuse and specular colors work the same way as the ambient color, except they are stored in their own variables. Both have the same type of color chunks and values.

Shininess 0xA040 and Transparency 0xA050

A percentage sub-chunk gives these two values. The sub-chunk 0x0030 contains a single two-byte integer that ranges from 0 to 100

(the percentage). Dividing by 100 returns the floating-point value to between 0 and 1. The shininess percentage (0 percent being dull, 100 percent being as shiny as possible) goes into the m_fShininess variable; the transparency is the fourth value in the m_fDiffuse array.

//pseudo code to read shininess and transparency Read shininess chunk

Convert shininess to a value between 0 and 1 by dividing by 100 Set shininess parameter equal this value Read the transparency value

Convert shininess into a value from 0 to 1 in the same way as transparency Set the fourth value in the diffuse color array equal to the resulting value

Texture Map 0xA200 and DxA3DD

The last and probably the most important chunk of the material is the texture. The texture is the “skin” that covers the model and contains details too difficult to create only with triangles and colors. This gives the model a definite look, enhancing believability and realism.

The texture map structure starts with a chunk of type 0xA200. This 0xA200 or texture map chunk contains sub-chunks as well, although the only sub-chunk one you need is 0xA300. It contains a null-terminated string that specifies the texture’s filename. This string can be fed directly into the Load function of the CImage class contained within the material structure. If you are not using the CImage class included in the basecode, you can extract this string with strcpy and store it in a temporary buffer to pass to your own image-loading functions.

//Reading and loading the texture filename

Set a pointer (char *) equal to the start of the data in the 0xA300 chunk.

Pass this pointer to the CImage::Load() function.

Whew, you now have all the static data you need to at least render the model with light and texture.

Rendering Your 3ds Files

Let’s take a look at how to pull all the data you have loaded together and put it onto the screen. You must deal with many issues while

rendering. Some meshes have material information stored in them; some meshes have no materials at all. Some meshes might not contain any triangles, due to the fact they are made for cameras and lights. You must consider and deal with all of these circumstances.

The best way to visualize how these files are rendered is to look at a flow chart. The rendering flow is shown in Figure 7.3. Take a look at it before you read on.

The 3D5 MODEL5

Figure 7.3 Rendering flowchart.

Your rendering function must contain a loop that loops through each of the meshes and draws them if necessary.

The first thing you must determine is whether the current mesh contains any face information. This can be done by checking the size of the face array. If it is 0, just skip the mesh altogether and continue.

If it contains no triangles, it is not a valid triangle-mesh.

You must next determine how different parts of the model are ren­dered. If there is information in the m_vObjMat variable, the mesh contains material information and it must be set up first. If this branch is taken, a new loop must be set up to loop through all of the ObjMat objects so the proper material is applied to the proper faces.

Then, using the material index, you must set up the material proper­ties. Make sure to enable blending, lighting, and to bind the texture as well. If you are using OpenGL, the materials are specified using the glMaterialf function, and blending is enabled using the glEnable function with the parameter GL_BLEND. Make sure to set your blending function using this code:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

As for the texture, you can use the CImage::Bind() function if you are using the CImage class, or glBindTexture if you are using another image- loading routine.

Then, you can render the faces specified by the current material information.

If there are no materials for the specified object, you can now render the faces. Because leaving blending enabled can cause undefined results, it is best to disable it when you take this route. You should also disable texturing as well.

In the C3ds::Render function, I use immediate mode to send vertex, texture coordinates, and normal information to OpenGL. There are better ways to do this; for instance using vertex arrays. I decided to leave it this way to make it easier to understand, particularly if you do not know OpenGL well.

& J

Conclusion

This chapter should contain plenty of information to get you going. You can now load and render a 3ds model, complete with textures and materials. You also have been introduced to a format that uses a system of independent chunks to create the file. Now you should be able to look at the full 3ds format, which can be obtained from a site such as https://www. wotsit. org, and be able to load and utilize any of the extra chunks stored in the file that were not covered here. The 3ds format stores a ton of information and holds endless possibilities. The next chapter shows you how to create a loader for Half-Life’s MDL format using basecode released by VALVe software. That chapter uses existing basecode rather than creating an entire loader from scratch due to the complexity of the format. Read on to find out how to load this popu­lar, powerful format.

This page intentionally left blank

Для любых предложений по сайту: [email protected]