<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Vercidium Blog]]></title><description><![CDATA[Voxel technology, raytraced audio, lag compensation, particle systems, unsynchronised multithreading]]></description><link>https://vercidium.com/blog/</link><image><url>https://vercidium.com/blog/favicon.png</url><title>Vercidium Blog</title><link>https://vercidium.com/blog/</link></image><generator>Ghost 5.2</generator><lastBuildDate>Tue, 21 Apr 2026 19:31:28 GMT</lastBuildDate><atom:link href="https://vercidium.com/blog/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Sector's Edge Game Engine]]></title><description><![CDATA[During the past year I've made exciting advancements with raytraced audio, rendering systems, unsynchronised multithreading and much more.]]></description><link>https://vercidium.com/blog/sectors-edge-game-engine/</link><guid isPermaLink="false">628e2629549695a923780d49</guid><category><![CDATA[engine]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Wed, 25 May 2022 13:58:29 GMT</pubDate><content:encoded><![CDATA[<p>Over the next few months I will be writing articles on each of the topics below, which will feature fancy diagrams, GIFs and GitHub repositories. I look forward to sharing what I&apos;ve learned!</p><ul><li>Experimentation with <a href="https://www.youtube.com/watch?v=oeDmgDFBnvc">raytraced audio</a>, which sends rays outwards from the player to determine reverb properties and muffles occluded sounds</li><li>Moving to unmanaged memory using the new <a href="https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-7/">NativeMemory APIs available in &#xA0;.NET 6.0</a> for performance-critical areas like voxel storage and meshing, <a href="https://www.youtube.com/watch?v=UnKcselFTw0">particle systems</a>, texture loading and recolouring, minimap data and storage for skeletal animation systems.</li><li>Changing to <a href="https://www.khronos.org/opengl/wiki/Buffer_Object#Persistent_mapping">persistent-mapped buffers</a> for triple-buffered particle instance data with OpenGL 4.4, which removes sync points between the CPU and GPU</li><li>Changing to an unsynchronised threading system, where the main thread no longer waits for particle/UI/audio/meshing threads to complete before continuing. This allows particles to update at lower frame rates while the game continues to update and render at 144 FPS</li><li>Double-buffering in-game HUD bitmaps and deferring UI rendering to another thread using <a href="https://github.com/mono/SkiaSharp">SkiaSharp</a> and <a href="https://github.com/toptensoftware/RichTextKit">RichTextKit</a></li><li>Optimised floating voxel checks for smooth destruction</li></ul><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2022/05/cranefall.gif" class="kg-image" alt loading="lazy" width="600" height="338"></figure><ul><li>Lag compensation systems and <a href="https://twitter.com/Vercidium/status/1520341205032521728">packet delay detection</a> for accurate hit-registration when facing unstable network connections</li><li><a href="https://twitter.com/Vercidium/status/1508727652483756040">Removing all the loading screens</a> - once the game starts up, you&apos;ll never be locked in to a loading screen again</li><li><a href="https://twitter.com/Vercidium/status/1509486474538143745">Memory-efficient in-place multithreaded map reloading</a></li><li>Frosted/distorted glass effects combined with transparent smoke and glass. I had to use a few tricks with framebuffers in OpenGL to get this to work but it <a href="https://twitter.com/Vercidium/status/1487428934262464514">all works nicely</a></li><li>Reliable sub-millisecond shooting timers for high-RPM weapons, to ensure that the correct amount of bullets are fired when playing at variable frame rates</li><li><a href="https://twitter.com/Vercidium/status/1494653703185666049">Simulating entities on the client</a> and syncing them with the server later on, which gives high-ping players better feedback when throwing grenades or using projectile-based weapons</li><li><a href="https://twitter.com/Vercidium/status/1488496224202555398">Stabilised cascaded shadow maps</a> (the bane of my existence)</li><li>Voxel meshing, which will expand on my previous <a href="https://vercidium.com/blog/further-voxel-world-optimisations/">voxel meshing articles</a> </li><li><a href="https://twitter.com/Vercidium/status/1488128472736501762">Multi-perspective rendering</a> with a single postprocessing pass</li></ul><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2022/05/multipass.png" class="kg-image" alt loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2022/05/multipass.png 600w, https://vercidium.com/blog/content/images/size/w1000/2022/05/multipass.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2022/05/multipass.png 1600w, https://vercidium.com/blog/content/images/2022/05/multipass.png 1920w" sizes="(min-width: 720px) 720px"></figure><p>That&apos;s all from me - some of these features were implemented in the Sector&apos;s Edge Beta a few months ago and since then I completely forgot about them, which goes to show that good design really is invisible.</p>]]></content:encoded></item><item><title><![CDATA[Sector's Edge - Allegiance + UX]]></title><description><![CDATA[Rocket and I have spent the past month improving the tedious, unintuitive parts of the Sector's Edge UI and exploring a new compelling, rewarding and unique progression system.]]></description><link>https://vercidium.com/blog/sectors-edge-allegiance-ux/</link><guid isPermaLink="false">6199fd990777ddf10ae6c9e1</guid><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Fri, 24 Dec 2021 13:24:05 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2021/12/allegiance8WallpaperNoText.png" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2021/12/allegiance8WallpaperNoText.png" alt="Sector&apos;s Edge - Allegiance + UX"><p>Rocket and I have spent the past month improving the tedious, unintuitive parts of the Sector&apos;s Edge UI and exploring a new compelling, rewarding and unique progression system.</p><p>The changes in this blog post will be implemented in the Attachment Update, which will be available for testing in our next Public Beta Testing event.</p><h2 id="home">Home</h2><p>Currently the home screen contains a list of Steam news posts and a dark animated smokey background. While I think the background is cool, it&apos;s very gloomy and needs to be livened up:</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/11/1024890_20211121191431_1.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/1024890_20211121191431_1.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/1024890_20211121191431_1.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/1024890_20211121191431_1.png 1600w, https://vercidium.com/blog/content/images/2021/11/1024890_20211121191431_1.png 1920w"></figure><p>We recently started working with artist <a href="https://www.instagram.com/juanp_moreno_/">Juan Moreno</a>, who is creating artwork for each of our maps. We love his Ice Station piece and have decided to use it as the background on a <strong>brand new </strong>home screen, which is divided into the following categories:</p><ul><li><strong>Player ID</strong> - shows what the player has achieved. This is customisable and is shown to enemies that they kill in-game</li><li><strong>Progression</strong> - reminds the player what they&apos;re working on and what&apos;s next</li><li><strong>Quick Play</strong> - simplifies the server selection process with an option to quickly jump in to a game</li><li><strong>Active Friends</strong> - this is a scrollable list of friends that are currently in game</li><li><strong>Party </strong>- this shows a list of party invites (if any) and a prompt to create a party</li><li><strong>News and Patch Notes</strong> - helps players stay up to date with what&apos;s new</li></ul><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/home2.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/home2.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/home2.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/home2.png 1600w, https://vercidium.com/blog/content/images/2021/12/home2.png 1920w"></figure><h2 id="navigation">Navigation</h2><p>To increase screen space, I reduced the top navigation bar from 60px to 50px high and removed the 2nd navigation bar row (more details on where this disappeared to below). The social sidebar on the right has been collapsed into the social button (third from the right) as it was consuming horizontal space and clashed with some UI elements.</p><p>Now each page has the same amount of screen space as the only persistent element across each page is the top navigation bar.</p><figure class="kg-card kg-image-card kg-width-full kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/11/navigation.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="254" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/navigation.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/navigation.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/navigation.png 1600w, https://vercidium.com/blog/content/images/2021/11/navigation.png 1920w"><figcaption>New (top) versus old (bottom)</figcaption></figure><p>When clicking the &apos;Customise&apos; tab, the buttons on the navigation bar are replaced with the customisation buttons, and the breadcrumbs element appears on the left:</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/11/breadcrumbs-1.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="50" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/breadcrumbs-1.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/breadcrumbs-1.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/breadcrumbs-1.png 1600w, https://vercidium.com/blog/content/images/2021/11/breadcrumbs-1.png 1920w"></figure><p>The breadcrumbs element displays the navigation tree that the player traversed to reach the current page. Players can then click on &apos;Home&apos; or &apos;Loadout List&apos; to go back to a previous page.</p><h2 id="player-id">Player ID</h2><p>Featured at the top of the new home page is what we call your Player ID. It&apos;s a customisable banner that represents the player and the things they have achieved while playing Sector&apos;s Edge.</p><p>The old Player ID shows your:</p><ul><li>Name (Vercidium)</li><li>Title (Developer)</li><li>Tag (Aegis Annihilator)</li><li>Level (193)</li><li>Character (Agaman Engineer)</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/11/image-73.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="522" height="252"><figcaption>The old Player ID</figcaption></figure><p>The new Player ID has the following customisation options:</p><ul><li>Allegiance background (far left)</li><li>A single title (below player name)</li><li>Three <strong>Feature Slots</strong> (center)</li><li>Weapon/character showcase (far right)</li><li>Background style (applies to all elements)</li></ul><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/11/playerID.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/playerID.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/playerID.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/playerID.png 1600w, https://vercidium.com/blog/content/images/2021/11/playerID.png 1920w"></figure><p>The Tag below the player&apos;s Title in the old Player ID has been removed and in it&apos;s place Feature Slots have been added. This is where players can display what that they have achieved in game, such as:</p><ul><li>Player statistics (KDR, accuracy, time played, matches won, etc)</li><li>Weapon statistics (grenade kills, rail gun headshots, LMG damage dealt, etc)</li><li>Tasks</li></ul><p><strong>Tasks </strong>are a new feature that will be added in the Attachment Update. Each task has a unique name and icon that can be displayed in a Feature Slot on the Player ID. More information on Tasks can be found further in this blog post.</p><h2 id="title-selection">Title Selection</h2><p>The old title/tags UI was overwhelming and difficult to navigate. To improve this, I added a search bar and grouped similar titles together. Hovering over a title will show a description on the left that describes how this title was unlocked, i.e. &apos;100 Rocket Rifle kills&apos;.</p><figure class="kg-card kg-image-card kg-width-full kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/11/titles.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="785" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/titles.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/titles.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/titles.png 1600w, https://vercidium.com/blog/content/images/2021/11/titles.png 1920w"><figcaption>New (top) versus old (bottom)</figcaption></figure><p>Titles that are grouped together will expand when clicking the dropdown arrow next to it, which allows the player to equip others variations of this title that they have unlocked:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/11/image-75.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1202" height="358" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-75.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-75.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-75.png 1202w" sizes="(min-width: 1200px) 1200px"><figcaption>Title dropdown menu</figcaption></figure><p>The new Player ID customisation also explores backgrounds/styles in more detail:</p><ul><li>Once a player has unlocked a background - e.g. the Fluid background unlocks when reaching a few thousand kills with any weapon - they can then equip it with any combination of titles/showcase/feature slots.</li><li>Players can now also customise the colour of the background effects using hue, saturation and lightness sliders.</li></ul><figure class="kg-card kg-image-card kg-width-full kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/11/background.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1393" height="544" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/background.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/background.png 1000w, https://vercidium.com/blog/content/images/2021/11/background.png 1393w"><figcaption>Player ID background customisation</figcaption></figure><h2 id="weapon-presets">Weapon Presets</h2><p>A common feature request has been the ability to customise weapons differently in each loadout. To address this I implemented <strong>weapon presets</strong>, which allows players to create a combination of attachments, sights and skins that can be reused across multiple loadouts.</p><p>In the example below I am editing my &apos;Quiet + Removed Stock&apos; preset and hovering over the &apos;Resonator&apos; barrel attachment, which causes the attachment statistics panel on the left to appear.</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/11/presetEdit.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/presetEdit.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/presetEdit.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/presetEdit.png 1600w, https://vercidium.com/blog/content/images/2021/11/presetEdit.png 1920w"></figure><p>The above UI is missing the weapon list from the old UI. This has been separated into a separate weapon list page (notice the breadcrumbs in the above screenshot).</p><p>The three tabs (Attachment, Sight and Appearance) refer to additional functionality that expands on the Attachment System from the last beta. If a player wishes to have the same red-dot crosshair style on each of their guns, they can create a red-dot <strong>Sight </strong>preset and then equip that on each of their weapons.</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/triplePreset.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/triplePreset.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/triplePreset.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/triplePreset.png 1600w, https://vercidium.com/blog/content/images/2021/12/triplePreset.png 1920w"></figure><p>I&apos;m using allegiance symbols to fill in the dead space and to integrate with the lore, allegiances and manufacturers of the weapons.</p><h2 id="loadouts">Loadouts</h2><p>The loadout list has been extracted from a simple sidebar into a full-page list, which allows players to easily compare the contents of each loadout.</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/11/loadoutList.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/loadoutList.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/loadoutList.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/loadoutList.png 1600w, https://vercidium.com/blog/content/images/2021/11/loadoutList.png 1920w"></figure><h2 id="crosshair-customisation">Crosshair Customisation</h2><p>This update allows players to customise their crosshair and control what information should be shown around it.</p><p>Players can control the colour, opacity, size, radius and thickness of each element on their crosshair.</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/crosshair.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/crosshair.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/crosshair.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/crosshair.png 1600w, https://vercidium.com/blog/content/images/2021/12/crosshair.png 1920w"></figure><h2 id="hud-customisation">HUD Customisation</h2><p>This feature has been requested for a long time and it&apos;s finally available! Players can customise these aspects of the &#xA0;chat, killfeed, minimap and ammo UI:</p><ul><li>Horizontal and vertical edge (i.e. top-left, bottom-right, etc)</li><li>Horizontal and vertical distance from the edge of the screen</li><li>Max height of the chat and killfeed</li><li>Height and width of the minimap</li></ul><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/hudCustomisation-1.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1015" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/hudCustomisation-1.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/hudCustomisation-1.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/hudCustomisation-1.png 1600w, https://vercidium.com/blog/content/images/2021/12/hudCustomisation-1.png 1920w"></figure><h2 id="scoreboard">Scoreboard</h2><p>The scoreboard has received a fresh coat of paint, with a feature that I&apos;ve wanted for quite a while: a <strong>top-player showcase</strong>. The top player on each team will have a taller row on the scoreboard to showcase their Player ID.</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/scoreboard.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/scoreboard.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/scoreboard.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/scoreboard.png 1600w, https://vercidium.com/blog/content/images/2021/12/scoreboard.png 1920w"></figure><h2 id="salvage-rework">Salvage Rework</h2><p>I am planning to replace the canisters in the Salvage game mode with Soltrium meteors that fall from the sky and impact into the ground. You can see these meteors as they fall, so you can predict where they will impact.</p><p>Some will impact on the surface and some will inset into the ground a few blocks and leave a small crater. These meteors will be a small sphere of indestructible &apos;Soltrium rock&apos; blocks (2 or 3 block radius).</p><p>Meteors will spawn in groups of 1 or 2 at ~4 minute intervals (will have to balance the interval based on how quickly these meteors end up being depleted).</p><p>Rather than simply standing near the meteor, players can place an <strong><strong>&apos;extraction&apos;</strong></strong> device on a surface of a meteor block, similar to how C4 is placed.</p><ul><li>This device will extract one point for your team per 10 seconds</li><li>You can destroy enemy extraction devices, but you have to run up to it and melee it. This is because I don&apos;t want orb spam to kill the devices</li><li>You can only have <strong><strong>one</strong></strong> active extraction device at a time. If yours is destroyed, you can re-place it without needing to respawn</li><li>Once an extraction device has depleted a meteor block, it will destroy the block and move itself to a nearby meteor block</li><li>For example, if your team has 12 active extraction devices, your team will be earning 12 points per 10 seconds. 12 is the max since there&apos;s 12 people on each team - You will gain personal points each time your extraction device earns a point for your team</li></ul><p>Planned benefits:</p><ul><li>Fighting is more spread out across the map, rather than clumped into one point</li><li>Players have the ability to directly contribute to their team by placing their device on a meteor block</li><li>Players have something tangible to defend</li></ul><p>Clarifications:</p><ul><li>In maps like Magma Chamber and Crashed Freighter where canisters spawn under cover, the meteors will crash through the terrain until they reach the designated &apos;spawn point&apos;, meaning there will be a gaping hole in the ceiling.</li><li>Players place their extraction devices by running up to an available meteor block face and pressing <code>E</code></li><li>Meteors will be visible on the minimap</li><li>Friendly and enemy extraction devices will be visible on the minimap</li></ul><p>Let me know your thoughts!</p><h2 id="no-loading-screens">No Loading Screens</h2><p>When a match finishes, players will no longer disconnect and re-connect to the server. Instead, players will be presented with these new screen when the match finishes.</p><p>During this time, players can vote for the next map + game mode and continue chatting + viewing stats while the server loads the next map in the background.</p><p>Once the map has loaded in, players will <strong>not</strong> be forced to join straight away. There will be a one minute grace period for players to continue browsing stats before they will be required to spawn in, else get kicked out of the server.</p><p>This new process means the server can balance teams based on the results of the previous match, and players no longer need to wait the 40 seconds at the start of the match while the server waits for people to join.</p><p>Players who wish to spend more time browsing stats are able to, and players who want less dead-time between matches can jump right in.</p><figure class="kg-card kg-image-card kg-width-full kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/12/matchEndTeam.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/matchEndTeam.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/matchEndTeam.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/matchEndTeam.png 1600w, https://vercidium.com/blog/content/images/2021/12/matchEndTeam.png 1920w"><figcaption>Post-match screen: Team Summary</figcaption></figure><p>Team-based stats will show first, which provide a general overview of how each team performed.</p><figure class="kg-card kg-image-card kg-width-full kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/12/podium.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/podium.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/podium.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/podium.png 1600w, https://vercidium.com/blog/content/images/2021/12/podium.png 1920w"><figcaption>Post-match screen - Podium</figcaption></figure><p>On the Podium tab, you can click on each of the top 5 players to view their Player ID. If you don&apos;t land in the top 5, you can see your personal medals in the bottom-left.</p><p>On the Weapons tab, you can view your progression towards unlocking weapon Attachment Points and Titles. On the Tasks tab, you can view the progression you&apos;ve made in Allegiance-specific Tasks.</p><h2 id="tasks">Tasks</h2><p>Each new Allegiance comes with 10 Tasks, which are composed of multiple criteria. By completing Tasks, players advance in their Allegiance standing and earn Allegiance-specific rewards:</p><ul><li>New weapons</li><li>New characters</li><li>New skins</li><li>New attachments</li></ul><p>For a long time players have been asking for rewards and unlocks that aren&apos;t Proton-based, and our goal with the Task + Allegiance system is to create a compelling, rewarding and unique progression system.</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/achievementsHardWired.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/achievementsHardWired.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/achievementsHardWired.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/achievementsHardWired.png 1600w, https://vercidium.com/blog/content/images/2021/12/achievementsHardWired.png 1920w"></figure><p>Once a player completes a Task, they can display it on their Player ID for all to see.</p><p>Each task has a unique icon and are colour-coded to their respective Allegiance. For example the three tasks below are associated with Soltec, Corahk and Irridyne:</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/achievementsHardSteelCell-1.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/achievementsHardSteelCell-1.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/achievementsHardSteelCell-1.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/achievementsHardSteelCell-1.png 1600w, https://vercidium.com/blog/content/images/2021/12/achievementsHardSteelCell-1.png 1920w"></figure><p>Players can view their Allegiance progression and upcoming rewards on their full-page dashboard, accessed by clicking the icon in the top-left:</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/allegiancePage.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/allegiancePage.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/allegiancePage.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/allegiancePage.png 1600w, https://vercidium.com/blog/content/images/2021/12/allegiancePage.png 1920w"></figure><blockquote>All mercenaries begin their journey as a Devoid, who pledge to no one but themselves and their personal gain. They are renegades, outlaws and mercenaries. They run the Arena on an undisclosed planet, where players compete to gain Soltrium for themselves. This alludes to questions like &apos;where is the Arena getting its Soltrium? Who shot down the freighter?&apos;</blockquote><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/12/devoid.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="740" height="370" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/devoid.png 600w, https://vercidium.com/blog/content/images/2021/12/devoid.png 740w" sizes="(min-width: 720px) 720px"></figure><p>More information on the 6 new Allegiances will be available soon.</p><h2 id="character-customisation">Character Customisation</h2><p>The character screen has also received an upgrade and allows you to customise more than just your character&apos;s skin, but we&apos;re not ready to share the details of this just yet... </p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/charactersScavenger.png" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/charactersScavenger.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/charactersScavenger.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/charactersScavenger.png 1600w, https://vercidium.com/blog/content/images/2021/12/charactersScavenger.png 1920w"></figure><h2 id="audio-raycasting">Audio Raycasting</h2><p>Verc has been working on combining 3D audio with raytracing to create accurate directional echo, reverb and occlusion effects. Check out the video description for the full details:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/LY9x_cVfp1Y?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><h2 id="the-future">The Future</h2><p>There is a lot more that I would like to pack into this post but I&apos;d rather you experience it yourself when the beta is available.</p><p>We have been working closely with <strong>Juan Moreno</strong> over the past few weeks as we fell in love with his style. We&apos;ll share more of his works soon!</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://vercidium.com/blog/content/images/2021/12/RENDER-C.jpg" class="kg-image" alt="Sector&apos;s Edge - Allegiance + UX" loading="lazy" width="1920" height="1080" srcset="https://vercidium.com/blog/content/images/size/w600/2021/12/RENDER-C.jpg 600w, https://vercidium.com/blog/content/images/size/w1000/2021/12/RENDER-C.jpg 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/12/RENDER-C.jpg 1600w, https://vercidium.com/blog/content/images/2021/12/RENDER-C.jpg 1920w"></figure><p><strong>Punch Deck</strong> is working on 12 new underscoring tracks that will play in-game to build up the atmosphere that we&apos;re seeking to create. We also recently started working with <a href="https://www.instagram.com/poxlstudio/"><strong>poxlstudio</strong></a>, who is creating new SFX for the game:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/FBHdWPP8OoU?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/gm-hh9ZJVds?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p>And lastly, we recently partnered with <strong>Dusk Marketing</strong>, who will be helping to get this game out there and in the hands of more players. It&apos;s about time!</p><p>We hope you all have a great Christmas break and enjoy your time with your family and friends. Verc just started his two-week break from his day job and will be powering through his massive to-do list and have the beta ready in a few days.</p><p>Thank you all for support and patience, we hope we are building a game that you will love and enjoy and we can&apos;t wait to for you to experience the next few updates.</p><p>See you at the Sector&apos;s Edge!<br>- Verc and Rocket</p>]]></content:encoded></item><item><title><![CDATA[Battlefield 2042 Portal - Creating a Zombies Game Mode]]></title><description><![CDATA[This guide explains how to create a Zombies game mode using the Battlefield 2042 Portal Rules Editor.]]></description><link>https://vercidium.com/blog/battlefield-2042-portal-creating-a-zombies-game-mode/</link><guid isPermaLink="false">6191c2210777ddf10ae6c850</guid><category><![CDATA[battlefield]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Mon, 15 Nov 2021 04:04:00 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2021/11/header2.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2021/11/header2.jpg" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode"><p>This guide that explains how use the Battlefield 2042 Portal Rules Editor to:</p><ul><li>Infect random players 45 seconds after the match starts</li><li>Change player teams on death</li><li>Spawn zombies near humans</li><li>Spawn resupply crates near humans</li><li>End the match when all humans are dead</li><li>Buff zombies over time</li></ul><h3 id="part-1team-management">Part 1 - Team Management</h3><p>When the match first starts, we want every player to be a human so they can run around and pick a position they want to defend. Then after 45 seconds elapses we&apos;ll infect random players in a loop until there&apos;s 8 <strong>humans </strong>left.</p><blockquote>Note that TeamId 1 represents humans, and TeamId 2 represents zombies</blockquote><p>When setting up the gamemode, we can&apos;t specify a team size of zero for the zombies team, so we need to manually move everyone over to the human team when they first deploy:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-41.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="852" height="321" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-41.png 600w, https://vercidium.com/blog/content/images/2021/11/image-41.png 852w"></figure><p>This has two conditions:</p><ul><li>The initial infection hasn&apos;t happened yet (i.e. there are no zombies)</li><li>The player was placed on the zombies team (this will happen because the gamemode settings say 32v32)</li></ul><h3 id="part-2infecting-random-players">Part 2 - Infecting Random Players</h3><p>We can use the <code>Wait</code> action to delay this rule from occuring, so that players can run around for 45 seconds at the start of the match before they get infected.</p><p>The <code>GetAliveHumans</code> subroutine populates the <code>AliveHumans</code> and <code>AliveHumanCount</code> global variables and will be re-used throughout many of our rules.</p><p>In the rule below, we call <code>GetAliveHumans</code> and infect random players until <code>GetAliveHumans</code> is no longer greater than 8. We call <code>GetAliveHumans</code> at the end of the loop as we need to update our global variables after infecting a player.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-45.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1184" height="859" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-45.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-45.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-45.png 1184w"></figure><p>Once this rule has executed we set <code>InitialInfectionOccured</code> to true so that our other rules can be aware of the fact that we&apos;re now in zombieland.</p><h3 id="part-3zombification">Part 3 - Zombification</h3><p>This one is pretty simple - when a zombie kills a human, that human should be moved over to the other team.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-48.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="710" height="252" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-48.png 600w, https://vercidium.com/blog/content/images/2021/11/image-48.png 710w"></figure><blockquote>Note there&apos;s a loophole here - if a human kills themself, they will remain a human. We counter that in Part 5</blockquote><h3 id="part-4input-restrictions">Part 4 - Input Restrictions</h3><p>If zombies could shoot, the humans would have a tough time surviving. We need to stop zombies from being able to switch weapons, and also ensure they have no ammo just in case there&apos;s a glitch that allows them to switch to a gun.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-47.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="988" height="802" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-47.png 600w, https://vercidium.com/blog/content/images/2021/11/image-47.png 988w"></figure><h3 id="part-5zombie-spawning">Part 5 - Zombie Spawning</h3><h4 id="criteria">Criteria</h4><p>This is a more complicated rule. When a zombie spawns we need to ensure:</p><ul><li>They aren&apos;t on top of an alive human</li><li>They aren&apos;t super far away from all humans</li></ul><p>In this rule we&apos;ll aim to spawn a zombie at a position where the <strong>minimum distance</strong> to an alive human is 50 meters, which we can achieve using this process:</p><ul><li>Get a random alive player&apos;s position</li><li>Pick a random horizontal direction (random yaw, zero pitch)</li><li>Multiply that direction by 50 meters</li><li>Add that direction to the alive player&apos;s position</li><li>Check if this final position is within 49.5 meters of an enemy. If so, we need to pick a new random position. If not, this is a valid spawn position</li></ul><p>For example, in the diagram below there are 4 alive humans. The circles around the humans are the area that we should <strong>not</strong> spawn a zombie, as that&apos;s too close.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/zombies1.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="720" height="480" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/zombies1.png 600w, https://vercidium.com/blog/content/images/2021/11/zombies1.png 720w"></figure><h4 id="player-variables">Player Variables</h4><p>To start, we&apos;ll have this rule structure:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-55.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1159" height="546" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-55.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-55.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-55.png 1159w"></figure><p>The <code>Condition</code> is present as this should only apply to players who spawn <strong>after </strong>the initial infection occurs. This rule uses two new player variables:</p><ul><li><code>Spawned</code> - keep track of whether or not we have successfully spawned this zombie</li><li><code>SpawnPosition</code> - this will store the random spawn position we will generate</li></ul><h4 id="the-maths-or-math-for-the-americans">The Maths (or &apos;Math&apos; for the Americans)</h4><p>To get the position of a random player, we will use <code>GetPlayerState</code> and <code>PlayerStateVector</code>:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-52.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1014" height="74" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-52.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-52.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-52.png 1014w"></figure><p>To get a random horizontal vector with length 50, we&apos;ll use <code>RandomReal</code> to generate a random number between -&#x3C0; and &#x3C0; (a full circle), then we convert this to a vector using <code>DirectionFromAngles</code> and <code>Multiply</code> it by 50:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-53.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="920" height="72" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-53.png 600w, https://vercidium.com/blog/content/images/2021/11/image-53.png 920w"></figure><p>Then we&apos;ll <code>Add</code> it to the random player&apos;s position and store it in the <code>SpawnPosition</code> player variable:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-54.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1642" height="60" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-54.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-54.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/image-54.png 1600w, https://vercidium.com/blog/content/images/2021/11/image-54.png 1642w" sizes="(min-width: 1200px) 1200px"></figure><p>Now that we&apos;ve generated this position, we need to ensure it&apos;s not within 50 meters of any other players:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/zombies2-1.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="720" height="480" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/zombies2-1.png 600w, https://vercidium.com/blog/content/images/2021/11/zombies2-1.png 720w" sizes="(min-width: 720px) 720px"></figure><p>We do this by looping over all alive humans, checking the distance between <code>SpawnPosition</code> and the human&apos;s position, and failing if the distance is less than 49.5:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-57.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1462" height="375" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-57.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-57.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-57.png 1462w" sizes="(min-width: 1200px) 1200px"></figure><blockquote>Note that at the start this sets <code>Spawned</code> to true, then sets <code>Spawned</code> to false if the position is too close to an alive human.</blockquote><p>This loop uses a <code>Break</code> operation because there&apos;s no point comparing <code>SpawnPosition</code> to humans #2, #3 and #4 if we already know this position is too close to human #1.</p><p>The super-long <code>LessThan</code> operator compares the <code>DistanceBetween</code> the alive human&apos;s position and the <code>SpawnPosition</code> player variable:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-58.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1578" height="197" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-58.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-58.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-58.png 1578w" sizes="(min-width: 1200px) 1200px"></figure><blockquote>Note that this uses a new player variable called <code>SpawnIndex</code> to loop over the alive humans</blockquote><p>The full rule is as follows:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-59.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1625" height="557" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-59.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-59.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/image-59.png 1600w, https://vercidium.com/blog/content/images/2021/11/image-59.png 1625w" sizes="(min-width: 1200px) 1200px"></figure><p>Note that we set the <code>TeamId</code> of the player to <code>2</code>, so that players who join the match <strong>after</strong> the initial infection occurs will become zombies, and so that humans who kill themselves will respawn as a zombie.</p><h3 id="part-6resupply">Part 6 - Resupply</h3><p>The humans will be using up a lot of ammo defending themselves from these zombies, so every 2 minutes we&apos;ll resupply each human&apos;s ammo.</p><p>We use a <code>while</code> loop here so that the players will receive loadout crates until the match ends.</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-62.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="761" height="409" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-62.png 600w, https://vercidium.com/blog/content/images/2021/11/image-62.png 761w" sizes="(min-width: 720px) 720px"></figure><h3 id="part-7winning">Part 7 - Winning</h3><p>The humans win by lasting till the timer runs out, but the zombies can win by killing all alive humans:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-63.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="918" height="303" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-63.png 600w, https://vercidium.com/blog/content/images/2021/11/image-63.png 918w" sizes="(min-width: 720px) 720px"></figure><p>To ensure this condition fires, we need to run the <code>GetAliveHumans</code> subroutine to update the global <code>AliveHumanCount</code> variable after every human dies:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-65.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="671" height="285" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-65.png 600w, https://vercidium.com/blog/content/images/2021/11/image-65.png 671w"></figure><p>We also need to run the subroutine if a human leaves the match:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-64.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="756" height="223" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-64.png 600w, https://vercidium.com/blog/content/images/2021/11/image-64.png 756w" sizes="(min-width: 720px) 720px"></figure><h3 id="part-8optional-buffs">Part 8 - Optional Buffs</h3><p>To help balance your game mode you might want to buff zombies when they get a kill, or buff alive humans if a human dies. For example when a zombie kills a human, you can give the zombie +50 max health like so:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-67.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1617" height="305" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-67.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-67.png 1000w, https://vercidium.com/blog/content/images/size/w1600/2021/11/image-67.png 1600w, https://vercidium.com/blog/content/images/2021/11/image-67.png 1617w" sizes="(min-width: 1200px) 1200px"></figure><blockquote>Note that we use <code>Max(..., 150)</code> because the default value for <code>ZombieMaxHealth</code> is 0</blockquote><p>Another option is to increase zombie max health steadily over 20 minutes. We can do this using the <code>TrackVariableOverTime</code> and <code>WaitUntil</code> actions:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-70.png" class="kg-image" alt="Battlefield 2042 Portal - Creating a Zombies Game Mode" loading="lazy" width="1590" height="470" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-70.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-70.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-70.png 1590w" sizes="(min-width: 1200px) 1200px"></figure><p>This sets the <code>ZombieMaxHealth</code> to the default value 100, then gradually increases it up to 300 over the next 1200 seconds (20 minutes).</p><p>Then in a <code>While</code> loop we:</p><ul><li>set the player&apos;s max health</li><li>wait until <code>ZombieMaxHealth - PlayerStateNumber.MaxHealth &gt;= 1</code></li></ul><p>The reason for the <code>WaitUntil</code> action is to avoid spam, as I&apos;m guessing <code>TrackVariableOverTime</code> increases the <code>ZombieMaxHealth</code> value as a decimal value, rather than an integer.</p><h3 id="conclusion">Conclusion</h3><p>That&apos;s all for this guide, if you have any further questions you can ask them in the <strong>#portal-builder</strong> channel on the <a href="https://discord.gg/battlefield">Official Battlefield Discord</a> server.</p>]]></content:encoded></item><item><title><![CDATA[Battlefield 2042 Portal - Rules Editor Guide]]></title><description><![CDATA[This guide explains how to use the Battlefield 2042 Portal Rule Editor to create new gamemodes and fun gimmicks.]]></description><link>https://vercidium.com/blog/battlefield-2042-portal-rules-editor-guide/</link><guid isPermaLink="false">618d91ce0777ddf10ae6c6a8</guid><category><![CDATA[battlefield]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Fri, 12 Nov 2021 01:11:39 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2021/11/header.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2021/11/header.jpg" alt="Battlefield 2042 Portal - Rules Editor Guide"><p>This guide explains how to use the Battlefield 2042 Portal Rule Editor to create new gamemodes and fun gimmicks.</p><p>This is a bit different from my usual posts but as some of you may know, Simon and I are huge Battlefield fans. I&apos;ve been playing around with the Rules Editor for quite some time and created this guide to help you get started.</p><blockquote>Last updated 13/11/2021 8:10pm AEDT</blockquote><h2 id="overview">Overview</h2><h3 id="types">Types</h3><p>Each Type icon represents a different kind of value:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/types-2.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="717" height="1107" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/types-2.png 600w, https://vercidium.com/blog/content/images/2021/11/types-2.png 717w"></figure><p>There is some ambiguity around the &apos;Element&apos; type, since some operations accept elements related to the player&apos;s inventory and some operations accept elements related to the player&apos;s soldier, for example:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="774" height="227" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image.png 600w, https://vercidium.com/blog/content/images/2021/11/image.png 774w" sizes="(min-width: 720px) 720px"></figure><p>To see which types are expected by each action and operation, right click it and click <code>Help</code> to view documentation:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-22.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="235" height="134"></figure><h3 id="event-rules">Event Rules</h3><p>The Portal supports a few different types of <strong>Events </strong>- such as &apos;On Game Mode Start&apos;, &apos;On Player Death&apos; - which we can attach <strong>Actions </strong>to.</p><p>For example, this rule contains: </p><ul><li>A name - <code>Zombify</code></li><li>An event - <code>OnPlayerDied</code></li><li>An action - <code>SetTeamId</code>, which moves the dead player to Team 1</li></ul><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-2.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="765" height="312" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-2.png 600w, https://vercidium.com/blog/content/images/2021/11/image-2.png 765w"></figure><h3 id="ongoing-rules">Ongoing Rules</h3><p>Rules can also be <strong>Ongoing</strong>, for example this rule will kill a player that&apos;s on the ground.</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-25.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="886" height="265" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-25.png 600w, https://vercidium.com/blog/content/images/2021/11/image-25.png 886w" sizes="(min-width: 720px) 720px"></figure><h3 id="scope">Scope</h3><p>Ongoing rules have a Scope - either <code>Global, Player or Team</code> - which affects which event payloads can be used within the rule:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/11/image-6.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="579" height="166"><figcaption>Rule scope types</figcaption></figure><h3 id="event-payloads">Event Payloads</h3><p>Event payloads - such as &#xA0;<code>EventPlayer and EventTeam</code> - can only be used with their respective scope. For example, the <code>EventPlayer</code> payload cannot be used in the Global scope:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-5.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="650" height="303" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-5.png 600w, https://vercidium.com/blog/content/images/2021/11/image-5.png 650w"></figure><h3 id="variables">Variables</h3><p>You can create variables that store:</p><ul><li>Numbers</li><li>Text</li><li>Players</li><li>Vectors</li><li>Arrays</li></ul><p>Variables can be stored globally and accessed anywhere, or you can store variables on a specific player.</p><p>To start, click the <code>Variables</code> tab in the bottom-left, and then click <code>Manage Variables</code> at the top:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-8.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="397" height="250"></figure><p>From here, click <code>New Variable</code> and then select the Player scope:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-9.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="753" height="355" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-9.png 600w, https://vercidium.com/blog/content/images/2021/11/image-9.png 753w"></figure><p>You can now combine this variable with the <code>Add</code> operation to increase a player&apos;s max health by 10 each time they get a kill:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-7.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1522" height="328" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-7.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-7.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-7.png 1522w" sizes="(min-width: 1200px) 1200px"></figure><blockquote>Note that although this Rule doesn&apos;t specify the <code>Player</code> scope, we can use the <strong>EventPlayer </strong>payload here because the event is an <code>OnPlayer...</code> event</blockquote><h4 id="arrays">Arrays</h4><p>Arrays are useful for creating collections of players. As an example, let&apos;s create an array that contains all players with less than 50 health.</p><p>First, we need to create a global variable that will contain these players:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-11.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="748" height="355" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-11.png 600w, https://vercidium.com/blog/content/images/2021/11/image-11.png 748w"></figure><p>Then, we&apos;ll create a subroutine, so we can re-use this code in many different Rules.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-10.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1546" height="241" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-10.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-10.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-10.png 1546w" sizes="(min-width: 1200px) 1200px"></figure><p>This subroutine will filter through all players and create an array of players who have less than 50 health. This array will then be stored in the <code>WeakEnemies</code> global variable.</p><p>Let&apos;s break it down:</p><ul><li><code>GetPlayers</code> is an array that contains every player in the match</li><li>The <code>FilteredArray</code> operation checks each player in the <code>GetPlayers</code> array, one at a time</li><li>The <code>CurrentArrayElement</code> represents the single player that <code>FilteredArray</code> is currently checking</li><li>The <code>GetPlayerState</code> block allows us to retrieve the <code>Current Health</code> of the single player that is currently being checked</li><li>Finally, <code>SetVariable</code> stores the result of the <code>FilteredArray</code> operation into the <code>WeakEnemies</code> variable</li></ul><p>We can then use this subroutine and variable like so:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://vercidium.com/blog/content/images/2021/11/image-12.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1056" height="325" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-12.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-12.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-12.png 1056w"><figcaption>Instantly kill any player that drops below 50 health</figcaption></figure><hr><h2 id="examples">Examples</h2><h4 id="jump-damage">Jump Damage</h4><blockquote>This Rule will deal 10 damage to all enemies when a player jumps</blockquote><p>To start, let&apos;s create a subroutine that gets all enemies and stores them in a global array called <code>Enemies</code>:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-14.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1459" height="280" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-14.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-14.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-14.png 1459w" sizes="(min-width: 720px) 720px"></figure><p>We can then use the <code>PlayerStateBool</code> expression to check if the player is currently jumping, and if so deal 10 damage to all enemies:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-15.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1056" height="408" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-15.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-15.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-15.png 1056w"></figure><p>However, this Rule will deal damage to all enemies <strong>while</strong> a player is jumping, but we want to deal damage <strong>when</strong> a player jumps. To do this, we&apos;ll need to add a new Player variable called <code>Jumped</code>, and set it to true after it deals damage to all enemies:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-16.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1343" height="417" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-16.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-16.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-16.png 1343w" sizes="(min-width: 1200px) 1200px"></figure><p>This will permanently set their <code>Jumped</code> variable to true when they first jump, so we need an extra statement that sets the <code>Jumped</code> variable back to false when they finish jumping:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-17.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1327" height="551" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-17.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-17.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-17.png 1327w" sizes="(min-width: 1200px) 1200px"></figure><h4 id="juggernaut-landing">Juggernaut Landing</h4><blockquote>This rule will deal 100 damage to all enemies near a player that lands on the ground</blockquote><p>To start, we&apos;ll create:</p><ul><li>a new global variable called <code>NearEnemies</code> </li><li>a new player variable called <code>OnGround</code></li><li>a new subroutine called <code>GetNearbyEnemies</code>, which gets all players that are within 3 blocks of the player.</li></ul><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-19.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1569" height="222" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-19.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-19.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-19.png 1569w" sizes="(min-width: 1200px) 1200px"></figure><p>Note that the above <code>FilteredArray</code> operation does not check Team IDs. This is because the expression was too long to get a high-resolution screenshot of, so this rule will damage friendlies as well.</p><p>Our rule will then use the new player <code>OnGround</code> variable to track whether a player has just landed on the ground after jumping:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-20.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1258" height="497" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-20.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-20.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-20.png 1258w" sizes="(min-width: 1200px) 1200px"></figure><h4 id="bonus-kill">Bonus Kill</h4><blockquote>Kill an extra random enemy when getting a kill</blockquote><p>To start, we&apos;ll create:</p><ul><li>a new global variable called <code>AliveEnemies</code> </li><li>a new subroutine called <code>GetAliveEnemies</code>, which gets all enemy players with greater than zero health</li></ul><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-37.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1560" height="170" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-37.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-37.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-37.png 1560w" sizes="(min-width: 1200px) 1200px"></figure><p>Then when a player lands a kill, we call the <code>GetAliveEnemies</code> subroutine to update the <code>AliveEnemies</code> global array, and then kill a random player from <code>AliveEnemies</code>:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-39.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1373" height="526" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-39.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-39.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-39.png 1373w" sizes="(min-width: 720px) 720px"></figure><blockquote>Note that we use <code>DealDamage</code> rather than <code>Kill</code>, because <code>DealDamage</code> allows us to attribute this extra damage to the <code>EventPlayer</code>, which is done through the third parameter</blockquote><hr><h3 id="life-link">Life Link</h3><blockquote>All players on one team will share the same health pool</blockquote><p>This is a more complex one, as we need to:</p><ul><li>accumulate the total health of all players on a team</li><li>divide the total health by the number of players on that team, so we know how much health each player should have</li><li>damage all players on a team by a specific amount so that their health is the same</li></ul><p>To start, we&apos;ll create these three global variables. TeamHealth would be a team-scoped variable but I don&apos;t think <code>EventTeam</code> is working yet.</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-26.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="734" height="351" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-26.png 600w, https://vercidium.com/blog/content/images/2021/11/image-26.png 734w" sizes="(min-width: 720px) 720px"></figure><p>Then we&apos;ll create two subroutines, since this task will require a decent amount of code and it&apos;s best to split it up:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-27.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="624" height="598" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-27.png 600w, https://vercidium.com/blog/content/images/2021/11/image-27.png 624w"></figure><p>To start, the <code>GetTotalTeamHealths</code> subroutine will:</p><ul><li>Reset the <code>Team1Health</code> global variable back to 0</li><li>Loop over every player in the game</li><li>Check if their <code>TeamId</code> is <code>1</code></li></ul><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-30.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1162" height="401" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-30.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-30.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-30.png 1162w"></figure><p>Then, if their <code>TeamId</code> is <code>1</code>, we need to add the health of this player to the <code>Team1Health</code> global variable:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-31.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1506" height="76" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-31.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-31.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-31.png 1506w" sizes="(min-width: 1200px) 1200px"></figure><p>We do this using the <code>Add</code> operator, which takes two parameters:</p><ul><li>The <code>Team1Health</code> global variable</li><li>The <code>CurrentHealth</code> of the nth player in <code>GetPlayers</code></li></ul><p>The result is then stored back in the <code>Team1Health</code> global variable.</p><p>To distribute the total team health among all players, we first divide <code>Team1Health</code> by the number of players in Team 1 to determine how much health each team member should have. This means that <code>Team1Health</code> now holds the average player health, rather than the total team health.</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-33.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1485" height="104" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-33.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-33.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-33.png 1485w" sizes="(min-width: 720px) 720px"></figure><p>Then we need to either heal or damage each player in Team 1 to ensure they have the same health value:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-32.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1177" height="404" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-32.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-32.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-32.png 1177w" sizes="(min-width: 720px) 720px"></figure><blockquote>This assumes that a negative value sent to <code>DealDamage</code> will heal the player</blockquote><p>The <code>Subtract</code> operation in the above picture contains these two values, where it will damage the player by the amount <code>PlayerHealth - Team1Health</code>:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-34.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1438" height="79" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-34.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-34.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-34.png 1438w" sizes="(min-width: 720px) 720px"></figure><blockquote>For example if Player X has 80 health, and the value of <code>Team1Health</code> is 70, we need to deal 10 damage to Player X.</blockquote><p>Lastly, to make this work for both teams, we&apos;ll duplicate the above code but instead use the <code>Team2Health</code> global variable with the TeamId <code>2</code>:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-35.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1196" height="639" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-35.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-35.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-35.png 1196w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/11/image-36.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1170" height="547" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-36.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-36.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-36.png 1170w" sizes="(min-width: 720px) 720px"></figure><h2 id="issues">Issues</h2><p>Currently the <code>EventTeam</code> payload cannot be used. The documentation for it has this example:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-23.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="785" height="330" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-23.png 600w, https://vercidium.com/blog/content/images/2021/11/image-23.png 785w"></figure><p>Although the Rules Editor states that it&apos;s invalid:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://vercidium.com/blog/content/images/2021/11/image-24.png" class="kg-image" alt="Battlefield 2042 Portal - Rules Editor Guide" loading="lazy" width="1590" height="313" srcset="https://vercidium.com/blog/content/images/size/w600/2021/11/image-24.png 600w, https://vercidium.com/blog/content/images/size/w1000/2021/11/image-24.png 1000w, https://vercidium.com/blog/content/images/2021/11/image-24.png 1590w" sizes="(min-width: 1200px) 1200px"></figure><hr><h2 id="community">Community</h2><p>If you have some cool ideas or extra questions, head over to the <a href="https://discord.gg/battlefield">Battlefield Discord server</a> and chat with us in the <strong>#portal</strong> channel.</p><p>Special thanks to Dragory for answering everyone&apos;s questions regarding the Rules Editor, as well as for providing a great solution to the <strong>Jump Damage</strong> rule.</p>]]></content:encoded></item><item><title><![CDATA[OpenGL Particle Systems]]></title><description><![CDATA[This article details the changes in memory structure, algorithms and shaders that contributed to a performance increase in the Sector's Edge particle system.]]></description><link>https://vercidium.com/blog/opengl-particle-systems/</link><guid isPermaLink="false">61190945d364ab77972ea25d</guid><category><![CDATA[particle]]></category><category><![CDATA[optimisations]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Tue, 17 Aug 2021 03:01:14 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2021/08/ctfRain.png" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2021/08/ctfRain.png" alt="OpenGL Particle Systems"><p>This is a continuation on the original article <a href="https://vercidium.com/blog/particle-optimisations/">Particle Optimisations with OpenGL and Multithreading</a>, which details the initial methods used in the particle system in our free to play first person shooter <a href="https://store.steampowered.com/app/1024890/Sectors_Edge/">Sector&apos;s Edge</a>.</p><p>This article details the changes in memory structure, algorithms and shaders that contributed to a performance increase in this particle system.</p><p>The source code for this particle system is <a href="https://github.com/Vercidium/particles">available on GitHub</a>.</p><h2 id="particle-storage-benchmarks">Particle Storage Benchmarks</h2><p>Particles were initially stored in multiple linked lists as it allowed particles to be added and removed quickly without worrying about array capacity. Based on feedback I received, I measured the time spent in ticks (10000 ticks = 1 millisecond) of adding new particles, accessing particle data and removing particles with different storage types.</p><h3 id="adding-particles">Adding Particles</h3><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/02/image-3.png" class="kg-image" alt="OpenGL Particle Systems" loading="lazy"></figure><p>Class and Struct arrays were fastest here since they allow for particles to be pre-allocated and reset in-place, whereas Class and Struct lists were slower since they require allocating a new Particle instance each time.</p><p>In an attempt to improve the performance of adding to Class and Struct lists, I kept a separate list of &apos;disposed particles&apos;, which I would copy from rather than allocating a new particle each time. This increased the speed by about 2x, but as seen above it&apos;s still much slower than the other methods.</p><p>I believe adding to Class and Struct lists were slower than Linked List because of the function call overhead of <code>List&lt;T&gt;.Add(...)</code>.</p><h3 id="accessing-particles">Accessing Particles</h3><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/02/image-4.png" class="kg-image" alt="OpenGL Particle Systems" loading="lazy"></figure><p>The above numbers were measured by accessing each particle in order, which allows for a realistic comparison between Linked Lists and the other methods. In Sector&apos;s Edge there isn&apos;t a use case for accessing a particle at an arbitrary index, so we don&apos;t need to benchmark random access.</p><p>Struct Arrays received a slight advantage since they can be accessed by reference with a fixed pointer, however Class List/Array were still slightly faster for reasons I&apos;m unsure of.</p><p>Accessing Linked Lists was surprisingly the fastest, which I assume is because it requires no array bounds checks.</p><h3 id="removing-particles">Removing Particles</h3><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/02/image-5.png" class="kg-image" alt="OpenGL Particle Systems" loading="lazy"></figure><p>Elements were removed from classes and lists using the swap and pop method, where the nth element is replaced with the last element and the array size is reduced by one.</p><p>Removing from Class and Struct lists was the slowest because of the function call overhead of <code>List&lt;T&gt;.RemoveAt(...)</code>. Removing from a Struct list was by far the slowest since C# does not allow accessing List elements by reference, meaning the struct gets copied around in memory more than the other storage types. </p><p>I was surprised that Linked List did not have the fastest remove operations, since it requires only one operation:</p><pre><code class="language-csharp">// Remove from the linked list
particle.Next = particle.Next.Next;

