feat: add imguizmo + trigger buxfix

This commit is contained in:
2026-10-18 11:45:37 +02:00
parent 70c940c476
commit 4bd99b25d1
37 changed files with 700 additions and 58 deletions
+2
View File
@@ -112,6 +112,7 @@ set(SRC_FILES
"src/Util/DeltaTime.cpp"
"src/Util/ImGuiUtils.cpp"
"src/Util/TransformGizmo.cpp"
"src/Physics/PhysicsWorld.cpp"
"src/Physics/SimplePhysicsWorld.cpp"
@@ -150,6 +151,7 @@ target_link_libraries(destrum
tinygltf
assimp
imgui
imguizmo
Jolt
Tracy::TracyClient
@@ -13,6 +13,8 @@ public:
~Collider() override = default;
[[nodiscard]] bool AffectsPhysics() const override { return true; }
void ImGuiInspector() override;
[[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0;
@@ -15,6 +15,8 @@ public:
~Rigidbody() override;
[[nodiscard]] bool AffectsPhysics() const override { return true; }
void Update(float) override {}
void Start() override;
void Destroy() override;
@@ -126,6 +126,9 @@ public:
void SetMovementSpeed(float speed) { m_movementSpeed = speed; }
[[nodiscard]] float GetMovementSpeed() const { return m_movementSpeed; }
void SetMouseSensitivity(float sensitivity) { m_mouseSensitivity = sensitivity; }
[[nodiscard]] float GetMouseSensitivity() const { return m_mouseSensitivity; }
void SetRotation(float yawRadians, float pitchRadians);
void SetRotation(const glm::vec2& yawPitchRadians);
void SetOrientation(const glm::vec3& forward, const glm::vec3& up);
@@ -168,6 +171,7 @@ private:
Frustum m_frustum{};
float m_movementSpeed{2.0f};
float m_mouseSensitivity{0.003f};
float m_yaw = 0.0f; // radians
float m_pitch = 0.0f; // radians
+1 -1
View File
@@ -63,7 +63,7 @@ public:
void init(SDL_Window* window, const std::string& appName, bool vSync);
void cleanup();
void recreateSwapchain(int width, int height);
[[nodiscard]] bool recreateSwapchain(int width, int height);
VkCommandBuffer beginFrame();
+1 -1
View File
@@ -34,7 +34,7 @@ public:
std::uint32_t height,
bool vSync);
void recreateSwapchain(
[[nodiscard]] bool recreateSwapchain(
VkDevice device,
vkb::Device vkbDevice,
VkSurfaceKHR surface,
@@ -33,6 +33,8 @@ public:
virtual void Start();
virtual void SetEnabled(bool enabled);
[[nodiscard]] virtual bool AffectsPhysics() const { return false; }
// Reason for keeping No DT function -> i am tired and lazy
// If im feeling frisky im gonna claim backwards compatibility
virtual void Update(float dt);
@@ -28,6 +28,7 @@ public:
void CleanupComponents();
void RefreshPhysics();
void RefreshPhysicsInHierarchy();
void SetActive(bool active) {
if (active == m_Active) {
@@ -36,7 +37,7 @@ public:
m_Active = active;
SetActiveDirty();
RefreshPhysics();
RefreshPhysicsInHierarchy();
}
[[nodiscard]] bool IsActive() const { return m_Active; }
@@ -76,7 +77,9 @@ public:
auto& addedComponent = m_Components.emplace_back(
std::make_unique<TComponent>(*this, std::forward<Args>(args)...));
RefreshPhysics();
if (addedComponent->AffectsPhysics()) {
RefreshPhysics();
}
return static_cast<TComponent*>(addedComponent.get());
}
+1
View File
@@ -65,6 +65,7 @@ public:
return m_objects;
}
[[nodiscard]] GameObject* FindGameObject(ObjectId id) const;
[[nodiscard]] GameObject* GetSelectedObject() const { return FindGameObject(m_SelectedObjectId); }
void SetRegisterBindings(std::function<void()> registerBindings) {
m_registerBindings = std::move(registerBindings);
@@ -0,0 +1,35 @@
#ifndef DESTRUM_TRANSFORMGIZMO_H
#define DESTRUM_TRANSFORMGIZMO_H
class Camera;
class GameObject;
enum class GizmoOperation {
Translate,
Rotate,
Scale,
};
enum class GizmoSpace {
World,
Local,
};
class TransformGizmo final {
public:
void HandleShortcuts();
void DrawToolbar();
bool Manipulate(GameObject& object, const Camera& camera);
[[nodiscard]] bool IsUsing() const;
[[nodiscard]] bool IsOver() const;
GizmoOperation operation{GizmoOperation::Translate};
GizmoSpace space{GizmoSpace::World};
bool snap{false};
float translationSnap{0.5f};
float rotationSnapDegrees{15.0f};
float scaleSnap{0.1f};
};
#endif // DESTRUM_TRANSFORMGIZMO_H
+13 -3
View File
@@ -170,6 +170,14 @@ void App::run()
if (!isRunning) break;
if (window.IsMinimized())
{
// The surface is 0x0 while minimized, there is nothing to render or present to
accumulator = 0.0f;
std::this_thread::sleep_for(std::chrono::milliseconds(16));
continue;
}
// Consume SDL events before updating the base camera so held and
// newly pressed inputs are applied in the same frame.
camera.Update(dt);
@@ -290,11 +298,13 @@ if (!isRunning) break;
continue;
}
spdlog::info("Recreating swapchain to size: {}x{}", w, h);
{
ZoneScopedN("Recreate Swapchain");
gfxDevice.recreateSwapchain(w, h);
if (!gfxDevice.recreateSwapchain(w, h))
{
continue;
}
spdlog::info("Recreated swapchain at {}x{}", w, h);
imguiPass.onSwapchainRecreated();
onWindowResize(w, h);
}
+5
View File
@@ -33,6 +33,11 @@ void Camera::Update(float deltaTime) {
if (input.IsKeyDown(SDL_SCANCODE_LEFT)) m_yaw -= keyLookSpeed * deltaTime;
if (input.IsKeyDown(SDL_SCANCODE_RIGHT)) m_yaw += keyLookSpeed * deltaTime;
if (input.IsMouseDown(SDL_BUTTON_RIGHT)) {
m_yaw += static_cast<float>(input.MouseDeltaX()) * m_mouseSensitivity;
m_pitch -= static_cast<float>(input.MouseDeltaY()) * m_mouseSensitivity;
}
if (pad0 >= 0) {
const float rx = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_RIGHTX);
const float ry = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_RIGHTY);
+3 -2
View File
@@ -96,7 +96,7 @@ void GfxDevice::cleanup() {
initialized = false;
}
void GfxDevice::recreateSwapchain(int width, int height) {
bool GfxDevice::recreateSwapchain(int width, int height) {
if (!initialized) {
throw std::logic_error("GfxDevice::recreateSwapchain called before init");
}
@@ -105,7 +105,7 @@ void GfxDevice::recreateSwapchain(int width, int height) {
}
waitIdle();
swapchain.recreateSwapchain(
const bool recreated = swapchain.recreateSwapchain(
instanceManager.getDevice(),
instanceManager.getVkbDevice(),
instanceManager.getSurface(),
@@ -115,6 +115,7 @@ void GfxDevice::recreateSwapchain(int width, int height) {
m_vSync);
swapchainFormat = swapchain.getFormat();
return recreated;
}
VkCommandBuffer GfxDevice::beginFrame()
+12 -2
View File
@@ -107,7 +107,7 @@ void Swapchain::createSwapchain(
dirty = false;
}
void Swapchain::recreateSwapchain(
bool Swapchain::recreateSwapchain(
VkDevice,
vkb::Device vkbDevice,
VkSurfaceKHR surf,
@@ -120,7 +120,15 @@ void Swapchain::recreateSwapchain(
if (width == 0 || height == 0) {
dirty = true;
return;
return false;
}
// A minimized window can still report its old size while the surface is 0x0
VkSurfaceCapabilitiesKHR capabilities{};
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vkbDevice.physical_device, surf, &capabilities) == VK_SUCCESS &&
(capabilities.currentExtent.width == 0 || capabilities.currentExtent.height == 0)) {
dirty = true;
return false;
}
surface = surf;
@@ -209,6 +217,8 @@ void Swapchain::recreateSwapchain(
extent = m_swapchain.extent;
dirty = false;
return true;
}
void Swapchain::cleanup(VkDevice device) {
+7 -8
View File
@@ -16,7 +16,9 @@ Transform& Component::GetTransform() const {
void Component::Destroy() {
Object::Destroy();
NotifyPhysicsChanged();
if (AffectsPhysics()) {
NotifyPhysicsChanged();
}
}
void Component::Start() {
@@ -24,17 +26,14 @@ void Component::Start() {
void Component::SetEnabled(bool enabled) {
m_IsEnabled = enabled;
NotifyPhysicsChanged();
if (AffectsPhysics()) {
NotifyPhysicsChanged();
}
}
void Component::NotifyPhysicsChanged() {
if (m_ParentGameObjectPtr != nullptr) {
if (auto* rigidbody = m_ParentGameObjectPtr->GetComponent<Rigidbody>();
rigidbody != nullptr && rigidbody->GetPhysicsWorld() != nullptr) {
rigidbody->GetPhysicsWorld()->RefreshRigidbody(*rigidbody);
} else {
m_ParentGameObjectPtr->RefreshPhysics();
}
m_ParentGameObjectPtr->RefreshPhysics();
}
}
+36 -3
View File
@@ -196,8 +196,41 @@ void GameObject::CleanupComponents() {
});
}
void GameObject::RefreshPhysics() {
if (m_Scene != nullptr) {
m_Scene->RefreshPhysics();
namespace {
void RefreshBody(GameObject& object, Scene* scene) {
auto* rigidbody = object.GetComponent<Rigidbody>();
if (rigidbody == nullptr || rigidbody->IsBeingDestroyed()) {
return;
}
if (PhysicsWorld* world = rigidbody->GetPhysicsWorld()) {
world->RefreshRigidbody(*rigidbody);
} else if (scene != nullptr) {
scene->GetPhysics().RefreshGameObject(object);
}
}
void RefreshChildBodies(const Transform& transform, Scene* scene) {
for (Transform* child : transform.GetChildren()) {
if (GameObject* owner = child->GetOwner()) {
RefreshBody(*owner, scene);
}
RefreshChildBodies(*child, scene);
}
}
}
void GameObject::RefreshPhysics() {
for (Transform* transform = &m_TransformPtr; transform != nullptr; transform = transform->GetParent()) {
GameObject* owner = transform->GetOwner();
if (owner != nullptr && owner->GetComponent<Rigidbody>() != nullptr) {
RefreshBody(*owner, m_Scene);
return;
}
}
}
void GameObject::RefreshPhysicsInHierarchy() {
RefreshPhysics();
RefreshChildBodies(m_TransformPtr, m_Scene);
}
+1 -1
View File
@@ -242,7 +242,7 @@ void Transform::SetParent(Transform* parent, bool useWorldPosition) {
if (m_Owner != nullptr) {
m_Owner->SetActiveDirty();
m_Owner->RefreshPhysics();
m_Owner->RefreshPhysicsInHierarchy();
}
try {
+4 -3
View File
@@ -362,10 +362,11 @@ namespace
m_Overlaps.clear();
}
std::unordered_map<BodyPair, PairData, PairHash> SwapOverlaps()
// Persisted contacts are not reported again, so the set has to outlive each step
std::unordered_map<BodyPair, PairData, PairHash> SnapshotOverlaps()
{
std::lock_guard<std::mutex> lock(m_Mutex);
return std::move(m_Overlaps);
return m_Overlaps;
}
private:
@@ -752,7 +753,7 @@ private:
void ProcessSensorOverlaps()
{
const auto currentOverlaps = m_SensorListener.SwapOverlaps();
auto currentOverlaps = m_SensorListener.SnapshotOverlaps();
for (const auto& [pair, data] : currentOverlaps)
{
+1 -1
View File
@@ -277,7 +277,7 @@ namespace ImGuiUtils {
}
if (transformChanged) {
selectedObject->RefreshPhysics();
selectedObject->RefreshPhysicsInHierarchy();
}
const glm::vec3& worldPosition = transform.GetWorldPosition();
+166
View File
@@ -0,0 +1,166 @@
#include <destrum/Util/TransformGizmo.h>
#include <destrum/Graphics/Camera.h>
#include <destrum/ObjectModel/GameObject.h>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/matrix_decompose.hpp>
#include <imgui.h>
#include <ImGuizmo.h>
namespace {
ImGuizmo::OPERATION toImGuizmo(GizmoOperation operation)
{
switch (operation) {
case GizmoOperation::Rotate:
return ImGuizmo::ROTATE;
case GizmoOperation::Scale:
return ImGuizmo::SCALE;
case GizmoOperation::Translate:
break;
}
return ImGuizmo::TRANSLATE;
}
}
void TransformGizmo::HandleShortcuts()
{
if (ImGui::GetIO().WantTextInput || ImGuizmo::IsUsing()) {
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_T, false)) {
operation = GizmoOperation::Translate;
}
if (ImGui::IsKeyPressed(ImGuiKey_R, false)) {
operation = GizmoOperation::Rotate;
}
if (ImGui::IsKeyPressed(ImGuiKey_Y, false)) {
operation = GizmoOperation::Scale;
}
if (ImGui::IsKeyPressed(ImGuiKey_L, false)) {
space = space == GizmoSpace::World ? GizmoSpace::Local : GizmoSpace::World;
}
}
void TransformGizmo::DrawToolbar()
{
if (!ImGui::Begin("Gizmo", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::End();
return;
}
const auto operationButton = [this](const char* label, GizmoOperation buttonOperation) {
if (ImGui::RadioButton(label, operation == buttonOperation)) {
operation = buttonOperation;
}
};
operationButton("Move (T)", GizmoOperation::Translate);
ImGui::SameLine();
operationButton("Rotate (R)", GizmoOperation::Rotate);
ImGui::SameLine();
operationButton("Scale (Y)", GizmoOperation::Scale);
if (operation != GizmoOperation::Scale) {
bool local = space == GizmoSpace::Local;
if (ImGui::Checkbox("Local (L)", &local)) {
space = local ? GizmoSpace::Local : GizmoSpace::World;
}
ImGui::SameLine();
}
ImGui::Checkbox("Snap", &snap);
if (snap) {
ImGui::SetNextItemWidth(80.0f);
switch (operation) {
case GizmoOperation::Translate:
ImGui::DragFloat("Step", &translationSnap, 0.05f, 0.01f, 10.0f, "%.2f m");
break;
case GizmoOperation::Rotate:
ImGui::DragFloat("Step", &rotationSnapDegrees, 1.0f, 1.0f, 90.0f, "%.0f deg");
break;
case GizmoOperation::Scale:
ImGui::DragFloat("Step", &scaleSnap, 0.01f, 0.01f, 1.0f, "%.2f");
break;
}
}
ImGui::End();
}
bool TransformGizmo::Manipulate(GameObject& object, const Camera& camera)
{
ImGuizmo::BeginFrame();
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGuizmo::SetOrthographic(false);
ImGuizmo::SetRect(viewport->Pos.x, viewport->Pos.y, viewport->Size.x, viewport->Size.y);
const glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = camera.GetProjectionMatrix();
// ImGuizmo expects OpenGL's y up clip space, the engine flips y for Vulkan
projection[1][1] *= -1.0f;
Transform& transform = object.GetTransform();
glm::mat4 worldMatrix = transform.GetWorldMatrix();
const float snapStep = operation == GizmoOperation::Translate ? translationSnap
: operation == GizmoOperation::Rotate ? rotationSnapDegrees
: scaleSnap;
const float snapValues[3] = {snapStep, snapStep, snapStep};
const ImGuizmo::MODE mode =
operation == GizmoOperation::Scale || space == GizmoSpace::Local ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
const bool changed = ImGuizmo::Manipulate(
glm::value_ptr(view),
glm::value_ptr(projection),
toImGuizmo(operation),
mode,
glm::value_ptr(worldMatrix),
nullptr,
snap ? snapValues : nullptr);
// Stops the click on a handle from reaching the game, e.g. grabbing the mouse for first person
if (ImGuizmo::IsOver() || ImGuizmo::IsUsing()) {
ImGui::SetNextFrameWantCaptureMouse(true);
}
if (!changed) {
return false;
}
glm::vec3 position;
glm::quat rotation;
glm::vec3 scale;
glm::vec3 skew;
glm::vec4 perspective;
if (!glm::decompose(worldMatrix, scale, rotation, position, skew, perspective)) {
return false;
}
switch (operation) {
case GizmoOperation::Translate:
transform.SetWorldPosition(position);
break;
case GizmoOperation::Rotate:
transform.SetWorldRotation(rotation);
break;
case GizmoOperation::Scale:
transform.SetWorldScale(scale);
break;
}
object.RefreshPhysicsInHierarchy();
return true;
}
bool TransformGizmo::IsUsing() const
{
return ImGuizmo::IsUsing();
}
bool TransformGizmo::IsOver() const
{
return ImGuizmo::IsOver();
}
+4
View File
@@ -119,6 +119,10 @@ target_link_libraries(imgui PUBLIC
volk
)
# ImGuizmo
add_subdirectory(imguizmo)
target_link_libraries(imguizmo PUBLIC imgui)
# Jolt Physics
set(DOUBLE_PRECISION OFF CACHE BOOL "" FORCE)
set(GENERATE_DEBUG_SYMBOLS ON CACHE BOOL "" FORCE)