SceneKit camera rotation wrong although camera and nodes share origin - ios

I am playing with SceneKit trying to make a star field. Here are some sample stars with coordinates:
Name: Alpheratz, X: 25.9746 Y: 0.951042 Z: 14.4613
Name: Caph, X: 8.60001 Y: 0.344589 Z: 14.4095
Name: Algenib, X: 115.664 Y: 6.68732 Z: 31.4421
Name: Ankaa, X: 19.0837 Y: 2.19828 Z: -17.4833
Name: Shedir, X: 37.9848 Y: 6.78448 Z: 58.3796
Name: Diphda, X: 27.5836 Y: 5.31046 Z: -9.11979
Name: 96 G. Psc, X: 7.25837 Y: 1.5555 Z: 0.686093
Name: Van Maanen's Star, X: 4.14696 Y: 0.903519 Z: 0.400348
Name: Cih, X: 79.8367 Y: 20.168 Z: 146.837
(coordinates are from the HYG Database.)
The SceneView is simply a full-window view added in the storyboard. Setup of the scene, camera etc. is like this:
let scene = SCNScene()
sceneView.scene = scene
let camera = SCNCamera()
camera.usesOrthographicProjection = true
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3Make(0.0, 0.0, 0.0)
scene.rootNode.addChildNode(cameraNode)
Then for each star I do:
let starGeometry = SCNSphere(radius: CGFloat(1.0))
let starNode = SCNNode(geometry: starGeometry)
starNode.transform = SCNMatrix4MakeTranslation(star.cartesianX.floatValue, star.cartesianY.floatValue, star.cartesianZ.floatValue)
scene.rootNode.addChildNode(starNode)
This produces some shaded spheres in the view but they appear to be in the wrong place and they rotate around a point somewhere to the right of the screen.
What I expect to see is all the spheres rotating around the camera. As they move they should not change apparent size or swing away from the camera which is what they do. As more stars are plotted it gets harder to find anything to see even when fully zoomed out and as soon as a pan is applied everything disappears.
How can the camera be fixed at the origin and all the objects be positioned around the origin so that they rotate around that point?

What I expect to see is all the spheres rotating around the camera. As they move they should not change apparent size or swing away from the camera which is what they do. As more stars are plotted it gets harder to find anything to see even when fully zoomed out and as soon as a pan is applied everything disappears.
how is you camera behaviour implemented?
You should definitely not rely on allowsCameraControl, which is a convenience to get started rapidly.

Related

How to ensure SceneKit camera always points at specific position

I'm writing a game in Swift 5 (but had the same problem with Swift 4 which i recently updated by game from), with all my SCNNode's centered around SCNVector3Zero. In other word's (0,0,0) is at the center of my play area.
The SCNCamera is attached to a SNCNode and positioned just outside the limits of the play area, looking at (0,0,0).
cameraNode.look(at: SCNVector3Zero)
If I place 5 nodes in the scene, all on the y=0 plane, (0,0,0), (-1,0,-1), (1,0,1), (-1,0,1) and (1,0,-1) like the face of a dice showing five, it all works great. I can rotate around the scene with the node at (0,0,0) staying centered with no movement.
If I add a 2nd row, so the first row moves to y=1 and the second row gets nodes with y=-1 the scene wobbles a little when rotating.
The further a node is moved from the center, the more exaggerated this wobble becomes.
Here's the code setting up the scene (this example has three rows and looks like a three dimensional "five" face of a dice, point in the center at (0,0,0) and the other dots at each corner of a cube) ...
addSphere(
x: 0.0,
y: 0.0,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: -2.0 * spacing,
y: -2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: 2.0 * spacing,
y: 2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: -2.0 * spacing,
y: 2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "granite")
addSphere(
x: 2.0 * spacing,
y: -2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "granite")
addSphere(
x: 0.0,
y: -2.0 * spacing,
z: -2.0 * spacing,
radius: radius,
textureName: "slime")
addSphere(
x: 0.0,
y: 2.0 * spacing,
z: 2.0 * spacing,
radius: radius,
textureName: "slime")
addSphere(
x: 0.0,
y: 2.0 * spacing,
z: -2.0 * spacing,
radius: radius,
textureName: "wood")
addSphere(
x: 0.0,
y: -2.0 * spacing,
z: 2.0 * spacing,
radius: radius,
textureName: "wood")
This code behaves, everything is symmetrical around (0,0,0) with the node at (0,0,0) staying exactly where it should.
If I introduce this node into the scene, it all goes badly wrong ...
addSphere(
x: 6.0,
y: 6.0,
z: 6.0,
radius: radius,
textureName: "earth")
I've tried adding a fixed physics body to each node that has a mass of zero to no avail. It's like the camera is no longer looking directly at (0,0,0) but is influenced but the nodes in the scene.
I've tried all sorts of permutations of adding nodes, and on some tests it appears adding anything with value for the z-axis caused problems
This is the addSphere method ...
internal func addSphere(x: Float, y: Float, z: Float, radius: Float, color: UIColor, textureName: String) -> SCNNode {
let sphereGeometry = SCNSphere(radius: CGFloat(radius))
sphereGeometry.firstMaterial?.diffuse.contents = UIImage(imageLiteralResourceName: textureName)
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.name = "dot"
sphereNode.position = SCNVector3(x: x, y: y, z: z)
sphereNode.lines = [];
sphereNode.ignoreTaps = false
sphereNode.categoryBitMask = NodeBitMasks.dot
//sphereNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
self.rootNode.addChildNode(sphereNode)
return sphereNode
}
There are no errors and as you might expect, any point positioned at (0,0,0) should remain static when rotating about while "looking at" this point.
Any pointers would be really appreciated as I can't make heads or tails of why it's behaving like this.
I assume you use the SCNView.allowsCameraControl = true to move the camera around. This built-in setting of SceneKit is actually only for debug purposes of your scene. It is not suited for anything when you want to have dedicated control over your camera movement.
You should instead try to implement a camera orbit, see https://stackoverflow.com/a/25674762/3358138.
I could reproduce your problem with the "wobbling" center of your scene, see Playground Gist https://gist.github.com/dirkolbrich/e2c247619b28a287c464abbc0595e23c.
A camera orbit solves this "wobbling" and let’s the camera stay on center, see Playground Gist https://gist.github.com/dirkolbrich/9e4dffb3026d0540d6edf6877f27d1e4.

