微信小程序 Three.js 3D展示:适配、加载与性能优化

发布时间:2026/11/2 22:45:44
微信小程序 Three.js 3D展示:适配、加载与性能优化 简介这份源码包面向希望在微信小程序中实现3D模型展示效果的开发者以three.js为核心着重解决小程序环境下加载gltf模型、渲染与交互的常见问题适合已具备小程序基础、想快速验证3D方案的中级开发者参考。压缩包共18个文件、约258KB以8个js脚本承载渲染逻辑与工具函数5个json负责页面与项目配置2个wxml与2个wxss构成页面结构与样式另含1个gltf格式的虾模型体积轻量、结构紧凑可直接导入开发者工具运行。目前已有2309人学习下载具备一定的可行性验证基础。读者可借此跑通从模型上传服务器到在wxml中按url引用的完整链路并通过调整js中的函数实现旋转、掉落等动画效果目录划分清晰也便于排查模型加载失败、路径配置错误等典型问题作为小程序3D入门的练手样例较为合适。1. 小程序里做 3D 展示先认清它并非“把网页搬过来”微信小程序 3D 展示这个需求十个里有八个最后会落到 Three.js 上模型由美术提供 glb交互是能转、能放大看细节最好点一下还能换个颜色。真动手才发现网页里那套new THREE.WebGLRenderer()在小程序里第一行就跑不通没有 document没有 windowcanvas 不是 DOM 节点requestAnimationFrame也不在全局对象上挂着。这份资源解决的正是这段“从网页到小程序”的落差一套把 Three.js 接进微信小程序、把 3D 模型稳定显示出来的可运行工程包含 canvas 节点获取、渲染器初始化、GLTFLoader 适配、触摸交互和资源释放这几块拼图。适合两类人手上已有 glb 模型、要在小程序里做产品展示的前端以及做过网页版 Three.js、第一次往小程序迁移、被黑屏和贴图丢失折腾过的人。下面按“先跑通管线再上模型最后抠性能和坑”的顺序拆。2. 适配层怎么搭canvas 节点、像素比与触摸坐标的三个改造点小程序里的 Three.js 并非“装个依赖就完事”它有一层必须自己写的适配代码。这层代码大概一百多行但每一行都对应一个具体的失败场景。我一般把这段单独放在lib/render/下和业务页面隔开后面换页面、换模型都不用重写。2.1 用 createScopedThreejs 拿到“这块 canvas 专属”的 THREE直接require(three)在小程序里会在模块加载阶段就失败因为 three 的构建产物顶层会去访问self、window、document这些在小程序环境里不存在的全局对象。常见做法是用小程序官方那套作用域化的构建把 canvas 注入进去生成一个只在当前 canvas 上生效的 THREE 命名空间。// pages/viewer/viewer.js const { createScopedThreejs } require(threejs-miniprogram) Page({ onReady() { wx.createSelectorQuery() .select(#webgl) // 必须和 wxml 里 canvas 的 id 一致 .node() // 取真实节点不是节点快照 .exec((res) { const canvas res[0] res[0].node if (!canvas) { // 低版本基础库拿不到节点这里要能报出来不要静默失败 console.error(canvas node 获取失败检查基础库版本与 typewebgl) return } // 关键一步canvas 注入后返回的 THREE 只服务于这块 canvas const THREE createScopedThreejs(canvas) this.canvas canvas this.THREE THREE this.bootstrap(THREE, canvas) }) } })这段代码里三个点值得说清楚。.node()返回的是真实的 canvas 节点只有这种节点才有getContext、requestAnimationFrame和createImage用旧的wx.createCanvasContext拿到的绘图上下文调用不了 Three.js。createScopedThreejs(canvas)返回的 THREE 和 npm 上那份不是同一个实例里面的Vector3、Mesh、Matrix4都带着这块 canvas 的上下文所以一个页面创建一个不要跨页面复用也不要和自己require进来的 three 混着用。混用的后果很隐蔽你自己 new 的Vector3传给 scoped 的MeshisVector3判断不过位置静默变成 NaN模型就是不出现控制台干干净净——这种问题得靠经验才能发现。对应的 wxml 长这样typewebgl是必须的canvas-id保留一份是为了兼容旧接口兜底canvas idwebgl typewebgl canvas-idwebgl classwebgl disable-scroll{{true}} bindtouchstartonTouchStart bindtouchmoveonTouchMove bindtouchendonTouchEnd /canvasdisable-scroll和后面事件里的catchtouchmove是两回事前者阻止 canvas 区域内的页面滚动后者阻止事件冒泡到页面。做 3D 旋转的时候两个都要否则用户一拖页面跟着上下滑模型只转了一半体验非常糟糕。2.2 渲染器三行参数决定真机上是不是一片模糊渲染器初始化的代码很短但参数选错在真机上就是灾难。我吃过的最典型的一次亏是在 3 倍像素比的安卓机上直接setPixelRatio(systemInfo.pixelRatio)画面确实清晰了帧率从 50 掉到 18拖动像幻灯片。const sys wx.getSystemInfoSync() const dpr Math.min(sys.pixelRatio || 2, 2) // 上限锁死 2别信 3 倍屏 const width sys.windowWidth // 单位 px和 canvas 的 CSS 尺寸一致 const height Math.round(width * 0.75) // 按设计稿比例算避免拉伸 const renderer new THREE.WebGLRenderer({ antialias: dpr 2, // 低端机建议直接关掉抗锯齿走 FXAA 更划算 alpha: true, // 需要透明背景时开不要用 setClearColor 黑色顶 preserveDrawingBuffer: false // 除非要截图否则关掉能省一次拷贝 }) renderer.setPixelRatio(dpr) renderer.setSize(width, height, false) // 第三个参数 false不写 canvas.style renderer.setClearColor(0x000000, 0)setSize的第三个参数在小程序里必须传false。小程序 canvas 节点没有可供设置的 style传true会把宽高写到并不存在的地方表现是画面被拉伸或者只渲染左下角一小块。width、height一定要用 canvas 在页面上的实际 px 尺寸设计稿给的是 rpx得先rpx * windowWidth / 750换算这一步忘了iPad 和折叠屏上必然错位。相机用最普通的透视相机就够视场角 45、近裁面 0.1、远裁面 1000 是最通用的组合const camera new THREE.PerspectiveCamera(45, width / height, 0.1, 1000) camera.position.set(0, 1.2, 3.5) camera.lookAt(0, 0, 0)注意在接真模型之前先用一个BoxGeometry加MeshNormalMaterial把管线跑通。管线不通的时候你看不出是加载问题还是渲染问题先有个能转的盒子后面所有排查都有参照物。2.3 触摸事件与 requestAnimationFrame这两处必须自己接网页里我们用OrbitControls一行代码搞定旋转缩放。小程序里这套控件用不了因为它绑的是 DOM 的addEventListener和pointerdown小程序没有这些。硬要改也不是不行但OrbitControls里有大量event.preventDefault()和domElement.getBoundingClientRect()改到最后不如自己写三十行。自己写的版本核心在于坐标来源。小程序 canvas 的触摸事件里e.touches[0]带的是x、y单位是相对 canvas 左上角的 px它没有clientX、clientY也没有pageX。用错字段的表现是手指在屏幕左边划模型往反方向转越靠边偏得越离谱。onTouchStart(e) { const t e.touches[0] this.lastPoint { x: t.x, y: t.y } this.rotating true }, onTouchMove(e) { if (!this.rotating) return const t e.touches[0] const dx t.x - this.lastPoint.x const dy t.y - this.lastPoint.y this.rotY dx * 0.008 // 横向拖动 → 绕 Y 轴 this.rotX Math.max( // 纵向俯仰夹住避免翻过头 -Math.PI / 3, Math.min(Math.PI / 3, this.rotX dy * 0.008) ) this.lastPoint { x: t.x, y: t.y } this.applyRotation() // 写进 group.rotation this.requestRender() }, onTouchEnd() { this.rotating false }0.008这个系数和 canvas 的物理宽度相关宽屏上拖同样的像素转得更少想让手感统一就改成dx / this.canvasWidth * 2。俯仰角夹在 ±60 度是有意的产品展示类的模型翻到背面除了看到一堆破面没有任何价值限制住反而显得专业。帧循环这里也有个坑scoped 出来的 THREE 里renderer.setAnimationLoop不一定可用稳妥写法是走 canvas 节点自己的requestAnimationFramestartLoop() { const canvas this.canvas const tick () { this.rafId canvas.requestAnimationFrame(tick) this.renderer.render(this.scene, this.camera) } this.rafId canvas.requestAnimationFrame(tick) }, onHide() { // 页面切后台必须停不停就是持续耗电 后台被回收后回来黑屏 if (this.rafId) this.canvas.cancelAnimationFrame(this.rafId) this.rafId null }onHide里停循环这一条写 demo 的时候几乎所有人都会漏上线后被用户投诉“看个模型手机发烫”才回头补。停的时候记得把rafId置空否则onShow里判断重复启动会失效出现两个循环同时跑帧率直接翻倍消耗。3. 模型加载链路GLTFLoader 改造与两种取文件方式管线跑通、盒子能转了接下来才是真正的硬骨头把美术给的模型加载进来。这一段的失败率远高于前面原因是 Three.js 的加载器默认按浏览器环境写XMLHttpRequest、fetch、Blob、URL.createObjectURL在小程序里一个都没有。适配并非改一行而是要把整条链路拆开重接。3.1 FileLoader 必须换掉wx.request 与文件系统管理器二选一GLTFLoader内部靠FileLoader拿二进制FileLoader内部靠fetch。小程序里没有 fetch所以不管加载远程模型还是包内模型第一步都是把FileLoader.prototype.load整个替换掉。我一般的写法是先判断协议远程走wx.request本地走文件系统管理器。function patchFileLoader(THREE) { const { FileLoader, Cache } THREE FileLoader.prototype.load function (url, onLoad, onProgress, onError) { const scope this // 命中缓存直接回调避免同一张贴图/同一个 bin 被重复请求 const cached Cache.get(url) if (cached) { onLoad onLoad(cached) return cached } scope.manager.itemStart(url) // 与 itemEnd 必须成对否则 LoadingManager 永远不触发 const done (buffer) { Cache.add(url, buffer) onLoad onLoad(buffer) scope.manager.itemEnd(url) } const fail (err) { onError onError(err) scope.manager.itemError(url) scope.manager.itemEnd(url) } if (/^https?:\/\//.test(url)) { wx.request({ url, method: GET, responseType: arraybuffer, // 不写这行拿到的是被转成字符串的乱码 success: (res) { if (res.statusCode 200) done(res.data) else fail(new Error(HTTP res.statusCode)) }, fail }) } else { wx.getFileSystemManager().readFile({ filePath: url, success: (res) done(res.data), fail }) } } }几个参数值得单独强调。responseType: arraybuffer是最容易漏的一行漏了以后res.data是字符串GLTFLoader.parse拿到它会解析失败但只报一句很笼统的错你会以为是模型文件坏了来回折腾半天。Cache.add必须配上Cache.get因为 gltf 里的.bin和外置贴图会被多次请求缓存能省掉一半流量。manager.itemStart/itemEnd/itemError三个调用要严格配对配不上时LoadingManager.onLoad永远不触发你写的“加载完成隐藏 loading”永远等不到。包内文件读取这里有个版本差异要注意getFileSystemManager().readFile读代码包内的文件在较新的基础库上没问题老版本上会报路径不存在。稳妥的做法是在app.js启动时把模型从包内拷到用户目录之后统一从wx.env.USER_DATA_PATH读路径变了但接口稳定。// 把包内模型拷到用户目录后续统一用这个路径 const fs wx.getFileSystemManager() const target ${wx.env.USER_DATA_PATH}/scene.gltf try { fs.accessSync(target) } catch (e) { fs.copyFileSync(models/scene.gltf, target) // 第一次进来才拷 }3.2 贴图走 canvas.createImage内嵌贴图的 glb 会静默失败模型能加载了第二个问题紧接着来模型是灰的、纯白或者纯黑材质完全没生效。八成是贴图没加载上。GLTFLoader加载外置贴图时走的是ImageLoader而ImageLoader内部是document.createElementNS(img)。小程序里能当texture.image用的只有canvas.createImage()返回的对象所以这一步也得替换function patchImageLoader(THREE, canvas) { THREE.ImageLoader.prototype.load function (url, onLoad, onProgress, onError) { const img canvas.createImage() // 唯一能被 Texture 接受的图像对象 img.onload () onLoad onLoad(img) img.onerror (e) { console.error(贴图加载失败, url) onError onError(e) } img.src url return img } }比这更隐蔽的是 glb 内嵌贴图。glb 把纹理塞在 bufferView 里GLTFLoader解析到这种纹理时会new Blob(...)再URL.createObjectURL而小程序两个都没有。结果是贴图加载直接抛异常但这个异常在 loader 内部被吞掉一部分你在控制台只看到“模型加载完成”却是一片白。绕开这个问题有两个办法。省事的办法是导出时就别内嵌在建模软件里导出 glTF 时选“分离.gltf .bin textures”格式贴图变成独立文件ImageLoader那条替换路径就能正常工作。想继续用单文件 glb就得在GLTFLoader里把 bufferView 到 base64 data URI 的那段改掉把二进制转成data:image/png;base64,...再喂给canvas.createImage()这条路能走通但内存占用会涨一截大贴图慎用。注意贴图的尺寸也有限制。1024 边长以内最稳2048 在部分中低端安卓上会创建失败且不报错表现就是材质变成纯色。3.3 parse 之后的居中与缩放不做这一步模型会绕着空气旋转加载成功、贴图正常第三个问题往往是“模型在画面外”或者“转起来像在绕空气旋转”。原因是模型的原点由美术在建模软件里定的可能在脚底、可能在场景某个角落和你的相机、你的旋转中心都没关系。拿到 gltf 之后不要直接scene.add先算包围盒再做归一loader.load(modelPath, (gltf) { const root gltf.scene const box new THREE.Box3().setFromObject(root) const size box.getSize(new THREE.Vector3()) const center box.getCenter(new THREE.Vector3()) // 1) 把几何中心平移到原点旋转才是“自转” root.position.sub(center) // 2) 统一缩放到包围球直径 2相机距离就能固定不用一个模型调一次 const maxSide Math.max(size.x, size.y, size.z) || 1 root.scale.setScalar(2 / maxSide) // 3) 有动画的话在这里起 mixer没有就直接挂上去 if (gltf.animations gltf.animations.length) { this.mixer new THREE.AnimationMixer(root) this.mixer.clipAction(gltf.animations[0]).play() } this.group.add(root) this.requestRender() }, undefined, (err) { console.error(模型加载失败, err) })2 / maxSide这个归一化是我最推荐的一个习惯。不做归一每个模型的尺寸都不一样相机z得 하나一个调产品列表页换一个模型就崩一次。做了归一之后相机固定z 3.5模型永远占画面 60% 左右视觉统一配置也不用改。root.position.sub(center)有个前提center是相对root父坐标系算出来的如果模型自身position不为零直接减会有偏差。稳妥写法是先root.position.set(0,0,0)再算 box算完再平移。这个细节在小模型上看不出来在有嵌套层级的模型上会让模型偏出去半个屏幕。4. 包体与帧率小程序硬约束下的取舍小程序对包体和内存的限制比网页严得多主包 2MB、总包 20MB 这两条线卡在那里一个没优化的 glb 轻松上 30MB。而 Three.js 本身加上去也有三百多 KB剩下的空间要留给业务代码。这一章讲的都是“砍”的艺术砍面数、砍贴图、砍不必要的渲染。4.1 面数、贴图、材质先卡死三个数字我一般在美术出模型之前就把预算表丢过去而不是等出完再让他们改后者沟通成本更高。项目建议上限超了会怎样三角面数3 万以内中低端安卓首帧超过 1.5 秒拖动掉到 20 帧以下贴图总大小1MB 以内包体超限且显存吃紧容易闪退单张贴图边长10242048 在部分机型创建失败且无报错材质数量3 个以内每个材质一次 draw call数量翻倍帧率腰斩骨骼数量30 以内骨骼动画的矩阵计算是 CPU 侧开销数量线性增长超了就用 gltf-transform 做一轮离线优化这是目前最省事的一条路# 减面 限制贴图尺寸 清理无用节点一条命令搞定 npx gltf-transform/cli optimize raw.glb web.glb \ --texture-size 1024 \ --simplify-ratio 0.5 \ --simplify-error 0.001--texture-size 1024把所有贴图等比压到最长边 1024通常能省 60% 以上体积--simplify-ratio 0.5是减面比例0.5 表示保留一半三角面配合--simplify-error 0.001控制误差后者太小减不下来、太大会出现明显破面0.001 到 0.01 之间试。这个命令会顺手做 prune删无用节点和 weld合并重复顶点体积还能再降一点。至于 Draco 这类几何压缩扩展我的建议是能不碰就不碰。它需要额外的 WASM 或 JS 解码器而解码器本身在小程序里还要再适配一轮收益是体积减半成本是一到两天的调试和一个不确定的兼容性风险。除非包体真的顶到 20MB 红线、减面和压贴图都用完了否则不值得。4.2 按需渲染静止时把帧循环停掉一个产品展示页用户有 70% 的时间是在看不是在拖。持续 60 帧渲染纯属浪费而且在低端机上会让整个页面都变卡——WebGL 渲染线程占满setData 都变慢。改成“脏标记 需要时才渲染”的模型逻辑很简单所有会改变画面的操作旋转、缩放、动画帧、材质切换调用requestRender()渲染完就停有持续动画的时候才开连续循环。requestRender() { if (this.rafId) return // 已经有一帧在排队不重复排 const step () { this.rafId null this.renderer.render(this.scene, this.camera) if (this.mixer) { // 有动画就必须连续跑这时退化成常驻循环 this.requestRender() } } this.rafId this.canvas.requestAnimationFrame(step) }这个写法有个前提mixer.update(delta)得在 render 之前调用并且 delta 要自己算。用Date.now()差值除以 1000不要用clock.getDelta()scoped 版本的Clock依赖performance.now在没有 polyfill 的环境里会拿到 0动画就会原地冻住——这也是个常见问题表现是模型动了一下就不动了看起来像加载没完成。配合按需渲染还有两个开关收益很明显。关阴影renderer.shadowMap.enabled false实时阴影在移动端是纯 CPU/GPU 大户产品展示用烘焙到贴图里的假阴影效果不差。降材质MeshStandardMaterial换成MeshLambertMaterial甚至MeshBasicMaterial贴图里已经烘好光照的情况下片元着色器复杂度直接砍一半。4.3 内存释放dispose 的顺序和 forceContextLoss小程序里 WebGL 上下文的数量是有上限的同层渲染的 canvas 更是占资源。用户在产品页和详情页之间来回切三五次第六次进来黑屏控制台什么都不报——这个现象我见过太多次根因都是页面卸载时没释放。释放不是一句renderer.dispose()就完事得按几何体、材质、贴图、渲染器的顺序逐个清onUnload() { if (this.rafId) { this.canvas.cancelAnimationFrame(this.rafId) this.rafId null } if (this.mixer) { this.mixer.stopAllAction() this.mixer null } this.scene.traverse((obj) { if (obj.geometry) obj.geometry.dispose() // 顶点数据在显存里 if (!obj.material) return const mats Array.isArray(obj.material) ? obj.material : [obj.material] mats.forEach((m) { // 材质上挂的贴图要单独释放material.dispose() 不管贴图 Object.keys(m).forEach((key) { const val m[key] if (val val.isTexture) val.dispose() }) m.dispose() }) }) this.renderer.dispose() this.renderer.forceContextLoss() // 主动交还 WebGL 上下文关键的一步 this.renderer null this.scene null }forceContextLoss()是最容易被忽略的一行它主动触发WEBGL_lose_context扩展把上下文还给系统。少了这一步dispose()只是释放了 Three.js 内部持有的一层引用底层上下文要等 GC而 GC 在小程序里的时机完全不可控。5. 排查清单黑屏、模型不显示、触摸错位这些事下面这五条是我在真机上实际遇到、并且在别人的项目里也反复见到的。每条按现象、原因、解决写遇到问题照着对。5.1 开发者工具里正常真机一片黑现象模拟器上模型清清楚楚扫码到手机上打开canvas 区域纯黑没有任何报错。原因通常有三个层次。最外层是像素比工具里pixelRatio是 1 或 2真机 3 倍屏渲染的帧缓冲是 1170×2532中低端 GPU 直接分配失败。中间层是纹理用了非 2 的幂尺寸的贴图又开了默认的 mipmapWebGL1 下会创建失败。最里层是抗锯齿MSAA 的显存在部分机型上会直接超限。解决顺序先把setPixelRatio锁到 2 以下再试不行就把antialias关掉还不行就给所有贴图设generateMipmaps false和minFilter THREE.LinearFilter。三步下来九成的真机黑屏能解决。这个排查顺序值得记住从“开销最大的开关”往“开销最小的开关”试。5.2 模型不显示控制台一点错都没有现象加载回调触发了gltf.scene也有内容但画面里只有背景色。原因可能有三相机在模型内部模型尺寸几十米相机在原点等于站在模型肚子里材质是 PBR 但没有环境贴图metalness默认 1结果全是黑的FileLoader拿到的是文本不是二进制parse出来的场景是空的。解决先加一行console.log(new THREE.Box3().setFromObject(root))看包围盒是不是离谱是就做归一化再把所有材质临时换成MeshNormalMaterial能看到彩色模型说明是材质/光照问题看不到就是几何/相机问题最后检查responseType有没有写。这个三分法比漫无目的地改代码快十倍。5.3 模型是白模贴图完全没有现象形状对光照对就是纯白或纯灰没有花纹。原因ImageLoader没换或者换了但模型用的是内嵌贴图的 glb。前者会在控制台报document is not defined后者什么都不报因为异常在 loader 内部被捕获了。解决先确认导出格式。让美术重新导一份.gltf 外置贴图如果换格式就正常了那就别在 glb 内嵌上继续挣扎。必须用单文件 glb 的话在GLTFLoader里找URL.createObjectURL那段改成把 bufferView 转 base64 塞进data:URI。5.4 手指往左划模型往右转现象交互能用但方向是反的或者转动的幅度和手指位移不成比例屏幕边角处偏得特别厉害。原因用了clientX或者pageX而不是 canvas 触摸事件里的x/y。前两者在小程序 canvas 上是undefined拿到的dx是 NaN某些机型上 NaN 参与运算会被截断成 0 或者异常值表现就是方向反了或者乱转。另一种情况是 canvas 的 CSS 尺寸和绘制缓冲尺寸不一致rpx 没换算坐标比例对不上。解决统一用e.touches[0].x和e.touches[0].y并且在onTouchMove里加一行断言if (typeof t.x ! number) return出问题能立刻定位。旋转系数不要写死改成按 canvas 宽度归一dx / canvasWidth * 2各种屏幕上手感才一致。5.5 来回切页面几次后进来黑屏或直接闪退现象第一次进正常退出再进也正常切到第五六次开始黑屏或者微信直接闪退到聊天列表。原因WebGL 上下文和显存没释放。小程序单页面的上下文数量有配额页面onUnload之后如果不主动交还配额被占满新的 canvas 创建出来就是个空壳能拿到节点但getContext返回 null。解决onUnload里按上一章的顺序做完整释放特别是forceContextLoss()。验证办法很土但有效在onUnload里打一行日志连续进出页面十次看日志是不是每次都打、内存曲线开发者工具的性能面板是不是回到基线。回不去就说明还有引用没断。6. 进阶Raycaster 拾取与上线前的自检模型能转能看之后产品经理多半会提下一个需求“点一下模型的某个部位弹个说明”。这就是拾取用Raycaster做但小程序里坐标转换这步要特别小心。onTap(e) { const t e.changedTouches[0] const rect this.canvasSize // 初始化时存下来的 { width, height }px // canvas 坐标 → 归一化设备坐标这一步错了就永远点不中 const ndc new this.THREE.Vector2( (t.x / rect.width) * 2 - 1, -(t.y / rect.height) * 2 1 ) const raycaster new this.THREE.Raycaster() raycaster.setFromCamera(ndc, this.camera) const hits raycaster.intersectObjects(this.pickables, true) if (!hits.length) return // 拿到的第一个交点object.userData 里放业务 id const id hits[0].object.userData.partId this.setData({ activePart: id }) }两个参数要盯住。rect.width、rect.height必须是 canvas 的实际 px 尺寸不是 rpx、不是pixelRatio乘过的绘制缓冲尺寸用错会整体偏移越靠边越明显。intersectObjects的第二个参数true表示递归子节点模型拆件之后目标物往往是深层子 mesh不传true永远拿不到。另外pickables只放需要响应的对象别把整个场景丢进去三角形数量大的时候射线检测是 CPU 侧开销能省则省。上线前我会固定跑一遍这套自检连续进出页面十次看内存是否回落把手机调成省电模式看帧率用 3 倍像素比的旧安卓机跑一遍拖动断网进页面看降级提示是否出现模型换成一个 5 万面的“超标样本”看会不会闪退。这五项花不到二十分钟能挡掉绝大部分线上事故。血泪经验是有一年吃过一次亏本地调试全用模拟器上线当天安卓端大面积黑屏回过头才发现整个链路里pixelRatio和贴图 mipmap 两处都没做限制。从那以后我每次接 3D 需求第一步就是在真机上先跑一个立方体把渲染管线确认过再谈模型。这套东西拆开看每一步都不复杂难的是每一步都有一个容易遗漏的适配点希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询