Статьи

MercurialText: тисненый тип с использованием SceneKit и CIShadedMaterial

В iOS 9 были представлены два новых фильтра Core Image для iOS,  CIHeightFieldFromMask и  CIShadedMaterial , которые вместе позволяют разработчикам создавать трехмерное рельефное изображение из монохромного источника, такого как текст или штриховые рисунки. Источником затенения является изображение полушария, и именно оно определяет внешний вид поверхности. Конечно, для затенения можно использовать внешнее изображение, но, поскольку у нас есть SceneKit для рендеринга 3D, мы можем использовать его и добавить динамичности в тиснение.

Имея это в виду, я представляю  MercurialText , приложение для проверки концепции, которое позволяет пользователям редактировать исходный материал, настраивать освещение и применять этот материал к определенному пользователем тексту в различных шрифтах. CIShadedMaterial делает фантастическую работу по созданию красивой металлической рельефной поверхности.

MercurialText состоит из двух основных классов,  ShadingImageEditor , который позволяет пользователю редактировать материал и его освещение, а также генерирует затеняющее изображение и  TextEditor,  который позволяет пользователю редактировать свой текст, и применяет фильтры Core Image для создания рельефного изображения.

Редактор затенения изображений

Пользовательский интерфейс редактора затененных изображений состоит из представления SceneKit для отображения полусферы и представления таблицы для отображения редактируемых параметров, таких как блеск и положение света. 

Сцена SceneKit состоит из одиночной сферы и четырех огней. Поскольку радиус сферы равен 1, и я использую ортографическую камеру с масштабом орфографии 1, сфера хорошо заполняет кадр. 

Чтобы заполнить табличное представление, я создал массив ParameterGroup, каждый из которых содержит массив дочерних параметров. Параметр интересен (по крайней мере, IMHO) тем, что наряду с фундаментальными свойствами, такими как  имя  и  значение :

    struct Parameter
    {
        let name: String
        let parameterFunction: ParameterFunction
        var value: CGFloat
        let minMax: MinMax

    }

… он также имеет перечисление со связанным значением типа ParameterFunction:

    enum ParameterFunction
    {
        case AdjustLightPosition(index: Int, axis: PositionAxis)
        case AdjustLightHue(index: Int)
        case AdjustLightBrightness(index: Int)
        case AdjustMaterialShininess

    }

… который использует редактор затенения изображения для обновления сцены:

    func updateSceneFromParameter(parameter: Parameter)
    {
        switch parameter.parameterFunction
        {
        case let .AdjustLightPosition(index, axis):
            switch axis
            {
            case .X:
                lights[index].position.x = Float(parameter.value)
            case .Y:
                lights[index].position.y = Float(parameter.value)
            case .Z:
                lights[index].position.z = Float(parameter.value)
            }

        case .AdjustMaterialShininess:
            material.shininess = parameter.value

        case let .AdjustLightHue(index):
            lights[index].hue = parameter.value

        case let .AdjustLightBrightness(index):
            lights[index].brightness = parameter.value
        }

        sceneChanged = true

    }

Всякий раз, когда сцена SceneKit изменяется, редактор, который реализует SCNSceneRendererDelegate, проверяет флаг sceneChanged и, если изменение произошло от жеста пользователя, отправляет UIControlEvents.ValueChanged, который выбирается в  контроллере представления

Чтобы получить доступ к визуализированному изображению, редактор предоставляет конкурирующее свойство image, которое является просто снимком () представления SceneKit:

    var image: UIImage?
    {
        return sceneKitView.snapshot()

    }

Текстовый редактор / Рендерер

Контроллер представления является посредником между редактором затененных изображений и текстовым редактором / средством визуализации. После того, как редактор изображений отправляет действие «измененное значение», контроллер представления устанавливает свойство shadingImage для экземпляра текстового редактора и вызывает createImage ():

    func shadingImageChange()
    {
        textEditor.shadingImage = shadingImageEditor.image

        textEditor.createImage()

    }

