1. 程式人生 > >three.js 06-06 之 Binary Operations 幾何體

three.js 06-06 之 Binary Operations 幾何體

    本篇將要介紹的是在 three.js 中如何使用二元操作來自由組合物體。為此,我們需要引入一個 three.js 的擴充套件 ThreeBSP.js 庫,你可以從網上找到這個庫,譬如從 http://download.csdn.net/download/zhulx_sz/10202730 中下載,這個資源裡面提供的這個 ThreeBSP.js 庫,已經針對 three.js r8x 進行了小小的修復,包括一些警告資訊都消除了,建議從這裡下載。當然,你可以從官方下載,但必須自己來編譯,這樣才能引入到你的工程中使用,網址為 https://github.com/skalnik/threebsp

    所謂的二元操作是指,將各種標準的幾何體(如 THREE.BoxGeometry、THREE.SphereGeometry 等)組合在一起,並通過 subtract(相減)、intersect(相交)、union(聯合)等運算操作後創建出一個新的幾何體。下面我們給出這幾種運算操作的大致定義:

操作 描述
intersect(相交) 此運算操作可以在兩個幾何體的交集上創建出新的幾何體。兩個幾何體相互交疊的部分就是新的幾何體
subtract(相減) 此運算操作可以在第一個幾何體中減去兩個幾何體交疊的部分,剩下的部分就是新的幾何體
union(聯合) 此運算操作可以將兩個幾何體聯合在一起,從而創建出新的幾何體
    為了便於理解以上這幾種操作,我們先給出一個完整的示例,程式碼如下:
<!DOCTYPE html>
<html>
<head>
    <title>示例 06.08 - Binary Operations Geometry</title>
	<script src="../build/three.js"></script>
	<script src="../build/js/controls/OrbitControls.js"></script>
	<script src="../build/js/libs/stats.min.js"></script>
	<script src="../build/js/libs/dat.gui.min.js"></script>
	<script src="../jquery/jquery-3.2.1.min.js"></script>
	
	<script src="../build/js/ThreeBSP.js"></script>
	<script src="../build/js/libs/spin.js"></script>
    <style>
        body {
            /* 設定 margin 為 0,並且 overflow 為 hidden,來完成頁面樣式 */
            margin: 0;
            overflow: hidden;
        }
		/* 統計物件的樣式 */
		#Stats-output {
			position: absolute;
			left: 0px;
			top: 0px;
		}
    </style>
</head>
<body>

<!-- 用於 WebGL 輸出的 Div -->
<div id="webgl-output"></div>
<!-- 用於統計 FPS 輸出的 Div -->
<div id="stats-output"></div>

