Get frame's position at an specific rate per second - swift

I'm trying to get the current position of camera in scene adding a renderer delegate method (a method in ARSCNViewDelegate):
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else { return }
let transform = pointOfView.transform
let position = SCNVector3Make(transform.m41, transform.m42, transform.m43)
let orientation = SCNVector3Make(transform.m31, transform.m32, transform.m33)
...
}
And I would like to get these positions for a specific number of frames per second (25 fps in my case). I can get this by modifying this parameter: preferredFramesPerSecond but it always adjusts automatically to 30 fps, not 25.
It works ok for 5, 15, 20, 30, 45 or 60, but why not for 25 fps?
override func viewDidLoad() {
super.viewDidLoad()
...
sceneView.preferredFramesPerSecond = 25
...
}
Anyway, of course reducing FPS means poorer AR performance so... is there another way to ask ARKit for a frame camera position each time I need one? specifically every 25 fps.
Thanks a lot in advance!

This is used to run block of code after certain time in second. (1/25 in your case)
func recurringFunction(){
// your code here
guard let pointOfView = sceneView.pointOfView else { return }
let transform = pointOfView.transform
let position = SCNVector3Make(transform.m41, transform.m42, transform.m43)
let orientation = SCNVector3Make(transform.m31, transform.m32, transform.m33)
....
DispatchQueue.main.asyncAfter(deadline: .now() + 1/25) {
self.recurringFunction()
}
}
It will also help in running your app without dropping your usual fps.

Related

ARKit SCNNode always in the center when camera move

I am working on a project where I have to place a green dot to be always in the center even when we rotate the camera in ARKit. I am using ARSCNView and I have added the node so far everything is good. Now I know I need to modify the position of the node in
func session(_ session: ARSession, didUpdate frame: ARFrame)
But I have no idea how to do that. I saw some example which was close to what I have but it does not run as it suppose to.
func session(_ session: ARSession, didUpdate frame: ARFrame) {
let location = sceneView.center
let hitTest = sceneView.hitTest(location, types: .featurePoint)
if hitTest.isEmpty {
print("No Plane Detected")
return
} else {
let columns = hitTest.first?.worldTransform.columns.3
let position = SCNVector3(x: columns!.x, y: columns!.y, z: columns!.z)
var node = sceneView.scene.rootNode.childNode(withName: "CenterShip", recursively: false) ?? nil
if node == nil {
let scene = SCNScene(named: "art.scnassets/ship.scn")!
node = scene.rootNode.childNode(withName: "ship", recursively: false)
node?.opacity = 0.7
let columns = hitTest.first?.worldTransform.columns.3
node!.name = "CenterShip"
node!.position = SCNVector3(x: columns!.x, y: columns!.y, z: columns!.z)
sceneView.scene.rootNode.addChildNode(node!)
}
let position2 = node?.position
if position == position2! {
return
} else {
//action
let action = SCNAction.move(to: position, duration: 0.1)
node?.runAction(action)
}
}
}
It doesn't matter how I rotate the camera this dot must be in the middle.
It's not clear exactly what you're trying to do, but I assume its one of the following:
A) Place the green dot centered in front of the camera at a fixed distance, eg. always exactly 1 meter in front of the camera.
B) Place the green dot centered in front of the camera at the depth of the nearest detected plane, i.e. using the results of a raycast from the mid point of the ARSCNView
I would have assumed A, but your example code is using (now deprecated) sceneView.hitTest() function which in this case would give you the depth of whatever is behind the pixel at sceneView.center
Anyway here's both:
Fixed Depth Solution
This is pretty straightforward, though there are few options. The simplest is to make the green dot a child node of the scene's camera node, and give it position with a negative z value, since z increases as a position moves toward the camera.
cameraNode.addChildNode(textNode)
textNode.position = SCNVector3(x: 0, y: 0, z: -1)
As the camera moves, so too will its child nodes. More details in this very thorough answer
Scene Depth Solution
To determine the estimated depth behind a pixel, you should use ARSession.raycast instead of SceneView.hitTest, because the latter is definitely deprecated.
Note that, if the raycast() (or still hitTest()) methods return an empty result set (not uncommon given the complexity of scene estimation going on in ARKit), you won't have a position to update the node and this it might not be directly centered in every frame. To handle this is a bit more complex, as you'd need decide exactly what you want to do in that case.
The SCNAction is unnecessary and potentially causing problems. These delegate methods run 60fps, so simply updating the position directly will produce smooth results.
Adapting and simplifying the code you posted:
func createCenterShipNode() -> SCNNode {
let scene = SCNScene(named: "art.scnassets/ship.scn")!
let node = scene.rootNode.childNode(withName: "ship", recursively: false)
node!.opacity = 0.7
node!.name = "CenterShip"
sceneView.scene.rootNode.addChildNode(node!)
return node!
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
// Check the docs for what the different raycast query parameters mean, but these
// give you the depth of anything ARKit has detected
guard let query = sceneView.raycastQuery(from: sceneView.center, allowing: .estimatedPlane, alignment: .any) else {
return
}
let results = session.raycast(query)
if let hit = results.first {
let node = sceneView.scene.rootNode.childNode(withName: "CenterShip", recursively: false) ?? createCenterShipNode()
let pos = hit.worldTransform.columns.3
node.simdPosition = simd_float3(pos.x, pos.y, pos.z)
}
}
See also: ARRaycastQuery
One last note - you generally don't want to do scene manipulation within this delegate method. It runs on a different thread than the Scenekit rendering thread, and SceneKit is very thread sensitive. This will likely work fine, but beyond adding or moving a node will certainly cause crashes from time to time. You'd ideally want to store the new position, and then update the actual scene contents from within the renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) delegate method.

