three.js cannon.js物理引擎之約束

郭先生的部落格發表於2021-01-20

今天郭先生繼續說cannon.js,主演內容就是點對點約束和2D座標轉3D座標。仍然以一個案例為例,場景由一個地面、若干網格組成的約束體和一些擁有初速度的球體組成,如下圖。線案例請點選部落格原文

下面來說說如何使用約束來完成一個這樣的物理場景。

1. 建立three場景

這一步是基礎工作,對於有一定three基礎的同學都不會陌生,我就直接上程式碼了。

 

initThree() {
    scene = new THREE.Scene();

    camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 );
    camera.position.x = 40;
    camera.position.y = 52;
    camera.position.z = 78;
    scene.add( camera );

    scene.add(new THREE.AxesHelper(40)); 

    scene.add(new THREE.AmbientLight(0x888888));

    const light = new THREE.DirectionalLight(0xbbbbbb, 1);
    light.position.set(0, 50, 50);
    const distance = 200;

    let texture = new THREE.TextureLoader().load('/static/images/base/ground.png');
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.copy(new THREE.Vector2(40, 40));

    let groundGeom = new THREE.BoxBufferGeometry(100, 0.2, 100);
    let groundMate = new THREE.MeshPhongMaterial({color: 0xdddddd, map: texture})
    ground = new THREE.Mesh(groundGeom, groundMate);
    ground.position.y = -0.1;
    ground.receiveShadow = true;
    scene.add(ground);

    geometry = new THREE.BoxGeometry( 2, 2, 2 );

    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setSize( window.innerWidth, window.innerHeight );
    renderer.shadowMap.enabled = true;
    renderer.setClearColor(0xbfd1e5);

    controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 10, 0);
    camera.lookAt(0,10,0);

    this.$refs.box.appendChild( renderer.domElement );

    stats = new Stats();
    this.$refs.box.appendChild(stats.dom);
},

這裡面主要進行初始化場景、相機、渲染器、燈光和地面等操作。

2. 初始化物理世界

這裡麵包括建立CANNON.World,建立地面剛體,每塊需要被約束的剛體和設定點對點約束(在給定的偏移點連線兩個實體),接下來我們仍以程式碼註釋的形式詳細的講解對於物理世界的建立。

initCannon() {
    world = new CANNON.World();
    world.gravity.set(0, -9.8, 0);
    world.broadphase = new CANNON.NaiveBroadphase();
    world.solver.iterations = 10;
    bodyGround = new CANNON.Body({
        mass: 0,
        position: new CANNON.Vec3(0, -0.1, 0),
        shape: new CANNON.Box(new CANNON.Vec3(50, 0.1, 50)),
        material: new CANNON.Material({friction: 0.05, restitution: params.restitution})
    });
    ground.userData = bodyGround;
    world.addBody(bodyGround);
        //上面的程式碼意義上一節已經講過了,我就不多言,主要看下面的程式碼。
        //這裡設定了一些變數,N表示組成約束體剛體的數量,space表示相鄰兩個剛體直接的距離間隔,mass為剛體的質量變數,width表示剛體半寬度,height表示剛體半高度,last表示上一個相連的剛體。
    var N = 20, space = 0.1, mass = 0, width = 10, hHeight = 1, last;
    var halfVec = new CANNON.Vec3(width, hHeight, 0.2);//剛體的長寬高的halfSize向量
    var boxShape = new CANNON.Box(halfVec);//定義一個長方體資料
    var boxGeometry = new THREE.BoxBufferGeometry(halfVec.x * 2, halfVec.y * 2, halfVec.z * 2);//定義一個長方几何體
    var boxMaterial = new THREE.MeshLambertMaterial( { color: 0xffaa00 } );//定義幾何體材質

    for(var i=0; i<N; i++) {//遍歷N次,從上到下建立長方體網格和剛體,位置逐漸變低,質量逐漸變小。
        var boxBody = new CANNON.Body({mass: mass, material: new CANNON.Material({friction: 0.05, restitution: params.restitution})});//建立剛體,第一個剛體的質量設定成0(即為不動的剛體),定義材質,並設定摩擦係數和彈性係數
        boxBody.addShape(boxShape);//為剛體新增形狀
        var boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);//建立three世界的網格
        boxBody.position.set(0, (N - i + 5) * (hHeight * 2 + space * 2), 0);//這裡設定剛體的位置,是由上倒下的順序
        boxBody.linearDamping = 0.01;//設定線性阻尼
        boxBody.angularDamping = 0.01;//設定旋轉阻尼
        world.addBody(boxBody);//將剛體新增到物理世界中
        scene.add(boxMesh);//將網格新增到three場景中
        boxes.push(boxBody);//將剛體新增到陣列中
        boxMeshes.push(boxMesh);//將網格新增到陣列中,這兩步可以在更新物理世界中找到他們的對應關係,也可以新增到Mesh的userData屬性中去,具體可以參見上一篇文章
        if(i == 0) { //當i=0時,也就是第一個剛體,在剛體建立完畢後,我們將mass變數設定成1
            mass = 1;
        } else {//從第二個剛體往後都會建立兩個點對點的約束,點對點約束我們下面講
            var ptp1 = new CANNON.PointToPointConstraint(boxBody, new CANNON.Vec3(-width, hHeight + space, 0), last, new CANNON.Vec3(-width, -hHeight - space, 0), (N - i) / 4);
            var ptp2 = new CANNON.PointToPointConstraint(boxBody, new CANNON.Vec3(width, hHeight + space, 0), last, new CANNON.Vec3(width, -hHeight - space, 0), (N - i) / 4);
            world.addConstraint(ptp1);//將約束新增到物理世界
            world.addConstraint(ptp2);//將約束新增到物理世界
        }
        last = boxBody;//這裡將本次建立的剛體賦值給last變數,一遍下一個迴圈使用
    }
},

