chenlei 1 سال پیش
کامیت
e5907a5441
47فایلهای تغییر یافته به همراه12805 افزوده شده و 0 حذف شده
  1. 12 0
      .editorconfig
  2. 8 0
      .eslintrc.js
  3. 6 0
      .gitignore
  4. 3 0
      .vscode/settings.json
  5. 10 0
      babel.config.js
  6. 9 0
      config/dev.js
  7. 70 0
      config/index.js
  8. 37 0
      config/prod.js
  9. 18 0
      global.d.ts
  10. 73 0
      package.json
  11. 13 0
      project.config.json
  12. 13 0
      project.tt.json
  13. 9 0
      src/app.config.ts
  14. 0 0
      src/app.scss
  15. 21 0
      src/app.tsx
  16. 20 0
      src/index.html
  17. 407 0
      src/models/RenderModel.ts
  18. 126 0
      src/models/ReticleModel.ts
  19. 100 0
      src/models/ShadowModel.ts
  20. 120 0
      src/models/TagModel.ts
  21. 67 0
      src/models/TextureAnimator.ts
  22. 5 0
      src/models/index.ts
  23. 3 0
      src/pages/tnd/index.config.ts
  24. 19 0
      src/pages/tnd/index.scss
  25. 533 0
      src/pages/tnd/index.tsx
  26. BIN
      src/pages/tnd/resource/biaoqian_left.png
  27. BIN
      src/pages/tnd/resource/biaoqian_right.png
  28. BIN
      src/pages/tnd/resource/daoyanguan.png
  29. BIN
      src/pages/tnd/resource/denggai.png
  30. BIN
      src/pages/tnd/resource/dengguan_D.jpg
  31. BIN
      src/pages/tnd/resource/dengguan_D.png
  32. BIN
      src/pages/tnd/resource/dengguan_R.jpg
  33. BIN
      src/pages/tnd/resource/dengguan_S.jpg
  34. BIN
      src/pages/tnd/resource/dengzhao_D.jpg
  35. BIN
      src/pages/tnd/resource/dengzhao_D.png
  36. BIN
      src/pages/tnd/resource/dengzhao_R.jpg
  37. BIN
      src/pages/tnd/resource/dengzhao_S.jpg
  38. BIN
      src/pages/tnd/resource/model.FBX
  39. BIN
      src/pages/tnd/resource/model_t.FBX
  40. BIN
      src/pages/tnd/resource/reticle-animation.png
  41. BIN
      src/pages/tnd/resource/shadow.png
  42. BIN
      src/pages/tnd/resource/tongniudengdizuo_D.jpg
  43. BIN
      src/pages/tnd/resource/tongniudengdizuo_D.png
  44. BIN
      src/pages/tnd/resource/tongniudengdizuo_R.jpg
  45. BIN
      src/pages/tnd/resource/tongniudengdizuo_S.jpg
  46. 25 0
      tsconfig.json
  47. 11078 0
      yarn.lock

+ 12 - 0
.editorconfig

@@ -0,0 +1,12 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false

+ 8 - 0
.eslintrc.js

@@ -0,0 +1,8 @@
+module.exports = {
+  extends: ["taro/react"],
+  rules: {
+    "react/jsx-uses-react": "off",
+    "react/react-in-jsx-scope": "off",
+    "jsx-quotes": false,
+  },
+};

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+dist/
+deploy_versions/
+.temp/
+.rn_temp/
+node_modules/
+.DS_Store

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+  "editor.defaultFormatter": "esbenp.prettier-vscode"
+}

+ 10 - 0
babel.config.js

@@ -0,0 +1,10 @@
+// babel-preset-taro 更多选项和默认值:
+// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
+module.exports = {
+  presets: [
+    ['taro', {
+      framework: 'react',
+      ts: true
+    }]
+  ]
+}

+ 9 - 0
config/dev.js

@@ -0,0 +1,9 @@
+module.exports = {
+  env: {
+    NODE_ENV: '"development"'
+  },
+  defineConstants: {
+  },
+  mini: {},
+  h5: {}
+}

+ 70 - 0
config/index.js

@@ -0,0 +1,70 @@
+const config = {
+  projectName: 'modelDemo',
+  date: '2024-2-22',
+  designWidth: 750,
+  deviceRatio: {
+    640: 2.34 / 2,
+    750: 1,
+    828: 1.81 / 2
+  },
+  sourceRoot: 'src',
+  outputRoot: 'dist',
+  plugins: [],
+  defineConstants: {
+  },
+  copy: {
+    patterns: [
+    ],
+    options: {
+    }
+  },
+  framework: 'react',
+  mini: {
+    postcss: {
+      pxtransform: {
+        enable: true,
+        config: {
+
+        }
+      },
+      url: {
+        enable: true,
+        config: {
+          limit: 1024 // 设定转换尺寸上限
+        }
+      },
+      cssModules: {
+        enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+        config: {
+          namingPattern: 'module', // 转换模式,取值为 global/module
+          generateScopedName: '[name]__[local]___[hash:base64:5]'
+        }
+      }
+    }
+  },
+  h5: {
+    publicPath: '/',
+    staticDirectory: 'static',
+    postcss: {
+      autoprefixer: {
+        enable: true,
+        config: {
+        }
+      },
+      cssModules: {
+        enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+        config: {
+          namingPattern: 'module', // 转换模式,取值为 global/module
+          generateScopedName: '[name]__[local]___[hash:base64:5]'
+        }
+      }
+    }
+  }
+}
+
+module.exports = function (merge) {
+  if (process.env.NODE_ENV === 'development') {
+    return merge({}, config, require('./dev'))
+  }
+  return merge({}, config, require('./prod'))
+}

+ 37 - 0
config/prod.js

@@ -0,0 +1,37 @@
+module.exports = {
+  env: {
+    NODE_ENV: '"production"'
+  },
+  defineConstants: {
+  },
+  mini: {},
+  h5: {
+    /**
+     * WebpackChain 插件配置
+     * @docs https://github.com/neutrinojs/webpack-chain
+     */
+    // webpackChain (chain) {
+    //   /**
+    //    * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
+    //    * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
+    //    */
+    //   chain.plugin('analyzer')
+    //     .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
+
+    //   /**
+    //    * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
+    //    * @docs https://github.com/chrisvfritz/prerender-spa-plugin
+    //    */
+    //   const path = require('path')
+    //   const Prerender = require('prerender-spa-plugin')
+    //   const staticDir = path.join(__dirname, '..', 'dist')
+    //   chain
+    //     .plugin('prerender')
+    //     .use(new Prerender({
+    //       staticDir,
+    //       routes: [ '/pages/index/index' ],
+    //       postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
+    //     }))
+    // }
+  }
+}

+ 18 - 0
global.d.ts

