How to use "*.scnp" file in SwiftUI for button click (iOS)? - ios

I have "Explode.scnp" SceneKit file. It's already configured, the texture has been set.
How we can use it in SwiftUI?
For example, after the button clicks the background will be animated once.
var body: some View {
ZStack {
VStack {
Button(action: {
Particles()
}) {
Text("Animate")
}
}
}
}
This code works for scn. but how to use it with scnp?
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
Moreover, Xcode version is 11.3.1.
When I try to create a new file I have this:
And the extension is SKS... any ideas?
Reworked the code according to #Asperi version:
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
#Binding var exploding: Bool
//let scene = SCNScene(named: "SceneKit.scnassets/Explode.scnp")!
let scene = SCNScene(named: "SceneKit.scnassets/scene.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: "blow", 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 exploding {
if let scene = scene.rootNode.childNode(withName: "SceneKit.scnassets/scene", recursively: false),
let particles = SCNParticleSystem(named: "SceneKit.scnassets/Explode", inDirectory: nil) {
let node = SCNNode()
node.addParticleSystem(particles)
node.position = scene.position
scnView.scene?.rootNode.addChildNode(node)
scene.removeFromParentNode()
}
}
}
}
Errors related to the path names ...
// retrieve the ship node
here. Tried .sks, .scnp ... any ideas?

Here is modified code with demo. Tested with Xcode 11.4 / macOS 10.15.4
Note: as I don't know your project structure, all dependent resource files were added as Resources (not in Assets). Just in case.
struct DemoSceneKitParticles: View {
#State private var exploding = false
var body: some View {
VStack {
ScenekitView(exploding: $exploding)
Button("BOOM") { self.exploding = true }
}
}
}
struct ScenekitView : NSViewRepresentable {
#Binding var exploding: Bool
let scene = SCNScene(named: "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
if exploding {
if let ship = scene.rootNode.childNode(withName: "ship", recursively: true),
let particles = SCNParticleSystem(named: "Explosion", inDirectory: nil) {
let node = SCNNode()
node.addParticleSystem(particles)
node.position = ship.position
scnView.scene?.rootNode.addChildNode(node)
ship.removeFromParentNode()
}
}
}
}
Update: variant for iOS
Tested with Xcode 11.4 / iOS 13.4
Full module code (resource files as before at top level)
import SwiftUI
import SceneKit
struct DemoSceneKitParticles: View {
#State private var exploding = false
var body: some View {
VStack {
ScenekitView(exploding: $exploding)
Button("BOOM") { self.exploding = true }
}
}
}
struct ScenekitView : UIViewRepresentable {
#Binding var exploding: Bool
let scene = SCNScene(named: "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 exploding {
if let ship = scene.rootNode.childNode(withName: "ship", recursively: true),
let particles = SCNParticleSystem(named: "Explosion", inDirectory: nil) {
let node = SCNNode()
node.addParticleSystem(particles)
node.position = ship.position
scnView.scene?.rootNode.addChildNode(node)
ship.removeFromParentNode()
}
}
}
}
struct DemeSKParticles_Previews: PreviewProvider {
static var previews: some View {
DemoSceneKitParticles()
}
}

Related

Perform SCNAction using a button (SwiftUI)

