diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index d5d2585..13a4571 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -5,6 +5,7 @@ find_package(OGRE REQUIRED COMPONENTS Bites Overlay CONFIG) find_package(flecs REQUIRED CONFIG) find_package(nlohmann_json REQUIRED) find_package(SDL2 REQUIRED) +find_package(Jolt REQUIRED) # Collect all source files set(EDITSCENE_SOURCES @@ -12,10 +13,14 @@ set(EDITSCENE_SOURCES EditorApp.cpp systems/EditorUISystem.cpp systems/SceneSerializer.cpp + systems/PhysicsSystem.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp + ui/PhysicsColliderEditor.cpp + ui/RigidBodyEditor.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp + physics/physics.cpp ) set(EDITSCENE_HEADERS @@ -24,24 +29,34 @@ set(EDITSCENE_HEADERS components/Renderable.hpp components/EntityName.hpp components/Relationship.hpp + components/PhysicsCollider.hpp + components/RigidBody.hpp systems/EditorUISystem.hpp systems/SceneSerializer.hpp + systems/PhysicsSystem.hpp ui/ComponentEditor.hpp ui/ComponentRegistry.hpp ui/TransformEditor.hpp ui/RenderableEditor.hpp + ui/PhysicsColliderEditor.hpp + ui/RigidBodyEditor.hpp camera/EditorCamera.hpp gizmo/Gizmo.hpp + physics/physics.h ) add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS}) +# Define JPH_DEBUG_RENDERER for physics debug drawing +target_compile_definitions(editSceneEditor PRIVATE JPH_DEBUG_RENDERER) + target_link_libraries(editSceneEditor OgreMain OgreBites OgreOverlay flecs::flecs_static nlohmann_json::nlohmann_json + Jolt::Jolt ) target_include_directories(editSceneEditor PRIVATE diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index bcf29d8..3262e9f 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -1,11 +1,14 @@ #include #include "EditorApp.hpp" #include "systems/EditorUISystem.hpp" +#include "systems/PhysicsSystem.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" #include "components/Renderable.hpp" #include "components/EditorMarker.hpp" +#include "components/PhysicsCollider.hpp" +#include "components/RigidBody.hpp" #include #include @@ -104,6 +107,11 @@ void EditorApp::setup() // Setup UI system m_uiSystem = std::make_unique(m_world, m_sceneMgr); + // Setup physics system + m_physicsSystem = std::make_unique(m_world, m_sceneMgr); + m_physicsSystem->initialize(); + m_uiSystem->setPhysicsSystem(m_physicsSystem.get()); + // Add default entities to UI cache for (auto &e : m_defaultEntities) { m_uiSystem->addEntity(e); @@ -131,6 +139,10 @@ void EditorApp::setupECS() m_world.component(); m_world.component(); m_world.component(); + + // Register physics components + m_world.component(); + m_world.component(); } void EditorApp::createDefaultEntities() @@ -254,6 +266,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_camera->update(evt.timeSinceLastFrame); } + // Update physics + if (m_physicsSystem) { + m_physicsSystem->update(evt.timeSinceLastFrame); + } + // Don't call base class - it crashes when iterating input listeners return true; } diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index 5260694..3f057e9 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -13,6 +13,7 @@ // Forward declarations class EditorUISystem; class EditorCamera; +class EditorPhysicsSystem; /** * RenderTargetListener for ImGui frame management @@ -78,6 +79,7 @@ private: std::unique_ptr m_uiSystem; std::unique_ptr m_camera; std::unique_ptr m_imguiListener; + std::unique_ptr m_physicsSystem; // State uint16_t m_currentModifiers; diff --git a/src/features/editScene/components/PhysicsCollider.hpp b/src/features/editScene/components/PhysicsCollider.hpp new file mode 100644 index 0000000..ff72ed3 --- /dev/null +++ b/src/features/editScene/components/PhysicsCollider.hpp @@ -0,0 +1,50 @@ +#ifndef EDITSCENE_PHYSICSCOLLIDER_HPP +#define EDITSCENE_PHYSICSCOLLIDER_HPP +#pragma once + +#include +#include +#include +#include + +/** + * Physics Collider component + * Defines a collision shape for use in rigid bodies + * Multiple colliders can be attached to a single rigid body (as children) + */ +struct PhysicsColliderComponent { + // Collider shape types + enum class ShapeType { + Box, + Sphere, + Capsule, + Cylinder, + Mesh, + ConvexHull + }; + + ShapeType shapeType = ShapeType::Box; + + // Shape parameters + // Box: half extents + // Sphere: radius + // Capsule/Cylinder: halfHeight, radius + Ogre::Vector3 parameters = Ogre::Vector3(0.5f, 0.5f, 0.5f); // For box: half extents + float radius = 0.5f; // For sphere, capsule, cylinder + float halfHeight = 1.0f; // For capsule, cylinder + + // For mesh/convex hull colliders + std::string meshName; + + // Offset from parent transform + Ogre::Vector3 offset = Ogre::Vector3::ZERO; + Ogre::Quaternion rotationOffset = Ogre::Quaternion::IDENTITY; + + // The actual Jolt shape (created when body is built) + JPH::ShapeRefC shape; + bool shapeDirty = true; // Needs rebuild + + void markDirty() { shapeDirty = true; } +}; + +#endif // EDITSCENE_PHYSICSCOLLIDER_HPP diff --git a/src/features/editScene/components/RigidBody.hpp b/src/features/editScene/components/RigidBody.hpp new file mode 100644 index 0000000..24f2956 --- /dev/null +++ b/src/features/editScene/components/RigidBody.hpp @@ -0,0 +1,40 @@ +#ifndef EDITSCENE_RIGIDBODY_HPP +#define EDITSCENE_RIGIDBODY_HPP +#pragma once + +#include +#include +#include + +/** + * RigidBody component + * Represents a physics body that can be static or dynamic + * Uses children's Collider components for collision shapes + */ +struct RigidBodyComponent { + // Body type + enum class BodyType { + Static, // Non-moving (infinite mass) + Dynamic, // Affected by forces and collisions + Kinematic // Controlled by code, not physics + }; + + BodyType bodyType = BodyType::Static; + + // Mass (only used for dynamic bodies) + float mass = 1.0f; + + // Physics properties + float friction = 0.5f; + float restitution = 0.0f; // Bounciness + bool isSensor = false; // Detects collisions but doesn't respond + + // The Jolt body ID + JPH::BodyID bodyID; + bool bodyCreated = false; + bool bodyDirty = true; // Needs rebuild + + void markDirty() { bodyDirty = true; } +}; + +#endif // EDITSCENE_RIGIDBODY_HPP diff --git a/src/features/editScene/physics/physics.cpp b/src/features/editScene/physics/physics.cpp new file mode 100644 index 0000000..bd15c13 --- /dev/null +++ b/src/features/editScene/physics/physics.cpp @@ -0,0 +1,1837 @@ +// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) +// SPDX-FileCopyrightText: 2025 Jorrit Rouwe +// SPDX-License-Identifier: CC0-1.0 +// This file is in the public domain. It serves as an example to start building your own application using Jolt Physics. Feel free to copy paste without attribution! + +// The Jolt headers don't include Jolt.h. Always include Jolt.h before including any other Jolt header. +// You can use Jolt.h in your precompiled header to speed up compilation. +#include +#include +#include + +// Jolt includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// STL includes +#include +#include +#include +#include "physics.h" + +// Disable common warnings triggered by Jolt, you can use JPH_SUPPRESS_WARNING_PUSH / JPH_SUPPRESS_WARNING_POP to store and restore the warning state +JPH_SUPPRESS_WARNINGS + +// All Jolt symbols are in the JPH namespace + +// Callback for traces, connect this to your own trace function if you have one +static void TraceImpl(const char *inFMT, ...) +{ + // Format the message + va_list list; + va_start(list, inFMT); + char buffer[1024]; + vsnprintf(buffer, sizeof(buffer), inFMT, list); + va_end(list); + + // Print to the TTY + std::cout << buffer << std::endl; +} + +#ifdef JPH_ENABLE_ASSERTS + +// Callback for asserts, connect this to your own assert handler if you have one +static bool AssertFailedImpl(const char *inExpression, const char *inMessage, + const char *inFile, uint inLine) +{ + // Print to the TTY + std::cout << inFile << ":" << inLine << ": (" << inExpression << ") " + << (inMessage != nullptr ? inMessage : "") << std::endl; + + // Breakpoint + return true; +}; + +#endif // JPH_ENABLE_ASSERTS + +/// Class that determines if two object layers can collide +class ObjectLayerPairFilterImpl : public JPH::ObjectLayerPairFilter { +public: + virtual bool ShouldCollide(JPH::ObjectLayer inObject1, + JPH::ObjectLayer inObject2) const override + { + switch (inObject1) { + case Layers::NON_MOVING: + return inObject2 == + 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: + JPH_ASSERT(false); + return false; + } + } +}; + +// BroadPhaseLayerInterface implementation +// This defines a mapping between object and broadphase layers. +class BPLayerInterfaceImpl final : public JPH::BroadPhaseLayerInterface { +public: + BPLayerInterfaceImpl() + { + // Create a mapping table from object to broad phase layer + mObjectToBroadPhase[Layers::NON_MOVING] = + BroadPhaseLayers::NON_MOVING; + mObjectToBroadPhase[Layers::MOVING] = BroadPhaseLayers::MOVING; + mObjectToBroadPhase[Layers::SENSORS] = BroadPhaseLayers::MOVING; + } + + virtual uint GetNumBroadPhaseLayers() const override + { + return BroadPhaseLayers::NUM_LAYERS; + } + + virtual JPH::BroadPhaseLayer + GetBroadPhaseLayer(JPH::ObjectLayer inLayer) const override + { + JPH_ASSERT(inLayer < Layers::NUM_LAYERS); + return mObjectToBroadPhase[inLayer]; + } + +#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED) + virtual const char * + GetBroadPhaseLayerName(JPH::BroadPhaseLayer inLayer) const override + { + switch ((JPH::BroadPhaseLayer::Type)inLayer) { + case (JPH::BroadPhaseLayer::Type)BroadPhaseLayers::NON_MOVING: + return "NON_MOVING"; + case (JPH::BroadPhaseLayer::Type)BroadPhaseLayers::MOVING: + return "MOVING"; + default: + JPH_ASSERT(false); + return "INVALID"; + } + } +#endif // JPH_EXTERNAL_PROFILE || JPH_PROFILE_ENABLED + +private: + JPH::BroadPhaseLayer mObjectToBroadPhase[Layers::NUM_LAYERS]; +}; + +/// Class that determines if an object layer can collide with a broadphase layer +class ObjectVsBroadPhaseLayerFilterImpl + : public JPH::ObjectVsBroadPhaseLayerFilter { +public: + virtual bool ShouldCollide(JPH::ObjectLayer inLayer1, + JPH::BroadPhaseLayer inLayer2) const override + { + switch (inLayer1) { + case Layers::NON_MOVING: + return inLayer2 == BroadPhaseLayers::MOVING; + case Layers::MOVING: + return true; + default: + JPH_ASSERT(false); + return false; + } + } +}; + +#if 0 +// An example contact listener +class MyContactListener : public JPH::ContactListener { +public: + // See: ContactListener + virtual JPH::ValidateResult OnContactValidate( + const JPH::Body &inBody1, const JPH::Body &inBody2, + JPH::RVec3Arg inBaseOffset, + const JPH::CollideShapeResult &inCollisionResult) override + { + std::cout << "Contact validate callback" << std::endl; + + // Allows you to ignore a contact before it is created (using layers to not make objects collide is cheaper!) + return JPH::ValidateResult::AcceptAllContactsForThisBodyPair; + } + + virtual void OnContactAdded(const JPH::Body &inBody1, + const JPH::Body &inBody2, + const JPH::ContactManifold &inManifold, + JPH::ContactSettings &ioSettings) override + { + std::cout << "A contact was added" << std::endl; + } + + virtual void + OnContactPersisted(const JPH::Body &inBody1, const JPH::Body &inBody2, + const JPH::ContactManifold &inManifold, + JPH::ContactSettings &ioSettings) override + { + std::cout << "A contact was persisted" << std::endl; + } + + virtual void + OnContactRemoved(const JPH::SubShapeIDPair &inSubShapePair) override + { + std::cout << "A contact was removed" << std::endl; + } +}; + +// An example activation listener +class MyBodyActivationListener : public JPH::BodyActivationListener { +public: + virtual void OnBodyActivated(const JPH::BodyID &inBodyID, + JPH::uint64 inBodyUserData) override + { + std::cout << "A body got activated" << std::endl; + } + + virtual void OnBodyDeactivated(const JPH::BodyID &inBodyID, + JPH::uint64 inBodyUserData) override + { + std::cout << "A body went to sleep" << std::endl; + } +}; +#endif +class MyCollector : public JPH::CollideShapeBodyCollector { +public: + MyCollector(JPH::PhysicsSystem *inSystem, + JPH::RVec3Arg inSurfacePosition, + JPH::Vec3Arg inSurfaceNormal, float inDeltaTime) + : mSystem(inSystem) + , mSurfacePosition(inSurfacePosition) + , mSurfaceNormal(inSurfaceNormal) + , mDeltaTime(inDeltaTime) + { + } + + virtual void AddHit(const JPH::BodyID &inBodyID) override + { + mInWater.insert(inBodyID); + } + +private: + JPH::PhysicsSystem *mSystem; + JPH::RVec3 mSurfacePosition; + float mDeltaTime; + +public: + std::set mInWater; + JPH::Vec3 mSurfaceNormal; +}; + +class DebugRenderer final : public JPH::DebugRendererSimple { + Ogre::ManualObject *mObject; + Ogre::SceneManager *mScnMgr; + Ogre::SceneNode *mCameraNode; + Ogre::MaterialPtr mat; + struct line { + Ogre::Vector3 from; + Ogre::Vector3 to; + Ogre::ColourValue c; + }; + std::vector mLines; + struct tri { + Ogre::Vector3 p[3]; + Ogre::ColourValue c; + }; + std::vector mTriangles; + +public: + DebugRenderer(Ogre::SceneManager *scnMgr, Ogre::SceneNode *cameraNode); + ~DebugRenderer() override; + 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]) }); + } + void DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2, + JPH::RVec3Arg inV3, JPH::ColorArg inColor, + ECastShadow inCastShadow = ECastShadow::Off) override + { + Ogre::Vector3 d = mCameraNode->_getDerivedOrientation() * + Ogre::Vector3(0, 0, -1); + JPH::Vec4 color = inColor.ToVec4(); + Ogre::Vector3 p1 = JoltPhysics::convert(inV1); + Ogre::Vector3 p2 = JoltPhysics::convert(inV2); + Ogre::Vector3 p3 = JoltPhysics::convert(inV3); + Ogre::ColourValue cv(color[0], color[1], color[2], color[3]); + +#if 0 + float dproj1 = p1.dotProduct(d); + float dproj2 = p2.dotProduct(d); + float dproj3 = p3.dotProduct(d); + if (dproj1 < 0 && dproj2 < 0 && dproj3 < 0) + return; + if (dproj1 > 50 && dproj2 > 50 && dproj3 > 50) + return; +#endif + mLines.push_back({ p1, p2, cv }); +#if 0 + mTriangles.push_back({ { { inV1[0], inV1[1], inV1[2] }, + { inV2[0], inV2[1], inV2[2] }, + { inV3[0], inV3[1], inV3[2] } }, + Ogre::ColourValue(color[0], color[1], + color[2], color[3]) }); +#endif + } +#if 0 + Batch CreateTriangleBatch(const Triangle *inTriangles, + int inTriangleCount) override + { + return Batch(); + } + Batch CreateTriangleBatch(const Vertex *inVertices, int inVertexCount, + const JPH::uint32 *inIndices, + int inIndexCount) override + { + return Batch(); + } +#endif + void DrawText3D(JPH::RVec3Arg inPosition, + const std::string_view &inString, + JPH::ColorArg inColor = JPH::Color::sWhite, + float inHeight = 0.5f) override + { + std::cout << "text\n"; + } +#if 0 + void DrawGeometry(JPH::RMat44Arg inModelMatrix, + const JPH::AABox &inWorldSpaceBounds, + float inLODScaleSq, JPH::ColorArg inModelColor, + const GeometryRef &inGeometry, + ECullMode inCullMode = ECullMode::CullBackFace, + ECastShadow inCastShadow = ECastShadow::On, + EDrawMode inDrawMode = EDrawMode::Solid) override + { + std::cout << "geometry\n"; + } +#endif + void finish() + { + Ogre::Vector3 d = mCameraNode->_getDerivedOrientation() * + Ogre::Vector3(0, 0, -1); + int i; + mObject->clear(); + mObject->begin(mat, Ogre::RenderOperation::OT_LINE_LIST); + for (i = 0; i < mLines.size(); i++) { +#if 0 + float dproj1 = mLines[i].from.dotProduct(d); + float dproj2 = mLines[i].to.dotProduct(d); + if (dproj1 < 0 && dproj2 < 0) + continue; + if (dproj1 > 50 && dproj2 > 50) + continue; +#endif + mObject->position(mLines[i].from); + mObject->colour(mLines[i].c); + mObject->position(mLines[i].to); + mObject->colour(mLines[i].c); + } + mObject->end(); + mLines.clear(); +#if 0 + mObject->begin(mat, Ogre::RenderOperation::OT_TRIANGLE_LIST); + for (i = 0; i < mTriangles.size(); i++) { + mObject->position(mTriangles[i].p[0]); + mObject->colour(mTriangles[i].c); + mObject->position(mTriangles[i].p[1]); + mObject->colour(mTriangles[i].c); + mObject->position(mTriangles[i].p[2]); + mObject->colour(mTriangles[i].c); + mObject->triangle(0, 1, 2); + mObject->triangle(2, 1, 0); + std::cout << mTriangles[i].p[0] << " "; + std::cout << mTriangles[i].p[1] << " "; + std::cout << mTriangles[i].p[2] << " " << std::endl; + } + mObject->end(); +#else +#if 0 + mObject->begin(mat, Ogre::RenderOperation::OT_LINE_LIST); + for (i = 0; i < mTriangles.size(); i++) { + float dproj1 = mTriangles[i].p[0].dotProduct(d); + float dproj2 = mTriangles[i].p[1].dotProduct(d); + float dproj3 = mTriangles[i].p[2].dotProduct(d); + if (dproj1 < 0 && dproj2 < 0 && dproj3 < 0) + continue; + if (dproj1 > 50 && dproj2 > 50 && dproj3 > 50) + continue; + mObject->position(mTriangles[i].p[0]); + mObject->colour(mTriangles[i].c); + mObject->position(mTriangles[i].p[1]); + mObject->colour(mTriangles[i].c); + mObject->position(mTriangles[i].p[1]); + mObject->colour(mTriangles[i].c); + mObject->position(mTriangles[i].p[2]); + mObject->colour(mTriangles[i].c); + mObject->position(mTriangles[i].p[2]); + mObject->colour(mTriangles[i].c); + mObject->position(mTriangles[i].p[0]); + mObject->colour(mTriangles[i].c); + } + mObject->end(); +#endif +#endif + mTriangles.clear(); + } +}; +DebugRenderer::DebugRenderer(Ogre::SceneManager *scnMgr, + Ogre::SceneNode *cameraNode) + : mObject(scnMgr->createManualObject("joltDebugRenderer")) + , mScnMgr(scnMgr) + , mCameraNode(cameraNode) + , mat(Ogre::MaterialManager::getSingleton().create("joltDebugDraw", + "General")) +{ + Ogre::Technique *technique = mat->getTechnique(0); + Ogre::Pass *pass = technique->getPass(0); + Ogre::ColourValue color(1, 0, 0, 1); + pass->setCullingMode(Ogre::CullingMode::CULL_NONE); + pass->setVertexColourTracking(Ogre::TVC_AMBIENT); + pass->setLightingEnabled(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); +} +DebugRenderer::~DebugRenderer() +{ + mObject->getParentSceneNode()->detachObject(mObject); + mScnMgr->destroyManualObject(mObject); +} + +namespace JoltPhysics +{ +struct ShapeData { + JPH::ShapeRefC shape; +}; +Ogre::Vector3 convert(const JPH::Vec3Arg &vec) +{ + return { vec[0], vec[1], vec[2] }; +} +JPH::RVec3 convert(const Ogre::Vector3 &vec) +{ + return { vec.x, vec.y, vec.z }; +} +Ogre::Quaternion convert(const JPH::QuatArg &rot) +{ + return { rot.GetW(), rot.GetX(), rot.GetY(), rot.GetZ() }; +} +JPH::Quat convert(const Ogre::Quaternion &rot) +{ + return { rot.x, rot.y, rot.z, rot.w }; +} +void CompoundShapeBuilder::addShape(JPH::ShapeRefC shape, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation) +{ + shapeSettings.AddShape(JoltPhysics::convert(position), + JoltPhysics::convert(rotation), shape.GetPtr()); +} +JPH::ShapeRefC CompoundShapeBuilder::build() +{ + JPH::ShapeSettings::ShapeResult result = shapeSettings.Create(); + return result.Get(); +} +ContactListener::ContactListener() + : JPH::ContactListener() + , dispatch(nullptr) +{ +} +JPH::ValidateResult ContactListener::OnContactValidate( + const JPH::Body &inBody1, const JPH::Body &inBody2, + JPH::RVec3Arg inBaseOffset, + const JPH::CollideShapeResult &inCollisionResult) +{ + return JPH::ValidateResult::AcceptAllContactsForThisBodyPair; +} +void ContactListener::OnContactAdded(const JPH::Body &inBody1, + const JPH::Body &inBody2, + const JPH::ContactManifold &inManifold, + JPH::ContactSettings &ioSettings) +{ + reports.push_back({ true, inBody1.GetID(), inBody2.GetID(), inManifold, + ioSettings }); +} +void ContactListener::OnContactPersisted(const JPH::Body &inBody1, + const JPH::Body &inBody2, + const JPH::ContactManifold &inManifold, + JPH::ContactSettings &ioSettings) +{ +} +void ContactListener::OnContactRemoved(const JPH::SubShapeIDPair &inSubShapePair) +{ + reports.push_back({ false, inSubShapePair.GetBody1ID(), + inSubShapePair.GetBody2ID(), JPH::ContactManifold(), + JPH::ContactSettings() }); +} +void ContactListener::update() +{ + for (auto contact : reports) { + bool handled = false; + if (listeners.find(contact.id1) != listeners.end()) { + listeners[contact.id1](contact); + handled = true; + } + if (listeners.find(contact.id2) != listeners.end()) { + listeners[contact.id2](contact); + handled = true; + } + if (!handled && dispatch) { + dispatch(contact); + } + } + reports.clear(); +} +} + +class Physics { + // We need a temp allocator for temporary allocations during the physics update. We're + // pre-allocating 10 MB to avoid having to do allocations during the physics update. + // B.t.w. 10 MB is way too much for this example but it is a typical value you can use. + // If you don't want to pre-allocate you can also use TempAllocatorMalloc to fall back to + // malloc / free. + JPH::TempAllocatorImpl temp_allocator; + // We need a job system that will execute physics jobs on multiple threads. Typically + // you would implement the JobSystem interface yourself and let Jolt Physics run on top + // of your own job scheduler. JobSystemThreadPool is an example implementation. + JPH::JobSystemThreadPool job_system; + + // Create mapping table from object layer to broadphase layer + // Note: As this is an interface, PhysicsSystem will take a reference to this so this instance needs to stay alive! + // Also have a look at BroadPhaseLayerInterfaceTable or BroadPhaseLayerInterfaceMask for a simpler interface. + BPLayerInterfaceImpl broad_phase_layer_interface; + + // Create class that filters object vs broadphase layers + // Note: As this is an interface, PhysicsSystem will take a reference to this so this instance needs to stay alive! + // Also have a look at ObjectVsBroadPhaseLayerFilterTable or ObjectVsBroadPhaseLayerFilterMask for a simpler interface. + ObjectVsBroadPhaseLayerFilterImpl object_vs_broadphase_layer_filter; + + // Create class that filters object vs object layers + // Note: As this is an interface, PhysicsSystem will take a reference to this so this instance needs to stay alive! + // Also have a look at ObjectLayerPairFilterTable or ObjectLayerPairFilterMask for a simpler interface. + ObjectLayerPairFilterImpl object_vs_object_layer_filter; + + JPH::PhysicsSystem physics_system; + DebugRenderer *mDebugRenderer; + std::map id2node; + std::map node2id; + std::set characters; + std::set characterBodies; + bool debugDraw; + +public: + class ActivationListener : public JPH::BodyActivationListener { + public: + virtual void OnBodyActivated(const JPH::BodyID &inBodyID, + JPH::uint64 inBodyUserData) = 0; + virtual void OnBodyDeactivated(const JPH::BodyID &inBodyID, + JPH::uint64 inBodyUserData) = 0; + }; + Physics(Ogre::SceneManager *scnMgr, Ogre::SceneNode *cameraNode, + ActivationListener *activationListener = nullptr, + JPH::ContactListener *contactListener = nullptr) + : temp_allocator(10 * 1024 * 1024) + , 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{} + , debugDraw(false) + { + 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. + const uint cMaxBodies = 65536; + + // This determines how many mutexes to allocate to protect rigid bodies from concurrent access. Set it to 0 for the default settings. + const uint cNumBodyMutexes = 0; + + // This is the max amount of body pairs that can be queued at any time (the broad phase will detect overlapping + // body pairs based on their bounding boxes and will insert them into a queue for the narrowphase). If you make this buffer + // too small the queue will fill up and the broad phase jobs will start to do narrow phase work. This is slightly less efficient. + // Note: This value is low because this is a simple test. For a real project use something in the order of 65536. + const uint cMaxBodyPairs = 65536; + + // This is the maximum size of the contact constraint buffer. If more contacts (collisions between bodies) are detected than this + // number then these contacts will be ignored and bodies will start interpenetrating / fall through the world. + // Note: This value is low because this is a simple test. For a real project use something in the order of 10240. + const uint cMaxContactConstraints = 10240; + + // Now we can create the actual physics system. + physics_system.Init(cMaxBodies, cNumBodyMutexes, cMaxBodyPairs, + cMaxContactConstraints, + broad_phase_layer_interface, + object_vs_broadphase_layer_filter, + object_vs_object_layer_filter); + + // A body activation listener gets notified when bodies activate and go to sleep + // Note that this is called from a job so whatever you do here needs to be thread safe. + // Registering one is entirely optional. + if (activationListener) + physics_system.SetBodyActivationListener( + activationListener); + + // A contact listener gets notified when bodies (are about to) collide, and when they separate again. + // Note that this is called from a job so whatever you do here needs to be thread safe. + // Registering one is entirely optional. + if (contactListener) + physics_system.SetContactListener(contactListener); + + // The main way to interact with the bodies in the physics system is through the body interface. There is a locking and a non-locking + // variant of this. We're going to use the locking version (even though we're not planning to access bodies from multiple threads) + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); +#if 0 + + // Next we can create a rigid body to serve as the floor, we make a large box + // Create the settings for the collision volume (the shape). + // Note that for simple shapes (like boxes) you can also directly construct a BoxShape. + JPH::BoxShapeSettings floor_shape_settings( + JPH::Vec3(100.0f, 1.0f, 100.0f)); + floor_shape_settings + .SetEmbedded(); // A ref counted object on the stack (base class RefTarget) should be marked as such to prevent it from being freed when its reference count goes to 0. + + // Create the shape + JPH::ShapeSettings::ShapeResult floor_shape_result = + floor_shape_settings.Create(); + JPH::ShapeRefC floor_shape = + floor_shape_result + .Get(); // We don't expect an error here, but you can check floor_shape_result for HasError() / GetError() + + // Create the settings for the body itself. Note that here you can also set other properties like the restitution / friction. + JPH::Body *floor; + JPH::BodyID sphere_id; + { + using namespace JPH::literals; + JPH::BodyCreationSettings floor_settings( + floor_shape, JPH::RVec3(0.0_r, -1.0_r, 0.0_r), + JPH::Quat::sIdentity(), + JPH::EMotionType::Static, Layers::NON_MOVING); + floor = body_interface.CreateBody( + floor_settings); // Note that if we run out of bodies this can return nullptr + // Create the actual rigid body + // Add it to the world + body_interface.AddBody(floor->GetID(), + JPH::EActivation::DontActivate); + + // Now create a dynamic body to bounce on the floor + // Note that this uses the shorthand version of creating and adding a body to the world + JPH::BodyCreationSettings sphere_settings( + new JPH::SphereShape(0.5f), + JPH::RVec3(0.0_r, 2.0_r, 0.0_r), + JPH::Quat::sIdentity(), + JPH::EMotionType::Dynamic, Layers::MOVING); + sphere_id = body_interface.CreateAndAddBody( + sphere_settings, JPH::EActivation::Activate); + + // Now you can interact with the dynamic body, in this case we're going to give it a velocity. + // (note that if we had used CreateBody then we could have set the velocity straight on the body before adding it to the physics system) + body_interface.SetLinearVelocity( + sphere_id, JPH::Vec3(0.0f, -5.0f, 0.0f)); + } + + // We simulate the physics world in discrete time steps. 60 Hz is a good rate to update the physics system. + const float cDeltaTime = 1.0f / 60.0f; + + // Optional step: Before starting the physics simulation you can optimize the broad phase. This improves collision detection performance (it's pointless here because we only have 2 bodies). + // You should definitely not call this every frame or when e.g. streaming in a new level section as it is an expensive operation. + // Instead insert all new objects in batches instead of 1 at a time to keep the broad phase efficient. + physics_system.OptimizeBroadPhase(); + + // Now we're ready to simulate the body, keep simulating until it goes to sleep + uint step = 0; + while (body_interface.IsActive(sphere_id)) { + // Next step + ++step; + + // Output current position and velocity of the sphere + JPH::RVec3 position = + body_interface.GetCenterOfMassPosition( + sphere_id); + JPH::Vec3 velocity = + body_interface.GetLinearVelocity(sphere_id); + std::cout << "Step " << step << ": Position = (" + << position.GetX() << ", " << position.GetY() + << ", " << position.GetZ() + << "), Velocity = (" << velocity.GetX() + << ", " << velocity.GetY() << ", " + << velocity.GetZ() << ")" << std::endl; + + // If you take larger steps than 1 / 60th of a second you need to do multiple collision steps in order to keep the simulation stable. Do 1 collision step per 1 / 60th of a second (round up). + const int cCollisionSteps = 1; + + // Step the world + physics_system.Update(cDeltaTime, cCollisionSteps, + &temp_allocator, &job_system); + } + + // Remove the sphere from the physics system. Note that the sphere itself keeps all of its state and can be re-added at any time. + body_interface.RemoveBody(sphere_id); + + // Destroy the sphere. After this the sphere ID is no longer valid. + body_interface.DestroyBody(sphere_id); + + // Remove and destroy the floor + body_interface.RemoveBody(floor->GetID()); + body_interface.DestroyBody(floor->GetID()); +#endif + physics_system.SetGravity(JPH::Vec3(0, -0.1f, 0)); + } + ~Physics() + { + // Unregisters all types with the factory and cleans up the default material + JPH::UnregisterTypes(); + + // Destroy the factory + delete JPH::Factory::sInstance; + JPH::Factory::sInstance = nullptr; + } + float timeAccumulator = 0.0f; + float fixedDeltaTime = 1.0f / 60.0f; + void update(float dt) + { + JPH::BodyIDVector bodies; + physics_system.GetBodies(bodies); + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + for (JPH::BodyID bID : bodies) { + if (id2node.find(bID) == id2node.end()) + continue; + if (body_interface.GetMotionType(bID) == + JPH::EMotionType::Kinematic) { + Ogre::SceneNode *node = id2node[bID]; + body_interface.SetPositionAndRotationWhenChanged( + bID, + JoltPhysics::convert( + node->_getDerivedPosition()), + JoltPhysics::convert( + node->_getDerivedOrientation()), + JPH::EActivation::Activate); + } + } + for (JPH::Character *ch : characters) { + JPH::BodyID bID = ch->GetBodyID(); + if (id2node.find(bID) == id2node.end()) + continue; + Ogre::SceneNode *node = id2node[bID]; + ch->SetRotation(JoltPhysics::convert( + node->_getDerivedOrientation())); + } + int cCollisionSteps = 1; + timeAccumulator += dt; + if (debugDraw) + cCollisionSteps = 4; + while (timeAccumulator >= fixedDeltaTime) { + physics_system.Update(dt, cCollisionSteps, + &temp_allocator, &job_system); + timeAccumulator -= fixedDeltaTime; + } + for (JPH::BodyID bID : bodies) { + JPH::RVec3 p; + JPH::Quat q; + if (id2node.find(bID) == id2node.end()) + continue; + if (!body_interface.IsAdded(bID)) + continue; + if (!body_interface.IsActive(bID)) + continue; + if (body_interface.GetMotionType(bID) != + JPH::EMotionType::Dynamic) + continue; + body_interface.GetPositionAndRotation(bID, p, q); + Ogre::SceneNode *node = id2node[bID]; + node->_setDerivedPosition(JoltPhysics::convert(p)); + node->_setDerivedOrientation(JoltPhysics::convert(q)); + } + for (JPH::Character *ch : characters) { + if (body_interface.IsAdded(ch->GetBodyID())) + ch->PostSimulation(0.1f); + } + + if (debugDraw) + physics_system.DrawBodies( + JPH::BodyManager::DrawSettings(), + mDebugRenderer); + mDebugRenderer->finish(); + mDebugRenderer->NextFrame(); +#if 0 + std::cout << "bodies: " << physics_system.GetNumBodies() + << " / " + << physics_system.GetNumActiveBodies( + JPH::EBodyType::RigidBody) + << std::endl; +#endif + } + void setDebugDraw(bool enable) + { + debugDraw = enable; + } + static JPH::ShapeRefC createBoxShape(float x, float y, float z) + { + return new JPH::BoxShape(JPH::Vec3(x, y, z)); + } + static JPH::ShapeRefC createCylinderShape(float halfHeight, + float radius) + { + return new JPH::CylinderShape(halfHeight, radius); + } + JPH::BodyCreationSettings createBodyCreationSettings( + JPH::Shape *shape, JPH::RVec3 &position, JPH::Quat &rotation, + JPH::EMotionType motionType, JPH::ObjectLayer layer) + { + JPH::BodyCreationSettings body_settings( + shape, position, rotation, motionType, layer); + return body_settings; + } + JPH::BodyCreationSettings + createBodyCreationSettings(JPH::ShapeSettings *shapeSettings, + JPH::RVec3 &position, JPH::Quat &rotation, + JPH::EMotionType motionType, + JPH::ObjectLayer layer) + { + JPH::BodyCreationSettings body_settings( + shapeSettings, position, rotation, motionType, layer); + return body_settings; + } + void addBody(const JPH::BodyID &body, JPH::EActivation activation) + { + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + body_interface.AddBody(body, activation); + } + bool isAdded(const JPH::BodyID &body) + { + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + return body_interface.IsAdded(body); + } + void addAngularImpulse(const JPH::BodyID &id, + const Ogre::Vector3 &impulse) + { + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + body_interface.AddAngularImpulse( + id, JoltPhysics::convert(impulse)); + } + JPH::BodyID createBody(const JPH::BodyCreationSettings &settings, + ActivationListener *listener = nullptr) + { + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + JPH::Body *body = body_interface.CreateBody(settings); + if (!body) + return JPH::BodyID(); + return body->GetID(); + } + void removeBody(const JPH::BodyID &id) + { + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + body_interface.RemoveBody(id); + } + void destroyBody(const JPH::BodyID &id) + { + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + body_interface.DestroyBody(id); + node2id.erase(id2node[id]); + id2node.erase(id); + } + JPH::ShapeRefC createShape(const JPH::ShapeSettings &settings) + { + JPH::ShapeSettings::ShapeResult result = settings.Create(); + JPH::ShapeRefC shape = result.Get(); + return shape; + } + JPH::BodyID createBody(const JPH::Shape *shape, float mass, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + JPH::EMotionType motion, JPH::ObjectLayer layer, + ActivationListener *listener = nullptr) + { + JPH::BodyCreationSettings bodySettings( + shape, JoltPhysics::convert(position), + JoltPhysics::convert(rotation), motion, layer); + if (mass > 0.001f) { + JPH::MassProperties msp; + 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 createBody(const JPH::Shape *shape, float mass, + Ogre::SceneNode *node, JPH::EMotionType motion, + JPH::ObjectLayer layer, + ActivationListener *listener = nullptr) + { + const Ogre::Vector3 &position = node->_getDerivedPosition(); + const Ogre::Quaternion &rotation = + node->_getDerivedOrientation(); + std::cout << "body position: " << position << std::endl; + JPH::BodyID id = createBody(shape, mass, position, rotation, + motion, layer, listener); + id2node[id] = node; + node2id[node] = id; + return id; + } + JPH::BodyID createSensor(const JPH::Shape *shape, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + JPH::EMotionType motion, + JPH::ObjectLayer layer) + { + JPH::BodyCreationSettings bodySettings( + shape, JoltPhysics::convert(position), + JoltPhysics::convert(rotation), motion, layer); + bodySettings.mIsSensor = true; + return createBody(bodySettings, nullptr); + } + JPH::BodyID createSensor(const JPH::Shape *shape, Ogre::SceneNode *node, + JPH::EMotionType motion, + JPH::ObjectLayer layer) + { + const Ogre::Vector3 &position = node->_getDerivedPosition(); + const Ogre::Quaternion &rotation = + node->_getDerivedOrientation(); + std::cout << "body position: " << position << std::endl; + JPH::BodyCreationSettings bodySettings( + shape, JoltPhysics::convert(position), + JoltPhysics::convert(rotation), motion, layer); + bodySettings.mIsSensor = true; + JPH::BodyID id = createBody(bodySettings); + if (shape->GetType() == JPH::EShapeType::HeightField) { + JPH::BodyInterface &body_interface = + physics_system.GetBodyInterface(); + body_interface.SetFriction(id, 0.7f); + } + id2node[id] = node; + node2id[node] = id; + return id; + } + void addShapeToCompound(JPH::Ref compoundShape, + JPH::ShapeRefC childShape, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation) + { + JPH::MutableCompoundShape *master = + static_cast( + compoundShape.GetPtr()); + master->AddShape(JoltPhysics::convert(position), + JoltPhysics::convert(rotation), + childShape.GetPtr()); + } + class CharacterListener : public JPH::ContactListener {}; + JPH::CharacterBase *createCharacter(Ogre::SceneNode *node, + float characterHeight, + float characterRadius) + { + JPH::CharacterSettings settings; + settings.mLayer = Layers::MOVING; + float characterRadiusStanding = 0.2f; + settings.mSupportingVolume = + JPH::Plane(JPH::Vec3::sAxisY(), -0.2f); + settings.mShape = + JPH::RotatedTranslatedShapeSettings( + JPH::Vec3(0, + 0.5f * characterHeight + + characterRadius, + 0), + JPH::Quat::sIdentity(), + new JPH::CapsuleShape(0.5f * characterHeight, + characterRadius)) + .Create() + .Get(); + settings.mSupportingVolume = + JPH::Plane(JPH::Vec3::sAxisY(), -characterRadius); + JPH::Character *ch = new JPH::Character( + &settings, + JoltPhysics::convert(node->_getDerivedPosition()), + JoltPhysics::convert(node->_getDerivedOrientation()), 0, + &physics_system); + JPH::BodyID id = ch->GetBodyID(); + id2node[id] = node; + node2id[node] = id; + characterBodies.insert(id); + characters.insert(ch); + return ch; + } + JPH::ShapeRefC createBoxShape(Ogre::Vector3 extents) + { + JPH::Vec3 h(extents.x, extents.y, extents.z); + return new JPH::BoxShape(h); + } + JPH::ShapeRefC createSphereShape(float radius) + { + return new JPH::SphereShape(radius); + } + JPH::ShapeRefC createCapsuleShape(float halfHeightOfCylinder, + float radius) + { + return new JPH::CapsuleShape(halfHeightOfCylinder, radius); + } + JPH::ShapeRefC createMeshShape(Ogre::MeshPtr mesh) + { + JPH::VertexList vertices; + JPH::IndexedTriangleList triangles; + std::vector indices; + int count = mesh->getNumSubMeshes(); + int i, j; + int indexCount = 0; + int vertexCount = 0; + int sharedVertexOffset = 0; + for (i = 0; i < count; i++) { + Ogre::SubMesh *submesh = mesh->getSubMesh(i); + indexCount += submesh->indexData->indexCount; + if (submesh->useSharedVertices) + vertexCount += + mesh->sharedVertexData->vertexCount; + else + vertexCount += submesh->vertexData->vertexCount; + } + indices.reserve(indexCount); + vertices.reserve(vertexCount); + size_t currentVertexOffset = 0; + bool added_shared = false; + for (i = 0; i < count; i++) { + Ogre::SubMesh *submesh = mesh->getSubMesh(i); + Ogre::VertexData *vertex_data = + submesh->useSharedVertices ? + mesh->sharedVertexData : + submesh->vertexData; + bool add_vertices = + (submesh->useSharedVertices && !added_shared) || + !submesh->useSharedVertices; + if (add_vertices) { + if (submesh->useSharedVertices) + sharedVertexOffset = vertices.size(); + const Ogre::VertexDeclaration *decl = + vertex_data->vertexDeclaration; + const Ogre::VertexBufferBinding *bind = + vertex_data->vertexBufferBinding; + const Ogre::VertexElement *position_element = + decl->findElementBySemantic( + Ogre::VES_POSITION); + if (!position_element) + continue; + Ogre::HardwareVertexBufferSharedPtr vbuf = + bind->getBuffer( + position_element->getSource()); + unsigned char *vertex_buffer = static_cast< + unsigned char *>(vbuf->lock( + Ogre::HardwareBuffer::HBL_READ_ONLY)); + int vertexSize = vbuf->getVertexSize(); + for (j = 0; j < vertex_data->vertexCount; j++) { + float *position_data; + position_element + ->baseVertexPointerToElement( + vertex_buffer, + &position_data); + vertices.push_back( + { position_data[0], + position_data[1], + position_data[2] }); + vertex_buffer += vertexSize; + } + if (submesh->useSharedVertices) + added_shared = true; + vbuf->unlock(); + } + Ogre::HardwareIndexBufferSharedPtr ibuf = + submesh->indexData->indexBuffer; + size_t numIndices = submesh->indexData->indexCount; + size_t vertexOffset = submesh->useSharedVertices ? + sharedVertexOffset : + currentVertexOffset; + if (ibuf->getType() == + Ogre::HardwareIndexBuffer::IT_32BIT) { + unsigned int *pIndices = static_cast< + unsigned int *>(ibuf->lock( + Ogre::HardwareBuffer::HBL_READ_ONLY)); + for (j = 0; j < numIndices; j++) { + indices.push_back( + (uint32_t)pIndices[j] + + vertexOffset); + } + ibuf->unlock(); + } else { + unsigned short *pIndices = static_cast< + unsigned short *>(ibuf->lock( + Ogre::HardwareBuffer::HBL_READ_ONLY)); + for (j = 0; j < numIndices; j++) { + indices.push_back( + (uint32_t)pIndices[j] + + vertexOffset); + } + ibuf->unlock(); + } + currentVertexOffset = vertices.size(); + } + triangles.resize(indices.size() / 3); + for (j = 0; j < indices.size() / 3; j++) + triangles[j] = { indices[j * 3 + 0], indices[j * 3 + 1], + indices[j * 3 + 2] }; + JPH::MeshShapeSettings mesh_shape_settings(vertices, triangles); + JPH::ShapeSettings::ShapeResult result = + mesh_shape_settings.Create(); + OgreAssert(result.Get(), "Can not create mesh shape"); + return result.Get(); + } + JPH::ShapeRefC createMeshShape(Ogre::String meshName) + { + Ogre::DefaultHardwareBufferManager dmgr; + Ogre::MeshPtr mesh = + Ogre::MeshManager::getSingleton().getByName(meshName); + if (!mesh->isLoaded()) { + mesh->setHardwareBufferManager(&dmgr); + mesh->load(); + } + return createMeshShape(mesh); + } + JPH::ShapeRefC createConvexHullShape(Ogre::MeshPtr mesh) + { + std::vector vertices; + JPH::IndexedTriangleList triangles; + std::vector indices; + int count = mesh->getNumSubMeshes(); + int i, j; + int indexCount = 0; + int vertexCount = 0; + int sharedVertexOffset = 0; + for (i = 0; i < count; i++) { + Ogre::SubMesh *submesh = mesh->getSubMesh(i); + indexCount += submesh->indexData->indexCount; + if (submesh->useSharedVertices) + vertexCount += + mesh->sharedVertexData->vertexCount; + else + vertexCount += submesh->vertexData->vertexCount; + } + indices.reserve(indexCount); + vertices.reserve(vertexCount); + size_t currentVertexOffset = 0; + bool added_shared = false; + for (i = 0; i < count; i++) { + Ogre::SubMesh *submesh = mesh->getSubMesh(i); + Ogre::VertexData *vertex_data = + submesh->useSharedVertices ? + mesh->sharedVertexData : + submesh->vertexData; + bool add_vertices = + (submesh->useSharedVertices && !added_shared) || + !submesh->useSharedVertices; + if (add_vertices) { + if (submesh->useSharedVertices) + sharedVertexOffset = vertices.size(); + const Ogre::VertexDeclaration *decl = + vertex_data->vertexDeclaration; + const Ogre::VertexBufferBinding *bind = + vertex_data->vertexBufferBinding; + const Ogre::VertexElement *position_element = + decl->findElementBySemantic( + Ogre::VES_POSITION); + if (!position_element) + continue; + Ogre::HardwareVertexBufferSharedPtr vbuf = + bind->getBuffer( + position_element->getSource()); + unsigned char *vertex_buffer = static_cast< + unsigned char *>(vbuf->lock( + Ogre::HardwareBuffer::HBL_READ_ONLY)); + int vertexSize = vbuf->getVertexSize(); + for (j = 0; j < vertex_data->vertexCount; j++) { + float *position_data; + position_element + ->baseVertexPointerToElement( + vertex_buffer, + &position_data); + vertices.push_back( + { position_data[0], + position_data[1], + position_data[2] }); + vertex_buffer += vertexSize; + } + if (submesh->useSharedVertices) + added_shared = true; + vbuf->unlock(); + } + Ogre::HardwareIndexBufferSharedPtr ibuf = + submesh->indexData->indexBuffer; + size_t numIndices = submesh->indexData->indexCount; + size_t vertexOffset = submesh->useSharedVertices ? + sharedVertexOffset : + currentVertexOffset; + if (ibuf->getType() == + Ogre::HardwareIndexBuffer::IT_32BIT) { + unsigned int *pIndices = static_cast< + unsigned int *>(ibuf->lock( + Ogre::HardwareBuffer::HBL_READ_ONLY)); + for (j = 0; j < numIndices; j++) { + indices.push_back( + (uint32_t)pIndices[j] + + vertexOffset); + } + ibuf->unlock(); + } else { + unsigned short *pIndices = static_cast< + unsigned short *>(ibuf->lock( + Ogre::HardwareBuffer::HBL_READ_ONLY)); + for (j = 0; j < numIndices; j++) { + indices.push_back( + (uint32_t)pIndices[j] + + vertexOffset); + } + ibuf->unlock(); + } + currentVertexOffset = vertices.size(); + } + triangles.resize(indices.size() / 3); + for (j = 0; j < indices.size() / 3; j++) + triangles[j] = { indices[j * 3 + 0], indices[j * 3 + 1], + indices[j * 3 + 2] }; + JPH::ConvexHullShapeSettings mesh_shape_settings( + vertices.data(), vertices.size()); + JPH::ShapeSettings::ShapeResult result = + mesh_shape_settings.Create(); + OgreAssert(result.Get(), "Can not create mesh shape"); + return result.Get(); + } + JPH::ShapeRefC createConvexHullShape(Ogre::String meshName) + { + auto p = Ogre::DefaultHardwareBufferManager::getSingletonPtr(); + if (!p) { + new Ogre::DefaultHardwareBufferManager; + p = Ogre::DefaultHardwareBufferManager::getSingletonPtr(); + } + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load( + meshName, "General"); + if (mesh.get()) { + if (!mesh->isLoaded()) { + mesh->setHardwareBufferManager(p); + mesh->load(); + } + return createConvexHullShape(mesh); + } + OgreAssert(mesh.get(), "No file " + meshName); + return JPH::ShapeRefC(); + } + /// Create a height field shape of inSampleCount * inSampleCount vertices. + /// The height field is a surface defined by: inOffset + inScale * (x, inSamples[y * inSampleCount + x], y). + /// where x and y are integers in the range x and y e [0, inSampleCount - 1]. + /// inSampleCount: inSampleCount / mBlockSize must be minimally 2 and a power of 2 is the most efficient in terms of performance and storage. + /// inSamples: inSampleCount^2 vertices. + /// inMaterialIndices: (inSampleCount - 1)^2 indices that index into inMaterialList. + JPH::ShapeRefC createHeightfieldShape(const float *samples, + Ogre::Vector3 offset, + Ogre::Vector3 scale, + int sampleCount) + { + int i; + JPH::HeightFieldShapeSettings heightfieldSettings( + samples, JoltPhysics::convert(offset), + JoltPhysics::convert(scale), + (uint32_t)sampleCount); + for (i = 0; i < sampleCount; i++) { + memcpy(heightfieldSettings.mHeightSamples.data() + + sampleCount * i, + samples + sampleCount * (sampleCount - i - 1), + sizeof(float) * sampleCount); + } + JPH::ShapeSettings::ShapeResult result = + heightfieldSettings.Create(); + OgreAssert(result.Get(), "Can not create heightfield shape"); + return result.Get(); + } + JPH::ShapeRefC createMutableCompoundShape( + const std::vector &shapes, + const std::vector &positions, + const std::vector &rotations) + { + int i; + OgreAssert(shapes.size() == positions.size() && + shapes.size() == rotations.size(), + "bad parameters"); + JPH::MutableCompoundShapeSettings settings; + for (i = 0; i < shapes.size(); i++) + settings.AddShape( + JoltPhysics::convert(positions[i]), + JoltPhysics::convert(rotations[i]), + shapes[i].GetPtr()); + JPH::ShapeSettings::ShapeResult result = settings.Create(); + OgreAssert(result.Get(), "Can not create compound shape"); + return result.Get(); + } + JPH::ShapeRefC createStaticCompoundShape( + const std::vector &shapes, + const std::vector &positions, + const std::vector &rotations) + { + int i; + OgreAssert(shapes.size() == positions.size() && + shapes.size() == rotations.size(), + "bad parameters"); + JPH::StaticCompoundShapeSettings settings; + for (i = 0; i < shapes.size(); i++) + settings.AddShape( + JoltPhysics::convert(positions[i]), + JoltPhysics::convert(rotations[i]), + shapes[i].GetPtr()); + JPH::ShapeSettings::ShapeResult result = settings.Create(); + OgreAssert(result.Get(), "Can not create compound shape"); + return result.Get(); + } + JPH::ShapeRefC + createOffsetCenterOfMassShape(const Ogre::Vector3 &offset, + JPH::ShapeRefC shape) + { + JPH::OffsetCenterOfMassShapeSettings settings( + JoltPhysics::convert(offset), + shape.GetPtr()); + JPH::ShapeSettings::ShapeResult result = settings.Create(); + OgreAssert(result.Get(), "Can not create com offset shape"); + return result.Get(); + } + JPH::ShapeRefC + createRotatedTranslatedShape(const Ogre::Vector3 &offset, + const Ogre::Quaternion rotation, + JPH::ShapeRefC shape) + { + return JPH::RotatedTranslatedShapeSettings( + JoltPhysics::convert(offset), + JoltPhysics::convert(rotation), shape) + .Create() + .Get(); + } + void applyBuoyancyImpulse(JPH::BodyID id, + const Ogre::Vector3 &surfacePosition, + const Ogre::Vector3 &surfaceNormal, + float buoyancy, float linearDrag, + float angularDrag, + const Ogre::Vector3 &fluidVelocity, + const Ogre::Vector3 &gravity, float dt) + { + JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(), + id); + JPH::Body &body = lock.GetBody(); + body.ApplyBuoyancyImpulse( + JoltPhysics::convert(surfacePosition), + JoltPhysics::convert(surfaceNormal), + buoyancy, linearDrag, angularDrag, + JoltPhysics::convert(fluidVelocity), + JoltPhysics::convert(gravity), dt); + } + void applyBuoyancyImpulse(JPH::BodyID id, + const Ogre::Vector3 &surfacePosition, + const Ogre::Vector3 &surfaceNormal, + float buoyancy, float linearDrag, + float angularDrag, + const Ogre::Vector3 &fluidVelocity, float dt) + { + JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(), + id); + JPH::Body &body = lock.GetBody(); + body.ApplyBuoyancyImpulse( + JoltPhysics::convert(surfacePosition), + JoltPhysics::convert(surfaceNormal), + buoyancy, linearDrag, angularDrag, + JoltPhysics::convert(fluidVelocity), + physics_system.GetGravity(), dt); + } + bool isActive(JPH::BodyID id) + { + return physics_system.GetBodyInterface().IsActive(id); + } + void activate(JPH::BodyID id) + { + return physics_system.GetBodyInterface().ActivateBody(id); + } + Ogre::Vector3 getPosition(JPH::BodyID id) + { + return JoltPhysics::convert( + physics_system.GetBodyInterface().GetPosition(id)); + } + Ogre::Quaternion getRotation(JPH::BodyID id) + { + return JoltPhysics::convert( + physics_system.GetBodyInterface().GetRotation(id)); + } + void getPositionAndRotation(JPH::BodyID id, Ogre::Vector3 &position, + Ogre::Quaternion &rotation) + { + JPH::RVec3 _position; + JPH::Quat _rotation; + physics_system.GetBodyInterface().GetPositionAndRotation( + id, _position, _rotation); + position = JoltPhysics::convert(_position); + rotation = JoltPhysics::convert(_rotation); + } + void setPosition(JPH::BodyID id, const Ogre::Vector3 &position, + bool activate = true) + { + physics_system.GetBodyInterface().SetPosition( + id, JoltPhysics::convert(position), + activate ? JPH::EActivation::Activate : + JPH::EActivation::DontActivate); + } + void setRotation(JPH::BodyID id, const Ogre::Quaternion &rotation, + bool activate = true) + { + physics_system.GetBodyInterface().SetRotation( + id, JoltPhysics::convert(rotation), + activate ? JPH::EActivation::Activate : + JPH::EActivation::DontActivate); + } + void setPositionAndRotation(JPH::BodyID id, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + bool activate = true) + { + physics_system.GetBodyInterface().SetPositionAndRotation( + id, JoltPhysics::convert(position), + JoltPhysics::convert(rotation), + activate ? JPH::EActivation::Activate : + JPH::EActivation::DontActivate); + } + Ogre::Vector3 getLinearVelocity(JPH::BodyID id) + { + return JoltPhysics::convert( + physics_system.GetBodyInterface().GetLinearVelocity( + id)); + } + Ogre::Vector3 getAngularVelocity(JPH::BodyID id) + { + return JoltPhysics::convert( + physics_system.GetBodyInterface().GetAngularVelocity( + id)); + } + float getFriction(JPH::BodyID id) + { + return physics_system.GetBodyInterface().GetFriction(id); + } + void setFriction(JPH::BodyID id, float friction) + { + return physics_system.GetBodyInterface().SetFriction(id, + friction); + } + void broadphaseQuery(float dt, const Ogre::Vector3 &position, + std::set &inWater) + { + JPH::RVec3 surface_point = JoltPhysics::convert( + position + Ogre::Vector3(0, -0.1f, 0)); + + MyCollector collector(&physics_system, surface_point, + 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)); + water_box.Translate(JPH::Vec3(surface_point)); + physics_system.GetBroadPhaseQuery().CollideAABox( + water_box, collector, + JPH::SpecifiedBroadPhaseLayerFilter( + BroadPhaseLayers::MOVING), + JPH::SpecifiedObjectLayerFilter(Layers::MOVING)); + inWater.clear(); + for (JPH::BodyID inBodyID : collector.mInWater) { + inWater.insert(inBodyID); +#if 0 + std::cout << "addHit: " + << JoltPhysics::convert( + body.GetPosition()) + << std::endl; +#endif + } + } + bool raycastQuery(Ogre::Vector3 startPoint, Ogre::Vector3 endPoint, + Ogre::Vector3 &position, JPH::BodyID &id) + { + int i; + Ogre::Vector3 direction = endPoint - startPoint; + JPH::RRayCast ray{ JoltPhysics::convert(startPoint), + JoltPhysics::convert(direction) }; + JPH::RayCastResult hit; + bool hadHit = physics_system.GetNarrowPhaseQuery().CastRay( + ray, hit, {}, + JPH::SpecifiedObjectLayerFilter(Layers::NON_MOVING)); + if (hadHit) { + position = JoltPhysics::convert( + ray.GetPointOnRay(hit.mFraction)); + id = hit.mBodyID; + } + return hadHit; + } + bool bodyIsCharacter(JPH::BodyID id) const + { + return characterBodies.find(id) != characterBodies.end(); + } + void destroyCharacter(std::shared_ptr ch) + { + characterBodies.erase(characterBodies.find(ch->GetBodyID())); + characters.erase(ch.get()); + Ogre::SceneNode *node = id2node[ch->GetBodyID()]; + id2node.erase(ch->GetBodyID()); + node2id.erase(node); + ch = nullptr; + } +}; + +void physics() +{ + // Physics physics; + // physics.update(1.0f / 60.0f); +} + +static std::unique_ptr phys = nullptr; +JoltPhysicsWrapper::JoltPhysicsWrapper(Ogre::SceneManager *scnMgr, + Ogre::SceneNode *cameraNode) + : Ogre::Singleton() +{ + // Register allocation hook. In this example we'll just let Jolt use malloc / free but you can override these if you want (see Memory.h). + // This needs to be done before any other Jolt function is called. + JPH::RegisterDefaultAllocator(); + + // 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. + 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. + JPH::RegisterTypes(); + + phys = std::make_unique(scnMgr, cameraNode, nullptr, + &contacts); +} + +JoltPhysicsWrapper::~JoltPhysicsWrapper() +{ +} + +void JoltPhysicsWrapper::update(float dt) +{ + phys->update(dt); + contacts.update(); +} + +void JoltPhysicsWrapper::addBody(const JPH::BodyID &body, + JPH::EActivation activation) +{ + phys->addBody(body, activation); +} + +bool JoltPhysicsWrapper::isAdded(const JPH::BodyID &body) +{ + return phys->isAdded(body); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createBoxShape(const Ogre::Vector3 &extents) +{ + return phys->createBoxShape(extents); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createSphereShape(float radius) +{ + return phys->createSphereShape(radius); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createCapsuleShape(float halfHeight, + float radius) +{ + return phys->createCapsuleShape(halfHeight, radius); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createCylinderShape(float halfHeight, + float radius) +{ + return phys->createCylinderShape(halfHeight, radius); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createMeshShape(Ogre::MeshPtr mesh) +{ + return phys->createMeshShape(mesh); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createMeshShape(Ogre::String meshName) +{ + return phys->createMeshShape(meshName); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createConvexHullShape(Ogre::MeshPtr mesh) +{ + return phys->createConvexHullShape(mesh); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createConvexHullShape(Ogre::String meshName) +{ + return phys->createConvexHullShape(meshName); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createHeightfieldShape(const float *samples, + Ogre::Vector3 offset, + Ogre::Vector3 scale, + int sampleCount) +{ + return phys->createHeightfieldShape(samples, offset, scale, + sampleCount); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createMutableCompoundShape( + const std::vector &shapes, + const std::vector &positions, + const std::vector &rotations) +{ + return phys->createMutableCompoundShape(shapes, positions, rotations); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createStaticCompoundShape( + const std::vector &shapes, + const std::vector &positions, + const std::vector &rotations) +{ + return phys->createStaticCompoundShape(shapes, positions, rotations); +} + +JPH::ShapeRefC +JoltPhysicsWrapper::createOffsetCenterOfMassShape(const Ogre::Vector3 &offset, + JPH::ShapeRefC shape) +{ + return phys->createOffsetCenterOfMassShape(offset, shape); +} + +JPH::ShapeRefC JoltPhysicsWrapper::createRotatedTranslatedShape( + const Ogre::Vector3 &offset, const Ogre::Quaternion rotation, + JPH::ShapeRefC shape) +{ + return phys->createRotatedTranslatedShape(offset, rotation, shape); +} + +JPH::BodyID +JoltPhysicsWrapper::createBody(const JPH::BodyCreationSettings &settings) +{ + return phys->createBody(settings); +} +JPH::BodyID JoltPhysicsWrapper::createBody(const JPH::Shape *shape, float mass, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + JPH::EMotionType motion, + JPH::ObjectLayer layer) +{ + return phys->createBody(shape, mass, position, rotation, motion, layer); +} +JPH::BodyID JoltPhysicsWrapper::createBody(const JPH::Shape *shape, float mass, + Ogre::SceneNode *node, + JPH::EMotionType motion, + JPH::ObjectLayer layer) +{ + return phys->createBody(shape, mass, node, motion, layer); +} +JPH::BodyID JoltPhysicsWrapper::createSensor(const JPH::Shape *shape, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + JPH::EMotionType motion, + JPH::ObjectLayer layer) +{ + return phys->createSensor(shape, position, rotation, motion, layer); +} +JPH::BodyID JoltPhysicsWrapper::createSensor(const JPH::Shape *shape, + Ogre::SceneNode *node, + JPH::EMotionType motion, + JPH::ObjectLayer layer) +{ + return phys->createSensor(shape, node, motion, layer); +} +JPH::CharacterBase *JoltPhysicsWrapper::createCharacter(Ogre::SceneNode *node, + float characterHeight, + float characterRadius) +{ + return phys->createCharacter(node, characterHeight, characterRadius); +} +void JoltPhysicsWrapper::addShapeToCompound(JPH::Ref compoundShape, + JPH::ShapeRefC childShape, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation) +{ + phys->addShapeToCompound(compoundShape, childShape, position, rotation); +} +void JoltPhysicsWrapper::removeBody(const JPH::BodyID &id) +{ + phys->removeBody(id); +} +void JoltPhysicsWrapper::destroyBody(const JPH::BodyID &id) +{ + phys->destroyBody(id); +} +void JoltPhysicsWrapper::setDebugDraw(bool enable) +{ + phys->setDebugDraw(enable); +} +void JoltPhysicsWrapper::broadphaseQuery(float dt, + const Ogre::Vector3 &position, + std::set &inWater) +{ + phys->broadphaseQuery(dt, position, inWater); +} +void JoltPhysicsWrapper::applyBuoyancyImpulse( + JPH::BodyID id, const Ogre::Vector3 &surfacePosition, + const Ogre::Vector3 &surfaceNormal, float buoyancy, float linearDrag, + float angularDrag, const Ogre::Vector3 &fluidVelocity, + const Ogre::Vector3 &gravity, float dt) +{ + phys->applyBuoyancyImpulse(id, surfacePosition, surfaceNormal, buoyancy, + linearDrag, angularDrag, fluidVelocity, + gravity, dt); +} +void JoltPhysicsWrapper::applyBuoyancyImpulse( + JPH::BodyID id, const Ogre::Vector3 &surfacePosition, + const Ogre::Vector3 &surfaceNormal, float buoyancy, float linearDrag, + float angularDrag, const Ogre::Vector3 &fluidVelocity, float dt) +{ + phys->applyBuoyancyImpulse(id, surfacePosition, surfaceNormal, buoyancy, + linearDrag, angularDrag, fluidVelocity, dt); +} +bool JoltPhysicsWrapper::isActive(JPH::BodyID id) +{ + return phys->isActive(id); +} +void JoltPhysicsWrapper::activate(JPH::BodyID id) +{ + phys->activate(id); +} +Ogre::Vector3 JoltPhysicsWrapper::getPosition(JPH::BodyID id) +{ + return phys->getPosition(id); +} +void JoltPhysicsWrapper::setPosition(JPH::BodyID id, + const Ogre::Vector3 &position, + bool activate) +{ + return phys->setPosition(id, position, activate); +} +Ogre::Quaternion JoltPhysicsWrapper::getRotation(JPH::BodyID id) +{ + return phys->getRotation(id); +} +void JoltPhysicsWrapper::setRotation(JPH::BodyID id, + const Ogre::Quaternion &rotation, + bool activate) +{ + phys->setRotation(id, rotation, activate); +} +void JoltPhysicsWrapper::getPositionAndRotation(JPH::BodyID id, + Ogre::Vector3 &position, + Ogre::Quaternion &rotation) +{ + phys->getPositionAndRotation(id, position, rotation); +} +void JoltPhysicsWrapper::setPositionAndRotation( + JPH::BodyID id, const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, bool activate) +{ + phys->setPositionAndRotation(id, position, rotation, activate); +} +Ogre::Vector3 JoltPhysicsWrapper::getLinearVelocity(JPH::BodyID id) +{ + return phys->getLinearVelocity(id); +} +Ogre::Vector3 JoltPhysicsWrapper::getAngularVelocity(JPH::BodyID id) +{ + return phys->getAngularVelocity(id); +} +float JoltPhysicsWrapper::getFriction(JPH::BodyID id) +{ + return phys->getFriction(id); +} +void JoltPhysicsWrapper::setFriction(JPH::BodyID id, float friction) +{ + phys->setFriction(id, friction); +} +void JoltPhysicsWrapper::addAngularImpulse(const JPH::BodyID &id, + const Ogre::Vector3 &impulse) +{ + return phys->addAngularImpulse(id, impulse); +} +void JoltPhysicsWrapper::setDispatch( + std::function< + void(const JoltPhysics::ContactListener::ContactReport &report)> + dispatcher) +{ + contacts.setDispatch(dispatcher); +} +void JoltPhysicsWrapper::addContactListener( + const JPH::BodyID &id, + std::function< + void(const JoltPhysics::ContactListener::ContactReport &report)> + listener) +{ + contacts.addListener(id, listener); +} +void JoltPhysicsWrapper::removeContactListener(const JPH::BodyID &id) +{ + contacts.removeListener(id); +} +bool JoltPhysicsWrapper::raycastQuery(Ogre::Vector3 startPoint, + Ogre::Vector3 endPoint, + Ogre::Vector3 &position, JPH::BodyID &id) +{ + return phys->raycastQuery(startPoint, endPoint, position, id); +} + +bool JoltPhysicsWrapper::bodyIsCharacter(JPH::BodyID id) const +{ + return phys->bodyIsCharacter(id); +} + +void JoltPhysicsWrapper::destroyCharacter(std::shared_ptr ch) +{ + phys->destroyCharacter(ch); +} +template <> +JoltPhysicsWrapper *Ogre::Singleton::msSingleton = 0; diff --git a/src/features/editScene/physics/physics.h b/src/features/editScene/physics/physics.h new file mode 100644 index 0000000..85880f0 --- /dev/null +++ b/src/features/editScene/physics/physics.h @@ -0,0 +1,225 @@ +#ifndef __PHYSICS_H_ +#define __PHYSICS_H_ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void physics(); +namespace JPH +{ +class CharacterBase; +class Character; +class ContactManifold; +class ContactSettings; +class SubShapeIDPair; +} +// Layer that objects can be in, determines which other objects it can collide with +// Typically you at least want to have 1 layer for moving bodies and 1 layer for static bodies, but you can have more +// layers if you want. E.g. you could have a layer for high detail collision (which is not used by the physics simulation +// but only if you do collision testing). +namespace Layers +{ +static constexpr JPH::ObjectLayer NON_MOVING = 0; +static constexpr JPH::ObjectLayer MOVING = 1; +static constexpr JPH::ObjectLayer SENSORS = 2; +static constexpr JPH::ObjectLayer NUM_LAYERS = 3; +}; + +// Each broadphase layer results in a separate bounding volume tree in the broad phase. You at least want to have +// a layer for non-moving and moving objects to avoid having to update a tree full of static objects every frame. +// You can have a 1-on-1 mapping between object layers and broadphase layers (like in this case) but if you have +// many object layers you'll be creating many broad phase trees, which is not efficient. If you want to fine tune +// your broadphase layers define JPH_TRACK_BROADPHASE_STATS and look at the stats reported on the TTY. +namespace BroadPhaseLayers +{ +static constexpr JPH::BroadPhaseLayer NON_MOVING(0); +static constexpr JPH::BroadPhaseLayer MOVING(1); +static constexpr uint NUM_LAYERS(2); +}; + +namespace JoltPhysics +{ +template +Ogre::Vector3 convert(const T &vec) +{ + return {vec[0], vec[1], vec[2]}; +} +template T convert(const Ogre::Vector3 &vec) +{ + return { vec.x, vec.y, vec.z }; +} +Ogre::Quaternion convert(const JPH::QuatArg &rot); +JPH::Quat convert(const Ogre::Quaternion &rot); +struct ShapeData; +struct CompoundShapeBuilder { + JPH::StaticCompoundShapeSettings shapeSettings; + void addShape(JPH::ShapeRefC shape, const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation); + JPH::ShapeRefC build(); +}; +class ContactListener : public JPH::ContactListener { +public: + struct ContactReport { + bool entered; + JPH::BodyID id1, id2; + JPH::ContactManifold manifold; + JPH::ContactSettings settings; + }; + +private: + std::list reports; + std::function dispatch; + std::map > + listeners; + +public: + ContactListener(); + JPH::ValidateResult OnContactValidate( + const JPH::Body &inBody1, const JPH::Body &inBody2, + JPH::RVec3Arg inBaseOffset, + const JPH::CollideShapeResult &inCollisionResult) override; + void OnContactAdded(const JPH::Body &inBody1, const JPH::Body &inBody2, + const JPH::ContactManifold &inManifold, + JPH::ContactSettings &ioSettings) override; + void OnContactPersisted(const JPH::Body &inBody1, + const JPH::Body &inBody2, + const JPH::ContactManifold &inManifold, + JPH::ContactSettings &ioSettings) override; + void + OnContactRemoved(const JPH::SubShapeIDPair &inSubShapePair) override; + void setDispatch(const std::function + dispatcher) + { + dispatch = dispatcher; + } + void addListener( + const JPH::BodyID &id, + const std::function listener) + { + listeners[id] = listener; + } + void removeListener(const JPH::BodyID &id) + { + listeners.erase(id); + } + void update(); +}; +} + +class JoltPhysicsWrapper : public Ogre::Singleton { +public: + JoltPhysics::ContactListener contacts; + JoltPhysicsWrapper(Ogre::SceneManager *scnMgr, + Ogre::SceneNode *cameraNode); + ~JoltPhysicsWrapper(); + void update(float dt); + void addBody(const JPH::BodyID &body, JPH::EActivation activation); + bool isAdded(const JPH::BodyID &body); + JPH::ShapeRefC createBoxShape(const Ogre::Vector3 &extents); + JPH::ShapeRefC createSphereShape(float radius); + JPH::ShapeRefC createCapsuleShape(float halfHeight, float radius); + JPH::ShapeRefC createCylinderShape(float halfHeight, float radius); + JPH::ShapeRefC createMeshShape(Ogre::MeshPtr mesh); + JPH::ShapeRefC createMeshShape(Ogre::String meshName); + JPH::ShapeRefC createConvexHullShape(Ogre::MeshPtr mesh); + JPH::ShapeRefC createConvexHullShape(Ogre::String meshName); + JPH::ShapeRefC createHeightfieldShape(const float *samples, + Ogre::Vector3 offset, + Ogre::Vector3 scale, + int sampleCount); + JPH::ShapeRefC createMutableCompoundShape( + const std::vector &shapes, + const std::vector &positions, + const std::vector &rotations); + JPH::ShapeRefC createStaticCompoundShape( + const std::vector &shapes, + const std::vector &positions, + const std::vector &rotations); + 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::BodyID createBody(const JPH::Shape *shape, float mass, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + JPH::EMotionType motion, JPH::ObjectLayer layer); + JPH::BodyID createBody(const JPH::Shape *shape, float mass, + Ogre::SceneNode *node, JPH::EMotionType motion, + JPH::ObjectLayer layer); + JPH::BodyID createSensor(const JPH::Shape *shape, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + JPH::EMotionType motion, + JPH::ObjectLayer layer); + JPH::BodyID createSensor(const JPH::Shape *shape, Ogre::SceneNode *node, + JPH::EMotionType motion, + JPH::ObjectLayer layer); + JPH::CharacterBase *createCharacter(Ogre::SceneNode *node, + float characterHeight, + float characterRadius); + void addShapeToCompound(JPH::Ref compoundShape, + JPH::ShapeRefC childShape, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation); + void removeBody(const JPH::BodyID &id); + void destroyBody(const JPH::BodyID &id); + void setDebugDraw(bool enable); + void broadphaseQuery(float dt, const Ogre::Vector3 &position, + std::set &inWater); + void applyBuoyancyImpulse(JPH::BodyID id, + const Ogre::Vector3 &surfacePosition, + const Ogre::Vector3 &surfaceNormal, + float buoyancy, float linearDrag, + float angularDrag, + const Ogre::Vector3 &fluidVelocity, + const Ogre::Vector3 &gravity, float dt); + void applyBuoyancyImpulse(JPH::BodyID id, + const Ogre::Vector3 &surfacePosition, + const Ogre::Vector3 &surfaceNormal, + float buoyancy, float linearDrag, + float angularDrag, + const Ogre::Vector3 &fluidVelocity, float dt); + bool isActive(JPH::BodyID id); + void activate(JPH::BodyID id); + Ogre::Vector3 getPosition(JPH::BodyID id); + void setPosition(JPH::BodyID id, const Ogre::Vector3 &position, + bool activate = true); + Ogre::Quaternion getRotation(JPH::BodyID id); + void setRotation(JPH::BodyID id, const Ogre::Quaternion &rotation, + bool activate = true); + void getPositionAndRotation(JPH::BodyID id, Ogre::Vector3 &position, + Ogre::Quaternion &rotation); + void setPositionAndRotation(JPH::BodyID id, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, + bool activate = true); + Ogre::Vector3 getLinearVelocity(JPH::BodyID id); + Ogre::Vector3 getAngularVelocity(JPH::BodyID id); + float getFriction(JPH::BodyID id); + void setFriction(JPH::BodyID id, float friction); + void addAngularImpulse(const JPH::BodyID &id, + const Ogre::Vector3 &impulse); + void setDispatch(std::function + dispatcher); + void addContactListener( + const JPH::BodyID &id, + const std::function + listener); + void removeContactListener(const JPH::BodyID &id); + bool raycastQuery(Ogre::Vector3 startPoint, Ogre::Vector3 endPoint, + Ogre::Vector3 &position, JPH::BodyID &id); + bool bodyIsCharacter(JPH::BodyID id) const; + void destroyCharacter(std::shared_ptr ch); +}; +#endif diff --git a/src/features/editScene/resources/materials/scripts/editor.material b/src/features/editScene/resources/materials/scripts/editor.material new file mode 100644 index 0000000..e6de0a8 --- /dev/null +++ b/src/features/editScene/resources/materials/scripts/editor.material @@ -0,0 +1,65 @@ +// Simple editor materials for GLES2 compatibility +material Editor/Red +{ + technique + { + pass + { + lighting off + ambient 1 0 0 + diffuse 1 0 0 + } + } +} + +material Editor/Green +{ + technique + { + pass + { + lighting off + ambient 0 1 0 + diffuse 0 1 0 + } + } +} + +material Editor/Blue +{ + technique + { + pass + { + lighting off + ambient 0 0 1 + diffuse 0 0 1 + } + } +} + +material Editor/White +{ + technique + { + pass + { + lighting off + ambient 1 1 1 + diffuse 1 1 1 + } + } +} + +material Editor/Grey +{ + technique + { + pass + { + lighting off + ambient 0.5 0.5 0.5 + diffuse 0.5 0.5 0.5 + } + } +} diff --git a/src/features/editScene/resources/materials/scripts/test.material b/src/features/editScene/resources/materials/scripts/test.material new file mode 100644 index 0000000..6254c17 --- /dev/null +++ b/src/features/editScene/resources/materials/scripts/test.material @@ -0,0 +1,52 @@ +// Simple test material for GL3Plus +material Test/Red +{ + technique + { + pass + { + lighting off + ambient 1 0 0 + diffuse 1 0 0 + } + } +} + +material Test/Green +{ + technique + { + pass + { + lighting off + ambient 0 1 0 + diffuse 0 1 0 + } + } +} + +material Test/Blue +{ + technique + { + pass + { + lighting off + ambient 0 0 1 + diffuse 0 0 1 + } + } +} + +material Test/White +{ + technique + { + pass + { + lighting off + ambient 1 1 1 + diffuse 1 1 1 + } + } +} diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index c76f2d9..f6a48f5 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -3,8 +3,13 @@ #include "../components/Transform.hpp" #include "../components/Renderable.hpp" #include "../components/EditorMarker.hpp" +#include "../components/PhysicsCollider.hpp" +#include "../components/RigidBody.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" +#include "../ui/PhysicsColliderEditor.hpp" +#include "../ui/RigidBodyEditor.hpp" +#include "PhysicsSystem.hpp" #include #include #include @@ -104,6 +109,40 @@ void EditorUISystem::registerComponentEditors() e.remove(); } }); + + // Register RigidBody component + auto rigidBodyEditor = std::make_unique(); + m_componentRegistry.registerComponent( + "Rigid Body", std::move(rigidBodyEditor), + // Adder + [this](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [this](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); + + // Register PhysicsCollider component + auto colliderEditor = std::make_unique(m_sceneMgr); + m_componentRegistry.registerComponent( + "Physics Collider", std::move(colliderEditor), + // Adder + [this](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [this](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); } void EditorUISystem::update() @@ -199,6 +238,18 @@ void EditorUISystem::renderHierarchyWindow() 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."); } + + // Physics debug draw toggle + if (m_physicsSystem) { + 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::EndMenu(); } ImGui::EndMenuBar(); @@ -290,6 +341,10 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth) indicators += " [T]"; if (entity.has()) indicators += " [R]"; + if (entity.has()) + indicators += " [RB]"; + if (entity.has()) + indicators += " [C]"; snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(), indicators.c_str(), (unsigned long long)entity.id()); @@ -410,9 +465,23 @@ void EditorUISystem::renderComponentList(flecs::entity entity) renderable); } + if (entity.has()) { + auto &rigidBody = entity.get_mut(); + m_componentRegistry.render(entity, + rigidBody); + } + + if (entity.has()) { + auto &collider = entity.get_mut(); + m_componentRegistry.render(entity, + collider); + } + // Show message if no components if (!entity.has() && - !entity.has()) { + !entity.has() && + !entity.has() && + !entity.has()) { ImGui::TextDisabled("No components"); ImGui::Text("Click 'Add Component' to add components"); } @@ -428,6 +497,8 @@ void EditorUISystem::renderAddComponentMenu(flecs::entity entity) bool hasTransform = entity.has(); bool hasRenderable = entity.has(); + bool hasRigidBody = entity.has(); + bool hasCollider = entity.has(); if (!hasTransform) { if (ImGui::MenuItem("Transform")) { @@ -445,8 +516,21 @@ void EditorUISystem::renderAddComponentMenu(flecs::entity entity) } } - if (hasTransform && hasRenderable) { - ImGui::TextDisabled("All components added"); + ImGui::Separator(); + ImGui::Text("Physics:"); + + if (!hasRigidBody) { + if (ImGui::MenuItem("Rigid Body")) { + m_componentRegistry + .addComponent(entity); + } + } + + if (!hasCollider) { + if (ImGui::MenuItem("Physics Collider")) { + m_componentRegistry + .addComponent(entity); + } } ImGui::EndPopup(); @@ -478,6 +562,22 @@ void EditorUISystem::renderRemoveComponentMenu(flecs::entity entity) } } + if (entity.has()) { + hasAny = true; + if (ImGui::MenuItem("Rigid Body")) { + m_componentRegistry.removeComponent< + RigidBodyComponent>(entity); + } + } + + if (entity.has()) { + hasAny = true; + if (ImGui::MenuItem("Physics Collider")) { + m_componentRegistry.removeComponent< + PhysicsColliderComponent>(entity); + } + } + if (!hasAny) { ImGui::TextDisabled("No components to remove"); } diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index 84f5990..9fba0e1 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -11,6 +11,9 @@ #include "../gizmo/Gizmo.hpp" #include "SceneSerializer.hpp" +// Forward declaration +class EditorPhysicsSystem; + /** * Main UI system for the scene editor * Handles rendering of: @@ -66,6 +69,11 @@ public: 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; } + /** * Save scene to file */ @@ -124,6 +132,9 @@ private: // Settings bool m_parentSceneNodes = true; // Whether child entities inherit parent's SceneNode + // Physics system reference (for debug toggle) + EditorPhysicsSystem* m_physicsSystem = nullptr; + // File dialog state bool m_showFileDialog = false; bool m_fileDialogIsSave = false; // true = save, false = load diff --git a/src/features/editScene/systems/PhysicsSystem.cpp b/src/features/editScene/systems/PhysicsSystem.cpp new file mode 100644 index 0000000..343e88e --- /dev/null +++ b/src/features/editScene/systems/PhysicsSystem.cpp @@ -0,0 +1,243 @@ +#include "PhysicsSystem.hpp" +#include "../components/Transform.hpp" +#include + +EditorPhysicsSystem::EditorPhysicsSystem(flecs::world& world, + Ogre::SceneManager* sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_rigidBodyQuery(world.query()) +{ +} + +EditorPhysicsSystem::~EditorPhysicsSystem() = default; + +void EditorPhysicsSystem::initialize() +{ + if (m_initialized) return; + + // Create physics wrapper + Ogre::SceneNode* cameraNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode("PhysicsCameraNode"); + m_physics = std::make_unique(m_sceneMgr, cameraNode); + + m_initialized = true; + Ogre::LogManager::getSingleton().logMessage("Physics system initialized"); +} + +void EditorPhysicsSystem::update(float deltaTime) +{ + if (!m_initialized || !m_physics) return; + + // Sync bodies before simulation + syncBodies(); + + // Step physics + m_physics->update(deltaTime); +} + +void EditorPhysicsSystem::syncBodies() +{ + if (!m_initialized || !m_physics) return; + + // Process all rigid bodies + m_rigidBodyQuery.each([&](flecs::entity entity, + RigidBodyComponent& rigidBody, + TransformComponent& transform) { + if (rigidBody.bodyDirty || !rigidBody.bodyCreated) { + updateRigidBody(entity, rigidBody, transform); + } + }); +} + +JPH::ShapeRefC EditorPhysicsSystem::createShape(PhysicsColliderComponent& collider) +{ + if (!collider.shapeDirty && collider.shape) { + return collider.shape; + } + + JPH::ShapeRefC result; + + switch (collider.shapeType) { + case PhysicsColliderComponent::ShapeType::Box: { + result = m_physics->createBoxShape(collider.parameters); + break; + } + case PhysicsColliderComponent::ShapeType::Sphere: { + result = m_physics->createSphereShape(collider.radius); + break; + } + case PhysicsColliderComponent::ShapeType::Capsule: { + result = m_physics->createCapsuleShape(collider.halfHeight, + collider.radius); + break; + } + case PhysicsColliderComponent::ShapeType::Cylinder: { + result = m_physics->createCylinderShape(collider.halfHeight, + collider.radius); + break; + } + case PhysicsColliderComponent::ShapeType::Mesh: { + if (!collider.meshName.empty()) { + try { + // Ensure mesh is loaded first + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load( + collider.meshName, "General"); + if (mesh && mesh->isLoaded()) { + result = m_physics->createMeshShape(collider.meshName); + } + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "Failed to create mesh shape: " + collider.meshName + + " - " + e.getDescription()); + } + } + break; + } + case PhysicsColliderComponent::ShapeType::ConvexHull: { + if (!collider.meshName.empty()) { + try { + result = m_physics->createConvexHullShape(collider.meshName); + } catch (...) { + Ogre::LogManager::getSingleton().logMessage( + "Failed to create convex hull shape: " + + collider.meshName); + } + } + break; + } + } + + // If no shape created, default to a small box + if (!result) { + result = m_physics->createBoxShape(Ogre::Vector3(0.1f, 0.1f, 0.1f)); + } + + // Apply offset if any + if (collider.offset != Ogre::Vector3::ZERO || + collider.rotationOffset != Ogre::Quaternion::IDENTITY) { + result = m_physics->createRotatedTranslatedShape( + collider.offset, collider.rotationOffset, result); + } + + collider.shape = result; + collider.shapeDirty = false; + return result; +} + +JPH::ShapeRefC EditorPhysicsSystem::buildCompoundShape(flecs::entity rigidBodyEntity) +{ + std::vector shapes; + std::vector positions; + std::vector rotations; + + // Collect all collider children + rigidBodyEntity.children([&](flecs::entity child) { + if (child.has() && + child.has()) { + auto& collider = child.get_mut(); + auto& transform = child.get(); + + JPH::ShapeRefC shape = createShape(collider); + if (shape) { + shapes.push_back(shape); + positions.push_back(transform.position); + rotations.push_back(transform.rotation); + } + } + }); + + if (shapes.empty()) { + // No colliders, use default box + return m_physics->createBoxShape(Ogre::Vector3(0.5f, 0.5f, 0.5f)); + } + + if (shapes.size() == 1) { + // Single shape - use rotated translated shape + return m_physics->createRotatedTranslatedShape(positions[0], + rotations[0], + shapes[0]); + } + + // Multiple shapes - create static compound + return m_physics->createStaticCompoundShape(shapes, positions, rotations); +} + +void EditorPhysicsSystem::updateRigidBody(flecs::entity entity, + RigidBodyComponent& rigidBody, + TransformComponent& transform) +{ + // Remove existing body if any + if (rigidBody.bodyCreated) { + removeRigidBody(rigidBody); + } + + // Build collision shape + JPH::ShapeRefC shape = buildCompoundShape(entity); + if (!shape) return; + + // Determine motion type and layer + JPH::EMotionType motionType; + JPH::ObjectLayer layer; + + switch (rigidBody.bodyType) { + case RigidBodyComponent::BodyType::Static: + motionType = JPH::EMotionType::Static; + layer = Layers::NON_MOVING; + break; + case RigidBodyComponent::BodyType::Kinematic: + motionType = JPH::EMotionType::Kinematic; + layer = Layers::MOVING; + break; + case RigidBodyComponent::BodyType::Dynamic: + default: + motionType = JPH::EMotionType::Dynamic; + layer = Layers::MOVING; + break; + } + + // Create body + if (rigidBody.isSensor) { + rigidBody.bodyID = m_physics->createSensor(shape, transform.node, + motionType, layer); + } else { + rigidBody.bodyID = m_physics->createBody(shape, rigidBody.mass, + transform.node, + motionType, layer); + } + + if (rigidBody.bodyID.IsInvalid()) { + Ogre::LogManager::getSingleton().logMessage( + "Failed to create rigid body"); + return; + } + + // Set properties + m_physics->setFriction(rigidBody.bodyID, rigidBody.friction); + + // Add to physics world + m_physics->addBody(rigidBody.bodyID, JPH::EActivation::Activate); + + rigidBody.bodyCreated = true; + rigidBody.bodyDirty = false; + + Ogre::LogManager::getSingleton().logMessage("Rigid body created"); +} + +void EditorPhysicsSystem::removeRigidBody(RigidBodyComponent& rigidBody) +{ + if (!rigidBody.bodyCreated || rigidBody.bodyID.IsInvalid()) return; + + m_physics->removeBody(rigidBody.bodyID); + m_physics->destroyBody(rigidBody.bodyID); + + rigidBody.bodyCreated = false; + rigidBody.bodyID = JPH::BodyID(); +} + +void EditorPhysicsSystem::setDebugDraw(bool enable) +{ + m_debugDraw = enable; + if (m_physics) { + m_physics->setDebugDraw(enable); + } +} diff --git a/src/features/editScene/systems/PhysicsSystem.hpp b/src/features/editScene/systems/PhysicsSystem.hpp new file mode 100644 index 0000000..4b659c3 --- /dev/null +++ b/src/features/editScene/systems/PhysicsSystem.hpp @@ -0,0 +1,61 @@ +#ifndef EDITSCENE_PHYSICSSYSTEM_HPP +#define EDITSCENE_PHYSICSSYSTEM_HPP +#pragma once + +#include +#include +#include +#include "../physics/physics.h" +#include "../components/RigidBody.hpp" +#include "../components/PhysicsCollider.hpp" + +/** + * Physics system for the editor + * Manages Jolt physics world and creates/updates bodies from components + */ +class EditorPhysicsSystem { +public: + EditorPhysicsSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + ~EditorPhysicsSystem(); + + // Initialize physics world + void initialize(); + + // Update physics simulation + void update(float deltaTime); + + // Process component changes and create/update bodies + void syncBodies(); + + // Enable/disable debug drawing + void setDebugDraw(bool enable); + bool isDebugDrawEnabled() const { return m_debugDraw; } + + bool isInitialized() const { return m_initialized; } + +private: + // Create a shape from collider component + 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); + + // Remove a rigid body + void removeRigidBody(RigidBodyComponent& rigidBody); + + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + + std::unique_ptr m_physics; + bool m_initialized = false; + bool m_debugDraw = false; + + // Query for entities with RigidBody + flecs::query m_rigidBodyQuery; +}; + +#endif // EDITSCENE_PHYSICSSYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 46ab8a7..6352431 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -3,6 +3,8 @@ #include "../components/Renderable.hpp" #include "../components/EntityName.hpp" #include "../components/EditorMarker.hpp" +#include "../components/RigidBody.hpp" +#include "../components/PhysicsCollider.hpp" #include "EditorUISystem.hpp" #include #include @@ -113,6 +115,14 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["renderable"] = serializeRenderable(entity); } + if (entity.has()) { + json["rigidBody"] = serializeRigidBody(entity); + } + + if (entity.has()) { + json["collider"] = serializeCollider(entity); + } + // Serialize children json["children"] = nlohmann::json::array(); entity.children([&](flecs::entity child) { @@ -156,6 +166,14 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit deserializeRenderable(entity, json["renderable"]); } + if (json.contains("rigidBody")) { + deserializeRigidBody(entity, json["rigidBody"]); + } + + if (json.contains("collider")) { + deserializeCollider(entity, json["collider"]); + } + // Add to UI system if provided if (uiSystem) { uiSystem->addEntity(entity); @@ -213,6 +231,88 @@ nlohmann::json SceneSerializer::serializeEntityName(flecs::entity entity) return json; } +nlohmann::json SceneSerializer::serializeRigidBody(flecs::entity entity) +{ + auto& rb = entity.get(); + nlohmann::json json; + + // Serialize body type as string + switch (rb.bodyType) { + case RigidBodyComponent::BodyType::Static: + json["bodyType"] = "static"; + break; + case RigidBodyComponent::BodyType::Dynamic: + json["bodyType"] = "dynamic"; + break; + case RigidBodyComponent::BodyType::Kinematic: + json["bodyType"] = "kinematic"; + break; + } + + json["mass"] = rb.mass; + json["friction"] = rb.friction; + json["restitution"] = rb.restitution; + json["isSensor"] = rb.isSensor; + + return json; +} + +nlohmann::json SceneSerializer::serializeCollider(flecs::entity entity) +{ + auto& collider = entity.get(); + nlohmann::json json; + + // Serialize shape type as string + switch (collider.shapeType) { + case PhysicsColliderComponent::ShapeType::Box: + json["shapeType"] = "box"; + break; + case PhysicsColliderComponent::ShapeType::Sphere: + json["shapeType"] = "sphere"; + break; + case PhysicsColliderComponent::ShapeType::Capsule: + json["shapeType"] = "capsule"; + break; + case PhysicsColliderComponent::ShapeType::Cylinder: + json["shapeType"] = "cylinder"; + break; + case PhysicsColliderComponent::ShapeType::Mesh: + json["shapeType"] = "mesh"; + break; + case PhysicsColliderComponent::ShapeType::ConvexHull: + json["shapeType"] = "convexHull"; + break; + } + + // Serialize shape parameters + json["parameters"] = { + {"x", collider.parameters.x}, + {"y", collider.parameters.y}, + {"z", collider.parameters.z} + }; + + json["radius"] = collider.radius; + json["halfHeight"] = collider.halfHeight; + json["meshName"] = collider.meshName; + + // Serialize offset + json["offset"] = { + {"x", collider.offset.x}, + {"y", collider.offset.y}, + {"z", collider.offset.z} + }; + + // Serialize rotation offset + json["rotationOffset"] = { + {"w", collider.rotationOffset.w}, + {"x", collider.rotationOffset.x}, + {"y", collider.rotationOffset.y}, + {"z", collider.rotationOffset.z} + }; + + return json; +} + void SceneSerializer::deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity) { TransformComponent transform; @@ -299,3 +399,90 @@ void SceneSerializer::deserializeEntityName(flecs::entity entity, const nlohmann std::string name = json.value("name", "Entity"); entity.set(EntityNameComponent(name)); } + +void SceneSerializer::deserializeRigidBody(flecs::entity entity, const nlohmann::json& json) +{ + RigidBodyComponent rb; + + // Deserialize body type + std::string bodyType = json.value("bodyType", "static"); + if (bodyType == "static") { + rb.bodyType = RigidBodyComponent::BodyType::Static; + } else if (bodyType == "dynamic") { + rb.bodyType = RigidBodyComponent::BodyType::Dynamic; + } else if (bodyType == "kinematic") { + rb.bodyType = RigidBodyComponent::BodyType::Kinematic; + } + + rb.mass = json.value("mass", 1.0f); + rb.friction = json.value("friction", 0.5f); + rb.restitution = json.value("restitution", 0.0f); + rb.isSensor = json.value("isSensor", false); + + // Mark as dirty so physics system will create the body + rb.bodyDirty = true; + rb.bodyCreated = false; + + entity.set(rb); +} + +void SceneSerializer::deserializeCollider(flecs::entity entity, const nlohmann::json& json) +{ + PhysicsColliderComponent collider; + + // Deserialize shape type + std::string shapeType = json.value("shapeType", "box"); + if (shapeType == "box") { + collider.shapeType = PhysicsColliderComponent::ShapeType::Box; + } else if (shapeType == "sphere") { + collider.shapeType = PhysicsColliderComponent::ShapeType::Sphere; + } else if (shapeType == "capsule") { + collider.shapeType = PhysicsColliderComponent::ShapeType::Capsule; + } else if (shapeType == "cylinder") { + collider.shapeType = PhysicsColliderComponent::ShapeType::Cylinder; + } else if (shapeType == "mesh") { + collider.shapeType = PhysicsColliderComponent::ShapeType::Mesh; + } else if (shapeType == "convexHull") { + collider.shapeType = PhysicsColliderComponent::ShapeType::ConvexHull; + } + + // Deserialize shape parameters + if (json.contains("parameters")) { + auto& params = json["parameters"]; + collider.parameters = Ogre::Vector3( + params.value("x", 0.5f), + params.value("y", 0.5f), + params.value("z", 0.5f) + ); + } + + collider.radius = json.value("radius", 0.5f); + collider.halfHeight = json.value("halfHeight", 1.0f); + collider.meshName = json.value("meshName", ""); + + // Deserialize offset + if (json.contains("offset")) { + auto& off = json["offset"]; + collider.offset = Ogre::Vector3( + off.value("x", 0.0f), + off.value("y", 0.0f), + off.value("z", 0.0f) + ); + } + + // Deserialize rotation offset + if (json.contains("rotationOffset")) { + auto& rot = json["rotationOffset"]; + collider.rotationOffset = Ogre::Quaternion( + rot.value("w", 1.0f), + rot.value("x", 0.0f), + rot.value("y", 0.0f), + rot.value("z", 0.0f) + ); + } + + // Mark as dirty so physics system will create the shape + collider.shapeDirty = true; + + entity.set(collider); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 7377d95..e309e46 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -42,11 +42,15 @@ private: nlohmann::json serializeTransform(flecs::entity entity); nlohmann::json serializeRenderable(flecs::entity entity); nlohmann::json serializeEntityName(flecs::entity entity); + nlohmann::json serializeRigidBody(flecs::entity entity); + nlohmann::json serializeCollider(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); flecs::world& m_world; Ogre::SceneManager* m_sceneMgr; diff --git a/src/features/editScene/ui/PhysicsColliderEditor.cpp b/src/features/editScene/ui/PhysicsColliderEditor.cpp new file mode 100644 index 0000000..6e009b0 --- /dev/null +++ b/src/features/editScene/ui/PhysicsColliderEditor.cpp @@ -0,0 +1,299 @@ +#include "PhysicsColliderEditor.hpp" +#include "../components/RigidBody.hpp" +#include +#include +#include + +PhysicsColliderEditor::PhysicsColliderEditor(Ogre::SceneManager *sceneMgr) + : m_sceneMgr(sceneMgr) +{ +} + +void PhysicsColliderEditor::scanMeshFiles() +{ + if (m_meshesScanned) + return; + + m_meshFiles.clear(); + + // Get all resource groups + Ogre::ResourceGroupManager &rgm = Ogre::ResourceGroupManager::getSingleton(); + Ogre::StringVector groups = rgm.getResourceGroups(); + + for (const auto &group : groups) { + // Get file info for this group + Ogre::FileInfoListPtr fileList = rgm.findResourceFileInfo(group, "*"); + + if (fileList) { + for (const auto &fileInfo : *fileList) { + const std::string &filename = fileInfo.filename; + // Check for supported extensions + if (filename.size() > 5) { + std::string ext = filename.substr(filename.find_last_of('.') + 1); + // Convert to lowercase + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + if (ext == "mesh" || ext == "glb" || ext == "gltf") { + m_meshFiles.push_back(filename); + } + } + } + } + } + + // Sort and remove duplicates + std::sort(m_meshFiles.begin(), m_meshFiles.end()); + m_meshFiles.erase(std::unique(m_meshFiles.begin(), m_meshFiles.end()), + m_meshFiles.end()); + + m_meshesScanned = true; +} + +bool PhysicsColliderEditor::matchesSearch(const std::string &meshName, + const std::string &search) +{ + if (search.empty()) + return true; + + std::string lowerMesh = meshName; + std::string lowerSearch = search; + std::transform(lowerMesh.begin(), lowerMesh.end(), lowerMesh.begin(), + ::tolower); + std::transform(lowerSearch.begin(), lowerSearch.end(), lowerSearch.begin(), + ::tolower); + + return lowerMesh.find(lowerSearch) != std::string::npos; +} + +void PhysicsColliderEditor::markParentRigidBodiesDirty(flecs::entity entity) +{ + // Mark this entity's collider as dirty + if (entity.has()) { + auto &collider = entity.get_mut(); + collider.markDirty(); + } + + // Check if parent has RigidBody component and mark it dirty + flecs::entity parent = entity.parent(); + if (parent.is_valid() && parent.is_alive() && parent.has()) { + auto &rigidBody = parent.get_mut(); + rigidBody.markDirty(); + } +} + +void PhysicsColliderEditor::renderMeshBrowser(PhysicsColliderComponent &collider) +{ + if (ImGui::BeginPopupModal("Mesh Browser (Collider)", &m_showMeshBrowser, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Search:"); + ImGui::SameLine(); + ImGui::InputText("##search", m_searchBuffer, sizeof(m_searchBuffer)); + + ImGui::Separator(); + + // Show filtered mesh list + ImGui::BeginChild("MeshList", ImVec2(400, 300), true); + + std::string searchStr(m_searchBuffer); + + for (const auto &meshName : m_meshFiles) { + if (!matchesSearch(meshName, searchStr)) + continue; + + // Highlight current selection + bool isCurrent = (collider.meshName == meshName); + if (isCurrent) { + ImGui::PushStyleColor(ImGuiCol_Text, + ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + } + + if (ImGui::Selectable(meshName.c_str(), isCurrent)) { + collider.meshName = meshName; + } + + if (isCurrent) { + ImGui::PopStyleColor(); + } + } + + if (m_meshFiles.empty()) { + ImGui::TextDisabled("No mesh files found"); + } + + ImGui::EndChild(); + + ImGui::Separator(); + + // Show count + int visibleCount = 0; + for (const auto &meshName : m_meshFiles) { + if (matchesSearch(meshName, searchStr)) + visibleCount++; + } + ImGui::Text("Showing %d of %zu meshes", visibleCount, m_meshFiles.size()); + + // Buttons + if (ImGui::Button("Select", ImVec2(120, 0))) { + m_showMeshBrowser = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + m_showMeshBrowser = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Refresh", ImVec2(120, 0))) { + m_meshesScanned = false; + scanMeshFiles(); + } + + ImGui::EndPopup(); + } +} + +bool PhysicsColliderEditor::renderComponent(flecs::entity entity, + PhysicsColliderComponent &collider) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Physics Collider", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Shape type selector + const char *shapeTypes[] = { + "Box", "Sphere", "Capsule", "Cylinder", "Mesh", "Convex Hull" + }; + int currentType = static_cast(collider.shapeType); + if (ImGui::Combo("Shape Type", ¤tType, shapeTypes, + IM_ARRAYSIZE(shapeTypes))) { + collider.shapeType = + static_cast( + currentType); + collider.markDirty(); + markParentRigidBodiesDirty(entity); + modified = true; + } + + // Shape-specific parameters + switch (collider.shapeType) { + case PhysicsColliderComponent::ShapeType::Box: { + float extents[3] = { collider.parameters.x, + collider.parameters.y, + collider.parameters.z }; + if (ImGui::DragFloat3("Half Extents", extents, 0.01f, + 0.001f)) { + collider.parameters = + Ogre::Vector3(extents[0], extents[1], + extents[2]); + collider.markDirty(); + markParentRigidBodiesDirty(entity); + modified = true; + } + break; + } + case PhysicsColliderComponent::ShapeType::Sphere: { + if (ImGui::DragFloat("Radius", &collider.radius, 0.01f, + 0.001f)) { + collider.markDirty(); + markParentRigidBodiesDirty(entity); + modified = true; + } + break; + } + case PhysicsColliderComponent::ShapeType::Capsule: + case PhysicsColliderComponent::ShapeType::Cylinder: { + if (ImGui::DragFloat("Half Height", &collider.halfHeight, 0.01f, + 0.001f)) { + collider.markDirty(); + markParentRigidBodiesDirty(entity); + modified = true; + } + if (ImGui::DragFloat("Radius", &collider.radius, 0.01f, + 0.001f)) { + collider.markDirty(); + markParentRigidBodiesDirty(entity); + modified = true; + } + break; + } + case PhysicsColliderComponent::ShapeType::Mesh: + case PhysicsColliderComponent::ShapeType::ConvexHull: { + char meshName[256]; + std::strncpy(meshName, collider.meshName.c_str(), + sizeof(meshName) - 1); + meshName[sizeof(meshName) - 1] = '\0'; + if (ImGui::InputText("Mesh Name", meshName, + sizeof(meshName))) { + collider.meshName = meshName; + collider.markDirty(); + markParentRigidBodiesDirty(entity); + modified = true; + } + + ImGui::SameLine(); + if (ImGui::Button("Browse...")) { + scanMeshFiles(); + m_showMeshBrowser = true; + m_searchBuffer[0] = '\0'; + } + + // Mesh Browser Popup + if (m_showMeshBrowser) { + ImGui::OpenPopup("Mesh Browser (Collider)"); + } + + renderMeshBrowser(collider); + break; + } + } + + // Offset from parent + float offset[3] = { collider.offset.x, collider.offset.y, + collider.offset.z }; + if (ImGui::DragFloat3("Position Offset", offset, 0.01f)) { + collider.offset = + Ogre::Vector3(offset[0], offset[1], offset[2]); + collider.markDirty(); + markParentRigidBodiesDirty(entity); + modified = true; + } + + // Rotation offset + float yaw = 0.0f, pitch = 0.0f, roll = 0.0f; + if (ImGui::TreeNode("Rotation Offset")) { + Ogre::Radian y = collider.rotationOffset.getYaw(); + Ogre::Radian p = collider.rotationOffset.getPitch(); + Ogre::Radian r = collider.rotationOffset.getRoll(); + yaw = y.valueDegrees(); + pitch = p.valueDegrees(); + roll = r.valueDegrees(); + bool rotModified = false; + if (ImGui::DragFloat("Yaw", &yaw, 0.5f)) rotModified = true; + if (ImGui::DragFloat("Pitch", &pitch, 0.5f)) rotModified = true; + if (ImGui::DragFloat("Roll", &roll, 0.5f)) rotModified = true; + if (rotModified) { + Ogre::Quaternion q1(Ogre::Degree(yaw), + Ogre::Vector3::UNIT_Y); + Ogre::Quaternion q2(Ogre::Degree(pitch), + Ogre::Vector3::UNIT_X); + Ogre::Quaternion q3(Ogre::Degree(roll), + Ogre::Vector3::UNIT_Z); + collider.rotationOffset = q1 * q2 * q3; + collider.markDirty(); + markParentRigidBodiesDirty(entity); + modified = true; + } + ImGui::TreePop(); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/PhysicsColliderEditor.hpp b/src/features/editScene/ui/PhysicsColliderEditor.hpp new file mode 100644 index 0000000..e94c724 --- /dev/null +++ b/src/features/editScene/ui/PhysicsColliderEditor.hpp @@ -0,0 +1,34 @@ +#ifndef EDITSCENE_PHYSICSCOLLIDEREDITOR_HPP +#define EDITSCENE_PHYSICSCOLLIDEREDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/PhysicsCollider.hpp" +#include +#include +#include + +/** + * Editor for PhysicsColliderComponent + */ +class PhysicsColliderEditor : public ComponentEditor { +public: + PhysicsColliderEditor(Ogre::SceneManager *sceneMgr = nullptr); + bool renderComponent(flecs::entity entity, + PhysicsColliderComponent &collider) override; + const char *getName() const override { return "Physics Collider"; } + +private: + void scanMeshFiles(); + bool matchesSearch(const std::string &meshName, const std::string &search); + void renderMeshBrowser(PhysicsColliderComponent &collider); + void markParentRigidBodiesDirty(flecs::entity entity); + + Ogre::SceneManager *m_sceneMgr; + std::vector m_meshFiles; + bool m_meshesScanned = false; + bool m_showMeshBrowser = false; + char m_searchBuffer[256] = { 0 }; +}; + +#endif // EDITSCENE_PHYSICSCOLLIDEREDITOR_HPP diff --git a/src/features/editScene/ui/RigidBodyEditor.cpp b/src/features/editScene/ui/RigidBodyEditor.cpp new file mode 100644 index 0000000..c8e5466 --- /dev/null +++ b/src/features/editScene/ui/RigidBodyEditor.cpp @@ -0,0 +1,78 @@ +#include "RigidBodyEditor.hpp" +#include + +bool RigidBodyEditor::renderComponent(flecs::entity entity, + RigidBodyComponent &rigidBody) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Rigid Body", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Body type selector + const char *bodyTypes[] = { "Static", "Dynamic", "Kinematic" }; + int currentType = static_cast(rigidBody.bodyType); + if (ImGui::Combo("Body Type", ¤tType, bodyTypes, + IM_ARRAYSIZE(bodyTypes))) { + rigidBody.bodyType = + static_cast(currentType); + rigidBody.markDirty(); + modified = true; + } + + // Mass (only for dynamic bodies) + bool isDynamic = rigidBody.bodyType == + RigidBodyComponent::BodyType::Dynamic; + if (isDynamic) { + if (ImGui::DragFloat("Mass (kg)", &rigidBody.mass, 0.1f, + 0.001f)) { + if (rigidBody.mass < 0.001f) rigidBody.mass = 0.001f; + rigidBody.markDirty(); + modified = true; + } + } else { + ImGui::Text("Mass: N/A (infinite for static/kinematic)"); + } + + // Friction + if (ImGui::DragFloat("Friction", &rigidBody.friction, 0.01f, 0.0f, + 1.0f)) { + rigidBody.markDirty(); + modified = true; + } + + // Restitution (bounciness) + if (ImGui::DragFloat("Restitution (Bounciness)", + &rigidBody.restitution, 0.01f, 0.0f, 1.0f)) { + rigidBody.markDirty(); + modified = true; + } + + // Is sensor + if (ImGui::Checkbox("Is Sensor", &rigidBody.isSensor)) { + rigidBody.markDirty(); + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Sensors detect collisions but don't physically react"); + } + + // Status info + ImGui::Separator(); + if (rigidBody.bodyCreated) { + ImGui::Text("Status: Body created"); + } else { + ImGui::Text("Status: Body not created"); + } + if (rigidBody.bodyDirty) { + ImGui::TextColored(ImVec4(1, 0.5f, 0, 1), + "(needs rebuild)"); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/RigidBodyEditor.hpp b/src/features/editScene/ui/RigidBodyEditor.hpp new file mode 100644 index 0000000..5d58802 --- /dev/null +++ b/src/features/editScene/ui/RigidBodyEditor.hpp @@ -0,0 +1,18 @@ +#ifndef EDITSCENE_RIGIDBODYEDITOR_HPP +#define EDITSCENE_RIGIDBODYEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/RigidBody.hpp" + +/** + * Editor for RigidBodyComponent + */ +class RigidBodyEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, + RigidBodyComponent &rigidBody) override; + const char *getName() const override { return "Rigid Body"; } +}; + +#endif // EDITSCENE_RIGIDBODYEDITOR_HPP diff --git a/src/physics/physics.cpp b/src/physics/physics.cpp index 567542a..bd15c13 100644 --- a/src/physics/physics.cpp +++ b/src/physics/physics.cpp @@ -1582,6 +1582,12 @@ JPH::ShapeRefC JoltPhysicsWrapper::createSphereShape(float radius) return phys->createSphereShape(radius); } +JPH::ShapeRefC JoltPhysicsWrapper::createCapsuleShape(float halfHeight, + float radius) +{ + return phys->createCapsuleShape(halfHeight, radius); +} + JPH::ShapeRefC JoltPhysicsWrapper::createCylinderShape(float halfHeight, float radius) { diff --git a/src/physics/physics.h b/src/physics/physics.h index 2faa71b..85880f0 100644 --- a/src/physics/physics.h +++ b/src/physics/physics.h @@ -123,6 +123,7 @@ public: bool isAdded(const JPH::BodyID &body); JPH::ShapeRefC createBoxShape(const Ogre::Vector3 &extents); JPH::ShapeRefC createSphereShape(float radius); + JPH::ShapeRefC createCapsuleShape(float halfHeight, float radius); JPH::ShapeRefC createCylinderShape(float halfHeight, float radius); JPH::ShapeRefC createMeshShape(Ogre::MeshPtr mesh); JPH::ShapeRefC createMeshShape(Ogre::String meshName);