three.js 製作魔方

Vadim發表於2020-07-27

因為之前的幾節講了一些數學方法,例如Vector3、Matrix4、Euler還有Quaternion的知識。所以這篇郭先生就來說說用three.js怎麼製作一個魔方。線上案例請點選部落格原文

製作魔方主要運用座標變換的知識,製作魔方的方法有很多,建議你先在大腦中構思,然後試著做一做,下面我將一種比較簡單的方法。

  1. 製作出魔方各個方塊的位置座標(27個)的陣列,然後製作出魔方各個面的材質(6個)
  2. 根據座標和材質製作魔方的方塊,並新增到一個組group
  3. 製作一個標誌被選面的幾何體(我是用球體),然後隱藏
  4. 使用THREE.Raycaster,給模型繫結事件,並記錄選定的一些資料,將標誌顯示並放到合適的位置
  5. 點選模擬方向鍵盤,根據之前記錄的資料,轉動魔方(重點)

以上是主要的步驟,但是對於新同學,需要注意的仍然很多,下面上主要程式碼,按照方法說

1. 定義的變數

posArr = [
    [100,100,100],[100,100,0],[100,100,-100],[100,0,100],[100,0,0],[100,0,-100],[100,-100,100],[100,-100,0],[100,-100,-100],
    [0,100,100],[0,100,0],[0,100,-100],[0,0,100],[0,0,0],[0,0,-100],[0,-100,100],[0,-100,0],[0,-100,-100],
    [-100,100,100],[-100,100,0],[-100,100,-100],[-100,0,100],[-100,0,0],[-100,0,-100],[-100,-100,100],[-100,-100,0],[-100,-100,-100]
],//方塊位置座標
materials,//材質陣列
mouse = new THREE.Vector2(),//通過滑鼠點選的位置計算出raycaster所需要的點的位置,以螢幕中心為原點,值的範圍為-1到1.
raycaster,//射線物件
group,//存放魔方方塊的陣列
groupTemp,//魔方轉動時臨時陣列
object3d,//魔方被選擇面的標誌物物件
currentPos,//魔方被點選小塊的位置
currentNor,//魔方被點選小塊面的法向量
currentUp,//魔方被點選時,相機up的向量

2. 定義材質陣列

initMaterial() {
    var map_red = new THREE.TextureLoader().load('/static/images/color/m_red.jpg', () => this.loadover --);
    var map_orange = new THREE.TextureLoader().load('/static/images/color/m_orange.jpg', () => this.loadover --);
    var map_yellow = new THREE.TextureLoader().load('/static/images/color/m_yellow.jpg', () => this.loadover --);
    var map_blue = new THREE.TextureLoader().load('/static/images/color/m_blue.jpg', () => this.loadover --);
    var map_green = new THREE.TextureLoader().load('/static/images/color/m_green.jpg', () => this.loadover --);
    var map_white = new THREE.TextureLoader().load('/static/images/color/m_white.jpg', () => this.loadover --);
    var map_sprite = new THREE.TextureLoader().load('/static/images/base/direction.png', () => this.loadover --);

    let mater_red = new THREE.MeshBasicMaterial({map: map_red, side: THREE.DoubleSide});
    let mater_orange = new THREE.MeshBasicMaterial({map: map_orange, side: THREE.DoubleSide});
    let mater_yellow = new THREE.MeshBasicMaterial({map: map_yellow, side: THREE.DoubleSide});
    let mater_white = new THREE.MeshBasicMaterial({map: map_white, side: THREE.DoubleSide});
    let mater_blue = new THREE.MeshBasicMaterial({map: map_blue, side: THREE.DoubleSide});
    let mater_green = new THREE.MeshBasicMaterial({map: map_green, side: THREE.DoubleSide});

    materials = [mater_red, mater_orange, mater_yellow,mater_white, mater_blue, mater_green];
}

3. 繪製小方塊並繪製標誌物(先隱藏)

