How can I combine SceneKit with existing OpenGL using SCNRenderer? - ios

I would like to use SceneKit’s SCNRenderer to draw a few models into my existing GLKit OpenGL ES 3.0 application and match my existing modelViewProjection transform. When I load a SceneKit scene and render it using SCNRenderer the SceneKit camera transformations seem to be ignored and I just get a default bounding box view. More generally of course I would prefer to supply my own matrix and I am not sure how to do that.
//
// Called from my glkView(view: GLKView, drawInRect rect: CGRect)
//
// Load the scene
let scene = SCNScene(named: "art.scnassets/model.scn")!
// Create and add regular SceneKit camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
scene.rootNode.addChildNode(cameraNode)
// Render into the current OpenGL context
let renderer = SCNRenderer( context: EAGLContext.currentContext(), options: nil )
renderer.scene = scene
renderer.renderAtTime(0)
I see that there is a projectionTransform exposed on the SCN Camera and I have tried manipulating that as well, but with no results. I’m also guessing that that is just literally a projection transform and not expecting the full modelViewProjection transform.
// Probably WRONG
cameraNode.camera!.projectionTransform = SCNMatrix4FromGLKMatrix4( modelViewProjectionTransform )
Does anyone have an example of mixing SceneKit into OpenGL drawing in this way? Thanks.
EDIT:
Bobelyuk's reference to the example is on point and has helped me solve half of my problem so far. It turns out that although I was adding the camera node I had failed to set the camera node as the 'pointOfView':
scene.pointOfView = cameraNode
The example appears to show how to take an opengl matrix and invert it to set it on the camera, however as of yet I have not been able to get this to work with my code. I will update again with a code example shortly.

http://qiita.com/akira108/items/a743138fca532ee193fe here you can find how to combine SCNRenderer and OpenGLContext

Ultimately it turned out to be very simple. In addition to setting the pointOfView to the camera node I just needed to properly set the camera transform and camera projection transform to my own matrices.
Here is the updated example:
//
// Called from my glkView(view: GLKView, drawInRect rect: CGRect)
//
// Load the scene
let scene = SCNScene(named: "art.scnassets/model.scn")!
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// Set my own matrices
cameraNode.transform = SCNMatrix4Invert(SCNMatrix4FromGLKMatrix4(myModelViewMatrix))
cameraNode.camera!.projectionTransform = SCNMatrix4FromGLKMatrix4(myProjectionMatrix)
// Render into the current OpenGL context
let renderer = SCNRenderer( context: EAGLContext.currentContext(), options: nil )
renderer.scene = scene
renderer.pointOfView = cameraNode // set the point of view for the scene
renderer.renderAtTime(0)
For thos new to this: The model view matrix is the matrix that orients your scene, e.g. composing GLKMatrix4Scale and GLKMatrix4Rotate calls. The projection matrix is the one that sets your view frustrum and point of view, e.g. by composing calls to GLKMatrix4MakePerspective and GLKMatrix4MakeLookAt calls.

Related

Limit Scene Kit Camera Rotation around a single (y) axis in iOS

So I have a sphere in scene kit and currently, I am orbiting the sphere with the default .allowsCameraControl = true. I want to limit the orbit of the camera to only orbit the Y-axis (basically it will look like a globe spinning).
I have tried messing around with spinning the actual sphere with pan gestures, but that seemed to complicate things rather quickly and I would like to keep the default physics and gestures involved with .allowsCameraControl = true if at all possible.
Below is what I have currently. Thanks!
private func createSceneHelpers() {
scnView.allowsCameraControl = true
}
private func createCamera() {
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
myScene.rootNode.addChildNode(cameraNode)
}
You can take a look at the SCNCameraController class and its maximumVerticalAngle property. The SCNInteractionModeOrbitTurntable interaction mode is also of interest. Note that if the online documentation is lacking, the documentation in SceneKit's header file as more often up-to-date.

How to scale and position a SCNNode that has a SCNSkinner attached?

