學習Metal:後處理

學習Metal:後處理

離屏渲染

在之前的例子中,圖像的內容是直接渲染到屏幕上,也就是我們畫了什麼內容,在屏幕上立即就顯示出來了,這種模式稱之爲當前屏幕渲染(on-screen rendering)。但是有些情況下,我們希望對圖像的內容做一些後處理,然後在顯示出來,這就需要用到離屏渲染了(off-screen rendering)。
所以離屏渲染實際上就是把我們需要顯示的內容渲染到另外的內存中,而不是直接上屏。在OpenGL中用到的是幀緩衝(FBO)的概念,而在metal中我們只需要把MTLRenderPassDescriptor中的texture設置成我們自己定義的texture即可。在設置好後,渲染的內容自動在我們定義的這個texture中,這樣這個texture的內容就可以作爲下一步渲染的輸入。我們可以訪問渲染後內容的每一個像素值,然後就可以對渲染後的場景進行我們自己的處理,所以我們稱這種操作爲後處理(Post-Processing)。

簡單的銳化處理

我們可以對一副圖像做一個簡單的銳化處理,它的原理是取樣一個像素點的值以及它周圍上下左右四個點的值。例如分別取樣的值爲color, color_t, color_b, color_l, color_r,然後通過如下的處理作爲返回值,實際上就完成了一個最簡單的銳化的處理。

5*color - (color_t + color_b + color_l + color_r)

以上公式對於邊緣的部分,也就是和周圍像素相比,差值比較大的點,處理後的結果會使得差值更大。但是對於平坦的區域,也就是和周圍像素的差值很小的點,處理後的結果會基本保持和原圖一直。

Metal後處理實例

還是利用之前的例子,只不過我們這次是進行兩邊的處理,第一遍先把一副圖像渲染到一個紋理中,然後在對這個紋理進行取樣,做銳化處理。

初始化

由於這次我們是有兩次渲染,所以我們需要有兩個MTLRenderPipelineState對象,來分別表示兩次渲染的過程。同時需要一個MTLTexture來存儲第一次渲染的結果,也就是離屏渲染的目標紋理。同時爲了確定銳化後處理中,採樣周圍元素所需要的步長,也就是一個像素在採樣的時候具體偏移是多少,我們需要一個MTLBuffer來傳遞。所以我們有如下的定義

id<MTLTexture> offScreenTexture;
id<MTLRenderPipelineState> renderPipelineState2;

id<MTLBuffer> uTexStepBuffer;

同時初始化renderPipelineState2如下所示

id<MTLFunction> fragmentProgram2 = [mtlLibrary newFunctionWithName:@"fragmentShader2"];
mtlRenderPipelineDescriptor.fragmentFunction = fragmentProgram2;
renderPipelineState2 = [mtlDevice newRenderPipelineStateWithDescriptor:mtlRenderPipelineDescriptor error:nil];

offScreenTexture = [self generateTextureWithWidthFormat:MTLPixelFormatBGRA8Unorm width:imageWidth height:imageHeight];

uTexStepBuffer = [mtlDevice newBufferWithLength:sizeof(uTexStep) options:MTLResourceOptionCPUCacheModeDefault];

渲染的目標紋理通過以下的方式來生成, format可以設置爲MTLPixelFormatBGRA8Unorm,同時width和height可以和加載的圖像的寬和高保持相同。特別注意的是usage一定要相應的設置。

- (id<MTLTexture>) generateTextureWithWidthFormat: (MTLPixelFormat)format width: (int)width height:(int) height {
    MTLTextureDescriptor *textureDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format width:width height:height mipmapped:NO];
    
    textureDesc.usage = MTLTextureUsageRenderTarget|MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite;

    id<MTLTexture> texture = [mtlDevice newTextureWithDescriptor:textureDesc];
    return texture;
}

而步長是涉及到寬和高兩個方向的偏移,所以通過一個結構體來表示

typedef struct {
    float widthStep;
    float heightStep;
} uTexStep;

渲染

