渲染原理

  • 本文是基于“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批量加载顶点数量较多的图形渲染相关推荐

  1. Android实现图片的高效批量加载

    前言:图片的加载,图片的处理,是Android开发程序员在开发中经常遇到的问题,比如,图片的压缩,图片批量的网络获取等.但是往往处理不当,就会报oom,那么如何解决这一类问题了,在分析了弘扬大牛的图片 ...

  2. 2021年大数据HBase(十五):HBase的Bulk Load批量加载操作

    全网最详细的大数据HBase文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 系列历史文章 HBase的Bulk Load批量加载操作 一.Bulk L ...

  3. ARM的批量加载/存储指令

    批量加载/存储指令格式: LDMXX|STMXX{条件符}{寻址模式}Rb{!},{寄存器列表} 其中XX表示IA/IB/DA/DB. LDMIA/STMIA    访问/存储后地址递增(Increm ...

  4. Oracle通过OCI批量加载需要注意的问题

    ORACLE调用接口(Oracle Call Interface简称OCI)提供了一组可对ORACLE数据库进行存取的接口子例程(函数),通过在第三代程序设计语言(如C语言)中进行调用可达到存取ORA ...

  5. 【机器视觉】 Halcon批量加载图像

    00. 目录 文章目录 00. 目录 01. 概述 02. 开发环境 03. 加载图像文件方式一 04. 加载图像文件方式二 05. 下载 06. 附录 01. 概述 halcon是一款非常不错的视觉 ...

  6. android 从相册读取多张图片大小,Android优化查询加载大数量的本地相册图片

    一.概述 讲解优化查询相册图片之前,我们先来看下PM提出的需求,PM的需求很简单,就是要做一个类似微信的本地相册图片查询控件,主要包含两个两部分: 进入图片选择页面就要显示出手机中所有的照片,包括系统 ...

  7. vue项目批量加载url文件并打包到zip下载

    vue项目批量加载url文件并打包到zip下载 项目里之前是遇到这样一个需求哈,需要根据选择的不同动态批量生成并下载图片, 而且这个图片不是一张一张生成下载,而是要等他选好条件之后, 把对应的图片动态 ...

  8. Android_优化查询加载大数量的本地相册图片

    一.概述 讲解优化查询相册图片之前,我们先来看下PM提出的需求,PM的需求很简单,就是要做一个类似微信的本地相册图片查询控件,主要包含两个两部分: 进入图片选择页面就要显示出手机中所有的照片,包括系统 ...

  9. insert into select 批量加载出错解决方案

    当使用insert into select 批量加载数据的时候,可能会碰到因为某些数据不符合加载条件,而导致整个insert 语句无法执行,全部rollback.这时可以使用DML 错误日志的特性,解 ...

最新文章

  1. ps aux|grep
  2. animation与transition区别
  3. Cell综述:动植物界的微生物群和宿主营养
  4. 浅析Google技术底蕴
  5. 颜水成团队开源VOLO:无需额外数据,首次在ImageNet上达到87.1%的精度
  6. 第三章:3.2  get 请求
  7. 文本三剑客之grep
  8. 【美文保存】nosql数据库对比以及如何巧妙利用redis来提高效率?
  9. FPGA原语使用方法
  10. stringr | 文本处理方法(Ⅰ-1):字符串处理函数(上)
  11. 用Arduino剖析PWM脉宽调制
  12. headless-virtualbox
  13. Android EditText 不得不说的InputFilter、TextWatcher、ActionMode.Callback、OnEditorActionListener
  14. 01Postgresql下载安装和配置
  15. 电脑屏幕为什么没有手机屏幕清晰?
  16. 脉搏波形分析_国家脉搏2020年美国总统大选的推特分析
  17. Button控件的使用方法
  18. 20款国内外免费使用 主流杀毒软件
  19. TikTok涨粉?参考抖音?账号增粉解析!
  20. 顺丰科技2018校园招聘在线笔试题

热门文章

  1. 浅谈 G1 GC 日志格式
  2. myeclipse打开jsp页面慢或者卡死
  3. 如何垂直居中一个img
  4. python with用法
  5. 项目实践精解:C#核心技术应用开发
  6. Gitlab上传代码
  7. 开机动画适配方案_2020 年 4 月前 App 启动画面、屏幕调整需按要求适配,否则存拒审风险!...
  8. 计算机如何搜索相关文字,搜索引擎:“请输入你要搜索的内容”|你是如何使用搜索引擎的呢?...
  9. Java黑皮书课后题第2章:2.5(金融应用:计算小费)编写一个程序,读入一笔费用与小费利率,计算小费和总钱数
  10. JavaScript Tutorial