Подтвердить что ты не робот

Поверните SCNCamera node, глядя на объект вокруг воображаемой сферы

У меня есть SCNCamera в позиции (30,30,30) с SCNLookAtConstraint на объекте, расположенном в позиции (0,0,0). Я пытаюсь заставить камеру вращаться вокруг объекта на воображаемой сфере с помощью UIPanGestureRecognizer, сохраняя при этом радиус между камерой и объектом. Я предполагаю, что должен использовать кватернионные проекции, но мои математические знания в этой области ужасны. Мои известные переменные - это x & y translation + радиус, который я пытаюсь сохранить. Я написал проект на Swift, но ответ на Objective-C был бы одинаково принят (надеюсь, используя стандартную Cocoa Touch Framework).

Куда:

private var cubeView : SCNView!;
private var cubeScene : SCNScene!;
private var cameraNode : SCNNode!;

Вот мой код для установки сцены:

// setup the SCNView
cubeView = SCNView(frame: CGRectMake(0, 0, self.width(), 175));
cubeView.autoenablesDefaultLighting = YES;
self.addSubview(cubeView);

// setup the scene
cubeScene = SCNScene();
cubeView.scene = cubeScene;

// setup the camera
let camera = SCNCamera();
camera.usesOrthographicProjection = YES;
camera.orthographicScale = 9;
camera.zNear = 0;
camera.zFar = 100;

cameraNode = SCNNode();
cameraNode.camera = camera;
cameraNode.position = SCNVector3Make(30, 30, 30)  
cubeScene.rootNode.addChildNode(cameraNode)

// setup a target object
let box = SCNBox(width: 10, height: 10, length: 10, chamferRadius: 0);
let boxNode = SCNNode(geometry: box)
cubeScene.rootNode.addChildNode(boxNode)

// put a constraint on the camera
let targetNode = SCNLookAtConstraint(target: boxNode);
targetNode.gimbalLockEnabled = YES;
cameraNode.constraints = [targetNode];

// add a gesture recogniser
let gesture = UIPanGestureRecognizer(target: self, action: "panDetected:");
cubeView.addGestureRecognizer(gesture);

А вот код для обработки распознавателя жестов:

private var position: CGPoint!;

internal func panDetected(gesture:UIPanGestureRecognizer) {

    switch(gesture.state) {
    case UIGestureRecognizerState.Began:
        position = CGPointZero;
    case UIGestureRecognizerState.Changed:
        let aPosition = gesture.translationInView(cubeView);
        let delta = CGPointMake(aPosition.x-position.x, aPosition.y-position.y);

        // ??? no idea...

        position = aPosition;
    default:
        break
    }
}

Спасибо!

4b9b3361

Ответ 1

Это может помочь разбить вашу проблему на подзадачи.

Настройка сцены

Во-первых, подумайте о том, как организовать свою сцену, чтобы обеспечить желаемое движение. Вы говорите о перемещении камеры, как будто она привязана к невидимой сфере. Используйте эту идею! Вместо того, чтобы пытаться выработать математику, чтобы установить ваш cameraNode.position в какой-то момент на воображаемой сфере, просто подумайте о том, что вы сделали бы, чтобы переместить камеру, если она была привязана к сфере. То есть просто поверните сферу.

Если вы хотите повернуть сферу отдельно от остальной части содержимого сцены, вы должны прикрепить ее к отдельному node. Конечно, вам действительно не нужно вставлять геометрию шара в вашу сцену. Просто создайте node, чей position концентричен с объектом, на котором вы хотите, чтобы ваша камера выходила на орбиту, затем присоедините камеру к дочернему элементу node этого node. Затем вы можете повернуть этот node для перемещения камеры. Здесь приведена быстрая демонстрация этого, отсутствующая в бизнесе обработки прокрутки:

let camera = SCNCamera()
camera.usesOrthographicProjection = true
camera.orthographicScale = 9
camera.zNear = 0
camera.zFar = 100
let cameraNode = SCNNode()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
cameraNode.camera = camera
let cameraOrbit = SCNNode()
cameraOrbit.addChildNode(cameraNode)
cubeScene.rootNode.addChildNode(cameraOrbit)

// rotate it (I've left out some animation code here to show just the rotation)
cameraOrbit.eulerAngles.x -= CGFloat(M_PI_4)
cameraOrbit.eulerAngles.y -= CGFloat(M_PI_4*3)

Здесь вы видите слева и визуализируете, как это работает справа. Качающаяся сфера равна cameraOrbit, а зеленый конус cameraNode.

camera rotate around cubecamera rotate visualization

