// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) // SPDX-FileCopyrightText: 2025 Jorrit Rouwe // SPDX-License-Identifier: MIT #pragma once // Jolt includes #include #include #include #include #include #include // Local includes #include "PerformanceTestScene.h" #include "Layers.h" // A scene that drops a number of virtual characters on a scene and simulates them class CharacterVirtualScene : public PerformanceTestScene, public CharacterContactListener { public: virtual const char * GetName() const override { return "CharacterVirtual"; } virtual bool Load(const String &inAssetPath) override { const int n = 100; const float cell_size = 0.5f; const float max_height = 2.0f; float center = n * cell_size / 2; // Create vertices const int num_vertices = (n + 1) * (n + 1); VertexList vertices; vertices.resize(num_vertices); for (int x = 0; x <= n; ++x) for (int z = 0; z <= n; ++z) { float height = Sin(float(x) * 20.0f / n) * Cos(float(z) * 20.0f / n); vertices[z * (n + 1) + x] = Float3(cell_size * x, max_height * height, cell_size * z); } // Create regular grid of triangles const int num_triangles = n * n * 2; IndexedTriangleList indices; indices.resize(num_triangles); IndexedTriangle *next = indices.data(); for (int x = 0; x < n; ++x) for (int z = 0; z < n; ++z) { int start = (n + 1) * z + x; next->mIdx[0] = start; next->mIdx[1] = start + n + 1; next->mIdx[2] = start + 1; next++; next->mIdx[0] = start + 1; next->mIdx[1] = start + n + 1; next->mIdx[2] = start + n + 2; next++; } // Create mesh BodyCreationSettings mesh(new MeshShapeSettings(vertices, indices), RVec3(Real(-center), 0, Real(-center)), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING); mWorld.push_back(mesh); // Create pyramid stairs for (int i = 0; i < 10; ++i) { float width = 4.0f - 0.4f * i; BodyCreationSettings step(new BoxShape(Vec3(width, 0.5f * cStairsStepHeight, width)), RVec3(-4.0_r, -1.0_r + Real(i * cStairsStepHeight), 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING); mWorld.push_back(step); } // Create wall consisting of vertical pillars Ref wall = new BoxShape(Vec3(0.1f, 2.5f, 0.1f), 0.0f); for (int z = 0; z < 10; ++z) { BodyCreationSettings bcs(wall, RVec3(2.0_r, 1.0_r, 2.0_r + 0.2_r * z), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING); mWorld.push_back(bcs); } // Create some dynamic boxes Ref box = new BoxShape(Vec3::sReplicate(0.25f)); for (int x = 0; x < 10; ++x) for (int z = 0; z < 10; ++z) { BodyCreationSettings bcs(box, RVec3(4.0_r * x - 20.0_r, 5.0_r, 4.0_r * z - 20.0_r), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING); bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia; bcs.mMassPropertiesOverride.mMass = 1.0f; mWorld.push_back(bcs); } return true; } virtual void StartTest(PhysicsSystem &inPhysicsSystem, EMotionQuality inMotionQuality) override { // Construct bodies BodyInterface &bi = inPhysicsSystem.GetBodyInterface(); for (BodyCreationSettings &bcs : mWorld) if (bcs.mMotionType == EMotionType::Dynamic) { bcs.mMotionQuality = inMotionQuality; bi.CreateAndAddBody(bcs, EActivation::Activate); } else bi.CreateAndAddBody(bcs, EActivation::DontActivate); // Construct characters CharacterID::sSetNextCharacterID(); RefConst standing_shape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get(); RefConst inner_standing_shape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cInnerShapeFraction * cCharacterHeightStanding, cInnerShapeFraction * cCharacterRadiusStanding)).Create().Get(); for (int y = 0; y < cNumCharactersY; ++y) for (int x = 0; x < cNumCharactersX; ++x) { Ref settings = new CharacterVirtualSettings(); settings->mShape = standing_shape; settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule settings->mInnerBodyShape = inner_standing_shape; settings->mInnerBodyLayer = Layers::MOVING; Ref character = new CharacterVirtual(settings, RVec3(4.0_r * x - 20.0_r, 2.0_r, 4.0_r * y - 20.0_r), Quat::sIdentity(), 0, &inPhysicsSystem); character->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision); character->SetListener(this); mCharacters.push_back(character); mCharacterVsCharacterCollision.Add(character); } // Start at time 0 mTime = 0.0f; mHash = HashBytes(nullptr, 0); } virtual void UpdateTest(PhysicsSystem &inPhysicsSystem, TempAllocator &ioTempAllocator, float inDeltaTime) override { // Change direction every 2 seconds mTime += inDeltaTime; uint64 count = uint64(mTime / 2.0f) * cNumCharactersX * cNumCharactersY; for (CharacterVirtual *ch : mCharacters) { // Calculate new vertical velocity Vec3 new_velocity; if (ch->GetGroundState() == CharacterVirtual::EGroundState::OnGround // If on ground && ch->GetLinearVelocity().GetY() < 0.1f) // And not moving away from ground new_velocity = Vec3::sZero(); else new_velocity = ch->GetLinearVelocity() * Vec3(0, 1, 0); new_velocity += inPhysicsSystem.GetGravity() * inDeltaTime; // Deterministic random input uint64 hash = Hash {} (count); int x = int(hash % 10); int y = int((hash / 10) % 10); int speed = int((hash / 100) % 10); // Determine target position RVec3 target = RVec3(4.0_r * x - 20.0_r, 5.0_r, 4.0_r * y - 20.0_r); // Determine new character velocity Vec3 direction = Vec3(target - ch->GetPosition()).NormalizedOr(Vec3::sZero()); direction.SetY(0); new_velocity += (5.0f + 0.5f * speed) * direction; ch->SetLinearVelocity(new_velocity); // Update the character position CharacterVirtual::ExtendedUpdateSettings update_settings; ch->ExtendedUpdate(inDeltaTime, inPhysicsSystem.GetGravity(), update_settings, inPhysicsSystem.GetDefaultBroadPhaseLayerFilter(Layers::MOVING), inPhysicsSystem.GetDefaultLayerFilter(Layers::MOVING), { }, { }, ioTempAllocator); ++count; } } virtual void UpdateHash(uint64 &ioHash) const override { // Hash the contact callback hash HashCombine(ioHash, mHash); // Hash the state of all characters for (const CharacterVirtual *ch : mCharacters) HashCombine(ioHash, ch->GetPosition()); } virtual void StopTest(PhysicsSystem &inPhysicsSystem) override { for (const CharacterVirtual *ch : mCharacters) mCharacterVsCharacterCollision.Remove(ch); mCharacters.clear(); } // See: CharacterContactListener virtual void OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override { HashCombine(mHash, 1); HashCombine(mHash, inCharacter->GetID()); HashCombine(mHash, inBodyID2); HashCombine(mHash, inSubShapeID2.GetValue()); HashCombine(mHash, inContactPosition); HashCombine(mHash, inContactNormal); } virtual void OnContactPersisted(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override { HashCombine(mHash, 2); HashCombine(mHash, inCharacter->GetID()); HashCombine(mHash, inBodyID2); HashCombine(mHash, inSubShapeID2.GetValue()); HashCombine(mHash, inContactPosition); HashCombine(mHash, inContactNormal); } virtual void OnContactRemoved(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) override { HashCombine(mHash, 3); HashCombine(mHash, inCharacter->GetID()); HashCombine(mHash, inBodyID2); HashCombine(mHash, inSubShapeID2.GetValue()); } virtual void OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override { HashCombine(mHash, 4); HashCombine(mHash, inCharacter->GetID()); HashCombine(mHash, inOtherCharacter->GetID()); HashCombine(mHash, inSubShapeID2.GetValue()); HashCombine(mHash, inContactPosition); HashCombine(mHash, inContactNormal); } virtual void OnCharacterContactPersisted(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override { HashCombine(mHash, 5); HashCombine(mHash, inCharacter->GetID()); HashCombine(mHash, inOtherCharacter->GetID()); HashCombine(mHash, inSubShapeID2.GetValue()); HashCombine(mHash, inContactPosition); HashCombine(mHash, inContactNormal); } virtual void OnCharacterContactRemoved(const CharacterVirtual *inCharacter, const CharacterID &inOtherCharacterID, const SubShapeID &inSubShapeID2) override { HashCombine(mHash, 6); HashCombine(mHash, inCharacter->GetID()); HashCombine(mHash, inOtherCharacterID); HashCombine(mHash, inSubShapeID2.GetValue()); } private: static constexpr int cNumCharactersX = 10; static constexpr int cNumCharactersY = 10; static constexpr float cCharacterHeightStanding = 1.35f; static constexpr float cCharacterRadiusStanding = 0.3f; static constexpr float cInnerShapeFraction = 0.9f; static constexpr float cStairsStepHeight = 0.3f; float mTime = 0.0f; uint64 mHash = 0; Array mWorld; Array> mCharacters; CharacterVsCharacterCollisionSimple mCharacterVsCharacterCollision; };