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:
- ClothSoftbody - Cloth softbody component
- RopeSoftbody - Rope softbody component
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:
Physics.init({useSoftBody: true});Basic Features
Softbody components provide some common APIs, as shown in the table below:
| Property | Type | Description |
|---|---|---|
| btSoftBody | Ammo.btSoftBody | Gets the native softbody object of Ammo.js |
| mass | number | The total mass of the softbody. Default value is 1 |
| margin | number | The collision margin. Default value is 0.15 |
| group | number | The collision group. Default value is 1 |
| mask | number | The collision mask. Default value is -1 |
| influence | number | The influence of the anchor. Default value is 1 |
| disableCollision | boolean | Whether to disable collision with the anchored rigidbody. Default value is false |
| activationState | ActivationState | Sets the activation state of the softbody |
| Method | Description |
|---|---|
| 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:
| Property | Type | Description |
|---|---|---|
| clothCorners | Vector3[] | Defines the positions of the four corners of the cloth; by default, the corners are calculated from the plane's normal vector |
| fixNodeIndices | CornerType[] | number[] | The node indices or corner types to fix on the cloth |
| anchorIndices | CornerType[] | number[] | The anchor node indices or corner types of the cloth |
| anchorPosition | Vector3 | The position of the cloth relative to the rigidbody after anchoring to it |
| anchorRotation | Vector3 | The rotation of the cloth relative to the rigidbody after anchoring to it |
| anchorRigidbody | Rigidbody | The rigidbody required when adding anchors |
Basic Usage
Add a ClothSoftbody component to an object:
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:
clothSoftbody.fixNodeIndices = ['leftTop', 'rightTop'];After the cloth is initialized, you can continue to fix nodes:
clothSoftbody.applyFixedNodes(['leftBottom', 'rightBottom']);Set anchor nodes through the anchorIndices property, and specify the attached rigidbody:
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 anchorRotationTIP
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:
clothSoftbody.clearAnchors();Example
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:
| API | Type | Description |
|---|---|---|
| fixeds | number | Rope fixing option; 0: neither end fixed, 1: start point fixed, 2: end point fixed, 3: both ends fixed |
| fixNodeIndices | number[] | Fixed node indices; has the same effect as the fixeds property, but allows more flexible control of any node |
| elasticity | number | Rope elasticity; the larger the value, the lower the elasticity. Default value is 0.5 |
| anchorRigidbodyHead | Rigidbody | The rigidbody anchored at the start point of the rope |
| anchorRigidbodyTail | Rigidbody | The rigidbody anchored at the end point of the rope |
| anchorOffsetHead | Vector3 | The offset of the anchor at the start point |
| anchorOffsetTail | Vector3 | The offset of the anchor at the end point |
| setElasticity() | void | Sets the rope elasticity |
| buildRopeGeometry() | GeometryBase | A static method for building the rope (line) geometry |
Basic Usage
Add a RopeSoftbody component to an object:
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:
ropeSoftbody.fixeds = 1; // Fix the start point of the ropeConnect a rigidbody at the end:
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 positionExample
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:
// 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.

