Передача текстур с типом компонента UInt8 в вычислительный шейдер Metal
У меня есть изображение, которое я генерирую программно, и я хочу отправить это изображение в качестве текстуры в вычислительный шейдер. То, как я генерирую это изображение, - это то, что я вычисляю каждый из компонентов RGBA как UInt8
ценности и объединить их в UInt32
и сохранить его в буфере изображения. Я делаю это с помощью следующего фрагмента кода:
guard let cgContext = CGContext(data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: RGBA32.bitmapInfo) else {
print("Unable to create CGContext")
return
}
guard let buffer = cgContext.data else {
print("Unable to create textures")
return
}
let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)
let heightFloat = Float(height)
let widthFloat = Float(width)
for i in 0 ..< height {
let latitude = Float(i + 1) / heightFloat
for j in 0 ..< width {
let longitude = Float(j + 1) / widthFloat
let x = UInt8(((sin(longitude * Float.pi * 2) * cos(latitude * Float.pi) + 1) / 2) * 255)
let y = UInt8(((sin(longitude * Float.pi * 2) * sin(latitude * Float.pi) + 1) / 2) * 255)
let z = UInt8(((cos(latitude * Float.pi) + 1) / 2) * 255)
let offset = width * i + j
pixelBuffer[offset] = RGBA32(red: x, green: y, blue: z, alpha: 255)
}
}
let coordinateConversionImage = cgContext.makeImage()
где RGBA32
это небольшая структура, которая делает сдвиг и создание UInt32
значение. Это изображение получается хорошо, как я могу преобразовать его в UIImage
и сохранить его в моей библиотеке фотографий.
Проблема возникает, когда я пытаюсь отправить это изображение в качестве текстуры в вычислительный шейдер. Ниже приведен мой код шейдера:
kernel void updateEnvironmentMap(texture2d<uint, access::read> currentFrameTexture [[texture(0)]],
texture2d<uint, access::read> coordinateConversionTexture [[texture(1)]],
texture2d<uint, access::write> environmentMap [[texture(2)]]
uint2 gid [[thread_position_in_grid]])
{
const uint4 pixel = {255, 127, 63, 255};
environmentMap.write(pixel, gid);
}
Проблема с этим кодом в том, что тип моих текстур uint
, который является 32-битным, и я хочу генерировать 32-битные пиксели точно так же, как я делаю на CPU, добавляя 4 8-битных значения. Тем не менее, я не могу сделать это на металле, так как нет byte
типа, что я могу просто сложить вместе и сделать uint32
, Итак, мой вопрос в том, как правильно обрабатывать 2D-текстуры и устанавливать 32-битные пиксели в металлическом вычислительном шейдере?
Бонусный вопрос: Кроме того, я видел пример кодов шейдеров с texture2d<float, access::read>
в качестве входного типа текстуры. Я предполагаю, что это представляет значение между 0.0 и 1.0, но какое преимущество это имеет по сравнению с беззнаковым int со значениями между 0 и 255?
Изменить: чтобы уточнить, выходной текстуры шейдера, environmentMap
, имеет те же свойства (ширина, высота, pixelFormat и т. д.), что и входные текстуры. Почему я думаю, что это противоречит интуитивно понятным тем, что мы устанавливаем uint4
как пиксель, что означает, что он состоит из 4 32-битных значений, тогда как каждый пиксель должен быть 32-битным. С этим текущим кодом, {255, 127, 63, 255}
имеет тот же результат, что и {2550, 127, 63, 255}
Это означает, что значения каким-то образом фиксируются в диапазоне 0-255 перед записью в выходную текстуру. Но это крайне нелогично.
1 ответ
В игре немного больше магии, чем вы, кажется, знакомы, поэтому я попытаюсь объяснить.
Прежде всего, в дизайне существует слабая связь между форматом хранения текстур в металле и типом, который вы получаете, когда читаете / пробуете. Вы можете иметь текстуру в .bgra8Unorm
отформатировать, когда при выборке через текстуру, связанную как texture2d<float, access::sample>
даст вам float4
с его компонентами в порядке RGBA. Преобразование из этих упакованных байтов в вектор с плавающей запятой со сверкающими компонентами следует хорошо документированным правилам преобразования, как указано в Спецификации языка затенения металла.
Это также тот случай, когда при записи в текстуру, чье хранилище составляет (например) 8 бит на компонент, значения будут ограничены для соответствия нижележащему формату хранения. Это также зависит от того, является ли текстура norm
Тип: если формат содержит norm
значения интерпретируются так, как если бы они указывали значение от 0 до 1. В противном случае прочитанные значения не нормализуются.
Пример: если текстура .bgra8Unorm
и данный пиксель содержит байтовые значения [0, 64, 128, 255]
тогда при чтении в шейдер запрашивает float
компоненты, вы получите [0.5, 0.25, 0, 1.0]
когда вы пробуете это. Напротив, если формат .rgba8Uint
, ты получишь [0, 64, 128, 255]
, Формат хранения текстуры оказывает преобладающее влияние на то, как ее содержимое интерпретируется при выборке.
Я предполагаю, что формат пикселя вашей текстуры что-то вроде .rgba8Unorm
, Если это так, вы можете достичь желаемого, написав свое ядро следующим образом:
kernel void updateEnvironmentMap(texture2d<float, access::read> currentFrameTexture [[texture(0)]],
texture2d<float, access::read> coordinateConversionTexture [[texture(1)]],
texture2d<float, access::write> environmentMap [[texture(2)]]
uint2 gid [[thread_position_in_grid]])
{
const float4 pixel(255, 127, 63, 255);
environmentMap.write(pixel * (1 / 255.0), gid);
}
Напротив, если ваша текстура имеет формат .rgba8Uint
вы получите тот же эффект, написав его так:
kernel void updateEnvironmentMap(texture2d<float, access::read> currentFrameTexture [[texture(0)]],
texture2d<float, access::read> coordinateConversionTexture [[texture(1)]],
texture2d<float, access::write> environmentMap [[texture(2)]]
uint2 gid [[thread_position_in_grid]])
{
const float4 pixel(255, 127, 63, 255);
environmentMap.write(pixel, gid);
}
Я понимаю, что это игрушечный пример, но я надеюсь, что с помощью вышеизложенной информации вы сможете выяснить, как правильно хранить и выбирать значения для достижения того, чего вы хотите.