@@ -0,0 +1,18 @@
+/// <reference types="@tarojs/taro" />
+
+declare module '*.png';
+declare module '*.gif';
+declare module '*.jpg';
+declare module '*.jpeg';
+declare module '*.svg';
+declare module '*.css';
+declare module '*.less';
+declare module '*.scss';
+declare module '*.sass';
+declare module '*.styl';
+
+declare namespace NodeJS {
+  interface ProcessEnv {
+    TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'
+  }
+}

+ 73 - 0
package.json

@@ -0,0 +1,73 @@
+{
+  "name": "model-demo",
+  "version": "1.0.0",
+  "private": true,
+  "description": "three.js模型操作示例",
+  "templateInfo": {
+    "name": "redux",
+    "typescript": true,
+    "css": "sass"
+  },
+  "scripts": {
+    "build:weapp": "taro build --type weapp",
+    "build:swan": "taro build --type swan",
+    "build:alipay": "taro build --type alipay",
+    "build:tt": "taro build --type tt",
+    "build:h5": "taro build --type h5",
+    "build:rn": "taro build --type rn",
+    "build:qq": "taro build --type qq",
+    "build:jd": "taro build --type jd",
+    "build:quickapp": "taro build --type quickapp",
+    "dev:weapp": "npm run build:weapp -- --watch",
+    "dev:swan": "npm run build:swan -- --watch",
+    "dev:alipay": "npm run build:alipay -- --watch",
+    "dev:tt": "npm run build:tt -- --watch",
+    "dev:h5": "npm run build:h5 -- --watch",
+    "dev:rn": "npm run build:rn -- --watch",
+    "dev:qq": "npm run build:qq -- --watch",
+    "dev:jd": "npm run build:jd -- --watch",
+    "dev:quickapp": "npm run build:quickapp -- --watch"
+  },
+  "browserslist": [
+    "last 3 versions",
+    "Android >= 4.1",
+    "ios >= 8"
+  ],
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "@babel/runtime": "^7.7.7",
+    "@tarojs/components": "3.4.9",
+    "@tarojs/plugin-framework-react": "3.4.9",
+    "@tarojs/react": "3.4.9",
+    "@tarojs/runtime": "3.4.9",
+    "@tarojs/taro": "3.4.9",
+    "@tweenjs/tween.js": "^23.1.1",
+    "mobx": "^6.12.0",
+    "mobx-react": "^9.1.0",
+    "react": "^17.0.0",
+    "react-dom": "^17.0.0",
+    "three-platformize": "^1.133.3"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.8.0",
+    "@tarojs/mini-runner": "3.4.9",
+    "@tarojs/webpack-runner": "3.4.9",
+    "@types/react": "^17.0.2",
+    "@types/webpack-env": "^1.13.6",
+    "@typescript-eslint/eslint-plugin": "^5.20.0",
+    "@typescript-eslint/parser": "^5.20.0",
+    "babel-preset-taro": "3.4.9",
+    "eslint": "^8.12.0",
+    "eslint-config-taro": "3.4.9",
+    "eslint-plugin-import": "^2.12.0",
+    "eslint-plugin-react": "^7.8.2",
+    "eslint-plugin-react-hooks": "^4.2.0",
+    "stylelint": "9.3.0",
+    "typescript": "^4.1.0"
+  },
+  "resolutions": {
+    "@types/react": "^17.0.2",
+    "@types/sass": "1.43.1"
+  }
+}

+ 13 - 0
project.config.json

@@ -0,0 +1,13 @@
+{
+  "miniprogramRoot": "./dist",
+  "projectname": "modelDemo",
+  "description": "three.js模型操作示例",
+  "appid": "touristappid",
+  "setting": {
+    "urlCheck": true,
+    "es6": false,
+    "postcss": false,
+    "minified": false
+  },
+  "compileType": "miniprogram"
+}

+ 13 - 0
project.tt.json

@@ -0,0 +1,13 @@
+{
+  "miniprogramRoot": "./",
+  "projectname": "modelDemo",
+  "description": "three.js模型操作示例",
+  "appid": "touristappid",
+  "setting": {
+    "urlCheck": true,
+    "es6": false,
+    "postcss": false,
+    "minified": false
+  },
+  "compileType": "miniprogram"
+}

+ 9 - 0
src/app.config.ts

@@ -0,0 +1,9 @@
+export default defineAppConfig({
+  pages: ["pages/tnd/index"],
+  window: {
+    backgroundTextStyle: "light",
+    navigationBarBackgroundColor: "#fff",
+    navigationBarTitleText: "WeChat",
+    navigationBarTextStyle: "black",
+  },
+});

+ 0 - 0
src/app.scss


+ 21 - 0
src/app.tsx

@@ -0,0 +1,21 @@
+import { Component } from "react";
+
+import "./app.scss";
+
+class App extends Component {
+  componentDidMount() {}
+
+  componentDidShow() {}
+
+  componentDidHide() {}
+
+  componentDidCatchError() {}
+
+  // 在 App 类中的 render() 函数没有实际作用
+  // 请勿修改此函数
+  render() {
+    return this.props.children;
+  }
+}
+
+export default App;

+ 20 - 0
src/index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
+  <meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-touch-fullscreen" content="yes">
+  <meta name="format-detection" content="telephone=no,address=no">
+  <meta name="apple-mobile-web-app-status-bar-style" content="white">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
+  <title></title>
+  <script>
+    /* 以下为 H5 响应式脚本,请不要删除! */
+    !function(x){function w(){var v,u,t,tes,s=x.document,r=s.documentElement,a=r.getBoundingClientRect().width;if(!v&&!u){var n=!!x.navigator.appVersion.match(/AppleWebKit.*Mobile.*/);v=x.devicePixelRatio;tes=x.devicePixelRatio;v=n?v:1,u=1/v}if(a>=640){r.style.fontSize="40px"}else{if(a<=320){r.style.fontSize="20px"}else{r.style.fontSize=a/320*20+"px"}}}x.addEventListener("resize",function(){w()});w()}(window);
+  </script>
+</head>
+<body>
+  <div id="app"></div>
+</body>
+</html>

+ 407 - 0
src/models/RenderModel.ts

