فهرست منبع

feat: 单模型展示

chenlei 1 سال پیش
والد
کامیت
fbb09a4c08
5فایلهای تغییر یافته به همراه244 افزوده شده و 115 حذف شده
  1. 14 0
      src/components/CanvasAdapter/index.h5.tsx
  2. 21 0
      src/components/CanvasAdapter/index.tsx
  3. 1 0
      src/components/index.ts
  4. 21 40
      src/models/RenderModel.ts
  5. 187 75
      src/pages/tnd/index.tsx

+ 14 - 0
src/components/CanvasAdapter/index.h5.tsx

@@ -0,0 +1,14 @@
+import { FC } from "@tarojs/taro";
+import { MouseEventHandler } from "react";
+
+export interface CanvasAdapterProps {
+  onTouchStart: MouseEventHandler<HTMLCanvasElement>;
+  onTouchEnd: MouseEventHandler<HTMLCanvasElement>;
+}
+
+export const CanvasAdapter: FC<CanvasAdapterProps> = ({
+  onTouchEnd,
+  onTouchStart,
+}) => {
+  return <canvas id="wgl" onMouseDown={onTouchStart} onMouseUp={onTouchEnd} />;
+};

+ 21 - 0
src/components/CanvasAdapter/index.tsx

@@ -0,0 +1,21 @@
+import { Canvas, CanvasTouchEventFunction } from "@tarojs/components";
+import { FC } from "@tarojs/taro";
+
+export interface CanvasAdapterProps {
+  onTouchStart: CanvasTouchEventFunction;
+  onTouchEnd: CanvasTouchEventFunction;
+}
+
+export const CanvasAdapter: FC<CanvasAdapterProps> = ({
+  onTouchEnd,
+  onTouchStart,
+}) => {
+  return (
+    <Canvas
+      id="wgl"
+      type="webgl"
+      onTouchStart={onTouchStart}
+      onTouchEnd={onTouchEnd}
+    />
+  );
+};

+ 1 - 0
src/components/index.ts

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

+ 21 - 40
src/models/RenderModel.ts

@@ -14,9 +14,10 @@ import {
   MeshStandardMaterial,
   PLATFORM,
   PerspectiveCamera,
+  Raycaster,
   Scene,
   TextureLoader,
-  Vector3,
+  Vector2,
   WebGL1Renderer,
 } from "three-platformize";
 import { FBXLoader } from "three-platformize/examples/jsm/loaders/FBXLoader";
