前端加载 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 个高频坑
- 别用
ref()存 three 对象 :Vue 的响应式会递归代理 THREE.Scene 里成千上万个属性,直接把帧率打到个位数。用shallowRef或普通let变量。 onBeforeUnmount必须 dispose:geometry / material / texture / renderer 都要手动释放,SPA 路由切换时尤其致命。- 别在
setup里操作 DOM :onMounted之后才能拿到容器宽高,否则 canvas 会是 0×0。 - 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 的小提示
- 代码块务必标注语言(
html/vue/js/bash),CSDN 才会高亮。 - 表格用标准 Markdown 语法,发布前在预览里确认渲染正常。
- 建议配 3~5 张效果图(各端真机截图),阅读量会明显更好。
- 文末加一段"如果对你有帮助,点个赞收藏吧",互动数据会好看很多 🙂。
本文代码基于 three.js r169、model-viewer 3.5.0 编写,各版本 API 可能略有差异,以官方文档为准。