Skip to content

碰撞体

碰撞体形状 Collision Shape 定义了刚体响应碰撞的实际物理形状,物理系统可以通过 Shape 判定两个物体是否相交,从而产生碰撞效果。

TIP

@orillusion/physics@0.3 开始,我们推荐直接使用 Ammo 原生 Shape 管理碰撞体

碰撞体工具

为了简化碰撞体的创建过程,CollisionShapeUtil 工具类提供了便捷的物理形状构建方法,涵盖了多种常见的碰撞体形状。该工具类将复杂的物理形状生成过程封装为一系列易于调用的方法,使开发者能够快速高效地为模型对象生成适配的碰撞体。

内置形状

目前 CollisionShapeUtil 提供的物理形状创建方法如下表所示:

函数名称返回类型描述
createStaticPlaneShapeAmmo.btStaticPlaneShape创建一个静态平面碰撞形状,适用于无限大且静止的平面,如地面或墙壁
createBoxShapeAmmo.btBoxShape盒型碰撞形状
createSphereShapeAmmo.btSphereShape球型碰撞形状
createCapsuleShapeAmmo.btCapsuleShape胶囊型碰撞形状
createCylinderShapeAmmo.btCylinderShape圆柱型碰撞形状
createConeShapeAmmo.btConeShape圆锥形碰撞形状
createCompoundShapeAmmo.btCompoundShape复合形状,将多个子形状组合成一个复杂的碰撞体
createHeightfieldTerrainShapeAmmo.btHeightfieldTerrainShape高度场形状,适用于地形的碰撞检测
createConvexHullShapeAmmo.btConvexHullShape凸包形状,适用于动态物体的快速碰撞检测
createConvexTriangleMeshShapeAmmo.btConvexTriangleMeshShape凸包网格形状,适用于需要复杂几何表示的动态物体
createBvhTriangleMeshShapeAmmo.btBvhTriangleMeshShape边界体积层次 BVH 网格形状,适用于需要复杂几何表示的静态物体
createGImpactMeshShapeAmmo.btGImpactMeshShapeGImpact 网格形状,适用于复杂的三角网格碰撞检测,特别是动态物体
createShapeFromObjectAmmo.btCollisionShape创建一个匹配 Object3D 几何体类型的碰撞形状

复杂结构支持

CollisionShapeUtil 提供了两个支持处理嵌套对象的 API。这些 API 可以自动生成适合的碰撞形状或提取几何数据,适用于由引擎创建的复杂结构,无需手动为每个子对象单独配置。

函数名称返回类型描述
createCompoundShapeFromObjectAmmo.btCompoundShape根据传入的 Object3D 及其子对象的几何体类型,自动创建一个复合碰撞体
getAllMeshVerticesAndIndices{ vertices:Float32Array; indices: Uint16Array; }返回 Object3D 及其子对象的所有顶点和索引数据,经过世界变换矩阵转换后,可用于创建高精度的网格碰撞体

基本使用

创建碰撞体的过程已被简化,在大多数情况下,只需传入 Object3D,即可生成碰撞体。以下是使用 CollisionShapeUtil 创建基础碰撞形状的示例代码:

ts
import { Object3D, MeshRenderer, CylinderGeometry, LitMaterial } from '@orillusion/core';
import { CollisionShapeUtil } from '@orillusion/physics';

// 创建一个圆锥体
const coneObject = new Object3D();
let mr = coneObject.addComponent(MeshRenderer);
mr.geometry = new CylinderGeometry(0.01, 1, 5);
mr.material = new LitMaterial();

// 对于简单类型的几何体,如盒型、球型、圆锥、圆柱可以使用通用的方法创建碰撞体
let coneShape1 = CollisionShapeUtil.createShapeFromObject(coneObject);
// 或者通过计算局部包围盒创建圆锥形状
let coneShape2 = CollisionShapeUtil.createConeShape(coneObject);
// 或者指定形状尺寸
let coneShape3 = CollisionShapeUtil.createConeShape(null, 1, 5);

同时,针对复杂类型的碰撞形状,构建流程也得到了简化。为了满足自定义需求,开发者可以传入 verticesindices 以生成自定义的碰撞形状:

ts
const object = await Engine3D.res.loadGltf('model.glb');

// 创建一个BVH网格形状,使用模型自身的顶点和索引
let bvhMeshShape = CollisionShapeUtil.createBvhTriangleMeshShape(object);

// 或手动传入的顶点和索引
const vertices = [...]
const indices = [...]
const vertices = new Float32Array(vertices);
const indices = new Uint16Array(data.indices);
let bvhMeshShape = CollisionShapeUtil.createBvhTriangleMeshShape(object, vertices, indices);

此外,基于 TerrainGeometryPlaneGeometry,可以创建适用于模拟地形的高度场碰撞形状:

ts
import { TerrainGeometry } from '@orillusion/geometry';

