资讯动态

微信小程序加载外部3D模型:从GLB到WebGL渲染的完整指南

发布时间:2026/10/14 14:47:53 来源:尧图企业网站定制
简介微信小程序集成three.js加载外部3D模型的完整示例项目面向需要在小程序中实现3D展示与交互的前端开发者。资源包含完整的小程序目录结构演示了从引入three.js、初始化场景与相机到通过Loader加载OBJ/GLTF模型、驱动动画循环及处理触摸交互的关键实现。压缩包共11个文件涵盖js逻辑、json配置、wxml页面、wxss样式及md说明文档整体仅167KB轻量易读便于快速对照学习。目前已有3205人学习下载。通过学习可掌握小程序环境下加载外部3D模型的具体流程、性能优化思路以及网络异常等问题的处理方法适合有一定JavaScript基础、希望拓展小程序开发能力的读者。1. miniprogramThree 解决什么微信小程序加载外部 3D 模型的完整链路miniprogramThree 解决的是微信小程序里加载外部 3D 模型这件事。商品展示、家居预览、工业模型查看本质都是把 CDN 上的 GLB 模型下载到小程序端用 WebGL 实时渲染。但小程序不是浏览器没有 DOM、没有 Blob、网络请求要走 wx.requestthree.js 不能直接搬。很多人第一版就在真机上白屏开发者工具里却一切正常。这个方向的核心是给 three.js 套一层小程序适配打通外部模型从下载、解析到上屏的全链路。这篇文章写给三类人准备做小程序 3D 展示的开发者、被真机白屏折腾的人、还没想清模型格式取舍的负责人。照着做你至少能跑通一个可旋转的外部 GLB 模型。2. 小程序里的 WebGL 环境适配层在补什么模型格式为什么选 GLB2.1 canvas 节点查询typewebgl 与 node() 的正确姿势先看一个最小 WXML。小程序里要跑 WebGLcanvas 组件必须显式声明typewebgl。如果你沿用旧习惯写canvas idmodelCanvas/canvas拿到的节点只有 2d 上下文后面的getContext(webgl)直接返回 null代码在初始化渲染器那一行就断掉。canvas typewebgl idmodelCanvas classmodel-canvas/canvas/* viewer.wxss */ .model-canvas { width: 100%; height: 60vh; }接下来在页面脚本里取节点。注意时机必须在onReady之后取此时 canvas 已经渲染完成selector query 才能拿到节点。// viewer.js Page({ onReady() { this.initViewer() }, initViewer() { wx.createSelectorQuery() .select(#modelCanvas) .node() .exec((res) { const canvas res[0].node if (!canvas) { console.error(canvas 节点获取失败) return } const gl canvas.getContext(webgl) console.log(WebGL 版本:, gl.getParameter(gl.VERSION)) // 这里继续初始化渲染器、场景、相机 }) } })代码逻辑不复杂但有三个关键点。第一node()需要较新的基础库版本支持开发时建议把调试基础库调到最近几个版本否则res[0].node是 undefined。第二canvas 外面如果套了带 transform、opacity 或 overflow 样式的 view同层渲染可能出问题canvas 会浮在其他元素之上这个放到避坑章展开。第三node()拿到的 canvas 节点其宽高由 WXML 样式决定但 WebGL 的 drawing buffer 大小需要手动确认。真机上 canvas.width 通常是样式宽度乘以像素比所以需要把像素比传给渲染器。关于像素比多说一句。我一般会读wx.getSystemInfoSync().pixelRatio然后传给渲染器的setPixelRatio。如果忽略这一步模型边缘在真机上会明显发虚因为 drawing buffer 只有 CSS 像素大小而屏幕物理像素是它的两三倍。const systemInfo wx.getSystemInfoSync() this.renderer.setPixelRatio(Math.min(systemInfo.pixelRatio, 2)) this.renderer.setSize(canvas.width, canvas.height)参数说明setPixelRatio控制渲染分辨率与 CSS 尺寸的倍率。设成 2 意味着 drawing buffer 是 CSS 尺寸的两倍画面更锐利但 GPU 负载也更高。低端 Android 上设到 3 基本就是主动崩溃所以我会用Math.min(pixelRatio, 2)兜底。这不是玄学是 GPU 浮点能力和内存带宽的硬限制。2.2 适配层three.js 在小程序里缺什么、补什么three.js 在浏览器里跑得好好的一搬进小程序就四处报错根因是它默认依赖浏览器全局对象。具体来说有三样东西必须替换。第一是 canvas 创建。three.js 的 WebGLRenderer 默认会调用document.createElement(canvas)来创建画布。小程序里没有 document所以适配层要允许你传入一个现成的 canvas 节点。社区里常见的适配方案使用方式通常是先拿到节点再传给一个工厂函数之后用这个工厂返回的 THREE 命名空间初始化渲染器。第二是渲染循环。浏览器用window.requestAnimationFrame驱动小程序 canvas 节点自带requestAnimationFrame方法用法和 window 版本基本一致。少数旧版本基础库不提供可以用 setTimeout 按 16ms 间隔模拟代价是掉帧。第三是网络请求。GLTFLoader 内部默认用 fetch 或 XMLHttpRequest 加载资源小程序里这两个都没有。适配层会把 GLTFLoader 的网络请求替换成 wx.requestresponseType 为 arraybuffer同时把 TextureLoader 的图片加载替换成 wx.downloadFile 得到临时文件路径再交给 canvas 的 createImage 创建图片对象。任何一个环节漏掉你都会看到加载回调不触发或者模型解析到一半报诡异错误。下面是一段非常简化的适配示意用来理解原理。实际使用时不建议自己写直接用社区维护好的适配包。// 简化版适配思路仅示意把 three.js 依赖的浏览器全局替换成小程序对象 function createScopedThreejs(canvas) { const THREE require(./three.min.js) // 1. 告诉 three.js 不要自己创建 canvas THREE.createCanvas () canvas // 2. 用 canvas 的 rAF 驱动渲染循环 THREE.requestAnimationFrame (cb) canvas.requestAnimationFrame(cb) // 3. loader 的网络层替换这里省略细节 return THREE }这段代码说明了一个事实适配层本质是依赖注入。自己适配的收益是可控但成本是你得持续跟进 three.js 上游的改动三个版本一升内部对 DOM 的依赖可能就变一次又得花几个晚上排查。我的建议是直接站在现成适配方案上做业务把时间花在模型格式、渲染调优和真机适配这些更有价值的地方。2.3 模型格式选型为什么 GLB 是默认答案写代码之前先定格式。外部 3D 模型常见的有 GLB、glTF、OBJ、FBX小程序端的取舍逻辑和 Web 端不太一样。GLB 是 glTF 的二进制打包形态几何、材质、动画、纹理全部塞进一个文件。小程序里加载一个文件只需要一次下载请求而且纹理内嵌不用处理模型和贴图分散在不同 URL 的并发问题。这是它成为默认答案的两个硬理由。glTF未打包的 JSON 形态在 Web 端很常见但小程序里要额外管理 .bin 和贴图文件的多次请求而且相对路径解析在某些适配版本里实现不完整踩坑成本高。OBJ 只描述几何材质在单独的 MTL 文件里贴图又是独立图片一套模型三四个文件下载和缓存都麻烦。FBX 体积大解析依赖复杂转换层真机上基本跑不动。对比起来一张表说清楚格式文件形态小程序端下载次数材质/PBR 支持适配层解析风险GLB单文件二进制1完整低glTFJSON bin 贴图多次完整中路径解析OBJ MTL文本多文件多次基本中FBX二进制/ASCII1视导出设置高所以我的建议很直接给小程序用的外部模型让美术或工具链统一输出 GLB。导出时勾选二进制Blender 导出 glTF 2.0 时选 .glb贴图尽量内嵌避免导出成外部纹理。如果模型面数大再加 Draco 压缩这个放到最后一章。选型定了工程目录也基本定型。常见做法是在项目下建一个 libs/threejs 目录放适配层和 three.js 核心文件页面代码只管业务逻辑。miniprogram/ ├── app.js ├── app.json ├── libs/ │ └── threejs/ │ ├── adapter.js # 小程序适配层 │ ├── three.min.js # three.js 核心 │ ├── GLTFLoader.js # 模型加载器 │ └── DRACOLoader.js # 压缩模型解码器可选 └── pages/ └── viewer/ ├── viewer.js ├── viewer.json ├── viewer.wxml └── viewer.wxss这个结构要注意libs 下的文件都会打进小程序主包。three.js 全量 min 版本加上 loader对包体是有压力的。如果你的主包已经接近上限需要把 3D 页面拆成独立分包libs 放进分包里。这个决定应该在写业务代码之前做否则后面迁分包会改一堆相对路径改到怀疑人生。3. 用 GLTFLoader 加载外部 GLB初始化、下载、解析一条龙3.1 初始化渲染器、场景与相机的标准套路Page 的 onReady 里拿到 canvas 节点之后接下来是 three.js 的标准三件套renderer、scene、camera。小程序版的代码和 Web 版几乎一样只有创建 renderer 时是把 canvas 节点传进去。// viewer.js const { createScopedThreejs } require(../../libs/threejs/adapter.js) Page({ data: { progress: 0, loaded: false, errorMsg: }, onReady() { this.initViewer() }, initViewer() { wx.createSelectorQuery() .select(#modelCanvas) .node() .exec((res) { const canvas res[0].node const THREE createScopedThreejs(canvas) // 渲染器 this.renderer new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true }) const pixelRatio Math.min(wx.getSystemInfoSync().pixelRatio, 2) this.renderer.setPixelRatio(pixelRatio) this.renderer.setSize(canvas.width, canvas.height) // 场景 this.scene new THREE.Scene() // 相机透视相机参数分别是视角、宽高比、近裁剪面、远裁剪面 this.camera new THREE.PerspectiveCamera( 45, canvas.width / canvas.height, 0.1, 100 ) this.camera.position.set(3, 2, 5) this.camera.lookAt(0, 0, 0) // 基础光照 const ambient new THREE.AmbientLight(0xffffff, 0.6) this.scene.add(ambient) const dirLight new THREE.DirectionalLight(0xffffff, 1.0) dirLight.position.set(5, 8, 6) this.scene.add(dirLight) // 启动渲染循环 this.startRenderLoop(canvas) // 加载模型 this.loadModel() }) }, startRenderLoop(canvas) { const render () { if (this.renderer this.scene this.camera) { this.renderer.render(this.scene, this.camera) } canvas.requestAnimationFrame(render) } render() } })参数逐个说。PerspectiveCamera(fov, aspect, near, far)fov 是垂直视角45 度在手机竖屏上拍一个 3D 物体比较舒服near 和 far 是裁剪平面0.1 到 100 覆盖常规场景模型被裁掉一半说明 far 太小模型像被穿透了说明 near 太大这两个值在真机排查白屏时经常要调。AmbientLight(0xffffff, 0.6)提供基础亮度强度建议 0.4 到 0.7调太高整个模型发白没有立体感。DirectionalLight(0xffffff, 1.0)是主光源强度 0.8 到 1.2 常用位置放在相机同侧偏上方模型正面有光照背光面也有自然明暗过渡。关键点先说antialias 和 alpha 同时打开时部分 Android 机型的 WebGL 上下文创建会失败。这不是玄学是 GPU 驱动对 MSAA 加透明通道组合的支持问题。惯例是先把 antialias 关了跑通真机再根据表现决定要不要开。3.2 GLTFLoader 直接加载远程 GLB适配层已经把 loader 的网络请求替换成 wx.request所以写法可以和 Web 端几乎一样。// 接在 initViewer 末尾 loadModel() { const THREE createScopedThreejs(this.canvas) const loader new THREE.GLTFLoader() const modelUrl https://your-cdn.example.com/models/mecha.glb loader.load( modelUrl, (gltf) { // 成功回调拿到 gltf.scene是一棵场景树 this.handleModelLoaded(gltf) }, (progress) { // 进度回调progress.loaded / progress.total if (progress.total 0) { const pct Math.round((progress.loaded / progress.total) * 100) this.setData({ progress: pct }) } }, (error) { // 错误回调 console.error(加载失败, error) this.setData({ errorMsg: 模型加载失败请重试 }) } ) }, handleModelLoaded(gltf) { const model gltf.scene || gltf.scenes[0] if (!model) { this.setData({ errorMsg: 模型内容为空 }) return } // 规整模型先算包围盒再居中、缩放 const box new THREE.Box3().setFromObject(model) const center box.getCenter(new THREE.Vector3()) const size box.getSize(new THREE.Vector3()) const maxDim Math.max(size.x, size.y, size.z) const scale 2 / maxDim model.position.sub(center) model.scale.set(scale, scale, scale) this.model model this.scene.add(model) this.setData({ loaded: true, progress: 100 }) }逻辑说明loader.load第一个参数是模型 URL成功回调里的gltf.scene是一棵 Object3D 树包含网格、材质和可能的动画进度回调只在服务器返回 Content-Length 时有意义错误回调能拿到 HTTP 状态或解析异常。规整模型这一步很重要外部模型来自不同工具链有的原点在模型底部有的在几何中心尺寸有厘米级也有米级。用 Box3 取包围盒把模型中心移到世界原点、最长边缩放到 2 个单位无论原始状态多乱进入场景后都能完整落在相机视野内。参数说明scale 2 / maxDim里的 2 是我常用的目标尺寸。相机离原点 5 个单位、fov 45 度时最长边为 2 的模型在竖屏里大约占屏高 60% 左右。想要模型占屏更大把除数调小到 1.5想要更小调到 3。这是纯视觉调优参数跟模型本身无关你需要根据自己的场景多试几次。3.3 加载进度、超时兜底与合法域名progress 回调的progress.total在两种情况下是 0CDN 没返回 Content-Length或者适配层用的下载接口不暴露总长度。这时候不能用百分比进度改用加载中文案即可别让 UI 卡在 0% 让用户以为死了。超时兜底是必须做的防御。小程序默认网络请求超时 60 秒对 3D 模型来说太久了。我一般会加一个自己的计时器发起加载后 15 秒内成功回调没触发就主动放弃并提示用户。loadModelWithTimeout() { const timer setTimeout(() { console.warn(模型加载超时) this.setData({ errorMsg: 加载超时请检查网络 }) this.isLoadFinished true }, 15000) const onSuccess (gltf) { clearTimeout(timer) this.handleModelLoaded(gltf) } // 把 onSuccess 传给 loader.load 的成功回调 }另外域名白名单是绕不开的。小程序 request 合法域名必须是 https且不允许带端口和 IP 直连真机上请求非白名单域名直接失败失败信息在真机调试的 Network 面板里能看到。开发阶段可以在开发者工具里勾选「不校验合法域名」临时调试但提审前必须把 CDN 域名加到后台白名单里。4. 外部模型加载避坑指南五个翻车现场与对应解药4.1 现象开发者工具渲染正常真机白屏原因开发者工具的 WebGL 跑在桌面浏览器上桌面 GPU 和手机 GPU 对 WebGL 的实现完整度差很多。最常见的差异有三处antialias 在部分 Android 驱动上创建失败、three.js 默认开启的某些扩展真机不支持却不报错、材质用了真机不支持的高精度纹理格式。解决先将渲染器配置改成最小化逐个排除。this.renderer new THREE.WebGLRenderer({ canvas: canvas, antialias: false, // 关闭抗锯齿优先保证能出画面 alpha: true, powerPreference: default })如果关掉 antialias 后真机恢复正常再决定要不要开。我的实际做法是 iOS 开 Android 关用wx.getSystemInfoSync().platform判断平台分别设置。排查白屏时先看 canvas 有没有实际渲染内容在渲染循环里打印一帧renderer.info.render.triangles如果三角形数量在动但屏幕白屏说明是 canvas 合成层的问题去查同层渲染如果三角形数量为 0说明场景里没有东西去查模型加载回调有没有触发。4.2 现象GLTFLoader 报 fetch is not a function 或 Failed to fetch原因适配层替换网络请求不彻底。有些适配方案只替换了 renderer 和 rAF没替换 loader 的 fetch有些替换了 fetch 但没处理 responseType导致返回的是文本而不是 ArrayBuffer还有的是基础库版本太低wx.request 的 responseType 参数不生效。解决升级适配方案到支持 loader 网络层替换的版本或者绕开 loader 的网络层自己用 wx.request 下载 arrayBuffer再调 loader.parse。downloadAndParse(url) { wx.request({ url: url, responseType: arraybuffer, timeout: 30000, success: (res) { if (res.statusCode ! 200) { this.setData({ errorMsg: HTTP res.statusCode }) return } const THREE createScopedThreejs(this.canvas) const loader new THREE.GLTFLoader() loader.parse( res.data, // ArrayBuffer , // path外部贴图相对路径的基准GLB 内嵌纹理可传空 (gltf) this.handleModelLoaded(gltf), (err) this.setData({ errorMsg: 模型解析失败 }) ) }, fail: () { this.setData({ errorMsg: 模型下载失败 }) } }) }说明loader.parse(arrayBuffer, path, onLoad, onError)是 three.js 内部把二进制解成场景树的入口。自己下载再 parse 的好处是网络行为完全可控超时、重试都自己说了算坏处是要自己处理域名白名单。如果你发现 wx.request 能下、loader 却失败多半就是适配层问题直接走 parse 路线最稳。4.3 现象进度卡在 0% 或加载完成后回调不触发原因进度回调依赖总长度CDN 未返回 Content-Length 或返回错误值时progress.total为 0百分比算出来是 NaN。更隐蔽的一种情况是有些适配方案把进度事件改成了下载完成才触发一次表现就是 0% 然后突然 100%。解决进度条做成不确定状态或者自己做真实进度估算。一个可用方案是用 wx.downloadFile 的 onProgressUpdate 拿真实进度下载成功后读文件再 parse。downloadWithProgress(url) { const task wx.downloadFile({ url: url, success: (res) { if (res.statusCode 200) { // 临时文件路径需要读成 ArrayBuffer 再交给 loader.parse const fs wx.getFileSystemManager() fs.readFile({ filePath: res.tempFilePath, success: (readRes) this.parseBuffer(readRes.data), fail: () this.setData({ errorMsg: 读取模型文件失败 }) }) } }, fail: () this.setData({ errorMsg: 下载失败 }) }) task.onProgressUpdate((res) { if (res.totalBytesExpectedToWrite 0) { const pct Math.round(res.totalBytesWritten / res.totalBytesExpectedToWrite * 100) this.setData({ progress: pct }) } }) }注意wx.downloadFile成功后拿到的是临时文件路径不是数据本体要再用wx.getFileSystemManager().readFile读成 ArrayBuffer。这条链路的请求域名要配在 downloadFile 合法域名而不是 request 合法域名配错会被拦截。4.4 现象模型加载成功但全黑或贴图丢失原因GLB 内嵌的纹理一般能正常显示glTF 引用外部贴图的场景里贴图 URL 是相对路径loader 会基于模型 URL 去拼接。适配层如果没把纹理加载替换成 downloadFile贴图请求就会失败TextureLoader 直接传网络 URL 在小程序里也大概率失败因为 canvas 的 createImage 不允许直接加载网络图片必须先下载成临时文件。解决需要外部贴图时先 wx.downloadFile 拿到临时文件路径再交给 TextureLoader。wx.downloadFile({ url: https://your-cdn.example.com/textures/wood.jpg, success: (res) { if (res.statusCode 200) { const THREE createScopedThreejs(this.canvas) const texture new THREE.TextureLoader().load(res.tempFilePath) // 把 texture 赋给对应材质并标记更新 this.model.traverse((child) { if (child.isMesh child.material.name body) { child.material.map texture child.material.needsUpdate true } }) } } })逻辑说明小程序里图片必须走 downloadFile 得到临时文件路径TextureLoader 才能把它加载成 WebGL 纹理。如果模型其他部分正常只有贴图是灰色基本就是这条路没打通。更省事的方案是导出模型时把贴图内嵌进 GLB文件大一点但省掉一切纹理路径问题。4.5 现象连续切换多个模型页面越来越卡最后闪退原因three.js 在 Web 端靠浏览器接管 GPU 资源回收小程序里 canvas 的 GPU 资源只在页面销毁时释放。替换模型时旧模型的 geometry、material、texture 还挂在 GPU 上内存只增不减。解决加载新模型前显式释放旧模型资源。disposeModel(model) { if (!model) return model.traverse((child) { if (child.isMesh) { if (child.geometry) child.geometry.dispose() if (child.material) { const mats Array.isArray(child.material) ? child.material : [child.material] mats.forEach((mat) { if (mat.map) mat.map.dispose() mat.dispose() }) } } }) this.scene.remove(model) }这条是血泪经验某开发者做过一个案例用户连续切换十余个模型后iPhone 上直接闪退。加了这个 dispose 逻辑之后内存占用曲线平稳闪退消失。另外页面 onHide 时可以考虑暂停渲染循环onShow 再恢复减少后台 GPU 占用这个习惯值得保持。5. 模型上屏后的渲染调优光照、坐标规整与触摸交互5.1 用包围盒把任意外部模型规整到视口内外部模型来源复杂有的建模软件 Y 轴朝上有的 Z 轴朝上尺寸单位也不统一。glTF 标准规定 Y 轴朝上但导出时常有偏差。所以模型加载成功后第一件事不是加效果而是规整。function normalizeModel(model) { // 计算包围盒 const box new THREE.Box3().setFromObject(model) const center box.getCenter(new THREE.Vector3()) const size box.getSize(new THREE.Vector3()) // 找到最长边缩放到目标尺寸 const maxDim Math.max(size.x, size.y, size.z) const scale 2 / maxDim // 中心移到原点缩放 model.position.sub(center) model.scale.set(scale, scale, scale) // 如果模型是 Z 轴朝上导出的绕 X 轴旋转 90 度修正 // model.rotation.x Math.PI / 2 return { scale, center } }参数说明scale 2 / maxDim是最长边缩放到 2 个单位配合 45 度 fov 和距离 5 的相机模型在画面里占约 60% 高度。如果你发现模型初始朝向不对优先检查是建模工具轴向问题还是导出设置问题旋转修正写在规整函数里集中管理别散落在各处否则后面调交互时会怀疑人生。5.2 光照参数与阴影环境光、平行光怎么配光照是 3D 展示质感的关键。基础组合是环境光加平行光环境光保证暗部不纯黑平行光制造立体感。// 环境光基础亮度不要超过 0.7 const ambient new THREE.AmbientLight(0xffffff, 0.5) this.scene.add(ambient) // 平行光主光源 const dirLight new THREE.DirectionalLight(0xffffff, 1.0) dirLight.position.set(5, 8, 6) this.scene.add(dirLight) // 需要阴影时开启 this.renderer.shadowMap.enabled true this.renderer.shadowMap.type THREE.PCFSoftShadowMap dirLight.castShadow true dirLight.shadow.mapSize.set(1024, 1024)参数说明环境光强度超过 0.7 会让模型发白平行光位置决定了阴影方向和亮面位置放在相机同侧上方比较自然。shadowMap.mapSize是阴影贴图分辨率1024 在手机上够用开 2048 画质更好但发热明显。阴影是小程序端最昂贵的特性之一建议只给桌面级展示需求开启常规电商商品展示用不上。提示真机调试时如果发现模型表面闪烁或出现条纹多半是阴影贴图精度不够或近裁剪面设置不当优先调 shadow.mapSize 和 light.shadow.camera.near/far。5.3 触摸交互单指旋转、双指缩放的实现模型转起来才算落地。WXML 里在 canvas 外层套一个 view绑定 touch 事件。view classcanvas-wrap bindtouchstartonTouchStart bindtouchmoveonTouchMove bindtouchendonTouchEnd canvas typewebgl idmodelCanvas classmodel-canvas/canvas /viewonTouchStart(e) { if (e.touches.length 1) { this.touchMode rotate this.lastX e.touches[0].clientX this.lastY e.touches[0].clientY } else if (e.touches.length 2) { this.touchMode zoom this.lastPinchDist this.getDist(e.touches) } }, onTouchMove(e) { if (!this.model) return if (this.touchMode rotate e.touches.length 1) { const x e.touches[0].clientX const y e.touches[0].clientY const dx x - this.lastX const dy y - this.lastY this.lastX x this.lastY y // 旋转灵敏度系数0.01 和 0.005 是手感调出来的 this.model.rotation.y dx * 0.01 this.model.rotation.x dy * 0.005 // 限制俯仰角防止模型翻转到背面失控 this.model.rotation.x Math.max(-1.2, Math.min(1.2, this.model.rotation.x)) } else if (this.touchMode zoom e.touches.length 2) { const dist this.getDist(e.touches) const ratio dist / this.lastPinchDist this.lastPinchDist dist // 限制缩放范围避免缩到看不到或穿模 const s Math.max(0.3, Math.min(4, this.model.scale.x * ratio)) this.model.scale.set(s, s, s) } }, onTouchEnd() { this.touchMode }, getDist(touches) { const dx touches[0].clientX - touches[1].clientX const dy touches[0].clientY - touches[1].clientY return Math.sqrt(dx * dx dy * dy) }逻辑说明单指时记录上一次触摸点计算 deltaX 和 deltaY 并累加到模型 rotation双指时通过两指距离比例缩放模型。旋转灵敏度 0.01 和 0.005 不是理论值是根据真机手感调出来的竖屏上手指横向滑动 100 像素模型转 57 度体感接近自然俯仰角限制在 ±1.2 弧度防止模型倒扣。缩放范围限制在 0.3 到 4 倍防止缩小到看不见或放大到穿模。如果想让手感更好可以加惯性touchend 时记录最后几帧的速度矢量在渲染循环里按衰减系数递减旋转速度。惯性代码不复杂但要注意页面切后台时把速度清零否则回到页面模型还在转。6. 进阶验证用 Draco 压缩把外部 GLB 模型加载时间砍掉一半加载外部模型最直接的瓶颈在文件体积。一个 2.8MB 的 GLB 在 4G 弱网下可能要 5 秒以上用户早退出了。Draco 压缩是几个三维格式里最适合小程序场景的优化手段压缩率高、three.js 原生支持解码器、改动量小。先用命令行工具把 GLB 做压缩。以 gltf-transform 为例一条命令完成。# 安装并执行 Draco 压缩input.glb 是原始文件output.glb 是压缩产物 npx gltf-transform draco input.glb output.glb压缩后模型文件可能只有原来的三分之一甚至更小代价是前端加载时要多一个解码步骤。解码器是 WebAssembly 文件需要随小程序包一起发布。启用方式是在 GLTFLoader 上挂 DRACOLoaderconst { DRACOLoader } THREE const dracoLoader new DRACOLoader() dracoLoader.setDecoderPath(/libs/threejs/draco/) // decoders 放在小程序包内 loader.setDRACOLoader(dracoLoader)注意setDecoderPath指向的是小程序包内的目录里面包含 draco_decoder.wasm 和对应的 JS 包装文件。这个 wasm 文件大约 100 到 200KB会占包体但相比压缩掉的那几 MB 网络传输这笔开销非常划算。如果主包空间紧张把解码器放进子包或者独立分包加载模型前按需初始化。验证压缩收益的方法很简单在加载前后记录时间戳。const startTime Date.now() loader.load(url, (gltf) { const cost Date.now() - startTime console.log(模型加载解析总耗时(ms), cost) // 分别在弱网、WiFi 下记录对比压缩前后的数值 })更严谨的做法是在开发者工具里模拟弱网Network 面板选 Slow 3G测压缩前后各自的加载总耗时。某模型从 2.8MB 压到 800KB 后弱网下总耗时从 6.2 秒降到 2.1 秒这个量级的提升在真机体验上是质变。压缩之后材质和动画都不受影响吗理论上是无损的几何信息解压后重建视觉差异极小。但如果你用了顶点颜色或自定义属性压缩前先确认 Draco 编码器保留这些属性否则模型颜色会丢。纹理不在 Draco 压缩范围内大贴图要另外用图片压缩策略比如转成 WebP 或降低分辨率。还要提醒一点Draco 压缩适合面数大、顶点密集的模型。如果模型本身只有几千面压缩率有限还倒贴解码器的加载时间不如不压。判断标准是原始 GLB 超过 1MB 就值得试低于 500KB 直接跳过。我第一次接小程序 3D 展示时也是先被真机白屏打懵后来才意识到适配、格式、压缩这三件事必须一起考虑。现在我的习惯是任何外部模型先转 GLB再压 Draco先在真机验证再谈上线。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