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 :
Instead of what I want, which is :
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);
}
}
}