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
Related
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
}
}
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
}
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
}
}
I have a SCNPlane that is added to the scene when a sufficient area is detected for a horizontal surface. The plane appears to be placed in a correct spot, according to the floor/table it's being placed on. The problem is when I drop a SCNNode(this has been consistent whether it was a box, pyramid, 3D-model, etc.) onto the plane, it will eventually find a spot to land and 99% start jiggling all crazy. Very few times has it just landed and not moved at all. I also think this may be cause by the node being dropped and landing slightly below the plane surface. It is not "on top" neither "below" the plane. Maybe the node is freaking out because it's kind of teetering between both levels?
Here is a video of what's going on, you can see at the beginning that the box is below and above the plane and the orange box does stop when it collides with the dark blue box, but does go back to its jiggling ways when the green box collides with it at the end:
The code is here on github
I will also show some of the relevant parts embedded in code:
I just create a Plane class to add to the scene when I need to
class Plane: SCNNode {
var anchor :ARPlaneAnchor
var planeGeometry :SCNPlane!
init(anchor :ARPlaneAnchor) {
self.anchor = anchor
super.init()
setup()
}
func update(anchor: ARPlaneAnchor) {
self.planeGeometry.width = CGFloat(anchor.extent.x)
self.planeGeometry.height = CGFloat(anchor.extent.z)
self.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)
let planeNode = self.childNodes.first!
planeNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: self.planeGeometry, options: nil))
}
private func setup() {
//plane dimensions
self.planeGeometry = SCNPlane(width: CGFloat(self.anchor.extent.x), height: CGFloat(self.anchor.extent.z))
//plane material
let material = SCNMaterial()
material.diffuse.contents = UIImage(named: "tronGrid.png")
self.planeGeometry.materials = [material]
//plane geometry and physics
let planeNode = SCNNode(geometry: self.planeGeometry)
planeNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: self.planeGeometry, options: nil))
planeNode.physicsBody?.categoryBitMask = BodyType.plane.rawValue
planeNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)
planeNode.transform = SCNMatrix4MakeRotation(Float(-Double.pi / 2.0), 1, 0, 0)
//add plane node
self.addChildNode(planeNode)
}
This is the ViewController
enum BodyType: Int {
case box = 1
case pyramid = 2
case plane = 3
}
class ViewController: UIViewController, ARSCNViewDelegate, SCNPhysicsContactDelegate {
//outlets
#IBOutlet var sceneView: ARSCNView!
//globals
var planes = [Plane]()
var boxes = [SCNNode]()
//life cycle
override func viewDidLoad() {
super.viewDidLoad()
//set sceneView's frame
self.sceneView = ARSCNView(frame: self.view.frame)
//add debugging option for sceneView (show x, y , z coords)
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
//give lighting to the scene
self.sceneView.autoenablesDefaultLighting = true
//add subview to scene
self.view.addSubview(self.sceneView)
// Set the view's delegate
sceneView.delegate = self
//subscribe to physics contact delegate
self.sceneView.scene.physicsWorld.contactDelegate = self
//show statistics such as fps and timing information
sceneView.showsStatistics = true
//create new scene
let scene = SCNScene()
//set scene to view
sceneView.scene = scene
//setup recognizer to add scooter to scene
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped))
sceneView.addGestureRecognizer(tapGestureRecognizer)
}
//MARK: helper funcs
#objc func tapped(recognizer: UIGestureRecognizer) {
let scnView = recognizer.view as! ARSCNView
let touchLocation = recognizer.location(in: scnView)
let touch = scnView.hitTest(touchLocation, types: .existingPlaneUsingExtent)
//take action if user touches box
if !touch.isEmpty {
guard let hitResult = touch.first else { return }
addBox(hitResult: hitResult)
}
}
private func addBox(hitResult: ARHitTestResult) {
let boxGeometry = SCNBox(width: 0.1,
height: 0.1,
length: 0.1,
chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor(red: .random(),
green: .random(),
blue: .random(),
alpha: 1.0)
boxGeometry.materials = [material]
let boxNode = SCNNode(geometry: boxGeometry)
//adding physics body, a box already has a shape, so nil is fine
boxNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
//set bitMask on boxNode, enabling objects with diff categoryBitMasks to collide w/ each other
boxNode.physicsBody?.categoryBitMask = BodyType.plane.rawValue | BodyType.box.rawValue
boxNode.position = SCNVector3(hitResult.worldTransform.columns.3.x,
hitResult.worldTransform.columns.3.y + 0.3,
hitResult.worldTransform.columns.3.z)
self.sceneView.scene.rootNode.addChildNode(boxNode)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
//track objects in ARWorld and start session
sceneView.session.run(configuration)
}
//MARK: - ARSCNViewDelegate
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//if no anchor found, don't render anything!
if !(anchor is ARPlaneAnchor) {
return
}
DispatchQueue.main.async {
//add plane to scene
let plane = Plane(anchor: anchor as! ARPlaneAnchor)
self.planes.append(plane)
node.addChildNode(plane)
//add initial scene object
let pyramidGeometry = SCNPyramid(width: CGFloat(plane.planeGeometry.width / 8), height: plane.planeGeometry.height / 8, length: plane.planeGeometry.height / 8)
pyramidGeometry.firstMaterial?.diffuse.contents = UIColor.white
let pyramidNode = SCNNode(geometry: pyramidGeometry)
pyramidNode.name = "pyramid"
pyramidNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
pyramidNode.physicsBody?.categoryBitMask = BodyType.pyramid.rawValue | BodyType.plane.rawValue
pyramidNode.physicsBody?.contactTestBitMask = BodyType.box.rawValue
pyramidNode.position = SCNVector3(-(plane.planeGeometry.width) / 3, 0, plane.planeGeometry.height / 3)
node.addChildNode(pyramidNode)
}
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
let plane = self.planes.filter {
plane in return plane.anchor.identifier == anchor.identifier
}.first
if plane == nil {
return
}
plane?.update(anchor: anchor as! ARPlaneAnchor)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//pause session
sceneView.session.pause()
}
}
I think i followed the same tutorial. I also had same result. Reason is because when the cube drops from higher place, it accelerates and doesnot exactly hit on the plane but passes through. If you scale down the cube to '1 mm' you can see box completely passes through plane and continue falling below plane. You can try droping cube from nearer to the plane, box drops slower and this 'jiggling' will not occur. Or you can try with box with small height instead of plane.
I had the same problem i found out one solution.I was initializing the ARSCNView programmatically.I just removed those code and just added a ARSCNView in the storyboard joined it in my UIViewcontroller class using IBOutlet it worked like a charm.
Hope it helps anyone who is going through this problem.
The same code is below.
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints,ARSCNDebugOptions.showWorldOrigin]
sceneView.delegate = self
sceneView.showsStatistics = true
let scene = SCNScene()
sceneView.scene = scene
}
The "jiggling" is probably caused by an incorrect gravity vector. Try experimenting with setting the gravity of your scene.
For example, add this to your viewDidLoad function:
sceneView.scene.physicsWorld.gravity = SCNVector3Make(0.0, -1.0, 0.0)
I found that setting the gravity - either through code, or by loading an empty scene - resolves this issue.
I'm having an issue with the physics of cones and pyramids in SceneKit where the physics body seems to hover above the ground. If I replace it with say a SCNBox then there is no issue. It is more noticeable at smaller scales where the space above the ground is much larger relative to the size of the node. It's almost like there is a fixed offset. It happens whether the cone has the flat side or point facing the floor.
Code and screenshots below (linked), sample created and runs in a newly created Xcode SceneKit project.
(Xcode 9, swift 4 but was having the issue in prior version of Xcode and swift 3 as well).
image with geometry
image with just physics
import UIKit
import QuartzCore
import SceneKit
var scnView: SCNView!
var scnScene: SCNScene!
var cameraNode: SCNNode!
let universalScale: Float = 1 / 40
enter image description hereclass GameViewController: UIViewController, SCNSceneRendererDelegate {
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupScene()
setupCamera()
spawnFloor()
spawnShape()
}
func setupView() {
scnView = self.view as! SCNView
scnView.showsStatistics = false
scnView.allowsCameraControl = true
scnView.autoenablesDefaultLighting = true
scnView.debugOptions = .showPhysicsShapes
scnView.showsStatistics = true
scnView.delegate = self
}
func setupScene() {
scnScene = SCNScene()
scnView.scene = scnScene
}
func setupCamera() {
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0 * universalScale, y: 50 * universalScale, z: 50 * universalScale)
cameraNode.rotation = SCNVector4Make(1, 0, 0, -Float(Double.pi)/4)
scnScene.rootNode.addChildNode(cameraNode)
}
func spawnFloor() {
let floor = SCNFloor()
floor.reflectivity = 0.5
let material = floor.firstMaterial
material?.diffuse.contents = UIColor.gray
let floorNode = SCNNode(geometry: floor)
floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
scnScene.rootNode.addChildNode(floorNode)
}
var shapeNode: SCNNode?
func spawnShape(){
let cone = SCNCone(topRadius: 0, bottomRadius:CGFloat(1 * universalScale), height: CGFloat(universalScale * 2))
let coneShape = SCNPhysicsShape(geometry: cone, options: nil)
let coneNode = SCNNode(geometry: cone)
coneNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: coneShape)
coneNode.position = SCNVector3(0, -coneNode.boundingBox.min.y + 20*universalScale, 0)
scnScene.rootNode.addChildNode(coneNode)
shapeNode = coneNode
}
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
if shapeNode != nil{
print(shapeNode!.presentation.position)
print(shapeNode!.boundingBox.min)
}
}
}