How to create basic shapes without getting sharp edges ?

I’m working on a game with the Monogame engine and I can’t find a proper way to generate basic shapes with smooth edges, even after trying to dug all the Internet…

Currently, I’m generating all my shapes at runtime by creating and setting some vertices with some additionnal informations (color, texture, …). I’m doing this (instead of importing some sprites, for instance) because I want the game to be as independant as possible from screen specs (resolution, refresh rate, …), while being able to manipulate the shapes geometry if I need to.

Each shape has its own BasicEffect which is used to draw it every time a Draw() is called.
The problem is that when rendered, these shapes have “sharp” edges, like below :

Sharp edges

Instead of what I want, which is :

Smooth edges

This last screenshot was taken after I tried MSAA in my game (MultiSampleCount at maximum level) :

Graphics.GraphicsProfile = GraphicsProfile.HiDef; Graphics.PreferMultiSampling = true;

But here is the new problem : on my Intel Integrated Graphic Card, when there are more shapes (with around 1000 vertices), FPS start to drop at ~40fps, which is not acceptable to me for a game with simple shapes like these. If I decrease the MultiSampleCount, FPS are better, but the result is not that good.

I looked into generating simple shapes textures at runtime and then drawing them with some SamplerState.LinearWrap to smooth edges, but it seems like a “wobbly” solution to me, because I’m then losing the possibility to tweak the geometry of my shapes if needed.

So my question is :

Is there a way to generate “smooth” simple but flexible shapes to create a game which is playable at minimum 144fps even on Integrated Graphic Cards ? Is there a “cheap” antialiasing algorithm that can run properly on low-end graphic cards ? Or do I already have the answer (turning on MSAA) but the FPS drop comes for another problem in my code ?


Example of drawing 100 squares with MSAA on (~90fps on my Intel Integrated Graphic Card) :

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Patapits.Framework.RawTests
{
    public class Square
    {
        VertexBuffer _vertexBuffer;
        IndexBuffer _indexBufer;

        BasicEffect _basicEffect;
        private Matrix _view;
        private Matrix _projection;

        private Vector2 _position;

        public Square(GraphicsDevice device, int screenWidth, int screenHeight)
        {
            Generate(device, screenWidth, screenHeight);
        }

        public void MoveTo(Vector2 position)
        {
            _position = position;
        }
        
        public void Generate(GraphicsDevice device, int screenWidth, int screenHeight)
        {
            VertexPositionColor[] vertices = new VertexPositionColor[4];

            vertices[0] = new VertexPositionColor(new Vector3(-0.5f, 0.5f, 0.0f), Color.Red);
            vertices[1] = new VertexPositionColor(new Vector3(0.5f, 0.5f, 0.0f), Color.Red);
            vertices[2] = new VertexPositionColor(new Vector3(0.5f, -0.5f, 0.0f), Color.Red);
            vertices[3] = new VertexPositionColor(new Vector3(-0.5f, -0.5f, 0.0f), Color.Red);

            short[] indexes = new short[6];

            indexes[0] = 0;
            indexes[1] = 1;
            indexes[2] = 2;
            indexes[3] = 0;
            indexes[4] = 2;
            indexes[5] = 3;

            _vertexBuffer = new VertexBuffer(device, typeof(VertexPositionColor), vertices.Length,
                BufferUsage.WriteOnly);
            _vertexBuffer.SetData(vertices);
            _indexBufer = new IndexBuffer(device, typeof(short), indexes.Length, BufferUsage.WriteOnly);
            _indexBufer.SetData(indexes);

            _basicEffect = new BasicEffect(device);

            _view = Matrix.CreateLookAt(new Vector3(0, 0, 1), Vector3.Zero, Vector3.Up);
            _projection = Matrix.CreateOrthographic(screenWidth, screenHeight, 0, 1);
        }

        public void Draw(GraphicsDevice device)
        {
            _basicEffect.World = Matrix.CreateRotationZ(MathHelper.Pi / 3) *
                                 Matrix.CreateScale(100.0f, 100.0f, 1.0f) *
                                 Matrix.CreateTranslation(new Vector3(_position, 0.0f));
            _basicEffect.View = _view;
            _basicEffect.Projection = _projection;
            _basicEffect.VertexColorEnabled = true;

            device.BlendState = BlendState.AlphaBlend;
            device.RasterizerState = RasterizerState.CullNone;
            device.DepthStencilState = DepthStencilState.Default;

            device.SetVertexBuffer(_vertexBuffer);
            device.Indices = _indexBufer;

            foreach (EffectPass pass in _basicEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _indexBufer.IndexCount / 3);
            }
        }
    }
    
    public class Game1 : Game
    {
        GraphicsDeviceManager _graphics;

        private Square[] _squares;

        public Game1()
        {
            _graphics = new GraphicsDeviceManager(this) {SynchronizeWithVerticalRetrace = false};
            _graphics.PreferredBackBufferWidth = 1500;
            _graphics.PreferredBackBufferHeight = 500;
            
            IsFixedTimeStep = false;
            Content.RootDirectory = "Content";

            Window.AllowUserResizing = true;
        }

        protected override void Initialize()
        {
            _graphics.GraphicsProfile = GraphicsProfile.HiDef;
            _graphics.PreferMultiSampling = true;
            _graphics.ApplyChanges();

            base.Initialize();
        }

        protected override void LoadContent()
        {
            int squaresCount = 100;
            _squares = new Square[squaresCount];

            Random rand = new Random();

            for (int i = 0; i < squaresCount; i++)
            {
                _squares[i] = new Square(GraphicsDevice, _graphics.PreferredBackBufferWidth,
                    _graphics.PreferredBackBufferHeight);
                _squares[i].MoveTo(
                    new Vector2(
                        rand.Next(-_graphics.PreferredBackBufferWidth / 2, _graphics.PreferredBackBufferWidth / 2),
                        rand.Next(-_graphics.PreferredBackBufferHeight / 2, _graphics.PreferredBackBufferHeight / 2)));
            }
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.White);

            for (int i = 0; i < _squares.Length; i++)
            {
                _squares[i].Draw(GraphicsDevice);
            }

            base.Draw(gameTime);
        }
    }
}

