SCNNodes disappear after zooming out in Scene Kit? - swift

I am attempting to generate a 3d voxel style island and am generating bricks and placing them on the scene. However, when I set the z axis on the camera node to more than 150, objects disappear behind the white background.
import Cocoa
import SceneKit
import PlaygroundSupport
let view = SCNView()
let scene = SCNScene()
view.scene = scene
view.frame = CGRect(x: 0, y: 0, width: 650, height: 650)
public func buildIsland(size: Int, image: NSImage, scene: SCNScene){
//Start building the island
var blocks = 0
for x in 0...size {
for y in 0...size {
//Create Block
var block = SCNBox(width: 10, height: 10, length: 10, chamferRadius: 0)
var color = SCNMaterial()
color.diffuse.contents = CGColor.init(red: 0, green: 1, blue: 0, alpha: 1)
block.materials[0] = color
var node = SCNNode(geometry: block)
node.position = SCNVector3(x/2, y/2, 0)
scene.rootNode.addChildNode(node)
blocks = blocks + 1
}
}
}
view.autoenablesDefaultLighting = true
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 200)
scene.rootNode.addChildNode(cameraNode)
buildIsland(size: 4, image: NSImage(), scene: scene)
view.allowsCameraControl = true
PlaygroundPage.current.liveView = view

Configure the zFar property of the camera to avoid clipping when the camera is further away. (The default value is 100)
For example:
cameraNode.camera?.zFar = 500
You can play with this property to get everything visible while trying to keep good performance.

Related

How can I rotate 3d object using right and left arrows in Swift? (SceneKit)

How can I enable the user to move the object using the right and left arrows I put below, instead of manually moving it?
"allowscameracontrol" allows the user to rotate the object with their hand. But I just want it to be rotated using arrows.
** -> sceneView.allowsCameraControl = true**
import UIKit
import SceneKit
class ViewController: UIViewController {
#IBOutlet weak var sceneView: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
// 1: Load .obj file
let scene = SCNScene(named: "converse_obj.obj")
//Add camera node
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
//Place camera
cameraNode.position = SCNVector3(x: 0, y: 10, z: 35)
//*Set camera on scene
scene?.rootNode.addChildNode(cameraNode)
//Adding light to scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 35)
scene?.rootNode.addChildNode(lightNode)
// 6: Creating and adding ambien light to scen
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light?.type = .ambient
ambientLightNode.light?.color = UIColor.darkGray
scene?.rootNode.addChildNode(ambientLightNode)
// Allow user to manipulate camera
sceneView.allowsCameraControl = true
// Set background color
sceneView.backgroundColor = UIColor.white
// Allow user translate image
sceneView.cameraControlConfiguration.allowsTranslation = false
// Set scene settings
sceneView.scene = scene
}
}
I haven't used SceneKit before.
I'm not sure if you would want to rotate your camera or rotate the node that contains the object. From the way it's named, it makes it sound like allowsCameraControl rotates the camera. In any case you would use rotationMatrixAroundZ to create a rotation matrix, and then multiply that by the matrix for whichever thing you want to rotate.
If rotating the camera is what you want to do, your code might look like this:
func rotateCameraZByDegrees(_ zRotation: Double) {
let radians = zRotation / 180.0 * Double.pi
let rotationZ = rotationMatrixAroundZ(radians: radians)
cameraNode.transform = SCNMatrix4(simd_mul(simd_float4x4(cameraNode.transform), rotationZ))
}
If you instead want to rotate your object's node, and your object's node is named sneakersNode, change the last line to:
sneakersNode.transform = SCNMatrix4(simd_mul(simd_float4x4(sneakersNode.transform), rotationZ))
This post [31881911] in stackO shows you some examples for arrow keys.
Turn allowsCameraControl = false
This post [68506605] see my answer - creating a camera control class makes things a lot easier.
For your rotation, just rotate the object directly - several ways to do this.
func resetRotation()
{
for (_, vNode) in displayNodes
{
vNode.node.removeAllActions()
}
vRotate0 = SCNAction.rotateTo(x: 0, y: 0, z: 0, duration: 2)
vRotate1 = SCNAction.rotateBy(x: 0, y: CGFloat(GLKMathDegreesToRadians(450)), z: 0, duration: 3)
vRotate2 = SCNAction.rotateBy(x: 0, y: CGFloat(GLKMathDegreesToRadians(-90)), z: 0, duration: 1)
vRotate3 = SCNAction.rotateBy(x: CGFloat(GLKMathDegreesToRadians(-45)), y: 0, z: 0, duration: 1)
vRotate4 = SCNAction.rotateBy(x: CGFloat(GLKMathDegreesToRadians(45)), y: 0, z: 0, duration: 1)
seq = SCNAction.sequence([vRotate0, vRotate1, vRotate2, vRotate3, vRotate4, vRotate3, vRotate4])
allSeq = SCNAction.repeatForever(seq)
for (_, vNode) in displayNodes
{
vNode.node.runAction(allSeq)
}
}
You could use some sequences - so your object could do a 360 and stop, come back 90 at a slower pace, whatever you want. The above repeats forever, but you can take that out.
Or just on Left key press, rotate Y += n degrees.