@@ -0,0 +1,407 @@
+import { CanvasProps } from "@tarojs/components";
+import Taro from "@tarojs/taro";
+import { ComponentType } from "react";
+import {
+  $cancelAnimationFrame,
+  $requestAnimationFrame,
+  $window,
+  AmbientLight,
+  Camera,
+  Color,
+  DirectionalLight,
+  Group,
+  LoadingManager,
+  MeshStandardMaterial,
+  PLATFORM,
+  PerspectiveCamera,
+  Scene,
+  TextureLoader,
+  Vector3,
+  WebGL1Renderer,
+} from "three-platformize";
+import { FBXLoader } from "three-platformize/examples/jsm/loaders/FBXLoader";
+import { BrowserPlatform } from "three-platformize/src/BrowserPlatform";
+import { OrbitControls } from "three-platformize/examples/jsm/controls/OrbitControls";
+import { observable } from "mobx";
+
+export type FileLoaderMap = {
+  fbx: FBXLoader;
+};
+
+export enum ROTATE_TYPE {
+  DELAY,
+  FALSE,
+  TRUE,
+}
+
+export class RenderModel {
+  canvas: ComponentType<CanvasProps>;
+  platform: BrowserPlatform;
+  disposing = false;
+
+  @observable
+  loading = false;
+
+  frameId = 0;
+  /**
+   * 文件加载器类型
+   */
+  fileLoaderMap: FileLoaderMap;
+  /**
+   * 加载进度监听
+   */
+  loadingManager: LoadingManager;
+
+  /**
+   * 模型
+   */
+  @observable
+  model?: Group;
+
+  /**
+   * 场景
+   */
+  scene: Scene;
+  /**
+   * 相机
+   */
+  camera: Camera;
+  /**
+   * 渲染器
+   */
+  renderer: WebGL1Renderer;
+  /**
+   * 控制器
+   */
+  controls: OrbitControls;
+  /**
+   * 模型材质列表
+   * TODO: 类型后续完善
+   */
+  modelMaterialList: any[] = [];
+
+  /**
+   * 画布属性
+   */
+  @observable
+  rect?: Taro.NodesRef.BoundingClientRectCallbackResult;
+
+  /**
+   * 自动旋转
+   */
+  @observable
+  autoRotate = false;
+
+  constructor() {
+    this.loadingManager = new LoadingManager();
+
+    this.fileLoaderMap = {
+      fbx: new FBXLoader(this.loadingManager),
+    };
+    this.scene = new Scene();
+  }
+
+  async init(selector: string) {
+    await this.initWebGLPlatform(selector);
+    this.initScene();
+    this.initCamera();
+    this.initControls();
+    this.initRender();
+    this.initLight();
+  }
+
+  /**
+   * 平台初始化
+   */
+  initWebGLPlatform = (selector: string) => {
+    return new Promise((resolve) => {
+      Taro.createSelectorQuery()
+        .select(selector)
+        .node(async (res) => {
+          const _canvas = res.node as ComponentType<CanvasProps>;
+
+          this.platform = new BrowserPlatform();
+          PLATFORM.set(this.platform);
+
+          this.canvas = _canvas;
+        })
+        .select(selector)
+        .boundingClientRect((rect) => {
+          this.rect = rect;
+        })
+        .exec(() => {
+          resolve(true);
+        });
+    });
+  };
+
+  /**
+   * 创建相机
+   */
+  initCamera() {
+    if (!this.rect) return;
+
+    this.camera = new PerspectiveCamera(
+      25,
+      this.rect.width / this.rect.height,
+      1,
+      1e4
+    );
+  }
+
+  /**
+   * 创建渲染器
+   */
+  initRender() {
+    if (!this.rect) return;
+
+    this.renderer = new WebGL1Renderer({
+      // @ts-ignore
+      canvas: this.canvas,
+      antialias: true,
+      alpha: true,
+      preserveDrawingBuffer: true,
+    });
+    this.renderer.setSize(this.rect.width, this.rect.height);
+    this.renderer.setPixelRatio($window.devicePixelRatio);
+
+    const render = () => {
+      if (!this.disposing) this.frameId = $requestAnimationFrame(render);
+      this.controls.update();
+      this.renderer.render(this.scene, this.camera);
+    };
+    render();
+  }
+
+  /**
+   * 创建场景
+   */
+  initScene() {
+    this.scene = new Scene();
+    this.scene.add(new AmbientLight(0xffffff, 1.0));
+    this.scene.add(new DirectionalLight(0xffffff, 1.0));
+    this.scene.background = new Color(0x462422);
+  }
+
+  /**
+   * 创建控制器
+   */
+  initControls() {
+    // @ts-ignore
+    this.controls = new OrbitControls(this.camera, this.canvas);
+  }
+
+  /**
+   * 初始化灯光
+   */
+  initLight() {
+    // 环境光
+    const ambientLight = new AmbientLight(4473924, 1);
+    // 方向光
+    const directionalLight = new DirectionalLight(16777215, 0.5);
+    const directionalLight2 = new DirectionalLight(16777215, 0.5);
+
+    this.scene.add(ambientLight);
+    directionalLight.position.set(-50, 25, 62.5);
+    directionalLight2.position.set(50, 25, -62.5);
+    this.scene.add(directionalLight);
+    this.scene.add(directionalLight2);
+  }
+
+  /**
+   * 加载模型
+   */
+  loadModel(params: { filePath: string; fileType: keyof FileLoaderMap }) {
+    return new Promise((resolve, reject) => {
+      try {
+        this.loading = true;
+
+        const loader = this.fileLoaderMap[params.fileType];
+
+        loader.load(params.filePath, (res) => {
+          switch (params.fileType) {
+            case "fbx":
+              this.model = res;
+              this.scene.add(res);
+              break;
+          }
+
+          this.getModelMaterialList();
+
+          resolve(res);
+        });
+      } finally {
+        this.loading = false;
+      }
+    });
+  }
+
+  /**
+   * 获取当前模型材质
+   */
+  getModelMaterialList() {
+    let i = 0;
+    this.model?.traverse((v: any) => {
+      // TODO: 小程序待确认
+      if (v.isMesh && v.material) {
+        v.castShadow = true;
+        v.frustumCulled = false;
+
+        if (Array.isArray(v.material)) {
+          v.material = v.material[0];
+          this.setMaterialMeshParams(v, i);
+        } else {
+          this.setMaterialMeshParams(v, i);
+        }
+      }
+    });
+  }
+
+  /**
+   * 材质参数二次处理
+   */
+  setMaterialMeshParams(v: any, i: number) {
+    const newMesh = v.clone();
+    Object.assign(v.userData, {
+      rotation: newMesh.rotation,
+      scale: newMesh.scale,
+      position: newMesh.position,
+    });
+
+    const newMaterial = v.material.clone();
+    v.mapId = v.name + "_" + i;
+    v.material = newMaterial;
+    const { mapId, uuid, userData, type, name, isMesh, visible } = v;
+    const { color, wireframe, depthWrite, opacity } = v.material;
+    const meshMaterial = { color, wireframe, depthWrite, opacity };
+    const mesh = {
+      mapId,
+      uuid,
+      userData,
+      type,
+      name,
+      isMesh,
+      visible,
+      material: meshMaterial,
+      position: v.position.clone(),
+    };
+    this.modelMaterialList.push(mesh);
+  }
+
+  /**
+   * 设置贴图
+   * @param meshStandardMaterial
+   * @param url 贴图地址
+   */
+  setModelMap(meshStandardMaterial: MeshStandardMaterial, url: string) {
+    const texture = new TextureLoader().load(url);
+    meshStandardMaterial.map = texture;
+    meshStandardMaterial.needsUpdate = true;
+    texture.dispose();
+  }
+
+  /**
+   * 设置金属度
+   * @param meshStandardMaterial
+   * @param url 贴图地址
+   */
+  setModelMetalness(meshStandardMaterial: MeshStandardMaterial, url: string) {
+    const texture = new TextureLoader().load(url);
+    meshStandardMaterial.metalnessMap = texture;
+    meshStandardMaterial.needsUpdate = true;
+    texture.dispose();
+  }
+
+  /**
+   * 设置表面粗糙度
+   * @param meshStandardMaterial
+   * @param url 贴图地址
+   */
+  setModelRoughness(meshStandardMaterial: MeshStandardMaterial, url: string) {
+    const texture = new TextureLoader().load(url);
+    meshStandardMaterial.roughnessMap = texture;
+    meshStandardMaterial.needsUpdate = true;
+    texture.dispose();
+  }
+
+  /**
+   * 设置发光贴图
+   * @param meshStandardMaterial
+   * @param url 贴图地址
+   */
+  setModelEmissive(meshStandardMaterial: MeshStandardMaterial, url: string) {
+    const texture = new TextureLoader().load(url);
+    meshStandardMaterial.emissiveMap = texture;
+    meshStandardMaterial.needsUpdate = true;
+    texture.dispose();
+  }
+
+  /**
+   * 设置自动旋转
+   * @param type
+   */
+  setAutoRotate(type: ROTATE_TYPE) {
+    switch (type) {
+      case ROTATE_TYPE.DELAY:
+        this.controls.autoRotate = false;
+
+        if (this.autoRotate) {
+          setTimeout(() => {
+            this.controls.autoRotate = true;
+          }, 10000);
+        }
+        break;
+      case ROTATE_TYPE.FALSE:
+        this.autoRotate = false;
+        this.controls.autoRotate = false;
+        break;
+      default:
+        this.autoRotate = true;
+        this.controls.autoRotate = true;
+    }
+  }
+
+  /**
+   * 设置控制器状态
+   * @param enableZoom 是否可以放大
+   * @param enableRotate 是否可以旋转
+   * @param enablePan 是否可以平移
+   */
+  setControlsStatus(
+    enableZoom: boolean,
+    enableRotate: boolean,
+    enablePan: boolean,
+    enableRotateX?: boolean,
+    enableRotateY?: boolean
+  ) {
+    this.controls.enablePan = enablePan;
+    this.controls.enableZoom = enableZoom;
+    this.controls.enableRotate = enableRotate;
+
+    if (typeof enableRotateX === "boolean" && !enableRotateX) {
+      const angle = this.controls.getAzimuthalAngle();
+      this.controls.maxAzimuthAngle = angle;
+      this.controls.minAzimuthAngle = angle;
+    } else if (enableRotate) {
+      this.controls.maxAzimuthAngle = Infinity;
+      this.controls.minAzimuthAngle = -Infinity;
+    }
+
+    if (typeof enableRotateY === "boolean" && !enableRotateY) {
+      const angle = this.controls.getPolarAngle();
+      this.controls.minPolarAngle = angle;
+      this.controls.maxPolarAngle = angle;
+    } else if (enableRotate) {
+      this.controls.minPolarAngle = 0;
+      this.controls.maxPolarAngle = Math.PI / 2;
+    }
+  }
+
+  dispose() {
+    this.disposing = true;
+    $cancelAnimationFrame(this.frameId);
+    this.platform?.dispose();
+  }
+}
+
+export const renderModel = new RenderModel();

