diff --git a/.github/workflows/ESLintFormat.yml b/.github/workflows/FissionESLintFormat.yml similarity index 92% rename from .github/workflows/ESLintFormat.yml rename to .github/workflows/FissionESLintFormat.yml index 5cae0c1e66..38e3c55c2d 100644 --- a/.github/workflows/ESLintFormat.yml +++ b/.github/workflows/FissionESLintFormat.yml @@ -1,4 +1,4 @@ -name: Format Validation (ESLint) +name: Fission on: workflow_dispatch: {} @@ -9,7 +9,7 @@ on: jobs: runFormatValidationScript: - name: Run ESLint Format Validation + name: ESLint Format Validation runs-on: ubuntu-latest steps: - name: Checkout Code diff --git a/.github/workflows/FissionPackage.yml b/.github/workflows/FissionPackage.yml new file mode 100644 index 0000000000..2272d8fb21 --- /dev/null +++ b/.github/workflows/FissionPackage.yml @@ -0,0 +1,47 @@ +name: Fission + +on: + workflow_dispatch: {} + push: + branches: [ master ] + +jobs: + runUnitTests: + name: Package + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: JavaScript Setup + uses: actions/setup-node@v2 + with: + node-version: 20 + + - name: Get date + id: date # this is used on variable path + run: | + echo "timestamp=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_OUTPUT + + - name: Install Dependencies + run: | + cd fission + npm install + + - name: Get package info + id: info + uses: codex-team/action-nodejs-package-info@v1.1 + with: + path: fission/ + + - name: Build + id: build + run: | + cd fission + npm run build + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + id: upload-artifact + with: + name: "${{ steps.info.outputs.name }}@${{ steps.info.outputs.version }}[${{ steps.date.outputs.timestamp }}]" + path: fission/dist/ \ No newline at end of file diff --git a/.github/workflows/FissionUnitTest.yml b/.github/workflows/FissionUnitTest.yml new file mode 100644 index 0000000000..6e7a0b61db --- /dev/null +++ b/.github/workflows/FissionUnitTest.yml @@ -0,0 +1,38 @@ +name: Fission + +on: + workflow_dispatch: {} + push: + branches: [ master, dev ] + pull_request: + branches: [ master, dev ] + +jobs: + runUnitTests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: JavaScript Setup + uses: actions/setup-node@v2 + with: + node-version: 20 + - name: Install Dependencies + run: | + cd fission + npm install + - name: Unit Tests + id: unit-tests + run: | + cd fission + npm run test + continue-on-error: true + - name: Check Success + run: | + if [ ${{ steps.unit-tests.outcome }} == "success" ]; then + echo "Format Validation Passed" + else + echo "Format Validation Failed" + exit 1 + fi \ No newline at end of file diff --git a/fission/package.json b/fission/package.json index 66e49aa676..38bbaad6af 100644 --- a/fission/package.json +++ b/fission/package.json @@ -1,7 +1,7 @@ { - "name": "vite-proj", - "private": true, - "version": "0.0.0", + "name": "synthesis-fission", + "private": false, + "version": "0.0.1", "type": "module", "scripts": { "dev": "vite", diff --git a/fission/src/App.css b/fission/src/App.css deleted file mode 100644 index 74b5e05345..0000000000 --- a/fission/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/fission/src/App.tsx b/fission/src/App.tsx deleted file mode 100644 index 8df75375fe..0000000000 --- a/fission/src/App.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import './App.css'; -import MyThree from './samples/JoltExample.tsx'; - -function App() { - - return ; -} - -export default App; diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 157f4a9871..96522143af 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -1,5 +1,4 @@ import Scene from './components/Scene.tsx'; -import GetSceneRenderer from './systems/scene/SceneRenderer.ts'; import MirabufSceneObject from './mirabuf/MirabufSceneObject.ts'; import { LoadMirabufRemote } from './mirabuf/MirabufLoader.ts'; import { mirabuf } from './proto/mirabuf'; @@ -54,8 +53,10 @@ import ZoneConfigPanel from "./panels/configuring/scoring/ZoneConfigPanel" import ScoreboardPanel from "./panels/information/ScoreboardPanel" 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/Dozer_v2.mira'; function Synthesis() { const { openModal, closeModal, getActiveModalElement } = @@ -145,6 +146,8 @@ function Synthesis() { useEffect(() => { + World.InitWorld(); + let mira_path = DEFAULT_MIRA_PATH; const urlParams = new URLSearchParams(document.location.search); @@ -161,18 +164,20 @@ function Synthesis() { _ => LoadMirabufRemote(DEFAULT_MIRA_PATH) ).catch(console.error); - if (!miraAssembly || !(miraAssembly instanceof mirabuf.Assembly)) { - return; - } - - const parser = new MirabufParser(miraAssembly); - if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { - console.error(`Assembly Parser produced significant errors for '${miraAssembly.info!.name!}'`); - return; - } - - const mirabufSceneObject = new MirabufSceneObject(new MirabufInstance(parser)); - GetSceneRenderer().RegisterSceneObject(mirabufSceneObject); + await (async () => { + if (!miraAssembly || !(miraAssembly instanceof mirabuf.Assembly)) { + return; + } + + const parser = new MirabufParser(miraAssembly); + if (parser.maxErrorSeverity >= ParseErrorSeverity.Unimportable) { + console.error(`Assembly Parser produced significant errors for '${miraAssembly.info!.name!}'`); + return; + } + + const mirabufSceneObject = new MirabufSceneObject(new MirabufInstance(parser)); + World.SceneRenderer.RegisterSceneObject(mirabufSceneObject); + })(); }; setup(); @@ -180,14 +185,15 @@ function Synthesis() { const mainLoop = () => { mainLoopHandle = requestAnimationFrame(mainLoop); - GetSceneRenderer().Update(); + World.UpdateWorld(); }; mainLoop(); // Cleanup return () => { // TODO: Teardown literally everything cancelAnimationFrame(mainLoopHandle); - GetSceneRenderer().RemoveAllSceneObjects(); + World.DestroyWorld(); + // World.SceneRenderer.RemoveAllSceneObjects(); }; }, []); diff --git a/fission/src/components/Scene.tsx b/fission/src/components/Scene.tsx index 6d83f803e1..2ee5a71b33 100644 --- a/fission/src/components/Scene.tsx +++ b/fission/src/components/Scene.tsx @@ -1,9 +1,9 @@ import './Scene.css'; import { useEffect, useRef } from "react"; -import GetSceneRenderer from "../systems/scene/SceneRenderer"; import Stats from 'stats.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import SceneObject from "../systems/scene/SceneObject"; +import World from '@/systems/World'; let stats: Stats | null; @@ -17,10 +17,12 @@ function Scene({ useStats }: SceneProps) { const refContainer = useRef(null); useEffect(() => { + World.InitWorld(); + if (refContainer.current) { console.debug('Adding ThreeJs to DOM'); - const sr = GetSceneRenderer(); + const sr = World.SceneRenderer; sr.renderer.domElement.style.width = '100%'; sr.renderer.domElement.style.height = '100%'; diff --git a/fission/src/main.tsx b/fission/src/main.tsx index 9c32b6357b..e6f566189f 100644 --- a/fission/src/main.tsx +++ b/fission/src/main.tsx @@ -1,4 +1,3 @@ -import React from "react" import ReactDOM from "react-dom/client" import { Theme, ThemeProvider } from "./ThemeContext" import Synthesis from "./Synthesis" diff --git a/fission/src/mirabuf/MirabufInstance.ts b/fission/src/mirabuf/MirabufInstance.ts index a964336925..495f89c11e 100644 --- a/fission/src/mirabuf/MirabufInstance.ts +++ b/fission/src/mirabuf/MirabufInstance.ts @@ -1,9 +1,13 @@ import * as THREE from 'three'; import { mirabuf } from "../proto/mirabuf" import MirabufParser, { ParseErrorSeverity } from './MirabufParser.ts'; -import { MirabufTransform_ThreeMatrix4 } from '../util/TypeConversions.ts'; +import World from '@/systems/World.ts'; -const NORMAL_MATERIALS = false; +export enum MaterialStyle { + Regular = 0, + Normals = 1, + Toon = 2 +} export const matToString = (mat: THREE.Matrix4) => { const arr = mat.toArray(); @@ -78,7 +82,7 @@ class MirabufInstance { public get materials() { return this._materials; } public get meshes() { return this._meshes; } - public constructor(parser: MirabufParser) { + public constructor(parser: MirabufParser, materialStyle?: MaterialStyle) { if (parser.errors.some(x => x[0] >= ParseErrorSeverity.Unimportable)) { throw new Error('Parser has significant errors...'); } @@ -87,14 +91,14 @@ class MirabufInstance { this._materials = new Map(); this._meshes = new Map(); - this.LoadMaterials(); + this.LoadMaterials(materialStyle ?? MaterialStyle.Regular); this.CreateMeshes(); } /** * Parses all mirabuf appearances into ThreeJs materials. */ - private LoadMaterials() { + private LoadMaterials(materialStyle: MaterialStyle) { Object.entries(this._mirabufParser.assembly.data!.materials!.appearances!).forEach(([appearanceId, appearance]) => { let hex = 0xe32b50; if (appearance.albedo) { @@ -103,12 +107,22 @@ class MirabufInstance { hex = A << 24 | R << 16 | G << 8 | B; } - this._materials.set( - appearanceId, - new THREE.MeshPhongMaterial({ + let material: THREE.Material; + if (materialStyle == MaterialStyle.Regular) { + material = new THREE.MeshPhongMaterial({ color: hex, shininess: 0.0, - }) + }); + } else if (materialStyle == MaterialStyle.Normals) { + material = new THREE.MeshNormalMaterial(); + } else if (materialStyle == MaterialStyle.Toon) { + material = World.SceneRenderer.CreateToonMaterial(hex, 5); + console.debug('Toon Material'); + } + + this._materials.set( + appearanceId, + material! ); }); } @@ -135,14 +149,14 @@ class MirabufInstance { transformGeometry(geometry, mesh.mesh!); const appearanceOverride = body.appearanceOverride; - let material: THREE.Material = + const material: THREE.Material = appearanceOverride && this._materials.has(appearanceOverride) ? this._materials.get(appearanceOverride)! : fillerMaterials[nextFillerMaterial++ % fillerMaterials.length]; - if (NORMAL_MATERIALS) { - material = new THREE.MeshNormalMaterial(); - } + // if (NORMAL_MATERIALS) { + // material = new THREE.MeshNormalMaterial(); + // } const threeMesh = new THREE.Mesh( geometry, material ); threeMesh.receiveShadow = true; diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index f9a8edc08a..e13010d2b2 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -32,20 +32,16 @@ class MirabufParser { protected _rigidNodes: Array = []; private _globalTransforms: Map; - public get errors() { return new Array(...this._errors); } + private _groundedNode: RigidNode | undefined; + public get errors() { return new Array(...this._errors); } public get maxErrorSeverity() { return Math.max(...this._errors.map(x => x[0])); } - public get assembly() { return this._assembly; } - public get partTreeValues() { return this._partTreeValues; } - public get designHierarchyRoot() { return this._designHierarchyRoot; } - public get partToNodeMap() { return this._partToNodeMap; } - public get globalTransforms() { return this._globalTransforms; } - + public get groundedNode() { return this._groundedNode ? new RigidNodeReadOnly(this._groundedNode) : undefined; } public get rigidNodes(): Array { return this._rigidNodes.map(x => new RigidNodeReadOnly(x)); } @@ -83,6 +79,8 @@ class MirabufParser { } }); + // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); + // Fields Only: Assign Game Piece rigid nodes if (!assembly.dynamic) { // Collect all definitions labelled as gamepieces (dynamic = true) @@ -110,9 +108,12 @@ class MirabufParser { // 2: Grounded joint const gInst = assembly.data!.joints!.jointInstances![GROUNDED_JOINT_ID]; const gNode = this.NewRigidNode(); + this.MovePartToRigidNode(gInst.parts!.nodes!.at(0)!.value!, gNode); - traverseTree(gInst.parts!.nodes!, x => (!this._partToNodeMap.has) && this.MovePartToRigidNode(x.value!, gNode)); + // traverseTree(gInst.parts!.nodes!, x => (!this._partToNodeMap.has(x.value!)) && this.MovePartToRigidNode(x.value!, gNode)); + // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); + // 3: Traverse and round up const traverseNodeRoundup = (node: mirabuf.INode, parentNode: RigidNode) => { const currentNode = that._partToNodeMap.get(node.value!); @@ -126,6 +127,8 @@ class MirabufParser { } this._designHierarchyRoot.children?.forEach(x => traverseNodeRoundup(x, gNode)); + // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); + // 4: Bandage via rigidgroups assembly.data!.joints!.rigidGroups!.forEach(rg => { let rn: RigidNode | null = null; @@ -139,8 +142,16 @@ class MirabufParser { }); }); + // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); + // 5. Remove Empty RNs this._rigidNodes = this._rigidNodes.filter(x => x.parts.size > 0); + + // 6. If field, find grounded node and set isDynamic to false. Also just find grounded node again + this._groundedNode = this.partToNodeMap.get(gInst.parts!.nodes!.at(0)!.value!); + if (!assembly.dynamic && this._groundedNode) { + this._groundedNode.isDynamic = false; + } } private NewRigidNode(suffix?: string): RigidNode { @@ -316,9 +327,11 @@ class MirabufParser { class RigidNode { public name: string; public parts: Set = new Set(); + public isDynamic: boolean; - public constructor(name: string) { + public constructor(name: string, isDynamic?: boolean) { this.name = name; + this.isDynamic = isDynamic ?? true; } } @@ -333,6 +346,10 @@ export class RigidNodeReadOnly { return this._original.parts; } + public get isDynamic(): boolean { + return this._original.isDynamic; + } + public constructor(original: RigidNode) { this._original = original; } diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index de4ed2d771..2a2d9b00cb 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -1,28 +1,116 @@ import { mirabuf } from "@/proto/mirabuf"; import SceneObject from "../systems/scene/SceneObject"; -import GetSceneRenderer from "../systems/scene/SceneRenderer"; import MirabufInstance from "./MirabufInstance"; import { LoadMirabufRemote } from "./MirabufLoader"; import MirabufParser, { ParseErrorSeverity } from "./MirabufParser"; +import World from "@/systems/World"; +import Jolt from '@barclah/jolt-physics'; +import { JoltMat44_ThreeMatrix4 } from "@/util/TypeConversions"; +import * as THREE from 'three'; +import JOLT from "@/util/loading/JoltSyncLoader"; + +const DEBUG_BODIES = true; + +interface RnDebugMeshes { + colliderMesh: THREE.Mesh; + comMesh: THREE.Mesh; +} class MirabufSceneObject extends SceneObject { private _mirabufInstance: MirabufInstance; + private _bodies: Map; + private _debugBodies: Map | null; public constructor(mirabufInstance: MirabufInstance) { super(); this._mirabufInstance = mirabufInstance; + this._bodies = World.PhysicsSystem.CreateBodiesFromParser(mirabufInstance.parser); + this._debugBodies = null; } public Setup(): void { - this._mirabufInstance.AddToScene(GetSceneRenderer().scene); + this._mirabufInstance.AddToScene(World.SceneRenderer.scene); + + if (DEBUG_BODIES) { + this._debugBodies = new Map(); + this._bodies.forEach((bodyId, rnName) => { + + const body = World.PhysicsSystem.GetBody(bodyId); + + const colliderMesh = this.CreateMeshForShape(body.GetShape()); + const comMesh = World.SceneRenderer.CreateSphere(0.05); + World.SceneRenderer.scene.add(colliderMesh); + World.SceneRenderer.scene.add(comMesh); + (comMesh.material as THREE.Material).depthTest = false; + this._debugBodies!.set(rnName, { colliderMesh: colliderMesh, comMesh: comMesh }); + }); + } } - public Update(): void { } + public Update(): void { + this._mirabufInstance.parser.rigidNodes.forEach(rn => { + const body = World.PhysicsSystem.GetBody(this._bodies.get(rn.name)!); + const transform = JoltMat44_ThreeMatrix4(body.GetWorldTransform()); + rn.parts.forEach(part => { + const partTransform = this._mirabufInstance.parser.globalTransforms.get(part)!.clone().premultiply(transform); + this._mirabufInstance.meshes.get(part)!.forEach(mesh => { + mesh.position.setFromMatrixPosition(partTransform); + mesh.rotation.setFromRotationMatrix(partTransform); + }); + }); + + if (isNaN(body.GetPosition().GetX())) { + const vel = body.GetLinearVelocity(); + const pos = body.GetPosition(); + console.debug(`Invalid Position.\nPosition => ${pos.GetX()}, ${pos.GetY()}, ${pos.GetZ()}\nVelocity => ${vel.GetX()}, ${vel.GetY()}, ${vel.GetZ()}`); + } + // console.debug(`POSITION: ${body.GetPosition().GetX()}, ${body.GetPosition().GetY()}, ${body.GetPosition().GetZ()}`) + + if (this._debugBodies) { + const { colliderMesh, comMesh } = this._debugBodies.get(rn.name)!; + colliderMesh.position.setFromMatrixPosition(transform); + colliderMesh.rotation.setFromRotationMatrix(transform); + + const comTransform = JoltMat44_ThreeMatrix4(body.GetCenterOfMassTransform()); + comMesh.position.setFromMatrixPosition(comTransform); + comMesh.rotation.setFromRotationMatrix(comTransform); + } + }); + } public Dispose(): void { - this._mirabufInstance.Dispose(GetSceneRenderer().scene); + World.PhysicsSystem.DestroyBodyIds(...this._bodies.values()); + this._mirabufInstance.Dispose(World.SceneRenderer.scene); + this._debugBodies?.forEach(x => { + World.SceneRenderer.scene.remove(x.colliderMesh, x.comMesh); + x.colliderMesh.geometry.dispose(); + x.comMesh.geometry.dispose(); + (x.colliderMesh.material as THREE.Material).dispose(); + (x.comMesh.material as THREE.Material).dispose(); + }); + this._debugBodies?.clear(); + } + + private CreateMeshForShape(shape: Jolt.Shape): THREE.Mesh { + const scale = new JOLT.Vec3(1, 1, 1); + const triangleContext = new JOLT.ShapeGetTriangles(shape, JOLT.AABox.prototype.sBiggest(), shape.GetCenterOfMass(), JOLT.Quat.prototype.sIdentity(), scale); + JOLT.destroy(scale); + + const vertices = new Float32Array(JOLT.HEAP32.buffer, triangleContext.GetVerticesData(), triangleContext.GetVerticesSize() / Float32Array.BYTES_PER_ELEMENT); + const buffer = new THREE.BufferAttribute(vertices, 3).clone(); + JOLT.destroy(triangleContext); + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', buffer); + geometry.computeVertexNormals(); + + const material = new THREE.MeshStandardMaterial({ color: 0x33ff33, wireframe: true }) + const mesh = new THREE.Mesh(geometry, material); + mesh.castShadow = true; + + return mesh; } } diff --git a/fission/src/modals/spawning/AddRobotModal.tsx b/fission/src/modals/spawning/AddRobotModal.tsx index 622ef65c61..0da3a9c8ff 100644 --- a/fission/src/modals/spawning/AddRobotModal.tsx +++ b/fission/src/modals/spawning/AddRobotModal.tsx @@ -4,7 +4,7 @@ import { FaPlus } from "react-icons/fa6" import Dropdown from "../../components/Dropdown" import { useTooltipControlContext } from "@/TooltipContext" import { CreateMirabufFromUrl } from "@/mirabuf/MirabufSceneObject" -import GetSceneRenderer from "@/systems/scene/SceneRenderer" +import World from "@/systems/World" const RobotsModal: React.FC = ({ modalId }) => { // update tooltip based on type of drivetrain, receive message from Synthesis @@ -27,7 +27,7 @@ const RobotsModal: React.FC = ({ modalId }) => { if (selectedRobot) { CreateMirabufFromUrl(`test_mira/${selectedRobot}`).then(x => { if (x) { - GetSceneRenderer().RegisterSceneObject(x); + World.SceneRenderer.RegisterSceneObject(x); } }); } diff --git a/fission/src/modals/spawning/ManageAssembliesModal.tsx b/fission/src/modals/spawning/ManageAssembliesModal.tsx index 82c4d658b5..02e492e0d9 100644 --- a/fission/src/modals/spawning/ManageAssembliesModal.tsx +++ b/fission/src/modals/spawning/ManageAssembliesModal.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { RefObject, useEffect, useRef } from "react" import Modal, { ModalPropsImpl } from "../../components/Modal" import { FaPlus } from "react-icons/fa6" import ScrollView from "@/components/ScrollView" @@ -7,6 +7,20 @@ const ManageAssembliesModal: React.FC = ({ modalId }) => { // update tooltip based on type of drivetrain, receive message from Synthesis // const { showTooltip } = useTooltipControlContext() + const refs: Array> = new Array(2); + for (let i = 0; i < refs.length; i++) { + // eslint-disable-next-line react-hooks/rules-of-hooks + refs[i] = useRef(null); + } + + useEffect(() => { + refs.forEach((x, i) => { + if (x.current) { + x.current.innerHTML = `Item ${i}`; + } + }); + }); + return ( = ({ modalId }) => { } } > - -

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

