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.
This commit is contained in:
Kelsi 2026-03-06 17:03:29 -08:00
parent d6de60e413
commit dfc53f30a8
3 changed files with 37 additions and 11 deletions

View file

@ -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();
}

View file

@ -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);

View file

@ -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;