I have been playing with session(session: didUpdate frame: ARFrame) and trying to make a ModelEntity move and scale at the same time. I start the movement by calling:
model.move(to: [-1, 0, 0], relativeTo: model.anchor, duration: 1)
Then in the session(session: didUpdate frame: ARFrame) I watch the ModelEntity move. But when I also try to scale down the ModelEntity by 50%, the ModelEntity's position goes to [-27, 0, 0] (expected is [-1, 0, 0]).
let entDistanceFromAnchor = simd_distance(transform.columns.3, entity.anchor!.transform.matrix.columns.3)
entity.setScale([0.5,0.5,0.5], relativeTo: entity.anchor)
Why is changing the scale causing the position to change as well? Is there a way to change the models scale without changing the position?
Related
I'm currently working on a game in scenekit with swift, and i've got a spaceship flying around. I'm using the following code to make the camera follow the spaceship:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
updateCameraPosition()
}
func updateCameraPosition () {
let currentPosition = player.node.presentation.position
let x: Float = lerp(a: Float(prevCameraPosition.x), b: currentPosition.x, t: 0.03)
let z: Float = lerp(a: Float(prevCameraPosition.z), b: currentPosition.z, t: 0.03) + (cameraZoom/2)
let vector = SCNVector3(x: x, y: cameraZoom, z: z)
cameraNode.runAction(SCNAction.move(to: vector, duration: 0.2))
prevCameraPosition = currentPosition
}
func lerp (a: Float, b: Float, t: Float) -> Float {
return (1 - t) * a + t * b;
}
in addition to just following the ship, it add's some nice offset motion when you change directions for a nice fluid camera.
The problem i'm facing is the ship glitches back and forth a good portion of the time, it always moves in the correct directions, but it almost looks like the ship position is getting reset back a few frames. You can see this in action with this video.
Without the camera follow code, the ship moves much smoother, as you can see in this video
Can anybody see anything wrong with my code that is maybe inefficient? Maybe there is a more optimized way to do this? Any tips/resources/advice is greatly appriciated!
You need to stop the previous SCNAction before applying new SCNAction.
To add an action with a key you use the
cameraNode.runAction(SCNAction, forKey: String)
method.
Now you can remove a specific action by that key
cameraNode.removeAction(forKey: String)
I have some camera with perspective projection and object in the (0,0,0).
I computed "bounding frame"(size in pixels on screen) of this object according to this camera and scaled it to fit the screen size. And now it in the middle of screen.
Frame of the scnView is equal to the screen size and I have some UI components above this view(e.g. navigation bar and some big transparent view in the bottom of view). The scnView has some fullscreen background and this object can be scaled/moved/... therefore I need scnview to be fullscreen.
Then I move this object in the middle of "clear area" (the area with absence of UIComponents) and see the bottom of this object (as if it's above me), because my projection is perspective.
I want to move this object in the center of clear area and see like it's in front of me without any distortion. How can I achieve that?
I see 2 solutions.
1) Draw my object offscreen in texture and after that draw this texture on screen in required position.
2) create scnView a bit taller. For example set frame size to = (0, (bottomHeight - topHeight) / 2, width, height + (bottomHeight - topHeight) / 2) to move the center of scnView in required position.
But I don't like first solution due to addition draw call and second solution even sounds crappy.
P.S. sorry for that pictures
Update: Basically I want to use perspective projection and move 3D object like a 2D image in SceneKit.
Sorry if I'm not be understanding correctly, but can you strafe camera up and center it, or will that not be clear enough?
func strafeY(vAmount: XFloat)
{
nPosition = cgVecAdd(v1: nPosition, v2: cgVecScalarMult(v: nTarget, s: vAmount))
}
Try this... generate a new scenekit game project, then replace the default rotation with the code below.
Rotate X slightly as you move the object up to to "somewhat" maintain the relationship to the eye.
You could create a similar but separate routine to do smaller increments to smooth it out.
ship.runAction(SCNAction.rotateBy(x: CGFloat(GLKMathDegreesToRadians(-90)), y: 0, z: 0, duration: 0))
let vRotateAmount: Float = 3
let vAction1 = SCNAction.move(to: SCNVector3Make(0, 1, 0), duration: 1)
let vAction1a = SCNAction.rotateBy(x: CGFloat(GLKMathDegreesToRadians(vRotateAmount)), y:0, z:0, duration: 1)
let vAction2 = SCNAction.move(to: SCNVector3Make(0, 2, 0), duration: 1)
let vAction2a = SCNAction.rotateBy(x: CGFloat(GLKMathDegreesToRadians(vRotateAmount)), y:0, z:0, duration: 1)
let vAction3 = SCNAction.move(to: SCNVector3Make(0, 1, 0), duration: 1)
let vAction3a = SCNAction.rotateBy(x: CGFloat(GLKMathDegreesToRadians(-vRotateAmount)), y:0, z:0, duration: 1)
let vAction4 = SCNAction.move(to: SCNVector3Make(0, 0, 0), duration: 1)
let vAction4a = SCNAction.rotateBy(x: CGFloat(GLKMathDegreesToRadians(-vRotateAmount)), y:0, z:0, duration: 1)
let seq = SCNAction.sequence([vAction1, vAction1a, vAction2, vAction2a, vAction3, vAction3a, vAction4, vAction4a])
let allSeq = SCNAction.repeatForever(seq)
ship.runAction(allSeq)
Solved it by changing camera projectionTransform. Smth like:
let newMatrix = SCNMatrix4Mult(initialProjectionCameraMatrix, translationMatrix)
I am using this function to animate a rotation of a SCNNode:
let rotateNode = SCNAction.rotateTo(x: 0.0, y: CGFloat((headingAngle )), z: 0.0, duration: TimeInterval(1.1), usesShortestUnitArc: true)
node.runAction(rotateNode)
It works really well for different directions except for when I need to rotate it towards the user (or the camera for that matter).
My question is how to direct the SCNNode to move towards the user/camera and how to calculate the headingAngle in order to rotate the SCNNode to face the user/camera when it is moving.
I perform the moving by using
let impulseVector = SCNVector3(
x: 0.0 ,
y: 5.0,
z: 0.0
)
node.physicsBody?.applyForce(impulseVector, at: positionOnNodeToApplyForceTo, asImpulse: true) // propell
And I am aware that the headingAngle needs to be calculated using atan2 function, but for some reason I do not manage to properly direct the SCNNode to move towards the camera nor to rotate and face the camera.
I am using two virtual joysticks to move my camera around the scene. The left stick controls the position and the right one controls the rotation.
When using the right stick, the camera rotates, but it seems that the camera rotates around the center point of the model.
This is my code:
fileprivate func rotateCamera(_ x: Float, _ y: Float)
{
if let cameraNode = self.cameraNode
{
let moveX = x / 50.0
let rotated = SCNMatrix4Rotate(cameraNode.transform, moveX, 0, 1, 0)
cameraNode.transform = rotated
}
}
I have also tried this code:
fileprivate func rotateCamera(_ x: Float, _ y: Float)
{
if let cameraNode = self.cameraNode
{
let moveX = x / 50.0
cameraNode.rotate(by: SCNQuaternion(moveX, 0, 1, 0), aroundTarget: cameraNode.transform)
}
}
But the camera just jumps around. What is my error here?
There are many ways to handle rotation, some are very suitable for giving headaches to the coder.
It sounds like the model is at 0,0,0, meaning it’s in the center of the world, and the camera is tranformed to a certain location. In the first example using matrices, you basically rotate that transformation. So you transform first, then rotate, which yes will cause it to rotate around the origin (0,0,0).
What you should do instead, to rotate the camera in local space, is rotate the camera first in local space and then translate it to its position in world space.
Translation x rotation matrix results in rotation in world space
Rotation x translation matrix results in rotation in local space
So a solution is to remove the translation from the camera first (moving it back to 0,0,0), then apply the rotation matrix, and then reapply the translation. This comes down to the same result as starting with an identity matrix. For example:
let rotated = SCNMatrix4Rotate(SCNMatrixIdentity, moveX, 0, 1, 0)
cameraNode.transform = SCNMatrix4Multiply(rotated, cameraNode.transform)
I have created a big circle with a UIBezierPath and turned it into a Sprite using this,
let path = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: CGFloat(226), startAngle: 0.0, endAngle: CGFloat(M_PI * 2), clockwise: false)
// create a shape from the path and customize it
let shape = SKShapeNode(path: path.cgPath)
shape.lineWidth = 20
shape.position = center
shape.strokeColor = UIColor(red:0.98, green:0.99, blue:0.99, alpha:1.00)
let trackViewTexture = self.view!.texture(from: shape, crop: outerPath.bounds)
let trackViewSprite = SKSpriteNode(texture: trackViewTexture)
trackViewSprite.physicsBody = SKPhysicsBody(edgeChainFrom: innerPath.cgPath)
self.addChild(trackViewSprite)
It works fine. It creates the circle perfectly. But I need to resize it using
SKAction.resize(byWidth: -43, height: -43, duration: 0.3)
Which will make it a bit smaller. But, when it resizes the 20 line width I set now is very small because of the aspect fill. So when I shink it looks something like this:
But I need it to shrink like this-- keeping the 20 line width:
How would I do this?
Don't know if this would affect anything, but the sprites are rotating with an SKAction forever
-- EDIT --
Now, how do I use this method to scale to a specific size? Like turn 226x226 to 183x183?
Since by scaling down the circle, not only its radius gets scaled but its line's width too, you need to set a new lineWidth proportional with the scale. For example, when scaling the circle down by 2, you will need to double the lineWidth.
This can be done in two ways:
Setting the lineWidth in the completion block of the run(SKAction, completion: #escaping () -> Void) method. However this will result in seeing the line shrinking while the animation is running, then jumping to its new width once the animation finishes. If your animation is short, this may not be easy to observe tough.
Running a parallel animation together with the scaling one, which constantly adjusts the lineWidth. For this, you can use SKAction's customAction method.
Here is an example for your case:
let scale = CGFloat(0.5)
let finalLineWidth = initialLineWidth / scale
let animationDuration = 1.0
let scaleAction = SKAction.scale(by: scale, duration: animationDuration)
let lineWidthAction = SKAction.customAction(withDuration: animationDuration) { (shapeNode, time) in
if let shape = shapeNode as? SKShapeNode {
let progress = time / CGFloat(animationDuration)
shape.lineWidth = initialLineWidth + progress * (finalLineWidth - initialLineWidth)
}
}
let group = SKAction.group([scaleAction, lineWidthAction])
shape.run(group)
In this example, your shape will be scaled by 0.5, therefore in case of an initial line width of 10, the final width will be 20. First we create a scaleAction with a specified duration, then a custom action which will update the line's width every time its actionBlock is called, by using the progress of the animation to make the line's width look like it's not changing. At the end we group the two actions so they will run in parallel once you call run.
As a hint, you don't need to use Bezier paths to create circles, there is a init(circleOfRadius: CGFloat) initializer for SKShapeNode which creates a circle for you.