Browse Source

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
master
Nico de Poel 3 years ago
parent
commit
fc281251ea
  1. 10
      common.h
  2. 8
      lighting.cpp
  3. 180
      main.cpp
  4. 35
      ps1bsp.h
  5. 59
      tesselate.cpp
  6. 33
      tesselate.h

10
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();

8
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);
}

180
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<const face_t*, std::vector<Tesselator::Polygon>> FacePolygons;
int process_faces(const world_t* world, const std::vector<ps1bsp_texture_t>& textures)
{
// Write some data to a file
@ -83,32 +78,6 @@ int process_faces(const world_t* world, const std::vector<ps1bsp_texture_t>& 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<ps1bsp_vertex_t> 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<ps1bsp_texture_t>& 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<ps1bsp_texture_t>& 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<ps1bsp_texture_t>& 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<ps1bsp_texture_t>& 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<ps1bsp_surfvertex_t> outSurfVertices;
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];
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();
// Sample lightmap contribution of this face on each vertex
for (size_t faceVertIdx = 0; faceVertIdx < outFace.numFaceVertices; ++faceVertIdx)
for (auto polyIter = polygons.begin(); polyIter != polygons.end(); ++polyIter)
{
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;
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);
}
// 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 * 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
@ -283,6 +265,8 @@ int process_faces(const world_t* world, const std::vector<ps1bsp_texture_t>& 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);

35
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

59
tesselate.cpp

@ -21,7 +21,11 @@ std::vector<Tesselator::Polygon> 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<Vec3> 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::Polygon> 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::Polygon> 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::Polygon> 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::Polygon> 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::Polygon> 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::Polygon> Tesselator::tesselateFace(const face_t* face)
}
}
if (vertexIndices.find(faceVert) == vertexIndices.end())
{
gpc_free_polygon(&facePolygon);
return polygons;
}
gpc_free_polygon(&facePolygon);
return polygons;
}

33
tesselate.h

@ -5,13 +5,16 @@ class Tesselator
public:
typedef std::vector<Vec3> VertexList;
typedef std::unordered_map<Vec3, size_t> VertexIndexMap;
typedef std::pair<Vec3, const texinfo_t*> VertexTexturePair;
typedef std::map<VertexTexturePair, Vec3> VertexUVMap;
struct PolyVertex
{
size_t vertexIndex;
Vec3 normalizedUV;
};
struct Polygon
{
const texinfo_t* texinfo;
std::vector<size_t> indices;
std::vector<PolyVertex> 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<Polygon> tesselateFace(const face_t* face);
@ -29,5 +51,4 @@ private:
VertexList vertices;
VertexIndexMap vertexIndices;
VertexUVMap vertexUVs;
};
Loading…
Cancel
Save