美文网首页
Metal 处理图形渲染和加载图片

Metal 处理图形渲染和加载图片

作者: 大橘猪猪侠 | 来源:发表于2020-08-26 10:41 被阅读0次

这篇文章将介绍如何使用Metal处理顶点数量多的图形和使用Metal去加载图片。

首先我们来实现使用Metal来处理顶点数量多的图形。

在之前我们介绍过如何使用Metal API的使用流程,在这个文章中,有一个案例就是如何使用Metal去实现一个三角形的效果。

在实现三角形效果时,我们设置到顶点缓冲区时使用的是下面的方法:
[setVertexBytes:<#(nonnull const void *)#> length:<#(NSUInteger)#> atIndex:<#(NSUInteger)#>]

而上面的那个方法只能加载少量顶点的数据,因此,在加载大量顶点数据组成的图形时,我们就要使用renderEncoder setVertexBuffer:<#(nullable id<MTLBuffer>)#> offset:<#(NSUInteger)#> atIndex:<#(NSUInteger)#>这个方法来实现.

下面我们先来看一下加载大量顶点的效果图:


Simulator Screen Shot - iPhone SE -2nd generation- - 2020-08-25 at 11.01.29.png

接下来我们来实现一下上面的效果:
实现上面的效果,我们按照流程一步一步来实现:
首先实现初始化工作流程
1、创建Metal文件和加载Metal的类文件
在类中调用Metal文件时,我们会需要进行传值操作,因此,我们需要定义一个.h文件来共享与 shader 和 C 代码

// 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
typedef enum VertexInputIndex{
    //顶点
    VertexInputIndexVertices = 0,
    //视图大小
    VertexInputIndexViewportSize = 1,
}VertexInputIndex;


typedef struct {
    // 像素空间的位置
    // 像素中心点(100,100)
    //float float
    vector_float2 position;
    // RGBA颜色
    //float float float float
    vector_float4 color;
}Vertex;

然后在Metal文件中引入.h的头文件

typedef struct{
    float4 clipSpacePosition [[position]];
    float4 color;
} RasterizerData;

vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],constant Vertex *vertices [[buffer(VertexInputIndexVertices)]],constant vector_uint2 *viewportSizePointer [[buffer(VertexInputIndexViewportSize)]]){
    /*
     处理顶点数据:
     1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
     2) 将顶点颜色值传递给返回值
     */
    
    //定义out
    RasterizerData 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;
}

fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
    //返回输入的片元颜色
    return in.color;
}

在Metal文件中的VertexInputIndexVertices、VertexInputIndexViewportSize、position、color为外界传值给着色器调用,这边利用了一个枚举和结构体来定义值:


typedef enum VertexInputIndex{
    //顶点
    VertexInputIndexVertices = 0,
    //视图大小
    VertexInputIndexViewportSize = 1,
}VertexInputIndex;


typedef struct {
    // 像素空间的位置
    // 像素中心点(100,100)
    //float float
    vector_float2 position;
    // RGBA颜色
    //float float float float
    vector_float4 color;
}Vertex;

2、加载着色器文件,包含顶点函数和片元函数

id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
    //从库中加载顶点函数
    id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
    //从库中加载片元函数
    id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

3、创建管道用于创建管道状态

MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    //管道名称
    pipelineStateDescriptor.label = @"Simple Pipeline";
    //可编程函数,用于处理渲染过程中的各个顶点
    pipelineStateDescriptor.vertexFunction = vertexFunction;
    //可编程函数,用于处理渲染过程总的各个片段/片元
    pipelineStateDescriptor.fragmentFunction = fragmentFunction;
    //设置管道中存储颜色数据的组件格式
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
    
    //4.同步创建并返回渲染管线对象
    NSError *error = NULL;
    _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
                                                             error:&error];
    //判断是否创建成功
    if (!_pipelineState)
    {
        NSLog(@"Failed to created pipeline state, error %@", error);
    }
    

