1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-09-09 16:10:42 +02:00

chinese localizations

This commit is contained in:
Pomax
2022-09-03 09:57:23 -07:00
parent 737e020eaa
commit 152db3cdf6
126 changed files with 20648 additions and 48349 deletions

View File

@@ -0,0 +1,110 @@
# 三维法向量
在进入下一章之前需要花点时间探究二维和三维的区别。尽管这一区别影响的范围不大无关且两种情形的做法相同比如求三维切向量与二维情形所做的一样不过所求为x、y、z而不只是x、y但是法向量的情况有点复杂所做的也就更多。尽管不是“极其困难”但是所需的步骤更多需要仔细看看。
三维法向量的求法原则上与二维一样——将规范化的切向量旋转90度。然而这就是情况变得略微复杂的地方因为三维的“法向量”是法平面上的任意一个向量所以可以旋转的方向并不唯一因此需要定义三维情形中“唯一的”法向量是什么。
“朴素”的方法是构造[弗勒内法向量](https://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas)而以下采用的简单做法在很多情况下都可行但在其他情况下会得到极其怪异的结果。思路是虽然有无穷多个向量与切向量垂直即与之成90度角但是切向量本身已差不多位于自带的平面上——因为曲线上的每一点无论间隔多小都有自己的切向量所以可以说每个点都位于此处的切向量和“近旁”的切向量所在的平面上。
即使这两个切向量的差微乎其微只要“有差”就可求出这一平面或者说求出垂直于平面的向量。计算出这一向量之后因为切向量在平面上所以将切向量绕垂直向量旋转即可。计算这一法向量的逻辑与二维情形相同——“直接旋转90度”。
那么开始吧!令人意外的是四行就做完了:
- **a** = normalize(*B'*(*t*))
- **b** = normalize(**a** + *B''*(*t*))
- **r** = normalize(**b** × **a**)
- **normal** = normalize(**r** × **a**)
展开说几句:
- 先将曲线上一点的导数规范化得到[单位向量](https://en.wikipedia.org/wiki/Unit_vector)。规范化可以减少计算量,而计算量越少越好。
- 再计算**b**。假如曲线从这个点开始不再变化,保持导数和二阶导数不变,则**b**表示下一个点处的切向量。
- 得到两个共面向量后(导数、导数与二阶导数的和),用[叉积](https://en.wikipedia.org/wiki/Cross_product)这一基本的向量运算可以求出与这一平面垂直的向量。注意这一运算使用的符号×绝非乘法运算叉积所得向量可以当做“旋转轴”像二维情形一样将切向量旋转90度得到法向量。
- 既然由叉积可得垂直于由两个向量所确定的平面的另一向量,而法向量又与切向量和旋转轴所在平面垂直,那么再用一次叉积即得法向量。
这样就求出了三维曲线“唯一”的法向量。以一条曲线为例看看效果如何?从左往右拖动滚动条,根据鼠标位置所确定的*t*值显示在此处的法向量——最左为0最右为1中间为0.5,等等:
<graphics-element title="一些已知和未知向量" width="350" height="300" src="./frenet.js">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
然而摆弄图像一阵之后可能会察觉到异样——法向量似乎在t=0.65和t=0.75之间“绕着曲线急转弯”……为什么会这样?
其实出现这种现象是因为数学公式就是这样推导的,所以弗勒内法向量的问题就在于此:虽然“从数学上看”是对的,但是“从实际上看”有问题。因此为了让任何图像都不出问题,所真正需要的是只要……看起来不错就好的方法。
还好不只有弗勒内法向量这一种选择。另一种选择是采用稍微偏算法的方式计算一种形式的[旋转最小化标架](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf)(亦称“平行输运标架”或“比舍标架”),此处“标架”是以线上点为原点,由切向量、旋转轴和法向量构成的集合。
因为计算这种类型的标架依赖于“上一个标架”,所以无法像弗勒内标架一样“按需”对单独的点直接计算,而是需要对整条曲线进行计算。好在计算过程相当简单,而且可以与曲线查询表的构建同时进行。
思路是在t=0处取一个由切向量、旋转轴、法向量构成的初始标架再使用一定的规则计算下一标架“应有”的形式。上文链接的旋转最小化标架论文给出的规则为
- 取曲线上已经求出旋转最小化标架的一个点,
- 取曲线上尚未求出旋转最小化标架的下一个点,
- 再以上一个点和下一个点的中垂面为镜面,将已有标架翻转到下一个点上。
- 翻转后的切向量方向与下一个点的切向量方向大致相反,而且法向量也略有歪斜。
- 于是再以翻转后的切向量和下一个点的切向量所确定的平面为镜面,将翻转后的标架再次翻转。
- 切向量和法向量修正完毕,所得即为好用的标架。
来写点代码吧!
<div class="howtocode">
### 实现旋转最小化标架
首先假设已有函数用于计算上文提及的指定点的弗勒内标架,输出的标架具有如下性质:
```
{
o所有向量的起点即线上点
t切向量
r旋转轴向量
n法向量
}
```
再如下写出生成一系列旋转最小化标架的函数:
```
generateRMFrames(steps) -> frames:
step = 1.0/steps
// 从曲线上t=0处标准的切向量/旋转轴/法向量标架开始:
frames.add(getFrenetFrame(0))
// 开始构造旋转最小化标架:
for t0 = 0, t0 < 1.0, t0 += step:
// 从已有的上一标架开始
x0 = frames.last
// 求出下一标架:要保留它的位置和切向量,
// 但要重新计算轴向量和法向量。
t1 = t0 + step
x1 = { o: getPoint(t1), t: getDerivative(t) }
// 首先以x0和x1的中垂面为镜面
// 将x0的切向量和旋转轴翻转到x1处
v1 = x1.o - x0.o
c1 = v1 · v1
riL = x0.r - v1 * 2/c1 * v1 · x0.r
tiL = x0.t - v1 * 2/c1 * v1 · x0.t
// 注意到v1为向量而2/c1和v1 · ……就是数,
// 因此上述代码只是对v1进行一定倍数的缩放。
// 然后再以过x1的平面为镜面再次翻转
// 标架向量的方向即重新与曲线切向量一致:
v2 = x1.t - tiL
c2 = v2 · v2
// 收尾工作:
x1.r = riL - v2 * 2/c2 * v2 · riL
x1.n = x1.r × x1.t
frames.add(x1)
```
即使忽略注释,代码也明显比计算单个弗勒内标架的多,但也没有多得离谱,而且得到了长得更好的法向量。
</div>
提到长得更好,这样的标架到底是什么样子?下面回顾之前的那条曲线,但这次用的不是弗勒内标架而是旋转最小化标架:
<graphics-element title="一些已知和未知向量" width="350" height="300" src="./rotation-minimizing.js">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
看起来好多了!
给看过代码的读者的话严格来说一开始甚至不需要弗勒内标架。比方说可以将z轴当作初始旋转轴于是初始法向量为 **(0,0,1)** × **切向量**,然后再继续下去。不过求出“数学上正确”的初始标架,从而让初始法向量的方向符合曲线在三维空间中的定向,这总归是不错的。