Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Culling GL.LINES

Discussion in 'General Graphics' started by Digika, Jul 31, 2021.

  1. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    So I have a need to do wireframe-line pass over colliders at runtime. GL.LINES does job just fine using Internal-Colored shader with ZTest=Less and Zwrite. When it comes to drawing boxes or capsules it is not issue, since these are simple object and I can easily test visibility with little overhead.

    The issue arises when it comes to mesh collider. To draw these I have to retrieve mesh and basically trace it over. And it is fine until I hit something big like world mesh collider that is massive and spans wide. Testing its bound against relevant Camera will always be true since some of its points always will be visible and therefore I will draw it in full, and this type of overhead kill performance. And I cant figure out a way to cull it. Sure, I can test if the point I'm about to draw is visible by Camera but since mesh contains gorrilions of point, transformation through Camera matrix will put on its knees any CPU. There is also seems to be no way to use any built-in culling because GL.LINES is a manual drawing (basically abstraction over commandbuffer on the engine side).

    Any ideas what else can I try?
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    You can test a surprising number of points using frustum planes instead of transforming everything into view space.
    https://docs.unity3d.com/ScriptReference/GeometryUtility.CalculateFrustumPlanes.html
    https://docs.unity3d.com/ScriptReference/GeometryUtility.CalculateBounds.html
    https://docs.unity3d.com/ScriptReference/GeometryUtility.TestPlanesAABB.html

    The short version is you could group together lines by their world space positions. Then create a world space AABB that encapsulates that entire group. After that you get the frustum planes for the current camera view every frame and test if the AABB is visible or not.

    Or you can (ab)use CullingGroup.
    https://docs.unity3d.com/Manual/CullingGroupAPI.html
    This would work similarly, except it uses bounding spheres, and instead of having to check each frame manually, the CullingGroup calls a script event each time a sphere bounds goes in or out of visibility. This is probably fast enough that you could do this for each individual line segment.
     
  3. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    How are you checking the visibility?
    TestPlanesAABB
    is in my experience very fast, especially if you have the planes for the current frame cached (ie: get them once per frame rather than getting them every single time you call
    TestPlanesAABB
    ). If you're transforming your points every frame, I'd recommend caching their world positions or world AABB so you only need to do it once instead of every frame. Alternatively you could use GeometryUtility.CalculateBounds() to transform a list of points into a world space AABB. Under the hood I believe this works by calculating a local space AABB and then transforming that into world space rather than all of the points. This goes with my previous suggestion of not testing each individual line, but groups of locally group lines.

    The CullingGroup API is confusing to use at first glance, and it does seem like there's something missing from the documentation, but there isn't. That's literally all of the information you need to get it to work.

    The TLDR version of how to use it:
    Code (csharp):
    1. private CullingGroup cullingGroup;
    2. private BoundingSphere[] sphereBounds;
    3.  
    4. // arbitrary number matching max vertex count of a 16 bit index mesh
    5. private int maxNumBounds = 65535;
    6.  
    7. // initial setup
    8. void Setup()
    9. {
    10.     cullingGroup = new CullingGroup;
    11.     cullingGroup.targetCamera = Camera.mainCamera;
    12.     sphereBounds = new BoundingSphere[maxNumBounds];
    13.     cullingGroup.SetBoundingSpheres(sphereBounds);
    14.     cullingGroup.onStateChanged += OnStateChanged;
    15. }
    16.  
    17. // fill the sphere bounds data from existing
    18. void SetPositions(Bounds[] bounds)
    19. {
    20.     int count = Mathf.Min(bounds.Length, maxNumBounds);
    21.     for (i=0; i<count; i++)
    22.     {
    23.         sphereBounds[i] = new BoundingSphere(bounds[i].center, bounds[i].extents.magnitude);
    24.     }
    25.     cullingGroup.SetBoundingSphereCount(count);
    26. }
    27.  
    28. // event that gets called when spheres go in and out of visibility
    29. void OnStateChanged(CullingGroupEvent evt)
    30. {
    31.     // do stuff
    32.     // evt.index is which sphere is changing
    33.     // evt.isVisible is if it's visible or not
    34. }
    https://docs.unity3d.com/ScriptReference/CullingGroupEvent.html

    For the per-line setup, one way you could use this is you could have each line element have a matching visibility array that you'd update with the
    OnStateChanged
    function. Then when iterating over the lines you're drawing, you check if they're visible in that array before adding them.



    The other option for all of this is to not use
    GL.LINES
    at all. You could use
    GL.wireframe
    and
    Graphics.DrawMeshNow
    to skip most of this pain.
     
  5. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    As I mentioned:
    > get the mesh collider bounds
    > test against camera's AABB planes
    > it always matches because world mesh collider is big and camera always sees some point of it

    And so my GL.lines code draws all of it. I cant test individual points because i run into the performance problem mentioned earlier - points first need to be transformed to global values for TestPlanesAABB

    That's a given.

    I cant do that because I need to refresh them every frame in case there are changes in collider shape and/or position

    Now there is an issue of overshoot - if, let's say, I take X triangles, and half of them are visible and half are not - what will be the result? Will it be match? Then I'm back to the same issue of overdrawing non-visible

    GL.WireFrame renders all the meshes WF, which is not what I need. Graphics.DrawMeshNow sure, but it requires shader.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Turn on
    GL.wireframe
    , call
    Graphics.DrawMeshNow(mesh, transformMatrix)
    , turn off
    GL.wireframe
    . Then only that one mesh is drawn as a wireframe.

    Also you need a material when using
    GL.LINES
    too using
    mat.SetPass(0)
    , otherwise you're kind of YOLOing it and getting whatever the last used material was.
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    If you're running into performance problems doing it every update, then the solution is don't do it every update, only do it when something changes. That means fleshing out code to detect changes, or if you can make whatever code is doing modifications set that object to be "dirty" in your system.

    If you have a few highly dynamic objects, then maybe you can set them to be updated every frame, but don't do that for anything that won't be!

    The aim shouldn't be for perfect accuracy with your culling. Frustum / occlusion culling is already very sloppy: the AABB and BoundingSphere for any mesh is almost always going to be much, much larger than the mesh it surrounds unless it's an axis aligned box or perfect sphere. The goal is to try and get rid of as many as you can as cheaply as possible.

    All of Unity's built in culling systems are built around taking some local space AABB, getting the (much larger) world space AABB of that, and culling if it's out of view / occluded. There's going to be a lot of "leaking" with meshes that aren't visible still rendering. And that's okay, because you can still get rid of a massive number of objects this way.
     
  8. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    Gl.wireframe is neat but it does not respect my Unity's default "Hidden/Internal-Colored" shader and material with color set.

    That's what I do rn.
    GL.Lines are good because they always same width i.e. if a mesh is REALLLY close to camera nearlcip plane the line still will be 1 "gl unit" thick where was wireframe shaders will scale accordingly. Of course I could address it by calculating how close line to the camera and then setting material property.. which is gonna be even more bigger perfhit.
     
    Last edited: Aug 4, 2021
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    AFAIK GL.LINES renders 1 pixel wide, just like GL.wireframe. There's something about your setup that must be different than what I'm expecting.

    upload_2021-8-4_15-7-44.png
    Code (csharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [ExecuteInEditMode]
    6. public class LinesVsWireframe : MonoBehaviour
    7. {
    8.     public MeshCollider coll;
    9.  
    10.     private Material mat;
    11.  
    12.     void OnEnable()
    13.     {
    14.         Camera.onPostRender += RenderLines;
    15.     }
    16.  
    17.     void OnDisable()
    18.     {
    19.         Camera.onPostRender -= RenderLines;
    20.     }
    21.  
    22.     void RenderLines(Camera cam)
    23.     {
    24.         if (coll == null)
    25.             return;
    26.  
    27.         if (mat == null)
    28.             mat = new Material(Shader.Find("Hidden/Internal-Colored"));
    29.  
    30.         // GL.Color(Color.red);
    31.         mat.SetColor("_Color", Color.red);
    32.         mat.SetPass(0);
    33.      
    34.         GL.PushMatrix();
    35.  
    36.         GL.Begin(GL.LINES);
    37.  
    38.         Matrix4x4 modelView = cam.worldToCameraMatrix * coll.transform.localToWorldMatrix;
    39.  
    40.         // hack to move the GL.LINES 1 unit to the camera's left
    41.         GL.modelview = Matrix4x4.Translate(Vector3.left) * modelView;
    42.  
    43.         Mesh mesh = coll.sharedMesh;
    44.  
    45.         for (int i=0; i<mesh.triangles.Length; i+=3)
    46.         {
    47.             GL.Vertex(mesh.vertices[mesh.triangles[i+0]]);
    48.             GL.Vertex(mesh.vertices[mesh.triangles[i+1]]);
    49.  
    50.             GL.Vertex(mesh.vertices[mesh.triangles[i+1]]);
    51.             GL.Vertex(mesh.vertices[mesh.triangles[i+2]]);
    52.  
    53.             GL.Vertex(mesh.vertices[mesh.triangles[i+2]]);
    54.             GL.Vertex(mesh.vertices[mesh.triangles[i+0]]);
    55.         }
    56.  
    57.         GL.End();
    58.  
    59.         GL.wireframe = true;
    60.         GL.modelview = cam.worldToCameraMatrix;
    61.  
    62.         // GL.Color(Color.green);
    63.         mat.SetColor("_Color", Color.green);
    64.         mat.SetPass(0);
    65.         // hack to move the DrawMeshNow 1 unit to the camera's right
    66.         Graphics.DrawMeshNow(mesh, Matrix4x4.Translate(cam.transform.right) * coll.transform.localToWorldMatrix);
    67.         GL.wireframe = false;
    68.  
    69.         GL.PopMatrix();
    70.     }
    71. }
     
  10. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    As a note, if I use the default sphere mesh on the collider, my framerate goes to absolute trash ... when using the manual
    GL.LINES
    rendering. Roughly 90ms just to iterate over the triangles and draw their edges. Granted this is a nasty method that's doubling up a lot of the edges. But I can draw a dozen wireframe meshes via
    DrawMeshNow
    and still be in the <0.1 ms range.

    Basically, I can't help but wonder if the source of your performance woes is the use of GL.LINES itself, and not everything else.
     
  11. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    So DrawMeshNow render mesh immediately, i.e you inject it into render pipeline at the moment you want, in this case that'd be OnPostRender.
    However, I believe Graphics.DrawMesh does some kind of batching? Wouldnt latter be faster theoretically?

    What's going on here
     
  12. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    No,
    DrawMesh
    does not do any “batching”. But it can be faster just by virtue of not being bottlenecked by c# execution speed. It’s tossed into the big render queue that’s passed onto the GPU via the native render thread instead. It also means you only get as much control about when it draws as allowed by material queues.

    But then you can’t use
    GL.wireframe
    since you can’t call c#
    GL
    code to toggle the wireframe rendering on and off for only that mesh.

    I'm setting the object space to view space matrix to be used for the shader. They're kind of confusingly named, but by default
    GL
    draws use an identity matrix for the
    unity_ObjectToWorld
    matrix. So you can set a combined "model" (local to world space transform) and "view" (world to camera space transform) to get it to draw on screen.

    Alternatively you can set the model and view matrices separately, though the function for setting the model matrix is confusingly named
    GL.MultiMatrix()
    . I have to assume this is some legacy OpenGL thing, because internally that calls a function called
    SetWorldMatrix()
    . You also must call
    GL.MultiMatrix()
    after setting
    GL.modeview
    otherwise it resets the model matrix back to an identity matrix.

    So you can replace
    Code (csharp):
    1. Matrix4x4 modelView = cam.worldToCameraMatrix * coll.transform.localToWorldMatrix;
    2. GL.modelview = Matrix4x4.Translate(Vector3.left) * modelView;
    with
    Code (csharp):
    1. GL.modelview = cam.worldToCameraMatrix;
    2. GL.MultMatrix(Matrix4x4.Translate(-cam.transform.right) * coll.transform.localToWorldMatrix);
    And get the same visual result. For my little demo using that second approach would have meant I wouldn't have had to set the
    GL.modeview
    again before calling
    DrawMeshNow()
    , as that overrides the
    GL.MultiMatrix
    .
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    On the topic of performance, the
    DrawMeshNow()
    method still isn't the "fastest" option from a purely rendering perspective. But it is likely the fastest from the point of view of dynamic updates. You shouldn't have to do anything but your existing full-mesh bounds check with this approach, so it effectively removes the problem you're trying to solve of making wireframe rendering of your larger meshes fast.

    A "faster" option might be to make a copy of the collision mesh using
    MeshTopology.Lines
    and
    Mesh.SetIndices()
    .
    https://docs.unity3d.com/ScriptReference/Mesh.SetIndices.html
    Use something similar to however you're currently creating your lines by adding them to the indices in pairs of values. Then you can draw that mesh using
    Graphics.DrawMesh()
    , or
    CommandBuffer.DrawMesh()
    or even slap it on a
    MeshFilter
    /
    MeshRenderer
    pair on a game object and it'll draw as a wireframe mesh. Basically it's mirroring exactly how the
    GL.Begin(GL.LINES)
    and
    GL.Vertex()
    ends up rendering, but lets you cache it as a
    Mesh
    object. But if you need to update it every frame, that'll be expensive to do, just like the
    GL.LINES
    approach already is. This will only be faster if you don't update the collision mesh every frame.

    The fastest overall option (depending on your device) would probably involve not using
    GL.LINES
    or
    GL.wireframe
    , or any kind of mesh topology mesh. Instead it could be faster using geometry shaders. You could output line topology from the geometry shader stage, or use barycentrics and screen space derivatives to render anti-aliased lines. But that really would require a custom shader & material.

    Really though, if you're already using the
    GL.LINES
    approach, I'd switch to the
    Graphics.DrawMeshNow()
    instead and save yourself the pain of having to construct the lines in c#. I might have suggested you do that anyway. This is basically how Unity's scene view selection wireframe works, with a few other behind the scenes render state stuff they don't expose to c#.
     
  14. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    What would be the difference compared to just getting `sharedMesh` (verts/tris) form MeshCollider?

    Well, compared to CommandBuffer it clearly does better:
    https://forum.unity.com/threads/is-...h-commandbuffer-drawmesh.402861/#post-6297919
     
  15. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Okay, I stand corrected then. DrawMesh does do some batching!

    I knew it does automatic frustum culling, which is convenient, but again it's difficult to use for this case because you can't set
    GL.wireframe
    to only affect those meshes.

    By default most meshes are
    MeshTopology.Triangles
    . This includes collision meshes. I haven't tried it but I would expect trying to use a collision mesh that's any other topology to throw an error.

    While the visual result of
    GL.LINES
    or
    MeshTopology.Lines
    or
    GL.wireframe
    is essentially identical, only
    GL.wireframe
    lets you use a mesh that's currently using
    MeshTopology.Triangles
    and still show a proper wireframe.

    In your first post you described how you "trace over" the mesh to construct the lines. I assume that's doing something similar to what my code does where it's doing two
    GL.Vertex()
    calls per line. That's basically what the indices are for
    MeshTopology.Lines
    , but instead of actual positions they're the index of the mesh's
    mesh.vertices
    position array. When setting the indices for a
    MeshTopology.Triangles
    mesh, the values are in sets of 3 ... actually it's the
    mesh.triangles
    array you're presumably iterating over to trace it in the first place. You can't use triangle indices for a
    MeshTopology.Lines
    mesh though, because then you'll get random lines all over the place from the last value of a triangle set and the first value of the next triangle set being treated as a "line" even though they're potentially unrelated triangles.


    You're also (hopefully) not drawing 8 thousand collision meshes like in that post you linked to. And unless you're rendering on a mobile device the focus on reducing "draw calls" to a minimum is unnecessary effort. A few hundred, or even a few thousand set pass calls is entirely reasonable for desktop or consoles.
     
  16. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    To be more precise, I trace over reconstructed (for capsule collider) or existing mesh (MeshCollider's sharedMesh), not over THE mesh of an object, that data is irrelevant to me.

    Oh I see, so it just what I do alreayd manually with verts and tris data, it just handy reprsentation, no biggie.