SceneKit Cocoa snapshot failed assertion - swift

I am working on a Swift/Cocoa/Xcode application.
This application contains a SceneKit View. The rendering API is set to Default (I think this is Metal).
If I run a snapshot() on this SceneKit view object, I get this error message. What I want to do is to capture an UIImage of the scene, viewed from camera
Texture PixelFormat MTLPixelFormatBGRA8Unorm does not match Resolve PixelFormat MTLPixelFormatRGBA8Unorm
If I set the rendering API to OpenGL, I have no error, everything works.
I have tried the same thing on an iOS app, it works on both cases (Metal or OpenGL).
I do not understand why I get this error and what should I do to avoid it.
Here is sample code:
import SceneKit
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var vue_scene: SCNView!
#IBOutlet weak var img_snapshot: NSImageView!
let camera_node = SCNNode()
var box_node:SCNNode = SCNNode()
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
vue_scene.scene = scene
vue_scene.backgroundColor = NSColor.clear
vue_scene.showsStatistics = false
vue_scene.allowsCameraControl = false
vue_scene.autoenablesDefaultLighting = true
camera_node.camera = SCNCamera()
camera_node.camera?.zNear = 0.01
camera_node.camera?.zFar = 1000000.0
vue_scene.pointOfView = camera_node
vue_scene.scene!.rootNode.addChildNode(camera_node)
let box = SCNBox(width: 10.0,
height: 10.0,
length: 10.0,
chamferRadius: 0.0)
box.firstMaterial?.diffuse.contents = NSColor.red
box.firstMaterial?.isDoubleSided = true
box_node = SCNNode(geometry:box)
box_node.position = SCNVector3Make(0,0,0)
box_node.opacity = 1.0
vue_scene.scene!.rootNode.addChildNode(box_node)
camera_node.position = SCNVector3Make(0.0,
0.0,
70.0)
}
#IBAction func on_btn(_ sender: Any) {
// signal SIGABRT here:
// /Library/Caches/com.apple.xbs/Sources/Metal/Metal-56.6.1/ToolsLayers/Debug/MTLDebugCommandBuffer.mm:215: failed assertion `Texture PixelFormat MTLPixelFormatBGRA8Unorm does not match Resolve PixelFormat MTLPixelFormatRGBA8Unorm'
let image = vue_scene.snapshot()
img_snapshot.image = image;
}
}

It works on Metal.
In macOS use Tab View (for example) for accommodation of SceneView and NSImageView.
I also tested it in Xcode 13.3 on macOS 12.3 Monterey.
import SceneKit
import Cocoa
class ViewController: NSViewController {
#IBOutlet var sceneView: SCNView!
#IBOutlet var imageView: NSImageView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.scene = SCNScene()
sceneView.pointOfView?.position.z = 20
sceneView.allowsCameraControl = true
sceneView.showsStatistics = true
sceneView.backgroundColor = NSColor.black
sceneView.autoenablesDefaultLighting = true
let box = SCNBox(width: 1.0, height: 1.0,
length: 1.0, chamferRadius: 0.0)
box.firstMaterial?.diffuse.contents = NSColor.systemTeal
let boxNode = SCNNode(geometry: box)
boxNode.position = SCNVector3(0,0,-10)
sceneView.scene!.rootNode.addChildNode(boxNode)
}
#IBAction func createSnapshot(_ sender: NSButton) {
let image = sceneView.snapshot()
imageView.image = image
}
}

Related

Apply GIF to SCNPlane

Right now my Swift code creates a SCNPlane with an image called we.jpg. What I would like to do is to replace we.jpg with a ball.gif. material.diffuse.contents is where the UIImage is attached to the SCNPlane.
let planeGeometry = SCNPlane(width: 0.2, height: 0.35)
let material = SCNMaterial()
material.diffuse.contents = UIImage(named: "we.jpg")
planeGeometry.materials = [material]
You can easily replace SCNPlane's .jpg texture with .gif texture. But remember that in SceneKit there's no support for animated GIF textures.
Here's a code:
import ARKit
import SceneKit
class ViewController: UIViewController {
#IBOutlet var sceneView: ARSCNView! // in case you're using AR app
//#IBOutlet var sceneView: SCNView! // in case you're using VR app
let planeNode = SCNNode()
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
sceneView.scene = scene
sceneView.allowsCameraControl = true
planeNode.geometry = SCNPlane(width: 0.2, height: 0.35)
planeNode.position = SCNVector3(0, 0,-0.5)
sceneView.scene?.rootNode.addChildNode(planeNode)
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { // 3 sec delay
let image = UIImage(named: "texture.jpg")
self.planeNode.geometry?.firstMaterial?.diffuse.contents = image
}
// Other stuff...
}
//...and you can replace a texture with a button's click:
#IBAction func replaceTexture(_ sender: UIButton) {
let image = UIImage(named: "texture.gif")
planeNode.geometry?.firstMaterial?.diffuse.contents = image
}
}

