【Three.js】粒子爱心

在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/9b12388fbf9a4845ab6327d76c28a5f5.png

前言

最近很火的电视剧《点燃我,温暖你》男主角学神和女主角课代表计算机考试实现的跳动的爱心,那我也来做一个粒子爱心送给女朋友。因为不想直接加载心形的模型文件作为基础,所以主要思路是三维直角坐标系内,通过心形函数绘制图形。然后呢,细化到空间中坐标粒子的发光闪烁特效。实现思路讲完了,讲一下性能优化,因为通过函数找心形表面粒子的过程是需要遍历的(没办法令函数直接 = 0,只能取<0的数据点,取最大、最小值,再去重),多层遍历嵌套会降低页面加载速度,所以这里的思想是构建对象,因为去掉了一层循环,肯定是快很多的,只牺牲一点点内存构建对象。由于对象的键值对查找几乎不用时间,不受下标影响,就可以将性能发挥到极致。

三维心形函数

// Get surface points
// Calc xRange: [-1.12, 1.12]; yRange: [-0.96, 1.2]; zRange: [-0.64, 0.64]
// f() = (x^2 + 9/4 * z^2 + y^2 + 1)^3 - x^2 * y^3 - 9/80 * z^2 * y^3
const func = Math.pow(Math.pow(x, 2) + 9 / 4 * Math.pow(z, 2) + Math.pow(y, 2) - 1, 3) - Math.pow(x, 2) * Math.pow(y, 3) - 9 / 80 * Math.pow(z, 2) * Math.pow(y, 3)
if (func == 0) {
	// you know
}

发光shader

<script type="x-shader/x-vertex" id="vertexshader">
 varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
  uniform sampler2D baseTexture;
  uniform sampler2D bloomTexture;
  varying vec2 vUv;
  void main() {
    gl_FragColor = (texture2D(baseTexture, vUv) + vec4(1.0) * texture2D(bloomTexture, vUv));
  }
</script>

使用对象键值对去重思路

function objArrDistinct(objArr) {
  let resultArr = [], itemKeyVal = {}
  objArr.forEach(item => {
    if (!itemKeyVal[`${item.x}_${item.y}_${item.z}`]) {
      itemKeyVal[`${item.x}_${item.y}_${item.z}`] = true
      resultArr.push(item)
    }
  })
  return resultArr
}

最后

对比一下,谁的爱心更好看呢?(●ˇ∀ˇ●)

最后的最后——完整源码

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Heart</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  <link rel="shortcut icon" href="../../svg/heart.svg">
  <link type="text/css" rel="stylesheet" href="../../main.css">
  <style type="text/css"></style>
