【问题标题】:Drawing Bezier curves in MonoGame (XNA) produces scratchy lines在 MonoGame (XNA) 中绘制贝塞尔曲线会产生粗糙的线条
【发布时间】:2015-11-28 22:36:30
【问题描述】:

我最近开始使用 MonoGame,我喜欢这个库。 但是,我似乎在绘制贝塞尔曲线时遇到了一些问题

我的代码产生的结果看起来像这样

看起来很糟糕,不是吗? 线条一点都不流畅。

让我给你看一些代码:

//This is what I call to get all points between which to draw.
public static List<Point> computeCurvePoints(int steps)
{   
    List<Point> curvePoints = new List<Point>();
    for (float x = 0; x < 1; x += 1 / (float)steps)
    {
        curvePoints.Add(getBezierPointRecursive(x, pointsQ));
    }   
    return curvePoints; 
}

//Calculates a point on the bezier curve based on the timeStep.
private static Point getBezierPointRecursive(float timeStep, Point[] ps)
{   
    if (ps.Length > 2)
    {
        List<Point> newPoints = new List<Point>();
        for (int x = 0; x < ps.Length-1; x++)
        {
            newPoints.Add(interpolatedPoint(ps[x], ps[x + 1], timeStep));
        }
        return getBezierPointRecursive(timeStep, newPoints.ToArray());
    }
    else
    {
        return interpolatedPoint(ps[0], ps[1], timeStep);
    }
}

//Gets the linearly interpolated point at t between two given points (without manual rounding).
//Bad results!
private static Point interpolatedPoint(Point p1, Point p2, float t)
{
    Vector2 roundedVector = (Vector2.Multiply(p2.ToVector2() - p1.ToVector2(), t) + p1.ToVector2());          
    return new Point((int)roundedVector.X, (int)roundedVector.Y);   
}

//Method used to draw a line between two points.
public static void DrawLine(this SpriteBatch spriteBatch, Texture2D pixel, Vector2 begin, Vector2 end, Color color, int width = 1)
{
    Rectangle r = new Rectangle((int)begin.X, (int)begin.Y, (int)(end - begin).Length() + width, width);
    Vector2 v = Vector2.Normalize(begin - end);
    float angle = (float)Math.Acos(Vector2.Dot(v, -Vector2.UnitX));
    if (begin.Y > end.Y) angle = MathHelper.TwoPi - angle;
    spriteBatch.Draw(pixel, r, null, color, angle, Vector2.Zero, SpriteEffects.None, 0);
}

//DrawLine() is called as following. "pixel" is just a Texture2D with a single black pixel.
protected override void Draw(GameTime gameTime)
{
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();          
            for(int x = 0; x < curvePoints.Count-1; x++)
            {
                DrawExtenstion.DrawLine(spriteBatch, pixel, curvePoints[x].ToVector2(), curvePoints[x + 1].ToVector2(), Color.Black, 2);
            }
            spriteBatch.End();

            base.Draw(gameTime);
}

通过向 interpolatedPoint 方法添加一些手动 Math.Round() 调用,我设法使线条更平滑

//Gets the linearly interpolated point at t between two given points (with manual rounding).
//Better results (but still not good).
private static Point interpolatedPoint(Point p1, Point p2, float t)
{
    Vector2 roundedVector = (Vector2.Multiply(p2.ToVector2() - p1.ToVector2(), t) + p1.ToVector2());          
    return new Point((int)Math.Round(roundedVector.X), (int)Math.Round(roundedVector.Y));   
}

这会产生以下结果:

我不得不删除一张图片,因为 Stackoverflow 不允许我使用两个以上的链接

有什么方法可以让这条曲线绝对平滑? 可能是 DrawLine 方法有问题?

提前致谢。

编辑:

好的,通过使用 Vector2Ds 进行所有计算并仅在需要绘制时将其转换为点,我设法使曲线看起来更好

它仍然不完美:/