How to add a semi-transparent background on top of camera ARKit

I'm trying to add a semi transparent background that cover my camera view from ARKit.
I try different things :
Add background to sceneView.scene but that not support transparency
Add an overlaySKScene but nodes on my scene are overlayed too.
Use CIImage from session > capturedImage but too slow.
Use this post : Reliable access and modify captured camera frames under SceneKit, it's OK for transform to Black And White but I don't understand how I can keep colors and blend gray color.
Search on OpenGL or Metal but I'm a noob !
So, do you have an idea to realize that operation in ARKit ?
Thanks in advance.
You can do it this way:
import ARKit
class ViewController: UIViewController,
ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.session.delegate = self
let scene = SCNScene()
sceneView.scene = scene
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 100,
height: 100)
planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor(white: 0,
alpha: 0.9)
planeNode.position.z = -5 // 5 meters away
sceneView.pointOfView?.addChildNode(planeNode) // PINNING TO CAMERA
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
}
... or in extension:
extension ViewController: ARSessionDelegate {
func session(_ session: ARSession,
didUpdate frame: ARFrame) {
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 100,
height: 100)
planeNode.position.z = -5
planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor(white: 0,
alpha: 0.9)
// var translation = matrix_identity_float4x4
// translation.columns.3.z = -5
// planeNode.simdTransform = matrix_multiply(translation,
// frame.camera.transform)
sceneView.pointOfView?.addChildNode(planeNode)
}
}

How to prevent an NSImageView from loosing an animation when its parent NSToolbar is hidden/shown?

Xcode: 9.2.
macOS Target: 10.13
It appears that an NSImageView will loose any animations added to its layer when the parent NSToolbar is made hidden then subsequently shown.
Is there a way to instruct AppKit to be hands off/restore the state of the animation?
Example code
class WindowController: NSWindowController, CALayerDelegate {
static let spinAnimation: CAAnimation = {
let basicAnimation = CABasicAnimation(keyPath:"transform.rotation")
basicAnimation.fromValue = 2.0 * .pi
basicAnimation.toValue = 0.0
basicAnimation.duration = 1.0
basicAnimation.repeatCount = Float.infinity
return basicAnimation
}()
#IBOutlet weak var imageView: NSImageView! {
didSet{
let layer = CALayer()
layer.contentsScale = 2.0
layer.contentsGravity = "aspectFit"
layer.contents = #imageLiteral(resourceName: "windmill")
imageView.layer = layer
imageView.wantsLayer = true
imageView.layerContentsRedrawPolicy = .onSetNeedsDisplay
imageView.layer?.delegate = self
imageView.needsDisplay = true
}
}
func display(_ layer: CALayer) {
let frame = layer.frame
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
layer.frame = frame
}
override func windowDidLoad() {
super.windowDidLoad()
let key = "spinAnimation"
self.imageView.layer?.add(WindowController.spinAnimation, forKey: key)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(5)) {
self.imageView.layer?.removeAnimation(forKey: key)
}
}
}
Sample Xcode project on GitHub
Normally, an animation is considered “completed” when its layer is removed from an on-screen layer tree. By default, an animation is removed from its layer when the animation completes. AppKit removes the toolbar view (and hence all its subviews and their layers) from the window, so the animation is considered completed and removed from its layer.
To keep the animation installed, you can set the animation's isRemovedOnCompletion to false.
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet var customItem: NSToolbarItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let view = customItem.view!
view.wantsLayer = true
let layer = view.layer!
let frame = layer.frame
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
layer.frame = frame
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = CGFloat(0)
animation.toValue = 2 * CGFloat.pi
animation.duration = 1
animation.repeatCount = .infinity
animation.isRemovedOnCompletion = false
layer.add(animation, forKey: animation.keyPath)
}
}
Result:

Access Geometry Child Node Scenekit

