Skip to content

Softbody

A softbody is a type of object that produces significant deformation when subjected to external forces. Unlike a rigidbody, a softbody can simulate the dynamic behavior of flexible objects such as cloth and rubber. Although softbody simulation is more complex, it can provide more realistic animation effects for objects in the physics engine, especially when flexible materials are involved.

Component Introduction

Softbody simulation is an advanced feature of the physics engine. The current system provides corresponding components for different types of softbodies:

The Synchronization Mechanism Between Softbody and Model Object

After a softbody component is added to a model object, the physics engine calculates the deformation and motion state of the softbody in each physics simulation step. This process covers the response of the flexible material under physical forces such as external forces and gravity. Based on these calculations, the physics engine updates the softbody's vertices, normals, and other data. The softbody component synchronizes these changes to the model object's geometry in its update function, so that it exhibits realistic physical effects in the scene. Please note that after a softbody component is added, the deformation of the geometry will be handled automatically by the physics engine, and usually no manual adjustment is required. Modifying the model object's transform may cause inconsistency with the physics simulation.

Before using softbody components, you need to ensure that softbody simulation is enabled in the physics system:

ts
Physics.init({useSoftBody: true});

Basic Features

Softbody components provide some common APIs, as shown in the table below:

PropertyTypeDescription
btSoftBodyAmmo.btSoftBodyGets the native softbody object of Ammo.js
massnumberThe total mass of the softbody. Default value is 1
marginnumberThe collision margin. Default value is 0.15
groupnumberThe collision group. Default value is 1
masknumberThe collision mask. Default value is -1
influencenumberThe influence of the anchor. Default value is 1
disableCollisionbooleanWhether to disable collision with the anchored rigidbody. Default value is false
activationStateActivationStateSets the activation state of the softbody
MethodDescription
wait()Asynchronously gets the fully initialized native softbody instance
applyFixedNodes()Fixes softbody nodes
clearAnchors()Clears all anchors
appendAnchor()Anchors a softbody node to a specified rigidbody (a wrapper around the native method, does not account for the transform)

Cloth Softbody ClothSoftbody

The cloth softbody component ClothSoftbody is mainly used to simulate the flexible dynamic behavior of cloth. The supported APIs are as follows:

PropertyTypeDescription
clothCornersVector3[]Defines the positions of the four corners of the cloth; by default, the corners are calculated from the plane's normal vector
fixNodeIndicesCornerType[] | number[]The node indices or corner types to fix on the cloth
anchorIndicesCornerType[] | number[]The anchor node indices or corner types of the cloth
anchorPositionVector3The position of the cloth relative to the rigidbody after anchoring to it
anchorRotationVector3The rotation of the cloth relative to the rigidbody after anchoring to it
anchorRigidbodyRigidbodyThe rigidbody required when adding anchors

Basic Usage

Add a ClothSoftbody component to an object:

ts
import { Object3D, MeshRenderer, PlaneGeometry, LitMaterial, Vector3 } from '@orillusion/core'
import { ClothSoftbody } from '@orillusion/physics'

let object = new Object3D();
let mr = object.addComponent(MeshRenderer);

// Set the plane's normal vector, which determines the positions of the four corners of the cloth
mr.geometry = new PlaneGeometry(5, 5, 10, 10, Vector3.Z_AXIS);
mr.material = new LitMaterial();

// Add the cloth component
let clothSoftbody = object.addComponent(ClothSoftbody);

TIP

Please note: the ClothSoftbody component only supports PlaneGeometry type geometry.

By setting the fixNodeIndices property, you can fix specific cloth nodes:

ts
clothSoftbody.fixNodeIndices = ['leftTop', 'rightTop'];

After the cloth is initialized, you can continue to fix nodes:

ts
clothSoftbody.applyFixedNodes(['leftBottom', 'rightBottom']);

Set anchor nodes through the anchorIndices property, and specify the attached rigidbody:

ts
clothSoftbody.anchorIndices = ['top'];
clothSoftbody.anchorRigidbody = rigidbody;

// After attaching to the rigidbody, the center point and rotation of the softbody will be consistent with the rigidbody's transform.
clothSoftbody.anchorPosition.set(0, 5, 0); // Set the relative position through anchorPosition
clothSoftbody.anchorRotation.set(0, 90, 0); // Set the relative rotation through anchorRotation

