学习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的图片,如下所示
在这里插入图片描述
进行锐化处理后的结果如下所示
在这里插入图片描述
可以看到边缘增加了很强的锐化效果,同时由于原图存在很多噪点,这种锐化的方式把噪点也锐化了。

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