Updated version with all Squares sharing the same VertexBuffer and IndexBuffer (but still ~90FPS) :

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Patapits.Framework.RawTests
{
    public class Square
    {
        private static VertexBuffer _vertexBuffer;
        private static IndexBuffer _indexBufer;

        BasicEffect _basicEffect;
        private Matrix _view;
        private Matrix _projection;

        private Vector2 _position;

        public Square(GraphicsDevice device, int screenWidth, int screenHeight)
        {
            Generate(device, screenWidth, screenHeight);
        }

        public void MoveTo(Vector2 position)
        {
            _position = position;
        }
        
        public void Generate(GraphicsDevice device, int screenWidth, int screenHeight)
        {
            if (_vertexBuffer == null)
            {
                VertexPositionColor[] vertices = new VertexPositionColor[4];

                vertices[0] = new VertexPositionColor(new Vector3(-0.5f, 0.5f, 0.0f), Color.Red);
                vertices[1] = new VertexPositionColor(new Vector3(0.5f, 0.5f, 0.0f), Color.Red);
                vertices[2] = new VertexPositionColor(new Vector3(0.5f, -0.5f, 0.0f), Color.Red);
                vertices[3] = new VertexPositionColor(new Vector3(-0.5f, -0.5f, 0.0f), Color.Red);

                short[] indexes = new short[6];

                indexes[0] = 0;
                indexes[1] = 1;
                indexes[2] = 2;
                indexes[3] = 0;
                indexes[4] = 2;
                indexes[5] = 3;

                _vertexBuffer = new VertexBuffer(device, typeof(VertexPositionColor), vertices.Length,
                    BufferUsage.WriteOnly);
                _vertexBuffer.SetData(vertices);
                _indexBufer = new IndexBuffer(device, typeof(short), indexes.Length, BufferUsage.WriteOnly);
                _indexBufer.SetData(indexes);
            }

            _basicEffect = new BasicEffect(device);

            _view = Matrix.CreateLookAt(new Vector3(0, 0, 1), Vector3.Zero, Vector3.Up);
            _projection = Matrix.CreateOrthographic(screenWidth, screenHeight, 0, 1);
        }

        public void Draw(GraphicsDevice device)
        {
            _basicEffect.World = Matrix.CreateRotationZ(MathHelper.Pi / 3) *
                                 Matrix.CreateScale(100.0f, 100.0f, 1.0f) *
                                 Matrix.CreateTranslation(new Vector3(_position, 0.0f));
            _basicEffect.View = _view;
            _basicEffect.Projection = _projection;
            _basicEffect.VertexColorEnabled = true;

            device.BlendState = BlendState.AlphaBlend;
            device.RasterizerState = RasterizerState.CullNone;
            device.DepthStencilState = DepthStencilState.Default;

            device.SetVertexBuffer(_vertexBuffer);
            device.Indices = _indexBufer;

            foreach (EffectPass pass in _basicEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _indexBufer.IndexCount / 3);
            }
        }
    }
    
    public class Game1 : Game
    {
        GraphicsDeviceManager _graphics;

        private Square[] _squares;

        public Game1()
        {
            _graphics = new GraphicsDeviceManager(this) {SynchronizeWithVerticalRetrace = false};
            _graphics.PreferredBackBufferWidth = 1500;
            _graphics.PreferredBackBufferHeight = 500;
            
            IsFixedTimeStep = false;
            Content.RootDirectory = "Content";

            Window.AllowUserResizing = true;
        }

        protected override void Initialize()
        {
            _graphics.GraphicsProfile = GraphicsProfile.HiDef;
            _graphics.PreferMultiSampling = true;
            _graphics.ApplyChanges();

            base.Initialize();
        }

        protected override void LoadContent()
        {
            int squaresCount = 100;
            _squares = new Square[squaresCount];

            Random rand = new Random();

            for (int i = 0; i < squaresCount; i++)
            {
                _squares[i] = new Square(GraphicsDevice, _graphics.PreferredBackBufferWidth,
                    _graphics.PreferredBackBufferHeight);
                _squares[i].MoveTo(
                    new Vector2(
                        rand.Next(-_graphics.PreferredBackBufferWidth / 2, _graphics.PreferredBackBufferWidth / 2),
                        rand.Next(-_graphics.PreferredBackBufferHeight / 2, _graphics.PreferredBackBufferHeight / 2)));
            }
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.White);

            for (int i = 0; i < _squares.Length; i++)
            {
                _squares[i].Draw(GraphicsDevice);
            }

            base.Draw(gameTime);
        }
    }
}

