Make InvisibleTrap objects invisible and non-collidable

Event objects like Fire Festival Fury Trap and Mercutio Post use
SpellObject_InvisibleTrap.m2 models which were rendering as white
tiles using WHITE1.BLP texture. These are meant to be invisible
spell trigger objects that should not obstruct player movement.

Changes:
- Added isInvisibleTrap flag to M2ModelGPU struct
- Detect models with "invisibletrap" in name during loading
- Skip rendering invisible trap instances in render loop
- Disable all collision checks (floor/wall/occlusion) for invisible traps
- Objects remain functional for spell casting but are now invisible
This commit is contained in:
Kelsi 2026-02-09 22:31:36 -08:00
parent 463ee4a311
commit 8cb6311470
3 changed files with 65 additions and 6 deletions

View file

@ -60,6 +60,7 @@ struct M2ModelGPU {
bool collisionNoBlock = false;
bool collisionStatue = false;
bool isSmallFoliage = false; // Small foliage (bushes, grass, plants) - skip during taxi
bool isInvisibleTrap = false; // Invisible trap objects (don't render, no collision)
// Collision mesh with spatial grid (from M2 bounding geometry)
struct CollisionMesh {

View file

@ -792,6 +792,16 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
M2ModelGPU gpuModel;
gpuModel.name = model.name;
// Detect invisible trap models (event objects that should not render or collide)
std::string lowerName = model.name;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
bool isInvisibleTrap = (lowerName.find("invisibletrap") != std::string::npos);
gpuModel.isInvisibleTrap = isInvisibleTrap;
if (isInvisibleTrap) {
LOG_INFO("Loading InvisibleTrap model: ", model.name, " (will be invisible, no collision)");
}
// Use tight bounds from actual vertices for collision/camera occlusion.
// Header bounds in some M2s are overly conservative.
glm::vec3 tightMin( std::numeric_limits<float>::max());
@ -1045,10 +1055,22 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
// Load ALL textures from the model into a local vector
std::vector<GLuint> allTextures;
if (assetManager) {
for (const auto& tex : model.textures) {
for (size_t ti = 0; ti < model.textures.size(); ti++) {
const auto& tex = model.textures[ti];
if (!tex.filename.empty()) {
allTextures.push_back(loadTexture(tex.filename));
GLuint texId = loadTexture(tex.filename);
if (texId == whiteTexture) {
LOG_WARNING("M2 model ", model.name, " texture[", ti, "] failed to load: ", tex.filename);
}
if (isInvisibleTrap) {
LOG_INFO(" InvisibleTrap texture[", ti, "]: ", tex.filename, " -> ", (texId == whiteTexture ? "WHITE" : "OK"));
}
allTextures.push_back(texId);
} else {
LOG_WARNING("M2 model ", model.name, " texture[", ti, "] has empty filename (using white fallback)");
if (isInvisibleTrap) {
LOG_INFO(" InvisibleTrap texture[", ti, "]: EMPTY (using white fallback)");
}
allTextures.push_back(whiteTexture);
}
}
@ -1669,7 +1691,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
auto it = models.find(instance.modelId);
if (it == models.end()) continue;
const M2ModelGPU& model = it->second;
if (!model.isValid() || model.isSmoke) continue;
if (!model.isValid() || model.isSmoke || model.isInvisibleTrap) continue;
glm::vec3 toCam = instance.position - camPos;
float distSq = glm::dot(toCam, toCam);
@ -2671,7 +2693,7 @@ std::optional<float> M2Renderer::getFloorHeight(float glX, float glY, float glZ)
if (instance.scale <= 0.001f) continue;
const M2ModelGPU& model = it->second;
if (model.collisionNoBlock) continue;
if (model.collisionNoBlock || model.isInvisibleTrap) continue;
// --- Mesh-based floor: vertical ray vs collision triangles ---
// Does NOT skip the AABB path — both contribute and highest wins.
@ -2818,7 +2840,7 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to,
if (it == models.end()) continue;
const M2ModelGPU& model = it->second;
if (model.collisionNoBlock) continue;
if (model.collisionNoBlock || model.isInvisibleTrap) continue;
if (instance.scale <= 0.001f) continue;
// --- Mesh-based wall collision: closest-point push ---
@ -3058,7 +3080,7 @@ float M2Renderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3&
if (it == models.end()) continue;
const M2ModelGPU& model = it->second;
if (model.collisionNoBlock) continue;
if (model.collisionNoBlock || model.isInvisibleTrap) continue;
glm::vec3 localMin, localMax;
getTightCollisionBounds(model, localMin, localMax);
// Skip tiny doodads for camera occlusion; they cause jitter and false hits.

View file

@ -0,0 +1,36 @@
#include <iostream>
#include "pipeline/dbc.hpp"
#include "pipeline/asset_manager.hpp"
int main() {
wowee::pipeline::AssetManager assetManager;
assetManager.initialize("Data");
auto godi = assetManager.loadDBC("GameObjectDisplayInfo.dbc");
if (!godi || !godi->isLoaded()) {
std::cerr << "Failed to load GameObjectDisplayInfo.dbc\n";
return 1;
}
std::cout << "GameObjectDisplayInfo.dbc loaded with " << godi->getRecordCount() << " records\n\n";
// Check displayIds 35 and 1287
uint32_t targetIds[] = {35, 1287};
for (uint32_t targetId : targetIds) {
bool found = false;
for (uint32_t i = 0; i < godi->getRecordCount(); i++) {
uint32_t displayId = godi->getUInt32(i, 0);
if (displayId == targetId) {
std::string modelName = godi->getString(i, 1);
std::cout << "DisplayId " << displayId << ": " << modelName << "\n";
found = true;
break;
}
}
if (!found) {
std::cout << "DisplayId " << targetId << ": NOT FOUND\n";
}
}
return 0;
}