前端项目实战之Vue3.0+Three.js+TypeScript+Vite搭建一个设备数字化三维展厅(一)

效果预览

机器人运动展示

项目结构

引用组件

Vue3.0+Vite

npm create vite   
npm i @types/node --save-dev  

vue-router

npm i vue-router --save-dev
  • 1.新建一个router目录
  • 2.在router目录下创建一个index.ts文件
    • 2.1.在目录中添加需要路由的模块
    • 2.2.设置路由模式
  • 3.在main.ts中引入router

threejs

  1. 安装 three.js的type
  2. 安装 three
npm i @types/three --save-dev
npm i three --save-dev

创建场景


/** 创建场景 */
const setScene = () => {
  scene = new Scene();
  renderer = new WebGLRenderer();
  renderer.physicallyCorrectLights = true;
  renderer.shadowMap.enabled = true;
  renderer.setSize(innerWidth, innerHeight);
  renderer.setClearColor(0xc9c9c9, 1);
  document.querySelector(".boxs").appendChild(renderer.domElement);
};

设置相机

/** 创建相机 */
const setCamera = () => {
  const { x, y, z } = defaultMap;
  camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
  camera.position.set(x, y, z);
};

创建灯光

/** 设置场景灯光 */
const setLight = () => {
  directionalLight = new DirectionalLight(0xffffff, 5);
  directionalLight.position.set(-4, 8, 4);
  dhelper = new DirectionalLightHelper(directionalLight, 5, 0xff0000);
  hemisphereLight = new HemisphereLight(0xffffff, 0xffffff, 5);
  hemisphereLight.position.set(0, 8, 0);
  hHelper = new HemisphereLightHelper(hemisphereLight, 5);
  scene.add(directionalLight, hemisphereLight);
  ambientLight = new AmbientLight(0xffffff, 5);
  ambientLight.position.set(0, 10, 0);
  scene.add(ambientLight);
};

导入模型

/** 通过Promise处理一下loadfile函数 */
const loadFile = (url): any => {
  loader = new GLTFLoader();
  return new Promise((resolve, reject) => {
    loader.load(
      url,
      (gltf) => {
        resolve(gltf);
      },
      ({ loaded, total }) => {
        let load = Math.abs((loaded / total) * 100);
        loadingWidth.value = load;
        if (load >= 100) {
          setTimeout(() => {
            isLoading.value = false;
          }, 1000);
        }
        console.log(load + "% loaded");
      },
      (err) => {
        reject(err);
      }
    );
  });
};

const loadFBX = (url) => {
  const loader = new FBXLoader();
  return new Promise((resolve, reject) => {
    loader.load(
      url,
      (fbx) => resolve(fbx),
      ({ loaded, total }) => {
        let load = Math.abs((loaded / total) * 100);
        loadingWidth.value = load;
        if (loaded >= 100) {
          setTimeout(() => {
            isLoading.value = false;
          }, 1000);
        }
        console.log(load + "% loaded");
      }
    );
  });
};
添加stats监控
const addStats = () => {
  stats = Stats();
  document.querySelector(".boxs").appendChild(stats.domElement);
};

设置轨道控制
/** 设置轨道控制器 */
const setControls = () => {
  controls = new OrbitControls(camera, renderer.domElement);
  /**  */
  controls.maxPolarAngle = (0.9 * Math.PI) / 2;
  /** 是否开启缩放 */
  controls.enableZoom = true;
  /** 是否开启右键拖拽 */
  controls.enablePan = true;
  controls.addEventListener("change", render);
};

添加TWEEN动画

/** 设置模型动画 */
const setTweens = (obj, newObj, duration = 1500) => {
  var ro = new TWEEN.Tween(obj);
  ro.to(newObj, duration); // 变化后的位置以及动画时间
  ro.easing(TWEEN.Easing.Sinusoidal.InOut);
  ro.onUpdate(function () {});
  ro.onComplete(function () {});
  ro.start();
};

完整代码