Есть пара бонусов к этому подходу:

  • Вам не нужно устанавливать начальную позицию камеры в декартовых координатах. Просто разместите его на любом расстоянии, которое вы хотите по оси z. Так как cameraNode является дочерним элементом node of cameraOrbit, его собственное положение остается постоянным - камера перемещается из-за вращения cameraOrbit.
  • Пока вы просто хотите, чтобы камера была направлена ​​в центр этой воображаемой сферы, вам не нужно ограничение взгляда. Камера указывает на направление -Z в пространстве, в котором он находится - если вы переместите его в направлении + Z, затем поверните родительский node, камера всегда будет указывать на центр родительского node (т.е. центр вращения).

Обработка ввода

Теперь, когда у вас есть сцена, созданная для вращения камеры, поворот входных событий во вращение довольно прост. Насколько легко зависит от того, какой контроль вы выполняете:

  • Ищете вращение дуги? (Это отлично подходит для непосредственных манипуляций, так как вы можете чувствовать, что физически толкаете точку на трехмерном объекте.) Есть несколько вопросов и ответов об этом уже на SO - Большинство из них используют GLKQuaternion. ( UPDATE: Типы GLK являются "сортированными" в Swift 1.2/Xcode 6.3. До этих версий вы можете выполнять свою математику в ObjC через заголовок моста.)
  • Для более простой альтернативы вы можете просто сопоставить оси x и y вашего жестов с углами рыскания и наклона вашего node. Это не так круто, как вращение в дуге, но это довольно легко реализовать - все, что вам нужно сделать, это выработать преобразование точек в радианы, которое покрывает количество вращения, которое вы после.

В любом случае вы можете пропустить часть шаблона распознавания жестов и получить удобное интерактивное поведение, используя вместо этого UIScrollView. (Не то, чтобы не было полезности придерживаться распознавателей жестов - это просто легко реализованная альтернатива.)

Отбросьте один из них поверх SCNView (не заставляя прокручивать другой вид внутри него) и установите его contentSize на кратное его размеру кадра... тогда во время прокрутки вы можете сопоставить contentOffset с ваш eulerAngles:

func scrollViewDidScroll(scrollView: UIScrollView) {
    let scrollWidthRatio = Float(scrollView.contentOffset.x / scrollView.frame.size.width)
    let scrollHeightRatio = Float(scrollView.contentOffset.y / scrollView.frame.size.height)
    cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * scrollWidthRatio
    cameraOrbit.eulerAngles.x = Float(-M_PI) * scrollHeightRatio
}

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

Ответ 2

Эй, я столкнулся с проблемой на днях, и решение, которое я придумал, довольно просто, но работает хорошо.

Сначала я создал свою камеру и добавил ее в свою сцену следующим образом:

    // create and add a camera to the scene
    cameraNode = [SCNNode node];
    cameraNode.camera = [SCNCamera camera];
    cameraNode.camera.automaticallyAdjustsZRange = YES;
    [scene.rootNode addChildNode:cameraNode];

    // place the camera
    cameraNode.position = SCNVector3Make(0, 0, 0);
    cameraNode.pivot = SCNMatrix4MakeTranslation(0, 0, -15); //the -15 here will become the rotation radius

Затем я создал переменную класса CGPoint slideVelocity. И создал a UIPanGestureRecognizer и a и в своем обратном вызове я поставил следующее:

-(void)handlePan:(UIPanGestureRecognizer *)gestureRecognize{
    slideVelocity = [gestureRecognize velocityInView:self.view];
}

Тогда у меня есть этот метод, который называется каждым фреймом. Обратите внимание, что я использую GLKit для математики кватерниона.

-(void)renderer:(id<SCNSceneRenderer>)aRenderer didRenderScene:(SCNScene *)scenie atTime:(NSTimeInterval)time {        
    //spin the camera according the the user swipes
    SCNQuaternion oldRot = cameraNode.rotation;  //get the current rotation of the camera as a quaternion
    GLKQuaternion rot = GLKQuaternionMakeWithAngleAndAxis(oldRot.w, oldRot.x, oldRot.y, oldRot.z);  //make a GLKQuaternion from the SCNQuaternion


    //The next function calls take these parameters: rotationAngle, xVector, yVector, zVector
    //The angle is the size of the rotation (radians) and the vectors define the axis of rotation
    GLKQuaternion rotX = GLKQuaternionMakeWithAngleAndAxis(-slideVelocity.x/viewSlideDivisor, 0, 1, 0); //For rotation when swiping with X we want to rotate *around* y axis, so if our vector is 0,1,0 that will be the y axis
    GLKQuaternion rotY = GLKQuaternionMakeWithAngleAndAxis(-slideVelocity.y/viewSlideDivisor, 1, 0, 0); //For rotation by swiping with Y we want to rotate *around* the x axis.  By the same logic, we use 1,0,0
    GLKQuaternion netRot = GLKQuaternionMultiply(rotX, rotY); //To combine rotations, you multiply the quaternions.  Here we are combining the x and y rotations
    rot = GLKQuaternionMultiply(rot, netRot); //finally, we take the current rotation of the camera and rotate it by the new modified rotation.

    //Then we have to separate the GLKQuaternion into components we can feed back into SceneKit
    GLKVector3 axis = GLKQuaternionAxis(rot);
    float angle = GLKQuaternionAngle(rot);

    //finally we replace the current rotation of the camera with the updated rotation
    cameraNode.rotation = SCNVector4Make(axis.x, axis.y, axis.z, angle);

    //This specific implementation uses velocity.  If you don't want that, use the rotation method above just replace slideVelocity.
    //decrease the slider velocity
    if (slideVelocity.x > -0.1 && slideVelocity.x < 0.1) {
        slideVelocity.x = 0;
    }
    else {
        slideVelocity.x += (slideVelocity.x > 0) ? -1 : 1;
    }

    if (slideVelocity.y > -0.1 && slideVelocity.y < 0.1) {
        slideVelocity.y = 0;
    }
    else {
        slideVelocity.y += (slideVelocity.y > 0) ? -1 : 1;
    }
}

