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主要遵循以下几个要点:
- 我们需要指定点的大小,可以通过
glPointParameterf
接口通过指定GL_POINT_SIZE_MIN
和GL_POINT_SIZE_MAX
这两个参数即可。 - 我们需要显式使用
glEnable(GL_POINT_SPRITE)
来开启Point Sprite功能。 - 在使用粒子效果的纹理时,需要使用
glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE)
对点做纹理元素与像素颜色的插值处理。 - 对于固定功能的图形流水线,我们需要将表示粒子效果的纹理单独作为一张图拿出来,而不能合并到其他图上去做采样。另外我们需要确保纹理大小的最小范围。比如,如果当前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绘制的效果:
屏幕快照 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中不需要对纹理做任何特殊设置。
因此,这里的要点是:
- 在Metal API中,我们需要在顶点着色器输出的对象中包含
[[ point_size ]]
属性的成员,指示当前点图元的大小。在OpenGL中则是在顶点着色器中设置gl_PointSize
内建变量的值即可。它们都是float
类型。 - 在Metal API中,对片段着色器函数显式指定
[[ point_coord ]]
属性的形式参数,它指示在一个点图元内,当前片段所处的位置。其类型为float2
,并且它的x值与y值范围均在[0.0, 1.0]范围内。而在OpenGL中则是直接通过gl_PointCoord
这一内建变量来访问该位置值。 - 因为我们可以在片段着色器中确定片段所处点图元的位置,所以我们可以定位当前片段所对应的纹理坐标。从而,我们不需要将表示粒子效果的纹理单独作为一个图片存放,而是可以将它放到一个大纹理中去采坐标。
下面我们将展示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
展示效果图如下:
屏幕快照 2019-01-25 上午1.27.56.png
大家还有神马问题,欢迎在底下评论~