兩次渲染實際上就是兩個MTLRenderCommand,可以通過一個MTLCommandBuffer來提交。需要注意的是在第一個MTLRenderCommand的渲染到我們自己的紋理中,並把這個紋理作爲第二次渲染的輸入。同時採用的步長分別是1/imageWidth和1/imageHeight,設置到第二次渲染中。第二次渲染真正上屏。

   id<MTLCommandBuffer> mtlCommandBuffer = [mtlCommandQueue commandBuffer];

    MTLRenderPassDescriptor *mtlRenderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
    mtlRenderPassDescriptor.colorAttachments[0].texture = offScreenTexture;
    mtlRenderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
    mtlRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0);
    mtlRenderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;

    id<MTLRenderCommandEncoder> renderEncoder = [mtlCommandBuffer renderCommandEncoderWithDescriptor:mtlRenderPassDescriptor];
    [renderEncoder setRenderPipelineState:renderPipelineState];
    [renderEncoder setVertexBuffer:vertexBuffer offset:0 atIndex:0];
    [renderEncoder setVertexBuffer:texCoordBuffer offset:0 atIndex:1];
    [renderEncoder setFragmentTexture:texture atIndex:0];
    [renderEncoder setFragmentSamplerState:sampler atIndex:0];
    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
    [renderEncoder endEncoding];
    
    uTexStep texStep;
    texStep.widthStep = 1.0/imageWidth;
    texStep.heightStep = 1.0/imageHeight;
    memcpy(uTexStepBuffer.contents, &texStep, sizeof(uTexStep));
    
    frameDrawable = [metalLayer nextDrawable];
    mtlRenderPassDescriptor.colorAttachments[0].texture = frameDrawable.texture;
    id<MTLRenderCommandEncoder> renderEncoder2 = [mtlCommandBuffer renderCommandEncoderWithDescriptor:mtlRenderPassDescriptor];
    [renderEncoder2 setRenderPipelineState:renderPipelineState2];
    [renderEncoder2 setVertexBuffer:vertexBuffer offset:0 atIndex:0];
    [renderEncoder2 setVertexBuffer:texCoordBuffer offset:0 atIndex:1];
    [renderEncoder2 setFragmentBuffer:uTexStepBuffer offset:0 atIndex:0];
    [renderEncoder2 setFragmentTexture:offScreenTexture atIndex:0];
    [renderEncoder2 setFragmentSamplerState:sampler atIndex:0];
    [renderEncoder2 drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
    [renderEncoder2 endEncoding];

    [mtlCommandBuffer presentDrawable:frameDrawable];
    [mtlCommandBuffer commit];

Shader實現

第一遍渲染和之前沒什麼區別,就是把圖片渲染出來,只不過是渲染到我們自己的紋理中,而非上屏。第二遍渲染就實現了上文提到的銳化後處理,分別採用像素點本身和周圍上下左右四個像素點的值,然後通過計算得到處理後的值。

fragment float4 fragmentShader2(VertexOut vert [[stage_in]],
                    constant float2& uTexStep [[buffer(0)]],
                    texture2d<float> texture [[texture(0)]],
                    sampler sam [[sampler(0)]]) {
    float4 samplerColor = texture.sample(sam, vert.texCoord);
    float4 samplerColor_t = texture.sample(sam, vert.texCoord - float2(0.0, uTexStep.y));
    float4 samplerColor_b = texture.sample(sam, vert.texCoord + float2(0.0, uTexStep.y));
    float4 samplerColor_l = texture.sample(sam, vert.texCoord - float2(uTexStep.x, 0.0));
    float4 samplerColor_r = texture.sample(sam, vert.texCoord - float2(uTexStep.x, 0.0));
    float4 color = samplerColor * 5 - (samplerColor_t + samplerColor_b + samplerColor_l + samplerColor_r);
    return color;
}

後處理結果

爲了更好的看到效果,我們把之前的笑臉換成lena的圖片,如下所示
在這裏插入圖片描述
進行銳化處理後的結果如下所示
在這裏插入圖片描述
可以看到邊緣增加了很強的銳化效果,同時由於原圖存在很多噪點,這種銳化的方式把噪點也銳化了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章