Aligning SCNText with SCNBox in iOS SceneKit with boundingBox? - ios

REVISED PROBLEM:
I don't understand why the white node is centered when it's a box or sphere but not when it's text. You can comment/uncomment the whiteGeometry variable to see how each different geometry is displayed. I was originally thinking that I had to manually center the text by determining the width of the box and the text and calculating the position of the text. Do I need to do that? Why is the text behaving differently?
import SceneKit
import PlaygroundSupport
let scene = SCNScene()
let sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
sceneView.scene = scene
sceneView.backgroundColor = .darkGray
sceneView.autoenablesDefaultLighting = true
sceneView.allowsCameraControl = true
PlaygroundPage.current.liveView = sceneView
// Camera
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 0, z: 25)
sceneView.pointOfView = cameraNode
let blackGeometry = SCNBox(width: 10.0, height: 10.0, length: 10.0, chamferRadius: 0)
blackGeometry.firstMaterial?.diffuse.contents = UIColor.black
print("blackGeometry min=\(blackGeometry.boundingBox.min)")
print("blackGeometry max=\(blackGeometry.boundingBox.max)")
let blackNode = SCNNode(geometry: blackGeometry)
blackNode.position = SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(blackNode)
// let whiteGeometry = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0)
let whiteGeometry = SCNText(string: "L", extrusionDepth: 0)
whiteGeometry.alignmentMode = kCAAlignmentLeft
whiteGeometry.font = UIFont.systemFont(ofSize: 8.0)
// let whiteGeometry = SCNSphere(radius: 3.0)
whiteGeometry.firstMaterial?.diffuse.contents = UIColor.white
print("whiteGeometry min=\(whiteGeometry.boundingBox.min)")
print("whiteGeometry max=\(whiteGeometry.boundingBox.max)")
let whiteNode = SCNNode(geometry: whiteGeometry)
let boxWidth = blackGeometry.boundingBox.max.x - blackGeometry.boundingBox.min.x
let boxHeight = blackGeometry.boundingBox.max.y - blackGeometry.boundingBox.min.y
print("boxWidth=\(boxWidth)")
print("boxHeight=\(boxHeight)")
let txtWidth = whiteGeometry.boundingBox.max.x - whiteGeometry.boundingBox.min.x
let txtHeight = whiteGeometry.boundingBox.max.y - whiteGeometry.boundingBox.min.y
print("txtWidth=\(txtWidth)")
print("txtHeight=\(txtHeight)")
whiteNode.position = SCNVector3(x: -5.0, y: -5.0, z: 10)
scene.rootNode.addChildNode(whiteNode)
//blackNode.addChildNode(whiteNode)
print("done")
ORIGINAL PROBLEM (OLD):
Let's say I have two SCNBox nodes (I'm simplifying this to make it clear BUT the solution must work for other geometries). A large black box and a small white box. I want to center the white box in front of the black box.
To do this, I need to determine the width and height of the two nodes. Remember that the node could be something other than a box like a sphere or text. From what I can tell, the only way to determine width/height is via the boundingBox property on the geometry. It has a min and max value that is NOT clearly and fully described in Apple's reference manual. To get the height, it seems like I would calculate it based on boundingBox.max.y and boundingBox.min.y. So looking at the example below of a 10x10x10 box, I can't see how I can get 10.0 as the height because max.y 5.20507812
e.g.
let box = SCNBox(width: 10.0, height: 10.0, length: 10.0, chamferRadius: 0)
print("box min=\(box.boundingBox.min)")
print("box max=\(box.boundingBox.max)")
yields:
box min=SCNVector3(x: -5.0, y: -5.0, z: -5.0)
box max=SCNVector3(x: 5.0, y: 5.20507812, z: 5.0)
Why is max.y=5.20507812? How should I determine the height/width?
See also: SCNBoundingVolume

I think I understand now.
It looks like you're trying to put some text in front of an object, with the text centered on the object relative to the camera. And this question isn't about bounding boxes per se. Right?
You see different behavior with the white geometry being a sphere or SCNText because SCNText's uses a different origin convention than the other concrete SCNGeometry classes. Other geometries put the origin at the center. SCNText puts it at the lower left of the text. If I remember correctly, "lower" means the bottom of the font (lowest descender), not the baseline.
So you'll need to compute the text's bounding box, and account for that when you position the text node, to get it centered. You don't need to do that for the other geometry types. If the covered object or the camera is moving, you'll need to compute the ray from the camera to the covered object in the render loop, and update the text's position.
Swift Scenekit - Centering SCNText - the getBoundingBoxMin:Max issue is relevant.
If you just want overlay text, you might find it easier to put the text in an SKScene overlay. An example of that is at https://github.com/halmueller/ImmersiveInterfaces/tree/master/Tracking%20Overlay (warning, bit rot may have set in, I haven't tried that code in a while).

Related

Scene Kit node not rotating

