文章目录
- 优点
- 实现思路
- 传递给Shader的数据
- 根据模型数据生成数据传递给Shader
- 自定义shader 连接cpu与gpu
- 顶点着色器 计算位置
- 片元着色器
优点
性能卓越
上一篇使用的更改坐标实现 9万个点 页面非常卡顿 光是计算9万个点坐标更替的js就已经造成了堵塞 尝试了在顶点着色器中实现动画发现无丝毫卡顿发生,实为粒子动效正道
实现思路
实现思路
计算交给GPU而不是js线程计算 这样页面不会卡顿
交给Shader的几个数据: 当前的进度 开始坐标 终点坐标 持续时间
传递给Shader的数据
/**
* 粒子控制器
* @param {number} numberOfPoints 数量必须大于所有模型中最大的点数量 不然显示不全
*/
constructor(numberOfPoints: number) {
this.numberOfPoints = numberOfPoints;
this.particlesGeometry = new BufferGeometry();
//顶点着色器的坐标
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.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();
this.particles = new Points(this.particlesGeometry, PointsShaderMaterial);
}
根据模型数据生成数据传递给Shader
/**
* 向某形状变换
* @param {ArrayLike<number>} array 数据
* @param { min: number; max: number } duration 时间段
*/
to(array: ArrayLike<number>, duration: { min: number; max: number }) {
const { length } = array;
const pointCount = length / 3;
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.positions[i3];
this.oldPositions[i3 + 1] = this.positions[i3 + 1];
this.oldPositions[i3 + 2] = this.positions[i3 + 2];
//设置终点
this.toPositions[i3] = array[r3];
this.toPositions[i3 + 1] = array[r3 + 1];
this.toPositions[i3 + 2] = array[r3 + 2];
//设置运动时间
const useDuration = duration.min + Math.random() * (duration.max - duration.min);
this.toPositionsDuration[i] = useDuration;
}
console.log(this.particlesGeometry);
}
在生成数据后 至关重要的一步是更新这些属性否则同步不到GPU中
this.particlesGeometry.attributes.position.needsUpdate = true;
this.particlesGeometry.attributes.toPositions.needsUpdate = true;
this.particlesGeometry.attributes.oldPositions.needsUpdate = true;
this.particlesGeometry.attributes.toPositionsDuration.needsUpdate = true;
自定义shader 连接cpu与gpu
然后通过uniform 传递当前的时间供shader计算应该当前应该是什么位置
通过自定义shader实现
import * as THREE from "three";
import vertexShader from "./points.vt.glsl";
import fragmentShader from "./points.fs.glsl";
import { AdditiveBlending } from "three";
export const PointsShaderMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
//弥补自定义shader没有PointsMaterial材质的size属性
size: { value: 8 }
},
blending: AdditiveBlending,
transparent: true,
vertexShader,
fragmentShader,
alphaTest: 0.001,
depthTest: false,
depthWrite: false,
});
逐帧更新 uniforms 的time
const clock = new Clock();
let t = 0;
td.animation(() => {
t += clock.getDelta();
PointsControl.update(t * 1000);
});
顶点着色器 计算位置
最后工作就交给 顶点着色器来计算粒子的位置
uniform float time;
uniform float size;
attribute vec3 toPositions;
attribute vec3 oldPositions;
attribute float toPositionsDuration;
void main() {
vec3 dispatchPos = position;
//顶点位置移动
//当前时间在点的运行时间中占比
float percent = time / toPositionsDuration;
vpercent = percent;
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);
} else {
dispatchPos = toPositions;
}
vec4 viewPosition = modelViewMatrix * vec4(dispatchPos, 1.0);
gl_Position = projectionMatrix * viewPosition;
gl_PointSize = size;
//近大远小效果 值自己调节
gl_PointSize *= (120. / -(modelViewMatrix * vec4(dispatchPos, 1.0)).z);
}
片元着色器
片元着色器基本不需要改动设置一个颜色即可
void main() {
gl_FragColor = vec4(vec3(0.1, 0.5, 0.4), 0.8);
}
完成代码
粒子控制器
/*
* @Author: hongbin
* @Date: 2022-12-14 10:48:40
* @LastEditors: hongbin
* @LastEditTime: 2022-12-14 15:34:19
* @Description:使用shader计算粒子位置 减小开销
*/
import {
AdditiveBlending,
BufferAttribute,
BufferGeometry,
Color,
Material,
Points,
PointsMaterial,
TextureLoader,
} from "three";
import TWEEN, { Tween } from "@tweenjs/tween.js";
import starMap from "../assets/img/star_09.png";
import { PointsShaderMaterial } from "./shader/points.material";
type ITween = Tween<{
x: number;
y: number;
z: number;
}>;
export class PointsControlClass {
numberOfPoints: number;
positions: Float32Array;
particles: Points<BufferGeometry, Material>;
// particlesMaterial: PointsMaterial;
particlesGeometry: BufferGeometry;
toPositions: Float32Array;
toPositionsDuration: Float32Array;
oldPositions: Float32Array;
/**
* 粒子控制器
* @param {number} numberOfPoints 数量必须大于所有模型中最大的点数量 不然显示不全
*/
constructor(numberOfPoints: number) {
this.numberOfPoints = numberOfPoints;
this.particlesGeometry = new BufferGeometry();
//顶点着色器的坐标
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.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();
this.particles = new Points(this.particlesGeometry, PointsShaderMaterial);
this.init();
}
/**
* 根据初始的粒子数量生成对应的动画控制器
*/
/**
* 向某形状变换
* @param {ArrayLike<number>} array 数据
* @param { min: number; max: number } duration 时间段
*/
to(array: ArrayLike<number>, duration: { min: number; max: number }) {
const { length } = array;
const pointCount = length / 3;
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.positions[i3];
this.oldPositions[i3 + 1] = this.positions[i3 + 1];
this.oldPositions[i3 + 2] = this.positions[i3 + 2];
//设置终点
this.toPositions[i3] = array[r3];
this.toPositions[i3 + 1] = array[r3 + 1];
this.toPositions[i3 + 2] = array[r3 + 2];
//设置运动时间
const useDuration = duration.min + Math.random() * (duration.max - duration.min);
this.toPositionsDuration[i] = useDuration;
}
console.log(this.particlesGeometry);
//至关重要
this.particlesGeometry.attributes.position.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;
}
setModelData(array: ArrayLike<number>) {
const { length } = array;
const pointCount = length / 3;
for (let i = 0, realCount = 0; i < this.numberOfPoints; i++, realCount++) {
//模型的点和生成的点数量未必相等 多的则重合前面的位置
realCount = realCount % pointCount;
const i3 = i * 3;
const r3 = realCount * 3;
this.positions[i3] = array[r3];
this.positions[i3 + 1] = array[r3 + 1];
this.positions[i3 + 2] = array[r3 + 2];
}
this.particlesGeometry.attributes.position.needsUpdate = true;
}
/**
* 初始化粒子系统 随机方形排布
* @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.positions[i3] = x;
this.positions[i3 + 1] = y;
this.positions[i3 + 2] = z;
this.oldPositions[i3] = x;
this.oldPositions[i3 + 1] = y;
this.oldPositions[i3 + 2] = z;
}
this.particlesGeometry.attributes.position.needsUpdate = true;
}
}
自定义shader
/*
* @Author: hongbin
* @Date: 2022-11-10 10:54:21
* @LastEditors: hongbin
* @LastEditTime: 2022-12-14 12:39:11
* @Description:粒子材料
*/
import * as THREE from "three";
import vertexShader from "./points.vt.glsl";
import fragmentShader from "./points.fs.glsl";
import { AdditiveBlending } from "three";
export const PointsShaderMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
//弥补自定义shader没有PointsMaterial材质的size属性
size: { value: 8 },
},
blending: AdditiveBlending,
// side: 2,
transparent: true,
// blending: THREE.AdditiveBlending,
vertexShader,
//弥补自定义shader没有PointsMaterial材质的sizeAttenuation属性
fragmentShader,
alphaTest: 0.001,
depthTest: false,
depthWrite: false,
});
着色器代码上述已列出
调用
/**
* 生成粒子系统控制器 传入粒子数量
*/
const PointsControl = new PointsControlClass(90686);
scene.add(PointsControl.particles);
const clock = new Clock();
let t = 0;
td.animation(() => {
t += clock.getDelta();
PointsControl.update(t * 1000);
});
//...加载完模型后 传递模型数据
const targetMesh = g.scene.children[0] as Mesh;
if (!targetMesh) return new Error("获取目标物体失败");
const { array, count } = targetMesh.geometry.attributes.position;
PointsControl.to(array, { min: 1000, max: 4000 });