SCNText with SCNLookAtConstraint looking the wrong way - ios

I want to display text alongside my 3D model using SceneKit in iOS. It just has to appear like it is 2D. So the text should always be facing the camera, however I rotate the 3D model. I have an SCNNode with a SCNText geometry and I attached an SCNLookAtConstraint like so:
let constraint = SCNLookAtConstraint(target: cameraNode)
constraint.gimbalLockEnabled = true
textNode.constraints = [constraint]
They seem to be facing the camera, but only the wrong way! I see all text mirrored! Also, sometimes the text rotates, I want it to be aligned horizontally at all times. So this is all the behaviour I don't want.
Can somebody help me out here? I just want to have some text that follows a few nodes in my 3D object. I don't care how. It doesn't have to be SCNText for all I care, if this can be done with a simple UILabel: fine with me! Just tell me how!

From the documentation:
When SceneKit evaluates a look-at constraint, it updates the constrained node’s transform property so that the node’s negative z-axis points toward the constraint’s target node.
You can workaround that by setting the node's pivot or by using an intermediate node. That node would be constrained, and would have the text as a child node (which would be rotated by π along the y axis)
Update
Starting iOS 11.0 the SCNLookAtConstraint class exposes the localFront property which allows you to specify a different front axis. It defaults to (0, 0, -1) and you can change it to (0, 0, 1) to achieve what you want.

Based on #mnuages response, setting the node's pivot to the following should do the trick.
node.pivot = SCNMatrix4Rotate(node.pivot, Float.pi, 0, 1, 0)

I faced this problem on iOS11 beta 5, Swift4:
//1. Rotate your `textNode`, and don't add `textNode` into `sceneView`
textNode.eulerAngles = SCNVector3Make(0, .pi, 0)
//2. Create new wrapper node, add `textNode` as a child node
let textWrapperNode = SCNNode()
textWrapperNode.addChildNode(textNode)
//3. Add constraint for wrapper node
let constraint = SCNLookAtConstraint(target: cameraNode)
constraint.gimbalLockEnabled = true
textWrapperNode.constraints = [constraint]
//4. Add wrapper node into `sceneView`
sceneView.scene.rootNode.addChildNode(textNode)

Related

UILabel text doesn't appear when using ARKit

I'm programmatically generating a set of UILabels, attaching them to SCNNodes and then placing them in a scene.
The problem is that the text on some of the labels doesn't appear. This occurs (seemingly) randomly.
Here's the code:
var labels = [SCNNode]()
var index: Int
var x: Float
var y: Float
let N = 3
for i in 0 ... N-1 {
for k in 0 ... N-1 {
let node = label()
labels.append(node)
index = labels.count - 1
x = Float(i) * 0.5 - 0.5
y = Float(k) * 0.5 - 0.5
sceneView.scene.rootNode.addChildNode(labels[index])
labels[index].position = SCNVector3Make(x, y, -1)
}
}
and the method to create the label node:
func label() -> SCNNode {
let node = SCNNode()
let label = UILabel(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
width: CGFloat(100), height: CGFloat(50)))
let plane = SCNPlane(width: 0.2, height: 0.1)
label.text = "test"
label.adjustsFontSizeToFitWidth = true
plane.firstMaterial?.diffuse.contents = label
node.geometry = plane
return node
}
The labels themselves always appear correctly, it's just that some of them are blank, with no text.
I've tried playing around with the size of the label, the size of the plane it is attached to, the font size etc - nothing seems to work.
I've also tried enclosing the label creation in DispatchQueue.main.async { ... }, which didn't help either.
I'm moderately new to Swift and iOS, so could easily have missed something very obvious.
Thanks in advance!
EDIT:
(1) Setting label.backgroundColor = UIColor.magenta makes it clear that in fact the label is not being created, but the node / plane is.
Some of the labels are left white (i.e only the SCNNode is being rendered), however after a short delay they sometimes then become magenta and the text will appear. Some of the labels will remain missing though.
(2) It further appears that it's related to the position and orientation of the node (label) relative to the camera. I created a large (10x10) grid of labels, then tested placing the camera at different initial positions in the grid. The likelihood that a node appeared seemed directly related to the distance of the node from the initial camera position. Those nodes directly in front of the camera were always rendered, and those far away almost never were.
(3) workaround / hack is to convert the labels to images, and use them instead - code is at https://github.com/Jordan-Campbell/uiimage-arkit-testing if anyone is interested.
If you are labeling things in AR, 99% of the time it is better to do so in "Screen Space" rather than in "Perspective".
Benefits of labels in Screen Space:
ALWAYS readable, regardless of user's distance from the label
You can use regular UILabels, no need to draw them to an image and then map the image to an SCNPlane.
Your app will have a first party feel to it because Apple uses Screen Space for their labels in all of their AR apps (see Measure).
You will be able to use standard animations on your UILabel, animations are much more complex to set up when working with content in Perspective.
If you are sold on Screen Space, let me know and I'll be happy to put up some code showing you the basics.
Use main thread for creating and adding labels to scene. This will make things faster, and avoid coupling this addition to scene with plane detection this really slows down the rendering. this works at times.
DispatchQueue.main.async {
}
Use a UIView SuperView as Parent to your label this would make things smoother.

