Skip to content

约束

物理约束用于限制两个物理对象(通常是刚体)之间的相对运动。它们允许在物理模拟中创建更复杂的行为,如铰链、滑块或固定的连接。通过合理配置约束,可以实现现实世界中的各种机械结构和连接方式。

约束组件概述

约束以组件的形式添加和使用。约束父类实现了通用的约束功能,各个具体的约束组件通过继承该父类,并封装了底层的 Ammo.js 约束类型,提供了与原生约束类似的 API。这使得开发者能够省去手动创建约束的流程,可以快速、便捷地集成各种物理约束,实现复杂的物理行为。

ts
import { Object3D } from '@orillusion/core';
import { HingeConstraint, Rigidbody } from '@orillusion/physics';

let object = new Object3D();
let targetObject = new Object3D();
let rigidbody = object.addComponent(Rigidbody);
let targetRigidbody = targetObject.addComponent(Rigidbody);

// 分别为两个刚体进行相关配置,如设置 mass、shape 等
... 

// 为 object 添加铰链约束,并指定目标刚体,约束会将这两个刚体进行连接
let hingeConstraint = object.addComponent(HingeConstraint);
hingeConstraint.targetRigidbody = targetRigidbody;
// 具体的约束配置请参考下述介绍的API
...

请注意,对象必须在添加约束组件之前添加 刚体 组件。

基本用法

以下是约束的通用 API,各个约束类型还提供了独特的设置选项。

属性类型描述
constraintAmmo.btTypedConstraint获取 Ammo.js 的原生约束
breakingThresholdnumber断裂阈值,值越大,约束越不易断裂
disableCollisionsBetweenLinkedBodiesboolean禁用关联刚体的碰撞,默认值为 true
targetRigidbodyRigidbody目标刚体,约束将限制当前刚体与目标刚体之间的相对运动
pivotSelfVector3自身刚体的枢轴点,决定了约束的旋转中心
pivotTargetVector3目标刚体的枢轴点
rotationSelfQuaternion自身刚体的旋转设置
rotationTargetQuaternion目标刚体的旋转设置
方法描述
wait()异步获取完成初始化的原生约束实例
resetConstraint()重置约束,销毁当前约束实例后重新创建并返回新的约束实例

重载支持

在原生 Ammo.js 中,除了 FixedConstraint 之外,其余约束都提供了多种构造方法的重载。为了确保这些功能的完整性,约束组件中也提供了相应的重载支持。通常情况下,如果 targetRigidbody 属性未设置,约束将默认以单个刚体的形式创建。开发者可以根据具体需求,自由选择适合的约束构造方式。

约束类型

当前系统已集成了 Ammo.js 中的 7 种主要约束类型,每种约束均适用于特定的应用场景。

1. 铰链约束 HingeConstraint

铰链约束允许物体绕某个轴进行旋转,适用于门、机械臂等需要单轴旋转的场景。

属性类型描述
axisSelfVector3自身刚体上的铰链轴方向,默认值为 Vector3.UP
axisTargetVector3目标刚体上的铰链轴方向,默认值为 Vector3.UP
useReferenceFrameAboolean是否使用自身刚体的参考框架,默认值为 true
useTwoBodiesTransformOverloadboolean是否使用两个刚体的变换重载方式,默认值为 false
方法描述
setLimit()设置旋转限制
enableAngularMotor()启用或禁用角度马达
ts
let hingeConstraint = object.addComponent(HingeConstraint);
hingeConstraint.setLimit(-Math.PI / 2, Math.PI / 2, 0.9, 0.3);
hingeConstraint.enableAngularMotor(true, 1.0, 10.0);

2. 滑动关节约束 SliderConstraint

滑动关节约束允许物体沿着一个轴进行平移,并绕该轴旋转,适用于滑轨或电梯等应用场景。

