Positioning virtual assets with ARKit and SCNKit - ios

I'm trying to understand SCNKit and ARKit a little better and have a barebones Xcode 9 Augmented Reality app deployed and working on my iPhone (which I'm using as a simple test device).
This app's source code is here.
Basically, the app starts, the camera is initialized, and it renders a 3D fighter jet inside the scene (world view) in a similar fashion to how Pokemon Go injects monsters into your camera viewport (wherever your pointing your camera at). Pretty cool!
This code was all auto-generated for me by Xcode. So I'm trying to understand where the logic lives that determines where to position/orient the fighter jet (the SCN file titled art.scnassets/ship.scn). From here we see the jet being loaded:
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// Create a new scene
print("Hello there Mr. Zac")
let scene = SCNScene(named: "art.scnassets/ship.scn")!
// Set the scene to the view
sceneView.scene = scene
}
But I don't understand how the app chooses where to place the jet/ship and to orient it in which direction. I ask because as a first step I'd like to try repositioning the jet and then also swapping it out for my own asset files.

The "logic" for that, such as there is any, lives in two places.
The ship.scn file defines not just a model, but the model's position in the scene. (That is, in a global "world" coordinate space.)
In the scn file that ships in that Xcode project template, the model is positioned at something like 0, 0, -0.5, so if a camera is placed at the origin of the coordinate system, the ship appears directly in front of the camera, half a meter away.
ARKit itself defines scene/world space relative to the initial real-world position/orientation of the device. By default, that coordinate system's z-axis matches the initial orientation of the device, so anything placed "in front of" the coordinate origin will appear in front of the camera when you start the AR session.

Related

How to temporarily freeze a node in front of the camera using ARKit, SceneKit in Swift

I built a complete structure as a node (with its child nodes) and the user will walk through it using ARKit.
At some point, if the user cannot continue because of some real obstacle in the real world, I added a "pause" button which should freeze whatever the user currently sees in front of the camera, the user could then move freely to some other open space and when the user will release the pause button he/she will be able to resume where they left off (only someplace else in the real world).
A while ago I asked about it in the Apple Developer forum and an Apple Frameworks Engineer gave the following reply:
For "freezing" the scene, you could transform the anchor's position (in world coordinates) to camera coordinates, and then anchor your content to the camera. This will give you the effect that the scene is "frozen", i.e., does not move relative to the camera.
I'm currently not using an anchor because I don't necessarily need to find a flat surface. Rather, my node is placed at a certain position relative to where we start at (0,0,0).
My question is how do I exactly do what the Apple engineer told me to do?
I have the following code which I'm still stuck with. When I add the node to the camera (pointOfView, last line of the code below), it does freeze in place, but I can't get it to freeze in the same position and orientation as it was before it was frozen.
#IBAction func pauseButtonClicked(_ sender: UIButton) {
let currentPosition = sceneView.pointOfView?.position
let currentEulerAngles = sceneView.pointOfView?.eulerAngles
var internalNodeTraversal = lastNodeRootPosition - currentPosition! // for now, lastNodeRootPosition is (0,0,0)
internalNodeTraversal.y = lastNodeRootPosition.y + 20 // just so it’s positioned a little higher in front of the camera
myNode?.removeFromParentNode() // remove the node from the Real World view. Looks like this line has no effect and just adding the node as a child to the camera (pointOfView) is enough, but it feels more right to do this anyway.
myNode?.position = internalNodeTraversal // the whole node is moved respectively in the opposite direction from the root to where I’m standing to reposition the camera in my current position inside the node
// myNode?.eulerAngles = (currentEulerAngles! * -1) — this code put the whole node in weird positions so I removed it
myNode?.eulerAngles.y = currentEulerAngles!.y * -1 // opposite orientation of the node so the camera will be oriented in the same direction
myNode?.eulerAngles.x = 0.3 // just tilting it up a little bit to have a better view, more similar to the view as before it was locked to the camera
// I don’t think I need to change the eulerAngles.z
myNode!.convertPosition(internalNodeTraversal, to: sceneView.pointOfView) // I’m not sure I wrote this correctly. Also, this line doesn’t seem tp change anything
sceneView.pointOfView?.addChildNode(myNode!) // attaching the node to the camera so it will remain stuck while the user moves around until the button is released
}
So I first calculate where in the node I'm currently standing and then I change the position of the node in the opposite direction so that the camera will now be in that position. That seems to be correct.
Now I need to change the orientation of the node so that it will point in the right direction and here things get funky. I've been trying so many things for days now.
I use the eulerAngles for the orientation. If I set the whole vector multiplied by -1, it would show weird orientations. I ended up only using the eulerAngles.y which is the left/right orientation and I hardcoded the x orientation (up/down).
Ultimately what I have in the code above is the closest that I was able to get. If I'm pointing straight, the freeze will be correct. If I turn just a little bit, the freeze will be pretty close as well. Almost the same as what the user saw before the freeze. But the more I turn, the more the frozen image is off and more slanted. At some point (say I turn 50 or 60 degrees to the side) the whole node is off the camera and cannot be seen.
Somehow I have a feeling that there must be an easier and more correct way to achieve the above.
The Apple engineer wrote to "transform the anchor's position (in world coordinates) to camera coordinates". For that reason I added the "convertPosition" function in my code, but a) I'm not sure I used it correctly and b) it doesn't seem to change anything in my code if I have that line or not.
What am I doing wrong?
Any help would be very much appreciated.
Thanks!
I found the solution!
Actually, the problem I had was not even described as I didn't think it was relevant. I built the AR nodes 2 meters in front of the origin (-2 for the z-coordinate) while the center of my node was still at the origin. So when I changed the rotation or eulerAngles, it rotated around the origin so my nodes moved in a large curve and in fact also changed their position as a result.
The solution was to use a simdPivot. Instead of changing the position and rotation of the node itself, I created a translation matrix and a rotation matrix which was at the point of the camera (where the user is standing) and I then multiplied both matrices. Now when I added the node as a child of the camera (pointOfView) this would freeze the image and in effect show exactly what the user was seeing before it was frozen as the position is the same and the rotation is exactly around the user's standing position.