How to use SKConstraint in swift

I have a moving camera in my scene, which always follows my player. But I also have some other content(On screen controls) which I want to stay in a single place on the screen, but when the camera moves, The controls are moving away to. How would I go about doing this. I have searched a lot, and found the SKConstraint, but I couldn't find any tutorials to use it in swift 3.
Should I be using the SKConstraint? If yes, how can I use it, if no, how do I keep the controls at a certain position on the screen at all times.
I also know that I could change the position of the controls in the update method, but I do not want to do that as there are many on screen controls, and I try to refrain from writing code in the update method as much as possible.
Any help would be appreciated, thanks!
You can use an SKConstraint to cause a camera to follow a character.
First, create a camera node and a hero and add them to the scene
let cameraNode = SKCameraNode()
let hero = SKSpriteNode(imageNamed: "Spaceship")
addChild(hero)
camera = cameraNode
addChild(cameraNode)
Next, create a constraint and assign it to the camera node's constraints property
let range = SKRange(constantValue:0)
let constraint = SKConstraint.distance(range, to: hero)
cameraNode.constraints = [constraint]
Lastly, if you have controls or labels that need to be at fixed locations relative to the camera, you can add them to the camera node
let label = SKLabelNode(text: "Score: 123")
// Position the label relative to the camera node
label.position = CGPoint(x: 100, y: 100)
cameraNode.addChild(label)

SCNBillboardConstraint isn’t working. Node with constraint doesn’t change

Perhaps I am not setting up the camera properly… I’m starting with a scn file with a camera. In Xcode, rotating the free camera around, the geometries rotate as expected. However, at runtime, nothing happens.
It doesn’t seem to matter if I add the constraint in code or in the editor. The look at constraint works.
It also doesn’t seem to matter if I use the camera from the scn file or if I add a camera in code.
The sample code is
class Poster: SCNNode {
let match:GKTurnBasedMatch
init(match:GKTurnBasedMatch, width:CGFloat, height:CGFloat) {
self.match = match
super.init()
// SCNPlane(width: width, height: height)
self.geometry = SCNBox(width: width, height: height, length: 0.1, chamferRadius: 0)
self.constraints = [SCNBillboardConstraint()]
self.updatePosterImage()
}
}
So… I gave up on the billboard constraint.
I’m using a SCNLookAtConstraint that looks at the camera node, with the gimbal lock enabled.
I was using a SCNPlane but it was doing weird stuff. So I went with a SCNBox for the geometry.
So, in the constructor:
self.geometry = SCNBox(width: self.size.width, height: self.size.height, length: 0.1, chamferRadius: 0)
let it = SCNLookAtConstraint(target: cameraNode)
it.isGimbalLockEnabled = true
self.constraints = [it]
It works.
You're missing ".init()" on the SCNBillboardConstraint(). This line alone did all the work for me:
node.constraints = [SCNBillboardConstraint.init()]
Try this:
A constraint that orients theNode to always point toward the current camera.
// Create the constraint
SCNBillboardConstraint *aConstraint = [SCNBillboardConstraint billboardConstraint];
theNode.constraints = #[aConstraint];
theNode is the node you want pointing to the camera. This should work.
Updated
Ok, if you were to create a sample Project with the Game template. And then make the following changes:
// create a clone of the ship, change position and rotation
SCNNode *ship2 = [ship clone];
ship2.position = SCNVector3Make(0, 4, 0);
ship2.eulerAngles = SCNVector3Make(0, M_PI_2, 0);
[scene.rootNode addChildNode:ship2];
// Add the constraint to `ship`
SCNBillboardConstraint *aConstraint = [SCNBillboardConstraint billboardConstraint];
ship.constraints = #[aConstraint];
ship is constrained but ship2 isn't.
If you were to add this:
ship2.constraints = #[aConstraint];
Now, both will face the camera. Isn't this what you are looking for?
By any chance are you using the allowsCameraControl property on SCNView?
If so, remember setting that will add a new camera to your scene (cloning an existing camera if one is present to match its settings), so if you create a constraint to your own camera, the constraint will not be linked to the camera that’s actually being used.
Per Apple in their WWDC 2017 video, they say that property is really for debugging purposes, not for a real-world use.
Simply put, you have to ensure you are moving around your own camera, not relying on the auto-created one.

How to position a SCNNode to cover the whole SCNView?

