OpenGL与Metal API的Point Sprite

我们在实际用OpenGL等3D图形渲染API时点图元往往用得不多,而在粒子系统中可能也是用一个正方形来绘制一单个粒子。不过在当前大部分3D图形渲染API中都能支持用点图元来绘制一个具有纹理贴图的粒子,从早在OpenGL 1.4开始就能支持了,而在OpenGL ES 1.1中,大部分GPU都能实现 GL_OES_point_sprite 这一扩展,同样也能使用此功能。
使用Point Sprite的一大好处就是顶点数量大大降低,本来需要绘制一个具有四个顶点的正方形图元,而现在缩减到了只含一个顶点的点图元,这样大大节省了带宽。此外,GPU对于点精灵的渲染往往也会有特别的优化处理。所以如果我们要制作大规模的粒子特效的话可以考虑使用point sprite技术。

下面我们将分别通过使用固定功能流水线的OpenGL 2.1以及Metal API来讲解如何使用Point Sprite。

OpenGL中使用Point Sprite

在固定功能的OpenGL中使用Point Sprite主要遵循以下几个要点:

  1. 我们需要指定点的大小,可以通过glPointParameterf接口通过指定GL_POINT_SIZE_MINGL_POINT_SIZE_MAX这两个参数即可。
  2. 我们需要显式使用glEnable(GL_POINT_SPRITE)来开启Point Sprite功能。
  3. 在使用粒子效果的纹理时,需要使用glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE)对点做纹理元素与像素颜色的插值处理。
  4. 对于固定功能的图形流水线,我们需要将表示粒子效果的纹理单独作为一张图拿出来,而不能合并到其他图上去做采样。另外我们需要确保纹理大小的最小范围。比如,如果当前GPU所能支持的最小纹理图片的分辨率为64x64,那么我们需要提供一张64x64的png图片。

下面我们将列出OpenGL的相关代码。笔者在macOS 10.14系统上通过Xcode 10.1完成的。
首先简单看一下MyGLLayer.h头文件:

//
//  MyGLLayer.h
//  GLPointSprite
//
//  Created by Zenny Chen on 2019/1/24.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

@import Cocoa;
@import QuartzCore;

#ifndef let
#define let     __auto_type
#endif

@interface MyGLLayer : NSOpenGLLayer

@end

然后我们看这里最最关键的MyGLLayer.m源文件:

//
//  MyGLLayer.m
//  GLPointSprite
//
//  Created by Zenny Chen on 2019/1/24.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

#import "MyGLLayer.h"
#include <OpenGL/gl.h>

@import OpenGL;

@implementation MyGLLayer
{
@private
    
    /// 当前OpenGL上下文的像素格式
    NSOpenGLPixelFormat *mPixelFormat;
    
    /// 当前OpenGL的上下文
    NSOpenGLContext *mContext;
    
    /// 纹理ID
    GLuint mTexName;
}

- (instancetype)init
{
    self = super.init;

    self.backgroundColor = NSColor.clearColor.CGColor;

    self.opaque = YES;

    // 由于我们这里不做周期性动画更新,因此只有当layer接收到setNeedsDisplay消息时才做更新
    self.asynchronous = NO;

    NSOpenGLPixelFormatAttribute attrs[] =
    {
        // 可选地,我们这里使用了双缓冲机制
        NSOpenGLPFADoubleBuffer,
        
        // 由于我们这里就用固定功能流水线,因此直接是用legacy的OpenGL版本即可
        NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersionLegacy,
        
        // 开启多重采样反走样
        NSOpenGLPFAMultisample,
        
        // 指定一个用于MSAA的缓存
        NSOpenGLPFASampleBuffers, (NSOpenGLPixelFormatAttribute)1,
        
        // 指定MSAA使用四个样本
        NSOpenGLPFASamples, (NSOpenGLPixelFormatAttribute)4,
        
        0
    };

    mPixelFormat = [NSOpenGLPixelFormat.alloc initWithAttributes:attrs];

    mContext = [NSOpenGLContext.alloc initWithFormat:mPixelFormat shareContext:nil];

    [self.openGLContext makeCurrentContext];

    // 以垂直刷新率来同步缓存交换
    [self.openGLContext setValues:(const GLint[]){1} forParameter:NSOpenGLCPSwapInterval];
    
    GLfloat fSizes[2];
    glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, fSizes);
    printf("Point minimum size: %.1f, maximum size: %.1f", fSizes[0], fSizes[1]);

    return self;
}

- (void)dealloc
{
    glDeleteTextures(1, &mTexName);
    
    if(mPixelFormat != nil)
    {
        [mPixelFormat release];
        mPixelFormat = nil;
    }
    if(mContext != nil)
    {
        [mContext release];
        mContext = nil;
    }
    
    [super dealloc];
}

