Three.js ExtrudeGeometry.js 源碼研究

ExtrudeGeometry.js 是Three.js 一個幾何體類,可以把自己創建的或者從svg導入的平面2D圖形拉伸爲幾何體。最能體現這個幾何體類強大的例子是 http://www.wjceo.com/blog/threejs/2018-02-12/46.html
把其中的
斜角厚度bevelThickness = 3,
斜角尺寸bevelSize = 1.4,
斜角分段數bevelSegments = 1,
曲線分段數curveSegments = 2, bevelEnabled = true

從這個例子中可以看到,ExtrudeGeometry可以把任意凸多邊形或者凹多邊形拉伸,如果啓用斜角,斜角總是向幾何體的中心傾斜。

對於凸多邊形,讓拉伸後產生的斜角都向幾何中心傾斜是比較容易實現的;但是對於凹多邊形則非常麻煩,我最感興趣的就在於ExtrudeGeometry.js ,是如何讓任意凹多邊形經過拉伸後,產生的斜角一律向幾何中心傾斜,經過閱讀代碼,發現最關鍵的函數是

// Find directions for point movement
		function getBevelVec( inPt, inPrev, inNext ) {
			// computes for inPt the corresponding point inPt' on a new contour
			//   shifted by 1 unit (length of normalized vector) to the left
			// if we walk along contour clockwise, this new contour is outside the old one
			//
			// inPt' is the intersection of the two lines parallel to the two
			//  adjacent edges of inPt at a distance of 1 unit on the left side.

			var v_trans_x, v_trans_y, shrink_by; // resulting translation vector for inPt

			// good reading for geometry algorithms (here: line-line intersection)
			// http://geomalgorithms.com/a05-_intersect-1.html

			var v_prev_x = inPt.x - inPrev.x,
				v_prev_y = inPt.y - inPrev.y;
			var v_next_x = inNext.x - inPt.x,
				v_next_y = inNext.y - inPt.y;

			var v_prev_lensq = ( v_prev_x * v_prev_x + v_prev_y * v_prev_y );

			// check for collinear edges
			var collinear0 = ( v_prev_x * v_next_y - v_prev_y * v_next_x );

			if ( Math.abs( collinear0 ) > Number.EPSILON ) {

				// not collinear

				// length of vectors for normalizing

				var v_prev_len = Math.sqrt( v_prev_lensq );
				var v_next_len = Math.sqrt( v_next_x * v_next_x + v_next_y * v_next_y );

				// shift adjacent points by unit vectors to the left

				var ptPrevShift_x = ( inPrev.x - v_prev_y / v_prev_len );
				var ptPrevShift_y = ( inPrev.y + v_prev_x / v_prev_len );

				var ptNextShift_x = ( inNext.x - v_next_y / v_next_len );
				var ptNextShift_y = ( inNext.y + v_next_x / v_next_len );

				// scaling factor for v_prev to intersection point

				var sf = ( ( ptNextShift_x - ptPrevShift_x ) * v_next_y -
						( ptNextShift_y - ptPrevShift_y ) * v_next_x ) /
					( v_prev_x * v_next_y - v_prev_y * v_next_x );

				// vector from inPt to intersection point

				v_trans_x = ( ptPrevShift_x + v_prev_x * sf - inPt.x );
				v_trans_y = ( ptPrevShift_y + v_prev_y * sf - inPt.y );
				// Don't normalize!, otherwise sharp corners become ugly
				//  but prevent crazy spikes
				var v_trans_lensq = ( v_trans_x * v_trans_x + v_trans_y * v_trans_y );
				if ( v_trans_lensq <= 2 ) {

					return new Vector2( v_trans_x, v_trans_y );

				} else {

					shrink_by = Math.sqrt( v_trans_lensq / 2 );

				}

			} else {

				// handle special case of collinear edges

				var direction_eq = false; // assumes: opposite
				if ( v_prev_x > Number.EPSILON ) {

					if ( v_next_x > Number.EPSILON ) {

						direction_eq = true;

					}

				} else {

					if ( v_prev_x < - Number.EPSILON ) {

						if ( v_next_x < - Number.EPSILON ) {

							direction_eq = true;

						}

					} else {

						if ( Math.sign( v_prev_y ) === Math.sign( v_next_y ) ) {

							direction_eq = true;

						}

					}

				}

				if ( direction_eq ) {

					// console.log("Warning: lines are a straight sequence");
					v_trans_x = - v_prev_y;
					v_trans_y = v_prev_x;
					shrink_by = Math.sqrt( v_prev_lensq );

				} else {

					// console.log("Warning: lines are a straight spike");
					v_trans_x = v_prev_x;
					v_trans_y = v_prev_y;
					shrink_by = Math.sqrt( v_prev_lensq / 2 );

				}

			}

			return new Vector2( v_trans_x / shrink_by, v_trans_y / shrink_by );

		}

關於如何理解這個函數,源碼給出鏈接 // http://geomalgorithms.com/a05-_intersect-1.html
但是我始終打不開這個鏈接,只好自己嘗試分析了。