属性类型描述
lowerLinLimitnumber线性运动的下限限制
upperLinLimitnumber线性运动的上限限制
lowerAngLimitnumber角度运动的下限限制
upperAngLimitnumber角度运动的上限限制
poweredLinMotorboolean是否启用线性马达
maxLinMotorForcenumber线性马达的最大推力
targetLinMotorVelocitynumber线性马达的目标速度
ts
let sliderConstraint = object.addComponent(SliderConstraint);
sliderConstraint.lowerLinLimit = -10;
sliderConstraint.upperLinLimit = 10;
sliderConstraint.poweredLinMotor = true;
sliderConstraint.maxLinMotorForce = 100;
sliderConstraint.targetLinMotorVelocity = 5;

3. 固定约束 FixedConstraint

固定约束将两个物体完全固定在一起,限制其相对位置和旋转,从而实现刚性的连接效果。

ts
let fixedConstraint = object.addComponent(FixedConstraint);
fixedConstraint.targetRigidbody = targetRigidbody; // 固定约束类型必须指定目标刚体

4. 点到点约束 PointToPointConstraint

该约束限制了两个点之间的相对运动,但允许它们在空间中自由旋转。通常用于模拟绳索或链条的连接。

ts
let p2pConstraint = object.addComponent(PointToPointConstraint);
p2pConstraint.targetRigidbody = targetRigidbody;
p2pConstraint.pivotSelf.set(0, 0, 0);
p2pConstraint.pivotTarget.set(0, 5, 0);

5. 锥形扭转约束 ConeTwistConstraint

锥形扭转约束用于创建类似球窝关节的运动,允许物体在一个锥形范围内自由旋转,并限制其绕某轴的扭转角度。

属性类型描述
twistSpannumber扭转角度限制,绕 X 轴的扭转范围
swingSpan1number摆动角度限制1,绕 Y 轴的摆动范围
swingSpan2number摆动角度限制2,绕 Z 轴的摆动范围
ts
let coneTwistConstraint = object.addComponent(ConeTwistConstraint);
coneTwistConstraint.twistSpan = Math.PI / 4;  // 限制扭转角度为 45 度

6. 通用六自由度约束 Generic6DofConstraint

该约束允许沿三个线性轴和三个角度轴自由设置运动限制,提供了最大的灵活性以满足各种复杂的连接需求。

属性类型描述
linearLowerLimitVector3线性运动的下限限制
linearUpperLimitVector3线性运动的上限限制
angularLowerLimitVector3角度运动的下限限制
angularUpperLimitVector3角度运动的上限限制
useLinearFrameReferenceFrameboolean是否使用线性参考坐标系
ts
let sixDofConstraint = object.addComponent(Generic6DofConstraint);
sixDofConstraint.linearLowerLimit = new Vector3(-1, -1, -1);  // 设置线性下限
sixDofConstraint.linearUpperLimit = new Vector3(1, 1, 1);     // 设置线性上限

7. 弹簧特性六自由度约束 Generic6DofSpringConstraint

此约束是在通用六自由度约束的基础上增加了弹簧特性,可以模拟弹簧的效果,如伸缩和振动。

方法描述
enableSpring()启用或禁用弹簧功能
setStiffness()设置弹簧的刚度
setDamping()设置弹簧的阻尼
setEquilibriumPoint()设置弹簧的平衡点
ts
let springConstraint = object.addComponent(Generic6DofSpringConstraint);
// 启用并配置弹簧:索引0、1、2对应线性轴(x, y, z),3、4、5对应角度轴(x, y, z)
for (let j = 3; j < 6; j++) {
    dofSpringConstraint.enableSpring(j, true);
    dofSpringConstraint.setStiffness(j, 10.0);
    dofSpringConstraint.setDamping(j, 0.5);
    dofSpringConstraint.setEquilibriumPoint(j);
}

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
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();

注意事项

当两个刚体通过约束连接时,目标刚体的连接点默认会位于自身刚体的中心位置。可以在创建约束时通过调整 pivotSelfpivotTarget 属性来修改它们的相对位置。然而,如果两个刚体在添加约束前处于重叠状态,这可能会导致约束模拟的不稳定。建议在添加约束前确保两个刚体没有重叠。

示例

合理配置物理约束可以显著提升物理模拟的表现力和真实性。以下示例展示了刚体、多种约束和软体之间的相互联动,充分体现了它们的协同作用。

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
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();