Skip to content

Constraint

Physical constraints are used to limit the relative motion between two physical objects (usually rigidbodies). They allow more complex behaviors to be created in physics simulations, such as hinges, sliders, or fixed connections. By properly configuring constraints, you can implement various mechanical structures and connection methods found in the real world.

Constraint Component Overview

Constraints are added and used in the form of components. The constraint base class implements common constraint functionality, while each specific constraint component inherits from this base class and encapsulates the underlying Ammo.js constraint types, providing APIs similar to the native constraints. This allows developers to skip the process of manually creating constraints and to quickly and conveniently integrate various physical constraints to achieve complex physical behaviors.

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

// Configure the two rigidbodies respectively, such as setting mass, shape, etc.
... 

// Add a hinge constraint to object and specify the target rigidbody; the constraint will connect these two rigidbodies
let hingeConstraint = object.addComponent(HingeConstraint);
hingeConstraint.targetRigidbody = targetRigidbody;
// For specific constraint configuration, refer to the API described below
...

Please note that an object must have a Rigidbody component added before adding a constraint component.

Basic Usage

The following are the common APIs for constraints. Each constraint type also provides its own unique configuration options.

PropertyTypeDescription
constraintAmmo.btTypedConstraintGets the native Ammo.js constraint
breakingThresholdnumberBreaking threshold; the larger the value, the harder it is for the constraint to break
disableCollisionsBetweenLinkedBodiesbooleanDisables collisions between linked rigidbodies; default value is true
targetRigidbodyRigidbodyTarget rigidbody; the constraint will limit the relative motion between the current rigidbody and the target rigidbody
pivotSelfVector3The pivot point of the rigidbody itself, which determines the rotation center of the constraint
pivotTargetVector3The pivot point of the target rigidbody
rotationSelfQuaternionThe rotation setting of the rigidbody itself
rotationTargetQuaternionThe rotation setting of the target rigidbody
MethodDescription
wait()Asynchronously gets the native constraint instance once initialization is complete
resetConstraint()Resets the constraint, destroying the current constraint instance, then recreating and returning a new constraint instance

Overload Support

In native Ammo.js, except for FixedConstraint, all other constraints provide multiple constructor overloads. To ensure the completeness of these features, the constraint components also provide corresponding overload support. Generally, if the targetRigidbody property is not set, the constraint will be created with a single rigidbody by default. Developers can freely choose the appropriate constraint construction method based on their specific needs.

Constraint Types

The current system has integrated the 7 main constraint types from Ammo.js, each suitable for specific application scenarios.

1. Hinge Constraint HingeConstraint

The hinge constraint allows an object to rotate around a certain axis, suitable for scenarios requiring single-axis rotation such as doors and robotic arms.

PropertyTypeDescription
axisSelfVector3The hinge axis direction on the rigidbody itself; default value is Vector3.UP
axisTargetVector3The hinge axis direction on the target rigidbody; default value is Vector3.UP
useReferenceFrameAbooleanWhether to use the reference frame of the rigidbody itself; default value is true
useTwoBodiesTransformOverloadbooleanWhether to use the two-rigidbody transform overload; default value is false
MethodDescription
setLimit()Sets the rotation limit
enableAngularMotor()Enables or disables the angular motor
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. Slider Constraint SliderConstraint

The slider constraint allows an object to translate along an axis and rotate around that axis, suitable for application scenarios such as slide rails or elevators.

PropertyTypeDescription
lowerLinLimitnumberLower limit of linear motion
upperLinLimitnumberUpper limit of linear motion
lowerAngLimitnumberLower limit of angular motion
upperAngLimitnumberUpper limit of angular motion
poweredLinMotorbooleanWhether to enable the linear motor
maxLinMotorForcenumberThe maximum force of the linear motor
targetLinMotorVelocitynumberThe target velocity of the linear motor
ts
let sliderConstraint = object.addComponent(SliderConstraint);
sliderConstraint.lowerLinLimit = -10;
sliderConstraint.upperLinLimit = 10;
sliderConstraint.poweredLinMotor = true;
sliderConstraint.maxLinMotorForce = 100;
sliderConstraint.targetLinMotorVelocity = 5;

3. Fixed Constraint FixedConstraint

The fixed constraint completely fixes two objects together, limiting their relative position and rotation, thereby achieving a rigid connection effect.

ts
let fixedConstraint = object.addComponent(FixedConstraint);
fixedConstraint.targetRigidbody = targetRigidbody; // The fixed constraint type must specify a target rigidbody

4. Point-to-Point Constraint PointToPointConstraint

