Core Image 颜色内核的金属着色语言,如何传递 float3 数组

Metal Shading language for Core Image color kernel, how to pass an array of float3

我正在尝试通过为 Core Image 使用金属着色语言从这个 source 移植一些 CIFilter
我有一个由 RGB 结构数组组成的调色板,我想将它们作为参数传递给自定义 CI 彩色图像内核。
RGB 结构被转换为 SIMD3<Float>.

的数组
 static func SIMD3Palette(_ palette: [RGB]) -> [SIMD3<Float>] {
        return palette.map{[=10=].toFloat3()}
    }

内核应该采用 simd_float3 值数组,问题是当我启动过滤器时它告诉我索引 1 处的参数需要一个 NSData.

override var outputImage: CIImage? {
        guard let inputImage = inputImage else
        {
            return nil
        }
         let palette = EightBitColorFilter.palettes[Int(inputPaletteIndex)]
        let extent = inputImage.extent
        let arguments = [inputImage, palette, Float(palette.count)] as [Any]

        let final = colorKernel.apply(extent: extent, arguments: arguments)

        return final
    }

这是内核:

float4 eight_bit(sample_t image, simd_float3 palette[], float paletteSize, destination dest) {
        float dist = distance(image.rgb, palette[0]);
        float3 returnColor = palette[0];
        for (int i = 1; i < floor(paletteSize); ++i) {
            float tempDist = distance(image.rgb, palette[i]);
            if (tempDist < dist) {
                dist = tempDist;
                returnColor = palette[i];
            }
        }
        return float4(returnColor, 1);
    }

我想知道如何将数据缓冲区传递给内核,因为将其转换为 NSData 似乎还不够。
我看到了一些示例,但他们使用的 "full" 着色语言不适用于 Core Image,这是一种 subset 仅用于处理片段的语言。

Core Image 内核似乎不支持指针或数组参数类型。虽然 iOS 似乎有一些东西 13. 来自发行说明:

Metal CIKernel instances support arguments with arbitrarily structured data.

但是,与 Core Image 一样,似乎没有进一步的文档......

但是,您仍然可以使用传递缓冲区数据的“旧方法”,方法是将其包装在 CIImage 中并在内核中对其进行采样。例如:

    let array: [Float] = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
    let data = array.withUnsafeBufferPointer { Data(buffer: [=10=]) }
    let dataImage = CIImage(bitmapData: data, bytesPerRow: data.count, size: CGSize(width: array.count/4, height: 1), format: .RGBAf, colorSpace: nil)

请注意,3 通道图像没有 CIFormat,因为 GPU 不支持它们。因此,您要么必须使用单通道 .Rf 并再次将内核中的值重新打包为 float3,要么向数据添加一些步幅并使用 .RGBAffloat4 分别(我建议这样做,因为它减少了纹理提取)。

当您将该图像传递给内核时,您可能希望将采样模式设置为 nearest,否则在两个像素之间采样时可能会得到内插值:

kernel.apply(..., arguments: [dataImage.samplingNearest(), ...])

在您的 (Metal) 内核中,您可以像使用普通输入图像一样通过 sampler:

评估数据
extern "C" float4 myKernel(coreimage::sampler data, ...) {
    float4 data0 = data.sample(float2(0.5, 0.5)); // data[0]
    float4 data1 = data.sample(float2(1.5, 0.5)); // data[1]
    // ...
}

请注意,我将 0.5 添加到坐标中,以便它们指向数据图像中某个像素的 中间 以避免歧义和插值。

另请注意,您从 sampler 获得的像素值始终有 4 个通道。因此,即使您使用 formate .Rf 创建数据图像,您在采样时也会得到 float4(其他值填充 0.0 用于 G 和 B 以及 1.0 对于 alpha)。在这种情况下,你可以做

float data0 = data.sample(float2(0.5, 0.5)).x;

我做到了,如果答案很好并且还部署到较低的目标,那么结果并不完全符合我的预期。写成字符串的原始内核和上面创建图像作为数据源的方法之间的区别有点大。
没有得到确切的原因,但我作为调色板来源传递的图像在大小和颜色上与创建的图像有点不同(可能是由于色彩空间)。
由于没有关于此声明的文档:

Metal CIKernel instances support arguments with arbitrarily structured data.

我在业余时间尝试了很多,然后想出了这个。
首先是着色器:

   float4 eight_bit_buffer(sampler image, constant simd_float3 palette[], float paletteSize, destination dest) {
        float4 color = image.sample(image.transform(dest.coord()));

        float dist = distance(color.rgb, palette[0]);
        float3 returnColor = palette[0];
        for (int i = 1; i < floor(paletteSize); ++i) {
            float tempDist = distance(color.rgb, palette[i]);
            if (tempDist < dist) {
                dist = tempDist;
                returnColor = palette[i];
            }
        }
        return float4(returnColor, 1);
    }

二次调色板转换成SIMD3<Float>:

 static func toSIMD3Buffer(from palette: [RGB]) -> Data {
        var simd3Palette = SIMD3Palette(palette)
        let size = MemoryLayout<SIMD3<Float>>.size
        let count = palette.count * size
        let palettePointer = UnsafeMutableRawPointer.allocate(
            byteCount: simd3Palette.count * MemoryLayout<SIMD3<Float>>.stride,
            alignment: MemoryLayout<SIMD3<Float>>.alignment)
        let simd3Pointer = simd3Palette.withUnsafeMutableBufferPointer { (buffer) -> UnsafeMutablePointer<SIMD3<Float>> in
            let p = palettePointer.initializeMemory(as: SIMD3<Float>.self,
            from: buffer.baseAddress!,
            count: buffer.count)
            return p
        }
        let data = Data(bytesNoCopy: simd3Pointer, count: count * MemoryLayout<SIMD3<Float>>.stride, deallocator: .free)

        return data
    }

我第一次尝试将 SIMD3 附加到数据对象,但可能由于内存对齐而无法正常工作。 记得释放使用后创建的内存。
希望能帮助到别人。