SceneKit AR game fps getting low and the device getting hot with use
Using ARKit's 6 DoF tracking at 60 fps along with SceneKit's or RealityKit's rendering (of poly geometry, PBR shaders and shadows) at 60 fps is extremely heavy burden for the processor and graphical unit. It also drains your battery very fast and makes the phone generate a lot of heat. For AR, VR and AI apps it's a common issue though.
To lower an energy impact follow these recommendations:
- Keep the total number of polygons of your model not more than 10K
- Use not more than 50K polygons per a scene (use low-poly models)
- Don't use ray-traced shadows (use pre-baked or fake shadows)
- Try not to use many PBR shaders, like metallic or reflective materials
- Try to lower a tracking frame rate and, if needed, a rendering frame rate
- Take into consideration that physics consumes much processing power
- Do not use high-resolution JPEG or PNG textures for your 3D models
Low Framerate in SceneKit
The issue was that Physics was being called on every frame, then performing some logic. The player was hitting the floor, used to center the block, every frame. I changed the contact bit mask and all the issues went away.
Get CVPixelBuffer (camera frame along with ar models) in a performant manner
You can create your own CVPixelBuffer
, then get a MTLTexture
from it and finally have SCNRenderer
render into that texture. This won't involve the CPU cost of generating a UIImage
from the snapshot API.
The key part here is to use a CVMetalTextureCache
to obtain a Metal texture from a pixel buffer.
Run and Pause an ARSession in a specified period of time
I don't think the run()
and pause()
strategy is the way to go because the DispatchQueue API is not designed for realtime accuracy. Which means there will be no guarantee that the pause will be 16ms every time. On top of that, restarting a session might not be immediate and could add more delay.
Also, the code you shared will at most capture only one image and as session.run(configuration)
is asynchronous will probably capture no frame.
As you're not using ARSCNView/ARSKView
the only way is to implement the ARSession
delegate to be notified of every captured frame.
Of course the delegate will most likely be called every 16ms because that's how the camera works. But you can decide which frames you are going to process. By using the timestamp of the frame you can process a frame every 32ms and drop the other ones. Which is equivalent to a 30 fps processing.
Here is some code to get you started, make sure that dispatchQueue
is not concurrent to process your buffers sequentially:
var lastProcessedFrame: ARFrame?
func session(_ session: ARSession, didUpdate frame: ARFrame) {
dispatchQueue.async {
self.updateCoreML(with: frame)
}
}
private func shouldProcessFrame(_ frame: ARFrame) -> Bool {
guard let lastProcessedFrame = lastProcessedFrame else {
// Always process the first frame
return true
}
return frame.timestamp - lastProcessedFrame.timestamp >= 0.032 // 32ms for 30fps
}
func updateCoreML(with frame: ARFrame) {
guard shouldProcessFrame(frame) else {
// Less than 32ms with the previous frame
return
}
lastProcessedFrame = frame
let pixelBuffer = frame.capturedImage
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
do {
try imageRequestHandler.perform(self.visionRequests)
} catch {
print(error)
}
}
What does the SceneKit statistics window tell us?
Here's what you see (note: I know most of them from experience so feel free to correct me in comments if I'm wrong):
- The red and grey bar is kind of a performance review. It's based on the FPS and you should do everything to keep it green and full. Right now, it's pretty bad!
GL
tells you which rendering engine you're using6FPS
is your framerate. That means how many times your screen is updated in one second. Your target should be60
, (it's the maximum, and what is expected from modern games), but30
is acceptable.- That diamond with the
6
is the node count, i.e. how many nodes are in your scene graph. 40.3k
is the polycount, or the number of polygons in your scene. This seems pretty high considering you only have 6 nodes and can explain the low FPS.- That donut chart is what each frame spends its time doing. In your case, you can see most of the time is spent on rendering (the section on the right explains the meaning for each color)
0.2s
is the time spent to render each frame. It's directly linked to the framerate.
Related Topics
Add New Card Is Not Being Called in Stripe Paymentoptionviewcontroller
Working with Nested Async Firebase Calls Swiftui
Retrieving Common Values in Firebase
Generic Function with Binary Operations
Swiftui App Crashes Every Time When Trying to Add Item to Grouped Items
What Is This Syntax: Func Funcname(Stuff1)(Stuff2)->Returntype {}
Delete Tableview Cell, and Remove Data from Firebase
How to Constrain 'self' to a Generic Type
Swift: How to Catch Exception When Parsing a Numeric String
Swift: Tvos Ibaction for UIcontrol in Collection View Cell Never Gets Called
Swift Janus Can Not Publish Video, But Get Remote Video Successful - Can Not Know Reason
Index or Range of Second Ocurence of Bytes in File
Cannot Convert Value of Type 'Foo!' to Expected Argument Type 'Foo!'
Modify Scpreferences Persistent Storage: Invalid Argument
How to Pass Text from Cell to Textview in Another View Controller
Xcode + Swift + Darwin.Ncurses = "A_Bold Not Found" Compilation Error. I Can't Get Bright Colors