Three.js - 模型加载

779 阅读2分钟

模型加载弹窗组件

<template>
  <div
    class="model-preview"
    v-show="visible"
  >
    <!-- 标题 -->
    <div class="popup-title">
      模型查看
      <img
        class="popup-close"
        :src="require(`@/assets/home/close.png`)"
        @click="closeClick"
      />
    </div>
    <!-- 模型 -->
    <div id="modelPreview" class="model-view">
      <el-progress
        type="circle"
        v-if="isModelLoading"
        :percentage="modelProgress"
      ></el-progress>
    </div>
  </div>
</template>

<script>
import satellite3D from "@/plugins/three/initSatellite.js";
import Bus from "@/plugins/bus/bus.js";
import { ElMessage } from "element-plus";
import { defineComponent, onMounted, ref, toRefs, watch, onBeforeUnmount, nextTick } from "vue";

export default defineComponent({
  props: {
    visible: {
      type: Boolean,
      default: false,
    },
    url: {
      type: String,
      default: "",
    }
  },
  setup(props, { emit }) {
    const { visible, url } = toRefs(props);

    let isModelLoading = ref(false);
    let modelProgress = ref(0);

    // 监听面板类型,设置组件
    watch(
      () => visible.value,
      (newVal) => {
        if (newVal) {
          url.value
            ? satelliteModel(url.value)
            : satellite3D.clearSatelliteAnimation(); // clearSatelliteAnimation 停止动画渲染循环,清除场景数据
        } else {
          satellite3D.clearSatelliteAnimation();
        }
      },
      {
        immediate: true,
      }
    );

    const satelliteModel = async (url) => {
      await nextTick();
      satellite3D.changeSatellite(url);
    };

    const closeClick = () => {
      emit("close");
    };

    onMounted(() => {
      // 是否关闭模型加载loading
      Bus.$on("ModelPreview-modelLoading", (val) => {
        isModelLoading.value = val;
        !val && (modelProgress.value = 0);
      });
      // 模型加载进度更新
      Bus.$on("ModelPreview-modelProgress", (val) => {
        isModelLoading.value = true;
        modelProgress.value = val;
      });

      // 浏览器不支持webGL
      Bus.$on("ModelPreview-noWebgl", () => {
        ElMessage({
          message: "浏览器不支持webGL",
          type: "error",
          showClose: true,
        });
        satellite3D.clearSatelliteAnimation();
      });
    });

    onBeforeUnmount(() => {
      // 帧跟随撤销
      satellite3D.destroyAnimation();

      Bus.$off([
        "ModelPreview-modelLoading",
        "ModelPreview-modelProgress",
        "ModelPreview-noWebgl",
      ]);
    });

    return {
      isModelLoading,
      modelProgress,
      closeClick,
    };
  },
});
</script>

<style lang="stylus" scoped>
.model-preview
  width 9.9rem
  height 7.2rem
  background #ffffff
  border 0.01rem solid #e1e4eb
  border-radius 0.08rem
  box-shadow 0 0.02rem 0.08rem 0 #000
  margin 0
  position absolute
  top 50%
  left 50%
  transform translateX(-50%) translateY(-50%)
  z-index 99
  .model-view
    width 100%
    height calc(100% - 0.4rem)
    border-radius 0 0 0.08rem 0.08rem
    :deep() .el-progress
      width 100%
      height 100%
      display flex
      justify-content center
      .el-progress__text
        font-size 0.16rem
        color #fff
  .popup-title
    position relative
    height 0.4rem
    line-height 0.4rem
    text-align center
    font-size 0.14rem
    color #5C6472
    border-radius 0.08rem
    .popup-close
      position absolute
      right 0.1rem
      top 0.08rem
      font-size 0.24rem
      cursor pointer
</style>

模型加载模块文件

加载环境

import THREE from "@/plugins/three/three3D.js";
import Detector from "@/plugins/three/Detector.js";
import Bus from "@/plugins/bus/bus";

let init = false; // 是否已初始化
let dom, width, height;
let model, animationId;
let scene, camera, renderer, controls;

// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.9); // 颜色,光照强度
ambientLight.castShadow = true; // 阴影

// 平行光
const directionalLight = new THREE.DirectionalLight("#ffffff");
directionalLight.castShadow = true;
directionalLight.position.set(5, 10, 7.5);

// 初始化
function initSatellite () {
  // 检测浏览器是否支持webGL
  if (Detector.webgl) {
    dom = document.getElementById('modelPreview');
    width = dom.clientWidth
    height = dom.clientHeight
    scene = new THREE.Scene(); // 新建场景
    scene.add(ambientLight); // 添加环境光源
    scene.add(directionalLight); // 添加平行光源

    // 设置远景相机
    camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 5000); // 透视摄像机PerspectiveCamera:Fov 视野角度 默认50,Aspect 长宽比,Near 近截面 默认0.1,Far 远截面 默认2000。当物体某些部分比摄像机的远截面远或者比近截面近的时候,这些部分将不会被渲染到场景中。
    camera.position.set(0, 0, 5000); // 设置相机位置xyz。防止 scene.add() 后摄像机和模型处于同一位置。

    // 创建渲染器
    renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true
    });
    renderer.setSize(width, height); // 设置窗口尺寸
    renderer.setPixelRatio(window.devicePixelRatio); // 抗锯齿
    renderer.outputEncoding = THREE.sRGBEncoding; // gltf 适配纹理
    dom.appendChild(renderer.domElement); // 添加到box节点下:<canvas>

    // 控制器
    controls = new THREE.OrbitControls(camera, renderer.domElement); // 初始化控制器
    setControls(controls) // 设置控制器

    return true
  } else {
    Bus.$emit('ModelPreview-noWebgl')
    return false
  }
}

