之前写过入门案例 Metal MTKView入门案例 请参考。
我们先来看下本案例的实现效果图:
首先说明一下该案例需要构建的几个类:
-
CQGridsVC
:网格控制器。用于初始化MTKView
和CQGridsRender
。 -
CQGrids.metal
:编写MSL
程序。 -
CQGridsRender
:负责网格渲染的类。 -
CQGrids.h
:用于定义.metal
和CQGridsRender
共用的数据类型。
一、首先看下CQGrids.metal
代码:
#include <metal_stdlib>
using namespace metal;
#import "CQGrids.h"
struct VertexOut {
float4 clipSpacePosition [[position]];
float4 color;
};
vertex VertexOut vertexShaderGrid(uint vertexIndex [[vertex_id]],
constant CQVertexInput *vertexInput [[buffer(CQVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(CQVertexInputIndexViewportSize)]]) {
//取出顶点坐标的xy,该案例中的位置是在像素维度中指定的。
float2 pixelSpacePosition = vertexInput[vertexIndex].position.xy;
//视口
vector_float2 viewportSize = vector_float2 (*viewportSizePointer);
//每个顶点着色器的输出位置在 剪辑空间中(也称为归一化设备坐标空间,NDC坐标空间)。
//剪辑空间的(-1,-1)表示视口的左下角,剪辑空间的(1,1)表示视口的右上角。
//为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口大小的一半。
float2 xy = pixelSpacePosition / (viewportSize / 2.0);
VertexOut out;
out.clipSpacePosition.xy = xy;
out.clipSpacePosition.zw = vector_float2(0.0, 1.0);
out.color = vertexInput[vertexIndex].color;
return out;
}
fragment float4 fragmentShaderGrid(VertexOut in [[stage_in]]) {
return in.color;
}
顶点函数 和 片元函数 的相关名称这里就不做介绍了,不了解的可以参考下Metal MTKView入门案例 。
介绍下 修饰符 constant
常量地址空间: 指向的缓存对象也是从设备内存池分配的,但是它是只读的。
更多介绍在文章 Metal 语言规范 中查看。
因为我们这里传过来的 *vertexInput
顶点数据指针中的 position
是像素空间坐标位置,所以我们需要进行归一化处理。也就是这里传过来*viewportSizePointer
视口大小的指针的原因。
float2 xy = pixelSpacePosition / (viewportSize / 2.0);
这句代码就是对上图坐标系归一化的处理。
二、看下CQGridsRender
代码:
- 2.1加载
metal
资源:
- (void)loadMetal:(MTKView *)mtkView {
//1.
id<MTLLibrary> library = [mtkView.device newDefaultLibrary];
id<MTLFunction> vertexShaderFunction = [library newFunctionWithName:@"vertexShaderGrid"];
id<MTLFunction> fragmentShaderFunction = [library newFunctionWithName:@"fragmentShaderGrid"];
//2.
MTLRenderPipelineDescriptor *renderPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDescriptor.label = @"GridsRenderPipelineDescriptor";
renderPipelineDescriptor.vertexFunction = vertexShaderFunction;
renderPipelineDescriptor.fragmentFunction = fragmentShaderFunction;
//设置管道中存储颜色数据的组件格式
renderPipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
//3.
NSError *error;
_renderPipelineState = [_device newRenderPipelineStateWithDescriptor:renderPipelineDescriptor error:&error];
if (!_renderPipelineState) {
NSLog(@"Failed to created pipeline state, error %@", error);
return;
}
//4.
NSData *verticesData = [CQGridsRender generateVerticesData];
//5.创建一个vertex buffer,可以由GPU来读取
_vertexBuffer = [_device newBufferWithLength:verticesData.length options:MTLResourceStorageModeShared];
/*
memcpy(void *dst, const void *src, size_t n);
dst:目的地
src:源内容
n: 长度
*/
memcpy(_vertexBuffer.contents, verticesData.bytes, verticesData.length);
_totalNumVertices = verticesData.length / sizeof(CQVertexInput);
//6.
_commandQueue = [_device newCommandQueue];
}
- 1.加载 顶点函数 和 片元函数。
- 2.创建渲染管线描述符
MTLRenderPipelineDescriptor
。 - 3.创建渲染管线状态
id<MTLRenderPipelineState>
对象。 - 4.生成顶点数据
verticesData
. - 5.创建
id<MTLBuffer>
顶点缓冲区。 - 6.创建命令队列
id<MTLCommandQueue>
这里我们重点看下第4步生 成顶点数据verticesData
:
- 2.2生成顶点数据:
+ (nonnull NSData *)generateVerticesData {
const CQVertexInput gridVertices[] =
{
// Pixel 位置, RGBA 颜色
{ { -20.0, 20.0 }, { 1.0, 0.0, 0.6, 1.0 } },
{ { 20.0, 20.0 }, { 1.0, 0.0, 0.6, 1.0 } },
{ { -20.0, -20.0 }, { 1.0, 0.0, 0.6, 1.0 } },
{ { 20.0, -20.0 }, { 0.0, 1.0, 0.0, 1.0 } },
{ { -20.0, -20.0 }, { 0.0, 1.0, 0.0, 1.0 } },
{ { 20.0, 20.0 }, { 0.0, 1.0, 0.0, 1.0 } },
};
const NSInteger columNum = 25;
const NSInteger rowNum = 15;
const float gridSpacing = 50.0;//四边形间距
//单个网格顶点个数
const NSInteger singleGridVertexNum = sizeof(gridVertices) / sizeof(CQVertexInput);
//所有网格顶点数据占用内存大小
NSUInteger gridsDataSize = sizeof(gridVertices) * columNum * rowNum;
NSMutableData *verticsData = [[NSMutableData alloc] initWithLength:gridsDataSize];
CQVertexInput * currentGrid = verticsData.mutableBytes;
//获取顶点坐标
for(NSUInteger rowIndex = 0; rowIndex < rowNum; rowIndex++) {
for(NSUInteger columnIndex = 0; columnIndex < columNum; columnIndex++) {
vector_float2 upperLeftPosition;//左上角的位置
//计算 X,Y 位置,注意坐标系基于2D笛卡尔坐标系 中心点(0,0),所以会出现负数位置。
upperLeftPosition.x = ((-((float)columNum) / 2.0) + columnIndex) * gridSpacing + gridSpacing/2.0;
upperLeftPosition.y = ((-((float)rowNum) / 2.0) + rowIndex) * gridSpacing + gridSpacing/2.0;
//将gridVertices数据复制到currentGrid
memcpy(currentGrid, &gridVertices, sizeof(gridVertices));
//遍历currentGrid中的数据
for (NSUInteger vertexInGrid = 0; vertexInGrid < singleGridVertexNum; vertexInGrid++)
{
//修改vertexInGrid中的position
currentGrid[vertexInGrid].position += upperLeftPosition;
}
//更新索引
currentGrid += 6;
}
}
return verticsData;
}
- 该段代码中顶点坐标数组
gridVertices[]
,类型是
typedef struct {
vector_float2 position;//顶点位置
vector_float4 color;//颜色
}CQVertexInput;
该顶点数组中的position
的值为 像素坐标系 中的坐标,这也就是我们一开始在.metal
文件中归一化的原因。
-
顶点数据的生成逻辑在代码中做了详细介绍,参考注释理解一下。
-
2.3修改视口的大小
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
_viewportSize.x = size.width;
_viewportSize.y = size.height;
}
当视图大小发生变化时,我们这里需要同步修改下视口的大小。
- 2.4绘制
- (void)drawInMTKView:(nonnull MTKView *)view {
//1.
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
commandBuffer.label = @"GridsCommandBuffer";
//2.
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
if (renderPassDescriptor) {
//3.
id<MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
renderCommandEncoder.label = @"GridsRenderCommandEncoder";
MTLViewport viewport = {0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 };
[renderCommandEncoder setViewport:viewport];//视口
[renderCommandEncoder setRenderPipelineState:_renderPipelineState];//渲染管道状态
//将_vertexBuffer 设置到顶点缓存区中
[renderCommandEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:CQVertexInputIndexVertices];
//将 _viewportSize 设置到顶点缓存区绑定点设置数据
[renderCommandEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:CQVertexInputIndexViewportSize];
//4.绘制
[renderCommandEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_totalNumVertices];
//5.
[renderCommandEncoder endEncoding];
[commandBuffer presentDrawable:view.currentDrawable];
}
[commandBuffer commit];
}
- 1.创建命令缓冲区
id<MTLCommandBuffer>
。 - 2.获取渲染过程描述符
MTLRenderPassDescriptor
。一组渲染目标,用作渲染通道生成的像素的输出目标。 - 3.创建渲染命令编码器
id<MTLRenderCommandEncoder>
。 - 4.绘制。
- 5.结束编码
endEncoding
。表示该编码器生成的命令都已完成,并且从MTLCommandBuffer
中分离。 - 6.
presentDrawable:
一旦框架缓冲区完成,使用当前可绘制的进度表。 - 7.
commit
提交命令。完成渲染并将命令缓冲区推送到GPU
。
网友评论