- (NSOpenGLPixelFormat*)openGLPixelFormat
{
    return mPixelFormat;
}

- (NSOpenGLContext*)openGLContext
{
    return mContext;
}

/// 创建原图像位图数据缓存
/// @param image 指定原图像对象
/// @param pWidth 输出图像宽度
/// @param pHeight 输出图像高度
/// @return 创建出来的图像位图数据
- (uint8_t*)allocSourceImageData:(NSImage*)image width:(int*)pWidth height:(int*)pHeight
{
    const int width = image.size.width;
    const int height = image.size.height;
    
    if(pWidth != NULL)
        *pWidth = width;
    if(pHeight != NULL)
        *pHeight = height;
    
    const size_t length = width * height * 4;
    
    uint8_t *buffer = malloc(length);
    
    /**
     * [0] => R
     * [1] => G
     * [2] => B
     * [3] => A
     */
    const CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big;
    
    // Initialize the source image buffer
    let colorSpace = CGColorSpaceCreateDeviceRGB();
    let context = CGBitmapContextCreate(buffer,
                                        width,
                                        height,
                                        8,         /* bits per component*/
                                        width * 4,  /* bytes per row */
                                        colorSpace,
                                        bitmapInfo);
    
    CGColorSpaceRelease(colorSpace);
    
    let cImageRef = [image CGImageForProposedRect:NULL context:NULL hints:NULL];
    
    CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, width, height), cImageRef);
    CGContextRelease(context);
    
    return buffer;
}

/// 正常点的顶点数组,左侧为顶点坐标信息,右侧为色值信息
static const GLfloat sNormalVertices[] = {
    // 左上顶点,红色
    -0.8f, 0.8f,    0.9f, 0.1f, 0.1f, 1.0f,
    
    // 左下顶点,绿色
    -0.8f, -0.8f,    0.1f, 0.9f, 0.1f, 1.0f,
    
    // 右上顶点,蓝色
    0.8f, 0.8f,    0.1f, 0.1f, 0.9f, 1.0f,
    
    // 右下角,黄色
    0.8f, -0.8f,    0.95f, 0.8f, 0.15f, 1.0f
};

/// 具有粒子效果的纹理贴图的点的顶点数组,左侧为顶点坐标,中间为纹理坐标,右侧为色值
static const GLfloat sTexturedVertices[] = {
    // 左上顶点,红色
    -0.5f, 0.5f,    0.0f, 0.0f,    0.9f, 0.1f, 0.1f, 1.0f,
    
    // 左下顶点,绿色
    -0.5f, -0.5f,    0.0f, 0.0f,    0.1f, 0.9f, 0.1f, 1.0f,
    
    // 右上顶点,蓝色
    0.5f, 0.5f,    0.0f, 0.0f,    0.1f, 0.1f, 0.9f, 1.0f,
    
    // 右下角,黄色
    0.5f, -0.5f,    0.0f, 0.0f,    0.95f, 0.8f, 0.15f, 1.0f
};

- (void)drawInOpenGLContext:(NSOpenGLContext *)context pixelFormat:(NSOpenGLPixelFormat *)pixelFormat forLayerTime:(CFTimeInterval)timeInterval displayTime:(const CVTimeStamp *)timeStamp
{
    const let scale = self.contentsScale;
    let viewPort = self.frame.size;
    viewPort.width *= scale;
    viewPort.height *= scale;
    
    // 设置视口大小
    glViewport(0, 0, viewPort.width, viewPort.height);
    
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
    
    glVertexPointer(2, GL_FLOAT, 6 * sizeof(GLfloat), sNormalVertices);
    glColorPointer(4, GL_FLOAT, 6 * sizeof(GLfloat), &sNormalVertices[2]);
    
    // 我们这里设置点点大小为32个像素
    glPointParameterf(GL_POINT_SIZE_MIN, 32.0f);
    glPointParameterf(GL_POINT_SIZE_MAX, 32.0f);

    // 设置清除颜色
    glClearColor(0.4f, 0.5f, 0.4f, 1.0f);
    
    // 允许切除面
    glEnable(GL_CULL_FACE);
    // 切除背面
    glCullFace(GL_BACK);
    // 以逆时针作为正面
    glFrontFace(GL_CCW);
    
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 做正交投影变换
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 3.0f);
    
    // 做模型视图变换
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -2.3f);
    
    // 绘制正常顶点
    glDrawArrays(GL_POINTS, 0, 4);
    
    // 开启颜色混合
    glEnable(GL_BLEND);
    
    // 设置混合方程
    // 这里设置当前要绘制上的多边形(src)的alpha为ONE,
    // 因为macOS采用的是pre-multiplied alpha机制,alpha已经与RGBy三个颜色分量相乘了;
    // 原背景色(dst)的alpha值始终为1.0
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    
    // 设置纹理
    if(mTexName == 0)
    {
        glEnable(GL_TEXTURE_2D);
        
        int texWidth, texHeight;
        let imgBuffer = [self allocSourceImageData:[NSImage imageNamed:@"particle.png"] width:&texWidth height:&texHeight];
        
        glGenTextures(1, &mTexName);
        glBindTexture(GL_TEXTURE_2D, mTexName);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        
        // 开启point sprite
        glEnable(GL_POINT_SPRITE);

        // 对纹理环境设置是将纹理与颜色进行混合的关键。
        // 这里将纹理模式由原来的GL_REPLACE改为GL_COMBINE以对输入颜色做混合,
        // 当然,这里不设置GL_TEXTURE_ENV这个参数也没有问题。
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
        // 让OpenGL贯穿整个点对纹理坐标进行插值处理
        glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texWidth, texHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imgBuffer);
        free(imgBuffer);
    }

    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glVertexPointer(2, GL_FLOAT, 8 * sizeof(GLfloat), sTexturedVertices);
    glTexCoordPointer(2, GL_FLOAT, 8 * sizeof(GLfloat), &sTexturedVertices[2]);
    glColorPointer(4, GL_FLOAT, 8 * sizeof(GLfloat), &sTexturedVertices[4]);
    
    // 做正交投影变换
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 3.0f);
    
    // 做模型视图变换
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -2.3f);
    
    // 绘制具有粒子效果的顶点
    glDrawArrays(GL_POINTS, 0, 4);
    
    glFlush();
    
    [context flushBuffer];
}