Node rotation value is reset when animating

I'm working on an AR project and need to position some 3D models in the scene when a specific image is recognized.
I also need the 3d models to rotate indefinitely around their y axis, which I managed to do with the following code.
let spin = CABasicAnimation(keyPath: "rotation")
spin.fromValue = NSValue(scnVector4: SCNVector4(x: 0, y: 1, z: 0, w: 0))
spin.toValue = NSValue(scnVector4: SCNVector4(x: 0, y: 1, z: 0, w: Float(2 * Double.pi)))
spin.duration = 8
spin.repeatCount = .infinity
modelNode.addAnimation(spin, forKey: nil)
The issue I'm encountering is when I try to apply this rotation animation to a node that has been rotated beforehand to be set in the correct starting orientation using
modelNode.transform = SCNMatrix4Mult(modelNode.transform, SCNMatrix4MakeRotation(Float(-Double.pi/2), 1, 0, 0))
In this case it seems that the animation doesn't take into account the current rotation of the node but uses the original one, nullifying my setup.
Am I doing something wrong?
How can I set a starting object rotation before animating another rotation?

Rotate BezierPath around the x axis for PDF annotation Swift 4 iOS

I am trying to annotate a PDF using type .ink in my application using a UIBezierPath. I have included a snippet of the pertinent code below (can add the whole sample but issue is only with rotating the path). The issue is when I apply this path, it is rotated 180 degrees around the x- axis so basically it is flipped upside down. I would like to be able to rotate this path 180 degrees around the x-axis so it appears as initially intended. I have seen example of rotating around the z-axis but none around the x-axis. Any help would be greatly appreciated!
let rect = CGRect(x: 110, y: 100, width: 400, height: 300)
let annotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil)
annotation.backgroundColor = .blue
path.apply(CGAffineTransform(scaleX: 0.2, y: 0.2))
annotation.add(path)
// Add annotation to the first page
page.addAnnotation(annotation)
pdfView?.document?.page(at: 0)?.addAnnotation(annotation)
I was actually able to solve this issue using the following scale and a translation transforms:
let rect = CGRect(x: 110, y: 100, width: 400, height: 300)
let annotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil)
annotation.backgroundColor = .blue
// OVER HERE 🙂
path.apply(CGAffineTransform(scaleX: 1.0, y: -1.0))
path.apply(CGAffineTransform(translationX: 0, y: rect.size.height))
annotation.add(path)
// Add annotation to the first page
page.addAnnotation(annotation)
pdfView?.document?.page(at: 0)?.addAnnotation(annotation)
This solution is inspired from the Apple Developer Documentation example.
It was a bit tricky because the PDFKit Coordinate System uses the bottom/left as the origin, with the x- axis going left-to-right and the y- axis going bottom-to-top. That's contrary to the origin: top/left, x: left-to-right and y: top-to-bottom pattern usually encountered on iOS. But this is what we're doing:
CGAffineTransform(scaleX: 1.0, y: -1.0) - scaling the y coordinate to -1.0 makes your path flip 180 degrees around the x- axis (said axis visually being the bottom line of the rect). This means the path is now below of the rect, which might give you the impression that it has disappeared (you won't even find it by Capturing the View Hierarchy since it will show you the entire PDFView as one UIView component, which may or may not drive you insane).
CGAffineTransform(translationX: 0, y: rect.size.height) - now that the path is at the bottom of the rect (but actually at the "top" according to the PDFKit Coordinate System), we need to bring it back into the visible area. Which is why we need to apply a translation transform to move the path up (or down - thanks again PDFKit) into the rect.
Hope this helps! Cheers

Could someone explain why/how this sample scene kit code works?

I am working with scene kit and am trying to make a first person game. I found this sample code for making the first person camera with a pan gesture. Everything works but I do not understand what is going on here. Could someone explain what is happening?
func lookGestureRecognized(gesture: UIPanGestureRecognizer) {
//get translation and convert to rotation
let translation = gesture.translationInView(self.view)
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
let vAngle = acos(Float(translation.y) / 200) - Float(M_PI_2)
//rotate hero
heroNode.physicsBody?.applyTorque(SCNVector4(x: 0, y: 1, z: 0, w: hAngle), impulse: true)
//tilt camera
elevation = max(Float(-M_PI_4), min(Float(M_PI_4), elevation + vAngle))
camNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: elevation)
//reset translation
gesture.setTranslation(CGPointZero, inView: self.view)
}
Here is the same code, with a few additional comments:
func lookGestureRecognized(gesture: UIPanGestureRecognizer) {
// Create Translation variable, containing the
// "distance" traveled by the finger since the last event
let translation = gesture.translationInView(self.view)
// From this distance, calculate how much the camera should move
// 1) horizontally, 2) vertically using angles (FPS controls)
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
let vAngle = acos(Float(translation.y) / 200) - Float(M_PI_2)
// Apply the horizontal angle to the Hero node as a force to
// Make it rotate accordingly (physics body use forces to move)
heroNode.physicsBody?.applyTorque(SCNVector4(x: 0, y: 1, z: 0, w: hAngle), impulse: true)
// Use the other angle to look up and down, clamped to ±pi/4
elevation = max(Float(-M_PI_4), min(Float(M_PI_4), elevation + vAngle))
// Apply the new angle to teh camera on the X axis
camNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: elevation)
// Set the translation to 0 to avoid accumulation
// the next time the event is triggered
gesture.setTranslation(CGPointZero, inView: self.view)
}
This should help understand, let me know if you need more details on how it works!
(Note: the "Distance" is actually a 2D Vector)
EDIT: here's a better explanation of the angle:
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
First, the translation (so pixel distance on x) is divided by 200. This is to both slow down the movement and (unsafely) keep x between -1 and 1.
Acos gives the arc cosinus of a value. The result is between 0 to pi and (to simplify) only works for x from -1 to 1. Here's a graph for it: http://www.wolframalpha.com/input/?i=acos%28x%29-pi%2F2
Since we want to move in positive and negative directions, we remove half of the max value (M_PI_2, which is pi/2) to keep the result within -pi/2 to pi/2
In the end, if you move your finger 200 pixels in a direction, you would look pi/2=90° on your screen.

