Tools for preprocessing data files from Quake to make them suitable for use on PS1 hardware
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

610 lines
19 KiB

#include "common.h"
#include "bsp.h"
#include "ps1types.h"
#include "ps1bsp.h"
#include "lighting.h"
#include "texture.h"
#include "tesselate.h"
static char path[_MAX_PATH];
template<class TData> size_t writeMapData(const std::vector<TData>& data, ps1bsp_dentry_t& dentry, FILE* f)
{
dentry.offset = (unsigned int)ftell(f);
dentry.size = sizeof(TData) * data.size();
return fwrite(data.data(), sizeof(TData), data.size(), f);
}
static float computeFaceArea(const world_t* world, const face_t* face)
{
const plane_t* plane = &world->planes[face->plane_id];
// Construct a tangent and bitangent for the plane
Vec3 tangent, bitangent;
plane->getTangents(tangent, bitangent);
// Project all face vertices onto the face's plane
BoundBox bounds;
for (int edgeListIdx = 0; edgeListIdx < face->ledge_num; ++edgeListIdx)
{
int edgeIdx = world->edgeList[face->ledge_id + edgeListIdx];
int vertIndex = edgeIdx > 0 ?
world->edges[edgeIdx].vertex0 :
world->edges[-edgeIdx].vertex1;
const vertex_t* vertex = &world->vertices[vertIndex];
Vec3 vec = vertex->toVec();
double x = tangent.dotProduct(vec);
double y = bitangent.dotProduct(vec);
bounds.includePoint(Vec3(x, y, 0.0));
}
Vec3 extents = bounds.max - bounds.min;
return extents.x * extents.y;
}
typedef std::unordered_map<const face_t*, std::vector<Tesselator::Polygon>> FacePolygons;
int process_faces(const world_t* world, const TextureList& textures)
{
// Write some data to a file
FILE* fbsp;
fopen_s(&fbsp, "test.ps1bsp", "wb");
if (!fbsp)
return 0;
ps1bsp_header_t outHeader = { 0 }; // Write an empty placeholder header first
outHeader.version = 1;
fwrite(&outHeader, sizeof(ps1bsp_header_t), 1, fbsp);
Tesselator tesselator(world);
// Convert faces defined by edges into faces defined by vertex indices
std::vector<ps1bsp_face_t> outFaces;
std::vector<ps1bsp_facevertex_t> outFaceVertices;
FaceBounds faceBounds;
for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx)
{
const face_t* face = &world->faces[faceIdx];
const texinfo_t* texinfo = &world->texInfos[face->texinfo_id];
const miptex_t* miptex = &world->miptexes[texinfo->texture_id];
const plane_t* plane = &world->planes[face->plane_id];
ps1bsp_face_t outFace = { 0 };
outFace.planeId = face->plane_id;
outFace.side = (unsigned char)face->side;
outFace.firstFaceVertex = (unsigned short)outFaceVertices.size();
outFace.textureId = (unsigned char)texinfo->texture_id;
Matrix4x4 textureTrsf = tesselator.buildTextureSpaceTransform(texinfo, miptex, plane);
// Traverse the list of face edges to collect all of the face's vertices
Vec3 vertexSum;
FaceBound bounds;
for (int edgeListIdx = 0; edgeListIdx < face->ledge_num; ++edgeListIdx)
{
int edgeIdx = world->edgeList[face->ledge_id + edgeListIdx];
unsigned short vertIndex = edgeIdx > 0 ?
world->edges[edgeIdx].vertex0 :
world->edges[-edgeIdx].vertex1;
const vertex_t* vertex = &world->vertices[vertIndex];
Vec3 vertexPoint = vertex->toVec();
// Calculate bounding box of this face
if (edgeListIdx == 0)
bounds.worldBounds.init(vertexPoint);
else
bounds.worldBounds.includePoint(vertexPoint);
Vec3 texturePoint = textureTrsf.TransformPoint(vertexPoint);
bounds.addTexturePoint(texturePoint.x, texturePoint.y);
// Sum all vertices to calculate an average center point
vertexSum = vertexSum + vertexPoint;
ps1bsp_facevertex_t faceVertex = { 0 };
faceVertex.index = (unsigned short)tesselator.addVertex(vertexPoint);
outFaceVertices.push_back(faceVertex);
}
faceBounds[face] = bounds;
// For visualizing and debugging lightmaps
//if (face->ledge_num >= 10)
// export_lightmap(world, face, bounds, faceIdx);
outFace.numFaceVertices = (unsigned char)(outFaceVertices.size() - outFace.firstFaceVertex);
outFace.center = (vertexSum / face->ledge_num).convertWorldPosition();
float area = computeFaceArea(world, face); // TODO: divide by number of polygons
outFace.center.pad = (short)(sqrt(area));
outFaces.push_back(outFace);
}
std::vector<ps1bsp_polyvertex_t> outPolyVertices;
std::vector<ps1bsp_polygon_t> outPolygons;
// Iterate over all faces again; now that we know the bounds of each face, we can calculate lighting for all of them
for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx)
{
face_t* face = &world->faces[faceIdx];
ps1bsp_face_t* outFace = &outFaces[faceIdx];
const texinfo_t* texinfo = &world->texInfos[face->texinfo_id];
const miptex_t* miptex = &world->miptexes[texinfo->texture_id];
const auto& texture = textures[texinfo->texture_id];
// Skip over invisible collision volumes entirely
if (!strcmp(miptex->name, "clip") || !strcmp(miptex->name, "trigger"))
{
outFace->firstPolygon = 0;
outFace->numPolygons = 0;
continue;
}
// Detect special surface types
if (!strncmp(miptex->name, "sky", 3))
{
outFace->flags |= SURF_DRAWSKY;
}
else if (miptex->name[0] == '*')
{
// Certain liquid types should not be transparent
if (strncmp(miptex->name, "*lava", 5) && strncmp(miptex->name, "*slime", 6) && strncmp(miptex->name, "*tele", 5))
outFace->flags |= SURF_DRAWWATER;
// Draw liquids as fullbright transparent surfaces
outFace->flags |= SURF_DRAWLIQUID;
}
// Calculate average face lighting * color from texture data
for (int faceVertIdx = 0; faceVertIdx < outFace->numFaceVertices; ++faceVertIdx)
{
auto& faceVert = outFaceVertices[outFace->firstFaceVertex + faceVertIdx];
Vec3 point = tesselator.getVertices()[faceVert.index];
unsigned char light;
if (outFace->flags & SURF_DRAWSKY || outFace->flags & SURF_DRAWLIQUID || face->lightmap < 0)
{
light = 255;
}
else
{
light = compute_faceVertex_light5(world, face, faceBounds, point);
light = (int)((float)light * 1.5f); // Compromise between overbright and non-overbright lighting. Looks good in practice.
if (light > 255)
light = 255;
}
// Apply lighting to each color channel, and keep track of any overflows
int col[3], maxCol = 0;
for (int i = 0; i < 3; ++i)
{
col[i] = texture.dominantColor.channel[i] * light / 255;
if (col[i] > maxCol)
maxCol = col[i];
}
if (maxCol > 255)
{
// Saturate color to the highest channel value
for (int i = 0; i < 3; ++i)
{
col[i] = 255 * col[i] / maxCol;
}
}
// Boost the color value by 2, to simulate the PS1's overbright vertex color modulation
faceVert.r = (unsigned char)(col[0] >> 2);
faceVert.g = (unsigned char)(col[1] >> 2);
faceVert.b = (unsigned char)(col[2] >> 2);
faceVert.a = 0;
}
// Sky surfaces do not need to be tesselated
if (outFace->flags & SURF_DRAWSKY)
{
outFace->firstPolygon = 0;
outFace->numPolygons = 0;
continue;
}
// Tesselate the face into smaller polygons based on repeating textures
outFace->firstPolygon = (unsigned short)outPolygons.size();
auto polygons = tesselator.tesselateFace(face);
for (auto polyIter = polygons.begin(); polyIter != polygons.end(); ++polyIter)
{
ps1bsp_polygon_t outPoly = { 0 };
outPoly.firstPolyVertex = (unsigned short)outPolyVertices.size();
for (auto polyVertIter = polyIter->polyVertices.begin(); polyVertIter != polyIter->polyVertices.end(); ++polyVertIter)
{
size_t vertIndex = polyVertIter->vertexIndex;
Vec3 normalizedUV = polyVertIter->normalizedUV;
ps1bsp_polyvertex_t polyVert = { 0 };
polyVert.index = (unsigned short)vertIndex;
polyVert.u = (unsigned char)(normalizedUV.x * (texture.w - 1)) + texture.uoffs;
polyVert.v = (unsigned char)(normalizedUV.y * (texture.h - 1)) + texture.voffs;
Vec3 vertex = tesselator.getVertices()[vertIndex];
int light = compute_faceVertex_light5(world, face, faceBounds, vertex);
light = (int)((float)light * 1.5f); // Compromise between overbright and non-overbright lighting. Looks good in practice.
if (light > 255)
light = 255;
polyVert.light = (unsigned short)light;
outPolyVertices.push_back(polyVert);
}
outPoly.numPolyVertices = (unsigned short)(outPolyVertices.size() - outPoly.firstPolyVertex);
outPolygons.push_back(outPoly);
outFace->totalQuads += (outPoly.numPolyVertices - 1) / 2;
}
outFace->numPolygons = (unsigned char)(outPolygons.size() - outFace->firstPolygon);
}
// Convert vertex data
const auto& inVertices = tesselator.getVertices();
std::vector<ps1bsp_vertex_t> outVertices;
for (auto vertIter = inVertices.begin(); vertIter != inVertices.end(); ++vertIter)
{
const Vec3& inVertex = *vertIter;
ps1bsp_vertex_t outVertex = { 0 };
// Ensure we don't overflow 16-bit short values. Most Quake maps will stay within these bounds so it *should* be fine (for now).
if (inVertex.x > -8192 && inVertex.x < 8192 && inVertex.y > -8192 && inVertex.y < 8192 && inVertex.z > -8192 && inVertex.z < 8192)
{
outVertex.x = (short)(inVertex.x * WORLDSCALE);
outVertex.y = (short)(inVertex.y * WORLDSCALE);
outVertex.z = (short)(inVertex.z * WORLDSCALE);
}
else
{
printf("Error: vertices found outside of acceptable range: (%f, %f, %f)\n", inVertex.x, inVertex.y, inVertex.z);
fclose(fbsp);
return 0;
}
outVertices.push_back(outVertex);
}
// Copy PS1 texture data
std::vector<ps1bsp_texture_t> outTextures;
for (auto texIter = textures.begin(); texIter != textures.end(); ++texIter)
{
outTextures.push_back(texIter->ps1tex);
}
// Convert planes
std::vector<ps1bsp_plane_t> outPlanes;
for (int planeIdx = 0; planeIdx < world->numPlanes; ++planeIdx)
{
plane_t* plane = &world->planes[planeIdx];
ps1bsp_plane_t outPlane = { 0 };
outPlane.normal = plane->normal.convertNormal();
outPlane.dist = (short)(plane->dist * WORLDSCALE);
outPlane.type = (short)plane->type;
outPlanes.push_back(outPlane);
}
// Convert nodes
std::vector<ps1bsp_node_t> outNodes;
for (int nodeIdx = 0; nodeIdx < world->numNodes; ++nodeIdx)
{
node_t* node = &world->nodes[nodeIdx];
ps1bsp_node_t outNode = { 0 };
outNode.planeId = node->plane_id;
outNode.children[0] = node->front;
outNode.children[1] = node->back;
outNode.boundingSphere = node->box.toBoundingSphere();
outNodes.push_back(outNode);
}
// Convert leaves
std::vector<ps1bsp_leaf_t> outLeaves;
for (int leafIdx = 0; leafIdx < world->numLeaves; ++leafIdx)
{
dleaf_t* leaf = &world->leaves[leafIdx];
ps1bsp_leaf_t outLeaf = { 0 };
outLeaf.type = (short)leaf->type;
outLeaf.vislist = leaf->vislist;
outLeaf.firstLeafFace = leaf->lface_id;
outLeaf.numLeafFaces = leaf->lface_num;
outLeaf.mins = leaf->bound.getMins().convertWorldPosition();
outLeaf.maxs = leaf->bound.getMaxs().convertWorldPosition();
outLeaves.push_back(outLeaf);
}
// Convert models
std::vector<ps1bsp_model_t> outModels;
for (int modelIdx = 0; modelIdx < world->numModels; ++modelIdx)
{
model_t* model = &world->models[modelIdx];
ps1bsp_model_t outModel = { 0 };
outModel.boundingSphere = model->bound.toBoundingSphere();
outModel.origin = model->origin.convertWorldPosition();
outModel.nodeId = (u_short)model->node_id0;
outModel.clipNodeId = (u_short)model->node_id1;
outModels.push_back(outModel);
}
std::vector<unsigned short> outLeafFaces(world->faceList, world->faceList + world->faceListLength);
std::vector<unsigned char> outVisData(world->visList, world->visList + world->visListLength);
// Write collected data to file and update header info
writeMapData(outTextures, outHeader.textures, fbsp);
writeMapData(outVertices, outHeader.vertices, fbsp);
writeMapData(outPolygons, outHeader.polygons, fbsp);
writeMapData(outPolyVertices, outHeader.polyVertices, fbsp);
writeMapData(outFaces, outHeader.faces, fbsp);
writeMapData(outFaceVertices, outHeader.faceVertices, fbsp);
writeMapData(outPlanes, outHeader.planes, fbsp);
writeMapData(outNodes, outHeader.nodes, fbsp);
writeMapData(outLeaves, outHeader.leaves, fbsp);
writeMapData(outLeafFaces, outHeader.leafFaces, fbsp);
writeMapData(outVisData, outHeader.visData, fbsp);
writeMapData(outModels, outHeader.models, fbsp);
// Write final header
fseek(fbsp, 0, SEEK_SET);
fwrite(&outHeader, sizeof(ps1bsp_header_t), 1, fbsp);
fclose(fbsp);
printf("PS1BSP: wrote %d vertices, %d faces, %d polygons, %d planes, %d nodes, %d leaves, %d leaf faces, %d models\n",
outVertices.size(), outFaces.size(), outPolygons.size(),
outPlanes.size(), outNodes.size(), outLeaves.size(), outLeafFaces.size(),
outModels.size());
return 1;
}
int process_bsp(const world_t *world)
{
// Test exporting texture data
TextureList textures;
if (!process_textures(world, textures))
{
return 0;
}
// Inspect faces/edges data
if (!process_faces(world, textures))
{
return 0;
}
return 1;
}
int load_bsp(const char* bspname, world_t* world)
{
FILE* f;
dheader_t* header = &world->header;
world->name = bspname;
sprintf_s(path, _MAX_PATH, "%s.bsp", bspname);
fopen_s(&f, path, "rb");
if (f == NULL)
return 0;
fread(header, sizeof(dheader_t), 1, f);
printf("Header model version: %d\n", header->version);
// Load entities
fseek(f, header->entities.offset, SEEK_SET);
world->entitiesLength = header->entities.size + 1;
world->entities = (char*)malloc(world->entitiesLength * sizeof(char));
if (world->entities == NULL)
return 0;
memset(world->entities, 0, world->entitiesLength * sizeof(char));
fread(world->entities, sizeof(char), world->entitiesLength, f);
// Load textures
mipheader_t* mipheader = &world->mipheader;
fseek(f, header->miptex.offset, SEEK_SET);
fread(&mipheader->numtex, sizeof(long), 1, f);
mipheader->offset = (long*)malloc(mipheader->numtex * sizeof(long));
if (mipheader->offset == NULL)
return 0;
fread(mipheader->offset, sizeof(long), mipheader->numtex, f);
world->miptexes = (miptex_t*)malloc(mipheader->numtex * sizeof(miptex_t));
if (world->miptexes == NULL)
return 0;
const int numMipLevels = 4;
world->textures = (unsigned char**)malloc(mipheader->numtex * numMipLevels * sizeof(unsigned char*));
if (world->textures == NULL)
return 0;
memset(world->textures, 0, mipheader->numtex * numMipLevels * sizeof(unsigned char*));
for (int texNum = 0; texNum < mipheader->numtex; ++texNum)
{
miptex_t* miptex = &world->miptexes[texNum];
unsigned long miptexOffset = header->miptex.offset + mipheader->offset[texNum];
fseek(f, miptexOffset, SEEK_SET);
fread(miptex, sizeof(miptex_t), 1, f);
for (int mipLevel = 0; mipLevel < numMipLevels; ++mipLevel)
{
unsigned long mipOffset = *(&miptex->offset1 + mipLevel);
fseek(f, miptexOffset + mipOffset, SEEK_SET);
size_t numBytes = (miptex->width * miptex->height) >> mipLevel;
unsigned char* texBytes = (unsigned char*)malloc(sizeof(unsigned char) * numBytes);
if (texBytes == NULL)
return 0;
fread(texBytes, sizeof(unsigned char), numBytes, f);
world->textures[texNum * numMipLevels + mipLevel] = texBytes;
}
}
// Load planes
world->numPlanes = header->planes.size / sizeof(plane_t);
world->planes = (plane_t*)malloc(header->planes.size);
if (world->planes == NULL)
return 0;
fseek(f, header->planes.offset, SEEK_SET);
fread(world->planes, sizeof(plane_t), world->numPlanes, f);
// Load vertices
world->numVertices = header->vertices.size / sizeof(vertex_t);
world->vertices = (vertex_t*)malloc(header->vertices.size);
if (world->vertices == NULL)
return 0;
fseek(f, header->vertices.offset, SEEK_SET);
fread(world->vertices, sizeof(vertex_t), world->numVertices, f);
// Load edges
world->numEdges = header->edges.size / sizeof(edge_t);
world->edges = (edge_t*)malloc(header->edges.size);
if (world->edges == NULL)
return 0;
fseek(f, header->edges.offset, SEEK_SET);
fread(world->edges, sizeof(edge_t), world->numEdges, f);
world->edgeListLength = header->ledges.size / sizeof(int);
world->edgeList = (int*)malloc(header->ledges.size);
if (world->edgeList == NULL)
return 0;
fseek(f, header->ledges.offset, SEEK_SET);
fread(world->edgeList, sizeof(int), world->edgeListLength, f);
// Load texture info
world->numTexInfos = header->texinfo.size / sizeof(texinfo_t);
world->texInfos = (texinfo_t*)malloc(header->texinfo.size);
if (world->texInfos == NULL)
return 0;
fseek(f, header->texinfo.offset, SEEK_SET);
fread(world->texInfos, sizeof(texinfo_t), world->numTexInfos, f);
// Load faces
world->numFaces = header->faces.size / sizeof(face_t);
world->faces = (face_t*)malloc(header->faces.size);
if (world->faces == NULL)
return 0;
fseek(f, header->faces.offset, SEEK_SET);
fread(world->faces, sizeof(face_t), world->numFaces, f);
world->faceListLength = header->lface.size / sizeof(unsigned short);
world->faceList = (unsigned short*)malloc(header->lface.size);
if (world->faceList == NULL)
return 0;
// Load visibility list
fseek(f, header->lface.offset, SEEK_SET);
fread(world->faceList, sizeof(unsigned short), world->faceListLength, f);
world->visListLength = header->visilist.size / sizeof(unsigned char);
world->visList = (unsigned char*)malloc(header->visilist.size);
if (world->visList == NULL)
return 0;
fseek(f, header->visilist.offset, SEEK_SET);
fread(world->visList, sizeof(unsigned char), world->visListLength, f);
// Load nodes
world->numNodes = header->nodes.size / sizeof(node_t);
world->nodes = (node_t*)malloc(header->nodes.size);
if (world->nodes == NULL)
return 0;
fseek(f, header->nodes.offset, SEEK_SET);
fread(world->nodes, sizeof(node_t), world->numNodes, f);
// Load leaves
world->numLeaves = header->leaves.size / sizeof(dleaf_t);
world->leaves = (dleaf_t*)malloc(header->leaves.size);
if (world->leaves == NULL)
return 0;
fseek(f, header->leaves.offset, SEEK_SET);
fread(world->leaves, sizeof(dleaf_t), world->numLeaves, f);
// Load models
world->numModels = header->models.size / sizeof(model_t);
world->models = (model_t*)malloc(header->models.size);
if (world->models == NULL)
return 0;
fseek(f, header->models.offset, SEEK_SET);
fread(world->models, sizeof(model_t), world->numModels, f);
// Load lightmaps
world->lightmapLength = header->lightmaps.size / sizeof(unsigned char);
world->lightmap = (unsigned char*)malloc(header->lightmaps.size);
if (world->lightmap == NULL)
return 0;
fseek(f, header->lightmaps.offset, SEEK_SET);
fread(world->lightmap, sizeof(unsigned char), world->lightmapLength, f);
fclose(f);
return 1;
}
void free_bsp(world_t* world)
{
free(world->lightmap);
free(world->leaves);
free(world->nodes);
free(world->visList);
free(world->texInfos);
free(world->faces);
free(world->faceList);
free(world->edges);
free(world->edgeList);
free(world->vertices);
free(world->planes);
for (int i = 0; i < world->mipheader.numtex; ++i)
{
free(world->textures[i]);
}
free(world->textures);
free(world->miptexes);
free(world->mipheader.offset);
free(world->entities);
}
int main(int argc, char** argv)
{
world_t world = { 0 };
if (!load_bsp(argv[1], &world))
return 1;
int result = process_bsp(&world);
free_bsp(&world);
return !result;
}