Статьи

MercurialPaint: шаровое тиснение с металлическим и базовым изображением

В продолжение моего недавнего   эксперимента с MercurialText приведем еще одну реализацию  CIHeightFieldFromMask  и  CIShadedMaterialMercurialPaint . 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 здесь