three.js cannon.js物理引擎製作一個保齡球遊戲

郭先生的部落格發表於2021-02-04

關於cannon.js我們已經學習了一些知識,今天郭先生就使用已學的cannon.js物理引擎的知識配合three基礎知識來做一個保齡球小遊戲,效果如下圖,線上案例請點選部落格原文

我們需要掌握的技能點,就是已經學過的cannon.js物理引擎知識、three.js車削幾何體、threeBSP和簡單的shaderMaterial。下面我們來詳細的說一說如何製作這個遊戲。

1. 設計遊戲

因為我們已經使用過一些物理引擎,所以第一步我們很容易想到要用three做地面網格和牆面網格併為他們生成尺寸相當的剛體資料,這裡面要求牆面和地面固定不動,所以剛體質量設為0。然後就是瓶子,瓶子我們可以直接下載模型,但是為了複習之前的知識,我選擇使用車削幾何體配合著色器來完成。瓶子的剛體我們暫時使用柱體來模擬(雖然和瓶子網格不匹配,但是在物理引擎中其實很少使用外形匹配的剛體,一是因為和實際的效果相差並不大,二是因為簡單剛體的計算相對簡單),車削幾何體所需要的點我們可以通過畫圖或者ps來算出,讓。但是cannon.js的Cylinder預設的up方向和three.js的CylinderGeometry的up方向是不同的,這裡要注意。然後就是關於保齡球的設計思路,玩過保齡球的都知道,保齡球上面是有三個洞的(方便手指拿球),我們考慮使用ThreeBSP來繪製網格,相應的剛體我們使用球體即可。關於相機的控制,我們不使用控制器,在投球之前我們使用左右鍵來控制相機的左右移動,投球后我們讓相機跟隨球運動,在球發生相撞時,我們固定相機的位置。球的出射方向我們仍然使用滑鼠指標控制(使用螢幕座標轉三維座標),最後使用GUi來重置遊戲即可,差不多就是這個思路,下面我們來看程式碼。

2. 遊戲程式碼

程式碼比較簡潔,有必要的我們在程式碼中標註。

1. 初始化剛體

initCannon() {
        //初始化物理世界
    world = new CANNON.World();
    world.gravity.set(0, -9.8, 0);
    world.broadphase = new CANNON.NaiveBroadphase();
    world.solver.iterations = 10;
        //初始化地面剛體
    let groundBody = new CANNON.Body({
        mass: 0,
        shape: new CANNON.Box(new CANNON.Vec3(groundSize.x / 2, groundSize.y / 2, groundSize.z / 2)),
        position: new CANNON.Vec3(0, -groundSize.y / 2, 0),
        material: new CANNON.Material({friction: 1, restitution: 0})
    })
    world.addBody(groundBody);
        //初始化牆面剛體
    let wallLeftBody = new CANNON.Body({
        mass: 0,
        shape: new CANNON.Box(new CANNON.Vec3(wallSize.x / 2, wallSize.y / 2, wallSize.z / 2)),
        position: new CANNON.Vec3(-(wallSize.x + groundSize.x) / 2, wallSize.y / 2, 0),
        material: new CANNON.Material({friction: 0, restitution: 0})
    })
    world.addBody(wallLeftBody);

    let wallRightBody = new CANNON.Body({
        mass: 0,
        shape: new CANNON.Box(new CANNON.Vec3(wallSize.x / 2, wallSize.y / 2, wallSize.z / 2)),
        position: new CANNON.Vec3((wallSize.x + groundSize.x) / 2, wallSize.y / 2, 0),
        material: new CANNON.Material({friction: 0, restitution: 0})
    })
    world.addBody(wallRightBody);
        //初始化保齡球剛體
    sphereBody = new CANNON.Body({
        mass: 50,
        shape: new CANNON.Sphere(sphereRadius),
        position: new CANNON.Vec3(0, sphereRadius, 400),
        material: new CANNON.Material({friction: 0.2, restitution: 0})
    })
    world.addBody(sphereBody);
        //初始化瓶子剛體
    for(let i=0; i<pingPositionArray.length; i++) {
        let pingBody = new CANNON.Body({
            mass: 1,
            shape: new CANNON.Cylinder(2.5,2.5,20,18),
            quaternion: new CANNON.Quaternion().setFromEuler(Math.PI / 2, 0, 0),//因為柱體的up方向和three的up方向相差90度,這裡我們先旋轉90度讓圓柱體“站起來”。
            position: new CANNON.Vec3(pingPositionArray[i][0],pingPositionArray[i][1],pingPositionArray[i][2]),
            material: new CANNON.Material({friction: 0.01, restitution: 1})
        })
        pingBodies.push(pingBody);//將瓶子剛體新增到剛體陣列中,這樣更容易計算
        world.addBody(pingBody);
    }
},

