From fc281251eaf7de8fa9d424b8c3c057e318d7a99b Mon Sep 17 00:00:00 2001 From: Nico de Poel Date: Fri, 27 Jan 2023 13:31:41 +0100 Subject: [PATCH] Overhaul of geometry conversion: - Faces are now tessellated into one or more polygons, based on UV coordinates and texture tiling - Polygons have their own vertex index list and light/UV values per vertex - Lighting is sampled per polygon vertex, allowing for more light samples in general and more refined lighting - There are still problems with reconstruction of vertices for sloped surfaces, but this is enough of a result for a checkpoint commit --- common.h | 10 +++ lighting.cpp | 8 ++- main.cpp | 180 +++++++++++++++++++++++--------------------------- ps1bsp.h | 35 ++++++++-- tesselate.cpp | 59 ++++++++--------- tesselate.h | 33 +++++++-- 6 files changed, 183 insertions(+), 142 deletions(-) diff --git a/common.h b/common.h index 513d82f..c7fc01a 100644 --- a/common.h +++ b/common.h @@ -37,6 +37,11 @@ typedef struct Vec3 // Vector or Position return Vec3(x * mul, y * mul, z * mul); } + Vec3 operator*(double mul) const + { + return Vec3((double)x * mul, (double)y * mul, (double)z * mul); + } + Vec3 operator/(float div) const { return Vec3(x / div, y / div, z / div); @@ -52,6 +57,11 @@ typedef struct Vec3 // Vector or Position return sqrt((double)x * x + (double)y * y + (double)z * z); } + double sqrMagnitude() const + { + return (double)x * x + (double)y * y + (double)z * z; + } + Vec3 normalized() const { double invMag = 1.0 / magnitude(); diff --git a/lighting.cpp b/lighting.cpp index bde36fd..64a755b 100644 --- a/lighting.cpp +++ b/lighting.cpp @@ -9,8 +9,8 @@ bool sample_lightmap(const world_t* world, const face_t* face, const BoundBox& b const unsigned char* lightmap = &world->lightmap[face->lightmap]; const plane_t* plane = &world->planes[face->plane_id]; - Vec3 minBounds = (bounds.min / 16).floor() * 16; - Vec3 maxBounds = (bounds.max / 16).ceil() * 16; + Vec3 minBounds = (bounds.min / 16).floor() * 16.f; + Vec3 maxBounds = (bounds.max / 16).ceil() * 16.f; int width, height; int u, v; @@ -419,5 +419,9 @@ unsigned char compute_faceVertex_light5(const world_t* world, const face_t* refF } } + if (!numSamples) + return 0; // Shouldn't happen + + // We should always end up with at least one sample (that from refFace itself), so if we divide by zero here something is very much wrong return (unsigned char)(light / numSamples); } diff --git a/main.cpp b/main.cpp index d753403..e49ef85 100644 --- a/main.cpp +++ b/main.cpp @@ -39,27 +39,20 @@ 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 using the face's first vertex - int edgeIdx = world->edgeList[face->ledge_id]; - unsigned short vertIndex = edgeIdx > 0 ? - world->edges[edgeIdx].vertex0 : - world->edges[-edgeIdx].vertex1; - - const vertex_t* vertex = &world->vertices[vertIndex]; - Vec3 refPoint = plane->normal * plane->dist; - Vec3 tangent = (vertex->toVec() - refPoint).normalized(); - Vec3 bitangent = plane->normal.crossProduct(tangent); + // 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) { - edgeIdx = world->edgeList[face->ledge_id + edgeListIdx]; - vertIndex = edgeIdx > 0 ? + int edgeIdx = world->edgeList[face->ledge_id + edgeListIdx]; + int vertIndex = edgeIdx > 0 ? world->edges[edgeIdx].vertex0 : world->edges[-edgeIdx].vertex1; - vertex_t* vertex = &world->vertices[vertIndex]; + const vertex_t* vertex = &world->vertices[vertIndex]; Vec3 vec = vertex->toVec(); double x = tangent.dotProduct(vec); @@ -71,6 +64,8 @@ static float computeFaceArea(const world_t* world, const face_t* face) 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 @@ -83,32 +78,6 @@ int process_faces(const world_t* world, const std::vector& tex outHeader.version = 1; fwrite(&outHeader, sizeof(ps1bsp_header_t), 1, fbsp); - auto edgeData = analyze_edges(world); - - // Convert vertex data (no vertex splitting yet) - std::vector outVertices; - for (unsigned short i = 0; i < world->numVertices; ++i) - { - 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); - } - 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); - } - Tesselator tesselator(world); // Convert faces defined by edges into faces defined by vertex indices @@ -117,20 +86,15 @@ int process_faces(const world_t* world, const std::vector& tex FaceBounds faceBounds; for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx) { - face_t* face = &world->faces[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 ps1bsp_texture_t& ps1tex = textures[texinfo->texture_id]; ps1bsp_face_t outFace = { 0 }; outFace.planeId = face->plane_id; - outFace.side = face->side; + outFace.side = (unsigned char)face->side; outFace.firstFaceVertex = (unsigned short)outFaceVertices.size(); outFace.textureId = (unsigned char)texinfo->texture_id; - double minS = DBL_MAX, minT = DBL_MAX; - double maxS = DBL_MIN, maxT = DBL_MIN; - // Traverse the list of face edges to collect all of the face's vertices Vec3 vertexSum; BoundBox bounds; @@ -145,12 +109,6 @@ int process_faces(const world_t* world, const std::vector& tex const vertex_t* vertex = &world->vertices[vertIndex]; Vec3 vertexPoint = vertex->toVec(); - // Calculate texture UV bounds - double s = (vertexPoint.dotProduct(texinfo->vectorS) + texinfo->distS) / miptex->width; - double t = (vertexPoint.dotProduct(texinfo->vectorT) + texinfo->distT) / miptex->height; - if (s > maxS) maxS = s; if (s < minS) minS = s; - if (t > maxT) maxT = t; if (t < minT) minT = t; - // Calculate bounding box of this face if (edgeListIdx == 0) bounds.init(vertexPoint); @@ -159,41 +117,11 @@ int process_faces(const world_t* world, const std::vector& tex // Sum all vertices to calculate an average center point vertexSum = vertexSum + vertexPoint; - } - - // If the texture doesn't tile, we don't need to correct the UVs as much - double sRange = maxS - minS; - double tRange = maxT - minT; - if (sRange < 1) sRange = 1; - if (tRange < 1) tRange = 1; - - // Go over the edges again to fudge some UVs for the vertices (this second pass is only necessary because we don't have texture tiling yet) - 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(); - - ps1bsp_facevertex_t faceVertex = { 0 }; - faceVertex.index = vertIndex; - faceVertex.light = 0; - - // Calculate texture UVs - double s = (vertexPoint.dotProduct(texinfo->vectorS) + texinfo->distS) / miptex->width; - double t = (vertexPoint.dotProduct(texinfo->vectorT) + texinfo->distT) / miptex->height; - if (minS < 0 || maxS > 1) s = (s - minS) / sRange; - if (minT < 0 || maxT > 1) t = (t - minT) / tRange; - - // Rescale the UVs to the dimensions of the mipmap we've selected for our texture atlas - faceVertex.u = (unsigned char)(s * (ps1tex.w - 1)) + ps1tex.uoffs; - faceVertex.v = (unsigned char)(t * (ps1tex.h - 1)) + ps1tex.voffs; - outFaceVertices.push_back(faceVertex); + // TODO: face vertex indices will have to be updated to match the tesselator's vertex list + //ps1bsp_facevertex_t faceVertex = { 0 }; + //faceVertex.index = vertIndex; + //outFaceVertices.push_back(faceVertex); } faceBounds[face] = bounds; @@ -203,30 +131,84 @@ int process_faces(const world_t* world, const std::vector& tex // export_lightmap(world, face, bounds, faceIdx); outFace.numFaceVertices = (unsigned char)(outFaceVertices.size() - outFace.firstFaceVertex); - outFace.center = convertWorldPosition(vertexSum / outFace.numFaceVertices); + outFace.center = convertWorldPosition(vertexSum / face->ledge_num); float area = computeFaceArea(world, face); outFace.center.pad = (short)(sqrt(area)); outFaces.push_back(outFace); - - auto tessPolys = tesselator.tesselateFace(face); } + std::vector outSurfVertices; + 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]; + 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]; + + auto polygons = tesselator.tesselateFace(face); + + outFace->firstPolygon = (unsigned short)outPolygons.size(); + + for (auto polyIter = polygons.begin(); polyIter != polygons.end(); ++polyIter) + { + ps1bsp_polygon_t outPoly = { 0 }; + outPoly.firstPolyVertex = (unsigned short)outSurfVertices.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_surfvertex_t surfVert = { 0 }; + surfVert.index = (unsigned short)vertIndex; + surfVert.u = (unsigned char)(normalizedUV.x * (ps1tex.w - 1)) + ps1tex.uoffs; + surfVert.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; + + surfVert.light = (unsigned short)light; + outSurfVertices.push_back(surfVert); + } + + outPoly.numPolyVertices = (unsigned short)(outSurfVertices.size() - outPoly.firstPolyVertex); + outPolygons.push_back(outPoly); + } + + outFace->numPolygons = (unsigned char)(outPolygons.size() - outFace->firstPolygon); + } - // Sample lightmap contribution of this face on each vertex - for (size_t faceVertIdx = 0; faceVertIdx < outFace.numFaceVertices; ++faceVertIdx) + // 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 { - ps1bsp_facevertex_t& faceVertex = outFaceVertices[outFace.firstFaceVertex + faceVertIdx]; - const vertex_t* vertex = &world->vertices[faceVertex.index]; - faceVertex.light = compute_faceVertex_light5(world, face, faceBounds, vertex->toVec()); - faceVertex.light = (short)((float)faceVertex.light * 1.5f); // Compromise between overbright and non-overbright lighting. Looks good in practice. - if (faceVertex.light > 255) - faceVertex.light = 255; + 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 @@ -283,6 +265,8 @@ int process_faces(const world_t* world, const std::vector& tex // Write collected data to file and update header info writeMapData(textures, outHeader.textures, fbsp); writeMapData(outVertices, outHeader.vertices, fbsp); + writeMapData(outSurfVertices, outHeader.surfVertices, fbsp); + writeMapData(outPolygons, outHeader.polygons, fbsp); writeMapData(outFaces, outHeader.faces, fbsp); writeMapData(outFaceVertices, outHeader.faceVertices, fbsp); writeMapData(outPlanes, outHeader.planes, fbsp); diff --git a/ps1bsp.h b/ps1bsp.h index d825739..55d7ecd 100644 --- a/ps1bsp.h +++ b/ps1bsp.h @@ -30,6 +30,9 @@ typedef struct ps1bsp_dentry_t textures; ps1bsp_dentry_t vertices; + ps1bsp_dentry_t surfVertices; + ps1bsp_dentry_t polygons; + ps1bsp_dentry_t polyVertices; ps1bsp_dentry_t faces; ps1bsp_dentry_t faceVertices; ps1bsp_dentry_t planes; @@ -54,12 +57,25 @@ typedef struct short pad; } ps1bsp_vertex_t; +// Texture UV and lighting data for a vertex on a particular surface. Can be shared between multiple polygons on the same surface. typedef struct { unsigned short index; - unsigned short light; + unsigned short light; // Can be made into u_char if we need to store more data; currently u_short for 32-bit alignment purposes + unsigned short u, v; // Can be made into u_char if we need to store more data; currently u_short for 32-bit alignment purposes +} ps1bsp_surfvertex_t; - unsigned char u, v; // TODO: make into unsigned short, clamp/mask/modulo to u_char at run-time. So we can build tiling polygons later. +// Faces are broken up into one or more polygons, each of which can be drawn as a quad/triangle strip with a single texture. +// This ahead-of-time tesselation is done to deal with the fact that the PS1 can't do texture wrapping, meaning tiling textures have to be broken up into separate polygons. +typedef struct +{ + unsigned short firstPolyVertex; + unsigned short numPolyVertices; +} ps1bsp_polygon_t; + +typedef struct +{ + unsigned short index; // Sampled texture color * light, for untextured gouraud shaded drawing at range unsigned char a : 1; @@ -68,19 +84,30 @@ typedef struct unsigned char b : 5; } ps1bsp_facevertex_t; +// High quality: Face -> polygons -> polygon vertex indices (index + UV + light) -> vertices +// Low quality: Face -> face vertex indices (index + color) -> vertices typedef struct { unsigned short planeId; - unsigned short side; + unsigned char side; + // Used for high-quality tesselated textured drawing + unsigned short firstPolygon; + unsigned char numPolygons; + + // Used for low-quality untextured vertex colored drawing unsigned short firstFaceVertex; unsigned char numFaceVertices; unsigned char textureId; + // Used for backface culling SVECTOR center; - u_long drawFrame; // Which frame was this face last drawn on? Used to check if this face should be drawn. + // Which frame was this face last drawn on? Used to check if this face should be drawn. + u_long drawFrame; + + u_short pad; // For 32-bit alignment } ps1bsp_face_t; typedef struct diff --git a/tesselate.cpp b/tesselate.cpp index 033e881..4c647fa 100644 --- a/tesselate.cpp +++ b/tesselate.cpp @@ -21,7 +21,11 @@ std::vector Tesselator::tesselateFace(const face_t* face) if (contour.vertex == NULL) return polygons; + double invSLenSqr = 1.0 / texinfo->vectorS.sqrMagnitude(); + double invTLenSqr = 1.0 / texinfo->vectorT.sqrMagnitude(); + // Build a polygon in normalized 2D texture space from the original face data + std::vector faceVertices; for (int edgeListIdx = 0; edgeListIdx < face->ledge_num; ++edgeListIdx) { int edgeIdx = world->edgeList[face->ledge_id + edgeListIdx]; @@ -32,6 +36,11 @@ std::vector Tesselator::tesselateFace(const face_t* face) const vertex_t* vertex = &world->vertices[vertIndex]; Vec3 vertexPoint = vertex->toVec(); + faceVertices.push_back(vertexPoint); + + // vectorS and vectorT are normally perpendicular (dot product is 0), magnitude isn't always 1 but that's fine + // Means we can construct a coordinate space from them (cross product for the third vector) and transform the vertex point to ST-space + // And we can create an inverse transform... though just having s and t values probably isn't enough to completely transform back... // Calculate texture UV bounds double s = (vertexPoint.dotProduct(texinfo->vectorS) + texinfo->distS) / miptex->width; @@ -44,6 +53,8 @@ std::vector Tesselator::tesselateFace(const face_t* face) gpc_add_contour(&facePolygon, &contour, 0); + auto faceVert = *faceVertices.begin(); + // Create a virtual grid at the texture bounds and iterate over each cell to break up the face into repeating tiles for (double y = floor(minT); y <= ceil(maxT); y += 1.0) { @@ -55,13 +66,13 @@ std::vector Tesselator::tesselateFace(const face_t* face) cell_bounds.num_vertices = 4; cell_bounds.vertex = (gpc_vertex*)malloc(4 * sizeof(gpc_vertex)); cell_bounds.vertex[0] = gpc_vertex{ x, y }; - cell_bounds.vertex[1] = gpc_vertex{ x + 1.0, y }; + cell_bounds.vertex[1] = gpc_vertex{ x, y + 1.0 }; cell_bounds.vertex[2] = gpc_vertex{ x + 1.0, y + 1.0 }; - cell_bounds.vertex[3] = gpc_vertex{ x, y + 1.0 }; + cell_bounds.vertex[3] = gpc_vertex{ x + 1.0, y }; gpc_add_contour(&cell, &cell_bounds, 0); // Take the intersection to get the chunk of the face that's inside this cell - gpc_polygon result; + gpc_polygon result = { 0 }; gpc_polygon_clip(GPC_INT, &facePolygon, &cell, &result); // We should get a polygon with exactly one contour as a result; if not, the face was not on this grid cell @@ -69,7 +80,6 @@ std::vector Tesselator::tesselateFace(const face_t* face) continue; Polygon newPoly; - newPoly.texinfo = texinfo; // Reconstruct the polygon's vertices in 3D world space for (int v = 0; v < result.contour[0].num_vertices; ++v) @@ -77,34 +87,13 @@ std::vector Tesselator::tesselateFace(const face_t* face) const auto vert = &result.contour[0].vertex[v]; Vec3 newVert = plane->normal * plane->dist + - texinfo->vectorS * (float)(vert->x * miptex->width - texinfo->distS) + - texinfo->vectorT * (float)(vert->y * miptex->height - texinfo->distT); - - // Make sure we don't store duplicate vertices - size_t vertexIndex; - auto vertIter = vertexIndices.find(newVert); - if (vertIter != vertexIndices.end()) - { - vertexIndex = vertIter->second; - } - else - { - vertexIndex = vertices.size(); - vertexIndices[newVert] = vertexIndex; - vertices.push_back(newVert); - } - - newPoly.indices.push_back(vertexIndex); - - // Store the relevant (S, T) coordinates for each vertex-texinfo pair - VertexTexturePair uvPair{ newVert, texinfo }; - auto vertUVIter = vertexUVs.find(uvPair); - if (vertUVIter == vertexUVs.end()) - { - // Normalize the UV to fall within [0..1] range - Vec3 uv(vert->x - x, vert->y - y, 0); - vertexUVs[uvPair] = uv; - } + texinfo->vectorS * (vert->x * miptex->width - texinfo->distS) * invSLenSqr + + texinfo->vectorT * (vert->y * miptex->height - texinfo->distT) * invTLenSqr; + + size_t vertIndex = addVertex(newVert); + Vec3 normalizedUV(vert->x - x, vert->y - y, 0); // Normalize the UV to fall within [0..1] range + + newPoly.polyVertices.push_back(PolyVertex{ vertIndex, normalizedUV }); } polygons.push_back(newPoly); @@ -114,6 +103,12 @@ std::vector Tesselator::tesselateFace(const face_t* face) } } + if (vertexIndices.find(faceVert) == vertexIndices.end()) + { + gpc_free_polygon(&facePolygon); + return polygons; + } + gpc_free_polygon(&facePolygon); return polygons; } diff --git a/tesselate.h b/tesselate.h index f56a209..128ae64 100644 --- a/tesselate.h +++ b/tesselate.h @@ -5,13 +5,16 @@ class Tesselator public: typedef std::vector VertexList; typedef std::unordered_map VertexIndexMap; - typedef std::pair VertexTexturePair; - typedef std::map VertexUVMap; + + struct PolyVertex + { + size_t vertexIndex; + Vec3 normalizedUV; + }; struct Polygon { - const texinfo_t* texinfo; - std::vector indices; + std::vector polyVertices; }; Tesselator(const world_t* world) : world(world) @@ -20,7 +23,26 @@ public: const VertexList& getVertices() const { return vertices; } const VertexIndexMap& getVertexIndices() const { return vertexIndices; } - const VertexUVMap& getVertexUVs() const { return vertexUVs; } + + size_t addVertex(const Vec3& vert) + { + size_t vertexIndex; + + // Make sure we don't store duplicate vertices + auto vertIter = vertexIndices.find(vert); + if (vertIter != vertexIndices.end()) + { + vertexIndex = vertIter->second; + } + else + { + vertexIndex = vertices.size(); + vertexIndices[vert] = vertexIndex; + vertices.push_back(vert); + } + + return vertexIndex; + } std::vector tesselateFace(const face_t* face); @@ -29,5 +51,4 @@ private: VertexList vertices; VertexIndexMap vertexIndices; - VertexUVMap vertexUVs; };