ThreeJS还原`流浪气球`

5,074 阅读3分钟

大家好,我是大傻。近些日子,想必到大家也看了新闻,我们的热气球由于风向问题,不小心飞到了pl国,结果引起了pl国的过度防范,在此,大傻还原了当时的场景,一起来看看吧。

开始 前期准备工作

先来看下效果吧。 test.gif

素材思考

根据效果,我们首先先来捋一遍我们需要的素材。

  • 热气球(可以拿上一篇的孔明灯来代替)
  • 大炮模型(溜溜网有免费素材)
  • 房屋模型(溜溜网)

内容思考

根据效果,我们可以总结出以下几点

  • 场景需要一个光源,我们没有去引入HDR作为环境

  • 大炮发射可以分为三个步骤

    • 发射线性炮弹
    • 轨迹消失 炮弹炸裂开来
    • 炮弹的炸裂呈圆形效果且颜色随机
  • 热气球的位置有一定的约束(相对于炮弹的位置约束)

技术思考

在上一篇中我们是用了gsap最终完成了热气球的动画模拟,这一篇中我们可以通过着色器shader使用来完成炮弹的轨迹动画,我们在此拆分下需要完成的工作。

  • 创建热气球函数(也可以和上篇一样,循环生成)
  • 创建炮弹轨迹函数
  • 创建最终爆炸效果函数
  • 通过点击调用我们的创建函数,并在相对时间后调用另外一个函数

中期 开发阶段

项目初始化

image.png 话不多说,首先还是我们的素材以及package.json,素材链接

{
  "name": "biubiubiu",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "dev": "parcel src/index.html",
    "build": "parcel build src/index.html"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@parcel/transformer-glsl": "2.8.3",
    "parcel": "^2.4.1"
  },
  "dependencies": {
    "dat.gui": "^0.7.9",
    "gsap": "^3.11.4",
    "three": "^0.139.2"
  }
}

炮弹函数封装

热气球函数我们就不多解释了,感兴趣的可以按照大傻上一篇的内容进行创建,我们先来对炮弹进行封装, 首先我们需要对调用进行分析,每次我们点击时候都会创建一个炮弹并且会爆炸开,那么我们可以封装一个类,目前我们已知条件中,我们需要传入颜色以及位置,也就是我们炮弹的爆炸位置以及发射时候的颜色。我们还需要一个addScene的函数用来给我们的文件引入当前环境的scene以及相机camera

let createShell = () => {
// 随机颜色
  let color = `hsl(${Math.floor(Math.random()*360)},100%,80%)`
  let position = {
    // x: 0,
    x: 5,
    z: 10,
    y: 40
  }
  // 颜色随机生成 位置
  let shell = new Shell(color, position)
  shell.addScene(scene, camera)
  scene.push(shell)
}

根据以上的信息我们先来模拟一下炮弹函数的结构

import * as THREE from 'three'
export default class Shell {
    constructor(color, to, from = {
        x:15,
        y:15,
        z: -15
    }) {
       // 这里我们对炮弹以及炮弹炸裂效果进行处理
    }
    // 添加到场景
    addScene(scene, camera) {
        // 这里我们对生成的炮弹以及效果 添加到场景

    }
    // 更新变量
    update() {
       // 在这对时间进行把控 也就是我们炮弹或者效果 执行完后进行回收操作
      
    }

}

image.png 接下来我们先对炮弹进行处理,这里我们模拟的炮弹为点结构

        this.color = new THREE.Color(color)
           // 初始化炮弹物体
        this.startGeometry = new THREE.BufferGeometry()
          // 点成线 需要的多个点
        const startPointCount = 50 + Math.floor(Math.random() * 50);
          // 初始位置
        const startPositionArray = new Float32Array(3*startPointCount)
          // 初始的大小
        const startPointScaleFireworkArray = new Float32Array(startPointCount)
         // 运动偏移位置
        const startPointDirectionArray = new Float32Array(startPointCount * 3)
        for (let i = 0; i < startPointCount; i++) {
            // 一开始炮弹的位置
            startPositionArray[i * 3 + 0] = to.x - from.x;
            startPositionArray[i * 3 + 1] = to.y - from.y;
            startPositionArray[i * 3 + 2] = to.z - from.z;
            // 设置炮弹初始大小
            startPointScaleFireworkArray[i + 0] = Math.random();
            // 设置炮弹大小 轨迹是一条线 所以我们没有角度参与
            let r = Math.random();

            startPointDirectionArray[i * 3 + 0] = r *(  from.x)*0.1
            startPointDirectionArray[i * 3 + 1] = r*( from.y)*0.1
            startPointDirectionArray[i * 3 + 2] = r *( from.z)*0.1
        }
        // 设置位置
         this.startGeometry.setAttribute('position', new THREE.BufferAttribute(startPositionArray, 3))
  //设置轨迹
        this.startGeometry.setAttribute('aRandom', new THREE.BufferAttribute(startPointDirectionArray, 3))
        // 设置着色器材质
        this.startMaterial = new THREE.ShaderMaterial({
            vertexShader,
            fragmentShader,
            transparent: true,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            uniforms: {
                uTime: {
                    value: 0
                },
                uSize: {
                    value: 2
                },
                uColor: {
                    value: this.color
                }
            }
        })
        // 创建炮弹点球
        this.startPoint = new THREE.Points(this.startGeometry, this.startMaterial);

紧接着是我们的炮弹着色器,首先是顶点着色器