// Load textures and create terrain geometry
let heightTexture = await Engine3D.res.loadTexture('height.png');
let terrainGeometry = new TerrainGeometry(100, 100, 60, 60);
terrainGeometry.setHeight(heightTexture as BitmapTexture2D, 50);

const terrain = new Object3D();
let mr = terrain.addComponent(MeshRenderer);
mr.geometry = terrainGeometry;
mr.material = new LitMaterial();

// 创建地形碰撞体
let terrainShape = CollisionShapeUtil.createHeightfieldTerrainShape(terrain);

通过上述操作,我们可以创建多种碰撞体以适应不同的物理需求。然而,为了实现全面的物理模拟,单独的碰撞体是不够的。要获得真实的物理效果,还需要与 刚体 结合使用,从而实现完整的物理交互和模拟。

示例

不同的碰撞体形状适用于各类物理场景。以下示例展示了如何使用 CollisionShapeUtil 为多种几何形状生成对应的碰撞体,并结合 刚体 在物理系统中应用这些形状。

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

<
ts
import { Engine3D, LitMaterial, MeshRenderer, BoxGeometry, Object3D, Scene3D, View3D, Object3DUtil, Vector3, AtmosphericComponent, DirectLight, SphereGeometry, CameraUtil, HoverCameraController, BitmapTexture2D, Color, CylinderGeometry, TorusGeometry, ComponentBase } from "@orillusion/core";
import { TerrainGeometry } from "@orillusion/geometry";
import { Ammo, CollisionShapeUtil, Physics, Rigidbody } from "@orillusion/physics";

class Sample_MultipleShapes {
    scene: Scene3D;
    terrain: Object3D;
    gui: dat.GUI;

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

        // shadow settings
        Engine3D.setting.shadow.shadowBias = 0.01;
        Engine3D.setting.shadow.shadowSize = 1024 * 4;
        Engine3D.setting.shadow.csmMargin = 0.1;
        Engine3D.setting.shadow.csmScatteringExp = 0.8;
        Engine3D.setting.shadow.csmAreaScale = 0.1;
        Engine3D.setting.shadow.updateFrameRate = 1;

        this.scene = new Scene3D();

        // Setup camera
        let camera = CameraUtil.createCamera3DObject(this.scene);
        camera.perspective(60, Engine3D.aspect, 0.1, 800.0);
        camera.enableCSM = true;

        let hoverCtrl = camera.object3D.addComponent(HoverCameraController);
        hoverCtrl.setCamera(0, -25, 100);
        hoverCtrl.dragSmooth = 4;

        // Create directional light
        let lightObj3D = new Object3D();
        lightObj3D.localRotation = new Vector3(-35, -143, 92);

        let light = lightObj3D.addComponent(DirectLight);
        light.lightColor = Color.COLOR_WHITE;
        light.castShadow = true;
        light.intensity = 2.2;
        this.scene.addChild(light.object3D);

        // init sky
        let atmosphericSky = this.scene.addComponent(AtmosphericComponent);
        atmosphericSky.sunY = 0.6;

        // Setup view
        let view = new View3D();
        view.camera = camera;
        view.scene = this.scene;

        Engine3D.startRenderView(view);

        // init terrain and create static planes
        await this.initTerrain();
        this.createStaticPlanes();

        this.scene.addComponent(BoxGenerator);
    }

    async initTerrain() {
        // Load textures
        let bitmapTexture = await Engine3D.res.loadTexture('https://cdn.orillusion.com/terrain/test01/bitmap.png');
        let heightTexture = await Engine3D.res.loadTexture('https://cdn.orillusion.com/terrain/test01/height.png');

        const width = 100;
        const height = 100;
        const terrainMaxHeight = 60;
        const segment = 60

        // Create terrain geometry
        let terrainGeometry = new TerrainGeometry(width, height, segment, segment);
        terrainGeometry.setHeight(heightTexture as BitmapTexture2D, terrainMaxHeight);

        let terrain = new Object3D();
        let mr = terrain.addComponent(MeshRenderer);
        mr.geometry = terrainGeometry;

        let mat = new LitMaterial();
        mat.baseMap = bitmapTexture;
        mat.metallic = 0;
        mat.roughness = 1.3;
        mr.material = mat;

        this.terrain = terrain;
        this.scene.addChild(terrain);

        // Add rigidbody to terrain
        let terrainRb = terrain.addComponent(Rigidbody);
        terrainRb.shape = Rigidbody.collisionShape.createHeightfieldTerrainShape(terrain);
        terrainRb.mass = 0; // Static rigidbody
        terrainRb.margin = 0.05;
        terrainRb.isDisableDebugVisible = true;
        terrainRb.friction = 1;
    }

    // Create static planes for boundaries
    createStaticPlanes() {
        // Create bottom static plane
        let staticFloorBottom = Object3DUtil.GetPlane(Engine3D.res.whiteTexture);
        staticFloorBottom.y = -500;
        staticFloorBottom.transform.enable = false;
        this.scene.addChild(staticFloorBottom);

        let bottomRb = staticFloorBottom.addComponent(Rigidbody);
        bottomRb.shape = CollisionShapeUtil.createStaticPlaneShape();
        bottomRb.mass = 0;

        // Create top static plane
        let staticFloorTop = Object3DUtil.GetPlane(Engine3D.res.whiteTexture);
        staticFloorTop.y = 100;
        staticFloorTop.transform.enable = false;
        this.scene.addChild(staticFloorTop);

        let topRb = staticFloorTop.addComponent(Rigidbody);
        topRb.shape = CollisionShapeUtil.createStaticPlaneShape(Vector3.DOWN);
        topRb.mass = 0;
    }
}

