Skip to content

软体

软体是一类在受到外力时会产生明显形变的物体,与刚体不同,软体能够模拟如布料、橡胶等柔性物体的动态行为。尽管软体模拟更为复杂,但它能为物理引擎中的物体提供更逼真的动画效果,特别是在涉及到柔性材质时。

组件介绍

软体模拟是物理引擎的高级功能,当前系统针对不同类型的软体提供了相应的组件:

软体与模型对象的同步机制

当为模型对象添加软体组件后,物理引擎会在每一个物理模拟步长中计算软体的形变和运动状态。这个过程涵盖了柔性材料在外力、重力等物理作用力下的响应。根据这些计算,物理引擎会更新软体的顶点、法线等数据。软体组件会在其更新函数中将这些变化同步到模型对象的几何体上,使其在场景中展现逼真的物理效果。请注意,添加软体组件后,几何体的变形将由物理引擎自动处理,通常无需手动调整。若修改模型对象的变换可能会导致与物理模拟的不一致。

在使用软体组件前,需要确保物理系统已启用软体模拟:

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

基本功能

软体组件提供了一些通用的 API,如下表所示:

属性类型描述
btSoftBodyAmmo.btSoftBody获取 Ammo.js 的原生软体对象
massnumber软体的总质量,默认值为 1
marginnumber碰撞边距,默认值为 0.15
groupnumber碰撞组,默认值为 1
masknumber碰撞掩码,默认值为 -1
influencenumber锚点的影响力,默认值为 1
disableCollisionboolean是否禁用与锚定刚体之间的碰撞,默认值为 false
activationStateActivationState设置软体的激活状态
方法描述
wait()异步获取完全初始化的原生软体实例
applyFixedNodes()固定软体节点
clearAnchors()清除所有锚点
appendAnchor()锚定软体节点到指定刚体(原生方法的封装,不考虑变换)

布料软体 ClothSoftbody

布料软体组件 ClothSoftbody 主要用于模拟布料的柔性动态行为,支持的 API 如下:

属性类型描述
clothCornersVector3[]定义布料四个角的位置,默认以平面法向量计算各角
fixNodeIndicesCornerType[] | number[]固定布料的节点索引或角类型
anchorIndicesCornerType[] | number[]布料的锚点节点索引或角类型
anchorPositionVector3布料锚定刚体后相对刚体的位置
anchorRotationVector3布料锚定刚体后相对刚体的旋转
anchorRigidbodyRigidbody添加锚点时需要的刚体

基本用法

为对象添加 ClothSoftbody 组件:

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

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

// 设置平面的法向量,它决定了布料四个角的位置
mr.geometry = new PlaneGeometry(5, 5, 10, 10, Vector3.Z_AXIS);
mr.material = new LitMaterial();

// 添加布料组件
let clothSoftbody = object.addComponent(ClothSoftbody);

TIP

请注意:ClothSoftbody 组件仅支持 PlaneGeometry 类型的几何体。

通过设置 fixNodeIndices 属性,可以固定特定的布料节点:

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

在布料初始化完毕后,可以继续固定节点:

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

通过 anchorIndices 属性设置锚定节点,并指定附加的刚体:

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

// 附加到刚体后软体的中心点与旋转将会与刚体变换一致。
clothSoftbody.anchorPosition.set(0, 5, 0); // 通过 anchorPosition 设置相对位置
clothSoftbody.anchorRotation.set(0, 90, 0); // 通过 anchorRotation 设置相对旋转

TIP

锚点设置时会自动将软体附加到刚体,并可以设置 influencedisableCollision 等属性。

如果需要移除所有锚点,使软体从锚定的刚体上脱落,可以调用 clearAnchors() 方法:

ts
clothSoftbody.clearAnchors();

示例

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

<
ts
import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, webGPUContext, HoverCameraController, Object3D, DirectLight, LitMaterial, MeshRenderer, PlaneGeometry, Vector3, Object3DUtil } from "@orillusion/core";
import { Graphic3D } from "@orillusion/graphic";
import { Physics, Rigidbody, ClothSoftbody } from "@orillusion/physics";
import dat from "dat.gui";

