From 452d730ef3377867cd81fe6d78e3a1b744c4e2b5 Mon Sep 17 00:00:00 2001 From: MorenID <51897387+ID-Emmett@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:41:54 +0800 Subject: [PATCH] feat(physics): add RopeSoftBody, rigidbody dragger, and enhance collisionShapeUtil (#448) --- packages/physics/Physics.ts | 19 +- packages/physics/index.ts | 1 + packages/physics/package.json | 6 +- packages/physics/rigidbody/Rigidbody.ts | 8 +- packages/physics/softbody/ClothSoftbody.ts | 409 ++++++------------ packages/physics/softbody/RopeSoftbody.ts | 200 +++++++++ packages/physics/softbody/SoftbodyBase.ts | 170 ++++++++ packages/physics/utils/CollisionShapeUtil.ts | 201 +++++++-- packages/physics/utils/PhysicsDragger.ts | 197 +++++++++ samples/physics/Sample_Cloth.ts | 110 +++++ samples/physics/Sample_Dominoes.ts | 159 ++++--- samples/physics/Sample_MultipleConstraints.ts | 137 +++--- samples/physics/Sample_PhysicsBox.ts | 2 +- samples/physics/Sample_Rope.ts | 129 ++++++ samples/physics/Sample_dofSpringConstraint.ts | 11 +- 15 files changed, 1300 insertions(+), 459 deletions(-) create mode 100644 packages/physics/softbody/RopeSoftbody.ts create mode 100644 packages/physics/softbody/SoftbodyBase.ts create mode 100644 packages/physics/utils/PhysicsDragger.ts create mode 100644 samples/physics/Sample_Cloth.ts create mode 100644 samples/physics/Sample_Rope.ts diff --git a/packages/physics/Physics.ts b/packages/physics/Physics.ts index fa16f89c..3630aa45 100644 --- a/packages/physics/Physics.ts +++ b/packages/physics/Physics.ts @@ -6,6 +6,7 @@ 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' class _Physics { private _world: Ammo.btDiscreteDynamicsWorld | Ammo.btSoftRigidDynamicsWorld; @@ -14,6 +15,7 @@ class _Physics { private _gravity: Vector3 = new Vector3(0, -9.8, 0); private _worldInfo: Ammo.btSoftBodyWorldInfo | null = null; private _debugDrawer: PhysicsDebugDrawer; + private _physicsDragger: PhysicsDragger; private _physicBound: BoundingBox; private _destroyObjectBeyondBounds: boolean; @@ -33,17 +35,28 @@ class _Physics { return this._debugDrawer; } + /** + * 物理拖拽器 + */ + public get physicsDragger() { + if (!this._physicsDragger) { + console.warn('To enable the dragger, set useDrag: true in Physics.init() during initialization.'); + } + return this._physicsDragger; + } + public TEMP_TRANSFORM: Ammo.btTransform; // Temp cache, save results from body.getWorldTransform() /** * 初始化物理引擎和相关配置。 * * @param options - 初始化选项参数对象。 - * @param options.useSoftBody - 是否启用软体模拟,目前仅支持布料软体类型。 + * @param options.useSoftBody - 是否启用软体模拟。 + * @param options.useDrag - 是否启用刚体拖拽功能。 * @param options.physicBound - 物理边界,默认范围:2000 2000 2000,超出边界时将会销毁该刚体。 * @param options.destroyObjectBeyondBounds - 是否在超出边界时销毁3D对象。默认 `false` 仅销毁刚体。 */ - public async init(options: { useSoftBody?: boolean, physicBound?: Vector3, destroyObjectBeyondBounds?: boolean } = {}) { + public async init(options: { useSoftBody?: boolean, useDrag?: boolean, physicBound?: Vector3, destroyObjectBeyondBounds?: boolean } = {}) { await Ammo.bind(window)(Ammo); TempPhyMath.init(); @@ -51,6 +64,8 @@ class _Physics { this.TEMP_TRANSFORM = new Ammo.btTransform(); this.initWorld(options.useSoftBody); + 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)); diff --git a/packages/physics/index.ts b/packages/physics/index.ts index 6c0793b2..d88506ff 100644 --- a/packages/physics/index.ts +++ b/packages/physics/index.ts @@ -9,6 +9,7 @@ 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'; diff --git a/packages/physics/package.json b/packages/physics/package.json index a5a9a1f2..90d0db53 100644 --- a/packages/physics/package.json +++ b/packages/physics/package.json @@ -1,6 +1,6 @@ { "name": "@orillusion/physics", - "version": "0.3.2", + "version": "0.3.3", "author": "Orillusion", "description": "Orillusion Physics Plugin, Powerd by Ammo.js", "main": "./dist/physics.umd.js", @@ -20,8 +20,10 @@ "type": "git", "url": "git+https://github.com/Orillusion/orillusion.git" }, + "dependencies": { + "@orillusion/ammo": ">=0.2.1" + }, "peerDependencies": { - "@orillusion/ammo": ">=0.2.1", "@orillusion/core": ">=0.8.0" } } diff --git a/packages/physics/rigidbody/Rigidbody.ts b/packages/physics/rigidbody/Rigidbody.ts index 7934806b..fc8cbe4c 100644 --- a/packages/physics/rigidbody/Rigidbody.ts +++ b/packages/physics/rigidbody/Rigidbody.ts @@ -452,7 +452,7 @@ export class Rigidbody extends ComponentBase { */ public set velocity(value: Vector3) { this._velocity.copyFrom(value); - this._btRigidbody?.applyForce(TempPhyMath.toBtVec(value), TempPhyMath.zeroBtVec(TempPhyMath.tmpVecB)); + this.wait().then(rb => rb.applyForce(TempPhyMath.toBtVec(this._velocity), TempPhyMath.zeroBtVec(TempPhyMath.tmpVecB))); } /** @@ -468,7 +468,8 @@ export class Rigidbody extends ComponentBase { * Set the angular velocity value of current object */ public set angularVelocity(value: Vector3) { - this._btRigidbody?.setAngularVelocity(TempPhyMath.toBtVec(value)) + this._angularVelocity.copyFrom(value) + this.wait().then(rb => rb.setAngularVelocity(TempPhyMath.toBtVec(this._angularVelocity))); } /** * Get the linear velocity value of current object @@ -483,7 +484,8 @@ export class Rigidbody extends ComponentBase { * Set the linear velocity value of current object */ public set linearVelocity(value: Vector3) { - this._btRigidbody?.setLinearVelocity(TempPhyMath.toBtVec(value)) + this._linearVelocity.copyFrom(value) + this.wait().then(rb => rb.setLinearVelocity(TempPhyMath.toBtVec(this._linearVelocity))); } /** * Get mass value diff --git a/packages/physics/softbody/ClothSoftbody.ts b/packages/physics/softbody/ClothSoftbody.ts index dcf1c13b..749ebcfa 100644 --- a/packages/physics/softbody/ClothSoftbody.ts +++ b/packages/physics/softbody/ClothSoftbody.ts @@ -1,6 +1,5 @@ -// ClothSoftbody.ts - -import { Vector3, MeshRenderer, PlaneGeometry, ComponentBase, VertexAttributeName, Quaternion } from '@orillusion/core'; +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'; @@ -10,149 +9,60 @@ import { Rigidbody } from '../rigidbody/Rigidbody'; */ export type CornerType = 'leftTop' | 'rightTop' | 'leftBottom' | 'rightBottom' | 'left' | 'right' | 'top' | 'bottom' | 'center'; -export class ClothSoftbody extends ComponentBase { - private _initResolve!: () => void; - private _initializationPromise: Promise = new Promise(r => this._initResolve = r); - private _btBodyInited: boolean = false; - private _btSoftbody: Ammo.btSoftBody; // 创建的 Ammo 软体实例 - private _btRigidbody: Ammo.btRigidBody; // 通过锚点附加的 Ammo 刚体实例 - private _anchorRigidbody: Rigidbody; +export class ClothSoftbody extends SoftbodyBase { + protected declare _geometry: PlaneGeometry; private _segmentW: number; private _segmentH: number; - private _geometry: PlaneGeometry; - private _diff: Vector3 = new Vector3(); + private _offset: Vector3 = new Vector3(); + private _btRigidbody: Ammo.btRigidBody; // 通过锚点附加的 Ammo 刚体实例 /** - * 布料四个角的位置 (00,01,10,11) + * 布料的四个角,默认以平面法向量计算各角。 */ public clothCorners: [Vector3, Vector3, Vector3, Vector3]; /** - * 软体的总质量 - * @default 1 - */ - public mass: number = 1; - - /** - * 软体的碰撞边距 - * @default 0.05 - */ - public margin: number = 0.05; - - /** - * 固定布料的节点 + * 固定节点索引。 */ public fixNodeIndices: CornerType[] | number[] = []; /** - * 布料的锚点 + * 添加锚点时需要的刚体。 */ - public anchorIndices: CornerType[] | number[] = []; + public anchorRigidbody: Rigidbody; /** - * 锚定的影响力。影响力值越大,软体节点越紧密地跟随刚体的运动。通常,这个值在0到1之间 - * @default 0.5 + * 布料的锚点。 */ - public influence: number | number[] = 0.5; - - /** - * 是否禁用锚定节点与刚体之间的碰撞,将其设置为true可以防止锚定节点和刚体之间发生物理碰撞 - * @default false - */ - public disableCollision: boolean | boolean[] = false; - - /** - * 当没有附加(锚定)到刚体时,应用绝对位置,否则是基于刚体的相对位置 - */ - public applyPosition: Vector3 = new Vector3(); - - /** - * 当没有附加(锚定)到刚体时,应用绝对旋转,否则是基于刚体的相对旋转 - */ - public applyRotation: Vector3 = new Vector3(); + public anchorIndices: CornerType[] | number[] = []; /** - * 碰撞组 - * @default 1 + * 仅在设置 `anchorRigidbody` 后有效,表示布料软体相对刚体的位置。 */ - public group: number = 1; + public anchorPosition: Vector3 = new Vector3(); /** - * 碰撞掩码 - * @default -1 + * 仅在设置 `anchorRigidbody` 后有效,表示布料软体相对刚体的旋转。 */ - public mask: number = -1; + public anchorRotation: Vector3 = new Vector3(); - /** - * 添加锚点时需要的刚体 - */ - public get anchorRigidbody(): Rigidbody { - return this._anchorRigidbody; - } - - public set anchorRigidbody(value: Rigidbody) { - this._anchorRigidbody = value; - this._diff.set(0, 0, 0); - } - - public get btBodyInited(): boolean { - return this._btBodyInited; - } - - /** - * return the soft body instance - */ - public get btSoftbody(): Ammo.btSoftBody { - return this._btSoftbody; - } - - /** - * Asynchronously retrieves the fully initialized soft body instance. - */ - public async wait(): Promise { - await this._initializationPromise; - return this._btSoftbody; - } + async start(): Promise { - /** - * 停止软体运动 - */ - public stopSoftBodyMovement(): void { - const nodes = this._btSoftbody.get_m_nodes(); - for (let i = 0; i < nodes.size(); i++) { - const node = nodes.at(i); - node.get_m_v().setValue(0, 0, 0); - node.get_m_f().setValue(0, 0, 0); + if (!(this._geometry instanceof PlaneGeometry)) { + throw new Error('The cloth softbody requires plane geometry.'); } - } - init(): void { - - if (!Physics.isSoftBodyWord) { - throw new Error('Enable soft body simulation by setting Physics.init({useSoftBody: true}) during initialization.'); + if (this.anchorRigidbody) { + this._btRigidbody = await this.anchorRigidbody.wait(); } + this._segmentW = this._geometry.segmentW; + this._segmentH = this._geometry.segmentH; - let geometry = this.object3D.getComponent(MeshRenderer).geometry; - if (!(geometry instanceof PlaneGeometry)) throw new Error('The cloth softbody requires plane geometry'); - this._geometry = geometry; - this._segmentW = geometry.segmentW; - this._segmentH = geometry.segmentH; + super.start() } - async start(): Promise { + protected initSoftBody(): Ammo.btSoftBody { - if (this._anchorRigidbody) { - this._btRigidbody = await this._anchorRigidbody.wait(); - } - - this.initSoftBody(); - - this._btBodyInited = true; - this._initResolve(); - } - - private initSoftBody(): void { - // Defines the four corners of the cloth let clothCorner00: Ammo.btVector3, clothCorner01: Ammo.btVector3, @@ -160,12 +70,25 @@ export class ClothSoftbody extends ComponentBase { 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; - clothCorner00 = TempPhyMath.setBtVec(-halfWidth, halfHeight, 0, TempPhyMath.tmpVecA); - clothCorner01 = TempPhyMath.setBtVec(halfWidth, halfHeight, 0, TempPhyMath.tmpVecB); - clothCorner10 = TempPhyMath.setBtVec(-halfWidth, -halfHeight, 0, TempPhyMath.tmpVecC); - clothCorner11 = TempPhyMath.setBtVec(halfWidth, -halfHeight, 0, TempPhyMath.tmpVecD); + + 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); @@ -173,7 +96,7 @@ export class ClothSoftbody extends ComponentBase { clothCorner11 = TempPhyMath.toBtVec(this.clothCorners[3], TempPhyMath.tmpVecD); } - this._btSoftbody = new Ammo.btSoftBodyHelpers().CreatePatch( + const clothSoftbody = new Ammo.btSoftBodyHelpers().CreatePatch( Physics.worldInfo, clothCorner00, clothCorner01, @@ -185,213 +108,141 @@ export class ClothSoftbody extends ComponentBase { true ); - this.configureSoftBody(this._btSoftbody); + return clothSoftbody; + } - this._btSoftbody.setTotalMass(this.mass, false); - Ammo.castObject(this._btSoftbody, Ammo.btCollisionObject).getCollisionShape().setMargin(this.margin); - this._btSoftbody.generateBendingConstraints(2, this._btSoftbody.get_m_materials().at(0)); + 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.fixNodeIndices.length > 0) this.applyFixedNodes(this.fixNodeIndices); // 添加锚点 if (this.anchorIndices.length > 0) { if (!this._btRigidbody) throw new Error('Needs a rigid body'); - this.setAnchor(); + this.applyAnchor(clothSoftbody); } else { - // 先旋转再平移,矩阵变换不满足交换律 - this._btSoftbody.rotate(TempPhyMath.eulerToBtQua(this.applyRotation)); - this._btSoftbody.translate(TempPhyMath.toBtVec(this.applyPosition)); - } - - // 布料变换将由顶点更新表示,避免影响需要重置三维对象变换 - this.transform.localPosition = Vector3.ZERO; - this.transform.localRotation = Vector3.ZERO; - - (Physics.world as Ammo.btSoftRigidDynamicsWorld).addSoftBody(this._btSoftbody, this.group, this.mask); - } - - private configureSoftBody(softBody: Ammo.btSoftBody): void { - // 设置配置参数 - let sbConfig = softBody.get_m_cfg(); - sbConfig.set_viterations(10); // 位置迭代次数 - sbConfig.set_piterations(10); // 位置求解器迭代次数 - // sbConfig.set_diterations(10); // 动力学迭代次数 - // sbConfig.set_citerations(10); // 碰撞迭代次数 - // sbConfig.set_kVCF(1.0); // 速度收敛系数 - // sbConfig.set_kDP(0.1); // 阻尼系数 - // sbConfig.set_kDG(0.0); // 阻力系数 - // sbConfig.set_kLF(0.05); // 升力系数 - // sbConfig.set_kPR(0.0); // 压力系数 - // sbConfig.set_kVC(0.0); // 体积保护系数 - // sbConfig.set_kDF(0.0); // 动力学系数 - // sbConfig.set_kMT(0.0); // 电磁系数 - // sbConfig.set_kCHR(1.0); // 刚性系数 - // sbConfig.set_kKHR(0.5); // 刚性恢复系数 - // sbConfig.set_kSHR(1.0); // 剪切刚性系数 - // sbConfig.set_kAHR(0.1); // 角度恢复系数 - // sbConfig.set_kSRHR_CL(1.0); // 拉伸刚性恢复系数 - // sbConfig.set_kSKHR_CL(0.5); // 刚性恢复系数 - // sbConfig.set_kSSHR_CL(0.1); // 剪切刚性恢复系数 - // sbConfig.set_kSR_SPLT_CL(0.5); // 拉伸分割系数 - // sbConfig.set_kSK_SPLT_CL(0.5); // 剪切分割系数 - // sbConfig.set_kSS_SPLT_CL(0.5); // 剪切分割系数 - // sbConfig.set_maxvolume(1.0); // 最大体积 - // sbConfig.set_timescale(1.0); // 时间缩放系数 - // sbConfig.set_collisions(0); // 碰撞设置 - - // 获取材质并设置参数 - const material = softBody.get_m_materials().at(0); - material.set_m_kLST(0.4); // 设置线性弹性系数 - material.set_m_kAST(0.4); // 设置角度弹性系数 - // material.set_m_kVST(0.2); // 设置体积弹性系数 - // material.set_m_flags(0); // 设置材质标志 - } - - 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._diff); + clothSoftbody.rotate(TempPhyMath.eulerToBtQua(this.transform.localRotation)); + clothSoftbody.translate(TempPhyMath.toBtVec(this.transform.localPosition)); } - 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._diff.x; - vertices.data[3 * i + 1] = pos.y() + this._diff.y; - vertices.data[3 * i + 2] = pos.z() + this._diff.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); } - private setAnchor() { - const anchorIndices = typeof this.anchorIndices[0] === 'number' - ? this.anchorIndices as number[] - : this.getCornerIndices(this.anchorIndices as CornerType[]); - - const nodesSize = this._btSoftbody.get_m_nodes().size(); - anchorIndices.forEach(nodeIndex => { - if (nodeIndex < 0 || nodeIndex >= nodesSize) { - console.error(`Invalid node index ${nodeIndex} for soft body`); - return; - } - }); + private applyAnchor(clothSoftbody: Ammo.btSoftBody): void { let tm = this._btRigidbody.getWorldTransform(); TempPhyMath.fromBtVec(tm.getOrigin(), Vector3.HELP_0); - Vector3.HELP_0.add(this.applyPosition, Vector3.HELP_1); + Vector3.HELP_0.add(this.anchorPosition, Vector3.HELP_1); - TempPhyMath.fromBtQua(tm.getRotation(), Quaternion.HELP_0) - Quaternion.HELP_1.fromEulerAngles(this.applyRotation.x, this.applyRotation.y, this.applyRotation.z); + 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); - this._btSoftbody.rotate(TempPhyMath.toBtQua(Quaternion.HELP_1)); - this._btSoftbody.translate(TempPhyMath.toBtVec(Vector3.HELP_1)); + clothSoftbody.rotate(TempPhyMath.toBtQua(Quaternion.HELP_1)); + clothSoftbody.translate(TempPhyMath.toBtVec(Vector3.HELP_1)); - anchorIndices.forEach((nodeIndex, idx) => { - const influence = Array.isArray(this.influence) ? (this.influence[idx] ?? 0.5) : this.influence; - const disableCollision = Array.isArray(this.disableCollision) ? (this.disableCollision[idx] ?? false) : this.disableCollision; - this._btSoftbody.appendAnchor(nodeIndex, this._btRigidbody, disableCollision, influence); + const anchorIndices = this.getCornerIndices(this.anchorIndices); + anchorIndices.forEach((nodeIndex) => { + clothSoftbody.appendAnchor(nodeIndex, this._btRigidbody, this.disableCollision, this.influence); }); } - private getVertexIndex(x: number, y: number): number { - return y * (this._segmentW + 1) + x; - } - /** * 将 CornerType 数组转换成节点索引数组。 * @param cornerList 需要转换的 CornerType 数组。 * @returns 节点索引数组 */ - private getCornerIndices(cornerList: CornerType[]): number[] { + 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.map(corner => { + 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'); + 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[]) { - // 确定索引数组 - const indexArray: number[] = typeof fixedNodeIndices[0] === 'number' - ? fixedNodeIndices as number[] - : this.getCornerIndices(fixedNodeIndices as CornerType[]); - - const nodes = this._btSoftbody.get_m_nodes(); - indexArray.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.`); - } - }); + public applyFixedNodes(fixedNodeIndices: CornerType[] | number[]): void { + this.wait().then(() => { + const indexArray = this.getCornerIndices(fixedNodeIndices); + super.applyFixedNodes(indexArray); + }) } /** - * 清除所有锚点,软体将会从附加的刚体上脱落 + * 清除锚点,软体将会从附加的刚体上脱落 */ - public clearAnchors() { + public clearAnchors(): void { this._btSoftbody.get_m_anchors().clear(); + this._offset.set(0, 0, 0); this._btRigidbody = null; + this.anchorRigidbody = null; } - public destroy(force?: boolean): void { - if (this._btBodyInited) { - (Physics.world as Ammo.btSoftRigidDynamicsWorld).removeSoftBody(this._btSoftbody); - Ammo.destroy(this._btSoftbody); - this._btSoftbody = 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); } - this._btBodyInited = false; + + 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; + 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 index b1faf766..4b80ee3f 100644 --- a/packages/physics/utils/CollisionShapeUtil.ts +++ b/packages/physics/utils/CollisionShapeUtil.ts @@ -1,4 +1,4 @@ -import { Object3D, BoundUtil, Vector3, MeshRenderer, VertexAttributeName, PlaneGeometry, Quaternion, Matrix4, BoundingBox } from '@orillusion/core'; +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'; @@ -113,31 +113,6 @@ export class CollisionShapeUtil { return shape; } - // 通过测试发现当前版本Ammo.btMultiSphereShape参数出现问题, - // 当前仅支持传入单个坐标,且球体们的位置会出现混乱, - // 传入坐标数组则完全无效。 - // 如果需要类似功能,可以使用复合或是网格形状。 - /** - * 创建多球体碰撞形状,适用于通过多个球体组合来近似复杂形状的情况。 - * 可以通过球体组合来创建近似椭球形状。 - * @param positions - 球体的位置数组。 - * @param radii - 球体的半径数组。 - * @returns Ammo.btMultiSphereShape - 多球体碰撞形状实例。 - */ - // public static createMultiSphereShape(positions: Vector3[], radii: number[]): Ammo.btMultiSphereShape { - // if (positions.length !== radii.length) { - // throw new Error("Positions and radii arrays must have the same length."); - // } - - // const btPositions = positions.map(pos => new Ammo.btVector3(pos.x, pos.y, pos.z)); - - // const shape = new Ammo.btMultiSphereShape((btPositions as any), radii, btPositions.length); - - // btPositions.forEach(pos => Ammo.destroy(pos)); - - // return shape; - // } - /** * 创建复合形状,将多个子形状组合成一个形状。 * @param childShapes - 包含子形状实例与位置、旋转属性的数组。 @@ -158,6 +133,107 @@ export class CollisionShapeUtil { 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 - 用于创建碰撞体的三维对象。 @@ -251,7 +327,7 @@ export class CollisionShapeUtil { const { vertices, indices } = (modelVertices && modelIndices) ? { vertices: modelVertices, indices: modelIndices } - : this.getAllMeshVerticesAndIndices(object3D); + : this.getAllMeshVerticesAndIndices(object3D, false); const triangleMesh = this.buildTriangleMesh(vertices, indices); const shape = new Ammo.btConvexTriangleMeshShape(triangleMesh, true); @@ -278,7 +354,7 @@ export class CollisionShapeUtil { const { vertices, indices } = (modelVertices && modelIndices) ? { vertices: modelVertices, indices: modelIndices } - : this.getAllMeshVerticesAndIndices(object3D); + : this.getAllMeshVerticesAndIndices(object3D, false); const triangleMesh = this.buildTriangleMesh(vertices, indices); const shape = new Ammo.btBvhTriangleMeshShape(triangleMesh, true, true); @@ -305,7 +381,7 @@ export class CollisionShapeUtil { const { vertices, indices } = (modelVertices && modelIndices) ? { vertices: modelVertices, indices: modelIndices } - : this.getAllMeshVerticesAndIndices(object3D); + : this.getAllMeshVerticesAndIndices(object3D, false); const triangleMesh = this.buildTriangleMesh(vertices, indices); const shape = new Ammo.btGImpactMeshShape(triangleMesh); @@ -343,26 +419,26 @@ export class CollisionShapeUtil { /** * 获取3D对象所有网格的顶点与索引。 - * @param object3D - 三维对象,通常是模型对象。 + * @param object3D - 三维对象。 + * @param isTransformChildren - 是否将子对象的顶点转换到父对象的局部坐标系。默认值为 `true`。 * @returns 顶点数据和索引数据。 */ - public static getAllMeshVerticesAndIndices(object3D: Object3D) { - let mr = object3D.getComponents(MeshRenderer); + public static getAllMeshVerticesAndIndices(object3D: Object3D, isTransformChildren: boolean = true) { + let meshRenderers = object3D.getComponents(MeshRenderer); - if (mr.length === 1) { + if (meshRenderers.length === 1 && !isTransformChildren) { return { - vertices: mr[0].geometry.getAttribute(VertexAttributeName.position).data as Float32Array, - indices: mr[0].geometry.getAttribute(VertexAttributeName.indices).data as Uint16Array + 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; - // 计算总顶点数和总索引数 - mr.forEach(e => { - totalVertexLength += e.geometry.getAttribute(VertexAttributeName.position).data.length; - totalIndexLength += e.geometry.getAttribute(VertexAttributeName.indices).data.length; + meshRenderers.forEach(renderer => { + totalVertexLength += renderer.geometry.getAttribute(VertexAttributeName.position).data.length; + totalIndexLength += renderer.geometry.getAttribute(VertexAttributeName.indices).data.length; }); let vertices = new Float32Array(totalVertexLength); @@ -372,13 +448,45 @@ export class CollisionShapeUtil { let indexOffset = 0; let currentIndexOffset = 0; - // 合并顶点和索引数据 - mr.forEach(e => { - let vertexArray = e.geometry.getAttribute(VertexAttributeName.position).data; + 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 = e.geometry.getAttribute(VertexAttributeName.indices).data; + let indexArray = renderer.geometry.getAttribute(VertexAttributeName.indices).data; for (let i = 0; i < indexArray.length; i++) { indices[indexOffset + i] = indexArray[i] + currentIndexOffset; } @@ -395,17 +503,20 @@ export class CollisionShapeUtil { * @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/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/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 index 6cc8bc27..bae5c0a3 100644 --- a/samples/physics/Sample_Dominoes.ts +++ b/samples/physics/Sample_Dominoes.ts @@ -1,4 +1,4 @@ -import { Engine3D, LitMaterial, MeshRenderer, Object3D, Scene3D, View3D, Object3DUtil, Vector3, AtmosphericComponent, DirectLight, CameraUtil, HoverCameraController, Color, Quaternion, GridObject } from "@orillusion/core"; +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"; @@ -8,44 +8,34 @@ import { Graphic3D } from '@orillusion/graphic' * Sample class demonstrating the creation of a domino effect with physics interactions. */ class Sample_Dominoes { - scene: Scene3D; - gui: dat.GUI; - async run() { // init physics and engine - await Physics.init(); + await Physics.init({ useDrag: true }); await Engine3D.init({ renderLoop: () => Physics.update() }); - Engine3D.setting.shadow.updateFrameRate = 1; - Engine3D.setting.shadow.shadowSize = 2048; - Engine3D.setting.shadow.shadowBound = 200; - let scene = this.scene = new Scene3D(); + let scene = new Scene3D(); scene.addComponent(Stats); // 启用物理调试功能时,需要为绘制器传入graphic3D对象 const graphic3D = new Graphic3D(); scene.addChild(graphic3D); Physics.initDebugDrawer(graphic3D, { enable: false }); - - 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.add(Physics, 'isStop'); - f.open(); let camera = CameraUtil.createCamera3DObject(scene); camera.perspective(60, Engine3D.aspect, 0.1, 800.0); - camera.object3D.addComponent(HoverCameraController).setCamera(0, -25, 100); + camera.object3D.addComponent(HoverCameraController).setCamera(0, -32, 80); // Create directional light let lightObj3D = new Object3D(); - lightObj3D.localRotation = new Vector3(120, 130, 50); - lightObj3D.addComponent(DirectLight).castShadow = true; + 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.6; + scene.addComponent(AtmosphericComponent).sunY = 0.8; let view = new View3D(); view.camera = camera; @@ -53,57 +43,93 @@ class Sample_Dominoes { Engine3D.startRenderView(view); - await this.initScene(); + await this.initScene(scene); + + this.debug(scene) } - // init the scene with ground, slide, ball, and dominoes. - private async initScene() { + // init the scene with ground, Pipe, ball, and dominoes. + private async initScene(scene: Scene3D) { // Create ground and add rigidbody - let ground = Object3DUtil.GetPlane(Engine3D.res.whiteTexture); - ground.scaleX = ground.scaleY = ground.scaleZ = 200; - - this.scene.addChild(ground); + let ground = Object3DUtil.GetSingleCube(100, 0.1, 100, 1, 1, 1); + scene.addChild(ground); let rigidbody = ground.addComponent(Rigidbody); - rigidbody.shape = CollisionShapeUtil.createStaticPlaneShape(); // Static plane shape at origin, extending infinitely upwards + 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(); + this.createDominoes(scene); - // init slide - await this.initSlide(); + // Create Pipe + this.createPipe(scene); // Create ball - this.createBall(); - + this.createBall(scene); } - // Load and initialize the slide model. - private async initSlide() { - let model = await Engine3D.res.loadGltf('https://raw.githubusercontent.com/ID-Emmett/static-assets/main/models/slide.glb'); - model.x = -40; - this.scene.addChild(model); + 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); + } - let rigidbody = model.addComponent(Rigidbody); - rigidbody.shape = Rigidbody.collisionShape.createBvhTriangleMeshShape(model); + // 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; - rigidbody.friction = 0.1; - // Disable debug visibility for the physics shape - rigidbody.isDisableDebugVisible = true; - this.gui.__folders['PhysicsDebug'].add(rigidbody, 'isDisableDebugVisible').listen(); } // Create a series of dominoes with rigid bodies and arrange them in an S-shaped curve. - private createDominoes() { + private createDominoes(scene: Scene3D) { const width = 0.5; const height = 5; const depth = 2; - const originX = -7; + const originX = -30; const originZ = 4.7; const totalDominoes = 40; @@ -125,9 +151,7 @@ class Sample_Dominoes { let deltaX = x - previousX; let deltaZ = z - previousZ; box.rotationY = i === 0 ? -48 : -Math.atan2(deltaZ, deltaX) * (180 / Math.PI); - - this.scene.addChild(box); - + scene.addChild(box); previousX = x; previousZ = z; @@ -143,27 +167,28 @@ class Sample_Dominoes { } // Create a ball with a rigid body. - private createBall() { - let ball = Object3DUtil.GetSingleSphere(0.8, Math.random(), Math.random(), Math.random()); + 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); - const originPos = new Vector3(-13.2 - 40, 28.6, 6.2); - ball.localPosition = originPos; let rigidbody = ball.addComponent(Rigidbody); rigidbody.shape = Rigidbody.collisionShape.createSphereShape(ball); rigidbody.mass = 50; - rigidbody.enablePhysicsTransformSync = true; - rigidbody.friction = 0.05; - - let f = this.gui.addFolder("ball"); - f.open(); - f.add(rigidbody, 'isKinematic').onChange(v => v || (rigidbody.enablePhysicsTransformSync = true)); - f.add({ SyncInfo: "Modify XYZ to sync rigidbody" }, "SyncInfo"); - f.add(ball.transform, 'x', -100, 100, 0.01).listen().onChange(() => rigidbody.clearForcesAndVelocities()); - f.add(ball.transform, 'y', 0.8, 40, 0.01).listen().onChange(() => rigidbody.clearForcesAndVelocities()); - f.add(ball.transform, 'z', -100, 100, 0.01).listen().onChange(() => rigidbody.clearForcesAndVelocities()); - f.add({ ResetPosition: () => rigidbody.updateTransform(originPos, Quaternion._zero, true) }, 'ResetPosition'); - - this.scene.addChild(ball); + } + + 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'); } } diff --git a/samples/physics/Sample_MultipleConstraints.ts b/samples/physics/Sample_MultipleConstraints.ts index 25183033..db0aefa5 100644 --- a/samples/physics/Sample_MultipleConstraints.ts +++ b/samples/physics/Sample_MultipleConstraints.ts @@ -1,6 +1,6 @@ -import { Engine3D, LitMaterial, MeshRenderer, Object3D, Scene3D, View3D, Object3DUtil, Vector3, AtmosphericComponent, DirectLight, CameraUtil, HoverCameraController, BitmapTexture2D, UnLitMaterial, PlaneGeometry, GPUCullMode, Quaternion, Color } from "@orillusion/core"; +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, ClothSoftbody, CollisionShapeUtil, DebugDrawMode, FixedConstraint, Generic6DofSpringConstraint, HingeConstraint, Physics, PointToPointConstraint, Rigidbody, SliderConstraint } from "@orillusion/physics"; +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"; @@ -13,7 +13,7 @@ class Sample_MultipleConstraints { async run() { // init physics and engine - await Physics.init({ useSoftBody: true }); + await Physics.init({ useSoftBody: true, useDrag: true }); await Engine3D.init({ renderLoop: () => Physics.update() }); this.gui = new dat.GUI(); @@ -21,7 +21,7 @@ class Sample_MultipleConstraints { this.scene = new Scene3D(); this.scene.addComponent(Stats); - // 在引擎启动后初始化物理调试功能,需要为绘制器传入 graphic3D 对象 + // 在引擎启动后初始化物理调试功能,需要为调试器传入 graphic3D 对象 const graphic3D = new Graphic3D(); this.scene.addChild(graphic3D); Physics.initDebugDrawer(graphic3D, { @@ -36,7 +36,7 @@ class Sample_MultipleConstraints { // create directional light let light = new Object3D(); light.localRotation = new Vector3(36, -130, 60); - let dl = light.addComponent(DirectLight) + let dl = light.addComponent(DirectLight); dl.castShadow = true; dl.intensity = 3; this.scene.addChild(light); @@ -48,16 +48,19 @@ class Sample_MultipleConstraints { view.camera = camera; view.scene = this.scene; - this.physicsDebug() + this.physicsDebug(); Engine3D.startRenderView(view); - // Create ground, impactor, turntable, and chains + // Create ground, turntable, and chains this.createGround(); - await this.createImpactor(); - await this.createTurntable(); - await this.createChains(); + this.createTurntable(); + this.createChains(); + // Create impactor and softBody + let impactorRb = this.createImpactor(); + this.createClothSoftbody(impactorRb); + this.createRopeSoftbody(impactorRb); } private physicsDebug() { @@ -65,22 +68,23 @@ class Sample_MultipleConstraints { 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(61, 2, 20, 1, 1, 1); + 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.createStaticPlaneShape(Vector3.UP, 1); + groundRb.shape = CollisionShapeUtil.createBoxShape(ground); groundRb.mass = 0; } - private async createImpactor() { + private createImpactor(): Rigidbody { // Create shelves const shelfSize = 0.5; const shelfHeight = 5; @@ -112,19 +116,17 @@ class Sample_MultipleConstraints { // Add rigidbody to slider let sliderRb = this.addBoxShapeRigidBody(slider, 500, true, [0.2, 0]); - // Create fulcrum - let fulcrum = Object3DUtil.GetCube(); - fulcrum.localScale = new Vector3(1, 1, 5); - fulcrum.localPosition = new Vector3(0, shelfHeight - shelfSize / 2, 3); - this.scene.addChild(fulcrum); + // 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); - // Add rigidbody to fulcrum and initialize cloth softbody - let fulcrumRb = this.addBoxShapeRigidBody(fulcrum, 200, true); - this.initClothSoftBody(fulcrumRb); + let impactorRb = this.addBoxShapeRigidBody(impactor, 200, true); - // Create fixed constraint to attach slider to fulcrum + // Create fixed constraint to attach slider to impactor let fixedConstraint = slider.addComponent(FixedConstraint); - fixedConstraint.targetRigidbody = fulcrumRb; + fixedConstraint.targetRigidbody = impactorRb; fixedConstraint.pivotTarget = new Vector3(0, 0, -3); // Create slider constraint @@ -140,6 +142,8 @@ class Sample_MultipleConstraints { // Setup slider motor event controller this.sliderMotorEventController(shelfLeftRb, shelfRightRb, sliderConstraint); + + return impactorRb; } private sliderMotorEventController(leftRb: Rigidbody, rightRb: Rigidbody, slider: SliderConstraint) { @@ -175,7 +179,7 @@ class Sample_MultipleConstraints { folder.add(timer, 'pauseDuration', 0, 3000, 1000); } - private async createTurntable() { + private createTurntable() { // Create turntable components const columnWidth = 0.5; const columnHeight = 4.75 - columnWidth / 2; @@ -184,38 +188,38 @@ class Sample_MultipleConstraints { 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 - let arm1 = Object3DUtil.GetCube(); - arm1.localScale = new Vector3(10, 0.5, 0.5); - arm1.localPosition = new Vector3(0, columnHeight + columnWidth / 2, 8); - let arm2 = arm1.clone(); - arm2.y += 10; // Ensure no overlap before adding constraints - arm2.rotationY = 45; + // Create arm compound shape + let armParent = new Object3D(); + armParent.localPosition = new Vector3(0, columnHeight + columnWidth / 2, 8); - this.scene.addChild(column); - this.scene.addChild(arm1); - this.scene.addChild(arm2); + let armChild1 = Object3DUtil.GetCube(); + armChild1.rotationY = 45; + armChild1.localScale = new Vector3(10, 0.5, 0.5); + + let armChild2 = armChild1.clone(); + armChild2.rotationY = 135; - // Add rigidbodies to turntable components - this.addBoxShapeRigidBody(column, 0); - let arm1Rb = this.addBoxShapeRigidBody(arm1, 500, true); - let arm2Rb = this.addBoxShapeRigidBody(arm2, 500, true); + 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 = arm1Rb; + hinge.targetRigidbody = armRigidbody; hinge.pivotSelf.set(0, columnHeight / 2 + columnWidth / 2, 0); hinge.enableAngularMotor(true, 5, 50); - - // Create fixed constraint to attach arm2 to arm1 - let fixedConstraint = arm2.addComponent(FixedConstraint); - fixedConstraint.targetRigidbody = arm1Rb; - fixedConstraint.rotationTarget.fromEulerAngles(0, 90, 0); - fixedConstraint.pivotTarget.set(0, 0, 0); } - private async createChains() { + private createChains() { const chainHeight = 1; let chainLink = Object3DUtil.GetCube(); @@ -277,15 +281,14 @@ class Sample_MultipleConstraints { p2p.pivotSelf.y = sphereRadius; } - private async initClothSoftBody(anchorRb: Rigidbody) { + private createClothSoftbody(anchorRb: Rigidbody) { const cloth = new Object3D(); let meshRenderer = cloth.addComponent(MeshRenderer); - meshRenderer.geometry = new PlaneGeometry(3, 3, 10, 10); + 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 @@ -295,18 +298,46 @@ class Sample_MultipleConstraints { softBody.anchorRigidbody = anchorRb; // Anchor rigidbody softBody.anchorIndices = ['leftTop', 'top', 'rightTop']; // Anchor points softBody.influence = 1; // Attachment influence - softBody.disableCollision = false; // Enable collision with anchor - softBody.applyPosition = new Vector3(0, -2.1, 0); // Relative position to anchor - softBody.applyRotation = new Vector3(0, 90, 0); // Relative rotation to anchor + softBody.disableCollision = false; // Enable collision with rigidbody + softBody.anchorPosition = new Vector3(0, -2.1, 0); // Relative position to anchor - // Configure softbody parameters softBody.wait().then(btSoftbody => { - let sbConfig = btSoftbody.get_m_cfg(); + // 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]) { 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_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_dofSpringConstraint.ts b/samples/physics/Sample_dofSpringConstraint.ts index 2fe769f6..06d205c1 100644 --- a/samples/physics/Sample_dofSpringConstraint.ts +++ b/samples/physics/Sample_dofSpringConstraint.ts @@ -1,19 +1,16 @@ -import { Engine3D, Object3D, Scene3D, View3D, Object3DUtil, Vector3, AtmosphericComponent, DirectLight, CameraUtil, HoverCameraController, Quaternion, GridObject } from "@orillusion/core"; +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"; -/** - * Sample class demonstrating the use of multiple constraints in a physics simulation. - */ -class Sample_MultipleConstraints { +class Sample_dofSpringConstraint { scene: Scene3D; gui: dat.GUI; async run() { // Initialize physics and engine - await Physics.init(); + await Physics.init({ useDrag: true }); await Engine3D.init({ renderLoop: () => Physics.update() }); let scene = this.scene = new Scene3D(); @@ -222,4 +219,4 @@ class Sample_MultipleConstraints { } } -new Sample_MultipleConstraints().run(); +new Sample_dofSpringConstraint().run();