diff --git a/build.gradle b/build.gradle index 6166ba4d..a3140ab4 100644 --- a/build.gradle +++ b/build.gradle @@ -290,6 +290,11 @@ tasks.register('debugRagdoll', JavaExec) { mainClass = 'testjoltjni.app.performancetest.PerformanceTest' } +tasks.register('runCharacterVirtual', JavaExec) { + args '-s=CharacterVirtual' + enableAssertions = false + mainClass = 'testjoltjni.app.performancetest.PerformanceTest' +} tasks.register('runConvexVsMesh', JavaExec) { args '-s=ConvexVsMesh' enableAssertions = false diff --git a/src/test/java/testjoltjni/app/performancetest/CharacterVirtualScene.java b/src/test/java/testjoltjni/app/performancetest/CharacterVirtualScene.java new file mode 100644 index 00000000..f18a3e4e --- /dev/null +++ b/src/test/java/testjoltjni/app/performancetest/CharacterVirtualScene.java @@ -0,0 +1,318 @@ +/* +Copyright (c) 2025 Stephen Gold + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package testjoltjni.app.performancetest; +import com.github.stephengold.joltjni.*; +import com.github.stephengold.joltjni.enumerate.*; +import com.github.stephengold.joltjni.readonly.*; +import static com.github.stephengold.joltjni.Jolt.*; +import static com.github.stephengold.joltjni.operator.Op.*; +import static com.github.stephengold.joltjni.std.Std.*; +import java.util.*; + +/** + * A line-for-line Java translation of the Jolt Physics CharacterVirtual performance test. + *

+ * Compare with the original by Jorrit Rouwe at + * https://github.com/jrouwe/JoltPhysics/blob/master/PerformanceTest/CharacterVirtualScene.h + */ +// A scene that drops a number of virtual characters on a scene and simulates them +class CharacterVirtualScene implements PerformanceTestScene +{ +public + String GetName() + { + return "CharacterVirtual"; + } + + public boolean Load() + { + final int n = 100; + final float cell_size = 0.5f; + final float max_height = 2.0f; + float center = n * cell_size / 2; + + // Create vertices + final int num_vertices = (n + 1) * (n + 1); + VertexList vertices=new VertexList(); + 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.set(z * (n + 1) + x, new Float3(cell_size * x, max_height * height, cell_size * z)); + } + + // Create regular grid of triangles + final int num_triangles = n * n * 2; + IndexedTriangleList indices=new IndexedTriangleList(); + indices.resize(num_triangles); + int next = 0; + for (int x = 0; x < n; ++x) + for (int z = 0; z < n; ++z) + { + int start = (n + 1) * z + x; + + IndexedTriangle it = indices.get(next++); + it.setIdx(0, start); + it.setIdx(1, start + n + 1); + it.setIdx(2, start + 1); + + it = indices.get(next++); + it.setIdx(0, start + 1); + it.setIdx(1, start + n + 1); + it.setIdx(2, start + n + 2); + } + + // Create mesh + BodyCreationSettings mesh=new BodyCreationSettings(new MeshShapeSettings(vertices, indices),new RVec3((-center), 0, (-center)), Quat.sIdentity(), EMotionType.Static, Layers.NON_MOVING); + mWorld.add(mesh); + + // Create pyramid stairs + for (int i = 0; i < 10; ++i) + { + float width = 4.0f - 0.4f * i; + BodyCreationSettings step=new BodyCreationSettings(new BoxShape(new Vec3(width, 0.5f * cStairsStepHeight, width)),new RVec3(-4.0, -1.0 + (i * cStairsStepHeight), 0), Quat.sIdentity(), EMotionType.Static, Layers.NON_MOVING); + mWorld.add(step); + } + + // Create wall consisting of vertical pillars + ShapeRef wall = new BoxShape(new Vec3(0.1f, 2.5f, 0.1f), 0.0f).toRef(); + for (int z = 0; z < 10; ++z) + { + BodyCreationSettings bcs=new BodyCreationSettings(wall,new RVec3(2.0, 1.0, 2.0 + 0.2 * z), Quat.sIdentity(), EMotionType.Static, Layers.NON_MOVING); + mWorld.add(bcs); + } + + // Create some dynamic boxes + ShapeRef box = new BoxShape(Vec3.sReplicate(0.25f)).toRef(); + for (int x = 0; x < 10; ++x) + for (int z = 0; z < 10; ++z) + { + BodyCreationSettings bcs=new BodyCreationSettings(box,new RVec3(4.0 * x - 20.0, 5.0, 4.0 * z - 20.0), Quat.sIdentity(), EMotionType.Dynamic, Layers.MOVING); + bcs.setOverrideMassProperties ( EOverrideMassProperties.CalculateInertia); + bcs.getMassPropertiesOverride().setMass ( 1.0f); + mWorld.add(bcs); + } + + return true; + } + + public void StartTest(PhysicsSystem inPhysicsSystem, EMotionQuality inMotionQuality) + { + // Construct bodies + BodyInterface bi = inPhysicsSystem.getBodyInterface(); + for (BodyCreationSettings bcs : mWorld) + if (bcs.getMotionType() == EMotionType.Dynamic) + { + bcs.setMotionQuality ( inMotionQuality); + bi.createAndAddBody(bcs, EActivation.Activate); + } + else + bi.createAndAddBody(bcs, EActivation.DontActivate); + + // Construct characters + CharacterId.sSetNextCharacterId(); + ShapeRefC standing_shape =new RotatedTranslatedShapeSettings(new Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat.sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).create().get(); + ShapeRefC inner_standing_shape =new RotatedTranslatedShapeSettings(new 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) + { + CharacterVirtualSettings settings = new CharacterVirtualSettings(); + settings.setShape ( standing_shape); + settings.setSupportingVolume (new Plane(Vec3.sAxisY(), -cCharacterRadiusStanding)); // Accept contacts that touch the lower sphere of the capsule + settings.setInnerBodyShape ( inner_standing_shape); + settings.setInnerBodyLayer ( Layers.MOVING); + CharacterVirtual character = new CharacterVirtual(settings,new RVec3(4.0 * x - 20.0, 2.0, 4.0 * y - 20.0), Quat.sIdentity(), 0, inPhysicsSystem); + character.setCharacterVsCharacterCollision(mCharacterVsCharacterCollision); + character.setListener(new CustomCharacterContactListener() { + public void onCharacterContactAdded(long characterVa, long otherCharacterVa, long subShapeId2Va, double contactLocationX, double contactLocationY, + double contactLocationZ, float contactNormalX, float contactNormalY, float contactNormalZ, long settingsVa) { + RVec3Arg inContactPosition=new RVec3(contactLocationX, contactLocationY, contactLocationZ); + Vec3Arg inContactNormal=new Vec3(contactNormalX, contactNormalY, contactNormalZ); + CharacterVirtualScene.this.OnCharacterContactAdded(new CharacterVirtual(characterVa, mPhysicsSystem), new CharacterVirtual(otherCharacterVa, mPhysicsSystem), new SubShapeId(subShapeId2Va), inContactPosition, inContactNormal, new CharacterContactSettings(settingsVa)); + } + public void onCharacterContactPersisted(long characterVa, long otherCharacterVa, long subShapeId2Va, double contactLocationX, double contactLocationY, + double contactLocationZ, float contactNormalX, float contactNormalY, float contactNormalZ, long settingsVa) { + RVec3Arg inContactPosition=new RVec3(contactLocationX, contactLocationY, contactLocationZ); + Vec3Arg inContactNormal=new Vec3(contactNormalX, contactNormalY, contactNormalZ); + CharacterVirtualScene.this.OnCharacterContactPersisted(new CharacterVirtual(characterVa, mPhysicsSystem), new CharacterVirtual(otherCharacterVa, mPhysicsSystem), new SubShapeId(subShapeId2Va), inContactPosition, inContactNormal, new CharacterContactSettings(settingsVa)); + } + public void onCharacterContactRemoved(long characterVa, long otherCharacterIdVa, long subShapeId2Va) { + CharacterVirtualScene.this.OnCharacterContactRemoved(new CharacterVirtual(characterVa, mPhysicsSystem), new CharacterId(otherCharacterIdVa), new SubShapeId(subShapeId2Va)); + } + public void onContactAdded(long characterVa, long bodyId2Va, long subShapeId2Va, double contactLocationX, double contactLocationY, + double contactLocationZ, float contactNormalX, float contactNormalY, float contactNormalZ, long settingsVa) { + RVec3Arg inContactPosition=new RVec3(contactLocationX, contactLocationY, contactLocationZ); + Vec3Arg inContactNormal=new Vec3(contactNormalX, contactNormalY, contactNormalZ); + CharacterVirtualScene.this.OnContactAdded(new CharacterVirtual(characterVa, mPhysicsSystem), new BodyId(bodyId2Va), new SubShapeId(subShapeId2Va), inContactPosition, inContactNormal, new CharacterContactSettings(settingsVa)); + } + public void onContactPersisted(long characterVa, long bodyId2Va, long subShapeId2Va, double contactLocationX, double contactLocationY, + double contactLocationZ, float contactNormalX, float contactNormalY, float contactNormalZ, long settingsVa) { + RVec3Arg inContactPosition=new RVec3(contactLocationX, contactLocationY, contactLocationZ); + Vec3Arg inContactNormal=new Vec3(contactNormalX, contactNormalY, contactNormalZ); + CharacterVirtualScene.this.OnContactPersisted(new CharacterVirtual(characterVa, mPhysicsSystem), new BodyId(bodyId2Va), new SubShapeId(subShapeId2Va), inContactPosition, inContactNormal, new CharacterContactSettings(settingsVa)); + } + public void onContactRemoved(long characterVa, long bodyId2Va, long subShapeId2Va) { + CharacterVirtualScene.this.OnContactRemoved(new CharacterVirtual(characterVa, mPhysicsSystem), new BodyId(bodyId2Va), new SubShapeId(subShapeId2Va)); + } + }); + mCharacters.add(character.toRef()); + mCharacterVsCharacterCollision.add(character.toRef()); + } + + // Start at time 0 + mTime = 0.0f; + mHash = hashBytes(0L, 0); + } + + public void UpdateTest(PhysicsSystem inPhysicsSystem, TempAllocator ioTempAllocator, float inDeltaTime) + { + // Change direction every 2 seconds + mTime += inDeltaTime; + long count = (long)(mTime / 2.0f) * cNumCharactersX * cNumCharactersY; + + for (CharacterVirtualRef ch : mCharacters) + { + // Calculate new vertical velocity + Vec3 new_velocity; + if (ch.getGroundState() == EGroundState.OnGround // If on ground + && ch.getLinearVelocity().getY() < 0.1f) // And not moving away from ground + new_velocity = Vec3.sZero(); + else + new_velocity = star(ch.getLinearVelocity() ,new Vec3(0, 1, 0)); + plusEquals(new_velocity , star(inPhysicsSystem.getGravity() , inDeltaTime)); + + // Deterministic random input + long hash = (count); + int x = (int)(hash % 10); + int y = (int)((hash / 10) % 10); + int speed = (int)((hash / 100) % 10); + + // Determine target position + RVec3 target =new RVec3(4.0 * x - 20.0, 5.0, 4.0 * y - 20.0); + + // Determine new character velocity + Vec3 direction =new Vec3(minus(target , ch.getPosition())).normalizedOr(Vec3.sZero()); + direction.setY(0); + plusEquals(new_velocity , star((5.0f + 0.5f * speed) , direction)); + ch.setLinearVelocity(new_velocity); + + // Update the character position + ExtendedUpdateSettings update_settings=new ExtendedUpdateSettings(); + ch.extendedUpdate(inDeltaTime, + inPhysicsSystem.getGravity(), + update_settings, + inPhysicsSystem.getDefaultBroadPhaseLayerFilter(Layers.MOVING), + inPhysicsSystem.getDefaultLayerFilter(Layers.MOVING), + new BodyFilter(){ }, + new ShapeFilter(){ }, + ioTempAllocator); + + ++count; + } + } + + public long UpdateHash(long ioHash) + { + // Hash the contact callback hash + ioHash=hashCombine(ioHash, mHash); + + // Hash the state of all characters + for (CharacterVirtualRef ch : mCharacters) + ioHash=hashCombine(ioHash, ch.getPosition()); + return ioHash; + } + + public void StopTest(PhysicsSystem inPhysicsSystem) + { + for (CharacterVirtualRef ch : mCharacters) + mCharacterVsCharacterCollision.remove(ch); + mCharacters.clear(); + } + + // See: CharacterContactListener + void OnContactAdded(ConstCharacterVirtual inCharacter, ConstBodyId inBodyID2, ConstSubShapeId inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings ioSettings) + { + mHash=hashCombine(mHash, 1); + mHash=hashCombine(mHash, inCharacter.getId()); + mHash=hashCombine(mHash, inBodyID2); + mHash=hashCombine(mHash, inSubShapeID2.getValue()); + mHash=hashCombine(mHash, inContactPosition); + mHash=hashCombine(mHash, inContactNormal); + } + void OnContactPersisted(ConstCharacterVirtual inCharacter, ConstBodyId inBodyID2, ConstSubShapeId inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings ioSettings) + { + mHash=hashCombine(mHash, 2); + mHash=hashCombine(mHash, inCharacter.getId()); + mHash=hashCombine(mHash, inBodyID2); + mHash=hashCombine(mHash, inSubShapeID2.getValue()); + mHash=hashCombine(mHash, inContactPosition); + mHash=hashCombine(mHash, inContactNormal); + } + void OnContactRemoved(ConstCharacterVirtual inCharacter, ConstBodyId inBodyID2, ConstSubShapeId inSubShapeID2) + { + mHash=hashCombine(mHash, 3); + mHash=hashCombine(mHash, inCharacter.getId()); + mHash=hashCombine(mHash, inBodyID2); + mHash=hashCombine(mHash, inSubShapeID2.getValue()); + } + void OnCharacterContactAdded(ConstCharacterVirtual inCharacter, ConstCharacterVirtual inOtherCharacter, ConstSubShapeId inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings ioSettings) + { + mHash=hashCombine(mHash, 4); + mHash=hashCombine(mHash, inCharacter.getId()); + mHash=hashCombine(mHash, inOtherCharacter.getId()); + mHash=hashCombine(mHash, inSubShapeID2.getValue()); + mHash=hashCombine(mHash, inContactPosition); + mHash=hashCombine(mHash, inContactNormal); + } + void OnCharacterContactPersisted(ConstCharacterVirtual inCharacter, ConstCharacterVirtual inOtherCharacter, ConstSubShapeId inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings ioSettings) + { + mHash=hashCombine(mHash, 5); + mHash=hashCombine(mHash, inCharacter.getId()); + mHash=hashCombine(mHash, inOtherCharacter.getId()); + mHash=hashCombine(mHash, inSubShapeID2.getValue()); + mHash=hashCombine(mHash, inContactPosition); + mHash=hashCombine(mHash, inContactNormal); + } + void OnCharacterContactRemoved(ConstCharacterVirtual inCharacter, CharacterId inOtherCharacterID, ConstSubShapeId inSubShapeID2) + { + mHash=hashCombine(mHash, 6); + mHash=hashCombine(mHash, inCharacter.getId()); + mHash=hashCombine(mHash, inOtherCharacterID); + mHash=hashCombine(mHash, inSubShapeID2.getValue()); + } + +private + static final int cNumCharactersX = 10; + static final int cNumCharactersY = 10; + static final float cCharacterHeightStanding = 1.35f; + static final float cCharacterRadiusStanding = 0.3f; + static final float cInnerShapeFraction = 0.9f; + static final float cStairsStepHeight = 0.3f; + + float mTime = 0.0f; + long mHash = 0; + List mWorld=new ArrayList<>(); + List mCharacters=new ArrayList<>(); + CharacterVsCharacterCollisionSimple mCharacterVsCharacterCollision=new CharacterVsCharacterCollisionSimple(); + PhysicsSystem mPhysicsSystem; +}; \ No newline at end of file diff --git a/src/test/java/testjoltjni/app/performancetest/PerformanceTest.java b/src/test/java/testjoltjni/app/performancetest/PerformanceTest.java index 50a9b412..3e4f74c8 100644 --- a/src/test/java/testjoltjni/app/performancetest/PerformanceTest.java +++ b/src/test/java/testjoltjni/app/performancetest/PerformanceTest.java @@ -93,6 +93,8 @@ else if (arg.substring(3).equals("Pyramid")) scene = new PyramidScene(); else if (arg.substring(3).equals("LargeMesh")) scene = new LargeMeshScene(); + else if (arg.substring(3).equals("CharacterVirtual")) + scene = new CharacterVirtualScene(); else { Trace("Invalid scene");