Add shadow to SCNPlane - swift

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.

Related

swift SceneKit Sphere deformation after changing position

I have two spheres on the sceneView
let earthSphere = SCNSphere(radius: 5.0)
earthSphere.firstMaterial?.diffuse.contents = UIColor.green
let objNode1 = SCNNode(geometry: earthSphere)
objNode1.position = SCNVector3(x: 0, y: 0, z: 0)
let moonSphere = SCNSphere(radius: 0.5)
moonSphere.firstMaterial?.diffuse.contents = UIColor.red
let objNode2 = SCNNode(geometry: moonSphere)
objNode2.position = SCNVector3(x: 3, y: 3, z: 0)
but moonSphere is deforming image. How to fix that ? It is possible to do that earthSphere and moonSphere look same ? without deformation
etc: cameraNode set to position SCNVector3(x: 0, y: 0, z: 7)

In SceneKit, putting a text label onto SCNGeometry or SCNBox

I'm trying to put a simple text label onto one (hopefully all types) of built in SCNGeometry shapes as they move across the screen. The closest I have come to success is adding a CALayer with CATextLayer to a SCNBox via .firstMaterial.diffuse.contents, as described in this thread.
BUT, the text is never readable. With a SCNBox of height 1.0: when the size of the layer.frame and textLayer.fontSize is 1.0, the text does not appear; as the frame and font size increase (not the box) the text appears blotchy, like in the image below; and when very large, the text appears as squiggly lines.
The following code is part of the method that spawns shapes:
var geometry:SCNGeometry
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 4, height: 4)
layer.backgroundColor = UIColor.white.cgColor
var textLayer = CATextLayer()
textLayer.frame = layer.bounds
textLayer.fontSize = layer.bounds.size.height
textLayer.string = "Matilda"
textLayer.alignmentMode = kCAAlignmentLeft
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.display()
layer.addSublayer(textLayer)
let geometry = SCNBox(width: 1.0,
height: 1.0,
length: 3.0,
chamferRadius: 0.0)
geometry.firstMaterial?.locksAmbientWithDiffuse = true
geometry.firstMaterial?.diffuse.contents = layer
let geometryNode = SCNNode(geometry: geometry)
geometryNode.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
scnScene.rootNode.addChildNode(geometryNode)
As pointed out by David R., the units of the box size is not the same as the units of the frame size.
It worked after adjusting the frame to (and optimising SCNBox size):
layer.frame = CGRect(x: 0, y: 0, width: 200, height: 50)

simd_float4x4 Columns

I want to translate a plane without rotating the image. For any reason my image is being rotated.
var translation = matrix_identity_float4x4
translation.colum = -0.2
let transform = simd_mul(currentFrame.camera.transform, translation)
planeNode.simdWorldTransform = matrix_multiply(currentFrame.camera.transform, translation)
Also, I notice that matrix_identity_float4x4 contains 4 columns but the documentation is not available.
Why 4 columns? Are there the frame of the plane?
The simplest way to do it is to use the following code for positioning:
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 20, height: 20)
// At first we need to rotate a plane about its x axis in radians:
planeNode.rotation = SCNVector4(1, 0, 0, -Double.pi/2)
planeNode.geometry?.materials.first?.diffuse.contents = UIColor.red
planeNode.position.x = 10
planeNode.position.z = 10
// planeNode.position = SCNVector3(x: 10, y: 0, z: 10)
scene.rootNode.addChildNode(planeNode)
or this way:
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
scene.rootNode.addChildNode(cameraNode)
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 20, height: 20)
planeNode.rotation = SCNVector4(1, 0, 0, -Double.pi/2)
planeNode.geometry?.materials.first?.diffuse.contents = UIColor.red
let distance: Float = 50
planeNode.simdPosition = cameraNode.simdWorldFront * distance // -Z axis
planeNode.simdPosition = cameraNode.simdWorldRight * distance // +X axis
scene.rootNode.addChildNode(planeNode)
If you wanna know more about matrices used in ARKit and SceneKit frameworks just look at Figure 1-8 Matrix configurations for common transformations.
Hope this helps.

SCNNodes disappear after zooming out in Scene Kit?

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.

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.