SceneKit - positioning SCNCamera and permitting object rotation

I am close to completing my first project in SceneKit but I'm struggling with the last few steps. It is probably easiest to explain my progress by sharing a short screen capture video of the Xcode Simulator displaying my current scene.
As you can see by the screen capture my project is composed of three elements (this is all done in code, I do not import any external assets):
outside box (defined via six SCNBox objects per corner)
inside sun (defined via a SCNTube object for the circle and UIBezierPath objects per "ray")
position of camera
Based on feedback I have committed the code to GitHub.
Right now the camera is allowed to rotate as seen in the screen capture but the centre of rotation of the camera and of the objects doesn't align so it appears to spin off-axis.
Here's where I want to get to:
correct camera position so that the combined box & sun is positioned directly in front of the camera, filling the screen
maintain the sun's position as being fixed (already done I guess)
allow the box to rotate freely in x, y & z around the sun based on touch input - so the user can "flick" the box and watch it flip and spin around the sun
The code structure is straight forward:
class GameViewController: UIViewController {
var gameView: SCNView!
var gameScene: SCNScene!
var cameraNode: SCNNode!
var targetCreationTime: TimeInterval = 0
override func viewDidLoad() {
super.viewDidLoad()
initView()
initScene() // createSun() and createCube() called here
initCamera()
}
And with respect to the camera position:
func initCamera() {
let camera = SCNCamera()
cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 0)
cameraNode.rotation = SCNVector4Make(1, 0, 0, .pi/2)
}
But what I've found is that despite playing around with the random cameraNode.position and cameraNode.rotation values the camera view doesn't seem to change.
My questions - any help will be greatly appreciated:
advice on repositioning the camera (what am I doing wrong?!) - once it's in the right place I can easily set "gameView.allowsCameraControl = false"
advice on how to enable the box to spin about its axis around the sun (while the sun remains fixed)
stretch goal! Any kind of general "check out this tutorial" type info on materials and lighting, and embedding this view into a SwiftUI view
Thanks!
I decided to stop fighting the point of rotation and instead reposition the elements around this.
One interesting thing, which I’ve mentioned at the start of the createBox() func.
// originally debugCube & debugNode were used for debugging the pivot point of the box
// but I found have this large node helped to balance out the centre of mass
// set to fully transparent and added to boxNode as final step after all other transformations
If you comment out the lines 19-26 plus 117 you will completely remove debugNode. And funnily enough when you do that the box stops spinning correctly. But you add it back in and everything is fixed. I’m guessing it’s adding “mass” to the overall node and helping lock the point of rotation to the correct position. So in the end I just made it transparent!
The final (version 1.0) code is posted on GitHub at github.com/LedenMcLeden/logo
Use this answer in post for your camera: 57586437, remove camera rotation and take camera control off. Rotate your box with a simple (I'd do an x,y,z independent spin just to verify it) spin so that you'll know if your pivot point is correct. It should be ok by default and spin in place right in front of the camera, but depends on how you built your cube.
If you added the sun and stuff as a subnode of your box, then you're probably in decent shape and the pieces will rotate together.
If you want to do camera rotations similar to cameraControl, then you'll need to add a gesture recognizer and then you can start experimenting with it.
Hope that helps!

How can I load multiple nodes of the same 3d model all at once?

I'm working on an ARKit application where you would walk through a portal and you'd be somewhere else. I purchased a 3d model of an archway to serve as the portal. I took the model in to Blender to flatten it down to one piece, but the texture disappeared and whenever I apply a new texture it only shows up on one portion of the model.
Here is the original model, only modified by changing it to .dae format. however, I can't load arch1, arch2, arch3, etc. without writing the same line over and over
With this model I'd be writing the same code over and over
private func addPortalArch(hitResult: ARHitTestResult) {
let portalScene = SCNScene(named: "art.scnassets/Medieval_portal.dae")
let portalNode = portalScene?.rootNode.childNode(withName: "arch1", recursively: true)
// I'd be writing this portalNode = portalScene line over and over for each arch.
//I'd also have to get the position for each piece as far as I understand, which I do not know how I'd begin to do that based on where the user taps to place the portal.
portalNode?.position = SCNVector3(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
self.sceneView.scene.rootNode.addChildNode(portalNode!)
}
I need the model to be all once piece but still have the same stonework material over the whole of it.
Looking at the file you sent, your best bet is to use the model which contains each aspect of the Arch (your 1st picture).
The 1st step (not always necessary but useful) is to convert the dae file to an .scn file:
Having done this you then want to create an Empty Node to be the Portal Door RootNode and make each element of the Portal a child of this e.g:
You will also need to rotate each ChildNode's (not the PortalRoot) Euler Angle X by -90 (as when you load the model the Portal Door is on it's side):
Now since you have a single RootNode, there is no need to loop through all the nodes so that is displayed on the screen. You can simple use a function like this:
/// Places The Portal Door 5m Aeay From The Camera
func setupPortalDoor(){
//1. Get The Portal Scene
guard let portalScene = SCNScene(named: "portalDoor.scn"),
//2. Get The Portal Node
let portalModel = portalScene.rootNode.childNode(withName: "PortalRoot", recursively: false) else { return }
//3. Add To The ARSCNView Root
augmentedRealityView?.scene.rootNode.addChildNode(portalModel)
//4. Position It 5m Away From The Camera
portalModel.position = SCNVector3(0, 0, -5)
/*
The Portal Is Large So I Suggest Scaling It ^__________*
*/
}
If you later wanted to change the portal arch to say marble you can easily do it like so:
for node in portalModel.childNodes{
node.geometry?.firstMaterial?.diffuse.contents = UIColor.cyan
}
Hope this helps...

Plot an SCNNode in a ARSCNVIEW irespective of the Camera's position

I am developing an iOS in-store navigation app for a retail store using AR Scene Kit which should resembles as Lowe's Vision Navigation. Hence, At first I want to programatically plot the position of any xyz product which is available in the store in an AR Scene Kit irrespective of the camera's initial position, but the position of the Product will remain same in the Store. I was totally new to AR as well as Scene Kit.
I am able to add a SCNNode in the ARSCNView but the problem will be my camera's initial position according to that only the SCNNode is plotting. Once this is done, then I need to give the in-store navigation for the selected product from my position inside the store may be using iBeacon or other equivalent.
SCNNodes operate somewhat like UIViews, in the sense that, as you said, the positions are relative to the parent node/view. For the cases where you want the position relative to the whole view or the world you can use worldTransform and -convertPosition:toNode: .
World Transform
Convert Position To Node

How do I know when geometry (SCNNodes) are at the edge of the view?

I want to do an endless parallax style background with SceneKit, but I don't think I understand quite how to detect when a mesh is "off camera".
The scene renderer (only the view on iOS) can check if an object is inside the frustum (the shape of what is viewable) from a specific point of view (like a camera) using isNodeInsideFrustum:withPointOfView:
This checks if the bounding box of the node is inside of the frustum (ignoring if it's obscured by something else). I.e. the node is in the viewable region but it's not guaranteed to be visible on the screen.
To check if something is "off camera", you can check if it's outside the viewable region for that camera (here I've assumed that the camera is the scene view's point of view):
BOOL isOffCamera = ![yourSceneView isNodeInsideFrustum:theNodeToCheck
withPointOfView:yourSceneView.pointOfView];

Resources