Has anybody got the SCNMorpher to work? If so, what exactly have you done?
I'm down now to a small test program, that should show a red cone and when I tap on the screen it should (should!) morphe that into a sphere/ball. But the only thing that happens is, that the first geometry goes away (get very small) and the second one (Morphers first target) never appears.
I must miss something very simple. Can anybody get me on the horse, please?
import SceneKit
private let DISTANCE_FAKTOR = CGFloat(sqrt(3.0)/2.0)
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let (scene, view) = configScene(self.view.frame)
self.view = view
// Position camera to see picture full screen and light at same position
let pov = SCNVector3(x: 0.0, y: 0.0, z: Float(max(view.frame.width, view.frame.height) * DISTANCE_FAKTOR))
let pol = SCNVector3(x: 0.0, y: 0.0, z: Float(max(view.frame.width, view.frame.height) * DISTANCE_FAKTOR))
scene.rootNode.addChildNode(makeShapes()) // Create and add background plane
scene.rootNode.addChildNode(makeCamera(pov)) // Add camera to scene
scene.rootNode.addChildNode(makeLight(pol)) // Add light to scene
// add a tap gesture recognizer
view.gestureRecognizers = [UITapGestureRecognizer(target: self, action: "handleTap:")]
}
func handleTap(gestureRecognize: UIGestureRecognizer) {
NSLog("Tapped")
if let effectNode = (view as? SCNView)?.scene?.rootNode.childNodeWithName("EFFECT", recursively: true) {
NSLog("Animating")
animate(effectNode)
}
}
func makeShapes() -> SCNNode {
// First shape is a cone
let torus = SCNCone(topRadius: 0.0, bottomRadius: view.frame.width/2.0, height: view.frame.width/4.0)
torus.firstMaterial = SCNMaterial()
torus.firstMaterial?.diffuse.contents = UIColor.redColor()
// Now an additional sphere/ball
let sphere = SCNSphere(radius: view.frame.width/2.0)
sphere.firstMaterial = SCNMaterial()
sphere.firstMaterial?.diffuse.contents = UIColor.greenColor()
// Put all in the node
let node = SCNNode()
node.geometry = torus
node.morpher = SCNMorpher()
node.morpher?.targets = [sphere]
node.name = "EFFECT"
// I would expect now something between a ball and a torus in red/green
node.morpher?.setWeight(0.5, forTargetAtIndex: 0)
return node
}
func animate(node: SCNNode) {
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(5.0)
node.morpher?.setWeight(1.0, forTargetAtIndex: 0) // From torus to ball
SCNTransaction.setCompletionBlock {
NSLog("Transaction completing")
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(2.5)
node.morpher?.setWeight(0.0, forTargetAtIndex: 0) // And back
SCNTransaction.commit()
}
SCNTransaction.commit()
}
func configScene(frame: CGRect) -> (scene: SCNScene, view: SCNView) {
let view = SCNView()
view.frame = frame
view.autoresizingMask = UIViewAutoresizing.allZeros
view.backgroundColor = UIColor.blueColor()
let scene = SCNScene()
view.scene = scene
return (scene, view)
}
func makeCamera(pov: SCNVector3) -> SCNNode {
let camera = SCNCamera()
camera.zFar = Double(pov.z)
let node = SCNNode()
node.position = pov
node.camera = camera
return node
}
func makeLight(pol: SCNVector3) -> SCNNode {
let light = SCNLight()
light.type = SCNLightTypeOmni
light.zFar = CGFloat(pol.z)
let node = SCNNode()
node.position = pol
node.light = light
return node
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
} else {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}
When I exchange the ball and the cone in the code, I can see the ball and it disappears when tapping the screen, but the cone never appears.
If you want to run this code, just create a new GameScene project in Xcode and copy this code into GameViewController.swift
Has anybody got the SCNMorpher to work?
Yes, I've gotten SCNMorpher to work between both custom geometry and geometry that has been loaded from a file.
The piece that you are missing is that all the morph targets need to have the same number of vertices and the vertices need to have the same arrangement. This is discussed in the documentation for the targets property:
The base geometry and all target geometries must be topologically identical — that is, they must contain the same number and structural arrangement of vertices.
Your example (morning between a sphere and a cone) doesn't fulfill this requirement and is not expected to work.
You can try and morph between two different spheres (with the same number of segments!) to see that it does work.
Related
I am trying to add child node to the parent node which have it's own camera. on tap of the user on scene, using hit test I am adding child node (i.e plane node) to the the parent node (sphere node). But I don't know why the child node is not facing properly to the camera. it's looking different on different location. I wanted it to fix looking at the camera.
Code :
// Creating sphere node and adding camera to it
func setup() {
let sphere = SCNSphere(radius: 10)
sphere.segmentCount = 360
let material = getTextureMaterial()
material.diffuse.contents = UIImage(named: "sample")
sphere.firstMaterial = material
let sphereNode = SCNNode()
sphereNode.geometry = sphere
sceneView.scene?.rootNode.addChildNode(sphereNode)
}
//Creating texture material to show 360 degree image...
func getTextureMaterial() -> SCNMaterial {
let material = SCNMaterial()
material.diffuse.mipFilter = .nearest
material.diffuse.magnificationFilter = .linear
material.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1)
material.diffuse.wrapS = .repeat
material.cullMode = .front
material.isDoubleSided = true
return material
}
#objc func handleTap(rec: UITapGestureRecognizer){
if rec.state == .ended {
let location: CGPoint = rec.location(in: sceneView)
let hits = sceneView.hitTest(location, options: nil)
if !hits.isEmpty {
let result: SCNHitTestResult = hits[0]
createPlaneNode(result: result)
}
}
}
func createPlaneNode(result : SCNHitTestResult) {
let plane = SCNPlane(width: 5, height: 5)
plane.firstMaterial?.diffuse.contents = UIColor.red
plane.firstMaterial?.isDoubleSided = true
let planeNode = SCNNode()
planeNode.name = "B"
planeNode.geometry = plane
planeNode.position = result.worldCoordinates
result.node.addChildNode(planeNode)
}
Results I am getting using above code:
Added Plane node is looking weird
I don't know if you ever found your answer but, it would be done freeing the node axes with SCNBillboardConstraint :)
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = [.all]
node.constraints = [billboardConstraint]
I have an odd problem, or maybe what I am doing is odd.
I have an ARKit app that displays a SCNNode with a data view embedded when a barcode is detected. When the data view is tapped I want to detach it from the node and add it as a subview to the main view. When tapped again I want to re attach it to the SCNNode.
I found this code that does at least part of what I want.
My app works well the first time. The data appears in the SCNNode:
{
let transform = anchor.transform
let label = SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil)
let plane = SCNPlane(width: self.sceneView.bounds.width/2000, height: self.sceneView.bounds.height/2000/2) //was sceneView
label.update(dataItem.title!, andValue: dataItem.value!, withTrend: dataItem.trend)
label.delegate = self
plane.firstMaterial?.blendMode = .alpha
plane.firstMaterial?.diffuse.contents = label.view //self.contentController?.view
let planeNode = SCNNode(geometry: plane)
planeNode.opacity = 0.75
planeNode.simdPosition = float3(transform.columns.3.x + ( base * count), 0, transform.columns.3.z)
node.addChildNode(planeNode)
childControllers[label] = planeNode
label.myNode = planeNode
label.parentNode = node
if("Speed" == anchorLabels[anchor.identifier]!) {
let constraint = SCNBillboardConstraint()
planeNode.constraints = [constraint]
}
count += 1
}
SimpleValueViewController has a tap recognizer that toggles between being a subview and being inside the node.
Here is the problem.
It comes up fine. The view is inside the node.
When tapped it gets attached to the main view.
When tapped again the node just shows a white square and taps no longer work.
I finally gave up and recreate a new viewController, but it still occasionally fails with only a white square:
Here is the code to detach/attach It is pretty messy as I have been pounding on it for a while:
func detach(_ vc:Detachable) {
if let controller = vc as? SimpleValueViewController {
controller.view.removeFromSuperview();
self.childControllers.removeValue(forKey: controller)
let node = controller.myNode
let parent = controller.parentNode
let newController = SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil)
if let v = newController.view {
print("View: \(v)")
newController.myNode = node
newController.parentNode = parent
newController.update(controller.titleString!, andValue: controller.valueString!, withTrend: controller.trendString!)
newController.delegate = self
newController.scaley = vc.scaley
newController.scalex = vc.scalex
newController.refreshView()
let plane = node?.geometry as! SCNPlane
plane.firstMaterial?.blendMode = .alpha
plane.firstMaterial?.diffuse.contents = v
// newController.viewWillAppear(false)
newController.parentNode?.addChildNode(newController.myNode!)
newController.attached = false;
self.childControllers[newController] = node
}
} else {
print("Not a know type of controller")
}
}
func attach(_ vc:Detachable) {
DispatchQueue.main.async {
if let controller = vc as? SimpleValueViewController {
self.childControllers.removeValue(forKey: controller)
let newController = SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil)
let node = controller.myNode
let parent = controller.parentNode
let scaleX = vc.scalex!/self.view.frame.size.width
let scaleY = vc.scaley!/self.view.frame.size.height
let scale = min(scaleX, scaleY)
let transform = CGAffineTransform(scaleX: scale, y: scale)
newController.view.transform = transform
newController.myNode = node
newController.parentNode = parent
newController.update(controller.titleString!, andValue: controller.valueString!, withTrend: controller.trendString!)
newController.delegate = self
newController.scaley = vc.scaley
newController.scalex = vc.scalex
node?.removeFromParentNode()
self.childControllers[newController] = node
newController.attached = true
self.view.addSubview(newController.view)
} else {
print("Not a know type of controller to attach")
}
}
}
I was thinking it was a timing issue as loading a view from a nib is done lazily so I put in a sleep for 1 second after the SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil) but that did not help.
Anything else I can try? I am way off base? I would like not to have to recreate the view controller every time the data is toggled.
Thanks in Advance.
Render functions:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
// updateQueue.async {
//Loop through the near and far data and add stuff
let data = "Speed" == anchorLabels[anchor.identifier]! ? nearData : farData
let base = "Speed" == anchorLabels[anchor.identifier]! ? 0.27 : 0.2 as Float
print ("Getting Node for: " + self.anchorLabels[anchor.identifier]!)
var count = 0.0 as Float
for dataItem in data {
let transform = anchor.transform
let label = SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil)
let plane = SCNPlane(width: self.sceneView.bounds.width/2000, height: self.sceneView.bounds.height/2000/2) //was sceneView
label.update(dataItem.title!, andValue: dataItem.value!, withTrend: dataItem.trend)
label.delegate = self
plane.firstMaterial?.blendMode = .alpha
plane.firstMaterial?.diffuse.contents = label.view //self.contentController?.view
let planeNode = SCNNode(geometry: plane)
planeNode.opacity = 0.75
planeNode.simdPosition = float3(transform.columns.3.x + ( base * count), 0, transform.columns.3.z)
node.addChildNode(planeNode)
childControllers[label] = planeNode
label.myNode = planeNode
label.parentNode = node
if("Speed" == anchorLabels[anchor.identifier]!) {
let constraint = SCNBillboardConstraint()
planeNode.constraints = [constraint]
}
count+=1
}
and update
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard currentBuffer == nil else {
return
}
self.updateFocusSquare(isObjectVisible: false)
// Retain the image buffer for Vision processing.
self.currentBuffer = sceneView.session.currentFrame?.capturedImage
classifyCurrentImage()
}
ClassifyCurrentImage is where the barcode recognizer lives. If a barcode is found it adds an anchor at the spot in the scene.
how can I make a grid with 3d objects(box). I already know how to setup scnscene and how to create an object. But I don't know how to make the layout. The grid should look like this one, with 3d object in a 3D space.
Here's what I tried:
convenience init(create: Bool) {
self.init()
let geometry = SCNBox(width: 0.8 , height: 0.8,
length: 0.1, chamferRadius: 0.005)
geometry.firstMaterial?.diffuse.contents = UIColor.red
geometry.firstMaterial?.specular.contents = UIColor.white
geometry.firstMaterial?.emission.contents = UIColor.blue
let offset: Int = 10
for xIndex:Int in 0...2 {
for yIndex:Int in 0...2 {
// create a geometry copy
let geoCopy = geometry.copy() as! SCNGeometry
var images:[UIImage]=[]
for i in 1...5 {
if let img = UIImage(named: "\(i)"){
images.append(img)
let material = SCNMaterial()
material.diffuse.contents = img
geoCopy.firstMaterial = material
}
}
let boxnode = SCNNode(geometry: geoCopy)
let boxCopy = boxnode.copy() as! SCNNode
boxCopy.position.x = Float(xIndex - offset)
boxCopy.position.y = Float(yIndex - offset)
self.rootNode.addChildNode(boxCopy)
}
}
}
But I only see one box.
Thanks!
Picture of my Images:
You need to create one geometry, one box node and then copy that boxNode. You use clone when you have node with children and flattenedClone when you want to merge geometries/materials of the entire subtree at the node. In your case, copy should suffice. Just change the position of your copied node.
GameScene
import Foundation
import SceneKit
class GameScene: SCNScene {
override init() {
super.init()
let geometry = SCNBox(width: 0.6 , height: 0.6,
length: 0.1, chamferRadius: 0.005)
geometry.firstMaterial?.diffuse.contents = UIColor.red
geometry.firstMaterial?.specular.contents = UIColor.white
geometry.firstMaterial?.emission.contents = UIColor.blue
let boxnode = SCNNode(geometry: geometry)
let offset: Int = 16
for xIndex:Int in 0...32 {
for yIndex:Int in 0...32 {
let boxCopy = boxnode.copy() as! SCNNode
boxCopy.position.x = Float(xIndex - offset)
boxCopy.position.y = Float(yIndex - offset)
self.rootNode.addChildNode(boxCopy)
}
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
In your view controller, viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = GameScene()
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
scnView.pointOfView?.position = SCNVector3Make(0, 0, 100)
// 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.white
}
Note that I have just pushed the camera point of view back on the +Z axis to have a better view of your grid.
The grid screenshot
Edit: New material for each geometry
If you want to assign a new material to each geometry, you need to create a copy of the geometry and assign a new material to that geometry copy. See the code below which randomly assign a UIImage for each diffuse property, from a set of seven images named 1.png to 8.png.
import Foundation
import SceneKit
class GameScene: SCNScene {
override init() {
super.init()
let geometry = SCNBox(width: 6 , height: 6,
length: 6, chamferRadius: 0.5)
for xIndex:Int in stride(from: 0, to: 32, by:8) {
for yIndex:Int in stride(from: 0, to: 32, by: 8) {
// create a geometry copy
let geoCopy = geometry.copy() as! SCNGeometry
// create a random material
let r = arc4random_uniform(7) + 1
let img = UIImage(named: "\(r).png")
let mat = SCNMaterial()
mat.diffuse.contents = img
geoCopy.firstMaterial = mat
// create a copy node with new material and geo copy
let boxnode = SCNNode(geometry: geoCopy)
let boxCopy = boxnode.copy() as! SCNNode
boxCopy.position.x = Float(xIndex - offset)
boxCopy.position.y = Float(yIndex - offset)
self.rootNode.addChildNode(boxCopy)
}
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Screenshot
You need to put the statement -let boxnode = SCNNode(geometry: self.geometry) - inside the loop. If you wish to use the same materials, you can use the same geometry for all nodes (just store the geometry in a variable and assign it). Otherwise, if you wish to have different materials, copy the geometry and assign different materials each time.
I have been working on coin like UIView, which has 3d rotation.
I was able to implement rotate effect with CATransform3dRotate, based on the gesture. Now I need to implement shadow like effect, when image rotates.
Please refer to the image attached herewith.
Any suggestions? recommendations? or sample code would be great
Sample Requirement
You might want to look at SceneKit. I've put together a quick demo, which you can download here, of a 3D coin object which rotates horizontally left or right by a swipe gesture, with a realistic light source and shadowing. The relevant code follows:
import UIKit
import SceneKit
class ViewController: UIViewController {
#IBOutlet weak var sceneView: SCNView!
let rotate90AboutZ = SCNAction.rotateByX(0.0, y: 0.0, z: CGFloat(M_PI_2), duration: 0.0)
var currentAngle: Float = 0.0
var coinNode = SCNNode()
override func viewDidLoad() {
super.viewDidLoad()
sceneView.userInteractionEnabled = true
sceneView.backgroundColor = UIColor.blackColor()
sceneView.autoenablesDefaultLighting = true
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
coinSceneSetup()
}
func coinSceneSetup() {
let coinScene = SCNScene()
//coin is rendered as a cylinder with a very small height
let coinGeometry = SCNCylinder(radius: 50, height: 2)
coinNode = SCNNode(geometry: coinGeometry)
coinNode.position = SCNVector3Make(0.0, 25.0, 25.0)
coinScene.rootNode.addChildNode(coinNode)
//rotate coin 90 degrees about the z axis so that it stands upright
coinNode.runAction(rotate90AboutZ)
let shinyCoinMaterial = SCNMaterial()
shinyCoinMaterial.diffuse.contents = UIColor.lightGrayColor()
shinyCoinMaterial.specular.contents = UIColor.whiteColor()
shinyCoinMaterial.shininess = 1.0
coinGeometry.firstMaterial = shinyCoinMaterial
sceneView.scene = coinScene
let panRecognizer = UIPanGestureRecognizer(target: self, action: "panGesture:")
sceneView.addGestureRecognizer(panRecognizer)
}
//allows for coin to spin with a right or left finger swipe, while still keeping it rotated 90 degrees about z axis
func panGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view!)
var newAngle = (Float)(translation.x)*(Float)(M_PI)/180.0
newAngle += currentAngle
coinNode.runAction(SCNAction.rotateToX(CGFloat(newAngle), y: 0.0, z: CGFloat(M_PI_2), duration: 0.0))
if(sender.state == UIGestureRecognizerState.Ended) {
currentAngle = newAngle
}
}
}
Several excellent introductory tutorials are available, for example here, here, and here. weheartswift.com put out a three part tutorial in Swift that I personally find especially useful. Here are parts 1, 2 and 3.
Jeff Lamarche's introduction to SceneKit on OS X, along with his accompanying source code is a valuable resource as well.
I've created a new Xcode game project and selected SpriteKit. By default, Xcode provides a game template which I modified to add image. The image was displayed on the screen but in incorrect position. I followed this tutorial to setup the correct scene dimension and it works. But the image is still in incorrect position.
I'm trying to position the image at Y=0, but it appears at the bottom of the screen instead of the top.
Here's what I've done so far.
ViewController Code:
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews();
let skView = self.view as! SKView
println(skView)
let scale:CGFloat = UIScreen.mainScreen().scale;
let size = CGSizeMake(skView.frame.size.width*scale, skView.frame.size.height*scale)
if (skView.scene == nil) {
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
skView.showsFPS = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = true
scene.scaleMode = .AspectFill
scene.size = size
skView.presentScene(scene)
}
}
else {
skView.scene!.size = size;
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
} else {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
}
Scene Code:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
println(self)
self.backgroundColor = UIColor(red: 31.0/255.0, green: 31.0/255.0, blue: 31.0/255.0, alpha: 1.0)
//Add the app logo
let logo = SKSpriteNode(imageNamed:"imgLogo")
logo.position = CGPoint(x:self.frame.size.width/2, y:0)
self.addChild(logo)
let lblTitle = SKLabelNode(fontNamed:"LibelSuit-Regular")
lblTitle.text = "Hello World!";
lblTitle.fontSize = 65;
lblTitle.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));
self.addChild(lblTitle)
}
}
I assume logo is the red/green boxes on the bottom? If so, y = 0 is the bottom edge, not the top. Try:
logo.position = CGPoint(x:self.frame.size.width/2,
y:self.frame.size.height - logo.size.height / 2)
Unlike game development for other platforms, the y-axis in spritekit runs from down to up instead of up to down. In this case you'd want to replace the 0 with self.frame.size.height
logo.position = CGPoint(x:self.frame.size.width/2, y:0) //Your current code
logo.position = CGPoint(x:self.frame.size.width/2, y:self.frame.size.height)//What you want
Since the Scene anchor point was set to (0,0) - lower left of the screen by default, what I did is to set the anchor point to (0,1) - upper left instead.
self.anchorPoint = CGPointMake(0, 1);
I also set the same anchor point for each child, then I set the Y position to a negative value.
let logo = SKSpriteNode(imageNamed:"imgLogo")
logo.anchorPoint = CGPointMake(0, 1);
logo.position = CGPoint(x:0, y:-180)
self.addChild(logo)
This works perfectly. Appreciate your inputs.
Why would y = 0 be at the top? Just think of it as a graph like the ones you've learned in school. y = 0 is in the bottom and x = 0 is on the left side. So 0,0 would be on the left bottom corner.