-

Hello

+ {refs.map(x =>
)} }>
diff --git a/fission/src/panels/information/ScoreboardPanel.tsx b/fission/src/panels/information/ScoreboardPanel.tsx index 8dd07f1ec1..7aa21b3481 100644 --- a/fission/src/panels/information/ScoreboardPanel.tsx +++ b/fission/src/panels/information/ScoreboardPanel.tsx @@ -21,7 +21,7 @@ const ScoreboardPanel: React.FC = ({ panelId }) => { ) useEffect(() => { - const interval: NodeJS.Timer = setInterval(() => { + const interval: NodeJS.Timeout = setInterval(() => { const elapsed = Math.round((Date.now() - startTime) / 1_000) if (initialTime > 0) { if (elapsed <= initialTime) setTime(initialTime - elapsed) diff --git a/fission/src/samples/JoltExample.tsx b/fission/src/samples/JoltExample.tsx index 91aea2f9dd..130f4562e2 100644 --- a/fission/src/samples/JoltExample.tsx +++ b/fission/src/samples/JoltExample.tsx @@ -12,7 +12,7 @@ import Jolt from '@barclah/jolt-physics'; import { mirabuf } from "../proto/mirabuf" import { LoadMirabufRemote } from '../mirabuf/MirabufLoader.ts'; import { JoltVec3_ThreeVector3, JoltQuat_ThreeQuaternion } from '../util/TypeConversions.ts'; -import { COUNT_OBJECT_LAYERS, LAYER_MOVING, LAYER_NOT_MOVING, addToScene, removeFromScene } from '../util/threejs/MeshCreation.ts'; +import { COUNT_OBJECT_LAYERS, LAYER_MOVING, LAYER_NOT_MOVING } from '../util/threejs/MeshCreation.ts'; import MirabufInstance from '../mirabuf/MirabufInstance.ts'; import MirabufParser, { ParseErrorSeverity } from '../mirabuf/MirabufParser.ts'; @@ -26,8 +26,8 @@ let camera: THREE.PerspectiveCamera; let scene: THREE.Scene; let joltInterface: Jolt.JoltInterface; -let physicsSystem: Jolt.PhysicsSystem; -let bodyInterface: Jolt.BodyInterface; +// let physicsSystem: Jolt.PhysicsSystem; +// let bodyInterface: Jolt.BodyInterface; const dynamicObjects: THREE.Mesh[] = []; @@ -63,8 +63,8 @@ function initPhysics() { joltInterface = new JOLT.JoltInterface(settings); JOLT.destroy(settings); - physicsSystem = joltInterface.GetPhysicsSystem(); - bodyInterface = physicsSystem.GetBodyInterface(); + // physicsSystem = joltInterface.GetPhysicsSystem(); + // bodyInterface = physicsSystem.GetBodyInterface(); } function initGraphics() { @@ -115,20 +115,6 @@ function initGraphics() { // TODO: Add resize event } -function createFloor(size = 50) { - const shape = new JOLT.BoxShape(new JOLT.Vec3(size, 0.5, size), 0.05, undefined); - const position = new JOLT.Vec3(0, -0.5, 0); - const rotation = new JOLT.Quat(0, 0, 0, 1); - const creationSettings = new JOLT.BodyCreationSettings(shape, position, rotation, JOLT.EMotionType_Static, LAYER_NOT_MOVING) - const body = bodyInterface.CreateBody(creationSettings); - JOLT.destroy(position); - JOLT.destroy(rotation); - JOLT.destroy(creationSettings); - addToScene(scene, body, new THREE.Color(0xc7c7c7), bodyInterface, dynamicObjects); - - return body; -} - function updatePhysics(deltaTime: number) { // If below 55hz run 2 steps. Otherwise things run very slow. const numSteps = deltaTime > 1.0 / 55.0 ? 2 : 1; @@ -169,7 +155,7 @@ function MyThree() { const urlParams = new URLSearchParams(document.location.search); let mira_path = MIRA_FILE; - urlParams.forEach((v, k, p) => console.debug(`${k}: ${v}`)); + urlParams.forEach((v, k) => console.debug(`${k}: ${v}`)); if (urlParams.has("mira")) { mira_path = `test_mira/${urlParams.get("mira")!}`; diff --git a/fission/src/samples/ThreeExample.tsx b/fission/src/samples/ThreeExample.tsx deleted file mode 100644 index d77663be5b..0000000000 --- a/fission/src/samples/ThreeExample.tsx +++ /dev/null @@ -1,281 +0,0 @@ -/** - * This Example will be used not for the physics, but the rendering setup for ThreeJS. - */ - -// SOURCE: https://dev.to/omher/how-to-start-using-react-and-threejs-in-a-few-minutes-2h6g -import * as THREE from "three"; - -import { useEffect, useRef, useState } from "react"; -// import { wasmWrapper } from '../WasmWrapper.mjs'; -import React from "react"; -import * as AppTest from "../App.tsx"; -import DetailsPanel from "../components/Details.tsx"; - -var staticCube: THREE.Mesh; -var mainBar: THREE.Mesh; -var lightBar: THREE.Mesh; -var heavyBar: THREE.Mesh; - -var scene: THREE.Scene; - -var lastBallSpawned: number = Date.now(); -var lastFrameCheck: number = Date.now(); -var numFrames: number = 0; - -var balls: Array<[THREE.Mesh, number]> = new Array(); - -var materials = [ - new THREE.MeshPhongMaterial({ - color: 0xe32b50, - shininess: 0.5, - }), - new THREE.MeshPhongMaterial({ - color: 0x4ccf57, - shininess: 0.5, - }), - new THREE.MeshPhongMaterial({ - color: 0xcf4cca, - shininess: 0.5, - }), - new THREE.MeshPhongMaterial({ - color: 0x8c37db, - shininess: 0.5, - }), - new THREE.MeshPhongMaterial({ - color: 0xbddb37, - shininess: 0.5, - }) -]; - -var matIndex = 0; - -var physicsInit = false; - -var spawnBalls = true; - -function createBall( - radius: number, - position: THREE.Vector3, - velocity: THREE.Vector3, - restitution: number -) { - matIndex++; - var ballGeo = new THREE.SphereGeometry(radius); - var mesh = new THREE.Mesh(ballGeo, materials[matIndex % materials.length]); - mesh.receiveShadow = true; - mesh.castShadow = true; - scene.add(mesh); - var phys = PhysicsManager.getInstance().makeBall(position, radius); - phys.setLinvel(velocity, true); - phys.collider(0).setRestitution(restitution); - balls.push([mesh, phys.handle]); -} - -function createBox( - halfExtents: RAPIER.Vector3, - position: RAPIER.Vector3, - velocity: RAPIER.Vector3, - restitution: number -) { - matIndex++; - var ballGeo = new THREE.BoxGeometry(halfExtents.x * 2.0, halfExtents.y * 2.0, halfExtents.z * 2.0); - var mesh = new THREE.Mesh(ballGeo, materials[matIndex % materials.length]); - mesh.receiveShadow = true; - mesh.castShadow = true; - scene.add(mesh); - var phys = PhysicsManager.getInstance().makeBox(position, halfExtents); - phys.setLinvel(velocity, true); - phys.collider(0).setRestitution(restitution); - balls.push([mesh, phys.handle]); -} - -function makeStaticBox(position: THREE.Vector3, extents: THREE.Vector3) { - var groundGeo = new THREE.BoxGeometry(extents.x, extents.y, extents.z); - var groundMat = new THREE.MeshPhongMaterial({ - color: 0xeeeeee, - shininess: 0.1, - }); - var ground = new THREE.Mesh(groundGeo, groundMat); - ground.receiveShadow = true; - ground.position.set(position.x, position.y, position.z); - - scene.add(ground); -} - -function MyThree() { - - const [ballCount, setBallCount] = useState(0); - const [fps, setFps] = useState(0.0); - - const refContainer = useRef(null); - useEffect(() => { - - console.log('MyThree effect'); - - // === THREE.JS CODE START === - scene = new THREE.Scene(); - var camera = new THREE.PerspectiveCamera( - 75, - window.innerWidth / window.innerHeight, - 0.1, - 1000 - ); - var renderer = new THREE.WebGLRenderer(); - renderer.setSize(window.innerWidth, window.innerHeight); - renderer.shadowMap.enabled = true; - renderer.shadowMap.type = THREE.PCFSoftShadowMap; - renderer.setClearColor(0x121212); - - // document.body.appendChild( renderer.domElement ); - // use ref as a mount point of the Three.js scene instead of the document.body - if (refContainer.current) { - refContainer.current.innerHTML = ""; - refContainer.current.appendChild(renderer.domElement); - console.log("Added dom element"); - } - - // var geometry = new THREE.BoxGeometry(1.0, 1.0, 1.0); - // var material = new THREE.MeshPhongMaterial({ - // color: 0xe32b50, - // shininess: 0.1, - // }); - - var [X, Y] = [30.0, 30.0]; - - makeStaticBox(new THREE.Vector3(0.0, -0.25, 0.0), new THREE.Vector3(X, 0.5, Y)); - // makeStaticBox(new THREE.Vector3((-X / 2.0) + 0.25, -1.0, 0.0), new THREE.Vector3(0.5, 2.0, Y)); - // makeStaticBox(new THREE.Vector3((X / 2.0) - 0.25, -1.0, 0.0), new THREE.Vector3(0.5, 2.0, Y)); - // makeStaticBox(new THREE.Vector3(0.0, -1.0, (-Y / 2.0) + 0.25), new THREE.Vector3(X, 2.0, 0.5)); - // makeStaticBox(new THREE.Vector3(0.0, -1.0, (Y / 2.0) - 0.25), new THREE.Vector3(X, 2.0, 0.5)); - - var directionalLight = new THREE.DirectionalLight(0xffffff, 3.0); - directionalLight.position.set(-1.0, 3.0, 2.0); - directionalLight.castShadow = true; - - var ambientLight = new THREE.AmbientLight(0xffffff, 0.2); - scene.add(directionalLight, ambientLight); - - camera.position.set(6.0, 5.0, 6.0); - camera.rotateY(3.14159 * 0.25); - camera.rotateX(-0.5); - - var shadowMapSize = Math.min(1024, renderer.capabilities.maxTextureSize); - var shadowCamSize = 15; - - // console.log(`Cam Top: ${directionalLight.shadow.camera.top}`); - // console.log(`Cam Bottom: ${directionalLight.shadow.camera.bottom}`); - // console.log(`Cam Left: ${directionalLight.shadow.camera.left}`); - // console.log(`Cam Right: ${directionalLight.shadow.camera.right}`); - - directionalLight.shadow.camera.top = shadowCamSize; - directionalLight.shadow.camera.bottom = -shadowCamSize; - directionalLight.shadow.camera.left = -shadowCamSize; - directionalLight.shadow.camera.right = shadowCamSize; - directionalLight.shadow.mapSize = new THREE.Vector2(shadowMapSize, shadowMapSize); - directionalLight.shadow.blurSamples = 16; - directionalLight.shadow.normalBias = -0.05; - - // directionalLight.shadow.camera = new THREE.OrthographicCamera() - - // directionalLight.shadow.camera.position.copy(camera.position); - // directionalLight.shadow.camera.matrix.copy(camera.matrix); - - var animate = function () { - requestAnimationFrame(animate); - - // var pos: THREE.Vector3 = new THREE.Vector3(); - // var rot: THREE.Quaternion = new THREE.Quaternion(); - // var scale: THREE.Vector3 = new THREE.Vector3(); - - // ObjTransform.decompose(pos, rot, scale); - - // cube.position.set(pos.x, pos.y, pos.z); - // cube.rotation.setFromQuaternion(rot); - - renderer.render(scene, camera); - }; - animate(); - }, []); - - useEffect(() => { - var frameReq: number | undefined = undefined; - - async function physicsStuff() { - var update = function () { - - var delta = Date.now() - lastFrameCheck; - lastFrameCheck = Date.now(); - setFps(1000.0 / delta); - - frameReq = requestAnimationFrame(update); - - if (spawnBalls && Date.now() - lastBallSpawned > 10) { - lastBallSpawned = Date.now(); - - var randPos = { - x: Math.random() * 8.0 - 4.0, - y: 10.0, - z: Math.random() * 8.0 - 4.0 - } - - if (Math.random() > 0.3) { - createBall( - Math.random() * 0.3 + 0.1, - randPos, - new RAPIER.Vector3( - Math.random() * 0.5 - 0.25, - 0.0, - Math.random() * 0.5 - 0.25 - ), - Math.random() * 0.3 - ); - } else { - createBox( - { x: 0.25, y: 0.25, z: 0.25 }, - randPos, - { x: 0.0, y: 0.0, z: 0.0 }, - Math.random() * 0.3 - ); - } - setBallCount(balls.length); - - if (balls.length % 200 == 0) { - spawnBalls = false; - } - } - - PhysicsManager.getInstance().step(delta / 1000.0); - - for (var i = 0; i < balls.length; i++) { - var [mesh, handle] = balls[i]; - var body = PhysicsManager.getInstance().getBody(handle); - - (body) && Translations.loadMeshWithRigidbody(body, mesh); - } - }; - - frameReq = requestAnimationFrame(update); - } - - physicsStuff(); - - return () => { - if (frameReq) { - cancelAnimationFrame(frameReq); - console.log("Canceling animation"); - } - // wasmWrapper.coreDestroy(); - }; - }, []); - - console.log('MyThree Component'); - - return ( -
-
- < DetailsPanel ballCount={ballCount} fps={fps} toggleSpawning={() => { spawnBalls = !spawnBalls; }} /> -
- ); -} - -export default MyThree; diff --git a/fission/src/systems/World.ts b/fission/src/systems/World.ts new file mode 100644 index 0000000000..68a2c6c52f --- /dev/null +++ b/fission/src/systems/World.ts @@ -0,0 +1,47 @@ +import * as THREE from "three"; + +import PhysicsSystem from "./physics/PhysicsSystem"; +import SceneRenderer from "./scene/SceneRenderer"; + +class World { + + private static _isAlive: boolean = false; + private static _clock: THREE.Clock; + + private static _sceneRenderer: SceneRenderer; + private static _physicsSystem: PhysicsSystem; + + public static get isAlive() { return World._isAlive; } + + public static get SceneRenderer() { return World._sceneRenderer; } + public static get PhysicsSystem() { return World._physicsSystem; } + + public static InitWorld() { + if (World._isAlive) + return; + + World._clock = new THREE.Clock(); + World._isAlive = true; + + World._sceneRenderer = new SceneRenderer(); + World._physicsSystem = new PhysicsSystem(); + } + + public static DestroyWorld() { + if (!World._isAlive) + return; + + World._isAlive = false; + + World._physicsSystem.Destroy(); + World._sceneRenderer.Destroy(); + } + + public static UpdateWorld() { + const deltaT = World._clock.getDelta(); + this._physicsSystem.Update(deltaT); + this._sceneRenderer.Update(deltaT); + } +} + +export default World; \ No newline at end of file diff --git a/fission/src/systems/WorldSystem.ts b/fission/src/systems/WorldSystem.ts new file mode 100644 index 0000000000..f45891980c --- /dev/null +++ b/fission/src/systems/WorldSystem.ts @@ -0,0 +1,5 @@ +abstract class WorldSystem { + public abstract Update(deltaT: number): void; + public abstract Destroy(): void; +} +export default WorldSystem; \ No newline at end of file diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index fcfc8a8cf8..2cfc967a5a 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -1,33 +1,36 @@ -import { MirabufVector3_JoltVec3, MirabufVector3_ThreeVector3, ThreeVector3_JoltVec3, _JoltQuat } from "../../util/TypeConversions"; +import { MirabufFloatArr_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 from "../../mirabuf/MirabufParser"; +import MirabufParser, { RigidNodeReadOnly } from "../../mirabuf/MirabufParser"; +import WorldSystem from "../WorldSystem"; const LAYER_NOT_MOVING = 0; const LAYER_MOVING = 1; const COUNT_OBJECT_LAYERS = 2; -const STANDARD_TIME_STEP = 1.0 / 60.0; -const STANDARD_SUB_STEPS = 1; +const STANDARD_TIME_STEP = 1.0 / 120.0; +const STANDARD_SUB_STEPS = 3; /** * The PhysicsSystem handles all Jolt Phyiscs interactions within Synthesis. * This system can create physical representations of objects such as Robots, * Fields, and Game pieces, and simulate them. */ -export class PhysicsSystem { +class PhysicsSystem extends WorldSystem { private _joltInterface: Jolt.JoltInterface; private _joltPhysSystem: Jolt.PhysicsSystem; private _joltBodyInterface: Jolt.BodyInterface; - private _bodies: Array; + private _bodies: Array; /** * Creates a PhysicsSystem object. */ constructor() { + super(); + this._bodies = []; const joltSettings = new JOLT.JoltSettings(); @@ -38,6 +41,9 @@ export class PhysicsSystem { this._joltPhysSystem = this._joltInterface.GetPhysicsSystem(); this._joltBodyInterface = this._joltPhysSystem.GetBodyInterface(); + + const ground = this.CreateBox(new THREE.Vector3(5.0, 0.5, 5.0), undefined, new THREE.Vector3(0.0, -2.0, 0.0), undefined); + this._joltBodyInterface.AddBody(ground.GetID(), JOLT.EActivation_Activate); } /** @@ -66,7 +72,7 @@ export class PhysicsSystem { pos, rot, mass ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, - LAYER_NOT_MOVING + mass ? LAYER_MOVING : LAYER_NOT_MOVING ); if (mass) { creationSettings.mMassPropertiesOverride.mMass = mass; @@ -76,10 +82,19 @@ export class PhysicsSystem { JOLT.destroy(rot); JOLT.destroy(creationSettings); - this._bodies.push(body); + this._bodies.push(body.GetID()); return body; } + /** + * This creates a body in Jolt. Mostly used for Unit test validation. + * + * @param shape Shape to impart on the body. + * @param mass Mass of the body. + * @param position Position of the body. + * @param rotation Rotation of the body. + * @returns Resulting Body object. + */ public CreateBody( shape: Jolt.Shape, mass: number | undefined, @@ -102,11 +117,18 @@ export class PhysicsSystem { JOLT.destroy(rot); JOLT.destroy(creationSettings); - this._bodies.push(body); + this._bodies.push(body.GetID()); return body; } - public CreateConvexHull(points: Float32Array, density: number = 1.0) { + /** + * Utility function for creating convex hulls. Mostly used for Unit test validation. + * + * @param points Flat pack array of vector 3 components. + * @param density Density of the convex hull. + * @returns Resulting shape. + */ + public CreateConvexHull(points: Float32Array, density: number = 1.0): Jolt.ShapeResult { if (points.length % 3) { throw new Error(`Invalid size of points: ${points.length}`); } @@ -120,44 +142,195 @@ export class PhysicsSystem { return settings.Create(); } - public CreateBodiesFromParser(parser: MirabufParser): Map { - const rnToBodies = new Map(); + /** + * Creates a map, mapping the name of RigidNodes to Jolt BodyIDs + * + * @param parser MirabufParser containing properly parsed RigidNodes + * @returns Mapping of Jolt BodyIDs + */ + public CreateBodiesFromParser(parser: MirabufParser): Map { + const rnToBodies = new Map(); - parser.rigidNodes.forEach(rn => { + filterNonPhysicsNodes(parser.rigidNodes, parser.assembly).forEach(rn => { + + const compoundShapeSettings = new JOLT.StaticCompoundShapeSettings(); + let shapesAdded = 0; + + let totalMass = 0; + const comAccum = new mirabuf.Vector3(); + + const minBounds = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0); + const maxBounds = new JOLT.Vec3(-1000000.0, -1000000.0, -1000000.0); + rn.parts.forEach(partId => { const partInstance = parser.assembly.data!.parts!.partInstances![partId]!; - const partDefinition = parser.assembly.data!.parts!.partDefinitions![partInstance.partDefinitionReference!]; - + if (partInstance.skipCollider == null || partInstance == undefined || partInstance.skipCollider == false) { + const partDefinition = parser.assembly.data!.parts!.partDefinitions![partInstance.partDefinitionReference!]!; + + const partShapeResult = this.CreateShapeSettingsFromPart(partDefinition); + + if (partShapeResult) { + + const [shapeSettings, partMin, partMax] = partShapeResult; + + const transform = ThreeMatrix4_JoltMat44(parser.globalTransforms.get(partId)!); + const translation = transform.GetTranslation(); + const rotation = transform.GetQuaternion(); + compoundShapeSettings.AddShape( + translation, + rotation, + shapeSettings, + 0 + ); + shapesAdded++; + + this.UpdateMinMaxBounds(transform.Multiply3x3(partMin), minBounds, maxBounds); + this.UpdateMinMaxBounds(transform.Multiply3x3(partMax), minBounds, maxBounds); + + JOLT.destroy(partMin); + JOLT.destroy(partMax); + JOLT.destroy(transform); + + if (partDefinition.physicalData && partDefinition.physicalData.com && partDefinition.physicalData.mass) { + const mass = partDefinition.massOverride ? partDefinition.massOverride! : partDefinition.physicalData.mass!; + totalMass += mass; + comAccum.x += partDefinition.physicalData.com.x! * mass / 100.0; + comAccum.y += partDefinition.physicalData.com.y! * mass / 100.0; + comAccum.z += partDefinition.physicalData.com.z! * mass / 100.0; + } + } + } }); + + if (shapesAdded > 0) { + + const shapeResult = compoundShapeSettings.Create(); + + if (!shapeResult.IsValid || shapeResult.HasError()) { + console.error(`Failed to create shape for RigidNode ${rn.name}\n${shapeResult.GetError().c_str()}`); + } + + const shape = shapeResult.Get(); + + 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 + ); + const body = this._joltBodyInterface.CreateBody(bodySettings); + this._joltBodyInterface.AddBody(body.GetID(), JOLT.EActivation_Activate); + 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); + } + + // Cleanup + JOLT.destroy(compoundShapeSettings); }); return rnToBodies; } - private CreateColliderFromPart(partDefinition: mirabuf.PartDefinition): Jolt.ShapeResult { + /** + * Creates the Jolt ShapeSettings for a given part using the Part Definition of said part. + * + * @param partDefinition Definition of the part to create. + * @returns If successful, the created convex hull shape settings from the given Part Definition. + */ + private CreateShapeSettingsFromPart(partDefinition: mirabuf.IPartDefinition): [Jolt.ShapeSettings, Jolt.Vec3, Jolt.Vec3] | undefined | null { const settings = new JOLT.ConvexHullShapeSettings(); + + const min = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0); + const max = new JOLT.Vec3(-1000000.0, -1000000.0, -1000000.0); + const points = settings.mPoints; - partDefinition.bodies.forEach(body => { + partDefinition.bodies!.forEach(body => { if (body.triangleMesh && body.triangleMesh.mesh && body.triangleMesh.mesh.verts) { const vertArr = body.triangleMesh.mesh.verts; for (let i = 0; i < body.triangleMesh.mesh.verts.length; i += 3) { - points.push_back(new JOLT.Vec3(vertArr[i], vertArr[i + 1], vertArr[i + 2])); + const vert = MirabufFloatArr_JoltVec3(vertArr, i); + points.push_back(vert); + this.UpdateMinMaxBounds(vert, min, max); + JOLT.destroy(vert); } } }); - return settings.Create(); + if (points.size() < 4) { + JOLT.destroy(settings); + JOLT.destroy(min); + JOLT.destroy(max); + return; + } else { + return [settings, min, max]; + } } - public Step() { + /** + * Helper function to update min and max vector bounds. + * + * @param v Vector to add to min, max, bounds. + * @param min Minimum vector of the bounds. + * @param max Maximum vector of the bounds. + */ + private UpdateMinMaxBounds(v: Jolt.Vec3, min: Jolt.Vec3, max: Jolt.Vec3) { + if (v.GetX() < min.GetX()) + min.SetX(v.GetX()); + if (v.GetY() < min.GetY()) + min.SetY(v.GetY()); + if (v.GetZ() < min.GetZ()) + min.SetZ(v.GetZ()); + + if (v.GetX() > max.GetX()) + max.SetX(v.GetX()); + if (v.GetY() > max.GetY()) + max.SetY(v.GetY()); + if (v.GetZ() > max.GetZ()) + max.SetZ(v.GetZ()); + } + + /** + * Destroys bodies. + * + * @param bodies Bodies to destroy. + */ + public DestroyBodies(...bodies: Jolt.Body[]) { + bodies.forEach(x => { + this._joltBodyInterface.RemoveBody(x.GetID()); + this._joltBodyInterface.DestroyBody(x.GetID()); + }); + } + + public DestroyBodyIds(...bodies: Jolt.BodyID[]) { + bodies.forEach(x => { + this._joltBodyInterface.RemoveBody(x); + this._joltBodyInterface.DestroyBody(x); + }); + } + + public GetBody(bodyId: Jolt.BodyID) { + return this._joltPhysSystem.GetBodyLockInterface().TryGetBody(bodyId); + } + + public Update(_: number): void { this._joltInterface.Step(STANDARD_TIME_STEP, STANDARD_SUB_STEPS); } - public Destroy() { + public Destroy(): void { // Destroy Jolt Bodies. - this._bodies.forEach(x => JOLT.destroy(x)); + this.DestroyBodyIds(...this._bodies); this._bodies = []; + JOLT.destroy(this._joltBodyInterface); JOLT.destroy(this._joltInterface); } } @@ -165,7 +338,8 @@ export class PhysicsSystem { function SetupCollisionFiltering(settings: Jolt.JoltSettings) { const objectFilter = new JOLT.ObjectLayerPairFilterTable(COUNT_OBJECT_LAYERS); objectFilter.EnableCollision(LAYER_NOT_MOVING, LAYER_MOVING); - objectFilter.EnableCollision(LAYER_MOVING, LAYER_MOVING); + // TODO: Collision between dynamic objects temporarily disabled. + // objectFilter.EnableCollision(LAYER_MOVING, LAYER_MOVING); const BP_LAYER_NOT_MOVING = new JOLT.BroadPhaseLayer(LAYER_NOT_MOVING); const BP_LAYER_MOVING = new JOLT.BroadPhaseLayer(LAYER_MOVING); @@ -183,4 +357,19 @@ function SetupCollisionFiltering(settings: Jolt.JoltSettings) { settings.mObjectLayerPairFilter, COUNT_OBJECT_LAYERS ); -} \ No newline at end of file +} + +function filterNonPhysicsNodes(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembly): RigidNodeReadOnly[] { + return nodes.filter(x => { + for (const part of x.parts) { + const inst = mira.data!.parts!.partInstances![part]!; + const def = mira.data!.parts!.partDefinitions![inst.partDefinitionReference!]!; + if (def.bodies && def.bodies.length > 0) { + return true; + } + } + return false; + }); +} + +export default PhysicsSystem; \ No newline at end of file diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts index f68f800dca..13c203ebe9 100644 --- a/fission/src/systems/scene/SceneRenderer.ts +++ b/fission/src/systems/scene/SceneRenderer.ts @@ -1,16 +1,16 @@ import * as THREE from 'three'; import SceneObject from './SceneObject'; +import WorldSystem from '../WorldSystem'; const CLEAR_COLOR = 0x121212; let nextSceneObjectId = 1; -class SceneRenderer { +class SceneRenderer extends WorldSystem { private _mainCamera: THREE.PerspectiveCamera; private _scene: THREE.Scene; private _renderer: THREE.WebGLRenderer; - private _clock: THREE.Clock; private _sceneObjects: Map; @@ -27,8 +27,9 @@ class SceneRenderer { } public constructor() { + super(); + this._sceneObjects = new Map(); - this._clock = new THREE.Clock(); this._mainCamera = new THREE.PerspectiveCamera( 75, @@ -68,6 +69,12 @@ class SceneRenderer { const ambientLight = new THREE.AmbientLight(0xffffff, 0.1); this._scene.add(ambientLight); + + const ground = new THREE.Mesh(new THREE.BoxGeometry(10, 1, 10), this.CreateToonMaterial(CLEAR_COLOR)); + ground.position.set(0.0, -2.0, 0.0); + ground.receiveShadow = true; + ground.castShadow = true; + this._scene.add(ground); } public UpdateCanvasSize() { @@ -77,10 +84,7 @@ class SceneRenderer { this._mainCamera.updateProjectionMatrix(); } - public Update() { - // Prevents a problem when rendering at 30hz. Referred to as the spiral of death. - const deltaTime = this._clock.getDelta(); - + public Update(_: number): void { this._sceneObjects.forEach(obj => { obj.Update(); }); @@ -89,6 +93,10 @@ class SceneRenderer { this._renderer.render(this._scene, this._mainCamera); } + public Destroy(): void { + + } + public RegisterSceneObject(obj: T): number { const id = nextSceneObjectId++; obj.id = id; @@ -101,15 +109,26 @@ class SceneRenderer { this._sceneObjects.forEach(obj => obj.Dispose()); this._sceneObjects.clear(); } -} -let instance: SceneRenderer | null = null; + public CreateSphere(radius: number, material?: THREE.Material | undefined): THREE.Mesh { + const geo = new THREE.SphereGeometry(radius); + if (material) { + return new THREE.Mesh(geo, material); + } else { + return new THREE.Mesh(geo, this.CreateToonMaterial()); + } + } -function GetSceneRenderer() { - if (!instance) { - instance = new SceneRenderer(); + public CreateToonMaterial(color: THREE.ColorRepresentation = 0xff00aa, steps: number = 5): THREE.MeshToonMaterial { + const format = ( this._renderer.capabilities.isWebGL2 ) ? THREE.RedFormat : THREE.LuminanceFormat; + const colors = new Uint8Array(steps); + for ( let c = 0; c < colors.length; c ++ ) { + colors[c] = 128 + (c / colors.length) * 128; + } + const gradientMap = new THREE.DataTexture(colors, colors.length, 1, format); + gradientMap.needsUpdate = true; + return new THREE.MeshToonMaterial({color: color, gradientMap: gradientMap}); } - return instance; } -export default GetSceneRenderer; \ No newline at end of file +export default SceneRenderer; \ No newline at end of file diff --git a/fission/src/systems/world/WorldSystem.ts b/fission/src/systems/world/WorldSystem.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/fission/src/test/mirabuf/MirabufParser.test.ts b/fission/src/test/MirabufParser.test.ts similarity index 77% rename from fission/src/test/mirabuf/MirabufParser.test.ts rename to fission/src/test/MirabufParser.test.ts index c55061f556..93a0b3b2d7 100644 --- a/fission/src/test/mirabuf/MirabufParser.test.ts +++ b/fission/src/test/MirabufParser.test.ts @@ -1,8 +1,8 @@ import { describe, test, expect } from "vitest"; -import { mirabuf } from "../../proto/mirabuf"; -import MirabufParser, { RigidNodeReadOnly } from "../../mirabuf/MirabufParser"; -import { LoadMirabufLocal } from "../../mirabuf/MirabufLoader"; +import { mirabuf } from "../proto/mirabuf"; +import MirabufParser, { RigidNodeReadOnly } from "../mirabuf/MirabufParser"; +import { LoadMirabufLocal } from "../mirabuf/MirabufLoader"; describe('Mirabuf Parser Tests', () => { // test('Get Dozer JSON', () => { @@ -36,6 +36,16 @@ describe('Mirabuf Parser Tests', () => { // printRigidNodeParts(rn, testCubeMira); }); + test('Generate Rigid Nodes (Dozer_v2.mira)', () => { + const spikeMira = LoadMirabufLocal('./public/test_mira/Dozer_v2.mira'); + + const t = new MirabufParser(spikeMira); + const rn = t.rigidNodes; + + expect(filterNonPhysicsNodes(rn, spikeMira).length).toBe(7); + // printRigidNodeParts(rn, spikeMira); + }); + test('Generate Rigid Nodes (PhysicsSpikeTest_v1.mira)', () => { const spikeMira = LoadMirabufLocal('./public/test_mira/PhysicsSpikeTest_v1.mira'); @@ -78,10 +88,10 @@ function filterNonPhysicsNodes(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembl }); } -function printRigidNodeParts(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembly) { - nodes.forEach(x => { - console.log(`[ ${x.name} ]:`); - x.parts.forEach(y => console.log(`-> '${mira.data!.parts!.partInstances![y]!.info!.name!}'`)); - console.log(''); - }); -} \ No newline at end of file +// function printRigidNodeParts(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembly) { +// nodes.forEach(x => { +// console.log(`[ ${x.name} ]:`); +// x.parts.forEach(y => console.log(`-> '${mira.data!.parts!.partInstances![y]!.info!.name!}'`)); +// console.log(''); +// }); +// } \ No newline at end of file diff --git a/fission/src/test/PhysicsSystem.test.ts b/fission/src/test/PhysicsSystem.test.ts index ee56e85741..2665ae56a5 100644 --- a/fission/src/test/PhysicsSystem.test.ts +++ b/fission/src/test/PhysicsSystem.test.ts @@ -1,7 +1,9 @@ import { test, expect, describe, assert } from 'vitest'; -import { PhysicsSystem } from '../systems/physics/PhysicsSystem'; +import PhysicsSystem from '../systems/physics/PhysicsSystem'; +import { LoadMirabufLocal } from '@/mirabuf/MirabufLoader'; +import MirabufParser from '@/mirabuf/MirabufParser'; -describe('Physics System Tests', () => { +describe('Physics Sansity Checks', () => { test('Convex Hull Shape (Cube)', () => { const points: Float32Array = new Float32Array( [ @@ -60,4 +62,24 @@ describe('Physics System Tests', () => { shape.Release(); system.Destroy(); }); +}); + +describe('Mirabuf Body 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); + + expect(mapping.size).toBe(7); + }); + + test('Body Loading (Team_2471_(2018)_v7.mira)', () => { + 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); + + expect(mapping.size).toBe(10); + }); }); \ No newline at end of file diff --git a/fission/src/test/World.test.ts b/fission/src/test/World.test.ts new file mode 100644 index 0000000000..2ffb335834 --- /dev/null +++ b/fission/src/test/World.test.ts @@ -0,0 +1,19 @@ +import World from "@/systems/World"; +import { describe, test, expect, beforeEach, vi } from "vitest"; + +describe('World Tests', () => { + + beforeEach(() => { + vi.resetAllMocks(); + }); + + test('World Sanity Check', () => { + expect(World.isAlive).toBeFalsy(); + + // TODO: Find a way to mock window global + // World.InitWorld(); + // expect(World.isAlive).toBeTruthy(); + // expect(World.SceneRenderer).toBeTruthy(); + // expect(World.DestroyWorld).toBeTruthy(); + }); +}) \ No newline at end of file diff --git a/fission/src/util/TypeConversions.ts b/fission/src/util/TypeConversions.ts index 5443f55762..78e63b18e1 100644 --- a/fission/src/util/TypeConversions.ts +++ b/fission/src/util/TypeConversions.ts @@ -52,7 +52,7 @@ export function JoltQuat_ThreeQuaternion(quat: Jolt.Quat) { return new THREE.Quaternion(quat.GetX(), quat.GetY(), quat.GetZ(), quat.GetW()); } -export function JoltMat44_ThreeMatrix4(m: Jolt.Mat44): THREE.Matrix4 { +export function JoltMat44_ThreeMatrix4(m: Jolt.RMat44): THREE.Matrix4 { return new THREE.Matrix4().compose( JoltVec3_ThreeVector3(m.GetTranslation()), JoltQuat_ThreeQuaternion(m.GetQuaternion()), @@ -74,5 +74,17 @@ export function MirabufVector3_ThreeVector3(v: mirabuf.Vector3): THREE.Vector3 { } export function MirabufVector3_JoltVec3(v: mirabuf.Vector3): Jolt.Vec3 { - return new Jolt.Vec3(v.x / 100.0, v.y / 100.0, v.z / 100.0); + return new JOLT.Vec3(v.x / 100.0, v.y / 100.0, v.z / 100.0); +} + +export function MirabufFloatArr_JoltVec3(v: number[], offsetIndex: number): Jolt.Vec3 { + return new JOLT.Vec3(v[offsetIndex] / 100.0, v[offsetIndex + 1] / 100.0, v[offsetIndex + 2] / 100.0); +} + +export function MirabufFloatArr_JoltVec3Arr(v: number[]): Jolt.Vec3[] { + const arr = []; + for (let i = 0; i < v.length; i += 3) { + arr.push(MirabufFloatArr_JoltVec3(v, i)); + } + return arr; } \ No newline at end of file diff --git a/fission/src/util/debug/DebugPrint.ts b/fission/src/util/debug/DebugPrint.ts new file mode 100644 index 0000000000..00e44a1f15 --- /dev/null +++ b/fission/src/util/debug/DebugPrint.ts @@ -0,0 +1,26 @@ +import { RigidNodeReadOnly } from "@/mirabuf/MirabufParser"; +import { mirabuf } from "@/proto/mirabuf"; + +export function printRigidNodeParts(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembly) { + nodes.forEach(x => { + console.log(`[ ${x.name} ]:`); + x.parts.forEach(y => console.log(`-> '${mira.data!.parts!.partInstances![y]!.info!.name!}'`)); + console.log(''); + }); +} + +export function threeMatrix4ToString(mat: THREE.Matrix4) { + const arr = mat.toArray(); + return `[\n${arr[0].toFixed(4)}, ${arr[4].toFixed(4)}, ${arr[8].toFixed(4)}, ${arr[12].toFixed(4)},\n` + + `${arr[1].toFixed(4)}, ${arr[5].toFixed(4)}, ${arr[9].toFixed(4)}, ${arr[13].toFixed(4)},\n` + + `${arr[2].toFixed(4)}, ${arr[6].toFixed(4)}, ${arr[10].toFixed(4)}, ${arr[14].toFixed(4)},\n` + + `${arr[3].toFixed(4)}, ${arr[7].toFixed(4)}, ${arr[11].toFixed(4)}, ${arr[15].toFixed(4)},\n]` +} + +export function mirabufTransformToString(mat: mirabuf.ITransform) { + const arr = mat.spatialMatrix!; + return `[\n${arr[0].toFixed(4)}, ${arr[1].toFixed(4)}, ${arr[2].toFixed(4)}, ${arr[3].toFixed(4)},\n` + + `${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]` +} \ No newline at end of file diff --git a/fission/src/util/threejs/MeshCreation.ts b/fission/src/util/threejs/MeshCreation.ts index 267a8ce542..3342d4898e 100644 --- a/fission/src/util/threejs/MeshCreation.ts +++ b/fission/src/util/threejs/MeshCreation.ts @@ -2,7 +2,6 @@ import JOLT from '../loading/JoltSyncLoader.ts'; import Jolt from '@barclah/jolt-physics'; import * as THREE from 'three'; import { JoltVec3_ThreeVector3, JoltQuat_ThreeQuaternion } from '../TypeConversions.ts'; -import { Random } from '../Random.ts'; export const LAYER_NOT_MOVING = 0; export const LAYER_MOVING = 1; @@ -84,37 +83,4 @@ export function removeFromScene(scene: THREE.Scene, threeObject: THREE.Mesh, bod scene.remove(threeObject); const idx = dynamicObjects.indexOf(threeObject); dynamicObjects.splice(idx, 1); -} - -function getRandomQuat() { - const vec = new JOLT.Vec3(0.001 + Random(), Random(), Random()); - const quat = JOLT.Quat.prototype.sRotation(vec.Normalized(), 2 * Math.PI * Random()); - JOLT.destroy(vec); - return quat; -} - -function makeRandomBox(scene: THREE.Scene, bodyInterface: Jolt.BodyInterface, dynamicObjects: THREE.Mesh[]) { - const pos = new JOLT.Vec3((Random() - 0.5) * 25, 15, (Random() - 0.5) * 25); - const rot = getRandomQuat(); - - const x = Random(); - const y = Random(); - const z = Random(); - const size = new JOLT.Vec3(x, y, z); - const shape = new JOLT.BoxShape(size, 0.05, undefined); - const creationSettings = new JOLT.BodyCreationSettings(shape, pos, rot, JOLT.EMotionType_Dynamic, LAYER_MOVING); - creationSettings.mRestitution = 0.5; - const body = bodyInterface.CreateBody(creationSettings); - - JOLT.destroy(pos); - JOLT.destroy(rot); - JOLT.destroy(size); - - // I feel as though this object should be freed at this point but doing so will cause a crash at runtime. - // This is the only object where this happens. I'm not sure why. Seems problematic. - // Jolt.destroy(shape); - - JOLT.destroy(creationSettings); - - addToScene(scene, body, new THREE.Color(0xff0000), bodyInterface, dynamicObjects); -} +} \ No newline at end of file diff --git a/fission/vite.config.ts b/fission/vite.config.ts index 0adffa11cd..b1e2517549 100644 --- a/fission/vite.config.ts +++ b/fission/vite.config.ts @@ -6,15 +6,18 @@ import { viteSingleFile } from 'vite-plugin-singlefile' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), /* viteSingleFile() */], -resolve: { - alias: [ - { find: '@', replacement: path.resolve(__dirname, 'src') } - ] -}, + resolve: { + alias: [ + { find: '@', replacement: path.resolve(__dirname, 'src') } + ] + }, server: { // this ensures that the browser opens upon server start open: true, // this sets a default port to 3000 port: 3000, + }, + build: { + target: 'esnext' } })