浏览代码

feat: 烟雾动画

chenlei 1 年之前
父节点
当前提交
6b65c3995b

+ 215 - 0
src/pages/tnd/ActModel.ts

@@ -0,0 +1,215 @@
+import { ReticleModel, renderModel } from "../../models";
+import {
+  BackSide,
+  BufferAttribute,
+  BufferGeometry,
+  CatmullRomCurve3,
+  Group,
+  Line,
+  LineBasicMaterial,
+  MeshPhongMaterial,
+  RepeatWrapping,
+  Texture,
+  Vector3,
+} from "three-platformize";
+import { SmokeModel } from "./SmokeModel";
+
+export class ActModel {
+  model: Group;
+  scaleNum = 20;
+  private point = new Vector3();
+  private ARC_SEGMENTS = 280;
+  private smokePoint: Vector3[] = [];
+  private smokePosAll: Vector3[] = [];
+  private smokeGroupArr: SmokeModel[] = [];
+  yandaoTexture: Texture | null = null;
+  curve: CatmullRomCurve3 | null = null;
+
+  constructor() {
+    renderModel.fileLoaderMap.fbx.load(
+      "https://houseoss.4dkankan.com/project/yzdyh-dadu/test/model_t.FBX",
+      (model) => {
+        this.model = model;
+        renderModel.scene.add(this.model);
+        this.model.position.set(0, 0, 0);
+        this.onComplete();
+      }
+    );
+  }
+
+  onComplete() {
+    this.model.castShadow = true;
+    this.model.receiveShadow = true;
+    this.model.traverse((m) => {
+      // @ts-ignore
+      if (m.isMesh) {
+        const meshPhongMaterial = new MeshPhongMaterial();
+        // @ts-ignore
+        meshPhongMaterial.copy(m.material);
+        switch (m.name) {
+          case "yanwu":
+            m.visible = false;
+            meshPhongMaterial.opacity = 0.001;
+            meshPhongMaterial.transparent = true;
+            meshPhongMaterial.side = 0;
+            meshPhongMaterial.shininess = 10; // 光泽度
+            // @ts-ignore
+            this.yandaoTexture = m.material.map;
+            this.yandaoTexture!.wrapS = this.yandaoTexture!.wrapT =
+              RepeatWrapping;
+            this.yandaoTexture!.repeat.set(1, 1);
+            break;
+          case "yanwu_shang":
+          case "yanwu_xia":
+            meshPhongMaterial.opacity = 0.3;
+            meshPhongMaterial.transparent = true;
+            meshPhongMaterial.side = 2;
+            break;
+          default:
+            meshPhongMaterial.opacity = 0.2;
+            meshPhongMaterial.transparent = true;
+            meshPhongMaterial.depthWrite = false;
+            meshPhongMaterial.side = BackSide;
+        }
+
+        // @ts-ignore
+        m.material.dispose();
+        // @ts-ignore
+        m.material = meshPhongMaterial;
+        // @ts-ignore
+        m.material.needsUpdate = true;
+      }
+    });
+    this.model.scale.copy(
+      new Vector3(this.scaleNum, this.scaleNum, this.scaleNum)
+    );
+
+    for (let i = 1; i <= 14; i++) {
+      // 局部坐标转世界坐标
+      const point = this.model
+        .localToWorld(
+          this.model.getObjectByName("烟点位".concat(`${i}`))!.position.clone()
+        )
+        .multiplyScalar(this.scaleNum);
+      this.smokePoint.push(point);
+    }
+
+    this.smokeparticle();
+    this.handleAnimatorCallout(false);
+    this.addWater();
+    this.startPlay(false);
+  }
+
+  addWater() {
+    const waterModel = this.model.getObjectByName("water01");
+    // @ts-ignore
+    const geometry = waterModel.geometry.clone();
+
+    console.log(geometry);
+  }
+
+  smokeparticle() {
+    const bufferGemoetry = new BufferGeometry();
+
+    bufferGemoetry.setAttribute(
+      "position",
+      new BufferAttribute(new Float32Array(3 * this.ARC_SEGMENTS), 3)
+    );
+
+    // 绘制烟雾曲线路径
+    this.curve = new CatmullRomCurve3(this.smokePoint);
+    // @ts-ignore
+    this.curve.mesh = new Line(
+      bufferGemoetry.clone(),
+      new LineBasicMaterial({ color: 255, opacity: 1 })
+    );
+    // @ts-ignore
+    this.curve.mesh.castShadow = true;
+
+    // @ts-ignore
+    const positions = this.curve.mesh.geometry.attributes.position;
+    for (let n = 0; n < this.ARC_SEGMENTS; n++) {
+      const r = n / (this.ARC_SEGMENTS - 1);
+      this.curve.getPoint(r, this.point);
+      positions.setXYZ(n, this.point.x, this.point.y, this.point.z);
+    }
+    positions.needsUpdate = true;
+
+    const positionArray = positions.array;
+    for (let a = 0; a < positionArray.length / 3; a++) {
+      const o = 3 * a;
+      const l = new Vector3(
+        positionArray[o],
+        positionArray[o + 1],
+        positionArray[o + 2]
+      );
+      this.smokePosAll.push(l);
+    }
+
+    this.smokePosAll.reverse();
+  }
+
+  private hotspotAnimArr: ReticleModel[] = [];
+
+  handleAnimatorCallout(visible: boolean) {
+    if (!this.hotspotAnimArr.length) {
+      const v3 = new Vector3(
+        this.model.getObjectByName("灯芯中心点")!.position.x,
+        this.model.getObjectByName("灯芯中心点")!.position.y + 0.5,
+        this.model.getObjectByName("灯芯中心点")!.position.z
+      );
+
+      const reticleHuo = new ReticleModel({
+        name: "huoAnim",
+        url: require("./resource/zhuhuo_anim.png"),
+        tilesHorizontal: 16,
+        tilesVertical: 1,
+        camera: renderModel.camera,
+        scale: 0.004,
+        planeWidth: 192,
+        tileDisplayDuration: 150,
+        position: v3,
+      });
+
+      // @ts-ignore
+      reticleHuo.reticleAnim.material.opacity = 0;
+      // @ts-ignore
+      reticleHuo.reticleAnim.material.depthTest = true;
+      this.model.add(reticleHuo.reticleAnim);
+      this.hotspotAnimArr.push(reticleHuo);
+    }
+
+    this.hotspotAnimArr.forEach((i) => {
+      visible ? i.show() : i.hide();
+    });
+  }
+
+  private interTime: NodeJS.Timeout;
+
+  startPlay(type: boolean) {
+    if (type) {
+      this.interTime = setInterval(() => {
+        const smoke = new SmokeModel(this.smokePosAll, 0);
+        smoke.initSmokeGroup(0);
+        this.smokeGroupArr.push(smoke);
+      }, 50);
+    } else {
+      clearInterval(this.interTime);
+      this.smokeGroupArr.forEach((item) => {
+        item.clear();
+      });
+      this.smokeGroupArr = [];
+    }
+
+    this.model.visible = type;
+    this.handleAnimatorCallout(type);
+  }
+
+  update(time: number) {
+    if (this.model?.visible) {
+      this.hotspotAnimArr.forEach((i) => {
+        i.update(time);
+      });
+    }
+  }
+}