Этот код дает бесконечное вращение арбалета со скоростью, которое, я считаю, является тем, что вы ищете. Кроме того, этот метод не нужен SCNLookAtConstraint. Фактически, это, вероятно, испортит его, поэтому не делайте этого.

Ответ 3

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

var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0

И внедрил свой код поворота следующим образом:

func handlePanGesture(sender: UIPanGestureRecognizer) {
    let translation = sender.translationInView(sender.view!)
    let widthRatio = Float(translation.x) / Float(sender.view!.frame.size.width) + lastWidthRatio
    let heightRatio = Float(translation.y) / Float(sender.view!.frame.size.height) + lastHeightRatio
    self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * widthRatio
    self.cameraOrbit.eulerAngles.x = Float(-M_PI) * heightRatio
    if (sender.state == .Ended) {
        lastWidthRatio = widthRatio % 1
        lastHeightRatio = heightRatio % 1
    }
}

Ответ 4

Возможно, это может быть полезно для читателей.

class GameViewController: UIViewController {

var cameraOrbit = SCNNode()
let cameraNode = SCNNode()
let camera = SCNCamera()


//HANDLE PAN CAMERA
var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0.2
var fingersNeededToPan = 1
var maxWidthRatioRight: Float = 0.2
var maxWidthRatioLeft: Float = -0.2
var maxHeightRatioXDown: Float = 0.02
var maxHeightRatioXUp: Float = 0.4

//HANDLE PINCH CAMERA
var pinchAttenuation = 20.0  //1.0: very fast ---- 100.0 very slow
var lastFingersNumber = 0

override func viewDidLoad() {
    super.viewDidLoad()

    // create a new scene
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    // create and add a light to the scene
    let lightNode = SCNNode()
    lightNode.light = SCNLight()
    lightNode.light!.type = SCNLightTypeOmni
    lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
    scene.rootNode.addChildNode(lightNode)

    // create and add an ambient light to the scene
    let ambientLightNode = SCNNode()
    ambientLightNode.light = SCNLight()
    ambientLightNode.light!.type = SCNLightTypeAmbient
    ambientLightNode.light!.color = UIColor.darkGrayColor()
    scene.rootNode.addChildNode(ambientLightNode)

//Create a camera like Rickster said
    camera.usesOrthographicProjection = true
    camera.orthographicScale = 9
    camera.zNear = 1
    camera.zFar = 100

    cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
    cameraNode.camera = camera
    cameraOrbit = SCNNode()
    cameraOrbit.addChildNode(cameraNode)
    scene.rootNode.addChildNode(cameraOrbit)

    //initial camera setup
    self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * lastWidthRatio
    self.cameraOrbit.eulerAngles.x = Float(-M_PI) * lastHeightRatio

    // retrieve the SCNView
    let scnView = self.view as! SCNView

    // set the scene to the view
    scnView.scene = scene

    //allows the user to manipulate the camera
    scnView.allowsCameraControl = false  //not needed

    // add a tap gesture recognizer
    let panGesture = UIPanGestureRecognizer(target: self, action: "handlePan:")
    scnView.addGestureRecognizer(panGesture)

    // add a pinch gesture recognizer
    let pinchGesture = UIPinchGestureRecognizer(target: self, action: "handlePinch:")
    scnView.addGestureRecognizer(pinchGesture)
}

func handlePan(gestureRecognize: UIPanGestureRecognizer) {

    let numberOfTouches = gestureRecognize.numberOfTouches()

    let translation = gestureRecognize.translationInView(gestureRecognize.view!)
    var widthRatio = Float(translation.x) / Float(gestureRecognize.view!.frame.size.width) + lastWidthRatio
    var heightRatio = Float(translation.y) / Float(gestureRecognize.view!.frame.size.height) + lastHeightRatio

    if (numberOfTouches==fingersNeededToPan) {

        //  HEIGHT constraints
        if (heightRatio >= maxHeightRatioXUp ) {
            heightRatio = maxHeightRatioXUp
        }
        if (heightRatio <= maxHeightRatioXDown ) {
            heightRatio = maxHeightRatioXDown
        }


        //  WIDTH constraints
        if(widthRatio >= maxWidthRatioRight) {
            widthRatio = maxWidthRatioRight
        }
        if(widthRatio <= maxWidthRatioLeft) {
            widthRatio = maxWidthRatioLeft
        }

        self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * widthRatio
        self.cameraOrbit.eulerAngles.x = Float(-M_PI) * heightRatio

        print("Height: \(round(heightRatio*100))")
        print("Width: \(round(widthRatio*100))")


        //for final check on fingers number
        lastFingersNumber = fingersNeededToPan
    }

    lastFingersNumber = (numberOfTouches>0 ? numberOfTouches : lastFingersNumber)

    if (gestureRecognize.state == .Ended && lastFingersNumber==fingersNeededToPan) {
        lastWidthRatio = widthRatio
        lastHeightRatio = heightRatio
        print("Pan with \(lastFingersNumber) finger\(lastFingersNumber>1 ? "s" : "")")
    }
}

func handlePinch(gestureRecognize: UIPinchGestureRecognizer) {
    let pinchVelocity = Double.init(gestureRecognize.velocity)
    //print("PinchVelocity \(pinchVelocity)")

    camera.orthographicScale -= (pinchVelocity/pinchAttenuation)

    if camera.orthographicScale <= 0.5 {
        camera.orthographicScale = 0.5
    }

    if camera.orthographicScale >= 10.0 {
        camera.orthographicScale = 10.0
    }

}

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return .Landscape
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Release any cached data, images, etc that aren't in use.
}
}

