#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 size_t writeMapData(const std::vector& 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> FacePolygons; int process_faces(const world_t* world, const std::vector& 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 outFaces; std::vector 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]; 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; // Traverse the list of face edges to collect all of the face's vertices Vec3 vertexSum; BoundBox 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.init(vertexPoint); else bounds.includePoint(vertexPoint); // Sum all vertices to calculate an average center point vertexSum = vertexSum + vertexPoint; // TODO: compute texture color * light at this vertex 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 outPolyVertices; std::vector 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 ps1bsp_texture_t& ps1tex = textures[texinfo->texture_id]; outFace->firstPolygon = (unsigned short)outPolygons.size(); // Skip sky surfaces for now if (!strncmp(miptex->name, "sky", 3)) { outFace->flags |= SURF_DRAWSKY; outFace->numPolygons = 0; continue; } // Invisible collision volumes don't have to be tessellated if (!strcmp(miptex->name, "clip") || !strcmp(miptex->name, "trigger")) { outFace->numPolygons = 0; continue; } // Draw water as fullbright transparent surfaces if (miptex->name[0] == '*') { outFace->flags |= SURF_DRAWTURB | SURF_DRAWWATER; } 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; Vec3 vertex = tesselator.getVertices()[vertIndex]; ps1bsp_polyvertex_t polyVert = { 0 }; polyVert.index = (unsigned short)vertIndex; polyVert.u = (unsigned char)(normalizedUV.x * (ps1tex.w - 1)) + ps1tex.uoffs; polyVert.v = (unsigned char)(normalizedUV.y * (ps1tex.h - 1)) + ps1tex.voffs; 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; } // TODO: calculate average face lighting * color from texture data outFace->numPolygons = (unsigned char)(outPolygons.size() - outFace->firstPolygon); } // Convert vertex data const auto& inVertices = tesselator.getVertices(); std::vector 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 * 4); outVertex.y = (short)(inVertex.y * 4); outVertex.z = (short)(inVertex.z * 4); } 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); } // Convert planes std::vector 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 * 4); outPlane.type = (short)plane->type; outPlanes.push_back(outPlane); } // Convert nodes std::vector 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 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); } std::vector outLeafFaces(world->faceList, world->faceList + world->faceListLength); std::vector outVisData(world->visList, world->visList + world->visListLength); // Write collected data to file and update header info writeMapData(textures, 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); // 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\n", outVertices.size(), outFaces.size(), outPolygons.size(), outPlanes.size(), outNodes.size(), outLeaves.size(), outLeafFaces.size()); return 1; } int process_bsp(const world_t *world) { // Test exporting texture data std::vector 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 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; }