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;/// 纹理IDGLuint 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 bufferlet 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.0glBlendFunc(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 spriteglEnable(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中不需要对纹理做任何特殊设置。
因此,这里的要点是:
- 在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;/// 当前所保持的drawableid<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 bufferlet 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 shaderlet 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
{// 获取下一帧的drawablelet drawable = self.nextDrawable;// 设置当前DrawablemCurrentDrawable = 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
{
@privateMyMetalLayer *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
展示效果图如下:
大家还有神马问题,欢迎在底下评论~
相关文章:

OpenGL与Metal API的Point Sprite
我们在实际用OpenGL等3D图形渲染API时 点图元 往往用得不多,而在粒子系统中可能也是用一个正方形来绘制一单个粒子。不过在当前大部分3D图形渲染API中都能支持用点图元来绘制一个具有纹理贴图的粒子,从早在OpenGL 1.4开始就能支持了,而在Open…...

从0搭建Vue3组件库(七):使用 gulp 打包组件库并实现按需加载
使用 gulp 打包组件库并实现按需加载 当我们使用 Vite 库模式打包的时候,vite 会将样式文件全部打包到同一个文件中,这样的话我们每次都要全量引入所有样式文件做不到按需引入的效果。所以打包的时候我们可以不让 vite 打包样式文件,样式文件将使用 gulp 进行打包。那么本篇文…...

Python入门教程+项目实战-11.4节: 元组与列表的区别
目录 11.4.1 元组与列表的区别 11.4.2 可变数据类型 11.4.3 元组与列表的区别 11.4.4 知识要点 11.4.5 系统学习python 11.4.1 不可变数据类型 不可变数据类型是指不可以对该数据类型进行修改,即只读的数据类型。迄今为止学过的不可变数据类型有字符串&#x…...

如何做好采购计划和库存管理?
“销售计划不专业且不稳定”“准确性低” “目前只按照过往销量和采购周期做安全库存,但欠货和滞销依然严重” 题主的问题其实蛮有代表性的, 也是传统采购和库存管理常常面临的问题: ① 前后方协作困难 采购/销售/财务工作相互独立&#x…...

客户管理系统的作用有哪些?
阅读本文您将了解:1.客户管理系统的作用;2.客户管理系统软件怎么用;3.客户管理的注意事项。 一、客户管理系统的作用 客户是企业的重要财富,因此客户管理是企业发展过程中至关重要的一部分,那么客户管理怎么做&#…...

Sqlmap手册—史上最全
Sqlmap手册—史上最全 一.介绍 开源的SQL注入漏洞检测的工具,能够检测动态页面中的get/post参数,cookie,http头,还能够查看数据,文件系统访问,甚至能够操作系统命令执行。 检测方式:布尔盲注、…...

《花雕学AI》13:早出对策,积极应对ChatGPT带来的一系列风险和挑战
ChatGPT是一款能和人类聊天的机器人,它可以学习和理解人类语言,也可以帮人们做一些工作,比如翻译、写文章、写代码等。ChatGPT很强大,让很多人感兴趣,也让很多人担心。 使用ChatGPT有一些风险,比如数据的质…...

windows开机启动软件、执行脚本,免登录账户
文章目录 前言一、打开任务计划程序1.我电脑上的是点搜索“任务计划程序”,可能每个电脑的搜索按钮不一样,自行查找2.打开后应该是长这样的 二、创建文件夹1.点击任务计划程序库、右键选择新建文件夹2.名字顺便,点击确定3.创建后如图、点击目…...
Rocky Linux 8 安装实时内核
【方法一:yum 安装】 在 /etc/yum.repos.d 目录下新建一个Rocky8-rt.repo安装rt内核和相关工具$ sudo yum install kernel-rt重启系统$ sudo reboot【方法二:rpm安装】 查看系统内核版本$ uname -a 4.18.0-425.3.1.el8_7.x86_64根据系统内核版本下载实…...

数据预处理(Data Preprocessing)
Data Preprocessing 前言Why preprocess?Major Tasks in Data PreprocessingData CleaningIncomplete (Missing) DataWhat to Consider When Handling Missing Data?MCARMARMNAR How to Handle Missing Data - ImputationMore on ImputationEven More on ImputationPreproces…...
MySQL数据库——MySQL WHERE:条件查询数据
在 MySQL 中,如果需要有条件的从数据表中查询数据,可以使用 WHERE 关键字来指定查询条件。 使用 WHERE 关键字的语法格式如下: WHERE 查询条件 查询条件可以是: 带比较运算符和逻辑运算符的查询条件带 BETWEEN AND 关键字的查询…...

【JavaEE初阶】多线程(三)volatile wait notify关键字 单例模式
摄影分享~~ 文章目录 volatile关键字volatile能保证内存可见性 wait和notifywaitnotifynotifyAllwait和sleep的区别小练习 多线程案例单例模式饿汉模式懒汉模式 volatile关键字 volatile能保证内存可见性 import java.util.Scanner;class MyCounter {public int flag 0; }p…...

git把一个分支上的某次修改同步到另一个分支上,并解决git cherry-pick 冲突
背景 我们在工作中经常会碰到好几个同事共同在一个分支上开发,一个项目同时会有好几个分支,我们在feature上开发的功能时,有可能某个需求需要提前发布,这个时候我们已经在feature A上开发完成,但是需要在master上发布…...

S32K3系列单片机开发笔记(SIUL是什么/配置引脚复用的功能·)
前言 今天花时间看了一下,SIUL2模块的相关内容,并参照文档,以及例程作了一些小记录,知道该如何使用这个外设,包括引脚的配置,中断配置,以及常用函数的使用等,但对其中的一些细节还需…...

Linux没网络的情况下快速安装依赖或软件(挂载本地yum仓库源(Repository))
一、上传iso系统镜像: 上传和系统同一版本、同一位数(32bit:i686或i386/64bit:x86_64)的系统,不能是Minimal版本,可以是DVD(较全)或everything(最全)。 注&am…...
为了安装pip install pyaudio花费不少时间,坑
记录一下吧: 环境: mac OS Monterey 12.6.5 pyaudio是没有mac下的whl, 通过pip安装是需要进行编译的,我原来系统的是/usr/local/bin/clang 15.0.0版本,然后调用的C_CLUDE_PATH就是/usr/local/include和下面的c/v1&am…...

第十一章 组合模式
文章目录 前言一、组合模式基本介绍二、UML类图三、完整代码抽象类,所有类都继承此类学校类以父类型引用组合一个学院类学院类以父类型引用组合一个专业类专业类,叶子节点,不能再组合其他类测试类 四、组合模式在JDK集合的源码分析五、组合模…...

LeetCode链表OJ题目 代码+思路分享
目录 删除有序数组中的重复项合并两个有序数组移除链表元素 删除有序数组中的重复项 链接: link 题目描述: 题目思路: 本题使用两个指针dst和src一前一后 相同情况: 如果nums[dst]nums[src],那么src 不相同情况: 此…...

第06讲:为何各大开源框架专宠 SPI 技术?
在此前的内容中,已经详细介绍了 SkyWalking Agent 用到的多种基础技术,例如,Byte Buddy、Java Agent 以及 OpenTracing 中的核心概念。本课时将深入介绍 SkyWalking Agent 以及 OAP 中都会使用到的 SPI 技术。 JDK SPI 机制 SPI(…...
[Unity] No.1 Single单例模式
单例模式 1. 基础 定义:单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地…...

ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...

JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...

使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...

【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制
目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …...

wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...

stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...

【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
Pydantic + Function Calling的结合
1、Pydantic Pydantic 是一个 Python 库,用于数据验证和设置管理,通过 Python 类型注解强制执行数据类型。它广泛用于 API 开发(如 FastAPI)、配置管理和数据解析,核心功能包括: 数据验证:通过…...

C# winform教程(二)----checkbox
一、作用 提供一个用户选择或者不选的状态,这是一个可以多选的控件。 二、属性 其实功能大差不差,除了特殊的几个外,与button基本相同,所有说几个独有的 checkbox属性 名称内容含义appearance控件外观可以变成按钮形状checkali…...