@end

下面给出无关紧要的UI相关的ViewController.m的代码:

//
//  ViewController.m
//  GLPointSprite
//
//  Created by Zenny Chen on 2019/1/24.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

#import "ViewController.h"
#import "MyGLLayer.h"

@implementation ViewController
{
@private
    
    /// MyGLLayer图层对象
    MyGLLayer *mLayer;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // Do any additional setup after loading the view.
    self.view.wantsLayer = YES;
    
    const let viewSize = self.view.frame.size;
    const let y = viewSize.height - 20.0 - 25.0;
    CGFloat x = 20.0;
    let button = [NSButton buttonWithTitle:@"Show" target:self action:@selector(showButtonClicked:)];
    button.frame = NSMakeRect(x, y, 70.0, 25.0);
    [self.view addSubview:button];
    
    x += button.frame.size.width + 10.0;
    button = [NSButton buttonWithTitle:@"Close" target:self action:@selector(closeButtonClicked:)];
    button.frame = NSMakeRect(x, y, 70.0, 25.0);
    [self.view addSubview:button];
}

- (void)showButtonClicked:(NSButton*)sender
{
    if(mLayer != nil)
        return;
    
    const let viewSize = self.view.frame.size;
    const let x = (viewSize.width - 512.0) * 0.5;
    
    mLayer = MyGLLayer.new;
    mLayer.contentsScale = NSScreen.mainScreen.backingScaleFactor;
    mLayer.frame = CGRectMake(x, 50.0, 512.0, 512.0);
    [self.view.layer addSublayer:mLayer];
    [mLayer release];
}

- (void)closeButtonClicked:(NSButton*)sender
{
    if(mLayer != nil)
    {
        [mLayer removeFromSuperlayer];
        mLayer = nil;
    }
}

- (void)setRepresentedObject:(id)representedObject {
    [super setRepresentedObject:representedObject];

    // Update the view, if already loaded.
}

@end

最后给出OpenGL绘制的效果:


8136508-00e708985472e6b8.png
屏幕快照 2019-01-25 上午12.38.12.png

Metal API中使用Point Sprite

由于Metal API中实现Point Sprite与可编程流水线的OpenGL十分类似,因此这里就不把可编程流水线的OpenGL实现单独拿出来了。当然,对于OpenGL实现而言,我们仍然需要调用glEnable(GL_POINT_SPRITE)来开启Point Sprite功能,并且对具有粒子效果的纹理环境做glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE)这种设置。不过我们不是在主机端来指定点的大小了,而是在GPU的顶点着色器端指定。然后在片段着色器中根据当前片段位于点图元的位置做纹理采样,再与颜色值做插值。而在Metal API中不需要对纹理做任何特殊设置。

