SceneKit hit test error while moving camera - ios

I declare my camera like this at init:
defaultCameraNode.camera = SCNCamera()
defaultCameraNode.position = SCNVector3Make(0, 200, 500)
defaultCameraNode.camera?.zFar = 1000.0
defaultCameraNode.camera?.zNear = 10.0
defaultCameraNode.camera?.xFov = 30.0
defaultCameraNode.camera?.yFov = 30.0
scene.rootNode.addChildNode(defaultCameraNode)
sceneView.pointOfView = defaultCameraNode
defaultCameraNode.constraints = [SCNLookAtConstraint(target: rootNode)]
After this in a tapGesture block I do a hit test:
let hitResults = sceneView.hitTest(sender.locationInView(sceneView), options: nil)
This returns what I want, got the node.
After I add a new camera and change the scene's point of view
var cameraNode = SCNNode()
cameraNode.name = "cameraNode"
cameraNode.position = SCNVector3Make(position.x, position.y + 50.0, position.z + Float(radius * 3))
cameraNode.rotation = SCNVector4Make(1, 0, 0, -atan2f(20.0, 40.0))
var camera = SCNCamera()
camera.zNear = 0.0
camera.zFar = 1000.0
camera.xFov = 40.0
camera.yFov = 40.0
cameraNode.camera = camera
node.addChildNode(cameraNode)
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(animationDuration)
sceneView.pointOfView = cameraNode
SCNTransaction.commit()
When the camera position is changed the same hit test I used before returns a 0 length array and got this error on the console:
SceneKit: error, error in _C3DUnProjectPoints
Anyone can help me solving this?
thanks

I've started a new project and figured it out step by step when does the hittest go wrong. I didn't find it anywhere in the offical Apple documentation, but my experiences are the followings:
If you want to change the camera's position or any other property, you can do it by adding a new camera to a new node with new position, parameters, etc. then you set the SCNView's pointOfView property, you can do it animated like this:
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(2.0)
sceneView.pointOfView = cameraNode
SCNTransaction.commit()
One important point here: the node that holding the new SCNCamera has to be added to the SCNScene's rootView, otherwise (if you add it to the rootView's childNode) the hittest will give you an error instead the SCNNode that you touched.