I am trying to rotate by scene kit node, however it is not rotating.
I want it to rotate around the y axis. It is a sphere.
let node = SCNNode()
node.geometry = SCNSphere(radius: 1)
node.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "diffuse.png")
node.geometry?.firstMaterial?.specular.contents = UIImage(named: "specular.png")
node.geometry?.firstMaterial?.emission.contents = UIImage(named: "emission.png")
scene.rootNode.addChildNode(node)
let action = SCNAction.rotate(by:360 * CGFloat((Double.pi)/180.0), around: SCNVector3(x: 0, y: 1, z: 0), duration: 8)
let repeatAction = SCNAction.repeatForever(action)
node.runAction(repeatAction)
Are you certain it isn't rotating? I'm not sure what your diffuse, specular and emission images are but maybe because it's a sphere it just doesn't really look like it is rotating because it looks the same from all sides.
Running the same action on a cube in viewDidAppear is working for me, so it could just be that you can't really notice the sphere rotating. I used this code:
let node = SCNNode()
node.geometry = SCNBox (width: 0.5, height: 0.5, length: 0.5, chamferRadius: 0.1)
node.position = SCNVector3Make(0, 0, -1)
sceneView.scene.rootNode.addChildNode(node)
let action = SCNAction.rotate(by:360 * CGFloat((Double.pi)/180.0), around: SCNVector3(x: 0, y: 1, z: 0), duration: 8)
let repeatAction = SCNAction.repeatForever(action)
node.runAction(repeatAction)
The cube is rotating fine with the way that you rotated the SCNNode.
Hope this helps

Why does calculateAccumulatedFrame give an unexpected CGRect?

I'm trying to determine the right scaling factor for my node tree to make it fit exactly in my presentation rectangle, so I'm trying to find the smallest bounding rectangle around all my nodes. Apple's docs say that calculateAccumulatedFrame "Calculates a rectangle in the parent’s coordinate system that contains the content of the node and all of its descendants." That sounds like what I need, but it's not giving me the tight fit that I expect. My complete playground code is:
import SpriteKit
import PlaygroundSupport
let view:SKView = SKView(frame: CGRect(x: 0, y: 0, width: 800, height: 800))
PlaygroundPage.current.liveView = view
let scene:SKScene = SKScene(size: CGSize(width: 1000, height: 800))
scene.scaleMode = SKSceneScaleMode.aspectFill
view.presentScene(scene)
let yellowBox = SKSpriteNode(color: .yellow, size:CGSize(width: 300, height: 300))
yellowBox.position = CGPoint(x: 400, y: 500)
yellowBox.zRotation = CGFloat.pi / 10
scene.addChild(yellowBox)
let greenCircle = SKShapeNode(circleOfRadius: 100)
greenCircle.fillColor = .green
greenCircle.position = CGPoint(x: 300, y: 50)
greenCircle.frame
yellowBox.addChild(greenCircle)
let uberFrame = yellowBox.calculateAccumulatedFrame()
let blueBox = SKShapeNode(rect: uberFrame)
blueBox.strokeColor = .blue
blueBox.lineWidth = 2
scene.addChild(blueBox)
And the results are:
The left and bottom edges of the blue rectangle look good, but why are there gaps between the blue rectangle and the green circle on the top and right?
The notion "frame" does funny things when you add a transform. The bounding box around the box and circle is a rectangle. You have rotated that rectangle. Therefore its corners stick out. The accumulated frame embraces that rotated rectangle, including the sticking-out corners. It does not magically hug the drawn appearance of the nodes (e.g. the circle).

Don't draw node outside of parent node

I'm making a SpriteKit game on iOS using Swift (although an Objective-C answer would still help), and I have an SKSpriteNode whose parent is another SKSpriteNode. However, I only want to sub-node to be drawn when it is within the frame of the parent node. So for example, I have:
let scene = GameScene(size: CGSize(width: 10, height: 10))
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let skView = self.view as! SKView
skView.presentScene(scene)
let superNode = SKSpriteNode(color: UIColor.blueColor(), size: CGSize(width: 1, height: 1))
scene.addChild(superNode)
let subNode = SKSpriteNode(color: UIColor.redColor(), size: CGSize(width: 0.5, height: 0.5))
superNode.addChild(subNode)
It looks as follows:
However, if I move subNode to (1.5, 1.5):
subNode.position = CGPoint(1.5, 1.5)
Then subNode is visually outside of superNode, but it is still drawn:
However, I would like it to look as follows:
As a further example, if I set subNode's position to (1.0, 1.0), it looks as follows:
But I would like it to look like this:
How do I get it to only be drawn when it is within superNode? Any help would be much appreciated. Thanks:)

Create smoother edges or otherwise fix jagged edges on box in SCNView?

