Hi guys,
very quick rundown of how to make MSAA work for your 3d game.
First of all - what is msaa? It’s multisample anti aliasing and it helps to combat the “jaggies” at the edges of 3d models.
I won’t explain why oversampling is needed, but for reference:
Make it work with the backbuffer (no rendertarget used)
This is the default option and it works right from the get-go.
(If you click on the images they will show up)
inside your game.cs (so the main class from which everything is running, it’s also the first class you have next to program.cs)
public Game()
{
//Initialize graphics and content
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
[....]
//HiDef enables usable shaders, safe to enable for basically all pc users
_graphics.GraphicsProfile = GraphicsProfile.HiDef;
_graphics.PreferMultiSampling = true;
_graphics.ApplyChanges();
[...]
}
and if you want more control over how it performs:
protected override void Initialize()
{
[...]
// The number determines how good our antialiasing works.
// Possible values are 2,4,8,16,32, but not all work on all computers.
// 4 is safe, and 8 is too in almost all cases
// Higher numbers mean lower framerates
GraphicsDevice.PresentationParameters.MultiSampleCount = 8;
_graphics.ApplyChanges();
[...]
}
The second part is not needed, the default one will work already.
This should work on either DirectX or OpenGL platforms.
Make it work with a rendertarget
A rendertarget is basically just a texture we draw to. The advantage of drawing to a texture first instead of the backbuffer means we can reuse it for other stuff and modify it extensively.
Unfortunately, when using DirectX, Monogame does not support MSAA currently per default.
However, if you modify the monogame source a little bit you can make it work for your game!
The simplest way to do so is to modify the RenderTarget2D.DirectX.cs a little bit (add a few lines, nothing gets lost)
for convenience: here is the complete class
namespace Microsoft.Xna.Framework.Graphics
{
public partial class RenderTarget2D
{
internal RenderTargetView[] _renderTargetViews;
internal DepthStencilView _depthStencilView;
private void PlatformConstruct(GraphicsDevice graphicsDevice, int width, int height, bool mipMap,
SurfaceFormat preferredFormat, DepthFormat preferredDepthFormat, int preferredMultiSampleCount, RenderTargetUsage usage, bool shared)
{
GenerateIfRequired();
}
private void GenerateIfRequired()
{
if (_renderTargetViews != null)
return;
// Create a view interface on the rendertarget to use on bind.
if (ArraySize > 1)
{
_renderTargetViews = new RenderTargetView[ArraySize];
for (var i = 0; i < ArraySize; i++)
{
var renderTargetViewDescription = new RenderTargetViewDescription();
if (GetTextureSampleDescription().Count > 1)
{
renderTargetViewDescription.Dimension = RenderTargetViewDimension.Texture2DMultisampledArray;
renderTargetViewDescription.Texture2DMSArray.ArraySize = 1;
renderTargetViewDescription.Texture2DMSArray.FirstArraySlice = i;
}
else
{
renderTargetViewDescription.Dimension = RenderTargetViewDimension.Texture2DArray;
renderTargetViewDescription.Texture2DArray.ArraySize = 1;
renderTargetViewDescription.Texture2DArray.FirstArraySlice = i;
renderTargetViewDescription.Texture2DArray.MipSlice = 0;
}
_renderTargetViews[i] = new RenderTargetView(
GraphicsDevice._d3dDevice, GetTexture(),
renderTargetViewDescription);
}
}
else
{
if (MultiSampleCount > 1)
{
//_multiSampledTexture = GetMultiSampledTexture();
_renderTargetViews = new [] {new RenderTargetView(GraphicsDevice._d3dDevice, GetMultiSampledTexture()) };
}
else
{
_renderTargetViews = new[] { new RenderTargetView(GraphicsDevice._d3dDevice, GetTexture())};
}
}
// If we don't need a depth buffer then we're done.
if (DepthStencilFormat == DepthFormat.None)
return;
// The depth stencil view's multisampling configuration must strictly
// match the texture's multisampling configuration. Ignore whatever parameters
// were provided and use the texture's configuration so that things are
// guarenteed to work.
var multisampleDesc = GetTextureSampleDescription();
// Create a descriptor for the depth/stencil buffer.
// Allocate a 2-D surface as the depth/stencil buffer.
// Create a DepthStencil view on this surface to use on bind.
using (var depthBuffer = new SharpDX.Direct3D11.Texture2D(GraphicsDevice._d3dDevice, new Texture2DDescription
{
Format = SharpDXHelper.ToFormat(DepthStencilFormat),
ArraySize = 1,
MipLevels = 1,
Width = width,
Height = height,
SampleDescription = multisampleDesc,
BindFlags = BindFlags.DepthStencil,
}))
{
// Create the view for binding to the device.
_depthStencilView = new DepthStencilView(GraphicsDevice._d3dDevice, depthBuffer,
new DepthStencilViewDescription()
{
Format = SharpDXHelper.ToFormat(DepthStencilFormat),
Dimension = GetTextureSampleDescription().Count > 1 ? DepthStencilViewDimension.Texture2DMultisampled : DepthStencilViewDimension.Texture2D
});
}
}
private SharpDX.Direct3D11.Resource GetMultiSampledTexture()
{
var desc = new SharpDX.Direct3D11.Texture2DDescription();
desc.Width = width;
desc.Height = height;
desc.MipLevels = _levelCount;
desc.ArraySize = 1;
desc.Format = SharpDXHelper.ToFormat(_format);
desc.BindFlags = SharpDX.Direct3D11.BindFlags.RenderTarget;
desc.CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.None;
desc.SampleDescription.Count = MultiSampleCount;
desc.SampleDescription.Quality = (int)StandardMultisampleQualityLevels.StandardMultisamplePattern;
desc.Usage = SharpDX.Direct3D11.ResourceUsage.Default;
desc.OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.None;
if (_mipmap)
desc.OptionFlags |= SharpDX.Direct3D11.ResourceOptionFlags.GenerateMipMaps;
if (_shared)
desc.OptionFlags |= SharpDX.Direct3D11.ResourceOptionFlags.Shared;
_sampleDescription = desc.SampleDescription;
_texture = new SharpDX.Direct3D11.Texture2D(GraphicsDevice._d3dDevice, desc);
return _texture;
}
private void PlatformGraphicsDeviceResetting()
{
if (_renderTargetViews != null)
{
for (var i = 0; i < _renderTargetViews.Length; i++)
_renderTargetViews[i].Dispose();
_renderTargetViews = null;
}
SharpDX.Utilities.Dispose(ref _depthStencilView);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_renderTargetViews != null)
{
for (var i = 0; i < _renderTargetViews.Length; i++)
_renderTargetViews[i].Dispose();
_renderTargetViews = null;
}
SharpDX.Utilities.Dispose(ref _depthStencilView);
}
base.Dispose(disposing);
}
RenderTargetView IRenderTarget.GetRenderTargetView(int arraySlice)
{
GenerateIfRequired();
return _renderTargetViews[arraySlice];
}
DepthStencilView IRenderTarget.GetDepthStencilView()
{
GenerateIfRequired();
return _depthStencilView;
}
public void ResolveSubresource(RenderTarget2D destination)
{
GraphicsDevice._d3dContext.ResolveSubresource(this._texture, 0, destination._texture, 0,
SharpDXHelper.ToFormat(_format));
}
}
}
The main things added are:
- private SharpDX.Direct3D11.Resource GetMultiSampledTexture()
which is an alternative to our default GetTexture() - public void ResolveSubresource(RenderTarget2D destination)
which copies our multisampled texture to a not multisampled texture so we can use it - an if function to check which getTexture we use
alright. That’s it for the monogame source.
Most of this stuff is copied from here: MSAA? Does it work? How to make it work?
Now we can use a rendertarget with a preferred MultisampleCount > 0, as always 2,4,8 are your candidates.
We also need a resolve texture which we can use as a resource after the rendering of our geometry.
Here is an example from my application.
_renderTarget = new RenderTarget2D(graphicsDevice: _graphicsDevice,
width: GameSettings.g_ScreenWidth / scale,
height: GameSettings.g_ScreenHeight / scale,
mipMap: false,
preferredFormat: SurfaceFormat.Color,
preferredDepthFormat: DepthFormat.Depth24,
preferredMultiSampleCount: 8,
usage: RenderTargetUsage.DiscardContents
);
_resolvedRenderTarget = new RenderTarget2D(graphicsDevice: _graphicsDevice,
width: GameSettings.g_ScreenWidth / scale,
height: GameSettings.g_ScreenHeight / scale,
mipMap: false,
preferredFormat: SurfaceFormat.Color,
preferredDepthFormat: DepthFormat.None,
preferredMultiSampleCount: 0,
usage: RenderTargetUsage.DiscardContents
);
When we finish the rendering (we drew all meshes) we need to copy the MSAA’d texture to the standard texture.
We can do it like this
if (_renderTarget.MultiSampleCount > 1)
{
_renderTarget.ResolveSubresource(_resolvedRenderTarget);
DrawMapToScreenToFullScreen(_resolvedRenderTarget);
}
else
{
DrawMapToScreenToFullScreen(_renderTarget);
}
In this case DrawMapToFullScreen just draws a texture to the backbuffer.
Done!
FAQ
- why are you not contributing to the monogame source?
Several things. First of all, this does only work with non-mipmapped non-atlassed rendertargets. That’s most likely all the RT’s you need, but not good enough for a release to the public.
Then you also have the issue that the developer has to resolve the texture “by hand”. Which is good in cases where people know what they are doing but ideally this is solved behind the scenes and changing the multisamplecount on a rendertarget just works. That’s not trivial though.
internal SharpDX.Direct3D11.ShaderResourceView GetShaderResourceView() in Texture.DirectX.cs has to be handled, too.
One could have an additional referenced texture inside the RenderTarget2D as well and that could be used as a target for the MSAA resolve, but would be dead weight for non-msaa rendertargets.
It would also be useful to find out whether or not one can use a simple Texture2D instead of RT2D as a resolve texture. Most likely, yes. But that might be an issue for someone in the future, too.
- Why desc.SampleDescription.Quality = (int)StandardMultisampleQualityLevels.StandardMultisamplePattern ?
You can also use zero here, or find out what MSAA patterns / techniques your vendor supports (I might write some stuff about that soon). There is also potential to let the developer decide the quality or to set it to maximum by default, but the exact quality level number depends on sample count. Setting it to -1 (StandardMultisamplePattern) or 0 is safe.