SceneKit and using a custom camera class - scenekit

I am trying to create a custom Camera class to reuse across all levels in SceneKit.
I have defined the cameraNode
Set the sceneView to use the cameraNode pointOfView
Define class:
class GameCamera: SCNCamera {
let cameraNodeHorizontal: SCNNode!
override init() {
cameraNodeHorizontal = SCNScene(named: "/GameAssets.scnassets/Camera.scn")?.rootNode.childNode(withName: "GameCamera", recursively: true)
super.init()
}
func setup(scnView: SCNView) {
scnView.scene?.rootNode.addChildNode(cameraNodeHorizontal)
scnView.pointOfView = cameraNodeHorizontal
}
}
Inside ViewController:
private var camera = GameCamera()
private func loadCamera() {
camera.setup(scnView: self.scnView)
}
The scene renders from a default pointOfView other than the one I defined.
Wondering if anyone can help?

I don't use .scn - but just a basic class, something like this:
var cameraEye = SCNNode()
var cameraFocus = SCNNode()
init()
{
cameraEye.name = "Camera Eye"
cameraFocus.name = "Camera Focus"
cameraFocus.isHidden = true
cameraFocus.position = SCNVector3(x: 0, y: 0, z: 0)
cameraEye.camera = SCNCamera()
cameraEye.constraints = []
cameraEye.position = SCNVector3(x: 0, y: 15, z: 0.1)
let vConstraint = SCNLookAtConstraint(target: cameraFocus)
vConstraint.isGimbalLockEnabled = true
cameraEye.constraints = [vConstraint]
}
// Add camera and focus nodes to your Scenekit nodes
gameNodes.addChildNode(camera.cameraEye)
gameNodes.addChildNode(camera.cameraFocus)

Related

In scene kit, adding a physics body manually (in code) to a node, seems to not work? You have to add the physics body in the editor?

It seems you just can't add a physics body in code.
Not much more can be said than this, has anyone found this issue?
Some example code ...
class SomeSceneView: SCNView {
public var box: SCNNode!
..
var physBody = SCNPhysicsBody()
private func bringup() {
box = scene?.rootNode.childNode(withName: "box", recursively: true)
physBody.type = .dynamic
physBody.isAffectedByGravity = false
physBody.friction = 0
box.physicsBody = physBody
rendersContinuously = true
play(nil)
}
}
Any experience with this?
You need also to file a SCNPhysicsShape.
Try something like this:
func makeNode() {
let node = SCNNode(geometry: SCNSphere(radius: 1.0))
...
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(geometry: node.geometry, options: nil))
...
scene.rootnode.addChildNode(node)
}

ImageView inside of ScrollView showing wrong picture

