diff --git a/src/bsp/Bsp.cpp b/src/bsp/Bsp.cpp index 2c0048cf..f3f8b38f 100644 --- a/src/bsp/Bsp.cpp +++ b/src/bsp/Bsp.cpp @@ -1650,6 +1650,253 @@ STRUCTCOUNT Bsp::delete_unused_hulls(bool noProgress) { return removed; } +struct CompareVert { + vec3 pos; + float u, v; +}; + +struct ModelIdxRemap { + int newIdx; + vec3 offset; +}; + +void Bsp::deduplicate_models() { + const float epsilon = 1.0f; + + 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; + string modelKeyA = "*" + 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) { + string modelKeyB = "*" + 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++) { + BSPFACE& faceA = faces[modelA.iFirstFace + fa]; + BSPTEXTUREINFO& infoA = texinfos[faceA.iTextureInfo]; + BSPPLANE& planeA = planes[faceA.iPlane]; + int32_t texOffset = ((int32_t*)textures)[infoA.iMiptex + 1]; + BSPMIPTEX& tex = *((BSPMIPTEX*)(textures + texOffset)); + float tw = 1.0f / (float)tex.nWidth; + float th = 1.0f / (float)tex.nHeight; + + vector vertsA; + for (int e = 0; e < faceA.nEdges; e++) { + int32_t edgeIdx = surfedges[faceA.iFirstEdge + e]; + BSPEDGE& 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); + //logf("A Face %d vert %d uv: %.2f %.2f\n", fa, e, v.u, v.v); + } + + bool foundMatch = false; + for (int fb = 0; fb < modelB.nFaces; fb++) { + BSPFACE& 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 + + vector vertsB; + for (int e = 0; e < faceB.nEdges; e++) { + int32_t edgeIdx = surfedges[faceB.iFirstEdge + e]; + BSPEDGE& 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); + //logf("B Face %d vert %d uv: %.2f %.2f\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; + + //logf("Model %d and %d seem very similar (%d faces)\n", i, k, modelA.nFaces); + ModelIdxRemap remap; + remap.newIdx = i; + remap.offset = minsB - minsA; + modelRemap[k] = remap; + } + } + + logf("Remapped %d BSP model references\n", modelRemap.size()); + + for (Entity* ent : ents) { + if (!ent->keyvalues.count("model")) { + continue; + } + + 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->getOrigin() + remap.offset).toKeyvalueString()); + ent->setOrAddKeyvalue("model", "*" + to_string(remap.newIdx)); + } + } +} + +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; + string modelKey = "*" + 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++) { + BSPFACE& 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++; + logf("Scale up model %d\n", i); + } + + logf("Scaled up textures on %d invisible models\n", scaleCount); +} + bool Bsp::is_invisible_solid(Entity* ent) { if (!ent->isBspModel()) return false; diff --git a/src/bsp/Entity.cpp b/src/bsp/Entity.cpp index 35cca8fe..657aedc2 100644 --- a/src/bsp/Entity.cpp +++ b/src/bsp/Entity.cpp @@ -1,5 +1,6 @@ #include "Entity.h" #include "util.h" +#include using namespace std; @@ -443,4 +444,51 @@ int Entity::getMemoryUsage() { } return size; +} + +bool Entity::isEverVisible() { + string cname = keyvalues["classname"]; + string tname = hasKey("targetname") ? keyvalues["targetname"] : ""; + + static 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; } \ No newline at end of file diff --git a/src/bsp/Entity.h b/src/bsp/Entity.h index ae145eef..7af15ef2 100644 --- a/src/bsp/Entity.h +++ b/src/bsp/Entity.h @@ -43,5 +43,7 @@ class Entity void renameTargetnameValues(string oldTargetname, string newTargetname); int getMemoryUsage(); // aproximate + + bool isEverVisible(); }; diff --git a/src/editor/Gui.cpp b/src/editor/Gui.cpp index 9c96b6f1..7e3b2bcb 100644 --- a/src/editor/Gui.cpp +++ b/src/editor/Gui.cpp @@ -465,6 +465,25 @@ void Gui::draw3dContextMenus() { if (ImGui::MenuItem("Paste texture", "Ctrl+V", false, copiedMiptex >= 0 && copiedMiptex < map->textureCount)) { pasteTexture(); } + if (ImGui::MenuItem("Select all of this texture")) { + if (!app->pickInfo.valid) { + return; + } + Bsp* map = app->pickInfo.map; + BSPTEXTUREINFO& texinfo = map->texinfos[map->faces[app->pickInfo.faceIdx].iTextureInfo]; + uint32_t selectedMiptex = texinfo.iMiptex; + + app->selectedFaces.clear(); + for (int i = 0; i < map->faceCount; i++) { + BSPTEXTUREINFO& info = map->texinfos[map->faces[i].iTextureInfo]; + if (info.iMiptex == selectedMiptex) { + app->selectedFaces.push_back(i); + } + } + + logf("Selected %d faces\n", app->selectedFaces.size()); + refreshSelectedFaces = true; + } ImGui::Separator(); @@ -812,6 +831,26 @@ void Gui::drawMenuBar() { app->pushUndoCommand(command); } + 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("AllocBlock Reduction", 0, false, !app->isLoading && mapSelected)) { + map->allocblock_reduction(); + + BspRenderer* renderer = mapSelected ? app->mapRenderers[app->pickInfo.mapIdx] : NULL; + if (renderer) { + renderer->preRenderFaces(); + g_app->gui->refresh(); + } + } + ImGui::Separator(); bool hasAnyCollision = anyHullValid[1] || anyHullValid[2] || anyHullValid[3];