1. Overview of 3D Models
Some
games have their own file formats that are used for storing 3D
information. This allows the developers to create a format that has all
of the information needed by a particular gaming application. What this
information is may vary from game to game, so it really depends on the
application.
For example, if you have models in your game that
require the position, texture coordinates, normals, and material
information, you can create a file format that suits your needs by
specifying this information. The game can then load this data and render
the geometry with its material in real time.
Tools Used for Creating 3D Geometry
Another option is to utilize an already existing
file format. This is convenient because any tools available on the
market that save information to the format of your choice can be used to
create game assets. If you use an existing file format, it might not
suit your needs, or it might specify more information than you need for
your assets. This is often the case with formats saved by
general-purpose tools, where a lot of information can appear in the file
that you would not need in an actual game. Some of the most popular
applications used to create 3D geometry include the following.
3D Studio Max
Lightwave
XSI
Maya
ZBrush
Truespace
Along with using existing tools and
formats, you can also write your own file exporters and converters to
change a file to a format that your game is ready to use. This is a very
common practice in video games, as it allows developers to use powerful
and complex tools such as 3D Studio Max and save the content created in
a format the game is able to read and use.
2. Files in C++
In this section we will briefly review how C++ loads file data. C++ uses the standard ifstream and ofstream classes. The ifstream class represents an input file stream, and ofstream represents an output file stream. Both classes derive from the base class, ifstream, and are part of the C++ standard.
Input and Output Streams
The data is loaded into the demo and displayed within the function LoadFileData(). This function creates an ifstream
object, opens the file, checks if the file actually opened, determines
the size of the file, and then reads the file’s data into memory. Once
loaded, the text that was loaded from the file is displayed, the
allocated memory that was used to store the file’s data is deleted, and
the function returns.
To open a file using ifstream or ofstream, you call the open() function. This function takes as parameters the file name and the file mode. The file mode can be one of the following flags:
app: This flag tells the stream object to open a file and append new information to it.
ate: This flag sets the file pointer to the end of the file stream upon opening it.
binary: This flag indicates that the file being opened is considered a binary file and not a text file.
in: This flag is used to specify that the file is being used for reading rather than writing.
out: This flag specifies that the file is being used for writing rather than reading.
trunc: This flag discards data upon opening.
To check if the file successfully opened, you can use the function is_open(), which returns true if the file stream is open or false if it is not.
In
the Files demo the next step calculates the file size. This can be done
by setting the file pointer to the end of the file stream by calling
the function seekg(), calling tellg() to get the number of bytes at that position, and then calling seekg() again to return us to the beginning of the file stream so that we are ready to read the information from the beginning. The seekg()
function takes as parameters the file position to set, an offset from
that position to set, and a seeking direction. In this demo we are using
one of the overloaded versions that accepts an offset and a seek
direction. The seek direction can be either beg, which stands for the beginning of the stream, end, which is the end of the stream, or cur, which is the current position in the stream. The function tellg()
gets the file position, which can be used to represent the byte
position at the current location. So if we seek to the end of the file, a
call to tellg() will tell us the total bytes in the file. We
seek back to the start so we can begin reading since reading occurs
where the file pointer is located.
With the size of the file, we can allocate a buffer to hold that information, and then we can read it with a call to read(). The read()
function takes as parameters a pointer to a buffer to read the data
into and the amount you want to read. To read the entire file we use the
file’s size from the beginning of the stream in this demo. Once the
information is read, the file is closed, the allocated memory is
deleted, and the function returns.
Other functions that are part of the ifstream class include the following that are inherited from the istream class.
gcount(): This function returns the number of characters extracted by the last input operation.
get(): This function is used to read unformatted data from the input file stream.
getline(): This function is used to read data from the input stream into an array.
ignore(): This function is used to read data from the stream and then discard it.
peek(): This function returns the next character in the stream but does not extract it (i.e., doesn’t move the file pointer).
readsome():
This function reads data up to the size of the array even if the end of
the file or the number of bytes to read has yet to be reached.
putback():
This function decrements the file pointer back one and makes the
character passed to its parameter the next character to be read from the
stream.
unget(): This function decrements the file pointer back one.
sync(): This function synchronizes the input buffer with a source of characters.
sentry(): This function performs exception-safe prefix and suffix operations on the stream.
The ofstream class has many of the same functions minus the ones dealing with input. The ofstream class also has a function called write() that is used to write data to a file. The write() function takes as parameters the buffer to write and the amount of bytes to write. The ofstream class also has a function called flush(), which is used to force the object to write out all unwritten data as soon as possible.
The main.cpp source file for the Files demo is shown in Listing 1. The demo’s main() function saves data to a file by calling the demo’s SaveFileData() function, and then it reads it back by calling the demo’s LoadFileData() function. The data that has been read is displayed inside LoadFileData().
Listing 1. The main.cpp Source File for the Files Demo
/*
Files in C++
Ultimate Game Programming with DirectX 2nd Edition
Created by Allen Sherrod
*/
#include<iostream>
#include<fstream>
using namespace std;
bool LoadFileData()
{
ifstream fileStream;
int fileSize = 0;
// Open file then test that it actually opened.
fileStream.open("test.txt", ifstream::in);
if(fileStream.is_open() == false)
return false;
// Get file size.
fileStream.seekg(0, ios::end);
fileSize = fileStream.tellg();
fileStream.seekg(0, ios::beg);
if(fileSize <= 0)
return false;
// Allocate memory for text data.
char *buffer = new char[fileSize];
memset(buffer, 0, fileSize);
if(buffer == NULL)
return false;
// Read data and close the file.
fileStream.read(buffer, fileSize);
fileStream.close();
buffer[fileSize - 1] = '\0';
// Display the data.
cout << "test.txt (" << fileSize << " bytes) contents:" << endl;
cout << buffer << endl;
delete[] buffer;
return true;
}
bool SaveFileData()
{
ofstream fileStream;
// Open file then test that it actually opened.
fileStream.open("test.txt", ofstream::out);
if(fileStream.is_open() == false)
return false;
char buffer[] = { "This is saved out to the file!" };
// Write information to the file then close the file.
fileStream.write(buffer, sizeof(buffer));
fileStream.close();
return true;
}
int main(int args, char *argc[])
{
cout << "Loading files example in C++." << endl << endl;
// Try to save the file.
if(!SaveFileData())
{
cout << "Could not save file!" << endl << endl;
}
// Try to load the file.
if(!LoadFileData())
{
cout << "Could not read file!" << endl << endl;
}
cout << "Press enter to quit." << endl;
char c;
cin >> c;
return 1;
}
|
Binary Files and Byte Ordering
Loading
text files consists of loading data in a stream of characters, where a
character is a single byte. This assumes the file has ASCII text, which
essentially means there are no values that take up more than a byte of
space. Binary files, on the other hand, or any file that assumes more
than one byte per value, can have multibyte values saved to the file.
Therefore, if an integer variable is 4 bytes in C++, you can save the
entire variable to a file. To read it you would read the 4 bytes that
make up the integer.
The problem with multibyte values is in their
byte ordering. Different hardware works on different byte ordering. For
example, big endian is the byte ordering used by PowerPC processors,
while little endian is used by processors. When saving information to a
file, the byte ordering from one piece of hardware is not translated
automatically to another. This means that if you try to load a value
that is in big endian on a little endian machine, the value will not be
what you expect.
The solution to this problem is fairly
straightforward. To start, you have to be aware of what byte ordering
the machine uses and what order the file was saved in. If the two orders
are different, you can simply swap the bytes that make up the variable
when you read it. This can be done by simply casting the variable to a
character pointer and swapping the bytes using array indexes just like
you would if you had an array of four characters. When swapping, the
fourth byte becomes the first, the third becomes the second, the second
becomes the third, and the first byte becomes the last.