2. 初始化three.js

initThree() {
        //建立地面
    this.initGround();
        //建立牆體
    this.initWall();
        //建立瓶子 並引用
    let pingMesh = this.createPing();
        //pingPositionArray是瓶子位置陣列
    for(let i=0; i<pingPositionArray.length; i++) {
        let pingMeshCopy = pingMesh.clone();
        pingMeshCopy.position.set(pingPositionArray[i][0],pingPositionArray[i][1],pingPositionArray[i][2]);
        pingMeshes.push(pingMeshCopy);
        scene.add(pingMeshCopy);
    }
        //建立保齡球並引用
    sphereMesh = this.createSphere();
    sphereMesh.position.set(0, sphereRadius, 400);
    sphereMesh.rotation.set(Math.PI / 6, 0, - Math.PI / 12);
    scene.add(sphereMesh);
},
createPing() {
    let points = [];
        //latheArray是瓶子車削幾何體所需點的陣列
    for(let i=0; i<latheArray.length; i++) {
        points.push(new THREE.Vector2(latheArray[i][0]/10, latheArray[i][1]/10))
    }
    let geometry = new THREE.LatheGeometry(points, 30);
    geometry.computeVertexNormals();
        //著色器材質
    let material = new THREE.ShaderMaterial({
        vertexShader: `
            varying vec3 vPosition;
            varying vec3 vNormal;
            void main() {
                vNormal = normal;
                vPosition = position;
                gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
            }
        `,
        fragmentShader: `
            varying vec3 vPosition;
            varying vec3 vNormal;
            void main() {
                                //光線向量
                vec3 light = vec3(10.0, 10.0, 10.0);
                float strength = dot(light, vNormal) / length(light);
                float y = vPosition.y;
                                //在 [3.1, 3.7]和[4.2, 4.8]之間被渲染成紅色並根據光線向量和法向量模擬光照
                if(y < 4.8 && y > 4.2 || y < 3.7 && y > 3.1) {
                    gl_FragColor=vec4(1.0, 0.4 * pow(strength, 2.0), 0.4 * pow(strength, 2.0), 1.0);
                } else {
                    gl_FragColor=vec4( 0.6 + 0.4 * pow(strength, 2.0), 0.6 + 0.4 * pow(strength, 2.0), 0.6 + 0.4 * pow(strength, 2.0), 1.0);
                }
            }
        `,
        side: THREE.DoubleSide
    }); 
    let mesh = new THREE.Mesh(geometry, material);
    mesh.quaternion.copy(new THREE.Quaternion().setFromEuler(new THREE.Euler(-Math.PI / 2, 0, 0)));
        //這裡將柱體網格新增到group中,為的是group的旋轉
    let group = new THREE.Group();
    group.add(mesh);
    return group;
},
createSphere() {
    let material = new THREE.MeshPhongMaterial({color: 0xEE100F, shininess: 60, specular: 0x2C85E1, side: THREE.DoubleSide});
    let sphereGeometry = new THREE.SphereGeometry(sphereRadius, 40, 24);
    let cylinderGeometry = new THREE.CylinderGeometry(sphereRadius/10,sphereRadius/10,sphereRadius,30);
    let sphereMesh = new THREE.Mesh(sphereGeometry, material);
    let cMesh1 = new THREE.Mesh(cylinderGeometry, material);
    let cMesh2 = cMesh1.clone();
    let cMesh3 = cMesh1.clone();
    cMesh1.position.set(1.14, sphereRadius, 0.67);
    cMesh2.position.set(-1.14, sphereRadius, 0.67);
    cMesh3.position.set(0, sphereRadius, -1.33);
        //構造BSP
    let bsp1 = new ThreeBSP(sphereMesh);
    let bsp2 = new ThreeBSP(cMesh1);
    let bsp3 = new ThreeBSP(cMesh2);
    let bsp4 = new ThreeBSP(cMesh3);
        //用球形幾何體,減去三個小的圓柱體
    let resultBsp = bsp1.subtract(bsp2).subtract(bsp3).subtract(bsp4);
    let resultGeom = resultBsp.toGeometry();//這裡我們只需要匯出幾何體
    resultGeom.mergeVertices();//注意這兩步,不然保齡球不會計演算法向量,也就不會平滑著色
    resultGeom.computeVertexNormals();
    return new THREE.Mesh(resultGeom, material);
},
initGround() {
    let texture = new THREE.TextureLoader().load('/static/images/base/ground.jpg');
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.set(1, 4);
    let geometry = new THREE.BoxBufferGeometry(groundSize.x, groundSize.y, groundSize.z);
    let material = new THREE.MeshPhongMaterial({map: texture});
    let mesh = new THREE.Mesh(geometry, material);
    mesh.position.y = -groundSize.y / 2;
    scene.add(mesh);
},
initWall() {
    let material = new THREE.MeshLambertMaterial({color: 0x77dddd});
    let geometry = new THREE.BoxBufferGeometry(wallSize.x, wallSize.y, wallSize.z);
    let leftMesh = new THREE.Mesh(geometry, material);
    let rightMesh = leftMesh.clone();
    leftMesh.position.set(-(wallSize.x + groundSize.x) / 2, wallSize.y / 2, 0);
    rightMesh.position.set((wallSize.x + groundSize.x) / 2, wallSize.y / 2, 0);
    scene.add(leftMesh);
    scene.add(rightMesh);
},

