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.
484 lines
15 KiB
484 lines
15 KiB
#include <memory.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <vector>
|
|
#include <unordered_map>
|
|
#include "bsp.h"
|
|
#include "rectpack/finders_interface.h"
|
|
#include "ps1bsp.h"
|
|
|
|
static char path[_MAX_PATH];
|
|
|
|
int process_entities(const world_t *world)
|
|
{
|
|
printf("Entities list:\n%s\n", world->entities);
|
|
return 1;
|
|
}
|
|
|
|
int process_textures(const world_t* world)
|
|
{
|
|
using spaces_type = rectpack2D::empty_spaces<false>;
|
|
using rect_type = rectpack2D::output_rect_t<spaces_type>;
|
|
|
|
auto report_successful = [](rect_type&) {
|
|
return rectpack2D::callback_result::CONTINUE_PACKING;
|
|
};
|
|
|
|
auto report_unsuccessful = [](rect_type&) {
|
|
return rectpack2D::callback_result::ABORT_PACKING;
|
|
};
|
|
|
|
const auto max_side = 512; // Max height of PS1 VRAM. 8-bit textures take up half the horizontal space so this is 512x256 in practice, or a quarter of the PS1's VRAM allocation.
|
|
const auto discard_step = -4;
|
|
|
|
std::vector<rect_type> rectangles;
|
|
|
|
// Try some texture packing and see if we fit inside the PS1's VRAM
|
|
for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum)
|
|
{
|
|
miptex_t *miptex = &world->miptexes[texNum];
|
|
if (miptex->name[0] == '\0') // Weird edge case on N64START.bsp, corrupt data perhaps?
|
|
miptex->width = miptex->height = 0;
|
|
|
|
//printf("Texture %d (%dx%d): %.16s\n", texNum, miptex->width, miptex->height, miptex->name);
|
|
|
|
// Shrink the larger textures, but keep smaller ones at their original size
|
|
int ps1mip = miptex->width > 64 || miptex->height > 64 ? 1 : 0;
|
|
|
|
if (strcmp(miptex->name, "clip") && strcmp(miptex->name, "trigger"))
|
|
rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex->width >> ps1mip, miptex->height >> ps1mip));
|
|
else
|
|
rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, 0, 0));
|
|
}
|
|
|
|
// Automatic atlas packing. Nice but it tries to make a square atlas which is not what we want. (This is solved by hacking the header itself)
|
|
const auto result_size = rectpack2D::find_best_packing<spaces_type>(
|
|
rectangles,
|
|
rectpack2D::make_finder_input(
|
|
max_side,
|
|
discard_step,
|
|
report_successful,
|
|
report_unsuccessful,
|
|
rectpack2D::flipping_option::DISABLED
|
|
)
|
|
);
|
|
|
|
printf("%d textures. Packed texture atlas size: %d x %d\n", world->mipheader.numtex, result_size.w, result_size.h);
|
|
unsigned char* atlas = (unsigned char*)malloc(result_size.w * result_size.h * sizeof(unsigned char));
|
|
if (atlas == NULL)
|
|
return 0;
|
|
|
|
memset(atlas, 0, result_size.w * result_size.h * sizeof(unsigned char));
|
|
|
|
// Try to construct the texture atlas, see what we get
|
|
for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum)
|
|
{
|
|
miptex_t* miptex = &world->miptexes[texNum];
|
|
if (miptex->name[0] == '\0') // Weird edge case on N64START.bsp, corrupt data perhaps?
|
|
continue;
|
|
|
|
char* outName = miptex->name;
|
|
if (*outName == '*' || *outName == '+')
|
|
outName++;
|
|
|
|
for (int mipLevel = 0; mipLevel < 4; ++mipLevel)
|
|
{
|
|
unsigned char* texBytes = world->textures[texNum * 4 + mipLevel];
|
|
|
|
FILE* fraw;
|
|
sprintf_s(path, _MAX_PATH, "textures/%s-%s-mip%d-%dx%d.raw", world->name, outName, mipLevel, miptex->width >> mipLevel, miptex->height >> mipLevel);
|
|
fopen_s(&fraw, path, "wb");
|
|
if (fraw != NULL)
|
|
{
|
|
size_t numBytes = (miptex->width * miptex->height) >> mipLevel;
|
|
fwrite(texBytes, sizeof(unsigned char), numBytes, fraw);
|
|
fclose(fraw);
|
|
}
|
|
|
|
const auto& rectangle = rectangles[texNum];
|
|
if (miptex->width >> mipLevel == rectangle.w) // This is the mip level we've previously decided we want for our PS1 atlas
|
|
{
|
|
//printf("Writing texture %s mip %d to position: (%d, %d) w = %d, h = %d\n", miptex->name, mipLevel, rectangle.x, rectangle.y, rectangle.w, rectangle.h);
|
|
for (int y = 0; y < rectangle.h; ++y)
|
|
{
|
|
memcpy_s(atlas + ((rectangle.y + y) * result_size.w + rectangle.x), rectangle.w * sizeof(unsigned char), texBytes + (y * rectangle.w), rectangle.w * sizeof(unsigned char));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FILE* fatlas;
|
|
sprintf_s(path, _MAX_PATH, "%s-atlas-%dx%d.raw", world->name, result_size.w, result_size.h);
|
|
fopen_s(&fatlas, path, "wb");
|
|
if (fatlas != NULL)
|
|
{
|
|
fwrite(atlas, sizeof(unsigned char), result_size.w * result_size.h, fatlas);
|
|
fclose(fatlas);
|
|
}
|
|
|
|
free(atlas);
|
|
return 1;
|
|
}
|
|
|
|
int process_vertices(const world_t* world)
|
|
{
|
|
vec3_t min = { FLT_MAX, FLT_MAX, FLT_MAX }, max = { -FLT_MAX, -FLT_MAX, -FLT_MAX };
|
|
for (int vertIdx = 0; vertIdx < world->numVertices; ++vertIdx)
|
|
{
|
|
vertex_t* vert = &world->vertices[vertIdx];
|
|
if (vert->X > max.x) max.x = vert->X;
|
|
if (vert->Y > max.y) max.y = vert->Y;
|
|
if (vert->Z > max.z) max.z = vert->Z;
|
|
if (vert->X < min.x) min.x = vert->X;
|
|
if (vert->Y < min.y) min.y = vert->Y;
|
|
if (vert->Z < min.z) min.z = vert->Z;
|
|
}
|
|
|
|
printf("%d vertices, %d faces, min = (%f, %f, %f), max = (%f, %f, %f)\n", world->numVertices, world->numFaces, min.x, min.y, min.z, max.x, max.y, max.z);
|
|
|
|
const int fixedScale = 1 << 14;
|
|
int fixedMin[3] = { (int)(min.x * fixedScale), (int)(min.y * fixedScale), (int)(min.z * fixedScale) };
|
|
int fixedMax[3] = { (int)(max.x * fixedScale), (int)(max.y * fixedScale), (int)(max.z * fixedScale) };
|
|
printf("Fixed point min = (%d, %d, %d), max = (%d, %d, %d)\n", fixedMin[0], fixedMin[1], fixedMin[2], fixedMax[0], fixedMax[1], fixedMax[2]);
|
|
|
|
return 1;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
vertex_t* vertex;
|
|
unsigned short index;
|
|
} vertexref_t;
|
|
|
|
// Determines a floating origin for the given leaf
|
|
static void leaf_zone(const dleaf_t* leaf, short zone[3])
|
|
{
|
|
const unsigned short mask = 0xFE00; // Zero out the 9 least significant bits
|
|
|
|
short midX = (leaf->bound.min[0] + leaf->bound.max[0]) / 2;
|
|
short midY = (leaf->bound.min[1] + leaf->bound.max[1]) / 2;
|
|
short midZ = (leaf->bound.min[2] + leaf->bound.max[2]) / 2;
|
|
|
|
zone[0] = midX & mask;
|
|
zone[1] = midY & mask;
|
|
zone[2] = midZ & mask;
|
|
}
|
|
|
|
int process_faces(const world_t* world)
|
|
{
|
|
// 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
|
|
fwrite(&outHeader, sizeof(ps1bsp_header_t), 1, fbsp);
|
|
|
|
// TODO: convert vertices and group them by material properties (texture, lightmap) and floating origin zone (based on leaf data), duplicate where necessary
|
|
short zone[3];
|
|
for (int leafIdx = 0; leafIdx < world->numLeaves; ++leafIdx)
|
|
{
|
|
dleaf_t* leaf = &world->leaves[leafIdx];
|
|
leaf_zone(leaf, zone);
|
|
//printf("Leaf %d zone (%d, %d, %d) %d face(s)\n", leafIdx, zone[0], zone[1], zone[2], leaf->lface_num);
|
|
}
|
|
|
|
// Write vertex data to a file (no vertex splitting yet)
|
|
for (unsigned short i = 0; i < world->numVertices; ++i)
|
|
{
|
|
// TODO: we should respect the ordering from vertexRef->index here but meh, problem for later
|
|
vertex_t* inVertex = &world->vertices[i];
|
|
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);
|
|
}
|
|
outVertex.baseLight = 128;
|
|
outVertex.finalLight = 128;
|
|
|
|
fwrite(&outVertex, sizeof(ps1bsp_vertex_t), 1, fbsp);
|
|
}
|
|
|
|
std::vector<ps1bsp_triangle_t> outTriangles;
|
|
std::vector<ps1bsp_face_t> outFaces;
|
|
|
|
std::vector<unsigned short> faceVertIndices;
|
|
for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx)
|
|
{
|
|
face_t* face = &world->faces[faceIdx];
|
|
|
|
// Traverse the list of face edges to collect all of the face's vertices
|
|
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;
|
|
|
|
faceVertIndices.push_back(vertIndex);
|
|
}
|
|
|
|
ps1bsp_face_t outFace = { 0 };
|
|
outFace.firstTriangleId = outTriangles.size();
|
|
|
|
//printf("Face %d: %d vertices\n", faceIdx, faceVerts.size());
|
|
|
|
// Triangulate face into polygons (triangle fan, the naive method)
|
|
// TODO better method: generate a quad strip topology
|
|
ps1bsp_triangle_t outTriangle;
|
|
outTriangle.vertex0 = faceVertIndices[0];
|
|
for (int faceVertIdx = 1; faceVertIdx < faceVertIndices.size() - 1; ++faceVertIdx)
|
|
{
|
|
outTriangle.vertex1 = faceVertIndices[faceVertIdx];
|
|
outTriangle.vertex2 = faceVertIndices[faceVertIdx + 1];
|
|
|
|
outTriangles.push_back(outTriangle);
|
|
}
|
|
|
|
outFace.numTriangles = outTriangles.size() - outFace.firstTriangleId;
|
|
outFaces.push_back(outFace);
|
|
|
|
faceVertIndices.clear();
|
|
}
|
|
|
|
// Write triangle and face data to file
|
|
fwrite(outTriangles.data(), sizeof(ps1bsp_triangle_t), outTriangles.size(), fbsp);
|
|
fwrite(outFaces.data(), sizeof(ps1bsp_face_t), outFaces.size(), fbsp);
|
|
|
|
// Update header information
|
|
outHeader.numVertices = world->numVertices;
|
|
outHeader.numTriangles = outTriangles.size();
|
|
outHeader.numFaces = outFaces.size();
|
|
|
|
// Write final header
|
|
fseek(fbsp, 0, SEEK_SET);
|
|
fwrite(&outHeader, sizeof(ps1bsp_header_t), 1, fbsp);
|
|
fclose(fbsp);
|
|
|
|
printf("PS1BSP: wrote %d vertices, %d triangles, %d faces\n", outHeader.numVertices, outHeader.numTriangles, outHeader.numFaces);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int process_bsp(const world_t *world)
|
|
{
|
|
// Test reading the entity string data
|
|
if (!process_entities(world))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Test exporting texture data
|
|
if (!process_textures(world))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Inspect vertex data
|
|
if (!process_vertices(world))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Inspect faces/edges data
|
|
if (!process_faces(world))
|
|
{
|
|
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 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);
|
|
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
void free_bsp(world_t* world)
|
|
{
|
|
free(world->leaves);
|
|
free(world->nodes);
|
|
free(world->visList);
|
|
|
|
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;
|
|
}
|