getBevelVec函數的三個參數 ( inPt, inPrev, inNext ) , 分別是輪廓的當前點inPt,前一個點inPrev,後一個點inNext , 當輪廓擴大一個單位寬度後,當前點 inPt 經過拉伸後,在 z值 不同的xy平面產生了 新的點 inPt2

getBevelVec函數返回一個二維向量, 該向量等於 inPt2 - inPt。 其中 輪廓 指代 一個順時針順序排列 Vector2 序列,假設序列長度爲6, inPt 在此序列的索引爲 3, inPrev的索引是2, inNext的索引是4;
如果inPt 在此序列的索引爲 0, inPrev的索引是5, inNext的索引是1;
如果inPt 在此序列的索引爲 5, inPrev的索引是4, inNext的索引是0;

getBevelVec函數 會先判斷平面三點 inPt, inPrev, inNext 是否共線,如果共線,則爲特殊情況跳到 else 分支,一般都不會共線,那麼在 if 分支,先找兩個點 B 和 C,先 命名 inPt 爲 a 點, inPrev 爲 b 點, inNext 爲 c 點,要求的點 inPt2 爲 A 點,怎麼找 B 點的座標呢? 先說幾何的方法,便於理解代碼;
先以 b點 爲圓心做半徑爲 一 的單位圓,然後做線段 ab 的平行線AB, 使得平行線AB 正好與單位圓相切於B點,

注意這裏線段 ab 的平行線有兩條,那麼怎麼知道到底選哪個平行線呢?

先約定 角BAC 小於180 度,如果序列B A C 是逆時針序列,則要找的 B點 在 角BAC 的內部, 反之如果序列B A C 是順時針序列, 那麼 B點 在 角BAC 的外部,這樣就可以確定平行線選哪條了。做出平行線AB後,用同樣的方法做出平行線AC後,
平行線AB 和 平行線AC 交於 A 點,最後函數返回 二維向量 A點 - a點

其中B點 到 b點的距離 等於平行線AB 和 線段ab 的距離 是 一, C點 到 c點距離也是一, A點到 a點距離一般情況下大於 一

getBevelVec代碼中
ptPrevShift_x 和 ptPrevShift_y 分別是 B點 x 和 y 座標
ptNextShift_x 和 ptNextShift_y 分別是 C點 x 和 y 座標
A點 x 和 y 座標 分別是代碼中的 ptPrevShift_x + v_prev_x * sf 和 ptPrevShift_y + v_prev_y * sf

最後給出我用來分析 這個函數 自己寫的極其簡單的例子,這個html 文件直接從 \git_3js\three.js\examples\webgl_geometry_extrude_shapes.html 複製, 然後拷貝到 目錄 \git_3js\three.js\examples\

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - geometry - extrude shapes simple</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
		<style>
			body {
				background-color: #222;
			}
			a {
				color: #f80;
			}
		</style>
	</head>

	<body>

		<script type="module">

			import * as THREE from '../build/three.module.js';

			import { TrackballControls } from './jsm/controls/TrackballControls.js';

			var camera, scene, renderer, controls;

			init();
			animate();

			function init() {

				var info = document.createElement( 'div' );
				info.style.position = 'absolute';
				info.style.top = '10px';
				info.style.width = '100%';
				info.style.textAlign = 'center';
				info.style.color = '#fff';
				info.style.link = '#f80';
				info.innerHTML = '<a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - geometry extrude shapes';
				document.body.appendChild( info );

				renderer = new THREE.WebGLRenderer();
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				document.body.appendChild( renderer.domElement );

				scene = new THREE.Scene();
				scene.background = new THREE.Color( 0x222222 );

				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
				camera.position.set( 0, 0, 500 );

				controls = new TrackballControls( camera, renderer.domElement );
				controls.minDistance = 200;
				controls.maxDistance = 500;

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

				var light = new THREE.PointLight( 0xffffff );
				light.position.copy( camera.position );
				scene.add( light );

				var extrudeSettings = {
                    depth: 10,
                    steps: 1,
                    bevelEnabled: true,
                    bevelThickness: 4,
                    bevelSize: 4,
                    bevelSegments: 1
				};


                var pts = [new THREE.Vector2(10, 0), new THREE.Vector2(20, -20), new THREE.Vector2(-10, 0), new THREE.Vector2(20, 20)];
                // var pts = [new THREE.Vector2(22, 0), new THREE.Vector2(32, -20), new THREE.Vector2(2, 0), new THREE.Vector2(32, 20)];

				var shape = new THREE.Shape( pts );

				var geometry = new THREE.ExtrudeBufferGeometry( shape, extrudeSettings );

				var material = new THREE.MeshLambertMaterial( { color: 0xb00000, wireframe: false } );
				// var material = new THREE.MeshBasicMaterial( { color: 0xb00000, wireframe: true } );

				var mesh = new THREE.Mesh( geometry, material );

				scene.add( mesh );
			}

			function animate() {

				requestAnimationFrame( animate );

				controls.update();
				renderer.render( scene, camera );

			}

		</script>

	</body>

</html>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章