With UIKit I have created a SceneView with an object, that can perform SCNActions when a button has been pressed:
import UIKit
import SceneKit
class ViewController: UIViewController {
#IBOutlet weak var ScenekitView: SCNView!
var scene:SCNScene!
var ship:SCNNode!
#IBAction func button(_ sender: Any) {
let sequence = SCNAction.sequence([SCNAction.moveBy(x: 0, y: 0, z: -10, duration: 0.5),SCNAction.moveBy(x: 0, y: 0, z: 10, duration: 0.5)])
ship.runAction(sequence)
}
override func viewDidLoad() {
super.viewDidLoad()
scene = SCNScene(named: "mainScene.scn")!
var cameraNode: SCNNode!
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
cameraNode.scale = SCNVector3(1, 1, 0.5)
cameraNode.eulerAngles = SCNVector3(0, 0, 0)
ScenekitView.scene = scene
ship = scene.rootNode.childNode(withName: "ship reference", recursively: true)!
let pos = SCNVector3Make(0, 0, 0)
ship.runAction(SCNAction.move(to: pos, duration: 1))
}
}
Now I want to achieve the same using SwiftUI.
While I am able to, in this example, let the camera perform an action once the view has loaded, I have no idea how to let it perform another action after the press of a button.
Especially because the node is defined in a function in another sub view.
This is what I got so far:
import SwiftUI
import SceneKit
struct ContentView: View {
var body: some View{
VStack {
ScenekitView().ignoresSafeArea()
Button("move") {
//here I would like to call a function including a SCNAction
}
}
}
}
struct ScenekitView : UIViewRepresentable {
func makeUIView(context: Context) -> SCNView {
let scene = SCNScene(named: "SceneFile.scn")!
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
scene.rootNode.addChildNode(cameraNode)
cameraNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 5, duration: 1)))
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
scnView.allowsCameraControl = false
}
}
Thanks for any helpful ideas.
You can create an object owned by the parent view (ContentView) that distributes the scene reference to the ScenekitView and allows access from the Button.
You might want to adjust details (like where the camera setup is done), but this is the general concept:
class SceneManager : ObservableObject {
let scene = SCNScene(named: "SceneFile.scn")!
var ship:SCNNode
init() {
ship = scene.rootNode.childNode(withName: "ship reference", recursively: true)!
}
func move() {
let pos = SCNVector3Make(0, 0, 0)
ship.runAction(SCNAction.move(to: pos, duration: 1))
}
}
struct ContentView: View {
#StateObject var sceneManager = SceneManager()
var body: some View{
VStack {
ScenekitView(scene: sceneManager.scene)
.ignoresSafeArea()
Button("move") {
sceneManager.move()
}
}
}
}
struct ScenekitView : UIViewRepresentable {
var scene : SCNScene
func makeUIView(context: Context) -> SCNView {
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
scene.rootNode.addChildNode(cameraNode)
cameraNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 5, duration: 1)))
let scnView = SCNView()
scnView.scene = scene
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.allowsCameraControl = false
}
}

How to use ScnNode as a ScnLight?

var sun = SCNNode(geometry: SCNSphere(radius:0.35))
sun.geometry?.firstMaterial?.diffuse.contents=#imageLiteral(resourceName: "Sun")
sun.position=SCNVector3(0,0,-1)
And i want to use the sun SCNSphere as a omni light source.
let OmniLight = SCNLight()
OmniLight.type = SCNLight.LightType.omni
OmniLight.color = UIColor.white
But if i run this code, the sun is full black.
add it to the scene:
scene.rootNode.addChildNode(sun)
Here's example playground code:
import UIKit
import SceneKit
import PlaygroundSupport
// create a scene view with an empty scene
var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 640, height: 480))
var scene = SCNScene()
sceneView.scene = scene
PlaygroundPage.current.liveView = sceneView
// default lighting
sceneView.autoenablesDefaultLighting = false
// a camera
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 3)
scene.rootNode.addChildNode(cameraNode)
// your code (minus the image)
var sun = SCNNode(geometry: SCNSphere(radius:0.35))
sun.geometry?.firstMaterial?.diffuse.contents = UIColor.orange
sun.position=SCNVector3(0,0,-1)
let OmniLight = SCNLight()
OmniLight.type = SCNLight.LightType.omni
OmniLight.color = UIColor.white
// add to scene
scene.rootNode.addChildNode(sun)

Rotate the Scene using panGestureRecognizer