【问题讨论】:

  • 第二张图片的链接是什么?我们可以编辑它。也就是说,很难说出你认为“完美”应该是什么。在处理这样的事情时,永远不要直到最后一刻才回过神来。而且,如果您的绘图表面可以进行亚像素绘图,甚至不要在最后进行圆形 - 让画布处理放置。 (通常,您不是绘制单个点,而是跟踪“当前”和“上一个”点,而是在它们之间画线)
  • 对我来说看起来不错。您可以在下面发布您的 edit 作为答案。 :)
  • 您是否考虑过使用 BasicEffect 代替 SpriteBatch?我认为这将是处理这类事情的更好方法。
  • 那不是用来做3D渲染的吗?但是,是的,整个项目都是基本的概念证明。所以我想现在这样很好。

标签: c# xna rounding monogame bezier


【解决方案1】:

正如 Mike 'Pomax' Kamermans 所说, 2D 表面似乎存在问题,不允许子像素绘制,从而导致舍入问题

按照craftworkgames 的建议,我调整了算法以使用BasicEffect 绘制3D 曲线。这还允许抗锯齿,从而使曲线平滑很多。

结果如下:

好多了!

非常感谢您的有用建议!

编辑:

这是我用于执行此操作的代码。

我还想补充一点,这个网页 (http://gamedevelopment.tutsplus.com/tutorials/create-a-glowing-flowing-lava-river-using-bezier-curves-and-shaders--gamedev-919) 在编写这段代码时帮助了我很多。

另外,请注意,我用于定义方法的一些名称可能没有真正的意义或可能会造成混淆。这是我在一个晚上很快整理好的。

//Used for generating the mesh for the curve
//First object is vertex data, second is indices (both as arrays)
public static object[] computeCurve3D(int steps)
{
    List<VertexPositionTexture> path = new List<VertexPositionTexture>();
    List<int> indices = new List<int>();

    List<Vector2> curvePoints = new List<Vector2>();
    for (float x = 0; x < 1; x += 1 / (float)steps)
    {
        curvePoints.Add(getBezierPointRecursive(x, points3D));
    }

    float curveWidth = 0.003f;

    for(int x = 0; x < curvePoints.Count; x++)
    {
        Vector2 normal;

        if(x == 0)
        {
            //First point, Take normal from first line segment
            normal = getNormalizedVector(getLineNormal(curvePoints[x+1] - curvePoints[x]));
        }
        else if (x + 1 == curvePoints.Count)
        {
            //Last point, take normal from last line segment
            normal = getNormalizedVector(getLineNormal(curvePoints[x] - curvePoints[x-1]));
        } else
        {
            //Middle point, interpolate normals from adjacent line segments
            normal = getNormalizedVertexNormal(getLineNormal(curvePoints[x] - curvePoints[x - 1]), getLineNormal(curvePoints[x + 1] - curvePoints[x]));
        }

        path.Add(new VertexPositionTexture(new Vector3(curvePoints[x] + normal * curveWidth, 0), new Vector2()));
        path.Add(new VertexPositionTexture(new Vector3(curvePoints[x] + normal * -curveWidth, 0), new Vector2()));
    } 

    for(int x = 0; x < curvePoints.Count-1; x++)
    {
        indices.Add(2 * x + 0);
        indices.Add(2 * x + 1);
        indices.Add(2 * x + 2);

        indices.Add(2 * x + 1);
        indices.Add(2 * x + 3);
        indices.Add(2 * x + 2);
    }

    return new object[] {
        path.ToArray(),
        indices.ToArray()
    };
}

//Recursive algorithm for getting the bezier curve points 
private static Vector2 getBezierPointRecursive(float timeStep, Vector2[] ps)
{

    if (ps.Length > 2)
    {
        List<Vector2> newPoints = new List<Vector2>();
        for (int x = 0; x < ps.Length - 1; x++)
        {
            newPoints.Add(interpolatedPoint(ps[x], ps[x + 1], timeStep));
        }
        return getBezierPointRecursive(timeStep, newPoints.ToArray());
    }
    else
    {
        return interpolatedPoint(ps[0], ps[1], timeStep);
    }
}

//Gets the interpolated Vector2 based on t
private static Vector2 interpolatedPoint(Vector2 p1, Vector2 p2, float t)
{
    return Vector2.Multiply(p2 - p1, t) + p1;
}

//Gets the normalized normal of a vertex, given two adjacent normals (2D)
private static Vector2 getNormalizedVertexNormal(Vector2 v1, Vector2 v2) //v1 and v2 are normals
{
    return getNormalizedVector(v1 + v2);
}

//Normalizes the given Vector2
private static Vector2 getNormalizedVector(Vector2 v)
{
    Vector2 temp = new Vector2(v.X, v.Y);
    v.Normalize();
    return v;
}

//Gets the normal of a given Vector2
private static Vector2 getLineNormal(Vector2 v)
{
    Vector2 normal = new Vector2(v.Y, -v.X);            
    return normal;
}

//Drawing method in main Game class
//curveData is a private object[] that is initialized in the constructor (by calling computeCurve3D)
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    var camPos = new Vector3(0, 0, 0.1f);
    var camLookAtVector = Vector3.Forward;
    var camUpVector = Vector3.Up;

    effect.View = Matrix.CreateLookAt(camPos, camLookAtVector, camUpVector);
    float aspectRatio = graphics.PreferredBackBufferWidth / (float)graphics.PreferredBackBufferHeight;
    float fieldOfView = MathHelper.PiOver4;
    float nearClip = 0.1f;
    float farClip = 200f;

    //Orthogonal
    effect.Projection = Matrix.CreateOrthographic(480 * aspectRatio, 480, nearClip, farClip);

    foreach (var pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        effect.World = Matrix.CreateScale(200);

        graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, 
            (VertexPositionTexture[])curveData[0],
            0,
            ((VertexPositionTexture[])curveData[0]).Length,
            (int[])curveData[1],
            0,
            ((int[])curveData[1]).Length/3);            
    }

    base.Draw(gameTime);
}

