iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 8
0
Modern Web

在Three.js探索CAD的奧秘系列 第 8

Day 8 : 模組化CAD平台

前言

模組化,也就是Module化,是將我們所需要的功能建立成一個模組增加重用性。在我們的應用是為了建立一個CAD平台,將Three.js模組化可以更方便的擴充各種CAD功能,才能夠在功能越來越複雜的情況下還能夠有條理的進行擴充。本文中筆者習慣使用prototype的方式分離功能項,後續將會大量使用此方法進行函數的整理。

將Three.js功能模組化

延續上一次的程式,拷貝一份example5,在 /client/example5/ 目錄下建立一個 Viewer.js 檔案,在裡頭建立一個Viewer的函數,並且加入一個引數 container 用來引入要提供給Three.js創建canvas的容器。

var Viewer = function (container) {
    this.container = container;
    this.scene = null;
    this.camera = null;
    this.renderer = null;
    this.light = null;
    this.cameraFov = 60;
    this.meshs = [];
};

從原本的繪圖程式中,抽出場景設定、相機設定、渲染器設定、光源設定、執行動畫整理到init內,其中anime只保留函式呼叫。

Viewer.prototype.init = function () {
    var self = this;
    
    //場景設定
    self.scene = new THREE.Scene();
    //相機設定
    self.camera = new THREE.PerspectiveCamera(self.cameraFov, window.innerWidth / window.innerHeight, 0.1, 1000);

    //渲染器設定
    self.renderer = new THREE.WebGLRenderer();
    self.renderer.setSize(window.innerWidth, window.innerHeight);
    self.container.appendChild(self.renderer.domElement);

    //光源設定
    var ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
    self.scene.add(ambientLight); //加入環境光源
    var pointLight = new THREE.PointLight(0xffffff, 0.8, 0);
    pointLight.position.set(50, 50, 50);
    self.scene.add(pointLight); //加入點光源

    self.camera.position.z = 50;

    //執行動畫
    self.animate();
};

接著建立一個animate函數將原本的程式都整理進去,但由於requestAnimationFrame函數是隸屬於window底下的,它在執行的時候不會帶著我們的Viewer物件進去,因此要將this給bind進去才能夠正常抓到場景、相機跟渲染器。

Viewer.prototype.animate = function () {
    var self = this;
    //循環動畫
    requestAnimationFrame(self.animate.bind(self));
    self.renderer.render(self.scene, self.camera);
};

設置好基本的環境以後,要新增一個add函數將用來加入三角網格,並且函數的引數加入geometry,然後將材質與網格物件設定放進來。

Viewer.prototype.add = function (geometry) {
    var self = this;

    //材質設定
    var material = new THREE.MeshPhongMaterial({
        color: Math.floor(Math.random() * 16777215) //Random color
    });

    //網格物件設定
    var mesh = new THREE.Mesh(geometry, material);
    self.scene.add(mesh);
};

為了將Viewer模組化,因此在程式的最下方加上一個module.exports,這樣我們就可以在別的程式將Viewer給import進來。

module.exports = Viewer;

接著打開 /server/views/example5/index.html 修改HTML將原本的app標籤刪掉,我們改創建一個openfile按鈕與viewer容器提供放置功能。

<div>
    <label for="openfile">
        <div class="openfile" style="cursor: pointer;"></div>
    </label>
    <input id="openfile" type="file" accept=".stl" style="display: none;" />
</div>
<div id="viewer"></div>

回到 /client/example5/index.js 主程式,將創建好的Viewer給import進來,接著new一個Viewer函數並提供它一個HTML容器,並且先給viewer執行初始化。

import Viewer from './Viewer';

var viewer = new Viewer($("#viewer"));
viewer.init();

最後將原本onchange函式指派給openfile標籤,並且將呼叫draw函數的部分改為呼叫viewer.add函數,這樣就可以完成基本的模組化,如下圖。

$("#openfile").onchange = function (e) {
    var file = e.target.files[0];
    if (e.target.files.length == 1) {
        var reader = new FileReader();
        reader.onloadend = function (e) {
            var loader = new STLLoader();
            var geometry = loader.parse(e.target.result);
            viewer.add(geometry);
        };
        reader.readAsBinaryString(file);
    }
};

https://ithelp.ithome.com.tw/upload/images/20171227/20107175eMtDFHHpPP.png
模組化後讀入monkey.stl

同一場景讀入更多模型

在前一個階段已經將功能模組化了,但是還欠缺完善讀取多筆STL檔案的能力,首先是場景內的網格管理方式,眼尖的朋友可能已經發現在函數宣告的時候,有一個meshs參數並沒有被使用到,接著我們要將Viewer模組中加入這部分,請在add函數最尾端加上push網格,如此一來就能夠在Viewer內任意的使用mesh物件了。

self.meshs.push(mesh);

接著建立一個清除用的函數 clear,透過一個簡單的for迴圈就能夠移除場景中的網格物件mesh。

Viewer.prototype.clear = function (geometry) {
    var self = this;

    for (var key in self.meshs) {
        self.scene.remove(self.meshs[key]);
    }
};

然後同時也在 /server/views/example5/index.html 加入一顆用來清除的按鈕。

<div id="deletefile" class="deletefile" style="cursor: pointer;"></div>

接著我們將按鈕綁定clear函數。

$("#deletefile").onclick = function (e) {
    viewer.clear();
};

最後要注意的是,onchange函式中需要加入一行,讓每一次加入geometry後可以清除選取的檔案,下一次的選取檔案才會再進入到onchange函式中喔,完成的結果如下圖:

$("#openfile").value = "";

https://i.imgur.com/QBkRM0X.gif
讀取、再讀取然後清除的範例

同場加映

為了讓畫布能夠隨著調整視窗大小而變動,我們在init加入一個resize函數監視瀏覽器事件。

window.addEventListener('resize', self.onResize.bind(self), false);

並放入onResize事件提供resize呼叫使用,在這個函數中更新相機的比例與渲染器的大小。

Viewer.prototype.onResize = function () {
    var self = this;
    self.camera.aspect = window.innerWidth / window.innerHeight;
    self.camera.updateProjectionMatrix();
    self.renderer.setSize(window.innerWidth, window.innerHeight);
};

https://i.imgur.com/fdYKneF.gif
視窗大小變更效果

範例原始碼

https://github.com/QQBoxy/threecad/tree/master/client/example5

範例模型檔案

https://github.com/QQBoxy/threecad/tree/master/models

後記

這一次的內容稍微多了一些,大家是不是還能夠正常吸收呢? 請加緊腳步跟上我,目前還是前導環境,我們還沒有進入CAD更多元的應用呢,所以目前為止的內容務必要好好瞭解清楚喔!


上一篇
Day 7 : 渲染讀取的三角網格
下一篇
Day 9 : 基本相機控制
系列文
在Three.js探索CAD的奧秘30

尚未有邦友留言

立即登入留言