【问题标题】:Rendering MTLTexture on MTKView is not keeping aspect ratio在 MTKView 上渲染 MTLTexture 没有保持纵横比
【发布时间】:2018-06-12 09:26:21
【问题描述】:

我有一个 1080x1920 像素的纹理。我正在尝试在纵横比不同的MTKView 上渲染它。 (即 iPad/iPhone X 全屏)。

这就是我为MTKView 渲染纹理的方式:

private func render(_ texture: MTLTexture, withCommandBuffer commandBuffer: MTLCommandBuffer, device: MTLDevice) {
    guard let currentRenderPassDescriptor = metalView?.currentRenderPassDescriptor,
            let currentDrawable = metalView?.currentDrawable,
            let renderPipelineState = renderPipelineState,
            let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: currentRenderPassDescriptor) else {
                semaphore.signal()
                return
        }

    encoder.pushDebugGroup("RenderFrame")
    encoder.setRenderPipelineState(renderPipelineState)
    encoder.setFragmentTexture(texture, index: 0)
    encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1)
    encoder.popDebugGroup()
    encoder.endEncoding()

    // Called after the command buffer is scheduled
    commandBuffer.addScheduledHandler { [weak self] _ in
        guard let strongSelf = self else {
            return
        }
        strongSelf.didRender(texture: texture)
        strongSelf.semaphore.signal()
    }

    commandBuffer.present(currentDrawable)
    commandBuffer.commit()
}

我希望纹理在UIView 上呈现为.scaleAspectFill,我正在尝试学习Metal,所以我不确定我应该在哪里寻找这个(.metal 文件,管道、视图本身、编码器等)

谢谢!

编辑:这是着色器代码:

#include <metal_stdlib> using namespace metal;

typedef struct {
    float4 renderedCoordinate [[position]];
    float2 textureCoordinate; } TextureMappingVertex;

vertex TextureMappingVertex mapTexture(unsigned int vertex_id [[ vertex_id ]]) {
    float4x4 renderedCoordinates = float4x4(float4( -1.0, -1.0, 0.0, 1.0 ),
                                            float4(  1.0, -1.0, 0.0, 1.0 ),
                                            float4( -1.0,  1.0, 0.0, 1.0 ),
                                            float4(  1.0,  1.0, 0.0, 1.0 ));

    float4x2 textureCoordinates = float4x2(float2( 0.0, 1.0 ),
                                           float2( 1.0, 1.0 ),
                                           float2( 0.0, 0.0 ),
                                           float2( 1.0, 0.0 ));
    TextureMappingVertex outVertex;
    outVertex.renderedCoordinate = renderedCoordinates[vertex_id];
    outVertex.textureCoordinate = textureCoordinates[vertex_id];

    return outVertex; }

fragment half4 displayTexture(TextureMappingVertex mappingVertex [[ stage_in ]],texture2d<float, access::sample> texture [[ texture(0) ]]) {
    constexpr sampler s(address::clamp_to_edge, filter::linear);

    return half4(texture.sample(s, mappingVertex.textureCoordinate));
}

【问题讨论】:

  • 我已经通过几种不同的方式完成了这项工作,要么在 CPU 上生成顶点位置/纹理坐标并将它们传递给顶点着色器,要么在顶点着色器中即时生成它们本身。你能分享一下你目前拥有的着色器代码吗?
  • 我用着色器代码更新了帖子。这东西对我来说仍然有点神秘。我最终做了什么,因为纹理的大小是静态的,我动态计算 metal view 以正确调整自身大小以保持纹理的方面......我知道这不是应该这样做的,但我是卡住了。

标签: ios swift metal metalkit


【解决方案1】:

