This commit is contained in:
2026-04-22 17:27:40 +03:00
parent 7e4e8f6638
commit ca5b5b3052
25 changed files with 2122 additions and 426 deletions

View File

@@ -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

View File

@@ -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<BuoyancySystem>(
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<EditorLightSystem>(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<PhysicsColliderComponent>();
m_world.component<GeneratedPhysicsTag>();
m_world.component<RigidBodyComponent>();
m_world.component<BuoyancyInfo>();
m_world.component<WaterPhysics>();
m_world.component<WaterPlane>();
// Register light and camera components
m_world.component<LightComponent>();
@@ -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>(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();

View File

@@ -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<EditorCamera> m_camera;
std::unique_ptr<ImGuiRenderListener> m_imguiListener;
std::unique_ptr<EditorPhysicsSystem> m_physicsSystem;
std::unique_ptr<BuoyancySystem> m_buoyancySystem;
std::unique_ptr<EditorLightSystem> m_lightSystem;
std::unique_ptr<EditorCameraSystem> m_cameraSystem;
std::unique_ptr<EditorLodSystem> 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;

View File

@@ -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<OgreBites::CameraMan> m_cameraMan;

View File

@@ -0,0 +1,46 @@
#ifndef EDITSCENE_BUOYANCYINFO_HPP
#define EDITSCENE_BUOYANCYINFO_HPP
#pragma once
#include <Ogre.h>
/**
* 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

View File

@@ -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<BuoyancyInfo>(
"Buoyancy Info", std::make_unique<BuoyancyInfoEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<BuoyancyInfo>()) {
e.set<BuoyancyInfo>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<BuoyancyInfo>()) {
e.remove<BuoyancyInfo>();
}
});
}

View File

@@ -0,0 +1,43 @@
#ifndef WATER_PHYSICS_HPP
#define WATER_PHYSICS_HPP
#include <Ogre.h>
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

View File

@@ -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<WaterPhysics>(
"Water Physics", std::make_unique<WaterPhysicsEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<WaterPhysics>()) {
e.set<WaterPhysics>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<WaterPhysics>()) {
e.remove<WaterPhysics>();
}
});
}

View File

@@ -0,0 +1,63 @@
#ifndef EDITSCENE_WATERPLANE_HPP
#define EDITSCENE_WATERPLANE_HPP
#pragma once
#include <Ogre.h>
/**
* 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

View File

@@ -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 <OgreLogManager.h>
#include <OgreManualObject.h>
#include <OgreSceneManager.h>
#include <OgreSceneNode.h>
#include <OgreMaterialManager.h>
#include <OgreTechnique.h>
#include <OgrePass.h>
// Register WaterPlane component
REGISTER_COMPONENT("Water Plane", WaterPlane, WaterPlaneEditor)
{
registry.registerComponent<WaterPlane>(
"Water Plane", "Water", std::make_unique<WaterPlaneEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<WaterPlane>()) {
e.set<WaterPlane>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<WaterPlane>()) {
e.remove<WaterPlane>();
}
});
}
/**
* 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<WaterPlane>();
// Create system for updating water planes
world.system<WaterPlane>("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<WaterPlane>("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<WaterPlane>(
[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<WaterPhysics>().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<Ogre::Material>();
// 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<WaterPlane>().each(
[&](flecs::entity, WaterPlane &) {
hasWaterPlane = true;
});
if (!hasWaterPlane) {
flecs::entity waterPlaneEntity =
world.entity("WaterPlane");
waterPlaneEntity.set<WaterPlane>({});
waterPlaneEntity.add<EditorMarkerComponent>();
waterPlaneEntity.set<EntityNameComponent>(
EntityNameComponent("Water Plane"));
// Add Transform component for potential future use
waterPlaneEntity.set<TransformComponent>(
TransformComponent());
Ogre::LogManager::getSingleton().logMessage(
"Created default WaterPlane entity");
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<JPH::Vec3>(position),
shapeSettings.AddShape(JoltPhysics::convert<JPH::Vec3>(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<JPH::Vec3>(impulse));
body_interface.AddAngularImpulse(
id, JoltPhysics::convert<JPH::Vec3>(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<JPH::MutableCompoundShape *>(
compoundShape.GetPtr());
master->AddShape(JoltPhysics::convert<JPH::Vec3>(position),
master->AddShape(JoltPhysics::convert<JPH::Vec3>(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<JPH::Vec3>(offset),
JoltPhysics::convert<JPH::Vec3>(scale),
(uint32_t)sampleCount);
samples, JoltPhysics::convert<JPH::Vec3>(offset),
JoltPhysics::convert<JPH::Vec3>(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<JPH::Vec3>(positions[i]),
JoltPhysics::convert(rotations[i]),
shapes[i].GetPtr());
settings.AddShape(
JoltPhysics::convert<JPH::Vec3>(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<JPH::Vec3>(positions[i]),
JoltPhysics::convert(rotations[i]),
shapes[i].GetPtr());
settings.AddShape(
JoltPhysics::convert<JPH::Vec3>(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<JPH::Vec3>(offset),
shape.GetPtr());
JoltPhysics::convert<JPH::Vec3>(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<JPH::Vec3>(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<JPH::Vec3>(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<JPH::Vec3>(surfaceNormal),
buoyancy, linearDrag, angularDrag,
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
JoltPhysics::convert<JPH::Vec3>(gravity), dt);
body.ApplyBuoyancyImpulse(
JoltPhysics::convert(surfacePosition),
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
buoyancy, linearDrag, angularDrag,
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
JoltPhysics::convert<JPH::Vec3>(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<JPH::Vec3>(surfaceNormal),
buoyancy, linearDrag, angularDrag,
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
physics_system.GetGravity(), dt);
body.ApplyBuoyancyImpulse(
JoltPhysics::convert(surfacePosition),
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
buoyancy, linearDrag, angularDrag,
JoltPhysics::convert<JPH::Vec3>(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<JPH::BodyID> &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<JPH::Vec3>(direction) };
JoltPhysics::convert<JPH::Vec3>(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<JPH::Character> 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<JPH::Character> 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<Physics>(scnMgr, cameraNode, nullptr,
&contacts);
phys = std::make_unique<Physics>(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<JPH::Character> ch)
{
phys->destroyCharacter(ch);
phys->destroyCharacter(ch);
}
Ogre::SceneNode *
JoltPhysicsWrapper::getSceneNodeFromBodyID(JPH::BodyID id) const
{
return phys->getSceneNodeFromBodyID(id);
}
template <>
JoltPhysicsWrapper *Ogre::Singleton<JoltPhysicsWrapper>::msSingleton = 0;

View File

@@ -45,14 +45,13 @@ static constexpr uint NUM_LAYERS(2);
namespace JoltPhysics
{
template<class T>
Ogre::Vector3 convert(const T &vec)
template <class T> Ogre::Vector3 convert(const T &vec)
{
return {vec[0], vec[1], vec[2]};
return { vec[0], vec[1], vec[2] };
}
template<class T> T convert(const Ogre::Vector3 &vec)
template <class T> 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<JPH::Character> ch);
Ogre::Vector3 &position, JPH::BodyID &id);
bool bodyIsCharacter(JPH::BodyID id) const;
void destroyCharacter(std::shared_ptr<JPH::Character> ch);
Ogre::SceneNode *getSceneNodeFromBodyID(JPH::BodyID id) const;
};
#endif

View File

@@ -0,0 +1,328 @@
#include "BuoyancySystem.hpp"
#include "../components/EditorMarker.hpp"
#include "../components/EntityName.hpp"
#include "../components/Character.hpp"
#include <OgreLogManager.h>
#include <set>
BuoyancySystem::BuoyancySystem(flecs::world &world, JoltPhysicsWrapper *physics)
: m_world(world)
, m_physics(physics)
, m_buoyancyInfoQuery(world.query<BuoyancyInfo>())
{
// Create default WaterPhysics entity if none exists
bool hasWaterPhysics = false;
world.query<WaterPhysics>().each(
[&](flecs::entity, WaterPhysics &) { hasWaterPhysics = true; });
if (!hasWaterPhysics) {
flecs::entity waterEntity = world.entity("WaterPhysics");
waterEntity.set<WaterPhysics>({});
waterEntity.add<EditorMarkerComponent>();
waterEntity.set<EntityNameComponent>(
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<WaterPhysics>().each(
[&](flecs::entity entity, WaterPhysics &wp) {
waterPhysics = &wp;
});
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<TransformComponent>().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<flecs::entity_t> 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<CharacterComponent>()) +
")");
}
// Check if entity has BuoyancyInfo component
if (entitiesWithBuoyancyInfo.find(entity.id()) !=
entitiesWithBuoyancyInfo.end()) {
const BuoyancyInfo *buoyancyInfo =
entity.try_get<BuoyancyInfo>();
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<WaterPhysics>().each(
[&](flecs::entity entity, WaterPhysics &wp) {
waterPhysics = &wp;
});
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<CharacterComponent, TransformComponent>().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 ===");
}

View File

@@ -0,0 +1,98 @@
#ifndef EDITSCENE_BUOYANCYSYSTEM_HPP
#define EDITSCENE_BUOYANCYSYSTEM_HPP
#pragma once
#include <flecs.h>
#include <Ogre.h>
#include <memory>
#include <unordered_map>
#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<JPH::BodyID> m_bodiesInWater;
// Cache for scene node -> entity mapping
std::unordered_map<Ogre::SceneNode *, flecs::entity_t> m_nodeToEntity;
// Query for entities with BuoyancyInfo component
flecs::query<BuoyancyInfo> m_buoyancyInfoQuery;
};
#endif // EDITSCENE_BUOYANCYSYSTEM_HPP

View File

@@ -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<JPH::ShapeRefC> shapes;
std::vector<Ogre::Vector3> 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<JPH::Character *>(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<CharacterComponent, TransformComponent>().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<JPH::Vec3>(
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()));
});
}

View File

@@ -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 <imgui.h>
#include <algorithm>
#include <filesystem>
@@ -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<PhysicsColliderEditor>(m_sceneMgr);
auto colliderEditor =
std::make_unique<PhysicsColliderEditor>(m_sceneMgr);
m_componentRegistry.registerComponent<PhysicsColliderComponent>(
"Physics Collider", "Physics", std::move(colliderEditor),
// Adder
@@ -171,7 +175,7 @@ void EditorUISystem::registerComponentEditors()
e.remove<PhysicsColliderComponent>();
}
});
// 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<TransformComponent>()) {
@@ -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<WaterPhysics>().each(
[&](flecs::entity entity,
WaterPhysics &wp) {
waterPhysics = &wp;
});
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<TransformComponent>()) {
auto &transform = entity.get_mut<TransformComponent>();
if (m_componentRegistry.render<TransformComponent>(entity, transform)) {
if (m_componentRegistry.render<TransformComponent>(entity,
transform)) {
// Transform changed - mark StaticGeometryMember dirty if present
if (entity.has<StaticGeometryMemberComponent>()) {
entity.get_mut<StaticGeometryMemberComponent>().markDirty();
entity.get_mut<StaticGeometryMemberComponent>()
.markDirty();
}
}
componentCount++;
@@ -550,7 +641,8 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
// Render Renderable
if (entity.has<RenderableComponent>()) {
auto &renderable = entity.get_mut<RenderableComponent>();
m_componentRegistry.render<RenderableComponent>(entity, renderable);
m_componentRegistry.render<RenderableComponent>(entity,
renderable);
componentCount++;
}
@@ -571,66 +663,81 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
// Render RigidBody
if (entity.has<RigidBodyComponent>()) {
auto &rigidBody = entity.get_mut<RigidBodyComponent>();
m_componentRegistry.render<RigidBodyComponent>(entity, rigidBody);
m_componentRegistry.render<RigidBodyComponent>(entity,
rigidBody);
componentCount++;
}
// Render PhysicsCollider
if (entity.has<PhysicsColliderComponent>()) {
auto &collider = entity.get_mut<PhysicsColliderComponent>();
m_componentRegistry.render<PhysicsColliderComponent>(entity, collider);
m_componentRegistry.render<PhysicsColliderComponent>(entity,
collider);
componentCount++;
}
// Render BuoyancyInfo if present
if (entity.has<BuoyancyInfo>()) {
auto &buoyancy = entity.get_mut<BuoyancyInfo>();
m_componentRegistry.render<BuoyancyInfo>(entity, buoyancy);
componentCount++;
}
// Render LOD Settings if present
if (entity.has<LodSettingsComponent>()) {
auto &lodSettings = entity.get_mut<LodSettingsComponent>();
m_componentRegistry.render<LodSettingsComponent>(entity, lodSettings);
m_componentRegistry.render<LodSettingsComponent>(entity,
lodSettings);
componentCount++;
}
// Render LOD if present
if (entity.has<LodComponent>()) {
auto &lod = entity.get_mut<LodComponent>();
m_componentRegistry.render<LodComponent>(entity, lod);
componentCount++;
}
// Render StaticGeometry Region if present
if (entity.has<StaticGeometryComponent>()) {
auto &region = entity.get_mut<StaticGeometryComponent>();
m_componentRegistry.render<StaticGeometryComponent>(entity, region);
m_componentRegistry.render<StaticGeometryComponent>(entity,
region);
componentCount++;
}
// Render StaticGeometry Member if present
if (entity.has<StaticGeometryMemberComponent>()) {
auto &member = entity.get_mut<StaticGeometryMemberComponent>();
m_componentRegistry.render<StaticGeometryMemberComponent>(entity, member);
m_componentRegistry.render<StaticGeometryMemberComponent>(
entity, member);
componentCount++;
}
// Render ProceduralTexture if present
if (entity.has<ProceduralTextureComponent>()) {
auto &texture = entity.get_mut<ProceduralTextureComponent>();
m_componentRegistry.render<ProceduralTextureComponent>(entity, texture);
m_componentRegistry.render<ProceduralTextureComponent>(entity,
texture);
componentCount++;
}
// Render ProceduralMaterial if present
if (entity.has<ProceduralMaterialComponent>()) {
auto &material = entity.get_mut<ProceduralMaterialComponent>();
m_componentRegistry.render<ProceduralMaterialComponent>(entity, material);
m_componentRegistry.render<ProceduralMaterialComponent>(
entity, material);
componentCount++;
}
// Render Primitive if present
if (entity.has<PrimitiveComponent>()) {
auto &primitive = entity.get_mut<PrimitiveComponent>();
m_componentRegistry.render<PrimitiveComponent>(entity, primitive);
m_componentRegistry.render<PrimitiveComponent>(entity,
primitive);
componentCount++;
}
// Render TriangleBuffer if present
if (entity.has<TriangleBufferComponent>()) {
auto &tb = entity.get_mut<TriangleBufferComponent>();
@@ -669,7 +776,8 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
// Render PlayerController if present
if (entity.has<PlayerControllerComponent>()) {
auto &pc = entity.get_mut<PlayerControllerComponent>();
m_componentRegistry.render<PlayerControllerComponent>(entity, pc);
m_componentRegistry.render<PlayerControllerComponent>(entity,
pc);
componentCount++;
}
@@ -679,35 +787,35 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
m_componentRegistry.render<CellGridComponent>(entity, grid);
componentCount++;
}
// Render Lot if present
if (entity.has<LotComponent>()) {
auto &lot = entity.get_mut<LotComponent>();
m_componentRegistry.render<LotComponent>(entity, lot);
componentCount++;
}
// Render District if present
if (entity.has<DistrictComponent>()) {
auto &district = entity.get_mut<DistrictComponent>();
m_componentRegistry.render<DistrictComponent>(entity, district);
componentCount++;
}
// Render Town if present
if (entity.has<TownComponent>()) {
auto &town = entity.get_mut<TownComponent>();
m_componentRegistry.render<TownComponent>(entity, town);
componentCount++;
}
// Render Roof if present
if (entity.has<RoofComponent>()) {
auto &roof = entity.get_mut<RoofComponent>();
m_componentRegistry.render<RoofComponent>(entity, roof);
componentCount++;
}
// Render Room if present
if (entity.has<RoomComponent>()) {
auto &room = entity.get_mut<RoomComponent>();
@@ -716,24 +824,22 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
}
componentCount++;
}
// Render ClearArea if present
if (entity.has<ClearAreaComponent>()) {
auto &clearArea = entity.get_mut<ClearAreaComponent>();
if (m_componentRegistry.render<ClearAreaComponent>(entity, clearArea)) {
if (m_componentRegistry.render<ClearAreaComponent>(entity,
clearArea)) {
clearArea.markDirty();
}
componentCount++;
}
// Render FurnitureTemplate if present
if (entity.has<FurnitureTemplateComponent>()) {
auto &furniture = entity.get_mut<FurnitureTemplateComponent>();
m_componentRegistry.render<FurnitureTemplateComponent>(entity, furniture);
m_componentRegistry.render<FurnitureTemplateComponent>(
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<TransformComponent>();
// 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<TransformComponent>(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<TransformComponent>()) {
parentNode = parent.get<TransformComponent>().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");
}

View File

@@ -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<SceneSerializer> 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;

View File

@@ -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<JoltPhysicsWrapper> m_physics;
bool m_initialized = false;
bool m_debugDraw = false;

View File

@@ -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 <random>
#include <fstream>
@@ -243,6 +245,15 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
json["clearArea"] = serializeClearArea(entity);
}
// Buoyancy components
if (entity.has<BuoyancyInfo>()) {
json["buoyancyInfo"] = serializeBuoyancyInfo(entity);
}
if (entity.has<WaterPhysics>()) {
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<PlayerControllerComponent>(pc);
}
// ============================================================================
// Buoyancy Component Serialization
// ============================================================================
nlohmann::json SceneSerializer::serializeBuoyancyInfo(flecs::entity entity)
{
auto &buoyancy = entity.get<BuoyancyInfo>();
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<WaterPhysics>();
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<BuoyancyInfo>(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<WaterPhysics>(water);
}

View File

@@ -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<uint64_t, flecs::entity> m_entityMap;
};

View File

@@ -0,0 +1,103 @@
#include "BuoyancyInfoEditor.hpp"
#include <imgui.h>
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;
}

View File

@@ -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<BuoyancyInfo> {
public:
bool renderComponent(flecs::entity entity,
BuoyancyInfo &buoyancyInfo) override;
const char *getName() const override
{
return "Buoyancy Info";
}
};
#endif // EDITSCENE_BUOYANCYINFOEDITOR_HPP

View File

@@ -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<WaterPhysics> {
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

View File

@@ -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<WaterPlane> {
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