【Three.js基础学习】30.Firework Shaders

前言

通过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()

html.html

<!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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值