feat: improve fps controller / add fog and ambient light to the scene

This commit is contained in:
2026-10-16 09:29:59 +02:00
parent 21621001c2
commit 4c9b77c948
105 changed files with 609 additions and 110 deletions
@@ -1,18 +1,33 @@
#include <components/FirstPersonController.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/norm.hpp>
#include <destrum/Components/Physics/CapsuleCollider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Physics/PhysicsWorld.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Util/DeltaTime.h>
#include <array>
#include <cmath>
#include <imgui.h>
namespace {
glm::vec3 MoveTowards(const glm::vec3& current, const glm::vec3& target, float maxDelta)
{
const glm::vec3 delta = target - current;
const float distance = glm::length(delta);
if (distance <= maxDelta || distance < 1e-6f) {
return target;
}
return current + delta / distance * maxDelta;
}
}
FirstPersonController::FirstPersonController(GameObject& parent)
: Component(parent, "FirstPersonController")
{
@@ -26,14 +41,26 @@ void FirstPersonController::SetMouseCaptured(bool captured)
void FirstPersonController::Start()
{
auto* obj = GetGameObject();
m_Capsule = obj->AddComponent<CapsuleCollider>(0.5f, 2.0f);
m_Rigidbody = obj->AddComponent<Rigidbody>();
m_Capsule = obj->GetComponent<CapsuleCollider>();
if (m_Capsule == nullptr) {
m_Capsule = obj->AddComponent<CapsuleCollider>(0.4f, 1.8f);
}
m_Rigidbody = obj->GetComponent<Rigidbody>();
if (m_Rigidbody == nullptr) {
m_Rigidbody = obj->AddComponent<Rigidbody>();
}
// The controller owns gravity and friction, the physics body only resolves collisions
m_Rigidbody->SetMass(80.0f);
m_Rigidbody->SetFriction(0.0f);
m_Rigidbody->SetAllowSleep(false);
m_Rigidbody->SetUseGravity(true);
m_Rigidbody->SetUseGravity(false);
m_Rigidbody->SetFreezeRotation(true);
}
void FirstPersonController::Update(float)
void FirstPersonController::Update(float dt)
{
const auto& input = InputManager::GetInstance();
@@ -44,6 +71,7 @@ void FirstPersonController::Update(float)
if (!IsMouseCaptured())
{
m_JumpHeld = false;
if (input.WasMousePressed(SDL_BUTTON_LEFT) && !ImGui::GetIO().WantCaptureMouse)
{
SetMouseCaptured(true);
@@ -57,59 +85,144 @@ void FirstPersonController::Update(float)
return;
}
if (input.WasKeyPressed(SDL_SCANCODE_SPACE) && m_Rigidbody->HasPhysicsBody())
// Jump presses are remembered for a moment, so pressing just before landing still jumps
m_JumpBufferTimer = std::max(m_JumpBufferTimer - dt, 0.0f);
if (input.WasKeyPressed(SDL_SCANCODE_SPACE))
{
m_Rigidbody->AddImpulse(glm::vec3(0.0f, 400.0f, 0.0f));
m_JumpBufferTimer = m_JumpBufferTime;
}
m_JumpHeld = input.IsKeyDown(SDL_SCANCODE_SPACE);
m_Yaw += static_cast<float>(input.MouseDeltaX()) * m_MouseSensitivity;
m_Pitch -= static_cast<float>(input.MouseDeltaY()) * m_MouseSensitivity;
m_Pitch = glm::clamp(m_Pitch, glm::radians(-89.0f), glm::radians(89.0f));
}
void FirstPersonController::FixedUpdate(float)
void FirstPersonController::FixedUpdate(float fixedDt)
{
if (!IsMouseCaptured() || m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody())
if (m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody())
return;
m_PreviousPosition = GetTransform().GetWorldPosition();
m_HasPreviousPosition = true;
glm::vec3 velocity = m_Rigidbody->GetLinearVelocity();
m_Grounded = velocity.y <= 0.1f && CheckGrounded();
m_CoyoteTimer = m_Grounded ? m_CoyoteTime : std::max(m_CoyoteTimer - fixedDt, 0.0f);
const auto& input = InputManager::GetInstance();
const bool sprinting = IsMouseCaptured() && input.IsKeyDown(SDL_SCANCODE_LSHIFT);
const glm::vec3 wishVelocity = GetWishDirection() * (sprinting ? m_SprintSpeed : m_WalkSpeed);
const float acceleration = m_Grounded ? m_GroundAcceleration : m_AirAcceleration;
const glm::vec3 horizontal = MoveTowards(
glm::vec3{velocity.x, 0.0f, velocity.z},
wishVelocity,
acceleration * fixedDt);
velocity.x = horizontal.x;
velocity.z = horizontal.z;
if (m_JumpBufferTimer > 0.0f && m_CoyoteTimer > 0.0f)
{
velocity.y = std::sqrt(2.0f * m_Gravity * m_JumpHeight);
m_JumpBufferTimer = 0.0f;
m_CoyoteTimer = 0.0f;
m_Grounded = false;
}
// Falling faster than rising, and letting go of jump early cuts the jump short
float gravity = m_Gravity;
if (velocity.y < 0.0f || !m_JumpHeld)
{
gravity *= m_FallGravityMultiplier;
}
velocity.y -= gravity * fixedDt;
m_Rigidbody->SetLinearVelocity(velocity);
}
bool FirstPersonController::CheckGrounded() const
{
const auto* world = m_Rigidbody->GetPhysicsWorld();
if (world == nullptr || m_Capsule == nullptr)
return false;
const glm::vec3 center = GetTransform().GetWorldPosition() + m_Capsule->GetCenterOffset();
const float radius = m_Capsule->GetRadius();
const float bottom = center.y - m_Capsule->GetHeight() * 0.5f;
// Rays start inside the capsule (ignoring it) so they are always above the floor, even when the solver
// leaves the capsule sunk in a little. The outer rays catch standing on an edge.
const float spread = radius * 0.7f;
const std::array<glm::vec2, 5> offsets{
glm::vec2{0.0f, 0.0f},
glm::vec2{spread, 0.0f},
glm::vec2{-spread, 0.0f},
glm::vec2{0.0f, spread},
glm::vec2{0.0f, -spread},
};
constexpr float startHeight = 0.2f;
constexpr float probeDistance = 0.1f;
for (const glm::vec2& offset : offsets)
{
const glm::vec3 origin{center.x + offset.x, bottom + startHeight, center.z + offset.y};
PhysicsRaycastHit hit{};
if (world->Raycast(origin, glm::vec3{0.0f, -1.0f, 0.0f}, startHeight + probeDistance, hit,
m_Rigidbody->GetBody()) &&
hit.normal.y > 0.6f)
{
return true;
}
}
return false;
}
glm::vec3 FirstPersonController::GetWishDirection() const
{
if (!IsMouseCaptured())
return glm::vec3{0.0f};
const auto& input = InputManager::GetInstance();
const float cosY = glm::cos(m_Yaw);
const float sinY = glm::sin(m_Yaw);
const glm::vec3 forward{cosY, 0.0f, sinY};
const glm::vec3 right{-sinY, 0.0f, cosY};
const glm::vec3 forward{std::cos(m_Yaw), 0.0f, std::sin(m_Yaw)};
const glm::vec3 right{-std::sin(m_Yaw), 0.0f, std::cos(m_Yaw)};
glm::vec3 moveDir(0.0f);
if (input.IsKeyDown(SDL_SCANCODE_W)) moveDir += forward;
if (input.IsKeyDown(SDL_SCANCODE_S)) moveDir -= forward;
if (input.IsKeyDown(SDL_SCANCODE_D)) moveDir += right;
if (input.IsKeyDown(SDL_SCANCODE_A)) moveDir -= right;
glm::vec3 direction(0.0f);
if (input.IsKeyDown(SDL_SCANCODE_W)) direction += forward;
if (input.IsKeyDown(SDL_SCANCODE_S)) direction -= forward;
if (input.IsKeyDown(SDL_SCANCODE_D)) direction += right;
if (input.IsKeyDown(SDL_SCANCODE_A)) direction -= right;
glm::vec3 velocity(0.0f);
if (glm::length2(moveDir) > 0.0f)
{
moveDir = glm::normalize(moveDir);
velocity = moveDir * m_MovementSpeed;
}
return glm::length2(direction) > 0.0f ? glm::normalize(direction) : direction;
}
velocity.y = m_Rigidbody->GetLinearVelocity().y;
m_Rigidbody->SetLinearVelocity(velocity);
auto* world = m_Rigidbody->GetPhysicsWorld();
if (world != nullptr)
{
PhysicsTransform xform = world->GetBodyTransform(m_Rigidbody->GetBody());
xform.rotation = glm::quat(glm::vec3(0.0f, m_Yaw, 0.0f));
world->SetBodyTransform(m_Rigidbody->GetBody(), xform);
}
glm::vec3 FirstPersonController::GetEyePosition() const
{
const glm::vec3 current = GetTransform().GetWorldPosition();
const glm::vec3 position = m_HasPreviousPosition
? glm::mix(m_PreviousPosition, current, Time::GetInstance().FixedAlpha())
: current;
return position + glm::vec3{0.0f, m_EyeHeight, 0.0f};
}
void FirstPersonController::ImGuiInspector()
{
Component::ImGuiInspector();
ImGui::DragFloat("Movement Speed", &m_MovementSpeed, 0.1f, 0.0f, 100.0f);
ImGui::DragFloat("Walk Speed", &m_WalkSpeed, 0.1f, 0.0f, 100.0f);
ImGui::DragFloat("Sprint Speed", &m_SprintSpeed, 0.1f, 0.0f, 100.0f);
ImGui::DragFloat("Ground Acceleration", &m_GroundAcceleration, 0.5f, 0.0f, 500.0f);
ImGui::DragFloat("Air Acceleration", &m_AirAcceleration, 0.5f, 0.0f, 500.0f);
ImGui::DragFloat("Jump Height", &m_JumpHeight, 0.01f, 0.0f, 10.0f);
ImGui::DragFloat("Gravity", &m_Gravity, 0.1f, 0.0f, 100.0f);
ImGui::DragFloat("Fall Gravity Multiplier", &m_FallGravityMultiplier, 0.01f, 1.0f, 5.0f);
ImGui::DragFloat("Coyote Time", &m_CoyoteTime, 0.005f, 0.0f, 1.0f);
ImGui::DragFloat("Jump Buffer Time", &m_JumpBufferTime, 0.005f, 0.0f, 1.0f);
ImGui::DragFloat("Mouse Sensitivity", &m_MouseSensitivity, 0.0001f, 0.0f, 0.1f);
ImGui::DragFloat("Eye Height", &m_EyeHeight, 0.01f, 0.0f, 5.0f);
ImGui::DragFloat("Eye Height", &m_EyeHeight, 0.01f, -2.0f, 2.0f);
ImGui::Text("Grounded: %s", m_Grounded ? "yes" : "no");
ImGui::Text("Yaw: %.1f deg", glm::degrees(m_Yaw));
ImGui::Text("Pitch: %.1f deg", glm::degrees(m_Pitch));
bool captured = IsMouseCaptured();
@@ -122,7 +235,15 @@ void FirstPersonController::ImGuiInspector()
nlohmann::json FirstPersonController::Serialize() const
{
return {
{"movementSpeed", m_MovementSpeed},
{"walkSpeed", m_WalkSpeed},
{"sprintSpeed", m_SprintSpeed},
{"groundAcceleration", m_GroundAcceleration},
{"airAcceleration", m_AirAcceleration},
{"jumpHeight", m_JumpHeight},
{"gravity", m_Gravity},
{"fallGravityMultiplier", m_FallGravityMultiplier},
{"coyoteTime", m_CoyoteTime},
{"jumpBufferTime", m_JumpBufferTime},
{"mouseSensitivity", m_MouseSensitivity},
{"eyeHeight", m_EyeHeight},
{"yaw", m_Yaw},
@@ -132,9 +253,17 @@ nlohmann::json FirstPersonController::Serialize() const
void FirstPersonController::Deserialize(const nlohmann::json& data)
{
if (data.contains("movementSpeed")) m_MovementSpeed = data.at("movementSpeed").get<float>();
if (data.contains("mouseSensitivity")) m_MouseSensitivity = data.at("mouseSensitivity").get<float>();
if (data.contains("eyeHeight")) m_EyeHeight = data.at("eyeHeight").get<float>();
if (data.contains("yaw")) m_Yaw = data.at("yaw").get<float>();
if (data.contains("pitch")) m_Pitch = data.at("pitch").get<float>();
}
m_WalkSpeed = data.value("walkSpeed", data.value("movementSpeed", m_WalkSpeed));
m_SprintSpeed = data.value("sprintSpeed", m_SprintSpeed);
m_GroundAcceleration = data.value("groundAcceleration", m_GroundAcceleration);
m_AirAcceleration = data.value("airAcceleration", m_AirAcceleration);
m_JumpHeight = data.value("jumpHeight", m_JumpHeight);
m_Gravity = data.value("gravity", m_Gravity);
m_FallGravityMultiplier = data.value("fallGravityMultiplier", m_FallGravityMultiplier);
m_CoyoteTime = data.value("coyoteTime", m_CoyoteTime);
m_JumpBufferTime = data.value("jumpBufferTime", m_JumpBufferTime);
m_MouseSensitivity = data.value("mouseSensitivity", m_MouseSensitivity);
m_EyeHeight = data.value("eyeHeight", m_EyeHeight);
m_Yaw = data.value("yaw", m_Yaw);
m_Pitch = data.value("pitch", m_Pitch);
}