threejs 手写 GLSL 着色器效果 - 烟花效果

957 阅读3分钟

image.png

1. 准备 threejs 的渲染操作 基础绘制坐标系

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({
  antialias: true // 消除锯齿
});


camera.position.set(0, 0, 1);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

// 绘制坐标轴
const axesHelper = new THREE.AxesHelper(10);
scene.add(axesHelper);

renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

// 渲染函数
const render = (callback, time) => {
  camera.updateProjectionMatrix();
  renderer.render(scene, camera);
  callback && callback(time);
  requestAnimationFrame((time) => render(callback, time));
}
render();

2. 准备 vertexShader | fragmentShader 烟花效果基础着色器

2.1 基础的顶点着色器格式

  • modelPosition 为模型的位置信息 设置 xyz 可修改位置
void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    gl_Position = projectionMatrix * (viewMatrix * modelPosition);
}

2.2 基础片元着色器格式

  • gl_FragColor 设置基础的颜色 vec4 = rgba;
void main() {
    gl_FragColor = vec4(1.0);
}

3 基础着色器使用 封装创建Box的方法 并 return 当前材质

  • uniforms 传递参数给着色器内部接收使用
  • uColor = rgb;
const createBox = (color,y = -1) => {
    const geometry = new THREE.BoxGeometry( 0.3, 0.1, 0.1 );
    const BoxMaterial = new THREE.ShaderMaterial({
        uniforms:{
            uColor:{
                value: new THREE.Color(0x73162392)
            }
        },
        vertexShader:`
            void main() {
                vec4 modelPosition = modelMatrix * vec4(position, 1.0);
                gl_Position = projectionMatrix * (viewMatrix * modelPosition);
            }
        `,
        fragmentShader:`
            uniform vec3 uColor;
            void main() {
                gl_FragColor = vec4(uColor,1.0);
                //gl_FragColor = vec4(1.0,1.0,1.0,1.0);
            }
        `,
    });
    const cube = new THREE.Mesh( geometry, BoxMaterial );
    cube.position.y = y;
    scene.add( cube );
    
    return {
        BoxMaterial:BoxMaterial
    }
}

//执行
createBox();
  • 效果如下 立方体图形

image.png

4 基础着色器使用 创建单个点 (烟花绽放效果就是由很多的点散开形成)

  • gl_PointSize 着色器材质创建点时需要在 顶点着色器设置 点的 大小 (不然点会显示不出来)
  • distance 计算两个向量的距离
  • gl_PointCoord 和 uv 相似但是又不同

image.png

  • opacity 设置点的 透明度 (结合代码解析)

    • 1 float opacity = distance(gl_PointCoord , vec2(0.5)); 得到一个 两个向量距离的透明度

    image.png

    • 2 透明度反转 可以看到透明度不明显 float opacity = 1.0 - distance(gl_PointCoord , vec2(0.5));

    image.png

    • 3 我们 再乘一个值使效果明显float opacity = 1.0 - distance(gl_PointCoord , vec2(0.5)) * 3.0;

    image.png

  • 代码

const geometry = new THREE.BufferGeometry();
const material = new THREE.ShaderMaterial({
    vertexShader: `
    attribute vec3 aStep;
    uniform float uTime;
    uniform float uSize;

    void main() {
        vec4 modelPosition = modelMatrix * vec4( position, 1.0 );
        gl_Position = projectionMatrix * viewMatrix * modelPosition;
        gl_PointSize = 20.0;
    }
`,
    fragmentShader: `
    void main() {
        // 解析这里
        float opacity = distance(gl_PointCoord , vec2(0.5));
        gl_FragColor = vec4(1.0,0,0,opacity); 
    } 
     `,
    transparent: true,  // 如果设置材质透明则必须要设置
    blending: THREE.AdditiveBlending,  // 混合目标 表示两个点 重合 需要怎么显示 
    depthWrite: false, //渲染此材质是否对深度缓冲区有任何影响。默认为true。
 });
 