<!-- 執行 Three.js 示例的 Javascript 程式碼 -->
<script type="text/javascript">

	var scene;
	var camera;
	var render;
	var webglRender;
	//var canvasRender;
	var controls;
	var stats;
	var guiParams;
	
	var ground;
	var sphere1;
	var sphere2;
	var cube;
	var result;
	
	var spinner;
	
	var ambientLight;
	var spotLight;
	var axesHelper;
	//var cameraHelper;

    $(function() {
		stats = initStats();
		scene = new THREE.Scene();
		
		webglRender = new THREE.WebGLRenderer( {antialias: true, alpha: true} ); // antialias 抗鋸齒
		webglRender.setSize(window.innerWidth, window.innerHeight);
		webglRender.setClearColor(0xeeeeee, 1.0);
		webglRender.shadowMap.enabled = true; // 允許陰影投射
		render = webglRender;
		
		camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 2147483647); // 2147483647
		camera.position.set(-45.5, 68.2, 90.9);
		
		var target = new THREE.Vector3(10, 0 , 0);
		controls = new THREE.OrbitControls(camera, render.domElement);
		controls.target = target;
		camera.lookAt(target);
		
		$('#webgl-output')[0].appendChild(render.domElement);
		window.addEventListener('resize', onWindowResize, false);
		
		// 加入一個座標軸:X(橙色)、Y(綠色)、Z(藍色)
		axesHelper = new THREE.AxesHelper(60);
		scene.add(axesHelper);
		
		ambientLight = new THREE.AmbientLight(0x0c0c0c);
		scene.add(ambientLight);
		
		spotLight = new THREE.SpotLight(0xffffff);
        spotLight.position.set(0, 260, 230);
		spotLight.shadow.mapSize.width = 5120; // 必須是 2的冪,預設值為 512
		spotLight.shadow.mapSize.height = 5120; // 必須是 2的冪,預設值為 512
        spotLight.castShadow = true;
        scene.add(spotLight);
		//cameraHelper = new THREE.CameraHelper(spotLight.shadow.camera);
		//scene.add(cameraHelper);
		
		// sphere1
		sphere1 = createMesh(new THREE.SphereGeometry(5, 20, 30));
        sphere1.position.x = -2;

		// sphere1
        sphere2 = createMesh(new THREE.SphereGeometry(5, 20, 30));
        sphere2.position.set(3, 0, 0);

		// cube
        cube = createMesh(new THREE.BoxGeometry(5, 5, 5));
        cube.position.x = -7;

        // 加入到場景中
        scene.add(sphere1);
        scene.add(sphere2);
        scene.add(cube);
		
		/** 用來儲存那些需要修改的變數 */
		guiParams = new function() {
			this.rotationSpeed = 0.02;
			
			//this.actionSphere = 'none';
			//this.actionCube = 'none';
			this.showResult = function() {
				redrawResult();
			};
			this.rotateResult = false;
			this.hideWireframes = false;
			
			this.sphere1 = {
				posx: 3,
				posy: 0,
				posz: 0,
				scale: 1
			}
			this.sphere2 = {
				posx: 3,
				posy: 0,
				posz: 0,
				scale: 1,
				actionSphere: 'subtract'
			}
			this.cube = {
				posx: 3,
				posy: 0,
				posz: 0,
				scalex: 1,
				scaley: 1,
				scalez: 1,
				actionCube: 'none'
			}
		}
		/** 定義 dat.GUI 物件,並繫結 guiParams 的幾個屬性 */
		var gui = new dat.GUI();
		var folder = gui.addFolder('sphere1');
		//folder.open();
		folder.add(guiParams.sphere1, "posx", -15, 15, 0.1).onChange(function(e){
			sphere1.position.set(guiParams.sphere1.posx, guiParams.sphere1.posy, guiParams.sphere1.posz);
		});
		folder.add(guiParams.sphere1, "posy", -15, 15, 0.1).onChange(function(e){
			sphere1.position.set(guiParams.sphere1.posx, guiParams.sphere1.posy, guiParams.sphere1.posz);
		});
		folder.add(guiParams.sphere1, "posz", -15, 15, 0.1).onChange(function(e){
			sphere1.position.set(guiParams.sphere1.posx, guiParams.sphere1.posy, guiParams.sphere1.posz);
		});
		folder.add(guiParams.sphere1, "scale", -15, 15, 0.1).onChange(function(e){
			sphere1.scale.set(guiParams.sphere1.scale, guiParams.sphere1.scale, guiParams.sphere1.scale);
		});
		
		folder = gui.addFolder('sphere2');
		folder.open();
		folder.add(guiParams.sphere2, "posx", -15, 15, 0.1).onChange(function(e){
			sphere2.position.set(guiParams.sphere2.posx, guiParams.sphere2.posy, guiParams.sphere2.posz);
		});
		folder.add(guiParams.sphere2, "posy", -15, 15, 0.1).onChange(function(e){
			sphere2.position.set(guiParams.sphere2.posx, guiParams.sphere2.posy, guiParams.sphere2.posz);
		});
		folder.add(guiParams.sphere2, "posz", -15, 15, 0.1).onChange(function(e){
			sphere2.position.set(guiParams.sphere2.posx, guiParams.sphere2.posy, guiParams.sphere2.posz);
		});
		folder.add(guiParams.sphere2, "scale", -15, 15, 0.1).onChange(function(e){
			sphere2.scale.set(guiParams.sphere2.scale, guiParams.sphere2.scale, guiParams.sphere2.scale);
		});
		folder.add(guiParams.sphere2, "actionSphere", ['subtract', 'intersect', 'union', 'none']);
		
		folder = gui.addFolder('cube');
		folder.open();
		folder.add(guiParams.cube, "posx", -15, 15, 0.1).onChange(function(e){
			cube.position.set(guiParams.cube.posx, guiParams.cube.posy, guiParams.cube.posz);
		});
		folder.add(guiParams.cube, "posy", -15, 15, 0.1).onChange(function(e){
			cube.position.set(guiParams.cube.posx, guiParams.cube.posy, guiParams.cube.posz);
		});
		folder.add(guiParams.cube, "posz", -15, 15, 0.1).onChange(function(e){
			cube.position.set(guiParams.cube.posx, guiParams.cube.posy, guiParams.cube.posz);
		});
		folder.add(guiParams.cube, "scalex", -15, 15, 0.1).onChange(function(e){
			cube.scale.set(guiParams.cube.scalex, guiParams.cube.scaley, guiParams.cube.scalez);
		});
		folder.add(guiParams.cube, "scaley", -15, 15, 0.1).onChange(function(e){
			cube.scale.set(guiParams.cube.scalex, guiParams.cube.scaley, guiParams.cube.scalez);
		});
		folder.add(guiParams.cube, "scalez", -15, 15, 0.1).onChange(function(e){
			cube.scale.set(guiParams.cube.scalex, guiParams.cube.scaley, guiParams.cube.scalez);
		});
		folder.add(guiParams.cube, "actionCube", ['subtract', 'intersect', 'union', 'none']);
		
		gui.add(guiParams, "showResult");
		gui.add(guiParams, "rotateResult");
		gui.add(guiParams, "hideWireframes").onChange(function(e){
			if (e) {
				sphere1.visible = false;
				sphere2.visible = false;
				cube.visible = false;
			} else {
				sphere1.visible = true;
				sphere2.visible = true;
				cube.visible = true;
			}
		});
		
		renderScene();
    });
	
	/** 渲染場景 */
	function renderScene() {
		stats.update();
		rotateMesh(); // 旋轉物體
		
		requestAnimationFrame(renderScene);
		render.render(scene, camera);
	}
	
	/** 初始化 stats 統計物件 */
	function initStats() {
		stats = new Stats();
		stats.setMode(0); // 0 為監測 FPS;1 為監測渲染時間
		$('#stats-output').append(stats.domElement);
		return stats;
	}
	
	/** 當瀏覽器視窗大小變化時觸發 */
	function onWindowResize() {
		camera.aspect = window.innerWidth / window.innerHeight;
		camera.updateProjectionMatrix();
		render.setSize(window.innerWidth, window.innerHeight);
	}
	
	/** 旋轉物體 */
	var step = 0;
	function rotateMesh() {
		step += guiParams.rotationSpeed;
		scene.traverse(function(mesh) {
			if (mesh === result && guiParams.rotateResult) {
				//mesh.rotation.x = step;
				mesh.rotation.y = step;
				//mesh.rotation.z = step;
			}
		});
	}
	
	function createMesh(geom) {
		var wireFrameMat = new THREE.MeshBasicMaterial({
			//transparency: true,
			opacity: 0.5,
			wireframe: true,
			wireframeLinewidth: 0.5
		});
		return new THREE.Mesh(geom, wireFrameMat);
	}
	
	function redrawResult() {
		showSpinner();
		
		setTimeout(function() {
			scene.remove(result);
			
			var sphere1BSP = new ThreeBSP(sphere1);
			var sphere2BSP = new ThreeBSP(sphere2);
			var cubeBSP = new ThreeBSP(cube);
			var resultBSP;
			
			// 第一步:球體
			switch(guiParams.sphere2.actionSphere) {
				case 'subtract':
					resultBSP = sphere1BSP.subtract(sphere2BSP);
					break;
				case 'intersect':
					resultBSP = sphere1BSP.intersect(sphere2BSP);
					break;
				case 'union':
					resultBSP = sphere1BSP.union(sphere2BSP);
					break;
				case 'none':
					// 無操作
					break;
			}
			
			// 第二步:方塊
			if (!resultBSP) resultBSP = sphere1BSP;
			switch(guiParams.cube.actionCube) {
				case 'subtract':
					resultBSP = resultBSP.subtract(cubeBSP);
					break;
				case 'intersect':
					resultBSP = resultBSP.intersect(cubeBSP);
					break;
				case 'union':
					resultBSP = resultBSP.union(cubeBSP);
					break;
				case 'none':
					// noop
					break;
			}
			
			if (guiParams.sphere2.actionSphere === 'none' && guiParams.cube.actionCube === 'none') {
				// 無操作
			} else {
				result = resultBSP.toMesh();
				result.geometry.computeFaceNormals();
				result.geometry.computeVertexNormals();
				scene.add(result);
			}
			
			hideSpinner(spinner);
		}, 100);
	}
	
	/** 顯示等待畫面 */
	function showSpinner() {
		var opts = {
			lines: 13, // The number of lines to draw
			length: 20, // The length of each line
			width: 10, // The line thickness
			radius: 30, // The radius of the inner circle
			corners: 1, // Corner roundness (0..1)
			rotate: 0, // The rotation offset
			direction: 1, // 1: clockwise, -1: counterclockwise
			color: '#000', // #rgb or #rrggbb or array of colors
			speed: 1, // Rounds per second
			trail: 60, // Afterglow percentage
			shadow: false, // Whether to render a shadow
			hwaccel: false, // Whether to use hardware acceleration
			className: 'spinner', // The CSS class to assign to the spinner
			zIndex: 2e9, // The z-index (defaults to 2000000000)
			top: 'auto', // Top position relative to parent in px
			left: 'auto' // Left position relative to parent in px
		};
		var target = $('#webgl-output')[0];
		//var target = document.getElementById('webgl-output');
		spinner = new Spinner(opts).spin(target);
		return spinner;
	}
	
	/** 隱藏等待畫面 */
	function hideSpinner(spinner) {
		spinner.stop();
	}