Add shadow to SCNPlane

I have ARKit artwork app. And I want my artworks to have a shadow just behind it. The shadow mustn't be dynamic. Just static shadow.
I've tried to add a plane behind the artwork. And set a shadow to it. But I discovered that it is not easy.
Can you help me?
let shadow = SCNPlane(width: artwork.size.width * 0.0254 + 0.03, height: artwork.size.height * 0.0254 + 0.015)
let layer = CALayer()
layer.frame = CGRect(origin: .zero, size: CGSize(width: shadow.width, height: shadow.height))
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 1
layer.shadowOffset = .zero
layer.shadowRadius = 10
shadow.firstMaterial?.diffuse.contents = layer
The shadow should be like in UIKit's UIView. But this code just doesn't work.
I need something like
UPDATED.
In order to cast and receive a shadows from 3D lights, you have to use a 3D primitives (like cube, sphere, cylinder, etc), or 3D models in such formats as .usdz, .dae or .obj.
Shadow on a visible plane.
Use the following code to test how light generates shadows for 3D geometry:
let scene = SCNScene()
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .spot
lightNode.light!.castsShadow = true
lightNode.light!.shadowMode = .deferred
lightNode.rotation = SCNVector4(x: -1, y: 0, z: 0, w: CGFloat.pi/2)
lightNode.position = SCNVector3(x: 0, y: 20, z: 0)
scene.rootNode.addChildNode(lightNode)
let sphereNode = SCNNode()
sphereNode.geometry = SCNSphere(radius: 2)
sphereNode.position = SCNVector3(x: 0, y: -2, z: 0)
scene.rootNode.addChildNode(sphereNode)
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 15, height: 15)
planeNode.position = SCNVector3(x: 0, y: -5, z: 0)
planeNode.rotation = SCNVector4(x: -1, y: 0, z: 0, w: CGFloat.pi/2)
planeNode.geometry?.materials.first?.diffuse.contents = UIColor.red
scene.rootNode.addChildNode(planeNode)
Shadow on invisible plane.
In case you need an invisible plane that receives a shadow, use the following code:
planeNode.geometry?.materials.first?.colorBufferWriteMask = []
planeNode.geometry?.materials.first?.writesToDepthBuffer = true
planeNode.geometry?.materials.first?.lightingModel = .constant
Fake shadow.
And if you want to use a fake shadow written as a .png (png file format can hold 4 channels) texture on geometry (with a premultiplied alpha channel – RGB x A), use the following approach:
planeNode.geometry?.materials.first?.diffuse.contents = UIImage(named: "shadow.png")
lightNode.light!.castsShadow = false
Here's my answer on fake shadows.
And here's your premultiplied shadow in .png format (just drag-and-drop it on your desktop):
You can change its size and transparency and, of course, you can blur it.

How to achieve realistic Depth of Field effect in SceneKit?