因此,这里的要点是:

  1. 在Metal API中,我们需要在顶点着色器输出的对象中包含[[ point_size ]]属性的成员,指示当前点图元的大小。在OpenGL中则是在顶点着色器中设置gl_PointSize内建变量的值即可。它们都是float类型。
  2. 在Metal API中,对片段着色器函数显式指定[[ point_coord ]]属性的形式参数,它指示在一个点图元内,当前片段所处的位置。其类型为float2,并且它的x值与y值范围均在[0.0, 1.0]范围内。而在OpenGL中则是直接通过gl_PointCoord这一内建变量来访问该位置值。
  3. 因为我们可以在片段着色器中确定片段所处点图元的位置,所以我们可以定位当前片段所对应的纹理坐标。从而,我们不需要将表示粒子效果的纹理单独作为一个图片存放,而是可以将它放到一个大纹理中去采坐标。

下面我们将展示Metal API工程相应的代码。
首先给出MyMetalLayer.h头文件内容:

//
//  MyMetalLayer.h
//  MetalPointSprite
//
//  Created by Zenny Chen on 2019/1/23.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

@import QuartzCore;

#ifndef let
#define let     __auto_type
#endif

@interface MyMetalLayer : CAMetalLayer

/// 设置当前Metal Layer
- (void)setup;

@end

然后再给出关键的MyMetalLayer.m源文件:

//
//  MyMetalLayer.m
//  MetalPointSprite
//
//  Created by Zenny Chen on 2019/1/23.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

#import "MyMetalLayer.h"

@import Cocoa;
@import Metal;

@implementation MyMetalLayer
{
@private
    
    /// 命令队列
    id<MTLCommandQueue> mCommandQueue;
    
    /// Metal Shader的库
    id<MTLLibrary> mLibrary;

    /// 普通点的顶点缓存
    id<MTLBuffer> mVertexBuffer;
    
    /// 普通点的偏移缓存
    id<MTLBuffer> mNormalOffsetBuffer;
    
    /// 纹理贴图点的顶点缓存
    id<MTLBuffer> mTexturedVertexBuffer;
    
    /// 纹理贴图点的偏移缓存
    id<MTLBuffer> mTexturedOffsetBuffer;

    /// 纹理对象
    id<MTLTexture> mTexture;
    
    /// 纹理采样器
    id<MTLSamplerState> mSamplerState;
    
    /// 普通顶点渲染流水线
    id<MTLRenderPipelineState> mPipelineState;
    
    /// 纹理贴图点的渲染流水线
    id<MTLRenderPipelineState> mTexturedPipelineState;

    /// 当前所保持的drawable
    id<CAMetalDrawable> mCurrentDrawable;
}

- (instancetype)init
{
    self = super.init;
    
    self.backgroundColor = NSColor.clearColor.CGColor;
    
    // 指定该layer为实体,以优化绘制
    self.opaque = YES;
    
    // 使用默认的RGBA8888像素格式
    self.pixelFormat = MTLPixelFormatBGRA8Unorm;
    
    // 默认为YES,但如果我们要在最后渲染的layer上执行计算,那么我们可以将此参数设置为NO。
    self.framebufferOnly = YES;
    
    return self;
}

/// 普通的四个点的顶点坐标
static const float sNormalVertices[] = {
    // 第一个顶点,颜色为红色
    0.0f, 0.0f,    0.9f, 0.1f, 0.1f, 1.0f,
    
    // 第二个顶点,颜色为蓝色
    0.0f, 0.0f,    0.0f, 0.9f, 0.1f, 1.0f,
    
    // 第三个顶点,颜色为绿色
    0.0f, 0.0f,    0.0f, 0.0f, 0.9f, 1.0f,
    
    // 第四个顶点,颜色为白色
    0.0f, 0.0f,    0.9f, 0.9f, 0.9f, 1.0f
};

/// 普通的四个点的偏移位置
static const float sNormalOffsets[] = {
    // 第一个顶点在左上角
    -0.8f, 0.8f,
    
    // 第二个顶点在左下角
    -0.8f, -0.8f,
    
    // 第三个顶点在右上角
    0.8f, 0.8f,
    
    // 第四个顶点在右下角
    0.8f, -0.8f
};

/// 具有纹理贴图的四个点的顶点坐标
static const float sTexturedVertices[] = {
    // 第一个顶点,颜色为红色
    0.0f, 0.0f,    0.0f, 0.59f,    0.9f, 0.1f, 0.1f, 1.0f,
    
    // 第二个顶点,颜色为蓝色
    0.0f, 0.0f,    0.0f, 0.59f,    0.0f, 0.9f, 0.1f, 1.0f,
    
    // 第三个顶点,颜色为绿色
    0.0f, 0.0f,    0.0f, 0.59f,    0.0f, 0.0f, 0.9f, 1.0f,
    
    // 第四个顶点,颜色为白色
    0.0f, 0.0f,    0.0f, 0.59f,    0.9f, 0.9f, 0.9f, 1.0f
};

