我在计算贝塞尔曲线和b样条曲线的弧长时遇到了一些问题。几天来,我一直在反对这一点,我想我几乎就在那里,但似乎无法让它完全正确。我在Swift中开发,但我认为它的语法很清楚,任何知道C / C ++的人都能读懂它。如果没有,请告诉我,我会尝试将其翻译成C / C ++。
我已经一遍又一遍地检查了我对几个来源的实现,并且,就算法而言,它们似乎是正确的,尽管我对B样条算法不太确定。一些教程使用学位,有些使用曲线的顺序计算,我真的很困惑。另外,在使用Gauss-Legendre积分时,我理解我应该总结跨度的积分,但我不确定我是否理解如何正确地做到这一点。根据我的理解,我应该在每个结跨度上进行整合。这是对的吗?
当我使用以下控制多边形计算贝塞尔曲线的长度时,我得到28.2842712474619,而3D软件(Cinema 4D和Maya)告诉我长度应为30.871。
let bezierControlPoints = [
Vector(-10.0, -10.0),
Vector(0.0, -10.0),
Vector(0.0, 10.0),
Vector(10.0, 10.0)
]
b样条的长度同样关闭。我的算法产生5.6062782185353,而它应该是7.437。
let splineControlPoints = [
Vector(-2.0, -1.0),
Vector(-1.0, 1.0),
Vector(-0.25, 1.0),
Vector(0.25, -1.0),
Vector(1.0, -1.0),
Vector(2.0, 1.0)
]
我不是数学家,所以我在数学方面苦苦挣扎,但我认为我有它的主旨。
Vector类非常简单,但我为了方便/易读而使一些操作符超载,这使得代码非常冗长,所以我不在这里发布它。我还没有包括Gauss-Legendre重量和横坐标。您可以从here(53K)下载源代码和Xcode项目。
这是我的bezier曲线课程:
class Bezier
{
var c0:Vector
var c1:Vector
var c2:Vector
var c3:Vector
init(ic0 _ic0:Vector, ic1 _ic1:Vector, ic2 _ic2:Vector, ic3 _ic3:Vector) {
c0 = _ic0
c1 = _ic1
c2 = _ic2
c3 = _ic3
}
// Calculate curve length using Gauss-Legendre quadrature
func curveLength()->Double {
let gl = GaussLegendre()
gl.order = 3 // Good enough for a quadratic polynomial
let xprime = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in return self.dx(atTime:t) })
let yprime = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in return self.dy(atTime:t) })
return sqrt(xprime*xprime + yprime*yprime)
}
// I could vectorize this, but correctness > efficiency
// The derivative of the x-component
func dx(atTime t:Double)->Double {
let tc = (1.0-t)
let r0 = (3.0 * tc*tc) * (c1.x - c0.x)
let r1 = (6.0 * tc*t) * (c2.x - c1.x)
let r2 = (3.0 * t*t) * (c3.x - c2.x)
return r0 + r1 + r2
}
// The derivative of the y-component
func dy(atTime t:Double)->Double {
let tc = (1.0-t)
let r0 = (3.0 * tc*tc) * (c1.y - c0.y)
let r1 = (6.0 * tc*t) * (c2.y - c1.y)
let r2 = (3.0 * t*t) * (c3.y - c2.y)
return r0 + r1 + r2
}
}
这是我的b-spline类:
class BSpline
{
var spanLengths:[Double]! = nil
var totalLength:Double = 0.0
var cp:[Vector]
var knots:[Double]! = nil
var o:Int = 4
init(controlPoints:[Vector]) {
cp = controlPoints
calcKnots()
}
// Method to return length of the curve using Gauss-Legendre numerical integration
func cacheSpanLengths() {
spanLengths = [Double]()
totalLength = 0.0
let gl = GaussLegendre()
gl.order = o-1 // The derivative should be quadratic, so o-2 would suffice?
// Am I doing this right? Piece-wise integration?
for i in o-1 ..< knots.count-o {
let t0 = knots[i]
let t1 = knots[i+1]
let xprime = gl.integrate(a:t0, b:t1, closure:self.dx)
let yprime = gl.integrate(a:t0, b:t1, closure:self.dy)
let spanLength = sqrt(xprime*xprime + yprime*yprime)
spanLengths.append(spanLength)
totalLength += spanLength
}
}
// The b-spline basis function
func basis(i:Int, _ k:Int, _ x:Double)->Double {
var r:Double = 0.0
switch k {
case 0:
if (knots[i] <= x) && (x <= knots[i+1]) {
r = 1.0
} else {
r = 0.0
}
default:
var n0 = x - knots[i]
var d0 = knots[i+k]-knots[i]
var b0 = basis(i,k-1,x)
var n1 = knots[i+k+1] - x
var d1 = knots[i+k+1]-knots[i+1]
var b1 = basis(i+1,k-1,x)
var left = Double(0.0)
var right = Double(0.0)
if b0 != 0 && d0 != 0 { left = n0 * b0 / d0 }
if b1 != 0 && d1 != 0 { right = n1 * b1 / d1 }
r = left + right
}
return r
}
// Method to calculate and store the knot vector
func calcKnots() {
// The number of knots in the knot vector = number of control points + order (i.e. degree + 1)
let knotCount = cp.count + o
knots = [Double]()
// For an open b-spline where the ends are incident on the first and last control points,
// the first o knots are the same and the last o knots are the same, where o is the order
// of the curve.
var k = 0
for i in 0 ..< o {
knots.append(0.0)
}
for i in o ..< cp.count {
k++
knots.append(Double(k))
}
k++
for i in cp.count ..< knotCount {
knots.append(Double(k))
}
}
// I could vectorize this, but correctness > efficiency
// Derivative of the x-component
func dx(t:Double)->Double {
var p = Double(0.0)
let n = o
for i in 0 ..< cp.count-1 {
let u0 = knots[i + n + 1]
let u1 = knots[i + 1]
let fn = Double(n) / (u0 - u1)
let thePoint = (cp[i+1].x - cp[i].x) * fn
let b = basis(i+1, n-1, Double(t))
p += thePoint * b
}
return Double(p)
}
// Derivative of the y-component
func dy(t:Double)->Double {
var p = Double(0.0)
let n = o
for i in 0 ..< cp.count-1 {
let u0 = knots[i + n + 1]
let u1 = knots[i + 1]
let fn = Double(n) / (u0 - u1)
let thePoint = (cp[i+1].y - cp[i].y) * fn
let b = basis(i+1, n-1, Double(t))
p += thePoint * b
}
return Double(p)
}
}
这是我的高斯 - 勒让德实施:
class GaussLegendre
{
var order:Int = 5
init() {
}
// Numerical integration of arbitrary function
func integrate(a _a:Double, b _b:Double, closure f:(Double)->Double)->Double {
var result = 0.0
let wgts = gl_weights[order-2]
let absc = gl_abscissae[order-2]
for i in 0..<order {
let a0 = absc[i]
let w0 = wgts[i]
result += w0 * f(0.5 * (_b + _a + a0 * (_b - _a)))
}
return 0.5 * (_b - _a) * result
}
}
我的主要逻辑是:
let bezierControlPoints = [
Vector(-10.0, -10.0),
Vector(0.0, -10.0),
Vector(0.0, 10.0),
Vector(10.0, 10.0)
]
let splineControlPoints = [
Vector(-2.0, -1.0),
Vector(-1.0, 1.0),
Vector(-0.25, 1.0),
Vector(0.25, -1.0),
Vector(1.0, -1.0),
Vector(2.0, 1.0)
]
var bezier = Bezier(controlPoints:bezierControlPoints)
println("Bezier curve length: \(bezier.curveLength())\n")
var spline:BSpline = BSpline(controlPoints:splineControlPoints)
spline.cacheSpanLengths()
println("B-Spline curve length: \(spline.totalLength)\n")
感谢Mike的回答!
我验证了我正确地将数值积分从间隔a..b重新映射到-1..1,以用于勒让德 - 高斯求积分。数学是here(对任何真正的数学家道歉,这是我用我长期遗忘的微积分所做的最好的事情)。
我建议将勒让德 - 高斯积分的阶数从5增加到32。
然后经过Mathematica的大量挣扎后,我回来重新阅读Mike的代码,发现我的代码 NOT 等同于他。
我取得了导数分量的平方积分之和的平方根:
当我应该采用导数向量的积分时:
在代码方面,在我的Bezier类中,而不是:
// INCORRECT
func curveLength()->Double {
let gl = GaussLegendre()
gl.order = 3 // Good enough for a quadratic polynomial
let xprime = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in return self.dx(atTime:t) })
let yprime = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in return self.dy(atTime:t) })
return sqrt(xprime*xprime + yprime*yprime)
}
我应该写这个:
// CORRECT
func curveLength()->Double {
let gl = GaussLegendre()
gl.order = 32
return = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in
let x = self.dx(atTime:t)
let y = self.dy(atTime:t)
return sqrt(x*x + y*y)
})
}
我的代码计算弧长为:3.59835872777095
Mathematica:3.598358727834686
所以,我的结果非常接近。有趣的是,我的测试贝塞尔曲线的Mathematica图与Cinema 4D渲染的图之间存在差异,这可以解释为什么Mathematica和Cinema 4D计算的弧长也不同。不过,我认为我相信Mathematica会更正确。
在我的B-Spline课程中,而不是:
// INCORRECT
func cacheSpanLengths() {
spanLengths = [Double]()
totalLength = 0.0
let gl = GaussLegendre()
gl.order = o-1 // The derivative should be quadratic, so o-2 would suffice?
// Am I doing this right? Piece-wise integration?
for i in o-1 ..< knots.count-o {
let t0 = knots[i]
let t1 = knots[i+1]
let xprime = gl.integrate(a:t0, b:t1, closure:self.dx)
let yprime = gl.integrate(a:t0, b:t1, closure:self.dy)
let spanLength = sqrt(xprime*xprime + yprime*yprime)
spanLengths.append(spanLength)
totalLength += spanLength
}
}
我应该写这个:
// CORRECT
func cacheSpanLengths() {
spanLengths = [Double]()
totalLength = 0.0
let gl = GaussLegendre()
gl.order = 32
// Am I doing this right? Piece-wise integration?
for i in o-1 ..< knots.count-o {
let t0 = knots[i]
let t1 = knots[i+1]
let spanLength = gl.integrate(a:t0, b:t1, closure:{ (t:Double)->Double in
let x = self.dx(atTime:t)
let y = self.dy(atTime:t)
return sqrt(x*x + y*y)
})
spanLengths.append(spanLength)
totalLength += spanLength
}
}
不幸的是,B-Spline数学并不是那么直截了当,我还没有像Bezier数学那样在Mathematica中测试它,所以我不能完全确定我的代码是工作,即使有上述变化。我在验证时会发布另一个更新。
而不是
// Derivative of the x-component
func dx(t:Double)->Double {
var p = Double(0.0)
let n = o // INCORRECT (should be one less)
for i in 0 ..< cp.count-1 {
let u0 = knots[i + n + 1]
let u1 = knots[i + 1]
let fn = Double(n) / (u0 - u1)
let thePoint = (cp[i+1].x - cp[i].x) * fn
let b = basis(i+1, n-1, Double(t))
p += thePoint * b
}
return Double(p)
}
// Derivative of the y-component
func dy(t:Double)->Double {
var p = Double(0.0)
let n = o // INCORRECT (should be one less_
for i in 0 ..< cp.count-1 {
let u0 = knots[i + n + 1]
let u1 = knots[i + 1]
let fn = Double(n) / (u0 - u1)
let thePoint = (cp[i+1].y - cp[i].y) * fn
let b = basis(i+1, n-1, Double(t))
p += thePoint * b
}
return Double(p)
}
我应该写
// Derivative of the x-component
func dx(t:Double)->Double {
var p = Double(0.0)
let n = o-1 // CORRECT
for i in 0 ..< cp.count-1 {
let u0 = knots[i + n + 1]
let u1 = knots[i + 1]
let fn = Double(n) / (u0 - u1)
let thePoint = (cp[i+1].x - cp[i].x) * fn
let b = basis(i+1, n-1, Double(t))
p += thePoint * b
}
return Double(p)
}
// Derivative of the y-component
func dy(t:Double)->Double {
var p = Double(0.0)
let n = o-1 // CORRECT
for i in 0 ..< cp.count-1 {
let u0 = knots[i + n + 1]
let u1 = knots[i + 1]
let fn = Double(n) / (u0 - u1)
let thePoint = (cp[i+1].y - cp[i].y) * fn
let b = basis(i+1, n-1, Double(t))
p += thePoint * b
}
return Double(p)
}
我的代码现在计算B样条曲线的长度为6.87309971722132。 Mathematica:6.87309884638438。
它可能在科学上并不精确,但对我来说足够好。
答案 0 :(得分:2)
Legendre-Gauss程序专门为区间[-1,1]定义,而Beziers和B-Splines在[0,1]上定义,所以这是一个简单的转换,至少在你尝试时确保你的代码做正确的事情,轻松烘焙而不是提供动态间隔(正如你所说,准确性超过效率。一旦它工作,我们可以担心优化)
所以,给定权重W
和横向A
(两者长度相同n
),您可以这样做:
z = 0.5
for i in 1..n
w = W[i]
a = A[i]
t = z * a + z
sum += w * arcfn(t, xpoints, ypoints)
return z * sum
伪代码假设列表从1开始索引。arcfn
将被定义为:
arcfn(t, xpoints, ypoints):
x = derive(xpoints, t)
y = derive(ypoints, t)
c = x*x + y*y
return sqrt(c)
但那部分看起来已经是正确的了。
你的衍生物看起来也是正确的,所以主要问题是:“你在勒让德高斯求积法中使用了足够多的切片吗?”。您的代码建议您仅使用5个切片,这远远不足以获得良好的结果。使用http://pomax.github.io/bezierinfo/legendre-gauss.html作为术语数据,通常需要n
为16或更高的集合(对于三次贝塞尔曲线,24通常是安全的,但对于具有尖点或大量变形的曲线仍然不太合适)。 / p>
我建议在这里采用“单元测试”方法:测试bezier和bspline代码(单独)以获取已知的基本值和派生值。退房吗?排除了一个问题。在您的LG代码上:如果您使用以下参数函数对直线执行Legendre-Gauss:
fx(t) = t
fy(t) = t
fx'(t) = 1
fy'(t) = 1
超过间隔t = [0,1],我们知道长度应该正好 2的平方根,并且导数是最简单的。如果这些有效,请使用以下方法进行非线性测试:
fx(t) = sin(t)
fy(t) = cos(t)
fx'(t) = cos(t)
fy'(t) = -sin(t)
过度间隔t = [0,1];我们知道长度应该正好 1.您的LG实现是否会产生正确的值?排除了另一个问题。如果没有,检查你的重量和横纹。它们是否与链接页面中的那些匹配(使用可验证的正确Mathematica程序生成,所以几乎保证是正确的)?你用的是足够的切片吗?将数字提高到10,16,24,32;增加切片数量将显示稳定求和,其中添加更多切片不会在第2,第3,第4,第5等小数点之前更改数字,因为您增加了计数。
您正在测试的曲线是否是有问题的曲线?绘制它们,它们是否有尖点或许多变形?对于LG来说,这将是一个问题,尝试更简单的曲线,看看你回来的价值是否至少是正确的。
最后,检查您的类型:您使用的是精度最高的数据类型吗?当使用合理数量的切片进行LG时,32位浮点数将会出现神秘消失的FPU以及我们需要使用的值的奇妙舍入误差。