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>