// 点的位置 
const postions = new Float32Array([0, 0, 0]);
geometry.setAttribute(
    'position',
    new THREE.BufferAttribute(postions, 3)
);
//   生成点
const points = new THREE.Points(geometry, material);
scene.add(points);

点效果

image.png

5. 封装创建点发射一个点到指定的位置的方法

5.1 createClick 方法

  • pointsArray 创建的点添加到数组中
  • color 随机颜色值
  • from 点开始发射的位置
  • position 随机指定位置
  • createBox 创建一个 Box 盒子
let pointsArray = [];
let box = createBox();
const createClick = () => {
    // 随机颜色值
    let color = new THREE.Color(`hsl(${Math.floor(Math.random() * 360)},100%,80%)`);
    let position = {
        x: Math.random() - 0.5,
        y: Math.abs(Math.random() + 0.5),
        z: Math.random() - 0.5
    }
    // 点开始的位置
    let from = { x: 0, y: -1, z: 0 };
    const point = createPoint(color, position, from);
    box.material.uniforms.uColor.value = color;
    point.addScene();
    pointsArray.push(point);
}
window.addEventListener('click', createClick);

5.2 创建点的方法 createPoint

  • modelPosition 可以修改模型的位置
    • modelPosition.xyz += (aStep*uTime); aStep 就是to 的 xyz ,uTime 就是 [0,1]
function createPoint (color, position, from) {
    const geometry = new THREE.BufferGeometry();
    const material = new THREE.ShaderMaterial({
        uniforms: {
            uTime: {
                value: 0
            },
            uColor: {
                value: color
            },
            uSize: {
                value: 20.0
            }
        },
        vertexShader: ` 
            attribute vec3 aStep;
            uniform float uTime;
            uniform float uSize;

            void main() {
                vec4 modelPosition = modelMatrix * vec4( position, 1.0 );

                // 设置点的 位置 aStep 就是 to 的 xyz  uTime 就是 [0,1];
                modelPosition.xyz += (aStep*uTime);

                gl_Position = projectionMatrix * viewMatrix * modelPosition;
                gl_PointSize = uSize;
            }
        `,
        fragmentShader: `
            uniform vec3 uColor;
            void main() {
                float str = 1.0 - distance(gl_PointCoord , vec2(0.5)) * 3.0;
                gl_FragColor = vec4(uColor,str);
            }
        `,
        transparent: true,
        blending: THREE.AdditiveBlending,
        depthWrite: false
    });

    // 设置点开始的位置
    const buffreArray = new Float32Array(3);
    buffreArray[0] = from.x;
    buffreArray[1] = from.y;
    buffreArray[2] = from.z;

    // 点发射到什么位置 也可以直接 写 to.x to.y to.z
    const asteArray = new Float32Array(3);
    asteArray[0] = to.x - from.x;
    asteArray[1] = to.y - from.y;
    asteArray[2] = to.z - from.z;

    // 设置属性 传递倒着色器中
    geometry.setAttribute(
        'aStep',
        new THREE.BufferAttribute(asteArray, 3)
    );
    geometry.setAttribute(
        'position',
        new THREE.BufferAttribute(buffreArray, 3)
    );

    // 创建点
    const points = new THREE.Points(geometry, material);

    // 获取一个 执行的时间
    const clock = new THREE.Clock();
    
    // 创建烟花
    const fireworks = createFireworks(color , to);
    fireworks.addScene();
    
    return {
        material,
        // 添加材质 到 scene 中
        addScene(){
            scene.add(points);
        },
        update() {
            // 获取时间
            const elapsedTime = clock.getElapsedTime();

            if (elapsedTime < 1) {
                material.uniforms.uTime.value = elapsedTime;
            } else {
                const time = elapsedTime - 1;
                material.uniforms.uSize.value = 0;
                points.clear();
                geometry.dispose();
                scene.remove(points);
                
                
                // 这里是创建烟花的代码 ....
                // 设置烟花显示
                fireworks.update(time);
                
           }
        }
    }
}

