flowingSmokeComponent.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <script setup lang="ts">
  2. import { useLoop } from '@tresjs/core'
  3. import * as THREE from 'three'
  4. import CustomShaderMaterial from 'three-custom-shader-material/vanilla'
  5. import {
  6. computed,
  7. onBeforeUnmount,
  8. shallowRef,
  9. watch,
  10. } from 'vue'
  11. import smokeFragmentShader from '../shader/flowingSmokeComponent/smoke.frag?raw'
  12. import smokeVertexShader from '../shader/flowingSmokeComponent/smoke.vert?raw'
  13. type SmokeCustomMaterial = CustomShaderMaterial<typeof THREE.PointsMaterial>
  14. const props = withDefaults(
  15. defineProps<{
  16. color?: string
  17. density?: number
  18. directionX?: number
  19. directionY?: number
  20. directionZ?: number
  21. emitterRadius?: number
  22. flowRate?: number
  23. lifeRange?: number
  24. opacity?: number
  25. particleSize?: number
  26. sizeGrowth?: number
  27. spread?: number
  28. turbulence?: number
  29. }>(),
  30. {
  31. color: '#aeb4b8',
  32. density: 0.38,
  33. directionX: -0.08,
  34. directionY: 1,
  35. directionZ: 0.03,
  36. emitterRadius: 0.12,
  37. flowRate: 0.16,
  38. lifeRange: 3.2,
  39. opacity: 0.26,
  40. particleSize: 0.22,
  41. sizeGrowth: 2.2,
  42. spread: 0.22,
  43. turbulence: 0.14,
  44. },
  45. )
  46. const MAX_PARTICLES = 360
  47. const smokePoints = shallowRef<THREE.Points | null>(null)
  48. const smokeGeometry = shallowRef<THREE.BufferGeometry | null>(null)
  49. const smokeMaterial = shallowRef<SmokeCustomMaterial | null>(null)
  50. const clampNumber = (value: number | undefined, min: number, max: number, fallback: number) => {
  51. if (!Number.isFinite(value)) {
  52. return fallback
  53. }
  54. return THREE.MathUtils.clamp(value as number, min, max)
  55. }
  56. const safeFlowRate = computed(() => clampNumber(props.flowRate, 0.02, 6, 0.16))
  57. const safeDensity = computed(() => clampNumber(props.density, 0.05, 1, 0.38))
  58. const safeEmitterRadius = computed(() => clampNumber(props.emitterRadius, 0.01, 2, 0.12))
  59. const safeSpread = computed(() => clampNumber(props.spread, 0, 4, 0.22))
  60. const safeTurbulence = computed(() => clampNumber(props.turbulence, 0, 2, 0.14))
  61. const safeOpacity = computed(() => clampNumber(props.opacity, 0.01, 1, 0.26))
  62. const safeParticleSize = computed(() => {
  63. const value = clampNumber(props.particleSize, 0.02, 300, 0.22)
  64. // Existing editor scenes stored the old 8-90 range. Keep them usable after upgrading.
  65. return THREE.MathUtils.clamp(value > 3 ? value / 100 : value, 0.02, 3)
  66. })
  67. const safeSizeGrowth = computed(() => clampNumber(props.sizeGrowth, 1, 4, 2.2))
  68. const safeLifeRange = computed(() => clampNumber(props.lifeRange, 0.5, 14, 3.2))
  69. const safeDirection = computed(() => {
  70. const direction = new THREE.Vector3(
  71. clampNumber(props.directionX, -1, 1, 0),
  72. clampNumber(props.directionY, -1, 1, 1),
  73. clampNumber(props.directionZ, -1, 1, 0),
  74. )
  75. if (direction.lengthSq() < 0.0001) {
  76. direction.set(0, 1, 0)
  77. }
  78. return direction.normalize()
  79. })
  80. const visibleParticleCount = computed(() => Math.max(8, Math.round(MAX_PARTICLES * safeDensity.value)))
  81. const smokeUniforms = {
  82. uColor: { value: new THREE.Color(props.color) },
  83. uDensity: { value: safeDensity.value },
  84. uDirection: { value: safeDirection.value.clone() },
  85. uEmitterRadius: { value: safeEmitterRadius.value },
  86. uFlowRate: { value: safeFlowRate.value },
  87. uLifeRange: { value: safeLifeRange.value },
  88. uOpacity: { value: safeOpacity.value },
  89. uParticleSize: { value: safeParticleSize.value },
  90. uSizeGrowth: { value: safeSizeGrowth.value },
  91. uSpread: { value: safeSpread.value },
  92. uTime: { value: 0 },
  93. uTurbulence: { value: safeTurbulence.value },
  94. }
  95. const createSmokeMaterial = (): SmokeCustomMaterial => {
  96. return new CustomShaderMaterial({
  97. baseMaterial: THREE.PointsMaterial,
  98. blending: THREE.NormalBlending,
  99. color: 0xFFFFFF,
  100. depthTest: true,
  101. depthWrite: false,
  102. fragmentShader: smokeFragmentShader,
  103. silent: true,
  104. size: safeParticleSize.value,
  105. sizeAttenuation: true,
  106. toneMapped: false,
  107. transparent: true,
  108. uniforms: smokeUniforms,
  109. vertexShader: smokeVertexShader,
  110. })
  111. }
  112. const disposeSmokeCloud = () => {
  113. smokeGeometry.value?.dispose()
  114. smokeMaterial.value?.dispose()
  115. smokeGeometry.value = null
  116. smokeMaterial.value = null
  117. smokePoints.value = null
  118. }
  119. const createSmokeCloud = () => {
  120. disposeSmokeCloud()
  121. const positions = new Float32Array(MAX_PARTICLES * 3)
  122. const jitters = new Float32Array(MAX_PARTICLES * 3)
  123. const seeds = new Float32Array(MAX_PARTICLES)
  124. const lifeOffsets = new Float32Array(MAX_PARTICLES)
  125. const sizeFactors = new Float32Array(MAX_PARTICLES)
  126. for (let index = 0; index < MAX_PARTICLES; index += 1) {
  127. const cursor = index * 3
  128. const angle = Math.random() * Math.PI * 2
  129. const radius = Math.sqrt(Math.random())
  130. positions[cursor] = Math.cos(angle) * radius
  131. positions[cursor + 1] = (Math.random() - 0.5) * 0.3
  132. positions[cursor + 2] = Math.sin(angle) * radius
  133. jitters[cursor] = Math.random() * 2 - 1
  134. jitters[cursor + 1] = Math.random() * 2 - 1
  135. jitters[cursor + 2] = Math.random() * 2 - 1
  136. seeds[index] = Math.random()
  137. lifeOffsets[index] = Math.random()
  138. sizeFactors[index] = 0.75 + Math.random() * 0.95
  139. }
  140. const geometry = new THREE.BufferGeometry()
  141. geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
  142. geometry.setAttribute('aJitter', new THREE.BufferAttribute(jitters, 3))
  143. geometry.setAttribute('aSeed', new THREE.BufferAttribute(seeds, 1))
  144. geometry.setAttribute('aLifeOffset', new THREE.BufferAttribute(lifeOffsets, 1))
  145. geometry.setAttribute('aSizeFactor', new THREE.BufferAttribute(sizeFactors, 1))
  146. geometry.setDrawRange(0, visibleParticleCount.value)
  147. const material = createSmokeMaterial()
  148. const points = new THREE.Points(geometry, material)
  149. points.frustumCulled = false
  150. points.renderOrder = 8
  151. smokeGeometry.value = geometry
  152. smokeMaterial.value = material
  153. smokePoints.value = points
  154. }
  155. createSmokeCloud()
  156. watch(
  157. [
  158. () => props.color,
  159. safeDensity,
  160. safeDirection,
  161. safeEmitterRadius,
  162. safeFlowRate,
  163. safeLifeRange,
  164. safeOpacity,
  165. safeParticleSize,
  166. safeSizeGrowth,
  167. safeSpread,
  168. safeTurbulence,
  169. ],
  170. ([
  171. color,
  172. density,
  173. direction,
  174. emitterRadius,
  175. flowRate,
  176. lifeRange,
  177. opacity,
  178. particleSize,
  179. sizeGrowth,
  180. spread,
  181. turbulence,
  182. ]) => {
  183. if (!smokeMaterial.value || !smokeGeometry.value) {
  184. return
  185. }
  186. smokeMaterial.value.uniforms.uColor.value.set(color)
  187. smokeMaterial.value.uniforms.uDensity.value = density
  188. smokeMaterial.value.uniforms.uDirection.value.copy(direction)
  189. smokeMaterial.value.uniforms.uEmitterRadius.value = emitterRadius
  190. smokeMaterial.value.uniforms.uFlowRate.value = flowRate
  191. smokeMaterial.value.uniforms.uLifeRange.value = lifeRange
  192. smokeMaterial.value.uniforms.uOpacity.value = opacity
  193. smokeMaterial.value.uniforms.uParticleSize.value = particleSize
  194. smokeMaterial.value.uniforms.uSizeGrowth.value = sizeGrowth
  195. smokeMaterial.value.uniforms.uSpread.value = spread
  196. smokeMaterial.value.uniforms.uTurbulence.value = turbulence
  197. smokeGeometry.value.setDrawRange(0, visibleParticleCount.value)
  198. },
  199. { immediate: true },
  200. )
  201. const { onBeforeRender } = useLoop()
  202. onBeforeRender(({ elapsed }) => {
  203. if (!smokeMaterial.value) {
  204. return
  205. }
  206. smokeMaterial.value.uniforms.uTime.value = elapsed
  207. })
  208. onBeforeUnmount(() => {
  209. disposeSmokeCloud()
  210. })
  211. </script>
  212. <template>
  213. <TresGroup>
  214. <primitive
  215. v-if="smokePoints"
  216. :object="smokePoints"
  217. />
  218. </TresGroup>
  219. </template>