EDIT: I have solved the problem and will post the solution in the next couple of days.
I'm building 3D achievements similar to Apple's Activity app.
I've already loaded my 3D model (a scene with a single node), can show it, and can tap on it to apply a rotational force:
#objc func objectTapped(_ gesture: UITapGestureRecognizer) {
let tapLocation = gesture.location(in: scnView)
let hitResults = scnView.hitTest(tapLocation, options: [:])
if let tappedNode = (hitResults.first { $0.node === badgeNode })?.node {
let pos = Float(tapLocation.x) - tappedNode.boundingBox.max.x
let tappedVector = SCNVector4(x: 0, y: pos, z: 0, w: 0.1)
tappedNode.physicsBody?.applyTorque(tappedVector,
asImpulse: true)
}
}
This works fine. Now to the tricky part:
I want the node to rotate until it either shows its front or backside (like in the Activity app), where it then should stop. It should stop naturally, which means it can overshoot a bit and then return.
To describe it with pictures - here I am holding the node in this position...
...and if I let go of the node, it will rotate to show the front side, which includes a little bit of overshooting. This is the ending position:
Since I'm quite new to SceneKit, I have troubles figuring out how to achieve this effect. It seems like I can achieve that by using SceneKit objects like gravity fields, without having to calculate a whole lot of stuff by myself, or at least that's what I'm hoping for.
I don't necessarily ask for a full solution, I basically just need a point in the right direction. Thanks in advance!
Related
I'm writing an application that displays chemical reactions and molecules in 3D. I read in all the values and positions of each atom from a text file and I am creating each atom shape with SCNSpheres. I have all the other values I need read in properly, but I can't figure out how to add keyframe animations to each node object in my scene.
I set up the molecules like this in ViewController.swift
func makeAtom(atomName: String, coords: [Double], scene: SCNScene) {
guard let radius = atomRadii[atomName]?.atomicRadius else { return }
atoms.append(Atom(name: atomName, x: coords[0], y: coords[1], z: coords[2], radius: radius, positions: []))
let atomGeometry = SCNSphere(radius: CGFloat(radius))
let atomNode = SCNNode(geometry: atomGeometry)
atomNode.position = SCNVector3(coords[0], coords[1], coords[2])
scene.rootNode.addChildNode(atomNode)
atomNodes.append(atomNode)
}
I know that the CAKeyframeAnimations are supposed to be set up like this
let animation = CAKeyframeAnimation()
animation.keyPath = "position.y"
animation.values = [0, 300, 0]
animation.keyTimes = [0, 0.5, 1]
animation.duration = 2
animation.isAdditive = true
vw.layer.add(animation, forKey: "move")
I just don't know where I should be declaring these animations and how the layers factor into all this. What layer should I be adding the animations to? And how can I trigger them to play? I've been searching all over the internet for help with this but I can't find anything that just shows a simple implementation.
I can provide more code if need be, I'm pretty new to StackOverflow and want to make sure I'm doing this right.
You can do it different ways, but I like this method: 58001288 (my answer here) as you can pre-build some animations using scenekit and then run them as a sequence.
Per the comment.. needed more room.
A sequence is a fixed thing. You can start, repeat, and stop it. However, it's hard to interact with it during phases.
If you really need to do that, then one way is to break up your sequence into its parts and call the next one yourself after a completion handler of the current one. I keep an array and a counter so that I know where I am. So basically it's just a queue of actions that I manage - if I'm on a certain step and the button is pressed, then I can cancel all current actions, set the desired effect, and restart it.
Edit:
The completion handler calls itself at the end of the function and advances its own array count so that the next one in the list can be called. This is obviously a bit dangerous, so I would use sparingly, but that's how I did it. I started mine on a timer, then don't forget to clean it up. I had a global GAME_ACTIVE switch and within the code I checked for it before calling myself again.
Edit2: This is actually a moveTo, but it's still just a custom set of SCNActions that calls itself when complete based on duration so that it immediately goes to the next one without a delay.
func moveTo()
{
let vPanelName = moves[moveCount]
let vLaneNode = grid.gridPanels[vPanelName]!.laneNodes[lane]
let vAction = SCNAction.move(to: vLaneNode.presentation.position, duration: TimeInterval(data.getAttackSpeed(vGameType: gameType)))
node.runAction(vAction, completionHandler:
{
self.moveCount += 1
if(self.moveCount >= self.moves.count - 1)
{
self.killMe(vRealKill: false)
return
}
else
{
self.moveTo()
}
})
}
For my game I need to detect if nodes are still moving or not. Now I try to compare old and new positions within a time interval. If positions have the same value, nodes are not moving. And vice versa. Something like that:
if aStonesPositionsOld[index] == aStonesPositionsNew[index] {
print("# stone \(index) is not moving")
}
Does somebody know the better and easier way to check if node is moving or not?
Update. Answer to question of KnighOfDragon (How nodes begin moving?)
Nodes begin to move if being touched, or more exactly - moved with finger. Here is a code:
if touching {
let dt:CGFloat = 1.0/60.0
let distance = CGVector(dx: touchPoint.x-aCuesMe[touchingNr].position.x, dy: touchPoint.y-aCuesMe[touchingNr].position.y)
velocity = CGVector(dx: distance.dx/dt, dy: distance.dy/dt)
aCuesMe[touchingNr].physicsBody!.velocity=velocity
}
Since you are moving the object using the physics engine, all you have to do is check the velocity.
if let v = aCuesMe[someIndex].physicsBody?.velocity {
if v == CGVector(dx: 0, dy: 0) {
// Object has 0 velocity and is not moving
}
}
You can use GKAgent2D for this purpose from GameplayKit. It has property velocity. For more information read Apple's documentation: https://developer.apple.com/library/archive/documentation/General/Conceptual/GameplayKit_Guide/Agent.html#//apple_ref/doc/uid/TP40015172-CH8
and watch video:
https://developer.apple.com/videos/play/wwdc2016/608/
I am adding a 3D model containing animations to the scene that I previously download from the internet. Before adding this node I use prepare function on it because I wan't to avoid frame drop. But still I get a very short frame drop to about 47 fps. This is caused by executing this prepare function. I also tried using prepare(_:, shouldAbortBlock:) on other dispatch queue, but this still didn't help. Can someone help me resolve this or tell me why there is this happening?
arView.sceneView.prepare([mediaNode]) { [mediaNode, weak self] (success) in
guard let `self` = self else { return }
guard
let currentMediaNode = self.mediaNode as? SCNNode,
currentMediaNode === mediaNode,
!self.mainNode.childNodes.contains(mediaNode)
else { return }
self.mainNode.addChildNode(mediaNode)
}
By the way this is a list of files I'm using to load this model:
https://www.dropbox.com/s/7968fe5wfdcxbyu/Serah-iOS.dae?dl=1
https://www.dropbox.com/s/zqb6b6rxynnvc5e/0001.png?dl=1
https://www.dropbox.com/s/hy9y8qyazkcnvef/0002.tga?dl=1
https://www.dropbox.com/s/fll9jbjud7zjlsq/0004.tga?dl=1
https://www.dropbox.com/s/4niq12mezlvi5oz/0005.png?dl=1
https://www.dropbox.com/s/wikqgd46643327i/0007.png?dl=1
https://www.dropbox.com/s/fioj9bqt90vq70c/0008.tga?dl=1
https://www.dropbox.com/s/4a5jtmccyx413j7/0010.png?dl=1
DAE file is already compiled by Xcode tools so that it can be loaded after being downloaded from the internet. And this is the code I use to load it after it's downloaded:
class func loadModel(fromURL url: URL) -> SCNNode? {
let options = [SCNSceneSource.LoadingOption.animationImportPolicy : SCNSceneSource.AnimationImportPolicy.playRepeatedly]
let sceneSource = SCNSceneSource(url: url, options: options)
let node = sceneSource?.entryWithIdentifier("MDL_Obj", withClass: SCNNode.self)
return node
}
I was experiencing the same issue. My nodes were all taking advantage of physically-based rendering (PBR) and the first time I added a node to the scene, the frame rate dropped significantly, but was fine after that. I could add as many other nodes without a frame rate drop.
I figured out a work around to this issue. What I do is after I create my ARConfiguration and before I call session.run(configuration) I add a test node with PBR to the scene. In order for that node to not appear, I set the node's material's colorBufferWriteMask to an empty array (see this answer: ARKit hide objects behind walls) Then before I add my content I remove that node. Adding and removing this test node does the trick for me.
Here is an example:
var pbrTestNode: SCNNode!
func addPBRTestNode() {
let testGeometrie = SCNBox(width: 0.5, height: 0.5, length: 0.5, chamferRadius: 0)
testGeometrie.materials.first?.diffuse.contents = UIColor.blue
testGeometrie.materials.first?.colorBufferWriteMask = []
testGeometrie.materials.first?.lightingModel = .physicallyBased
pbrTestNode = SCNNode(geometry: testGeometrie)
scene.rootNode.addChildNode(pbrTestNode)
}
func removePBRTestNode() {
pbrTestNode.removeFromParentNode()
}
func startSessionWithPlaneDetection() {
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
if #available(iOS 11.3, *) {
configuration.planeDetection = [.horizontal, .vertical]
} else {
configuration.planeDetection = .horizontal
}
configuration.isLightEstimationEnabled = true
// this prevents the delay when adding any nodes with PBR later
sceneController.addPBRTestNode()
// Run the view's session
sceneView.session.run(configuration)
}
Call removePBRTestNode() when you add your content to the scene.
Firstly
Get 3D model for AR app with no more than 10K polygons and a texture of 1K x 1K. The best result can be accomplished with 5K...7K polygons per each model. Totally, SceneKit's scene may contain not more than 100K polygons. This recommendation helps you considerably improve rendering performance and, I suppose, you'll have a minimal drop frame.
Secondly
The simplest way to get rid of drop frame in ARKit/SceneKit/AVKit is to use Metal framework. Just imagine: a simple image filter can be more than a hundred times faster to perform on the GPU than an equivalent CPU-based filter. The same things I could say about realtime AV-video and 3D animation – they perform much better on GPU.
For instance, you can read this useful post about using Metal rendering for AVCaptureSession. There's awesome workflow how to use Metal.
P.S. Check your animated object/scene in 3D authoring tool (if it's OK) before writing a code.
I have a SKAction that runs an action if an area on the screen is touched. However I cannot get the SKanimate to only run through the SKarray once (both actions that is), it seems to run around 4 times. The count parameter doesn't seem to make any difference either. Any help on how to get it to run through the frames in the array just once then stop would be appreciated!
//Touch location check
for touch in touches {
let location = touch.location(in: self)
if myButton.contains(location) {
//run shoot animation.
MainGuy.run(SKAction.repeat(SKAction.animate(with: TextureArrayShoot, timePerFrame: 0.10), count: 1),withKey: "outlaw")
print ("touched")
let witchaction = SKAction.animate(with: TextureArrayWitch, timePerFrame: 0.20)
witch.run(witchaction)
missedLabel1.text = "Good Shot!"
}
}
Apologies - solved it. There were 3 arrays for sprites in a row, and the first one wasn't closed properly and encapsulating the other two, meaning the 'I' in for I in...was being used 3 times!
I have been creating my own very simple test game based on Breakout while learning SpriteKit (using iOS Games by Tutorials by Ray Wenderlich et al.) to see if I can apply concepts that I have learned. I have decided to simplify my code by using an .sks file to create the sprite nodes and replacing my manual bounds checking and collision with physics bodies.
However, my ball keeps running parallel to walls/other rectangles (as in, simply sliding up and down them) any time it collides with them at a steep angle. Here is the relevant code--I have moved the physics body properties into code to make them more visible:
import SpriteKit
struct PhysicsCategory {
static let None: UInt32 = 0 // 0
static let Edge: UInt32 = 0b1 // 1
static let Paddle: UInt32 = 0b10 // 2
static let Ball: UInt32 = 0b100 // 4
}
var paddle: SKSpriteNode!
var ball: SKSpriteNode!
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector.zeroVector
let edge = SKNode()
edge.physicsBody = SKPhysicsBody(edgeLoopFromRect: frame)
edge.physicsBody!.usesPreciseCollisionDetection = true
edge.physicsBody!.categoryBitMask = PhysicsCategory.Edge
edge.physicsBody!.friction = 0
edge.physicsBody!.restitution = 1
edge.physicsBody!.angularDamping = 0
edge.physicsBody!.linearDamping = 0
edge.physicsBody!.dynamic = false
addChild(edge)
ball = childNodeWithName("ball") as SKSpriteNode
ball.physicsBody = SKPhysicsBody(rectangleOfSize: ball.size))
ball.physicsBody!.usesPreciseCollisionDetection = true
ball.physicsBody!.categoryBitMask = PhysicsCategory.Ball
ball.physicsBody!.collisionBitMask = PhysicsCategory.Edge | PhysicsCategory.Paddle
ball.physicsBody!.allowsRotation = false
ball.physicsBody!.friction = 0
ball.physicsBody!.restitution = 1
ball.physicsBody!.angularDamping = 0
ball.physicsBody!.linearDamping = 0
physicsWorld.contactDelegate = self
}
Forgot to mention this before, but I added a simple touchesBegan function to debug the bounces - it just adjusts the velocity to point the ball at the touch point:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let touch = touches.anyObject() as UITouch
let moveToward = touch.locationInNode(self)
let targetVector = (moveToward - ball.position).normalized() * 300.0
ball.physicsBody!.velocity = CGVector(point: targetVector)
}
The normalized() function just reduces the ball/touch position delta to a unit vector, and there is an override of the minus operator that allows for CGPoint subtraction.
The ball/edge collisions should always reflect the ball at a precisely opposite angle but for some reason the ball really seems to have a thing for right angles. I can of course implement some workaround to reflect the ball's angle manually, but the point is that I want to do this all using the built in physics functionality in SpriteKit. Is there something obvious that I am missing?
This appears to be an issue with collision detection. Most have found solutions by using the didBeginContact and reapplying the force at an opposite direction. Note he says didMoveToView but corrects himself in a later comment to didBeginContact.
See comments at the bottom of the Ray Wenderlich tutorial here
I have a fix for the problem with the ball "riding the rail" if it
strikes at a shallow angle (#aziz76 and #colinf). I added another
category, "BorderCategory" and assigned it to the border PhysicsBody
we create in didMoveToView.
and a similar SO question here explaining why it is happening.
Even if you do that, though, many physics engines (including
SpriteKit's) have trouble with situations like this because of
floating point rounding errors. I've found that when I want a body to
keep a constant speed after a collision, it's best to force it to --
use a didEndContact: or didSimulatePhysics handler to reset the moving
body's velocity so it's going the same speed it was before the
collision (but in the opposite direction).
Also another thing I noticed is you are using a square instead of a circle for your ball and you may want to consider using...
ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.width/2)
So turns out you aren't crazy which is always good to hear from someone else and hopefully this will help you find a solution that works best for your application.
I came up with a temporary solution that is working surprisingly well. Simply apply a very small impulse opposite of the border. You may need to change the strength based on the masses in your system.
func didBeginContact(contact: SKPhysicsContact) {
let otherNode = contact.bodyA.node == ball.sprite ? contact.bodyB.node : contact.bodyA.node
if let obstacle = otherNode as? Obstacle {
ball.onCollision(obstacle)
}
else if let border = otherNode as? SKSpriteNode {
assert(border.name == "border", "Bad assumption")
let strength = 1.0 * (ball.sprite.position.x < frame.width / 2 ? 1 : -1)
let body = ball.sprite.physicsBody!
body.applyImpulse(CGVector(dx: strength, dy: 0))
}
}
In reality, this should not be necessary, since as described in the question, frictionless, fully elastic collision dictates that the ball should rebound by inverting the x velocity (assuming side borders) no matter how small the collision angle is.
Instead, what is happening in the game is as if sprite kit ignores the X velocity if it is smaller than a certain value, making the ball slide against the wall without rebound.
Final Note
After reading this and this, it's obvious to me that the real answer is for any serious physics game you have, you should be using Box2D instead. You get way too many perks from the migration.
This problem only seems to occur when the velocity is small in either direction. However to reduce the effect it is possible to decrease the speed of the physicsWorld, e.g.,
physicsWorld.speed = 0.1
and then increase the velocity of the physicsBody, e.g.,
let targetVector = (moveToward - ball.position).normalized() * 300.0 * 10
ball.physicsBody!.velocity = CGVector(point: targetVector)
Add code below:
let border = SKPhysicsBody(edgeLoopFrom: self.frame)
border.friction = 0
border.restitution = 1
self.physicsBody = border
which will make your ball bounce back when it collides with wall.
Restitution is the bounciness of the physics body so setting it to 1 will bounce ball back.
I was seeing exactly the same issue, but the fix for me was not related to the collision detection issues mentioned in the other answers. Turns out I was setting the ball into motion by using an SKAction that repeats forever. I eventually discovered that this conflicts with SpriteKit's physics simulation leading to the node/ball travelling along the wall instead of bouncing off it.
I'm assuming that the repeating SKAction continues to be applied and overrides/conflicts with the physics simulation's auto-adjustment of the the ball's physicsBody.velocity property.
The fix for this was to set the ball into motion by setting the velocity on its physicsBody property. Once I'd done this the ball began bouncing correctly. I'm guessing that manipulating its position via physicsBody by using forces and impulses will also work given that they are a part of the physics simulation.
It took me an embarrassing amount of time to realise this issue, so I'm posting this here in case I can save anyone else some time. Thank you to 0x141e! Your comment put me (and my ball) on the right path.
The problem is twofold in that 1) it will not be solved by altering friction/restitution of the physics bodies and 2) will not be reliably addressed by a return impulse in the renderer() loop due to the contact occurring after the body has already begun decelerating.
Issue 1: Adjusting physics properties has no effect --
Because the angular component of the collision is below some predetermined threshold, the physics engine will not register it as a physical collision and therefore, the bodies will not react per the physics properties you've set. In this case, restitution will not be considered, regardless of the setting.
Issue 2: Applying an impulse force when the collision is detected will not produce consistent results -- This is due to the fact that in order to simulate restitution, one needs the velocity of the object just prior to impact.
-->For instance, if an object hits the floor at -10m/s and you want to simulate 0.8 restitution, you would want that object to be propelled 8m/s in the oppostie direction.
Unfortunately, due to the render loop, the velocity registered when the collision occurs is much lower since the object has already decelerated.
-->For example, in the simulations I was running, a ball hitting a floor at a low angle was arriving at -9m/s, but the velocity registered when the collision was detected was -2m/s.
This is important since in order to create a consistent representation of restitution, we must know the pre-collision velocity in order to arrive at our desired post-collision velocity...you can't ascertain this in the Swift collision callback delegate.
Solution:
Step 1. During the render cycle, record the velocity of the object.
//Prior to the extension define two variables:
var objectNode : SCNNode!
var objectVelocity : SCNVector3!
//Then, in the renderer delegate, capture the velocity of the object
extension GameViewController: SCNSceneRendererDelegate
{
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval)
{
if objectNode != nil {
//Capture the object's velocity here, which will be saved prior to the collision
if objectNode.physicsBody != nil {
objectVelocity = objectNode.physicsBody!.velocity
}
}
}
}
Step 2: Apply a return impulse when the object collides, using the velocity saved prior to the collision. In this example, I am only using the y-component since I am simulating restitution in that axis.
extension GameViewController: SCNPhysicsContactDelegate {
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
let contactNode: SCNNode!
//Bounceback factor is in essence restitution. It is negative signifying the direction of the vector will be opposite the impact
let bounceBackFactor : Float! = -0.8
//This is the slowest impact registered before the restitution will no longer take place
let minYVelocity : Float! = -2.5
// This is the smallest return force that can be applied (optional)
let minBounceBack : Float! = 2.5
if contact.nodeA.name == "YourMovingObjectName" && contact.nodeB.name == "Border" {
//Using the velocity saved during the render loop
let yVel = objectVelocity.y
let vel = contact.nodeA.physicsBody?.velocity
let bounceBack : Float! = yVel * bounceBackFactor
if yVel < minYVelocity
{
// Here, the opposite force is applied (in the y axis in this example)
contact.nodeA.physicsBody?.velocity = SCNVector3(x: vel!.x, y: bounceBack, z: vel!.z)
}
}
if contact.nodeB.name == "YourMovingObjectName" && contact.nodeA.name == "Border" {
//Using the velocity saved during the render loop
let yVel = objectVelocity.y
let vel = contact.nodeB.physicsBody?.velocity
let bounceBack : Float! = yVel * bounceBackFactor
if yVel < minYVelocity
{
// Here, the opposite force is applied (in the y axis in this example)
contact.nodeB.physicsBody?.velocity = SCNVector3(x: vel!.x, y: bounceBack, z: vel!.z)
}
}
}
}