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 noise
float 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 permutation
vec4 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); // x
positions[i + 1] = 0; // y
positions[i + 2] = iy * SEPARATION - ((AMOUNTY * SEPARATION) / 2); // z
scales[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 us
hemisphere.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-vars
const 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-vars
const 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;
}
// Animations
render() {
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);
}
// Utils
degToRad(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>
粒子场景效果