metal的基础知识入门,首推Metal By Example系列:http://metalbyexample.com/。博主的相关文章,主要给出工程实际遇到的典型问题及其解决方案。
本节源码:https://github.com/sjy234sjy234/Learn-Metal/tree/master/TrueDepthStreaming。从第7节开始,渲染统一采用该节介绍的工程化渲染框架:https://blog.youkuaiyun.com/sjy234sjy234/article/details/82497799。
这一次,主要介绍两个内容:1)iphone X的真实感深度相机调用,获取实时深度帧;2)利用metal的通用计算kernel核函数将深度帧可视化为纹理。如图所示,是实时获取的彩色帧和深度帧的可视化效果。注意这个项目只能在iphone X平台上才可以运行,目前只有iphone X支持真实感深度相机。
1、iphone X的真实感深度相机调用:
参考博主封装的FrontCamera类,要获取最高的深度图像帧率,需要设置如下:
if(isDepthEnabled)
{
[self.avCaptureSession setSessionPreset: AVCaptureSessionPreset640x480];
self.videoDevice=[AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInTrueDepthCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
}
这里的preset,有一些支持深度,有一些不支持深度。但是只有AVCaptureSessionPreset640x480支持最高的帧率和最大的深度利用率。但是这个设置下,得到的彩色帧的分辨率也只有640x480,可以尝试用其他的preset。调用真实感深度相机的主要坑点是在AVCaptureDeviceTypeBuiltInTrueDepthCamera这个配置上,当时没有搜到demo,博主自己尝试出来的。其余的AVCaptureSession、AVCaptureVideoDataOutput、AVCaptureDepthDataOutput、AVCaptureDataOutputSynchronizer这些都非常常规,而且iphone双摄支持深度图像,官方有demo可以参考的。
2、metal通用计算GPGPU的kernel函数
对于从相机获取的彩色帧,直接调用VideoRenderer类进行纹理的渲染即可,里面封装了TextureRendererEncoder。而对于深度帧,是不能直接进行纹理渲染的,DepthRenderer类里面封装了DisparityToTextureEncoder和TextureRendererEncoder。首先,在DepthRenderer中,把深度帧转化为16进制的id<MTLBuffer>,作为kernel函数的输入:
id<MTLBuffer> inDisparityBuffer = [_metalContext bufferWithF16PixelBuffer: depthPixelBuffer];
然后调用DisparityToTextureEncoder类,把id<MTLBuffer>格式的深度帧转化为可视化的纹理id<MTLTexture>,这里实现了一个非常简单的GPGPU的kernel函数的封装,首先是encode函数:
- (void)encodeToCommandBuffer: (id<MTLCommandBuffer>) commandBuffer inDisparityBuffer:(const id<MTLBuffer>)inDisparityBuffer outTexture: (id<MTLTexture>) outTexture
{
if(!commandBuffer)
{
NSLog(@"invalid commandBuffer");
return ;
}
if(!inDisparityBuffer)
{
NSLog(@"invalid disparity buffer");
return ;
}
if(!outTexture)
{
NSLog(@"invalid out texture");
return ;
}
const NSUInteger width = 8;
const NSUInteger height = 8;
const NSUInteger depth = 1;
_threadgroupSize = MTLSizeMake((width), (height), depth);
_threadgroupCount.width = (outTexture.width + _threadgroupSize.width - 1) / _threadgroupSize.width;
_threadgroupCount.height = (outTexture.height + _threadgroupSize.height - 1) / _threadgroupSize.height;
_threadgroupCount.depth = depth;
id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder];
[computeEncoder setComputePipelineState:_computePipeline];
[computeEncoder setBuffer: inDisparityBuffer offset:0 atIndex:0];
[computeEncoder setTexture: outTexture atIndex:0];
[computeEncoder dispatchThreadgroups:_threadgroupCount
threadsPerThreadgroup:_threadgroupSize];
[computeEncoder endEncoding];
}
encode函数中,首先根据纹理尺寸,配置kernel的thread group size和thread group count,然后分配computeEncoder(这里区别于渲染时分配的renderEncoder),并对其进行程序编码。然后是DisparityToTextureEncoder.metal文件中的kernel函数:
#include <metal_stdlib>
using namespace metal;
// disparityToTexture compute kernel
kernel void
disparityToTexture(constant half* currentDisparityBuffer [[buffer(0)]],
texture2d<float, access::write> outTexture [[texture(0)]],
uint2 gid [[thread_position_in_grid]],
uint2 tspg [[threads_per_grid]])
{
uint invid = gid.y * tspg.x + gid.x;
half inDisparity = currentDisparityBuffer[invid];
half inDepth = 1.0 / inDisparity;
float4 outColor = {inDepth, inDepth, inDepth, inDepth};
outTexture.write(outColor, gid);
}
这里,tspg是整个kernel的size,等于输入帧的尺寸。gid是当前执行线程在kernel中的位置,用于数据的重定位。在这个kernel中,gid即纹理坐标,但是对于MTLBuffer来说,不能用纹理方式读取数据,只能用重新计算的方式定位,它们在内存中是行优先存储的,有重定位计算式 —— invid = gid.y * tspg.x + gid.x。
在调用完DisparityToTextureEncoder类得到可以渲染的可视化纹理以后,再调用TextureRendererEncoder类进行视图渲染即可。
PS:
(1)说tspg等于输入帧的尺寸是不准确的,只有输入帧的尺寸整除thread group size的时候这才是成立的,实际上tspg等于thread group size * thread group count。但在这个demo中是成立的,因为输入帧的尺寸是640x480,可以整除8。因此这个DisparityToTextureEncoder只支持分辨率尺寸能整除8的帧作为输入,否则需要修改kernel的重定位算式。
(2)iphone X获取的实时深度帧是以float16的格式存储的,并且存储的值叫做disparity,它的单位是(1/m),因此换算成深度值是:inDepth = 1.0 / inDisparity。
(3)metal的GPGPU计算,和其他语言的GPGPU是相似的,如Cuda,可以学习Cuda进行入门。Metal的学习资料比较有限,基本上只能靠查官方文档,并且官方文档也有一些过时,有时靠自己试错。