chargeOrbGatherComponent.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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 baseVertexShader from '../shader/chargeOrbGatherComponent/base.vert?raw'
  12. import fresnelFragmentShader from '../shader/chargeOrbGatherComponent/fresnel.frag?raw'
  13. import glowFragmentShader from '../shader/chargeOrbGatherComponent/glow.frag?raw'
  14. import noiseFragmentShader from '../shader/chargeOrbGatherComponent/noise.frag?raw'
  15. type OrbCustomMaterial = CustomShaderMaterial<typeof THREE.MeshBasicMaterial>
  16. interface Particle {
  17. active: boolean
  18. life: number
  19. maxLife: number
  20. phase: number
  21. pos: THREE.Vector3
  22. }
  23. const props = withDefaults(
  24. defineProps<{
  25. autoAnimate?: boolean
  26. bodyFalloff?: number
  27. chargeProgress?: number
  28. coreBoost?: number
  29. coreFalloff?: number
  30. cycleDuration?: number
  31. flowIntensity?: number
  32. fresnelEmission?: number
  33. fresnelPower?: number
  34. glowColorInner?: string
  35. glowColorOuter?: string
  36. glowEdgeSoftness?: number
  37. glowEmission?: number
  38. glowEnabled?: boolean
  39. glowFalloffPower?: number
  40. glowSizeMult?: number
  41. noiseColor?: string
  42. noiseScale?: number
  43. noiseSpeed?: number
  44. orbCoreColor?: string
  45. orbMaxSize?: number
  46. orbRimColor?: string
  47. particleColor?: string
  48. particleCount?: number
  49. particleEmission?: number
  50. particleLength?: number
  51. particleLifetime?: number
  52. particleSpawnRadius?: number
  53. particleSpeed?: number
  54. particleTurbulence?: number
  55. particleWidth?: number
  56. warpStrength?: number
  57. }>(),
  58. {
  59. autoAnimate: true,
  60. bodyFalloff: 5.6,
  61. chargeProgress: 0,
  62. coreBoost: 2.3,
  63. coreFalloff: 4.4,
  64. cycleDuration: 2.4,
  65. flowIntensity: 4.7,
  66. fresnelEmission: 1.8,
  67. fresnelPower: 0.1,
  68. glowColorInner: '#cb5151',
  69. glowColorOuter: '#000000',
  70. glowEdgeSoftness: 4,
  71. glowEmission: 1.6,
  72. glowEnabled: false,
  73. glowFalloffPower: 6,
  74. glowSizeMult: 1.7,
  75. noiseColor: '#128bff',
  76. noiseScale: 0.4,
  77. noiseSpeed: 1.35,
  78. orbCoreColor: '#eaf8fc',
  79. orbMaxSize: 0.4,
  80. orbRimColor: '#0c61f5',
  81. particleColor: '#59afff',
  82. particleCount: 16,
  83. particleEmission: 3,
  84. particleLength: 0.13,
  85. particleLifetime: 0.6,
  86. particleSpawnRadius: 2.1,
  87. particleSpeed: 3.9,
  88. particleTurbulence: 1.1,
  89. particleWidth: 0.1,
  90. warpStrength: 2.3,
  91. },
  92. )
  93. const MAX_PARTICLES = 40
  94. const clampNumber = (value: number | undefined, min: number, max: number, fallback: number) => {
  95. if (!Number.isFinite(value)) {
  96. return fallback
  97. }
  98. return THREE.MathUtils.clamp(value as number, min, max)
  99. }
  100. const setMaterialColor = (uniform: THREE.IUniform<THREE.Color>, value: string | undefined, fallback: string) => {
  101. try {
  102. uniform.value.set(value ?? fallback)
  103. }
  104. catch {
  105. uniform.value.set(fallback)
  106. }
  107. }
  108. const safeBodyFalloff = computed(() => clampNumber(props.bodyFalloff, 0.1, 8, 5.6))
  109. const safeChargeProgress = computed(() => clampNumber(props.chargeProgress, 0, 1, 0))
  110. const safeCoreBoost = computed(() => clampNumber(props.coreBoost, 0, 10, 2.3))
  111. const safeCoreFalloff = computed(() => clampNumber(props.coreFalloff, 0.5, 10, 4.4))
  112. const safeCycleDuration = computed(() => clampNumber(props.cycleDuration, 0.6, 8, 2.4))
  113. const safeFlowIntensity = computed(() => clampNumber(props.flowIntensity, 0, 15, 4.7))
  114. const safeFresnelEmission = computed(() => clampNumber(props.fresnelEmission, 0.1, 30, 1.8))
  115. const safeFresnelPower = computed(() => clampNumber(props.fresnelPower, 0.1, 4, 0.1))
  116. const safeGlowEdgeSoftness = computed(() => clampNumber(props.glowEdgeSoftness, 0.1, 8, 4))
  117. const safeGlowEmission = computed(() => clampNumber(props.glowEmission, 0, 8, 1.6))
  118. const safeGlowFalloffPower = computed(() => clampNumber(props.glowFalloffPower, 0.1, 8, 6))
  119. const safeGlowSizeMult = computed(() => clampNumber(props.glowSizeMult, 1, 10, 1.7))
  120. const safeNoiseScale = computed(() => clampNumber(props.noiseScale, 0.1, 20, 0.4))
  121. const safeNoiseSpeed = computed(() => clampNumber(props.noiseSpeed, 0, 4, 1.35))
  122. const safeOrbMaxSize = computed(() => clampNumber(props.orbMaxSize, 0.01, 3, 0.4))
  123. const safeParticleCount = computed(() => Math.round(clampNumber(props.particleCount, 1, MAX_PARTICLES, 16)))
  124. const safeParticleEmission = computed(() => clampNumber(props.particleEmission, 0.1, 20, 3))
  125. const safeParticleLength = computed(() => clampNumber(props.particleLength, 0.005, 0.5, 0.13))
  126. const safeParticleLifetime = computed(() => clampNumber(props.particleLifetime, 0.1, 3, 0.6))
  127. const safeParticleSpawnRadius = computed(() => clampNumber(props.particleSpawnRadius, 0.1, 5, 2.1))
  128. const safeParticleSpeed = computed(() => clampNumber(props.particleSpeed, 0.1, 10, 3.9))
  129. const safeParticleTurbulence = computed(() => clampNumber(props.particleTurbulence, 0, 5, 1.1))
  130. const safeParticleWidth = computed(() => clampNumber(props.particleWidth, 0.005, 0.3, 0.1))
  131. const safeWarpStrength = computed(() => clampNumber(props.warpStrength, 0, 8, 2.3))
  132. const rootGroup = shallowRef<THREE.Group | null>(null)
  133. const glowMesh = shallowRef<THREE.Mesh | null>(null)
  134. const fresnelMesh = shallowRef<THREE.Mesh | null>(null)
  135. const noiseMesh = shallowRef<THREE.Mesh | null>(null)
  136. const particleMesh = shallowRef<THREE.InstancedMesh | null>(null)
  137. const sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
  138. const capsuleGeometry = new THREE.CapsuleGeometry(0.5, 1, 4, 8)
  139. const createGlowMaterial = (): OrbCustomMaterial => {
  140. return new CustomShaderMaterial({
  141. baseMaterial: THREE.MeshBasicMaterial,
  142. blending: THREE.AdditiveBlending,
  143. color: 0xFFFFFF,
  144. depthTest: true,
  145. depthWrite: false,
  146. fragmentShader: glowFragmentShader,
  147. silent: true,
  148. toneMapped: false,
  149. transparent: true,
  150. uniforms: {
  151. uColorInner: { value: new THREE.Color(props.glowColorInner) },
  152. uColorOuter: { value: new THREE.Color(props.glowColorOuter) },
  153. uEdgeSoftness: { value: safeGlowEdgeSoftness.value },
  154. uEmission: { value: safeGlowEmission.value },
  155. uFalloffPower: { value: safeGlowFalloffPower.value },
  156. uProgress: { value: 0 },
  157. },
  158. vertexShader: baseVertexShader,
  159. })
  160. }
  161. const createFresnelMaterial = (): OrbCustomMaterial => {
  162. return new CustomShaderMaterial({
  163. baseMaterial: THREE.MeshBasicMaterial,
  164. blending: THREE.AdditiveBlending,
  165. color: 0xFFFFFF,
  166. depthTest: true,
  167. depthWrite: false,
  168. fragmentShader: fresnelFragmentShader,
  169. silent: true,
  170. toneMapped: false,
  171. transparent: true,
  172. uniforms: {
  173. uBodyFalloff: { value: safeBodyFalloff.value },
  174. uCoreBoost: { value: safeCoreBoost.value },
  175. uCoreColor: { value: new THREE.Color(props.orbCoreColor) },
  176. uCoreFalloff: { value: safeCoreFalloff.value },
  177. uEmission: { value: safeFresnelEmission.value },
  178. uFresnelPower: { value: safeFresnelPower.value },
  179. uProgress: { value: 0 },
  180. uRimColor: { value: new THREE.Color(props.orbRimColor) },
  181. },
  182. vertexShader: baseVertexShader,
  183. })
  184. }
  185. const createNoiseMaterial = (): OrbCustomMaterial => {
  186. return new CustomShaderMaterial({
  187. baseMaterial: THREE.MeshBasicMaterial,
  188. blending: THREE.AdditiveBlending,
  189. color: 0xFFFFFF,
  190. depthTest: true,
  191. depthWrite: false,
  192. fragmentShader: noiseFragmentShader,
  193. silent: true,
  194. toneMapped: false,
  195. transparent: true,
  196. uniforms: {
  197. uColor: { value: new THREE.Color(props.noiseColor) },
  198. uFlowIntensity: { value: safeFlowIntensity.value },
  199. uNoiseScale: { value: safeNoiseScale.value },
  200. uNoiseSpeed: { value: safeNoiseSpeed.value },
  201. uProgress: { value: 0 },
  202. uTime: { value: 0 },
  203. uWarpStrength: { value: safeWarpStrength.value },
  204. },
  205. vertexShader: baseVertexShader,
  206. })
  207. }
  208. const glowMaterial = createGlowMaterial()
  209. const fresnelMaterial = createFresnelMaterial()
  210. const noiseMaterial = createNoiseMaterial()
  211. const particlesMaterial = new THREE.MeshBasicMaterial({
  212. blending: THREE.AdditiveBlending,
  213. color: new THREE.Color(props.particleColor).multiplyScalar(safeParticleEmission.value),
  214. depthWrite: false,
  215. toneMapped: false,
  216. transparent: true,
  217. })
  218. const particles = Array.from({ length: MAX_PARTICLES }, (): Particle => ({
  219. active: false,
  220. life: 0,
  221. maxLife: 0,
  222. phase: 0,
  223. pos: new THREE.Vector3(),
  224. }))
  225. const dummy = new THREE.Object3D()
  226. const toCenter = new THREE.Vector3()
  227. const spawnDirection = new THREE.Vector3()
  228. const particleDirection = new THREE.Vector3()
  229. const particleQuaternion = new THREE.Quaternion()
  230. const upVector = new THREE.Vector3(0, 1, 0)
  231. let previousProgress = 0
  232. const createRootGroup = () => {
  233. const nextGlowMesh = new THREE.Mesh(sphereGeometry, glowMaterial)
  234. nextGlowMesh.renderOrder = 10
  235. nextGlowMesh.visible = false
  236. nextGlowMesh.frustumCulled = false
  237. const nextFresnelMesh = new THREE.Mesh(sphereGeometry, fresnelMaterial)
  238. nextFresnelMesh.renderOrder = 11
  239. nextFresnelMesh.visible = false
  240. nextFresnelMesh.frustumCulled = false
  241. const nextNoiseMesh = new THREE.Mesh(sphereGeometry, noiseMaterial)
  242. nextNoiseMesh.renderOrder = 12
  243. nextNoiseMesh.visible = false
  244. nextNoiseMesh.frustumCulled = false
  245. const nextParticleMesh = new THREE.InstancedMesh(capsuleGeometry, particlesMaterial, MAX_PARTICLES)
  246. nextParticleMesh.renderOrder = 9
  247. nextParticleMesh.frustumCulled = false
  248. nextParticleMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
  249. dummy.scale.setScalar(0)
  250. dummy.updateMatrix()
  251. for (let index = 0; index < MAX_PARTICLES; index += 1) {
  252. nextParticleMesh.setMatrixAt(index, dummy.matrix)
  253. }
  254. nextParticleMesh.instanceMatrix.needsUpdate = true
  255. const group = new THREE.Group()
  256. group.add(nextParticleMesh)
  257. group.add(nextGlowMesh)
  258. group.add(nextFresnelMesh)
  259. group.add(nextNoiseMesh)
  260. glowMesh.value = nextGlowMesh
  261. fresnelMesh.value = nextFresnelMesh
  262. noiseMesh.value = nextNoiseMesh
  263. particleMesh.value = nextParticleMesh
  264. rootGroup.value = group
  265. }
  266. const syncMaterialUniforms = () => {
  267. setMaterialColor(glowMaterial.uniforms.uColorInner, props.glowColorInner, '#cb5151')
  268. setMaterialColor(glowMaterial.uniforms.uColorOuter, props.glowColorOuter, '#000000')
  269. glowMaterial.uniforms.uEmission.value = safeGlowEmission.value
  270. glowMaterial.uniforms.uFalloffPower.value = safeGlowFalloffPower.value
  271. glowMaterial.uniforms.uEdgeSoftness.value = safeGlowEdgeSoftness.value
  272. setMaterialColor(fresnelMaterial.uniforms.uCoreColor, props.orbCoreColor, '#eaf8fc')
  273. setMaterialColor(fresnelMaterial.uniforms.uRimColor, props.orbRimColor, '#0c61f5')
  274. fresnelMaterial.uniforms.uEmission.value = safeFresnelEmission.value
  275. fresnelMaterial.uniforms.uFresnelPower.value = safeFresnelPower.value
  276. fresnelMaterial.uniforms.uBodyFalloff.value = safeBodyFalloff.value
  277. fresnelMaterial.uniforms.uCoreFalloff.value = safeCoreFalloff.value
  278. fresnelMaterial.uniforms.uCoreBoost.value = safeCoreBoost.value
  279. setMaterialColor(noiseMaterial.uniforms.uColor, props.noiseColor, '#128bff')
  280. noiseMaterial.uniforms.uNoiseScale.value = safeNoiseScale.value
  281. noiseMaterial.uniforms.uNoiseSpeed.value = safeNoiseSpeed.value
  282. noiseMaterial.uniforms.uFlowIntensity.value = safeFlowIntensity.value
  283. noiseMaterial.uniforms.uWarpStrength.value = safeWarpStrength.value
  284. particlesMaterial.color.set(props.particleColor ?? '#59afff').multiplyScalar(safeParticleEmission.value)
  285. }
  286. const getProgress = (elapsed: number) => {
  287. if (!props.autoAnimate) {
  288. return safeChargeProgress.value
  289. }
  290. const duration = safeCycleDuration.value
  291. const phase = (elapsed % duration) / duration
  292. if (phase < 0.8) {
  293. return THREE.MathUtils.smootherstep(phase / 0.8, 0, 1)
  294. }
  295. return 1 - THREE.MathUtils.smootherstep((phase - 0.8) / 0.2, 0, 1)
  296. }
  297. const fastFadeParticles = () => {
  298. for (let index = 0; index < MAX_PARTICLES; index += 1) {
  299. const particle = particles[index]
  300. if (particle.active) {
  301. particle.life = Math.max(particle.life, particle.maxLife - 0.08)
  302. }
  303. }
  304. }
  305. createRootGroup()
  306. watch(
  307. [
  308. () => props.glowColorInner,
  309. () => props.glowColorOuter,
  310. () => props.noiseColor,
  311. () => props.orbCoreColor,
  312. () => props.orbRimColor,
  313. () => props.particleColor,
  314. safeBodyFalloff,
  315. safeCoreBoost,
  316. safeCoreFalloff,
  317. safeFlowIntensity,
  318. safeFresnelEmission,
  319. safeFresnelPower,
  320. safeGlowEdgeSoftness,
  321. safeGlowEmission,
  322. safeGlowFalloffPower,
  323. safeNoiseScale,
  324. safeNoiseSpeed,
  325. safeParticleEmission,
  326. safeWarpStrength,
  327. ],
  328. syncMaterialUniforms,
  329. { immediate: true },
  330. )
  331. const { onBeforeRender } = useLoop()
  332. onBeforeRender(({ delta, elapsed }) => {
  333. if (!glowMesh.value || !fresnelMesh.value || !noiseMesh.value || !particleMesh.value) {
  334. return
  335. }
  336. const progress = getProgress(elapsed)
  337. const pulse = progress > 0.001 ? 1 + Math.sin(elapsed * 10) * 0.06 * progress : 0
  338. const orbScale = progress * safeOrbMaxSize.value * pulse
  339. const visible = orbScale > 0.001
  340. if (progress < 0.02 && previousProgress > 0.35) {
  341. fastFadeParticles()
  342. }
  343. previousProgress = progress
  344. glowMaterial.uniforms.uProgress.value = progress
  345. fresnelMaterial.uniforms.uProgress.value = progress
  346. noiseMaterial.uniforms.uProgress.value = progress
  347. noiseMaterial.uniforms.uTime.value = elapsed
  348. glowMesh.value.visible = visible && props.glowEnabled
  349. glowMesh.value.scale.setScalar(Math.max(0, orbScale * safeGlowSizeMult.value))
  350. fresnelMesh.value.visible = visible
  351. fresnelMesh.value.scale.setScalar(Math.max(0, orbScale))
  352. noiseMesh.value.visible = visible
  353. noiseMesh.value.scale.setScalar(Math.max(0, orbScale))
  354. if (progress > 0.001) {
  355. let activeCount = 0
  356. for (let index = 0; index < MAX_PARTICLES; index += 1) {
  357. if (particles[index].active) {
  358. activeCount += 1
  359. }
  360. }
  361. const targetCount = Math.floor(progress * safeParticleCount.value)
  362. for (let index = 0; index < MAX_PARTICLES && activeCount < targetCount; index += 1) {
  363. const particle = particles[index]
  364. if (particle.active) {
  365. continue
  366. }
  367. const theta = Math.random() * Math.PI * 2
  368. const phi = Math.acos(2 * Math.random() - 1)
  369. spawnDirection.set(
  370. Math.sin(phi) * Math.cos(theta),
  371. Math.sin(phi) * Math.sin(theta),
  372. Math.cos(phi),
  373. )
  374. particle.pos.copy(spawnDirection).multiplyScalar(safeParticleSpawnRadius.value * (0.8 + Math.random() * 0.2))
  375. particle.maxLife = safeParticleLifetime.value * (0.6 + Math.random() * 0.4)
  376. particle.life = 0
  377. particle.phase = Math.random() * Math.PI * 2
  378. particle.active = true
  379. activeCount += 1
  380. }
  381. }
  382. for (let index = 0; index < MAX_PARTICLES; index += 1) {
  383. const particle = particles[index]
  384. if (!particle.active) {
  385. dummy.scale.setScalar(0)
  386. dummy.updateMatrix()
  387. particleMesh.value.setMatrixAt(index, dummy.matrix)
  388. continue
  389. }
  390. particle.life += delta
  391. if (particle.life >= particle.maxLife) {
  392. particle.active = false
  393. dummy.scale.setScalar(0)
  394. dummy.updateMatrix()
  395. particleMesh.value.setMatrixAt(index, dummy.matrix)
  396. continue
  397. }
  398. toCenter.copy(particle.pos).multiplyScalar(-1)
  399. const distance = toCenter.length()
  400. if (distance < 0.04) {
  401. particle.active = false
  402. dummy.scale.setScalar(0)
  403. dummy.updateMatrix()
  404. particleMesh.value.setMatrixAt(index, dummy.matrix)
  405. continue
  406. }
  407. const pullFactor = Math.max(0, 1 - distance / safeParticleSpawnRadius.value) ** 2
  408. const pull = safeParticleSpeed.value * (1 + pullFactor * 4)
  409. toCenter.multiplyScalar((pull * delta) / distance)
  410. particle.pos.add(toCenter)
  411. if (safeParticleTurbulence.value > 0) {
  412. const lifeWave = particle.life * 6
  413. const turbulence = safeParticleTurbulence.value * delta * (distance / safeParticleSpawnRadius.value)
  414. particle.pos.x += Math.sin(lifeWave * 1.3 + particle.phase) * turbulence
  415. particle.pos.y += Math.sin(lifeWave * 0.9 + particle.phase + 1.1) * turbulence
  416. particle.pos.z += Math.sin(lifeWave * 1.7 + particle.phase + 2.5) * turbulence
  417. }
  418. const timeProgress = particle.life / particle.maxLife
  419. const fade = timeProgress < 0.25 ? timeProgress / 0.25 : 1 - (timeProgress - 0.25) / 0.75
  420. particleDirection.copy(particle.pos).multiplyScalar(-1).normalize()
  421. particleQuaternion.setFromUnitVectors(upVector, particleDirection)
  422. dummy.position.copy(particle.pos)
  423. dummy.quaternion.copy(particleQuaternion)
  424. dummy.scale.set(
  425. safeParticleWidth.value * Math.max(0, fade),
  426. safeParticleLength.value * Math.max(0, fade),
  427. safeParticleWidth.value * Math.max(0, fade),
  428. )
  429. dummy.updateMatrix()
  430. particleMesh.value.setMatrixAt(index, dummy.matrix)
  431. }
  432. particleMesh.value.instanceMatrix.needsUpdate = true
  433. })
  434. onBeforeUnmount(() => {
  435. rootGroup.value?.removeFromParent()
  436. rootGroup.value = null
  437. glowMesh.value = null
  438. fresnelMesh.value = null
  439. noiseMesh.value = null
  440. particleMesh.value = null
  441. sphereGeometry.dispose()
  442. capsuleGeometry.dispose()
  443. glowMaterial.dispose()
  444. fresnelMaterial.dispose()
  445. noiseMaterial.dispose()
  446. particlesMaterial.dispose()
  447. })
  448. </script>
  449. <template>
  450. <primitive
  451. v-if="rootGroup"
  452. :object="rootGroup"
  453. />
  454. </template>