Skip to content

Collision Shape

The collision shape Collision Shape defines the actual physical shape that a rigidbody uses to respond to collisions. The physics system uses the Shape to determine whether two objects intersect, thereby producing collision effects.

TIP

Starting from @orillusion/physics@0.3, we recommend directly using the native Ammo Shape to manage collision shapes.

Collision Shape Utility

To simplify the process of creating collision shapes, the CollisionShapeUtil utility class provides convenient methods for building physical shapes, covering a variety of common collision shapes. This utility class encapsulates the complex process of generating physical shapes into a series of easy-to-call methods, enabling developers to quickly and efficiently generate matching collision shapes for model objects.

Built-in Shapes

Currently, the physical shape creation methods provided by CollisionShapeUtil are shown in the table below:

Function NameReturn TypeDescription
createStaticPlaneShapeAmmo.btStaticPlaneShapeCreates a static plane collision shape, suitable for an infinite and stationary plane, such as a ground or wall
createBoxShapeAmmo.btBoxShapeBox collision shape
createSphereShapeAmmo.btSphereShapeSphere collision shape
createCapsuleShapeAmmo.btCapsuleShapeCapsule collision shape
createCylinderShapeAmmo.btCylinderShapeCylinder collision shape
createConeShapeAmmo.btConeShapeCone collision shape
createCompoundShapeAmmo.btCompoundShapeCompound shape, combining multiple child shapes into one complex collision shape
createHeightfieldTerrainShapeAmmo.btHeightfieldTerrainShapeHeightfield shape, suitable for terrain collision detection
createConvexHullShapeAmmo.btConvexHullShapeConvex hull shape, suitable for fast collision detection of dynamic objects
createConvexTriangleMeshShapeAmmo.btConvexTriangleMeshShapeConvex triangle mesh shape, suitable for dynamic objects requiring complex geometry representation
createBvhTriangleMeshShapeAmmo.btBvhTriangleMeshShapeBounding Volume Hierarchy BVH mesh shape, suitable for static objects requiring complex geometry representation
createGImpactMeshShapeAmmo.btGImpactMeshShapeGImpact mesh shape, suitable for complex triangle mesh collision detection, especially for dynamic objects
createShapeFromObjectAmmo.btCollisionShapeCreates a collision shape matching the geometry type of an Object3D

Complex Structure Support

CollisionShapeUtil provides two APIs that support handling nested objects. These APIs can automatically generate appropriate collision shapes or extract geometry data, suitable for complex structures created by the engine, without the need to manually configure each child object individually.

Function NameReturn TypeDescription
createCompoundShapeFromObjectAmmo.btCompoundShapeAutomatically creates a compound collision shape based on the geometry types of the passed-in Object3D and its child objects
getAllMeshVerticesAndIndices{ vertices:Float32Array; indices: Uint16Array; }Returns all vertex and index data of the Object3D and its child objects, transformed by the world transformation matrix, which can be used to create high-precision mesh collision shapes

Basic Usage

The process of creating collision shapes has been simplified. In most cases, you only need to pass in an Object3D to generate a collision shape. The following is example code for creating basic collision shapes using CollisionShapeUtil:

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

// Create a cone
const coneObject = new Object3D();
let mr = coneObject.addComponent(MeshRenderer);
mr.geometry = new CylinderGeometry(0.01, 1, 5);
mr.material = new LitMaterial();

// For simple geometry types such as box, sphere, cone, and cylinder, the generic method can be used to create the collision shape
let coneShape1 = CollisionShapeUtil.createShapeFromObject(coneObject);
// Or create the cone shape by computing the local bounding box
let coneShape2 = CollisionShapeUtil.createConeShape(coneObject);
// Or specify the shape dimensions
let coneShape3 = CollisionShapeUtil.createConeShape(null, 1, 5);

Meanwhile, the construction process for complex collision shapes has also been simplified. To meet custom requirements, developers can pass in vertices and indices to generate custom collision shapes:

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

// Create a BVH mesh shape using the model's own vertices and indices
let bvhMeshShape = CollisionShapeUtil.createBvhTriangleMeshShape(object);

// Or manually pass in vertices and indices
const vertices = [...]
const indices = [...]
const vertices = new Float32Array(vertices);
const indices = new Uint16Array(data.indices);
let bvhMeshShape = CollisionShapeUtil.createBvhTriangleMeshShape(object, vertices, indices);

In addition, based on TerrainGeometry or PlaneGeometry, you can create a heightfield collision shape suitable for simulating terrain:

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

// Load textures and create terrain geometry
let heightTexture = await engine.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();

// Create terrain collision shape
let terrainShape = CollisionShapeUtil.createHeightfieldTerrainShape(terrain);

Through the operations above, we can create a variety of collision shapes to suit different physical needs. However, to achieve a complete physics simulation, collision shapes alone are not enough. To obtain realistic physical effects, they must also be used in combination with a Rigidbody, thereby enabling complete physical interaction and simulation.

Example

Different collision shapes are suitable for various physical scenarios. The following example demonstrates how to use CollisionShapeUtil to generate corresponding collision shapes for various geometries, and applies these shapes in the physics system in combination with a Rigidbody.

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

    async run() {
        // init physics and engine
        await Physics.init({ useDrag: true });
        this.engine = await Engine3D.init({
            renderLoop: () => Physics.update(),
            // shadow settings
            setting: {
                shadow: {
                    shadowBias: 0.01,
                    shadowSize: 1024 * 4,
                    csmMargin: 0.1,
                    csmScatteringExp: 0.8,
                    csmAreaScale: 0.1,
                    updateFrameRate: 1
                }
            }
        });

        this.scene = new Scene3D();

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

        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.enableCSM = 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;

        this.engine.startRenderView(view);

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

        this.scene.addComponent(BoxGenerator);
    }

    async initTerrain() {
        // Load textures
        let bitmapTexture = await this.engine.res.loadTexture('https://cdn.orillusion.com/terrain/test01/bitmap.png');
        let heightTexture = await this.engine.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(this.engine.context3D, this.engine.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(this.engine.context3D, this.engine.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();

More physics examples

Released under the MIT License