Cannon.jsのRaycastVehicleのデモ

前回の三次元物理演算エンジン、Cannon.jsを使ってみた。に引き続き、Cannon.jsを触ってみました。Heightfieldシェイプで地面を作成するところは一緒ですが、今回はそこに車両を走らせてみました。

まずは以下のデモをご覧ください。

20141106_img

▼操作方法

  • UP : 前進
  • DOWN : 後退
  • LEFT : 左に曲がる
  • RIGHT : 右に曲がる
  • SPACE : ブレーキ

▼ソースコードは以下よりダウンロードいただけます。

RaycastVehicleについて

Cannon.jsにはシャーシとホイールというBodyを持った、いわゆる車両を生成するために、RigidVehicleクラスRaycastVehicleクラスというヘルパークラスが用意されています。

RigidVehicleは車両を形成する基本的な機能のみを備えたシンプルなクラスです。とりあえず動く車両という事でしたら、問題なく利用出来るでしょう。こちらは以前から用意されていたものです。

RaycastVehicleは、設定項目が多く、サスペンションやブレーキといった機能がある、より車両らしく走行出来るようにカスタマイズされたクラスです。本格的に車両を走らせる目的では、このRaycastVehicleの方が、よりリアルに車両として走行している感じを出せるかと思います。

今回はこのRaycastVehicleを使ってデモを作成しました。

物理世界にContactMaterialを追加

車両を作成する前に、ContactMaterialクラスでホイールと地面の2つの素材に関して、摩擦や反発を定義します。

/index.html 355行目〜

_world.addContactMaterial(new CANNON.ContactMaterial(new CANNON.Material("wheelMaterial"), new CANNON.Material("groundMaterial"), {
    friction: 0.3,
    restitution: 0,
    contactEquationStiffness: 1000
}));

RaycastVehicle(車両)生成

RaycastVehicleクラスで車両を作成します。まずはシャーシ用にCannon.jsでBodyを作成します。作成したシャーシ用BodyをRaycastVehicleクラスのコンストラクタに引数で渡して車両を作成します。その後、ホイールを追加していきます。

/index.html 427行目〜

function create() {
    // 車体追加
    _body = new CANNON.Body({mass: 500});
    _body.addShape(new CANNON.Box(new CANNON.Vec3(2, 1, 0.5)));
    _body.position.set(0, 1, 0);
    _mesh = Utility().shape2mesh({
        body: _body,
        color: 0xff0000
    });
    _scene.add(_mesh);
    // 車生成
    var options = {
        radius: 0.5,
        directionLocal: new CANNON.Vec3(0, 0, -1),
        suspensionStiffness: 50,
        suspensionRestLength: 0.5,
        frictionSlip: 5,
        dampingRelaxation: 2.3,
        dampingCompression: 4.4, // 跳ね返る強さ
        maxSuspensionForce: 100000,
        rollInfluence:  0.01,
        axleLocal: new CANNON.Vec3(0, 1, 0),
        chassisConnectionPointLocal: new CANNON.Vec3(1, 1, 0),
        maxSuspensionTravel: 0.3,
        customSlidingRotationalSpeed: -30,
        useCustomSlidingRotationalSpeed: true
    };
    // 車両ヘルパークラス
    _raycastVehicle = new CANNON.RaycastVehicle({chassisBody: _body});
    options.chassisConnectionPointLocal.set(1, 1, 0);
    _raycastVehicle.addWheel(options);
    options.chassisConnectionPointLocal.set(1, -1, 0);
    _raycastVehicle.addWheel(options);
    options.chassisConnectionPointLocal.set(-1, 1, 0);
    _raycastVehicle.addWheel(options);
    options.chassisConnectionPointLocal.set(-1, -1, 0);
    _raycastVehicle.addWheel(options);
    _raycastVehicle.addToWorld(_world);
    //
    _wheelInfoArr = [];
    for(var i=0; i < _raycastVehicle.wheelInfos.length; i++) {
        var wheel = _raycastVehicle.wheelInfos[i];
        var body = new CANNON.Body({mass: 1});
        var q = new CANNON.Quaternion();
        q.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI / 2);
        body.addShape(new CANNON.Cylinder(wheel.radius, wheel.radius, wheel.radius, 20), new CANNON.Vec3(), q);
        var mesh = Utility().shape2mesh({
            body: body,
            color: 0x000000
        });
        _scene.add(mesh);
        _wheelInfoArr.push({
            body: body,
            mesh: mesh
        });
    }
    // ホイール更新
    _world.addEventListener("postStep", function(){
        for (var i = 0; i < _wheelInfoArr.length; i++) {
            _raycastVehicle.updateWheelTransform(i);
            var t = _raycastVehicle.wheelInfos[i].worldTransform;
            var info = _wheelInfoArr[i];
            info.body.position.copy(t.position);
            info.body.quaternion.copy(t.quaternion);
        }
    });
}

Cannon.jsのBodyを元にThree.jsのMeshを作成

公式サイトから落とせるデモ内の/build/cannon.demo.jsのCannon.jsのBodyを元にThree.jsのMeshを作成するshape2meshメソッドをコピペして利用しています。引数にCannon.jsで作成したBodyとマテリアルカラーをオブジェクトに格納して渡すと、その形状にあわせて、Three.jsのMeshを作成して、返却するメソッドです。

/index.html 146行目〜