ARKit – How to know if 3d object is in the center of a screen?

I place 3d object in the world space. After that I try to move camera randomly. Then right now I need to know after I knew object has became inside frustum by isNode method, if the object is in center, top or bottom of camera view.
For a solution that's not a hack you can use the projectPoint: API.
It's probably better to work with pixel coordinates because this method uses the actual camera's settings to determine where the object appears on screen.
let projectedPoint = sceneView.projectPoint(self.sphereNode.worldPosition)
let xOffset = projectedPoint.x - screenCenter.x;
let yOffset = projectedPoint.y - screenCenter.y;
if xOffset * xOffset + yOffset * yOffset < R_squared {
// inside a disc of radius 'R' at the center of the screen
}
Solution
To achieve this you need to use a trick. Create new SCNCamera, make it a child of pointOfView default camera and set its FoV to approximately 10 degrees.
Then inside renderer(_:updateAtTime:) instance method use isNode(:insideFrustumOf:) method.
Here's working code:
import ARKit
class ViewController: UIViewController,
ARSCNViewDelegate,
SCNSceneRendererDelegate {
#IBOutlet var sceneView: ARSCNView!
#IBOutlet var label: UILabel!
let cameraNode = SCNNode()
let sphereNode = SCNNode()
let config = ARWorldTrackingConfiguration()
public func renderer(_ renderer: SCNSceneRenderer,
updateAtTime time: TimeInterval) {
DispatchQueue.main.async {
if self.sceneView.isNode(self.sphereNode,
insideFrustumOf: self.cameraNode) {
self.label.text = "In the center..."
} else {
self.label.text = "Out OF CENTER"
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.allowsCameraControl = true
let scene = SCNScene()
sceneView.scene = scene
cameraNode.camera = SCNCamera()
cameraNode.camera?.fieldOfView = 10
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sceneView.pointOfView!.addChildNode(self.cameraNode)
}
sphereNode.geometry = SCNSphere(radius: 0.05)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
sphereNode.position.z = -1.0
sceneView.scene.rootNode.addChildNode(sphereNode)
sceneView.session.run(config)
}
}
Also, in this solution you may turn on an orthographic projection for child camera, instead of perspective one. It helps when a model is far from the camera.
cameraNode.camera?.usesOrthographicProjection = true
Here's how your screen might look like:
Next steps
The same way you can append two additional SCNCameras, place them above and below central SCNCamera, and test your object with two extra isNode(:insideFrustumOf:) instance methods.
I solved problem with another way:
let results = self.sceneView.hitTest(screenCenter!, options: [SCNHitTestOption.rootNode: parentnode])
where parentnode is the parent of target node, because I have multiple nodes.
func nodeInCenter() -> SCNNode? {
let x = (Int(sceneView.projectPoint(sceneView.pointOfView!.worldPosition).x - sceneView.projectPoint(sphereNode.worldPosition).x) ^^ 2) < 9
let y = (Int(sceneView.projectPoint(sceneView.pointOfView!.worldPosition).y - sceneView.projectPoint(sphereNode.worldPosition).y) ^^ 2) < 9
if x && y {
return node
}
return nil
}

how to track the position of a node after applying force in Arkit 1.5

As soon as a node starts moving, for some reason, it stops tracking the position of the node. and here's what I mean:
internal func launchNode(force:Float) {
let force = force * 0.004
currNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
currNode.physicsBody?.mass = 0.05
let direction = currNode.worldFront + SCNVector3(force, -force, -force)
currNode.physicsBody?.applyForce(direction, asImpulse: true)
}
The Node is moving, which is great, but I'd like to track the current position of the node while it's moving:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard currNode != nil else { return }
print("simdWorldPosition: \(currNode.simdWorldPosition.y) position: \(currNode.position.y) simWorldPosition: \(currNode.simdWorldPosition.y) simPosition: \(currNode.simdPosition.y)")
}
None of these properties is updating the location, so I know I'm missing something. I'd like to stop it from moving when it gets to a certain position (in y coordinate). If anyone had a success tracking the location of a node after .applyForce, I'd much appreciate it if you could point out what I did wrong, and perhaps what worked for you. thanks.
UPDATE
Here's some code I'm using to test it.
I'm declaring the node at the beginning of this ViewController:
var currNode = SCNNode()
You can try the following for testing to track the location of a node with the ARKit default project:
//call this once your scene is set
func addTapGestureToSceneView() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.didTap(withGestureRecognizer:)))
sceneView.addGestureRecognizer(tapGestureRecognizer)
}
#objc func didTap(withGestureRecognizer recognizer: UIGestureRecognizer) {
let tapLocation = recognizer.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation)
guard let node = hitTestResults.first?.node else { return }
if node.name == "shipMesh" {
currNode = node
moveShip()
}
}
internal func moveShip() {
let force = 2 * 0.004
currNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
currNode.physicsBody?.mass = 0.05
let direction = currNode.worldFront + SCNVector3(force, -force, -force)
currNode.physicsBody?.applyForce(direction, asImpulse: true)
}
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
print("simdWorldPosition: \(currNode.simdWorldPosition.y) position: \(currNode.position.y) simWorldPosition: \(currNode.simdWorldPosition.y) simPosition: \(currNode.simdPosition.y)")
}
So Answer of your question is in one line
use node.presentation property
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
print("simdWorldPosition: \(currNode.presentation.simdWorldPosition.y) position: \(currNode.presentation.position.y) simWorldPosition: \(currNode.presentation.simdWorldPosition.y) simPosition: \(currNode.presentation.simdPosition.y)")
}
From Apple Docs
When you use implicit animation (see SCNTransaction) to change a node’s properties, those node properties are set immediately to their target values, even though the animated node content appears to transition from the old property values to the new. During the animation SceneKit maintains a copy of the node, called the presentation node, whose properties reflect the transitory values determined by any in-flight animations currently affecting the node.
Hope it is helpful

