How to change the position of an object Swift SceneKit - ios

I've two objects in one SceneKit. One is the Earth and the other is the Moon. Both of them are positioned at x:0, y:0, z:0 and are overlapping. How should I change the coordinates of the Moon so it's around the Earth?
Here the code:
import UIKit
import SceneKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x:0, y:0, z:10)
scene.rootNode.addChildNode(cameraNode)
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .directional
lightNode.position = SCNVector3(x:0, y:0, z:2)
scene.rootNode.addChildNode(lightNode)
let stars = SCNParticleSystem(named: "StarsParticles.scnp", inDirectory: nil)!
scene.rootNode.addParticleSystem(stars)
let moonNode = MoonNode()
scene.rootNode.addChildNode(moonNode)
let sceneview = self.view as! SCNView
sceneview.scene = scene
let earthNode = EarthNode()
scene.rootNode.addChildNode(earthNode)
let sceneView = self.view as! SCNView
sceneView.scene = scene
sceneView.showsStatistics = false
sceneView.backgroundColor = UIColor.black
sceneView.allowsCameraControl = true
}
override var prefersStatusBarHidden: Bool {
return true
}
}

You could make your view controller a SCNSceneRendererDelegate, and implement the delegate method renderer:willRenderScene which gives you a time. Based on that time, you can set something like this:
moonNode.position = SCNVector3(r * cos(Float(time), r * sin(Float(time)), 0)
where r is the radius of your Earth node - if you don't know this at compile time, just include something like this above that line:
let r = length(float3(earthNode.boundingBox.max) - float3(earthNode.boundingBox.min))
this won't quite be exact for the code I included which causes a rotation in the xy plane but it will at least give you the right order of magnitude for r. If you get an error that it can't find float3 or length, try and import simd.
Let me know if you have any issues with this, as I typed this up from memory.
More info in documentation: https://developer.apple.com/documentation/scenekit/scnscenerendererdelegate/1523483-renderer

You can easily do it using simdPivot instance property.
var simdPivot: simd_float4x4 { get set }
Here's a code for testing:
import SceneKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let action = SCNAction.repeatForever(SCNAction.rotate(by: .pi,
around: SCNVector3(0,1,0),
duration: 1))
let earthNode = SCNNode(geometry: SCNSphere(radius: 3.57))
earthNode.geometry?.materials.first?.diffuse.contents = UIColor.blue
scene.rootNode.addChildNode(earthNode)
let moonNode = SCNNode(geometry: SCNSphere(radius: 0.15))
moonNode.geometry?.materials.first?.diffuse.contents = UIColor.yellow
moonNode.simdPivot.columns.3.x = 5
moonNode.runAction(action)
scene.rootNode.addChildNode(moonNode)
let sceneView = self.view as! SCNView
sceneView.scene = scene
sceneView.backgroundColor = UIColor.black
sceneView.allowsCameraControl = true
}
}

Related

ARKit + SceneKit not rendering any shadows

I'm using ARKit and SceneKit to render a very simple scene with a sphere hovering above a plane. However no matter what I try, I cannot get shadows to render at all. The sphere is shaded properly from the light, but no shadows are drawn.
Here's my complete ARSCNView:
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
public var baseNode = SCNNode()
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.autoenablesDefaultLighting = false
sceneView.automaticallyUpdatesLighting = false
sceneView.rendersCameraGrain = true
sceneView.preferredFramesPerSecond = 0
sceneView.debugOptions = [.showBoundingBoxes]
let scene = SCNScene()
sceneView.scene = scene
self.baseNode = SCNNode()
baseNode.position.z -= 1 // draw in front of viewer at arts
self.sceneView.scene.rootNode.addChildNode(baseNode)
// Plane to catch shadows
let shadowCatcher = SCNNode(geometry: SCNPlane(width: 0.5, height: 0.5))
shadowCatcher.name = "shadow catcher"
shadowCatcher.castsShadow = false
shadowCatcher.renderingOrder = -10
let groundMaterial = SCNMaterial()
groundMaterial.lightingModel = .constant
groundMaterial.isDoubleSided = true
shadowCatcher.geometry!.materials = [groundMaterial]
self.baseNode.addChildNode(shadowCatcher)
// A shere that should cast shadows
let sphere = SCNNode(geometry: SCNSphere(radius: 0.05))
sphere.position = SCNVector3(0, 0, 0.3)
sphere.castsShadow = true
baseNode.addChildNode(sphere)
// The light
let light = SCNLight()
light.type = .spot
light.intensity = 1000
light.castsShadow = true
light.shadowMode = .deferred
light.automaticallyAdjustsShadowProjection = true
light.shadowMapSize = CGSize(width: 2048, height: 2048)
let lightNode = SCNNode()
lightNode.name = "light"
lightNode.light = light
lightNode.position = SCNVector3(0, 0, 2)
lightNode.look(at: SCNVector3(0, 0, 0))
baseNode.addChildNode(lightNode)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
if ARWorldTrackingConfiguration.supportsFrameSemantics(.personSegmentation) {
configuration.frameSemantics.insert(.personSegmentation)
}
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
}
Why doesn't this render shadows? The same basic scenegraph does render shadows if I use a normal SCNView instead of an ARSCNView
This is caused by enabling the personSegmentation frame semantic. After removing this, shadows should be rendered properly again:
This took me forever to track down and seems like a bug. I've filed an issue against Apple but unfortunately I am not aware of any workarounds at the moment

