Metal 进阶:渲染图片

279 阅读3分钟

前言

大家好,我是一牛,今天我和大家分享的是如何使用 Metal 渲染图片。在上一节我们学会了如何渲染三角形,在这个基础上,渲染一张图片就会变得很容易。

纹理

首先我们先来复习下纹理的定义。纹理(Texture)是计算机图形学和图像处理中的一个重要概念,通常指用来为几何表面添加细节的二维或三维图像数据。在视觉上,纹理可以提供物体表面细节的逼真感,例如木纹、金属反光或粗糙度。在本文中我们通过图片生成纹理。

Metal中我们可以使用MetalKit中的工具类MTKTextureLoader来生成纹理。

nonisolated func createTexture() async -> MTLTexture? {
    let device = MTLCreateSystemDefaultDevice()!
    let loader = MTKTextureLoader(device: device)
    guard let url = Bundle.main.url(forResource: "lena_color", withExtension: "png") else {
        return nil
    }
    let texture = try? await loader.newTexture(URL: url)
    return texture
}

对于简单格式图片,我们可以使用这个工具类生成纹理,对于特殊格式使用这个方法生成纹理可能会失败。

纹理坐标系

也就是说屏幕的左上角对应纹理坐标(0, 0)屏幕右下角对应纹理坐标(1.0,1.0), 屏幕中心点是纹理坐标(0.5,0.5)

大家都知道,Metal的绘制的基本图元是三角形,渲染一张图片我们只需要绘制两个三角形。

let vertices = [
        // 左上角
        Vertex(pixelPosition: [-256,  256], textureCoordinate: [0.0, 0.0]),
        // 左下角
        Vertex(pixelPosition: [-256, -256], textureCoordinate: [0.0, 1.0]),
        // 右下角
        Vertex(pixelPosition: [ 256, -256], textureCoordinate: [1.0, 1.0]),
        // 左上角
        Vertex(pixelPosition: [-256,  256], textureCoordinate: [0.0, 0.0]),
        // 右下角
        Vertex(pixelPosition: [ 256, -256], textureCoordinate: [1.0, 1.0]),
        // 右上角
        Vertex(pixelPosition: [ 256,  256], textureCoordinate: [1.0, 0.0]),
    ]

(-256, 256)是图片的像素坐标,表示图片的左上角,它对应的纹理坐标是(0, 0)。图片像素坐标需要在顶点阶段转换成光栅化阶段需要的归一化坐标。为了演示方便,图片的长度和宽度恰好是256

设置纹理

commanderEncoder?.setFragmentTexture(texture, index: 0)

将生成的纹理交给片元函数处理。

顶点阶段

vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
                           constant Vertex *vertices [[buffer(0)]],
                                   constant uint2 *viewportSizePointer [[buffer(1)]]
                                   ) {
    float2 pixelPosition = vertices[vertexID].pixelPosition;
    float2 viewportSize = float2(*viewportSizePointer);
    RasterizerData out;
    out.position = float4(0.0, 0.0, 0.0, 1.0);
    out.position.xy = pixelPosition / (viewportSize / 2.0);
    out.textureCoordinate = vertices[vertexID].textureCoordinate;
    return out;
}

和上一篇文章不同的是,我们传入了画布的尺寸。[[buffer(1)]]表示我们插槽1传入数据。

commanderEncoder?.setVertexBytes(&viewPortSize, length: MemoryLayout<vector_int2>.stride, index: 1)

我们通过转化我们得到光栅化所需要的归一化坐标,可以看出图片会一直保持在画布的中间。

片元阶段

fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                               texture2d<half> colorTexture [[texture(0)]]
                               ) {
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear);
    const half4 colorSample = colorTexture.sample(textureSampler, in.textureCoordinate);
    return float4(colorSample);
}

sampler 是 Metal Shading Language 中用于定义纹理采样方式的对象。

•mag_filter::linear: 当纹理被放大时,使用线性插值进行采样。

•min_filter::linear: 当纹理被缩小时,使用线性插值进行采样。

总结

通过本文的学习,我们了解了如何在 Metal 中加载纹理、设置纹理以及在着色器中采样纹理数据,最终实现了图片的渲染。相比于渲染简单的几何图形,纹理的引入让渲染内容更加丰富和多样化。下一步,我们将学习离屏渲染。

创作不易,请大家多多点赞收藏!感谢大家!

Demogithub