I have a scroll view that has an image view embedded into it. The user clicks the "Take Photo" button, takes a picture, and then those photos are stored in an array and then displayed on the scrollable imageView. My issue is that after the didFinishSelectingMedia is called and the camera closes, the imageView always shows the first image in the array. I want the imageView to show the most recent image added to the array and scroll backward through the images until the user comes to the first image in the array. How do I make this happen?
Btw: Idk if this is even plausible, but I tried reversing the for loop and that didn't work.
I'm a new programmer, so please explain your answers.
Here's my code:
import UIKit
class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#IBOutlet weak var scrollImageView: UIScrollView!
var imageTaken: [UIImage] = []
var imagePicker = UIImagePickerController()
override func viewDidLoad() {
super.viewDidLoad()
scrollImageView.frame = scrollImageView.frame
}
#IBAction func takePhotoButton(_ sender: Any) {
imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .camera
present(imagePicker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
imageTaken.append((info[.originalImage] as? UIImage)!)
imagePicker.dismiss(animated: true, completion: nil)
for i in 0..<imageTaken.count {
let imageView = UIImageView()
imageView.image = imageTaken[i]
imageView.contentMode = .scaleAspectFit
let xPosition = self.scrollImageView.frame.width * CGFloat(i)
imageView.frame = CGRect(x: xPosition, y: 0, width: self.scrollImageView.frame.width, height: self.scrollImageView.frame.height)
scrollImageView.contentSize.width = scrollImageView.frame.width * CGFloat(i + 1)
scrollImageView.addSubview(imageView)
}
}
}
Two options I guess you can take which ever one is seems easier.
OOP — Create an array of objects example:
struct TakePhotos {
var image : UIImage
var timestamp : Date
}
Then sort the array by the timestamp.
Use PHAsset
import Photos
Using PHAsset you should be able to retrieve the images taken from the user's album. Set a count when the user takes a new image.
Then you can run a query and sort the images in reverse order, they will already have the timestamp.
let photoOptions = PHFetchOptions()
photoOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
This is what I did in my application.
// FIXME: - Query only happens once this needs to be refactored so it happens when the album is updated on the device.
// Query only happens one time.
if imageArray.isEmpty {
let photoOptions = PHFetchOptions()
photoOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let allPhotos = PHAsset.fetchAssets(with: photoOptions)
let fetchOptions = PHImageRequestOptions()
fetchOptions.deliveryMode = .highQualityFormat
fetchOptions.resizeMode = .exact
fetchOptions.normalizedCropRect = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width)
// Must be a synchronous call otherwise the view loads before images are retrieved (result is an empty view).
fetchOptions.isSynchronous = true
let imageSize : CGSize = CGSize(width: view.frame.width, height: view.frame.height)
//Retrieves Images using assets.
allPhotos.enumerateObjects { (assets, index, pointer) in
DispatchQueue.global(qos: .background).sync {
PHImageManager.default().requestImage(for: assets, targetSize: imageSize, contentMode: .aspectFit, options: fetchOptions) { (image, hashDictionary) in
guard let image = image else {return}
self.imageArray.append(image)
self.albumImages.append(ImageAlbum(images: image))
}
}
}
selectedImage.image = imageArray.first
selectedImage.contentMode = .scaleAspectFill
}

SwiftUI + Scenekit + Combine + GeometryReader ==> Scrollable List of 3D Models + Rotation of 3D Models By Scrolling the List