How to give a paint brush affect using bare finger with ARKit & SceneKit

I am trying to build an app to draw graffiti in ARKit using bare hands.
The graffiti should look realistic. I have gone through many examples and I see most of us using SCNSphere. Well, It does do that job but there are gaps between each sphere that do not give a realistic touch.
How do we come up with a brush/drawn line effect?
Is scenekit the best way to do this or shall we try spritekit/Unity?
My basic code looks like this:
import UIKit
import ARKit
import SceneKit
class ViewController : UIViewController, ARSCNViewDelegate, ARSessionDelegate {
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self as ARSCNViewDelegate
sceneView.showsStatistics = true
sceneView.autoenablesDefaultLighting = true
sceneView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:))))
}
override func loadView() {
sceneView = ARSCNView(frame:CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.height, height: UIScreen.main.bounds.width))
sceneView.delegate = self
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal, .vertical]
sceneView.session.delegate = self
self.view = sceneView
sceneView.session.run(config)
}
#objc func panGesture(_ gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
guard let query = sceneView.raycastQuery(from: gesture.location(in: gesture.view), allowing: .existingPlaneInfinite, alignment: .any) else {
return
}
let results = sceneView.session.raycast(query)
guard let hitTestResult = results.first else {
return
}
let position = SCNVector3Make(hitTestResult.worldTransform.columns.3.x, hitTestResult.worldTransform.columns.3.y, hitTestResult.worldTransform.columns.3.z)
let sphere = SCNSphere(radius: 0.5)
let material = SCNMaterial()
material.diffuse.contents = UIColor.blue
sphere.materials = [material]
let sphereNode = SCNNode()
sphereNode.scale = SCNVector3(x:0.004,y:0.004,z:0.004)
sphereNode.geometry = sphere
sphereNode.position = SCNVector3(x: 0, y:0.02, z: -1)
self.sceneView.scene.rootNode.addChildNode(sphereNode)
sphereNode.position = position
}
}

Move camera to tapped SCNNode