</head>
<body>
  <script type="x-shader/x-vertex" id="vertexshader">
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  </script>
  <script type="x-shader/x-fragment" id="fragmentshader">
    uniform sampler2D baseTexture;
    uniform sampler2D bloomTexture;
    varying vec2 vUv;
    void main() {
      gl_FragColor = (texture2D(baseTexture, vUv) + vec4(1.0) * texture2D(bloomTexture, vUv));
    }
  </script>
  <script type="module">
    import * as THREE from '../../build/three.module.js'
    import Stats from '../jsm/libs/stats.module.js'
    import { OrbitControls } from '../jsm/controls/OrbitControls.js'
    import { EffectComposer } from '../jsm/postprocessing/EffectComposer.js'
    import { RenderPass } from '../jsm/postprocessing/RenderPass.js'
    import { ShaderPass } from '../jsm/postprocessing/ShaderPass.js'
    import { UnrealBloomPass } from '../jsm/postprocessing/UnrealBloomPass.js'
    import { FXAAShader } from '../jsm/shaders/FXAAShader.js'

    let renderer, scene, camera, controls, stats, pointLight
    let transform = new THREE.Object3D()
    let result = []
    let heartMesh
    let time = 0
    let bloomComposer, finalComposer
    const materials = {}
    const ENTIRE_SCENE = 0
    const BLOOM_SCENE = 1
    const bloomLayer = new THREE.Layers()
    bloomLayer.set(BLOOM_SCENE)
    const darkMaterial = new THREE.MeshBasicMaterial({ color: 'black' })

    initThree()
    initBloom()
    animate()

    function randomSort(a, b) {
      return Math.random() > 0.5 ? -1 : 1
    }
    function darkenNonBloomed(obj) {
      if (obj instanceof THREE.Scene) {
        materials.scene = obj.background
        obj.background = null
        return
      }
      if (obj instanceof THREE.Sprite || (obj.isMesh && bloomLayer.test(obj.layers) === false)) {
        materials[obj.uuid] = obj.material
        obj.material = darkMaterial
      }
    }
    function restoreMaterial(obj) {
      if (obj instanceof THREE.Scene) {
        obj.background = materials.scene
        delete materials.background
        return
      }
      if (materials[obj.uuid]) {
        obj.material = materials[obj.uuid]
        delete materials[obj.uuid]
      }
    }
    function initBloom() {
      const effectFXAA = new ShaderPass(FXAAShader)
      effectFXAA.uniforms['resolution'].value.set(0.6 / window.innerWidth, 0.6 / window.innerHeight)
      effectFXAA.renderToScreen = true
      const renderScene = new RenderPass(scene, camera)
      const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85)
      bloomPass.threshold = 0
      bloomPass.strength = 1.5
      bloomPass.radius = 0
      bloomComposer = new EffectComposer(renderer)
      bloomComposer.renderToScreen = false
      bloomComposer.addPass(renderScene)
      bloomComposer.addPass(bloomPass)
      bloomComposer.addPass(effectFXAA)
      const finalPass = new ShaderPass(
        new THREE.ShaderMaterial({
          uniforms: {
            baseTexture: { value: null },
            bloomTexture: { value: bloomComposer.renderTarget2.texture },
          },
          vertexShader: document.getElementById('vertexshader').textContent,
          fragmentShader: document.getElementById('fragmentshader').textContent,
          defines: {},
        }),
        'baseTexture'
      )
      finalPass.needsSwap = true
      finalComposer = new EffectComposer(renderer)
      finalComposer.addPass(renderScene)
      finalComposer.addPass(finalPass)
      finalComposer.addPass(effectFXAA)
    }
    function initThree() {
      stats = new Stats()
      // document.body.appendChild(stats.domElement)
      renderer = new THREE.WebGLRenderer({ antialias: false })
      renderer.setPixelRatio(window.devicePixelRatio)
      renderer.setSize(window.innerWidth, window.innerHeight)
      document.body.appendChild(renderer.domElement)
      scene = new THREE.Scene()
      scene.background = new THREE.Color(0x111111)
      camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000)
      camera.position.set(3, 3, 6)
      camera.lookAt(0, 0, 0)
      initOrbit()
      initLight()
      // initHelper()
      initHeart()
      toggleBloom()
      window.addEventListener('resize', onWindowResize)
      window.addEventListener('orientationchange', onWindowResize)
    }
    function toggleBloom() {
      scene.traverse((obj) => {
        if (obj.name === 'Heart') {
          obj.layers.toggle(1)
        }
      })
    }
    function initOrbit() {
      controls = new OrbitControls(camera, renderer.domElement)
      controls.enablePan = false
      controls.enableZoom = false
      controls.enableDamping = true
      controls.dampingFactor = 0.05
      controls.target.set(0, 0, 0)
      controls.autoRotate = true
    }
    function initLight() {
      pointLight = new THREE.PointLight('#ffffff')
      scene.add(pointLight)
    }
    function initHelper() {
      const mainAxesHelper = new THREE.AxesHelper(10000)
      scene.add(mainAxesHelper)
    }
    function initHeart() {
      const arr_xyz = [] // All points
      let arr_xy = [] // Points of x,y
      let arr_yz = [] // Points of y,z
      let arr_xz = [] // Points of x,z
      const unitSize = 0.01 // unit size
      const unitSpacing = 0.04 // unit spacing
      // Get surface points
      // Calc xRange: [-1.12, 1.12]; yRange: [-0.96, 1.2]; zRange: [-0.64, 0.64]
      // let xmax = 0
      // let xmin = 0
      // let ymax = 0
      // let ymin = 0
      // let zmax = 0
      // let zmin = 0
      // arr_xyz.map(v => {
      //   xmax = Math.max(xmax, v.x)
      //   xmin = Math.min(xmin, v.x)
      //   ymax = Math.max(ymax, v.y)
      //   ymin = Math.min(ymin, v.y)
      //   zmax = Math.max(zmax, v.z)
      //   zmin = Math.min(zmin, v.z)
      // })
      // console.log(xmax, xmin, ymax, ymin, zmax, zmin)
      let kvx = {}
      let kvy = {}
      let kvz = {}
      for (let x = -1.12; x <= 1.12; x += unitSpacing) {
        for (let y = -0.96; y <= 1.2; y += unitSpacing) {
          for (let z = -0.64; z <= 0.64; z += unitSpacing) {
            x = Number(x.toFixed(2))
            y = Number(y.toFixed(2))
            z = Number(z.toFixed(2))
            // f() = (x^2 + 9/4 * z^2 + y^2 + 1)^3 - x^2 * y^3 - 9/80 * z^2 * y^3
            const func = Math.pow(Math.pow(x, 2) + 9 / 4 * Math.pow(z, 2) + Math.pow(y, 2) - 1, 3) - Math.pow(x, 2) * Math.pow(y, 3) - 9 / 80 * Math.pow(z, 2) * Math.pow(y, 3)
            if (func < 0) {
              arr_xyz.push({ x, y, z })
              kvx[`${y}_${z}`] = kvx[`${y}_${z}`] ? kvx[`${y}_${z}`].concat([x]) : [x]
              kvy[`${x}_${z}`] = kvy[`${x}_${z}`] ? kvy[`${x}_${z}`].concat([y]) : [y]
              kvz[`${x}_${y}`] = kvz[`${x}_${y}`] ? kvz[`${x}_${y}`].concat([z]) : [z]
              arr_xy.push({ x, y, max_z: 0, min_z: 0 })
              arr_yz.push({ z, y, max_x: 0, min_x: 0 })
              arr_xz.push({ x, z, max_y: 0, min_y: 0 })
            }
          }
        }
      }
      arr_xy = objArrDistinct(arr_xy)
      arr_yz = objArrDistinct(arr_yz)
      arr_xz = objArrDistinct(arr_xz)
      // Filter out the maximum and minimum axial value
      arr_xy.forEach(xy => {
        xy.min_z = Math.min(...kvz[`${xy.x}_${xy.y}`])
        xy.max_z = Math.max(...kvz[`${xy.x}_${xy.y}`])
      })
      arr_yz.forEach(yz => {
        yz.min_x = Math.min(...kvx[`${yz.y}_${yz.z}`])
        yz.max_x = Math.max(...kvx[`${yz.y}_${yz.z}`])
      })
      arr_xz.forEach(xz => {
        xz.min_y = Math.min(...kvy[`${xz.x}_${xz.z}`])
        xz.max_y = Math.max(...kvy[`${xz.x}_${xz.z}`])
      })
      // Filter out the surface points => result
      arr_xy.map(xy => {
        result.push({ x: xy.x, y: xy.y, z: xy.max_z })
        result.push({ x: xy.x, y: xy.y, z: xy.min_z })
      })
      arr_yz.map(yz => {
        result.push({ z: yz.z, y: yz.y, x: yz.max_x })
        result.push({ z: yz.z, y: yz.y, x: yz.min_x })
      })
      arr_xz.map(xz => {
        result.push({ x: xz.x, z: xz.z, z: xz.max_y })
        result.push({ x: xz.x, z: xz.z, z: xz.min_y })
      })
      result = objArrDistinct(result)
      // Set InstancedMesh
      const geometry = new THREE.BoxBufferGeometry(unitSize, unitSize, unitSize)
      const material = new THREE.MeshBasicMaterial()
      heartMesh = new THREE.InstancedMesh(geometry, material, result.length)
      heartMesh.name = 'Heart'
      result = result.sort(randomSort)
      result.map((res, i) => {
        transform.position.set(res.x, res.y, res.z)
        transform.updateMatrix()
        heartMesh.setMatrixAt(i, transform.matrix)
        heartMesh.setColorAt(i, new THREE.Color(`rgb(${Math.floor(255 * Math.random())}, 0, 0)`))
      })
      scene.add(heartMesh)
    }
    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight
      camera.updateProjectionMatrix()
      renderer.setSize(window.innerWidth, window.innerHeight)
    }
    /**
     * Distinct 3d point (x, y, z)
     */
    function objArrDistinct(objArr) {
      let resultArr = [], itemKeyVal = {}
      objArr.forEach(item => {
        if (!itemKeyVal[`${item.x}_${item.y}_${item.z}`]) {
          itemKeyVal[`${item.x}_${item.y}_${item.z}`] = true
          resultArr.push(item)
        }
      })
      return resultArr
    }
    function blingbling() {
      const t = Date.now() * 0.005
      result.map((res, i) => {
        const scale = 1.2 * (0.5 * Math.sin(t + i * 0.1) + 0.5) // range of [0, 1.2]
        transform.position.set(res.x, res.y, res.z)
        transform.scale.set(scale, scale, scale)
        transform.updateMatrix()
        heartMesh.setMatrixAt(i, transform.matrix)
      })
      heartMesh.instanceMatrix.needsUpdate = true
    }
    function animate() {
      blingbling()
      stats.update()
      controls.update()
      scene.traverse(darkenNonBloomed)
      bloomComposer.render()
      scene.traverse(restoreMaterial)
      finalComposer.render()
      const vector = camera.position.clone()
      pointLight.position.set(vector.x, vector.y, vector.z)
      requestAnimationFrame(animate)
    }
  </script>
</body>
</html>

相关项目

🚩——坦克大战
📦—— 立体库房
🎄—— 圣诞树
✅—— 程序员升职记
🏀—— 投个篮吧
💖——粒子爱心

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饺子大魔王12138

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值