diff --git a/src/bsp/Bsp.cpp b/src/bsp/Bsp.cpp index e8317e2f..66754992 100644 --- a/src/bsp/Bsp.cpp +++ b/src/bsp/Bsp.cpp @@ -13,6 +13,7 @@ #include "forcecrc32.h" #include "quantizer.h" #include "Wad.h" +#include "Clipper.h" #include "JACK_jmf.h" #include "XASH_csm.h" @@ -138,6 +139,7 @@ Bsp::Bsp() bsp_header = BSPHEADER(); bsp_header_ex = BSPHEADER_EX(); parentMap = NULL; + pvsFaces = NULL; lumps = new unsigned char* [HEADER_LUMPS]; memset(lumps, 0, sizeof(unsigned char*) * HEADER_LUMPS); @@ -178,7 +180,7 @@ Bsp::Bsp(std::string fpath) bsp_header = BSPHEADER(); bsp_header_ex = BSPHEADER_EX(); parentMap = NULL; - + pvsFaces = NULL; save_cam_pos = save_cam_angles = {}; if (fpath.empty()) @@ -390,6 +392,10 @@ Bsp::~Bsp() replacedLump[i] = false; } + if (pvsFaces) + { + delete[] pvsFaces; + } //if (mdl) //{ // delete mdl; @@ -416,39 +422,58 @@ void Bsp::get_bounding_box(vec3& mins, vec3& maxs) } } -bool Bsp::get_model_vertex_bounds(int modelIdx, vec3& mins, vec3& maxs) +void Bsp::get_model_vertex_bounds(int modelIdx, vec3& mins, vec3& maxs) { - if (modelIdx < 0) - return false; - mins = vec3(FLT_MAX_COORD, FLT_MAX_COORD, FLT_MAX_COORD); - maxs = vec3(-FLT_MAX_COORD, -FLT_MAX_COORD, -FLT_MAX_COORD); + mins = vec3(FLT_MAX, FLT_MAX, FLT_MAX); + maxs = vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); - if (modelIdx == 0) - { - get_bounding_box(mins, maxs); - return false; - } + BSPMODEL& model = models[modelIdx]; - //BSPMODEL& model = models[modelIdx]; - std::vector rnd_verts = getModelTransformVerts(modelIdx); - std::vector rnd_plane_verts; - getModelPlaneIntersectVerts(modelIdx, rnd_plane_verts); + for (int i = 0; i < model.nFaces; i++) { + BSPFACE32& face = faces[model.iFirstFace + i]; - if (rnd_verts.size() == 0 && rnd_plane_verts.size() == 0) - { - return false; - } + for (int e = 0; e < face.nEdges; e++) { + int edgeIdx = surfedges[face.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx > 0 ? edge.iVertex[0] : edge.iVertex[1]; - for (auto const& s : rnd_verts) - { - expandBoundingBox(s.pos, mins, maxs); - } - for (auto const& s : rnd_plane_verts) - { - expandBoundingBox(s.pos, mins, maxs); + expandBoundingBox(verts[vertIdx], mins, maxs); + } } - return true; + if (!model.nFaces) { + // use the clipping hull "faces" instead + Clipper clipper; + std::vector solidNodes = get_model_leaf_volume_cuts(modelIdx, 0, CONTENTS_SOLID); + + std::vector solidMeshes; + for (int k = 0; k < solidNodes.size(); k++) { + solidMeshes.push_back(clipper.clip(solidNodes[k].cuts)); + } + + for (int m = 0; m < solidMeshes.size(); m++) { + CMesh& mesh = solidMeshes[m]; + + for (int i = 0; i < mesh.faces.size(); i++) { + + if (!mesh.faces[i].visible) { + continue; + } + + std::set uniqueFaceVerts; + + for (int k = 0; k < mesh.faces[i].edges.size(); k++) { + for (int v = 0; v < 2; v++) { + int vertIdx = mesh.edges[mesh.faces[i].edges[k]].verts[v]; + if (!mesh.verts[vertIdx].visible) { + continue; + } + expandBoundingBox(mesh.verts[vertIdx].pos, mins, maxs); + } + } + } + } + } } std::vector Bsp::getModelVertsIds(int modelIdx) @@ -648,8 +673,7 @@ void Bsp::getClipNodePlanes(int iClipNode, std::vector& nodePlanes) } } -std::vector Bsp::get_model_leaf_volume_cuts(int modelIdx, int hullIdx) -{ +std::vector Bsp::get_model_leaf_volume_cuts(int modelIdx, int hullIdx, int contents) { std::vector modelVolumeCuts; if (hullIdx >= 0 && hullIdx < MAX_MAP_HULLS) @@ -657,73 +681,54 @@ std::vector Bsp::get_model_leaf_volume_cuts(int modelIdx, int hu int nodeIdx = models[modelIdx].iHeadnodes[hullIdx]; bool is_valid_node = false; - if (hullIdx == 0) - { + if (hullIdx == 0) { is_valid_node = nodeIdx >= 0 && nodeIdx < nodeCount; } - else - { + else { is_valid_node = nodeIdx >= 0 && nodeIdx < clipnodeCount; } - if (nodeIdx >= 0 && is_valid_node) - { + if (nodeIdx >= 0 && is_valid_node) { std::vector clipOrder; - if (hullIdx == 0) - { - get_node_leaf_cuts(nodeIdx, 0, clipOrder, modelVolumeCuts); + if (hullIdx == 0) { + get_node_leaf_cuts(nodeIdx, clipOrder, modelVolumeCuts, contents); } - else - { - get_clipnode_leaf_cuts(nodeIdx, 0, clipOrder, modelVolumeCuts); + else { + get_clipnode_leaf_cuts(nodeIdx, clipOrder, modelVolumeCuts, contents); } } } return modelVolumeCuts; } -void Bsp::get_clipnode_leaf_cuts(int iNode, int iStartNode, std::vector& clipOrder, std::vector& output) -{ +void Bsp::get_clipnode_leaf_cuts(int iNode, std::vector& clipOrder, std::vector& output, int contents) { BSPCLIPNODE32& node = clipnodes[iNode]; - if (node.iPlane < 0 || node.iPlane >= planeCount) - { + if (node.iPlane < 0) { return; } - for (int i = 0; i < 2; i++) - { + for (int i = 0; i < 2; i++) { BSPPLANE plane = planes[node.iPlane]; - if (i != 0) - { + if (i != 0) { plane.vNormal = plane.vNormal.invert(); plane.fDist = -plane.fDist; } - if (node.iChildren[i] == iStartNode) - { - print_log(get_localized_string(LANG_0043), node.iChildren[i]); - return; - } clipOrder.push_back(plane); - if (node.iChildren[i] >= 0) - { - get_clipnode_leaf_cuts(node.iChildren[i], iStartNode, clipOrder, output); + if (node.iChildren[i] >= 0) { + get_clipnode_leaf_cuts(node.iChildren[i], clipOrder, output, contents); } - else if (node.iChildren[i] != CONTENTS_EMPTY) - { + else if (node.iChildren[i] == contents) { NodeVolumeCuts nodeVolumeCuts; nodeVolumeCuts.nodeIdx = iNode; - if (clipOrder.size()) - { - // reverse order of branched planes = order of cuts to the world which define this node's volume - // https://qph.fs.quoracdn.net/main-qimg-2a8faad60cc9d437b58a6e215e6e874d - for (int k = (int)clipOrder.size() - 1; k >= 0; k--) - { - nodeVolumeCuts.cuts.push_back(clipOrder[k]); - } + // reverse order of branched planes = order of cuts to the world which define this node's volume + // https://qph.fs.quoracdn.net/main-qimg-2a8faad60cc9d437b58a6e215e6e874d + for (int k = (int)clipOrder.size() - 1; k >= 0; k--) { + nodeVolumeCuts.cuts.push_back(clipOrder[k]); } + output.push_back(nodeVolumeCuts); } @@ -731,60 +736,30 @@ void Bsp::get_clipnode_leaf_cuts(int iNode, int iStartNode, std::vector& out_nodes) -{ - for (int i = 0; i < nodeCount; i++) - { - if (~nodes[i].iChildren[0] == leaf) - { - out_nodes.push_back(i); - } - if (~nodes[i].iChildren[1] == leaf) - { - out_nodes.push_back(i); - } - } -} - -void Bsp::get_node_leaf_cuts(int iNode, int iStartNode, std::vector& clipOrder, std::vector& output) -{ +void Bsp::get_node_leaf_cuts(int iNode, std::vector& clipOrder, std::vector& output, int contents) { BSPNODE32& node = nodes[iNode]; - for (int i = 0; i < 2; i++) - { + for (int i = 0; i < 2; i++) { BSPPLANE plane = planes[node.iPlane]; - if (i != 0) - { + if (i != 0) { plane.vNormal = plane.vNormal.invert(); plane.fDist = -plane.fDist; } - - if (node.iChildren[i] == iStartNode) - { - print_log(get_localized_string(LANG_0044), node.iChildren[i]); - return; - } - clipOrder.push_back(plane); - if (node.iChildren[i] >= 0) - { - get_node_leaf_cuts(node.iChildren[i], iStartNode, clipOrder, output); + if (node.iChildren[i] >= 0) { + get_node_leaf_cuts(node.iChildren[i], clipOrder, output, contents); } - else if (leaves[~node.iChildren[i]].nContents != CONTENTS_EMPTY) - { + else if (leaves[~node.iChildren[i]].nContents == contents) { NodeVolumeCuts nodeVolumeCuts; nodeVolumeCuts.nodeIdx = iNode; - if (clipOrder.size()) - { - // reverse order of branched planes = order of cuts to the world which define this node's volume - // https://qph.fs.quoracdn.net/main-qimg-2a8faad60cc9d437b58a6e215e6e874d - for (int k = (int)clipOrder.size() - 1; k >= 0; k--) - { - nodeVolumeCuts.cuts.push_back(clipOrder[k]); - } + + // reverse order of branched planes = order of cuts to the world which define this node's volume + // https://qph.fs.quoracdn.net/main-qimg-2a8faad60cc9d437b58a6e215e6e874d + for (int k = (int)clipOrder.size() - 1; k >= 0; k--) { + nodeVolumeCuts.cuts.push_back(clipOrder[k]); } + output.push_back(nodeVolumeCuts); } @@ -792,6 +767,23 @@ void Bsp::get_node_leaf_cuts(int iNode, int iStartNode, std::vector& c } } + + +void Bsp::get_leaf_nodes(int leaf, std::vector& out_nodes) +{ + for (int i = 0; i < nodeCount; i++) + { + if (~nodes[i].iChildren[0] == leaf) + { + out_nodes.push_back(i); + } + if (~nodes[i].iChildren[1] == leaf) + { + out_nodes.push_back(i); + } + } +} + bool Bsp::is_convex(int modelIdx) { return models[modelIdx].iHeadnodes[0] >= 0 && is_node_hull_convex(models[modelIdx].iHeadnodes[0]); @@ -821,6 +813,12 @@ bool Bsp::is_node_hull_convex(int iNode) return true; } +bool Bsp::isInteriorFace(const Polygon3D& poly, int hull) { + int headnode = models[0].iHeadnodes[hull]; + vec3 testPos = poly.center + poly.plane_z * 0.5f; + return pointContents(headnode, testPos, hull) == CONTENTS_EMPTY; +} + int Bsp::addTextureInfo(BSPTEXTUREINFO& copy) { BSPTEXTUREINFO* newInfos = new BSPTEXTUREINFO[texinfoCount + 1]; @@ -1482,7 +1480,7 @@ void Bsp::split_shared_model_structures(int modelIdx) replace_lump(LUMP_CLIPNODES, newClipnodes, newClipnodeCount * sizeof(BSPCLIPNODE32)); replace_lump(LUMP_TEXINFO, newTexinfos, newTexinfoCount * sizeof(BSPTEXTUREINFO)); - std::vector newVisitedClipnodes(newClipnodeCount,false); + std::vector newVisitedClipnodes(newClipnodeCount, false); remappedStuff.visitedClipnodes = newVisitedClipnodes; remap_model_structures(modelIdx, &remappedStuff); @@ -1611,7 +1609,7 @@ void Bsp::replace_lumps(const LumpState& state) update_lump_pointers(); } -unsigned int Bsp::remove_unused_structs(int lumpIdx, std::vector& usedStructs, std::vector & remappedIndexes) +unsigned int Bsp::remove_unused_structs(int lumpIdx, std::vector& usedStructs, std::vector& remappedIndexes) { int structSize = 0; @@ -2647,66 +2645,1672 @@ STRUCTCOUNT Bsp::delete_unused_hulls(bool noProgress) } } - BSPMODEL& model = ((BSPMODEL*)lumps[LUMP_MODELS])[i]; + BSPMODEL& model = ((BSPMODEL*)lumps[LUMP_MODELS])[i]; + + if (!needsVisibleHull && !needsMonsterHulls) + { + if (models[i].iHeadnodes[0] >= 0) + print_log(get_localized_string(LANG_0069), i, uses); + + deletedHulls += models[i].iHeadnodes[0] >= 0; + + model.iHeadnodes[0] = -1; + model.nVisLeafs = 0; + model.nFaces = 0; + model.iFirstFace = 0; + } + if (!needsPlayerHulls && !needsMonsterHulls) + { + bool deletedAnyHulls = false; + for (int k = 1; k < MAX_MAP_HULLS; k++) + { + deletedHulls += models[i].iHeadnodes[k] >= 0; + if (models[i].iHeadnodes[k] >= 0) + { + deletedHulls++; + deletedAnyHulls = true; + } + } + + if (deletedAnyHulls) + print_log(get_localized_string(LANG_0070), i, uses); + + model.iHeadnodes[1] = -1; + model.iHeadnodes[2] = -1; + model.iHeadnodes[3] = -1; + } + else if (!needsMonsterHulls) + { + if (models[i].iHeadnodes[2] >= 0) + print_log(get_localized_string(LANG_0071), i, uses); + + deletedHulls += models[i].iHeadnodes[2] >= 0; + + model.iHeadnodes[2] = -1; + } + else if (!needsPlayerHulls) + { + // monsters use all hulls so can't do anything about this + } + } + + STRUCTCOUNT removed = remove_unused_model_structures(); + + update_ent_lump(); + + if (!noProgress) + { + g_progress.clear(); + g_progress = ProgressMeter(); + + } + + return removed; +} + +void Bsp::delete_oob_nodes(int iNode, int* parentBranch, std::vector& clipOrder, int oobFlags, + bool* oobHistory, bool isFirstPass, int& removedNodes) { + BSPNODE32& node = nodes[iNode]; + float oob_coord = MAX_MAP_BOUNDARY; + + if (node.iPlane < 0) { + return; + } + + bool isoob = isFirstPass ? true : oobHistory[iNode]; + + for (int i = 0; i < 2; i++) { + BSPPLANE plane = planes[node.iPlane]; + if (i != 0) { + plane.vNormal = plane.vNormal.invert(); + plane.fDist = -plane.fDist; + } + clipOrder.push_back(plane); + + if (node.iChildren[i] >= 0) { + delete_oob_nodes(node.iChildren[i], &node.iChildren[i], clipOrder, oobFlags, oobHistory, + isFirstPass, removedNodes); + if (node.iChildren[i] >= 0) { + isoob = false; // children weren't empty, so this node isn't empty either + } + } + else if (isFirstPass) { + std::vector cuts; + for (int k = (int)clipOrder.size() - 1; k >= 0; k--) { + cuts.push_back(clipOrder[k]); + } + + Clipper clipper; + CMesh nodeVolume = clipper.clip(cuts); + + for (int k = 0; k < nodeVolume.verts.size(); k++) { + if (!nodeVolume.verts[k].visible) + continue; + vec3 v = nodeVolume.verts[k].pos; + + bool oobx0 = (oobFlags & OOB_CLIP_X) ? (v.x > oob_coord) : false; + bool oobx1 = (oobFlags & OOB_CLIP_X_NEG) ? (v.x < -oob_coord) : false; + bool ooby0 = (oobFlags & OOB_CLIP_Y) ? (v.y > oob_coord) : false; + bool ooby1 = (oobFlags & OOB_CLIP_Y_NEG) ? (v.y < -oob_coord) : false; + bool oobz0 = (oobFlags & OOB_CLIP_Z) ? (v.z > oob_coord) : false; + bool oobz1 = (oobFlags & OOB_CLIP_Z_NEG) ? (v.z < -oob_coord) : false; + + if (!oobx0 && !ooby0 && !oobz0 && !oobx1 && !ooby1 && !oobz1) { + isoob = false; // node can't be empty if both children aren't oob + } + } + } + + clipOrder.pop_back(); + } + + if (isFirstPass) { + // only check if each node is ever considered in bounds, after considering all branches. + // don't remove anything until the entire tree has been scanned + + if (!isoob) { + oobHistory[iNode] = false; + } + } + else if (parentBranch && isoob) { + // we know which nodes are OOB now, so it's safe to unlink this node from the paranet + *parentBranch = CONTENTS_SOLID; + removedNodes++; + } +} + +void Bsp::delete_oob_clipnodes(int iNode, int* parentBranch, std::vector& clipOrder, int oobFlags, + bool* oobHistory, bool isFirstPass, int& removedNodes) { + BSPCLIPNODE32& node = clipnodes[iNode]; + float oob_coord = MAX_MAP_BOUNDARY; + + if (node.iPlane < 0) { + return; + } + + bool isoob = isFirstPass ? true : oobHistory[iNode]; + + for (int i = 0; i < 2; i++) { + BSPPLANE plane = planes[node.iPlane]; + if (i != 0) { + plane.vNormal = plane.vNormal.invert(); + plane.fDist = -plane.fDist; + } + clipOrder.push_back(plane); + + if (node.iChildren[i] >= 0) { + delete_oob_clipnodes(node.iChildren[i], &node.iChildren[i], clipOrder, oobFlags, + oobHistory, isFirstPass, removedNodes); + if (node.iChildren[i] >= 0) { + isoob = false; // children weren't empty, so this node isn't empty either + } + } + else if (isFirstPass) { + std::vector cuts; + for (int k = (int)clipOrder.size() - 1; k >= 0; k--) { + cuts.push_back(clipOrder[k]); + } + + Clipper clipper; + CMesh nodeVolume = clipper.clip(cuts); + + vec3 mins(FLT_MAX, FLT_MAX, FLT_MAX); + vec3 maxs(-FLT_MAX, -FLT_MAX, -FLT_MAX); + + for (int k = 0; k < nodeVolume.verts.size(); k++) { + if (!nodeVolume.verts[k].visible) + continue; + vec3 v = nodeVolume.verts[k].pos; + + expandBoundingBox(v, mins, maxs); + } + + bool oobx0 = (oobFlags & OOB_CLIP_X) ? (mins.x > oob_coord) : false; + bool oobx1 = (oobFlags & OOB_CLIP_X_NEG) ? (maxs.x < -oob_coord) : false; + bool ooby0 = (oobFlags & OOB_CLIP_Y) ? (mins.y > oob_coord) : false; + bool ooby1 = (oobFlags & OOB_CLIP_Y_NEG) ? (maxs.y < -oob_coord) : false; + bool oobz0 = (oobFlags & OOB_CLIP_Z) ? (mins.z > oob_coord) : false; + bool oobz1 = (oobFlags & OOB_CLIP_Z_NEG) ? (maxs.z < -oob_coord) : false; + + if (!oobx0 && !ooby0 && !oobz0 && !oobx1 && !ooby1 && !oobz1) { + isoob = false; // node can't be empty if both children aren't oob + } + } + + clipOrder.pop_back(); + } + + // clipnodes are reused in the BSP tree. Some paths to the same node involve more plane intersections + // than others. So, there will be some paths where the node is considered OOB and others not. If it + // was EVER considered to be within bounds, on any branch, then don't let be stripped. Otherwise you + // end up with broken clipnodes that are expanded too much because a deeper branch was deleted and + // so there are fewer clipping planes to define the volume. This then then leads to players getting + // stuck on shit and unable to escape when touching that region. + + if (isFirstPass) { + // only check if each node is ever considered in bounds, after considering all branches. + // don't remove anything until the entire tree has been scanned + + if (!isoob) { + oobHistory[iNode] = false; + } + } + else if (parentBranch && isoob) { + // we know which nodes are OOB now, so it's safe to unlink this node from the paranet + *parentBranch = CONTENTS_SOLID; + removedNodes++; + } +} + +void Bsp::delete_oob_data(int clipFlags) { + float oob_coord = MAX_MAP_BOUNDARY; + BSPMODEL& worldmodel = models[0]; + + // remove OOB nodes and clipnodes + std::vector clipOrder; + + bool* oobMarks = new bool[nodeCount]; + + // collect oob data, then actually remove the nodes + int removedNodes = 0; + do { + removedNodes = 0; + memset(oobMarks, 1, nodeCount * sizeof(bool)); // assume everything is oob at first + delete_oob_nodes(worldmodel.iHeadnodes[0], NULL, clipOrder, clipFlags, oobMarks, true, removedNodes); + delete_oob_nodes(worldmodel.iHeadnodes[0], NULL, clipOrder, clipFlags, oobMarks, false, removedNodes); + } while (removedNodes); + delete[] oobMarks; + + oobMarks = new bool[clipnodeCount]; + for (int i = 1; i < MAX_MAP_HULLS; i++) { + // collect oob data, then actually remove the nodes + removedNodes = 0; + do { + removedNodes = 0; + memset(oobMarks, 1, clipnodeCount * sizeof(bool)); // assume everything is oob at first + delete_oob_clipnodes(worldmodel.iHeadnodes[i], NULL, clipOrder, clipFlags, oobMarks, true, removedNodes); + delete_oob_clipnodes(worldmodel.iHeadnodes[i], NULL, clipOrder, clipFlags, oobMarks, false, removedNodes); + } while (removedNodes); + } + delete[] oobMarks; + + + std::vector newEnts; + newEnts.push_back(ents[0]); // never remove worldspawn + + for (int i = 1; i < ents.size(); i++) { + vec3 v = ents[i]->origin; + int modelIdx = ents[i]->getBspModelIdx(); + + if (modelIdx != -1) { + vec3 mins, maxs; + get_model_vertex_bounds(modelIdx, mins, maxs); + mins += v; + maxs += v; + + bool oobx0 = (clipFlags & OOB_CLIP_X) ? (mins.x > oob_coord) : false; + bool oobx1 = (clipFlags & OOB_CLIP_X_NEG) ? (maxs.x < -oob_coord) : false; + bool ooby0 = (clipFlags & OOB_CLIP_Y) ? (mins.y > oob_coord) : false; + bool ooby1 = (clipFlags & OOB_CLIP_Y_NEG) ? (maxs.y < -oob_coord) : false; + bool oobz0 = (clipFlags & OOB_CLIP_Z) ? (mins.z > oob_coord) : false; + bool oobz1 = (clipFlags & OOB_CLIP_Z_NEG) ? (maxs.z < -oob_coord) : false; + + if (!oobx0 && !ooby0 && !oobz0 && !oobx1 && !ooby1 && !oobz1) { + newEnts.push_back(ents[i]); + } + } + else { + bool oobx0 = (clipFlags & OOB_CLIP_X) ? (v.x > oob_coord) : false; + bool oobx1 = (clipFlags & OOB_CLIP_X_NEG) ? (v.x < -oob_coord) : false; + bool ooby0 = (clipFlags & OOB_CLIP_Y) ? (v.y > oob_coord) : false; + bool ooby1 = (clipFlags & OOB_CLIP_Y_NEG) ? (v.y < -oob_coord) : false; + bool oobz0 = (clipFlags & OOB_CLIP_Z) ? (v.z > oob_coord) : false; + bool oobz1 = (clipFlags & OOB_CLIP_Z_NEG) ? (v.z < -oob_coord) : false; + + if (!oobx0 && !ooby0 && !oobz0 && !oobx1 && !ooby1 && !oobz1) { + newEnts.push_back(ents[i]); + } + } + + } + int deletedEnts = (int)ents.size() - (int)newEnts.size(); + if (deletedEnts) + print_log(" Deleted {} entities\n", deletedEnts); + ents = newEnts; + + uint8_t* oobFaces = new uint8_t[faceCount]; + memset(oobFaces, 0, faceCount * sizeof(bool)); + int oobFaceCount = 0; + + for (int i = 0; i < worldmodel.nFaces; i++) { + BSPFACE32& face = faces[worldmodel.iFirstFace + i]; + + bool inBounds = true; + for (int e = 0; e < face.nEdges; e++) { + int edgeIdx = surfedges[face.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + + vec3 v = verts[vertIdx]; + + bool oobx0 = (clipFlags & OOB_CLIP_X) ? (v.x > oob_coord) : false; + bool oobx1 = (clipFlags & OOB_CLIP_X_NEG) ? (v.x < -oob_coord) : false; + bool ooby0 = (clipFlags & OOB_CLIP_Y) ? (v.y > oob_coord) : false; + bool ooby1 = (clipFlags & OOB_CLIP_Y_NEG) ? (v.y < -oob_coord) : false; + bool oobz0 = (clipFlags & OOB_CLIP_Z) ? (v.z > oob_coord) : false; + bool oobz1 = (clipFlags & OOB_CLIP_Z_NEG) ? (v.z < -oob_coord) : false; + + if (oobx0 || ooby0 || oobz0 || oobx1 || ooby1 || oobz1) { + inBounds = false; + break; + } + } + + if (!inBounds) { + oobFaces[worldmodel.iFirstFace + i] = 1; + oobFaceCount++; + } + } + + BSPFACE32* newFaces = new BSPFACE32[faceCount - oobFaceCount]; + + int outIdx = 0; + for (int i = 0; i < faceCount; i++) { + if (!oobFaces[i]) { + newFaces[outIdx++] = faces[i]; + } + } + + for (int i = 0; i < modelCount; i++) { + BSPMODEL& model = models[i]; + + int offset = 0; + int countReduce = 0; + + for (int k = 0; k < model.iFirstFace; k++) { + offset += oobFaces[k]; + } + for (int k = 0; k < model.nFaces; k++) { + countReduce += oobFaces[model.iFirstFace + k]; + } + + model.iFirstFace -= offset; + model.nFaces -= countReduce; + } + + for (int i = 0; i < nodeCount; i++) { + BSPNODE32& node = nodes[i]; + + int offset = 0; + int countReduce = 0; + + for (int k = 0; k < node.iFirstFace; k++) { + offset += oobFaces[k]; + } + for (int k = 0; k < node.nFaces; k++) { + countReduce += oobFaces[node.iFirstFace + k]; + } + + node.iFirstFace -= offset; + node.nFaces -= countReduce; + } + + for (int i = 0; i < leafCount; i++) { + BSPLEAF32& leaf = leaves[i]; + + if (!leaf.nMarkSurfaces) + continue; + + int oobCount = 0; + + for (int k = 0; k < leaf.nMarkSurfaces; k++) { + if (oobFaces[marksurfs[leaf.iFirstMarkSurface + k]]) { + oobCount++; + } + } + + if (oobCount) { + leaf.nMarkSurfaces = 0; + leaf.iFirstMarkSurface = 0; + + if (oobCount != leaf.nMarkSurfaces) { + //print_log("leaf {} partially OOB\n", i); + } + } + else { + for (int k = 0; k < leaf.nMarkSurfaces; k++) { + unsigned int faceIdx = marksurfs[leaf.iFirstMarkSurface + k]; + + int offset = 0; + for (int j = 0; j < faceIdx; j++) { + offset += oobFaces[j]; + } + + marksurfs[leaf.iFirstMarkSurface + k] = faceIdx - offset; + } + } + } + + replace_lump(LUMP_FACES, newFaces, (faceCount - oobFaceCount) * sizeof(BSPFACE32)); + + delete[] oobFaces; + + worldmodel = models[0]; + + vec3 mins, maxs; + get_model_vertex_bounds(0, mins, maxs); + + vec3 buffer = vec3(64, 64, 128); // leave room for largest collision hull wall thickness + worldmodel.nMins = mins - buffer; + worldmodel.nMaxs = maxs + buffer; + + remove_unused_model_structures().print_delete_stats(1); +} + + +void Bsp::delete_box_nodes(int iNode, int* parentBranch, std::vector& clipOrder, + vec3 clipMins, vec3 clipMaxs, bool* oobHistory, bool isFirstPass, int& removedNodes) { + BSPNODE32& node = nodes[iNode]; + + if (node.iPlane < 0) { + return; + } + + bool isoob = isFirstPass ? true : oobHistory[iNode]; + + for (int i = 0; i < 2; i++) { + BSPPLANE plane = planes[node.iPlane]; + if (i != 0) { + plane.vNormal = plane.vNormal.invert(); + plane.fDist = -plane.fDist; + } + clipOrder.push_back(plane); + + if (node.iChildren[i] >= 0) { + delete_box_nodes(node.iChildren[i], &node.iChildren[i], clipOrder, clipMins, clipMaxs, + oobHistory, isFirstPass, removedNodes); + if (node.iChildren[i] >= 0) { + isoob = false; // children weren't empty, so this node isn't empty either + } + } + else if (isFirstPass) { + std::vector cuts; + for (int k = (int)clipOrder.size() - 1; k >= 0; k--) { + cuts.push_back(clipOrder[k]); + } + + Clipper clipper; + CMesh nodeVolume = clipper.clip(cuts); + + for (int k = 0; k < nodeVolume.verts.size(); k++) { + if (!nodeVolume.verts[k].visible) + continue; + vec3 v = nodeVolume.verts[k].pos; + + if (!pointInBox(v, clipMins, clipMaxs)) { + isoob = false; // node can't be empty if both children aren't oob + } + } + } + + clipOrder.pop_back(); + } + + if (isFirstPass) { + // only check if each node is ever considered in bounds, after considering all branches. + // don't remove anything until the entire tree has been scanned + + if (!isoob) { + oobHistory[iNode] = false; + } + } + else if (parentBranch && isoob) { + // we know which nodes are OOB now, so it's safe to unlink this node from the paranet + *parentBranch = CONTENTS_SOLID; + removedNodes++; + } +} + +void Bsp::delete_box_clipnodes(int iNode, int* parentBranch, std::vector& clipOrder, + vec3 clipMins, vec3 clipMaxs, bool* oobHistory, bool isFirstPass, int& removedNodes) { + BSPCLIPNODE32& node = clipnodes[iNode]; + + if (node.iPlane < 0) { + return; + } + + bool isoob = isFirstPass ? true : oobHistory[iNode]; + + for (int i = 0; i < 2; i++) { + BSPPLANE plane = planes[node.iPlane]; + if (i != 0) { + plane.vNormal = plane.vNormal.invert(); + plane.fDist = -plane.fDist; + } + clipOrder.push_back(plane); + + if (node.iChildren[i] >= 0) { + delete_box_clipnodes(node.iChildren[i], &node.iChildren[i], clipOrder, clipMins, clipMaxs, + oobHistory, isFirstPass, removedNodes); + if (node.iChildren[i] >= 0) { + isoob = false; // children weren't empty, so this node isn't empty either + } + } + else if (isFirstPass) { + std::vector cuts; + for (int k = (int)clipOrder.size() - 1; k >= 0; k--) { + cuts.push_back(clipOrder[k]); + } + + Clipper clipper; + CMesh nodeVolume = clipper.clip(cuts); + + vec3 mins(FLT_MAX, FLT_MAX, FLT_MAX); + vec3 maxs(-FLT_MAX, -FLT_MAX, -FLT_MAX); + + for (int k = 0; k < nodeVolume.verts.size(); k++) { + if (!nodeVolume.verts[k].visible) + continue; + vec3 v = nodeVolume.verts[k].pos; + + expandBoundingBox(v, mins, maxs); + } + + if (!boxesIntersect(mins, maxs, clipMins, clipMaxs)) { + isoob = false; // node can't be empty if both children aren't in the clip box + } + } + + clipOrder.pop_back(); + } + + if (isFirstPass) { + // only check if each node is ever considered in bounds, after considering all branches. + // don't remove anything until the entire tree has been scanned + + if (!isoob) { + oobHistory[iNode] = false; + } + } + else if (parentBranch && isoob) { + // we know which nodes are OOB now, so it's safe to unlink this node from the paranet + *parentBranch = CONTENTS_SOLID; + removedNodes++; + } +} + +void Bsp::delete_box_data(vec3 clipMins, vec3 clipMaxs) { + // TODO: most of this code is duplicated in delete_oob_* + + BSPMODEL& worldmodel = models[0]; + + // remove nodes and clipnodes in the clipping box + { + std::vector clipOrder; + + bool* oobMarks = new bool[nodeCount]; + + // collect oob data, then actually remove the nodes + int removedNodes = 0; + do { + removedNodes = 0; + memset(oobMarks, 1, nodeCount * sizeof(bool)); // assume everything is oob at first + delete_box_nodes(worldmodel.iHeadnodes[0], NULL, clipOrder, clipMins, clipMaxs, oobMarks, true, removedNodes); + delete_box_nodes(worldmodel.iHeadnodes[0], NULL, clipOrder, clipMins, clipMaxs, oobMarks, false, removedNodes); + } while (removedNodes); + delete[] oobMarks; + + oobMarks = new bool[clipnodeCount]; + for (int i = 1; i < MAX_MAP_HULLS; i++) { + // collect oob data, then actually remove the nodes + removedNodes = 0; + do { + removedNodes = 0; + memset(oobMarks, 1, clipnodeCount * sizeof(bool)); // assume everything is oob at first + delete_box_clipnodes(worldmodel.iHeadnodes[i], NULL, clipOrder, clipMins, clipMaxs, oobMarks, true, removedNodes); + delete_box_clipnodes(worldmodel.iHeadnodes[i], NULL, clipOrder, clipMins, clipMaxs, oobMarks, false, removedNodes); + } while (removedNodes); + } + delete[] oobMarks; + } + + std::vector newEnts; + newEnts.push_back(ents[0]); // never remove worldspawn + + for (int i = 1; i < ents.size(); i++) { + vec3 v = ents[i]->origin; + int modelIdx = ents[i]->getBspModelIdx(); + + if (modelIdx != -1) { + vec3 mins, maxs; + get_model_vertex_bounds(modelIdx, mins, maxs); + mins += v; + maxs += v; + + if (!boxesIntersect(mins, maxs, clipMins, clipMaxs)) { + newEnts.push_back(ents[i]); + } + } + else { + bool isCullEnt = ents[i]->hasKey("classname") && ents[i]->keyvalues["classname"] == "cull"; + if (!pointInBox(v, clipMins, clipMaxs) || isCullEnt) { + newEnts.push_back(ents[i]); + } + } + + } + int deletedEnts = (int)ents.size() - (int)newEnts.size(); + if (deletedEnts) + print_log(" Deleted {} entities\n", deletedEnts); + ents = newEnts; + + uint8_t* oobFaces = new uint8_t[faceCount]; + memset(oobFaces, 0, faceCount * sizeof(bool)); + int oobFaceCount = 0; + + for (int i = 0; i < worldmodel.nFaces; i++) { + BSPFACE32& face = faces[worldmodel.iFirstFace + i]; + + bool isClipped = false; + for (int e = 0; e < face.nEdges; e++) { + int edgeIdx = surfedges[face.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + + vec3 v = verts[vertIdx]; + + if (pointInBox(v, clipMins, clipMaxs)) { + isClipped = true; + break; + } + } + + if (isClipped) { + oobFaces[worldmodel.iFirstFace + i] = 1; + oobFaceCount++; + } + } + + BSPFACE32* newFaces = new BSPFACE32[faceCount - oobFaceCount]; + + int outIdx = 0; + for (int i = 0; i < faceCount; i++) { + if (!oobFaces[i]) { + newFaces[outIdx++] = faces[i]; + } + } + + for (int i = 0; i < modelCount; i++) { + BSPMODEL& model = models[i]; + + int offset = 0; + int countReduce = 0; + + for (int k = 0; k < model.iFirstFace; k++) { + offset += oobFaces[k]; + } + for (int k = 0; k < model.nFaces; k++) { + countReduce += oobFaces[model.iFirstFace + k]; + } + + model.iFirstFace -= offset; + model.nFaces -= countReduce; + } + + for (int i = 0; i < nodeCount; i++) { + BSPNODE32& node = nodes[i]; + + int offset = 0; + int countReduce = 0; + + for (int k = 0; k < node.iFirstFace; k++) { + offset += oobFaces[k]; + } + for (int k = 0; k < node.nFaces; k++) { + countReduce += oobFaces[node.iFirstFace + k]; + } + + node.iFirstFace -= offset; + node.nFaces -= countReduce; + } + + for (int i = 0; i < leafCount; i++) { + BSPLEAF32& leaf = leaves[i]; + + if (!leaf.nMarkSurfaces) + continue; + + int oobCount = 0; + + for (int k = 0; k < leaf.nMarkSurfaces; k++) { + if (oobFaces[marksurfs[leaf.iFirstMarkSurface + k]]) { + oobCount++; + } + } + + if (oobCount) { + leaf.nMarkSurfaces = 0; + leaf.iFirstMarkSurface = 0; + + if (oobCount != leaf.nMarkSurfaces) { + //print_log("leaf {} partially OOB\n", i); + } + } + else { + for (int k = 0; k < leaf.nMarkSurfaces; k++) { + unsigned int faceIdx = marksurfs[leaf.iFirstMarkSurface + k]; + + int offset = 0; + for (int j = 0; j < faceIdx; j++) { + offset += oobFaces[j]; + } + + marksurfs[leaf.iFirstMarkSurface + k] = faceIdx - offset; + } + } + } + + replace_lump(LUMP_FACES, newFaces, (faceCount - oobFaceCount) * sizeof(BSPFACE32)); + + delete[] oobFaces; + + worldmodel = models[0]; + + vec3 mins, maxs; + get_model_vertex_bounds(0, mins, maxs); + + vec3 buffer = vec3(64, 64, 128); // leave room for largest collision hull wall thickness + worldmodel.nMins = mins - buffer; + worldmodel.nMaxs = maxs + buffer; + + remove_unused_model_structures().print_delete_stats(1); +} + +void Bsp::count_leaves(int iNode, int& count) { + BSPNODE32& node = nodes[iNode]; + + for (int i = 0; i < 2; i++) { + if (node.iChildren[i] >= 0) { + count_leaves(node.iChildren[i], count); + } + else { + int leafIdx = ~node.iChildren[i]; + if (leafIdx > count) + count = leafIdx; + } + } +} + +struct CompareVert { + vec3 pos; + float u, v; +}; + +struct ModelIdxRemap { + int newIdx; + vec3 offset; +}; + +void Bsp::deduplicate_models() { + const float epsilon = 1.0f; + + std::map modelRemap; + + for (int i = 1; i < modelCount; i++) { + BSPMODEL& modelA = models[i]; + + if (modelA.nFaces == 0) + continue; + + if (modelRemap.find(i) != modelRemap.end()) { + continue; + } + + bool shouldCompareTextures = false; + std::string modelKeyA = "*" + std::to_string(i); + + for (Entity* ent : ents) { + if (ent->hasKey("model") && ent->keyvalues["model"] == modelKeyA) { + if (ent->isEverVisible()) { + shouldCompareTextures = true; + break; + } + } + } + + for (int k = 1; k < modelCount; k++) { + if (i == k) + continue; + + BSPMODEL& modelB = models[k]; + + if (modelA.nFaces != modelB.nFaces) + continue; + + vec3 minsA, maxsA, minsB, maxsB; + get_model_vertex_bounds(i, minsA, maxsA); + get_model_vertex_bounds(k, minsB, maxsB); + + vec3 sizeA = maxsA - minsA; + vec3 sizeB = maxsB - minsB; + + if ((sizeB - sizeA).length() > epsilon) { + continue; + } + + if (!shouldCompareTextures) { + std::string modelKeyB = "*" + std::to_string(k); + + for (Entity* ent : ents) { + if (ent->hasKey("model") && ent->keyvalues["model"] == modelKeyB) { + if (ent->isEverVisible()) { + shouldCompareTextures = true; + break; + } + } + } + } + + bool similarFaces = true; + for (int fa = 0; fa < modelA.nFaces; fa++) { + BSPFACE32& faceA = faces[modelA.iFirstFace + fa]; + BSPTEXTUREINFO& infoA = texinfos[faceA.iTextureInfo]; + BSPPLANE& planeA = planes[faceA.iPlane]; + int texOffset = ((int*)textures)[infoA.iMiptex + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + float tw = 1.0f / (float)tex.nWidth; + float th = 1.0f / (float)tex.nHeight; + + std::vector vertsA; + for (int e = 0; e < faceA.nEdges; e++) { + int edgeIdx = surfedges[faceA.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + + CompareVert v; + v.pos = verts[vertIdx]; + + float fU = dotProduct(infoA.vS, v.pos) + infoA.shiftS; + float fV = dotProduct(infoA.vT, v.pos) + infoA.shiftT; + v.u = fU * tw; + v.v = fV * th; + + // wrap coords + v.u = v.u > 0 ? (v.u - (int)v.u) : 1.0f - (v.u - (int)v.u); + v.v = v.v > 0 ? (v.v - (int)v.v) : 1.0f - (v.v - (int)v.v); + + vertsA.push_back(v); + //print_log("A Face {} vert {} uv: {} {}\n", fa, e, v.u, v.v); + } + + bool foundMatch = false; + for (int fb = 0; fb < modelB.nFaces; fb++) { + BSPFACE32& faceB = faces[modelB.iFirstFace + fb]; + BSPTEXTUREINFO& infoB = texinfos[faceB.iTextureInfo]; + BSPPLANE& planeB = planes[faceB.iPlane]; + + if ((!shouldCompareTextures || infoA.iMiptex == infoB.iMiptex) + && planeA.vNormal == planeB.vNormal + && faceA.nPlaneSide == faceB.nPlaneSide) { + // face planes and textures match + // now check if vertices have same relative positions and texture coords + + std::vector vertsB; + for (int e = 0; e < faceB.nEdges; e++) { + int edgeIdx = surfedges[faceB.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + + CompareVert v; + v.pos = verts[vertIdx]; + + float fU = dotProduct(infoB.vS, v.pos) + infoB.shiftS; + float fV = dotProduct(infoB.vT, v.pos) + infoB.shiftT; + v.u = fU * tw; + v.v = fV * th; + + // wrap coords + v.u = v.u > 0 ? (v.u - (int)v.u) : 1.0f - (v.u - (int)v.u); + v.v = v.v > 0 ? (v.v - (int)v.v) : 1.0f - (v.v - (int)v.v); + + vertsB.push_back(v); + //print_log("B Face {} vert {} uv: {} {}\n", fb, e, v.u, v.v); + } + + bool vertsMatch = true; + for (CompareVert& vertA : vertsA) { + bool foundVertMatch = false; + + for (CompareVert& vertB : vertsB) { + + float diffU = fabs(vertA.u - vertB.u); + float diffV = fabs(vertA.v - vertB.v); + const float uvEpsilon = 0.005f; + + bool uvsMatch = !shouldCompareTextures || + ((diffU < uvEpsilon || fabs(diffU - 1.0f) < uvEpsilon) + && (diffV < uvEpsilon || fabs(diffV - 1.0f) < uvEpsilon)); + + if (((vertA.pos - minsA) - (vertB.pos - minsB)).length() < epsilon + && uvsMatch) { + foundVertMatch = true; + break; + } + } + + if (!foundVertMatch) { + vertsMatch = false; + break; + } + } + + if (vertsMatch) { + foundMatch = true; + break; + } + } + } + + if (!foundMatch) { + similarFaces = false; + break; + } + } + + if (!similarFaces) + continue; + + //print_log("Model {} and {} seem very similar ({} faces)\n", i, k, modelA.nFaces); + ModelIdxRemap remap; + remap.newIdx = i; + remap.offset = minsB - minsA; + modelRemap[k] = remap; + } + } + + print_log("Remapped {} BSP model references\n", modelRemap.size()); + + for (Entity* ent : ents) { + if (!ent->keyvalues.count("model")) { + continue; + } + + std::string model = ent->keyvalues["model"]; + + if (model[0] != '*') + continue; + + int modelIdx = atoi(model.substr(1).c_str()); + + if (modelRemap.find(modelIdx) != modelRemap.end()) { + ModelIdxRemap remap = modelRemap[modelIdx]; + + ent->setOrAddKeyvalue("origin", (ent->origin + remap.offset).toKeyvalueString()); + ent->setOrAddKeyvalue("model", "*" + std::to_string(remap.newIdx)); + } + } +} + +float Bsp::calc_allocblock_usage() { + int total = 0; + + for (int i = 0; i < faceCount; i++) { + int size[2]; + GetFaceLightmapSize(i, size); + + total += size[0] * size[1]; + } + + const int allocBlockSize = 128 * 128; + + return total / (float)allocBlockSize; +} + +void Bsp::allocblock_reduction() { + int scaleCount = 0; + + for (int i = 1; i < modelCount; i++) { + BSPMODEL& model = models[i]; + + if (model.nFaces == 0) + continue; + + bool isVisibleModel = false; + std::string modelKey = "*" + std::to_string(i); + + for (Entity* ent : ents) { + if (ent->hasKey("model") && ent->keyvalues["model"] == modelKey) { + if (ent->isEverVisible()) { + isVisibleModel = true; + break; + } + } + } + + if (isVisibleModel) + continue; + for (int fa = 0; fa < model.nFaces; fa++) { + BSPFACE32& face = faces[model.iFirstFace + fa]; + BSPTEXTUREINFO& info = texinfos[face.iTextureInfo]; + info.vS = info.vS.normalize(0.01f); + info.vT = info.vT.normalize(0.01f); + } + + scaleCount++; + print_log("Scale up model {}\n", i); + } + + print_log("Scaled up textures on {} invisible models\n", scaleCount); +} + +bool Bsp::subdivide_face(int faceIdx) { + BSPFACE32& face = faces[faceIdx]; + BSPTEXTUREINFO& info = texinfos[face.iTextureInfo]; + + std::vector faceVerts; + for (int e = 0; e < face.nEdges; e++) { + int edgeIdx = surfedges[face.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[0] : edge.iVertex[1]; + + faceVerts.push_back(verts[vertIdx]); + } + + Polygon3D poly(faceVerts); + + vec3 minVertU, maxVertU; + vec3 minVertV, maxVertV; + + float minU = FLT_MAX; + float maxU = -FLT_MAX; + float minV = FLT_MAX; + float maxV = -FLT_MAX; + for (int i = 0; i < faceVerts.size(); i++) { + vec3& pos = faceVerts[i]; + + float u = dotProduct(info.vS, pos); + float v = dotProduct(info.vT, pos); + + if (u < minU) { + minU = u; + minVertU = pos; + } + if (u > maxU) { + maxU = u; + maxVertU = pos; + } + if (v < minV) { + minV = v; + minVertV = pos; + } + if (v > maxV) { + maxV = v; + maxVertV = pos; + } + } + vec2 axisU = poly.project(info.vS).normalize(); + vec2 axisV = poly.project(info.vT).normalize(); + + vec2 midVertU = poly.project(minVertU + (maxVertU - minVertU) * 0.5f); + vec2 midVertV = poly.project(minVertV + (maxVertV - minVertV) * 0.5f); + + Line2D ucut(midVertU + axisV * 1000.0f, midVertU + axisV * -1000.0f); + Line2D vcut(midVertV + axisU * 1000.0f, midVertV + axisU * -1000.0f); + + int size[2]; + GetFaceLightmapSize(faceIdx, size); + + Line2D& cutLine = size[0] > size[1] ? ucut : vcut; + + std::vector> polys = poly.cut(cutLine); + + if (polys.empty()) { + int texOffset = ((int*)textures)[info.iMiptex + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + vec3 center = get_face_center(faceIdx); + print_log("Failed to subdivide face {} {} ({} {} {})\n", faceIdx, tex.szName, + (int)center.x, (int)center.y, (int)center.z); + return false; + } + + size_t addVerts = polys[0].size() + polys[1].size(); + + BSPFACE32* newFaces = new BSPFACE32[faceCount + 1]; + memcpy(newFaces, faces, faceIdx * sizeof(BSPFACE32)); + memcpy(newFaces + faceIdx + 1, faces + faceIdx, (faceCount - faceIdx) * sizeof(BSPFACE32)); + + int addMarks = 0; + for (int i = 0; i < marksurfCount; i++) { + if (marksurfs[i] == faceIdx) { + addMarks++; + } + } + int totalMarks = marksurfCount + addMarks; + int* newMarkSurfs = new int[totalMarks]; + memcpy(newMarkSurfs, marksurfs, marksurfCount * sizeof(int)); + + BSPEDGE32* newEdges = new BSPEDGE32[edgeCount + addVerts]; + memcpy(newEdges, edges, edgeCount * sizeof(BSPEDGE32)); + + vec3* newVerts = new vec3[vertCount + addVerts]; + memcpy(newVerts, verts, vertCount * sizeof(vec3)); + + int* newSurfEdges = new int[surfedgeCount + addVerts]; + memcpy(newSurfEdges, surfedges, surfedgeCount * sizeof(int)); + + BSPEDGE32* edgePtr = newEdges + edgeCount; + vec3* vertPtr = newVerts + vertCount; + int* surfedgePtr = newSurfEdges + surfedgeCount; + + for (int k = 0; k < 2; k++) { + std::vector& cutPoly = polys[k]; + + newFaces[faceIdx + k] = faces[faceIdx]; + newFaces[faceIdx + k].iFirstEdge = surfedgePtr - newSurfEdges; + newFaces[faceIdx + k].nEdges = (int)cutPoly.size(); + + int vertOffset = vertPtr - newVerts; + int edgeOffset = edgePtr - newEdges; + + for (int i = 0; i < cutPoly.size(); i++) { + edgePtr->iVertex[0] = vertOffset + i; + edgePtr->iVertex[1] = vertOffset + ((i + 1) % cutPoly.size()); + edgePtr++; + + *vertPtr++ = cutPoly[i]; + + // TODO: make fewer edges and make use of both vertexes? + *surfedgePtr++ = -(edgeOffset + i); + } + } + + for (int i = 0; i < modelCount; i++) { + BSPMODEL& model = models[i]; + + if (model.iFirstFace > faceIdx) { + model.iFirstFace += 1; + } + else if (model.iFirstFace <= faceIdx && model.iFirstFace + model.nFaces > faceIdx) { + model.nFaces++; + } + } + + for (int i = 0; i < nodeCount; i++) { + BSPNODE32& node = nodes[i]; + + if (node.iFirstFace > faceIdx) { + node.iFirstFace += 1; + } + else if (node.iFirstFace <= faceIdx && node.iFirstFace + node.nFaces > faceIdx) { + node.nFaces++; + } + } + + for (int i = 0; i < totalMarks; i++) + { + if (newMarkSurfs[i] == faceIdx) + { + memmove(newMarkSurfs + i + 1, newMarkSurfs + i, (totalMarks - (i + 1)) * sizeof(unsigned int)); + newMarkSurfs[i + 1] = faceIdx + 1; + + for (int k = 0; k < leafCount; k++) { + BSPLEAF32& leaf = leaves[k]; + + if (!leaf.nMarkSurfaces) + continue; + else if (leaf.iFirstMarkSurface > i) { + leaf.iFirstMarkSurface += 1; + } + else if (leaf.iFirstMarkSurface <= i && leaf.iFirstMarkSurface + leaf.nMarkSurfaces > i) { + //print_log("Added mark {}/{} to leaf {} ({} + {})\n", i, marksurfCount, k, leaf.iFirstMarkSurface, leaf.nMarkSurfaces); + leaf.nMarkSurfaces += 1; + } + } + + i++; // skip the other side of the subdivided face, or else it triggers the next block + } + else if (newMarkSurfs[i] > faceIdx) { + newMarkSurfs[i]++; + } + } + + replace_lump(LUMP_MARKSURFACES, newMarkSurfs, totalMarks * sizeof(unsigned int)); + replace_lump(LUMP_FACES, newFaces, (faceCount + 1) * sizeof(BSPFACE32)); + replace_lump(LUMP_EDGES, newEdges, (edgeCount + addVerts) * sizeof(BSPEDGE32)); + replace_lump(LUMP_SURFEDGES, newSurfEdges, (surfedgeCount + addVerts) * sizeof(int)); + replace_lump(LUMP_VERTICES, newVerts, (vertCount + addVerts) * sizeof(vec3)); + + return true; +} + +void Bsp::fix_bad_surface_extents_with_subdivide(int faceIdx) +{ + // f... ? + std::vector tmpfaces; + tmpfaces.push_back(faceIdx); + + int totalFaces = 1; + + while (tmpfaces.size()) { + int size[2]; + int i = tmpfaces[tmpfaces.size() - 1]; + if (GetFaceLightmapSize(i, size)) { + tmpfaces.pop_back(); + continue; + } + + // adjust face indexes if about to split a face with a lower index + for (int n = 0; n < tmpfaces.size(); n++) { + if (tmpfaces[n] > n) { + tmpfaces[n]++; + } + } + + totalFaces++; + subdivide_face(i); + tmpfaces.push_back(i + 1); + tmpfaces.push_back(i); + } + + print_log("Subdivided into {} faces\n", totalFaces); +} + +void Bsp::fix_bad_surface_extents(bool scaleNotSubdivide, bool downscaleOnly, int maxTextureDim) { + int numSub = 0; + int numScale = 0; + int numShrink = 0; + bool anySubdivides = true; + + if (scaleNotSubdivide) { + // create unique texinfos in case any are shared with both good and bad faces + for (int fa = 0; fa < faceCount; fa++) { + int faceIdx = fa; + BSPFACE32& face = faces[faceIdx]; + BSPTEXTUREINFO& info = texinfos[face.iTextureInfo]; + + if (info.nFlags & TEX_SPECIAL) { + continue; + } + + int size[2]; + if (GetFaceLightmapSize(faceIdx, size)) { + continue; + } + + get_unique_texinfo(faceIdx); + } + } + + while (anySubdivides) { + anySubdivides = false; + for (int fa = 0; fa < faceCount; fa++) { + int faceIdx = fa; + BSPFACE32& face = faces[faceIdx]; + BSPTEXTUREINFO& info = texinfos[face.iTextureInfo]; + + if (info.nFlags & TEX_SPECIAL) { + continue; + } + + int size[2]; + if (GetFaceLightmapSize(faceIdx, size)) { + continue; + } + + if (maxTextureDim > 0 && downscale_texture(info.iMiptex, maxTextureDim, false)) { + // retry after downscaling + numShrink++; + fa--; + continue; + } + + if (downscaleOnly) { + continue; + } + + if (!scaleNotSubdivide) { + if (subdivide_face(faceIdx)) { + numSub++; + anySubdivides = true; + break; + } + // else scale the face because it was too skinny to be subdivided or something + } + + vec2 oldScale(1.0f / info.vS.length(), 1.0f / info.vT.length()); + + bool scaledOk = false; + for (int i = 0; i < 128; i++) { + info.vS *= 0.5f; + info.vT *= 0.5f; + + if (GetFaceLightmapSize(faceIdx, size)) { + scaledOk = true; + break; + } + } + + if (!scaledOk) { + int texOffset = ((int*)textures)[info.iMiptex + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + print_log("Failed to fix face {} with scales {} {}\n", tex.szName, oldScale.x, oldScale.y); + } + else { + int texOffset = ((int*)textures)[info.iMiptex + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + vec2 newScale(1.0f / info.vS.length(), 1.0f / info.vT.length()); + + vec3 center = get_face_center(faceIdx); + print_log("Scaled up {} from {}x{} -> {}x{} ({} {} {})\n", + tex.szName, oldScale.x, oldScale.y, newScale.x, newScale.y, + (int)center.x, (int)center.y, (int)center.z); + numScale++; + } + } + } + + if (numScale) { + print_log("Scaled up {} face textures\n", numScale); + } + if (numSub) { + print_log("Subdivided {} faces\n", numSub); + } + if (numShrink) { + print_log("Downscaled {} textures\n", numShrink); + } +} + +vec3 Bsp::get_face_center(int faceIdx) { + BSPFACE32& face = faces[faceIdx]; + + vec3 centroid; + + for (int k = 0; k < face.nEdges; k++) { + int edgeIdx = surfedges[face.iFirstEdge + k]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + centroid += verts[vertIdx]; + } + + return centroid / (float)face.nEdges; +} + +bool Bsp::downscale_texture(int textureId, int newWidth, int newHeight) { + if ((newWidth % 16 != 0) || (newHeight % 16 != 0) || newWidth <= 0 || newHeight <= 0) { + print_log("Invalid downscale dimensions: {}x{}\n", newWidth, newHeight); + return false; + } + + int texOffset = ((int*)textures)[textureId + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + + int oldWidth = tex.nWidth; + int oldHeight = tex.nHeight; + + tex.nWidth = newWidth; + tex.nHeight = newHeight; + + int lastMipSize = (oldWidth >> 3) * (oldHeight >> 3); + unsigned char* palette = (unsigned char*)(textures + texOffset + tex.nOffsets[3] + lastMipSize); + + int oldWidths[4]; + int oldHeights[4]; + int newWidths[4]; + int newHeights[4]; + int newOffset[4]; + for (int i = 0; i < 4; i++) { + oldWidths[i] = oldWidth >> (1 * i); + oldHeights[i] = oldHeight >> (1 * i); + newWidths[i] = tex.nWidth >> (1 * i); + newHeights[i] = tex.nHeight >> (1 * i); + + if (i > 0) { + newOffset[i] = newOffset[i - 1] + newWidths[i - 1] * newHeights[i - 1]; + } + else { + newOffset[i] = sizeof(BSPMIPTEX); + } + } + unsigned char* newPalette = (unsigned char*)(textures + texOffset + newOffset[3] + newWidths[3] * newHeights[3]); + + float srcScaleX = (float)oldWidth / tex.nWidth; + float srcScaleY = (float)oldHeight / tex.nHeight; + + for (int i = 0; i < 4; i++) { + unsigned char* srcData = (unsigned char*)(textures + texOffset + tex.nOffsets[i]); + unsigned char* dstData = (unsigned char*)(textures + texOffset + newOffset[i]); + int srcWidth = oldWidths[i]; + int dstWidth = newWidths[i]; + + for (int y = 0; y < newHeights[i]; y++) { + int srcY = (int)(srcScaleY * y + 0.5f); + + for (int x = 0; x < newWidths[i]; x++) { + int srcX = (int)(srcScaleX * x + 0.5f); + + dstData[y * dstWidth + x] = srcData[srcY * srcWidth + srcX]; + } + } + } + // 2 = palette color count (should always be 256) + memcpy(newPalette, palette, 256 * sizeof(COLOR3) + 2); + + for (int i = 0; i < 4; i++) { + tex.nOffsets[i] = newOffset[i]; + } + + adjust_downscaled_texture_coordinates(textureId, oldWidth, oldHeight); + + // shrink texture lump + int removedBytes = palette - newPalette; + unsigned char* texEnd = newPalette + 256 * sizeof(COLOR3); + int shiftBytes = (texEnd - textures) + removedBytes; + + memcpy(texEnd, texEnd + removedBytes, bsp_header.lump[LUMP_TEXTURES].nLength - shiftBytes); + for (int k = textureId + 1; k < textureCount; k++) { + ((int*)textures)[k + 1] -= removedBytes; + } + + /*for (int i = 0; i < textureCount; i++) { + int tmpOffset = ((int*)textures)[i + 1]; + BSPMIPTEX& tmpTex = *((BSPMIPTEX*)(textures + tmpOffset)); + // f.... ? + } + */ + + print_log("Downscale {} {}x{} -> {}x{}\n", tex.szName, oldWidth, oldHeight, tex.nWidth, tex.nHeight); + + return true; +} + +bool Bsp::downscale_texture(int textureId, int maxDim, bool allowWad) { + int texOffset = ((int*)textures)[textureId + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + + int oldWidth = tex.nWidth; + int oldHeight = tex.nHeight; + int newWidth = tex.nWidth; + int newHeight = tex.nHeight; + + if (tex.nWidth > maxDim && tex.nWidth > tex.nHeight) { + float ratio = oldHeight / (float)oldWidth; + newWidth = maxDim; + newHeight = (int)(((newWidth * ratio) + 8) / 16) * 16; + if (newHeight > oldHeight) { + newHeight = (int)((newWidth * ratio) / 16) * 16; + } + } + else if (tex.nHeight > maxDim) { + float ratio = oldWidth / (float)oldHeight; + newHeight = maxDim; + newWidth = (int)(((newHeight * ratio) + 8) / 16) * 16; + if (newWidth > oldWidth) { + newWidth = (int)((newHeight * ratio) / 16) * 16; + } + } + else { + return false; // no need to downscale + } + + if (oldWidth == newWidth && oldHeight == newHeight) { + print_log("Failed to downscale texture {} {}x{} to max dim {}\n", tex.szName, oldWidth, oldHeight, maxDim); + return false; + } + + if (tex.nOffsets[0] == 0) { + if (allowWad) { + tex.nWidth = newWidth; + tex.nHeight = newHeight; + adjust_downscaled_texture_coordinates(textureId, oldWidth, oldHeight); + print_log("Texture coords were updated for {}. The WAD texture must be updated separately.\n", tex.szName); + } + else { + print_log("Can't downscale WAD texture {}\n", tex.szName); + } + + return false; + } - if (!needsVisibleHull && !needsMonsterHulls) - { - if (models[i].iHeadnodes[0] >= 0) - print_log(get_localized_string(LANG_0069), i, uses); + return downscale_texture(textureId, newWidth, newHeight); +} - deletedHulls += models[i].iHeadnodes[0] >= 0; +void Bsp::adjust_downscaled_texture_coordinates(int textureId, int oldWidth, int oldHeight) { + int texOffset = ((int*)textures)[textureId + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); - model.iHeadnodes[0] = -1; - model.nVisLeafs = 0; - model.nFaces = 0; - model.iFirstFace = 0; + int newWidth = tex.nWidth; + int newHeight = tex.nHeight; + + // scale up face texture coordinates + float scaleX = newWidth / (float)oldWidth; + float scaleY = newHeight / (float)oldHeight; + + for (int i = 0; i < faceCount; i++) { + BSPFACE32& face = faces[i]; + + if (texinfos[face.iTextureInfo].iMiptex != textureId) + continue; + + // each affected face should have a unique texinfo because + // the shift amount may be different for every face after scaling + BSPTEXTUREINFO* info = get_unique_texinfo(i); + + // get any vert on the face to use a reference point. Why? + // When textures are scaled, the texture relative to the face will depend on how far away its + // vertices are from the world origin. This means faces far away from the world origin shift many + // pixels per scale unit, and faces aligned with the world origin don't shift at all when scaled. + int edgeIdx = surfedges[face.iFirstEdge]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + vec3 vert = verts[vertIdx]; + + vec3 oldvs = info->vS; + vec3 oldvt = info->vT; + info->vS *= scaleX; + info->vT *= scaleY; + + // get before/after uv coordinates + float oldu = (dotProduct(oldvs, vert) + info->shiftS) * (1.0f / (float)oldWidth); + float oldv = (dotProduct(oldvt, vert) + info->shiftT) * (1.0f / (float)oldHeight); + float u = dotProduct(info->vS, vert) + info->shiftS; + float v = dotProduct(info->vT, vert) + info->shiftT; + + // undo the shift in uv coordinates for this face + info->shiftS += (oldu * newWidth) - u; + info->shiftT += (oldv * newHeight) - v; + } +} + +void Bsp::downscale_invalid_textures() { + int count = 0; + + for (int i = 0; i < textureCount; i++) { + int texOffset = ((int*)textures)[i + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + + if (tex.nOffsets[0] == 0) { + print_log("Skipping WAD texture {}\n", tex.szName); + continue; } - if (!needsPlayerHulls && !needsMonsterHulls) - { - bool deletedAnyHulls = false; - for (int k = 1; k < MAX_MAP_HULLS; k++) - { - deletedHulls += models[i].iHeadnodes[k] >= 0; - if (models[i].iHeadnodes[k] >= 0) - { - deletedHulls++; - deletedAnyHulls = true; + + if (tex.nWidth * tex.nHeight > MAX_TEXTURE_SIZE) { + + int oldWidth = tex.nWidth; + int oldHeight = tex.nHeight; + int newWidth = tex.nWidth; + int newHeight = tex.nHeight; + + float ratio = oldHeight / (float)oldWidth; + + while (newWidth > 16) { + newWidth -= 16; + newHeight = newWidth * ratio; + + if (newHeight % 16 != 0) { + continue; } - } - if (deletedAnyHulls) - print_log(get_localized_string(LANG_0070), i, uses); + if (newWidth * newHeight <= MAX_TEXTURE_SIZE) { + break; + } + } - model.iHeadnodes[1] = -1; - model.iHeadnodes[2] = -1; - model.iHeadnodes[3] = -1; + downscale_texture(i, newWidth, newHeight); + count++; } - else if (!needsMonsterHulls) - { - if (models[i].iHeadnodes[2] >= 0) - print_log(get_localized_string(LANG_0071), i, uses); + } - deletedHulls += models[i].iHeadnodes[2] >= 0; + print_log("Downscaled {} textures\n", count); +} - model.iHeadnodes[2] = -1; - } - else if (!needsPlayerHulls) - { - // monsters use all hulls so can't do anything about this +bool Bsp::rename_texture(const char* oldName, const char* newName) { + if (strlen(newName) > 16) { + print_log("ERROR: New texture name longer than 15 characters ({})\n", strlen(newName)); + return false; + } + + for (int i = 0; i < textureCount; i++) { + int texOffset = ((int*)textures)[i + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + + if (!strncmp(tex.szName, oldName, 16)) { + strncpy(tex.szName, newName, 16); + print_log("Renamed texture '{}' -> '{}'\n", oldName, newName); + return true; } } - STRUCTCOUNT removed = remove_unused_model_structures(); + print_log("No texture found with name '{}'\n", oldName); + return false; +} - update_ent_lump(); +std::set Bsp::selectConnectedTexture(int modelId, int faceId) { + std::set selected; + const float epsilon = 1.0f; - if (!noProgress) - { - g_progress.clear(); - g_progress = ProgressMeter(); + BSPMODEL& model = models[modelId]; + + BSPFACE32& face = faces[faceId]; + BSPTEXTUREINFO& info = texinfos[face.iTextureInfo]; + BSPPLANE& plane = planes[face.iPlane]; + + std::vector selectedVerts; + for (int e = 0; e < face.nEdges; e++) { + int edgeIdx = surfedges[face.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + selectedVerts.push_back(verts[vertIdx]); } - return removed; + bool anyNewFaces = true; + while (anyNewFaces) { + anyNewFaces = false; + + print_log("Loop again!\n"); + for (int fa = 0; fa < model.nFaces; fa++) { + int testFaceIdx = model.iFirstFace + fa; + BSPFACE32& faceA = faces[testFaceIdx]; + BSPTEXTUREINFO& infoA = texinfos[faceA.iTextureInfo]; + BSPPLANE& planeA = planes[faceA.iPlane]; + + if (planeA.vNormal != plane.vNormal || info.iMiptex != infoA.iMiptex || selected.count(testFaceIdx)) { + continue; + } + + std::vector uniqueVerts; + bool isConnected = false; + + for (int e = 0; e < faceA.nEdges && !isConnected; e++) { + int edgeIdx = surfedges[faceA.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + + vec3 v2 = verts[vertIdx]; + for (vec3 v1 : selectedVerts) { + if ((v1 - v2).length() < epsilon) { + isConnected = true; + break; + } + } + } + + // shares an edge. Select this face + if (isConnected) { + for (int e = 0; e < faceA.nEdges; e++) { + int edgeIdx = surfedges[faceA.iFirstEdge + e]; + BSPEDGE32& edge = edges[abs(edgeIdx)]; + int vertIdx = edgeIdx >= 0 ? edge.iVertex[1] : edge.iVertex[0]; + selectedVerts.push_back(verts[vertIdx]); + } + + selected.insert(testFaceIdx); + anyNewFaces = true; + print_log("Select {} add {}\n", testFaceIdx, uniqueVerts.size()); + } + } + } + + return selected; } bool Bsp::is_invisible_solid(Entity* ent) @@ -4750,6 +6354,9 @@ bool Bsp::validate() print_log(PRINT_RED | PRINT_INTENSITY, get_localized_string(LANG_0136), texlen, tex->szName[0] != '\0' ? tex->szName : "UNKNOWN_NAME", texOffset, dataOffset); } } + else if (tex->nWidth * tex->nHeight > MAX_TEXTURE_SIZE) { + print_log("Texture '{}' too large ({}x{})\n", tex->szName, tex->nWidth, tex->nHeight); + } } } @@ -5309,6 +6916,129 @@ int Bsp::pointContents(int iNode, const vec3& p, int hull) return pointContents(iNode, p, hull, nodeBranch, leafIdx, childIdx); } +bool Bsp::recursiveHullCheck(int hull, int num, float p1f, float p2f, vec3 p1, vec3 p2, TraceResult* trace) +{ + if (num < 0) { + if (num != CONTENTS_SOLID) { + trace->fAllSolid = false; + + if (num == CONTENTS_EMPTY) + trace->fInOpen = true; + + else if (num != CONTENTS_TRANSLUCENT) + trace->fInWater = true; + } + else { + trace->fStartSolid = true; + } + + // empty + return true; + } + + if (num >= clipnodeCount) { + print_log("{}: bad node number\n", __func__); + return false; + } + + // find the point distances + BSPCLIPNODE32* node = &clipnodes[num]; + BSPPLANE* plane = &planes[node->iPlane]; + + float t1 = dotProduct(plane->vNormal, p1) - plane->fDist; + float t2 = dotProduct(plane->vNormal, p2) - plane->fDist; + + // keep descending until we find a plane that bisects the trace line + if (t1 >= 0.0f && t2 >= 0.0f) + return recursiveHullCheck(hull, node->iChildren[0], p1f, p2f, p1, p2, trace); + if (t1 < 0.0f && t2 < 0.0f) + return recursiveHullCheck(hull, node->iChildren[1], p1f, p2f, p1, p2, trace); + + int side = (t1 < 0.0f) ? 1 : 0; + + // put the crosspoint DIST_EPSILON pixels on the near side + float frac; + if (side) { + frac = (t1 + EPSILON) / (t1 - t2); + } + else { + frac = (t1 - EPSILON) / (t1 - t2); + } + frac = clamp(frac, 0.0f, 1.0f); + + if (frac != frac) { + return false; // NaN + } + + float pdif = p2f - p1f; + float midf = p1f + pdif * frac; + + vec3 point = p2 - p1; + vec3 mid = p1 + (point * frac); + + // check if trace is empty up until this plane that was just intersected + if (!recursiveHullCheck(hull, node->iChildren[side], p1f, midf, p1, mid, trace)) { + // hit an earlier plane that caused the trace to be fully solid here + return false; + } + + // check if trace can go through this plane without entering a solid area + if (pointContents(node->iChildren[side ^ 1], mid, hull) != CONTENTS_SOLID) { + // continue the trace from this plane + // won't collide with it again because trace starts from a child of the intersected node + return recursiveHullCheck(hull, node->iChildren[side ^ 1], midf, p2f, mid, p2, trace); + } + + if (trace->fAllSolid) { + return false; // never got out of the solid area + } + + // the other side of the node is solid, this is the impact point + trace->vecPlaneNormal = plane->vNormal; + trace->flPlaneDist = side ? -plane->fDist : plane->fDist; + + // backup the trace if the collision point is considered solid due to poor float precision + // shouldn't really happen, but does occasionally + int headnode = models[0].iHeadnodes[hull]; + while (pointContents(headnode, mid, hull) == CONTENTS_SOLID) { + frac -= 0.1f; + if (frac < 0.0f) + { + trace->flFraction = midf; + trace->vecEndPos = mid; + print_log("backup past 0\n"); + return false; + } + + midf = p1f + pdif * frac; + + point = p2 - p1; + mid = p1 + (point * frac); + } + + trace->flFraction = midf; + trace->vecEndPos = mid; + + return false; +} + +void Bsp::traceHull(vec3 start, vec3 end, int hull, TraceResult* trace) +{ + if (hull < 0 || hull > 3) + hull = 0; + + int headnode = models[0].iHeadnodes[hull]; + + // fill in a default trace + memset(trace, 0, sizeof(TraceResult)); + trace->vecEndPos = end; + trace->flFraction = 1.0f; + trace->fAllSolid = true; + + // trace a line through the appropriate clipping hull + recursiveHullCheck(hull, headnode, 0.0f, 1.0f, start, end, trace); +} + const char* Bsp::getLeafContentsName(int contents) { switch (contents) @@ -5346,6 +7076,150 @@ const char* Bsp::getLeafContentsName(int contents) } } +int Bsp::get_leaf(vec3 pos, int hull) { + int iNode = models->iHeadnodes[hull]; + + if (hull == 0) { + while (iNode >= 0) + { + BSPNODE32& node = nodes[iNode]; + BSPPLANE& plane = planes[node.iPlane]; + + float d = dotProduct(plane.vNormal, pos) - plane.fDist; + if (d < 0) { + iNode = node.iChildren[1]; + } + else { + iNode = node.iChildren[0]; + } + } + + return ~iNode; + } + + int lastNode = -1; + int lastSide = 0; + + while (iNode >= 0) + { + BSPCLIPNODE32& node = clipnodes[iNode]; + BSPPLANE& plane = planes[node.iPlane]; + + float d = dotProduct(plane.vNormal, pos) - plane.fDist; + if (d < 0) { + lastNode = iNode; + iNode = node.iChildren[1]; + lastSide = 1; + } + else { + lastNode = iNode; + iNode = node.iChildren[0]; + lastSide = 0; + } + } + + // clipnodes don't have leaf structs, so generate an id based on the last clipnode index and + // the side of the plane that would be recursed to reach the leaf contents, if there were a leaf + return lastNode * 2 + lastSide; +} + +bool Bsp::is_leaf_visible(int ileaf, vec3 pos) { + int ipvsLeaf = get_leaf(pos, 0); + BSPLEAF32& pvsLeaf = leaves[ipvsLeaf]; + + int p = pvsLeaf.nVisOffset; // pvs offset + unsigned char* pvs = lumps[LUMP_VISIBILITY]; + + bool isVisible = false; + int numVisible = 0; + + if (!pvs) { + return true; + } + + //print_log("leaf {} can see:", ipvsLeaf); + + for (int lf = 1; lf < leafCount; p++) + { + if (pvs[p] == 0) // prepare to skip leafs + lf += 8 * pvs[++p]; // next unsigned char holds number of leafs to skip + else + { + for (unsigned char bit = 1; bit != 0; bit *= 2, lf++) + { + if ((pvs[p] & bit) && lf < leafCount) // leaf is flagged as visible + { + numVisible++; + //print_log(" {}", lf); + if (lf == ileaf) { + isVisible = true; + } + } + } + } + } + + //print_log("\n"); + + return isVisible; +} + +bool Bsp::is_face_visible(int faceIdx, vec3 pos, vec3 angles) { + BSPFACE32& face = faces[faceIdx]; + BSPPLANE& plane = planes[face.iPlane]; + vec3 normal = plane.vNormal; + + // TODO: is it in the frustrum? Is it part of an entity model? If so is the entity linked in the PVS? + // is it facing the camera? Is it a special face? + + return true; +} + +int Bsp::count_visible_polys(vec3 pos, vec3 angles) { + int ipvsLeaf = get_leaf(pos, 0); + BSPLEAF32& pvsLeaf = leaves[ipvsLeaf]; + + int p = pvsLeaf.nVisOffset; // pvs offset + unsigned char* pvs = lumps[LUMP_VISIBILITY]; + + int numVisible = 0; + + if (ipvsLeaf == 0) { + return faceCount; + } + + memset(pvsFaces, 0, pvsFaceCount * sizeof(bool)); + int renderFaceCount = 0; + + for (int lf = 1; lf < leafCount; p++) + { + if (pvs[p] == 0) // prepare to skip leafs + lf += 8 * pvs[++p]; // next unsigned char holds number of leafs to skip + else + { + for (unsigned char bit = 1; bit != 0; bit *= 2, lf++) + { + if ((pvs[p] & bit) && lf < leafCount) // leaf is flagged as visible + { + numVisible++; + BSPLEAF32& leaf = leaves[lf]; + + for (int i = 0; i < leaf.nMarkSurfaces; i++) { + int faceIdx = marksurfs[leaf.iFirstMarkSurface + i]; + if (!pvsFaces[faceIdx]) { + pvsFaces[faceIdx] = true; + if (is_face_visible(faceIdx, pos, angles)) + renderFaceCount++; + } + } + } + } + } + } + + return renderFaceCount; +} + void Bsp::mark_face_structures(int iFace, STRUCTUSAGE* usage) { if (iFace > faceCount) @@ -7007,14 +8881,8 @@ void Bsp::simplify_model_collision(int modelIdx, int hullIdx) vec3 vertMin(FLT_MAX_COORD, FLT_MAX_COORD, FLT_MAX_COORD); vec3 vertMax(-FLT_MAX_COORD, -FLT_MAX_COORD, -FLT_MAX_COORD); - if (get_model_vertex_bounds(modelIdx, vertMin, vertMax)) - { - create_clipnode_box(vertMin, vertMax, &model, hullIdx, true); - } - else - { - print_log(PRINT_RED | PRINT_INTENSITY, get_localized_string(LANG_1036), modelIdx); - } + + create_clipnode_box(vertMin, vertMax, &model, hullIdx, true); } int Bsp::create_clipnode(bool force_reversed, int reversed_id) @@ -8302,26 +10170,8 @@ int Bsp::merge_two_models_ents(int src_ent, int dst_ent, int& tryanotherway) vec3 amin, amax, bmin, bmax; - bool valid1 = get_model_vertex_bounds(src_model, amin, amax); - bool valid2 = get_model_vertex_bounds(dst_model, bmin, bmax); - - if (!valid1 && !valid2) - { - amin = models[src_model].nMins; - amax = models[src_model].nMaxs; - bmin = models[dst_model].nMins; - bmax = models[dst_model].nMaxs; - } - else if (!valid1) - { - amin = models[src_model].nMins; - amax = models[src_model].nMaxs; - } - else if (!valid2) - { - bmin = models[dst_model].nMins; - bmax = models[dst_model].nMaxs; - } + get_model_vertex_bounds(src_model, amin, amax); + get_model_vertex_bounds(dst_model, bmin, bmax); vec3 ent_offset = ents[src_ent]->origin - ents[dst_ent]->origin; @@ -9083,6 +10933,16 @@ void Bsp::update_lump_pointers() } } } + + if (pvsFaceCount != faceCount) { + pvsFaceCount = faceCount; + + if (pvsFaces) + { + delete[] pvsFaces; + } + pvsFaces = new bool[pvsFaceCount]; + } } void Bsp::replace_lump(int lumpIdx, void* newData, size_t newLength) @@ -12421,13 +14281,13 @@ int Bsp::import_mdl_to_bspmodel(std::vector& meshes, mat4x4 angles, std::vector hullVerts; if (getModelPlaneIntersectVerts(newModelIdx, hullVerts)) { - print_log(PRINT_GREEN, "Found valid intersect verts for model %d\n", newModelIdx); + print_log(PRINT_GREEN, "Found valid intersect verts for model {}\n", newModelIdx); regenerate_clipnodes(newModelIdx, -1); validNodes = true; } else {*/ - // print_log(PRINT_RED, "No intersect verts for model %d\n", newModelIdx); + // print_log(PRINT_RED, "No intersect verts for model {}\n", newModelIdx); create_node_box(models[newModelIdx].nMins, models[newModelIdx].nMaxs, &models[newModelIdx], true, empty_leaf); validNodes = false; /*}*/ diff --git a/src/bsp/Bsp.h b/src/bsp/Bsp.h index 13639ca4..a2de8be4 100644 --- a/src/bsp/Bsp.h +++ b/src/bsp/Bsp.h @@ -1,19 +1,27 @@ #pragma once #include #include +#include +#include + #include "Wad.h" #include "Entity.h" #include "bsplimits.h" #include "rad.h" -#include #include "remap.h" -#include #include "bsptypes.h" #include "mdl_studio.h" #include "Sprite.h" #include "XASH_csm.h" +#include "Polygon3D.h" class BspRenderer; +#define OOB_CLIP_X 1 +#define OOB_CLIP_X_NEG 2 +#define OOB_CLIP_Y 4 +#define OOB_CLIP_Y_NEG 8 +#define OOB_CLIP_Z 16 +#define OOB_CLIP_Z_NEG 32 struct membuf : std::streambuf { @@ -137,15 +145,23 @@ class Bsp void recurse_node_print(int node, int depth); void get_last_node(int nodeIdx, int& node, int& count, int last_node = -1); - void get_last_clipnode(int nodeIdx, int& node, int& count, int last_node = -1); + void get_last_clipnode(int nodeIdx, int& node, int& count, int last_node = -1); // get leaf index from world position + int get_leaf(vec3 pos, int hull); int pointContents(int iNode, const vec3& p, int hull, std::vector& nodeBranch, int& leafIdx, int& childIdx); int modelLeafs(int modelIdx, std::vector& modelLeafs); int modelLeafs(const BSPMODEL& model, std::vector& modelLeafs); int pointContents(int iNode, const vec3& p, int hull); + bool recursiveHullCheck(int hull, int num, float p1f, float p2f, vec3 p1, vec3 p2, TraceResult* trace); + void traceHull(vec3 start, vec3 end, int hull, TraceResult* ptr); int pointLeaf(int iNode, const vec3& p, int hull, int& leafIdx, int & planeIdx); std::vector getLeafsFromPos(const vec3& p, float radius); const char* getLeafContentsName(int contents); + // returns true if leaf is in the PVS from the given position + bool is_leaf_visible(int ileaf, vec3 pos); + + bool is_face_visible(int faceIdx, vec3 pos, vec3 angles); + int count_visible_polys(vec3 pos, vec3 angles); // strips a collision hull from the given model index // and redirects to the given hull, if redirect>0 @@ -161,7 +177,7 @@ class Bsp void get_bounding_box(vec3& mins, vec3& maxs); // get the bounding box for all vertexes in a BSP tree - bool get_model_vertex_bounds(int modelIdx, vec3& mins, vec3& maxs); + void get_model_vertex_bounds(int modelIdx, vec3& mins, vec3& maxs); // face has duplicate verts, this is bad? bool is_face_duplicate_edges(int faceIdx); @@ -180,10 +196,14 @@ class Bsp bool is_convex(int modelIdx); bool is_node_hull_convex(int iNode); + // true if the center of this face is touching an empty leaf + bool isInteriorFace(const Polygon3D& poly, int hull); + // get cuts required to create bounding volumes for each solid leaf in the model - std::vector get_model_leaf_volume_cuts(int modelIdx, int hullIdx); - void get_clipnode_leaf_cuts(int iNode, int iStartNode, std::vector& clipOrder, std::vector& output); - void get_node_leaf_cuts(int iNode, int iStartNode, std::vector& clipOrder, std::vector& output); + std::vector get_model_leaf_volume_cuts(int modelIdx, int hullIdx, int contents); + void get_clipnode_leaf_cuts(int iNode, std::vector& clipOrder, std::vector& output, int contents); + void get_node_leaf_cuts(int iNode, std::vector& clipOrder, std::vector& output, int contents); + void get_leaf_nodes(int leaf, std::vector& out_nodes); @@ -222,6 +242,76 @@ class Bsp // conditionally deletes hulls for entities that aren't using them STRUCTCOUNT delete_unused_hulls(bool noProgress = false); + + // deletes data outside the map bounds + void delete_oob_data(int clipFlags); + + void delete_oob_clipnodes(int iNode, int* parentBranch, std::vector& clipOrder, + int oobFlags, bool* oobHistory, bool isFirstPass, int& removedNodes); + + void delete_oob_nodes(int iNode, int* parentBranch, std::vector& clipOrder, + int oobFlags, bool* oobHistory, bool isFirstPass, int& removedNodes); + + // deletes data inside a bounding box + void delete_box_data(vec3 clipMins, vec3 clipMaxs); + void delete_box_clipnodes(int iNode, int* parentBranch, std::vector& clipOrder, + vec3 clipMins, vec3 clipMaxs, bool* oobHistory, bool isFirstPass, int& removedNodes); + void delete_box_nodes(int iNode, int* parentBranch, std::vector& clipOrder, + vec3 clipMins, vec3 clipMaxs, bool* oobHistory, bool isFirstPass, int& removedNodes); + + // assumes contiguous leaves starting at 0. Only works for worldspawn, which is the only model which + // should have leaves anyway. + void count_leaves(int iNode, int& leafCount); + + // searches for entities that have very similar models, + // then updates the entities to share a single model reference + // this reduces the precached model count even though the models are still present in the bsp + void deduplicate_models(); + + // scales up texture axes for any face with bad surface extents + // connected planar faces which use the same texture will also be scaled up to prevent seams + // showing between faces with different texture scales + // scaleNotSubdivide:true = scale face textures to lower extents + // scaleNotSubdivide:false = subdivide face textures to lower extents + // downscaleOnly:true = don't scale or subdivide anything, just downscale the textures + // maxTextureDim = downscale textures first if they are larger than this (0 = disable) + void fix_bad_surface_extents(bool scaleNotSubdivide, bool downscaleOnly, int maxTextureDim); + + // subdivide a face until it has valid surface extents + void fix_bad_surface_extents_with_subdivide(int faceIdx); + + // reduces size of textures that exceed game limits and adjusts face scales accordingly + void downscale_invalid_textures(); + + void downscale_textures(int maxDim); + + // downscales a texture to the maximum specified width/height + // allowWad:true = texture coordinates will be scaled even if the the texture is from a WAD and must be scaled separately + // returns true if was downscaled + bool downscale_texture(int textureId, int maxDim, bool allowWad); + + bool downscale_texture(int textureId, int newWidth, int newHeight); + + bool rename_texture(const char* oldName, const char* newName); + + // updates texture coordinates after a texture has been downscaled + void adjust_downscaled_texture_coordinates(int textureId, int oldWidth, int oldHeight); + + vec3 get_face_center(int faceIdx); + + // scales up texture sizes on models that aren't used by visible entities + void allocblock_reduction(); + + // gets estimated number of allocblocks filled + // actual amount will vary because there is some wasted space when the engine generates lightmap atlases + float calc_allocblock_usage(); + + // subdivides along the axis with the most texture pixels (for biggest surface extent reduction) + bool subdivide_face(int faceIdx); + + // select faces connected to the given one, which lie on the same plane and use the same texture + std::set selectConnectedTexture(int modelId, int faceId); + // returns true if the map has eny entities that make use of hull 2 bool has_hull2_ents(); @@ -423,6 +513,9 @@ class Bsp BspRenderer* renderer; unsigned int originCrc32 = 0; + + bool* pvsFaces = NULL; // flags which faces are marked for rendering in the PVS + int pvsFaceCount = 0; }; void update_unused_wad_files(Bsp* baseMap, Bsp* targetMap, int tex_type = 0); \ No newline at end of file diff --git a/src/bsp/BspMerger.cpp b/src/bsp/BspMerger.cpp index 444be62e..0be65d14 100644 --- a/src/bsp/BspMerger.cpp +++ b/src/bsp/BspMerger.cpp @@ -4,14 +4,19 @@ #include "log.h" -Bsp* BspMerger::merge(std::vector maps, const vec3& gap, const std::string& output_name, bool noripent, bool noscript, bool nomergestyles) +MergeResult BspMerger::merge(std::vector maps, const vec3& gap, const std::string& output_name, bool noripent, bool noscript, bool nomove, bool nomergestyles) { + MergeResult result; + result.fpath = ""; + result.map = NULL; + result.moveFixes = vec3(); + result.overflow = false; if (maps.size() < 2) { print_log(PRINT_RED | PRINT_INTENSITY, get_localized_string(LANG_0219)); - return NULL; + return result; } - + result.fpath = maps[1]->bsp_path; skipLightStyles = nomergestyles; @@ -122,7 +127,7 @@ Bsp* BspMerger::merge(std::vector maps, const vec3& gap, const std::string } - std::vector>> blocks = separate(maps, gap); + std::vector>> blocks = separate(maps, gap, nomove, result); print_log(get_localized_string(LANG_0220)); @@ -245,7 +250,9 @@ Bsp* BspMerger::merge(std::vector maps, const vec3& gap, const std::string } } - return output; + result.map = output; + result.overflow = !output->isValid(); + return result; } void BspMerger::merge(MAPBLOCK& dst, MAPBLOCK& src, std::string resultType) @@ -258,7 +265,7 @@ void BspMerger::merge(MAPBLOCK& dst, MAPBLOCK& src, std::string resultType) merge(*dst.map, *src.map); } -std::vector>> BspMerger::separate(std::vector& maps, const vec3& gap) +std::vector>> BspMerger::separate(std::vector& maps, const vec3& gap, bool nomove, MergeResult& result) { std::vector blocks; @@ -292,21 +299,42 @@ std::vector>> BspMerger::separate(std::vector< } bool noOverlap = true; - for (size_t i = 0; i < blocks.size() && noOverlap; i++) - { - for (size_t k = i + i; k < blocks.size(); k++) - { - if (blocks[i].intersects(blocks[k])) - { + for (int i = 0; i < blocks.size() && noOverlap; i++) { + for (int k = 0; k < blocks.size(); k++) { + if (i != k && blocks[i].intersects(blocks[k])) { noOverlap = false; + + if (nomove) { + print_log("Merge aborted because the maps overlap.\n"); + blocks[i].suggest_intersection_fix(blocks[k], result); + } + break; } } } - if (noOverlap) - { - print_log(get_localized_string(LANG_0224)); + if (noOverlap) { + if (!nomove) + print_log("Maps do not overlap. They will be merged without moving.\n"); + + std::vector> col; + std::vector row; + for (const MAPBLOCK& block : blocks) { + row.push_back(block); + if (block.map->ents[0]->hasKey("origin")) { + // apply the transform move in the GUI + block.map->move(block.map->ents[0]->origin); + block.map->ents[0]->removeKeyvalue("origin"); + } + } + col.push_back(row); + orderedBlocks.push_back(col); + + return orderedBlocks; + } + + if (nomove) { return orderedBlocks; } diff --git a/src/bsp/BspMerger.h b/src/bsp/BspMerger.h index 9c691c10..3a0b0532 100644 --- a/src/bsp/BspMerger.h +++ b/src/bsp/BspMerger.h @@ -1,5 +1,16 @@ -#include "util.h" +#pragma once #include "Bsp.h" +#include "vectors.h" + +struct MergeResult { + Bsp* map; + + // merge failed if map is null, and below are suggested fixes + std::string fpath; + vec3 moveFixes; + vec3 moveFixes2; + bool overflow; +}; // bounding box for a map, used for arranging maps for merging struct MAPBLOCK @@ -7,6 +18,23 @@ struct MAPBLOCK vec3 mins, maxs, size, offset; Bsp* map; std::string merge_name; + + void suggest_intersection_fix(MAPBLOCK& other, MergeResult& result) + { + float xdelta_neg = other.maxs.x - mins.x; + float xdelta_pos = maxs.x - other.mins.x; + float ydelta_neg = other.maxs.y - mins.y; + float ydelta_pos = maxs.y - other.mins.y; + float zdelta_neg = other.maxs.z - mins.z; + float zdelta_pos = maxs.z - other.mins.z; + + int xdelta = xdelta_neg < xdelta_pos ? ceilf(xdelta_neg + 1.5f) : -ceilf(xdelta_pos + 1.5f); + int ydelta = ydelta_neg < ydelta_pos ? ceilf(ydelta_neg + 1.5f) : -ceilf(ydelta_pos + 1.5f); + int zdelta = zdelta_neg < zdelta_pos ? ceilf(zdelta_neg + 1.5f) : -ceilf(zdelta_pos + 1.5f); + + result.moveFixes = vec3(xdelta, ydelta, zdelta); + result.moveFixes2 = vec3(-xdelta, -ydelta, -zdelta); + } bool intersects(MAPBLOCK& other) { @@ -24,7 +52,8 @@ class BspMerger // merges all maps into one // noripent - don't change any entity logic // noscript - don't add support for the bspguy map script (worse performance + buggy, but simpler) - Bsp* merge(std::vector maps, const vec3& gap, const std::string& output_name, bool noripent, bool noscript, bool nomergestyles); + + MergeResult merge(std::vector maps, const vec3& gap, const std::string& output_name, bool noripent, bool noscript, bool nomove, bool nomergestyles); // wrapper around BSP data merging for nicer console output @@ -33,7 +62,7 @@ class BspMerger // merge BSP data bool merge(Bsp& mapA, Bsp& mapB, bool modelMerge = false); - std::vector>> separate(std::vector& maps, const vec3& gap); + std::vector>> separate(std::vector& maps, const vec3& gap, bool nomove, MergeResult& result); // for maps in a series: // - changelevels should be replaced with teleports or respawn triggers diff --git a/src/bsp/Entity.cpp b/src/bsp/Entity.cpp index 9a384423..f50d1388 100644 --- a/src/bsp/Entity.cpp +++ b/src/bsp/Entity.cpp @@ -1,6 +1,7 @@ #include "Entity.h" #include "util.h" - +#include "Bsp.h" +#include void Entity::addKeyvalue(const std::string & key, const std::string & value, bool multisupport) { @@ -623,6 +624,21 @@ size_t Entity::getMemoryUsage() return size; } +vec3 Entity::getHullOrigin(Bsp* map) { + vec3 ori = origin; + int modelIdx = getBspModelIdx(); + + if (modelIdx != -1) { + BSPMODEL& model = map->models[modelIdx]; + + vec3 mins, maxs; + map->get_model_vertex_bounds(modelIdx, mins, maxs); + ori += (maxs + mins) * 0.5f; + } + + return ori; +} + void Entity::updateRenderModes() { rendermode = kRenderNormal; @@ -646,4 +662,68 @@ void Entity::updateRenderModes() vec3 color = parseVector(keyvalues["rendercolor"]); rendercolor = vec3(color[0] / 255.f, color[1] / 255.f, color[2] / 255.f); } -} \ No newline at end of file +} + +bool Entity::isEverVisible() { + std::string cname = keyvalues["classname"]; + std::string tname = hasKey("targetname") ? keyvalues["targetname"] : ""; + + static std::set invisibleEnts = { + "env_bubbles", + "func_clip", + "func_friction", + "func_ladder", + "func_monsterclip", + "func_mortar_field", + "func_op4mortarcontroller", + "func_tankcontrols", + "func_traincontrols", + "trigger_autosave", + "trigger_cameratarget", + "trigger_cdaudio", + "trigger_changelevel", + "trigger_counter", + "trigger_endsection", + "trigger_gravity", + "trigger_hurt", + "trigger_monsterjump", + "trigger_multiple", + "trigger_once", + "trigger_push", + "trigger_teleport", + "trigger_transition", + "game_zone_player", + "info_hullshape", + "player_respawn_zone", + }; + + if (invisibleEnts.count(cname)) { + return false; + } + + if (!tname.length() && hasKey("rendermode") && atoi(keyvalues["rendermode"].c_str()) != 0) { + if (!hasKey("renderamt") || atoi(keyvalues["renderamt"].c_str()) == 0) { + // starts invisible and likely nothing will change that because it has no targetname + return false; + } + } + + return true; +} + +std::string Entity::serialize() +{ + std::stringstream ent_data; + + ent_data << "{\n"; + + for (int k = 0; k < keyOrder.size(); k++) { + std::string key = keyOrder[k]; + ent_data << "\"" << key << "\" \"" << keyvalues[key] << "\"\n"; + } + + ent_data << "}\n"; + + return ent_data.str(); +} + diff --git a/src/bsp/Entity.h b/src/bsp/Entity.h index 78957c96..6389b342 100644 --- a/src/bsp/Entity.h +++ b/src/bsp/Entity.h @@ -3,6 +3,7 @@ #include typedef std::map< std::string, std::string > hashmap; +class Bsp; class Entity { @@ -72,7 +73,15 @@ class Entity size_t getMemoryUsage(); // aproximate - vec3 origin; + vec3 origin; + + vec3 getHullOrigin(Bsp* map); + + + bool isEverVisible(); + + std::string serialize(); + int rendermode; int renderamt; int renderfx; diff --git a/src/bsp/bsplimits.cpp b/src/bsp/bsplimits.cpp index 77080665..857d052e 100644 --- a/src/bsp/bsplimits.cpp +++ b/src/bsp/bsplimits.cpp @@ -21,6 +21,8 @@ unsigned int MAX_MAP_LIGHTDATA = 64 * (1024 * 1024); // 64 MB unsigned int MAX_TEXTURE_DIMENSION = 1024; unsigned int MAX_TEXTURE_SIZE = ((MAX_TEXTURE_DIMENSION * MAX_TEXTURE_DIMENSION * 2 * 3) / 2); +float MAX_MAP_BOUNDARY = 4096.0f; + unsigned int MAX_KEY_LEN = 256; // not sure if this includes the null char unsigned int MAX_VAL_LEN = 4096; // not sure if this includes the null char @@ -46,6 +48,8 @@ void ResetBspLimits() MAX_KEY_LEN = 256; MAX_VAL_LEN = 4096; + MAX_MAP_BOUNDARY = 4096.0f; + TEXTURE_STEP = 16; MAX_SURFACE_EXTENT = 64; diff --git a/src/bsp/bsplimits.h b/src/bsp/bsplimits.h index dda3dfb0..06e5a00f 100644 --- a/src/bsp/bsplimits.h +++ b/src/bsp/bsplimits.h @@ -16,7 +16,7 @@ #define MAX_MAP_FACES 65535 // (unsgined short) This ought to be 32768, otherwise faces(in world) can become invisible. --vluzacn #define MAX_KEYS_PER_ENT 128 #define MAX_LIGHTMAPS 4 -#define MAX_LIGHTSTYLES 256 // a byte limit, don't modify +#define MAX_LIGHTSTYLES 256 // a unsigned char limit, don't modify extern int LIGHTMAP_ATLAS_SIZE; //max for glTexImage2D @@ -39,6 +39,8 @@ extern unsigned int MAX_MAP_LIGHTDATA; // 64 MB extern unsigned int MAX_TEXTURE_DIMENSION; extern unsigned int MAX_TEXTURE_SIZE; +extern float MAX_MAP_BOUNDARY; + extern unsigned int MAX_KEY_LEN; // not sure if this includes the null char extern unsigned int MAX_VAL_LEN; // not sure if this includes the null char diff --git a/src/bsp/bsptypes.h b/src/bsp/bsptypes.h index 4a4c09c1..63268b37 100644 --- a/src/bsp/bsptypes.h +++ b/src/bsp/bsptypes.h @@ -545,4 +545,19 @@ struct BBOX { vec3 mins, maxs; int row; +}; + + +struct TraceResult +{ + int fAllSolid; // if true, plane is not valid + int fStartSolid; // if true, the initial point was in a solid area + int fInOpen; + int fInWater; + float flFraction; // time completed, 1.0 = didn't hit anything + vec3 vecEndPos; // final position + float flPlaneDist; + vec3 vecPlaneNormal; // surface normal at impact + //edict_t* pHit; // entity the surface is on + int iHitgroup; // 0 == generic, non zero is specific body part }; \ No newline at end of file diff --git a/src/editor/BspRenderer.cpp b/src/editor/BspRenderer.cpp index a31733d8..f171dea9 100644 --- a/src/editor/BspRenderer.cpp +++ b/src/editor/BspRenderer.cpp @@ -11,7 +11,7 @@ #include "Command.h" #include "Sprite.h" #include "Gui.h" - +#include "Polygon3D.h" #include "util.h" #include "log.h" @@ -1369,6 +1369,267 @@ void BspRenderer::loadClipnodes() ); } + +void BspRenderer::generateNavMeshBuffer() { + int hull = 3; + RenderClipnodes* renderClip = &renderClipnodes[0]; + renderClip->clipnodeBuffer[hull] = NULL; + renderClip->wireframeClipnodeBuffer[hull] = NULL; + + NavMesh* navMesh = NavMeshGenerator().generate(map, hull); + std::vector navPolys = navMesh->getPolys(); + + g_app->debugNavMesh = navMesh; + g_app->debugNavPoly = 529; + debugNavMesh = navMesh; + debugFaces = navPolys; + + static COLOR4 hullColors[] = { + COLOR4(255, 255, 255, 128), + COLOR4(96, 255, 255, 128), + COLOR4(255, 96, 255, 128), + COLOR4(255, 255, 96, 128), + }; + COLOR4 color = hullColors[hull]; + + std::vector allVerts; + std::vector wireframeVerts; + std::vector navFaceMaths; + + for (int m = 0; m < navPolys.size(); m++) { + Polygon3D& poly = navPolys[m]; + + vec3 normal = poly.plane_z; + + // calculations for face picking + { + FaceMath faceMath; + faceMath.normal = normal; + faceMath.fdist = poly.fdist; + faceMath.worldToLocal = poly.worldToLocal; + faceMath.localVerts = poly.localVerts; + navFaceMaths.push_back(faceMath); + } + + // create the verts for rendering + { + std::vector renderVerts; + renderVerts.resize(poly.verts.size()); + for (int i = 0; i < poly.verts.size(); i++) { + renderVerts[i] = poly.verts[i].flip(); + } + + COLOR4 wireframeColor = { 0, 0, 0, 255 }; + for (int k = 0; k < renderVerts.size(); k++) { + wireframeVerts.push_back(cVert(renderVerts[k], wireframeColor)); + wireframeVerts.push_back(cVert(renderVerts[(k + 1) % renderVerts.size()], wireframeColor)); + } + + vec3 lightDir = vec3(1, 1, -1).normalize(); + float dot = (dotProduct(normal, lightDir) + 1) / 2.0f; + if (dot > 0.5f) { + dot = dot * dot; + } + color = hullColors[hull]; + if (normal.z < -0.8 || true) { + static int r = 0; + r = (r + 1) % 8; + if (r == 0) { + color = COLOR4(255, 32, 32, 255); + } + else if (r == 1) { + color = COLOR4(255, 255, 32, 255); + } + else if (r == 2) { + color = COLOR4(255, 32, 255, 255); + } + else if (r == 3) { + color = COLOR4(255, 128, 255, 255); + } + else if (r == 4) { + color = COLOR4(32, 32, 255, 255); + } + else if (r == 5) { + color = COLOR4(32, 255, 255, 255); + } + else if (r == 6) { + color = COLOR4(32, 128, 255, 255); + } + else if (r == 7) { + color = COLOR4(32, 255, 128, 255); + } + } + COLOR4 faceColor = color * (dot); + + // convert from TRIANGLE_FAN style verts to TRIANGLES + for (int k = 2; k < renderVerts.size(); k++) { + allVerts.push_back(cVert(renderVerts[0], faceColor)); + allVerts.push_back(cVert(renderVerts[k - 1], faceColor)); + allVerts.push_back(cVert(renderVerts[k], faceColor)); + } + } + } + + cVert* output = new cVert[allVerts.size()]; + for (int i = 0; i < allVerts.size(); i++) { + output[i] = allVerts[i]; + } + + cVert* wireOutput = new cVert[wireframeVerts.size()]; + for (int i = 0; i < wireframeVerts.size(); i++) { + wireOutput[i] = wireframeVerts[i]; + } + + if (allVerts.size() == 0 || wireframeVerts.size() == 0) { + renderClip->clipnodeBuffer[hull] = NULL; + renderClip->wireframeClipnodeBuffer[hull] = NULL; + return; + } + + renderClip->clipnodeBuffer[hull] = new VertexBuffer(g_app->colorShader, output, (int)allVerts.size(), GL_TRIANGLES); + renderClip->clipnodeBuffer[hull]->ownData = true; + + renderClip->wireframeClipnodeBuffer[hull] = new VertexBuffer(g_app->colorShader, wireOutput, (int)wireframeVerts.size(), GL_LINES); + renderClip->wireframeClipnodeBuffer[hull]->ownData = true; + + renderClip->faceMaths[hull] = navFaceMaths; + + std::ofstream file(map->bsp_name + "_hull" + std::to_string(hull) + ".obj", std::ios::out | std::ios::trunc); + for (int i = 0; i < allVerts.size(); i++) { + vec3 v = vec3(allVerts[i].pos.x, allVerts[i].pos.y, allVerts[i].pos.z); + file << "v " << std::fixed << std::setprecision(2) << v.x << " " << v.y << " " << v.z << std::endl; + } + for (int i = 0; i < allVerts.size(); i += 3) { + file << "f " << (i + 1) << " " << (i + 2) << " " << (i + 3) << std::endl; + } + print_log("Wrote {} verts\n", allVerts.size()); + file.close(); +} + +void BspRenderer::generateLeafNavMeshBuffer() { + int hull = NAV_HULL; + RenderClipnodes* renderClip = &renderClipnodes[0]; + renderClip->clipnodeBuffer[hull] = NULL; + renderClip->wireframeClipnodeBuffer[hull] = NULL; + + LeafNavMesh* navMesh = LeafNavMeshGenerator().generate(map); + g_app->debugLeafNavMesh = navMesh; + + static COLOR4 hullColors[] = { + COLOR4(255, 255, 255, 128), + COLOR4(96, 255, 255, 128), + COLOR4(255, 96, 255, 128), + COLOR4(255, 255, 96, 128), + }; + COLOR4 color = hullColors[hull]; + + std::vector allVerts; + std::vector wireframeVerts; + std::vector navFaceMaths; + + for (int lf = 0; lf < navMesh->nodes.size(); lf++) { + LeafNode& mesh = navMesh->nodes[lf]; + + color = hullColors[hull]; + static int r = 0; + r = (r + 1) % 8; + if (r == 0) { + color = COLOR4(255, 32, 32, 128); + } + else if (r == 1) { + color = COLOR4(255, 255, 32, 128); + } + else if (r == 2) { + color = COLOR4(255, 32, 255, 128); + } + else if (r == 3) { + color = COLOR4(255, 128, 255, 128); + } + else if (r == 4) { + color = COLOR4(32, 32, 255, 128); + } + else if (r == 5) { + color = COLOR4(32, 255, 255, 128); + } + else if (r == 6) { + color = COLOR4(32, 128, 255, 128); + } + else if (r == 7) { + color = COLOR4(32, 255, 128, 128); + } + + for (int m = 0; m < mesh.leafFaces.size(); m++) { + Polygon3D& poly = mesh.leafFaces[m]; + + vec3 normal = poly.plane_z; + + // calculations for face picking + { + FaceMath faceMath; + faceMath.normal = normal; + faceMath.fdist = poly.fdist; + faceMath.worldToLocal = poly.worldToLocal; + faceMath.localVerts = poly.localVerts; + navFaceMaths.push_back(faceMath); + } + + // create the verts for rendering + { + std::vector renderVerts; + renderVerts.resize(poly.verts.size()); + for (int i = 0; i < poly.verts.size(); i++) { + renderVerts[i] = poly.verts[i].flip(); + } + + COLOR4 wireframeColor = { 0, 0, 0, 255 }; + for (int k = 0; k < renderVerts.size(); k++) { + wireframeVerts.push_back(cVert(renderVerts[k], wireframeColor)); + wireframeVerts.push_back(cVert(renderVerts[(k + 1) % renderVerts.size()], wireframeColor)); + } + + vec3 lightDir = vec3(1, 1, -1).normalize(); + float dot = (dotProduct(normal, lightDir) + 1) / 2.0f; + if (dot > 0.5f) { + dot = dot * dot; + } + COLOR4 faceColor = color * (dot); + + // convert from TRIANGLE_FAN style verts to TRIANGLES + for (int k = 2; k < renderVerts.size(); k++) { + allVerts.push_back(cVert(renderVerts[0], faceColor)); + allVerts.push_back(cVert(renderVerts[k - 1], faceColor)); + allVerts.push_back(cVert(renderVerts[k], faceColor)); + } + } + } + } + + cVert* output = new cVert[allVerts.size()]; + for (int i = 0; i < allVerts.size(); i++) { + output[i] = allVerts[i]; + } + + cVert* wireOutput = new cVert[wireframeVerts.size()]; + for (int i = 0; i < wireframeVerts.size(); i++) { + wireOutput[i] = wireframeVerts[i]; + } + + if (allVerts.size() == 0 || wireframeVerts.size() == 0) { + renderClip->clipnodeBuffer[hull] = NULL; + renderClip->wireframeClipnodeBuffer[hull] = NULL; + return; + } + + renderClip->clipnodeBuffer[hull] = new VertexBuffer(g_app->colorShader, output, (int)allVerts.size(), GL_TRIANGLES); + renderClip->clipnodeBuffer[hull]->ownData = true; + + renderClip->wireframeClipnodeBuffer[hull] = new VertexBuffer(g_app->colorShader, wireOutput, (int)wireframeVerts.size(), GL_LINES); + renderClip->wireframeClipnodeBuffer[hull]->ownData = true; + + renderClip->faceMaths[hull] = navFaceMaths; +} + + void BspRenderer::generateClipnodeBufferForHull(int modelIdx, int hullIdx) { if (hullIdx < 0 || hullIdx > 3) @@ -1433,11 +1694,11 @@ void BspRenderer::generateClipnodeBufferForHull(int modelIdx, int hullIdx) memcpy(wireOutput, cachedRenderClip->wireframeClipnodeBuffer[oldHullIdxStruct.hullIdx]->data, cachedRenderClip->wireframeClipnodeBuffer[oldHullIdxStruct.hullIdx]->numVerts * sizeof(cVert)); - renderClip->clipnodeBuffer[hullIdx] = new VertexBuffer(colorShader, output, + renderClip->clipnodeBuffer[hullIdx] = new VertexBuffer(g_app->colorShader, output, (GLsizei)cachedRenderClip->clipnodeBuffer[oldHullIdxStruct.hullIdx]->numVerts, GL_TRIANGLES); renderClip->clipnodeBuffer[hullIdx]->ownData = true; - renderClip->wireframeClipnodeBuffer[hullIdx] = new VertexBuffer(colorShader, wireOutput, + renderClip->wireframeClipnodeBuffer[hullIdx] = new VertexBuffer(g_app->colorShader, wireOutput, @@ -1446,7 +1707,7 @@ void BspRenderer::generateClipnodeBufferForHull(int modelIdx, int hullIdx) } - std::vector solidNodes = map->get_model_leaf_volume_cuts(modelIdx, hullIdx); + std::vector solidNodes = map->get_model_leaf_volume_cuts(modelIdx, hullIdx, CONTENTS_SOLID); // std::vector meshes; for (size_t k = 0; k < solidNodes.size(); k++) @@ -1656,6 +1917,12 @@ void BspRenderer::generateClipnodeBuffer(int modelIdx) { generateClipnodeBufferForHull(modelIdx, i); } + + if (modelIdx == 0) + { + /*generateNavMeshBuffer(); + generateLeafNavMeshBuffer();*/ + } } void BspRenderer::updateClipnodeOpacity(unsigned char newValue) @@ -3271,7 +3538,7 @@ bool BspRenderer::pickModelPoly(vec3 start, const vec3& dir, vec3 offset, int mo hullIdx = getBestClipnodeHull(modelIdx); } - if (clipnodesLoaded && (selectWorldClips || selectEntClips) && hullIdx != -1) + if (clipnodesLoaded && (selectWorldClips || selectEntClips) && hullIdx >= 0 && modelIdx >= 0 && modelIdx < map->modelCount) { int nodeIdx = map->models[modelIdx].iHeadnodes[hullIdx]; nodeBuffStr oldHullIdxStruct = nodeBuffStr(); @@ -3294,7 +3561,7 @@ bool BspRenderer::pickModelPoly(vec3 start, const vec3& dir, vec3 offset, int mo oldHullIdxStruct.hullIdx = hullIdx; generateClipnodeBufferForHull(modelIdx, hullIdx); } - for (size_t i = 0; i < renderClipnodes[oldHullIdxStruct.modelIdx].faceMaths[oldHullIdxStruct.hullIdx].size(); i++) + for (int i = 0; i < (int)renderClipnodes[oldHullIdxStruct.modelIdx].faceMaths[oldHullIdxStruct.hullIdx].size(); i++) { FaceMath& faceMath = renderClipnodes[oldHullIdxStruct.modelIdx].faceMaths[oldHullIdxStruct.hullIdx][i]; @@ -3304,6 +3571,25 @@ bool BspRenderer::pickModelPoly(vec3 start, const vec3& dir, vec3 offset, int mo foundBetterPick = true; tempPickInfo.bestDist = t; tempPickInfo.selectedFaces.clear(); + + + // Nav mesh WIP code + if (g_app->debugNavMesh && modelIdx == 0 && hullIdx == 3) { + static int lastPick = 0; + + g_app->debugPoly = debugFaces[i]; + g_app->debugNavPoly = i; + + //Polygon3D merged = debugFaces[lastPick].merge(debugFaces[i]); + //vector> split = debugFaces[i].split(debugFaces[lastPick]); + //logf("split %d by %d == %d\n", i, lastPick, split.size()); + + NavNode& node = g_app->debugNavMesh->nodes[i]; + + lastPick = i; + print_log("Picked hull {}, face {}, verts {}, area {}\nNav links {}\n", hullIdx, i, debugFaces[i].verts.size(), debugFaces[i].area, node.numLinks()); + } + } } } @@ -3393,7 +3679,6 @@ void BspRenderer::pushEntityUndoStateDelay(const std::string& actionDesc, int en delayEntUndoList.push_back({ actionDesc,entIdx,ent }); } - void BspRenderer::pushEntityUndoState(const std::string& actionDesc, int entIdx) { if (g_verbose) diff --git a/src/editor/BspRenderer.h b/src/editor/BspRenderer.h index 5b9b9732..bb687eb3 100644 --- a/src/editor/BspRenderer.h +++ b/src/editor/BspRenderer.h @@ -1,14 +1,17 @@ #pragma once #pragma once #include "Bsp.h" +#include +#include #include "Texture.h" #include "ShaderProgram.h" #include "LightmapNode.h" #include "VertexBuffer.h" #include "primitives.h" #include "PointEntRenderer.h" -#include -#include +#include "NavMeshGenerator.h" +#include "LeafNavMeshGenerator.h" + #include #include "mdl_studio.h" #include "Sprite.h" @@ -258,7 +261,8 @@ class BspRenderer int numRenderLightmapInfos; int numLoadedTextures; - + std::vector debugFaces; + NavMesh* debugNavMesh; std::vector* glTextures = NULL; // textures loaded in a separate thread @@ -279,7 +283,9 @@ class BspRenderer void addNewRenderFace(); void loadClipnodes(); void generateClipnodeBufferForHull(int modelIdx, int hullId); - void generateClipnodeBuffer(int modelIdx); + void generateClipnodeBuffer(int modelIdx); + void generateNavMeshBuffer(); + void generateLeafNavMeshBuffer(); void deleteRenderModelClipnodes(RenderClipnodes* renderClip); void deleteRenderClipnodes(); void deleteRenderFaces(); diff --git a/src/editor/Command.cpp b/src/editor/Command.cpp index 22e952de..efbeffeb 100644 --- a/src/editor/Command.cpp +++ b/src/editor/Command.cpp @@ -209,6 +209,113 @@ size_t CreateEntityCommand::memoryUsage() return sizeof(CreateEntityCommand) + entData->getMemoryUsage(); } +// +// Create Entities From Text +// +CreateEntityFromTextCommand::CreateEntityFromTextCommand(std::string desc, int mapIdx, std::string textData) : Command(desc, mapIdx) { + this->textData = textData; + this->allowedDuringLoad = true; +} + +CreateEntityFromTextCommand::~CreateEntityFromTextCommand() { +} + +void CreateEntityFromTextCommand::execute() { + Bsp* map = getBsp(); + + std::istringstream in(textData); + + int lineNum = 0; + int lastBracket = -1; + Entity* ent = NULL; + + std::vector ents; + + std::string line = ""; + while (std::getline(in, line)) + { + lineNum++; + if (line.length() < 1 || line[0] == '\n') + continue; + + if (line[0] == '{') + { + if (lastBracket == 0) + { + print_log("clipboard ent text data (line {}): Unexpected '{'\n", lineNum); + continue; + } + lastBracket = 0; + + if (ent != NULL) + delete ent; + ent = new Entity(); + } + else if (line[0] == '}') + { + if (lastBracket == 1) + print_log("clipboard ent text data (line {}): Unexpected '}'\n", lineNum); + lastBracket = 1; + + if (ent == NULL) + continue; + + if (ent->keyvalues.count("classname")) + ents.push_back(ent); + else + print_log("Found unknown classname entity. Skip it.\n"); + ent = NULL; + + // you can end/start an ent on the same line, you know + if (line.find("{") != std::string::npos) + { + ent = new Entity(); + lastBracket = 0; + } + } + else if (lastBracket == 0 && ent != NULL) // currently defining an entity + { + Keyvalues keyvals(line); + for (size_t k = 0; k < keyvals.keys.size();k++) + { + if (keyvals.keys[k].length() && keyvals.values[k].length()) + ent->addKeyvalue(keyvals.keys[k], keyvals.values[k]); + } + } + } + + for (Entity* e : ents) { + map->ents.push_back(e); + } + createdEnts = ents.size(); + print_log("Pasted {} entities from clipboard\n", createdEnts); + + refresh(); +} + +void CreateEntityFromTextCommand::undo() { + Bsp* map = getBsp(); + + g_app->deselectObject(); + + for (int i = 0; i < createdEnts; i++) { + delete map->ents[map->ents.size() - 1]; + map->ents.pop_back(); + } + + refresh(); +} + +void CreateEntityFromTextCommand::refresh() { + BspRenderer* renderer = getBspRenderer(); + renderer->preRenderEnts(); + g_app->gui->refresh(); + g_app->updateCullBox(); +} + +size_t CreateEntityFromTextCommand::memoryUsage() { + return sizeof(CreateEntityFromTextCommand) + textData.size(); +} // // Duplicate BSP Model command @@ -591,6 +698,58 @@ size_t EditBspModelCommand::memoryUsage() } +// +// Delete boxed data +// +DeleteBoxedDataCommand::DeleteBoxedDataCommand(std::string desc, int mapIdx, vec3 mins, vec3 maxs, LumpState oldLumps) : Command(desc, mapIdx) { + this->oldLumps = oldLumps; + this->allowedDuringLoad = false; + this->mins = mins; + this->maxs = maxs; +} + +DeleteBoxedDataCommand::~DeleteBoxedDataCommand() +{ + +} + +void DeleteBoxedDataCommand::execute() { + Bsp* map = getBsp(); + + map->delete_box_data(mins, maxs); + + refresh(); +} + +void DeleteBoxedDataCommand::undo() { + Bsp* map = getBsp(); + + map->replace_lumps(oldLumps); + map->load_ents(); + + refresh(); +} + +void DeleteBoxedDataCommand::refresh() { + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); +} + +size_t DeleteBoxedDataCommand::memoryUsage() +{ + size_t size = sizeof(DeleteBoxedDataCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumps[i].size(); + } + + return size; +} + + // // Clean Map @@ -660,6 +819,204 @@ size_t CleanMapCommand::memoryUsage() } +// +// Delete OOB data +// +DeleteOobDataCommand::DeleteOobDataCommand(std::string desc, int mapIdx, int clipFlags, LumpState oldLumps) : Command(desc, mapIdx) { + this->oldLumps = oldLumps; + this->allowedDuringLoad = false; + this->clipFlags = clipFlags; +} + +DeleteOobDataCommand::~DeleteOobDataCommand() +{ + +} + +void DeleteOobDataCommand::execute() { + Bsp* map = getBsp(); + + map->delete_oob_data(clipFlags); + + refresh(); +} + +void DeleteOobDataCommand::undo() { + Bsp* map = getBsp(); + + map->replace_lumps(oldLumps); + map->load_ents(); + + refresh(); +} + +void DeleteOobDataCommand::refresh() { + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); +} + +size_t DeleteOobDataCommand::memoryUsage() { + size_t size = sizeof(DeleteOobDataCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumps[i].size(); + } + + return size; +} + + +// +// Fix bad surface extents +// +FixSurfaceExtentsCommand::FixSurfaceExtentsCommand(std::string desc, int mapIdx, bool scaleNotSubdivide, + bool downscaleOnly, int maxTextureDim, LumpState oldLumps) : Command(desc, mapIdx) { + this->oldLumps = oldLumps; + this->allowedDuringLoad = false; + this->scaleNotSubdivide = scaleNotSubdivide; + this->downscaleOnly = downscaleOnly; + this->maxTextureDim = maxTextureDim; +} + +FixSurfaceExtentsCommand::~FixSurfaceExtentsCommand() +{ + +} + +void FixSurfaceExtentsCommand::execute() { + Bsp* map = getBsp(); + + map->fix_bad_surface_extents(scaleNotSubdivide, downscaleOnly, maxTextureDim); + + refresh(); +} + +void FixSurfaceExtentsCommand::undo() { + Bsp* map = getBsp(); + + map->replace_lumps(oldLumps); + + refresh(); +} + +void FixSurfaceExtentsCommand::refresh() { + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); +} + +size_t FixSurfaceExtentsCommand::memoryUsage() { + size_t size = sizeof(FixSurfaceExtentsCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumps[i].size(); + } + + return size; +} + + +// +// Deduplicate models +// +DeduplicateModelsCommand::DeduplicateModelsCommand(std::string desc, int mapIdx, LumpState oldLumps) : Command(desc, mapIdx) { + this->oldLumps = oldLumps; + this->allowedDuringLoad = false; +} + +DeduplicateModelsCommand::~DeduplicateModelsCommand() { + +} + +void DeduplicateModelsCommand::execute() { + Bsp* map = getBsp(); + + map->deduplicate_models(); + + refresh(); +} + +void DeduplicateModelsCommand::undo() { + Bsp* map = getBsp(); + + map->replace_lumps(oldLumps); + map->load_ents(); + + refresh(); +} + +void DeduplicateModelsCommand::refresh() { + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); +} + +size_t DeduplicateModelsCommand::memoryUsage() { + size_t size = sizeof(DeduplicateModelsCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumps[i].size(); + } + + return size; +} + + +// +// Move the entire map +// +MoveMapCommand::MoveMapCommand(std::string desc, int mapIdx, vec3 offset, LumpState oldLumps) : Command(desc, mapIdx) { + this->oldLumps = oldLumps; + this->allowedDuringLoad = false; + this->offset = offset; +} + +MoveMapCommand::~MoveMapCommand() { + +} + +void MoveMapCommand::execute() { + Bsp* map = getBsp(); + + map->ents[0]->removeKeyvalue("origin"); + map->move(offset); + + refresh(); +} + +void MoveMapCommand::undo() { + Bsp* map = getBsp(); + + map->replace_lumps(oldLumps); + map->ents[0]->setOrAddKeyvalue("origin", offset.toKeyvalueString()); + + refresh(); +} + +void MoveMapCommand::refresh() { + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); +} + +size_t MoveMapCommand::memoryUsage() { + size_t size = sizeof(MoveMapCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumps[i].size(); + } + + return size; +} // // Optimize Map diff --git a/src/editor/Command.h b/src/editor/Command.h index cf854006..0c8494e9 100644 --- a/src/editor/Command.h +++ b/src/editor/Command.h @@ -81,6 +81,22 @@ class CreateEntityCommand : public Command }; + +class CreateEntityFromTextCommand : public Command { +public: + std::string textData; + size_t createdEnts; + + CreateEntityFromTextCommand(std::string desc, int mapIdx, std::string textData); + ~CreateEntityFromTextCommand(); + + void execute(); + void undo(); + void refresh(); + size_t memoryUsage(); +}; + + class DuplicateBspModelCommand : public Command { public: @@ -167,3 +183,76 @@ class OptimizeMapCommand : public Command void refresh(); size_t memoryUsage() override; }; + + +class DeleteBoxedDataCommand : public Command { +public: + LumpState oldLumps = LumpState(); + vec3 mins, maxs; + + DeleteBoxedDataCommand(std::string desc, int mapIdx, vec3 mins, vec3 maxs, LumpState oldLumps); + ~DeleteBoxedDataCommand(); + + void execute(); + void undo(); + void refresh(); + size_t memoryUsage(); +}; + +class DeleteOobDataCommand : public Command { +public: + LumpState oldLumps = LumpState(); + int clipFlags; + + DeleteOobDataCommand(std::string desc, int mapIdx, int clipFlags, LumpState oldLumps); + ~DeleteOobDataCommand(); + + void execute(); + void undo(); + void refresh(); + size_t memoryUsage(); +}; + +class FixSurfaceExtentsCommand : public Command { +public: + LumpState oldLumps = LumpState(); + bool scaleNotSubdivide; + bool downscaleOnly; + int maxTextureDim; + + FixSurfaceExtentsCommand(std::string desc, int mapIdx, bool scaleNotSubdivide, bool downscaleOnly, int maxTextureDim, LumpState oldLumps); + ~FixSurfaceExtentsCommand(); + + void execute(); + void undo(); + void refresh(); + size_t memoryUsage(); +}; + +class DeduplicateModelsCommand : public Command { +public: + LumpState oldLumps = LumpState(); + + DeduplicateModelsCommand(std::string desc, int mapIdx, LumpState oldLumps); + ~DeduplicateModelsCommand(); + + void execute(); + void undo(); + void refresh(); + size_t memoryUsage(); +}; + +class MoveMapCommand : public Command { +public: + LumpState oldLumps = LumpState(); + vec3 offset; + + MoveMapCommand(std::string desc, int mapIdx, vec3 offset, LumpState oldLumps); + ~MoveMapCommand(); + + void execute(); + void undo(); + void refresh(); + size_t memoryUsage(); +}; + diff --git a/src/editor/Gui.cpp b/src/editor/Gui.cpp index 7f682f56..83e9c50e 100644 --- a/src/editor/Gui.cpp +++ b/src/editor/Gui.cpp @@ -1,11 +1,11 @@ #include "lang.h" #include "Gui.h" +#include "Renderer.h" #include "ShaderProgram.h" #include "primitives.h" #include "VertexBuffer.h" #include "shaders.h" #include "Settings.h" -#include "Renderer.h" #include "BspMerger.h" #include "filedialog/ImFileDialog.h" #include "imgui_stdlib.h" @@ -21,8 +21,9 @@ #include #endif #include +#include "LeafNavMesh.h" -float g_tooltip_delay = 0.6f; // time in seconds before showing a tooltip +float g_tooltip_delay = 0.6f; // time in seconds before showing a IMGUI_TOOLTIP bool filterNeeded = true; @@ -66,6 +67,17 @@ int cell_idx(const vec3& pos, const vec3& mins, float cell_size, int cell_x, int return index < 0 ? 0 : index; } +void IMGUI_TOOLTIP(ImGuiContext& g, const std::string& IMGUI_TOOLTIP) +{ + if (ImGui::IsItemHovered() && g.HoveredIdTimer > g_tooltip_delay) { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(IMGUI_TOOLTIP.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + Gui::Gui(Renderer* app) { guiHoverAxis = 0; @@ -1842,12 +1854,12 @@ void Gui::drawMenuBar() { ImGuiContext& g = *GImGui; static bool ditheringEnabled = false; + Bsp* map = app->getSelectedMap(); + BspRenderer* rend = map ? + rend = map->getBspRender() : NULL; if (ImGui::BeginMainMenuBar()) { - Bsp* map = app->getSelectedMap(); - BspRenderer* rend = NULL; - if (ifd::FileDialog::Instance().IsDone("PngDirOpenDialog")) { if (ifd::FileDialog::Instance().HasResult()) @@ -1871,7 +1883,6 @@ void Gui::drawMenuBar() if (map) { - rend = map->getBspRender(); if (ifd::FileDialog::Instance().IsDone("WadOpenDialog")) { if (ifd::FileDialog::Instance().HasResult()) @@ -3882,6 +3893,24 @@ void Gui::drawMenuBar() } } + /* + + if (ImGui::MenuItem("Merge", NULL, false, !app->isLoading)) { + char* fname = tinyfd_openFileDialog("Merge Map", "", + 1, bspFilterPatterns, "GoldSrc Map Files (*.bsp)", 1); + + if (fname) + g_app->merge(fname); + } + Bsp* map = g_app->mapRenderers[0]->map; + tooltip(g, ("Merge one other BSP into the current file.\n\n" + "Equivalent CLI command:\nbspguy merge " + map->name + " -noscript -noripent -maps \"" + + map->name + ",other_map\"\n\nUse the CLI for automatic arrangement and optimization of " + "many maps. The CLI also offers ripent fixes and script setup which can " + "generate a playable map without you having to make any manual edits (Sven Co-op only).").c_str()); + + */ + if (ImGui::MenuItem(get_localized_string(LANG_0552).c_str(), 0, false, map && !map->is_mdl_model && !app->isLoading)) { app->reloadMaps(); @@ -3950,7 +3979,6 @@ void Gui::drawMenuBar() bool canUndo = undoCmd && (!app->isLoading || undoCmd->allowedDuringLoad); bool canRedo = redoCmd && (!app->isLoading || redoCmd->allowedDuringLoad); bool entSelected = app->pickInfo.selectedEnts.size(); - bool mapSelected = map; bool nonWorldspawnEntSelected = entSelected; if (nonWorldspawnEntSelected) @@ -3987,7 +4015,7 @@ void Gui::drawMenuBar() if (app->pickInfo.selectedFaces.size()) copyTexture(); } - if (ImGui::MenuItem(get_localized_string(LANG_1157).c_str(), get_localized_string(LANG_1158).c_str(), false, mapSelected && app->copiedEnts.size())) + if (ImGui::MenuItem(get_localized_string(LANG_1157).c_str(), get_localized_string(LANG_1158).c_str(), false, app->getSelectedMap() && app->copiedEnts.size())) { app->pasteEnt(false); } @@ -4007,6 +4035,17 @@ void Gui::drawMenuBar() pickCount++; } + + const char* clipBoardText = ImGui::GetClipboardText(); + if (ImGui::MenuItem("Paste entities from clipboard", 0, false, clipBoardText && clipBoardText[0] == '{')) { + app->pasteEntsFromText(clipBoardText); + } + + IMGUI_TOOLTIP(g, "Creates entities from text data. You can use this to transfer entities " + "from one bspguy window to another, or paste from .ent file text. Copy any entity " + "in the viewer then paste to a text editor to see the format of the text data."); + + ImGui::Separator(); @@ -4329,7 +4368,169 @@ void Gui::drawMenuBar() ImGui::EndTooltip(); } - if (ImGui::BeginMenu("MAP TRANFORMATION [WIP]", map)) + if (ImGui::BeginMenu("Porting tools")) + { + if (ImGui::BeginMenu("Delete OOB Data", !app->isLoading && app->getSelectedMap())) + { + + static const char* optionNames[10] = { + "All Axes", + "X Axis", + "X Axis (positive only)", + "X Axis (negative only)", + "Y Axis", + "Y Axis (positive only)", + "Y Axis (negative only)", + "Z Axis", + "Z Axis (positive only)", + "Z Axis (negative only)", + }; + + static int clipFlags[10] = { + -1, + OOB_CLIP_X | OOB_CLIP_X_NEG, + OOB_CLIP_X, + OOB_CLIP_X_NEG, + OOB_CLIP_Y | OOB_CLIP_Y_NEG, + OOB_CLIP_Y, + OOB_CLIP_Y_NEG, + OOB_CLIP_Z | OOB_CLIP_Z_NEG, + OOB_CLIP_Z, + OOB_CLIP_Z_NEG, + }; + + for (int i = 0; i < 10; i++) { + if (ImGui::MenuItem(optionNames[i], 0, false, !app->isLoading && app->getSelectedMap())) { + if (map->ents[0]->hasKey("origin")) { + vec3 ori = map->ents[0]->origin; + print_log("Moved worldspawn origin by {} {} {}\n", ori.x, ori.y, ori.z); + map->move(ori); + map->ents[0]->removeKeyvalue("origin"); + + } + + DeleteOobDataCommand* command = new DeleteOobDataCommand("Delete OOB Data", + app->getSelectedMapId(), clipFlags[i], rend->undoLumpState); + rend->pushUndoCommand(command); + } + IMGUI_TOOLTIP(g, "Deletes BSP data and entities outside of the " + "max map boundary.\n\n" + "This is useful for splitting maps to run in an engine with stricter map limits."); + } + + ImGui::EndMenu(); + } + if (ImGui::MenuItem("Delete Boxed Data", 0, false, !app->isLoading && app->getSelectedMap())) { + if (!g_app->hasCullbox) { + print_log("Create at least 2 entities with \"cull\" as a classname first!\n"); + } + else { + DeleteBoxedDataCommand* command = new DeleteBoxedDataCommand("Delete Boxed Data", + app->getSelectedMapId(), g_app->cullMins, g_app->cullMaxs, rend->undoLumpState); + rend->pushUndoCommand(command); + } + + } + IMGUI_TOOLTIP(g, "Deletes BSP data and entities inside of a box defined by 2 \"cull\" entities " + "(for the min and max extent of the box). This is useful for getting maps to run in an " + "engine with stricter map limits.\n\n" + "Create 2 cull entities from the \"Create\" menu to define the culling box. " + "A transparent red box will form between them."); + if (ImGui::MenuItem("Deduplicate Models", 0, false, !app->isLoading && app->getSelectedMap())) { + DeduplicateModelsCommand* command = new DeduplicateModelsCommand("Deduplicate models", + app->getSelectedMapId(), rend->undoLumpState); + rend->pushUndoCommand(command); + } + IMGUI_TOOLTIP(g, "Scans for duplicated BSP models and updates entity model keys to reference only one model in set of duplicated models. " + "This lowers the model count and allows more game models to be precached.\n\n" + "This does not delete BSP data structures unless you run the Clean command afterward."); + if (ImGui::MenuItem("Downscale Invalid Textures", "(WIP)", false, !app->isLoading && app->getSelectedMap())) { + map->downscale_invalid_textures(); + + if (rend) + { + rend->preRenderFaces(); + g_app->gui->refresh(); + reloadLimits(); + } + } + IMGUI_TOOLTIP(g, "Shrinks textures that exceed the max texture size and adjusts texture coordinates accordingly. Does not work with WAD textures yet.\n"); + if (ImGui::BeginMenu("Fix Bad Surface Extents", !app->isLoading && app->getSelectedMap())) + { + if (ImGui::MenuItem("Shrink Textures (512)", 0, false, !app->isLoading && app->getSelectedMap())) { + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Shrink textures (512)", + app->getSelectedMapId(), false, true, 512, rend->undoLumpState); + rend->pushUndoCommand(command); + } + IMGUI_TOOLTIP(g, "Downscales embedded textures on bad faces to a max resolution of 512x512 pixels. " + "This alone will likely not be enough to fix all faces with bad surface extents." + "You may also have to apply the Subdivide or Scale methods."); + + if (ImGui::MenuItem("Shrink Textures (256)", 0, false, !app->isLoading && app->getSelectedMap())) { + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Shrink textures (256)", + app->getSelectedMapId(), false, true, 256, rend->undoLumpState); + rend->pushUndoCommand(command); + } + IMGUI_TOOLTIP(g, "Downscales embedded textures on bad faces to a max resolution of 256x256 pixels. " + "This alone will likely not be enough to fix all faces with bad surface extents." + "You may also have to apply the Subdivide or Scale methods."); + + if (ImGui::MenuItem("Shrink Textures (128)", 0, false, !app->isLoading && app->getSelectedMap())) { + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Shrink textures (128)", + app->getSelectedMapId(), false, true, 128, rend->undoLumpState); + rend->pushUndoCommand(command); + } + IMGUI_TOOLTIP(g, "Downscales embedded textures on bad faces to a max resolution of 128x128 pixels. " + "This alone will likely not be enough to fix all faces with bad surface extents." + "You may also have to apply the Subdivide or Scale methods."); + + if (ImGui::MenuItem("Shrink Textures (64)", 0, false, !app->isLoading && app->getSelectedMap())) { + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Shrink textures (64)", + app->getSelectedMapId(), false, true, 64, rend->undoLumpState); + rend->pushUndoCommand(command); + } + IMGUI_TOOLTIP(g, "Downscales embedded textures to a max resolution of 64x64 pixels. " + "This alone will likely not be enough to fix all faces with bad surface extents." + "You may also have to apply the Subdivide or Scale methods."); + + ImGui::Separator(); + + if (ImGui::MenuItem("Scale", 0, false, !app->isLoading && app->getSelectedMap())) { + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Scale faces", + app->getSelectedMapId(), true, false, 0, rend->undoLumpState); + rend->pushUndoCommand(command); + } + IMGUI_TOOLTIP(g, "Scales up face textures until they have valid extents. The drawback to this method is shifted texture coordinates and lower apparent texture quality."); + + if (ImGui::MenuItem("Subdivide", 0, false, !app->isLoading && app->getSelectedMap())) { + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Subdivide faces", + app->getSelectedMapId(), false, false, 0, rend->undoLumpState); + rend->pushUndoCommand(command); + } + IMGUI_TOOLTIP(g, "Subdivides faces until they have valid extents. The drawback to this method is reduced in-game performace from higher poly counts."); + + ImGui::MenuItem("##", "WIP"); + IMGUI_TOOLTIP(g, "Anything you choose here will break lightmaps. " + "Run the map through a RAD compiler to fix, and pray that the mapper didn't " + "customize compile settings much."); + ImGui::EndMenu(); + } + if (ImGui::MenuItem("Cull Entity", 0, false, app->getSelectedMap())) { + Entity* newEnt = new Entity(); + vec3 origin = (cameraOrigin + app->cameraForward * 100); + if (app->gridSnappingEnabled) + origin = app->snapToGrid(origin); + newEnt->addKeyvalue("origin", origin.toKeyvalueString()); + newEnt->addKeyvalue("classname", "cull"); + + CreateEntityCommand* createCommand = new CreateEntityCommand("Create Entity", app->getSelectedMapId(), newEnt); + delete newEnt; + rend->pushUndoCommand(createCommand); + } + IMGUI_TOOLTIP(g, "Create a point entity for use with the culling tool. 2 of these define the bounding box for structure culling operations.\n"); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("MAP TRANSFORMATION [WIP]", map)) { if (ImGui::MenuItem("Mirror map x/y", NULL, false, map)) { @@ -5401,7 +5602,6 @@ void Gui::drawMenuBar() } ImGui::EndMenu(); } - if (ImGui::BeginMenu(get_localized_string(LANG_0592).c_str())) { if (map && map->is_mdl_model) @@ -5836,7 +6036,6 @@ void Gui::drawMenuBar() if (selectedMap) { - BspRenderer* rend = selectedMap->getBspRender(); if (rend) { ImGui::TextUnformatted(fmt::format("Click [{:^5},{:^5},{:^5}]", floatRound(rend->intersectVec.x), floatRound(rend->intersectVec.y), floatRound(rend->intersectVec.z)).c_str()); @@ -5870,7 +6069,6 @@ void Gui::drawMenuBar() } ImGui::End(); } - } void Gui::drawToolbar() @@ -6352,6 +6550,16 @@ void Gui::drawDebugWidget() { ImGui::Text(fmt::format(fmt::runtime(get_localized_string(LANG_0381)), leafIdx).c_str()); } + else if (i == 3 && g_app->debugLeafNavMesh) { + int tmpLeafIdx = map->get_leaf(renderer->localCameraOrigin, 3); + int leafNavIdx = -1; + + if (tmpLeafIdx >= 0 && tmpLeafIdx < MAX_MAP_CLIPNODE_LEAVES) { + leafNavIdx = g_app->debugLeafNavMesh->leafMap[tmpLeafIdx]; + } + + ImGui::Text("Nav ID: %d", leafNavIdx); + } ImGui::Text(fmt::format("Parent Node: {} (child {})", nodeBranch.size() ? nodeBranch[nodeBranch.size() - 1] : headNode, childIdx).c_str()); @@ -9592,26 +9800,28 @@ void Gui::drawMergeWindow() print_log("\n"); } BspMerger merger; - Bsp* result = merger.merge(maps, vec3(), outPath, NoRipent, NoScript, NoStyles); + MergeResult result = merger.merge(maps, vec3(), outPath, NoRipent, NoScript, false, NoStyles); print_log("\n"); - if (result->isValid()) result->write(outPath); - print_log("\n"); - result->print_info(false, 0, 0); + if (result.map && result.map->isValid()) + { + result.map->write(outPath); + print_log("\n"); + result.map->print_info(false, 0, 0); - app->clearMaps(); + app->clearMaps(); - fixupPath(outPath, FIXUPPATH_SLASH::FIXUPPATH_SLASH_SKIP, FIXUPPATH_SLASH::FIXUPPATH_SLASH_SKIP); + fixupPath(outPath, FIXUPPATH_SLASH::FIXUPPATH_SLASH_SKIP, FIXUPPATH_SLASH::FIXUPPATH_SLASH_SKIP); - if (fileExists(outPath)) - { - result = new Bsp(outPath); - app->addMap(result); - } - else - { - print_log(PRINT_RED | PRINT_INTENSITY, get_localized_string(LANG_0398)); - app->addMap(new Bsp()); + if (fileExists(outPath)) + { + app->addMap(new Bsp(outPath)); + } + else + { + print_log(PRINT_RED | PRINT_INTENSITY, get_localized_string(LANG_0398)); + app->addMap(new Bsp()); + } } for (auto& map : maps) diff --git a/src/editor/Gui.h b/src/editor/Gui.h index 58b39527..7e744e1f 100644 --- a/src/editor/Gui.h +++ b/src/editor/Gui.h @@ -62,6 +62,8 @@ class Gui void refresh(); + + bool polycount = false; bool showDebugWidget = false; bool showKeyvalueWidget = false; bool showTransformWidget = false; diff --git a/src/editor/Renderer.cpp b/src/editor/Renderer.cpp index 960c9054..a06e7fba 100644 --- a/src/editor/Renderer.cpp +++ b/src/editor/Renderer.cpp @@ -1,5 +1,4 @@ #include "lang.h" -#include "Settings.h" #include "Renderer.h" #include "ShaderProgram.h" #include "primitives.h" @@ -13,6 +12,9 @@ #include #include +#include "NavMesh.h" +#include "LeafNavMesh.h" +#include "Settings.h" Renderer* g_app = NULL; std::vector mapRenderers{}; @@ -72,22 +74,22 @@ void drop_callback(GLFWwindow* window, int count, const char** paths) if (fileExists(tmpPath.string())) { - if (ends_with(lowerPath,".bsp")) + if (ends_with(lowerPath, ".bsp")) { print_log(get_localized_string(LANG_0896), tmpPath.string()); g_app->addMap(new Bsp(tmpPath.string())); } - else if (ends_with(lowerPath,".mdl")) + else if (ends_with(lowerPath, ".mdl")) { print_log(get_localized_string(LANG_0897), tmpPath.string()); g_app->addMap(new Bsp(tmpPath.string())); } - else if (ends_with(lowerPath,".spr")) + else if (ends_with(lowerPath, ".spr")) { print_log(get_localized_string(LANG_0897), tmpPath.string()); g_app->addMap(new Bsp(tmpPath.string())); } - else if (ends_with(lowerPath,".csm")) + else if (ends_with(lowerPath, ".csm")) { print_log(get_localized_string(LANG_0897), tmpPath.string()); g_app->addMap(new Bsp(tmpPath.string())); @@ -750,22 +752,32 @@ void Renderer::renderLoop() glEnable(GL_CULL_FACE); debugNodeMax = currentPlane - 1; } - - if (g_render_flags & RENDER_ORIGIN) + if (g_render_flags & RENDER_ORIGIN || g_render_flags & RENDER_MAP_BOUNDARY || hasCullbox) { glDisable(GL_CULL_FACE); - matmodel.loadIdentity(); - vec3 offset = SelectedMap->getBspRender()->mapOffset; - colorShader->updateMatrixes(); - vec3 p1 = offset + vec3(-10240.0f, 0.0f, 0.0f); - vec3 p2 = offset + vec3(10240.0f, 0.0f, 0.0f); - drawLine(p1, p2, { 128, 128, 255, 255 }); - vec3 p3 = offset + vec3(0.0f, -10240.0f, 0.0f); - vec3 p4 = offset + vec3(0.0f, 10240.0f, 0.0f); - drawLine(p3, p4, { 0, 0, 255, 255 }); - vec3 p5 = offset + vec3(0.0f, 0.0f, -10240.0f); - vec3 p6 = offset + vec3(0.0f, 0.0f, 10240.0f); - drawLine(p5, p6, { 0, 255, 0, 255 }); + if (g_render_flags & RENDER_ORIGIN) + { + matmodel.loadIdentity(); + vec3 offset = SelectedMap->getBspRender()->mapOffset; + colorShader->updateMatrixes(); + vec3 p1 = offset + vec3(-10240.0f, 0.0f, 0.0f); + vec3 p2 = offset + vec3(10240.0f, 0.0f, 0.0f); + drawLine(p1, p2, { 128, 128, 255, 255 }); + vec3 p3 = offset + vec3(0.0f, -10240.0f, 0.0f); + vec3 p4 = offset + vec3(0.0f, 10240.0f, 0.0f); + drawLine(p3, p4, { 0, 0, 255, 255 }); + vec3 p5 = offset + vec3(0.0f, 0.0f, -10240.0f); + vec3 p6 = offset + vec3(0.0f, 0.0f, 10240.0f); + drawLine(p5, p6, { 0, 255, 0, 255 }); + } + + if (g_render_flags & RENDER_MAP_BOUNDARY) { + drawBox(SelectedMap->ents[0]->origin * -1, MAX_MAP_BOUNDARY * 2, COLOR4(0, 255, 0, 64)); + } + + if (hasCullbox) { + drawBox(cullMins, cullMaxs, COLOR4(255, 0, 0, 64)); + } glEnable(GL_CULL_FACE); } } @@ -814,6 +826,256 @@ void Renderer::renderLoop() } } + { + colorShader->bind(); + matmodel.loadIdentity(); + colorShader->updateMatrixes(); + glDisable(GL_CULL_FACE); + + glLineWidth(128.0f); + drawLine(debugLine0, debugLine1, { 255, 0, 0, 255 }); + + drawLine(debugTraceStart, debugTrace.vecEndPos, COLOR4(255, 0, 0, 255)); + + if (debugNavMesh && debugNavPoly != -1) { + glLineWidth(1); + NavNode& node = debugNavMesh->nodes[debugNavPoly]; + Polygon3D& poly = debugNavMesh->polys[debugNavPoly]; + + for (int i = 0; i < MAX_NAV_LINKS; i++) { + NavLink& link = node.links[i]; + if (link.node == -1) { + break; + } + Polygon3D& linkPoly = debugNavMesh->polys[link.node]; + + vec3 srcMid, dstMid; + debugNavMesh->getLinkMidPoints(debugNavPoly, i, srcMid, dstMid); + + glDisable(GL_DEPTH_TEST); + drawLine(poly.center, srcMid, COLOR4(0, 255, 255, 255)); + drawLine(srcMid, dstMid, COLOR4(0, 255, 255, 255)); + drawLine(dstMid, linkPoly.center, COLOR4(0, 255, 255, 255)); + + if (fabs(link.zDist) > NAV_STEP_HEIGHT) { + Bsp* map = mapRenderers[0]->map; + int n = link.srcEdge; + int k = link.dstEdge; + int inext = (n + 1) % poly.verts.size(); + int knext = (k + 1) % linkPoly.verts.size(); + + Line2D thisEdge(poly.topdownVerts[n], poly.topdownVerts[inext]); + Line2D otherEdge(linkPoly.topdownVerts[k], linkPoly.topdownVerts[knext]); + + float t0, t1, t2, t3; + float overlapDist = thisEdge.getOverlapRanges(otherEdge, t0, t1, t2, t3); + + vec3 delta1 = poly.verts[inext] - poly.verts[n]; + vec3 delta2 = linkPoly.verts[knext] - linkPoly.verts[k]; + vec3 e1 = poly.verts[n] + delta1 * t0; + vec3 e2 = poly.verts[n] + delta1 * t1; + vec3 e3 = linkPoly.verts[k] + delta2 * t2; + vec3 e4 = linkPoly.verts[k] + delta2 * t3; + + bool isBelow = link.zDist > 0; + delta1 = e2 - e1; + delta2 = e4 - e3; + vec3 mid1 = e1 + delta1 * 0.5f; + vec3 mid2 = e3 + delta2 * 0.5f; + vec3 inwardDir = crossProduct(poly.plane_z, delta1.normalize()); + vec3 testOffset = (isBelow ? inwardDir : inwardDir * -1) + vec3(0, 0, 1.0f); + + float flatLen = (vec2(e2.x,e2.y) - vec2(e1.x,e1.y)).length(); + float stepUnits = 1.0f; + float step = stepUnits / flatLen; + TraceResult tr; + bool isBlocked = true; + for (float f = 0; f < 0.5f; f += step) { + vec3 test1 = mid1 + (delta1 * f) + testOffset; + vec3 test2 = mid2 + (delta2 * f) + testOffset; + vec3 test3 = mid1 + (delta1 * -f) + testOffset; + vec3 test4 = mid2 + (delta2 * -f) + testOffset; + + map->traceHull(test1, test2, 3, &tr); + if (!tr.fAllSolid && !tr.fStartSolid && tr.flFraction > 0.99f) { + drawLine(test1, test2, COLOR4(255, 255, 0, 255)); + } + else { + drawLine(test1, test2, COLOR4(255, 0, 0, 255)); + } + + map->traceHull(test3, test4, 3, &tr); + if (!tr.fAllSolid && !tr.fStartSolid && tr.flFraction > 0.99f) { + drawLine(test3, test4, COLOR4(255, 255, 0, 255)); + } + else { + drawLine(test3, test4, COLOR4(255, 0, 0, 255)); + } + } + + //if (isBlocked) { + // continue; + //} + } + + glEnable(GL_DEPTH_TEST); + drawBox(linkPoly.center, 4, COLOR4(0, 255, 255, 255)); + } + } + + if (debugLeafNavMesh) { + glLineWidth(1); + glDisable(GL_DEPTH_TEST); + + Bsp* map = mapRenderers[0]->map; + int leafIdx = map->get_leaf(cameraOrigin, 3); + int leafNavIdx = -1; + + if (leafIdx >= 0 && leafIdx < MAX_MAP_CLIPNODE_LEAVES) { + leafNavIdx = debugLeafNavMesh->leafMap[leafIdx]; + } + + if (leafNavIdx >= 0 && leafNavIdx < debugLeafNavMesh->nodes.size()) { + + if (pickInfo.selectedEnts.size()) { + glDisable(GL_DEPTH_TEST); + + int endNode = debugLeafNavMesh->getNodeIdx(map, map->ents[pickInfo.selectedEnts[0]]); + //vector route = debugLeafNavMesh->AStarRoute(leafNavIdx, endNode); + std::vector route = debugLeafNavMesh->dijkstraRoute(leafNavIdx, endNode); + + if (route.size()) { + LeafNode* lastNode = &debugLeafNavMesh->nodes[route[0]]; + + vec3 lastPos = lastNode->origin; + drawBox(lastNode->origin, 2, COLOR4(0, 255, 255, 255)); + + for (int i = 1; i < route.size(); i++) { + LeafNode& node = debugLeafNavMesh->nodes[route[i]]; + + vec3 nodeCenter = node.origin; + + for (int k = 0; k < lastNode->links.size(); k++) { + LeafLink& link = lastNode->links[k]; + + if (link.node == route[i]) { + vec3 linkPoint = link.pos; + + if (link.baseCost > 16000) { + drawLine(lastPos, linkPoint, COLOR4(255, 0, 0, 255)); + drawLine(linkPoint, node.origin, COLOR4(255, 0, 0, 255)); + } + else if (link.baseCost > 0) { + drawLine(lastPos, linkPoint, COLOR4(255, 64, 0, 255)); + drawLine(linkPoint, node.origin, COLOR4(255, 64, 0, 255)); + } + else if (link.costMultiplier > 99.0f) { + drawLine(lastPos, linkPoint, COLOR4(255, 255, 0, 255)); + drawLine(linkPoint, node.origin, COLOR4(255, 255, 0, 255)); + } + else if (link.costMultiplier > 9.0f) { + drawLine(lastPos, linkPoint, COLOR4(255, 0, 255, 255)); + drawLine(linkPoint, node.origin, COLOR4(255, 0, 255, 255)); + } + else if (link.costMultiplier > 1.9f) { + drawLine(lastPos, linkPoint, COLOR4(64, 255, 0, 255)); + drawLine(linkPoint, node.origin, COLOR4(64, 255, 0, 255)); + } + else { + drawLine(lastPos, linkPoint, COLOR4(0, 255, 255, 255)); + drawLine(linkPoint, node.origin, COLOR4(0, 255, 255, 255)); + } + drawBox(nodeCenter, 2, COLOR4(0, 255, 255, 255)); + lastPos = nodeCenter; + break; + } + } + + lastNode = &node; + } + vec3 lastPosEnd = map->ents[pickInfo.selectedEnts[0]]->getHullOrigin(map); + drawLine(lastPos, lastPosEnd, COLOR4(0, 255, 255, 255)); + } + } + else { + LeafNode& node = debugLeafNavMesh->nodes[leafNavIdx]; + + drawBox(node.origin, 2, COLOR4(0, 255, 0, 255)); + + std::string linkStr; + + for (int i = 0; i < node.links.size(); i++) { + LeafLink& link = node.links[i]; + if (link.node == -1) { + break; + } + LeafNode& linkLeaf = debugLeafNavMesh->nodes[link.node]; + Polygon3D& linkArea = link.linkArea; + + if (link.baseCost > 16000) { + drawLine(node.origin, link.pos, COLOR4(255, 0, 0, 255)); + drawLine(link.pos, linkLeaf.origin, COLOR4(255, 0, 0, 255)); + } + else if (link.baseCost > 0) { + drawLine(node.origin, link.pos, COLOR4(255, 128, 0, 255)); + drawLine(link.pos, linkLeaf.origin, COLOR4(255, 128, 0, 255)); + } + else if (link.costMultiplier > 99.0f) { + drawLine(node.origin, link.pos, COLOR4(255, 255, 0, 255)); + drawLine(link.pos, linkLeaf.origin, COLOR4(255, 255, 0, 255)); + } + else if (link.costMultiplier > 9.0f) { + drawLine(node.origin, link.pos, COLOR4(255, 0, 255, 255)); + drawLine(link.pos, linkLeaf.origin, COLOR4(255, 0, 255, 255)); + } + else if (link.costMultiplier > 1.9f) { + drawLine(node.origin, link.pos, COLOR4(64, 255, 0, 255)); + drawLine(link.pos, linkLeaf.origin, COLOR4(64, 255, 0, 255)); + } + else { + drawLine(node.origin, link.pos, COLOR4(0, 255, 255, 255)); + drawLine(link.pos, linkLeaf.origin, COLOR4(0, 255, 255, 255)); + } + + for (int k = 0; k < linkArea.verts.size(); k++) { + //drawBox(linkArea.verts[k], 1, COLOR4(255, 255, 0, 255)); + } + drawBox(link.pos, 1, COLOR4(0, 255, 0, 255)); + drawBox(linkLeaf.origin, 2, COLOR4(0, 255, 255, 255)); + linkStr += std::to_string(link.node) + " (" + std::to_string(linkArea.verts.size()) + "v), "; + + /* + for (int k = 0; k < node.links.size(); k++) { + if (i == k) + continue; + drawLine(link.pos, node.links[k].pos, COLOR4(64, 0, 255, 255)); + } + */ + } + + //logf("Leaf node idx: %d, links: %s\n", leafNavIdx, linkStr.c_str()); + } + + } + + /* + colorShader->pushMatrix(MAT_PROJECTION); + colorShader->pushMatrix(MAT_VIEW); + projection.ortho(0, windowWidth, windowHeight, 0, -1.0f, 1.0f); + view.loadIdentity(); + colorShader->updateMatrixes(); + + drawPolygon2D(debugPoly, vec2(800, 100), vec2(500, 500), COLOR4(255, 0, 0, 255)); + + colorShader->popMatrix(MAT_PROJECTION); + colorShader->popMatrix(MAT_VIEW); + */ + } + + glLineWidth(1); + } + + glDepthMask(GL_TRUE); glDepthFunc(GL_LESS); vec3 forward, right, up; @@ -934,6 +1196,8 @@ void Renderer::reloadMaps() } reloadBspModels(); + updateCullBox(); + print_log(get_localized_string(LANG_0908)); } @@ -1436,12 +1700,7 @@ void Renderer::revertInvalidSolid(Bsp* map, int modelIdx) { map->vertex_manipulation_sync(modelIdx, modelVerts, false); BSPMODEL& model = map->models[modelIdx]; - vec3 mins, maxs; - if (map->get_model_vertex_bounds(modelIdx, mins, maxs)) - { - model.nMins = mins; - model.nMaxs = maxs; - } + map->get_model_vertex_bounds(modelIdx, model.nMins, model.nMaxs); map->getBspRender()->refreshModel(modelIdx); } pickCount++; @@ -1509,12 +1768,7 @@ void Renderer::applyTransform(Bsp* map, bool forceUpdate) if (modelTransform >= 0) { BSPMODEL& model = map->models[modelTransform]; - vec3 mins, maxs; - if (map->get_model_vertex_bounds(modelTransform, mins, maxs)) - { - model.nMins = mins; - model.nMaxs = maxs; - } + map->get_model_vertex_bounds(modelTransform, model.nMins, model.nMaxs); } } @@ -1855,6 +2109,23 @@ void Renderer::pickObject() vec3 pickStart, pickDir; getPickRay(pickStart, pickDir); + if (DebugKeyPressed) + { + TraceResult& tr = debugTrace; + mapRenderers[0]->map->traceHull(pickStart, pickStart + pickDir * 512, 1, &tr); + print_log("Fraction={}, StartSolid={}, AllSolid={}, InOpen={}, PlaneDist={}\nStart=({},{},{}) End=({},{},{}) PlaneNormal=({},{},{})\n", + tr.flFraction, tr.fStartSolid, tr.fAllSolid, tr.fInOpen, tr.flPlaneDist, + pickStart.x, pickStart.y, pickStart.z, + tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z, + tr.vecPlaneNormal.x, tr.vecPlaneNormal.y, tr.vecPlaneNormal.z); + debugTraceStart = pickStart; + } + else + { + + } + + Bsp* oldmap = map; PickInfo tmpPickInfo = PickInfo(); @@ -2466,7 +2737,7 @@ void Renderer::reloadBspModels() if (entity->hasKey("model")) { std::string modelPath = entity->keyvalues["model"]; - if (ends_with(toLowerCase(modelPath),".bsp")) + if (ends_with(toLowerCase(modelPath), ".bsp")) { std::string newBspPath; if (FindPathInAssets(bsprend->map, modelPath, newBspPath)) @@ -2525,6 +2796,9 @@ void Renderer::addMap(Bsp* map) if (map->ents.size()) pickInfo.SetSelectedEnt(0); } + + updateCullBox(); + } void Renderer::drawLine(vec3& start, vec3& end, COLOR4 color) @@ -2539,6 +2813,84 @@ void Renderer::drawLine(vec3& start, vec3& end, COLOR4 color) lineBuf->drawFull(); } +void Renderer::drawLine2D(vec2 start, vec2 end, COLOR4 color) { + line_verts[0].pos = vec3(start.x,start.y,0.0f).flip(); + line_verts[0].c = color; + + line_verts[1].pos = vec3(end.x, end.y, 0.0f).flip(); + line_verts[1].c = color; + + lineBuf->uploaded = false; + lineBuf->drawFull(); +} + +void Renderer::drawBox(vec3 center, float width, COLOR4 color) { + width *= 0.5f; + vec3 sz = vec3(width, width, width); + vec3 pos = vec3(center.x, center.z, -center.y); + cCube cube(pos - sz, pos + sz, color); + VertexBuffer buffer(g_app->colorShader, &cube, 6 * 6, GL_TRIANGLES); + buffer.drawFull(); +} + +void Renderer::drawBox(vec3 mins, vec3 maxs, COLOR4 color) { + mins = vec3(mins.x, mins.z, -mins.y); + maxs = vec3(maxs.x, maxs.z, -maxs.y); + + cCube cube(mins, maxs, color); + + VertexBuffer buffer(g_app->colorShader, &cube, 6 * 6, GL_TRIANGLES); + buffer.drawFull(); +} + +void Renderer::drawPolygon3D(Polygon3D& poly, COLOR4 color) { + static cVert verts[64]; + + for (int i = 0; i < poly.verts.size() && i < 64; i++) { + vec3 pos = poly.verts[i]; + verts[i].pos = vec3(pos.x,pos.z,-pos.y); + verts[i].c = color; + } + + VertexBuffer buffer(g_app->colorShader, verts, poly.verts.size(), GL_TRIANGLE_FAN); + buffer.drawFull(); +} + +float Renderer::drawPolygon2D(Polygon3D poly, vec2 pos, vec2 maxSz, COLOR4 color) { + vec2 sz = poly.localMaxs - poly.localMins; + float scale = std::min(maxSz.y / sz.y, maxSz.x / sz.x); + + vec2 offset = poly.localMins * -scale + pos; + + for (int i = 0; i < poly.verts.size(); i++) { + vec2 v1 = poly.localVerts[i]; + vec2 v2 = poly.localVerts[(i + 1) % poly.verts.size()]; + drawLine2D(offset + v1 * scale, offset + v2 * scale, color); + if (i == 0) { + drawLine2D(offset + v1 * scale, offset + (v1 + (v2 - v1) * 0.5f) * scale, COLOR4(0, 255, 0, 255)); + } + } + + // draw camera origin in the same coordinate space + { + vec2 cam = poly.project(cameraOrigin); + drawBox2D(offset + cam * scale, 16, poly.isInside(cam) ? COLOR4(0, 255, 0, 255) : COLOR4(255, 32, 0, 255)); + } + + + return scale; +} + +void Renderer::drawBox2D(vec2 center, float width, COLOR4 color) { + vec2 pos = vec2(center.x, center.y) - vec2(width * 0.5f, width * 0.5f); + cQuad cube(pos.x, pos.y, width, width, color); + + VertexBuffer buffer(g_app->colorShader, &cube, 6, GL_TRIANGLES); + buffer.drawFull(); +} + + + void Renderer::drawPlane(BSPPLANE& plane, COLOR4 color, vec3 offset) { vec3 ori = plane.vNormal * plane.fDist; @@ -2659,22 +3011,8 @@ void Renderer::updateDragAxes() entMin = vec3(FLT_MAX_COORD, FLT_MAX_COORD, FLT_MAX_COORD); entMax = vec3(-FLT_MAX_COORD, -FLT_MAX_COORD, -FLT_MAX_COORD); - if (modelVerts.size()) - { - for (auto& vert : modelVerts) - { - expandBoundingBox(vert.pos, entMin, entMax); - } - } - else - { - vec3 mins, maxs; - if (map->get_model_vertex_bounds(modelIdx, mins, maxs)) - { - entMin = mins; - entMax = maxs; - } - } + map->get_model_vertex_bounds(modelIdx, entMin, entMax); + vec3 modelOrigin = entMin + (entMax - entMin) * 0.5f; entMax -= modelOrigin; @@ -3053,11 +3391,7 @@ void Renderer::updateSelectionSize() else { vec3 mins, maxs; - if (!map->get_model_vertex_bounds(modelIdx, mins, maxs)) - { - mins = map->models[modelIdx].nMins; - maxs = map->models[modelIdx].nMaxs; - } + map->get_model_vertex_bounds(modelIdx, mins, maxs); selectionSize = maxs - mins; } } @@ -3179,6 +3513,7 @@ void Renderer::updateEntConnections() entConnectionPoints = new VertexBuffer(colorShader, points, ((int)(numPoints) * 6 * 6), GL_TRIANGLES); entConnections->ownData = true; entConnectionPoints->ownData = true; + updateCullBox(); } void Renderer::updateEntConnectionPositions() @@ -3196,6 +3531,30 @@ void Renderer::updateEntConnectionPositions() } entConnections->uploaded = false; } + + updateCullBox(); +} + +void Renderer::updateCullBox() { + if (!mapRenderers.size()) { + hasCullbox = false; + return; + } + + Bsp* map = mapRenderers[0]->map; + + cullMins = vec3(FLT_MAX, FLT_MAX, FLT_MAX); + cullMaxs = vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); + + int findCount = 0; + for (Entity* ent : map->ents) { + if (ent->hasKey("classname") && ent->keyvalues["classname"] == "cull") { + expandBoundingBox(ent->origin, cullMins, cullMaxs); + findCount++; + } + } + + hasCullbox = findCount > 1; } bool Renderer::getModelSolid(std::vector& hullVerts, Bsp* map, Solid& outSolid) @@ -3938,6 +4297,35 @@ void Renderer::pasteEnt(bool noModifyOrigin) } } +void Renderer::pasteEntsFromText(std::string text) { + Bsp* map = getSelectedMap(); + if (!map) + { + return; + } + BspRenderer* rend = map->getBspRender(); + + CreateEntityFromTextCommand* createCommand = + new CreateEntityFromTextCommand("Paste entities from clipboard", getSelectedMapId(), text); + rend->pushUndoCommand(createCommand); + + if (createCommand->createdEnts == 1) { + Entity* createdEnt = map->ents[map->ents.size() - 1]; + vec3 oldOrigin = getEntOrigin(map, createdEnt); + vec3 modelOffset = getEntOffset(map, createdEnt); + vec3 mapOffset = rend->mapOffset; + + vec3 moveDist = (cameraOrigin + cameraForward * 100) - oldOrigin; + vec3 newOri = (oldOrigin + moveDist) - (modelOffset + mapOffset); + vec3 rounded = gridSnappingEnabled ? snapToGrid(newOri) : newOri; + createdEnt->setOrAddKeyvalue("origin", rounded.toKeyvalueString(!gridSnappingEnabled)); + createCommand->refresh(); + } + + if (map->ents.size() > 0) + selectEnt(map, (int)map->ents.size() - 1); +} + void Renderer::deleteEnt(int entIdx) { Bsp* map = SelectedMap; @@ -3966,7 +4354,7 @@ void Renderer::deleteEnts() for (auto entIdx : tmpEnts) { if (map->ents[entIdx]->hasKey("model") && - ends_with(toLowerCase(map->ents[entIdx]->keyvalues["model"]),".bsp")) + ends_with(toLowerCase(map->ents[entIdx]->keyvalues["model"]), ".bsp")) { reloadbspmdls = true; } @@ -4294,4 +4682,51 @@ Texture* Renderer::giveMeTexture(const std::string& texname, const std::string& } } return missingTex; +} + +void Renderer::merge(std::string fpath) +{ + Bsp* thismap = SelectedMap; + if (!thismap) + return; + + thismap->update_ent_lump(); + + Bsp* map2 = new Bsp(fpath); + Bsp* thisCopy = new Bsp(*thismap); + + if (!map2->bsp_valid) { + delete map2; + print_log("Merge aborted because the BSP load failed.\n"); + return; + } + + std::vector maps; + + maps.push_back(thisCopy); + maps.push_back(map2); + + BspMerger merger; + mergeResult = merger.merge(maps, vec3(), thismap->bsp_name, true, true, true,false); + + if (!mergeResult.map || !mergeResult.map->bsp_valid) { + delete map2; + if (mergeResult.map) + delete mergeResult.map; + + mergeResult.map = NULL; + return; + } + + if (mergeResult.overflow) { + return; // map deleted later in gui modal, after displaying limit overflows + } + + mapRenderers.clear(); + addMap(mergeResult.map); + + gui->refresh(); + updateCullBox(); + + print_log("Merged maps!\n"); } \ No newline at end of file diff --git a/src/editor/Renderer.h b/src/editor/Renderer.h index e0f5021f..f073e3ce 100644 --- a/src/editor/Renderer.h +++ b/src/editor/Renderer.h @@ -4,14 +4,13 @@ #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include "imgui_internal.h" +#include "BspMerger.h" #include "ShaderProgram.h" #include "BspRenderer.h" #include "Fgd.h" #include #include #include "Command.h" -#include -#include #include "mdl_studio.h" #define EDIT_MODEL_LUMPS (FL_PLANES | FL_TEXTURES | FL_VERTICES | FL_NODES | FL_TEXINFO | FL_FACES | FL_LIGHTING | FL_CLIPNODES | FL_LEAVES | FL_EDGES | FL_SURFEDGES | FL_MODELS | FL_MARKSURFACES) @@ -98,6 +97,18 @@ class Renderer vec3 debugVec1; vec3 debugVec2; vec3 debugVec3; + + vec3 debugLine0; + vec3 debugLine1; + Line2D debugCut; + Polygon3D debugPoly; + Polygon3D debugPoly2; + NavMesh* debugNavMesh = NULL; + LeafNavMesh* debugLeafNavMesh = NULL; + int debugNavPoly = -1; + vec3 debugTraceStart; + TraceResult debugTrace; + MergeResult mergeResult; unsigned int colorShaderMultId; @@ -118,6 +129,10 @@ class Renderer Fgd* fgd = NULL; bool hideGui = false; + bool isFocused = false; + bool isHovered = false; + bool isIconified = false; + bool isModelsReloading = false; Renderer(); @@ -280,6 +295,10 @@ class Renderer bool isTransformingWorld = false; bool oldTransforming = false; + bool hasCullbox; + vec3 cullMins; + vec3 cullMaxs; + vec3 getMoveDir(); void controls(); void cameraPickingControls(); @@ -302,6 +321,12 @@ class Renderer void drawTransformAxes(); void drawEntConnections(); void drawLine(vec3& start, vec3& end, COLOR4 color); + void drawLine2D(vec2 start, vec2 end, COLOR4 color); + void drawBox(vec3 center, float width, COLOR4 color); + void drawBox(vec3 mins, vec3 maxs, COLOR4 color); + void drawPolygon3D(Polygon3D& poly, COLOR4 color); + float drawPolygon2D(Polygon3D poly, vec2 pos, vec2 maxSz, COLOR4 color); // returns render scale + void drawBox2D(vec2 center, float width, COLOR4 color); void drawPlane(BSPPLANE& plane, COLOR4 color, vec3 offset = vec3()); void drawClipnodes(Bsp* map, int iNode, int& currentPlane, int activePlane, vec3 offset = vec3()); void drawNodes(Bsp* map, int iNode, int& currentPlane, int activePlane, vec3 offset = vec3()); @@ -319,6 +344,7 @@ class Renderer bool getModelSolid(std::vector& hullVerts, Bsp* map, Solid& outSolid); // calculate face vertices from plane intersections void moveSelectedVerts(const vec3& delta); bool splitModelFace(); + void updateCullBox(); vec3 snapToGrid(vec3 pos); @@ -326,6 +352,7 @@ class Renderer void cutEnt(); void copyEnt(); void pasteEnt(bool noModifyOrigin); + void pasteEntsFromText(std::string text); void deleteEnt(int entIdx = 0); void deleteEnts(); void scaleSelectedObject(Bsp* map, float x, float y, float z); @@ -341,6 +368,7 @@ class Renderer void goToFace(Bsp* map, int faceIdx); void ungrabEnt(); void loadFgds(); + void merge(std::string fpath); std::vector glExteralTextures_names; std::vector glExteralTextures_textures; diff --git a/src/editor/Settings.cpp b/src/editor/Settings.cpp index 0e6063d8..2ea344c9 100644 --- a/src/editor/Settings.cpp +++ b/src/editor/Settings.cpp @@ -623,6 +623,12 @@ void AppSettings::load() MAX_TEXTURE_DIMENSION = str_to_int(val); MAX_TEXTURE_SIZE = ((MAX_TEXTURE_DIMENSION * MAX_TEXTURE_DIMENSION * 2 * 3) / 2); } + else if (key == "MAX_MAP_BOUNDARY") + { + MAX_MAP_BOUNDARY = str_to_float(val); + if (std::abs(MAX_MAP_BOUNDARY) < 512.0f) + MAX_MAP_BOUNDARY = 4096; + } else if (key == "TEXTURE_STEP") { TEXTURE_STEP = str_to_int(val); @@ -917,6 +923,7 @@ void AppSettings::save(std::string path) file << "MAX_MAP_TEXTURES=" << MAX_MAP_TEXTURES << std::endl; file << "MAX_MAP_LIGHTDATA=" << MAX_MAP_LIGHTDATA / (1024 * 1024) << std::endl; file << "MAX_TEXTURE_DIMENSION=" << MAX_TEXTURE_DIMENSION << std::endl; + file << "MAX_MAP_BOUNDARY=" << MAX_MAP_BOUNDARY << std::endl; file << "TEXTURE_STEP=" << TEXTURE_STEP << std::endl; file.flush(); diff --git a/src/editor/Settings.h b/src/editor/Settings.h index 44f7af85..e3140fa5 100644 --- a/src/editor/Settings.h +++ b/src/editor/Settings.h @@ -24,12 +24,13 @@ enum RenderFlags RENDER_ORIGIN = 128, RENDER_WORLD_CLIPNODES = 256, RENDER_ENT_CLIPNODES = 512, - RENDER_ENT_CONNECTIONS = 1024, + RENDER_MAP_BOUNDARY = 1024, RENDER_TRANSPARENT = 2048, RENDER_MODELS = 4096, RENDER_MODELS_ANIMATED = 8192, RENDER_SELECTED_AT_TOP = 16384, - RENDER_TEXTURES_NOFILTER = 32768 + RENDER_TEXTURES_NOFILTER = 32768, + RENDER_ENT_CONNECTIONS = 65536 }; diff --git a/src/gl/Shader.cpp b/src/gl/Shader.cpp index 5c7231cd..3beaf3bd 100644 --- a/src/gl/Shader.cpp +++ b/src/gl/Shader.cpp @@ -1,8 +1,9 @@ #include "lang.h" -#include #include "Shader.h" #include "log.h" +#include + Shader::Shader(const char* sourceCode, int shaderType) { // Create Shader And Program Objects diff --git a/src/gl/ShaderProgram.cpp b/src/gl/ShaderProgram.cpp index 763751c6..1ad1a093 100644 --- a/src/gl/ShaderProgram.cpp +++ b/src/gl/ShaderProgram.cpp @@ -1,5 +1,4 @@ #include "lang.h" -#include #include "ShaderProgram.h" #include "log.h" #include "Renderer.h" diff --git a/src/gl/Texture.cpp b/src/gl/Texture.cpp index 72831d71..53a4a0d5 100644 --- a/src/gl/Texture.cpp +++ b/src/gl/Texture.cpp @@ -1,6 +1,6 @@ #include "lang.h" -#include #include "Wad.h" +#include #include "Texture.h" #include "lodepng.h" #include "log.h" diff --git a/src/gl/VertexBuffer.h b/src/gl/VertexBuffer.h index 6c3d248b..de730373 100644 --- a/src/gl/VertexBuffer.h +++ b/src/gl/VertexBuffer.h @@ -1,6 +1,6 @@ #pragma once -#include #include +#include #include "ShaderProgram.h" #include "util.h" diff --git a/src/gl/primitives.cpp b/src/gl/primitives.cpp index c2da6187..2a9a827a 100644 --- a/src/gl/primitives.cpp +++ b/src/gl/primitives.cpp @@ -9,6 +9,15 @@ cQuad::cQuad(cVert _v1, cVert _v2, cVert _v3, cVert _v4) : v1(_v1), v2(_v2), v3( { } +cQuad::cQuad(float x, float y, float w, float h, COLOR4 color) { + v1 = cVert(x, y, 0, color); + v2 = cVert(x, y + h, 0, color); + v3 = cVert(x + w, y + h, 0, color); + + v4 = cVert(x, y, 0, color); + v5 = cVert(x + w, y + h, 0, color); + v6 = cVert(x + w, y, 0, color); +} void cQuad::setColor(COLOR4 c) { diff --git a/src/gl/primitives.h b/src/gl/primitives.h index 52009e8b..34c066a2 100644 --- a/src/gl/primitives.h +++ b/src/gl/primitives.h @@ -89,9 +89,11 @@ struct cQuad cQuad() = default; cQuad(cVert v1, cVert v2, cVert v3, cVert v4); + cQuad(float x, float y, float w, float h, COLOR4 color); void setColor(COLOR4 c); // color for the entire quad void setColor(COLOR4 c1, COLOR4 c2, COLOR4 c3, COLOR4 c4); // color each vertex in CCW order + }; // Textured 3D Cube diff --git a/src/main.cpp b/src/main.cpp index 2e005100..bbfb3760 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -100,13 +100,13 @@ int test() removed.print_delete_stats(1); BspMerger merger; - Bsp* result = merger.merge(maps, vec3(1, 1, 1), "yabma_move", false, false, false); + MergeResult result = merger.merge(maps, vec3(1, 1, 1), "yabma_move", false, false, false, false); print_log("\n"); - if (result) + if (result.map) { - result->write("yabma_move.bsp"); - result->write("D:/Steam/steamapps/common/Sven Co-op/svencoop_addon/maps/yabma_move.bsp"); - result->print_info(false, 0, 0); + result.map->write("yabma_move.bsp"); + result.map->write("D:/Steam/steamapps/common/Sven Co-op/svencoop_addon/maps/yabma_move.bsp"); + result.map->print_info(false, 0, 0); } start_viewer("yabma_move.bsp"); @@ -172,18 +172,20 @@ int merge_maps(CommandLine& cli) std::string output_name = cli.hasOption("-o") ? cli.getOption("-o") : cli.bspfile; BspMerger merger; - Bsp* result = merger.merge(maps, gap, output_name, cli.hasOption("-noripent"), cli.hasOption("-noscript"), cli.hasOption("-nostyles")); + MergeResult result = merger.merge(maps, gap, output_name, cli.hasOption("-noripent"), cli.hasOption("-noscript"), cli.hasOption("-nomove"), cli.hasOption("-nostyles")); print_log("\n"); - if (result->validate() && result->isValid()) result->write(output_name); - print_log("\n"); - result->print_info(false, 0, 0); + if (result.map && result.map->validate() && result.map->isValid()) + { + result.map->write(output_name); + print_log("\n"); + result.map->print_info(false, 0, 0); + } for (size_t i = 0; i < maps.size(); i++) { delete maps[i]; } - return 0; } diff --git a/src/nav/LeafNavMesh.cpp b/src/nav/LeafNavMesh.cpp new file mode 100644 index 00000000..c42233e9 --- /dev/null +++ b/src/nav/LeafNavMesh.cpp @@ -0,0 +1,400 @@ +#include "Renderer.h" +#include "LeafNavMesh.h" +#include "GLFW/glfw3.h" +#include "PolyOctree.h" +#include "Clipper.h" +#include "log.h" +#include "util.h" +#include +#include "Bsp.h" +#include +#include "Entity.h" +#include "Fgd.h" +#include +#include +#include + +LeafNode::LeafNode() { + id = -1; + entidx = 0; + center = origin = mins = maxs = vec3(); +} + +bool LeafNode::isInside(vec3 p) { + for (int i = 0; i < leafFaces.size(); i++) { + if (leafFaces[i].distance(p) > 0) { + return false; + } + } + + return true; +} + +bool LeafNode::addLink(int node, Polygon3D linkArea) { + for (int i = 0; i < links.size(); i++) { + if (links[i].node == node) { + return true; + } + } + + LeafLink link; + link.linkArea = linkArea; + link.node = node; + + link.pos = linkArea.center; + if (fabs(linkArea.plane_z.z) < 0.7f) { + // wall links should be positioned at the bottom of the intersection to keep paths near the floor + linkArea.intersect2D(linkArea.center, linkArea.center - vec3(0, 0, 4096), link.pos); + link.pos.z += NAV_BOTTOM_EPSILON; + } + + links.push_back(link); + + return true; +} + +bool LeafNode::addLink(int node, vec3 linkPos) { + for (int i = 0; i < links.size(); i++) { + if (links[i].node == node) { + return true; + } + } + + LeafLink link; + link.node = node; + link.pos = linkPos; + links.push_back(link); + + return true; +} + +LeafNavMesh::LeafNavMesh() { + clear(); +} + +void LeafNavMesh::clear() { + memset(leafMap, 65535, sizeof(unsigned int) * MAX_MAP_CLIPNODE_LEAVES); + nodes.clear(); +} + +LeafNavMesh::LeafNavMesh(std::vector inleaves, LeafOctree* octree) { + clear(); + + this->nodes = inleaves; + this->octree = octree; +} + +bool LeafNavMesh::addLink(int from, int to, Polygon3D linkArea) { + if (from < 0 || to < 0 || from >= nodes.size() || to >= nodes.size()) { + print_log("Error: add link from/to invalid node {} {}\n", from, to); + return false; + } + + if (!nodes[from].addLink(to, linkArea)) { + vec3& pos = nodes[from].center; + print_log("Failed to add link at {} {} {}\n", (int)pos.x, (int)pos.y, (int)pos.z); + return false; + } + + return true; +} + +int LeafNavMesh::getNodeIdx(Bsp* map, Entity* ent) { + vec3 ori = ent->origin; + vec3 mins, maxs; + int modelIdx = ent->getBspModelIdx(); + + if (modelIdx != -1) { + map->get_model_vertex_bounds(modelIdx, mins, maxs); + ori += (maxs + mins) * 0.5f; + } + else { + FgdClass* fclass = g_app->fgd->getFgdClass(ent->keyvalues["classname"]); + if (fclass->sizeSet) { + mins = fclass->mins; + maxs = fclass->maxs; + } + } + + // first try testing a few points on the entity box for an early exit + + mins += ori; + maxs += ori; + + vec3 testPoints[10] = { + ori, + (mins + maxs) * 0.5f, + vec3(mins.x, mins.y, mins.z), + vec3(mins.x, mins.y, maxs.z), + vec3(mins.x, maxs.y, mins.z), + vec3(mins.x, maxs.y, maxs.z), + vec3(maxs.x, mins.y, mins.z), + vec3(maxs.x, mins.y, maxs.z), + vec3(maxs.x, maxs.y, mins.z), + vec3(maxs.x, maxs.y, maxs.z), + }; + + for (int i = 0; i < 10; i++) { + int targetLeaf = map->get_leaf(testPoints[i], 3); + + if (targetLeaf >= 0 && targetLeaf < MAX_MAP_CLIPNODE_LEAVES) { + int navIdx = leafMap[targetLeaf]; + + if (navIdx < 65535) { + return navIdx; + } + } + } + + if ((maxs - mins).length() < 1) { + return -1; // point sized, so can't intersect any leaf + } + + // no points are inside, so test for plane intersections + + cCube entCube(mins, maxs, COLOR4(0, 0, 0, 0)); + cQuad* faces[6] = { + &entCube.top, + &entCube.bottom, + &entCube.left, + &entCube.right, + &entCube.front, + &entCube.back, + }; + + Polygon3D boxPolys[6]; + for (int i = 0; i < 6; i++) { + cQuad& face = *faces[i]; + boxPolys[i] = std::vector{ face.v1.pos, face.v2.pos, face.v3.pos, face.v6.pos }; + } + + for (int i = 0; i < nodes.size(); i++) { + LeafNode& mesh = nodes[i]; + + for (int k = 0; k < mesh.leafFaces.size(); k++) { + Polygon3D& leafFace = mesh.leafFaces[k]; + + for (int n = 0; n < 6; k++) { + if (leafFace.intersects(boxPolys[n])) { + return i; + } + } + } + } + + return -1; +} + +float LeafNavMesh::path_cost(int a, int b) { + + LeafNode& nodea = nodes[a]; + LeafNode& nodeb = nodes[b]; + vec3 delta = nodea.origin - nodeb.origin; + + for (int i = 0; i < nodea.links.size(); i++) { + LeafLink& link = nodea.links[i]; + if (link.node == b) { + return link.baseCost + delta.length() * link.costMultiplier; + } + } + + return delta.length(); +} + +std::vector LeafNavMesh::AStarRoute(int startNodeIdx, int endNodeIdx) +{ + std::set closedSet; + std::set openSet; + + std::unordered_map gScore; + std::unordered_map fScore; + std::unordered_map cameFrom; + + std::vector emptyRoute; + + if (startNodeIdx < 0 || endNodeIdx < 0 || startNodeIdx > nodes.size() || endNodeIdx > nodes.size()) { + print_log("AStarRoute: invalid start/end nodes\n"); + return emptyRoute; + } + + if (startNodeIdx == endNodeIdx) { + emptyRoute.push_back(startNodeIdx); + return emptyRoute; + } + + LeafNode& start = nodes[startNodeIdx]; + LeafNode& goal = nodes[endNodeIdx]; + + openSet.insert(startNodeIdx); + gScore[startNodeIdx] = 0; + fScore[startNodeIdx] = path_cost(start.id, goal.id); + + const int maxIter = 8192; + int curIter = 0; + while (!openSet.empty()) { + if (++curIter > maxIter) { + print_log("AStarRoute exceeded max iterations searching path ({})", maxIter); + break; + } + + // get node in openset with lowest cost + int current = -1; + float bestScore = (float)9e99; + for (int nodeId : openSet) + { + float score = fScore[nodeId]; + if (score < bestScore) { + bestScore = score; + current = nodeId; + } + } + + //println("Current is " + current); + + if (current == goal.id) { + //println("MAde it to the goal"); + // goal reached, build the route + std::vector path; + path.push_back(current); + + int maxPathLen = 1000; + int i = 0; + while (cameFrom.count(current)) { + current = cameFrom[current]; + path.push_back(current); + if (++i > maxPathLen) { + print_log("AStarRoute exceeded max path length ({})", maxPathLen); + break; + } + } + reverse(path.begin(), path.end()); + + return path; + } + + openSet.erase(current); + closedSet.insert(current); + + LeafNode& currentNode = nodes[current]; + + for (int i = 0; i < currentNode.links.size(); i++) { + LeafLink& link = currentNode.links[i]; + if (link.node == -1) { + break; + } + + int neighbor = link.node; + if (neighbor < 0 || neighbor >= nodes.size()) { + continue; + } + if (closedSet.count(neighbor)) + continue; + //if (currentNode.blockers.size() > i and currentNode.blockers[i] & blockers != 0) + // continue; // blocked by something (monsterclip, normal clip, etc.). Don't route through this path. + + // discover a new node + openSet.insert(neighbor); + + // The distance from start to a neighbor + LeafNode& neighborNode = nodes[neighbor]; + + float tentative_gScore = gScore[current]; + tentative_gScore += path_cost(currentNode.id, neighborNode.id); + + float neighbor_gScore = (float)9e99; + if (gScore.count(neighbor)) + neighbor_gScore = gScore[neighbor]; + + if (tentative_gScore >= neighbor_gScore) + continue; // not a better path + + // This path is the best until now. Record it! + cameFrom[neighbor] = current; + gScore[neighbor] = tentative_gScore; + fScore[neighbor] = tentative_gScore + path_cost(neighborNode.id, goal.id); + } + } + + return emptyRoute; +} + +// Dijkstra's algorithm to find shortest path from start to end vertex (chat-gpt code) +std::vector LeafNavMesh::dijkstraRoute(int start, int end) { + std::vector emptyRoute; + + if (start < 0 || end < 0 || start > nodes.size() || end > nodes.size()) { + print_log("dijkstraRoute: invalid start/end nodes\n"); + return emptyRoute; + } + + if (start == end) { + emptyRoute.push_back(start); + return emptyRoute; + } + + size_t n = nodes.size(); + std::vector dist(n, FLT_MAX); // Initialize distances with infinity + std::vector previous(n, -1); // Array to store previous node in the shortest path + dist[start] = 0.0f; // Distance from start node to itself is 0 + + std::priority_queue, std::vector>, std::greater>> pq; + pq.push({ 0.0f, start }); // Push start node with distance 0 + + while (!pq.empty()) { + int u = pq.top().second; // Get node with smallest distance + float d = pq.top().first; // Get the distance + pq.pop(); + + // If the extracted node is already processed, skip + if (d > dist[u]) + continue; + + // Stop early if we reached the end node + if (u == end) + break; + + // Traverse all links of node u + for (int i = 0; i < nodes[u].links.size(); i++) { + LeafLink& link = nodes[u].links[i]; + + if (link.node == -1) { + break; + } + + int v = link.node; + float weight = path_cost(u, link.node); + + // Relaxation step + if (dist[u] + weight < dist[v]) { + dist[v] = dist[u] + weight; + previous[v] = u; // Set previous node for path reconstruction + pq.push({ dist[v], v }); + } + } + } + + // Reconstruct the shortest path from start to end node + std::vector path; + for (int at = end; at != -1; at = previous[at]) { + path.push_back(at); + if (at == start) + break; + } + reverse(path.begin(), path.end()); + + // If end node is unreachable, return an empty path + if (path.empty() || path[0] != start) + return {}; + + float len = 0; + float cost = 0; + for (int i = 1; i < path.size(); i++) { + LeafNode& mesha = nodes[path[i-1]]; + LeafNode& meshb = nodes[path[i]]; + len += (mesha.origin - meshb.origin).length(); + cost += path_cost(path[i - 1], path[i]); + } + print_log("Path length: {}, cost: {}\n", (int)len, (int)cost); + + return path; +} \ No newline at end of file diff --git a/src/nav/LeafNavMesh.h b/src/nav/LeafNavMesh.h new file mode 100644 index 00000000..576af437 --- /dev/null +++ b/src/nav/LeafNavMesh.h @@ -0,0 +1,75 @@ +#pragma once +#include "Polygon3D.h" +#include + +#define MAX_MAP_CLIPNODE_LEAVES 65536 // doubled to account for each clipnode's child contents having its own ID + +#define NAV_STEP_HEIGHT 18 +#define NAV_JUMP_HEIGHT 44 +#define NAV_CROUCHJUMP_HEIGHT 63 // 208 gravity 50% +#define NAV_CROUCHJUMP_STACK_HEIGHT 135 +#define NAV_AUTOCLIMB_HEIGHT 117 +#define NAV_HULL 3 + +#define NAV_BOTTOM_EPSILON 1.0f // move waypoints this far from the bottom of the node + +class Bsp; +class Entity; +class LeafOctree; + +struct LeafLink { + int node; // which leaf is linked to + vec3 pos; // link position + float baseCost; // flat cost for using this path + float costMultiplier; // cost applied to length of path + + // for debugging + Polygon3D linkArea; // region in which leaves are making contact +}; + +struct LeafNode { + std::vector links; + int id; + vec3 origin; // the best position for pathing (not necessarily the center) + int entidx; // 0 for world leaves, else an entity leaf which may be relocated, enabled, or disabled + + // for debugging + vec3 center; + vec3 mins, maxs; // for octree insertion, not needed after generation + std::vector leafFaces; + + LeafNode(); + + bool addLink(int node, Polygon3D linkArea); + + bool addLink(int node, vec3 linkPos); + + // returns true if point is inside leaf volume + bool isInside(vec3 p); +}; + + +class LeafNavMesh { +public: + std::vector nodes; + LeafOctree* octree; // finds nearby leaves from any point in space, even outside of the BSP tree + int leafMap[MAX_MAP_CLIPNODE_LEAVES]; // maps a BSP leaf index to nav mesh node index + + LeafNavMesh(); + + LeafNavMesh(std::vector polys, LeafOctree* octree); + + bool addLink(int from, int to, Polygon3D linkArea); + + void clear(); + + std::vector AStarRoute(int startNodeIdx, int endNodeIdx); + + std::vector dijkstraRoute(int start, int end); + + float path_cost(int a, int b); + + int getNodeIdx(Bsp* map, Entity* ent); + +private: +}; \ No newline at end of file diff --git a/src/nav/LeafNavMeshGenerator.cpp b/src/nav/LeafNavMeshGenerator.cpp new file mode 100644 index 00000000..6512578a --- /dev/null +++ b/src/nav/LeafNavMeshGenerator.cpp @@ -0,0 +1,528 @@ +#include "Renderer.h" +#include "LeafNavMeshGenerator.h" +#include "GLFW/glfw3.h" +#include "PolyOctree.h" +#include "Clipper.h" +#include "Bsp.h" +#include "LeafNavMesh.h" +#include +#include "log.h" +#include "util.h" +#include "LeafOctree.h" +#include +#include +#include "Entity.h" + + +LeafNavMesh* LeafNavMeshGenerator::generate(Bsp* map) { + float NavMeshGeneratorGenStart = glfwGetTime(); + + float createLeavesStart = glfwGetTime(); + std::vector leaves = getHullLeaves(map, 0, CONTENTS_EMPTY); + print_log("Created {} leaf nodes in {}\n", leaves.size(), glfwGetTime() - createLeavesStart); + + LeafOctree* octree = createLeafOctree(map, leaves, octreeDepth); + LeafNavMesh* navmesh = new LeafNavMesh(leaves, octree); + + linkNavLeaves(map, navmesh); + setLeafOrigins(map, navmesh); + linkEntityLeaves(map, navmesh); + calcPathCosts(map, navmesh); + + size_t totalSz = 0; + for (int i = 0; i < navmesh->nodes.size(); i++) { + totalSz += sizeof(LeafNode) + (sizeof(LeafLink) * navmesh->nodes[i].links.size()); + + for (int k = 0; k < navmesh->nodes[i].links.size(); k++) { + totalSz += navmesh->nodes[i].links[k].linkArea.sizeBytes() - sizeof(Polygon3D); + } + for (int k = 0; k < navmesh->nodes[i].leafFaces.size(); k++) { + totalSz += navmesh->nodes[i].leafFaces[k].sizeBytes(); + } + } + + print_log("Generated {} node nav mesh in {} ({} KB)\n", navmesh->nodes.size(), + glfwGetTime() - NavMeshGeneratorGenStart, totalSz / 1024); + + return navmesh; +} + +std::vector LeafNavMeshGenerator::getHullLeaves(Bsp* map, int modelIdx, int contents) { + std::vector emptyLeaves; + + if (modelIdx < 0 || modelIdx >= map->modelCount) { + return emptyLeaves; + } + + Clipper clipper; + + std::vector emptyNodes = map->get_model_leaf_volume_cuts(modelIdx, NAV_HULL, contents); + + std::vector emptyMeshes; + for (int k = 0; k < emptyNodes.size(); k++) { + emptyMeshes.push_back(clipper.clip(emptyNodes[k].cuts)); + } + + // GET FACES FROM MESHES + for (int m = 0; m < emptyMeshes.size(); m++) { + CMesh& mesh = emptyMeshes[m]; + + LeafNode leaf = LeafNode(); + leaf.mins = vec3(FLT_MAX, FLT_MAX, FLT_MAX); + leaf.maxs = vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); + + for (int f = 0; f < mesh.faces.size(); f++) { + CFace& face = mesh.faces[f]; + if (!face.visible) { + continue; + } + + std::set uniqueFaceVerts; + + for (int k = 0; k < face.edges.size(); k++) { + for (int v = 0; v < 2; v++) { + int vertIdx = mesh.edges[face.edges[k]].verts[v]; + if (!mesh.verts[vertIdx].visible) { + continue; + } + uniqueFaceVerts.insert(vertIdx); + } + } + + std::vector faceVerts; + for (auto vertIdx : uniqueFaceVerts) { + faceVerts.push_back(mesh.verts[vertIdx].pos); + } + + faceVerts = getSortedPlanarVerts(faceVerts); + + if (faceVerts.size() < 3) { + //print_log("Degenerate clipnode face discarded {}\n", faceVerts.size()); + continue; + } + + vec3 normal = getNormalFromVerts(faceVerts); + + if (dotProduct(face.normal, normal) < 0) { + reverse(faceVerts.begin(), faceVerts.end()); + normal = normal.invert(); + } + + Polygon3D poly = Polygon3D(faceVerts); + poly.removeDuplicateVerts(); + + leaf.leafFaces.push_back(poly); + } + + if (leaf.leafFaces.size() > 2) { + leaf.center = vec3(); + for (int i = 0; i < leaf.leafFaces.size(); i++) { + Polygon3D& face = leaf.leafFaces[i]; + leaf.center += face.center; + + for (int k = 0; k < face.verts.size(); k++) { + expandBoundingBox(face.verts[k], leaf.mins, leaf.maxs); + } + } + leaf.center /= leaf.leafFaces.size(); + leaf.id = (int)emptyLeaves.size(); + leaf.origin = leaf.center; + + emptyLeaves.push_back(leaf); + } + } + + return emptyLeaves; +} + +void LeafNavMeshGenerator::getOctreeBox(Bsp* map, vec3& min, vec3& max) { + vec3 mapMins; + vec3 mapMaxs; + map->get_bounding_box(mapMins, mapMaxs); + + min = vec3(-FLT_MAX_COORD, -FLT_MAX_COORD, -FLT_MAX_COORD); + max = vec3(FLT_MAX_COORD, FLT_MAX_COORD, FLT_MAX_COORD); + + while (isBoxContained(mapMins, mapMaxs, min * 0.5f, max * 0.5f)) { + max *= 0.5f; + min *= 0.5f; + } +} + +LeafOctree* LeafNavMeshGenerator::createLeafOctree(Bsp* map, std::vector& nodes, int treeDepth) { + float treeStart = glfwGetTime(); + + vec3 treeMin, treeMax; + getOctreeBox(map, treeMin, treeMax); + + LeafOctree* octree = new LeafOctree(treeMin, treeMax, treeDepth); + + for (int i = 0; i < nodes.size(); i++) { + octree->insertLeaf(&nodes[i]); + } + + print_log("Create octree depth {}, size {} -> {} in {}\n", treeDepth, + treeMax.x, treeMax.x / pow(2, treeDepth), (float)glfwGetTime() - treeStart); + + return octree; +} + +void LeafNavMeshGenerator::setLeafOrigins(Bsp* map, LeafNavMesh* mesh) { + float timeStart = glfwGetTime(); + + for (int i = 0; i < mesh->nodes.size(); i++) { + LeafNode& node = mesh->nodes[i]; + + vec3 testBottom = node.center - vec3(0, 0, 4096); + node.origin = node.center; + int bottomFaceIdx = -1; + for (int n = 0; n < node.leafFaces.size(); i++) { + Polygon3D& face = node.leafFaces[n]; + if (face.intersect(node.center, testBottom, node.origin)) { + bottomFaceIdx = n; + break; + } + } + node.origin.z += NAV_BOTTOM_EPSILON; + + if (bottomFaceIdx != -1) { + node.origin = getBestPolyOrigin(map, node.leafFaces[bottomFaceIdx], node.origin); + } + + for (int k = 0; k < node.links.size(); k++) { + LeafLink& link = node.links[k]; + + link.pos = getBestPolyOrigin(map, link.linkArea, link.pos); + } + } + + print_log("Set leaf origins in {}\n", (float)glfwGetTime() - timeStart); +} + +vec3 LeafNavMeshGenerator::getBestPolyOrigin(Bsp* map, Polygon3D& poly, vec3 bias) { + TraceResult tr; + map->traceHull(bias, bias + vec3(0, 0, -4096), NAV_HULL, &tr); + float height = bias.z - tr.vecEndPos.z; + + if (height < NAV_STEP_HEIGHT) { + return bias; + } + + float step = 8.0f; + + float bestHeight = FLT_MAX; + float bestCenterDist = FLT_MAX; + vec3 bestPos = bias; + float pad = 1.0f + EPSILON; // don't choose a point right against a face of the volume + + for (int y = poly.localMins.y + pad; y < poly.localMaxs.y - pad; y += step) { + for (int x = poly.localMins.x + pad; x < poly.localMaxs.x - pad; x += step) { + vec3 testPos = poly.unproject(vec2(x, y)); + testPos.z += NAV_BOTTOM_EPSILON; + + map->traceHull(testPos, testPos + vec3(0, 0, -4096), NAV_HULL, &tr); + height = testPos.z - tr.vecEndPos.z; + float heightDelta = height - bestHeight; + float centerDist = (testPos - bias).lengthSquared(); + + if (bestHeight <= NAV_STEP_HEIGHT) { + if (height <= NAV_STEP_HEIGHT && centerDist < bestCenterDist) { + bestHeight = height; + bestCenterDist = centerDist; + bestPos = testPos; + } + } + else if (heightDelta < -EPSILON) { + bestHeight = height; + bestCenterDist = centerDist; + bestPos = testPos; + } + else if (fabs(heightDelta) < EPSILON && centerDist < bestCenterDist) { + bestHeight = height; + bestCenterDist = centerDist; + bestPos = testPos; + } + } + } + + return bestPos; +} + +void LeafNavMeshGenerator::linkNavLeaves(Bsp* map, LeafNavMesh* mesh) { + int numLinks = 0; + float linkStart = glfwGetTime(); + + std::vector regionLeaves; + regionLeaves.resize(mesh->nodes.size()); + + for (int i = 0; i < mesh->nodes.size(); i++) { + LeafNode& leaf = mesh->nodes[i]; + int leafIdx = map->get_leaf(leaf.center, 3); + + if (leafIdx >= 0 && leafIdx < MAX_MAP_CLIPNODE_LEAVES) { + mesh->leafMap[leafIdx] = i; + } + + mesh->octree->getLeavesInRegion(&leaf, regionLeaves); + + for (int k = i + 1; k < mesh->nodes.size(); k++) { + if (!regionLeaves[k]) { + continue; + } + + numLinks += tryFaceLinkLeaves(map, mesh, i, k); + } + } + + print_log("Added {} nav leaf links in {}\n", numLinks, (float)glfwGetTime() - linkStart); +} + +void LeafNavMeshGenerator::linkEntityLeaves(Bsp* map, LeafNavMesh* mesh) { + std::vector regionLeaves; + regionLeaves.resize(mesh->nodes.size()); + + const vec3 pointMins = vec3(-16, -16, -36); + const vec3 pointMaxs = vec3(16, 16, 36); + + for (int i = 0; i < map->ents.size(); i++) { + Entity* ent = map->ents[i]; + + if (ent->keyvalues["classname"] == "func_ladder") { + LeafNode& entNode = addSolidEntityNode(map, mesh, i); + entNode.maxs.z += NAV_CROUCHJUMP_HEIGHT; // players can stand on top of the ladder for more height + entNode.origin = (entNode.mins + entNode.maxs) * 0.5f; + + linkEntityLeaves(map, mesh, entNode, regionLeaves); + } + else if (ent->keyvalues["classname"] == "trigger_teleport") { + LeafNode& teleNode = addSolidEntityNode(map, mesh, i); + linkEntityLeaves(map, mesh, teleNode, regionLeaves); + + // link teleport destination(s) to touched nodes + int pentTarget = -1; + std::vector targets; + + const int SF_TELE_RANDOM_DESTINATION = 64; + std::string target = ent->keyvalues["target"]; + bool randomDestinations = atoi(ent->keyvalues["spawnflags"].c_str()) & SF_TELE_RANDOM_DESTINATION; + + if (!target.length()) { + continue; + } + + for (int k = 0; k < map->ents.size(); k++) { + Entity* tar = map->ents[k]; + if (tar->keyvalues["targetname"] == target) { + if (tar->keyvalues["classname"] == "info_teleport_destination") { + targets.push_back(k); + } + else if (pentTarget == -1) { + pentTarget = k; + } + } + } + + if (!randomDestinations && targets.size()) { + pentTarget = targets[0]; // prefer teleport destinations + } + + if (randomDestinations && !targets.empty()) { + // link all possible targets + for (int k = 0; k < targets.size(); k++) { + LeafNode& entNode = addPointEntityNode(map, mesh, targets[k], pointMins, pointMaxs); + linkEntityLeaves(map, mesh, entNode, regionLeaves); + + teleNode.addLink(entNode.id, teleNode.origin); + } + } + else if (pentTarget != -1) { + LeafNode& entNode = addPointEntityNode(map, mesh, pentTarget, pointMins, pointMaxs); + linkEntityLeaves(map, mesh, entNode, regionLeaves); + + teleNode.addLink(entNode.id, teleNode.origin); + } + } + } +} + +void LeafNavMeshGenerator::linkEntityLeaves(Bsp* map, LeafNavMesh* mesh, LeafNode& entNode, std::vector& regionLeaves) { + mesh->octree->getLeavesInRegion(&entNode, regionLeaves); + + // link teleport destinations to touched nodes + for (int i = 0; i < mesh->nodes.size(); i++) { + if (!regionLeaves[i]) { + continue; + } + + LeafNode& node = mesh->nodes[i]; + if (boxesIntersect(node.mins, node.maxs, entNode.mins, entNode.maxs)) { + vec3 linkPos = entNode.origin; + linkPos.z = node.origin.z; + + entNode.addLink(i, linkPos); + node.addLink(entNode.id, linkPos); + } + } +} + +LeafNode& LeafNavMeshGenerator::addSolidEntityNode(Bsp* map, LeafNavMesh* mesh, int entidx) { + Entity* ent = map->ents[entidx]; + std::vector leaves = getHullLeaves(map, ent->getBspModelIdx(), CONTENTS_SOLID); + + // create a special ladder node which is a combination of all its leaves + LeafNode ladderNode = LeafNode(); + ladderNode.mins = vec3(FLT_MAX, FLT_MAX, FLT_MAX); + ladderNode.maxs = vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); + + for (LeafNode& node : leaves) { + expandBoundingBox(node.mins, ladderNode.mins, ladderNode.maxs); + expandBoundingBox(node.maxs, ladderNode.mins, ladderNode.maxs); + + for (int i = 0; i < node.leafFaces.size(); i++) { + ladderNode.leafFaces.push_back(node.leafFaces[i]); + } + } + ladderNode.origin = (ladderNode.mins + ladderNode.maxs) * 0.5f; + ladderNode.id = (int)mesh->nodes.size(); + ladderNode.entidx = entidx; + + mesh->nodes.push_back(ladderNode); + return mesh->nodes[mesh->nodes.size() - 1]; +} + +LeafNode& LeafNavMeshGenerator::addPointEntityNode(Bsp* map, LeafNavMesh* mesh, int entidx, vec3 mins, vec3 maxs) { + Entity* ent = map->ents[entidx]; + + LeafNode node = LeafNode(); + node.origin = node.center = ent->origin; + node.mins = node.origin + mins; + node.maxs = node.origin + maxs; + node.id = (int)mesh->nodes.size(); + node.entidx = entidx; + + mesh->nodes.push_back(node); + return mesh->nodes[mesh->nodes.size() - 1]; +} + +int LeafNavMeshGenerator::tryFaceLinkLeaves(Bsp* map, LeafNavMesh* mesh, int srcLeafIdx, int dstLeafIdx) { + LeafNode& srcLeaf = mesh->nodes[srcLeafIdx]; + LeafNode& dstLeaf = mesh->nodes[dstLeafIdx]; + + for (int i = 0; i < srcLeaf.leafFaces.size(); i++) { + Polygon3D& srcFace = srcLeaf.leafFaces[i]; + + for (int k = 0; k < dstLeaf.leafFaces.size(); k++) { + Polygon3D& dstFace = dstLeaf.leafFaces[k]; + + Polygon3D intersectFace = srcFace.coplanerIntersectArea(dstFace); + + if (intersectFace.isValid) { + mesh->addLink(srcLeafIdx, dstLeafIdx, intersectFace); + mesh->addLink(dstLeafIdx, srcLeafIdx, intersectFace); + return 2; + } + } + } + + return 0; +} + +void LeafNavMeshGenerator::calcPathCosts(Bsp* bsp, LeafNavMesh* mesh) { + float markStart = glfwGetTime(); + + for (int i = 0; i < mesh->nodes.size(); i++) { + LeafNode& node = mesh->nodes[i]; + + for (int k = 0; k < node.links.size(); k++) { + LeafLink& link = node.links[k]; + LeafNode& otherNode = mesh->nodes[link.node]; + + link.baseCost = 0; + link.costMultiplier = 1.0f; + + if (node.entidx != 0 || otherNode.entidx != 0) { + // entity links are things like ladders and elevators and cost nothing to use + // so that the path finder prefers them to flying or jumping off ledges + continue; + } + + vec3 start = node.origin; + vec3 mid = link.pos; + vec3 end = otherNode.origin; + bool isDrop = end.z + EPSILON < start.z; + + TraceResult tr; + bsp->traceHull(node.origin, link.pos, NAV_HULL, &tr); + + addPathCost(link, bsp, start, mid, isDrop); + addPathCost(link, bsp, mid, end, isDrop); + } + } + + print_log("Calculated path costs in {}\n", (float)glfwGetTime() - markStart); +} + +void LeafNavMeshGenerator::addPathCost(LeafLink& link, Bsp* bsp, vec3 start, vec3 end, bool isDrop) { + TraceResult tr; + + int steps = (end - start).length() / 8.0f; + vec3 delta = end - start; + vec3 dir = delta.normalize(); + + bool flyingNeeded = false; + bool stackingNeeded = false; + bool isSteepSlope = false; + float maxHeight = 0; + + for (int i = 0; i < steps; i++) { + float t = i * (1.0f / (float)steps); + + vec3 top = start + delta * t; + vec3 bottom = top + vec3(0, 0, -4096); + + bsp->traceHull(top, bottom, NAV_HULL, &tr); + float height = (tr.vecEndPos - top).length(); + + if (tr.vecPlaneNormal.z < 0.7f) { + isSteepSlope = true; + } + + if (height > maxHeight) { + maxHeight = height; + } + + if (height > NAV_CROUCHJUMP_STACK_HEIGHT) { + flyingNeeded = true; + } + else if (height > NAV_CROUCHJUMP_HEIGHT) { + stackingNeeded = true; + } + } + + if (isDrop && (flyingNeeded || stackingNeeded)) { + // probably falling. not much cost but prefer hitting the ground + // TODO: deadly fall distances should be avoided + link.costMultiplier = std::max(link.costMultiplier, 10.0f); + } + else if (flyingNeeded) { + // players can't fly normally so any valid ground path will be better, no matter how long it is. + // As a last resort, "flying" is possible by getting a bunch of players to stack or by using the + // gauss gun. + link.baseCost = std::max(link.baseCost, 64000.0f); + link.costMultiplier = std::max(link.costMultiplier, 100.0f); + } + else if (stackingNeeded) { + // a player can't reach this high on their own, stacking is needed. + // prefer walking an additional X units instead of waiting for a player or box to stack on + link.baseCost = std::max(link.baseCost, 8000.0f); + link.costMultiplier = std::max(link.costMultiplier, 100.0f); + } + else if (isSteepSlope) { + // players can slide up slopes but its excruciatingly slow. Try to find stairs or something. + link.costMultiplier = std::max(link.costMultiplier, 10.0f); + } + else if (maxHeight > NAV_STEP_HEIGHT) { + // prefer paths which don't require jumping + link.costMultiplier = std::max(link.costMultiplier, 2.0f); + } +} \ No newline at end of file diff --git a/src/nav/LeafNavMeshGenerator.h b/src/nav/LeafNavMeshGenerator.h new file mode 100644 index 00000000..4f8c17c9 --- /dev/null +++ b/src/nav/LeafNavMeshGenerator.h @@ -0,0 +1,54 @@ +#pragma once +#include "Polygon3D.h" +#include "LeafNavMesh.h" + +class Bsp; +class LeafOctree; + +// generates a navigation mesh for a BSP +class LeafNavMeshGenerator { +public: + LeafNavMeshGenerator() {} + + // generate a nav mesh from the bsp + // returns polygons used to construct the mesh + LeafNavMesh* generate(Bsp* map); + +private: + int octreeDepth = 6; + + // get leaves of the bsp tree with the given contents + std::vector getHullLeaves(Bsp* map, int modelIdx, int contents); + + // get smallest octree box that can contain the entire map + void getOctreeBox(Bsp* map, vec3& min, vec3& max); + + // group polys that are close together for fewer collision checks later + LeafOctree* createLeafOctree(Bsp* map, std::vector& mesh, int treeDepth); + + // finds best origin for a leaf + void setLeafOrigins(Bsp* map, LeafNavMesh* mesh); + + // find point on poly which is closest to a floor, using distance to the bias point as a tie breaker + vec3 getBestPolyOrigin(Bsp* map, Polygon3D& poly, vec3 bias); + + // links nav leaves which have faces touching each other + void linkNavLeaves(Bsp* map, LeafNavMesh* mesh); + + // use entities to create cheaper paths between leaves + void linkEntityLeaves(Bsp* map, LeafNavMesh* mesh); + + void linkEntityLeaves(Bsp* map, LeafNavMesh* mesh, LeafNode& entNode, std::vector& regionLeaves); + + // returns a combined node for an entity, which is the bounding box of all its model leaves + LeafNode& addSolidEntityNode(Bsp* map, LeafNavMesh* mesh, int entidx); + + // returns a node for an entity, which is its bounding box + LeafNode& addPointEntityNode(Bsp* map, LeafNavMesh* mesh, int entidx, vec3 mins, vec3 maxs); + + int tryFaceLinkLeaves(Bsp* map, LeafNavMesh* mesh, int srcLeafIdx, int dstLeafIdx); + + void calcPathCosts(Bsp* bsp, LeafNavMesh* mesh); + + void addPathCost(LeafLink& link, Bsp* bsp, vec3 start, vec3 end, bool isDrop); +}; \ No newline at end of file diff --git a/src/nav/LeafOctree.cpp b/src/nav/LeafOctree.cpp new file mode 100644 index 00000000..5a967262 --- /dev/null +++ b/src/nav/LeafOctree.cpp @@ -0,0 +1,102 @@ +#include "LeafOctree.h" +#include "util.h" +#include +#include + +LeafOctant::LeafOctant(vec3 min, vec3 max) { + this->min = min; + this->max = max; + memset(children, NULL, sizeof(LeafOctant*) * 8); +} + +LeafOctant::~LeafOctant() { + for (LeafOctant* child : children) { + delete child; + } +} + +void LeafOctant::removeLeaf(LeafNode* leaf) { + leaves.erase(std::remove(leaves.begin(), leaves.end(), leaf), leaves.end()); + for (int i = 0; i < 8; i++) { + if (children[i]) + children[i]->removeLeaf(leaf); + } +} + +LeafOctree::~LeafOctree() { + delete root; +} + +LeafOctree::LeafOctree(const vec3& min, const vec3& max, int depth) { + root = new LeafOctant(min, max); + maxDepth = depth; + buildOctree(root, 0); +} + +void LeafOctree::buildOctree(LeafOctant* node, int currentDepth) { + if (currentDepth >= maxDepth) { + return; + } + const vec3& min = node->min; + const vec3& max = node->max; + vec3 mid((min.x + max.x) / 2, (min.y + max.y) / 2, (min.z + max.z) / 2); + + // Define eight child octants using the min and max values + node->children[0] = new LeafOctant(min, mid); + node->children[1] = new LeafOctant(vec3(mid.x, min.y, min.z), vec3(max.x, mid.y, mid.z)); + node->children[2] = new LeafOctant(vec3(min.x, mid.y, min.z), vec3(mid.x, max.y, mid.z)); + node->children[3] = new LeafOctant(vec3(mid.x, mid.y, min.z), vec3(max.x, max.y, mid.z)); + node->children[4] = new LeafOctant(vec3(min.x, min.y, mid.z), vec3(mid.x, mid.y, max.z)); + node->children[5] = new LeafOctant(vec3(mid.x, min.y, mid.z), vec3(max.x, mid.y, max.z)); + node->children[6] = new LeafOctant(vec3(min.x, mid.y, mid.z), vec3(mid.x, max.y, max.z)); + node->children[7] = new LeafOctant(mid, max); + + for (LeafOctant* child : node->children) { + buildOctree(child, currentDepth + 1); + } +} + +void LeafOctree::insertLeaf(LeafNode* leaf) { + insertLeaf(root, leaf, 0); +} + +void LeafOctree::insertLeaf(LeafOctant* node, LeafNode* leaf, int currentDepth) { + if (currentDepth >= maxDepth) { + node->leaves.push_back(leaf); + return; + } + for (int i = 0; i < 8; ++i) { + if (isLeafInOctant(leaf, node->children[i])) { + insertLeaf(node->children[i], leaf, currentDepth + 1); + } + } +} + +void LeafOctree::removeLeaf(LeafNode* leaf) { + root->removeLeaf(leaf); +} + +bool LeafOctree::isLeafInOctant(LeafNode* leaf, LeafOctant* node) { + vec3 epsilon = vec3(1, 1, 1); // in case leaves are touching right on the border of an octree leaf + return boxesIntersect(leaf->mins - epsilon, leaf->maxs + epsilon, node->min, node->max); +} + +void LeafOctree::getLeavesInRegion(LeafNode* leaf, std::vector& regionLeaves) { + fill(regionLeaves.begin(), regionLeaves.end(), false); + getLeavesInRegion(root, leaf, 0, regionLeaves); +} + +void LeafOctree::getLeavesInRegion(LeafOctant* node, LeafNode* leaf, int currentDepth, std::vector& regionLeaves) { + if (currentDepth >= maxDepth) { + for (auto p : node->leaves) { + if (p->id != -1) + regionLeaves[p->id] = true; + } + return; + } + for (int i = 0; i < 8; ++i) { + if (isLeafInOctant(leaf, node->children[i])) { + getLeavesInRegion(node->children[i], leaf, currentDepth + 1, regionLeaves); + } + } +} diff --git a/src/nav/LeafOctree.h b/src/nav/LeafOctree.h new file mode 100644 index 00000000..578782fd --- /dev/null +++ b/src/nav/LeafOctree.h @@ -0,0 +1,42 @@ +#pragma once +#include "Polygon3D.h" +#include +#include "LeafNavMesh.h" + +struct LeafOctant { + vec3 min; + vec3 max; + std::vector leaves; + LeafOctant* children[8]; // Eight children octants + + LeafOctant(vec3 min, vec3 max); + + ~LeafOctant(); + + void removeLeaf(LeafNode* polygon); +}; + +class LeafOctree { +public: + LeafOctant* root; + int maxDepth; + + LeafOctree(const vec3& min, const vec3& max, int depth); + + ~LeafOctree(); + + void insertLeaf(LeafNode* leaf); + + void removeLeaf(LeafNode* leaf); + + bool isLeafInOctant(LeafNode* leaf, LeafOctant* node); + + void getLeavesInRegion(LeafNode* leaf, std::vector& regionLeaves); + +private: + void buildOctree(LeafOctant* node, int currentDepth); + + void getLeavesInRegion(LeafOctant* node, LeafNode* leaf, int currentDepth, std::vector& regionLeaves); + + void insertLeaf(LeafOctant* node, LeafNode* leaf, int currentDepth); +}; \ No newline at end of file diff --git a/src/nav/NavMesh.cpp b/src/nav/NavMesh.cpp new file mode 100644 index 00000000..e2b201f9 --- /dev/null +++ b/src/nav/NavMesh.cpp @@ -0,0 +1,159 @@ +#include "NavMesh.h" +#include "PolyOctree.h" +#include "Clipper.h" +#include "log.h" +#include "util.h" +#include +#include "GLFW/glfw3.h" + +bool NavNode::addLink(int node, int srcEdge, int dstEdge, int zDist, uint8_t _flags) { + if (srcEdge < 0 || srcEdge >= MAX_NAV_POLY_VERTS) { + print_log("Error: add link to invalid src edge {}\n", srcEdge); + return false; + } + if (dstEdge < 0 || dstEdge >= MAX_NAV_POLY_VERTS) { + print_log("Error: add link to invalid dst edge {}\n", dstEdge); + return false; + } + + for (int i = 0; i < MAX_NAV_LINKS; i++) { + if (links[i].node == node) { + links[i].srcEdge = srcEdge; + links[i].dstEdge = dstEdge; + links[i].zDist = zDist; + links[i].flags = _flags; + return true; + } + if (links[i].node == -1) { + links[i].srcEdge = srcEdge; + links[i].dstEdge = dstEdge; + links[i].node = node; + links[i].zDist = zDist; + links[i].flags = _flags; + return true; + } + } + + print_log("Error: Max links reached on node {}\n", id); + return false; +} + +int NavNode::numLinks() { + int numLinks = 0; + + for (int i = 0; i < MAX_NAV_LINKS; i++) { + if (links[i].node == -1) { + break; + } + numLinks++; + } + + return numLinks; +} + +NavMesh::NavMesh() { + clear(); +} + +void NavMesh::clear() { + memset(nodes, 0, sizeof(NavNode) * MAX_NAV_POLYS); + + for (int i = 0; i < MAX_NAV_POLYS; i++) { + polys[i] = Polygon3D(); + nodes[i].id = i; + + for (int k = 0; k < MAX_NAV_LINKS; k++) { + nodes[i].links[k].srcEdge = 0; + nodes[i].links[k].dstEdge = 0; + nodes[i].links[k].node = -1; + nodes[i].links[k].zDist = 0; + } + } +} + +NavMesh::NavMesh(std::vector faces) { + clear(); + + for (int i = 0; i < faces.size(); i++) { + polys[i] = Polygon3D(faces[i].verts); + if (faces[i].verts.size() > MAX_NAV_POLY_VERTS) + print_log("Error: Face {} has {} verts (max is {})\n", i, faces[i].verts.size(), MAX_NAV_POLY_VERTS); + } + numPolys = faces.size(); + + print_log("Created nav mesh with {} polys (x{} = {} KB)\n", + numPolys, sizeof(NavNode), (sizeof(NavNode)*numPolys) / 1024); + + print_log("NavPolyNode = {} bytes, NavLink = {} bytes\n", + sizeof(NavNode), sizeof(NavLink)); +} + +bool NavMesh::addLink(int from, int to, int srcEdge, int dstEdge, int zDist, uint8_t flags) { + if (from < 0 || to < 0 || from >= MAX_NAV_POLYS || to >= MAX_NAV_POLYS) { + print_log("Error: add link from/to invalid node {} {}\n", from, to); + return false; + } + + if (!nodes[from].addLink(to, srcEdge, dstEdge, zDist, flags)) { + vec3& pos = polys[from].center; + print_log("Failed to add link at {} {} {}\n", (int)pos.x, (int)pos.y, (int)pos.z); + return false; + } + + return true; +} + +std::vector NavMesh::getPolys() { + std::vector ret; + + for (int i = 0; i < numPolys; i++) { + ret.push_back(polys[i]); + } + + return ret; +} + +void NavMesh::getLinkMidPoints(int iNode, int iLink, vec3& srcMid, vec3& dstMid) { + srcMid = dstMid = vec3(); + if (iNode < 0 || iNode >= MAX_NAV_POLYS) { + return; + } + if (iLink < 0 || iLink >= MAX_NAV_LINKS) { + return; + } + + NavLink& link = nodes[iNode].links[iLink]; + if (link.node < 0 || link.node >= MAX_NAV_POLYS) { + return; + } + + Polygon3D& srcPoly = polys[iNode]; + Polygon3D& dstPoly = polys[link.node]; + + int e2i = (link.srcEdge + 1) % srcPoly.verts.size(); + int e4i = (link.dstEdge + 1) % dstPoly.verts.size(); + vec2 e1 = srcPoly.topdownVerts[link.srcEdge]; + vec2 e2 = srcPoly.topdownVerts[e2i]; + vec2 e3 = dstPoly.topdownVerts[link.dstEdge]; + vec2 e4 = dstPoly.topdownVerts[e4i]; + + float t0, t1, t2, t3; + Line2D e34(e3, e4); + Line2D(e1, e2).getOverlapRanges(e34, t0, t1, t2, t3); + + { + vec3 edgeStart = srcPoly.verts[link.srcEdge]; + vec3 edgeDelta = srcPoly.verts[e2i] - edgeStart; + vec3 borderStart = edgeStart + edgeDelta * t0; + vec3 borderEnd = edgeStart + edgeDelta * t1; + srcMid = borderStart + (borderEnd - borderStart) * 0.5f; + } + + { + vec3 edgeStart = dstPoly.verts[link.dstEdge]; + vec3 edgeDelta = dstPoly.verts[e4i] - edgeStart; + vec3 borderStart = edgeStart + edgeDelta * t2; + vec3 borderEnd = edgeStart + edgeDelta * t3; + dstMid = borderStart + (borderEnd - borderStart) * 0.5f; + } +} diff --git a/src/nav/NavMesh.h b/src/nav/NavMesh.h new file mode 100644 index 00000000..c61bcd92 --- /dev/null +++ b/src/nav/NavMesh.h @@ -0,0 +1,56 @@ +#pragma once +#include "Polygon3D.h" + +#define MAX_NAV_POLYS 4096 +#define MAX_NAV_POLY_VERTS 16 +#define MAX_NAV_LINKS 32 + +#define FL_LINK_LONGJUMP (1<<1) // must use the longjump to reach poly +#define FL_LINK_TELEPORT (1<<2) // must use teleport in source poly to reach target poly + +#define NAV_STEP_HEIGHT 18 +#define NAV_JUMP_HEIGHT 44 +#define NAV_CROUCHJUMP_HEIGHT 63 // 208 gravity 50% +#define NAV_AUTOCLIMB_HEIGHT 117 + +struct NavLink { + uint8_t srcEdge : 4; // edge to move from in source poly + uint8_t dstEdge : 4; // edge to move to in target/destination poly + uint8_t flags; + int node; // which poly is linked to. -1 = end of links + int zDist; // minimum height difference between the connecting edges +}; + +struct NavNode { + NavLink links[MAX_NAV_LINKS]; + unsigned int flags; + unsigned int id; + + // adds a link to node "node" on edge "edge" with height difference "zDist" + bool addLink(int node, int srcEdge, int dstEdge, int zDist, uint8_t flags); + int numLinks(); +}; + + +class NavMesh { +public: + NavNode nodes[MAX_NAV_POLYS]; + Polygon3D polys[MAX_NAV_POLYS]; + + size_t numPolys; + + NavMesh(); + + NavMesh(std::vector polys); + + bool addLink(int from, int to, int srcEdge, int dstEdge, int zDist, uint8_t flags); + + void clear(); + + // get mid points on borders between 2 polys + void getLinkMidPoints(int iNode, int iLink, vec3& srcMid, vec3& dstMid); + + std::vector getPolys(); + +private: +}; \ No newline at end of file diff --git a/src/nav/NavMeshGenerator.cpp b/src/nav/NavMeshGenerator.cpp new file mode 100644 index 00000000..decb7eee --- /dev/null +++ b/src/nav/NavMeshGenerator.cpp @@ -0,0 +1,462 @@ +#include "NavMeshGenerator.h" +#include "PolyOctree.h" +#include "Clipper.h" +#include "Bsp.h" +#include "NavMesh.h" +#include +#include "log.h" +#include "util.h" +#include + + +#include "GLFW/glfw3.h" + +NavMesh* NavMeshGenerator::generate(Bsp* map, int hull) { + float NavMeshGeneratorGenStart = glfwGetTime(); + + std::vector solidFaces = getHullFaces(map, hull); + std::vector faces = getInteriorFaces(map, hull, solidFaces); + mergeFaces(map, faces); + cullTinyFaces(faces); + + for (int i = 0; i < solidFaces.size(); i++) { + if (solidFaces[i]) + delete solidFaces[i]; + } + + print_log("Generated nav mesh in {}\n", faces.size(), glfwGetTime() - NavMeshGeneratorGenStart); + + NavMesh* navmesh = new NavMesh(faces); + linkNavPolys(map, navmesh); + + return navmesh; +} + +std::vector NavMeshGenerator::getHullFaces(Bsp* map, int hull) { + float hullShrink = 0; + std::vector solidFaces; + + Clipper clipper; + + std::vector solidNodes = map->get_model_leaf_volume_cuts(0, hull, CONTENTS_SOLID); + + std::vector solidMeshes; + for (int k = 0; k < solidNodes.size(); k++) { + solidMeshes.push_back(clipper.clip(solidNodes[k].cuts)); + } + + // GET FACES FROM MESHES + for (int m = 0; m < solidMeshes.size(); m++) { + CMesh& mesh = solidMeshes[m]; + + for (int f = 0; f < mesh.faces.size(); f++) { + CFace& face = mesh.faces[f]; + if (!face.visible) { + continue; + } + + std::set uniqueFaceVerts; + + for (int k = 0; k < face.edges.size(); k++) { + for (int v = 0; v < 2; v++) { + int vertIdx = mesh.edges[face.edges[k]].verts[v]; + if (!mesh.verts[vertIdx].visible) { + continue; + } + uniqueFaceVerts.insert(vertIdx); + } + } + + std::vector faceVerts; + for (auto vertIdx : uniqueFaceVerts) { + faceVerts.push_back(mesh.verts[vertIdx].pos); + } + + faceVerts = getSortedPlanarVerts(faceVerts); + + if (faceVerts.size() < 3) { + //print_log("Degenerate clipnode face discarded {}\n", faceVerts.size()); + continue; + } + + vec3 normal = getNormalFromVerts(faceVerts); + + if (dotProduct(face.normal, normal) < 0) { + reverse(faceVerts.begin(), faceVerts.end()); + normal = normal.invert(); + } + + Polygon3D* poly = new Polygon3D(faceVerts, (int)solidFaces.size()); + poly->removeDuplicateVerts(); + if (hullShrink) + poly->extendAlongAxis(hullShrink); + + solidFaces.push_back(poly); + + } + } + + return solidFaces; +} + +void NavMeshGenerator::getOctreeBox(Bsp* map, vec3& min, vec3& max) { + vec3 mapMins; + vec3 mapMaxs; + map->get_bounding_box(mapMins, mapMaxs); + + min = vec3(-FLT_MAX_COORD, -FLT_MAX_COORD, -FLT_MAX_COORD); + max = vec3(FLT_MAX_COORD, FLT_MAX_COORD, FLT_MAX_COORD); + + while (isBoxContained(mapMins, mapMaxs, min * 0.5f, max * 0.5f)) { + max *= 0.5f; + min *= 0.5f; + } +} + +PolygonOctree* NavMeshGenerator::createPolyOctree(Bsp* map, const std::vector& faces, int treeDepth) { + vec3 treeMin, treeMax; + getOctreeBox(map, treeMin, treeMax); + + print_log("Create octree depth {}, size {} -> {}\n", treeDepth, treeMax.x, treeMax.x / pow(2, treeDepth)); + PolygonOctree* octree = new PolygonOctree(treeMin, treeMax, treeDepth); + + for (int i = 0; i < faces.size(); i++) { + octree->insertPolygon(faces[i]); + } + + return octree; +} + +std::vector NavMeshGenerator::getInteriorFaces(Bsp* map, int hull, std::vector& faces) { + PolygonOctree* octree = createPolyOctree(map, faces, octreeDepth); + + int debugPoly = 0; + //debugPoly = 601; + + int avgInRegion = 0; + int regionChecks = 0; + + std::vector interiorFaces; + + size_t cuttingPolyCount = faces.size(); + size_t presplit = faces.size(); + int numSplits = 0; + float startTime = glfwGetTime(); + bool doSplit = true; + bool doCull = true; + bool walkableSurfacesOnly = true; + + std::vector regionPolys; + regionPolys.resize(cuttingPolyCount); + + for (int i = 0; i < faces.size(); i++) { + Polygon3D* poly = faces[i]; + //if (debugPoly && i != debugPoly && i < cuttingPolys.size()) { + // continue; + //} + if (!poly->isValid) { + continue; + } + if (walkableSurfacesOnly && poly->plane_z.z < 0.7) { + continue; + } + + //print_log("debug poly idx {} -> {}\n", didx, i); + //didx++; + + //print_log("Splitting {}\n", i); + + octree->getPolysInRegion(poly, regionPolys); + if (poly->idx < cuttingPolyCount) + regionPolys[poly->idx] = false; + regionChecks++; + + bool anySplits = false; + size_t sz = cuttingPolyCount; + + if (!doSplit || (debugPoly && i != debugPoly && i < cuttingPolyCount)) + sz = 0; + + for (size_t k = 0; k < sz; k++) { + if (!regionPolys[k]) { + continue; + } + Polygon3D* cutPoly = faces[k]; + avgInRegion++; + //if (k != 1547) { + // continue; + //} + + std::vector> splitPolys = poly->split(*cutPoly); + + if (splitPolys.size()) { + Polygon3D* newpoly0 = new Polygon3D(splitPolys[0], (int)faces.size()); + Polygon3D* newpoly1 = new Polygon3D(splitPolys[1], (int)faces.size()); + + if (newpoly0->area < EPSILON || newpoly1->area < EPSILON) { + delete newpoly0; + delete newpoly1; + continue; + } + + faces.push_back(newpoly0); + faces.push_back(newpoly1); + + anySplits = true; + numSplits++; + + float newArea = newpoly0->area + newpoly1->area; + if (newArea < poly->area * 0.9f) { + print_log("Poly {} area shrunk by {} ({} -> {})\n", i, (poly->area - newArea), poly->area, newArea); + } + + //print_log("Split poly {} by {} into areas {} {}\n", i, k, newpoly0->area, newpoly1->area); + break; + } + } + if (!doSplit) { + if (i < cuttingPolyCount) { + interiorFaces.push_back(*poly); + } + } + else if (!anySplits && (map->isInteriorFace(*poly, hull) || !doCull)) { + interiorFaces.push_back(*poly); + } + } + print_log("Finished cutting in {}\n", (float)(glfwGetTime() - startTime)); + print_log("Split {} faces into {} ({} splits)\n", presplit, faces.size(), numSplits); + print_log("Average of {} in poly regions\n", regionChecks ? (avgInRegion / regionChecks) : 0); + print_log("Got {} interior faces\n", interiorFaces.size()); + + delete octree; + + return interiorFaces; +} + +void NavMeshGenerator::mergeFaces(Bsp* map, std::vector& faces) { + float mergeStart = glfwGetTime(); + + vec3 treeMin, treeMax; + getOctreeBox(map, treeMin, treeMax); + + size_t preMergePolys = faces.size(); + std::vector mergedFaces = faces; + int pass = 0; + int maxPass = 10; + for (pass = 0; pass <= maxPass; pass++) { + + PolygonOctree mergeOctree(treeMin, treeMax, octreeDepth); + for (int i = 0; i < mergedFaces.size(); i++) { + mergedFaces[i].idx = i; + //interiorFaces[i].removeColinearVerts(); + mergeOctree.insertPolygon(&mergedFaces[i]); + } + + std::vector regionPolys; + regionPolys.resize(mergedFaces.size()); + + std::vector newMergedFaces; + + for (int i = 0; i < mergedFaces.size(); i++) { + Polygon3D& poly = mergedFaces[i]; + if (poly.idx == -1) + continue; + //if (pass == 4 && i != 149) + // continue; + + mergeOctree.getPolysInRegion(&poly, regionPolys); + regionPolys[poly.idx] = false; + + size_t sz = regionPolys.size(); + bool anyMerges = false; + + for (int k = i + 1; k < sz; k++) { + if (!regionPolys[k]) { + continue; + } + Polygon3D& mergePoly = mergedFaces[k]; + /* + if (pass == 4 && k != 242) { + continue; + } + if (pass == 4) { + print_log("debug time\n"); + } + */ + + Polygon3D mergedPoly = poly.merge(mergePoly); + + if (!mergedPoly.isValid || mergedPoly.verts.size() > MAX_NAV_POLY_VERTS) { + continue; + } + + anyMerges = true; + + // prevent any further merges on the original polys + mergePoly.idx = -1; + poly.idx = -1; + + newMergedFaces.push_back(mergedPoly); + break; + } + + if (!anyMerges) + newMergedFaces.push_back(poly); + } + + //print_log("Removed {} polys in pass {}\n", mergedFaces.size() - newMergedFaces.size(), pass + 1); + + if (mergedFaces.size() == newMergedFaces.size() || pass == maxPass) { + break; + } + else { + mergedFaces = newMergedFaces; + } + } + + print_log("Finished merging in {}\n", (float)(glfwGetTime() - mergeStart)); + print_log("Merged {} polys down to {} in {} passes\n", preMergePolys, mergedFaces.size(), pass); + + faces = mergedFaces; +} + +void NavMeshGenerator::cullTinyFaces(std::vector& faces) { + const int TINY_POLY = 64; // cull faces smaller than this + + std::vector finalPolys; + for (int i = 0; i < faces.size(); i++) { + if (faces[i].area < TINY_POLY) { + // TODO: only remove if there is at least one unconnected edge, + // otherwise there will be holes + continue; + } + finalPolys.push_back(faces[i]); + } + + print_log("Removed {} tiny polys\n", faces.size() - finalPolys.size()); + faces = finalPolys; +} + +void NavMeshGenerator::linkNavPolys(Bsp* map, NavMesh* mesh) { + int numLinks = 0; + + float linkStart = glfwGetTime(); + + for (int i = 0; i < mesh->numPolys; i++) { + for (int k = i + 1; k < mesh->numPolys; k++) { + if (i == k) + continue; + numLinks += tryEdgeLinkPolys(map, mesh, i, k); + } + } + + print_log("Added {} nav poly links in {}\n", numLinks, (float)glfwGetTime() - linkStart); +} + +int NavMeshGenerator::tryEdgeLinkPolys(Bsp* map, NavMesh* mesh, int srcPolyIdx, int dstPolyIdx) { + const Polygon3D& srcPoly = mesh->polys[srcPolyIdx]; + const Polygon3D& dstPoly = mesh->polys[dstPolyIdx]; + + for (int i = 0; i < srcPoly.topdownVerts.size(); i++) { + int inext = (i + 1) % srcPoly.topdownVerts.size(); + Line2D thisEdge(srcPoly.topdownVerts[i], srcPoly.topdownVerts[inext]); + + for (int k = 0; k < dstPoly.topdownVerts.size(); k++) { + int knext = (k + 1) % dstPoly.topdownVerts.size(); + Line2D otherEdge(dstPoly.topdownVerts[k], dstPoly.topdownVerts[knext]); + + if (!thisEdge.isAlignedWith(otherEdge)) { + continue; + } + + float t0, t1, t2, t3; + float overlapDist = thisEdge.getOverlapRanges(otherEdge, t0, t1, t2, t3); + + if (overlapDist < 1.0f) { + continue; // shared region too short + } + + vec3 delta1 = srcPoly.verts[inext] - srcPoly.verts[i]; + vec3 delta2 = srcPoly.verts[knext] - srcPoly.verts[k]; + vec3 e1 = srcPoly.verts[i] + delta1 * t0; + vec3 e2 = srcPoly.verts[i] + delta1 * t1; + vec3 e3 = dstPoly.verts[k] + delta2 * t2; + vec3 e4 = dstPoly.verts[k] + delta2 * t3; + + float min1 = std::min(e1.z, e2.z); + float max1 = std::max(e1.z, e2.z); + float min2 = std::min(e3.z, e4.z); + float max2 = std::max(e3.z, e4.z); + + int zDist = 0; // 0 = edges are are the same height or cross at some point + if (max1 < min2) { // dst is above src + zDist = ceilf(min2 - max1); + } + else if (min1 > max2) { // dst is below src + zDist = floorf(max2 - min1); + } + + if (fabs(zDist) > NAV_STEP_HEIGHT) { + // trace at every point along the edge to see if this connection is possible + // starting at the mid point and working outwards + bool isBelow = zDist > 0; + delta1 = e2 - e1; + delta2 = e4 - e3; + vec3 mid1 = e1 + delta1 * 0.5f; + vec3 mid2 = e3 + delta2 * 0.5f; + vec3 inwardDir = crossProduct(srcPoly.plane_z, delta1.normalize()); + vec3 testOffset = (isBelow ? inwardDir : inwardDir * -1) + vec3(0, 0, 1.0f); + + float flatLen = (vec2(e2.x, e2.y) - vec2(e1.x, e1.y)).length(); + float stepUnits = 1.0f; + float step = stepUnits / flatLen; + TraceResult tr; + bool isBlocked = true; + for (float f = 0; f < 0.5f; f += step) { + vec3 test1 = mid1 + (delta1 * f) + testOffset; + vec3 test2 = mid2 + (delta2 * f) + testOffset; + vec3 test3 = mid1 + (delta1 * -f) + testOffset; + vec3 test4 = mid2 + (delta2 * -f) + testOffset; + + map->traceHull(test1, test2, 3, &tr); + if (!tr.fAllSolid && !tr.fStartSolid && tr.flFraction > 0.9f) { + isBlocked = false; + break; + } + map->traceHull(test3, test4, 3, &tr); + if (!tr.fAllSolid && !tr.fStartSolid && tr.flFraction > 0.9f) { + isBlocked = false; + break; + } + } + + if (isBlocked) { + continue; + } + } + + if (dotProduct(thisEdge.dir, otherEdge.dir) > 0) { + // Polygons overlap, but this is ok when dropping down. + // Technically it's possible to go up too but that's + // hard to pull off and no map requires that + + if (srcPoly.verts[i].z < dstPoly.verts[k].z) { + mesh->addLink(dstPolyIdx, srcPolyIdx, k, i, -zDist, 0); + } + else { + mesh->addLink(srcPolyIdx, dstPolyIdx, i, k, zDist, 0); + } + + return 1; + } + + mesh->addLink(srcPolyIdx, dstPolyIdx, i, k, zDist, 0); + mesh->addLink(dstPolyIdx, srcPolyIdx, k, i, -zDist, 0); + + // TODO: multiple edge links are possible for overlapping polys + return 2; + } + } + + return 0; +} diff --git a/src/nav/NavMeshGenerator.h b/src/nav/NavMeshGenerator.h new file mode 100644 index 00000000..64573a93 --- /dev/null +++ b/src/nav/NavMeshGenerator.h @@ -0,0 +1,47 @@ +#pragma once +#include "Polygon3D.h" +#include "NavMesh.h" + +class Bsp; +class PolygonOctree; + +// generates a navigation mesh for a BSP +class NavMeshGenerator { +public: + NavMeshGenerator() {} + + // generate a nav mesh from the bsp + // returns polygons used to construct the mesh + NavMesh* generate(Bsp* map, int hull); + +private: + int octreeDepth = 6; + + // get faces of the hull that form the borders of the map + std::vector getHullFaces(Bsp* map, int hull); + + // get smallest octree box that can contain the entire map + void getOctreeBox(Bsp* map, vec3& min, vec3& max); + + // group polys that are close together for fewer collision checks later + PolygonOctree* createPolyOctree(Bsp* map, const std::vector& faces, int treeDepth); + + // splits faces along their intersections with each other to clip polys that extend out + // into the void, then tests each poly to see if it faces into the map or into the void. + // Returns clipped faces that face the interior of the map + std::vector getInteriorFaces(Bsp* map, int hull, std::vector& faces); + + // merged polys adjacent to each other to reduce node count + void mergeFaces(Bsp* map, std::vector& faces); + + // removes tiny faces + void cullTinyFaces(std::vector& faces); + + // links nav polys that share an edge from a top-down view + // climbability depends on game settings (gravity, stepsize, autoclimb, grapple/gauss weapon, etc.) + void linkNavPolys(Bsp* map, NavMesh* mesh); + + // tests of polys can be linked by an overlapping edge from the top-down perspective + // returns number of links created + int tryEdgeLinkPolys(Bsp* map, NavMesh* mesh, int srcPolyIdx, int dstPolyIdx); +}; \ No newline at end of file diff --git a/src/nav/PolyOctree.cpp b/src/nav/PolyOctree.cpp new file mode 100644 index 00000000..f4e27c0a --- /dev/null +++ b/src/nav/PolyOctree.cpp @@ -0,0 +1,101 @@ +#include "PolyOctree.h" +#include "util.h" +#include +#include + +PolyOctant::PolyOctant(vec3 min, vec3 max) { + this->min = min; + this->max = max; + memset(children, NULL, sizeof(PolyOctant*) * 8); +} + +PolyOctant::~PolyOctant() { + for (PolyOctant* child : children) { + delete child; + } +} + +void PolyOctant::removePolygon(Polygon3D* polygon) { + polygons.erase(std::remove(polygons.begin(), polygons.end(), polygon), polygons.end()); + for (int i = 0; i < 8; i++) { + if (children[i]) + children[i]->removePolygon(polygon); + } +} + +PolygonOctree::~PolygonOctree() { + delete root; +} + +PolygonOctree::PolygonOctree(const vec3& min, const vec3& max, int depth) { + root = new PolyOctant(min, max); + maxDepth = depth; + buildOctree(root, 0); +} + +void PolygonOctree::buildOctree(PolyOctant* node, int currentDepth) { + if (currentDepth >= maxDepth) { + return; + } + const vec3& min = node->min; + const vec3& max = node->max; + vec3 mid((min.x + max.x) / 2, (min.y + max.y) / 2, (min.z + max.z) / 2); + + // Define eight child octants using the min and max values + node->children[0] = new PolyOctant(min, mid); + node->children[1] = new PolyOctant(vec3(mid.x, min.y, min.z), vec3(max.x, mid.y, mid.z)); + node->children[2] = new PolyOctant(vec3(min.x, mid.y, min.z), vec3(mid.x, max.y, mid.z)); + node->children[3] = new PolyOctant(vec3(mid.x, mid.y, min.z), vec3(max.x, max.y, mid.z)); + node->children[4] = new PolyOctant(vec3(min.x, min.y, mid.z), vec3(mid.x, mid.y, max.z)); + node->children[5] = new PolyOctant(vec3(mid.x, min.y, mid.z), vec3(max.x, mid.y, max.z)); + node->children[6] = new PolyOctant(vec3(min.x, mid.y, mid.z), vec3(mid.x, max.y, max.z)); + node->children[7] = new PolyOctant(mid, max); + + for (PolyOctant* child : node->children) { + buildOctree(child, currentDepth + 1); + } +} + +void PolygonOctree::insertPolygon(Polygon3D* polygon) { + insertPolygon(root, polygon, 0); +} + +void PolygonOctree::insertPolygon(PolyOctant* node, Polygon3D* polygon, int currentDepth) { + if (currentDepth >= maxDepth) { + node->polygons.push_back(polygon); + return; + } + for (int i = 0; i < 8; ++i) { + if (isPolygonInOctant(polygon, node->children[i])) { + insertPolygon(node->children[i], polygon, currentDepth + 1); + } + } +} + +void PolygonOctree::removePolygon(Polygon3D* polygon) { + root->removePolygon(polygon); +} + +bool PolygonOctree::isPolygonInOctant(Polygon3D* polygon, PolyOctant* node) { + return boxesIntersect(polygon->worldMins, polygon->worldMaxs, node->min, node->max); +} + +void PolygonOctree::getPolysInRegion(Polygon3D* poly, std::vector& regionPolys) { + fill(regionPolys.begin(), regionPolys.end(), false); + getPolysInRegion(root, poly, 0, regionPolys); +} + +void PolygonOctree::getPolysInRegion(PolyOctant* node, Polygon3D* poly, int currentDepth, std::vector& regionPolys) { + if (currentDepth >= maxDepth) { + for (auto p : node->polygons) { + if (p->idx != -1) + regionPolys[p->idx] = true; + } + return; + } + for (int i = 0; i < 8; ++i) { + if (isPolygonInOctant(poly, node->children[i])) { + getPolysInRegion(node->children[i], poly, currentDepth + 1, regionPolys); + } + } +} diff --git a/src/nav/PolyOctree.h b/src/nav/PolyOctree.h new file mode 100644 index 00000000..8955a7af --- /dev/null +++ b/src/nav/PolyOctree.h @@ -0,0 +1,41 @@ +#pragma once +#include "Polygon3D.h" +#include + +struct PolyOctant { + vec3 min; + vec3 max; + std::vector polygons; + PolyOctant* children[8]; // Eight children octants + + PolyOctant(vec3 min, vec3 max); + + ~PolyOctant(); + + void removePolygon(Polygon3D* polygon); +}; + +class PolygonOctree { +public: + PolyOctant* root; + int maxDepth; + + PolygonOctree(const vec3& min, const vec3& max, int depth); + + ~PolygonOctree(); + + void insertPolygon(Polygon3D* polygon); + + void removePolygon(Polygon3D* polygon); + + bool isPolygonInOctant(Polygon3D* polygon, PolyOctant* node); + + void getPolysInRegion(Polygon3D* poly, std::vector& regionPolys); + +private: + void buildOctree(PolyOctant* node, int currentDepth); + + void getPolysInRegion(PolyOctant* node, Polygon3D* poly, int currentDepth, std::vector& regionPolys); + + void insertPolygon(PolyOctant* node, Polygon3D* polygon, int currentDepth); +}; \ No newline at end of file diff --git a/src/util/Line2D.cpp b/src/util/Line2D.cpp new file mode 100644 index 00000000..a158e88b --- /dev/null +++ b/src/util/Line2D.cpp @@ -0,0 +1,148 @@ +#include "Line2D.h" +#include "log.h" +#include "util.h" +#include + +Line2D::Line2D(vec2 start, vec2 end) { + this->start = start; + this->end = end; + dir = (end - start).normalize(); +} + +float Line2D::distanceAxis(vec2 p) { + return crossProduct(dir, start - p); +} + +float Line2D::distance(vec2 p) { + float len = (end - start).length(); + float t = dotProduct(p - start, dir) / len; + + if (t < 0) { + return (p - start).length(); + } else if (t > 1) { + return (p - end).length(); + } + + return distanceAxis(p); +} + +vec2 Line2D::project(vec2 p) { + float dot = dotProduct(p - start, dir); + return start + dir*dot; +} + +bool Line2D::isAlignedWith(const Line2D& other) { + if (fabs(dotProduct(dir, other.dir)) < 0.999f) { + return false; // lines not colinear + } + + // Calculate the cross products + float cross1 = crossProduct(dir, other.start - start); + float cross2 = crossProduct(dir, other.end - start); + + // If the cross products have same signs, the lines don't overlap + return cross1 * cross2 < EPSILON; +} + +float Line2D::getOverlapRanges(Line2D& other, float& t0, float& t1, float& t2, float& t3) { + float d1 = dotProduct(start, dir); + float d2 = dotProduct(end, dir); + float d3 = dotProduct(other.start, dir); + float d4 = dotProduct(other.end, dir); + + bool flipOtherEdge = d4 < d3; + if (flipOtherEdge) { + float temp = d3; + d3 = d4; + d4 = temp; + } + + float overlapStart = std::max(d1, d3); + float overlapEnd = std::min(d2, d4); + float overlapDist = overlapEnd - overlapStart; + + if (overlapDist < 0.0f) { + return 0; + } + + float len1 = d2 - d1; + float len2 = d4 - d3; + + if (len1 == 0 || len2 == 0) { + print_log("{}: 0 length segments\n", __func__); + return 0; + } + + t0 = (overlapStart - d1) / len1; + t1 = (overlapEnd - d1) / len1; + + t2 = (overlapStart - d3) / len2; + t3 = (overlapEnd - d3) / len2; + if (flipOtherEdge) { + t2 = 1.0f - t2; + t3 = 1.0f - t3; + } + + return overlapDist; +} + +bool onSegment(const vec2& p, const vec2& q, const vec2& r) { + return (q.x <= std::max(p.x, r.x) && q.x >= std::min(p.x, r.x) && + q.y <= std::max(p.y, r.y) && q.y >= std::min(p.y, r.y)); +} + +int orientation(const vec2& p, const vec2& q, const vec2& r) { + float val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); + + if (val == 0.0) return 0; // Collinear + return (val > 0.0) ? 1 : 2; // Clockwise or counterclockwise +} + +bool Line2D::doesIntersect(const Line2D& l2) { + const vec2& A = start; + const vec2& B = end; + const vec2& C = l2.start - l2.dir * EPSILON; // extend a bit in case point is on edge + const vec2& D = l2.end + l2.dir * EPSILON; + + int o1 = orientation(A, B, C); + int o2 = orientation(A, B, D); + int o3 = orientation(C, D, A); + int o4 = orientation(C, D, B); + + if (o1 != o2 && o3 != o4) + return true; // They intersect + + if (o1 == 0 && onSegment(A, C, B)) return true; + if (o2 == 0 && onSegment(A, D, B)) return true; + if (o3 == 0 && onSegment(C, A, D)) return true; + if (o4 == 0 && onSegment(C, B, D)) return true; + + return false; // Doesn't intersect +} + +vec2 Line2D::intersect(const Line2D& l2) { + const vec2& A = start; + const vec2& B = end; + const vec2& C = l2.start; + const vec2& D = l2.end; + + float a1 = B.y - A.y; + float b1 = A.x - B.x; + float c1 = a1 * A.x + b1 * A.y; + + float a2 = D.y - C.y; + float b2 = C.x - D.x; + float c2 = a2 * C.x + b2 * C.y; + + float determinant = a1 * b2 - a2 * b1; + + if (determinant == 0.0) { + print_log("Line2D intersect:determinant is 0\n"); + return vec2(); + } + else { + float x = (b2 * c1 - b1 * c2) / determinant; + float y = (a1 * c2 - a2 * c1) / determinant; + return vec2(x, y); + } +} \ No newline at end of file diff --git a/src/util/Line2D.h b/src/util/Line2D.h new file mode 100644 index 00000000..28a052da --- /dev/null +++ b/src/util/Line2D.h @@ -0,0 +1,34 @@ +#pragma once +#include "vectors.h" + +struct Line2D { + vec2 start; + vec2 end; + vec2 dir; + + Line2D() {} + + Line2D(vec2 start, vec2 end); + + // distance between this point and the axis of this line + float distanceAxis(vec2 p); + + // distance between this point and line segment, accounting for points beyond the start/end + float distance(vec2 p); + + // projects a point onto the line segment + vec2 project(vec2 p); + + bool doesIntersect(const Line2D& l2); + + // call doesIntersect for line segments first, this returns the intersection point for infinite lines + vec2 intersect(const Line2D& l2); + + // returns true if the lines align on the same axis + bool isAlignedWith(const Line2D& other); + + // returns the the distance that the lines overlap, and sets range of overlap for each segment + // t0-t1 = 0-1 range for this segment + // t2-t2 = 0-1 range for other segment + float getOverlapRanges(Line2D& other, float& t0, float& t1, float& t2, float& t3); +}; diff --git a/src/util/Polygon3D.cpp b/src/util/Polygon3D.cpp new file mode 100644 index 00000000..52e4d4a1 --- /dev/null +++ b/src/util/Polygon3D.cpp @@ -0,0 +1,616 @@ +#include "Polygon3D.h" +#include "log.h" +#include "util.h" +#include "Renderer.h" +#include +#include + +#define COLINEAR_EPSILON 0.125f +#define SAME_VERT_EPSILON 0.125f +#define COLINEAR_CUT_EPSILON 0.25f // increase if cutter gets stuck in a loop cutting the same polys + +bool vec3Equal(vec3 v1, vec3 v2, float epsilon) +{ + vec3 v = v1 - v2; + if (fabs(v.x) >= epsilon) + return false; + if (fabs(v.y) >= epsilon) + return false; + if (fabs(v.z) >= epsilon) + return false; + return true; +} + +Polygon3D::Polygon3D(const std::vector& verts) { + this->verts = verts; + init(); +} + +Polygon3D::Polygon3D(const std::vector& verts, int idx) { + this->verts = verts; + this->idx = idx; + init(); +} + +size_t Polygon3D::sizeBytes() { + return sizeof(Polygon3D) + + sizeof(vec3) * verts.size() + + sizeof(vec2) * localVerts.size() + + sizeof(vec2) * topdownVerts.size(); +} + +void Polygon3D::init() { + std::vector triangularVerts = getTriangularVerts(this->verts); + localVerts.clear(); + topdownVerts.clear(); + isValid = false; + center = vec3(); + area = 0; + + localMins = vec2(FLT_MAX, FLT_MAX); + localMaxs = vec2(-FLT_MAX, -FLT_MAX); + + worldMins = vec3(FLT_MAX, FLT_MAX, FLT_MAX); + worldMaxs = vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); + + if (triangularVerts.empty()) + return; + + vec3 e1 = (triangularVerts[1] - triangularVerts[0]).normalize(); + vec3 e2 = (triangularVerts[2] - triangularVerts[0]).normalize(); + + plane_z = crossProduct(e1, e2).normalize(); + plane_x = e1; + plane_y = crossProduct(plane_z, plane_x).normalize(); + fdist = dotProduct(triangularVerts[0], plane_z); + + worldToLocal = worldToLocalTransform(plane_x, plane_y, plane_z); + localToWorld = worldToLocal.invert(); + + if (localToWorld.m[15] == 0) { + // failed matrix inversion + return; + } + + for (int e = 0; e < verts.size(); e++) { + vec2 localPoint = project(verts[e]); + localVerts.push_back(localPoint); + topdownVerts.push_back(vec2(verts[e].x, verts[e].y)); + expandBoundingBox(localPoint, localMins, localMaxs); + expandBoundingBox(verts[e], worldMins, worldMaxs); + center += verts[e]; + } + + for (int i = 0; i < localVerts.size(); i++) { + area += crossProduct(localVerts[i], localVerts[(i+1) % localVerts.size()]); + } + area = fabs(area) * 0.5f; + + center /= (float)verts.size(); + + vec3 vep(EPSILON, EPSILON, EPSILON); + worldMins -= vep; + worldMaxs += vep; + + isValid = true; +} + +vec2 Polygon3D::project(vec3 p) { + return (worldToLocal * vec4(p, 1)).xy(); +} + +vec3 Polygon3D::unproject(vec2 p) { + return (localToWorld * vec4(p.x, p.y, fdist, 1)).xyz(); +} + +float Polygon3D::distance(const vec3& p) { + return dotProduct(p - verts[0], plane_z); +} + +bool Polygon3D::isInside(vec3 p) { + if (fabs(distance(p)) > EPSILON) { + return false; + } + + return isInside(project(p)); +} + +float isLeft(const vec2& p1, const vec2& p2, const vec2& point) { + return crossProduct(p2 - p1, point - p1); +} + +// winding method +bool Polygon3D::isInside(vec2 p, bool includeEdge) { + int windingNumber = 0; + + for (int i = 0; i < localVerts.size(); i++) { + const vec2& p1 = localVerts[i]; + const vec2& p2 = localVerts[(i + 1) % localVerts.size()]; + + if (p1.y <= p.y) { + if (p2.y > p.y && isLeft(p1, p2, p) > 0) { + windingNumber += 1; + } + } + else if (p2.y <= p.y && isLeft(p1, p2, p) < 0) { + windingNumber -= 1; + } + + Line2D edge(p1, p2); + float dist = edge.distance(p); + + if (fabs(dist) < INPOLY_EPSILON) { + return includeEdge; // point is too close to an edge + } + } + + return windingNumber != 0; +} + +std::vector> Polygon3D::cut(Line2D cutLine) { + std::vector> splitPolys; + + bool intersectsAnyEdge = false; + if (isInside(cutLine.start) || isInside(cutLine.end)) { + intersectsAnyEdge = true; + } + + if (!intersectsAnyEdge) { + for (int i = 0; i < localVerts.size(); i++) { + vec2 e1 = localVerts[i]; + vec2 e2 = localVerts[(i + 1) % localVerts.size()]; + Line2D edge(e1, e2); + + if (edge.doesIntersect(cutLine)) { + intersectsAnyEdge = true; + break; + } + } + } + if (!intersectsAnyEdge) { + //print_log("No edge intersections\n"); + return splitPolys; + } + + // extend to "infinity" if we know the cutting edge is touching the poly somewhere + // a split should happen along that edge across the entire polygon + cutLine.start = cutLine.start - cutLine.dir * FLT_MAX_COORD; + cutLine.end = cutLine.end + cutLine.dir * FLT_MAX_COORD; + + for (int i = 0; i < localVerts.size(); i++) { + vec2 e1 = localVerts[i]; + vec2 e2 = localVerts[(i + 1) % localVerts.size()]; + + float dist1 = fabs(cutLine.distanceAxis(e1)); + float dist2 = fabs(cutLine.distanceAxis(e2)); + + if (dist1 < COLINEAR_CUT_EPSILON && dist2 < COLINEAR_CUT_EPSILON) { + //print_log("cut is colinear with an edge\n"); + return splitPolys; // line is colinear with an edge, no intersections possible + } + } + + + splitPolys.push_back(std::vector()); + splitPolys.push_back(std::vector()); + + + // get new verts with intersection points included + std::vector newVerts; + std::vector newLocalVerts; + + for (int i = 0; i < localVerts.size(); i++) { + int next = (i + 1) % localVerts.size(); + vec2 e1 = localVerts[i]; + vec2 e2 = localVerts[next]; + Line2D edge(e1, e2); + + newVerts.push_back(verts[i]); + newLocalVerts.push_back(e1); + + if (edge.doesIntersect(cutLine)) { + vec2 intersect = edge.intersect(cutLine); + vec3 worldPos = (localToWorld * vec4(intersect.x, intersect.y, fdist, 1)).xyz(); + + if (!vec3Equal(worldPos, verts[i], SAME_VERT_EPSILON) && !vec3Equal(worldPos, verts[next], SAME_VERT_EPSILON)) { + newVerts.push_back(worldPos); + newLocalVerts.push_back(intersect); + } + } + } + + // define new polys (separate by left/right of line + for (int i = 0; i < newLocalVerts.size(); i++) { + float dist = cutLine.distanceAxis(newLocalVerts[i]); + + if (dist < -SAME_VERT_EPSILON) { + splitPolys[0].push_back(newVerts[i]); + } + else if (dist > SAME_VERT_EPSILON) { + splitPolys[1].push_back(newVerts[i]); + } + else { + splitPolys[0].push_back(newVerts[i]); + splitPolys[1].push_back(newVerts[i]); + } + } + + g_app->debugCut = cutLine; + + if (splitPolys[0].size() < 3 || splitPolys[1].size() < 3) { + //print_log("Degenerate split!\n"); + return std::vector>(); + } + + return splitPolys; +} + +std::vector> Polygon3D::split(const Polygon3D& cutPoly) { + if (!boxesIntersect(worldMins, worldMaxs, cutPoly.worldMins, cutPoly.worldMaxs)) { + return std::vector>(); + } + + for (int i = 0; i < cutPoly.verts.size(); i++) { + const vec3& e1 = cutPoly.verts[i]; + const vec3& e2 = cutPoly.verts[(i + 1) % cutPoly.verts.size()]; + + if (fabs(distance(e1)) < EPSILON && fabs(distance(e2)) < EPSILON) { + //print_log("Edge {} is inside {} {}\n", i, distance(e1), distance(e2)); + g_app->debugLine0 = e1; + g_app->debugLine1 = e2; + return cut(Line2D(project(e1), project(e2))); + } + } + + return std::vector>(); +} + +bool Polygon3D::isConvex() { + size_t n = localVerts.size(); + if (n < 3) { + return false; + } + + int sign = 0; // Initialize the sign of the cross product + + for (int i = 0; i < n; i++) { + const vec2& A = localVerts[i]; + const vec2& B = localVerts[(i + 1) % n]; // Next vertex + const vec2& C = localVerts[(i + 2) % n]; // Vertex after the next + + // normalizing prevents small epsilons not working for large differences in edge lengths + vec2 AB = vec2(B.x - A.x, B.y - A.y).normalize(); + vec2 BC = vec2(C.x - B.x, C.y - B.y).normalize(); + + float current_cross_product = crossProduct(AB, BC); + + if (fabs(current_cross_product) < COLINEAR_EPSILON) { + continue; // Skip collinear points + } + + if (sign == 0) { + sign = (current_cross_product > 0) ? 1 : -1; + } + else { + if ((current_cross_product > 0 && sign == -1) || (current_cross_product < 0 && sign == 1)) { + return false; + } + } + } + + return true; +} + +void Polygon3D::removeDuplicateVerts(float epsilon) { + std::vector newVerts; + + size_t sz = verts.size(); + if (sz > 1) + { + for (int i = 0; i < sz; i++) { + size_t last = (i + (sz - 1)) % sz; + + if (!vec3Equal(verts[i], verts[last], epsilon)) + newVerts.push_back(verts[i]); + } + } + if (verts.size() != newVerts.size()) { + //print_log("Removed {} duplicate verts\n", verts.size() - newVerts.size()); + verts = newVerts; + init(); + } +} + +void Polygon3D::extendAlongAxis(float amt) { + for (int i = 0; i < verts.size(); i++) { + verts[i] += plane_z * amt; + } + + init(); +} + +void Polygon3D::removeColinearVerts() { + std::vector newVerts; + + if (verts.size() < 3) { + print_log("Not enough verts to remove colinear ones\n"); + return; + } + + size_t sz = localVerts.size(); + if (sz > 1) + { + for (int i = 0; i < sz; i++) { + const vec2& A = localVerts[(i + (sz - 1)) % sz]; + const vec2& B = localVerts[i]; + const vec2& C = localVerts[(i + 1) % sz]; + + vec2 AB = vec2(B.x - A.x, B.y - A.y).normalize(); + vec2 BC = vec2(C.x - B.x, C.y - B.y).normalize(); + float cross = crossProduct(AB, BC); + + if (fabs(cross) >= COLINEAR_EPSILON) { + newVerts.push_back(verts[i]); + } + } + } + + if (verts.size() != newVerts.size()) { + //print_log("Removed {} colinear verts\n", verts.size() - newVerts.size()); + verts = newVerts; + init(); + } +} + +Polygon3D Polygon3D::merge(const Polygon3D& mergePoly) { + std::vector mergedVerts; + + float epsilon = 1.0f; + + if (fabs(fdist - mergePoly.fdist) > epsilon || dotProduct(plane_z, mergePoly.plane_z) < 0.99f) + return mergedVerts; // faces not coplaner + + int sharedEdges = 0; + int commonEdgeStart1 = -1, commonEdgeEnd1 = -1; + int commonEdgeStart2 = -1, commonEdgeEnd2 = -1; + for (int i = 0; i < verts.size(); i++) { + const vec3& e1 = verts[i]; + const vec3& e2 = verts[(i + 1) % verts.size()]; + + for (int k = 0; k < mergePoly.verts.size(); k++) { + const vec3& other1 = mergePoly.verts[k]; + const vec3& other2 = mergePoly.verts[(k + 1) % mergePoly.verts.size()]; + + if ((vec3Equal(e1, other1, epsilon) && vec3Equal(e2, other2, epsilon)) + || (vec3Equal(e1, other2, epsilon) && vec3Equal(e2, other1, epsilon))) { + commonEdgeStart1 = i; + commonEdgeEnd1 = (i + 1) % verts.size(); + commonEdgeStart2 = k; + commonEdgeEnd2 = (k + 1) % mergePoly.verts.size(); + sharedEdges++; + } + } + } + + if (sharedEdges == 0) + return Polygon3D(); + if (sharedEdges > 1) { + //print_log("More than 1 shared edge for merge!\n"); + return Polygon3D(); + } + + mergedVerts.reserve(verts.size() + mergePoly.verts.size() - 2); + for (int i = commonEdgeEnd1; i != commonEdgeStart1; i = (i + 1) % verts.size()) { + mergedVerts.push_back(verts[i]); + } + for (int i = commonEdgeEnd2; i != commonEdgeStart2; i = (i + 1) % mergePoly.verts.size()) { + mergedVerts.push_back(mergePoly.verts[i]); + } + + Polygon3D newPoly(mergedVerts); + + if (!newPoly.isConvex()) { + return Polygon3D(); + } + newPoly.removeColinearVerts(); + + return newPoly; +} + +void push_unique_vert(std::vector& verts, vec2 vert) { + for (int k = 0; k < verts.size(); k++) { + if ((verts[k] - vert).length() < 0.125f) { + return; + } + } + + verts.push_back(vert); +} + + +namespace GrahamScan { + // https://www.tutorialspoint.com/cplusplus-program-to-implement-graham-scan-algorithm-to-find-the-convex-hull + vec2 p0; + + vec2 secondTop(std::stack& stk) { + vec2 tempvec2 = stk.top(); + stk.pop(); + vec2 res = stk.top(); //get the second top element + stk.push(tempvec2); //push previous top again + return res; + } + + int squaredDist(vec2 p1, vec2 p2) { + return ((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)); + } + + int direction(vec2 a, vec2 b, vec2 c) { + int val = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y); + if (val == 0) + return 0; //colinear + else if (val < 0) + return 2; //anti-clockwise direction + return 1; //clockwise direction + } + + int comp(const void* point1, const void* point2) { + vec2* p1 = (vec2*)point1; + vec2* p2 = (vec2*)point2; + int dir = direction(p0, *p1, *p2); + if (dir == 0) + return (squaredDist(p0, *p2) >= squaredDist(p0, *p1)) ? -1 : 1; + return (dir == 2) ? -1 : 1; + } + + std::vector findConvexHull(vec2 points[], int n) { + std::vector convexHullPoints; + int minY = points[0].y, min = 0; + + for (int i = 1; i < n; i++) { + int y = points[i].y; + //find bottom most or left most point + if ((y < minY) || (minY == y) && points[i].x < points[min].x) { + minY = points[i].y; + min = i; + } + } + + std::swap(points[0], points[min]); //swap min point to 0th location + p0 = points[0]; + qsort(&points[1], n - 1, sizeof(vec2), comp); //sort points from 1 place to end + + int arrSize = 1; //used to locate items in modified array + for (int i = 1; i < n; i++) { + //when the angle of ith and (i+1)th elements are same, remove points + while (i < n - 1 && direction(p0, points[i], points[i + 1]) == 0) + i++; + points[arrSize] = points[i]; + arrSize++; + } + + if (arrSize < 3) + return convexHullPoints; //there must be at least 3 points, return empty list. + //create a stack and add first three points in the stack + + std::stack stk; + stk.push(points[0]); stk.push(points[1]); stk.push(points[2]); + for (int i = 3; i < arrSize; i++) { //for remaining vertices + while (direction(secondTop(stk), stk.top(), points[i]) != 2) + stk.pop(); //when top, second top and ith point are not making left turn, remove point + stk.push(points[i]); + } + + while (!stk.empty()) { + convexHullPoints.push_back(stk.top()); //add points from stack + stk.pop(); + } + + return convexHullPoints; + } +}; + +Polygon3D Polygon3D::coplanerIntersectArea(Polygon3D otherPoly) { + std::vector outVerts; + + float epsilon = 1.0f; + + if (fabs(-fdist - otherPoly.fdist) > epsilon || dotProduct(plane_z, otherPoly.plane_z) > -0.99f) + return outVerts; // faces are not coplaner with opposite normals + + // project other polys verts onto the same coordinate system as this face + std::vector otherLocalVerts; + for (int i = 0; i < otherPoly.verts.size(); i++) { + otherLocalVerts.push_back(project(otherPoly.verts[i])); + } + otherPoly.localVerts = otherLocalVerts; + + std::vector localOutVerts; + + // find intersection points + for (int i = 0; i < localVerts.size(); i++) { + vec2& va1 = localVerts[i]; + vec2& va2 = localVerts[(i + 1) % localVerts.size()]; + Line2D edgeA(va1, va2); + + if (otherPoly.isInside(va1, true)) { + otherPoly.isInside(va1, true); + push_unique_vert(localOutVerts, va1); + } + + for (int k = 0; k < otherLocalVerts.size(); k++) { + vec2& vb1 = otherLocalVerts[k]; + vec2& vb2 = otherLocalVerts[(k + 1) % otherLocalVerts.size()]; + Line2D edgeB(vb1, vb2); + + if (!edgeA.isAlignedWith(edgeB) && edgeA.doesIntersect(edgeB)) { + push_unique_vert(localOutVerts, edgeA.intersect(edgeB)); + } + + if (isInside(vb1, true)) { + push_unique_vert(localOutVerts, vb1); + } + } + } + + if (localOutVerts.size() < 3) { + return outVerts; + } + + localOutVerts = GrahamScan::findConvexHull(&localOutVerts[0], (int) localOutVerts.size()); + + for (int i = 0; i < localOutVerts.size(); i++) { + outVerts.push_back(unproject(localOutVerts[i])); + } + + return outVerts; +} + +bool Polygon3D::intersects(Polygon3D& otherPoly) { + return false; +} + +bool Polygon3D::intersect(vec3 p1, vec3 p2, vec3& ipos) { + float t1 = dotProduct(plane_z, p1) - fdist; + float t2 = dotProduct(plane_z, p2) - fdist; + + if ((t1 >= 0.0f && t2 >= 0.0f) || (t1 < 0.0f && t2 < 0.0f)) { + return false; + } + + float frac = t1 / (t1 - t2); + frac = clamp(frac, 0.0f, 1.0f); + + if (frac != frac) { + return false; // NaN + } + + ipos = p1 + (p2 - p1) * frac; + + return isInside(project(ipos)); +} + +bool Polygon3D::intersect2D(vec3 p1, vec3 p2, vec3& ipos) { + vec2 p1_2d = project(p1); + vec2 p2_2d = project(p2); + + Line2D line(p1_2d, p2_2d); + + if (isInside(p1_2d, false) == isInside(p2_2d, false)) { + ipos = p1; + return false; + } + + for (int i = 0; i < localVerts.size(); i++) { + vec2 e1 = localVerts[i]; + vec2 e2 = localVerts[(i + 1) % localVerts.size()]; + Line2D edge(e1, e2); + + if (edge.doesIntersect(line)) { + ipos = unproject(edge.intersect(line)); + return true; + } + } + + ipos = p1; + return false; +} \ No newline at end of file diff --git a/src/util/Polygon3D.h b/src/util/Polygon3D.h new file mode 100644 index 00000000..0755bdf7 --- /dev/null +++ b/src/util/Polygon3D.h @@ -0,0 +1,103 @@ +#pragma once +#include "vectors.h" +#include +#include "mat4x4.h" +#include "Line2D.h" + +// points must be at least this far inside the polygon edges +// large enough to prevent PointContents returning empty on the edges of underground polys +// smaller than the starting offset for content checks +#define INPOLY_EPSILON (0.4f) + +// convex 3D polygon +class Polygon3D { +public: + bool isValid = false; + vec3 plane_x; + vec3 plane_y; + vec3 plane_z; // plane normal + float fdist = 0; + + std::vector verts; + std::vector localVerts; // points relative to the plane orientation + std::vector topdownVerts; // points without a z coordinate + + mat4x4 worldToLocal; + mat4x4 localToWorld; + + // extents of local coordinates + vec2 localMins; + vec2 localMaxs; + + // extents of world coordinates + vec3 worldMins; + vec3 worldMaxs; + + vec3 center; // average/centroid in world coordinates + + float area = 0; // area of the 2D polygon + + int idx = -1; // for octree lookup + + Polygon3D() {} + + Polygon3D(const std::vector& verts); + + Polygon3D(const std::vector& verts, int idx); + + void init(); + + size_t sizeBytes(); + + float distance(const vec3& p); + + bool isConvex(); + + void removeColinearVerts(); + void removeDuplicateVerts(float epsilon=0.125f); + + void extendAlongAxis(float amt); + + // returns split polys for first edge on cutPoly that contacts this polygon + // multiple intersections (overlapping polys) are not handled + // returns empty on no intersection + // only applies to edges that lie flat on the plane, not ones that pierce it + std::vector> split(const Polygon3D& cutPoly); + + // cut the polygon by an edge defined in this polygon's coordinate space + // returns empty if cutting not possible + // returns 2 new convex polygons otherwise + std::vector> cut(Line2D cutLine); + + // returns merged polygon vertices if polys are coplaner and share an edge + // otherwise returns an empty polygon + Polygon3D merge(const Polygon3D& mergePoly); + + // returns the area of intersection if polys are coplaner and overlap + // otherwise returns an empty polygon + Polygon3D coplanerIntersectArea(Polygon3D otherPoly); + + // returns true if the polygons intersect + bool intersects(Polygon3D& otherPoly); + + // if true, ipos is set to the intersection point with the given line segment + bool intersect(vec3 p1, vec3 p2, vec3& ipos); + + // if true, ipos is set to the projected intersection point of the given line segment + // the line segment is first projected onto the plane for a 2D intersect test + bool intersect2D(vec3 p1, vec3 p2, vec3& ipos); + + // is point inside this polygon? Coordinates are in world space. + // Points within EPSILON of an edge are not inside. + bool isInside(vec3 p); + + // is point inside this polygon? coordinates are in polygon's local space. + // Points within EPSILON of an edge are not inside. + bool isInside(vec2 p, bool includeEdge=false); + + // project a 3d point onto this polygon's local coordinate system + vec2 project(vec3 p); + + // get the world position of a point in the polygon's local coordinate system + vec3 unproject(vec2 p); +}; \ No newline at end of file diff --git a/src/util/util.cpp b/src/util/util.cpp index 85aa68d2..95055667 100644 --- a/src/util/util.cpp +++ b/src/util/util.cpp @@ -799,6 +799,23 @@ std::vector getTriangularVerts(std::vector& verts) return { verts[i0], verts[i1], verts[i2] }; } +bool boxesIntersect(const vec3& mins1, const vec3& maxs1, const vec3& mins2, const vec3& maxs2) { + return (maxs1.x >= mins2.x && mins1.x <= maxs2.x) && + (maxs1.y >= mins2.y && mins1.y <= maxs2.y) && + (maxs1.z >= mins2.z && mins1.z <= maxs2.z); +} + +bool pointInBox(const vec3& p, const vec3& mins, const vec3& maxs) { + return (p.x >= mins.x && p.x <= maxs.x && + p.y >= mins.y && p.y <= maxs.y && + p.z >= mins.z && p.z <= maxs.z); +} + +bool isBoxContained(const vec3& innerMins, const vec3& innerMaxs, const vec3& outerMins, const vec3& outerMaxs) { + return (innerMins.x >= outerMins.x && innerMins.y >= outerMins.y && innerMins.z >= outerMins.z && + innerMaxs.x <= outerMaxs.x && innerMaxs.y <= outerMaxs.y && innerMaxs.z <= outerMaxs.z); +} + vec3 getNormalFromVerts(std::vector& verts) { std::vector triangularVerts = getTriangularVerts(verts); diff --git a/src/util/util.h b/src/util/util.h index 7af38329..a2e856b1 100644 --- a/src/util/util.h +++ b/src/util/util.h @@ -115,6 +115,13 @@ std::vector getPlaneIntersectVerts(const std::vector& planes); bool vertsAllOnOneSide(std::vector& verts, BSPPLANE& plane); +bool boxesIntersect(const vec3& mins1, const vec3& maxs1, const vec3& mins2, const vec3& maxs2); + +bool pointInBox(const vec3& p, const vec3& mins, const vec3& maxs); + +bool isBoxContained(const vec3& innerMins, const vec3& innerMaxs, const vec3& outerMins, const vec3& outerMaxs); + + // get verts from the given set that form a triangle (no duplicates and not colinear) std::vector getTriangularVerts(std::vector& verts); diff --git a/src/util/vectors.cpp b/src/util/vectors.cpp index f9b85401..8302de2f 100644 --- a/src/util/vectors.cpp +++ b/src/util/vectors.cpp @@ -145,6 +145,18 @@ void vec3::operator/=(float f) y /= f; z /= f; } +vec3 crossProduct( vec3 v1, vec3 v2 ) +{ + float x = v1.y*v2.z - v2.y*v1.z; + float y = v2.x*v1.z - v1.x*v2.z; + float z = v1.x*v2.y - v1.y*v2.x; + return vec3(x, y, z); +} + +float crossProduct(vec2 v1, vec2 v2) +{ + return (v1.x * v2.y) - (v1.y * v2.x); +} vec3 crossProduct(const vec3& v1, const vec3& v2) { @@ -159,6 +171,10 @@ float dotProduct(const vec3& v1, const vec3& v2) return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } +float dotProduct(vec2 v1, vec2 v2) { + return v1.x * v2.x + v1.y * v2.y; +} + float distanceToPlane(const vec3& point, const vec3& planeNormal, float planeDist) { return std::abs(planeNormal.dot(point) - planeDist); } @@ -269,6 +285,11 @@ float vec3::length() const return sqrt((x * x) + (y * y) + (z * z)); } +float vec3::lengthSquared() const +{ + return (x * x) + (y * y) + (z * z); +} + bool vec3::IsZero() const { return (std::abs(x) + std::abs(y) + std::abs(z)) < EPSILON; @@ -480,6 +501,8 @@ vec2 vec2::swap() return vec2(y, x); } + + bool operator==(const vec4& v1, const vec4& v2) { vec4 v = v1 - v2; diff --git a/src/util/vectors.h b/src/util/vectors.h index 5215c0de..83151566 100644 --- a/src/util/vectors.h +++ b/src/util/vectors.h @@ -48,7 +48,6 @@ struct COLOR4 } }; - struct vec3 { float x, y, z; @@ -99,6 +98,7 @@ struct vec3 float sizeXY_test(); vec3 abs(); float length() const; + float lengthSquared() const; bool IsZero() const; vec3 invert(); std::string toKeyvalueString(bool truncate = false, const std::string& suffix_x = " ", const std::string& suffix_y = " ", const std::string& suffix_z = ""); @@ -278,6 +278,11 @@ vec2 operator/(vec2 v, float f); bool operator==(const vec2& v1, const vec2& v2); bool operator!=(const vec2& v1, const vec2& v2); + +float dotProduct(vec2 v1, vec2 v2); +float crossProduct(vec2 v1, vec2 v2); + + struct vec4 { float x, y, z, w; diff --git a/vs-project/bspguy.vcxproj b/vs-project/bspguy.vcxproj index c0de2820..d57f7da3 100644 --- a/vs-project/bspguy.vcxproj +++ b/vs-project/bspguy.vcxproj @@ -172,7 +172,7 @@ - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) $(IntDir) EnableFastChecks ProgramDatabase @@ -206,10 +206,10 @@ %(PreprocessorDefinitions);WIN32;_DEBUG;_WINDOWS;GLEW_STATIC; - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) $(ProjectDir)/$(IntDir) %(Filename).h %(Filename).tlb @@ -252,7 +252,7 @@ del "$(TargetDir)bspguy.cfg_bak" - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) $(IntDir) EnableFastChecks ProgramDatabase @@ -286,10 +286,10 @@ del "$(TargetDir)bspguy.cfg_bak" %(PreprocessorDefinitions);WIN32;_DEBUG;_WINDOWS;GLEW_STATIC; - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) $(ProjectDir)/$(IntDir) %(Filename).h %(Filename).tlb @@ -332,7 +332,7 @@ del "$(TargetDir)bspguy.cfg_bak" - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) $(IntDir) EnableFastChecks EditAndContinue @@ -364,10 +364,10 @@ del "$(TargetDir)bspguy.cfg_bak" %(PreprocessorDefinitions);WIN32;_DEBUG;_WINDOWS;GLEW_STATIC; - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) $(ProjectDir)/$(IntDir) %(Filename).h %(Filename).tlb @@ -401,7 +401,7 @@ del "$(TargetDir)bspguy.cfg_bak" - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) $(IntDir) EnableFastChecks EditAndContinue @@ -433,10 +433,10 @@ del "$(TargetDir)bspguy.cfg_bak" %(PreprocessorDefinitions);WIN32;_DEBUG;_WINDOWS;GLEW_STATIC; - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) $(ProjectDir)/$(IntDir) %(Filename).h %(Filename).tlb @@ -470,7 +470,7 @@ del "$(TargetDir)bspguy.cfg_bak" - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) $(IntDir) @@ -508,10 +508,10 @@ del "$(TargetDir)bspguy.cfg_bak" %(PreprocessorDefinitions);WIN32;_WINDOWS;NDEBUG;GLEW_STATIC; - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) $(ProjectDir)/$(IntDir) %(Filename).h %(Filename).tlb @@ -553,7 +553,7 @@ del "$(TargetDir)bspguy.cfg_bak" - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) $(IntDir) @@ -592,10 +592,10 @@ del "$(TargetDir)bspguy.cfg_bak" %(PreprocessorDefinitions);WIN32;_WINDOWS;NDEBUG;GLEW_STATIC; - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) $(ProjectDir)/$(IntDir) %(Filename).h %(Filename).tlb @@ -637,7 +637,7 @@ del "$(TargetDir)bspguy.cfg_bak" - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) $(IntDir) @@ -678,10 +678,10 @@ del "$(TargetDir)bspguy.cfg_bak" %(PreprocessorDefinitions);WIN32;_WINDOWS;NDEBUG;GLEW_STATIC; - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) $(ProjectDir)/$(IntDir) %(Filename).h %(Filename).tlb @@ -726,7 +726,7 @@ del "$(TargetDir)bspguy.cfg_bak" - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\nav;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;.\..\fmt\include;%(AdditionalIncludeDirectories) $(IntDir) @@ -767,10 +767,10 @@ del "$(TargetDir)bspguy.cfg_bak" %(PreprocessorDefinitions);WIN32;_WINDOWS;NDEBUG;GLEW_STATIC; - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\src\nav;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) - .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) + .\..\src;.\..\src\bsp;.\..\src\res;.\..\src\cli;.\..\src\data;.\..\src\editor;.\..\src\filedialog;.\..\src\gl;.\..\src\qtools;.\..\src\util;.\..\src\mdl;.\..\src\nav;.\..\imgui;.\..\imgui\examples;.\..\imgui\backends;.\..\imgui\misc\cpp;.\..\glew\include;.\..\glfw\include;%(AdditionalIncludeDirectories) $(ProjectDir)/$(IntDir) %(Filename).h %(Filename).tlb @@ -856,6 +856,22 @@ del "$(TargetDir)bspguy.cfg_bak" + + + + + + + + + + + + + + + + diff --git a/vs-project/bspguy.vcxproj.filters b/vs-project/bspguy.vcxproj.filters index da44d7c8..f6b2fe36 100644 --- a/vs-project/bspguy.vcxproj.filters +++ b/vs-project/bspguy.vcxproj.filters @@ -145,6 +145,48 @@ Source Files\util + + Header Files\nav + + + Header Files\nav + + + Header Files\nav + + + Header Files\nav + + + Header Files\nav + + + Header Files\nav + + + Source Files\nav + + + Source Files\nav + + + Source Files\nav + + + Source Files\nav + + + Source Files\nav + + + Source Files\nav + + + Source Files\util + + + Source Files\util + @@ -279,6 +321,12 @@ Header Files\util + + Header Files\util + + + Header Files\util + @@ -350,6 +398,12 @@ {523fefee-7d40-4552-ae1a-f27341a6fd76} + + {20c145d4-0591-44cc-88cd-a1dc0f0d64e1} + + + {a03f7762-92c3-48be-a7ca-269eda99d76e} +