From ca5b5b305292aefd5710088bd92edd797f89a002 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Wed, 22 Apr 2026 17:27:40 +0300 Subject: [PATCH] Buoyancy --- src/features/editScene/CMakeLists.txt | 10 + src/features/editScene/EditorApp.cpp | 69 +++ src/features/editScene/EditorApp.hpp | 10 + .../editScene/camera/EditorCamera.hpp | 23 +- .../editScene/components/BuoyancyInfo.hpp | 46 ++ .../components/BuoyancyInfoModule.cpp | 23 + .../editScene/components/WaterPhysics.hpp | 43 ++ .../components/WaterPhysicsModule.cpp | 22 + .../editScene/components/WaterPlane.hpp | 63 +++ .../editScene/components/WaterPlaneModule.cpp | 266 ++++++++++ src/features/editScene/main.cpp | 20 +- src/features/editScene/physics/physics.cpp | 303 ++++++------ src/features/editScene/physics/physics.h | 25 +- .../editScene/systems/BuoyancySystem.cpp | 328 ++++++++++++ .../editScene/systems/BuoyancySystem.hpp | 98 ++++ .../editScene/systems/CharacterSystem.cpp | 64 +-- .../editScene/systems/EditorUISystem.cpp | 467 ++++++++++++------ .../editScene/systems/EditorUISystem.hpp | 85 +++- .../editScene/systems/PhysicsSystem.hpp | 37 +- .../editScene/systems/SceneSerializer.cpp | 91 ++++ .../editScene/systems/SceneSerializer.hpp | 120 +++-- .../editScene/ui/BuoyancyInfoEditor.cpp | 103 ++++ .../editScene/ui/BuoyancyInfoEditor.hpp | 21 + .../editScene/ui/WaterPhysicsEditor.hpp | 110 +++++ .../editScene/ui/WaterPlaneEditor.hpp | 101 ++++ 25 files changed, 2122 insertions(+), 426 deletions(-) create mode 100644 src/features/editScene/components/BuoyancyInfo.hpp create mode 100644 src/features/editScene/components/BuoyancyInfoModule.cpp create mode 100644 src/features/editScene/components/WaterPhysics.hpp create mode 100644 src/features/editScene/components/WaterPhysicsModule.cpp create mode 100644 src/features/editScene/components/WaterPlane.hpp create mode 100644 src/features/editScene/components/WaterPlaneModule.cpp create mode 100644 src/features/editScene/systems/BuoyancySystem.cpp create mode 100644 src/features/editScene/systems/BuoyancySystem.hpp create mode 100644 src/features/editScene/ui/BuoyancyInfoEditor.cpp create mode 100644 src/features/editScene/ui/BuoyancyInfoEditor.hpp create mode 100644 src/features/editScene/ui/WaterPhysicsEditor.hpp create mode 100644 src/features/editScene/ui/WaterPlaneEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index e0dbf15..4a64861 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -15,6 +15,7 @@ set(EDITSCENE_SOURCES systems/EditorUISystem.cpp systems/SceneSerializer.cpp systems/PhysicsSystem.cpp + systems/BuoyancySystem.cpp systems/LightSystem.cpp systems/CameraSystem.cpp systems/LodSystem.cpp @@ -59,6 +60,7 @@ set(EDITSCENE_SOURCES ui/FurnitureTemplateEditor.cpp ui/StartupMenuEditor.cpp ui/PlayerControllerEditor.cpp + ui/BuoyancyInfoEditor.cpp ui/ComponentRegistration.cpp components/LightModule.cpp components/CameraModule.cpp @@ -77,6 +79,9 @@ set(EDITSCENE_SOURCES components/CellGrid.cpp components/StartupMenuModule.cpp components/PlayerControllerModule.cpp + components/BuoyancyInfoModule.cpp + components/WaterPhysicsModule.cpp + components/WaterPlaneModule.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp physics/physics.cpp @@ -90,6 +95,9 @@ set(EDITSCENE_HEADERS components/Relationship.hpp components/PhysicsCollider.hpp components/RigidBody.hpp + components/BuoyancyInfo.hpp + components/WaterPhysics.hpp + components/WaterPlane.hpp components/Light.hpp components/Camera.hpp components/Lod.hpp @@ -121,6 +129,7 @@ set(EDITSCENE_HEADERS systems/StaticGeometrySystem.hpp systems/SceneSerializer.hpp systems/PhysicsSystem.hpp + systems/BuoyancySystem.hpp systems/LightSystem.hpp systems/CameraSystem.hpp systems/LodSystem.hpp @@ -156,6 +165,7 @@ set(EDITSCENE_HEADERS ui/FurnitureTemplateEditor.hpp ui/StartupMenuEditor.hpp ui/PlayerControllerEditor.hpp + ui/BuoyancyInfoEditor.hpp camera/EditorCamera.hpp gizmo/Gizmo.hpp physics/physics.h diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 8817306..eda5799 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -2,6 +2,7 @@ #include "EditorApp.hpp" #include "systems/EditorUISystem.hpp" #include "systems/PhysicsSystem.hpp" +#include "systems/BuoyancySystem.hpp" #include "systems/LightSystem.hpp" #include "systems/CameraSystem.hpp" #include "systems/LodSystem.hpp" @@ -25,6 +26,9 @@ #include "components/PhysicsCollider.hpp" #include "components/RigidBody.hpp" #include "components/GeneratedPhysicsTag.hpp" +#include "components/BuoyancyInfo.hpp" +#include "components/WaterPhysics.hpp" +#include "components/WaterPlane.hpp" #include "components/Light.hpp" #include "components/Camera.hpp" #include "components/Lod.hpp" @@ -219,6 +223,22 @@ void EditorApp::setup() m_physicsSystem->initialize(); m_uiSystem->setPhysicsSystem(m_physicsSystem.get()); + // Setup buoyancy system (requires physics system) + // Get the physics wrapper from the physics system + m_buoyancySystem = std::make_unique( + m_world, m_physicsSystem->getPhysicsWrapper()); + m_buoyancySystem->initialize(); + + // Apply debug setting if it was set before system creation + if (m_debugBuoyancy) { + m_buoyancySystem->setDebugEnabled(true); + } + + // Set buoyancy system in UI system for configuration + if (m_uiSystem) { + m_uiSystem->setBuoyancySystem(m_buoyancySystem.get()); + } + // Setup light system m_lightSystem = std::make_unique(m_world, m_sceneMgr); @@ -355,6 +375,28 @@ void EditorApp::setGameMode(GameMode mode) } } +void EditorApp::setDebugBuoyancy(bool enabled) +{ + m_debugBuoyancy = enabled; + + // Apply debug setting to buoyancy system if it exists + if (m_buoyancySystem) { + m_buoyancySystem->setDebugEnabled(enabled); + } + + // Log the debug mode change only if OGRE is initialized + // (setDebugBuoyancy may be called before OGRE setup) + try { + if (Ogre::LogManager::getSingletonPtr()) { + Ogre::LogManager::getSingleton().logMessage( + "Buoyancy debug mode: " + + Ogre::StringConverter::toString(enabled)); + } + } catch (...) { + // Ignore if OGRE not initialized yet + } +} + void EditorApp::setGamePlayState(GamePlayState state) { m_gamePlayState = state; @@ -413,6 +455,9 @@ void EditorApp::setupECS() m_world.component(); m_world.component(); m_world.component(); + m_world.component(); + m_world.component(); + m_world.component(); // Register light and camera components m_world.component(); @@ -456,6 +501,20 @@ void EditorApp::setupECS() void EditorApp::createDefaultEntities() { // Start with empty scene - user creates entities as needed + + // Create global WaterPhysics component for buoyancy system + flecs::entity waterPhysicsEntity = m_world.entity("WaterPhysics"); + WaterPhysics waterPhysics; + waterPhysics.enabled = true; + waterPhysics.waterSurfaceY = + 1.0f; // Raise water surface to 1.0m above ground + waterPhysics.waterDensity = 1000.0f; // kg/m³ + waterPhysics.gravity = 9.81f; // m/s² + waterPhysics.defaultBuoyancy = 1.0f; + waterPhysics.defaultLinearDrag = 0.1f; + waterPhysics.defaultAngularDrag = 0.05f; + waterPhysics.defaultSubmergedThreshold = 0.3f; + waterPhysicsEntity.set(waterPhysics); } void EditorApp::setupLights() @@ -620,6 +679,16 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_physicsSystem->update(evt.timeSinceLastFrame); } + /* --- Buoyancy system (after physics) --- */ + if (m_buoyancySystem) { + // Update camera position for water detection area + if (m_camera) { + Ogre::Vector3 cameraPos = m_camera->getPosition(); + m_buoyancySystem->setCameraPosition(cameraPos); + } + m_buoyancySystem->update(evt.timeSinceLastFrame); + } + /* --- Rendering support systems --- */ if (m_lightSystem) { m_lightSystem->update(); diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index 4a76c74..f3f2cb4 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -28,6 +28,7 @@ class CellGridSystem; class RoomLayoutSystem; class StartupMenuSystem; class PlayerControllerSystem; +class BuoyancySystem; class EditorApp; /** @@ -127,6 +128,13 @@ public: { return m_gameMode; } + + // Debug buoyancy + void setDebugBuoyancy(bool enabled); + bool getDebugBuoyancy() const + { + return m_debugBuoyancy; + } GamePlayState getGamePlayState() const { return m_gamePlayState; @@ -187,6 +195,7 @@ private: std::unique_ptr m_camera; std::unique_ptr m_imguiListener; std::unique_ptr m_physicsSystem; + std::unique_ptr m_buoyancySystem; std::unique_ptr m_lightSystem; std::unique_ptr m_cameraSystem; std::unique_ptr m_lodSystem; @@ -210,6 +219,7 @@ private: GamePlayState m_gamePlayState = GamePlayState::Menu; GameInputState m_gameInput; bool m_setupComplete = false; + bool m_debugBuoyancy = false; // Editor visualization nodes Ogre::SceneNode *m_gridNode = nullptr; diff --git a/src/features/editScene/camera/EditorCamera.hpp b/src/features/editScene/camera/EditorCamera.hpp index 1bb241e..70edcc2 100644 --- a/src/features/editScene/camera/EditorCamera.hpp +++ b/src/features/editScene/camera/EditorCamera.hpp @@ -15,8 +15,7 @@ */ class EditorCamera { public: - EditorCamera(Ogre::SceneManager *sceneMgr, - Ogre::RenderWindow *window); + EditorCamera(Ogre::SceneManager *sceneMgr, Ogre::RenderWindow *window); ~EditorCamera(); /** @@ -35,7 +34,10 @@ public: /** * Get the camera */ - Ogre::Camera *getCamera() const { return m_camera; } + Ogre::Camera *getCamera() const + { + return m_camera; + } /** * Focus camera on a point @@ -47,6 +49,14 @@ public: */ void setPosition(const Ogre::Vector3 &pos); + /** + * Get camera position + */ + Ogre::Vector3 getPosition() const + { + return m_position; + } + /** * Get ray from mouse position */ @@ -55,7 +65,10 @@ public: /** * Check if in FPS mode */ - bool isFPSMode() const { return m_fpsMode; } + bool isFPSMode() const + { + return m_fpsMode; + } private: void updateCameraPosition(); @@ -65,7 +78,7 @@ private: Ogre::Camera *m_camera; Ogre::SceneNode *m_cameraNode; Ogre::SceneNode *m_targetNode; - + // Use OgreBites::CameraMan for proper camera control std::unique_ptr m_cameraMan; diff --git a/src/features/editScene/components/BuoyancyInfo.hpp b/src/features/editScene/components/BuoyancyInfo.hpp new file mode 100644 index 0000000..cd075e2 --- /dev/null +++ b/src/features/editScene/components/BuoyancyInfo.hpp @@ -0,0 +1,46 @@ +#ifndef EDITSCENE_BUOYANCYINFO_HPP +#define EDITSCENE_BUOYANCYINFO_HPP +#pragma once + +#include + +/** + * BuoyancyInfo component + * Provides per-entity buoyancy settings for water physics + * If an entity has this component, it will use these settings + * Otherwise, default settings from the buoyancy system will be used + */ +struct BuoyancyInfo { + // Enable/disable buoyancy for this entity + bool enabled = true; + + // Buoyancy strength (0 = no buoyancy, 1 = neutral buoyancy, >1 = floats) + float buoyancy = 1.0f; + + // Linear drag when submerged (0 = no drag, 1 = full drag) + float linearDrag = 0.1f; + + // Angular drag when submerged (0 = no drag, 1 = full drag) + float angularDrag = 0.05f; + + // Water surface Y level for this entity (world space) + // If not set (0), uses global water level from buoyancy system + float waterSurfaceY = 0.0f; + + // Submergedness threshold (0-1) - how much of the body must be submerged + // before buoyancy is applied (0 = any contact, 1 = fully submerged) + float submergedThreshold = 0.3f; + + // Use custom water surface level (if false, uses global water level) + bool useCustomWaterLevel = false; + + // Mark component as dirty (needs update) + bool dirty = true; + + void markDirty() + { + dirty = true; + } +}; + +#endif // EDITSCENE_BUOYANCYINFO_HPP \ No newline at end of file diff --git a/src/features/editScene/components/BuoyancyInfoModule.cpp b/src/features/editScene/components/BuoyancyInfoModule.cpp new file mode 100644 index 0000000..c69b23c --- /dev/null +++ b/src/features/editScene/components/BuoyancyInfoModule.cpp @@ -0,0 +1,23 @@ +#include "BuoyancyInfo.hpp" +#include "Transform.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/BuoyancyInfoEditor.hpp" + +// Register BuoyancyInfo component +REGISTER_COMPONENT("Buoyancy Info", BuoyancyInfo, BuoyancyInfoEditor) +{ + registry.registerComponent( + "Buoyancy Info", std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); +} \ No newline at end of file diff --git a/src/features/editScene/components/WaterPhysics.hpp b/src/features/editScene/components/WaterPhysics.hpp new file mode 100644 index 0000000..a62c89a --- /dev/null +++ b/src/features/editScene/components/WaterPhysics.hpp @@ -0,0 +1,43 @@ +#ifndef WATER_PHYSICS_HPP +#define WATER_PHYSICS_HPP + +#include + +struct WaterPhysics { + // Water surface Y level (world space) + float waterSurfaceY = -0.1f; + + // Default buoyancy parameters (used when entity has no BuoyancyInfo) + float defaultBuoyancy = 1.0f; // 1.0 = neutral buoyancy + float defaultLinearDrag = 0.1f; + float defaultAngularDrag = 0.05f; + float defaultSubmergedThreshold = + 0.1f; // Minimum submerged fraction to apply buoyancy + + // Water density (kg/m³) + float waterDensity = 1000.0f; + + // Gravity acceleration (m/s²) + float gravity = 9.81f; + + // Enable/disable water physics globally + bool enabled = true; + + WaterPhysics() = default; + + WaterPhysics(float surfaceY, float buoyancy, float linearDrag, + float angularDrag, float submergedThreshold, float density, + float grav, bool enable) + : waterSurfaceY(surfaceY) + , defaultBuoyancy(buoyancy) + , defaultLinearDrag(linearDrag) + , defaultAngularDrag(angularDrag) + , defaultSubmergedThreshold(submergedThreshold) + , waterDensity(density) + , gravity(grav) + , enabled(enable) + { + } +}; + +#endif // WATER_PHYSICS_HPP \ No newline at end of file diff --git a/src/features/editScene/components/WaterPhysicsModule.cpp b/src/features/editScene/components/WaterPhysicsModule.cpp new file mode 100644 index 0000000..7496f08 --- /dev/null +++ b/src/features/editScene/components/WaterPhysicsModule.cpp @@ -0,0 +1,22 @@ +#include "WaterPhysics.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/WaterPhysicsEditor.hpp" + +// Register WaterPhysics component +REGISTER_COMPONENT("Water Physics", WaterPhysics, WaterPhysicsEditor) +{ + registry.registerComponent( + "Water Physics", std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); +} diff --git a/src/features/editScene/components/WaterPlane.hpp b/src/features/editScene/components/WaterPlane.hpp new file mode 100644 index 0000000..51ef08c --- /dev/null +++ b/src/features/editScene/components/WaterPlane.hpp @@ -0,0 +1,63 @@ +#ifndef EDITSCENE_WATERPLANE_HPP +#define EDITSCENE_WATERPLANE_HPP +#pragma once + +#include + +/** + * WaterPlane component + * Provides visual representation of water surface for buoyancy system + * Creates a plane mesh at the water surface Y level for visualization + */ +struct WaterPlane { + // Enable/disable water plane visualization + bool enabled = true; + + // Water surface Y level (world space) - should match WaterPhysics::waterSurfaceY + float waterSurfaceY = -0.1f; + + // Plane size (width and depth) + float planeSize = 100.0f; + + // Material name for water plane + Ogre::String materialName = "BaseWhiteNoLighting"; + + // Opacity (0.0 = transparent, 1.0 = opaque) + float opacity = 0.3f; + + // Color tint + Ogre::ColourValue color = Ogre::ColourValue(0.2f, 0.4f, 0.8f, 0.3f); + + // Whether to update automatically from WaterPhysics component + bool autoUpdateFromWaterPhysics = true; + + // Scene node for the water plane (managed by system) + Ogre::SceneNode *sceneNode = nullptr; + + // Manual object for the water plane (managed by system) + Ogre::ManualObject *manualObject = nullptr; + + // Mark component as dirty (needs update) + bool dirty = true; + + void markDirty() + { + dirty = true; + } + + WaterPlane() = default; + + WaterPlane(float surfaceY, float size, const Ogre::String &material, + float opac, const Ogre::ColourValue &col, bool autoUpdate) + : waterSurfaceY(surfaceY) + , planeSize(size) + , materialName(material) + , opacity(opac) + , color(col) + , autoUpdateFromWaterPhysics(autoUpdate) + , dirty(true) + { + } +}; + +#endif // EDITSCENE_WATERPLANE_HPP \ No newline at end of file diff --git a/src/features/editScene/components/WaterPlaneModule.cpp b/src/features/editScene/components/WaterPlaneModule.cpp new file mode 100644 index 0000000..d5a536f --- /dev/null +++ b/src/features/editScene/components/WaterPlaneModule.cpp @@ -0,0 +1,266 @@ +#include "WaterPlane.hpp" +#include "../components/WaterPhysics.hpp" +#include "../components/EditorMarker.hpp" +#include "../components/EntityName.hpp" +#include "../components/Transform.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/WaterPlaneEditor.hpp" +#include +#include +#include +#include +#include +#include +#include + +// Register WaterPlane component +REGISTER_COMPONENT("Water Plane", WaterPlane, WaterPlaneEditor) +{ + registry.registerComponent( + "Water Plane", "Water", std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); +} + +/** + * WaterPlaneModule + * Manages WaterPlane components and creates visual water surface representations + */ +class WaterPlaneModule { +public: + WaterPlaneModule(flecs::world &world, Ogre::SceneManager *sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + { + // Register WaterPlane component + world.component(); + + // Create system for updating water planes + world.system("UpdateWaterPlanes") + .kind(flecs::OnUpdate) + .each([this](flecs::entity entity, + WaterPlane &waterPlane) { + updateWaterPlane(entity, waterPlane); + }); + + // Create system for cleaning up water planes when entities are destroyed + world.observer("CleanupWaterPlanes") + .event(flecs::OnRemove) + .each([this](flecs::entity entity, + WaterPlane &waterPlane) { + cleanupWaterPlane(waterPlane); + }); + + Ogre::LogManager::getSingleton().logMessage( + "WaterPlaneModule initialized"); + } + + ~WaterPlaneModule() + { + // Clean up any remaining water planes + m_world.each( + [this](flecs::entity entity, WaterPlane &waterPlane) { + cleanupWaterPlane(waterPlane); + }); + } + +private: + void updateWaterPlane(flecs::entity entity, WaterPlane &waterPlane) + { + // Skip if not enabled + if (!waterPlane.enabled) { + if (waterPlane.sceneNode) { + waterPlane.sceneNode->setVisible(false); + } + return; + } + + // Auto-update water surface Y from WaterPhysics if enabled + if (waterPlane.autoUpdateFromWaterPhysics) { + // Find WaterPhysics component in the world + m_world.query().each( + [&](flecs::entity wpEntity, + WaterPhysics &waterPhysics) { + if (waterPhysics.waterSurfaceY != + waterPlane.waterSurfaceY) { + waterPlane.waterSurfaceY = + waterPhysics + .waterSurfaceY; + waterPlane.markDirty(); + } + }); + } + + // Create or update water plane visualization + if (waterPlane.dirty || !waterPlane.sceneNode) { + createOrUpdateWaterPlane(entity, waterPlane); + waterPlane.dirty = false; + } + + // Ensure visibility + if (waterPlane.sceneNode) { + waterPlane.sceneNode->setVisible(true); + } + } + + void createOrUpdateWaterPlane(flecs::entity entity, + WaterPlane &waterPlane) + { + // Clean up existing water plane + cleanupWaterPlane(waterPlane); + + // Create scene node + Ogre::String nodeName = + "WaterPlaneNode_" + + Ogre::StringConverter::toString(entity.id()); + waterPlane.sceneNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode( + nodeName); + + // Create manual object + Ogre::String objName = + "WaterPlaneObject_" + + Ogre::StringConverter::toString(entity.id()); + waterPlane.manualObject = + m_sceneMgr->createManualObject(objName); + + // Create or get material for water plane + Ogre::String materialName = + "WaterPlaneMaterial_" + + Ogre::StringConverter::toString(entity.id()); + Ogre::MaterialPtr material = + Ogre::MaterialManager::getSingleton() + .createOrRetrieve( + materialName, + Ogre::ResourceGroupManager:: + DEFAULT_RESOURCE_GROUP_NAME) + .first.staticCast(); + + // Configure material for transparent water + material->setReceiveShadows(false); + material->getTechnique(0)->getPass(0)->setDiffuse( + waterPlane.color); + material->getTechnique(0)->getPass(0)->setAmbient( + waterPlane.color * 0.5f); + material->getTechnique(0)->getPass(0)->setSelfIllumination( + waterPlane.color * 0.2f); + material->getTechnique(0)->getPass(0)->setSceneBlending( + Ogre::SBT_TRANSPARENT_ALPHA); + material->getTechnique(0)->getPass(0)->setDepthWriteEnabled( + false); + material->getTechnique(0)->getPass(0)->setLightingEnabled(true); + + // Create water plane geometry + float halfSize = waterPlane.planeSize * 0.5f; + waterPlane.manualObject->begin( + materialName, Ogre::RenderOperation::OT_TRIANGLE_LIST); + + // Create a simple plane (2 triangles) + // Vertex 0: (-halfSize, waterSurfaceY, -halfSize) + waterPlane.manualObject->position( + -halfSize, waterPlane.waterSurfaceY, -halfSize); + waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f); + waterPlane.manualObject->textureCoord(0.0f, 0.0f); + + // Vertex 1: (halfSize, waterSurfaceY, -halfSize) + waterPlane.manualObject->position( + halfSize, waterPlane.waterSurfaceY, -halfSize); + waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f); + waterPlane.manualObject->textureCoord(1.0f, 0.0f); + + // Vertex 2: (halfSize, waterSurfaceY, halfSize) + waterPlane.manualObject->position( + halfSize, waterPlane.waterSurfaceY, halfSize); + waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f); + waterPlane.manualObject->textureCoord(1.0f, 1.0f); + + // Vertex 3: (-halfSize, waterSurfaceY, halfSize) + waterPlane.manualObject->position( + -halfSize, waterPlane.waterSurfaceY, halfSize); + waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f); + waterPlane.manualObject->textureCoord(0.0f, 1.0f); + + // First triangle + waterPlane.manualObject->triangle(0, 1, 2); + + // Second triangle + waterPlane.manualObject->triangle(0, 2, 3); + + waterPlane.manualObject->end(); + + // Attach to scene node + waterPlane.sceneNode->attachObject(waterPlane.manualObject); + + // Log creation + Ogre::LogManager::getSingleton().logMessage( + "Created water plane at Y=" + + Ogre::StringConverter::toString( + waterPlane.waterSurfaceY) + + ", size=" + + Ogre::StringConverter::toString(waterPlane.planeSize)); + } + + void cleanupWaterPlane(WaterPlane &waterPlane) + { + if (waterPlane.manualObject) { + if (waterPlane.sceneNode) { + waterPlane.sceneNode->detachObject( + waterPlane.manualObject); + } + m_sceneMgr->destroyManualObject( + waterPlane.manualObject); + waterPlane.manualObject = nullptr; + } + + if (waterPlane.sceneNode) { + m_sceneMgr->destroySceneNode(waterPlane.sceneNode); + waterPlane.sceneNode = nullptr; + } + } + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; +}; + +// Function to initialize WaterPlaneModule +void initializeWaterPlaneModule(flecs::world &world, + Ogre::SceneManager *sceneMgr) +{ + static WaterPlaneModule *module = nullptr; + if (!module) { + module = new WaterPlaneModule(world, sceneMgr); + + // Create default WaterPlane entity if none exists + bool hasWaterPlane = false; + world.query().each( + [&](flecs::entity, WaterPlane &) { + hasWaterPlane = true; + }); + + if (!hasWaterPlane) { + flecs::entity waterPlaneEntity = + world.entity("WaterPlane"); + waterPlaneEntity.set({}); + waterPlaneEntity.add(); + waterPlaneEntity.set( + EntityNameComponent("Water Plane")); + + // Add Transform component for potential future use + waterPlaneEntity.set( + TransformComponent()); + + Ogre::LogManager::getSingleton().logMessage( + "Created default WaterPlane entity"); + } + } +} \ No newline at end of file diff --git a/src/features/editScene/main.cpp b/src/features/editScene/main.cpp index b162e2c..bec1426 100644 --- a/src/features/editScene/main.cpp +++ b/src/features/editScene/main.cpp @@ -9,11 +9,14 @@ int main(int argc, char *argv[]) // Parse command line arguments bool gameMode = false; + bool debugBuoyancy = false; Ogre::String sceneFile; for (int i = 1; i < argc; i++) { Ogre::String arg = argv[i]; if (arg == "--game") { gameMode = true; + } else if (arg == "--debug-buoyancy") { + debugBuoyancy = true; } else if (arg.length() > 0 && arg[0] != '-') { sceneFile = arg; } @@ -23,14 +26,23 @@ int main(int argc, char *argv[]) app.setGameMode(EditorApp::GameMode::Game); } + if (debugBuoyancy) { + app.setDebugBuoyancy(true); + } + app.initApp(); // Auto-load scene if provided as argument (editor mode only) - if (!sceneFile.empty() && app.getGameMode() == EditorApp::GameMode::Editor) { - std::cout << "Auto-loading scene: " << sceneFile << std::endl; - SceneSerializer serializer(*app.getWorld(), app.getSceneManager()); + if (!sceneFile.empty() && + app.getGameMode() == EditorApp::GameMode::Editor) { + std::cout << "Auto-loading scene: " << sceneFile + << std::endl; + SceneSerializer serializer(*app.getWorld(), + app.getSceneManager()); if (!serializer.loadFromFile(sceneFile, nullptr)) { - std::cerr << "Failed to load scene: " << serializer.getLastError() << std::endl; + std::cerr << "Failed to load scene: " + << serializer.getLastError() + << std::endl; } } diff --git a/src/features/editScene/physics/physics.cpp b/src/features/editScene/physics/physics.cpp index d3978bf..6364085 100644 --- a/src/features/editScene/physics/physics.cpp +++ b/src/features/editScene/physics/physics.cpp @@ -90,10 +90,10 @@ public: Layers::MOVING; // Non moving only collides with moving case Layers::MOVING: return true; // Moving collides with everything - case Layers::SENSORS: - return inObject2 == - Layers::MOVING; // Non moving only collides with moving - default: + case Layers::SENSORS: + return inObject2 == + Layers::MOVING; // Non moving only collides with moving + default: JPH_ASSERT(false); return false; } @@ -110,8 +110,8 @@ public: mObjectToBroadPhase[Layers::NON_MOVING] = BroadPhaseLayers::NON_MOVING; mObjectToBroadPhase[Layers::MOVING] = BroadPhaseLayers::MOVING; - mObjectToBroadPhase[Layers::SENSORS] = BroadPhaseLayers::MOVING; - } + mObjectToBroadPhase[Layers::SENSORS] = BroadPhaseLayers::MOVING; + } virtual uint GetNumBroadPhaseLayers() const override { @@ -269,13 +269,13 @@ public: void DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo, JPH::ColorArg inColor) override { - JPH::Vec4 color = inColor.ToVec4(); - mLines.push_back( - { { (float)inFrom[0], (float)inFrom[1], - (float)inFrom[2] }, - { (float)inTo[0], (float)inTo[1], (float)inTo[2] }, - Ogre::ColourValue(color[0], color[1], color[2], - color[3]) }); + JPH::Vec4 color = inColor.ToVec4(); + mLines.push_back( + { { (float)inFrom[0], (float)inFrom[1], + (float)inFrom[2] }, + { (float)inTo[0], (float)inTo[1], (float)inTo[2] }, + Ogre::ColourValue(color[0], color[1], color[2], + color[3]) }); } void DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2, JPH::RVec3Arg inV3, JPH::ColorArg inColor, @@ -422,14 +422,14 @@ DebugRenderer::DebugRenderer(Ogre::SceneManager *scnMgr, pass->setCullingMode(Ogre::CullingMode::CULL_NONE); pass->setVertexColourTracking(Ogre::TVC_AMBIENT); pass->setLightingEnabled(false); - pass->setDepthWriteEnabled(false); - pass->setDepthCheckEnabled(false); + pass->setDepthWriteEnabled(false); + pass->setDepthCheckEnabled(false); DebugRenderer::Initialize(); scnMgr->getRootSceneNode()->attachObject(mObject); mLines.reserve(6000); mObject->estimateVertexCount(64000); mObject->estimateIndexCount(8000); - mObject->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + mObject->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); } DebugRenderer::~DebugRenderer() { @@ -466,7 +466,7 @@ void CompoundShapeBuilder::addShape(JPH::ShapeRefC shape, const Ogre::Vector3 &position, const Ogre::Quaternion &rotation) { - shapeSettings.AddShape(JoltPhysics::convert(position), + shapeSettings.AddShape(JoltPhysics::convert(position), JoltPhysics::convert(rotation), shape.GetPtr()); } JPH::ShapeRefC CompoundShapeBuilder::build() @@ -577,13 +577,13 @@ public: , job_system(JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers, std::thread::hardware_concurrency() - 1) , mDebugRenderer(new DebugRenderer(scnMgr, cameraNode)) - , object_vs_broadphase_layer_filter{} - , object_vs_object_layer_filter{} + , object_vs_broadphase_layer_filter{} + , object_vs_object_layer_filter{} , debugDraw(false) { - static int instanceCount = 0; - OgreAssert(instanceCount == 0, "Bad initialisation"); - instanceCount++; + static int instanceCount = 0; + OgreAssert(instanceCount == 0, "Bad initialisation"); + instanceCount++; // This is the max amount of rigid bodies that you can add to the physics system. If you try to add more you'll get an error. // Note: This value is low because this is a simple test. For a real project use something in the order of 65536. @@ -773,7 +773,7 @@ public: timeAccumulator -= fixedDeltaTime; } for (JPH::BodyID bID : bodies) { - JPH::RVec3 p; + JPH::RVec3 p; JPH::Quat q; if (id2node.find(bID) == id2node.end()) continue; @@ -792,10 +792,12 @@ public: for (JPH::Character *ch : characters) { if (body_interface.IsAdded(ch->GetBodyID())) { ch->PostSimulation(0.1f); - Ogre::SceneNode *node = id2node[ch->GetBodyID()]; + Ogre::SceneNode *node = + id2node[ch->GetBodyID()]; if (node) node->_setDerivedPosition( - JoltPhysics::convert(ch->GetPosition())); + JoltPhysics::convert( + ch->GetPosition())); } } @@ -828,7 +830,7 @@ public: } static JPH::ShapeRefC createBoxShape(float x, float y, float z) { - return new JPH::BoxShape(JPH::Vec3(x, y, z)); + return new JPH::BoxShape(JPH::Vec3(x, y, z)); } static JPH::ShapeRefC createCylinderShape(float halfHeight, float radius) @@ -836,7 +838,7 @@ public: return new JPH::CylinderShape(halfHeight, radius); } JPH::BodyCreationSettings createBodyCreationSettings( - JPH::Shape *shape, JPH::RVec3 &position, JPH::Quat &rotation, + JPH::Shape *shape, JPH::RVec3 &position, JPH::Quat &rotation, JPH::EMotionType motionType, JPH::ObjectLayer layer) { JPH::BodyCreationSettings body_settings( @@ -845,7 +847,7 @@ public: } JPH::BodyCreationSettings createBodyCreationSettings(JPH::ShapeSettings *shapeSettings, - JPH::RVec3 &position, JPH::Quat &rotation, + JPH::RVec3 &position, JPH::Quat &rotation, JPH::EMotionType motionType, JPH::ObjectLayer layer) { @@ -870,8 +872,8 @@ public: { JPH::BodyInterface &body_interface = physics_system.GetBodyInterface(); - body_interface.AddAngularImpulse( - id, JoltPhysics::convert(impulse)); + body_interface.AddAngularImpulse( + id, JoltPhysics::convert(impulse)); } JPH::BodyID createBody(const JPH::BodyCreationSettings &settings, ActivationListener *listener = nullptr) @@ -917,14 +919,14 @@ public: msp.ScaleToMass(mass); bodySettings.mMassPropertiesOverride = msp; } - JPH::BodyID id = createBody(bodySettings, listener); - if (shape->GetType() == JPH::EShapeType::HeightField) { - JPH::BodyInterface &body_interface = - physics_system.GetBodyInterface(); - body_interface.SetFriction(id, 1.0f); - } - return id; - } + JPH::BodyID id = createBody(bodySettings, listener); + if (shape->GetType() == JPH::EShapeType::HeightField) { + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + body_interface.SetFriction(id, 1.0f); + } + return id; + } JPH::BodyID createBody(const JPH::Shape *shape, float mass, Ogre::SceneNode *node, JPH::EMotionType motion, JPH::ObjectLayer layer, @@ -934,8 +936,8 @@ public: const Ogre::Quaternion &rotation = node->_getDerivedOrientation(); std::cout << "body position: " << position << std::endl; - JPH::BodyID id = createBody(shape, mass, position, rotation, - motion, layer, listener); + JPH::BodyID id = createBody(shape, mass, position, rotation, + motion, layer, listener); id2node[id] = node; node2id[node] = id; return id; @@ -982,7 +984,7 @@ public: JPH::MutableCompoundShape *master = static_cast( compoundShape.GetPtr()); - master->AddShape(JoltPhysics::convert(position), + master->AddShape(JoltPhysics::convert(position), JoltPhysics::convert(rotation), childShape.GetPtr()); } @@ -1156,46 +1158,54 @@ public: for (j = 0; j < indices.size() / 3; j++) triangles[j] = { indices[j * 3 + 0], indices[j * 3 + 1], indices[j * 3 + 2] }; - + // Validate we have enough data if (vertices.size() < 3) { Ogre::LogManager::getSingleton().logMessage( - "ERROR: Mesh has too few vertices: " + mesh->getName() + - " (" + Ogre::StringConverter::toString(vertices.size()) + ")"); + "ERROR: Mesh has too few vertices: " + + mesh->getName() + " (" + + Ogre::StringConverter::toString( + vertices.size()) + + ")"); return JPH::ShapeRefC(); } if (triangles.size() < 1) { Ogre::LogManager::getSingleton().logMessage( - "ERROR: Mesh has no triangles: " + mesh->getName()); + "ERROR: Mesh has no triangles: " + + mesh->getName()); return JPH::ShapeRefC(); } - + JPH::MeshShapeSettings mesh_shape_settings(vertices, triangles); - + // Configure settings to be more permissive mesh_shape_settings.mPerTriangleUserData = false; - + // Log mesh stats for debugging Ogre::LogManager::getSingleton().logMessage( - "Creating mesh shape: " + mesh->getName() + - " - Vertices: " + Ogre::StringConverter::toString(vertices.size()) + - " - Triangles: " + Ogre::StringConverter::toString(triangles.size())); - + "Creating mesh shape: " + mesh->getName() + + " - Vertices: " + + Ogre::StringConverter::toString(vertices.size()) + + " - Triangles: " + + Ogre::StringConverter::toString(triangles.size())); + // Sanitize the mesh data to help with validation mesh_shape_settings.Sanitize(); - + JPH::ShapeSettings::ShapeResult result = mesh_shape_settings.Create(); - + if (!result.Get()) { Ogre::LogManager::getSingleton().logMessage( - "ERROR: Failed to create mesh shape: " + mesh->getName() + + "ERROR: Failed to create mesh shape: " + + mesh->getName() + " - Error: " + result.GetError().c_str()); return JPH::ShapeRefC(); } - + Ogre::LogManager::getSingleton().logMessage( - "Successfully created mesh shape for: " + mesh->getName()); + "Successfully created mesh shape for: " + + mesh->getName()); return result.Get(); } JPH::ShapeRefC createMeshShape(Ogre::String meshName) @@ -1206,7 +1216,7 @@ public: new Ogre::DefaultHardwareBufferManager; p = Ogre::DefaultHardwareBufferManager::getSingletonPtr(); } - + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load( meshName, "General"); if (mesh.get()) { @@ -1216,7 +1226,7 @@ public: } return createMeshShape(mesh); } - + Ogre::LogManager::getSingleton().logMessage( "ERROR: Could not load mesh for collider: " + meshName); return JPH::ShapeRefC(); @@ -1361,9 +1371,9 @@ public: { int i; JPH::HeightFieldShapeSettings heightfieldSettings( - samples, JoltPhysics::convert(offset), - JoltPhysics::convert(scale), - (uint32_t)sampleCount); + samples, JoltPhysics::convert(offset), + JoltPhysics::convert(scale), + (uint32_t)sampleCount); for (i = 0; i < sampleCount; i++) { memcpy(heightfieldSettings.mHeightSamples.data() + sampleCount * i, @@ -1386,10 +1396,10 @@ public: "bad parameters"); JPH::MutableCompoundShapeSettings settings; for (i = 0; i < shapes.size(); i++) - settings.AddShape( - JoltPhysics::convert(positions[i]), - JoltPhysics::convert(rotations[i]), - shapes[i].GetPtr()); + settings.AddShape( + JoltPhysics::convert(positions[i]), + JoltPhysics::convert(rotations[i]), + shapes[i].GetPtr()); JPH::ShapeSettings::ShapeResult result = settings.Create(); OgreAssert(result.Get(), "Can not create compound shape"); return result.Get(); @@ -1405,10 +1415,10 @@ public: "bad parameters"); JPH::StaticCompoundShapeSettings settings; for (i = 0; i < shapes.size(); i++) - settings.AddShape( - JoltPhysics::convert(positions[i]), - JoltPhysics::convert(rotations[i]), - shapes[i].GetPtr()); + settings.AddShape( + JoltPhysics::convert(positions[i]), + JoltPhysics::convert(rotations[i]), + shapes[i].GetPtr()); JPH::ShapeSettings::ShapeResult result = settings.Create(); OgreAssert(result.Get(), "Can not create compound shape"); return result.Get(); @@ -1418,24 +1428,24 @@ public: JPH::ShapeRefC shape) { JPH::OffsetCenterOfMassShapeSettings settings( - JoltPhysics::convert(offset), - shape.GetPtr()); + JoltPhysics::convert(offset), + shape.GetPtr()); JPH::ShapeSettings::ShapeResult result = settings.Create(); OgreAssert(result.Get(), "Can not create com offset shape"); return result.Get(); } - JPH::ShapeRefC - createRotatedTranslatedShape(const Ogre::Vector3 &offset, - const Ogre::Quaternion rotation, - JPH::ShapeRefC shape) - { - return JPH::RotatedTranslatedShapeSettings( - JoltPhysics::convert(offset), - JoltPhysics::convert(rotation), shape) - .Create() - .Get(); - } - void applyBuoyancyImpulse(JPH::BodyID id, + JPH::ShapeRefC + createRotatedTranslatedShape(const Ogre::Vector3 &offset, + const Ogre::Quaternion rotation, + JPH::ShapeRefC shape) + { + return JPH::RotatedTranslatedShapeSettings( + JoltPhysics::convert(offset), + JoltPhysics::convert(rotation), shape) + .Create() + .Get(); + } + void applyBuoyancyImpulse(JPH::BodyID id, const Ogre::Vector3 &surfacePosition, const Ogre::Vector3 &surfaceNormal, float buoyancy, float linearDrag, @@ -1446,12 +1456,12 @@ public: JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(), id); JPH::Body &body = lock.GetBody(); - body.ApplyBuoyancyImpulse( - JoltPhysics::convert(surfacePosition), - JoltPhysics::convert(surfaceNormal), - buoyancy, linearDrag, angularDrag, - JoltPhysics::convert(fluidVelocity), - JoltPhysics::convert(gravity), dt); + body.ApplyBuoyancyImpulse( + JoltPhysics::convert(surfacePosition), + JoltPhysics::convert(surfaceNormal), + buoyancy, linearDrag, angularDrag, + JoltPhysics::convert(fluidVelocity), + JoltPhysics::convert(gravity), dt); } void applyBuoyancyImpulse(JPH::BodyID id, const Ogre::Vector3 &surfacePosition, @@ -1463,12 +1473,12 @@ public: JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(), id); JPH::Body &body = lock.GetBody(); - body.ApplyBuoyancyImpulse( - JoltPhysics::convert(surfacePosition), - JoltPhysics::convert(surfaceNormal), - buoyancy, linearDrag, angularDrag, - JoltPhysics::convert(fluidVelocity), - physics_system.GetGravity(), dt); + body.ApplyBuoyancyImpulse( + JoltPhysics::convert(surfacePosition), + JoltPhysics::convert(surfaceNormal), + buoyancy, linearDrag, angularDrag, + JoltPhysics::convert(fluidVelocity), + physics_system.GetGravity(), dt); } bool isActive(JPH::BodyID id) { @@ -1491,7 +1501,7 @@ public: void getPositionAndRotation(JPH::BodyID id, Ogre::Vector3 &position, Ogre::Quaternion &rotation) { - JPH::RVec3 _position; + JPH::RVec3 _position; JPH::Quat _rotation; physics_system.GetBodyInterface().GetPositionAndRotation( id, _position, _rotation); @@ -1552,20 +1562,23 @@ public: } void setGravityFactor(JPH::BodyID id, float factor) { - return physics_system.GetBodyInterface().SetGravityFactor(id, - factor); + return physics_system.GetBodyInterface().SetGravityFactor( + id, factor); } void broadphaseQuery(float dt, const Ogre::Vector3 &position, std::set &inWater) { - JPH::RVec3 surface_point = JoltPhysics::convert( + JPH::RVec3 surface_point = JoltPhysics::convert( position + Ogre::Vector3(0, -0.1f, 0)); MyCollector collector(&physics_system, surface_point, - JPH::Vec3::sAxisY(), dt); + JPH::Vec3::sAxisY(), dt); // Apply buoyancy to all bodies that intersect with the water - JPH::AABox water_box(-JPH::Vec3(1000, 1000, 1000), - JPH::Vec3(1000, 0.1f, 1000)); + // Create a symmetrical box around the surface point: + // 1000 units in X/Z, 1.0 unit above and below in Y + // This detects bodies slightly above and well below water surface + JPH::AABox water_box(-JPH::Vec3(1000, 1.0f, 1000), + JPH::Vec3(1000, 1000, 1000)); water_box.Translate(JPH::Vec3(surface_point)); physics_system.GetBroadPhaseQuery().CollideAABox( water_box, collector, @@ -1584,36 +1597,43 @@ public: } } bool raycastQuery(Ogre::Vector3 startPoint, Ogre::Vector3 endPoint, - Ogre::Vector3 &position, JPH::BodyID &id) + Ogre::Vector3 &position, JPH::BodyID &id) { int i; Ogre::Vector3 direction = endPoint - startPoint; JPH::RRayCast ray{ JoltPhysics::convert(startPoint), - JoltPhysics::convert(direction) }; + JoltPhysics::convert(direction) }; JPH::RayCastResult hit; bool hadHit = physics_system.GetNarrowPhaseQuery().CastRay( ray, hit, {}, JPH::SpecifiedObjectLayerFilter(Layers::NON_MOVING)); - if (hadHit) { + if (hadHit) { position = JoltPhysics::convert( ray.GetPointOnRay(hit.mFraction)); - id = hit.mBodyID; - } + id = hit.mBodyID; + } return hadHit; } - bool bodyIsCharacter(JPH::BodyID id) const - { - return characterBodies.find(id) != characterBodies.end(); - } - void destroyCharacter(std::shared_ptr ch) - { - characterBodies.erase(characterBodies.find(ch->GetBodyID())); - characters.erase(ch.get()); - Ogre::SceneNode *node = id2node[ch->GetBodyID()]; - id2node.erase(ch->GetBodyID()); - node2id.erase(node); - ch = nullptr; - } + bool bodyIsCharacter(JPH::BodyID id) const + { + return characterBodies.find(id) != characterBodies.end(); + } + void destroyCharacter(std::shared_ptr ch) + { + characterBodies.erase(characterBodies.find(ch->GetBodyID())); + characters.erase(ch.get()); + Ogre::SceneNode *node = id2node[ch->GetBodyID()]; + id2node.erase(ch->GetBodyID()); + node2id.erase(node); + ch = nullptr; + } + Ogre::SceneNode *getSceneNodeFromBodyID(JPH::BodyID id) const + { + auto it = id2node.find(id); + if (it != id2node.end()) + return it->second; + return nullptr; + } }; void physics() @@ -1631,20 +1651,20 @@ JoltPhysicsWrapper::JoltPhysicsWrapper(Ogre::SceneManager *scnMgr, // This needs to be done before any other Jolt function is called. JPH::RegisterDefaultAllocator(); - // Install trace and assert callbacks + // Install trace and assert callbacks JPH::Trace = TraceImpl; JPH_IF_ENABLE_ASSERTS(JPH::AssertFailed = AssertFailedImpl;) - // Create a factory, this class is responsible for creating instances of classes based on their name or hash and is mainly used for deserialization of saved data. - // It is not directly used in this example but still required. + // Create a factory, this class is responsible for creating instances of classes based on their name or hash and is mainly used for deserialization of saved data. + // It is not directly used in this example but still required. JPH::Factory::sInstance = new JPH::Factory(); - // Register all physics types with the factory and install their collision handlers with the CollisionDispatch class. - // If you have your own custom shape types you probably need to register their handlers with the CollisionDispatch before calling this function. - // If you implement your own default material (PhysicsMaterial::sDefault) make sure to initialize it before this function or else this function will create one for you. + // Register all physics types with the factory and install their collision handlers with the CollisionDispatch class. + // If you have your own custom shape types you probably need to register their handlers with the CollisionDispatch before calling this function. + // If you implement your own default material (PhysicsMaterial::sDefault) make sure to initialize it before this function or else this function will create one for you. JPH::RegisterTypes(); - phys = std::make_unique(scnMgr, cameraNode, nullptr, - &contacts); + phys = std::make_unique(scnMgr, cameraNode, nullptr, + &contacts); } JoltPhysicsWrapper::~JoltPhysicsWrapper() @@ -1679,7 +1699,7 @@ JPH::ShapeRefC JoltPhysicsWrapper::createSphereShape(float radius) } JPH::ShapeRefC JoltPhysicsWrapper::createCapsuleShape(float halfHeight, - float radius) + float radius) { return phys->createCapsuleShape(halfHeight, radius); } @@ -1739,14 +1759,14 @@ JPH::ShapeRefC JoltPhysicsWrapper::createOffsetCenterOfMassShape(const Ogre::Vector3 &offset, JPH::ShapeRefC shape) { - return phys->createOffsetCenterOfMassShape(offset, shape); + return phys->createOffsetCenterOfMassShape(offset, shape); } JPH::ShapeRefC JoltPhysicsWrapper::createRotatedTranslatedShape( - const Ogre::Vector3 &offset, const Ogre::Quaternion rotation, - JPH::ShapeRefC shape) + const Ogre::Vector3 &offset, const Ogre::Quaternion rotation, + JPH::ShapeRefC shape) { - return phys->createRotatedTranslatedShape(offset, rotation, shape); + return phys->createRotatedTranslatedShape(offset, rotation, shape); } JPH::BodyID @@ -1936,19 +1956,26 @@ void JoltPhysicsWrapper::removeContactListener(const JPH::BodyID &id) } bool JoltPhysicsWrapper::raycastQuery(Ogre::Vector3 startPoint, Ogre::Vector3 endPoint, - Ogre::Vector3 &position, JPH::BodyID &id) + Ogre::Vector3 &position, JPH::BodyID &id) { - return phys->raycastQuery(startPoint, endPoint, position, id); + return phys->raycastQuery(startPoint, endPoint, position, id); } bool JoltPhysicsWrapper::bodyIsCharacter(JPH::BodyID id) const { - return phys->bodyIsCharacter(id); + return phys->bodyIsCharacter(id); } void JoltPhysicsWrapper::destroyCharacter(std::shared_ptr ch) { - phys->destroyCharacter(ch); + phys->destroyCharacter(ch); } + +Ogre::SceneNode * +JoltPhysicsWrapper::getSceneNodeFromBodyID(JPH::BodyID id) const +{ + return phys->getSceneNodeFromBodyID(id); +} + template <> JoltPhysicsWrapper *Ogre::Singleton::msSingleton = 0; diff --git a/src/features/editScene/physics/physics.h b/src/features/editScene/physics/physics.h index 26383ad..100c802 100644 --- a/src/features/editScene/physics/physics.h +++ b/src/features/editScene/physics/physics.h @@ -45,14 +45,13 @@ static constexpr uint NUM_LAYERS(2); namespace JoltPhysics { -template -Ogre::Vector3 convert(const T &vec) +template Ogre::Vector3 convert(const T &vec) { - return {vec[0], vec[1], vec[2]}; + return { vec[0], vec[1], vec[2] }; } -template T convert(const Ogre::Vector3 &vec) +template T convert(const Ogre::Vector3 &vec) { - return { vec.x, vec.y, vec.z }; + return { vec.x, vec.y, vec.z }; } Ogre::Vector3 convert(const JPH::RVec3Arg &vec); JPH::RVec3 convert(const Ogre::Vector3 &vec); @@ -146,10 +145,11 @@ public: JPH::ShapeRefC createOffsetCenterOfMassShape(const Ogre::Vector3 &offset, JPH::ShapeRefC shape); - JPH::ShapeRefC - createRotatedTranslatedShape(const Ogre::Vector3 &offset, const Ogre::Quaternion rotation, - JPH::ShapeRefC shape); - JPH::BodyID createBody(const JPH::BodyCreationSettings &settings); + JPH::ShapeRefC + createRotatedTranslatedShape(const Ogre::Vector3 &offset, + const Ogre::Quaternion rotation, + JPH::ShapeRefC shape); + JPH::BodyID createBody(const JPH::BodyCreationSettings &settings); JPH::BodyID createBody(const JPH::Shape *shape, float mass, const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, @@ -226,8 +226,9 @@ public: listener); void removeContactListener(const JPH::BodyID &id); bool raycastQuery(Ogre::Vector3 startPoint, Ogre::Vector3 endPoint, - Ogre::Vector3 &position, JPH::BodyID &id); - bool bodyIsCharacter(JPH::BodyID id) const; - void destroyCharacter(std::shared_ptr ch); + Ogre::Vector3 &position, JPH::BodyID &id); + bool bodyIsCharacter(JPH::BodyID id) const; + void destroyCharacter(std::shared_ptr ch); + Ogre::SceneNode *getSceneNodeFromBodyID(JPH::BodyID id) const; }; #endif diff --git a/src/features/editScene/systems/BuoyancySystem.cpp b/src/features/editScene/systems/BuoyancySystem.cpp new file mode 100644 index 0000000..1628eec --- /dev/null +++ b/src/features/editScene/systems/BuoyancySystem.cpp @@ -0,0 +1,328 @@ +#include "BuoyancySystem.hpp" +#include "../components/EditorMarker.hpp" +#include "../components/EntityName.hpp" +#include "../components/Character.hpp" +#include +#include + +BuoyancySystem::BuoyancySystem(flecs::world &world, JoltPhysicsWrapper *physics) + : m_world(world) + , m_physics(physics) + , m_buoyancyInfoQuery(world.query()) +{ + // Create default WaterPhysics entity if none exists + bool hasWaterPhysics = false; + world.query().each( + [&](flecs::entity, WaterPhysics &) { hasWaterPhysics = true; }); + + if (!hasWaterPhysics) { + flecs::entity waterEntity = world.entity("WaterPhysics"); + waterEntity.set({}); + waterEntity.add(); + waterEntity.set( + EntityNameComponent("Water Physics")); + Ogre::LogManager::getSingleton().logMessage( + "BuoyancySystem: Created default WaterPhysics entity"); + } +} + +void BuoyancySystem::initialize() +{ + Ogre::LogManager::getSingleton().logMessage( + "Buoyancy system initialized"); +} + +void BuoyancySystem::update(float deltaTime) +{ + // Check if there's a WaterPhysics component in the world + WaterPhysics *waterPhysics = nullptr; + m_world.query().each( + [&](flecs::entity entity, WaterPhysics &wp) { + waterPhysics = ℘ + }); + + if (!waterPhysics) { + Ogre::LogManager::getSingleton().logMessage( + "BuoyancySystem: No WaterPhysics component found"); + return; + } + + if (!waterPhysics->enabled) { + return; + } + + if (!m_physics || deltaTime <= 0.0f) { + return; + } + + // Rebuild scene node -> entity cache each frame + // This ensures we have up-to-date mappings even if entities are added/removed + m_nodeToEntity.clear(); + m_world.query().each( + [&](flecs::entity entity, TransformComponent &transform) { + if (transform.node) { + m_nodeToEntity[transform.node] = entity.id(); + } + }); + + // Clear previous frame's water bodies + m_bodiesInWater.clear(); + + // Perform broadphase query to find bodies in water + // Use water surface position from WaterPhysics component, camera XZ position + Ogre::Vector3 waterSurfacePos(m_cameraPosition.x, + waterPhysics->waterSurfaceY, + m_cameraPosition.z); + m_physics->broadphaseQuery(deltaTime, waterSurfacePos, m_bodiesInWater); + + // Debug logging if enabled + if (m_debugEnabled) { + debugLogWaterDetection(waterSurfacePos); + } + + // Build a set of entity IDs that have BuoyancyInfo for quick lookup + std::set entitiesWithBuoyancyInfo; + m_buoyancyInfoQuery.each( + [&](flecs::entity entity, BuoyancyInfo &buoyancyInfo) { + if (buoyancyInfo.enabled) { + entitiesWithBuoyancyInfo.insert(entity.id()); + } + }); + + // Apply buoyancy to all bodies found in water + for (JPH::BodyID bodyID : m_bodiesInWater) { + // Try to find the entity associated with this body + Ogre::SceneNode *node = + m_physics->getSceneNodeFromBodyID(bodyID); + if (!node) { + // No scene node found for this body - apply default buoyancy + applyBuoyancyToBody(bodyID, deltaTime, nullptr, + waterPhysics); + continue; + } + + // Find entity from scene node + auto it = m_nodeToEntity.find(node); + if (it == m_nodeToEntity.end()) { + // No entity found for this scene node - apply default buoyancy + applyBuoyancyToBody(bodyID, deltaTime, nullptr, + waterPhysics); + continue; + } + + flecs::entity entity = m_world.entity(it->second); + if (!entity.is_valid()) { + applyBuoyancyToBody(bodyID, deltaTime, nullptr, + waterPhysics); + continue; + } + + // Debug logging for buoyancy application + if (m_debugEnabled) { + Ogre::Vector3 bodyPos = m_physics->getPosition(bodyID); + Ogre::LogManager::getSingleton().logMessage( + "BuoyancySystem: Applying buoyancy to body " + + Ogre::StringConverter::toString( + bodyID.GetIndex()) + + " at position " + + Ogre::StringConverter::toString(bodyPos) + + " (character: " + + Ogre::StringConverter::toString( + entity.has()) + + ")"); + } + + // Check if entity has BuoyancyInfo component + if (entitiesWithBuoyancyInfo.find(entity.id()) != + entitiesWithBuoyancyInfo.end()) { + const BuoyancyInfo *buoyancyInfo = + entity.try_get(); + if (buoyancyInfo && buoyancyInfo->enabled) { + applyBuoyancyToBody(bodyID, deltaTime, + buoyancyInfo, waterPhysics); + continue; + } + } + + // Entity found but no BuoyancyInfo - apply default buoyancy + applyBuoyancyToBody(bodyID, deltaTime, nullptr, waterPhysics); + } +} + +void BuoyancySystem::applyBuoyancyToBody(JPH::BodyID bodyID, float deltaTime, + const BuoyancyInfo *buoyancyInfo, + const WaterPhysics *waterPhysics) +{ + if (!m_physics || !m_physics->isActive(bodyID) || !waterPhysics) { + return; + } + + // Get water surface parameters from WaterPhysics component + float waterSurfaceY = waterPhysics->waterSurfaceY; + float buoyancy = waterPhysics->defaultBuoyancy; + float linearDrag = waterPhysics->defaultLinearDrag; + float angularDrag = waterPhysics->defaultAngularDrag; + float submergedThreshold = waterPhysics->defaultSubmergedThreshold; + + // Override with per-entity settings if available + if (buoyancyInfo) { + if (buoyancyInfo->useCustomWaterLevel) { + waterSurfaceY = buoyancyInfo->waterSurfaceY; + } + buoyancy = buoyancyInfo->buoyancy; + linearDrag = buoyancyInfo->linearDrag; + angularDrag = buoyancyInfo->angularDrag; + submergedThreshold = buoyancyInfo->submergedThreshold; + } + + // Check if body is sufficiently submerged + if (!isBodySubmerged(bodyID, waterSurfaceY)) { + return; + } + + // Get body position + Ogre::Vector3 bodyPos = m_physics->getPosition(bodyID); + + // Calculate submerged depth (how far below water surface) + float submergedDepth = waterSurfaceY - bodyPos.y; + if (submergedDepth <= 0.0f) { + return; // Not submerged + } + + // Simple approximation: use body's bounding sphere radius + // In a more advanced implementation, we would calculate actual submerged volume + Ogre::Vector3 surfacePosition(bodyPos.x, waterSurfaceY, bodyPos.z); + Ogre::Vector3 surfaceNormal(0.0f, 1.0f, 0.0f); // Water surface faces up + Ogre::Vector3 fluidVelocity(0.0f, 0.0f, 0.0f); // Still water for now + Ogre::Vector3 gravity(0.0f, -waterPhysics->gravity, 0.0f); + + // Apply buoyancy impulse + m_physics->applyBuoyancyImpulse(bodyID, surfacePosition, surfaceNormal, + buoyancy, linearDrag, angularDrag, + fluidVelocity, gravity, deltaTime); +} + +bool BuoyancySystem::isBodySubmerged(JPH::BodyID bodyID, + float waterSurfaceY) const +{ + if (!m_physics) { + return false; + } + + // Get body position + Ogre::Vector3 bodyPos = m_physics->getPosition(bodyID); + + // Simple check: body is submerged if its Y position is below water surface + // In a more complete implementation, we would check the actual submerged volume + // or use Jolt's submerged shape query + return bodyPos.y < waterSurfaceY; +} + +flecs::entity BuoyancySystem::findEntityFromSceneNode(Ogre::SceneNode *node) +{ + auto it = m_nodeToEntity.find(node); + if (it != m_nodeToEntity.end()) { + return m_world.entity(it->second); + } + return flecs::entity(); +} + +void BuoyancySystem::debugLogWaterDetection(const Ogre::Vector3 &waterSurfacePos) +{ + static int frameCounter = 0; + frameCounter++; + + // Log every 60 frames (about 1 second at 60 FPS) + if (frameCounter % 60 != 0) { + return; + } + + Ogre::LogManager::getSingleton().logMessage( + "=== Buoyancy System Debug Frame " + + Ogre::StringConverter::toString(frameCounter) + " ==="); + + // Log water physics state + WaterPhysics *waterPhysics = nullptr; + m_world.query().each( + [&](flecs::entity entity, WaterPhysics &wp) { + waterPhysics = ℘ + }); + + if (waterPhysics) { + Ogre::LogManager::getSingleton().logMessage( + "WaterPhysics: surfaceY=" + + Ogre::StringConverter::toString( + waterPhysics->waterSurfaceY) + + ", enabled=" + + Ogre::StringConverter::toString(waterPhysics->enabled) + + ", buoyancy=" + + Ogre::StringConverter::toString( + waterPhysics->defaultBuoyancy)); + } else { + Ogre::LogManager::getSingleton().logMessage( + "WaterPhysics: NOT FOUND"); + } + + // Log camera position + Ogre::LogManager::getSingleton().logMessage( + "Camera position: " + + Ogre::StringConverter::toString(m_cameraPosition)); + + // Log water surface position used for detection + Ogre::LogManager::getSingleton().logMessage( + "Water detection center: " + + Ogre::StringConverter::toString(waterSurfacePos)); + + // Log all characters and their positions + int characterCount = 0; + m_world.query().each( + [&](flecs::entity entity, CharacterComponent &cc, + TransformComponent &transform) { + characterCount++; + Ogre::Vector3 worldPos = + transform.node ? + transform.node->_getDerivedPosition() : + Ogre::Vector3::ZERO; + + // Check if character is in water (simple Y position check) + bool inWater = + waterPhysics && + (worldPos.y < waterPhysics->waterSurfaceY); + + Ogre::LogManager::getSingleton().logMessage( + "Character " + + Ogre::StringConverter::toString(entity.id()) + + ": pos=" + + Ogre::StringConverter::toString(worldPos) + + ", inWater=" + + Ogre::StringConverter::toString(inWater) + + ", enabled=" + + Ogre::StringConverter::toString(cc.enabled)); + }); + + Ogre::LogManager::getSingleton().logMessage( + "Total characters: " + + Ogre::StringConverter::toString(characterCount)); + + // Log bodies detected in water by broadphase query + Ogre::LogManager::getSingleton().logMessage( + "Bodies in water (broadphase): " + + Ogre::StringConverter::toString(m_bodiesInWater.size())); + + for (JPH::BodyID bodyID : m_bodiesInWater) { + Ogre::Vector3 bodyPos = m_physics->getPosition(bodyID); + bool isChar = m_physics->bodyIsCharacter(bodyID); + float gravityFactor = m_physics->getGravityFactor(bodyID); + + Ogre::LogManager::getSingleton().logMessage( + " Body " + + Ogre::StringConverter::toString(bodyID.GetIndex()) + + ": pos=" + Ogre::StringConverter::toString(bodyPos) + + ", isCharacter=" + + Ogre::StringConverter::toString(isChar) + + ", gravityFactor=" + + Ogre::StringConverter::toString(gravityFactor)); + } + + Ogre::LogManager::getSingleton().logMessage("=== End Debug Frame ==="); +} diff --git a/src/features/editScene/systems/BuoyancySystem.hpp b/src/features/editScene/systems/BuoyancySystem.hpp new file mode 100644 index 0000000..9804dd5 --- /dev/null +++ b/src/features/editScene/systems/BuoyancySystem.hpp @@ -0,0 +1,98 @@ +#ifndef EDITSCENE_BUOYANCYSYSTEM_HPP +#define EDITSCENE_BUOYANCYSYSTEM_HPP +#pragma once + +#include +#include +#include +#include +#include "../physics/physics.h" +#include "../components/BuoyancyInfo.hpp" +#include "../components/WaterPhysics.hpp" +#include "../components/Transform.hpp" + +/** + * Buoyancy system for water physics + * Applies buoyancy forces to entities submerged in water + * Uses broadphase queries to detect submerged bodies + * + * The system handles all dynamic physics bodies (rigid bodies, characters, etc.) + * by mapping JPH::BodyID to flecs::entity via scene node lookup. + * If an entity has a BuoyancyInfo component, its settings are used. + * Otherwise, default settings from WaterPhysics component are applied. + */ +class BuoyancySystem { +public: + BuoyancySystem(flecs::world &world, JoltPhysicsWrapper *physics); + ~BuoyancySystem() = default; + + // Initialize buoyancy system + void initialize(); + + // Update buoyancy forces + void update(float deltaTime); + + // Enable/disable buoyancy system + void setEnabled(bool enabled) + { + m_enabled = enabled; + } + bool isEnabled() const + { + return m_enabled; + } + + // Enable/disable debug mode + void setDebugEnabled(bool enabled) + { + m_debugEnabled = enabled; + } + bool isDebugEnabled() const + { + return m_debugEnabled; + } + + // Set camera position for water detection area + void setCameraPosition(const Ogre::Vector3 &cameraPos) + { + m_cameraPosition = cameraPos; + } + +private: + // Apply buoyancy to a specific body + void applyBuoyancyToBody(JPH::BodyID bodyID, float deltaTime, + const BuoyancyInfo *buoyancyInfo, + const WaterPhysics *waterPhysics); + + // Check if a body is submerged based on its position + bool isBodySubmerged(JPH::BodyID bodyID, float waterSurfaceY) const; + + // Find entity from scene node (cached lookup) + flecs::entity findEntityFromSceneNode(Ogre::SceneNode *node); + + // Debug logging + void debugLogWaterDetection(const Ogre::Vector3 &waterSurfacePos); + + flecs::world &m_world; + JoltPhysicsWrapper *m_physics; + + // System enabled state + bool m_enabled = true; + + // Debug mode + bool m_debugEnabled = false; + + // Camera position for water detection area + Ogre::Vector3 m_cameraPosition = Ogre::Vector3::ZERO; + + // Bodies currently in water (cached from broadphase query) + std::set m_bodiesInWater; + + // Cache for scene node -> entity mapping + std::unordered_map m_nodeToEntity; + + // Query for entities with BuoyancyInfo component + flecs::query m_buoyancyInfoQuery; +}; + +#endif // EDITSCENE_BUOYANCYSYSTEM_HPP diff --git a/src/features/editScene/systems/CharacterSystem.cpp b/src/features/editScene/systems/CharacterSystem.cpp index 71e2bd5..b7faa93 100644 --- a/src/features/editScene/systems/CharacterSystem.cpp +++ b/src/features/editScene/systems/CharacterSystem.cpp @@ -30,8 +30,8 @@ void CharacterSystem::initialize() m_initialized = true; } -JPH::ShapeRefC CharacterSystem::createColliderShape( - PhysicsColliderComponent &collider) +JPH::ShapeRefC +CharacterSystem::createColliderShape(PhysicsColliderComponent &collider) { if (!collider.shapeDirty && collider.shape) return collider.shape; @@ -55,10 +55,12 @@ JPH::ShapeRefC CharacterSystem::createColliderShape( case PhysicsColliderComponent::ShapeType::Mesh: { if (!collider.meshName.empty()) { try { - result = m_physics->createMeshShape(collider.meshName); + result = m_physics->createMeshShape( + collider.meshName); } catch (...) { Ogre::LogManager::getSingleton().logMessage( - "Failed to create mesh shape: " + collider.meshName); + "Failed to create mesh shape: " + + collider.meshName); } } break; @@ -79,8 +81,8 @@ JPH::ShapeRefC CharacterSystem::createColliderShape( } if (!result) - result = m_physics->createBoxShape(Ogre::Vector3(0.1f, 0.1f, - 0.1f)); + result = m_physics->createBoxShape( + Ogre::Vector3(0.1f, 0.1f, 0.1f)); if (collider.offset != Ogre::Vector3::ZERO || collider.rotationOffset != Ogre::Quaternion::IDENTITY) @@ -93,7 +95,7 @@ JPH::ShapeRefC CharacterSystem::createColliderShape( } JPH::ShapeRefC CharacterSystem::buildCharacterShape(flecs::entity e, - CharacterComponent &cc) + CharacterComponent &cc) { std::vector shapes; std::vector positions; @@ -101,13 +103,12 @@ JPH::ShapeRefC CharacterSystem::buildCharacterShape(flecs::entity e, /* Main capsule */ float halfHeight = cc.getHalfHeight(); - JPH::ShapeRefC capsule = m_physics->createCapsuleShape(halfHeight, - cc.radius); + JPH::ShapeRefC capsule = + m_physics->createCapsuleShape(halfHeight, cc.radius); /* Shift capsule so its bottom sits at y = 0 */ Ogre::Vector3 capsulePos(0.0f, halfHeight + cc.radius, 0.0f); capsule = m_physics->createRotatedTranslatedShape( - capsulePos + cc.offset, Ogre::Quaternion::IDENTITY, - capsule); + capsulePos + cc.offset, Ogre::Quaternion::IDENTITY, capsule); shapes.push_back(capsule); positions.push_back(Ogre::Vector3::ZERO); rotations.push_back(Ogre::Quaternion::IDENTITY); @@ -135,8 +136,7 @@ JPH::ShapeRefC CharacterSystem::buildCharacterShape(flecs::entity e, rotations); } -void CharacterSystem::setupEntity(flecs::entity e, - CharacterComponent &cc) +void CharacterSystem::setupEntity(flecs::entity e, CharacterComponent &cc) { if (!m_physics) return; @@ -153,14 +153,15 @@ void CharacterSystem::setupEntity(flecs::entity e, if (!shape) return; - JPH::CharacterBase *base = m_physics->createCharacter( - transform.node, shape); + JPH::CharacterBase *base = + m_physics->createCharacter(transform.node, shape); if (!base) return; auto *ch = static_cast(base); ch->AddToPhysicsSystem(); - m_physics->setGravityFactor(ch->GetBodyID(), 0.0f); + // Don't modify gravity factor - let buoyancy handle floating/sinking + // m_physics->setGravityFactor(ch->GetBodyID(), 0.0f); cc.hasFloor = false; std::cout << "CharacterSystem::setupEntity: entity=" << e.id() << " nodePos=" << transform.node->_getDerivedPosition() @@ -196,8 +197,7 @@ void CharacterSystem::update(float deltaTime) return; m_world.query().each( - [this, deltaTime](flecs::entity e, - CharacterComponent &cc, + [this, deltaTime](flecs::entity e, CharacterComponent &cc, TransformComponent &transform) { if (!cc.enabled) { teardownEntity(e); @@ -237,7 +237,8 @@ void CharacterSystem::update(float deltaTime) JoltPhysics::convert( cc.linearVelocity)); if (cc.linearVelocity.squaredLength() > 0.0001f) { - std::cout << "CharacterSystem::update: entity=" << e.id() + std::cout << "CharacterSystem::update: entity=" + << e.id() << " vel=" << cc.linearVelocity << std::endl; } @@ -247,25 +248,28 @@ void CharacterSystem::update(float deltaTime) Ogre::Vector3 rayStart = charPos; Ogre::Vector3 rayEnd = rayStart + - Ogre::Vector3(0.0f, -cc.floorCheckDistance, 0.0f); + Ogre::Vector3(0.0f, + -cc.floorCheckDistance, + 0.0f); Ogre::Vector3 hitPos; JPH::BodyID hitBody; if (m_physics->raycastQuery(rayStart, rayEnd, hitPos, hitBody)) { cc.hasFloor = true; - m_physics->setGravityFactor( - state.character->GetBodyID(), 1.0f); - std::cout << "CharacterSystem::floor detected: entity=" - << e.id() - << " dist=" - << (charPos - hitPos).length() - << std::endl; + // Don't modify gravity factor - let buoyancy handle floating/sinking + // m_physics->setGravityFactor( + // state.character->GetBodyID(), + // 1.0f); + std::cout + << "CharacterSystem::floor detected: entity=" + << e.id() << " dist=" + << (charPos - hitPos).length() + << std::endl; } } /* Sync rotation from scene node */ - state.character->SetRotation( - JoltPhysics::convert( - state.sceneNode->_getDerivedOrientation())); + state.character->SetRotation(JoltPhysics::convert( + state.sceneNode->_getDerivedOrientation())); }); } diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 14d9ef0..575ba7b 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -6,6 +6,7 @@ #include "../components/EditorMarker.hpp" #include "../components/PhysicsCollider.hpp" #include "../components/RigidBody.hpp" +#include "../components/BuoyancyInfo.hpp" #include "../components/Light.hpp" #include "../components/Camera.hpp" #include "../components/Lod.hpp" @@ -28,6 +29,7 @@ #include "../ui/RigidBodyEditor.hpp" #include "../ui/ComponentRegistration.hpp" #include "PhysicsSystem.hpp" +#include "BuoyancySystem.hpp" #include #include #include @@ -35,7 +37,7 @@ EditorUISystem::EditorUISystem(flecs::world &world, Ogre::SceneManager *sceneMgr, - Ogre::RenderWindow* renderWindow) + Ogre::RenderWindow *renderWindow) : m_world(world) , m_sceneMgr(sceneMgr) , m_renderWindow(renderWindow) @@ -65,7 +67,8 @@ bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay) return false; } -bool EditorUISystem::onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDelta) +bool EditorUISystem::onMouseMoved(const Ogre::Ray &mouseRay, + const Ogre::Vector2 &mouseDelta) { if (m_gizmo) { return m_gizmo->onMouseMoved(mouseRay, mouseDelta); @@ -156,7 +159,8 @@ void EditorUISystem::registerComponentEditors() }); // Register PhysicsCollider component - auto colliderEditor = std::make_unique(m_sceneMgr); + auto colliderEditor = + std::make_unique(m_sceneMgr); m_componentRegistry.registerComponent( "Physics Collider", "Physics", std::move(colliderEditor), // Adder @@ -171,7 +175,7 @@ void EditorUISystem::registerComponentEditors() e.remove(); } }); - + // Register modular components (Light, Camera, etc.) registerModularComponents(); } @@ -179,7 +183,8 @@ void EditorUISystem::registerComponentEditors() void EditorUISystem::registerModularComponents() { // This calls all component modules registered via REGISTER_COMPONENT macro - ComponentRegistration::registerAll(m_componentRegistry, m_sceneMgr, m_physicsSystem, m_renderWindow); + ComponentRegistration::registerAll(m_componentRegistry, m_sceneMgr, + m_physicsSystem, m_renderWindow); } void EditorUISystem::update(float deltaTime) @@ -194,17 +199,17 @@ void EditorUISystem::update(float deltaTime) // Note: NewFrame() is called by ImGuiRenderListener::preViewportUpdate renderHierarchyWindow(); renderPropertyWindow(); - + // Update gizmo position to match selected entity if (m_gizmo && m_gizmo->isAttached()) { m_gizmo->update(); } - + // Render file dialog if open if (m_showFileDialog) { renderFileDialog(); } - + // Render FPS overlay renderFPSOverlay(deltaTime); } @@ -212,7 +217,7 @@ void EditorUISystem::update(float deltaTime) void EditorUISystem::setSelectedEntity(flecs::entity entity) { m_selectedEntity = entity; - + // Update gizmo attachment if (m_gizmo) { if (entity.is_alive() && entity.has()) { @@ -242,15 +247,17 @@ void EditorUISystem::renderHierarchyWindow() if (ImGui::BeginMenuBar()) { // File menu if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Save Scene...", "Ctrl+S")) { + if (ImGui::MenuItem("Save Scene...", + "Ctrl+S")) { showFileDialog(true); } - if (ImGui::MenuItem("Load Scene...", "Ctrl+O")) { + if (ImGui::MenuItem("Load Scene...", + "Ctrl+O")) { showFileDialog(false); } ImGui::EndMenu(); } - + // Entity menu if (ImGui::BeginMenu("Entity")) { if (ImGui::MenuItem("New Entity", "Ctrl+N")) { @@ -276,27 +283,107 @@ void EditorUISystem::renderHierarchyWindow() } ImGui::EndMenu(); } - + // Settings menu if (ImGui::BeginMenu("Settings")) { - if (ImGui::Checkbox("Parent SceneNodes", &m_parentSceneNodes)) { + if (ImGui::Checkbox("Parent SceneNodes", + &m_parentSceneNodes)) { // Toggle applied immediately to new entities } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("When enabled, child entities' SceneNodes are parented to their parent's SceneNode, inheriting transforms. When disabled, SceneNodes are created at root level."); + ImGui::SetTooltip( + "When enabled, child entities' SceneNodes are parented to their parent's SceneNode, inheriting transforms. When disabled, SceneNodes are created at root level."); } - + // Physics debug draw toggle if (m_physicsSystem) { - bool debugDraw = m_physicsSystem->isDebugDrawEnabled(); - if (ImGui::Checkbox("Physics Debug Draw", &debugDraw)) { - m_physicsSystem->setDebugDraw(debugDraw); + bool debugDraw = + m_physicsSystem + ->isDebugDrawEnabled(); + if (ImGui::Checkbox("Physics Debug Draw", + &debugDraw)) { + m_physicsSystem->setDebugDraw( + debugDraw); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Toggle physics collision shape visualization"); + ImGui::SetTooltip( + "Show physics collision shapes and constraints"); } } - + + ImGui::Separator(); + ImGui::Text("Water Physics"); + ImGui::Separator(); + + // Find and edit WaterPhysics component + WaterPhysics *waterPhysics = nullptr; + m_world.query().each( + [&](flecs::entity entity, + WaterPhysics &wp) { + waterPhysics = ℘ + }); + + if (waterPhysics) { + if (ImGui::Checkbox( + "Water Physics Enabled", + &waterPhysics->enabled)) { + } + if (ImGui::SliderFloat( + "Water Surface Y", + &waterPhysics->waterSurfaceY, + -10.0f, 10.0f, "%.2f")) { + } + if (ImGui::SliderFloat( + "Water Density (kg/m³)", + &waterPhysics->waterDensity, + 500.0f, 2000.0f, "%.0f")) { + } + if (ImGui::SliderFloat( + "Gravity (m/s²)", + &waterPhysics->gravity, + 0.0f, 20.0f, "%.2f")) { + } + + ImGui::Separator(); + ImGui::Text( + "Default Buoyancy Parameters"); + + if (ImGui::SliderFloat( + "Default Buoyancy", + &waterPhysics + ->defaultBuoyancy, + 0.0f, 5.0f, "%.2f")) { + } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Default buoyancy density multiplier (1.0 = neutral buoyancy)"); + } + + if (ImGui::SliderFloat( + "Default Linear Drag", + &waterPhysics + ->defaultLinearDrag, + 0.0f, 1.0f, "%.3f")) { + } + if (ImGui::SliderFloat( + "Default Angular Drag", + &waterPhysics + ->defaultAngularDrag, + 0.0f, 1.0f, "%.3f")) { + } + if (ImGui::SliderFloat( + "Submerged Threshold", + &waterPhysics + ->defaultSubmergedThreshold, + 0.0f, 1.0f, "%.2f")) { + } + } else { + ImGui::TextDisabled( + "No WaterPhysics component found"); + } + ImGui::EndMenu(); } ImGui::EndMenuBar(); @@ -327,7 +414,8 @@ void EditorUISystem::renderHierarchyWindow() // Just checking if we can iterate children }); // Alternative: use parent() function - if (entity.parent().is_valid() && entity.parent() != 0) { + if (entity.parent().is_valid() && + entity.parent() != 0) { isRoot = false; } if (isRoot) { @@ -376,7 +464,8 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth) // Leaf nodes (no children) have bullet style if (!hasChildren) { - flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + flags |= ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_NoTreePushOnOpen; } // Highlight selected @@ -534,14 +623,16 @@ void EditorUISystem::renderComponentList(flecs::entity entity) // Dynamically render all components the entity has int componentCount = 0; - + // Render Transform first if present (it's the base component) if (entity.has()) { auto &transform = entity.get_mut(); - if (m_componentRegistry.render(entity, transform)) { + if (m_componentRegistry.render(entity, + transform)) { // Transform changed - mark StaticGeometryMember dirty if present if (entity.has()) { - entity.get_mut().markDirty(); + entity.get_mut() + .markDirty(); } } componentCount++; @@ -550,7 +641,8 @@ void EditorUISystem::renderComponentList(flecs::entity entity) // Render Renderable if (entity.has()) { auto &renderable = entity.get_mut(); - m_componentRegistry.render(entity, renderable); + m_componentRegistry.render(entity, + renderable); componentCount++; } @@ -571,66 +663,81 @@ void EditorUISystem::renderComponentList(flecs::entity entity) // Render RigidBody if (entity.has()) { auto &rigidBody = entity.get_mut(); - m_componentRegistry.render(entity, rigidBody); + m_componentRegistry.render(entity, + rigidBody); componentCount++; } // Render PhysicsCollider if (entity.has()) { auto &collider = entity.get_mut(); - m_componentRegistry.render(entity, collider); + m_componentRegistry.render(entity, + collider); componentCount++; } - + + // Render BuoyancyInfo if present + if (entity.has()) { + auto &buoyancy = entity.get_mut(); + m_componentRegistry.render(entity, buoyancy); + componentCount++; + } + // Render LOD Settings if present if (entity.has()) { auto &lodSettings = entity.get_mut(); - m_componentRegistry.render(entity, lodSettings); + m_componentRegistry.render(entity, + lodSettings); componentCount++; } - + // Render LOD if present if (entity.has()) { auto &lod = entity.get_mut(); m_componentRegistry.render(entity, lod); componentCount++; } - + // Render StaticGeometry Region if present if (entity.has()) { auto ®ion = entity.get_mut(); - m_componentRegistry.render(entity, region); + m_componentRegistry.render(entity, + region); componentCount++; } - + // Render StaticGeometry Member if present if (entity.has()) { auto &member = entity.get_mut(); - m_componentRegistry.render(entity, member); + m_componentRegistry.render( + entity, member); componentCount++; } - + // Render ProceduralTexture if present if (entity.has()) { auto &texture = entity.get_mut(); - m_componentRegistry.render(entity, texture); + m_componentRegistry.render(entity, + texture); componentCount++; } - + // Render ProceduralMaterial if present if (entity.has()) { auto &material = entity.get_mut(); - m_componentRegistry.render(entity, material); + m_componentRegistry.render( + entity, material); componentCount++; } - + // Render Primitive if present if (entity.has()) { auto &primitive = entity.get_mut(); - m_componentRegistry.render(entity, primitive); + m_componentRegistry.render(entity, + primitive); componentCount++; } - + // Render TriangleBuffer if present if (entity.has()) { auto &tb = entity.get_mut(); @@ -669,7 +776,8 @@ void EditorUISystem::renderComponentList(flecs::entity entity) // Render PlayerController if present if (entity.has()) { auto &pc = entity.get_mut(); - m_componentRegistry.render(entity, pc); + m_componentRegistry.render(entity, + pc); componentCount++; } @@ -679,35 +787,35 @@ void EditorUISystem::renderComponentList(flecs::entity entity) m_componentRegistry.render(entity, grid); componentCount++; } - + // Render Lot if present if (entity.has()) { auto &lot = entity.get_mut(); m_componentRegistry.render(entity, lot); componentCount++; } - + // Render District if present if (entity.has()) { auto &district = entity.get_mut(); m_componentRegistry.render(entity, district); componentCount++; } - + // Render Town if present if (entity.has()) { auto &town = entity.get_mut(); m_componentRegistry.render(entity, town); componentCount++; } - + // Render Roof if present if (entity.has()) { auto &roof = entity.get_mut(); m_componentRegistry.render(entity, roof); componentCount++; } - + // Render Room if present if (entity.has()) { auto &room = entity.get_mut(); @@ -716,24 +824,22 @@ void EditorUISystem::renderComponentList(flecs::entity entity) } componentCount++; } - - - - // Render ClearArea if present if (entity.has()) { auto &clearArea = entity.get_mut(); - if (m_componentRegistry.render(entity, clearArea)) { + if (m_componentRegistry.render(entity, + clearArea)) { clearArea.markDirty(); } componentCount++; } - + // Render FurnitureTemplate if present if (entity.has()) { auto &furniture = entity.get_mut(); - m_componentRegistry.render(entity, furniture); + m_componentRegistry.render( + entity, furniture); componentCount++; } @@ -755,28 +861,46 @@ void EditorUISystem::renderAddComponentMenu(flecs::entity entity) // Group components by their group name auto groups = m_componentRegistry.getGroups(); bool hasAny = false; - - for (const auto& group : groups) { + + for (const auto &group : groups) { // Count components in this group that the entity doesn't have int availableCount = 0; - m_componentRegistry.forEachInGroup(group.c_str(), [&](const std::type_index& type, const ComponentRegistry::ComponentInfo& info) { - if (!m_componentRegistry.entityHasComponent(entity, type)) { - availableCount++; - } - }); - - if (availableCount == 0) continue; - hasAny = true; - - // Create a submenu for each group - if (ImGui::BeginMenu(group.c_str())) { - m_componentRegistry.forEachInGroup(group.c_str(), [&](const std::type_index& type, const ComponentRegistry::ComponentInfo& info) { - if (!m_componentRegistry.entityHasComponent(entity, type)) { - if (ImGui::MenuItem(info.name)) { - m_componentRegistry.addComponentByType(entity, type); - } + m_componentRegistry.forEachInGroup( + group.c_str(), + [&](const std::type_index &type, + const ComponentRegistry::ComponentInfo + &info) { + if (!m_componentRegistry + .entityHasComponent( + entity, type)) { + availableCount++; } }); + + if (availableCount == 0) + continue; + hasAny = true; + + // Create a submenu for each group + if (ImGui::BeginMenu(group.c_str())) { + m_componentRegistry.forEachInGroup( + group.c_str(), + [&](const std::type_index &type, + const ComponentRegistry::ComponentInfo + &info) { + if (!m_componentRegistry + .entityHasComponent( + entity, + type)) { + if (ImGui::MenuItem( + info.name)) { + m_componentRegistry + .addComponentByType( + entity, + type); + } + } + }); ImGui::EndMenu(); } } @@ -797,14 +921,19 @@ void EditorUISystem::renderRemoveComponentMenu(flecs::entity entity) // Dynamically list all registered components that the entity has bool hasAny = false; - m_componentRegistry.forEach([&](const std::type_index& type, const ComponentRegistry::ComponentInfo& info) { - if (m_componentRegistry.entityHasComponent(entity, type)) { - hasAny = true; - if (ImGui::MenuItem(info.name)) { - m_componentRegistry.removeComponentByType(entity, type); + m_componentRegistry.forEach( + [&](const std::type_index &type, + const ComponentRegistry::ComponentInfo &info) { + if (m_componentRegistry.entityHasComponent( + entity, type)) { + hasAny = true; + if (ImGui::MenuItem(info.name)) { + m_componentRegistry + .removeComponentByType( + entity, type); + } } - } - }); + }); if (!hasAny) { ImGui::TextDisabled("No components to remove"); @@ -845,7 +974,7 @@ void EditorUISystem::createChildEntity(flecs::entity parent) // Create transform TransformComponent transform; auto &parentTransform = parent.get_mut(); - + // SceneNode parenting depends on setting if (m_parentSceneNodes) { // Child SceneNode inherits parent's transform @@ -853,10 +982,11 @@ void EditorUISystem::createChildEntity(flecs::entity parent) transform.position = Ogre::Vector3::ZERO; } else { // Child SceneNode at root level, position relative to parent - transform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + transform.node = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); transform.position = parentTransform.position; } - + transform.rotation = Ogre::Quaternion::IDENTITY; transform.scale = Ogre::Vector3::UNIT_SCALE; entity.set(transform); @@ -940,7 +1070,7 @@ void EditorUISystem::duplicateEntity(flecs::entity entity) // Find parent entity and node flecs::entity parent = entity.parent(); Ogre::SceneNode *parentNode = m_sceneMgr->getRootSceneNode(); - + if (parent.is_valid() && parent != 0 && parent.has()) { parentNode = parent.get().node; @@ -950,13 +1080,16 @@ void EditorUISystem::duplicateEntity(flecs::entity entity) if (m_parentSceneNodes && parent.is_valid() && parent != 0) { // Create as child of parent's SceneNode newTransform.node = parentNode->createChildSceneNode(); - newTransform.position = oldTransform.position + Ogre::Vector3(1, 0, 0); + newTransform.position = + oldTransform.position + Ogre::Vector3(1, 0, 0); } else { // Create at root level - newTransform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(); - newTransform.position = oldTransform.position + Ogre::Vector3(1, 0, 0); + newTransform.node = m_sceneMgr->getRootSceneNode() + ->createChildSceneNode(); + newTransform.position = + oldTransform.position + Ogre::Vector3(1, 0, 0); } - + newTransform.rotation = oldTransform.rotation; newTransform.scale = oldTransform.scale; newTransform.applyToNode(); @@ -998,9 +1131,8 @@ EditorUISystem::getEntityChildren(flecs::entity parent) return children; // Use flecs::ChildOf relationship to get children - parent.children([&](flecs::entity child) { - children.push_back(child); - }); + parent.children( + [&](flecs::entity child) { children.push_back(child); }); return children; } @@ -1020,33 +1152,36 @@ bool EditorUISystem::isDescendantOf(flecs::entity potentialChild, return false; } -void EditorUISystem::saveScene(const std::string& filepath) +void EditorUISystem::saveScene(const std::string &filepath) { - if (!m_serializer) return; - + if (!m_serializer) + return; + if (m_serializer->saveToFile(filepath)) { - Ogre::LogManager::getSingleton().logMessage( - "Scene saved to: " + filepath); + Ogre::LogManager::getSingleton().logMessage("Scene saved to: " + + filepath); } else { Ogre::LogManager::getSingleton().logMessage( - "Failed to save scene: " + m_serializer->getLastError()); + "Failed to save scene: " + + m_serializer->getLastError()); } } -void EditorUISystem::loadScene(const std::string& filepath) +void EditorUISystem::loadScene(const std::string &filepath) { - if (!m_serializer) return; - + if (!m_serializer) + return; + if (m_serializer->loadFromFile(filepath, this)) { Ogre::LogManager::getSingleton().logMessage( "Scene loaded from: " + filepath); } else { Ogre::LogManager::getSingleton().logMessage( - "Failed to load scene: " + m_serializer->getLastError()); + "Failed to load scene: " + + m_serializer->getLastError()); } } - // File Dialog Implementation void EditorUISystem::showFileDialog(bool isSave) @@ -1054,15 +1189,16 @@ void EditorUISystem::showFileDialog(bool isSave) m_showFileDialog = true; m_fileDialogIsSave = isSave; m_refreshDirectory = true; - + // Initialize current path if empty if (m_currentPath.empty()) { m_currentPath = std::filesystem::current_path().string(); } - + // Set default filename for save dialog if (isSave && strlen(m_filenameBuffer) == 0) { - std::strncpy(m_filenameBuffer, "scene.json", sizeof(m_filenameBuffer) - 1); + std::strncpy(m_filenameBuffer, "scene.json", + sizeof(m_filenameBuffer) - 1); } } @@ -1073,36 +1209,41 @@ void EditorUISystem::closeFileDialog() void EditorUISystem::renderFileDialog() { - const char* title = m_fileDialogIsSave ? "Save Scene" : "Load Scene"; - + const char *title = m_fileDialogIsSave ? "Save Scene" : "Load Scene"; + // Center the dialog ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, + ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver); - + if (!ImGui::Begin(title, &m_showFileDialog)) { ImGui::End(); return; } - + // Current path display ImGui::Text("Location: %s", m_currentPath.c_str()); ImGui::Separator(); - + // Refresh directory contents if needed if (m_refreshDirectory) { m_directoryContents.clear(); try { - for (const auto& entry : std::filesystem::directory_iterator(m_currentPath)) { - m_directoryContents.push_back(entry.path().filename().string()); + for (const auto &entry : + std::filesystem::directory_iterator( + m_currentPath)) { + m_directoryContents.push_back( + entry.path().filename().string()); } - std::sort(m_directoryContents.begin(), m_directoryContents.end()); + std::sort(m_directoryContents.begin(), + m_directoryContents.end()); } catch (...) { // Ignore errors (e.g., permission denied) } m_refreshDirectory = false; } - + // Parent directory button if (ImGui::Button("..")) { std::filesystem::path p(m_currentPath); @@ -1113,23 +1254,24 @@ void EditorUISystem::renderFileDialog() } ImGui::SameLine(); ImGui::Text("(Parent Directory)"); - + // File list ImGui::BeginChild("FileList", ImVec2(0, -60), true); - - for (const auto& name : m_directoryContents) { - std::filesystem::path fullPath = std::filesystem::path(m_currentPath) / name; + + for (const auto &name : m_directoryContents) { + std::filesystem::path fullPath = + std::filesystem::path(m_currentPath) / name; bool isDirectory = std::filesystem::is_directory(fullPath); - + // Skip non-json files in load mode (but show directories) if (!m_fileDialogIsSave && !isDirectory) { if (fullPath.extension() != ".json") { continue; } } - + std::string label = isDirectory ? "[D] " + name : name; - + if (ImGui::Selectable(label.c_str(), m_selectedFile == name)) { if (isDirectory) { m_currentPath = fullPath.string(); @@ -1137,10 +1279,11 @@ void EditorUISystem::renderFileDialog() m_selectedFile.clear(); } else { m_selectedFile = name; - std::strncpy(m_filenameBuffer, name.c_str(), sizeof(m_filenameBuffer) - 1); + std::strncpy(m_filenameBuffer, name.c_str(), + sizeof(m_filenameBuffer) - 1); } } - + // Double-click to enter directory or select file if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { if (isDirectory) { @@ -1150,26 +1293,32 @@ void EditorUISystem::renderFileDialog() } } } - + ImGui::EndChild(); - + // Filename input ImGui::Separator(); ImGui::Text("Filename:"); ImGui::SameLine(); - ImGui::InputText("##filename", m_filenameBuffer, sizeof(m_filenameBuffer)); - + ImGui::InputText("##filename", m_filenameBuffer, + sizeof(m_filenameBuffer)); + // Buttons - if (ImGui::Button(m_fileDialogIsSave ? "Save" : "Load", ImVec2(120, 0))) { + if (ImGui::Button(m_fileDialogIsSave ? "Save" : "Load", + ImVec2(120, 0))) { std::string filename(m_filenameBuffer); if (!filename.empty()) { // Add .json extension if missing in save mode - if (m_fileDialogIsSave && filename.find('.') == std::string::npos) { + if (m_fileDialogIsSave && + filename.find('.') == std::string::npos) { filename += ".json"; } - - std::string fullPath = (std::filesystem::path(m_currentPath) / filename).string(); - + + std::string fullPath = + (std::filesystem::path(m_currentPath) / + filename) + .string(); + if (m_fileDialogIsSave) { saveScene(fullPath); } else { @@ -1178,72 +1327,74 @@ void EditorUISystem::renderFileDialog() closeFileDialog(); } } - + ImGui::SameLine(); - + if (ImGui::Button("Cancel", ImVec2(120, 0))) { closeFileDialog(); } - + ImGui::End(); } - void EditorUISystem::renderFPSOverlay(float deltaTime) { // Toggle FPS overlay with F12 key if (ImGui::IsKeyPressed(ImGuiKey_F12)) { m_showFPSOverlay = !m_showFPSOverlay; } - + if (!m_showFPSOverlay) { return; } - + // Update FPS calculation m_fpsTimeAccumulator += deltaTime; m_fpsFrameCount++; - - // Note: Batch count is set by ImGuiRenderListener::postViewportUpdate + + // Note: Batch count is set by ImGuiRenderListener::postViewportUpdate // after each frame render (stats are updated at end of frame) - + // Update FPS every 0.5 seconds for smoother display if (m_fpsTimeAccumulator >= 0.5f) { m_currentFPS = m_fpsFrameCount / m_fpsTimeAccumulator; m_fpsTimeAccumulator = 0.0f; m_fpsFrameCount = 0; } - + // Set up transparent window style - ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoNav; - + ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav; + // Position in top-right corner with padding ImVec2 windowPos = ImVec2(ImGui::GetIO().DisplaySize.x - 10.0f, 10.0f); ImVec2 windowPivot = ImVec2(1.0f, 0.0f); // Top-right pivot - + ImGui::SetNextWindowPos(windowPos, ImGuiCond_Always, windowPivot); ImGui::SetNextWindowBgAlpha(0.35f); // Semi-transparent background - + if (ImGui::Begin("FPS Overlay", nullptr, windowFlags)) { // FPS text color based on performance ImVec4 fpsColor; if (m_currentFPS >= 60.0f) { - fpsColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green for 60+ FPS + fpsColor = ImVec4(0.0f, 1.0f, 0.0f, + 1.0f); // Green for 60+ FPS } else if (m_currentFPS >= 30.0f) { - fpsColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Yellow for 30-60 FPS + fpsColor = ImVec4(1.0f, 1.0f, 0.0f, + 1.0f); // Yellow for 30-60 FPS } else { - fpsColor = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red for <30 FPS + fpsColor = ImVec4(1.0f, 0.0f, 0.0f, + 1.0f); // Red for <30 FPS } - + ImGui::TextColored(fpsColor, "%.1f FPS", m_currentFPS); ImGui::Text("Batches: %d", m_lastBatchCount); - + ImGui::Separator(); - + ImVec4 hintColor = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); ImGui::TextColored(hintColor, "F12 to toggle"); } diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index b36a6bc..5ed0945 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -13,9 +13,11 @@ // Forward declarations class EditorPhysicsSystem; +class BuoyancySystem; -namespace Ogre { - class RenderWindow; +namespace Ogre +{ +class RenderWindow; } /** @@ -28,7 +30,7 @@ namespace Ogre { class EditorUISystem { public: EditorUISystem(flecs::world &world, Ogre::SceneManager *sceneMgr, - Ogre::RenderWindow* renderWindow = nullptr); + Ogre::RenderWindow *renderWindow = nullptr); ~EditorUISystem(); /** @@ -36,7 +38,7 @@ public: * Call this once per frame */ void update(float deltaTime); - + /** * Render FPS and batch count overlay */ @@ -50,12 +52,16 @@ public: /** * Add existing entity to cache (for entities created before UI) */ - void addEntity(flecs::entity entity) { m_allEntities.push_back(entity); } + void addEntity(flecs::entity entity) + { + m_allEntities.push_back(entity); + } /** * Clear entity cache and deselect */ - void clearEntityCache() { + void clearEntityCache() + { m_allEntities.clear(); setSelectedEntity(flecs::entity::null()); } @@ -73,14 +79,18 @@ public: * Returns true if gizmo handled the event */ bool onMousePressed(const Ogre::Ray &mouseRay); - bool onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDelta); + bool onMouseMoved(const Ogre::Ray &mouseRay, + const Ogre::Vector2 &mouseDelta); bool onMouseReleased(); /** * Get the gizmo for external interaction */ - Gizmo *getGizmo() const { return m_gizmo.get(); } - + Gizmo *getGizmo() const + { + return m_gizmo.get(); + } + /** * Shutdown UI system - must be called before SceneManager destruction */ @@ -89,35 +99,56 @@ public: /** * Get/set SceneNode parenting mode */ - bool getParentSceneNodes() const { return m_parentSceneNodes; } - void setParentSceneNodes(bool value) { m_parentSceneNodes = value; } + bool getParentSceneNodes() const + { + return m_parentSceneNodes; + } + void setParentSceneNodes(bool value) + { + m_parentSceneNodes = value; + } /** * Set physics system for debug toggle */ - void setPhysicsSystem(EditorPhysicsSystem* physics) { m_physicsSystem = physics; } + void setPhysicsSystem(EditorPhysicsSystem *physics) + { + m_physicsSystem = physics; + } + + /** + * Set buoyancy system for configuration + */ + void setBuoyancySystem(BuoyancySystem *buoyancy) + { + m_buoyancySystem = buoyancy; + } /** * Enable/disable editor UI rendering */ - void setEditorUIEnabled(bool enabled) { m_editorUIEnabled = enabled; } - + void setEditorUIEnabled(bool enabled) + { + m_editorUIEnabled = enabled; + } + /** * Set last frame batch count (called from render listener) */ - void setLastBatchCount(int count) { m_lastBatchCount = count; } - - + void setLastBatchCount(int count) + { + m_lastBatchCount = count; + } /** * Save scene to file */ - void saveScene(const std::string& filepath); + void saveScene(const std::string &filepath); /** * Load scene from file */ - void loadScene(const std::string& filepath); + void loadScene(const std::string &filepath); /** * Show file dialog for save/load @@ -166,11 +197,12 @@ private: std::unique_ptr m_serializer; // Settings - bool m_parentSceneNodes = true; // Whether child entities inherit parent's SceneNode - + bool m_parentSceneNodes = + true; // Whether child entities inherit parent's SceneNode + // FPS overlay settings - bool m_showFPSOverlay = true; // Show FPS counter by default - + bool m_showFPSOverlay = true; // Show FPS counter by default + // FPS tracking float m_fpsTimeAccumulator = 0.0f; int m_fpsFrameCount = 0; @@ -178,13 +210,16 @@ private: int m_lastBatchCount = 0; // Physics system reference (for debug toggle) - EditorPhysicsSystem* m_physicsSystem = nullptr; + EditorPhysicsSystem *m_physicsSystem = nullptr; + + // Buoyancy system reference (for configuration) + BuoyancySystem *m_buoyancySystem = nullptr; // Editor UI enabled flag bool m_editorUIEnabled = true; // Render window reference (for viewport access) - Ogre::RenderWindow* m_renderWindow = nullptr; + Ogre::RenderWindow *m_renderWindow = nullptr; // File dialog state bool m_showFileDialog = false; diff --git a/src/features/editScene/systems/PhysicsSystem.hpp b/src/features/editScene/systems/PhysicsSystem.hpp index 8cd1d8a..538f57b 100644 --- a/src/features/editScene/systems/PhysicsSystem.hpp +++ b/src/features/editScene/systems/PhysicsSystem.hpp @@ -15,7 +15,7 @@ */ class EditorPhysicsSystem { public: - EditorPhysicsSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + EditorPhysicsSystem(flecs::world &world, Ogre::SceneManager *sceneMgr); ~EditorPhysicsSystem(); // Initialize physics world @@ -26,33 +26,46 @@ public: // Process component changes and create/update bodies void syncBodies(); - + // Update SceneNodes from physics for dynamic bodies void updateDynamicBodies(); // Enable/disable debug drawing void setDebugDraw(bool enable); - bool isDebugDrawEnabled() const { return m_debugDraw; } - - bool isInitialized() const { return m_initialized; } + bool isDebugDrawEnabled() const + { + return m_debugDraw; + } + + bool isInitialized() const + { + return m_initialized; + } // Remove a rigid body (public for external cleanup) - void removeRigidBody(RigidBodyComponent& rigidBody); + void removeRigidBody(RigidBodyComponent &rigidBody); + + // Get physics wrapper for other systems (e.g., buoyancy) + JoltPhysicsWrapper *getPhysicsWrapper() const + { + return m_physics.get(); + } private: // Create a shape from collider component - JPH::ShapeRefC createShape(PhysicsColliderComponent& collider); + JPH::ShapeRefC createShape(PhysicsColliderComponent &collider); // Build compound shape from multiple colliders JPH::ShapeRefC buildCompoundShape(flecs::entity rigidBodyEntity); // Create or update a rigid body - void updateRigidBody(flecs::entity entity, RigidBodyComponent& rigidBody, - class TransformComponent& transform); + void updateRigidBody(flecs::entity entity, + RigidBodyComponent &rigidBody, + class TransformComponent &transform); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; - flecs::world& m_world; - Ogre::SceneManager* m_sceneMgr; - std::unique_ptr m_physics; bool m_initialized = false; bool m_debugDraw = false; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 845c4c7..ac8d181 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -22,6 +22,8 @@ #include "../components/PlayerController.hpp" #include "../components/CellGrid.hpp" #include "../components/GeneratedPhysicsTag.hpp" +#include "../components/BuoyancyInfo.hpp" +#include "../components/WaterPhysics.hpp" #include "EditorUISystem.hpp" #include #include @@ -243,6 +245,15 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["clearArea"] = serializeClearArea(entity); } + // Buoyancy components + if (entity.has()) { + json["buoyancyInfo"] = serializeBuoyancyInfo(entity); + } + + if (entity.has()) { + json["waterPhysics"] = serializeWaterPhysics(entity); + } + // Serialize children json["children"] = nlohmann::json::array(); entity.children([&](flecs::entity child) { @@ -391,6 +402,15 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, deserializeClearArea(entity, json["clearArea"]); } + // Buoyancy components + if (json.contains("buoyancyInfo")) { + deserializeBuoyancyInfo(entity, json["buoyancyInfo"]); + } + + if (json.contains("waterPhysics")) { + deserializeWaterPhysics(entity, json["waterPhysics"]); + } + // Add to UI system if provided if (uiSystem) { uiSystem->addEntity(entity); @@ -2204,3 +2224,74 @@ void SceneSerializer::deserializePlayerController(flecs::entity entity, pc.runState = json.value("runState", pc.runState); entity.set(pc); } + +// ============================================================================ +// Buoyancy Component Serialization +// ============================================================================ + +nlohmann::json SceneSerializer::serializeBuoyancyInfo(flecs::entity entity) +{ + auto &buoyancy = entity.get(); + nlohmann::json json; + + json["enabled"] = buoyancy.enabled; + json["buoyancy"] = buoyancy.buoyancy; + json["linearDrag"] = buoyancy.linearDrag; + json["angularDrag"] = buoyancy.angularDrag; + json["waterSurfaceY"] = buoyancy.waterSurfaceY; + json["useCustomWaterLevel"] = buoyancy.useCustomWaterLevel; + json["submergedThreshold"] = buoyancy.submergedThreshold; + + return json; +} + +nlohmann::json SceneSerializer::serializeWaterPhysics(flecs::entity entity) +{ + auto &water = entity.get(); + nlohmann::json json; + + json["enabled"] = water.enabled; + json["waterSurfaceY"] = water.waterSurfaceY; + json["waterDensity"] = water.waterDensity; + json["gravity"] = water.gravity; + json["defaultBuoyancy"] = water.defaultBuoyancy; + json["defaultLinearDrag"] = water.defaultLinearDrag; + json["defaultAngularDrag"] = water.defaultAngularDrag; + json["defaultSubmergedThreshold"] = water.defaultSubmergedThreshold; + + return json; +} + +void SceneSerializer::deserializeBuoyancyInfo(flecs::entity entity, + const nlohmann::json &json) +{ + BuoyancyInfo buoyancy; + + buoyancy.enabled = json.value("enabled", true); + buoyancy.buoyancy = json.value("buoyancy", 1.0f); + buoyancy.linearDrag = json.value("linearDrag", 0.1f); + buoyancy.angularDrag = json.value("angularDrag", 0.05f); + buoyancy.waterSurfaceY = json.value("waterSurfaceY", -0.1f); + buoyancy.useCustomWaterLevel = json.value("useCustomWaterLevel", false); + buoyancy.submergedThreshold = json.value("submergedThreshold", 0.3f); + + entity.set(buoyancy); +} + +void SceneSerializer::deserializeWaterPhysics(flecs::entity entity, + const nlohmann::json &json) +{ + WaterPhysics water; + + water.enabled = json.value("enabled", true); + water.waterSurfaceY = json.value("waterSurfaceY", -0.1f); + water.waterDensity = json.value("waterDensity", 1000.0f); + water.gravity = json.value("gravity", 9.81f); + water.defaultBuoyancy = json.value("defaultBuoyancy", 1.0f); + water.defaultLinearDrag = json.value("defaultLinearDrag", 0.1f); + water.defaultAngularDrag = json.value("defaultAngularDrag", 0.05f); + water.defaultSubmergedThreshold = + json.value("defaultSubmergedThreshold", 0.3f); + + entity.set(water); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index eafa7e3..68d2812 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -16,28 +16,33 @@ class EditorUISystem; */ class SceneSerializer { public: - SceneSerializer(flecs::world& world, Ogre::SceneManager* sceneMgr); + SceneSerializer(flecs::world &world, Ogre::SceneManager *sceneMgr); /** * Save scene to JSON file */ - bool saveToFile(const std::string& filepath); + bool saveToFile(const std::string &filepath); /** * Load scene from JSON file */ - bool loadFromFile(const std::string& filepath, EditorUISystem* uiSystem = nullptr); + bool loadFromFile(const std::string &filepath, + EditorUISystem *uiSystem = nullptr); /** * Get last error message */ - const std::string& getLastError() const { return m_lastError; } + const std::string &getLastError() const + { + return m_lastError; + } private: // Serialization helpers nlohmann::json serializeEntity(flecs::entity entity); - void deserializeEntity(const nlohmann::json& json, flecs::entity parent, EditorUISystem* uiSystem); - + void deserializeEntity(const nlohmann::json &json, flecs::entity parent, + EditorUISystem *uiSystem); + // Component serialization nlohmann::json serializeTransform(flecs::entity entity); nlohmann::json serializeRenderable(flecs::entity entity); @@ -59,7 +64,7 @@ private: nlohmann::json serializeAnimationTree(flecs::entity entity); nlohmann::json serializeStartupMenu(flecs::entity entity); nlohmann::json serializePlayerController(flecs::entity entity); - + // CellGrid/Town component serialization nlohmann::json serializeCellGrid(flecs::entity entity); nlohmann::json serializeTown(flecs::entity entity); @@ -69,43 +74,74 @@ private: nlohmann::json serializeRoof(flecs::entity entity); nlohmann::json serializeFurnitureTemplate(flecs::entity entity); nlohmann::json serializeClearArea(flecs::entity entity); - - // Component deserialization - void deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity); - void deserializeRenderable(flecs::entity entity, const nlohmann::json& json); - void deserializeEntityName(flecs::entity entity, const nlohmann::json& json); - void deserializeRigidBody(flecs::entity entity, const nlohmann::json& json); - void deserializeCollider(flecs::entity entity, const nlohmann::json& json); - void deserializeLight(flecs::entity entity, const nlohmann::json& json); - void deserializeCamera(flecs::entity entity, const nlohmann::json& json); - void deserializeLodSettings(flecs::entity entity, const nlohmann::json& json); - void deserializeLod(flecs::entity entity, const nlohmann::json& json); - void deserializeStaticGeometry(flecs::entity entity, const nlohmann::json& json); - void deserializeStaticGeometryMember(flecs::entity entity, const nlohmann::json& json); - void deserializeProceduralTexture(flecs::entity entity, const nlohmann::json& json); - void deserializeProceduralMaterial(flecs::entity entity, const nlohmann::json& json); - void deserializePrimitive(flecs::entity entity, const nlohmann::json& json); - void deserializeTriangleBuffer(flecs::entity entity, const nlohmann::json& json); - void deserializeCharacter(flecs::entity entity, const nlohmann::json& json); - void deserializeCharacterSlots(flecs::entity entity, const nlohmann::json& json); - void deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json); - void deserializeStartupMenu(flecs::entity entity, const nlohmann::json& json); - void deserializePlayerController(flecs::entity entity, const nlohmann::json& json); - - // CellGrid/Town component deserialization - void deserializeCellGrid(flecs::entity entity, const nlohmann::json& json); - void deserializeTown(flecs::entity entity, const nlohmann::json& json); - void deserializeDistrict(flecs::entity entity, const nlohmann::json& json); - void deserializeLot(flecs::entity entity, const nlohmann::json& json); - void deserializeRoom(flecs::entity entity, const nlohmann::json& json); - void deserializeRoof(flecs::entity entity, const nlohmann::json& json); - void deserializeFurnitureTemplate(flecs::entity entity, const nlohmann::json& json); - void deserializeClearArea(flecs::entity entity, const nlohmann::json& json); - flecs::world& m_world; - Ogre::SceneManager* m_sceneMgr; + // Component deserialization + void deserializeTransform(flecs::entity entity, + const nlohmann::json &json, + flecs::entity parentEntity); + void deserializeRenderable(flecs::entity entity, + const nlohmann::json &json); + void deserializeEntityName(flecs::entity entity, + const nlohmann::json &json); + void deserializeRigidBody(flecs::entity entity, + const nlohmann::json &json); + void deserializeCollider(flecs::entity entity, + const nlohmann::json &json); + void deserializeLight(flecs::entity entity, const nlohmann::json &json); + void deserializeCamera(flecs::entity entity, + const nlohmann::json &json); + void deserializeLodSettings(flecs::entity entity, + const nlohmann::json &json); + void deserializeLod(flecs::entity entity, const nlohmann::json &json); + void deserializeStaticGeometry(flecs::entity entity, + const nlohmann::json &json); + void deserializeStaticGeometryMember(flecs::entity entity, + const nlohmann::json &json); + void deserializeProceduralTexture(flecs::entity entity, + const nlohmann::json &json); + void deserializeProceduralMaterial(flecs::entity entity, + const nlohmann::json &json); + void deserializePrimitive(flecs::entity entity, + const nlohmann::json &json); + void deserializeTriangleBuffer(flecs::entity entity, + const nlohmann::json &json); + void deserializeCharacter(flecs::entity entity, + const nlohmann::json &json); + void deserializeCharacterSlots(flecs::entity entity, + const nlohmann::json &json); + void deserializeAnimationTree(flecs::entity entity, + const nlohmann::json &json); + void deserializeStartupMenu(flecs::entity entity, + const nlohmann::json &json); + void deserializePlayerController(flecs::entity entity, + const nlohmann::json &json); + + // CellGrid/Town component deserialization + void deserializeCellGrid(flecs::entity entity, + const nlohmann::json &json); + void deserializeTown(flecs::entity entity, const nlohmann::json &json); + void deserializeDistrict(flecs::entity entity, + const nlohmann::json &json); + void deserializeLot(flecs::entity entity, const nlohmann::json &json); + void deserializeRoom(flecs::entity entity, const nlohmann::json &json); + void deserializeRoof(flecs::entity entity, const nlohmann::json &json); + void deserializeFurnitureTemplate(flecs::entity entity, + const nlohmann::json &json); + void deserializeClearArea(flecs::entity entity, + const nlohmann::json &json); + + // Buoyancy component serialization + nlohmann::json serializeBuoyancyInfo(flecs::entity entity); + nlohmann::json serializeWaterPhysics(flecs::entity entity); + void deserializeBuoyancyInfo(flecs::entity entity, + const nlohmann::json &json); + void deserializeWaterPhysics(flecs::entity entity, + const nlohmann::json &json); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; std::string m_lastError; - + // Track entity ID mapping for parent/child relationships std::unordered_map m_entityMap; }; diff --git a/src/features/editScene/ui/BuoyancyInfoEditor.cpp b/src/features/editScene/ui/BuoyancyInfoEditor.cpp new file mode 100644 index 0000000..becffed --- /dev/null +++ b/src/features/editScene/ui/BuoyancyInfoEditor.cpp @@ -0,0 +1,103 @@ +#include "BuoyancyInfoEditor.hpp" +#include + +bool BuoyancyInfoEditor::renderComponent(flecs::entity entity, + BuoyancyInfo &buoyancyInfo) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Buoyancy Info", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Enabled checkbox + if (ImGui::Checkbox("Enabled", &buoyancyInfo.enabled)) { + buoyancyInfo.markDirty(); + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Enable or disable buoyancy for this entity"); + } + + ImGui::Separator(); + + // Buoyancy strength + if (ImGui::DragFloat("Buoyancy Strength", + &buoyancyInfo.buoyancy, 0.01f, 0.0f, + 5.0f)) { + buoyancyInfo.markDirty(); + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "0 = no buoyancy, 1 = neutral buoyancy, >1 = floats"); + } + + // Linear drag + if (ImGui::DragFloat("Linear Drag", &buoyancyInfo.linearDrag, + 0.01f, 0.0f, 1.0f)) { + buoyancyInfo.markDirty(); + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Drag force applied to linear movement when submerged (0-1)"); + } + + // Angular drag + if (ImGui::DragFloat("Angular Drag", &buoyancyInfo.angularDrag, + 0.01f, 0.0f, 1.0f)) { + buoyancyInfo.markDirty(); + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Drag force applied to rotation when submerged (0-1)"); + } + + // Submerged threshold + if (ImGui::DragFloat("Submerged Threshold", + &buoyancyInfo.submergedThreshold, 0.01f, + 0.0f, 1.0f)) { + buoyancyInfo.markDirty(); + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "How much of the body must be submerged before buoyancy applies (0-1)"); + } + + ImGui::Separator(); + + // Custom water level + if (ImGui::Checkbox("Use Custom Water Level", + &buoyancyInfo.useCustomWaterLevel)) { + buoyancyInfo.markDirty(); + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Use custom water surface level for this entity instead of global"); + } + + if (buoyancyInfo.useCustomWaterLevel) { + if (ImGui::DragFloat("Water Surface Y", + &buoyancyInfo.waterSurfaceY, 0.1f, + -100.0f, 100.0f)) { + buoyancyInfo.markDirty(); + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Water surface Y coordinate in world space"); + } + } else { + ImGui::Text("Using global water level"); + } + + ImGui::Unindent(); + } + + return modified; +} \ No newline at end of file diff --git a/src/features/editScene/ui/BuoyancyInfoEditor.hpp b/src/features/editScene/ui/BuoyancyInfoEditor.hpp new file mode 100644 index 0000000..6753abd --- /dev/null +++ b/src/features/editScene/ui/BuoyancyInfoEditor.hpp @@ -0,0 +1,21 @@ +#ifndef EDITSCENE_BUOYANCYINFOEDITOR_HPP +#define EDITSCENE_BUOYANCYINFOEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/BuoyancyInfo.hpp" + +/** + * Editor for BuoyancyInfo component + */ +class BuoyancyInfoEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, + BuoyancyInfo &buoyancyInfo) override; + const char *getName() const override + { + return "Buoyancy Info"; + } +}; + +#endif // EDITSCENE_BUOYANCYINFOEDITOR_HPP \ No newline at end of file diff --git a/src/features/editScene/ui/WaterPhysicsEditor.hpp b/src/features/editScene/ui/WaterPhysicsEditor.hpp new file mode 100644 index 0000000..41631b5 --- /dev/null +++ b/src/features/editScene/ui/WaterPhysicsEditor.hpp @@ -0,0 +1,110 @@ +#ifndef WATER_PHYSICS_EDITOR_HPP +#define WATER_PHYSICS_EDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/WaterPhysics.hpp" + +/** + * Editor for WaterPhysics component + */ +class WaterPhysicsEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, + WaterPhysics &component) override + { + bool changed = false; + + ImGui::Text("Water Physics Settings"); + ImGui::Separator(); + + // Enabled toggle + if (ImGui::Checkbox("Enabled", &component.enabled)) { + changed = true; + } + + // Water surface Y level + if (ImGui::SliderFloat("Water Surface Y", + &component.waterSurfaceY, -10.0f, 10.0f, + "%.2f")) { + changed = true; + } + + // Water density + if (ImGui::SliderFloat("Water Density (kg/m³)", + &component.waterDensity, 500.0f, 2000.0f, + "%.0f")) { + changed = true; + } + + // Gravity + if (ImGui::SliderFloat("Gravity (m/s²)", &component.gravity, + 0.0f, 20.0f, "%.2f")) { + changed = true; + } + + ImGui::Separator(); + ImGui::Text("Default Buoyancy Parameters"); + + // Default buoyancy + if (ImGui::SliderFloat("Default Buoyancy", + &component.defaultBuoyancy, 0.0f, 5.0f, + "%.2f")) { + changed = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Default buoyancy density multiplier (1.0 = neutral buoyancy)"); + } + + // Default linear drag + if (ImGui::SliderFloat("Default Linear Drag", + &component.defaultLinearDrag, 0.0f, 1.0f, + "%.3f")) { + changed = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Default linear drag coefficient for objects in water"); + } + + // Default angular drag + if (ImGui::SliderFloat("Default Angular Drag", + &component.defaultAngularDrag, 0.0f, + 1.0f, "%.3f")) { + changed = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Default angular drag coefficient for objects in water"); + } + + // Default submerged threshold + if (ImGui::SliderFloat("Submerged Threshold", + &component.defaultSubmergedThreshold, + 0.0f, 1.0f, "%.2f")) { + changed = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Minimum submerged fraction to apply buoyancy (0.0 = any contact, 1.0 = fully submerged)"); + } + + return changed; + } + + const char *getName() const override + { + return "Water Physics"; + } +}; + +#endif // WATER_PHYSICS_EDITOR_HPP diff --git a/src/features/editScene/ui/WaterPlaneEditor.hpp b/src/features/editScene/ui/WaterPlaneEditor.hpp new file mode 100644 index 0000000..5b429e9 --- /dev/null +++ b/src/features/editScene/ui/WaterPlaneEditor.hpp @@ -0,0 +1,101 @@ +#ifndef WATER_PLANE_EDITOR_HPP +#define WATER_PLANE_EDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/WaterPlane.hpp" + +/** + * Editor for WaterPlane component + */ +class WaterPlaneEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, + WaterPlane &component) override + { + bool changed = false; + + ImGui::Text("Water Plane Visualization"); + ImGui::Separator(); + + // Enabled toggle + if (ImGui::Checkbox("Enabled", &component.enabled)) { + changed = true; + component.markDirty(); + } + + // Auto-update from WaterPhysics + if (ImGui::Checkbox("Auto-update from Water Physics", + &component.autoUpdateFromWaterPhysics)) { + changed = true; + } + + // Water surface Y level (editable if not auto-updating) + if (!component.autoUpdateFromWaterPhysics) { + if (ImGui::SliderFloat("Water Surface Y", + &component.waterSurfaceY, -10.0f, + 10.0f, "%.2f")) { + changed = true; + component.markDirty(); + } + } else { + ImGui::Text( + "Water Surface Y: %.2f (from Water Physics)", + component.waterSurfaceY); + } + + // Plane size + if (ImGui::SliderFloat("Plane Size", &component.planeSize, + 10.0f, 500.0f, "%.0f")) { + changed = true; + component.markDirty(); + } + + ImGui::Separator(); + ImGui::Text("Visual Appearance"); + + // Color picker + float color[4] = { component.color.r, component.color.g, + component.color.b, component.color.a }; + if (ImGui::ColorEdit4("Color", color, + ImGuiColorEditFlags_AlphaBar)) { + component.color = Ogre::ColourValue(color[0], color[1], + color[2], color[3]); + changed = true; + component.markDirty(); + } + + // Opacity + if (ImGui::SliderFloat("Opacity", &component.opacity, 0.0f, + 1.0f, "%.2f")) { + component.color.a = component.opacity; + changed = true; + component.markDirty(); + } + + // Material name (read-only for now) + ImGui::Text("Material: %s", component.materialName.c_str()); + + // Force update button + ImGui::Separator(); + if (ImGui::Button("Force Update")) { + component.markDirty(); + changed = true; + } + ImGui::SameLine(); + if (ImGui::Button("Reset to Defaults")) { + component = WaterPlane(); + changed = true; + component.markDirty(); + } + + return changed; + } + + const char *getName() const override + { + return "Water Plane"; + } +}; + +#endif // WATER_PLANE_EDITOR_HPP \ No newline at end of file