📜 ⬆️ ⬇️

Creating a dynamically changing landscape for RTS on Unity3D

Once upon a time I had the joy of playing the wonderful RTS called “Perimeter: Geometry of Warriors” from the local developer KD Labs. This is a game about how huge flying cities called Frames plow the expanses of Spunja - a chain of interconnected worlds. The plot is rather strange and abstract, but a much more interesting and innovative component of the game was one of its technical features, and not the plot. Unlike most RTS, where battles take place on static terrain, in the "Perimeter", one of the key game mechanics was terraforming. The player had the means to manipulate the landscape in order to build his structures on it, as well as a whole arsenal of combat units capable of turning this landscape into cracked, sailing and belching heated stones / nasty insects.

As you know, the world of RTS is now experiencing some decline. Indie developers are too busy riveting retro platformers and rouge-like games of insubstantial complexity, and therefore, having replayed Perimeter some time ago, I decided that I should try to implement something like that myself - the idea was interesting and technical and gameplay points of view. Having some practical experience in game development (I tried to do something on XNA before), I thought that in order to achieve at least some success alone I would have to use something higher level and simpler. My choice fell on Unity 3D, whose fifth version just came out from under the press.

Armed with a car of enthusiasm, inspiration from the newly completed “Perimeter” and a scanned series of video tutorials on Unity, I began to make sketches and get acquainted with the tools that the Unity Editor offered me.

What does the community offer?


As always, my first pancake came out lumpy. Without sufficient deliberation, I began to implement the landscape using a plane and code that was supposed to raise or lower the vertices of this plane. Many readers, at least a little familiar with Unity, may object, “But after all, Unity has a Terrain component designed specifically for this purpose!” - and they will be right. The only problem is that I, being too passionate about the implementation of my idea, forgot about one important thing: RTFM! I studied the documentation and forums a little more thoroughly; I would not solve the problem in such an openly foolish way, but I would immediately use the ready-made component.
')
After two days of useless sweat and algorithmism (the plane was clearly not intended to be used for such purposes), I started making terrain using Terrain. I must say that among the individual members of the Unity community, there was the idea of ​​creating a dynamic landscape for your game. Some people asked questions in the forums and received answers. They were recommended to use the SetHeights method, which takes as input a piece of normalized from 0f to 1f heightmap, which will be set starting from the point (xBase; yBase) on the selected terrain.

Pleased with the results of my searches, I began developing. The first working prototype was ready a couple of hours later and included a simple camera dvigalka (using the right key), a simple crater generator and actually adding these craters to the landscape by pressing the left key (you can grab the build and see here ).

By itself, the deforming part was obscenely simple.

This was a script similar to the following:
void ApplyAt(DeformationData data) { currentHeights = data.terrainData.GetHeights(data.X - data.W/2, data.Y - data.H/2, W, H); for (int i = 0; i < data.W; i++) { for (int j = 0; j < data.H; j++) { if (data.Type == DeformationType.Additive) currentHeights[i, j] += data.heightmap[i, j]; else currentHeights[i, j] *= data.heightmap[i, j]; } } data.terrainData.SetHeights(data.X - data.W/2, data.Y - data.H/2, currentHeights); } 


The DeformationData object contained the X and Y coordinates to which the deformation should be applied, normalized to heightmap, which additively or multiplicatively superimposed on the current landscape and the other boilerplate necessary for the deformation mechanism to work.

There was also a strain generator, allowing, for example,

generate crater according to specified parameters
 //     -  H, W   . //  ,  H -   , W -  . float GetDepth(float distanceToCenter, int holeDepth, int wallsHeight, int holeRadius, int craterRadius) { if (distanceToCenter <= holeRadius) { return (Mathf.Pow(distanceToCenter, 2) * (holeDepth + wallsHeight) / Mathf.Pow(holeRadius, 2)) - holeDepth; } else if (distanceToCenter <= craterRadius) { return Mathf.Pow(craterRadius - distanceToCenter, 2) * wallsHeight / Mathf.Pow(craterRadius - holeRadius, 2); } else return 0f; } float[,] Generate(int holeDepth, int wallsHeight, int holeRadius, int craterRadius) { var heightmap = new float[W, H]; for (var x = 0; x < W; x++) { for (var y = 0; y < H; y++) { var offsetX = x - W / 2; var offsetY = y - H / 2; var depth = GetDepth(Mathf.Sqrt(offsetX * offsetX + offsetY * offsetY), holeDepth, wallsHeight, holeRadius, craterRadius); heightmap[x, y] = depth; } } } 