I am trying to access the childnode: boxNode, and then rotate the node using the input from a pan gesture recognizer. How should I access this node so I can manipulate it? Should I declare the cube node in a global scope and modify that way? I am new to Swift so I apologize if this code looks sloppy. I would like to add rotation actions inside of my panGesture() function. Thanks!
import UIKit
import SceneKit
class GameViewController: UIViewController {
var scnView: SCNView!
var scnScene: SCNScene!
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupScene()
}
// override var shouldAutorotate: Bool {
// return true
// }
//
// override var prefersStatusBarHidden: Bool {
// return true
// }
func setupView() {
scnView = self.view as! SCNView
}
func setupScene() {
scnScene = SCNScene()
scnView.scene = scnScene
scnView.backgroundColor = UIColor.blueColor()
initCube()
initLight()
initCamera()
}
func initCube () {
var cubeGeometry:SCNGeometry
cubeGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
let boxNode = SCNNode(geometry: cubeGeometry)
scnScene.rootNode.addChildNode(boxNode)
}
func initLight () {
let light = SCNLight()
light.type = SCNLightTypeAmbient
let lightNode = SCNNode()
lightNode.light = light
light.castsShadow = false
lightNode.position = SCNVector3(x: 1.5, y: 1.5, z: 1.5)
scnScene.rootNode.addChildNode(lightNode)
}
func initCamera () {
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0.0, y: 0.0, z: 5.0)
scnScene.rootNode.addChildNode(cameraNode)
}
func initRecognizer () {
let panTestRecognizer = UIPanGestureRecognizer(target: self, action: #selector(GameViewController.panGesture(_:)))
scnView.addGestureRecognizer(panTestRecognizer)
}
//TAKE GESTURE INPUT AND ROTATE CUBE ACCORDINGLY
func panGesture(sender: UIPanGestureRecognizer) {
//let translation = sender.translationInView(sender.view!)
}
}
First of all you should update your Xcode. It looks like this is an older Swift version you are using. SCNLightTypeAmbient is .ambient in Swift 3.
So the way you should go is by giving your nodes names to identify them:
myChildNode.name = "FooChildBar"
and then you can call on your parent node
let theNodeYouAreLookingFor = parentNode.childNode(withName: "FooChildBar", recursively: true)
You can also use this on your scene's root node
let theNodeYouAreLookingFor = scene.rootNode.childNode(withName: "FooChildBar", recursively: true)
which will look at all nodes in your scene and return the one that you gave the name.

Update all views while UISlider is being changed

Hello I have a SCNScene with a sprite kit overlay. I also have a subview that contains just a UISlider. The Slider controls the value of one of the SKLabel nodes in the sprite kit overlay. However when I am moving the slider the label does not update. It only updates once I touch outside of the slider. So I am guessing that the SCNScene and the overlay don't update while my touches are in the subview and only update once I touch back to the main view. How can I fix this?
Thanks!
My project is in swift, but if you only know objective-c ill still gratefully take your answer and translate on my own.
SceneKit Code:
class MyMarbleScene: UIViewController {
var scene: SCNScene!
var sceneView: SCNView!
var hud : MyMarbleSpriteKit!
var cameraNode: SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
scene = SCNScene()
sceneView = SCNView()
sceneView.frame = self.view.frame
sceneView.autoresizingMask = UIViewAutoresizing.allZeros
sceneView.scene = scene
sceneView.autoenablesDefaultLighting = false
sceneView.allowsCameraControl = false
sceneView.backgroundColor = UIColor.blackColor()
self.view = sceneView
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 20, z: 60)
hud = MyMarbleSpriteKit(size: self.view.frame.size, game: self)
sceneView.overlaySKScene = hud
let frame = CGRectMake(10.0, self.view.frame.height*0.5, 180.0, 10.0)
let slider = UISlider(frame: frame)
slider.addTarget(self, action: "sliderValueChanged:", forControlEvents: UIControlEvents.ValueChanged)
slider.backgroundColor = UIColor.clearColor()
slider.minimumValue = 1.0
slider.maximumValue = 10.0
slider.continuous = true
slider.value = 5
self.view?.addSubview(slider)
}
func sliderValueChanged(sender: UISlider) {
var currentValue = Int(sender.value)
println("\(currentValue)")
self.hud.updateLabel(currentValue)
}
}
SpriteKit code
class MyMarbleSpriteKit: SKScene {
var myScene: MyMarbleScene!
var SensLabelTwo = SKLabelNode()
override func didMoveToView(view: SKView) {
}
init(size: CGSize, game:MyMarbleScene) {
super.init(size: size)
myScene = game
let SensLabel = SKLabelNode()
SensLabel.text = "Marble Sensitivity:"
SensLabel.fontName = "AvenirNext-HeavyIta"
SensLabel.fontSize = 40
SensLabel.fontColor = UIColor.whiteColor()
SensLabel.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Top
SensLabel.position = CGPoint(x: 170, y: size.height-20)
self.addChild(SensLabel)
SensLabelTwo.text = "5"
SensLabelTwo.fontName = "AvenirNext-HeavyIta"
SensLabelTwo.fontSize = 60
SensLabelTwo.fontColor = UIColor.whiteColor()
SensLabelTwo.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Top
SensLabelTwo.position = CGPoint(x: 100, y: size.height-80)
self.addChild(SensLabelTwo)
}
func updateLabel(text: Int){
println("helllo")
SensLabelTwo.text = String(format:"%i", text)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
the 2D scene is not automatically refreshed when it's modified.
You will have to wait for the 3D scene to be redisplayed first. This happens when 3D animations or actions are running or when playing is set to YES. Alternatively, you can manually request a refresh with -[SCNView setNeedsDisplay].