class Sample_Cloth {
    async run() {
        await Physics.init({ useSoftBody: true, useDrag: true });
        await Engine3D.init({ renderLoop: () => Physics.update() });
        let view = new View3D();
        view.scene = new Scene3D();
        let sky = view.scene.addComponent(AtmosphericComponent);

        view.camera = CameraUtil.createCamera3DObject(view.scene);
        view.camera.perspective(60, webGPUContext.aspect, 1, 1000.0);
        view.camera.object3D.addComponent(HoverCameraController).setCamera(0, -30, 20, new Vector3(0, 3, 0));

        let lightObj3D = new Object3D();
        let sunLight = lightObj3D.addComponent(DirectLight);
        sunLight.intensity = 2;
        sunLight.castShadow = true;
        lightObj3D.rotationX = 24;
        lightObj3D.rotationY = -151;
        view.scene.addChild(lightObj3D);
        sky.relativeTransform = lightObj3D.transform;

        Engine3D.startRenderView(view);

        this.createScene(view.scene);
    }

    createScene(scene: Scene3D) {
        // create the ground and add a rigid body
        let ground = Object3DUtil.GetSingleCube(30, 0, 30, 1, 1, 1);
        scene.addChild(ground);

        let rigidbody = ground.addComponent(Rigidbody);
        rigidbody.mass = 0;
        rigidbody.shape = Rigidbody.collisionShape.createStaticPlaneShape();

        // create shelves, cloth, and ball
        this.createShelves(scene);
        this.createCloth(scene);
        const ballRb = this.createBall(scene);

        this.debug(scene, ballRb);
    }


    createShelves(scene: Scene3D) {
        let shelf1 = Object3DUtil.GetSingleCube(0.5, 5, 0.5, 1, 1, 1); // left top
        let shelf2 = shelf1.clone(); // right top
        let shelf3 = shelf1.clone(); // left bottom
        let shelf4 = shelf1.clone(); // right bottom
        shelf1.localPosition = new Vector3(-4, 2.5, -4);
        shelf2.localPosition = new Vector3(4, 2.5, -4);
        shelf3.localPosition = new Vector3(-4, 2.5, 4);
        shelf4.localPosition = new Vector3(4, 2.5, 4);
        scene.addChild(shelf1);
        scene.addChild(shelf2);
        scene.addChild(shelf3);
        scene.addChild(shelf4);
    }

    createCloth(scene: Scene3D) {
        const cloth = new Object3D();
        let meshRenderer = cloth.addComponent(MeshRenderer);
        meshRenderer.geometry = new PlaneGeometry(8, 8, 20, 20, Vector3.UP);
        let material = new LitMaterial();
        material.baseMap = Engine3D.res.redTexture;
        material.cullMode = 'none';
        meshRenderer.material = material;

        cloth.y = 5;
        scene.addChild(cloth);

        // add cloth softbody component
        let softBody = cloth.addComponent(ClothSoftbody);
        softBody.mass = 1;
        softBody.margin = 0.2;
        softBody.fixNodeIndices = ['leftTop', 'rightTop', 'leftBottom', 'rightBottom'];
    }

    createBall(scene: Scene3D) {
        const ball = Object3DUtil.GetSingleSphere(1, 0.5, 0.2, 0.8);
        ball.y = 10;
        scene.addChild(ball);

        let rigidbody = ball.addComponent(Rigidbody);
        rigidbody.mass = 1.6;
        rigidbody.shape = Rigidbody.collisionShape.createShapeFromObject(ball);

        return rigidbody;
    }

    debug(scene: Scene3D, ballRb: Rigidbody) {
        const graphic3D = new Graphic3D();
        scene.addChild(graphic3D);
        Physics.initDebugDrawer(graphic3D);

        let gui = new dat.GUI();
        let f = gui.addFolder('PhysicsDebug');
        f.add(Physics.debugDrawer, 'enable');
        f.add(Physics.debugDrawer, 'debugMode', Physics.debugDrawer.debugModeList);
        gui.add({ ResetBall: () => ballRb.updateTransform(new Vector3(0, 10, 0), null, true) }, 'ResetBall');
    }

}

