我在位置(30,30,30)处有一个SCNCamera,在位于(0,0,0)位置的对象上有一个SCNLookAtConstraint。我试图使用A UIPanGestureRecognizer让相机围绕虚拟球体上的物体旋转,同时保持相机和物体之间的半径。我假设我应该使用四元数预测,但我在这方面的数学知识非常糟糕。我已知的变量是x& y翻译+我想保留的半径。我已经在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
}
}
谢谢!
答案 0 :(得分:93)
将问题分解为子问题可能会有所帮助。
首先,考虑如何组织场景以启用所需的运动。你谈到移动相机就像它附着在一个看不见的球体上一样。用这个主意!而不是试图计算出将cameraNode.position
设置在假想球体上的某个点的数学计算,而只考虑如果将相机连接到球体上,您将如何移动相机。也就是说,只需旋转球体。
如果要将球体与场景内容的其余部分分开旋转,则需要将其附加到单独的节点。当然,您实际上不需要在场景中插入sphere geometry。只需创建一个节点,其position
与您希望相机围绕的对象同心,然后将相机连接到该节点的子节点。然后,您可以旋转该节点以移动相机。这是一个快速演示,没有滚动事件处理业务:
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
。
这种方法有几个奖励:
cameraNode
是cameraOrbit
的子节点,因此其自身的位置保持不变 - 由于cameraOrbit
的旋转,相机会移动。 现在您已经为摄像机旋转设计了场景,将输入事件转换为旋转非常简单。究竟有多简单取决于您所追求的控制程度:
GLKQuaternion
。 (更新: GLK类型在Swift 1.2 / Xcode 6.3中是“sorta”。在这些版本之前,您可以通过桥接头在ObjC中进行数学运算。)无论哪种方式,您都可以跳过一些手势识别器样板,并使用UIScrollView
来获得一些方便的交互行为。 (并不是说坚持使用手势识别器没有用处 - 这只是一个容易实现的替代方案。)
将一个放在SCNView
的顶部(不在其中放置另一个视图以进行滚动)并将其contentSize
设置为其帧大小的倍数...然后在滚动期间,您可以映射contentOffset
的{{1}}:
eulerAngles
一方面,如果你想在一个或两个方向无休止地旋转,你必须为infinite scrolling做更多的工作。另一方面,你得到了很好的滚动式惯性和反弹行为。
答案 1 :(得分:9)
嘿,我前几天遇到了这个问题,我提出的解决方案相当简单,但效果很好。
首先我创建了我的相机并将其添加到我的场景中,如下所示:
// 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
类变量。并创建了一个UIPanGestureRecognizer
和一个,并在其回调中我提出了以下内容:
-(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's 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;
}
}
这段代码给出了无限的Arcball旋转速度,我相信这就是你要找的东西。此外,您不需要SCNLookAtConstraint
这种方法。事实上,这可能会搞砸,所以不要这样做。
答案 2 :(得分:6)
如果您想使用手势识别器实现rickster的答案,则必须保存状态信息,因为您只会获得相对于手势开头的翻译。我在课堂上加了两个变种
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
}
}
答案 3 :(得分: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.
}
}
答案 4 :(得分:2)
除了节点本身之外,无需将状态保存在其他任何地方。 当您反复来回滚动时,使用某种宽度比例的代码会表现得很奇怪,而此处的其他代码看起来过于复杂。 基于@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
}
}
我使用了速度,就像 translation 一样,当您放慢触摸速度时,它仍然是同一事件,导致相机旋转得非常快,而不是您的旋转速度。 d期望。
panModifier
和pinchModifier
是简单的常数,可用于调整响应度。我发现最佳值为分别为 100 和 15 。
MaxZoomOut
和MaxZoomIn
也是常量,并且看起来也一样。
我还在Float上使用扩展名,将度数转换为弧度,反之亦然。
extension Float {
var radians: Float {
return self * .pi / 180
}
var degrees: Float {
return self * 180 / .pi
}
}
答案 5 :(得分:0)
在尝试实现这些解决方案后(在Objective-C中)我意识到Scene Kit实际上比完成所有这些更容易。 SCNView有一个名为allowsCameraControl的甜蜜属性,可放入适当的手势识别器并相应地移动相机。唯一的问题是,它不是您正在寻找的弧形旋转,尽管可以通过创建子节点,将其放置在任何您想要的位置并为其提供SCNCamera来轻松添加。例如:
$ cut -d, -f2 < x.csv | xargs -I '{}' mv '{}' /home/me/new-directory