Metal之MTLBuffer批量加载顶点数量较多的图形渲染
渲染原理
- 本文是基于“Metal渲染绘制三角形”这样顶点较少图形基础之上的延伸, 在渲染三角形的时候, 顶点数据的存储使用的是数组,当顶点传递时通过setVertexBytes(_:length:index:)方法,主要是由于绘制三角形时,所需的顶点只有三个,顶点数据很少,所以可以通过数组存储,此时的数据是存储在CPU中的;
- Metal三角形的渲染绘制请参考:Metal之渲染绘制三角形
- 对于小于4KB(即4096字节)的一次性数据,使用setVertexBytes(:length:index:),如果数据长度超过4KB 或者需要多次使用顶点数据时,需要创建一个MTLBuffer对象,创建的buffer的目的就是为了将顶点数据存储到顶点缓存区,GPU可以直接访问该缓存区获取顶点数据,并且buffer缓存的数据需要通过setVertexBuffer(:offset:index:)方法传递到顶点着色器。
- 当图形的顶点数据较多时, 顶点的传递与存储过程如下:
① Metal -> MTLBuffer -> 缓存区(存储非常多自定义数据,GPU直接访问 -> 显存) -> 存储顶点数据;
② 创建的buffer的目的就是为了将顶点数据存储到顶点缓存区,GPU可以直接访问该缓存区获取顶点数据,并且buffer缓存的数据需要通过 setVertexBuffer(_:offset:index:)方法传递到顶点着色器。
渲染流程
一、Metal文件
metal文件中,在顶点着色函数需要对顶点坐标进行归一化处理,因为顶点数据初始化时使用的是物体坐标。顶点坐标的归一化主要有以下步骤:
- 定义顶点着色器输出
- 初始化输出剪辑空间位置
- 获取当前顶点坐标的xy:主要是因为绘制的图形是2D的,其z都为0
- 将传入的视图大小转换为vector_float2二维向量类型
- 顶点坐标归一化:可以通过一行代码同时分隔两个通道x和y,并执行除法,然后将结果放入输出的x和y通道中,即从像素空间位置转换为裁剪空间位置
#include <metal_stdlib>
// 使用命名空间 Metal
using namespace metal;// 导入Metal shader代码和执行Metal API命令的C代码之间共享的头
#import "YDWShaderTypes.h"// 顶点着色器输出和片段着色器输入
// 结构体
typedef struct {// 处理空间的顶点信息float4 clipSpacePosition [[position]];// 颜色float4 color;} RasterizerData;// 顶点着色函数
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],constant CCVertex *vertices [[buffer(CCVertexInputIndexVertices)]],constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]]) {/*处理顶点数据:1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中2) 将顶点颜色值传递给返回值*/// 定义outRasterizerData out;// 初始化输出剪辑空间位置out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);// 索引到数组位置以获得当前顶点, 位置是在像素维度中指定的float2 pixelSpacePosition = vertices[vertexID].position.xy;// 将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型vector_float2 viewportSize = vector_float2(*viewportSizePointer);// 每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.// 计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);// 把输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.out.color = vertices[vertexID].color;// 完成, 将结构体传递到管道中下一个阶段return out;
}//当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化.
// 片元函数
// [[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.
// 一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.
// 被stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.
fragment float4 fragmentShader(RasterizerData in [[stage_in]]) {// 返回输入的片元颜色return in.color;
}
二、 initWithMetalKitView
主要需要加载metal文件来获取顶点数据
- 获取GPU设备device: 通过视图控制器中初始化render对象时传入的MTKView对象view,利用view来获取GPU的使用权限
_device = mtkView.device;
- 设置绘制纹理的像素格式
mtkView.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;
- 从项目中加载所以的.metal着色器文件
// 从项目中加载所以的.metal着色器文件id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];// 从库中加载顶点函数id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];// 从库中加载片元函数id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
- 配置用于创建管道状态的管道描述符
// 配置用于创建管道状态的管道MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];// 管道名称pipelineStateDescriptor.label = @"Simple Pipeline";// 可编程函数,用于处理渲染过程中的各个顶点pipelineStateDescriptor.vertexFunction = vertexFunction;// 可编程函数,用于处理渲染过程总的各个片段/片元pipelineStateDescriptor.fragmentFunction = fragmentFunction;// 设置管道中存储颜色数据的组件格式pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
- 同步创建并返回渲染管线对象
// 同步创建并返回渲染管线对象NSError *error = NULL;_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptorerror:&error];
- 获取顶点数据
// 获取顶点数据NSData *vertexData = [YDWRenderer generateVertexData];// 创建一个vertex buffer,可以由GPU来读取_vertexBuffer = [_device newBufferWithLength:vertexData.lengthoptions:MTLResourceStorageModeShared];/* 复制vertex data 到vertex buffer 通过缓存区的"content"内容属性访问指针** memcpy(void *dst, const void *src, size_t n);* dst:目的地* src:源内容* n: 长度*/memcpy(_vertexBuffer.contents, vertexData.bytes, vertexData.length);// 计算顶点个数 = 顶点数据长度 / 单个顶点大小_numVertices = vertexData.length / sizeof(CCVertex);
// 顶点数据
+ (nonnull NSData *)generateVertexData {// 正方形 = 三角形+三角形const CCVertex quadVertices[] = {// Pixel 位置, RGBA 颜色{ { -20, 20 }, { 1, 0, 0, 1 } },{ { 20, 20 }, { 1, 0, 0, 1 } },{ { -20, -20 }, { 1, 0, 0, 1 } },{ { 20, -20 }, { 0, 0, 1, 1 } },{ { -20, -20 }, { 0, 0, 1, 1 } },{ { 20, 20 }, { 0, 0, 1, 1 } },};// 行/列 数量const NSUInteger NUM_COLUMNS = 25;const NSUInteger NUM_ROWS = 15;// 顶点个数const NSUInteger NUM_VERTICES_PER_QUAD = sizeof(quadVertices) / sizeof(CCVertex);// 四边形间距const float QUAD_SPACING = 50.0;// 数据大小 = 单个四边形大小 * 行 * 列NSUInteger dataSize = sizeof(quadVertices) * NUM_COLUMNS * NUM_ROWS;// 开辟空间NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize];// 当前四边形CCVertex * currentQuad = vertexData.mutableBytes;// 获取顶点坐标(循环计算)// 行for(NSUInteger row = 0; row < NUM_ROWS; row++) {// 列for(NSUInteger column = 0; column < NUM_COLUMNS; column++) {// 左上角的位置vector_float2 upperLeftPosition;// 计算X,Y 位置.注意坐标系基于2D笛卡尔坐标系,中心点(0,0),所以会出现负数位置upperLeftPosition.x = ((-((float)NUM_COLUMNS) / 2.0) + column) * QUAD_SPACING + QUAD_SPACING/2.0;upperLeftPosition.y = ((-((float)NUM_ROWS) / 2.0) + row) * QUAD_SPACING + QUAD_SPACING/2.0;// 将quadVertices数据复制到currentQuadmemcpy(currentQuad, &quadVertices, sizeof(quadVertices));// 遍历currentQuad中的数据for (NSUInteger vertexInQuad = 0; vertexInQuad < NUM_VERTICES_PER_QUAD; vertexInQuad++) {//修改vertexInQuad中的positioncurrentQuad[vertexInQuad].position += upperLeftPosition;}// 更新索引currentQuad += 6;}}return vertexData;
}
- 创建命令队列
// 创建命令队列_commandQueue = [_device newCommandQueue];
三、drawInMTKView
主要加载顶点缓冲区数据
- 为当前渲染的每个渲染传递创建一个新的命令缓冲区
// 为当前渲染的每个渲染传递创建一个新的命令缓冲区id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];// 指定缓存区名称commandBuffer.label = @"MyCommand";
- 创建渲染描述符
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;// 判断渲染目标是否为空if(renderPassDescriptor != nil) {// 创建渲染命令编码器,这样才可以渲染到somethingid<MTLRenderCommandEncoder> renderEncoder =[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];// 渲染器名称renderEncoder.label = @"MyRenderEncoder";}
- 设置我们绘制的可绘制区域
/*设置绘制的可绘制区域**typedef struct {double originX, originY, width, height, znear, zfar;} MTLViewport;*/[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0}];
- 设置渲染管道
// 设置渲染管道[renderEncoder setRenderPipelineState:_pipelineState];
- 为了从OC代码找发送数据预加载的MTLBuffer 到Metal 顶点着色函数中
// 将_vertexBuffer 设置到顶点缓存区中[renderEncoder setVertexBuffer:_vertexBufferoffset:0atIndex:CCVertexInputIndexVertices];// 将 _viewportSize 设置到顶点缓存区绑定点设置数据[renderEncoder setVertexBytes:&_viewportSizelength:sizeof(_viewportSize)atIndex:CCVertexInputIndexViewportSize];
- 开始绘图
[renderEncoder drawPrimitives:MTLPrimitiveTypeTrianglevertexStart:0vertexCount:_numVertices];
- 结束编码,表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离
[renderEncoder endEncoding];
- 一旦框架缓冲区完成,使用当前可绘制的进度表
[commandBuffer presentDrawable:view.currentDrawable];
- 完成渲染并将命令缓冲区推送到GPU
[commandBuffer commit];
效果展示
完整示例
Metal之MTLBuffer批量加载顶点数量较多的图形渲染
Metal之MTLBuffer批量加载顶点数量较多的图形渲染相关推荐
- Android实现图片的高效批量加载
前言:图片的加载,图片的处理,是Android开发程序员在开发中经常遇到的问题,比如,图片的压缩,图片批量的网络获取等.但是往往处理不当,就会报oom,那么如何解决这一类问题了,在分析了弘扬大牛的图片 ...
- 2021年大数据HBase(十五):HBase的Bulk Load批量加载操作
全网最详细的大数据HBase文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 系列历史文章 HBase的Bulk Load批量加载操作 一.Bulk L ...
- ARM的批量加载/存储指令
批量加载/存储指令格式: LDMXX|STMXX{条件符}{寻址模式}Rb{!},{寄存器列表} 其中XX表示IA/IB/DA/DB. LDMIA/STMIA 访问/存储后地址递增(Increm ...
- Oracle通过OCI批量加载需要注意的问题
ORACLE调用接口(Oracle Call Interface简称OCI)提供了一组可对ORACLE数据库进行存取的接口子例程(函数),通过在第三代程序设计语言(如C语言)中进行调用可达到存取ORA ...
- 【机器视觉】 Halcon批量加载图像
00. 目录 文章目录 00. 目录 01. 概述 02. 开发环境 03. 加载图像文件方式一 04. 加载图像文件方式二 05. 下载 06. 附录 01. 概述 halcon是一款非常不错的视觉 ...
- android 从相册读取多张图片大小,Android优化查询加载大数量的本地相册图片
一.概述 讲解优化查询相册图片之前,我们先来看下PM提出的需求,PM的需求很简单,就是要做一个类似微信的本地相册图片查询控件,主要包含两个两部分: 进入图片选择页面就要显示出手机中所有的照片,包括系统 ...
- vue项目批量加载url文件并打包到zip下载
vue项目批量加载url文件并打包到zip下载 项目里之前是遇到这样一个需求哈,需要根据选择的不同动态批量生成并下载图片, 而且这个图片不是一张一张生成下载,而是要等他选好条件之后, 把对应的图片动态 ...
- Android_优化查询加载大数量的本地相册图片
一.概述 讲解优化查询相册图片之前,我们先来看下PM提出的需求,PM的需求很简单,就是要做一个类似微信的本地相册图片查询控件,主要包含两个两部分: 进入图片选择页面就要显示出手机中所有的照片,包括系统 ...
- insert into select 批量加载出错解决方案
当使用insert into select 批量加载数据的时候,可能会碰到因为某些数据不符合加载条件,而导致整个insert 语句无法执行,全部rollback.这时可以使用DML 错误日志的特性,解 ...
最新文章
- ps aux|grep
- animation与transition区别
- Cell综述:动植物界的微生物群和宿主营养
- 浅析Google技术底蕴
- 颜水成团队开源VOLO:无需额外数据,首次在ImageNet上达到87.1%的精度
- 第三章:3.2 get 请求
- 文本三剑客之grep
- 【美文保存】nosql数据库对比以及如何巧妙利用redis来提高效率?
- FPGA原语使用方法
- stringr | 文本处理方法(Ⅰ-1):字符串处理函数(上)
- 用Arduino剖析PWM脉宽调制
- headless-virtualbox
- Android EditText 不得不说的InputFilter、TextWatcher、ActionMode.Callback、OnEditorActionListener
- 01Postgresql下载安装和配置
- 电脑屏幕为什么没有手机屏幕清晰?
- 脉搏波形分析_国家脉搏2020年美国总统大选的推特分析
- Button控件的使用方法
- 20款国内外免费使用 主流杀毒软件
- TikTok涨粉?参考抖音?账号增粉解析!
- 顺丰科技2018校园招聘在线笔试题
热门文章
- 浅谈 G1 GC 日志格式
- myeclipse打开jsp页面慢或者卡死
- 如何垂直居中一个img
- python with用法
- 项目实践精解:C#核心技术应用开发
- Gitlab上传代码
- 开机动画适配方案_2020 年 4 月前 App 启动画面、屏幕调整需按要求适配,否则存拒审风险!...
- 计算机如何搜索相关文字,搜索引擎:“请输入你要搜索的内容”|你是如何使用搜索引擎的呢?...
- Java黑皮书课后题第2章:2.5(金融应用:计算小费)编写一个程序,读入一笔费用与小费利率,计算小费和总钱数
- JavaScript Tutorial