I'm using SceneKit and Swift to try and move the camera so it's 'focused' on the selected node. I understand I have the defaultCameraController enabled but I was trying to adjust the camera's position via dolly, rotate and translateInCameraSpaceBy but there was no animated transition - it just jumped to the new position.
Is there anyway for the camera to glide into position like how Google Maps slides/then zooms over to a searched location?
Any help would be greatly appreciated :)
Here's my code:
import UIKit
import SceneKit
class ViewController: UIViewController {
var gameView: SCNView!
var scene: SCNScene!
var cameraNode: SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
// Scene
scene = SCNScene()
// Camera
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 0, 10)
scene.rootNode.addChildNode(cameraNode)
// Light
/*
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .omni
lightNode.position = SCNVector3(0, 10, 2)
scene.rootNode.addChildNode(lightNode)
*/
// Stars
//let stars = SCNParticleSystem(named: "starsParticles.scnp", inDirectory: nil)!
//scene.rootNode.addParticleSystem(stars)
// Earth
let earthNode = itemPlate()
earthNode.position = SCNVector3(0, 0, 0)
scene.rootNode.addChildNode(earthNode)
// Create orbiting moonOne
let moonNodeOne = itemPlate()
moonNodeOne.position = SCNVector3(3, 0, 0)
earthNode.addChildNode(moonNodeOne)
// Create orbiting moonOne
let moonNodeTwo = itemPlate()
moonNodeTwo.position = SCNVector3(5, 3, 2)
earthNode.addChildNode(moonNodeTwo)
// Create orbiting moonOne
let moonNodeThree = itemPlate()
moonNodeThree.position = SCNVector3(-4, -3, 5)
earthNode.addChildNode(moonNodeThree)
// Scene formation
gameView = self.view as! SCNView
gameView.scene = scene
gameView.showsStatistics = true
gameView.allowsCameraControl = true
gameView.autoenablesDefaultLighting = true
gameView.defaultCameraController.interactionMode = .fly
gameView.defaultCameraController.inertiaEnabled = true
gameView.defaultCameraController.maximumVerticalAngle = 89
gameView.defaultCameraController.minimumVerticalAngle = -89
scene.background.contents = UIImage(named: "orangeBg.jpg")
}
override var prefersStatusBarHidden: Bool {
return true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: gameView)
let hitList = gameView.hitTest(location, options: nil)
if let hitObject = hitList.first {
let node = hitObject.node
// Update camera position
//gameView.defaultCameraController.translateInCameraSpaceBy(x: node.position.x, y: node.position.y, z: node.position.z + 5)
let onScreenPoint:CGPoint = CGPoint(x: 1.0, y: 1.0)
let viewport:CGSize = CGSize(width: 50, height: 50)
gameView.defaultCameraController.dolly(by: 1.0, onScreenPoint: onScreenPoint, viewport: viewport)
//let newCameraPosition = SCNVector3Make(node.position.x, node.position.y, node.position.z + 10)
print("NODE_HIT_OBJECT_COORDS: \(node.position.x), \(node.position.y) \(node.position.y)")
//let moveToAction = SCNAction.move(by: newCameraPosition, duration: 1.0)
}
}
}
You can implement in your code a methodology like this (sorry, I used macOS project instead iOS, but it's almost the same):
func handleClick(_ gestureRecognizer: NSGestureRecognizer) {
let scnView = self.view as! SCNView
let p = gestureRecognizer.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
if hitResults.count > 0 {
let result = hitResults[0]
let nodePosition = result.node.position.z
var matrix = matrix_identity_float4x4
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.5 // duration in seconds
matrix.columns.3.z = Float(nodePosition + 5.0)
scnView.pointOfView?.position.z = CGFloat(matrix.columns.3.z)
SCNTransaction.commit()
}
}
Or, as a second logical option, you can use SceneKit's constraints:
func handleClick(_ gestureRecognizer: NSGestureRecognizer) {
let scnView = self.view as! SCNView
let p = gestureRecognizer.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
if hitResults.count > 0 {
let result = hitResults[0]
let nodePosition = result.node
let constraint1 = SCNLookAtConstraint(target: nodePosition)
let constraint2 = SCNDistanceConstraint(target: nodePosition)
constraint2.minimumDistance = 5
constraint2.maximumDistance = 9
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.5
scnView.pointOfView?.constraints = [constraint2, constraint1]
SCNTransaction.commit()
}
}
P.S. These two approaches ain't out-of-the-box solutions but rather hints on how to implement what you want to.

How can I change a SCNView back to a UIView?

I am trying to use code from an example of a Game with Xcode as a Swift Playground. The code works perfectly in the Xcode version, but in the playground, I get the error:
Could not cast value of type 'UIView' (0x114debe38) to 'SCNView' (0x12521d3d0).
The type of class is set to be of type UIViewController, so I am not sure why this does not work in only the playground. II have the same files in both the app and the playground.
I have already looked at this question, but the method seems to be built in already.
I also tried to cast it back to a UIView if it was a SCNView and I also tried making a new view, adding a subview as a SCNView to it, and then setting the final view as the new view. None of my attempts worked.
class GameScene: UIViewController {
let finalView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
cameraNode.rotation = SCNVector4(0, 0, 0, 30)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// retrieve the shark node
let shark = scene.rootNode.childNode(withName: "ship", recursively: true)!
// animate the 3d object
//shark.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.black
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
#objc
func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let scnView = self.view as! SCNView
finalView.addSubview(scnView)
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result = hitResults[0]
// get its material
let material = result.node.geometry!.firstMaterial!
// highlight it
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
// on completion - unhighlight
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
material.emission.contents = UIColor.black
SCNTransaction.commit()
}
material.emission.contents = UIColor.red
SCNTransaction.commit()
}
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .pad {
return .allButUpsideDown
} else {
return .all
}
}
}
PlaygroundSupport.PlaygroundPage.current.liveView = GameScene()
What this should show up is a 3D Model and a camera that can pan around it by touch on a mobile device.
In your example the UIViewController view was probably set to an SCNView in interface builder. If you're not using a nib you can do this by overriding loadView.
override func loadView() {
let scnView = SCNView(frame: UIScreen.main.bounds, options: nil)
self.view = scnView
}

SceneKit: Hit test not work with billboarded quads

