本篇介紹的是邊分裂的方式,網格細分需要大量使用到先前所學到的拓樸觀念,實作也與單純的觀念理解上有些許的不同,在撰寫的複雜度也有所提升因此分為兩次說明。
本次要實作的邊分裂方式
首先在 index.html 中加入一顆按鈕。
<div id="refinement" class="refinement" style="cursor: pointer;"></div>
並且在 index.scss 建立這顆按鈕的樣式。
.refinement {
position: absolute;
background-image: url('../images/refinement_white_18dp.png');
width: 72px;
height: 72px;
top: 72px;
&:hover {
left: 2px;
}
}
建立一個 Refinement 模組,並且加入一個用於設定Viewer的函數。
var Refinement = function () {
this.viewer = null;
};
Refinement.prototype.setViewer = function (viewer) {
var self = this;
self.viewer = viewer;
};
module.exports = Refinement;
接著在 index.js 加入宣告,並且設定 viewer 函數進行綁定。
import Refinement from './Refinement';
refinement.setViewer(viewer);
最後將按鈕綁定一個 midedge 事件,用來建立中間邊線方法。
$("#refinement").onclick = function (e) {
refinement.midedge();
};
首先要開始建立 midedge 函數,先準備好 viewer 中的 geometry 與 topology 備用。
Refinement.prototype.midedge = function () {
var self = this;
var geometry = self.viewer.meshs[0].geometry;
var topology = self.viewer.meshs[0].topology;
// Do Something
};
接著要搜尋所有的邊並且都進行網格細分,因此建立一個迴圈跑完所有的邊。
var len = topology.edge.length;
var i = 0;
for (i = 0; i < len; i++) {
// Do Something
}
由於需要的參數會有很多,需要先整理起來備用避免使用混亂。
var edge = topology.edge[i];
var center = edge.center;
var vertexIDs = edge.vertexIDs;
var faceIDs = edge.faceIDs;
搭配前回筆者繪製的示意圖,透過 topology 函數中的 creat 方法建立新的 vertex 頂點 D ,並且複製邊線的中心點座標到頂點 D。
var VertexD = topology.create('vertex');
VertexD.vector3 = center.clone();
透過邊線內的面資訊找到兩個面的物件,整理好備用。
var faceTop = topology.face[faceIDs[0]];
var faceBottom = topology.face[faceIDs[1]];
最後再整理一下每個舊有的頂點ID,其中頂點 C 與頂點 E 需要由面拓樸找到有三個頂點,再扣掉邊拓樸儲存的兩個頂點,剩下的頂點就是我們需要的頂點 C 與 E。
var vertexIdA = vertexIDs[0];
var vertexIdB = vertexIDs[1];
var vertexIdC = faceTop.vertexIDs.find(
(element, index, array) => (vertexIDs.indexOf(element) === -1)
);
var vertexIdD = VertexD.ID;
var vertexIdE = faceBottom.vertexIDs.find(
(element, index, array) => (vertexIDs.indexOf(element) === -1)
);
首先整理舊有的邊線,在 topology.js 中的 edgeIDWithVertices 函數可以幫助我們透過頂點的ID查找邊線。
var edgeAC = topology.edgeIDWithVertices(vertexIdA, vertexIdC);
var edgeBC = topology.edgeIDWithVertices(vertexIdB, vertexIdC);
var edgeAE = topology.edgeIDWithVertices(vertexIdA, vertexIdE);
var edgeBE = topology.edgeIDWithVertices(vertexIdB, vertexIdE);
接著要創造新的四條邊線,兩條是由線段 AB 所拆分出來的 AD 與 BD;另外兩條是新增的線段 CD 與 ED 。
var edgeAD = topology.create('edge').ID;
var edgeBD = topology.create('edge').ID;
var edgeCD = topology.create('edge').ID;
var edgeED = topology.create('edge').ID;
四個面都是需要重新創造的面,舊有的之後會直接刪除。
var faceACD = topology.create('face').ID;
var faceCBD = topology.create('face').ID;
var faceAED = topology.create('face').ID;
var faceEBD = topology.create('face').ID;
在建立新的網格之前,為了確認是否與舊的網格方向一致,需要先計算舊有的網格法向量,與其中一片新的網格法向量,首先建立一個法向量計算函數。
Refinement.prototype.computeNormal = function (vA, vB, vC) {
var cb = new THREE.Vector3();
var ab = new THREE.Vector3();
cb.subVectors(vC, vB);
ab.subVectors(vA, vB);
cb.cross(ab);
return cb.normalize();
};
接著筆者取上方的網格與第一片新網格進行法向量的計算。
//計算原始網格法向量
var topNormal = self.computeNormal(
topology.vertex[faceTop.vertexIDs[0]].vector3,
topology.vertex[faceTop.vertexIDs[1]].vector3,
topology.vertex[faceTop.vertexIDs[2]].vector3
);
//計算第一個分割面法向量
var subNormal = self.computeNormal(
topology.vertex[vertexIdA].vector3,
topology.vertex[vertexIdC].vector3,
topology.vertex[vertexIdD].vector3
);
然後小心翼翼的按照順序將每一片新建的網格資訊設定好,一個順向填入,一個逆向填入,並透過內積去計算是否方向是一致的。
//Va, Vb, Vc, Eab, Ebc, Eca, F
if (topNormal.dot(subNormal) >= 0) {
topology.addTriangleData(vertexIdC, vertexIdD, vertexIdA, edgeCD, edgeAD, edgeAC, faceACD);
topology.addTriangleData(vertexIdA, vertexIdD, vertexIdE, edgeAD, edgeED, edgeAE, faceAED);
topology.addTriangleData(vertexIdE, vertexIdD, vertexIdB, edgeED, edgeBD, edgeBE, faceEBD);
topology.addTriangleData(vertexIdB, vertexIdD, vertexIdC, edgeBD, edgeCD, edgeBC, faceCBD);
} else {
topology.addTriangleData(vertexIdC, vertexIdA, vertexIdD, edgeAC, edgeAD, edgeCD, faceACD);
topology.addTriangleData(vertexIdA, vertexIdE, vertexIdD, edgeAE, edgeED, edgeAD, faceAED);
topology.addTriangleData(vertexIdE, vertexIdB, vertexIdD, edgeBE, edgeBD, edgeED, faceEBD);
topology.addTriangleData(vertexIdB, vertexIdC, vertexIdD, edgeBC, edgeCD, edgeBD, faceCBD);
}
迴圈的最後將舊的拓樸資訊刪除,就完成完整的拓樸資訊建構。
topology.remove(edge);
topology.remove(faceTop);
topology.remove(faceBottom);
最後透過 topology.js 的 convertToGeometry 函數生成新的 Geometry 後,將舊的 mesh 刪除,再增加新的 Geometry 就完成了。
var newGeometry = topology.convertToGeometry();
self.viewer.scene.remove(self.viewer.meshs[0]);
self.viewer.add(newGeometry);
由於新的三角網格是 Geometry 資料格式,並不是 BufferGeometry 因此需要在 Viewer.js 的 add 函數中判斷一下型態。
var geometry = null;
if (bufferGeometry.constructor.name === 'BufferGeometry') {
geometry = new THREE.Geometry().fromBufferGeometry(bufferGeometry);
} else {
geometry = bufferGeometry;
}
在 topology.js 的 convertToGeometry 函數會將顏色資訊物件重複放進去,造成即時高亮網格功能出錯,因此需要改為拷貝的方式將基本顏色重新覆蓋掉。
geometry.faces[i].color = baseColor.clone();
補充說明一下,此範例是全域網格進行細分,之前的 monkey.stl 檔案在部分區域會有一些非封閉網格,導致細分計算失敗的問題是正常的,可以改用 CubeBinary.STL 或是 hat.stl 檔案都可以正常計算。
使用 CubeBinary.STL 實體模型測試結果
https://github.com/QQBoxy/threecad/tree/master/models
https://qqboxy.github.io/threecad/public/example18.html
https://github.com/QQBoxy/threecad/blob/master/client/example18/Editor.js
這次內容比較多,花了超出預算的時間在撰寫,希望大家能夠好好吸收,這篇範例的原理應該是花點時間就能夠想通的。