4、获取顶点数据

+ (nonnull NSData *)generateVertexData
{
    //1.正方形 = 三角形+三角形
    const Vertex 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(Vertex);
    //四边形间距
    const float QUAD_SPACING = 50.0;
    //数据大小 = 单个四边形大小 * 行 * 列
    NSUInteger dataSize = sizeof(quadVertices) * NUM_COLUMNS * NUM_ROWS;
    
    //2. 开辟空间
    NSMutableData *vertexData = [[NSMutableData alloc] initWithLength:dataSize];
    //当前四边形
    Vertex * currentQuad = vertexData.mutableBytes;
    
    
    //3.获取顶点坐标(循环计算)
    //行
    for(NSUInteger row = 0; row < NUM_ROWS; row++)
    {
        //列
        for(NSUInteger column = 0; column < NUM_COLUMNS; column++)
        {
            //1.左上角的位置
            vector_float2 upperLeftPosition;
            
            //2.计算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;
            
            //3.将quadVertices数据复制到currentQuad
            memcpy(currentQuad, &quadVertices, sizeof(quadVertices));
            
            //4.遍历currentQuad中的数据
            for (NSUInteger vertexInQuad = 0; vertexInQuad < NUM_VERTICES_PER_QUAD; vertexInQuad++)
            {
                //修改vertexInQuad中的position
                currentQuad[vertexInQuad].position += upperLeftPosition;
            }
            
            //5.更新索引
            currentQuad += 6;
        }
    }
    
    return vertexData;
    
}

5、创建命令队列

_commandQueue = [_device newCommandQueue];

在初始化工作完成后,我们就需要实现MTKViewDelegate两个代理方法
重点部分在于将_vertexBuffer设置到顶点缓冲区和将_viewportSize设置到顶点缓冲区数据


[renderEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:VertexInputIndexVertices];
 

[renderEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:VertexInputIndexViewportSize];

在设置完之后,开始绘制

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_numVertices];       
[renderEncoder endEncoding];      
[commandBuffer presentDrawable:view.currentDrawable];

最后提交:

[commandBuffer commit]; 

实现将加载大量顶点绘制成的图形之后,我们来实现如何使用Metal来加载tga和jpg图片。

同样的先来看一下效果图:

Simulator Screen Shot - iPhone SE -2nd generation- - 2020-08-25 at 11.28.23.png

同样的,加载图片和绘制图形代码有一些区别,首先在着色器代码中我们还需要传入一个纹理索引,在设置纹理对象时,将纹理索引进行赋值。

在需要传值的.h文件中,加入一个索引枚举值

// 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
typedef enum VertexInputIndex
{
    //顶点
    VertexInputIndexVertices     = 0,
    //视图大小
    VertexInputIndexViewportSize = 1,
} VertexInputIndex;

//纹理索引
typedef enum CCTextureIndex
{
    TextureIndexBaseColor = 0
}TextureIndex;

//结构体: 顶点/颜色值
typedef struct
{
    // 像素空间的位置
    // 像素中心点(100,100)
    vector_float2 position;
    // 2D 纹理
    vector_float2 textureCoordinate;
} Vertex;

下面贴出着色器代码

// 顶点着色器输出和片段着色器输入
//结构体
typedef struct
{
    float4 clipSpacePosition [[position]];
    float2 textureCoordinate;
    
} RasterizerData;

//顶点着色函数
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
             constant Vertex *vertexArray [[buffer(VertexInputIndexVertices)]],
             constant vector_uint2 *viewportSizePointer [[buffer(VertexInputIndexViewportSize)]])
{
    //定义out
    RasterizerData out;
    
    //初始化输出剪辑空间位置
    out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);
    
    // 索引到我们的数组位置以获得当前顶点
    // 我们的位置是在像素维度中指定的.
    float2 pixelSpacePosition = vertexArray[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.clipSpacePosition.z = 0.0f;
    out.clipSpacePosition.w = 1.0f;
    
    //把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
    out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
    
    //完成! 将结构体传递到管道中下一个阶段:
    return out;
}

fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                               texture2d<half> colorTexture [[texture(TextureIndexBaseColor)]])
{
    constexpr sampler textureSampler(mag_filter::linear,
                                     min_filter::linear);
    
    const half4 colorSampler = colorTexture.sample(textureSampler,in.textureCoordinate);
    
    return float4(colorSampler);
    
    //返回输入的片元颜色
    //return in.color;
}


接下来,我们先实现加载tga和jpg公共部分代码:

1.首先加载顶点操作


    //1.根据顶点/纹理坐标建立一个MTLBuffer
    static const Vertex quadVertices[] = {
        //像素坐标,纹理坐标
        { {  250,  -250 },  { 1.f, 0.f } },
        { { -250,  -250 },  { 0.f, 0.f } },
        { { -250,   250 },  { 0.f, 1.f } },
        
        { {  250,  -250 },  { 1.f, 0.f } },
        { { -250,   250 },  { 0.f, 1.f } },
        { {  250,   250 },  { 1.f, 1.f } },
        
    };
    
    //2.创建我们的顶点缓冲区,并用我们的Qualsits数组初始化它
    _vertices = [_device newBufferWithBytes:quadVertices
                                     length:sizeof(quadVertices)
                                    options:MTLResourceStorageModeShared];
    //3.通过将字节长度除以每个顶点的大小来计算顶点的数目
    _numVertices = sizeof(quadVertices) / sizeof(Vertex);

2.设置渲染管道操作

//1.创建我们的渲染通道
    //从项目中加载.metal文件,创建一个library
    id<MTLLibrary>defalutLibrary = [_device newDefaultLibrary];
    //从库中加载顶点函数
    id<MTLFunction>vertexFunction = [defalutLibrary newFunctionWithName:@"vertexShader"];
    //从库中加载片元函数
    id<MTLFunction> fragmentFunction = [defalutLibrary newFunctionWithName:@"fragmentShader"];
    
    //2.配置用于创建管道状态的管道
    MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    //管道名称
    pipelineStateDescriptor.label = @"Texturing Pipeline";
    //可编程函数,用于处理渲染过程中的各个顶点
    pipelineStateDescriptor.vertexFunction = vertexFunction;
    //可编程函数,用于处理渲染过程总的各个片段/片元
    pipelineStateDescriptor.fragmentFunction = fragmentFunction;
    //设置管道中存储颜色数据的组件格式
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = ccMTKView.colorPixelFormat;
    
    //3.同步创建并返回渲染管线对象
    NSError *error = NULL;
    _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
    //判断是否创建成功
    if (!_pipelineState)
    {
        NSLog(@"Failed to created pipeline state, error %@", error);
    }
    
    //4.使用_device创建commandQueue
    _commandQueue = [_device newCommandQueue];

3.实现MTKViewDelegate的两个代理方法:
跟上一个加载图形其实是一摸一样的,这边不在写了。
4.接下来实现加载图片操作了,首先加载tga图片,我们先将图片转化为一个对象
这个转化方法:

-(nullable instancetype) initWithTGAFileAtLocation:(nonnull NSURL *)location
{
    self = [super init];
    if(self)
    {
        NSString *fileExtension = location.pathExtension;
        
        //判断文件后缀是否为tga
        if(!([fileExtension caseInsensitiveCompare:@"TGA"] == NSOrderedSame))
        {
            NSLog(@"此Image只加载TGA文件");
            return nil;
            
        }
        
        //定义一个TGA文件的头.
        typedef struct __attribute__ ((packed)) TGAHeader
        {
            uint8_t  IDSize;         // ID信息
            uint8_t  colorMapType;   // 颜色类型
            uint8_t  imageType;      // 图片类型 0=none, 1=indexed, 2=rgb, 3=grey, +8=rle packed
            
            int16_t  colorMapStart;  // 调色板中颜色映射的偏移量
            int16_t  colorMapLength; // 在调色板的颜色数
            uint8_t  colorMapBpp;    // 每个调色板条目的位数
            
            uint16_t xOffset;        // 图像开始右方的像素数
            uint16_t yOffset;        // 图像开始向下的像素数
            uint16_t width;          // 像素宽度
            uint16_t height;         // 像素高度
            uint8_t  bitsPerPixel;   // 每像素的位数 8,16,24,32
            uint8_t  descriptor;     // bits描述 (flipping, etc)
            
        }TGAHeader;
        
        NSError *error;
        
        //将TGA文件中整个复制到此变量中
        NSData *fileData = [[NSData alloc]initWithContentsOfURL:location options:0x0 error:&error];
        
        if(fileData == nil)
        {
            NSLog(@"打开TGA文件失败:%@",error.localizedDescription);
            return nil;
        }
        
        //定义TGAHeader对象
        TGAHeader *tgaInfo = (TGAHeader *)fileData.bytes;
        _width = tgaInfo->width;
        _height = tgaInfo->height;
        
        //计算图像数据的字节大小,因为我们把图像数据存储为/每像素32位BGRA数据.
        NSUInteger dataSize = _width * _height * 4;
        
        if(tgaInfo->bitsPerPixel == 24)
        {
            //Metal是不能理解一个24-BPP格式的图像.所以我们必须转化成TGA数据.从24比特BGA格式到32比特BGRA格式.(类似MTLPixelFormatBGRA8Unorm)
            NSMutableData *mutableData = [[NSMutableData alloc] initWithLength:dataSize];
            
            //TGA规范,图像数据是在标题和ID之后立即设置指针到文件的开头+头的大小+ID的大小.初始化源指针,源代码数据为BGR格式
            uint8_t *srcImageData = ((uint8_t*)fileData.bytes +
                                     sizeof(TGAHeader) +
                                     tgaInfo->IDSize);
            
            //初始化将存储转换后的BGRA图像数据的目标指针
            uint8_t *dstImageData = mutableData.mutableBytes;
            
            //图像的每一行
            for(NSUInteger y = 0; y < _height; y++)
            {
                //对于当前行的每一列
                for(NSUInteger x = 0; x < _width; x++)
                {
                    //计算源和目标图像中正在转换的像素的第一个字节的索引.
                    NSUInteger srcPixelIndex = 3 * (y * _width + x);
                    NSUInteger dstPixelIndex = 4 * (y * _width + x);
                    
                    //将BGR信道从源复制到目的地,将目标像素的alpha通道设置为255
                    dstImageData[dstPixelIndex + 0] = srcImageData[srcPixelIndex + 0];
                    dstImageData[dstPixelIndex + 1] = srcImageData[srcPixelIndex + 1];
                    dstImageData[dstPixelIndex + 2] = srcImageData[srcPixelIndex + 2];
                    dstImageData[dstPixelIndex + 3] = 255;
                }
            }
            _data = mutableData;
        }else
        {
            
            uint8_t *srcImageData = ((uint8_t*)fileData.bytes + sizeof(TGAHeader) + tgaInfo->IDSize);
            
            _data = [[NSData alloc] initWithBytes:srcImageData
                                           length:dataSize];
        }
        
    }
    return self;
    
}

在转化完之后,我们就可以设置纹理了:

 MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc]init];
 textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
//设置纹理的像素尺寸
textureDescriptor.width = image.width;
textureDescriptor.height = image.height;
//使用描述符从设备中创建纹理
_texture = [_device newTextureWithDescriptor:textureDescriptor];
//计算图像每行的字节数
NSUInteger bytesPerRow = 4 * image.width;
/3. 创建MTLRegion 结构体
    MTLRegion region = {
        {0,0,0},
        {image.width,image.height,1}
    };
    
    //4.复制图片数据到texture
    [_texture replaceRegion:region mipmapLevel:0 withBytes:image.data.bytes bytesPerRow:bytesPerRow];

