diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c7ff7d..2b133c98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## [0.8.3](https://github.com/Orillusion/orillusion/compare/v0.8.2...v0.8.3) (2024-08-28) + + +### Bug Fixes + +* fix frameRate and camera resize ([c4b8626](https://github.com/Orillusion/orillusion/commit/c4b8626c91937d50fba1a2d94101e84053a83d4c)) +* fix InstanceDraw destroy error ([4529594](https://github.com/Orillusion/orillusion/commit/4529594491e111e2d98ca4319189215614d97654)) +* **GUI:** add option to receive post effects ([#426](https://github.com/Orillusion/orillusion/issues/426)) ([af74bb1](https://github.com/Orillusion/orillusion/commit/af74bb1c14a1ee42af749868271f9b45a65c2384)) +* **inputsystem:** capture pointer on pointerdown ([#432](https://github.com/Orillusion/orillusion/issues/432)) ([cc90b82](https://github.com/Orillusion/orillusion/commit/cc90b82d4d9ab8250553263e3c0499a84e3e503c)) +* **shadow:** fix acceptShadow ([4d6a838](https://github.com/Orillusion/orillusion/commit/4d6a8387310381d158fc13bc168cc7482cc656b3)) +* **transform:** fix lookAt at vertical angle ([#431](https://github.com/Orillusion/orillusion/issues/431)) ([1922f18](https://github.com/Orillusion/orillusion/commit/1922f185f67b450dcbb04216ee30dfba8cc0e0a2)) + + +### Features + +* add GridObject ([#436](https://github.com/Orillusion/orillusion/issues/436)) ([a939ce6](https://github.com/Orillusion/orillusion/commit/a939ce62ccbe3e6db6e964ebcf2921d975b23a1c)) +* **geometry:** add extra geometry package, extrude geometry and text geometry ([#442](https://github.com/Orillusion/orillusion/issues/442)) ([069e6d4](https://github.com/Orillusion/orillusion/commit/069e6d40d4510be09dfe3c7af9ac1b97bb855ccd)) +* **graphic:** move graphic3D to @orillusion/graphic ([#427](https://github.com/Orillusion/orillusion/issues/427)) ([a1d1b2a](https://github.com/Orillusion/orillusion/commit/a1d1b2aa9fc0b6abc55ad7894312f1100f6b466e)) +* **physics:** add RopeSoftBody, rigidbody dragger, and enhance collisionShapeUtil ([#448](https://github.com/Orillusion/orillusion/issues/448)) ([452d730](https://github.com/Orillusion/orillusion/commit/452d730ef3377867cd81fe6d78e3a1b744c4e2b5)) +* **physics:** Refactor physics plugin with extensive enhancements and new features ([#440](https://github.com/Orillusion/orillusion/issues/440)) ([7c18db5](https://github.com/Orillusion/orillusion/commit/7c18db5157a0001c9f056e6c7a158e62ff5f0e2b)) + + + ## [0.8.2](https://github.com/Orillusion/orillusion/compare/v0.8.1...v0.8.2) (2024-07-21) diff --git a/package.json b/package.json index 89a4a284..1d93bf7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@orillusion/core", - "version": "0.8.3-dev.2", + "version": "0.8.4-dev.1", "author": "Orillusion", "description": "Orillusion WebGPU Engine", "type": "module", @@ -31,7 +31,14 @@ "scripts": { "dev": "vite", "build": "tsc --p tsconfig.build.json && vite build && npm run build:types && npm run minify", - "build:test": "tsc --p tsconfig.build.json && vite build", + "build:test": "tsc --p tsconfig.build.json && vite build && npm run build:packages", + "build:packages": "npm run build:physics && npm run build:media && npm run build:stats && npm run build:particle && npm run build:graphic && npm run build:geometry", + "build:physics": "cd packages/physics && npm run build", + "build:media": "cd packages/media-extention && npm run build", + "build:stats": "cd packages/stats && npm run build", + "build:particle": "cd packages/particle && npm run build", + "build:graphic": "cd packages/graphic && npm run build", + "build:geometry": "cd packages/geometry && npm run build", "build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json && rm -rf dist/packages && mv dist/src dist/types", "minify": "./node_modules/vite/node_modules/.bin/esbuild dist/orillusion.es.max.js --sourcemap --minify --outfile=dist/orillusion.es.js && ./node_modules/vite/node_modules/.bin/esbuild dist/orillusion.umd.max.js --minify --sourcemap --outfile=dist/orillusion.umd.js", "test": "electron test/ci/main.js", @@ -47,7 +54,7 @@ "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" }, "devDependencies": { - "@webgpu/types": "^0.1.40", + "@webgpu/types": "^0.1.45", "conventional-changelog-cli": "^2.2.2", "electron": "^31.1.0", "typedoc": "^0.25.7", diff --git a/packages/ammo/ammo.d.ts b/packages/ammo/ammo.d.ts index ff259acf..be37de25 100644 --- a/packages/ammo/ammo.d.ts +++ b/packages/ammo/ammo.d.ts @@ -7,6 +7,11 @@ declare function Ammo(target?: T): Promise; * Ammo.js by Bullet2 */ declare module Ammo { + function wrapPointer(ptr: number, type: new (...args: any[]) => T): T; + function addFunction(func: Function): number; + function getPointer(object: any): number; + function UTF8ToString(warningString: number): string; + function castObject T>(object: any, type: C): InstanceType; function destroy(obj: any): void; function _malloc(size: number): number; function _free(ptr: number): void; @@ -120,6 +125,7 @@ declare module Ammo { set_m_graphicsWorldTrans(m_graphicsWorldTrans: btTransform): void; } class btCollisionObject { + kB: number; setAnisotropicFriction(anisotropicFriction: btVector3, frictionMode: number): void; getCollisionShape(): btCollisionShape; setContactProcessingThreshold(contactProcessingThreshold: number): void; @@ -410,6 +416,10 @@ declare module Ammo { class btBvhTriangleMeshShape extends btTriangleMeshShape { constructor(meshInterface: btStridingMeshInterface, useQuantizedAabbCompression: boolean, buildBvh?: boolean); } + class btGImpactMeshShape extends btTriangleMeshShape { + constructor(meshInterface: btStridingMeshInterface); + updateBound(): void; + } class btHeightfieldTerrainShape extends btConcaveShape { constructor(heightStickWidth: number, heightStickLength: number, heightfieldData: unknown, heightScale: number, minHeight: number, maxHeight: number, upAxis: number, hdt: PHY_ScalarType, flipQuadEdges: boolean); setMargin(margin: number): void; @@ -598,6 +608,11 @@ declare module Ammo { setUpperLinLimit(upperLimit: number): void; setLowerAngLimit(lowerAngLimit: number): void; setUpperAngLimit(upperAngLimit: number): void; + getLinearPos(): number; + getAngularPos(): number; + setTargetLinMotorVelocity(velocity: number): void; + setPoweredLinMotor(onOff: boolean): void; + setMaxLinMotorForce(force: number): void; } class btFixedConstraint extends btTypedConstraint { constructor(rbA: btRigidBody, rbB: btRigidBody, frameInA: btTransform, frameInB: btTransform); diff --git a/packages/ammo/package.json b/packages/ammo/package.json index 3055ea0e..398c4e3c 100644 --- a/packages/ammo/package.json +++ b/packages/ammo/package.json @@ -1,6 +1,6 @@ { "name": "@orillusion/ammo", - "version": "0.2.0", + "version": "0.2.1", "author": "Orillusion", "description": "Orillusion WebGPU Engine", "main": "ammo.js", diff --git a/packages/effect/README.md b/packages/effect/README.md deleted file mode 100644 index 2dadee67..00000000 --- a/packages/effect/README.md +++ /dev/null @@ -1,15 +0,0 @@ -Add effect components for [Orillusion](https://www.orillusion.com) - -## Install -```bash -npm install @orillusion/core --save -npm install @orillusion/effect --save -``` - -Or access Global build from CDN -```html - - -``` - -More doc from [Orillusion](https://www.orillusion.com/guide/effect/Readme.html) \ No newline at end of file diff --git a/packages/effect/grass/GrassNode.ts b/packages/effect/grass/GrassNode.ts deleted file mode 100644 index 5f77d432..00000000 --- a/packages/effect/grass/GrassNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Transform } from "@orillusion/core"; - -export class GrassNode extends Transform { - -} \ No newline at end of file diff --git a/packages/effect/index.ts b/packages/effect/index.ts deleted file mode 100644 index 39a2fef9..00000000 --- a/packages/effect/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./grass/GrassNode"; -export * from "./grass/component/GrassComponent"; -export * from "./grass/geometry/GrassGeometry"; -export * from "./grass/material/GrassMaterial"; -export * from "./terrain/geometry/TerrainGeometry"; diff --git a/packages/effect/package.json b/packages/effect/package.json deleted file mode 100644 index 85bdeeab..00000000 --- a/packages/effect/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@orillusion/effect", - "version": "0.1.3", - "author": "Orillusion", - "description": "Orillusion Effects Plugin", - "main": "./dist/effect.umd.js", - "module": "./dist/effect.es.js", - "module:dev": "./index.ts", - "types": "./dist/index.d.ts", - "files": ["dist"], - "scripts": { - "build": "vite build && npm run build:types && npm run build:clean", - "build:types": "tsc --emitDeclarationOnly -p tsconfig.json", - "build:clean": "mv dist/packages/effect/* dist && rm -rf dist/src && rm -rf dist/packages" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/Orillusion/orillusion.git" - }, - "dependencies": { - "@orillusion/core": "^0.8.0" - } -} diff --git a/packages/effect/tsconfig.json b/packages/effect/tsconfig.json deleted file mode 100644 index 99cc83e4..00000000 --- a/packages/effect/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "sourceMap": false, - "resolveJsonModule": true, - "esModuleInterop": true, - "types": ["vite/client", "@webgpu/types"], - // for build - // "noEmit": true, - "declaration": true, - "declarationDir": "dist", - "outDir": "dist", - "skipLibCheck": true, - "noImplicitAny": false, - "noUnusedLocals": false, - "noUnusedParameters":false, - "noImplicitReturns": false, - "strictPropertyInitialization": false, - "strictNullChecks": false, - "strict": false, - "paths": { - "@orillusion/core": ["../../src"], - "@orillusion/*": ["../*"] - } - } -} \ No newline at end of file diff --git a/packages/effect/vite.config.js b/packages/effect/vite.config.js deleted file mode 100644 index 1ad90b75..00000000 --- a/packages/effect/vite.config.js +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vite' -const path = require('path') -export default defineConfig({ - resolve: { - alias: { - '@orillusion/core': path.resolve(__dirname, '../../src'), - '@orillusion': path.resolve(__dirname, '../') - } - }, - build: { - target: 'esnext', - lib: { - entry: path.resolve('index.ts'), - name: 'Effect', - fileName: (format) => `effect.${format}.js` - }, - rollupOptions: { - external: ['@orillusion/core'], - output: { - globals: { - '@orillusion/core': 'Orillusion' - } - } - } - } -}) \ No newline at end of file diff --git a/packages/geometry/README.md b/packages/geometry/README.md new file mode 100644 index 00000000..4f3f3f23 --- /dev/null +++ b/packages/geometry/README.md @@ -0,0 +1,19 @@ +Extra geometry plugins for [Orillusion](https://www.orillusion.com) + +## Usage +```bash +npm install @orillusion/core --save +npm install @orillusion/geometry --save +``` +```ts +import { Scene3D } from "@orillusion/core" +import { TextGeometry, ExtrudeGeometry, TerrainGeometry } from "@orillusion/geometry" +``` + +Or access Global build from CDN +```html + + +``` + +More doc from [Orillusion](https://www.orillusion.com/guide/graphics/mesh.html) \ No newline at end of file diff --git a/packages/effect/grass/geometry/GrassGeometry.ts b/packages/geometry/grass/GrassGeometry.ts similarity index 95% rename from packages/effect/grass/geometry/GrassGeometry.ts rename to packages/geometry/grass/GrassGeometry.ts index 0289b3b5..4ec4dbfa 100644 --- a/packages/effect/grass/geometry/GrassGeometry.ts +++ b/packages/geometry/grass/GrassGeometry.ts @@ -1,15 +1,11 @@ -import { BoundingBox, GeometryBase, Vector3, VertexAttributeName } from "@orillusion/core"; -import { GrassNode } from "../GrassNode"; - - - +import { BoundingBox, GeometryBase, Transform, Vector3, VertexAttributeName } from "@orillusion/core"; export class GrassGeometry extends GeometryBase { public width: number; public height: number; public segmentW: number; public segmentH: number; - public nodes: GrassNode[]; + public nodes: Transform[]; constructor(width: number, height: number, segmentW: number = 1, segmentH: number = 1, count: number) { super(); @@ -43,7 +39,7 @@ export class GrassGeometry extends GeometryBase { let pi = 3.1415926 * 0.5; for (let gi = 0; gi < count; gi++) { - let node = new GrassNode(); + let node = new Transform(); this.nodes.push(node); let dir = new Vector3(1 * Math.random() - 0.5, 0.0, 1 * Math.random() - 0.5); diff --git a/packages/effect/grass/component/GrassComponent.ts b/packages/geometry/grass/component/GrassComponent.ts similarity index 83% rename from packages/effect/grass/component/GrassComponent.ts rename to packages/geometry/grass/component/GrassComponent.ts index db1e08c4..cbbd4861 100644 --- a/packages/effect/grass/component/GrassComponent.ts +++ b/packages/geometry/grass/component/GrassComponent.ts @@ -1,7 +1,6 @@ -import { BoundingBox, Color, ComponentBase, GPUPrimitiveTopology, LightingFunction_frag, MeshRenderer, Texture, UnLitMaterial, Vector3, VertexAttributeName } from "@orillusion/core"; +import { BoundingBox, Color, ComponentBase, GPUPrimitiveTopology, LightingFunction_frag, MeshRenderer, Texture, Transform, UnLitMaterial, Vector3, VertexAttributeName } from "@orillusion/core"; import { GrassMaterial } from "../material/GrassMaterial"; -import { GrassGeometry } from "../geometry/GrassGeometry"; -import { GrassNode } from "../GrassNode"; +import { GrassGeometry } from "../GrassGeometry"; export class GrassComponent extends MeshRenderer { @@ -40,7 +39,7 @@ export class GrassComponent extends MeshRenderer { this.grassMaterial.baseMap = grassTexture; } - public get nodes(): GrassNode[] { + public get nodes(): Transform[] { return this.grassGeometry.nodes; } } \ No newline at end of file diff --git a/packages/effect/grass/material/GrassMaterial.ts b/packages/geometry/grass/material/GrassMaterial.ts similarity index 100% rename from packages/effect/grass/material/GrassMaterial.ts rename to packages/geometry/grass/material/GrassMaterial.ts diff --git a/packages/effect/grass/shader/GrassBaseShader.ts b/packages/geometry/grass/shader/GrassBaseShader.ts similarity index 100% rename from packages/effect/grass/shader/GrassBaseShader.ts rename to packages/geometry/grass/shader/GrassBaseShader.ts diff --git a/packages/effect/grass/shader/GrassCastShadowShader.ts b/packages/geometry/grass/shader/GrassCastShadowShader.ts similarity index 100% rename from packages/effect/grass/shader/GrassCastShadowShader.ts rename to packages/geometry/grass/shader/GrassCastShadowShader.ts diff --git a/packages/effect/grass/shader/GrassShader.ts b/packages/geometry/grass/shader/GrassShader.ts similarity index 100% rename from packages/effect/grass/shader/GrassShader.ts rename to packages/geometry/grass/shader/GrassShader.ts diff --git a/packages/effect/grass/shader/GrassVertexAttributeShader.ts b/packages/geometry/grass/shader/GrassVertexAttributeShader.ts similarity index 100% rename from packages/effect/grass/shader/GrassVertexAttributeShader.ts rename to packages/geometry/grass/shader/GrassVertexAttributeShader.ts diff --git a/packages/geometry/index.ts b/packages/geometry/index.ts index eda175ee..8aa21b39 100644 --- a/packages/geometry/index.ts +++ b/packages/geometry/index.ts @@ -7,4 +7,8 @@ export * from "./ExtrudeGeometry/Path2D" export * from "./ExtrudeGeometry/Shape2D" export * from "./ExtrudeGeometry/ShapeUtils" export * from "./parser/FontParser" -export * from "./TextGeometry" \ No newline at end of file +export * from "./text/TextGeometry" +export * from "./terrain/TerrainGeometry" +export * from "./grass/GrassGeometry" +export * from "./grass/component/GrassComponent"; +export * from "./grass/material/GrassMaterial"; diff --git a/packages/geometry/package.json b/packages/geometry/package.json index 747bdc16..05218faf 100644 --- a/packages/geometry/package.json +++ b/packages/geometry/package.json @@ -1,8 +1,8 @@ { "name": "@orillusion/geometry", - "version": "0.2.0", + "version": "0.2.4", "author": "Orillusion", - "description": "Orillusion geometry Plugin", + "description": "Orillusion Geometry Plugin", "main": "./dist/geometry.umd.js", "module": "./dist/geometry.es.js", "module:dev": "./index.ts", @@ -21,7 +21,7 @@ "type": "git", "url": "git+https://github.com/Orillusion/orillusion.git" }, - "dependencies": { - "@orillusion/core": "^0.8.0" + "peerDependencies": { + "@orillusion/core": ">=0.8.0" } } diff --git a/packages/effect/terrain/geometry/TerrainGeometry.ts b/packages/geometry/terrain/TerrainGeometry.ts similarity index 96% rename from packages/effect/terrain/geometry/TerrainGeometry.ts rename to packages/geometry/terrain/TerrainGeometry.ts index 4cfd3b7e..d602c051 100644 --- a/packages/effect/terrain/geometry/TerrainGeometry.ts +++ b/packages/geometry/terrain/TerrainGeometry.ts @@ -1,4 +1,4 @@ -import { BitmapTexture2D, Plane, PlaneGeometry, Texture, Vector3, VertexAttributeName, lerp } from "@orillusion/core" +import { BitmapTexture2D, PlaneGeometry, Vector3, VertexAttributeName, lerp } from "@orillusion/core" export class TerrainGeometry extends PlaneGeometry { diff --git a/packages/geometry/text/TextGeometry.ts b/packages/geometry/text/TextGeometry.ts new file mode 100644 index 00000000..a1372b25 --- /dev/null +++ b/packages/geometry/text/TextGeometry.ts @@ -0,0 +1,75 @@ +import { Shape2D } from "../ExtrudeGeometry/Shape2D"; +import { ExtrudeGeometry, ExtrudeGeometryArgs } from "../ExtrudeGeometry/ExtrudeGeometry"; +import { ShapeUtils } from "../ExtrudeGeometry/ShapeUtils"; +import { Font } from "../lib/opentype"; + +export type TextGeometryArgs = ExtrudeGeometryArgs & { + font: Font; + fontSize: number; +} + +export class TextGeometry extends ExtrudeGeometry { + private _text: string; + declare public options: TextGeometryArgs; + constructor(text: string, options: TextGeometryArgs) { + super([], options); + this.options = options; + this.text = text; + } + + public get font(): Font { + return this.options.font; + } + + public get text(): string { + return this._text + } + + public get fontSize(): number { + return this.options.fontSize; + } + + public set fontSize(v: number) { + this.options.fontSize = v; + } + + public set text(v: string) { + this._text = v; + let paths = this.font.getPath(v, 0, 0, this.fontSize); + this.buildShape(paths); + this.buildGeometry(this.options); + } + + protected buildShape(path: any) { + let first: any, latest: any; + let shape2D = new Shape2D(); + const commands = path.commands; + for (let i = 0; i < commands.length; i++) { + const c = commands[i]; + shape2D = shape2D || new Shape2D(); + switch (c.type) { + case 'M': shape2D.moveTo(c.x, -c.y); first = c; break; + case 'L': shape2D.lineTo(c.x, -c.y); latest = c; break; + case 'C': shape2D.bezierCurveTo(c.x1, -c.y1, c.x2, -c.y2, c.x, -c.y); latest = c; break; + case 'Q': shape2D.quadraticCurveTo(c.x1, -c.y1, c.x, -c.y); latest = c; break; + case 'Z': + shape2D.lineTo(first.x, -first.y); + if (ShapeUtils.isClockWise(shape2D.getPoints(1))) { + this.shapes.push(shape2D); + } else { + for (let shape of this.shapes) { + if (shape.isIntersect(shape2D)) { + shape.holes.push(shape2D); + } + } + } + shape2D = null; + break; + } + } + + if (shape2D) { + this.shapes.push(shape2D); + } + } +} diff --git a/packages/graphic/package.json b/packages/graphic/package.json index 9ed99e18..08f4db6e 100644 --- a/packages/graphic/package.json +++ b/packages/graphic/package.json @@ -1,8 +1,8 @@ { "name": "@orillusion/graphic", - "version": "0.2.0", + "version": "0.2.2", "author": "Orillusion", - "description": "Orillusion graphic Plugin", + "description": "Orillusion Graphic Plugin", "main": "./dist/graphic.umd.js", "module": "./dist/graphic.es.js", "module:dev": "./index.ts", @@ -21,7 +21,7 @@ "type": "git", "url": "git+https://github.com/Orillusion/orillusion.git" }, - "dependencies": { - "@orillusion/core": "^0.8.0" + "peerDependencies": { + "@orillusion/core": ">=0.8.0" } } diff --git a/packages/media-extention/package.json b/packages/media-extention/package.json index 6a2a7225..9ffc67e7 100644 --- a/packages/media-extention/package.json +++ b/packages/media-extention/package.json @@ -1,6 +1,6 @@ { "name": "@orillusion/media-extention", - "version": "0.3.4", + "version": "0.3.6", "author": "Orillusion", "description": "Orillusion Media Material Extention", "main": "./dist/media.umd.js", @@ -20,7 +20,7 @@ "type": "git", "url": "git+https://github.com/Orillusion/orillusion.git" }, - "dependencies": { - "@orillusion/core": "^0.7.0" + "peerDependencies": { + "@orillusion/core": ">=0.8.0" } } diff --git a/packages/particle/package.json b/packages/particle/package.json index e63c7790..b292b79b 100644 --- a/packages/particle/package.json +++ b/packages/particle/package.json @@ -1,6 +1,6 @@ { "name": "@orillusion/particle", - "version": "0.1.0", + "version": "0.1.6", "author": "Orillusion", "description": "Orillusion Particle Plugin", "main": "./dist/particle.umd.js", @@ -20,7 +20,7 @@ "type": "git", "url": "git+https://github.com/Orillusion/orillusion.git" }, - "dependencies": { - "@orillusion/core": "^0.7.0" + "peerDependencies": { + "@orillusion/core": ">=0.8.0" } } diff --git a/packages/physics/ClothSoftBody.ts b/packages/physics/ClothSoftBody.ts deleted file mode 100644 index 4d1b86a7..00000000 --- a/packages/physics/ClothSoftBody.ts +++ /dev/null @@ -1,124 +0,0 @@ -import Ammo from '@orillusion/ammo'; -import { ComponentBase, Vector3, PlaneGeometry } from '@orillusion/core' -import { Physics } from './Physics'; -/** - * @internal - * @group Plugin - */ -export class ClothSoftBody extends ComponentBase { - private _mass: number = 0.01; - - private _planeGeo: PlaneGeometry; - - private _clothCorner00: Vector3; - - private _clothCorner01: Vector3; - - private _clothCorner10: Vector3; - - private _clothCorner11: Vector3; - - private _softBody: Ammo.btSoftBody; - - // get setter clothcorner - public get clothCorner00(): Vector3 { - return this._clothCorner00; - } - - public set clothCorner00(value: Vector3) { - this._clothCorner00 = value; - } - - public set clothCorner01(value: Vector3) { - this._clothCorner01 = value; - } - - public set clothCorner10(value: Vector3) { - this._clothCorner10 = value; - } - - public get clothCorner11(): Vector3 { - return this._clothCorner11; - } - - public set clothCorner11(value: Vector3) { - this._clothCorner11 = value; - } - - public get planeGeometry(): PlaneGeometry { - return this._planeGeo; - } - - public set planeGeometry(value: PlaneGeometry) { - this._planeGeo = value; - } - - public get mass(): number { - return this._mass; - } - - public set mass(value: number) { - this._mass = value; - } - - start(): void { - if (!this._planeGeo) { - console.error('cloth need planeGeometry'); - return; - } - - if (!this._clothCorner00) { - console.error('cloth need clothCorner00'); - return; - } - - if (!this._clothCorner01) { - console.error('cloth need clothCorner01'); - return; - } - - if (!this._clothCorner10) { - console.error('cloth need clothCorner10'); - return; - } - - if (!this.clothCorner11) { - console.error('cloth need clothCorner11'); - return; - } - var cc00 = new Ammo.btVector3(this._clothCorner00.x, this._clothCorner00.y, this._clothCorner00.z); - var cc01 = new Ammo.btVector3(this._clothCorner01.x, this._clothCorner01.y, this._clothCorner01.z); - var cc10 = new Ammo.btVector3(this._clothCorner10.x, this._clothCorner10.y, this._clothCorner10.z); - var cc11 = new Ammo.btVector3(this._clothCorner11.x, this._clothCorner11.y, this._clothCorner11.z); - - var softBodyHelpers = new Ammo.btSoftBodyHelpers(); - var softBody = softBodyHelpers.CreatePatch( - (Physics.world as Ammo.btSoftRigidDynamicsWorld).getWorldInfo(), - //todo calc clothCorner; - cc00, - cc01, - cc10, - cc11, - this._planeGeo.width, - this._planeGeo.height, - 0, - true, - ); - var sbconfig = softBody.get_m_cfg(); - sbconfig.set_viterations(10); - sbconfig.set_piterations(10); - softBody.setTotalMass(0.9, false); - // Ammo.castObject(softBody, Ammo.btCollisionObject).getCollisionShape().setMargin(0.05); - (Physics.world as Ammo.btSoftRigidDynamicsWorld).addSoftBody(softBody, 1, -1); - this._softBody = softBody; - // this._planeGeo.indexBuffer.buffer - - // var c00 = new - - - } - - public onUpdate(): void { - //todo update geo vecs - } -} diff --git a/packages/physics/HingeConstraint.ts b/packages/physics/HingeConstraint.ts deleted file mode 100644 index ed0b6163..00000000 --- a/packages/physics/HingeConstraint.ts +++ /dev/null @@ -1,69 +0,0 @@ -import Ammo from '@orillusion/ammo'; -import { ComponentBase, Vector3 } from '@orillusion/core' -import { Physics } from './Physics'; -import { Rigidbody } from './Rigidbody'; -/** - * @internal - * @group Plugin - */ -export class HingeConstraint extends ComponentBase { - private _targetRigidbody: Rigidbody; - public pivotSelf: Vector3 = new Vector3(); - public pivotTarget: Vector3 = new Vector3(); - public axisSelf: Vector3 = new Vector3(0, 1, 0); - public axisTarget: Vector3 = new Vector3(0, 1, 0); - private _hinge: Ammo.btHingeConstraint; - - start(): void { - var selfRb = this.object3D.getComponent(Rigidbody); - if (selfRb == null) { - console.error('HingeConstraint need rigidbody'); - return; - } - - if (this._targetRigidbody == null) { - console.error('HingeConstraint need target rigidbody'); - return; - } - - let canStart = true; - if (!selfRb.btRigidbodyInited) { - selfRb.addInitedFunction(this.start, this); - canStart = false; - } - if (!this._targetRigidbody.btRigidbodyInited) { - this._targetRigidbody.addInitedFunction(this.start, this); - canStart = false; - } - - // console.log("hinge true start init"); - - let axisSelf = new Ammo.btVector3(this.axisSelf.x, this.axisSelf.y, this.axisSelf.z); - let axisTarget = new Ammo.btVector3(this.axisTarget.x, this.axisTarget.y, this.axisTarget.z); - - if (!canStart) { - return; - } - let pa = new Ammo.btVector3(this.pivotSelf.x, this.pivotSelf.y, this.pivotSelf.z); - let pb = new Ammo.btVector3(this.pivotTarget.x, this.pivotTarget.y, this.pivotTarget.z); - let hinge = new Ammo.btHingeConstraint(selfRb.btRigidbody, this._targetRigidbody.btRigidbody, pa, pb, axisSelf, axisTarget, true); - this._hinge = hinge; - Physics.world.addConstraint(hinge, true); - } - - public get hinge(): Ammo.btHingeConstraint { - return this._hinge; - } - - public get targetRigidbody(): Rigidbody { - return this._targetRigidbody; - } - - public set targetRigidbody(value: Rigidbody) { - this._targetRigidbody = value; - } - - public destroy(force?: boolean): void { - super.destroy(force); - } -} diff --git a/packages/physics/Physics.ts b/packages/physics/Physics.ts index 115310f4..3630aa45 100644 --- a/packages/physics/Physics.ts +++ b/packages/physics/Physics.ts @@ -1,165 +1,184 @@ import Ammo from '@orillusion/ammo'; -import {BoundingBox, Vector3, Time} from '@orillusion/core' -import { Rigidbody } from './Rigidbody'; +import { Vector3, Time, BoundingBox, Object3D, Quaternion } from '@orillusion/core'; +import { ContactProcessedUtil } from './utils/ContactProcessedUtil'; +import { RigidBodyUtil } from './utils/RigidBodyUtil'; +import { TempPhyMath } from './utils/TempPhyMath'; +import { Rigidbody } from './rigidbody/Rigidbody'; +import { PhysicsDebugDrawer } from './visualDebug/PhysicsDebugDrawer'; +import { DebugDrawerOptions } from './visualDebug/DebugDrawModeEnum'; +import { PhysicsDragger } from './utils/PhysicsDragger' -/** - * Physics Engine - * @group Plugin - */ class _Physics { private _world: Ammo.btDiscreteDynamicsWorld | Ammo.btSoftRigidDynamicsWorld; + private _isInited: boolean = false; private _isStop: boolean = false; private _gravity: Vector3 = new Vector3(0, -9.8, 0); - private _gravityEnabled: boolean = true; - private _maxSubSteps: number = 10; - private _fixedTimeStep: number = 1 / 60; - private _maxVelocity: number = 1000; - private _maxAngularVelocity: number = 1000; - private _maxForce: number = 1000; - private _maxTorque: number = 1000; - private _maxLinearCorrection: number = 0.2; - private _maxAngularCorrection: number = 0.2; - private _maxTranslation: number = 1000; - private _maxRotation: number = 1000; - private _maxSolverIterations: number = 20; - private _enableFriction: boolean = true; - private _enableCollisionEvents: boolean = true; - private _enableContinuous: boolean = true; - private _enableCCD: boolean = true; - private _enableWarmStarting: boolean = true; - private _enableTOI: boolean = true; - private _enableSAT: boolean = true; - private _enableSATNormal: boolean = true; - - private physicBound: BoundingBox; - private _isInited: boolean = false; + private _worldInfo: Ammo.btSoftBodyWorldInfo | null = null; + private _debugDrawer: PhysicsDebugDrawer; + private _physicsDragger: PhysicsDragger; + private _physicBound: BoundingBox; + private _destroyObjectBeyondBounds: boolean; - public TEMP_TRANSFORM: Ammo.btTransform; //Temp cache, save results from body.getWorldTransform() + public readonly contactProcessedUtil = ContactProcessedUtil; + public readonly rigidBodyUtil = RigidBodyUtil; - constructor() { } + public maxSubSteps: number = 10; + public fixedTimeStep: number = 1 / 60; /** - * Init Physics Engine + * 物理调试绘制器 */ - public async init() { - await Ammo.bind(window)(Ammo); - this.TEMP_TRANSFORM = new Ammo.btTransform(); - var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(); - var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration); - var overlappingPairCache = new Ammo.btDbvtBroadphase(); - var solver = new Ammo.btSequentialImpulseConstraintSolver(); - this._world = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration); - this._world.setGravity(new Ammo.btVector3(this._gravity.x, this._gravity.y, this._gravity.z)); - this._isInited = true; - - this.physicBound = new BoundingBox(new Vector3(), new Vector3(2000, 2000, 2000)); + public get debugDrawer() { + if (!this._debugDrawer) { + console.warn('To enable debugging, configure with: Physics.initDebugDrawer'); + } + return this._debugDrawer; } - public get maxSubSteps(): number { - return this._maxSubSteps; - } - public set maxSubSteps(value: number) { - this._maxSubSteps = value; + /** + * 物理拖拽器 + */ + public get physicsDragger() { + if (!this._physicsDragger) { + console.warn('To enable the dragger, set useDrag: true in Physics.init() during initialization.'); + } + return this._physicsDragger; } - public get fixedTimeStep(): number { - return this._fixedTimeStep; - } - public set fixedTimeStep(value: number) { - this._fixedTimeStep = value; - } + public TEMP_TRANSFORM: Ammo.btTransform; // Temp cache, save results from body.getWorldTransform() - public get isStop(): boolean { - return this._isStop; - } + /** + * 初始化物理引擎和相关配置。 + * + * @param options - 初始化选项参数对象。 + * @param options.useSoftBody - 是否启用软体模拟。 + * @param options.useDrag - 是否启用刚体拖拽功能。 + * @param options.physicBound - 物理边界,默认范围:2000 2000 2000,超出边界时将会销毁该刚体。 + * @param options.destroyObjectBeyondBounds - 是否在超出边界时销毁3D对象。默认 `false` 仅销毁刚体。 + */ + public async init(options: { useSoftBody?: boolean, useDrag?: boolean, physicBound?: Vector3, destroyObjectBeyondBounds?: boolean } = {}) { + await Ammo.bind(window)(Ammo); - public set isStop(value: boolean) { - this._isStop = value; - } + TempPhyMath.init(); - public set gravity(gravity: Vector3) { - this._gravity = gravity; - } + this.TEMP_TRANSFORM = new Ammo.btTransform(); + this.initWorld(options.useSoftBody); - public get gravity(): Vector3 { - return this._gravity; + if (options.useDrag) this._physicsDragger = new PhysicsDragger(); + + this._isInited = true; + this._destroyObjectBeyondBounds = options.destroyObjectBeyondBounds; + this._physicBound = new BoundingBox(new Vector3(), options.physicBound || new Vector3(2000, 2000, 2000)); } - public get world(): Ammo.btDiscreteDynamicsWorld { - return this._world; + /** + * 初始化物理调试绘制器 + * + * @param {Graphic3D} graphic3D - Type: `Graphic3D` A graphic object used to draw lines. + * @param {DebugDrawerOptions} [options] - 调试绘制选项,用于配置物理调试绘制器。 {@link DebugDrawerOptions} + */ + public initDebugDrawer(graphic3D: Object3D, options?: DebugDrawerOptions) { + this._debugDrawer = new PhysicsDebugDrawer(this.world, graphic3D, options); + } + + private initWorld(useSoftBody: boolean) { + const collisionConfiguration = useSoftBody + ? new Ammo.btSoftBodyRigidBodyCollisionConfiguration() + : new Ammo.btDefaultCollisionConfiguration(); + const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration); + const broadphase = new Ammo.btDbvtBroadphase(); + const solver = new Ammo.btSequentialImpulseConstraintSolver(); + + if (useSoftBody) { + const softBodySolver = new Ammo.btDefaultSoftBodySolver(); + this._world = new Ammo.btSoftRigidDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration, softBodySolver); + this._worldInfo = (this.world as Ammo.btSoftRigidDynamicsWorld).getWorldInfo(); + this._worldInfo.set_m_broadphase(broadphase); + this._worldInfo.set_m_dispatcher(dispatcher); + this._worldInfo.set_m_gravity(TempPhyMath.toBtVec(this._gravity)); + this._worldInfo.set_air_density(1.2); + this._worldInfo.set_water_density(0); + this._worldInfo.set_water_offset(0); + this._worldInfo.set_water_normal(TempPhyMath.setBtVec(0, 0, 0)); + this._worldInfo.set_m_maxDisplacement(0.5); + } else { + this._world = new Ammo.btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration); + } + + this._world.setGravity(TempPhyMath.toBtVec(this._gravity)); } - private initByDefault() { - var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(); - var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration); - var overlappingPairCache = new Ammo.btDbvtBroadphase(); - var solver = new Ammo.btSequentialImpulseConstraintSolver(); - this._world = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration); - this._world.setGravity(new Ammo.btVector3(this._gravity.x, this._gravity.y, this._gravity.z)); - this._isInited = true; - this.physicBound = new BoundingBox(new Vector3(), new Vector3(2000, 2000, 2000)); + /** + * 物理模拟更新 + * @param timeStep - 时间步长 + * @default Time.delta * 0.001 + */ + public update(timeStep: number = Time.delta * 0.001) { + if (!this._isInited || this.isStop) return; + this.world.stepSimulation(timeStep, this.maxSubSteps, this.fixedTimeStep); + // this.world.stepSimulation(Time.delta, 1, this.fixedTimeStep); + + this._debugDrawer?.update(); } - private initBySoft() { - var collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration(); - var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration); - var broadphase = new Ammo.btDbvtBroadphase(); - var solver = new Ammo.btSequentialImpulseConstraintSolver(); - var softBodySolver = new Ammo.btDefaultSoftBodySolver(); - this._world = new Ammo.btSoftRigidDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration, softBodySolver); - this._world.setGravity(new Ammo.btVector3(this._gravity.x, this._gravity.y, this._gravity.z)); - this._isInited = true; - this.physicBound = new BoundingBox(new Vector3(), new Vector3(2000, 2000, 2000)); + public get world(): Ammo.btDiscreteDynamicsWorld | Ammo.btSoftRigidDynamicsWorld { + return this._world; } public get isInited(): boolean { return this._isInited; } - private __updateWithDelta(time:number ) { - if(!this._isInited) return; - if(this.isStop) return; - this._world.stepSimulation(time); + public set isStop(value: boolean) { + this._isStop = value; } - private __updateWithFixedTimeStep(time:number, maxSubSteps:number, fixedTimeStep:number ) { - if(!this._isInited) return; - if(this.isStop) return; - this.world.stepSimulation(time, maxSubSteps, fixedTimeStep); + public get isStop() { + return this._isStop; } + public set gravity(value: Vector3) { + this._gravity.copyFrom(value); + this._world?.setGravity(TempPhyMath.toBtVec(value)); // 设置刚体物理重力 + this._worldInfo?.set_m_gravity(TempPhyMath.toBtVec(value)); // 设置软体物理重力 + } - public update() { - if (!this._isInited) { - return; - } - if (this.isStop) return; - - // let fix = Math.max(this._fixedTimeStep, Time.detail); - - this.__updateWithFixedTimeStep(Time.delta, 1, this._fixedTimeStep); - // this._world.stepSimulation(Time.delta, 1, this._fixedTimeStep); + public get gravity(): Vector3 { + return this._gravity; } - public addRigidbody(rigidBody: Rigidbody) { - this._world.addRigidBody(rigidBody.btRigidbody); + public get worldInfo(): Ammo.btSoftBodyWorldInfo { + return this._worldInfo; } - public removeRigidbody(rigidBody: Rigidbody) { - this._world.removeRigidBody(rigidBody.btRigidbody); + public get isSoftBodyWord() { + return this._world instanceof Ammo.btSoftRigidDynamicsWorld; } - checkBound(body: Rigidbody) { + public checkBound(body: Rigidbody) { if (body) { let wp = body.transform.worldPosition; - let inside = this.physicBound.containsPoint(wp); + let inside = this._physicBound.containsPoint(wp); if (!inside) { - body.btRigidbody.activate(false); - // this._world.removeRigidBody(body.btRigidbody); - body.destroy(); + if (this._destroyObjectBeyondBounds) { + body.object3D.destroy(); + } else { + body.btRigidbody.activate(false); + body.destroy(); + } } } } + + /** + * 将物理对象的位置和旋转同步至三维对象 + * @param object3D - 三维对象 + * @param tm - 物理对象变换 + */ + public syncGraphic(object3D: Object3D, tm: Ammo.btTransform): void { + object3D.localPosition = TempPhyMath.fromBtVec(tm.getOrigin(), Vector3.HELP_0); + object3D.localQuaternion = TempPhyMath.fromBtQua(tm.getRotation(), Quaternion.HELP_0); + } } /** @@ -173,4 +192,4 @@ class _Physics { * @internal */ export let Physics = new _Physics(); -export {Ammo} +export { Ammo }; diff --git a/packages/physics/README.md b/packages/physics/README.md index 0e9422cc..779eb0c0 100644 --- a/packages/physics/README.md +++ b/packages/physics/README.md @@ -7,7 +7,7 @@ npm install @orillusion/physics --save ``` ```ts import { Scene3D } from "@orillusion/core" -import { Stats } from "@orillusion/physics" +import { Physics } from "@orillusion/physics" ``` Or access Global build from CDN diff --git a/packages/physics/Rigidbody.ts b/packages/physics/Rigidbody.ts deleted file mode 100644 index 5f5b52c9..00000000 --- a/packages/physics/Rigidbody.ts +++ /dev/null @@ -1,286 +0,0 @@ -import Ammo from '@orillusion/ammo'; -import { Vector3, BoxColliderShape, CapsuleColliderShape, ColliderComponent, ComponentBase, MeshColliderShape, Quaternion, SphereColliderShape } from '@orillusion/core' -import { Physics } from './Physics'; - -enum CollisionFlags { - STATIC_OBJECT = 1, - KINEMATIC_OBJECT = 2, - NO_CONTACT_RESPONSE = 4, - CUSTOM_MATERIAL_CALLBACK = 8, - CHARACTER_OBJECT = 16, - DISABLE_VISUALIZE_OBJECT = 32, - DISABLE_SPU_COLLISION_PROCESSING = 64, - HAS_CONTACT_STIFFNESS_DAMPING = 128, - HAS_CUSTOM_DEBUG_RENDERING_COLOR = 256, - HAS_FRICTION_ANCHOR = 512, - HAS_COLLISION_SOUND_TRIGGER = 1024 -} - -enum CollisionObjectTypes { - COLLISION_OBJECT = 1, - RIGID_BODY = 2, - GHOST_OBJECT = 4, - SOFT_BODY = 8, - HF_FLUID = 16, - USER_TYPE = 32, - FEATHERSTONE_LINK = 64 -} - -/** - * Rigidbody Component - * Rigid bodies can endow game objects with physical properties, allowing them to be controlled by the physics system and subjected to forces and torques, thus achieving realistic motion effects. - * @group Components - */ -export class Rigidbody extends ComponentBase { - private _mass: number = 0.01; - private _velocity: Vector3 = new Vector3(); - private _angularVelocity: Vector3 = new Vector3(); - private _force: Vector3 = new Vector3(); - private _useGravity: boolean = true; - private _isKinematic: boolean = false; - private _isStatic: boolean = false; - private _isTrigger: boolean = false; - private _btRigidbody: Ammo.btRigidBody; - private _btRigidbodyInited: boolean = false; - private _friction: number = 0.6; - private _rollingFriction: number = 0.1; - private _restitution: number = 0.8 - - private _initedFunctions: { fun: Function; thisObj: Object }[] = []; - - init(): void { - - } - - public start(): void { - if (!this.object3D.getComponent(ColliderComponent)) { - console.error('rigidbody need collider'); - return; - } - this.initRigidbody(); - } - - /** - * Get friction value - */ - public get friction() { - return this._friction; - } - /** - * Set friction value - */ - public set friction(value: number) { - this._friction = value; - if (this._btRigidbody) this._btRigidbody.setFriction(value); - } - /** - * Get rolling friction value - */ - public get rollingFriction(): number { - return this._rollingFriction; - } - /** - * Set rolling friction value - */ - public set rollingFriction(value: number) { - this._rollingFriction = value; - if (this._btRigidbody) this._btRigidbody.setRollingFriction(value); - } - /** - * Get restitution value - */ - public get restitution(): number { - return this._restitution; - } - /** - * Set restitution value - */ - public set restitution(value: number) { - this._restitution = value; - if (this._btRigidbody) this._btRigidbody.setRestitution(value); - } - /** - * Check if rigidbody inited - */ - public get btRigidbodyInited(): boolean { - return this._btRigidbodyInited; - } - - private addAmmoRigidbody(): void { - var shape = this.getPhysicShape(); - var btTransform = new Ammo.btTransform(); - btTransform.setIdentity(); - var localInertia = new Ammo.btVector3(0, 0, 0); - - shape.calculateLocalInertia(this.mass, localInertia); - - btTransform.setOrigin(new Ammo.btVector3(this.object3D.x, this.object3D.y, this.object3D.z)); - let t = this.object3D.transform; - - Quaternion.HELP_0.fromEulerAngles(t.rotationX, t.rotationY, t.rotationZ); - let btq = new Ammo.btQuaternion(Quaternion.HELP_0.x, Quaternion.HELP_0.y, Quaternion.HELP_0.z, Quaternion.HELP_0.w); - btTransform.setRotation(btq); - - var motionState = new Ammo.btDefaultMotionState(btTransform); - var rbInfo = new Ammo.btRigidBodyConstructionInfo(this.mass, motionState, shape, localInertia); - - this._btRigidbody = new Ammo.btRigidBody(rbInfo); - this._btRigidbody.setRestitution(this.restitution); - this.btRigidbody.setFriction(this.friction); - this.btRigidbody.setRollingFriction(this.rollingFriction); - Physics.addRigidbody(this); - } - private initRigidbody(): void { - this.addAmmoRigidbody(); - - for (let i = 0; i < this._initedFunctions.length; i++) { - let fun = this._initedFunctions[i]; - fun.fun.call(fun.thisObj); - } - this._btRigidbodyInited = true; - } - - /** - * Add init callback - * @param fun callback function - * @param thisObj this - */ - public addInitedFunction(fun: Function, thisObj: Object) { - this._initedFunctions.push({ fun: fun, thisObj: thisObj }); - } - /** - * Remove init callback - * @param fun callback function - * @param thisObj this - */ - public removeInitedFunction(fun: Function, thisObj: Object) { - for (let i = 0; i < this._initedFunctions.length; i++) { - let item = this._initedFunctions[i]; - if (item.fun === fun && item.thisObj === thisObj) { - this._initedFunctions.splice(i, 1); - break; - } - } - } - - private getPhysicShape() { - let collider = this.object3D.getComponent(ColliderComponent); - let colliderShape = collider.shape; - - var shape: Ammo.btCollisionShape; - - if (colliderShape instanceof BoxColliderShape) { - shape = new Ammo.btBoxShape(new Ammo.btVector3(colliderShape.halfSize.x, colliderShape.halfSize.y, colliderShape.halfSize.z)); - } else if (colliderShape instanceof CapsuleColliderShape) { - shape = new Ammo.btCapsuleShape(colliderShape.radius, colliderShape.height); - } else if (colliderShape instanceof MeshColliderShape) { - // let triangleMeshShape = new Ammo.btTriangleMeshShape(); - } else if (colliderShape instanceof SphereColliderShape) { - shape = new Ammo.btSphereShape(colliderShape.radius); - } - return shape; - } - - /** - * Return internal Ammo.btRigidBody - */ - public get btRigidbody(): Ammo.btRigidBody { - return this._btRigidbody; - } - - onUpdate(): void { - if (this._btRigidbody && this._btRigidbody.getMotionState()) { - this._btRigidbody.getMotionState().getWorldTransform(Physics.TEMP_TRANSFORM); - - this.transform.x = Physics.TEMP_TRANSFORM.getOrigin().x(); - this.transform.y = Physics.TEMP_TRANSFORM.getOrigin().y(); - this.transform.z = Physics.TEMP_TRANSFORM.getOrigin().z(); - - let q = Quaternion.HELP_0; - q.set(Physics.TEMP_TRANSFORM.getRotation().x(), Physics.TEMP_TRANSFORM.getRotation().y(), Physics.TEMP_TRANSFORM.getRotation().z(), Physics.TEMP_TRANSFORM.getRotation().w()); - - this.object3D.transform.localRotQuat = q; - - Physics.checkBound(this); - } - } - - public destroy(force?: boolean): void { - Physics.removeRigidbody(this); - this._initedFunctions = null; - super.destroy(force); - } - - /** - * Get mass value。 - */ - public get mass(): number { - return this._mass; - } - /** - * Set mass value。 - */ - public set mass(value: number) { - this._mass = value; - if (this._btRigidbody) { - Physics.world.removeRigidBody(this._btRigidbody); - this.addAmmoRigidbody(); - // console.log("setMassProps", "mass: " + value, "flag: " + this._btRigidbody.getCollisionFlags()); - // this._btRigidbody.setMassProps(value, new Ammo.btVector3(0, 0, 0)); - // this._btRigidbody.setCollisionFlags(this._btRigidbody.getCollisionFlags()); - // console.log("setMassProps", "mass: " + value, "flag: " + this._btRigidbody.getCollisionFlags()); - // if(this.mass <= 0) { - // this._btRigidbody.setLinearVelocity(new Ammo.btVector3(0, 0, 0)); - // this._btRigidbody.setAngularVelocity(new Ammo.btVector3(0, 0, 0)); - // } - } - } - /** - * Get velocity value of current object - */ - public get velocity(): Vector3 { - return this._velocity; - } - /** - * Set velocity value of current object - */ - public set velocity(value: Vector3) { - this._velocity = value.clone(); - if (this._btRigidbody) { - this._btRigidbody.applyForce(new Ammo.btVector3(value.x, value.y, value.z), new Ammo.btVector3(0, 0, 0)); - } - } - /** - * Get the angular velocity value of current object - */ - public get angularVelocity(): Vector3 { - return this._angularVelocity; - } - - /** - * Set the angular velocity value of current object - */ - public set angularVelocity(value: Vector3) { - this._angularVelocity = value; - } - /** - * Check if the rigidbody affect physics system - */ - public get isKinematic(): boolean { - return this._isKinematic; - } - /** - * Set if the rigidbody affect physics system - */ - public set isKinematic(value: boolean) { - this._isKinematic = value; - } - - public get isTrigger(): boolean { - return this._isTrigger; - } - - public set isTrigger(value: boolean) { - this._isTrigger = value; - } -} diff --git a/packages/physics/constraint/ConeTwistConstraint.ts b/packages/physics/constraint/ConeTwistConstraint.ts new file mode 100644 index 00000000..0dc11079 --- /dev/null +++ b/packages/physics/constraint/ConeTwistConstraint.ts @@ -0,0 +1,83 @@ +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath' +import { ConstraintBase } from './ConstraintBase'; + +/** + * 锥形扭转约束 + */ +export class ConeTwistConstraint extends ConstraintBase { + private _twistSpan: number = Math.PI; + private _swingSpan1: number = Math.PI; + private _swingSpan2: number = Math.PI; + + /** + * 扭转角度限制,绕 X 轴的扭转范围。 + * 默认值 `Math.PI` + */ + public get twistSpan() { + return this._twistSpan; + } + public set twistSpan(value: number) { + this._twistSpan = value; + this._constraint?.setLimit(3, value); + } + + /** + * 摆动角度限制1,绕 Y 轴的摆动范围。 + * 默认值 `Math.PI` + */ + public get swingSpan1() { + return this._swingSpan1; + } + public set swingSpan1(value: number) { + this._swingSpan1 = value; + this._constraint?.setLimit(5, value); + } + + /** + * 摆动角度限制2,绕 Z 轴的摆动范围。 + * 默认值 `Math.PI` + */ + public get swingSpan2() { + return this._swingSpan2; + } + public set swingSpan2(value: number) { + this._swingSpan2 = value; + this._constraint?.setLimit(4, value); + } + + protected createConstraint(selfBody: Ammo.btRigidBody, targetBody: Ammo.btRigidBody | null) { + const frameInA = TempPhyMath.toBtVec(this.pivotSelf); + const rotInA = TempPhyMath.toBtQua(this.rotationSelf); + + const transformA = Physics.TEMP_TRANSFORM; + transformA.setIdentity(); + transformA.setOrigin(frameInA); + transformA.setRotation(rotInA); + + if (targetBody) { + const frameInB = TempPhyMath.toBtVec(this.pivotTarget, TempPhyMath.tmpVecB); + const rotInB = TempPhyMath.toBtQua(this.rotationTarget, TempPhyMath.tmpQuaB); + const transformB = new Ammo.btTransform(); + transformB.setIdentity(); + transformB.setOrigin(frameInB); + transformB.setRotation(rotInB); + + this._constraint = new Ammo.btConeTwistConstraint(selfBody, targetBody, transformA, transformB); + Ammo.destroy(transformB); + } else { + this._constraint = new Ammo.btConeTwistConstraint(selfBody, transformA); + } + + //******************************************** + //* 当前版本 Ammo 无法设置柔软度/偏差/松弛度 + //* 索引 3 是 m_twistSpan axe X + //* 索引 4 是 m_swingSpan2 axe Z + //* 索引 5 是 m_swingSpan1 axe Y + //******************************************** + + this._constraint.setLimit(3, this.twistSpan); + this._constraint.setLimit(5, this.swingSpan1); + this._constraint.setLimit(4, this.swingSpan2); + } +} diff --git a/packages/physics/constraint/ConstraintBase.ts b/packages/physics/constraint/ConstraintBase.ts new file mode 100644 index 00000000..b9d1df52 --- /dev/null +++ b/packages/physics/constraint/ConstraintBase.ts @@ -0,0 +1,133 @@ +import { ComponentBase, Vector3, Quaternion } from '@orillusion/core'; +import { Ammo, Physics } from '../Physics'; +import { Rigidbody } from '../rigidbody/Rigidbody'; + +/** + * 约束基类 + */ +export class ConstraintBase extends ComponentBase { + protected _targetRigidbody: Rigidbody; + protected _constraint: T; + private _initResolve!: () => void; + private _initializationPromise: Promise = new Promise(r => this._initResolve = r); + private _breakingThreshold: number; + + /** + * The pivot point for the self body + * `FrameInA Origin` + */ + public pivotSelf: Vector3 = new Vector3(); + /** + * The pivot point for the target body + * `FrameInB Origin` + */ + public pivotTarget: Vector3 = new Vector3(); + /** + * The rotation for the self body + * `FrameInA Rotation` + */ + public rotationSelf: Quaternion = new Quaternion(); + /** + * The rotation for the target body + * `FrameInB Rotation` + */ + public rotationTarget: Quaternion = new Quaternion(); + + public disableCollisionsBetweenLinkedBodies: boolean = true; + + /** + * 断裂脉冲阈值,值越大,约束越不易断裂。 + */ + public get breakingThreshold() { + return this._breakingThreshold; + } + + public set breakingThreshold(value: number) { + this._breakingThreshold = value; + this._constraint?.setBreakingImpulseThreshold(value); + } + + async start() { + const selfRb = this.object3D.getComponent(Rigidbody); + if (!selfRb) { + throw new Error(`${this.constructor.name} requires a rigidbody on the object.`); + } + + // 确保刚体初始化完成 + if (!selfRb.btBodyInited) { + await selfRb.wait() + } + + if (this._targetRigidbody && !this._targetRigidbody.btBodyInited) { + await this._targetRigidbody.wait() + } + + // 创建约束 + this.createConstraint(selfRb.btRigidbody, this._targetRigidbody?.btRigidbody); + + if (this._constraint) { + if (this._breakingThreshold != null) { + this._constraint.setBreakingImpulseThreshold(this._breakingThreshold); + } + Physics.world.addConstraint(this._constraint, this.disableCollisionsBetweenLinkedBodies); + this._initResolve(); + } + } + + /** + * 子类实现具体的约束创建逻辑 + * @param selfBody + * @param targetBody + */ + protected createConstraint(selfBody: Ammo.btRigidBody, targetBody: Ammo.btRigidBody | null) { } + + /** + * 获取约束实例 + */ + public get constraint(): T { + if (!this._constraint) { + console.warn('Constraint has not been initialized. Please use wait() to get the constraint instance asynchronously.'); + } + return this._constraint; + } + + /** + * 异步获取完成初始化的约束实例 + */ + public async wait(): Promise { + await this._initializationPromise; + return this._constraint!; + } + + /** + * 重置约束,销毁当前约束实例后重新创建并返回新的约束实例 + */ + public async resetConstraint(): Promise { + if (this._constraint) { + Physics.rigidBodyUtil.destroyConstraint(this._constraint) + this._constraint = null; + + await this.start(); + return this._constraint!; + } + console.warn('No constraint to reset.'); + } + + /** + * 目标刚体组件 + */ + public get targetRigidbody(): Rigidbody { + return this._targetRigidbody; + } + + public set targetRigidbody(value: Rigidbody) { + this._targetRigidbody = value; + } + + public destroy(force?: boolean): void { + Physics.rigidBodyUtil.destroyConstraint(this._constraint); + this._constraint = null; + this._targetRigidbody = null; + super.destroy(force); + } +} diff --git a/packages/physics/constraint/FixedConstraint.ts b/packages/physics/constraint/FixedConstraint.ts new file mode 100644 index 00000000..e31cc207 --- /dev/null +++ b/packages/physics/constraint/FixedConstraint.ts @@ -0,0 +1,31 @@ +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { ConstraintBase } from './ConstraintBase'; + +/** + * 固定约束 + */ +export class FixedConstraint extends ConstraintBase { + protected createConstraint(selfBody: Ammo.btRigidBody, targetBody: Ammo.btRigidBody | null): void { + if (!targetBody) throw new Error('FixedConstraint requires a target rigidbody'); + + const pivotInA = TempPhyMath.toBtVec(this.pivotSelf); + const rotInA = TempPhyMath.toBtQua(this.rotationSelf); + const frameInA = Physics.TEMP_TRANSFORM; + frameInA.setIdentity(); + frameInA.setOrigin(pivotInA); + frameInA.setRotation(rotInA); + + const pivotInB = TempPhyMath.toBtVec(this.pivotTarget, TempPhyMath.tmpVecB); + const rotInB = TempPhyMath.toBtQua(this.rotationTarget, TempPhyMath.tmpQuaB); + const frameInB = new Ammo.btTransform(); + frameInB.setIdentity(); + frameInB.setOrigin(pivotInB); + frameInB.setRotation(rotInB); + + this._constraint = new Ammo.btFixedConstraint(selfBody, targetBody, frameInA, frameInB); + Ammo.destroy(frameInB); + } + + +} \ No newline at end of file diff --git a/packages/physics/constraint/Generic6DofConstraint.ts b/packages/physics/constraint/Generic6DofConstraint.ts new file mode 100644 index 00000000..c10dd313 --- /dev/null +++ b/packages/physics/constraint/Generic6DofConstraint.ts @@ -0,0 +1,93 @@ +import { Vector3 } from '@orillusion/core'; +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { ConstraintBase } from './ConstraintBase'; + +/** + * 通用六自由度约束 + */ +export class Generic6DofConstraint extends ConstraintBase { + private _linearLowerLimit: Vector3 = new Vector3(-1e30, -1e30, -1e30); + private _linearUpperLimit: Vector3 = new Vector3(1e30, 1e30, 1e30); + private _angularLowerLimit: Vector3 = new Vector3(-Math.PI, -Math.PI, -Math.PI); + private _angularUpperLimit: Vector3 = new Vector3(Math.PI, Math.PI, Math.PI); + + /** + * default: `-1e30, -1e30, -1e30` + */ + public get linearLowerLimit(): Vector3 { + return this._linearLowerLimit; + } + public set linearLowerLimit(value: Vector3) { + this._linearLowerLimit.copyFrom(value); + this._constraint?.setLinearLowerLimit(TempPhyMath.toBtVec(value)); + } + + /** + * default: `1e30, 1e30, 1e30` + */ + public get linearUpperLimit(): Vector3 { + return this._linearUpperLimit; + } + public set linearUpperLimit(value: Vector3) { + this._linearUpperLimit.copyFrom(value); + this._constraint?.setLinearUpperLimit(TempPhyMath.toBtVec(value)); + } + + /** + * default: `-Math.PI, -Math.PI, -Math.PI` + */ + public get angularLowerLimit(): Vector3 { + return this._angularLowerLimit; + } + public set angularLowerLimit(value: Vector3) { + this._angularLowerLimit.copyFrom(value); + this._constraint?.setAngularLowerLimit(TempPhyMath.toBtVec(value)); + } + + /** + * default: `Math.PI, Math.PI, Math.PI` + */ + public get angularUpperLimit(): Vector3 { + return this._angularUpperLimit; + } + public set angularUpperLimit(value: Vector3) { + this._angularUpperLimit.copyFrom(value); + this._constraint?.setAngularUpperLimit(TempPhyMath.toBtVec(value)); + } + + /** + * 是否使用线性参考坐标系。 + * 默认值: `true` + */ + public useLinearFrameReferenceFrame: boolean = true; + + protected createConstraint(selfBody: Ammo.btRigidBody, targetBody: Ammo.btRigidBody | null): void { + const pivotInA = TempPhyMath.toBtVec(this.pivotSelf); + const rotInA = TempPhyMath.toBtQua(this.rotationSelf); + + const frameInA = Physics.TEMP_TRANSFORM; + frameInA.setIdentity(); + frameInA.setOrigin(pivotInA); + frameInA.setRotation(rotInA); + + if (targetBody) { + const pivotInB = TempPhyMath.toBtVec(this.pivotTarget, TempPhyMath.tmpVecB); + const rotInB = TempPhyMath.toBtQua(this.rotationTarget, TempPhyMath.tmpQuaB); + const frameInB = new Ammo.btTransform(); + frameInB.setIdentity(); + frameInB.setOrigin(pivotInB); + frameInB.setRotation(rotInB); + + this._constraint = new Ammo.btGeneric6DofConstraint(selfBody, targetBody, frameInA, frameInB, this.useLinearFrameReferenceFrame); + Ammo.destroy(frameInB); + } else { + this._constraint = new Ammo.btGeneric6DofConstraint(selfBody, frameInA, this.useLinearFrameReferenceFrame); + } + + this._constraint.setLinearLowerLimit(TempPhyMath.toBtVec(this._linearLowerLimit)); + this._constraint.setLinearUpperLimit(TempPhyMath.toBtVec(this._linearUpperLimit)); + this._constraint.setAngularLowerLimit(TempPhyMath.toBtVec(this._angularLowerLimit)); + this._constraint.setAngularUpperLimit(TempPhyMath.toBtVec(this._angularUpperLimit)); + } +} diff --git a/packages/physics/constraint/Generic6DofSpringConstraint.ts b/packages/physics/constraint/Generic6DofSpringConstraint.ts new file mode 100644 index 00000000..b15d401e --- /dev/null +++ b/packages/physics/constraint/Generic6DofSpringConstraint.ts @@ -0,0 +1,180 @@ +import { Vector3 } from '@orillusion/core'; +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { ConstraintBase } from './ConstraintBase'; + +/** + * 弹簧特性六自由度约束 + */ +export class Generic6DofSpringConstraint extends ConstraintBase { + private _linearLowerLimit: Vector3 = new Vector3(-1e30, -1e30, -1e30); + private _linearUpperLimit: Vector3 = new Vector3(1e30, 1e30, 1e30); + private _angularLowerLimit: Vector3 = new Vector3(-Math.PI, -Math.PI, -Math.PI); + private _angularUpperLimit: Vector3 = new Vector3(Math.PI, Math.PI, Math.PI); + + // 缓存约束配置参数 + private _springParams: { index: number, onOff: boolean }[] = []; + private _stiffnessParams: { index: number, stiffness: number }[] = []; + private _dampingParams: { index: number, damping: number }[] = []; + private _equilibriumPointParams: { index?: number, val?: number }[] = []; + + /** + * default: `-1e30, -1e30, -1e30` + */ + public get linearLowerLimit(): Vector3 { + return this._linearLowerLimit; + } + public set linearLowerLimit(value: Vector3) { + this._linearLowerLimit.copyFrom(value); + this._constraint?.setLinearLowerLimit(TempPhyMath.toBtVec(value)); + } + + /** + * default: `1e30, 1e30, 1e30` + */ + public get linearUpperLimit(): Vector3 { + return this._linearUpperLimit; + } + public set linearUpperLimit(value: Vector3) { + this._linearUpperLimit.copyFrom(value); + this._constraint?.setLinearUpperLimit(TempPhyMath.toBtVec(value)); + } + + /** + * default: `-Math.PI, -Math.PI, -Math.PI` + */ + public get angularLowerLimit(): Vector3 { + return this._angularLowerLimit; + } + public set angularLowerLimit(value: Vector3) { + this._angularLowerLimit.copyFrom(value); + this._constraint?.setAngularLowerLimit(TempPhyMath.toBtVec(value)); + } + + /** + * default: `Math.PI, Math.PI, Math.PI` + */ + public get angularUpperLimit(): Vector3 { + return this._angularUpperLimit; + } + public set angularUpperLimit(value: Vector3) { + this._angularUpperLimit.copyFrom(value); + this._constraint?.setAngularUpperLimit(TempPhyMath.toBtVec(value)); + } + + /** + * 启用或禁用弹簧功能。 + * @param index 弹簧的索引 + * @param onOff 是否启用 + */ + public enableSpring(index: number, onOff: boolean): void { + if (this._constraint) { + this._constraint.enableSpring(index, onOff); + } else { + this._springParams.push({ index, onOff }); + } + } + + /** + * 设置弹簧的刚度。 + * @param index 弹簧的索引 + * @param stiffness 刚度值 + */ + public setStiffness(index: number, stiffness: number): void { + if (this._constraint) { + this._constraint.setStiffness(index, stiffness); + } else { + this._stiffnessParams.push({ index, stiffness }); + } + } + + /** + * 设置弹簧的阻尼。 + * @param index 弹簧的索引 + * @param damping 阻尼值 + */ + public setDamping(index: number, damping: number): void { + if (this._constraint) { + this._constraint.setDamping(index, damping); + } else { + this._dampingParams.push({ index, damping }); + } + } + + /** + * 设置弹簧的平衡点。 + * + * @param index 弹簧的索引(可选)。如果不提供,则重置所有弹簧的平衡点。 + * @param val 平衡点值(可选)。如果提供,则设置指定弹簧的平衡点为该值。 + * + * - 不带参数时,重置所有弹簧的平衡点。 + * - 只带 `index` 参数时,设置指定弹簧的平衡点(值由系统内部处理)。 + * - 带 `index` 和 `val` 参数时,设置指定弹簧的平衡点为 `val`。 + */ + public setEquilibriumPoint(index?: number, val?: number): void { + if (this._constraint) { + if (index == undefined) { + this._constraint.setEquilibriumPoint(); + } else if (val == undefined) { + this._constraint.setEquilibriumPoint(index); + } else { + this._constraint.setEquilibriumPoint(index, val); + } + } else { + this._equilibriumPointParams.push({ index, val }); + } + } + + /** + * 是否使用线性参考坐标系。 + * 默认值 `true` + */ + public useLinearFrameReferenceFrame: boolean = true; + + protected createConstraint(selfBody: Ammo.btRigidBody, targetBody: Ammo.btRigidBody | null): void { + const pivotInA = TempPhyMath.toBtVec(this.pivotSelf); + const rotInA = TempPhyMath.toBtQua(this.rotationSelf); + + const frameInA = Physics.TEMP_TRANSFORM; + frameInA.setIdentity(); + frameInA.setOrigin(pivotInA); + frameInA.setRotation(rotInA); + + if (targetBody) { + const pivotInB = TempPhyMath.toBtVec(this.pivotTarget, TempPhyMath.tmpVecB); + const rotInB = TempPhyMath.toBtQua(this.rotationTarget, TempPhyMath.tmpQuaB); + const frameInB = new Ammo.btTransform(); + frameInB.setIdentity(); + frameInB.setOrigin(pivotInB); + frameInB.setRotation(rotInB); + + this._constraint = new Ammo.btGeneric6DofSpringConstraint(selfBody, targetBody, frameInA, frameInB, this.useLinearFrameReferenceFrame); + Ammo.destroy(frameInB); + } else { + this._constraint = new Ammo.btGeneric6DofSpringConstraint(selfBody, frameInA, this.useLinearFrameReferenceFrame); + } + + this.setConstraint() + } + + private setConstraint() { + + // 设置线性和角度限制 + this._constraint.setLinearLowerLimit(TempPhyMath.toBtVec(this._linearLowerLimit)); + this._constraint.setLinearUpperLimit(TempPhyMath.toBtVec(this._linearUpperLimit)); + this._constraint.setAngularLowerLimit(TempPhyMath.toBtVec(this._angularLowerLimit)); + this._constraint.setAngularUpperLimit(TempPhyMath.toBtVec(this._angularUpperLimit)); + + // 应用缓存的弹簧参数 + this._springParams.forEach(param => this._constraint.enableSpring(param.index, param.onOff)); + this._stiffnessParams.forEach(param => this._constraint.setStiffness(param.index, param.stiffness)); + this._dampingParams.forEach(param => this._constraint.setDamping(param.index, param.damping)); + this._equilibriumPointParams.forEach(param => this.setEquilibriumPoint(param.index, param.val)); + + // 清空缓存 + this._springParams = []; + this._stiffnessParams = []; + this._dampingParams = []; + this._equilibriumPointParams = []; + } +} diff --git a/packages/physics/constraint/HingeConstraint.ts b/packages/physics/constraint/HingeConstraint.ts new file mode 100644 index 00000000..3b315a7a --- /dev/null +++ b/packages/physics/constraint/HingeConstraint.ts @@ -0,0 +1,113 @@ +import { Vector3 } from '@orillusion/core'; +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { ConstraintBase } from './ConstraintBase'; + +/** + * 铰链约束 + */ +export class HingeConstraint extends ConstraintBase { + /** + * 自身刚体上的铰链轴方向。 + * 默认值 `Vector3.UP` + */ + public axisSelf: Vector3 = Vector3.UP; + /** + * 目标刚体上的铰链轴方向。 + * 默认值 `Vector3.UP` + */ + public axisTarget: Vector3 = Vector3.UP; + /** + * 是否使用自身刚体的参考框架。 + * 默认值 `true` + */ + public useReferenceFrameA: boolean = true; + /** + * 是否使用两个刚体的变换重载方式。 + * 如果为 true,则使用两个刚体的变换作为约束的参考框架。 + * 默认值 `false` + */ + public useTwoBodiesTransformOverload: boolean = false; + + private _pendingLimits: [number, number, number, number, number?]; + private _pendingMotorConfig: [boolean, number, number]; + + /** + * 获取当前的限制参数。 + */ + public get limitInfo() { return this._pendingLimits; } + /** + * 获取当前的马达配置参数。 + */ + public get motorConfigInfo() { return this._pendingMotorConfig; } + + /** + * 设置铰链约束的旋转限制。 + * @param low - 铰链旋转的最小角度(下限)。 + * @param high - 铰链旋转的最大角度(上限)。 + * @param softness - 软限制系数,表示限制的柔软程度。值在0到1之间,1表示完全刚性。 + * @param biasFactor - 偏置因子,用于控制限制恢复力的力度。值通常在0到1之间。 + * @param relaxationFactor -(可选)松弛因子,控制限制恢复的速度。值越大,恢复越快。 + */ + public setLimit(low: number, high: number, softness: number, biasFactor: number, relaxationFactor?: number): void { + this._pendingLimits = [low, high, softness, biasFactor, relaxationFactor]; + this._constraint?.setLimit(...this._pendingLimits); + }; + + /** + * 启用或禁用角度马达。 + * @param enableMotor - 是否启用马达。 + * @param targetVelocity - 马达的目标速度。 + * @param maxMotorImpulse - 马达的最大推力。 + */ + public enableAngularMotor(enableMotor: boolean, targetVelocity: number, maxMotorImpulse: number): void { + this._pendingMotorConfig = [enableMotor, targetVelocity, maxMotorImpulse] + this._constraint?.enableAngularMotor(...this._pendingMotorConfig) + }; + + protected createConstraint(selfBody: Ammo.btRigidBody, targetBody: Ammo.btRigidBody | null) { + const constraintType = !targetBody ? + 'SINGLE_BODY_TRANSFORM' : this.useTwoBodiesTransformOverload ? + 'TWO_BODIES_TRANSFORM' : 'TWO_BODIES_PIVOT'; + + const pivotInA = TempPhyMath.toBtVec(this.pivotSelf, TempPhyMath.tmpVecA); + const pivotInB = TempPhyMath.toBtVec(this.pivotTarget, TempPhyMath.tmpVecB); + + switch (constraintType) { + case 'SINGLE_BODY_TRANSFORM': + const frameA_single = Physics.TEMP_TRANSFORM; + frameA_single.setIdentity(); + frameA_single.setOrigin(pivotInA); + frameA_single.setRotation(TempPhyMath.toBtQua(this.rotationSelf)); + + this._constraint = new Ammo.btHingeConstraint(selfBody, frameA_single, this.useReferenceFrameA); + break; + case 'TWO_BODIES_TRANSFORM': + const frameA = Physics.TEMP_TRANSFORM; + frameA.setIdentity(); + frameA.setOrigin(pivotInA); + frameA.setRotation(TempPhyMath.toBtQua(this.rotationSelf)); + + const frameB = new Ammo.btTransform(); + frameB.setIdentity(); + frameB.setOrigin(pivotInB); + frameB.setRotation(TempPhyMath.toBtQua(this.rotationTarget, TempPhyMath.tmpQuaB)); + + this._constraint = new Ammo.btHingeConstraint(selfBody, targetBody, frameA, frameB, this.useReferenceFrameA); + Ammo.destroy(frameB); + break; + case 'TWO_BODIES_PIVOT': + const axisSelf = TempPhyMath.toBtVec(this.axisSelf, TempPhyMath.tmpVecC); + const axisTarget = TempPhyMath.toBtVec(this.axisTarget, TempPhyMath.tmpVecD); + + this._constraint = new Ammo.btHingeConstraint(selfBody, targetBody, pivotInA, pivotInB, axisSelf, axisTarget); + break; + default: + console.error('Invalid constraint type'); + return; + } + + this._pendingLimits && this.setLimit(...this._pendingLimits) + this._pendingMotorConfig && this.enableAngularMotor(...this._pendingMotorConfig) + } +} diff --git a/packages/physics/constraint/PointToPointConstraint.ts b/packages/physics/constraint/PointToPointConstraint.ts new file mode 100644 index 00000000..0d4ed466 --- /dev/null +++ b/packages/physics/constraint/PointToPointConstraint.ts @@ -0,0 +1,20 @@ +import { Ammo } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { ConstraintBase } from './ConstraintBase'; + +/** + * 点到点约束 + */ +export class PointToPointConstraint extends ConstraintBase { + protected createConstraint(selfBody: Ammo.btRigidBody, targetBody: Ammo.btRigidBody | null) { + const pivotInA = TempPhyMath.toBtVec(this.pivotSelf); + + if (targetBody) { + const pivotInB = TempPhyMath.toBtVec(this.pivotTarget, TempPhyMath.tmpVecB); + this._constraint = new Ammo.btPoint2PointConstraint(selfBody, targetBody, pivotInA, pivotInB); + } else { + this._constraint = new Ammo.btPoint2PointConstraint(selfBody, pivotInA); + } + + } +} diff --git a/packages/physics/constraint/SliderConstraint.ts b/packages/physics/constraint/SliderConstraint.ts new file mode 100644 index 00000000..8f288b98 --- /dev/null +++ b/packages/physics/constraint/SliderConstraint.ts @@ -0,0 +1,138 @@ +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { ConstraintBase } from './ConstraintBase'; + +/** + * 滑动关节约束 + */ +export class SliderConstraint extends ConstraintBase { + private _lowerLinLimit: number = -1e30; + private _upperLinLimit: number = 1e30; + private _lowerAngLimit: number = -Math.PI; + private _upperAngLimit: number = Math.PI; + private _poweredLinMotor: boolean = false; + private _maxLinMotorForce: number = 0; + private _targetLinMotorVelocity: number = 0; + + /** + * 是否使用线性参考框架。 + * 默认值 `true` + */ + public useLinearReferenceFrame: boolean = true; + + protected createConstraint(selfBody: Ammo.btRigidBody, targetBody: Ammo.btRigidBody | null): void { + const pivotInA = TempPhyMath.toBtVec(this.pivotSelf); + const rotInA = TempPhyMath.toBtQua(this.rotationSelf); + + const frameInA = Physics.TEMP_TRANSFORM; + frameInA.setIdentity(); + frameInA.setOrigin(pivotInA); + frameInA.setRotation(rotInA); + + if (targetBody) { + const pivotInB = TempPhyMath.toBtVec(this.pivotTarget, TempPhyMath.tmpVecB); + const rotInB = TempPhyMath.toBtQua(this.rotationTarget, TempPhyMath.tmpQuaB); + const frameInB = new Ammo.btTransform(); + frameInB.setIdentity(); + frameInB.setOrigin(pivotInB); + frameInB.setRotation(rotInB); + this._constraint = new Ammo.btSliderConstraint(selfBody, targetBody, frameInA, frameInB, this.useLinearReferenceFrame); + Ammo.destroy(frameInB); + } else { + this._constraint = new Ammo.btSliderConstraint(selfBody, frameInA, this.useLinearReferenceFrame); + } + + this._constraint.setLowerLinLimit(this._lowerLinLimit); + this._constraint.setUpperLinLimit(this._upperLinLimit); + this._constraint.setLowerAngLimit(this._lowerAngLimit); + this._constraint.setUpperAngLimit(this._upperAngLimit); + + this._constraint.setPoweredLinMotor(this._poweredLinMotor); + this._constraint.setMaxLinMotorForce(this._maxLinMotorForce); + this._constraint.setTargetLinMotorVelocity(this._targetLinMotorVelocity); + } + + /** + * 线性运动的下限限制。 + * 默认值 `-1e30` 表示无限制 + */ + public get lowerLinLimit(): number { + return this._lowerLinLimit; + } + public set lowerLinLimit(value: number) { + this._lowerLinLimit = value; + this._constraint?.setLowerLinLimit(value); + } + + /** + * 线性运动的上限限制。 + * 默认值 `1e30` 表示无限制 + */ + public get upperLinLimit(): number { + return this._upperLinLimit; + } + public set upperLinLimit(value: number) { + this._upperLinLimit = value; + this._constraint?.setUpperLinLimit(value); + } + + /** + * 角度运动的下限限制。 + * 默认值 `-Math.PI` + */ + public get lowerAngLimit(): number { + return this._lowerAngLimit; + } + public set lowerAngLimit(value: number) { + this._lowerAngLimit = value; + this._constraint?.setLowerAngLimit(value); + } + + /** + * 角度运动的上限限制。 + * 默认值 `Math.PI` + */ + public get upperAngLimit(): number { + return this._upperAngLimit; + } + public set upperAngLimit(value: number) { + this._upperAngLimit = value; + this._constraint?.setUpperAngLimit(value); + } + + /** + * 是否启用线性马达。 + * 默认值 `false` + */ + public get poweredLinMotor(): boolean { + return this._poweredLinMotor; + } + public set poweredLinMotor(value: boolean) { + this._poweredLinMotor = value; + this._constraint?.setPoweredLinMotor(value) + } + + /** + * 线性马达的最大推力。 + * 默认值 `0` + */ + public get maxLinMotorForce(): number { + return this._maxLinMotorForce; + } + public set maxLinMotorForce(value: number) { + this._maxLinMotorForce = value; + this._constraint?.setMaxLinMotorForce(value) + } + + /** + * 线性马达的目标速度。 + * 默认值 `0` + */ + public get targetLinMotorVelocity(): number { + return this._targetLinMotorVelocity; + } + public set targetLinMotorVelocity(value: number) { + this._targetLinMotorVelocity = value; + this._constraint?.setTargetLinMotorVelocity(value) + } +} diff --git a/packages/physics/index.ts b/packages/physics/index.ts index 190efb56..bbc2398f 100644 --- a/packages/physics/index.ts +++ b/packages/physics/index.ts @@ -1,4 +1,20 @@ -export * from './Physics' -export * from './Rigidbody' -export * from './ClothSoftBody' -export * from './HingeConstraint' \ No newline at end of file +export * from './utils/CollisionShapeUtil'; +export * from './utils/ContactProcessedUtil'; +export * from './utils/RigidBodyUtil'; +export * from './utils/RigidBodyMapping'; +export * from './utils/TempPhyMath'; +export * from './utils/PhysicsDragger'; +export * from './Physics'; +export * from './visualDebug/DebugDrawModeEnum'; +export * from './rigidbody/RigidbodyEnum'; +export * from './rigidbody/Rigidbody'; +export * from './rigidbody/GhostTrigger'; +export * from './softbody/ClothSoftbody'; +export * from './softbody/RopeSoftbody'; +export * from './constraint/ConeTwistConstraint'; +export * from './constraint/FixedConstraint'; +export * from './constraint/Generic6DofConstraint'; +export * from './constraint/Generic6DofSpringConstraint'; +export * from './constraint/HingeConstraint'; +export * from './constraint/PointToPointConstraint'; +export * from './constraint/SliderConstraint'; \ No newline at end of file diff --git a/packages/physics/package.json b/packages/physics/package.json index e9ff90bf..794debcb 100644 --- a/packages/physics/package.json +++ b/packages/physics/package.json @@ -1,6 +1,6 @@ { "name": "@orillusion/physics", - "version": "0.2.2", + "version": "0.3.4", "author": "Orillusion", "description": "Orillusion Physics Plugin, Powerd by Ammo.js", "main": "./dist/physics.umd.js", @@ -11,7 +11,7 @@ "scripts": { "build": "vite build && npm run build:types && npm run build:clean", "build:types": "tsc --emitDeclarationOnly -p tsconfig.json", - "build:clean": "mv dist/packages/physics/* dist && rm -rf dist/src & rm -rf dist/packages", + "build:clean": "mv dist/packages/physics/* dist && rm -rf dist/src && rm -rf dist/packages", "docs": "npm run docs:typedoc ../../docs/physics index.ts", "docs:typedoc": "npx typedoc --plugin typedoc-plugin-markdown --tsconfig tsconfig.json --gitRevision main --hideBreadcrumbs true --allReflectionsHaveOwnDocument true --readme none --excludeInternal --excludePrivate --excludeProtected --sort source-order --out" }, @@ -21,7 +21,9 @@ "url": "git+https://github.com/Orillusion/orillusion.git" }, "dependencies": { - "@orillusion/ammo": "^0.2.0", - "@orillusion/core": "^0.7.0" + "@orillusion/ammo": ">=0.2.1" + }, + "peerDependencies": { + "@orillusion/core": ">=0.8.0" } } diff --git a/packages/physics/rigidbody/GhostTrigger.ts b/packages/physics/rigidbody/GhostTrigger.ts new file mode 100644 index 00000000..43797101 --- /dev/null +++ b/packages/physics/rigidbody/GhostTrigger.ts @@ -0,0 +1,178 @@ +import { ComponentBase, Vector3 } from '@orillusion/core'; +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { CollisionEventHandler } from './RigidbodyExpansion' +import { CollisionFlags } from './RigidbodyEnum'; + +/** + * The GhostTrigger Component represents a non-physical trigger in the physics world. + * It uses a ghost object to detect overlapping collisions without producing physical responses. + */ +export class GhostTrigger extends ComponentBase { + private _initResolve!: () => void; + private _initializationPromise: Promise = new Promise(r => this._initResolve = r); + private _ghostObject: Ammo.btPairCachingGhostObject; + private _userIndex: number; + private _shape: Ammo.btCollisionShape; + private collisionEventHandler: CollisionEventHandler = new CollisionEventHandler(); + + public get shape() { + return this._shape + } + public set shape(value: Ammo.btCollisionShape) { + this._shape = value; + if (this._ghostObject) { + Ammo.destroy(this._ghostObject.getCollisionShape()); + this._ghostObject.setCollisionShape(value); + // Physics.world.updateSingleAabb(this._ghostObject); // 更新世界中的碰撞体状态 + } + } + + public get userIndex() { + return this._userIndex; + } + + public set userIndex(value: number) { + this._userIndex = value; + this._ghostObject?.setUserIndex(value) + } + + private _collisionFlags: CollisionFlags = CollisionFlags.NO_CONTACT_RESPONSE; + + /** + * 获取碰撞标志 + */ + public get collisionFlags(): number { + return this._ghostObject?.getCollisionFlags() ?? this._collisionFlags; + } + + /** + * 添加单个碰撞标志 + */ + public addCollisionFlag(value: CollisionFlags) { + this._collisionFlags = this.collisionFlags | value; + this._ghostObject?.setCollisionFlags(this._collisionFlags); + } + /** + * 删除单个碰撞标志 + */ + public removeCollisionFlag(value: CollisionFlags) { + this._collisionFlags = this.collisionFlags & ~value; + this._ghostObject?.setCollisionFlags(this._collisionFlags); + } + + async start() { + + if (!this._shape) throw new Error('Ghost object need collision shape') + + let position = this.object3D.localPosition; + let rotation = this.object3D.localRotation; + + this._ghostObject = GhostTrigger.createAndAddGhostObject(this._shape, position, rotation, this._collisionFlags, this._userIndex); + + + // 变换更新,确保三维对象更新变换时同步到幽灵对象 + this.transform.onPositionChange = (oldValue: Vector3, newValue: Vector3) => { + newValue ||= this.transform.localPosition; + this._ghostObject.getWorldTransform().setOrigin(TempPhyMath.toBtVec(newValue)) + }; + this.transform.onRotationChange = (oldValue: Vector3, newValue: Vector3) => { + newValue ||= this.transform.localRotation; + this._ghostObject.getWorldTransform().setRotation(TempPhyMath.eulerToBtQua(newValue)) + }; + this.transform.onScaleChange = (oldValue: Vector3, newValue: Vector3) => { + newValue ||= this.transform.localScale; + this._shape.setLocalScaling(TempPhyMath.toBtVec(newValue)) + }; + + this._initResolve(); + + this.collisionEventHandler.configure(Ammo.getPointer(this._ghostObject)); + + } + + /** + * 创建幽灵对象并添加到物理世界。 + * @param shape - 碰撞形状。 + * @param position - 幽灵对象的位置。 + * @param rotation - 幽灵对象的旋转。 + * @param collisionFlags - 可选参数,碰撞标志,默认值为 4 `NO_CONTACT_RESPONSE` 表示对象不参与碰撞响应,但仍会触发碰撞事件。 + * @param userIndex - 可选参数,用户索引,可作为物理对象标识。 + * @returns 新创建的 Ammo.btPairCachingGhostObject 对象。 + */ + public static createAndAddGhostObject(shape: Ammo.btCollisionShape, position: Vector3, rotation: Vector3, collisionFlags?: number, userIndex?: number) { + let ghostObject = new Ammo.btPairCachingGhostObject(); + let transform = Physics.TEMP_TRANSFORM; + transform.setIdentity(); + transform.setOrigin(TempPhyMath.toBtVec(position)); + transform.setRotation(TempPhyMath.eulerToBtQua(rotation)); + ghostObject.setWorldTransform(transform); + + // 设置形状和属性 + ghostObject.setCollisionShape(shape); + collisionFlags ??= CollisionFlags.NO_CONTACT_RESPONSE; + ghostObject.setCollisionFlags(ghostObject.getCollisionFlags() | collisionFlags); + + if (userIndex != null) { + ghostObject.setUserIndex(userIndex); + } + + // 将 Ghost Object 添加到物理世界 + Physics.world.addCollisionObject(ghostObject); + + return ghostObject; + } + + /** + * 获取幽灵对象 + */ + public get ghostObject() { + return this._ghostObject; + } + + /** + * 异步获取完成初始化的幽灵对象 + */ + public async wait() { + await this._initializationPromise; + return this._ghostObject!; + } + + /** + * 启用/禁用碰撞回调 + */ + public get enableCollisionEvent(): boolean { + return this.collisionEventHandler.enableCollisionEvent; + } + public set enableCollisionEvent(value: boolean) { + this.collisionEventHandler.enableCollisionEvent = value; + + } + + /** + * 碰撞事件回调 + */ + public get collisionEvent() { + return this.collisionEventHandler.collisionEvent; + } + public set collisionEvent(callback: (contactPoint: Ammo.btManifoldPoint, selfBody: Ammo.btRigidBody, otherBody: Ammo.btRigidBody) => void) { + this.collisionEventHandler.collisionEvent = callback; + } + + public destroy(force?: boolean): void { + if (this._ghostObject) { + Physics.world.removeCollisionObject(this._ghostObject); + Ammo.destroy(this._ghostObject.getCollisionShape()); + Ammo.destroy(this._ghostObject); + this._ghostObject = null; + } + + this._shape = null; + this.transform.onPositionChange = null; + this.transform.onRotationChange = null; + this.transform.onScaleChange = null; + this.collisionEventHandler.destroy(); + + super.destroy(force); + } +} diff --git a/packages/physics/rigidbody/Rigidbody.ts b/packages/physics/rigidbody/Rigidbody.ts new file mode 100644 index 00000000..fc8cbe4c --- /dev/null +++ b/packages/physics/rigidbody/Rigidbody.ts @@ -0,0 +1,600 @@ +import { Vector3, BoxColliderShape, CapsuleColliderShape, ColliderComponent, ComponentBase, Quaternion, SphereColliderShape } from '@orillusion/core' +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { ActivationState, CollisionFlags } from './RigidbodyEnum'; +import { PhysicsTransformSync, CollisionEventHandler } from './RigidbodyExpansion' +import { CollisionShapeUtil } from '../utils/CollisionShapeUtil'; +import { ContactProcessedUtil } from '../utils/ContactProcessedUtil'; +import { RigidBodyUtil } from '../utils/RigidBodyUtil'; + +/** + * Rigidbody Component + * Rigid bodies can endow game objects with physical properties, allowing them to be controlled by the physics system and subjected to forces and torques, thus achieving realistic motion effects. + * @group Components + */ +export class Rigidbody extends ComponentBase { + private _initResolve!: () => void; + private _initializationPromise: Promise = new Promise(r => this._initResolve = r); + private _btBodyInited: boolean = false; + private _btRigidbody: Ammo.btRigidBody; + private _shape: Ammo.btCollisionShape; + private _mass: number = 0.01; + private _margin: number = 0.02; + private _velocity: Vector3 = new Vector3(); + private _angularVelocity: Vector3 = new Vector3(); + private _linearVelocity: Vector3 = new Vector3(); + private _gravity: Vector3 = Physics.gravity.clone(); + private _restitution: number = 0.5; // 低恢复系数以减少弹跳 + private _friction: number = 0.5; // 高摩擦系数以防止滑动 + private _rollingFriction: number; + private _contactProcessingThreshold: number; // 接触处理阈值 值越小,精度越高 + private _damping: [number, number]; + private _ccdSettings: [number, number]; + private _activationState: ActivationState; + private _collisionFlags: CollisionFlags; // Default static: 1, dynamic: 0 + private _userIndex: number; + private _isSilent: boolean = false; + + private collisionEventHandler: CollisionEventHandler; + private physicsTransformSync: PhysicsTransformSync; + public static readonly collisionShape = CollisionShapeUtil; + + init() { + this.physicsTransformSync = new PhysicsTransformSync(this.transform); + this.collisionEventHandler = new CollisionEventHandler(); + } + + public start(): void { + + this.initRigidbody(); + + this.physicsTransformSync.configure(this._btRigidbody, this._mass); + this.collisionEventHandler.configure(this._btRigidbody.kB); + + this._isSilent && ContactProcessedUtil.addIgnoredPointer(this._btRigidbody.kB); + + this._btBodyInited = true; + this._initResolve(); + } + + private initRigidbody(): void { + // 如果未传入形状则应用碰撞组件的形状与参数构建碰撞体 + if (!this._shape) this._shape = this.createColliderComponentShape(); + + let position: Vector3 = this.object3D.localPosition; + + // 处理特殊形状 高度场地形 + if (this._shape instanceof Ammo.btHeightfieldTerrainShape) { + // averageHeight 是碰撞体对象中自定义的属性,应用该值以调整刚体的位置 + position = position.clone(); + position.y += (this._shape as any)?.averageHeight || 0; + } + + this._shape.setMargin(this._margin); + + this._btRigidbody = RigidBodyUtil.createRigidBody(this.object3D, this._shape, this._mass, position); + + // 刚体配置信息 + this._btRigidbody.setRestitution(this._restitution); + this._btRigidbody.setFriction(this._friction); + + if (this._rollingFriction != null) { + this._btRigidbody.setRollingFriction(this._rollingFriction); + } + + if (this._damping != null) { + this._btRigidbody.setDamping(...this._damping); + } + + if (this._userIndex != null) { + this._btRigidbody.setUserIndex(this._userIndex); + } + + if (this._activationState != null) { + this._btRigidbody.setActivationState(this._activationState); + } + + if (this._contactProcessingThreshold != null) { + this._btRigidbody.setContactProcessingThreshold(this._contactProcessingThreshold); + } + + if (this._collisionFlags != null) { + this._btRigidbody.setCollisionFlags(this._collisionFlags); + } + + if (this.group != null && this.mask != null) { + Physics.world.addRigidBody(this._btRigidbody, this.group, this.mask); + } else { + Physics.world.addRigidBody(this._btRigidbody); + } + + // The gravity setting is done after the rigid body is added to the physical world. + if (!this._gravity.equals(Physics.gravity)) { + this._btRigidbody.setGravity(TempPhyMath.toBtVec(this._gravity)); + } + + // Continuous Collision Detection + if (this._ccdSettings != null) { + this._btRigidbody.setCcdMotionThreshold(this._ccdSettings[0]); + this._btRigidbody.setCcdSweptSphereRadius(this._ccdSettings[1]); + } + } + + public onUpdate(): void { + // Check if the rigid body is active in the physics simulation. + if (this._btRigidbody?.isActive()) { + // Retrieve the current interpolated world transform of the rigid body from its motion state. + // The motion state provides an interpolated transformation, which is smoother and more suitable + this._btRigidbody.getMotionState().getWorldTransform(Physics.TEMP_TRANSFORM); + let pos = Physics.TEMP_TRANSFORM.getOrigin(); + let qua = Physics.TEMP_TRANSFORM.getRotation(); + + // When physicsTransformSync is enabled, setting isUpdatingFromPhysics to true + // prevents the 3D object's onChange event from reacting to this automatic update. + // This is used to distinguish between automatic synchronization and manual transformations. + this.physicsTransformSync.isUpdatingFromPhysics = true; + + // Synchronize the position and rotation of the 3D object with the rigid body's transform. + this.transform.localPosition = Vector3.HELP_0.set(pos.x(), pos.y(), pos.z()); + this.transform.localRotQuat = Quaternion.HELP_0.set(qua.x(), qua.y(), qua.z(), qua.w()); + + // Re-enable onChange event handling for manual transformations. + this.physicsTransformSync.isUpdatingFromPhysics = false; + + Physics.checkBound(this); + } + } + + private createColliderComponentShape(): Ammo.btCollisionShape { + let collider = this.object3D.getComponent(ColliderComponent); + if (!collider) throw new Error("Rigid bodies need collision shape"); + + let colliderShape = collider.shape; + let shape: Ammo.btCollisionShape; + if (colliderShape instanceof BoxColliderShape) { + shape = new Ammo.btBoxShape(TempPhyMath.toBtVec(colliderShape.halfSize)); + } else if (colliderShape instanceof CapsuleColliderShape) { + shape = new Ammo.btCapsuleShape(colliderShape.radius, colliderShape.height); + } else if (colliderShape instanceof SphereColliderShape) { + shape = new Ammo.btSphereShape(colliderShape.radius); + } else { + throw new Error("Wrong collision shape"); + } + + return shape; + } + + /** + * 更新刚体的位置和旋转,并同步三维对象 + * @param position 可选,默认为三维对象的位置 + * @param rotation 可选,默认为三维对象的欧拉角旋转 + * @param clearFV 可选,清除刚体的力和速度,默认为 false + */ + public updateTransform(position?: Vector3, rotation?: Vector3 | Quaternion, clearFV?: boolean): void { + if (!this._btRigidbody) return; + position ||= this.transform.localPosition; + rotation ||= this.transform.localRotation; + RigidBodyUtil.updateTransform(this._btRigidbody, position, rotation, clearFV); + Physics.syncGraphic(this.object3D, this._btRigidbody.getWorldTransform()); + } + + /** + * Remove the force and velocity of the rigid body + */ + public clearForcesAndVelocities() { + if (this._btRigidbody) { + RigidBodyUtil.clearForcesAndVelocities(this._btRigidbody); + } + } + + /** + * Check if rigidbody inited + */ + public get btBodyInited(): boolean { + return this._btBodyInited; + } + + /** + * Return internal Ammo.btRigidBody + */ + public get btRigidbody(): Ammo.btRigidBody { + return this._btRigidbody; + } + /** + * Asynchronously retrieves the fully initialized rigid body instance. + */ + public async wait(): Promise { + await this._initializationPromise; + return this._btRigidbody!; + } + + /** + * The collision shape of the rigid body. + */ + public get shape() { + return this._shape; + } + public set shape(value: Ammo.btCollisionShape) { + this._shape = value; + if (this._btRigidbody) { + Ammo.destroy(this._btRigidbody.getCollisionShape()); + this._btRigidbody.setCollisionShape(value); + // Physics.world.updateSingleAabb(this._btRigidbody); // 更新世界中的碰撞体状态 + // 对于高度场形状,需要调整其刚体的位置以匹配三维对象 + if (value instanceof Ammo.btHeightfieldTerrainShape) { + this._btRigidbody.getWorldTransform().getOrigin().setY((value as any).averageHeight + this.object3D.y) + } + } + } + + /** + * The collision group of the rigid body. + */ + public group: number; + + /** + * The collision mask of the rigid body. + */ + public mask: number; + + /** + * User index, which can be used as an identifier for the rigid body. + */ + public get userIndex() { + return this._userIndex + } + + /** + * Sets the user index for the rigid body. + */ + public set userIndex(value: number) { + this._userIndex = value; + this._btRigidbody?.setUserIndex(value); + } + + /** + * Activation state of the rigid body. + */ + public get activationState() { + return this._activationState; + } + + /** + * Sets the activation state of the rigid body. + */ + public set activationState(value: ActivationState) { + this._activationState = value; + this._btRigidbody?.setActivationState(value); + } + + /** + * Collision flags of the rigid body. + */ + public get collisionFlags(): number { + return this._btRigidbody?.getCollisionFlags() ?? (this.mass === 0 ? 1 : 0); + } + + /** + * Adds a collision flag to the rigid body. + */ + public addCollisionFlag(value: CollisionFlags) { + this._collisionFlags = this.collisionFlags | value; + this._btRigidbody?.setCollisionFlags(this._collisionFlags); + } + /** + * Removes a collision flag from the rigid body. + */ + public removeCollisionFlag(value: CollisionFlags) { + this._collisionFlags = this.collisionFlags & ~value; + this._btRigidbody?.setCollisionFlags(this._collisionFlags); + } + + /** + * Check if the rigidbody affect physics system + */ + public get isKinematic(): boolean { + return Boolean(this._btRigidbody?.isKinematicObject()); + } + /** + * Set the rigid body to a kinematic object + */ + public set isKinematic(value: boolean) { + if (value === this.isKinematic) return; + let flag = CollisionFlags.KINEMATIC_OBJECT; + value ? this.addCollisionFlag(flag) : this.removeCollisionFlag(flag); + if (!this._btRigidbody) return; + this.enablePhysicsTransformSync = value; + + if (value) { + // pause onUpdate + this.enable = false + this._btRigidbody.setActivationState(ActivationState.DISABLE_DEACTIVATION) + // sync transfrom + this.updateTransform(); + } else { + // resume onUpdate + this.enable = true + const state = this._activationState ?? ((this._btRigidbody.isStaticObject() + ? ActivationState.ISLAND_SLEEPING + : ActivationState.ACTIVE_TAG)); + this._btRigidbody.forceActivationState(state); + this._btRigidbody.activate() + } + } + /** + * Check if the rigid body is a trigger + */ + public get isTrigger(): boolean { + return (this.collisionFlags & CollisionFlags.NO_CONTACT_RESPONSE) !== 0; + } + /** + * Set the rigid body as a trigger + */ + public set isTrigger(value: boolean) { + let flag = CollisionFlags.NO_CONTACT_RESPONSE; + value ? this.addCollisionFlag(flag) : this.removeCollisionFlag(flag); + } + /** + * Check if the rigid body is visible in debug mode + */ + public get isDisableDebugVisible(): boolean { + return (this.collisionFlags & CollisionFlags.DISABLE_VISUALIZE_OBJECT) !== 0; + } + /** + * Set the rigid body to be visible in debug mode + */ + public set isDisableDebugVisible(value: boolean) { + let flag = CollisionFlags.DISABLE_VISUALIZE_OBJECT; + value ? this.addCollisionFlag(flag) : this.removeCollisionFlag(flag); + } + /** + * Margin of the collision shape. + */ + public get margin() { + return this._margin; + } + /** + * Sets the margin of the collision shape. + * @default 0.02 + */ + public set margin(value: number) { + this._margin = value; + this._shape?.setMargin(value) + } + + /** + * Damping of the rigid body. + * + * Sets the damping parameters. The first value is the linear damping, the second is the angular damping. + * @param params - [linear damping, angular damping] + */ + public get damping(): [number, number] { + return this._damping; + } + + public set damping(params: [number, number]) { + this._damping = [params[0], params[1]]; + this._btRigidbody?.setDamping(...params); + } + /** + * Contact processing threshold of the rigid body. + */ + public get contactProcessingThreshold() { + return this._contactProcessingThreshold + } + /** + * Sets the contact processing threshold of the rigid body. + */ + public set contactProcessingThreshold(value: number) { + this._contactProcessingThreshold = value; + this._btRigidbody?.setContactProcessingThreshold(value) + } + /** + * Gravity vector applied to the rigid body. + */ + public get gravity() { + return this._gravity + } + /** + * Sets the gravity vector applied to the rigid body. + */ + public set gravity(value: Vector3) { + this._gravity.copyFrom(value); + this._btRigidbody?.setGravity(TempPhyMath.toBtVec(value)); + } + /** + * Get friction value + */ + public get friction() { + return this._friction; + } + /** + * Set friction value. default `0.5` + */ + public set friction(value: number) { + this._friction = value; + this._btRigidbody?.setFriction(value); + } + /** + * Get rolling friction value + */ + public get rollingFriction(): number { + return this._rollingFriction; + } + /** + * Set rolling friction value + */ + public set rollingFriction(value: number) { + this._rollingFriction = value; + this._btRigidbody?.setRollingFriction(value); + } + /** + * Get restitution value + */ + public get restitution(): number { + return this._restitution; + } + /** + * Set restitution value default `0.5` + */ + public set restitution(value: number) { + this._restitution = value; + this._btRigidbody?.setRestitution(value); + } + /** + * Get velocity value of current object + */ + public get velocity(): Vector3 { + return this._velocity; + } + /** + * Set velocity value of current object + */ + public set velocity(value: Vector3) { + this._velocity.copyFrom(value); + this.wait().then(rb => rb.applyForce(TempPhyMath.toBtVec(this._velocity), TempPhyMath.zeroBtVec(TempPhyMath.tmpVecB))); + } + + /** + * Get the angular velocity value of current object + */ + public get angularVelocity(): Vector3 { + if (this._btBodyInited) { + return TempPhyMath.fromBtVec(this._btRigidbody.getAngularVelocity(), this._angularVelocity); + } + return this._angularVelocity; + } + /** + * Set the angular velocity value of current object + */ + public set angularVelocity(value: Vector3) { + this._angularVelocity.copyFrom(value) + this.wait().then(rb => rb.setAngularVelocity(TempPhyMath.toBtVec(this._angularVelocity))); + } + /** + * Get the linear velocity value of current object + */ + public get linearVelocity(): Vector3 { + if (this._btBodyInited) { + return TempPhyMath.fromBtVec(this._btRigidbody.getLinearVelocity(), this._linearVelocity); + } + return this._linearVelocity; + } + /** + * Set the linear velocity value of current object + */ + public set linearVelocity(value: Vector3) { + this._linearVelocity.copyFrom(value) + this.wait().then(rb => rb.setLinearVelocity(TempPhyMath.toBtVec(this._linearVelocity))); + } + /** + * Get mass value + */ + public get mass(): number { + return this._mass; + } + /** + * Set mass value. default `0.01` + */ + public set mass(value: number) { + const oldMass = this._mass; + this._mass = value; + if (this._btRigidbody && oldMass !== value) { + if (oldMass === 0 || value === 0) { + ContactProcessedUtil.removeIgnoredPointer(this._btRigidbody.kB); // 指针将会无效,从静默状态表中移除 + Physics.world.removeRigidBody(this._btRigidbody); // 删除刚体 + this.initRigidbody(); // 重新创建刚体 + this.collisionEventHandler.configure(this._btRigidbody.kB); + this._isSilent && ContactProcessedUtil.addIgnoredPointer(this._btRigidbody.kB); + + } else { + // 根据碰撞形状计算新的惯性进行更新 + const localInertia = TempPhyMath.zeroBtVec(); + this._btRigidbody.getCollisionShape().calculateLocalInertia(value, localInertia); + this._btRigidbody.setMassProps(value, localInertia); + this._btRigidbody.updateInertiaTensor(); + this.clearForcesAndVelocities(); + } + this.physicsTransformSync.configure(this._btRigidbody, value); + } + } + + /** + * 刚体的静默状态。 + * 如果为 true 则任何物理对象与静默状态的对象发生碰撞时都不会触发双方的碰撞回调。 + */ + public get isSilent(): boolean { + return this._isSilent; + } + public set isSilent(value: boolean) { + this._isSilent = value; + if (value) { + ContactProcessedUtil.addIgnoredPointer(this._btRigidbody?.kB) + } else { + ContactProcessedUtil.removeIgnoredPointer(this._btRigidbody?.kB) + } + } + + /** + * CCD (Continuous Collision Detection) + * + * Sets the CCD parameters. The first value is the motion threshold, the second is the swept sphere radius. + * @param params - [motion threshold, swept sphere radius] + */ + public set ccdSettings(params: [number, number]) { + this._ccdSettings = [params[0], params[1]]; + this._btRigidbody?.setCcdMotionThreshold(params[0]); + this._btRigidbody?.setCcdSweptSphereRadius(params[1]); + } + + public get ccdSettings(): [number, number] { + return this._ccdSettings; + } + + /** + * Enable/disable collision callbacks + */ + public get enableCollisionEvent(): boolean { + return this.collisionEventHandler.enableCollisionEvent; + } + public set enableCollisionEvent(value: boolean) { + this.collisionEventHandler.enableCollisionEvent = value; + + } + + /** + * Collision callbacks + */ + public get collisionEvent() { + return this.collisionEventHandler.collisionEvent; + } + public set collisionEvent(callback: (contactPoint: Ammo.btManifoldPoint, selfBody: Ammo.btRigidBody, otherBody: Ammo.btRigidBody) => void) { + this.collisionEventHandler.collisionEvent = callback; + } + + /** + * Enables or disables the transform sync with physics. + * If enabled, changes to the transform will automatically update the physics body. + */ + public get enablePhysicsTransformSync() { + return this.physicsTransformSync.enablePhysicsTransformSync; + } + public set enablePhysicsTransformSync(value: boolean) { + if (this.isDestroyed) return; + this.physicsTransformSync.enablePhysicsTransformSync = value; + } + + public destroy(force?: boolean): void { + if (this._btRigidbody) { + ContactProcessedUtil.removeIgnoredPointer(this._btRigidbody.kB); + RigidBodyUtil.destroyRigidBody(this._btRigidbody) + } + this._btRigidbody = null; + this._btBodyInited = false; + this._shape = null; + + this.physicsTransformSync.destroy() + this.collisionEventHandler.destroy() + super.destroy(force); + } +} diff --git a/packages/physics/rigidbody/RigidbodyEnum.ts b/packages/physics/rigidbody/RigidbodyEnum.ts new file mode 100644 index 00000000..cdbe6800 --- /dev/null +++ b/packages/physics/rigidbody/RigidbodyEnum.ts @@ -0,0 +1,79 @@ +/** + * Collision flags + */ +export enum CollisionFlags { + /** + * Default flag for dynamic rigid bodies. + */ + DEFAULT = 0, + /** + * Used for static objects. These objects do not move but can be collided with by other objects. + */ + STATIC_OBJECT = 1, + /** + * Used for kinematic objects. These objects are not affected by physical forces (like gravity or collisions) but can be moved programmatically and affect dynamic objects they collide with. + */ + KINEMATIC_OBJECT = 2, + /** + * Objects with this flag do not participate in collision response but still trigger collision events. + */ + NO_CONTACT_RESPONSE = 4, + /** + * This flag indicates that the object will use a custom material interaction callback. + */ + CUSTOM_MATERIAL_CALLBACK = 8, + /** + * Special flag for collision objects used by character controllers. This is typically used to optimize character collision handling in games. + */ + CHARACTER_OBJECT = 16, + /** + * Prevents this object from being displayed in the physical debug view. + */ + DISABLE_VISUALIZE_OBJECT = 32, + /** + * Prevents this object’s collision from being processed on the auxiliary processing unit, optimizing performance on specific hardware platforms. + */ + DISABLE_SPU_COLLISION_PROCESSING = 64, + /** + * Enables custom contact stiffness and damping settings for this object. This allows adjusting the physical response's stiffness and damping when handling collisions, used to simulate more complex physical interactions. + */ + HAS_CONTACT_STIFFNESS_DAMPING = 128, + /** + * Allows specifying a custom rendering color for this object in the physical debug view. This helps differentiate and identify specific physical objects during debugging. + */ + HAS_CUSTOM_DEBUG_RENDERING_COLOR = 256, + /** + * Enables friction anchors for this object. Friction anchors improve the friction effect on contact surfaces, typically used for vehicle tires to enhance grip on the ground and reduce sliding. + */ + HAS_FRICTION_ANCHOR = 512, + /** + * Triggers sound effects when this object collides. This flag can be used to configure sound feedback for specific collisions, enhancing the realism and immersion of the game or simulation environment. + */ + HAS_COLLISION_SOUND_TRIGGER = 1024, +} + +/** + * Activation states + */ +export enum ActivationState { + /** + * The object is active and will be processed by the simulation. + */ + ACTIVE_TAG = 1, + /** + * The object is inactive but may be activated if other active objects collide with it. + */ + ISLAND_SLEEPING = 2, + /** + * The object is requesting to be deactivated in the next simulation step. If there is no further interaction, the object will enter a sleeping state. + */ + WANTS_DEACTIVATION = 3, + /** + * Disables automatic sleeping. The object will continue to be simulated even if it is stationary. + */ + DISABLE_DEACTIVATION = 4, + /** + * The object will not be simulated by the physics engine, whether dynamic or colliding, but can be moved or manipulated programmatically. + */ + DISABLE_SIMULATION = 5, +} diff --git a/packages/physics/rigidbody/RigidbodyExpansion.ts b/packages/physics/rigidbody/RigidbodyExpansion.ts new file mode 100644 index 00000000..588b42c2 --- /dev/null +++ b/packages/physics/rigidbody/RigidbodyExpansion.ts @@ -0,0 +1,132 @@ +import { Vector3, Transform } from '@orillusion/core' +import { Ammo, Physics } from '../Physics' +import { RigidBodyUtil } from '../utils/RigidBodyUtil'; +import { ContactProcessedUtil } from '../utils/ContactProcessedUtil'; + + +/** + * @class PhysicsTransformSync + * @description This class manages the synchronization between the physics engine's rigid body and the 3D object's transform. + * It allows enabling or disabling the automatic update of the physics body when the transform changes. + */ +export class PhysicsTransformSync { + public isUpdatingFromPhysics: boolean = false; + private _btRigidbody: Ammo.btRigidBody; + private _mass: number; + + private _enablePhysicsTransformSync: boolean = false; + private transform: Transform; + + constructor(transform: Transform) { + this.transform = transform; + } + + public configure(body: Ammo.btRigidBody, mass: number) { + this._btRigidbody = body; + this._mass = mass; + } + + /** + * Enables or disables the transform sync with physics. + * If enabled, changes to the transform will automatically update the physics body. + */ + public set enablePhysicsTransformSync(value: boolean) { + if (this._enablePhysicsTransformSync === value) return; + this._enablePhysicsTransformSync = value; + this.isUpdatingFromPhysics = !value; + + this.transform.onPositionChange = value ? this.onPositionChange.bind(this) : null; + this.transform.onRotationChange = value ? this.onRotationChange.bind(this) : null; + this.transform.onScaleChange = value ? this.onScaleChange.bind(this) : null; + + if (value && this._btRigidbody) { + RigidBodyUtil.updateTransform(this._btRigidbody, this.transform.localPosition, this.transform.localRotation, false); + Physics.syncGraphic(this.transform.object3D, this._btRigidbody.getWorldTransform()); + this.onScaleChange(); + } + } + + public get enablePhysicsTransformSync() { + return this._enablePhysicsTransformSync; + } + + private onPositionChange(oldValue?: Vector3, newValue?: Vector3) { + if (this.isUpdatingFromPhysics) return; + newValue ||= this.transform.localPosition; + RigidBodyUtil.updatePosition(this._btRigidbody, newValue); + }; + + private onRotationChange(oldValue?: Vector3, newValue?: Vector3) { + if (this.isUpdatingFromPhysics) return; + newValue ||= this.transform.localRotation; + RigidBodyUtil.updateRotation(this._btRigidbody, newValue); + } + + private onScaleChange(oldValue?: Vector3, newValue?: Vector3) { + newValue ||= this.transform.localScale; + RigidBodyUtil.updateScale(this._btRigidbody, newValue, this._mass); + } + + public destroy() { + this._btRigidbody = null; + if (this._enablePhysicsTransformSync) { + this.transform.onPositionChange = null; + this.transform.onRotationChange = null; + this.transform.onScaleChange = null; + } + this.transform = null; + } +} + + +/** + * @class CollisionEventHandler + * @description This class handles the registration and configuration of collision events for physics bodies. + * It allows enabling or disabling collision events and setting custom collision callbacks. + */ +export class CollisionEventHandler { + private _pointer: number; + private _collisionEvent: (contactPoint: Ammo.btManifoldPoint, selfBody: Ammo.btRigidBody, otherBody: Ammo.btRigidBody) => void; + private _enableCollisionEvent: boolean = true; + + public configure(pointer: number) { + this._pointer && ContactProcessedUtil.unregisterCollisionCallback(this._pointer); + this._pointer = pointer; + this.configureCollisionEvent(); + } + + public get enableCollisionEvent() { + return this._enableCollisionEvent; + } + + public set enableCollisionEvent(value: boolean) { + if (this._enableCollisionEvent !== value) { + this._enableCollisionEvent = value; + this.configureCollisionEvent(); + } + } + + public get collisionEvent() { + return this._collisionEvent; + } + + public set collisionEvent(callback: (contactPoint: Ammo.btManifoldPoint, selfBody: Ammo.btRigidBody, otherBody: Ammo.btRigidBody) => void) { + this._collisionEvent = callback; + this.configureCollisionEvent() + } + + private configureCollisionEvent() { + if (this._pointer && this._collisionEvent) { + if (this._enableCollisionEvent) { + ContactProcessedUtil.registerCollisionCallback(this._pointer, this.collisionEvent); + } else { + ContactProcessedUtil.unregisterCollisionCallback(this._pointer); + } + } + } + + public destroy() { + ContactProcessedUtil.unregisterCollisionCallback(this._pointer); + this._collisionEvent = null; + } +} \ No newline at end of file diff --git a/packages/physics/softbody/ClothSoftbody.ts b/packages/physics/softbody/ClothSoftbody.ts new file mode 100644 index 00000000..749ebcfa --- /dev/null +++ b/packages/physics/softbody/ClothSoftbody.ts @@ -0,0 +1,248 @@ +import { Vector3, PlaneGeometry, VertexAttributeName, Quaternion } from '@orillusion/core'; +import { SoftbodyBase } from './SoftbodyBase'; +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { Rigidbody } from '../rigidbody/Rigidbody'; + +/** + * 软体布料平面的各个角 + */ +export type CornerType = 'leftTop' | 'rightTop' | 'leftBottom' | 'rightBottom' | 'left' | 'right' | 'top' | 'bottom' | 'center'; + +export class ClothSoftbody extends SoftbodyBase { + protected declare _geometry: PlaneGeometry; + private _segmentW: number; + private _segmentH: number; + private _offset: Vector3 = new Vector3(); + private _btRigidbody: Ammo.btRigidBody; // 通过锚点附加的 Ammo 刚体实例 + + /** + * 布料的四个角,默认以平面法向量计算各角。 + */ + public clothCorners: [Vector3, Vector3, Vector3, Vector3]; + + /** + * 固定节点索引。 + */ + public fixNodeIndices: CornerType[] | number[] = []; + + /** + * 添加锚点时需要的刚体。 + */ + public anchorRigidbody: Rigidbody; + + /** + * 布料的锚点。 + */ + public anchorIndices: CornerType[] | number[] = []; + + /** + * 仅在设置 `anchorRigidbody` 后有效,表示布料软体相对刚体的位置。 + */ + public anchorPosition: Vector3 = new Vector3(); + + /** + * 仅在设置 `anchorRigidbody` 后有效,表示布料软体相对刚体的旋转。 + */ + public anchorRotation: Vector3 = new Vector3(); + + async start(): Promise { + + if (!(this._geometry instanceof PlaneGeometry)) { + throw new Error('The cloth softbody requires plane geometry.'); + } + + if (this.anchorRigidbody) { + this._btRigidbody = await this.anchorRigidbody.wait(); + } + this._segmentW = this._geometry.segmentW; + this._segmentH = this._geometry.segmentH; + + super.start() + } + + protected initSoftBody(): Ammo.btSoftBody { + + // Defines the four corners of the cloth + let clothCorner00: Ammo.btVector3, + clothCorner01: Ammo.btVector3, + clothCorner10: Ammo.btVector3, + clothCorner11: Ammo.btVector3; + + if (!this.clothCorners) { + const up = this._geometry.up; + let right = up.equals(Vector3.X_AXIS) ? Vector3.BACK : Vector3.X_AXIS; + + right = up.crossProduct(right).normalize(); + const forward = right.crossProduct(up).normalize(); + + const halfWidth = this._geometry.width / 2; + const halfHeight = this._geometry.height / 2; + + const corner00 = right.mul(halfWidth).add(forward.mul(-halfHeight)); // leftTop + const corner01 = right.mul(halfWidth).add(forward.mul(halfHeight)); // rightTop + const corner10 = right.mul(-halfWidth).add(forward.mul(-halfHeight)); // leftBottom + const corner11 = right.mul(-halfWidth).add(forward.mul(halfHeight)); // rightBottom + + clothCorner00 = TempPhyMath.toBtVec(corner00, TempPhyMath.tmpVecA); + clothCorner01 = TempPhyMath.toBtVec(corner01, TempPhyMath.tmpVecB); + clothCorner10 = TempPhyMath.toBtVec(corner10, TempPhyMath.tmpVecC); + clothCorner11 = TempPhyMath.toBtVec(corner11, TempPhyMath.tmpVecD); + + } else { + clothCorner00 = TempPhyMath.toBtVec(this.clothCorners[0], TempPhyMath.tmpVecA) + clothCorner01 = TempPhyMath.toBtVec(this.clothCorners[1], TempPhyMath.tmpVecB); + clothCorner10 = TempPhyMath.toBtVec(this.clothCorners[2], TempPhyMath.tmpVecC); + clothCorner11 = TempPhyMath.toBtVec(this.clothCorners[3], TempPhyMath.tmpVecD); + } + + const clothSoftbody = new Ammo.btSoftBodyHelpers().CreatePatch( + Physics.worldInfo, + clothCorner00, + clothCorner01, + clothCorner10, + clothCorner11, + this._segmentW + 1, + this._segmentH + 1, + 0, + true + ); + + return clothSoftbody; + } + + protected configureSoftBody(clothSoftbody: Ammo.btSoftBody): void { + + // 软体配置 + const sbConfig = clothSoftbody.get_m_cfg(); + sbConfig.set_viterations(10); // 位置迭代次数 + sbConfig.set_piterations(10); // 位置求解器迭代次数 + + clothSoftbody.generateBendingConstraints(2, clothSoftbody.get_m_materials().at(0)); + + // 固定节点 + if (this.fixNodeIndices.length > 0) this.applyFixedNodes(this.fixNodeIndices); + + // 添加锚点 + if (this.anchorIndices.length > 0) { + if (!this._btRigidbody) throw new Error('Needs a rigid body'); + this.applyAnchor(clothSoftbody); + } else { + clothSoftbody.rotate(TempPhyMath.eulerToBtQua(this.transform.localRotation)); + clothSoftbody.translate(TempPhyMath.toBtVec(this.transform.localPosition)); + } + + } + + private applyAnchor(clothSoftbody: Ammo.btSoftBody): void { + + let tm = this._btRigidbody.getWorldTransform(); + TempPhyMath.fromBtVec(tm.getOrigin(), Vector3.HELP_0); + Vector3.HELP_0.add(this.anchorPosition, Vector3.HELP_1); + + TempPhyMath.fromBtQua(tm.getRotation(), Quaternion.HELP_0); + Quaternion.HELP_1.fromEulerAngles(this.anchorRotation.x, this.anchorRotation.y, this.anchorRotation.z); + Quaternion.HELP_1.multiply(Quaternion.HELP_0, Quaternion.HELP_1); + + clothSoftbody.rotate(TempPhyMath.toBtQua(Quaternion.HELP_1)); + clothSoftbody.translate(TempPhyMath.toBtVec(Vector3.HELP_1)); + + const anchorIndices = this.getCornerIndices(this.anchorIndices); + anchorIndices.forEach((nodeIndex) => { + clothSoftbody.appendAnchor(nodeIndex, this._btRigidbody, this.disableCollision, this.influence); + }); + } + + /** + * 将 CornerType 数组转换成节点索引数组。 + * @param cornerList 需要转换的 CornerType 数组。 + * @returns 节点索引数组 + */ + private getCornerIndices(cornerList: CornerType[] | number[]): number[] { + + if (typeof cornerList[0] === 'number') return cornerList as number[]; + + const W = this._segmentW; + const H = this._segmentH; + return (cornerList as CornerType[]).map(corner => { + switch (corner) { + case 'left': return this.getVertexIndex(0, Math.floor(H / 2)); + case 'right': return this.getVertexIndex(W, Math.floor(H / 2)); + case 'top': return this.getVertexIndex(Math.floor(W / 2), 0); + case 'bottom': return this.getVertexIndex(Math.floor(W / 2), H); + case 'center': return this.getVertexIndex(Math.floor(W / 2), Math.floor(H / 2)); + case 'leftTop': return 0; + case 'rightTop': return W; + case 'leftBottom': return this.getVertexIndex(0, H); + case 'rightBottom': return this.getVertexIndex(W, H); + default: throw new Error('Invalid corner'); + } + }); + + } + + private getVertexIndex(x: number, y: number): number { + return y * (this._segmentW + 1) + x; + } + + /** + * 固定软体节点。 + * @param fixedNodeIndices 表示需要固定的节点索引或 CornerType 数组。 + */ + public applyFixedNodes(fixedNodeIndices: CornerType[] | number[]): void { + this.wait().then(() => { + const indexArray = this.getCornerIndices(fixedNodeIndices); + super.applyFixedNodes(indexArray); + }) + } + + /** + * 清除锚点,软体将会从附加的刚体上脱落 + */ + public clearAnchors(): void { + this._btSoftbody.get_m_anchors().clear(); + this._offset.set(0, 0, 0); + this._btRigidbody = null; + this.anchorRigidbody = null; + } + + onUpdate(): void { + if (!this._btBodyInited) return; + + // 根据锚点刚体的插值坐标平滑软体运动 + if (this._btRigidbody) { + this._btRigidbody.getMotionState().getWorldTransform(Physics.TEMP_TRANSFORM); + const nowPos = this._btRigidbody.getWorldTransform().getOrigin(); + + TempPhyMath.fromBtVec(Physics.TEMP_TRANSFORM.getOrigin(), Vector3.HELP_0); + TempPhyMath.fromBtVec(nowPos, Vector3.HELP_1); + Vector3.sub(Vector3.HELP_0, Vector3.HELP_1, this._offset); + } + + const vertices = this._geometry.getAttribute(VertexAttributeName.position); + const normals = this._geometry.getAttribute(VertexAttributeName.normal); + + const nodes = this._btSoftbody.get_m_nodes(); + for (let i = 0; i < nodes.size(); i++) { + const node = nodes.at(i); + const pos = node.get_m_x(); + vertices.data[3 * i] = pos.x() + this._offset.x; + vertices.data[3 * i + 1] = pos.y() + this._offset.y; + vertices.data[3 * i + 2] = pos.z() + this._offset.z; + + const normal = node.get_m_n(); + normals.data[3 * i] = -normal.x(); + normals.data[3 * i + 1] = -normal.y(); + normals.data[3 * i + 2] = -normal.z(); + } + + this._geometry.vertexBuffer.upload(VertexAttributeName.position, vertices); + this._geometry.vertexBuffer.upload(VertexAttributeName.normal, normals); + } + + public destroy(force?: boolean): void { + this._btRigidbody = null; + this.anchorRigidbody = null; + super.destroy(force); + } +} diff --git a/packages/physics/softbody/RopeSoftbody.ts b/packages/physics/softbody/RopeSoftbody.ts new file mode 100644 index 00000000..1661ec61 --- /dev/null +++ b/packages/physics/softbody/RopeSoftbody.ts @@ -0,0 +1,200 @@ +import { Vector3, VertexAttributeName, GeometryBase } from '@orillusion/core'; +import { SoftbodyBase } from './SoftbodyBase'; +import { Ammo, Physics } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { Rigidbody } from '../rigidbody/Rigidbody'; + +export class RopeSoftbody extends SoftbodyBase { + /** + * 绳索两端的固定选项,默认值为 `0` + * + * `0`:两端不固定,`1`:起点固定,`2`:终点固定,`3`:两端固定 + */ + public fixeds: number = 0; + + /** + * 固定节点索引,与 `fixeds` 属性作用相同,但可以更自由的控制任意节点。 + */ + public fixNodeIndices: number[] = []; + + /** + * 绳索弹性,值越大弹性越低,通常设置为 0 到 1 之间,默认值为 `0.5`。 + */ + public elasticity: number = 0.5; + + /** + * 绳索起点处锚定的刚体,设置此项后绳索的起点将与该刚体的位置相同。 + */ + public anchorRigidbodyHead: Rigidbody; + + /** + * 绳索终点处锚定的刚体,设置此项后绳索的终点将与该刚体的位置相同。 + */ + public anchorRigidbodyTail: Rigidbody; + + /** + * 锚点的起点偏移量,表示起点与锚定的刚体之间的相对位置。 + */ + public anchorOffsetHead: Vector3 = new Vector3(); + + /** + * 锚点的终点偏移量,表示终点与锚定的刚体之间的相对位置。 + */ + public anchorOffsetTail: Vector3 = new Vector3(); + + private _positionHead: Vector3; + private _positionTail: Vector3; + + async start(): Promise { + if (this.anchorRigidbodyHead) { + const bodyA = await this.anchorRigidbodyHead.wait(); + this._positionHead = TempPhyMath.fromBtVec(bodyA.getWorldTransform().getOrigin()); + this._positionHead.add(this.anchorOffsetHead, this._positionHead); + } + if (this.anchorRigidbodyTail) { + const bodyB = await this.anchorRigidbodyTail.wait(); + this._positionTail = TempPhyMath.fromBtVec(bodyB.getWorldTransform().getOrigin()); + this._positionTail.add(this.anchorOffsetTail, this._positionTail); + } + super.start(); + } + + protected initSoftBody(): Ammo.btSoftBody { + const vertexArray = this._geometry.getAttribute(VertexAttributeName.position).data; + + this._positionHead ||= new Vector3(vertexArray[0], vertexArray[1], vertexArray[2]); + this._positionTail ||= new Vector3(vertexArray.at(-3), vertexArray.at(-2), vertexArray.at(-1)); + + const ropeStart = TempPhyMath.toBtVec(this._positionHead, TempPhyMath.tmpVecA); + const ropeEnd = TempPhyMath.toBtVec(this._positionTail, TempPhyMath.tmpVecB); + const segmentCount = this._geometry.vertexCount - 1; + + const ropeSoftbody = new Ammo.btSoftBodyHelpers().CreateRope( + Physics.worldInfo, + ropeStart, + ropeEnd, + segmentCount - 1, + this.fixeds + ); + + return ropeSoftbody; + } + + protected configureSoftBody(ropeSoftbody: Ammo.btSoftBody): void { + + // 设置软体配置与材质 + const sbConfig = ropeSoftbody.get_m_cfg(); + sbConfig.set_viterations(10); // 位置迭代次数 + sbConfig.set_piterations(10); // 位置求解器迭代次数 + + this.setElasticity(this.elasticity); + + // 固定节点 + if (this.fixNodeIndices.length > 0) this.applyFixedNodes(this.fixNodeIndices); + + // 锚定刚体 + if (this.anchorRigidbodyHead) { + const body = this.anchorRigidbodyHead.btRigidbody; + ropeSoftbody.appendAnchor(0, body, this.disableCollision, this.influence); + } + if (this.anchorRigidbodyTail) { + const body = this.anchorRigidbodyTail.btRigidbody; + ropeSoftbody.appendAnchor(this._geometry.vertexCount - 1, body, this.disableCollision, this.influence); + } + + } + + /** + * set rope elasticity to 0~1 + */ + public setElasticity(value: number): void { + this.elasticity = value; + this.wait().then(ropeSoftbody => { + const material = ropeSoftbody.get_m_materials().at(0); + material.set_m_kLST(value); // 线性弹性 + material.set_m_kAST(value); // 角度弹性 + }) + } + + /** + * 清除锚点,软体将会从附加的刚体上脱落 + * @param isPopBack 是否只删除一个锚点,当存在首尾两个锚点时,删除终点的锚点。 + */ + public clearAnchors(isPopBack?: boolean): void { + if (isPopBack) { + this._btSoftbody.get_m_anchors().pop_back(); + } else { + this._btSoftbody.get_m_anchors().clear(); + } + } + + onUpdate(): void { + + if (!this._btBodyInited) return; + + const nodes = this._btSoftbody.get_m_nodes(); + const vertices = this._geometry.getAttribute(VertexAttributeName.position); + + for (let i = 0; i < nodes.size(); i++) { + const pos = nodes.at(i).get_m_x(); + vertices.data[3 * i] = pos.x(); + vertices.data[3 * i + 1] = pos.y(); + vertices.data[3 * i + 2] = pos.z(); + } + + this._geometry.vertexBuffer.upload(VertexAttributeName.position, vertices); + + } + + + public destroy(force?: boolean): void { + this.anchorRigidbodyHead = null; + this.anchorRigidbodyTail = null; + super.destroy(force); + } + + /** + * 构建绳索(线条)几何体,注意添加材质时需要将拓扑结构 `topology` 设置为 `'line-list'`。 + * @param segmentCount 分段数 + * @param startPos 起点 + * @param endPos 终点 + * @returns GeometryBase + */ + public static buildRopeGeometry(segmentCount: number, startPos: Vector3, endPos: Vector3): GeometryBase { + + let vertices = new Float32Array((segmentCount + 1) * 3); + let indices = new Uint16Array(segmentCount * 2); + + for (let i = 0; i < segmentCount; i++) { + indices[i * 2] = i; + indices[i * 2 + 1] = i + 1; + } + + // 计算每个顶点之间的增量 + const deltaX = (endPos.x - startPos.x) / segmentCount; + const deltaY = (endPos.y - startPos.y) / segmentCount; + const deltaZ = (endPos.z - startPos.z) / segmentCount; + + for (let i = 0; i <= segmentCount; i++) { + vertices[i * 3] = startPos.x + deltaX * i; + vertices[i * 3 + 1] = startPos.y + deltaY * i; + vertices[i * 3 + 2] = startPos.z + deltaZ * i; + } + + const ropeGeometry = new GeometryBase(); + ropeGeometry.setIndices(indices); + ropeGeometry.setAttribute(VertexAttributeName.position, vertices); + ropeGeometry.addSubGeometry({ + indexStart: 0, + indexCount: indices.length, + vertexStart: 0, + vertexCount: 0, + firstStart: 0, + index: 0, + topology: 0 + }); + + return ropeGeometry; + } + +} diff --git a/packages/physics/softbody/SoftbodyBase.ts b/packages/physics/softbody/SoftbodyBase.ts new file mode 100644 index 00000000..f4a263bd --- /dev/null +++ b/packages/physics/softbody/SoftbodyBase.ts @@ -0,0 +1,170 @@ +import { ComponentBase, GeometryBase, MeshRenderer } from '@orillusion/core'; +import { Ammo, Physics } from '../Physics'; +import { ActivationState } from '../rigidbody/RigidbodyEnum'; +import { Rigidbody } from '../rigidbody/Rigidbody'; + +export abstract class SoftbodyBase extends ComponentBase { + private _initResolve!: () => void; + private _initializationPromise: Promise = new Promise(r => this._initResolve = r); + + protected _btBodyInited: boolean = false; + protected _btSoftbody: Ammo.btSoftBody; + protected _geometry: GeometryBase; + + /** + * 软体的总质量,默认值为 `1` + */ + public mass: number = 1; + + /** + * 碰撞边距,默认值为 `0.15` + */ + public margin: number = 0.15; + + /** + * 碰撞组,默认值为 `1` + */ + public group: number = 1; + + /** + * 碰撞掩码,默认值为 `-1` + */ + public mask: number = -1; + + /** + * 锚点的影响力。影响力值越大,软体节点越紧密地跟随刚体的运动。通常,这个值在0到1之间。默认值为 `1`。 + */ + public influence: number = 1; + + /** + * 是否禁用与锚定刚体之间的碰撞,默认值为 `false`。 + */ + public disableCollision: boolean = false; + + /** + * 设置软体激活状态。 + */ + public set activationState(value: ActivationState) { + this.wait().then(btSoftbody => btSoftbody.setActivationState(value)); + } + + public get btBodyInited(): boolean { + return this._btBodyInited; + } + + public get btSoftBody(): Ammo.btSoftBody { + return this._btSoftbody; + } + + init(): void { + if (!Physics.isSoftBodyWord) { + throw new Error('Enable soft body simulation by setting Physics.init({useSoftBody: true}) during initialization.'); + } + + this._geometry = this.object3D.getComponent(MeshRenderer)?.geometry; + + if (!this._geometry) { + throw new Error('SoftBody requires valid geometry.'); + } + + } + + async start(): Promise { + const btSoftbody = this._btSoftbody = this.initSoftBody(); + this.configureSoftBody(btSoftbody); + + btSoftbody.setTotalMass(this.mass, false); + Ammo.castObject(btSoftbody, Ammo.btCollisionObject).getCollisionShape().setMargin(this.margin); + (Physics.world as Ammo.btSoftRigidDynamicsWorld).addSoftBody(btSoftbody, this.group, this.mask); + + // 软体变换将由顶点更新表示,避免影响需要重置对象变换 + // this.transform.localPosition = this.transform.localRotation = Vector3.ZERO; + // this.transform.localScale = Vector3.ONE; + this.transform.worldMatrix.identity(); + + this._btBodyInited = true; + this._initResolve(); + } + + protected abstract initSoftBody(): Ammo.btSoftBody; + protected abstract configureSoftBody(softbody: Ammo.btSoftBody): void; + + /** + * Asynchronously retrieves the fully initialized soft body instance. + */ + public async wait(): Promise { + await this._initializationPromise; + return this._btSoftbody; + } + + /** + * Wraps the native soft body's `appendAnchor` method to anchor a node to a rigid body. + * @param nodeIndex - Index of the node to anchor. + * @param targetRigidbody - The rigid body to anchor to. + * @param disCollision - Optional. Disable collisions if true. + * @param influence - Optional. Anchor's influence. + */ + public appendAnchor(nodeIndex: number, targetRigidbody: Rigidbody, disCollision?: boolean, influence?: number): void { + disCollision ??= this.disableCollision; + influence ??= this.influence; + targetRigidbody.wait().then(btRigidbody => { + this.wait().then(ropeSoftbody => { + ropeSoftbody.appendAnchor(nodeIndex, btRigidbody, disCollision, influence); + }) + }) + } + + /** + * 固定软体节点。 + * @param fixedNodeIndices 需要固定的节点索引。 + */ + public applyFixedNodes(fixedNodeIndices: number[]): void { + this.wait().then(btSoftbody => { + const nodes = btSoftbody.get_m_nodes(); + fixedNodeIndices.forEach(i => { + if (i >= 0 && i < nodes.size()) { + nodes.at(i).get_m_v().setValue(0, 0, 0); + nodes.at(i).get_m_f().setValue(0, 0, 0); + nodes.at(i).set_m_im(0); + } else { + console.warn(`Index ${i} is out of bounds for nodes array.`); + } + }); + }) + } + + /** + * 清除固定节点 + * @param index 需要清除的节点索引,如果未提供,则清除所有节点。 + */ + public clearFixedNodes(index?: number): void { + const nodes = this._btSoftbody.get_m_nodes(); + const size = nodes.size(); + let inverseMass = 1 / this.mass * size; + + if (index != undefined) { + nodes.at(index).set_m_im(inverseMass); + return; + } + + for (let i = 0; i < size; i++) { + nodes.at(i).set_m_im(inverseMass); + } + } + + + public destroy(force?: boolean): void { + if (this._btBodyInited) { + if (Physics.world instanceof Ammo.btSoftRigidDynamicsWorld) { + Physics.world.removeSoftBody(this._btSoftbody); + Ammo.destroy(this._btSoftbody); + } + + this._geometry = null; + this._btSoftbody = null; + this._btBodyInited = false; + } + super.destroy(force); + } + +} diff --git a/packages/physics/utils/CollisionShapeUtil.ts b/packages/physics/utils/CollisionShapeUtil.ts new file mode 100644 index 00000000..4b80ee3f --- /dev/null +++ b/packages/physics/utils/CollisionShapeUtil.ts @@ -0,0 +1,522 @@ +import { Object3D, BoundUtil, Vector3, MeshRenderer, VertexAttributeName, PlaneGeometry, Quaternion, Matrix4, BoundingBox, BoxGeometry, SphereGeometry, CylinderGeometry } from '@orillusion/core'; +import { Physics, Ammo } from '../Physics'; +import { TempPhyMath } from './TempPhyMath'; + +export interface ChildShape { + shape: Ammo.btCollisionShape; + position: Vector3; + rotation: Quaternion; +} + +/** + * CollisionShapeUtil + * 提供多种碰撞体构建功能 + */ +export class CollisionShapeUtil { + /** + * 创建静态平面碰撞形状,适用于静态无限平面的碰撞,如地面或墙壁。 + * @param planeNormal - 平面法向量,默认值为 Vector3.UP。 + * @param planeConstant - 平面常数,表示平面距离原点的距离,默认值为 0。 + * @returns Ammo.btStaticPlaneShape - 静态平面碰撞形状实例。 + */ + public static createStaticPlaneShape(planeNormal: Vector3 = Vector3.UP, planeConstant: number = 0) { + const normal = TempPhyMath.toBtVec(planeNormal); + const shape = new Ammo.btStaticPlaneShape(normal, planeConstant); + + return shape; + } + + /** + * 创建盒型碰撞形状,适用于具有明确尺寸的盒形物体。 + * 如果未指定尺寸,则使用三维对象的包围盒大小。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param size - 可选参数,盒型碰撞体的尺寸。 + * @returns Ammo.btBoxShape - 盒型碰撞形状实例。 + */ + public static createBoxShape(object3D: Object3D, size?: Vector3) { + size ||= this.calculateLocalBoundingBox(object3D).size; + const halfExtents = TempPhyMath.setBtVec(size.x / 2, size.y / 2, size.z / 2); + const shape = new Ammo.btBoxShape(halfExtents); + + return shape; + } + + /** + * 创建球型碰撞形状,适用于球形物体。 + * 如果未指定半径,则使用三维对象的包围盒半径 `X`。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param radius - 可选参数,球型碰撞体的半径。 + * @returns Ammo.btSphereShape - 球型碰撞形状实例。 + */ + public static createSphereShape(object3D: Object3D, radius?: number) { + radius ||= this.calculateLocalBoundingBox(object3D).extents.x; + const shape = new Ammo.btSphereShape(radius); + + return shape; + } + + /** + * 创建胶囊型碰撞形状,适用于胶囊形物体。 + * 如果未指定尺寸,则使用三维对象的包围盒半径 `X` 和高度 `Y`。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param radius - 可选参数,胶囊的半径。 + * @param height - 可选参数,胶囊中间的圆柱部分的高度。 + * @returns Ammo.btCapsuleShape - 胶囊型碰撞形状实例。 + */ + public static createCapsuleShape(object3D: Object3D, radius?: number, height?: number) { + let boundSize: Vector3 + if (!radius || !height) boundSize = this.calculateLocalBoundingBox(object3D).size; + + radius ||= boundSize.x / 2; + height ||= boundSize.y - radius * 2; + const shape = new Ammo.btCapsuleShape(radius, height); + + return shape; + } + + /** + * 创建圆柱型碰撞形状,适用于圆柱形物体。 + * 如果未指定尺寸,则使用三维对象的包围盒半径 `X` 和高度 `Y`。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param radius - 可选参数,圆柱的半径。 + * @param height - 可选参数,圆柱的完整高度。 + * @returns Ammo.btCylinderShape - 圆柱型碰撞形状实例。 + */ + public static createCylinderShape(object3D: Object3D, radius?: number, height?: number) { + let boundSize: Vector3 + if (!radius || !height) boundSize = this.calculateLocalBoundingBox(object3D).size; + + radius ||= boundSize.x / 2; + height ||= boundSize.y; + const halfExtents = TempPhyMath.setBtVec(radius, height / 2, radius); + const shape = new Ammo.btCylinderShape(halfExtents); + + return shape; + } + + /** + * 创建圆锥形碰撞形状,适用于圆锥形物体。 + * 如果未指定尺寸,则使用三维对象的包围盒半径 `X` 和高度 `Y`。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param radius - 可选参数,圆锥的半径。 + * @param height - 可选参数,圆锥的高度。 + * @returns Ammo.btConeShape - 圆锥形碰撞形状实例。 + */ + public static createConeShape(object3D: Object3D, radius?: number, height?: number) { + let boundSize: Vector3 + if (!radius || !height) boundSize = this.calculateLocalBoundingBox(object3D).size; + + radius ||= boundSize.x / 2; + height ||= boundSize.y; + const shape = new Ammo.btConeShape(radius, height); + + return shape; + } + + /** + * 创建复合形状,将多个子形状组合成一个形状。 + * @param childShapes - 包含子形状实例与位置、旋转属性的数组。 + * @returns Ammo.btCompoundShape - 复合形状实例。 + */ + public static createCompoundShape(childShapes: ChildShape[]) { + const compoundShape = new Ammo.btCompoundShape(); + const transform = Physics.TEMP_TRANSFORM; + + childShapes.forEach(desc => { + transform.setIdentity(); + transform.setOrigin(TempPhyMath.toBtVec(desc.position)); + transform.setRotation(TempPhyMath.toBtQua(desc.rotation)); + + compoundShape.addChildShape(transform, desc.shape); + }); + + return compoundShape; + } + + /** + * 根据 Object3D 对象及其子对象创建复合碰撞形状。 + * @param object3D - 三维对象,包含多个子对象。 + * @param includeParent - 是否包含父对象的几何体,默认值为 `true`。 + * @returns 复合碰撞形状。 + */ + public static createCompoundShapeFromObject(object3D: Object3D, includeParent: boolean = true) { + + const childShapes: ChildShape[] = []; + + // 处理父对象几何体 + if (includeParent) { + const shape = this.createShapeFromObject(object3D); + if (shape) { + const position = new Vector3(); + const rotation = new Quaternion(); + childShapes.push({ shape, position, rotation }); + } + } + + // 计算父对象的逆矩阵 + const parentMatrixInverse = object3D.transform.worldMatrix.clone(); + parentMatrixInverse.invert(); + + // 遍历并处理子对象 + object3D.forChild((child: Object3D) => { + const shape = this.createShapeFromObject(child); + if (shape) { + // 矩阵相乘并分解 + const childMatrix = child.transform.worldMatrix; + const localMatrix = Matrix4.help_matrix_0; + localMatrix.multiplyMatrices(parentMatrixInverse, childMatrix); + + const position = new Vector3(); + const rotation = new Quaternion(); + localMatrix.decompose('quaternion', [position, rotation as any, Vector3.HELP_0]); + childShapes.push({ shape, position, rotation }); + } + }); + + // 创建复合碰撞形状 + const compoundShape = this.createCompoundShape(childShapes); + return compoundShape; + } + + /** + * 根据 Object3D 对象的几何体类型创建相应的碰撞形状。 + * + * 仅支持Box、Sphere、Plane、Cylinder类型的几何体。对于不匹配的几何体类型,返回 btConvexHullShape 凸包形状。 + * @param object3D + * @returns Ammo.btCollisionShape + */ + public static createShapeFromObject(object3D: Object3D): Ammo.btCollisionShape | null { + + const geometry = object3D.getComponent(MeshRenderer)?.geometry; + if (!geometry) return null; + + let shape: Ammo.btCollisionShape; + let scale = Vector3.HELP_0.copyFrom(object3D.localScale); + + // 根据几何类型创建相应的碰撞形状 + switch (true) { + case geometry instanceof BoxGeometry: { + const { width, height, depth } = geometry; + const size = new Vector3(width, height, depth).scale(scale); + shape = this.createBoxShape(object3D, size); + break; + } + case geometry instanceof SphereGeometry: { + const radius = geometry.radius * scale.x; + shape = this.createSphereShape(object3D, radius); + break; + } + case geometry instanceof PlaneGeometry: { + const { width, height } = geometry; + const size = new Vector3(width, 0, height).scale(scale); + shape = this.createBoxShape(object3D, size); + break; + } + case geometry instanceof CylinderGeometry: { + const radiusBottom = geometry.radiusBottom * scale.x + const height = geometry.height * scale.y + + if (geometry.radiusTop === geometry.radiusBottom) { + shape = this.createCylinderShape(object3D, radiusBottom, height); + } else if (geometry.radiusTop <= 0.1) { + shape = this.createConeShape(object3D, radiusBottom, height); + } else { + shape = this.createConvexHullShape(object3D); + } + break; + } + default: { + shape = this.createConvexHullShape(object3D); + break; + } + } + + return shape; + } + + /** + * 创建高度场形状,基于平面顶点数据模拟地形。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param heightScale - 高度缩放比例,默认值为 `1`。 + * @param upAxis - 高度场的上轴,默认值为 `1`。 + * @param hdt - 高度场的数据类型,默认值为 `Ammo.PHY_FLOAT`。 + * @param flipQuadEdges - 是否翻转四边形的边,默认值为 `false`。 + * @returns Ammo.btHeightfieldTerrainShape - 高度场形状实例。 + */ + public static createHeightfieldTerrainShape( + object3D: Object3D, + heightScale: number = 1, + upAxis: number = 1, + hdt: Ammo.PHY_ScalarType = 'PHY_FLOAT', + flipQuadEdges: boolean = false, + ) { + let geometry = object3D.getComponent(MeshRenderer)?.geometry; + + if (!(geometry instanceof PlaneGeometry)) throw new Error("Wrong geometry type"); + + const { width, height, segmentW, segmentH } = geometry; + let posAttrData = geometry.getAttribute(VertexAttributeName.position); + const heightData = new Float32Array(posAttrData.data.length / 3); + let minHeight = Infinity, maxHeight = -Infinity; + + for (let i = 0, count = posAttrData.data.length / 3; i < count; i++) { + let y = posAttrData.data[i * 3 + 1]; + heightData[i] = y; + if (y < minHeight) minHeight = y; + if (y > maxHeight) maxHeight = y; + } + + let ammoHeightData = Ammo._malloc(heightData.length * 4); + let ammoHeightDataF32 = new Float32Array(Ammo.HEAPF32.buffer, ammoHeightData, heightData.length); + ammoHeightDataF32.set(heightData); + + let shape = new Ammo.btHeightfieldTerrainShape( + segmentW + 1, + segmentH + 1, + ammoHeightData, + heightScale, + minHeight, + maxHeight, + upAxis, + hdt, + flipQuadEdges + ); + + let localScaling = TempPhyMath.setBtVec(width / segmentW, 1, height / segmentH); + shape.setLocalScaling(localScaling); + (shape as any).averageHeight = (minHeight + maxHeight) / 2; + + return shape; + } + + /** + * 创建凸包形状,适用于具有凹陷填充的模型。 + * 此形状适用于动态物体并提供快速的碰撞检测。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param modelVertices - 可选参数,提供碰撞体所需的顶点数据,默认为三维对象的顶点数据。 + * @returns Ammo.btConvexHullShape - 凸包形状实例。 + */ + public static createConvexHullShape(object3D: Object3D, modelVertices?: Float32Array) { + let vertices = modelVertices || this.getAllMeshVerticesAndIndices(object3D).vertices; + + let shape = new Ammo.btConvexHullShape(); + for (let i = 0, count = vertices.length / 3; i < count; i++) { + let point = TempPhyMath.setBtVec(vertices[3 * i], vertices[3 * i + 1], vertices[3 * i + 2]); + shape.addPoint(point, true); + } + + let scaling = TempPhyMath.toBtVec(object3D.localScale); + shape.setLocalScaling(scaling); + + return shape; + } + + /** + * 创建凸包网格形状,适用于需要复杂几何表示的动态物体。 + * 此形状不要求额外的凸包生成步骤,适用于凸的三角形网格。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param modelVertices - 可选参数,提供碰撞体所需的顶点数据。 + * @param modelIndices - 可选参数,提供碰撞体所需的索引数据。 + * @returns Ammo.btConvexTriangleMeshShape - 凸包网格形状实例。 + */ + public static createConvexTriangleMeshShape(object3D: Object3D, modelVertices?: Float32Array, modelIndices?: Uint16Array): Ammo.btBvhTriangleMeshShape { + // 检查 modelVertices 和 modelIndices 是否同时被提供或同时未提供 + if ((modelVertices && !modelIndices) || (!modelVertices && modelIndices)) { + console.warn('createConvexTriangleMeshShape: Both modelVertices and modelIndices must be provided or neither should be provided.'); + } + + const { vertices, indices } = (modelVertices && modelIndices) + ? { vertices: modelVertices, indices: modelIndices } + : this.getAllMeshVerticesAndIndices(object3D, false); + + const triangleMesh = this.buildTriangleMesh(vertices, indices); + const shape = new Ammo.btConvexTriangleMeshShape(triangleMesh, true); + + const scaling = TempPhyMath.toBtVec(object3D.localScale); + shape.setLocalScaling(scaling); + + return shape; + } + + /** + * 创建边界体积层次(BVH)网格形状,适用于需要复杂几何表示的静态物体。 + * 此形状适合大规模静态网格,但对动态对象不适用。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param modelVertices - 可选参数,提供碰撞体所需的顶点数据。 + * @param modelIndices - 可选参数,提供碰撞体所需的索引数据。 + * @returns Ammo.btBvhTriangleMeshShape - BVH 网格形状实例。 + */ + public static createBvhTriangleMeshShape(object3D: Object3D, modelVertices?: Float32Array, modelIndices?: Uint16Array): Ammo.btBvhTriangleMeshShape { + // 检查 modelVertices 和 modelIndices 是否同时被提供或同时未提供 + if ((modelVertices && !modelIndices) || (!modelVertices && modelIndices)) { + console.warn('createBvhTriangleMeshShape: Both modelVertices and modelIndices must be provided or neither should be provided.'); + } + + const { vertices, indices } = (modelVertices && modelIndices) + ? { vertices: modelVertices, indices: modelIndices } + : this.getAllMeshVerticesAndIndices(object3D, false); + + const triangleMesh = this.buildTriangleMesh(vertices, indices); + const shape = new Ammo.btBvhTriangleMeshShape(triangleMesh, true, true); + + const scaling = TempPhyMath.toBtVec(object3D.localScale); + shape.setLocalScaling(scaling); + + return shape; + } + + /** + * 创建 GImpact 网格形状,适用于需要复杂几何表示的动态物体。 + * 基于 GIMPACT 算法,可以用于复杂的三角网格碰撞检测,包括动态物体的交互,此形状性能消耗较高,但提供更精确的碰撞检测。 + * @param object3D - 用于创建碰撞体的三维对象。 + * @param modelVertices - 可选参数,提供碰撞体所需的顶点数据。 + * @param modelIndices - 可选参数,提供碰撞体所需的索引数据。 + * @returns Ammo.btGImpactMeshShape - GImpact 网格形状实例。 + */ + public static createGImpactMeshShape(object3D: Object3D, modelVertices?: Float32Array, modelIndices?: Uint16Array): Ammo.btGImpactMeshShape { + // 检查 modelVertices 和 modelIndices 是否同时被提供或同时未提供 + if ((modelVertices && !modelIndices) || (!modelVertices && modelIndices)) { + console.warn('createGImpactMeshShape: Both modelVertices and modelIndices must be provided or neither should be provided.'); + } + + const { vertices, indices } = (modelVertices && modelIndices) + ? { vertices: modelVertices, indices: modelIndices } + : this.getAllMeshVerticesAndIndices(object3D, false); + + const triangleMesh = this.buildTriangleMesh(vertices, indices); + const shape = new Ammo.btGImpactMeshShape(triangleMesh); + shape.updateBound(); + + const scaling = TempPhyMath.toBtVec(object3D.localScale); + shape.setLocalScaling(scaling); + + return shape; + } + + /** + * 构建 btTriangleMesh 对象,用于创建网格形状。 + * @param vertices - 顶点数据,按 xyz 顺序排列。 + * @param indices - 索引数据,定义三角形的顶点索引。 + * @returns Ammo.btTriangleMesh - 构建的三角形网格。 + */ + public static buildTriangleMesh(vertices: Float32Array, indices: Uint16Array): Ammo.btTriangleMesh { + let triangleMesh = new Ammo.btTriangleMesh(); + + for (let i = 0; i < indices.length; i += 3) { + const index0 = indices[i] * 3; + const index1 = indices[i + 1] * 3; + const index2 = indices[i + 2] * 3; + + const v0 = TempPhyMath.setBtVec(vertices[index0], vertices[index0 + 1], vertices[index0 + 2], TempPhyMath.tmpVecA); + const v1 = TempPhyMath.setBtVec(vertices[index1], vertices[index1 + 1], vertices[index1 + 2], TempPhyMath.tmpVecB); + const v2 = TempPhyMath.setBtVec(vertices[index2], vertices[index2 + 1], vertices[index2 + 2], TempPhyMath.tmpVecC); + + triangleMesh.addTriangle(v0, v1, v2, true); + } + + return triangleMesh; + } + + /** + * 获取3D对象所有网格的顶点与索引。 + * @param object3D - 三维对象。 + * @param isTransformChildren - 是否将子对象的顶点转换到父对象的局部坐标系。默认值为 `true`。 + * @returns 顶点数据和索引数据。 + */ + public static getAllMeshVerticesAndIndices(object3D: Object3D, isTransformChildren: boolean = true) { + let meshRenderers = object3D.getComponents(MeshRenderer); + + if (meshRenderers.length === 1 && !isTransformChildren) { + return { + vertices: meshRenderers[0].geometry.getAttribute(VertexAttributeName.position).data as Float32Array, + indices: meshRenderers[0].geometry.getAttribute(VertexAttributeName.indices).data as Uint16Array + }; + } + + let totalVertexLength = 0; + let totalIndexLength = 0; + + meshRenderers.forEach(renderer => { + totalVertexLength += renderer.geometry.getAttribute(VertexAttributeName.position).data.length; + totalIndexLength += renderer.geometry.getAttribute(VertexAttributeName.indices).data.length; + }); + + let vertices = new Float32Array(totalVertexLength); + let indices = new Uint16Array(totalIndexLength); + + let vertexOffset = 0; + let indexOffset = 0; + let currentIndexOffset = 0; + + let parentMatrixInverse: Matrix4; + if (isTransformChildren) { + // 计算父对象的逆矩阵 + parentMatrixInverse = object3D.transform.worldMatrix.clone(); + parentMatrixInverse.invert(); + } + + meshRenderers.forEach(renderer => { + let vertexArray = renderer.geometry.getAttribute(VertexAttributeName.position).data; + + if (isTransformChildren) { + const childWorldMatrix = renderer.object3D.transform.worldMatrix; + + // 计算子对象相对父对象的局部变换矩阵 + let localMatrix = Matrix4.help_matrix_1; + localMatrix.multiplyMatrices(parentMatrixInverse, childWorldMatrix); + + let transformedVertexArray = new Float32Array(vertexArray.length); + + for (let index = 0; index < vertexArray.length / 3; index++) { + Vector3.HELP_0.set( + vertexArray[index * 3], + vertexArray[index * 3 + 1], + vertexArray[index * 3 + 2] + ); + + Vector3.HELP_0.applyMatrix4(localMatrix); + + transformedVertexArray[index * 3] = Vector3.HELP_0.x; + transformedVertexArray[index * 3 + 1] = Vector3.HELP_0.y; + transformedVertexArray[index * 3 + 2] = Vector3.HELP_0.z; + } + vertexArray = transformedVertexArray; + } + + vertices.set(vertexArray, vertexOffset); + vertexOffset += vertexArray.length; + + let indexArray = renderer.geometry.getAttribute(VertexAttributeName.indices).data; + for (let i = 0; i < indexArray.length; i++) { + indices[indexOffset + i] = indexArray[i] + currentIndexOffset; + } + indexOffset += indexArray.length; + currentIndexOffset += vertexArray.length / 3; + }); + + return { vertices, indices }; + } + + /** + * 计算三维对象的局部包围盒 + * @param object3D - 三维对象 + * @returns 局部包围盒 + */ + private static calculateLocalBoundingBox(object3D: Object3D): BoundingBox { + if (object3D.renderNode && !object3D.numChildren) { + return object3D.renderNode.geometry.bounds; + } + + let originalRotation = object3D.localRotation.clone(); + object3D.localRotation = Vector3.ZERO; + let bounds = BoundUtil.genMeshBounds(object3D); + object3D.localRotation = originalRotation; + return bounds; + // const { x, y, z } = object3D.localRotation; + // object3D.localRotation.set(0, 0, 0); + // let bounds = BoundUtil.genMeshBounds(object3D); + // object3D.localRotation.set(x, y, z); + // return bounds; + } + +} diff --git a/packages/physics/utils/ContactProcessedUtil.ts b/packages/physics/utils/ContactProcessedUtil.ts new file mode 100644 index 00000000..8acce64a --- /dev/null +++ b/packages/physics/utils/ContactProcessedUtil.ts @@ -0,0 +1,188 @@ +import { Physics, Ammo } from '../Physics'; + +type Callback = (contactPoint: Ammo.btManifoldPoint, bodyA: Ammo.btRigidBody, bodyB: Ammo.btRigidBody) => void; + +/** + * 碰撞处理工具 + */ +export class ContactProcessedUtil { + private static callbacks: Map = new Map(); + private static ignoredPointers: Set = new Set(); + private static contactProcessedCallbackPointer: number | null = null; + + /** + * 注册碰撞事件 + * @param pointer 物理对象指针 + * @param callback 事件回调 + */ + public static registerCollisionCallback(pointer: number, callback: Callback): void { + if (pointer == null) return; + + ContactProcessedUtil.callbacks.set(pointer, callback); + if (ContactProcessedUtil.callbacks.size === 1) { + // 第一个注册的回调,注册碰撞处理回调 + ContactProcessedUtil.registerContactProcessedCallback(); + } + } + + /** + * 注销碰撞事件 + * @param pointer 物理对象指针 + */ + public static unregisterCollisionCallback(pointer: number): void { + ContactProcessedUtil.callbacks.delete(pointer); + if (ContactProcessedUtil.callbacks.size === 0) { + // 最后一个注销的回调,禁用碰撞处理回调 + ContactProcessedUtil.unregisterContactProcessedCallback(); + } + } + + /** + * 注册全局碰撞处理回调 + */ + private static registerContactProcessedCallback(): void { + if (ContactProcessedUtil.contactProcessedCallbackPointer === null) { + ContactProcessedUtil.contactProcessedCallbackPointer = Ammo.addFunction(ContactProcessedUtil.contactProcessedCallback); + Physics.world.setContactProcessedCallback(ContactProcessedUtil.contactProcessedCallbackPointer); + } + } + + /** + * 注销全局碰撞处理回调 + */ + private static unregisterContactProcessedCallback(): void { + if (ContactProcessedUtil.contactProcessedCallbackPointer !== null) { + Physics.world.setContactProcessedCallback(null); // 禁用回调 + ContactProcessedUtil.contactProcessedCallbackPointer = null; + } + } + + /** + * 将指针添加到忽略集合中,添加后,任何物体与该指针对象碰撞时都无法触发碰撞事件 + * @param pointer 物理对象指针 + */ + public static addIgnoredPointer(pointer: number): void { + if (pointer != null) { + ContactProcessedUtil.ignoredPointers.add(pointer); + } + } + + /** + * 从忽略集合中移除指针 + * @param pointer 物理对象指针 + */ + public static removeIgnoredPointer(pointer: number): void { + ContactProcessedUtil.ignoredPointers.delete(pointer); + } + + /** + * 检查指针是否在忽略集合中 + * @param pointer 物理对象指针 + */ + public static isIgnored(pointer: number): boolean { + return ContactProcessedUtil.ignoredPointers.has(pointer); + } + + /** + * 检查指针是否注册了碰撞事件 + * @param pointer 物理对象指针 + */ + public static isCollision(pointer: number): boolean { + return ContactProcessedUtil.callbacks.has(pointer); + } + + /** + * 全局接触(碰撞)事件回调函数 + */ + private static contactProcessedCallback(cpPtr: number, colObj0WrapPtr: number, colObj1WrapPtr: number): number { + // 检查是否需要忽略 + if (ContactProcessedUtil.ignoredPointers.has(colObj0WrapPtr) || ContactProcessedUtil.ignoredPointers.has(colObj1WrapPtr)) { + return 0; + } + + // 通过碰撞对象包装器指针获取其注册的事件 + const callbackA = ContactProcessedUtil.callbacks.get(colObj0WrapPtr); + const callbackB = ContactProcessedUtil.callbacks.get(colObj1WrapPtr); + + // 排除均未注册碰撞事件的碰撞对 + if (callbackA || callbackB) { + // 指针转换 + const cp = Ammo.wrapPointer(cpPtr, Ammo.btManifoldPoint); + const bodyA = Ammo.wrapPointer(colObj0WrapPtr, Ammo.btRigidBody); + const bodyB = Ammo.wrapPointer(colObj1WrapPtr, Ammo.btRigidBody); + + callbackA?.(cp, bodyA, bodyB); + callbackB?.(cp, bodyB, bodyA); + } + + return 0; // 返回0表示已处理本次碰撞 + } + + /** + * 执行一次性的碰撞测试。 + * 如果提供了 bodyB,则检测 bodyA 与 bodyB 是否碰撞。 + * 否则,检测 bodyA 是否与其他所有刚体碰撞。 + * @param bodyA - 第一个刚体。 + * @param bodyB - (可选)第二个刚体。 + * @returns 如果发生碰撞,返回包含碰撞信息的对象;否则返回 null。 + */ + public static performCollisionTest(bodyA: Ammo.btRigidBody, bodyB?: Ammo.btRigidBody) { + const callback = new Ammo.ConcreteContactResultCallback(); + let collisionDetected: { + cpPtr: number, + colObj0Wrap: Ammo.btCollisionObjectWrapper, + colObj1Wrap: Ammo.btCollisionObjectWrapper, + partId0: number, + index0: number, + partId1: number, + index1: number + } | null = null; + + callback.addSingleResult = (cp, colObj0Wrap, partId0, index0, colObj1Wrap, partId1, index1) => { + let collisionObjectWrapperA = Ammo.wrapPointer(colObj0Wrap as unknown as number, Ammo.btCollisionObjectWrapper) + let collisionObjectWrapperB = Ammo.wrapPointer(colObj1Wrap as unknown as number, Ammo.btCollisionObjectWrapper) + collisionDetected = { + cpPtr: cp as unknown as number, + colObj0Wrap: collisionObjectWrapperA, + colObj1Wrap: collisionObjectWrapperB, + partId0, + index0, + partId1, + index1 + }; + + return 0; + }; + + if (bodyB) { + Physics.world.contactPairTest(bodyA, bodyB, callback); + } else { + Physics.world.contactTest(bodyA, callback); + } + + Ammo.destroy(callback); + + return collisionDetected; + } + + /** + * 碰撞检测,判断两个刚体是否正在发生碰撞 + * @param bodyA + * @param bodyB + * @returns boolean + */ + public static checkCollision(bodyA: Ammo.btRigidBody, bodyB: Ammo.btRigidBody): boolean { + const dispatcher = Physics.world.getDispatcher(); + const manifoldCount = dispatcher.getNumManifolds(); + for (let i = 0; i < manifoldCount; i++) { + const manifold = dispatcher.getManifoldByIndexInternal(i); + const rbA = Ammo.castObject(manifold.getBody0(), Ammo.btRigidBody); + const rbB = Ammo.castObject(manifold.getBody1(), Ammo.btRigidBody); + if ((rbA === bodyA && rbB === bodyB) || (rbA === bodyB && rbB === bodyA)) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/packages/physics/utils/PhysicsDragger.ts b/packages/physics/utils/PhysicsDragger.ts new file mode 100644 index 00000000..45e2cfe4 --- /dev/null +++ b/packages/physics/utils/PhysicsDragger.ts @@ -0,0 +1,197 @@ +import { Engine3D, View3D, PointerEvent3D, Vector3 } from "@orillusion/core"; +import { Ammo, Physics } from "../Physics"; +import { TempPhyMath } from "./TempPhyMath"; +import { CollisionFlags } from "../rigidbody/RigidbodyEnum"; + +/** + * PhysicsDragger 类用于通过鼠标操作拖拽3D物体。 + * 利用物理引擎中的射线检测与刚体交互,实现物体的实时拖拽效果。 + */ +export class PhysicsDragger { + private _view: View3D; + private _interactionDepth: number; + private _rigidBody: Ammo.btRigidBody; + private _rayStart: Ammo.btVector3; + private _rayEnd: Ammo.btVector3; + private _raycastResult: Ammo.ClosestRayResultCallback; + private _isDragging: boolean = false; + private _hitPoint: Vector3 = new Vector3(); + private _offset: Vector3 = new Vector3(); + private _enable: boolean = true; + + public get enable(): boolean { + return this._enable; + } + + /** + * 是否启用拖拽功能 + */ + public set enable(value: boolean) { + if (this._enable === value) return; + this._enable = value; + value ? this.registerEvents() : this.unregisterEvents(); + } + + /** + * 是否过滤静态刚体对象,默认值为 `true` + */ + public filterStatic: boolean = true; + + /** + * 设置射线过滤组 + */ + public set collisionFilterGroup(value: number) { + this._raycastResult?.set_m_collisionFilterGroup(value); + } + + /** + * 设置射线过滤掩码 + */ + public set collisionFilterMask(value: number) { + this._raycastResult?.set_m_collisionFilterMask(value); + } + + constructor() { + this.initRaycast(); + this.tryRegisterEvents(); + } + + private initRaycast() { + this._rayStart = new Ammo.btVector3(); + this._rayEnd = new Ammo.btVector3(); + this._raycastResult = new Ammo.ClosestRayResultCallback(this._rayStart, this._rayEnd); + } + + private tryRegisterEvents() { + const intervalId = setInterval(() => { + if (Engine3D.inputSystem) { + this.registerEvents(); + clearInterval(intervalId); + } + }, 100); + } + + private registerEvents() { + this._view = Engine3D.views[0]; + Engine3D.inputSystem?.addEventListener(PointerEvent3D.POINTER_DOWN, this.onMouseDown, this); + Engine3D.inputSystem?.addEventListener(PointerEvent3D.POINTER_MOVE, this.onMouseMove, this, null, 20); + Engine3D.inputSystem?.addEventListener(PointerEvent3D.POINTER_UP, this.onMouseUp, this, null, 20); + Engine3D.inputSystem?.addEventListener(PointerEvent3D.POINTER_WHEEL, this.onMouseWheel, this, null, 20); + } + + private unregisterEvents() { + Engine3D.inputSystem?.removeEventListener(PointerEvent3D.POINTER_DOWN, this.onMouseDown, this); + Engine3D.inputSystem?.removeEventListener(PointerEvent3D.POINTER_MOVE, this.onMouseMove, this); + Engine3D.inputSystem?.removeEventListener(PointerEvent3D.POINTER_UP, this.onMouseUp, this); + Engine3D.inputSystem?.removeEventListener(PointerEvent3D.POINTER_WHEEL, this.onMouseWheel, this); + + this.resetState(); + this._view = null; + } + + private onMouseDown(e: PointerEvent3D) { + if (!this._enable) return; + + if (e.mouseCode === 0) { // left key + const camera = this._view.camera; + let ray = camera.screenPointToRay(e.mouseX, e.mouseY); + + let adjustedDirection = ray.direction.normalize(); + let endPos = ray.origin.add(adjustedDirection.multiplyScalar(1000), ray.origin); + + this.resetRayCallback(this._raycastResult); + this.castRay(camera.object3D.localPosition, endPos); + + if (this._isDragging) { + e.stopImmediatePropagation(); + const worldCoordinates = camera.worldToScreenPoint(this._hitPoint, Vector3.HELP_1); + this._interactionDepth = worldCoordinates.z; + } + } + } + + private onMouseMove(e: PointerEvent3D) { + if (!this._enable || !this._isDragging) return; + + e.stopImmediatePropagation(); + this.updateRigidBody(); + } + + private onMouseUp(e: PointerEvent3D) { + if (!this._enable || !this._isDragging) return; + + if (e.mouseCode === 0) { + this.resetState(); + } + } + + private onMouseWheel(e: PointerEvent3D) { + if (!this._enable || !this._isDragging) return; + + this.updateRigidBody(); + } + + private resetRayCallback(callback: Ammo.ClosestRayResultCallback) { + callback.set_m_closestHitFraction(1); // 重置最近击中分数为最大 + callback.set_m_collisionObject(null); // 清除碰撞对象 + } + + private castRay(cameraPos: Vector3, targetPos: Vector3) { + this._rayStart.setValue(cameraPos.x, cameraPos.y, cameraPos.z); + this._rayEnd.setValue(targetPos.x, targetPos.y, targetPos.z); + + this._raycastResult.set_m_rayFromWorld(this._rayStart); + this._raycastResult.set_m_rayToWorld(this._rayEnd); + + Physics.world.rayTest(this._rayStart, this._rayEnd, this._raycastResult); + + if (this._raycastResult.hasHit()) { + const collisionObject = this._raycastResult.get_m_collisionObject(); + if (this.filterStatic && collisionObject.isStaticObject()) return; + + this._rigidBody = Ammo.castObject(collisionObject, Ammo.btRigidBody); + + // 交点 + TempPhyMath.fromBtVec(this._raycastResult.get_m_hitPointWorld(), this._hitPoint); + + this._rigidBody.setCollisionFlags(this._rigidBody.getCollisionFlags() | CollisionFlags.KINEMATIC_OBJECT); + + // 根据选中对象的位置与交点计算出偏移量 + this._rigidBody.getMotionState().getWorldTransform(Physics.TEMP_TRANSFORM); + let originPos = TempPhyMath.fromBtVec(Physics.TEMP_TRANSFORM.getOrigin(), Vector3.HELP_0); + Vector3.sub(originPos, this._hitPoint, this._offset); + + this._isDragging = true; + document.body.style.cursor = 'grab'; + } + } + + // 更新刚体位置 + private updateRigidBody() { + let pos = this._view.camera.screenPointToWorld(Engine3D.inputSystem.mouseX, Engine3D.inputSystem.mouseY, this._interactionDepth); + + // 结合偏移量的新位置 + let newPos = pos.add(this._offset, pos); + + // 更新位置 + this._rigidBody.getMotionState().getWorldTransform(Physics.TEMP_TRANSFORM); + Physics.TEMP_TRANSFORM.setOrigin(TempPhyMath.toBtVec(newPos)); + this._rigidBody.getMotionState().setWorldTransform(Physics.TEMP_TRANSFORM); + this._rigidBody.getWorldTransform().setOrigin(Physics.TEMP_TRANSFORM.getOrigin()); // 确保静态刚体的位置信息是同步的 + + this._rigidBody.activate(true); + document.body.style.cursor = 'grabbing'; + } + + private resetState() { + if (this._rigidBody) { + this._rigidBody.setCollisionFlags(this._rigidBody.getCollisionFlags() & ~CollisionFlags.KINEMATIC_OBJECT); + this._rigidBody.activate(true); + this._rigidBody = null; + } + + this._isDragging = false; + document.body.style.cursor = 'default'; + } + +} diff --git a/packages/physics/utils/RigidBodyMapping.ts b/packages/physics/utils/RigidBodyMapping.ts new file mode 100644 index 00000000..446dec80 --- /dev/null +++ b/packages/physics/utils/RigidBodyMapping.ts @@ -0,0 +1,68 @@ +import { Object3D, BiMap } from '@orillusion/core'; +import { Ammo } from "../Physics"; + +/** + * A bidirectional mapping between RigidBody objects and 3D objects. + */ +export class RigidBodyMapping { + private static mapping: BiMap = new BiMap(); + + /** + * Retrieves the entire mapping of all RigidBody objects. + * @returns A map of RigidBody objects to 3D objects. + */ + public static get getAllPhysicsObjectMap(): Map { + return this.mapping["negtive"]; + } + + /** + * Retrieves the entire mapping of all 3D objects. + * @returns A map of 3D objects to RigidBody objects. + */ + public static get getAllGraphicObjectMap(): Map { + return this.mapping; + } + + /** + * Adds a mapping between a 3D object and a RigidBody object. + * @param object3D The 3D object. + * @param physics The RigidBody object. + */ + public static addMapping(object3D: Object3D, physics: Ammo.btRigidBody) { + this.mapping.set(object3D, physics); + } + + /** + * Retrieves the RigidBody object associated with a given 3D object. + * @param object3D The 3D object. + * @returns The associated RigidBody object, or undefined if not found. + */ + public static getPhysicsObject(object3D: Object3D): Ammo.btRigidBody | undefined { + return this.mapping.get(object3D); + } + + /** + * Retrieves the 3D object associated with a given RigidBody object. + * @param physics The RigidBody object. + * @returns The associated 3D object, or undefined if not found. + */ + public static getGraphicObject(physics: Ammo.btRigidBody): Object3D | undefined { + return this.mapping.getKey(physics); + } + + /** + * Removes the mapping associated with a given 3D object. + * @param object3D The 3D object. + */ + public static removeMappingByGraphic(object3D: Object3D) { + this.mapping.delete(object3D); + } + + /** + * Removes the mapping associated with a given RigidBody object. + * @param physics The RigidBody object. + */ + public static removeMappingByPhysics(physics: Ammo.btRigidBody) { + this.mapping.deleteValue(physics); + } +} diff --git a/packages/physics/utils/RigidBodyUtil.ts b/packages/physics/utils/RigidBodyUtil.ts new file mode 100644 index 00000000..9971a5c7 --- /dev/null +++ b/packages/physics/utils/RigidBodyUtil.ts @@ -0,0 +1,169 @@ +import { Vector3, Quaternion, Object3D } from '@orillusion/core'; +import { Physics, Ammo } from '../Physics'; +import { TempPhyMath } from './TempPhyMath'; + +/** + * 提供一系列AMMO刚体相关的方法 + */ +export class RigidBodyUtil { + /** + * 创建 Ammo 刚体。 + * @param object3D - 三维对象。 + * @param shape - 碰撞形状。 + * @param mass - 碰撞体的质量。 + * @param position - 可选参数,刚体的位置,默认使用三维对象的 `localPosition` + * @param rotation - 可选参数,刚体的旋转,默认使用三维对象的 `localRotation` + * @returns 新创建的 Ammo.btRigidBody 对象。 + */ + public static createRigidBody(object3D: Object3D, shape: Ammo.btCollisionShape, mass: number, position?: Vector3, rotation?: Vector3 | Quaternion): Ammo.btRigidBody { + position ||= object3D.localPosition; + rotation ||= object3D.localRotation; + + const transform = Physics.TEMP_TRANSFORM; + transform.setIdentity(); + transform.setOrigin(TempPhyMath.toBtVec(position)); + let rotQuat = (rotation instanceof Vector3) ? TempPhyMath.eulerToBtQua(rotation) : TempPhyMath.toBtQua(rotation); + transform.setRotation(rotQuat); + + const motionState = new Ammo.btDefaultMotionState(transform); + const localInertia = TempPhyMath.zeroBtVec(); + shape.calculateLocalInertia(mass, localInertia); + + const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia); + const bodyRb = new Ammo.btRigidBody(rbInfo); + + return bodyRb; + } + + /** + * 更新刚体的位置和旋转。 + * 此函数将新的位置和旋转应用到刚体上。 + * @param bodyRb - 刚体对象。 + * @param position - 刚体的新位置,以 Vector3 形式表示。 + * @param rotation - 刚体的新旋转,可选,可以是 Vector3 形式表示的欧拉角(将自动转换为四元数),默认为四元数零值。 + * @param clearFV - 清除力和速度,可选,默认为 false 。 + */ + public static updateTransform(bodyRb: Ammo.btRigidBody, position: Vector3, rotation: Vector3 | Quaternion, clearFV?: boolean) { + rotation ||= Quaternion._zero; + + const transform = bodyRb.getWorldTransform(); + transform.setOrigin(TempPhyMath.toBtVec(position)); + let rotQuat = (rotation instanceof Vector3) ? TempPhyMath.eulerToBtQua(rotation) : TempPhyMath.toBtQua(rotation); + transform.setRotation(rotQuat); + + bodyRb.setWorldTransform(transform); + bodyRb.getMotionState().setWorldTransform(transform); + bodyRb.activate(); + + if (clearFV) { + this.clearForcesAndVelocities(bodyRb); + } + } + + /** + * 更新刚体位置 + * @param bodyRb + * @param value + */ + public static updatePosition(bodyRb: Ammo.btRigidBody, value: Vector3) { + if (bodyRb.isKinematicObject()) { + bodyRb.getMotionState().getWorldTransform(Physics.TEMP_TRANSFORM); + Physics.TEMP_TRANSFORM.setOrigin(TempPhyMath.toBtVec(value)); + bodyRb.getMotionState().setWorldTransform(Physics.TEMP_TRANSFORM); + } else { + bodyRb.getWorldTransform().getOrigin().setValue(value.x, value.y, value.z); + bodyRb.activate(); + } + } + + /** + * 更新刚体旋转 + * @param bodyRb + * @param value + */ + public static updateRotation(bodyRb: Ammo.btRigidBody, value: Vector3) { + if (bodyRb.isKinematicObject()) { + bodyRb.getMotionState().getWorldTransform(Physics.TEMP_TRANSFORM); + Physics.TEMP_TRANSFORM.setRotation(TempPhyMath.eulerToBtQua(value)); + bodyRb.getMotionState().setWorldTransform(Physics.TEMP_TRANSFORM); + } else { + bodyRb.getWorldTransform().setRotation(TempPhyMath.eulerToBtQua(value)); + bodyRb.activate(); + } + } + + /** + * 更新刚体缩放 + * @param bodyRb + * @param value + * @param mass + */ + public static updateScale(bodyRb: Ammo.btRigidBody, value: Vector3, mass: number) { + const shape = bodyRb.getCollisionShape(); + shape.setLocalScaling(TempPhyMath.toBtVec(value)); + if (mass > 0) { + const localInertia = TempPhyMath.zeroBtVec(); + shape.calculateLocalInertia(mass, localInertia); + bodyRb.setMassProps(mass, localInertia); + bodyRb.activate(); + } + } + + /** + * 清除力和速度 + * @param bodyRb + */ + public static clearForcesAndVelocities(bodyRb: Ammo.btRigidBody) { + bodyRb.clearForces(); + bodyRb.setLinearVelocity(TempPhyMath.zeroBtVec()); + bodyRb.setAngularVelocity(TempPhyMath.zeroBtVec()); + } + + /** + * 激活物理世界中的全部碰撞对 + */ + public static activateCollisionBodies(): void { + const dispatcher = Physics.world.getDispatcher(); + const numManifolds = dispatcher.getNumManifolds(); + + for (let i = 0; i < numManifolds; i++) { + const manifold = dispatcher.getManifoldByIndexInternal(i); + + const body0 = Ammo.castObject(manifold.getBody0(), Ammo.btRigidBody); + const body1 = Ammo.castObject(manifold.getBody1(), Ammo.btRigidBody); + + if (body0 && body0.getMotionState()) { + body0.activate(); + } + + if (body1 && body1.getMotionState()) { + body1.activate(); + } + } + } + + /** + * 销毁刚体及其状态和形状 + * @param bodyRb + */ + public static destroyRigidBody(bodyRb: Ammo.btRigidBody): void { + if (!bodyRb) return console.warn('There is no rigid body'); + + Physics.world.removeRigidBody(bodyRb); + Ammo.destroy(bodyRb.getCollisionShape()); + Ammo.destroy(bodyRb.getMotionState()); + Ammo.destroy(bodyRb); + } + + /** + * 销毁约束 + * @param constraint + */ + public static destroyConstraint(constraint: Ammo.btTypedConstraint) { + if (constraint) { + Physics.world.removeConstraint(constraint); + Ammo.destroy(constraint); + constraint = null; + } + } +} diff --git a/packages/physics/utils/TempPhyMath.ts b/packages/physics/utils/TempPhyMath.ts new file mode 100644 index 00000000..dae04212 --- /dev/null +++ b/packages/physics/utils/TempPhyMath.ts @@ -0,0 +1,105 @@ +import { Vector3, Quaternion, DEGREES_TO_RADIANS } from '@orillusion/core'; +import { Ammo } from '../Physics'; + +/** + * Temporary Physics Math Utility + * + * 提供临时的 Ammo btVector3 和 btQuaternion 实例,并支持与引擎数据相互转换 + */ +export class TempPhyMath { + public static readonly tmpVecA: Ammo.btVector3; + public static readonly tmpVecB: Ammo.btVector3; + public static readonly tmpVecC: Ammo.btVector3; + public static readonly tmpVecD: Ammo.btVector3; + public static readonly tmpQuaA: Ammo.btQuaternion; + public static readonly tmpQuaB: Ammo.btQuaternion; + + /** + * 初始化 Ammo 后创建预定义的 btVector3 和 btQuaternion 实例,以便复用 + */ + public static init() { + (this as any).tmpVecA = new Ammo.btVector3(0, 0, 0); + (this as any).tmpVecB = new Ammo.btVector3(0, 0, 0); + (this as any).tmpVecC = new Ammo.btVector3(0, 0, 0); + (this as any).tmpVecD = new Ammo.btVector3(0, 0, 0); + (this as any).tmpQuaA = new Ammo.btQuaternion(0, 0, 0, 1); + (this as any).tmpQuaB = new Ammo.btQuaternion(0, 0, 0, 1); + } + + /** + * Quaternion to Ammo.btQuaternion + */ + public static toBtQua(qua: Quaternion, btQua?: Ammo.btQuaternion): Ammo.btQuaternion { + btQua ||= this.tmpQuaA; + btQua.setValue(qua.x, qua.y, qua.z, qua.w); + return btQua; + } + + /** + * Vector3 to Ammo.btVector3 + */ + public static toBtVec(vec: Vector3, btVec?: Ammo.btVector3): Ammo.btVector3 { + btVec ||= this.tmpVecA; + btVec.setValue(vec.x, vec.y, vec.z); + return btVec; + } + + /** + * Set Ammo.btVector3 using x, y, z + */ + public static setBtVec(x: number, y: number, z: number, btVec?: Ammo.btVector3): Ammo.btVector3 { + btVec ||= this.tmpVecA; + btVec.setValue(x, y, z); + return btVec; + } + + /** + * Set Ammo.btQuaternion using x, y, z, w + */ + public static setBtQua(x: number, y: number, z: number, w: number, btQua?: Ammo.btQuaternion): Ammo.btQuaternion { + btQua ||= this.tmpQuaA; + btQua.setValue(x, y, z, w); + return btQua; + } + + /** + * Ammo.btVector3 to Vector3 + */ + public static fromBtVec(btVec: Ammo.btVector3, vec?: Vector3): Vector3 { + vec ||= new Vector3(); + vec.set(btVec.x(), btVec.y(), btVec.z()); + return vec; + } + + /** + * Ammo.btQuaternion to Quaternion + */ + public static fromBtQua(btQua: Ammo.btQuaternion, qua?: Quaternion): Quaternion { + qua ||= new Quaternion(); + qua.set(btQua.x(), btQua.y(), btQua.z(), btQua.w()); + return qua; + } + + /** + * Euler Vector3 to Ammo.Quaternion + */ + public static eulerToBtQua(vec: Vector3, qua?: Ammo.btQuaternion): Ammo.btQuaternion { + qua ||= this.tmpQuaA; + qua.setEulerZYX(vec.z * DEGREES_TO_RADIANS, vec.y * DEGREES_TO_RADIANS, vec.x * DEGREES_TO_RADIANS); + return qua; + } + + /** + * Sets the given Ammo.btVector3 to (0, 0, 0) + */ + public static zeroBtVec(btVec?: Ammo.btVector3): Ammo.btVector3 { + return this.setBtVec(0, 0, 0, btVec); + } + + /** + * Sets the given Ammo.btQuaternion to (0, 0, 0, 1) + */ + public static resetBtQua(btQua?: Ammo.btQuaternion): Ammo.btQuaternion { + return this.setBtQua(0, 0, 0, 1, btQua); + } +} diff --git a/packages/physics/visualDebug/DebugDrawModeEnum.ts b/packages/physics/visualDebug/DebugDrawModeEnum.ts new file mode 100644 index 00000000..91287730 --- /dev/null +++ b/packages/physics/visualDebug/DebugDrawModeEnum.ts @@ -0,0 +1,90 @@ +export type DebugDrawerOptions = Partial<{ + /** + * 启用状态,默认 false + */ + enable: boolean; + /** + * 设置 debug 模式,默认值为 1 (DrawWireframe: 绘制物理对象的线框) + */ + debugDrawMode: DebugDrawMode; + /** + * 更新频率,默认每 1 帧更新一次 + */ + updateFreq: number; + /** + * 最多渲染的线条,默认 25,000 (超过 32,000 可能会导致错误 V0.8.2) + */ + maxLineCount: number; +}>; + + +export enum DebugDrawMode { + /** + * 不显示调试信息 + */ + NoDebug = 0, + /** + * 绘制物理对象的线框 + */ + DrawWireframe = 1, + /** + * 绘制物理对象的包围盒(AABB) + */ + DrawAabb = 2, + /** + * 绘制特征点文本 + */ + DrawFeaturesText = 4, + /** + * 绘制接触点 + */ + DrawContactPoints = 8, + /** + * 禁用去激活 + */ + NoDeactivation = 16, + /** + * 不显示帮助文本 + */ + NoHelpText = 32, + /** + * 绘制文本信息 + */ + DrawText = 64, + /** + * 显示性能计时信息 + */ + ProfileTimings = 128, + /** + * 启用 SAT 比较 + */ + EnableSatComparison = 256, + /** + * 禁用 Bullet 的 LCP 算法 + */ + DisableBulletLCP = 512, + /** + * 启用连续碰撞检测 + */ + EnableCCD = 1024, + /** + * 绘制约束 + */ + DrawConstraints = 2048, + /** + * 绘制约束限制 + */ + DrawConstraintLimits = 4096, + /** + * 绘制快速剔除代理的 AABB + */ + FastWireframe = 8192, + /** + * 绘制动态 AABB 树 + */ + DrawAabbDynamic = 16384, + /** + * 绘制软体物理 + */ + DrawSoftBodies = 32768, +} \ No newline at end of file diff --git a/packages/physics/visualDebug/PhysicsDebugDrawer.ts b/packages/physics/visualDebug/PhysicsDebugDrawer.ts new file mode 100644 index 00000000..ed5f42d1 --- /dev/null +++ b/packages/physics/visualDebug/PhysicsDebugDrawer.ts @@ -0,0 +1,172 @@ +import { Object3D, Vector3, Color, GetCountInstanceID } from "@orillusion/core"; +import { Ammo } from '../Physics'; +import { TempPhyMath } from '../utils/TempPhyMath'; +import { DebugDrawMode, DebugDrawerOptions } from "./DebugDrawModeEnum"; + +export class PhysicsDebugDrawer { + private debugDrawer: Ammo.DebugDrawer; + private _enable: boolean = true; + private frameCount: number = 0; + /** + * A graphic object used to draw lines + * + * Type: `Graphic3D` + */ + private graphic3D; + + // Exceeding 32,000 lines may cause engine crash. + private lineCount: number = 0; + private lineNameList: string[] = []; + private readonly _tmpCor: Color = new Color(); + private readonly _tmpVecA: Vector3 = new Vector3(); + private readonly _tmpVecB: Vector3 = new Vector3(); + + public world: Ammo.btDiscreteDynamicsWorld | Ammo.btSoftRigidDynamicsWorld; + public debugMode: number; + public updateFreq: number; + public maxLineCount: number; + + public readonly debugModeList = { + NoDebug: DebugDrawMode.NoDebug, + DrawWireframe: DebugDrawMode.DrawWireframe, + DrawAabb: DebugDrawMode.DrawAabb, + DrawFeaturesText: DebugDrawMode.DrawFeaturesText, + DrawContactPoints: DebugDrawMode.DrawContactPoints, + NoDeactivation: DebugDrawMode.NoDeactivation, + NoHelpText: DebugDrawMode.NoHelpText, + DrawText: DebugDrawMode.DrawText, + ProfileTimings: DebugDrawMode.ProfileTimings, + EnableSatComparison: DebugDrawMode.EnableSatComparison, + DisableBulletLCP: DebugDrawMode.DisableBulletLCP, + EnableCCD: DebugDrawMode.EnableCCD, + DrawConstraints: DebugDrawMode.DrawConstraints, + DrawConstraintLimits: DebugDrawMode.DrawConstraintLimits, + FastWireframe: DebugDrawMode.FastWireframe, + DrawAabbDynamic: DebugDrawMode.DrawAabbDynamic, + DrawSoftBodies: DebugDrawMode.DrawSoftBodies, + }; + + constructor(world: Ammo.btDiscreteDynamicsWorld | Ammo.btSoftRigidDynamicsWorld, graphic3D: Object3D, options: DebugDrawerOptions = {}) { + if (!graphic3D) throw new Error("Physics Debug Drawer requires a Graphic3D object."); + + this.world = world; + this.graphic3D = graphic3D; + + this._enable = options.enable || false; + this.debugMode = options.debugDrawMode ?? DebugDrawMode.DrawWireframe; + this.updateFreq = options.updateFreq || 1; + this.maxLineCount = options.maxLineCount || 25000; + + this.debugDrawer = new Ammo.DebugDrawer(); + this.debugDrawer.drawLine = this.drawLine.bind(this); + this.debugDrawer.drawContactPoint = this.drawContactPoint.bind(this); + this.debugDrawer.reportErrorWarning = this.reportErrorWarning.bind(this); + this.debugDrawer.draw3dText = this.draw3dText.bind(this); + this.debugDrawer.setDebugMode = this.setDebugMode.bind(this); + this.debugDrawer.getDebugMode = this.getDebugMode.bind(this); + + this.world.setDebugDrawer(this.debugDrawer); + } + + /** + * 启用/禁用物理调试绘制 + */ + public set enable(value: boolean) { + this._enable = value; + if (this.lineNameList.length > 0) { + this.clearLines() + } + + // this.world.setDebugDrawer(value ? this.debugDrawer : null); + + } + + public get enable() { + return this._enable; + } + + public setDebugMode(debugMode: DebugDrawMode): void { + this.debugMode = debugMode; + } + + public getDebugMode(): DebugDrawMode { + return this.debugMode; + } + + public update(): void { + if (!this._enable) return; + + if (++this.frameCount % this.updateFreq !== 0) return; + + this.clearLines(); + + this.world.debugDrawWorld(); + + // console.log(this.lineCount); + this.lineCount = 0; + } + + private drawLine(from: Ammo.btVector3, to: Ammo.btVector3, color: Ammo.btVector3): void { + if (!this._enable) return; + + if (++this.lineCount > this.maxLineCount) return; // console.log(`超出限制,正在渲染第 ${this.lineCount} 条线`); + + const fromVector = Ammo.wrapPointer(from as unknown as number, Ammo.btVector3); + const toVector = Ammo.wrapPointer(to as unknown as number, Ammo.btVector3); + const colorVector = Ammo.wrapPointer(color as unknown as number, Ammo.btVector3); + + const lineColor = this._tmpCor.copyFromVector(TempPhyMath.fromBtVec(colorVector, this._tmpVecA)); + const p0 = TempPhyMath.fromBtVec(fromVector, this._tmpVecA); + const p1 = TempPhyMath.fromBtVec(toVector, this._tmpVecB); + + const name = `AmmoLine_${this.lineCount}`; + this.lineNameList.push(name); + // Engine3D.views[this.viewIndex].graphic3D.drawLines(name, [p0, p1], lineColor); + this.graphic3D.drawLines(name, [p0, p1], lineColor); + + } + + private drawContactPoint(pointOnB: Ammo.btVector3, normalOnB: Ammo.btVector3, distance: number, lifeTime: number, color: Ammo.btVector3): void { + if (!this._enable) return; + + if (++this.lineCount > this.maxLineCount) return; // console.log(`超出限制,正在渲染第 ${this.lineCount} 条线`); + + const colorVector = Ammo.wrapPointer(color as unknown as number, Ammo.btVector3); + const pointOnBVector = Ammo.wrapPointer(pointOnB as unknown as number, Ammo.btVector3); + const normalOnBVector = Ammo.wrapPointer(normalOnB as unknown as number, Ammo.btVector3); + + const lineColor = this._tmpCor.copyFromVector(TempPhyMath.fromBtVec(colorVector, this._tmpVecA)); + const p0 = TempPhyMath.fromBtVec(pointOnBVector, this._tmpVecA); + const normal = TempPhyMath.fromBtVec(normalOnBVector, this._tmpVecB); + const p1 = p0.add(normal.multiplyScalar(distance), this._tmpVecB); + + const name = `AmmoContactPoint_${GetCountInstanceID()}`; + this.lineNameList.push(name); + + // Engine3D.views[this.viewIndex].graphic3D.drawLines(name, [p0, p1], lineColor); + this.graphic3D.drawLines(name, [p0, p1], lineColor); + + // 在接触点生命周期结束后进行清理 + // setTimeout(() => { + // Engine3D.views[this.viewIndex].graphic3D.Clear(name) + // }, lifeTime * 1000); + } + + private reportErrorWarning(warningString: string): void { + const warning = Ammo.UTF8ToString(warningString as unknown as number); + console.error(warning); + } + + private draw3dText(location: Ammo.btVector3, textString: string): void { + const _location = Ammo.wrapPointer(location as unknown as number, Ammo.btVector3); + const _textString = Ammo.UTF8ToString(textString as unknown as number); + console.log("draw3dText", _location, _textString); + } + + private clearLines(): void { + // let view = Engine3D.views[this.viewIndex]; + // this.lineNameList.forEach(name => view.graphic3D.Clear(name)); + this.lineNameList.forEach(name => this.graphic3D.Clear(name)); + this.lineNameList.length = 0; + } +} diff --git a/packages/stats/package.json b/packages/stats/package.json index 9147f621..ce1ea61e 100644 --- a/packages/stats/package.json +++ b/packages/stats/package.json @@ -1,6 +1,6 @@ { "name": "@orillusion/stats", - "version": "0.2.2", + "version": "0.2.6", "author": "Orillusion", "description": "Orillusion Stats Plugin", "main": "./dist/stats.umd.js", @@ -20,7 +20,7 @@ "type": "git", "url": "git+https://github.com/Orillusion/orillusion.git" }, - "dependencies": { - "@orillusion/core": "^0.7.0" + "peerDependencies": { + "@orillusion/core": ">=0.8.0" } } diff --git a/packages/wasm-matrix/WasmMatrix.ts b/packages/wasm-matrix/WasmMatrix.ts index a76d212a..94390ec9 100644 --- a/packages/wasm-matrix/WasmMatrix.ts +++ b/packages/wasm-matrix/WasmMatrix.ts @@ -1,38 +1,32 @@ -import { Matrix4 } from '../../src'; -import matrixjs from './matrix?raw' +import { Engine3D, Matrix4 } from '../../src'; +import matrix from './matrix'; + +export type FloatArray = Float32Array | Float64Array; + +export function CreateFloatArray(buffer: ArrayBufferLike, byteOffset?: number, length?: number) { + if (Engine3D.setting.doublePrecision) + return new Float64Array(buffer, byteOffset, length); + return new Float32Array(buffer, byteOffset, length); +} export class WasmMatrix { - public static matrixBuffer: Float32Array; - public static matrixSRTBuffer: Float32Array; - public static matrixContinuedSRTBuffer: Float32Array; + public static matrixBuffer: FloatArray; + public static matrixSRTBuffer: FloatArray; + public static matrixContinuedSRTBuffer: FloatArray; public static matrixStateBuffer: Int32Array; static matrixBufferPtr: number; static matrixSRTBufferPtr: number; static matrixContinuedSRTBufferPtr: number; static matrixStateBufferPtr: number; - static wasm: any; + static wasm: typeof matrix; static stateStruct: number = 4; + static useDoublePrecision: boolean = false; - public static async init(count: number) { - await new Promise((resolve)=>{ - const script = document.createElement('script'); - script.async = true; - script.type = "text/javascript"; - script.src = URL.createObjectURL(new Blob([matrixjs])); - document.head.appendChild(script) - script.onload = () => { - let check = ()=>{ - this.wasm = window['wasmMatrix']; - if (this.wasm && this.wasm['calledRun']) - resolve(true) - else - setTimeout(check, 20) - } - check() - } - }) - // this.wasm = window['wasmMatrix']; + public static async init(count: number, useDoublePrecision: boolean = false) { + this.wasm = await matrix(); + this.useDoublePrecision = useDoublePrecision; + this.wasm._initialize(count, useDoublePrecision, 0); this.allocMatrix(count); } @@ -41,16 +35,25 @@ export class WasmMatrix { console.error(`The maximum allocation size is exceeded! current:${count}, limit:${Matrix4.maxCount}`); } - this.wasm._allocation(count); + this.wasm._allocMatrix(count); this.matrixBufferPtr = this.wasm._getMatrixBufferPtr(); this.matrixSRTBufferPtr = this.wasm._getSRTPtr(); this.matrixStateBufferPtr = this.wasm._getInfoPtr(); this.matrixContinuedSRTBufferPtr = this.wasm._getContinuedSRTPtr(); - this.matrixBuffer = new Float32Array(this.wasm.HEAPF32.buffer, this.matrixBufferPtr, 16 * count); - this.matrixSRTBuffer = new Float32Array(this.wasm.HEAPF32.buffer, this.matrixSRTBufferPtr, (3 * 3) * count); - this.matrixContinuedSRTBuffer = new Float32Array(this.wasm.HEAPF32.buffer, this.matrixContinuedSRTBufferPtr, (3 * 3) * count); + if (this.useDoublePrecision) { + this.matrixBuffer = CreateFloatArray(this.wasm.HEAPF64.buffer, this.matrixBufferPtr, 16 * count); + this.matrixSRTBuffer = CreateFloatArray(this.wasm.HEAPF64.buffer, this.matrixSRTBufferPtr, (3 * 3) * count); + this.matrixContinuedSRTBuffer = CreateFloatArray(this.wasm.HEAPF64.buffer, this.matrixContinuedSRTBufferPtr, (3 * 3) * count); + Matrix4.blockBytes = Matrix4.block * 8; + } else { + this.matrixBuffer = CreateFloatArray(this.wasm.HEAPF32.buffer, this.matrixBufferPtr, 16 * count); + this.matrixSRTBuffer = CreateFloatArray(this.wasm.HEAPF32.buffer, this.matrixSRTBufferPtr, (3 * 3) * count); + this.matrixContinuedSRTBuffer = CreateFloatArray(this.wasm.HEAPF32.buffer, this.matrixContinuedSRTBufferPtr, (3 * 3) * count); + Matrix4.blockBytes = Matrix4.block * 4; + } + this.matrixStateBuffer = new Int32Array(this.wasm.HEAP32.buffer, this.matrixStateBufferPtr, (WasmMatrix.stateStruct) * count); Matrix4.allocMatrix(count); diff --git a/packages/wasm-matrix/_WasmMatrix4.d.ts b/packages/wasm-matrix/matrix.d.ts similarity index 83% rename from packages/wasm-matrix/_WasmMatrix4.d.ts rename to packages/wasm-matrix/matrix.d.ts index d85045fd..22bd5392 100644 --- a/packages/wasm-matrix/_WasmMatrix4.d.ts +++ b/packages/wasm-matrix/matrix.d.ts @@ -13,15 +13,16 @@ declare module Module { const HEAPF32: Float32Array; const HEAPF64: Float64Array; - function _initialize(count: number); + function _initialize(count: number, useDoublePrecision: boolean, threadCount: number); + function _allocMatrix(count: number); + function _updateAllMatrixTransform(start: number, end: number); - function _updateAllMatrixContinueTransform(start: number, end: number); + function _updateAllMatrixContinueTransform(start: number, end: number, dt: number); function _printMatrix(index: number); - function _getSRTPtr(): number; function _getInfoPtr(): number; diff --git a/packages/wasm-matrix/matrix.js b/packages/wasm-matrix/matrix.js index 25ba13f9..12a52892 100644 --- a/packages/wasm-matrix/matrix.js +++ b/packages/wasm-matrix/matrix.js @@ -1,7 +1,15 @@ -var Module=typeof Module!="undefined"?Module:{};var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");if(ENVIRONMENT_IS_WORKER){scriptDirectory=nodePath.dirname(scriptDirectory)+"/"}else{scriptDirectory=__dirname+"/"}read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,binary?undefined:"utf8",((err,data)=>{if(err)onerror(err);else onload(binary?data.buffer:data)}))};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}process.on("uncaughtException",(ex=>{if(ex!=="unwind"&&!(ex instanceof ExitStatus)&&!(ex.context instanceof ExitStatus)){throw ex}}));quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow};Module["inspect"]=()=>"[Emscripten Module object]"}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}setWindowTitle=title=>document.title=title}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeKeepaliveCounter=0;function keepRuntimeAlive(){return noExitRuntime||runtimeKeepaliveCounter>0}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return filename.startsWith(dataURIPrefix)}function isFileURI(filename){return filename.startsWith("file://")}var wasmBinaryFile;wasmBinaryFile="data:application/octet-stream;base64,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IAZBAmsiCiAFQQ9qOgAAIAZBAWtBLUErIAdBAEgbOgAAIARBCHEhBiAMQRBqIQcDQCAHIgUCfyABmUQAAAAAAADgQWMEQCABqgwBC0GAgICAeAsiB0HgI2otAAAgDXI6AAAgASAHt6FEAAAAAAAAMECiIQECQCAFQQFqIgcgDEEQamtBAUcNAAJAIAYNACADQQBKDQAgAUQAAAAAAAAAAGENAQsgBUEuOgABIAVBAmohBwsgAUQAAAAAAAAAAGINAAtBfyEJQf3///8HIAsgESAKayIGaiINayADSA0AIABBICACIA0gA0ECaiAHIAxBEGoiB2siBSAFQQJrIANIGyAFIAMbIglqIgMgBBAGIAAgCCALEAUgAEEwIAIgAyAEQYCABHMQBiAAIAcgBRAFIABBMCAJIAVrQQBBABAGIAAgCiAGEAUgAEEgIAIgAyAEQYDAAHMQBiADIAIgAiADSBshCQsgDEGwBGokACAJC7QCAAJAAkACQAJAAkACQAJAAkACQAJAAkAgAUEJaw4SAAgJCggJAQIDBAoJCgoICQUGBwsgAiACKAIAIgFBBGo2AgAgACABKAIANgIADwsgAiACKAIAIgFBBGo2AgAgACABMgEANwMADwsgAiACKAIAIgFBBGo2AgAgACABMwEANwMADwsgAiACKAIAIgFBBGo2AgAgACABMAAANwMADwsgAiACKAIAIgFBBGo2AgAgACABMQAANwMADwsgAiACKAIAQQdqQXhxIgFBCGo2AgAgACABKwMAOQMADwsACw8LIAIgAigCACIBQQRqNgIAIAAgATQCADcDAA8LIAIgAigCACIBQQRqNgIAIAAgATUCADcDAA8LIAIgAigCAEEHakF4cSIBQQhqNgIAIAAgASkDADcDAAtyAQN/IAAoAgAsAABBMGtBCk8EQEEADwsDQCAAKAIAIQNBfyEBIAJBzJmz5gBNBEBBfyADLAAAQTBrIgEgAkEKbCICaiABIAJB/////wdzShshAQsgACADQQFqNgIAIAEhAiADLAABQTBrQQpJDQALIAILkRQCE38BfkGHCSELIwBB0ABrIgUkACAFQYcJNgJMIAVBN2ohFSAFQThqIRACQAJAAkACQANAIAshCiAEIAxB/////wdzSg0BIAQgDGohDAJAAkACQCAKIgQtAAAiBgRAA0ACQAJAIAZB/wFx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GAALIAAgAU8NAEHoKSACIABBBnRqNgIACwJAQfQpKAIAQfApKAIAIgJrQSRtIgEgAEkEQEHwKSAAIAFrEC8MAQsgACABTw0AQfQpIAIgAEEkbGo2AgALAkBBgCooAgBB/CkoAgAiAmtBBHUiASAASQRAQQAhBEEAIQcgACABayIDQYQqKAIAIgZBgCooAgAiAmtBBHVNBEACQCADRQ0AIAIhASADQQdxIggEQANAIAFC/////w83AgggAUIANwIAIAFBEGohASAEQQFqIgQgCEcNAAsLIANBBHQgAmohAiADQQFrQf////8AcUEHSQ0AA0AgAUIANwJwIAFCADcCYCABQgA3AlAgAUIANwJAIAFCADcCMCABQgA3AiAgAUIANwIQIAFC/////w83AgggAUIANwIAIAFC/////w83AnggAUL/////DzcCaCABQv////8PNwJYIAFC/////w83AkggAUL/////DzcCOCABQv////8PNwIoIAFC/////w83AhggAUGAAWoiASACRw0ACwtBgCogAjYCAAwCCwJAIAJB/CkoAgAiAWtBBHUiBCADaiIFQYCAgIABSQRAQf////8AIAYgAWsiBkEDdSIBIAUgASAFSxsgBkHw////B08bIgUEQCAFQYCAgIABTw0CIAVBBHQQDiEHCyAHIARBBHRqIgQhASADQQdxIgYEQANAIAFC/////w83AgggAUIANwIAIAFBEGohASAIQQFqIgggBkcNAAsLIANBBHQgBGohCCADQQFrQf////8AcUEHTwRAA0AgAUIANwJwIAFCADcCYCABQgA3AlAgAUIANwJAIAFCADcCMCABQgA3AiAgAUIANwIQIAFC/////w83AgggAUIANwIAIAFC/////w83AnggAUL/////DzcCaCABQv////8PNwJYIAFC/////w83AkggAUL/////DzcCOCABQv////8PNwIoIAFC/////w83AhggAUGAAWoiASAIRw0ACwtB/CkoAgAiASACRwRAA0AgBEEQayIEIAJBEGsiAv0AAgD9CwIAIAEgAkcNAAtB/CkoAgAhAgtBhCogByAFQQR0ajYCAEGAKiAINgIAQfwpIAQ2AgAgAgRAIAIQEAsMAwsQGQALEBgACyAAIAFPDQBBgCogAiAAQQR0ajYCAAtBjCooAgBBiCooAgAiAmtBJG0i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";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch=="function"&&!isFileURI(binaryFile)){return fetch(binaryFile,{credentials:"same-origin"}).then((response=>{if(!response["ok"]){throw"failed to load wasm binary file at '"+binaryFile+"'"}return response["arrayBuffer"]()})).catch((()=>getBinarySync(binaryFile)))}else if(readAsync){return new Promise(((resolve,reject)=>{readAsync(binaryFile,(response=>resolve(new Uint8Array(response))),reject)}))}}return Promise.resolve().then((()=>getBinarySync(binaryFile)))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then((binary=>WebAssembly.instantiate(binary,imports))).then((instance=>instance)).then(receiver,(reason=>{err("failed to asynchronously prepare wasm: "+reason);abort(reason)}))}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then((response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,(function(reason){err("wasm streaming compile failed: "+reason);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)}))}))}return instantiateArrayBuffer(binaryFile,imports,callback)}function createWasm(){var info={"a":wasmImports};function receiveInstance(instance,module){var exports=instance.exports;Module["asm"]=exports;wasmMemory=Module["asm"]["f"];updateMemoryViews();wasmTable=Module["asm"]["h"];addOnInit(Module["asm"]["g"]);removeRunDependency("wasm-instantiate");return exports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err("Module.instantiateWasm callback failed with error: "+e);return false}}instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult);return{}}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};function ExceptionInfo(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24;this.set_type=function(type){HEAPU32[this.ptr+4>>2]=type};this.get_type=function(){return HEAPU32[this.ptr+4>>2]};this.set_destructor=function(destructor){HEAPU32[this.ptr+8>>2]=destructor};this.get_destructor=function(){return HEAPU32[this.ptr+8>>2]};this.set_caught=function(caught){caught=caught?1:0;HEAP8[this.ptr+12>>0]=caught};this.get_caught=function(){return HEAP8[this.ptr+12>>0]!=0};this.set_rethrown=function(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13>>0]=rethrown};this.get_rethrown=function(){return HEAP8[this.ptr+13>>0]!=0};this.init=function(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)};this.set_adjusted_ptr=function(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr};this.get_adjusted_ptr=function(){return HEAPU32[this.ptr+16>>2]};this.get_exception_ptr=function(){var isPointer=___cxa_is_pointer_type(this.get_type());if(isPointer){return HEAPU32[this.excPtr>>2]}var adjusted=this.get_adjusted_ptr();if(adjusted!==0)return adjusted;return this.excPtr}}var exceptionLast=0;var uncaughtExceptionCount=0;function ___cxa_throw(ptr,type,destructor){var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast}var _abort=()=>{abort("")};var _emscripten_memcpy_big=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=size-b.byteLength+65535>>>16;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}var alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf8"):undefined;var UTF8ArrayToString=(heapOrArray,idx,maxBytesToRead)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={varargs:undefined,get(){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret},getStr(ptr){var ret=UTF8ToString(ptr);return ret}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){if(Module["onExit"])Module["onExit"](code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var wasmImports={b:___cxa_throw,d:_abort,e:_emscripten_memcpy_big,c:_emscripten_resize_heap,a:_fd_write};var asm=createWasm();var ___wasm_call_ctors=function(){return(___wasm_call_ctors=Module["asm"]["g"]).apply(null,arguments)};var _allocation=Module["_allocation"]=function(){return(_allocation=Module["_allocation"]=Module["asm"]["i"]).apply(null,arguments)};var _getMatrixBufferPtr=Module["_getMatrixBufferPtr"]=function(){return(_getMatrixBufferPtr=Module["_getMatrixBufferPtr"]=Module["asm"]["j"]).apply(null,arguments)};var _getSRTPtr=Module["_getSRTPtr"]=function(){return(_getSRTPtr=Module["_getSRTPtr"]=Module["asm"]["k"]).apply(null,arguments)};var _getInfoPtr=Module["_getInfoPtr"]=function(){return(_getInfoPtr=Module["_getInfoPtr"]=Module["asm"]["l"]).apply(null,arguments)};var _getContinuedSRTPtr=Module["_getContinuedSRTPtr"]=function(){return(_getContinuedSRTPtr=Module["_getContinuedSRTPtr"]=Module["asm"]["m"]).apply(null,arguments)};var _printMatrix=Module["_printMatrix"]=function(){return(_printMatrix=Module["_printMatrix"]=Module["asm"]["n"]).apply(null,arguments)};var _updateAllMatrixContinueTransform=Module["_updateAllMatrixContinueTransform"]=function(){return(_updateAllMatrixContinueTransform=Module["_updateAllMatrixContinueTransform"]=Module["asm"]["o"]).apply(null,arguments)};var _main=Module["_main"]=function(){return(_main=Module["_main"]=Module["asm"]["p"]).apply(null,arguments)};var ___errno_location=function(){return(___errno_location=Module["asm"]["__errno_location"]).apply(null,arguments)};var stackAlloc=function(){return(stackAlloc=Module["asm"]["q"]).apply(null,arguments)};var ___cxa_is_pointer_type=function(){return(___cxa_is_pointer_type=Module["asm"]["r"]).apply(null,arguments)};var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv>>2;args.forEach((arg=>{HEAP32[argv_ptr++]=stringToUTF8OnStack(arg)}));HEAP32[argv_ptr]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();if(shouldRunNow)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout((function(){setTimeout((function(){Module["setStatus"]("")}),1);doRun()}),1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}var shouldRunNow=true;if(Module["noInitialRun"])shouldRunNow=false;run(); -if (!Module['ENVIRONMENT_IS_PTHREAD']) { - // console.log("is main"); - window['wasmMatrix'] = Module; +var Module = (() => { + var _scriptDir = import.meta.url; + + return ( +async function(moduleArg = {}) { + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;Module["ready"]=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary;if(ENVIRONMENT_IS_NODE){const{createRequire:createRequire}=await import("module");var require=createRequire(import.meta.url);var fs=require("fs");var nodePath=require("path");if(ENVIRONMENT_IS_WORKER){scriptDirectory=nodePath.dirname(scriptDirectory)+"/"}else{scriptDirectory=require("url").fileURLToPath(new URL("./",import.meta.url))}read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)onerror(err);else onload(binary?data.buffer:data)})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];function intArrayFromBase64(s){if(typeof ENVIRONMENT_IS_NODE!="undefined"&&ENVIRONMENT_IS_NODE){var buf=Buffer.from(s,"base64");return new Uint8Array(buf.buffer,buf.byteOffset,buf.length)}var decoded=atob(s);var bytes=new Uint8Array(decoded.length);for(var i=0;ifilename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");var wasmBinaryFile;wasmBinaryFile="data:application/octet-stream;base64,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gICAgID4PzcD0AIgAkKAgICAgICA+D83A6gCIAJCgICAgICAgPg/NwOAAiACQYgDakEAQfAAEAcaIAJCgICAgICAgPg/NwP4AyACQoCAgICAgID4PzcD0AMgAkKAgICAgICA+D83A6gDIAJCgICAgICAgPg/NwOAAyACQYAEaiICIANHDQALCyAAIAM2AgwMAgsCQCADIAAoAggiAmtBB3UiBCAGaiIHQYCAgBBJBEBBACEDQf///w8gBSACayIFQQZ1IgIgByACIAdLGyAFQYD///8HTxsiBwRAIAdBgICAEE8NAiAHQQd0EAshCAsgCCAEQQd0aiIEIQIgBkEDcSIFBEADQCACQQhqQQBB8AAQBxogAkKAgICAgICA+D83A3ggAkKAgICAgICA+D83A1AgAkKAgICAgICA+D83AyggAkKAgICAgICA+D83AwAgAkGAAWohAiADQQFqIgMgBUcNAAsLIAZBB3QgBGohBSAGQQFrQf///w9xQQNPBEADQCACQQhqQQBB8AAQBxogAkKAgICAgICA+D83A3ggAkKAgICAgICA+D83A1AgAkKAgICAgICA+D83AyggAkKAgICAgICA+D83AwAgAkGIAWpBAEHwABAHGiACQoCAgICAgID4PzcD+AEgAkKAgICAgICA+D83A9ABIAJCgICAgICAgPg/NwOoASACQoCAgICAgID4PzcDgAEgAkGIAmpBAEHwABAHGiACQoCAgICAgID4PzcD+AIgAkKAgICAgICA+D83A9ACIAJCgICAgICAgPg/NwOoAiACQoCAgICAgID4PzcDgAIgAkGIA2pBAEHwABAHGiACQoCAgICAgID4PzcD+AMgAkKAgICAgICA+D83A9ADIAJCgICAgICAgPg/NwOoAyACQoCAgICAgID4PzcDgAMgAkGABGoiAiAFRw0ACwsgACgCDCICIAAoAggiA0cEQANAIARBgAFrIgQgAkGAAWsiAkGAARAVGiACIANHDQALIAAoAgghAgsgACAIIAdBB3RqNgIQIAAgBTYCDCAAIAQ2AgggAgRAIAIQFgsMAwsQEwALEBIACyABIAJPDQAgACADIAFBB3RqNgIMCwJAIAAoAhggACgCFCIDa0HIAG0iAiABSQRAIABBFGogASACaxA8DAELIAEgAk8NACAAIAMgAUHIAGxqNgIYCwJAIAAoAiQgACgCICIDa0EEdSICIAFJBEAgAEEgaiABIAJrEDsMAQsgASACTw0AIAAgAyABQQR0ajYCJAsgACgCMCAAKAIsIgNrQcgAbSICIAFJBEAgAEEsaiABIAJrEDwPCyABIAJPDQAgACADIAFByABsajYCMAsLBwAgACgCLAsHACAAKAIgCwcAIAAoAhQLBwAgACgCCAvUBQEHfyABIAAoAggiByAAKAIEIgNrQQR1TQRAAkAgAUUNACADIQIgAUEHcSIFBEADQCACQv////8PNwIIIAJCADcCACACQRBqIQIgBEEBaiIEIAVHDQALCyABQQR0IANqIQMgAUEBa0H/////AHFBB0kNAANAIAJC/////w83AnggAkIANwJwIAJC/////w83AmggAkIANwJgIAJC/////w83AlggAkIANwJQIAJC/////w83AkggAkIANwJAIAJC/////w83AjggAkIANwIwIAJC/////w83AiggAkIANwIgIAJC/////w83AhggAkIANwIQIAJC/////w83AgggAkIANwIAIAJBgAFqIgIgA0cNAAsLIAAgAzYCBA8LAkAgAyAAKAIAIgJrQQR1IgQgAWoiBkGAgICAAUkEQEH/////ACAHIAJrIgdBA3UiAiAGIAIgBksbIAdB8P///wdPGyIGBEAgBkGAgICAAU8NAiAGQQR0EAshCAsgCCAEQQR0aiIEIQIgAUEHcSIHBEADQCACQv////8PNwIIIAJCADcCACACQRBqIQIgBUEBaiIFIAdHDQALCyABQQR0IARqIQUgAUEBa0H/////AHFBB08EQANAIAJC/////w83AnggAkIANwJwIAJC/////w83AmggAkIANwJgIAJC/////w83AlggAkIANwJQIAJC/////w83AkggAkIANwJAIAJC/////w83AjggAkIANwIwIAJC/////w83AiggAkIANwIgIAJC/////w83AhggAkIANwIQIAJC/////w83AgggAkIANwIAIAJBgAFqIgIgBUcNAAsLIAAoAgAiASADRwRAA0AgBEEQayIEIANBEGsiA/0AAgD9CwIAIAEgA0cNAAsgACgCACEDCyAAIAggBkEEdGo2AgggACAFNgIEIAAgBDYCACADBEAgAxAMCw8LEBMACxASAAuLAwEHfyABIAAoAggiAiAAKAIEIgNrQcgAbU0EQCAAIAEEfyADQQAgAUHIAGxByABrIgAgAEHIAHBrQcgAaiIAEAcgAGoFIAMLNgIEDwsCQCADIAAoAgAiBmtByABtIgggAWoiBEHk8bgcSQRAQePxuBwgAiAGa0HIAG0iB0EBdCICIAQgAiAESxsgB0HxuJwOTxsiBARAIARB5PG4HE8NAiAEQcgAbBALIQULIAUgCEHIAGxqIgJBACABQcgAbEHIAGsiASABQcgAcGtByABqIgcQByIBIAdqIQggBSAEQcgAbGohBwJAIAMgBkYEQCABIQUMAQsDQCACQcgAayIFIANByABrIgH9AAMA/QsDACACQThrIANBOGv9AAMA/QsDACACQShrIANBKGv9AAMA/QsDACACQRhrIANBGGv9AAMA/QsDACACQQhrIANBCGsrAwA5AwAgBSECIAEiAyAGRw0ACwsgACAHNgIIIAAgCDYCBCAAIAU2AgAgBgRAIAYQDAsPCxATAAsQEgALGQEBf0GgKygCACIBIAAgASgCACgCGBEBAAsQACMAIABrQXBxIgAkACAACwcAIAAoAgQLBQBBowkLBQBBzQkLBQBBkAkLFwEBf0GgKygCACIAIAAoAgAoAhQRAAALFQAgAEUEQEEADwsgAEHkJhAiQQBHCxoAIAAgASgCCCAFEAoEQCABIAIgAyAEECALCzcAIAAgASgCCCAFEAoEQCABIAIgAyAEECAPCyAAKAIIIgAgASACIAMgBCAFIAAoAgAoAhQRCQALpwEAIAAgASgCCCAEEAoEQAJAIAEoAgQgAkcNACABKAIcQQFGDQAgASADNgIcCw8LAkAgACABKAIAIAQQCkUNAAJAIAIgASgCEEcEQCABKAIUIAJHDQELIANBAUcNASABQQE2AiAPCyABIAI2AhQgASADNgIgIAEgASgCKEEBajYCKAJAIAEoAiRBAUcNACABKAIYQQJHDQAgAUEBOgA2CyABQQQ2AiwLC4gCACAAIAEoAgggBBAKBEACQCABKAIEIAJHDQAgASgCHEEBRg0AIAEgAzYCHAsPCwJAIAAgASgCACAEEAoEQAJAIAIgASgCEEcEQCABKAIUIAJHDQELIANBAUcNAiABQQE2AiAPCyABIAM2AiACQCABKAIsQQRGDQAgAUEAOwE0IAAoAggiACABIAIgAkEBIAQgACgCACgCFBEJACABLQA1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";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}var binary=tryParseAsDataURI(file);if(binary){return binary}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){return instantiateArrayBuffer(binaryFile,imports,callback)}function createWasm(){var info={"a":wasmImports};function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["f"];updateMemoryViews();addOnInit(wasmExports["g"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var noExitRuntime=Module["noExitRuntime"]||true;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}get_exception_ptr(){var isPointer=___cxa_is_pointer_type(this.get_type());if(isPointer){return HEAPU32[this.excPtr>>2]}var adjusted=this.get_adjusted_ptr();if(adjusted!==0)return adjusted;return this.excPtr}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var _abort=()=>{abort("")};var _emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}var alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf8"):undefined;var UTF8ArrayToString=(heapOrArray,idx,maxBytesToRead)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={varargs:undefined,get(){var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret},getp(){return SYSCALLS.get()},getStr(ptr){var ret=UTF8ToString(ptr);return ret}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var wasmImports={b:___cxa_throw,c:_abort,e:_emscripten_memcpy_js,d:_emscripten_resize_heap,a:_fd_write};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["g"])();var _initialize=Module["_initialize"]=(a0,a1,a2)=>(_initialize=Module["_initialize"]=wasmExports["h"])(a0,a1,a2);var _allocMatrix=Module["_allocMatrix"]=a0=>(_allocMatrix=Module["_allocMatrix"]=wasmExports["i"])(a0);var _getMatrixBufferPtr=Module["_getMatrixBufferPtr"]=()=>(_getMatrixBufferPtr=Module["_getMatrixBufferPtr"]=wasmExports["j"])();var _getSRTPtr=Module["_getSRTPtr"]=()=>(_getSRTPtr=Module["_getSRTPtr"]=wasmExports["k"])();var _getInfoPtr=Module["_getInfoPtr"]=()=>(_getInfoPtr=Module["_getInfoPtr"]=wasmExports["l"])();var _getContinuedSRTPtr=Module["_getContinuedSRTPtr"]=()=>(_getContinuedSRTPtr=Module["_getContinuedSRTPtr"]=wasmExports["m"])();var _printMatrix=Module["_printMatrix"]=a0=>(_printMatrix=Module["_printMatrix"]=wasmExports["n"])(a0);var _updateAllMatrixContinueTransform=Module["_updateAllMatrixContinueTransform"]=(a0,a1,a2)=>(_updateAllMatrixContinueTransform=Module["_updateAllMatrixContinueTransform"]=wasmExports["o"])(a0,a1,a2);var _main=Module["_main"]=(a0,a1)=>(_main=Module["_main"]=wasmExports["p"])(a0,a1);var stackAlloc=a0=>(stackAlloc=wasmExports["r"])(a0);var ___cxa_is_pointer_type=a0=>(___cxa_is_pointer_type=wasmExports["s"])(a0);var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;args.forEach(arg=>{HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4});HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve(Module);if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();if(shouldRunNow)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}var shouldRunNow=true;if(Module["noInitialRun"])shouldRunNow=false;run(); + + + return moduleArg.ready } - \ No newline at end of file +); +})(); +export default Module; \ No newline at end of file diff --git a/packages/wasm-matrix/matrix.wasm b/packages/wasm-matrix/matrix.wasm deleted file mode 100644 index 10160fe8..00000000 Binary files a/packages/wasm-matrix/matrix.wasm and /dev/null differ diff --git a/packages/wasm-matrix/matrix.worker.js b/packages/wasm-matrix/matrix.worker.js deleted file mode 100644 index 4a7764a7..00000000 --- a/packages/wasm-matrix/matrix.worker.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",(data=>onmessage({data:data})));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:f=>(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f),postMessage:msg=>parentPort.postMessage(msg),performance:global.performance||{now:Date.now}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=(...args)=>{postMessage({cmd:"callHandler",handler:handler,args:args})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage; diff --git a/packages/wasm-matrix/package.json b/packages/wasm-matrix/package.json index 04d95be3..f24e4c35 100644 --- a/packages/wasm-matrix/package.json +++ b/packages/wasm-matrix/package.json @@ -1,13 +1,13 @@ { "name": "@orillusion/wasm-matrix", - "version": "0.7.0", + "version": "0.8.3", "author": "Orillusion", - "description": "Orillusion WebGPU Engine", + "description": "Update matrix by WASM", "main": "matrix.js", - "types": "_WasmMatrix4.d.ts", + "types": "matrix.d.ts", "files": [ "matrix.js", - "_WasmMatrix4.d.ts" + "matrix.d.ts" ], "license": "MIT", "repository": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4aa5caf5..58f27039 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,8 +2,8 @@ lockfileVersion: '6.0' devDependencies: '@webgpu/types': - specifier: ^0.1.40 - version: 0.1.40 + specifier: ^0.1.45 + version: 0.1.45 conventional-changelog-cli: specifier: ^2.2.2 version: 2.2.2 @@ -469,8 +469,8 @@ packages: dev: true optional: true - /@webgpu/types@0.1.40: - resolution: {integrity: sha512-/BBkHLS6/eQjyWhY2H7Dx5DHcVrS2ICj9owvSRdgtQT6KcafLZA86tPze0xAOsd4FbsYKCUBUQyNi87q7gV7kw==} + /@webgpu/types@0.1.45: + resolution: {integrity: sha512-0TBBF/mhakJoK0qUWCZugBnh23x+VwmYA5RLmtNQwvZt1pQ4P2fzIvQUiSe6jxzkBi4GF8R4BejJjro0ZSoSXQ==} dev: true /JSONStream@1.3.5: @@ -1130,7 +1130,7 @@ packages: source-map: 0.6.1 wordwrap: 1.0.0 optionalDependencies: - uglify-js: 3.17.4 + uglify-js: 3.19.3 dev: true /hard-rejection@2.1.0: @@ -2005,8 +2005,8 @@ packages: hasBin: true dev: true - /uglify-js@3.17.4: - resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + /uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} hasBin: true requiresBuild: true diff --git a/samples/base/Sample_MatrixAllocation.ts b/samples/base/Sample_MatrixAllocation.ts index ae9be4e2..9314ef0e 100644 --- a/samples/base/Sample_MatrixAllocation.ts +++ b/samples/base/Sample_MatrixAllocation.ts @@ -5,6 +5,8 @@ import { GUIUtil } from '@samples/utils/GUIUtil'; class Sample_MatrixAllocation { async run() { + Engine3D.setting.doublePrecision = true; + Matrix4.allocCount = 10; Matrix4.allocOnceCount = 5; diff --git a/samples/benchmark/Sample_drawCallInstance.ts b/samples/benchmark/Sample_InstancedrawCall.ts similarity index 88% rename from samples/benchmark/Sample_drawCallInstance.ts rename to samples/benchmark/Sample_InstancedrawCall.ts index e2ecb9c3..ca564a5b 100644 --- a/samples/benchmark/Sample_drawCallInstance.ts +++ b/samples/benchmark/Sample_InstancedrawCall.ts @@ -1,7 +1,6 @@ import { GUIHelp } from '@orillusion/debug/GUIHelp'; import { Stats } from '@orillusion/stats' import { Engine3D, Scene3D, AtmosphericComponent, CameraUtil, HoverCameraController, Object3D, MeshRenderer, BoxGeometry, LitMaterial, DirectLight, KelvinUtil, View3D, Vector3, Vector3Ex, UnLitMaterial, InstanceDrawComponent, LambertMaterial, Time, BoundingBox, Color, OcclusionSystem, PostProcessingComponent, GlobalFog, SphereGeometry } from '@orillusion/core'; -import { GUIUtil } from '@samples/utils/GUIUtil'; // simple base demo class Sample_drawCallInstance { @@ -54,17 +53,13 @@ class Sample_drawCallInstance { // start render Engine3D.startRenderView(view); GUIHelp.init(); - - GUIHelp.add(this, "anim").onChange = () => { - this.anim != this.anim; - }; - + GUIHelp.open(); + GUIHelp.add(this, "anim").onChange = () => this.anim != this.anim; this.initScene(); } private _list: Object3D[] = []; - private _rotList: number[] = []; initScene() { { this.lightObj3D = new Object3D(); @@ -79,8 +74,6 @@ class Sample_drawCallInstance { directLight.castShadow = true; directLight.intensity = 30; directLight.indirect = 1; - GUIHelp.init(); - GUIUtil.renderDirLight(directLight); this.scene.addChild(this.lightObj3D); } @@ -95,10 +88,7 @@ class Sample_drawCallInstance { let group = new Object3D(); let count = 10 * 10000; - // let count = 200; - GUIHelp.addFolder('info'); - GUIHelp.open(); GUIHelp.addLabel(`use instance draw box`); GUIHelp.addInfo(`count `, count); @@ -123,9 +113,8 @@ class Sample_drawCallInstance { obj.transform.rotationX = Math.random() * 360; obj.transform.rotationY = Math.random() * 360; obj.transform.rotationZ = Math.random() * 360; - - this._rotList.push((Math.random() * 1 - 1 * 0.5) * 2.0 * Math.random() * 100); - + + // use localDetailRot to update rotation by time obj.transform.localDetailRot = new Vector3( (Math.random() * 1 - 1 * 0.5) * 2.0 * Math.random() * 50 * 0.001, (Math.random() * 1 - 1 * 0.5) * 2.0 * Math.random() * 50 * 0.001, @@ -136,9 +125,8 @@ class Sample_drawCallInstance { } group.addComponent(InstanceDrawComponent); - group.transform.localDetailRot = new Vector3(0, 1.0 * 0.001, 0); - this._rotList.push(1.0); - + // use localDetailRot to update rotation by time + group.transform.localDetailRot = new Vector3(0, 0.01, 0); group.bound = new BoundingBox(Vector3.SAFE_MIN, Vector3.SAFE_MAX); this._list.push(group); this.scene.addChild(group); @@ -147,10 +135,8 @@ class Sample_drawCallInstance { renderLoop() { if (this.anim) { - let i = 0; - for (i = 0; i < this._list.length; i++) { + for (let i = 0; i < this._list.length; i++) { let element = this._list[i]; - // element.transform.rotationY += 1; element.transform.localChange = true; } } diff --git a/samples/benchmark/Sample_SphereDraw.ts b/samples/benchmark/Sample_InstancedrawCall2.ts similarity index 72% rename from samples/benchmark/Sample_SphereDraw.ts rename to samples/benchmark/Sample_InstancedrawCall2.ts index 6742c58a..1fe421ab 100644 --- a/samples/benchmark/Sample_SphereDraw.ts +++ b/samples/benchmark/Sample_InstancedrawCall2.ts @@ -1,7 +1,6 @@ import { GUIHelp } from '@orillusion/debug/GUIHelp'; import { Stats } from '@orillusion/stats' import { Engine3D, Scene3D, AtmosphericComponent, CameraUtil, HoverCameraController, Object3D, MeshRenderer, BoxGeometry, LitMaterial, DirectLight, KelvinUtil, View3D, Vector3, Vector3Ex, UnLitMaterial, InstanceDrawComponent, LambertMaterial, Time, BoundingBox, Color } from '@orillusion/core'; -import { GUIUtil } from '@samples/utils/GUIUtil'; // simple base demo class Sample_SphereDraw { @@ -28,7 +27,7 @@ class Sample_SphereDraw { // add a basic camera controller let hoverCameraController = mainCamera.object3D.addComponent(HoverCameraController); - hoverCameraController.setCamera(15, -15, 100); + hoverCameraController.setCamera(15, -15, 200); // add a basic direct light let lightObj = new Object3D(); @@ -51,11 +50,10 @@ class Sample_SphereDraw { Engine3D.startRenderView(view); GUIHelp.init(); - + GUIHelp.open(); GUIHelp.add(this, "anim").onChange = () => { this.anim != this.anim; }; - this.initScene(); } @@ -63,28 +61,27 @@ class Sample_SphereDraw { private _list: Object3D[] = []; initScene() { let shareGeometry = new BoxGeometry(); - let materials = [ - new LambertMaterial() - ]; - - for (let i = 0; i < materials.length; i++) { - const element = materials[i]; - element.baseColor = Color.random(); + let materials = []; + for (let i = 0; i < 1000; i++) { + let mat = new UnLitMaterial() + mat.baseColor = Color.random(); + materials.push(mat) } - // let material = new LitMaterial(); - let group = new Object3D(); this.scene.addChild(group); - // let count = 150000; - let count = 10000; + let count = 100000; + + GUIHelp.addLabel(`instance draw with multi materials`); + GUIHelp.addInfo(`object count `, count); + GUIHelp.addInfo(`material count`, materials.length) + for (let i = 0; i < count; i++) { let pos = Vector3Ex.sphere(100); - // let pos = Vector3Ex.getRandomXYZ(-2, 2); let obj = new Object3D(); let mr = obj.addComponent(MeshRenderer); mr.geometry = shareGeometry; - mr.material = materials[Math.floor(Math.random() * materials.length)]; + mr.material = materials[i % materials.length]; obj.localPosition = pos; group.addChild(obj); this._list.push(obj); @@ -98,20 +95,26 @@ class Sample_SphereDraw { obj.transform.scaleZ = Math.random() * 5 + 1; obj.transform.forward = d; - obj["rot"] = (Math.random() * 1 - 1 * 0.5) * 2.0 * Math.random() * 20; + + obj.transform.localDetailRot = new Vector3( + (Math.random() * 1 - 1 * 0.5) * 2.0 * Math.random() * 50 * 0.001, + (Math.random() * 1 - 1 * 0.5) * 2.0 * Math.random() * 50 * 0.001, + (Math.random() * 1 - 1 * 0.5) * 2.0 * Math.random() * 50 * 0.001); } group.addComponent(InstanceDrawComponent); - group["rot"] = 1.0; + // use localDetailRot to update rotation by time + group.transform.localDetailRot = new Vector3(0, 0.01, 0); group.bound = new BoundingBox(Vector3.SAFE_MIN, Vector3.SAFE_MAX); this._list.push(group); } renderLoop() { if (this.anim) { - this._list[this._list.length - 1].rotationY += Time.delta * 0.01; - this._list.forEach((v) => { - // v.transform.rotationY += Time.delta * 0.01 * v["rot"]; - }) + for (let i = 0; i < this._list.length; i++) { + let element = this._list[i]; + element.transform.localChange = true; + } + // this._list[this._list.length - 1].transform.rotationY += 0.1 } } } diff --git a/samples/benchmark/Sample_drawCallShareGeometry.ts b/samples/benchmark/Sample_drawCall.ts similarity index 81% rename from samples/benchmark/Sample_drawCallShareGeometry.ts rename to samples/benchmark/Sample_drawCall.ts index d3bc674b..618f3b7e 100644 --- a/samples/benchmark/Sample_drawCallShareGeometry.ts +++ b/samples/benchmark/Sample_drawCall.ts @@ -1,7 +1,6 @@ import { GUIHelp } from '@orillusion/debug/GUIHelp'; import { Stats } from '@orillusion/stats' import { Engine3D, Scene3D, AtmosphericComponent, CameraUtil, HoverCameraController, Object3D, MeshRenderer, BoxGeometry, LitMaterial, DirectLight, KelvinUtil, View3D, Vector3, Vector3Ex, UnLitMaterial, InstanceDrawComponent, LambertMaterial, Time, BoundingBox, Color, OcclusionSystem, PostProcessingComponent, GlobalFog, SphereGeometry, RendererMask, RenderLayer } from '@orillusion/core'; -import { GUIUtil } from '@samples/utils/GUIUtil'; // simple base demo export class Sample_drawCallShareGeometry { @@ -66,23 +65,14 @@ export class Sample_drawCallShareGeometry { private _rotList: number[] = []; initScene() { let shareGeometry = new BoxGeometry(); - - let mats = []; - for (let i = 0; i < 1; i++) { - const mat = new LambertMaterial(); - mat.baseColor = new Color( - Math.random() * 0.85, - Math.random() * 0.85, - Math.random() * 0.85, - ) - - // mat.baseColor = new Color().hexToRGB(0xcccccc) - mats.push(mat); - } - - + const mat = new LambertMaterial(); + mat.baseColor = new Color( + Math.random() * 0.85, + Math.random() * 0.85, + Math.random() * 0.85, + ) let group = new Object3D(); - let count = 5 * 10000; + let count = 10 * 10000; GUIHelp.addFolder('info'); GUIHelp.open(); @@ -90,15 +80,12 @@ export class Sample_drawCallShareGeometry { GUIHelp.addInfo(`count `, count); let ii = 0; - // let count = 70000; for (let i = 0; i < count; i++) { let pos = Vector3Ex.sphereXYZ(ii * 60 + 20, ii * 60 + 100, 100, i * 0.001 + 10, 100); - // let pos = Vector3Ex.getRandomXYZ(-2, 2); let obj = new Object3D(); let mr = obj.addComponent(MeshRenderer); - // mr.renderLayer = RenderLayer.DynamicBatch; mr.geometry = shareGeometry; - mr.material = mats[Math.floor(Math.random() * mats.length)]; + mr.material = mat; obj.localPosition = pos; group.addChild(obj); this._list.push(obj); @@ -122,8 +109,7 @@ export class Sample_drawCallShareGeometry { } } - // group.addComponent(InstanceDrawComponent); - group.transform.localDetailRot = new Vector3(0, 1.0 * 0.001 * 0.15, 0); + group.transform.localDetailRot = new Vector3(0, 0.01, 0); this._rotList.push(1.0 * 0.35); group.bound = new BoundingBox(Vector3.SAFE_MIN, Vector3.SAFE_MAX); @@ -136,8 +122,6 @@ export class Sample_drawCallShareGeometry { let i = 0; for (let i = 0; i < this._list.length; i++) { const element = this._list[i]; - // element.transform.rotationY += Time.delta * 0.01 * this._rotList[i]; - // element.transform._localRot.y += Time.delta * 0.01 * this._rotList[i]; element.transform.localChange = true; } } diff --git a/samples/compute/fluid/shader/addforce.wgsl.ts b/samples/compute/fluid/shader/addforce.wgsl.ts index 7a986391..82994e83 100644 --- a/samples/compute/fluid/shader/addforce.wgsl.ts +++ b/samples/compute/fluid/shader/addforce.wgsl.ts @@ -38,7 +38,7 @@ export class addforce { fn kernel (position: vec3, radius: f32, direction: vec3, origin: vec3) -> f32{ var distanceToMouseRay: f32 = length(cross(direction, position - origin)); var normalizedDistance = max(0.0, distanceToMouseRay / radius); - return smoothstep(1.0, 0.9, normalizedDistance); + return smoothstep(0.9, 1.0, normalizedDistance); } // fn _mod (x: f32, y: f32) -> f32{ diff --git a/samples/ext/_Sample_Boxes.ts b/samples/ext/_Sample_Boxes.ts deleted file mode 100644 index 2336ff6e..00000000 --- a/samples/ext/_Sample_Boxes.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { GUIHelp } from "@orillusion/debug/GUIHelp"; -import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, webGPUContext, HoverCameraController, Object3D, DirectLight, KelvinUtil, PlaneGeometry, VertexAttributeName, LitMaterial, MeshRenderer, Vector4, Vector3, Matrix3, PostProcessingComponent, TAAPost, BitmapTexture2D, GlobalFog, Color, BoxGeometry, UnLitMaterial, PointLight, GTAOPost, BloomPost } from "@orillusion/core"; -import { GUIUtil } from "@samples/utils/GUIUtil"; -import { GrassComponent, TerrainGeometry } from "@orillusion/effect"; - -// An sample of custom vertex attribute of geometry -class Sample_Boxes { - view: View3D; - post: PostProcessingComponent; - async run() { - Engine3D.setting.shadow.enable = true; - Engine3D.setting.shadow.updateFrameRate = 1; - Engine3D.setting.shadow.shadowBound = 500; - Engine3D.setting.shadow.autoUpdate = true; - Engine3D.setting.shadow.shadowSize = 1024; - // Engine3D.setting.render.zPrePass = true; - - GUIHelp.init(); - - await Engine3D.init(); - this.view = new View3D(); - this.view.scene = new Scene3D(); - this.view.scene.addComponent(AtmosphericComponent); - - this.view.camera = CameraUtil.createCamera3DObject(this.view.scene); - this.view.camera.enableCSM = true; - this.view.camera.perspective(60, webGPUContext.aspect, 1, 50000.0); - this.view.camera.object3D.z = -15; - this.view.camera.object3D.addComponent(HoverCameraController).setCamera(35, -20, 1000); - - Engine3D.startRenderView(this.view); - - this.post = this.view.scene.addComponent(PostProcessingComponent); - this.post.addPost(GTAOPost); - this.post.addPost(BloomPost); - let fog = this.post.addPost(GlobalFog); - fog.start = 91.0862; - fog.end = 487.5528; - fog.fogHeightScale = 0.0141; - fog.density = 0.2343; - fog.ins = 0.1041; - fog.skyFactor = 0.5316; - fog.overrideSkyFactor = 0.025; - - fog.fogColor = new Color(136 / 255, 215 / 255, 236 / 255, 1); - fog.fogHeightScale = 0.1; - fog.falloff = 2.5; - fog.scatteringExponent = 7.196; - fog.dirHeightLine = 6.5; - - GUIUtil.renderGlobalFog(fog); - - this.createScene(this.view.scene); - } - - private async createScene(scene: Scene3D) { - { - let sunObj = new Object3D(); - let sunLight = sunObj.addComponent(DirectLight); - sunLight.lightColor = KelvinUtil.color_temperature_to_rgb(6553); - sunLight.castShadow = true; - sunLight.intensity = 3; - sunObj.transform.rotationX = 50; - sunObj.transform.rotationY = 50; - GUIUtil.renderDirLight(sunLight); - scene.addChild(sunObj); - } - - { - let geometry = new BoxGeometry(5, 200, 5); - let litMaterial = new LitMaterial(); - // let litMaterial = new UnLitMaterial(); - let w = 30; - let h = 30; - for (let i = 0; i < w; i++) { - for (let j = 0; j < h; j++) { - let obj = new Object3D(); - let mr = obj.addComponent(MeshRenderer); - mr.material = litMaterial; - mr.geometry = geometry; - obj.x = i * 10 - w * 0.5 * 10; - obj.y = Math.random() * 100; - obj.z = j * 10 - h * 0.5 * 10; - scene.addChild(obj); - } - } - - let obj = new Object3D(); - let mr = obj.addComponent(MeshRenderer); - mr.material = litMaterial; - mr.geometry = geometry; - obj.localScale = new Vector3(1000, 1, 1000); - scene.addChild(obj); - } - - { - // for (let j = 0; j < 100; j++) { - // const lightObj = new Object3D();; - // let pointLight = lightObj.addComponent(PointLight); - // pointLight.castShadow = false; - // lightObj.transform.x = Math.random() * 100 * 10 - 100 * 10 * 0.5; - // lightObj.transform.y = 15; - // lightObj.transform.z = Math.random() * 100 * 10 - 100 * 10 * 0.5; - // pointLight.range = 100; - // pointLight.intensity = 60; - // scene.addChild(lightObj); - // } - } - - - // let globalFog = this.post.getPost(GlobalFog); - // GUIUtil.renderGlobalFog(globalFog); - } - -} - -new Sample_Boxes().run(); \ No newline at end of file diff --git a/samples/ext/Sample_Grass.ts b/samples/geometry/Sample_GrassGeometry.ts similarity index 98% rename from samples/ext/Sample_Grass.ts rename to samples/geometry/Sample_GrassGeometry.ts index 3e62d6b4..3d82a912 100644 --- a/samples/ext/Sample_Grass.ts +++ b/samples/geometry/Sample_GrassGeometry.ts @@ -1,7 +1,7 @@ import { GUIHelp } from "@orillusion/debug/GUIHelp"; import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, webGPUContext, HoverCameraController, Object3D, DirectLight, KelvinUtil, PlaneGeometry, VertexAttributeName, LitMaterial, MeshRenderer, Vector4, Vector3, Matrix3, PostProcessingComponent, TAAPost, BitmapTexture2D, GlobalFog, Color, FXAAPost } from "@orillusion/core"; import { GUIUtil } from "@samples/utils/GUIUtil"; -import { GrassComponent, TerrainGeometry } from "@orillusion/effect"; +import { GrassComponent, TerrainGeometry } from "@orillusion/geometry"; import { Stats } from "@orillusion/stats"; // An sample of custom vertex attribute of geometry diff --git a/samples/ext/Sample_Terrain.ts b/samples/geometry/Sample_TerrainGeometry.ts similarity index 98% rename from samples/ext/Sample_Terrain.ts rename to samples/geometry/Sample_TerrainGeometry.ts index 031266ed..9cc33726 100644 --- a/samples/ext/Sample_Terrain.ts +++ b/samples/geometry/Sample_TerrainGeometry.ts @@ -1,7 +1,7 @@ import { GUIHelp } from "@orillusion/debug/GUIHelp"; import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, webGPUContext, HoverCameraController, Object3D, DirectLight, KelvinUtil, PlaneGeometry, VertexAttributeName, LitMaterial, MeshRenderer, Vector4, Vector3, Matrix3, PostProcessingComponent, TAAPost, BitmapTexture2D, GlobalFog, Color, FXAAPost } from "@orillusion/core"; import { GUIUtil } from "@samples/utils/GUIUtil"; -import { GrassComponent, TerrainGeometry } from "@orillusion/effect"; +import { TerrainGeometry } from "@orillusion/geometry"; // An sample of custom vertex attribute of geometry class Sample_Terrain { diff --git a/samples/index.ts b/samples/index.ts index a66a72eb..a65bfb48 100644 --- a/samples/index.ts +++ b/samples/index.ts @@ -1,16 +1,3 @@ -import { Sample_AnimCurve } from "./animation/Sample_CurveAnimation"; -import { Sample_PointLight } from "./lights/Sample_PointLight"; -import { Sample_PointLightShadow } from "./lights/Sample_PointLightShadow"; -import { Sample_CarPaint } from "./material/Sample_CarPaint"; -import { Sample_SSGI } from "./post/Sample_SSGI"; - -// new Sample_CarPaint().run(); -// new Sample_SSGI().run(); - -// new Sample_AnimCurve().run(); - -// new Sample_PointLightShadow().run(); - /******** Load all samples in /src/sample/ ********/ { // find all demos in /sample diff --git a/samples/physics/Sample_Cloth.ts b/samples/physics/Sample_Cloth.ts new file mode 100644 index 00000000..067610f9 --- /dev/null +++ b/samples/physics/Sample_Cloth.ts @@ -0,0 +1,110 @@ +import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, webGPUContext, HoverCameraController, Object3D, DirectLight, LitMaterial, MeshRenderer, PlaneGeometry, Vector3, Object3DUtil } from "@orillusion/core"; +import { Graphic3D } from "@orillusion/graphic"; +import { Physics, Rigidbody, ClothSoftbody } from "@orillusion/physics"; +import dat from "dat.gui"; + +class Sample_Cloth { + async run() { + await Physics.init({ useSoftBody: true, useDrag: true }); + await Engine3D.init({ renderLoop: () => Physics.update() }); + let view = new View3D(); + view.scene = new Scene3D(); + let sky = view.scene.addComponent(AtmosphericComponent); + + view.camera = CameraUtil.createCamera3DObject(view.scene); + view.camera.perspective(60, webGPUContext.aspect, 1, 1000.0); + view.camera.object3D.addComponent(HoverCameraController).setCamera(0, -30, 20, new Vector3(0, 3, 0)); + + let lightObj3D = new Object3D(); + let sunLight = lightObj3D.addComponent(DirectLight); + sunLight.intensity = 2; + sunLight.castShadow = true; + lightObj3D.rotationX = 24; + lightObj3D.rotationY = -151; + view.scene.addChild(lightObj3D); + sky.relativeTransform = lightObj3D.transform; + + Engine3D.startRenderView(view); + + this.createScene(view.scene); + } + + createScene(scene: Scene3D) { + // create the ground and add a rigid body + let ground = Object3DUtil.GetSingleCube(30, 0, 30, 1, 1, 1); + scene.addChild(ground); + + let rigidbody = ground.addComponent(Rigidbody); + rigidbody.mass = 0; + rigidbody.shape = Rigidbody.collisionShape.createStaticPlaneShape(); + + // create shelves, cloth, and ball + this.createShelves(scene); + this.createCloth(scene); + const ballRb = this.createBall(scene); + + this.debug(scene, ballRb); + } + + + createShelves(scene: Scene3D) { + let shelf1 = Object3DUtil.GetSingleCube(0.5, 5, 0.5, 1, 1, 1); // left top + let shelf2 = shelf1.clone(); // right top + let shelf3 = shelf1.clone(); // left bottom + let shelf4 = shelf1.clone(); // right bottom + shelf1.localPosition = new Vector3(-4, 2.5, -4); + shelf2.localPosition = new Vector3(4, 2.5, -4); + shelf3.localPosition = new Vector3(-4, 2.5, 4); + shelf4.localPosition = new Vector3(4, 2.5, 4); + scene.addChild(shelf1); + scene.addChild(shelf2); + scene.addChild(shelf3); + scene.addChild(shelf4); + } + + createCloth(scene: Scene3D) { + const cloth = new Object3D(); + let meshRenderer = cloth.addComponent(MeshRenderer); + meshRenderer.geometry = new PlaneGeometry(8, 8, 20, 20, Vector3.UP); + let material = new LitMaterial(); + material.baseMap = Engine3D.res.redTexture; + material.cullMode = 'none'; + meshRenderer.material = material; + + cloth.y = 5; + scene.addChild(cloth); + + // add cloth softbody component + let softBody = cloth.addComponent(ClothSoftbody); + softBody.mass = 1; + softBody.margin = 0.2; + softBody.fixNodeIndices = ['leftTop', 'rightTop', 'leftBottom', 'rightBottom']; + } + + createBall(scene: Scene3D) { + const ball = Object3DUtil.GetSingleSphere(1, 0.5, 0.2, 0.8); + ball.y = 10; + scene.addChild(ball); + + let rigidbody = ball.addComponent(Rigidbody); + rigidbody.mass = 1.6; + rigidbody.shape = Rigidbody.collisionShape.createShapeFromObject(ball); + + return rigidbody; + } + + debug(scene: Scene3D, ballRb: Rigidbody) { + const graphic3D = new Graphic3D(); + scene.addChild(graphic3D); + Physics.initDebugDrawer(graphic3D); + + let gui = new dat.GUI(); + let f = gui.addFolder('PhysicsDebug'); + f.add(Physics.debugDrawer, 'enable'); + f.add(Physics.debugDrawer, 'debugMode', Physics.debugDrawer.debugModeList); + gui.add({ ResetBall: () => ballRb.updateTransform(new Vector3(0, 10, 0), null, true) }, 'ResetBall'); + } + +} + +new Sample_Cloth().run(); diff --git a/samples/physics/Sample_Dominoes.ts b/samples/physics/Sample_Dominoes.ts new file mode 100644 index 00000000..bae5c0a3 --- /dev/null +++ b/samples/physics/Sample_Dominoes.ts @@ -0,0 +1,195 @@ +import { Engine3D, LitMaterial, MeshRenderer, Object3D, Scene3D, View3D, Object3DUtil, Vector3, AtmosphericComponent, DirectLight, CameraUtil, HoverCameraController, Color, Quaternion, ExtrudeGeometry, BlendMode, BitmapTexture2D } from "@orillusion/core"; +import { CollisionShapeUtil, Physics, Rigidbody } from "@orillusion/physics"; +import { Stats } from "@orillusion/stats"; +import dat from "dat.gui"; +import { Graphic3D } from '@orillusion/graphic' + +/** + * Sample class demonstrating the creation of a domino effect with physics interactions. + */ +class Sample_Dominoes { + async run() { + // init physics and engine + await Physics.init({ useDrag: true }); + await Engine3D.init({ renderLoop: () => Physics.update() }); + + let scene = new Scene3D(); + scene.addComponent(Stats); + + // 启用物理调试功能时,需要为绘制器传入graphic3D对象 + const graphic3D = new Graphic3D(); + scene.addChild(graphic3D); + Physics.initDebugDrawer(graphic3D, { enable: false }); + + let camera = CameraUtil.createCamera3DObject(scene); + camera.perspective(60, Engine3D.aspect, 0.1, 800.0); + camera.object3D.addComponent(HoverCameraController).setCamera(0, -32, 80); + + // Create directional light + let lightObj3D = new Object3D(); + lightObj3D.localPosition = new Vector3(0, 30, -40); + lightObj3D.localRotation = new Vector3(20, 160, 0); + let directLight = lightObj3D.addComponent(DirectLight); + directLight.castShadow = true; + directLight.intensity = 2; + scene.addChild(lightObj3D); + + // init sky + scene.addComponent(AtmosphericComponent).sunY = 0.8; + + let view = new View3D(); + view.camera = camera; + view.scene = scene; + + Engine3D.startRenderView(view); + + await this.initScene(scene); + + this.debug(scene) + } + + // init the scene with ground, Pipe, ball, and dominoes. + private async initScene(scene: Scene3D) { + // Create ground and add rigidbody + let ground = Object3DUtil.GetSingleCube(100, 0.1, 100, 1, 1, 1); + scene.addChild(ground); + + let rigidbody = ground.addComponent(Rigidbody); + rigidbody.shape = CollisionShapeUtil.createBoxShape(ground); + rigidbody.mass = 0; + rigidbody.friction = 100; // Set high friction for the ground + rigidbody.isSilent = true; // Disable collision events + + // Create dominoes + this.createDominoes(scene); + + // Create Pipe + this.createPipe(scene); + + // Create ball + this.createBall(scene); + } + + private async createPipe(scene: Scene3D) { + // create a object + const obj: Object3D = new Object3D(); + // add MeshRenderer to the object + let mr: MeshRenderer = obj.addComponent(MeshRenderer); + + // build shape + let shape: Vector3[] = [], + vertexCount = 8, + shapeRadius = 1; + for (let i = 0; i < vertexCount; i++) { + let angle = (Math.PI * 2 * i) / vertexCount; + let point = new Vector3(Math.sin(angle), 0, Math.cos(angle)).multiplyScalar(shapeRadius); + shape.push(point); + } + // build curve path + let curve: Vector3[] = [], + sectionCount = 44, + modelRadius = 4; + for (let i = 0; i < sectionCount; i++) { + let angle = (Math.PI * 2 * i) / 22; + modelRadius += (0.1 * i) / sectionCount; + let offsetY = 0.6 - Math.sqrt(i / sectionCount); + let point = new Vector3(Math.sin(angle), offsetY * 6, Math.cos(angle)).multiplyScalar(modelRadius); + curve.push(point); + } + + // build ExtrudeGeometry from shape & curve + mr.geometry = new ExtrudeGeometry().build(shape, true, curve, 0.2); + // set a pbr lit material + let material = new LitMaterial(); + material.cullMode = 'none'; + material.depthCompare = 'always'; + material.blendMode = BlendMode.ADD; + material.baseColor = new Color(0, 1, 0.5, 1.0); + material.transparent = true; + + let texture = new BitmapTexture2D(); + texture.addressModeU = 'repeat'; + texture.addressModeV = 'repeat'; + await texture.load('https://cdn.orillusion.com/textures/grid.webp'); + + material.baseMap = texture; + mr.material = material; + + obj.localPosition = new Vector3(-30, 20, -3); + scene.addChild(obj); + + let rigidbody = obj.addComponent(Rigidbody); + rigidbody.shape = CollisionShapeUtil.createBvhTriangleMeshShape(obj); + rigidbody.mass = 0; + } + + // Create a series of dominoes with rigid bodies and arrange them in an S-shaped curve. + private createDominoes(scene: Scene3D) { + const width = 0.5; + const height = 5; + const depth = 2; + + const originX = -30; + const originZ = 4.7; + + const totalDominoes = 40; + const segmentLength = 2; // Distance between dominoes + + let previousX = originX; + let previousZ = originZ; + + for (let i = 0; i < totalDominoes; i++) { + let box = Object3DUtil.GetSingleCube(width, height, depth, Math.random(), Math.random(), Math.random()); + + let angle = (Math.PI / (totalDominoes / 2)) * i; + let x = originX + segmentLength * i; + let z = originZ + Math.sin(angle) * 15; // Adjust sine curve amplitude for S-shape + + box.localPosition = new Vector3(x, height / 2, z); + + // Adjust each domino's rotation to align with the curve + let deltaX = x - previousX; + let deltaZ = z - previousZ; + box.rotationY = i === 0 ? -48 : -Math.atan2(deltaZ, deltaX) * (180 / Math.PI); + scene.addChild(box); + previousX = x; + previousZ = z; + + let rigidbody = box.addComponent(Rigidbody); + rigidbody.shape = Rigidbody.collisionShape.createBoxShape(box); + rigidbody.mass = 30; + rigidbody.friction = 0.1; + rigidbody.collisionEvent = (contactPoint, selfBody, otherBody) => { + rigidbody.enableCollisionEvent = false; // Handle collision only once + (box.getComponent(MeshRenderer).material as LitMaterial).baseColor = Color.random(); + }; + } + } + + // Create a ball with a rigid body. + private createBall(scene: Scene3D) { + let ball = Object3DUtil.GetSingleSphere(0.8, 1, 0, 0); + ball.name = 'ball'; + ball.localPosition = new Vector3(-30, 40, 1); + scene.addChild(ball); + + let rigidbody = ball.addComponent(Rigidbody); + rigidbody.shape = Rigidbody.collisionShape.createSphereShape(ball); + rigidbody.mass = 50; + } + + private debug(scene: Scene3D) { + let gui = new dat.GUI(); + let f = gui.addFolder('PhysicsDebug'); + f.add(Physics.debugDrawer, 'enable'); + f.add(Physics.debugDrawer, 'debugMode', Physics.debugDrawer.debugModeList); + gui.add({ + ResetBall: () => { + const ballObj = scene.getChildByName('ball') as Object3D; + ballObj?.getComponent(Rigidbody).updateTransform(new Vector3(-30, 40, 1), Quaternion._zero, true); + } + }, 'ResetBall'); + } +} + +new Sample_Dominoes().run(); diff --git a/samples/physics/Sample_EatTheBox.ts b/samples/physics/Sample_EatTheBox.ts index 5d7fae43..f60bb957 100644 --- a/samples/physics/Sample_EatTheBox.ts +++ b/samples/physics/Sample_EatTheBox.ts @@ -116,21 +116,12 @@ class Sample_EatTheBox { rigidbody.rollingFriction = 10; rigidbody1.restitution = rigidbody2.restitution = rigidbody3.restitution = rigidbody4.restitution = 0.3; //set their index to -1 - rigidbody.addInitedFunction(() => { - rigidbody.btRigidbody.setUserIndex(-1); - }, this); - rigidbody1.addInitedFunction(() => { - rigidbody1.btRigidbody.setUserIndex(-1); - }, this); - rigidbody2.addInitedFunction(() => { - rigidbody2.btRigidbody.setUserIndex(-1); - }, this); - rigidbody3.addInitedFunction(() => { - rigidbody3.btRigidbody.setUserIndex(-1); - }, this); - rigidbody4.addInitedFunction(() => { - rigidbody4.btRigidbody.setUserIndex(-1); - }, this); + rigidbody.wait().then(btRigidbody => btRigidbody.setUserIndex(-1)); + rigidbody1.wait().then(btRigidbody => btRigidbody.setUserIndex(-1)); + rigidbody2.wait().then(btRigidbody => btRigidbody.setUserIndex(-1)); + rigidbody3.wait().then(btRigidbody => btRigidbody.setUserIndex(-1)); + rigidbody4.wait().then(btRigidbody => btRigidbody.setUserIndex(-1)); + this.view.scene.addChild(floor); this.view.scene.addChild(border1); this.view.scene.addChild(border2); @@ -156,15 +147,13 @@ class Sample_EatTheBox { rig.mass = 0; let col = boxObj.addComponent(ColliderComponent); col.shape = boxColliderShape; - rig.addInitedFunction(() => { - //get original rigidbody to get/set more property - let btrig = rig.btRigidbody; + rig.wait().then(btRigidbody => { //set this colider as trigger,trigger will not respond to collision - btrig.setCollisionFlags(4); + btRigidbody.setCollisionFlags(4); //set index to 0~9 - btrig.setUserIndex(index); + btRigidbody.setUserIndex(index); this.foods[index] = boxObj; - }, this); + }); boxObj.addComponent(RotateScript); this.view.scene.addChild(boxObj); } @@ -181,9 +170,7 @@ class Sample_EatTheBox { //add movescript this.moveScript = sphereObj.addComponent(MoveScript); this.moveScript.rigidbody = sphereObj.addComponent(Rigidbody); - this.moveScript.rigidbody.addInitedFunction(() => { - this.moveScript.rigidbody.btRigidbody.setUserIndex(-1); - }, this); + this.moveScript.rigidbody.wait().then(btRigidbody => btRigidbody.setUserIndex(-1)); this.moveScript.rigidbody.mass = 10; let collider = sphereObj.addComponent(ColliderComponent); collider.shape = new SphereColliderShape(1); diff --git a/samples/physics/Sample_MultipleConstraints.ts b/samples/physics/Sample_MultipleConstraints.ts new file mode 100644 index 00000000..db0aefa5 --- /dev/null +++ b/samples/physics/Sample_MultipleConstraints.ts @@ -0,0 +1,355 @@ +import { Engine3D, LitMaterial, MeshRenderer, Object3D, Scene3D, View3D, Object3DUtil, Vector3, AtmosphericComponent, DirectLight, CameraUtil, HoverCameraController, PlaneGeometry, GPUCullMode, Color } from "@orillusion/core"; +import { Stats } from "@orillusion/stats"; +import { ActivationState, CollisionShapeUtil, DebugDrawMode, FixedConstraint, HingeConstraint, Physics, PointToPointConstraint, Rigidbody, SliderConstraint, ClothSoftbody, RopeSoftbody } from "@orillusion/physics"; +import dat from "dat.gui"; +import { Graphic3D } from "@orillusion/graphic"; + +/** + * Sample class demonstrating the use of multiple constraints in a physics simulation. + */ +class Sample_MultipleConstraints { + scene: Scene3D; + gui: dat.GUI; + + async run() { + // init physics and engine + await Physics.init({ useSoftBody: true, useDrag: true }); + await Engine3D.init({ renderLoop: () => Physics.update() }); + + this.gui = new dat.GUI(); + + this.scene = new Scene3D(); + this.scene.addComponent(Stats); + + // 在引擎启动后初始化物理调试功能,需要为调试器传入 graphic3D 对象 + const graphic3D = new Graphic3D(); + this.scene.addChild(graphic3D); + Physics.initDebugDrawer(graphic3D, { + enable: false, + debugDrawMode: DebugDrawMode.DrawConstraintLimits + }) + + let camera = CameraUtil.createCamera3DObject(this.scene); + camera.perspective(60, Engine3D.aspect, 0.1, 800.0); + camera.object3D.addComponent(HoverCameraController).setCamera(60, -25, 50); + + // create directional light + let light = new Object3D(); + light.localRotation = new Vector3(36, -130, 60); + let dl = light.addComponent(DirectLight); + dl.castShadow = true; + dl.intensity = 3; + this.scene.addChild(light); + + // init sky + this.scene.addComponent(AtmosphericComponent).sunY = 0.6; + + let view = new View3D(); + view.camera = camera; + view.scene = this.scene; + + this.physicsDebug(); + + Engine3D.startRenderView(view); + + // Create ground, turntable, and chains + this.createGround(); + this.createTurntable(); + this.createChains(); + + // Create impactor and softBody + let impactorRb = this.createImpactor(); + this.createClothSoftbody(impactorRb); + this.createRopeSoftbody(impactorRb); + } + + private physicsDebug() { + let physicsFolder = this.gui.addFolder('PhysicsDebug'); + physicsFolder.add(Physics.debugDrawer, 'enable'); + physicsFolder.add(Physics.debugDrawer, 'debugMode', Physics.debugDrawer.debugModeList); + physicsFolder.add(Physics, 'isStop'); + physicsFolder.add({ hint: "Drag dynamic rigid bodies with the mouse." }, "hint"); + physicsFolder.open(); + } + + private async createGround() { + // Create ground + let ground = Object3DUtil.GetSingleCube(80, 2, 20, 1, 1, 1); + ground.y = -1; // Set ground half-height + this.scene.addChild(ground); + + // Add rigidbody to ground + let groundRb = ground.addComponent(Rigidbody); + groundRb.shape = CollisionShapeUtil.createBoxShape(ground); + groundRb.mass = 0; + } + + private createImpactor(): Rigidbody { + // Create shelves + const shelfSize = 0.5; + const shelfHeight = 5; + + let shelfLeft = Object3DUtil.GetCube(); + shelfLeft.localScale = new Vector3(shelfSize, shelfHeight, shelfSize); + shelfLeft.localPosition = new Vector3(-30, shelfHeight / 2, 0); + + let shelfRight = shelfLeft.clone(); + shelfRight.localPosition = new Vector3(30, shelfHeight / 2, 0); + + let shelfTop = Object3DUtil.GetCube(); + shelfTop.localScale = new Vector3(60 - shelfSize, shelfSize, shelfSize); + shelfTop.localPosition = new Vector3(0, shelfHeight - shelfSize / 2, 0); + + // Add rigidbodies to shelves + let shelfRightRb = this.addBoxShapeRigidBody(shelfRight, 0); + let shelfLeftRb = this.addBoxShapeRigidBody(shelfLeft, 0); + this.addBoxShapeRigidBody(shelfTop, 0); + + this.scene.addChild(shelfLeft); + this.scene.addChild(shelfRight); + this.scene.addChild(shelfTop); + + // Create slider + let slider = Object3DUtil.GetSingleCube(4, 1, 1, Math.random(), Math.random(), Math.random()); + this.scene.addChild(slider); + + // Add rigidbody to slider + let sliderRb = this.addBoxShapeRigidBody(slider, 500, true, [0.2, 0]); + + // Create Impactor + let impactor = Object3DUtil.GetCube(); + impactor.localScale = new Vector3(1, 1, 5); + impactor.localPosition = new Vector3(0, shelfHeight - shelfSize / 2, 3); + this.scene.addChild(impactor); + + let impactorRb = this.addBoxShapeRigidBody(impactor, 200, true); + + // Create fixed constraint to attach slider to impactor + let fixedConstraint = slider.addComponent(FixedConstraint); + fixedConstraint.targetRigidbody = impactorRb; + fixedConstraint.pivotTarget = new Vector3(0, 0, -3); + + // Create slider constraint + let sliderConstraint = shelfTop.addComponent(SliderConstraint); + sliderConstraint.targetRigidbody = sliderRb; + sliderConstraint.lowerLinLimit = -30; + sliderConstraint.upperLinLimit = 30; + sliderConstraint.lowerAngLimit = 0; + sliderConstraint.upperAngLimit = 0; + sliderConstraint.poweredLinMotor = true; + sliderConstraint.maxLinMotorForce = 1; + sliderConstraint.targetLinMotorVelocity = 20; + + // Setup slider motor event controller + this.sliderMotorEventController(shelfLeftRb, shelfRightRb, sliderConstraint); + + return impactorRb; + } + + private sliderMotorEventController(leftRb: Rigidbody, rightRb: Rigidbody, slider: SliderConstraint) { + // Control slider movement based on collision events + const timer = { pauseDuration: 1000 }; + + leftRb.collisionEvent = () => { + rightRb.enableCollisionEvent = true; + leftRb.enableCollisionEvent = false; + setTimeout(() => { + slider.targetLinMotorVelocity = Math.abs(slider.targetLinMotorVelocity); + setTimeout(() => leftRb.enableCollisionEvent = true, 1000); + }, timer.pauseDuration); + }; + + rightRb.collisionEvent = () => { + rightRb.enableCollisionEvent = false; + leftRb.enableCollisionEvent = true; + setTimeout(() => { + slider.targetLinMotorVelocity = -Math.abs(slider.targetLinMotorVelocity); + setTimeout(() => rightRb.enableCollisionEvent = true, 1000); + }, timer.pauseDuration); + }; + + // GUI controls for slider motor + let folder = this.gui.addFolder('Slider Motor Controller'); + folder.open(); + folder.add(slider, 'poweredLinMotor'); + folder.add(slider, 'maxLinMotorForce', 0, 30, 1); + folder.add({ velocity: slider.targetLinMotorVelocity }, 'velocity', 0, 30, 1).onChange(v => { + slider.targetLinMotorVelocity = slider.targetLinMotorVelocity > 0 ? v : -v; + }); + folder.add(timer, 'pauseDuration', 0, 3000, 1000); + } + + private createTurntable() { + // Create turntable components + const columnWidth = 0.5; + const columnHeight = 4.75 - columnWidth / 2; + const columnDepth = 0.5; + + let column = Object3DUtil.GetCube(); + column.localScale = new Vector3(columnWidth, columnHeight, columnDepth); + column.localPosition = new Vector3(0, columnHeight / 2, 8); + this.scene.addChild(column); + this.addBoxShapeRigidBody(column, 0); // Add rigidbodies to turntable components + + + // Create arm compound shape + let armParent = new Object3D(); + armParent.localPosition = new Vector3(0, columnHeight + columnWidth / 2, 8); + + let armChild1 = Object3DUtil.GetCube(); + armChild1.rotationY = 45; + armChild1.localScale = new Vector3(10, 0.5, 0.5); + + let armChild2 = armChild1.clone(); + armChild2.rotationY = 135; + + armParent.addChild(armChild1); + armParent.addChild(armChild2); + this.scene.addChild(armParent); + + let armRigidbody = armParent.addComponent(Rigidbody); + armRigidbody.shape = CollisionShapeUtil.createCompoundShapeFromObject(armParent); + armRigidbody.mass = 500; + armRigidbody.activationState = ActivationState.DISABLE_DEACTIVATION; + + // Create hinge constraint to attach arm1 to column + let hinge = column.addComponent(HingeConstraint); + hinge.targetRigidbody = armRigidbody; + hinge.pivotSelf.set(0, columnHeight / 2 + columnWidth / 2, 0); + hinge.enableAngularMotor(true, 5, 50); + } + + private createChains() { + const chainHeight = 1; + + let chainLink = Object3DUtil.GetCube(); + chainLink.localScale = new Vector3(0.25, chainHeight, 0.25); + chainLink.localPosition = new Vector3(5, 16, 5); + this.scene.addChild(chainLink); + + // Add static rigidbody to the first chain link + let chainRb = this.addBoxShapeRigidBody(chainLink, 0); + let prevRb = chainRb; + + // Create chain links and add point-to-point constraints + for (let i = 0; i < 10; i++) { + let link = chainLink.clone(); + link.y -= (i + 1) * chainHeight; + this.scene.addChild(link); + + let linkRb = this.addBoxShapeRigidBody(link, 1, false, [0.3, 0.3]); + linkRb.isSilent = true; // Disable collision events + + let p2p = link.addComponent(PointToPointConstraint); + p2p.targetRigidbody = prevRb; + p2p.pivotTarget.y = -chainHeight / 2; + p2p.pivotSelf.y = chainHeight / 2; + + prevRb = linkRb; + } + + // Create a sphere and add point-to-point constraint to the last chain link + const sphereRadius = 0.8; + let sphere = Object3DUtil.GetSingleSphere(sphereRadius, 1, 1, 1); + let sphereMaterial = (sphere.getComponent(MeshRenderer).material as LitMaterial); + + sphere.localPosition = new Vector3(5, 4.5, 5); + this.scene.addChild(sphere); + + let sphereRb = sphere.addComponent(Rigidbody); + sphereRb.shape = CollisionShapeUtil.createSphereShape(sphere); + sphereRb.mass = 2; + sphereRb.damping = [0.3, 0.3]; + sphereRb.enablePhysicsTransformSync = true; + + // Sphere collision event to change color + let timer: number | null = null; + sphereRb.collisionEvent = () => { + if (timer !== null) clearTimeout(timer); + else sphereMaterial.baseColor = new Color(Color.SALMON); + + timer = setTimeout(() => { + sphereMaterial.baseColor = Color.COLOR_WHITE; + timer = null; + }, 1000); + }; + + let p2p = sphere.addComponent(PointToPointConstraint); + p2p.disableCollisionsBetweenLinkedBodies = true; + p2p.targetRigidbody = prevRb; + p2p.pivotTarget.y = -chainHeight / 2; + p2p.pivotSelf.y = sphereRadius; + } + + private createClothSoftbody(anchorRb: Rigidbody) { + const cloth = new Object3D(); + let meshRenderer = cloth.addComponent(MeshRenderer); + meshRenderer.geometry = new PlaneGeometry(3, 3, 10, 10, Vector3.X_AXIS); // Set the plane direction to determine the four corners + let material = new LitMaterial(); + material.baseMap = Engine3D.res.redTexture; + material.cullMode = GPUCullMode.none; + meshRenderer.material = material; + this.scene.addChild(cloth); + + // Add cloth softbody component + let softBody = cloth.addComponent(ClothSoftbody); + softBody.mass = 5; + softBody.margin = 0.1; + softBody.anchorRigidbody = anchorRb; // Anchor rigidbody + softBody.anchorIndices = ['leftTop', 'top', 'rightTop']; // Anchor points + softBody.influence = 1; // Attachment influence + softBody.disableCollision = false; // Enable collision with rigidbody + softBody.anchorPosition = new Vector3(0, -2.1, 0); // Relative position to anchor + + softBody.wait().then(btSoftbody => { + // native softbody API + let sbConfig = btSoftbody.get_m_cfg(); // configure softbody parameters + sbConfig.set_kDF(0.2); + sbConfig.set_kDP(0.01); + sbConfig.set_kLF(0.02); + sbConfig.set_kDG(0.001); + }); + + } + + private createRopeSoftbody(headRb: Rigidbody) { + + const box = Object3DUtil.GetSingleCube(1, 1, 1, 1, 1, 1); + box.localPosition = new Vector3(0, 10, 0); + this.scene.addChild(box); + let tailRb = this.addBoxShapeRigidBody(box, 1, true, [0.2, 0.2]); + + const rope = new Object3D(); + let mr = rope.addComponent(MeshRenderer); + let startPos = new Vector3(0, 4.75, 3); + let endPos = new Vector3(0, 10, 0); + mr.geometry = RopeSoftbody.buildRopeGeometry(10, startPos, endPos); + + mr.material = new LitMaterial(); + mr.material.topology = 'line-list'; + this.scene.addChild(rope); + + // Add rope softbody component + let softBody = rope.addComponent(RopeSoftbody); + softBody.mass = 1; + softBody.elasticity = 0.1; + softBody.anchorRigidbodyHead = headRb; + softBody.anchorOffsetHead = new Vector3(0, -0.5, 2.1); + softBody.anchorRigidbodyTail = tailRb; + softBody.anchorOffsetTail = new Vector3(0, 0.5, 0); + + } + + private addBoxShapeRigidBody(obj: Object3D, mass: number, disableHibernation?: boolean, damping?: [number, number]) { + let rigidbody = obj.addComponent(Rigidbody); + rigidbody.shape = CollisionShapeUtil.createBoxShape(obj); + rigidbody.mass = mass; + + if (disableHibernation) rigidbody.activationState = ActivationState.DISABLE_DEACTIVATION; + if (damping) rigidbody.damping = damping; + + return rigidbody; + } +} + +new Sample_MultipleConstraints().run(); diff --git a/samples/physics/Sample_MultipleShapes.ts b/samples/physics/Sample_MultipleShapes.ts new file mode 100644 index 00000000..edd5a851 --- /dev/null +++ b/samples/physics/Sample_MultipleShapes.ts @@ -0,0 +1,308 @@ +import { Engine3D, LitMaterial, MeshRenderer, BoxGeometry, Object3D, Scene3D, View3D, Object3DUtil, Vector3, AtmosphericComponent, DirectLight, SphereGeometry, CameraUtil, HoverCameraController, BitmapTexture2D, VertexAttributeName, Color, CylinderGeometry, TorusGeometry, ComponentBase } from "@orillusion/core"; +import { TerrainGeometry } from "@orillusion/geometry"; +import { Graphic3D } from "@orillusion/graphic"; +import { Ammo, CollisionShapeUtil, Physics, Rigidbody } from "@orillusion/physics"; +import { Stats } from "@orillusion/stats"; +import dat from "dat.gui"; + +class Sample_MultipleShapes { + scene: Scene3D; + terrain: Object3D; + gui: dat.GUI; + + async run() { + // init physics and engine + await Physics.init(); + await Engine3D.init({ + renderLoop: () => Physics.update() + }); + + this.gui = new dat.GUI(); + + // shadow settings + Engine3D.setting.shadow.shadowBias = 0.01; + Engine3D.setting.shadow.shadowSize = 1024 * 4; + Engine3D.setting.shadow.csmMargin = 0.1; + Engine3D.setting.shadow.csmScatteringExp = 0.8; + Engine3D.setting.shadow.csmAreaScale = 0.1; + Engine3D.setting.shadow.updateFrameRate = 1; + + this.scene = new Scene3D(); + this.scene.addComponent(Stats); + + // 在引擎启动后初始化物理调试功能,需要为绘制器传入 graphic3D 对象 + const graphic3D = new Graphic3D(); + this.scene.addChild(graphic3D); + Physics.initDebugDrawer(graphic3D, { + enable: false, + }) + + // Setup camera + let camera = CameraUtil.createCamera3DObject(this.scene); + camera.perspective(60, Engine3D.aspect, 0.1, 800.0); + camera.enableCSM = true; + + let hoverCtrl = camera.object3D.addComponent(HoverCameraController); + hoverCtrl.setCamera(0, -25, 100); + hoverCtrl.dragSmooth = 4; + + // Create directional light + let lightObj3D = new Object3D(); + lightObj3D.localRotation = new Vector3(-35, -143, 92); + + let light = lightObj3D.addComponent(DirectLight); + light.lightColor = Color.COLOR_WHITE; + light.castShadow = true; + light.intensity = 2.2; + this.scene.addChild(light.object3D); + + // init sky + let atmosphericSky = this.scene.addComponent(AtmosphericComponent); + atmosphericSky.sunY = 0.6; + + // Setup view + let view = new View3D(); + view.camera = camera; + view.scene = this.scene; + + Engine3D.startRenderView(view); + + this.setupPhysicsGUI(); + + // init terrain and create static planes + await this.initTerrain(); + this.createStaticPlanes(); + + this.scene.addComponent(BoxGenerator); + } + + async initTerrain() { + // Load textures + let bitmapTexture = await Engine3D.res.loadTexture('terrain/test01/bitmap.png'); + let heightTexture = await Engine3D.res.loadTexture('terrain/test01/height.png'); + + const width = 100; + const height = 100; + const terrainMaxHeight = 60; + const segment = 60 + + // Create terrain geometry + let terrainGeometry = new TerrainGeometry(width, height, segment, segment); + terrainGeometry.setHeight(heightTexture as BitmapTexture2D, terrainMaxHeight); + + let terrain = new Object3D(); + let mr = terrain.addComponent(MeshRenderer); + mr.geometry = terrainGeometry; + + let mat = new LitMaterial(); + mat.baseMap = bitmapTexture; + mat.metallic = 0; + mat.roughness = 1.3; + mr.material = mat; + + this.terrain = terrain; + this.scene.addChild(terrain); + + // Add rigidbody to terrain + let terrainRb = terrain.addComponent(Rigidbody); + terrainRb.shape = Rigidbody.collisionShape.createHeightfieldTerrainShape(terrain); + terrainRb.mass = 0; // Static rigidbody + terrainRb.margin = 0.05; + terrainRb.isDisableDebugVisible = true; + terrainRb.friction = 1; + + this.gui.__folders['PhysicsDebugDrawer'].add(terrainRb, 'isDisableDebugVisible').name('disableTerrain').listen(); + this.setupTerrainGUI(width, height, terrainMaxHeight); + } + + // Create static planes for boundaries + createStaticPlanes() { + // Create bottom static plane + let staticFloorBottom = Object3DUtil.GetPlane(Engine3D.res.whiteTexture); + staticFloorBottom.y = -500; + staticFloorBottom.transform.enable = false; + this.scene.addChild(staticFloorBottom); + + let bottomRb = staticFloorBottom.addComponent(Rigidbody); + bottomRb.shape = CollisionShapeUtil.createStaticPlaneShape(); + bottomRb.mass = 0; + + // Create top static plane + let staticFloorTop = Object3DUtil.GetPlane(Engine3D.res.whiteTexture); + staticFloorTop.y = 100; + staticFloorTop.transform.enable = false; + this.scene.addChild(staticFloorTop); + + let topRb = staticFloorTop.addComponent(Rigidbody); + topRb.shape = CollisionShapeUtil.createStaticPlaneShape(Vector3.DOWN); + topRb.mass = 0; + } + + setupPhysicsGUI() { + // Physics debug drawer settings + let debugDrawer = Physics.debugDrawer; + let f = this.gui.addFolder("PhysicsDebugDrawer"); + f.add(debugDrawer, 'enable').listen(); + f.add(debugDrawer, 'debugMode', debugDrawer.debugModeList); + f.add(debugDrawer, 'updateFreq', 1, 360, 1); + f.add(debugDrawer, 'maxLineCount', 100, 33000, 100); + f.open(); + + // General physics settings + let p = this.gui.addFolder("Physics"); + p.add(Physics, 'isStop'); + p.add(Physics.gravity, 'y', -20, 20, 0.1).name('gravity').onChange(() => { + Physics.gravity = Physics.gravity; + Physics.rigidBodyUtil.activateCollisionBodies(); + }); + p.open(); + } + + setupTerrainGUI(width: number, height: number, terrainMaxHeight: number) { + let terrainData = { + width, height, terrainMaxHeight + }; + + let f = this.gui.addFolder("terrain"); + f.add(terrainData, 'terrainMaxHeight', -100, 100, 1).name('terrainScale').onChange(v => setTerrainSize(v, 'terrainMaxHeight')).onFinishChange(v => updateShape()); + f.add(terrainData, 'width', 100, 200, 1).onChange(v => setTerrainSize(v, 'width')).onFinishChange(v => updateShape()); + f.add(terrainData, 'height', 100, 200, 1).onChange(v => setTerrainSize(v, 'height')).onFinishChange(v => updateShape()); + f.open(); + + const dimensionSpecs = { + width: { index: 0, value: terrainData.width }, + height: { index: 2, value: terrainData.height }, + terrainMaxHeight: { index: 1, value: terrainData.terrainMaxHeight } + }; + + const terrainGeometry = this.terrain.getComponent(MeshRenderer).geometry; + const setTerrainSize = (size: number, specs: 'width' | 'height' | 'terrainMaxHeight') => { + size ||= 0.01; // Avoid zero to prevent data loss + let posAttrData = terrainGeometry.getAttribute(VertexAttributeName.position); + let dimension = dimensionSpecs[specs]; + for (let i = 0, count = posAttrData.data.length / 3; i < count; i++) { + posAttrData.data[i * 3 + dimension.index] *= size / dimension.value; + } + dimension.value = size; + + if (specs !== 'terrainMaxHeight') terrainGeometry[specs] = size; + + terrainGeometry.vertexBuffer.upload(VertexAttributeName.position, posAttrData); + terrainGeometry.computeNormals(); + }; + + const terrainRb = this.terrain.getComponent(Rigidbody); + const updateShape = () => { + terrainRb.shape = Rigidbody.collisionShape.createHeightfieldTerrainShape(this.terrain); + Physics.rigidBodyUtil.activateCollisionBodies(); + }; + } +} + +class BoxGenerator extends ComponentBase { + private lastTime: number = performance.now(); // Save last time + + public container: Object3D; + public interval: number = 1000; // Interval for adding shapes + public totalShapes: number = 30; // Maximum number of shapes + + async start() { + this.container = new Object3D(); + this.object3D.addChild(this.container); + } + + // Update loop + public onUpdate(): void { + let now: number = performance.now(); + if (now - this.lastTime > this.interval) { + if (this.container.numChildren >= this.totalShapes) { + let index = Math.floor(now / this.interval) % this.totalShapes; + let shapeObject = this.container.getChildByIndex(index) as Object3D; + shapeObject.localPosition.set(Math.random() * 60 - 60 / 2, 40, Math.random() * 60 - 60 / 2); + shapeObject.getComponent(Rigidbody).updateTransform(shapeObject.localPosition, null, true); + } else { + this.addRandomShape(); + } + this.lastTime = now; // Save current time + } + } + + private addRandomShape(): void { + const shapeObject = new Object3D(); + let mr = shapeObject.addComponent(MeshRenderer); + let mat = new LitMaterial(); + mat.baseColor = Color.random(); + + let size = 1 + Math.random() / 2; + let height = 1 + Math.random() * (3 - 1); + let radius = 0.5 + Math.random() / 2; + const segments = 32; + + let shape: Ammo.btCollisionShape; + let shapeType = Math.floor(Math.random() * 6); // Six basic shapes + switch (shapeType) { + case 0: // Box shape + mr.geometry = new BoxGeometry(size, size, size); + mr.material = mat; + shape = CollisionShapeUtil.createBoxShape(shapeObject); + break; + case 1: // Sphere shape + mr.geometry = new SphereGeometry(radius, segments, segments); + mr.material = mat; + shape = CollisionShapeUtil.createSphereShape(shapeObject); + break; + case 2: // Cylinder shape + mr.geometry = new CylinderGeometry(radius, radius, height, segments, segments); + mr.materials = [mat, mat, mat]; + shape = CollisionShapeUtil.createCylinderShape(shapeObject); + break; + case 3: // Cone shape + mr.geometry = new CylinderGeometry(0.01, radius, height, segments, segments); + mr.materials = [mat, mat, mat]; + shape = CollisionShapeUtil.createConeShape(shapeObject); + break; + case 4: // Capsule shape + mr.geometry = new CylinderGeometry(radius, radius, height, segments, segments); + mr.material = mat; + const { r, g, b } = mat.baseColor; + let topSphere = Object3DUtil.GetSingleSphere(radius, r, g, b); + topSphere.y = height / 2; + let bottomSphere = topSphere.clone(); + bottomSphere.y = -height / 2; + shapeObject.addChild(topSphere); + shapeObject.addChild(bottomSphere); + shape = CollisionShapeUtil.createCapsuleShape(shapeObject); + break; + case 5: // Torus shape (convex hull shape) + mr.geometry = new TorusGeometry(radius, size / 5, segments / 2, segments / 2); + mr.material = mat; + shape = CollisionShapeUtil.createConvexHullShape(shapeObject); + break; + default: + break; + } + + const posRange = 60; + shapeObject.x = Math.random() * posRange - posRange / 2; + shapeObject.y = 40; + shapeObject.z = Math.random() * posRange - posRange / 2; + + shapeObject.localRotation = new Vector3(Math.random() * 360, Math.random() * 360, Math.random() * 360); + this.container.addChild(shapeObject); + + // Add rigidbody to shape + let rigidbody = shapeObject.addComponent(Rigidbody); + rigidbody.shape = shape; + rigidbody.mass = Math.random() * 10 + 0.1; + rigidbody.rollingFriction = 0.5; + rigidbody.damping = [0.1, 0.1]; + + // Enable continuous collision detection (CCD) + const maxDimension = Math.max(size, height, radius); + const ccdMotionThreshold = maxDimension * 0.1; // Set motion threshold to 10% of max dimension + const ccdSweptSphereRadius = maxDimension * 0.05; // Set swept sphere radius to 5% of max dimension + rigidbody.ccdSettings = [ccdMotionThreshold, ccdSweptSphereRadius]; + } +} + +new Sample_MultipleShapes().run(); diff --git a/samples/physics/Sample_PhysicsBox.ts b/samples/physics/Sample_PhysicsBox.ts index 8befd643..c69e0610 100644 --- a/samples/physics/Sample_PhysicsBox.ts +++ b/samples/physics/Sample_PhysicsBox.ts @@ -75,7 +75,7 @@ class Sample_PhysicsBox { let collider = sphere.addComponent(ColliderComponent); collider.shape = new SphereColliderShape(sphereGeo.radius); - sphere.addComponent(Rigidbody); + sphere.addComponent(Rigidbody).mass = 0.5; this.scene.addChild(sphere); } diff --git a/samples/physics/Sample_PhysicsCar.ts b/samples/physics/Sample_PhysicsCar.ts index b17337fa..7f4b1537 100644 --- a/samples/physics/Sample_PhysicsCar.ts +++ b/samples/physics/Sample_PhysicsCar.ts @@ -131,7 +131,7 @@ class VehicleKeyboardController extends ComponentBase { protected mEngineForce = 0; protected mBreakingForce = 0; protected mVehicleSteering = 0; - protected mAmmoVehicle; + protected mAmmoVehicle: Ammo.btRaycastVehicle; protected mVehicleArgs = { bodyMass: 800, friction: 1000, @@ -235,7 +235,8 @@ class VehicleKeyboardController extends ComponentBase { Engine3D.inputSystem.addEventListener(KeyEvent.KEY_UP, this.onKeyUp, this); Engine3D.inputSystem.addEventListener(KeyEvent.KEY_DOWN, this.onKeyDown, this); } - onUpdate() { + // onUpdate() { + onLateUpdate() { if (!this.mAmmoVehicle) return; const vehicle = this.mAmmoVehicle; const speed = vehicle.getCurrentSpeedKmHour(); @@ -299,7 +300,12 @@ class VehicleKeyboardController extends ComponentBase { } // update body position let tm, p, q, qua = Quaternion.HELP_0; - tm = vehicle.getChassisWorldTransform(); + // tm = vehicle.getChassisWorldTransform(); + + // Use an interpolation transform + vehicle.getRigidBody().getMotionState().getWorldTransform(Physics.TEMP_TRANSFORM); + tm = Physics.TEMP_TRANSFORM; + p = tm.getOrigin(); this.mBody.x = p.x() this.mBody.y = p.y() @@ -367,7 +373,7 @@ class fixedCameraController extends ComponentBase { set target(obj) { this._target = obj; } - onUpdate() { + onBeforeUpdate() { if (!this._target) return; this._tempDir.set(0, 0, -1); const q = Quaternion.HELP_0; diff --git a/samples/physics/Sample_Rope.ts b/samples/physics/Sample_Rope.ts new file mode 100644 index 00000000..87a4dab1 --- /dev/null +++ b/samples/physics/Sample_Rope.ts @@ -0,0 +1,129 @@ +import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, webGPUContext, HoverCameraController, Object3D, DirectLight, LitMaterial, MeshRenderer, Vector3, Object3DUtil, Color, } from "@orillusion/core"; +import { Graphic3D } from "@orillusion/graphic"; +import { Physics, Rigidbody, RopeSoftbody } from "@orillusion/physics"; +import dat from "dat.gui"; + +class Sample_Rope { + async run() { + await Physics.init({ useSoftBody: true, useDrag: true }); + await Engine3D.init({ renderLoop: () => Physics.update() }); + let view = new View3D(); + view.scene = new Scene3D(); + let sky = view.scene.addComponent(AtmosphericComponent); + + view.camera = CameraUtil.createCamera3DObject(view.scene); + view.camera.perspective(60, webGPUContext.aspect, 1, 1000.0); + view.camera.object3D.addComponent(HoverCameraController).setCamera(0, -30, 20, new Vector3(0, 3, 0)); + + let lightObj3D = new Object3D(); + let sunLight = lightObj3D.addComponent(DirectLight); + sunLight.intensity = 2; + sunLight.castShadow = true; + lightObj3D.rotationX = 24; + lightObj3D.rotationY = -151; + view.scene.addChild(lightObj3D); + sky.relativeTransform = lightObj3D.transform; + + Engine3D.startRenderView(view); + + this.createScene(view.scene); + } + + createScene(scene: Scene3D) { + // create the ground and add a rigid body + let ground = Object3DUtil.GetSingleCube(30, 0, 30, 1, 1, 1); + scene.addChild(ground); + + let rigidbody = ground.addComponent(Rigidbody); + rigidbody.mass = 0; + rigidbody.shape = Rigidbody.collisionShape.createStaticPlaneShape(); + + // create shelves + this.createShelves(scene); + + // create balls and ropes + for (let i = 0; i < 7; i++) { + let pos = new Vector3(6 - i * 2, 8, 0); + + // check if this is the last ball (tail) + let ballRb = this.createBall(scene, pos, i === 6); + + // create the rope connected to the ball + this.createRope(scene, pos, ballRb); + } + + this.debug(scene); + } + + + createShelves(scene: Scene3D) { + let shelf1 = Object3DUtil.GetSingleCube(0.2, 8, 0.2, 1, 1, 1); // left + let shelf2 = Object3DUtil.GetSingleCube(0.2, 8, 0.2, 1, 1, 1); // right + let shelf3 = Object3DUtil.GetSingleCube(20.2, 0.2, 0.2, 1, 1, 1); // top + shelf1.localPosition = new Vector3(-10, 4, 0); + shelf2.localPosition = new Vector3(10, 4, 0); + shelf3.localPosition = new Vector3(0, 8, 0); + scene.addChild(shelf1); + scene.addChild(shelf2); + scene.addChild(shelf3); + } + + createBall(scene: Scene3D, pos: Vector3, isTail: boolean) { + const ball = Object3DUtil.GetSingleSphere(0.82, 1, 1, 1); + ball.x = pos.x - (isTail ? 3 : 0); + ball.y = pos.y / 3 + (isTail ? 1.16 : 0); + scene.addChild(ball); + + let rigidbody = ball.addComponent(Rigidbody); + rigidbody.shape = Rigidbody.collisionShape.createShapeFromObject(ball); + rigidbody.mass = 1.1; + rigidbody.restitution = 1.13; + + // ball collision event to change color + let ballMaterial = ball.getComponent(MeshRenderer).material as LitMaterial; + + let timer: number | null = null; + rigidbody.collisionEvent = (contactPoint, selfBody, otherBody) => { + if (timer !== null) clearTimeout(timer); + else ballMaterial.baseColor = new Color(Color.SALMON); + + timer = setTimeout(() => { + ballMaterial.baseColor = Color.COLOR_WHITE; + timer = null; + }, 100); + } + + return rigidbody; + } + + createRope(scene: Scene3D, pos: Vector3, tailRb: Rigidbody) { + let ropeObj = new Object3D(); + let mr = ropeObj.addComponent(MeshRenderer); + mr.material = new LitMaterial(); + mr.material.topology = 'line-list'; + mr.geometry = RopeSoftbody.buildRopeGeometry(10, pos, new Vector3(0, 0, 0)); + scene.addChild(ropeObj); + + // add rope softbody component + let ropeSoftbody = ropeObj.addComponent(RopeSoftbody); + ropeSoftbody.fixeds = 1; // fixed top + ropeSoftbody.mass = 1.0; + ropeSoftbody.elasticity = 1; + ropeSoftbody.anchorRigidbodyTail = tailRb; + ropeSoftbody.anchorOffsetTail.set(0, 0.82, 0); // 0.82 is ball radius + } + + debug(scene: Scene3D) { + const graphic3D = new Graphic3D(); + scene.addChild(graphic3D); + Physics.initDebugDrawer(graphic3D); + + let gui = new dat.GUI(); + let f = gui.addFolder('PhysicsDebug'); + f.add(Physics.debugDrawer, 'enable'); + f.add(Physics.debugDrawer, 'debugMode', Physics.debugDrawer.debugModeList); + } + +} + +new Sample_Rope().run(); diff --git a/samples/physics/Sample_ShootTheBox.ts b/samples/physics/Sample_ShootTheBox.ts index c2302daf..615c9d63 100644 --- a/samples/physics/Sample_ShootTheBox.ts +++ b/samples/physics/Sample_ShootTheBox.ts @@ -43,7 +43,7 @@ class Sample_ShootTheBox { //add DirectLight let lightObj = new Object3D(); let light = lightObj.addComponent(DirectLight); - light.intensity = 8; + light.intensity = 4; light.castShadow = true; lightObj.rotationX = 60; lightObj.rotationY = 140; @@ -129,9 +129,9 @@ class Sample_ShootTheBox { let rigidBody = ball.addComponent(Rigidbody); rigidBody.mass = 10; //set velocity after rigidbody inited - rigidBody.addInitedFunction(() => { - rigidBody.velocity = ray.direction.multiplyScalar(10000 * this.ballSpeed); - }, this); + rigidBody.wait().then(() => { + rigidBody.linearVelocity = ray.direction.multiplyScalar(20 * this.ballSpeed); + }); ball.transform.localPosition = ray.origin; this.view.scene.addChild(ball); } diff --git a/samples/physics/Sample_dofSpringConstraint.ts b/samples/physics/Sample_dofSpringConstraint.ts new file mode 100644 index 00000000..06d205c1 --- /dev/null +++ b/samples/physics/Sample_dofSpringConstraint.ts @@ -0,0 +1,222 @@ +import { Engine3D, Object3D, Scene3D, View3D, Object3DUtil, Vector3, AtmosphericComponent, DirectLight, CameraUtil, HoverCameraController, Quaternion } from "@orillusion/core"; +import { Stats } from "@orillusion/stats"; +import { ActivationState, CollisionShapeUtil, DebugDrawMode, Generic6DofSpringConstraint, Physics, Rigidbody } from "@orillusion/physics"; +import dat from "dat.gui"; +import { Graphic3D } from "@orillusion/graphic"; + +class Sample_dofSpringConstraint { + scene: Scene3D; + gui: dat.GUI; + + async run() { + // Initialize physics and engine + await Physics.init({ useDrag: true }); + await Engine3D.init({ renderLoop: () => Physics.update() }); + + let scene = this.scene = new Scene3D(); + scene.addComponent(Stats); + + // 在引擎启动后初始化物理调试功能,需要为绘制器传入 graphic3D 对象 + const graphic3D = new Graphic3D(); + scene.addChild(graphic3D); + Physics.initDebugDrawer(graphic3D, { + enable: false, + debugDrawMode: DebugDrawMode.DrawConstraintLimits + }) + + this.gui = new dat.GUI(); + let f = this.gui.addFolder('PhysicsDebug'); + f.add(Physics.debugDrawer, 'enable'); + f.add(Physics.debugDrawer, 'debugMode', Physics.debugDrawer.debugModeList); + f.open(); + + let camera = CameraUtil.createCamera3DObject(scene); + camera.perspective(60, Engine3D.aspect, 0.1, 800.0); + camera.object3D.addComponent(HoverCameraController).setCamera(140, -25, 20, new Vector3(8, 4, 0)); + + // Create directional light + let lightObj3D = new Object3D(); + lightObj3D.localRotation = new Vector3(36, -130, 60); + lightObj3D.addComponent(DirectLight).castShadow = true; + scene.addChild(lightObj3D); + + // Initialize sky + scene.addComponent(AtmosphericComponent).sunY = 0.6; + + let view = new View3D(); + view.camera = camera; + view.scene = scene; + + Engine3D.startRenderView(view); + + // Create ground, bridge, and ball + this.createGround(); + this.createBridge(); + this.createBall(); + } + + //Create the ground plane. + private async createGround() { + let ground = Object3DUtil.GetPlane(Engine3D.res.whiteTexture); + ground.scaleX = 50; + ground.scaleZ = 50; + this.scene.addChild(ground); + + let rigidbody = ground.addComponent(Rigidbody); + rigidbody.shape = CollisionShapeUtil.createStaticPlaneShape(); + rigidbody.mass = 0; + } + + // Create a ball with a rigid body. + private createBall() { + let ball = Object3DUtil.GetSingleSphere(1, 1, 1, 1); + ball.localPosition = new Vector3(2, 10, 0); + this.scene.addChild(ball); + + let ballRb = ball.addComponent(Rigidbody); + ballRb.shape = CollisionShapeUtil.createSphereShape(ball); + ballRb.mass = 50; + ballRb.restitution = 1.2; + + let f = this.gui.addFolder('ball'); + f.add({ + ResetPosition: () => { + let pos = new Vector3(Math.random() * 15, 10, 0); + ballRb.updateTransform(pos, Quaternion._zero, true); + } + }, 'ResetPosition'); + f.open(); + } + + // Create a bridge using multiple segments and constraints. + private createBridge() { + const numSegments = 15; + const segmentWidth = 1; + const segmentHeight = 0.2; + const segmentDepth = 5; + const distance = 0.1; // Distance between bridge segments + const pierHeight = 5; // Height of the piers + + let bridgeSegments: Rigidbody[] = []; + for (let i = 0; i < numSegments; i++) { + const isStatic = i === 0 || i === numSegments - 1; + const mass = isStatic ? 0 : 2; + const staticHeight = isStatic ? pierHeight : 0; + let bridgeObj = Object3DUtil.GetSingleCube(segmentWidth, segmentHeight + staticHeight, segmentDepth, Math.random(), Math.random(), Math.random()); + + const posX = i * segmentWidth + i * distance || distance; + const posY = isStatic ? pierHeight / 2 + segmentHeight / 2 : pierHeight; + bridgeObj.localPosition = new Vector3(posX, posY, 0); + + this.scene.addChild(bridgeObj); + let segment = this.addBoxShapeRigidBody(bridgeObj, mass, !isStatic); + bridgeSegments.push(segment); + } + + let constraintList: Generic6DofSpringConstraint[] = []; + for (let i = 0; i < numSegments - 1; i++) { + let segmentA = bridgeSegments[i]; + let segmentB = bridgeSegments[i + 1]; + + let dofSpringConstraint = segmentA.object3D.addComponent(Generic6DofSpringConstraint); + dofSpringConstraint.targetRigidbody = segmentB; + + let selfHeight = i === 0 ? pierHeight / 2 : 0; // Start + let targetHeight = i === numSegments - 2 ? pierHeight / 2 : 0; // End + + dofSpringConstraint.pivotSelf.set(segmentWidth / 2, selfHeight, 0); + dofSpringConstraint.pivotTarget.set(-segmentWidth / 2, targetHeight, 0); + + dofSpringConstraint.linearLowerLimit.set(-distance, 0, 0); + dofSpringConstraint.linearUpperLimit.set(distance, 0, 0); + dofSpringConstraint.angularLowerLimit.set(0, -0.03, -Math.PI / 2); + dofSpringConstraint.angularUpperLimit.set(0, 0.03, Math.PI / 2); + + // Enable angular spring and configure parameters + for (let j = 3; j < 6; j++) { + dofSpringConstraint.enableSpring(j, true); + dofSpringConstraint.setStiffness(j, 10.0); + dofSpringConstraint.setDamping(j, 0.5); + dofSpringConstraint.setEquilibriumPoint(j); + } + + constraintList.push(dofSpringConstraint); + } + + this.debug(constraintList, distance); + } + + // Add a rigid body with a box shape to an object. + private addBoxShapeRigidBody(obj: Object3D, mass: number, disableHibernation?: boolean) { + let rigidbody = obj.addComponent(Rigidbody); + rigidbody.shape = CollisionShapeUtil.createBoxShape(obj); + rigidbody.mass = mass; + if (disableHibernation) rigidbody.activationState = ActivationState.DISABLE_DEACTIVATION; + return rigidbody; + } + + // Debug constraints using the dat.GUI interface. + private debug(constraintList: Generic6DofSpringConstraint[], distance: number) { + let f = this.gui.addFolder('Constraint'); + let refer = constraintList[0]; + + const spring = { + stiffness: 10.0, + damping: 0.5 + }; + f.add(spring, 'stiffness', 0, 100, 0.1).onChange(() => updateSpring()).listen(); + f.add(spring, 'damping', 0, 100, 0.1).onChange(() => updateSpring()).listen(); + + const updateSpring = () => { + constraintList.forEach(constraint => { + for (let j = 0; j < 6; j++) { + constraint.enableSpring(j, true); + constraint.setStiffness(j, spring.stiffness); + constraint.setDamping(j, spring.damping); + } + constraint.setEquilibriumPoint(); + }); + }; + + f.add({ angularLower: "angularLowerLimit" }, "angularLower"); + f.add(refer.angularLowerLimit, 'x', -Math.PI, 0, 0.01).onChange(() => updateLimit('angularLowerLimit')).listen(); + f.add(refer.angularLowerLimit, 'y', -Math.PI, 0, 0.01).onChange(() => updateLimit('angularLowerLimit')).listen(); + f.add(refer.angularLowerLimit, 'z', -Math.PI, 0, 0.01).onChange(() => updateLimit('angularLowerLimit')).listen(); + + f.add({ angularUpper: "angularUpperLimit" }, "angularUpper"); + f.add(refer.angularUpperLimit, 'x', 0, Math.PI, 0.01).onChange(() => updateLimit('angularUpperLimit')).listen(); + f.add(refer.angularUpperLimit, 'y', 0, Math.PI, 0.01).onChange(() => updateLimit('angularUpperLimit')).listen(); + f.add(refer.angularUpperLimit, 'z', 0, Math.PI, 0.01).onChange(() => updateLimit('angularUpperLimit')).listen(); + + f.add({ linearLower: "linearLowerLimit" }, "linearLower"); + f.add(refer.linearLowerLimit, 'x', -10, 0, 0.01).onChange(() => updateLimit('linearLowerLimit')).listen(); + f.add(refer.linearLowerLimit, 'y', -10, 0, 0.01).onChange(() => updateLimit('linearLowerLimit')).listen(); + f.add(refer.linearLowerLimit, 'z', -10, 0, 0.01).onChange(() => updateLimit('linearLowerLimit')).listen(); + + f.add({ linearUpper: "linearUpperLimit" }, "linearUpper"); + f.add(refer.linearUpperLimit, 'x', 0, 10, 0.01).onChange(() => updateLimit('linearUpperLimit')).listen(); + f.add(refer.linearUpperLimit, 'y', 0, 10, 0.01).onChange(() => updateLimit('linearUpperLimit')).listen(); + f.add(refer.linearUpperLimit, 'z', 0, 10, 0.01).onChange(() => updateLimit('linearUpperLimit')).listen(); + + f.add({ + Reset: () => { + constraintList.forEach(constraint => { + constraint.linearLowerLimit = new Vector3(-distance, 0, 0); + constraint.linearUpperLimit = new Vector3(distance, 0, 0); + constraint.angularLowerLimit = new Vector3(0, -0.03, -Math.PI / 2); + constraint.angularUpperLimit = new Vector3(0, 0.03, Math.PI / 2); + }); + + spring['stiffness'] = 10.0; + spring['damping'] = 0.5; + updateSpring(); + } + }, 'Reset'); + + const updateLimit = (key: string) => { + constraintList.forEach(constraint => constraint[key] = refer[key]); + }; + } +} + +new Sample_dofSpringConstraint().run(); diff --git a/src/Engine3D.ts b/src/Engine3D.ts index 18fc299d..9efca7bf 100644 --- a/src/Engine3D.ts +++ b/src/Engine3D.ts @@ -6,9 +6,7 @@ import { InputSystem } from './io/InputSystem'; import { View3D } from './core/View3D'; import { version } from '../package.json'; -import { GPUTextureFormat } from './gfx/graphics/webGpu/WebGPUConst'; import { webGPUContext } from './gfx/graphics/webGpu/Context3D'; -import { RTResourceConfig } from './gfx/renderJob/config/RTResourceConfig'; import { RTResourceMap } from './gfx/renderJob/frame/RTResourceMap'; import { ForwardRenderJob } from './gfx/renderJob/jobs/ForwardRenderJob'; @@ -20,7 +18,6 @@ import { ShaderLib } from './assets/shader/ShaderLib'; import { ShaderUtil } from './gfx/graphics/webGpu/shader/util/ShaderUtil'; import { ComponentCollect } from './gfx/renderJob/collect/ComponentCollect'; import { ShadowLightsCollect } from './gfx/renderJob/collect/ShadowLightsCollect'; -import { GUIConfig } from './components/gui/GUIConfig'; import { WasmMatrix } from '@orillusion/wasm-matrix/WasmMatrix'; import { Matrix4 } from './math/Matrix4'; import { FXAAPost } from './gfx/renderJob/post/FXAAPost'; @@ -47,19 +44,12 @@ export class Engine3D { */ public static inputSystem: InputSystem; - /** - * input system in engine3d - */ - public static divB: HTMLDivElement; - /** * more view in engine3d */ public static views: View3D[]; private static _frameRateValue: number = 0; private static _frameRate: number = 360; - private static _frameTimeCount: number = 0; - private static _deltaTime: number = 0; private static _time: number = 0; private static _beforeRender: Function; private static _renderLoop: Function; @@ -78,7 +68,7 @@ export class Engine3D { */ public static set frameRate(value: number) { this._frameRate = value; - this._frameRateValue = 1.0 / value; + this._frameRateValue = 1000 / value; if (value >= 360) { this._frameRateValue = 0; } @@ -116,6 +106,8 @@ export class Engine3D { * engine setting */ public static setting: EngineSetting = { + doublePrecision: false, + occlusionQuery: { enable: true, debug: false, @@ -326,20 +318,13 @@ export class Engine3D { */ public static async init(descriptor: { canvasConfig?: CanvasConfig; beforeRender?: Function; renderLoop?: Function; lateRender?: Function, engineSetting?: EngineSetting } = {}) { console.log('Engine Version', version); - - // for dev debug - if (import.meta.env.DEV) { - this.divB = document.createElement("div"); - this.divB.style.position = 'absolute' - this.divB.style.zIndex = '999' - this.divB.style.color = '#FFFFFF' - this.divB.style.top = '150px' - document.body.appendChild(this.divB); + if (!window.isSecureContext){ + console.warn('WebGPU is only supported in secure contexts (HTTPS or localhost)') } this.setting = { ...this.setting, ...descriptor.engineSetting } - await WasmMatrix.init(Matrix4.allocCount); + await WasmMatrix.init(Matrix4.allocCount, this.setting.doublePrecision); await webGPUContext.init(descriptor.canvasConfig); @@ -376,30 +361,30 @@ export class Engine3D { return; } - /** - * set render view and start renderer - * @param view - * @returns - */ - public static startRenderView(view: View3D) { - this.renderJobs ||= new Map(); - this.views = [view]; + private static startRenderJob(view: View3D){ let renderJob = new ForwardRenderJob(view); this.renderJobs.set(view, renderJob); - let presentationSize = webGPUContext.presentationSize; - // RTResourceMap.createRTTexture(RTResourceConfig.colorBufferTex_NAME, presentationSize[0], presentationSize[1], GPUTextureFormat.rgba16float, false); if (this.setting.pick.mode == `pixel`) { let postProcessing = view.scene.getOrAddComponent(PostProcessingComponent); postProcessing.addPost(FXAAPost); - - } else { } if (this.setting.pick.mode == `pixel` || this.setting.pick.mode == `bound`) { view.enablePick = true; } + return renderJob; + } + /** + * set render view and start renderer + * @param view + * @returns + */ + public static startRenderView(view: View3D) { + this.renderJobs ||= new Map(); + this.views = [view]; + let renderJob = this.startRenderJob(view); this.resume(); return renderJob; } @@ -414,21 +399,7 @@ export class Engine3D { this.renderJobs ||= new Map(); this.views = views; for (let i = 0; i < views.length; i++) { - const view = views[i]; - let renderJob = new ForwardRenderJob(view); - this.renderJobs.set(view, renderJob); - let presentationSize = webGPUContext.presentationSize; - - if (this.setting.pick.mode == `pixel`) { - let postProcessing = view.scene.addComponent(PostProcessingComponent); - postProcessing.addPost(FXAAPost); - } else { - RTResourceMap.createRTTexture(RTResourceConfig.colorBufferTex_NAME, presentationSize[0], presentationSize[1], GPUTextureFormat.rgba16float, false); - } - - if (this.setting.pick.mode == `pixel` || this.setting.pick.mode == `bound`) { - view.enablePick = true; - } + this.startRenderJob(views[i]) } this.resume(); } @@ -446,7 +417,7 @@ export class Engine3D { * Pause the engine render */ public static pause() { - if (this._requestAnimationFrameID != 0) { + if (this._requestAnimationFrameID !== 0) { cancelAnimationFrame(this._requestAnimationFrameID); this._requestAnimationFrameID = 0; } @@ -463,23 +434,26 @@ export class Engine3D { * start engine render * @internal */ - private static render(time) { - this._deltaTime = time - this._time; - this._time = time; - + private static async render(time: number) { if (this._frameRateValue > 0) { - this._frameTimeCount += this._deltaTime * 0.001; - if (this._frameTimeCount >= this._frameRateValue * 0.95) { - this._frameTimeCount = 0; - this.updateFrame(time); + let delta = time - this._time; + while(delta < this._frameRateValue){ + let t = performance.now() + await Promise.resolve().then(()=>{ + time += (performance.now() - t) + delta = time - this._time + }) } + this._time = time; + await this.updateFrame(time); } else { - this.updateFrame(time); + await this.updateFrame(time); } - this.resume(); + this.resume() + } - private static updateFrame(time: number) { + private static async updateFrame(time: number) { Time.delta = time - Time.time; Time.time = time; Time.frame += 1; @@ -493,10 +467,10 @@ export class Engine3D { view.scene.waitUpdate(); let [w, h] = webGPUContext.presentationSize; view.camera.viewPort.setTo(0, 0, w, h); - view.camera.resetPerspective(webGPUContext.aspect); } - if (this._beforeRender) this._beforeRender(); + if (this._beforeRender) + await this._beforeRender(); /****** auto start with component list *****/ // ComponentCollect.startComponents(); @@ -555,14 +529,10 @@ export class Engine3D { } if (this._renderLoop) { - this._renderLoop(); + await this._renderLoop(); } - // console.log("useCount", Matrix4.useCount); - // let t = performance.now(); WasmMatrix.updateAllContinueTransform(0, Matrix4.useCount, 16); - // this.divB.innerText = "wasm:" + (performance.now() - t).toFixed(2); - /****** auto update global matrix share buffer write to gpu *****/ let globalMatrixBindGroup = GlobalBindGroup.modelMatrixBindGroup; globalMatrixBindGroup.writeBuffer(Matrix4.useCount * 16); @@ -587,8 +557,7 @@ export class Engine3D { } } - if (this._lateRender) this._lateRender(); + if (this._lateRender) + await this._lateRender(); } - - } diff --git a/src/assets/shader/materials/Lambert_shader.ts b/src/assets/shader/materials/Lambert_shader.ts index 7271438c..d6d8a4e1 100644 --- a/src/assets/shader/materials/Lambert_shader.ts +++ b/src/assets/shader/materials/Lambert_shader.ts @@ -24,7 +24,7 @@ export let Lambert_shader: string = /*wgsl*/ ` var uv = transformUV1.zw * ORI_VertexVarying.fragUV0 + transformUV1.xy; let baseColor = textureSample(baseMap,baseMapSampler,uv) ; - if(baseColor.w < 0.5){ + if(baseColor.w < materialUniform.alphaCutoff){ discard ; } diff --git a/src/assets/shader/materials/UnLit.ts b/src/assets/shader/materials/UnLit.ts index 05971fd0..8b311e13 100644 --- a/src/assets/shader/materials/UnLit.ts +++ b/src/assets/shader/materials/UnLit.ts @@ -32,7 +32,7 @@ export let UnLit: string = /*wgsl*/ ` var uv = transformUV1.zw * ORI_VertexVarying.fragUV0 + transformUV1.xy; let color = textureSample(baseMap,baseMapSampler,uv) ; - if(color.w < 0.5){ + if(color.w < materialUniform.alphaCutoff){ discard ; } diff --git a/src/assets/shader/materials/UnLitTextureArray.ts b/src/assets/shader/materials/UnLitTextureArray.ts index 85b20e41..e915a528 100644 --- a/src/assets/shader/materials/UnLitTextureArray.ts +++ b/src/assets/shader/materials/UnLitTextureArray.ts @@ -89,7 +89,7 @@ export let UnLitTextureArray: string = /*wgsl*/ ` // irradiance = LinearToGammaSpace(irradiance.rgb) * color.rgb ;//* att ; color += graphicNode.emissiveColor ; - if(color.w < 0.5){ + if(color.w < materialUniform.alphaCutoff){ discard ; } diff --git a/src/components/anim/AnimatorComponent.ts b/src/components/anim/AnimatorComponent.ts index 8f364bac..c39a78d5 100644 --- a/src/components/anim/AnimatorComponent.ts +++ b/src/components/anim/AnimatorComponent.ts @@ -1,3 +1,4 @@ +import { FloatArray } from "@orillusion/wasm-matrix/WasmMatrix"; import { Engine3D, Matrix4, MeshRenderer, Object3D, PrefabAvatarData, Quaternion, RenderNode, RendererMask, RendererMaskUtil, SkinnedMeshRenderer2, StorageGPUBuffer, Time, Vector3, Vector4, View3D } from "../.."; import { PropertyAnimationClip } from "../../math/AnimationCurveClip"; import { RegisterComponent } from "../../util/SerializeDecoration"; @@ -8,7 +9,7 @@ export class AnimatorComponent extends ComponentBase { public timeScale: number = 1.0; public jointMatrixIndexTableBuffer: StorageGPUBuffer; public playBlendShapeLoop: boolean = false; - protected inverseBindMatrices: Float32Array[]; + protected inverseBindMatrices: FloatArray[]; protected _avatar: PrefabAvatarData; protected _rendererList: SkinnedMeshRenderer2[]; protected propertyCache: Map diff --git a/src/components/controller/CameraControllerBase.ts b/src/components/controller/CameraControllerBase.ts index 00201a89..1d7e0539 100644 --- a/src/components/controller/CameraControllerBase.ts +++ b/src/components/controller/CameraControllerBase.ts @@ -66,7 +66,6 @@ export class CameraControllerBase { * * Get moving speed * @returns number - * @version FlyEngine */ public get speed(): number { return this._speed; @@ -76,7 +75,6 @@ export class CameraControllerBase { * * Set moving speed * @returns number - * @version FlyEngine */ public set speed(val: number) { this._speed = val; diff --git a/src/components/controller/HoverCameraController.ts b/src/components/controller/HoverCameraController.ts index bd317ee1..f3c2cafa 100644 --- a/src/components/controller/HoverCameraController.ts +++ b/src/components/controller/HoverCameraController.ts @@ -283,8 +283,7 @@ export class HoverCameraController extends ComponentBase { this._tempPos = Vector3Ex.mulScale(this._tempDir, this._distance, this._tempPos); this._tempPos.add(this._currentPos.transform.localPosition, this._tempPos); - this.transform.lookAt(this._tempPos, this._currentPos.transform.localPosition, Vector3.UP); - this.camera.lookTarget.copy(this._currentPos.transform.localPosition); + this.camera.lookAt(this._tempPos, this._currentPos.transform.localPosition, Vector3.UP); } /** diff --git a/src/components/renderer/InstanceDrawComponent.ts b/src/components/renderer/InstanceDrawComponent.ts index f1d61ee8..242a20f0 100644 --- a/src/components/renderer/InstanceDrawComponent.ts +++ b/src/components/renderer/InstanceDrawComponent.ts @@ -1,6 +1,5 @@ import { GPUContext } from "../../gfx/renderJob/GPUContext"; import { RTResourceMap } from "../../gfx/renderJob/frame/RTResourceMap"; -import { ClusterLightingRender } from "../../gfx/renderJob/passRenderer/cluster/ClusterLightingRender"; import { RenderContext } from "../../gfx/renderJob/passRenderer/RenderContext"; import { MeshRenderer } from "./MeshRenderer"; import { RenderNode } from "./RenderNode"; @@ -9,6 +8,7 @@ import { View3D } from "../../core/View3D"; import { RendererPassState } from "../../gfx/renderJob/passRenderer/state/RendererPassState"; import { PassType } from "../../gfx/renderJob/passRenderer/state/PassType"; import { ClusterLightingBuffer } from "../../gfx/renderJob/passRenderer/cluster/ClusterLightingBuffer"; +import { ComponentCollect } from "../../gfx/renderJob/collect/ComponentCollect"; export class InstanceDrawComponent extends RenderNode { @@ -64,10 +64,14 @@ export class InstanceDrawComponent extends RenderNode { }) } - public stop(): void { - + public reset(){ + if(this._keyRenderGroup.size > 0){ + this._keyRenderGroup.clear() + this._keyBufferGroup.clear() + this._keyIdsGroup.clear() + this.start() + } } - public nodeUpdate(view: View3D, passType: PassType, renderPassState: RendererPassState, clusterLightingBuffer?: ClusterLightingBuffer): void { this._keyRenderGroup.forEach((v, k) => { let instanceMatrixBuffer = this._keyBufferGroup.get(k); @@ -107,11 +111,7 @@ export class InstanceDrawComponent extends RenderNode { continue; for (let j = 0; j < passes.length; j++) { - if (!passes || passes.length == 0) - continue; let matPass = passes[j]; - // if (!matPass.enable) - // continue; GPUContext.bindGeometryBuffer(renderContext.encoder, renderNode.geometry); const renderShader = matPass; @@ -138,4 +138,13 @@ export class InstanceDrawComponent extends RenderNode { } } } + + public beforeDestroy(force?: boolean): void { + this._keyRenderGroup.clear(); + this._keyBufferGroup.clear(); + this._keyIdsGroup.clear(); + //@ts-ignore + this._keyRenderGroup = this._keyBufferGroup = this._keyIdsGroup = undefined; + ComponentCollect.removeWaitStart(this.object3D, this); + } } diff --git a/src/components/renderer/SkinnedMeshRenderer2.ts b/src/components/renderer/SkinnedMeshRenderer2.ts index 455f6cfd..8b9811ee 100644 --- a/src/components/renderer/SkinnedMeshRenderer2.ts +++ b/src/components/renderer/SkinnedMeshRenderer2.ts @@ -36,7 +36,7 @@ export class SkinnedMeshRenderer2 extends MeshRenderer { this.skinJointsName = value.skinNames; let matrixList: Float32Array[] = []; for (let i = 0; i < value.bindPose.length; i++) { - matrixList.push(value.bindPose[i].rawData.slice(0, 16)); + matrixList.push(new Float32Array(value.bindPose[i].rawData.slice(0, 16))); } this.skinInverseBindMatrices = matrixList; super.geometry = value; diff --git a/src/core/Camera3D.ts b/src/core/Camera3D.ts index 021979f8..a99f9db6 100644 --- a/src/core/Camera3D.ts +++ b/src/core/Camera3D.ts @@ -14,6 +14,7 @@ import { CubeCamera } from './CubeCamera'; import { webGPUContext } from '../gfx/graphics/webGpu/Context3D'; import { FrustumCSM } from './csm/FrustumCSM'; import { CSM } from './csm/CSM'; +import { CResizeEvent } from '../event/CResizeEvent'; /** * Camera components @@ -24,7 +25,7 @@ export class Camera3D extends ComponentBase { /** * camera Perspective */ - public fov: number = 1; + public fov: number = 60; /** * camera use name @@ -46,6 +47,31 @@ export class Camera3D extends ComponentBase { */ public far: number = 5000; + /** + * orth camera right plane + */ + public left: number = -100; + + /** + * orth camera left plane + */ + public right: number = 100; + + /** + * orth camera top plane + */ + public top: number = 100; + + /** + * orth camera bottom plane + */ + public bottom: number = -100; + + /** + * orth view size + */ + public frustumSize: number = 100; + /** * camera view port size */ @@ -56,17 +82,17 @@ export class Camera3D extends ComponentBase { */ public frustum: Frustum; - public sh_bak: Float32Array = new Float32Array([ - 2.485296, 2.52417, 2.683965, 3.544894, - 0.2323964, 0.1813751, 0.08516902, -4.860471E-05, - -0.2744142, -0.04131086, 0.2248164, -0.005996059, - 0.1551732, 0.137717, 0.1002693, -0.0006728604, - 0.2209381, 0.2109673, 0.1770538, -1.395991E-05, - 0.3529238, 0.2824739, 0.1817433, -0.0005164869, - -0.1344275, -0.1289607, -0.1347626, 7.825881E-06, - 0.2125785, 0.1779549, 0.124602, 0.000503074, - -0.1039777, -0.09676537, -0.07681116, -0.0004372867, - ]); + // public sh_bak: Float32Array = new Float32Array([ + // 2.485296, 2.52417, 2.683965, 3.544894, + // 0.2323964, 0.1813751, 0.08516902, -4.860471E-05, + // -0.2744142, -0.04131086, 0.2248164, -0.005996059, + // 0.1551732, 0.137717, 0.1002693, -0.0006728604, + // 0.2209381, 0.2109673, 0.1770538, -1.395991E-05, + // 0.3529238, 0.2824739, 0.1817433, -0.0005164869, + // -0.1344275, -0.1289607, -0.1347626, 7.825881E-06, + // 0.2125785, 0.1779549, 0.124602, 0.000503074, + // -0.1039777, -0.09676537, -0.07681116, -0.0004372867, + // ]); public sh: Float32Array = new Float32Array(36); @@ -126,21 +152,35 @@ export class Camera3D extends ComponentBase { this._enableCSM = value; } constructor() { - super(); + super(); } public init() { super.init(); this._ray = new Ray(); this.frustum = new Frustum(); + this.lookTarget = new Vector3(0, 0, 0); + // TODO: set viewport based on View3D size this.viewPort.x = 0; this.viewPort.y = 0; this.viewPort.w = webGPUContext.presentationSize[0]; this.viewPort.h = webGPUContext.presentationSize[1]; - this.lookTarget = new Vector3(0, 0, 0); - this.perspective(60, webGPUContext.aspect, 1, 1000.0); + this.updateProjection(); + webGPUContext.addEventListener(CResizeEvent.RESIZE, this.updateProjection, this) + } + + public updateProjection() { + this.aspect = webGPUContext.aspect; + if (this.type == CameraType.perspective) { + this.perspective(this.fov, this.aspect, this.near, this.far); + }else if(this.type == CameraType.ortho) { + if(this.frustumSize) + this.ortho(this.frustumSize, this.near, this.far); + else + this.orthoOffCenter(this.left, this.right, this.bottom, this.top, this.near, this.far); + } } public getShadowBias(depthTexSize: number): number { @@ -189,54 +229,47 @@ export class Camera3D extends ComponentBase { public perspective(fov: number, aspect: number, near: number, far: number) { this.fov = fov; this.aspect = aspect; - this.near = near; + this.near = Math.max(0.001, near); this.far = far; - this._projectionMatrix.perspective(fov, aspect, near, far); + this._projectionMatrix.perspective(this.fov, this.aspect, this.near, this.far); this.type = CameraType.perspective; } - public resetPerspective(aspect: number) { - if (this.type == CameraType.perspective) { - this._projectionMatrix.perspective(this.fov, aspect, this.near, this.far); - } - } - /** - * Create an orthographic camera - * @param width screen width - * @param height screen height - * @param znear camera near plane - * @param zfar camera far plane + * set an orthographic camera with a frustumSize + * @param frustumSize the frustum size + * @param near camera near plane + * @param far camera far plane */ - public ortho(width: number, height: number, znear: number, zfar: number) { - this.near = Math.max(znear, 0.1); - this.far = zfar; - this._projectionMatrix.ortho(width, height, znear, zfar); - this.type = CameraType.ortho; + public ortho(frustumSize: number, near: number, far: number) { + this.frustumSize = frustumSize; + let w = frustumSize * 0.5 * this.aspect; + let h = frustumSize * 0.5; + let left = -w / 2; + let right = w / 2; + let top = h / 2; + let bottom = -h / 2; + this.orthoOffCenter(left, right, bottom, top, near, far); } /** - * - * Create an orthographic camera - * @param l - * @param r - * @param b - * @param t - * @param zn camera near plane - * @param zf camera far plane - */ - public orthoOffCenter(l: number, r: number, b: number, t: number, zn: number, zf: number) { - this.near = Math.max(zn, 0.01); - this.far = zf; - this._projectionMatrix.orthoOffCenter(l, r, b, t, zn, zf); - this.type = CameraType.ortho; - } - - public orthoZo(l: number, r: number, b: number, t: number, zn: number, zf: number) { - this.near = Math.max(zn, 0.01); - this.far = zf; - this._projectionMatrix.orthoZO(l, r, b, t, zn, zf); + * set an orthographic camera with specified frustum space + * @param left camera left plane + * @param right camera right plane + * @param bottom camera bottom plane + * @param top camera top plane + * @param near camera near plane + * @param far camera far plane + */ + public orthoOffCenter(left: number, right: number, bottom: number, top: number, near: number, far: number){ + this.near = near; + this.far = far; + this.left = left; + this.right = right; + this.top = top; + this.bottom = bottom; this.type = CameraType.ortho; + this._projectionMatrix.orthoOffCenter(this.left, this.right, this.bottom, this.top, this.near, this.far); } /** @@ -451,21 +484,10 @@ export class Camera3D extends ComponentBase { if (target) this.lookTarget.copyFrom(target); } - /** - * @internal - */ - public resetProjectMatrix() { - this.perspective(this.fov, this.aspect, this.near, this.far); - } - /** * @internal */ public onUpdate() { - if (this.type == CameraType.perspective) { - this.aspect = webGPUContext.aspect; - this.resetProjectMatrix(); - } if (this._useJitterProjection) { this.getJitteredProjectionMatrix(); } diff --git a/src/core/geometry/GeometryBase.ts b/src/core/geometry/GeometryBase.ts index b8b18ce7..3912d00a 100644 --- a/src/core/geometry/GeometryBase.ts +++ b/src/core/geometry/GeometryBase.ts @@ -57,12 +57,8 @@ export class GeometryBase { constructor() { this.instanceID = UUID(); - this._attributeMap = new Map(); this._attributes = []; - - - this._vertexBuffer = new GeometryVertexBuffer(); } diff --git a/src/core/pool/memory/MemoryInfo.ts b/src/core/pool/memory/MemoryInfo.ts index 24b09001..c7ee4d4e 100644 --- a/src/core/pool/memory/MemoryInfo.ts +++ b/src/core/pool/memory/MemoryInfo.ts @@ -1,3 +1,4 @@ +import { FloatArray } from '@orillusion/wasm-matrix/WasmMatrix'; import { Color } from '../../../math/Color'; import { Quaternion } from '../../../math/Quaternion'; import { Vector2 } from '../../../math/Vector2'; @@ -174,6 +175,19 @@ export class MemoryInfo { tmp.set(data); } + public setFloatArray(index: number, value: FloatArray) { + let data: Float32Array; + if (value instanceof Float32Array) { + data = value; + } else { + // GPU nonsupport f64 + data = new Float32Array(value) + } + + let tmp = new Float32Array(this.dataBytes.buffer, this.dataBytes.byteOffset + index * Float32Array.BYTES_PER_ELEMENT, data.length); + tmp.set(data); + } + public setArrayBuffer(index: number, arrayBuffer: ArrayBuffer) { if (arrayBuffer instanceof Uint8Array) { this.setUint8Array(index, arrayBuffer); diff --git a/src/gfx/graphics/webGpu/Context3D.ts b/src/gfx/graphics/webGpu/Context3D.ts index d7757266..48e679df 100644 --- a/src/gfx/graphics/webGpu/Context3D.ts +++ b/src/gfx/graphics/webGpu/Context3D.ts @@ -116,7 +116,7 @@ export class Context3D extends CEventDispatcher { format: this.presentationFormat, usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, alphaMode: 'premultiplied', - colorSpace: `srgb`, + colorSpace: `srgb` }); this._resizeEvent = new CResizeEvent(CResizeEvent.RESIZE, { width: this.windowWidth, height: this.windowHeight }) diff --git a/src/gfx/graphics/webGpu/core/bindGroups/GlobalUniformGroup.ts b/src/gfx/graphics/webGpu/core/bindGroups/GlobalUniformGroup.ts index 6b1987f4..84b27223 100644 --- a/src/gfx/graphics/webGpu/core/bindGroups/GlobalUniformGroup.ts +++ b/src/gfx/graphics/webGpu/core/bindGroups/GlobalUniformGroup.ts @@ -56,7 +56,7 @@ export class GlobalUniformGroup { createBindGroup() { this.uniformByteLength = this.uniformGPUBuffer.memory.shareDataBuffer.byteLength; - this.matrixesByteLength = Matrix4.blockBytes * Matrix4.maxCount; + this.matrixesByteLength = (Matrix4.block * 4) * Matrix4.maxCount; this.globalBindGroup = webGPUContext.device.createBindGroup({ label: `global_bindGroupLayout`, diff --git a/src/gfx/graphics/webGpu/core/buffer/GPUBufferBase.ts b/src/gfx/graphics/webGpu/core/buffer/GPUBufferBase.ts index facbab66..962f1b51 100644 --- a/src/gfx/graphics/webGpu/core/buffer/GPUBufferBase.ts +++ b/src/gfx/graphics/webGpu/core/buffer/GPUBufferBase.ts @@ -11,6 +11,7 @@ import { Struct } from "../../../../../util/struct/Struct"; import { webGPUContext } from "../../Context3D"; import { MemoryDO } from "../../../../../core/pool/memory/MemoryDO"; import { MemoryInfo } from "../../../../../core/pool/memory/MemoryInfo"; +import { FloatArray } from "@orillusion/wasm-matrix/WasmMatrix"; /** * @internal @@ -279,7 +280,8 @@ export class GPUBufferBase { node = this.memory.allocation_node(16 * 4); this.memoryNodes.set(name, node); } - node.setFloat32Array(0, mat.rawData); + + node.setFloatArray(0, mat.rawData); } public setMatrixArray(name: string, mats: Matrix4[]) { @@ -290,7 +292,7 @@ export class GPUBufferBase { } for (let i = 0; i < mats.length; i++) { const mat = mats[i]; - node.setFloat32Array(i * 16, mat.rawData); + node.setFloatArray(i * 16, mat.rawData); } } @@ -365,6 +367,10 @@ export class GPUBufferBase { node.writeFloat32Array(value); break; + case `Float64Array`: + node.writeFloat32Array(new Float32Array(value)); + break; + case `Vector2`: node.writeVector2(value); break; @@ -410,7 +416,13 @@ export class GPUBufferBase { // this.applyMapAsync(); } - public mapAsyncWrite(mapAsyncArray: Float32Array, len: number) { + public mapAsyncWrite(floatArray: FloatArray, len: number) { + let mapAsyncArray: Float32Array; + if (floatArray instanceof Float64Array) { + mapAsyncArray = new Float32Array(floatArray); + } else { + mapAsyncArray = floatArray as Float32Array; + } // Upload data using mapAsync and a queue of staging buffers. let bytesLen = len; let device = webGPUContext.device; diff --git a/src/gfx/graphics/webGpu/core/buffer/MatrixGPUBuffer.ts b/src/gfx/graphics/webGpu/core/buffer/MatrixGPUBuffer.ts index 2ac8ccd6..26b1e7db 100644 --- a/src/gfx/graphics/webGpu/core/buffer/MatrixGPUBuffer.ts +++ b/src/gfx/graphics/webGpu/core/buffer/MatrixGPUBuffer.ts @@ -1,3 +1,4 @@ +import { FloatArray } from '@orillusion/wasm-matrix/WasmMatrix'; import { webGPUContext } from '../../Context3D'; import { ArrayBufferData } from './ArrayBufferData'; import { GPUBufferBase } from './GPUBufferBase'; @@ -20,7 +21,13 @@ export class MatrixGPUBuffer extends GPUBufferBase { this.createBuffer(GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | usage, size, data, "MatrixGPUBuffer"); } - public writeBufferByHeap(mapAsyncArray: Float32Array, len: number) { + public writeBufferByHeap(floatArray: FloatArray, len: number) { + let mapAsyncArray: Float32Array; + if (floatArray instanceof Float64Array) { + mapAsyncArray = new Float32Array(floatArray); + } else { + mapAsyncArray = floatArray as Float32Array; + } // Upload data using mapAsync and a queue of staging buffers. let bytesLen = len; let device = webGPUContext.device; diff --git a/src/loader/parser/prefab/mats/shader/UnLitShader.ts b/src/loader/parser/prefab/mats/shader/UnLitShader.ts index 4ccd4ce5..d0d94212 100644 --- a/src/loader/parser/prefab/mats/shader/UnLitShader.ts +++ b/src/loader/parser/prefab/mats/shader/UnLitShader.ts @@ -1,4 +1,3 @@ -import { Engine3D } from "../../../../../Engine3D"; import { GPUCullMode } from "../../../../../gfx/graphics/webGpu/WebGPUConst"; import { Texture } from "../../../../../gfx/graphics/webGpu/core/texture/Texture"; import { RenderShaderPass } from "../../../../../gfx/graphics/webGpu/shader/RenderShaderPass"; diff --git a/src/materials/Material.ts b/src/materials/Material.ts index 3247f765..4eaccba7 100644 --- a/src/materials/Material.ts +++ b/src/materials/Material.ts @@ -9,6 +9,7 @@ import { Vector2 } from "../math/Vector2"; import { Vector3 } from "../math/Vector3"; import { Vector4 } from "../math/Vector4"; import { BlendMode } from "./BlendMode"; +import { UUID } from "../util/Global"; export class Material { @@ -31,6 +32,7 @@ export class Material { protected _shader: Shader; constructor() { + this.instanceID = UUID(); } public set shader(shader: Shader) { @@ -96,6 +98,11 @@ export class Material { public set depthCompare(value: GPUCompareFunction) { this._defaultSubShader.depthCompare = value; + for (let item of this._shader.passShader.values()) { + for (let s of item) { + s.depthCompare = value; + } + } } @@ -137,7 +144,7 @@ export class Material { this._defaultSubShader.setDefine("USE_BILLBOARD", value); } - public get topology(){ + public get topology() { return this._defaultSubShader.topology; } @@ -174,6 +181,8 @@ export class Material { destroy(force: boolean) { + this.name = null; + this.instanceID = null; this._shader.destroy(); this._shader = null; } diff --git a/src/math/MathUtil.ts b/src/math/MathUtil.ts index fbcfcc4d..5994ef5c 100644 --- a/src/math/MathUtil.ts +++ b/src/math/MathUtil.ts @@ -227,7 +227,6 @@ export class MathUtil { * @param toDirection The transformed direction * @param target The calculated quaternion is null by default and the result is returned * @returns Quaternion The calculated quaternion returns a new instance created if target is null - * @version Orillusion3D 0.5.1 */ public static fromToRotation(fromDirection: Vector3, toDirection: Vector3, target: Quaternion = null): Quaternion { target ||= new Quaternion(); @@ -258,7 +257,7 @@ export class MathUtil { */ public static transformVector(matrix: Matrix4, vector: Vector3, result: Vector3 = null): Vector3 { result ||= new Vector3(); - let raw: Float32Array = matrix.rawData; + let raw = matrix.rawData; let a: number = raw[0]; let e: number = raw[1]; let i: number = raw[2]; diff --git a/src/math/Matrix4.ts b/src/math/Matrix4.ts index 97c6fbf8..eb9d59a8 100644 --- a/src/math/Matrix4.ts +++ b/src/math/Matrix4.ts @@ -1,4 +1,4 @@ -import { WasmMatrix } from '@orillusion/wasm-matrix/WasmMatrix'; +import { CreateFloatArray, FloatArray, WasmMatrix } from '@orillusion/wasm-matrix/WasmMatrix'; import { DEGREES_TO_RADIANS, clamp, RADIANS_TO_DEGREES } from './MathUtil'; import { Orientation3D } from './Orientation3D'; import { Quaternion } from './Quaternion'; @@ -55,7 +55,7 @@ export class Matrix4 { /** * matrix do use share bytesArray */ - public static dynamicMatrixBytes: Float32Array; + public static dynamicMatrixBytes: FloatArray; /** * cache all use do matrix @@ -109,11 +109,10 @@ export class Matrix4 { public offset: number = 0; /** - * matrix raw data format Float32Array - * @see {@link Float32Array} - * @version Orillusion3D 0.5.1 + * matrix raw data format FloatArray + * @see {@link FloatArray} */ - public rawData: Float32Array; + public rawData: FloatArray; private _position: Vector3; @@ -121,8 +120,7 @@ export class Matrix4 { * alloc web runtime cpu memory totalCount * 4(float) * 4 * init matrix memory by totalCount * 4(float) * 4 * @param count every alloc matrix count - * @version Orillusion3D 0.5.1 - */ + */ public static allocMatrix(allocCount: number) { this.allocCount = allocCount; @@ -133,7 +131,7 @@ export class Matrix4 { this.dynamicGlobalMatrixRef ||= []; this.dynamicGlobalMatrixRef.forEach((m) => { m.offset = Matrix4.wasmMatrixPtr + m.index * Matrix4.blockBytes; - m.rawData = new Float32Array(Matrix4.dynamicMatrixBytes.buffer, m.offset, 16); + m.rawData = CreateFloatArray(Matrix4.dynamicMatrixBytes.buffer, m.offset, 16); }); Matrix4.help_matrix_0 ||= new Matrix4(); @@ -151,7 +149,6 @@ export class Matrix4 { * @param toDirection second direction * @param target ref matrix * @returns return new one matrix - * @version Orillusion3D 0.5.1 */ public static fromToRotation(fromDirection: Vector3, toDirection: Vector3, target?: Matrix4): Matrix4 { target ||= new Matrix4(); @@ -166,7 +163,6 @@ export class Matrix4 { * @param z z on the central axis * @param degrees rotation angle * @returns Matrix4 result - * @version Orillusion3D 0.5.1 */ public static getAxisRotation(x: number, y: number, z: number, degrees: number): Matrix4 { let m: Matrix4 = new Matrix4(); @@ -332,7 +328,7 @@ export class Matrix4 { // if (Matrix4.dynamicGlobalMatrixRef) { Matrix4.dynamicGlobalMatrixRef[this.index] = this; Matrix4.useCount++; - this.rawData = new Float32Array(Matrix4.dynamicMatrixBytes.buffer, this.offset, 16); + this.rawData = CreateFloatArray(Matrix4.dynamicMatrixBytes.buffer, this.offset, 16); // } else { // this.rawData = new Float32Array(16); // } @@ -347,29 +343,27 @@ export class Matrix4 { * @param eye eye position * @param at target position * @param up normalize axis way - * @version Orillusion3D 0.5.1 */ public lookAt(eye: Vector3, at: Vector3, up: Vector3 = Vector3.Y_AXIS): void { let data = this.rawData; let zAxis: Vector3 = at.subtract(eye, Vector3.HELP_0); - if (zAxis.length < 0.0001) { + if (zAxis.length === 0) { zAxis.z = 1; } zAxis.normalize(); let xAxis: Vector3 = up.crossProduct(zAxis, Vector3.HELP_1); - - if (xAxis.length < 0.0001) { - if (Math.abs(up.z) > 0.9999) { + if (xAxis.length === 0) { + if (Math.abs(up.z) === 1) { zAxis.x += 0.0001; } else { zAxis.z -= 0.0001; } zAxis.normalize(); + xAxis = up.crossProduct(zAxis, Vector3.HELP_1) } - xAxis = up.crossProduct(zAxis, xAxis).normalize(); - - let yAxis = zAxis.crossProduct(xAxis, Vector3.HELP_2).normalize(); + xAxis.normalize(); + let yAxis = zAxis.crossProduct(xAxis, Vector3.HELP_2) data[0] = xAxis.x; data[1] = yAxis.x; @@ -389,21 +383,19 @@ export class Matrix4 { data[12] = -xAxis.dotProduct(eye); data[13] = -yAxis.dotProduct(eye); data[14] = -zAxis.dotProduct(eye); - data[15] = 1; } - private static float32Array = new Float32Array(16).fill(0); + private static floatArray: FloatArray = new Float64Array(16).fill(0); /** * matrix multiply * @param mat4 multiply target - * @version Orillusion3D 0.5.1 */ public multiply(mat4: Matrix4): void { let a = this.rawData; let b = mat4.rawData; - let r = Matrix4.float32Array; + let r = Matrix4.floatArray; r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; @@ -526,10 +518,9 @@ export class Matrix4 { * @param v convert target * @param target ref one vector3 * @returns Vector3 - * @version Orillusion3D 0.5.1 */ public transformVector4(v: Vector3, target?: Vector3): Vector3 { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; target ||= new Vector3(); @@ -580,7 +571,6 @@ export class Matrix4 { * @param aspect aspect ratio * @param zn near plane * @param zf far plane - * @version Orillusion3D 0.5.1 */ public perspective(fov: number, aspect: number, zn: number, zf: number) { let data = this.rawData; @@ -640,7 +630,6 @@ export class Matrix4 { }; /** - * @version Orillusion3D 0.5.1 * set matrix orthogonal projection * @param w screen width * @param h screen height @@ -739,7 +728,6 @@ export class Matrix4 { * set matrix from two direction * @param fromDirection first direction * @param toDirection second direction - * @version Orillusion3D 0.5.1 */ public transformDir(fromDirection: Vector3, toDirection: Vector3): this { let data = this.rawData; @@ -862,7 +850,6 @@ export class Matrix4 { /** * multiply matrix a b * @param lhs target matrix - * @version Orillusion3D 0.5.1 */ public append(lhs: Matrix4): void { let data = this.rawData; @@ -908,7 +895,6 @@ export class Matrix4 { * matrix a add matrix b * @param lhs target matrix. * @returns Matrix4 result. - * @version Orillusion3D 0.5.1 */ public add(lhs: Matrix4): Matrix4 { let data = this.rawData; @@ -971,7 +957,6 @@ export class Matrix4 { * matrix a sub matrix b * @param lhs target matrix b. * @returns Matrix4 . - * @version Orillusion3D 0.5.1 */ public sub(lhs: Matrix4): Matrix4 { let data = this.rawData; @@ -1035,7 +1020,6 @@ export class Matrix4 { * Matrix times components. * @param v This matrix is going to be multiplied by this value * @returns Matrix4 Returns a multiplicative result matrix. - * @version Orillusion3D 0.5.1 */ public mult(v: number): Matrix4 { let data = this.rawData; @@ -1067,8 +1051,7 @@ export class Matrix4 { // * @param x Angle of rotation around the x axis. // * @param y Angle of rotation around the y axis. // * @param z Angle of rotation around the z axis. - // * @version Orillusion3D 0.5.1 - // */ + // // */ // public rotation(x: number, y: number, z: number) { // Quaternion.CALCULATION_QUATERNION.fromEulerAngles(x, y, z); // this.makeTransform( @@ -1093,7 +1076,6 @@ export class Matrix4 { * Create a matrix based on the axis and rotation Angle (the matrix created by rotating the degrees according to the axis) * @param degrees Angle of rotation. * @param axis Rotation Angle around axis axis. Axis needs to be specified as the orientation of an axis between x/y/z - * @version Orillusion3D 0.5.1 */ public createByRotation(degrees: number, axis: Vector3): this { let tmp: Matrix4 = Matrix4.helpMatrix; @@ -1170,7 +1152,6 @@ export class Matrix4 { * @param xScale x axis scaling * @param yScale y axis scaling * @param zScale z axis scaling - * @version Orillusion3D 0.5.1 */ public appendScale(xScale: number, yScale: number, zScale: number) { Matrix4.helpMatrix.createByScale(xScale, yScale, zScale); @@ -1182,7 +1163,6 @@ export class Matrix4 { * @param xScale x axis scaling * @param yScale y axis scaling * @param zScale z axis scaling - * @version Orillusion3D 0.5.1 */ public createByScale(xScale: number, yScale: number, zScale: number): void { let data = this.rawData; @@ -1209,7 +1189,6 @@ export class Matrix4 { * @param x x axis scaling * @param y y axis scaling * @param z z axis scaling - * @version Orillusion3D 0.5.1 */ public appendTranslation(x: number, y: number, z: number) { let data = this.rawData; @@ -1221,7 +1200,6 @@ export class Matrix4 { /** * Returns a clone of the current matrix * @returns Matrix4 The cloned matrix - * @version Orillusion3D 0.5.1 */ public clone(): Matrix4 { let ret: Matrix4 = new Matrix4(); @@ -1233,7 +1211,6 @@ export class Matrix4 { * Assigns a value to one row of the current matrix * @param row Row of copy * @param Vector3 Value of copy - * @version Orillusion3D 0.5.1 */ public copyRowFrom(row: number, Vector3: Vector3) { let data = this.rawData; @@ -1271,7 +1248,6 @@ export class Matrix4 { * One of the rows in the copy matrix stores the values in Vector3. * @param row Row of copy * @param Vector3 Copy the storage target - * @version Orillusion3D 0.5.1 */ public copyRowTo(row: number, Vector3: Vector3) { let data = this.rawData; @@ -1309,10 +1285,9 @@ export class Matrix4 { * Assigns the value of a matrix to the current matrix. * @param sourceMatrix3D source Matrix * @returns Returns the current matrix - * @version Orillusion3D 0.5.1 */ public copyFrom(sourceMatrix3D: Matrix4): Matrix4 { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; data[0] = sourceMatrix3D.rawData[0]; data[1] = sourceMatrix3D.rawData[1]; data[2] = sourceMatrix3D.rawData[2]; @@ -1337,10 +1312,9 @@ export class Matrix4 { * @param vector The target array. * @param index copy from the index of the array. * @param transpose Whether to transpose the current matrix. - * @version Orillusion3D 0.5.1 */ - public copyRawDataTo(vector: Float32Array, index: number = 0, transpose: boolean = false) { - let data: Float32Array = this.rawData; + public copyRawDataTo(vector: FloatArray, index: number = 0, transpose: boolean = false) { + let data: FloatArray = this.rawData; vector[0 + index] = data[0]; vector[1 + index] = data[1]; vector[2 + index] = data[2]; @@ -1363,10 +1337,9 @@ export class Matrix4 { * Assigns a value to a column of the current matrix * @param col column * @param Vector3 Source of value - * @version Orillusion3D 0.5.1 */ public copyColFrom(col: number, Vector3: Vector3) { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; switch (col) { case 0: data[0] = Vector3.x; @@ -1401,10 +1374,9 @@ export class Matrix4 { * Copy a column of the current matrix * @param col column * @param Vector3 Target of copy - * @version Orillusion3D 0.5.1 */ public copyColTo(col: number, Vector3: Vector3) { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; switch (col) { case 0: Vector3.x = data[0]; @@ -1438,7 +1410,6 @@ export class Matrix4 { /** * Copy the current matrix * @param dest Target of copy - * @version Orillusion3D 0.5.1 */ public copyToMatrix3D(dest: Matrix4) { dest.rawData = this.rawData.slice(0); @@ -1454,7 +1425,7 @@ export class Matrix4 { return this; } - private static decomposeRawData = new Float32Array(16).fill(0) + private static decomposeRawData = new Float64Array(16).fill(0) /** * Decompose the current matrix * @param orientationStyle The default decomposition type is Orientation3D.EULER_ANGLES @@ -1462,7 +1433,6 @@ export class Matrix4 { * @see Orientation3D.EULER_ANGLES * @see Orientation3D.QUATERNION * @returns Vector3[3] pos rot scale - * @version Orillusion3D 0.5.1 */ public decompose(orientationStyle: string = 'eulerAngles', target?: Vector3[]): Vector3[] { let q: Quaternion = Quaternion.CALCULATION_QUATERNION; @@ -1657,12 +1627,11 @@ export class Matrix4 { * @param v Vector to transform * @param target The default is null and if the current argument is null then a new Vector3 will be returned * @returns Vector3 The transformed vector - * @version Orillusion3D 0.5.1 */ public deltaTransformVector(v: Vector3, target?: Vector3): Vector3 { target ||= new Vector3(); - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; let x: number = v.x; let y: number = v.y; let z: number = v.z; @@ -1676,10 +1645,9 @@ export class Matrix4 { /** * Unifies the current matrix - * @version Orillusion3D 0.5.1 */ public identity() { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; //1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 data[0] = 1; data[1] = 0; @@ -1703,10 +1671,9 @@ export class Matrix4 { /** * Fill the current matrix * @param value The filled value - * @version Orillusion3D 0.5.1 */ public fill(value: number) { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; data[1] = value; data[2] = value; data[3] = value; @@ -1727,12 +1694,11 @@ export class Matrix4 { /** * Invert the current matrix - * @version Orillusion3D 0.5.1 */ public invers33() { /// Invert a 3x3 using cofactors. This is about 8 times faster than /// the Numerical Recipes code which uses Gaussian elimination. - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; let rkInverse_00 = data[5] * data[10] - data[9] * data[6]; let rkInverse_01 = data[8] * data[6] - data[4] * data[10]; @@ -1764,12 +1730,11 @@ export class Matrix4 { /** * Invert the current matrix * @returns boolean Whether can invert it - * @version Orillusion3D 0.5.1 */ public invert(): boolean { let d = this.determinant; let invertable = Math.abs(d) > 0.00000000001; - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; if (invertable) { d = 1 / d; @@ -1817,7 +1782,7 @@ export class Matrix4 { * @returns world coordinate */ public transformPoint(v: Vector3, target?: Vector3): Vector3 { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; target ||= new Vector3(); let x: number = v.x; @@ -1836,10 +1801,9 @@ export class Matrix4 { * @param v Vector of transformation * @param target If the current argument is null then a new Vector3 will be returned * @returns Vector3 The transformed vector - * @version Orillusion3D 0.5.1 */ public transformVector(v: Vector3, target?: Vector3): Vector3 { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; target ||= new Vector3(); @@ -1856,10 +1820,9 @@ export class Matrix4 { /** * The current matrix transpose - * @version Orillusion3D 0.5.1 */ public transpose() { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; for (let i: number = 0; i < Matrix4.helpMatrix.rawData.length; i++) { Matrix4.helpMatrix.rawData[i] = data[i]; @@ -1882,10 +1845,9 @@ export class Matrix4 { /** * Returns the matrix determinant * @returns number determinant - * @version Orillusion3D 0.5.1 */ public get determinant(): number { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; return ( (data[0] * data[5] - data[4] * data[1]) * (data[10] * data[15] - data[14] * data[11]) - (data[0] * data[9] - data[8] * data[1]) * (data[6] * data[15] - data[14] * data[7]) + @@ -1903,7 +1865,7 @@ export class Matrix4 { */ public getPosition(out?: Vector3): Vector3 { out ||= new Vector3(); - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; out.x = data[12]; out.y = data[13]; out.z = data[14]; @@ -1913,7 +1875,6 @@ export class Matrix4 { /** * Return translation * @returns Vector3 Position of translation - * @version Orillusion3D 0.5.1 */ public get position(): Vector3 { this._position.set(this.rawData[12], this.rawData[13], this.rawData[14]); @@ -1923,10 +1884,9 @@ export class Matrix4 { /** * Set Position of translation * @param value Position of translation - * @version Orillusion3D 0.5.1 */ public set position(value: Vector3) { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; data[12] = value.x; data[13] = value.y; data[14] = value.z; @@ -1936,10 +1896,9 @@ export class Matrix4 { * get Component of scale * * @returns Vector3 scale - * @version Orillusion3D 0.5.1 */ public get scale(): Vector3 { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; return new Vector3(data[0], data[5], data[10]); } @@ -1947,7 +1906,7 @@ export class Matrix4 { * Set component of scale */ public set scale(value: Vector3) { - let data: Float32Array = this.rawData; + let data: FloatArray = this.rawData; data[0] = value.x; data[5] = value.y; data[10] = value.z; @@ -1961,7 +1920,6 @@ export class Matrix4 { * Returns the value of the matrix as a string * * @returns string - * @version Orillusion3D 0.5.1 */ public toString(): string { let data = this.rawData; @@ -2007,7 +1965,6 @@ export class Matrix4 { * @param m0 Matrix 0 * @param m1 Matrix 1 * @param t Factor of interpolation 0.0 - 1.0 - * @version Orillusion3D 0.5.1 */ public lerp(m0: Matrix4, m1: Matrix4, t: number): void { ///t(m1 - m0) + m0 @@ -2036,7 +1993,6 @@ export class Matrix4 { /** * Get the maximum value of the matrix scaled on each axis - * @version Orillusion3D 0.5.1 4.0 */ public getMaxScaleOnAxis(): number { let te = this.rawData; @@ -2458,7 +2414,7 @@ export function rotMatrix(mat: Matrix4, q: Quaternion) { let z: number = q.z; let w: number = q.w; - let rawData: Float32Array = mat.rawData; + let rawData: FloatArray = mat.rawData; let xy2: number = 2.0 * x * y; let xz2: number = 2.0 * x * z; let xw2: number = 2.0 * x * w; diff --git a/src/math/Plane3D.ts b/src/math/Plane3D.ts index 5f8c532d..8f52c13a 100644 --- a/src/math/Plane3D.ts +++ b/src/math/Plane3D.ts @@ -8,7 +8,6 @@ import { PlaneClassification } from "./PlaneClassification"; * Plane3D 类 3D空间中的平面表示数据 * 由a,b,c,d4个分量组成 在三维空间中定义了一个平面 Ax + By + Cz + D = 0 * @includeExample geom/Plane3D.ts -* @version * @platform Web,Native */ export class Plane3D { diff --git a/src/math/Quaternion.ts b/src/math/Quaternion.ts index ac084298..3ff6f28b 100644 --- a/src/math/Quaternion.ts +++ b/src/math/Quaternion.ts @@ -1,3 +1,4 @@ +import { FloatArray } from '@orillusion/wasm-matrix/WasmMatrix'; import { DEGREES_TO_RADIANS, RADIANS_TO_DEGREES } from './MathUtil'; import { Orientation3D } from './Orientation3D'; import { Vector3 } from './Vector3'; @@ -339,7 +340,7 @@ export class Quaternion { * @param m * @returns */ - public setFromRotationMatrix(m: { rawData: Float32Array }) { + public setFromRotationMatrix(m: { rawData: FloatArray }) { const te = m.rawData; const m11 = te[0]; const m12 = te[4]; diff --git a/src/math/Vector4.ts b/src/math/Vector4.ts index 4fc56e92..8ab26b4a 100644 --- a/src/math/Vector4.ts +++ b/src/math/Vector4.ts @@ -102,7 +102,6 @@ export class Vector4 { /** * A three-dimensional position or projection that can be used as * a perspective projection can also be a w in a quaternion - * @version Orillusion3D 0.5.1 */ public w: number = 1; diff --git a/src/setting/EngineSetting.ts b/src/setting/EngineSetting.ts index 209adc37..5a440c8e 100644 --- a/src/setting/EngineSetting.ts +++ b/src/setting/EngineSetting.ts @@ -11,6 +11,11 @@ import { SkySetting } from "./SkySetting"; export type EngineSetting = { + /** + * use double precision matrix + */ + doublePrecision: boolean; + /** * @internal */