SmokePath.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import * as THREE from 'three';
  2. import gsap from 'gsap';
  3. // import * as dat from 'dat.gui';
  4. // const gui = new dat.GUI();
  5. export default class SmokePath {
  6. smokeTexturePath;
  7. pointsMesh;
  8. points = [];
  9. opacity = 0;
  10. size = 15;
  11. scale = 0.8;
  12. color;
  13. range = 30; // 半径
  14. progress = 0;
  15. frameId;
  16. startSpeed = 100;
  17. speed = 0;
  18. speedFactor = 2; // 速度因素
  19. scaleFactor = 0.1; // 缩放比率, 范围0-1
  20. opacityFactor = 1; // 透明度因素, 范围0-1
  21. section1Num = 0; // 默认direction为true时后面一段
  22. section2Num = 25; // 默认direction为true时前面一段 仅用来测试 18
  23. spreadSection = 32; // 默认direction为true时前面一段 扩散部分
  24. flyPointArr = new Map();
  25. geometry;
  26. direction = false; // true 从口到里面
  27. isStop = false;
  28. constructor(smokeTexturePath, color, range, opacityFactor, scaleFactor, life, path) {
  29. this.smokeTexturePath = smokeTexturePath;
  30. this.color = color;
  31. this.opacityFactor = opacityFactor;
  32. this.scaleFactor = scaleFactor;
  33. this.range = range;
  34. // this.life = life;
  35. path.getSpacedPoints(100).forEach((point) => {
  36. const { x, y, z } = point;
  37. this.points.push([x, y, z]);
  38. });
  39. //
  40. //
  41. // gui.add(this, 'speedFactor', 0, 100);
  42. // gui.add(this, 'scaleFactor', 0, 1);
  43. // gui.add(this, 'opacityFactor', 0, 10);
  44. }
  45. setPoints() {
  46. const textureLoader = new THREE.TextureLoader();
  47. // 先创建一个空的缓冲几何体
  48. const geometry = new THREE.BufferGeometry();
  49. geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([]), 3)); // 一个顶点由3个坐标构成
  50. geometry.setAttribute('a_opacity', new THREE.BufferAttribute(new Float32Array([]), 1)); // 点的透明度,用1个浮点数表示
  51. geometry.setAttribute('a_size', new THREE.BufferAttribute(new Float32Array([]), 1)); // 点的初始大小,用1个浮点数表示
  52. geometry.setAttribute('a_scale', new THREE.BufferAttribute(new Float32Array([]), 1)); // 点的放大量,用1个浮点数表示
  53. geometry.boundingBox = new THREE.Box3(new THREE.Vector3(10000, -10000, 10000), new THREE.Vector3(-10000, 10000, -10000));
  54. geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 10000);
  55. // 创建材质
  56. const material = new THREE.PointsMaterial({
  57. color: this.color,
  58. map: textureLoader.load(this.smokeTexturePath), // 纹理图
  59. transparent: true, // 开启透明度
  60. depthWrite: false, // 禁止深度写入
  61. sizeAttenuation: true,
  62. side: THREE.DoubleSide,
  63. });
  64. // 修正着色器
  65. material.onBeforeCompile = function (shader) {
  66. const vertexShader_attribute = `
  67. attribute float a_opacity;
  68. attribute float a_size;
  69. attribute float a_scale;
  70. varying float v_opacity;
  71. void main() {
  72. v_opacity = a_opacity;
  73. `;
  74. const vertexShader_size = `
  75. gl_PointSize = a_size * a_scale;
  76. `;
  77. shader.vertexShader = shader.vertexShader.replace('void main() {', vertexShader_attribute);
  78. shader.vertexShader = shader.vertexShader.replace('gl_PointSize = size;', vertexShader_size);
  79. const fragmentShader_varying = `
  80. varying float v_opacity;
  81. void main() {
  82. `;
  83. const fragmentShader_opacity = `
  84. vec4 diffuseColor = vec4( diffuse, opacity * v_opacity );
  85. `;
  86. shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragmentShader_varying);
  87. shader.fragmentShader = shader.fragmentShader.replace('vec4 diffuseColor = vec4( diffuse, opacity );', fragmentShader_opacity);
  88. };
  89. // 创建点,并添加进场景
  90. this.pointsMesh = new THREE.Points(geometry, material);
  91. geometry.dispose();
  92. material.dispose();
  93. }
  94. updateFly() {
  95. type attributeType = {
  96. index: number;
  97. point: number[];
  98. scale: number;
  99. size: number;
  100. opacity: number;
  101. oldOpacity: number;
  102. };
  103. const total = this.points.length - this.section1Num - Math.floor(this.section2Num);
  104. let startIndex = this.direction ? this.section1Num : this.points.length - Math.floor(this.section2Num);
  105. if (this.flyPointArr.size < total - 1) {
  106. let index;
  107. if (this.flyPointArr.size == 0) {
  108. // 起始计算
  109. index = this.direction ? ++startIndex : --startIndex;
  110. } else {
  111. // 根据flyPointArr 已有数量时根据自己的数量结合起点时的数,判断方向, 但是要注意最大、最小的临界值
  112. if (this.direction) {
  113. index = this.flyPointArr.size + startIndex + 1;
  114. } else {
  115. index = startIndex - this.flyPointArr.size - 1;
  116. }
  117. }
  118. if (index != undefined) {
  119. let opacity;
  120. opacity = (this.flyPointArr.size / total) * 0.2 * this.opacityFactor + 0.02;
  121. const attribute: attributeType = {
  122. index: index,
  123. point: this.points[index],
  124. scale: 0.5 + Math.random() * 0.5,
  125. size: this.size,
  126. opacity: opacity,
  127. oldOpacity: opacity,
  128. };
  129. // 判断点是否已经存在
  130. if (!this.flyPointArr.has(index)) {
  131. this.flyPointArr.set(index, attribute);
  132. }
  133. }
  134. } else {
  135. // 变短时候要移除点
  136. }
  137. if (this.flyPointArr.size > 0) {
  138. // 计算新数据
  139. const posArr = [];
  140. const scaleArr: number[] = [];
  141. const sizeArr: number[] = [];
  142. let opacityArr: number[] = [];
  143. const flyPointArr = this.flyPointArr.entries();
  144. for (let i = 0; i < this.flyPointArr.size; i++) {
  145. const attribute = flyPointArr.next().value[1];
  146. let scale,
  147. point = attribute.point,
  148. index = attribute.index;
  149. const num = this.points.length - index;
  150. const factor = 1 - num / this.spreadSection; // 扩散点对scale的影响
  151. if (index > this.points.length - 1 - this.spreadSection) {
  152. // 扩散气体部分
  153. attribute.scale = 1.1 + Math.random() * factor * 3;
  154. scale = (attribute.scale / 4) * 3 + (Math.random() * attribute.scale) / 4;
  155. point = [attribute.point[0] + Math.random() * 1, attribute.point[1] + Math.random() * 7, attribute.point[2] + Math.random() * 1];
  156. } else {
  157. if (this.flyPointArr.size < total - 1 && !this.isStop) {
  158. // 逐渐增加粒子时
  159. scale = 0.6 + Math.random() * 0.3;
  160. } else {
  161. // 粒子数全部加载后
  162. scale = this.scaleFactor + Math.random() * (1 - this.scaleFactor);
  163. }
  164. }
  165. let opacity;
  166. if (this.isStop) {
  167. if (index > this.points.length - 1 - this.spreadSection) {
  168. point = [attribute.point[0] - Math.random() * 1, attribute.point[1] - Math.random() * 7, attribute.point[2] - Math.random() * 1];
  169. }
  170. attribute.opacity = attribute.oldOpacity - (1 - this.speed / this.startSpeed) * attribute.oldOpacity;
  171. attribute.scale = (attribute.scale * this.speed) / this.startSpeed;
  172. }
  173. opacity = this.opacityFactor * attribute.opacity;
  174. posArr.push(...point);
  175. sizeArr.push(attribute.size);
  176. scaleArr.push(scale);
  177. opacityArr.push(opacity);
  178. }
  179. if (this.direction) {
  180. opacityArr = opacityArr.reverse();
  181. }
  182. // 更新几何体
  183. this.pointsMesh.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(posArr), 3));
  184. this.pointsMesh.geometry.setAttribute('a_scale', new THREE.BufferAttribute(new Float32Array(scaleArr), 1));
  185. this.pointsMesh.geometry.setAttribute('a_opacity', new THREE.BufferAttribute(new Float32Array(opacityArr), 1));
  186. this.pointsMesh.geometry.setAttribute('a_size', new THREE.BufferAttribute(new Float32Array(sizeArr), 1));
  187. }
  188. }
  189. // 移动
  190. move(duration = 10) {
  191. if (this.frameId) return;
  192. const _this = this;
  193. gsap.to(this, {
  194. opacity: 0.2,
  195. duration: duration,
  196. ease: 'easeInCirc',
  197. overwrite: true,
  198. });
  199. gsap.to(this, {
  200. speed: this.startSpeed,
  201. duration: duration,
  202. ease: 'easeInCirc',
  203. overwrite: true,
  204. });
  205. let startTime = new Date().getTime();
  206. const h = () => {
  207. if (_this.frameId) {
  208. _this.frameId = requestAnimationFrame(h);
  209. }
  210. const now = new Date().getTime();
  211. if (((now - startTime) as number) > this.startSpeed - this.speed + this.speedFactor) {
  212. _this.updateFly();
  213. startTime = new Date().getTime();
  214. }
  215. };
  216. this.frameId = requestAnimationFrame(h);
  217. }
  218. resetParam() {
  219. window.cancelAnimationFrame(this.frameId);
  220. this.frameId = 0;
  221. this.isStop = false;
  222. this.flyPointArr.clear();
  223. this.opacity = 0;
  224. this.speed = 0;
  225. }
  226. changeDirection() {
  227. const _this = this;
  228. this.isStop = true;
  229. return new Promise((resolve) => {
  230. if (this.direction) {
  231. gsap.to(this, {
  232. section2Num: 25,
  233. spreadSection: 32,
  234. speed: 0,
  235. duration: 10,
  236. ease: 'easeInCirc',
  237. overwrite: true,
  238. onComplete: function () {
  239. _this.resetParam();
  240. _this.direction = !_this.direction;
  241. _this.move();
  242. resolve(null);
  243. },
  244. });
  245. } else {
  246. gsap.to(this, {
  247. section2Num: 0,
  248. spreadSection: 30,
  249. speed: 0,
  250. duration: 10,
  251. ease: 'easeInCirc',
  252. overwrite: true,
  253. onComplete: function () {
  254. _this.resetParam();
  255. _this.direction = !_this.direction;
  256. _this.move();
  257. resolve(null);
  258. },
  259. });
  260. }
  261. });
  262. }
  263. stopSmoke() {
  264. const _this = this;
  265. this.isStop = true;
  266. return new Promise((resolve) => {
  267. if (this.direction) {
  268. gsap.to(this, {
  269. speed: 0,
  270. opacity: 0,
  271. duration: 10,
  272. ease: 'easeInCirc',
  273. overwrite: true,
  274. onComplete: function () {
  275. _this.resetParam();
  276. resolve(null);
  277. },
  278. });
  279. } else {
  280. gsap.to(this, {
  281. speed: 0,
  282. opacity: 0,
  283. duration: 10,
  284. ease: 'easeInCirc',
  285. overwrite: true,
  286. onComplete: function () {
  287. _this.resetParam();
  288. resolve(null);
  289. },
  290. });
  291. }
  292. });
  293. }
  294. clearSmoke() {
  295. this.stopSmoke();
  296. this.flyPointArr.clear();
  297. this.pointsMesh.geometry.dispose();
  298. this.pointsMesh.texture.dispose();
  299. this.points = [];
  300. }
  301. }