// Remove from the struct array
array[n] = array[--arrayLength];

// Remove from the struct list
int removeIndex = list.Count - 1;
list[i] = list[removeIndex];
list.RemoveAt(removeIndex);</code></pre><h3 id="class-array-vs-struct-array">Class Array vs Struct Array</h3><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/02/image-6.png" class="kg-image" alt="OpenGL Particle Systems" loading="lazy"></figure><p>Based on the numbers below, I decided to switch to Struct Arrays since the access speed difference is negligible and Struct Arrays can be accessed with fixed pointers, which provides additional benefits in the Sector&apos;s Edge engine.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">Struct Array Add is 13% faster
Class Array Access is 5% faster
Struct Array Remove is 10% faster
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="particle-structure">Particle Structure</h2><p>Since structs are copied around a lot in memory, the size of a Particle needed to be trimmed down. The original Particle class was 152 bytes and had the following composition:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class OldParticle
{
    public Matrix4F transform; // Matrix4F = struct of 16 floats
    public Vector3 position; // Vector3 = struct of 3 doubles
    public Vector3 velocity;
    public Vector3 rotation;
    public float scale;
    public uint colour;
    public float lifeTime;
    public Particle Next;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The new Particle struct is 60 bytes and copies ~2.5x faster than if it remained the same size:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public struct NewParticle
{
    public Vector3F position; // Vector3F = 3 floats
    public Vector3F velocity;
    public Vector3F rotation;
    public float scale;
    public uint colour;
    public int lifetime;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The <code>transform</code> variable was stored in the old particle system so that when the particle comes to rest, its transform no longer needed to be recalculated each frame. In the new particle system the percentage of a particle&apos;s lifetime where it is actually at rest is so small that it&apos;s no longer worth storing this variable in the Particle.</p><p>Previously the <code>transform</code> was updated in-place in the Particle and then copied to the mapped OpenGL buffer. In the new particle system we obtain a reference to the struct in shared GPU memory and write to it directly, saving the overhead of copying a <code>Matrix4F</code> struct:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Obtain a direct reference to the Matrix4F struct in shared GPU memory
ref Matrix4F transform = ref mappedPtr[bufferOffset++];

// Update it directly
transform.M11 = ...
transform.M12 = ...</code></pre>
<!--kg-card-end: markdown--><p></p><p>All <code>Vector3</code> structs were changed to <code>Vector3F</code> structs to reduce memory usage and all particle-related math was changed from double to float. Double and float operations like addition and multiplication are identical in speed and the reduced precision is not visible to the human eye in-game.</p><h2></h2><h2 id="batch-particle-adds">Batch Particle Adds</h2><p>Previously, the particle system worked with N linked lists, where N is the amount of threads supported by the CPU. This allowed each thread to work separately with its own linked list (note that variables prefixed with <code>t_</code> are accessed by multiple threads):</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">Particle[] t_LinkedLists = new Particle[THREAD_COUNT];
</code></pre>
<!--kg-card-end: markdown--><p></p><p>Due to the nature of Linked Lists, particles could only be added to one list at a time. With struct arrays, we can now add an entire batch of particles to an array at once.</p><p>The particle storage now looks like this:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">ParticleStruct[][] t_Particles  = new ParticleStruct[THREAD_COUNT][];

// This tracks how many particles have been added to each array
int[] t_ParticleCount = new int[THREAD_COUNT];
</code></pre>
<!--kg-card-end: markdown--><p></p><p>To add a new batch of particles, we must first check there is enough available space in the array:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">protected void CheckParticleArraySize(ref Particle[] currentArray, int currentCount, int batchSize)
{
    // Early exit if already at max size
    if (currentCount == Constants.MaxParticlesPerThread)
        return;

    var currentCapacity = currentArray.Length;

    // Calculate the total amount of particles after we add this batch
    int newCount = currentCount + batchSize;

    // If the current array can&apos;t hold that many particles
    if (newCount &gt;= currentCapacity)
    {
        // Either double the array size or increase it by 2048
        var newLength = Math.Min(newCount + 2048, newCount * 2);

        // There is no need to ever allocate more memory than the max amount of particles
        newLength = Math.Min(newLength, Constants.MaxParticlesPerThread);
                
        Array.Resize(ref currentArray, newLength);
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The entire batch of particles is then added to one thread&apos;s particle storage:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">void AddABatchOfParticles()
{
    int batchSize = 6;

    // Select which thread to add these particles to
    var index = CurrentThreadAddIndex;
            
    // Get references to that thread&apos;s particle storage
    ref var currentArray = ref t_Particles[index];
    ref var currentAmount = ref t_ParticleCount[index];
    
    // Check we have enough room in the array to add this batch
    CheckParticleArraySize(ref currentArray, currentAmount, batchSize);
    
    // Add the particles
    for (int i = 0; i &lt; batchSize; i++)
        AddParticle(ref currentArray, ref currentAmount, Vector3F.Zero, Vector3F.Zero, 0, 0, 0);
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="adding-particles-1">Adding Particles</h2><p>The slowest part of the old <code>AddParticle(...)</code> function (<a href="https://github.com/Vercidium/particles/blob/master/source/ParticleManager.cs">source code</a>) was that it had to move a Particle from the decayed particle list to the active particle list and then update it. If there were no decayed particles available, a new Particle had to be allocated.</p><p>The overhead of copying and allocating new Particles is avoided in the new particle system as it modifies existing memory using the <code>Reset(...)</code> function instead:</p><pre><code class="language-csharp">void AddParticle(ref Particle[] currentArray, ref int currentCount, in Vector3F position, in Vector3F velocity, in int lifetime, in float scaleMod, in uint colour)
{
	// If the current array exceeds the max amount of particles allowed, replace a random particle
    var index = currentCount &gt; Constants.MaxParticles ? rand.Next(currentCount) : currentCount++;

    currentArray[index].Reset(
            position,
            velocity,
            lifetime,
            scaleMod,
            colour);
}</code></pre><p></p><p>The Reset function simply sets each variable in the struct, which was faster than allocating a new struct, i.e. <code>currentArray[index] = new Particle(...)</code></p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public void Reset(Vector3F p, Vector3F v, int l, float s, uint c)
{
    position = p;
    velocity = v;
    scale = s;
    colour = c;
    lifetime = l;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="updating-particles">Updating Particles</h2><p>After we update a particle (move it, rotate it, check collision with the map), we write its matrix transform directly to shared GPU memory with <code>glMapBuffer</code>. In the old system, this process was as follows:</p><ul><li>frame start</li><li>map the particle buffer</li><li>write to the buffer with multiple Tasks</li><li>wait for all Tasks to finish</li><li>unmap the buffer</li><li>render the particles</li><li>frame end</li></ul><p>This meant that each frame there were two bottlenecks:</p><ul><li>waiting for all Tasks to finish</li><li>waiting for the GPU driver to copy the new buffer data to the GPU before rendering</li></ul><p>The new system avoids these bottlenecks with a slightly different process:</p><ul><li>frame start</li><li>wait for all Tasks to finish*</li><li>unmap the particle buffer*</li><li>render the particles*</li><li>orphan the buffer</li><li>map the buffer</li><li>write to the buffer with multiple Tasks</li><li>frame end</li></ul><blockquote>Stages above that are marked with * do not occur on the very first frame since the particle buffer has not been written to yet.</blockquote><p>There are two main differences with the new process:</p><ul><li>the particle buffer is orphaned before writing to it</li><li>the Tasks can continue running once the frame ends, meaning it is likely that there will be little to no time spent waiting for the Tasks to finish when the next frame starts</li></ul><p>The reason we orphan the particle buffer is to avoid sync points, which is where the CPU is waiting for the GPU to finish it&apos;s current work. This negatively impacts the performance of the game and should be avoided where possible.</p><p>A sync point can happen when the CPU attempts to modify a buffer immediately after requesting it to be rendered. An example of this is below, where the <code>Gl.MapBuffer(...)</code> call will wait until the above <code>Gl.DrawArrays(...)</code> function has been processed by the GPU before continuing:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">Gl.BindBuffer(BufferTarget.ArrayBuffer, bufferHandle);

Gl.DrawArrays(PrimitiveType.TriangleStrip, 0, currentLength);

var ptr = Gl.MapBuffer(BufferTarget.ArrayBuffer, Gl.READ_WRITE); // SYNC POINT
// Write data to ptr...
Gl.UnmapBuffer(BufferTarget.ArrayBuffer);

Gl.BindBuffer(BufferTarget.ArrayBuffer, 0);
</code></pre>
<!--kg-card-end: markdown--><p></p><p>This sync point can be avoided with <strong>buffer orphaning</strong>, which is where calling <code>Gl.BufferData</code> after <code>Gl.DrawArrays</code> will tell the GPU driver to allocate a new region of memory for this buffer, while still retaining the old region of memory for the prior <code>Gl.DrawArrays</code> call.</p><p>We can now write to this new region of memory with <code>Gl.MapBuffer(...)</code> without creating a sync point. Both regions of memory are associated with this single buffer and once that <code>Gl.DrawArrays</code> call is processed by the GPU, the old region of memory is freed.</p><p>This is essentially double-buffering, but the GPU driver handles it for us.</p><p>The actual process of updating particles is the same as the old system, where the particle buffer is mapped and written to by multiple Tasks.</p><h2 id="trigonometry">Trigonometry</h2><p>The old particle system pre-calculated the sin wave and stored it in an array for faster lookup (<a href="https://github.com/Vercidium/particles/blob/master/source/ModelHelper.cs">source code</a>).</p><p>At the time of writing this article, I decided to benchmark these systems again and it seems that optimisations in the .NET runtime mean that <code>Math.Sin(...)</code> is now the fastest method.</p><p>Here are the results of calculating 30 million sin and cos values:</p><figure class="kg-card kg-image-card"><img src="https://vercidium.com/blog/content/images/2021/02/image-7.png" class="kg-image" alt="OpenGL Particle Systems" loading="lazy"></figure><p>Based on these measurements I have switched back to using the base <code>Math.Sin(...)</code> and <code>Math.Cos(...)</code> functions.</p><h2 id="particle-matrix">Particle Matrix</h2><p>The old particle system worked with traditional 4x4 matrices, however the values <code>M13, M14, M24, M34 and M44</code> were never used:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public static Matrix4F OldParticleMatrix(in float scale, in float rotationX, in float rotationZ, in Vector3 position)
{
    // Get the sin and cos values for the X and Z rotation amount
    GetSinAndCosCheap(rotationX, out float sinX, out float cosX);
    GetSinAndCosCheap(rotationZ, out float sinZ, out float cosZ);

    Matrix4F m = Matrix4F.Identity;

    m.M11 = scale * cosZ;
    m.M12 = scale * sinZ;

    m.M21 = -scale * cosX * sinZ;
    m.M22 = scale * cosX * cosZ;
    m.M23 = scale * sinX;

    m.M31 = scale * sinX * sinZ;
    m.M32 = -scale * sinX * cosZ;
    m.M33 = scale * cosX;

    m.M41 = (float)position.X;
    m.M42 = (float)position.Y;
    m.M43 = (float)position.Z;

    return m;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The new particle system uses a custom 3x4 Matrix, which stores the same values but without the unused variables. </p><!--kg-card-begin: markdown--><pre><code class="language-csharp">float a = p.renderScale;

float sX = (float)Math.Sin(p.rotation.X);
float cX = (float)Math.Cos(p.rotation.X);
float sZ = (float)Math.Sin(p.rotation.Z);
float cZ = (float)Math.Cos(p.rotation.Z);

float acZ = a * cZ;
float acX = a * cX;
float asX = a * sX;

// Custom Matrix3x4 that uses less operations in the vertex shader
transform.M11 = acZ;
transform.M12 = -a * sZ;

transform.M13 = acX * sZ;
transform.M14 = acX * cZ;
transform.M21 = -asX;

transform.M22 = asX * sZ;
transform.M23 = acZ * sX;
transform.M24 = acX;

transform.M31 = p.position.X;
transform.M32 = p.position.Y;
transform.M33 = p.position.Z;
</code></pre>
<!--kg-card-end: markdown--><p></p><p>I then wrote the full <code>Scale * RotationX * RotationZ * Translation * MVP</code> matrix out on paper and asked my mathematician brother to simplify it down and remove any unnecessary additions and multiplications. This produced the following behemoth in the vertex shader, which improved the frames-per-second of rendering 50,000 particles from the low 130&apos;s to the high 140&apos;s on my laptop:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">uniform mat4 mvp;

layout (location = 0) in vec4 aPosition;
layout (location = 1) in vec4 aColour;
layout (location = 2) in mat4 aTransform;

flat out vec4 colour;

void main()
{
    vec4 a0 = aTransform[0];
    vec4 a1 = aTransform[1];
    vec4 a2 = aTransform[2];

    vec4 b0 = mvp[0];
    vec4 b1 = mvp[1];
    vec4 b2 = mvp[2];
    vec4 b3 = mvp[3];

    vec3 v = aPosition.xyz;

    gl_Position = vec4((a0.x * b0.x)          * v.x + (a0.z * b0.x + a0.w * b1.x + a1.x * b2.x) * v.y + (a1.y * b0.x + a1.w * b2.x)               * v.z + (a2.x * b0.x + a2.z * b2.x + b3.x),
                  (a0.x * b0.y + a0.y * b1.y) * v.x + (a0.z * b0.y + a0.w * b1.y + a1.x * b2.y) * v.y + (a1.y * b0.y + a1.z * b1.y + a1.w * b2.y) * v.z + (a2.x * b0.y + a2.y * b1.y + a2.z * b2.y + b3.y),
                  (a0.x * b0.z + a0.y * b1.z) * v.x + (a0.z * b0.z + a0.w * b1.z + a1.x * b2.z) * v.y + (a1.y * b0.z + a1.z * b1.z + a1.w * b2.z) * v.z + (a2.x * b0.z + a2.y * b1.z + a2.z * b2.z + b3.z),
                  (a0.x * b0.w + a0.y * b1.w) * v.x + (a0.z * b0.w + a0.w * b1.w + a1.x * b2.w) * v.y + (a1.y * b0.w + a1.z * b1.w + a1.w * b2.w) * v.z + (a2.x * b0.w + a2.y * b1.w + a2.z * b2.w + b3.w));
    
    colour = aColour;

}
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="particle-model">Particle Model</h2><p>The old system used a particle cube model with 36 vertices (2 triangles on each face) which was rendered with <code>GL_TRIANGLES</code>.</p><p>The new system uses a cube model composed of 14 vertices rendered with <code>GL_TRIANGLE_STRIP</code>, which renders ~2x faster on the GPU than previously. However since each side of the cube now shares two vertices with another side, the normal values are interpolated incorrectly across multiple faces.</p><p>This was solved by using the <code>flat out</code> and <code>flat in</code> qualifiers in the vertex and fragment shaders and adjusting the order of normals in the triangle strip so that the <a href="https://www.khronos.org/opengl/wiki/Primitive#Provoking_vertex">provoking vertex</a> is aligned correctly with each face of the cube.</p><blockquote>When using flat-shading on output variables, only outputs from the <strong>provoking vertex</strong> are used; every fragment generated by that primitive gets its input from the output of the provoking vertex - <a href="https://www.khronos.org/opengl/wiki/Primitive#Provoking_vertex">Khronos</a></blockquote><h1 id="conclusion">Conclusion</h1><p>With the optimisations detailed in the above sections, the new particle system benefits from:</p><ul><li>reduced memory usage due to smaller structs and vertices</li><li>faster particle creation with batch additions</li><li>faster particle deletion with swap and pop</li><li>faster rendering on the GPU due to less vertices, faster matrix operations</li><li>less time waiting for Tasks to finish</li><li>no OpenGL sync points</li></ul><p>You can see the particle system in action in the launch trailer for Sector&apos;s Edge, which has now <a href="https://sectorsedge.com/s/6ccl">released free to play on Steam</a>!</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/fRzvh8K9zEA?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Particle Optimisations]]></title><description><![CDATA[This article explains the optimisations made to the particle system in Sector's Edge that allow tens of thousands of particles to be ray-marched, buffered and rendered each frame.]]></description><link>https://vercidium.com/blog/particle-optimisations/</link><guid isPermaLink="false">61190945d364ab77972ea25b</guid><category><![CDATA[optimisations]]></category><category><![CDATA[particle]]></category><category><![CDATA[gpu]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Thu, 20 Feb 2020 13:58:27 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2020/02/particleArticle-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2020/02/particleArticle-1.jpg" alt="Particle Optimisations"><p>This article explains the optimisations made to the particle system in Sector&apos;s Edge that allow tens of thousands of particles to be ray-marched, buffered and rendered each frame.</p><p><a href="https://www.youtube.com/watch?v=Dklpzrg1Zko">Particles in Sector&apos;s Edge</a> are small cubes that are created from explosions, block destruction, projectile trails, player damage, rain and other causes. The main features of the particle system are:</p><ul><li>Updating particles across multiple threads</li><li>Ray-marching a voxel map</li><li>Writing directly to GPU memory across multiple threads</li><li>Fast matrix calculations</li><li>Fast creation and destruction of particles</li></ul><p>The full source code for this article is available <a href="https://github.com/Vercidium/particles">here on GitHub</a>.</p><h2 id="terminology">Terminology</h2><p>The following terms are referenced often throughout the article:</p><ul><li>Active Particle - a particle that is rendered and collides with the map</li><li>Particle lifetime - how many milliseconds a particle will be visible for</li><li>Particle decay - the process of a particle&apos;s lifetime reducing to zero over time</li><li>Decayed Particle - a particle that has completely decayed and moved to temporary storage for later re-use</li></ul><h2 id="particle-storage-structure">Particle Storage Structure</h2><p>Each thread separately manages its own linked list of particles. As hundreds of particles are created and destroyed each frame, using a linked list allows this to happen quickly.</p><p>When a particle is created it is in the <code>active</code> state, meaning it should be rendered and collide with the map. Once a particle&apos;s lifetime reaches zero, it has <code>decayed</code> and is moved to a separate linked list. When creating a new active particle, we re-use a decayed particle. This puts less pressure on the C# garbage collector.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">const int MAX_THREAD_COUNT = 128;

// For clarity, any variable that is used by multiple threads is prefixed with t_
int t_ActiveParticleCount = new int[MAX_THREAD_COUNT];
Particle[] t_ActiveParticles = new Particle[MAX_THREAD_COUNT];
Particle[] t_DecayedParticles = new Particle[MAX_THREAD_COUNT];
</code></pre>
<!--kg-card-end: markdown--><p></p><blockquote>Note the first particle in each linked list will not be rendered and is only used for managing the linked list.</blockquote><p>The particle class has the following structure:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class Particle
{
    // Base particle values
    public Vector3 position;
    public Vector3 velocity;
    public Vector3 rotation;
    public float scale;
    public uint colour;
    
    // As an optimisation, the transform matrix can be precalculated and stored here
    // (explained in the Matrix section below)
    public Matrix4F transform;
    
    // When this value reaches 0, the particle has decayed
    public float lifeTime;

    // The linked list
    public Particle Next;

    public void AddNext(Particle p)
    {
        if (Next == null)
        {
            // Start the linked list
            Next = p;
        }
        else
        {
            // Insert the particle at the start of the linked list
            p.Next = Next;
            Next = p;
        }
    }

    // Remove the particle at the start of the linked list
    public void PopNext()
    {
        Next = Next.Next;
    }
}</code></pre>
<!--kg-card-end: markdown--><p></p><p>We use the <code>ThreadPool</code> to determine how many threads are available and therefore how many linked lists we should use:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">void GetThreadCount()
{
    ThreadPool.GetMinThreads(out int count, out _);
    THREAD_COUNT = count;
    THREAD_COUNT_MINUS_ONE = count - 1;
}

int THREAD_COUNT;
int THREAD_COUNT_MINUS_ONE;
</code></pre>
<!--kg-card-end: markdown--><p></p><p>Particles must be distributed evenly across all threads, so we use the <code>CurrentThreadAddIndex</code> variable to keep track of which linked list the next particle should be inserted into:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">int _ctai;
public int CurrentThreadAddIndex
{
    get
    {
        if (_ctai == THREAD_COUNT_MINUS_ONE)
            _ctai = 0;
        else
            _ctai++;

        return _ctai;
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>When creating a new particle we will attempt to re-use a decayed particle to save time allocating memory:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">protected void AddParticle(in Vector3 position, in Vector3 velocity, in float scale, in uint colour, in float lifeTime)
{
    int index = CurrentThreadAddIndex;

    var p = t_DecayedParticles[index].Next;

    // If there are no decayed particles available, allocate a new one
    if (p == null)
    {
        p = new Particle()
        {
            position = position,
            velocity = velocity,
            scale = scale,
            colour = colour,
            lifeTime = lifeTime,
        };
    }
    else
    {
        // Modify an existing decayed particle
        p.position = position;
        p.velocity = velocity;
        p.scale = scale;
        p.colour = colour;
        p.lifeTime = lifeTime;

        // Remove the particle from the decayed linked list
        t_DecayedParticles[index].PopNext();

        // Disconnect the particle from the decayed linked list
        p.Next = null;
    }

    // Add the particle to the current active linked list
    t_ActiveParticles[index].AddNext(p);

    // Keep track of how many particles are in this active linked list
    // so that we can allocate the correct buffer size on the GPU
    t_ActiveParticleCount[index]++;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="calculating-and-buffering-particle-data">Calculating and Buffering Particle Data</h2><h3 id="vertex-data">Vertex Data</h3><p>As all particles share the same cube shape, we can render them using instancing with OpenGL. The base particle vertex has the following structure:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">byte PositionX;
byte PositionY;
byte PositionZ;
byte Normal;
</code></pre>
<!--kg-card-end: markdown--><p></p><p>Two instance buffers are also used to store the colour (uint) and transformation (mat4) of each particle. The particle&apos;s colour is determined when it is created, but its transformation must be recalculated each frame based on its position, rotation and scale.</p><h3 id="calculating-particle-transformations">Calculating Particle Transformations</h3><p>To calculate the particle&apos;s transformation, we have to create a position, rotationX, rotationZ and scale matrix and combine them together. Multiplying four matrices together requires 192 multiplications and 144 additions, however by combining these matrices on paper we have created our own &apos;Particle Matrix&apos; that requires less operations:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public static Matrix4F ParticleMatrix(in float scale, in float rotationX, in float rotationZ, in Vector3 position)
{
    // Access sin/cos values from an array rather than with Math.Sin(...)
    GetSinAndCosCheap(rotationX, out float sinX, out float cosX);
    GetSinAndCosCheap(rotationZ, out float sinZ, out float cosZ);

    Matrix4F m = Matrix4F.Identity;

    m.M11 = scale * cosZ;
    m.M12 = scale * sinZ;

    m.M21 = -scale * cosX * sinZ;
    m.M22 = scale * cosX * cosZ;
    m.M23 = scale * sinX;

    m.M31 = scale * sinX * sinZ;
    m.M32 = -scale * sinX * cosZ;
    m.M33 = scale * cosX;

    m.M41 = (float)position.X;
    m.M42 = (float)position.Y;
    m.M43 = (float)position.Z;

    return m;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>This particle matrix requires 14 multiplications and runs 5.83x faster than the original matrix multiplications. The two methods were benchmarked by calculating the final transformation matrix for 524288 random scale, rotationX, rotationZ and position values:</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th></th>
<th>Matrix Multiplication</th>
<th>Particle Matrix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Average</td>
<td>266.9ms</td>
<td>45.8ms</td>
</tr>
<tr>
<td>Worst</td>
<td>507.3ms</td>
<td>112.9ms</td>
</tr>
<tr>
<td>Best</td>
<td>251.0ms</td>
<td>41.9ms</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h3 id="sin-and-cos-precalculation">Sin and Cos Precalculation</h3><p>Accessing precalculated sine wave values from an array was found to be 2.07x faster than using <code>Math.Sin(...)</code>:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Decreasing this value will increase the accuracy of the precalculated sine wave
private const float epsilon = 0.001f;

private static float[] sinWave;

// Precalculate the sin wave
public static void InitialiseTrigonometry()
{
    int elements = (int)(Math.PI * 2 / epsilon) + 1;
    sinWave = new float[elements];

    int i = 0;
    for (double a = 0; a &lt;= Math.PI * 2; a += epsilon)
    {
        sinWave[i] = (float)Math.Sin(a);
        i++;
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>Both sine and cosine values can be accessed from this array. Cosine values are accessed from this array by shifting the array access to the right:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">private const float minusHalfPI = (float)-Math.PI / 2;
private const float halfPI = (float)Math.PI / 2;
private const float doublePI = (float)Math.PI * 2;

// 1571 = PI / 2 / 0.0001
// 6283 = PI * 2 / 0.0001 (number of elements in the array)
public static void GetSinAndCosCheap(float t, out float sin, out float cos)
{
    if (t &lt; minusHalfPI)
    {
        int access = (int)(-t % doublePI / epsilon);
        sin = -sinWave[access];
        cos = -sinWave[(access + 1571) % 6283];
    }
    else if (t &lt; 0)
    {
        sin = -sinWave[(int)(-t % doublePI / epsilon)];
        cos = sinWave[(int)((t + halfPI) % doublePI / epsilon)];
    }
    else
    {
        int access = (int)(t % doublePI / epsilon);
        sin = sinWave[access];
        cos = sinWave[(access + 1571) % 6283];
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The two methods were benchmarked by calculating the sine and cosine values for 524288 random numbers in the range -50 to 50:</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th></th>
<th>Math.Sin/Cos</th>
<th>Array</th>
</tr>
</thead>
<tbody>
<tr>
<td>Average</td>
<td>59.5ms</td>
<td>28.6ms</td>
</tr>
<tr>
<td>Worst</td>
<td>116.3ms</td>
<td>70.0ms</td>
</tr>
<tr>
<td>Best</td>
<td>49.3ms</td>
<td>21.7ms</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h2 id="buffering-particle-data">Buffering Particle Data</h2><p>Every frame the <code>uint</code> and <code>Matrix4F</code> instance data must be buffered to the GPU for rendering. Originally, this data was stored in memory and sent to the GPU with <code>glSubBufferData(...)</code>, however this was found to be 3.3x slower than writing directly to shared GPU memory with <code>glMapBuffer(...)</code>. The difference in speed comes from the fact that the original method involved writing to memory twice:</p><ul><li>writing individual values to an array - which has inherit bounds checks</li><li>copying the entire array to shared memory with <code>glSubBufferData(...)</code></li></ul><p>The new method involves getting a pointer to shared memory with <code>glMapBuffer(...)</code> and writing directly to that pointer. The pointer can be shared by multiple threads as they are each writing to a different section of GPU memory.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Get pointers to both VBO&apos;s data
ParticleColourBuffer.Bind();
var cPtrBase = Gl.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite);
ParticleMatrixBuffer.Bind();
var mPtrBase = Gl.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite);

unsafe
{
    // Convert IntPtr to actual pointers
    uint* cPtr = (uint*)cPtrBase.ToPointer();
    Matrix4F* mPtr = (Matrix4F*)mPtrBase.ToPointer();

    // Example write
    cPtr[0] = 100;
    mPtr[3] = ModelHelper.ParticleMatrix(...);
}

// Finish writing to both VBO&apos;s
ParticleColourBuffer.Bind();
Gl.UnmapBuffer(BufferTarget.ArrayBuffer);
ParticleMatrixBuffer.Bind();
Gl.UnmapBuffer(BufferTarget.ArrayBuffer);
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="updating-particles">Updating Particles</h2><p>Checking collision detection with the map, removing decayed particles and updating particle transformation matrices is handled across multiple threads with <code>Tasks</code>. As each thread is writing the colours and transformations of each particle directly to GPU memory, we need to first calculate the pointer offset for each thread.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">void UpdateParticles(double elapsed)
{
    int[] pointerOffsets = new int[THREAD_COUNT];
    int particleCount = 0;

    // Calculate the VBO write offset for each thread
    for (int i = 0; i &lt; THREAD_COUNT; i++)
    {
        pointerOffsets[i] = particleCount;
        particleCount += t_ActiveParticleCount[i];
    }

    if (particleCount == 0)
        return;

    // Check there is enough memory allocated for the VBO
    ParticleMatrixBuffer.Expand(particleCount);
    ParticleColourBuffer.Expand(particleCount);

    var tasks = new List&lt;Task&gt;();
    
    ParticleColourBuffer.Bind();
    var cPtrBase = Gl.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite);
    ParticleMatrixBuffer.Bind();
    var mPtrBase = Gl.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite);

    unsafe
    {
        uint* cPtr = (uint*)cPtrBase.ToPointer();
        Matrix4F* mPtr = (Matrix4F*)mPtrBase.ToPointer();

        for (int i = 0; i &lt; THREAD_COUNT; i++)
        {
            if (t_ActiveParticles[i].Next != null)
            {
                int index = i;
                int pointerOffset = pointerOffsets[i];
                
                tasks.Add(Task.Run(() =&gt; UpdateParticleRange(cPtr, mPtr, index, pointerOffset, elapsed)));
            }
        }
    }

    ParticleColourBuffer.Bind();
    Gl.UnmapBuffer(BufferTarget.ArrayBuffer);
    ParticleMatrixBuffer.Bind();
    Gl.UnmapBuffer(BufferTarget.ArrayBuffer);

    // Wait for all particles to be updated
    Task.WaitAll(tasks.ToArray());
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>We use ray marching to handle particle collision with our voxel map. The voxel ray marching algorithm we use is based on <a href="http://www.cse.chalmers.se/edu/year/2010/course/TDA361/grid.pdf">this paper</a> by John Amanatides and Andrew Woo and has been optimised by keeping block lookups within the current working chunk. This algorithm is quite detailed and is explained in our <a href="https://vercidium.com/blog/optimised-voxel-raymarching/">other blog post here</a>.</p><p>The <code>UpdateParticleRange(...)</code> function has four stages:</p><ul><li>Precalculate common variables</li><li>Handle decayed particles</li><li>Update particles + ray-marching</li><li>Write particle data to the GPU</li></ul><p>The first stage is fairly straightforward and involves precalculating common variables and array accesses that will be used when updating each particle:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">unsafe void UpdateParticleRange(uint* cPtr, Matrix4F* mPtr, int index, int bufferOffset, double elapsed)
{
    Particle p = t_ActiveParticles[index];
    Particle q = p.Next;
            
    // Precalculate variables
    double thisGravity = Constants.Gravity * elapsed;
    double scaleMultiplier = Math.Pow(0.9, elapsed / 16d);
    float elapsedF = (float)elapsed;
    float elapsed1500 = elapsedF / 1500;

    // Allocate variables
    bool hit = false;
    Axis axis = Axis.None;
            
    // Dereference array accesses
    ref int particleCount = ref t_ActiveParticleCount[index];
    var temporaryParticles = t_DecayedParticles[index];

    while (q != null)
    {
        // Update particles
        ...
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>To show that a particle is decaying, it will shrink during the last 1.5s of its lifetime. Once it is virtually invisible, we will move it to the decayed linked list. As this particle was included in the total active particle count at the time we were calculating pointer offsets in <code>UpdateParticles(...)</code>, we must write <code>Matrix4F.Hidden</code> to the instance buffer to prevent the particle from rendering using a transform value from the last frame.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">unsafe void UpdateParticleRange(uint* cPtr, Matrix4F* mPtr, int index, int bufferOffset, double elapsed)
{
    // Part 1
    ...
    
    while (q != null)
    {
        bool shrinking = false;

        // Particles shrink during the last 1.5s of their lifetime
        if (q.lifeTime &lt; 1500)
        {
            q.scale -= elapsed1500;

            // If the particle is virtually invisible, remove it
            if (q.scale &lt; 0.03)
            {
                // Hide the particle
                mPtr[bufferOffset++] = Matrix4F.Hidden;

                // Remove the particle from the active linked list
                p.Next = q.Next;
                q.Next = null;

                // Add the particle to temporary storage
                temporaryParticles.AddNext(q);
                particleCount--;

                // Get the next active particle
                q = p.Next;
                continue;
            }

            shrinking = true;
        }

        // Handle active particles
        ...
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>Handling active particles involves ray-marching the particle and updating its position, velocity, rotation and lifetime.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">unsafe void UpdateParticleRange(uint* cPtr, Matrix4F* mPtr, int index, int bufferOffset, double elapsed)
{
    // Part 1
    ...
    
    while (q != null)
    {
        // Part 2
        ...
        
        var elapsedVelocity = q.velocity * elapsed;
        var mag = elapsedVelocity.Magnitude;

        // Only raymarch if the particle is moving
        if (mag &gt; 0.000001)
            map.RayMarch(q.position, elapsedVelocity, mag, ref hit, ref axis);
        else
            hit = false;

        if (hit)
        {
            // Reflect and dampen the particle&apos;s velocity based on which axis it collided on
            q.velocity *= Constants.AxisToBounce[(int)axis];
        }
        else if (!map.NoBlock((int)q.position.X, (int)q.position.Y, (int)q.position.Z))
        {
            // Slide to a stop along the ground
            q.velocity.X *= Constants.ParticleSlideDampening;
            q.velocity.Z *= Constants.ParticleSlideDampening;
        }
        else
        {
            q.velocity.Y += thisGravity;
        }

        // Recalculate elapsedVelocity
        elapsedVelocity = q.velocity * elapsed;

        q.rotation -= elapsedVelocity;
        q.lifeTime -= elapsedF;
        
        // Part 4
        ...
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>As an optimisation, particle transformations are only calculated if the particle is shrinking or moving. Quite often particles are at rest on the ground before shrinking, so this optimisation saves us some time.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">unsafe void UpdateParticleRange(uint* cPtr, Matrix4F* mPtr, int index, int bufferOffset, double elapsed)
{
    // Part 1
    ...
    
    while (q != null)
    {
        // Part 2 + 3
        ...
        
        // Only update the transform if the particle is shrinking or moving
        if (shrinking || elapsedVelocity.Magnitude &gt; 0.000001)
        {
            q.position += elapsedVelocity;
            q.transform = ModelHelper.ParticleMatrix(q.scale, (float)q.rotation.X, (float)q.rotation.Z, q.position);
        }

        // Write the colour and transformation matrix directly to shared memory
        cPtr[bufferOffset] = q.colour;
        mPtr[bufferOffset++] = q.transform;

        // Advance the linked list
        q = q.Next;
        p = p.Next;
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="rendering-particles">Rendering Particles</h2><p>Since all data has now been buffered to the GPU, rendering particles is quite straightforward:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">protected void RenderParticles()
{
    // Each particle VBO and instance VBO shares the same VAO
    // Bind the VAO
    Gl.BindVertexArray(ParticleMatrixBuffer.arrayHandle);

    // Draw the particles
    Gl.DrawArraysInstanced(PrimitiveType.Triangles, 0, 36, TotalParticleCount());

    // Unbind the VAO
    Gl.BindVertexArray(0);
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The vertex shader is as follows:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">#version 330

uniform mat4 mvp;

layout (location = 0) in vec4 aPosition;
layout (location = 1) in vec4 aColour;
layout (location = 2) in mat4 aTransform;

flat out vec4 colour;

void main()
{
    gl_Position = mvp * aTransform * vec4(aPosition.xyz, 1.0);

    colour = aColour;

    int normal = int(aPosition.w);
	
    // Slightly darken the sides of the particle
    if (normal &gt; 3)
        colour *= 0.8;
    else if (normal &gt; 1)
        colour *= 0.9;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>In this case, the normal is an index that represents the Y+, Y-, X+, X-, Z+ and Z- unit vectors. For advanced lighting, the <code>vec3</code> normal can be determined using either branching or an array access, however <code>const vec3</code> arrays have <a href="https://community.khronos.org/t/constant-vec3-array-no-go/60184/7">poor performance on some GPUs</a>.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">int n = int(aPosition.w);
vec3 normal;

// Option 1 - branching
if (n == 0)
    normal = vec3(0, 1, 0);
else if (n == 1)
    normal = vec3(0, -1, 0);
else if (n == 2)
    normal = vec3(1, 0, 0);
else if (n == 3)
    normal = vec3(-1, 0, 0);
else if (n == 4)
    normal = vec3(0, 0, 1);
else
    normal = vec3(0, 0, -1);
   
// Option 2 - array access
const vec3 NORMALS = ( vec3(0,1,0), vec3(0,-1,0), vec3(1,0,0), vec3(-1,0,0), vec3(0,0,1), vec3(0,0,-1) );
normal = NORMALS[n];

// Rotate the normal
vec3 normal = normalize(mat3(transpose(inverse(aTransform))) * N);
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The fragment shader then writes the particle colour to the framebuffer:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">#version 330

out vec4 gColor;

flat in vec4 colour;

void main()
{
    gColor = colour;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="in-practice">In Practice</h2><p>On a Ryzen 5 1600 CPU and GTX 960 GPU, the game can handle 33,000 particles before dropping below 60FPS.</p><p>The full source code for this article is available <a href="https://github.com/Vercidium/particles">here on GitHub</a>.</p><p>Every update to the game comes with many hours of testing between my brother and I to check for visual artifacts, bugs and crashes. You can see the particle system in action in our Developer Highlights video below, as well as lots of headshots and lucky shots!</p><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/Dklpzrg1Zko?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Optimised CPU Ray Marching in a Voxel World]]></title><description><![CDATA[This article explains how we optimised our ray marching algorithm that handles collision detection for tens of thousands of particles.]]></description><link>https://vercidium.com/blog/optimised-voxel-raymarching/</link><guid isPermaLink="false">61190945d364ab77972ea25c</guid><category><![CDATA[voxel]]></category><category><![CDATA[optimisations]]></category><category><![CDATA[raymarching]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Sat, 18 Jan 2020 03:12:22 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2020/01/raymarching.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2020/01/raymarching.jpg" alt="Optimised CPU Ray Marching in a Voxel World"><p>This article explains how we optimised the ray marching algorithm in <a href="https://www.youtube.com/watch?v=qoKzhIouzsk">Sector&apos;s Edge</a> that handles collision detection for tens of thousands of particles.</p><p>The full source code for this article is available <a href="https://github.com/Vercidium/voxel-ray-marching">here on GitHub</a>.</p><h2 id="overview">Overview</h2><p>The voxel world is divided into groups of 32 x 32 x 32 blocks, each of which are managed by a separate <code>Chunk</code> class instance. This structure is especially useful for:</p><ul><li>Rendering optimisations, as chunks outside the current viewport can be skipped</li><li><a href="https://vercidium.com/blog/voxel-world-optimisations/">Fast mesh regeneration</a> when blocks in a chunk are changed</li><li>Memory optimisations, as chunks that contain no blocks are left uninitialised</li></ul><p>This voxel ray marching algorithm is based on <a href="http://www.cse.chalmers.se/edu/year/2010/course/TDA361/grid.pdf">A Fast Voxel Traversal Algorithm for Ray Tracing</a> by <em>John Amanatides</em> and <em>Andrew Woo</em> and has been optimised by keeping block lookups within the current working chunk. As this algorithm is quite long, we will analyse it in sections.</p><h2 id="map-structure">Map Structure</h2><p><code>Chunks</code> are stored in a three-dimensional array within the <code>Map</code> class, however <code>Blocks</code> are stored in a one-dimensional array in the <code>Chunk</code> for fast lookup:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class Map
{
    Chunk[,,] chunks;
}

public class Chunk
{
    const int CHUNK_SIZE = 32;
    const int CHUNK_SIZE_SQUARED = 1024;
    const int CHUNK_SIZE_CUBED = 32768;
    Block[] data = new Block[CHUNK_SIZE_CUBED];
}

public struct Block
{
    byte kind;  // e.g. empty, dirt, metal, etc.
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p><code>Blocks</code> within the <code>Chunk</code> are accessed as follows:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">int access = j + i * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED;
Block b = data[access];
</code></pre>
<!--kg-card-end: markdown--><p></p><p><em><em>For clarity, <code>i, j and k</code> refer to chunk-relative positions (range 0-31)</em></em><br><em><em>and <code>x, y and z</code> refer to </em>map-relative<em> positions (range 0-511)</em></em></p><h2 id="ray-marching">Ray Marching</h2><p>The problem with the original ray marching algorithm in Sector&apos;s Edge was that it worked with map-relative positions. This meant that every time the algorithm checked if a block existed at a specific position, it had to calculate which chunk the block belonged to and then convert the map-relative position to a chunk-relative position:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">const int SHIFT = 5;
const int MASK = 0x1f;
public Block GetBlock(int x, int y, int z)
{
    // Get the chunk the block belongs to
    var c = chunks[x &gt;&gt; SHIFT, y &gt;&gt; SHIFT, z &gt;&gt; SHIFT];

    if (c == null)
        return true;

    // Bitmask to get the chunk-relative position
    return c.data[(y &amp; MASK) +
                  (x &amp; MASK) * CHUNK_SIZE +
                  (z &amp; MASK) * CHUNK_SIZE_SQUARED];
}</code></pre>
<!--kg-card-end: markdown--><p></p><p>The new algorithm obtains a reference to the chunk at the beginning of the function and only updates it when the ray enters a new chunk. This chunk is referred to as the <code>current working chunk</code>.</p><p>The second optimisation was to add two exit checks at the beginning of the function that check if the ray starts outside the map, or if the ray begins and ends at the same map-relative position:</p><ul><li>Exit if the ray starts outside the map bounds as there are no blocks to collide with. There will never be a case where the ray starts outside the map and travels towards the centre of the map</li><li>Exit if the start and end position of the ray both lie on the same coordinate on the voxel grid. This check saves a lot of time for particles, as they are often at rest on the top of a block</li></ul><p>The <code>RayMarch(...)</code> function parameters are as follows:</p><ul><li><code>in Vector3 start</code> - the start position of the ray</li><li><code>Vector3 velocity</code> - the direction of the ray</li><li><code>in double max</code> - the maximum length that the ray can travel</li><li><code>ref bool hit</code> - set to true if the ray collides with a solid block</li><li><code>ref Axis axis</code> - set to either <code>Axis.X, Axis.Y or Axis.Z</code> depending on which block face the ray hit</li></ul><!--kg-card-begin: markdown--><pre><code class="language-csharp">public void RayMarch(in Vector3 start, Vector3 velocity, in double max, ref bool hit, ref Axis axis)
{
    // Convert the start position to integer voxel coordinates
    int x = (int)start.X;
    int y = (int)start.Y;
    int z = (int)start.Z;

    // If the start coordinate is outside the map, there is nothing to hit.
    if (y &lt; 0 || y &gt;= Constants.MAP_SIZE_Y ||
        x &lt; 0 || x &gt;= Constants.MAP_SIZE_X ||
        z &lt; 0 || z &gt;= Constants.MAP_SIZE_Z)
    {
        hit = false;
        return;
    }
    
    // Determine the index of the current working chunk in the Map by
    // bit-shifting the map-relative block coordinates
    int chunkIndexX = x &gt;&gt; 5;
    int chunkIndexY = y &gt;&gt; 5;
    int chunkIndexZ = z &gt;&gt; 5;

    // Get a reference to the current working chunk
    var c = chunks[chunkIndexX, chunkIndexY, chunkIndexZ];

    // Determine the chunk-relative position of the ray with a bit-mask
    int i = x &amp; 0x1f;
    int j = y &amp; 0x1f;
    int k = z &amp; 0x1f;

    // Calculate the access position of this block in the Chunk&apos;s data[] array
    int access = j + i * Constants.CHUNK_SIZE + k * Constants.CHUNK_SIZE_SQUARED;

    // Calculate the end position of the ray
    var end = start + velocity;

    // If the start and end positions of the ray both lie on the same coordinate on the voxel grid
    if (x == (int)end.X &amp;&amp; y == (int)end.Y &amp;&amp; z == (int)end.Z)
    {
        // The chunk is null if it contains no blocks
        if (c == null)
        {
            hit = false;
        }

        // If the block is empty
        else if (c.data[access].kind == 0)
        {
            hit = false;
        }

        // Else the ray begins and ends within the same non-empty block
        else
        {
            hit = true;
        }
        
        return;
    }
    
    ...
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>After this stage we can confirm that the ray will start and end at different voxel coordinates and therefore a ray march is required. To improve ray marching efficiency, we pre-calculate the following variables:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// These variables are used to determine whether the ray has left the current working chunk.
//  For example when travelling in the negative Y direction,
//  if j == -1 then we have left the current working chunk
int iComparison, jComparison, kComparison;

// When leaving the current working chunk, the chunk-relative position must be reset.
//  For example when travelling in the negative Y direction,
//  j should be reset to CHUNK_SIZE - 1 when entering the new current working chunk
int iReset, jReset, kReset;

// When leaving the current working chunk, the access variable must also be updated.
//  These values store how much to add or subtract from the access, depending on
//  the direction of the ray:
int xAccessReset, yAccessReset, zAccessReset;

// The amount to increase i, j and k in each axis (either 1 or -1)
int iStep, jStep, kStep;

// When incrementing j, the chunk access is simply increased by 1
// When incrementing i, the chunk access is increased by 32 (CHUNK_SIZE)
// When incrementing k, the chunk access is increased by 1024 (CHUNK_SIZE_SQUARED)
// These variables store whether to increase or decrease by the above amounts
int xAccessIncrement, zAccessIncrement;

// The distance to the closest voxel boundary in map units
double xDist, yDist, zDist;

if (velocity.Y &gt; 0)
{
    jStep = 1;
    jComparison = Constants.CHUNK_SIZE;
    jReset = 0;
    yAccessReset = -Constants.CHUNK_SIZE;
    yDist = (y - start.Y + 1);
}
else
{
    jStep = -1;
    jComparison = -1;
    jReset = Constants.CHUNK_SIZE - 1;
    yAccessReset = Constants.CHUNK_SIZE;
    yDist = (start.Y - y);
}

// Same for velocity.X and velocity.Z
...

// This variable stores the current progress throughout the ray march
double t = 0.0;

velocity.Normalize();
double xInverted = Math.Abs(1 / velocity.X);
double yInverted = Math.Abs(1 / velocity.Y);
double zInverted = Math.Abs(1 / velocity.Z);

// Determine the distance to the closest voxel boundary in units of t
//  - These values indicate how far we have to travel along the ray to reach the next voxel
//  - If any component of the direction is perpendicular to an axis, the distance is double.PositiveInfinity
double xDistance = velocity.X == 0 ? double.PositiveInfinity : xInverted * xDist;
double yDistance = velocity.Y == 0 ? double.PositiveInfinity : yInverted * yDist;
double zDistance = velocity.Z == 0 ? double.PositiveInfinity : zInverted * zDist;</code></pre>
<!--kg-card-end: markdown--><p></p><p>The algorithm then runs in a loop until either:</p><ul><li><code>t</code> has reached the <code>max</code> length of the ray</li><li>a non-empty block is found</li><li>the ray exits the map</li></ul><!--kg-card-begin: markdown--><pre><code class="language-csharp">while (t &lt;= max)
{
    // Exit when we find a non-empty block
    if (c != null &amp;&amp; c.data[access].kind != 0)
    {
        hit = true;
        return;
    }
    
    // Determine the closest voxel boundary
    if (yDistance &lt; xDistance)
    {
        if (yDistance &lt; zDistance)
        {
            // Advance to the closest voxel boundary in the Y direction
            
            // Increment the chunk-relative position and the block access position
            j += jStep;
            access += jStep;

            // Check if we have exited the current working chunk.
            // This means that j is either -1 or 32
            if (j == jComparison)
            {
                // If moving in the positive direction, reset j to 0.
                // If moving in the negative Y direction, reset j to 31
                j = jReset;
                
                // Reset the chunk access
                access += yAccessReset;

                // Calculate the new chunk index
                chunkIndexY += jStep;

                // If the new chunk is outside the map, exit
                if (chunkIndexY &lt; 0 || chunkIndexY &gt;= Constants.CHUNK_AMOUNT_Y)
                {
                    hit = false;
                    return;
                }

                // Get a reference to the new working chunk
                c = chunks[chunkIndexX, chunkIndexY, chunkIndexZ];
            }

            // Update our progress in the ray 
            t = yDistance;
            
            // Set the new distance to the next voxel Y boundary
            yDistance += yInverted;
            
            // For collision purposes we also store the last axis that the ray collided with
            // This allows us to reflect particle velocity on the correct axis
            axis = Axis.Y;
        }
        else
        {
            // Advance to the closest voxel boundary in the Z direction
            ...
        }
    }
    else if (xDistance &lt; zDistance)
    {
        // Advance to the closest voxel boundary in the X direction
        ...
    }
    else
    {    
        // Advance to the closest voxel boundary in the Z direction
        ...
    }
}</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="benchmarks">Benchmarks</h2><p>The following benchmarks were run on a Ryzen 5 1600 CPU.</p><p>The average particle collision ray march in Sector&apos;s Edge travels 1-10 blocks and takes 250 nanoseconds to run. At 60 frames per second, this allows for 64000 rays per frame per thread.</p><p>For stress testing, 100,000 rays were cast in random directions across the map. The average ray length was 200-400 blocks and takes 3400 nanoseconds to run. At 60 frames per second, this allows for 4700 rays per frame per thread.</p><h2 id="in-practice">In Practice</h2><p>The full source code for this article is available <a href="https://github.com/Vercidium/voxel-ray-marching">here on GitHub</a>.</p><p>In Sector&apos;s Edge, ray marching is required for particle collisions and hit detection between projectiles and the map. See it in action in our latest video below:</p><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/qoKzhIouzsk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Lag Compensation - Fair Play for all Pings]]></title><description><![CDATA[This article explains the lag compensation system used in Sector's Edge that improves hit-detection accuracy for players with high ping time.]]></description><link>https://vercidium.com/blog/lag-compensation/</link><guid isPermaLink="false">61190945d364ab77972ea25a</guid><category><![CDATA[networking]]></category><category><![CDATA[collision]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Sun, 12 Jan 2020 08:46:07 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2020/01/lagCompensationHeader.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2020/01/lagCompensationHeader.jpg" alt="Lag Compensation - Fair Play for all Pings"><p>In multiplayer games it is common for players to have high ping times to the game server. This means that the world the player sees is slightly behind what the server sees and this leads to problems with hit detection.</p><p>This article explains the lag compensation system used in <a href="https://www.youtube.com/watch?v=7J8u3h0lvrQ">Sector&apos;s Edge</a> that improves hit-detection accuracy for players with high ping time.</p><p>The full source code for this system is available <a href="https://github.com/Vercidium/lag-compensation">on GitHub here</a>.</p><h2 id="terminology">Terminology</h2><ul><li>Tick - a 50 millisecond time span</li><li>Tick Lag - how many Ticks the client is behind the server</li><li>Player - a player-controlled character</li><li>Entity - a moving player, grenade, scanner, etc.</li><li>Record - an object that stores the state of all entities at a certain timestamp</li><li>History - the object that manages lag compensation and stores <code>Records</code></li><li>Message - a network message sent from the client to the server</li></ul><h2 id="overview">Overview</h2><p>Sector&apos;s Edge uses the <a href="https://github.com/lidgren/lidgren-network-gen3">Lidgren C# networking library</a>, which provides the ping time for each player. Traditionally, we would use this ping time to calculate the player&apos;s Tick Lag, however this value can fluctuate and doesn&apos;t account for player position smoothing and other factors on the client.</p><p>Instead, the server stores a <code>Record</code> every tick (50ms) that contains information about every entity&apos;s position and state. The client periodically sends the position of a random moving entity to the server, whether it be a player, grenade, scanner, etc. The server will then use this position to determine the player&apos;s Tick Lag.</p><p><em>Records that are older than 400ms are discarded for anti-cheat purposes. Players with &gt; 400ms ping will not benefit from lag compensation.</em></p><p>For example, the server is currently running at Tick 9 and a client states that player K is at position 2.5 (the red dotted line):</p><figure class="kg-card kg-image-card"><img src="/blog/content/images/2020/01/lagCompensation-2.png" class="kg-image" alt="Lag Compensation - Fair Play for all Pings" loading="lazy"></figure><p>The server will then look through its <code>History</code> for player K and determine that the client is currently running at around Tick 7.25. This means that the client has a Tick Lag of 1.75 Ticks (about 88ms ping).</p><p><em>In the case that multiple matching positions are found, the most recent position will be used.</em></p><p>The past 20 Tick Lag values are stored for each <code>Player</code>. To minimise fluctuations, the average value of the Tick Lag history is used when calculating hit detection:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class Player
{
    const int TICK_TIME = 50;
    const int LAG_HISTORY_MAX = 20;    
    double ping;

    List&lt;double&gt; TickLagHistory = new List&lt;double&gt;();
    double AccumulatedTickLag = 0;

    public void AddTickLag(double d)
    {
        TickLagHistory.Add(d);
        
        AccumulatedTickLag += d;

        if (TickLagHistory.Count &gt; LAG_HISTORY_MAX)
        {
            AccumulatedTickLag -= TickLagHistory[0];
            TickLagHistory.RemoveAt(0);
        }
    }
    
    public double AverageTickLag
    {
        get
        {
            // Use ping as an approximation until TickLagHistory is populated
            if (TickLagHistory.Count &lt; LAG_HISTORY_MAX)
                return ping / TICK_TIME;

            return AccumulatedTickLag / LAG_HISTORY_MAX;
        }
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>It takes 1000ms for <code>TickLagHistory</code> to populate when a player first connects to the server. Until it is populated, the player&apos;s ping will be used as an approximation.</p><p><em>Note that <code>List&lt;double&gt;</code> can be replaced with a ring buffer as an optimisation.</em></p><h2 id="system-structure">System Structure</h2><p>The structure of the <code>GameServer</code>, <code>Record</code> and the <code>History</code> class are as follows:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class GameServer
{
    List&lt;Player&gt; Players;
    List&lt;Projectile&gt; Projectiles;
    History history;
    
    Stopwatch tickWatch = new Stopwatch();
    int PresentTick
    {
        get
        {
            return (int)(tickWatch.ElapsedMilliseconds / TICK_TIME);
        }
    }
}

public class Record
{
    public int tick;
    public List&lt;Player&gt; players = new List&lt;Player&gt;();

    // As there are 16 players max, we use a linear lookup
    public Player GetPlayer(byte ID)
    {
        for (int i = 0; i &lt; players.Count; i++)
            if (players[i].ID == ID)
                return players[i];

        return null;
    }
}

public class History
{
    GameServer parent;
    List&lt;Record&gt; records;
        
    // As there are 8 records max, we use a linear lookup
    Record GetRecord(int tick)
    {
        for (int i = 0; i &lt; records.Count; i++)
            if (records[i].tick == tick)
                return records[i];

        return records.Last();
    }
    
    Player GetPlayer(List&lt;Player&gt; players, byte ID) { ... }
    void ProjectileLoop(double tick, Projectile proj) { ... }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>We also rely on interpolating between two <code>Vector3</code> values, which is handled in a <code>Helper</code> class:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public static class Helper
{
    public static double Interpolate(double first, double second, double by)
    {
        return first + by * (second - first);
    }

    public static Vector3 Interpolate(in Vector3 first, in Vector3 second, double by)
    {
        double x = Interpolate(first.X, second.X, by);
        double y = Interpolate(first.Y, second.Y, by);
        double z = Interpolate(first.Z, second.Z, by);
        return new Vector3(x, y, z);
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="the-projectile-loop">The Projectile Loop</h2><p>When the server receives a mouse click message from the client, it simulates the projectile through each <code>Record</code> in the past until it reaches the present Tick. This is called the Projectile Loop.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">void HandleMouseDown(Player player)
{
    // Calculate the player&apos;s current tick
    double tick = PresentTick - player.AverageTickLag;   
    
    // Create a projectile
    Projectile proj = player.CreateProjectile();    
    
    // Process the projectile
    history.ProjectileLoop(tick, proj);
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The <code>ProjectileLoop</code> function recursively checks the projectile against each <code>Record</code> in its history, starting at the provided <code>tick</code>. If the projectile doesn&apos;t collide with anything in each <code>Record</code>, the projectile is moved to the current list of projectiles on the <code>GameServer</code>, which are then updated each frame in real time.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">void ProjectileLoop(double tick, Projectile proj)
{
    Record lastRecord = records.Last();
    
    // If we have checked each Record and the projectile is still alive, add it to the present
    if (tick &gt; lastRecord.tick)
    {
        parent.Projectiles.Add(proj);
        return;
    }

    bool repeat = false;

    // If tick is 4.75, we want to interpolate 75% between the two Records
    double interpolation = tick - (int)tick;

    // Interpolate between the most recent Record and the present
    if ((int)tick == lastRecord.tick)
    {
        repeat = ProcessCollision(lastRecord.players, parent.Players, interpolation, proj);
    }

    // Interpolate between two Records
    else 
    {
        var eOld    = GetRecord((int)tick);         // Less recent
        var eRecent = GetRecord((int)tick + 1);     // More recent

        repeat = ProcessCollision(eOld.players, eRecent.players, interpolation, proj);
    }

    // Some projectiles are hitcast, which means they have infinite velocity
    // and therefore we only need to check one frame
    if (proj.IsHitCast)
        return;

    // If the projectile didn&apos;t hit anything
    if (repeat)
    {
        // Move the projectile
        proj.Position += proj.Velocity * TICK_TIME;

        // Check the next most recent record
        ProjectileLoop(tick + 1, proj);
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The player&apos;s Tick Lag is not always a whole integer and therefore the <code>ProcessCollision</code> function must interpolate between the player positions stored in two <code>Records</code> for accurate hit detection:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">bool ProcessCollision(List&lt;Player&gt; playersOld, List&lt;Player&gt; playersRecent, double interpolation, Projectile proj)
{
    // We don&apos;t want to modify the player positions stored in the Record, so we use a new list
    List&lt;Player&gt; playerCopies = new List&lt;Player&gt;();

    foreach (var pOld in playersOld)
    {
        // Create a copy of the player containing only the information
        // we need for collision (position, velocity, crouching, aiming, etc)
        var pCopy = pOld.Copy();

        // Find a player with a matching ID in the more recent Record
        var pRecent = GetPlayer(playersRecent, pOld.ID);

        // If the player hasn&apos;t disconnected, interpolate their position
        // between the two ticks for more accurate collision detection
        if (pRecent != null)
            pCopy.position = Interpolate(pOld.position, pRecent.position, interpolation);

        playerCopies.Add(pCopy);
    }

    // Calculate collision, deal damage to the players and map, etc
    // Returns true if the projectile didn&apos;t hit anything
    return parent.DoCollision(proj, playerCopies);
}</code></pre>
<!--kg-card-end: markdown--><p></p><p><em>Note that the <code>parent.DoCollision()</code> function handles collision between projectiles, the voxel map and players. This will be covered in another article.</em></p><h2 id="in-practice">In Practice</h2><p>The full source code is available <a href="https://github.com/Vercidium/lag-compensation">on GitHub here</a>.</p><p>See the hit detection in action from the point of view of one of our best snipers:</p><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/7J8u3h0lvrQ?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Further Voxel World Optimisations]]></title><description><![CDATA[This article explains how we optimised chunk mesh generation down to 0.48ms per chunk.]]></description><link>https://vercidium.com/blog/further-voxel-world-optimisations/</link><guid isPermaLink="false">61190945d364ab77972ea259</guid><category><![CDATA[voxel]]></category><category><![CDATA[mesh]]></category><category><![CDATA[optimisations]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Sat, 11 Jan 2020 04:01:42 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2020/01/cover-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2020/01/cover-1.jpg" alt="Further Voxel World Optimisations"><p>This article is a continuation from the original article <a href="https://vercidium.com/blog/voxel-world-optimisations/">Voxel World Optimisations</a>, which details the initial methods used to increase chunk initialisation time in our destructive first person shooter <a href="https://sectorsedge.com">Sector&apos;s Edge</a>.</p><p>In the previous article, the benchmark for initialising one chunk was 0.89 milliseconds. This article describes the changes made to reduce this time down to <strong>0.48ms (54% faster)</strong>.</p><p>If any terms used in this article are unfamiliar, please see the <a href="https://vercidium.com/blog/voxel-world-optimisations/">original article</a>.</p><p>The full source code is available <a href="https://github.com/Vercidium/voxel-mesh-generation">here</a>.</p><p><em>For clarity, <code>i, j and k</code> refer to chunk-relative positions (range 0-31)<br>and <code>x, y and z</code> refer to global map positions (range 0-511)</em></p><h2 id="constants">Constants</h2><p>These constants are referenced multiple times throughout the article:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">const int EMPTY = 0;
const int CHUNK_SIZE = 32;
const int CHUNK_SIZE_SQUARED = 1024;
const int CHUNK_SIZE_CUBED = 32768;
const int CHUNK_SIZE_MINUS_ONE = 31;
const int CHUNK_SIZE_SHIFTED = 32 &lt;&lt; 6;
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="generatemesh">GenerateMesh</h2><p>The <code>GenerateMesh</code> function iterates over each block in the chunk, combining faces across multiple blocks where possible to reduce the triangle count of the mesh. The first downfall of the original function is that all 32768 blocks in the data array are checked, when most may be empty.</p><p>To reduce the amount of blocks checked, we store two heightmaps for the bottom- and top-most non-empty blocks in each column in the chunk. These arrays are updated each time a block is added or removed from the chunk:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">byte[] MinY = new byte[CHUNK_SIZE_SQUARED];
byte[] MaxY = new byte[CHUNK_SIZE_SQUARED];
</code></pre>
<!--kg-card-end: markdown--><p></p><p>To make use of this heightmap, we restructure the loop as follows:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">int access;

// Z axis
for (int k = 0; k &lt; CHUNK_SIZE; k++)
{
    // X axis
    for (int i = 0; i &lt; CHUNK_SIZE; i++)
    {
        int j =    MinY[i + k * CHUNK_SIZE_SQUARED];
        int topJ = MaxY[i + k * CHUNK_SIZE_SQUARED];
        
        // Y axis
        for (; j &lt; topJ; j++)
        {
            access = i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED;
            ref Block b = ref data[access];

            if (b.kind == EMPTY)
                continue;

            CreateRun(ref b, i, j, k, chunkHelper, access);
        }
    }

    // Extend the array if it is nearly full
    if (vertexBuffer.used &gt; vertexBuffer.data.Length - 2048)
        vertexBuffer.Extend(2048);

}```</code></pre>
<!--kg-card-end: markdown--><p></p><p>As the inner-most loop now iterates over the Y axis, we should restructure the way that we store block positions in <code>data[]</code> so that we can access the next highest block in the column by incrementing <code>access</code> by 1:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Old
int access = i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED;

// New
int access = j + i * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED;</code></pre>
<!--kg-card-end: markdown--><p></p><p>The next issue is that <code>i * CHUNK_SIZE</code>, <code>k * CHUNK_SIZE_SQUARED</code> and other values within <code>CreateRun()</code> are redundantly calculated many times. Therefore where possible, any value that is calculated multiple times should be stored in a variable in the outer loop and referenced later on. This reduces the amount of multiplications from 65536 down to 1056, and the amount of additions from 197664 down to 135232 (worst case).</p><p>The new loop structure is as follows:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Precalculate the map-relative Y position of the chunk in the map
int chunkY = chunkPosY * CHUNK_SIZE;

// Allocate variables on the stack
int access, heightMapAccess, iCS, kCS2, i1, k1, j, topJ;
bool minXEdge, maxXEdge, minZEdge, maxZEdge;

k1 = 1;

for (int k = 0; k &lt; CHUNK_SIZE; k++, k1++)
{
    // Calculate this once, rather than multiple times in the inner loop
    kCS2 = k * CHUNK_SIZE_SQUARED;

    i1 = 1;
    heightMapAccess = k * CHUNK_SIZE;
    
    // Is the current run on the Z- or Z+ edge of the chunk
    minZEdge = k == 0;
    maxZEdge = k == CHUNK_SIZE_MINUS_ONE;

    for (int i = 0; i &lt; CHUNK_SIZE; i++; i1++)
    {
        // Determine where to start the innermost loop
        j = MinY[heightMapAccess];
        topJ = MaxY[heightMapAccess];
        heightMapAccess++;
        
        // Calculate this once, rather than multiple times in the inner loop
        iCS = i * CHUNK_SIZE;
        
        // Calculate access here and increment it each time in the innermost loop
        access = kCS2 + iCS + j;

        // Is the current run on the X- or X+ edge of the chunk
        minX = i == 0;
        maxX = i == CHUNK_SIZE_MINUS_ONE;

        // X and Z runs search upwards to create runs, so start at the bottom.
        for (; j &lt; topJ; j++, access++)
        {
            ref Block b = ref data[access];

            if (b.kind != EMPTY)
            {
                CreateRun(ref b, i, j,
                          k &lt;&lt; 12,
                          i1,
                          k1 &lt;&lt; 12,
                          j + chunkY, access,
                          minX, maxX,
                          j == 0, j == CHUNK_SIZE_MINUS_ONE,
                          minZ, maxZ,
                          iCS, kCS2);
            }
        }

        // Extend the array if it is nearly full
        if (vertexBuffer.used &gt; vertexBuffer.data.Length - 2048)
            vertexBuffer.Extend(2048);
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="createrun">CreateRun</h2><p>As the new <code>CreateRun()</code> function has many changes, we will split it into sections. The first difference is the new parameters:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// New CreateRun function
void CreateRun(ref Block b, int i, int j,
               int k,
               int i1,
               int k1,
               int y, int access,
               bool minX, bool maxX,
               bool minY, bool maxY,
               bool minZ, bool maxZ,
               int iCS, int kCS2)
{
    ...
}
</code></pre>
<!--kg-card-end: markdown--><p></p><ul><li><code>int i, int j</code> - chunk-relative position of the block</li><li><code>k</code> - chunk-relative position of the block shifted left 12 bits</li><li><code>int i1</code> - precalculated <code>i + 1</code></li><li><code>int k1</code> - precalculated <code>(k + 1) &lt;&lt; 12</code></li><li><code>int y</code> - map-relative y position of the block</li><li><code>int access</code> - current position in the data[] array</li><li><code>bool minX, bool maxX</code> - if the current run is on the X edge of the chunk</li><li><code>bool minY, bool maxY</code> - if the current run is on the Y edge of the chunk</li><li><code>bool minZ, bool maxZ</code> - if the current run is on the Z edge of the chunk</li><li><code>int iCS</code> - precalculated <code>i * CHUNK_SIZE</code></li><li><code>int kCS2</code> - precalculated <code>k * CHUNK_SIZE_SQUARED</code></li></ul><h3 id="precalculating">Precalculating</h3><p>At the start of the function, we can see the following changes:</p><ul><li>The <code>BlockVertex</code> helper class now has a <code>int[] indexToTextureShifted</code> array, which stores the texture values shifted left 18 bits. Since the texture and health values are the same for each face of the block, they can be bit-shifted and combined here rather than in the <code>AppendQuad()</code> function</li><li>The <code>length</code> variable has been replaced with <code>runFinish</code>, which stores the chunk-relative position of the end of the run</li></ul><!--kg-card-begin: markdown--><pre><code class="language-csharp">int textureHealth16 = BlockVertex.indexToTextureShifted[b.index] | ((b.health / 16) &lt;&lt; 23);
int runFinish;
int accessIncremented = access + 1;
int chunkAccess;
int j1 = j + 1;
int jS = j &lt;&lt; 6;
int jS1 = j1 &lt;&lt; 6;
</code></pre>
<!--kg-card-end: markdown--><p></p><h3 id="combining-faces">Combining Faces</h3><p>The next part combines faces upwards along the negative X face, with the following changes:</p><ul><li><code>kCS2</code> is passed to <code>VisibleFaceXN()</code> so that the function doesn&apos;t have to calculate <code>k * CHUNK_SIZE_SQUARED</code> each time</li><li><code>chunkAccess</code> has the default value of <code>accessIncremented</code>, which is equivalent to<br><code>j + i * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED + 1</code>. This value is then incremented at the end of the loop</li><li>Rather than creating an extra variable <code>q</code> for managing the loop, we can use the <code>runFinish</code> variable. By initialising this variable to <code>j1 &lt;&lt; 6</code> and incrementing it by 64 (<code>1 &lt;&lt; 6 == 64</code>), we can save an extra addition and avoid bit-shifts throughout the entire <code>CreateRun()</code> function</li><li><code>AppendQuad()</code> has been split into three functions <code>AppendQuadX()</code>, <code>AppendQuadY()</code> and <code>AppendQuadZ()</code>, which are specifically for generating X, Y and Z faces</li><li><code>Int3</code> structs are no longer passed to <code>AppendQuad()</code>, therefore removing unnecessary memory allocation</li><li>The <code>FaceType</code> enum has been replaced with <code>FaceTypeShifted</code>, which stores the same values shifted left 27 bits</li></ul><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Left (X-)
if (!chunkHelper.visitXN[access] &amp;&amp; VisibleFaceXN(j, access, minX, kCS2))
{
    chunkHelper.visitXN[access] = true;
    chunkAccess = accessIncremented;

    for (runFinish = jS1; length &lt; Constants.ChunkSizeShifted; length += 64)
    {
        if (DifferentBlock(chunkAccess, ref b))
            break;

        chunkHelper.visitXN[chunkAccess++] = true;
    }

    // k1 and k are already shifted
    BlockVertex.AppendQuadX(vertexBuffer, i, jS, length, k1, k, (int)FaceTypeShifted.xn, textureHealth16);
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h3 id="appendquad">AppendQuad</h3><p>The new <code>AppendQuadX</code> function contains no bit-shifts and fewer or operations:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Stores the values of the old blockToTexture[] shifted left 18 bits
public static byte[] blockToTextureShifted;
        
public static void AppendQuadX(BlockVertexBuffer buffer, int x,
                               int jBottom, int jTop,
                               int kLeft, int kRight, 
                               int normal, int textureHealth)
{
    // Combine data that is common for each face
    var shared = x             | 
                 textureHealth |
                 normal;

    // Combine the shared data with the Y and Z position of the vertex
    buffer.data[buffer.used] = jTop | kRight | shared;
    buffer.data[buffer.used + 1] = buffer.data[buffer.used + 4] = jBottom | kRight | shared;
    buffer.data[buffer.used + 2] = buffer.data[buffer.used + 3] = jTop | kLeft | shared;
    buffer.data[buffer.used + 5] = jBottom | kKeft | shared;

    buffer.used += 6;
}</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="visible-face-checking">Visible Face Checking</h2><p>The new <code>VisibleFaceXN</code> function uses the precalculated values <code>min</code> and <code>kCS2</code> and prevents excess faces being produced on the edge of the map:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">protected bool VisibleFaceXN(int j, int k, int access, bool min, int kCS2)
{
    // Access directly from a neighbouring chunk
    if (min)
    {
        // If on the edge of the map, don&apos;t produce faces
        if (chunkPosX == 0)
            return false;

        if (cXN == null)
            return true;

        return cXN.data[31 * CHUNK_SIZE + j + kCS2].kind == EMPTY;
    }

    // The block to the left can be accessed by subtracting CHUNK_SIZE
    return data[access - CHUNK_SIZE].kind == EMPTY;
}```</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="full-source-code">Full Source Code</h2><p>The full source code including OpenGL data buffering and shaders is available <a href="https://github.com/Vercidium/voxel-mesh-generation">here</a>.</p><p>See the code in action in our latest video:</p><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/qoKzhIouzsk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Voxel World Optimisations]]></title><description><![CDATA[This article details the voxel mesh generation technique used in our game Sector's Edge.]]></description><link>https://vercidium.com/blog/voxel-world-optimisations/</link><guid isPermaLink="false">61190945d364ab77972ea257</guid><category><![CDATA[voxel]]></category><category><![CDATA[mesh]]></category><category><![CDATA[optimisations]]></category><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Sun, 28 Jul 2019 23:58:41 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2019/07/destruction-min-2.png" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2019/07/destruction-min-2.png" alt="Voxel World Optimisations"><p><a href="https://www.youtube.com/watch?v=WAwNuauIK4Q&amp;t=12s">Sector&apos;s Edge</a> is a first person shooter that takes place in a voxel environment, where the world is composed of millions of destructible cubes. With 16 players per match, the mesh must be regenerated nearly every frame. This is a CPU-intensive process and this article dives into the optimisations that make it possible.</p><p>Our two goals for this engine are:</p><ul><li>fast mesh regeneration balanced with low triangle count</li><li>fast vertex processing on the GPU</li></ul><p>The second topic will be available in a separate article soon.</p><p>The full source code for this article is available <a href="https://github.com/Vercidium/voxel-mesh-generation">here on GitHub</a>.</p><p><strong>Update: </strong><a href="https://vercidium.com/blog/further-voxel-world-optimisations/">a new article has been posted</a> that describes the further optimisations made to reduce chunk mesh initialisation time down to 0.48ms.</p><h2 id="map-structure">Map Structure</h2><p>The map is divided into chunks of 32 x 32 x 32 blocks. Each chunk has its own mesh - a list of triangles - which must be regenerated each time a block is added, removed or damaged. Block data is stored in the following structure:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class Map
{
    Chunk[,,] chunks;
}

public class Chunk
{
    const int CHUNK_SIZE = 32;
    const int CHUNK_SIZE_SQUARED = 1024;
    const int CHUNK_SIZE_CUBED = 32768;
    Block[] data = new Block[CHUNK_SIZE_CUBED];
}

public struct Block
{
    byte kind;  // Block type, e.g. empty, dirt, metal, etc.
    byte health;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p><a href="https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/">Greedy meshing</a> is a popular algorithm for producing a mesh with a low triangle count, however it requires many block comparisons and in our case wasn&apos;t regenerating chunks fast enough. In our approach, blocks are combined into runs along the X and Y axis:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="/blog/content/images/2019/07/merging-1.jpg" class="kg-image" alt="Voxel World Optimisations" loading="lazy"><figcaption>Actual image (left), underlying mesh (right)</figcaption></figure><p>To reduce triangle count, a run will not be split if it is covered by a block. Runs are only split if the next block has a different texture or is damaged:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="/blog/content/images/2019/07/runs.png" class="kg-image" alt="Voxel World Optimisations" loading="lazy"><figcaption>Actual image (top), underlying mesh (bottom)</figcaption></figure><p>This allows chunk meshes to be regenerated with less block comparisons while still combining faces to reduce the triangle count.</p><h2 id="mesh-generation">Mesh Generation</h2><p>The full <code>Chunk</code> class has the following structure:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class Chunk
{
    const int CHUNK_SIZE = 32;
    const int CHUNK_SIZE_SQUARED = 1024;
    const int CHUNK_SIZE_CUBED = 32768;
    const int EMPTY = 0;
    
    Block[,,] data = new Block[CHUNK_SIZE_CUBED];
    
    // Global map position of this chunk
    public int chunkX, chunkY, chunkZ;
    
    // Each chunk has a reference to the map it is located in, so it can access
    // blocks in other chunks
    Map map;
    
    // Stores vertex data and manages buffering to the GPU
    BlockVertexBuffer vertexBuffer;
    
    // References to neighbouring chunks
    Chunk cXN, cXP, cYN, cYP, cZN, cZP;
    
    public Chunk(int i, int j, int k, Map map)
    {
        chunkX = i * CHUNK_SIZE;
        chunkY = j * CHUNK_SIZE;
        chunkZ = k * CHUNK_SIZE;
        this.map = map;
    }
    
    public void GenerateMesh(...) ...
    void CreateRuns(...) ...
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p><em>For clarity, <code>i, j and k</code> refer to chunk-relative positions (range 0-31)<br>and <code>x, y and z</code> refer to global map positions (range 0-511)</em></p><p>To start, <code>GenerateMesh</code> gets references to chunk neighbours for quick edge-case block comparisons. It then iterates over each block in the chunk and creates runs.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public void GenerateMesh(ChunkHelper chunkHelper)
{
    // X- neighbouring chunk
    cXN = chunkPosX &gt; 0 ? m.chunks[chunkPosX - 1, chunkPosY, chunkPosZ] : null;

    // X+ neighbouring chunk
    cXP = chunkPosX &lt; Constants.ChunkXAmount - 1 ? m.chunks[chunkPosX + 1, chunkPosY, chunkPosZ] : null;

    // Y- neighbouring chunk
    cYN = chunkPosY &gt; 0 ? m.chunks[chunkPosX, chunkPosY - 1, chunkPosZ] : null;

    // Y+ neighbouring chunk
    cYP = chunkPosY &lt; Constants.ChunkYAmount - 1 ? m.chunks[chunkPosX, chunkPosY + 1, chunkPosZ] : null;

    // Z- neighbouring chunk
    cZN = chunkPosZ &gt; 0 ? m.chunks[chunkPosX, chunkPosY, chunkPosZ - 1] : null;
    
    // Z+ neighbouring chunk
    cZP = chunkPosZ &lt; Constants.ChunkZAmount - 1 ? m.chunks[chunkPosX, chunkPosY, chunkPosZ + 1] : null;
    
    int access;
    // Y axis - start from the bottom and search up
    for (int j = 0; j &lt; CHUNK_SIZE; j++)
    {
        // Z axis
        for (int k = 0; k &lt; CHUNK_SIZE; k++)
        {
            // X axis
            for (int i = 0; i &lt; CHUNK_SIZE; i++)
            {
                access = i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED;
                ref Block b = ref data[access];

                if (b.kind == EMPTY)
                    continue;

                CreateRun(ref b, i, j, k, chunkHelper, access);
            }

            // Extend the array if it is nearly full
            if (vertexBuffer.used &gt; vertexBuffer.data.Length - 2048)
                vertexBuffer.Extend(2048);
        }
    }
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The <code>ChunkHelper</code> class is used to record which faces have been merged, to prevent creating duplicate faces. Each chunk mesh is regenerated on a different thread and one <code>ChunkHelper</code> instance is allocated to each thread.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class ChunkHelper
{
    public bool[] visitedXN = new bool[CHUNK_SIZE_CUBED];
    public bool[] visitedXP = new bool[CHUNK_SIZE_CUBED];
    public bool[] visitedZN = new bool[CHUNK_SIZE_CUBED];
    public bool[] visitedZP = new bool[CHUNK_SIZE_CUBED];
    public bool[] visitedYN = new bool[CHUNK_SIZE_CUBED];
    public bool[] visitedYP = new bool[CHUNK_SIZE_CUBED];
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>Separate runs are created for each block face orientation (X-, X+, Z-, Z+, Y-, Y+). For each block in the combined face, a flag is set in the respective array in <code>ChunkHelper</code> to indicate it has been merged.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">void CreateRun(ref Block b, int i, int j, int k, ChunkHelper chunkHelper, int access)
{
    // Precalculate variables
    int i1 = i + 1;
    int j1 = j + 1;
    int k1 = k + 1;
    byte health16 = (byte)(b.health / 16);
    
    int length = 0;
    int chunkAccess = 0;

    // Left face (X-)
    if (!chunkHelper.visitedXN[access] &amp;&amp; VisibleFaceXN(i - 1, j, k))
    {
        // Search upwards to determine run length
        for (int q = j; q &lt; CHUNK_SIZE; q++)
        {
            // Pre-calculate the array lookup as it is used twice
            chunkAccess = i + q * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED;
            
            // If we reach a different block or an empty block, end the run
            if (DifferentBlock(chunkAccess, b))
                break;

            // Store that we have visited this block
            chunkHelper.visitedXN[chunkAccess] = true;

            length++;
        }


        if (length &gt; 0)
        {
            // Create a quad and write it directly to the buffer
            BlockVertex.AppendQuad(buffer, new Int3(i, length + j, k1),
                                           new Int3(i, length + j, k),
                                           new Int3(i, j,          k1),
                                           new Int3(i, j,          k), 
                                           (byte)FaceType.xn, b.kind, health16);

            buffer.used += 6;
        }
    }
    
    // Same algorithm for right (X+)
    if (!chunkHelper.visitedXP[access] &amp;&amp; VisibleFaceXP(i1, j, k))
    {
        ...
    }

    // Same algorithm for back (Z-)
    ...
    
    // Same algorithm for front (Z+)
    ...
    
    // Same algorithm for bottom (Y-)
    ...

    // Same algorithm for top (Y+)
    ...
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The <code>VisibleFace</code> functions are used to check whether or not a block is empty. Most comparisons are within the working chunk, so we prioritise accessing block data directly within the chunk if possible.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">bool VisibleFaceXN(int i, int j, int k)
{
    // Access directly from a neighbouring chunk
    if (i &lt; 0)
    {
        if (cXN == null)
            return true;

        return cXN.data[31 + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED].kind == EMPTY;
    }

    return data[i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED].kind == EMPTY;
}

bool VisibleFaceXP(int i, int j, int k)
{
    if (i &gt;= CHUNK_SIZE)
    {
        if (cXP == null)
            return true;

        return cXP.data[0 + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED].kind == EMPTY;
    }

    return data[i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED].kind == EMPTY;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p><code>DifferentBlock</code> is used to determine whether a block is different to the current run. Runs do not span across multiple chunks and therefore we do not need bounds checks when accessing the block.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">protected bool DifferentBlock(int access, Block current)
{
    // Using a ref variable here increased performance by ~4%
    ref var b = ref data[access];
    return b.kind != current.kind || b.health != current.health;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The <code>AppendQuad</code> function creates 6 vertices (two triangles) and writes them directly to the vertex buffer.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public static byte[] blockToTexture;
        
public static void AppendQuad(BlockVertexBuffer buffer, Int3 tl, Int3 tr, Int3 bl, 
                              Int3 br, byte normal, byte kind, byte health)
{
    // Each block kind maps to a texture unit on the GPU
    byte texture = blockToTexture[kind];

    // Each vertex is packed into one uint to reduce GPU memory overhead
    // 18 bits for position XYZ
    // 5 bits for texture unit
    // 4 bits for health
    // 3 bits for normal
    
    // Each vertex in a quad shares the same texture unit, health and normal value
    uint shared = (uint)((texture &amp; 31) &lt;&lt; 18 |
                         (health  &amp; 15) &lt;&lt; 23 |
                         (normal  &amp; 7)  &lt;&lt; 27);

    // Top left vertex
    uint tlv = CombinePosition(tl, shared);
    
    // Top right vertex
    uint trv = CombinePosition(tr, shared);
    
    // Bottom left vertex
    uint blv = CombinePosition(bl, shared);
    
    // Bottom right vertex
    uint brv = CombinePosition(br, shared);

    // Store each vertex directly into the buffer
    buffer.data[buffer.used]     = tlv;
    buffer.data[buffer.used + 1] = blv;
    buffer.data[buffer.used + 2] = trv;
    buffer.data[buffer.used + 3] = trv;
    buffer.data[buffer.used + 4] = blv;
    buffer.data[buffer.used + 5] = brv;
}

// Combine position data with the shared uint
static uint CombinePosition(Int3 pos, uint shared)
{
    return (uint)(shared | 
                 ((uint)pos.X &amp; 63) |
                 ((uint)pos.Y &amp; 63) &lt;&lt; 6 |
                 ((uint)pos.Z &amp; 63) &lt;&lt; 12);
}</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="benchmarking">Benchmarking</h2><p>To test our optimisations, the same map was initialised three times in a row and the time taken to run <code>GenerateMesh</code> for each chunk was recorded. This was done single-threaded to ensure we were only measuring our function, not task scheduling.</p><p>Our initial benchmark was:<br><code>807 chunks initialised in 4158ms</code><br><code>1 chunk initialised in 5.15ms (average)</code></p><p>After optimisations: <br><code>807 chunks initialised in 722ms</code><br><code>1 chunk initialised in 0.89ms (average)</code></p><p>How this 5.7x increase in speed was achieved is outlined below.</p><p><strong><strong>Update: </strong></strong><a href="https://vercidium.com/blog/further-voxel-world-optimisations/">a new article has been posted</a> that describes the further optimisations made to reduce chunk mesh initialisation time down to 0.48ms.</p><h2 id="optimisations">Optimisations</h2><h3 id="inlining-3-speed-increase-">Inlining (3% speed increase)</h3><p>Apart from <code>GenerateMesh</code>, every method is inlined using this MethodImpl attribute:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">using [MethodImpl(MethodImplOptions.AggressiveInlining)]
void CreateStrips(...) ...
</code></pre>
<!--kg-card-end: markdown--><p></p><h3 id="pre-calculating-and-ref-locals-5-speed-increase-">Pre-Calculating and Ref Locals (5% speed increase)</h3><p>Every multiplication, addition and memory lookup affects performance, so we pre-calculate the common operations.</p><p>For example at the start of the <code>CreateRun</code> function:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">protected void CreateStrips(Block b, int i, int j, int k, ChunkHelper chunkHelper, int access)
{
    int i1 = i + 1;
    int j1 = j + 1;
    int k1 = k + 1;
    byte health16 = (byte)(b.health / 16);
    
    ...
}
</code></pre>
<!--kg-card-end: markdown--><p></p><p>In the BlockVertex class, we reduced 24 bitwise operations down to 19:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Old - 24 bitwise operations per quad
static uint toInt(Int3 pos, byte texID, byte health, byte normal)
{
    return (uint)((pos.X  &amp; 63) |
                  (pos.Y  &amp; 63) &lt;&lt; 6 |
                  (pos.Z  &amp; 63) &lt;&lt; 12 |
                  (texID  &amp; 31) &lt;&lt; 18 |
                  (health &amp; 15) &lt;&lt; 23 |
                  (normal &amp; 7) &lt;&lt; 27);
}

// New - 19 bitwise operations per quad
public static void AppendQuad(...)
{
    var shared = (uint)((t      &amp; 31) &lt;&lt; 18 |
                        (health &amp; 15) &lt;&lt; 23 |
                        (normal &amp; 7)  &lt;&lt; 27);
                        
    uint tlv = CombinePosition(tl, shared);
    uint trv = CombinePosition(tr, shared);
    uint blv = CombinePosition(bl, shared);
    uint brv = CombinePosition(br, shared);
    
    ...                        
}

static uint CombinePosition(Int3 pos, uint shared)
{
    return (uint)(shared | 
                  ((uint)pos.X &amp; 63) |
                  ((uint)pos.Y &amp; 63) &lt;&lt; 6 |
                  ((uint)pos.Z &amp; 63) &lt;&lt; 12);
}

</code></pre>
<!--kg-card-end: markdown--><p></p><p>Ref locals are used where possible to avoid copying structs onto the stack and avoid multiple array lookups:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">protected bool DifferentBlock(int i, int j, int k, Block compare)
{
    ref var b = ref data[i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED];
    return b.kind != compare.kind || b.health != compare.health;
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h3 id="neighbouring-chunks-10-speed-increase-">Neighbouring Chunks (10% speed increase)</h3><p>By storing references to neighbouring chunks at the start of <code>GenerateMesh</code>, we saved 32*32 expensive <code>Map.IsNoBlock</code> functions calls on each side of the chunk.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Old
bool VisibleFace(int i, int j, int k)
{
    // Access from global map coordinates
    if (i &lt; 0 || j &lt; 0 || k &lt; 0 ||
        i &gt;= CHUNK_SIZE || j &gt;= CHUNK_SIZE || k &gt;= CHUNK_SIZE)
    {
        return m.IsNoBlock(i + chunkPosX, j + chunkPosY, k + chunkPos.Z);
    }

    return data[i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED].kind == EMPTY;
}

// New
bool VisibleFaceXN(int i, int j, int k)
{
    // Access directly from neighbouring chunk
    if (i &lt; 0)
    {
        if (cXN == null)
            return true;

        return cXN.data[31 + j * Constants.ChunkSize + k * Constants.ChunkSizeSquared].kind == EMPTY;
    }

    return data[i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED].kind == EMPTY;
}

// Same for VisibleFaceXP, VisibleFaceZN, ...
...
</code></pre>
<!--kg-card-end: markdown--><p></p><h3 id="single-dimensional-arrays-20-speed-increase-">Single-Dimensional Arrays (20% speed increase)</h3><p>Changing the block data array in the <code>Chunk</code> class from a multi-dimensional array to a single-dimensional array provided a noticeable improvement in performance:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Old
Block[,,] data;
Block b = data[i,j,k];

// New
Block[] data;
Block b = data[i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED];
</code></pre>
<!--kg-card-end: markdown--><p></p><p>The same was done with the <code>ChunkHelper</code> arrays:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Old
bool[,,] visitedXN = new bool[CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE];
bool b = visitedXN[i,j,k];

// New
bool[] visitedXN = new bool[CHUNK_SIZE_CUBED];
bool b = visitedXN[i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED];
</code></pre>
<!--kg-card-end: markdown--><p></p><p>This performance increase came from the fact that <code>CHUNK_SIZE</code> is a multiple of 2, and therefore the C# compiler could simplify the multiplications to bitshifts:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Old
data[i, j, k];

// Underlying array access produced by the compiler
data[i + j * data.GetLength(0) + j * data.GetLength(0) * data.GetLength(1)];

// New
data[i + j * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED];

// Simplified array access produced by the compiler
data[i + j &lt;&lt; 5 + k &lt;&lt; 10];</code></pre>
<!--kg-card-end: markdown--><p></p><p>At times, we are also able to pre-calculate the single-dimensional array lookup, as seen in the <code>CreateRun</code> function:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">for (int q = j; q &lt; CHUNK_SIZE; q++)
{
    // Pre-calculate the array lookup as it is used twice
    chunkAccess = i + q * CHUNK_SIZE + k * CHUNK_SIZE_SQUARED;

    if (DifferentBlock(chunkAccess, b))
        break;

    chunkHelper.visitedXN[chunkAccess] = true;
    
    ...
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h3 id="garbage-collection-10-speed-increase-">Garbage Collection (10% speed increase)</h3><p>Each array in <code>ChunkHelper</code> must be reset to false before being re-used by another chunk. Initially we were re-allocating a new boolean array, however this put excessive pressure on the garbage collector. <code>Array.Clear</code> was a much better solution and quickly resets the array back to false:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Old
public void Reset()
{
    visitedXN = new bool[CHUNK_SIZE_CUBED];
    
    // Same for X+, Y-, Y+, Z- and Z+
	...
}

// New
public void Reset()
{
    Array.Clear(visitedXN, 0, CHUNK_SIZE_CUBED);
    
    // Same for X+, Y-, Y+, Z- and Z+
    ...
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h3 id="memory-20-speed-increase-">Memory (20% speed increase)</h3><p>Initially, <code>AppendQuad</code> returned an array of 6 uints, which was then copied to a temporary buffer stored in a <code>ChunkHelper</code>. In the <code>GenerateMesh</code> function we would then check if this buffer has exceeded a certain size and copy it to the vertex buffer. This involved a <em>lot</em> of <code>Array.Copy</code> calls.</p><p>Instead, <code>AppendQuad</code> is passed the vertex buffer as a parameter and writes to it directly. This removes a lot of expensive <code>Array.Copy</code> functions, however we need to ensure we do not overflow the vertex buffer:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public void GenerateMesh(...)
{
    ...    
    // Extend the array if it is nearly full
    if (vertexBuffer.used &gt; vertexBuffer.data.Length - 2048)
        vertexBuffer.Extend(2048);        
    ...
}
    
// Function inside BlockVertexBuffer:
class BlockVertexBuffer
{
    ...    
    public void Extend(int amount)
    {
        uint[] newData = new uint[data.Length + amount];
        Array.Copy(data, newData, data.Length);
        data = newData;
    }    
    ...
}
</code></pre>
<!--kg-card-end: markdown--><p></p><h3 id="block-access-20-speed-increase-">Block Access (20% speed increase)</h3><p>Initially, blocks were only accessed in the map class by their global map positions. We reduced our chunk initialisation time by accessing blocks directly within the chunk where possible.</p><p><code>IsNoBlock</code> is still used and has been optimised using bit shifts and bit masks:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class Map
{
    Chunk[,,] chunks;
    const byte SHIFT = 5;
    const int MASK = 0x1f;
    const int MAP_SIZE_X = 512;
    const int MAP_SIZE_Y = 160;
    const int MAP_SIZE_Z = 512;
    
    // Returns true if there is no block at this global map position
    public bool IsNoBlock(int x, int y, int z)
    {
        // If it is at the edge of the map, return true
        if (x &lt; 0 || x &gt;= MAP_SIZE_X ||
            y &lt; 0 || y &gt;= MAP_SIZE_Y ||
            z &lt; 0 || z &gt;= MAP_SIZE_Z)
            return true;

        // Chunk accessed quickly using bitwise shifts
        var c = chunks[x &gt;&gt; SHIFT, y &gt;&gt; SHIFT, z &gt;&gt; SHIFT];

        // To lower memory usage, a chunk is null if it has no blocks
        if (c == null)
            return true;

        // Chunk data accessed quickly using bit masks        
        return c.data[(x &amp; MASK) + (y &amp; MASK) * CHUNK_SIZE, (z &amp; MASK) * CHUNK_SIZE_SQUARED].kind == EMPTY;
    }
}</code></pre>
<!--kg-card-end: markdown--><p></p><h2 id="in-practice">In Practice</h2><p>This engine was created from scratch for our first person shooter <a href="https://sectorsedge.com">Sector&apos;s Edge</a>. We did not use an existing game engine as we desired the ability to fine-tune every aspect of the rendering pipeline. This allows the game to run on older hardware and at rates higher than 60 frames per second.</p><p>The full source code for this article is available <a href="https://github.com/Vercidium/voxel-mesh-generation">here on GitHub</a>.</p><p>If you have any questions, feel free to reach out to me on our <a href="https://sectorsedge.com/discord">Discord server</a>.</p><p>I have also written a breakdown of each stage in the rendering and post-processing pipeline, which is <a href="https://vercidium.com/blog/3d-rendering-behind-the-scenes/">available here</a>.</p><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/WAwNuauIK4Q?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure>]]></content:encoded></item><item><title><![CDATA[3D Game Rendering - Behind the Scenes]]></title><description><![CDATA[<p>Each frame seen in a modern game is the combination of many different layers and effects. This article explains each stage of the rendering process in our first person shooter <a href="https://sectorsedge.com">Sector&apos;s Edge</a>.</p><p>There are a few terms used in this article:</p><ul><li>Buffer - a 2D image that stores</li></ul>]]></description><link>https://vercidium.com/blog/3d-rendering-behind-the-scenes/</link><guid isPermaLink="false">61190945d364ab77972ea256</guid><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Sun, 28 Jul 2019 23:58:33 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2019/07/final-2.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2019/07/final-2.jpg" alt="3D Game Rendering - Behind the Scenes"><p>Each frame seen in a modern game is the combination of many different layers and effects. This article explains each stage of the rendering process in our first person shooter <a href="https://sectorsedge.com">Sector&apos;s Edge</a>.</p><p>There are a few terms used in this article:</p><ul><li>Buffer - a 2D image that stores either byte or float data for each pixel</li><li>Normal - the XYZ direction that each pixel is facing. For example, the top of your desk is facing upwards and the ceiling is facing downwards</li><li>Camera - the position of the player and the direction they are looking</li></ul><h1 id="stage-1-physical-rendering">Stage 1. Physical Rendering</h1><p>The first stage involves rendering all in-game physical objects to four buffers:</p><ul><li>Colour buffer - stores the RGB value of each pixel</li><li>Depth buffer - stores how far away each pixel is from the camera</li><li>Normal buffer - stores an XYZ normal for each pixel</li><li>Data buffer - stores special data for each pixel (explained below)</li></ul><h2 id="colour-buffer">Colour Buffer</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image831.jpg" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>The colour buffer</figcaption></figure><p>The colour buffer stores an RGB colour for each pixel on the screen.</p><h2 id="depth-buffer">Depth Buffer</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/depth.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>The Depth Buffer</figcaption></figure><p>This one is a bit harder to see, so we&apos;ve zoomed the image onto the character model.</p><p>The depth buffer stores how far away each pixel is from the camera. Darker parts of the image are closer to the camera and lighter parts are further away. These values are used later to calculate the 3D position of each pixel for post-processing effects like lighting.</p><h2 id="normal-buffer">Normal Buffer</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image842.jpg" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>The Normal Buffer</figcaption></figure><p>The normal buffer uses RGB values to represent the XYZ direction that each pixel is facing. These values range between -1 and 1 and are used for calculating shading and lighting during post-processing.</p><p>In the image above:</p><ul><li>Upwards facing pixels have a Y normal of 1, which is represented as green</li><li>The walls on the right have a Z normal of 1, which is represented as blue</li><li>Black parts of the image represent normals with a negative value, however we can&apos;t see them as monitors can only render positive colour values.</li></ul><p>Here is another example:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/characternormals.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>Character normals</figcaption></figure><p>Left-facing (X) pixels have some red colour, pixels facing the camera (Z) have some blue colour and any upwards-facing (Y) pixels have some green colour.</p><h2 id="data-buffer">Data Buffer</h2><p>The data buffer stores two bytes for each pixel that define which post-processing effects will be applied to certain parts of the image.</p><p>Three bits in the first byte are used to determine whether the pixel:</p><ul><li>is part of the skybox, therefore no post-processing effects should be applied to these pixels</li><li>is part of the voxel environment, meaning these pixels are affected by SSAO and shadows</li><li>should have bloom effects applied</li></ul><p>The second byte stores how reflective the pixel is, for example metal pixels should be more illuminated by lights than dirt.</p><h1 id="stage-2-shadow-rendering">Stage 2. Shadow Rendering</h1><p>The scene is rendered again into another 5 depth buffers, each of which store how far away each pixel is from a hypothetical &apos;sun&apos;. By using 5 buffers we can have higher-quality shadows for the parts of the screen that are closer to the camera.</p><p>These buffers are generated from the sun&apos;s point of view looking down at the map, each more slightly zoomed than the last:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image941.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>Shadow Buffer 1</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image919.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>Shadow Buffer 2</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image908.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>Shadow Buffer 3</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image897.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>Shadow Buffer 4</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image886.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>Shadow Buffer 5</figcaption></figure><p>These buffers are used to darken certain parts of the colour buffer to create the illusion of shadows. This happens after the post-processing stage using some maths involving the values stored in the original depth buffer and the 5 shadow buffers.</p><h1 id="stage-3-post-processing">Stage 3. Post Processing</h1><h2 id="screen-space-ambient-occlusion">Screen Space Ambient Occlusion</h2><p>The first post-processing effect used is called Screen Space Ambient Occlusion (SSAO), which looks at the XYZ values in the normal buffer to locate crevices between objects.</p><p>The output is stored in the red component of the SSAO Buffer and is used to darken certain parts of the image later.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image875.jpg" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>The SSAO Buffer</figcaption></figure><h2 id="bloom">Bloom</h2><p>Bloom is a post-processing effect that brightens and blurs certain parts of the image, to mimic looking directly at a light in real life.</p><p>Pixels that have a bloom bit set in the data buffer mean that they will be copied from the colour buffer to the Bloom Buffer. Once copied, the image is blurred:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image864.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>The blurred Bloom Buffer</figcaption></figure><h2 id="lighting">Lighting</h2><p>The average map in Sector&apos;s Edge has around 30-60 lights in it, each of which are compared against the depth, normal and reflectivity values of each pixel.</p><p>The final colour of each pixel is based on:</p><ul><li>The distance between the pixel and the light (the position of each pixel is calculated using some maths with the depth buffer)</li><li>The pixel&apos;s XYZ normal value. Pixels facing away from a light will be illuminated less than pixels facing the light directly</li><li>The reflectivity of each pixel - stored in the second byte of the data buffer - controls how much the pixel&apos;s colour will be affected by the light. For example, dirt has a lower reflectivity than metal</li></ul><p>Lighting is applied directly to the colour buffer, but for this article we have separated the lighting values into their own buffer:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image853-1.jpg" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>The Lighting Buffer</figcaption></figure><h1 id="stage-4-putting-it-all-together">Stage 4. Putting it all Together</h1><p>Starting with the colour buffer:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image831-1.jpg" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>The original Colour Buffer</figcaption></figure><p>The shadow buffers and SSAO buffer are used to darken certain parts of the image:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image974.jpg" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>Colour Buffer with shadows and SSAO applied</figcaption></figure><p>Bloom and lighting is added to the image:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/image963.jpg" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>Colour Buffer with bloom and lighting applied</figcaption></figure><p>Transparent objects are then rendered directly to the final image. This is done last so that glass doesn&apos;t modify the depth/normal/data buffers, which would affect the post-processing effects:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/07/final-1.jpg" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"><figcaption>The final image shown in game</figcaption></figure><p>And there you have it! All of this must be computed within 16ms to provide smooth gameplay at 60 frames per second. I describe how the game has been optimised to run at much higher frame rates in <a href="https://vercidium.com/blog/voxel-world-optimisations/">this article</a>.</p><p>We are searching for people to help test Sector&apos;s Edge. Download the game <a href="https://sectorsedge.com">for free on our website</a> and let us know what you think on our <a href="https://sectorsedge.com/discord">Discord Server</a>.</p><figure class="kg-card kg-image-card"><img src="/blog/content/images/2019/07/alphaTestersNeeded.png" class="kg-image" alt="3D Game Rendering - Behind the Scenes" loading="lazy"></figure>]]></content:encoded></item><item><title><![CDATA[Random Galaxy Generation with C# and OpenGL]]></title><description><![CDATA[<p>This article explains the galaxy generation algorithm showcased in the video below. It is written in C#, rendered with OpenGL and the source code is available <a href="https://www.dropbox.com/s/sxhgc6dlff1ej1g/galaxyGeneration.cs?dl=0" rel="noopener">here</a>.</p><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/c4jDo2csOCk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p>At the end of the article you can see how the galaxy is used as the server selection screen in our game <a href="https://www.youtube.com/watch?v=pwmvMQy6p7E" rel="noopener">Sector&</a></p>]]></description><link>https://vercidium.com/blog/random-galaxy-generation-with-c-and-opengl/</link><guid isPermaLink="false">61190945d364ab77972ea255</guid><dc:creator><![CDATA[Mitchell Robinson]]></dc:creator><pubDate>Sun, 05 May 2019 13:22:23 GMT</pubDate><media:content url="https://vercidium.com/blog/content/images/2019/07/image1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://vercidium.com/blog/content/images/2019/07/image1.jpg" alt="Random Galaxy Generation with C# and OpenGL"><p>This article explains the galaxy generation algorithm showcased in the video below. It is written in C#, rendered with OpenGL and the source code is available <a href="https://www.dropbox.com/s/sxhgc6dlff1ej1g/galaxyGeneration.cs?dl=0" rel="noopener">here</a>.</p><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/c4jDo2csOCk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p>At the end of the article you can see how the galaxy is used as the server selection screen in our game <a href="https://www.youtube.com/watch?v=pwmvMQy6p7E" rel="noopener">Sector&#x2019;s Edge</a>, as well as some extreme examples.</p><h3 id="galaxy-anatomy">Galaxy Anatomy</h3><p>Each galaxy is composed of an <strong>axis </strong>and multiple <strong>arms</strong>:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/diagram.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>The arms of this galaxy are straight for visual clarity</figcaption></figure><p>The generation process follows the following stages:</p><ul><li>Arm generation</li><li>Axis generation</li><li>Arm scaling</li><li>Arm bending</li><li>Height variance</li><li>Applying colour</li><li>Creating multiple arm layers</li></ul><h3 id="part-1-generating-the-arms">Part 1 - Generating the Arms</h3><p>The code below produces multiple flat arms and is controlled by the following parameters:</p><ul><li><strong>gravity</strong>&#x200A;&#x2014;&#x200A;pushes points towards the center of the galaxy so that the galaxy will appear brighter and more densely populated towards the center</li><li><strong>galaxySize&#x200A;</strong>&#x2014;&#x200A;controls the radius of the galaxy</li><li><strong>armCount</strong>&#x2014; controls the amount of arms</li><li><strong>armSpread&#x200A;</strong>&#x2014;&#x200A;controls the width of each arm</li></ul><!--kg-card-begin: markdown--><pre><code class="language-csharp">Vector3 GetPoint()
{
    Vector3 v;
    double armDivisor = Math.PI / armCount;

    while (true)
    {
        // Generate a random normalised point with a weighting
        // towards the center controlled by the gravity variable
        v = NextV3(-1, 1).Normalized() * Math.Pow(Next(0, 1.0), gravity);

        // Some points will not conform to the parameters
        if (random.NextDouble() &gt; conformChance)
            break;

        bool valid = false;

        // Calculate the global angle of the point around the axis
        var d = Math.Atan2(v.X, v.Z);

        // Check if this point lands on an arm
        for (double j = -Math.PI; j &lt;= Math.PI; j += armDivisor)
        {
            // A point lands on an arm if its distance
            // from the start of the arm is less than angle * armDistance
            if (d &gt; j &amp;&amp; d &lt; j + armDivisor * armSpread)
            {
                valid = true;
                break;
            }
        }

        if (valid)
            break;
    }
    
    v.Y = 0;
    return v * galaxySize;
}
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/diagram1.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>A flat galaxy with four arms</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/diagram2.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>A flat galaxy with four arms and larger <strong>armSpread</strong></figcaption></figure><h3 id="part-2-generating-the-axis">Part 2 - Generating the Axis</h3><p>The axis is composed of stars, which are generated in the same manner as the points in the arms. Stars closer to the center of the galaxy are shifted vertically proportional to the <strong>beamHeight </strong>parameter.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">var v = GetPoint();

// Add some variance to each star
v += NextV3(-0.5, 0.5);

// Shift stars vertically as they get closer to the center of the galaxy
var s = beamHeight / v.Magnitude;
v.Y = Next(-s, s);
</code></pre>
<!--kg-card-end: markdown--><p><br>Stars are rendered as points into a framebuffer and then modified with a Gaussian blur to mimic a diffraction spike.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/SnapShot-4718.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>A flat galaxy populated with stars</figcaption></figure><h3 id="part-3-arm-scaling">Part 3 - Arm Scaling</h3><p>In a generated galaxy, some arms can be longer than others. The diagram below represents a top-down view of the galaxy. Any points that land inside the scaling region are extended further outwards.</p><figure class="kg-card kg-image-card"><img src="/blog/content/images/2019/05/propulsionZone.png" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"></figure><p>&#x3B8;&#xB9; represents the <strong>scalingAngleStart </strong>parameter and &#x3B8;&#xB2; represents the <strong>scalingAngleEnd</strong> parameter. Any points that lie within the scaling region are extended outwards proportional to the <strong>scalingPower</strong> parameter. This calculation is added to the end of the GetPoint() function:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">Vector3 GetPoint()
{
    Vector3 v = Vector3.Zero;
    
    // Generate a point
    ...

    // Calculate the global angle of the point around the axis
    var a = Math.Atan2(v.X, v.Z);

    // Calculate the angle of this point inside its quadrant
    var e = Math.Abs(Math.Abs(a) - Math.PI / 2);

    var magnitude = galaxySize;

    // Scale the point outwards if it lands within the scaling region
    if (e &gt; scalingAngleStart &amp;&amp; e &lt; scalingAngleEnd)
        magnitude = magnitude * 2 / Math.Pow(e, scalingPower);

    v.X *= magnitude;
    v.Z *= magnitude;     

    v.Y = 0;        
    return v;
}
</code></pre>
<!--kg-card-end: markdown--><p><br>In the example below, the top and bottom arms land within the scaling region and are extended further outwards:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/diagram3.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>The top and bottom arms have been scaled outwards</figcaption></figure><p>If <strong>scalingAngleEnd </strong>is set to<strong> PI</strong>,<strong> </strong>all points will land in the scaling zone:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/image6.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>All points have been scaled outwards</figcaption></figure><h3 id="part-4-arm-bending">Part 4 - Arm Bending</h3><p>To bend each arm around the galaxy we apply a matrix transformation to each point. Points further from the center of the galaxy will have a stronger rotation applied to them.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">Vector3 GetPoint()
{
    // Generate the point
    ...
    
    // Calculate arm scaling
    ...
    
    // Rotate the point around the galaxy proportional to its magnitude
    v *= Matrix4.CreateRotationY(-v.Magnitude * rotationStrength);
    
    v.Y = 0;
    return v;
}
</code></pre>
<!--kg-card-end: markdown--><p><br>The images below show the results of changing the <strong>rotationStrength</strong> parameter. Note these examples have more arms to demonstrate the bending more clearly.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/image7.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>A galaxy with weak bending</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/image8.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>A galaxy with strong bending</figcaption></figure><h3 id="part-5-vertical-variance">Part 5 - Vertical Variance</h3><p>By default, the arms generated are flat. By setting the <strong>heightMagnitude </strong>parameter to a value greater than 0, the vertical position of each point is calculated using a sine wave:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">Vector3 position = GetPoint();
position.Y -= heightMagnitude * Math.Sin(position.Magnitude * heightFrequency);
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/diagram4.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>The arms of this galaxy are manipulated vertically using a sin&#xA0;wave</figcaption></figure><h3 id="part-6-applying-colour">Part 6 - Applying Colour</h3><p>Each quadrant of the galaxy has its own colour (variables c1, c2, c3 and c4).</p><p>Once we have generated a point, its colour is calculated based on its angle and distance from the center of the galaxy.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">Vector3 position = GetPoint();
position.Y -= heightMagnitude * Math.Sin(position.Magnitude * heightFrequency);

// Colour each point based on their global angle around the axis
Color blendedColour;
var angle = Math.Atan2(position.X, position.Z);

// c1, c2, c3 and c4 are random colours generated at the start of the program
// Blend the colours of adjacent quadrants together
if (angle &lt; -Math.PI / 2)
    blendedColour = Color.Interpolate(c1, c2, (angle + Math.PI) / (Math.PI / 2));
else if (angle &lt; 0)
    blendedColour = Color.Interpolate(c2, c3, (Math.PI / 2 + angle) / (Math.PI / 2));
else if (angle &lt; Math.PI / 2)
    blendedColour = Color.Interpolate(c3, c4, angle / (Math.PI / 2));
else
    blendedColour = Color.Interpolate(c4, c1, (angle - Math.PI / 2) / (Math.PI / 2));

// maxMagnitude stores the distance of the furthest known point
if (position.Magnitude / maxMagnitude &gt; 1)
    maxMagnitude = position.Magnitude;

// Colours get brighter the closer they are to the center
uint pointColour = Color.Interpolate(Color.Black, Color.Interpolate(Color.White, blendedColour, position.Magnitude / maxMagnitude), 0.8).Value;
</code></pre>
<!--kg-card-end: markdown--><h3 id="part-7-multiple-arm-layers"><br>Part 7&#x200A;&#x2014;&#x200A;Multiple Arm Layers</h3><p>To add depth to the galaxy, the arm creation process can be repeated multiple times. Every iteration uses a different <strong>heightMagnitude</strong> to make each arm follow a different vertical path.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">// Create multiple layers
for (int o = 0; o &lt; layers; o++)
{
    // Create the arms
    for (int i = 0; i &lt; 100000; i++)
    {
        Vector3 position = GetPoint();
        position.Y -= heightMagnitude * Math.Sin(position.Magnitude * heightFrequency);
        
        // Colour and store each point
        ...
    }

    // Randomise the height magnitude for the next layer
    // so that each layer follows a different path
    heightMagnitude *= random.NextDouble();
    
    // 50% chance that the next layer will be flipped vertically
    if (random.NextDouble() &gt; 0.5)
        heightMagnitude *= -1;
}
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/diagram5.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>A galaxy with two layers</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="/blog/content/images/2019/05/diagram6-1.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"><figcaption>A galaxy with three layers</figcaption></figure><h3 id="in-practice">In Practice</h3><p>This side project began while developing the server selection screen for our first person shooter <a href="https://www.youtube.com/watch?v=pwmvMQy6p7E" rel="noopener">Sector&#x2019;s Edge</a>, where each map is represented by one of the stars:</p><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/seGCbKEuzEw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><h3 id="extreme-examples">Extreme Examples</h3><p>Below are some extreme examples of galaxies with the parameters that were used to generate them:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="/blog/content/images/2019/05/diagram7.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"></figure><figure class="kg-card kg-image-card kg-width-wide"><img src="/blog/content/images/2019/05/diagram10.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"></figure><figure class="kg-card kg-image-card kg-width-wide"><img src="/blog/content/images/2019/05/aextreme1.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"></figure><figure class="kg-card kg-image-card kg-width-wide"><img src="/blog/content/images/2019/05/aextreme3.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"></figure><figure class="kg-card kg-image-card kg-width-wide"><img src="/blog/content/images/2019/05/aextreme4.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"></figure><figure class="kg-card kg-image-card kg-width-wide"><img src="/blog/content/images/2019/05/aextreme5.jpg" class="kg-image" alt="Random Galaxy Generation with C# and OpenGL" loading="lazy"></figure>]]></content:encoded></item></channel></rss>