I'm a new programmer working on a simple demo iOS program which I use SceneKit to render the scene.
I want to rotate the camera to see different perspective of the scene. But the original camera control is little bit tricky. Especially when I want to rotate the scene in one direction. e.g. if I swipe from right to left and the camera goes from right to left, then go downwards, then go upwards, then go downwards again, finally it goes left again.
What I want to achieve is that when I swipe from right to the left, the camera rotate around the z-axis. And when I swipe up and down my camera just move up and down. And I can also zoom in and out. Additionally I want to set a constraint so that the camera won't go underground.
So I come up with an idea that I fix the camera to look at the object that I focus on. Then place the object in the center of the scene. And I want to rotate the scene just around z-axis when I swipe the screen.
I assume that I use panGestureRecognizer and translate the position change to a rotation order.But how can I achieve that?
Or you have some other idea to get this results?
Here's a simple version of what I write so far. I've tried using UIPanGestureRecognizer but it didn't work, so I delete it and set allowCameraControl = true
let sceneView = SCNView(frame: self.view.frame)
self.view.addSubview(sceneView)
let scene = SCNScene()
sceneView.scene = scene
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: -5.0, y: 5.0, z: 5.0)
let light = SCNLight()
light.type = SCNLight.LightType.spot
light.spotInnerAngle = 30
light.spotOuterAngle = 80
light.castsShadow = true
let lightNode = SCNNode()
lightNode.light = light
lightNode.position = SCNVector3(x: 1.5, y: 1.5, z: 1.5)
let ambientLight = SCNLight()
ambientLight.type = SCNLight.LightType.ambient
ambientLight.color = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0)
cameraNode.light = ambientLight
let cubeGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
let cubeNode = SCNNode(geometry: cubeGeometry)
let planeGeometry = SCNPlane(width: 50.0, height: 50.0)
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.eulerAngles = SCNVector3(x: GLKMathDegreesToRadians(-90), y: 0, z: 0)
planeNode.position = SCNVector3(x: 0, y: -0.5, z: 0)
cameraNode.position = SCNVector3(x: -3.0, y: 3.0, z: 3.0)
let constraint = SCNLookAtConstraint(target: cubeNode)
constraint.isGimbalLockEnabled = true
cameraNode.constraints = [constraint]
lightNode.constraints = [constraint]
scene.rootNode.addChildNode(lightNode)
scene.rootNode.addChildNode(cameraNode)
scene.rootNode.addChildNode(cubeNode)
scene.rootNode.addChildNode(planeNode)
sceneView.allowsCameraControl = true
I solved this problem by using Spherical coordinate system.
General idea is to get the start position and the angle, then transform it to spherical coordinate system. Then you do rotation by just changing the two angle.
It's even easier to use spherical coordinate system to deal with pinch gesture. You just have to change the radius.
here's some code that I use.
func handlePan(_ gestureRecognize: UIPanGestureRecognizer) {
// retrieve scene
let carView = self.view as! SCNView
// save node data and pan gesture data
let cameraNode = carView.scene?.rootNode.childNode(withName: "Camera", recursively: true)!
//let translation = gestureRecognize.translation(in: gestureRecognize.view!)
let speed = gestureRecognize.velocity(in: gestureRecognize.view!)
var speedX = sign(Float(speed.x)) * Float(sqrt(abs(speed.x)))
var speedY = sign(Float(speed.y)) * Float(sqrt(abs(speed.y)))
if speedX.isNaN {speedX = 0}
if speedY.isNaN {speedY = 0}
// record start value
let cameraXStart = cameraNode!.position.x
let cameraYStart = cameraNode!.position.y
let cameraZStart = cameraNode!.position.z - 1.0
let cameraAngleStartZ = cameraNode!.eulerAngles.z
let cameraAngleStartX = cameraNode!.eulerAngles.x
let radiusSquare = cameraXStart * cameraXStart + cameraYStart * cameraYStart + cameraZStart * cameraZStart
// calculate delta value
let deltaAngleZ = -0.003 * Float(speedX)
let deltaAngleX = -0.003 * Float(speedY)
// get new Value
var cameraNewAngleZ = cameraAngleStartZ + deltaAngleZ
var cameraNewAngleX = cameraAngleStartX + deltaAngleX
if cameraNewAngleZ >= 100 * Float.pi {
cameraNewAngleZ = cameraNewAngleZ - 100 * Float.pi
} else if cameraNewAngleZ < -100 * Float.pi {
cameraNewAngleZ = cameraNewAngleZ + 100 * Float.pi
} else {
// set limit
if cameraNewAngleX > 1.4 {
cameraNewAngleX = 1.4
} else if cameraNewAngleX < 0.1 {
cameraNewAngleX = 0.1
}
// use angle value to get position value
let cameraNewX = sqrt(radiusSquare) * cos(cameraNewAngleZ - Float.pi/2) * cos(cameraNewAngleX - Float.pi/2)
let cameraNewY = sqrt(radiusSquare) * sin(cameraNewAngleZ - Float.pi/2) * cos(cameraNewAngleX - Float.pi/2)
let cameraNewZ = -sqrt(radiusSquare) * sin(cameraNewAngleX - Float.pi/2) + 1
if cameraNode?.camera?.usesOrthographicProjection == false {
cameraNode?.position = SCNVector3Make(cameraNewX, cameraNewY, cameraNewZ)
cameraNode?.eulerAngles = SCNVector3Make(cameraNewAngleX, 0, cameraNewAngleZ)
}
else if cameraNode?.camera?.usesOrthographicProjection == true {
cameraNode?.position = SCNVector3Make(0, 0, 10)
cameraNode?.eulerAngles = SCNVector3Make(0, 0, cameraNewAngleZ)
}
}
}

