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)