From 867a15d6aaab65b4b2081cf1356657aeb96d7894 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Fri, 11 Sep 2026 14:38:57 +0300 Subject: [PATCH] Camera collision fixes --- src/features/editScene/AGENTS.md | 11 +++ .../editScene/systems/CharacterSlotSystem.cpp | 11 +++ .../editScene/systems/CharacterSlotSystem.hpp | 5 ++ .../systems/PlayerControllerSystem.cpp | 88 ++++++++++++++++++- .../systems/PlayerControllerSystem.hpp | 7 ++ 5 files changed, 119 insertions(+), 3 deletions(-) diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index 07ca8b7..ee97798 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -334,6 +334,17 @@ system that should drive player locomotion animation. (`orientation * UNIT_Z`) so the camera starts behind the character whatever its rotation; an identity-rotation character keeps the old fixed yaw-180 behaviour. +- The TPS camera boom is collision-clamped: every frame + `updateTPSCamera` raycasts (`JoltPhysicsWrapper::raycastQuery`, which only + sees `Layers::NON_MOVING`) from the pivot towards the desired camera + position and clamps the boom at the first wall/floor/terrain hit minus a + small margin. Pull-in is instant, recovery towards the full `tpsDistance` + is eased (`CAMERA_RECOVER_SPEED`), and the clamped length is kept in + `ControllerState::tpsCurrentDistance`. Below `CAMERA_HIDE_DISTANCE` the + character geometry is hidden via + `CharacterSlotSystem::setCharacterVisible` (with hysteresis) so the camera + never renders the inside of the body; `shutdownController` restores + visibility because the character can survive scene switches. ### CharacterSpawnerSystem diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index 32d96ea..d50e2a4 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -814,3 +814,14 @@ void CharacterSlotSystem::setSlotVisible(flecs::entity e, ent->setVisible(visible); } } + +void CharacterSlotSystem::setCharacterVisible(flecs::entity e, bool visible) +{ + auto it = m_entities.find(e.id()); + if (it == m_entities.end()) + return; + for (auto &pair : it->second.parts) { + if (pair.second) + pair.second->setVisible(visible); + } +} diff --git a/src/features/editScene/systems/CharacterSlotSystem.hpp b/src/features/editScene/systems/CharacterSlotSystem.hpp index 0820831..58dfdb0 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.hpp +++ b/src/features/editScene/systems/CharacterSlotSystem.hpp @@ -91,6 +91,11 @@ public: void setSlotVisible(flecs::entity e, const Ogre::String &slot, bool visible); + /* Show/hide all part entities of a character at once (used when + * the TPS camera is pushed so close that it would end up inside + * the character geometry). */ + void setCharacterVisible(flecs::entity e, bool visible); + private: static CharacterSlotSystem *ms_singleton; static bool s_catalogLoaded; diff --git a/src/features/editScene/systems/PlayerControllerSystem.cpp b/src/features/editScene/systems/PlayerControllerSystem.cpp index 1df0afb..7d162ae 100644 --- a/src/features/editScene/systems/PlayerControllerSystem.cpp +++ b/src/features/editScene/systems/PlayerControllerSystem.cpp @@ -12,6 +12,7 @@ #include "CharacterSlotSystem.hpp" #include "CharacterSpawnerSystem.hpp" #include "../camera/EditorCamera.hpp" +#include "../physics/physics.h" #include #include #include @@ -19,6 +20,20 @@ #include #include +/* TPS camera collision tuning: the boom is clamped by a physics + * raycast from the pivot to the desired camera position; the hit is + * pulled back by CAMERA_COLLISION_MARGIN so the near plane stays out + * of the wall, pull-in is instant while recovery back to the full + * tpsDistance is eased at CAMERA_RECOVER_SPEED units/s to avoid + * popping. Below CAMERA_HIDE_DISTANCE the character geometry is + * hidden (with hysteresis) so the camera never renders the inside of + * the body. */ +static constexpr float CAMERA_COLLISION_MARGIN = 0.25f; +static constexpr float CAMERA_MIN_DISTANCE = 0.2f; +static constexpr float CAMERA_HIDE_DISTANCE = 1.15f; +static constexpr float CAMERA_HIDE_HYSTERESIS = 0.15f; +static constexpr float CAMERA_RECOVER_SPEED = 5.0f; + #ifdef EDITSCENE_VERBOSE_DEBUG #define EDITSCENE_LOG_PC(msg) \ Ogre::LogManager::getSingleton().logMessage(msg) @@ -66,6 +81,19 @@ void PlayerControllerSystem::shutdownController(ControllerState &state) { if (state.targetEntity.is_alive()) state.targetEntity.remove(); + + /* Restore character geometry hidden by the TPS camera + * proximity handling; the character may survive the controller + * shutdown (game-mode scene switches carry it over). */ + if (state.geometryHidden && state.targetEntity.is_alive()) { + CharacterSlotSystem *css = m_editorApp ? + m_editorApp->getCharacterSlotSystem() : + nullptr; + if (css) + css->setCharacterVisible(state.targetEntity, true); + } + state.geometryHidden = false; + state.tpsCurrentDistance = -1.0f; state.targetEntity = flecs::entity::null(); if (state.pivotNode) { @@ -281,12 +309,66 @@ void PlayerControllerSystem::updateTPSCamera(PlayerControllerComponent &pc, Ogre::Quaternion yawRot(Ogre::Degree(state.yaw), Ogre::Vector3::UNIT_Y); Ogre::Quaternion pitchRot(Ogre::Degree(state.pitch), Ogre::Vector3::UNIT_X); - Ogre::Vector3 offset = - yawRot * pitchRot * Ogre::Vector3(0, 0, pc.tpsDistance); - Ogre::Vector3 goalPos = pivotPos + offset; + Ogre::Vector3 boomDir = + yawRot * pitchRot * Ogre::Vector3(0, 0, 1.0f); + + /* Camera collision: raycast from the pivot towards the desired + * camera position and clamp the boom at the first static + * obstacle. raycastQuery only sees Layers::NON_MOVING (walls, + * floor, terrain), so the character itself never blocks the + * camera. Pull-in is instant; recovery towards the full + * tpsDistance is eased to avoid popping. */ + if (state.tpsCurrentDistance < 0.0f) + state.tpsCurrentDistance = pc.tpsDistance; + + float allowedDist = pc.tpsDistance; + JoltPhysicsWrapper *physics = JoltPhysicsWrapper::getSingletonPtr(); + if (physics) { + Ogre::Vector3 hitPos; + JPH::BodyID hitBody; + Ogre::Vector3 goalPos = + pivotPos + boomDir * pc.tpsDistance; + if (physics->raycastQuery(pivotPos, goalPos, hitPos, + hitBody)) { + float hitDist = (hitPos - pivotPos).length() - + CAMERA_COLLISION_MARGIN; + if (hitDist < allowedDist) + allowedDist = hitDist; + } + } + if (allowedDist < state.tpsCurrentDistance) + state.tpsCurrentDistance = allowedDist; + else if (state.tpsCurrentDistance < allowedDist) { + state.tpsCurrentDistance += CAMERA_RECOVER_SPEED * deltaTime; + if (state.tpsCurrentDistance > allowedDist) + state.tpsCurrentDistance = allowedDist; + } + if (state.tpsCurrentDistance < CAMERA_MIN_DISTANCE) + state.tpsCurrentDistance = CAMERA_MIN_DISTANCE; + + Ogre::Vector3 goalPos = + pivotPos + boomDir * state.tpsCurrentDistance; state.goalNode->setPosition(goalPos); + /* Hide the character geometry when the clamped boom is so + * short that the camera would end up inside the body; restore + * it (with a bit of hysteresis) once there is room again. */ + CharacterSlotSystem *css = m_editorApp->getCharacterSlotSystem(); + if (css) { + if (!state.geometryHidden && + state.tpsCurrentDistance < CAMERA_HIDE_DISTANCE) { + css->setCharacterVisible(state.targetEntity, false); + state.geometryHidden = true; + } else if (state.geometryHidden && + state.tpsCurrentDistance > + CAMERA_HIDE_DISTANCE + + CAMERA_HIDE_HYSTERESIS) { + css->setCharacterVisible(state.targetEntity, true); + state.geometryHidden = false; + } + } + // Snap camera directly to goal to avoid jitter // caused by lerp smoothing with varying dt. camNode->setPosition(goalPos); diff --git a/src/features/editScene/systems/PlayerControllerSystem.hpp b/src/features/editScene/systems/PlayerControllerSystem.hpp index cdf90af..02257be 100644 --- a/src/features/editScene/systems/PlayerControllerSystem.hpp +++ b/src/features/editScene/systems/PlayerControllerSystem.hpp @@ -68,6 +68,13 @@ private: Ogre::SceneNode *goalNode = nullptr; bool initialized = false; bool faceHidden = false; + /* Current TPS boom length after collision clamping; + * negative until the first updateTPSCamera initializes it + * to PlayerControllerComponent::tpsDistance. */ + float tpsCurrentDistance = -1.0f; + /* True while the character geometry is hidden because the + * clamped TPS camera sits inside/too close to the body. */ + bool geometryHidden = false; }; void initController(flecs::entity controllerEntity,