自定义 Metal 渲染视图

1,749 阅读3分钟

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

概述

我们可以通过 MetalKit 提供的 MTKView 来快速编写基于 Metal 渲染的视图,MTKView 视图把 Metal 的操作都封装了起来,一定程度上方面了开发。

但有时我们希望能够更加灵活的来控制 Metal 内容的呈现方式,这时就需要自己从头开始操作 Metal 来实现渲染流程。

本例将自定义 NSView 或 UIView 来实现 Metal 渲染,使用 CAMetalLayer 对象来保存视图的内容。

使用 Metal Layer 来配置视图

为了支持 Metal 渲染到视图,必须指定视图的图层为 CAMetalLayer 。

UIKit 中的所有视图都是有 layer 来渲染的。视图实现了 layerClass 类方法,来指定 layer 的类型。


+ (Class) layerClass

{

    return [CAMetalLayer class];

}

在 AppKit 中,需要设置视图的 WantsLayer 属性来支持视图层。


self.wantsLayer = YES;

这会触发对视图的 makeBackingLayer 方法的调用,该方法返回一个 CAMetalLayer 对象。


- (CALayer *)makeBackingLayer

{

    return [CAMetalLayer layer];

}

渲染到视图

要渲染到视图,需要创建一个 MTLRenderPassDescriptor 对象。该对象以图层提供的纹理为目标,可以设置清空的背景色,并在渲染通道完成时将渲染的内容存储到纹理中。


_drawableRenderDescriptor = [MTLRenderPassDescriptor new];

_drawableRenderDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;

_drawableRenderDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;

_drawableRenderDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 1, 1, 1);

需要创建一个纹理来存储渲染通道渲染的结果。每次应用程序渲染一帧时,渲染器都会从 Metal 层获取一个 CAMetalDrawable 对象, 该对象为 Core Animation 提供了在屏幕上呈现的纹理。


id<CAMetalDrawable> currentDrawable = [metalLayer nextDrawable];

// If the current drawable is nil, skip rendering this frame

if(!currentDrawable)

{

    return;

}

_drawableRenderDescriptor.colorAttachments[0].texture = currentDrawable.texture;

实现一个渲染的循环

需要设置一个定时器以指定的时间间隔重绘制视图。iOS 中使用 CADisplayLink 来创建一个定时器。

需要在创建定时器之前知道窗口在哪个屏幕上,所以当 UIKit 调用视图的 didMoveToWindow() 方法时,会调用这个方法。 UIKit 在第一次将视图添加到窗口以及将视图移动到另一个屏幕时调用此方法。下面的代码停止渲染循环并初始化一个新的循环。


- (void)setupCADisplayLinkForScreen:(UIScreen*)screen

{

    [self stopRenderLoop];

    _displayLink = [screen displayLinkWithTarget:self selector:@selector(render)];

    _displayLink.paused = self.paused;

    _displayLink.preferredFramesPerSecond = 60;

}

在 macOS 中 CADisplayLink 不可用,需要使用 CVDisplayLink 来创建定时器, 它们看起来不同,但原则上具有相同的目标,即允许回调与显示同步。创建定时器时机也与 iOS 一样,在第一次将视图添加到窗口以及将视图移动到另一个屏幕时调用此方法。


- (BOOL)setupCVDisplayLinkForScreen:(NSScreen*)screen

{
#if RENDER_ON_MAIN_THREAD
    // The CVDisplayLink callback, DispatchRenderLoop, never executes
    // on the main thread. To execute rendering on the main thread, create
    // a dispatch source using the main queue (the main thread).
    // DispatchRenderLoop merges this dispatch source in each call
    // to execute rendering on the main thread.
    _displaySource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD,
    0, 
    0, 
    dispatch_get_main_queue());

    __weak AAPLView* weakSelf = self;

    dispatch_source_set_event_handler(_displaySource, ^(){
        @autoreleasepool
        {
            [weakSelf render];
        }
    });
    dispatch_resume(_displaySource);
#endif // END RENDER_ON_MAIN_THREAD
    CVReturn cvReturn;
    // Create a display link capable of being used with all active displays

    cvReturn = CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
    if(cvReturn != kCVReturnSuccess)

    {
        return NO;
    }

#if RENDER_ON_MAIN_THREAD

    // Set DispatchRenderLoop as the callback function and
    // supply _displaySource as the argument to the callback.
    cvReturn = CVDisplayLinkSetOutputCallback(_displayLink, 
    &DispatchRenderLoop, 
    (__bridge void*)_displaySource);

#else // IF !RENDER_ON_MAIN_THREAD

    // Set DispatchRenderLoop as the callback function and

    // supply this view as the argument to the callback.

    cvReturn = CVDisplayLinkSetOutputCallback(_displayLink, 
    &DispatchRenderLoop, 
    (__bridge void*)self);

#endif // END !RENDER_ON_MAIN_THREAD

    if(cvReturn != kCVReturnSuccess)

    {
        return NO;
    }

    // Associate the display link with the display on which the

    // view resides

    CGDirectDisplayID viewDisplayID =

        
 (CGDirectDisplayID) [self.window.screen.deviceDescription[@"NSScreenNumber"] 
        unsignedIntegerValue];


cvReturn = CVDisplayLinkSetCurrentCGDisplay(_displayLink, 
viewDisplayID);

    if(cvReturn != kCVReturnSuccess)

    {

        return NO;

    }

    CVDisplayLinkStart(_displayLink);

    NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];

    // Register to be notified when the window closes so that you

    // can stop the display link

    [notificationCenter addObserver:self

                           selector:@selector(windowWillClose:)

                               name:NSWindowWillCloseNotification

                             object:self.window];

    return YES;

}

渲染

绘制的代码需要写在这个循环里面,即render,具体绘制方法在本文中暂时讲解。


- (void)render

{
    //此处省略
}

总结

本文介绍了自定义基于 Metal 渲染视图的步骤,主要包括指定图层类型为CAMetalLayer,创建MTLRenderPassDescriptor对象,实现渲染循环并绘制。