Three.js粒子变换特效

关于单个模型用粒子组成可参考使用粒子效果动画组成模型[自定义shader实现]

本文介绍多模型切换技巧

思路

  1. 获取需要切换的几个模型的数据就是每个点的坐标在geometry.attributes.positionarray即时构成mesh的数据。
    例:
const targetMesh = g.scene.getObjectByProperty("type", "Mesh");
if (!targetMesh) throw new Error("获取目标物体失败");
const { array} = targetMesh.geometry.attributes.position;
  1. 将数据保存到一个列表中供切换数据使用
  2. 切换模型数据实现思路

粒子的动画应该使用自定义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);
    });