Ответ 5

После попытки реализовать эти решения (в Objective-C) я понял, что Scene Kit на самом деле делает это намного проще, чем все это. SCNView имеет сладкое свойство allowsCameraControl, которое помещает соответствующие распознаватели жестов и соответствующим образом перемещает камеру. Единственная проблема заключается в том, что это не вращение дуги, которое вы ищете, хотя это можно легко добавить, создав дочерний элемент node, расположив его там, где вы хотите, и предоставив ему SCNCamera. Например:

    _sceneKitView.allowsCameraControl = YES; //_sceneKitView is a SCNView

    //Setup Camera
    SCNNode *cameraNode = [[SCNNode alloc]init];
    cameraNode.position = SCNVector3Make(0, 0, 1);

    SCNCamera *camera = [SCNCamera camera];
    //setup your camera to fit your specific scene
    camera.zNear = .1;
    camera.zFar = 3;

    cameraNode.camera = camera;
    [_sceneKitView.scene.rootNode addChildNode:cameraNode];

Ответ 6

Там нет необходимости сохранять состояние нигде, кроме самого узла. Код, который использует какое-то соотношение ширины, ведет себя странно, когда вы несколько раз прокручиваете назад и вперед, а другой код здесь выглядит слишком сложным. Я придумал другое (и, мне кажется, лучшее) решение для распознавания жестов, основанное на подходе @rickster.

UIPanGestureRecognizer:

@objc func handlePan(recognizer: UIPanGestureRecognizer) {
    let translation = recognizer.velocity(in: recognizer.view)
    cameraOrbit.eulerAngles.y -= Float(translation.x/CGFloat(panModifier)).radians
    cameraOrbit.eulerAngles.x -= Float(translation.y/CGFloat(panModifier)).radians
}

UIPinchGestureRecognizer:

@objc func handlePinch(recognizer: UIPinchGestureRecognizer) {
    guard let camera = cameraOrbit.childNodes.first else {
      return
    }
    let scale = recognizer.velocity
    let z = camera.position.z - Float(scale)/Float(pinchModifier)
    if z < MaxZoomOut, z > MaxZoomIn {
      camera.position.z = z
    }
  }

Я использовал скорость, так как при переводе, когда вы замедляете касание, это все равно будет то же самое, заставляя камеру вращаться очень быстро, а не то, что вы ожидаете.

panModifier и pinchModifier - это простые постоянные числа, которые вы можете использовать для настройки pinchModifier отклика. Я нашел оптимальные значения 100 и 15 соответственно.

MaxZoomOut и MaxZoomIn являются константами и являются именно такими, какими они кажутся.

Я также использую расширение на Float для преобразования градусов в радианы и наоборот.

extension Float {
  var radians: Float {
    return self * .pi / 180
  }

  var degrees: Float {
    return self  * 180 / .pi
  }
}