From 22d59efff4a6a6b23ec0d0a0f2a33a8aa421ddbc Mon Sep 17 00:00:00 2001 From: wootguy Date: Tue, 30 Apr 2024 02:38:07 -0700 Subject: [PATCH] add tool to delete BSP data inside a bounding box works like the delete OOB data option except you define the region you want to delete with "cull" entities (new option for that in the Create menu). Also added the undo ability for most of the porting tools. --- src/bsp/Bsp.cpp | 323 ++++++++++++++++++++++++++++++++++++++++ src/bsp/Bsp.h | 7 + src/editor/Command.cpp | 275 ++++++++++++++++++++++++++++++++++ src/editor/Command.h | 71 +++++++++ src/editor/Gui.cpp | 163 ++++++++++---------- src/editor/Renderer.cpp | 54 ++++++- src/editor/Renderer.h | 12 ++ src/util/util.cpp | 6 + src/util/util.h | 2 + 9 files changed, 834 insertions(+), 79 deletions(-) diff --git a/src/bsp/Bsp.cpp b/src/bsp/Bsp.cpp index fba1c9a2..dba6a28a 100644 --- a/src/bsp/Bsp.cpp +++ b/src/bsp/Bsp.cpp @@ -2123,6 +2123,329 @@ void Bsp::delete_oob_data(int clipFlags) { remove_unused_model_structures().print_delete_stats(1); } + +void Bsp::delete_box_nodes(int iNode, int16_t* parentBranch, vector& clipOrder, + vec3 clipMins, vec3 clipMaxs, bool* oobHistory, bool isFirstPass, int& removedNodes) { + BSPNODE& node = nodes[iNode]; + float oob_coord = g_limits.max_mapboundary; + + 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) { + vector cuts; + for (int k = 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, int16_t* parentBranch, vector& clipOrder, + vec3 clipMins, vec3 clipMaxs, bool* oobHistory, bool isFirstPass, int& removedNodes) { + BSPCLIPNODE& node = clipnodes[iNode]; + float oob_coord = g_limits.max_mapboundary; + + 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) { + vector cuts; + for (int k = 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 + { + 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 + int 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; + } + + vector newEnts; + newEnts.push_back(ents[0]); // never remove worldspawn + + for (int i = 1; i < ents.size(); i++) { + vec3 v = ents[i]->getOrigin(); + int modelIdx = ents[i]->getBspModelIdx(); + + if (modelIdx != -1) { + BSPMODEL& model = models[modelIdx]; + + 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 = ents.size() - newEnts.size(); + if (deletedEnts) + logf(" Deleted %d 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++) { + BSPFACE& face = faces[worldmodel.iFirstFace + i]; + + bool isClipped = false; + for (int e = 0; e < face.nEdges; e++) { + int32_t edgeIdx = surfedges[face.iFirstEdge + e]; + BSPEDGE& 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++; + } + } + + BSPFACE* newFaces = new BSPFACE[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++) { + BSPNODE& node = nodes[i]; + + int offset = 0; + int countReduce = 0; + + for (int k = 0; k < node.firstFace; k++) { + offset += oobFaces[k]; + } + for (int k = 0; k < node.nFaces; k++) { + countReduce += oobFaces[node.firstFace + k]; + } + + node.firstFace -= offset; + node.nFaces -= countReduce; + } + + for (int i = 0; i < leafCount; i++) { + BSPLEAF& 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) { + //logf("leaf %d partially OOB\n", i); + } + } + else { + for (int k = 0; k < leaf.nMarkSurfaces; k++) { + uint16_t 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(BSPFACE)); + + 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& leafCount) { BSPNODE& node = nodes[iNode]; diff --git a/src/bsp/Bsp.h b/src/bsp/Bsp.h index f1c67ef8..5f9e96b7 100644 --- a/src/bsp/Bsp.h +++ b/src/bsp/Bsp.h @@ -182,6 +182,13 @@ class Bsp void delete_oob_nodes(int iNode, int16_t* parentBranch, 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, int16_t* parentBranch, vector& clipOrder, + vec3 clipMins, vec3 clipMaxs, bool* oobHistory, bool isFirstPass, int& removedNodes); + void delete_box_nodes(int iNode, int16_t* parentBranch, 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); diff --git a/src/editor/Command.cpp b/src/editor/Command.cpp index 836cf564..df68b6c3 100644 --- a/src/editor/Command.cpp +++ b/src/editor/Command.cpp @@ -140,6 +140,7 @@ void DeleteEntityCommand::refresh() { BspRenderer* renderer = getBspRenderer(); renderer->preRenderEnts(); g_app->gui->refresh(); + g_app->updateCullBox(); } int DeleteEntityCommand::memoryUsage() { @@ -186,6 +187,7 @@ void CreateEntityCommand::refresh() { BspRenderer* renderer = getBspRenderer(); renderer->preRenderEnts(); g_app->gui->refresh(); + g_app->updateCullBox(); } int CreateEntityCommand::memoryUsage() { @@ -690,5 +692,278 @@ int OptimizeMapCommand::memoryUsage() { size += oldLumps.lumpLen[i]; } + return size; +} + + + +// +// Delete boxed data +// +DeleteBoxedDataCommand::DeleteBoxedDataCommand(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() { + for (int i = 0; i < HEADER_LUMPS; i++) { + if (oldLumps.lumps[i]) + delete[] oldLumps.lumps[i]; + } +} + +void DeleteBoxedDataCommand::execute() { + Bsp* map = getBsp(); + + map->delete_box_data(mins, maxs); + + refresh(); +} + +void DeleteBoxedDataCommand::undo() { + Bsp* map = getBsp(); + + map->replace_lumps(oldLumps); + + refresh(); +} + +void DeleteBoxedDataCommand::refresh() { + Bsp* map = getBsp(); + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); + g_app->saveLumpState(map, 0xffffffff, true); +} + +int DeleteBoxedDataCommand::memoryUsage() { + int size = sizeof(DeleteBoxedDataCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumpLen[i]; + } + + return size; +} + + + +// +// Delete OOB data +// +DeleteOobDataCommand::DeleteOobDataCommand(string desc, int mapIdx, int clipFlags, LumpState oldLumps) : Command(desc, mapIdx) { + this->oldLumps = oldLumps; + this->allowedDuringLoad = false; + this->clipFlags = clipFlags; +} + +DeleteOobDataCommand::~DeleteOobDataCommand() { + for (int i = 0; i < HEADER_LUMPS; i++) { + if (oldLumps.lumps[i]) + delete[] oldLumps.lumps[i]; + } +} + +void DeleteOobDataCommand::execute() { + Bsp* map = getBsp(); + + map->delete_oob_data(clipFlags); + + refresh(); +} + +void DeleteOobDataCommand::undo() { + Bsp* map = getBsp(); + + map->replace_lumps(oldLumps); + + refresh(); +} + +void DeleteOobDataCommand::refresh() { + Bsp* map = getBsp(); + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); + g_app->saveLumpState(map, 0xffffffff, true); +} + +int DeleteOobDataCommand::memoryUsage() { + int size = sizeof(DeleteOobDataCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumpLen[i]; + } + + return size; +} + + +// +// Fix bad surface extents +// +FixSurfaceExtentsCommand::FixSurfaceExtentsCommand(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() { + for (int i = 0; i < HEADER_LUMPS; i++) { + if (oldLumps.lumps[i]) + delete[] oldLumps.lumps[i]; + } +} + +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() { + Bsp* map = getBsp(); + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); + g_app->saveLumpState(map, 0xffffffff, true); +} + +int FixSurfaceExtentsCommand::memoryUsage() { + int size = sizeof(FixSurfaceExtentsCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumpLen[i]; + } + + return size; +} + + + +// +// Deduplicate models +// +DeduplicateModelsCommand::DeduplicateModelsCommand(string desc, int mapIdx, LumpState oldLumps) : Command(desc, mapIdx) { + this->oldLumps = oldLumps; + this->allowedDuringLoad = false; +} + +DeduplicateModelsCommand::~DeduplicateModelsCommand() { + for (int i = 0; i < HEADER_LUMPS; i++) { + if (oldLumps.lumps[i]) + delete[] oldLumps.lumps[i]; + } +} + +void DeduplicateModelsCommand::execute() { + Bsp* map = getBsp(); + + map->deduplicate_models(); + + refresh(); +} + +void DeduplicateModelsCommand::undo() { + Bsp* map = getBsp(); + + map->replace_lumps(oldLumps); + + refresh(); +} + +void DeduplicateModelsCommand::refresh() { + Bsp* map = getBsp(); + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); + g_app->saveLumpState(map, 0xffffffff, true); +} + +int DeduplicateModelsCommand::memoryUsage() { + int size = sizeof(DeduplicateModelsCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumpLen[i]; + } + + return size; +} + + +// +// Move the entire map +// +MoveMapCommand::MoveMapCommand(string desc, int mapIdx, vec3 offset, LumpState oldLumps) : Command(desc, mapIdx) { + this->oldLumps = oldLumps; + this->allowedDuringLoad = false; + this->offset = offset; +} + +MoveMapCommand::~MoveMapCommand() { + for (int i = 0; i < HEADER_LUMPS; i++) { + if (oldLumps.lumps[i]) + delete[] oldLumps.lumps[i]; + } +} + +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() { + Bsp* map = getBsp(); + BspRenderer* renderer = getBspRenderer(); + + renderer->reload(); + g_app->deselectObject(); + g_app->gui->refresh(); + g_app->saveLumpState(map, 0xffffffff, true); +} + +int MoveMapCommand::memoryUsage() { + int size = sizeof(MoveMapCommand); + + for (int i = 0; i < HEADER_LUMPS; i++) { + size += oldLumps.lumpLen[i]; + } + return size; } \ No newline at end of file diff --git a/src/editor/Command.h b/src/editor/Command.h index 3862b380..8cf23fc3 100644 --- a/src/editor/Command.h +++ b/src/editor/Command.h @@ -163,3 +163,74 @@ class OptimizeMapCommand : public Command { void refresh(); int memoryUsage(); }; + +class DeleteBoxedDataCommand : public Command { +public: + LumpState oldLumps = LumpState(); + vec3 mins, maxs; + + DeleteBoxedDataCommand(string desc, int mapIdx, vec3 mins, vec3 maxs, LumpState oldLumps); + ~DeleteBoxedDataCommand(); + + void execute(); + void undo(); + void refresh(); + int memoryUsage(); +}; + +class DeleteOobDataCommand : public Command { +public: + LumpState oldLumps = LumpState(); + int clipFlags; + + DeleteOobDataCommand(string desc, int mapIdx, int clipFlags, LumpState oldLumps); + ~DeleteOobDataCommand(); + + void execute(); + void undo(); + void refresh(); + int memoryUsage(); +}; + +class FixSurfaceExtentsCommand : public Command { +public: + LumpState oldLumps = LumpState(); + bool scaleNotSubdivide; + bool downscaleOnly; + int maxTextureDim; + + FixSurfaceExtentsCommand(string desc, int mapIdx, bool scaleNotSubdivide, bool downscaleOnly, int maxTextureDim, LumpState oldLumps); + ~FixSurfaceExtentsCommand(); + + void execute(); + void undo(); + void refresh(); + int memoryUsage(); +}; + +class DeduplicateModelsCommand : public Command { +public: + LumpState oldLumps = LumpState(); + + DeduplicateModelsCommand(string desc, int mapIdx, LumpState oldLumps); + ~DeduplicateModelsCommand(); + + void execute(); + void undo(); + void refresh(); + int memoryUsage(); +}; + +class MoveMapCommand : public Command { +public: + LumpState oldLumps = LumpState(); + vec3 offset; + + MoveMapCommand(string desc, int mapIdx, vec3 offset, LumpState oldLumps); + ~MoveMapCommand(); + + void execute(); + void undo(); + void refresh(); + int memoryUsage(); +}; diff --git a/src/editor/Gui.cpp b/src/editor/Gui.cpp index 76e5930b..b53b23f9 100644 --- a/src/editor/Gui.cpp +++ b/src/editor/Gui.cpp @@ -1156,17 +1156,11 @@ void Gui::drawMenuBar() { if (ImGui::MenuItem("Apply Worldspawn Transform", 0, false, !app->isLoading && mapSelected)) { if (map->ents[0]->hasKey("origin")) { - vec3 ori = map->ents[0]->getOrigin(); - logf("Moved worldspawn origin by %f %f %f\n", ori.x, ori.y, ori.z); - map->move(ori); - map->ents[0]->removeKeyvalue("origin"); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - renderer->reload(); - g_app->gui->refresh(); - g_app->deselectObject(); - } + MoveMapCommand* command = new MoveMapCommand("Apply Worldspawn Transform", + app->pickInfo.mapIdx, map->ents[0]->getOrigin(), app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } else { logf("Transform the worldspawn origin first using the transform widget!\n"); @@ -1212,32 +1206,49 @@ void Gui::drawMenuBar() { } - map->delete_oob_data(clipFlags[i]); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - renderer->reload(); - g_app->gui->refresh(); - g_app->deselectObject(); - } + DeleteOobDataCommand* command = new DeleteOobDataCommand("Delete OOB Data", + app->pickInfo.mapIdx, clipFlags[i], app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } 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("De-duplicate Models", 0, false, !app->isLoading && mapSelected)) { - map->deduplicate_models(); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - renderer->preRenderEnts(); - g_app->gui->refresh(); + if (ImGui::MenuItem("Delete Boxed Data", 0, false, !app->isLoading && mapSelected)) { + if (!g_app->hasCullbox) { + logf("Create at least 2 entities with \"cull\" as a classname first!\n"); + } + else { + DeleteBoxedDataCommand* command = new DeleteBoxedDataCommand("Delete Boxed Data", + app->pickInfo.mapIdx, g_app->cullMins, g_app->cullMaxs, app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } + + } + 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 && mapSelected)) { + DeduplicateModelsCommand* command = new DeduplicateModelsCommand("Deduplicate models", + app->pickInfo.mapIdx, app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } - tooltip(g, "Deletes duplicated BSP models and updates entity model keyvalues accordingly. This lowers the model count and allows more game models to be precached."); + 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 && mapSelected)) { map->downscale_invalid_textures(); @@ -1253,56 +1264,44 @@ void Gui::drawMenuBar() { if (ImGui::BeginMenu("Fix Bad Surface Extents", !app->isLoading && mapSelected)) { if (ImGui::MenuItem("Shrink Textures (512)", 0, false, !app->isLoading && mapSelected)) { - map->fix_bad_surface_extents(false, true, 512); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - map->remove_unused_model_structures(); - renderer->reload(); - reloadLimits(); - } + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Shrink textures (512)", + app->pickInfo.mapIdx, false, true, 512, app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } 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 && mapSelected)) { - map->fix_bad_surface_extents(false, true, 256); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - map->remove_unused_model_structures(); - renderer->reload(); - reloadLimits(); - } + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Shrink textures (256)", + app->pickInfo.mapIdx, false, true, 256, app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } 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 && mapSelected)) { - map->fix_bad_surface_extents(false, true, 128); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - map->remove_unused_model_structures(); - renderer->reload(); - reloadLimits(); - } + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Shrink textures (128)", + app->pickInfo.mapIdx, false, true, 128, app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } 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 && mapSelected)) { - map->fix_bad_surface_extents(false, true, 64); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - map->remove_unused_model_structures(); - renderer->reload(); - reloadLimits(); - } + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Shrink textures (64)", + app->pickInfo.mapIdx, false, true, 64, app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } 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." @@ -1311,26 +1310,20 @@ void Gui::drawMenuBar() { ImGui::Separator(); if (ImGui::MenuItem("Scale", 0, false, !app->isLoading && mapSelected)) { - map->fix_bad_surface_extents(true, false, 0); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - map->remove_unused_model_structures(); - renderer->reload(); - reloadLimits(); - } + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Scale faces", + app->pickInfo.mapIdx, true, false, 0, app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } 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 && mapSelected)) { - map->fix_bad_surface_extents(false, false, 0); - - BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (renderer) { - map->remove_unused_model_structures(); - renderer->reload(); - reloadLimits(); - } + FixSurfaceExtentsCommand* command = new FixSurfaceExtentsCommand("Subdivide faces", + app->pickInfo.mapIdx, false, false, 0, app->undoLumpState); + g_app->saveLumpState(map, 0xffffffff, false); + command->execute(); + app->pushUndoCommand(command); } tooltip(g, "Subdivides faces until they have valid extents. The drawback to this method is reduced in-game performace from higher poly counts."); @@ -1395,7 +1388,7 @@ void Gui::drawMenuBar() { Bsp* map = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx]->map : NULL; BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; - if (ImGui::MenuItem("Entity", 0, false, mapSelected)) { + if (ImGui::MenuItem("Point Entity", 0, false, mapSelected)) { Entity* newEnt = new Entity(); vec3 origin = (app->cameraOrigin + app->cameraForward * 100); if (app->gridSnappingEnabled) @@ -1408,6 +1401,7 @@ void Gui::drawMenuBar() { createCommand->execute(); app->pushUndoCommand(createCommand); } + tooltip(g, "Create a point entity. This is a ripent-only operation which does not affect BSP structures.\n"); if (ImGui::MenuItem("BSP Model", 0, false, !app->isLoading && mapSelected)) { vec3 origin = app->cameraOrigin + app->cameraForward * 100; @@ -1428,6 +1422,23 @@ void Gui::drawMenuBar() { delete newEnt; app->pushUndoCommand(command); } + tooltip(g, "Create a BSP model and attach it to a new entity. This is not a ripent-only operation and will create new BSP structures.\n"); + + if (ImGui::MenuItem("Cull Entity", 0, false, mapSelected)) { + Entity* newEnt = new Entity(); + vec3 origin = (app->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->pickInfo.mapIdx, newEnt); + delete newEnt; + createCommand->execute(); + app->pushUndoCommand(createCommand); + } + 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(); } diff --git a/src/editor/Renderer.cpp b/src/editor/Renderer.cpp index b6656146..0cc85a07 100644 --- a/src/editor/Renderer.cpp +++ b/src/editor/Renderer.cpp @@ -301,7 +301,7 @@ void Renderer::renderLoop() { glEnable(GL_CULL_FACE); } - if (g_render_flags & (RENDER_ORIGIN | RENDER_MAP_BOUNDARY)) { + if ((g_render_flags & (RENDER_ORIGIN | RENDER_MAP_BOUNDARY)) || hasCullbox) { colorShader->bind(); model.loadIdentity(); colorShader->pushMatrix(MAT_MODEL); @@ -310,6 +310,7 @@ void Renderer::renderLoop() { model.translate(offset.x, offset.y, offset.z); } colorShader->updateMatrixes(); + glDisable(GL_CULL_FACE); if (g_render_flags & RENDER_ORIGIN) { drawLine(debugPoint - vec3(32, 0, 0), debugPoint + vec3(32, 0, 0), { 128, 128, 255, 255 }); @@ -318,11 +319,14 @@ void Renderer::renderLoop() { } if (g_render_flags & RENDER_MAP_BOUNDARY) { - glDisable(GL_CULL_FACE); drawBox(mapRenderers[0]->map->ents[0]->getOrigin() * -1, g_limits.max_mapboundary * 2, COLOR4(0, 255, 0, 64)); - glEnable(GL_CULL_FACE); } + if (hasCullbox) { + drawBox(cullMins, cullMaxs, COLOR4(255, 0, 0, 64)); + } + + glEnable(GL_CULL_FACE); colorShader->popMatrix(MAT_MODEL); } } @@ -564,6 +568,8 @@ void Renderer::reloadMaps() { copiedEnt = NULL; } + updateCullBox(); + logf("Reloaded maps\n"); } @@ -589,6 +595,8 @@ void Renderer::openMap(const char* fpath) { clearRedoCommands(); gui->refresh(); + updateCullBox(); + logf("Loaded map: %s\n", fpath); } @@ -1634,6 +1642,9 @@ void Renderer::addMap(Bsp* map) { } */ } + + updateCullBox(); + saveLumpState(map, 0xffffffff, false); // set up initial undo state } void Renderer::drawLine(vec3 start, vec3 end, COLOR4 color) { @@ -1680,6 +1691,16 @@ void Renderer::drawBox(vec3 center, float width, COLOR4 color) { buffer.draw(GL_TRIANGLES); } +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(colorShader, COLOR_4B | POS_3F, &cube, 6 * 6); + buffer.draw(GL_TRIANGLES); +} + float Renderer::drawPolygon2D(Polygon3D poly, vec2 pos, vec2 maxSz, COLOR4 color) { vec2 sz = poly.localMaxs - poly.localMins; float scale = min(maxSz.y / sz.y, maxSz.x / sz.x); @@ -2180,6 +2201,8 @@ void Renderer::updateEntConnections() { entConnections->ownData = true; entConnectionPoints->ownData = true; } + + updateCullBox(); } void Renderer::updateEntConnectionPositions() { @@ -2193,6 +2216,30 @@ void Renderer::updateEntConnectionPositions() { verts[i].z = pos.z; } } + + 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->getOrigin(), cullMins, cullMaxs); + findCount++; + } + } + + hasCullbox = findCount > 1; } bool Renderer::getModelSolid(vector& hullVerts, Bsp* map, Solid& outSolid) { @@ -3123,6 +3170,7 @@ void Renderer::merge(string fpath) { clearUndoCommands(); clearRedoCommands(); gui->refresh(); + updateCullBox(); logf("Merged maps!\n"); } \ No newline at end of file diff --git a/src/editor/Renderer.h b/src/editor/Renderer.h index 97ab16e7..c113382e 100644 --- a/src/editor/Renderer.h +++ b/src/editor/Renderer.h @@ -56,6 +56,11 @@ class Renderer { friend class EditBspModelCommand; friend class CleanMapCommand; friend class OptimizeMapCommand; + friend class DeleteBoxedDataCommand; + friend class DeleteOobDataCommand; + friend class FixSurfaceExtentsCommand; + friend class DeduplicateModelsCommand; + friend class MoveMapCommand; public: vector mapRenderers; @@ -213,6 +218,10 @@ class Renderer { LumpState undoLumpState = LumpState(); vec3 undoEntOrigin; + bool hasCullbox; + vec3 cullMins; + vec3 cullMaxs; + vec3 getMoveDir(); void controls(); void cameraPickingControls(); @@ -237,6 +246,7 @@ class Renderer { 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); 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); @@ -257,6 +267,8 @@ class Renderer { void moveSelectedVerts(vec3 delta); void splitFace(); + void updateCullBox(); + vec3 snapToGrid(vec3 pos); void grabEnt(); diff --git a/src/util/util.cpp b/src/util/util.cpp index 6d165371..4337f914 100644 --- a/src/util/util.cpp +++ b/src/util/util.cpp @@ -510,6 +510,12 @@ bool boxesIntersect(const vec3& mins1, const vec3& maxs1, const vec3& mins2, con (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); diff --git a/src/util/util.h b/src/util/util.h index ca7b9865..c4a229fc 100644 --- a/src/util/util.h +++ b/src/util/util.h @@ -86,6 +86,8 @@ bool vertsAllOnOneSide(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)