Статьи

Создание 3D-эскизов CoreMotion на iPhone с помощью Swift

Я был действительно впечатлен демонстрацией  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.