5.3 在 render 方法中对生成的点进行运动

  • 更新生成的每个点的 update 方法 自动发射点
let oldTime = 0;
render((time) => {
    if(time - oldTime > 200){
        createPoint();
        oldTime = time;
    }
    objectF.forEach((item) => {
        item.update();
    })
});

5.4 效果

6 烟花散开 createFireworks 函数封装

function createFireworks(color, to) {
    const fireworkGeometry = new THREE.BufferGeometry();
    const fireworkMateial = new THREE.ShaderMaterial({
        vertexShader:`
            attribute float aScale;
            attribute vec3 aRandom;
            uniform float uTime; 

            uniform float uSize;
            void main() {
                vec4 modelPosition = modelMatrix * vec4(position, 1.0);
                modelPosition.xyz += aRandom*uTime;
                gl_Position = projectionMatrix * (viewMatrix * modelPosition);
                gl_PointSize = (uSize*aScale)-uTime*20.0;
            }
        `,
        fragmentShader:`
            uniform vec3 uColor;
            void main() {
                float distanceTo = distance(gl_PointCoord , vec2(0.5));
                float str = distanceTo * 2.0;
                str = 1.0 - str;
                str = pow(str,1.5);
                gl_FragColor = vec4(uColor,str); 
            }
        `,
        blending:THREE.AdditiveBlending,
        depthWrite:false,
        uniforms:{
            uTime:{
                value:0
            },
            uSize:{
                value:0.0
            },
            uColor:{
                value:color
            }
        }
    });
    
    // 随机的烟花数量
    const fireworkConst = 180 + Math.floor(Math.random() * 180);
    const positionFireworksArray = new Float32Array(fireworkConst * 3); // 位置 
    const scaleFireworkArray = new Float32Array(fireworkConst); // 大小
    const direcationArray = new Float32Array(fireworkConst * 3); // 移动方向

    for (let index = 0; index < fireworkConst; index++) {
        // 开始烟花的位置
        positionFireworksArray[index * 3 + 0] = to.x;
        positionFireworksArray[index * 3 + 1] = to.y;
        positionFireworksArray[index * 3 + 2] = to.z;
        // 设置烟花所有粒子的初始大小
        scaleFireworkArray[index] = Math.random();
        // 设置旋转的角度 四周发射的角度
        let theta = Math.random() * 2 * Math.PI;
        let beta = Math.random() * 2 * Math.PI;

        // 半径
        let r = Math.random();

        // 最不了解的是这里 三角形的弧度值
        direcationArray[index * 3 + 0] = r * Math.sin(theta) + r * Math.sin(beta);
        direcationArray[index * 3 + 1] = r * Math.cos(theta) + r * Math.cos(beta);
        direcationArray[index * 3 + 2] = r * Math.sin(theta) + r * Math.cos(beta);
    }


    fireworkGeometry.setAttribute(
        'position',
        new THREE.BufferAttribute(positionFireworksArray, 3)
    );
    // 大小
    fireworkGeometry.setAttribute(
        'aScale',
        new THREE.BufferAttribute(scaleFireworkArray, 3)
    );
    // 随技方向
    fireworkGeometry.setAttribute(
        'aRandom',
        new THREE.BufferAttribute(direcationArray, 3)
    );

    const foreworksPoints = new THREE.Points(fireworkGeometry, fireworkMateial);

    return {
        fireworkGeometry,
        fireworkMateial,
        delete() {
            fireworkMateial.uniforms.uSize.value = 0;
            foreworksPoints.clear();
            fireworkGeometry.dispose();
            scene.remove(foreworksPoints);
        },
        update(time){
            fireworkMateial.uniforms.uTime.value = time;
            fireworkMateial.uniforms.uSize.value = 20.0;
            if(time > 5){
                this.delete();
            }
        },
        addScene() {
            scene.add(foreworksPoints);
        }
    }
}

7 最终效果