Three.js 图元速查:官方内置的立体图形
原文出处:Three.js Manual -- Primitives
本文基于 three.js 官方手册"图元"章节整理,用通俗的方式带你过一遍 three.js 内置的所有 3D 基础形状。
写在前面
前面我们学会了搭场景、画立方体、画线、加文字。但光一个立方体显然不够玩------three.js 其实内置了一大堆"开箱即用"的 3D 形状,官方把它们叫做 Primitives(图元)。
这篇就来做一个全面的速查,把 three.js 里所有内置图元过一遍。以后做原型、做实验、或者临时需要一个形状 placeholder 的时候,你知道该用哪个。
一、什么是图元?
简单来说,图元就是 three.js 用代码帮你实时生成的 3D 几何体 。你给几个参数------比如半径、宽高、分段数------它就在内存里把顶点和三角面算出来,变成一个可以直接拿来用的 Geometry。
这和从外部加载模型(比如 .gltf、.obj)是不同的。图元的优势是快------不用等网络加载,一行代码就出来一个球。适合做原型、数据可视化、或者一些程序化生成的场景。
当然,正经做项目的时候,复杂模型还是得让美术用 Blender、Maya 这些工具来做。图元更多是"够用就行"。
二、three.js 有哪些内置图元?
three.js 的内置图元还挺多的,我们按类别来认:
基础形状
| 图元 | 是什么 | 常用场景 |
|---|---|---|
BoxGeometry |
长方体/立方体 | 建筑块、容器、默认 placeholder |
SphereGeometry |
球体 | 地球、星球、粒子球 |
PlaneGeometry |
平面 | 地面、墙面、2D 效果的载体 |
CircleGeometry |
圆形(平的) | 圆盘、按钮底座、光圈 |
RingGeometry |
圆环(中间有洞) | 呼啦圈、瞄准镜、UI 装饰 |
柱状/锥状
| 图元 | 是什么 | 常用场景 |
|---|---|---|
CylinderGeometry |
圆柱体 | 柱子、管道、杯子 |
ConeGeometry |
圆锥体 | 箭头、路障、圣诞树 |
TubeGeometry |
沿路径扫出一个管状 | 管道、线缆、弯曲的触手 |
多面体
| 图元 | 是什么 | 面数 |
|---|---|---|
TetrahedronGeometry |
四面体 | 4 面 |
OctahedronGeometry |
八面体 | 8 面 |
DodecahedronGeometry |
十二面体 | 12 面 |
IcosahedronGeometry |
二十面体 | 20 面 |
这些多面体看起来像是几何知识点,但在 制作3D 场景时还挺有用的------尤其是低面数的版本,配合 flat shading(平面着色)会有一种复古的 low-poly 风格,现在很多游戏和视觉设计都在用。
复杂形状
| 图元 | 是什么 | 常用场景 |
|---|---|---|
TorusGeometry |
圆环体(甜甜圈) | 甜甜圈、传送门、装饰环 |
TorusKnotGeometry |
环形结(扭来扭去的甜甜圈) | 炫酷的装饰物、数学可视化 |
LatheGeometry |
旋转体(把一条轮廓线绕轴旋转) | 花瓶、酒杯、保龄球瓶、灯罩 |
ExtrudeGeometry |
挤出几何体(把 2D 形状拉成 3D) | 自定义截面、文字挤出(TextGeometry 就是基于它) |
ShapeGeometry |
2D 轮廓(自动三角化) | 平面 Logo、剪影 |
ParametricGeometry |
参数化曲面(用函数生成) | 数学曲面、波浪、地形实验 |
辅助几何体
| 图元 | 是什么 | 作用 |
|---|---|---|
EdgesGeometry |
提取另一个几何体的边缘线 | 显示模型的线框轮廓,过滤掉共面边 |
WireframeGeometry |
把几何体转成线框格式 | 生成适合 LineSegments 渲染的线框 |
这两个不是"形状",而是"形状的加工工具"。比如你想看一个立方体的线框,直接设 wireframe: true 会看到每个三角形的分割线,而用 EdgesGeometry 可以只显示外轮廓,更干净。
三、怎么创建一个图元?
大部分图元的创建方式都差不多:传几个尺寸参数,拿到 geometry,包上 material,变成 Mesh,塞进场景。
以立方体为例:
js
const width = 8;
const height = 8;
const depth = 8;
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshPhongMaterial({ color: 0x44aa88 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
球体也是类似的套路:
js
const radius = 7;
const widthSegments = 12; // 水平方向分多少段
const heightSegments = 8; // 垂直方向分多少段
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
每个图元的具体参数可以去 官方文档 查,这里不一一列举了。重点是记住这个模式:new THREE.XxxGeometry(参数...) → 包 Material → 变 Mesh → 进场景。
四、TextGeometry:有点特殊
TextGeometry 比较特别,它需要异步加载字体数据才能生成。不能像其他图元那样一行代码搞定。
js
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
const loader = new FontLoader();
loader.load('fonts/helvetiker_regular.typeface.json', function (font) {
const geometry = new TextGeometry('Hello', {
font: font, // 加载好的字体对象
size: 3, // 文字大小
depth: 0.2, // 挤出厚度
curveSegments: 12, // 曲线分段数
bevelEnabled: true, // 是否倒角
bevelThickness: 0.15,
bevelSize: 0.3,
bevelSegments: 5,
});
const material = new THREE.MeshPhongMaterial({ color: 0x44aa88 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
});
注意这里是回调函数 的形式,因为字体加载是异步的。如果你用 async/await 也可以把它 promisify:
js
function loadFont(url) {
return new Promise((resolve, reject) => {
loader.load(url, resolve, undefined, reject);
});
}
async function createText() {
const font = await loadFont('fonts/helvetiker_regular.typeface.json');
// ... 创建 TextGeometry
}
createText();
还有一个坑:默认生成的文字,旋转中心在左边边缘。如果你想让文字绕着自己的中心旋转,需要手动调整位置:
js
geometry.computeBoundingBox(); // 计算包围盒
geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);
这两行的意思是:先算出文字几何体的中心点,然后把 mesh 的位置往反方向挪同样的距离,这样中心点就回到原点了。后面再做旋转,就是绕中心转了。
五、EdgesGeometry 和 WireframeGeometry:看骨架
有时候你想看看一个模型的"骨架"------比如调试的时候确认一下三角面长什么样。这时候可以用 EdgesGeometry 或 WireframeGeometry。
EdgesGeometry:只看外轮廓
js
const boxGeometry = new THREE.BoxGeometry(8, 8, 8);
const edgesGeometry = new THREE.EdgesGeometry(boxGeometry);
const edgesMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });
const edges = new THREE.LineSegments(edgesGeometry, edgesMaterial);
scene.add(edges);
EdgesGeometry 很聪明,它只显示夹角超过某个阈值的边。立方体的外轮廓线会保留,但每个面上对角线分割成两个三角形的内部线会被过滤掉,看起来更干净。
WireframeGeometry:所有边都显示
js
const wireframeGeometry = new THREE.WireframeGeometry(boxGeometry);
const wireframe = new THREE.LineSegments(wireframeGeometry, edgesMaterial);
scene.add(wireframe);
WireframeGeometry 会把几何体的每一条边都转成线段,包括内部的三角形分割线。如果你想看模型的完整三角面结构,用这个。
注意这两个都要配合 LineSegments 来用,而不是 Mesh。
六、Points:只画点
如果你不想画三角面,只想在几何体的每个顶点位置画一个小点,可以用 Points:
js
const geometry = new THREE.SphereGeometry(7, 12, 8);
const material = new THREE.PointsMaterial({
color: 'red',
size: 0.2, // 点的大小(世界单位)
});
const points = new THREE.Points(geometry, material);
scene.add(points);
效果就是一个由红点组成的球体轮廓,有点像粒子系统的感觉。
默认情况下,点的大小会随距离变化------离相机越远,看起来越小。如果你希望点的大小固定(比如像素级固定),可以关掉 sizeAttenuation:
js
const material = new THREE.PointsMaterial({
color: 'red',
size: 3, // 像素单位
sizeAttenuation: false, // 关闭透视衰减
});
七、分段数:多一点还是少一点?
几乎每个图元都有"分段数"这个参数。比如 SphereGeometry(radius, widthSegments, heightSegments),后两个就是控制球体分多少块的。
分段数直接影响两件事:视觉质量 和性能。
- 分段数低:面数少,性能好,但看起来棱角分明
- 分段数高:面数多,更圆润,但更耗性能
举个例子:
SphereGeometry(7, 5, 3):只有 30 个三角形,看起来像多面体SphereGeometry(7, 24, 10):480 个三角形,已经挺圆润了SphereGeometry(7, 50, 50):5000 个三角形,非常光滑
但这里有个反直觉的事实:加了平滑着色(smooth shading)之后,480 个三角形的球和 5000 个的球,远看差别可能并没有那么大。如果你只画一两个球,5000 面无所谓;但如果你要画 1000 个球,1000 × 5000 = 500 万个三角形,每帧都要算,浏览器就顶不住了。
所以分段数没有绝对的标准答案,看你的场景需求来定。原型阶段可以随便设,上线前再根据实际情况优化。
平面和盒子比较特殊------除非你要对它们做变形、扭曲、或者当作地形来修改顶点,否则 2 个三角形的平面和 200 个的平面,视觉上几乎没有区别。
八、完整代码示例
如果你想一次性把所有图元都摆出来看看效果,可以用类似这样的结构:
js
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xaaaaaa);
const camera = new THREE.PerspectiveCamera(40, 2, 0.1, 1000);
camera.position.z = 120;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 辅助函数:创建随机颜色的材质
function createMaterial() {
const material = new THREE.MeshPhongMaterial({
side: THREE.DoubleSide, // 双面渲染
});
const hue = Math.random();
material.color.setHSL(hue, 1, 0.5);
return material;
}
// 辅助函数:把物体摆到网格位置上
const objects = [];
const spread = 15;
function addObject(x, y, obj) {
obj.position.x = x * spread;
obj.position.y = y * spread;
scene.add(obj);
objects.push(obj);
}
function addSolidGeometry(x, y, geometry) {
const mesh = new THREE.Mesh(geometry, createMaterial());
addObject(x, y, mesh);
}
// 创建各种图元
addSolidGeometry(-2, -2, new THREE.BoxGeometry(8, 8, 8));
addSolidGeometry(-1, -2, new THREE.SphereGeometry(7, 12, 8));
addSolidGeometry(0, -2, new THREE.ConeGeometry(6, 10, 16));
addSolidGeometry(1, -2, new THREE.CylinderGeometry(4, 4, 10, 16));
addSolidGeometry(2, -2, new THREE.TorusGeometry(5, 2, 8, 24));
// ... 更多图元
// 渲染
function animate(time) {
objects.forEach((obj, ndx) => {
const speed = 1 + ndx * 0.1;
const rot = time * 0.001 * speed;
obj.rotation.x = rot;
obj.rotation.y = rot;
});
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
这里有几个值得注意的点:
side: THREE.DoubleSide:让材质双面渲染。像PlaneGeometry、ShapeGeometry这种没有厚度的 2D 形状,如果不设双面,从背面看会直接消失。- 每个图元给一个随机的 HSL 颜色,摆成网格状,方便对比。
- 动画循环里让每个物体以不同速度旋转,一眼就能看出哪个是哪个。
回顾一下
这篇我们过了一遍 three.js 的内置图元,可以简单记住这几点:
- 图元是代码生成的 3D 形状,适合 prototyping 和程序化场景,复杂模型还是交给美术工具
- 大部分图元的用法都一样 :
new THREE.XxxGeometry(参数)→ Material → Mesh → 场景 - TextGeometry 是特例:需要异步加载字体,而且要注意旋转中心的问题
- EdgesGeometry / WireframeGeometry 是辅助工具,用来显示模型的线框结构
- Points 可以在顶点位置画点,适合做粒子效果或点云
- 分段数要自己权衡:质量和性能之间的取舍,没有万能答案
如果这些内置图元都不够你用,后面还可以学怎么加载外部模型(.obj、.gltf),或者直接用 BufferGeometry 手写顶点数据,创建完全自定义的形状。