Using SceneKit, I'm loading a very simple .dae file consisting of a large cylinder with three associated bones. I want to scale the cylinder down and position it on the ground. Here's the code
public class MyNode: SCNNode {
public convenience init() {
self.init()
let scene = SCNScene(named: "test.dae")
let cylinder = (scene?.rootNode.childNode(withName: "Cylinder", recursively: true))!
let scale: Float = 0.1
cylinder.scale = SCNVector3Make(scale, scale, scale)
cylinder.position = SCNVector3(0, scale, 0)
self.addChildNode(cylinder)
}
}
This doesn't work; the cylinder is still huge when I view it. The only way I can get the code to work is to remove associated SCNSKinner.
cylinder.skinner = nil
Why does this happen and how can I properly scale and position the model, bones and all?
when a geometry is skinned it is driven by its skeleton. Which means that the transform of the skinned node is no longer used, it's the transforms of the bones that are important.
For this file Armature is the root of the skeleton. If you translate/scale this node instead of Cylinder you'll get what you want.

iOS ARKit - move objects along path

I am trying to move implement bouncing balls using ARKit. I want the balls coming from one end of screen bouncing and moving out of screen.
Can anyone please recommend best approach or point to sample code to implement this?
Can I use UIBezierPath to create a path and move SCNNode along the path. If yes, how can I move the node along path.
Create a scene and a ball Node. Use force direction for ball Node and animate the ball Node with Physics. This is just an example.
// Create a new scene
guard let scene = SCNScene(named: "BouncingBalls.scn", inDirectory: "art.scnassets") else { return }
sceneView.scene = scene
// Add physics bodies
guard let ballsNode = scene.rootNode.childNode(withName: "balls", recursively: true) else { return }
let forceDirection = SCNVector3Make(0, 3, 0)
for ballNode in ballsNode.childNodes {
let physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
physicsBody.applyForce(forceDirection, asImpulse: true)
ballNode.physicsBody = physicsBody
}
Try this in View didLoad. Hope this works.
The best way to get what you want is to use a predefined animation or dynamics that was made in such 3D apps as Autodesk Maya, Autodesk 3dsMax, or Maxon Cinema4D.
After producing a ball animation (or dynamics, it's up to you) you need to bake this animation (baking is a process of keyframe generation for every frame instead of using interpolated curve) and export this scene as a .dae file format. ARKit/SceneKit supports not only .dae animations but also brand-new format .usdz.
SceneKit API (as well as other Apple frameworks' APIs) is not designed for 3D animation.

Add shape to sphere surface in SceneKit

I'd like to be able to add shapes to the surface of a sphere using SceneKit. I started with a simple example where I'm just trying to color a portion of the sphere's surface another color. I'd like this to be an object that can be tapped, selected, etc... so my thought was to add shapes as SCNNodes using custom SCNShape objects for the geometry.
What I have now is a blue square that I'm drawing from a series of points and adding to the scene containing a red sphere. It basically ends up tangent to a point on the sphere, but the real goal is to draw it on the surface. Is there anything in SceneKit that will allow me to do this? Do I need to do some math/geometry to make it the same shape as the sphere or map to a sphere's coordinates? Is what I'm trying to do outside the scope of SceneKit?
If this question is way too broad I'd be glad if anyone could point me towards books or resources to learn what I'm missing. I'm totally new to SceneKit and 3D in general, just having fun playing around with some ideas.
Here's some playground code for what I have now:
import UIKit
import SceneKit
import XCPlayground
class SceneViewController: UIViewController {
let sceneView = SCNView()
private lazy var sphere: SCNSphere = {
let sphere = SCNSphere(radius: 100.0)
sphere.materials = [self.surfaceMaterial]
return sphere
}()
private lazy var testScene: SCNScene = {
let scene = SCNScene()
let sphereNode: SCNNode = SCNNode(geometry: self.sphere)
sphereNode.addChildNode(self.blueChildNode)
scene.rootNode.addChildNode(sphereNode)
//scene.rootNode.addChildNode(self.blueChildNode)
return scene
}()
private lazy var surfaceMaterial: SCNMaterial = {
let material = SCNMaterial()
material.diffuse.contents = UIColor.redColor()
material.specular.contents = UIColor(white: 0.6, alpha: 1.0)
material.shininess = 0.3
return material
}()
private lazy var blueChildNode: SCNNode = {
let node: SCNNode = SCNNode(geometry: self.blueGeometry)
node.position = SCNVector3(0, 0, 100)
return node
}()
private lazy var blueGeometry: SCNShape = {
let points: [CGPoint] = [
CGPointMake(0, 0),
CGPointMake(50, 0),
CGPointMake(50, 50),
CGPointMake(0, 50),
CGPointMake(0, 0)]
var pathRef: CGMutablePathRef = CGPathCreateMutable()
CGPathAddLines(pathRef, nil, points, points.count)
let bezierPath: UIBezierPath = UIBezierPath(CGPath: pathRef)
let shape = SCNShape(path: bezierPath, extrusionDepth: 1)
shape.materials = [self.blueNodeMaterial]
return shape
}()
private lazy var blueNodeMaterial: SCNMaterial = {
let material = SCNMaterial()
material.diffuse.contents = UIColor.blueColor()
return material
}()
override func viewDidLoad() {
super.viewDidLoad()
sceneView.frame = self.view.bounds
sceneView.backgroundColor = UIColor.blackColor()
self.view.addSubview(sceneView)
sceneView.autoenablesDefaultLighting = true
sceneView.allowsCameraControl = true
sceneView.scene = testScene
}
}
XCPShowView("SceneKit", view: SceneViewController().view)
If you want to map 2D content into the surface of a 3D SceneKit object, and have the 2D content be dynamic/interactive, one of the easiest solutions is to use SpriteKit for the 2D content. You can set your sphere's diffuse contents to an SKScene, and create/position/decorate SpriteKit nodes in that scene to arrange them on the face of the sphere.
If you want to have this content respond to tap events... Using hitTest in your SceneKit view gets you a SCNHitTestResult, and from that you can get texture coordinates for the hit point on the sphere. From texture coordinates you can convert to SKScene coordinates and spawn nodes, run actions, or whatever.
For further details, your best bet is probably Apple's SceneKitReel sample code project. This is the demo that introduced SceneKit for iOS at WWDC14. There's a "slide" in that demo where paint globs fly from the camera at a spinning torus and leave paint splashes where they hit it — the torus has a SpriteKit scene as its material, and the trick for leaving splashes on collisions is basically the same hit test -> texture coordinate -> SpriteKit coordinate approach outlined above.
David Rönnqvist's SceneKit book (available as an iBook) has an example (the EarthView example, a talking globe, chapter 5) that is worth looking at. That example constructs a 3D pushpin, which is then attached to the surface of a globe at the location of a tap.
Your problem is more complicated because you're constructing a shape that covers a segment of the sphere. Your "square" is really a spherical trapezium, a segment of the sphere bounded by four great circle arcs. I can see three possible approaches, depending on what you're ultimately looking for.
The simplest way to do it is to use an image as the material for the sphere's surface. That approach is well illustrated in the Ronnqvist EarthView example, which uses several images to show the earth's surface. Instead of drawing continents, you'd draw your square. This approach isn't suitable for interactivity, though. Look at SCNMaterial.
Another approach would be to use hit test results. That's documented on SCNSceneRenderer (which SCNView complies with) and SCNHitTest. Using the hit test results, you could pull out the face that was tapped, and then its geometry elements. This won't get you all the way home, though, because SceneKit uses triangles for SCNSphere, and you're looking for quads. You will also be limited to squares that line up with SceneKit's underlying wireframe representation.
If you want full control of where the "square" is drawn, including varying its angle relative to the equator, I think you'll have to build your own geometry from scratch. That means calculating the latitude/longitude of each corner point, then generating arcs between those points, then calculating a bunch of intermediate points along the arcs. You'll have to add a fudge factor, to raise the intermediate points slightly above the sphere's surface, and build up your own quads or triangle strips. Classes here are SCNGeometry, SCNGeometryElement, and SCNGeometrySource.

SceneKit get texture coordinate after touch with Swift

I want to manipulate 2D textures in a 3D SceneKit scene.
Therefore i used this code to get local coordinates:
#IBAction func tap(sender: UITapGestureRecognizer) {
var arr:NSArray = my3dView.hitTest(sender.locationInView(my3dView), options: NSDictionary(dictionary: [SCNHitTestFirstFoundOnlyKey:true]))
var res:SCNHitTestResult = arr.firstObject as SCNHitTestResult
var vect:SCNVector3 = res.localCoordinates}
I have the texture read out from my scene with:
var mat:SCNNode = myscene.rootNode.childNodes[0] as SCNNode
var child:SCNNode = mat.childNodeWithName("ID12", recursively: false)
var geo:SCNMaterial = child.geometry.firstMaterial
var channel = geo.diffuse.mappingChannel
var textureimg:UIImage = geo.diffuse.contents as UIImage
and now i want to draw at the touchpoint to the texture...
how can i do that? how can i transform my coordinate from touch to the texture image?
Sounds like you have two problems. (Without even having used regular expressions. :))
First, you need to get the texture coordinates of the tapped point -- that is, the point in 2D texture space on the surface of the object. You've almost got that right already. SCNHitTestResult provides those with the textureCoordinatesWithMappingChannel method. (You're using localCoordinates, which gets you a point in the 3D space owned by the node in the hit-test result.) And you already seem to have found the business about mapping channels, so you know what to pass to that method.
Problem #2 is how to draw.
You're doing the right thing to get the material's contents as a UIImage. Once you've got that, you could look into drawing with UIGraphics and CGContext functions -- create an image with UIGraphicsBeginImageContext, draw the existing image into it, then draw whatever new content you want to add at the tapped point. After that, you can get the image you were drawing with UIGraphicsGetImageFromCurrentImageContext and set it as the new diffuse.contents of your material. However, that's probably not the best way -- you're schlepping a bunch of image data around on the CPU, and the code is a bit unwieldy, too.
A better approach might be to take advantage of the integration between SceneKit and SpriteKit. This way, all your 2D drawing is happening in the same GPU context as the 3D drawing -- and the code's a bit simpler.
You can set your material's diffuse.contents to a SpriteKit scene. (To use the UIImage you currently have for that texture, just stick it on an SKSpriteNode that fills the scene.) Once you have the texture coordinates, you can add a sprite to the scene at that point.
var nodeToDrawOn: SCNNode!
var skScene: SKScene!
func mySetup() { // or viewDidLoad, or wherever you do setup
// whatever else you're doing for setup, plus:
// 1. remember which node we want to draw on
nodeToDrawOn = myScene.rootNode.childNodeWithName("ID12", recursively: true)
// 2. set up that node's texture as a SpriteKit scene
let currentImage = nodeToDrawOn.geometry!.firstMaterial!.diffuse.contents as UIImage
skScene = SKScene(size: currentImage.size)
nodeToDrawOn.geometry!.firstMaterial!.diffuse.contents = skScene
// 3. put the currentImage into a background sprite for the skScene
let background = SKSpriteNode(texture: SKTexture(image: currentImage))
background.position = CGPoint(x: skScene.frame.midX, y: skScene.frame.midY)
skScene.addChild(background)
}
#IBAction func tap(sender: UITapGestureRecognizer) {
let results = my3dView.hitTest(sender.locationInView(my3dView), options: [SCNHitTestFirstFoundOnlyKey: true]) as [SCNHitTestResult]
if let result = results.first {
if result.node === nodeToDrawOn {
// 1. get the texture coordinates
let channel = nodeToDrawOn.geometry!.firstMaterial!.diffuse.mappingChannel
let texcoord = result.textureCoordinatesWithMappingChannel(channel)
// 2. place a sprite there
let sprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSize(width: 10, height: 10))
// scale coords: texcoords go 0.0-1.0, skScene space is is pixels
sprite.position.x = texcoord.x * skScene.size.width
sprite.position.y = texcoord.y * skScene.size.height
skScene.addChild(sprite)
}
}
}
For more details on the SpriteKit approach (in Objective-C) see the SceneKit State of the Union Demo from WWDC14. That shows a SpriteKit scene used as the texture map for a torus, with spheres of paint getting thrown at it -- whenever a sphere collides with the torus, it gets a SCNHitTestResult and uses its texcoords to create a paint splatter in the SpriteKit scene.
Finally, some Swift style comments on your code (unrelated to the question and answer):
Use let instead of var wherever you don't need to reassign a value, and the optimizer will make your code go faster.
Explicit type annotations (res: SCNHitTestResult) are rarely necessary.
Swift dictionaries are bridged to NSDictionary, so you can pass them directly to an API that takes NSDictionary.
Casting to a Swift typed array (hitTest(...) as [SCNHitTestResult]) saves you from having to cast the contents.

Resources