【问题标题】:How to create bezier curves for an arc with different start and end tangent slopes如何为具有不同起点和终点切线斜率的弧创建贝塞尔曲线
【发布时间】:2020-08-22 10:18:35
【问题描述】:

我已经坚持了一个星期,现在我似乎无法解决它。

我有一条弧线,当弧线平坦时,我可以很容易地将其转换为一系列贝塞尔曲线:

但是当弧是螺旋线并且端切线具有不同的斜率时,我正在努力找出如何找到贝塞尔曲线。

这是我目前所了解的:

如您所见,每条贝塞尔曲线都有不在正确平面上的控制点,并且由于我无法工作,因此未考虑完整弧线的起点和终点切线(第二张图像中的红色矢量)知道怎么做。

要从弧中找到贝塞尔切片的平面版本,我有这段代码,它肯定适用于平面弧:

    // from https://pomax.github.io/bezierinfo/#circles_cubic
    public CubicBezier ConvertArc(Vector3 origin, float radius, Vector3 from, Vector3 to, float angle)
    {
        var c = Math.Tan(angle * Mathf.Deg2Rad / 4f) * 4 / 3f * radius;

        var c1 = from + (from - origin).Perp().normalized * c;
        var c2 = to - (to - origin).Perp().normalized * c;
        return new CubicBezier(from, c1, c2, to);
    }

这是我当前创建每个贝塞尔切割的代码:

        //cut the arc in to bezier curves up to 90 degrees max
        float cuts = _arc.totalAngle / 90f;
        for (int i = 0; i < cuts; i++)
        {
            float t = i / cuts;
            float t2 = (i + 1) / cuts;

            Arc slice = new Arc(_arc,_arc.Point(t),_arc.Point(t2));

            //this function below is the issue, it needs start and end tangent for the slice, 
            //but i also don't know how to find the tangents at each slice for the whole arc
            //relating the start and end tangents of the entire arc
            //see above snippet for function code
            var cb = ConvertArc(slice.origin, slice.radius, slice.a, slice.b, slice.totalAngle);
            cb.DebugDraw(Color.yellow);
        }

希望有人可以帮助解释解决如何正确找到控制点以匹配切线的逻辑,已经浪费了一周的时间。

这是用 C# 编写的,但我认为语言无关紧要,无论语言如何,数学都是数学。

我希望结果如何尊重端切线斜率的视觉效果(虽然画得不好):

【问题讨论】:

  • 看看这个How can i produce multi point linear interpolation? 尤其是链接Interpolation cubic vs. Bezier cubic 你可以直接采样你的螺旋点并通过那里的方程将它们转换为BEZIER
  • 大多数示例都是 2d 的,这让我很困惑如何在最终端点的两条切线之间进行插值,我整个星期都很累,例如在端切线斜率之间进行插值,但这没有用。我不确定自己做错了什么。
  • 你不需要插值任何东西......你只需设置一些点,插值三次将为你制作平滑的路径......开始结束切线方向由第一个和最后一个控制点决定路径...移植到任何维度都不是问题,因为等式是相同的。你想要一个简单的例子吗?但在 C++ 中,我不在 C# 中编写代码
  • 例子很好,你可以使用pseduo代码,只要我理解它的逻辑我可以解释它尝试并理解它

标签: c# math geometry bezier curve


【解决方案1】:

问题在于 Bezier 控制点不如插值三次方直观。所以我们可以使用这些来代替,然后将它们的控制点转换为贝塞尔曲线,以使事情变得更容易。

  1. 只需创建路径上的点列表

    所有这些都直接在路径上,曲线的连续性由插值三次方程本身保证,因此不需要调整......

    确保你有足够的分数......例如,对于完整的圆圈,至少需要 8 个点,螺母 16 更好......

  2. 将路径点转换为贝塞尔三次控制点

    所以只需在路径上选择 4 个后续点并使用以下方法将它们转换为贝塞尔控制点:

    为了确保连续性,应该从下一个点开始下一个贝塞尔曲线...所以如果我们有点 p0,p1,p2,p3,p4,p5... 那么我们从 (p0,p1,p2,p3) , (p1,p2,p3,p4) 创建贝塞尔曲线, ... 等等。第一个点p0 确定起始方向,最后一个点确定结束方向。如果您希望您的路径开始/结束那些只需复制它们...

这里有一个 C++ 中未优化且粗略的小例子:

//---------------------------------------------------------------------------
List<double> it4;   // interpolation cubic control points
List<double> bz4;   // bezier cubic control points
//---------------------------------------------------------------------------
void generate()
    {
    int i,j,n;
    double x,y,z,a,a0,a1,z0,z1,da,dz,r;
    const double deg=M_PI/180.0;
    const double rad=180.0/M_PI;

    // generate some helix path points
    n=32;                           // number of points along path
    r=0.75;                         // radius
    z0=0.0; z1=0.5;                 // height range
    a0=-25.0*deg; a1=+720.0*deg;    // angle range
    da=(a1-a0)/double(n);
    dz=(z1-z0)/double(n);
    it4.num=0;  // clear list of points
    for (z=z0,a=a0,i=0;i<n;i++,a+=da,z+=dz)
        {
        // 3D point on helix
        x=r*cos(a);
        y=r*sin(a);
        // add it to the list
        it4.add(x);
        it4.add(y);
        it4.add(z);
        }

    // convert it4 into bz4 control points
    bz4.num=0;  // clear list of points
    for (i=0;i<=it4.num-12;i+=3)
        {
        const double m=1.0/6.0;
        double x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3;
        double X0,Y0,Z0,X1,Y1,Z1,X2,Y2,Z2,X3,Y3,Z3;
        j=i;
        X0=it4[j]; j++; Y0=it4[j]; j++; Z0=it4[j]; j++;
        X1=it4[j]; j++; Y1=it4[j]; j++; Z1=it4[j]; j++;
        X2=it4[j]; j++; Y2=it4[j]; j++; Z2=it4[j]; j++;
        X3=it4[j]; j++; Y3=it4[j]; j++; Z3=it4[j]; j++;
        x0 = X1;           y0 = Y1;           z0 = Z1;
        x1 = X1-(X0-X2)*m; y1 = Y1-(Y0-Y2)*m; z1 = Z1-(Z0-Z2)*m;
        x2 = X2+(X1-X3)*m; y2 = Y2+(Y1-Y3)*m; z2 = Z2+(Z1-Z3)*m;
        x3 = X2;           y3 = Y2;           z3 = Z2;
        bz4.add(x0); bz4.add(y0); bz4.add(z0);
        bz4.add(x1); bz4.add(y1); bz4.add(z1);
        bz4.add(x2); bz4.add(y2); bz4.add(z2);
        bz4.add(x3); bz4.add(y3); bz4.add(z3);
        }
    }
//---------------------------------------------------------------------------

在 VCL/GL/C++ 中进行简单渲染

//---------------------------------------------------------------------------
void gl_draw()
    {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    float aspect=float(xs)/float(ys);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.0/aspect,aspect,0.1,100.0);
    glMatrixMode(GL_TEXTURE);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0.0,0.0,-2.5);
    glRotatef(-70.0,1.0,0.0,0.0);
    glRotatef(-130.0,0.0,0.0,1.0);

    glEnable(GL_DEPTH_TEST);
    glDisable(GL_TEXTURE_2D);

    int i,j;
    // render axises
    glBegin(GL_LINES);
    glColor3f(1.0,0.0,0.0); glVertex3d(1.0,0.0,0.0); glVertex3d(0.0,0.0,0.0);
    glColor3f(0.0,1.0,0.0); glVertex3d(0.0,1.0,0.0); glVertex3d(0.0,0.0,0.0);
    glColor3f(0.0,0.0,1.0); glVertex3d(0.0,0.0,1.0); glVertex3d(0.0,0.0,0.0);
    glEnd();


    // render it4 control points (aqua)
    glColor3f(0.0,1.0,1.0);
    glPointSize(8);
    glBegin(GL_POINTS);
    for (i=0;i<it4.num;i+=3) glVertex3dv(it4.dat+i);
    glEnd();
    glPointSize(1);

    // render bz4 control points (magenta)
    glColor3f(1.0,0.0,1.0);
    glPointSize(4);
    glBegin(GL_POINTS);
    for (i=0;i<bz4.num;i+=3) glVertex3dv(bz4.dat+i);
    glEnd();
    glPointSize(1);

    // render bz4 path (yellow)
    double t,tt,ttt,cx[4],cy[4],cz[4],x,y,z;
    double x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3;
    glColor3f(1.0,1.0,0.0);
    glLineWidth(2);
    for (i=0;i<=bz4.num-12;i+=12)
        {
        j=i;
        x0=bz4[j]; j++; y0=bz4[j]; j++; z0=bz4[j]; j++;
        x1=bz4[j]; j++; y1=bz4[j]; j++; z1=bz4[j]; j++;
        x2=bz4[j]; j++; y2=bz4[j]; j++; z2=bz4[j]; j++;
        x3=bz4[j]; j++; y3=bz4[j]; j++; z3=bz4[j]; j++;
        cx[0]=                            (    x0);
        cx[1]=                   (3.0*x1)-(3.0*x0);
        cx[2]=          (3.0*x2)-(6.0*x1)+(3.0*x0);
        cx[3]= (    x3)-(3.0*x2)+(3.0*x1)-(    x0);
        cy[0]=                            (    y0);
        cy[1]=                   (3.0*y1)-(3.0*y0);
        cy[2]=          (3.0*y2)-(6.0*y1)+(3.0*y0);
        cy[3]= (    y3)-(3.0*y2)+(3.0*y1)-(    y0);
        cz[0]=                            (    z0);
        cz[1]=                   (3.0*z1)-(3.0*z0);
        cz[2]=          (3.0*z2)-(6.0*z1)+(3.0*z0);
        cz[3]= (    z3)-(3.0*z2)+(3.0*z1)-(    z0);
        glBegin(GL_LINE_STRIP);
        for (t=0.0,j=0;j<20;j++,t+=0.05)
            {
            tt=t*t; ttt=tt*t;
            x=cx[0]+cx[1]*t+cx[2]*tt+cx[3]*ttt;
            y=cy[0]+cy[1]*t+cy[2]*tt+cy[3]*ttt;
            z=cz[0]+cz[1]*t+cz[2]*tt+cz[3]*ttt;
            glVertex3d(x,y,z);
            }
        glEnd();
        }
    glLineWidth(1);

    glFlush();
    SwapBuffers(hdc);
    }