This constraint limits the relative motion between two points but allows them to rotate freely in space. It is commonly used to simulate the connection of ropes or chains.

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

5. Cone Twist Constraint ConeTwistConstraint

The cone twist constraint is used to create motion similar to a ball-and-socket joint, allowing an object to rotate freely within a cone-shaped range and limiting its twist angle around a certain axis.

PropertyTypeDescription
twistSpannumberTwist angle limit, the twist range around the X axis
swingSpan1numberSwing angle limit 1, the swing range around the Y axis
swingSpan2numberSwing angle limit 2, the swing range around the Z axis
ts
let coneTwistConstraint = object.addComponent(ConeTwistConstraint);
coneTwistConstraint.twistSpan = Math.PI / 4;  // Limit the twist angle to 45 degrees

6. Generic 6-DOF Constraint Generic6DofConstraint

This constraint allows motion limits to be freely set along three linear axes and three angular axes, providing maximum flexibility to meet various complex connection requirements.

PropertyTypeDescription
linearLowerLimitVector3Lower limit of linear motion
linearUpperLimitVector3Upper limit of linear motion
angularLowerLimitVector3Lower limit of angular motion
angularUpperLimitVector3Upper limit of angular motion
useLinearFrameReferenceFramebooleanWhether to use the linear reference coordinate frame
ts
let sixDofConstraint = object.addComponent(Generic6DofConstraint);
sixDofConstraint.linearLowerLimit = new Vector3(-1, -1, -1);  // Set the linear lower limit
sixDofConstraint.linearUpperLimit = new Vector3(1, 1, 1);     // Set the linear upper limit

7. Generic 6-DOF Spring Constraint Generic6DofSpringConstraint

This constraint adds spring characteristics on top of the generic 6-DOF constraint, allowing it to simulate spring effects such as stretching and vibration.

MethodDescription
enableSpring()Enables or disables the spring functionality
setStiffness()Sets the stiffness of the spring
setDamping()Sets the damping of the spring
setEquilibriumPoint()Sets the equilibrium point of the spring
ts
let springConstraint = object.addComponent(Generic6DofSpringConstraint);
// Enable and configure the spring: indices 0, 1, 2 correspond to the linear axes (x, y, z), and 3, 4, 5 correspond to the angular axes (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;
    engine: Engine3D;

    async run() {
        // Initialize physics and engine
        await Physics.init({ useDrag: true });
        let engine = this.engine = await Engine3D.init({ renderLoop: () => Physics.update() });

        let scene = this.scene = new Scene3D();
        scene.addComponent(Stats);

        // Initialize the physics debug feature after the engine starts; a graphic3D object must be passed to the drawer
        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, engine.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);
        let light = lightObj3D.addComponent(DirectLight);
        light.castShadow = true;
        light.enableCSM = true;
        scene.addChild(lightObj3D);

        // Initialize sky
        scene.addComponent(AtmosphericComponent).sunY = 0.6;

        let view = new View3D();
        view.camera = camera;
        view.scene = scene;

        engine.startRenderView(view);

        // Create ground, bridge, and ball
        this.createGround();
        this.createBridge();
        this.createBall();
    }

    //Create the ground plane.
    private async createGround() {
        let ground = Object3DUtil.GetPlane(this.engine.context3D, this.engine.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();

Notes

When two rigidbodies are connected through a constraint, the connection point of the target rigidbody is by default located at the center of the rigidbody itself. You can modify their relative positions by adjusting the pivotSelf or pivotTarget properties when creating the constraint. However, if the two rigidbodies are overlapping before the constraint is added, this may cause instability in the constraint simulation. It is recommended to ensure that the two rigidbodies do not overlap before adding the constraint.

Example

Properly configuring physical constraints can significantly enhance the expressiveness and realism of physics simulations. The following example demonstrates the interaction among rigidbodies, various constraints, and softbodies, fully reflecting their collaborative effects.

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;
    engine: Engine3D;

    async run() {
        // init physics and engine
        await Physics.init({ useSoftBody: true, useDrag: true });
        let engine = this.engine = await Engine3D.init({ renderLoop: () => Physics.update() });

        this.gui = new dat.GUI();

        this.scene = new Scene3D();
        this.scene.addComponent(Stats);

        // Initialize the physics debug feature after the engine starts; a graphic3D object must be passed to the debugger
        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, engine.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;
        dl.enableCSM = true;
        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();

        engine.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(this.engine.context3D);
        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(this.engine.context3D);
        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(this.engine.context3D);
        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(this.engine.context3D);
        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(this.engine.context3D);
        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(this.engine.context3D);
        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 = this.engine.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();

Released under the MIT License