+ 126 - 0
src/models/ReticleModel.ts

@@ -0,0 +1,126 @@
+import {
+  Camera,
+  DoubleSide,
+  LinearFilter,
+  Material,
+  Mesh,
+  MeshBasicMaterial,
+  PlaneGeometry,
+  TextureLoader,
+  Vector3,
+  sRGBEncoding,
+} from "three-platformize";
+import { TextureAnimator, TextureAnimatorParams } from "./TextureAnimator";
+
+export interface ReticleModelParams
+  extends Omit<TextureAnimatorParams, "texture" | "numberOfTiles"> {
+  name?: string;
+  url: string;
+  planeWidth: number;
+  scale: number;
+  position: Vector3;
+  camera: Camera;
+}
+
+/**
+ * 创建光照焦点
+ */
+export class ReticleModel {
+  name = "";
+
+  textureAnimator: TextureAnimator;
+  reticleAnim: Mesh;
+  camera: Camera;
+
+  constructor(params: ReticleModelParams) {
+    const {
+      name,
+      url,
+      planeWidth,
+      scale,
+      camera,
+      position,
+      ...textureAnimatorParams
+    } = params;
+    const texture = new TextureLoader().load(url);
+
+    texture.magFilter = LinearFilter;
+    texture.minFilter = LinearFilter;
+    texture.encoding = sRGBEncoding;
+
+    this.textureAnimator = new TextureAnimator({
+      ...textureAnimatorParams,
+      texture,
+      numberOfTiles: Math.max(
+        textureAnimatorParams.tilesHorizontal,
+        textureAnimatorParams.tilesVertical
+      ),
+    });
+
+    const planeGeometry = new PlaneGeometry(planeWidth, planeWidth, 1, 1);
+    const meshBasicMaterial = new MeshBasicMaterial({
+      map: texture,
+      side: DoubleSide,
+      transparent: true,
+      opacity: 1,
+      depthTest: false,
+    });
+
+    this.reticleAnim = new Mesh(planeGeometry, meshBasicMaterial);
+    this.reticleAnim.lookAt(camera.position);
+    this.reticleAnim.name = name ?? "";
+    this.reticleAnim.userData.depthlevel = 1;
+    this.reticleAnim.scale.copy(new Vector3(scale, scale, scale));
+    this.reticleAnim.visible = true;
+    this.reticleAnim.position.copy(position);
+    this.camera = camera;
+  }
+
+  private hideTime: NodeJS.Timeout;
+  private showTime: NodeJS.Timeout;
+
+  show() {
+    this.reticleAnim.visible = true;
+
+    this.clearTime();
+
+    this.showTime = setInterval(() => {
+      const material = this.reticleAnim.material as Material;
+      material.opacity += 0.02;
+
+      if (material.opacity >= 1) {
+        material.opacity = 1;
+        clearInterval(this.showTime);
+      }
+    }, 20);
+  }
+
+  hide() {
+    this.clearTime();
+
+    this.hideTime = setInterval(() => {
+      const material = this.reticleAnim.material as Material;
+      material.opacity -= 0.05;
+
+      if (material.opacity <= 0) {
+        material.opacity = 0;
+        this.reticleAnim.visible = false;
+        clearInterval(this.hideTime);
+      }
+    }, 20);
+  }
+
+  /**
+   * 更新
+   * @param time 秒
+   */
+  update(time: number) {
+    this.reticleAnim.lookAt(this.camera.position);
+    this.textureAnimator.update(time * 2000);
+  }
+
+  clearTime() {
+    this.hideTime && clearInterval(this.hideTime);
+    this.showTime && clearInterval(this.showTime);
+  }
+}