TIP

When setting anchors, the softbody will be automatically attached to the rigidbody, and properties such as influence and disableCollision can be set.

If you need to remove all anchors so that the softbody detaches from the anchored rigidbody, you can call the clearAnchors() method:

ts
clothSoftbody.clearAnchors();

Example

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

<
ts
import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, 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 {
    engine: Engine3D;
    async run() {
        await Physics.init({ useSoftBody: true });
        this.engine = 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, this.engine.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;
        sunLight.enableCSM = true;
        lightObj3D.rotationX = 24;
        lightObj3D.rotationY = -151;
        view.scene.addChild(lightObj3D);
        sky.relativeTransform = lightObj3D.transform;

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

Rope Softbody RopeSoftbody

The rope softbody component RopeSoftbody is mainly used to simulate the flexible dynamic behavior of ropes. The supported APIs are as follows:

APITypeDescription
fixedsnumberRope fixing option; 0: neither end fixed, 1: start point fixed, 2: end point fixed, 3: both ends fixed
fixNodeIndicesnumber[]Fixed node indices; has the same effect as the fixeds property, but allows more flexible control of any node
elasticitynumberRope elasticity; the larger the value, the lower the elasticity. Default value is 0.5
anchorRigidbodyHeadRigidbodyThe rigidbody anchored at the start point of the rope
anchorRigidbodyTailRigidbodyThe rigidbody anchored at the end point of the rope
anchorOffsetHeadVector3The offset of the anchor at the start point
anchorOffsetTailVector3The offset of the anchor at the end point
setElasticity()voidSets the rope elasticity
buildRopeGeometry()GeometryBaseA static method for building the rope (line) geometry

Basic Usage

Add a RopeSoftbody component to an object:

ts
import { Object3D, MeshRenderer, PlaneGeometry, LitMaterial, Vector3 } from '@orillusion/core'
import { RopeSoftbody } from '@orillusion/physics'

let object = new Object3D();
let mr = object.addComponent(MeshRenderer);
let segmentCount = 10;
let startPos = new Vector3(0, 10, 0);
let endPos = new Vector3(10, 10, 0);
// Set the rope geometry
mr.geometry = RopeSoftbody.buildRopeGeometry(segmentCount, startPos, endPos);
mr.material = new LitMaterial();
mr.material.topology = 'line-list'; // Needs to be set to line rendering mode

// Add the rope component
let ropeSoftbody = object.addComponent(RopeSoftbody);

TIP

The RopeSoftbody component only supports line type geometry. For convenience, the component provides a buildRopeGeometry() static method.
Note that when adding the material, the topology topology must be set to 'line-list'.

Fix rope nodes:

ts
ropeSoftbody.fixeds = 1; // Fix the start point of the rope

Connect a rigidbody at the end:

ts
ropeSoftbody.anchorRigidbodyTail = rigidbody;
ropeSoftbody.anchorOffsetTail.set(0, 1, 0); // After attaching to the rigidbody, the end point of the rope will be consistent with the rigidbody's position; set anchorOffsetTail to adjust the relative position

Example

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

<
ts
import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, 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 });
        let engine = 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, engine.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;
        sunLight.enableCSM = true;
        lightObj3D.rotationX = 24;
        lightObj3D.rotationY = -151;
        view.scene.addChild(lightObj3D);
        sky.relativeTransform = lightObj3D.transform;

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

Softbody Configuration

During softbody creation, some basic parameters are configured internally to control the softbody's behavior, including position iterations, damping coefficient, stiffness coefficient, etc. Developers can perform custom configuration by operating on the native Ammo.js softbody to ensure that the softbody has the desired physical effects:

ts
// Asynchronously wait for the softbody initialization to complete
let bt = await clothSoftbody.wait()
// native softbody API
let sbConfig = bt.get_m_cfg();
sbConfig.set_kDF(0.2); // Set the dynamic friction coefficient
sbConfig.set_kDP(0.01); // Set the damping coefficient
sbConfig.set_kLF(0.02); // Set the lift coefficient
sbConfig.set_kDG(0.001); // Set the drag coefficient
...

TIP

The properties of the softbody component are only effective when set during initialization.

Released under the MIT License