В 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 здесь . Наслаждайтесь!