首页 / 资讯中心 / 文章详情

微信小程序中Three.js集成实战:从适配到性能优化

微信小程序中Three.js集成实战:从适配到性能优化 ★ FEATURED ARTICLE
简介这份资源面向希望在微信小程序中实现3D模型展示的开发者尤其适合刚接触three.js、需要快速跑通渲染流程的初中级前端与小程序学习者。它解决的是小程序端引入3D引擎、加载外部模型并驱动动画的入门问题可作为可运行的最小示例参考。压缩包共18个文件约258KB以8个js脚本承载页面逻辑与three.js相关调用5个json负责项目与页面配置另有2个wxml、2个wxss构成界面与样式并附带1个gltf格式的虾模型资源整体结构精简、便于按模块阅读。已有2306人学习说明该示例在小程序3D入门场景中具备一定参考价值。读者可据此了解模型上传服务器后通过url引用的组织方式并在js中自行调整函数尝试旋转、掉落等动画效果同时借助现成示例规避常见配置与加载问题降低上手成本。1. 小程序里跑 Three.js先把「能不能用」这件事说透微信小程序原生渲染层只认 WebGL 上下文不给你 DOM也不给你window。所以第一次把 Three.js 往小程序里塞的人十个里有九个会卡在document is not defined上然后开始怀疑这条路是不是根本走不通。答案是能走但走的不是官方那条路——你得用适配层把 Three.js 对 DOM 和浏览器 API 的依赖「翻译」成小程序能听懂的话再让 canvas 的 WebGL 上下文接管渲染。这套方案解决的核心问题就一个在不引入 web-view、不牺牲原生交互的前提下把 glTF/GLB 模型稳定地展示在小程序页面里支持旋转、缩放、材质和基础光照。适合谁手上已经有 Three.js 场景代码、想低成本迁移到小程序的开发者以及需要在小程序里做产品 3D 预览、展馆导览、教育演示这类轻交互场景的团队。如果你要做的是重度游戏或者复杂后处理这篇的边界你得先看清楚。2. 适配层选型与工程骨架为什么不能直接 npm install three2.1 小程序渲染层和浏览器渲染层的三处硬差异先把差异摆清楚后面所有选型都是围绕这三处展开的。第一处是没有 DOMThree.js 内部会碰document.createElementNS去造 canvas、碰document.createElement(img)去加载纹理小程序里这些全是 undefined。第二处是canvas 是组件不是标签小程序的canvas typewebgl需要通过wx.createSelectorQuery()拿到节点再调node.getContext(webgl)上下文对象跟浏览器里的canvas.getContext(webgl)长得像但不是一回事。第三处是资源加载走 wx API纹理图、模型文件不能靠new Image()或fetch得用wx.getFileSystemManager().readFile读本地包内文件或者wx.request拿网络资源再转 ArrayBuffer。这三处差异决定了Three.js 本体不能直接跑必须有一个适配层在中间做桥接。常见做法是引入一个社区维护的适配包本质是一份打了补丁的 Three.js 构建产物 一层平台 API 垫片它把document、window、Image、XMLHttpRequest这些符号在小程序全局环境里做最小实现。我一般会先确认适配包对应的 Three.js 版本因为不同版本内部 API 变动不小版本对不上会出现「模型能加载但材质全黑」这种玄学问题。2.2 工程目录与依赖落位小程序对 npm 的支持是「构建 npm」那一套node_modules不会直接进包得在开发者工具里点一次构建。适配包和 Three.js 的放置方式有两种一种是把适配包当普通 npm 依赖装构建后从miniprogram_npm里引另一种是把适配后的 Three.js 单文件直接丢进libs/目录手动引。前者好升级后者好排查我倾向后者——出问题时你能直接翻源码看垫片改了什么。# 项目根目录初始化装适配包包名以实际适配层为准这里用占位 npm init -y npm install three0.152.0 npm install three-platform-adapter --save # 装完后在开发者工具「工具 - 构建 npm」生成 miniprogram_npm这段命令的关键不在装什么而在版本锁定。Three.js 从 r150 之后对颜色管理、纹理编码做了较大调整适配层如果没跟上outputEncoding这类属性会静默失效表现为贴图偏色。参数上three0.152.0只是示例锚点你实际要跟适配包 README 里声明的兼容版本对齐别自己拍脑袋升到最新。构建 npm 之后miniprogram_npm目录里应该能看到适配包和 three 两个文件夹缺一个就说明构建没成功先解决这个再往下走。2.3 页面骨架canvas 节点、上下文与渲染器初始化页面结构上canvas 必须给明确的宽高且typewebgl、id要和查询选择器对上。下面是一个最小可跑的初始化骨架。// pages/viewer/viewer.js import * as THREE from three import { createRenderer } from three-platform-adapter Page({ data: { loaded: false }, onReady() { // 1. 拿到 canvas 节点注意回调里才有 node wx.createSelectorQuery() .select(#glcanvas) .node() .exec((res) { const canvas res[0].node // 2. 用适配层创建渲染器内部会接管 getContext const renderer createRenderer(canvas, { antialias: true, alpha: false, pixelRatio: wx.getSystemInfoSync().pixelRatio }) // 3. 尺寸必须手动设小程序不会帮你同步 CSS 尺寸 const info wx.getSystemInfoSync() renderer.setSize(info.windowWidth, info.windowHeight) this.initScene(renderer) }) }, initScene(renderer) { const scene new THREE.Scene() scene.background new THREE.Color(0x1a1a1a) // 透视相机fov 45 适合产品展示太广会畸变 const camera new THREE.PerspectiveCamera( 45, wx.getSystemInfoSync().windowWidth / wx.getSystemInfoSync().windowHeight, 0.1, 1000 ) camera.position.set(0, 1.5, 4) camera.lookAt(0, 0, 0) // 环境光 平行光缺一个模型就会一半黑 scene.add(new THREE.AmbientLight(0xffffff, 0.6)) const dirLight new THREE.DirectionalLight(0xffffff, 1.2) dirLight.position.set(3, 5, 2) scene.add(dirLight) this.renderer renderer this.scene scene this.camera camera this.animate() }, animate() { const canvas this.renderer.domElement canvas.requestAnimationFrame(() { this.renderer.render(this.scene, this.camera) this.animate() }) } })逻辑说明分三层看。第一层是节点获取createSelectorQuery().select().node()是异步的exec回调里拿到的res[0].node才是真正的 canvas 节点在回调外拿永远是 undefined这是新手第一个翻车点。第二层是渲染器创建适配层的createRenderer内部会调canvas.getContext(webgl)并把上下文包成 Three.js 认识的形态antialias开抗锯齿在小程序上性能开销不小中低端机建议关掉。第三层是尺寸与像素比setSize用的是逻辑像素pixelRatio单独传两者不匹配会导致画面模糊或渲染区域只占左上角。requestAnimationFrame这里挂在 canvas 节点上而不是全局因为小程序全局没有这个函数这也是适配层要垫的东西之一。提示onReady里做初始化而不是onLoad因为onLoad阶段节点还没渲染出来查询必空。3. 模型加载与交互GLB 进包、纹理读取、手势控制3.1 GLB 模型加载文件系统读取与加载器接管小程序包内文件用wx.getFileSystemManager().readFile读成 ArrayBuffer再交给 GLTFLoader 的parse方法。注意是parse不是loadload走的是 XHR小程序里没有。import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader loadModel(path) { const fs wx.getFileSystemManager() fs.readFile({ filePath: path, // 例如 /models/product.glb success: (res) { const loader new GLTFLoader() // parse 第二个参数是资源路径前缀纹理内嵌时可传空 loader.parse(res.data, , (gltf) { const model gltf.scene // 自动居中模型原点不在几何中心时必做 const box new THREE.Box3().setFromObject(model) const center box.getCenter(new THREE.Vector3()) model.position.sub(center) // 按包围盒最大边缩放到 2 个单位避免大小不一 const size box.getSize(new THREE.Vector3()) const scale 2 / Math.max(size.x, size.y, size.z) model.scale.setScalar(scale) this.scene.add(model) this.model model this.setData({ loaded: true }) }, (err) { console.error(GLB 解析失败, err) }) }, fail: (err) console.error(文件读取失败, err) }) }参数上重点说三个。filePath必须是包内绝对路径以/开头相对路径在小程序文件系统里不认。parse的第二个参数path是外部资源比如分离的 .bin 和贴图的基准路径GLB 是单文件内嵌所以传空字符串即可如果你用的是 glTF 分离资源这里要传目录且资源得一起打进包。第三个是居中与缩放不同建模软件导出的模型原点五花八门不居中就会出现「模型转到背面时飞出视野」包围盒归一化是必做步骤2 / max这个系数按你相机距离调相机在 z4 时模型最大边 2 个单位刚好占屏一半左右。3.2 纹理与贴图的读取路径GLB 内嵌纹理走的是加载器内部逻辑适配层一般已经处理。但如果你要单独加载一张贴图做背景或 UI 叠加就得自己读文件再转纹理。loadTexture(path) { return new Promise((resolve, reject) { wx.getFileSystemManager().readFile({ filePath: path, success: (res) { // 小程序读出来是 ArrayBuffer转 base64 再交给 Image 垫片 const base64 wx.arrayBufferToBase64(res.data) const img new Image() // 适配层提供的 Image 垫片 img.onload () { const tex new THREE.Texture(img) tex.needsUpdate true // 手动标记否则纹理不刷新 resolve(tex) } img.onerror reject img.src data:image/png;base64, base64 }, fail: reject }) }) }这里的关键是needsUpdate true。小程序环境里纹理上传时机和浏览器不同不手动标记会出现「第一次渲染是黑的转一下才出来」的现象血泪经验。另外 base64 转换对大图有内存压力贴图控制在 1024×1024 以内比较稳超过就考虑压缩或分块。3.3 手势交互touch 事件映射到轨道控制小程序没有 OrbitControls 依赖的鼠标事件得自己把touchstart/touchmove/touchend映射成旋转和缩放。核心是用触摸位移量驱动模型或相机的球坐标。onTouchStart(e) { const t e.touches[0] this.lastX t.clientX this.lastY t.clientY if (e.touches.length 2) { // 双指记录初始距离用于缩放 this.lastDist this.getDistance(e.touches) } }, onTouchMove(e) { const t e.touches[0] if (e.touches.length 1 this.model) { const dx t.clientX - this.lastX const dy t.clientY - this.lastY // 0.01 是灵敏度系数屏幕越大越要调小 this.model.rotation.y dx * 0.01 this.model.rotation.x dy * 0.01 // 限制俯仰角防止翻过头看到模型底部穿帮 this.model.rotation.x Math.max(-Math.PI / 3, Math.min(Math.PI / 3, this.model.rotation.x)) } else if (e.touches.length 2) { const dist this.getDistance(e.touches) const ratio dist / this.lastDist this.camera.position.multiplyScalar(1 / ratio) this.lastDist dist } this.lastX t.clientX this.lastY t.clientY }, getDistance(touches) { const dx touches[0].clientX - touches[1].clientX const dy touches[0].clientY - touches[1].clientY return Math.sqrt(dx * dx dy * dy) }参数说明0.01的灵敏度是按 375pt 宽屏幕调的平板或大屏要降到 0.005 左右否则轻轻一划模型转半圈。俯仰角限制在 ±60 度是产品展示的常见约束放开会看到模型底面和穿模。缩放用相机位置乘除而不是改 fov因为改 fov 会带来透视畸变视觉上不自然。双指缩放记得在touchend里重置lastDist不然下一次双指会跳变。4. 避坑与排查五个真实翻车现场4.1 现象控制台报document is not defined页面白屏原因几乎都是 Three.js 本体在适配层之前被加载了或者你引的是three/build/three.module.js而不是适配包导出的那份。适配层的垫片必须在 Three.js 执行前注入全局。解决确认 import 顺序从适配包统一导出 THREE不要在业务代码里再单独import three检查miniprogram_npm里是不是同时存在两份 three重复打包会绕过垫片。4.2 现象模型加载成功但全黑转个角度偶尔闪一下这是光照和材质编码双重问题。一方面环境光强度不够PBR 材质在没有环境贴图时对光照极其敏感另一方面 r152 之后默认色彩空间变了贴图没设colorSpace会偏暗。解决先加AmbientLight保底再补一盏平行光纹理加载后设tex.colorSpace THREE.SRGBColorSpace渲染器初始化时确认outputColorSpace与适配层版本匹配。排查顺序是先关材质看纯色几何体亮不亮亮则问题在材质不亮则问题在光照或相机。4.3 现象真机上帧率掉到十几帧低端机直接卡死原因通常是三件事叠加antialias: true、pixelRatio取了设备原始值有些机型到 3、模型面数过高。解决中低端机关抗锯齿pixelRatio上限压到 2模型面数控制在 5 万三角面以内必要时用 Draco 压缩。另外requestAnimationFrame循环在页面隐藏时不会自动停onHide里要手动置标志位停掉渲染否则后台还在烧 GPU。4.4 现象纹理第一次渲染是黑的交互一下才显示小程序里纹理上传时机和渲染循环不同步Texture构造后没有触发上传。解决加载完纹理显式设needsUpdate true并且确保纹理加载完成后再把模型加进场景或者用LoadingManager统一等资源就绪再启动渲染循环。这个坑在浏览器里很少遇到小程序上几乎必现。4.5 现象开发者工具正常真机报getContext返回 null工具和真机的 WebGL 实现有差异常见于 canvas 尺寸为 0 或节点还没布局完成就取上下文。解决确保 canvas 有明确宽高样式初始化放在onReady且用nextTick包一层部分机型对typewebgl和typewebgl2支持不同适配层若不支持 webgl2 就显式降级。真机调试时打开 vConsole 看具体报错别只信工具里的表现。5. 性能压测与上线前验证一套我每次都走的检查清单上线前我固定做三件事缺一件都不敢发版。第一件是分档压测找一台低端安卓、一台中端、一台 iPhone 分别跑记录首帧时间和稳定帧率。首帧超过 2 秒就要考虑模型懒加载或降面数稳定帧率低于 30 就得砍效果。第二件是内存曲线观察连续进出页面十次看内存是否持续上涨涨了就是渲染循环或纹理没释放onUnload里要renderer.dispose()、遍历场景geometry.dispose()和material.dispose()纹理单独texture.dispose()。第三件是弱网与包体检查模型走网络加载时测 3G 模拟包内模型则看主包是否超限超了就把模型挪到分包。下面这张表是我常用的参数基线按设备档位调别一套参数打天下。参数低端机中端机高端机antialiasfalsefalsetruepixelRatio 上限1.522模型三角面上限3 万5 万10 万阴影关闭关闭可选纹理最大边长51210241024验证方法上我会在渲染循环里挂一个简易帧率采样每 60 帧算一次平均低于阈值就在控制台打点真机跑十分钟看有没有持续劣化。还有一个容易忽略的点小程序切后台再回来WebGL 上下文可能丢失onShow里要检测上下文状态并重建渲染器否则回来就是黑屏。这套检查我踩过一次线上黑屏之后就成了肌肉记忆每次发版前必走一遍。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站