+ 155 - 0
src/pages/tnd/SmokeModel.ts

@@ -0,0 +1,155 @@
+import { Easing, Tween, remove } from "@tweenjs/tween.js";
+import { renderModel } from "../../models";
+import { tweenHandler } from "../../utils";
+import {
+  DoubleSide,
+  Group,
+  Mesh,
+  Sprite,
+  SpriteMaterial,
+  Texture,
+  TextureLoader,
+  Vector3,
+} from "three-platformize";
+
+export class SmokeModel {
+  private smokePointArr: Vector3[] = [];
+  private intervals = 0;
+  private tweenNum: Tween<any>;
+  private swSmokeSpriteArr: SmokeSprite[] = [];
+  private smokeGroup: null | Group = null;
+  smokeTexture: Texture;
+
+  constructor(smokePointArr: Vector3[], intervals: number) {
+    this.smokePointArr = smokePointArr;
+    this.intervals = intervals;
+    this.smokeTexture = new TextureLoader().load(
+      require("./resource/Smoke-Element.png")
+    );
+  }
+
+  initSmokeGroup(idx: number) {
+    if (idx >= this.smokePointArr.length - 1) {
+      this.smokeGroup?.traverse((mesh) => {
+        if (mesh instanceof Mesh) {
+          mesh.material.dispose();
+          mesh.geometry.dispose();
+          mesh.removeFromParent();
+        }
+      });
+      this.swSmokeSpriteArr = [];
+      this.smokeGroup?.removeFromParent();
+      remove(this.tweenNum);
+      return;
+    }
+
+    this.getSmokeGroup();
+
+    this.tweenNum = tweenHandler({
+      curProps: {
+        x: this.smokePointArr[idx].x,
+        y: this.smokePointArr[idx].y,
+        z: this.smokePointArr[idx].z,
+      },
+      targetProps: {
+        x: this.smokePointArr[idx + 1].x,
+        y: this.smokePointArr[idx + 1].y,
+        z: this.smokePointArr[idx + 1].z,
+      },
+      delay: 10,
+      onUpdate: (e) => {
+        this.smokeGroup?.position.copy(new Vector3(e.x, e.y, e.z));
+      },
+      cb: () => {
+        remove(this.tweenNum);
+        this.swSmokeSpriteArr.forEach(function (t) {
+          t.updateAnim(idx);
+        });
+        idx++;
+        this.initSmokeGroup(idx);
+      },
+      easing: Easing.Exponential.Out,
+    });
+  }
+
+  getSmokeGroup() {
+    if (!this.smokeGroup) {
+      this.smokeGroup = new Group();
+      for (let i = 0; i < 2; i++) {
+        const smokeSprite = new SmokeSprite(this.smokeTexture);
+        this.swSmokeSpriteArr.push(smokeSprite);
+        this.smokeGroup.add(smokeSprite.smokeSprite);
+      }
+      renderModel.scene.add(this.smokeGroup);
+    }
+  }
+
+  clear() {
+    this.smokeGroup?.traverse((item) => {
+      if (item instanceof Mesh) {
+        item.material.dispose();
+        item.geometry.dispose();
+        item.removeFromParent();
+      }
+    });
+    this.swSmokeSpriteArr = [];
+    this.smokeGroup?.removeFromParent();
+    remove(this.tweenNum);
+  }
+}
+
+export class SmokeSprite {
+  opacity = 0.001;
+  opacity_ratio = 0.5;
+  smokeSprite: Sprite;
+
+  constructor(smokeTexture: Texture) {
+    // 创建粒子材质
+    const spriteMaterial = new SpriteMaterial({
+      map: smokeTexture,
+      color: "#ffffff",
+      transparent: true,
+      opacity: this.opacity,
+      depthWrite: false,
+      side: DoubleSide,
+    });
+
+    this.smokeSprite = new Sprite(spriteMaterial);
+    this.smokeSprite.position.set(
+      1 * (Math.random() - 0.5),
+      1 * (Math.random() - 0.5),
+      1 * (Math.random() - 0.5)
+    );
+    this.smokeSprite.rotation.z = 360 * Math.random();
+    this.smokeSprite.scale.set(
+      1 + 30 * Math.random(),
+      1 + 30 * Math.random(),
+      1
+    );
+  }
+
+  private scale_ratio = 1;
+  updateAnim(num: number) {
+    let t = this.smokeSprite.scale.x;
+    let n = this.smokeSprite.scale.y;
+
+    if (num < 40) {
+      this.scale_ratio = 0.95;
+      t *= this.scale_ratio;
+      n *= this.scale_ratio;
+      this.opacity += 0.01;
+      const r = 5;
+      t = t < r ? r : t;
+      n = n < r ? r : n;
+    } else if (num > 200) {
+      this.scale_ratio = 0.12;
+      t += this.scale_ratio;
+      n += this.scale_ratio;
+      this.opacity -= 0.004;
+    }
+
+    this.smokeSprite.scale.set(t, n, 1);
+    this.smokeSprite.material.opacity = this.opacity;
+    this.smokeSprite.lookAt(renderModel.camera.position);
+  }
+}

