Scenekit. Which Way Is Up

SceneKit. Which way is up?

SceneKit does not directly load DAE (or ABC) files on iOS -- it loads scenes from a private Apple format, which Xcode automatically converts to when you include scene files in your project. Part of this conversion is the option to transform the up axis.

I don't believe that option is exposed when you simply include the DAE file as a bundle resource. (That might be a good bug to file.) However, it's a good idea to use the new SceneKit Asset Catalog feature instead, anyway -- put your DAE files and whatever external resources (textures) into a folder with a .scnassets extension, and Xcode will process them together to optimize for the target device when you build. Then, when you select that folder in the Xcode navigator, you'll get an editor for scene building options:

SceneKit Asset Catalog options

All the boxes there are good to check. :) (Since the first one doesn't come with an explanation: interleaving means organizing the vertex data for a geometry so you get better GPU-memory locality for fewer cache misses during vertex processing, which is important for performance on embedded devices.)

Hm, I don't see anything about units in there, though. Might be another good bug to file.

There's another option, too -- all SceneKit objects implement the NSSecureCoding protocol, so you can load and preprocess your scene on OS X, then use NSKeyedArchiver to write it out. Include the resulting file in your iOS project as a bundle resource, and Xcode won't preprocess it (it's already as compressed and optimized as it can get) -- and if you name it with an .scn extension, you can use all the SCNScene and SCNSceneSource methods for loading it just like you would a (preprocessed) DAE file.

How to determine if an ScnNode is left or right of the view direction of a camera node in SceneKit


  1. Calculate the vector to the node relative to the camera. Calculate the vector by subtracting the camera position from the node position
  2. Use atan2 to convert the vector into an angle. The Apple docs are bit thin on explanation. There's a tutorial on raywenderlich.com.

E.g.

let v = CGPoint(x: nodePosition.x - cameraPosition.x, y: nodePosition.y - cameraPosition.y);
let a = atan2(v.y, v.x) // Note: order of arguments

The angle is in radians, and will range between -π and π (-180 to 180 degrees). If the value is negative the node is to the left of the camera vector, positive values mean the node is to the right, and zero means the node is straight ahead.

Conveniently, you can use this angle as-is to actually perform the rotation if you want. Just increment the camera angle by some fraction of the angle to the node.

E.g.

cameraRotation += a * 0.1

Caveats:

  • "Forward" or "Up" is not necessarily going to be the same as your internal coordinate system. Usually 0 is to the right, so if you want to show an arrow or other UI you may need to add or subtract an amount from the angle (usually -(π/2) to rotate a quarter turn).
  • The angle will always only be between -π and +π. If the node happens to fall directly behind the camera (exactly 180 degrees) you will get either -π or +π, and you might see the angle jump between these two extremes as the node cross over this discontinuity. You sometimes see the side effects of this in games where a nav arrow will point to a target then suddenly flick to the opposite side of the screen. There are various ways to handle this. One easy way is to use a low pass filter on the angle (ie accumulate multiple samples over time).

Get up side of SCNNode from Orientation

Here is a method that returns the index of the face that's facing up. It assumes that "boxNode" is a box made of 6 faces with the following (arbitrary) order: front / right / back / left / up / bottom. It returns the index of the face that is facing up.
Don't forget to import then .
For an arbitrary mesh, you would have to use the face normals instead of "boxNormals" (which is not obvious to compute since SceneKit meshes have one normal per vertex, not one normal per face, so you would have to compute the normals per face yourself).

- (NSUInteger) boxUpIndex:(SCNNode *)boxNode
{
SCNVector4 rotation = boxNode.rotation;
SCNVector4 invRotation = rotation; invRotation.w = -invRotation.w;

SCNVector3 up = SCNVector3Make(0,1,0);

//rotate up by invRotation
SCNMatrix4 transform = SCNMatrix4MakeRotation(invRotation.w, invRotation.x, invRotation.y, invRotation.z);
GLKMatrix4 glkTransform = SCNMatrix4ToGLKMatrix4(transform);
GLKVector3 glkUp = SCNVector3ToGLKVector3(up);
GLKVector3 rotatedUp = GLKMatrix4MultiplyVector3(glkTransform, glkUp);

//build box normals (arbitrary order here)
GLKVector3 boxNormals[6] = {{{0,0,1}},
{{1,0,0}},
{{0,0,-1}},
{{-1,0,0}},
{{0,1,0}},
{{0,-1,0}},
};

int bestIndex = 0;
float maxDot = -1;

for(int i=0; i<6; i++){
float dot = GLKVector3DotProduct(boxNormals[i], rotatedUp);
if(dot > maxDot){
maxDot = dot;
bestIndex = i;
}
}

return bestIndex;
}

Position a SceneKit object in front of SCNCamera's current orientation

(Swift 4)

Hey, you can use this simpler function if you want to put the object a certain position relative to another node (e.g. the camera node) and also in the same orientation as the reference node:

func updatePositionAndOrientationOf(_ node: SCNNode, withPosition position: SCNVector3, relativeTo referenceNode: SCNNode) {
let referenceNodeTransform = matrix_float4x4(referenceNode.transform)

// Setup a translation matrix with the desired position
var translationMatrix = matrix_identity_float4x4
translationMatrix.columns.3.x = position.x
translationMatrix.columns.3.y = position.y
translationMatrix.columns.3.z = position.z

// Combine the configured translation matrix with the referenceNode's transform to get the desired position AND orientation
let updatedTransform = matrix_multiply(referenceNodeTransform, translationMatrix)
node.transform = SCNMatrix4(updatedTransform)
}

If you'd like to put 'node' 2m right in front of a certain 'cameraNode', you'd call it like:

let position = SCNVector3(x: 0, y: 0, z: -2)
updatePositionAndOrientationOf(node, withPosition: position, relativeTo: cameraNode)

Edit: Getting the camera node

To get the camera node, it depends if you're using SceneKit, ARKit, or other framework. Below are examples for ARKit and SceneKit.

With ARKit, you have ARSCNView to render the 3D objects of an SCNScene overlapping the camera content. You can get the camera node from ARSCNView's pointOfView property:

let cameraNode = sceneView.pointOfView

For SceneKit, you have an SCNView that renders the 3D objects of an SCNScene. You can create camera nodes and position them wherever you want, so you'd do something like:

let scnScene = SCNScene()
// (Configure scnScene here if necessary)
scnView.scene = scnScene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 5, 10) // For example
scnScene.rootNode.addChildNode(cameraNode)

Once a camera node has been setup, you can access the current camera in the same way as ARKit:

let cameraNode = scnView.pointOfView

SceneKit – Get direction of camera

You can create an SCNNode that place it in worldFront property to get a vector with the x, y, and z direction.

Another way you could do it is like how this project did it:

// Credit to https://github.com/farice/ARShooter

func getUserVector() -> (SCNVector3, SCNVector3) { // (direction, position)
if let frame = self.sceneView.session.currentFrame {
let mat = SCNMatrix4(frame.camera.transform) // 4x4 transform matrix describing camera in world space
let dir = SCNVector3(-1 * mat.m31, -1 * mat.m32, -1 * mat.m33) // orientation of camera in world space
let pos = SCNVector3(mat.m41, mat.m42, mat.m43) // location of camera in world space

return (dir, pos)
}
return (SCNVector3(0, 0, -1), SCNVector3(0, 0, -0.2))
}

SceneKit, flip direction of SCNMaterial

Instead of scaling the node (which may break your lighting) you can flip the mapping using SCNMaterialProperty's contentsTransform property:

material.diffuse.contentsTransform = SCNMatrix4MakeScale(1,-1,1)
material.diffuse.wrapT = SCNWrapModeRepeat // or translate contentsTransform by (0,1,0)

Getting direction that SCNNode is facing

Negative z is the "facing" direction of a node only in its own coordinate space (i.e. the one its children and geometry are positioned in). When you apply a force, you're working in the coordinate space containing the node.

So, if you want to apply a force in a node's forward direction, you'll need to convert a vector like {0,0,-1} from the node's space to its parent's using a method like convertPosition:toNode:.

SceneKit: move camera towards direction its facing with pan gesture

In Objective C. The key part is the last three lines and specifically the order of multiplication of the matrices in the last line (causing the movement to happen in local space). If the transmat and cammat are switched it would behave again like you have now (moving in world space). The refactor part is just something that works for my specific situation where both perspective and orthographic camera is possible.

-(void)panCamera :(CGPoint)location {

CGFloat dx = _prevlocation.x - location.x;
CGFloat dy = location.y - _prevlocation.y;
_prevlocation = location;

//refactor dx and dy based on camera distance or orthoscale
if (cameraNode.camera.usesOrthographicProjection) {
dx = dx / 416 * cameraNode.camera.orthographicScale;
dy = dy / 416 * cameraNode.camera.orthographicScale;
} else {
dx = dx / 720 * cameraNode.position.z;
dy = dy / 720 * cameraNode.position.z;
}

SCNMatrix4 cammat = self.cameraNode.transform;
SCNMatrix4 transmat = SCNMatrix4MakeTranslation(dx, 0, dy);
self.cameraNode.transform = SCNMatrix4Mult(transmat, cammat);

}

SceneKit - Modifying velocity for the direction a node is facing

I guess you use iOS11, so I think you can use SCNNode.convertVector for your purpose.

let velocityInLocalSpace = SCNVector3(0, 0, -0.15)
let velocityInWorldSpace = t.presentation.convertVector(velocityInLocalSpace, to: nil)
t.physicsBody?.velocity = velocityInWorldSpace

SceneKit - rotate SCNNode with pan gesture based on direction

After a LOT of research and few hours of head-banging the wall, i finally found a working solution.

My working code:

func handlePan(sender: UIPanGestureRecognizer){

// get pan direction
let velocity: CGPoint = sender.velocity(in: sender.view!)
if self.panDirection == nil {
self.panDirection = GameHelper.getPanDirection(velocity: velocity)
}

// if selected brick
if self.selectedBrickNode != nil {

if sender.state == UIGestureRecognizerState.began{
lastAngle = 0.0 // reset last angle
}

let translation = sender.translation(in: sender.view!)
let anglePan = (self.panDirection == "horizontal") ? GameHelper.deg2rad(deg: Float(translation.x)) : GameHelper.deg2rad(deg: Float(translation.y))

let x:Float = (self.panDirection == "vertical" ) ? 1 : 0.0
let y:Float = (self.panDirection == "horizontal" ) ? 1 : 0.0

// calculate the angle change from last call
let fraction = anglePan - lastAngle
lastAngle = anglePan

// perform rotation by difference to last angle
selectedBrickNode.transform = SCNMatrix4Mult(selectedBrickNode.transform,SCNMatrix4MakeRotation(fraction, x, y, 0.0))


if(sender.state == UIGestureRecognizerState.ended) {

// calculate angle to snap to 90 degree increments
let finalRotation = GameHelper.rad2deg(rad:anglePan)
let diff = finalRotation.truncatingRemainder(dividingBy: 90.0)
var finalDiff = Float(0.0)

switch diff {
case 45..<90 :
finalDiff = 90 - diff
case 0..<45 :
finalDiff = -diff
case -45..<0 :
finalDiff = abs(diff)
case -90 ..< -45 :
finalDiff = -90 - diff
default:
print("do nothing")
}

// final transform to apply snap to closest 90deg increment
let snapAngle = GameHelper.deg2rad(deg: finalDiff)
selectedBrickNode.transform = SCNMatrix4Mult(selectedBrickNode.transform, SCNMatrix4MakeRotation(snapAngle, x, y, 0.0))

// remove highlight from node and deselect
self.selectedBrickNode?.geometry?.materials = [hlp.defaultMaterial]
self.selectedBrickNode = nil
}
}


}

and GameHelper.swift

class GameHelper {

static func rad2deg( rad:Float ) -> Float {
return rad * (Float) (180.0 / Double.pi)
}

static func deg2rad( deg:Float ) -> Float{
return deg * (Float)(Double.pi / 180)
}

static func getPanDirection(velocity: CGPoint) -> String {
var panDirection:String = ""
if ( velocity.x > 0 && velocity.x > abs(velocity.y) || velocity.x < 0 && abs(velocity.x) > abs(velocity.y) ){
panDirection = "horizontal"
}

if ( velocity.y < 0 && abs(velocity.y) > abs(velocity.x) || velocity.y > 0 && velocity.y > abs(velocity.x)) {
panDirection = "vertical"
}


return panDirection
}

}


Related Topics



Leave a reply



Submit