文章
问答
冒泡
React+Three 实现模型外壳隐藏以及零件分解效果

💡 Tips:需要对threejs 的基础知识有一定了解,一些公共函数封装可以看以前文章或者代码仓库:https://github.com/Gzx97/umi-three-demo 使用了TS类型规范以及中文注释。

实现效果

拆解.gif

裁剪实现简要原理:

筛选出模型的每部结构和外部结构,把外部结构的材质属性设置为可裁剪,通过Three.js数学模块的API平面Plane对Three.js的网格模型对象进行剪裁。

零件分解效果简要原理:

通过配置模型每个模块位置信息的config文件,遍历模型控制每一部分的轻微位移,使用动画过度实现零件拆解特效。

核心功能实现:

首先把场景初始化出来,并且把模型加载到场景中去,这一部分比较基础,使用封装好的Viewer类(封装过程可以参考代码仓库或者以前文章)来初始化页面。

由于要加载两部分模型,所以提取了一些公共的配置常量方便调试。

const MODEL_SCALES = [0.0001 * 3, 0.0001 * 3, 0.0001 * 3] as const;
const MODEL_URL = {
  SKELETON: `/models/turbine.glb`,
  EQUIPMENT: `/models/equipment.glb`,
} as const;

初始化页面的核心代码实现:

这一部分实现了基础的场景初始化,并且把模型安置到场景种,根据实际模型的信息,修改相机以及控制器等配置。如果模型设置了动画播放模型动画。


//加载gltf文件
const loadGLTF = (url: string): Promise<GLTF> => {
  const loader = new GLTFLoader();
  const onCompleted = (object: GLTF, resolve: any) => resolve(object);
  return new Promise<GLTF>((resolve) => {
    loader.load(url, (object: GLTF) => onCompleted(object, resolve));
  });
};
const loadModels = async (tasks: Promise<any>[]) => {
  setModelLoading(true);
  await Promise.all(tasks);
  setModelLoading(false);
};
// 加载灯光
const loadLights = () => {
  const LIGHT_LIST = [
    [100, 100, 100],
    [-100, 100, 100],
    [100, -100, 100],
    [100, 100, -100],
  ];
  forEach(LIGHT_LIST, ([x, y, z]) => {
    const directionalLight = new THREE.DirectionalLight(0xffffff, 3);
    directionalLight.position.set(x, y, z);
    viewerRef?.current?.scene?.add(directionalLight);
  });
};
// 加载零件设备
const loadTurbineEquipments = async () => {
  const { scene: object } = await loadGLTF(MODEL_URL.EQUIPMENT);
  object.scale.set(...MODEL_SCALES);
  // object.position.set(0, -2, 0);
  object.name = "equipment";
  modelEquipment.current = object;
  turbineGroup.add(object);
};
//加载风机骨架
const loadTurbineSkeleton = async (viewer: Viewer) => {
  const gltfModel = await loadGLTF(MODEL_URL.SKELETON);
  const baseModel = new BaseModel(gltfModel, viewer);
  baseModel.setScalc(...MODEL_SCALES);
  const object = baseModel.gltf.scene;
  object.position.set(0, 0, 0);
  object.name = "equipment";
  modelSkeleton.current = object;
  turbineGroup.add(object);
  baseModel.startAnima(0, "风机");
};
// 初始化
const init = () => {
  viewerRef.current = new Viewer(PAGE_ID);
  const viewer = viewerRef.current;
  viewer.addAxis();
  viewer.controls.target.set(0, 2, 0);
  viewer.camera.position.set(-5.42, 5, 9);
  loadLights();
  viewer?.scene.add(turbineGroup);
  loadModels([loadTurbineSkeleton(viewer), loadTurbineEquipments()]);
};

useEffect(() => {
  init();
  return () => {
    viewerRef.current?.destroy();
  };
}, []);
return (
  <div
    id={PAGE_ID}
    style={{ width: 1000, height: 1000, border: "1px solid red" }}
    ></div>
);

使用裁剪效果实现模型的外壳隐藏