It looks like you are setting another node (on that doesn't have a camera attached to it) to be the scenes point of view.
Look at your code. The node you are attaching the camera node to is cameraNode, and the node you are making the point of view is node (which you are adding the camera node to).

Related

SCNNode appears more/less warped depending on position (SceneKit)

I have a SceneKit scene in which the camera looks down at a single sphere that rolls around atop a flat plane.
The camera has the default configuration, and is positioned at SCNVector3(0.0, 0.0, 100.0).
The sphere starts out in the center of the screen. At this point, the sphere looks normal, as seen in this screenshot.
But the farther away from the center it moves, the more warped/stretched it appears. As seen in this screenshot, the sphere appears warped (it looks like an egg) when it's near the edge of the screen.
The sphere is configured like this:
let ballNode = SCNNode()
let sphereGeometry = SCNSphere(radius: MainData.screenWidth*0.005)
let targetMaterial = SCNMaterial()
targetMaterial.diffuse.contents = UIColor.red
sphereGeometry.materials = [targetMaterial]
sphereGeometry.firstMaterial?.diffuse.contents = targetMaterial
ballNode.geometry = sphereGeometry
ballNode.geometry?.firstMaterial?.lightingModel = .phong
ballNode.position = SCNVector3(0.0,0.0,20.0)
ballNode.physicsBody = SCNPhysicsBody(type: SCNPhysicsBodyType.dynamic, shape: SCNPhysicsShape.init(node: ballNode))
ballNode.physicsBody?.categoryBitMask = CollisionTypes.dynamicObjects
ballNode.physicsBody?.collisionBitMask = CollisionTypes.staticObjects
ballNode.physicsBody?.contactTestBitMask = CollisionTypes.nothing
ballNode.physicsBody?.isAffectedByGravity = true
ballNode.physicsBody?.allowsResting = false
scnView.scene?.rootNode.addChildNode(ballNode)
The "plane" is actually just an SCNBox node, and is configured like this:
let platform = SCNNode()
let platformGeometry = SCNBox(width: MainData.screenWidth, height: MainData.screenHeight, length: 2.0, chamferRadius: 0.0)
platform.geometry = platformGeometry
platform.physicsBody = SCNPhysicsBody(type: SCNPhysicsBodyType.static, shape: SCNPhysicsShape.init(node: platform))
platform.physicsBody?.categoryBitMask = CollisionTypes.staticObjects
platform.physicsBody?.collisionBitMask = CollisionTypes.dynamicObjects
platform.physicsBody?.contactTestBitMask = CollisionTypes.nothing
platform.physicsBody?.isAffectedByGravity = false
platform.physicsBody?.allowsResting = true
scnView.scene?.rootNode.addChildNode(platform)
Question: Why does the sphere appear warped (like an egg) the farther it travels from the center of the scene/screen?
Thanks for your help!
I don't really understand why, but adding the following solved the problem:
cameraNode.camera?.fieldOfView = 20.0

Normal mapping in Scenekit

I am trying to add normal map for a 3D model in swift using SCNMaterial properties. The diffuse property is working but no other property including normal property is visible on the screen. When I debug to check if the node's material consists of the normal property, it shows the property exists with the image that I added.
I have also checked if the normal image that I am using is correct or not in the SceneKit Editor where it works fine.
I have added the code that I am using.
let node = SCNNode()
node.geometry = SCNSphere(radius: 0.1)
node.geometry!.firstMaterial!.diffuse.contents = UIColor.lightGray
node.geometry!.firstMaterial!.normal.contents = UIImage(named: "normal")
node.position = SCNVector3(0,0,0)
sceneView.scene.rootNode.addChildNode(node)
This is the output I am getting
I am expecting something like this
I got the solution. Since I did not enable DefaultLighting, there was no lighting in the scene. Added this to the code.
sceneView.autoenablesDefaultLighting = true
Given the screenshot, it seems like there is no lighting in the scene, or the material does not respond to lighting, since the sphere is not shaded. For a normal map to work, lighting has to be taken into account, because it responds to lighting direction. Have you tried creating an entirely new SCNMaterial and played with its properties? (I.E. https://developer.apple.com/documentation/scenekit/scnmaterial/lightingmodel seems interesting)
I would try setting
node.geometry!.firstMaterial!.lightingModel = .physicallyBased
Try this.
let scene = SCNScene()
let sphere = SCNSphere(radius: 0.1)
let sphereMaterial = SCNMaterial()
sphereMaterial.diffuse.contents = UIImage(named: "normal.png")
let sphereNode = SCNNode()
sphereNode.geometry = sphere
sphereNode.geometry?.materials = [sphereMaterial]
sphereNode.position = SCNVector3(0.5,0.1,-1)
scene.rootNode.addChildNode(sphereNode)
sceneView.scene = scene

How to use SCNTechnique to create a "masked" portal effect in SceneKit / ARKit?

I'm trying to wrap my head around how to use SCNTechnique with Scenekit.
The effect I'm trying to create can easily be achieved in Unity by adding a custom shader to two materials, making objects with the "filtered" shader to only be visible when it is seen through the other shader (the portal). I've followed this tutorial to create this in Unity: https://www.youtube.com/watch?v=b9xUO0zHNXw
I'm trying to achieve the same effect in SceneKit, but I'm not sure how to apply the passes. I'm looking at SCNTechnique because it has options for stencilStates, but I'm not sure if this will work.
There are very little resources and tutorials on SCNTechnique
As mentioned in the comments above, I am not sure how to achieve an occlusion effect using Stencil Tests which as we know are fairly (and I use that term very loosely) easy to do in Unity.
Having said this, we can achieve a similar effect in Swift using transparency, and rendering order.
Rendering Order simply refers to:
The order the node’s content is drawn in relative to that of other
nodes.
Whereby SCNNodes with a larger rendering order are rendered last and vice versa.
In order to make an object virtually invisible to the naked eye we would need to set the transparency value of an SCNMaterial to a value such as 0.0000001.
So how would we go about this?
In this very basic example which is simply a portal door, and two walls we can do something like this (which is easy enough to adapt into something more substantial and aesthetically pleasing):
The example is fully commented so it should be easy to understand what we are doing.
/// Generates A Portal Door And Walls Which Can Only Be Seen From Behind e.g. When Walking Through The Portsal
///
/// - Returns: SCNNode
func portalNode() -> SCNNode{
//1. Create An SCNNode To Hold Our Portal
let portal = SCNNode()
//2. Create The Portal Door
let doorFrame = SCNNode()
//a. Create The Top Of The Door Frame
let doorFrameTop = SCNNode()
let doorFrameTopGeometry = SCNPlane(width: 0.55, height: 0.1)
doorFrameTopGeometry.firstMaterial?.diffuse.contents = UIColor.black
doorFrameTopGeometry.firstMaterial?.isDoubleSided = true
doorFrameTop.geometry = doorFrameTopGeometry
doorFrame.addChildNode(doorFrameTop)
doorFrameTop.position = SCNVector3(0, 0.45, 0)
//b. Create The Left Side Of The Door Frame
let doorFrameLeft = SCNNode()
let doorFrameLeftGeometry = SCNPlane(width: 0.1, height: 1)
doorFrameLeftGeometry.firstMaterial?.diffuse.contents = UIColor.black
doorFrameLeftGeometry.firstMaterial?.isDoubleSided = true
doorFrameLeft.geometry = doorFrameLeftGeometry
doorFrame.addChildNode(doorFrameLeft)
doorFrameLeft.position = SCNVector3(-0.25, 0, 0)
//c. Create The Right Side Of The Door Frame
let doorFrameRight = SCNNode()
let doorFrameRightGeometry = SCNPlane(width: 0.1, height: 1)
doorFrameRightGeometry.firstMaterial?.diffuse.contents = UIColor.black
doorFrameRightGeometry.firstMaterial?.isDoubleSided = true
doorFrameRight.geometry = doorFrameRightGeometry
doorFrame.addChildNode(doorFrameRight)
doorFrameRight.position = SCNVector3(0.25, 0, 0)
//d. Add The Portal Door To The Portal And Set Its Rendering Order To 200 Meaning It Will Be Rendered After Our Masks
portal.addChildNode(doorFrame)
doorFrame.renderingOrder = 200
doorFrame.position = SCNVector3(0, 0, -1)
//3. Create The Left Wall Enclosure To Hold Our Wall And The Occlusion Node
let leftWallEnclosure = SCNNode()
//a. Create The Left Wall And Set Its Rendering Order To 200 Meaning It Will Be Rendered After Our Masks
let leftWallNode = SCNNode()
let leftWallMainGeometry = SCNPlane(width: 0.5, height: 1)
leftWallNode.geometry = leftWallMainGeometry
leftWallMainGeometry.firstMaterial?.diffuse.contents = UIColor.red
leftWallMainGeometry.firstMaterial?.isDoubleSided = true
leftWallNode.renderingOrder = 200
//b. Create The Left Wall Mask And Set Its Rendering Order To 10 Meaning It Will Be Rendered Before Our Walls
let leftWallMaskNode = SCNNode()
let leftWallMaskGeometry = SCNPlane(width: 0.5, height: 1)
leftWallMaskNode.geometry = leftWallMaskGeometry
leftWallMaskGeometry.firstMaterial?.diffuse.contents = UIColor.blue
leftWallMaskGeometry.firstMaterial?.isDoubleSided = true
leftWallMaskGeometry.firstMaterial?.transparency = 0.0000001
leftWallMaskNode.renderingOrder = 10
leftWallMaskNode.position = SCNVector3(0, 0, 0.001)
//c. Add Our Wall And Mask To The Wall Enclosure
leftWallEnclosure.addChildNode(leftWallMaskNode)
leftWallEnclosure.addChildNode(leftWallNode)
//4. Add The Left Wall Enclosure To Our Portal
portal.addChildNode(leftWallEnclosure)
leftWallEnclosure.position = SCNVector3(-0.55, 0, -1)
//5. Create The Left Wall Enclosure
let rightWallEnclosure = SCNNode()
//a. Create The Right Wall And Set Its Rendering Order To 200 Meaning It Will Be Rendered After Our Masks
let rightWallNode = SCNNode()
let rightWallMainGeometry = SCNPlane(width: 0.5, height: 1)
rightWallNode.geometry = rightWallMainGeometry
rightWallMainGeometry.firstMaterial?.diffuse.contents = UIColor.red
rightWallMainGeometry.firstMaterial?.isDoubleSided = true
rightWallNode.renderingOrder = 200
//b. Create The Right Wall Mask And Set Its Rendering Order To 10 Meaning It Will Be Rendered Before Our Walls
let rightWallMaskNode = SCNNode()
let rightWallMaskGeometry = SCNPlane(width: 0.5, height: 1)
rightWallMaskNode.geometry = rightWallMaskGeometry
rightWallMaskGeometry.firstMaterial?.diffuse.contents = UIColor.blue
rightWallMaskGeometry.firstMaterial?.isDoubleSided = true
rightWallMaskGeometry.firstMaterial?.transparency = 0.0000001
rightWallMaskNode.renderingOrder = 10
rightWallMaskNode.position = SCNVector3(0, 0, 0.001)
//c. Add Our Wall And Mask To The Wall Enclosure
rightWallEnclosure.addChildNode(rightWallMaskNode)
rightWallEnclosure.addChildNode(rightWallNode)
//6. Add The Left Wall Enclosure To Our Portal
portal.addChildNode(rightWallEnclosure)
rightWallEnclosure.position = SCNVector3(0.55, 0, -1)
return portal
}
Which can be tested like so:
let portal = portalNode()
portal.position = SCNVector3(0, 0, -1.5)
self.sceneView.scene.rootNode.addChildNode(portal)
Hope it points you in the right direction...
FYI there is a good tutorial here from Ray Wenderlich which will also be of use to you...

How can I set an SKCameraNode's position x and y relative to a node?

I'm setting up my camera node programatically. My goal is to have the camera follow the player node's x axis, but have the y axis of the camera higher than "player.position.y", something like "player.position.y + 300".
I found this after googling a while about this issue:
cam.position = CGPoint(x: player.position.x, y: player.position.y + 500)
I put this in didMove(to:) along with the setup of the camera:
cam = SKCameraNode() //initialize and assign an instance of SKCameraNode to the cam variable.
self.camera = cam //set the scene's camera to reference cam
//cam.setScale(1.3)
cam.position = CGPoint(x: player.position.x, y: player.position.y + 500)
addChild(cam) //have cam be a child
I've tried putting it before and after the add child(cam) line, but the cam.position line had no effect.
I've tried both setting my camera up as a child of the player node and the scene, but with either method I can get the camera to follow the player but I can't figure out how to manipulate the X and Y separately. The only way Ive been able to affect the cameras position is using "set scale" but thats more of a global zoom in/out.
Any advice would be greatly appreciated.
EDIT: The first part of the answer is all I needed:
cam = SKCameraNode()
player.addChild(cam)
cam.position = CGPoint(x:0,y:500)
self.camera = cam
Why not just make camera a child of player, then set the relative position to +500, so that you never have to worry about setting the camera x position again
cam = SKCameraNode()
player.addChild(cam)
cam.position = CGPoint(x:0,y:500)
self.camera = cam
Then on your end update, set your camera y based on what you need:
let cam = player.childNode[0] as! SKCameraNode //Do whatever you need to do to find the camera
cam.position.y = 500 - player.position.y //This way if the player goes up 1 point, the camera moves down one point thus keeping it on the same spot

ARKit - Applying Force in User's Phone Direction

I have the following code that creates a SCNBox and shoots it on the screen. This works but as soon as I turn the phone in any other direction then the force impulse does not get updated and it always shoots the box in the same old position.
Here is the code:
#objc func tapped(recognizer :UIGestureRecognizer) {
guard let currentFrame = self.sceneView.session.currentFrame else {
return
}
/
let box = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
material.lightingModel = .constant
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.01
let node = SCNNode()
node.geometry = box
node.geometry?.materials = [material]
print(currentFrame.camera.transform)
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
node.simdTransform = matrix_multiply(currentFrame.camera.transform, translation)
node.physicsBody?.applyForce(SCNVector3(0,2,-10), asImpulse: true)
self.sceneView.scene.rootNode.addChildNode(node)
}
Line 26 is where I apply the force but it does not take into account the user's current phone orientation. How can I fix that?
On line 26 you're passing a constant vector to applyForce. That method takes a vector in world space, so passing a constant vector means you're always applying a force in the same direction — if you want a direction that's based on the direction the camera or something else is pointing, you'll need to calculate a vector based on that direction.
The (new) SCNNode property worldFront might prove helpful here — it gives you the direction a node is pointing, automatically converted to world space, so it's useful with physics methods. (Though you might want to scale it.)

Resources