<template>
  <div class="boxs">
    <div class="maskLoading" v-if="isLoading">
      <div class="loading">
        <div :style="{ width: loadingWidth + '%' }"></div>
      </div>
      <div style="padding-left: 10px">{{ parseInt(loadingWidth) }}%</div>
    </div>
    <div class="mask">
      <p>x : {{ x }} y:{{ y }} z :{{ z }}</p>
      <button @click="autoRotate">转动</button>
      <button @click="stopRotate">停止</button>

      <button v-if="robot.robot" @click="robotAnimate">机器人运动</button>
      <button v-if="robot.robot" @click="stopRobotAnimate">机器人停止</button>

      <div class="flex">
        <div
          @click="setColor(index)"
          v-for="(item, index) in colorArray"
          :style="{ backgroundColor: item }"
          :key="index"
        ></div>
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, toRefs } from "vue";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import {
  Scene,
  WebGLRenderer,
  PerspectiveCamera,
  ImageUtils,
  TextureLoader,
  HemisphereLight,
  HemisphereLightHelper,
  DirectionalLight,
  DirectionalLightHelper,
  Color,
  Mesh,
  Group,
  MeshLambertMaterial,
  PlaneGeometry,
  DoubleSide,
  Box3,
  AnimationMixer,
  AmbientLight,
} from "three";

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import Stats from "three/examples/jsm/libs/stats.module";
// import * as TWEEN from "../../public/tween/dist/tween.esm"
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
let stats = Stats();

let colorArray = [
  "rgb(216, 27, 67)",
  "rgb(142, 36, 170)",
  "rgb(81, 45, 168)",
  "rgb(48, 63, 159)",
  "rgb(30, 136, 229)",
  "rgb(0, 137, 123)",
  "rgb(67, 160, 71)",
  "rgb(251, 192, 45)",
  "rgb(245, 124, 0)",
  "rgb(230, 74, 25)",
  "rgb(233, 30, 78)",
  "rgb(156, 39, 176)",
  "rgb(0, 0, 0)",
]; // 车身颜色数组

let scene: Scene,
  renderer: WebGLRenderer,
  camera: PerspectiveCamera,
  directionalLight: DirectionalLight,
  hemisphereLight: HemisphereLight,
  ambientLight: AmbientLight,
  dhelper: DirectionalLightHelper,
  hHelper: HemisphereLightHelper,
  controls: OrbitControls;
let isLoading = ref(true); // 是否显示loading,监听loading模型的进度
let loadingWidth = ref(0); // 加载进度显示
let loader: GLTFLoader; // gltf文件加载器
let car: any = {
  wheel: null,
  main: null,
  door: null,
}; // 加载的汽车模型
let robot: any = {
  robot: null,
  bottom: null,
  armHeader: null,
  sole: null,
  arm001: null,
  fork001: null,
  pully001: null,
  tong001: null,
  mixer2: null,
};
let textureLoader = new TextureLoader(); // 材质加载器
let marker;
let orbitControls: OrbitControls;
let animateTime;

//相机的默认坐标
const defaultMap = {
  x: 50,
  y: 10,
  z: 0,
};

let map = reactive(defaultMap); // 把相机坐标设置为可观察对象
let { x, y, z } = toRefs(map); // 输出坐标

/** 创建场景 */
const setScene = () => {
  scene = new Scene();
  renderer = new WebGLRenderer();
  renderer.physicallyCorrectLights = true;
  renderer.shadowMap.enabled = true;
  renderer.setSize(innerWidth, innerHeight);
  renderer.setClearColor(0xc9c9c9, 1);
  document.querySelector(".boxs").appendChild(renderer.domElement);
};

/** 创建相机 */
const setCamera = () => {
  const { x, y, z } = defaultMap;
  camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
  camera.position.set(x, y, z);
};

/** 设置场景灯光 */
const setLight = () => {
  directionalLight = new DirectionalLight(0xffffff, 5);
  directionalLight.position.set(-4, 8, 4);
  dhelper = new DirectionalLightHelper(directionalLight, 5, 0xff0000);
  hemisphereLight = new HemisphereLight(0xffffff, 0xffffff, 5);
  hemisphereLight.position.set(0, 8, 0);
  hHelper = new HemisphereLightHelper(hemisphereLight, 5);
  scene.add(directionalLight, hemisphereLight);
  ambientLight = new AmbientLight(0xffffff, 5);
  ambientLight.position.set(0, 10, 0);
  scene.add(ambientLight);
};

/** 设置轨道控制器 */
const setControls = () => {
  controls = new OrbitControls(camera, renderer.domElement);
  /**  */
  controls.maxPolarAngle = (0.9 * Math.PI) / 2;
  /** 是否开启缩放 */
  controls.enableZoom = true;
  /** 是否开启右键拖拽 */
  controls.enablePan = true;
  controls.addEventListener("change", render);
};