</script>
</body>
</html>
    在這個示例中,我們首先添加了三個物體:一個立方體和兩個球體。初始場景裡中間那個 sphere1 球體,所有操作都會在這個物件上進行。它的右邊是 sphere2 球體,左邊是 cube 立方體。讀者可以在 sphere2 及 cube 上指定四種操作中的一種,即:subtract、intersect、union 和 none(無操作)。這些操作都是基於 sphere1 的。

    其中最核心的部分是 showResult() 函式,在這個函式中,我們首先將各種待運算操作的網格物件包裝成 ThreeBSP 物件,只有這樣才能進行 subtract、intersect 和 union 操作。隨後我們在操作結果物件 resultBSP 上呼叫 toMesh() 函式,並通過呼叫 computeFaceNormals() 和 computeVertexNormals() 函式以確保所有的法向量可以正確的計算出來。之所以要呼叫這兩個函式,是因為在執行二元操作後,幾何體中頂點和麵的法向量可能會被改變,而 three.js 在著色時會用到面法向量和頂點法向量。所以顯式呼叫這兩個函式可以保證新生成的物件著色光滑、渲染正確,並能加入到場景 scene 中。

    特別地,這種 union 操作的方法並不是很好的,我們後面將會給出一種更好的 three.js 的合併方法。因為此處這種方法,你在旋轉時會發現,中心還是在原來 sphere1 那個球上,而不在 union 後的新幾何體的中心。其實另外兩種操作也存在類似的問題。

