فهرست منبع

3d模型优化

dsy 2 هفته پیش
والد
کامیت
910046ce9e

+ 1 - 1
.env

@@ -11,4 +11,4 @@ VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'
 VUE_APP_EepServer = '/EEPServer/SI'
 
 #version
-VUE_APP_VERSION = '2025.12.24'
+VUE_APP_VERSION = '2025.12.26'

+ 1 - 2
main.js

@@ -70,7 +70,7 @@ ipcMain.on('install-update', (event, filePath) => {
   }
 
   if (process.platform === 'win32') {
-    // Windows:直接执行安装程序(根据你的安装包参数添加静默/等待参数)
+    // Windows:直接执行安装程序(根据安装包参数添加静默/等待参数)
     try {
       spawn(filePath, [], { detached: true, stdio: 'ignore' }).unref();
       app.quit();
@@ -81,7 +81,6 @@ ipcMain.on('install-update', (event, filePath) => {
     // macOS:打开 dmg 或者打开 .pkg
     shell.openPath(filePath).then(() => app.quit());
   } else {
-    // linux:按照需要实现
     shell.openPath(filePath).then(() => app.quit());
   }
 });

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "eep_page",
-  "version": "2025.12.24",
+  "version": "2025.12.26",
   "private": true,
   "main": "main.js",
   "description": "智慧梧桐数字教材编辑器",

+ 1 - 1
src/App.vue

@@ -64,7 +64,7 @@ export default {
       });
       await window.fileAPI.downloadFile(this.client_download_url, downloadDir);
       this.loadingInstance.text = '下载完成,正在安装更新...';
-      await window.updateAPI.installUpdate(downloadDir);
+      await window.updateAPI.installUpdate(downloadDir); // 调用安装更新的API
       this.loadingInstance.close();
     },
   },

+ 12 - 0
src/api/app.js

