BrickGame

WorldMapI’m way behind on blogging about this, but in April 2014 I spent a month making a plugin for Unreal Engine 4 for rendering and interacting with Minecraft-style voxel worlds, along with a simple game called BrickGame to demonstrate it.

I released the source code for the plugin and game on Github. You can of course see how it all works by looking at the code, but there are some tradeoffs I made that deserve some explanation.

Regions and components

In BrickGame, the world is divided into regions, and regions into smaller render and collision components which correspond to UE4 PrimitiveComponents. Regions are generated, and render/collision components created in some radius around the player. The benefit to the different granularity of procedural brick generation and render components is that a region can span the whole Z axis of the world, which allows the procedural generation to only sample 2D noise functions once for each XY, but still divide the world into multiple render components on the Z axis for finer grained visibility culling and dynamic geometry updates.

Collision components are still smaller than the render components, and only created in a much smaller radius around the player than you can see. The small size is to avoid hitches when creating new collision components, and the small radius is to avoid limits in the total number of PhysX box elements. Since I wrote this code, UE4 added support for PhysX triangle mesh collision created at runtime, which would avoid the small radius problem, but would probably negatively affect performance and memory.

Geometry

The render components create a vertex buffer and an index buffer on the CPU. I don’t do anything to merge adjacent coplanar faces. I do use two tricks to minimize the amount of vertex data:

  • UVs are derived from the world-space position using a single, planar projection that is 45 degrees off the axes that the face normals are on.
  • I render faces with different normals in separate draw calls, and use a separate vertex stream with a stride of 0 to provide a single tangent basis for all those faces.

Each vertex consumes 4 bytes, with an 8-bit/component XYZ in the render component’s coordinate system, and an 8-bit ambient occlusion factor. A separate draw call is made for each face direction in a render component that may be visible with the vertex tangent basis bound to a zero-stride vertex stream.

This is tuned to minimize the cost of recreating the vertex and index buffers when a render component is modified or first becomes visible. As the player moves around and modifies bricks, render components must be created and updated. To maintain a smooth framerate, the amount of time it will spend doing so each frame is limited. Reducing the amount of time to create the vertex and index data allows either more components to be updated each frame, or the use of larger components that improve rendering efficiency.

The component sizes are configurable, but the settings I used for BrickGame use large components to minimize per-component CPU and GPU overhead, and enable large view distances. BrickGame uses 32x32x128 regions, 32x32x32 render components, and 16x16x32 collision components.

Global Illumination and Emissive Bricks

EmissiveBricks
BrickGame uses UE4’s Light Propagation Volumes for both Global Illumination and emissive bricks. It’s more expensive than it needs to be for a voxel world, but it was easy and mostly just works. However, I ran into problems with the reflective shadow map rendering time spiking when the sun reached the horizon, and disabling the sun’s directional light disables LPV, which causes the emissive bricks to stop working. My workaround was to add a moon that is always above the horizon, and switch the directional light to it once the sun approaches the horizon. This keeps the directional light at a high enough angle that performance is good, and keeps the emissive bricks working.

Adding a moon required modifying UE4’s starter sky blueprint and material. I added the moon to the material, and added a directional light component to the blueprint that it uses for either the sun or the moon, depending on how high the sun is above the horizon. The moon simply circles high enough above the horizon to avoid the performance problems with the reflective shadow maps.

Clouds

While I was modifying the sky material, I took the opportunity to add volumetric clouds. The clouds are ray-traced by discrete steps through a parametric noise function. It uses only four samples to determine opacity, and four to determine lighting. This is helped greatly by jittering the samples in cooperation with UE4’s temporal AA. Without the jittering, discrete cloud layers are very obvious, and with the jittering it looks smooth. I think the result is good, but by the time I was done I regretted the distraction from the brick-rendering part of the project.

Engine changes

The BrickGame code will work with completely stock UE4 code, but there are some engine changes necessary for it to work well:

Ambient Occlusion

WithAndWithoutAO

When I initially wrote BrickGame, I decided to compute the ambient occlusion on the CPU, pass it to the material through a vertex attribute, and output it from the material where “baked” ambient occlusion would be output. The goal was to avoid the need to modify the engine (which is so far required if you want to change any shaders). However, I eventually realized that I wanted to make this baked ambient occlusion only affect ambient lighting, not bounced lighting from LPVs, which I implemented with a one line shader change in the engine. Ironically, later versions of UE4 removed support for material-driven ambient occlusion, and so to make my approach work at all requires (small) changes to the engine code. Once it’s possible to add shaders without modifying the engine, it would be nice to come up with a GPU-based ambient occlusion solution.

Given that it eventually became necessary to modify the engine to make the CPU-based ambient occlusion work, and the amount of effort required to make the LPV-based emissive bricks work, it would have been better to just do GPU-based lighting.

Backface culling

