半边结构:把规则用到任意拓扑上要记的账
全课题骨架:几何不是"画"出来的,是控制点 + 一条可以反复施加的规则算出来的 ——
而那条规则只做一件事:在两点之间取一点。
前三课一直在说"任意拓扑":第 1 课把规则用在一排点上,第 2 课把它装到一个环上,
第 3 课把它在两个方向各用一次。可"一个环"是从哪儿来的?第 2 课的 topology() 是
每次要遍历就现算一遍的临时账本 —— 而真实建模里网格要反复编辑,这份账必须存下来。
四段路:把每条边拆成两条半边、记下四样账(23.1)→ 走指针就能绕一圈,邻域不用再重建(23.2)→
看清这份账什么时候根本记不下去(23.3)→ 把邻域用起来,算出一个与拓扑无关的几何量(23.4)。
★ 量出来才敢写的一件事:"任意拓扑"是有前提的 —— 一条边被三个面共用、一个顶点周围的面分成两片、
或者曲面像 Möbius 带那样不可定向,这份账就建不起来;三种情形本页都造了反例,
各自被拒在不同的那一条上。
以及:一个顶点周围的角度和能告诉你那一点的弯曲 —— 立方体每个顶点亏 π/2、八个顶点合起来恰好 4π
(实测与 2πχ 之差 0)。
保真度:示意 · 等价重现 —— 半边表、邻域遍历、角亏损、余切权都是纯算术
(页面现场跑的十条与离线定稿 q204.mjs 同口径),画面是本页自己的二维软件光栅
(正交投影 + 平面着色 + 线框),不是任何网格库的内部结构。
★ 本页的边界处 twin = -1(不补"虚拟边界面"那一圈)—— 一手源里两派做法都有,
本页取前者并如实标注;也不做半边结构的编辑算子(split / collapse / flip)。
本页的原理与五张数值表照常可读(画面只是把同一份半边表画出来)。
(脚本启动中…)
定点观察:(脚本启动中…)
本帧的读数
(脚本启动中…)
23.1 段 · 一条边拆成两条
本段只增加一样东西:把每条边拆成两条半边(读者只动「绕圈看谁」的滑块 —— 它在这一段选的是"哪一条半边")。
保真度:示意 · 等价重现。前三课一直在说"任意拓扑",可第 2 课那个 `topology()` 是
每次要遍历就现算一遍的临时账本;而真实建模里网格要反复编辑,"这条边的邻居是谁"会被问上千次 ⇒
这份账必须存下来。存法只有一行:每条边拆成两条方向相反的半边,每条半边记四样 ——
v(起点)、next(同一面里的下一条)、twin(反向的那条)、
f(左侧面)。
★ 全局只有一条约定:面在半边的左侧,于是绕面是逆时针(一手源原话:"we look at the surface such that
the halfedges around a face are oriented counterclockwise")。这条约定一改,所有指针的语义都要跟着改。
★ 账记完就有两条能逐条验的不变量(与一手源那条等价):h.twin.v === h.next.v、
h.twin.next.v === h.v —— 立方体 24 条半边全部成立。
// … 那三行是省略标记,其余每行都与本页源码逐字相同),右列是实测。
注意中间那两行 v: a 与 next: base + (k + 1) % n —— 一条半边"从哪来、往哪去"全在这两处。| 记的账 | 应然 | 实测 | 结论 | 怎么算 | 类别 |
|---|
定点观察:① 拖「绕圈看谁」的滑块(这一段它选的是"哪一条半边"); ② 看画面里每条边被两条方向相反的单线共用(一条偏高亮、一条偏暗,末端各有一个点表示方向); ③ 结论:半边数恰好是边数的两倍,而每条半边都有唯一的下一条与唯一的反向 —— "邻居是谁"从"现算"变成了"顺着指针读"。
23.2 段 · 走指针绕一圈
本段只增加一样东西:用指针遍历(读者只动「绕面 / 绕顶点」那对按钮与滑块,不再重建任何账本)。
保真度:示意 · 等价重现。上一段把账存下来了,可"存"本身不是目的 —— 目的是用它回答邻居问题。
只需要两个动作:h = h.next 绕一张面走一圈(面里几条边就走几步),
h = h.twin.next 绕一个顶点走一圈(那个顶点度数是几就走几步)——
一手源原话:"successive assignments of the form h = h->next() cycle counterclockwise around the facet …
successive assignments of the form h = h->next()->opposite() cycle clockwise around the vertex"。
★ 本段的账是两条独立路线:一条是"顺着指针绕圈"收集邻点 / 邻面 / 邻边,另一条是
"直接扫描 faces 数组"收集同一批东西 —— 两者不共用任何中间量,立方体 8 个顶点逐元素完全相同。
★ 顺带一条诚实的限制:有边界时 twin 为空,绕顶点会断。一手源里两派做法都有
(把边界标成"没有面"/补一圈"虚拟边界面"让它也能绕);本页取前者,并在开放网格上只报
"边界半边数 = 边界环长"这一条。
h = he.H[he.H[h].twin].next ——
本课的关键就是这一句:先退回那条反向半边,再走它的 next,于是从"一条边"转到了"下一条边"。| 量什么 | 应然 | 实测 | 结论 | 怎么走 | 类别 |
|---|
定点观察:① 在「绕面(next)」与「绕顶点(twin·next)」之间切换,并拖滑块换起点; ② 看画面里高亮的那一圈怎么走(面环一种颜色、顶点环另一种);③ 结论:两次都恰好走回起点, 步数分别等于面边数与顶点度数 —— 而且这一圈走完,你就拿到了原来要"重建整张账本"才能拿到的邻域。
23.3 段 · 这份账什么时候记不下去
本段只增加一样东西:把网格换成"不满足前提"的那种(读者只动「网格」那一排按钮)。
保真度:示意 · 等价重现。前两段一路顺风,是因为默认了一个前提:每条边至多两个面、每个顶点周围连成一片
—— 也就是一手源说的 2-manifold("a surface that when cut with a small sphere always yields a disk"),
以及半边结构只能表示可定向流形(CGAL 手册:"the variant here is restricted to orientable 2-manifolds only")。
★ 本段造了三个反例,它们分别踩在三条不同的检查上(这就是这段最值钱的地方):
① 三个面共用一条边 ⇒ 一条边数出 3 个面 ⇒ 报「边」;
② 两个面只共用一个顶点(沙漏)⇒ 那个顶点周围的面分成两片、绕不成一圈 ⇒ 报「顶点」;
③ Möbius 带 ⇒ 曲面不可定向,一条有向边被两个面同向用了两次 ⇒ 报「定向」。
★ 并且如实标注:开放网格(有边界)是合法的流形,只是边界处 twin = -1;
本页那条"顶点星要连成一片"的判法特意不依赖 twin(有边界时用 twin 会漏判),
所以开放网格照样建得起来。
| 网格 | V / E / F | χ | 半边 / 边界半边 | 结果 | 类别 |
|---|
定点观察:① 在「网格」里依次点七个;② 看后三个各自停在哪一步 (读数行会把类别与原因写出来,画面只画得出网格线,因为半边表根本没建起来); ③ 结论:"任意拓扑"有前提 —— 一条边三个面、顶点星分两片、曲面不可定向, 这三种都不在这份账能表示的范围内;而开放网格是合法的,只是边界处没有反向半边。
23.4 段 · 有了邻域就能算曲率
本段只增加一样东西:把邻域用起来,算一个数(读者只动滑块换顶点)。
保真度:示意 · 等价重现。前三段都在记"谁挨着谁",可记这个干什么用?
最干净的一个用途是角亏损:把某个顶点周围所有面的内角加起来,看它离一整圈(2π)差多少 ——
Ω = 2π − Σθ。平坦的地方恰好差 0;立方体每个顶点差 π/2;正四面体差 π;
正二十面体差 π/3。而这一切有个漂亮的守恒:ΣΩ = 2πχ(离散 Gauss–Bonnet)——
立方体八个顶点合起来恰好 4π(实测与 2πχ 的差是 0)。
★ 第二视角(同一份邻域,换个权):把"均匀地平均邻居"换成余切权
w_ij = (cot α + cot β)/2(α、β 是这条边两侧的对角)。于是平坦网格上
线性函数会被打成恰好 0(1.12e−16),规则网格上的权重只剩 三个取值(0 / 0.5 / 1,都能手算),
而单位球面上 |L| / (2|p|) → 2 且方向与 p 相反。
★ 余切权的已知缺陷也一并量出来:钝角 ⇒ 负权(一手源原话:"the cotangent becomes negative for angles
between 90 and 180 degrees")——本页那个钝角靶子给出 w = −24.9975。
F[(k + n - 1) % n] 是本课修过的一处:
三角形的 k+2 恰好等于 k−1,四边形上两者不等价(见设计稿 §六)。| 网格 | 每顶点 Ω | 顶点数 | ΣΩ | 2πχ | 差 |
|---|
| 算什么 | 实测 | 说明 | 规模 | 目标 | 类别 |
|---|
定点观察:① 拖滑块换顶点,并切换到另外几个网格;② 看那个顶点周围被高亮的角度扇形 (每一对青色线就是一个三角形的内角);③ 结论:角亏损只依赖"这个顶点周围有几个面、每个面在那里张开多大" —— 它是内蕴的:把网格揉成一团也好、摊平也好,只要不撕开,每一个顶点的角亏损都不变; 而对整张闭合网格,它们加起来恰好是 2πχ(与网格长什么样无关,只与拓扑有关)。
本页用到的做法,出处在哪
- 半边结构记哪几样、next / twin 怎么绕圈:CGAL 手册 Halfedge Data Structures("Halfedges are required
to provide a reference to the next halfedge and to the opposite halfedge. Optionally they may provide a reference
to the previous halfedge, to the incident vertex, and to the incident face.");
CGAL 3D Polyhedral Surface 给出两条绕圈不变量("successive assignments of the form
h = h->next()cycle counterclockwise around the facet …h = h->next()->opposite()cycle clockwise around the vertex") —— doc.cgal.org/latest/HalfedgeDS、 Halfedge Class Reference。 ★ 我们用的字段名twin与 CGAL 的opposite()是同一个东西(一手源里twin/opposite/pair/Sym四种叫法都有,语义相同); 而prev是可选字段(CGAL 列为 optional,Berkeley/UIUC 干脆不存)—— 这是结构差异,不是命名差异。 - 绕圈怎么写、怎么终止:UC Berkeley CS184/284A Lecture 8: Mesh Representations("
Halfedge* h = v->halfedge; do { process(h->edge); h = h->twin->next; } while( h != v->halfedge );")—— cs184.eecs.berkeley.edu; CMU 15-462/662 Scotty3D Developer Manual 与 UIUC CS418 给出同一套写法与终止条件。 - 流形与可定向是前提:Berkeley CS184 同一份讲义("a 2D manifold is a surface that when cut with a small sphere
always yields a disk"、"An edge connects exactly two faces");CGAL 手册("the variant here is restricted to
orientable 2-manifolds only");Campagna / Kobbelt / Seidel Directed Edges("many data structures are restricted to
orientable 2-manifold triangle meshes … not able to represent such an object")。边界两派做法见 CMU 手册
(补一个 "virtual" boundary face)与 UIUC CS418("Edge-of-mesh half-edges have a null
twin")。 - 角亏损与离散 Gauss–Bonnet:Keenan Crane, Discrete Differential Geometry(CMU 15-458/858) "Discrete Curvature I"("The angle defect at a vertex i is the deviation of the sum of interior angles from the Euclidean angle sum of 2π: Ω_i := 2π − Σ θ_i"、"∑_{i∈int V} Ω_i + ∑_{i∈∂V} κ_i = 2πχ"、 "For any polyhedron of genus g, total angle defect is equal to 2π(1−g)·2")—— brickisland.net。
- 余切权:Wagner / Bunge / Botsch, A Hands-On Introduction to Discrete Differential Operators on Polygon Meshes("w_ij = (cot α_ij + cot β_ij) / 2"、"(Lu)_i = Σ_j w_ij (u_i − u_j)",以及负权那条 "the cotangent becomes negative for angles between 90 and 180 degrees")—— cg.cs.tu-dortmund.de; 均值曲率法向的 (1/2A)Σ(cotα+cotβ)(·) 形式见 Meyer / Desbrun / Schröder / Barr 2003 cs.jhu.edu。
- 欧拉公式与度数:一手源给的是
V − E + F = 2与Σ_v deg(v) = 2E、 三角网格3F = 2E(Campagna 等 "Euler's formula, V - E + F = 2";Berkeley CS184 同一份讲义)—— 本页表①③ 里的 24 / 12 / 6 都能拿这几条手算核对。
保真度与不做的部分:半边表、邻域遍历、角亏损、余切权全部由本页自己的代码算出
(页面现场与离线定稿 q204.mjs 同口径),画面是本页的二维软件光栅(正交投影 + 平面着色 + 线框),
不声称与任何网格库(CGAL / OpenMesh / libigl)的内部结构逐位相同。
★ 本页的边界处理取"twin = -1"这一派,不补"虚拟边界面",因此开放网格上不做绕顶点一圈的断言
(如实标注);也不做半边结构的编辑算子(split / collapse / flip)——
那是这个数据结构的另一半用途,本页只交付"读"的那一半。均值曲率用的顶点面积是三分之一面积,
不是 Meyer 的 mixed Voronoi 面积(页面上已标明)。
自检面板
等待脚本…