从 3D 三次贝塞尔路径获取一致的法线
Posted
技术标签:
【中文标题】从 3D 三次贝塞尔路径获取一致的法线【英文标题】:Getting consistent normals from a 3D cubic bezier path 【发布时间】:2014-08-22 18:10:57 【问题描述】:我正在编写一个包含 BezierPoints 列表的 BezierPath 类。每个 BezierPoint 都有一个位置、一个 inTangent 和一个 outTangent:
BezierPath 包含从路径获取线性位置和切线的函数。我的下一步是提供从路径获取法线的功能。
我知道任何给定的 3D 线都将有无限数量的垂直线,因此不会有一个固定的答案。
我的目标是让用户能够在每个 BezierPoint 指定法线(或滚动角?),我将在它们之间进行插值以获得沿路径的法线。我的问题是我不知道如何选择起始切线(默认切线应该是什么?)。
我第一次尝试获取起始切线是使用 Unity3D 方法Quaternion.LookRotation:
Quaternion lookAt = Quaternion.LookRotation(tangent);
Vector3 normal = lookAt * Vector3.up;
Handles.DrawLine(position, position + normal * 10.0f);
这会产生以下结果(绿线是切线,蓝线是法线):
在大多数情况下,这似乎是一个很好的基本结果,但看起来某些方向突然发生了变化:
所以我的问题是:有没有一种好方法可以让 3D 线条的默认法线保持一致?
谢谢, 韦斯
【问题讨论】:
我不知道“in unity”,一点也不熟悉,但我对贝塞尔数学了如指掌,可以为您提供获得单位所需的功能如果您愿意,可以在任何坐标处正常。这不是一个统一的答案,但它会起作用。如果没问题,我会写一个答案 这确实不是 Unity 特有的问题。这里的版主似乎只是将我的大部分问题标记为与统一相关,所以我想我会为他们省去麻烦。我非常想听听您获得法线的解决方案! 【参考方案1】:获得贝塞尔曲线上一个点的法线实际上非常简单,因为法线只是垂直于函数的切线(定向在曲线行进方向的平面内),以及贝塞尔曲线的切线函数曲线实际上只是另一个贝塞尔曲线,低了 1 个订单。让我们找到三次贝塞尔曲线的法线。正则函数,(a,b,c,d) 为一维曲线坐标:
function computeBezier (t, a, b, c, d)
return a * (1-t)³ + 3 * b * (1-t)² * t + 3 * c * (1-t) * t² + d * t³
请注意,贝塞尔曲线是对称的,t
与 1-t
之间的唯一区别是曲线的哪一端代表“起点”。使用a * (1-t)³
表示曲线从a
开始。使用a * t³
会使其从d
开始。
所以让我们用以下坐标定义一条快速曲线:
a = (-100,100,0)
b = (200,-100,100)
c = (0,100,-500)
d = (-100,-100,100)
为了得到这个函数的法线,我们首先需要derivative:
function computeBezierDerivative (t,a,b,c,d)
a = 3*(b−a)
b = 3*(c-b)
c = 3*(d-c)
return a * (1-t)² + 2 * b * (1-t) * t + c * t²
完成。计算导数非常简单(贝塞尔曲线的奇妙特性)。
现在,为了获得法线,我们需要在某个值t
处取归一化切向量,并将其旋转四分之一圈。我们可以在很多方向上转动它,所以进一步的限制是我们只想在由切向量定义的平面上转动它,并且切向量“就在它旁边”,相隔一个无限小的间隔。
任何贝塞尔曲线的切线向量都是通过获取您拥有的任意多维并单独评估它们来形成的,因此对于 3D 曲线:
| computeBezierDerivative(t, x values) | |x'|
Tangent(t) = | computeBezierDerivative(t, y values) | => |y'|
| computeBezierDerivative(t, z values) | |z'|
同样,计算非常简单。为了规范化这个向量(或者实际上是任何向量),我们只需将向量除以它的长度:
|x'|
NormalTangent(t) = |y'| divided by sqrt(x'² + y'² + z'²)
|z'|
所以让我们把它们画成绿色:
现在唯一的技巧是找到旋转切线向量的平面,将切线转换为法线。我们知道我们可以使用另一个与我们想要的任意接近的 t 值,并将其转换为该死的在同一点附近的第二个切向量,以找到具有任意正确性的平面,因此我们可以这样做:
给定一个原始点f(t1)=p
,我们用t2=t1+e
取一个点f(t2)=q
,其中e 是一个像0.001 这样的小值——这个点q
有一个导数q' = pointDerivative(t2)
,并且为了使我们更容易,我们将那个切向量移动一点p-q
,这样两个向量都“开始”在p
。很简单。
但是,这相当于计算p
处的一阶和二阶导数,然后通过将这两者相加形成第二个向量,因为二阶导数为我们提供了一点切线的变化,因此添加一阶导数向量的二阶导数向量在p
处为我们提供平面中的两个向量,而无需找到相邻点。这在导数中存在不连续性的曲线(即具有尖点的曲线)中很有用。
我们现在有两个向量,在同一个坐标出发:我们的真正切线,以及“下一个”点的切线,它非常接近,可能是同一个点。值得庆幸的是,由于贝塞尔曲线的工作原理,第二条切线从不相同,但略有不同,我们只需要“略有不同”:如果我们有两个归一化向量,从同一点开始但是指向不同的方向,我们可以通过它们之间的cross product 找到我们需要旋转一个轴以获得另一个轴,因此我们可以找到它们都经过的平面。
顺序很重要:我们计算 c = tangent₂ × tangent₁,因为如果我们计算 c = tangent₁ × tangent₂,我们将计算旋转轴和生成的法线“方向错误。更正这实际上只是最后一个“取向量,乘以-1”,但是为什么在我们可以正确处理的事实之后纠正呢,在这里。让我们看看那些蓝色的旋转轴:
现在我们拥有了所需的一切:为了将归一化的切向量转换为法向量,我们所要做的就是将它们围绕我们刚刚找到的轴旋转四分之一圈。如果我们将它们转向一个方向,我们会得到法线,如果我们将它们转向另一个,我们就会得到背面的法线。
对于绕 3D 轴的任意旋转,that job is perhaps laborious, but not difficult,四分之一圈通常是特殊的,因为它们极大地简化了数学运算:要在我们的旋转轴 c 上旋转一个点,旋转矩阵原来是:
| c₁² c₁*c₂ - c₃ c₁*c₃ + c₂ |
R = | c₁*c₂ + c₃ c₂² c₂*c₃ - c₁ |
| c₁*c₃ - c₂ c₂*c₃ + c₁ c₃² |
其中 1、2 和 3 下标实际上只是我们向量的 x、y 和 z 分量。所以这仍然很容易,剩下的就是矩阵旋转我们的归一化切线:
n = R * Tangent "T"
这是:
| T₁ * R₁₁ + T₂ * R₁₂ + T₃ * R₁₃ | |nx|
n = | T₁ * R₂₁ + T₂ * R₂₂ + T₃ * R₂₃ | => |ny|
| T₁ * R₃₁ + T₂ * R₃₂ + T₃ * R₃₃ | |nz|
我们有我们需要的法线向量。完美!
除了我们可以做得更好:因为我们不是使用任意角度而是使用直角,所以我们可以使用一个重要的捷径。就像向量 c 垂直于两条切线一样,我们的法线 n 垂直于 c 和正切线,所以我们可以再次使用叉积求法线:
|nx|
n = c × tangent₁ => |ny|
|nz|
这将为我们提供完全相同的向量,而且工作量更少。
如果我们想要内部法线,它是同一个向量,只需乘以 -1:
知道技巧后就很容易了!最后,因为代码总是有用的this gist 是Processing 程序,我用来确保我说的是真话。
如果法线的行为真的很奇怪怎么办?
例如,如果我们使用的是 3D 曲线,但它是平面的(例如,所有 z
坐标都为 0)怎么办?事情突然做可怕的事情。例如,让我们看一下坐标为 (0,0,0)、(-38,260,0)、(-25,541,0) 和 (-15,821,0) 的曲线:
同样,特别是弯曲的曲线可能会产生相当扭曲的法线。查看坐标为 (0,0,0)、(-38,260,200)、(-25,541,-200) 和 (-15,821,600) 的曲线:
在这种情况下,我们希望法线尽可能少地旋转和扭曲,这可以使用旋转最小化框架算法找到,例如第 4 节或"Computation of Rotation Minimizing Frames" (Wenping Wang, Bert Jüttler, Dayue Zheng, and Yang Liu, 2008) 中的说明。
使用普通编程语言(例如 Java/Processing)实现他们的 9 行算法需要更多的工作:
ArrayList<VectorFrame> getRMF(int steps)
ArrayList<VectorFrame> frames = new ArrayList<VectorFrame>();
double c1, c2, step = 1.0/steps, t0, t1;
PointVector v1, v2, riL, tiL, riN, siN;
VectorFrame x0, x1;
// Start off with the standard tangent/axis/normal frame
// associated with the curve just prior the Bezier interval.
t0 = -step;
frames.add(getFrenetFrame(t0));
// start constructing RM frames
for (; t0 < 1.0; t0 += step)
// start with the previous, known frame
x0 = frames.get(frames.size() - 1);
// get the next frame: we're going to throw away its axis and normal
t1 = t0 + step;
x1 = getFrenetFrame(t1);
// First we reflect x0's tangent and axis onto x1, through
// the plane of reflection at the point midway x0--x1
v1 = x1.o.minus(x0.o);
c1 = v1.dot(v1);
riL = x0.r.minus(v1.scale( 2/c1 * v1.dot(x0.r) ));
tiL = x0.t.minus(v1.scale( 2/c1 * v1.dot(x0.t) ));
// Then we reflection a second time, over a plane at x1
// so that the frame tangent is aligned with the curve tangent:
v2 = x1.t.minus(tiL);
c2 = v2.dot(v2);
riN = riL.minus(v2.scale( 2/c2 * v2.dot(riL) ));
siN = x1.t.cross(riN);
x1.n = siN;
x1.r = riN;
// we record that frame, and move on
frames.add(x1);
// and before we return, we throw away the very first frame,
// because it lies outside the Bezier interval.
frames.remove(0);
return frames;
不过,这确实很好用。请注意,Frenet 框架是“标准”切线/轴/法线框架:
VectorFrame getFrenetFrame(double t)
PointVector origin = get(t);
PointVector tangent = derivative.get(t).normalise();
PointVector normal = getNormal(t).normalise();
return new VectorFrame(origin, tangent, normal);
对于我们的平面曲线,我们现在可以看到表现完美的法线:
并且在非平面曲线中,旋转最小:
最后,这些法线可以通过围绕相关切向量旋转所有向量来统一重新定向。
【讨论】:
非常感谢 Mike,这一切看起来都很棒。给我几个小时让它深入并重现它,我一定会将此答案标记为正确。 迈克,这绝对是完美的工作,但我注意到直线的法线为 0,0,0。这在处理上是一样的吗? 如果你有一条直线,严格来说,整个路径只有一条切线,因此无法确定我们正在通过哪个平面。如果它是折线的一部分,这里最简单的事情是确定曲线之前/之后的法线,并使用线性插值来获得沿线的“法线”。它们将是谎言(因为在 3D 线段中没有法线向量,只有法线平面)但它们将是善意的谎言,因为它们完全可以完成工作。 嗨。真的很有帮助的帖子。我花了一段时间才掌握它,但我现在明白了。我使用它围绕曲线生成 3D 圆柱体,法线可用于照明,也可用于生成网格本身。然而,随着某些曲线法线的符号转换,网格中存在内插扭曲。你知道我怎么能解决这个问题吗?让标志保持一致? 很确定答案涵盖了这一点:因为“本机”数学法线不是 好看 法线,所以如果您正在做诸如相机偏移之类的事情,那么边界线就没用了跟踪、路径规划、模型挤压和其他一百万个依赖体面法线的任务。此外,贝塞尔函数是无限曲线的一部分,因此您从 before 零开始,这样双反射会产生正常的 at 零,并且表现得足够好。至于最后一个问题:从字面上看,任何与曲线成直角的点的向量都是法线,所以我们不关心“正确”。以上是关于从 3D 三次贝塞尔路径获取一致的法线的主要内容,如果未能解决你的问题,请参考以下文章