在处理金属纹理或一般金属时,需要从以下几个方面着手:

  • 您应该考虑到 pointspixels 之间的差异,请参阅文档here。 UIView 子类的frame 属性(因为 MTKView 是其中之一)始终以 points 为单位提供视图的宽度和高度。

  • 从点到实际像素的映射是通过contentScaleFactor 选项控制的。 MTKView 会自动选择具有与您设备的实际像素相匹配的合适纵横比的纹理。例如,iPhone X 上 MTKView 的底层纹理将具有 2436 x 1125 的分辨率(以像素为单位的实际显示大小)。 here 记录了这一点:“MTKView 类自动支持原生屏幕缩放。默认情况下,视图当前可绘制对象的大小始终保证与视图本身的大小相匹配。”

  • 如此处所述,.scaleAspectFill 选项“缩放[s] 内容以填充视图的大小。部分内容可能会被剪裁以填充视图的边界”。您想模拟这种行为。

  • 使用 Metal 渲染只不过是“绘制”到由 MTKView 自动设置的解析纹理。但是,您仍然可以完全控制并且可以通过手动创建纹理并将它们设置在您的 renderPassDescriptor 中来自行完成。但是你现在不需要关心这个。您应该关心的一件事是您希望在解析纹理中渲染解析纹理中的 1080x1920 像素纹理的什么、位置和哪一部分(可能具有不同的纵横比)。我们想要完全填充(“scaleAspectFill”)解析纹理,因此我们将renderedCoordinates 保留在片段着色器中。在整个解析纹理上定义一个矩形,这意味着为解析纹理中的每个像素调用片段着色器。接下来,我们将简单地更改纹理坐标。

  • 让我们将纵横比定义为ratio = width / height,将解析纹理定义为r_tex,将要渲染的纹理定义为tex

  • 所以假设你的解析纹理没有相同的纵横比,有两种可能的情况:

    1. 您要渲染的纹理的纵横比大于解析纹理(Metal 渲染到的纹理)的纵横比,这意味着您要渲染的纹理具有更大的宽度比解析纹理。在这种情况下,我们将坐标的y 值保持原样。纹理坐标的x 值将被更改:

      x_left  = 0 + ((tex.width - r_tex.width) / 2.0)
      x_right = tex_width - ((tex.width - r_tex_width) / 2.0)
      

      这些值必须归一化,因为纹理样本需要从 0 到 1 范围内的坐标:

      x_left  = x_left / tex.width
      x_right = x_right / tex.width
      

      我们有了新的纹理坐标:

      topLeft = float2(x_left,0)
      topRight = float2(x_right,0)
      bottomLeft = float2(x_left,1)
      bottomRight = float2(x_right,1)
      

      这会产生这样的效果,即纹理的顶部或底部不会被剪掉,但左右两侧的一些外部部分会被剪掉,即不可见。

    2. 要渲染的纹理的纵横比小于解析纹理的纵横比。过程与第一种情况相同,但这次我们将更改y 坐标

这应该渲染您的纹理,以便完全填充解析纹理,并且您的纹理的纵横比保持在 x 轴上。维护 y 轴的工作方式类似。此外,您必须检查纹理的哪一侧更大/更小,并将其纳入您的计算中。这将像使用 scaleAspectFill 时一样剪切部分纹理。请注意,上述解决方案未经测试。但我希望它会有所帮助。请务必不时访问Metal Best Practices 文档,这对正确掌握基本概念非常有帮助。玩得开心!

【讨论】:

    【解决方案2】:

    因此,您的顶点着色器非常直接地指示将源纹理拉伸到视口的尺寸。您正在渲染一个填充视口的四边形,因为它的坐标在水平和垂直方向上处于标准化设备坐标系的极值 ([-1, 1])。

    并且您正在将源纹理逐角映射到同一范围内。那是因为您为纹理坐标指定了纹理坐标空间的极值 ([0, 1])。

    有多种方法可以实现您想要的。您可以通过缓冲区将顶点坐标传递给着色器,而不是对它们进行硬编码。这样,您可以在应用程序代码中计算适当的值。您将在渲染目标中计算所需的目标坐标,以 NDC 表示。因此,从概念上讲,类似于left_ndc = (left_pixel / target_width) * 2 - 1 等。

    或者,也可能更简单,您可以将着色器保持原样,并将绘制操作的视口更改为仅针对渲染目标的适当部分。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-01-21
      • 2012-08-25
      • 2018-04-02
      • 2018-03-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-06
      相关资源
      最近更新 更多