/// 具有纹理贴图的四个点的偏移位置
static const float sTexturedOffsets[] = {
    // 第一个顶点在左上角
    -0.5f, 0.5f,
    
    // 第二个顶点在左下角
    -0.5f, -0.5f,
    
    // 第三个顶点在右上角
    0.5f, 0.5f,
    
    // 第四个顶点在右下角
    0.5f, -0.5f
};

- (void)dealloc
{
    [mCommandQueue release];
    
    [mLibrary release];
    
    [mVertexBuffer release];
    [mNormalOffsetBuffer release];
    
    [mTexturedVertexBuffer release];
    [mTexturedOffsetBuffer release];

    [mTexture release];
    [mSamplerState release];
    
    [mPipelineState release];
    [mTexturedPipelineState release];
    
    self.device = nil;
    
    [super dealloc];
}

- (uint8_t*)allocSourceImageData:(NSImage*)image width:(int*)pWidth height:(int*)pHeight
{
    const int width = image.size.width;
    const int height = image.size.height;
    
    if(pWidth != NULL)
        *pWidth = width;
    if(pHeight != NULL)
        *pHeight = height;
    
    const size_t length = width * height * 4;
    
    uint8_t *buffer = malloc(length);
    
    /**
     * [0] => R
     * [1] => G
     * [2] => B
     * [3] => A
     */
    const CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big;
    
    // Initialize the source image buffer
    let colorSpace = CGColorSpaceCreateDeviceRGB();
    let context = CGBitmapContextCreate(buffer,
                                        width,
                                        height,
                                        8,         /* bits per component*/
                                        width * 4,  /* bytes per row */
                                        colorSpace,
                                        bitmapInfo);
    
    CGColorSpaceRelease(colorSpace);
    
    let cImageRef = [image CGImageForProposedRect:NULL context:NULL hints:NULL];
    
    CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, width, height), cImageRef);
    CGContextRelease(context);
    
    return buffer;
}

