Search code examples
iosobjective-copengl-esmetalantialiasing

iOS Metal: Jaggies Anit-aliasing


I was trying to draw a half circle with renderEncoder's drawIndexedPrimitives

[renderEncoder setVertexBuffer:self.vertexBuffer offset:0 atIndex:0];

[renderEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangleStrip
                              indexCount:self.indexCount
                               indexType:MTLIndexTypeUInt16
                             indexBuffer:self.indicesBuffer
                       indexBufferOffset:0];

where the vertexBuffer and indicesBuffer for the circle were created by calculation

int segments = 10;


float vertices02[ (segments +1)* (3+4)];
vertices02[0] = centerX;
vertices02[1] = centerY;
vertices02[2] = 0;

//3, 4, 5, 6 are RGBA
vertices02[3] = 1.0;
vertices02[4] = 0;
vertices02[5] = 0.0;
vertices02[6] = 1.0;    

uint16_t indices[(segments -1)*3];

for (int i = 1; i <= segments ; i++){
    float degree = (i -1) * (endDegree - startDegree)/ (segments -1) + startDegree;

    vertices02[i*7] = (centerX + cos([self degreesToRadians:degree])*radius);
    vertices02[i*7 +1] = (centerY +  sin([self degreesToRadians:degree])*radius);
    vertices02[i*7 +2] = 0;

    vertices02[i*7 +3] = 1.0;
    vertices02[i*7 +4] = 0;
    vertices02[i*7 +5] = 0.0;
    vertices02[i*7 +6] = 1.0;

    if (i < segments){
        indices[(i-1)*3 + 0] = 0;
        indices[(i-1)*3 + 1] = i;
        indices[(i-1)*3 + 2] = i+1;
    }
}

So I am combining 9 Triangle to form a 180 degree circle.

Then create vertexBuffer and indicesBuffer

self.vertexBuffer = [device newBufferWithBytes:vertexArrayPtr
                                            length:vertexDataSize
                                           options:MTLResourceOptionCPUCacheModeDefault];
self.indicesBuffer = [device newBufferWithBytes:indexArrayPtr
                                             length:indicesDataSize 
                                            options:MTLResourceOptionCPUCacheModeDefault];

The result is like this:

Half Circle by Metal

I believe this is Anti-Aliasing problem from Metal of iOS. I used to create half circle in OpenGL using same technique but the edges was much smoother.

Any suggestions to tackle the problem?


Suggested by warrenm, I should set the CAMetalLayer's drawableSize equals screenSize x scale. There are improvements:

enter image description here


Another Suggestion by warrenm, using MTKView and setting sampleCount = 4 solved the problem: enter image description here


Solution

  • There are a couple of things to consider here. First, you need to ensure that (when possible) the size of the grid you're rasterizing to matches the resolution of the display it will be viewed on. Second, you might need to use subpixel techniques to eke out additional smoothness, since raster techniques tend to undersample continuous functions.

    In Metal, the way we match the rendered image size to the display is by ensuring that the drawable size of the Metal layer matches the pixel dimensions it will occupy on the screen. When using CAMetalLayer directly, the default behavior is for the drawable size of the layer to be the size of the layer's bounds multiplied by the layer's contentsScale property. Setting the latter to the scale of the UIScreen onto which the layer is composited will match the layer's dimensions to the screen's pixels (ignoring other transformations that might be applied to the layer or its view hierarchy).

    When using MTKView, the autoResizeDrawable property determines whether the view automatically manages its layer's drawable size. This is the default behavior, but if you set this property to NO, you can manually set the drawable size to something else (e.g., use adaptive resolution rendering when fragment-bound).

    In order to sample more finely, we have our choice among any number of antialiasing techniques, but perhaps the easiest of these is multisampled antialiasing (MSAA), a hardware feature that—as the name suggests—takes multiple samples for each pixel along the edges of primitives, in order to reduce the jagged effects of aliasing.

    In Metal, using MSAA requires setting multisampling state (i.e., the sample count) on both the render pipeline state and the textures used for rendering. MSAA is a two-step process, where a render target that can hold the data for multiple fragments per pixel is rendered to, then a resolve step combines these samples into the final color for each pixel. When using CAMetalLayer (or drawing off-screen), you must create a texture of type MTLTextureType2DMultisample for each active color/depth attachment. These textures are configured as the texture property of their respective color/depth attachments, and the resolveTexture property is set to a texture of type MTLTextureType2D, into which the MSAA targets are resolved.

    When using MTKView, simply setting the sampleCount on the view to match the sampleCount of the render pipeline descriptor is sufficient to get MetalKit to create and manage the appropriate resources. By default, the render pass descriptors you receive from a view will have an internally-managed MSAA color target set as the primary color attachment, and the current drawable's texture set as the resolve texture of that attachment. In this way, enabling MSAA with MetalKit only requires a couple of lines of code.