前言
通过three.js,使用着色器如何实现烟花的效果,下面图片是实现的思路。
下面图是降落
下面图是闪烁
项目的结构
一、代码
script.js文件代码
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import GUI from 'lil-gui'
import gsap from 'gsap'
import {Sky} from 'three/addons/objects/Sky.js' // three.js 自带
import fireworkVertexShader from './shaders/firework/vertex.glsl'
import fireworkFragmentShader from './shaders/firework/fragment.glsl'
/*
vertex.glsl 顶点着色器
fragment.glsl 片段着色器
拖动控制台,希望内部的粒子整体有个特定的尺寸,实现像素比
因为在拖动时候,平面大小之间缩放比例不同,有些缩放更大,有些缩放更小,不协调 (问题)
解决:
要把渲染的分辨率发送到阴影
sizes.resolution
gl_PointSize = uSize * uResolution.y;
渲染高度,与高度成正比
动画由5个不同的阶段组成
粒子开始向各个方向快速膨胀
他们的规模扩大得更快
他们开始慢慢地倒下
他们缩小规模
它们消失时闪烁着光芒
*/
/**
* Base
*/
// Debug
const gui = new GUI({ width: 340 })
// Canvas
const canvas = document.querySelector('canvas.webgl')
// Scene
const scene = new THREE.Scene()
// Loaders
const textureLoader = new THREE.TextureLoader()
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
pixelRatio:Math.min(window.devicePixelRatio,2) // window.devicePixelRatio 获取当前设备的像素比
}
sizes.resolution = new THREE.Vector2(sizes.width * sizes.pixelRatio,sizes.height * sizes.pixelRatio) // 这里宽高*像素比
window.addEventListener('resize', () =>
{
// Update sizes
sizes.width = window.innerWidth
sizes.height = window.innerHeight
sizes.pixelRatio = Math.min(window.devicePixelRatio,2) // 多个屏幕时,更新像素比,防止
sizes.resolution.set(sizes.width * sizes.pixelRatio,sizes.height * sizes.pixelRatio)
// Update camera
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
// Update renderer
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(sizes.pixelRatio)
})
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(25, sizes.width / sizes.height, 0.1, 100)
camera.position.set(1.5, 0, 6)
scene.add(camera)
// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(sizes.pixelRatio)
/**
* fireworks
*/
// 引入加载纹理
const textures = [
textureLoader.load('./particles/1.png'),
textureLoader.load('./particles/2.png'),
textureLoader.load('./particles/3.png'),
textureLoader.load('./particles/4.png'),
textureLoader.load('./particles/5.png'),
textureLoader.load('./particles/6.png'),
textureLoader.load('./particles/7.png'),
textureLoader.load('./particles/8.png'),
]
// 创建烟花
const createFirework = (count,position,size,texture,radius,color) =>{
// Geometry
const positionArray = new Float32Array(count*3) // 为什么*3 :xyz
const sizeArray = new Float32Array(count)
const timeMultipliersArray = new Float32Array(count)
for(let i = 0; i< count ; i++){ // 处理数据
const i3 = i * 3
const spherical = new THREE.Spherical( // 设置球形状
radius * (0.75 + Math.random() * 0.25),
Math.random() * Math.PI,
Math.random() * Math.PI * 2
)
const position = new THREE.Vector3()
position.setFromSpherical(spherical)
positionArray[i3] = position.x; // 位置代替随机数
positionArray[i3+1] = position.y;
positionArray[i3+2] = position.z;
sizeArray[i] = Math.random()
timeMultipliersArray[i] = 1 + Math.random() // 消失的更快
}
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position',new THREE.BufferAttribute(positionArray,3))
geometry.setAttribute('aSize',new THREE.BufferAttribute(sizeArray,1))
geometry.setAttribute('aTimeMultiplier',new THREE.Float32BufferAttribute(timeMultipliersArray,1))
// material // 点材质 详情请参考之前实现粒子的注释 更改成着色器
texture.flipY = false // 反转
const material = new THREE.ShaderMaterial(
{
vertexShader:fireworkVertexShader,
fragmentShader:fireworkFragmentShader,
uniforms:{
uSize: new THREE.Uniform(size),
uResolution:new THREE.Uniform(sizes.resolution),
uTexture:new THREE.Uniform(texture),
uColor:new THREE.Uniform(color),
uProgress:new THREE.Uniform(0), // 动画
},
transparent:true,
depthWrite:false, // 深度写入
blending:THREE.AdditiveBlending, // 混合
}
)
// Points 点网格 ,类似Mesh 接受几何体和材质
const firework = new THREE.Points(
geometry,
material
)
firework.position.copy(position) // 位置复制给烟花
scene.add(firework)
// Destroy, 防止性能加载问题, 烟花完成动画之后进行销毁,减少性能开销
const destroy = () => {
scene.remove(firework)
geometry.dispose()
material.dispose()
}
// Animate
gsap.to(
material.uniforms.uProgress,
{
value:1,
duration:3,
ease:'linear',
onComplete:destroy, // 完成动画销毁
}
)
}
// 创建随机烟火
const createRandomFirework = () =>{
const count = Math.round(400 + Math.random() * 1000) // 数量
const position = new THREE.Vector3( // 位置
(Math.random() - 0.5) * 2,
Math.random(),
(Math.random() - 0.5) * 2
)
const size = 0.1 + Math.random() * 0.1 // 大小
const texture = textures[Math.floor(Math.random() * textures.length)] // 纹理
const radius = 0.5 + Math.random() // 半径
const color = new THREE.Color() // 颜色
color.setHSL(Math.random(),1,0.7) // 设置随机颜色的方法
createFirework(count,position,size,texture,radius,color)
}
createRandomFirework() // 自动化创建烟花,
window.addEventListener('click',()=>{
createRandomFirework()
})
/*
Sky
*/
// Add Sky
const sky = new Sky();
sky.scale.setScalar(450000);
scene.add(sky);
const sun = new THREE.Vector3();
/// GUI
const effectController = {
turbidity: 10,
rayleigh: 3,
mieCoefficient: 0.005,
mieDirectionalG: 0.95,
elevation: -2.2,
azimuth: 180,
exposure: renderer.toneMappingExposure
};
function guiChanged() {
const uniforms = sky.material.uniforms;
uniforms[ 'turbidity' ].value = effectController.turbidity;
uniforms[ 'rayleigh' ].value = effectController.rayleigh;
uniforms[ 'mieCoefficient' ].value = effectController.mieCoefficient;
uniforms[ 'mieDirectionalG' ].value = effectController.mieDirectionalG;
const phi = THREE.MathUtils.degToRad( 90 - effectController.elevation );
const theta = THREE.MathUtils.degToRad( effectController.azimuth );
sun.setFromSphericalCoords( 1, phi, theta );
uniforms[ 'sunPosition' ].value.copy( sun );
renderer.toneMappingExposure = effectController.exposure;
renderer.render( scene, camera );
}
gui.add( effectController, 'turbidity', 0.0, 20.0, 0.1 ).onChange( guiChanged );
gui.add( effectController, 'rayleigh', 0.0, 4, 0.001 ).onChange( guiChanged );
gui.add( effectController, 'mieCoefficient', 0.0, 0.1, 0.001 ).onChange( guiChanged );
gui.add( effectController, 'mieDirectionalG', 0.0, 1, 0.001 ).onChange( guiChanged );
gui.add( effectController, 'elevation', -3, 90, 0.01 ).onChange( guiChanged );
gui.add( effectController, 'azimuth', - 180, 180, 0.1 ).onChange( guiChanged );
gui.add( effectController, 'exposure', 0, 1, 0.0001 ).onChange( guiChanged );
guiChanged();
/**
* Animate
*/
const tick = () =>
{
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fireworks</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<canvas class="webgl"></canvas>
<script type="module" src="./script.js"></script>
</body>
</html>
style.css
*
{
margin: 0;
padding: 0;
}
html,
body
{
overflow: hidden;
}
.webgl
{
position: fixed;
top: 0;
left: 0;
outline: none;
}
vertex.glsl
uniform float uSize;
uniform vec2 uResolution;
uniform float uProgress;
attribute float aSize;
attribute float aTimeMultiplier;
/*
remap()
value:您要重新映射的值
originMin和originMax:原始范围的开始和结束
destinationMin和destinationMax:目标范围的开始与结束
clamp(x, minVal, maxVal)
将x值钳于minVal和maxVal之间,意思就是当x<minVal时返回minVal,当x>maxVal时返回maxVal,当x在minVal和maxVal之间时,返回x
pow(x,y)
x的y次方。如果x小于0,结果是未定义的。同样,如果x=0并且y<=0,结果也是未定义的。
*/
#include ../includes/remap.glsl // 引入remap.glsl文件,将remap函数分离出去
void main(){
float progress = uProgress * aTimeMultiplier;
vec3 newPosition = position;
// Exploding 爆炸
float explodingProgress = remap(progress,0.0,0.1,0.0,1.0); // 烟花 爆炸的运动曲线
explodingProgress = clamp(explodingProgress,0.0,1.0); // 烟花 限制爆炸的运动范围
explodingProgress = 1.0 - pow(1.0 - explodingProgress,3.0); // 再次控制运动状态,快到慢
newPosition *= explodingProgress;
// 掉落
float fallingProgress = remap(progress, 0.1,1.0 ,0.0 , 1.0);
fallingProgress = clamp(fallingProgress, 0.0 ,1.0);
fallingProgress = 1.0 - pow(1.0 - fallingProgress,3.0); // 控制落下的状态
newPosition.y -= fallingProgress * 0.2;
// 缩放
float sizeOpeningProgress = remap(progress,0.0 , 0.125, 0.0 , 1.0); // 开始
float sizeClosingProgress = remap(progress,0.125 , 1.0, 1.0 , 0.0); // 结束
float sizeProgress = min(sizeOpeningProgress,sizeClosingProgress);
sizeProgress = clamp(sizeProgress,0.0,1.0); // 控制范围 控制缩放的范围
// 闪烁
float twinklingProgress = remap(progress, 0.2,0.8,0.0,1.0);
twinklingProgress = clamp(twinklingProgress,0.0,1.0); // 限制
float sizeTwinkling = sin(progress * 30.0) * 0.5 + 0.5; // 闪烁
sizeTwinkling = 1.0 - sizeTwinkling * twinklingProgress;
// Final position
vec4 modelPosition = modelMatrix * vec4(newPosition,3.0); // 通过模型矩阵获得模型位置
vec4 viewPosition = viewMatrix * modelPosition; // // 通过模型矩阵获得模型位置
gl_Position = projectionMatrix * viewPosition;
// Final size
gl_PointSize = uSize * uResolution.y * aSize * sizeProgress * sizeTwinkling;
gl_PointSize *= 1.0 / - viewPosition.z;
if(gl_PointSize < 1.0){ // 防止window上防止会出现的问题,有些烟花会特别小就一个像素点
gl_Position = vec4(9999.9);
}
}
fragment.glsl
uniform sampler2D uTexture;
uniform vec3 uColor;
void main(){
// 从纹理中提取像素
// vec4 textureColor = texture(uTexture,gl_PointCoord); // 从纹理中选择像素的功能
float textureAlpha = texture(uTexture,gl_PointCoord).r;
// Final color
// gl_FragColor = textureColor;
gl_FragColor = vec4(uColor,textureAlpha);
// 色调映射
#include <tonemapping_fragment>
#include <colorspace_fragment>
}
remap.glsl
/*
value:您要重新映射的值
originMin和originMax:原始范围的开始和结束
destinationMin和destinationMax:目标范围的开始与结束
*/
// 特殊的数据函数,改变烟花的运动曲线
float remap(float value, float originMin, float originMax, float destinationMin, float destinationMax)
{
return destinationMin + (value - originMin) * (destinationMax - destinationMin) / (originMax - originMin);
}
二、效果
烟花-shanders
总结
这外形是个圆形,如果我想要别的形状,比如苏珊(monkey)代码应该怎么改?
烟花猴子 - shaders