我們來說說這個點對點約束,他時由5個引數組成

PointToPointConstraint ( bodyA  pivotA  bodyB  pivotB  maxForce )
  • bodyA – 剛體A
  • pivotA – 相對於剛體A質心的點,剛體A被約束到該點。
  • bodyB – 將被約束到與剛體A相同的點的主體。因此,我們將獲得剛體A和剛體B之間的連結。如果未指定,剛體A將被約束到一個靜態點。
  • pivotB – 相對於剛體B質心的點,剛體B被約束到該點。
  • maxForce – 約束物體應施加的最大力(如果施加的力過大,剛體A和剛體B之間的連結就會被拉長)

下面就是我們設定連結點的示意圖,這樣我們就可以清楚上面的程式碼了

3. 根據滑鼠點選,發射一個剛體球

這裡就要應用到2D座標轉3D座標的一些知識了,這裡網上已經有很多相關的知識了,可以看threejs 世界座標與螢幕座標相互轉換,這裡我就直接上程式碼了

document.addEventListener('click', event => { //點選滑鼠
    event.preventDefault();//阻止預設事件
    let x = (event.clientX / window.innerWidth) * 2 - 1;//將滑鼠點選的x值轉換成[-1, 1]
    let y = - (event.clientY / window.innerHeight) * 2 + 1;//將滑鼠點選的y值轉換成[-1, 1]
    let p = new THREE.Vector3(x, y, -1).unproject(camera);//通過unproject方法,使用所傳入的攝像機來反投影(projects)該向量,得到滑鼠對應三維空間點
    let v = p.sub(camera.position).normalize();//用滑鼠對應的三維空間點減去相機的位置向量,然後歸一化得到小球的射出方向的單位向量
    this.createSphere(v, camera.position);//把需要的兩個向量傳入建立小球的方法中
})
createSphere(v, c) {
        //建立小球的方法和上一篇很相似,我就不贅述了
    const speed = 50;
    var geometry = new THREE.SphereBufferGeometry(1.5, 32, 16);
    let sphere = new THREE.Mesh( geometry, this.createRandomMaterial());
    sphere.position.copy(c);
    sphere.castShadow = true;
    sphere.receiveShadow = true;
    scene.add( sphere );
    ballMeshes.push(sphere);

    let sphereBody = new CANNON.Body({
        mass: params.mass,
        position: new CANNON.Vec3(c.x, c.y, c.z),
        shape: new CANNON.Sphere(1.5),
        material: new CANNON.Material({friction: 0.1, restitution: params.restitution})
    });
    sphereBody.collisionResponse = 0.01;
    sphereBody.velocity.set(v.x * speed, v.y * speed, v.z * speed);//這裡要注意velocity屬性可以剛體帶有出速度
    world.addBody(sphereBody);
    balls.push(sphereBody)

    setTimeout(() => {
        scene.remove(sphere);
        sphere.material.dispose();
        sphere.geometry.dispose();
        world.removeBody(sphereBody);
        balls.shift();
        ballMeshes.shift();
    }, 60000)
}
createRandomMaterial() {
    color.setHSL(Math.random(), 1.0, 0.5);
    return new THREE.MeshPhongMaterial({color: color});
}

這樣就完成了點對點約束的物理效果,讓原本虛擬的three世界變得更加真實。

 

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

相關文章