three.js中粒子效果的实现方式大概分为三种:
1、Javascript直接计算粒子的状态变化,即基于CPU实现;
2、Javascript通知顶点着色器粒子的生命周期,由顶点着色器运行,即基于GPU实现;
3、粒子生成与状态维护全部由片元着色器负责,即屏幕特效,同样是基于GPU中实现。
粒子特效可以实现非常多的效果,如星空、烟雾、雨、灰尘、火等。粒子特效的优势是即使使用了成百上千的例子,也能保证比较高的帧率。
缺点是每个粒子都由一个始终面向相机的平面(两个三角形)组成。
点材质也是three.js最简单的类之一,相对于基类Material,它多做的事情只是传递了size,即点的尺寸这个值。
申明type是Points,执行渲染时,WebGL会绘制Point,即调用gl.drawArrays(gl.POINTS)。
type为Mesh时,three.js会调用gl.drawArrays(gl.TRIANGLES)。
着色器1
<script id="vertex-shader" type="x-shader/x-vertex">//// GLSL textureless classic 2D noise "cnoise",// with an RSL-style periodic variant "pnoise".// Author: Stefan Gustavson (stefan.gustavson@liu.se)// Version: 2011-08-22//// Many thanks to Ian McEwan of Ashima Arts for the// ideas for permutation and gradient selection.//// Copyright (c) 2011 Stefan Gustavson. All rights reserved.// Distributed under the MIT license. See LICENSE file.// https://github.com/ashima/webgl-noise//vec4 mod289(vec4 x){return x - floor(x * (1.0 / 289.0)) * 289.0;}vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}vec2 fade(vec2 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}// Classic Perlin noisefloat cnoise(vec2 P){vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);Pi = mod289(Pi); // To avoid truncation effects in permutationvec4 ix = Pi.xzxz;vec4 iy = Pi.yyww;vec4 fx = Pf.xzxz;vec4 fy = Pf.yyww;vec4 i = permute(permute(ix) + iy);vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;vec4 gy = abs(gx) - 0.5 ;vec4 tx = floor(gx + 0.5);gx = gx - tx;vec2 g00 = vec2(gx.x,gy.x);vec2 g10 = vec2(gx.y,gy.y);vec2 g01 = vec2(gx.z,gy.z);vec2 g11 = vec2(gx.w,gy.w);vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));g00 *= norm.x;g01 *= norm.y;g10 *= norm.z;g11 *= norm.w;float n00 = dot(g00, vec2(fx.x, fy.x));float n10 = dot(g10, vec2(fx.y, fy.y));float n01 = dot(g01, vec2(fx.z, fy.z));float n11 = dot(g11, vec2(fx.w, fy.w));vec2 fade_xy = fade(Pf.xy);vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);float n_xy = mix(n_x.x, n_x.y, fade_xy.y);return 2.3 * n_xy;}float map(float value, float oldMin, float oldMax, float newMin, float newMax) {return newMin + (newMax - newMin) * (value - oldMin) / (oldMax - oldMin);}varying vec3 vUv;varying float vTime;varying float vZ;uniform float time;void main(){vUv = position;vTime = time;vec3 newPos = position;vec2 peak = vec2(1.0 - abs(.5 - uv.x), 1.0 - abs(.5 - uv.y));vec2 noise = vec2(map(cnoise(vec2(0.3 * time + uv.x * 5., uv.y * 5.)), 0., 1., -2., (peak.x * peak.y * 30.)),map(cnoise(vec2(-0.3 * time + uv.x * 5., uv.y * 5.)), 0., 1., -2., 25.));//newPos.x += noise.x * 10.;newPos.z += noise.x * .06 * noise.y;vZ = newPos.z;vec4 mvPosition = modelViewMatrix * vec4( newPos, 1.0 );gl_PointSize = 5.0;gl_Position = projectionMatrix * mvPosition;}</script><script id="fragment-shader" type="x-shader/x-fragment">varying vec3 vUv;varying float vTime;varying float vZ;uniform sampler2D texture;float map(float value, float oldMin, float oldMax, float newMin, float newMax) {return newMin + (newMax - newMin) * (value - oldMin) / (oldMax - oldMin);}void main(){vec3 colorA = vec3(.6, 0.17, 0.17);vec3 colorB = vec3(0.17, 0.8, .7); //vec3 color = mix(colorA, colorB, vUv.x * vUv.y);float alpha = map(vZ / 2., -1. / 2., 30. / 2., 0.17, 1.); vec3 color = vec3(.5, .5, .6);gl_FragColor = vec4( color, alpha);//gl_FragColor = gl_FragColor * texture2D( texture, gl_PointCoord );}</script>
着色器2
<!-- built files will be auto injected --><!--粒子背景相关脚本--><script id="vertexShader" type="x-shader/x-vertex">attribute vec4 position;attribute float scale;uniform mat4 modelViewMatrix;uniform mat4 projectionMatrix;void main() {vec4 mvPosition = modelViewMatrix * position;gl_PointSize = scale*1.0 * ( 200.0 / - mvPosition.z );gl_Position = projectionMatrix * mvPosition;}</script><script id="fragmentShader" type="x-shader/x-fragment">void main() {if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.49 ) discard;//gl_FragColor = vec4(0.0,1.0,1.0,1.0);// 根据片元的x坐标,来设置片元的像素值if(gl_FragCoord.x < 120.0){// canvas画布上[0,20)之间片元像素值设置//gl_FragColor = vec4(1.0,0.0,0.0,1.0);// 片元沿着x方向渐变gl_FragColor = vec4(gl_FragCoord.x/1000.0*0.1,1.0,1.0,0.1);}else if (gl_FragCoord.x <= 1800.0) {// canvas画布上(20,1900]之间片元像素值设置//gl_FragColor = vec4(0.0,1.0,0.0,1.0);// 片元沿着y方向渐变gl_FragColor = vec4(0.0,gl_FragCoord.y/3000.0*1.0,1.0,0.1);}else {// canvas画布上(1900,1920]之间片元像素值设置//gl_FragColor = vec4(0.0,0.0,1.0,1.0);// 片元沿着z方向渐变gl_FragColor = vec4(0.0,1.0,gl_FragCoord.z/1000.0*1.0,0.1);}}</script>
依赖importmap
<script type="importmap">{"imports": {"three": "https://cdn.jsdelivr.net/npm/three@0.162.0/+esm","three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/","lil-gui": "https://threejsfundamentals.org/3rdparty/dat.gui.module.js","@tweenjs/tween.js": "https://cdn.jsdelivr.net/npm/@tweenjs/tween.js@23.1.1/dist/tween.esm.js","canvas-confetti": "https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/+esm"}}</script>
模块module代码
<script type="module">import * as THREE from 'three';import * as TWEEN from '@tweenjs/tween.js';import confetti from 'canvas-confetti';import { GUI } from 'lil-gui';import { OrbitControls } from 'three/addons/controls/OrbitControls.js';function drawWaveParticle(){let div = document.getElementById('webgl');let canvasWebgl = document.createElement('canvas');canvasWebgl.width = parseInt(window.innerWidth);canvasWebgl.height = parseInt(window.innerHeight);canvasWebgl.style.position = 'absolute';canvasWebgl.style.zIndex = -1;div.appendChild(canvasWebgl);let gl = canvasWebgl.getContext('webgl');let vertexShaderSource = document.getElementById('vertexShader').innerText;let fragShaderSource = document.getElementById('fragmentShader').innerText;let program = initShader(gl, vertexShaderSource, fragShaderSource);let aposLocation = gl.getAttribLocation(program, 'position');let scale = gl.getAttribLocation(program, 'scale');let modelViewMatrixLoc = gl.getUniformLocation(program, 'modelViewMatrix');let projectionMatrixLoc = gl.getUniformLocation(program, 'projectionMatrix');let SEPARATION = 100,AMOUNTX = 50,AMOUNTY = 50;let numParticles = AMOUNTX * AMOUNTY;let positions = new Float32Array(numParticles * 3);let scales = new Float32Array(numParticles);let i = 0, j = 0;for (let ix = 0; ix < AMOUNTX; ix++) {for (let iy = 0; iy < AMOUNTY; iy++) {positions[i] = ix * SEPARATION - ((AMOUNTX * SEPARATION) / 2); // xpositions[i + 1] = 0; // ypositions[i + 2] = iy * SEPARATION - ((AMOUNTY * SEPARATION) / 2); // zscales[j] = 1;i += 3;j++;}}let colorBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);gl.bufferData(gl.ARRAY_BUFFER, scales, gl.STATIC_DRAW);gl.vertexAttribPointer(scale, 1, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(scale);let buffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, buffer);gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);gl.vertexAttribPointer(aposLocation, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(aposLocation);gl.enable(gl.DEPTH_TEST);let width = window.innerWidth; let height = window.innerHeight; let camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);camera.position.set(200, 300, 200); camera.position.set(944, 206, -262);camera.lookAt(new THREE.Vector3(0, 0, 0)); camera.updateProjectionMatrix()camera.updateMatrixWorld(true)let mat4 = new THREE.Matrix4();mat4.copy(camera.projectionMatrix)let mxArr = new Float32Array(mat4.elements);gl.uniformMatrix4fv(projectionMatrixLoc, false, mxArr);let mat4y = new THREE.Matrix4();mat4y.copy(camera.matrixWorldInverse);//console.log(camera.matrixWorldInverse);let myArr = new Float32Array(mat4y.elements);gl.uniformMatrix4fv(modelViewMatrixLoc, false, myArr);let count = 0;let mouseX = 0,mouseY = 0;let windowHalfX = window.innerWidth / 2;let windowHalfY = window.innerHeight / 2;function draw() {camera.position.x += (mouseX - camera.position.x) * 0.001;camera.updateMatrixWorld(true)mat4y.copy(camera.matrixWorldInverse);let myArr = new Float32Array(mat4y.elements);gl.uniformMatrix4fv(modelViewMatrixLoc, false, myArr);let i = 0,j = 0;for (let ix = 0; ix < AMOUNTX; ix++) {for (let iy = 0; iy < AMOUNTY; iy++) {positions[i + 1] = (Math.sin((ix + count) * 0.3) * 50) +(Math.sin((iy + count) * 0.5) * 50);scales[j] = (Math.sin((ix + count) * 0.3) + 1.3) * 8 +(Math.sin((iy + count) * 0.5) + 1.3) * 8;i += 3;j++;}}count += 0.1;gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);gl.bufferData(gl.ARRAY_BUFFER, scales, gl.STATIC_DRAW);gl.bindBuffer(gl.ARRAY_BUFFER, buffer);gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);requestAnimationFrame(draw);gl.drawArrays(gl.POINTS, 0, 2500);}draw();function initShader(gl, vertexShaderSource, fragmentShaderSource) {let vertexShader = gl.createShader(gl.VERTEX_SHADER);let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(vertexShader, vertexShaderSource);gl.shaderSource(fragmentShader, fragmentShaderSource);gl.compileShader(vertexShader);gl.compileShader(fragmentShader);let program = gl.createProgram();gl.attachShader(program, vertexShader);gl.attachShader(program, fragmentShader);gl.linkProgram(program);gl.useProgram(program);return program;}document.addEventListener('mousemove', onDocumentMouseMove, false);document.addEventListener('touchstart', onDocumentTouchStart, false);document.addEventListener('touchmove', onDocumentTouchMove, false);function onDocumentMouseMove(event) {mouseX = event.clientX - windowHalfX;mouseY = event.clientY - windowHalfY;}function onDocumentTouchStart(event) {if (event.touches.length === 1) {event.preventDefault();mouseX = event.touches[0].pageX - windowHalfX;mouseY = event.touches[0].pageY - windowHalfY;}}function onDocumentTouchMove(event) {if (event.touches.length === 1) {event.preventDefault();mouseX = event.touches[0].pageX - windowHalfX;mouseY = event.touches[0].pageY - windowHalfY;}}}class SceneViewer {constructor(options) {this.$el = options.el;this.time = 0;this.bindAll();this.init();}bindAll() {this.render = this.render.bind(this);this.resize = this.resize.bind(this);}init() {this.textureLoader = new THREE.TextureLoader();this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);this.camera.lookAt(new THREE.Vector3(0, 0, 0));let boxSize = {width: 1,height: 1, thickness: 1};const fov = 45;const angle = fov / 2; // 夹角const rad = THREE.MathUtils.degToRad(angle); // 转为弧度值const distanceZ = boxSize.width / 2 / Math.tan(rad) * 10;/*** 调整相机的 X 轴位置,让视野能同时看到盒子顶部和侧面* 调整相机的 Z 轴位置,使盒子完整显示到场景 */this.camera.position.set(0, 1, distanceZ);this.scene = new THREE.Scene();this.renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});this.renderer.setPixelRatio(window.devicePixelRatio);this.renderer.setSize(window.innerWidth, window.innerHeight);let geometry = new THREE.BoxGeometry(boxSize.width, boxSize.height, boxSize.tickness);let material = new THREE.MeshNormalMaterial();this.mesh = new THREE.Mesh(geometry, material);this.scene.add(this.mesh);/* 相机轨道控制器 */new OrbitControls(this.camera, this.renderer.domElement);const axesHelper = new THREE.AxesHelper(10); // 辅助坐标轴const gridHelper = new THREE.GridHelper(10, 10); // 辅助网格线this.scene.add(axesHelper, gridHelper);const ambientLight = new THREE.AmbientLight('#fff', 1); // 环境光const directLight = new THREE.DirectionalLight('#fff', 3); // 平行光this.scene.add(ambientLight, directLight);const cubeMaterial = new THREE.MeshLambertMaterial({'color': 'gray',})const cubeGeometry = new THREE.CylinderGeometry(0.5, 1, 1)const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)this.scene.add(cube)this.$el.appendChild(this.renderer.domElement);this.createParticles();this.createLights();this.bindEvents();this.resize();this.render();}createLights(){let directionX = 10, directionY = 10, directionZ = 10;const hemisphere = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);// move the light right, up, and towards ushemisphere.position.set(10, 10, 10);const ambient = new THREE.AmbientLight(0xffffff, 1); // 环境光const spot = new THREE.SpotLight(0xfdf4d5);spot.position.set(5, directionY * 4, 0);spot.angle = Math.PI / 2;spot.power = 2000;// eslint-disable-next-line @typescript-eslint/no-unused-varsconst spotLightHelper = new THREE.SpotLightHelper(spot, 0x00f);const direct = new THREE.DirectionalLight(0xffffff, 3); // 平行光direct.position.set(-directionX / 3, directionY * 4, directionZ * 1.5);direct.castShadow = true;direct.shadow.camera.left = -directionX;direct.shadow.camera.right = directionX;direct.shadow.camera.top = directionZ;direct.shadow.camera.bottom = -directionZ;// eslint-disable-next-line @typescript-eslint/no-unused-varsconst directLightHelper = new THREE.DirectionalLightHelper(direct, 1, 0xf00);this.scene.add(hemisphere)this.scene.add(ambient)this.scene.add(spot)this.scene.add(direct)}createParticles() {//const plane = new THREE.PlaneBufferGeometry(500, 250, 250, 125);const plane = new THREE.PlaneGeometry(500, 250, 250, 125);const textureLoader = new THREE.TextureLoader();textureLoader.crossOrigin = '';const material = new THREE.ShaderMaterial({uniforms: {time: { value: 1.0 },texture: { value: textureLoader.load("https://s3-us-west-2.amazonaws.com/s.cdpn.io/1081752/spark1.png") },resolution: { value: new THREE.Vector2() } },vertexShader: document.getElementById('vertex-shader').textContent,fragmentShader: document.getElementById('fragment-shader').textContent,blending: THREE.AdditiveBlending,depthTest: false,transparent: true });//console.log(material.uniforms.texture);//const material = new THREE.PointsMaterial( { size: 1 } );this.particles = new THREE.Points(plane, material);this.particles.rotation.x = this.degToRad(-90);this.scene.add(this.particles);}bindEvents() {// window.addEventListener('mousemove', this.mousemove);window.addEventListener('resize', this.resize);}resize() {const w = window.innerWidth;const h = window.innerHeight;this.renderer.setSize(w, h);this.camera.aspect = w / h;this.camera.updateProjectionMatrix();}moveParticles() {this.particles.material.uniforms.time.value = this.time;this.particles.material.needsUpdate = true;}// Animationsrender() {requestAnimationFrame(this.render);this.time += .01;this.mesh.rotation.x += 0.01;this.mesh.rotation.y += 0.02;this.moveParticles();this.renderer.render(this.scene, this.camera);}// UtilsdegToRad(angle) {return angle * Math.PI / 180;}}initViewer = ()=>{drawWaveParticle();//let container = document.getElementById(domId);let container = document.querySelector('#webgl');let element = {el: container};new SceneViewer(element);}</script>
挂载到DOM渲染
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="theme-color" content="#000000" /><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="renderer" content="webkit"><meta name="force-rendering" content="webkit"><meta name="google-site-verification" content="FTeR0c8arOPKh8c5DYh_9uu98_zJbaWw53J-Sch9MTg"><meta data-rh="true" name="keywords" content="three.js实现粒子动画"><meta data-rh="true" name="description" content="three.js实现粒子动画"><meta data-rh="true" property="og:title" content="three.js实现粒子动画"><link rel="icon" href="./favicon.ico"><title>three.js实现粒子动画</title><style>body {padding: 0;margin: 0;font: normal 14px/1.42857 Tahoma;background: radial-gradient(#a1b2c3, #123456);}.container {position: relative;}.mask{width: 100vw;height: 100vh;position: absolute;left: 0;background: radial-gradient(#123456, #c1d1e1);z-index: -1;}#webgl{position: absolute;left: 0;z-index: 1;}</style>
</head>
<body onload="initViewer()"><div class="container"><!-- 遮罩 --><div class="mask"></div><div id="webgl"></div></div><script>let initViewer = null</script></body>
</html>
粒子场景效果