Appearance
path.module.js 使用文档
path是three.js的扩展,可以方便地创建3D几何路径。
使用方法
步骤1: 创建 PathPointList
以预处理并存储点。
js
import { PathPointList, PathGeometry } from "./path.module";
const list = new PathPointList();
/**
* 设置点
* @param {THREE.Vector3[]} points 关键点数组
* @param {number} cornerRadius 角落半径,设为0以禁用圆角,默认为0.1
* @param {number} cornerSplit 角落分割数,默认为10
* @param {number} up 强制向上,默认通过切线自动计算
* @param {boolean} close 是否关闭路径,默认为false
*/
list.set(points, 0.1, 10, undefined, false);
步骤2: 生成几何体
生成 PathGeometry
。
js
/**
* @param {Object|Number} initData - 如果 initData 是数字,几何体通过空数据初始化并设置为最大顶点数。如果 initData 是对象,它包含 pathPointList 和选项。
* @param {Boolean} [generateUv2=false] 是否生成第二组UV坐标
*/
// 使用最大顶点数初始化
const geometry = new THREE.PathGeometry(2000, false);
// 使用数据初始化
const geometry = new THREE.PathGeometry({
pathPointList: pathPointList,
options: {
width: 0.1, // 默认为0.1
arrow: true, // 默认为true
progress: 1, // 默认为1
side: "both" // "left"/"right"/"both",默认为"both"
},
usage: THREE.StaticDrawUsage // 几何体用途
}, false);
// 当 pathPointList 改变时更新几何体
geometry.update(pathPointList, {
width: 0.1, // 默认为0.1
arrow: true, // 默认为true
progress: 1, // 默认为1
side: "both" // "left"/"right"/"both",默认为"both"
});
或者生成 PathTubeGeometry
。
js
/**
* @param {Object|Number} initData - 如果 initData 是数字,几何体通过空数据初始化并设置为最大顶点数。如果 initData 是对象,它包含 pathPointList 和选项。
* @param {Boolean} [generateUv2=false] 是否生成第二组UV坐标
*/
// 使用最大顶点数初始化
const geometry = new THREE.PathTubeGeometry(2000, false);
// 使用数据初始化
const geometry = new THREE.PathTubeGeometry({
pathPointList: pathPointList,
options: {
radius: 0.1, // 默认为0.1
radialSegments: 8, // 默认为8
progress: 1, // 默认为1
startRad: 0 // 默认为0
},
usage: THREE.StaticDrawUsage // 几何体用途
}, false);
// 当 pathPointList 改变时更新几何体
geometry.update(pathPointList, {
radius: 0.1, // 默认为0.1
radialSegments: 8, // 默认为8
progress: 1, // 默认为1
startRad: 0 // 默认为0
});
源码
path.module.js
js
import { Vector3, Matrix4, QuadraticBezierCurve3, BufferGeometry, BufferAttribute, DynamicDrawUsage, Uint32BufferAttribute, Uint16BufferAttribute, StaticDrawUsage } from 'three';
/**
* PathPoint
*/
class PathPoint {
constructor() {
this.pos = new Vector3();
this.dir = new Vector3();
this.right = new Vector3();
this.up = new Vector3(); // normal
this.dist = 0; // distance from start
this.widthScale = 1; // for corner
this.sharp = false; // marks as sharp corner
}
lerpPathPoints(p1, p2, alpha) {
this.pos.lerpVectors(p1.pos, p2.pos, alpha);
this.dir.lerpVectors(p1.dir, p2.dir, alpha);
this.up.lerpVectors(p1.up, p2.up, alpha);
this.right.lerpVectors(p1.right, p2.right, alpha);
this.dist = (p2.dist - p1.dist) * alpha + p1.dist;
this.widthScale = (p2.widthScale - p1.widthScale) * alpha + p1.widthScale;
}
copy(source) {
this.pos.copy(source.pos);
this.dir.copy(source.dir);
this.up.copy(source.up);
this.right.copy(source.right);
this.dist = source.dist;
this.widthScale = source.widthScale;
}
}
/**
* PathPointList
* input points to generate a PathPoint list
*/
class PathPointList {
constructor() {
this.array = []; // path point array
this.count = 0;
}
/**
* Set points
* @param {Vector3[]} points key points array
* @param {number} cornerRadius? the corner radius. set 0 to disable round corner. default is 0.1
* @param {number} cornerSplit? the corner split. default is 10.
* @param {number} up? force up. default is auto up (calculate by tangent).
* @param {boolean} close? close path. default is false.
*/
set(points, cornerRadius = 0.1, cornerSplit = 10, up = null, close = false) {
points = points.slice(0);
if (points.length < 2) {
console.warn('PathPointList: points length less than 2.');
this.count = 0;
return;
}
// Auto close
if (close && !points[0].equals(points[points.length - 1])) {
points.push(new Vector3().copy(points[0]));
}
// Generate path point list
for (let i = 0, l = points.length; i < l; i++) {
if (i === 0) {
this._start(points[i], points[i + 1], up);
} else if (i === l - 1) {
if (close) {
// Connect end point and start point
this._corner(points[i], points[1], cornerRadius, cornerSplit, up);
// Fix start point
const dist = this.array[0].dist; // should not copy dist
this.array[0].copy(this.array[this.count - 1]);
this.array[0].dist = dist;
} else {
this._end(points[i]);
}
} else {
this._corner(points[i], points[i + 1], cornerRadius, cornerSplit, up);
}
}
}
/**
* Get distance of this path
* @return {number}
*/
distance() {
if (this.count > 0) {
return this.array[this.count - 1].dist;
}
return 0;
}
_getByIndex(index) {
if (!this.array[index]) {
this.array[index] = new PathPoint();
}
return this.array[index];
}
_start(current, next, up) {
this.count = 0;
const point = this._getByIndex(this.count);
point.pos.copy(current);
point.dir.subVectors(next, current);
// init start up dir
if (up) {
point.up.copy(up);
} else {
// select an initial normal vector perpendicular to the first tangent vector
let min = Number.MAX_VALUE;
const tx = Math.abs(point.dir.x);
const ty = Math.abs(point.dir.y);
const tz = Math.abs(point.dir.z);
if (tx < min) {
min = tx;
point.up.set(1, 0, 0);
}
if (ty < min) {
min = ty;
point.up.set(0, 1, 0);
}
if (tz < min) {
point.up.set(0, 0, 1);
}
}
point.right.crossVectors(point.dir, point.up).normalize();
point.up.crossVectors(point.right, point.dir).normalize();
point.dist = 0;
point.widthScale = 1;
point.sharp = false;
point.dir.normalize();
this.count++;
}
_end(current) {
const lastPoint = this.array[this.count - 1];
const point = this._getByIndex(this.count);
point.pos.copy(current);
point.dir.subVectors(current, lastPoint.pos);
const dist = point.dir.length();
point.dir.normalize();
point.up.copy(lastPoint.up); // copy last up
const vec = helpVec3_1.crossVectors(lastPoint.dir, point.dir);
if (vec.length() > Number.EPSILON) {
vec.normalize();
const theta = Math.acos(Math.min(Math.max(lastPoint.dir.dot(point.dir), -1), 1)); // clamp for floating pt errors
point.up.applyMatrix4(helpMat4.makeRotationAxis(vec, theta));
}
point.right.crossVectors(point.dir, point.up).normalize();
point.dist = lastPoint.dist + dist;
point.widthScale = 1;
point.sharp = false;
this.count++;
}
_corner(current, next, cornerRadius, cornerSplit, up) {
if (cornerRadius > 0 && cornerSplit > 0) {
const lastPoint = this.array[this.count - 1];
const curve = _getCornerBezierCurve(lastPoint.pos, current, next, cornerRadius, (this.count - 1) === 0, helpCurve);
const samplerPoints = curve.getPoints(cornerSplit); // TODO optimize
for (let f = 0; f < cornerSplit; f++) {
this._sharpCorner(samplerPoints[f], samplerPoints[f + 1], up, f === 0 ? 1 : 0);
}
if (!samplerPoints[cornerSplit].equals(next)) {
this._sharpCorner(samplerPoints[cornerSplit], next, up, 2);
}
} else {
this._sharpCorner(current, next, up, 0, true);
}
}
// dirType: 0 - use middle dir / 1 - use last dir / 2- use next dir
_sharpCorner(current, next, up, dirType = 0, sharp = false) {
const lastPoint = this.array[this.count - 1];
const point = this._getByIndex(this.count);
const lastDir = helpVec3_1.subVectors(current, lastPoint.pos);
const nextDir = helpVec3_2.subVectors(next, current);
const lastDirLength = lastDir.length();
lastDir.normalize();
nextDir.normalize();
point.pos.copy(current);
if (dirType === 1) {
point.dir.copy(lastDir);
} else if (dirType === 2) {
point.dir.copy(nextDir);
} else {
point.dir.addVectors(lastDir, nextDir);
point.dir.normalize();
}
if (up) {
if (point.dir.dot(up) === 1) {
point.right.crossVectors(nextDir, up).normalize();
} else {
point.right.crossVectors(point.dir, up).normalize();
}
point.up.crossVectors(point.right, point.dir).normalize();
} else {
point.up.copy(lastPoint.up);
const vec = helpVec3_3.crossVectors(lastPoint.dir, point.dir);
if (vec.length() > Number.EPSILON) {
vec.normalize();
const theta = Math.acos(Math.min(Math.max(lastPoint.dir.dot(point.dir), -1), 1)); // clamp for floating pt errors
point.up.applyMatrix4(helpMat4.makeRotationAxis(vec, theta));
}
point.right.crossVectors(point.dir, point.up).normalize();
}
point.dist = lastPoint.dist + lastDirLength;
const _cos = lastDir.dot(nextDir);
point.widthScale = Math.min(1 / Math.sqrt((1 + _cos) / 2), 1.415) || 1;
point.sharp = (Math.abs(_cos - 1) > 0.05) && sharp;
this.count++;
}
}
const helpVec3_1 = new Vector3();
const helpVec3_2 = new Vector3();
const helpVec3_3 = new Vector3();
const helpMat4 = new Matrix4();
const helpCurve = new QuadraticBezierCurve3();
function _getCornerBezierCurve(last, current, next, cornerRadius, firstCorner, out) {
const lastDir = helpVec3_1.subVectors(current, last);
const nextDir = helpVec3_2.subVectors(next, current);
const lastDirLength = lastDir.length();
const nextDirLength = nextDir.length();
lastDir.normalize();
nextDir.normalize();
// cornerRadius can not bigger then lineDistance / 2, auto fix this
const v0Dist = Math.min((firstCorner ? lastDirLength / 2 : lastDirLength) * 0.999999, cornerRadius);
out.v0.copy(current).sub(lastDir.multiplyScalar(v0Dist));
out.v1.copy(current);
const v2Dist = Math.min(nextDirLength / 2 * 0.999999, cornerRadius);
out.v2.copy(current).add(nextDir.multiplyScalar(v2Dist));
return out;
}
/**
* PathGeometry
*/
class PathGeometry extends BufferGeometry {
/**
* @param {object|number} initData - If initData is number, geometry init by empty data and set it as the max vertex. If initData is Object, it contains pathPointList and options.
* @param {boolean} [generateUv2=false]
*/
constructor(initData = 3000, generateUv2 = false) {
super();
if (isNaN(initData)) {
this._initByData(initData.pathPointList, initData.options, initData.usage, generateUv2);
} else {
this._initByMaxVertex(initData, generateUv2);
}
}
_initByMaxVertex(maxVertex, generateUv2) {
this.setAttribute('position', new BufferAttribute(new Float32Array(maxVertex * 3), 3).setUsage(DynamicDrawUsage));
this.setAttribute('normal', new BufferAttribute(new Float32Array(maxVertex * 3), 3).setUsage(DynamicDrawUsage));
this.setAttribute('uv', new BufferAttribute(new Float32Array(maxVertex * 2), 2).setUsage(DynamicDrawUsage));
if (generateUv2) {
this.setAttribute('uv2', new BufferAttribute(new Float32Array(maxVertex * 2), 2).setUsage(DynamicDrawUsage));
}
this.drawRange.start = 0;
this.drawRange.count = 0;
this.setIndex(maxVertex > 65536 ?
new Uint32BufferAttribute(maxVertex * 3, 1) :
new Uint16BufferAttribute(maxVertex * 3, 1)
);
}
_initByData(pathPointList, options = {}, usage, generateUv2) {
const vertexData = generatePathVertexData(pathPointList, options, generateUv2);
if (vertexData && vertexData.count !== 0) {
this.setAttribute('position', new BufferAttribute(new Float32Array(vertexData.position), 3).setUsage(usage || StaticDrawUsage));
this.setAttribute('normal', new BufferAttribute(new Float32Array(vertexData.normal), 3).setUsage(usage || StaticDrawUsage));
this.setAttribute('uv', new BufferAttribute(new Float32Array(vertexData.uv), 2).setUsage(usage || StaticDrawUsage));
if (generateUv2) {
this.setAttribute('uv2', new BufferAttribute(new Float32Array(vertexData.uv2), 2).setUsage(usage || StaticDrawUsage));
}
this.setIndex((vertexData.position.length / 3) > 65536 ?
new Uint32BufferAttribute(vertexData.indices, 1) :
new Uint16BufferAttribute(vertexData.indices, 1)
);
} else {
this._initByMaxVertex(2, generateUv2);
}
}
/**
* Update geometry by PathPointList instance
* @param {PathPointList} pathPointList
* @param {Object} options
* @param {Number} [options.width=0.1]
* @param {Number} [options.progress=1]
* @param {Boolean} [options.arrow=true]
* @param {String} [options.side='both'] - "left"/"right"/"both"
*/
update(pathPointList, options = {}) {
const generateUv2 = !!this.getAttribute('uv2');
const vertexData = generatePathVertexData(pathPointList, options, generateUv2);
if (vertexData) {
this._updateAttributes(vertexData.position, vertexData.normal, vertexData.uv, generateUv2 ? vertexData.uv2 : null, vertexData.indices);
this.drawRange.count = vertexData.count;
} else {
this.drawRange.count = 0;
}
}
_resizeAttribute(name, len) {
let attribute = this.getAttribute(name);
while (attribute.array.length < len) {
const oldLength = attribute.array.length;
const newAttribute = new BufferAttribute(
new Float32Array(oldLength * 2),
attribute.itemSize,
attribute.normalized
);
newAttribute.name = attribute.name;
newAttribute.usage = attribute.usage;
this.setAttribute(name, newAttribute);
attribute = newAttribute;
}
}
_resizeIndex(len) {
let index = this.getIndex();
while (index.array.length < len) {
const oldLength = index.array.length;
const newIndex = new BufferAttribute(
oldLength * 2 > 65535 ? new Uint32Array(oldLength * 2) : new Uint16Array(oldLength * 2),
1
);
newIndex.name = index.name;
newIndex.usage = index.usage;
this.setIndex(newIndex);
index = newIndex;
}
}
_updateAttributes(position, normal, uv, uv2, indices) {
this._resizeAttribute('position', position.length);
const positionAttribute = this.getAttribute('position');
positionAttribute.array.set(position, 0);
if (positionAttribute.addUpdateRange) {
positionAttribute.clearUpdateRanges();
positionAttribute.addUpdateRange(0, position.length);
} else { // compatibility with r158 earlier
positionAttribute.updateRange.count = position.length;
}
positionAttribute.needsUpdate = true;
this._resizeAttribute('normal', normal.length);
const normalAttribute = this.getAttribute('normal');
normalAttribute.array.set(normal, 0);
if (normalAttribute.addUpdateRange) {
normalAttribute.clearUpdateRanges();
normalAttribute.addUpdateRange(0, normal.length);
} else { // compatibility with r158 earlier
normalAttribute.updateRange.count = normal.length;
}
normalAttribute.needsUpdate = true;
this._resizeAttribute('uv', uv.length);
const uvAttribute = this.getAttribute('uv');
uvAttribute.array.set(uv, 0);
if (uvAttribute.addUpdateRange) {
uvAttribute.clearUpdateRanges();
uvAttribute.addUpdateRange(0, uv.length);
} else { // compatibility with r158 earlier
uvAttribute.updateRange.count = uv.length;
}
uvAttribute.needsUpdate = true;
if (uv2) {
this._resizeAttribute('uv2', uv2.length);
const uv2Attribute = this.getAttribute('uv2');
uv2Attribute.array.set(uv2, 0);
if (uv2Attribute.addUpdateRange) {
uv2Attribute.clearUpdateRanges();
uv2Attribute.addUpdateRange(0, uv2.length);
} else { // compatibility with r158 earlier
uv2Attribute.updateRange.count = uv2.length;
}
uv2Attribute.needsUpdate = true;
}
this._resizeIndex(indices.length);
const indexAttribute = this.getIndex();
indexAttribute.set(indices, 0);
if (indexAttribute.addUpdateRange) {
indexAttribute.clearUpdateRanges();
indexAttribute.addUpdateRange(0, indices.length);
} else { // compatibility with r158 earlier
indexAttribute.updateRange.count = indices.length;
}
indexAttribute.needsUpdate = true;
}
}
// Vertex Data Generate Functions
function generatePathVertexData(pathPointList, options, generateUv2 = false) {
const width = options.width || 0.1;
const progress = options.progress !== undefined ? options.progress : 1;
const arrow = options.arrow !== undefined ? options.arrow : true;
const side = options.side !== undefined ? options.side : 'both';
const halfWidth = width / 2;
const sideWidth = (side !== 'both' ? width / 2 : width);
const totalDistance = pathPointList.distance();
const progressDistance = progress * totalDistance;
if (totalDistance == 0) {
return null;
}
const sharpUvOffset = halfWidth / sideWidth;
const sharpUvOffset2 = halfWidth / totalDistance;
let count = 0;
// modify data
const position = [];
const normal = [];
const uv = [];
const uv2 = [];
const indices = [];
let verticesCount = 0;
const right = new Vector3();
const left = new Vector3();
// for sharp corners
const leftOffset = new Vector3();
const rightOffset = new Vector3();
const tempPoint1 = new Vector3();
const tempPoint2 = new Vector3();
function addVertices(pathPoint) {
const first = position.length === 0;
const sharpCorner = pathPoint.sharp && !first;
const uvDist = pathPoint.dist / sideWidth;
const uvDist2 = pathPoint.dist / totalDistance;
const dir = pathPoint.dir;
const up = pathPoint.up;
const _right = pathPoint.right;
if (side !== 'left') {
right.copy(_right).multiplyScalar(halfWidth * pathPoint.widthScale);
} else {
right.set(0, 0, 0);
}
if (side !== 'right') {
left.copy(_right).multiplyScalar(-halfWidth * pathPoint.widthScale);
} else {
left.set(0, 0, 0);
}
right.add(pathPoint.pos);
left.add(pathPoint.pos);
if (sharpCorner) {
leftOffset.fromArray(position, position.length - 6).sub(left);
rightOffset.fromArray(position, position.length - 3).sub(right);
const leftDist = leftOffset.length();
const rightDist = rightOffset.length();
const sideOffset = leftDist - rightDist;
let longerOffset, longEdge;
if (sideOffset > 0) {
longerOffset = leftOffset;
longEdge = left;
} else {
longerOffset = rightOffset;
longEdge = right;
}
tempPoint1.copy(longerOffset).setLength(Math.abs(sideOffset)).add(longEdge);
const _cos = tempPoint2.copy(longEdge).sub(tempPoint1).normalize().dot(dir);
const _len = tempPoint2.copy(longEdge).sub(tempPoint1).length();
const _dist = _cos * _len * 2;
tempPoint2.copy(dir).setLength(_dist).add(tempPoint1);
if (sideOffset > 0) {
position.push(
tempPoint1.x, tempPoint1.y, tempPoint1.z, // 6
right.x, right.y, right.z, // 5
left.x, left.y, left.z, // 4
right.x, right.y, right.z, // 3
tempPoint2.x, tempPoint2.y, tempPoint2.z, // 2
right.x, right.y, right.z // 1
);
verticesCount += 6;
indices.push(
verticesCount - 6, verticesCount - 8, verticesCount - 7,
verticesCount - 6, verticesCount - 7, verticesCount - 5,
verticesCount - 4, verticesCount - 6, verticesCount - 5,
verticesCount - 2, verticesCount - 4, verticesCount - 1
);
count += 12;
} else {
position.push(
left.x, left.y, left.z, // 6
tempPoint1.x, tempPoint1.y, tempPoint1.z, // 5
left.x, left.y, left.z, // 4
right.x, right.y, right.z, // 3
left.x, left.y, left.z, // 2
tempPoint2.x, tempPoint2.y, tempPoint2.z // 1
);
verticesCount += 6;
indices.push(
verticesCount - 6, verticesCount - 8, verticesCount - 7,
verticesCount - 6, verticesCount - 7, verticesCount - 5,
verticesCount - 6, verticesCount - 5, verticesCount - 3,
verticesCount - 2, verticesCount - 3, verticesCount - 1
);
count += 12;
}
normal.push(
up.x, up.y, up.z,
up.x, up.y, up.z,
up.x, up.y, up.z,
up.x, up.y, up.z,
up.x, up.y, up.z,
up.x, up.y, up.z
);
uv.push(
uvDist - sharpUvOffset, 0,
uvDist - sharpUvOffset, 1,
uvDist, 0,
uvDist, 1,
uvDist + sharpUvOffset, 0,
uvDist + sharpUvOffset, 1
);
if (generateUv2) {
uv2.push(
uvDist2 - sharpUvOffset2, 0,
uvDist2 - sharpUvOffset2, 1,
uvDist2, 0,
uvDist2, 1,
uvDist2 + sharpUvOffset2, 0,
uvDist2 + sharpUvOffset2, 1
);
}
} else {
position.push(
left.x, left.y, left.z,
right.x, right.y, right.z
);
normal.push(
up.x, up.y, up.z,
up.x, up.y, up.z
);
uv.push(
uvDist, 0,
uvDist, 1
);
if (generateUv2) {
uv2.push(
uvDist2, 0,
uvDist2, 1
);
}
verticesCount += 2;
if (!first) {
indices.push(
verticesCount - 2, verticesCount - 4, verticesCount - 3,
verticesCount - 2, verticesCount - 3, verticesCount - 1
);
count += 6;
}
}
}
const sharp = new Vector3();
function addStart(pathPoint) {
const dir = pathPoint.dir;
const up = pathPoint.up;
const _right = pathPoint.right;
const uvDist = pathPoint.dist / sideWidth;
const uvDist2 = pathPoint.dist / totalDistance;
if (side !== 'left') {
right.copy(_right).multiplyScalar(halfWidth * 2);
} else {
right.set(0, 0, 0);
}
if (side !== 'right') {
left.copy(_right).multiplyScalar(-halfWidth * 2);
} else {
left.set(0, 0, 0);
}
sharp.copy(dir).setLength(halfWidth * 3);
right.add(pathPoint.pos);
left.add(pathPoint.pos);
sharp.add(pathPoint.pos);
position.push(
left.x, left.y, left.z,
right.x, right.y, right.z,
sharp.x, sharp.y, sharp.z
);
normal.push(
up.x, up.y, up.z,
up.x, up.y, up.z,
up.x, up.y, up.z
);
uv.push(
uvDist, side !== 'both' ? (side !== 'right' ? -2 : 0) : -0.5,
uvDist, side !== 'both' ? (side !== 'left' ? 2 : 0) : 1.5,
uvDist + 1.5, side !== 'both' ? 0 : 0.5
);
if (generateUv2) {
uv2.push(
uvDist2, side !== 'both' ? (side !== 'right' ? -2 : 0) : -0.5,
uvDist2, side !== 'both' ? (side !== 'left' ? 2 : 0) : 1.5,
uvDist2 + (1.5 * width / totalDistance), side !== 'both' ? 0 : 0.5
);
}
verticesCount += 3;
indices.push(
verticesCount - 1, verticesCount - 3, verticesCount - 2
);
count += 3;
}
let lastPoint;
if (progressDistance > 0) {
for (let i = 0; i < pathPointList.count; i++) {
const pathPoint = pathPointList.array[i];
if (pathPoint.dist > progressDistance) {
const prevPoint = pathPointList.array[i - 1];
lastPoint = new PathPoint();
// linear lerp for progress
const alpha = (progressDistance - prevPoint.dist) / (pathPoint.dist - prevPoint.dist);
lastPoint.lerpPathPoints(prevPoint, pathPoint, alpha);
addVertices(lastPoint);
break;
} else {
addVertices(pathPoint);
}
}
} else {
lastPoint = pathPointList.array[0];
}
// build arrow geometry
if (arrow) {
lastPoint = lastPoint || pathPointList.array[pathPointList.count - 1];
addStart(lastPoint);
}
return {
position,
normal,
uv,
uv2,
indices,
count
};
}
/**
* PathTubeGeometry
*/
class PathTubeGeometry extends PathGeometry {
/**
* @param {object|number} initData - If initData is number, geometry init by empty data and set it as the max vertex. If initData is Object, it contains pathPointList and options.
* @param {boolean} [generateUv2=false]
*/
constructor(initData = 1000, generateUv2 = false) {
super(initData, generateUv2);
}
_initByData(pathPointList, options = {}, usage, generateUv2) {
const vertexData = generateTubeVertexData(pathPointList, options, generateUv2);
if (vertexData && vertexData.count !== 0) {
this.setAttribute('position', new BufferAttribute(new Float32Array(vertexData.position), 3).setUsage(usage || StaticDrawUsage));
this.setAttribute('normal', new BufferAttribute(new Float32Array(vertexData.normal), 3).setUsage(usage || StaticDrawUsage));
this.setAttribute('uv', new BufferAttribute(new Float32Array(vertexData.uv), 2).setUsage(usage || StaticDrawUsage));
if (generateUv2) {
this.setAttribute('uv2', new BufferAttribute(new Float32Array(vertexData.uv2), 2).setUsage(usage || StaticDrawUsage));
}
this.setIndex((vertexData.position.length / 3) > 65536 ?
new Uint32BufferAttribute(vertexData.indices, 1) :
new Uint16BufferAttribute(vertexData.indices, 1)
);
} else {
this._initByMaxVertex(2, generateUv2);
}
}
/**
* Update geometry by PathPointList instance
* @param {PathPointList} pathPointList
* @param {Object} options
* @param {Number} [options.radius=0.1]
* @param {Number} [options.progress=1]
* @param {Boolean} [options.radialSegments=8]
* @param {String} [options.startRad=0]
*/
update(pathPointList, options = {}) {
const generateUv2 = !!this.getAttribute('uv2');
const vertexData = generateTubeVertexData(pathPointList, options, generateUv2);
if (vertexData) {
this._updateAttributes(vertexData.position, vertexData.normal, vertexData.uv, generateUv2 ? vertexData.uv2 : null, vertexData.indices);
this.drawRange.count = vertexData.count;
} else {
this.drawRange.count = 0;
}
}
}
// Vertex Data Generate Functions
function generateTubeVertexData(pathPointList, options, generateUv2 = false) {
const radius = options.radius || 0.1;
const progress = options.progress !== undefined ? options.progress : 1;
const radialSegments = Math.max(2, options.radialSegments || 8);
const startRad = options.startRad || 0;
const circum = radius * 2 * Math.PI;
const totalDistance = pathPointList.distance();
const progressDistance = progress * totalDistance;
if (progressDistance == 0) {
return null;
}
let count = 0;
// modify data
const position = [];
const normal = [];
const uv = [];
const uv2 = [];
const indices = [];
let verticesCount = 0;
const normalDir = new Vector3();
function addVertices(pathPoint, radius, radialSegments) {
const first = position.length === 0;
const uvDist = pathPoint.dist / circum;
const uvDist2 = pathPoint.dist / totalDistance;
for (let r = 0; r <= radialSegments; r++) {
let _r = r;
if (_r == radialSegments) {
_r = 0;
}
normalDir.copy(pathPoint.up).applyAxisAngle(pathPoint.dir, startRad + Math.PI * 2 * _r / radialSegments).normalize();
position.push(pathPoint.pos.x + normalDir.x * radius * pathPoint.widthScale, pathPoint.pos.y + normalDir.y * radius * pathPoint.widthScale, pathPoint.pos.z + normalDir.z * radius * pathPoint.widthScale);
normal.push(normalDir.x, normalDir.y, normalDir.z);
uv.push(uvDist, r / radialSegments);
if (generateUv2) {
uv2.push(uvDist2, r / radialSegments);
}
verticesCount++;
}
if (!first) {
const begin1 = verticesCount - (radialSegments + 1) * 2;
const begin2 = verticesCount - (radialSegments + 1);
for (let i = 0; i < radialSegments; i++) {
indices.push(
begin2 + i, begin1 + i, begin1 + i + 1,
begin2 + i, begin1 + i + 1, begin2 + i + 1
);
count += 6;
}
}
}
if (progressDistance > 0) {
for (let i = 0; i < pathPointList.count; i++) {
const pathPoint = pathPointList.array[i];
if (pathPoint.dist > progressDistance) {
const prevPoint = pathPointList.array[i - 1];
const lastPoint = new PathPoint();
// linear lerp for progress
const alpha = (progressDistance - prevPoint.dist) / (pathPoint.dist - prevPoint.dist);
lastPoint.lerpPathPoints(prevPoint, pathPoint, alpha);
addVertices(lastPoint, radius, radialSegments);
break;
} else {
addVertices(pathPoint, radius, radialSegments);
}
}
}
return {
position,
normal,
uv,
uv2,
indices,
count
};
}
export { PathGeometry, PathPointList, PathTubeGeometry };