Question originally posted here : https://gamedev.stackexchange.com/questions/166814/smoothing-runtime-generated-shapes-edges-with-monogame

I’d say the right solution is definitely to generate the textures at runtime. What kind of tweaks did not work for you that way?

It’s not that it didn’t work, it’s just that I find it odd to generate textures to represent shapes, instead of directly generate and draw these shapes with vertices, and that this will lead to some new problems : what happen to textures when the user resize the game window (because I suppose that the textures will need to be re-generated to match the new screen resolution) ? How should I do if I want to animate only one vertex ? etc. I don’t know, this seems like I will spend a lot of time trying to make things working without any flaws.

In the meantime, I tried some tricks to draw “smooth” shapes, and maybe I have a clue on what can work : do some computation with the alpha of a shape color around its edges. But I can’t find any topic talking about that kind of thing (is that even a thing ?).

There’s a lot of ways to do AA. Depending on what else you’ll put in the game you may be able to get away with FXAA.

The game will only be composed of basic shapes, so FXAA might do the trick. I’ve already looked at this technique, but I thought it was just texture filtering, not “geometry” filtering, I will have a closer look at it then. Are there downsides of using custom shaders in Monogame ? Will the game be no more compatible with some devices for instance ?

And there is still one thing I don’t get : why without MSAA on, FPS are as low as 90FPS when drawing 100 squares on integrated graphic card ? Is this something that’s to be expected on low-end graphic cards ?

Anyway, thanks for your answers, I’m kinda lost at the moment, so I gladly appreciate any help :slight_smile:

Yes, FXAA is a post-process effect, but it uses depth so it doesn’t filter textures on 1 piece of geometry (but that’s not as good as multisampling of course). Also you’d have to artifically add depth if this is a 2D game.

It might be your driver supports a pretty high multisampling count. If you set PreferMultiSampling to true, MonoGame sets it to the highest value supported by your GPU driver. You can override the multi-sampling count by modifying GraphicsDeviceInformation.PresentationParameters.MultiSampleCount in the GraphicsDeviceManager.PreparingDeviceSettings. You might want to try and set it to 2 or 4.

Btw, instead of changing GraphicsDeviceManager properties and calling ApplyChanges, it’d be better to set the properties in your Game1 constructor (and not call ApplyChanges). That way the GraphicsDevice is not created twice.

I can’t find a proper

way to generate basic shapes with smooth edges

The anti aliasing …

It doesn’t seem like you need msaa on for what you are doing. Linear should be ok or even Ansiostropic.

Even point clamp with no aliasing at all doesn’t give jagged edges that bad.
Unless the resolution is super low.

What is the screen resolution that is drawn under ?
What is the texure size and the destination size of these rectangles ?

