I would like to create a MacOS application using Swift and SceneKit from scratch [using the Cocoa application template].
Somehow my overlay SpriteKit scene isn't visible while the same steps do work on iOS.
Steps taken:
new project > cocoa application
add sceneview to main.storyboard
add outlet from sceneview to viewcontroller
Code added to viewDidLoad:
// Create scenekit-scene
// ---------------------
let scene = SCNScene()
let nodeCamera = SCNNode()
nodeCamera.camera = SCNCamera()
scene.rootNode.addChildNode(nodeCamera)
let box = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)
box.firstMaterial?.diffuse.contents = NSColor.blue
let nodeBox = SCNNode(geometry: box)
nodeBox.position = SCNVector3(0,0, -2)
scene.rootNode.addChildNode(nodeBox)
// Add to view
sceneView.scene = scene
// Create spritekit-scene
// ----------------------
let spriteScene = SKScene(size: CGSize(width: sceneView.frame.width, height: sceneView.frame.height))
let nodelabel = SKLabelNode(fontNamed: "Menlo")
nodelabel.text = "SpriteKit"
nodelabel.fontColor = NSColor.black
nodelabel.position = CGPoint(x: spriteScene.size.width/2, y: spriteScene.size.height/2)
spriteScene.addChild(nodelabel)
// Add to scenekit
sceneView.overlaySKScene = spriteScene
In the iOS-simulator I see a blue cube with black "SpriteKit" in the middle. On MacOS I only see the blue cube.
[BTW: What is kind of strange: when I use the MacOS game template [scenekit] the overlaySKScene functions normal and is visible.]
So what am I missing?
While comparing the storyboards scene view properties between the cocoa-template and the game-template I noticed the view controller's view in the cocoa-template had a check-mark in view effects inspector>core animation layer.
When unchecked the OverlaySKScene became visible.
Related
Is it possible to animate the .backgroundColor property of an SCNView?
Please note, it is easy to animate the background of an actual scene (SCNScene) and I know how to do that. It is also easy to animate the background of a conventional UIView.
I've not been able to figure out how to animate the .backgroundColor property of an SCNView.
Assuming you take the default SceneKit Game Template (the one with the rotating Jet) I got it working by doing this:
Here is my viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene() // 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)
// 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 ship node
// let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
// animate the 3d object
// ship.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 initial background color of the SCNView
scnView.backgroundColor = UIColor.red
// Setup a SCNAction that rotates i.Ex the HUE Value of the Background
let animColor = SCNAction.customAction(duration: 10.0) { _ , timeElapsed in
scnView.backgroundColor = UIColor.init(hue: timeElapsed/10, saturation: 1.0, brightness: 1.0, alpha: 1.0)
}
// Run the Action (here using the rootNode)
scene.rootNode.runAction(animColor)
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
This might not be the best solution, but using a SCNTransaction I had no luck. Hope I could help in some way.
Just an addendum to the amazing & correct answer of #ZAY.
You have to do a frame-by-frame animation of the color and,
For some reason,
You do the action on the scene view
But. You run the action on the root node of the scene.
So, it's a miracle.
Works perfectly.
I am working on ARKit app and my requirement for switching from ARSCNView to another Viewcontroller frequently and also need to manage SCNNode in ARSCNView.
Main Issue:
Sometimes placed SCNNode is moving very fast uncommonly and goes
out of AR screen.
Sometimes When I try to put a node on the
surface it does not judge the surface and floating node in Air only.
I want to put SCNNode on the surface with transform vertically.
Thanks.
# E.com Please check below sample code which I am using right now to add SCNNode with transform.
let planeGeometry = SCNPlane(width: CGFloat(ARRectWidth), height: CGFloat(ARRectHeight))
let material = SCNMaterial()
material.diffuse.contents = UIImage.init(named: "img")
let matirials : [SCNMaterial] = [material]
planeGeometry.materials = matirials
let rectNode = SCNNode(geometry: planeGeometry)
var transform = SCNMatrix4MakeRotation(-Float.pi / 2.0, 1.0, 0.0, 0.0)
transform = SCNMatrix4Rotate(transform, Float(orientation), 0, 1, 0)
rectNode.transform = transform
self.addChildNode(rectNode)
self.position = position
I am working on an AR project using ARKit.
I want to add a UIView to ARKit Scene. When I tap on an object, I want to get information as a "pop-up" next to the object. This information is in a UIView.
Is it possible to add this UIView to ARKit Scene?
I set up this UIView as a scene and what can I do then?
Can I give it a node and then add it to the ARKit Scene? If so, how it works?
Or is there another way?
Thank you!
EDIT: Code of my SecondViewController
class InformationViewController: UIViewController {
#IBOutlet weak var secondView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.view = secondView
}
}
EDIT 2: Code in firstViewController
guard let secondViewController = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController else {
print ("No secondController")
return
}
let plane = SCNPlane(width: CGFloat(0.1), height: CGFloat(0.1))
plane.firstMaterial?.diffuse.contents = secondViewController.view
let node = SCNNode(geometry: plane)
I only get a white screen of a plane, not the view.
The simplest (although undocumented) way to achieve that is to set a UIView backed by a view controller as diffuse contents of a material on a SCNPlane (or any other geometry really, but it works best with planes for obvious reasons).
let plane = SCNPlane()
plane.firstMaterial?.diffuse.contents = someViewController.view
let planeNode = SCNNode(geometry: plane)
You will have to persist the view controller somewhere otherwise it's going to be released and plane will not be visible. Using just a UIView without any UIViewController will throw an error.
The best thing about it is that it keeps all of the gestures and practically works just as a simple view. For example, if you use UITableViewController's view you will be able to scroll it right inside a scene.
I haven't tested it on iOS 10 and lower, but it's been working on iOS 11 so far. Works both in plain SceneKit scenes and with ARKit.
I cannot provide you code now but this is how to do it.
Create a SCNPlane.
Create your UIView with all elements you need.
Create image context from UIView.
Use this image as material for SCNPlane.
Or even easier make SKScene with label and add it as material for SCNPlane.
Example: https://stackoverflow.com/a/74380559/294884
To place text in a label in the world you draw it into an image and then attach that image to a SCNNode.
For example:
let text = "Hello, Stack Overflow."
let font = UIFont(name: "Arial", size: CGFloat(size))
let width = 128
let height = 128
let fontAttrs: [NSAttributedStringKey: Any] =
[NSAttributedStringKey.font: font as UIFont]
let stringSize = self.text.size(withAttributes: fontAttrs)
let rect = CGRect(x: CGFloat((width / 2.0) - (stringSize.width/2.0)),
y: CGFloat((height / 2.0) - (stringSize.height/2.0)),
width: CGFloat(stringSize.width),
height: CGFloat(stringSize.height))
let renderer = UIGraphicsImageRenderer(size: CGSize(width: CGFloat(width), height: CGFloat(height)))
let image = renderer.image { context in
let color = UIColor.blue.withAlphaComponent(CGFloat(0.5))
color.setFill()
context.fill(rect)
text.draw(with: rect, options: .usesLineFragmentOrigin, attributes: fontAttrs, context: nil)
}
let plane = SCNPlane(width: CGFloat(0.1), height: CGFloat(0.1))
plane.firstMaterial?.diffuse.contents = image
let node = SCNNode(geometry: plane)
EDIT:
I added these lines:
let color = UIColor.blue.withAlphaComponent(CGFloat(0.5))
color.setFill()
context.fill(rect)
This lets you set the background color and the opacity. There are other ways of doing this - which also let you draw complex shapes - but this is the easiest for basic color.
EDIT 2: Added reference to stringSize and rect
I'm trying to put sphere on top of the box, but the position is strange: Ball is half hidden inside box. I tried to change box and sphere pivot, but it didn't help. Here's the code:
let cubeGeometry = SCNBox(width: 10, height: 10, length: 10,
chamferRadius: 0)
let cubeNode = SCNNode(geometry: cubeGeometry)
//cubeNode.pivot = SCNMatrix4MakeTranslation(0, 1, 0)
scene.rootNode.addChildNode(cubeNode)
let ballGeometry = SCNSphere(radius: 1)
let ballNode = SCNNode(geometry: ballGeometry)
ballNode.pivot = SCNMatrix4MakeTranslation(0.5, 0, 0.5)
ballNode.position = SCNVector3Make(0, 5, 0)
cubeNode.addChildNode(ballNode)`
and the result:
what I'm doing wrong? How to put the ball right on the top side of the box?
UPDATE: If I add Cube instead of ball, it looks good as I want
You need to translate on the Y-axis by cube-height/2 + sphere-radius. Thus you should have:
ballNode.position = SCNVector3Make(0, 6, 0)
Here is the screenshot:
The relevant complete code:
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene()
let cubeGeometry = SCNBox(width: 10, height: 10, length: 10,
chamferRadius: 0)
cubeGeometry.firstMaterial?.diffuse.contents = UIColor.yellow
let cubeNode = SCNNode(geometry: cubeGeometry)
scene.rootNode.addChildNode(cubeNode)
let ballGeometry = SCNSphere(radius: 1)
ballGeometry.firstMaterial?.diffuse.contents = UIColor.green
let ballNode = SCNNode(geometry: ballGeometry)
ballNode.position = SCNVector3Make(0, 6, 0)
cubeNode.addChildNode(ballNode)
// 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.gray
}
Update : Why radius and not radius / 2
See this screenshot from the Scene Editor in Xcode. The original position of cube is (0, 0, 0) and so is that of the ball; hence the ball needs to be moved by r and not r / 2; with r / 2 the lower section of the ball with height r/2 will be still inside the cube. You can just add a cube and sphere as in the scene below in the editor and that should help clarify.
Everything is running normally.
You added the ball node to the cube node. So the origin of the ball node is on the center of the cube.
Then you change the position of the ball to half the size of the cube on the y axis. So basically it pops off and you see just the half of the ball (its radius is 1).
So you have to add again half the size of the ball to put it just on the top of the cube :
ballNode.position = SCNVector3Make(0, 5.5, 0)
I'm trying to create a node and animate it's zRotation property with an action, however, when attempting to run the action, the zRotation property of my node is not changed. I'm unsure why this isn't working.
import UIKit
import SpriteKit
let aim = SKSpriteNode()
print(aim.zRotation)
aim.zRotation = CGFloat(Double.pi/2)
print(aim.zRotation)
let myAction = SKAction.rotate(toAngle: CGFloat(3.14), duration: 0)
aim.run(myAction)
print(aim.zRotation)
image of code with output
An SKAction will only be evaluated when your node is in a scene that is in turn being displayed in a view:
An SKAction object is an action that is executed by a node in the
scene (SKScene). ... When the scene processes its nodes, actions
associated with those nodes are evaluated.
(From Apple's SKAction documentation)
At the moment you have neither of those things so the action remains dormant. For an example of adding nodes to a scene and displaying that scene in a playground see this from Swift Studies.
I guess you want to achieve something like this
Issues
There a a few errors in your code:
An action with duration 0 will instantly apply the change, better setting a greater value.
You are not presenting the SKView in Playground
Your SKSpriteNode has not image
Solution
In Playground select View > Assistant > Show Assistant Editor in order to open a panel on the right where we'll add the SKView.
Now add this code an wait for the red square to appear in the Assistant Editor.
import PlaygroundSupport
import SpriteKit
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 500, height: 500))
let scene = SKScene(size: CGSize(width: 500, height: 500))
scene.anchorPoint.x = 0.5
scene.anchorPoint.y = 0.5
sceneView.showsFPS = true
sceneView.presentScene(scene)
PlaygroundPage.current.liveView = sceneView
let square = SKShapeNode(rect: CGRect(x: 0, y: 0, width: 100, height: 100))
square.fillColor = .red
scene.addChild(square)
square.run(.rotate(toAngle: 3.14, duration: 20))