This is the first part of the Low Poly World series, covering the basics of getting a globe procedurally generated and rendering inside the Unreal Engine.
This will rely on the Procedual Mesh Component which UE4 now has which allows us to create and modify actual mesh geometry at runtime.
A project including all of this, along with the rest of the Low Poly World series, can be found here on Github.
None of the standard UE4 kits are similar to what we're doing here and so we can start with a blank slate, and there's no need for the starter content as all Materials and so forth will be customized to suit the low-poly style.
As I'm going to be using Git I'm also adding the standard UE4 .gitignore at this point so that it doesn't check in unwanted data.
Although the Procedural Mesh Component we're going to be using would allow us to create the geometry from scratch it would be a lot of effort, requiring us to build the vertices and triangle data inside Blueprint. A much easier approach is to create a simple sphere in a 3d package and then use that as the base geometry for the procedural world, just moving the vertices as needed.
I'm going to be using MODO for this, but this can be done in any 3d package.
All we need is a sphere, but if we want it to deform correctly we need to get the underlying geometry right. There are three basic geometries most packages support, Globe, Quadball, and Tessellation. For this you need Tessllation as this guarantees that the points are evenly spaced round the surfaces.
The level of tessellation controls how many polygons you have, and so for flexibility I'm going to produce a range of meshes with different tessellation values, giving between 80 and 21,780 triangles per globe, and allow the user to choose which they use.
To keep all of the maths easy the sphere is centered on the origin, and with a radius of .5m.
After exporting these meshes as .fbx files they can just be imported into the Unreal Engine 4 project as standard static meshes.
IMPORTANT! In order for the Procedural Mesh Component to be able to use this as a base you need to se the "Allow CPUAccess" flag for your mesh. To do this open up the Mesh Editor and search for "Allow CPUAccess" in the details, make sure it's checked, and save the mesh.
Now the globes are in the editor we can drag one into the game world, give it a simple monochrome Material, and see that it looks awful.
It's all blobby with no crisp edges or flat faces, it's not a pretty sight.
The problem lies in the normals. The standard approach for shading relies on having a normal at each vertex and then smoothly interpolating across each triangle to get the proper lighting, but here we don't want that smooth interpolation as we want each triangle to appear flat and for that it needs a uniform normal.
There are two possible solutions for this. One is to go back to the 3d editor and splitting all of the edges, this gives each triangle its own copy of a vertex with its own normal but results in extra geometry, and the second is to fix it in the material using the approach here.
As we'd need to split all lowpoly models, resulting in considerable added geometry, I'm going to go for the material for now.
To do this I'm creating a new Material,
M_LowpolyDemo, unchecking the "Tangent Space Normals" which makes the calculation simpler, and plugging in the minimum to get the lowpoly appearance.
Applying this material to the globe gives the sharp edged look we're after, even though it's not looking right yet as we have bad lighting and other issues to deal with.
For now though we can take this and build a more useful Material from it, allowing the user to specify the BaseColor, Roughness, Emissive, and Metallic settings for the surface. While not yet supporting textures, that'll come later, this can be used for the basis of a nice enough low-poly set of Material Instances to get us started.
A quick and simple way to get nicer lighting so we can see what we're doing is to use a spherical 360 HDR image for our lighting.
Without getting into too much detail here what this involves is importing a wrap-around photo of a real-world scene which has been photographed in such a way that it captures the lighting well enough to be used as a light in a 3d scene.
Don't worry, it's a lot easier than it likely sounds.
There's a nice collection of suitable images called the sIBL Archive, and for this I'm using the Helipad Afternoon image by Blochi. Amongst the files in the archive are a few .hdr files, which are the ones we're interested in. As I'm planning on using this for fairly sharp reflections on some objects, so want a lot of detail, I'm taking the larger
LA_Downtown_Afternoon_Fishing_3k.hdr image, which I import into my project like a standard image.
I now add a "Sky Light" to my scene from the Lights palette, change SourceType to SLS Specified Cubemap to use an image for the lighting rather than sampling the game scene, and select the LA_Downtown_Afternoon_Fishing_3k image for the Cubemap. As I'm going to be using this for my main light source I also increase the Intensity up to 4.0, and reduce the "Light Source" down to Intensity 0.0.
Looking at the globe the lighting looks a lot nicer, the harsh lighting and dark shadows have been replaced by a set of more subtle lights. This will really start to shine when we have more complex materials and geometry to take advantage of it.
Having set things up so that we have things rendering nicely it's time to start actually building our globe.
Create a new Blueprint, BP_Globe, with Actor as the parent class.
We're going to be using a "Procedural Mesh" component for the actual planet's surface geometry so add one and name it SurfaceMesh.
To get things started we're going to make it so it just copies the geometry from a globe's Static Mesh, and applies a Material. To do this we'll need two new variables, GlobeStaticMesh which is a Static Mesh reference and will provide us with the base geometry, and GlobeSurfaceMaterial which is a Material Interface (Which allows both Material and Material Instance to be assigned) reference. Both of these should be editable, but we can also point them at a globe and a lowpoly material to provide sensible defaults.
We now create a new Function called "RebuildGlobeSurface", and we link this to the "Construction Script" node. If you're not familiar with Construction Scripts they allow you to do initialization of a Blueprint Class when it's created, allowing some quite complex setups. More details are in the UE4 documentation.
In order to copy the static mesh into the procedural mesh, to allow us to change it later on, we need to do the following:
Here's a Blueprint which does all of this.
If this all works we should now be able to drag an instance of our new Blueprint into the game world, point it at a globe mesh and a material, and see a globe appear. So far it's still flat, but now we have the procedural mesh working we can start to look into getting some shape there.
If it doesn't work then the first thing to check is that you've enabled "Allow CPUAccess" on the Static Mesh you're using for your globe, without that the Procedural Mesh cannot access the geometry data.
To shape the globe we need to calculate how high the surface should be at each point of the sphere and offset the vertices to match this. To keep things tidy the calculation part can be done in a new function called CalculateHeightAtPoint.
This function will take a single Input of a Vector called Position and return a single Output of a float called Height. The Position input tells us where on the surface we are, and is actually the vector from the center of the globe to this point, and the Height is a calculated height in the range of 0 (Lowest point) to 1 (Highest point).
TODO: Height diagram for how this will work
For now we can just build a lumpy surface using sine waves, it won't be very realistic but can create some fairly attractive simple little planets without too much work.
Create a new Vector variable called HeightFrequency to control the surface, give it a default value of 0,0,0 as this'll mean that if it's not set the surface will be flat.
Now for some maths. This is making a surface from three sine waves along the X, Y, and Z axis, adding them together, and scaling them in the range 0-1. Don't worry too much if you can't follow what this is doing- we're going to replace it with a much better and easier to understand system based on Unreal's Materials later on, this is just to get us going.
As this function doesn't change anything in the Blueprint, just calculates values, we can check the "Pure" button to allow this function to be called without an Execution Pin- more like Unreal's maths functions.
Now we have a function to calculate height we need to use it to offset our geometry. Looking at the RebuildGlobeSurface function we can see how the mesh data is passed through directly from Get Section from Static Mesh to Create Mesh Section, and to actually shape the globe we need to be able to change that data before it's used.
The first thing to do is to delete the lines connecting Normals and Tangents, which will force Unreal to rebuild these based on the Vertices. When we were just copying the data we knew the normals and tangents of the procedural mesh would be the same as the static mesh, but once we start moving things that's no longer true and so they'll need to be recalculated.
Now we need somewhere to store our transformed vertices and so create a new Local Variable called TransformedVertices, and this is an Array of Vectors. We know that each mesh section will need to have its own values of these so we Clear the array before we get the mesh data in order to have a blank slate.
As we're going to need to calculate the actual position from the 0-1 heights that CalculateHeightAsPoint gives we'll also need two new Float variables MinHeight and MaxHeight. Create these now, make them editable, and give MinHeight a value of 50 and MaxHeight a value of 100 so that we get manageably small spheres by default.
Now to calculate the new TransformedVertices we'll need to loop over the Vertices returned from Get Section from Static Mesh, pass each one through our CalculateHeightAtPoint function, do a Lerp to calculate the actual distance from the center of the object for each point from the 0-1 range of height, multiply by normalized vertex (to convert it into a Vector) and add the result to TransformedVertices.
Quite a bit of work, and you can see the final Blueprint below.
With this we're now able to create lumpy globes, varying the MinHeight, MaxHeight, and HeightFrequency to create different appearances. The following one has MinHeight=80, MaxHeight=100, and HeightFrequency=11,12,13.
Now we have a simple land surface it's time to add some water to make things more interesting to look at and provide a useful base to continue building on.
Open up the Blueprint editor for BP_Globe again and add a StaticMesh, calling it SeaMesh. Add two new editable variables, a Float called SeaHeight defaulting to 75 to match our default ground height, and another Material Interface called SeaMaterial with a Material of your choice as default.
To get things working go into the Rebuild Globe Surface function, and we'll add a new item into the main Sequence before we go into the "Delete all existing geometry" part.
This will be much simpler than all of the procedural stuff we've been doing.
First we need to Set Static Mesh on the SeaMesh, giving it the same GlobeStaticMesh we used as the basis for the land to keep things lined up nicely, then we can call Set Material to set SeaMesh's material to SeaMaterial.
Finally we need to scale the whole thing up. If you remember the globe was built with a radius of 0.5m which means we need to call Set Relative Scale 3D on SeaMesh to scale it uniformly up to SeaHeight * 0.02. This is because we need to scale it down by 100x to convert from meters to cm (Or UU), and then x2 as it was 0.5m radius not 1m.
The changes to Rebuild Globe Surface end up looking like this.
With this in place out BP_Globe, with simple instances of M_LowpolyUntextured, SeaHeight=95, MinHeight=90, MaxHeight=100, and HeightFrequency=11,12,12 starts to look half decent.
..and that's it for now.
We now have a simple procedural globe including both land and sea, with materials which show off their low-poly nature. There's still a lot to do before this can be used as the basis for a game but we're off to a nice start.