new Sample_Cloth().run();

绳索软体 RopeSoftbody

绳索软体组件 RopeSoftbody 主要用于模拟绳索的柔性动态行为,支持的 API 如下:

API类型描述
fixedsnumber绳索固定选项,0:两端不固定,1:起点固定,2:终点固定,3:两端固定
fixNodeIndicesnumber[]固定节点索引,与 fixeds 属性作用相同,但可以更自由的控制任意节点
elasticitynumber绳索弹性,值越大弹性越低,默认值为 0.5
anchorRigidbodyHeadRigidbody绳索起点处锚定的刚体
anchorRigidbodyTailRigidbody绳索终点处锚定的刚体
anchorOffsetHeadVector3锚点的起点偏移量
anchorOffsetTailVector3锚点的终点偏移量
setElasticity()void设置绳索弹性
buildRopeGeometry()GeometryBase构建绳索(线条)几何体的静态方法

基本用法

为对象添加 RopeSoftbody 组件:

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);
// 设置绳索几何体
mr.geometry = RopeSoftbody.buildRopeGeometry(segmentCount, startPos, endPos);
mr.material = new LitMaterial();
mr.material.topology = 'line-list'; // 需要设置为 line 渲染模式

// 添加绳索组件
let ropeSoftbody = object.addComponent(RopeSoftbody);

TIP

RopeSoftbody 组件仅支持 line 类型的几何体。为方便使用,组件内提供了 buildRopeGeometry() 静态方法。
注意添加材质时需要将拓扑结构 topology 设置为 'line-list'

固定绳索节点:

ts
ropeSoftbody.fixeds = 1; // 固定绳索起点

末端连接刚体:

ts
ropeSoftbody.anchorRigidbodyTail = rigidbody;
ropeSoftbody.anchorOffsetTail.set(0, 1, 0); // 附加到刚体后绳索的终点将会与刚体位置一致,设置 anchorOffsetTail 以调整相对位置

示例

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

<
ts
import { Engine3D, View3D, Scene3D, CameraUtil, AtmosphericComponent, webGPUContext, HoverCameraController, Object3D, DirectLight, LitMaterial, MeshRenderer, Vector3, Object3DUtil, Color, } from "@orillusion/core";
import { Graphic3D } from "@orillusion/graphic";
import { Physics, Rigidbody, RopeSoftbody } from "@orillusion/physics";
import dat from "dat.gui";

class Sample_Rope {
    async run() {
        await Physics.init({ useSoftBody: true, useDrag: true });
        await Engine3D.init({ renderLoop: () => Physics.update() });
        let view = new View3D();
        view.scene = new Scene3D();
        let sky = view.scene.addComponent(AtmosphericComponent);

        view.camera = CameraUtil.createCamera3DObject(view.scene);
        view.camera.perspective(60, webGPUContext.aspect, 1, 1000.0);
        view.camera.object3D.addComponent(HoverCameraController).setCamera(0, -30, 20, new Vector3(0, 3, 0));

        let lightObj3D = new Object3D();
        let sunLight = lightObj3D.addComponent(DirectLight);
        sunLight.intensity = 2;
        sunLight.castShadow = true;
        lightObj3D.rotationX = 24;
        lightObj3D.rotationY = -151;
        view.scene.addChild(lightObj3D);
        sky.relativeTransform = lightObj3D.transform;

        Engine3D.startRenderView(view);

        this.createScene(view.scene);
    }

    createScene(scene: Scene3D) {
        // create the ground and add a rigid body
        let ground = Object3DUtil.GetSingleCube(30, 0, 30, 1, 1, 1);
        scene.addChild(ground);

        let rigidbody = ground.addComponent(Rigidbody);
        rigidbody.mass = 0;
        rigidbody.shape = Rigidbody.collisionShape.createStaticPlaneShape();

        // create shelves
        this.createShelves(scene);

        // create balls and ropes
        for (let i = 0; i < 7; i++) {
            let pos = new Vector3(6 - i * 2, 8, 0);

            // check if this is the last ball (tail)
            let ballRb = this.createBall(scene, pos, i === 6);

            // create the rope connected to the ball
            this.createRope(scene, pos, ballRb);
        }

        this.debug(scene);
    }


