Skip to content

PhysicsCar


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

ts
import { Ammo, Physics, Rigidbody } from '@orillusion/physics';
import { Scene3D, Object3D, Engine3D, ColliderComponent, BoxColliderShape, Vector3, ComponentBase, KeyCode, KeyEvent, Quaternion, BoundUtil, Camera3D, Vector3Ex, MeshRenderer, LitMaterial, Color, BoxGeometry, AtmosphericComponent, CameraUtil, DirectLight, HoverCameraController, KelvinUtil, View3D } from '@orillusion/core';
import * as dat from 'dat.gui';
import { Stats } from '@orillusion/stats';

class Sample_PhysicsCar {
    private scene: Scene3D;
    private car: Object3D;
    private boxes: Object3D[];
    private road: Object3D;
    private camera: Camera3D;
    private controller: fixedCameraController;

    public score = { Score: 0 };
    async run() {
        Engine3D.setting.shadow.autoUpdate = true;
        Engine3D.setting.shadow.updateFrameRate = 1;
        Engine3D.setting.shadow.shadowSize = 2048;
        Engine3D.setting.shadow.shadowBound = 150;

        await Physics.init();
        await Engine3D.init({ renderLoop: () => this.loop() });

        let scene = (this.scene = new Scene3D());
        scene.addComponent(Stats);

        // init sky
        let atmosphericSky: AtmosphericComponent;
        atmosphericSky = scene.addComponent(AtmosphericComponent);

        // init Camera3D
        let camera = (this.camera = CameraUtil.createCamera3DObject(scene));
        camera.perspective(60, Engine3D.aspect, 1, 5000);
        camera.enableCSM = true;
        // init Camera Controller
        let hoverCtrl = camera.object3D.addComponent(HoverCameraController);
        hoverCtrl.setCamera(-30, -15, 50);

        // create direction light
        let lightObj3D = new Object3D();
        lightObj3D.x = 0;
        lightObj3D.y = 30;
        lightObj3D.z = -40;
        lightObj3D.rotationX = 20;
        lightObj3D.rotationY = 160;
        lightObj3D.rotationZ = 0;

        let light = lightObj3D.addComponent(DirectLight);
        light.lightColor = KelvinUtil.color_temperature_to_rgb(5355);
        light.castShadow = true;
        light.intensity = 30;

        scene.addChild(light.object3D);

        // relative light to sky
        atmosphericSky.relativeTransform = light.transform;

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

        await this.initScene(scene);
        Engine3D.startRenderView(view);

        let gui = new dat.GUI();
        let f = gui.addFolder('Orillusion');
        f.add(this.controller, 'enable').name('Fix Camera');
        f.add(this.score, 'Score').listen();
        f.add(
            {
                Reset: () => {
                    location.reload();
                }
            },
            'Reset'
        );
        f.add({ 'Use WASD': 'control car to hit box' }, 'Use WASD');
        f.open();
    }