The code below generates a red box in a SCNView. However, the edges are jagged along the top and bottom of the side/element facing you (as illustrated by the attachment). The goal is to render smoother edges similar to Minecraft boxes. Changing the camera position reduces the pixelation of certain edges, so is this a camera angle issue? If yes, is it possible to render boxes with smooth edges no matter the camera angle?
For instance setting the camera position to SCNVector3(x: 0.0, y: 0.0, z: 10.0) renders the box effectively in 2D and with crisp edges (second attachment).
let sceneView = SCNView(frame: self.view.frame)
self.view.addSubview(sceneView)
let scene = SCNScene()
sceneView.scene = scene
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: -3.0, y: 0.0, z: 10.0)
let ambientLight = SCNLight()
ambientLight.type = SCNLightTypeAmbient
ambientLight.color = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0)
cameraNode.light = ambientLight
let light = SCNLight()
light.type = SCNLightTypeOmni
let lightNode = SCNNode()
lightNode.light = light
lightNode.position = SCNVector3(x: 0, y: 20, z: 10)
let cubeGeometry = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0.0)
let cubeNode = SCNNode(geometry: cubeGeometry)
let planeGeometry = SCNPlane(width: 100.0, height: 100.0)
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.eulerAngles = SCNVector3(x: GLKMathDegreesToRadians(-90), y: 0, z: 0)
planeNode.position = SCNVector3(x: 0, y: -0.5, z: 0)
let redMaterial = SCNMaterial()
redMaterial.diffuse.contents = UIColor.redColor()
cubeGeometry.materials = [redMaterial]
let greenMaterial = SCNMaterial()
greenMaterial.diffuse.contents = UIColor.greenColor()
planeGeometry.materials = [greenMaterial]
let constraint = SCNLookAtConstraint(target: cubeNode)
constraint.gimbalLockEnabled = true
cameraNode.constraints = [constraint]
lightNode.constraints = [constraint]
scene.rootNode.addChildNode(lightNode)
scene.rootNode.addChildNode(cameraNode)
scene.rootNode.addChildNode(cubeNode)
scene.rootNode.addChildNode(planeNode)
As mentioned in the comments, if you want to antialias every frame set the antialiasingMode. It doesn’t do as nice a job as CoreGraphics but it’s reasonably nice. However, it does increase the workload a ton, so you’re going to burn batteries and slow down your framerate.
On OS X the default is to 4x multisample every frame. On iOS the default is no multisampling, since it’s so expensive.
Luckily there’s a solution I much prefer, which is to turn on jittering. It’s kind of like multisampling but lazy. When objects are moving they are rendered normally (with jaggies), but when they stop moving the renderer runs like 96 more frames in the background and smooths out all the jaggies.
The final look is much better than antialiasing (because there’s no “96x antialiasing mode”) and on fast hardware most people I’ve shown it to don’t realize that the quality is changing when objects move.

SCNShape with bezier path

I am tying to draw a 3d line in Scenekit iOS, i am trying below code to draw line,
func path() -> UIBezierPath
{
var bPath = UIBezierPath()
bPath.moveToPoint(CGPointMake(0, 0))
bPath.addLineToPoint(CGPointMake(10, 10))
bPath.addLineToPoint(CGPointMake(5, 5))
bPath.closePath()
return bPath
}
and the below code is to load the line in SCNNode,
let sap = SCNShape()
sap.path = path()
let carbonNode = SCNNode(geometry: sap)
carbonNode.position = SCNVector3Make(0, 0, 15)
scene.rootNode.addChildNode(carbonNode)
But, i am getting blank screen.
try changing your geometry's materials. You might have a white object on a white background.
edit:
it also turns out that the vertices of your triangle are colinear. You thus end up with a degenerate triangle.
Your path has no surface area since you start at (0,0), draw a line to (10, 10) and then a second line to (5,5) which is already on the line between (0,0) and (10, 10). Then you close the path by drawing another line to (0,0).
Changing either of the three points creates a path with a surface area.
Also, the z-axis points out of the screen. This means that depending on where your camera is positioned, you may be placing the carbonNode behind the camera. If instead you meant to position it further into the scene you should likely use a negative z value.
Here's some code I wrote to address the problem.
// Material colors
let cyanMaterial = SCNMaterial()
cyanMaterial.diffuse.contents = UIColor.cyanColor()
let anOrangeMaterial = SCNMaterial()
anOrangeMaterial.diffuse.contents = UIColor.orangeColor()
let aPurpleMaterial = SCNMaterial()
aPurpleMaterial.diffuse.contents = UIColor.purpleColor()
// A bezier path
let bezierPath = UIBezierPath()
bezierPath.moveToPoint(CGPointMake(-0.25, 0))
bezierPath.addLineToPoint(CGPointMake(0, -0.25))
bezierPath.addLineToPoint(CGPointMake(0.25, 0))
bezierPath.addLineToPoint(CGPointMake(0, 0.25))
bezierPath.closePath()
// Add shape
let shape = SCNShape(path: bezierPath, extrusionDepth: 0.75)
shape.materials = [cyanMaterial, anOrangeMaterial, aPurpleMaterial]
let shapeNode = SCNNode(geometry: shape)
shapeNode.position = SCNVector3(x: 0.2, y: 0.75, z: 0.1);
self.rootNode.addChildNode(shapeNode)
shapeNode.rotation = SCNVector4(x: -1.0, y: -1.0, z: 0.0, w: 0.0)
Keep in mind that the numbers, the Scene Kit scale, are in meters. So set the numbers similarly to those that work for the other shapes you create.

Resources