Web端3D模型加载实战指南_Vue3_uni-app_HTML

前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码)

摘要:想在网页里展示一个可旋转、可缩放的 3D 商品模型?本文手把手带你用 原生 HTML 、Vue3 、uni-app(H5 / App / 微信小程序) 三种技术栈加载 glTF / GLB 模型,覆盖 <model-viewer> 零代码方案、three.js 全自定义方案、renderjs 跨端方案与小程序 XR-Frame 方案,并附上模型压缩、资源释放、白名单配置与各端踩坑清单。所有代码均可直接复制运行。


### 文章目录

  • [前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码)](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [@toc](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [一、开篇:先想清楚这 3 件事,能省 80% 的返工](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [1.1 模型格式怎么选](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [1.2 技术栈怎么选](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [1.3 模型从哪来 / 怎么瘦身](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [二、原生 HTML:10 行代码跑起来](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [2.1 方案 A:`<model-viewer>`(最省事)](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [2.2 方案 B:原生 three.js(完全可控)](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [三、Vue3:封装一个可复用的 `<ModelViewer>` 组件](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [3.1 安装](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [3.2 组件源码](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [3.3 使用](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [3.4 Vue 里的 4 个高频坑](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [四、uni-app:一套代码,三个端,三种写法](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [4.1 目录与条件编译组织](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [4.2 H5 / App 端:renderjs + three.js](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [4.3 微信小程序端:XR-Frame(官方推荐)](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [4.4 小程序备选:threejs-miniprogram](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [五、性能优化清单(上线前逐条对照)](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [六、常见报错速查表](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [七、总结:一句话选型](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)
  • [附录:发布到 CSDN 的小提示](#文章目录 前端加载 3D 模型全攻略:Vue3 / uni-app / 原生 HTML 三种姿势(含完整源码) @[toc] 一、开篇:先想清楚这 3 件事,能省 80% 的返工 1.1 模型格式怎么选 1.2 技术栈怎么选 1.3 模型从哪来 / 怎么瘦身 二、原生 HTML:10 行代码跑起来 2.1 方案 A:<model-viewer>(最省事) 2.2 方案 B:原生 three.js(完全可控) 三、Vue3:封装一个可复用的 <ModelViewer> 组件 3.1 安装 3.2 组件源码 3.3 使用 3.4 Vue 里的 4 个高频坑 四、uni-app:一套代码,三个端,三种写法 4.1 目录与条件编译组织 4.2 H5 / App 端:renderjs + three.js 4.3 微信小程序端:XR-Frame(官方推荐) 4.4 小程序备选:threejs-miniprogram 五、性能优化清单(上线前逐条对照) 六、常见报错速查表 七、总结:一句话选型 附录:发布到 CSDN 的小提示)

一、开篇:先想清楚这 3 件事,能省 80% 的返工

很多同学一上来就问"uni-app 怎么加载 3D 模型",结果折腾三天发现方案根本不适用于自己的端。动手前先把下面这张表看完。

1.1 模型格式怎么选

格式 说明 单文件 动画 PBR 材质 推荐度
.glb glTF 的二进制版本,几何体/材质/贴图/动画全部打包进一个文件 ✅ ✅ ✅ ⭐⭐⭐⭐⭐ 首选
.gltf glTF 的 JSON 版,贴图等资源是外链文件 ❌ ✅ ✅ ⭐⭐⭐⭐
.obj 老牌格式,不含动画和高级材质,需配合 .mtl ❌ ❌ ❌ ⭐⭐
.fbx 动画能力强,但体积大、解析慢,Web 端不友好 ✅ ✅ 部分 ⭐⭐
.stl 只有几何体,无材质无贴图,工业/医疗常用 ✅ ❌ ❌ ⭐⭐(特定场景)

结论:Web 端一律优先用 .glb。 它是 Khronos 官方标准,被 three.js、model-viewer、微信小程序 XR-Frame 等所有主流方案原生支持,且是单文件,不涉及跨域、相对路径、资源丢失这些烦心事。

1.2 技术栈怎么选

你的场景 推荐方案 难度
官网/落地页/活动页,只想快速展示 <model-viewer> ⭐
PC 后台/数据可视化大屏,需要精细控制 Vue3 + three.js ⭐⭐⭐
uni-app 只发 H5 直接 three.js(同 Vue3 方案) ⭐⭐⭐
uni-app 发 App(Android/iOS) renderjs + three.js ⭐⭐⭐⭐
uni-app 发 微信小程序 XR-Frame(官方)或 threejs-miniprogram ⭐⭐⭐⭐
小程序 + App + H5 都要 条件编译,各端各写一套 ⭐⭐⭐⭐⭐

1.3 模型从哪来 / 怎么瘦身

免费模型资源站:Sketchfab、Poly Haven、Khronos glTF-Sample-Assets(官方示例模型,测试首选)。

上线前必做的瘦身动作(一个 30MB 的 Blender 原始导出文件,优化后通常能压到 2MB 以内):

bash 复制代码
# 用 gltf-transform 一键优化(需 Node.js)
npm i -g @gltf-transform/cli
gltf-transform optimize input.glb output.glb --compress draco --texture-compress webp
  • 减面:三角形数量控制在 5 万以内(小程序端建议 3 万以内)
  • 压缩:Draco 几何压缩(省 60%~90%)+ WebP/KTX2 纹理压缩
  • 贴图:单张贴图分辨率别超 2048×2048,能用 1024 就别用 2048
  • 合并:把多个 Mesh 尽量合并,减少 draw call

二、原生 HTML:10 行代码跑起来

2.1 方案 A:<model-viewer>(最省事)

Google 官方出品的 Web Component,把 3D 模型当 <img> 标签用,自带轨道控制、进度条、阴影、AR 预览。如果你只是要"展示",选它就对了,不要自己造轮子。

新建一个 index.html,双击打开就能看效果:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>model-viewer 3D 模型展示</title>
  <!-- 必须加 type="module" -->
  <script type="module"
    src="https://unpkg.com/@google/model-viewer@3.5.0/dist/model-viewer.min.js"></script>
  <style>
    model-viewer {
      width: 100%;
      height: 500px;
      background-color: #f5f5f5;
    }
  </style>
</head>
<body>
  <model-viewer
    src="https://modelviewer.dev/shared-assets/models/RobotExpressive.glb"
    alt="一个可交互的机器人模型"
    auto-rotate
    camera-controls
    auto-rotate-delay="2000"
    rotation-per-second="30deg"
    shadow-intensity="1"
    exposure="1.1"
    camera-orbit="0deg 75deg 105%"
    min-camera-orbit="auto auto 5%"
    ar
    poster="./poster.webp"
    loading="eager"
  >
    <!-- 热点标注:点击弹出说明 -->
    <button slot="hotspot-1" data-position="0 1.2 0.3" data-normal="0 0 1">
      头部传感器
    </button>
    <div class="progress-bar" slot="progress-bar">
      <div class="update-bar"></div>
    </div>
  </model-viewer>
</body>
</html>

常用属性速查:

属性 作用
src 模型地址(.glb / .gltf)
camera-controls 允许鼠标/手指旋转缩放
auto-rotate 自动旋转展示
ar 显示"在您的空间中查看"AR 按钮
poster 加载完成前显示的占位图
shadow-intensity 地面阴影浓度,0 为关闭
environment-image 自定义 HDR 环境贴图(金属/玻璃质感必备)
exposure 曝光度,模型太暗就调它
camera-orbit 初始相机角度与距离

注意: 模型文件放在别的域名下时,对方服务器必须返回 Access-Control-Allow-Origin 响应头,否则 fetch 会被浏览器拦截,页面一片空白。调试阶段请把模型放在同源目录下。

2.2 方案 B:原生 three.js(完全可控)

需要自定义光照、后期特效、模型拆解动画,就得上 three.js。下面是一份可直接双击运行的单文件,用 CDN + importmap,不需要任何构建工具:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>three.js 加载 GLB 模型</title>
  <style>
    body { margin: 0; overflow: hidden; }
    #app { width: 100vw; height: 100vh; }
    #loading {
      position: fixed; inset: 0; display: flex; align-items: center;
      justify-content: center; background: #fff; font: 16px/1.5 sans-serif;
    }
  </style>
  <!-- importmap 让浏览器认识裸模块名 'three' -->
  <script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/three@0.169.0/build/three.module.js",
      "three/addons/": "https://unpkg.com/three@0.169.0/examples/jsm/"
    }
  }
  </script>
</head>
<body>
  <div id="app"></div>
  <div id="loading">模型加载中... 0%</div>

  <script type="module">
    import * as THREE from 'three';
    import { GLTFLoader }   from 'three/addons/loaders/GLTFLoader.js';
    import { DRACOLoader }  from 'three/addons/loaders/DRACOLoader.js';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const MODEL_URL = './models/RobotExpressive.glb';
    const container = document.getElementById('app');
    const loadingEl = document.getElementById('loading');

    // ---------- 1. 场景 / 相机 / 渲染器 ----------
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf0f2f5);

    const camera = new THREE.PerspectiveCamera(
      45, container.clientWidth / container.clientHeight, 0.1, 1000
    );
    camera.position.set(0, 2, 6);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    // 关键:限制像素比,移动端 3 倍屏直接按 3 渲染会瞬间卡死
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputColorSpace = THREE.SRGBColorSpace;   // 颜色不发灰的关键
    renderer.shadowMap.enabled = true;
    container.appendChild(renderer.domElement);

    // ---------- 2. 灯光(没灯光 = 全黑)----------
    scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1.2));
    const dirLight = new THREE.DirectionalLight(0xffffff, 2);
    dirLight.position.set(5, 10, 7);
    dirLight.castShadow = true;
    scene.add(dirLight);

    // ---------- 3. 控制器 ----------
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;   // 惯性阻尼,手感更顺滑
    controls.target.set(0, 1, 0);

    // ---------- 4. 加载模型 ----------
    const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath('https://unpkg.com/three@0.169.0/examples/jsm/libs/draco/gltf/');

    const loader = new GLTFLoader();
    loader.setDRACOLoader(dracoLoader);   // 模型用了 Draco 压缩时必须配置

    let mixer = null;   // 动画混合器
    loader.load(
      MODEL_URL,
      (gltf) => {
        const model = gltf.scene;

        // 自动居中 + 自适应缩放到合适大小(强烈建议封装成工具函数)
        const box = new THREE.Box3().setFromObject(model);
        const size = box.getSize(new THREE.Vector3());
        const center = box.getCenter(new THREE.Vector3());
        const maxDim = Math.max(size.x, size.y, size.z);
        const scale = 3 / maxDim;                    // 统一缩放到约 3 个单位
        model.scale.setScalar(scale);
        model.position.sub(center.multiplyScalar(scale));

        // 开启阴影 & 修正材质
        model.traverse((node) => {
          if (node.isMesh) {
            node.castShadow = true;
            node.receiveShadow = true;
          }
        });
        scene.add(model);

        // 播放模型自带的第一段动画
        if (gltf.animations.length) {
          mixer = new THREE.AnimationMixer(model);
          mixer.clipAction(gltf.animations[0]).play();
        }
        loadingEl.style.display = 'none';
      },
      (xhr) => {
        const p = Math.floor((xhr.loaded / xhr.total) * 100);
        loadingEl.textContent = `模型加载中... ${Number.isFinite(p) ? p : 0}%`;
      },
      (err) => {
        console.error('模型加载失败:', err);
        loadingEl.textContent = '模型加载失败,请检查路径 / 跨域 / 是否启用 Draco';
      }
    );

    // ---------- 5. 渲染循环 ----------
    const clock = new THREE.Clock();
    function animate() {
      requestAnimationFrame(animate);
      const delta = clock.getDelta();
      mixer?.update(delta);      // 驱动动画
      controls.update();         // 开了 damping 就必须每帧更新
      renderer.render(scene, camera);
    }
    animate();

    // ---------- 6. 自适应窗口 ----------
    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

必须起一个本地服务再访问 (npx serve 或 VS Code 的 Live Server)。用 file:// 协议直接打开会因为 CORS 策略加载不了模型。


三、Vue3:封装一个可复用的 <ModelViewer> 组件

3.1 安装

bash 复制代码
npm i three

3.2 组件源码

src/components/ModelViewer.vue:

vue 复制代码
<template>
  <div ref="wrapRef" class="model-viewer">
    <slot name="loading" :progress="progress">
      <div v-if="!loaded" class="mv-loading">
        <div class="mv-bar"><i :style="{ width: progress + '%' }"></i></div>
        <span>{{ progress }}%</span>
      </div>
    </slot>
  </div>
</template>

<script setup>
import { ref, shallowRef, onMounted, onBeforeUnmount } from 'vue'
import * as THREE from 'three'
import { GLTFLoader }    from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader }   from 'three/addons/loaders/DRACOLoader.js'
import { KTX2Loader }    from 'three/addons/loaders/KTX2Loader.js'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

const props = defineProps({
  src:       { type: String, required: true },
  autoRotate:{ type: Boolean, default: false },
  autoPlay:  { type: Boolean, default: true },   // 是否播放模型自带动画
  fitSize:   { type: Number,  default: 3 },      // 自动缩放后的目标尺寸
  bgColor:   { type: String,  default: '#f0f2f5' }
})
const emit = defineEmits(['loaded', 'error'])

const wrapRef  = ref(null)
const progress = ref(0)
const loaded   = ref(false)

// 用 shallowRef 存 three 对象:避免 Vue 深度代理巨型对象导致性能雪崩
const three = shallowRef({})

let ro = null, rafId = 0

onMounted(() => {
  const el = wrapRef.value
  const { width: w, height: h } = el.getBoundingClientRect()

  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setSize(w, h)
  renderer.outputColorSpace = THREE.SRGBColorSpace
  renderer.shadowMap.enabled = true
  el.appendChild(renderer.domElement)

  const scene  = new THREE.Scene()
  const camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 1000)
  camera.position.set(0, 2, 6)

  const controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true
  controls.autoRotate = props.autoRotate

  scene.add(new THREE.HemisphereLight(0xffffff, 0x888888, 1.2))
  const dl = new THREE.DirectionalLight(0xffffff, 2)
  dl.position.set(5, 10, 7)
  dl.castShadow = true
  scene.add(dl)

  // 压缩模型解码器
  const draco = new DRACOLoader().setDecoderPath('/draco/')          // 放到 public/draco/
  const ktx2  = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer)
  const loader = new GLTFLoader().setDRACOLoader(draco).setKTX2Loader(ktx2)

  const clock = new THREE.Clock()
  three.value = { renderer, scene, camera, controls, clock, mixer: null, model: null }

  loader.load(
    props.src,
    (gltf) => {
      const model = gltf.scene
      // 自适应:无论模型原始尺度是毫米还是千米,都能放进画面
      const box    = new THREE.Box3().setFromObject(model)
      const size   = box.getSize(new THREE.Vector3())
      const center = box.getCenter(new THREE.Vector3())
      const s = props.fitSize / Math.max(size.x, size.y, size.z) || 1
      model.scale.setScalar(s)
      model.position.sub(center.multiplyScalar(s))
      model.traverse(n => { if (n.isMesh) { n.castShadow = true; n.receiveShadow = true } })
      scene.add(model)
      three.value.model = model

      if (props.autoPlay && gltf.animations.length) {
        three.value.mixer = new THREE.AnimationMixer(model)
        three.value.mixer.clipAction(gltf.animations[0]).play()
      }
      loaded.value = true
      emit('loaded', model)
    },
    (e) => { progress.value = Math.min(99, Math.floor(e.loaded / e.total * 100)) },
    (err) => { console.error('[ModelViewer] 加载失败', err); emit('error', err) }
  )

  const loop = () => {
    rafId = requestAnimationFrame(loop)
    const d = clock.getDelta()
    three.value.mixer?.update(d)
    controls.update()
    renderer.render(scene, camera)
  }
  loop()

  // 容器尺寸变化自适应(比监听 window.resize 更准)
  ro = new ResizeObserver(([entry]) => {
    const { width, height } = entry.contentRect
    if (!width || !height) return
    camera.aspect = width / height
    camera.updateProjectionMatrix()
    renderer.setSize(width, height)
  })
  ro.observe(el)
})

// ⚠️ 必须释放!否则路由来回切几次页面就卡死 / 崩溃
onBeforeUnmount(() => {
  cancelAnimationFrame(rafId)
  ro?.disconnect()
  const { renderer, scene, controls, mixer } = three.value
  scene?.traverse((obj) => {
    if (obj.isMesh) {
      obj.geometry?.dispose()
      const mats = Array.isArray(obj.material) ? obj.material : [obj.material]
      mats.forEach(m => {
        Object.values(m || {}).forEach(v => v?.isTexture && v.dispose())
        m?.dispose()
      })
    }
  })
  mixer?.stopAllAction()
  controls?.dispose()
  renderer?.dispose()
  renderer?.domElement?.remove()
  three.value = {}
})
</script>

<style scoped>
.model-viewer { position: relative; width: 100%; height: 400px; }
.model-viewer :deep(canvas) { display: block; }
.mv-loading {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  gap: 8px; align-items: center; justify-content: center;
  background: v-bind(bgColor); font-size: 14px; color: #666;
}
.mv-bar { width: 160px; height: 4px; background: #ddd; border-radius: 2px; overflow: hidden; }
.mv-bar i { display: block; height: 100%; background: #409eff; transition: width .2s; }
</style>

3.3 使用

vue 复制代码
<template>
  <ModelViewer
    src="/models/helmet.glb"
    :fit-size="3"
    auto-rotate
    @loaded="onLoaded"
  />
</template>

<script setup>
import ModelViewer from '@/components/ModelViewer.vue'
const onLoaded = (model) => console.log('模型就绪', model)
</script>

3.4 Vue 里的 4 个高频坑

  1. 别用 ref() 存 three 对象 :Vue 的响应式会递归代理 THREE.Scene 里成千上万个属性,直接把帧率打到个位数。用 shallowRef 或普通 let 变量。
  2. onBeforeUnmount 必须 dispose:geometry / material / texture / renderer 都要手动释放,SPA 路由切换时尤其致命。
  3. 别在 setup 里操作 DOM :onMounted 之后才能拿到容器宽高,否则 canvas 会是 0×0。
  4. Vite 下 Draco 解码器路径 :把 node_modules/three/examples/jsm/libs/draco/gltf/ 整个拷到 public/draco/,不然线上 404。

四、uni-app:一套代码,三个端,三种写法

uni-app 没有一个"通吃所有端"的 3D 方案,这是现实。先看能力矩阵:

端 方案 renderjs three.js XR-Frame 说明
H5 three.js 直接用 --- ✅ ❌ 与 Vue3 方案完全一致
App-vue renderjs + three.js ✅ ✅ ❌ Android/iOS 必须走 renderjs,否则不显示
App-nvue ❌ ❌ ❌ ❌ 不支持,请改用 vue 页面或 web-view
微信小程序 XR-Frame / threejs-miniprogram ❌ 需特化版 ✅ XR-Frame 是官方原生方案,性能最好
其他小程序 基本无解 ❌ ❌ ❌ 降级为图片或 web-view

4.1 目录与条件编译组织

推荐按端拆分组件,主页面只做分发:

复制代码
components/
├── ModelH5.vue          # H5 + App 的 renderjs 方案
└── ModelMp.vue          # 微信小程序 XR-Frame 方案
vue 复制代码
<!-- pages/model/index.vue -->
<template>
  <view class="page">
    <!-- #ifdef H5 || APP -->
    <ModelH5 :src="modelUrl" />
    <!-- #endif -->

    <!-- #ifdef MP-WEIXIN -->
    <ModelMp :src="modelUrl" />
    <!-- #endif -->

    <!-- #ifdef MP-ALIPAY || MP-TOUTIAO -->
    <image :src="posterUrl" mode="widthFix" />
    <!-- #endif -->
  </view>
</template>

4.2 H5 / App 端:renderjs + three.js

为什么 App 端必须用 renderjs? 因为 App 端的逻辑层跑在独立 JS 引擎里,拿不到 DOM 和 WebGL 上下文;renderjs 让这段代码运行在视图层(WebView),才能操作 canvas。

vue 复制代码
<template>
  <view class="container">
    <!-- :prop 传值给 renderjs,:change:prop 监听变化 -->
    <view
      id="threeView"
      class="canvas-wrap"
      :prop="modelUrl"
      :change:prop="three.onUrlChange"
    ></view>

    <view v-if="loading" class="mask">加载中 {{ progressText }}</view>
  </view>
</template>

<script>
export default {
  props: { url: String },
  data() {
    return { modelUrl: '', loading: true, progressText: '0%' }
  },
  created() { this.modelUrl = this.url },
  methods: {
    // renderjs 通过 ownerInstance.callMethod 回调到这里
    onProgress(val) { this.progressText = val },
    onFinish()      { this.loading = false }
  }
}
</script>

<!-- ⚠️ module 名必须和模板里 :change:prop="three.xxx" 的前缀一致 -->
<script module="three" lang="renderjs">
import * as THREE from 'three'
import { GLTFLoader }    from 'three/examples/jsm/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

export default {
  data() {
    return { scene: null, camera: null, renderer: null, controls: null, mixer: null }
  },
  mounted() { this.initThree() },
  beforeDestroy() { this.dispose() },
  methods: {
    // url 变化时触发(newValue, oldValue, ownerInstance, instance)
    onUrlChange(newVal, oldVal, ownerVm) {
      if (newVal && newVal !== oldVal) this.loadModel(newVal, ownerVm)
    },

    initThree() {
      const el = document.getElementById('threeView')
      const w = el.clientWidth, h = el.clientHeight

      this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
      this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
      this.renderer.setSize(w, h)
      this.renderer.outputColorSpace = THREE.SRGBColorSpace
      el.appendChild(this.renderer.domElement)

      this.scene  = new THREE.Scene()
      this.camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 1000)
      this.camera.position.set(0, 2, 6)

      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
      this.controls.enableDamping = true

      this.scene.add(new THREE.HemisphereLight(0xffffff, 0x888888, 1.2))
      const dl = new THREE.DirectionalLight(0xffffff, 2)
      dl.position.set(5, 10, 7)
      this.scene.add(dl)

      const clock = new THREE.Clock()
      const loop = () => {
        this._raf = requestAnimationFrame(loop)
        this.mixer?.update(clock.getDelta())
        this.controls.update()
        this.renderer.render(this.scene, this.camera)
      }
      loop()
    },

    loadModel(url, ownerVm) {
      new GLTFLoader().load(
        url,
        (gltf) => {
          const model = gltf.scene
          const box = new THREE.Box3().setFromObject(model)
          const size = box.getSize(new THREE.Vector3())
          const s = 3 / Math.max(size.x, size.y, size.z) || 1
          model.scale.setScalar(s)
          model.position.sub(box.getCenter(new THREE.Vector3()).multiplyScalar(s))
          this.scene.add(model)

          if (gltf.animations.length) {
            this.mixer = new THREE.AnimationMixer(model)
            this.mixer.clipAction(gltf.animations[0]).play()
          }
          ownerVm.callMethod('onFinish')   // 通知逻辑层加载完成
        },
        (e) => {
          const p = Math.floor(e.loaded / e.total * 100)
          ownerVm.callMethod('onProgress', Number.isFinite(p) ? p + '%' : '0%')
        },
        (err) => console.error('加载失败', err)
      )
    },

    dispose() {
      cancelAnimationFrame(this._raf)
      this.controls?.dispose()
      this.renderer?.dispose()
    }
  }
}
</script>

<style scoped>
.container { position: relative; }
.canvas-wrap { width: 750rpx; height: 750rpx; }
.mask {
  position: absolute; left: 0; top: 0; right: 0; bottom: 0;
  display: flex; align-items: center; justify-content: center; background: #fff;
}
</style>

App 端踩坑清单:

  • document.getElementById 只能在 renderjs 里用 ,写在普通 <script> 里打包成 App 会直接报错。
  • 模板里不能 直接读 renderjs 的 data(H5 能跑、打包安卓就报错),必须走 ownerInstance.callMethod() 回传。
  • three.js 建议放 static/ 下用相对路径引入,或用 import 从 npm 引入;不要用 three/build/three.min.js(新版已移除 UMD 构建)。
  • three 全量包 500KB+,务必放进分包或做成独立页面,别拖慢主包启动。

4.3 微信小程序端:XR-Frame(官方推荐)

XR-Frame 是微信小程序原生的 3D/AR 引擎,声明式标签写场景,性能比在 WebView 里跑 three.js 好一大截,且只有微信小程序支持。

第 1 步:manifest.json 配置插件

json 复制代码
{
  "mp-weixin": {
    "appid": "你的小程序AppID",
    "usingComponents": true,
    "plugins": {
      "xr-frame": {
        "version": "latest",
        "provider": "wx6afed118d9e81df9"
      }
    }
  }
}

第 2 步:页面级 pages/model/index.json 声明组件

json 复制代码
{
  "usingComponents": {
    "xr-scene":     "plugin://xr-frame/xr-scene",
    "xr-asset-load": "plugin://xr-frame/xr-asset-load",
    "xr-gltf":      "plugin://xr-frame/xr-gltf",
    "xr-light":     "plugin://xr-frame/xr-light",
    "xr-camera":    "plugin://xr-frame/xr-camera"
  }
}

第 3 步:页面 WXML

html 复制代码
<xr-scene id="xr-scene" bind:ready="handleReady">
  <!-- 加载 glb:asset-id 供下面引用,type 固定 gltf -->
  <xr-asset-load
    type="gltf"
    asset-id="robot"
    src="https://你的CDN域名/models/RobotExpressive.glb"
  />

  <xr-camera
    position="0 1.5 5"
    target="robot"
    camera-orbit-control
  />

  <xr-light type="ambient"  color="1 1 1" intensity="0.6" />
  <xr-light type="directional" rotation="40 170 0" color="1 1 1" intensity="2" />

  <!-- 放置模型:anim-autoplay 自动播放自带动画 -->
  <xr-gltf
    id="robot"
    model="robot"
    position="0 -1 0"
    scale="1 1 1"
    anim-autoplay
  />
</xr-scene>

第 4 步:动态加载 / 销毁资源(JS)

js 复制代码
Page({
  async handleReady({ detail }) {
    this.scene = detail.value
  },

  // 运行时切换模型
  async changeModel(src) {
    const { value: model } = await this.scene.assets.loadAsset({
      type: 'gltf', assetId: 'dynamic', src
    })
    const el = this.scene.createElement('xr-gltf')
    el.getComponent('gltf').setData({ model })
    el.setId('dynamic')                      // 给个 id 方便后面移除
    this.scene.shadow.root.addChild(el)
  },

  // ⚠️ 动态加载的资源必须手动释放,否则加载几个模型后就开始卡顿
  release(name) {
    this.scene.shadow.removeChild(this.scene.getElementById(name))
    this.scene.assets.releaseAsset('gltf', name)
  },

  onUnload() { this.release('dynamic') }
})

小程序端踩坑清单:

现象 原因 / 解决
模拟器能显示,真机白屏 模型域名没配进小程序业务域名/服务器域名白名单,且必须是 HTTPS
只认 .glb,.gltf 加载不全 .gltf 外链资源要保持原有相对目录结构一起上传;建议统一转 .glb
v-if 切换后场景丢失 XR-Frame 不支持 v-if 控制显隐,改用 v-show + opacity,或把整个页面独立出去
XRFrame is not defined 微信开发者工具/基础库版本过低,更新到较新版本(基础库 2.27.1+,推荐 2.32+)
复杂模型加载慢 用官方 xr-frame-toolkit 离线预处理模型,可跳过运行时的拼装步骤

4.4 小程序备选:threejs-miniprogram

如果你已经有一套 three.js 代码想复用到小程序,可以用官方适配包:

bash 复制代码
npm i threejs-miniprogram
# 微信开发者工具 → 工具 → 构建 npm
js 复制代码
import { createScopedThreejs } from 'threejs-miniprogram'

Page({
  onReady() {
    wx.createSelectorQuery()
      .select('#webgl')
      .node()
      .exec((res) => {
        const canvas = res[0].node
        const THREE = createScopedThreejs(canvas)   // 关键:作用域隔离的 THREE
        // 之后就是标准 three.js 写法
        const renderer = new THREE.WebGLRenderer({ antialias: true })
        const scene = new THREE.Scene()
        const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000)
        new THREE.GLTFLoader().load('https://cdn.xxx/a.glb', (gltf) => {
          scene.add(gltf.scene)
        })
      })
  },
  onUnload() {
    // 必须注销 canvas,否则反复进出页面会内存泄漏
    THREE.global.unregisterCanvas(this.canvas)
    cancelAnimationFrame(this._raf)
  }
})

注意:这个包体积不小,且对复杂 PBR / 骨骼动画支持有限。新项目优先用 XR-Frame,只有"复用已有 three.js 代码"场景才选它。


五、性能优化清单(上线前逐条对照)

优化项 做法 收益
像素比封顶 renderer.setPixelRatio(Math.min(dpr, 2)) 高倍屏帧率直接翻倍
按需渲染 无交互时暂停 requestAnimationFrame 省电、降 GPU 占用
几何压缩 Draco / Meshopt 体积降 60%~90%
纹理压缩 KTX2 / Basis / WebP 显存和体积双降
减面 三角面 < 5 万(小程序 < 3 万) 提升首帧速度
模型体积 单文件 < 3MB(小程序建议 < 500KB) 缩短白屏时间
CDN + 缓存 模型走 CDN,配 Cache-Control 长缓存 二次访问秒开
分包加载 three.js 与模型放分包 保住主包 2MB 红线
占位图 poster / 骨架屏 体验不白屏
资源释放 dispose 全家桶 + 注销 canvas 杜绝内存泄漏崩溃
降级方案 WebGL 不可用时显示静态图 兼容低端机

自动化适配工具函数(建议所有项目都加上,避免"模型太大/太小/偏出画面"):

js 复制代码
export function fitObject(object3D, targetSize = 3) {
  const box    = new THREE.Box3().setFromObject(object3D)
  const size   = box.getSize(new THREE.Vector3())
  const center = box.getCenter(new THREE.Vector3())
  const s = targetSize / Math.max(size.x, size.y, size.z) || 1
  object3D.scale.setScalar(s)
  object3D.position.sub(center.multiplyScalar(s))
  return object3D
}

六、常见报错速查表

报错 / 现象 原因 解决
页面全黑,什么都没有 没加灯光,或相机在模型内部 加 HemisphereLight + DirectionalLight;用 fitObject 归位
模型颜色发灰/发暗 色彩空间没设 renderer.outputColorSpace = THREE.SRGBColorSpace
控制台 Failed to fetch / CORS 跨域被拦 模型放同源目录,或让服务端返回 Access-Control-Allow-Origin: *
Unexpected token < 路径 404,返回了 HTML 检查模型路径,Vue 项目要放 public/ 而非 src/assets/
Cannot find module 'three' 用 require('three') 的老写法 + Vite 改成 import * as THREE from 'three'
setMeshoptDecoder must be called before loading Meshopt 压缩模型没配解码器 loader.setMeshoptDecoder(MeshoptDecoder)
Draco 模型加载失败 解码器路径 404 把 draco 解码文件拷到 public/draco/,setDecoderPath('/draco/')
H5 正常,打包 App 白屏 DOM 操作写在逻辑层 全部挪进 lang="renderjs" 的 script
小程序模拟器正常真机不行 域名白名单 微信公众平台配置服务器域名 + HTTPS
反复进出页面后卡死 没释放资源 dispose() + unregisterCanvas()

七、总结:一句话选型

  • 只要展示,不做定制 → <model-viewer>,10 行搞定,还白送 AR。
  • Vue3 后台 / 大屏,要精细控制 → three.js + shallowRef + 严格 dispose。
  • uni-app 发 H5/App → renderjs 包一层 three.js,注意通信只能用 callMethod。
  • uni-app 发微信小程序 → 首选 XR-Frame,性能好、体积小,但记得做条件编译降级其他端。

3D 在前端已经不再是"高难度特技",真正的门槛只在模型体积 和各端兼容性上。把模型压好、把资源释放干净,剩下的就是几行模板代码的事。


附录:发布到 CSDN 的小提示

  1. 代码块务必标注语言(html / vue / js / bash),CSDN 才会高亮。
  2. 表格用标准 Markdown 语法,发布前在预览里确认渲染正常。
  3. 建议配 3~5 张效果图(各端真机截图),阅读量会明显更好。
  4. 文末加一段"如果对你有帮助,点个赞收藏吧",互动数据会好看很多 🙂。

本文代码基于 three.js r169、model-viewer 3.5.0 编写,各版本 API 可能略有差异,以官方文档为准。

相关推荐
志尊宝1 小时前
AI原生前端开发(043):AI辅助的样式生成(Tailwind CSS)
前端·css·ai-native
志尊宝1 小时前
AI原生前端开发(042):AI辅助的路由设计与优化
javascript·react.js·ai-native
king王一帅1 小时前
QuireKit 编辑器的 AI-Native 是如何设计的?谈谈我对真正 AI-Native 的理解
前端·开源·agent
风骏时光牛马1 小时前
生成一个AI开源工具体验标题,只需要一个标题,不需要其他的任何文本。
前端
软件派2 小时前
生产动态数据智能风控:阈值判别、趋势预测、异常识别算法工程落地实战
前端·数据库·算法
花开盛夏^.^11 小时前
电脑打开多个浏览器Web窗口占用较大的运行内存的原因
前端·浏览器
Dxy123931021611 小时前
JS DES 加密详解
开发语言·javascript·arcgis
充值内卷11 小时前
我给桌面加了个鱼缸:C++ 写后端,WebView2 里养一座 Three.js 小岛
开发语言·javascript·c++
馒头不想说话12 小时前
实体店老板的24小时AI运营员工,写、剪、发全自动,省下一整个团队工资
前端