    async initScene(scene: Scene3D) {
        // load a car model
        {
            this.car = await Engine3D.res.loadGltf('https://cdn.orillusion.com/gltfs/glb/vevhicle.glb');
            this.car.y = 2;
            let collider = this.car.addComponent(ColliderComponent);
            collider.shape = new BoxColliderShape();
            collider.shape.size = BoundUtil.genMeshBounds(this.car).size.clone();
            scene.addChild(this.car);
            // add keyboard controller to the car
            this.car.addComponent(VehicleKeyboardController);
            // fix the camera to the car
            this.controller = this.camera.object3D.addComponent(fixedCameraController);
            this.controller.target = this.car;
            this.controller.distance = 50;
        }
        // add a plane as road
        {
            this.road = new Object3D();
            let mr = this.road.addComponent(MeshRenderer);
            mr.geometry = new BoxGeometry(200, 0.1, 40);
            let mat = (mr.material = new LitMaterial());
            mat.roughness = 1;
            mat.metallic = 0;
            mat.baseMap = await Engine3D.res.loadTexture('data:image/webp;base64,UklGRqAAAABXRUJQVlA4TJMAAAAvV8INER8gEEhxXGstIEmxu7qVgCTF7upWAgFCiv8qJwJXoF8wimQrDiiLCnCG0KzXL4DlRKoj+j8BtSxpW5XY2teypI3/+I//+I//+I//+I//+I//+I//+I//+I//+G8vkFO/Yzuj24P/flBy6nds0+Q//uM//uM//uM//uM//uM//uM//uM//uM//gOwL9Z0FwUA');
            let collider = this.road.addComponent(ColliderComponent);
            collider.shape = new BoxColliderShape();
            collider.shape.size = BoundUtil.genMeshBounds(this.road).size.clone();
            this.road.rotationY = -90;
            let rigidbody = this.road.addComponent(Rigidbody);
            rigidbody.mass = 0;
            scene.addChild(this.road);
        }
        // add boxes
        {
            let geometry = new BoxGeometry(1, 1, 1);
            let mat = new LitMaterial();
            mat.baseColor = Color.random();
            this.boxes = [];
            for (let i = 0; i < 20; i++) {
                this.boxes[i] = new Object3D();
                let mr = this.boxes[i].addComponent(MeshRenderer);
                mr.geometry = geometry;
                mr.material = mat;
                this.boxes[i].x = Math.random() * 30 - 15;
                this.boxes[i].y = Math.random() * 2;
                this.boxes[i].z = Math.random() * 200 - 100;
                let rigidbody = this.boxes[i].addComponent(Rigidbody);
                rigidbody.mass = 1;
                let collider = this.boxes[i].addComponent(ColliderComponent);
                collider.shape = new BoxColliderShape();
                collider.shape.size = BoundUtil.genMeshBounds(this.boxes[i]).size.clone();
                scene.addChild(this.boxes[i]);
            }
        }
    }

    private loop() {
        Physics.update();
        this.boxes.map((box, index) => {
            if (box.y < -5) {
                this.score.Score++;
                this.scene.removeChild(box);
                this.boxes.splice(index, 1);
            }
        });
    }
}

enum VehicleControlType {
    acceleration,
    braking,
    left,
    right
}
/**
 * Keyboard controller for the car
 */
class VehicleKeyboardController extends ComponentBase {
    protected mBody: Object3D;
    protected mWheels: Object3D[];
    protected mEngineForce = 0;
    protected mBreakingForce = 0;
    protected mVehicleSteering = 0;
    protected mAmmoVehicle;
    protected mVehicleArgs = {
        bodyMass: 800,
        friction: 1000,
        suspensionStiffness: 20.0,
        suspensionDamping: 2.3,
        suspensionCompression: 4.4,
        suspensionRestLength: 0.6,
        rollInfluence: 0.2,
        steeringIncrement: 0.04,
        steeringClamp: 0.5,
        maxEngineForce: 1500,
        maxBreakingForce: 500
    };
    protected mVehicleControlState = [false, false, false, false];
    async start() {
        this.mBody = this.object3D;
        let w1 = this.object3D.getChildByName('wheel_1');
        let w2 = this.object3D.getChildByName('wheel_2');
        let w3 = this.object3D.getChildByName('wheel_3');
        let w4 = this.object3D.getChildByName('wheel_4');
        this.mWheels = [w1, w2, w3, w4];
        this.initController();
    }
    initController() {
        let bound = BoundUtil.genMeshBounds(this.mBody);
        this.mBody.entityChildren[0].transform.y = -bound.size.y / 2 - 0.05;
        let geometry = new Ammo.btBoxShape(new Ammo.btVector3(bound.size.x / 2, bound.size.y / 2, bound.size.z / 2));
        let transform = new Ammo.btTransform();
        transform.setIdentity();
        transform.setOrigin(new Ammo.btVector3(this.mBody.transform.worldPosition.z, this.mBody.transform.worldPosition.y, this.mBody.transform.worldPosition.z));
        transform.setRotation(new Ammo.btQuaternion(0, 0, 0, 1));
        let motionState = new Ammo.btDefaultMotionState(transform);
        let localInertia = new Ammo.btVector3(0, 0, 0);
        geometry.calculateLocalInertia(this.mVehicleArgs.bodyMass, localInertia);
        let bodyRb = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(this.mVehicleArgs.bodyMass, motionState, geometry, localInertia));
        bodyRb.setActivationState(4);
        Physics.world.addRigidBody(bodyRb);
        //raycast Vehicle
        let tuning = new Ammo.btVehicleTuning();
        let rayCaster = new Ammo.btDefaultVehicleRaycaster(Physics.world);
        let vehicle = new Ammo.btRaycastVehicle(tuning, bodyRb, rayCaster);
        vehicle.setCoordinateSystem(0, 1, 2);
        this.mAmmoVehicle = vehicle;
        Physics.world.addAction(vehicle);
        let wheelDirectCS0 = new Ammo.btVector3(0, -1, 0);
        let wheelAxleCS = new Ammo.btVector3(-1, 0, 0);
        let addWheel = (isFront: boolean, x: number, y: number, z: number, radius: number) => {
            let pos = new Ammo.btVector3(x, y, z);
            let wheelInfo = vehicle.addWheel(pos, wheelDirectCS0, wheelAxleCS, this.mVehicleArgs.suspensionRestLength, radius, tuning, isFront);
            wheelInfo.set_m_suspensionStiffness(this.mVehicleArgs.suspensionStiffness);
            wheelInfo.set_m_wheelsDampingRelaxation(this.mVehicleArgs.suspensionDamping);
            wheelInfo.set_m_wheelsDampingCompression(this.mVehicleArgs.suspensionCompression);
            wheelInfo.set_m_frictionSlip(this.mVehicleArgs.friction);
            wheelInfo.set_m_rollInfluence(this.mVehicleArgs.rollInfluence);
        };