The problem is most likely else were.

90 fps

It could be a driver issue but i suspect its that the way you are doing it. Is either causing the cpu to gpu pipeline to flood and stall or the gpu is straight bottenecked.

Setting each quad the same up in the buffer for all the quads and also translating each quads position separately is the first red flag.

Is this a separate texture for each rectangle or the same one ?
Did you try this on another computer ?

Even SpriteBatch isn’t anywere near that slow.

What im working on now isn’t optimized with all my debug output is making tons of garbage and is running on my old laptop at over 150 fps with 10k quads 65k + vertices drawn

Im not using drawstring here im using vertices and basic effect.

Yeah, I already tried to change this value, but below MultiSampleCount = 8, the result is not good enough to me. But my statement about getting ~90FPS is without MultiSampling (at 1080p) !

Yes it’s a 2D game, but I already have a Depth on all my objects (using the Z-axis). But I don’t understand : let’s say I have only one shape in my game, FXAA will do nothing on it ? If so, it doesn’t look like something I can use to achieve the effect I’m aiming. And I must point out that I don’t have any texture (yet ?) on my shapes, just plain VertexPositionColor.

You’re conflating AA (which operates on geometry, not textures) and texture filtering. OP mentioned why they prefer a geometry solution.

In the example below :

_graphics.PreferredBackBufferWidth = 1500;
_graphics.PreferredBackBufferHeight = 500;

There’s no texture applied, only a Color (VertexPositionColor). And in the example, the base size of a square is 1 and the scale factor is :

Matrix.CreateScale(100.0f, 100.0f, 1.0f)

Oops, I misread. Wow, that’s really bad. Must be either the hardware is really that old or drivers suck.

It will. FXAA basically checks for edges based on depth and then blurs them a little.

Nope, I mismatched my numbers, sorry :sweat_smile:

So :

  • Drawing 100 squares at 1500x500 with MSAA on : ~90FPS
  • Drawing 100 squares at 1500x500 without MSAA : ~650FPS
  • Drawing 100 squares at 1920x1080 with MSAA on : ~50FPS
  • Drawing 100 squares at 1920x1080 without MSAA : ~200FPS

Is it normal to have “only” 200 FPS at 1080p without MSAA when drawing 100 squares on low-end graphic card (here a Intel UHD Graphics 630) ?

Or it could be me mismatching my numbers, sorry :sweat_smile:

And here is a profiling with these parameters :

Okay, I actually didn’t take a look at your code before… The way you draw the squares is very inefficient. You should not have separate graphics objects for them.
Preferably you’d have only 1 effect, 1 vertex buffer and index buffer so you can draw all your squares in one call to DrawIndexedPrimitives. You can do that by regenerating the vertices every frame for the right position. In fact that’s exactly what SpriteBatch does, so you may want to use just that. Performance will be tons better. If you write your own solution with a custom shader you can do even better, but really I think SpriteBatch will be good enough for this.

Haha, in fact, this is what I was doing before (sending all vertices and indexes in one buffer), but I had some performance issues, and after some research I assumed it was because I was recomputing all vertices every frame, instead of sending them once and using Effect.World on each shape :sweat_smile: I will try your suggestion and see if it improves my game FPS.
Thanks once again for your time answering my questions :slight_smile:

So I tried what seems like an implementation of what you suggested @Jjagg, and I get only around 150FPS :sweat:

Full working example :

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Patapits.Framework.RawTests
{
    public class Square
    {
        private static Vector3[] _vertices;
        private static short[] _indexes;

        private Vector2 _position;

        public Square(GraphicsDevice device, int screenWidth, int screenHeight)
        {
            Generate(device, screenWidth, screenHeight);
        }

        public void MoveTo(Vector2 position)
        {
            _position = position;
        }

        public void Generate(GraphicsDevice device, int screenWidth, int screenHeight)
        {
            if (_vertices == null)
            {
                _vertices = new Vector3[4];

                _vertices[0] = new Vector3(-0.5f, 0.5f, 0.0f);
                _vertices[1] = new Vector3(0.5f, 0.5f, 0.0f);
                _vertices[2] = new Vector3(0.5f, -0.5f, 0.0f);
                _vertices[3] = new Vector3(-0.5f, -0.5f, 0.0f);

                _indexes = new short[6];

                _indexes[0] = 0;
                _indexes[1] = 1;
                _indexes[2] = 2;
                _indexes[3] = 0;
                _indexes[4] = 2;
                _indexes[5] = 3;
            }
        }

        public Matrix GetWorldMatrix()
        {
            return Matrix.CreateRotationZ(MathHelper.Pi / 3) *
                   Matrix.CreateScale(100.0f, 100.0f, 1.0f) *
                   Matrix.CreateTranslation(new Vector3(_position, 0.0f));
        }

        public VertexPositionColor[] GetVertices()
        {
            Matrix worldMatrix = GetWorldMatrix();
            return _vertices.Select(v => new VertexPositionColor(Vector3.Transform(v, worldMatrix), Color.Red))
                .ToArray();
        }

        public short[] GetIndexes()
        {
            return _indexes;
        }

        public void Draw(GraphicsDevice device)
        {
        }
    }

    public class Game1 : Game
    {
        GraphicsDeviceManager _graphics;

        private Square[] _squares;

        private BasicEffect _basicEffect;

        private VertexBuffer _vertexBuffer;
        private IndexBuffer _indexBuffer;

        public Game1()
        {
            _graphics = new GraphicsDeviceManager(this) {SynchronizeWithVerticalRetrace = false};
            _graphics.PreferredBackBufferWidth = 1920;
            _graphics.PreferredBackBufferHeight = 1080;

            IsFixedTimeStep = false;
            Content.RootDirectory = "Content";

            Window.AllowUserResizing = true;
        }

        protected override void Initialize()
        {
            //_graphics.GraphicsProfile = GraphicsProfile.HiDef;
            //_graphics.PreferMultiSampling = true;
            //_graphics.ApplyChanges();

            _basicEffect = new BasicEffect(GraphicsDevice);
            _basicEffect.World = Matrix.Identity;
            _basicEffect.View = Matrix.CreateLookAt(new Vector3(0, 0, 1), Vector3.Zero, Vector3.Up);
            _basicEffect.Projection = Matrix.CreateOrthographic(1920, 1080, 0, 1);
            _basicEffect.VertexColorEnabled = true;

            _vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor),
                short.MaxValue, BufferUsage.WriteOnly);
            _indexBuffer = new IndexBuffer(GraphicsDevice, typeof(short), short.MaxValue, BufferUsage.WriteOnly);

            base.Initialize();
        }

        protected override void LoadContent()
        {
            int squaresCount = 100;
            _squares = new Square[squaresCount];

            Random rand = new Random();

            for (int i = 0; i < squaresCount; i++)
            {
                _squares[i] = new Square(GraphicsDevice, _graphics.PreferredBackBufferWidth,
                    _graphics.PreferredBackBufferHeight);
                _squares[i].MoveTo(
                    new Vector2(
                        rand.Next(-_graphics.PreferredBackBufferWidth / 2, _graphics.PreferredBackBufferWidth / 2),
                        rand.Next(-_graphics.PreferredBackBufferHeight / 2, _graphics.PreferredBackBufferHeight / 2)));
            }
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.White);

            VertexPositionColor[] vertices = new VertexPositionColor[short.MaxValue];
            short[] indexes = new short[short.MaxValue];
            
            int vertexIndex = 0;
            int indexIndex = 0;
            short lastVertexCount = 0;

            for (int i = 0; i < _squares.Length; i++)
            {
                short indexOffset = lastVertexCount;

                foreach (var vertex in _squares[i].GetVertices())
                {
                    vertices[vertexIndex++] = vertex;
                    lastVertexCount++;
                }

                foreach (short index in _squares[i].GetIndexes())
                {
                    indexes[indexIndex++] = (short) (index + indexOffset);
                }
            }

            _vertexBuffer.SetData(vertices);
            _indexBuffer.SetData(indexes);

            GraphicsDevice.BlendState = BlendState.AlphaBlend;
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            GraphicsDevice.SetVertexBuffer(_vertexBuffer);
            GraphicsDevice.Indices = _indexBuffer;

            foreach (EffectPass pass in _basicEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _indexBuffer.IndexCount / 3);
            }

            base.Draw(gameTime);
        }
    }
}

Screenshot of a profiling :

Am I missing something ? It seems like the SwapChain.Present is eating most of the consumed time, but maybe I missed some optimizations ?

That’s still fairly different from what I meant. I’ll try and set up an example tonight if I manage :slight_smile:

Btw, do you need basiceffect? Or would SpriteEffect do the trick (i.e. do you need 3D lighting)?

Well, I don’t get what mistakes I’ve made, so that would be very helpfull :smiley:

No, I don’t necessarily need lighting (for now ?), I will try using SpriteEffect to see if it’s better.