Drawing Bezier curves in MonoGame (XNA) produces scratchy lines

≯℡__Kan透↙ 提交于 2020-01-02 05:18:35

问题


I recently got into using MonoGame, and I love the library. However, I seem to be having some issues with drawing bezier curves

The result that my code produces looks something like this

Look bad, no? The lines aren't smooth at all.

Let me show you some of the code:

//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);
}

I managed to make the line a bit smoother by adding some manual Math.Round() calls to my interpolatedPoint method

//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));   
}

This produces the following result:

I had to remove one picture since Stackoverflow doesn't let me use more than two links

Are there any ways I can get this curve to be absolutely smooth? Perhaps there is a problem with the DrawLine method?

Thanks in advance.

EDIT:

Okay, I managed to make the curve look a lot better by doing all the calculations with Vector2Ds and only converting it to a Point at the moment that it needs to be drawn

It still isn't perfect though :/


回答1:


As Mike 'Pomax' Kamermans said, it seems to have been a problem with the 2D surface not allowing subpixel drawing and thus causing rounding issues

Following craftworkgames' advice, I adapted the algorithm to draw the curve in 3D using a BasicEffect. This also allows for antialiasing, which smoothes out the curve a lot.

The result is the following:

A lot better!

Thank you very much for the helpful advice!

EDIT:

Here is the code I used for doing this.

I would also like to add that this webpage (http://gamedevelopment.tutsplus.com/tutorials/create-a-glowing-flowing-lava-river-using-bezier-curves-and-shaders--gamedev-919) helped me a lot while writing this code.

Also, please note that some of the names I used for defining the methods might not really make sense or can be confusing. This was something I quickly put together on an evening.

//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);
}

Also, this image may be able to show what the code does a little bit better



来源:https://stackoverflow.com/questions/33977226/drawing-bezier-curves-in-monogame-xna-produces-scratchy-lines

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!