I want to create a scrollable list of 3D models(here: default ship model of Xcode which you can find in a Game project of Xcode).
For that I've created 3 View structures:
one for viewing the 3D model (called ScenekitView)
one for a CardView (called MyCardView)
one for the scrollable list of 3D models(called MyCardsListView)
Here are the Structures:
ScenekitView
(btw I added the art.scnassets folder of Xcode's default Game project to my Single View App project)
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
#Binding var yRotation : Float
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.eulerAngles = SCNVector3(x: 0, y: self.yRotation * Float((Double.pi)/180.0) , z: 0)
// 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(yRotation: .constant(0))
}
}
#endif
MyCardView
import SwiftUI
struct MyCardView : View {
#Binding var yRotation: Float
var body: some View {
VStack(spacing: 0) {
ZStack {
Rectangle()
.foregroundColor(.black)
.opacity(0.8)
.frame(width:250, height: 70)
Text( "Ship \( self.yRotation )" )
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
}
ScenekitView(yRotation: self.$yRotation )
.frame(width: 250, height: 250)
}
.cornerRadius(35)
.frame(width: 250,height: 320)
}
}
#if DEBUG
struct MyCardView_Previews : PreviewProvider {
static var previews: some View {
MyCardView(yRotation: .constant(90))
}
}
#endif
MyCardsListView
import SwiftUI
struct MyCardsListView: View {
#State var yRotation: Float = 0
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(0...2) { number in
GeometryReader { geometry in
MyCardView(yRotation: self.$yRotation )
.rotation3DEffect(Angle(degrees: Double(geometry.frame(in: .global).minX) / -20), axis: (x: 0, y: 10, z: 0))
}
.padding()
.frame(width: 250, height: 350)
}
}
}
}
}
#if DEBUG
struct Product3DView_Previews: PreviewProvider {
static var previews: some View {
MyCardsListView()
}
}
#endif
In MyCardsListView I'm getting each Card's minimum X position using GeometryReader
My goal is:
while I'm scrolling the Cards in MyCardsListView, every ship model should rotate with respect to its Card's minimum X position (all of them shouldn't rotate with same rotation angle at the same time)
(I know I might made lots of mistakes in using Bindings, States and etc, Sorry about that)
There are many problems in your code:
You never assign a value to yRotation.
You only have one yRotation variable in MyCardsListView, but because you want to have different rotation values, depending on the view's position, you need one yRotation variable for each view.
The following line:
ship.eulerAngles = SCNVector3(x: 0, y: self.yRotation * Float((Double.pi)/180.0) , z: 0)
is inside the makeUIView() method, but that method is only executed once (when your view is created). You want the value to update continously, so you should assign the value in updateUIView() instead.
You should definitely watch WWDC sessions 226: Data Flow in SwiftUI and 231: Integrating SwiftUI.

SwiftUI - how to add a Scenekit Scene

How can I add a Scenekit Scene to a SwiftUI view?
I tried the following Hello World, using the standard Ship Scene example...
import SwiftUI
import SceneKit
struct SwiftUIView : View {
var body: some View {
ship()
Text("hello World")
}
But it didn't work:
Rawbee's answer does the job if you are creating a SwiftUIView inside a Game project (the default game project that Xcode will create for you)
But if you are in a Single View App project, you can create the same SceneView like this:
First
drag the art.scnassets folder (which contains 2 files: ship.scn and texture.png) from your game project to your Single View App project.
Second
In your Single View App project create a new SwiftUI file — I called it: ScenekitView. This is a structure which conforms to the UIViewRepresentable protocol
Third
Copy and paste the code below to this file and turn on live preview mode
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
I'm not a pro but this code worked for me (Xcode 11 beta 4)
You don't need use UIViewRepresentable anymore. Here's an update code for SwiftUI
import SwiftUI
import SceneKit
struct ContentView: View {
var scene: SCNScene? {
SCNScene(named: "Models.scnassets/Avatar.scn")
}
var cameraNode: SCNNode? {
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 2)
return cameraNode
}
var body: some View {
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [
.allowsCameraControl,
.autoenablesDefaultLighting,
.temporalAntialiasingEnabled
]
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In order for this to work, your SwiftUI View must conform to UIViewRepresentable. There's more info about that in Apple's tutorial: Interfacing with UIKit.
import SwiftUI
struct SwiftUIView : UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
return UIStoryboard(name: "Main", bundle: Bundle.main).instantiateInitialViewController()!.view
}
func updateUIView(_ view: UIView, context: Context) {
}
}
#if DEBUG
struct SwiftUIView_Previews : PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
#endif
Note that you'll have to turn on live preview to see it working.

when dynamic body falls down, its `_ball.position.y` value doesn't change

I have pretty simple question -
func setupScene() {
let sceneView = view as SCNView
sceneView.backgroundColor = SKColor.blackColor()
_scene = SCNScene()
sceneView.scene = _scene
sceneView.showsStatistics = true
// Scene gravity
sceneView.scene?.physicsWorld.gravity = SCNVector3Make(0, -70, 0)
sceneView.scene?.physicsWorld.speed = 2.0
sceneView.delegate = self
}
At some point of time have spawnBall method:
func spawnBall() {
_ball = SCNNode(geometry: SCNSphere(radius: 15))
_ball.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "ball")
_ball.geometry?.firstMaterial?.emission.contents = UIImage(named: "ball")
_ball.geometry?.firstMaterial?.emission.intensity = 0
_ball.physicsBody = SCNPhysicsBody(type: SCNPhysicsBodyType.Dynamic, shape: nil)
_ball.physicsBody?.restitution = 0.1
_ball.physicsBody?.angularVelocity = SCNVector4(x: 1, y: 1, z: 1, w: 1)
let position = SCNVector3Make(0, 15*10, -200)
_ball.position = position
_scene.rootNode.addChildNode(_ball)
}
The question is - why when dynamic body falls down, the it's _ball.position.y value doesn't change?
I used to UIKit when you move UIView - it's origin changes (I was assuming to see something similar, coz I set _ball.position.y = 150 and it should go down)
And how to observe it's movement?
the position of its presentationNode will change. The model values are left untouched by the physics engine.

Resources