LCOV - code coverage report
Current view: top level - source/renderer - SilhouetteRenderer.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 0 218 0.0 %
Date: 2022-03-08 13:03:03 Functions: 0 14 0.0 %

          Line data    Source code
       1             : /* Copyright (C) 2022 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "SilhouetteRenderer.h"
      21             : 
      22             : #include "graphics/Camera.h"
      23             : #include "graphics/HFTracer.h"
      24             : #include "graphics/Model.h"
      25             : #include "graphics/Patch.h"
      26             : #include "graphics/ShaderManager.h"
      27             : #include "maths/MathUtil.h"
      28             : #include "ps/CStrInternStatic.h"
      29             : #include "ps/Profile.h"
      30             : #include "renderer/DebugRenderer.h"
      31             : #include "renderer/Renderer.h"
      32             : #include "renderer/Scene.h"
      33             : 
      34             : #include <cfloat>
      35             : 
      36             : extern int g_xres, g_yres;
      37             : 
      38             : // For debugging
      39             : static const bool g_DisablePreciseIntersections = false;
      40             : 
      41           0 : SilhouetteRenderer::SilhouetteRenderer()
      42             : {
      43           0 :     m_DebugEnabled = false;
      44           0 : }
      45             : 
      46           0 : void SilhouetteRenderer::AddOccluder(CPatch* patch)
      47             : {
      48           0 :     m_SubmittedPatchOccluders.push_back(patch);
      49           0 : }
      50             : 
      51           0 : void SilhouetteRenderer::AddOccluder(CModel* model)
      52             : {
      53           0 :     m_SubmittedModelOccluders.push_back(model);
      54           0 : }
      55             : 
      56           0 : void SilhouetteRenderer::AddCaster(CModel* model)
      57             : {
      58           0 :     m_SubmittedModelCasters.push_back(model);
      59           0 : }
      60             : 
      61             : /*
      62             :  * Silhouettes are the solid-colored versions of units that are rendered when
      63             :  * standing behind a building or terrain, so the player won't lose them.
      64             :  *
      65             :  * The rendering is done in CRenderer::RenderSilhouettes, by rendering the
      66             :  * units (silhouette casters) and buildings/terrain (silhouette occluders)
      67             :  * in an extra pass using depth and stencil buffers. It's very inefficient to
      68             :  * render those objects when they're not actually going to contribute to a
      69             :  * silhouette.
      70             :  *
      71             :  * This class is responsible for finding the subset of casters/occluders
      72             :  * that might contribute to a silhouette and will need to be rendered.
      73             :  *
      74             :  * The algorithm is largely based on sweep-and-prune for detecting intersection
      75             :  * along a single axis:
      76             :  *
      77             :  * First we compute the 2D screen-space bounding box of every occluder, and
      78             :  * their minimum distance from the camera. We also compute the screen-space
      79             :  * position of each caster (approximating them as points, which is not perfect
      80             :  * but almost always good enough).
      81             :  *
      82             :  * We split each occluder's screen-space bounds into a left ('in') edge and
      83             :  * right ('out') edge. We put those edges plus the caster points into a list,
      84             :  * and sort by x coordinate.
      85             :  *
      86             :  * Then we walk through the list, maintaining an active set of occluders.
      87             :  * An 'in' edge will add an occluder to the set, an 'out' edge will remove it.
      88             :  * When we reach a caster point, the active set contains all the occluders that
      89             :  * intersect it in x. We do a quick test of y and depth coordinates against
      90             :  * each occluder in the set. If they pass that test, we do a more precise ray
      91             :  * vs bounding box test (for model occluders) or ray vs patch (for terrain
      92             :  * occluders) to see if we really need to render that caster and occluder.
      93             :  *
      94             :  * Performance relies on the active set being quite small. Given the game's
      95             :  * typical occluder sizes and camera angles, this works out okay.
      96             :  *
      97             :  * We have to do precise ray/patch intersection tests for terrain, because
      98             :  * if we just used the patch's bounding box, pretty much every unit would
      99             :  * be seen as intersecting the patch it's standing on.
     100             :  *
     101             :  * We store screen-space coordinates as 14-bit integers (0..16383) because
     102             :  * that lets us pack and sort the edge/point list efficiently.
     103             :  */
     104             : 
     105             : static const u16 g_MaxCoord = 1 << 14;
     106             : static const u16 g_HalfMaxCoord = g_MaxCoord / 2;
     107             : 
     108             : struct Occluder
     109             : {
     110             :     CRenderableObject* renderable;
     111             :     bool isPatch;
     112             :     u16 x0, y0, x1, y1;
     113             :     float z;
     114             :     bool rendered;
     115             : };
     116             : 
     117             : struct Caster
     118             : {
     119             :     CModel* model;
     120             :     u16 x, y;
     121             :     float z;
     122             :     bool rendered;
     123             : };
     124             : 
     125             : enum { EDGE_IN, EDGE_OUT, POINT };
     126             : 
     127             : // Entry is essentially:
     128             : //   struct Entry {
     129             : //     u16 id; // index into occluders array
     130             : //     u16 type : 2;
     131             : //     u16 x : 14;
     132             : //  };
     133             : // where x is in the most significant bits, so that sorting as a uint32_t
     134             : // is the same as sorting by x. To avoid worrying about endianness and the
     135             : // compiler's ability to handle bitfields efficiently, we use uint32_t instead
     136             : // of the actual struct.
     137             : 
     138             : typedef uint32_t Entry;
     139             : 
     140           0 : static Entry EntryCreate(int type, u16 id, u16 x) { return (x << 18) | (type << 16) | id; }
     141           0 : static int EntryGetId(Entry e) { return e & 0xffff; }
     142           0 : static int EntryGetType(Entry e) { return (e >> 16) & 3; }
     143             : 
     144           0 : struct ActiveList
     145             : {
     146             :     std::vector<u16> m_Ids;
     147             : 
     148             :     void Add(u16 id)
     149             :     {
     150           0 :         m_Ids.push_back(id);
     151             :     }
     152             : 
     153           0 :     void Remove(u16 id)
     154             :     {
     155           0 :         ssize_t sz = m_Ids.size();
     156           0 :         for (ssize_t i = sz-1; i >= 0; --i)
     157             :         {
     158           0 :             if (m_Ids[i] == id)
     159             :             {
     160           0 :                 m_Ids[i] = m_Ids[sz-1];
     161           0 :                 m_Ids.pop_back();
     162           0 :                 return;
     163             :             }
     164             :         }
     165           0 :         debug_warn(L"Failed to find id");
     166             :     }
     167             : };
     168             : 
     169           0 : static void ComputeScreenBounds(Occluder& occluder, const CBoundingBoxAligned& bounds, CMatrix3D& proj)
     170             : {
     171           0 :     u16 x0 = std::numeric_limits<u16>::max();
     172           0 :     u16 y0 = std::numeric_limits<u16>::max();
     173           0 :     u16 x1 = std::numeric_limits<u16>::min();
     174           0 :     u16 y1 = std::numeric_limits<u16>::min();
     175           0 :     float z0 = std::numeric_limits<float>::max();
     176           0 :     for (size_t ix = 0; ix <= 1; ++ix)
     177             :     {
     178           0 :         for (size_t iy = 0; iy <= 1; ++iy)
     179             :         {
     180           0 :             for (size_t iz = 0; iz <= 1; ++iz)
     181             :             {
     182           0 :                 CVector4D svec = proj.Transform(CVector4D(bounds[ix].X, bounds[iy].Y, bounds[iz].Z, 1.0f));
     183           0 :                 x0 = std::min(x0,  static_cast<u16>(g_HalfMaxCoord + static_cast<u16>(g_HalfMaxCoord * svec.X / svec.W)));
     184           0 :                 y0 = std::min(y0,  static_cast<u16>(g_HalfMaxCoord + static_cast<u16>(g_HalfMaxCoord * svec.Y / svec.W)));
     185           0 :                 x1 = std::max(x1,  static_cast<u16>(g_HalfMaxCoord + static_cast<u16>(g_HalfMaxCoord * svec.X / svec.W)));
     186           0 :                 y1 = std::max(y1,  static_cast<u16>(g_HalfMaxCoord + static_cast<u16>(g_HalfMaxCoord * svec.Y / svec.W)));
     187           0 :                 z0 = std::min(z0, svec.Z / svec.W);
     188             :             }
     189             :         }
     190             :     }
     191             :     // TODO: there must be a quicker way to do this than to test every vertex,
     192             :     // given the symmetry of the bounding box
     193             : 
     194           0 :     occluder.x0 = Clamp(x0, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
     195           0 :     occluder.y0 = Clamp(y0, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
     196           0 :     occluder.x1 = Clamp(x1, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
     197           0 :     occluder.y1 = Clamp(y1, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
     198           0 :     occluder.z = z0;
     199           0 : }
     200             : 
     201           0 : static void ComputeScreenPos(Caster& caster, const CVector3D& pos, CMatrix3D& proj)
     202             : {
     203           0 :     CVector4D svec = proj.Transform(CVector4D(pos.X, pos.Y, pos.Z, 1.0f));
     204           0 :     u16 x = g_HalfMaxCoord + static_cast<int>(g_HalfMaxCoord * svec.X / svec.W);
     205           0 :     u16 y = g_HalfMaxCoord + static_cast<int>(g_HalfMaxCoord * svec.Y / svec.W);
     206           0 :     caster.x = Clamp(x, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
     207           0 :     caster.y = Clamp(y, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
     208           0 :     caster.z = svec.Z / svec.W;
     209           0 : }
     210             : 
     211           0 : void SilhouetteRenderer::ComputeSubmissions(const CCamera& camera)
     212             : {
     213           0 :     PROFILE3("compute silhouettes");
     214             : 
     215           0 :     m_DebugBounds.clear();
     216           0 :     m_DebugRects.clear();
     217           0 :     m_DebugSpheres.clear();
     218             : 
     219           0 :     m_VisiblePatchOccluders.clear();
     220           0 :     m_VisibleModelOccluders.clear();
     221           0 :     m_VisibleModelCasters.clear();
     222             : 
     223           0 :     std::vector<Occluder> occluders;
     224           0 :     std::vector<Caster> casters;
     225           0 :     std::vector<Entry> entries;
     226             : 
     227           0 :     occluders.reserve(m_SubmittedModelOccluders.size() + m_SubmittedPatchOccluders.size());
     228           0 :     casters.reserve(m_SubmittedModelCasters.size());
     229           0 :     entries.reserve((m_SubmittedModelOccluders.size() + m_SubmittedPatchOccluders.size()) * 2 + m_SubmittedModelCasters.size());
     230             : 
     231           0 :     CMatrix3D proj = camera.GetViewProjection();
     232             : 
     233             :     // Bump the positions of unit casters upwards a bit, so they're not always
     234             :     // detected as intersecting the terrain they're standing on
     235           0 :     CVector3D posOffset(0.0f, 0.1f, 0.0f);
     236             : 
     237             : #if 0
     238             :     // For debugging ray-patch intersections - casts a ton of rays and draws
     239             :     // a sphere where they intersect
     240             :     for (int y = 0; y < g_yres; y += 8)
     241             :     {
     242             :         for (int x = 0; x < g_xres; x += 8)
     243             :         {
     244             :             SOverlaySphere sphere;
     245             :             sphere.m_Color = CColor(1, 0, 0, 1);
     246             :             sphere.m_Radius = 0.25f;
     247             :             sphere.m_Center = camera.GetWorldCoordinates(x, y, false);
     248             : 
     249             :             CVector3D origin, dir;
     250             :             camera.BuildCameraRay(x, y, origin, dir);
     251             : 
     252             :             for (size_t i = 0; i < m_SubmittedPatchOccluders.size(); ++i)
     253             :             {
     254             :                 CPatch* occluder = m_SubmittedPatchOccluders[i];
     255             :                 if (CHFTracer::PatchRayIntersect(occluder, origin, dir, &sphere.m_Center))
     256             :                     sphere.m_Color = CColor(0, 0, 1, 1);
     257             :             }
     258             :             m_DebugSpheres.push_back(sphere);
     259             :         }
     260             :     }
     261             : #endif
     262             : 
     263           0 :     {
     264           0 :         PROFILE("compute bounds");
     265             : 
     266           0 :         for (size_t i = 0; i < m_SubmittedModelOccluders.size(); ++i)
     267             :         {
     268           0 :             CModel* occluder = m_SubmittedModelOccluders[i];
     269             : 
     270           0 :             Occluder d;
     271           0 :             d.renderable = occluder;
     272           0 :             d.isPatch = false;
     273           0 :             d.rendered = false;
     274           0 :             ComputeScreenBounds(d, occluder->GetWorldBounds(), proj);
     275             : 
     276             :             // Skip zero-sized occluders, so we don't need to worry about EDGE_OUT
     277             :             // getting sorted before EDGE_IN
     278           0 :             if (d.x0 == d.x1 || d.y0 == d.y1)
     279           0 :                 continue;
     280             : 
     281           0 :             u16 id = static_cast<u16>(occluders.size());
     282           0 :             occluders.push_back(d);
     283             : 
     284           0 :             entries.push_back(EntryCreate(EDGE_IN, id, d.x0));
     285           0 :             entries.push_back(EntryCreate(EDGE_OUT, id, d.x1));
     286             :         }
     287             : 
     288           0 :         for (size_t i = 0; i < m_SubmittedPatchOccluders.size(); ++i)
     289             :         {
     290           0 :             CPatch* occluder = m_SubmittedPatchOccluders[i];
     291             : 
     292           0 :             Occluder d;
     293           0 :             d.renderable = occluder;
     294           0 :             d.isPatch = true;
     295           0 :             d.rendered = false;
     296           0 :             ComputeScreenBounds(d, occluder->GetWorldBounds(), proj);
     297             : 
     298             :             // Skip zero-sized occluders
     299           0 :             if (d.x0 == d.x1 || d.y0 == d.y1)
     300           0 :                 continue;
     301             : 
     302           0 :             u16 id = static_cast<u16>(occluders.size());
     303           0 :             occluders.push_back(d);
     304             : 
     305           0 :             entries.push_back(EntryCreate(EDGE_IN, id, d.x0));
     306           0 :             entries.push_back(EntryCreate(EDGE_OUT, id, d.x1));
     307             :         }
     308             : 
     309           0 :         for (size_t i = 0; i < m_SubmittedModelCasters.size(); ++i)
     310             :         {
     311           0 :             CModel* model = m_SubmittedModelCasters[i];
     312           0 :             CVector3D pos = model->GetTransform().GetTranslation() + posOffset;
     313             : 
     314           0 :             Caster d;
     315           0 :             d.model = model;
     316           0 :             d.rendered = false;
     317           0 :             ComputeScreenPos(d, pos, proj);
     318             : 
     319           0 :             u16 id = static_cast<u16>(casters.size());
     320           0 :             casters.push_back(d);
     321             : 
     322           0 :             entries.push_back(EntryCreate(POINT, id, d.x));
     323             :         }
     324             :     }
     325             : 
     326             :     // Make sure the u16 id didn't overflow
     327           0 :     ENSURE(occluders.size() < 65536 && casters.size() < 65536);
     328             : 
     329           0 :     {
     330           0 :         PROFILE("sorting");
     331           0 :         std::sort(entries.begin(), entries.end());
     332             :     }
     333             : 
     334           0 :     {
     335           0 :         PROFILE("sweeping");
     336             : 
     337           0 :         ActiveList active;
     338           0 :         CVector3D cameraPos = camera.GetOrientation().GetTranslation();
     339             : 
     340           0 :         for (size_t i = 0; i < entries.size(); ++i)
     341             :         {
     342           0 :             Entry e = entries[i];
     343           0 :             int type = EntryGetType(e);
     344           0 :             u16 id = EntryGetId(e);
     345           0 :             if (type == EDGE_IN)
     346           0 :                 active.Add(id);
     347           0 :             else if (type == EDGE_OUT)
     348           0 :                 active.Remove(id);
     349             :             else
     350             :             {
     351           0 :                 Caster& caster = casters[id];
     352           0 :                 for (size_t j = 0; j < active.m_Ids.size(); ++j)
     353             :                 {
     354           0 :                     Occluder& occluder = occluders[active.m_Ids[j]];
     355             : 
     356           0 :                     if (caster.y < occluder.y0 || caster.y > occluder.y1)
     357             :                         continue;
     358             : 
     359           0 :                     if (caster.z < occluder.z)
     360             :                         continue;
     361             : 
     362             :                     // No point checking further if both are already being rendered
     363           0 :                     if (caster.rendered && occluder.rendered)
     364             :                         continue;
     365             : 
     366           0 :                     if (!g_DisablePreciseIntersections)
     367             :                     {
     368           0 :                         CVector3D pos = caster.model->GetTransform().GetTranslation() + posOffset;
     369           0 :                         if (occluder.isPatch)
     370             :                         {
     371           0 :                             CPatch* patch = static_cast<CPatch*>(occluder.renderable);
     372           0 :                             if (!CHFTracer::PatchRayIntersect(patch, pos, cameraPos - pos, NULL))
     373           0 :                                 continue;
     374             :                         }
     375             :                         else
     376             :                         {
     377           0 :                             float tmin, tmax;
     378           0 :                             if (!occluder.renderable->GetWorldBounds().RayIntersect(pos, cameraPos - pos, tmin, tmax))
     379           0 :                                 continue;
     380             :                         }
     381             :                     }
     382             : 
     383           0 :                     caster.rendered = true;
     384           0 :                     occluder.rendered = true;
     385             :                 }
     386             :             }
     387             :         }
     388             :     }
     389             : 
     390           0 :     if (m_DebugEnabled)
     391             :     {
     392           0 :         for (size_t i = 0; i < occluders.size(); ++i)
     393             :         {
     394           0 :             DebugRect r;
     395           0 :             r.color = occluders[i].rendered ? CColor(1.0f, 1.0f, 0.0f, 1.0f) : CColor(0.2f, 0.2f, 0.0f, 1.0f);
     396           0 :             r.x0 = occluders[i].x0;
     397           0 :             r.y0 = occluders[i].y0;
     398           0 :             r.x1 = occluders[i].x1;
     399           0 :             r.y1 = occluders[i].y1;
     400           0 :             m_DebugRects.push_back(r);
     401             : 
     402           0 :             DebugBounds b;
     403           0 :             b.color = r.color;
     404           0 :             b.bounds = occluders[i].renderable->GetWorldBounds();
     405           0 :             m_DebugBounds.push_back(b);
     406             :         }
     407             :     }
     408             : 
     409           0 :     for (size_t i = 0; i < occluders.size(); ++i)
     410             :     {
     411           0 :         if (occluders[i].rendered)
     412             :         {
     413           0 :             if (occluders[i].isPatch)
     414           0 :                 m_VisiblePatchOccluders.push_back(static_cast<CPatch*>(occluders[i].renderable));
     415             :             else
     416           0 :                 m_VisibleModelOccluders.push_back(static_cast<CModel*>(occluders[i].renderable));
     417             :         }
     418             :     }
     419             : 
     420           0 :     for (size_t i = 0; i < casters.size(); ++i)
     421           0 :         if (casters[i].rendered)
     422           0 :             m_VisibleModelCasters.push_back(casters[i].model);
     423           0 : }
     424             : 
     425           0 : void SilhouetteRenderer::RenderSubmitOverlays(SceneCollector& collector)
     426             : {
     427           0 :     for (size_t i = 0; i < m_DebugSpheres.size(); i++)
     428           0 :         collector.Submit(&m_DebugSpheres[i]);
     429           0 : }
     430             : 
     431           0 : void SilhouetteRenderer::RenderSubmitOccluders(SceneCollector& collector)
     432             : {
     433           0 :     for (size_t i = 0; i < m_VisiblePatchOccluders.size(); ++i)
     434           0 :         collector.Submit(m_VisiblePatchOccluders[i]);
     435             : 
     436           0 :     for (size_t i = 0; i < m_VisibleModelOccluders.size(); ++i)
     437           0 :         collector.SubmitNonRecursive(m_VisibleModelOccluders[i]);
     438           0 : }
     439             : 
     440           0 : void SilhouetteRenderer::RenderSubmitCasters(SceneCollector& collector)
     441             : {
     442           0 :     for (size_t i = 0; i < m_VisibleModelCasters.size(); ++i)
     443           0 :         collector.SubmitNonRecursive(m_VisibleModelCasters[i]);
     444           0 : }
     445             : 
     446           0 : void SilhouetteRenderer::RenderDebugBounds(
     447             :     Renderer::Backend::GL::CDeviceCommandContext* UNUSED(deviceCommandContext))
     448             : {
     449           0 :     if (m_DebugBounds.empty())
     450             :         return;
     451             : 
     452           0 :     for (size_t i = 0; i < m_DebugBounds.size(); ++i)
     453           0 :         g_Renderer.GetDebugRenderer().DrawBoundingBox(m_DebugBounds[i].bounds, m_DebugBounds[i].color, true);
     454             : }
     455             : 
     456           0 : void SilhouetteRenderer::RenderDebugOverlays(
     457             :     Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext)
     458             : {
     459           0 :     if (m_DebugRects.empty())
     460           0 :         return;
     461             : 
     462             :     // TODO: use CCanvas2D for drawing rects.
     463           0 :     CMatrix3D m;
     464           0 :     m.SetIdentity();
     465           0 :     m.Scale(1.0f, -1.f, 1.0f);
     466           0 :     m.Translate(0.0f, (float)g_yres, -1000.0f);
     467             : 
     468           0 :     CMatrix3D proj;
     469           0 :     proj.SetOrtho(0.f, g_MaxCoord, 0.f, g_MaxCoord, -1.f, 1000.f);
     470           0 :     m = proj * m;
     471             : 
     472           0 :     CShaderTechniquePtr shaderTech = g_Renderer.GetShaderManager().LoadEffect(str_solid);
     473           0 :     shaderTech->BeginPass();
     474           0 :     Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc =
     475           0 :         shaderTech->GetGraphicsPipelineStateDesc();
     476           0 :     pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE;
     477           0 :     pipelineStateDesc.rasterizationState.cullMode = Renderer::Backend::CullMode::NONE;
     478           0 :     deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc);
     479             : 
     480           0 :     const CShaderProgramPtr& shader = shaderTech->GetShader();
     481           0 :     shader->Uniform(str_transform, proj);
     482             : 
     483           0 :     for (size_t i = 0; i < m_DebugRects.size(); ++i)
     484             :     {
     485           0 :         const DebugRect& r = m_DebugRects[i];
     486           0 :         shader->Uniform(str_color, r.color);
     487           0 :         u16 verts[] =
     488             :         {
     489           0 :             r.x0, r.y0,
     490           0 :             r.x1, r.y0,
     491           0 :             r.x1, r.y1,
     492             :             r.x0, r.y0,
     493             :             r.x1, r.y1,
     494           0 :             r.x0, r.y1,
     495           0 :         };
     496           0 :         shader->VertexPointer(
     497           0 :             Renderer::Backend::Format::R16G16_SINT, 0, verts);
     498           0 :         deviceCommandContext->Draw(0, 6);
     499             :     }
     500             : 
     501           0 :     shaderTech->EndPass();
     502             : }
     503             : 
     504           0 : void SilhouetteRenderer::EndFrame()
     505             : {
     506           0 :     m_SubmittedPatchOccluders.clear();
     507           0 :     m_SubmittedModelOccluders.clear();
     508           0 :     m_SubmittedModelCasters.clear();
     509           0 : }

Generated by: LCOV version 1.13