    createShelves(scene: Scene3D) {
        let shelf1 = Object3DUtil.GetSingleCube(0.2, 8, 0.2, 1, 1, 1); // left 
        let shelf2 = Object3DUtil.GetSingleCube(0.2, 8, 0.2, 1, 1, 1); // right 
        let shelf3 = Object3DUtil.GetSingleCube(20.2, 0.2, 0.2, 1, 1, 1); // top
        shelf1.localPosition = new Vector3(-10, 4, 0);
        shelf2.localPosition = new Vector3(10, 4, 0);
        shelf3.localPosition = new Vector3(0, 8, 0);
        scene.addChild(shelf1);
        scene.addChild(shelf2);
        scene.addChild(shelf3);
    }

    createBall(scene: Scene3D, pos: Vector3, isTail: boolean) {
        const ball = Object3DUtil.GetSingleSphere(0.82, 1, 1, 1);
        ball.x = pos.x - (isTail ? 3 : 0);
        ball.y = pos.y / 3 + (isTail ? 1.16 : 0);
        scene.addChild(ball);

        let rigidbody = ball.addComponent(Rigidbody);
        rigidbody.shape = Rigidbody.collisionShape.createShapeFromObject(ball);
        rigidbody.mass = 1.1;
        rigidbody.restitution = 1.13;

        // ball collision event to change color
        let ballMaterial = ball.getComponent(MeshRenderer).material as LitMaterial;

        let timer: number | null = null;
        rigidbody.collisionEvent = (contactPoint, selfBody, otherBody) => {
            if (timer !== null) clearTimeout(timer);
            else ballMaterial.baseColor = new Color(Color.SALMON);

            timer = setTimeout(() => {
                ballMaterial.baseColor = Color.COLOR_WHITE;
                timer = null;
            }, 100);
        }

        return rigidbody;
    }

    createRope(scene: Scene3D, pos: Vector3, tailRb: Rigidbody) {
        let ropeObj = new Object3D();
        let mr = ropeObj.addComponent(MeshRenderer);
        mr.material = new LitMaterial();
        mr.material.topology = 'line-list';
        mr.geometry = RopeSoftbody.buildRopeGeometry(10, pos, new Vector3(0, 0, 0));
        scene.addChild(ropeObj);

        // add rope softbody component
        let ropeSoftbody = ropeObj.addComponent(RopeSoftbody);
        ropeSoftbody.fixeds = 1; // fixed top
        ropeSoftbody.mass = 1.0;
        ropeSoftbody.elasticity = 1;
        ropeSoftbody.anchorRigidbodyTail = tailRb;
        ropeSoftbody.anchorOffsetTail.set(0, 0.82, 0); // 0.82 is ball radius
    }

    debug(scene: Scene3D) {
        const graphic3D = new Graphic3D();
        scene.addChild(graphic3D);
        Physics.initDebugDrawer(graphic3D);

        let gui = new dat.GUI();
        let f = gui.addFolder('PhysicsDebug');
        f.add(Physics.debugDrawer, 'enable');
        f.add(Physics.debugDrawer, 'debugMode', Physics.debugDrawer.debugModeList);
    }

}

new Sample_Rope().run();

软体配置

在软体创建过程中,内部配置了一些基础的参数来控制软体的行为,包括位置迭代、阻尼系数、刚性系数等。开发者可以通过操作原生的 Ammo.js 软体进行自定义配置,确保软体具有理想的物理效果:

ts
// 异步等待软体初始化完成
let bt = await clothSoftbody.wait()
// native softbody API
let sbConfig = bt.get_m_cfg();
sbConfig.set_kDF(0.2); // 设置动力学系数
sbConfig.set_kDP(0.01); // 设置阻尼系数
sbConfig.set_kLF(0.02); // 设置升力系数
sbConfig.set_kDG(0.001); // 设置阻力系数
...

TIP

软体组件的属性仅在初始化时设置有效。