threejs中简单的点到点闪电效果,也可以作为电流效果
1、将起点和终点连线对齐到X轴上并且起点与原点重合,方便后续计算
2、在起点和终点之间均匀的获取数个点(如5个)作为基础的拐点
3、将上一个步骤获取的点生成一条Curve,在Curve上采样点,如50个点
4、将上一步骤采样的点在小范围内抖动,形成不规则弯曲的效果
5、加上动态效果
代码如下:
/**
* 获取3D空间任意两个点连接成直线,把直线变换到与X轴重合(起点与原点重合)的矩阵
* @param {*} p1
* @param {*} p2
*/
getMatrixAlignToX(p1, p2) {
// 1. 平移矩阵:将起点移至原点
const T = new THREE.Matrix4().makeTranslation(-p1.x, -p1.y, -p1.z);
// 2. 计算方向向量并归一化
const V = new THREE.Vector3().subVectors(p2, p1);
const u = V.clone().normalize();
// 3. 构造旋转矩阵:对齐到X轴
const q = new THREE.Quaternion().setFromUnitVectors(
u,
new THREE.Vector3(1, 0, 0)
);
const R = new THREE.Matrix4().makeRotationFromQuaternion(q);
// 4. 组合变换矩阵
const M = new THREE.Matrix4().multiplyMatrices(R, T);
return M;
}
代码中已经有比较详细的注释说明,这里再简单补充下:此矩阵计算就是组合了平移矩阵和旋转矩阵,平移矩阵用于将起点平移至原点,旋转矩阵用于将起点与终点的直线旋转到对齐X轴。获取到此矩阵后就可以将起点和终点乘上此矩阵,方便后面的计算。效果如下:黄色为变换前的起点和终点,蓝色为变换后的起点和终点。为了便于观察,已将z轴设置为0
代码如下:
const matrix = this.getMatrixAlignToX(startPos, endPos);
const transformedStart = startPos.clone().applyMatrix4(matrix); // 变换起点
const transformedEnd = endPos.clone().applyMatrix4(matrix); // 变换终点
// 使用线性插值沿直线生成n个中间点:
const points = [];
const offset = 1 / count;
for (let i = 0; i <= 1; i += offset) {
// 5个均匀分布点
const pos = new THREE.Vector3().lerpVectors(
transformedStart,
transformedEnd,
i
);
points.push(pos);
}
// 除了首尾点外,每个点在Y轴和Z轴上都有一个随机偏移,最大不超过radius
for (let i = 1; i < points.length - 1; i++) {
const yOffset = Math.random() * radius - radius / 2; // -r/2 到 r/2
const zOffset = Math.random() * radius - radius / 2; // -r/2 到 r/2
points[i].y += yOffset;
points[i].z += zOffset;
}
先使用Vector3的lerpVectors函数在起点和终点之间均匀获取数个点,然后给除了起点和终点之外的点进行y轴和z轴的偏移(此时已对齐到X轴,计算很方便),要使闪电有随机性,这里用Math.random()计算偏移,效果如下
代码如下:
// 生成平滑的曲线
const curve = new THREE.CatmullRomCurve3(
points, // 控制点数组
false, // 是否闭合(默认false)
"centripetal", // 曲线类型(可选:'centripetal'、'chordal'、'catmullrom')
);
// 平滑采样点
const pointsOnCurve = curve.getPoints(divisions);
此时将采样点可视化出来就是上面视频的效果
代码如下:
const transformPoints = [].concat(pointsOnCurve); // 复制数组
// 对采样出的点除首尾点外,每个点在Y轴和Z轴上都有一个随机偏移,最大不超过threshold。形成抖动的闪电效果
for (let i = 1; i < transformPoints.length - 1; i++) {
const yOffset = Math.random() * threshold - threshold / 2; // -r/2 到 r/2
const zOffset = Math.random() * threshold - threshold / 2; // -r/2 到 r/2
transformPoints[i].y += yOffset;
transformPoints[i].z += zOffset;
}
将采样点可视化出现如下视频效果,可以通过调整第2步中拐点生成的偏移参数radius和第4步中采样点偏移参数threshold来设置生成的弯曲程度
前面为了计算方便,将起点和终点对齐到X轴,此时已经生成了闪电,需要乘上变换矩阵的逆矩阵变换回去
this.line.applyMatrix4(matrix.invert());
原理就是使用setInterval函数每隔一段事件往line里增加几个顶点。同时增加闪电刷新
{
...
const geometry = new LineGeometry();
geometry.setPositions([
this.allPoints[0].x,
this.allPoints[0].y,
this.allPoints[0].z,
]);
if (!this.matLine)
this.matLine = new LineMaterial({
color: "#" + this._color,
linewidth: this._lineWidth,
dashed: false,
transparent: true,
});
else {
this.matLine.opacity = 1;
this.matLine.visible = true;
}
this.line = new Line2(geometry, this.matLine);
// 应用逆矩阵,将曲线变换回原始坐标系
this.line.applyMatrix4(matrix.invert());
this.obj.add(this.line);
// 创建定时器逐步添加点
this.pointAddInterval = setInterval(() => {
this.addNextPoint();
}, 10);
this.time = Math.max(this.showMinTime, Math.random() * this.showMaxTime); // 重置时间,随机一个起始点
this.updateLineTimer = setTimeout(() => {
this.updateLine();
}, this.time);
}
/**
* 逐帧显示顶点
*/
addNextPoint() {
if (this.currentPointIndex + 3 < this.allPoints.length) {
this.currentPointIndex += 3;
const newPositions = [];
for (var i = 0; i < this.currentPointIndex; i++) {
newPositions.push(
this.allPoints[i].x,
this.allPoints[i].y,
this.allPoints[i].z
);
}
this.line.geometry.dispose();
this.line.geometry = new LineGeometry();
this.line.geometry.setPositions(newPositions);
} else {
if (this.currentPointIndex < this.allPoints.length) {
this.currentPointIndex = this.allPoints.length;
const newPositions = [];
for (var i = 0; i < this.currentPointIndex; i++) {
newPositions.push(
this.allPoints[i].x,
this.allPoints[i].y,
this.allPoints[i].z
);
}
this.line.geometry.dispose();
this.line.geometry = new LineGeometry();
this.line.geometry.setPositions(newPositions);
}
clearInterval(this.pointAddInterval); // 停止添加点
}
}
/**
* 每隔一定时间隐藏闪电或重新绘制闪电
*/
updateLine() {
if (this.line) {
if (this.matLine.visible) {
this.opacityInterval = setInterval(() => {
this.fadeOpacity();
}, 10);
this.time = Math.max(
this.hideMinTime,
Math.random() * this.hideMaxTime
); // 重置时间,随机一个起始点
if (this.updateLineTimer) clearTimeout(this.updateLineTimer);
this.updateLineTimer = setTimeout(() => {
this.updateLine();
}, this.time);
} else {
if (this.opacityInterval) clearInterval(this.opacityInterval);
this.opacityInterval = null;
this.createCurve(); // 重新生成曲线
}
}
}
注意需要引用Line2 类型,这种类型的线才能控制宽度
import { LineMaterial } from "../../three/examples/jsm/lines/LineMaterial.js";
import { LineGeometry } from "../../three/examples/jsm/lines/LineGeometry.js";
import { Line2 } from "../../three/examples/jsm/lines/Line2.js";
createCurve() {
if (this.line) {
if (this.updateLineTimer) clearTimeout(this.updateLineTimer);
this.obj.remove(this.line);
this.line.geometry.dispose();
this.line = null;
}
// 添加当前绘制点索引
this.currentPointIndex = 0;
let res = this.generatePathPoints();
const matrix = res.matrix; // 保存变换矩阵
this.allPoints = res.points;
this.allDir = [];
for (var i = 0; i < this.allPoints.length; i++) {
this.allDir.push(
new THREE.Vector3()
.random()
.subScalar(0.5)
.normalize()
);
}
const geometry = new LineGeometry();
geometry.setPositions([
this.allPoints[0].x,
this.allPoints[0].y,
this.allPoints[0].z,
]);
if (!this.matLine)
this.matLine = new LineMaterial({
color: "#" + this._color,
linewidth: this._lineWidth,
dashed: false,
transparent: true,
});
else {
this.matLine.opacity = 1;
this.matLine.visible = true;
}
this.line = new Line2(geometry, this.matLine);
// 应用逆矩阵,将曲线变换回原始坐标系
this.line.applyMatrix4(matrix.invert());
this.obj.add(this.line);
// 创建定时器逐步添加点
this.pointAddInterval = setInterval(() => {
this.addNextPoint();
}, 10); // 每50ms添加一个点(约20帧/秒)
this.time = Math.max(this.showMinTime, Math.random() * this.showMaxTime); // 重置时间,随机一个起始点
this.updateLineTimer = setTimeout(() => {
this.updateLine();
}, this.time);
}
addNextPoint() {
if (this.currentPointIndex + 3 < this.allPoints.length) {
this.currentPointIndex += 3;
const newPositions = [];
for (var i = 0; i < this.currentPointIndex; i++) {
newPositions.push(
this.allPoints[i].x,
this.allPoints[i].y,
this.allPoints[i].z
); // x坐标
}
this.line.geometry.dispose();
this.line.geometry = new LineGeometry();
this.line.geometry.setPositions(newPositions);
} else {
if (this.currentPointIndex < this.allPoints.length) {
this.currentPointIndex = this.allPoints.length;
const newPositions = [];
for (var i = 0; i < this.currentPointIndex; i++) {
newPositions.push(
this.allPoints[i].x,
this.allPoints[i].y,
this.allPoints[i].z
); // x坐标
}
this.line.geometry.dispose();
this.line.geometry = new LineGeometry();
this.line.geometry.setPositions(newPositions);
}
clearInterval(this.pointAddInterval); // 停止添加点
}
}
// 将原有的路径生成逻辑提取为独立方法
generatePathPoints() {
const startPos = this._startPos; // 起始点
const endPos = this._endPos; // 终点
const count = this._count; // 中间点数量
const radius = this._radius; // 最大偏移半径
const divisions = this._divisions; // 曲线采样点数
const threshold = this._threshold; // 抖动范围
// 将起点与终点变换到与X轴重合的坐标系下方便计算
const matrix = this.getMatrixAlignToX(startPos, endPos);
const transformedStart = startPos.clone().applyMatrix4(matrix); // 应变为 (0, 0, 0)
const transformedEnd = endPos.clone().applyMatrix4(matrix); // 应变为 (||V||, 0, 0)
// 使用线性插值沿直线生成n个中间点:
const points = [];
const offset = 1 / count;
for (let i = 0; i <= 1; i += offset) {
// 5个均匀分布点
const pos = new THREE.Vector3().lerpVectors(
transformedStart,
transformedEnd,
i
);
points.push(pos);
}
// 除了首尾点外,每个点在Y轴和Z轴上都有一个随机偏移,最大不超过radius
for (let i = 1; i < points.length - 1; i++) {
const yOffset = Math.random() * radius - radius / 2; // -r/2 到 r/2
const zOffset = Math.random() * radius - radius / 2; // -r/2 到 r/2
points[i].y += yOffset;
points[i].z += zOffset;
}
// 生成平滑的曲线
const curve = new THREE.CatmullRomCurve3(
points, // 控制点数组
false, // 是否闭合(默认false)
"centripetal" // 曲线类型(可选:'centripetal'、'chordal'、'catmullrom')
);
// 平滑采样点
const pointsOnCurve = curve.getPoints(divisions);
// 查看采样出的点
if (false) {
const geometryTemp = new THREE.BufferGeometry().setFromPoints(
pointsOnCurve
);
const materialTemp = new THREE.LineBasicMaterial({ color: 0x00ffff });
const curveLineTemp = new THREE.Line(geometryTemp, materialTemp);
// 应用逆矩阵,将曲线变换回原始坐标系
//curveLineTemp.applyMatrix4(matrix.invert());
this.obj.add(curveLineTemp);
}
const transformPoints = [].concat(pointsOnCurve);
// 对采样出的点除首尾点外,每个点在Y轴和Z轴上都有一个随机偏移,最大不超过threshold。形成抖动的闪电效果
for (let i = 1; i < transformPoints.length - 1; i++) {
const yOffset = Math.random() * threshold - threshold / 2; // -r/2 到 r/2
const zOffset = Math.random() * threshold - threshold / 2; // -r/2 到 r/2
transformPoints[i].y += yOffset;
transformPoints[i].z += zOffset;
}
const positions = new Float32Array(transformPoints.length * 3);
for (let i = 0; i < transformPoints.length; i++) {
positions[i * 3] = transformPoints[i].x;
positions[i * 3 + 1] = transformPoints[i].y;
positions[i * 3 + 2] = transformPoints[i].z;
}
return { points: transformPoints, matrix: matrix, positions: positions };
}
updateLine() {
if (this.line) {
if (this.matLine.visible) {
this.opacityInterval = setInterval(() => {
this.fadeOpacity();
}, 10);
this.time = Math.max(
this.hideMinTime,
Math.random() * this.hideMaxTime
); // 重置时间,随机一个起始点
if (this.updateLineTimer) clearTimeout(this.updateLineTimer);
this.updateLineTimer = setTimeout(() => {
this.updateLine();
}, this.time);
} else {
if (this.opacityInterval) clearInterval(this.opacityInterval);
this.opacityInterval = null;
this.createCurve(); // 重新生成曲线
}
}
}
fadeOpacity() {
if (this.matLine && this.matLine.opacity > 0) {
this.matLine.opacity -= 0.05;
} else {
this.matLine.visible = false;
clearInterval(this.opacityInterval); // 停止添加点
this.opacityInterval = null;
}
}
/**
* 获取3D空间任意两个点连接成直线,把直线变换到与X轴重合(起点与原点重合)的矩阵
* @param {*} p1
* @param {*} p2
*/
getMatrixAlignToX(p1, p2) {
// 1. 平移矩阵:将起点移至原点
const T = new THREE.Matrix4().makeTranslation(-p1.x, -p1.y, -p1.z);
// 2. 计算方向向量并归一化
const V = new THREE.Vector3().subVectors(p2, p1);
const u = V.clone().normalize();
// 3. 构造旋转矩阵:对齐到X轴
const q = new THREE.Quaternion().setFromUnitVectors(
u,
new THREE.Vector3(1, 0, 0)
);
const R = new THREE.Matrix4().makeRotationFromQuaternion(q);
// 4. 组合变换矩阵
const M = new THREE.Matrix4().multiplyMatrices(R, T);
return M;
}