- (void)setup
{
    // 1、关联Metal设备
    let devices = MTLCopyAllDevices();
    NSLog(@"There are %tu Metal devices available!", devices.count);
    
    let device = devices[0];
    NSLog(@"The current device name: %@", device.name);
    self.device = device;
    
    [devices release];

    // 2、设置对此layer的绘制区域
    self.drawableSize = CGSizeMake(self.frame.size.width * self.contentsScale, self.frame.size.height * self.contentsScale);
    
    // 3、创建命令队列以及库
    mCommandQueue = device.newCommandQueue;
    mLibrary = device.newDefaultLibrary;
    
    // 4、分别获取vertex shader、fragment shader
    let vertexProgram = [mLibrary newFunctionWithName:@"point_vertex"];
    if(vertexProgram == nil)
    {
        NSLog(@"顶点着色器获取失败");
        return;
    }
    
    let fragmentProgram = [mLibrary newFunctionWithName:@"point_fragment"];
    if(fragmentProgram == nil)
    {
        NSLog(@"片段着色器获取失败");
        [vertexProgram release];
        return;
    }
    
    // 5、创建矩形顶点数据缓存
    mVertexBuffer = [device newBufferWithBytes:sNormalVertices length:sizeof(sNormalVertices) options:MTLResourceCPUCacheModeWriteCombined];
    
    mNormalOffsetBuffer = [device newBufferWithBytes:sNormalOffsets length:sizeof(sNormalOffsets) options:MTLResourceCPUCacheModeWriteCombined];
    
    // 6、创建流水线状态
    let descriptor = MTLRenderPipelineDescriptor.new;
    descriptor.sampleCount = 4;     // 我们将使用多重采样抗锯齿(MSAA),每个像素由4个样本构成
    descriptor.vertexFunction = vertexProgram;
    descriptor.fragmentFunction = fragmentProgram;
    // 像素格式要与CAMetalLayer的像素格式一致
    descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
    descriptor.depthAttachmentPixelFormat = MTLPixelFormatInvalid;      // 不启用深度测试
    descriptor.stencilAttachmentPixelFormat = MTLPixelFormatInvalid;    // 不启用stencil

    [vertexProgram release];
    [fragmentProgram release];
    
    mPipelineState = [device newRenderPipelineStateWithDescriptor:descriptor error:NULL];
    [descriptor release];
    
    // 创建具有纹理贴图的点的顶点缓存
    mTexturedVertexBuffer = [device newBufferWithBytes:sTexturedVertices length:sizeof(sTexturedVertices) options:MTLResourceCPUCacheModeWriteCombined];
    
    mTexturedOffsetBuffer = [device newBufferWithBytes:sTexturedOffsets length:sizeof(sTexturedOffsets) options:MTLResourceCPUCacheModeWriteCombined];
    
    // 创建具有纹理贴图的点的顶点、片段程序
    vertexProgram = [mLibrary newFunctionWithName:@"textured_point_vertex"];
    if(vertexProgram == nil)
    {
        NSLog(@"顶点着色器获取失败");
        return;
    }

    fragmentProgram = [mLibrary newFunctionWithName:@"textured_point_fragment"];
    if(fragmentProgram == nil)
    {
        NSLog(@"片段着色器获取失败");
        [vertexProgram release];
        return;
    }
    
    // 创建纹理贴图点的渲染流水线状态
    descriptor = MTLRenderPipelineDescriptor.new;
    descriptor.sampleCount = 4;     // 我们将使用多重采样抗锯齿(MSAA),每个像素由4个样本构成
    descriptor.vertexFunction = vertexProgram;
    descriptor.fragmentFunction = fragmentProgram;
    descriptor.depthAttachmentPixelFormat = MTLPixelFormatInvalid;      // 不启用深度测试
    descriptor.stencilAttachmentPixelFormat = MTLPixelFormatInvalid;    // 不启用stencil
    // 像素格式要与CAMetalLayer的像素格式一致
    descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
    descriptor.colorAttachments[0].blendingEnabled = YES;   // 将飞机渲染流水线设置为允许颜色混合
    descriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
    descriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
    descriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne;
    descriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
    descriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
    descriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;

    [vertexProgram release];
    [fragmentProgram release];

    mTexturedPipelineState = [device newRenderPipelineStateWithDescriptor:descriptor error:NULL];
    [descriptor release];

    // 创建纹理对象
    let textureDesc = MTLTextureDescriptor.new;
    textureDesc.textureType = MTLTextureType2D;
    textureDesc.width = 1024;
    textureDesc.height = 1024;
    textureDesc.pixelFormat = MTLPixelFormatRGBA8Unorm;
    textureDesc.arrayLength = 1;
    textureDesc.mipmapLevelCount = 1;
    
    mTexture = [device newTextureWithDescriptor:textureDesc];
    [textureDesc release];
    
    // 拷贝纹理数据
    int width = 1024;
    int height = 1024;
    let image = [NSImage imageNamed:@"planes_texture.png"];
    let textureData = [self allocSourceImageData:image width:&width height:&height];
    [mTexture replaceRegion:MTLRegionMake2D(0, 0, 1024, 1024) mipmapLevel:0 slice:0 withBytes:textureData bytesPerRow:1024 * 4 bytesPerImage:1024 * 1024 * 4];
    free(textureData);

    // 创建采样对象
    let samplerDesc = MTLSamplerDescriptor.new;
    samplerDesc.minFilter = MTLSamplerMinMagFilterLinear;
    samplerDesc.magFilter = MTLSamplerMinMagFilterLinear;
    samplerDesc.sAddressMode = MTLSamplerAddressModeClampToZero;
    samplerDesc.tAddressMode = MTLSamplerAddressModeClampToZero;
    samplerDesc.mipFilter = MTLSamplerMipFilterNotMipmapped;
    samplerDesc.maxAnisotropy = 1;
    samplerDesc.normalizedCoordinates = YES;
    samplerDesc.lodMinClamp = 0;
    samplerDesc.lodMaxClamp = FLT_MAX;

    mSamplerState = [device newSamplerStateWithDescriptor:samplerDesc];
    [samplerDesc release];
}

/// 获取下一帧的drawble以及下一帧渲染遍描述符
/// @preturn 下一帧的渲染遍描述符
- (MTLRenderPassDescriptor*)nextRenderPass
{
    // 获取下一帧的drawable
    let drawable = self.nextDrawable;
    
    // 设置当前Drawable
    mCurrentDrawable = drawable;
    
    let renderPassDesc = MTLRenderPassDescriptor.renderPassDescriptor;
    
    // 设置颜色属性
    let colorAttachment = renderPassDesc.colorAttachments[0];
    
    // 每一帧都做清除,以获得最好性能
    colorAttachment.loadAction = MTLLoadActionClear;
    colorAttachment.clearColor = MTLClearColorMake(0.4f, 0.5f, 0.4f, 1.0f);
    colorAttachment.storeAction = MTLStoreActionMultisampleResolve;
    // 每次都要更新的属性
    colorAttachment.resolveTexture = drawable.texture;
    
    // 设置MSAA纹理属性,像素格式要与CAMetalLayer的像素格式一致
    let texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:drawable.texture.width height:drawable.texture.height mipmapped: NO];
    texDesc.textureType = MTLTextureType2DMultisample;
    texDesc.resourceOptions = MTLResourceStorageModePrivate;
    texDesc.sampleCount = 4;
    texDesc.usage = MTLTextureUsageRenderTarget;
    let msaaTexture = [self.device newTextureWithDescriptor:texDesc];
    colorAttachment.texture = msaaTexture;
    [msaaTexture release];
    
    return renderPassDesc;
}