And all this was the basis of the Tech Demo, if so of course you can put it.

Analysis of the results of the first attempt


If you watched Tech Demo, you probably immediately noticed certain problems in the mechanism of deformations. If you did not watch it (for which I do not blame you), then I will tell you what was wrong. The main problem was performance. More precisely, its complete absence. When the deformations began, the framerate fell to very small (on some machines unambiguous) numbers, which was unacceptable, because in fact there was no graphics in the game. I was able to find out that the SetHeights () method itself causes a very complicated series of LOD calculations for a landscape and for this reason is not suitable for landscape deformations in real time. It would seem that my hopes collapsed and the implementation of real-time deformations on Unity is impossible, but I did not give up and found out the obvious, but very important feature of the LOD recalculation mechanism.

The lower the resolution of the terrain height map, the less impact on performance when using SetHeights ().

The resolution of the height map is a parameter characterizing the quality of the landscape display. It is an integer (obviously), why in the snippet above, integer variables were used to indicate the coordinates on the map. And it can be more than the size of the landscape, for example, for a landscape of 256x256, you can set the resolution of the height map to 513, which will give the landscape accuracy and less angular outlines. Why 513, and not 512, I will tell in the next section.

Games with a resolution map of heights allowed me to find more or less optimal sizes for my configuration, but I was very disappointed with the results. For the successful application of such a landscape in the RTS, its size must be large enough so that at least two players can coexist on it for some time. According to my initial estimates, a 2x2 km map (or 2048x2048 Unity Units) should have been just right. In order not to notice the effect on the framerate of deformations in real time, the size of the landscape had to be no more than 512x512 units. Moreover, the single accuracy of the height map did not give the most impressive results when it came to visual quality. The landscape was angular and crooked in places, which required doubling the accuracy of height maps.

In general, things were not very good.

Super Terrain - concept and theory


  :         Super Terrain.        . 

Approximately then the following thought began to visit me: “Since we cannot make one big landscape and still have sufficient performance during deformations, why not make a lot of small ones and not place them side by side? “How are chunk's in Minecraft?” The idea was not bad, but there were some problems:


First problem

The first problem was quite trivial: I just chose a size for a 256x256 chunk with double precision (heightmap resolution = 513). This setup did not cause performance problems on my machine . Perhaps in the future it would be necessary to revise the size of the chunk, but at the current stage, this decision was fine with me.

Second problem

As for the second problem, she had two components. The first, obviously, was to equalize the heights of the neighboring "pixels" of the heights of the neighboring chunk's. It was during the solution of this problem that I understood why the resolution of the height map is a power of two + 1. I will demonstrate in the illustration:



It is obvious that in order to maintain the equality of heights in neighboring landscapes, the “last” pixel of the height map of the first landscape must be equal in height to the “first” pixel of the following:



Obviously, “Super Terrain” is the matrix of the Unity Terrain, united by the mechanism of imposing heightmap and deformation.

After the implementation of the code for combining landscapes was completed (the application of local deformations of small size was left for later - now it was necessary to develop a mechanism for creating a matrix of Terrains and initial initialization of the height map), another component of the second problem was discovered (no seams ), which fortunately was resolved quite simply. The problem was that for such a “blending” of landscapes, it is necessary to explain to Unity that they are shared and neighboring. For this, the developers provided the SetHeighbors method. I still do not quite understand how it works, but without it, artifacts with shadows and dark stripes appear at the junctions of landscapes.

Third problem

The most interesting and difficult of the three, this problem gave me no rest for at least a week. I discarded four different implementations until I reached the final one, which I will tell you about. I will immediately make a small remark about one important limitation of my implementation - it assumes that the local deformation cannot be larger than one chunk. The deformation can still be at their junction, however, the side of the deformation matrix should not exceed the resolution of the chunk elevation map and all of them should be square (elevation maps, chunks themselves, and deformations). In general, this is not a big limitation, since any deformation of a large size can be obtained by applying several small ones in turn. As for the "square", then this is a restriction of the height map. That is, only its height map should be square, on which there can be “zero” sections with additive or “single” with multiplicative applications.

