6

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

Bad render

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

A lot smoother

It still isn't perfect though :/

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
Razacx
  • 144
  • 1
  • 9
  • what's the link for the second image? we can edit it in. That said, it's hard to tell what you think "perfect" should be. When dealing with things like this, never ever round until the very last moment. And if your drawing surface can do subpixel drawing, don't even round at the end - let the canvas handle the placement. (And usually, instead of drawing individual points, you track the "current" and "previous" point, and instead draw lines between those) – Mike 'Pomax' Kamermans Nov 29 '15 at 01:15
  • Looks fine to me. You arguably could post your **edit** as an answer below. :) –  Nov 29 '15 at 01:49
  • 1
    Have you looked into using BasicEffect instead of SpriteBatch? I think that would be a better way to go for this type of thing. – craftworkgames Nov 29 '15 at 05:08
  • That's used for 3D rendering isn't it? But, yeah this entire project was basic proof of concept stuff. So I guess it's good the way it is now. – Razacx Nov 29 '15 at 14:05

2 Answers2

8

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:

link

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

Wireframe of curve

Razacx
  • 144
  • 1
  • 9
  • 1
    Well done. That's interesting to note about BasicEffect and anti-aliasing. +1 –  Nov 30 '15 at 02:40
  • Nice work. It was just a guess on my behalf but I'm glad you got it working. If you could post the relevant code here for others that'd be great. – craftworkgames Nov 30 '15 at 05:11
0

So, I needed something like this working with SpriteBatch, so I poked around at the original code a bit (with the Point -> Vector2 and rounding changes.

If you render every other segment as a different color, and with a large enough width and low enough steps, you can see why it resulted in jagged lines with larger values of width. It turns out the lines go past where they should end!

Lines going past their end:

Lines going past their end

This is because the DrawLine function adds width to length of the segment. However, without this, you see a bunch of disconnected segments for anything that actually curves.

Lines being disconnected:

Lines being disconnected

There's probably some math you can do to get the appropriate value to add here, based on the angle of the connecting points. I don't know math well enough for that, so I'm just using a fixed value for them all. (10 seems to be the sweet spot for the image I posted, although it isn't perfect due to the low step count.)

(The following is DrawLine adjusted with the width being added, to using a constant instead.)

// 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);
}
Ramil Aliyev 007
  • 4,437
  • 2
  • 31
  • 47