В продолжение моего недавнего эксперимента с MercurialText приведем еще одну реализацию CIHeightFieldFromMask и CIShadedMaterial , MercurialPaint . MercurialPaint — это управляемое Apple Pencil приложение для создания эскизов, которое использует металлические и металлические шейдеры для создания скелетной или каркасной текстуры, за которым следует шаг Core Image для применения эффекта тиснения 3D на основе изображения полусферы, созданного с помощью Scene Kit .
В моем посте MercurialText обсуждаются шаги Core Image и Scene Kit, поэтому эта статья начинается с обсуждения того, как я создаю изображение скаффолда с использованием Metal.
Инициализация Металла
MercurialPaint — это UIView, который содержит металлический комплект MTKView для отображения изображения лесов в полете и стандартный UIImageView для отображения окончательного изображения. Техника, которую я использую для рисования пикселей, заимствована из моего проекта ParticleLab : для каждого движения касания я хочу закрасить 2048 пикселей, произвольно расположенных вокруг места касания. Для этого я создаю несколько переменных, которые будут содержать данные частиц:
private var particlesMemory:UnsafeMutablePointer<Void> = nil
private var particlesVoidPtr: COpaquePointer!
private var particlesParticlePtr: UnsafeMutablePointer<Int>!
private var particlesParticleBufferPtr: UnsafeMutableBufferPointer<Int>!
private var particlesBufferNoCopy: MTLBuffer!
Каждый элемент частицы будет содержать случайное значение, которое шейдер Metal будет использовать в качестве начального числа для своего собственного генератора случайных чисел, который будет определять положение частицы. Итак, после использования волшебной функции posix_memalign () и инициализации указателей и буферов:
posix_memalign(&particlesMemory, alignment, particlesMemoryByteSize)
particlesVoidPtr = COpaquePointer(particlesMemory)
particlesParticlePtr = UnsafeMutablePointer<Int>(particlesVoidPtr)
particlesParticleBufferPtr = UnsafeMutableBufferPointer(start: particlesParticlePtr,
count: particleCount)
Я заполняю указатель буфера случайными значениями:
for index in particlesParticleBufferPtr.startIndex ..< particlesParticleBufferPtr.endIndex
{
particlesParticleBufferPtr[index] = Int(arc4random_uniform(9999))
}
… и создайте новый металлический буфер, чтобы разделить частицы между Swift и Metal:
particlesBufferNoCopy = device.newBufferWithBytesNoCopy(particlesMemory,
length: Int(particlesMemoryByteSize),
options: MTLResourceOptions.StorageModeShared,
deallocator: nil)
Сенсорная обработка
Apple , карандаш может попробовать на 240Гц и touchesMoved будет только когда — либо будет вызываться при максимуме 60Гц, так MercurialPaint использует слившихся прикосновений . Чтобы упростить передачу сенсорных данных между Swift и Metal, я поддерживаю только до четырех объединенных касаний, а в touchesMoved () я отмечаю расположение объединенных касаний и силу первого:
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
{
guard let touch = touches.first,
coalescedTouches = event?.coalescedTouchesForTouch(touch) else
{
return
}
touchForce = touch.type == .Stylus
? Float(touch.force / touch.maximumPossibleForce)
: 0.5
touchLocations = coalescedTouches.map
{
return $0.locationInView(self)
}
}
Когда касания заканчиваются, я сбрасываю места касания в (-1, -1):
touchLocations = [CGPoint](count: 4,
repeatedValue: CGPoint(x: -1, y: -1))
Чтобы упростить передачу данных между Swift и Metal, местоположения касания преобразуются в два отдельных значения vector_int4 — одно для четырех координат «x» и одно для четырех координат «y». Итак, внутри drawInMTKView () делегата представления Metal я создаю буферы для хранения этих данных о местоположении и заполняю их, используя небольшую вспомогательную функцию touchLocationsToVector () :
var xLocation = touchLocationsToVector(.X)
let xLocationBuffer = device.newBufferWithBytes(&xLocation,
length: sizeof(vector_int4),
options: MTLResourceOptions.CPUCacheModeDefaultCache)
var yLocation = touchLocationsToVector(.Y)
let yLocationBuffer = device.newBufferWithBytes(&yLocation,
length: sizeof(vector_int4),
options: MTLResourceOptions.CPUCacheModeDefaultCache)
Сила прикосновения передается Металлу как значение с плавающей запятой.
Mercurial Paint Compute Shader
Compute Shader для создания эшафот изображения является довольно основным материалом. Наряду со списком частиц, он также прошел четыре позиции ‘x’ и четыре ‘y’ и нормированную силу касания.
Из вышеизложенного мы знаем, что если для одного из элементов в позиционных координатах нет слияния, значение будет равно -1, поэтому первая задача шейдера — перебрать векторы и выйти из функции, если ‘x’ или ‘y ‘это -1:
for (int i = 0; i < 4; i++)
{
if (xPosition[i] < 0 || yPosition[i] < 0)
{
return;
}
… затем используйте значение «частицы» в качестве случайного начального числа и создайте случайный угол и радиус, основанный на силе прикосновения:
const float randomAngle = rand(randomSeed + i, xPosition[i], yPosition[i]) * 6.283185;
const float randomRadius = rand(randomSeed + i, yPosition[i], xPosition[i]) * (touchForce * 200);
… и, наконец, нарисуйте пиксель текстуры, используя эти случайно сгенерированные значения:
const int writeAtX = xPosition[i] + int(sin(randomAngle) * randomRadius);
const int writeAtY = yPosition[i] + int(cos(randomAngle) * randomRadius);
outTexture.write(float4(1, 1, 1, 1), uint2(writeAtX, writeAtY));
Эффект метабола с металлическими шейдерами
Как и в моем проекте Globular , я использую размытие по Гауссу и порог, чтобы преобразовать отдельные пиксели, нарисованные шейдером, в изображение более жидкого типа. Поскольку я использую Metal, а не Core Image, я использую Metal Performance Shaders. Эти лениво созданы
lazy var blur: MPSImageGaussianBlur =
{
[unowned self] in
return MPSImageGaussianBlur(device: self.device,
sigma: 3)
}()
lazy var threshold: MPSImageThresholdBinary =
{
[unowned self] in
return MPSImageThresholdBinary(device: self.device,
thresholdValue: 0.5,
maximumValue: 1,
linearGrayColorTransform: nil)
}()
… и после того, как вычислительный шейдер закончил, два фильтра применяются к выходной текстуре и в конечном итоге нацеливаются на текстуру рисованного представления в Metal Kit:
blur.encodeToCommandBuffer(commandBuffer,
sourceTexture: paintingTexture,
destinationTexture: intermediateTexture)
threshold.encodeToCommandBuffer(commandBuffer,
sourceTexture: intermediateTexture,
destinationTexture: drawable.texture)
Шаг тиснения основного изображения
Как я уже упоминал выше, полное описание этапа тиснения см. В моем посте MercurialText . В MercurialPaint я жду окончания касаний, чтобы применить эти фильтры. Единственное реальное отличие состоит в том, что я создаю CIImage из текстуры Drawable представления Metal Kit:
let mercurialImage = CIImage(MTLTexture: drawable.texture, options: nil)
Я также добавил дополнительный фильтр Core Image, CIMaskToAlpha, чтобы использовать его в качестве исходного изображения для фильтра карты высот.
Вывод
Пока это может быть один из моих любимых экспериментальных проектов: он объединяет Scene Kit, Core Image, Metal и Metal Performance Shaders и использует данные силы! Надеемся, что это еще один пример не только смехотворной мощи iPad Pro, Metal и Core Image, но и творческого потенциала, которого можно достичь, комбинируя ряд различных платформ Apple.
Этот проект не является конечным продуктом и, как таковой, усыпан магическими числами.
Как всегда, исходный код этого проекта доступен в моем репозитории GitHub здесь .