@@ -87,11 +87,23 @@ export function GetBookWebSIContent(MethodName, data) {
 
 /**
  * 解压 H5 游戏包并创建启动文件
+ * @param {object} data 请求数据
+ * @param {string} data.file_id 文件 ID
+ * @param {string} data.index_file_name 启动文件名称
  */
 export function H5StartupFile(data) {
   return http.post(`/FileServer/SI?MethodName=file_store_manager-UnzipH5GamePackCreateStartupFile`, data);
 }
 
+/**
+ * 解压 3D 模型包并创建启动文件
+ * @param {object} data 请求数据
+ * @param {string} data.file_id 文件 ID
+ */
+export function Unzip3DModelPackCreateStartupFile(data) {
+  return http.post(`/FileServer/SI?MethodName=file_store_manager-Unzip3DModelPackCreateStartupFile`, data);
+}
+
 export function LearnWebSI(MethodName, data) {
   return http.post(`/GCLSLearnWebSI/ServiceInterface?MethodName=${MethodName}`, data);
 }

+ 20 - 15
src/views/book/courseware/create/components/base/3d_model/3DModel.vue

@@ -23,7 +23,9 @@
 <script>
 import ModuleMixin from '../../common/ModuleMixin';
 import UploadFile from '../common/UploadFile.vue';
+
 import { get3DModelData } from '@/views/book/courseware/data/3dModel';
+import { Unzip3DModelPackCreateStartupFile } from '@/api/app';
 
 export default {
   name: 'ThreeModelPage',
@@ -33,31 +35,34 @@ export default {
     return {
       data: get3DModelData(),
       labelText: '3D模型',
-      acceptFileType: '.fbx,.obj,.gltf,.glb,.mtl',
+      acceptFileType: '.fbx,.gltf,.glb,.zip',
       iconClass: '3d',
       limit: 1,
     };
   },
   methods: {
     updateFileList({ file_list, file_id_list }) {
-      if (file_list.length > 0) {
-        let suffixName =
-          'name' in file_list[0]
-            ? file_list[0].name.split('.').pop().toLowerCase()
-            : file_list[0].file_name.split('.').pop().toLowerCase();
-        if (suffixName === 'obj') {
-          this.limit = 2;
-        } else {
-          this.limit = 1;
-        }
-      }
-
       this.data.model_list = file_list;
       this.data.model_id_list = file_id_list;
       this.data.file_id_list = file_id_list;
+
+      if (file_id_list.length === 0) return;
+
+      let suffix =
+        'name' in file_list[0]
+          ? file_list[0].name.split('.').pop().toLowerCase()
+          : file_list[0].file_name.split('.').pop().toLowerCase();
+      if (suffix === 'zip') {
+        Unzip3DModelPackCreateStartupFile({ file_id: file_id_list[0] }).then(
+          ({ mtl_file_id, mtl_file_url, obj_file_id, obj_file_url }) => {
+            this.data.mtl_info.mtl_file_id = mtl_file_id;
+            this.data.mtl_info.mtl_file_url = mtl_file_url;
+            this.data.obj_info.obj_file_id = obj_file_id;
+            this.data.obj_info.obj_file_url = obj_file_url;
+          },
+        );
+      }
     },
   },
 };
 </script>
-
-<style lang="scss" scoped></style>

+ 2 - 2
src/views/book/courseware/create/components/base/common/UploadFile.vue

@@ -328,8 +328,8 @@ export default {
         fileType = ['zip', 'html'];
         typeTip = 'H5游戏文件只能是 zip、html 格式!';
       } else if (this.type === '3DModel') {
-        fileType = ['fbx', 'obj', 'gltf', 'glb', 'mtl'];
-        typeTip = '3D模型文件只能是 fbx、obj、gltf、glb、mtl 格式!';
+        fileType = ['fbx', 'zip', 'gltf', 'glb'];
+        typeTip = '3D模型文件只能是 fbx、gltf、glb、zip 格式!';
       }
       const isNeedType = fileType.includes(suffix);
       if (!isNeedType) {

+ 8 - 0
src/views/book/courseware/data/3dModel.js

@@ -12,6 +12,14 @@ export function get3DModelData() {
     title: '3D模型',
     min_height: 220,
     property: get3DModelProperty(),
+    mtl_info: {
+      mtl_file_id: '',
+      mtl_file_url: '',
+    },
+    obj_info: {
+      obj_file_id: '',
+      obj_file_url: '',
+    },
     model_list: [],
     model_id_list: [],
     mind_map: {

+ 50 - 61
src/views/book/courseware/preview/components/3d_model/3DModelPreview.vue

@@ -25,7 +25,6 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
 
 import { get3DModelData } from '@/views/book/courseware/data/3dModel';
 import { toggleFullScreen } from '@/utils/common';
-import { GetFileStoreInfo } from '@/api/app';
 
 export default {
   name: 'ThreeModelPreview',
@@ -33,11 +32,12 @@ export default {
   data() {
     return {
       data: get3DModelData(),
-      scene: null,
-      camera: null,
-      renderer: null,
-      controls: null,
-      animationId: null,
+      scene: null, // 三维场景
+      camera: null, // 相机
+      mixer: null, // 动画混合器
+      renderer: null, // 渲染器
+      controls: null, // 轨道控制器
+      animationId: null, // 动画帧ID
       isFullScreen: false, // 是否全屏
       loaded: false, // 是否加载完成
     };
@@ -81,40 +81,40 @@ export default {
       // 创建相机
       const width = container.clientWidth;
       const height = container.clientHeight;
-      this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
-      this.camera.position.set(5, 5, 5);
+      this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); // 视角75度
+      this.camera.position.set(5, 5, 5); // 设置相机位置
 
       // 创建渲染器
-      this.renderer = new THREE.WebGLRenderer({ antialias: true });
+      this.renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿
       this.renderer.setSize(width, height);
-      this.renderer.shadowMap.enabled = true;
-      this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+      this.renderer.shadowMap.enabled = true; // 启用阴影映射
+      this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 使用柔和阴影
 
       // 启用颜色管理和正确的色彩空间
-      this.renderer.outputColorSpace = THREE.SRGBColorSpace;
-      this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
-      this.renderer.toneMappingExposure = 1.0;
+      this.renderer.outputColorSpace = THREE.SRGBColorSpace; // 设置输出色彩空间
+      this.renderer.toneMapping = THREE.ACESFilmicToneMapping; // 使用ACES电影色调映射
+      this.renderer.toneMappingExposure = 1.0; // 曝光度
 
-      container.appendChild(this.renderer.domElement);
+      container.appendChild(this.renderer.domElement); // 将渲染器添加到DOM
 
       // 添加光源
-      const ambientLight = new THREE.AmbientLight(0x404040, 1.0); // 增加环境光强度
-      this.scene.add(ambientLight);
+      const ambientLight = new THREE.AmbientLight(0xffffff); // 环境光
+      this.scene.add(ambientLight); // 添加环境光到场景
 
       const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); // 增加方向光强度
-      directionalLight.position.set(10, 10, 5);
-      directionalLight.castShadow = true;
-      this.scene.add(directionalLight);
+      directionalLight.position.set(10, 10, 5); // 设置光源位置
+      directionalLight.castShadow = true; // 启用阴影投射
+      this.scene.add(directionalLight); // 添加方向光到场景
 
       // 添加额外的填充光
-      const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
-      fillLight.position.set(-10, -10, -5);
-      this.scene.add(fillLight);
+      const fillLight = new THREE.DirectionalLight(0xffffff, 0.5); // 较低强度的填充光
+      fillLight.position.set(-10, -10, -5); // 设置填充光位置
+      this.scene.add(fillLight); // 添加填充光到场景
 
       // 添加控制器
       this.controls = new OrbitControls(this.camera, this.renderer.domElement);
-      this.controls.enableDamping = true;
-      this.controls.dampingFactor = 0.25;
+      this.controls.enableDamping = true; // 启用阻尼效果
+      this.controls.dampingFactor = 0.25; // 阻尼系数
 
       // 开始渲染循环
       this.animate();
@@ -128,12 +128,11 @@ export default {
         return;
       }
 
-      const fileStore = await GetFileStoreInfo({ file_id: this.data.model_list[0].file_id });
-      if (!fileStore || fileStore.error) {
-        console.error('模型文件加载失败:', fileStore);
+      const modelUrl = this.data.file_list[0].file_url;
+      if (!modelUrl) {
+        console.error('模型文件URL不存在');
         return;
       }
-      const modelUrl = fileStore.file_url;
       // 根据文件扩展名选择合适的加载器
       const extension = modelUrl.split('.').pop().toLowerCase();
       let loader = null;
@@ -148,9 +147,9 @@ export default {
           loader = new FBXLoader();
           this.loadFBX(loader, modelUrl);
           break;
-        case 'obj':
+        case 'zip':
           loader = new OBJLoader();
-          this.loadOBJ(loader, modelUrl);
+          this.loadOBJ(loader, this.data.obj_info.obj_file_url);
           break;
         default:
           console.error('不支持的模型格式:', extension);
@@ -240,16 +239,13 @@ export default {
 
     async loadOBJ(loader, url) {
       // 首先尝试加载MTL材质文件
-      let mtlUrl = '';
-      const mtlLoader = new MTLLoader();
-      if (this.data.model_list.length > 1) {
-        const fileStore = await GetFileStoreInfo({ file_id: this.data.model_list[1].file_id });
-        if (!fileStore || fileStore.error) {
-          console.error('mtl 材质文件加载失败:', fileStore);
-        } else {
-          mtlUrl = fileStore.file_url;
-        }
+      let mtlUrl = this.data.mtl_info.mtl_file_url;
+      if (!mtlUrl) {
+        // 如果没有提供MTL文件URL,直接加载OBJ模型
+        this.loadOBJModel(loader, url);
+        return;
       }
+      const mtlLoader = new MTLLoader();
 
       // 设置材质加载路径
       const basePath = mtlUrl.substring(0, mtlUrl.lastIndexOf('/') + 1);
@@ -262,10 +258,9 @@ export default {
       mtlLoader.load(
         mtlFileName,
         (materials) => {
-          // 材质文件加载成功
-          materials.preload();
-          loader.setMaterials(materials);
-          this.loadOBJModel(loader, url, true);
+          materials.preload(); // 预加载
+          loader.setMaterials(materials); // 将材质应用到OBJ加载器
+          this.loadOBJModel(loader, url);
         },
         undefined,
         (error) => {
@@ -280,26 +275,11 @@ export default {
      * 加载OBJ模型并应用默认材质
      * @param {OBJLoader} loader - OBJ加载器
      * @param {string} url - OBJ文件URL
-     * @param {boolean} hasMaterials - 是否有材质文件
      */
-    loadOBJModel(loader, url, hasMaterials = false) {
+    loadOBJModel(loader, url) {
       loader.load(
         url,
         (obj) => {
-          let colorList = [0xff6666, 0x66ff66, 0x6666ff, 0xffff66, 0x66ffff, 0xff66ff]; // 可自定义颜色列表
-          let meshIndex = 0;
-          obj.traverse((child) => {
-            if (child.isMesh && !hasMaterials) {
-              // 为每个 mesh 设置自定义颜色材质
-              child.material = new THREE.MeshPhongMaterial({
-                color: colorList[meshIndex % colorList.length], // 循环使用颜色
-                shininess: 30,
-              });
-              meshIndex += 1;
-              child.castShadow = true;
-              child.receiveShadow = true;
-            }
-          });
           this.addModelToScene(obj);
         },
         (progress) => {
@@ -313,6 +293,10 @@ export default {
       );
     },
 
+    /**
+     * 将模型添加到场景中并调整相机位置
+     * @param {THREE.Object3D} model - 3D模型对象
+     */
     addModelToScene(model) {
       // 计算模型的包围盒,用于自动调整相机位置
       const box = new THREE.Box3().setFromObject(model);
@@ -353,6 +337,9 @@ export default {
       }
     },
 
+    /**
+     * 窗口大小变化处理函数
+     */
     onWindowResize() {
       const container = this.$refs.three;
       this.isFullScreen = document.fullscreenElement !== null;
@@ -403,6 +390,9 @@ export default {
       }
     },
 
+    /**
+     * 切换全屏模式
+     */
     handleToggleFullScreen() {
       if (!this.$refs.three) return;
       if (!this.loaded) {
@@ -410,7 +400,6 @@ export default {
         return;
       }
       toggleFullScreen(this.$refs.three);
-      // 延迟一帧来确保DOM已经更新
       this.$nextTick(() => {
         setTimeout(() => {
           this.onWindowResize();