+ 199 - 0
src/pages/tnd/Water.ts

@@ -0,0 +1,199 @@
+import {
+  BufferGeometry,
+  Color,
+  FrontSide,
+  LinearFilter,
+  Matrix4,
+  RGBFormat,
+  ShaderMaterial,
+  UniformsLib,
+  UniformsUtils,
+  Vector3,
+  WebGLRenderTarget,
+} from "three-platformize";
+
+export interface WaterParams {
+  textureWidth?: number;
+  textureHeight?: number;
+  alpha?: number;
+  time?: number;
+  waterNormals?: THREE.Texture | null;
+  sunDirection?: THREE.Vector3;
+  sunColor?: THREE.Color;
+  waterColor?: THREE.Color;
+  eye?: THREE.Vector3;
+  distortionScale?: number;
+  side?: THREE.Side;
+  fog?: boolean;
+}
+
+export class Water {
+  material: ShaderMaterial;
+
+  constructor(geometry: BufferGeometry, params: WaterParams) {
+    const {
+      textureWidth = 512,
+      textureHeight = 512,
+      alpha = 1,
+      time = 0,
+      waterNormals = null,
+      sunDirection = new Vector3(0.70707, 0.70707, 0),
+      sunColor = new Color(16777215),
+      waterColor = new Color(8355711),
+      eye = new Vector3(0, 0, 0),
+      distortionScale = 20,
+      side = FrontSide,
+      fog = true,
+    } = params;
+
+    const mirrorShader = {
+      uniforms: UniformsUtils.merge([
+        UniformsLib.fog,
+        UniformsLib.lights,
+        {
+          normalSampler: { value: null },
+          mirrorSampler: { value: null },
+          alpha: { value: 1 },
+          time: { value: 0 },
+          size: { value: 1 },
+          distortionScale: { value: 20 },
+          textureMatrix: { value: new Matrix4() },
+          sunColor: { value: new Color(8355711) },
+          sunDirection: { value: new Vector3(0.70707, 0.70707, 0) },
+          eye: { value: new Vector3() },
+          waterColor: { value: new Color(5592405) },
+        },
+      ]),
+      vertexShader: `
+        uniform mat4 textureMatrix;
+        uniform float time;
+
+        varying vec4 mirrorCoord;
+        varying vec4 worldPosition;
+
+        #include <common>
+        #include <fog_pars_vertex>
+        #include <shadowmap_pars_vertex>
+        #include <logdepthbuf_pars_vertex>
+
+        void main() {
+          mirrorCoord = modelMatrix * vec4(position, 1.0);
+          worldPosition = mirrorCoord.xyzw;
+          mirrorCoord = textureMatrix * mirrorCoord;
+          vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
+          gl_Position = projectionMatrix * mvPosition;
+
+          #include <beginnormal_vertex>
+          #include <defaultnormal_vertex>
+          #include <logdepthbuf_vertex>
+          #include <fog_vertex>
+          #include <shadowmap_vertex>
+        }
+      `,
+      fragmentShader: `
+        uniform sampler2D mirrorSampler;
+        uniform float alpha;
+        uniform float time;
+        uniform float size;
+        uniform float distortionScale;
+        uniform sampler2D normalSampler;
+        uniform vec3 sunColor;
+        uniform vec3 sunDirection;
+        uniform vec3 eye;
+        uniform vec3 waterColor;
+
+        varying vec4 mirrorCoord;
+        varying vec4 worldPosition;
+
+        vec4 getNoise(vec2 uv) {
+          vec2 uv0 = (uv / 103.0) + vec2(time / 17.0, time / 29.0);
+          vec2 uv1 = uv / 107.0 - vec2(time / -19.0, time / 31.0);
+          vec2 uv2 = uv / vec2(8907.0, 9803.0) + vec2(time / 101.0, time / 97.0);
+          vec2 uv3 = uv / vec2(1091.0, 1027.0) - vec2(time / 109.0, time / -113.0);
+          vec4 noise = texture2D(normalSampler, uv0) +
+                       texture2D(normalSampler, uv1) +
+                       texture2D(normalSampler, uv2) +
+                       texture2D(normalSampler, uv3);
+          return noise * 0.5 - 1.0;
+        }
+
+        void sunLight(const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse, inout vec3 diffuseColor, inout vec3 specularColor) {
+          vec3 reflection = normalize(reflect(-sunDirection, surfaceNormal));
+          float direction = max(0.0, dot(eyeDirection, reflection));
+          specularColor += pow(direction, shiny) * sunColor * spec;
+          diffuseColor += max(dot(sunDirection, surfaceNormal), 0.0) * sunColor * diffuse;
+        }
+        #include <common>
+        #include <packing>
+        #include <bsdfs>
+        #include <fog_pars_fragment>
+        #include <logdepthbuf_pars_fragment>
+        #include <lights_pars_begin>
+        #include <shadowmap_pars_fragment>
+        #include <shadowmask_pars_fragment>
+
+        void main() {
+          #include <logdepthbuf_fragment>
+
+          vec4 noise = getNoise(worldPosition.xz * size);
+          vec3 surfaceNormal = normalize(noise.xzy * vec3(1.5, 1.0, 1.5));
+
+          vec3 diffuseLight = vec3(0.0);
+          vec3 specularLight = vec3(0.0);
+
+          vec3 worldToEye = eye - worldPosition.xyz;
+          vec3 eyeDirection = normalize(worldToEye);
+          sunLight(surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight);
+
+          float distance = length(worldToEye);
+
+          vec2 distortion = surfaceNormal.xz * (0.001 + 1.0 / distance) * distortionScale;
+          vec3 reflectionSample = vec3(texture2D(mirrorSampler, mirrorCoord.xy / mirrorCoord.w + distortion));
+
+          float theta = max(dot(eyeDirection, surfaceNormal), 0.0);
+          float rf0 = 0.3;
+          float reflectance = rf0 + (1.0 - rf0) * pow((1.0 - theta), 5.0);
+
+          vec3 scatter = max(0.0, dot(surfaceNormal, eyeDirection)) * waterColor;
+          vec3 albedo = mix((sunColor * diffuseLight * 0.3 + scatter) * getShadowMask(), (vec3(0.1) + reflectionSample * 0.9 + reflectionSample * specularLight), reflectance);
+          vec3 outgoingLight = albedo;
+          gl_FragColor = vec4(outgoingLight, alpha);
+
+          #include <tonemapping_fragment>
+          #include <fog_fragment>
+        }
+      `,
+    };
+
+    const waterMaterial = new ShaderMaterial({
+      fragmentShader: mirrorShader.fragmentShader,
+      vertexShader: mirrorShader.vertexShader,
+      uniforms: UniformsUtils.clone(mirrorShader.uniforms),
+      lights: true,
+      side,
+      fog,
+    });
+
+    waterMaterial.uniforms.mirrorSampler.value = new WebGLRenderTarget(
+      textureWidth,
+      textureHeight,
+      {
+        minFilter: LinearFilter,
+        magFilter: LinearFilter,
+        format: RGBFormat,
+      }
+    ).texture;
+
+    waterMaterial.uniforms.textureMatrix.value = new Matrix4();
+    waterMaterial.uniforms.alpha.value = alpha;
+    waterMaterial.uniforms.time.value = time;
+    waterMaterial.uniforms.normalSampler.value = waterNormals;
+    waterMaterial.uniforms.sunColor.value = sunColor;
+    waterMaterial.uniforms.waterColor.value = waterColor;
+    waterMaterial.uniforms.sunDirection.value = sunDirection;
+    waterMaterial.uniforms.distortionScale.value = distortionScale;
+    waterMaterial.uniforms.eye.value = eye;
+
+    this.material = waterMaterial;
+  }
+}

