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>

输入框样式

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    * {
      padding: 0;
      margin: 0;
      box-sizing: border-box;
      background: white;
    }

    main {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      width: 400px;
      height: 400px;
      border: solid 5px silver;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    .field {
      position: relative;
      overflow: hidden;
      margin-bottom: 20px;
    }
    .field::before {
      content: '';
      position: absolute;
      left: 0;
      height: 2px;
      bottom: 0;
      width: 100%;
      background-color: #3e4fef;
      transition: 0.5s;
      transform: scale(0);
    }
    .field2::before {
      transform: scale(1);
    }
    .field input {
      border: none;
      outline: none;
      border-bottom: solid 1px #333;
      background: #ecf0f1;
      padding: 10px 0;
      font-size: 18px;
    }
  </style>
</head>
<body>
<main>
  <div class="field" id="dd1"><input type="text" placeholder="请输入账号" onfocus="focusIn(event)" onblur="focusOut(event)"></div>
  <div class="field"><input type="text" placeholder="请输入密码" onfocus="focusIn(event)" onblur="focusOut(event)"></div>
</main>
<script>
  let dd1 = document.getElementById('dd1');
  function focusIn(evt) {
    let div = evt.target.parentElement;
    div.classList.add('field2');
  }
  function focusOut(evt) {
    let div = evt.target.parentElement;
    div.classList.remove('field2');
  }
</script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值