demo address
1. Demand background
The company used to need its own internal model library and the function of model preview, so it found a model preview plug-in, but found that the animation could not be previewed, so it specially solved this problem.
Disadvantages: After developing this function, I feel that it is redundant because it is packaged and exported together with the scene, so a single fbx is useless, and the network loading of a larger fbx consumes cloud storage traffic and the loading speed is slow, which is not ideal. Reading locally fastest. Finally I localized the backend.
This method is limited to fbx because I only modified the reading code of fbx
2. Implementation method
Install the model preview plug-in
cnpm i online-3d-viewer -S
Plug-in usage documentation
Encapsulate a model component
The implementation principle has been written below. The bottom layer of this plug-in is threejs, so my first reaction should be that it can play animations like mixamo. However, the obtained model object does not have the animations attribute. In fact, the plug-in has re-adjusted the imported model. If the animation is lost, you only need to retrieve the parsed fbx model when reading and use threejs to play the animation.
<!-- * @Author: alpaca * @Date: 2023-10-31 14:21:49 * @LastEditors: alpaca * @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 ? 'Exit' : 'Full screen' }}</span> <div class="animations-group" v-if="animations.length>0 & amp; & amp;!hide" > <el-button @click="stopAnimation">Stop</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 model loading...</p> </div> </div> </template> <script> // The bottom layer of this library is actually 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, // width and height of Div before full screen originalWidth: null, originalHeight: null, // animation clip animations: [], mixer: null, hide: false, // update function update: null, clock: new THREE.Clock(), }; }, methods: { // full screen function fullScreenTable() { let element = document.querySelector("#model-viewer"); this.originalWidth = element.clientWidth; this.originalHeight = element.clientHeight; this.launchIntoFullscreen(element); }, // Compatible with all browsers launchIntoFullscreen(element) { // Exit Full Screen 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 { // Check whether it is still in full screen. If not, return the viewer's canvas size, otherwise the field of view position will change. this.interval = setInterval(() => { if ( document.fullscreenElement != document.querySelector("#model-viewer") ) { this.fullScreen = false; this.$nextTick(() => { //Reset canvas size this.viewer.viewer.Resize( this.originalWidth, this.originalHeight ); }); clearInterval(this.interval); } }, 100); // full screen 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; } }, //Model initialization init() { //Set the model code to determine whether it is local or url const setFbx = () => { this.hide = true; switch (this.type) { case "file": this.viewer.LoadModelFromFileList([this.file]); break; default: this.viewer.LoadModelFromUrlList([this.url]); break; } }; // If the scene has been generated, you only need to remove the original fbx on the scene to reduce the initialization overhead. 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(() => { // Under which div it is generated? let parentDiv = document.getElementById("model-viewer"); //Initialize the previewer this.viewer = new OV.EmbeddedViewer(parentDiv, { // background color backgroundColor: new OV.RGBAColor(51, 51, 51, 255), // When the model is loaded onModelLoaded: () => { let { renderer, camera, scene } = this.viewer.viewer; // After the model is processed by the plug-in, the model material display will change and the animation will be lost. let model = this.viewer.viewer.mainModel.mainModel.rootObject; // Unprocessed model Only from this can we get the animation of the model let original = this.viewer.model.originalModel; // We only use the original model when there is animation on the model, otherwise we use the processed model. if (original.animations.length > 0) { //Essentially, they are all objects generated by threejs, and you can use its methods. // Remove the rendered model and add the animated model back to the scene scene.remove(model); scene.add(original); // Get the animation clip of the model const clips = original.animations; this.animations = clips; // Generate an animation mixer on this model, otherwise the animation cannot be played const mixer = new THREE.AnimationMixer(original); //Record this mixer to facilitate subsequent changes to animations and stopping animations this.mixer = mixer; // Render and update the scene to ensure animation playback this.update = () => { if (!this.mixer) return; requestAnimationFrame(this.update); mixer.update(this.clock.getDelta()); renderer.render(scene, camera); }; this.update(); } this.hide = false; }, }); ///In changing the source code, it turns out that our sentence = this.viewer.model.originalModel cannot get the originally loaded model object. // Modify the fbx loader. When reading the model, save a parsed original model to the model loader so that we can get whether there is animation later. 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 sentence is probably useless. I forgot what I used it for after I wrote it. this.detaultAspect = this.viewer.viewer.camera.aspect; setFbx(); }, 100); }, // Stop animation stopAnimation() { this.mixer & amp; & amp; this.mixer.stopAllAction(); }, // play animation playAnimation(clip) { this.mixer & amp; & amp; this.mixer.clipAction(clip).play(); }, // clear 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>
Use this packaged component
<!-- * @Author: alpaca * @Date: 2023-10-27 16:15:23 * @LastEditors: alpaca * @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" >Click to upload model file</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>
The function of previewing the model and playing animation is completed.