Progress Update #17
Asteroid Heightmaps in UE4
The task for this week was to make asteroids more interesting by having their meshes not be plain old cubes. The original plan was to basically copy the terrain generation system from the terrestrial planets, but instead I tried challenging myself by creating a world displacement material instead.
Method of Terrain:
Starting out this feature I intended to copy, essentially line for line, the terrain generation process of terrestrial planets. This wouldn't have been terribly difficult but would defiantly have been very boring, so I looked for an alternative.
I knew from messing around in the editor that you can offset vertices of a mesh through a material, so googling that led me to a video which explained all the ways you can fake depth through a material. This video also introduced me to tessellation which adds more vertices to the mesh during the vertex shading stage of a shader (or material in UE4).
From the information in the video, I came to the solution that I could use a cube mesh with tessellation to add more vertices then use a noise-based heightmap for world displacement to offset those new vertices into the shape of the lumpy asteroid. This approach would hopefully be the most performant as the game wouldn't have to create multiple full, high detail, meshes.
Tri-Planar Mapping Round 2:
I also tried tri-planar mapping again for applying the texture to the mesh with the help of another video that I found. This actually worked and I created a material function based off it that takes in a scale and outputs tri-planar UVs:
https://blueprintue.com/blueprint/uqpcfclz/ |
The only issue is that, at the moment, it only works when applied to static meshes and freaks out a bit when you apply it to Niagara mesh particles. This is because the "local position" gets the position of the emitter actor and not the individual particle. There is a "Particle Position " node and I'm sure there would be some way of getting a local position from this but the deadline forced me to leave it be for now. (Spoiler alert: many more problems came from applying the material to mesh particles and not static meshes)
The Material:
https://blueprintue.com/blueprint/o3rkbbu_/ |
The emissive color is pretty standard for this game; just a texture lerped in intensity based off it's direction to the sun. One issue that came up here is that since the cube that the material is applied to has only 24 vertices at that stage of shading, there are only 24 pixel normals that can be calculated for meaning that the "lighting" is that of a perfect cube.
The world displacement starts with the sphere mask texture. The texture itself contains the absolute inverse normals for each vertex on the "sphere". Once these inverse normals are inverted to give regular normals, the heightmap is added. the heightmap comes from a Texture2DArray which I found just for this feature.
The world displacement starts with the sphere mask texture. The texture itself contains the absolute inverse normals for each vertex on the "sphere". Once these inverse normals are inverted to give regular normals, the heightmap is added. the heightmap comes from a Texture2DArray which I found just for this feature.
The way it works is pretty self-explanatory: it's an array of 2D textures and the texture or element that you want to access is controlled by a third UV channel W. Each asteroid particle should choose a different W value (you can see how I tried to do that with the HeightmapNum parameter and particle random value) to get a different heightmap. ParticleRandomValue and ParticleDynamicValue both wouldn't work here because you can't use those values during the vertex stage of shading, and I couldn't figure out how to get Niagara to set a static material parameter so at the last moment I decided to hook it up to world position and that works well enough for now.
Once the heightmap is added we then multiply by the vertex normal to the the negative faces to have negative values. Doing this though causes the sphere to be a little deformed but honestly this is the best I could do. I could seriously write a novel about all the ways I tried to fix minor problems with this shader but I won't right now for the sake of brevity.
The final addition of some scaled vertex normals adds just enough of an offset so that the planes of what was once a cube don't intersect each other.
The Asteroid Manager:
The class that I assigned to make the heightmaps is the AAsteroidManager. It should be a singleton, but time constraints forced me to focus more on getting the thing working than perfectly implementing design patterns. At some point in the future I will get around to implementing it as a singleton but for now just know that was the intent.
At the moment, AAsteroidManager is responsible for creating heightmaps and that's about it. I didn't have time to get it to apply to the material, so right now that has to be done manually, but that should be able to be fixed when I have a free afternoon.
Anyway, here's all the code and a broad explanation of how it works:
AsteroidManager.h
AsteroidManager.cpp
NewVariants()
Line 20 we update the settings on the noise generator so that the noise output has the properties given by ShapeSettings.
Lines 22-36 we create the texture array asset
Line 39 can be uncommented to create the asteroid sphere mask, but since it outputs the same texture every time I commented it out
Lines 42-49 we create the new heightmaps and add them to the texture array
Line 51 at this point the texture array should be fine but in the editor the asset doesn't seem to recognize that it's textures have changed so this line was trying to force the asset to update (this is what I did when updating materials) but it didn't work.
CreateHeightmapTexture()
This is the standard function I use to create a texture with 2 changes.
Line 74 we get a random number 0-1 and make it bigger by multiplying by 1000 which is then used on line 82 when we sample some noise.
CreateSphereTexture(FString TextureName)
Exact same standard texture creation function with one difference.
Line 144 we get the 3D location on a unit sphere of the current point on the texture.
PointOnUnitSphere(FVector2D pointOnUnitSquare)
Line 183 we define the normals for each face
Lines 186-199 we find the face of the cube that the current point is on, returning zero if it isn't.
Lines 201-208 are ripped from the terrestrial planet generation with only a change to percent to make it work on a flat plane.
Line 209 we have to absolute value because textures can't have negative values. The negatives are put back in the material when we multiply by the vertex normal
IsPointWithinFace(FVector2D pointToTest, int8 faceToTest)
Used on line 190 when finding the corresponding face to point on the texture.
This function compares the pointToTest against the UVs of Coordinates (defined in the private section of the header file) where the zeroth index is the top left corner and the first is the bottom right.
Comments
Post a Comment