From dfc53f30a8005b231a5a76f8b5b8abc693afeee5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Mar 2026 17:03:29 -0800 Subject: [PATCH] Effect model additive blend, teleport facing, tighter area triggers Classify light shafts, portals, spotlights, bubbles, and similar M2 doodads as spell effects so they render with additive blending instead of as solid opaque objects. Set camera yaw from server orientation on world load so teleports face the correct direction. Reduce area trigger minimum radius (3.0 sphere, 4.0 box) to prevent premature portal firing near tram entrances. --- src/core/application.cpp | 9 +++++++-- src/game/game_handler.cpp | 8 ++++---- src/rendering/m2_renderer.cpp | 31 ++++++++++++++++++++++++++----- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/core/application.cpp b/src/core/application.cpp index 8c94289a..d99f99f1 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3428,10 +3428,15 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float glm::vec3 spawnCanonical = core::coords::serverToCanonical(glm::vec3(x, y, z)); glm::vec3 spawnRender = core::coords::canonicalToRender(spawnCanonical); - // Set camera position + // Set camera position and facing from server orientation if (renderer->getCameraController()) { + float yawDeg = 0.0f; + if (gameHandler) { + float canonicalYaw = gameHandler->getMovementInfo().orientation; + yawDeg = 180.0f - glm::degrees(canonicalYaw); + } renderer->getCameraController()->setOnlineMode(true); - renderer->getCameraController()->setDefaultSpawn(spawnRender, 0.0f, -15.0f); + renderer->getCameraController()->setDefaultSpawn(spawnRender, yawDeg, -15.0f); renderer->getCameraController()->reset(); } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5ff720b3..c8dba11f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8814,16 +8814,16 @@ void GameHandler::checkAreaTriggers() { bool inside = false; if (at.radius > 0.0f) { - // Sphere trigger — small minimum so player must be near the portal - float effectiveRadius = std::max(at.radius, 12.0f); + // Sphere trigger — use actual radius, with small floor for very tiny triggers + float effectiveRadius = std::max(at.radius, 3.0f); float dx = px - at.x; float dy = py - at.y; float dz = pz - at.z; float distSq = dx * dx + dy * dy + dz * dz; inside = (distSq <= effectiveRadius * effectiveRadius); } else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) { - // Box trigger — small minimum so player must walk into the portal area - float boxMin = 16.0f; + // Box trigger — use actual size, with small floor for tiny triggers + float boxMin = 4.0f; float effLength = std::max(at.boxLength, boxMin); float effWidth = std::max(at.boxWidth, boxMin); float effHeight = std::max(at.boxHeight, boxMin); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 925c020b..0998694a 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1116,9 +1116,24 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { // Ground clutter (grass/pebbles/detail cards) should never block camera/movement. gpuModel.collisionNoBlock = true; } - // Spell effect models: particle-dominated with minimal geometry (e.g. LevelUp.m2) - gpuModel.isSpellEffect = hasParticles && model.vertices.size() <= 200 && - model.particleEmitters.size() >= 3; + // Spell effect / pure-visual models: particle-dominated with minimal geometry, + // or named effect models (light shafts, portals, emitters, spotlights) + bool effectByName = + (lowerName.find("lightshaft") != std::string::npos) || + (lowerName.find("volumetriclight") != std::string::npos) || + (lowerName.find("instanceportal") != std::string::npos) || + (lowerName.find("mageportal") != std::string::npos) || + (lowerName.find("worldtreeportal") != std::string::npos) || + (lowerName.find("particleemitter") != std::string::npos) || + (lowerName.find("bubbles") != std::string::npos) || + (lowerName.find("spotlight") != std::string::npos) || + (lowerName.find("hazardlight") != std::string::npos) || + (lowerName.find("lavasplash") != std::string::npos) || + (lowerName.find("lavabubble") != std::string::npos) || + (lowerName.find("wisps") != std::string::npos); + gpuModel.isSpellEffect = effectByName || + (hasParticles && model.vertices.size() <= 200 && + model.particleEmitters.size() >= 3); // Water vegetation: cattails, reeds, bulrushes, kelp, seaweed, lilypad near water gpuModel.isWaterVegetation = (lowerName.find("cattail") != std::string::npos) || @@ -2381,8 +2396,14 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const // Select pipeline based on blend mode uint8_t effectiveBlendMode = batch.blendMode; - if (model.isSpellEffect && (effectiveBlendMode == 4 || effectiveBlendMode == 5)) { - effectiveBlendMode = 3; + if (model.isSpellEffect) { + // Effect models: force additive blend for opaque/cutout batches + // so the mesh renders as a transparent glow, not a solid object + if (effectiveBlendMode <= 1) { + effectiveBlendMode = 3; // additive + } else if (effectiveBlendMode == 4 || effectiveBlendMode == 5) { + effectiveBlendMode = 3; + } } if (forceCutout) { effectiveBlendMode = 1;