//---------------------------------------------------------------------------

我也使用了我的动态列表模板,所以:


List&lt;double&gt; xxx;double xxx[]; 相同
xxx.add(5);5 添加到列表末尾
xxx[7]访问数组元素(安全)
xxx.dat[7]访问数组元素(不安全但快速直接访问)
xxx.num是数组实际使用的大小
xxx.reset() 清除数组并设置xxx.num=0
xxx.allocate(100)100 项目预分配空间

只是为了确保代码是可理解的。

和预览:

当您想要编辑路径时,最好控制插值三次控制点而不是贝塞尔曲线,因为您通过艰难的方式了解到这些控制点并不那么直观且易于操作以实现所需的输出。

[Edit1] 输入点更符合您的形状

当您最终提供所需形状的图像时……您只需沿路径采样一些点并将其转换为贝塞尔曲线。所以唯一改变的是输入点:

void generate()
    {
    int i,j,n;
    double x,y,z,a,a0,a1,b,b0,b1,z0,dz,r,t;
    const double deg=M_PI/180.0;
    const double rad=180.0/M_PI;

    // generate some helix path points
    n=32;                           // number of points along path
    r=0.75;                         // curve radius
    z0=0.0;                         // mid height
    dz=0.1;                         // height amplitude
    a0=180.0*deg; a1=   0.0*deg;    // angle range
    b0= 30.0*deg; b1=+330.0*deg;    // angle range
    it4.num=0;  // clear list of points
    for (i=0;i<n;i++)
        {
        // parameters
        t=double(i)/double(n-1);
        a=a0+(a1-a0)*t;
        b=b0+(b1-b0)*t;
        // curve
        x=r*cos(a);
        y=r*sin(a);
        // height
        z=z0+dz*sin(b);
        // add it to the list
        it4.add(x);
        it4.add(y);
        it4.add(z);
        }

    // convert it4 into bz4 control points
    bz4.num=0;  // clear list of points
    for (i=0;i<=it4.num-12;i+=3)
        {
        const double m=1.0/6.0;
        double x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3;
        double X0,Y0,Z0,X1,Y1,Z1,X2,Y2,Z2,X3,Y3,Z3;
        j=i;
        X0=it4[j]; j++; Y0=it4[j]; j++; Z0=it4[j]; j++;
        X1=it4[j]; j++; Y1=it4[j]; j++; Z1=it4[j]; j++;
        X2=it4[j]; j++; Y2=it4[j]; j++; Z2=it4[j]; j++;
        X3=it4[j]; j++; Y3=it4[j]; j++; Z3=it4[j]; j++;
        x0 = X1;           y0 = Y1;           z0 = Z1;
        x1 = X1-(X0-X2)*m; y1 = Y1-(Y0-Y2)*m; z1 = Z1-(Z0-Z2)*m;
        x2 = X2+(X1-X3)*m; y2 = Y2+(Y1-Y3)*m; z2 = Z2+(Z1-Z3)*m;
        x3 = X2;           y3 = Y2;           z3 = Z2;
        bz4.add(x0); bz4.add(y0); bz4.add(z0);
        bz4.add(x1); bz4.add(y1); bz4.add(z1);
        bz4.add(x2); bz4.add(y2); bz4.add(z2);
        bz4.add(x3); bz4.add(y3); bz4.add(z3);
        }
    }

