ff7MapToObjNote: This program uses info from Ficedula's World Map Viewer (Reeve):
http://forums.qhimm.com/index.php?topic=6057.0I wrote this little program so that I could render a heightmap of the ff7 world map.
Seeing as how this is my first c++ release ever, don't expect it to be fast or efficient. In fact, since it makes external calls to LZSCDec.exe (because I didn't write my own decompressor), expect it to run slow. Also, I don't guarantee that it works on all OS's due to variability of int sizes, but it probably does.
Instructions (sorry if its not as easy as plug and play, but it is not legal to bundle the textures):
1) Extract the .rar from:
http://www.mediafire.com/?c76sb586q66ee0q into your data/wm directory:
2) Use Biturn:
http://mirex.mypage.sk/FILES/bi087b4.rar to extract the textures from data/wm/world_us.lgp. You MUST extract ALL textures as .TGA files AND place them in a new folder: data/wm/textures. You can select multiple textures at once and extract all at once. No need to do it one by one.
The program will read wm0.map, wm2.map, and wm3.map and output wm0.obj, wm2.obj, and wm3.obj. It uses NFITC1's LZS decompressor.
Notes: The textures for wm0 are 90% accurate, but should be exactly the same as Reeve's due to some unknown data (read thread). The textures for wm2 and wm3 are mapped differently, so are totally incorrect. There is one small hole in the wm2.map (underwater) mesh that I am pretty sure was part of the original game and not caused by me. The .obj files are rather large (wm0.obj is 20mb) so expect it to act up unless you've got a good 3d program and adequate system memory.
Downloads:
ff7MapToObj:
http://www.mediafire.com/?c76sb586q66ee0qBiturn (required):
http://mirex.mypage.sk/FILES/bi087b4.rarNFITC1's LZS decompressor (required, bundled with permission):
http://www.mediafire.com/?hwyyk2zjjjjSee my Fallout 3 WIPs at the MDMD forums:
http://z9.invisionfree.com/industrialpolygons/index.php?showtopic=562Screenshots (click to enlarge):
World Map (wm0) textured mesh
World Map (wm0) mesh
Underwater Map (wm2) mesh -- Note that if you superimpose it in the central ocean of wm0, it drops in nicely.
There is a deep chasm that follows the path from Junon to Costa del Sol
and there is a channel that goes through the western continent to Lucrecia's cave.
Snow Map (wm3) mesh
Source (Visual C++ 2010):
/*
ff7MapToObj
by Omzy ([email protected]) 09/05/2010
[REL] thread at: http://forums.qhimm.com/index.php?topic=10717.0
See my WIP at the MDMD forums: http://z9.invisionfree.com/industrialpolygons/index.php?showtopic=562
Description: Converts a final fantasy 7 .map file to an .obj file that can be opened in various 3d rendering programs.
Requires: lzscdec.exe (by NFITC1) and .map files (wm0, wm2, wm3 from data/wm) must be in same directory.
Download lzscdec.exe from http://www.mediafire.com/?hwyyk2zjjjj
Requires: textures directory with .tga's from world_us.lgp in data/wm.
For documentation on .map format, see http://wiki.qhimm.com/FF7/WorldMap_Module
For documentation on .lsz compression, see http://wiki.qhimm.com/FF7/LZS_format
*/
#include <iostream>
#include <fstream>
#include <stdlib.h>
#include <string>
using namespace std;
string getMtl (short mtlIndex) //mtlIndex is 9 bit value that represents texture id
{
//mtlcodes.txt and mtlnames.txt are in separate files to make texture swapping easier
char * mtlCode = new char[2]; //mtlcode is 2 alpha characters
ifstream mtlcodefile ("mtlcodes.txt", ios::in); //Codes match material names in mtlnames.txt
if (mtlcodefile.is_open())
{
int count(0); //This loop gets the code at line mtlIndex
mtlcodefile.seekg(0, ios::beg);
while (!mtlcodefile.eof() && mtlcodefile.good() && count < mtlIndex)
{
mtlcodefile.get(mtlCode, 3);
mtlcodefile.ignore(100, '\n');
count++;
}
}
else cout << "Unable to open" << "mtlcodes.txt" << endl;
mtlcodefile.close();
char * curCode = new char[2]; //Current code in file
string mtlName = "";
ifstream mtlnamefile ("mtlnames.txt", ios::in); //Names match material codes in mtlcodes.txt
if (mtlnamefile.is_open())
{
mtlnamefile.seekg(0, ios::beg); //This loop gets the name at the line starting with mtlCode
while (!mtlnamefile.eof() && mtlnamefile.good())
{
mtlnamefile.get(curCode, 3);
if (mtlCode[0] == curCode[0] && mtlCode[1] == curCode[1])
{
mtlnamefile.seekg(0x0001, ios::cur); //Skip '=' in file
char * temp = new char[1];
while (*temp != '\0')
{
mtlnamefile.get(temp, 2);
mtlName.append(temp);
}
break;
}
else
{
mtlnamefile.ignore(100, '\n');
}
}
}
else cout << "Unable to open" << "mtlnames.txt" << endl;
mtlnamefile.close();
//cout << mtlIndex << " " << mtlCode << " " << mtlName << endl;
return mtlName;
}
char * getTexDims (string mtlName)
{
char * texDim = new char[4];
//Open .tga file to check dimensions given mtlName
ifstream mtltexfile ("textures\\" + mtlName + ".tga", ios::in);
if (mtltexfile.is_open())
{
mtltexfile.seekg(0x000C, ios::beg);
mtltexfile.read(texDim, 0x0004);
}
else
{
cout << "Unable to open " << mtlName << ".tga" << endl;
}
mtltexfile.close();
return texDim;
}
void mapToObj (string filename, int numblocks, int dimension)
{
ifstream mapfile (filename + ".map", ios::in|ios::binary); //Input file
ofstream objfile (filename + ".obj", ios::out); //Output file
int totalVertices = 0;
int totalVTs = 0;
if (mapfile.is_open() && objfile.is_open())
{
objfile << "mtllib tgalib.mtl" << endl;
int * meshloc = new int;
int * lzssize = new int;
for (int i = 0; i < numblocks; i++) //There are 69 Blocks in wm0.map, ignores last 6 blocks (see below)
{
//Calculate north-south block (nsblock) and east-west (ewblock) offsets of block (7x9 grid of blocks = 63)
//Block size is 8192*4 = 32768x32768 coordinate units
//Note that blocks 63, 64, 65, 66, 67 and 68 replace blocks 50, 41, 42, 60, 47 and 48 (respectively), according to the story of the game.
int ewblock = i % dimension; //take mod to get 0-8
int nsblock = floor ((float)(i / dimension)); //take floor to get 0-6
//cout << i << " " << ewblock << " " << nsblock << endl;
for (int j = 0; j < 16; j++) //There are 16 meshes in each block
{
//Calculate north-south (ns) and east-west (ew) offsets of mesh (4x4 grid of meshes = 16)
//Mesh size is 8192x8192 coordinate units
int ew = j % 4; //take mod to get 0-3
int ns = floor ((float)(j / 4)); //take floor to get 0-3
int pos = i * 0xB800 + j * 0x0004;
mapfile.seekg (pos, ios::beg); //Block i, Mesh j
mapfile.read ((char *) meshloc, 0x0004); //Read location of mesh (pointer is 4 bytes)
pos = i * 0xB800 + *meshloc;
mapfile.seekg (pos, ios::beg); //Mesh LZS starts here
mapfile.read ((char *) lzssize, 0x0004); //Size is 1st 4 bytes
mapfile.seekg (-0x0004, ios::cur); //Go back 4 bytes because .lzs file includes size
char * memblock = new char [*lzssize + 0x0004];
mapfile.read (memblock, *lzssize + 0x0004); //Read the compressed mesh to memory (don't forget to read the size header)
ofstream lzsfile ("mesh.lzs", ios::out|ios::binary); //Write compressed mesh to mesh.lzs file
if (lzsfile.is_open())
{
lzsfile.write (memblock, *lzssize + 0x0004);
}
lzsfile.close();
//system("lzs.exe -d -q mesh.lzs"); //Decompress mesh with Ficedula's LZS decompressor
system("lzscdec.exe mesh.lzs D mesh.dec"); //Decompress mesh with NFITC1's LZS decompressor
remove ("mesh.lzs");
fstream::pos_type meshsize; //Copy data from mesh.dec to memory
ifstream meshfile ("mesh.dec", ios::in|ios::binary|ios::ate);
if (meshfile.is_open())
{
meshsize = meshfile.tellg();
delete[] memblock;
memblock = new char [meshsize];
meshfile.seekg (0, ios::beg);
meshfile.read (memblock, meshsize); //Read size header? Just need #triangles & #vertices
}
meshfile.close();
remove ("mesh.dec");
//Here we add the mesh in memblock to the .obj file
//May need to add offsets for block # because map is 9x7 grid of 63 blocks.
unsigned char * mem_ptr;
unsigned short numTriangles, numVertices;
mem_ptr = (unsigned char *)(memblock);
numTriangles = *mem_ptr;
numTriangles += *(mem_ptr + 0x0001) << 8;
mem_ptr = (unsigned char *)(memblock + 0x0002);
numVertices = *mem_ptr;
numVertices += *(mem_ptr + 0x0001) << 8;
//cout << numTriangles << " " << numVertices << endl;
for (int f = 0; f < numTriangles; f++) //Triangle (face) data. Each triangle consists of 12 bytes. The first 3 are vertex indices.
{
pos = (int) (memblock + 0x0004 + 0x000C * f);
mem_ptr = (unsigned char *)(pos);
unsigned char v0 = *mem_ptr;
mem_ptr += 0x0001;
unsigned char v1 = *mem_ptr;
mem_ptr += 0x0001;
unsigned char v2 = *mem_ptr;
mem_ptr += 0x0002;
unsigned char u0 = *mem_ptr;
mem_ptr += 0x0001;
unsigned char vt0 = *mem_ptr;
mem_ptr += 0x0001;
unsigned char u1 = *mem_ptr;
mem_ptr += 0x0001;
unsigned char vt1 = *mem_ptr;
mem_ptr += 0x0001;
unsigned char u2 = *mem_ptr;
mem_ptr += 0x0001;
unsigned char vt2 = *mem_ptr;
mem_ptr += 0x0001;
unsigned short mtlIndex = *mem_ptr; //Texture is 2 bytes read lower then upper, but only lower 9 bits
mtlIndex += *(mem_ptr + 0x0001) << 8; //For that reason we throw out the top 7 bits
//Debug
//char upper7 = mtlIndex >> 9; //Upper 7 bits (unknown)
//Debug
mtlIndex = mtlIndex << 7;
mtlIndex = mtlIndex >> 7;
mtlIndex++;
//cout << getMtl (mtlIndex) << endl; //Material name
string mtlName = getMtl(mtlIndex);
objfile << "usemtl " << mtlName << endl; //Use texture from tgalib.mtl
objfile << "f " << (int) v0 + totalVertices + 1 << "/" << totalVTs + 1 << "/" << (int) v0 + totalVertices + 1 << " "; //face line format:
objfile << (int) v1 + totalVertices + 1 << "/" << totalVTs + 2 << "/" << (int) v1 + totalVertices + 1 << " "; //f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
objfile << (int) v2 + totalVertices + 1 << "/" << totalVTs + 3 << "/" << (int) v2 + totalVertices + 1 << endl; //Vertex and Vertex/Normal values are same
//Easier to write the UVs here since they are considered part of a triangle (face) in the .map file. Obj file doesn't care where they are, just their order
//Get texture dimensions given mtlName
char * texDim = new char[4];
texDim = getTexDims (mtlName);
short texWidth = (texDim[1] << 8) + texDim[0];
if (texWidth < 0) texWidth *= -1;
short texHeight = (texDim[3] << 8) + texDim[2];
if (texHeight < 0) texHeight *= -1;
//cout << texWidth << " " << texHeight << endl;
objfile << "vt " << (float)((float) u0 / texWidth) << " " << (float)((float) vt0 * -1 / texHeight) << endl; //Note that V's are flipped
objfile << "vt " << (float)((float) u1 / texWidth) << " " << (float)((float) vt1 * -1 / texHeight) << endl; //All UVs must be divided by texheight/width for normalization
objfile << "vt " << (float)((float) u2 / texWidth) << " " << (float)((float) vt2 * -1 / texHeight) << endl;
totalVTs += 3;
}
for (int v = 0; v < numVertices; v++) //Vertex data. Each vertex consists of 8 bytes. The first 6 are coordinates (2 each).
{
pos = (int) (memblock + 0x0004 + 0x000C * numTriangles + 0x0008 * v);
mem_ptr = (unsigned char *)(pos);
short x = *mem_ptr;
x += *(mem_ptr + 0x0001) << 8;
mem_ptr += 0x0002;
short y = *mem_ptr;
y += *(mem_ptr + 0x0001) << 8;
mem_ptr += 0x0002;
short z = *mem_ptr;
z += *(mem_ptr + 0x0001) << 8;
objfile << "v " << (int) x + ewblock * 32768 + ew * 8192 << " " << (int) y << " " << (int) z + nsblock * 32768 + ns * 8192 << endl;
}
for (int vn = 0; vn < numVertices; vn++) //Vertex/Normal data. Each vertex/normal consists of 8 bytes. The first 6 are coordinates (2 each).
{
pos = (int) (memblock + 0x0004 + 0x000C * numTriangles + 0x0008 * numVertices + 0x0008 * vn);
mem_ptr = (unsigned char *)(pos);
short x = *mem_ptr;
x += *(mem_ptr + 0x0001) << 8;
mem_ptr += 0x0002;
short y = *mem_ptr;
y += *(mem_ptr + 0x0001) << 8;
mem_ptr += 0x0002;
short z = *mem_ptr;
z += *(mem_ptr + 0x0001) << 8;
objfile << "vn " << (int) x + ewblock * 32768 + ew * 8192 << " " << (int) y << " " << (int) z + nsblock * 32768 + ns * 8192 << endl;
}
totalVertices += numVertices;
//Mesh added to .obj file.
delete[] memblock;
//Report progress
system("cls");
cout << "ff7MapToObj by Omzy" << endl << endl;
cout << "Converting " << filename << ".map..." << endl;
cout << "Progress: Block: " << i + 1 << "/" << numblocks << " Mesh: " << j + 1 << "/16" << endl;
}
}
delete[] meshloc;
delete[] lzssize;
}
else cout << "Unable to open" << filename << ".map" << endl;
mapfile.close();
objfile.close();
return;
}
int main () {
mapToObj ("wm0", 63, 9);
mapToObj ("wm2", 12, 3);
mapToObj ("wm3", 4, 2);
system("Pause");
return 0;
}