The very idea of ​​the algorithm for the universal application of deformation was as follows:

  1. Divide the heightmap deformations into nine parts, one for each of the chunks, which can potentially be affected by the deformation. So the central part will be responsible for the deformation of the chunk, which is directly affected by the deformation, the sides are responsible for the chunk, which are left / right or top / bottom, etc. If the deformation does not change the chunk, then its component will be equal to null.
  2. Apply partial heightmap to the appropriate chunk, or ignore the changes if the partial heightmap is null.

Such an approach allows to apply universally both deformations located both in the very center of the chunk and not affecting other chunkes, and at the borders with them or even in the corners. It is necessary to divide it into nine parts (and not into four) due to the fact that if the deformation begins or ends at the border of the chunk, the boundary pixels of the one next to it must also be changed. (In order to avoid visible seams - see the solution to problem number 2).

Super Terrain - practice


Creating SuperTerrain

Within the framework of the second problem, the mezanism was developed, which allows to combine several Terrains into one and apply “global” height maps, which lie on the entire landscape as a whole.

Let me say at once that this possibility of the global use of the height map was needed because the landscape creation was procedural, and for its use the Square-Diamond algorithm was used , the output of which was a large matrix of floats — our large height map.

In general, creating SuperTerrain is a fairly simple and intuitive process, described

here
 /// <summary> /// Compound terrain object. /// </summary> public class SuperTerrain { /// <summary> /// Contains the array of subterrain objects /// </summary> private Terrain[,] subterrains; /// <summary> /// Superterrain detail. The resulting superterrain is 2^detail terrains. /// </summary> /// <value>The detail.</value> public int Detail { get; private set; } /// <summary> /// Parent gameobject to nest created terrains into. /// </summary> /// <value>The parent.</value> public Transform Parent { get; private set; } /// <summary> /// Builds the new terrain object. /// </summary> /// <returns>The new terrain.</returns> private Terrain BuildNewTerrain() { // Using this divisor because of internal workings of the engine. // The resulting terrain is still going to be subterrain size. var divisor = GameplayConstants.SuperTerrainHeightmapResolution / GameplayConstants.SubterrainSize * 2; var terrainData = new TerrainData { size = new Vector3 (GameplayConstants.SubterrainSize / divisor, GameplayConstants.WorldHeight, GameplayConstants.SubterrainSize / divisor), heightmapResolution = GameplayConstants.SuperTerrainHeightmapResolution }; var newTerrain = Terrain.CreateTerrainGameObject(terrainData).GetComponent<Terrain>(); newTerrain.transform.parent = Parent; newTerrain.transform.gameObject.layer = GameplayConstants.TerrainLayer; newTerrain.heightmapPixelError = GameplayConstants.SuperTerrainPixelError; return newTerrain; } /// <summary> /// Initializes the terrain array and moves the terrain transforms to match their position in the array. /// </summary> private void InitializeTerrainArray() { subterrains = new Terrain[Detail, Detail]; for (int x = 0; x < Detail; x++) { for (int y = 0; y < Detail; y++) { subterrains[y, x] = BuildNewTerrain(); subterrains[y, x].transform.Translate(new Vector3(x * GameplayConstants.SubterrainSize, 0f, y * GameplayConstants.SubterrainSize)); } } } /// <summary> /// Initializes a new instance of the <see cref="SuperTerrain"/> class. /// </summary> /// <param name="detail">Superterrain detail. The resultsing superterrain is 2^detail terrains.</param> /// <param name="parent">Parent gameobject to nest created terrains into.</param> public SuperTerrain(int detail, Transform parent) { Detail = detail; Parent = parent; InitializeTerrainArray(); SetNeighbors(); } /// <summary> /// Iterates through the terrain object and sets the neightbours to match LOD settings. /// </summary> private void SetNeighbors() { ForEachSubterrain ((x, y, subterrain) => { subterrain.SetNeighbors(SafeGetTerrain(x - 1, y), SafeGetTerrain(x, y + 1), SafeGetTerrain(x + 1, y), SafeGetTerrain(x, y - 1)); }); } #region [ Array Helpers ] /// <summary> /// Safely retrieves the terrain object from the array. /// </summary> /// <param name="x">The x coordinate.</param> /// <param name="y">The y coordinate.</param> private Terrain SafeGetTerrain(int x, int y) { if (x < 0 || y < 0 || x >= Detail || y >= Detail) return null; return subterrains[y, x]; } /// <summary> /// Iterates over terrain object and executes the given action /// </summary> /// <param name="lambda">Lambda.</param> private void ForEachSubterrain(Action<int, int, Terrain> lambda) { for (int x = 0; x < Detail; x++) { for (int y = 0; y < Detail; y++) { lambda (x, y, SafeGetTerrain(x, y)); } } } #endregion } 