// 设置控制器
function setControls (controls) {
  // 旋转
  controls.rotateSpeed = 0.1; // 设置旋转速度
  controls.enableDamping = true; // 使动画循环使用时阻尼或自转 意思是否有惯性 
  controls.autoRotate = true; // 是否自动旋转 

  controls.enableZoom = true; // 是否可以缩放 
  controls.minDistance = 1; // 设置相机距离原点的最近距离(最大显示)
  controls.maxDistance = 3000; // 设置相机距离原点的最远距离(最小显示)

  controls.enablePan = true; // 是否开启右键拖拽
  controls.enabled = true; // 启用控制器

  // window.addEventListener('resize', resize, false); // 自适应监听
  controls.update(); // 更新控制器
}

切换模型

// 切换模型
async function changeSatellite (url) {
  // 帧播放关闭
  if (animationId) {
    window.cancelAnimationFrame(animationId)
  }

  // 判断初次加载,加载场景
  !init && (await initSatellite(), init = true)

  model && scene.remove(model) // 移除旧模型
  renderer.render(scene, camera) // 更新渲染器

  let isTimer = false

  // 加载模型
  Bus.$emit('ModelPreview-modelLoading', true);
  model = await loadGltf(url); // gltf
  isTimer = true;
  scene.add(model);
  renderer.render(scene, camera) // 更新渲染器

  // 不加定时器,进度条像没效果一样,不到100%就消失了
  if (isTimer) {
    let timer = setTimeout(() => {
      Bus.$emit('ModelPreview-modelLoading', false);
      modelAnimate(); // 渲染循环更新控制器
      clearTimeout(timer);
    }, 10);
  } else {
    modelAnimate();
  }
}

加载模型 gltf

const loadGltf = (url) => {
  return new Promise((resolve, reject) => {
    const dracoLoader = new DRACOLoader()
    dracoLoader.setDecoderPath('/draco/gltf/')
    // dracoLoader.setDecoderPath('/draco/')
    dracoLoader.preload()
    const loader = new GLTFLoader()
    loader.setDRACOLoader(dracoLoader).load(url, (gltf) => { // 模型文件这里用的线上的
      const gltfScene = gltf.scene

      // 修改模型材质
      gltfScene.traverse(function (child) {
        if (child.isMesh) {
          child.material.side = THREE.DoubleSide // 2 双面材质
          child.frustumCulled = false; // 即使物体中心点看不到也不消失
        }
      });

      setModelProperties(gltfScene)
      resolve(gltfScene)
    }, onProgress, function (xhr) { reject(xhr) })
  });
}

加载模型 obj

// 加载 obj 模型
let loadModel = (name) => {
    let path = '/models/OHS/', mtlPath = 'OHS.mtl', objPath = 'OHS.obj'; // 模型文件这里用的本地的
    // http://www.webgl3d.cn/Three.js/
    // 加载模型的 mtl 和 obj 数据
    return new Promise((resolve, reject)=>{
        // 加载 mtl
        new THREE.MTLLoader().setPath(path).load(mtlPath, function (materials) {
            materials.preload();
            // 加载 obj
            new THREE.OBJLoader().setMaterials(materials).setPath(path).load(objPath, function (object) {
                setModelProperties(object)
                resolve(object)
            }, onProgress, function (xhr) { reject(xhr) })
        });
    });
}

加载进度

// 进度通知
function onProgress (xhr) {
  let percentComplete = xhr.loaded / xhr.total * 100;
  percentComplete = Math.round( percentComplete, 2 )
  Bus.$emit('ModelPreview-modelProgress', percentComplete)
};

设置模型

// 设置模型旋转中心点
function setModelProperties (model) {
  // 设置旋转中心点 gltf
  model.children[0].children.forEach(item => {
    item.geometry.computeBoundingBox();
    item.geometry.center()
  })
  
  // 设置旋转中心点 obj
  // object.children[0].geometry.computeBoundingBox();
  // object.children[0].geometry.center()

  // 位置
  model.position.x = 0
  model.position.y = 0
  model.position.z = 0
  // 旋转
  model.rotation.x = 0
  model.rotation.y = 0
  model.rotation.z = 0
  // 缩放
  model.scale.x = 1
  model.scale.y = 1
  model.scale.z = 1
}

动画

// 时刻渲染/渲染循环 当程序运行时,如果你想要移动或者改变任何场景中的东西,都必须要经过这个动画循环。
const modelAnimate = function () {
  // 使模型切换时,重新从正面开始旋转。单纯 controls.update() ,会从上一个模型旋转的角度继续旋转
  let ohsClock = new THREE.Clock(); // 获取跟踪时间
  let delta = ohsClock.getDelta(); // 获取当前秒数

  controls.update(delta)
  renderer.render(scene, camera)
  animationId = requestAnimationFrame(modelAnimate) // 下一帧执行代码
}

// 停止动画渲染循环,清除场景数据(环境光保留)
const clearSatelliteAnimation = () => {
  window.cancelAnimationFrame(animationId)
  if (scene) {
    scene.clear() // 连灯光等一并清除
    scene.add(ambientLight) // 环境光保留
  }
}

// 页面卸载,停止动画渲染循环,清除场景数据,init置为false。正常进入页面重新渲染卫星模型
const destroyAnimation = () => {
  window.cancelAnimationFrame(animationId)
  if (scene) {
    scene.clear() // 连灯光等一并清除
    scene.dispose() // 清除渲染器 所缓存的场景相关的数据。
    init = false
  }
}

export default {
  initSatellite,
  changeSatellite,
  clearSatelliteAnimation,
  destroyAnimation
} 

模型查看器

可在另一个程序中打开模型进行查看,例如three.js提供的编辑器 three.js editor,或者glTF Viewer,以及babylonjs来预览模型。