From 0ca3a5b8f197ba87e3696cea521ea16377f5a058 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Fri, 26 Dec 2025 18:09:11 +0900 Subject: [PATCH] ADD: Camera mode --- src/CMakeLists.txt | 13 ++ src/core/engine.cpp | 2 +- src/core/engine.h | 2 +- src/core/engine_ui.cpp | 219 +++++++++++++++++++++++++ src/core/game_api.cpp | 263 +++++++++++++++++++++++++++++++ src/core/game_api.h | 79 ++++++++++ src/runtime/game_runtime.cpp | 2 +- src/scene/camera.cpp | 96 ----------- src/scene/camera.h | 14 -- src/scene/camera/camera_rig.cpp | 129 +++++++++++++++ src/scene/camera/camera_rig.h | 124 +++++++++++++++ src/scene/camera/icamera_mode.h | 20 +++ src/scene/camera/mode_chase.cpp | 74 +++++++++ src/scene/camera/mode_chase.h | 24 +++ src/scene/camera/mode_fixed.cpp | 25 +++ src/scene/camera/mode_fixed.h | 24 +++ src/scene/camera/mode_follow.cpp | 58 +++++++ src/scene/camera/mode_follow.h | 24 +++ src/scene/camera/mode_free.cpp | 147 +++++++++++++++++ src/scene/camera/mode_free.h | 27 ++++ src/scene/camera/mode_orbit.cpp | 163 +++++++++++++++++++ src/scene/camera/mode_orbit.h | 25 +++ src/scene/vk_scene.cpp | 41 ++--- src/scene/vk_scene.h | 4 + 24 files changed, 1466 insertions(+), 133 deletions(-) create mode 100644 src/scene/camera/camera_rig.cpp create mode 100644 src/scene/camera/camera_rig.h create mode 100644 src/scene/camera/icamera_mode.h create mode 100644 src/scene/camera/mode_chase.cpp create mode 100644 src/scene/camera/mode_chase.h create mode 100644 src/scene/camera/mode_fixed.cpp create mode 100644 src/scene/camera/mode_fixed.h create mode 100644 src/scene/camera/mode_follow.cpp create mode 100644 src/scene/camera/mode_follow.h create mode 100644 src/scene/camera/mode_free.cpp create mode 100644 src/scene/camera/mode_free.h create mode 100644 src/scene/camera/mode_orbit.cpp create mode 100644 src/scene/camera/mode_orbit.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8dffe5b..978dd27 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -117,6 +117,19 @@ add_executable (vulkan_engine scene/tangent_space.cpp scene/camera.h scene/camera.cpp + scene/camera/icamera_mode.h + scene/camera/camera_rig.h + scene/camera/camera_rig.cpp + scene/camera/mode_free.h + scene/camera/mode_free.cpp + scene/camera/mode_orbit.h + scene/camera/mode_orbit.cpp + scene/camera/mode_follow.h + scene/camera/mode_follow.cpp + scene/camera/mode_chase.h + scene/camera/mode_chase.cpp + scene/camera/mode_fixed.h + scene/camera/mode_fixed.cpp # compute compute/vk_compute.h compute/vk_compute.cpp diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 132a4ef..b93afa4 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -1671,7 +1671,7 @@ void VulkanEngine::run() if (_sceneManager && _input) { - _sceneManager->getMainCamera().process_input(*_input, ui_capture_keyboard, ui_capture_mouse); + _sceneManager->getCameraRig().process_input(*_input, ui_capture_keyboard, ui_capture_mouse); } if (freeze_rendering) diff --git a/src/core/engine.h b/src/core/engine.h index 958aea7..abb9437 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -39,8 +39,8 @@ #include "core/assets/ibl_manager.h" #include "core/ui/imgui_system.h" #include "core/picking/picking_system.h" +#include "core/input/input_system.h" -class InputSystem; class DebugDrawSystem; struct DebugDrawDeleter diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index 8acde16..805cd29 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -21,6 +21,7 @@ #include "render/passes/background.h" #include "render/passes/particles.h" #include +#include #include #include "render/graph/graph.h" #include "core/pipeline/manager.h" @@ -781,6 +782,213 @@ namespace } } + static void ui_camera(VulkanEngine *eng) + { + if (!eng || !eng->_sceneManager) + { + ImGui::TextUnformatted("SceneManager not available"); + return; + } + + SceneManager *sceneMgr = eng->_sceneManager.get(); + CameraRig &rig = sceneMgr->getCameraRig(); + Camera &cam = sceneMgr->getMainCamera(); + + // Mode switch + static const char *k_mode_names[] = {"Free", "Orbit", "Follow", "Chase", "Fixed"}; + int mode = static_cast(rig.mode()); + if (ImGui::Combo("Mode", &mode, k_mode_names, IM_ARRAYSIZE(k_mode_names))) + { + rig.set_mode(static_cast(mode), *sceneMgr, cam); + if (eng->_input) + { + eng->_input->set_cursor_mode(CursorMode::Normal); + } + } + + ImGui::Text("Active mode: %s", rig.mode_name()); + ImGui::Separator(); + + // Camera state (world) + double pos[3] = {cam.position_world.x, cam.position_world.y, cam.position_world.z}; + if (ImGui::InputScalarN("Position (world)", ImGuiDataType_Double, pos, 3, nullptr, nullptr, "%.3f")) + { + cam.position_world = WorldVec3(pos[0], pos[1], pos[2]); + } + float fov = cam.fovDegrees; + if (ImGui::SliderFloat("FOV (deg)", &fov, 30.0f, 110.0f)) + { + cam.fovDegrees = fov; + } + + WorldVec3 origin = sceneMgr->get_world_origin(); + glm::vec3 camLocal = sceneMgr->get_camera_local_position(); + ImGui::Text("Origin (world): (%.3f, %.3f, %.3f)", origin.x, origin.y, origin.z); + ImGui::Text("Camera (local): (%.3f, %.3f, %.3f)", camLocal.x, camLocal.y, camLocal.z); + + auto target_from_last_pick = [&](CameraTarget &target) -> bool { + PickingSystem *picking = eng->picking(); + if (!picking) return false; + const auto &pick = picking->last_pick(); + if (!pick.valid) return false; + + if (pick.ownerType == RenderObject::OwnerType::MeshInstance) + { + target.type = CameraTargetType::MeshInstance; + target.name = pick.ownerName; + } + else if (pick.ownerType == RenderObject::OwnerType::GLTFInstance) + { + target.type = CameraTargetType::GLTFInstance; + target.name = pick.ownerName; + } + else + { + target.type = CameraTargetType::WorldPoint; + target.world_point = pick.worldPos; + target.name.clear(); + } + return true; + }; + + auto draw_target = [&](const char *id, CameraTarget &target, char *name_buf, size_t name_buf_size) { + ImGui::PushID(id); + static const char *k_target_types[] = {"None", "WorldPoint", "MeshInstance", "GLTFInstance"}; + int type = static_cast(target.type); + if (ImGui::Combo("Target type", &type, k_target_types, IM_ARRAYSIZE(k_target_types))) + { + target.type = static_cast(type); + if (target.type != CameraTargetType::MeshInstance && target.type != CameraTargetType::GLTFInstance) + { + target.name.clear(); + if (name_buf_size > 0) + { + name_buf[0] = '\0'; + } + } + } + + if (target.type == CameraTargetType::WorldPoint) + { + double p[3] = {target.world_point.x, target.world_point.y, target.world_point.z}; + if (ImGui::InputScalarN("World point", ImGuiDataType_Double, p, 3, nullptr, nullptr, "%.3f")) + { + target.world_point = WorldVec3(p[0], p[1], p[2]); + } + } + else if (target.type == CameraTargetType::MeshInstance || target.type == CameraTargetType::GLTFInstance) + { + if (std::strncmp(name_buf, target.name.c_str(), name_buf_size) != 0) + { + std::snprintf(name_buf, name_buf_size, "%s", target.name.c_str()); + } + ImGui::InputText("Target name", name_buf, name_buf_size); + target.name = name_buf; + } + + WorldVec3 tpos{}; + glm::quat trot{}; + bool ok = rig.resolve_target(*sceneMgr, target, tpos, trot); + ImGui::Text("Resolved: %s", ok ? "yes" : "no"); + if (ok) + { + ImGui::Text("Target world: (%.3f, %.3f, %.3f)", tpos.x, tpos.y, tpos.z); + } + ImGui::PopID(); + }; + + // Free + if (ImGui::CollapsingHeader("Free", ImGuiTreeNodeFlags_DefaultOpen)) + { + auto &s = rig.free_settings(); + ImGui::InputFloat("Move speed (u/s)", &s.move_speed); + s.move_speed = std::clamp(s.move_speed, 0.06f, 300.0f); + ImGui::InputFloat("Look sensitivity", &s.look_sensitivity); + ImGui::InputFloat("Roll speed (rad/s)", &s.roll_speed); + ImGui::TextUnformatted("Roll keys: Q/E"); + } + + // Orbit + if (ImGui::CollapsingHeader("Orbit")) + { + auto &s = rig.orbit_settings(); + static char orbitName[128] = ""; + draw_target("orbit_target", s.target, orbitName, IM_ARRAYSIZE(orbitName)); + if (ImGui::Button("Orbit target = Last Pick")) + { + target_from_last_pick(s.target); + } + ImGui::InputDouble("Distance", &s.distance, 0.1, 1.0, "%.3f"); + s.distance = std::clamp(s.distance, 0.2, 100000.0); + float yawDeg = glm::degrees(s.yaw); + float pitchDeg = glm::degrees(s.pitch); + if (ImGui::SliderFloat("Yaw (deg)", &yawDeg, -180.0f, 180.0f)) + { + s.yaw = glm::radians(yawDeg); + } + if (ImGui::SliderFloat("Pitch (deg)", &pitchDeg, -89.0f, 89.0f)) + { + s.pitch = glm::radians(pitchDeg); + } + ImGui::InputFloat("Look sensitivity##orbit", &s.look_sensitivity); + } + + // Follow + if (ImGui::CollapsingHeader("Follow")) + { + auto &s = rig.follow_settings(); + static char followName[128] = ""; + draw_target("follow_target", s.target, followName, IM_ARRAYSIZE(followName)); + if (ImGui::Button("Follow target = Last Pick")) + { + target_from_last_pick(s.target); + } + ImGui::InputFloat3("Position offset (local)", &s.position_offset_local.x); + + glm::vec3 rotDeg = glm::degrees(glm::eulerAngles(s.rotation_offset)); + float r[3] = {rotDeg.x, rotDeg.y, rotDeg.z}; + if (ImGui::InputFloat3("Rotation offset (deg XYZ)", r)) + { + glm::mat4 R = glm::eulerAngleXYZ(glm::radians(r[0]), glm::radians(r[1]), glm::radians(r[2])); + s.rotation_offset = glm::quat_cast(R); + } + if (ImGui::Button("Reset rotation offset")) + { + s.rotation_offset = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + } + } + + // Chase + if (ImGui::CollapsingHeader("Chase")) + { + auto &s = rig.chase_settings(); + static char chaseName[128] = ""; + draw_target("chase_target", s.target, chaseName, IM_ARRAYSIZE(chaseName)); + if (ImGui::Button("Chase target = Last Pick")) + { + target_from_last_pick(s.target); + } + ImGui::InputFloat3("Position offset (local)##chase", &s.position_offset_local.x); + + glm::vec3 rotDeg = glm::degrees(glm::eulerAngles(s.rotation_offset)); + float r[3] = {rotDeg.x, rotDeg.y, rotDeg.z}; + if (ImGui::InputFloat3("Rotation offset (deg XYZ)##chase", r)) + { + glm::mat4 R = glm::eulerAngleXYZ(glm::radians(r[0]), glm::radians(r[1]), glm::radians(r[2])); + s.rotation_offset = glm::quat_cast(R); + } + + ImGui::SliderFloat("Position lag (1/s)", &s.position_lag, 0.0f, 30.0f); + ImGui::SliderFloat("Rotation lag (1/s)", &s.rotation_lag, 0.0f, 30.0f); + } + + // Fixed + if (ImGui::CollapsingHeader("Fixed")) + { + ImGui::TextUnformatted("Fixed mode does not modify the camera automatically."); + } + } + // Texture streaming + budget UI static const char *stateName(uint8_t s) { @@ -2051,6 +2259,7 @@ namespace bool show_ibl{false}; bool show_postfx{false}; bool show_scene{false}; + bool show_camera{false}; bool show_async_assets{false}; bool show_textures{false}; }; @@ -2072,6 +2281,7 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng) ImGui::MenuItem("Window", nullptr, &g_debug_windows.show_window); ImGui::Separator(); ImGui::MenuItem("Scene", nullptr, &g_debug_windows.show_scene); + ImGui::MenuItem("Camera", nullptr, &g_debug_windows.show_camera); ImGui::MenuItem("Render Graph", nullptr, &g_debug_windows.show_render_graph); ImGui::MenuItem("Pipelines", nullptr, &g_debug_windows.show_pipelines); ImGui::Separator(); @@ -2187,6 +2397,15 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng) ImGui::End(); } + if (g_debug_windows.show_camera) + { + if (ImGui::Begin("Camera", &g_debug_windows.show_camera)) + { + ui_camera(eng); + } + ImGui::End(); + } + if (g_debug_windows.show_async_assets) { if (ImGui::Begin("Async Assets", &g_debug_windows.show_async_assets)) diff --git a/src/core/game_api.cpp b/src/core/game_api.cpp index 446c51e..0f31d7a 100644 --- a/src/core/game_api.cpp +++ b/src/core/game_api.cpp @@ -13,6 +13,7 @@ #include "core/picking/picking_system.h" #include "scene/vk_scene.h" #include "scene/camera.h" +#include "scene/camera/camera_rig.h" #include #include @@ -1579,6 +1580,268 @@ void Engine::camera_look_at(const glm::dvec3& target) cam.orientation = glm::quat_cast(rot); } +namespace +{ + ::CameraMode to_internal_camera_mode(GameAPI::CameraMode mode) + { + switch (mode) + { + case GameAPI::CameraMode::Free: return ::CameraMode::Free; + case GameAPI::CameraMode::Orbit: return ::CameraMode::Orbit; + case GameAPI::CameraMode::Follow: return ::CameraMode::Follow; + case GameAPI::CameraMode::Chase: return ::CameraMode::Chase; + case GameAPI::CameraMode::Fixed: return ::CameraMode::Fixed; + default: return ::CameraMode::Free; + } + } + + GameAPI::CameraMode to_api_camera_mode(::CameraMode mode) + { + switch (mode) + { + case ::CameraMode::Free: return GameAPI::CameraMode::Free; + case ::CameraMode::Orbit: return GameAPI::CameraMode::Orbit; + case ::CameraMode::Follow: return GameAPI::CameraMode::Follow; + case ::CameraMode::Chase: return GameAPI::CameraMode::Chase; + case ::CameraMode::Fixed: return GameAPI::CameraMode::Fixed; + default: return GameAPI::CameraMode::Free; + } + } + + ::CameraTargetType to_internal_target_type(GameAPI::CameraTargetType type) + { + switch (type) + { + case GameAPI::CameraTargetType::None: return ::CameraTargetType::None; + case GameAPI::CameraTargetType::WorldPoint: return ::CameraTargetType::WorldPoint; + case GameAPI::CameraTargetType::MeshInstance: return ::CameraTargetType::MeshInstance; + case GameAPI::CameraTargetType::GLTFInstance: return ::CameraTargetType::GLTFInstance; + default: return ::CameraTargetType::None; + } + } + + GameAPI::CameraTargetType to_api_target_type(::CameraTargetType type) + { + switch (type) + { + case ::CameraTargetType::None: return GameAPI::CameraTargetType::None; + case ::CameraTargetType::WorldPoint: return GameAPI::CameraTargetType::WorldPoint; + case ::CameraTargetType::MeshInstance: return GameAPI::CameraTargetType::MeshInstance; + case ::CameraTargetType::GLTFInstance: return GameAPI::CameraTargetType::GLTFInstance; + default: return GameAPI::CameraTargetType::None; + } + } + + ::CameraTarget to_internal_target(const GameAPI::CameraTarget &target) + { + ::CameraTarget t; + t.type = to_internal_target_type(target.type); + t.name = target.name; + t.world_point = WorldVec3(target.worldPoint); + return t; + } + + GameAPI::CameraTarget to_api_target(const ::CameraTarget &target) + { + GameAPI::CameraTarget t; + t.type = to_api_target_type(target.type); + t.name = target.name; + t.worldPoint = glm::dvec3(target.world_point); + return t; + } +} // namespace + +void Engine::set_camera_mode(CameraMode mode) +{ + if (!_engine || !_engine->_sceneManager) + { + return; + } + + SceneManager *scene = _engine->_sceneManager.get(); + Camera &cam = scene->getMainCamera(); + CameraRig &rig = scene->getCameraRig(); + + if (_engine->_input) + { + _engine->_input->set_cursor_mode(CursorMode::Normal); + } + + rig.set_mode(to_internal_camera_mode(mode), *scene, cam); +} + +CameraMode Engine::get_camera_mode() const +{ + if (!_engine || !_engine->_sceneManager) + { + return CameraMode::Free; + } + return to_api_camera_mode(_engine->_sceneManager->getCameraRig().mode()); +} + +void Engine::set_free_camera_settings(const FreeCameraSettings& settings) +{ + if (!_engine || !_engine->_sceneManager) + { + return; + } + + ::FreeCameraSettings &s = _engine->_sceneManager->getCameraRig().free_settings(); + s.move_speed = settings.moveSpeed; + s.look_sensitivity = settings.lookSensitivity; + s.roll_speed = settings.rollSpeed; +} + +FreeCameraSettings Engine::get_free_camera_settings() const +{ + FreeCameraSettings out{}; + if (!_engine || !_engine->_sceneManager) + { + return out; + } + + const ::FreeCameraSettings &s = _engine->_sceneManager->getCameraRig().free_settings(); + out.moveSpeed = s.move_speed; + out.lookSensitivity = s.look_sensitivity; + out.rollSpeed = s.roll_speed; + return out; +} + +void Engine::set_orbit_camera_settings(const OrbitCameraSettings& settings) +{ + if (!_engine || !_engine->_sceneManager) + { + return; + } + + ::OrbitCameraSettings &s = _engine->_sceneManager->getCameraRig().orbit_settings(); + s.target = to_internal_target(settings.target); + s.distance = settings.distance; + s.yaw = settings.yaw; + s.pitch = settings.pitch; + s.look_sensitivity = settings.lookSensitivity; +} + +OrbitCameraSettings Engine::get_orbit_camera_settings() const +{ + OrbitCameraSettings out{}; + if (!_engine || !_engine->_sceneManager) + { + return out; + } + + const ::OrbitCameraSettings &s = _engine->_sceneManager->getCameraRig().orbit_settings(); + out.target = to_api_target(s.target); + out.distance = s.distance; + out.yaw = s.yaw; + out.pitch = s.pitch; + out.lookSensitivity = s.look_sensitivity; + return out; +} + +void Engine::set_follow_camera_settings(const FollowCameraSettings& settings) +{ + if (!_engine || !_engine->_sceneManager) + { + return; + } + + ::FollowCameraSettings &s = _engine->_sceneManager->getCameraRig().follow_settings(); + s.target = to_internal_target(settings.target); + s.position_offset_local = settings.positionOffsetLocal; + s.rotation_offset = settings.rotationOffset; +} + +FollowCameraSettings Engine::get_follow_camera_settings() const +{ + FollowCameraSettings out{}; + if (!_engine || !_engine->_sceneManager) + { + return out; + } + + const ::FollowCameraSettings &s = _engine->_sceneManager->getCameraRig().follow_settings(); + out.target = to_api_target(s.target); + out.positionOffsetLocal = s.position_offset_local; + out.rotationOffset = s.rotation_offset; + return out; +} + +void Engine::set_chase_camera_settings(const ChaseCameraSettings& settings) +{ + if (!_engine || !_engine->_sceneManager) + { + return; + } + + ::ChaseCameraSettings &s = _engine->_sceneManager->getCameraRig().chase_settings(); + s.target = to_internal_target(settings.target); + s.position_offset_local = settings.positionOffsetLocal; + s.rotation_offset = settings.rotationOffset; + s.position_lag = settings.positionLag; + s.rotation_lag = settings.rotationLag; +} + +ChaseCameraSettings Engine::get_chase_camera_settings() const +{ + ChaseCameraSettings out{}; + if (!_engine || !_engine->_sceneManager) + { + return out; + } + + const ::ChaseCameraSettings &s = _engine->_sceneManager->getCameraRig().chase_settings(); + out.target = to_api_target(s.target); + out.positionOffsetLocal = s.position_offset_local; + out.rotationOffset = s.rotation_offset; + out.positionLag = s.position_lag; + out.rotationLag = s.rotation_lag; + return out; +} + +bool Engine::set_camera_target_from_last_pick() +{ + if (!_engine || !_engine->_sceneManager) + { + return false; + } + + const PickingSystem *picking = _engine->picking(); + if (!picking) + { + return false; + } + + const auto &pick = picking->last_pick(); + if (!pick.valid) + { + return false; + } + + ::CameraTarget t; + if (pick.ownerType == RenderObject::OwnerType::MeshInstance) + { + t.type = ::CameraTargetType::MeshInstance; + t.name = pick.ownerName; + } + else if (pick.ownerType == RenderObject::OwnerType::GLTFInstance) + { + t.type = ::CameraTargetType::GLTFInstance; + t.name = pick.ownerName; + } + else + { + t.type = ::CameraTargetType::WorldPoint; + t.world_point = pick.worldPos; + } + + CameraRig &rig = _engine->_sceneManager->getCameraRig(); + rig.orbit_settings().target = t; + rig.follow_settings().target = t; + rig.chase_settings().target = t; + return true; +} + // ---------------------------------------------------------------------------- // Rendering // ---------------------------------------------------------------------------- diff --git a/src/core/game_api.h b/src/core/game_api.h index 02e5c4c..b033af5 100644 --- a/src/core/game_api.h +++ b/src/core/game_api.h @@ -304,6 +304,66 @@ struct Stats int drawCallCount{0}; }; +// ============================================================================ +// Camera Rig Types +// ============================================================================ + +enum class CameraMode : uint8_t +{ + Free = 0, + Orbit = 1, + Follow = 2, + Chase = 3, + Fixed = 4 +}; + +enum class CameraTargetType : uint8_t +{ + None = 0, + WorldPoint = 1, + MeshInstance = 2, + GLTFInstance = 3 +}; + +struct CameraTarget +{ + CameraTargetType type{CameraTargetType::None}; + std::string name{}; + glm::dvec3 worldPoint{0.0, 0.0, 0.0}; +}; + +struct FreeCameraSettings +{ + float moveSpeed{1.8f}; // world units / second + float lookSensitivity{0.0020f}; + float rollSpeed{1.0f}; // radians / second +}; + +struct OrbitCameraSettings +{ + CameraTarget target{}; + double distance{10.0}; + float yaw{0.0f}; // radians + float pitch{0.0f}; // radians + float lookSensitivity{0.0020f}; +}; + +struct FollowCameraSettings +{ + CameraTarget target{}; + glm::vec3 positionOffsetLocal{0.0f, 2.0f, 6.0f}; + glm::quat rotationOffset{1.0f, 0.0f, 0.0f, 0.0f}; +}; + +struct ChaseCameraSettings +{ + CameraTarget target{}; + glm::vec3 positionOffsetLocal{0.0f, 2.0f, 6.0f}; + glm::quat rotationOffset{1.0f, 0.0f, 0.0f, 0.0f}; + float positionLag{8.0f}; // smoothing rate (1/sec), higher = snappier + float rotationLag{10.0f}; // smoothing rate (1/sec) +}; + // ============================================================================ // Main API Class // ============================================================================ @@ -651,6 +711,25 @@ public: void camera_look_at(const glm::vec3& target); void camera_look_at(const glm::dvec3& target); + // Camera mode and per-mode settings + void set_camera_mode(CameraMode mode); + CameraMode get_camera_mode() const; + + void set_free_camera_settings(const FreeCameraSettings& settings); + FreeCameraSettings get_free_camera_settings() const; + + void set_orbit_camera_settings(const OrbitCameraSettings& settings); + OrbitCameraSettings get_orbit_camera_settings() const; + + void set_follow_camera_settings(const FollowCameraSettings& settings); + FollowCameraSettings get_follow_camera_settings() const; + + void set_chase_camera_settings(const ChaseCameraSettings& settings); + ChaseCameraSettings get_chase_camera_settings() const; + + // Convenience: set Orbit/Follow/Chase target from the engine's last pick. + bool set_camera_target_from_last_pick(); + // ------------------------------------------------------------------------ // Rendering // ------------------------------------------------------------------------ diff --git a/src/runtime/game_runtime.cpp b/src/runtime/game_runtime.cpp index e594de6..9a349a1 100644 --- a/src/runtime/game_runtime.cpp +++ b/src/runtime/game_runtime.cpp @@ -128,7 +128,7 @@ namespace GameRuntime // --- Camera input (if not captured by UI) --- if (_renderer->_sceneManager && input) { - _renderer->_sceneManager->getMainCamera().process_input(*input, ui_capture_keyboard, ui_capture_mouse); + _renderer->_sceneManager->getCameraRig().process_input(*input, ui_capture_keyboard, ui_capture_mouse); } // --- Throttle when minimized --- diff --git a/src/scene/camera.cpp b/src/scene/camera.cpp index 68cbb8d..9313675 100644 --- a/src/scene/camera.cpp +++ b/src/scene/camera.cpp @@ -1,102 +1,6 @@ #include "camera.h" #include #include -#include -#include - -void Camera::update() -{ - glm::mat4 cameraRotation = getRotationMatrix(); - glm::vec3 delta = glm::vec3(cameraRotation * glm::vec4(velocity * moveSpeed, 0.f)); - position_world += glm::dvec3(delta); -} - -void Camera::process_input(InputSystem &input, bool ui_capture_keyboard, bool ui_capture_mouse) -{ - const InputState &st = input.state(); - - // Movement is state-based so simultaneous keys work naturally. - if (ui_capture_keyboard) - { - velocity = glm::vec3(0.0f); - } - else - { - glm::vec3 v(0.0f); - if (st.key_down(Key::W)) { v.z -= 1.0f; } - if (st.key_down(Key::S)) { v.z += 1.0f; } - if (st.key_down(Key::A)) { v.x -= 1.0f; } - if (st.key_down(Key::D)) { v.x += 1.0f; } - velocity = v; - } - - // Event-based mouse handling so we don't apply motion that happened before RMB was pressed in the same frame. - for (const InputEvent &e : input.events()) - { - if (ui_capture_mouse) - { - continue; - } - - if (e.type == InputEvent::Type::MouseButtonDown && e.mouse_button == MouseButton::Right) - { - rmbDown = true; - input.set_cursor_mode(CursorMode::Relative); - } - else if (e.type == InputEvent::Type::MouseButtonUp && e.mouse_button == MouseButton::Right) - { - rmbDown = false; - input.set_cursor_mode(CursorMode::Normal); - } - else if (e.type == InputEvent::Type::MouseMove && rmbDown) - { - // Convert mouse motion to incremental yaw/pitch angles. - float dx = e.mouse_delta.x * lookSensitivity; - float dy = e.mouse_delta.y * lookSensitivity; - - // Mouse right (xrel > 0) turns view right with -Z-forward: yaw around +Y. - glm::quat yawRotation = glm::angleAxis(dx, glm::vec3 { 0.f, 1.f, 0.f }); - - // Mouse up (yrel < 0) looks up with -Z-forward: negative dy. - float pitchDelta = -dy; - // Pitch around the camera's local X (right) axis in world space. - glm::vec3 right = glm::rotate(orientation, glm::vec3 { 1.f, 0.f, 0.f }); - glm::quat pitchRotation = glm::angleAxis(pitchDelta, glm::vec3(right)); - - // Apply yaw, then pitch, to the current orientation. - orientation = glm::normalize(pitchRotation * yawRotation * orientation); - } - else if (e.type == InputEvent::Type::MouseWheel) - { - const float steps = e.wheel_delta.y; // positive = wheel up - if (std::abs(steps) < 0.001f) - { - continue; - } - - // Ctrl modifies FOV, otherwise adjust move speed - if (e.mods.ctrl) - { - // Wheel up -> zoom in (smaller FOV) - fovDegrees -= steps * 2.0f; - fovDegrees = std::clamp(fovDegrees, 30.0f, 110.0f); - } - else - { - // Exponential scale for pleasant feel - float factor = std::pow(1.15f, steps); - moveSpeed = std::clamp(moveSpeed * factor, 0.001f, 5.0f); - } - } - } - - // Safety: if mouse state shows RMB is no longer down, release relative mode. - if (rmbDown && !st.mouse_down(MouseButton::Right)) - { - rmbDown = false; - input.set_cursor_mode(CursorMode::Normal); - } -} glm::mat4 Camera::getViewMatrix(const glm::vec3 &position_local) const { diff --git a/src/scene/camera.h b/src/scene/camera.h index ac67279..cc66a8f 100644 --- a/src/scene/camera.h +++ b/src/scene/camera.h @@ -2,29 +2,15 @@ #include -#include - -#include "glm/vec3.hpp" - class Camera { public: - glm::vec3 velocity{0.0f, 0.0f, 0.0f}; glm::dvec3 position_world{0.0, 0.0, 0.0}; // Orientation stored as a quaternion (local -> world). glm::quat orientation { 1.0f, 0.0f, 0.0f, 0.0f }; - // Movement/look tuning - float moveSpeed { 0.03f }; - float lookSensitivity { 0.0020f }; - bool rmbDown { false }; - // Field of view in degrees for projection float fovDegrees { 50.f }; glm::mat4 getViewMatrix(const glm::vec3 &position_local) const; glm::mat4 getRotationMatrix() const; - - void process_input(InputSystem &input, bool ui_capture_keyboard, bool ui_capture_mouse); - - void update(); }; diff --git a/src/scene/camera/camera_rig.cpp b/src/scene/camera/camera_rig.cpp new file mode 100644 index 0000000..c87a205 --- /dev/null +++ b/src/scene/camera/camera_rig.cpp @@ -0,0 +1,129 @@ +#include "camera_rig.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + std::unique_ptr make_mode(CameraMode mode, + FreeCameraSettings &free_settings, + OrbitCameraSettings &orbit_settings, + FollowCameraSettings &follow_settings, + ChaseCameraSettings &chase_settings, + FixedCameraSettings &fixed_settings) + { + switch (mode) + { + case CameraMode::Free: return std::make_unique(free_settings); + case CameraMode::Orbit: return std::make_unique(orbit_settings); + case CameraMode::Follow: return std::make_unique(follow_settings); + case CameraMode::Chase: return std::make_unique(chase_settings); + case CameraMode::Fixed: return std::make_unique(fixed_settings); + default: return std::make_unique(free_settings); + } + } +} // namespace + +CameraRig::CameraRig() = default; +CameraRig::~CameraRig() = default; + +void CameraRig::init(SceneManager &scene, Camera &camera) +{ + _scene = &scene; + _camera = &camera; + recreate_mode(scene, camera); +} + +void CameraRig::set_mode(CameraMode mode, SceneManager &scene, Camera &camera) +{ + if (_mode == mode) + { + return; + } + _mode = mode; + recreate_mode(scene, camera); +} + +const char *CameraRig::mode_name() const +{ + if (_mode_impl) + { + return _mode_impl->name(); + } + return "None"; +} + +void CameraRig::process_input(InputSystem &input, bool ui_capture_keyboard, bool ui_capture_mouse) +{ + if (_mode_impl && _scene && _camera) + { + _mode_impl->process_input(*_scene, *_camera, input, ui_capture_keyboard, ui_capture_mouse); + } +} + +void CameraRig::update(SceneManager &scene, Camera &camera, float dt) +{ + if (_mode_impl) + { + _mode_impl->update(scene, camera, dt); + } +} + +bool CameraRig::resolve_target(SceneManager &scene, + const CameraTarget &target, + WorldVec3 &out_position_world, + glm::quat &out_rotation) const +{ + out_rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + + switch (target.type) + { + case CameraTargetType::WorldPoint: + out_position_world = target.world_point; + return true; + case CameraTargetType::MeshInstance: + { + WorldVec3 t{}; + glm::quat r{}; + glm::vec3 s{}; + if (!scene.getMeshInstanceTRSWorld(target.name, t, r, s)) + { + return false; + } + out_position_world = t; + out_rotation = r; + return true; + } + case CameraTargetType::GLTFInstance: + { + WorldVec3 t{}; + glm::quat r{}; + glm::vec3 s{}; + if (!scene.getGLTFInstanceTRSWorld(target.name, t, r, s)) + { + return false; + } + out_position_world = t; + out_rotation = r; + return true; + } + default: + return false; + } +} + +void CameraRig::recreate_mode(SceneManager &scene, Camera &camera) +{ + _mode_impl = make_mode(_mode, _free, _orbit, _follow, _chase, _fixed); + if (_mode_impl) + { + _mode_impl->on_activate(scene, camera); + } +} diff --git a/src/scene/camera/camera_rig.h b/src/scene/camera/camera_rig.h new file mode 100644 index 0000000..28db6ae --- /dev/null +++ b/src/scene/camera/camera_rig.h @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include + +#include +#include +#include + +class Camera; +class InputSystem; +class SceneManager; + +enum class CameraMode : uint8_t +{ + Free = 0, + Orbit = 1, + Follow = 2, + Chase = 3, + Fixed = 4 +}; + +enum class CameraTargetType : uint8_t +{ + None = 0, + WorldPoint = 1, + MeshInstance = 2, + GLTFInstance = 3 +}; + +struct CameraTarget +{ + CameraTargetType type{CameraTargetType::None}; + std::string name{}; + WorldVec3 world_point{0.0, 0.0, 0.0}; +}; + +struct FreeCameraSettings +{ + float move_speed{1.8f}; // world units / second + float look_sensitivity{0.0020f}; + float roll_speed{1.0f}; // radians / second +}; + +struct OrbitCameraSettings +{ + CameraTarget target{}; + double distance{10.0}; + float yaw{0.0f}; // radians + float pitch{0.0f}; // radians + float look_sensitivity{0.0020f}; +}; + +struct FollowCameraSettings +{ + CameraTarget target{}; + glm::vec3 position_offset_local{0.0f, 2.0f, 6.0f}; + glm::quat rotation_offset{1.0f, 0.0f, 0.0f, 0.0f}; +}; + +struct ChaseCameraSettings +{ + CameraTarget target{}; + glm::vec3 position_offset_local{0.0f, 2.0f, 6.0f}; + glm::quat rotation_offset{1.0f, 0.0f, 0.0f, 0.0f}; + float position_lag{8.0f}; // smoothing rate (1/sec), higher = snappier + float rotation_lag{10.0f}; // smoothing rate (1/sec) +}; + +struct FixedCameraSettings +{ +}; + +class CameraRig +{ +public: + CameraRig(); + ~CameraRig(); + + CameraRig(const CameraRig &) = delete; + CameraRig &operator=(const CameraRig &) = delete; + + void init(SceneManager &scene, Camera &camera); + + CameraMode mode() const { return _mode; } + void set_mode(CameraMode mode, SceneManager &scene, Camera &camera); + + const char *mode_name() const; + + FreeCameraSettings &free_settings() { return _free; } + OrbitCameraSettings &orbit_settings() { return _orbit; } + FollowCameraSettings &follow_settings() { return _follow; } + ChaseCameraSettings &chase_settings() { return _chase; } + FixedCameraSettings &fixed_settings() { return _fixed; } + + const FreeCameraSettings &free_settings() const { return _free; } + const OrbitCameraSettings &orbit_settings() const { return _orbit; } + const FollowCameraSettings &follow_settings() const { return _follow; } + const ChaseCameraSettings &chase_settings() const { return _chase; } + const FixedCameraSettings &fixed_settings() const { return _fixed; } + + void process_input(InputSystem &input, bool ui_capture_keyboard, bool ui_capture_mouse); + void update(SceneManager &scene, Camera &camera, float dt); + + bool resolve_target(SceneManager &scene, + const CameraTarget &target, + WorldVec3 &out_position_world, + glm::quat &out_rotation) const; + +private: + void recreate_mode(SceneManager &scene, Camera &camera); + + CameraMode _mode{CameraMode::Free}; + std::unique_ptr _mode_impl; + SceneManager *_scene = nullptr; + Camera *_camera = nullptr; + + FreeCameraSettings _free{}; + OrbitCameraSettings _orbit{}; + FollowCameraSettings _follow{}; + ChaseCameraSettings _chase{}; + FixedCameraSettings _fixed{}; +}; diff --git a/src/scene/camera/icamera_mode.h b/src/scene/camera/icamera_mode.h new file mode 100644 index 0000000..ef0fdde --- /dev/null +++ b/src/scene/camera/icamera_mode.h @@ -0,0 +1,20 @@ +#pragma once + +class Camera; +class InputSystem; +class SceneManager; + +class ICameraMode +{ +public: + virtual ~ICameraMode() = default; + + virtual const char *name() const = 0; + virtual void on_activate(SceneManager &scene, Camera &camera) = 0; + virtual void process_input(SceneManager &scene, + Camera &camera, + InputSystem &input, + bool ui_capture_keyboard, + bool ui_capture_mouse) = 0; + virtual void update(SceneManager &scene, Camera &camera, float dt) = 0; +}; diff --git a/src/scene/camera/mode_chase.cpp b/src/scene/camera/mode_chase.cpp new file mode 100644 index 0000000..44ff00c --- /dev/null +++ b/src/scene/camera/mode_chase.cpp @@ -0,0 +1,74 @@ +#include "mode_chase.h" + +#include +#include +#include + +#include + +#include + +ChaseCameraMode::ChaseCameraMode(ChaseCameraSettings &settings) + : _settings(settings) +{ +} + +void ChaseCameraMode::on_activate(SceneManager &scene, Camera &camera) +{ + // If no target set, chase a point in front of the camera. + if (_settings.target.type == CameraTargetType::None) + { + glm::vec3 forward = glm::rotate(camera.orientation, glm::vec3(0.0f, 0.0f, -1.0f)); + _settings.target.type = CameraTargetType::WorldPoint; + _settings.target.world_point = camera.position_world + WorldVec3(forward) * 10.0; + } + + // Preserve current relative transform to the target when possible. + WorldVec3 target_pos{}; + glm::quat target_rot{}; + if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot)) + { + return; + } + + glm::quat inv_target = glm::inverse(target_rot); + glm::vec3 rel_pos = glm::vec3(camera.position_world - target_pos); + _settings.position_offset_local = glm::rotate(inv_target, rel_pos); + _settings.rotation_offset = glm::normalize(inv_target * camera.orientation); +} + +void ChaseCameraMode::process_input(SceneManager & /*scene*/, + Camera & /*camera*/, + InputSystem & /*input*/, + bool /*ui_capture_keyboard*/, + bool /*ui_capture_mouse*/) +{ +} + +void ChaseCameraMode::update(SceneManager &scene, Camera &camera, float dt) +{ + WorldVec3 target_pos{}; + glm::quat target_rot{}; + if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot)) + { + return; + } + + glm::vec3 offset_world = glm::rotate(target_rot, _settings.position_offset_local); + WorldVec3 desired_pos = target_pos + WorldVec3(offset_world); + glm::quat desired_rot = glm::normalize(target_rot * _settings.rotation_offset); + + if (dt > 0.0f) + { + float pos_alpha = 1.0f - std::exp(-_settings.position_lag * dt); + float rot_alpha = 1.0f - std::exp(-_settings.rotation_lag * dt); + + camera.position_world += (desired_pos - camera.position_world) * static_cast(pos_alpha); + camera.orientation = glm::normalize(glm::slerp(camera.orientation, desired_rot, rot_alpha)); + } + else + { + camera.position_world = desired_pos; + camera.orientation = desired_rot; + } +} diff --git a/src/scene/camera/mode_chase.h b/src/scene/camera/mode_chase.h new file mode 100644 index 0000000..36c165e --- /dev/null +++ b/src/scene/camera/mode_chase.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +struct ChaseCameraSettings; + +class ChaseCameraMode : public ICameraMode +{ +public: + explicit ChaseCameraMode(ChaseCameraSettings &settings); + ~ChaseCameraMode() override = default; + + const char *name() const override { return "Chase"; } + void on_activate(SceneManager &scene, Camera &camera) override; + void process_input(SceneManager &scene, + Camera &camera, + InputSystem &input, + bool ui_capture_keyboard, + bool ui_capture_mouse) override; + void update(SceneManager &scene, Camera &camera, float dt) override; + +private: + ChaseCameraSettings &_settings; +}; diff --git a/src/scene/camera/mode_fixed.cpp b/src/scene/camera/mode_fixed.cpp new file mode 100644 index 0000000..73d279b --- /dev/null +++ b/src/scene/camera/mode_fixed.cpp @@ -0,0 +1,25 @@ +#include "mode_fixed.h" + +#include + +FixedCameraMode::FixedCameraMode(FixedCameraSettings &settings) + : _settings(settings) +{ +} + +void FixedCameraMode::on_activate(SceneManager & /*scene*/, Camera & /*camera*/) +{ +} + +void FixedCameraMode::process_input(SceneManager & /*scene*/, + Camera & /*camera*/, + InputSystem & /*input*/, + bool /*ui_capture_keyboard*/, + bool /*ui_capture_mouse*/) +{ +} + +void FixedCameraMode::update(SceneManager & /*scene*/, Camera & /*camera*/, float /*dt*/) +{ + (void)_settings; +} diff --git a/src/scene/camera/mode_fixed.h b/src/scene/camera/mode_fixed.h new file mode 100644 index 0000000..64b575c --- /dev/null +++ b/src/scene/camera/mode_fixed.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +struct FixedCameraSettings; + +class FixedCameraMode : public ICameraMode +{ +public: + explicit FixedCameraMode(FixedCameraSettings &settings); + ~FixedCameraMode() override = default; + + const char *name() const override { return "Fixed"; } + void on_activate(SceneManager &scene, Camera &camera) override; + void process_input(SceneManager &scene, + Camera &camera, + InputSystem &input, + bool ui_capture_keyboard, + bool ui_capture_mouse) override; + void update(SceneManager &scene, Camera &camera, float dt) override; + +private: + FixedCameraSettings &_settings; +}; diff --git a/src/scene/camera/mode_follow.cpp b/src/scene/camera/mode_follow.cpp new file mode 100644 index 0000000..32ab9a4 --- /dev/null +++ b/src/scene/camera/mode_follow.cpp @@ -0,0 +1,58 @@ +#include "mode_follow.h" + +#include +#include +#include + +#include + +FollowCameraMode::FollowCameraMode(FollowCameraSettings &settings) + : _settings(settings) +{ +} + +void FollowCameraMode::on_activate(SceneManager &scene, Camera &camera) +{ + // If no target set, follow a point in front of the camera. + if (_settings.target.type == CameraTargetType::None) + { + glm::vec3 forward = glm::rotate(camera.orientation, glm::vec3(0.0f, 0.0f, -1.0f)); + _settings.target.type = CameraTargetType::WorldPoint; + _settings.target.world_point = camera.position_world + WorldVec3(forward) * 10.0; + } + + // Preserve current relative transform to the target when possible. + WorldVec3 target_pos{}; + glm::quat target_rot{}; + if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot)) + { + return; + } + + glm::quat inv_target = glm::inverse(target_rot); + glm::vec3 rel_pos = glm::vec3(camera.position_world - target_pos); + _settings.position_offset_local = glm::rotate(inv_target, rel_pos); + _settings.rotation_offset = glm::normalize(inv_target * camera.orientation); +} + +void FollowCameraMode::process_input(SceneManager & /*scene*/, + Camera & /*camera*/, + InputSystem & /*input*/, + bool /*ui_capture_keyboard*/, + bool /*ui_capture_mouse*/) +{ +} + +void FollowCameraMode::update(SceneManager &scene, Camera &camera, float /*dt*/) +{ + WorldVec3 target_pos{}; + glm::quat target_rot{}; + if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot)) + { + return; + } + + glm::vec3 offset_world = glm::rotate(target_rot, _settings.position_offset_local); + camera.position_world = target_pos + WorldVec3(offset_world); + camera.orientation = glm::normalize(target_rot * _settings.rotation_offset); +} diff --git a/src/scene/camera/mode_follow.h b/src/scene/camera/mode_follow.h new file mode 100644 index 0000000..598611b --- /dev/null +++ b/src/scene/camera/mode_follow.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +struct FollowCameraSettings; + +class FollowCameraMode : public ICameraMode +{ +public: + explicit FollowCameraMode(FollowCameraSettings &settings); + ~FollowCameraMode() override = default; + + const char *name() const override { return "Follow"; } + void on_activate(SceneManager &scene, Camera &camera) override; + void process_input(SceneManager &scene, + Camera &camera, + InputSystem &input, + bool ui_capture_keyboard, + bool ui_capture_mouse) override; + void update(SceneManager &scene, Camera &camera, float dt) override; + +private: + FollowCameraSettings &_settings; +}; diff --git a/src/scene/camera/mode_free.cpp b/src/scene/camera/mode_free.cpp new file mode 100644 index 0000000..3c5195e --- /dev/null +++ b/src/scene/camera/mode_free.cpp @@ -0,0 +1,147 @@ +#include "mode_free.h" + +#include +#include + +#include + +#include + +#include +#include + +FreeCameraMode::FreeCameraMode(FreeCameraSettings &settings) + : _settings(settings) +{ +} + +void FreeCameraMode::on_activate(SceneManager & /*scene*/, Camera & /*camera*/) +{ + _velocity = glm::vec3(0.0f); + _roll_dir = 0.0f; + _rmb_down = false; +} + +void FreeCameraMode::process_input(SceneManager & /*scene*/, + Camera &camera, + InputSystem &input, + bool ui_capture_keyboard, + bool ui_capture_mouse) +{ + const InputState &st = input.state(); + + // Movement is state-based so simultaneous keys work naturally. + if (ui_capture_keyboard) + { + _velocity = glm::vec3(0.0f); + _roll_dir = 0.0f; + } + else + { + glm::vec3 v(0.0f); + if (st.key_down(Key::W)) { v.z -= 1.0f; } + if (st.key_down(Key::S)) { v.z += 1.0f; } + if (st.key_down(Key::A)) { v.x -= 1.0f; } + if (st.key_down(Key::D)) { v.x += 1.0f; } + _velocity = v; + + float roll = 0.0f; + if (st.key_down(Key::Q)) { roll -= 1.0f; } + if (st.key_down(Key::E)) { roll += 1.0f; } + _roll_dir = roll; + } + + // Event-based mouse handling so we don't apply motion that happened + // before RMB was pressed in the same frame. + for (const InputEvent &e : input.events()) + { + if (ui_capture_mouse) + { + continue; + } + + if (e.type == InputEvent::Type::MouseButtonDown && e.mouse_button == MouseButton::Right) + { + _rmb_down = true; + input.set_cursor_mode(CursorMode::Relative); + } + else if (e.type == InputEvent::Type::MouseButtonUp && e.mouse_button == MouseButton::Right) + { + _rmb_down = false; + input.set_cursor_mode(CursorMode::Normal); + } + else if (e.type == InputEvent::Type::MouseMove && _rmb_down) + { + // Convert mouse motion to incremental yaw/pitch angles. + float dx = e.mouse_delta.x * _settings.look_sensitivity; + float dy = e.mouse_delta.y * _settings.look_sensitivity; + + // Mouse right (xrel > 0) turns view right with -Z-forward: yaw around +Y. + glm::quat yaw_rotation = glm::angleAxis(dx, glm::vec3{0.f, 1.f, 0.f}); + + // Mouse up (yrel < 0) looks up with -Z-forward: negative dy. + float pitch_delta = -dy; + // Pitch around the camera's local X (right) axis in world space. + glm::vec3 right = glm::rotate(camera.orientation, glm::vec3{1.f, 0.f, 0.f}); + glm::quat pitch_rotation = glm::angleAxis(pitch_delta, glm::vec3(right)); + + // Apply yaw, then pitch, to the current orientation. + camera.orientation = glm::normalize(pitch_rotation * yaw_rotation * camera.orientation); + } + else if (e.type == InputEvent::Type::MouseWheel) + { + const float steps = e.wheel_delta.y; // positive = wheel up + if (std::abs(steps) < 0.001f) + { + continue; + } + + // Ctrl modifies FOV, otherwise adjust move speed. + if (e.mods.ctrl) + { + // Wheel up -> zoom in (smaller FOV) + camera.fovDegrees -= steps * 2.0f; + camera.fovDegrees = std::clamp(camera.fovDegrees, 30.0f, 110.0f); + } + else + { + // Exponential scale for pleasant feel + float factor = std::pow(1.15f, steps); + _settings.move_speed = std::clamp(_settings.move_speed * factor, 0.06f, 300.0f); + } + } + } + + // Safety: if mouse state shows RMB is no longer down, release relative mode. + if (_rmb_down && !st.mouse_down(MouseButton::Right)) + { + _rmb_down = false; + input.set_cursor_mode(CursorMode::Normal); + } +} + +void FreeCameraMode::update(SceneManager & /*scene*/, Camera &camera, float dt) +{ + if (dt <= 0.0f) + { + return; + } + + // Roll around the camera's forward axis (world-space axis). + if (_roll_dir != 0.0f && _settings.roll_speed > 0.0f) + { + glm::vec3 forward = glm::rotate(camera.orientation, glm::vec3{0.0f, 0.0f, -1.0f}); + float angle = _roll_dir * _settings.roll_speed * dt; + glm::quat roll_rotation = glm::angleAxis(angle, glm::normalize(forward)); + camera.orientation = glm::normalize(roll_rotation * camera.orientation); + } + + // Move in camera-local space. + if (_velocity.x != 0.0f || _velocity.y != 0.0f || _velocity.z != 0.0f) + { + glm::vec3 local_delta = _velocity * (_settings.move_speed * dt); + glm::mat4 camera_rotation = camera.getRotationMatrix(); + glm::vec3 world_delta = glm::vec3(camera_rotation * glm::vec4(local_delta, 0.0f)); + camera.position_world += glm::dvec3(world_delta); + } +} diff --git a/src/scene/camera/mode_free.h b/src/scene/camera/mode_free.h new file mode 100644 index 0000000..f85115d --- /dev/null +++ b/src/scene/camera/mode_free.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +struct FreeCameraSettings; + +class FreeCameraMode : public ICameraMode +{ +public: + explicit FreeCameraMode(FreeCameraSettings &settings); + ~FreeCameraMode() override = default; + + const char *name() const override { return "Free"; } + void on_activate(SceneManager &scene, Camera &camera) override; + void process_input(SceneManager &scene, + Camera &camera, + InputSystem &input, + bool ui_capture_keyboard, + bool ui_capture_mouse) override; + void update(SceneManager &scene, Camera &camera, float dt) override; + +private: + FreeCameraSettings &_settings; + glm::vec3 _velocity{0.0f, 0.0f, 0.0f}; + float _roll_dir = 0.0f; + bool _rmb_down = false; +}; diff --git a/src/scene/camera/mode_orbit.cpp b/src/scene/camera/mode_orbit.cpp new file mode 100644 index 0000000..80810c8 --- /dev/null +++ b/src/scene/camera/mode_orbit.cpp @@ -0,0 +1,163 @@ +#include "mode_orbit.h" + +#include +#include +#include + +#include + +#include +#include + +#include +#include + +OrbitCameraMode::OrbitCameraMode(OrbitCameraSettings &settings) + : _settings(settings) +{ +} + +void OrbitCameraMode::on_activate(SceneManager &scene, Camera &camera) +{ + _rmb_down = false; + + // If no target set, orbit around a point in front of the camera. + if (_settings.target.type == CameraTargetType::None) + { + glm::vec3 forward = glm::rotate(camera.orientation, glm::vec3(0.0f, 0.0f, -1.0f)); + _settings.target.type = CameraTargetType::WorldPoint; + _settings.target.world_point = camera.position_world + WorldVec3(forward) * _settings.distance; + } + + // Derive yaw/pitch/distance from current camera pose to avoid snapping. + WorldVec3 target_pos{}; + glm::quat target_rot{}; + if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot)) + { + return; + } + + WorldVec3 to_cam = camera.position_world - target_pos; + double dist = glm::length(to_cam); + if (!std::isfinite(dist) || dist < 0.001) + { + dist = _settings.distance; + } + _settings.distance = dist; + + glm::vec3 dir = glm::normalize(glm::vec3(to_cam / dist)); // target -> camera + _settings.yaw = std::atan2(dir.x, dir.z); + _settings.pitch = std::asin(std::clamp(-dir.y, -1.0f, 1.0f)); +} + +void OrbitCameraMode::process_input(SceneManager & /*scene*/, + Camera &camera, + InputSystem &input, + bool /*ui_capture_keyboard*/, + bool ui_capture_mouse) +{ + const InputState &st = input.state(); + + for (const InputEvent &e : input.events()) + { + if (ui_capture_mouse) + { + continue; + } + + if (e.type == InputEvent::Type::MouseButtonDown && e.mouse_button == MouseButton::Right) + { + _rmb_down = true; + input.set_cursor_mode(CursorMode::Relative); + } + else if (e.type == InputEvent::Type::MouseButtonUp && e.mouse_button == MouseButton::Right) + { + _rmb_down = false; + input.set_cursor_mode(CursorMode::Normal); + } + else if (e.type == InputEvent::Type::MouseMove && _rmb_down) + { + float dx = e.mouse_delta.x * _settings.look_sensitivity; + float dy = e.mouse_delta.y * _settings.look_sensitivity; + + _settings.yaw += dx; + _settings.pitch += dy; + + const float limit = glm::half_pi() - 0.01f; + _settings.pitch = std::clamp(_settings.pitch, -limit, limit); + } + else if (e.type == InputEvent::Type::MouseWheel) + { + const float steps = e.wheel_delta.y; // positive = wheel up + if (std::abs(steps) < 0.001f) + { + continue; + } + + if (e.mods.ctrl) + { + camera.fovDegrees -= steps * 2.0f; + camera.fovDegrees = std::clamp(camera.fovDegrees, 30.0f, 110.0f); + } + else + { + const double factor = std::pow(1.15, -static_cast(steps)); + _settings.distance = std::clamp(_settings.distance * factor, 0.2, 100000.0); + } + } + } + + if (_rmb_down && !st.mouse_down(MouseButton::Right)) + { + _rmb_down = false; + input.set_cursor_mode(CursorMode::Normal); + } +} + +void OrbitCameraMode::update(SceneManager &scene, Camera &camera, float /*dt*/) +{ + WorldVec3 target_pos{}; + glm::quat target_rot{}; + if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot)) + { + return; + } + + auto wrap_pi = [](float a) -> float { + // Wrap angle to [-pi, pi] to avoid precision issues over long play sessions. + constexpr float two_pi = glm::two_pi(); + a = std::fmod(a + glm::pi(), two_pi); + if (a < 0.0f) a += two_pi; + return a - glm::pi(); + }; + + float yaw = wrap_pi(_settings.yaw); + float pitch = std::clamp(_settings.pitch, + -glm::half_pi() + 0.01f, + glm::half_pi() - 0.01f); + _settings.yaw = yaw; + _settings.pitch = pitch; + double dist = std::max(0.2, _settings.distance); + + glm::quat yaw_q = glm::angleAxis(yaw, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 right = glm::rotate(yaw_q, glm::vec3(1.0f, 0.0f, 0.0f)); + glm::quat pitch_q = glm::angleAxis(pitch, right); + glm::quat orbit_q = glm::normalize(pitch_q * yaw_q); + + // Place the camera on its local +Z axis relative to the target so the camera's + // -Z forward axis points toward the target. + const double yaw_d = static_cast(yaw); + const double pitch_d = static_cast(pitch); + const double cos_pitch = std::cos(pitch_d); + const double sin_pitch = std::sin(pitch_d); + const double sin_yaw = std::sin(yaw_d); + const double cos_yaw = std::cos(yaw_d); + + const glm::dvec3 dir_target_to_camera( + sin_yaw * cos_pitch, + -sin_pitch, + cos_yaw * cos_pitch); + + camera.position_world = target_pos + dir_target_to_camera * dist; + camera.orientation = orbit_q; +} diff --git a/src/scene/camera/mode_orbit.h b/src/scene/camera/mode_orbit.h new file mode 100644 index 0000000..94f54c0 --- /dev/null +++ b/src/scene/camera/mode_orbit.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +struct OrbitCameraSettings; + +class OrbitCameraMode : public ICameraMode +{ +public: + explicit OrbitCameraMode(OrbitCameraSettings &settings); + ~OrbitCameraMode() override = default; + + const char *name() const override { return "Orbit"; } + void on_activate(SceneManager &scene, Camera &camera) override; + void process_input(SceneManager &scene, + Camera &camera, + InputSystem &input, + bool ui_capture_keyboard, + bool ui_capture_mouse) override; + void update(SceneManager &scene, Camera &camera, float dt) override; + +private: + OrbitCameraSettings &_settings; + bool _rmb_down = false; +}; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index e5d2097..5f89d72 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -111,7 +111,6 @@ void SceneManager::init(EngineContext *context) { _context = context; - mainCamera.velocity = glm::vec3(0.f); mainCamera.position_world = WorldVec3(30.0, 0.0, 85.0); mainCamera.orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); @@ -119,6 +118,8 @@ void SceneManager::init(EngineContext *context) sceneData.sunlightDirection = glm::vec4(-0.2f, -1.0f, -0.3f, 1.0f); sceneData.sunlightColor = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f); + cameraRig.init(*this, mainCamera); + _camera_position_local = world_to_local(mainCamera.position_world, _origin_world); } @@ -151,7 +152,25 @@ void SceneManager::update_scene() mainDrawContext.nextID = 1; mainDrawContext.gltfNodeLocalOverrides = nullptr; - mainCamera.update(); + // Simple per-frame dt (seconds) for animations and camera behavior. + auto now = std::chrono::steady_clock::now(); + if (_lastFrameTime.time_since_epoch().count() == 0) + { + _lastFrameTime = now; + } + float dt = std::chrono::duration(now - _lastFrameTime).count(); + _lastFrameTime = now; + if (dt < 0.f) + { + dt = 0.f; + } + if (dt > 0.1f) + { + dt = 0.1f; + } + _deltaTime = dt; + + cameraRig.update(*this, mainCamera, dt); // Floating origin: keep render-local coordinates near (0,0,0) by shifting the origin // when the camera drifts too far in world space. @@ -171,24 +190,6 @@ void SceneManager::update_scene() _camera_position_local = world_to_local(mainCamera.position_world, _origin_world); - // Simple per-frame dt (seconds) for animations - auto now = std::chrono::steady_clock::now(); - if (_lastFrameTime.time_since_epoch().count() == 0) - { - _lastFrameTime = now; - } - float dt = std::chrono::duration(now - _lastFrameTime).count(); - _lastFrameTime = now; - if (dt < 0.f) - { - dt = 0.f; - } - if (dt > 0.1f) - { - dt = 0.1f; - } - _deltaTime = dt; - auto tagOwner = [&](RenderObject::OwnerType type, const std::string &name, size_t opaqueBegin, size_t transpBegin) { diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index f12018f..8ef42d5 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +69,8 @@ public: void update_scene(); Camera &getMainCamera() { return mainCamera; } + CameraRig &getCameraRig() { return cameraRig; } + const CameraRig &getCameraRig() const { return cameraRig; } WorldVec3 get_world_origin() const { return _origin_world; } glm::vec3 get_camera_local_position() const { return _camera_position_local; } @@ -227,6 +230,7 @@ private: EngineContext *_context = nullptr; Camera mainCamera = {}; + CameraRig cameraRig{}; GPUSceneData sceneData = {}; DrawContext mainDrawContext; std::vector pointLights;