{"id":554,"date":"2017-07-30T22:27:27","date_gmt":"2017-07-31T05:27:27","guid":{"rendered":"https:\/\/invader.trystorymachine.com\/?p=554"},"modified":"2017-11-23T10:51:23","modified_gmt":"2017-11-23T18:51:23","slug":"custom-occlusion-culling-in-unity-improved","status":"publish","type":"post","link":"https:\/\/invader.trystorymachine.com\/?p=554","title":{"rendered":"Dev Diary: Custom Occlusion Culling in Unity Improved"},"content":{"rendered":"<p>Back in 2014 I <a href=\"https:\/\/invader.trystorymachine.com\/?p=460\">wrote about the custom occlusion system<\/a> we built\u00a0for DEAD SECRET. \u00a0It&#8217;s a pretty simple system that works by knowing all of the places the player can stand ahead of time. \u00a0It allowed us to cut our draw call count way down and ship high-fidelity mobile VR nearly two years ago. \u00a0But <a href=\"http:\/\/deadsecret.com\">DEAD SECRET CIRCLE<\/a>, the sequel to DEAD SECRET that we announced last month (check the <a href=\"http:\/\/deadsecret.com\">teaser<\/a>!), has a lot of new requirements. \u00a0One major change is\u00a0the ability to move around the environment freely, which the DEAD SECRET system didn&#8217;t support. \u00a0We needed\u00a0a new way to manage occlusion for this title.<\/p>\n<p>First we tried to leverage Unity&#8217;s built-in occlusion system, which is based on <a href=\"http:\/\/umbra3d.com\">Umbra<\/a>, an industry-standard tool that&#8217;s been around for over a decade. \u00a0But Unity&#8217;s interface to this tool is exceptionally restricted, with very few controls available. \u00a0The values that are exposed are hard to understand in terms of world units (the internet <a href=\"https:\/\/forum.unity3d.com\/threads\/occlusion-culling-woes.419273\/\">theorizes<\/a> that the scale value is off by a factor of 10), and in some cases the documentation Unity provides is misleading and\/or false. \u00a0While Unity&#8217;s built-in culling does work (very well!) in some cases, it is hard to understand why it fails in others. \u00a0The debug visualization adds to the confusion: when a &#8220;view line&#8221; passes straight through a wall, are we supposed to believe that the debug view is inaccurate, or the occlusion has broken, or that this is the way it&#8217;s &#8220;supposed&#8221; to work? \u00a0After about six months of trying to get by with Unity&#8217;s occlusion system, I gave up and decided to revisit the custom tech\u00a0we wrote for DEAD SECRET.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-556\" src=\"https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/KARLOFFlogo-1024x256.png\" alt=\"KARLOFFlogo\" width=\"640\" height=\"160\" srcset=\"https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/KARLOFFlogo-1024x256.png 1024w, https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/KARLOFFlogo-300x75.png 300w, https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/KARLOFFlogo-768x192.png 768w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/>Robot Invader&#8217;s internal technology stack is called KARLOFF, and it has had a hand in every game we&#8217;ve shipped since our first in 2011. \u00a0The great thing about a mature technology stack is that we can build new tools very quickly. \u00a0Using existing KARLOFF tech, we built a new version of our occlusion system in less than a week.<\/p>\n<p>Our new system is still based on rendering panoramas and color-coding geometry to find potentially visible geometry sets from a given point in space. \u00a0But for a world in which the camera can move freely, we need a lot more points. \u00a0We also need a way to map the current camera position to a set of visible geometry. \u00a0All of a sudden this system goes from being a simple occlusion calculation to computing a full-blown\u00a0<a href=\"https:\/\/en.wikipedia.org\/wiki\/Potentially_visible_set\"><em>potentially visible<\/em><em>\u00a0set<\/em><\/a>.<\/p>\n<p>My goal is always to trade build time and runtime memory (both of which we have plenty of) for runtime performance (which we are always constrained by). \u00a0Therefore this system uses a 3D grid (as opposed to a BSP or kd-tree, which are common for this purpose) that can index into a set of visible geometry in O(1) at runtime. \u00a0The world is cut up into grid cells\u00a0and a panorama is rendered for each. \u00a0The resulting geometry information is stored back to the grid and then looked up as the camera moves through the scene at runtime. \u00a0Simple, right?<\/p>\n<p>Well, the devil is in the details. \u00a0There&#8217;s a trade-off between cell\u00a0size, accuracy, and bake time. \u00a0Very small cells\u00a0(say, 0.3 meters per side) result in highly accurate occlusion but take a long time to bake. \u00a0For DEAD SECRET CIRCLE\u00a0our primary goal is to cut entire rooms and building floors that are on the other side of a wall away, not to occlude small objects within the frustum. \u00a0We can get away with a larger cell\u00a0if we render occlusion from several points within the cell\u00a0and then union the results into a single set. \u00a0We actually need to do two passes, one with transparent geometry hidden and the other with it opaque (in order to catch both the objects behind a transparent surface and the surface itself). \u00a0Here&#8217;s an example of these two passes rendered from four different points within a cell.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-555\" src=\"https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/Last-Visibility-Render-1024x256.png\" alt=\"Last Visibility Render\" width=\"640\" height=\"160\" srcset=\"https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/Last-Visibility-Render-1024x256.png 1024w, https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/Last-Visibility-Render-300x75.png 300w, https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/Last-Visibility-Render-768x192.png 768w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/p>\n<p>The output of this boils down to a bunch of lists of MeshRenderer pointers that get enabled and disabled as sets are selected. \u00a0It&#8217;s also necessary to do another set of renders for every occlusion portal (e.g. a door that can be opened or closed) so that we can adjust the visibility of the objects on the other side when the portal is opened at runtime. \u00a0At this point we have a fully functioning, highly accurate occlusion system that is nearly free at runtime.<\/p>\n<p>But there&#8217;s a catch: this method relies on walking a texture like the one above and picking colors to match to mesh. \u00a0At 1024&#215;512 per panorama (which seems to be the minimum resolution we can get away with based on the size of our objects in the world), a full transparent \/ nontransparent pass from four points results in a 4096&#215;1024 image. \u00a0With a 1x1x1 cell\u00a0size\u00a0we end up with about 450 cells for this small apartment level, which is\u00a01,426,063,360 pixel compares. \u00a0Add in more passes for portals and this time starts to grow exponentially. \u00a0Plus, 1x1x1 might not be small enough for perfect accuracy&#8211;we get better results with a 0.5 unit square cube, which on this level takes nearly 20 minutes to compute. \u00a0I know I just wrote that I was willing to trade build time for runtime performance, but 20 minutes to compute occlusion is unreasonable.<img loading=\"lazy\" decoding=\"async\" class=\"alignright size-full wp-image-557\" src=\"https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/ezgif-4-28823b1084.gif\" alt=\"Cell division\" width=\"450\" height=\"570\" \/><\/p>\n<p>There are probably some smart ways to tighten the algorithm itself up. \u00a0I managed to achieve a 5x speedup by optimizing just the inner pixel compare loop. \u00a0But part of the problem here is the design: the cell sizes are of fixed size and sometimes intersect with walls. \u00a0Cells have to be fairly small to prevent objects from the other side of a wall from being pulled into the set. \u00a0Plus levels that aren&#8217;t rectangular in shape end up with a lot of cells in dead areas the camera will never go, rendered for no reason.<\/p>\n<p>The next iteration of this system is to stop blindly mapping the entire level and instead restrict cells to hand-authored volumes. \u00a0Unity even provides a useful interface for this with\u00a0the fairly mysterious Occlusion Area. \u00a0Exactly how it works with Umbra is the topic of some debate (compare <a href=\"https:\/\/software.intel.com\/en-us\/android\/articles\/unity-optimization-guide-for-x86-android-part-3#\">Intel&#8217;s documentation<\/a> about the use\u00a0of Occlusion Areas to Unity&#8217;s own), but for our purposes we&#8217;re just using it as a way to size axis-aligned volumes in the world. \u00a0Each Occlusion Area produces an &#8220;island&#8221; of cells, and when within an island the camera can still find its cell in O(1). \u00a0Occlusion Islands don&#8217;t need to be the same resolution. \u00a0In fact, the cells don&#8217;t even need to be cubes any longer. \u00a0We can expose controls to control the granularity of the world per axis, resulting in rectangular volumes. \u00a0Why compute extra cells near the ceiling if the camera isn&#8217;t ever going to go up there?<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-558\" src=\"https:\/\/invader.trystorymachine.com\/wp-content\/uploads\/2017\/07\/ezgif.com-optimize.gif\" alt=\"ezgif.com-optimize\" width=\"600\" height=\"324\" \/><\/p>\n<p>So now we have something that looks like a real occlusion system. \u00a0It handles transparent objects, occlusion portals, and can be easily controlled on a per-area basis by breaking the world up into islands of culling information. \u00a0By tuning the granularity per island the apartment area above went from 20 minutes to bake down to a very reasonable 1.5 minutes. \u00a0This system can still be improved, and it doesn&#8217;t solve for dynamic objects at all, but already we&#8217;re achieving better results than we were able to after months of twiddling with Unity&#8217;s built in occlusion system.<\/p>\n<p>DEAD SECRET CIRCLE is scheduled to come out this year. \u00a0<a href=\"http:\/\/twitter.com\/robotinvader\">Follow us on twitter<\/a> for more information,\u00a0or <a href=\"http:\/\/deadsecret.com\">sign up to the mailing list<\/a> to get updates as they come out.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Back in 2014 I wrote about the custom occlusion system we built\u00a0for DEAD SECRET. \u00a0It&#8217;s a pretty simple system that works by knowing all of the places the player can stand ahead of time. \u00a0It allowed us to cut our &hellip; <a href=\"https:\/\/invader.trystorymachine.com\/?p=554\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[16,11,15],"tags":[],"class_list":["post-554","post","type-post","status-publish","format-standard","hentry","category-dead-secret","category-game-engineering","category-unity"],"_links":{"self":[{"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=\/wp\/v2\/posts\/554","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=554"}],"version-history":[{"count":2,"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=\/wp\/v2\/posts\/554\/revisions"}],"predecessor-version":[{"id":566,"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=\/wp\/v2\/posts\/554\/revisions\/566"}],"wp:attachment":[{"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=554"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=554"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/invader.trystorymachine.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=554"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}