I'm trying to render a frame, with realistic depth of field effect. I've already tried the depth of field properties in the camera node, but it doesn't produce usable results.
Is there a switch to max-out rendering quality of the depth of field effect? Performance is not a factor, I just need to render a frame, and user can wait for it.
Realistic Depth of Field effect in SceneKit
In SceneKit you can easily accomplish cool-looking shallow/deep depth of field (DoF). And it's not extremely intense for processing. .focusDistance and .fStop parameters are crucial for applying DoF:
cameraNode.camera?.wantsDepthOfField = true
cameraNode.camera?.focusDistance = 5
cameraNode.camera?.fStop = 0.01
cameraNode.camera?.focalLength = 24
Use the following code for testing (it's macOS version):
import SceneKit
import Cocoa
class GameViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.camera?.wantsDepthOfField = true
cameraNode.camera?.focusDistance = 5
cameraNode.camera?.fStop = 0.01
cameraNode.camera?.focalLength = 24
scene.rootNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = NSColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
let cylinderNode01 = SCNNode()
cylinderNode01.geometry = SCNCylinder(radius: 2, height: 10)
cylinderNode01.position = SCNVector3(0, 0, 0)
cylinderNode01.geometry?.materials.first?.diffuse.contents = NSImage(named: NSImage.Name("checker01.png"))
scene.rootNode.addChildNode(cylinderNode01)
let cylinderNode02 = SCNNode()
cylinderNode02.geometry = SCNCylinder(radius: 2, height: 10)
cylinderNode02.position = SCNVector3(5, 0, 5)
cylinderNode02.geometry?.materials.first?.diffuse.contents = NSImage(named: NSImage.Name("checker02.jpg"))
scene.rootNode.addChildNode(cylinderNode02)
let cylinderNode03 = SCNNode()
cylinderNode03.geometry = SCNCylinder(radius: 2, height: 10)
cylinderNode03.position = SCNVector3(10, 0, 10)
cylinderNode03.geometry?.materials.first?.diffuse.contents = NSImage(named: NSImage.Name("checker01.png"))
scene.rootNode.addChildNode(cylinderNode03)
let cylinderNode04 = SCNNode()
cylinderNode04.geometry = SCNCylinder(radius: 2, height: 10)
cylinderNode04.position = SCNVector3(-5, 0, -5)
cylinderNode04.geometry?.materials.first?.diffuse.contents = NSImage(named: NSImage.Name("checker02.jpg"))
scene.rootNode.addChildNode(cylinderNode04)
let cylinderNode05 = SCNNode()
cylinderNode05.geometry = SCNCylinder(radius: 2, height: 10)
cylinderNode05.position = SCNVector3(-10, 0, -10)
cylinderNode05.geometry?.materials.first?.diffuse.contents = NSImage(named: NSImage.Name("checker01.png"))
scene.rootNode.addChildNode(cylinderNode05)
let scnView = self.view as! SCNView
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.backgroundColor = NSColor.black
}
}
SceneKit isn't able to do (out of the box) heavy, high quality post processing or still image rendering computation of this type. Theoretically you could probably build a setup that uses its rendering approaches to do both. But it's not a high quality renderer. If the user can wait, and you really want to focus on quality of imagery, Unreal Engine has the capacity to do this sort of thing, built in, and far higher quality post processing, effects, lights, materials, particles and rendering.

Using SceneKit for hitTesting not returning a hit with SCNNode

