Goal: Add SceneKit Scene to SwiftUI MacOS project (not Catalyst) using UIViewRepresentable
What I did:
Following code works fine when target is SwiftUI iOS.
But when target is MacOS, I get error bellow:
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
let scene = SCNScene(named: "art.scnassets/ship.scn")!
func makeUIView(context: Context) -> SCNView {
// 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 = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
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
}
}
#if DEBUG
struct ScenekitView_Previews : PreviewProvider {
static var previews: some View {
ScenekitView()
}
}
#endif
Really appreciate if anyone here can help!
Cheers to all!
Not much to change - just instead of UIViewRepresentable conform to NSViewRepresentable, and similar replacements from AppKit instead of UIKit
Here it is (tested with Xcode 11.2):
import SwiftUI
import SceneKit
import AppKit
struct ScenekitView : NSViewRepresentable {
let scene = SCNScene(named: "art.scnassets/ship.scn")!
func makeNSView(context: NSViewRepresentableContext<ScenekitView>) -> SCNView {
// 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)
// 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 = SCNView()
return scnView
}
func updateNSView(_ scnView: SCNView, context: Context) {
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 = NSColor.black
}
}
Related
Goal: get the video feed from ARSCNView. and assign as a video material to a SceneKIt SCNGeometry.
What I did:
// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "shipMesh", recursively: true)!
// apply AR feed to the ship node material
let material = ship.geometry?.firstMaterial
material!.diffuse.contents = arView.scene.background.contents
Problem: the ship is white, without AR video feed
Full code bellow:
import UIKit
import QuartzCore
import SceneKit
import ARKit
class GameViewController: UIViewController, ARSCNViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let arView = ARSCNView()
// 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)
// 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: "shipMesh", recursively: true)!
let material = ship.geometry?.firstMaterial
material!.diffuse.contents = arView.scene.background.contents
material!.lightingModel = .constant
// 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 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
// 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 == .phone {
return .allButUpsideDown
} else {
return .all
}
}
}
Create a fresh (out of the box) ARKit SceneKit based App
(the one with the SpaceShip)
Then you modify your viewWillAppear section like this:
Add the DispatchQueue stuff below.
Important: Add a short delay (here 5.0 seconds) to give the AR Subsystem enough time to initialise the Camera.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// Run the view's session
sceneView.session.run(configuration)
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
let shipMesh = self.sceneView.scene.rootNode.childNode(withName: "shipMesh", recursively: true)
shipMesh?.geometry?.firstMaterial?.diffuse.contents = self.sceneView.scene.background.contents
}
}
I hope, this is what you were looking for.
I have a cylinder as SCNCylinder in a SCNScene in SceneKit and want to display it in a frame in SwiftUI. My goal is to rotate the cylinder by a angle of 180° or 90° (as the user chooses). To take the input (of the angle of rotation) i have used Text() and onTapGesture{ .... } property in SwiftUI. After I tap the text, the cylinder rotates but now I have two cylinders, one at the original position and one rotating at an desired angle. I am not sure why this happens. I want the same cylinder to rotate, not a identical copy of that doing it. I have connected the SwiftUI view and SceneKit view by using #State and #Binding.
Here is my code :
struct ContentView: View {
#State var rotationAngle = 0
var body: some View {
VStack{
Text("180°").onTapGesture {
self.rotationAngle = 180
}
Spacer()
Text("90°").onTapGesture {
self.rotationAngle = 90
}
SceneKitView(angle: $rotationAngle)
.position(x: 225.0, y: 200)
.frame(width: 300, height: 300, alignment: .center)
}
}
}
struct SceneKitView: UIViewRepresentable {
#Binding var angle: Int
func degreesToRadians(_ degrees: Float) -> CGFloat {
return CGFloat(degrees * .pi / 180)
}
func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {
let sceneView = SCNView()
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = UIColor.white
sceneView.frame = CGRect(x: 0, y: 10, width: 0, height: 1)
return sceneView
}
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
let cylinder = SCNCylinder(radius: 0.02, height: 2.0)
let cylindernode = SCNNode(geometry: cylinder)
cylindernode.position = SCNVector3(x: 0, y: 0, z: 0)
cylinder.firstMaterial?.diffuse.contents = UIColor.green
cylindernode.pivot = SCNMatrix4MakeTranslation(0, -1, 0)
let inttofloat = Float(self.angle)
let rotation = SCNAction.rotate(by: self.degreesToRadians(inttofloat), around: SCNVector3(1, 0, 0), duration: 5)
cylindernode.runAction(rotation)
sceneView.scene?.rootNode.addChildNode(cylindernode)
}
typealias UIViewType = SCNView
}
I want to have a single cylinder rotation at a given angle.
The problem is, that updateUIView will be called several times. You can check this by adding a debug point there. Because of that your cylinder will be added several times. So you can solve this by many ways...one way would be to delete all nodes in your sceneview before starting your animation like so:
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
sceneView.scene?.rootNode.enumerateChildNodes { (node, stop) in
node.removeFromParentNode()
}
Here is a simplified scenekit default scene with the ship. Tap the ship, release, and the ship spins. How do you modify program so that when you tap the ship, the action starts? No worry about releasing or holding tap.
class GameViewController: UIViewController {
override func viewDidLoad() { super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/ship.scn")!
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = SCNLightTypeOmni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = UIColor.darkGrayColor()
scene.rootNode.addChildNode(ambientLightNode)
let scnView = self.view as! SCNView
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.showsStatistics = true
scnView.backgroundColor = UIColor.blackColor()
let tapGesture = UITapGestureRecognizer(target: self, action: "handleTap:")
scnView.addGestureRecognizer(tapGesture)
}
func handleTap(gestureRecognize: UIGestureRecognizer) {
let scnView = self.view as! SCNView
// the ship
let ship = scnView.scene!.rootNode.childNodeWithName("ship", recursively: true)!
// the action
let rotateY = SCNAction.repeatActionForever(SCNAction.rotateByX(0, y: 2, z: 0, duration: 1))
let point = gestureRecognize.locationInView(scnView)
let hitResults = scnView.hitTest(point, options: nil)
if hitResults.count > 0 {
let result: AnyObject! = hitResults[0]
// the call
if result.node!.name!.hasPrefix("ship") {
ship.runAction(rotateY)
}
}
}
override func shouldAutorotate() -> Bool { return true }
override func prefersStatusBarHidden() -> Bool { return true }
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone { return .AllButUpsideDown }
else { return .All }
}
override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() }
}
A gesture recognizer recognizes an entire gesture. For a tap that happens when the touch begin and touch end events happen in close proximity. The built in recognizer will only fire once the "tap gesture" is complete and both events are complete.
If you want different behavior on the Touch Begin or Touch End, you will have to handle the low-level events yourself. To do this you can either create your own, custom UIGestureRecognizeror you can create a custom UIView and use the methods in UIResponder like
touchesBegan(_:withEvent:)
Please see the Event Handling Guide for iOS
I have 2 files that I imported from Blender(3D design program) they are both .dae specifically they are "CampusField1.dae" CampusField is the ground/floor of the game and "Bob.dae" is the Man/character. My question is when I set CampusField1 as the scene how do I get "Bob" in the scene too. And the other question is lets say I export the .dae from blender now I put the file in the game... every things good but then is the animation for Bob already attached to the Bob.dae file or do I have to export something else from blender so that I can run the animation because I don't know what the animation ID would be or how to actually make it run and to actually make Bob do something.
Code:
import UIKit
import QuartzCore
import SceneKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/CampusField1.dae")!
let src = SCNSceneSource(URL: yourSceneURL, options: nil)
let node = src.entryWithIdentifier("Bob", withClass: SCNNode.self) as SCNNode
let animation = node.entryWithIdentifier("yourAnimationID", withClass: CAAnimation.self) as CAAnimation
Full GameController Below!:
import UIKit
import QuartzCore
import SceneKit
//============================================================
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//-------------------------
let scene = SCNScene(named: "art.scnassets/CampusField1.dae")!
let src = SCNSceneSource(URL: yourSceneURL, options: nil)
let node = src.entryWithIdentifier("Bob", withClass: SCNNode.self) as SCNNode
let animation = node.entryWithIdentifier("yourAnimationID", withClass: CAAnimation.self) as CAAnimation
//--------------------------
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
//-----------------------------------------------
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = SCNLightTypeOmni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
//-----------------------------------------------
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = UIColor.darkGrayColor()
scene.rootNode.addChildNode(ambientLightNode)
//----------------------------------------------
//_ = scene.rootNode.childNodeWithName("Bob", recursively: true)!
// _ = scene.rootNode.childNodeWithName("CampusField1", recursively: true)!
//--------------------------------------------------------
// Bob.runAction(SCNAction.repeatActionForever(SCNAction.rotateByX(0, y: 2, z: 0, duration: 1)))
let scnView = self.view as! SCNView
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.showsStatistics = false
scnView.backgroundColor = UIColor.whiteColor()
let tapGesture = UITapGestureRecognizer(target: self, action: "handleTap:")
scnView.addGestureRecognizer(tapGesture)
}
func handleTap(gestureRecognize: UIGestureRecognizer) {
let scnView = self.view as! SCNView
let p = gestureRecognize.locationInView(scnView)
let hitResults = scnView.hitTest(p, options: nil)
if hitResults.count > 0 {
let result: AnyObject! = hitResults[0]
let material = result.node!.geometry!.firstMaterial!
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(0.5)
SCNTransaction.setCompletionBlock {
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(0.5)
material.emission.contents = UIColor.blackColor()
SCNTransaction.commit()
}
material.emission.contents = UIColor.yellowColor()
SCNTransaction.commit()
}
}
//==================================================
override func shouldAutorotate() -> Bool {
return true
}
//============================
override func prefersStatusBarHidden() -> Bool {
return true
}
//==========================
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
//=============================
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}
The first thing to tell you is you have got two SCNScene; CampusField and Bob. Therefore you need to take out the character node from the Bob scene.
You need to name the node as like from the above figure. And extract that node from the scene as:
let bobScene = SCNScene(named: "Bob.dae")
let bobNode = personScene?.rootNode.childNodeWithName("person", recursively: true)
let campusFieldScene = SCNScene(named: "CampusField1.dae")
campusFieldScene.rootNode.addChildNode(bobNode)
I'm trying to do this in Swift. However my SCNView doesn't show anything. I checked the connections in IB and everything is fine. I assume that I made an error while translating the source code from Objective C to Swift. Here is my code:
#IBOutlet var sceneview: SCNView
#IBOutlet var status: NSTextField
var statusCounter: Int = 1
#IBAction func paintRectButton (sender: AnyObject) {
status.stringValue = "Paint (#\(statusCounter++))"
var scene: SCNScene = SCNScene()
var cameraNode: SCNNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 15, 30)
cameraNode.transform = CATransform3DRotate(cameraNode.transform, 7.0, 1, 0, 0)
scene.rootNode.addChildNode(cameraNode)
var spotlight: SCNLight = SCNLight()
spotlight.type = SCNLightTypeSpot
spotlight.color = NSColor.redColor()
var spotlightNode: SCNNode = SCNNode()
spotlightNode.light = spotlight
spotlightNode.position = SCNVector3Make(-2, 1, 0)
cameraNode.addChildNode(spotlightNode)
let boxSide = 15.0
var box: SCNBox =
SCNBox(width: boxSide, height: boxSide, length: boxSide, chamferRadius: 0)
var boxNode: SCNNode = SCNNode(geometry: box)
boxNode.transform = CATransform3DMakeRotation(3, 0, 1, 0)
scene.rootNode.addChildNode(boxNode)
sceneview.scene = scene
}
The reason why nothing shows up is that the camera is looking in a direction where there isn't any geometry object to be rendered. The Objective-C code uses -M_PI/7.0 (≈ -0.4488 radians) for the rotation angle of the camera and but your Swift code is using 7.0 (≈ 0.7168 radians (the remainder after dividing by π)). Change the Swift code to:
cameraNode.transform = CATransform3DRotate(cameraNode.transform, -M_PI/7.0, 1, 0, 0)
A similar mistake seem to have happened with the rotation of the box where the original code uses the angle M_PI_2/3 and the Swift code is using the angle 3.
boxNode.transform = CATransform3DMakeRotation(M_PI_2/3.0, 0, 1, 0)