class BoxGenerator extends ComponentBase {
    private lastTime: number = performance.now(); // Save last time

    public container: Object3D;
    public interval: number = 1000; // Interval for adding shapes
    public totalShapes: number = 30; // Maximum number of shapes

    async start() {
        this.container = new Object3D();
        this.object3D.addChild(this.container);
    }

    // Update loop
    public onUpdate(): void {
        let now: number = performance.now();
        if (now - this.lastTime > this.interval) {
            if (this.container.numChildren >= this.totalShapes) {
                let index = Math.floor(now / this.interval) % this.totalShapes;
                let shapeObject = this.container.getChildByIndex(index) as Object3D;
                shapeObject.localPosition.set(Math.random() * 60 - 60 / 2, 40, Math.random() * 60 - 60 / 2);
                shapeObject.getComponent(Rigidbody).updateTransform(shapeObject.localPosition, null, true);
            } else {
                this.addRandomShape();
            }
            this.lastTime = now; // Save current time
        }
    }

    private addRandomShape(): void {
        const shapeObject = new Object3D();
        let mr = shapeObject.addComponent(MeshRenderer);
        let mat = new LitMaterial();
        mat.baseColor = Color.random();

        let size = 1 + Math.random() / 2;
        let height = 1 + Math.random() * (3 - 1);
        let radius = 0.5 + Math.random() / 2;
        const segments = 32;

        let shape: Ammo.btCollisionShape;
        let shapeType = Math.floor(Math.random() * 6); // Six basic shapes
        switch (shapeType) {
            case 0: // Box shape
                mr.geometry = new BoxGeometry(size, size, size);
                mr.material = mat;
                shape = CollisionShapeUtil.createBoxShape(shapeObject);
                break;
            case 1: // Sphere shape
                mr.geometry = new SphereGeometry(radius, segments, segments);
                mr.material = mat;
                shape = CollisionShapeUtil.createSphereShape(shapeObject);
                break;
            case 2: // Cylinder shape
                mr.geometry = new CylinderGeometry(radius, radius, height, segments, segments);
                mr.materials = [mat, mat, mat];
                shape = CollisionShapeUtil.createCylinderShape(shapeObject);
                break;
            case 3: // Cone shape
                mr.geometry = new CylinderGeometry(0.01, radius, height, segments, segments);
                mr.materials = [mat, mat, mat];
                shape = CollisionShapeUtil.createConeShape(shapeObject);
                break;
            case 4: // Capsule shape
                mr.geometry = new CylinderGeometry(radius, radius, height, segments, segments);
                mr.material = mat;
                const { r, g, b } = mat.baseColor;
                let topSphere = Object3DUtil.GetSingleSphere(radius, r, g, b);
                topSphere.y = height / 2;
                let bottomSphere = topSphere.clone();
                bottomSphere.y = -height / 2;
                shapeObject.addChild(topSphere);
                shapeObject.addChild(bottomSphere);
                shape = CollisionShapeUtil.createCapsuleShape(shapeObject);
                break;
            case 5: // Torus shape (convex hull shape)
                mr.geometry = new TorusGeometry(radius, size / 5, segments / 2, segments / 2);
                mr.material = mat;
                shape = CollisionShapeUtil.createConvexHullShape(shapeObject);
                break;
            default:
                break;
        }

        const posRange = 60;
        shapeObject.x = Math.random() * posRange - posRange / 2;
        shapeObject.y = 40;
        shapeObject.z = Math.random() * posRange - posRange / 2;

        shapeObject.localRotation = new Vector3(Math.random() * 360, Math.random() * 360, Math.random() * 360);
        this.container.addChild(shapeObject);

        // Add rigidbody to shape
        let rigidbody = shapeObject.addComponent(Rigidbody);
        rigidbody.shape = shape;
        rigidbody.mass = Math.random() * 10 + 0.1;
        rigidbody.rollingFriction = 0.5;
        rigidbody.damping = [0.1, 0.1];

        // Enable continuous collision detection (CCD)
        const maxDimension = Math.max(size, height, radius);
        const ccdMotionThreshold = maxDimension * 0.1; // Set motion threshold to 10% of max dimension
        const ccdSweptSphereRadius = maxDimension * 0.05; // Set swept sphere radius to 5% of max dimension
        rigidbody.ccdSettings = [ccdMotionThreshold, ccdSweptSphereRadius];
    }
}

new Sample_MultipleShapes().run();

更多物理示例