+ 9 - 0
src/pages/tnd/index.scss

@@ -17,3 +17,12 @@
   right: 20px;
   z-index: 999;
 }
+
+.slider {
+  position: fixed;
+  left: 50%;
+  bottom: 100px;
+  width: 400px;
+  transform: translateX(-50%);
+  z-index: 999;
+}

+ 82 - 52
src/pages/tnd/index.tsx

@@ -1,5 +1,11 @@
 import { useEffect, useMemo, useRef, useState } from "react";
-import { Button, View } from "@tarojs/components";
+import {
+  BaseEventOrig,
+  Button,
+  Slider,
+  SliderProps,
+  View,
+} from "@tarojs/components";
 import Taro, { FC } from "@tarojs/taro";
 import { observer } from "mobx-react";
 import {
@@ -25,9 +31,10 @@ import {
 import { OutlinePass } from "three-platformize/examples/jsm/postprocessing/OutlinePass";
 import { RenderPass } from "three-platformize/examples/jsm/postprocessing/RenderPass";
 import { EffectComposer } from "three-platformize/examples/jsm/postprocessing/EffectComposer";
-import { Easing, Tween } from "@tweenjs/tween.js";
-import { cancelAnimationFrame, requestAnimationFrame } from "@tarojs/runtime";
+import { requestAnimationFrame } from "@tarojs/runtime";
 import { CanvasAdapter } from "../../components";
+import { tweenHandler } from "../../utils";
+import { ActModel } from "./ActModel";
 import "./index.scss";
 
 enum MODEL_STATE {
@@ -38,19 +45,6 @@ enum MODEL_STATE {
   CHECK_HUO = 4,
 }
 
-interface TweenHandlerOptions<T extends object> {
-  /** 当前属性 */
-  curProps: T;
-  /** 目标属性 */
-  targetProps: T;
-  /** 动画更新回调 */
-  onUpdate?: (e: T) => void;
-  /** 动画完成回调 */
-  cb?: Function;
-  /** 动画时长 默认 1000 ms */
-  delay?: number;
-}
-
 const DEFUALT_SCALE = 20;
 const DENGZHAO_MIN_ROTATE = -1.2;
 const DENGZHAO_MAX_ROTATE = -2.55;
@@ -92,6 +86,10 @@ const IndexPage: FC = observer(() => {
     () => modelState === MODEL_STATE.CHECK_WEN,
     [modelState]
   );
+  const isHuo = useMemo(
+    () => modelState === MODEL_STATE.CHECK_HUO,
+    [modelState]
+  );
   const hotSeparateAnimArr = useRef<ReticleModel[]>([]);
   const hotspotAnimArr = useRef<ReticleModel[]>([]);
   const tagArr = useRef<TagModel[]>([]);
@@ -100,6 +98,7 @@ const IndexPage: FC = observer(() => {
   const wenCurStep = useRef(0);
   const wenStep = useRef(0.01);
   const outlinePass = useRef<OutlinePass>();
+  const modelT = useRef<ActModel>();
 
   useEffect(() => {
     setTimeout(async () => {
@@ -200,6 +199,7 @@ const IndexPage: FC = observer(() => {
         },
         cb: () => {
           handleAnimatorCallout(true);
+          modelT.current = new ActModel();
           setTimeout(() => {
             renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
           }, 800);
@@ -230,31 +230,6 @@ const IndexPage: FC = observer(() => {
     );
   };
 
-  /**
-   * 相机动画
-   */
-  function tweenHandler<T extends object>(options: TweenHandlerOptions<T>) {
-    let animaId: number | NodeJS.Timeout = 0;
-    const { curProps, targetProps, onUpdate, cb } = options;
-
-    const tween = new Tween(curProps)
-      .to(targetProps, options.delay || 1000)
-      .onUpdate(onUpdate)
-      .onComplete(() => {
-        cancelAnimationFrame(animaId as number);
-        cb && cb();
-      })
-      .easing(Easing.Quintic.Out)
-      .start();
-
-    function animate(time?: number) {
-      animaId = requestAnimationFrame(animate);
-      tween.update(time);
-    }
-
-    animate();
-  }
-
   const modelChildRotation = (e: number) => {
     const dengzhao1Mesh = renderModel.model?.getObjectByName("dengzhao1");
 
@@ -574,6 +549,8 @@ const IndexPage: FC = observer(() => {
       item.update(time);
     });
 
+    modelT.current?.update(time);
+
     if (_modelState.current === MODEL_STATE.CHECK_WEN) {
       wenCurStep.current += wenStep.current;
 
@@ -614,16 +591,22 @@ const IndexPage: FC = observer(() => {
         if (isSeparate) {
           handleModelZoomUp(name);
           break;
-        } else if (modelState === MODEL_STATE.DEFAULT) {
-          if (name === "wen") {
-            wenliHandler(true);
-            break;
-          }
+        } else if (modelState === MODEL_STATE.DEFAULT && name === "wen") {
+          wenliHandler(true);
+          break;
+        } else if (modelState === MODEL_STATE.DEFAULT && name === "huo") {
+          huoHandler(true);
+          break;
         }
       }
     }
   };
 
+  const setCanvasPosition = (x: number, y: number) => {
+    mouseV2.current.x = (x / system.windowWidth) * 2 - 1;
+    mouseV2.current.y = (-y / system.windowHeight) * 2 + 1;
+  };
+
   const wenliHandler = (visible: boolean) => {
     revertModel(() => {
       if (visible) {
@@ -736,11 +719,6 @@ const IndexPage: FC = observer(() => {
     });
   };
 
-  const setCanvasPosition = (x: number, y: number) => {
-    mouseV2.current.x = (x / system.windowWidth) * 2 - 1;
-    mouseV2.current.y = (-y / system.windowHeight) * 2 + 1;
-  };
-
   /**
    * 返回分解模型
    */
@@ -780,9 +758,57 @@ const IndexPage: FC = observer(() => {
       backForSeparate();
     } else if (isWen) {
       wenliHandler(false);
+    } else if (isHuo) {
+      huoHandler(false);
     }
   };
 
+  const huoHandler = (visible: boolean) => {
+    revertModel(() => {
+      if (visible) {
+        renderModel.setAutoRotate(ROTATE_TYPE.FALSE);
+        renderModel.setControlsStatus(true, true, false, true, false);
+        bulbLight.current!.visible = true;
+        handleAnimatorCallout(false);
+        handleHuoPlay(true);
+        setModelState(MODEL_STATE.CHECK_HUO);
+      } else {
+        handleHuoPlay(false);
+        modelChildRotation(5);
+        renderModel.setControlsStatus(true, true, false);
+        bulbLight.current!.visible = false;
+        handleAnimatorCallout(true);
+        setModelState(MODEL_STATE.DEFAULT);
+      }
+    });
+  };
+
+  const handleHuoPlay = (type: boolean) => {
+    if (type) {
+      modelT.current?.startPlay(true);
+    } else {
+      let opacity = renderModel.modelMaterialList[0].material.opacity;
+      const timer = setInterval(() => {
+        opacity += 0.05;
+        renderModel.modelMaterialList.forEach((i) => {
+          i.material.opacity = opacity;
+          i.visible = true;
+        });
+        if (opacity >= 1) {
+          clearInterval(timer);
+          modelT.current?.startPlay(false);
+        }
+      }, 50);
+    }
+  };
+
+  const handleSlider = (e: BaseEventOrig<SliderProps.onChangeEventDetail>) => {
+    const opt = e.detail.value / 10 + 1;
+    // 设置光源强度
+    bulbLight.current!.intensity = 0.5 * opt;
+    modelChildRotation(opt);
+  };
+
   return (
     <View className="page">
       <CanvasAdapter
@@ -798,7 +824,7 @@ const IndexPage: FC = observer(() => {
       />
 
       <View className="toolbar">
-        {isSingleModel || isWen ? (
+        {isSingleModel || isWen || isHuo ? (
           <Button className="btn" onClick={handleBack}>
             返回
           </Button>
@@ -808,6 +834,10 @@ const IndexPage: FC = observer(() => {
           </Button>
         )}
       </View>
+
+      {isHuo && (
+        <Slider className="slider" value={50} onChanging={handleSlider} />
+      )}
     </View>
   );
 });

二进制
src/pages/tnd/resource/Smoke-Element.png


二进制
src/pages/tnd/resource/waternormals.jpg


二进制
src/pages/tnd/resource/zhuhuo_anim.png


+ 1 - 0
src/utils/index.ts

@@ -0,0 +1 @@
+export * from "./tween";

+ 42 - 0
src/utils/tween.ts

@@ -0,0 +1,42 @@
+import { Easing, Tween } from "@tweenjs/tween.js";
+import { cancelAnimationFrame, requestAnimationFrame } from "@tarojs/runtime";
+
+export interface TweenHandlerOptions<T extends object> {
+  /** 当前属性 */
+  curProps: T;
+  /** 目标属性 */
+  targetProps: T;
+  /** 动画更新回调 */
+  onUpdate?: (e: T) => void;
+  /** 动画完成回调 */
+  cb?: Function;
+  /** 动画时长 默认 1000 ms */
+  delay?: number;
+  easing?: any;
+}
+
+export function tweenHandler<T extends object>(
+  options: TweenHandlerOptions<T>
+) {
+  let animaId: number | NodeJS.Timeout = 0;
+  const { curProps, targetProps, onUpdate, cb } = options;
+
+  const tween = new Tween(curProps)
+    .to(targetProps, typeof options.delay === "number" ? options.delay : 1000)
+    .onUpdate(onUpdate)
+    .onComplete(() => {
+      cancelAnimationFrame(animaId as number);
+      cb && cb();
+    })
+    .easing(options.easing || Easing.Quintic.Out)
+    .start();
+
+  function animate(time?: number) {
+    animaId = requestAnimationFrame(animate);
+    tween.update(time);
+  }
+
+  animate();
+
+  return tween;
+}