未完待續···

相關推薦

three.js 06-06 Binary Operations 幾何體

    本篇將要介紹的是在 three.js 中如何使用二元操作來自由組合物體。為此,我們需要引入一個 three.js 的擴充套件 ThreeBSP.js 庫,你可以從網上找到這個庫,譬如從 http://download.csdn.net/download/zhulx_

three.js 05-02 CircleGeometry 幾何體

    本篇我們來介紹 CircleGeometry 幾何體,其常用屬性如下表所示: 屬性 描述 radius (半徑) 必選。此屬性指定圓的半徑 segments (分段) 可選。此屬性定義建立圓所用的面數量。最少3個,預設為8個。值越大,圓就越光滑 thetaStart

three.js入門系列視角和輔助線

假設你已經建立好了three.js的開發環境(我是寫在vue專案中的),那麼接下來,從頭開始演示是如何用three.js來構建3D圖形的。(筆記本寫的程式碼,螢幕小,所以為了能夠整屏看到完整程式碼,就將字型縮小了,如果覺得看不清的,可以另行放大) 一、頁面DOM結構 正如你所見,這就是一

three.js入門系列旋轉的圓臺、球體、正方體

先來張圖: 一、調整機位和輔助線 由上述程式碼可知,現在的機位是三維座標軸上的點(2,2,2),方框的那一句很重要,有了這一句,你將獲得上帝視角!!! 接下來新增輔助線(立體空間三軸): 這樣就添加了一個軸輔助線,由於我們是站在(2,2,2),所以看到的輔助線是這樣的: 這是一個標

three.js入門系列光和陰影

初中物理教過我們鏡面反射和漫反射,這是由於物體的材質直接導致的。 在three.js中,由於物體的材料不同,對於光源的反應也是不一樣的,下面就讓我們一探究竟。 一、材料 據Three.js中描述,有兩種材料能對光源有所反應: 就是圖中箭頭標識的兩種材料。 二、檢驗 編輯前例,設定光源位置:

three.js入門系列光源

首先建立場景來試驗各種光源帶來的不同效果: 一、錐形光源(聚光燈) SpotLight 接下來縮小範圍(π/7): 二、基礎光源(環境光) AmbientLight 上例中沒有新增環境光,使得周圍黑漆漆的,下面就新增環境光: 效果: 三、點光源(照射所有方向) P

three.js入門系列材質

一、基礎網孔材料 MeshBasicMaterial 圖示(光源是(0,1,0)處的點光源): 二、深度網孔材料 MeshDepthMaterial (由於只是改了材料名,程式碼將不重複貼出) 在這裡,有必要提一下遠景相機的屬性了: 大概就是這麼個意思,下面,我們把上述兩個引