I made billboarded quads using SceneKit.
The cameraNode is synchronized with UIDeviceMotion, and the billboard nodes are appearing as I expected.
The thing is, I want these nodes to be called when I tap it.
For this, I used UITapGestureRecognizer with hitTest.
Here is some of my code.
// ==== in viewDidLoad
// initialize tap gesture
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onNodeTapped))
sceneView.addGestureRecognizer(tapGesture)
// initialize scenekit.scene
let scene = SCNScene()
scene.rootNode.addChildNode(cameraNode)
scene.rootNode.addChildNode(worldNode)
sceneView.scene = scene
sceneView.autoenablesDefaultLighting = true
sceneView.pointOfView = cameraNode
And this is the tap handler
func onNodeTapped(_ gestureRecognize: UIGestureRecognizer) {
let location = gestureRecognize.location(in: sceneView) // <---- updated
let hitResults = sceneView.hitTest(location, options: nil)
for result in hitResults {
// FOR_TEST: hit test visualization
if let material = result.node.geometry?.materials.first {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
material.emission.contents = UIColor.black
SCNTransaction.commit()
}
material.emission.contents = UIColor.red
SCNTransaction.commit()
}
// target tap event handling
if let target = (result.node as? TargetNode)?.target {
if onTargetTapped(target) {
return
}
}
}
}
This code works very rarly. What I mean is the visualization part respond only 1 out of 20 times, and the onTargetTapped is called only 1 out of 100...
The weired thing is the targeting is fine, which means this is not the coordinate problem.
I found something related to SCNHitTestOption.categoryBitMask, but it didn't help at all.
Also, when I open this Scenview, this error message appears on the console.
"[SceneKit] Error: error in _C3DUnProjectPoints"
Maybe this message is related to the hitTest malfunctioning?
Updated
This code is for building billboard SCNGeometry and SCNNode
override func initializeGeometry() -> SCNGeometry {
let geometry = SCNPlane(width: width, height: height)
let material = geometry.materials.first
material?.diffuse.contents = initializeTexture()
material?.writesToDepthBuffer = false
material?.readsFromDepthBuffer = false
return geometry
}
// ==== building node
node = SCNNode()
node.geometry = initializeGeometry()
node.categoryBitMask = MyConstraints.targetNodeHitTestCategoryBitMask
node.constraints = [SCNBillboardConstraint()]
You're retrieving the tap coordinates in annotationsDisplayView, but then passing those coordinates to sceneView for the hit test lookup. Unless those views are precisely on top of each other, you'll get a mismatch.
I think you want
let location = gestureRecognize.location(in: sceneView)
instead.
I was curious whether the hit test would use the rotated version of the billboard. I confirmed that it does. Here's a working (Mac) sample.
In the SCNView subclass:
override func mouseDown(with theEvent: NSEvent) {
/* Called when a mouse click occurs */
// check what nodes are clicked
let p = self.convert(theEvent.locationInWindow, from: nil)
let hitResults = self.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
Swift.print("hit me")
}
for result in hitResults {
Swift.print(result.node.name)
// get its material
let material = result.node.geometry!.firstMaterial!
// highlight it
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
// on completion - unhighlight
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
material.emission.contents = NSColor.black
SCNTransaction.commit()
}
material.emission.contents = NSColor.yellow
SCNTransaction.commit()
}
super.mouseDown(with: theEvent)
}
And in the view controller:
override func awakeFromNib(){
super.awakeFromNib()
// create a new scene
let scene = SCNScene()
let side = CGFloat(2.0)
let quad1 = SCNNode.init(geometry: SCNPlane(width: side, height: side))
quad1.name = "green"
quad1.geometry?.firstMaterial?.diffuse.contents = NSColor.green
let quad2 = SCNNode.init(geometry: SCNPlane(width: side, height: side))
quad2.name = "red"
quad2.geometry?.firstMaterial?.diffuse.contents = NSColor.red
let quad3 = SCNNode.init(geometry: SCNPlane(width: side, height: side))
quad3.name = "blue"
quad3.geometry?.firstMaterial?.diffuse.contents = NSColor.blue
scene.rootNode.addChildNode(quad1)
scene.rootNode.addChildNode(quad2)
scene.rootNode.addChildNode(quad3)
let rotation = CGFloat(M_PI_2) * 0.9
quad1.position.y = side/2
quad1.eulerAngles.y = rotation
quad2.position.x = side/2
quad2.eulerAngles.x = rotation
// comment out these constraints to verify that they matter
quad1.constraints = [SCNBillboardConstraint()]
quad2.constraints = [SCNBillboardConstraint()]
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = NSColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// set the scene to the view
self.gameView!.scene = scene
// allows the user to manipulate the camera
self.gameView!.allowsCameraControl = true
// show statistics such as fps and timing information
self.gameView!.showsStatistics = true
// configure the view
self.gameView!.backgroundColor = NSColor.black
}
Hit testing should work whether your node is billboard constrained or not. It does for me.
override func viewDidLoad() {
self.sceneView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap(_:))))
...
}
#objc func tap(_ recognizer: UITapGestureRecognizer) {
let loc = recognizer.location(in: self.sceneView)
if let hit = self.sceneView.hitTest(loc) {
...
}
}

Resources