I am very new to SceneKit and your help will be really appreciated!
I have a 200x200 sized SCNView in my UIView, which is at the centre of super view.
I want to put a SCNCylinder inside, such that the SCNCylinder covers full SCNView. I read that all these views of Scenekit are defined in meters, so how do I form a relationship between the dimensions of my screen and the
SCNCylinder.
I tried:
var coinNode = SCNNode()
let coinGeometry = SCNCylinder(radius: 100, height: 2)
coinNode = SCNNode(geometry: coinGeometry)
coinNode.position = SCNVector3Make(0, 0, 0)
coinScene.rootNode.addChildNode(coinNode)
let rotate90AboutZ = SCNAction.rotateByX(-CGFloat(M_PI_2), y: 0.0, z: CGFloat(M_PI_2), duration: 0.0)
coinNode.runAction(rotate90AboutZ)
ibOutletScene.scene = coinScene
But this leaves a margin between my coinScene and the ibOutletScene. How do I remove this space?
I also tried adding Camera:
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 0, 100)
coinScene.rootNode.addChildNode(cameraNode)
But I see random behaviour with this and the coinNode gets hidden! How should I position my camera? Or is there any other way to remove extra space from my ibOutletScene?
Edit:
This is how it looks if I don't add camera. There is a margin between red scene and green coin. I tried multiple sizes for the coin, but I am unable to remove this margin unless I add a camera. But, If I add camera, I get another problem, mentioned below this screenshot.
If I don't add the camera, The rotation animation on coin works perfectly, but If I add camera,the rotation enlarges the and then becomes small again with the animation. How can I rotate it on its axis, without increasing the size?
I am using following code to rotate the coin:
The same code works fine without camera, but enlarges the coin after adding camera. Checkout the snapshot.
let rotate = SCNAction.rotateByX(0, y: -2 * CGFloat(M_PI_2), z: 0, duration: 2)
coinNode.runAction(rotate)
The random behavior might be caused by the last line in your first code snippet. You're starting an animation, and then adding the scene to the view.
Instead, build your scene, attach it to the view, and then start your animation. Setting a non-zero duration for the action will give you a more pleasing transition.
As for the extra space, it would help us understand if you post a screenshot. But you're going to have to do a bit of trigonometry.
It looks like you have a scene that you want to be blocked by a coin, that then rotates out of the way? Simulate that yourself with real objects. Put your eye down at the edge of your desk. Put a coin out a ways from your eye. How far does that coin have to be in order to block particular objects farther away on your desk?
In SceneKit, you can query the field of view of the SCNCamera. You know the size of your coin and the size of the view. Calculate the distance from the camera needed for the projected diameter of your coin to equal the width of your view. Put the coin there.

SpriteKit: What's up with the coordinate system?

I'm teaching myself how to do SpriteKit programming by coding up a simple game that requires that I lay out a square "game field" on the left side of a landscape-oriented scene. I'm just using the stock 1024x768 view you get when creating a new SpriteKit "Game" project in XCode - nothing fancy. When I set up the game field in didMoveToView(), however, I'm finding the coordinate system to be a little weird. First of all, I expected I would have to place the board at (0, 0) for it to appear in the lower-left. Not so -- it turns out the game board has to be bumped up about 96 pixels in the y direction to work. So I end up with this weird code:
let gameFieldOrigin = CGPoint(x:0, y:96) // ???
let gameFieldSize = CGSize(width:560, height: 560)
let gameField = CGRect(origin: gameFieldOrigin, size: gameFieldSize)
gameBorder = SKShapeNode(rect: gameField)
gameBorder.strokeColor = UIColor.redColor()
gameBorder.lineWidth = 0.1
self.addChild(gameBorder) // "self" is the SKScene subclass GameScene
Furthermore, when I add a child to it (a ball that bounces inside the field), I assumed I would just use relative coordinates to place it in the center. However, I ended up having to use "absolute" coordinate and I had to offset the y-coordinate by 96 again.
Another thing I noticed is when I called touch.locationInNode(gameBorder), the coordinates were again not relative to the border, and start at (0, 96) at the bottom of the border instead of (0, 0) as I would have guessed.
So what am I missing here? Am I misunderstanding something fundamental about how coordinates work?
[PS: I wanted to add the tag "SpriteKit" to this question, but I don't have enough rep. :/]
You want to reference the whole screen as a coordinate system, but you're actually setting all the things on a scene loading from GameScene.sks. The right way to do is modify one line in your GameViewController.swift in order to set your scene size same as the screen size. Initialize scene size like this instead of unarchiving from .sks file:
let scene = GameScene(size: view.bounds.size)
Don't forget to remove the if-statement as well because we don't need it any more. In this way, the (0, 0) is at the lower-left corner.
To put something, e.g. aNode, in the center of the scene, you can set its position like:
aNode.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));

Resources