SceneKit how to draw a sphere showing mesh like surface instead of smooth?

I am drawing a sphere in Scene Kit, and it all works ok. I am drawing it like so:
...
let g = SCNSphere(radius: radius)
geometria.firstMaterial?.diffuse.contents = myColor
let node = SCNNode(geometry: g)
node.position = SCNVector3(x: x, y: y, z: z)
scene.rootNode.addChildNode(node)
This draws the sphere with a smooth surface (see image).
I would
What I am trying to accomplish is to have the sphere not rendered "smooth" like in the photo but I want to be able to have it so it shows the skeleton... so maybe control how many triangles it uses to draw the surface of the sphere but the triangles need to be empty, so I would just see the sides of the triangles...
Any suggestion?
So here's an image of what zI am trying to make the sphere look like:
Your wish #1: "not smooth" sphere
Your wish #2: wireframe
Past this into Xcode playground:
import Cocoa
import SceneKit
import QuartzCore
import XCPlayground
// create a scene
var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
var scene = SCNScene()
sceneView.scene = scene
XCPShowView("The Scene View", sceneView)
sceneView.autoenablesDefaultLighting = false
// create sphere
let g = SCNSphere(radius: 100)
g.firstMaterial?.diffuse.contents = NSColor.greenColor()
// WISH #1
g.segmentCount = 12
let node = SCNNode(geometry: g)
node.position = SCNVector3(x: 10, y: 10, z: 10)
scene.rootNode.addChildNode(node)
// WISH #2
glPolygonMode(GLenum(GL_FRONT), GLenum(GL_LINE));
glPolygonMode(GLenum(GL_BACK), GLenum(GL_LINE));
// animate
var spin = CABasicAnimation(keyPath: "rotation")
spin.toValue = NSValue(SCNVector4: SCNVector4(x: 1, y: 1, z: 0, w: CGFloat(2.0*M_PI)))
spin.duration = 3
spin.repeatCount = HUGE // for infinity
node.addAnimation(spin, forKey: "spin around")
Taken from the possible duplicate question Render an SCNGeometry as a wireframe, I'm going to copy it here for posterity:
it's possible to set the fillMode of the Material to just lines, which gives the desired effect:
g.firstMaterial?.fillMode = .lines
// (or possibly geometria.firstMaterial?.fillMode, not clear from your example)
you can then change the 'resolution' of the wireframe using:
g.segmentCount = 12
// 48 is the default, lower is 'coarser', less than 3
// is undefined and therefore unsupported
and you can probably also want to set:
g.isGeodesic = true
... which will give you triangular 'tiles' on your wireframe rather than the default rectangles.

Resources