Progress Update #4
Multithreading Hell
So it took awhile but it's finally "done". I have managed to parallelize the terrain generation of the planets. To be honest it's basically held together with duct tape but it works and that's all I honestly care about right now. The terrain of the planets generates soooooo much faster at higher resolutions, and the whole program doesn't lock up while the terrain is being generated.
Starting out, I thought that multithreading would be a common thing to do in Unreal, and thus I thought there would be good documentation on the subject. Apparently not. There was very little to go off when I first started so I went searching the web for answers.
I looked up C++ multithreading and it looked simple enough. Just #include <thread> and call whatever function you want on a seperate thread. Like this. But then somewhere along this journey I discovered that Unreal had its own system for multithreading that had its own special classes and functions to deal with. When finding this I got excited and quite worried. Excited because I know that Unreal has many things that are very powerful when used correctly and I anticipated this being one of them; and worried because I also knew that these powerful tools are very easy to screw up because of the shotty quality of documentation. So down the rabbit hole I went.
I looked everywhere for some coherent resource that gave all the information I needed in one place. I searched forums, YouTube, stack overflow and beyond until finally I stumbled back to Orfeas Eleftheriou's C++ tutorials. Specifically this one posted all the way back in May of 2016. Seriously though, this single blog post is the sacred text if you want to do C++ multithreading in Unreal (and as a bonus it even shows that you can multithread blueprint nodes).
After I had made the AsyncTask in my own project I booted it up, generated some terrain and... nothing. I checked the output log and there were prints signifying that the task had finished successfully, but nothing had changed, the planet still looked the same. So I dug in a little and found that I was no longer calling the CreateMeshSection method that would actually, you know, make the mesh. So I added that, generated another planet and... crash. Oh no.
An ensure statement got tripped: ensure(GetShadowIndex() == 0) I had no idea what happened, and the comments in the file weren't any help at understanding what just happened, so I went to Google for help. Funny enough it looked like someone had the exact same problem as me as there was an answerhub thread about it. I looked into it and this person and I had very similar experiences, he says: "I'm trying to move my RuntimeMeshComponent calculations to a separate thread... usually the update would kick in and you would see the procedural mesh updated. But now I'm getting [the] error listed". I thought this would be the easy solution to my problem, but then I saw that the thread was from October of 2016 and had ZERO answers. This was not a good sign.
Maybe you aren't meant to generate a procedural mesh from another thread. It makes sense now that I say that, but at the time I was determined to make this work. In hindsight I spent way too much time trying to solve this instead of just trying something else. I spent probably about a week reading documentation, looking through IConsoleManager.h and AsyncWork.h, and trying to find any way I could work around it but I just could not. But I still needed some way to generate the mesh after all the calculations had been made.
Now this is where my brain went into what I call "stupid mode". Just throwing whatever shitty solution I came up with at the compiler and if it didn't work just revert the files. I can't even remember how many dumb solutions I came up with that failed, but eventually, by some miracle, like a million monkeys writing for a million years, I had finally written Shakespeare and Unreal didn't crash when I generated the planet. And I'm not sure what actually solved it because I was just kind of running on autopilot at that point but I think my thought process went something like this:
Me Also Me
We can't under any circumstances generate the mesh exactly when the calculations are finished, so the generate mesh
method has to go somewhere after that.
Basically this was because I had forgot, the code is no longer running top to bottom in a single file line when you have multiple threads going. So this was the problem code:
Mainly just line 8 though. See, before I started multithreading, this block would run all six terrain faces then once they had all done their calculations then and only then would line 8 run. Line 8 would set the new minimum and maximum elevation to be used in the texture. Now though, it was getting to line 8 before all the faces had finished generating leading to the weirdness in the above picture. To fix this I moved line 8 to the color generation as well.
So what comes after the calculations?
The color generation.
Okay, so put the method in there and run it.
But that's bad programming practice. The color generation
function should only deal with color.
We can just clean it up when we're done.
Also we don't even know if it'll work.
Fair point.
So it did end up working and I'll have to rename some functions because the threaded task also calculates UVs and all color related stuff making the old color generation kind of obsolete. But there was another problem now. The color of some faces would go weird.
Basically this was because I had forgot, the code is no longer running top to bottom in a single file line when you have multiple threads going. So this was the problem code:
1 2 3 4 5 6 7 8 | for (int i = 0; i < 6; i++) { if (meshes[i]->IsVisibleInEditor()) { terrainFaces[i]->ConstructMeshAsync(colorGenerator); } } colorGenerator->UpdateElevation(shapeGenerator->ElevationMinMax); |
With all that done, multithreading is, for the most part, stable.
If you feel so inclined to read it, here's the code in its current form (or just go to the github page):
TerrainFace.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | class CPPGAME_API TerrainFace { public: TerrainFace(ShapeGenerator* shapeGenerator, UProceduralMeshComponent* mesh, int resolution, FVector localUp); ~TerrainFace(); TArray<FVector> verticies; TArray<int> triangles; TArray<FVector2D> uv; TArray<FVector> normals; TArray<FProcMeshTangent> tangents; UProceduralMeshComponent* Mesh; ShapeGenerator* shapeGenerator; TArray<FColor> VertexColors; int resolution; FVector localUp; FVector axisA; FVector axisB; void ColorMesh(UColorSettings* CS); void ConstructMesh(ColorGenerator* colorGenerator); void ConstructMeshAsync(ColorGenerator* colorGenerator); void UpdateUVs(); void UpdateTangentsNormals(); }; //================================================================================================================ namespace ThreadingCalculations { static void CalculateMesh(int resolution, FVector localUp, FVector axisA, FVector axisB, TArray<FVector>& verticies, TArray<int>& triangles, TArray<FVector2D>& uv, ShapeGenerator* shapeGenerator, ColorGenerator* colorGenerator) { int triIndex = 0; uv.Empty(); uv.SetNum(resolution * resolution); for (int y = 0; y < resolution; y++) { for (int x = 0; x < resolution; x++) { int i = x + y * resolution; FVector2D percent = FVector2D(x, y) / (resolution - 1); FVector pointOnUnitCube = -localUp + (percent.X - .5f) * 2 * axisA + (percent.Y - .5f) * 2 * axisB; FVector pointOnUnitSphere = pointOnUnitCube.GetSafeNormal(); float unscaledElevation = shapeGenerator->CalculateUnscaledElevation(pointOnUnitSphere); verticies.EmplaceAt(i, pointOnUnitSphere * shapeGenerator->GetScaledElevation(unscaledElevation)); uv[i].X = colorGenerator->BiomePercentFromPoint(pointOnUnitSphere); uv[i].Y = unscaledElevation; if (x != resolution - 1 && y != resolution - 1) { triangles.Insert(i, triIndex); triangles.Insert(i + resolution + 1, triIndex + 1); triangles.Insert(i + resolution, triIndex + 2); triangles.Insert(i, triIndex + 3); triangles.Insert(i + 1, triIndex + 4); triangles.Insert(i + resolution + 1, triIndex + 5); triIndex += 6; } } } return; } } //================================================================================================================ class ConstructMeshAsyncTask : public FNonAbandonableTask { int resolution; FVector localUp; FVector axisA; FVector axisB; TArray<FColor> VertexColors; TArray<FVector>& verticies; TArray<int>& triangles; TArray<FVector2D>& uv; TArray<FVector> normals; TArray<FProcMeshTangent> tangents; ShapeGenerator* shapeGenerator; ColorGenerator* colorGenerator; public: ConstructMeshAsyncTask(int Resolution, FVector LocalUp, FVector AxisA, FVector AxisB, TArray<FVector>& Verticies, TArray<int>& Triangles, TArray<FVector2D>& Uv, ShapeGenerator* shape_Generator, ColorGenerator* color_Generator) : verticies(Verticies), triangles(Triangles), uv(Uv) { resolution = Resolution; localUp = LocalUp; axisA = AxisA; axisB = AxisB; shapeGenerator = shape_Generator; colorGenerator = color_Generator; } ~ConstructMeshAsyncTask() { UE_LOG(LogTemp, Log, TEXT("Terrain face task finished calculating. Destroying task.")); } void DoWork() { ThreadingCalculations::CalculateMesh(resolution, localUp, axisA, axisB, verticies, triangles, uv, shapeGenerator, colorGenerator); } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(ConstructMeshAsyncTask, STATGROUP_ThreadPoolAsyncTasks); } }; |
TerrainFace.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | TerrainFace::TerrainFace(ShapeGenerator* shapeGenerator, UProceduralMeshComponent* mesh, int resolution, FVector localUp) { this->shapeGenerator = shapeGenerator; this->Mesh = mesh; this->resolution = resolution; this->localUp = localUp; axisA = FVector(this->localUp.Y, this->localUp.Z, this->localUp.X); axisB = FVector().CrossProduct(this->localUp, axisA); } TerrainFace::~TerrainFace() { } void TerrainFace::ConstructMesh(ColorGenerator* colorGenerator) { int triIndex = 0; uv.Empty(); uv.SetNum(resolution * resolution); for (int y = 0; y < resolution; y++) { for (int x = 0; x < resolution; x++) { int i = x + y * resolution; FVector2D percent = FVector2D(x, y) / (resolution - 1); FVector pointOnUnitCube = -localUp + (percent.X - .5f) * 2 * axisA + (percent.Y - .5f) * 2 * axisB; FVector pointOnUnitSphere = pointOnUnitCube.GetSafeNormal(); float unscaledElevation = shapeGenerator->CalculateUnscaledElevation(pointOnUnitSphere); verticies.EmplaceAt(i, pointOnUnitSphere * shapeGenerator->GetScaledElevation(unscaledElevation)); uv[i].X = colorGenerator->BiomePercentFromPoint(pointOnUnitSphere); uv[i].Y = unscaledElevation; if (x != resolution - 1 && y != resolution - 1) { triangles.Insert(i, triIndex); triangles.Insert(i + resolution + 1, triIndex + 1); triangles.Insert(i + resolution, triIndex + 2); triangles.Insert(i, triIndex + 3); triangles.Insert(i + 1, triIndex + 4); triangles.Insert(i + resolution + 1, triIndex + 5); triIndex += 6; } } } Mesh->CreateMeshSection(0, verticies, triangles, normals, uv, VertexColors, tangents, false); } void TerrainFace::ConstructMeshAsync(ColorGenerator* colorGenerator) { TArray<FVector>& vertsRef = verticies; TArray<int>& trisRef = triangles; TArray<FVector2D>& uvRef = uv; (new FAutoDeleteAsyncTask<ConstructMeshAsyncTask>(resolution, localUp, axisA, axisB, vertsRef, trisRef, uvRef, shapeGenerator, colorGenerator))->StartBackgroundTask(); } void TerrainFace::UpdateUVs() { Mesh->CreateMeshSection(0, verticies, triangles, normals, uv, VertexColors, tangents, false); } void TerrainFace::UpdateTangentsNormals() { UKismetProceduralMeshLibrary::CalculateTangentsForMesh(verticies, triangles, TArray<FVector2D>(), normals, tangents); Mesh->UpdateMeshSection(0, verticies, normals, uv, VertexColors, tangents); } |
Comments
Post a Comment