vue实现读取本地模型文件 在线预览其动画

demo地址

一、需求背景

        公司之前需要自己的内部模型库,需要模型预览的功能,所以找了一个模型预览插件,但是发现预览不了动画,所以特地解决了这个问题。

        缺点:开发完这个功能, 又觉得多余了,因为都是和场景一起打包导出的,所以单个fbx没啥用,而且大一点的fbx 网络加载耗费云存储流量 且 加载速度慢 不太理想,读本地时最快的。最后我把后端都本地化了。

      此方法仅限于fbx 因为我只修改了fbx的读取代码

二、实现方式

   安装模型预览插件

cnpm i online-3d-viewer -S

    插件使用文档

    封装一个模型组件

      实现原理已经写在下面注释了 这个插件的底层是threejs 所以我第一反应应该是可以和mixamo那样播放动画的 但是获取到的model对象  里面没有animations这个属性 其实是插件又重新调整了一下导入的模型 导致动画丢失 只需在读取的时候找回解析好的fbx模型 用threejs播放动画的形式即可

<!--
 * @Author: 羊驼
 * @Date: 2023-10-31 14:21:49
 * @LastEditors: 羊驼
 * @LastEditTime: 2023-10-31 15:08:56
 * @Description: file content
-->
<template>
  <div
    class="root"
    style="width: 100%; height: 100%; margin-top:10px"
  >
    <div
      class="box"
      id="model-viewer"
      :class="fullScreen ? 'fullbox' : 'exitfullbox'"
    >
      <span
        v-if="viewer != 'init'"
        class="fullscreen"
        @click="fullScreenTable"
      >{{
        fullScreen ? '退出' : '全屏'
      }}</span>
      <div
        class="animations-group"
        v-if="animations.length>0&&!hide"
      >
        <el-button @click="stopAnimation">停止</el-button>
        <el-button
          v-for="item in animations"
          :key="item.name"
          @click="playAnimation(item)"
        >{{item.name}}</el-button>
      </div>
      <p
        v-if="viewer == 'init'"
        style="line-height: 70vh; text-align: center"
      >3D模型加载中...</p>
    </div>
  </div>
</template>

<script>
// 此库底层其实就是threeJS
import * as OV from "online-3d-viewer";
import * as THREE from "three";

