Three.js粒子变换特效
关于单个模型用粒子组成可参考使用粒子效果动画组成模型[自定义shader实现]
本文介绍多模型切换技巧
思路
- 获取需要切换的几个模型的数据就是每个点的坐标在
geometry.attributes.position
中array
即时构成mesh的数据。
例:
const targetMesh = g.scene.getObjectByProperty("type", "Mesh");
if (!targetMesh) throw new Error("获取目标物体失败");
const { array} = targetMesh.geometry.attributes.position;
- 将数据保存到一个列表中供切换数据使用
- 切换模型数据实现思路
粒子的动画应该使用自定义shader实现,如果直接更新attributes 几千个点还好,数量达到几万就卡顿严重,无法使用,上述案例使用9万多点,运行时无丝毫卡顿不影响页面其他功能,实为上策。
为了让shader计算每个点在某一时刻应该在什么位置需要提供几个属性参与计算:变换前的位置,变换后应该的位置,变换的时长,当前的时间,如果需要颜色渐变可以额外提供之前和之后的颜色,这些数据通过geometry.attributes
传递给shader,在自定义shader中设置对应属性接收、计算。
例:
this.particlesGeometry = new BufferGeometry();
this.color = new Float32Array(this.numberOfPoints * 3);
this.particlesGeometry.setAttribute("color", new BufferAttribute(this.color, 3));
this.particles = new Points(this.particlesGeometry, PointsShaderMaterial);
每次切换模型数据 将原先的属性替换为新的模型数据 然后告诉shader需要更新这一属性,这样shader才会接受到新的有效的数据
this.particlesGeometry.attributes.color.needsUpdate = true;
shader中的计算和控制粒子变换的代码请看下面代码区域
顶点着色器
//决定片元着色器的颜色
varying vec3 vColor;
uniform float time;
uniform float size;
attribute vec3 color;
attribute vec3 oldColor;
attribute vec3 toPositions;
attribute vec3 oldPositions;
attribute float toPositionsDuration;
// attribute float scale;
void main() {
vec3 dispatchPos = toPositions;
vColor = color;
//顶点位置移动
//当前时间在点的运行时间中占比
float percent = time / toPositionsDuration;
if(percent <= 1.) {
dispatchPos.x = oldPositions.x + percent * (toPositions.x - oldPositions.x);
dispatchPos.y = oldPositions.y + percent * (toPositions.y - oldPositions.y);
dispatchPos.z = oldPositions.z + percent * (toPositions.z - oldPositions.z);
vColor.x = oldColor.x + percent * (color.x - oldColor.x);
vColor.y = oldColor.y + percent * (color.y - oldColor.y);
vColor.z = oldColor.z + percent * (color.z - oldColor.z);
}
vec4 viewPosition = modelViewMatrix * vec4(dispatchPos, 1.0);
gl_Position = projectionMatrix * viewPosition;
//PointsMaterial材质的size属性和每个粒子随机的缩放 大小不一效果
// gl_PointSize = size * scale;
gl_PointSize = size;
//近大远小效果 值自己调节
gl_PointSize *= (120. / -(modelViewMatrix * vec4(dispatchPos, 1.0)).z);
}
片元着色器
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.);
}
粒子控制器
/*
* @Author: hongbin
* @Date: 2022-12-14 10:48:40
* @LastEditors: hongbin
* @LastEditTime: 2022-12-16 17:53:08
* @Description:使用shader计算粒子位置 减小开销
*/
import { BufferAttribute, BufferGeometry, Color, Material, Points, TextureLoader } from "three";
import starMap from "../assets/img/star_09.png";
// import starMap from "../assets/img/box.jpg";
import { PointsShaderMaterial } from "./shader/points.material";
export interface IColor {
left: THREE.Color;
right: THREE.Color;
dividingLine: number;
}
interface IPointsModelParams {
positions: ArrayLike<number>;
duration: { min: number; max: number };
box3: THREE.Box3;
color: IColor;
}
export class PointsControlClass {
numberOfPoints: number;
particles: Points<BufferGeometry, Material>;
// particlesMaterial: PointsMaterial;
particlesGeometry: BufferGeometry;
toPositions: Float32Array;
toPositionsDuration: Float32Array;
oldPositions: Float32Array;
color: Float32Array;
defaultPointsColor = { left: new Color("#fff"), right: new Color("#51f"), dividingLine: 0.5 };
modelsList: { [key: number]: IPointsModelParams; current: number; length: number } = { current: -1, length: 0 };
oldColor: Float32Array;
positions: Float32Array;
/**
* 粒子控制器
* @param {number} numberOfPoints 数量必须大于所有模型中最大的点数量 不然显示不全
*/
constructor(numberOfPoints: number) {
this.numberOfPoints = numberOfPoints;
this.particlesGeometry = new BufferGeometry();
//顶点着色器需要 position属性来定位
this.positions = new Float32Array(numberOfPoints * 3);
//顶点着色器的变更前的坐标
this.oldPositions = new Float32Array(numberOfPoints * 3);
//顶点着色器要前往的目标位置
this.toPositions = new Float32Array(this.numberOfPoints * 3);
//顶点着色器要前往的目标位置的时间
this.toPositionsDuration = new Float32Array(this.numberOfPoints);
//要变换的颜色
this.color = new Float32Array(this.numberOfPoints * 3);
//变换之前的颜色
this.oldColor = new Float32Array(this.numberOfPoints * 3);
this.particlesGeometry.setAttribute("color", new BufferAttribute(this.color, 3));
this.particlesGeometry.setAttribute("oldColor", new BufferAttribute(this.oldColor, 3));
this.particlesGeometry.setAttribute("position", new BufferAttribute(this.positions, 3));
this.particlesGeometry.setAttribute("oldPositions", new BufferAttribute(this.oldPositions, 3));
this.particlesGeometry.setAttribute("toPositions", new BufferAttribute(this.toPositions, 3));
this.particlesGeometry.setAttribute("toPositionsDuration", new BufferAttribute(this.toPositionsDuration, 1));
const textureLoader = new TextureLoader();
PointsShaderMaterial.uniforms.textureMap = {
value: textureLoader.load(starMap.src),
};
this.particles = new Points(this.particlesGeometry, PointsShaderMaterial);
this.init();
// console.log(new Color("#f00"));
// console.log(new Color("#2e00fe"));
}
/**
* 前往列表中的某个形态
*/
toIndex(index: number) {
const { positions, color, box3, duration } = this.modelsList[index];
const { length } = positions;
const pointCount = length / 3;
const { dividingLine, left, right } = color;
const { abs } = Math;
const {
min: { x: minX },
max: { y: maxX },
} = box3;
// 颜色的差值
const disColor = {
r: left.r - right.r,
g: left.g - right.g,
b: left.b - right.b,
};
const width = abs(box3.min.x - box3.max.x);
//颜色分界
const dividingLineValue = box3.min.x + width * dividingLine;
console.log(`小:${box3.min.x} 大:${box3.max.x} 宽${width},分界线:${dividingLineValue}`);
for (let i = 0, realCount = 0; i < this.numberOfPoints; i++, realCount++) {
//模型的点和生成的点数量未必相等 多的则重合前面的位置
realCount = realCount % pointCount;
const i3 = i * 3;
const r3 = realCount * 3;
//设置给顶点着色器
//保存起点
this.oldPositions[i3] = this.toPositions[i3];
this.oldPositions[i3 + 1] = this.toPositions[i3 + 1];
this.oldPositions[i3 + 2] = this.toPositions[i3 + 2];
//设置终点
const x = positions[r3];
this.toPositions[i3] = x;
this.toPositions[i3 + 1] = positions[r3 + 1];
this.toPositions[i3 + 2] = positions[r3 + 2];
//设置颜色 从左到右过渡
//当前位置坐标占比
const percent = abs(
x <= dividingLineValue
? ((minX - x) / (dividingLineValue - minX)) * dividingLine * dividingLine
: ((x - dividingLineValue) / (maxX - dividingLineValue)) * (1 - dividingLine) + dividingLine
);
//设置变换前的颜色
this.oldColor[i3] = this.color[i3];
this.oldColor[i3 + 1] = this.color[i3 + 1];
this.oldColor[i3 + 2] = this.color[i3 + 2];
//设置最终颜色
const r = left.r - percent * disColor.r;
const g = left.g - percent * disColor.g;
const b = left.b - percent * disColor.b;
this.color[i3] = r;
this.color[i3 + 1] = g;
this.color[i3 + 2] = b;
//设置运动时间
const useDuration = duration.min + Math.random() * (duration.max - duration.min);
this.toPositionsDuration[i] = useDuration;
}
}
/**
* 向系统添加数据
*/
append(params: IPointsModelParams, index: number) {
if (this.modelsList[index]) {
console.log("已经有这项了");
}
this.modelsList[index] = params;
this.modelsList.length = Object.keys(this.modelsList).length - 2;
}
/**
* 下一个形态
*/
next() {
const current = this.modelsList.current;
const toIndex = current + 1;
if (toIndex < this.modelsList.length && toIndex >= 0) {
this.toIndex(toIndex);
this.updateAttributes();
this.modelsList.current++;
return true;
}
return false;
}
/**
* 上一个形态
*/
prev() {
const current = this.modelsList.current;
const toIndex = current - 1;
if (toIndex < this.modelsList.length && toIndex >= 0) {
this.toIndex(toIndex);
this.updateAttributes();
this.modelsList.current--;
return true;
}
return false;
}
/**
* 更新属性 同步到shader
*/
updateAttributes() {
this.particlesGeometry.attributes.color.needsUpdate = true;
this.particlesGeometry.attributes.oldColor.needsUpdate = true;
this.particlesGeometry.attributes.toPositions.needsUpdate = true;
this.particlesGeometry.attributes.oldPositions.needsUpdate = true;
this.particlesGeometry.attributes.toPositionsDuration.needsUpdate = true;
}
/**
* 更新粒子系统
// * @param {number} progress 动画进度
* @param {number} time 当前时间
*/
update(time: number) {
// PointsShaderMaterial.uniforms.progress.value = progress;
PointsShaderMaterial.uniforms.time.value = time;
}
/**
* 初始化粒子系统 随机方形排布
* @param {number} range 范围
*/
init(range: number = 1000) {
for (let i = 0; i < this.numberOfPoints; i++) {
const i3 = i * 3;
const x = (0.5 - Math.random()) * range;
const y = (0.5 - Math.random()) * range;
const z = (0.5 - Math.random()) * range;
this.toPositions[i3] = x;
this.toPositions[i3 + 1] = y;
this.toPositions[i3 + 2] = z;
const c = Math.random();
this.color[i3] = c;
this.color[i3 + 1] = c;
this.color[i3 + 2] = c;
}
this.particlesGeometry.attributes.toPositions.needsUpdate = true;
}
}
自定义shader
/*
* @Author: hongbin
* @Date: 2022-11-10 10:54:21
* @LastEditors: hongbin
* @LastEditTime: 2022-12-16 17:55:11
* @Description:粒子材料
*/
import vertexShader from "./points.vt.glsl";
import fragmentShader from "./points.fm.glsl";
import { AdditiveBlending,ShaderMaterial } from "three";
export const PointsShaderMaterial = new ShaderMaterial({
uniforms: {
time: { value: 0 },
//弥补自定义shader没有PointsMaterial材质的size属性
size: { value: 8 },
},
blending: AdditiveBlending,
// side: 2,
transparent: true,
vertexShader,
//弥补自定义shader没有PointsMaterial材质的sizeAttenuation属性
fragmentShader,
alphaTest: 0.001,
// depthTest: false,
depthWrite: false,
});
初始化控制器
/**
* 生成粒子系统控制器 传入粒子数量
*/
const PointsControl = new PointsControlClass(90686);
scene.add(PointsControl.particles);
加载模型 填充数据
td.loadGltf(url).then((g) => {
const targetMesh = g.scene.getObjectByProperty("type", "Mesh") as Mesh;
if (!targetMesh) throw new Error("获取目标物体失败");
const { array: positions} = targetMesh.geometry.attributes.position;
targetMesh.geometry.computeBoundingBox();
const color = { left: new Color("#f00"), right: new Color("#5f1"), dividingLine: 0.65 };
const params = {
duration: { min: 1000, max: 3000 },
color,
positions,
box3: targetMesh.geometry.boundingBox!,
};
PointsControl.append(params, 0);
});