        const r = BoundUtil.genMeshBounds(this.mWheels[0]).size.y / 2;
        const x = this.mWheels[0].transform.worldPosition.x - this.mBody.transform.worldPosition.x;
        const y = BoundUtil.genMeshBounds(this.mWheels[0]).size.y - r + 0.1;
        const z = this.mWheels[0].transform.worldPosition.z - this.mBody.transform.worldPosition.z;
        addWheel(true, -x, -y, z, r);
        addWheel(true, x, -y, z, r);
        addWheel(false, -x, -y, -z, r);
        addWheel(false, x, -y, -z, r);
    }
    onEnable() {
        Engine3D.inputSystem.addEventListener(KeyEvent.KEY_UP, this.onKeyUp, this);
        Engine3D.inputSystem.addEventListener(KeyEvent.KEY_DOWN, this.onKeyDown, this);
    }
    onDisable() {
        Engine3D.inputSystem.addEventListener(KeyEvent.KEY_UP, this.onKeyUp, this);
        Engine3D.inputSystem.addEventListener(KeyEvent.KEY_DOWN, this.onKeyDown, this);
    }
    onUpdate() {
        if (!this.mAmmoVehicle) return;
        const vehicle = this.mAmmoVehicle;
        const speed = vehicle.getCurrentSpeedKmHour();
        this.mBreakingForce = 0;
        this.mEngineForce = 0;
        if (this.mVehicleControlState[VehicleControlType.acceleration]) {
            if (speed < -1) this.mBreakingForce = Math.min(this.mVehicleArgs.maxEngineForce / 3, 1000);
            else this.mEngineForce = this.mVehicleArgs.maxEngineForce;
        }
        if (this.mVehicleControlState[VehicleControlType.braking]) {
            if (speed > 1) this.mBreakingForce = Math.min(this.mVehicleArgs.maxEngineForce / 3, 1000);
            else this.mEngineForce = -this.mVehicleArgs.maxEngineForce / 2;
        }
        if (this.mVehicleControlState[VehicleControlType.left]) {
            if (this.mVehicleSteering < this.mVehicleArgs.steeringClamp) this.mVehicleSteering += this.mVehicleArgs.steeringIncrement;
        } else if (this.mVehicleControlState[VehicleControlType.right]) {
            if (this.mVehicleSteering > -this.mVehicleArgs.steeringClamp) this.mVehicleSteering -= this.mVehicleArgs.steeringIncrement;
        } else {
            if (this.mVehicleSteering < -this.mVehicleArgs.steeringIncrement) {
                this.mVehicleSteering += this.mVehicleArgs.steeringIncrement;
            } else {
                if (this.mVehicleSteering > this.mVehicleArgs.steeringIncrement) this.mVehicleSteering -= this.mVehicleArgs.steeringIncrement;
                else this.mVehicleSteering = 0;
            }
        }
        const FRONT_LEFT = 0;
        const FRONT_RIGHT = 1;
        const BACK_LEFT = 2;
        const BACK_RIGHT = 3;
        vehicle.applyEngineForce(this.mEngineForce, BACK_LEFT);
        vehicle.applyEngineForce(this.mEngineForce, BACK_RIGHT);
        vehicle.setBrake(this.mBreakingForce / 2, FRONT_LEFT);
        vehicle.setBrake(this.mBreakingForce / 2, FRONT_RIGHT);
        vehicle.setBrake(this.mBreakingForce, BACK_LEFT);
        vehicle.setBrake(this.mBreakingForce, BACK_RIGHT);
        vehicle.setSteeringValue(this.mVehicleSteering, FRONT_LEFT);
        vehicle.setSteeringValue(this.mVehicleSteering, FRONT_RIGHT);

        // update wheel rotation
        const n = vehicle.getNumWheels();
        const angle = 40;
        for (let i = 0; i < n; i++) {
            let wheel = this.mWheels[i];
            wheel.rotationX += speed;
            if (i < 2) {
                let offset = wheel.rotationZ;
                this.mVehicleSteering === 0 ? (wheel.rotationZ -= offset / 5) : (wheel.rotationZ = offset - this.mVehicleSteering * 10);
                if (wheel.rotationZ < -angle) wheel.rotationZ = -angle;
                else if (wheel.rotationZ > angle) wheel.rotationZ = angle;
            }
        }
        // update body position
        let tm,
            p,
            q,
            qua = Quaternion.HELP_0;
        tm = vehicle.getChassisWorldTransform();
        p = tm.getOrigin();
        this.mBody.x = p.x();
        this.mBody.y = p.y();
        this.mBody.z = p.z();
        q = tm.getRotation();
        qua.set(q.x(), q.y(), q.z(), q.w());
        this.mBody.transform.localRotQuat = qua;
    }
    onKeyUp(e: KeyEvent) {
        this.updateControlState(e.keyCode, false);
    }
    onKeyDown(e: KeyEvent) {
        this.updateControlState(e.keyCode, true);
    }
    updateControlState(keyCode: number, state: boolean) {
        switch (keyCode) {
            case KeyCode.Key_W:
                this.mVehicleControlState[VehicleControlType.acceleration] = state;
                break;
            case KeyCode.Key_Up:
                this.mVehicleControlState[VehicleControlType.acceleration] = state;
                break;
            case KeyCode.Key_S:
                this.mVehicleControlState[VehicleControlType.braking] = state;
                break;
            case KeyCode.Key_Down:
                this.mVehicleControlState[VehicleControlType.braking] = state;
                break;
            case KeyCode.Key_A:
                this.mVehicleControlState[VehicleControlType.left] = state;
                break;
            case KeyCode.Key_Left:
                this.mVehicleControlState[VehicleControlType.left] = state;
                break;
            case KeyCode.Key_D:
                this.mVehicleControlState[VehicleControlType.right] = state;
                break;
            case KeyCode.Key_Right:
                this.mVehicleControlState[VehicleControlType.right] = state;
                break;
        }
    }
}