The actual creation of landscapes takes place in the InitializeTerrainArray () method, which fills the Terrain array with new instances and moves them to the right place in the game world. The BuildNewTerrain () method creates the next instance and initializes it with the necessary parameters and also places the “parent” inside the GameObject (it is assumed that a game object will be created on the scene that contains the chunk and SuperTerrain in order not to pollute objects and simplify cleanup if need be.)

It also applies to the treatment of one of the problems with black stripes on the borders of the landscape - the SetNeighbours () method, which is iterated over the created landscapes and puts down their neighbors. Important note: the TerrainData.SetNeighbors () method should apply to all landscapes in a group. That is, if you indicated that landscape A is a neighbor on top of landscape B, then you also need to indicate that landscape B is a neighbor below for landscape A. This redundancy is not entirely clear, but it greatly simplifies the iterative application of the method, as in our case.

In the code above, there are several interesting points, for example, the use of the divisor when creating the next landscape. To be honest, I myself do not understand why this is necessary - just creating a landscape in the usual way (without a divisor) creates a landscape of the wrong size (which may be a bug, or maybe I just didn’t read the documentation well). This amendment was received empirically and still did not fail, so I decided to leave it as it is.

You may also have noticed that at the bottom of the listing there are two suspicious helper methods. In fact, this is simply the result of refactoring (as I show listings of a more or less stable version, which has undergone several refactorings, but is still not perfect). These methods are used further in the application of local and global deformations. From their name it is easy to guess what they are doing.

Application of a global height map

Now that the landscape has been created, it’s time to teach him to use the “global height map”. For this, SuperTerrain provides

couple of methods
  /// <summary> /// Sets the global heightmap to match the given one. Given heightmap must match the (SubterrainHeightmapResolution * Detail). /// </summary> /// <param name="heightmap">Heightmap to set the heights from.</param> public void SetGlobalHeightmap(float[,] heightmap) { ForEachSubterrain((x, y, subterrain) => { var chunkStartX = x * GameplayConstants.SuperTerrainHeightmapResolution; var chunkStartY = y * GameplayConstants.SuperTerrainHeightmapResolution; var nextChunkStartX = chunkStartX + GameplayConstants.SuperTerrainHeightmapResolution + 1; var nextChunkStartY = chunkStartY + GameplayConstants.SuperTerrainHeightmapResolution + 1; var sumHm = GetSubHeightMap(heightmap, nextChunkStartX, nextChunkStartY, chunkStartX, chunkStartY)); subterrain.terrainData.SetHeights(0, 0, subHm); }); } /// <summary> /// Retrieves the minor heightmap from the entire heightmap array. /// </summary> /// <returns>The minor height map.</returns> /// <param name="heightMap">Major heightmap.</param> /// <param name="Xborder">Xborder.</param> /// <param name="Yborder">Yborder.</param> /// <param name="x">The x coordinate.</param> /// <param name="y">The y coordinate.</param> private float[,] GetSubHeightMap (float[,] heightMap, int Xborder, int Yborder, int x, int y) { if (Xborder == x || Yborder == y || x < 0 || y < 0) return null; var temp = new float[Yborder - y, Xborder - x]; for (int i = x; i < Xborder; i++) { for(int j = y; j < Yborder; j++) { temp[j - y, i - x] = heightMap[j, i]; } } return temp; } 


I agree, this pair of methods does not look very nice, but I will try to explain everything. So, the name of the SetGlobalHeightmap method speaks for itself. All he does is iterate over all chunks (which are called subterrains here) and apply to them exactly that piece of height map, which corresponds to its coordinates. It is here that the ill-fated SetHeights are used, the performance of which forces us to go to all these perversions. As can be seen from the code, the SuperTerrainHeightmapResolution constant does not take into account the difference by 1 resolution of the height map from the power of two (whose existence is justified in the previous section). And don't let her name confuse you - this constant stores the resolution of the height map for a chunk, not for the whole SuperTerrain. Since the SuperTerrain code is actively using various constants, I will immediately show you the GameplayConstants class. Perhaps it will be clearer what all the same is happening. I removed from this class all not related to SuperTerrain.