5.实现jpg图片的加载:

-(void)setupTexturePNG
{
    //1.获取图片
    UIImage *image = [UIImage imageNamed:@"kun.jpg"];
    //2.纹理描述符
    MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
    //表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1);
    textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
    //设置纹理的像素尺寸
    textureDescriptor.width = image.size.width;
    textureDescriptor.height = image.size.height;
    
    //3.使用描述符从设备中创建纹理
    _texture = [_device newTextureWithDescriptor:textureDescriptor];

    //MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
    //4. 创建MTLRegion 结构体  [纹理上传的范围]
    MTLRegion region = {{ 0, 0, 0 }, {image.size.width, image.size.height, 1}};
    
    //5.获取图片数据
    Byte *imageBytes = [self loadImage:image];
    
    //6.UIImage的数据需要转成二进制才能上传,且不用jpg、png的NSData
    if (imageBytes) {
        [_texture replaceRegion:region
                        mipmapLevel:0
                          withBytes:imageBytes
                        bytesPerRow:4 * image.size.width];
        free(imageBytes);
        imageBytes = NULL;
    }
    
}

//从UIImage 中读取Byte 数据返回
- (Byte *)loadImage:(UIImage *)image {
    // 1.获取图片的CGImageRef
    CGImageRef spriteImage = image.CGImage;
    
    // 2.读取图片的大小
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
   
    //3.计算图片大小.rgba共4个byte
    Byte * spriteData = (Byte *) calloc(width * height * 4, sizeof(Byte));
    
    //4.创建画布
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    
    //5.在CGContextRef上绘图
    CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
    
    //6.图片翻转过来
    CGRect rect = CGRectMake(0, 0, width, height);
    CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
    CGContextTranslateCTM(spriteContext, 0, rect.size.height);
    CGContextScaleCTM(spriteContext, 1.0, -1.0);
    CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
    CGContextDrawImage(spriteContext, rect, spriteImage);

    //7.释放spriteContext
    CGContextRelease(spriteContext);
    
    return spriteData;
}

案例demo

相关文章

  • Metal 处理图形渲染和加载图片

    这篇文章将介绍如何使用Metal处理顶点数量多的图形和使用Metal去加载图片。 首先我们来实现使用Metal来处...

  • Metal与图形渲染番外篇:不同填充样式的实现

    在Metal与图形渲染二:透明图片的渲染[https://www.jianshu.com/p/5de8fed023...

  • 初识MetalView

    Metal metal是iOS设备的一个底层图形API,功能类似OpenGL,支持图形渲染和GPU通用计算.met...

  • Metal Shader Language

    Metal Shading Language介绍 Metal 着⾊语⾔ 是⽤来编写 3D 图形渲染逻辑 和 并⾏计...

  • Metal语言规范

    Metal Shading Language Metal Shading Language是用来编写 3D图形渲染...

  • 初探Metal

    Metal简介 Metal和OpenGL ES类似的面向底层的图形编程接口,是渲染高级3D图形和执行数据计算使用图...

  • 十九、Metal渲染实例

    一、利用Metal实现渲染图形 加载一个由N个三角形组成的双色四边形 加载流程 在VC中创建渲染Renderer,...

  • iOS metal API(一) - 在GPU上执行计算

    Metal: 使用图形处理器渲染高级三维图形并执行数据并行计算 Download[https://docs-ass...

  • 杂项

    1.GPU 个人理解:GPU是图形处理器(或者 图形加载器),开启后,可以提升像素填充和渲染的效率,提升加载速度,...

  • Metal Shading Language - 语法小结

    Metal Shading Language简述 Metal 着色语言是用来编写3D图形渲染逻辑 和 并行计算核心...

网友评论

      本文标题:Metal 处理图形渲染和加载图片

      本文链接:https://www.haomeiwen.com/subject/aygajktx.html