资讯详情

资讯详情

Three.js内置几何体全解析:参数含义与实战技巧

老读者都知道我在项目里最常用的就是BoxGeometry和SphereGeometry但一遇到环面、多面体、车削体这类形状还是得临时翻文档。很多人习惯把 Three.js 里的几何体叫“集合体”其实准确说法是 Geometry几何体它就是描述物体顶点、面、法线、UV 这些数据的容器。这篇博文把 Three.js 内置几何体的快速创建方式、参数含义和坑位一次性理清楚基于我实际敲代码的经验来写适合刚入门想系统掌握几何体的朋友也适合已经写了几个 Demo 但总是记不住参数的老手。1. 快速创建几何体的前置理解从构造函数的参数套路说起1.1 几何体不是“模型”是“数据”很多新手容易把“几何体”和“模型”混为一谈。Three.js 里的几何体BufferGeometry本质上是顶点坐标数组 索引数组 法线数组 UV 数组的组合它只负责描述物体的形状不负责外观。外观由材质Material决定把几何体和材质组合成Mesh才能在场景里看到一个有颜色的物体。这个理解很重要因为它解释了为什么创建几何体时关注的是“分段数”“半径”“是否开口”这些参数而不是“颜色”“光泽”“贴图”。快速创建几何体实际上就是快速生成一组合理的顶点数据。Three.js 提供了一整套内置几何体类目的就是让你不用手写顶点坐标直接传参数就能生成常见的形状。1.2 内置几何体的通用套路构造函数参数顺序Three.js 内置了几十种几何体类但我用下来发现它们的构造函数参数有一个通用套路第一类参数控制形状的尺寸半径、宽度、高度、深度。第二类参数控制形状的精细度分段数、段数、节数。第三类参数控制形状的局部结构是否开口、是否倒角、是否封闭。举个例子SphereGeometry(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength)中radius是尺寸widthSegments和heightSegments是精细度后面四个参数控制切割范围。这个逻辑几乎贯穿所有内置几何体你只要记住“尺寸在前细分在后特殊结构在最后”看文档时就不会一头雾水。提示Three.js 内部大多使用BufferGeometry保存数据老版本的Geometry类早已移除。如果你在网上看到new THREE.Geometry()的代码那是旧教程直接跳过。2. 高频几何体逐个击破参数背后的“为什么”2.1 BoxGeometry为什么要调分段数BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)是最基础的几何体很多人只会用前三个参数。实际上第四个参数开始控制的是每个面上横竖方向被细分成多少个小网格。默认分段数都是 1也就是一个面由两个三角形组成。那什么时候需要提高分段数我总结了两类场景需要顶点数量来支撑形变效果。比如做一个波浪动画或者柔体效果只有顶点足够多顶点着色器或物理插件才有地方发力。分段数设为 1 时一个面只有 4 个顶点怎么改都看不出波浪。需要更精细的法线过渡。不过长方体的棱角本身就是硬边提高分段数对平滑效果帮助不大更多是配合纹理重复或顶点色做文章。在我自己的项目里普通盒子就用默认分段数。一旦涉及动画形变我会把主要形变方向的分段数提到 16 到 32。分段数不是越高越好分段越高顶点越多内存和 draw call 可能不变但顶点着色器压力会上去。对于静态网格来说个位数的分段已经足够。2.2 SphereGeometry经纬网格的秘密SphereGeometry(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength)是我用得最多的几何体之一但十个新手里有八个不调后面四个切割参数。它本质上是按经纬线方式生成球面网格widthSegments控制经线方向水平方向的分段数heightSegments控制纬线方向垂直方向的分段数。默认是 32 段和 16 段这个精度在大多数时候够用。如果你做低多边形风格可以刻意调低到 8 和 6会有一种复古的切面质感。后四个参数特别实用phiStart和phiLength控制水平方向的起始角度和扫描范围用它可以做“半颗球”。thetaStart和thetaLength控制垂直方向的起始角度和扫描范围用它可以做“球冠”或去掉球顶。我曾在做大气层效果时需要只显示球的上半部分最初是创建一个完整球体然后用着色器裁剪效率很差。后来直接用thetaLength设置为Math.PI / 2只画上半球顶点数瞬间减少渲染效率也更高。记住能通过几何体参数裁剪的就别在渲染时裁剪GPU 不做无用功。2.3 CylinderGeometry不只是圆柱CylinderGeometry(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength)这组参数里最容易理解偏差的是radiusTop和radiusBottom。把顶面半径设为 0底面半径保留就成了圆锥体。把顶面和底面半径设为不同的值就成了圆台。我用这个特性做过很多变体比如灯罩、喇叭口、管道连接件都不需要额外使用ConeGeometry。实际上 Three.js 里的ConeGeometry就是CylinderGeometry的封装内部只是把radiusTop固定为 0 而已。openEnded这个参数经常被忽略它控制圆柱两端是否封口。做水管、隧道、罐体时经常需要不封口这样从内部看过去不会出现奇怪的封闭面。默认是false封口所以要记得设为true。还有一点radialSegments控制圆周方向的分段数。默认是 8不是 16 也不是 32。这个默认值会导致圆柱看起来有明显棱角。如果你想要平滑圆柱至少调到 16 或 32如果你故意做低多边形风格保留默认反而合适。2.4 TorusGeometry 与 TorusKnotGeometry环形类几何体TorusGeometry(radius, tube, radialSegments, tubularSegments, arc)是一个圆环体。radius是环的整体半径tube是管道截面的半径radialSegments是管道截面的分段数tubularSegments是绕环一周的分段数arc是环的弧度范围默认 2π整圈。这里容易混淆的就是radius和tube的关系。可以想象成甜甜圈radius决定了甜甜圈整体的大小tube决定了面的粗细。很多人把radius调很大、tube调很小结果发现环细得像根线这不是 bug而是参数理解问题。TorusKnotGeometry(radius, tube, tubularSegments, radialSegments, p, q)是环面结比普通圆环体更有辨识度。参数里p和q是绕数控制结的缠绕方式。我印象中(2, 3)产生的是三叶结(3, 2)出来的形态又不一样。调这两个参数时可以大胆试数值范围不大但形态变化非常直观。环形类几何体在创建时要注意渲染层级问题。圆环内部是空的很多新手以为把TorusGeometry和SphereGeometry放在一起会自动“套上”实际上这只是网格相交看起来会有穿插。如果你需要做“环内套球”的效果建议把环的tube调大一些让结构上有足够空间否则只能看到 z-fighting 和穿插的边缘线。3. 容易忽略但很实用的那些几何体3.1 PlaneGeometry 不只是地面PlaneGeometry(width, height, widthSegments, heightSegments)是最容易上手的几何体但它的两个特性值得注意。第一它默认是单面正面可见从背面看是透明的。这是因为平面网格只有正面法线。如果你需要两面都显示要么把材质的side设为THREE.DoubleSide要么给平面加一个BackSide材质副本。从性能角度讲尽量用THREE.FrontSide或THREE.DoubleSide按需选择不要每个平面都开双面毕竟双面渲染相当于多渲染一倍的面。第二它默认位于 XY 平面即垂直平面。做地面时需要旋转 -90 度或者使用PlaneGeometry后转一下rotation.x。很多人忘了这一步把PlaneGeometry当作天花板放结果怎么调整位置都觉得不对。后来我发现直接对PlaneGeometry的rotation.x设置-Math.PI / 2是最直观的做法。3.2 ShapeGeometry从二维轮廓到三维体ShapeGeometry这个类特别有意思它可以把一个二维的THREE.Shape形状直接变成一个平面网格。比如你画一个五角星、一个齿轮轮廓、一个字形的外边框只要用Shape描述好轮廓ShapeGeometry就能生成对应的面。THREE.Shape支持moveTo、lineTo、quadraticCurveTo、bezierCurveTo、arc等路径方法就像在 2D 画布上画画一样。我做过一个印章效果就是先用Shape画出外圈圆再用ShapeGeometry生成配合贴图做个章面背景效果非常好。另一个值得提的是ExtrudeGeometry它是在ShapeGeometry的基础上增加了挤出高度把平面轮廓变成有厚度的三维体。如果你要做立体字、徽章、模型底座ExtrudeGeometry比手写顶点数据省太多时间。3.3 多面体家族与 PolyhedronGeometryThree.js 内置了TetrahedronGeometry四面体、OctahedronGeometry八面体、DodecahedronGeometry十二面体、IcosahedronGeometry二十面体它们的构造函数都是(radius, detail)。这里的detail参数很有趣。默认是 0表示不做细分。当你把detail调成 1每个面会被细分成更小的三角形网格几何体表面的硬棱角会逐渐柔和接近球体。这本质上是做“球面细分”等价于把每个面递归细分后再投影到球面上。我个人非常喜欢用IcosahedronGeometry做“低多边形球体”。把detail设为 0 时二十面体只有 20 个三角形面视觉上非常有几何美感设为 1 时有 80 个三角形面已经很接近圆润的球体。这种“可调细节”的几何体在风格化项目里特别好用。PolyhedronGeometry(vertices, indices, radius, detail)这个类更灵活它允许你自己传一个顶点数组和索引数组然后 Three.js 会帮你生成法线、UV 等数据。如果你想创建完全自定义的凸多面体这是最快捷的路径省去手动维护BufferGeometry属性的麻烦。4. 实操一个场景快速创建并展示多种几何体4.1 搭建最小可运行场景先说明一下这里的所有代码都基于 Three.js 现代版本ES Module 方式。如果你用的是 CDN 引入的旧版本部分 API 名称可能不同建议以官方文档为准。import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; const scene new THREE.Scene(); scene.background new THREE.Color(0x1a1a2e); const camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(8, 6, 12); camera.lookAt(0, 0, 0); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled true; document.body.appendChild(renderer.domElement); const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true;这是我最常用的最小场景模板。阴影默认是关闭的但在几何体展示场景里开启阴影能帮你快速判断形状的立体感和相对位置所以我会顺手打开。4.2 批量创建并摆放几何体接下来我用一个数组保存不同几何体的创建函数统一生成网格并摆放。这样后面想加新几何体只加一行配置即可。const geometries [ new THREE.BoxGeometry(1, 1, 1), new THREE.SphereGeometry(0.6, 32, 16), new THREE.CylinderGeometry(0.4, 0.4, 1.2, 32), new THREE.ConeGeometry(0.5, 1.2, 32), new THREE.TorusGeometry(0.5, 0.2, 16, 48), new THREE.TorusKnotGeometry(0.5, 0.15, 64, 8, 2, 3), new THREE.OctahedronGeometry(0.6, 0), new THREE.IcosahedronGeometry(0.6, 1), new THREE.TetrahedronGeometry(0.6, 0), new THREE.DodecahedronGeometry(0.6, 0), new THREE.PlaneGeometry(1.4, 0.9), new THREE.CircleGeometry(0.6, 32), ]; const material new THREE.MeshStandardMaterial({ color: 0x4fc3f7, roughness: 0.3, metalness: 0.1, }); const group new THREE.Group(); geometries.forEach((geo, index) { const mesh new THREE.Mesh(geo, material); mesh.position.x (index % 4) * 2 - 3; mesh.position.z Math.floor(index / 4) * 2 - 1; mesh.castShadow true; group.add(mesh); }); scene.add(group);这里把 12 个几何体按四列三行排列每个间距 2 个世界单位基本能保证彼此不重叠。如果你想要更随机的效果可以用Math.random()控制位置和旋转但展示几何体时整齐排列更方便对比形状差异。4.3 光照与环境配置没有光照MeshStandardMaterial看起来会是一片黑色。我通常会加一个方向光做主光再加一个环境光做补光这样几何体的棱角和阴影都能看得很清楚。const dirLight new THREE.DirectionalLight(0xffffff, 2); dirLight.position.set(5, 10, 7); dirLight.castShadow true; scene.add(dirLight); const ambientLight new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight);我踩过一次坑DirectionalLight默认不产生阴影需要手动设castShadow true并且这里的Shadow相关属性如shadow.mapSize最好也调高一点否则圆柱这类几何体的阴影边缘会有明显的锯齿。一般我把mapSize设为 1024 或 2048足够日常展示。渲染循环部分function animate() { requestAnimationFrame(animate); group.rotation.y 0.003; controls.update(); renderer.render(scene, camera); } animate();4.4 场景延伸用参数面板调整几何体如果你想把“快速创建”变成“快速调试”我建议引入一个简单的 GUI 面板。我用过很多调试工具最顺手的还是 lil-gui轻量级不占体积。import GUI from lil-gui; const gui new GUI(); mesh.geometry.dispose(); mesh.geometry new THREE.BoxGeometry(1, 1, 1, 1, 1, 1);实际项目中我不会真的每次拖拽参数就重建几何体但调试时非常好用可以直观看到SphereGeometry的widthSegments从 4 变到 32 时表面有什么变化。这种“参数 实时预览”的思路适用于所有几何体能帮你快速建立直觉。5. 高级技巧合并几何体与轻量化自定义5.1 mergeGeometries 的合理使用一个 Three.js 场景里每个Mesh都是一个独立的对象渲染时会产生一个 draw call。如果你的项目里有几百个盒子、几十个圆柱每个都是独立Mesh性能会明显下滑。这时可以把几何体合并成一个。BufferGeometryUtils提供了mergeGeometries方法import * as BufferGeometryUtils from three/addons/utils/BufferGeometryUtils.js; const merged BufferGeometryUtils.mergeGeometries([ new THREE.BoxGeometry(1, 1, 1), new THREE.SphereGeometry(0.5, 16, 8), new THREE.CylinderGeometry(0.3, 0.3, 1, 16), ]); const mesh new THREE.Mesh(merged, material);有一个关键细节合并后的几何体只能使用同一个材质。如果你的盒子是红色、圆柱是蓝色就不能直接用mergeGeometries而要用mergeGroups或Group单独渲染。我之前犯过这个错把不同材质的一组几何体强行合并结果所有部分都变成一个颜色排查了很久才意识到问题出在材质上。所以我的建议是静态装饰物、粒子形状、地面碎石这类不需要单独控制的物体合并几何体收益很大需要独立交互或独立材质的物体不要合并。5.2 手写 BufferGeometry 的入门思路内置几何体覆盖了大多数需求但总有不够用的时候。比如你需要的是一面带有特殊纹理扭曲的墙壁或者一个形状不规则的岩石内置几何体就帮不上忙了。这时候需要手写BufferGeometry。以下是最简单的手写三角形示例const geometry new THREE.BufferGeometry(); const vertices new Float32Array([ -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 0.0, 1.0, 0.0, ]); geometry.setAttribute(position, new THREE.BufferAttribute(vertices, 3)); geometry.computeVertexNormals();setAttribute里的第二个参数3表示每个顶点由三个数值组成x、y、z。如果不调用computeVertexNormals()你会发现这个三角形在某些光照角度下黑得不对劲因为根本没有法线数据。手写几何体的好处是你能完全掌控顶点布局适合做水面、地形、片元着色器实验等。坏处是容易漏掉法线或 UV导致显示异常。我自己写自定义几何体时会遵循一个惯例先定义顶点数组。再定义索引数组如果使用索引绘制。然后setAttribute添加position按需添加normal和uv。最后调用computeVertexNormals()自动计算法线。如果只是把顶点摆好但不加索引渲染时每个三角形都需要独立顶点内存会明显增加。多花一分钟写对索引运行效率完全不一样。6. 常见问题与排查技巧实录6.1 几何体全黑或颜色发黑这个现象十次有八次是法线方向错误或缺失。法线是决定光照计算方向的关键数据一旦法线朝向背面光照就照不到正面物体看起来就是暗的。排查路径确认用的材质是MeshStandardMaterial还是MeshBasicMaterial。MeshBasicMaterial不参与光照理论上不会发黑如果你用了MeshStandardMaterial但没加任何光也会全黑。先加一盏方向光再检查法线是通用的排查顺序。如果自定义几何体发黑多半是没调computeVertexNormals()。内置几何体一般不会出现这个问题因为它们构造函数里已经计算好了法线数据。6.2 几何体尺寸与单位感知Three.js 默认使用“米”作为世界单位BoxGeometry(1, 1, 1)是 1 米见方的盒子不是 1 像素。很多从 2D 转过来的同学会下意识把1当成 1px结果场景里的物体要么大得离谱要么小到看不见。我的经验是在编辑器或模型软件里建模时先设置好单位。如果是从外部导入模型注意导入尺寸是否被缩放。之前导入一套建筑模型顶点坐标动辄几千摄像机视角无论怎么拉都装不下最后统一缩放单位到 0.01 才恢复正常。尺寸感知问题还会影响相机参数。PerspectiveCamera的远裁剪面默认是 1000如果你的几何体坐标超过这个范围就会直接被裁剪掉物体“消失”了。排查时先看物体位置和相机远裁剪平面距离别一上来就怀疑渲染管线。6.3 分段数的认知误区我曾经在一个粒子效果里把SphereGeometry(1, 64, 32)的球放了 200 个帧率直线下降。后来换成SphereGeometry(1, 16, 12)视觉上几乎看不出差别但性能明显改善。很多项目根本不需要 64 段的分段数尤其是小物体、远景物体、低多边形风格模型。你需要一个重要认知分段数影响的是视觉平滑度和顶点数而不是模型的“分辨率”或“清晰度”。纹理清晰度由纹理图片本身决定跟几何体分段数没有直接关系。判断分段数是否够用的方法是旋转物体观察边缘轮廓如果能看到明显的多边形切面就提高分段如果看不到就不必再提。没有统一标准按需调整。6.4 关于双面渲染的误解刚入门时总喜欢把所有材质都设成side: THREE.DoubleSide认为这样看着更“完整”。实际上双面渲染会让片段着色器的工作量翻倍而且容易掩盖法线方向的错误导致模型导入后奇怪的光照问题。我的建议是平面几何体地板、广告牌需要双面时再开双面封闭的立体几何体球、盒子、圆柱保持默认的单面即可。如果你看到封闭几何体“内部透明”或“背面消失”这通常是法线方向反了而不是需要开双面。检查顶点顺序和法线方向从根上解决问题。比如CylinderGeometry如果openEnded设为true侧面是单面的从内侧看就会消失。这是正常现象不是 bug。你若是想让它内侧可见才需要把材质设为双面。6.5 快速排查几何体是否显示的最小测试法我每次遇到几何体不显示或显示异常都会退到最简单的测试环境一个MeshBasicMaterial、一个方向光、一个默认场景。如果MeshBasicMaterial下能显示但MeshStandardMaterial下不能问题出在光照或法线如果都不显示问题出在几何体数据本身或相机范围内。这个最小测试法帮我省去了无数排查时间。先排除环境因素再聚焦几何体数据问题范围一下就缩小了。7. 从“创建”到“使用”的几个进阶建议7.1 几何体对象要记得释放几何体创建后如果不再使用最好调用geometry.dispose()释放 GPU 内存。尤其在单页应用里频繁切换场景时旧几何体不释放内存会一路飙升。我自己是在场景切换函数里统一处理function clearScene() { scene.traverse((obj) { if (obj.isMesh) { obj.geometry?.dispose(); obj.material?.dispose(); } }); scene.clear(); }贴图material.map持有的纹理对象也要dispose。纹理如果不手动释放即使几何体释放了纹理数据残留在 GPU 显存里同样会导致内存问题。7.2 善用克隆与实例化同样的几何体如果只是位置、旋转、缩放不同可以复制几何体引用const baseGeo new THREE.SphereGeometry(0.5, 16, 8); const mesh1 new THREE.Mesh(baseGeo, material); const mesh2 new THREE.Mesh(baseGeo, material);只要不修改几何体的属性多个Mesh可以共享同一个几何体这样不会重复占用显存。对于成千上万的相同物体可以用InstancedMesh做实例化渲染一个 draw call 就能渲染几百个实例性能差距巨大。7.3 几何体调试三板斧最后分享一个我调试几何体时常用的组合拳开启renderer.useLegacyLights false新版本默认确保光照行为符合现代物理光源逻辑。给几何体加一个EdgesGeometry线框作辅助直接看清网格拓扑结构const edges new THREE.EdgesGeometry(geometry); const line new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff })); mesh.add(line);把camera.position移到(0, 0, 5)并看向原点排除视野问题干扰。这三板斧配合起来基本能快速定位 80% 的几何体显示问题。尤其是EdgesGeometry直接显示棱边和网格密度比盯着着色器结果猜要直观得多。我个人在实际操作中最大的体会是Three.js 的几何体创建不是“背 API 参数”而是理解每个参数背后的几何意义。SphereGeometry的切割参数、CylinderGeometry的上下半径、TorusKnotGeometry的 p 和 q每调一个数值你脑子里应当浮现出形状的对应变化。多拖拽几次 GUI 参数多建几个最小 Demo很快你就能形成下意识的判断这个形状应该用哪个几何体分段要调到多少纹理是否要配合 UV 调整。这种“手感”比单纯记住文档里的参数列表重要得多。下次遇到题目里说的“集合体”希望你能会心一笑然后熟练地敲出一个正确的BufferGeometry。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →