Search Unity

Camera Perfomance - Hint; it sucks.

Discussion in 'General Graphics' started by LightStriker, Jun 20, 2019.

  1. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    In our game, we have at minimum 5 camera rendering every frame;

    - Depth Occlusion
    - Skybox
    - Environment
    - Player UI
    - Full Screen UI

    When we switch to split screen multiplayer, the first 4 are duplicated, bringing to 9 cameras.

    We have our own lights, and our shadow caster also use a camera.

    For most of those camera, we would love to disable all the garbage associated to them. For example, there's no way to tell a camera there isn't any UI attached to it, or that updating particle is pointless for a Depth camera.

    Or am I missing something and there's some hidden options I missed? I'm honestly looking for potentially totally bypassing cameras for some render like Depth Occlusion.
     
  2. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,532
    You don't need five cameras. If you do, this was a poor design choice.
     
    hippocoder and xVergilx like this.
  3. Giometric

    Giometric

    Joined:
    Dec 20, 2011
    Posts:
    170
    I think the render pipelines would be the optimal way to solve this (where you would explicitly be coding what you wanted rendered by however many passes you wanted and so on), but it may also be possible to streamline this some in the Legacy pipeline by using Command Buffers to roll the work some of your passes are doing together: https://docs.unity3d.com/ScriptReference/Rendering.CommandBuffer.html

    But that depends a lot on what exactly you're rendering in those passes - it may not be worth the extra hassle, or it may not even be faster if you're doing something like trying to build up your own render list with a huge number of objects.

    If, by chance, you're rendering the Skybox with a separate camera to force it to render before everything else, as opposed to in the middle of rendering the way it normally does, it might be worth creating a custom implementation where you render your own cube just before all opaque rendering begins (an 'actual' clear-to-skybox instead of what Unity does which is generally more efficient but can have issues in some instances).

    This one I'm not sure what you mean. Cameras don't do anything with the UI if they don't have a PhysicsRaycaster attached, and no canvases have them assigned as their camera.

    I don't know about the particle systems, but I would hope they aren't trying to like billboard towards cameras that aren't even rendering them. I've never had cause to verify that though, could be that's what's going on.
     
  4. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Oh really? Except not knowing our design and not being constructive, how would you do it wise guy?

    1) Depth Occlusion; needed to render a single mesh that server as occlusion to the visual
    2) Skybox; needed because we have a dynamic sky (station orbiting a black hole) and the scale is massive compared to the environment
    3) Environment; Well duh
    4) Player UI; since we support splitscreen coop, we need a UI to take only half the screen when coop is running
    5) Full Screen UI; used to display boss fight health bar and other info that appear only once even in coop
     
  5. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    We actually have a ton of CommandBuffer. For example, we are not using any lights from Unity. Most of our lights are GPU Instanciated, saving us a ton of draw calls. We also have lot of extra features, like turning off specular per light.

    Our skybox actually contains a Black Hole simulation, including meshes and particles. We need a skybox camera.

    If you look at the Profiler when you do "Camera.Render()", you find that a camera, even while rendering nothing, will spend between 0.5 to 1 ms in various "event raising", such as;

    • Particles.Sync
    • Particles.ScheduleGeometryJobs
    • UpdateLights
    • Raising UI Events
    • LOD Update (even if no LOD exists or is seen by the camera)
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The short version is: there's no easy solution. Using multiple cameras has for a long time been "the Unity way" when it comes to doing a lot of stuff like this, but lately it seems like Cameras have gotten way more expensive, and Unity has officially pushed against this with the SRPs not even supporting this type of setup at all. For a Unity 5.4 project we did a VR game with 2 or 3 extra cameras rendering not counting the main VR camera(s) and they only cost half a ms or so each.

    For Falcon Age we have a map that was taking nearly 4ms to render on it's own ... all spent in "culling" on the CPU, in the end it's only rendering 4-5 quads in a Map layer with everything else disabled. So it feels like something changed for the worse between Unity 5.4 and Unity 2018. I swapped it to render the map using DrawMeshNow as a quick hack before properly using command buffers, and now it takes so little time the profiler sometimes shows 0.0 ms so it's still using DrawMeshNow.


    There's no way to tell cameras that they should ignore a bunch of stuff, so they always try every time. The only option is avoid rendering directly via cameras as much as possible. Like @Giometric mentioned the current "solution" is to use command buffers. I use disabled cameras as proxies for a lot of stuff, using their projection matrix and position to drive manually rendering to a render texture via DrawMeshNow or command buffers.

    For your "Depth Occlusion" camera, I'm not entirely sure what this is being used for, but if it is really rendering a single mesh, then a single DrawMesh() to a render texture should be able to reproduce this.

    For the skybox, you really can still use command buffers for this. DrawRenderer() should properly handle drawing each of the elements to a render texture without invoking a camera render, though if you're making use of soft shadows or similar effects that use built-in camera textures you'd have to recreate these on your own with more manual rendering. You could even get some nice performance improvements on the GPU rendering side if you draw the skybox after opaques in the main camera with an additional stencil write pass to only render where the "sky" is visible.

    The main scene environment... yeah, that's probably going to need a camera.

    Player UI: If you're using Unity's Canvas and GUI systems, then the canvas should already handle this for you without needing a separate camera to render it. Alternatively you could use a single all-UI camera and manually move the UI for the individual players around.
     
  7. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    I've never managed to do DrawMesh without a camera. I always end up with some messed up matrix. If you would have a tutorial or an example of a working setup where you replace a camera with a DrawMesh, that would be awesome.

    The Occlusion camera render a single "hull mesh" in a depth buffer. Then we push this and the bounds of all objects to the GPU, and have it discard anything "occluded" by the hull. It usually cut in half our triangles and draw calls, and it also work for particles, skinned mesh, etc. Anything with a bound.

    I'm not sure how you would handle a Player UI rendering only half a screen when you turn on splitscreen coop. It's a full RPG.
     
  8. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    11,792
    Yeah, I would also be interested in this. We draw the UI in a separate camera and lately I feel we're losing too much performance because of this, and I don't think the legacy pipeline is going to get any improvements. But somehow I feel like I'm missing a few pieces of information on how I would go about replacing our GUI camera with a bunch of DrawMesh commands.
     
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    So you're doing manual GPU readback occlusion? Yeah, that sounds like a perfect case for not using a camera.

    Like I said before, I still use a camera as a dummy for getting a projection and transform matrix. The trick is you need to set those matrices via GL commands before calling Graphics.DrawMeshNow().

    Pseudo code is something like this:
    Code (csharp):
    1. Graphics.SetRenderTarget(renderTexture);
    2. GL.PushMatrix();
    3. GL.modelview = dummyCam.worldToCameraMatrix;
    4. GL.LoadProjectionMatrix(GL.GetGPUProjectionMatrix(dummyCam.projectionMatrix, true));
    5. for (int i=0; i<meshesToRender.Length; i++)
    6. {
    7.     meshesToRender[i].material.SetPass(0);
    8.     // localToWorldMatrix is from a disabled Renderer component
    9.     Graphics.DrawMeshNow(meshesToRender[i].mesh, meshesToRender[i].localToWorldMatrix);
    10. }
    11. GL.PopMatrix();
    12. Graphics.SetRenderTarget(null);
    For command buffers you have the SetViewMatrix and SetProjectionMatrix functions to fill the same roles, and the matrix push/pop happens automatically.
     
    Last edited: Jun 26, 2019
    SugoiDev likes this.
  10. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Yup. We do that while the CPU is updating physic. We trigger physic manually to get rid of popping in animation-driven motion. So we pack the GPU, do the physic, and once done the GPU is usually finished processing the occlusion. I wish I could perform the ".enabled = false" in a Job or in a threat, as it's probably the slowest part of the system.

    Hmm... Looks like I was missing some piece you have there. Sad we need to have a MeshRenderer and a Camera just to pull proper matrices from them. Would be awesome if someone had a library to bypass that. I don't tend to keep MeshRenderer around when I'm using my CommandBuffer framework. (Need one when all your lights, decals, effect, etc are done by buffers) I have a dummy camera just to pull a valid matrix for spotlight. For some reason, I never managed to get a valid matrix in any other way, even the matrix utility in Unity.
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I'm using a camera and mesh renderer because I'm being lazy, and because the original system was designed with these already existing so it touched less code to reuse them. It also helps with visualizing what's happening if they're visible in the scene.

    The Matrix4x4.Perspective() function should produce identical projection matrices as the camera does. As best I understand it that's the function it uses internally to generate them to begin with, so it'd be weird if it didn't work. You do still have to use the GL.GetGPUProjectionMatrix() function as the camera and that matrix function produce OpenGL projection matrices which will not work properly on other platforms.

    The MeshRenderer.localToWorldMatrix can be skipped too, if you're mindful of how you import your meshes. Any meshes generated within Unity should work without any other issues, but I found meshes that were imported with any file scale other than 1.0 would sometimes be messed up (ie: they'd be as if they were unscaled by the import settings).
     
    xVergilx and SugoiDev like this.
  12. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Awesome, thanks. Will get it a try. Hopefully it will allow me to get rid of a couple camera.
     
    Last edited: Jun 26, 2019
  13. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Code (CSharp):
    1.             Graphics.SetRenderTarget(renderTexture.colorBuffer, depthTexture.depthBuffer);
    2.             GL.Clear(true, true, Color.black);
    3.  
    4.             GL.PushMatrix();
    5.             GL.modelview = depthCamera.worldToCameraMatrix;
    6.             GL.LoadProjectionMatrix(GL.GetGPUProjectionMatrix(depthCamera.projectionMatrix, true));
    7.  
    8.             for (int i = 0; i < occluders.Count; i++)
    9.             {
    10.                 occluders[i].renderer.sharedMaterial.SetPass(0);
    11.                 // localToWorldMatrix is from a disabled Renderer component
    12.                 Graphics.DrawMeshNow(occluders[i].filter.sharedMesh, occluders[i].renderer.localToWorldMatrix);
    13.             }
    14.  
    15.             GL.PopMatrix();
    16.             Graphics.SetRenderTarget(null);
    Sadly, I'm facing the same issues as before. The mesh rendered appears to be flipped somehow. But I can put some numbers; doing Camera.Render is about 1ms in the profiler, while doing it manually is 0.2 ms. 80% improvement, if I can figure out the stupid matrix issue.

    EDIT: After some check, the mesh appear flipped vertically somehow? But still shows up at the right position. No clue what's going on.

    REDIT: After more check, using a Unity basic cube, it appears it's the matrix of the camera that is playing some tricks.

    REREDIT: I managed to get the matrix to render properly. Sadly, I found out the depth buffer is all messed up. It appears to not write a proper depth. It looks like it's 1 if there a pixel, and 0 if there isn't, but no 0-1 range for the depth.
     
    Last edited: Jun 26, 2019
  14. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    About to give up. I have no idea why the depth buffer behave like this. Every pixel written are the same color, regardless of depth. Object get rendered over each other.

    After long research on the webs, nobody appears to have a solution.
     
  15. brownboot67

    brownboot67

    Joined:
    Jan 5, 2013
    Posts:
    375
    I ran into a situation like what you're describing with render textures drawing 2.5d mattes. Some platforms were fine some were busted. The "fix" was to initialize the rt with 16 depth instead of 0.
     
  16. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    I tried 16 and 24 bits depth. Both didn't work.