GameplayConstants.cs
 namespace Habitat.Game { /// <summary> /// Contains the gameplay constants. /// </summary> public static class GameplayConstants { /// <summary> /// The height of the world. Used in terrain raycasting and Superterrain generation. /// </summary> public const float WorldHeight = 512f; /// <summary> /// Number of the "Terrain" layer /// </summary> public const int TerrainLayer = 8; /// <summary> /// Calculated mask for raycasting against the terrain. /// </summary> public const int TerrainLayerMask = 1 << TerrainLayer; /// <summary> /// Superterrain part side size. /// </summary> public const int SubterrainSize = 256; /// <summary> /// Heightmap resolution for the SuperTerrain. /// </summary> public const int SuperTerrainHeightmapResolution = 512; /// <summary> /// Pixel error for the SuperTerrain. /// </summary> public const int SuperTerrainPixelError = 1; } } 


As for the GetSubHeightMap method, this is just another helper, copying a part of the transferred matrix into the matrix minor. This is necessary because SetHeights cannot apply part of the matrix. This restriction causes a whole bunch of extra memory allocations, but nothing can be done about it. Unfortunately, Unity developers have not provided a scenario for changing the landscape in real time.

The GetSubHeightMap method is used further when applying local deformations, but more on that later.

Application of local deformations

To use deformations, you need not only a height map, but also other information such as coordinates, method of use, dimensions, etc. In this version, all information is encapsulated in the TerrainDeformation class, the listing of which can be seen

here.
 namespace Habitat.DynamicTerrain.Deformation { public abstract class TerrainDeformation { /// <summary> /// Height of the deformation in hightmap pixels. /// </summary> public int H { get; private set; } /// <summary> /// Width of the deformation in hightmap pixels. /// </summary> public int W { get; private set; } /// <summary> /// Heightmap matrix object /// </summary> public float[,] Heightmap { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="Habitat.DynamicTerrain.Deformation.TerrainDeformation"/> class. /// </summary> /// <param name="height">Height in heightmap pixels</param> /// <param name="width">Width in heightmap pixels</param> protected TerrainDeformation(int height, int width) { H = height; W = width; Heightmap = new float[height,width]; } /// <summary> /// Initializes a new instance of the <see cref="Habitat.DynamicTerrain.Deformation.TerrainDeformation"/> class. /// </summary> /// <param name="bitmap">Normalized heightmap matrix.</param> protected TerrainDeformation(float[,] bitmap) { Heightmap = bitmap; H = bitmap.GetUpperBound(0); W = bitmap.GetUpperBound(1); } /// <summary> /// Applies deformation to the point. Additive by default. /// </summary> /// <returns>The to point.</returns> /// <param name="currentValue">Current value.</param> /// <param name="newValue">New value.</param> public virtual float ApplyToPoint(float currentValue, float newValue) { return currentValue + newValue; } /// <summary> /// Generates the heightmap matrix based on constructor parameters. /// </summary> public abstract TerrainDeformation Generate(); } } 


It is easy to guess that the heirs of this class implement the abstract method Generate (), where they describe the logic for creating the appropriate heightmap for deformation. TerrainDeformation also contains information about exactly how it should be applied to the current landscape — this defines the ApplyToPoint virtual method. By default, it defines the deformation as additive, but by overloading the method it is possible to achieve more complex methods of combining two heights. As for the separation of the deformation matrix into sub-matrices and applying them to the corresponding chunks, this code is in the class SuperTerrain and is highlighted in

The following group of methods:
 /// <summary> /// Compound terrain object. /// </summary> public class SuperTerrain { //... ///<summary> ///Resolution of each terrain in the SuperTerrain; ///</summary> private readonly int hmResolution = GameplayConstants.SuperTerrainHeightmapResolution; /// Applies the partial heightmap to a single terrain object. /// </summary> /// <param name="heightmap">Heightmap.</param> /// <param name="chunkX">Terrain x.</param> /// <param name="chunkY">Terrain y.</param> /// <param name="startX">Start x.</param> /// <param name="startY">Start y.</param> /// <param name="type">Deformation type.</param> private void ApplyPartialHeightmap(float[,] heightmap, int chunkX, int chunkY, int startX, int startY, TerrainDeformation td) { if (heightmap == null) return; var current = subterrains [chunkY, chunkX].terrainData.GetHeights( startX, startY, heightmap.GetUpperBound (1) + 1, heightmap.GetUpperBound (0) + 1); for (int x = 0; x <= heightmap.GetUpperBound(1); x++) { for (int y = 0; y <= heightmap.GetUpperBound(0); y++) { current[y, x] = td.ApplyToPoint(current[y, x], heightmap[y, x]); } } subterrains[chunkY, chunkX].terrainData.SetHeights (startX, startY, current); } private int TransformCoordinate (float coordinate) { return Mathf.RoundToInt(coordinate * hmResolution / GameplayConstants.SubterrainSize); } /// <summary> /// Applies the local deformation. /// </summary> /// <param name="deformation">Deformation.</param> /// <param name="x">The x coordinate.</param> /// <param name="y">The y coordinate.</param> public void ApplyDeformation(TerrainDeformation td, float xCoord, float yCoord) { int x = TransformCoordinate (xCoord); int y = TransformCoordinate (yCoord); var chunkX = x / hmResolution; var chunkY = y / hmResolution; ApplyPartialHeightmap(GetBottomLeftSubmap(td, x, y), chunkX - 1, chunkY - 1, hmResolution, hmResolution, td); ApplyPartialHeightmap(GetLeftSubmap(td, x, y), chunkX - 1, chunkY, hmResolution, y % hmResolution, td); ApplyPartialHeightmap(GetTopLeftSubmap(td, x, y), chunkX - 1, chunkY + 1, hmResolution, 0, td); ApplyPartialHeightmap(GetBottomSubmap(td, x, y), chunkX, chunkY - 1, x % hmResolution, hmResolution, td); ApplyPartialHeightmap(GetBottomRightSubmap(td, x, y), chunkX + 1, chunkY - 1, 0, hmResolution, td); ApplyPartialHeightmap(GetMiddleSubmap(td, x, y), chunkX, chunkY, x % hmResolution, y % hmResolution, td); ApplyPartialHeightmap(GetTopSubmap(td, x, y), chunkX, chunkY + 1, x % hmResolution, 0, td); ApplyPartialHeightmap(GetRightSubmap(td, x, y), chunkX + 1, chunkY, 0, y % hmResolution, td); ApplyPartialHeightmap(GetTopRightSubmap(td, x, y), chunkX + 1, chunkY + 1, 0, 0, td); } ///Retrieves the bottom-left part of the deformation (Subheightmap, applied to the bottom ///left chunk of the targetChunk) or null if no such submap has to be applied. ///Covers corner cases private float[,] GetBottomLeftSubmap(TerrainDeformation td, int x, int y) { if (x % hmResolution == 0 && y % hmResolution == 0 && x / hmResolution > 0 && y / hmResolution > 0) { return new float[,] {{ td.Heightmap[0, 0] }}; } return null; } ///Retrieves the left part of the deformation (Subheightmap, applied to the ///left chunk of the targetChunk) or null if no such submap has to be applied. ///Covers edge cases private float[,] GetLeftSubmap(TerrainDeformation td, int x, int y) { if (x % hmResolution == 0 && x / hmResolution > 0) { int endY = Math.Min((y / hmResolution + 1) * hmResolution, y + td.H); return GetSubHeightMap(td.Heightmap, 1, endY - y, 0, 0); } return null; } ///Retrieves the bottom part of the deformation (Subheightmap, applied to the bottom ///chunk of the targetChunk) or null if no such submap has to be applied. ///Covers edge cases private float[,] GetBottomSubmap(TerrainDeformation td, int x, int y) { if (y % hmResolution == 0 && y / hmResolution > 0) { int endX = Math.Min((x / hmResolution + 1) * hmResolution, x + td.W); return GetSubHeightMap(td.Heightmap, endX - x, 1, 0, 0); } return null; } ///Retrieves the top-left part of the deformation (Subheightmap, applied to the top ///left chunk of the targetChunk) or null if no such submap has to be applied. ///Covers split edge cases private float[,] GetTopLeftSubmap(TerrainDeformation td, int x, int y) { if (x % hmResolution == 0 && x / hmResolution > 0) { int startY = (y / hmResolution + 1) * hmResolution; int endY = y + td.H; if (startY > endY) return null; return GetSubHeightMap(td.Heightmap, 1, td.H, 0, startY - y); } return null; } ///Retrieves the bottom-right part of the deformation (Subheightmap, applied to the bottom ///right chunk of the targetChunk) or null if no such submap has to be applied. ///Covers split edge cases private float[,] GetBottomRightSubmap(TerrainDeformation td, int x, int y) { if (y % hmResolution == 0 && y / hmResolution > 0) { int startX = (x / hmResolution + 1) * hmResolution; int endX = x + td.W; if (startX > endX) return null; return GetSubHeightMap(td.Heightmap, td.W, 1, startX - x, 0); } return null; } ///Retrieves the main deformation part. private float[,] GetMiddleSubmap(TerrainDeformation td, int x, int y) { int endX = Math.Min((x / hmResolution + 1) * hmResolution, x + td.W); int endY = Math.Min((y / hmResolution + 1) * hmResolution, y + td.H); return GetSubHeightMap(td.Heightmap, Math.Min(endX - x + 1, td.Heightmap.GetUpperBound(0) + 1), Math.Min(endY - y + 1, td.Heightmap.GetUpperBound(1) + 1), 0, 0); } ///Retrieves the top deformation part or null if none required private float[,] GetTopSubmap(TerrainDeformation td, int x, int y) { int startY = (y / hmResolution + 1) * hmResolution; if (y + td.H < startY) return null; int endX = Math.Min((x / hmResolution + 1) * hmResolution, x + td.W); return GetSubHeightMap(td.Heightmap, Math.Min (endX - x + 1, td.Heightmap.GetUpperBound(0) + 1), td.H, 0, startY - y); } ///Retrieves the left deformation part or null if none required private float[,] GetRightSubmap(TerrainDeformation td, int x, int y) { int startX = (x / hmResolution + 1) * hmResolution; if (x + td.W < startX) return null; int endY = Math.Min((y / hmResolution + 1) * hmResolution, y + td.H); return GetSubHeightMap(td.Heightmap, td.W, Math.Min(endY - y + 1, td.Heightmap.GetUpperBound(1) + 1), startX - x, 0); } ///Retrieves the top-right part of the main deformation. private float[,] GetTopRightSubmap(TerrainDeformation td, int x, int y) { int startX = (x / hmResolution + 1) * hmResolution; int startY = (y / hmResolution + 1) * hmResolution; if (x + td.W < startX || y + td.H < startY) return null; return GetSubHeightMap(td.Heightmap, td.W, td.H, startX - x, startY - y); } } 


As you probably already guessed, the only public method that is in the listing is the most important one. The ApplyDeformation () method allows you to apply the specified deformation to the landscape in the given coordinates. The first thing you do when you call it is to convert the coordinates on the landscape into the coordinates on the height map (remember? If the size of the landscape differs from the resolution of the height map, then this should be taken into account). All work on the application of deformation occurs within nine ApplyPartialHeightmap calls that apply chunks of heights from deformation to the corresponding chunks. As I said earlier, we need exactly nine parts, not four, to take into account all possible boundary and angular cases:



It is this division that GetXXXSubmap () methods do - obtaining the necessary strain minors based on the position of the deformation and the boundaries of various chunks. Each of the methods returns null if the deformation does not affect the corresponding chunk and the method for applying these same minors (ApplyPartialHeightmap ()) does nothing if it receives null as input.

Results and conclusions


The resulting mechanism is certainly far from ideal, but it is already functional and allows you to adjust important landscape parameters in order to achieve some flexibility in terms of performance settings. Among the major potential improvements are the following:


Screenshots
, - :



( , — «» .



chunk' , , :



, chunk.



Video




And, of course, links to playable demos:

For Windows

For Linux

Few instructions
, RTS, . . , — . , "~" development console. «man» «help», , spawn_crater sv_spawn_animdef. / . , benchmark' ( framerate , ) ( google drive).

: + WASD = . = . Ctrl = .

Source: https://habr.com/ru/post/269645/


All Articles