/** 渲染场景 */
const render = () => {
  map.x = camera.position.x;
  map.y = camera.position.y;
  map.z = camera.position.z;
};

/** 循环渲染 场景 相机 和位置更新 */
const animate = () => {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  controls.update();
  stats.update();
  TWEEN.update();
};

/** 设置模型动画 */
const setTweens = (obj, newObj, duration = 1500) => {
  var ro = new TWEEN.Tween(obj);
  ro.to(newObj, duration); // 变化后的位置以及动画时间
  ro.easing(TWEEN.Easing.Sinusoidal.InOut);
  ro.onUpdate(function () {});
  ro.onComplete(function () {});
  ro.start();
};

/** 是否自动转动 */
const autoRotate = () => {
  controls.autoRotate = true;
};

/** 停止自动转动 */
const stopRotate = () => {
  controls.autoRotate = false;
};

/** 设置颜色 */
const setColor = (index) => {
  const currentColor = new Color(colorArray[index]);
  scene.traverse((child) => {
    if (child instanceof Mesh) {
      console.log(child.name);
      if (child.name.includes("")) {
        child.material.color.set(currentColor);
      }
    }
  });
};

const loadWithDraco = (module: GLTFLoader, url): any => {
  return new Promise((resovle, reject) => {
    const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath("../../public/models/draco/");

    module.setDRACOLoader(dracoLoader);
    dracoLoader.load(
      url,
      (gltf) => {
        resovle(gltf);
      },
      ({ loaded, total }) => {
        let load = Math.abs((loaded / total) * 100);
        loadingWidth.value = load;
        if (load >= 100) {
          setTimeout(() => {
            isLoading.value = false;
          }, 1000);
        }
        console.log((loaded / total) * 100 + "% loaded");
      },
      (err) => {
        reject(err);
      }
    );
  });
};

/** 通过Promise处理一下loadfile函数 */
const loadFile = (url): any => {
  loader = new GLTFLoader();
  return new Promise((resolve, reject) => {
    loader.load(
      url,
      (gltf) => {
        resolve(gltf);
      },
      ({ loaded, total }) => {
        let load = Math.abs((loaded / total) * 100);
        loadingWidth.value = load;
        if (load >= 100) {
          setTimeout(() => {
            isLoading.value = false;
          }, 1000);
        }
        console.log(load + "% loaded");
      },
      (err) => {
        reject(err);
      }
    );
  });
};

const loadFBX = (url) => {
  const loader = new FBXLoader();
  return new Promise((resolve, reject) => {
    loader.load(
      url,
      (fbx) => resolve(fbx),
      ({ loaded, total }) => {
        let load = Math.abs((loaded / total) * 100);
        loadingWidth.value = load;
        if (loaded >= 100) {
          setTimeout(() => {
            isLoading.value = false;
          }, 1000);
        }
        console.log(load + "% loaded");
      }
    );
  });
};

const processFBX = (obj) => {
  obj.scale.set(0.004, 0.008, 0.004);
  obj.position.set(0, 0, -4);
  obj.rotation.set(0, -41, 0);
  car.wheel = obj.children[1].children[5].children;
  car.wheel.map((cell) => {
    cell.rotation.set(cell.rotation.x, -0.5 * Math.PI, cell.rotation.z);
  });
  console.log("obj:", obj);
  obj.name = "cart";
  car.door = obj.children[1].children[3].children[0].children;
  const texture = textureLoader.load("../../public/models/marker3.png");
  car.main = [
    obj.children[1].children[0],
    ...obj.children[1].children[1].children,
    ...obj.children[1].children[3].children,
  ];
  car.trim = [
    ...obj.children[1].children[2].children,
    ...obj.children[1].children[4].children,
    ...obj.children[1].children[6].children,
  ];
  const marker = new Mesh( // 创建平面
    new PlaneGeometry(1, 1),
    new MeshLambertMaterial({
      map: texture,
      aoMapIntensity: 0,
      side: DoubleSide,
      opacity: 0.1,
      transparent: true,
      depthTest: false,
    })
  );
  car.marker = marker;
  marker.position.set(2.5, 3, 3);
  marker.rotation.y = 0.5 * Math.PI;
  var group = new Group();
  group.add(obj);
  group.add(marker);
  scene.add(group);
};

