Swift - How to change the Pivot of a SCNNode object - swift

I've been playing with the SCNNode object for a while now and I'm lost with the Pivot. How can I change the pivot of a SCNNode (SCNBox as a bar) and place the pivot on one of the edge of the bar?

A node's pivot is a transformation matrix, the inverse of which is applied to the node before its transform property takes effect. For example, take a look at this bit from the default SceneKit Game template in Xcode:
let boxNode = SCNNode()
boxNode.geometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.02)
If you set the boxNode's position, that point corresponds to the center of the cube, and if you rotate it (as the template does in an animation), it spins around its center.
To change the anchor point, set the pivot to a translation transform:
boxNode.pivot = SCNMatrix4MakeTranslation(0.5, 0.5, 0.5)
Now, when you set the position that point corresponds to the top-right-front corner of the cube, and when you rotate the cube it spins around that corner.
More generally, a pivot transforms the contents of a node relative to the node's own transform. Suppose you wanted to model the precession of the Earth's axis of rotation. You could do this by creating two animations: one that animates pivot to spin the node around its own Y axis, and another that animates rotation to move that axis relative to the space containing the node.

On the pivot topic:
Just in case you do not have dimensions for your geometry/node something like this might help (especially for SCNText).
var minVec = SCNVector3Zero
var maxVec = SCNVector3Zero
if node.getBoundingBoxMin(&minVec, max: &maxVec) {
let bound = SCNVector3(x: maxVec.x + minVec.x,
y: maxVec.y + minVec.y,
z: maxVec.z + minVec.z)
node.pivot = SCNMatrix4MakeTranslation(bound.x / 2,
bound.y / 2,
bound.z / 2)
}

Related

Keep node's X and Z axes parallel to ground while rotating Y axis to face camera

I'm trying to keep an SCNNode always one meter away from the front of the camera, and manipulate the node so that the X and Z axes are always parallel to the ground, while the node rotates around the Y-axis so that the node is always facing the camera.
The code below achieves my goal for the most part, but when turning more than 90˚ clockwise or counterclockwise, the node starts turning. How can I fix that?
override func viewDidLoad() {
super.viewDidLoad()
boxParent.position = (sceneView.pointOfView?.position)!
boxParent.orientation = (sceneView.pointOfView?.orientation)!
boxParent.eulerAngles = (sceneView.pointOfView?.eulerAngles)!
sceneView.scene.rootNode.addChildNode(boxParent)
boxOrigin.position = SCNVector3(0,0,-1)
boxParent.addChildNode(boxOrigin)
box = SCNNode(geometry: SCNBox(width: 0.5, height: 0.2, length: 0.3, chamferRadius: 0))
box.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
box.position = SCNVector3(0,0,0)
boxOrigin.addChildNode(box)
}
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
boxParent.eulerAngles = (sceneView.pointOfView?.eulerAngles)!
boxParent.orientation = (sceneView.pointOfView?.orientation)!
boxParent.position = (sceneView.pointOfView?.position)!
box.position = boxOrigin.worldPosition
box.eulerAngles.y = (sceneView.pointOfView?.eulerAngles.y)!
print(box.eulerAngles)
sceneView.scene.rootNode.addChildNode(box)
}
You're simultaneously using two types of rotation. It's wrong!
boxParent.orientation = (sceneView.pointOfView?.orientation)! //quaternion
This variable uses the node’s orientation, expressed as quaternion (4 components: x, y, z, w).
boxParent.eulerAngles = (sceneView.pointOfView?.eulerAngles)!
The node’s rotation, expressed as pitch, yaw, and roll angles, in radians (3 components: x, y, z).
You need to decide which var you'll be using: orientation or eulerAngles. I suppose you'll choose orientation.
Read this useful article and this one about Quaternions and what a Gimbal Lock is.
Also, use SCNLookAtConstraint object (node's negative z-axis points toward the constraint's target node) or SCNBillboardConstraint object (automatically adjusts a node's orientation so that its local z-axis always points toward the node's pointOfView) for automatically adjusting a node’s orientation, so you camera'll be always pointing toward another node.

How to handle 3D object size and its position in ARKit

I am facing difficulties with 3D object size and its x, y, z positioning. I added the 3D object to sceneView, but its size is too big. How do I reduce the 3D object size based on my requirement? Can anyone help me handle the 3D object's size and its x, y, z positioning?
I am using Swift to code.
Each SCNNode has a scale property:
Each component of the scale vector multiplies the corresponding
dimension of the node’s geometry. The default scale is 1.0 in all
three dimensions. For example, applying a scale of (2.0, 0.5, 2.0) to
a node containing a cube geometry reduces its height and increases its
width and depth.
Which can be set as follows:
var scale: SCNVector3 { get set }
If for example your node was called myNode, you could thus use the following to scale it by 1/10 of it's original size:
myNode.scale = SCNVector3(0.1, 0.1, 0.1)
Regarding positioning SCNNodes this can be achieved by setting the position property:
The node’s position locates it within the coordinate system of its
parent, as modified by the node’s pivot property. The default position
is the zero vector, indicating that the node is placed at the origin
of the parent node’s coordinate system.
If therefore, you wanted to add your SCNNode to the center of the worldOrigin, and 1m away from the camera you can use the following:
myNode.position = SCNVector3(0, 0, -1)
Hope it helps...