- (void)render
{
    /** 以下为Metal渲染 */
    if(mCurrentDrawable != nil)
    {
        NSLog(@"Previous render pass not completed!");
        return;     // 若之前的命令还没执行完,则直接返回
    }
    
    // 1、创建命令缓存并刷新渲染遍
    let commandBuffer = mCommandQueue.commandBuffer;
    [commandBuffer addCompletedHandler:^void(id<MTLCommandBuffer> cmdBuf){
        // 命令全都执行完之后,将mCurrentDrawable置空,表示可以绘制下面一帧
        mCurrentDrawable = nil;
    }];
    
    // 2、创建并设置渲染编码器
    let renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:self.nextRenderPass];
    [renderEncoder setRenderPipelineState:mPipelineState];
    [renderEncoder setVertexBuffer:mVertexBuffer offset:0 atIndex:0];
    [renderEncoder setVertexBuffer:mNormalOffsetBuffer offset:0 atIndex:1];

    // 设置面剔除
    [renderEncoder setCullMode:MTLCullModeBack];
    // 设置顶点逆时针方向为前面,而默认顺时针方向为前面
    [renderEncoder setFrontFacingWinding:MTLWindingCounterClockwise];
    
    // 3、绘制第一个矩形
    [renderEncoder drawPrimitives:MTLPrimitiveTypePoint vertexStart:0 vertexCount:4 instanceCount:1];

    // 设置纹理贴图点的顶点缓存属性
    [renderEncoder setRenderPipelineState:mTexturedPipelineState];
    // 顶点结构体的属性均对应为buffer索引0
    [renderEncoder setVertexBuffer:mTexturedVertexBuffer offset:0 atIndex:0];
    [renderEncoder setVertexBuffer:mTexturedOffsetBuffer offset:0 atIndex:1];
    
    // 设置片段属性
    [renderEncoder setFragmentTexture:mTexture atIndex:0];
    [renderEncoder setFragmentSamplerState:mSamplerState atIndex:0];
    
    // 绘制纹理贴图的点
    [renderEncoder drawPrimitives:MTLPrimitiveTypePoint vertexStart:0 vertexCount:4 instanceCount:1];
    
    // 4、结束渲染编码器,并将命令缓存内容呈现到屏幕上
    [renderEncoder endEncoding];
    
    [commandBuffer presentDrawable:mCurrentDrawable];
    
    // 5、提交命令
    [commandBuffer commit];
}

- (void)layoutSublayers
{
    [super layoutSublayers];
    
    [self render];
}

@end

随后给出这里很重要的Metal shader源文件:

//
//  shaders.metal
//  MetalPointSprite
//
//  Created by Zenny Chen on 2019/1/23.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

#include <metal_stdlib>
using namespace metal;

struct ColorInOut
{
    float4 position [[ position ]];
    half4  color    [[flat]];       // 使用单调着色模式
    float pointSize [[point_size]]; // 由顶点着色器指定点的大小
};

struct TexturedColorInOut
{
    float4 position [[ position ]];
    float2 texCoords;
    half4  color;
    float pointSize [[point_size]]; // 由顶点着色器指定点的大小
};

struct VertexInfo
{
    packed_float2 position;
    packed_float4 colors;
};

struct TexturedVertexInfo
{
    packed_float2 position;
    packed_float2 textureCoords;
    packed_float4 colors;
};

/**
 * ortho projection
 * left = -1.0, right = 1.0, bottom = -1.0, top = 1.0, near = 1.0, far = 3.0
 */
static constexpr constant const float4 projectionColumn1 = float4(1.0f, 0.0f, 0.0f, 0.0f);
static constexpr constant const float4 projectionColumn2 = float4(0.0f, 1.0f, 0.0f, 0.0f);
static constexpr constant const float4 protectionColumn3 = float4(0.0f, 0.0f, -1.0f, -2.0f);
static constexpr constant const float4 projectionColumn4 = float4(0.0f, 0.0f, 0.0f, 1.0f);

/**
 * model view translation
 * x = -0.4, y = 0.0, z = -2.3
 */
static constexpr constant const float4 translationColumn1 = float4(1.0f, 0.0f, 0.0f, 0.0f);
static constexpr constant const float4 translationColumn2 = float4(0.0f, 1.0f, 0.0f, 0.0f);
static constexpr constant const float4 translationColumn3 = float4(0.0f, 0.0f, 1.0f, -2.3f);
static constexpr constant const float4 translationColumn4 = float4(0.0f, 0.0f, 0.0f, 1.0f);