The second engine change I made was later in the project, when I was focused on performance. I discovered that doing some large-scale culling of backfaces on the CPU reduces the GPU cost substantially. To do that within the UE4 renderer’s fast path for static meshes required some changes to the engine. Epic has since then added similar functionality to the engine, but it’s not exactly what I need. I’m hoping to eventually submit a pull request to Epic with a change that merges the functionality of our similar static mesh culling changes.

Merging

After spending April intensely focused on BrickGame, I have been focused on my programming language and my non-programming projects. I’ve updated BrickGame and my engine changes to new versions of UE4 a few times, but each time it has been incredibly painful to integrate the engine changes. This is mostly an issue with my tools (and likely my lack of understanding of them): I could have done these merges in 15 minutes using P4+Araxis, but using SourceTree/Git to merge simply doesn’t work in ways that I haven’t yet figured out an explanation for. To summarize, I have a fork of Epic’s UE4 Github repository, and a branch within that fork which contains my changes on top of version N of UE4. When I try to UE4 merge version N+1 into that branch:

  • Git reports merge conflicts in thousands of files I’ve never touched
  • Using SourceTree to manually “accept theirs” on the conflicts doesn’t work if any of the conflicts are new files from Epic’s repo (yes, that’s apparently a conflict). So I have to manually accept thousands of files in small chunks
  • Hunks being merged from Epic’s repo that are simple deletions aren’t merged (e.g. deleting a file, or deleting some lines of code without any nearby added lines)

This should be a very simple merge, so there has GOT to be something I’m doing wrong. If anybody has any ideas, please let me know.

6 thoughts on “BrickGame

  1. kuranes

    Can happen with git…
    – try changing git merge algo http://git-scm.com/docs/git-merge#_merge_strategies (google for those to get more insight)
    – just add –patience to merge. (no algo change, but “slower” thus no default…)
    Or try anothr workflow
    – rebase ue4 into your code prior merge ( see git help rebase or https://www.atlassian.com/git/tutorials/merging-vs-rebasing/conceptual-overview )
    – cherrypick your commits into the ue4 branch (if there’s not much of them, but even there you can squish them)

    I would strongly suggest taking some time to read more on git, a good tutorial that really gives the keys like http://mrchlblng.me/2014/09/practical-git-introduction/ so that you gain time and avoid maddening moments.

    Reply
    1. andrew Post author

      I’ll read that tutorial; thanks!

      What I did last time was turning my changes into a patch file (and not a very large one), and then applying it to a fresh UE 4.5 branch.

      Once 4.7 is out I’m planning to try rebasing Epic’s 4.7 onto my branch. I’ve avoided rebasing since my changes are pushed to Github, but in this case I think it would work because it would be my first push of the 4.7 changes to my repo.. not 100% sure.

      Reply
  2. Christian

    Hello Andrew,

    first of all, let me thank you for this great project. It is by far the best looking “Minecraft Style” Terrain I have seen so far. I am no pro at UE4 Coding, but I feel like it might be a good basis for a project I want to realize.
    The idea is a Game that uses a RTS top down perspective, but using terrain that is modifyable, like the brick terrain here. In order to use it though, I would need to make some modifications and wondered if you could hint me into the right direction where to look at in your source. I tried digging through it last evening, but since it is so complex, I am having a hard time understanding what is happening where.

    In order to use it, I would have to make the following limitations:

    The terrain I need has fixed dimensions and is not endless, like minecraft is. It also need to be on a plane, with no caves underneath or stuff like it. I basically need a plane of land, with some mountains on top, in which you can dig tunnels into etc. Also for rendering, I wonder if it is possible to cut it off at the top, so that mountains dont stick into the camera too deep. I hope I made it somehow clear what I mean. It is sometimes difficult, since english is not my native language.

    Would be cool, if you could give me some hints.

    Cheers,

    Christian

    Reply
    1. Miguel Lemos

      Hey, I am trying to develop an RTS on top of andrew amazing project too! We should team up! hahaha

      Reply
  3. andrew Post author

    Hi Christian,

    Sorry for the late reply, but your comment was sitting in a pending queue for a while that I forgot to check.

    BrickGame should be able to do exactly what you say. It is by default constrained in height, so you can’t have mountains that are too high. The same mechanism works for the horizontal axes, they are just set to extremely large numbers. Basically, you can do this by just changing the MinRegionCoordinates/MaxRegionCoordinates members of the BrickGridParameters struct that you pass to the BrickGridComponent’s Init method.

    Andrew

    Reply
    1. Miguel Lemos

      Like the other comment said, your work is amazing. Hands down the best tutorial to get into UE4 by reading beautiful written code. A question that I always wanted to ask you was how to implement caching!

      Have caching crossed your mind at some point? Because by making a render cache all that information could be calculated no more!

      I am no expert, really, and that is why I came here to talk about it instead of coding it like a boss and then do something on GitHub so it would cross your path and you would see it done.

      Again, your work really shows how familiar you are with the engine. Really cool to look at.

      Reply

Leave a Reply to andrew Cancel reply

Your email address will not be published. Required fields are marked *