新建一个裁剪平面 new THREE.Plane,可以通过辅助工具PlaneHelper调试该平面的参数 通过Three.js材质对象的.clippingPlanes属性。一个网格模型所绑定材质对象的.clippingPlanes属性如果没有设置就不会被剪裁,想剪裁那个网格模型对象,就设置那个模型对象的.clippingPlanes属性。然后自定义动画来修改Plan的位置裁剪模型。

  // 风机骨架消隐动画
  const skeletonHideAnimation = () => {
    const viewer = viewerRef.current;
    if (!viewer) return;
    const shellModel = modelSkeleton.current?.getObjectByName(
      MODEL_SKELETON_ENUM.ColorMaterial
    ); //筛选出需要裁剪的部分
    const clippingPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 3.5); //裁剪平面
    // const helper = new THREE.PlaneHelper(clippingPlane, 300, 0xffff00);//辅助查看裁剪平面
    // viewer.scene?.add(helper);
    shellModel?.traverse((mesh) => {
      if (!(mesh instanceof THREE.Mesh)) return undefined;
      mesh.material = new THREE.MeshPhysicalMaterial({
        ...mesh.material,
        clipIntersection: true, //改变剪裁方式,剪裁所有平面要剪裁部分的交集
        clipShadows: true,
        clippingPlanes: [clippingPlane],
      });
      // 白色外壳消隐效果
      mesh.material.clippingPlanes = [clippingPlane];
      return undefined;
    });
    const fnOnj = {
      fun: () => {
        if (clippingPlane.constant <= -0.1) {
          modelSkeleton.current?.remove(shellModel!);
          viewer?.removeAnimate("clipping");
          console.log(viewer?.scene);
        }
        clippingPlane.constant -= 0.05;
      },
      content: viewer,
    };
    viewer?.addAnimate("clipping", fnOnj);

  };

注意:除了设置WebGL渲染器对象WebGLRenderer的.clippingPlanes属性外,还需要设置WebGL渲染器的.localClippingEnabled属性。不然裁剪不会有效果。

//src\modules\Viewer\index.ts
private initRenderer() {
  //...
    // 开启模型对象的局部剪裁平面功能
    // 如果不设置为true,设置剪裁平面的模型不会被剪裁
    this.renderer.localClippingEnabled = true;
  //...

  }

实现零件分解效果

加载模型时,把model对象使用ref保存起来。通过编写各个零件的坐标配置文件,为后面实现零件分部位移的动画做准备。然后使用补件动画库实现零件的位移。

调用.updateMatrixWorld()方法首先会更新对象的本地矩阵属性,然后更新对象的世界矩阵属性。

.updateMatrixWorld()方法封装了递归算法,会遍历对象的所有子对象和对象本身。

export const MODEL_EQUIPMENT_ENUM = {
  PRINCIPAL_AXIS: "主轴",
  YAWMOTOR: "偏航电机",
...
} as const;

export const MODEL_EQUIPMENT_POSITION_PARAMS_ENUM = {
  [MODEL_EQUIPMENT_ENUM.PRINCIPAL_AXIS]: {
    COMPOSE: { x: 20437.78515625, y: 8650, z: 0 },
    DECOMPOSE: { x: 20437.78515625, y: 8650, z: 400 },
  },
  [MODEL_EQUIPMENT_ENUM.YAWMOTOR]: {
    COMPOSE: { x: 20437.78515625, y: 8650, z: 0 },
    DECOMPOSE: { x: 21000, y: 8650, z: 100 },
  },
...
} ;

// 设备分解动画
  const equipmentDecomposeAnimation = async () => {
    // await sleep(1 * 1000);
    modelEquipment.current?.updateMatrixWorld();
    modelEquipment.current?.children.forEach((child: THREE.Object3D) => {
      const params = MODEL_EQUIPMENT_POSITION_PARAMS_ENUM[child.name];
      viewerRef?.current?.animation({
        from: child.position,
        to: params.DECOMPOSE,
        duration: 2 * 1000,
        onUpdate: (position: any) => {
          child.position.set(position.x, position.y, position.z);
        },
      });
    });
  };
调用过度动画的简单封装:
  public animation = (props: {
    from: Record<string, any>;
    to: Record<string, any>;
    duration: number;
    easing?: any;
    onUpdate: (params: Record<string, any>) => void;
    onComplete?: (params: Record<string, any>) => void;
  }) => {
    const {
      from,
      to,
      duration,
      easing = TWEEN.Easing.Quadratic.Out,
      onUpdate,
      onComplete,
    } = props;
    return new TWEEN.Tween(from)
      .to(to, duration)
      .easing(easing)
      .onUpdate((object) => isFunction(onUpdate) && onUpdate(object))
      .onComplete((object) => isFunction(onComplete) && onComplete(object))~~~~
      .start();
  };

以上是使用React+Umi搭建的一个脚手架,实现了对于模型的裁剪和拆解的功能操作。

react
three

关于作者

却黑
社恐
获得点赞
文章被阅读