Я был действительно впечатлен демонстрацией InkScape, о которой я недавно читал в Creative Applications . InkScape — это приложение для Android, которое позволяет пользователям рисовать в трехмерном пространстве, управляемом акселерометром устройства. Это вдохновлено Rhonda, которая предшествует акселерометрам и использует трекбол вместо этого.
Конечно, моей первой мыслью было: « Как я могу сделать это в Swift? ». Я никогда раньше не работал с CoreMotion , так что это была хорошая возможность изучить что-то новое. Моим первым портом захода была эта отличная статья о движении iOS в NSHipster .
Мой план для приложения состоял в том, чтобы сцена SceneKit с камерой, управляемой движением, вращалась вокруг смещенной точки поворота в центре мира SceneKit. С каждым прикосновением Began () я бы создавал новую плоскую рамку в центре экрана, которая совмещалась с камерой, и для touchesMoved () , я бы использовал местоположение касания, чтобы добавить путь, который я нарисовал на CAShapeLayer, который я бы использовал в качестве диффузного материала для вновь созданной геометрии.
Легко! Давайте разберемся с этим:
Создание камеры
Я хотел, чтобы камера всегда указывала и вращалась вокруг центра мира, будучи слегка смещенной относительно нее. В этом могут помочь две вещи: свойство поворота камеры и использование «взгляда на ограничение». Прежде всего, я создаю узел, представляющий центр мира и саму камеру:
let centreNode = SCNNode()
centreNode.position = SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(centreNode)
let camera = SCNCamera()
camera.xFov = 20
camera.yFov = 20
Далее, SCNLookAtConstraint означает, что, как бы я ни переводил камеру, она всегда будет указывать на центр:
let constraint = SCNLookAtConstraint(target: centreNode)
cameraNode.constraints = [constraint]
… и, наконец, установка поворота камеры изменит ее положение, но она будет вращаться вокруг центра мира:
cameraNode.pivot = SCNMatrix4MakeTranslation(0, 0, -cameraDistance)
Обработка iPhone Motion
Далее идет обработка движения iPhone, чтобы повернуть камеру. Помня, что рулон iPhone — это его вращение вдоль оси вперед-назад, а его шаг — это его вращение вдоль оси из стороны в сторону:
… Я буду использовать эти свойства для управления моей камеры х и у углов Эйлера.
Первым шагом является создание экземпляра CMMotionManager и обеспечение его доступности и работы (чтобы этот код не работал на симуляторе):
let motionManager = CMMotionManager()
guard motionManager.gyroAvailable else
{
fatalError("CMMotionManager not available.")
}
Затем я запускаю менеджер движений с небольшим блоком кода, который вызывается при каждом обновлении. Я использую кортеж для хранения начального положения iPhone и просто использую разницу между этим начальным значением и текущим отношением для установки углов Эйлера камеры:
let queue = NSOperationQueue.mainQueue
motionManager.deviceMotionUpdateInterval = 1 / 30
motionManager.startDeviceMotionUpdatesToQueue(queue())
{
(deviceMotionData: CMDeviceMotion?, error: NSError?) in
if let deviceMotionData = deviceMotionData
{
if (self.initialAttitude == nil)
{
self.initialAttitude = (deviceMotionData.attitude.roll,
deviceMotionData.attitude.pitch)
}
self.cameraNode.eulerAngles.y = Float(self.initialAttitude!.roll - deviceMotionData.attitude.roll)
self.cameraNode.eulerAngles.x = Float(self.initialAttitude!.pitch - deviceMotionData.attitude.pitch)
}
}
Рисование в 3D
Поскольку я знаю углы своей камеры, довольно просто выровнять геометрию цели для рисования с помощью метода touchesBegan () — он просто использует то же отношение:
currentDrawingNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 0, chamferRadius: 0))
currentDrawingNode.eulerAngles.x = self.cameraNode.eulerAngles.x
currentDrawingNode.eulerAngles.y = self.cameraNode.eulerAngles.y
В то же время я создаю новый CAShapeLayer, который будет содержать штриховой путь, который следует за пальцем пользователя:
currentDrawingLayer = CAShapeLayer()
let material = SCNMaterial()
material.diffuse.contents = currentDrawingLayer
material.lightingModelName = SCNLightingModelConstant
currentDrawingNode.geometry?.materials = [material]
В touchesMoved () мне нужно преобразовать местоположение в главном виде в местоположение в геометрии. Поскольку эта геометрия имеет размер 1 x 1 (от -0,5 до 0,5 в обоих направлениях), мне нужно преобразовать ее в координаты в моем CAShapeLayer (произвольно установить 512 x 512), чтобы добавить точки к ее пути.
Для этого есть несколько шагов: взяв locationInView () первого элемента в наборе штрихов, я передаю его в hitTest () моей сцены SceneKit. Это возвращает массив SCNHitTestResults для всех геометрий под касанием, которое я фильтрую для текущей геометрии, а затем просто масштабирую локальные координаты результата, чтобы найти координаты текущего CAShapeLayer :
let locationInView = touches.first?.locationInView(view)
if let hitTestResult:SCNHitTestResult = sceneKitView.hitTest(locationInView!, options: nil).filter( { $0.node== currentDrawingNode }).first,
currentDrawingLayer = currentDrawingLayer
{
let drawPath = UIBezierPath(CGPath: currentDrawingLayer.path!)
let newX = CGFloat((hitTestResult.localCoordinates.x + 0.5) * Float(currentDrawingLayerSize))
let newY = CGFloat((hitTestResult.localCoordinates.y + 0.5) * Float(currentDrawingLayerSize))
drawPath.addLineToPoint(CGPoint(x: newX, y: newY))
currentDrawingLayer.path = drawPath.CGPath
}
… и это так!
Исходный код этого проекта доступен здесь, в моем репозитории GitHub. Он был разработан под Xcode 7 beta 5 и протестирован на моем iPhone 6 под управлением iOS 8.4.1.