资讯动态

Three.js ShapeUtils 深入解析:轮廓面积、绕向判定与多边形三角剖分实战指南

发布时间:2026/9/10 9:35:20 来源:尧图企业网站定制
Three.js ShapeUtils 深入解析轮廓面积、绕向判定与多边形三角剖分实战指南【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsShapeUtils 是 Three.js 在 src/extras/ShapeUtils.js 中提供的一组静态 2D 几何工具函数负责对 2D 多边形轮廓计算有向面积、判断顶点绕向顺时针/逆时针并将「外轮廓 孔洞」的多边形定义剖分为三角形。它是 ShapeGeometry 与 ExtrudeGeometry 将任意平面形状含 SVG 字形、带洞多边形变为可渲染网格的内部基石。读完本文你将掌握这三个工具方法的手动调用方式、返回值的精确含义与符号约定并理解它们在几何工厂中的真实调用链。一、ShapeUtils 是什么ShapeUtils 是一个不可实例化、只包含静态方法的工具类在源码中通过hideconstructor标记不会被构造。它不像 Shape 那样保存几何数据而是对「由Vector2组成的点数组」执行纯函数式几何计算因此可以直接脱离渲染管线独立使用import { ShapeUtils } from three; // 或者从源码直接引入 // import { ShapeUtils } from ./src/extras/ShapeUtils.js;它服务于整条 2D→3D 几何流水线顶点绕向判定isClockWise与孔洞反向绕向要求构成 Shape 对 holes 的约束前提三角化结果triangulateShape被 ShapeGeometry、ExtrudeGeometry 用来直接写入BufferGeometry的 index 数组ShapePath.toShapes()src/extras/core/ShapePath.js在把 SVG 路径解析成多个子形状时也会用area计算有向面积来判定包含关系与绕向。原文档共收录 3 个静态方法.area()、.isClockWise()、.triangulateShape()下面逐一展开并对照源码给出完整说明。二、.area( contour )用鞋带公式计算有向面积2.1 签名与返回值.area( contour : Array.Vector2 ) : numbercontour由 2D 点Vector2组成的闭合多边形顶点数组返回一个有符号的浮点面积值。2.2 底层原理鞋带公式Shoelace / Surveyors Formula源码实现 采用经典的鞋带公式对相邻顶点求叉积累加static area( contour ) { const n contour.length; let a 0.0; for ( let p n - 1, q 0; q n; p q ) { a contour[ p ].x * contour[ q ].y - contour[ q ].x * contour[ p ].y; } return a * 0.5; }要点循环从p n - 1最后一个点出发、q从 0 递增即对每条边(p → q)求x_p·y_q − x_q·y_p累计后乘以0.5返回的是有符号面积在 Three.js 的 y 轴向上坐标系中顶点按逆时针CCW排列时结果为正顺时针CW排列时为负面积值不会对孔洞做特殊处理它只描述你传入的那一圈顶点的净面积若多边形自相交或顶点重复面积数值会按抵消规律变化因此该结果也常被用作多边形有效性/绕向的度量。2.3 手动调用示例import { Vector2, ShapeUtils } from three; // 逆时针正方形CCW从左上角开始逆时针绕行 const squareCCW [ new Vector2( 0, 0 ), new Vector2( 100, 0 ), new Vector2( 100, 100 ), new Vector2( 0, 100 ) ]; // 顺时针正方形CW把上面的数组整体反转即可 const squareCW squareCCW.slice().reverse(); console.log( ShapeUtils.area( squareCCW ) ); // 10000 正逆时针 console.log( ShapeUtils.area( squareCW ) ); // -10000 负顺时针 console.log( Math.abs( ShapeUtils.area( squareCCW ) ) ); // 10000绝对值为真实面积由此可以归纳出三元素记忆口诀逆时针为正、顺时针为负、取绝对值即实际面积。三、.isClockWise( pts )判断轮廓绕向3.1 签名与返回值.isClockWise( pts : Array.Vector2 ) : booleanpts定义多边形的 2D 点数组返回true表示该轮廓为顺时针绕向。3.2 实现一行委托给 area源码实现 极其简洁直接复用.area()static isClockWise( pts ) { return ShapeUtils.area( pts ) 0; }也就是说isClockWise并非独立算法而是对「有向面积为负」这一条件的语义化包装。继续上面的例子console.log( ShapeUtils.isClockWise( squareCCW ) ); // false console.log( ShapeUtils.isClockWise( squareCW ) ); // true3.3 为什么需要绕向判定孔洞必须反向绕行Shape 的源码注释明确指出孔洞holes的绕向必须与外轮廓相反CW/CCW 互换。这一要求的原因从几何上讲是「内外边界共同围成环带区域」从算法上讲是为了让耳切法正确区分内部边界——逆时针子路径对内部点贡献1环绕数顺时针贡献-1见 src/extras/core/ShapePath.js 的实现注释与ShapeUtils.area判定代码。因此凡是手工构造带孔 Shape 或直接调用三角化接口的场景都需要用isClockWise检查并修正绕向。四、.triangulateShape( contour, holes )多边形三角剖分4.1 签名与返回值.triangulateShape( contour : Array.Vector2, holes : Array.Array.Vector2 ) : Array.Array.numbercontour定义外轮廓的 2D 点数组holes数组的数组每个元素是一个定义孔洞的 2D 点数组无孔洞时传[]返回一个数组其中每个元素代表一个三角形面是包含 3 个顶点索引的数组如[ [ 0, 3, 2 ], [ 0, 2, 1 ] ]。4.2 完整算法流水线对照源码源码 将三角化拆成清晰的四步理解每一步有助于正确解读返回的索引① 剔除重复闭合点removeDupEndPts 会在点数大于 2 且「首尾坐标相等」时弹出末尾点。也就是说无论你传入的轮廓是否以「最后一个点等于第一个点」的方式闭合都会被规整为无重复首尾的标准形式。外轮廓与每个孔洞都会先经过这一步。② 顶点展平与孔洞起始索引记录addContour 把所有点按[ x0, y0, x1, y1, ... ]压平为一维数组vertices同时把每个孔洞在展平数组中的起始下标依次记入holeIndices第一个孔洞从contour.length开始后续累加。这正是耳切算法期望的「顶点数组 孔洞下标数组」输入形态。③ 调用 Earcut 耳切算法const triangles Earcut.triangulate( vertices, holeIndices );src/extras/Earcut.js 是对 Mapbox earcut 多边形三角化算法的移植封装Earcut.triangulate( data, holeIndices, dim 2 )接收一维坐标数组并返回一维索引数组每 3 个连续数字构成一个三角形dim 为每个顶点坐标维度默认 2。Earcut 能正确处理凹多边形和带洞多边形这是它相对朴素扇形三角化的核心优势。④ 索引三元组分组成面数组最后按步长 3 将一维结果切成三元组得到文档约定格式Array.Array.numberfor ( let i 0; i triangles.length; i 3 ) { faces.push( triangles.slice( i, i 3 ) ); }4.3 返回值索引的含义易错点返回的索引不是新拷贝坐标的下标而是「contour 顶点 各 holes 顶点按传入顺序拼接」后的整体数组下标contour顶点占0 ~ contour.length-1第一个孔洞顶点接着从contour.length开始编号以此类推源码中holeIndices正是按此规则累加。因此如果你要在外面消费这些面并重建顶点缓冲务必按同样顺序拼接顶点数组。4.4 完整调用示例带方孔的矩形import { Vector2, ShapeUtils } from three; // 外轮廓逆时针CCW const contour [ new Vector2( 0, 0 ), new Vector2( 100, 0 ), new Vector2( 100, 100 ), new Vector2( 0, 100 ) ]; // 孔洞与外轮廓绕向相反取顺时针CW const hole [ new Vector2( 25, 25 ), new Vector2( 25, 75 ), new Vector2( 75, 75 ), new Vector2( 75, 25 ) ]; // 防御性检查外轮廓应为逆时针孔洞应为顺时针 console.log( ShapeUtils.isClockWise( contour ) false ); // true console.log( ShapeUtils.isClockWise( hole ) true ); // true const faces ShapeUtils.triangulateShape( contour, [ hole ] ); // faces: [ [ i0, i1, i2 ], [ i3, i4, i5 ], ... ]每个三元组是一个三角形 // 重建「外轮廓 孔洞」拼接后的完整顶点表 const allVertices contour.concat( hole ); faces.forEach( ( face, index ) { console.log( 三角形 ${ index }, face.map( i allVertices[ i ] ) ); } );对于凸外轮廓 单个凸孔洞结果通常是把「外轮廓顶点在前、孔洞顶点在后」的顶点序列切分为若干个三角形返回三角形的具体个数由顶点总数决定实际索引顺序取决于耳切算法的裁剪次序读者应把它当作「三个索引构成一个面」来消费而不是依赖特定顺序。五、源码视角ShapeUtils 在几何工厂中的真实调用链5.1 ShapeGeometry构造平面填充网格src/geometries/ShapeGeometry.js 的addShape内部流程完整展示了三件套的协作方式调用 Shape.extractPoints 把曲线的线段细分curveSegments采样成离散点得到{ shape, holes }归一化绕向外轮廓若非顺时针则reverse()孔洞若为顺时针则reverse()ShapeGeometry.js保证内外部边界满足算法要求调用ShapeUtils.triangulateShape( shapeVertices, shapeHoles )得到三角形面把外轮廓与孔洞顶点拼接进vertices并把返回的每个面索引加上indexOffset后写入indices最终setIndex()setAttribute(position|normal|uv)完成BufferGeometry构建。从这一步可以看到ShapeUtils 本身不生成顶点坐标只回答「点怎么连成三角形」坐标装配由上层几何工厂完成。5.2 ExtrudeGeometry挤出体侧面与前后面src/geometries/ExtrudeGeometry.js 同样复用整套工具第 145-165 行用isClockWise决定是否需要reverse()归一化外轮廓/孔洞绕向确保所有 Shape 以统一绕向进入后续流程侧面法线方向才能一致第 402 行 与 第 452 行对原始轮廓及「倒角收缩后的轮廓」分别调用triangulateShape生成前后面含带洞情形。此外它还用area判断 Shape 的拓扑有效性只有顶点数 ≥3 且面积为非零的子路径才有资格参与填充见 ShapePath.js 对每个 subpath 的ShapeUtils.area(points)检查与零面积过滤。这说明area在有符号面积之外还充当了「多边形是否退化」的检测器。5.3 SVG 路径 → 文本/SVG 几何[S]VGLoader 会把 SVG 中的path解析为ShapePath再经ShapePath.toShapes()依据子路径间的包含关系由面积大小排序判定与 fill-rulenonzero/evenodd生成带正确绕向的Shape数组最终送入 ShapeUtils 三角化。因此TextGeometry基于字体轮廓的 Shape与各类 SVG 几何能生成无翻转、无错误孔洞的网格底层都依赖本文的三个方法。六、完整实战从 Shape 到可渲染网格将上述知识串联起来一段「手写形状 → 三角化 → 生成 Mesh」的完整最小示例大致如下完整可运行版可参考 Three.js 官方心形 Shape 示例模式源码注释见 src/extras/core/Shape.js#L9-L31import * as THREE from three; // 1) 用路径 API 构造 Shape贝塞尔曲线会被细分采样成点 const shape new THREE.Shape(); shape.moveTo( 25, 25 ); shape.bezierCurveTo( 25, 25, 20, 0, 0, 0 ); shape.bezierCurveTo( - 30, 0, - 30, 35, - 30, 35 ); shape.bezierCurveTo( - 30, 55, - 10, 77, 25, 95 ); shape.bezierCurveTo( 60, 77, 80, 55, 80, 35 ); shape.bezierCurveTo( 80, 35, 80, 0, 50, 0 ); shape.bezierCurveTo( 35, 0, 25, 25, 25, 25 ); // 2) 采样得到离散顶点并交给 ShapeUtils 三角化 const { shape: vertices, holes } shape.extractPoints( 12 ); const faces THREE.ShapeUtils.triangulateShape( vertices, holes ); console.log( 轮廓采样点数${ vertices.length }三角形数量${ faces.length } ); // 3) 也可直接交给 ShapeGeometry / ExtrudeGeometry 一键生成网格 const flatGeometry new THREE.ShapeGeometry( shape, 12 ); // 平面填充 const depthGeometry new THREE.ExtrudeGeometry( shape, { // 挤出成体 depth: 8, bevelEnabled: true, bevelSegments: 2, steps: 2, bevelSize: 1, bevelThickness: 1 } );其中flatGeometry.index中装载的正是ShapeUtils.triangulateShape返回索引经过偏移换算后的结果——你可以通过比对二者来验证方法行为。七、使用陷阱与工程建议绕向约定孔洞必须与外轮廓反向见 Shape 源码注释。手工构造 Shape 时建议先用isClockWise断言出错就reverse()几何工厂内部其实也是这样归一的。闭合点重复首尾点相同的轮廓会被自动去重removeDupEndPts无需手动剔除但该函数仅在length 2时生效退化的 0/1/2 点轮廓不会通过后续有效判定。索引是「拼接顶点表」的下标消费triangulateShape返回值时顶点表必须按「contour 在前、holes 依次在后」的顺序拼接否则三角形指向错误顶点。凹多边形与孔洞Earcut 支持凹多边形与任意多孔洞但不支持自相交多边形与重叠孔洞孔洞应严格落在轮廓内部否则结果不可预期。顶点类型接口约定为Vector2数组源码内部直接读取.x/.y/.equals。若手头是普通{x, y}对象至少保证带x/y属性且去重逻辑依赖Vector2.prototype.equals。保持 z 值无关所有方法都只读取 x、y适合任意平面z 恒 0的 2D 形状处理这也是 ShapeGeometry 直接以z 0写缓冲的原因。八、延伸阅读本文主体文档ShapeUtils 文档源码实现src/extras/ShapeUtils.js面积 / 绕向 / 三角化三方法及内部辅助函数底层算法封装src/extras/Earcut.jsMapbox earcut 算法移植支持凹多边形与孔洞调用方 AShapeGeometry 源码平面网格构建调用方 BExtrudeGeometry 源码挤出体前后端面与倒角轮廓三角化形状数据模型Shape 文档 / ShapePath 文档 / ExtrudeGeometry 文档顶点类型Vector2 文档x/y/equals为上述算法所依赖的最小接口【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

免费获取报价