three.js入門系列粒子系統

其實程式碼很簡單,也很容易懂(我用的是r99版本的three.js,目前網上大多數demo是60或者80的版本,其中的一些api已經廢棄,如下是r99版本支援的寫法): 注:渲染器是WebGl渲染器 如上的程式碼,你將看到如下畫面: 但是這麼多“粒子”都是正方形的啊,哪來的雪花呢,不急

three.js入門系列匯入拓展類

先來看一下three.js包的目錄結構: 我們使用的時候,可以一次性import所有的功能,也可以按需引入,全依賴three.module.js這個檔案對three.js的功能作了模組化處理; 但是,該模組化處理的功能僅僅是引入了src下面的所有功能類,實際開發中,我們還需要拓展包(examples)

three.js 02-02 使用幾何與網格物件

    上一篇我們對場景 Scene 物件進行了單獨講解。本篇我們將來看看 Three.js 中的幾個標準的幾何體的用法。我們先上一段完整的程式碼,如下所示: <!DOCTYPE html> <html> <head> <

three.js 04-07 MeshPhongMaterial 材質

    上篇我們已經介紹了 three.js 中高階材質中的 MeshLambertMaterial 材質。本篇將要介紹的是另一種與之對應的高階材質 MeshPhongMaterial 材質。通過它可以建立一種光亮表面的材質效果。這種材質的屬性基本跟 MeshLambert

three.js 09-02 選擇物件

    我們在討論相機和動畫之前,先來看看物件的選擇實現,儘管這個跟動畫沒有直接的關係,但瞭解這個實現之後將會是一個很有益的補充。    在給出示例程式碼之前,我們先來看一下核心程式碼:function onDocumentMouseDown(event) { event.

three.js 07-04 Points 粒子系統

    前面都是在介紹 如何通過 THREE.Sprite 來構建粒子系統。本篇我們來介紹一下如何通過 THREE.Points 及 THREE.PointsMaterial 來構建粒子系統。我們先來看一個完整的示例,程式碼如下所示: <!DOCTYPE html&g

three.js 02-04 網格物件函式及屬性

    上一篇中,我們對自定義幾何體的相關知識做了一個簡單介紹,並講解了如何克隆(複製)一個幾何體。本篇我們要介紹的是關於 Mesh 網格物件常用的一些相關函式和屬性。照例,我們先上一個完整的示例,程式碼如下: <!DOCTYPE html> <html

three.js 08-02 網格合併

    上一篇文中,我們介紹到利用 THREE.Group 物件可以很容易操縱和管理大量的網格。但是當網格物件的數量非常多時,效能就會成為一個瓶頸。實際上在組裡的每個物件還是獨立的,需要對它們分別進行處理和渲染。     在舊版 three.js 中,有個叫 THREE.G

three.js 04-09 LineBasicMaterial 材質

    關於 three.js 中的材質部分介紹就快講完了。接下來要介紹關於線段幾何的兩種材質:LineBasicMaterial 和 LineDashedMatertial。如下所示: LineBasicMaterial:通過線段基礎材質,可以設定線段的顏色、寬度、斷點

three.js 數學方法Box3

從今天開始郭先生就會說一下three.js 的一些數學方法了,像Box3、Plane、Vector3、Matrix3、Matrix4當然還有尤拉角和四元數。今天說一說three.js的Box3方法(Box2是Box3的二維版本,可以參考Box3)。線上案例點選部落格原文。 Box3在3D空間中表示一個包圍盒。

three.js 數學方法Plane

今天郭先生就來繼續說一說three.js數學方法中的plane(平面)。在三維空間中無限延伸的二維平面,平面方程用單位長度的法向量和常數表示。構造器為Plane( normal : Vector3, constant : Float )。第一個引數為平面的法向量,既然是法向量也就預示著這個平面是有方向之分的,

three.js 數學方法Vector3

今天郭先生來說一說three.js的Vector3,該類表示的是一個三維向量(3D vector)。 一個三維向量表示的是一個有順序的、三個為一組的數字組合(標記為x、y和z),可被用來表示很多事物,它的建構函式為Vector3( x : Float, y : Float, z : Float )x - 向量

three.js 數學方法Matrix3

今天郭先生來說一說three.js的三維矩陣,這塊知識需要結合線性代數的一些知識,畢業時間有點長,線性代數的知識大部分都還給了老師。於是一起簡單的複習了一下。所有的計算都是使用列優先順序進行的。然而,由於實際的排序在數學上沒有什麼不同, 而且大多數人習慣於以行優先順序考慮矩陣,所以three.js文件以行為主