OpenGL与Metal API的Point Sprite

news2024/12/24 3:30:19

我们在实际用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绘制的效果:

在这里插入图片描述


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;
}

// textured 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

展示效果图如下:

metal

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/455213.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习:基于逻辑回归(Logistic Regression)对股票客户流失预测分析

基于逻辑回归对股票客户流失预测分析 作者&#xff1a;i阿极 作者简介&#xff1a;Python领域新星作者、多项比赛获奖者&#xff1a;博主个人首页 &#x1f60a;&#x1f60a;&#x1f60a;如果觉得文章不错或能帮助到你学习&#xff0c;可以点赞&#x1f44d;收藏&#x1f4c1…

npm install报错

出现这个错误&#xff0c;我百度之后得到的解决方案是&#xff1a; 在node.js安装目录下找到node_modules文件夹并删除 但是当我操作完成之后&#xff0c;却出现了另外一个ERROR&#xff1a; 于是我又还原了原来的node_modules文件夹&#xff0c;于是又报一开始的错 仔细瞅瞅…

Netty 单机百万连接测试

1.Netty框架简介 1.1.Netty简介 netty是jboss提供的一个java开源框架&#xff0c;netty提供异步的、事件驱动的网络应用程序框架和工具&#xff0c;用以快速开发高性能、高可用性的网络服务器和客户端程序。也就是说netty是一个基于nio的编程框架&#xff0c;使用netty可以快…

NFC 学习笔记 5 MFRC522读写器2 NDEF

NDEF简介 NDEF&#xff08;NFC Data Exchange Format&#xff09;是一种标准化的数据格式&#xff0c;用于将数据存储在NFC标签或智能手机中。该格式是NFC论坛定义的&#xff0c;目的是在不同的NFC设备之间交换信息。 NDEF格式可以存储各种类型的数据&#xff0c;例如URL、文本…

面对市场内卷,不同品牌应该如何做客户增长?

后疫情时代&#xff0c;我国新生人口减少、人口老龄化加剧&#xff0c;chatgpt火爆和AI替代论盛行&#xff0c;市场上&#xff0c;口红效应依旧繁荣&#xff0c;消费者的延迟满足、替代性满足成为常见心理&#xff0c;面对宏观的不确定性&#xff0c;人们在消费上更需要确定性的…

github 基础

github 基础 前面讲了 git 的基本使用&#xff0c;这里简单的提一下 github 的基本使用&#xff0c;主要还是 pull 和 push 两个部分。其中 pull 好像有了一些变化&#xff0c;现在似乎是需要 rebase 而不是自动就帮你做了……&#xff1f;不过 rebase 的部分之后再提。 当然…

Vuex实现数据共享

目录 一&#xff1a;index.js的创建 二&#xff1a;index.js的引入 三&#xff1a;Count.vue 四&#xff1a;App.vue的使用 五&#xff1a;mapstate等的使用 五&#xff1a;多组件数据共享&#xff08;模块化编程&#xff09; vc通过dispatch联系actions&#xff0c;acti…

Python小姿势 - ###### 随机选取的知识点:Python日期时间处理

随机选取的知识点&#xff1a;Python日期时间处理 Python日期时间处理&#xff1a;一种更简单的方式 日期和时间处理是许多程序中必不可少的部分。Python提供了一个标准库来处理日期和时间&#xff0c;这个库叫做datetime&#xff0c;它提供了一些类来处理不同的日期和时间格式…

远程访问及控制(SSH)

远程访问及控制&#xff08;SSH&#xff09; 一、SSH远程管理二、SSH服务1、ssh远程登录方式2、scp远程复制3、sftp安全FTP4、sshd配置文件5、ssh密钥对配置5.1 ssh密钥对免交互登录 三、TCP wrappers 访问控制1、**TCP wrappers &#xff08;TCP封套&#xff09;**2、**TCP wr…

会话与会话技术(Cookie)

Web应用中的会话过程指的是一个客户端&#xff08;浏览器&#xff09;与Web服务器之间连续发生的一系列请求和响应过程 为保存会话过程产生的数据&#xff0c;Servlet提供了两个用于保存会话数据的对象&#xff0c;分别是Cookie和Session 1、Cookie对象 Cookie是一种会话技术…