The documentation in XCode clearly states that hitTesting a geometry in SceneKit can be done with SCNRender, SCNView or the SCNNode themselves when one plans to test a 3D line segment. I have a use for SCNScene with its nodes without a renderer or a view, therefore I am planning to use SCNNode hitTesting. I create a SCNScene, put a SCNNode in it and test a simple ray that goes through, but I always get an empty hitList and I don't understand why:
import Swift
import SceneKit
let boxGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0)
let boxNode = SCNNode(geometry: boxGeometry)
var scene = SCNScene()
scene.rootNode.addChildNode(boxNode)
let from = SCNVector3(x: 0, y: -2, z: 0)
let to = SCNVector3(x: 0, y: 2 , z: 0)
var hits = scene.rootNode.hitTestWithSegmentFromPoint(from, toPoint: to, options:nil) // this is always empty
if hits != nil {
if hits!.count > 0 {
var hit = (hits!.first as! SCNHitTestResult).node as SCNNode
}
}
I have tried passing various forms of options but nothing changes.
SCNHitTestFirstFoundOnlyKey: yes or no does not change anything
SCNHitTestSortResultsKey: yes or no does not change anything
SCNHitTestClipToZRangeKey: invalid for SCNNode
SCNHitTestBackFaceCullingKey: yes or no does not change anything
SCNHitTestBoundingBoxOnlyKey: yes or no does not change anything
SCNHitTestRootNodeKey: rootNOde of scene or boxNode does not change
anything
SCNHitTestIgnoreHiddenNodesKey: yes or no does not change anything
What am I doing wrong?
I have found the answer, which is either a bug or a feature: using SCNScene and its nodes SCNNode for 3D hitTesting, in particular the method: "hitTestWithSegmentFromPoint(toPoint:options:)" does not return a hit unless the scene is included in an SCNView. It appears it cannot be used offscreen. My guess is yours for why this is the case, although I can imagine it has something to do with performing some of these quite expensive calculations on the graphics card.
I have tested this using the GameView SCNScene starter project. The critical line is self.gameView!.scene = scene
override func awakeFromNib(){
let scene = SCNScene()
let boxGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
let boxNode = SCNNode(geometry: boxGeometry)
boxNode.position=SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(boxNode)
// 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 = SCNLightTypeOmni
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 = SCNLightTypeAmbient
ambientLightNode.light!.color = NSColor.darkGrayColor()
scene.rootNode.addChildNode(ambientLightNode)
// set the scene to the view
// uncomment this to fail
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.blackColor()
let hitList = scene.rootNode.hitTestWithSegmentFromPoint(SCNVector3(x:-10,y:0,z:0), toPoint: SCNVector3(x:10,y:0,z:0), options:[SCNHitTestBackFaceCullingKey:false, SCNHitTestSortResultsKey:true, SCNHitTestIgnoreHiddenNodesKey:false])
if hitList?.count > 0 {
println("Hit found: \n\n\( hitList![0] )") // assign self.gameView!.scene = scene to reach this point.
} else {
println("No hit") // uncomment self.gameView!.scene = scene to reach this point.
}
}
I've also had trouble with hitTestWithSegmentFromPoint.
I was calling it in viewDidLoad() and it returned a 0 elements array, though I was sure there was a hit.
Calling it in viewDidAppear() (or later) solved my problem.

Using SceneKit in Swift Playground

