Character hair physics implementation
This commit is contained in:
@@ -48,6 +48,7 @@ set(EDITSCENE_SOURCES
|
||||
systems/MarkovNameGenerator.cpp
|
||||
systems/PregnancySystem.cpp
|
||||
systems/AnimationTreeSystem.cpp
|
||||
systems/HairPhysicsSystem.cpp
|
||||
systems/BehaviorTreeSystem.cpp
|
||||
systems/NavMeshSystem.cpp
|
||||
recast/TileCacheNavMesh.cpp
|
||||
@@ -232,6 +233,7 @@ set(EDITSCENE_HEADERS
|
||||
systems/CharacterRegistry.hpp
|
||||
systems/PregnancySystem.hpp
|
||||
systems/AnimationTreeSystem.hpp
|
||||
systems/HairPhysicsSystem.hpp
|
||||
systems/BehaviorTreeSystem.hpp
|
||||
systems/NavMeshSystem.hpp
|
||||
recast/TileCacheNavMesh.hpp
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "systems/ProceduralMeshSystem.hpp"
|
||||
#include "systems/CharacterSlotSystem.hpp"
|
||||
#include "systems/AnimationTreeSystem.hpp"
|
||||
#include "systems/HairPhysicsSystem.hpp"
|
||||
#include "systems/BehaviorTreeSystem.hpp"
|
||||
#include "systems/NavMeshSystem.hpp"
|
||||
#include "systems/CharacterSystem.hpp"
|
||||
@@ -398,6 +399,13 @@ void EditorApp::setup()
|
||||
m_world, m_sceneMgr);
|
||||
m_animationTreeSystem->initialize();
|
||||
|
||||
// Setup HairPhysics system
|
||||
m_hairPhysicsSystem = std::make_unique<HairPhysicsSystem>(
|
||||
m_world, m_sceneMgr,
|
||||
m_physicsSystem->getPhysicsWrapper(),
|
||||
m_characterSlotSystem.get());
|
||||
m_hairPhysicsSystem->initialize();
|
||||
|
||||
// Setup Character physics system (needed by BehaviorTreeSystem)
|
||||
m_characterSystem =
|
||||
std::make_unique<CharacterSystem>(m_world, m_sceneMgr);
|
||||
@@ -1493,10 +1501,20 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_buoyancySystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Hair physics root sync (before physics step) --- */
|
||||
if (m_hairPhysicsSystem) {
|
||||
m_hairPhysicsSystem->prePhysicsUpdate();
|
||||
}
|
||||
|
||||
/* --- Main physics step --- */
|
||||
if (m_physicsSystem) {
|
||||
m_physicsSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Hair physics pose read-back (after physics step) --- */
|
||||
if (m_hairPhysicsSystem) {
|
||||
m_hairPhysicsSystem->postPhysicsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Rendering support systems --- */
|
||||
|
||||
@@ -24,6 +24,7 @@ class ProceduralMaterialSystem;
|
||||
class ProceduralMeshSystem;
|
||||
class CharacterSlotSystem;
|
||||
class AnimationTreeSystem;
|
||||
class HairPhysicsSystem;
|
||||
class BehaviorTreeSystem;
|
||||
class NavMeshSystem;
|
||||
class CharacterSystem;
|
||||
@@ -252,6 +253,7 @@ private:
|
||||
std::unique_ptr<ProceduralMeshSystem> m_proceduralMeshSystem;
|
||||
std::unique_ptr<CharacterSlotSystem> m_characterSlotSystem;
|
||||
std::unique_ptr<AnimationTreeSystem> m_animationTreeSystem;
|
||||
std::unique_ptr<HairPhysicsSystem> m_hairPhysicsSystem;
|
||||
std::unique_ptr<BehaviorTreeSystem> m_behaviorTreeSystem;
|
||||
std::unique_ptr<NavMeshSystem> m_navMeshSystem;
|
||||
std::unique_ptr<CharacterSystem> m_characterSystem;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/Body/BodyID.h>
|
||||
|
||||
/**
|
||||
* Character physics component
|
||||
@@ -48,6 +50,39 @@ struct CharacterComponent {
|
||||
float floorCheckDistance = 2.0f;
|
||||
bool useGravity = true;
|
||||
|
||||
/* Per-character collision group. Body = subgroup 0, head = subgroup 1,
|
||||
* hair joints = 2+. Runtime only — regenerated when character is rebuilt. */
|
||||
uint32_t collisionGroupId = 0;
|
||||
|
||||
/* Separate head collider body for hair physics. Runtime only. */
|
||||
JPH::BodyID headColliderBody;
|
||||
|
||||
/* Head sphere radius for hair physics collision. */
|
||||
float headRadius = 0.13f;
|
||||
|
||||
/* Vertical offset from the character base to the head sphere centre.
|
||||
* If zero, defaults to getTotalHeight() - headRadius. */
|
||||
float headOffsetY = 0.0f;
|
||||
|
||||
/* Bone that drives the head collider. If empty, falls back to the
|
||||
* static offset above. */
|
||||
Ogre::String headBoneName = "mixamorig:Head";
|
||||
|
||||
/* Optional chest/upper-body collider to stop long hair from clipping
|
||||
* through the torso. Runtime only. */
|
||||
JPH::BodyID chestColliderBody;
|
||||
|
||||
/* Chest box half-extents. If any axis is zero, no chest collider is
|
||||
* created. */
|
||||
Ogre::Vector3 chestHalfExtents = Ogre::Vector3(0.2f, 0.12f, 0.12f);
|
||||
|
||||
/* Vertical offset along the world Y axis applied to the chest bone
|
||||
* position when placing the chest collider. */
|
||||
float chestOffsetY = 0.0f;
|
||||
|
||||
/* Bone that drives the chest collider. */
|
||||
Ogre::String chestBoneName = "mixamorig:Spine2";
|
||||
|
||||
float getHalfHeight() const
|
||||
{
|
||||
return height * 0.5f;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "AnimationTree.hpp"
|
||||
|
||||
/**
|
||||
* Selection criteria for a single character slot.
|
||||
* Layer 0 (nude base) can be selected via combo box (e.g. hair styles).
|
||||
|
||||
@@ -469,6 +469,18 @@ static void registerAllComponents()
|
||||
lua_setfield(L, -2, "linearVelocity");
|
||||
lua_pushboolean(L, c.enabled ? 1 : 0);
|
||||
lua_setfield(L, -2, "enabled");
|
||||
lua_pushnumber(L, c.headRadius);
|
||||
lua_setfield(L, -2, "headRadius");
|
||||
lua_pushnumber(L, c.headOffsetY);
|
||||
lua_setfield(L, -2, "headOffsetY");
|
||||
lua_pushstring(L, c.headBoneName.c_str());
|
||||
lua_setfield(L, -2, "headBoneName");
|
||||
pushVector3(L, c.chestHalfExtents);
|
||||
lua_setfield(L, -2, "chestHalfExtents");
|
||||
lua_pushnumber(L, c.chestOffsetY);
|
||||
lua_setfield(L, -2, "chestOffsetY");
|
||||
lua_pushstring(L, c.chestBoneName.c_str());
|
||||
lua_setfield(L, -2, "chestBoneName");
|
||||
lua_pushboolean(L, c.useGravity ? 1 : 0);
|
||||
lua_setfield(L, -2, "useGravity");
|
||||
, if (lua_getfield(L, idx, "radius"), lua_isnumber(L, -1))
|
||||
@@ -486,6 +498,24 @@ static void registerAllComponents()
|
||||
if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
|
||||
c.enabled = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "headRadius"), lua_isnumber(L, -1))
|
||||
c.headRadius = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "headOffsetY"), lua_isnumber(L, -1))
|
||||
c.headOffsetY = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "headBoneName"), lua_isstring(L, -1))
|
||||
c.headBoneName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "chestHalfExtents"), lua_istable(L, -1))
|
||||
c.chestHalfExtents = readVector3(L, lua_gettop(L));
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "chestOffsetY"), lua_isnumber(L, -1))
|
||||
c.chestOffsetY = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "chestBoneName"), lua_isstring(L, -1))
|
||||
c.chestBoneName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "useGravity"), lua_isboolean(L, -1))
|
||||
c.useGravity = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1););
|
||||
|
||||
@@ -86,13 +86,19 @@ public:
|
||||
{
|
||||
switch (inObject1) {
|
||||
case Layers::NON_MOVING:
|
||||
return inObject2 ==
|
||||
Layers::MOVING; // Non moving only collides with moving
|
||||
return inObject2 == Layers::MOVING ||
|
||||
inObject2 == Layers::HAIR;
|
||||
case Layers::MOVING:
|
||||
return true; // Moving collides with everything
|
||||
return inObject2 != Layers::HEAD;
|
||||
case Layers::SENSORS:
|
||||
return inObject2 ==
|
||||
Layers::MOVING; // Non moving only collides with moving
|
||||
return inObject2 == Layers::MOVING;
|
||||
case Layers::HAIR:
|
||||
return inObject2 == Layers::NON_MOVING ||
|
||||
inObject2 == Layers::MOVING ||
|
||||
inObject2 == Layers::HAIR ||
|
||||
inObject2 == Layers::HEAD;
|
||||
case Layers::HEAD:
|
||||
return inObject2 == Layers::HAIR;
|
||||
default:
|
||||
JPH_ASSERT(false);
|
||||
return false;
|
||||
@@ -111,6 +117,8 @@ public:
|
||||
BroadPhaseLayers::NON_MOVING;
|
||||
mObjectToBroadPhase[Layers::MOVING] = BroadPhaseLayers::MOVING;
|
||||
mObjectToBroadPhase[Layers::SENSORS] = BroadPhaseLayers::MOVING;
|
||||
mObjectToBroadPhase[Layers::HAIR] = BroadPhaseLayers::MOVING;
|
||||
mObjectToBroadPhase[Layers::HEAD] = BroadPhaseLayers::MOVING;
|
||||
}
|
||||
|
||||
virtual uint GetNumBroadPhaseLayers() const override
|
||||
@@ -157,6 +165,13 @@ public:
|
||||
return inLayer2 == BroadPhaseLayers::MOVING;
|
||||
case Layers::MOVING:
|
||||
return true;
|
||||
case Layers::SENSORS:
|
||||
return inLayer2 == BroadPhaseLayers::MOVING;
|
||||
case Layers::HAIR:
|
||||
return inLayer2 == BroadPhaseLayers::NON_MOVING ||
|
||||
inLayer2 == BroadPhaseLayers::MOVING;
|
||||
case Layers::HEAD:
|
||||
return inLayer2 == BroadPhaseLayers::MOVING;
|
||||
default:
|
||||
JPH_ASSERT(false);
|
||||
return false;
|
||||
@@ -281,31 +296,15 @@ public:
|
||||
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
|
||||
mLines.push_back({ p2, p3, cv });
|
||||
mLines.push_back({ p3, p1, cv });
|
||||
}
|
||||
#if 0
|
||||
Batch CreateTriangleBatch(const Triangle *inTriangles,
|
||||
@@ -339,6 +338,11 @@ public:
|
||||
std::cout << "geometry\n";
|
||||
}
|
||||
#endif
|
||||
void updateCameraPos()
|
||||
{
|
||||
SetCameraPos(JoltPhysics::convert(
|
||||
mCameraNode->_getDerivedPosition()));
|
||||
}
|
||||
void finish()
|
||||
{
|
||||
Ogre::Vector3 d = mCameraNode->_getDerivedOrientation() *
|
||||
@@ -561,6 +565,7 @@ class Physics {
|
||||
std::set<JPH::BodyID> characterBodies;
|
||||
bool debugDraw;
|
||||
JPH::Vec3 gravity = JPH::Vec3(0.0f, -9.8f, 0.0f);
|
||||
std::unordered_map<uint32_t, JPH::Ref<JPH::GroupFilterTable> > groupFilters;
|
||||
|
||||
public:
|
||||
class ActivationListener : public JPH::BodyActivationListener {
|
||||
@@ -570,6 +575,32 @@ public:
|
||||
virtual void OnBodyDeactivated(const JPH::BodyID &inBodyID,
|
||||
JPH::uint64 inBodyUserData) = 0;
|
||||
};
|
||||
|
||||
JPH::PhysicsSystem *getPhysicsSystem()
|
||||
{
|
||||
return &physics_system;
|
||||
}
|
||||
|
||||
JPH::GroupFilterTable *getOrCreateGroupFilter(uint32_t groupId,
|
||||
uint32_t numSubGroups)
|
||||
{
|
||||
auto it = groupFilters.find(groupId);
|
||||
if (it != groupFilters.end())
|
||||
return it->second.GetPtr();
|
||||
JPH::Ref<JPH::GroupFilterTable> filter =
|
||||
new JPH::GroupFilterTable(numSubGroups);
|
||||
groupFilters[groupId] = filter;
|
||||
return filter.GetPtr();
|
||||
}
|
||||
|
||||
JPH::GroupFilterTable *getGroupFilter(uint32_t groupId) const
|
||||
{
|
||||
auto it = groupFilters.find(groupId);
|
||||
if (it != groupFilters.end())
|
||||
return it->second.GetPtr();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Physics(Ogre::SceneManager *scnMgr, Ogre::SceneNode *cameraNode,
|
||||
ActivationListener *activationListener = nullptr,
|
||||
JPH::ContactListener *contactListener = nullptr)
|
||||
@@ -813,10 +844,12 @@ public:
|
||||
ch->GetPosition()));
|
||||
}
|
||||
}
|
||||
if (debugDraw)
|
||||
if (debugDraw) {
|
||||
mDebugRenderer->updateCameraPos();
|
||||
physics_system.DrawBodies(
|
||||
JPH::BodyManager::DrawSettings(),
|
||||
mDebugRenderer);
|
||||
}
|
||||
mDebugRenderer->finish();
|
||||
mDebugRenderer->NextFrame();
|
||||
#if 0
|
||||
@@ -2000,5 +2033,21 @@ void JoltPhysicsWrapper::setRootMotionCharacter(JPH::BodyID id, bool enabled)
|
||||
phys->setRootMotionCharacter(id, enabled);
|
||||
}
|
||||
|
||||
JPH::PhysicsSystem *JoltPhysicsWrapper::getPhysicsSystem() const
|
||||
{
|
||||
return phys->getPhysicsSystem();
|
||||
}
|
||||
|
||||
JPH::GroupFilterTable *JoltPhysicsWrapper::getOrCreateGroupFilter(
|
||||
uint32_t groupId, uint32_t numSubGroups)
|
||||
{
|
||||
return phys->getOrCreateGroupFilter(groupId, numSubGroups);
|
||||
}
|
||||
|
||||
JPH::GroupFilterTable *JoltPhysicsWrapper::getGroupFilter(uint32_t groupId) const
|
||||
{
|
||||
return phys->getGroupFilter(groupId);
|
||||
}
|
||||
|
||||
template <>
|
||||
JoltPhysicsWrapper *Ogre::Singleton<JoltPhysicsWrapper>::msSingleton = 0;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
|
||||
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
||||
#include <Jolt/Physics/EActivation.h>
|
||||
#include <Jolt/Physics/Collision/GroupFilterTable.h>
|
||||
void physics();
|
||||
namespace JPH
|
||||
{
|
||||
@@ -18,6 +19,7 @@ class Character;
|
||||
class ContactManifold;
|
||||
class ContactSettings;
|
||||
class SubShapeIDPair;
|
||||
class PhysicsSystem;
|
||||
}
|
||||
// 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
|
||||
@@ -28,7 +30,13 @@ 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;
|
||||
static constexpr JPH::ObjectLayer HAIR = 3;
|
||||
static constexpr JPH::ObjectLayer HEAD = 4;
|
||||
static constexpr JPH::ObjectLayer NUM_LAYERS = 5;
|
||||
|
||||
/* Max subgroups per character collision group. Body = 0, head = 1,
|
||||
* hair joints = 2..MAX_SUBGROUPS-1. */
|
||||
static constexpr uint32_t MAX_CHARACTER_SUBGROUPS = 256;
|
||||
};
|
||||
|
||||
// Each broadphase layer results in a separate bounding volume tree in the broad phase. You at least want to have
|
||||
@@ -237,5 +245,14 @@ public:
|
||||
* because the scene node position is driven by root motion
|
||||
* from AnimationTreeSystem. */
|
||||
void setRootMotionCharacter(JPH::BodyID id, bool enabled);
|
||||
JPH::PhysicsSystem *getPhysicsSystem() const;
|
||||
|
||||
/* Shared group filters for per-character collision filtering.
|
||||
* Body = subgroup 0, head = subgroup 1, chest = subgroup 2,
|
||||
* hair joints = 3+. */
|
||||
JPH::GroupFilterTable *getOrCreateGroupFilter(
|
||||
uint32_t groupId,
|
||||
uint32_t numSubGroups = Layers::MAX_CHARACTER_SUBGROUPS);
|
||||
JPH::GroupFilterTable *getGroupFilter(uint32_t groupId) const;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -16,10 +16,13 @@ set(RECASTNAVIGATION_DEMO OFF CACHE BOOL "" FORCE)
|
||||
set(RECASTNAVIGATION_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(RECASTNAVIGATION_ENABLE_ASSERTS "$<CONFIG:Debug>" CACHE STRING "" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "" FORCE)
|
||||
|
||||
|
||||
# Build the core libraries only
|
||||
add_subdirectory(Recast)
|
||||
add_subdirectory(Detour)
|
||||
add_subdirectory(DetourTileCache)
|
||||
add_subdirectory(DetourCrowd)
|
||||
add_subdirectory(DebugUtils)
|
||||
add_subdirectory(Recast EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(Detour EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(DetourTileCache EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(DetourCrowd EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(DebugUtils EXCLUDE_FROM_ALL)
|
||||
|
||||
@@ -549,6 +549,7 @@ void AnimationTreeSystem::update(float deltaTime)
|
||||
/* Handle end-of-animation transitions */
|
||||
checkEndTransitions(e, at, state, ctx);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void AnimationTreeSystem::evaluateNode(const AnimationTreeNode &node,
|
||||
@@ -817,3 +818,5 @@ AnimationTreeSystem::findAnimationNode(const AnimationTreeNode &stateNode) const
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include "../components/AnimationTreeTemplate.hpp"
|
||||
|
||||
|
||||
/**
|
||||
* System that evaluates an AnimationTreeComponent each frame.
|
||||
*
|
||||
|
||||
@@ -504,13 +504,13 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
}
|
||||
}
|
||||
|
||||
/* Populate default slots from catalog if still empty */
|
||||
if (cs.slotSelections.empty()) {
|
||||
auto slots = getSlots(age, cs.sex);
|
||||
for (const auto &slot : slots) {
|
||||
SlotSelection sel;
|
||||
cs.slotSelections[slot] = sel;
|
||||
}
|
||||
/* Make sure every catalog slot exists in the selection map. This
|
||||
* guarantees a face/body master is available so that hair with its
|
||||
* own skeleton can attach to a real head bone. */
|
||||
auto slots = getSlots(age, cs.sex);
|
||||
for (const auto &slot : slots) {
|
||||
if (cs.slotSelections.find(slot) == cs.slotSelections.end())
|
||||
cs.slotSelections[slot] = SlotSelection();
|
||||
}
|
||||
|
||||
if (!e.has<TransformComponent>())
|
||||
@@ -602,9 +602,6 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
if (mesh.empty())
|
||||
continue;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[CharacterSlotSystem] slot='" + slot +
|
||||
|
||||
try {
|
||||
ensureMeshPoseAnimation(mesh);
|
||||
Ogre::MeshPtr partMesh =
|
||||
@@ -612,19 +609,47 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
mesh, "Characters");
|
||||
Ogre::Entity *partEnt =
|
||||
m_sceneMgr->createEntity(partMesh);
|
||||
partEnt->shareSkeletonInstanceWith(masterEnt);
|
||||
transform.node->attachObject(partEnt);
|
||||
m_entities[e.id()].parts[slot] = partEnt;
|
||||
|
||||
/* Check if this mesh has its own skeleton
|
||||
* (e.g. hair exported with a hair-specific
|
||||
* armature). If so, attach to the master's
|
||||
* Head bone instead of sharing skeletons. */
|
||||
const nlohmann::json *entry =
|
||||
findCatalogEntry(age, cs.sex, slot, mesh);
|
||||
bool ownSkeleton = false;
|
||||
Ogre::String attachBone = "mixamorig:Head";
|
||||
if (entry) {
|
||||
ownSkeleton = entry->value(
|
||||
"own_skeleton", false);
|
||||
attachBone = entry->value(
|
||||
"attach_to_bone", attachBone);
|
||||
}
|
||||
|
||||
if (ownSkeleton) {
|
||||
/* Hair with its own skeleton: attach
|
||||
* to the master entity's Head bone
|
||||
* so it follows head movement while
|
||||
* being animated by its own skeleton.
|
||||
*/
|
||||
masterEnt->attachObjectToBone(
|
||||
attachBone, partEnt);
|
||||
Ogre::LogManager::getSingleton()
|
||||
.logMessage(
|
||||
"[CharacterSlotSystem] "
|
||||
"Attached hair '" +
|
||||
slot +
|
||||
"' with own skeleton to "
|
||||
"bone '" +
|
||||
attachBone + "'");
|
||||
} else {
|
||||
partEnt->shareSkeletonInstanceWith(
|
||||
masterEnt);
|
||||
transform.node->attachObject(partEnt);
|
||||
}
|
||||
m_entities[e.id()].parts[slot] = partEnt;
|
||||
|
||||
applyShapeKeys(e, partEnt, entry);
|
||||
|
||||
/* Re-prepare temp buffers after skeleton sharing and
|
||||
* enabling vertex animation. shareSkeletonInstanceWith
|
||||
* replaces the part's AnimationStateSet but does not
|
||||
* recreate temp blend buffers, which are needed for
|
||||
* proper per-entity pose animation.
|
||||
*/
|
||||
prepareEntityTempBlendBuffers(partEnt);
|
||||
|
||||
} catch (const Ogre::Exception &ex) {
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
#include "CharacterSystem.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include <Jolt/Physics/Character/Character.h>
|
||||
#include <Jolt/Physics/Collision/GroupFilterTable.h>
|
||||
#include <Jolt/Physics/Body/BodyInterface.h>
|
||||
#include <Jolt/Physics/PhysicsSystem.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreEntity.h>
|
||||
#include <OgreSkeletonInstance.h>
|
||||
#include <iostream>
|
||||
|
||||
static Ogre::Entity *getMasterEntity(flecs::entity e)
|
||||
{
|
||||
if (!e.is_alive() || !e.has<CharacterSlotsComponent>())
|
||||
return nullptr;
|
||||
const CharacterSlotsComponent &cs = e.get<CharacterSlotsComponent>();
|
||||
return cs.masterEntity;
|
||||
}
|
||||
|
||||
static bool getBoneWorldMatrix(Ogre::Entity *masterEnt,
|
||||
const Ogre::String &boneName,
|
||||
Ogre::Matrix4 &outMat)
|
||||
{
|
||||
if (!masterEnt || boneName.empty() || !masterEnt->hasSkeleton())
|
||||
return false;
|
||||
|
||||
Ogre::SkeletonInstance *skel = masterEnt->getSkeleton();
|
||||
if (!skel)
|
||||
return false;
|
||||
|
||||
Ogre::Bone *bone = nullptr;
|
||||
try {
|
||||
bone = skel->getBone(boneName);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
if (!bone)
|
||||
return false;
|
||||
|
||||
outMat = bone->_getFullTransform();
|
||||
Ogre::SceneNode *node = masterEnt->getParentSceneNode();
|
||||
if (node)
|
||||
outMat = node->_getFullTransform() * outMat;
|
||||
return true;
|
||||
}
|
||||
|
||||
CharacterSystem::CharacterSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr)
|
||||
: m_world(world)
|
||||
@@ -136,6 +177,116 @@ JPH::ShapeRefC CharacterSystem::buildCharacterShape(flecs::entity e,
|
||||
rotations);
|
||||
}
|
||||
|
||||
void CharacterSystem::createHeadCollider(flecs::entity e,
|
||||
CharacterComponent &cc,
|
||||
const TransformComponent &transform)
|
||||
{
|
||||
if (!m_physics)
|
||||
return;
|
||||
|
||||
if (cc.headColliderBody.IsInvalid() == false) {
|
||||
m_physics->removeBody(cc.headColliderBody);
|
||||
m_physics->destroyBody(cc.headColliderBody);
|
||||
cc.headColliderBody = JPH::BodyID();
|
||||
}
|
||||
|
||||
if (cc.collisionGroupId == 0)
|
||||
return;
|
||||
|
||||
float headRadius = cc.headRadius > 0.0f ? cc.headRadius : 0.13f;
|
||||
|
||||
Ogre::Matrix4 headWorld;
|
||||
bool hasBone = getBoneWorldMatrix(getMasterEntity(e), cc.headBoneName,
|
||||
headWorld);
|
||||
if (hasBone) {
|
||||
headWorld.setTrans(headWorld.getTrans() +
|
||||
Ogre::Vector3(0.0f, cc.headOffsetY, 0.0f));
|
||||
} else {
|
||||
float headOffsetY = cc.headOffsetY > 0.0f ?
|
||||
cc.headOffsetY :
|
||||
cc.getTotalHeight() - headRadius;
|
||||
Ogre::Vector3 headOffset(0.0f, headOffsetY, 0.0f);
|
||||
headOffset += cc.offset;
|
||||
Ogre::Vector3 headPos =
|
||||
transform.node->_getDerivedPosition() +
|
||||
transform.node->_getDerivedOrientation() *
|
||||
Ogre::Vector3(cc.offset.x, 0.0f, cc.offset.z) +
|
||||
Ogre::Vector3(0.0f, headOffsetY + cc.offset.y, 0.0f);
|
||||
Ogre::Matrix4 local = Ogre::Matrix4::IDENTITY;
|
||||
local.makeTransform(headPos, Ogre::Vector3::UNIT_SCALE,
|
||||
Ogre::Quaternion::IDENTITY);
|
||||
headWorld = local;
|
||||
}
|
||||
|
||||
JPH::ShapeRefC sphere = m_physics->createSphereShape(headRadius);
|
||||
cc.headColliderBody = m_physics->createBody(
|
||||
sphere, 0.0f, headWorld.getTrans(),
|
||||
Ogre::Quaternion::IDENTITY,
|
||||
JPH::EMotionType::Kinematic, Layers::HEAD);
|
||||
|
||||
JPH::PhysicsSystem *physSystem = m_physics->getPhysicsSystem();
|
||||
if (physSystem) {
|
||||
JPH::BodyInterface &bi = physSystem->GetBodyInterface();
|
||||
JPH::GroupFilterTable *filter =
|
||||
m_physics->getOrCreateGroupFilter(
|
||||
cc.collisionGroupId);
|
||||
bi.SetCollisionGroup(cc.headColliderBody,
|
||||
JPH::CollisionGroup(filter, cc.collisionGroupId,
|
||||
1));
|
||||
}
|
||||
|
||||
m_physics->addBody(cc.headColliderBody,
|
||||
JPH::EActivation::Activate);
|
||||
}
|
||||
|
||||
void CharacterSystem::createChestCollider(flecs::entity e,
|
||||
CharacterComponent &cc)
|
||||
{
|
||||
if (!m_physics)
|
||||
return;
|
||||
|
||||
if (cc.chestColliderBody.IsInvalid() == false) {
|
||||
m_physics->removeBody(cc.chestColliderBody);
|
||||
m_physics->destroyBody(cc.chestColliderBody);
|
||||
cc.chestColliderBody = JPH::BodyID();
|
||||
}
|
||||
|
||||
if (cc.collisionGroupId == 0 ||
|
||||
cc.chestHalfExtents.x <= 0.0f ||
|
||||
cc.chestHalfExtents.y <= 0.0f ||
|
||||
cc.chestHalfExtents.z <= 0.0f)
|
||||
return;
|
||||
|
||||
Ogre::Matrix4 chestWorld;
|
||||
if (!getBoneWorldMatrix(getMasterEntity(e), cc.chestBoneName,
|
||||
chestWorld))
|
||||
return;
|
||||
|
||||
Ogre::Vector3 chestPos = chestWorld.getTrans() +
|
||||
Ogre::Vector3(0.0f, cc.chestOffsetY, 0.0f);
|
||||
Ogre::Matrix3 chestRotMat = chestWorld.linear().orthonormalised();
|
||||
Ogre::Quaternion chestRot = Ogre::Quaternion(chestRotMat);
|
||||
|
||||
JPH::ShapeRefC box = m_physics->createBoxShape(cc.chestHalfExtents);
|
||||
cc.chestColliderBody = m_physics->createBody(
|
||||
box, 0.0f, chestPos, chestRot,
|
||||
JPH::EMotionType::Kinematic, Layers::HEAD);
|
||||
|
||||
JPH::PhysicsSystem *physSystem = m_physics->getPhysicsSystem();
|
||||
if (physSystem) {
|
||||
JPH::BodyInterface &bi = physSystem->GetBodyInterface();
|
||||
JPH::GroupFilterTable *filter =
|
||||
m_physics->getOrCreateGroupFilter(
|
||||
cc.collisionGroupId);
|
||||
bi.SetCollisionGroup(cc.chestColliderBody,
|
||||
JPH::CollisionGroup(filter, cc.collisionGroupId,
|
||||
2));
|
||||
}
|
||||
|
||||
m_physics->addBody(cc.chestColliderBody,
|
||||
JPH::EActivation::Activate);
|
||||
}
|
||||
|
||||
void CharacterSystem::setupEntity(flecs::entity e, CharacterComponent &cc)
|
||||
{
|
||||
if (!m_physics)
|
||||
@@ -153,13 +304,37 @@ void CharacterSystem::setupEntity(flecs::entity e, CharacterComponent &cc)
|
||||
if (!shape)
|
||||
return;
|
||||
|
||||
/* Assign a non-zero collision group for per-character filtering
|
||||
* (body = subgroup 0, head = subgroup 1, chest = subgroup 2,
|
||||
* hair joints = 3+). */
|
||||
if (cc.collisionGroupId == 0)
|
||||
cc.collisionGroupId = static_cast<uint32_t>(e.id()) |
|
||||
0x80000000;
|
||||
|
||||
JPH::CharacterBase *base =
|
||||
m_physics->createCharacter(transform.node, shape);
|
||||
if (!base)
|
||||
return;
|
||||
|
||||
auto *ch = static_cast<JPH::Character *>(base);
|
||||
|
||||
/* Set collision group on character body so hair filtering works. */
|
||||
JPH::PhysicsSystem *physSystem = m_physics->getPhysicsSystem();
|
||||
if (physSystem) {
|
||||
JPH::BodyInterface &bi = physSystem->GetBodyInterface();
|
||||
JPH::GroupFilterTable *filter =
|
||||
m_physics->getOrCreateGroupFilter(
|
||||
cc.collisionGroupId);
|
||||
bi.SetCollisionGroup(
|
||||
ch->GetBodyID(),
|
||||
JPH::CollisionGroup(filter, cc.collisionGroupId,
|
||||
0));
|
||||
}
|
||||
|
||||
ch->AddToPhysicsSystem();
|
||||
|
||||
createHeadCollider(e, cc, transform);
|
||||
createChestCollider(e, cc);
|
||||
// Don't modify gravity factor - let buoyancy handle floating/sinking
|
||||
// m_physics->setGravityFactor(ch->GetBodyID(), 0.0f);
|
||||
cc.hasFloor = false;
|
||||
@@ -194,6 +369,20 @@ void CharacterSystem::teardownEntity(flecs::entity e)
|
||||
std::shared_ptr<JPH::Character>(state.character));
|
||||
}
|
||||
|
||||
if (e.has<CharacterComponent>()) {
|
||||
auto &cc = e.get_mut<CharacterComponent>();
|
||||
if (cc.headColliderBody.IsInvalid() == false) {
|
||||
m_physics->removeBody(cc.headColliderBody);
|
||||
m_physics->destroyBody(cc.headColliderBody);
|
||||
cc.headColliderBody = JPH::BodyID();
|
||||
}
|
||||
if (cc.chestColliderBody.IsInvalid() == false) {
|
||||
m_physics->removeBody(cc.chestColliderBody);
|
||||
m_physics->destroyBody(cc.chestColliderBody);
|
||||
cc.chestColliderBody = JPH::BodyID();
|
||||
}
|
||||
}
|
||||
|
||||
m_states.erase(it);
|
||||
}
|
||||
|
||||
@@ -353,5 +542,58 @@ void CharacterSystem::update(float deltaTime)
|
||||
/* Sync rotation from scene node */
|
||||
state.character->SetRotation(JoltPhysics::convert(
|
||||
state.sceneNode->_getDerivedOrientation()));
|
||||
|
||||
/* Sync head and chest colliders to animated bones. */
|
||||
Ogre::Entity *masterEnt = getMasterEntity(e);
|
||||
Ogre::Matrix4 boneWorld;
|
||||
|
||||
if (cc.headColliderBody.IsInvalid() == false &&
|
||||
transform.node) {
|
||||
float headRadius = cc.headRadius > 0.0f ?
|
||||
cc.headRadius :
|
||||
0.13f;
|
||||
float headOffsetY = cc.headOffsetY > 0.0f ?
|
||||
cc.headOffsetY :
|
||||
cc.getTotalHeight() - headRadius;
|
||||
|
||||
Ogre::Vector3 headPos;
|
||||
if (getBoneWorldMatrix(masterEnt, cc.headBoneName,
|
||||
boneWorld)) {
|
||||
headPos = boneWorld.getTrans() +
|
||||
Ogre::Vector3(0.0f,
|
||||
cc.headOffsetY,
|
||||
0.0f);
|
||||
} else {
|
||||
headPos =
|
||||
transform.node->_getDerivedPosition() +
|
||||
transform.node->_getDerivedOrientation() *
|
||||
Ogre::Vector3(cc.offset.x,
|
||||
0.0f,
|
||||
cc.offset.z) +
|
||||
Ogre::Vector3(0.0f,
|
||||
headOffsetY +
|
||||
cc.offset.y,
|
||||
0.0f);
|
||||
}
|
||||
m_physics->setPositionAndRotation(
|
||||
cc.headColliderBody, headPos,
|
||||
Ogre::Quaternion::IDENTITY, true);
|
||||
}
|
||||
|
||||
if (cc.chestColliderBody.IsInvalid() == false &&
|
||||
getBoneWorldMatrix(masterEnt, cc.chestBoneName,
|
||||
boneWorld)) {
|
||||
Ogre::Vector3 chestPos =
|
||||
boneWorld.getTrans() +
|
||||
Ogre::Vector3(0.0f, cc.chestOffsetY,
|
||||
0.0f);
|
||||
Ogre::Matrix3 chestRotMat =
|
||||
boneWorld.linear().orthonormalised();
|
||||
Ogre::Quaternion chestRot =
|
||||
Ogre::Quaternion(chestRotMat);
|
||||
m_physics->setPositionAndRotation(
|
||||
cc.chestColliderBody, chestPos,
|
||||
chestRot, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ private:
|
||||
JPH::ShapeRefC buildCharacterShape(flecs::entity e,
|
||||
CharacterComponent &cc);
|
||||
JPH::ShapeRefC createColliderShape(PhysicsColliderComponent &collider);
|
||||
void createHeadCollider(flecs::entity e, CharacterComponent &cc,
|
||||
const TransformComponent &transform);
|
||||
void createChestCollider(flecs::entity e, CharacterComponent &cc);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
#include "HairPhysicsSystem.hpp"
|
||||
#include "CharacterSlotSystem.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include <OgreEntity.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreTagPoint.h>
|
||||
|
||||
#include <queue>
|
||||
|
||||
namespace {
|
||||
/**
|
||||
* Find the master bone the hair is attached to.
|
||||
* Prefer the TagPoint's parent bone; fall back to the named head bone.
|
||||
*/
|
||||
Ogre::Bone *findAttachBone(Ogre::Entity *hairEnt,
|
||||
Ogre::Entity *masterEnt)
|
||||
{
|
||||
Ogre::TagPoint *tagPoint = dynamic_cast<Ogre::TagPoint *>(
|
||||
hairEnt->getParentNode());
|
||||
if (tagPoint) {
|
||||
Ogre::Bone *parentBone = dynamic_cast<Ogre::Bone *>(
|
||||
tagPoint->getParent());
|
||||
if (parentBone)
|
||||
return parentBone;
|
||||
}
|
||||
|
||||
if (masterEnt && masterEnt->hasSkeleton()) {
|
||||
Ogre::SkeletonInstance *skel = masterEnt->getSkeleton();
|
||||
if (skel) {
|
||||
try {
|
||||
return skel->getBone("mixamorig:Head");
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* World transform of the attachment bone, including the master entity's
|
||||
* scene-node transform.
|
||||
*/
|
||||
Ogre::Matrix4 getAttachBoneWorldMatrix(Ogre::Entity *masterEnt,
|
||||
Ogre::Bone *attachBone)
|
||||
{
|
||||
if (!attachBone)
|
||||
return Ogre::Matrix4::IDENTITY;
|
||||
|
||||
Ogre::Matrix4 boneWorld = attachBone->_getFullTransform();
|
||||
if (masterEnt) {
|
||||
Ogre::SceneNode *node = masterEnt->getParentSceneNode();
|
||||
if (node)
|
||||
boneWorld = node->_getFullTransform() * boneWorld;
|
||||
}
|
||||
return boneWorld;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
|
||||
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
||||
#include <Jolt/Physics/Constraints/SwingTwistConstraint.h>
|
||||
#include <Jolt/Skeleton/SkeletonPose.h>
|
||||
|
||||
uint32_t HairPhysicsSystem::s_nextCollisionGroup = 1;
|
||||
|
||||
HairPhysicsSystem::HairPhysicsSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
JoltPhysicsWrapper *physics,
|
||||
CharacterSlotSystem *slotSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_physics(physics)
|
||||
, m_slotSystem(slotSystem)
|
||||
{
|
||||
}
|
||||
|
||||
HairPhysicsSystem::~HairPhysicsSystem()
|
||||
{
|
||||
for (auto &entityPair : m_states) {
|
||||
for (auto &slotPair : entityPair.second) {
|
||||
destroyRagdoll(slotPair.second);
|
||||
}
|
||||
}
|
||||
m_states.clear();
|
||||
}
|
||||
|
||||
void HairPhysicsSystem::initialize()
|
||||
{
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
void HairPhysicsSystem::prePhysicsUpdate()
|
||||
{
|
||||
if (!m_initialized || !m_physics)
|
||||
return;
|
||||
|
||||
m_world.query<CharacterSlotsComponent>().each(
|
||||
[this](flecs::entity e, CharacterSlotsComponent &cs) {
|
||||
Ogre::Entity *masterEnt = cs.masterEntity;
|
||||
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
const Ogre::String &slot = pair.first;
|
||||
Ogre::Entity *partEnt =
|
||||
m_slotSystem->getSlotEntity(e, slot);
|
||||
if (!partEnt || !partEnt->hasSkeleton())
|
||||
continue;
|
||||
|
||||
auto &slotMap = m_states[e.id()];
|
||||
auto it = slotMap.find(slot);
|
||||
if (it == slotMap.end() ||
|
||||
it->second.hairEntity != partEnt) {
|
||||
/* Need to create or recreate ragdoll */
|
||||
if (it != slotMap.end())
|
||||
destroyRagdoll(it->second);
|
||||
createRagdoll(e, slot, partEnt,
|
||||
masterEnt);
|
||||
it = slotMap.find(slot);
|
||||
}
|
||||
|
||||
if (it != slotMap.end())
|
||||
syncRootToHead(it->second);
|
||||
}
|
||||
|
||||
/* Clean up ragdolls for slots that no longer have
|
||||
* skeleton parts. */
|
||||
auto &slotMap = m_states[e.id()];
|
||||
std::vector<Ogre::String> toRemove;
|
||||
for (auto &pair : slotMap) {
|
||||
Ogre::Entity *partEnt =
|
||||
m_slotSystem->getSlotEntity(e,
|
||||
pair.first);
|
||||
if (!partEnt || !partEnt->hasSkeleton()) {
|
||||
toRemove.push_back(pair.first);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const auto &slot : toRemove) {
|
||||
destroyRagdoll(slotMap[slot]);
|
||||
slotMap.erase(slot);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HairPhysicsSystem::postPhysicsUpdate()
|
||||
{
|
||||
if (!m_initialized || !m_physics)
|
||||
return;
|
||||
|
||||
for (auto &entityPair : m_states) {
|
||||
for (auto &slotPair : entityPair.second) {
|
||||
syncPhysicsToSkeleton(slotPair.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HairPhysicsSystem::createRagdoll(flecs::entity e,
|
||||
const Ogre::String &slot,
|
||||
Ogre::Entity *hairEnt,
|
||||
Ogre::Entity *masterEnt)
|
||||
{
|
||||
Ogre::SkeletonInstance *skel = hairEnt->getSkeleton();
|
||||
if (!skel)
|
||||
return;
|
||||
|
||||
/* Build ordered bone list via BFS so parents always precede children. */
|
||||
std::vector<Ogre::Bone *> orderedBones;
|
||||
std::unordered_map<Ogre::Bone *, size_t> boneToIndex;
|
||||
std::queue<Ogre::Bone *> bfsQueue;
|
||||
|
||||
for (unsigned short i = 0; i < skel->getNumBones(); ++i) {
|
||||
Ogre::Bone *b = skel->getBone(i);
|
||||
if (!b->getParent()) {
|
||||
bfsQueue.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
while (!bfsQueue.empty()) {
|
||||
Ogre::Bone *b = bfsQueue.front();
|
||||
bfsQueue.pop();
|
||||
boneToIndex[b] = orderedBones.size();
|
||||
orderedBones.push_back(b);
|
||||
for (unsigned short i = 0; i < b->numChildren(); ++i) {
|
||||
Ogre::Bone *child = static_cast<Ogre::Bone *>(b->getChild(i));
|
||||
if (child)
|
||||
bfsQueue.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
if (orderedBones.empty())
|
||||
return;
|
||||
|
||||
/* Sanity check: if the skeleton has too many bones, it's probably
|
||||
* a shared body skeleton rather than a hair-specific skeleton.
|
||||
* Skip ragdoll creation to avoid physics cache overflow. */
|
||||
constexpr size_t maxHairBones = 32;
|
||||
if (orderedBones.size() > maxHairBones)
|
||||
return;
|
||||
|
||||
/* Build JPH skeleton and bind-world matrices. */
|
||||
JPH::Ref<JPH::Skeleton> skeleton = new JPH::Skeleton;
|
||||
std::vector<JPH::Mat44> bindWorldMatrices;
|
||||
bindWorldMatrices.resize(orderedBones.size());
|
||||
|
||||
for (size_t i = 0; i < orderedBones.size(); ++i) {
|
||||
Ogre::Bone *bone = orderedBones[i];
|
||||
Ogre::Bone *parent = static_cast<Ogre::Bone *>(bone->getParent());
|
||||
int parentIndex = -1;
|
||||
if (parent) {
|
||||
auto it = boneToIndex.find(parent);
|
||||
if (it != boneToIndex.end())
|
||||
parentIndex = (int)it->second;
|
||||
}
|
||||
skeleton->AddJoint(bone->getName().c_str(), parentIndex);
|
||||
|
||||
/* Build bind-world matrix from initial pose. */
|
||||
Ogre::Matrix4 localMat = Ogre::Matrix4::IDENTITY;
|
||||
localMat.makeTransform(
|
||||
bone->getInitialPosition(),
|
||||
bone->getInitialScale(),
|
||||
bone->getInitialOrientation());
|
||||
if (parent) {
|
||||
bindWorldMatrices[i] =
|
||||
bindWorldMatrices[boneToIndex[parent]] *
|
||||
ogreMatrixToJolt(localMat);
|
||||
} else {
|
||||
bindWorldMatrices[i] = ogreMatrixToJolt(localMat);
|
||||
}
|
||||
}
|
||||
|
||||
/* World transform of the attachment bone (head) on the master entity. */
|
||||
Ogre::Bone *attachBone = findAttachBone(hairEnt, masterEnt);
|
||||
if (!attachBone) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"HairPhysicsSystem: cannot find attachment bone for " +
|
||||
slot + ", skipping ragdoll");
|
||||
return;
|
||||
}
|
||||
Ogre::Matrix4 attachBoneMat = getAttachBoneWorldMatrix(masterEnt,
|
||||
attachBone);
|
||||
JPH::Mat44 attachBoneJolt = ogreMatrixToJolt(attachBoneMat);
|
||||
|
||||
/* Build ragdoll settings. */
|
||||
JPH::Ref<JPH::RagdollSettings> settings = new JPH::RagdollSettings;
|
||||
settings->mSkeleton = skeleton;
|
||||
settings->mParts.resize(orderedBones.size());
|
||||
|
||||
std::vector<int> parentIndices;
|
||||
parentIndices.resize(orderedBones.size(), -1);
|
||||
|
||||
for (size_t i = 0; i < orderedBones.size(); ++i) {
|
||||
Ogre::Bone *bone = orderedBones[i];
|
||||
JPH::RagdollSettings::Part &part = settings->mParts[i];
|
||||
|
||||
/* Capsule size heuristic from average child distance. */
|
||||
float halfHeight = 0.05f;
|
||||
float radius = 0.02f;
|
||||
if (bone->numChildren() > 0) {
|
||||
float avgLen = 0.0f;
|
||||
int count = 0;
|
||||
for (unsigned short ci = 0; ci < bone->numChildren();
|
||||
++ci) {
|
||||
Ogre::Bone *child = static_cast<Ogre::Bone *>(
|
||||
bone->getChild(ci));
|
||||
if (!child)
|
||||
continue;
|
||||
Ogre::Vector3 diff =
|
||||
child->getInitialPosition();
|
||||
avgLen += diff.length();
|
||||
++count;
|
||||
}
|
||||
if (count > 0) {
|
||||
avgLen /= count;
|
||||
halfHeight = avgLen * 0.5f;
|
||||
radius = avgLen * 0.15f;
|
||||
}
|
||||
}
|
||||
/* Clamp to reasonable bounds. */
|
||||
if (halfHeight < 0.01f)
|
||||
halfHeight = 0.01f;
|
||||
if (radius < 0.005f)
|
||||
radius = 0.005f;
|
||||
if (radius > halfHeight * 0.9f)
|
||||
radius = halfHeight * 0.9f;
|
||||
|
||||
part.SetShape(new JPH::CapsuleShape(halfHeight, radius));
|
||||
|
||||
/* World-space bind position. */
|
||||
JPH::Mat44 worldMat = attachBoneJolt * bindWorldMatrices[i];
|
||||
part.mPosition = JPH::RVec3(worldMat.GetTranslation());
|
||||
part.mRotation = worldMat.GetRotationSafe().GetQuaternion().Normalized();
|
||||
part.mMotionType = (i == 0) ?
|
||||
JPH::EMotionType::Kinematic :
|
||||
JPH::EMotionType::Dynamic;
|
||||
part.mLinearDamping = 0.5f;
|
||||
part.mAngularDamping = 0.5f;
|
||||
part.mGravityFactor = 0.5f;
|
||||
part.mMaxLinearVelocity = 5.0f;
|
||||
part.mMaxAngularVelocity = JPH::DegreesToRadians(720.0f);
|
||||
|
||||
Ogre::Bone *parent = static_cast<Ogre::Bone *>(bone->getParent());
|
||||
if (parent) {
|
||||
auto it = boneToIndex.find(parent);
|
||||
if (it != boneToIndex.end())
|
||||
parentIndices[i] = (int)it->second;
|
||||
}
|
||||
|
||||
if (i > 0 && parentIndices[i] >= 0) {
|
||||
JPH::SwingTwistConstraintSettings *constraint =
|
||||
new JPH::SwingTwistConstraintSettings;
|
||||
constraint->mSpace = JPH::EConstraintSpace::LocalToBodyCOM;
|
||||
/* Position in parent local space (bone's initial pos). */
|
||||
constraint->mPosition1 = JPH::RVec3(
|
||||
JoltPhysics::convert(
|
||||
bone->getInitialPosition()));
|
||||
constraint->mPosition2 = JPH::RVec3::sZero();
|
||||
constraint->mTwistAxis2 = JPH::Vec3::sAxisY();
|
||||
JPH::Quat childLocalRot = JoltPhysics::convert(
|
||||
bone->getInitialOrientation());
|
||||
constraint->mTwistAxis1 = childLocalRot *
|
||||
constraint->mTwistAxis2;
|
||||
JPH::Vec3 planeAxis1 = childLocalRot *
|
||||
JPH::Vec3::sAxisX();
|
||||
constraint->mPlaneAxis1 = planeAxis1;
|
||||
constraint->mPlaneAxis2 = JPH::Vec3::sAxisX();
|
||||
constraint->mTwistMinAngle = -JPH::DegreesToRadians(
|
||||
45.0f);
|
||||
constraint->mTwistMaxAngle = JPH::DegreesToRadians(
|
||||
45.0f);
|
||||
constraint->mNormalHalfConeAngle =
|
||||
JPH::DegreesToRadians(30.0f);
|
||||
constraint->mPlaneHalfConeAngle =
|
||||
JPH::DegreesToRadians(30.0f);
|
||||
part.mToParent = constraint;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collision group setup.
|
||||
* Subgroup 0 = character capsule, 1 = head sphere, 2 = chest sphere,
|
||||
* 3+ = hair joints. */
|
||||
uint32_t collisionGroupId;
|
||||
constexpr uint32_t subgroupHairStart = 3;
|
||||
JPH::GroupFilterTable *groupFilter = nullptr;
|
||||
if (e.has<CharacterComponent>()) {
|
||||
auto &cc = e.get_mut<CharacterComponent>();
|
||||
if (cc.collisionGroupId == 0)
|
||||
cc.collisionGroupId =
|
||||
static_cast<uint32_t>(e.id()) |
|
||||
0x80000000;
|
||||
collisionGroupId = cc.collisionGroupId;
|
||||
groupFilter = m_physics->getOrCreateGroupFilter(
|
||||
collisionGroupId);
|
||||
/* Disable body (subgroup 0), head (1) and chest (2) vs every
|
||||
* hair joint. These kinematic spheres sit at the attachment
|
||||
* points; overlap with the hair chain creates an explosive
|
||||
* feedback loop that throws the joints across the map. */
|
||||
for (size_t i = 0; i < orderedBones.size(); ++i) {
|
||||
uint32_t hairSub = subgroupHairStart + (uint32_t)i;
|
||||
groupFilter->DisableCollision(0, hairSub);
|
||||
groupFilter->DisableCollision(1, hairSub);
|
||||
groupFilter->DisableCollision(2, hairSub);
|
||||
}
|
||||
/* Disable all hair-joint vs hair-joint collisions. Siblings
|
||||
* and parents/children start close together and can otherwise
|
||||
* generate explosive contact impulses. */
|
||||
for (size_t i = 0; i < orderedBones.size(); ++i) {
|
||||
for (size_t j = i + 1; j < orderedBones.size(); ++j) {
|
||||
groupFilter->DisableCollision(
|
||||
subgroupHairStart + (uint32_t)i,
|
||||
subgroupHairStart + (uint32_t)j);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
collisionGroupId = s_nextCollisionGroup++;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < orderedBones.size(); ++i) {
|
||||
settings->mParts[i].mObjectLayer = Layers::HAIR;
|
||||
if (groupFilter) {
|
||||
settings->mParts[i].mCollisionGroup.SetGroupFilter(
|
||||
groupFilter);
|
||||
settings->mParts[i].mCollisionGroup.SetGroupID(
|
||||
collisionGroupId);
|
||||
settings->mParts[i].mCollisionGroup.SetSubGroupID(
|
||||
subgroupHairStart + (uint32_t)i);
|
||||
}
|
||||
}
|
||||
if (!e.has<CharacterComponent>())
|
||||
settings->DisableParentChildCollisions();
|
||||
|
||||
settings->Stabilize();
|
||||
settings->CalculateConstraintPriorities();
|
||||
settings->CalculateBodyIndexToConstraintIndex();
|
||||
|
||||
JPH::PhysicsSystem *physSystem = m_physics->getPhysicsSystem();
|
||||
JPH::Ref<JPH::Ragdoll> ragdoll = settings->CreateRagdoll(
|
||||
collisionGroupId, 0, physSystem);
|
||||
|
||||
/* Store skeleton bones for later pose sync. */
|
||||
HairRagdollState state;
|
||||
state.ragdoll = ragdoll;
|
||||
state.entityId = e.id();
|
||||
state.hairEntity = hairEnt;
|
||||
state.masterEntity = masterEnt;
|
||||
state.slotName = slot;
|
||||
{
|
||||
Ogre::Bone *rootBone = orderedBones[0];
|
||||
Ogre::Matrix4 rootLocal = Ogre::Matrix4::IDENTITY;
|
||||
rootLocal.makeTransform(rootBone->getInitialPosition(),
|
||||
rootBone->getInitialScale(),
|
||||
rootBone->getInitialOrientation());
|
||||
state.rootBindTransform = rootLocal;
|
||||
}
|
||||
state.boneNames.reserve(orderedBones.size());
|
||||
for (Ogre::Bone *bone : orderedBones)
|
||||
state.boneNames.push_back(bone->getName());
|
||||
state.parentIndices = parentIndices;
|
||||
|
||||
/* Ensure all bones are manually controlled. */
|
||||
for (Ogre::Bone *bone : orderedBones) {
|
||||
bone->setManuallyControlled(true);
|
||||
}
|
||||
|
||||
ragdoll->AddToPhysicsSystem(JPH::EActivation::Activate);
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"HairPhysicsSystem: created ragdoll for " + slot +
|
||||
" ('" + hairEnt->getMesh()->getName() + "', " +
|
||||
std::to_string(orderedBones.size()) + " bones, " +
|
||||
std::to_string(ragdoll->GetConstraintCount()) +
|
||||
" constraints)");
|
||||
|
||||
m_states[e.id()][slot] = std::move(state);
|
||||
}
|
||||
|
||||
void HairPhysicsSystem::destroyRagdoll(HairRagdollState &state)
|
||||
{
|
||||
if (state.ragdoll) {
|
||||
state.ragdoll->RemoveFromPhysicsSystem();
|
||||
state.ragdoll = nullptr;
|
||||
}
|
||||
state.boneNames.clear();
|
||||
state.parentIndices.clear();
|
||||
}
|
||||
|
||||
bool HairPhysicsSystem::isStateValid(const HairRagdollState &state) const
|
||||
{
|
||||
if (!state.hairEntity || state.entityId == 0 || !m_slotSystem)
|
||||
return false;
|
||||
|
||||
flecs::entity e = m_world.entity(state.entityId);
|
||||
if (!e.is_alive() || !e.has<CharacterSlotsComponent>())
|
||||
return false;
|
||||
|
||||
Ogre::Entity *current = m_slotSystem->getSlotEntity(e, state.slotName);
|
||||
return current == state.hairEntity && current->hasSkeleton();
|
||||
}
|
||||
|
||||
void HairPhysicsSystem::syncRootToHead(HairRagdollState &state)
|
||||
{
|
||||
if (!state.ragdoll || !isStateValid(state))
|
||||
return;
|
||||
|
||||
flecs::entity e = m_world.entity(state.entityId);
|
||||
const CharacterSlotsComponent &cs = e.get<CharacterSlotsComponent>();
|
||||
Ogre::Entity *masterEnt = cs.masterEntity;
|
||||
if (!masterEnt)
|
||||
return;
|
||||
|
||||
Ogre::Bone *attachBone = findAttachBone(state.hairEntity, masterEnt);
|
||||
if (!attachBone)
|
||||
return;
|
||||
|
||||
/* The root body was created at attachBone * rootBindTransform, so we
|
||||
* must keep it there each frame. */
|
||||
Ogre::Matrix4 attachBoneMat = getAttachBoneWorldMatrix(masterEnt,
|
||||
attachBone);
|
||||
Ogre::Matrix4 rootWorldMat = attachBoneMat * state.rootBindTransform;
|
||||
Ogre::Vector3 pos = rootWorldMat.getTrans();
|
||||
Ogre::Quaternion rot(rootWorldMat.linear());
|
||||
rot.normalise();
|
||||
|
||||
JPH::BodyID rootBody = state.ragdoll->GetBodyID(0);
|
||||
m_physics->setPositionAndRotation(rootBody, pos, rot, true);
|
||||
}
|
||||
|
||||
void HairPhysicsSystem::syncPhysicsToSkeleton(HairRagdollState &state)
|
||||
{
|
||||
if (!state.ragdoll || state.boneNames.empty() ||
|
||||
!isStateValid(state))
|
||||
return;
|
||||
|
||||
Ogre::SkeletonInstance *skel = state.hairEntity->getSkeleton();
|
||||
if (!skel)
|
||||
return;
|
||||
|
||||
JPH::SkeletonPose pose;
|
||||
pose.SetSkeleton(
|
||||
state.ragdoll->GetRagdollSettings()->GetSkeleton());
|
||||
state.ragdoll->GetPose(pose);
|
||||
|
||||
const JPH::SkeletonPose::Mat44Vector &jointMatrices =
|
||||
pose.GetJointMatrices();
|
||||
JPH::RVec3 rootOffset = pose.GetRootOffset();
|
||||
Ogre::Matrix4 rootOffsetMat = Ogre::Matrix4::IDENTITY;
|
||||
rootOffsetMat.makeTransform(JoltPhysics::convert(rootOffset),
|
||||
Ogre::Vector3::UNIT_SCALE,
|
||||
Ogre::Quaternion::IDENTITY);
|
||||
|
||||
/* Get the attachment transform so we can compute hair-entity-local
|
||||
* transforms for the root bone. */
|
||||
flecs::entity e = m_world.entity(state.entityId);
|
||||
const CharacterSlotsComponent &cs = e.get<CharacterSlotsComponent>();
|
||||
Ogre::Entity *masterEnt = cs.masterEntity;
|
||||
|
||||
Ogre::Bone *attachBone = findAttachBone(state.hairEntity, masterEnt);
|
||||
Ogre::Matrix4 attachBoneInv = Ogre::Matrix4::IDENTITY;
|
||||
if (attachBone) {
|
||||
Ogre::Matrix4 attachBoneMat = getAttachBoneWorldMatrix(
|
||||
masterEnt, attachBone);
|
||||
attachBoneInv = attachBoneMat.inverse();
|
||||
}
|
||||
|
||||
std::vector<Ogre::Matrix4> worldMats(state.boneNames.size());
|
||||
for (size_t i = 0; i < state.boneNames.size(); ++i)
|
||||
worldMats[i] = rootOffsetMat * joltMatrixToOgre(jointMatrices[i]);
|
||||
|
||||
for (size_t i = 0; i < state.boneNames.size(); ++i) {
|
||||
Ogre::Bone *bone = skel->getBone(state.boneNames[i]);
|
||||
if (!bone)
|
||||
continue;
|
||||
|
||||
Ogre::Matrix4 localMat;
|
||||
if (state.parentIndices[i] >= 0) {
|
||||
/* Joint transforms from Jolt are in skeleton world space;
|
||||
* Ogre bones expect parent-local space. */
|
||||
localMat = worldMats[state.parentIndices[i]].inverse() *
|
||||
worldMats[i];
|
||||
} else {
|
||||
/* Root bone is in hair-entity local space. */
|
||||
localMat = attachBoneInv * worldMats[i];
|
||||
}
|
||||
|
||||
Ogre::Vector3 pos, scl(Ogre::Vector3::UNIT_SCALE);
|
||||
Ogre::Quaternion rot;
|
||||
pos = localMat.getTrans();
|
||||
rot = Ogre::Quaternion(localMat.linear());
|
||||
rot.normalise();
|
||||
/* Extract scale from basis vectors. */
|
||||
scl.x = Ogre::Vector3(localMat[0][0], localMat[0][1],
|
||||
localMat[0][2]).length();
|
||||
scl.y = Ogre::Vector3(localMat[1][0], localMat[1][1],
|
||||
localMat[1][2]).length();
|
||||
scl.z = Ogre::Vector3(localMat[2][0], localMat[2][1],
|
||||
localMat[2][2]).length();
|
||||
|
||||
bone->setManuallyControlled(true);
|
||||
bone->setPosition(pos);
|
||||
bone->setOrientation(rot);
|
||||
bone->setScale(scl);
|
||||
}
|
||||
}
|
||||
|
||||
JPH::Mat44 HairPhysicsSystem::ogreMatrixToJolt(
|
||||
const Ogre::Matrix4 &m) const
|
||||
{
|
||||
JPH::Mat44 jm;
|
||||
for (int row = 0; row < 4; ++row) {
|
||||
for (int col = 0; col < 4; ++col) {
|
||||
jm(row, col) = m[row][col];
|
||||
}
|
||||
}
|
||||
return jm;
|
||||
}
|
||||
|
||||
Ogre::Matrix4 HairPhysicsSystem::joltMatrixToOgre(
|
||||
const JPH::Mat44 &m) const
|
||||
{
|
||||
Ogre::Matrix4 om;
|
||||
for (int row = 0; row < 4; ++row) {
|
||||
for (int col = 0; col < 4; ++col) {
|
||||
om[row][col] = m(row, col);
|
||||
}
|
||||
}
|
||||
return om;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef EDITSCENE_HAIRPHYSICSSYSTEM_HPP
|
||||
#define EDITSCENE_HAIRPHYSICSSYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "../physics/physics.h"
|
||||
|
||||
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
|
||||
#include <Jolt/Skeleton/Skeleton.h>
|
||||
|
||||
class CharacterSlotSystem;
|
||||
|
||||
/**
|
||||
* System that creates and updates Jolt Physics ragdolls for hair parts
|
||||
* with their own skeleton. Replaces the previous animation-tree-based
|
||||
* hair animation.
|
||||
*/
|
||||
class HairPhysicsSystem {
|
||||
public:
|
||||
HairPhysicsSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
JoltPhysicsWrapper *physics,
|
||||
CharacterSlotSystem *slotSystem);
|
||||
~HairPhysicsSystem();
|
||||
|
||||
void initialize();
|
||||
|
||||
/** Call before physics step to sync root bodies to head transforms. */
|
||||
void prePhysicsUpdate();
|
||||
|
||||
/** Call after physics step to read poses back to Ogre skeletons. */
|
||||
void postPhysicsUpdate();
|
||||
|
||||
private:
|
||||
struct HairRagdollState {
|
||||
JPH::Ref<JPH::Ragdoll> ragdoll;
|
||||
flecs::entity_t entityId = 0;
|
||||
Ogre::Entity *hairEntity = nullptr;
|
||||
Ogre::Entity *masterEntity = nullptr;
|
||||
Ogre::String slotName;
|
||||
std::vector<Ogre::String> boneNames;
|
||||
std::vector<int> parentIndices;
|
||||
Ogre::Matrix4 rootBindTransform = Ogre::Matrix4::IDENTITY;
|
||||
};
|
||||
|
||||
void createRagdoll(flecs::entity e, const Ogre::String &slot,
|
||||
Ogre::Entity *hairEnt, Ogre::Entity *masterEnt);
|
||||
void destroyRagdoll(HairRagdollState &state);
|
||||
void syncRootToHead(HairRagdollState &state);
|
||||
void syncPhysicsToSkeleton(HairRagdollState &state);
|
||||
bool isStateValid(const HairRagdollState &state) const;
|
||||
|
||||
/* Helpers */
|
||||
JPH::Mat44 ogreMatrixToJolt(const Ogre::Matrix4 &m) const;
|
||||
Ogre::Matrix4 joltMatrixToOgre(const JPH::Mat44 &m) const;
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
JoltPhysicsWrapper *m_physics;
|
||||
CharacterSlotSystem *m_slotSystem;
|
||||
bool m_initialized = false;
|
||||
|
||||
/* Per-entity per-slot ragdoll states */
|
||||
std::unordered_map<
|
||||
flecs::entity_t,
|
||||
std::unordered_map<Ogre::String, HairRagdollState> >
|
||||
m_states;
|
||||
|
||||
static uint32_t s_nextCollisionGroup;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_HAIRPHYSICSSYSTEM_HPP
|
||||
@@ -2150,6 +2150,15 @@ nlohmann::json SceneSerializer::serializeCharacter(flecs::entity entity)
|
||||
json["offset"] = { cc.offset.x, cc.offset.y, cc.offset.z };
|
||||
json["linearVelocity"] = { cc.linearVelocity.x, cc.linearVelocity.y,
|
||||
cc.linearVelocity.z };
|
||||
json["headRadius"] = cc.headRadius;
|
||||
json["headOffsetY"] = cc.headOffsetY;
|
||||
json["headBoneName"] = cc.headBoneName;
|
||||
json["chestHalfExtents"] = {
|
||||
cc.chestHalfExtents.x, cc.chestHalfExtents.y,
|
||||
cc.chestHalfExtents.z
|
||||
};
|
||||
json["chestOffsetY"] = cc.chestOffsetY;
|
||||
json["chestBoneName"] = cc.chestBoneName;
|
||||
|
||||
return json;
|
||||
}
|
||||
@@ -2175,6 +2184,22 @@ void SceneSerializer::deserializeCharacter(flecs::entity entity,
|
||||
json["linearVelocity"][1].get<float>(),
|
||||
json["linearVelocity"][2].get<float>());
|
||||
}
|
||||
cc.headRadius = json.value("headRadius", 0.13f);
|
||||
cc.headOffsetY = json.value("headOffsetY", 0.0f);
|
||||
cc.headBoneName = json.value("headBoneName", Ogre::String("mixamorig:Head"));
|
||||
if (json.contains("chestHalfExtents") &&
|
||||
json["chestHalfExtents"].is_array() &&
|
||||
json["chestHalfExtents"].size() >= 3) {
|
||||
cc.chestHalfExtents = Ogre::Vector3(
|
||||
json["chestHalfExtents"][0].get<float>(),
|
||||
json["chestHalfExtents"][1].get<float>(),
|
||||
json["chestHalfExtents"][2].get<float>());
|
||||
} else if (json.contains("chestRadius")) {
|
||||
float r = json.value("chestRadius", 0.25f);
|
||||
cc.chestHalfExtents = Ogre::Vector3(r, r, r);
|
||||
}
|
||||
cc.chestOffsetY = json.value("chestOffsetY", 0.0f);
|
||||
cc.chestBoneName = json.value("chestBoneName", Ogre::String("mixamorig:Spine2"));
|
||||
cc.dirty = true;
|
||||
entity.set<CharacterComponent>(cc);
|
||||
}
|
||||
@@ -2195,6 +2220,11 @@ void SceneSerializer::deserializeCharacterIdentity(flecs::entity entity,
|
||||
entity.set<CharacterIdentityComponent>(ci);
|
||||
}
|
||||
|
||||
/* Forward declarations for AnimationTreeNode serialization */
|
||||
static nlohmann::json serializeAnimationTreeNode(const AnimationTreeNode &node);
|
||||
static void deserializeAnimationTreeNode(AnimationTreeNode &node,
|
||||
const nlohmann::json &json);
|
||||
|
||||
nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
|
||||
{
|
||||
auto &cs = entity.get<CharacterSlotsComponent>();
|
||||
@@ -2217,6 +2247,7 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
|
||||
}
|
||||
json["slotSelections"] = selections;
|
||||
|
||||
|
||||
// Serialize front axis
|
||||
json["frontAxis"] = { cs.frontAxis.x, cs.frontAxis.y, cs.frontAxis.z };
|
||||
|
||||
@@ -2269,6 +2300,7 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity,
|
||||
cs.frontAxis.normalise();
|
||||
}
|
||||
|
||||
|
||||
cs.dirty = true;
|
||||
entity.set<CharacterSlotsComponent>(cs);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "CharacterEditor.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstring>
|
||||
|
||||
bool CharacterEditor::renderComponent(flecs::entity entity,
|
||||
CharacterComponent &cc)
|
||||
@@ -25,6 +26,61 @@ bool CharacterEditor::renderComponent(flecs::entity entity,
|
||||
ImGui::TextDisabled("Total: %.2f m",
|
||||
cc.getTotalHeight());
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Head Collider");
|
||||
if (ImGui::DragFloat("Head Radius##Character",
|
||||
&cc.headRadius, 0.01f, 0.0f, 2.0f,
|
||||
"%.2f")) {
|
||||
modified = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("0 = default (0.13)");
|
||||
if (ImGui::DragFloat("Head Offset Y##Character",
|
||||
&cc.headOffsetY, 0.01f, 0.0f, 5.0f,
|
||||
"%.2f")) {
|
||||
modified = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("0 = default (totalHeight - headRadius)");
|
||||
|
||||
char headBoneBuf[64];
|
||||
std::strncpy(headBoneBuf, cc.headBoneName.c_str(), sizeof(headBoneBuf));
|
||||
headBoneBuf[sizeof(headBoneBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("Head Bone##Character", headBoneBuf,
|
||||
sizeof(headBoneBuf))) {
|
||||
cc.headBoneName = headBoneBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Chest Collider");
|
||||
float che[3] = { cc.chestHalfExtents.x, cc.chestHalfExtents.y,
|
||||
cc.chestHalfExtents.z };
|
||||
if (ImGui::InputFloat3("Chest Half Extents##Character", che,
|
||||
"%.2f")) {
|
||||
cc.chestHalfExtents = Ogre::Vector3(che[0], che[1], che[2]);
|
||||
modified = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Set any axis to 0 to disable");
|
||||
if (ImGui::DragFloat("Chest Offset Y##Character",
|
||||
&cc.chestOffsetY, 0.01f, -2.0f, 2.0f,
|
||||
"%.2f")) {
|
||||
modified = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Vertical offset along world Y");
|
||||
|
||||
char chestBoneBuf[64];
|
||||
std::strncpy(chestBoneBuf, cc.chestBoneName.c_str(),
|
||||
sizeof(chestBoneBuf));
|
||||
chestBoneBuf[sizeof(chestBoneBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("Chest Bone##Character", chestBoneBuf,
|
||||
sizeof(chestBoneBuf))) {
|
||||
cc.chestBoneName = chestBoneBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Offset");
|
||||
float off[3] = { cc.offset.x, cc.offset.y, cc.offset.z };
|
||||
|
||||
@@ -221,61 +221,65 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
|
||||
}
|
||||
} else {
|
||||
/* Layer 0 combo (base meshes like hair) */
|
||||
std::vector<Ogre::String> layer0Meshes =
|
||||
CharacterSlotSystem::
|
||||
getMeshesForLayer(
|
||||
currentAge,
|
||||
cs.sex, slot,
|
||||
0);
|
||||
if (!layer0Meshes.empty()) {
|
||||
Ogre::String l0Preview = "auto";
|
||||
if (!sel.layer0Mesh.empty() &&
|
||||
sel.layer0Mesh != "none")
|
||||
l0Preview = CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
/* Hair slot always uses auto stub; no base editing */
|
||||
if (slot != "hair") {
|
||||
std::vector<Ogre::String> layer0Meshes =
|
||||
CharacterSlotSystem::
|
||||
getMeshesForLayer(
|
||||
currentAge,
|
||||
cs.sex,
|
||||
slot,
|
||||
sel.layer0Mesh);
|
||||
if (ImGui::BeginCombo(
|
||||
"Base (Layer 0)",
|
||||
l0Preview.c_str())) {
|
||||
if (ImGui::Selectable(
|
||||
"auto",
|
||||
sel.layer0Mesh.empty() ||
|
||||
sel.layer0Mesh ==
|
||||
"none")) {
|
||||
sel.layer0Mesh =
|
||||
"none";
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
for (const auto &m :
|
||||
layer0Meshes) {
|
||||
Ogre::String label = CharacterSlotSystem::
|
||||
cs.sex, slot,
|
||||
0);
|
||||
if (!layer0Meshes.empty()) {
|
||||
Ogre::String l0Preview = "auto";
|
||||
if (!sel.layer0Mesh.empty() &&
|
||||
sel.layer0Mesh != "none")
|
||||
l0Preview = CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
cs.sex,
|
||||
slot,
|
||||
m);
|
||||
bool isSelected =
|
||||
(sel.layer0Mesh ==
|
||||
m);
|
||||
sel.layer0Mesh);
|
||||
if (ImGui::BeginCombo(
|
||||
"Base (Layer 0)",
|
||||
l0Preview.c_str())) {
|
||||
if (ImGui::Selectable(
|
||||
label.c_str(),
|
||||
isSelected)) {
|
||||
"auto",
|
||||
sel.layer0Mesh.empty() ||
|
||||
sel.layer0Mesh ==
|
||||
"none")) {
|
||||
sel.layer0Mesh =
|
||||
m;
|
||||
modified =
|
||||
true;
|
||||
cs.dirty =
|
||||
true;
|
||||
"none";
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
for (const auto &m :
|
||||
layer0Meshes) {
|
||||
Ogre::String label = CharacterSlotSystem::
|
||||
getMeshLabel(
|
||||
currentAge,
|
||||
cs.sex,
|
||||
slot,
|
||||
m);
|
||||
bool isSelected =
|
||||
(sel.layer0Mesh ==
|
||||
m);
|
||||
if (ImGui::Selectable(
|
||||
label.c_str(),
|
||||
isSelected)) {
|
||||
sel.layer0Mesh =
|
||||
m;
|
||||
modified =
|
||||
true;
|
||||
cs.dirty =
|
||||
true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("Base: auto (stub hair)");
|
||||
}
|
||||
|
||||
/* Layer 1 combo */
|
||||
std::vector<Ogre::String> layer1Meshes =
|
||||
CharacterSlotSystem::
|
||||
|
||||
+23
-11
@@ -232,22 +232,34 @@ struct GUIListener : public Ogre::RenderTargetListener {
|
||||
float width = size.x;
|
||||
float height = size.y;
|
||||
Ogre::Camera *camera = ECS::get<Camera>().mCamera;
|
||||
// 1. Convert to camera space
|
||||
// 1. Convert to camera space (OGRE camera looks down -Z)
|
||||
Ogre::Vector3 eyeSpacePoint =
|
||||
camera->getViewMatrix() * worldPoint;
|
||||
|
||||
// 2. Project to clip space
|
||||
Ogre::Vector3 clipSpacePoint =
|
||||
camera->getProjectionMatrix() * eyeSpacePoint;
|
||||
if (clipSpacePoint.z < 0.0f)
|
||||
if (eyeSpacePoint.z >= 0.0f)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
// 3. Convert from clip space (-1 to 1) to screen space (0 to 1)
|
||||
// Note: Y is usually flipped in API screen coordinates compared to projection
|
||||
float screenX = (clipSpacePoint.x / 2.0f) + 0.5f;
|
||||
float screenY = 1.0f - ((clipSpacePoint.y / 2.0f) + 0.5f);
|
||||
// 2. Project to homogeneous clip space using Vector4 to preserve W
|
||||
Ogre::Vector4 clipSpacePoint =
|
||||
camera->getProjectionMatrix() *
|
||||
Ogre::Vector4(eyeSpacePoint.x, eyeSpacePoint.y,
|
||||
eyeSpacePoint.z, 1.0f);
|
||||
if (clipSpacePoint.w <= 0.0f)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
// 4. Map to actual pixel dimensions
|
||||
// 3. Perspective divide to get NDC [-1, 1]
|
||||
float ndcX = clipSpacePoint.x / clipSpacePoint.w;
|
||||
float ndcY = clipSpacePoint.y / clipSpacePoint.w;
|
||||
|
||||
// 4. Convert NDC to screen space [0, 1], flipping Y for ImGui
|
||||
float screenX = (ndcX * 0.5f) + 0.5f;
|
||||
float screenY = 1.0f - ((ndcY * 0.5f) + 0.5f);
|
||||
|
||||
// 5. Reject if outside viewport bounds
|
||||
if (screenX < 0.0f || screenX > 1.0f ||
|
||||
screenY < 0.0f || screenY > 1.0f)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
// 6. Map to actual pixel dimensions
|
||||
return Ogre::Vector2(screenX * width, screenY * height);
|
||||
}
|
||||
void preview(const Ogre::RenderTargetViewportEvent &evt)
|
||||
|
||||
Reference in New Issue
Block a user