SS524V100 RTL8152B(USB转网卡)驱动移植

目录 前言 1. 内核下 USB Host 配置过程 2. 内核下 RTL8152 驱动配置 3. 重新编译内核 4. 测试USB转网卡 5. 总结 前言 本文主要是描述 SS524V100 对 RTL8152B(百兆网卡) 开发、移植的过程。 1. SS524V100 的 USB 2.0 支持 Host 模式&#xff1b; 2. 内核默认自带驱动…

0环境教你怎么安装配置GPU环境运行车流量检测代码

项目效果&#xff1a; python车流量检测双向车流计数 1、环境配置 1.1 安装显卡加速工具 (1) 安装CUDA和cudnn NVIDIA CUDA 深度神经网络库 (cuDNN) 是经 GPU 加速的深度神经网络基元库。cuDNN 可大幅优化标准例程&#xff08;例如用于前向传播和反向传播的卷积层、池化层、…

4月23日作业

#include <iostream> #include <cstring> using namespace std; class Student //学生类 { private: string name; //姓名 int year; //年龄 double sorce; //分数 public: Student (){} //无参构造 Student(string a,int b,double c):name(a),y…

五分钟学会在微信小程序中使用 vantUI 组件库

前言 我们在开发微信小程序时&#xff0c;设计和实现好用的用户界面无疑是至关重要的一步。但是微信小程序官方自带的 UI 组件库无法满足很多使用场景&#xff0c;这个时候就需要我们使用一些第三方的 UI 组件库。而 vant Weapp 作为一款优秀的前端 UI 组件库&#xff0c;可以帮…

MP长篇综述 | 植物泛基因组及其应用

2022年12月15日&#xff0c;中山大学史俊鹏副教授、中国科学院遗传与发育生物学研究所田志喜研究员、中国农业大学赖锦盛教授和上海师范大学黄学辉教授共同撰文&#xff0c;在Molecular Plant杂志发表了题为“Plant pan-genomics and its applications”的长篇综述。该论文对植…

(Ubuntu22.04 Jammy)安装ROS2 Humble

文章目录 (Ubuntu22.04 Jammy)安装ROS2 (Humble)版本一、设置本地区域二、设置源三、安装ROS2软件包四、环境设置五、测试用例Talker-listener 六、卸载ros2 (Ubuntu22.04 Jammy)安装ROS2 (Humble)版本 提示&#xff1a;以下内容是已经安装了ubuntu22.04 下进行安装ros2 一、设…

iptables防火墙和Firewalld

引言 在 Internet 中&#xff0c;企业通过各种应用系统来为用户提供各种服务&#xff0c;如 Web 网站、电子邮件系统、FTP 服务器、数据库系统等&#xff0c;那么&#xff0c;如何来保护这些服务器&#xff0c;过滤企业不需要的访问甚至是恶意的入侵呢&#xff0c;接下来&#…

设计模式--建造者模式

项目需求 盖房需求 (1) 需要建房子:过程为 打地基 砌墙 封顶 (2) 房子有高正各样的,比如 平房和高楼 建房子的过程虽然都一样 但是要求不要相同的细节 传统方式 public abstract class TraditionBuild {//打地基public abstract void foundation();//砌墙public abstract voi…

Linux进程的fork、exit、wait等函数;区分父子进程;GDB调试多进程

Linux系统中进程可以创建子进程。 1. fork函数&#xff1a;创建新进程 #include<sys/types.h> #include<unistd.h>pid_t fork(void); /* 功能&#xff1a;一个进程创建新进程。原进程为父进程&#xff0c;新进程为子进程。 返回值&#xff1a;成功&#xff1a;子…

SuperMap iObjects Docker打包全攻略

SuperMap iObjects Docker打包全攻略 文章目录 SuperMap iObjects Docker打包全攻略说明开始打包iObjects容器启动容器参考 说明 此教程编写时使用的iObjects版本为 10.2.1 &#xff0c;理论高版本同样支持&#xff0c;具体自测。基础镜像为 Docker 官方 ubuntu:16.04完整版。…