initsquare() {
    var sphereGeom = new THREE.SphereGeometry(10, 30, 20);
    var sphereMate = new THREE.MeshPhongMaterial({color: 0x333333});
    object3d = new THREE.Mesh(sphereGeom, sphereMate);
    object3d.name = 'object3d';
    object3d.visible = false;
    scene.add(object3d);

    group = new THREE.Group();
    group.name = 'group';

    var geometry = new THREE.BoxGeometry(100, 100, 100);
    var mesh = new THREE.Mesh(geometry, materials);
    posArr.forEach((d,i) => {
        let meshCopy = mesh.clone();
        meshCopy.position.set(d[0], d[1], d[2])
        meshCopy.name = 'box-' + i;
        group.add(meshCopy);
    })

    scene.add(group);

    this.render();
    document.getElementById("loading").style.display = "none";
}

4. 監聽模型的點選事件

initEventListener() {
    raycaster = new THREE.Raycaster();
    document.addEventListener('mousemove', function (event) {
        event.preventDefault();
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
    }, false)
    document.addEventListener('mousedown', () => {
        if (scene.children && scene.getObjectByName('group')) {
            raycaster.setFromCamera(mouse, camera);
            let intersects = raycaster.intersectObjects(scene.getObjectByName('group').children);
            if (intersects[0] && intersects[0].object.name != 'object3d') {
                let index = intersects[0].faceIndex;
                let point = intersects[0].point;
                currentUp = this.computedUp(camera);
                currentNor = this.computedNor(point)
                currentPos = intersects[0].object.position;
                let pos = this.computedPos(point);
                object3d.position.copy(pos);
                object3d.visible = true;
            }
        }
    })
}

5. 監聽方向軟鍵盤的點選,根據點選鍵的不同,生成旋轉軸

handleRotate(num) {
    if(!rotateFlag || !currentPos) return ;
    rotateFlag = false;
    groupTemp = new THREE.Group();
    groupTemp.name = 'group-temp';
    let axis;
    let tempMeshArr = [];
    switch (num) {
        case 1:
            axis = currentNor.clone().cross(currentUp);
            break;
        case 2:
            axis = currentNor.clone().cross(currentUp).negate();
            break;
        case 3:
            axis = currentUp.clone().negate();
            break;
        case 4:
            axis = currentUp;
            break;
    }
    let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(axis, currentPos);
    scene.getObjectByName('group').children.forEach(d => Math.abs(plane.distanceToPoint(d.position)) < 1 && tempMeshArr.push(d))
    tempMeshArr.forEach(d => {
        group.remove(d);
        groupTemp.add(d);
    })
    // object3d.visible = false;
    scene.remove(scene.getObjectByName('group-temp'));
    scene.add(groupTemp);
    this.handleTween(axis);
}

6. 加一點tween的補間動畫,效果更好哦

handleTween(axis) {
    let start = {angle: 0, axis};
    let end = {angle: Math.PI/2, axis};
    tween = new TWEEN.Tween(start).to(end, 500);
    tween.easing(TWEEN.Easing.Linear.None);
    tween.onUpdate(function () {
        let quaternion = new THREE.Quaternion().setFromAxisAngle(axis, this._object.angle);
        groupTemp.rotation.setFromQuaternion(quaternion);
    });
    tween.onComplete(() => {
        let matrix = this.standerMatrix(groupTemp.matrix);
        groupTemp.children.forEach(d => {
            let mesh = d.clone();
            mesh.applyMatrix4(matrix)
            mesh.position.copy(this.standarPos(mesh.position))
            group.add(mesh);
        })
        scene.remove(groupTemp)
        rotateFlag = true;
    })
    tween.start();
}

郭先生製作魔方的主要思路就是先做出27個方塊新增到組A,6個面分別新增不同顏色的貼圖(自己p的哦),然後使用raycaster選擇點選的面,並確定當時的up方向和法向量方向以備後用,點選上下左右並結合u方向和法向量方向計算出旋轉矩陣,根據已有條件計算出是那一排方塊改變,並將這9個塊新增到組B中,從組A中刪除這9個,根據旋轉矩陣旋轉組B,並且在旋轉完之後將組B中的方塊新增到組A中,旋轉完畢(這裡比較難的就是根據上下左右判斷旋轉軸向量)。

以上就是一種製作魔方的方法,綜合了很多矩陣向量四元數尤拉角和平面的知識,希望對新來的同遊有些幫助

 

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

相關文章