老读者都知道,我在项目里最常用的就是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 多面体家族与 PolyhedronGeometry
Three.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。