Camera collision fixes

This commit is contained in:
2026-09-11 14:38:57 +03:00
parent bb65c589f4
commit 867a15d6aa
5 changed files with 119 additions and 3 deletions
+11
View File
@@ -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
@@ -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);
}
}
@@ -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;
@@ -12,6 +12,7 @@
#include "CharacterSlotSystem.hpp"
#include "CharacterSpawnerSystem.hpp"
#include "../camera/EditorCamera.hpp"
#include "../physics/physics.h"
#include <OgreEntity.h>
#include <OgreCamera.h>
#include <OgreLogManager.h>
@@ -19,6 +20,20 @@
#include <OgreSkeletonInstance.h>
#include <cmath>
/* 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<PlayerControlledComponent>();
/* 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);
@@ -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,