ARKit - create world boundaries/get position at edge of visible plane?

Ok, what I am trying to do is create physics body/colliding boundaries for my character, my SCNNode, in my SceneKit game Im building with ARKit. This is so my node cannot move out of the user's vision/go so far away that it isn't visible as it is currently doing. My SCNNode is moved by user input, so I need to make "world boundaries" while ARKit still doesn't have vertical wall detection
I know you can place an object some set distance ahead of you in the real world as stated here Understand coordinate spaces in ARKit
and I have done that with this, just making a box with physics body here -
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0) //change to be VERY TALL - need to make it a giant room
node.physicsBody = SCNPhysicsBody(type: SCNPhysicsBodyType.static, shape: nil)
box.firstMaterial?.diffuse.contents = UIColor.red
box.firstMaterial?.isDoubleSided = true
node = SCNNode(geometry: box)
node.position = SCNVector3(view.pointOfView.simdWorldFront + float3(0, 0, -0.5)) //random distance ahead
And this works, and I could add it as a child node of camera so it moves as user moves, but I don't think Im doing this correctly.
Essentially I need 4 walls to box the user/SCNNode in (corral the character) that are infinitely high, and at the VERY edge of the horizontal plane that the user can see. Really I don't know what this distance should be in the x plane:
+ float3(0, 0, -0.5)
How would you create AR boundaries like this?

Placing "SCNNode" at a given position and facing "SCNCamera" with offset around Y-axis

So,
I have the exact position I want to place the node at. If I test things with a sphere geometry I can place spheres in the world by telling the node:
node.simdPosition = position
(I provide the "position" as an input to the function).
That successfully places the object in the world exactly where I want it to go.
What I really want to do is placing a plane:
let plane = SCNPlane(width: 0.2, height: 0.3)
plane.cornerRadius = plane.width / 10
plane.firstMaterial?.diffuse.contents = UIColor.red
plane.firstMaterial?.specular.contents = UIColor.white
let node = SCNNode(geometry: plane)
Then telling it to be placed at the "position":
node.simdPosition = position
All this works with the plane as well. What I have problems with is the angle:
I want to tell the plane's node to be placed with a given "angle" (around Y) offset to the camera. I tried this but it's not working:
node.rotation = SCNVector4Make(0, 1, 0, currentFrame.camera.eulerAngles.z - angle)
So then, the question is, how can a node be placed at a certain position and at the moment it gets placed in the world, also have a certain Y angle offset from the perpendicular to the camera?
I was using the wrong Euler angle... (z)
This made it work:
node.eulerAngles = SCNVector3Make(0, cameraEulerAngles.y - Float(0.7), 0)

SceneKit invert Y axis without inverting geometry normals

Generally what I'm trying to achieve: we have map data that historically was all 2D, and the coordinate system we use is the origin point (0,0) at the top left, positive x goes right, positive y goes down. We have now added 3D data by adding a z axis, positive z coming out of the screen towards you (think top-down map view). This is a left handed coordinate system, but SceneKit is a right handed coordinate system. I would like to apply some transform at the top level of my SceneKit Scene that will convert the Scene into a left handed coordinate system such that I can modify/position/add nodes to the scene in terms of our custom mapping coordinate system and things will just work.
So far I have this:
let scene = SCNScene()
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.scale = SCNVector3(1,-1,1)
scene.rootNode.addChildNode(cameraNode)
This achieves exactly what I want, but has one big problem. It inverts all of the geometry faces, so my geometry's disappear unless I change their material's cullMode:
let mapLength = 1000 //max X axis
let mapWidth = 800 //max Y axis
let mapHeight = 100 //max Z axis
cameraNode.position = SCNVector3(mapLength / 2, mapWidth / 2, 2000)
let mapPlane = SCNNode()
mapPlane.position = SCNVector3(mapLength / 2, mapWidth / 2, 0)
mapPlane.geometry = SCNPlane(width: mapLength, height: mapWidth)
mapPlane.geometry?.firstMaterial?.diffuse.contents = UIColor.blackColor()
scene.rootNode.addChildNode(mapPlane)
mapPlane doesn't show at all! You have to rotate the camera to the underside of mapPlane in order to see it. You can easily fix this by adding a single line:
mapPlane.geometry?.firstMaterial?.cullMode = .Front
But I don't want to have to change the cullMode for every geometry/material. Is there a way to achieve this without requiring extra code at each geometry/material? Some transform that would invert the geometry face normals for all child nodes of rootNode? Ideally this would be achieved entirely by settings on the actual Scene, or by transforms on rootNode or the camera.