首页
/ three.js Shape 类详解:从 2D 轮廓定义到挤出与平面网格的完整路径

three.js Shape 类详解:从 2D 轮廓定义到挤出与平面网格的完整路径

2026-09-07 16:14:36作者:柯茵沙

本文基于 three.js 官方文档 Shape 及其源码实现,系统讲解 THREE.Shape 的定位、构造方式、属性与方法,并结合 src/extras/core/Shape.jssrc/geometries/ExtrudeGeometry.jssrc/geometries/ShapeGeometry.jssrc/extras/ShapeUtils.js 深入剖析轮廓点如何被采样、朝向如何被校正、带孔形状如何被三角化,帮助你在项目中稳定地把任意 2D 图形(含孔洞)转换为可渲染的 3D 几何体。

一、Shape 是什么:一条可挤出、可三角化的闭合 2D 轮廓

官方文档对 Shape 的定义是:使用路径(可选地带有孔洞)定义任意 2D 形状平面,可用于 ExtrudeGeometry 做挤出、ShapeGeometry 生成单面平面网格、提取轮廓点、或直接获取三角化后的面。

从源码结构看,它的类继承链为:

Curve → CurvePath → Path → Shape

对应实现文件依次为 src/extras/core/Curve.jssrc/extras/core/CurvePath.jssrc/extras/core/Path.jssrc/extras/core/Shape.js。单元测试 test/unit/src/extras/core/Shape.tests.js 也显式断言了 Shape extends from Path。这条继承链决定了 Shape 的能力来源:

  • CurvePath 层Shape 内部持有 this.curves 数组,是一条"曲线序列",支持按弧长采样(getPoint(t)getSpacedPoints());
  • Path 层:提供与 2D Canvas API 相近的绘图方法(moveTolineTobezierCurveTo 等);
  • Shape 层:在此之上增加 holes(孔洞路径数组)、uuid,并新增 extractPointsgetPointsHoles 两个面向几何生成的 API。

src/extras/core/Shape.js 中,构造函数做的事情非常克制:

constructor( points ) {
    super( points );

    this.uuid = generateUUID();
    this.type = 'Shape';
    this.holes = [];
}

值得注意的是,Shape 并没有覆盖父类 CurvegetPoint(t)/getLength() 等接口,这些能力全部由 CurvePath 提供;Shape 真正新增的是"轮廓 + 孔洞"这一数据模型,这正是几何生成器(ExtrudeGeometry/ShapeGeometry)所消费的核心。

二、构造与文档示例:心形轮廓 + ExtrudeGeometry 挤出

构造函数

new Shape( points : Array.<Vector2> ) —— 构造一个新形状,points 为定义形状的二维点数组。若传入 points,父类 Path 会调用 setFromPoints( points ),其实现是:第一个点执行 moveTo,其余点依次 lineTo,即全部以直线段 LineCurve 连接(见 src/extras/core/Path.js#L66-L78)。这意味着:

// 由点云直接构造多边形形状(直线段连接)
const shape = new THREE.Shape( [
    new THREE.Vector2( 0, 0 ),
    new THREE.Vector2( 1, 0 ),
    new THREE.Vector2( 1, 1 ),
    new THREE.Vector2( 0, 1 )
] );

文档完整代码示例

官方文档给出的标准示例是用贝塞尔曲线画出心形,再挤出成实体。这里完整保留并补充说明:

const heartShape = new THREE.Shape();
heartShape.moveTo( 25, 25 );
heartShape.bezierCurveTo( 25, 25, 20, 0, 0, 0 );
heartShape.bezierCurveTo( - 30, 0, - 30, 35, - 30, 35 );
heartShape.bezierCurveTo( - 30, 55, - 10, 77, 25, 95 );
heartShape.bezierCurveTo( 60, 77, 80, 55, 80, 35 );
heartShape.bezierCurveTo( 80, 35, 80, 0, 50, 0 );
heartShape.bezierCurveTo( 35, 0, 25, 25, 25, 25 );

const extrudeSettings = {
    depth: 8,
    bevelEnabled: true,
    bevelSegments: 2,
    steps: 2,
    bevelSize: 1,
    bevelThickness: 1
};

const geometry = new THREE.ExtrudeGeometry( heartShape, extrudeSettings );
const mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial() );