/**
 * Fix camera to a target
 */
class fixedCameraController extends ComponentBase {
    private camera: Camera3D;
    public distance = 50; // distance to target
    public pitch = 30; // camera pitch angle
    private _tempDir: Vector3;
    private _tempPos: Vector3;
    private _target: Object3D;
    start() {
        this._tempDir = new Vector3();
        this._tempPos = new Vector3();
        this.camera = this.object3D.getComponent(Camera3D);
    }
    get target() {
        return this._target;
    }
    set target(obj) {
        this._target = obj;
    }
    onUpdate() {
        if (!this._target) return;
        this._tempDir.set(0, 0, -1);
        const q = Quaternion.HELP_0;
        q.fromEulerAngles(this.pitch, 0, 0.0);
        this._tempDir.applyQuaternion(q);
        this._tempDir = this._target.transform.worldMatrix.transformVector(this._tempDir, this._tempDir);
        this._tempDir.normalize();
        let position = this._target.transform.worldPosition;
        this._tempPos = Vector3Ex.mulScale(this._tempDir, this.distance, this._tempPos);
        this._tempPos = position.add(this._tempPos, this._tempPos);
        this.camera.lookAt(this._tempPos, this._target.transform.worldPosition);
    }
}
new Sample_PhysicsCar().run();