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>