SceneKit With Vuforia AR library custom models

I am new in SceneKit, working with swift 3 and latest Vuforia library 6-2-9.
I have a problem with applying textures on custom 3D models.
fileprivate func createDefaultMarkerScene(with view: VuforiaEAGLView) -> SCNScene {
let scene = SCNScene()
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .omni
lightNode.light?.color = UIColor.lightGray
lightNode.position = SCNVector3(x: 0, y: 0, z: 1000)
scene.rootNode.addChildNode(lightNode)
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light?.type = .ambient
ambientLightNode.light?.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
let geoNode = SCNNode()
geoNode.name = "model_model"
let normalsSources = SCNGeometrySource(normals: normalsSCNVector3)
let verticesSources = SCNGeometrySource(vertices: verticesSCNVector3)
let texCoordsSources = SCNGeometrySource(textureCoordinates: texCoordCGPoint)
let indicesElements = SCNGeometryElement(indices: indices, primitiveType: SCNGeometryPrimitiveType.triangles)
geoNode.geometry = SCNGeometry(sources: [verticesSources, normalsSources, texCoordsSources], elements: [indicesElements])
geoNode.position = SCNVector3(x: 0, y: 0, z: 0)
geoNode.scale = SCNVector3(x: 50, y: 50, z: 50)
let material = SCNMaterial()
material.diffuse.contents = UIImage(named: "grad_ao.png")
geoNode.geometry?.firstMaterial = material
scene.rootNode.addChildNode(geoNode)
return scene
}
Model is properly rendered, but applied texture is completely messed up. I have tried with rotated image, also uv coordinates all ok [0..1].
Any ideas? Thank you
I have managed to fix an issue.
The key is in "texture.png" file.
I just need to flip the image for 180 degrees with:
material.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(1, -1, 1), 0, 1, 0)

SCNText rendering without curves, not font-like

When I run this SceneKit code:
let txt = SCNText(string: "Hello", extrusionDepth: 0.2)
let textNode = SCNNode(geometry: txt)
scene.rootNode.addChildNode(textNode)
I get very angular text:
It seems to do this regardless of font and it behaves the same way on a device as in the simulator.
Here's the code in context:
// create a new scene
let scene = SCNScene()
// 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: 10, y: 0, z: 75)
// 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 = UIColor.darkGrayColor()
scene.rootNode.addChildNode(ambientLightNode)
let txt = SCNText(string: "Hello", extrusionDepth: 0.2)
let textNode = SCNNode(geometry: txt)
scene.rootNode.addChildNode(textNode)
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
SCNText subdivides the 2D Bézier path for your text just like Core Graphics would do if you were drawing it. You can always use the flatness property to make it smoother, but you won't get "sub-pixel smoothness".
To solve this issue you can use a bigger font size, so that the underlying Bézier path is bigger and more vertices are generated when discretization occurs.
My solution was using big font as #mnuages was suggested then using scale on the text node with
animatedTextNode.scale = SCNVector3(0.1, 0.1, 0.1)
to get what size I wanted.
In that case when you animate text and when it comes closer to the camera it looks smooth.

Resources