export default {
  props: ["type", "file", "url"],
  data() {
    return {
      viewer: null,
      fullScreen: false,
      interval: null,
      detaultAspect: 0,
      // 全屏前Div的宽高
      originalWidth: null,
      originalHeight: null,
      // 动画片段
      animations: [],
      mixer: null,
      hide: false,
      // 更新函数
      update: null,
      clock: new THREE.Clock(),
    };
  },
  methods: {
    // 全屏功能
    fullScreenTable() {
      let element = document.querySelector("#model-viewer");
      this.originalWidth = element.clientWidth;
      this.originalHeight = element.clientHeight;
      this.launchIntoFullscreen(element);
    },
    // 兼容各浏览器
    launchIntoFullscreen(element) {
      // 退出全屏
      if (this.fullScreen) {
        if (document.exitFullscreen) {
          document.exitFullscreen();
        } else if (document.webkitCancelFullScreen) {
          document.webkitCancelFullScreen();
        } else if (document.mozCancelFullScreen) {
          document.mozCancelFullScreen();
        } else if (document.msExitFullscreen) {
          document.msExitFullscreen();
        }
        this.fullScreen = false;
      } else {
        // 检测是否还在全屏的情况 不是的话就回复 viewer的画布大小 不然视野位置会变化
        this.interval = setInterval(() => {
          if (
            document.fullscreenElement !=
            document.querySelector("#model-viewer")
          ) {
            this.fullScreen = false;
            this.$nextTick(() => {
              // 重设画布大小
              this.viewer.viewer.Resize(
                this.originalWidth,
                this.originalHeight
              );
            });
            clearInterval(this.interval);
          }
        }, 100);
        // 全屏
        if (element.requestFullscreen) {
          element.requestFullscreen();
        } else if (element.mozRequestFullScreen) {
          element.mozRequestFullScreen();
        } else if (element.webkitRequestFullscreen) {
          element.webkitRequestFullscreen();
        } else if (element.msRequestFullscreen) {
          element.msRequestFullscreen();
        }
        this.fullScreen = true;
      }
    },
    // 模型初始化
    init() {
      // 设置模型代码 判断是本地的还是url的
      const setFbx = () => {
        this.hide = true;
        switch (this.type) {
          case "file":
            this.viewer.LoadModelFromFileList([this.file]);
            break;
          default:
            this.viewer.LoadModelFromUrlList([this.url]);
            break;
        }
      };
      // 如果已经生成了场景 只需要移除原来在场景上的fbx 减少初始化的开销
      if (this.viewer != null) {
        this.viewer.viewer.cameraMode = 2;
        let { scene } = this.viewer.viewer;
        if (scene.children.length > 3) {
          scene.remove(scene.children[3]);
        }
        return setFbx();
      }

      this.viewer = "init";

      setTimeout(() => {
        // 生成在哪个div下
        let parentDiv = document.getElementById("model-viewer");

        // 初始化预览器
        this.viewer = new OV.EmbeddedViewer(parentDiv, {
          // 背景颜色
          backgroundColor: new OV.RGBAColor(51, 51, 51, 255),
          // 当模型加载完毕
          onModelLoaded: () => {
            let { renderer, camera, scene } = this.viewer.viewer;
            // model是 经过插件处理以后 模型材质显示会变化 并且丢失动画
            let model = this.viewer.viewer.mainModel.mainModel.rootObject;
            // 未经过处理的模型 只有这上面我们才能拿到模型的动画
            let original = this.viewer.model.originalModel;

            // 当模型身上有动画的话 我们才使用原来的模型 不然就使用处理后的模型
            if (original.animations.length > 0) {
              // 本质上其实都是threejs生成的对象 可以进行使用它身上的方法
              // 移除渲染后的模型 添加带动画的模型回到场景
              scene.remove(model);
              scene.add(original);
              // 获取模型的动画片段
              const clips = original.animations;
              this.animations = clips;
              // 生成一个动画混合器在这个模型身上 不然无法播放动画
              const mixer = new THREE.AnimationMixer(original);
              // 记录这个混合器 方便后续更改动画以及停止动画
              this.mixer = mixer;
              // 渲染 更新场景 保证动画的播放
              this.update = () => {
                if (!this.mixer) return;
                requestAnimationFrame(this.update);
                mixer.update(this.clock.getDelta());
                renderer.render(scene, camera);
              };
              this.update();
            }
            this.hide = false;
          },
        });
        ///更改源码中 原来我们这一句 = this.viewer.model.originalModel 是拿不到原来加载的模型对象的
        // 修改fbx加载器中 读取模型时 保存一份解析完毕的原始模型到模型加载器上 这样我们后续才能拿到有无动画
        let original =
          this.viewer.modelLoader.importer.importers[11].OnThreeObjectsLoaded;
        this.viewer.modelLoader.importer.importers[11].OnThreeObjectsLoaded =
          function (loadedObject, onFinish) {
            this.GetMainObject = (loadedObject) => {
              return loadedObject;
            };
            this.model.originalModel = loadedObject;
            original.call(this, loadedObject, onFinish);
          };
        // 这句应该没啥用 写了忘记拿来干嘛了
        this.detaultAspect = this.viewer.viewer.camera.aspect;
        setFbx();
      }, 100);
    },
    // 停止动画
    stopAnimation() {
      this.mixer && this.mixer.stopAllAction();
    },
    // 播放动画
    playAnimation(clip) {
      this.mixer && this.mixer.clipAction(clip).play();
    },
    // 清除
    clear() {
      clearInterval(this.interval);
      if (this.viewer) {
        this.viewer.Destroy();
      }
      this.viewer = null;
      this.mixer = null;
      this.animations = [];
      this.interval = null;
    },
  },
  destroyed() {
    this.clear();
  },
};
</script>