+ 100 - 0
src/models/ShadowModel.ts

@@ -0,0 +1,100 @@
+import {
+  DoubleSide,
+  Group,
+  LinearFilter,
+  Material,
+  Mesh,
+  MeshBasicMaterial,
+  PlaneBufferGeometry,
+  TextureLoader,
+  Vector3,
+  sRGBEncoding,
+} from "three-platformize";
+
+export class ShadowModel {
+  mesh: Mesh;
+
+  private showTime: NodeJS.Timeout;
+  private hideTime: NodeJS.Timeout;
+
+  constructor(
+    url: string,
+    width: number,
+    height: number,
+    rotation: { x: number; y: number; z: number },
+    position: Vector3,
+    scale: number,
+    model: Group
+  ) {
+    const texture = new TextureLoader().load(url);
+    const planeBufferGeometry = new PlaneBufferGeometry(width, height);
+
+    texture.magFilter = LinearFilter;
+    texture.minFilter = LinearFilter;
+    texture.encoding = sRGBEncoding;
+    texture.repeat.set(1, 1);
+
+    const meshBasicMaterial = new MeshBasicMaterial({
+      color: 16777215,
+      map: texture,
+      transparent: true,
+      side: DoubleSide,
+      opacity: 1,
+    });
+
+    this.mesh = new Mesh(planeBufferGeometry, meshBasicMaterial);
+    this.mesh.rotation.set(rotation.x, rotation.y, rotation.z);
+    this.mesh.position.copy(position);
+    this.mesh.scale.set(scale, scale, scale);
+
+    model.add(this.mesh);
+    this.show();
+  }
+
+  show(cb?: Function) {
+    this.mesh.visible = true;
+    // @ts-ignore
+    this.mesh.material.map.offset.y += 0.01;
+
+    this.clearTimer();
+
+    this.showTime = setInterval(() => {
+      const material = this.mesh.material as Material;
+      material.opacity += 0.02;
+
+      if (material.opacity >= 1) {
+        material.opacity = 1;
+        clearInterval(this.showTime);
+
+        cb && cb();
+      }
+    }, 20);
+  }
+
+  hide(cb?: Function) {
+    this.clearTimer();
+
+    this.hideTime = setInterval(() => {
+      const material = this.mesh.material as Material;
+      material.opacity -= 0.05;
+
+      if (material.opacity <= 0) {
+        clearInterval(this.hideTime);
+        material.opacity = 0;
+        this.mesh.visible = false;
+
+        cb && cb();
+      }
+    });
+  }
+
+  clearTimer() {
+    if (this.hideTime) {
+      clearInterval(this.hideTime);
+    }
+
+    if (this.showTime) {
+      clearInterval(this.showTime);
+    }
+  }
+}

+ 120 - 0
src/models/TagModel.ts

@@ -0,0 +1,120 @@
+import {
+  DoubleSide,
+  Mesh,
+  PlaneGeometry,
+  ShaderMaterial,
+  TextureLoader,
+  Vector3,
+} from "three-platformize";
+
+export interface TagModelParams {
+  url: string;
+  maskUrl: string;
+  width: number;
+  height: number;
+  position: Vector3;
+  scale: number;
+  rotation: { x: number; y: number; z: number };
+}
+
+export class TagModel {
+  private showTime: NodeJS.Timeout;
+  private hideTime: NodeJS.Timeout;
+
+  mesh: Mesh;
+  progress = 0;
+
+  constructor(params: TagModelParams) {
+    const planeGeometry = new PlaneGeometry(
+      params.width,
+      params.height,
+      20,
+      20
+    );
+    const shaderMaterial = new ShaderMaterial({
+      uniforms: {
+        uTexture: { value: new TextureLoader().load(params.url) },
+        uMaskTexture: { value: new TextureLoader().load(params.maskUrl) },
+        uProgress: { value: 0 },
+      },
+      transparent: true,
+      vertexShader: `
+        varying vec2 vUv;
+        void main() {
+          vUv = uv;
+          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+        }
+      `,
+      fragmentShader: `
+        varying vec2 vUv;
+        uniform sampler2D uTexture;
+        uniform sampler2D uMaskTexture;
+        uniform float uProgress;
+        void main() {
+          vec4 tex = texture2D(uTexture, vUv);
+          vec4 maskTex = texture2D(uMaskTexture, vUv);
+          float alpha = clamp(maskTex.r - (1.0 - uProgress), 0.0, 1.0);
+          alpha = smoothstep(0.0, 0.0001, alpha);
+          vec4 color = tex;
+          color.a *= alpha;
+          gl_FragColor = color;
+        }
+      `,
+      side: DoubleSide,
+      opacity: 1,
+    });
+
+    this.mesh = new Mesh(planeGeometry, shaderMaterial);
+    this.mesh.rotation.set(
+      params.rotation.x,
+      params.rotation.y,
+      params.rotation.z
+    );
+    this.mesh.position.copy(params.position);
+    this.mesh.scale.set(params.scale, params.scale, params.scale);
+
+    this.show();
+  }
+
+  show() {
+    this.clearTimer();
+
+    this.showTime = setInterval(() => {
+      const material = this.mesh.material as any;
+
+      this.progress += 0.05;
+      material.uniforms.uProgress.value = this.progress;
+
+      if (this.progress >= 1) {
+        this.progress = 1;
+        clearInterval(this.showTime);
+      }
+    }, 20);
+  }
+
+  hide() {
+    this.clearTimer();
+
+    this.hideTime = setInterval(() => {
+      const material = this.mesh.material as any;
+
+      this.progress -= 0.05;
+      material.uniforms.uProgress.value = this.progress;
+
+      if (this.progress <= 0) {
+        this.progress = 0;
+        clearInterval(this.hideTime);
+      }
+    }, 20);
+  }
+
+  clearTimer() {
+    if (this.hideTime) {
+      clearInterval(this.hideTime);
+    }
+
+    if (this.showTime) {
+      clearInterval(this.showTime);
+    }
+  }
+}

+ 67 - 0
src/models/TextureAnimator.ts

