2023-03-14 13:23

[ThreeJS] 噴水粒子

要完成這個效果有三個問題要解決

  1. 自然的粒子分布
  2. 粒子的拋物線移動
  3. 粒子的擴散效果

我採用簡單的方式達成

  1. 用亂數產生分段的粒子團
  2. 用二次貝茲曲線建立拋物線
  3. 用粒子團的放大產生擴散效果

因為我用簡單的亂數分布粒子,粒子團會有立方體現象,球體分布會更好,但多個區段連起來就不明顯了。

二次貝茲曲線最大的困難在計算出結尾座標的位置,透過轉換到世界空間去定位出平面高度的座標,再轉換回物體空間來計算出結尾座標。

動畫用 AnimationMixer 去控制,只要定出每段粒子團的開始播放時間,粒子團就會接續的移動。

circle.png

程式:

  1. /* 初始化渲染器 */ 
  2. const renderer = new THREE.WebGLRenderer({ antialias: true }); 
  3. document.getElementById('Container').appendChild(renderer.domElement); 
  4. renderer.setPixelRatio(window.devicePixelRatio); 
  5. renderer.setSize(window.innerWidth, window.innerHeight); 
  6.  
  7. /* 初始化場景 */ 
  8. const scene = new THREE.Scene(); 
  9. scene.background = new THREE.Color('#111'); /* 背景顏色 */ 
  10. scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */ 
  11.  
  12. /* 地面 */ 
  13. const floorMesh = new THREE.Mesh( 
  14.    new THREE.PlaneGeometry(200, 200), 
  15.    new THREE.MeshBasicMaterial({ color: '#DDD', depthWrite: false }) 
  16. ); 
  17. floorMesh.rotation.x = THREE.MathUtils.degToRad(-90); 
  18. scene.add(floorMesh); 
  19.  
  20.  
  21. /* 初始化鏡頭 */ 
  22. const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300); 
  23. camera.position.set(0, 10, 22); 
  24.  
  25. /* 初始化軌道控制,鏡頭的移動 */ 
  26. const orbitControls = new THREE.OrbitControls(camera, renderer.domElement); 
  27. orbitControls.update(); 
  28.  
  29.  
  30. /* 動畫刷新器 */ 
  31. const mixers = []; 
  32.  
  33. function newMixer(target) { 
  34.    const mixer = new THREE.AnimationMixer(target); 
  35.    mixers.push(mixer); 
  36.    return mixer; 
  37. } 
  38.  
  39. /* 動畫計時器 */ 
  40. const clock = new THREE.Clock(); 
  41.  
  42. /* 渲染週期 */ 
  43. function renderCycle() { 
  44.    const delta = clock.getDelta(); 
  45.    mixers.forEach(x => x.update(delta)); 
  46.  
  47.    renderer.render(scene, camera); 
  48.    requestAnimationFrame(renderCycle); 
  49. } 
  50. renderCycle(); 
  51.  
  52.  
  53.  
  54. /*---------------------------------------------------------------*/ 
  55.  
  56. /* 水管物件,水滴粒子群組會附加在這裡 */ 
  57. const pipeMesh = new THREE.Mesh( 
  58.    new THREE.CylinderGeometry(0.5, 0.5, 9, 16), 
  59.    new THREE.MeshBasicMaterial( {color: '#eea236'} ) 
  60. ); 
  61. pipeMesh.rotation.x = THREE.MathUtils.degToRad(0); 
  62. pipeMesh.position.set(0, 4, 0); 
  63. scene.add(pipeMesh); 
  64.  
  65.  
  66. const dripGroup = new THREE.Group(); 
  67. dripGroup.position.set(0, 4, 0); 
  68. pipeMesh.add(dripGroup); 
  69.  
  70.  
  71. /* 水滴材質 */ 
  72. const dripMaterial = new THREE.PointsMaterial({ 
  73.    map: new THREE.TextureLoader().load('circle.png'), 
  74.    color: '#0aa', 
  75.    size: 1, 
  76.    opacity: 0.7, 
  77.    depthWrite: false, 
  78.    transparent: true, 
  79. }); 
  80.  
  81.  
  82. /* 建立水滴,用亂數建立粒子點 */ 
  83. for (let i = 0; i < 60; i++) { 
  84.    const vertices = []; 
  85.  
  86.    for (let j = 0; j < 40; j++) { 
  87.        const x = Math.random() - 0.5; 
  88.        const y = Math.random() - 0.5; 
  89.        const z = Math.random() - 0.5; 
  90.        vertices.push(x, y, z); 
  91.    } 
  92.  
  93.    const geometry = new THREE.BufferGeometry(); 
  94.    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); 
  95.  
  96.    const particles = new THREE.Points(geometry, dripMaterial); 
  97.    dripGroup.add(particles); 
  98.  
  99.    newMixer(particles); /* 水滴動畫 */ 
  100. } 
  101.  
  102.  
  103. /*---------------------------------------------------------------*/ 
  104.  
  105. const cycle = 2; /* 循環週期,會影響水流速度 */ 
  106. const scale = 8; /* 擴散大小 */ 
  107. const length = 14; /* 水流長度 */ 
  108.  
  109.  
  110. /* 二次貝茲曲線,用來取得拋物線的座標點 */ 
  111. const curve = new THREE.QuadraticBezierCurve3(); 
  112. curve.v1.set(0, length, 0); /* 曲線控制點座標 */ 
  113.  
  114.  
  115. /* 計算結尾座標 */ 
  116. const rootPos = dripGroup.getWorldPosition(new THREE.Vector3()); 
  117. const quaternion = dripGroup.getWorldQuaternion(new THREE.Quaternion()); 
  118. const toPos = curve.v1.clone(); 
  119. toPos.applyQuaternion(quaternion); /* 轉換到世界空間 */ 
  120.  
  121. /* 當水流是向上時,增加平面位置 */ 
  122. if (toPos.y > (length / 3)) { 
  123.    toPos.x *= 1.8; 
  124.    toPos.z *= 1.8; 
  125. } 
  126. toPos.y = Math.min(-rootPos.y, toPos.y * 1.5); /* 將結尾拉回平面高度 */ 
  127. toPos.applyQuaternion(quaternion.conjugate()); /* 轉換回物體空間 */ 
  128. curve.v2.copy(toPos); /* 曲線結尾點座標 */ 
  129.  
  130.  
  131. /* 建立拋物線及擴散動畫 */ 
  132. const points = curve.getPoints(10); /* 曲线分段 */ 
  133.  
  134. const curveTime = []; 
  135. const curvePos = []; 
  136. points.forEach((v, i) => { 
  137.    curveTime.push(cycle * i / points.length); 
  138.    curvePos.push(v.x, v.y, v.z); 
  139. }); 
  140. const posTrack = new THREE.VectorKeyframeTrack('.position', curveTime, curvePos); 
  141. const scaleTrack = new THREE.VectorKeyframeTrack('.scale', [0, cycle], [0, 0, 0, scale, scale, scale]); 
  142. const clip = new THREE.AnimationClip('scale', cycle, [posTrack, scaleTrack]); 
  143.  
  144. mixers.forEach((mixer, i) => { 
  145.    const action = mixer.clipAction(clip); 
  146.    action.time = cycle * i / mixers.length; 
  147.    action.play(); 
  148. }); 

1 回應:

Nishant Saurabh 提到...

Here all content so useful and helpful for beginner and experience both.This site is so amazing,This sites gives good knowledge of Threejs.This is very helpful for me.