资讯动态

网页3D模型展示全攻略:Three.js、glTF/GLB与交互分享

发布时间:2026/9/29 1:23:49 来源:尧图企业网站定制
你有没有遇到过这种情况辛辛苦苦用 Blender、SolidWorks 或者 C4D 建好了一个 3D 模型发给客户看对方要么得装软件要么得下载好几个 G 的工程文件最后只能截几张图凑合看。尤其是做工业设备、文物数字化、建筑方案汇报的时候静态图片根本讲不清楚结构关系客户反复问“这个角度能看到背面吗”“内部结构能不能拆开看”你只能干瞪眼。把 3D 模型直接放到网页里发一个链接就能让对方打开浏览器随便转、随便看、随便点甚至还能自己拖拽、旋转、缩放看完还能一键分享给同事——这套流程其实没那么玄乎而且现在的技术成熟度已经很高了。这篇文章我就把从零开始做网页 3D 模型展示的完整路径拆开讲清楚包括技术选型的思路、模型格式怎么处理、展示浏览互动分享每一步怎么落地以及我在实际项目里踩过的一堆坑。适合想做产品展示页的开发者、需要交付可视化方案的工程师还有单纯想把自己做的模型放上网的设计师。1. 内容整体设计与思路拆解1.1 先搞清楚“网页 3D 展示”到底要解决什么问题很多人的第一反应是“用 WebGL 渲染一个模型进去”但这只是最表层的动作。我做了几个项目之后再回头看发现网页 3D 展示本质上是三件事的打包资产轻量化、渲染实时化、交互人性化。资产轻量化说的是模型文件不能在网页里加载一个几百 MB 的原始工程文件必须转成浏览器能高效解析的格式比如 glTF/GLB。渲染实时化意味着所有画面都是 GPU 实时算出来的所以视角可以任意切换不像预渲染视频那样只能沿着固定路径走。交互人性化则是说浏览者能通过鼠标拖拽、滚轮缩放、点击热区这些最自然的方式去理解模型。这三个需求决定了整个技术路线的走向。如果你只是想把一个模型放到网页上“能看就行”那可以用一些傻瓜工具一键生成但如果要做到流畅浏览和丰富互动就必须自己掌握核心渲染引擎的控制权。我的建议是——除非你的模型非常简单、交互需求几乎为零否则尽量选择自己掌控渲染方案因为一旦后续要加功能被封装好的工具会极度束缚手脚。1.2 技术选型Three.js 是当下性价比最高的答案现在主流的 WebGL 渲染方案无非就几个原生 WebGL、Three.js、Babylon.js以及近年比较火的 WebGPU 方向。我直接说结论没有特殊原因就用 Three.js。原生 WebGL 的问题是开发效率太低——你要自己管理缓冲区、着色器、矩阵变换写一个能转视角的场景就得几百行代码而且还要处理各种兼容性问题。WebGL 本质上是一个光栅化接口不是给你做场景管理的直接用它建一个“可浏览的 3D 展厅”就像用汇编语言写操作系统能写但没必要。Babylon.js 也很强大而且它的内置 UI、场景编辑器都做得不错但生态和资料比 Three.js 少一截。真实感受是遇到问题时 Three.js 的解决方案一搜一大把而 Babylon.js 经常要自己啃源码。对于大多数项目来说这个差距很致命。Three.js 的优势在于API 设计直观、官方文档和示例丰富、社区庞大、扩展库齐全后面要讲的轨道控制器、射线检测、模型加载器都是它的生态。而且它底层的 WebGL 渲染器经过多年优化性能和兼容性都很成熟。如果你未来想升级到 WebGPUThree.js 也提供了对应的渲染器支持算是给自己留了一条升级路径。1.3 方案选型背后的核心考量性能、开发效率与场景适配选技术路线时我一般看三个维度模型复杂度、交互深度、目标设备。模型复杂度决定了你要不要在压缩和 LOD多层次细节上花功夫。交互深度决定了你是只需要轨道控制还是要做点击拾取、动画播放、模型拆解。目标设备则直接决定渲染精度——如果客户主要用手机看你按 PC 端标准做高精材质最终结果就是卡成幻灯片。另外一个常被忽略的点是工程化能力。Three.js 可以很好地融入 Vite、Webpack 这些现代前端工程体系组件化之后复用性很强。我以前接过一个数字化展厅项目里面要放二十多个可交互设备模型如果每个页面都从零搭场景维护成本简直不敢想。后来抽了一个公共组件传入模型路径和配置项就能生成可交互的展示模块后面接新展品只需要准备模型和写几行配置效率直接倍增。2. 环境准备与模型资产处理2.1 搭建一个现代化的 Three.js 项目我推荐直接用 Vite 搭项目比传统的 script 标签引入方式舒服太多。 Vite 启动快、热更新及时而且内置了对 glTF/GLB 等静态资源的处理能力。具体操作如下npm create vitelatest web3d-demo -- --template vanilla cd web3d-demo npm install three npm install --save-dev vite-plugin-static-copy然后在main.js里引入 Three.js 核心库import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; import { GLTFLoader } from three/addons/loaders/GLTFLoader.js;注意Three.js 从 r150 左右开始示例模块的导入路径统一走three/addons/直接在 node_modules 里找路径容易迷路最好用官方推荐的导入方式。2.2 模型格式选择glTF/GLB 是王道其他格式看情况做网页 3D 展示模型格式的选择直接决定后续流程顺不顺。我踩过的坑排序大概是OBJ 没有材质层级、FBX 依赖 Maya/3ds Max 的坐标系和动画配置、STL 只有三角面没有任何材质信息、3DS 更是远古产物。所以答案是尽量用 glTF/GLB。glTF 被称为“3D 界的 JPEG”设计目标就是为 GPU 实时渲染而生。它把网格、材质、骨骼动画、摄像机、灯光等信息打包成结构化数据而且支持二进制容器格式 GLB——一个文件装下所有资源前端加载特别方便。如果你的原始模型是 SolidWorks 导出的 STL或者 CAD 转了 OBJ我建议先用 Blender 做一次中转把模型导入后重新导出为 GLB。这个过程顺便可以做减面、展 UV、烘焙贴图等优化。注意从 Blender 导出时默认坐标轴是 Z 轴向上Three.js 的场景是 Y 轴向上所以导出时需要正确设置变换不然模型会“躺”在场景里。2.3 模型优化Draco 压缩与纹理处理是流畅的关键一个高精模型动辄几十上百 MB直接放到网页里加载用户等 10 秒还没看到东西早就关页面了。这里必须做减法和压缩。几何压缩我用的是 Google 的 Draco。它是专门为 3D 网格设计的压缩算法压缩率能达到 80%~90%。Three.js 官方提供了 DracoLoader使用方式如下import { DRACOLoader } from three/addons/loaders/DRACOLoader.js; const dracoLoader new DRACOLoader(); dracoLoader.setDecoderPath(https://www.gstatic.com/draco/v1/decoders/); loader.setDRACOLoader(dracoLoader);注意 Draco 的 decoder 文件要放到可访问的静态路径下本地开发时可以直接引用 gstatic 的 CDN生产环境建议把 decoder 文件下载到自己的服务器避免外部依赖。纹理处理上我一般的做法是漫反射贴图、法线贴图、粗糙度贴图都压缩到 2048 或 1024 分辨率格式转成 WebP 或 JPEG根据是否有透明通道选择。Three.js 的texture.colorSpace记得设为THREE.SRGBColorSpace否则颜色会偏淡发灰。这个细节很多人忽略出来的效果就是颜色不饱和客户一看就说“质感不对”。3. 核心功能实现展示、浏览、互动到分享3.1 展示写一个能加载 GLB 模型的最小渲染场景先把最核心的渲染流程跑通。一个 Three.js 场景最少要有三样东西场景容器、相机、渲染器。下面这段代码是基础骨架import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; import { GLTFLoader } from three/addons/loaders/GLTFLoader.js; const scene new THREE.Scene(); scene.background new THREE.Color(0xf5f5f5); const camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(5, 4, 8); camera.lookAt(0, 0, 0); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.shadowMap.enabled true; document.body.appendChild(renderer.domElement); const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; controls.dampingFactor 0.08; // 灯光环境光保证整体可见平行光产生阴影半球光补光 const ambientLight new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const dirLight new THREE.DirectionalLight(0xffffff, 1.2); dirLight.position.set(10, 20, 10); dirLight.castShadow true; scene.add(dirLight); const hemiLight new THREE.HemisphereLight(0xffffff, 0x444444, 0.4); scene.add(hemiLight); // 加载模型 const loader new GLTFLoader(); loader.load(/models/product.glb, (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()); model.position.sub(center); const maxDim Math.max(size.x, size.y, size.z); const scale 4 / maxDim; model.scale.setScalar(scale); scene.add(model); }); function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); // 窗口尺寸变化时自适应 window.addEventListener(resize, () { camera.aspect window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });这段代码跑起来你就能在浏览器里看到一个可以旋转、缩放、平移的 3D 模型了。Box3归一化这段逻辑非常重要——不同建模软件导出的模型尺寸千差万别有的单位是米有的是厘米有的模型在世界坐标系里离原点十万八千里。如果不做归一化经常出现模型一个面怼到相机脸上或者小成一个点的情况。3.2 浏览轨道控制器的参数调优与多视角体验OrbitControls 默认就能用但想让浏览体验更“专业”有几个参数值得花心思调。第一个是minDistance和maxDistance。这两个值限制相机缩放范围避免用户把视角拉到模型里面看到穿模的内表面或者拉太远模型变成一个小点。比如一个 4 个单位大小的模型我一般把 maxDistance 设为 20minDistance 设为 1.5。第二个是maxPolarAngle。这个限制垂直旋转角度防止用户转到模型正下方仰视把底部的三角面暴露出来。比如产品展示一般限制在 0 到 Math.PI 之间也就是不转到正下方。第三个是阻尼enableDamping。我之前测试发现开了阻尼之后旋转手感明显更平滑有种“重量感”不会一松鼠标就“啪”地停住。这个参数的调优直接决定用户对展示页面的第一印象——很多人说“你的页面看起来很高级”其实就是阻尼做得好。更进阶一点的多视角体验可以预设几个视角按钮比如“正面”“侧面”“俯视”“45° 黄金视角”。实现方式其实很简单就是用gsap或自写的补间动画把相机位置平滑过渡到预设坐标function flyTo(position, target) { // 用一个简单的时间插值实现平滑过渡也可以用 GSAP const startPos camera.position.clone(); const startTarget controls.target.clone(); const duration 800; const startTime performance.now(); function update() { const elapsed performance.now() - startTime; const progress Math.min(elapsed / duration, 1); const ease 1 - Math.pow(1 - progress, 3); // easeOutCubic camera.position.lerpVectors(startPos, position, ease); controls.target.lerpVectors(startTarget, target, ease); controls.update(); if (progress 1) requestAnimationFrame(update); } update(); }3.3 互动射线检测、模型拆解与热点标注浏览只是基本功真正的亮点在于互动。最常见的三个互动场景点击模型部件高亮、模型拆解/爆炸视图、热点标注弹窗。点击高亮的核心是射线检测。原理也简单当用户点击屏幕时从相机位置发射一条射线计算这条射线与场景中所有网格的交点命中的就是用户点到的物体。Three.js 的Raycaster把这件事封装得很到位const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); renderer.domElement.addEventListener(click, (event) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(model.children, true); if (intersects.length 0) { const obj intersects[0].object; // 向上查找可交互的父级 let target obj; while (target.parent !target.userData.interactive) { target target.parent; } if (target.userData.interactive) { highlightObject(target); showInfoPanel(target.userData.info); } } });这里有个细节intersectObjects的第二个参数设为true才会递归检测子网格。模型加载进来之后直接model.children通常只能拿到顶层节点真正的网格可能嵌套了好几层不递归的话很可能点不到东西。另外建议给每个可交互部件设置userData把名称、介绍、链接等信息挂上去这样后续逻辑清晰很多。模型拆解是我在工业设备展示里最喜欢的交互。原理并不复杂——每个部件有一个“装配位置”和一个“拆解位置”一般是沿某个轴偏移一定距离动画就是在这两个位置间做插值。比如发动机拆解可以把缸体、活塞、曲轴分别定义拆解方向和距离点击“爆炸视图”按钮后所有部件同时向外移动结构关系一目了然。热点标注则更偏向数字化展厅场景。在每个关键部件位置放一个小标签HTML 元素或 Sprite点击标签弹出图文说明。实现上核心是把 3D 坐标投影到屏幕坐标function getScreenPosition(worldPos) { const vector worldPos.clone().project(camera); return { x: (vector.x * 0.5 0.5) * window.innerWidth, y: (-vector.y * 0.5 0.5) * window.innerHeight }; }这个函数会在每次渲染时被调用保证标签始终跟随模型部件。注意当部件转到相机背面时要判断vector.z 1然后隐藏标签不然会出现标签倒挂的穿帮效果。3.4 分享状态序列化、截图分享与社交链路打通分享环节是整个流程的收尾也是很多项目一开始根本没想、最后被客户追着补的功能。其实 3D 展示的分享可以拆成两个层面状态分享和渠道分享。状态分享是指把用户当前的视角状态相机位置、旋转角度、缩放比例编码到 URL 参数里别人打开这个链接就能“瞬移”到同一个视角。实现方式很直接// 分享时把相机位置和控制器目标点编码进 URL function getShareUrl() { const pos camera.position; const target controls.target; const params new URLSearchParams({ px: pos.x.toFixed(3), py: pos.y.toFixed(3), pz: pos.z.toFixed(3), tx: target.x.toFixed(3), ty: target.y.toFixed(3), tz: target.z.toFixed(3) }); return window.location.origin window.location.pathname ? params.toString(); } // 加载时解析 URL 参数并设置相机 function applyShareParams() { const params new URLSearchParams(window.location.search); if (params.has(px)) { camera.position.set( parseFloat(params.get(px)), parseFloat(params.get(py)), parseFloat(params.get(pz)) ); controls.target.set( parseFloat(params.get(tx)), parseFloat(params.get(ty)), parseFloat(params.get(tz)) ); } }渠道分享则是把链接通过微信、微博、邮件等渠道发出去。现在大多数场景是 H5 页面所以核心是把 URL 生成二维码或者调起 Web Share APIif (navigator.share) { navigator.share({ title: 3D 模型展示, text: 来看看这个 3D 模型, url: getShareUrl() }); }另外很多运营场景需要一张“封面图”也就是模型的静态渲染截图。用 Three.js 的renderer.domElement.toDataURL()可以实现function captureSnapshot() { renderer.render(scene, camera); const dataURL renderer.domElement.toDataURL(image/png); const link document.createElement(a); link.download snapshot.png; link.href dataURL; link.click(); }这个小功能看起来不起眼但在客户提“我想把这个模型发给同事看但不想下载安装任何东西”的需求时截图 链接一套组合拳能解决大部分沟通问题。4. 常见问题与排查技巧实录4.1 模型加载后黑屏什么也看不见这个问题我遇到太多次了。排查顺序一般是这样先看控制台有没有报错再看模型是否加载成功loader.load的回调有没有执行最后检查相机朝向和灯光。90% 的黑屏原因是灯光没打好或相机在模型内部。尤其是从 Blender 导出的模型如果没有在 Blender 里添加灯光整个场景就是全黑的——因为 Three.js 默认没有光源。所以新手最容易犯的错是忘了加灯。另外检查摄像头位置如果模型尺寸特别大而相机初始位置在模型内部四面都是网格的内表面自然什么都看不出来。还有一个隐蔽问题模型材质使用了MeshStandardMaterial但没有环境贴图金属度较高的物体会显得暗黑一片。建议加载一个简单的环境贴图可以用 Three.js 的RoomEnvironment或者调高环境光强度。4.2 模型表面闪烁、破面闪烁Z-fighting是因为两个表面几乎完全重合深度缓冲区分不出谁前谁后。常见于模型在建模软件里复制了面没有合并或者地面和模型底座贴在一起。解决办法很简单把重合的两个面做一个细微偏移比如 0.001或者在 Blender 里用“合并重叠面”功能清理模型。如果闪烁发生在阴影区域可以调整阴影偏移量dirLight.shadow.bias -0.0005;4.3 模型加载慢、页面卡顿性能问题的优化顺序是先看几何复杂度顶点数、三角面数再看材质和贴图最后看渲染设置。几何复杂度最简单粗暴的优化就是减面。Blender 里用“Decimate”修改器可以快速减半三角面数视觉影响通常不大。材质方面把大尺寸贴图压缩到 1024 或 2048并确保使用 WebP 格式。渲染设置上如果不需要透明效果可以设置renderer.sortObjects true并按需调整抗锯齿策略。卡顿最容易被忽视的原因是每帧渲染的像素太多——也就是设备像素比设置太高。renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))这句是保命用的。手机上有些设备 devicePixelRatio 是 3如果按 3 渲染GPU 负载是 2 的 2.25 倍发热降频立刻卡成狗。4.4 移动端手势冲突OrbitControls 默认支持触摸手势单指旋转、双指缩放。但在 H5 页面里如果外层有滚动容器单指操作会把页面一起带动体验很割裂。我的做法是全屏展示时阻止页面滚动并给 canvas 加上touch-action: none样式canvas { display: block; touch-action: none; }这样触摸事件完全交由 Three.js 处理页面上其他区域正常滑动。如果模型展示只是页面的某个区块则建议把区块高度设置成接近全屏并打开controls.enablePan false减少手势冲突。4.5 分享链接参数丢失或被转义URL 参数在分享过程中被微信等聊天工具截断或转义是常见问题。解决方案是不要只依赖 URL 参数传递状态而是做一个短链服务把完整参数存到后端或数据库用短码映射。这个方案顺带还能统计分享人数和查看次数对运营很有价值。如果不方便做后端也可以把状态压缩成更短的字符串比如用encodeURIComponent(btoa(JSON.stringify(state)))再拼到 URL 后面但要注意长 URL 在部分 App 里会被截断。5. 经验心得做完十几个网页 3D 展示项目后我的感受是技术上并没有什么高不可攀的壁垒Three.js 把 WebGL 的门槛已经降得很低网上资料多到看不完。真正拉开差距的是流程上的精细度——模型有没有压缩到位、灯光氛围有没有调对、交互反馈是否跟手、分享链路是否顺畅。这些小点单独看都不难但连在一起决定了用户打开页面后的第一感受。如果你刚起步我的建议是先别追求炫酷的工业级效果用最简单的场景加载一个 GLB 模型把相机控制、点击交互、分享链接这三个主线跑通再做减法优化。一次只解决一个问题每次改完都要在真机特别是手机上测一遍这样很快就能攒出一套适合自己的模板。最后分享一个我常用的配置组合Vite 做工程底座Three.js 负责渲染Draco 压缩几何WebP 压缩纹理OrbitControls 做浏览Raycaster 做交互URL 参数做状态分享。这套组合的代码量不大、维护成本低、扩展性也好后面想加模型拆解、热点标注、自动旋转、多模型切换都只需要在现有骨架上加零件不会推翻重来。希望这篇文章能让你少走弯路早点把第一个可交互的 3D 模型挂到网上去。

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

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

免费获取报价 →
↑