@@ -0,0 +1,67 @@
+import { RepeatWrapping, Texture } from "three-platformize";
+
+export interface TextureAnimatorParams {
+  texture: Texture;
+  /**
+   * 纹理集水平方向上的纹理数量
+   */
+  tilesHorizontal: number;
+  /**
+   * 纹理集垂直方向上的纹理数量
+   */
+  tilesVertical: number;
+  /**
+   * 总纹理数量
+   */
+  numberOfTiles: number;
+  /**
+   * 每个纹理在动画中显示的时间
+   */
+  tileDisplayDuration: number;
+}
+
+/**
+ * 纹理动画
+ */
+export class TextureAnimator {
+  texture: Texture;
+  tilesHorizontal: number;
+  tilesVertical: number;
+  numberOfTiles: number;
+  tileDisplayDuration: number;
+
+  currentDisplayTime = 0;
+  currentTile = 0;
+
+  constructor(params: TextureAnimatorParams) {
+    this.texture = params.texture;
+    this.tilesHorizontal = params.tilesHorizontal;
+    this.tilesVertical = params.tilesVertical;
+    this.numberOfTiles = params.numberOfTiles;
+    this.tileDisplayDuration = params.tileDisplayDuration;
+
+    // 设置纹理在水平和垂直方向的包裹方式为重复
+    this.texture.wrapS = this.texture.wrapT = RepeatWrapping;
+    // 设置纹理的重复次数
+    this.texture.repeat.set(1 / this.tilesHorizontal, 1 / this.tilesVertical);
+  }
+
+  update(time: number) {
+    this.currentDisplayTime += time;
+
+    while (this.currentDisplayTime > this.tileDisplayDuration) {
+      this.currentDisplayTime -= this.tileDisplayDuration;
+      this.currentTile++;
+
+      if (this.currentTile === this.numberOfTiles) {
+        this.currentTile = 0;
+      }
+
+      const t = this.currentTile % this.tilesHorizontal;
+      this.texture.offset.x = t / this.tilesHorizontal;
+
+      const n = Math.floor(this.currentTile / this.tilesHorizontal);
+      this.texture.offset.y = n / this.tilesVertical;
+    }
+  }
+}

+ 5 - 0
src/models/index.ts

@@ -0,0 +1,5 @@
+export * from "./RenderModel";
+export * from "./ShadowModel";
+export * from "./ReticleModel";
+export * from "./TextureAnimator";
+export * from "./TagModel";

+ 3 - 0
src/pages/tnd/index.config.ts

@@ -0,0 +1,3 @@
+export default definePageConfig({
+  navigationBarTitleText: '首页'
+})

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

@@ -0,0 +1,19 @@
+.taro_page {
+  overflow: hidden;
+}
+
+#wgl {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 1;
+}
+
+.toolbar {
+  position: fixed;
+  top: 20px;
+  right: 20px;
+  z-index: 999;
+}

+ 533 - 0
src/pages/tnd/index.tsx