此外,这张图片可能会更好地显示代码的功能

【讨论】:

  • 干得好。有趣的是要注意 BasicEffect 和抗锯齿。 +1
  • 干得好。这只是代表我的猜测,但我很高兴你让它工作。如果您可以在此处为其他人发布相关代码,那就太好了。
【解决方案2】:

所以,我需要像这样使用 SpriteBatch 的东西,所以我稍微研究了一下原始代码(使用 Point -> Vector2 和舍入更改。

如果您将所有其他线段渲染为不同的颜色,并且具有足够大的宽度和足够低的步长,您就会明白为什么它会导致具有较大值 width 的锯齿线。事实证明,这些线条越过了它们应该结束的地方!

行结束:

这是因为DrawLine 函数将width 添加到段的长度。但是,如果没有这个,你会看到一堆不连贯的片段,这些片段实际上是弯曲的。

正在断开的线路:

根据连接点的角度,您可能需要进行一些数学运算才能在此处添加适当的值。我对数学不太了解,所以我只是为它们都使用了一个固定值。 (10 似乎是我发布的图片的最佳位置,尽管由于步数少,它并不完美。)

(以下是DrawLine 调整了添加的宽度,改为使用常量。)

// Method used to draw a line between two points.
public static void DrawLine(this SpriteBatch spriteBatch, Texture2D pixel, Vector2 begin, Vector2 end, Color color, int width = 1)
{
    Rectangle r = new Rectangle((int)begin.X, (int)begin.Y, (int)(end - begin).Length() + 10, width);
    Vector2 v = Vector2.Normalize(begin - end);
    float angle = (float)Math.Acos(Vector2.Dot(v, -Vector2.UnitX));
    if (begin.Y > end.Y) angle = MathHelper.TwoPi - angle;
    spriteBatch.Draw(pixel, r, null, color, angle, Vector2.Zero, SpriteEffects.None, 0);
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-21
    • 2011-02-26
    • 1970-01-01
    • 2014-09-29
    • 1970-01-01
    相关资源
    最近更新 更多