这里预览:

并以 N=8 点预览:

我只是用参数a 将曲线和高度分成圆形路径,用参数b 将正弦曲线分离。如您所见,无论输入点如何变化,转换代码都是相同的......

【讨论】:

  • 你确定这也是螺旋弧每一端不同斜率的因素吗? (我的圆弧不超过 360 度)但起始切线可能向上倾斜,结束切线可能向下倾斜。您的解决方案似乎暗示整个曲线的斜率恒定?
  • 对我来说,你如何找到三次贝塞尔曲线的控制点也不是很明显,而是你似乎是沿着这条线本身进行插值。我对这种方法不太熟悉,因为我的整个设置涉及使用控制点来保持三次样条之间的切线。
  • 也许这张图可能有助于解释我所说的螺旋弧两端的斜率是什么意思:imgur.com/jNYBohA 因为它们在这张图中没有相同的方向,例如它们都向下倾斜.
  • @WDUK 1. 你得到的形状是什么我只是使用简单的螺旋线,如果你想要不同的话,你会做不同的。无论如何你都在手动编辑这些点,所以不用担心 2. 从 it4 到 bz4 的计算是通过代数比较三次多项式系数并从 it4 控制点 (X0,Y0,...) 而不是 bz4 控制点 (x0,y0,...) 导出 bz4 系数来完成的)。那是我的答案中第一个链接底部的代码格式的东西。它是纯代数......你可以像我一样使用最终方程。
  • @WDUK 我添加了具有更好匹配的edit1。顺便说一句,如果您有地形图,最好使用 2D 轨迹/路径/道路,无论形状如何,并通过直接将它们投影/映射到表面上来将其转换为 3D it4 点...
【解决方案2】:

您有一段 3d 曲线,在端点处有已知切线,并希望建立贝塞尔近似。

贝塞尔曲线的内部控制点将位于与切向量共线的向量上。但是你需要知道它们的长度。

圆弧的近似方法选择这些向量的长度以提供与弧的中点重合的中间贝塞尔点。您可以在此处应用相同的方法。写

P1 = P0 + T0 * L
P2 = P3 - T3 * L

用 t=1/2 代入贝塞尔方程,P = 曲线的中间并找到未知 L。对所有三个分量进行此操作并获得一些平均值,从而提供相当好的误差(也许可以进行一些优化)。

如果曲线高度不对称 - 有人可能会尝试对两条切线使用不同的长度。

【讨论】:

  • 如果螺旋线有不同的端切线斜率,你确定它在 z 上是线性的吗? (红色向量)。当我尝试在斜坡上直线滑行时,它完全被打破了。等我回家说明我的意思,我会告诉你结果。
  • 完美螺旋是线性的。似乎你的曲线不是,在这种情况下,我的回答是不正确的,我会在你阅读后将其删除。将曲线细分为段,并使用基于切线的控制点使用 Bezier 逼近每个段
  • 您的建议基本上就是我所说的我正在努力解决我的问题。在考虑切线的情况下,我无法计算出控制点的位置。
  • 控件与圆弧相切,但没有正确的斜率。如果您在我的问题中看到我的第二张图片,弧线末端的两条红线是弧线两端的切线,我可以上下倾斜它们,但我无法计算匹配的贝塞尔曲线系列切线中的这些间距为贝塞尔曲线。
  • 对于曲线段,您有端点和切线(单位向量)。要在末端提供正确的 Bezier 方向,控制点必须位于切线上,您不能选择任意点。但是端点-控制点的距离是未知的,我们必须拟合它们以提供良好的近似值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-27
  • 1970-01-01
  • 2011-05-04
  • 2011-05-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多