@@ -79,6 +80,10 @@ export class RenderModel {
    * TODO: 类型后续完善
    */
   modelMaterialList: any[] = [];
+  /**
+   * 射线
+   */
+  raycaster: Raycaster;
 
   /**
    * 画布属性
@@ -99,6 +104,7 @@ export class RenderModel {
       fbx: new FBXLoader(this.loadingManager),
     };
     this.scene = new Scene();
+    this.raycaster = new Raycaster();
   }
 
   async init(selector: string) {
@@ -230,6 +236,8 @@ export class RenderModel {
 
           resolve(res);
         });
+      } catch (err) {
+        reject(err);
       } finally {
         this.loading = false;
       }
@@ -240,55 +248,20 @@ export class RenderModel {
    * 获取当前模型材质
    */
   getModelMaterialList() {
-    let i = 0;
     this.model?.traverse((v: any) => {
       // TODO: 小程序待确认
       if (v.isMesh && v.material) {
         v.castShadow = true;
         v.frustumCulled = false;
+        // 标记初始位置
+        v._position = v.position.clone();
 
-        if (Array.isArray(v.material)) {
-          v.material = v.material[0];
-          this.setMaterialMeshParams(v, i);
-        } else {
-          this.setMaterialMeshParams(v, i);
-        }
+        this.modelMaterialList.push(v);
       }
     });
   }
 
   /**
-   * 材质参数二次处理
-   */
-  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 贴图地址
@@ -336,17 +309,20 @@ export class RenderModel {
     texture.dispose();
   }
 
+  private autoRotateTimer: NodeJS.Timeout;
   /**
    * 设置自动旋转
    * @param type
    */
   setAutoRotate(type: ROTATE_TYPE) {
+    clearTimeout(this.autoRotateTimer);
+
     switch (type) {
       case ROTATE_TYPE.DELAY:
         this.controls.autoRotate = false;
 
         if (this.autoRotate) {
-          setTimeout(() => {
+          this.autoRotateTimer = setTimeout(() => {
             this.controls.autoRotate = true;
           }, 10000);
         }
@@ -397,6 +373,11 @@ export class RenderModel {
     }
   }
 
+  mouseRaycaster(v2: Vector2) {
+    this.raycaster.setFromCamera(v2, this.camera);
+    return this.raycaster.intersectObjects(this.scene.children, true);
+  }
+
   dispose() {
     this.disposing = true;
     $cancelAnimationFrame(this.frameId);

+ 187 - 75
src/pages/tnd/index.tsx

@@ -1,6 +1,6 @@
 import { useEffect, useMemo, useRef, useState } from "react";
-import { Button, Canvas, View } from "@tarojs/components";
-import { FC } from "@tarojs/taro";
+import { Button, View } from "@tarojs/components";
+import Taro, { FC } from "@tarojs/taro";
 import { observer } from "mobx-react";
 import {
   ROTATE_TYPE,
@@ -17,15 +17,18 @@ import {
   Mesh,
   MeshStandardMaterial,
   PointLight,
+  Vector2,
   Vector3,
 } from "three-platformize";
 import { Easing, Tween } from "@tweenjs/tween.js";
 import { cancelAnimationFrame, requestAnimationFrame } from "@tarojs/runtime";
+import { CanvasAdapter } from "../../components";
 import "./index.scss";
 
 enum MODEL_STATE {
   DEFAULT = 0,
   ZOOM_UP = 1,
+  ZOOM_UP_CLICK = 2,
 }
 
 interface TweenHandlerOptions<T extends object> {
@@ -41,8 +44,10 @@ interface TweenHandlerOptions<T extends object> {
   delay?: number;
 }
 
+const DEFUALT_SCALE = 20;
 const DENGZHAO_MIN_ROTATE = -1.2;
 const DENGZHAO_MAX_ROTATE = -2.55;
+const system = Taro.getSystemInfoSync();
 
 const IndexPage: FC = observer(() => {
   const clock = useRef(new Clock());
@@ -62,8 +67,17 @@ const IndexPage: FC = observer(() => {
     () => modelState === MODEL_STATE.ZOOM_UP,
     [modelState]
   );
+  /**
+   * 是否单独展示某个模型
+   */
+  const isSingleModel = useMemo(
+    () => modelState === MODEL_STATE.ZOOM_UP_CLICK,
+    [modelState]
+  );
   const hotSeparateAnimArr = useRef<ReticleModel[]>([]);
   const tagArr = useRef<TagModel[]>([]);
+  const startMouse = useRef(new Vector2(0, 0));
+  const mouseV2 = useRef(new Vector2(0, 0));
 
   useEffect(() => {
     setTimeout(async () => {
@@ -96,13 +110,8 @@ const IndexPage: FC = observer(() => {
       fileType: "fbx",
     });
 
-    renderModel.modelMaterialList.forEach((material) => {
+    renderModel.modelMaterialList.forEach((mesh) => {
       const meshStandardMaterial = new MeshStandardMaterial();
-      // TODO: 暂定 any
-      const mesh: any = renderModel.scene.getObjectByProperty(
-        "uuid",
-        material.uuid
-      );
 
       // 设置物体是否投射阴影
       mesh.castShadow = true;
@@ -117,7 +126,7 @@ const IndexPage: FC = observer(() => {
       // 设置材质渲染面,2:双面渲染;1:只渲染正面;0:只渲染背面
       meshStandardMaterial.side = 2;
 
-      switch (material.name) {
+      switch (mesh.name) {
         case "dengzhao1":
         case "dengzhao2":
         case "dengpan":
@@ -224,13 +233,7 @@ const IndexPage: FC = observer(() => {
         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
-        );
-
+      renderModel.modelMaterialList.forEach((mesh) => {
         if (e > 5) {
           const t = 1 - 10 * (e - 8) * (1 / 30);
           if (e >= 8) {
@@ -264,10 +267,6 @@ const IndexPage: FC = observer(() => {
     shadowPlan.current!.visible = show;
   };
 
-  const handleClick = () => {
-    renderModel.setAutoRotate(ROTATE_TYPE.DELAY);
-  };
-
   /**
    * 拆解/合并模型
    */
@@ -381,56 +380,47 @@ const IndexPage: FC = observer(() => {
   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,
-            });
-          }
+    renderModel.modelMaterialList.forEach((item) => {
+      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);
+              initReticleMeshs(true);
+              cb && cb();
+            }
+          },
+        });
+      } else {
+        initTag(false);
+        initReticleMeshs(false);
+        tweenHandler({
+          curProps: item.position,
+          targetProps: item._position,
+          cb,
+        });
       }
     });
   };
@@ -474,7 +464,7 @@ const IndexPage: FC = observer(() => {
     });
   };
 
-  const addReticleMeshs = (visible: boolean) => {
+  const initReticleMeshs = (visible: boolean) => {
     if (!hotSeparateAnimArr.current.length) {
       const temp: ReticleModel[] = [];
       const vectorStack = [
@@ -517,14 +507,136 @@ const IndexPage: FC = observer(() => {
     requestAnimationFrame(animate);
   };
 
+  const clickHandler = (x: number, y: number) => {
+    const v2 = new Vector2(x, y);
+
+    // 比较两个向量是否相等
+    if (v2.equals(startMouse.current)) {
+      setCanvasPosition(x, y);
+
+      const res = renderModel.mouseRaycaster(mouseV2.current);
+      for (let i = 0; i < res.length; i++) {
+        const name = res[i].object.name;
+
+        if (!name) continue;
+
+        console.log("click model:", name);
+
+        if (
+          isSeparate &&
+          res[i].object.visible &&
+          res[i].object.parent?.visible
+        ) {
+          handleModelZoomUp(name);
+          break;
+        }
+      }
+    }
+  };
+
+  /**
+   * 单独展示某个模型
+   */
+  const handleModelZoomUp = (name: string) => {
+    renderModel.setAutoRotate(ROTATE_TYPE.FALSE);
+    modelSingleClick(name, () => {
+      renderModel.setControlsStatus(false, true, false, true, false);
+      setModelState(MODEL_STATE.ZOOM_UP_CLICK);
+    });
+  };
+
+  const modelSingleClick = (name: string, cb: Function) => {
+    let targetProps = {
+      x: 0,
+      y: 0,
+      z: 0,
+      scale: DEFUALT_SCALE,
+    };
+
+    switch (name) {
+      case "dengguan":
+        targetProps = {
+          x: 0,
+          y: -68,
+          z: 0,
+          scale: 0.6 * DEFUALT_SCALE,
+        };
+        break;
+    }
+
+    if (name) {
+      renderModel.modelMaterialList.forEach((model) => {
+        model.visible = model.name.includes(name);
+      });
+    }
+
+    initTag(false);
+    initReticleMeshs(false);
+    tweenHandler({
+      curProps: {
+        x: renderModel.model!.position.x,
+        y: renderModel.model!.position.y,
+        z: renderModel.model!.position.z,
+        scale: renderModel.model!.scale.x,
+      },
+      targetProps,
+      onUpdate: (e) => {
+        renderModel.model?.position.set(e.x, e.y, e.z);
+        renderModel.model?.scale.set(e.scale, e.scale, e.scale);
+      },
+      cb: () => {
+        if (name === "") {
+          renderModel.modelMaterialList.forEach((model) => {
+            model.visible = true;
+          });
+          initTag(true);
+          initReticleMeshs(true);
+        }
+
+        cb && cb();
+      },
+    });
+  };
+
+  const setCanvasPosition = (x: number, y: number) => {
+    mouseV2.current.x = (x / system.windowWidth) * 2 - 1;
+    mouseV2.current.y = (-y / system.windowHeight) * 2 + 1;
+  };
+
+  /**
+   * 返回分解模型
+   */
+  const backForSeparate = () => {
+    modelSingleClick("", () => {
+      renderModel.setControlsStatus(true, true, false, true, false);
+      setModelState(MODEL_STATE.ZOOM_UP);
+    });
+  };
+
   return (
-    <View className="page" onClick={handleClick}>
-      <Canvas id="wgl" type="webgl" />
+    <View className="page">
+      <CanvasAdapter
+        onTouchStart={(e) => {
+          // @ts-ignore
+          startMouse.current = new Vector2(e.clientX, e.clientY);
+          renderModel.setAutoRotate(ROTATE_TYPE.DELAY);
+        }}
+        onTouchEnd={(e) => {
+          // @ts-ignore
+          clickHandler(e.clientX, e.clientY);
+        }}
+      />
 
       <View className="toolbar">
-        <Button className="btn" onClick={handleSeparate}>
-          {!isSeparate ? "拆解" : "合并"}
-        </Button>
+        {isSingleModel ? (
+          <Button className="btn" onClick={backForSeparate}>
+            返回
+          </Button>
+        ) : (
+          <Button className="btn" onClick={handleSeparate}>
+            {!isSeparate ? "拆解" : "合并"}
+          </Button>
+        )}
       </View>
     </View>
   );