Hey there. I’ve got a background camera rendering out some meshes to a RenderTexture. There is also a complex uGUI doing stuff with the user. Both of these have to run in parallel. I’ve found through profiling that calling a Camera.Render() on this background camera triggers Canvas.SendWillRenderCanvases() on the rest of the UI.
What in tarnation! The background camera is set to have an empty event mask, and a culling mask of only one layer where it renders what it needs to. There are no UI elements on this layer. Has anyone seen this before? How do you tell a camera that it doesn’t need to care about UI in any way, shape or form?
For anyone’s interest, I did end up solving this problem with some reflection. Basically I got the delegate, cached the value, nulled the value, did the camera render, and then set the delegate again from the cached value.
var canvasHackField = typeof(Canvas).GetField("willRenderCanvases", BindingFlags.NonPublic | BindingFlags.Static);
var canvasHackObject = canvasHackField.GetValue(null);
canvasHackField.SetValue(null, null);
Camera.Render();
canvasHackField.SetValue(null, canvasHackObject );
This is still an issue in 2017.3. Thanks @cowtrix1 for figuring out this hacky solution, though this really needs to be addressed officially. It makes no sense that this much performance is wasted - cameras that don’t render any UI should not have to do expensive calculations on whether or not they will render canvases. I have a set-up with many cameras rendering to render textures, and this eats away 50 milliseconds per frame for nothing.
We noticed the same issue in 2018.2.6f1 recently. It drastically impacts performance on consoles for us, so I’m happy to have found this workaround, thank you @cowtrix1 , I hope it still works in 2018.2.
I’ve also reported this as a bug (ID 1090213), but have not heard back from QA for a few days.
We actually heard back from QA after a while and had a bit of back-and-forth about this issue (ID 1090213)
The result was:
“After contacting developers about it, this seems to be expected behaviour. Consider all the separate Camera.Render() calls as different screenshots - the UI has to be rendered.
We’re sorry that it’s causing you inconveniences.”
Which seems absolutely ridiculous to me. This would imply Unity expects us to use Camera.Render() only for creating screenshots? If I want a screenshot that includes the UI, I can simply enable the UI layer in the CullingMask of my camera. But if I want to render literally anything else, why am I forced to endure this useless UIEvents.WillRenderCanvases call even though no canvases are actually rendered?
Sure, there is the workaround Hack posted above, but this shouldn’t be the only solution to such an inherently wrong behaviour.
Attached profiler screenshot shows a simple scene with some dynamic UI and one disabled camera (in addition to the main one) that is rendered from the Update function of a custom script. You can see almost 1ms being wasted on stuff that is simply not needed. The project this profiler screenshot has been taken in is also attached (2018.3.0b12).
It is definitely ridiculous. The post I made at the beginning of this year is about a running project, so we have the same requirements to this day. The cameras only render depth to render textures for use in a custom shader, they will never have anything to do with UI. If it wasn’t for cowtrix, Unity would not even have been viable for this project. It is such an easy thing to bypass and it hogs so much performance doing absolutely nothing. I see no reason why Unity is pulling the “intended behaviour” card here.
I decided to replace it fully with custom draw calls using CommandBuffer.DrawMesh (or DrawMeshInstanced). Luckily this was possible in my project because the camera’s only render a limited amount of meshes, so I don’t need occlusion culling. Frustum culling is easy to set up with GeometryUtility.TestPlanesAABB.
Hello from 2024 and this issue is still a problem! (Unity 2023.1.16f1)
I have 3 cameras that I must render with Camera.Render() and combine them with Graphics.Blit() with custom shader. None of those cameras render UI (UI is on a culled layer).
I need to render them manually so the XR inputs don’t unsync camera’s position between cameras writing to render textures and the other.
Why not give use a Camera.Render(bool updateCanvas = true) that we can choose to set to false? Or any other easy way to disable useless expensive canvas update?
(there is another expected Rendering.UpdateBatches() call at the end of update not visible in this screenshot)
Edit: just realized I have the same issue with the generation of miniature images (that don’t have UI) taking more time for useless update to the UI than actually render the object to a texture!
Edit 2: I can’t get the hacky solution to work, it seems the call is now from UIEvents.WillRenderCanvases and I can’t find it on the C# side throught reflection.
I’ve been doing some profiling today and lo and behold. A whopping 22% of my frame time in a pretty tame game state, is going to updating canvases because the water reflection camera rendered.
Unity 2021.3.31f1. (still LTS as far as I know)
But I have to say, incredibly many thanks to cowtrix for sharing his hack around the problem. It’s actually pretty beautiful.
It seems we are back in the trenches of this issue once again with newer versions of Unity now using UIEvents, and my solution is no longer a possibility. Bump - it’s been 7 years, folks. Devs need to be able to use cameras for many tasks entirely unrelated to taking screenshots.
This definitely isn’t a perfect solution but I think does remove the brunt of UIEvents.WillRenderCanvases by clearing the rebuild queues as the camera renders
private static IList<ICanvasElement> _layoutList;
private static IList<ICanvasElement> _graphicsList;
public static void RenderWithoutUpdatingUI(Camera camera)
{
Canvas.preWillRenderCanvases += CancelRebuild;
camera.Render();
Canvas.preWillRenderCanvases -= CancelRebuild;
void CancelRebuild()
{
if (_layoutList == null)
{
var layoutQueueField = typeof(CanvasUpdateRegistry).GetField("m_LayoutRebuildQueue", BindingFlags.Instance | BindingFlags.NonPublic);
_layoutList = layoutQueueField.GetValue(CanvasUpdateRegistry.instance) as IList<ICanvasElement>;
}
_layoutList.Clear();
if (_graphicsList == null)
{
var graphicsQueueField = typeof(CanvasUpdateRegistry).GetField("m_GraphicRebuildQueue", BindingFlags.Instance | BindingFlags.NonPublic);
_graphicsList = graphicsQueueField.GetValue(CanvasUpdateRegistry.instance) as IList<ICanvasElement>;
}
_graphicsList.Clear();
}
}