#include "common.h" #include "lighting.h" unsigned char sample_lightmap(const world_t* world, const face_t* face, const BoundBox& bounds, const Vec3& point) { if (face->lightmap < 0) return 0; const unsigned char* lightmap = &world->lightmap[face->lightmap]; const plane_t* plane = &world->planes[face->plane_id]; int width, height; float u, v; switch (plane->type) { case 0: case 3: // Towards X width = (int)(ceil(bounds.max.y / 16) - floor(bounds.min.y / 16)) * 16; height = (int)(ceil(bounds.max.z / 16) - floor(bounds.min.z / 16)) * 16; u = (point.y - bounds.min.y) / (bounds.max.y - bounds.min.y); v = (point.z - bounds.min.z) / (bounds.max.z - bounds.min.z); break; case 1: case 4: // Towards Y width = (int)(ceil(bounds.max.x / 16) - floor(bounds.min.x / 16)) * 16; height = (int)(ceil(bounds.max.z / 16) - floor(bounds.min.z / 16)) * 16; u = (point.x - bounds.min.x) / (bounds.max.x - bounds.min.x); v = (point.z - bounds.min.z) / (bounds.max.z - bounds.min.z); break; case 2: case 5: // Towards Z width = (int)(ceil(bounds.max.x / 16) - floor(bounds.min.x / 16)) * 16; height = (int)(ceil(bounds.max.y / 16) - floor(bounds.min.y / 16)) * 16; u = (point.x - bounds.min.x) / (bounds.max.x - bounds.min.x); v = (point.y - bounds.min.y) / (bounds.max.y - bounds.min.y); break; default: printf("Error: unknown plane type %d\n", plane->type); return 0; } height >>= 4; width >>= 4; return lightmap[(int)(v * (width + 1) + u)]; } void export_lightmap(const world_t* world, const face_t* face, const BoundBox& bounds, int faceIdx) { if (face->lightmap < 0) return; const unsigned char* lightmap = &world->lightmap[face->lightmap]; const plane_t* plane = &world->planes[face->plane_id]; int width, height; switch (plane->type) { case 0: case 3: // Towards X width = (int)(ceil(bounds.max.y / 16) - floor(bounds.min.y / 16)); height = (int)(ceil(bounds.max.z / 16) - floor(bounds.min.z / 16)); break; case 1: case 4: // Towards Y width = (int)(ceil(bounds.max.x / 16) - floor(bounds.min.x / 16)); height = (int)(ceil(bounds.max.z / 16) - floor(bounds.min.z / 16)); break; case 2: case 5: // Towards Z width = (int)(ceil(bounds.max.x / 16) - floor(bounds.min.x / 16)); height = (int)(ceil(bounds.max.y / 16) - floor(bounds.min.y / 16)); break; default: printf("Error: unknown plane type %d\n", plane->type); return; } width += 1; char path[_MAX_PATH]; sprintf_s(path, _MAX_PATH, "lightmap_face%d_e%d_PT%d_%dx%d.raw", faceIdx, face->ledge_num, plane->type, width, height); FILE* flm; fopen_s(&flm, path, "wb"); if (!flm) return; for (int y = 0; y < height; ++y) { fwrite(&lightmap[y * width], sizeof(unsigned char), width, flm); } fclose(flm); } std::unordered_map analyze_edges(const world_t* world) { std::unordered_map edgeData; for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx) { const face_t* face = &world->faces[faceIdx]; const int* edgeList = &world->edgeList[face->ledge_id]; for (int i = 0; i < face->ledge_num; ++i, ++edgeList) { int edgeIdx = *edgeList; if (edgeIdx < 0) edgeIdx = -edgeIdx; // Reverse direction edge const edge_t* edge = &world->edges[edgeIdx]; auto iter = edgeData.find(edge); if (iter != edgeData.end()) { iter->second.faces.push_back(face); } else { EdgeData newData = { 0 }; newData.edgeIndex = edgeIdx; newData.faces.push_back(face); edgeData[edge] = newData; } } } for (auto iter = edgeData.begin(); iter != edgeData.end(); ++iter) { size_t numFaces = iter->second.faces.size(); switch (numFaces) { case 1: iter->second.isSharpEdge = true; break; case 2: { // TODO: take into account the face's side auto faceA = iter->second.faces[0]; auto faceB = iter->second.faces[1]; const plane_t* planeA = &world->planes[faceA->plane_id]; const plane_t* planeB = &world->planes[faceB->plane_id]; vec3_t normalA = faceA->side ? -planeA->normal : planeA->normal; vec3_t normalB = faceB->side ? -planeB->normal : planeB->normal; float dot = normalA.dotProduct(normalB); bool isSmooth = dot >= 0.5f;//&& dot <= 1; iter->second.isSharpEdge = !isSmooth; break; } default: printf("Edge at index %d has %d adjacent face(s), weird\n", iter->second.edgeIndex, numFaces); break; } } return edgeData; } unsigned char compute_faceVertex_light(const world_t* world, const face_t* face, unsigned short vertexIndex, const FaceBounds& faceBounds, const std::unordered_map& edgeData) { const vertex_t* vertex = &world->vertices[vertexIndex]; auto point = vertex->toVec(); // Sample this face's lighting contribution unsigned int light = sample_lightmap(world, face, faceBounds.find(face)->second, point) + (0xFF - face->baselight); int numSamples = 1; // Collect edges connected to this vertex, filter out the smooth ones only std::vector smoothEdges; for (auto iter = edgeData.begin(); iter != edgeData.end(); ++iter) { auto edge = iter->first; if (edge->vertex0 != vertexIndex && edge->vertex1 != vertexIndex) continue; if (iter->second.isSharpEdge) continue; // If the current face doesn't appear in this edge's adjacency list, we're not interested for (auto faceIter = iter->second.faces.begin(); faceIter != iter->second.faces.end(); ++faceIter) { // TODO: actually I don't think this is the correct solution. We're allowed to sample light contributions from edges that aren't connected to this face. // However we need to ensure we sample contributions from each face only once, and we need to check the angle between faces on a case-by-case basis. // In fact I don't think we're interested in edges at all? Just in the faces that connect to a certain vertex. if (*faceIter == face) { smoothEdges.push_back(edge); break; } } } // Gather lighting contributions from neigbouring faces for (auto edgeIter = smoothEdges.begin(); edgeIter != smoothEdges.end(); ++edgeIter) { auto faces = edgeData.find(*edgeIter)->second.faces; for (auto faceIter = faces.begin(); faceIter != faces.end(); ++faceIter) // FIXME: "this" face doesn't always appear in faces list, when it absolutely should! { const face_t* otherFace = *faceIter; if (otherFace == face) // Skip the current face, we only sample it once continue; light += sample_lightmap(world, otherFace, faceBounds.find(otherFace)->second, point) + (0xFF - otherFace->baselight); ++numSamples; } } return (unsigned char)(light / numSamples); } unsigned char compute_faceVertex_light2(const world_t* world, const face_t* face, unsigned short vertexIndex, const FaceBounds& faceBounds, const VertexFaces& vertexFaces) { const vertex_t* vertex = &world->vertices[vertexIndex]; auto vertexFaceIter = vertexFaces.find(vertex); if (vertexFaceIter == vertexFaces.end()) return 0; auto point = vertex->toVec(); // Sample this face's lighting contribution unsigned int light = sample_lightmap(world, face, faceBounds.find(face)->second, point) + (0xFF - face->baselight); int numSamples = 1; const plane_t* thisPlane = &world->planes[face->plane_id]; vec3_t thisNormal = face->side ? -thisPlane->normal : thisPlane->normal; // Gather light samples from other faces adjacent to this vertex for (auto faceIter = vertexFaceIter->second.begin(); faceIter != vertexFaceIter->second.end(); ++faceIter) { const face_t* otherFace = *faceIter; if (otherFace == face) continue; const plane_t* otherPlane = &world->planes[otherFace->plane_id]; vec3_t otherNormal = otherFace->side ? -otherPlane->normal : otherPlane->normal; float dot = thisNormal.dotProduct(otherNormal); if (dot < 0.5f) continue; // Sharp edge, we don't want light contribution from this face light += sample_lightmap(world, otherFace, faceBounds.find(otherFace)->second, point) + (0xFF - otherFace->baselight); ++numSamples; } return (unsigned char)(light / numSamples); } // Start with a hash set of all faces // From the first face onward, group together faces into an interconnected surface based on angle between faces, using a flood-fill type of approach: // - Add the first face to a surface face list // - Take the next face from the surface face list (use index to go to next, don't remove it from the surface list) // - For each face vertex, look up all other faces connected to that vertex // - If the other face is not present in the original hash set, skip it (it was already added to a surface) // - If the other face has an angle > 60 degrees with the current face, skip it (sharp edge) // - Add the other face to the surface face list, remove it from the hash set // - Repeat from step 'take the next face' SurfaceList group_surfaces(const world_t* world, const VertexFaces& vertexFaces) { SurfaceList surfaces; std::unordered_set faceSet; for (int i = 0; i < world->numFaces; ++i) faceSet.insert(&world->faces[i]); while (!faceSet.empty()) { const face_t* firstFace = *faceSet.begin(); faceSet.erase(firstFace); std::vector surfaceFaces; surfaceFaces.push_back(firstFace); for (size_t faceIdx = 0; faceIdx < surfaceFaces.size(); ++faceIdx) { const face_t* thisFace = surfaceFaces[faceIdx]; const plane_t* thisPlane = &world->planes[thisFace->plane_id]; vec3_t thisNormal = thisFace->side ? -thisPlane->normal : thisPlane->normal; for (int edgeListIdx = 0; edgeListIdx < thisFace->ledge_num; ++edgeListIdx) { int edgeIdx = world->edgeList[thisFace->ledge_id + edgeListIdx]; const edge_t* edge = &world->edges[abs(edgeIdx)]; for (int v = 0; v < 1; ++v) { unsigned short vertIndex = *(&edge->vertex0 + v); const vertex_t* vertex = &world->vertices[vertIndex]; auto vertexFaceIter = vertexFaces.find(vertex); if (vertexFaceIter == vertexFaces.end()) { printf("Couldn't find list of faces for vertex %d, weird...\n", vertIndex); continue; } for (auto faceIter = vertexFaceIter->second.begin(); faceIter != vertexFaceIter->second.end(); ++faceIter) { const face_t* otherFace = *faceIter; if (faceSet.find(otherFace) == faceSet.end()) continue; // Face has already been added to a surface, skip it const plane_t* otherPlane = &world->planes[otherFace->plane_id]; vec3_t otherNormal = otherFace->side ? -otherPlane->normal : otherPlane->normal; float dot = thisNormal.dotProduct(otherNormal); if (dot < 0.5f) continue; // Sharp edge, face belongs to a different surface // Add face to this surface and make sure it won't be reconsidered for any other surfaces surfaceFaces.push_back(otherFace); faceSet.erase(otherFace); } } } } Surface surface; surface.faces.insert(surfaceFaces.begin(), surfaceFaces.end()); surfaces.push_back(surface); } return surfaces; } // To sample lightmap at any arbitrary world position: // - Find the surface that the face belongs to // - Select all faces from the surface where the position lies in its plane and is inside polygon boundaries // - Sample those faces at the specified position (sample_lightmap) and average std::vector find_facesWithPoint(const world_t* world, const face_t* refFace, const SurfaceList& surfaces, Vec3 point) { std::vector faces; // Find the surface that the face belongs to for (auto surfIter = surfaces.begin(); surfIter != surfaces.end(); ++surfIter) { if (surfIter->faces.find(refFace) == surfIter->faces.end()) continue; // Select all faces from the surface where the point lies in its plane and is inside polygon boundaries for (auto faceIter = surfIter->faces.begin(); faceIter != surfIter->faces.end(); ++faceIter) { const face_t* face = *faceIter; const plane_t* plane = &world->planes[face->plane_id]; // Check if the point lies on the face's plane float pointPlaneDist = (point.x * plane->normal.x + point.y * plane->normal.y + point.z * plane->normal.z) - plane->dist; if (fabs(pointPlaneDist) > FLT_EPSILON) continue; // Simple case first: check if this point lies on one of the face's edges bool onEdge = false; for (int edgeListIdx = 0; edgeListIdx < face->ledge_num; ++edgeListIdx) { int edgeIdx = world->edgeList[face->ledge_id + edgeListIdx]; const edge_t* edge = &world->edges[abs(edgeIdx)]; Vec3 vert0 = world->vertices[edge->vertex0].toVec(); Vec3 vert1 = world->vertices[edge->vertex1].toVec(); Vec3 dir0 = vert1 - vert0; Vec3 dir1 = point - vert0; float mag0 = dir0.magnitude(); float mag1 = dir1.magnitude(); if (mag1 / mag0 <= 1.0f + FLT_EPSILON && dir0.dotProduct(dir1) >= (mag0 - FLT_EPSILON) * (mag1 - FLT_EPSILON)) onEdge = true; } if (onEdge) faces.push_back(face); } break; } return faces; } // Further improvements: // - Reconstruct connected surfaces through vertices and edges, so we can fully sample all adjacent lightmaps. // Right now we don't properly detect when one face has vertices halfway along the edge of an adjacent face. // - Sample more lightmap points around each vertex to obtain a more representative average value unsigned char compute_faceVertex_light3(const world_t* world, const face_t* refFace, const SurfaceList& surfaces, const FaceBounds& faceBounds, Vec3 point) { auto faces = find_facesWithPoint(world, refFace, surfaces, point); if (faces.empty()) return 0; unsigned int light = 0; for (auto faceIter = faces.begin(); faceIter != faces.end(); ++faceIter) { const face_t* face = *faceIter; light += sample_lightmap(world, face, faceBounds.find(face)->second, point) + (0xFF - face->baselight); } return (unsigned char)(light / faces.size()); } // Find the list of all faces that contain the given point, i.e. the point lies on the face's plane and is contained within its polygon bounds std::vector world_facesWithPoint(const world_t* world, Vec3 point) { std::vector faces; for (int faceIdx = 0; faceIdx < world->numFaces; ++faceIdx) { const face_t* face = &world->faces[faceIdx]; const plane_t* plane = &world->planes[face->plane_id]; // Check if the point lies on the face's plane (it's not strictly necessary to do this, but this check makes the whole function a lot faster) double pointPlaneDist = ((double)point.x * plane->normal.x + (double)point.y * plane->normal.y + (double)point.z * plane->normal.z) - plane->dist; if (fabs(pointPlaneDist) > 0.0001) continue; // Check if the point is contained within the face's polygon double angleSum = 0; for (int edgeListIdx = 0; edgeListIdx < face->ledge_num; ++edgeListIdx) { int edgeIdx = world->edgeList[face->ledge_id + edgeListIdx]; Vec3 v0, v1; if (edgeIdx > 0) { const edge_t* edge = &world->edges[edgeIdx]; v0 = world->vertices[edge->vertex0].toVec(); v1 = world->vertices[edge->vertex1].toVec(); } else { const edge_t* edge = &world->edges[-edgeIdx]; v0 = world->vertices[edge->vertex1].toVec(); v1 = world->vertices[edge->vertex0].toVec(); } Vec3 p0 = v0 - point; Vec3 p1 = v1 - point; double m0 = p0.magnitude(); double m1 = p1.magnitude(); if ((m0 * m1) <= 0.0001) { faces.push_back(face); break; } double dot = p0.dotProduct(p1) / (m0 * m1); angleSum += acos(dot); } if (fabs(2 * M_PI - angleSum) <= 0.0001) faces.push_back(face); } return faces; } unsigned char compute_faceVertex_light4(const world_t* world, const face_t* refFace, const FaceBounds& faceBounds, Vec3 point) { auto faces = world_facesWithPoint(world, point); if (faces.empty()) return 0; const plane_t* refPlane = &world->planes[refFace->plane_id]; vec3_t refNormal = refFace->side ? -refPlane->normal : refPlane->normal; unsigned int light = 0, numSamples = 0; for (auto faceIter = faces.begin(); faceIter != faces.end(); ++faceIter) { const face_t* face = *faceIter; const plane_t* plane = &world->planes[face->plane_id]; vec3_t normal = face->side ? -plane->normal : plane->normal; // Check if the face is at a shallow angle with the reference face float dot = normal.dotProduct(refNormal); if (dot < 0.5f) continue; light += sample_lightmap(world, face, faceBounds.find(face)->second, point) + (0xFF - face->baselight); numSamples++; } // 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); }