function shape2mesh(params){
    var body = params.body;
    var color = params.color;
    //
    if (!color && color != 0) color = 0xdddddd;
    var obj = new THREE.Object3D();
    for (var l = 0; l < body.shapes.length; l++) {
        var shape = body.shapes[l];
        var mesh;
        var geometry;
        var i;
        switch(shape.type){
            case CANNON.Shape.types.SPHERE:
                geometry = new THREE.SphereGeometry(shape.radius, 8, 8);
                mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({color: color}));
                break;
            case CANNON.Shape.types.PARTICLE:
                mesh = new THREE.Mesh(this.particleGeo, this.particleMaterial);
                mesh.scale.set(10 , 10, 10);
                break;
            case CANNON.Shape.types.PLANE:
                geometry = new THREE.PlaneGeometry(10, 10, 4, 4);
                mesh = new THREE.Object3D();
                var submesh = new THREE.Object3D();
                var ground = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({color: color}));
                ground.scale.set(100, 100, 100);
                submesh.add(ground);
                ground.castShadow = true;
                ground.receiveShadow = true;
                mesh.add(submesh);
                break;
            case CANNON.Shape.types.BOX:
                geometry = new THREE.BoxGeometry(shape.halfExtents.x*2, shape.halfExtents.y*2, shape.halfExtents.z*2);
                mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({color: color}));
                break;
            case CANNON.Shape.types.CONVEXPOLYHEDRON:
                geometry = new THREE.Geometry();
                for (i = 0; i < shape.vertices.length; i++) {
                    var v = shape.vertices[i];
                    geometry.vertices.push(new THREE.Vector3(v.x, v.y, v.z));
                }
                for(i=0; i < shape.faces.length; i++){
                    var face = shape.faces[i];
                    var a = face[0];
                    for (j = 1; j < face.length - 1; j++) {
                        var b = face[j];
                        var c = face[j + 1];
                        geometry.faces.push(new THREE.Face3(a, b, c));
                    }
                }
                geometry.computeBoundingSphere();
                geometry.computeFaceNormals();
                mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({color: color}));
                break;
            case CANNON.Shape.types.HEIGHTFIELD:
                geometry = new THREE.Geometry();
                var v0 = new CANNON.Vec3();
                var v1 = new CANNON.Vec3();
                var v2 = new CANNON.Vec3();
                for (var xi = 0; xi < shape.data.length - 1; xi++) {
                    for (var yi = 0; yi < shape.data[xi].length - 1; yi++) {
                        for (var k = 0; k < 2; k++) {
                            shape.getConvexTrianglePillar(xi, yi, k===0);
                            v0.copy(shape.pillarConvex.vertices[0]);
                            v1.copy(shape.pillarConvex.vertices[1]);
                            v2.copy(shape.pillarConvex.vertices[2]);
                            v0.vadd(shape.pillarOffset, v0);
                            v1.vadd(shape.pillarOffset, v1);
                            v2.vadd(shape.pillarOffset, v2);
                            geometry.vertices.push(
                                    new THREE.Vector3(v0.x, v0.y, v0.z),
                                    new THREE.Vector3(v1.x, v1.y, v1.z),
                                    new THREE.Vector3(v2.x, v2.y, v2.z)
                            );
                            i = geometry.vertices.length - 3;
                            geometry.faces.push(new THREE.Face3(i, i+1, i+2));
                        }
                    }
                }
                geometry.computeBoundingSphere();
                geometry.computeFaceNormals();
                mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({color: color}));
                break;
        }
        mesh.receiveShadow = true;
        mesh.castShadow = true;
        if(mesh.children){
            for(i=0; i<mesh.children.length; i++){
                mesh.children[i].castShadow = true;
                mesh.children[i].receiveShadow = true;
                if(mesh.children[i]){
                    for(var j=0; j<mesh.children[i].length; j++){
                        mesh.children[i].children[j].castShadow = true;
                        mesh.children[i].children[j].receiveShadow = true;
                    }
                }
            }
        }
        var o = body.shapeOffsets[l];
        var q = body.shapeOrientations[l];
        mesh.position.set(o.x, o.y, o.z);
        mesh.quaternion.set(q.x, q.y, q.z, q.w);
        obj.add(mesh);
    }
    return obj;
}

Cannon.jsの車両情報をThree.jsのMeshに反映

シャーシとホイールのBodyの位置と姿勢をThree.jsのMeshにcopyメソッドで反映させます。

/index.html 495行目〜

function update() {
    _mesh.position.copy(_body.position);
    _mesh.quaternion.copy(_body.quaternion);
    var i = 0, max;
    for (i = 0, max = _wheelInfoArr.length; i < max; i = i + 1) {
        var info = _wheelInfoArr[i];
        info.mesh.position.copy(info.body.position);
        info.mesh.quaternion.copy(info.body.quaternion);
    }
}

入力に応じて、RaycastVehicle(車両)を制御

キーボード入力に応じて、RaycastVehicle(車両)に、エンジンのオンオフ、ブレーキのオンオフ、ホイールの角度を反映させます。

/index.html 596行目〜

function _applyEngineForce(val) {
    _raycastVehicle.applyEngineForce(val, 2);
    _raycastVehicle.applyEngineForce(val, 3);
}

function _setBrake(val) {
    _raycastVehicle.setBrake(val, 0);
    _raycastVehicle.setBrake(val, 1);
    _raycastVehicle.setBrake(val, 2);
    _raycastVehicle.setBrake(val, 3);
}

function _setSteeringAngle(val) {
    _raycastVehicle.setSteeringValue(val, 0);
    _raycastVehicle.setSteeringValue(val, 1);
}

まとめ

Cannon.jsでは、ドキュメントはあるのですが、実際に触っている人が少なく、ググってもあまりヒットしない現状では、きちんとソースコードを読み解かないと詳しい内容までわからない事が多いです。実際にRaycastVehicleでは、触りながら何となく理解していったところも多く、まだきちんと理解していないところも多々あります。公式デモは充実していますので、このデモを元に触ってみるのもいいと思います。

次は、Blenderでのモデリングデータを読み込んで、車両とコースに反映させて、表現力を高めていきたいと思います。