@@ -0,0 +1,533 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import { Button, Canvas, View } from "@tarojs/components";
+import { FC } from "@tarojs/taro";
+import { observer } from "mobx-react";
+import {
+  ROTATE_TYPE,
+  renderModel,
+  ShadowModel,
+  ReticleModel,
+  TagModelParams,
+  TagModel,
+} from "../../models";
+import {
+  Clock,
+  GammaEncoding,
+  LinearFilter,
+  Mesh,
+  MeshStandardMaterial,
+  PointLight,
+  Vector3,
+} from "three-platformize";
+import { Easing, Tween } from "@tweenjs/tween.js";
+import { cancelAnimationFrame, requestAnimationFrame } from "@tarojs/runtime";
+import "./index.scss";
+
+enum MODEL_STATE {
+  DEFAULT = 0,
+  ZOOM_UP = 1,
+}
+
+interface TweenHandlerOptions<T extends object> {
+  /** 当前属性 */
+  curProps: T;
+  /** 目标属性 */
+  targetProps: T;
+  /** 动画更新回调 */
+  onUpdate?: (e: T) => void;
+  /** 动画完成回调 */
+  cb?: Function;
+  /** 动画时长 默认 1000 ms */
+  delay?: number;
+}
+
+const DENGZHAO_MIN_ROTATE = -1.2;
+const DENGZHAO_MAX_ROTATE = -2.55;
+
+const IndexPage: FC = observer(() => {
+  const clock = useRef(new Clock());
+  const bulbLight = useRef<PointLight>();
+  const cameraPosition = useRef(new Vector3(0, 100, 360));
+  /**
+   * 底部阴影
+   */
+  const shadowPlan = useRef<Mesh>();
+  const [modelState, setModelState] = useState<MODEL_STATE>(
+    MODEL_STATE.DEFAULT
+  );
+  /**
+   * 是否分解
+   */
+  const isSeparate = useMemo(
+    () => modelState === MODEL_STATE.ZOOM_UP,
+    [modelState]
+  );
+  const hotSeparateAnimArr = useRef<ReticleModel[]>([]);
+  const tagArr = useRef<TagModel[]>([]);
+
+  useEffect(() => {
+    setTimeout(async () => {
+      await init();
+
+      animate();
+    }, 100);
+
+    return renderModel.dispose;
+  }, []);
+
+  const init = async () => {
+    await renderModel.init("#wgl");
+
+    // 创建点光源
+    bulbLight.current = new PointLight(16772744, 2.5, 50, 2);
+    bulbLight.current.position.set(1.45, -2.95, -1.17);
+    bulbLight.current.castShadow = true;
+    bulbLight.current.visible = false;
+
+    renderModel.scene.add(bulbLight.current);
+    renderModel.setControlsStatus(true, true, false);
+    handleControlsAngle(true);
+
+    renderModel.camera.position.copy(cameraPosition.current);
+
+    await renderModel.loadModel({
+      filePath:
+        "https://houseoss.4dkankan.com/project/yzdyh-dadu/test/model.FBX",
+      fileType: "fbx",
+    });
+
+    renderModel.modelMaterialList.forEach((material) => {
+      const meshStandardMaterial = new MeshStandardMaterial();
+      // TODO: 暂定 any
+      const mesh: any = renderModel.scene.getObjectByProperty(
+        "uuid",
+        material.uuid
+      );
+
+      // 设置物体是否投射阴影
+      mesh.castShadow = true;
+      // 设置物体是否接收阴影
+      mesh.receiveShadow = true;
+
+      meshStandardMaterial.copy(mesh.material);
+      // 设置材质透明度
+      meshStandardMaterial.opacity = 0.01;
+      // 告诉 three 库该材质渲染为半透明
+      meshStandardMaterial.transparent = true;
+      // 设置材质渲染面,2:双面渲染;1:只渲染正面;0:只渲染背面
+      meshStandardMaterial.side = 2;
+
+      switch (material.name) {
+        case "dengzhao1":
+        case "dengzhao2":
+        case "dengpan":
+          setModelMaterial(meshStandardMaterial, "dengzhao");
+          break;
+        case "dengguan":
+          setModelMaterial(meshStandardMaterial, "dengguan");
+          break;
+        case "tongniudengdizuo":
+          setModelMaterial(meshStandardMaterial, "tongniudengdizuo");
+          break;
+      }
+
+      // 设置材质粗糙度
+      meshStandardMaterial.roughness = 0.9;
+      // 设置材质金属度
+      meshStandardMaterial.metalness = 0.3;
+      // 设置材质纹理过滤器
+      meshStandardMaterial.map!.minFilter = LinearFilter;
+      meshStandardMaterial.map!.magFilter = LinearFilter;
+      // 设置材质纹理编码方式
+      meshStandardMaterial.map!.encoding = GammaEncoding;
+
+      mesh.material.dispose();
+      mesh.material = meshStandardMaterial;
+      mesh.material.needsUpdate = true;
+    });
+
+    setTimeout(() => {
+      shadowPlaneShow();
+      modelChildRotation(5);
+
+      tweenHandler({
+        curProps: {
+          x: 0,
+        },
+        targetProps: {
+          x: 20,
+        },
+        onUpdate: (e) => {
+          renderModel.model?.scale.copy(new Vector3(e.x, e.x, e.x));
+        },
+        cb: () => {
+          setTimeout(() => {
+            renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
+          }, 800);
+        },
+      });
+    }, 300);
+  };
+
+  const setModelMaterial = (
+    meshStandardMaterial: MeshStandardMaterial,
+    name: string
+  ) => {
+    renderModel.setModelMap(
+      meshStandardMaterial,
+      require(`./resource/${name}_D.jpg`)
+    );
+    renderModel.setModelRoughness(
+      meshStandardMaterial,
+      require(`./resource/${name}_S.jpg`)
+    );
+    renderModel.setModelMetalness(
+      meshStandardMaterial,
+      require(`./resource/${name}_R.jpg`)
+    );
+    renderModel.setModelEmissive(
+      meshStandardMaterial,
+      require(`./resource/${name}_D.png`)
+    );
+  };
+
+  /**
+   * 相机动画
+   */
+  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");
+
+    if (dengzhao1Mesh) {
+      dengzhao1Mesh.rotation.z =
+        DENGZHAO_MIN_ROTATE +
+        (DENGZHAO_MAX_ROTATE - DENGZHAO_MIN_ROTATE) * (e - 1) * 0.1;
+
+      renderModel.modelMaterialList.forEach((material) => {
+        // TODO: 暂定 any
+        const mesh: any = renderModel.scene.getObjectByProperty(
+          "uuid",
+          material.uuid
+        );
+
+        if (e > 5) {
+          const t = 1 - 10 * (e - 8) * (1 / 30);
+          if (e >= 8) {
+            mesh.material.opacity = t;
+          }
+
+          mesh.visible = !(t <= 0.01);
+        } else {
+          mesh.material.opacity = 1;
+          mesh.visible = true;
+        }
+      });
+    }
+  };
+
+  /**
+   * 是否展示底部阴影
+   */
+  const shadowPlaneShow = (show = true) => {
+    if (!shadowPlan.current && renderModel.model) {
+      shadowPlan.current = new ShadowModel(
+        "https://houseoss.4dkankan.com/project/yzdyh-dadu/test/shadow.png",
+        1024,
+        1024,
+        { x: Math.PI / 2, y: Math.PI / 80, z: -Math.PI / 2 },
+        new Vector3(0, -2.35, 0),
+        0.0055,
+        renderModel.model
+      ).mesh;
+    }
+    shadowPlan.current!.visible = show;
+  };
+
+  const handleClick = () => {
+    renderModel.setAutoRotate(ROTATE_TYPE.DELAY);
+  };
+
+  /**
+   * 拆解/合并模型
+   */
+  const handleSeparate = () => {
+    handleControlsAngle(isSeparate);
+    renderModel.setAutoRotate(ROTATE_TYPE.FALSE);
+    renderModel.setControlsStatus(false, false, false);
+
+    if (!isSeparate) {
+      handleControlsDistance(320);
+      shadowPlaneShow(false);
+
+      const pos = renderModel.camera.position.clone();
+      const lookAt = renderModel.controls.target.clone();
+      const targetPos = new Vector3(0, 0, 320);
+      const targetLookAt = new Vector3(0, 0, 0);
+
+      tweenHandler({
+        curProps: {
+          x1: pos.x,
+          y1: pos.y,
+          z1: pos.z,
+          x2: lookAt.x,
+          y2: lookAt.y,
+          z2: lookAt.z,
+        },
+        targetProps: {
+          x1: targetPos.x,
+          y1: targetPos.y,
+          z1: targetPos.z,
+          x2: targetLookAt.x,
+          y2: targetLookAt.y,
+          z2: targetLookAt.z,
+        },
+        onUpdate: (e) => {
+          renderModel.camera.position.set(e.x1, e.y1, e.z1);
+          renderModel.controls.target.set(e.x2, e.y2, e.z2);
+          renderModel.controls.update();
+        },
+        cb: () => {
+          handleModelSeparateAnimation(true, () => {
+            renderModel.setControlsStatus(true, true, false, true, false);
+            renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
+            setModelState(MODEL_STATE.ZOOM_UP);
+          });
+        },
+      });
+    } else {
+      handleModelSeparateAnimation(false, () => {
+        revertModel(() => {
+          renderModel.setControlsStatus(true, true, false);
+          renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
+          shadowPlaneShow(true);
+          setModelState(MODEL_STATE.DEFAULT);
+        });
+      });
+    }
+  };
+
+  /**
+   * 恢复模型
+   */
+  const revertModel = (cb?: Function) => {
+    const pos = renderModel.camera.position.clone();
+    const lookAt = renderModel.controls.target.clone();
+    const targetPos = new Vector3(0, 30, 372);
+    const targetLookAt = new Vector3(0, 0, 0);
+
+    tweenHandler({
+      curProps: {
+        x1: pos.x,
+        y1: pos.y,
+        z1: pos.z,
+        x2: lookAt.x,
+        y2: lookAt.y,
+        z2: lookAt.z,
+      },
+      targetProps: {
+        x1: targetPos.x,
+        y1: targetPos.y,
+        z1: targetPos.z,
+        x2: targetLookAt.x,
+        y2: targetLookAt.y,
+        z2: targetLookAt.z,
+      },
+      onUpdate: (e) => {
+        renderModel.camera.position.set(e.x1, e.y1, e.z1);
+        renderModel.controls.target.set(e.x2, e.y2, e.z2);
+        renderModel.controls.update();
+      },
+      cb,
+    });
+  };
+
+  const handleControlsDistance = (dis: number, maxDis = 470) => {
+    renderModel.controls.minDistance = dis;
+    renderModel.controls.maxDistance = maxDis;
+  };
+
+  /**
+   * 修改控制器垂直最大角度
+   */
+  const handleControlsAngle = (type: boolean) => {
+    renderModel.controls.maxPolarAngle = type ? 0.59 * Math.PI : Math.PI;
+  };
+
+  /**
+   * 模型分离动画
+   * @param type 是否分离
+   */
+  const handleModelSeparateAnimation = (type: boolean, cb?: Function) => {
+    let loadCount = 0;
+
+    renderModel.model?.traverse((item) => {
+      // @ts-ignore
+      if (item.isMesh) {
+        if (type) {
+          const pos = item.position.clone();
+          switch (item.name) {
+            case "dengguan":
+              pos.add(new Vector3(0, 1, 0));
+              break;
+            case "dengpan":
+              pos.add(new Vector3(0, -0.5, 0));
+              break;
+            case "dengzhao1":
+              pos.add(new Vector3(-0.3, 0.2, 0));
+              break;
+            case "dengzhao2":
+              pos.add(new Vector3(0.3, 0.2, 0));
+              break;
+            case "tongniudengdizuo":
+              pos.add(new Vector3(0, -1, 0));
+          }
+
+          tweenHandler({
+            curProps: item.position,
+            targetProps: pos,
+            cb: () => {
+              loadCount++;
+
+              if (loadCount === 5) {
+                initTag(true);
+                addReticleMeshs(true);
+                cb && cb();
+              }
+            },
+          });
+        } else {
+          const model = renderModel.modelMaterialList.find(
+            (i) => i.name === item.name
+          );
+
+          if (model) {
+            initTag(false);
+            addReticleMeshs(false);
+            tweenHandler({
+              curProps: item.position,
+              targetProps: model.position,
+              cb,
+            });
+          }
+        }
+      }
+    });
+  };
+
+  const initTag = (visible: boolean) => {
+    if (!tagArr.current.length) {
+      const temp: TagModel[] = [];
+      const tagStack: TagModelParams[] = [
+        {
+          url: require("./resource/daoyanguan.png"),
+          maskUrl: require("./resource/biaoqian_left.png"),
+          width: 256,
+          height: 36,
+          position: new Vector3(1.1, 3.1, 0),
+          scale: 0.007,
+          rotation: { x: 0, y: 0, z: 0 },
+        },
+        {
+          url: require("./resource/denggai.png"),
+          maskUrl: require("./resource/biaoqian_left.png"),
+          width: 256,
+          height: 36,
+          position: new Vector3(1.7, 2.4, 0),
+          scale: 0.005,
+          rotation: { x: 0, y: 0, z: 0 },
+        },
+      ];
+
+      tagStack.forEach((tag) => {
+        const tagIns = new TagModel(tag);
+
+        temp.push(tagIns);
+        renderModel.model?.add(tagIns.mesh);
+      });
+
+      tagArr.current.push(...temp);
+    }
+
+    tagArr.current.forEach((tag) => {
+      visible ? tag.show() : tag.hide();
+    });
+  };
+
+  const addReticleMeshs = (visible: boolean) => {
+    if (!hotSeparateAnimArr.current.length) {
+      const temp: ReticleModel[] = [];
+      const vectorStack = [
+        new Vector3(0, 2.7, 0.65),
+        new Vector3(0, 0.9, 0.65),
+        new Vector3(0, -0.7, 0.65),
+        new Vector3(0, -2, 0.65),
+      ];
+
+      vectorStack.forEach((vector) => {
+        const reticle = new ReticleModel({
+          url: require("./resource/reticle-animation.png"),
+          planeWidth: 150,
+          tilesHorizontal: 50,
+          tilesVertical: 1,
+          tileDisplayDuration: 50,
+          scale: 0.0022,
+          position: vector,
+          camera: renderModel.camera,
+        });
+        renderModel.model?.add(reticle.reticleAnim);
+        temp.push(reticle);
+      });
+
+      hotSeparateAnimArr.current.push(...temp);
+    }
+
+    hotSeparateAnimArr.current.forEach((item) => {
+      visible ? item.show() : item.hide();
+    });
+  };
+
+  const animate = () => {
+    const time = clock.current.getDelta();
+
+    hotSeparateAnimArr.current.forEach((item) => {
+      item.update(time);
+    });
+
+    requestAnimationFrame(animate);
+  };
+
+  return (
+    <View className="page" onClick={handleClick}>
+      <Canvas id="wgl" type="webgl" />
+
+      <View className="toolbar">
+        <Button className="btn" onClick={handleSeparate}>
+          {!isSeparate ? "拆解" : "合并"}
+        </Button>
+      </View>
+    </View>
+  );
+});
+
+export default IndexPage;