const addStats = () => {
  stats = Stats();
  document.querySelector(".boxs").appendChild(stats.domElement);
};

const robotAnimate = () => {
  //   robot.robot = gltf;
  //   console.log(gltf);
  //   robot.robot = gltf;
  //   robot.bottom = gltf.scene.children[1];

  //   robot.sole = gltf.scene.children[0];
  //   robot.arm001 = gltf.scene.children[0].children[2];
  //   robot.armHeader = gltf.scene.children[0].children[2].children[0];
  //   robot.fork001 = gltf.scene.children[0].children[2].children[0].children[4];
  //   robot.pulley001 =
  //     gltf.scene.children[0].children[2].children[0].children[4].children[4];
  //   robot.tong001 =
  //     gltf.scene.children[0].children[2].children[0].children[4].children[4].children[3];

  const gltf = robot.robot;

  const object = gltf.scene;
  object.position.set(0, 0, 0);
  object.rotation.set(0,-90,0)
  // 中电
  const bbox = new Box3();
  object.traverse(function (node) {
    if (node.isMesh) node.castShadow = true;
    bbox.expandByObject(object);
  });
  // get the animation
  console.log("gltf.animations[0]:", gltf.animations);
  robot.mixer2 = new AnimationMixer(object);
  // that.mixer2.clipAction(gltf.animations[0]).play();
  object.scale.set(1, 1, 1);

  controls.reset(); // 回到初始化视角

  const setCircle = (num: number, upperLimit?, lowerLimit?) => {
    // console.log("before", num);

    if (!!upperLimit && !!lowerLimit) {
      if (num > upperLimit) {
        num = lowerLimit;
      } else if (num < lowerLimit) {
        num = upperLimit;
      }
      return parseFloat((num % 360).toString()).toPrecision(3);
    }
    // console.log("after", num);
  };

  animateTime = setInterval(() => {
    setTweens(
      robot.sole.rotation,
      {
        x: robot.sole.rotation.x,
        y: robot.sole.rotation.y,
        z: setCircle(robot.sole.rotation.z + 1, 360, 0),
      },
      1000
    );

    console.log(
      "sole",
      robot.sole.rotation.x,
      robot.sole.rotation.y,
      robot.sole.rotation.z
    );

    setTweens(
      robot.arm001.rotation,
      {
        x: robot.arm001.rotation.x,
        y: setCircle(robot.arm001.rotation.y + 0.05, 1, -1),
        z: robot.arm001.rotation.z,
      },
      1000
    );

    console.log(
      "arm001",
      robot.arm001.rotation.x,
      robot.arm001.rotation.y,
      robot.arm001.rotation.z
    );

    setTweens(
      robot.armHeader.rotation,
      {
        x: robot.armHeader.rotation.x,
        y: setCircle(robot.armHeader.rotation.y + 0.02, 0.2, -0.4),
        z: robot.armHeader.rotation.z,
      },
      1000
    );

    console.log(
      "armHeader",
      robot.armHeader.rotation.x,
      robot.armHeader.rotation.y,
      robot.armHeader.rotation.z
    );

    setTweens(
      robot.tong001.rotation,
      {
        x: robot.tong001.rotation.x,
        y: robot.tong001.rotation.y,
        z: setCircle(robot.tong001.rotation.z + 1, 360, -360),
      },
      1000
    );

    console.log(
      "tong001",
      robot.tong001.rotation.x,
      robot.tong001.rotation.y,
      robot.tong001.rotation.z
    );
  }, 1000);
};

const stopRobotAnimate = () => {
  clearInterval(animateTime);

  setTimeout(() => {
    setTweens(camera.position, { x: 50, y: 10, z: 0 }, 1000);
    console.log("this.camera.up:", camera.up);
    camera.lookAt(camera.position);
    renderer.render(scene, camera);
    controls.reset(); // 回到初始化视角
    setTimeout(() => {}, 1000);
  }, 1000);
};