I've looked everywhere for this but I'm coming up blank. How do you replicate what Chris Lattner was demonstrating with Playgrounds and SceneKit at WWDC? I want to have a SceneKit scene, animating, in Playgrounds.
I tried cutting and pasting the setup code from the SceneKit project template, thinking it would magically start rendering, but it does not.
I tried watching the keynote and pausing and zooming on on Lattner's screen looking for hints at the source code, but he appeared to be importing all his code from elsewhere in his project, so it gave me no clues. There does not seem to be anything in the documentation, or I'm missing it.
Since Swift doesn't have source compatibility between versions, the code in this answer might not work in either future or previous versions of Swift. Currently is has been updated to work in Xcode 7.0 Playgrounds with Swift 2.0.
The XCPlayground framework is what you need, and it is documented here.
Here is a very simple scene to get you started with Scene Kit in Swift:
import SceneKit
import QuartzCore // for the basic animation
import XCPlayground // for the live preview
import PlaygroundSupport
// create a scene view with an empty scene
var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
var scene = SCNScene()
sceneView.scene = scene
// start a live preview of that view
PlaygroundPage.current.liveView = sceneView
// default lighting
sceneView.autoenablesDefaultLighting = true
// a camera
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 3)
scene.rootNode.addChildNode(cameraNode)
// a geometry object
var torus = SCNTorus(ringRadius: 1, pipeRadius: 0.35)
var torusNode = SCNNode(geometry: torus)
scene.rootNode.addChildNode(torusNode)
// configure the geometry object
torus.firstMaterial?.diffuse.contents = NSColor.red // (or UIColor on iOS)
torus.firstMaterial?.specular.contents = NSColor.white // (or UIColor on iOS)
// set a rotation axis (no angle) to be able to
// use a nicer keypath below and avoid needing
// to wrap it in an NSValue
torusNode.rotation = SCNVector4(x: 1.0, y: 1.0, z: 0.0, w: 0.0)
// animate the rotation of the torus
var spin = CABasicAnimation(keyPath: "rotation.w") // only animate the angle
spin.toValue = 2.0*Double.pi
spin.duration = 3
spin.repeatCount = HUGE // for infinity
torusNode.addAnimation(spin, forKey: "spin around")
When I run it, it looks like this:
Note that to run Scene Kit in an iOS playground, you need to check the "Run in Full Simulator" checkbox.
You find the Playground Setting in the Utilities Pane (⌥⌘0 to hide or show)
To get the playground running with iOS as target, and using the latest Xcode 8.1, I got it working with the following modifications to David Rönnqvist's original code.
import UIKit
import SceneKit
import QuartzCore // for the basic animation
import PlaygroundSupport
// create a scene view with an empty scene
var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
var scene = SCNScene()
sceneView.scene = scene
PlaygroundPage.current.liveView = sceneView
// default lighting
sceneView.autoenablesDefaultLighting = true
// a camera
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 3)
scene.rootNode.addChildNode(cameraNode)
// a geometry object
var torus = SCNTorus(ringRadius: 1, pipeRadius: 0.35)
var torusNode = SCNNode(geometry: torus)
scene.rootNode.addChildNode(torusNode)
// configure the geometry object
torus.firstMaterial?.diffuse.contents = UIColor.red
torus.firstMaterial?.specular.contents = UIColor.white
// set a rotation axis (no angle) to be able to
// use a nicer keypath below and avoid needing
// to wrap it in an NSValue
torusNode.rotation = SCNVector4(x: 1.0, y: 1.0, z: 0.0, w: 0.0)
// animate the rotation of the torus
var spin = CABasicAnimation(keyPath: "rotation.w") // only animate the angle
spin.toValue = 2.0*M_PI
spin.duration = 3
spin.repeatCount = HUGE // for infinity
torusNode.addAnimation(spin, forKey: "spin around")
The main things you have to do different are:
to assign to the playground's liveView and,
also open up Xcode's Assistant Editor (The two intersecting circles icon on the toolbar)
Expanding on Moshe's response.
If that keyboard combination doesn't work for you, try going to the the menu bar and select View > Assistant Editor > Show Assistant.
In Xcode 10.2 and higher, there's a PlaygroundSupport framework. It shares a playground data, manages live views, and controls the execution of a playground.
import PlaygroundSupport
You can use Playground Support from within playgrounds to:
Access a playground page and manage its execution
Access and share persistent data
Assess the progress of the learner, update hints, and show success text
About Swift Playgrounds for iPad read here.
Here's the code:
import PlaygroundSupport
import SceneKit
var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 1000, height: 200))
var scene = SCNScene()
sceneView.scene = scene
sceneView.backgroundColor = .black
PlaygroundPage.current.liveView = sceneView
var lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .directional
lightNode.light?.intensity = 3000
lightNode.light?.shadowMode = .deferred
lightNode.rotation = SCNVector4(x: 0, y: 0, z: 0.5, w: 1.5 * Float.pi)
scene.rootNode.addChildNode(lightNode)
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 2.5, y: 0, z: 5)
scene.rootNode.addChildNode(cameraNode)
var box = SCNBox(width: 3, height: 3, length: 3, chamferRadius: 0.4)
var boxNode = SCNNode(geometry: box)
scene.rootNode.addChildNode(boxNode)
box.firstMaterial?.diffuse.contents = UIColor.blue
box.firstMaterial?.specular.contents = UIColor.purple
boxNode.rotation = SCNVector4(x: 1.0, y: 1.0, z: 0.0, w: 0.0)
boxNode.scale = SCNVector3(x: 1.0, y: 1.0, z: 1.0)
Then apply a transform animation:
var spin = CABasicAnimation(keyPath: "rotation.w")
var scale = CABasicAnimation(keyPath: "scale.x")
spin.toValue = 3 * -CGFloat.pi
spin.duration = 2
spin.repeatCount = .greatestFiniteMagnitude
scale.toValue = 1.5
scale.duration = 2
scale.repeatCount = .infinity
boxNode.addAnimation(spin, forKey: "spin around")
boxNode.addAnimation(scale, forKey: "scale x")
If the playground is complaining with 'int is not convertible to CGFloat' then you can use this line of code:
spin.toValue = NSValue(SCNVector4: SCNVector4(x: 1, y: 1, z: 0, w: CGFloat(2.0*M_PI)))
Implicit typecasts seem not to be defined in swift.