Render off-screen SCNScene into UIImage

ios, opengl-es, scenekit

Solution

Swift 4 with SCNRenderer:

You can use SCNRenderer's snapshot method to render the off-screen SCNScene to a UIImage pretty easily.

Some caveats here, this uses Metal. I don't know where the device/iOS version cutoff is, but you'll need a newer device. You also won't be able to run it on the simulator.

Step 1 - Set up your scene like you normally would:

// Set up your scene which won't be displayed
let hiddenScene = SCNScene()
[insert code to set up your nodes, cameras, and lights here]

Step 2 - Set up the SCNRenderer -- renderer will be nil on simulator:

// Set up the renderer -- this returns nil on simulator
let renderer = SCNRenderer(device: MTLCreateSystemDefaultDevice(), options: nil)
renderer!.scene = hiddenScene

Step 3 - Render scene to UIImage:

// You can use zero for renderTime unless you are using animations,
// in which case, renderTime should be the current scene time.
let renderTime = TimeInterval(0)

// Output size
let size = CGSize(width:300, height: 150)

// Render the image
let image = renderer!.snapshot(atTime: renderTime, with: size,
                antialiasingMode: SCNAntialiasingMode.multisampling4X)

If you are running animations, you'll need to increment renderTime or set it to the time index you want to render. For example, if you want to render the frame 4 seconds into the scene, you would set it to 4. This only affects animations -- it won't go back in time and show you a historical view of your scene.

For example, if you run are running animations with SCNNode.runAction, you may want to keep incrementing renderTime every 60th of a second (0.16667 seconds), so that whenever you decide to render, you've got an updated renderTime:

var timer : Timer
var renderTime = TimeInterval(0)

timer = Timer.scheduledTimer(withTimeInterval: 0.016667, repeats: true, block: { (t) in 
        self?.renderTime += 0.016667
    }   
})

Using CADisplayLink is probably a better solution for the timing though.

Here's a very quick and dirty implementation example.

Problem

How can I render render an off-screen `SCNScene` into a `UIImage`? I know that `SCNView` provides a `-snapshot` method, but unfortunately that doesn't work for off-screen views. A similar question have been asked before where one of the answers suggest reading the bitmap data from OpenGL using `glReadPixels`, but that approach doesn't work for me with an off-screen scene. I tried rendering into the context of an `GLKView` using `SCNRenderer` without success.

Original source

Related problems