BIN
src/pages/tnd/resource/biaoqian_left.png


BIN
src/pages/tnd/resource/biaoqian_right.png


BIN
src/pages/tnd/resource/daoyanguan.png


BIN
src/pages/tnd/resource/denggai.png


BIN
src/pages/tnd/resource/dengguan_D.jpg


BIN
src/pages/tnd/resource/dengguan_D.png


BIN
src/pages/tnd/resource/dengguan_R.jpg


BIN
src/pages/tnd/resource/dengguan_S.jpg


BIN
src/pages/tnd/resource/dengzhao_D.jpg


BIN
src/pages/tnd/resource/dengzhao_D.png


BIN
src/pages/tnd/resource/dengzhao_R.jpg


BIN
src/pages/tnd/resource/dengzhao_S.jpg


BIN
src/pages/tnd/resource/model.FBX


BIN
src/pages/tnd/resource/model_t.FBX


BIN
src/pages/tnd/resource/reticle-animation.png


BIN
src/pages/tnd/resource/shadow.png


BIN
src/pages/tnd/resource/tongniudengdizuo_D.jpg


BIN
src/pages/tnd/resource/tongniudengdizuo_D.png


BIN
src/pages/tnd/resource/tongniudengdizuo_R.jpg


BIN
src/pages/tnd/resource/tongniudengdizuo_S.jpg


+ 25 - 0
tsconfig.json

@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "target": "es2017",
+    "module": "commonjs",
+    "removeComments": false,
+    "preserveConstEnums": true,
+    "moduleResolution": "node",
+    "experimentalDecorators": true,
+    "noImplicitAny": false,
+    "allowSyntheticDefaultImports": true,
+    "outDir": "lib",
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "strictNullChecks": true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "rootDir": ".",
+    "jsx": "react-jsx",
+    "allowJs": true,
+    "resolveJsonModule": true,
+    "typeRoots": ["node_modules/@types"]
+  },
+  "include": ["./src", "global.d.ts"],
+  "compileOnSave": false
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 11078 - 0
yarn.lock