attribute vec3 aRandom;
uniform float uTime;
uniform float uSize;
void main(){
    // 模型位置 
    vec4 modelPosition=modelMatrix*vec4(position,1.);
    // 将我们的模型位置加上我们的轨迹乘以时间 得到我们最终的方向位置
    modelPosition.xyz+=(aRandom*uTime)*5.;
    // 最终渲染位置就是我们的 视图矩阵以及模型位置的乘积
    gl_Position=projectionMatrix*viewMatrix*modelPosition;
    // 点的着色器必须有一个初始的大小
    gl_PointSize=uSize;
}

片元着色器

uniform vec3 uColor;
void main(){
   // 生成点状结构
    float strength=distance(gl_PointCoord,vec2(.5));
    strength*=2.;
    strength=1.-strength;
    // strength=step(.5,strength);
    // 渲染颜色
    gl_FragColor = vec4(uColor,strength);
}

image.png

炮弹爆炸效果函数

首先,我们先思考下,炮弹爆炸效果其实类似于我们创建炮弹的过程,只不过我们创建炮弹时候,是让炮弹做线性运动的,而爆炸就是向四周进行运动,由此我们来写下代码

  // 开始计时
        this.clock = new THREE.Clock()

        // 创建爆炸

        this.blastGeometry = new THREE.BufferGeometry()
        this.blastCount = 180 + Math.floor(Math.random() * 180);
        const positionBlastArray = new Float32Array(this.blastCount * 3)
        const scaleBlastArray = new Float32Array(this.blastCount)
        const directionArray = new Float32Array(this.blastCount * 3)
        for (let i = 0; i < this.fireworkCount; i++) {
            // 一开始的位置
            positionBlastArray[i * 3 + 0] = to.x;
            positionBlastArray[i * 3 + 1] = to.y;
            positionBlastArray[i * 3 + 2] = to.z;
            // 设置初始大小
            scaleBlastArray[i + 0] = Math.random();
            // 设置旋转角度 这里是为了爆炸的点可以四散开来
            let theta = Math.random() * 2 * Math.PI;
            let beta = Math.random() * 2 * Math.PI;
            let r = 1;

            directionArray[i * 3 + 0] = r * Math.sin(theta) + r * Math.sin(beta);
            directionArray[i * 3 + 1] = r * Math.cos(theta) + r * Math.cos(beta);
            directionArray[i * 3 + 2] = r * Math.sin(theta) + r * Math.cos(beta);
        }
        this.blastGeometry.setAttribute('position', new THREE.BufferAttribute(positionFireworksArray, 3))
        this.blastGeometry.setAttribute('aScale', new THREE.BufferAttribute(scaleFireworkArray, 1))
        this.blastGeometry.setAttribute('aRandom', new THREE.BufferAttribute(directionArray, 3))
        this.blastMaterial = new THREE.ShaderMaterial({
            vertexShader: blastVertexShader,
            fragmentShader: blastFragmentShader,
            transparent: true,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            uniforms: {
                uTime: {
                    value: 0
                },
                uSize: {
                    value: 0
                },
                uColor: {
                    value: this.color
                }
            }
        })
        this.blastPoint = new THREE.Points(this.blastGeometry, this.blastMaterial);

那么接下来就是我们爆炸效果的顶点着色器了


attribute float aScale;
attribute vec3 aRandom;
uniform float uTime;
uniform float uSize;
// 这块逻辑和我们的炮弹逻辑一样 唯一不同点在于大小的设置
void main(){
    vec4 modelPosition=modelMatrix*vec4(position,1.);
    modelPosition.xyz+=(aRandom*uTime)*5.;
    gl_Position=projectionMatrix*viewMatrix*modelPosition;
    // 因为我们爆炸后 点的大小 由大到小 所以在此和时间作一个运算
    gl_PointSize=uSize*aScale-(uTime*10.);
}

片元着色器

uniform vec3 uColor;

void main(){
    float strength=distance(gl_PointCoord,vec2(.5));
    strength*=2.;
    strength=1.-strength;
    strength=step(.5,strength);
    gl_FragColor=vec4(uColor,strength);
    
}

更新函数

截至目前我们已经创建了我们的炮弹以及我们的爆炸函数,是时候该让他们整合起来发挥效果了,首先就是先去添加到我们的场景中去

 addScene(scene, camera) {
        scene.add(this.startPoint)
        scene.add(this.blastPoint)
        this.scene = scene;
    }

image.png 接下来就是我们的更新函数,作用主要在于在合适的时机去触发我们的生成以及爆炸以及最后的消失

  const elapseTime = this.clock.getElapsedTime();
        if (elapseTime > 0.2 && elapseTime < 1) {// 当时间在0.2-1时候 让我们的炮弹以线性运动显示
            this.startMaterial.uniforms.uTime.value = elapseTime;
            this.startMaterial.uniforms.uSize.value =1
        } else if (elapseTime >= 1) {
        // 大于1后就让我们炮弹消失  爆炸效果显示
            const time = elapseTime - 1
            // 让元素消失
            this.startMaterial.uniforms.uSize.value = 0;
            this.startPoint.clear()
            this.startGeometry.dispose()
            this.scene.remove(this.startPoint)

            // 设置显示
            this.blastMaterial.uniforms.uSize.value = 20
            this.blastMaterial.uniforms.uTime.value = time

            if (time > 5) {
            // 超过5后 就给我们场景进行一次清空处理
                this.blastPoint.clear()
                this.blastGeometry.dispose()
                this.blastMaterial.dispose()
                this.scene.remove(this.firePoint)
                this.scene.remove(this.startPoint)
            }
        }

结尾讨论

至此为止,我们的流浪气球就已经完成了 看下最终结果吧

test.gif 有什么问题,欢迎大家指教,PS:这个射线轨迹一直没在炮口上,位置太难调了