/// normal vertex shader function
vertex struct ColorInOut point_vertex(device struct VertexInfo* vertex_array [[ buffer(0) ]],
                                       constant float *pOffset  [[ buffer(1) ]],
                                       unsigned int vid [[ vertex_id ]])
{
    struct ColorInOut out;
    
    auto in_position = float4(float2(vertex_array[vid].position), 0.0f, 1.0f);
    
    auto projection = float4x4(projectionColumn1, projectionColumn2, protectionColumn3, projectionColumn4);
    
    auto translation = float4x4(translationColumn1, translationColumn2, translationColumn3, translationColumn4);
    
    const auto offset = float2(pOffset[2 * vid + 0], pOffset[2 * vid + 1]);

    translation[0].w = offset.x;
    translation[1].w = offset.y;

    out.position = in_position * ((translation * projection));
    out.color = half4(vertex_array[vid].colors);
    // 设置点的大小为32个像素
    out.pointSize = 32.0f;
    
    return out;
}

// normal fragment shader function
fragment half4 point_fragment(struct ColorInOut in [[stage_in]])
{
    return in.color;
}

/// normal vertex shader function
vertex struct TexturedColorInOut
textured_point_vertex(device struct TexturedVertexInfo* vertex_array [[ buffer(0) ]],
                                      constant float *pOffset  [[ buffer(1) ]],
                                      unsigned int vid [[ vertex_id ]])
{
    struct TexturedColorInOut out;
    
    auto in_position = float4(float2(vertex_array[vid].position), 0.0f, 1.0f);
    
    auto projection = float4x4(projectionColumn1, projectionColumn2, protectionColumn3, projectionColumn4);
    
    auto translation = float4x4(translationColumn1, translationColumn2, translationColumn3, translationColumn4);
    
    const auto offset = float2(pOffset[2 * vid + 0], pOffset[2 * vid + 1]);

    translation[0].w = offset.x;
    translation[1].w = offset.y;

    out.position = in_position * ((translation * projection));
    out.texCoords = vertex_array[vid].textureCoords;
    out.color = half4(vertex_array[vid].colors);
    out.pointSize = 32.0f;
    
    return out;
}

// textured fragment shader
fragment half4 textured_point_fragment(struct TexturedColorInOut in [[stage_in]],
                                       texture2d<float> tex [[ texture(0) ]],
                                       sampler texSampler [[ sampler(0) ]],
                                       float2 pointCoord [[point_coord]])
{
    // 关于 [[point_coord]]:
    // Two-dimensional coordinates indicating where within a point primitive
    // the current fragment is located.
    // They range from 0.0 to 1.0 across the point.
    const auto x = in.texCoords.x + 0.06f * pointCoord.x;
    const auto y = in.texCoords.y + 0.06f * pointCoord.y;
    
    const auto texel = half4(tex.sample(texSampler, float2(x, y)));
    return half4(in.color * texel.a);
}

最后,我们给出UI相关的ViewController.m源代码:

//
//  ViewController.m
//  MetalPointSprite
//
//  Created by Zenny Chen on 2019/1/23.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

#import "ViewController.h"
#import "MyMetalLayer.h"

@implementation ViewController
{
@private
    
    MyMetalLayer *mLayer;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // Do any additional setup after loading the view.
    self.view.wantsLayer = YES;
    
    const let viewSize = self.view.frame.size;
    CGFloat x = 20.0;
    let y = viewSize.height - 20.0 - 25.0;
    let button = [NSButton buttonWithTitle:@"Show" target:self action:@selector(showButtonTouched:)];
    button.frame = NSMakeRect(x, y, 70.0, 25.0);
    [self.view addSubview:button];
    
    x += button.frame.size.width + 20.0;
    button = [NSButton buttonWithTitle:@"Close" target:self action:@selector(closeButtonTouched:)];
    button.frame = NSMakeRect(x, y, 70.0, 25.0);
    [self.view addSubview:button];
}

// MARK: 按钮事件处理

- (void)showButtonTouched:(NSButton*)sender
{
    if(mLayer != nil)
        return;

    const let x = (self.view.frame.size.width - 512.0) * 0.5;
    mLayer = MyMetalLayer.new;
    mLayer.frame = CGRectMake(x, 50.0, 512.0, 512.0);
    mLayer.contentsScale = NSScreen.mainScreen.backingScaleFactor;
    [mLayer setup];
    [self.view.layer addSublayer:mLayer];
    [mLayer release];
}

- (void)closeButtonTouched:(NSButton*)sender
{
    if(mLayer != nil)
    {
        [mLayer removeFromSuperlayer];
        mLayer = nil;
    }
}

- (void)setRepresentedObject:(id)representedObject {
    [super setRepresentedObject:representedObject];

    // Update the view, if already loaded.
}

@end

展示效果图如下:


8136508-d40a37c28b40cccf.png
屏幕快照 2019-01-25 上午1.27.56.png

大家还有神马问题,欢迎在底下评论~

猜你喜欢

转载自blog.csdn.net/weixin_34248705/article/details/86839654