Это функция createImage (), которая выполняет всю тяжелую работу. Несколько открывающих охранных инструкций гарантируют, что это еще не рендеринг и необходимые изображения доступны:

    guard !isBusy else
    {
        pendingUpdate = true
        return
    }

    guard let shadingImage = shadingImage, ciShadingImage = CIImage(image: shadingImage) else
    {
        return

    }

Затем я устанавливаю флаг занятости на true и создаю UIImage исходного текста по его метке:

    isBusy = true

    UIGraphicsBeginImageContextWithOptions(CGSize(width: self.label.frame.width,
        height: self.label.frame.height), false, 1)

    label.layer.renderInContext(UIGraphicsGetCurrentContext()!)

    let textImage = UIGraphicsGetImageFromCurrentImageContext()


    UIGraphicsEndImageContext();

Фактическая фильтрация изображений выполняется в фоновом потоке, чтобы пользовательский интерфейс был отзывчивым. Я создаю копии двух своих фильтров Core Image, это, вероятно, не является необходимым в этом проекте, но я думаю, что это, вероятно, лучшая практика в соответствии с этим руководством Apple:

CIContext и  CIImage объекты являются неизменяемыми, что означает, что каждый может быть безопасно разделен между потоками Несколько потоков могут использовать один и тот же CIContext объект GPU или CPU  для визуализации  CIImage объектов. Однако это не относится к  CIFilter объектам, которые могут изменяться. CIFilter Объект не может использоваться безопасно между потоками. Если ваше приложение является многопоточным, каждый поток должен создавать свои собственные  CIFilter объекты. В противном случае ваше приложение может работать неожиданно.

С этими копиями двух фильтров я установил необходимые значения:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0))
    {
        let heightMapFilter = self.heightMapFilter.copy()
        let shadedMaterialFilter = self.shadedMaterialFilter.copy()

        heightMapFilter.setValue(CIImage(image: textImage),
            forKey: kCIInputImageKey)

        shadedMaterialFilter.setValue(heightMapFilter.valueForKey(kCIOutputImageKey),
            forKey: kCIInputImageKey)

        shadedMaterialFilter.setValue(ciShadingImage,
            forKey: "inputShadingImage")


        [...]

… и теперь я готов получить окончательное изображение из фильтра затененных материалов и сгенерировать UIImage:

    let filteredImageData = shadedMaterialFilter.valueForKey(kCIOutputImageKey) as! CIImage
    let filteredImageRef = self.ciContext.createCGImage(filteredImageData,
        fromRect: filteredImageData.extent)


    let finalImage = UIImage(CGImage: filteredImageRef)

Чтобы обеспечить обновление экрана, настройка свойства изображения моего изображения должна происходить в главном потоке. После этого я также вижу, есть ли ожидающее обновление, и повторно запускаю self.createImage, если это так:

    dispatch_async(dispatch_get_main_queue())
    {
        self.imageView.image = finalImage
        self.isBusy = false

        if self.pendingUpdate
        {
            self.pendingUpdate = false

            self.createImage()
        }

    }

Чтобы добиться максимальной производительности, я следовал  рекомендациям Apple,  создал контекст Core Image из контекста EAGL и отключил управление цветом:

    let ciContext = CIContext(EAGLContext: EAGLContext(API: EAGLRenderingAPI.OpenGLES2),
        options: [kCIContextWorkingColorSpace: NSNull()])

В заключении

CIHeightFieldFromMask  и  CIShadedMaterial  делают фантастическую работу по созданию великолепно выглядящих 3D-рендеринга из плоского текста. Выполнение этих фильтров в фоновых потоках уменьшает тот факт, что для их выполнения может потребоваться несколько минут, и пользовательский интерфейс остается отзывчивым. Использование SceneKit вместо внешнего редактора изображений позволяет тонко настраивать конечные результаты.

Как всегда, исходный код этого проекта доступен в  моем репозитории GitHub здесь . Наслаждайтесь!