示例中的 6 段 bezierCurveTo 首尾闭合回到起点 (25, 25),构成一条闭合心形轮廓;extrudeSettingsdepth: 8 控制挤出厚度,bevelEnabled/bevelSize/bevelThickness/bevelSegments 控制斜角。完整的挤出效果演示可参考仓库中的官方示例页 examples/webgl_geometry_extrude_shapes.html

三、属性详解:holes 与 uuid

.holes : Array.

定义形状中的孔洞。文档明确要求:孔洞的定义必须使用与外轮廓相反的绕向(CW/CCW)。源码注释(src/extras/core/Shape.js#L56-L63)与文档一致:

"Hole definitions must use the opposite winding order (CW/CCW) than the outer shape."

这里的"绕向"由几何面积的正负来判定。src/extras/ShapeUtils.js 中:

  • ShapeUtils.area( contour ) 用鞋带公式计算有向面积(乘 0.5);
  • ShapeUtils.isClockWise( pts ) 当且仅当 area < 0 时返回 true

因此"外轮廓逆时针、孔洞顺时针"就是文档所说的相反绕向的量化含义。典型的加孔写法是:

const shape = new THREE.Shape();
shape.moveTo( - 50, - 50 );
shape.lineTo( 50, - 50 );
shape.lineTo( 50, 50 );
shape.lineTo( - 50, 50 );
shape.closePath();

const hole = new THREE.Path();
hole.moveTo( - 10, - 10 );
hole.lineTo( - 10, 10 );   // 与外轮廓绕向相反
hole.lineTo( 10, 10 );
hole.lineTo( 10, - 10 );
hole.closePath();
shape.holes.push( hole );

.uuid : string(readonly)

形状的唯一标识,由 generateUUID() 在构造时生成。它服务于序列化链路:ShapeGeometrytoJSON 通过 shape.uuid 记录形状引用(见 src/geometries/ShapeGeometry.js#L209-L231),使几何体与形状可以在 JSON 中解耦存储。

四、方法详解:extractPoints 与 getPointsHoles

这两个方法是 Shape 相对 Path 的核心增量,也是两个几何生成器的统一入口。

.getPointsHoles( divisions : number ) : Array.<Array.>

"返回一个数组,其中每个元素代表一个孔洞轮廓的二维点列表。divisions 表示结果精细度。"

实现(src/extras/core/Shape.js#L74-L86)就是对每个 holes[i] 调用 getPoints( divisions )

getPointsHoles( divisions ) {
    const holesPts = [];
    for ( let i = 0, l = this.holes.length; i < l; i ++ ) {
        holesPts[ i ] = this.holes[ i ].getPoints( divisions );
    }
    return holesPts;
}

.extractPoints( divisions : number ) : Object

"返回一个对象,以二维点数组形式保存形状及其孔洞的轮廓数据",即 { shape: [...], holes: [[...], ...] }src/extras/core/Shape.js#L97-L106)。

divisions 到底控制什么

采样发生在父类 CurvePath.getPoints()src/extras/core/CurvePath.js#L199-L235)。它对每条子曲线按类型采用不同分辨率:

子曲线类型 实际采样分辨率
EllipseCurve(圆弧/椭圆) divisions * 2
LineCurve 固定 1(端点即可)
SplineCurve divisions * curve.points.length
其他(如 CubicBezierCurve divisions

采样结果还会做去重(跳过与上一个点相同的点),并在 autoClose 为真时追加首点闭合。因此 divisions 越大,贝塞尔曲线与圆弧上的采样点越密,轮廓越平滑,但顶点数量也随之线性增长。

divisions 在下游几何生成器中的对应参数是 curveSegments,默认值为 12

也就是说,文档 API 中的 divisions 与几何生成器的 curveSegments 是同一个采样精度参数,只是在不同层级的命名。

五、下游消费:ShapeGeometry 与 ExtrudeGeometry 如何使用轮廓点

ShapeGeometry:采样 → 朝向校正 → Earcut 三角化

ShapeGeometry 处理单个形状的完整流程(src/geometries/ShapeGeometry.js#L93-L159):

  1. extractPoints( curveSegments ) 采样得到外轮廓点 shapeVertices 与孔洞点数组;
  2. 朝向校正:若外轮廓不是顺时针则 reverse();若某个孔洞是顺时针则 reverse()。这与文档"孔洞绕向必须与外轮廓相反"的要求互为表里——生成器会替你修正,但你手动构造时仍应遵循该约定;
  3. ShapeUtils.triangulateShape( shapeVertices, shapeHoles ) 三角化(src/extras/ShapeUtils.js#L50-L87):把外轮廓与所有孔洞展平为一条顶点数组,孔洞起点下标记入 holeIndices,交给 Earcut 求三角形索引;
  4. 逐顶点写入 position(z=0)、normal(0,0,1)、uv(直接使用世界坐标 x/y),逐三角形写入索引。

另外,当传入多个 Shape 时,ShapeGeometry 会为每个形状 addGroup,从而支持多材质(MultiMaterial)。

ExtrudeGeometry:同一条轮廓点管线 + 挤出

ExtrudeGeometry 的第一步与 ShapeGeometry 完全相同——extractPoints( curveSegments ),随后做同样的顺时针校正:外轮廓若为逆时针则反转,孔洞若为顺时针则反转。差异在于后续:顶点沿挤出方向复制出侧面与顶/底面,并支持 extrudePath 沿任意 3D 路径扫掠(此时强制禁用 bevel,见 src/geometries/ExtrudeGeometry.js#L106-L111)。

结论是:extractPoints 是"点提取"与"几何生成"之间的契约接口。任何自定义的 2D 轮廓生成逻辑(比如把测量数据转成 Shape)只要保证 extractPoints 可用,就能直接复用这套成熟的朝向校正与 Earcut 三角化管线。

六、如何绘制轮廓:从 Path 继承的绘图 API

Shape 继承自 Path,因此获得整套与 2D Canvas API 风格一致的构造方法(src/extras/core/Path.js):

方法 追加的子曲线类型 说明
moveTo( x, y ) 移动当前点 currentPoint,不产生曲线
setFromPoints( points ) LineCurve 首点 moveTo,其余逐点 lineTo
lineTo( x, y ) LineCurve 直线段
quadraticCurveTo( aCPx, aCPy, aX, aY ) QuadraticBezierCurve 二次贝塞尔
bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) CubicBezierCurve 三次贝塞尔(文档心形示例所用)
splineThru( pts ) SplineCurve 穿过给定点集的样条
arc( ... ) / absarc( ... ) EllipseCurve 相对/绝对圆弧,圆心可相对当前点偏移
ellipse( ... ) / absellipse( ... ) EllipseCurve 相对/绝对椭圆,支持旋转角

所有方法均返回 this,支持链式调用。两个容易踩坑的细节(见 src/extras/core/Path.js#L270-L294):

  • 椭圆类方法会先检查起点是否与 currentPoint 一致,不一致时自动补一条 lineTo 连接,保证曲线序列连续;
  • absarc 实际是 absellipse 的等半径特化,aStartAngle/aEndAngle 以弧度计,aClockwise 控制扫掠方向。

若形状未手动闭合,可利用 CurvePathclosePath()src/extras/core/CurvePath.js#L56-L71):它比较首末曲线端点,不重合时补一条直线曲线闭合路径。

七、序列化:toJSON / fromJSON / copy

Shape 在父类序列化的基础上补充了 uuidholessrc/extras/core/Shape.js#L108-L160):

  • toJSON():输出 data.uuid,并把每个孔洞(Path)的 JSON 收集到 data.holes 数组;
  • fromJSON( json ):恢复 uuid,并对 json.holes 中每个条目构造 new Path().fromJSON( hole ) 回填;
  • copy( source ):先 super.copy(复制曲线数组),再逐个 clone() 孔洞——孔洞是深拷贝,不会与源形状共享。

由于 CurvePath.toJSON 会递归序列化每条曲线(含 autoClose 标志,见 src/extras/core/CurvePath.js#L257-L291),Shape 的序列化是完整可逆的,这也是 SVGLoader 将加载结果写入场景并再次序列化时的依赖路径。

八、相关类速查:Path、CurvePath、ShapePath 的边界

同目录 src/extras/core/ 下的三个相关类常与 Shape 一起出现,用途边界值得厘清:

  • Path:纯 2D 路径,不关心"内部/孔洞"语义,适合画线、做描边轮廓(如 Path 文档示例:路径采样成点集后用 THREE.Line 渲染);
  • CurvePath:任意 2D/3D 曲线的串联基类,负责按弧长参数化采样(getPoint(t) 依据累计曲线长度定位子曲线,见 src/extras/core/CurvePath.js#L81-L120);
  • ShapePath:把一组 Path 子路径转换为 Shape[] 的工具,主要用于字体与 SVG 场景。其 toShapes()src/extras/core/ShapePath.js#L146-L351)实现了比"手工 push holes"更智能的内外判定:
    1. 对每条子路径采样、计算有向面积(ShapeUtils.area),剔除退化路径;
    2. 用"保证在内部"的点(包围盒中心或水平射线法求取,移植自 paper.js 的 getInteriorPoint 算法)配合 even-odd 射线法做点在多边形内测试;
    3. 按面积降序处理后,用 winding number + fillRulenonzero/evenodd,取自 userData.style.fillRule,默认 nonzero)判定每条子路径的角色:
      • 无容器、或容器本身是孔洞 → 新形状的外轮廓
      • 否则 → 容器的孔洞shape.holes.push( hole ))。

从源码结构看,ShapePath 相当于 Shape 的"智能工厂":它解决的是自动判定"哪条子路径是洞、洞属于哪个外形"的问题;而直接使用 Shape 时,这个职责由开发者按文档的绕向约定承担。

九、行为验证:单元测试与文档覆盖范围

官方文档 docs/pages/Shape.html.md 声明的 API 面(构造参数、holesuuidextractPointsgetPointsHoles)与当前源码 src/extras/core/Shape.js 完全一致,且源码 JSDoc 中还额外提供了 copytoJSONfromJSON 三个未在文档页面展开的方法,本文已一并补齐。

十、实践要点小结

  1. 绕向约定是契约:外轮廓逆时针、孔洞顺时针;即便如此,ShapeGeometry/ExtrudeGeometry 仍会在生成前用 ShapeUtils.isClockWise 做防御性校正,但手工处理轮廓点时应保持该约定,避免依赖隐式反转;
  2. 精度由 divisions/curveSegments 控制:默认 12 段/曲线,圆弧按 2 倍分辨率采样;提高该值可平滑轮廓,但顶点数随之增长,应按轮廓复杂度权衡;
  3. 孔洞是 Path 数组shape.holes 中的元素是 Path 而非 Shape,绘制 API 与 Shape 完全相同;
  4. 序列化可逆toJSON/fromJSON 覆盖了曲线、currentPointuuidholes 全量状态,可安全用于场景持久化与 ShapeGeometry 的 JSON 往返;
  5. 复杂来源交给 ShapePath:当轮廓来自 SVG/字体等"一堆子路径"而非人工构造时,优先使用 ShapePath.toShapes() 自动完成内外角色分配,而不是手工判定绕向。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388