使用粒子效果动画组成模型[自定义shader实现]

23 篇文章 1 订阅


请添加图片描述

视频教程:

THREE.js粒子效果-将模型转成粒子

优点

性能卓越
上一篇使用的更改坐标实现 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 });
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值