3. 定義事件

這裡我們需要滑鼠mousemove事件和onkeydown,onkeyup事件

document.onkeydown = this.handler;
document.onkeyup = this.handler;
this.$refs.box.addEventListener('mousemove', event => {
        //滑鼠移動,螢幕二維向量轉三維向量
    let x = (event.clientX / window.innerWidth) * 2 - 1;
    let y = - (event.clientY / window.innerHeight) * 2 + 1;
    direction = new THREE.Vector3(x,y,-1).applyQuaternion(camera.getWorldQuaternion(new THREE.Quaternion())).normalize();
})
handler(event) {
    var down = (event.type == 'keydown');
    switch(event.keyCode){
        case 32: {
            if(down && time > event.timeStamp) {
                time = event.timeStamp;//time預設值為Infinity,第一次按下空格,給time賦值
            } else if(down) {
                relaxation = event.timeStamp - time;//持續按下,計算累積時間
            } else {
                                //根據持續時間給球初始化速度
                let t = relaxation > 5000 ? 500 : relaxation / 10;
                sphereBody.velocity.set(direction.x * t, direction.y * t, direction.z * t);
                sphereBody.angularVelocity.set(-1,0,0);
                time = Infinity;
            }
        }
            break;

        case 37:
            camera.position.x --;
            sphereBody.position.x --;
            break;

        case 39:
            camera.position.x ++;
            sphereBody.position.x ++;
            break;
    }
},

主要程式碼大致就是這樣,下一節還會繼續cannon.js的學習。

 

轉載請註明地址:郭先生的部落格

相關文章