From 0b82b9df8fe669036d2f18a6b1cbde8add497b57 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Tue, 19 Mar 2024 14:06:15 -0600 Subject: [PATCH 1/5] Starting revolute jointing --- fission/src/mirabuf/MirabufParser.ts | 2 +- fission/src/mirabuf/MirabufSceneObject.ts | 3 + fission/src/systems/physics/PhysicsSystem.ts | 78 +++++++++++++++++++- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index e13010d2b2..f47f9d5630 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -9,7 +9,7 @@ export enum ParseErrorSeverity { JustAWarning = 2 } -const GROUNDED_JOINT_ID = 'grounded'; +export const GROUNDED_JOINT_ID = 'grounded'; export type ParseError = [severity: ParseErrorSeverity, message: string]; diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 2a2d9b00cb..059c5d45d4 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -26,7 +26,10 @@ class MirabufSceneObject extends SceneObject { super(); this._mirabufInstance = mirabufInstance; + this._bodies = World.PhysicsSystem.CreateBodiesFromParser(mirabufInstance.parser); + + this._debugBodies = null; } diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 2cfc967a5a..b41389c297 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -1,9 +1,9 @@ -import { MirabufFloatArr_JoltVec3, ThreeMatrix4_JoltMat44, ThreeVector3_JoltVec3, _JoltQuat } from "../../util/TypeConversions"; +import { MirabufFloatArr_JoltVec3, MirabufVector3_JoltVec3, ThreeMatrix4_JoltMat44, ThreeVector3_JoltVec3, _JoltQuat } from "../../util/TypeConversions"; import JOLT from "../../util/loading/JoltSyncLoader"; import Jolt from "@barclah/jolt-physics"; import * as THREE from 'three'; import { mirabuf } from '../../proto/mirabuf'; -import MirabufParser, { RigidNodeReadOnly } from "../../mirabuf/MirabufParser"; +import MirabufParser, { GROUNDED_JOINT_ID, RigidNodeReadOnly } from "../../mirabuf/MirabufParser"; import WorldSystem from "../WorldSystem"; const LAYER_NOT_MOVING = 0; @@ -142,6 +142,80 @@ class PhysicsSystem extends WorldSystem { return settings.Create(); } + public CreateJointsFromParser(parser: MirabufParser, rnMapping: Map) { + const jointData = parser.assembly.data!.joints!; + for (const [jGuid, jInst] of (Object.entries(jointData.jointInstances!) as [string, mirabuf.joint.JointInstance][])) { + if (jGuid == GROUNDED_JOINT_ID) + continue; + + const rnA = parser.partToNodeMap.get(jInst.parentPart!); + const rnB = parser.partToNodeMap.get(jInst.childPart!); + + if (!rnA || !rnB) { + console.warn(`Skipping joint '${jInst.info!.name!}'. Couldn't find associated rigid nodes.`); + continue; + } else if (rnA.name == rnB.name) { + console.warn(`Skipping joint '${jInst.info!.name!}'. Jointing the same parts. Likely in issue with Fusion Design structure.`); + continue; + } + + const jDef = parser.assembly.data!.joints!.jointDefinitions![jInst.jointReference!]! as mirabuf.joint.Joint; + const bodyIdA = rnMapping.get(rnA.name); + const bodyIdB = rnMapping.get(rnB.name); + if (!bodyIdA || !bodyIdB) { + console.warn(`Skipping joint '${jInst.info!.name!}'. Failed to find rigid nodes' associated bodies.`); + continue; + } + const bodyA = this.GetBody(bodyIdA); + const bodyB = this.GetBody(bodyIdB); + + switch (jDef.jointMotionType!) { + case mirabuf.joint.JointMotion.REVOLUTE: + this.CreateRevoluteJoint(jInst, jDef, bodyA, bodyB, parser.assembly.info!.version!); + break; + case mirabuf.joint.JointMotion.SLIDER: + console.debug('Slider joint detected. Skipping...'); + break; + default: + console.debug('Unsupported joint detected. Skipping...'); + break; + } + } + } + + private CreateRevoluteJoint( + jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, + bodyA: Jolt.Body, bodyB: Jolt.Body, versionNum: number) { + // HINGE CONSTRAINT + const hingeConstraintSettings = new Jolt.HingeConstraintSettings(); + + const jointOrigin = jointDefinition.origin + ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) + : new Jolt.Vec3(0, 0, 0); + // TODO: Offset transformation for robot builder. + const jointOriginOffset = jointInstance.offset + ? MirabufVector3_JoltVec3(jointInstance.offset as mirabuf.Vector3) + : new Jolt.Vec3(0, 0, 0); + + const anchorPoint = jointOrigin.Add(jointOriginOffset); + hingeConstraintSettings.mPoint1 = hingeConstraintSettings.mPoint2 = anchorPoint; + + const miraAxis = jointDefinition.rotational!.rotationalFreedom!.axis! as mirabuf.Vector3; + let axis: Jolt.Vec3; + // No scaling, these are unit vectors + if (versionNum < 5) { + axis = new JOLT.Vec3(-miraAxis.x ?? 0, miraAxis.y ?? 0, miraAxis.z! ?? 0); + } else { + axis = new JOLT.Vec3(miraAxis.x! ?? 0, miraAxis.y! ?? 0, miraAxis.z! ?? 0); + } + + + const normAxis = new Jolt.Vec3(0, -1, 0); + hingeConstraintSettings.mHingeAxis1 = hingeConstraintSettings.mHingeAxis2 = axis; + hingeConstraintSettings.mNormalAxis1 = hingeConstraintSettings.mNormalAxis2 = normAxis; + this._joltPhysSystem.AddConstraint(hingeConstraintSettings.Create(bodyA, bodyB)); + } + /** * Creates a map, mapping the name of RigidNodes to Jolt BodyIDs * From 257f886838401f7275cbf1bd4ed6065a8e35474e Mon Sep 17 00:00:00 2001 From: Hunter Barclay Date: Tue, 19 Mar 2024 20:52:22 -0600 Subject: [PATCH 2/5] No testing but added revolute joints --- fission/src/mirabuf/MirabufSceneObject.ts | 2 +- fission/src/systems/physics/PhysicsSystem.ts | 26 +++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 059c5d45d4..10d7d13b9f 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -28,7 +28,7 @@ class MirabufSceneObject extends SceneObject { this._mirabufInstance = mirabufInstance; this._bodies = World.PhysicsSystem.CreateBodiesFromParser(mirabufInstance.parser); - + World.PhysicsSystem.CreateJointsFromParser(mirabufInstance.parser, this._bodies); this._debugBodies = null; } diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index b41389c297..4fbe6e2823 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -208,11 +208,11 @@ class PhysicsSystem extends WorldSystem { } else { axis = new JOLT.Vec3(miraAxis.x! ?? 0, miraAxis.y! ?? 0, miraAxis.z! ?? 0); } + hingeConstraintSettings.mHingeAxis1 = hingeConstraintSettings.mHingeAxis2 + = axis.Normalized(); + hingeConstraintSettings.mNormalAxis1 = hingeConstraintSettings.mNormalAxis2 + = getPerpendicular(hingeConstraintSettings.mHingeAxis1); - - const normAxis = new Jolt.Vec3(0, -1, 0); - hingeConstraintSettings.mHingeAxis1 = hingeConstraintSettings.mHingeAxis2 = axis; - hingeConstraintSettings.mNormalAxis1 = hingeConstraintSettings.mNormalAxis2 = normAxis; this._joltPhysSystem.AddConstraint(hingeConstraintSettings.Create(bodyA, bodyB)); } @@ -446,4 +446,22 @@ function filterNonPhysicsNodes(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembl }); } +function getPerpendicular(vec: Jolt.Vec3): Jolt.Vec3 { + return tryGetPerpendicular(vec, new Jolt.Vec3(0, 1, 0)) + ?? tryGetPerpendicular(vec, new Jolt.Vec3(0, 0, 1))!; +} + +function tryGetPerpendicular(vec: Jolt.Vec3, toCheck: Jolt.Vec3): Jolt.Vec3 | undefined { + if (Math.abs(vec.Dot(toCheck) - 1.0) < 0.0001) { + return undefined; + } + + const a = vec.Dot(toCheck) - 1.0; + return new Jolt.Vec3( + toCheck.GetX() - vec.GetX() * a, + toCheck.GetY() - vec.GetY() * a, + toCheck.GetZ() - vec.GetZ() * a + ).Normalized(); +} + export default PhysicsSystem; \ No newline at end of file From a846957022bddb350387733eb60b4684c2f72ac3 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Wed, 20 Mar 2024 04:21:19 -0600 Subject: [PATCH 3/5] Revolute joints actually worked first try. Slider joints are sliding perpendicularly though. --- fission/src/Synthesis.tsx | 3 +- fission/src/systems/physics/PhysicsSystem.ts | 51 ++++++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 96522143af..611afca826 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -55,8 +55,9 @@ import DriverStationPanel from "./panels/simulation/DriverStationPanel" import ManageAssembliesModal from './modals/spawning/ManageAssembliesModal.tsx'; import World from './systems/World.ts'; -const DEFAULT_MIRA_PATH = 'test_mira/Team_2471_(2018)_v7.mira'; +// const DEFAULT_MIRA_PATH = 'test_mira/Team_2471_(2018)_v7.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/Dozer_v2.mira'; +const DEFAULT_MIRA_PATH = 'test_mira/PhysicsSpikeTest_v1.mira'; function Synthesis() { const { openModal, closeModal, getActiveModalElement } = diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 4fbe6e2823..e3b24af09a 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -174,7 +174,7 @@ class PhysicsSystem extends WorldSystem { this.CreateRevoluteJoint(jInst, jDef, bodyA, bodyB, parser.assembly.info!.version!); break; case mirabuf.joint.JointMotion.SLIDER: - console.debug('Slider joint detected. Skipping...'); + this.CreateSliderJoint(jInst, jDef, bodyA, bodyB); break; default: console.debug('Unsupported joint detected. Skipping...'); @@ -187,15 +187,15 @@ class PhysicsSystem extends WorldSystem { jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, bodyA: Jolt.Body, bodyB: Jolt.Body, versionNum: number) { // HINGE CONSTRAINT - const hingeConstraintSettings = new Jolt.HingeConstraintSettings(); + const hingeConstraintSettings = new JOLT.HingeConstraintSettings(); const jointOrigin = jointDefinition.origin ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) - : new Jolt.Vec3(0, 0, 0); + : new JOLT.Vec3(0, 0, 0); // TODO: Offset transformation for robot builder. const jointOriginOffset = jointInstance.offset ? MirabufVector3_JoltVec3(jointInstance.offset as mirabuf.Vector3) - : new Jolt.Vec3(0, 0, 0); + : new JOLT.Vec3(0, 0, 0); const anchorPoint = jointOrigin.Add(jointOriginOffset); hingeConstraintSettings.mPoint1 = hingeConstraintSettings.mPoint2 = anchorPoint; @@ -216,6 +216,37 @@ class PhysicsSystem extends WorldSystem { this._joltPhysSystem.AddConstraint(hingeConstraintSettings.Create(bodyA, bodyB)); } + private CreateSliderJoint( + jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, + bodyA: Jolt.Body, bodyB: Jolt.Body) { + // HINGE CONSTRAINT + const sliderConstraintSettings = new JOLT.SliderConstraintSettings(); + + const jointOrigin = jointDefinition.origin + ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) + : new JOLT.Vec3(0, 0, 0); + // TODO: Offset transformation for robot builder. + const jointOriginOffset = jointInstance.offset + ? MirabufVector3_JoltVec3(jointInstance.offset as mirabuf.Vector3) + : new JOLT.Vec3(0, 0, 0); + + const anchorPoint = jointOrigin.Add(jointOriginOffset); + sliderConstraintSettings.mPoint1 = sliderConstraintSettings.mPoint2 = anchorPoint; + + const miraAxis = jointDefinition.prismatic!.prismaticFreedom!.axis! as mirabuf.Vector3; + const axis = new JOLT.Vec3(miraAxis.x! ?? 0, miraAxis.y! ?? 0, miraAxis.z! ?? 0); + + sliderConstraintSettings.mSliderAxis1 = sliderConstraintSettings.mSliderAxis2 + = axis.Normalized(); + sliderConstraintSettings.mNormalAxis1 = sliderConstraintSettings.mNormalAxis2 + = getPerpendicular(sliderConstraintSettings.mSliderAxis1); + + sliderConstraintSettings.mLimitsMax = 1.0; + sliderConstraintSettings.mLimitsMin = -1.0; + + this._joltPhysSystem.AddConstraint(sliderConstraintSettings.Create(bodyA, bodyB)); + } + /** * Creates a map, mapping the name of RigidNodes to Jolt BodyIDs * @@ -302,9 +333,9 @@ class PhysicsSystem extends WorldSystem { // Little testing components body.SetRestitution(0.2); - const angVelocity = new JOLT.Vec3(2.0, 20.0, 5.0); - body.SetAngularVelocity(angVelocity); - JOLT.destroy(angVelocity); + // const angVelocity = new JOLT.Vec3(2.0, 20.0, 5.0); + // body.SetAngularVelocity(angVelocity); + // JOLT.destroy(angVelocity); } // Cleanup @@ -447,8 +478,8 @@ function filterNonPhysicsNodes(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembl } function getPerpendicular(vec: Jolt.Vec3): Jolt.Vec3 { - return tryGetPerpendicular(vec, new Jolt.Vec3(0, 1, 0)) - ?? tryGetPerpendicular(vec, new Jolt.Vec3(0, 0, 1))!; + return tryGetPerpendicular(vec, new JOLT.Vec3(0, 1, 0)) + ?? tryGetPerpendicular(vec, new JOLT.Vec3(0, 0, 1))!; } function tryGetPerpendicular(vec: Jolt.Vec3, toCheck: Jolt.Vec3): Jolt.Vec3 | undefined { @@ -457,7 +488,7 @@ function tryGetPerpendicular(vec: Jolt.Vec3, toCheck: Jolt.Vec3): Jolt.Vec3 | un } const a = vec.Dot(toCheck) - 1.0; - return new Jolt.Vec3( + return new JOLT.Vec3( toCheck.GetX() - vec.GetX() * a, toCheck.GetY() - vec.GetY() * a, toCheck.GetZ() - vec.GetZ() * a From 949e40d1e2185a28703d6ed033d7494ce3049148 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Wed, 20 Mar 2024 16:35:04 -0600 Subject: [PATCH 4/5] Hinge and Slider constraints have working limits --- .../public/test_mira/HingeTestFission_v1.mira | Bin 0 -> 27419 bytes .../test_mira/SliderTestFission_v2.mira | Bin 0 -> 21415 bytes fission/src/Synthesis.tsx | 4 +- fission/src/mirabuf/MirabufSceneObject.ts | 2 +- fission/src/systems/physics/PhysicsSystem.ts | 77 +++++++++++++++--- fission/src/util/debug/DebugPrint.ts | 13 +++ 6 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 fission/public/test_mira/HingeTestFission_v1.mira create mode 100644 fission/public/test_mira/SliderTestFission_v2.mira diff --git a/fission/public/test_mira/HingeTestFission_v1.mira b/fission/public/test_mira/HingeTestFission_v1.mira new file mode 100644 index 0000000000000000000000000000000000000000..e5a39cf6289a114af6d52b1ca4fbb4fd8f3effaa GIT binary patch literal 27419 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 KmmAYs%m4ryF?zcI literal 0 HcmV?d00001 diff --git a/fission/public/test_mira/SliderTestFission_v2.mira b/fission/public/test_mira/SliderTestFission_v2.mira new file mode 100644 index 0000000000000000000000000000000000000000..7c2b9832d51d220e8d7d732ad96d71f0f7c074c4 GIT binary patch literal 21415 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 + private _constraints: Array /** * Creates a PhysicsSystem object. @@ -32,6 +33,7 @@ class PhysicsSystem extends WorldSystem { super(); this._bodies = []; + this._constraints = []; const joltSettings = new JOLT.JoltSettings(); SetupCollisionFiltering(joltSettings); @@ -174,6 +176,11 @@ class PhysicsSystem extends WorldSystem { this.CreateRevoluteJoint(jInst, jDef, bodyA, bodyB, parser.assembly.info!.version!); break; case mirabuf.joint.JointMotion.SLIDER: + console.debug(`SLIDER: ${ + parser.assembly.data!.parts!.partInstances![jInst.parentPart]!.info!.name! + } <-> ${ + parser.assembly.data!.parts!.partInstances![jInst.childPart]!.info!.name! + }`); this.CreateSliderJoint(jInst, jDef, bodyA, bodyB); break; default: @@ -200,7 +207,9 @@ class PhysicsSystem extends WorldSystem { const anchorPoint = jointOrigin.Add(jointOriginOffset); hingeConstraintSettings.mPoint1 = hingeConstraintSettings.mPoint2 = anchorPoint; - const miraAxis = jointDefinition.rotational!.rotationalFreedom!.axis! as mirabuf.Vector3; + const rotationalFreedom = jointDefinition.rotational!.rotationalFreedom!; + + const miraAxis = rotationalFreedom.axis! as mirabuf.Vector3; let axis: Jolt.Vec3; // No scaling, these are unit vectors if (versionNum < 5) { @@ -212,6 +221,21 @@ class PhysicsSystem extends WorldSystem { = axis.Normalized(); hingeConstraintSettings.mNormalAxis1 = hingeConstraintSettings.mNormalAxis2 = getPerpendicular(hingeConstraintSettings.mHingeAxis1); + + // Some values that are meant to be exactly PI are perceived as being past it, causing unexpected beavior. + // This safety check caps the values to be within [-PI, PI] wth minimal difference in precision. + const piSafetyCheck = (v: number) => Math.min(3.14158, Math.max(-3.14158, v)); + + if (rotationalFreedom.limits && Math.abs((rotationalFreedom.limits.upper ?? 0) - (rotationalFreedom.limits.lower ?? 0)) > 0.001) { + const currentPos = piSafetyCheck(rotationalFreedom.value ?? 0); + const upper = piSafetyCheck(rotationalFreedom.limits.upper ?? 0) - currentPos; + const lower = piSafetyCheck(rotationalFreedom.limits.lower ?? 0) - currentPos; + + console.debug(`Lower: ${lower}\nUpper: ${upper}\nCurrent: ${currentPos}`); + + hingeConstraintSettings.mLimitsMin = -upper; + hingeConstraintSettings.mLimitsMax = -lower; + } this._joltPhysSystem.AddConstraint(hingeConstraintSettings.Create(bodyA, bodyB)); } @@ -219,7 +243,7 @@ class PhysicsSystem extends WorldSystem { private CreateSliderJoint( jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, bodyA: Jolt.Body, bodyB: Jolt.Body) { - // HINGE CONSTRAINT + const sliderConstraintSettings = new JOLT.SliderConstraintSettings(); const jointOrigin = jointDefinition.origin @@ -233,7 +257,9 @@ class PhysicsSystem extends WorldSystem { const anchorPoint = jointOrigin.Add(jointOriginOffset); sliderConstraintSettings.mPoint1 = sliderConstraintSettings.mPoint2 = anchorPoint; - const miraAxis = jointDefinition.prismatic!.prismaticFreedom!.axis! as mirabuf.Vector3; + const prismaticFreedom = jointDefinition.prismatic!.prismaticFreedom!; + + const miraAxis = prismaticFreedom.axis! as mirabuf.Vector3; const axis = new JOLT.Vec3(miraAxis.x! ?? 0, miraAxis.y! ?? 0, miraAxis.z! ?? 0); sliderConstraintSettings.mSliderAxis1 = sliderConstraintSettings.mSliderAxis2 @@ -241,10 +267,31 @@ class PhysicsSystem extends WorldSystem { sliderConstraintSettings.mNormalAxis1 = sliderConstraintSettings.mNormalAxis2 = getPerpendicular(sliderConstraintSettings.mSliderAxis1); - sliderConstraintSettings.mLimitsMax = 1.0; - sliderConstraintSettings.mLimitsMin = -1.0; + if (prismaticFreedom.limits && Math.abs((prismaticFreedom.limits.upper ?? 0) - (prismaticFreedom.limits.lower ?? 0)) > 0.001) { + + const currentPos = (prismaticFreedom.value ?? 0) * 0.01; + const upper = ((prismaticFreedom.limits.upper ?? 0) * 0.01) - currentPos; + const lower = ((prismaticFreedom.limits.lower ?? 0) * 0.01) - currentPos; + + // Calculate mid point + const midPoint = (upper + lower) / 2.0; + const halfRange = Math.abs((upper - lower) / 2.0); + + // Move the anchor points + sliderConstraintSettings.mPoint2 + = anchorPoint.Add(axis.Normalized().Mul(midPoint)); + + sliderConstraintSettings.mLimitsMax = halfRange; + sliderConstraintSettings.mLimitsMin = -halfRange; + } + + // sliderConstraintSettings.mLimitsMax = 1.0; + // sliderConstraintSettings.mLimitsMin = 0.0; + + const constraint = sliderConstraintSettings.Create(bodyA, bodyB); - this._joltPhysSystem.AddConstraint(sliderConstraintSettings.Create(bodyA, bodyB)); + this._constraints.push(constraint); + this._joltPhysSystem.AddConstraint(constraint); } /** @@ -332,10 +379,10 @@ class PhysicsSystem extends WorldSystem { rnToBodies.set(rn.name, body.GetID()); // Little testing components - body.SetRestitution(0.2); - // const angVelocity = new JOLT.Vec3(2.0, 20.0, 5.0); - // body.SetAngularVelocity(angVelocity); - // JOLT.destroy(angVelocity); + body.SetRestitution(0.4); + const angVelocity = new JOLT.Vec3(0, 0, 2); + body.SetAngularVelocity(angVelocity); + JOLT.destroy(angVelocity); } // Cleanup @@ -431,6 +478,12 @@ class PhysicsSystem extends WorldSystem { } public Destroy(): void { + this._constraints.forEach(x => { + this._joltPhysSystem.RemoveConstraint(x); + // JOLT.destroy(x); + }); + this._constraints = []; + // Destroy Jolt Bodies. this.DestroyBodyIds(...this._bodies); this._bodies = []; @@ -483,11 +536,11 @@ function getPerpendicular(vec: Jolt.Vec3): Jolt.Vec3 { } function tryGetPerpendicular(vec: Jolt.Vec3, toCheck: Jolt.Vec3): Jolt.Vec3 | undefined { - if (Math.abs(vec.Dot(toCheck) - 1.0) < 0.0001) { + if (Math.abs(Math.abs(vec.Dot(toCheck)) - 1.0) < 0.0001) { return undefined; } - const a = vec.Dot(toCheck) - 1.0; + const a = vec.Dot(toCheck); return new JOLT.Vec3( toCheck.GetX() - vec.GetX() * a, toCheck.GetY() - vec.GetY() * a, diff --git a/fission/src/util/debug/DebugPrint.ts b/fission/src/util/debug/DebugPrint.ts index 00e44a1f15..d920822152 100644 --- a/fission/src/util/debug/DebugPrint.ts +++ b/fission/src/util/debug/DebugPrint.ts @@ -1,5 +1,6 @@ import { RigidNodeReadOnly } from "@/mirabuf/MirabufParser"; import { mirabuf } from "@/proto/mirabuf"; +import Jolt from "@barclah/jolt-physics"; export function printRigidNodeParts(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembly) { nodes.forEach(x => { @@ -23,4 +24,16 @@ export function mirabufTransformToString(mat: mirabuf.ITransform) { + `${arr[4].toFixed(4)}, ${arr[5].toFixed(4)}, ${arr[6].toFixed(4)}, ${arr[7].toFixed(4)},\n` + `${arr[8].toFixed(4)}, ${arr[9].toFixed(4)}, ${arr[10].toFixed(4)}, ${arr[11].toFixed(4)},\n` + `${arr[12].toFixed(4)}, ${arr[13].toFixed(4)}, ${arr[14].toFixed(4)}, ${arr[15].toFixed(4)},\n]` +} + +export function mirabufVector3ToString(v: mirabuf.Vector3, units: number = 3) { + return `(${v.x.toFixed(units)}, ${v.y.toFixed(units)}, ${v.z.toFixed(units)})`; +} + +export function threeVector3ToString(v: THREE.Vector3, units: number = 3) { + return `(${v.x.toFixed(units)}, ${v.y.toFixed(units)}, ${v.z.toFixed(units)})`; +} + +export function joltVec3ToString(v: Jolt.Vec3 | Jolt.RVec3, units: number = 3) { + return `(${v.GetX().toFixed(units)}, ${v.GetY().toFixed(units)}, ${v.GetZ().toFixed(units)})`; } \ No newline at end of file From 261c82d1f5e5e531afae48f8db7267d5b669a5d3 Mon Sep 17 00:00:00 2001 From: KyroVibe Date: Wed, 20 Mar 2024 21:03:38 -0600 Subject: [PATCH 5/5] Jointing done. No motor control tested --- fission/src/Synthesis.tsx | 4 +- fission/src/mirabuf/MirabufParser.ts | 5 +- fission/src/mirabuf/MirabufSceneObject.ts | 10 +- fission/src/systems/physics/PhysicsSystem.ts | 154 +++++++++++++++---- fission/src/test/PhysicsSystem.test.ts | 8 +- 5 files changed, 139 insertions(+), 42 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 46b332d168..4209eeb1f6 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -55,11 +55,11 @@ import DriverStationPanel from "./panels/simulation/DriverStationPanel" import ManageAssembliesModal from './modals/spawning/ManageAssembliesModal.tsx'; import World from './systems/World.ts'; -// const DEFAULT_MIRA_PATH = 'test_mira/Team_2471_(2018)_v7.mira'; +const DEFAULT_MIRA_PATH = 'test_mira/Team_2471_(2018)_v7.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/Dozer_v2.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/PhysicsSpikeTest_v1.mira'; // const DEFAULT_MIRA_PATH = 'test_mira/SliderTestFission_v2.mira'; -const DEFAULT_MIRA_PATH = 'test_mira/HingeTestFission_v1.mira'; +// const DEFAULT_MIRA_PATH = 'test_mira/HingeTestFission_v1.mira'; function Synthesis() { const { openModal, closeModal, getActiveModalElement } = diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index f47f9d5630..9ae2dde91d 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -10,6 +10,7 @@ export enum ParseErrorSeverity { } export const GROUNDED_JOINT_ID = 'grounded'; +export const GAMEPIECE_SUFFIX = '_gp'; export type ParseError = [severity: ParseErrorSeverity, message: string]; @@ -95,7 +96,7 @@ class MirabufParser { if (gamepieceDefinitions.has(inst.partDefinitionReference!)) { const instNode = this.BinarySearchDesignTree(inst.info!.GUID!); if (instNode) { - const gpRn = this.NewRigidNode('gp'); + const gpRn = this.NewRigidNode(GAMEPIECE_SUFFIX); this.MovePartToRigidNode(instNode!.value!, gpRn); instNode.children && traverseTree(instNode.children, x => this.MovePartToRigidNode(x.value!, gpRn)); } else { @@ -155,7 +156,7 @@ class MirabufParser { } private NewRigidNode(suffix?: string): RigidNode { - const node = new RigidNode((this._nodeNameCounter++).toString() + (suffix ? `_${suffix}` : '')); + const node = new RigidNode(`${this._nodeNameCounter++}${suffix ? suffix : ''}`); this._rigidNodes.push(node); return node; } diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 5b32d4647e..71b8d41e18 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -8,8 +8,9 @@ import Jolt from '@barclah/jolt-physics'; import { JoltMat44_ThreeMatrix4 } from "@/util/TypeConversions"; import * as THREE from 'three'; import JOLT from "@/util/loading/JoltSyncLoader"; +import { LayerReserve } from "@/systems/physics/PhysicsSystem"; -const DEBUG_BODIES = false; +const DEBUG_BODIES = true; interface RnDebugMeshes { colliderMesh: THREE.Mesh; @@ -21,13 +22,17 @@ class MirabufSceneObject extends SceneObject { private _mirabufInstance: MirabufInstance; private _bodies: Map; private _debugBodies: Map | null; + private _physicsLayerReserve: LayerReserve | undefined = undefined; public constructor(mirabufInstance: MirabufInstance) { super(); this._mirabufInstance = mirabufInstance; - this._bodies = World.PhysicsSystem.CreateBodiesFromParser(mirabufInstance.parser); + if (this._mirabufInstance.parser.assembly.dynamic) { + this._physicsLayerReserve = new LayerReserve(); + } + this._bodies = World.PhysicsSystem.CreateBodiesFromParser(mirabufInstance.parser, this._physicsLayerReserve); World.PhysicsSystem.CreateJointsFromParser(mirabufInstance.parser, this._bodies); this._debugBodies = null; @@ -94,6 +99,7 @@ class MirabufSceneObject extends SceneObject { (x.comMesh.material as THREE.Material).dispose(); }); this._debugBodies?.clear(); + this._physicsLayerReserve?.Release(); } private CreateMeshForShape(shape: Jolt.Shape): THREE.Mesh { diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index d8c1992d40..bd152e5124 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -3,12 +3,20 @@ import JOLT from "../../util/loading/JoltSyncLoader"; import Jolt from "@barclah/jolt-physics"; import * as THREE from 'three'; import { mirabuf } from '../../proto/mirabuf'; -import MirabufParser, { GROUNDED_JOINT_ID, RigidNodeReadOnly } from "../../mirabuf/MirabufParser"; +import MirabufParser, { GAMEPIECE_SUFFIX, GROUNDED_JOINT_ID, RigidNodeReadOnly } from "../../mirabuf/MirabufParser"; import WorldSystem from "../WorldSystem"; -const LAYER_NOT_MOVING = 0; -const LAYER_MOVING = 1; -const COUNT_OBJECT_LAYERS = 2; +/** + * Layers used for determining enabled/disabled collisions. + */ +const LAYER_FIELD = 0; // Used for grounded rigid node of a field as well as any rigid nodes jointed to it. +const LAYER_GENERAL_DYNAMIC = 1; // Used for game pieces or any general dynamic objects that can collide with anything and everything. +const RobotLayers: number[] = [ // Reserved layers for robots. Robot layers have no collision with themselves but have collision with everything else. + 2, 3, 4, 5, 6, 7, 8, 9 +]; + +// Please update this accordingly. +const COUNT_OBJECT_LAYERS = 10; const STANDARD_TIME_STEP = 1.0 / 120.0; const STANDARD_SUB_STEPS = 3; @@ -74,7 +82,7 @@ class PhysicsSystem extends WorldSystem { pos, rot, mass ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, - mass ? LAYER_MOVING : LAYER_NOT_MOVING + mass ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD ); if (mass) { creationSettings.mMassPropertiesOverride.mMass = mass; @@ -109,7 +117,7 @@ class PhysicsSystem extends WorldSystem { pos, rot, mass ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, - LAYER_NOT_MOVING + mass ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD ); if (mass) { creationSettings.mMassPropertiesOverride.mMass = mass; @@ -144,6 +152,12 @@ class PhysicsSystem extends WorldSystem { return settings.Create(); } + /** + * Creates all the joints for a mirabuf assembly given an already compiled mapping of rigid nodes to bodies. + * + * @param parser Mirabuf parser with complete set of rigid nodes and assembly data. + * @param rnMapping Mapping of the name of rigid groups to Jolt bodies. Retrieved from CreateBodiesFromParser. + */ public CreateJointsFromParser(parser: MirabufParser, rnMapping: Map) { const jointData = parser.assembly.data!.joints!; for (const [jGuid, jInst] of (Object.entries(jointData.jointInstances!) as [string, mirabuf.joint.JointInstance][])) { @@ -173,15 +187,10 @@ class PhysicsSystem extends WorldSystem { switch (jDef.jointMotionType!) { case mirabuf.joint.JointMotion.REVOLUTE: - this.CreateRevoluteJoint(jInst, jDef, bodyA, bodyB, parser.assembly.info!.version!); + this.CreateHingeConstraint(jInst, jDef, bodyA, bodyB, parser.assembly.info!.version!); break; case mirabuf.joint.JointMotion.SLIDER: - console.debug(`SLIDER: ${ - parser.assembly.data!.parts!.partInstances![jInst.parentPart]!.info!.name! - } <-> ${ - parser.assembly.data!.parts!.partInstances![jInst.childPart]!.info!.name! - }`); - this.CreateSliderJoint(jInst, jDef, bodyA, bodyB); + this.CreateSliderConstraint(jInst, jDef, bodyA, bodyB); break; default: console.debug('Unsupported joint detected. Skipping...'); @@ -190,9 +199,19 @@ class PhysicsSystem extends WorldSystem { } } - private CreateRevoluteJoint( + /** + * Creates a Hinge constraint. + * + * @param jointInstance Joint instance. + * @param jointDefinition Joint definition. + * @param bodyA Parent body to connect. + * @param bodyB Child body to connect. + * @param versionNum Version number of the export. Used for compatability purposes. + * @returns Resulting Jolt Hinge Constraint. + */ + private CreateHingeConstraint( jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, - bodyA: Jolt.Body, bodyB: Jolt.Body, versionNum: number) { + bodyA: Jolt.Body, bodyB: Jolt.Body, versionNum: number): Jolt.HingeConstraint { // HINGE CONSTRAINT const hingeConstraintSettings = new JOLT.HingeConstraintSettings(); @@ -231,18 +250,29 @@ class PhysicsSystem extends WorldSystem { const upper = piSafetyCheck(rotationalFreedom.limits.upper ?? 0) - currentPos; const lower = piSafetyCheck(rotationalFreedom.limits.lower ?? 0) - currentPos; - console.debug(`Lower: ${lower}\nUpper: ${upper}\nCurrent: ${currentPos}`); - hingeConstraintSettings.mLimitsMin = -upper; hingeConstraintSettings.mLimitsMax = -lower; } - this._joltPhysSystem.AddConstraint(hingeConstraintSettings.Create(bodyA, bodyB)); + const constraint = hingeConstraintSettings.Create(bodyA, bodyB); + this._joltPhysSystem.AddConstraint(constraint); + + return JOLT.castObject(constraint, JOLT.HingeConstraint); } - private CreateSliderJoint( + /** + * Creates a new slider constraint. + * + * @param jointInstance Joint instance. + * @param jointDefinition Joint definition. + * @param bodyA Parent body to connect. + * @param bodyB Child body to connect. + * + * @returns Resulting Jolt constraint. + */ + private CreateSliderConstraint( jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, - bodyA: Jolt.Body, bodyB: Jolt.Body) { + bodyA: Jolt.Body, bodyB: Jolt.Body): Jolt.SliderConstraint { const sliderConstraintSettings = new JOLT.SliderConstraintSettings(); @@ -292,6 +322,8 @@ class PhysicsSystem extends WorldSystem { this._constraints.push(constraint); this._joltPhysSystem.AddConstraint(constraint); + + return JOLT.castObject(constraint, JOLT.SliderConstraint); } /** @@ -300,8 +332,14 @@ class PhysicsSystem extends WorldSystem { * @param parser MirabufParser containing properly parsed RigidNodes * @returns Mapping of Jolt BodyIDs */ - public CreateBodiesFromParser(parser: MirabufParser): Map { + public CreateBodiesFromParser(parser: MirabufParser, layerReserve?: LayerReserve): Map { const rnToBodies = new Map(); + + if ((parser.assembly.dynamic && !layerReserve) || layerReserve?.isReleased) { + throw new Error('No layer reserve for dynamic assembly'); + } + + const reservedLayer: number | undefined = layerReserve?.layer; filterNonPhysicsNodes(parser.rigidNodes, parser.assembly).forEach(rn => { @@ -314,6 +352,10 @@ class PhysicsSystem extends WorldSystem { const minBounds = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0); const maxBounds = new JOLT.Vec3(-1000000.0, -1000000.0, -1000000.0); + const rnLayer: number = reservedLayer + ? reservedLayer + : (rn.name.endsWith(GAMEPIECE_SUFFIX) ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD); + rn.parts.forEach(partId => { const partInstance = parser.assembly.data!.parts!.partInstances![partId]!; if (partInstance.skipCollider == null || partInstance == undefined || partInstance.skipCollider == false) { @@ -364,15 +406,16 @@ class PhysicsSystem extends WorldSystem { const shape = shapeResult.Get(); - if (rn.isDynamic) + if (rn.isDynamic) { shape.GetMassProperties().mMass = totalMass == 0.0 ? 1 : totalMass; + } const bodySettings = new JOLT.BodyCreationSettings( shape, new JOLT.Vec3(0.0, 0.0, 0.0), new JOLT.Quat(0, 0, 0, 1), rn.isDynamic ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, - rn.isDynamic ? LAYER_MOVING : LAYER_NOT_MOVING + rnLayer ); const body = this._joltBodyInterface.CreateBody(bodySettings); this._joltBodyInterface.AddBody(body.GetID(), JOLT.EActivation_Activate); @@ -380,7 +423,7 @@ class PhysicsSystem extends WorldSystem { // Little testing components body.SetRestitution(0.4); - const angVelocity = new JOLT.Vec3(0, 0, 2); + const angVelocity = new JOLT.Vec3(0, 3, 0); body.SetAngularVelocity(angVelocity); JOLT.destroy(angVelocity); } @@ -493,19 +536,66 @@ class PhysicsSystem extends WorldSystem { } } +export class LayerReserve { + private _layer: number; + private _isReleased: boolean; + + public get layer() { return this._layer; } + public get isReleased() { return this._isReleased; } + + public constructor() { + this._layer = RobotLayers.pop()!; + this._isReleased = false; + } + + public Release() { + if (!this._isReleased) { + RobotLayers.push(this._layer); + this._isReleased = true; + } + } +} + +/** + * Initialize collision groups and filtering for Jolt. + * + * @param settings Jolt object used for applying filters. + */ function SetupCollisionFiltering(settings: Jolt.JoltSettings) { const objectFilter = new JOLT.ObjectLayerPairFilterTable(COUNT_OBJECT_LAYERS); - objectFilter.EnableCollision(LAYER_NOT_MOVING, LAYER_MOVING); - // TODO: Collision between dynamic objects temporarily disabled. - // objectFilter.EnableCollision(LAYER_MOVING, LAYER_MOVING); + + // Enable Field layer collisions + objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, LAYER_GENERAL_DYNAMIC); + objectFilter.EnableCollision(LAYER_FIELD, LAYER_GENERAL_DYNAMIC); + for (let i = 0; i < RobotLayers.length; i++) { + objectFilter.EnableCollision(LAYER_FIELD, RobotLayers[i]); + objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, RobotLayers[i]); + } + + // Enable Collisions between other robots + for (let i = 0; i < RobotLayers.length - 1; i++) { + for (let j = i + 1; j < RobotLayers.length; j++) { + objectFilter.EnableCollision(RobotLayers[i], RobotLayers[j]); + } + } - const BP_LAYER_NOT_MOVING = new JOLT.BroadPhaseLayer(LAYER_NOT_MOVING); - const BP_LAYER_MOVING = new JOLT.BroadPhaseLayer(LAYER_MOVING); - const COUNT_BROAD_PHASE_LAYERS = 2; + const BP_LAYER_FIELD = new JOLT.BroadPhaseLayer(LAYER_FIELD); + const BP_LAYER_GENERAL_DYNAMIC = new JOLT.BroadPhaseLayer(LAYER_GENERAL_DYNAMIC); + + const bpRobotLayers = new Array(RobotLayers.length); + for (let i = 0; i < bpRobotLayers.length; i++) { + bpRobotLayers[i] = new JOLT.BroadPhaseLayer(RobotLayers[i]); + } + + const COUNT_BROAD_PHASE_LAYERS = 2 + RobotLayers.length; const bpInterface = new JOLT.BroadPhaseLayerInterfaceTable(COUNT_OBJECT_LAYERS, COUNT_BROAD_PHASE_LAYERS); - bpInterface.MapObjectToBroadPhaseLayer(LAYER_NOT_MOVING, BP_LAYER_NOT_MOVING); - bpInterface.MapObjectToBroadPhaseLayer(LAYER_MOVING, BP_LAYER_MOVING); + + bpInterface.MapObjectToBroadPhaseLayer(LAYER_FIELD, BP_LAYER_FIELD); + bpInterface.MapObjectToBroadPhaseLayer(LAYER_GENERAL_DYNAMIC, BP_LAYER_GENERAL_DYNAMIC); + for (let i = 0; i < bpRobotLayers.length; i++) { + bpInterface.MapObjectToBroadPhaseLayer(RobotLayers[i], bpRobotLayers[i]); + } settings.mObjectLayerPairFilter = objectFilter; settings.mBroadPhaseLayerInterface = bpInterface; diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index 2665ae56a5..0efeb05f34 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe, assert } from 'vitest'; -import PhysicsSystem from '../systems/physics/PhysicsSystem'; +import PhysicsSystem, { LayerReserve } from '../systems/physics/PhysicsSystem'; import { LoadMirabufLocal } from '@/mirabuf/MirabufLoader'; import MirabufParser from '@/mirabuf/MirabufParser'; @@ -64,12 +64,12 @@ describe('Physics Sansity Checks', () => { }); }); -describe('Mirabuf Body Loading', () => { +describe('Mirabuf Physics Loading', () => { test('Body Loading (Dozer)', () => { const assembly = LoadMirabufLocal('./public/test_mira/Dozer_v2.mira'); const parser = new MirabufParser(assembly); const physSystem = new PhysicsSystem(); - const mapping = physSystem.CreateBodiesFromParser(parser); + const mapping = physSystem.CreateBodiesFromParser(parser, new LayerReserve()); expect(mapping.size).toBe(7); }); @@ -78,7 +78,7 @@ describe('Mirabuf Body Loading', () => { const assembly = LoadMirabufLocal('./public/test_mira/Team_2471_(2018)_v7.mira'); const parser = new MirabufParser(assembly); const physSystem = new PhysicsSystem(); - const mapping = physSystem.CreateBodiesFromParser(parser); + const mapping = physSystem.CreateBodiesFromParser(parser, new LayerReserve()); expect(mapping.size).toBe(10); });