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?
Related
Say I have 2 nodes in my SceneKit scene. I want one to rotate around or orbit (like a planet orbiting a star), the other node once in a certain time interval. I know I can set up animations like so:
let anim = CABasicAnimation(keyPath: "rotation")
anim.fromValue = NSValue(scnVector4: SCNVector4(x: 0, y: 1, z: 0, w: 0))
anim.toValue = NSValue(scnVector4: SCNVector4(x: 0, y: 1, z: 0, w: Float(2 * Double.pi)))
anim.duration = 60
anim.repeatCount = .infinity
parentNode.addAnimation(aim, forKey: "spin around")
Is there an animation for "orbiting", and a way to specify the target node?
The way to do this is by using an additional (helper) SCNNode. You'll use the fact that it adds its own coordinate system and that all of its Child Nodes will move together with that (helper) coordinate system. The child nodes that are off-centre will effectively be orbiting if you view them from the world coordinate system.
You add the HelperNode at the centre of your FixedPlanetNode (orbited planet), perhaps as its child, but definitely at the same position
You add your OrbitingPlanetNode as a child to the HelperNode, but with an offset on one of the axes, e.g. 10 points on the X axis
You start the HelperNode rotating (together with its coordinate system) around a different axis, e.g. the Y axis
This will result in the OrbitingPlanetNode orbiting around the Y axis of HelperNode with an orbit radius of 10 points.
EXAMPLE
earthNode - fixed orbited planet
moonNode - orbiting planet
helperNode - helper node added to provide coordinate system
// assuming all planet geometry is at the centre of corresponding nodes
// also helperNode.position is set to (0, 0, 0)
[earthNode addChildNode:helperNode];
moonNode.position = SCNVector3Make(10, 0, 0);
[helperNode addChildNode:moonNode];
// set helperNode to rotate forever
SCNAction * rotation = [SCNAction rotateByX:0 y:3 z:0];
SCNAction * infiniteRotation = [SCNAction repeatActionForever:rotation];
[helperNode runAction:infiniteRotation];
I used actions and objective-c as this is what I am familiar with, but should be perfectly doable in Swift and with animations.
I am trying to create a simple line graph which is being updated live. Some kind of seismograph .
I was thinking about UIBezierPath , by only moving a point on the y-axis according to an input var, I can create a line moving on the time axis.
The problem is that you have to "push" the previous points to free up space for the new ones.(so the graph goes from left to right)
Can anybody help with some direction ?
var myBezier = UIBezierPath()
myBezier.moveToPoint(CGPoint(x: 0, y: 0))
myBezier.addLineToPoint(CGPoint(x: 100, y: 0))
myBezier.addLineToPoint(CGPoint(x: 50, y: 100))
myBezier.closePath()
UIColor.blackColor().setStroke()
myBezier.stroke()
You're correct: you need to push the previous points. Either divide the total width of the graph so it becomes increasingly scaled but retains all data, or drop the first point each time you add a new one to the end. You'll need to store an array of these points and recreate the path each time. Something like:
//Given...
let graphWidth: CGFloat = 50
let graphHeight: CGFloat = 20
var values: [CGFloat] = [0, 4, 3, 2, 6]
//Here's how you make your curve...
var myBezier = UIBezierPath()
myBezier.moveToPoint(CGPoint(x: 0, y: values.first!))
for (index, value) in values.enumerated() {
let point = CGPoint(x: CGFloat(index)/CGFloat(values.count) * graphWidth, y: value/values.max()! * graphHeight)
myBezier.addLineToPoint(point)
}
UIColor.blackColor().setStroke()
myBezier.stroke()
//And here's how you'd add a point...
values.removeFirst() //do this if you want to scroll rather than squish
values.append(8)
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.
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.
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.