Giving yourself a physics body in AR kit?

In my program, I am trying to make myself collidable with other objects so that I can test collisions. Is there a way to add a physics body that moves with myself in the new AR Kit which follows you? I can make one which is at my place at the beginning of the AR but I'm sure there is a way to add it to your actual self.
Good day!
There is a solution to your problem.
First of all you need to create a collider. For example:
func addCollider(position: SCNVector3, name: String) {
let collider = SCNNode(geometry: SCNSphere(radius: 0.5))
collider.position = position
collider.geometry?.firstMaterial?.diffuse.contents = UIColor.clear
collider.physicsBody?.categoryBitMask = 0 // example
collider.physicsBody?.contactTestBitMask = 1 // example
collider.name = name
sceneView.scene.rootNode.addChildNode(collider)
}
Don't forget to add proper bit masks, to collide with other objects.
Next you need to implement ARSCNViewDelegate protocol and call method willRenderScene. Next you need to locate your point of view and transform it into the matrix. Than you need to extract values from third column of the matrix to get current orientation. To get current location you need to extract values of forth column from your matrix. And finally you need to combine your location and orientation to get your current position.
To combine to SCNVector3 values you need to write global function "+". Example:
func +(left: SCNVector3, right: SCNVector3) -> SCNVector3 {
return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z)
}
Be sure, that you wrote this method outside of class or struct.
Final accord! A function that will move a collider accordingly to your current position.
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pontOfView = self.sceneView.pointOfView else { return }
let transform = pontOfView.transform
let orientation = SCNVector3(-transform.m31, -transform.m32, -transform.m33)
let location = SCNVector3(transform.m41, transform.m42, transform.m43)
let currentPosition = orientation + location
for node in sceneView.scene.rootNode.childNodes {
if node.name == "your collider name" {
node.position = currentPosition
}
}
}
P.S. Don't forget to add
self.sceneView.delegate = self
to your viewDidLoad.
Best of luck. I hope it helped!

Is there a way to read/get fps in spritekit swift

Is there a method to get the value of fps in swift?
No I dont mean changing the frameInterval
and no I dont mean
skView.showsFPS = true
I want to get the fps so that I can have my app wait until fps reaches 60 before it starts the game. For some reason my app occasionally gets 'stuck' at 40fps lingers for 5 seconds then slowly climbs to back to 60. This usually happens when I summon the ControlCenter.
You can calculate it yourself.
Doing something like this in your scene:
var lastUpdateTime: TimeInterval = 0
func update(currentTime: TimeInterval) {
let deltaTime = currentTime - lastUpdateTime
let currentFPS = 1 / deltaTime
print(currentFPS)
lastUpdateTime = currentTime
}
There's another way to get the FPS of the view itself independent from which scene is presented, using SKViewDelegate (taken from the Developer Documentation, just with the added FPS property):
class ViewController: NSViewController, SKViewDelegate {
let spriteView: SKView = SKView()
override func viewDidLoad() {
self.spriteView.delegate = self // Set the view's delegate to the ViewController.
}
let targetFps: TimeInterval = 60 // Set to your preferred fps
var lastRenderTime: TimeInterval = 0
var fps: TimeInterval = 0.0 // Access this property to get the view's fps
// Delegate method:
public func view(_ view: SKView, shouldRenderAtTime time: TimeInterval) -> Bool {
if time - lastRenderTime >= 1 / targetFps { // Check if the scene should render.
// Determine the fps with the time since the last render:
self.fps = 1 / (time - lastRenderTime)
// Set the last render time to the current time:
self.lastRenderTime = time
// Let scene know that it should render the next frame.
return true
}
else {
return false
}
}
}
gameView.showsStatistics = true