<style>
.animations-group {
  position: absolute;
  right: 10px;
  top: 10px;
  display: flex;
  flex-direction: column;
}
.animations-group .el-button {
  margin-left: 0 !important;
  margin-bottom: 10px;
  width: 200px;
}
@media (max-width: 767.98px) {
  .fbx-box {
    width: 96% !important;
    height: 60vh !important;
    margin-bottom: 20px;
  }

  .root,
  .box {
    height: 70vh !important;
  }
}

.box {
  background-color: #333;
  font-size: 30px;
  font-weight: bold;
  color: #fff;
  position: relative;
  flex: 1;
}

.fullscreen {
  font-size: 16px;
  font-weight: normal;
  position: absolute;
  left: 30px;
  bottom: 30px;
  user-select: none;
  cursor: pointer;
}

.fullscreen:hover {
  color: greenyellow;
}

.fullbox {
  width: 100vw !important;
  height: 100% !important;
}

.exitfullbox {
  width: 100% !important;
  height: 100% !important;
}
</style>

使用这个封装的组件

<!--
 * @Author: 羊驼
 * @Date: 2023-10-27 16:15:23
 * @LastEditors: 羊驼
 * @LastEditTime: 2023-10-31 14:45:43
 * @Description: file content
-->
<template>
  <div id="app">
    <el-upload
      class="upload-demo"
      action="https://jsonplaceholder.typicode.com/posts/"
      :auto-upload="false"
      :limit="1"
      :on-change="initModel"
      accept=".fbx"
      :show-file-list="false"
    >
      <el-button
        size="small"
        type="primary"
      >点击上传模型文件</el-button>
    </el-upload>
    <fbx-viewer
      :type="type"
      :file="file.raw"
      ref="model"
      v-if="file"
    />
  </div>
</template>

<script>
import FbxViewer from "./components/FbxViewer.vue";
export default {
  components: { FbxViewer },
  name: "App",
  data() {
    return {
      type: "file",
      file: null,
    };
  },
  methods: {
    initModel(file) {
      if (file != this.file) {
        this.file = file;
        this.$nextTick(() => {
          this.$refs.model.init();
        });
      }
    },
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

预览模型 并且额可以播放动画的功能就完成了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现Vue中的Word模板在线预览本地文件,可以借助于第三方库`docxtemplater`和`jszip`。具体步骤如下: 1. 安装`docxtemplater`和`jszip`库: ```bash npm install docxtemplater jszip --save ``` 2. 在Vue组件中引入这两个库: ```javascript import Docxtemplater from 'docxtemplater'; import JSZip from 'jszip'; ``` 3. 在Vue组件中添加一个文件上传的input框和一个用于显示Word文档内容的div: ```html <template> <div> <input type="file" ref="fileInput" @change="onFileChange"> <div ref="wordContent"></div> </div> </template> ``` 4. 在Vue组件的methods中编写处理文件上传和Word模板预览的方法: ```javascript methods: { onFileChange() { const file = this.$refs.fileInput.files[0]; const reader = new FileReader(); reader.onload = () => { this.previewWord(reader.result); }; reader.readAsArrayBuffer(file); }, previewWord(data) { JSZip.loadAsync(data).then((zip) => { const doc = new Docxtemplater().loadZip(zip); const result = doc.getZip().generate({type: 'blob'}); const wordContent = this.$refs.wordContent; wordContent.innerHTML = ''; const fileReader = new FileReader(); fileReader.onloadend = () => { const content = fileReader.result; const iframe = document.createElement('iframe'); iframe.src = `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${btoa(content)}`; iframe.width = '100%'; iframe.height = '800px'; wordContent.appendChild(iframe); }; fileReader.readAsBinaryString(result); }); }, } ``` 这段代码的作用是: - 当用户选择文件后,将文件读取为二进制数组,并调用`previewWord`方法进行预览。 - 在`previewWord`方法中,使用`JSZip`将Word文档的二进制数组解压缩,然后使用`docxtemplater`加载Word文档模板。 - 调用`docxtemplater`的`getZip()`方法生成Word文档,并将其转换为二进制字符串。 - 将二进制字符串转换为base64编码,生成一个iframe元素,并将其作为子元素添加到Vue组件中用于显示Word内容的div中。 这样,当用户上传一个Word文档后,就可以在Vue组件中预览该文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值