//初始化所有函数
const init = async () => {
  console.log(stats);

  setScene();
  setCamera();
  setLight();
  setControls();
  addStats();

  const loader = new GLTFLoader();
  const robotUrl = `../../public/models/zhongzhuan001.glb`;
  const ferarrisUrl = `../../public/models/ferrari.glb`;

  const gltf = await loadFile(robotUrl);

  if (gltf?.scene?.children[0].name == "sole") {
    robot.robot = gltf;
    console.log(gltf);
    robot.robot = gltf;
    robot.bottom = gltf.scene.children[1];

    robot.sole = gltf.scene.children[0];
    robot.arm001 = gltf.scene.children[0].children[2];
    robot.armHeader = gltf.scene.children[0].children[2].children[0];
    robot.fork001 = gltf.scene.children[0].children[2].children[0].children[4];
    robot.pulley001 =
      gltf.scene.children[0].children[2].children[0].children[4].children[4];
    robot.tong001 =
      gltf.scene.children[0].children[2].children[0].children[4].children[4].children[3];

    console.log(robot);
  }

  //const fbx = await loadFBX("../../public/models/MBSL.fbx");
  //processFBX(fbx);

  console.log(gltf);

  scene.add(gltf.scene);

  animate();
};
//用vue钩子函数调用
onMounted(init);
</script>
<style scoped>
body {
  margin: 0;
}

.maskLoading {
  background: #ccc;
  position: fixed;
  display: flex;
  justify-content: center;
  align-items: center;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1111111;
  color: #fff;
}

.maskLoading .loading {
  width: 400px;
  height: 20px;
  border: 1px solid #fff;
  background: #000;
  overflow: hidden;
  border-radius: 10px;
}

.maskLoading .loading div {
  background: #fff;
  height: 20px;
  width: 0;
  transition-duration: 500ms;
  transition-timing-function: ease-in;
}

canvas {
  width: 100%;
  height: 100%;
  margin: auto;
}

.mask {
  color: #fff;
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
}

.flex {
  display: flex;
  flex-wrap: wrap;
  padding: 20px;
  justify-content: center;
}

.flex div {
  width: 10px;
  height: 10px;
  margin: 5px;
  cursor: pointer;
}
</style>

注意事项

  • vue-router
  • vue3.0 setup模式
  • 引用文件路径
  • 引用页面组件

稍后整理后将上传源代码

https://github.com/cugzhaolei/three-v.git

参考链接

Vite 官方中文文档
vue3.0添加路由router
vue3.0配置vue-router
用最简单方式打造Three.js 3D汽车展示厅

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是搭建 Vue 3.0 + Vite + Pinia + TypeScript 的步骤: 1. 安装 Node.js,推荐使用 LTS 版本。 2. 安装 Vite: ``` npm init vite-app my-project ``` 这里我们使用 Vite 初始化一个新项目,名称为 my-project。 3. 安装依赖: ``` cd my-project npm install ``` 4. 安装 Vue 3.0: ``` npm install vue@next ``` 5. 安装 Pinia: ``` npm install pinia ``` 6. 安装 TypeScript: ``` npm install --save-dev typescript ``` 7. 配置 TypeScript: 在项目根目录下创建 `tsconfig.json` 文件,内容如下: ```json { "compilerOptions": { "target": "esnext", "module": "esnext", "strict": true, "jsx": "preserve", "sourceMap": true, "moduleResolution": "node", "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], "exclude": ["node_modules"] } ``` 8. 安装 Pinia Devtools(可选): ``` npm install @pinia/devtools --save-dev ``` 9. 在 `main.ts` 中进行配置: ```typescript import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const app = createApp(App) // 创建 Pinia 实例 const pinia = createPinia() // 将 Pinia 实例挂载到 app 上 app.use(pinia) app.mount('#app') ``` 10. 编写组件: 在 `src` 目录下创建一个 `components` 目录,然后创建一个 `HelloWorld.vue` 组件: ```vue <template> <div> <h1>Hello, {{ name }}</h1> <button @click="increase">Increase</button> <p>{{ count }}</p> </div> </template> <script lang="ts"> import { defineComponent } from 'vue' import { useStore } from 'pinia' export default defineComponent({ name: 'HelloWorld', setup() { const store = useStore() const name = store.getters.getName const count = store.state.count const increase = () => { store.commit('increase') } return { name, count, increase } }, }) </script> ``` 11. 在 `App.vue` 中使用组件: ```vue <template> <HelloWorld /> </template> <script lang="ts"> import { defineComponent } from 'vue' import HelloWorld from './components/HelloWorld.vue' export default defineComponent({ name: 'App', components: { HelloWorld, }, }) </script> ``` 12. 运行项目: ``` npm run dev ``` 至此,我们已经成功搭建Vue 3.0 + Vite + Pinia + TypeScript 的项目。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值