Ves*_*ian 7 3d bezier unity-game-engine normals
我正在编写一个包含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);
Run Code Online (Sandbox Code Playgroud)
这导致以下结果(绿线是切线,蓝线是法线):
在大多数情况下,这似乎是一个很好的基础结果,但看起来在某些方向上有突然的变化:
所以我的问题是:是否有一种很好的方法可以获得3D中线条的一致默认法线?
谢谢,Ves
Mik*_*ans 24
在Bezier曲线上获得点的法线实际上非常简单,因为法线只是垂直于函数的切线(在曲线的行进方向的平面中定向),并且Bezier曲线的切线函数实际上是只是另一条贝塞尔曲线,低一级.让我们找到一个三次贝塞尔曲线的法线.常规函数,(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³
}
Run Code Online (Sandbox Code Playgroud)
请注意,贝塞尔曲线是对称的,t
vs 之间的唯一区别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)
Run Code Online (Sandbox Code Playgroud)
为了使这个函数正常,我们首先需要导数:
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²
}
Run Code Online (Sandbox Code Playgroud)
完成.计算导数是非常简单的(贝塞尔曲线的奇妙特性).
现在,为了得到法线,我们需要将归一化的切向量取为某个值t
,并将其旋转四分之一圈.我们可以在很多方向上转向它,所以进一步的限制是我们只想在由切向量定义的平面中切换它,并且切线向量"紧挨着它",这是一个无限小的间隔.
任何Bezier曲线的切线向量都是通过采用多个维度来形成的,并分别对它们进行评估,因此对于3D曲线:
| computeBezierDerivative(t, x values) | |x'|
Tangent(t) = | computeBezierDerivative(t, y values) | => |y'|
| computeBezierDerivative(t, z values) | |z'|
Run Code Online (Sandbox Code Playgroud)
同样,计算起来非常简单.为了规范化这个向量(或实际上任何向量),我们只需按其长度执行向量除法:
|x'|
NormalTangent(t) = |y'| divided by sqrt(x'² + y'² + z'²)
|z'|
Run Code Online (Sandbox Code Playgroud)
那么让我们画绿色:
现在唯一的技巧是找到旋转切线向量的平面,将切线转换为法线.我们知道我们可以使用另一个t值任意接近我们想要的那个,并将其转换为在同一点附近的第二个切线向量,以找到具有任意正确性的平面,因此我们可以这样做:
考虑到原点f(t1)=p
,我们采取一个点f(t2)=q
用t2=t1+e
,其中ê就像是0.001一些小的价值-这一点q
有一个衍生q' = pointDerivative(t2)
,并且为了使事情对我们比较容易,我们谨切向量通过一点点p-q
让两个向量两者都"开始" p
.很简单.
然而,这相当于计算第一和第二导数p
,然后通过将这两个加在一起形成第二个矢量,因为二阶导数给出了一个点处切线的变化,因此将二阶导数加到第一个导数向量在平面中得到两个向量,p
而不必找到相邻的点.这在导数中存在不连续性的曲线中是有用的,即具有尖点的曲线.
我们现在有两个向量,在相同的坐标处离开:我们的实际切线,以及"下一个"点的切线,它是如此接近,它可能也是同一个点.值得庆幸的是,由于Bezier曲线的工作原理,这第二个切线从不相同,但略有不同,我们只需要"稍微不同":如果我们有两个归一化向量,从同一点开始但指向不同的方向,我们我们可以找到我们需要旋转一个轴的轴,只需通过它们之间的交叉产品来获得另一个轴,从而我们可以找到它们都经过的平面.
顺序问题:我们计算c = tangent 2×tangent 1,因为如果我们计算c = tangent 1×tangent 2,我们将计算旋转轴并在"错误"方向上得到法线.在最后校正这实际上只是一个"取向量,乘以-1",但为什么在我们能够正确的事实之后纠正,在这里.让我们看看那些蓝色的旋转轴:
现在我们拥有了我们需要的一切:为了将我们的归一化切向量转换为法向量,我们所要做的就是将它们围绕我们刚刚找到的轴旋转四分之一圈.如果我们将它们转向一个方向,我们就会得到法线,如果我们将它们转向另一个方向,我们就会得到法线.
对于3D中的轴的任意旋转,该作业可能是费力的,但并不困难,并且四分之一圈通常是特别的,因为它们极大地简化了数学:在我们的旋转轴c上旋转一个点,旋转矩阵结果是是:
| c?² c?*c? - c? c?*c? + c? |
R = | c?*c? + c? c?² c?*c? - c? |
| c?*c? - c? c?*c? + c? c?² |
Run Code Online (Sandbox Code Playgroud)
其中1,2和3下标实际上只是我们向量的x,y和z分量.所以这仍然很容易,剩下的就是矩阵旋转我们的归一化切线:
n = R * Tangent "T"
Run Code Online (Sandbox Code Playgroud)
这是:
| T? * R?? + T? * R?? + T? * R?? | |nx|
n = | T? * R?? + T? * R?? + T? * R?? | => |ny|
| T? * R?? + T? * R?? + T? * R?? | |nz|
Run Code Online (Sandbox Code Playgroud)
我们有我们需要的法向量.完善!
除了我们可以做得更好:因为我们不使用任意角度但是使用直角,我们可以使用一个重要的捷径.与矢量c垂直于两个切线的方式相同,我们的法线n垂直于c和常规切线,因此我们可以第二次使用叉积来找到法线:
|nx|
n = c × tangent? => |ny|
|nz|
Run Code Online (Sandbox Code Playgroud)
这将给我们完全相同的向量,减少工作量.
如果我们想要内部法线,它是相同的向量,只需乘以-1:
一旦你了解技巧就很容易!最后,因为代码总是很有用,所以这个要点就是我用来确保说实话的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节或"旋转最小化帧的计算"中所解释的(Wenping Wang,BertJüttler,Dayue Zheng)和杨柳,2008).
实现他们的9行算法需要在普通的编程语言中进行更多的工作,例如Java/Processing:
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;
}
Run Code Online (Sandbox Code Playgroud)
不过,这个效果还算不错.注意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);
}
Run Code Online (Sandbox Code Playgroud)
对于我们的平面曲线,我们现在看到完美表现的法线:
在非平面曲线中,旋转最小:
最后,通过围绕相关切向量旋转所有向量,可以均匀地重新定向这些法线.
归档时间: |
|
查看次数: |
3809 次 |
最近记录: |