Search Unity

Runtime terrain deformation

Discussion in 'World Building' started by jister, Jan 14, 2020.

  1. jister

    jister

    Joined:
    Oct 9, 2009
    Posts:
    1,700
    hey,
    Trying my hand at runtime terrain deformation.
    my setup so far (thx to @palten08 and @crysicle )
    relevant terrain cs code:
    Code (CSharp):
    1. void GetAreaToModify()
    2.     {
    3.         RaycastHit hit;
    4.         var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    5.  
    6.         if (Physics.Raycast(ray, out hit, Mathf.Infinity, 1 << LayerMask.NameToLayer("Terrain")))
    7.         {
    8.             if (!terrain)
    9.                 terrain = hit.transform.GetComponent<Terrain>();
    10.             terrainPoint = new Vector2((int)((hit.point.x - (rectSize.x * .5f)) - terrain.transform.position.x), (int)((hit.point.z - (rectSize.y * .5f)) - terrain.transform.position.z));
    11.  
    12.             if (terrainPoint != previousTerrainPoint && Vector2.Distance(terrainPoint, previousTerrainPoint) > 1f)
    13.             {
    14.                 Rect prevRect = new Rect(previousTerrainPoint, rectSize);
    15.                 if (prevRect.height != 0 && prevRect.width != 0 && prevRenderTexture)
    16.                     RestoreTerrain(prevRect);
    17.                 Rect rect = new Rect(terrainPoint, rectSize);
    18.                 ModifyTerrain(rect);
    19.                 previousTerrainPoint = terrainPoint;
    20.                 brush.position = new Vector3((int)hit.point.x, hit.point.y + 0.1f, (int)hit.point.z);
    21.             }
    22.         }
    23.     }
    24.  
    25.     void ModifyTerrain(Rect selection)
    26.     {
    27.         //terrain = Terrain.activeTerrain;
    28.         if (trees.Count > 0)
    29.         {
    30.             trees.AddRange(terrain.terrainData.treeInstances);
    31.             terrain.terrainData.treeInstances = trees.ToArray();
    32.             trees.Clear();
    33.         }
    34.         //Trees inside circle
    35.         foreach (var tree in terrain.terrainData.treeInstances)
    36.         {
    37.             Vector2 p = new Vector2((tree.position.x * terrain.terrainData.size.x) + terrain.transform.position.x, (tree.position.z * terrain.terrainData.size.z) + terrain.transform.position.z);
    38.             float d = Mathf.Sqrt(Mathf.Pow(p.x - transform.position.x, 2) + Mathf.Pow(p.y - transform.position.z, 2));
    39.             if (d < rectSize.x * .5f)
    40.                 trees.Add(tree);
    41.         }
    42.         if (trees.Count > 0)
    43.             terrain.terrainData.treeInstances = terrain.terrainData.treeInstances.Except(trees).ToArray();
    44.  
    45.         PaintContext paintContext = TerrainPaintUtility.BeginPaintHeightmap(terrain, selection);
    46.         Debugger.Log("Rect: " + selection + " contextRect: " + paintContext.pixelRect + "/" + paintContext.pixelSize);
    47.         RenderTexture terrainRenderTexture = new RenderTexture(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 0, RenderTextureFormat.R16);
    48.         terrainRenderTexture.enableRandomWrite = true;
    49.         Graphics.CopyTexture(paintContext.sourceRenderTexture, terrainRenderTexture);
    50.  
    51.         prevRenderTexture = new RenderTexture(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 0, RenderTextureFormat.R16);
    52.         Graphics.CopyTexture(paintContext.sourceRenderTexture, prevRenderTexture);
    53.  
    54.         float h0 = terrain.SampleHeight(new Vector3(selection.position.x + terrain.transform.position.x, 0, selection.position.y + terrain.transform.position.z));
    55.         float h1 = terrain.SampleHeight(new Vector3(selection.position.x + terrain.transform.position.x, 0, selection.position.y + terrain.transform.position.z + rectSize.y));
    56.         float h2 = terrain.SampleHeight(new Vector3(selection.position.x + terrain.transform.position.x + rectSize.x, 0, selection.position.y + terrain.transform.position.z + rectSize.y));
    57.         float h3 = terrain.SampleHeight(new Vector3(selection.position.x + terrain.transform.position.x + rectSize.x, 0, selection.position.y + terrain.transform.position.z));
    58.  
    59.         height = (((h0 + h1 + h2 + h3) / 4f)/* + terrain.transform.position.y*/) / terrain.terrainData.size.y;
    60.  
    61.         terrainPaintShader.SetFloat("height", height*smoothOffset);
    62.         terrainPaintShader.SetTexture(terrainPaintShader.FindKernel("CSMain"), "heightmap", terrainRenderTexture);
    63.         terrainPaintShader.SetTexture(terrainPaintShader.FindKernel("CSMain"), "brush", brushTexture);
    64.  
    65.         terrainPaintShader.Dispatch(terrainPaintShader.FindKernel("CSMain"), (int)Mathf.Ceil(selection.width / 32), (int)Mathf.Ceil(selection.height / 32), 1);
    66.  
    67.         Graphics.CopyTexture(terrainRenderTexture, paintContext.destinationRenderTexture);
    68.  
    69.         TerrainPaintUtility.EndPaintHeightmap(paintContext, "Terrain");
    70.  
    71.         terrain.terrainData.SyncHeightmap();
    72.  
    73.     }
    74.  
    75.     void RestoreTerrain(Rect selection)
    76.     {
    77.         if (baked)
    78.         {
    79.             baked = false;
    80.             return;
    81.         }
    82.         PaintContext prevPaintContext = TerrainPaintUtility.BeginPaintHeightmap(terrain, selection);
    83.  
    84.         Graphics.CopyTexture(prevRenderTexture, prevPaintContext.destinationRenderTexture);
    85.  
    86.         TerrainPaintUtility.EndPaintHeightmap(prevPaintContext, "Terrain");
    87.  
    88.         terrain.terrainData.SyncHeightmap();
    89.     }
    Compute shader code:
    Code (CSharp):
    1. #pragma kernel CSMain
    2.  
    3. half height;
    4. //half2 offset;
    5. RWTexture2D<half4> heightmap;
    6. Texture2D<half4> brush;
    7.  
    8. [numthreads(32, 32, 1)]
    9. void CSMain(uint2 id : SV_DispatchThreadID)
    10. {
    11.     for (int i = 0; i < 2; i++)
    12.     {
    13.         for (int j = 0; j < 2; j++)
    14.         {
    15.             half4 new_heightmap = lerp(heightmap[int2(id.x + (32 * i), id.y + (32 * j))], height, brush[int2(id.x + (32 * i), id.y + (32 * j))]);
    16.             heightmap[int2(id.x + (32 * i), id.y + (32 * j))] = clamp(new_heightmap, 0, .5);    // 0.5, because that's the range devs decided to treat the texture internally. Bugs occur beyond 0.5.
    17.         }
    18.     }
    19. }
    20.  
    my brush 64x64:
    upload_2020-1-14_12-50-23.png
    my result:
    upload_2020-1-14_12-49-15.png
    as you can see i get distortions...

    things i tried and found out:

    the brush size, rectSize, computeshader numthreads and terrain height map resolution are all connected.

    as i understand atm:
    - I pass a rect to the function TerrainPaintUtility.BeginPaintHeightmap with takes a "snapshot" from the heightmap with the position and size of the passed rect. Heightmap resolution comes into play here. Debugging the PaintContext rect and pixelSize gives sometimes strange values depending on the HeightMap resolution. (ex. i pass a 64x64 rect, but the paintcontext rect prints 18x19 for height/width and 4/4 pixelsize? guessing the pixelsize means resolution of the heightmap somehow)
    - numthreads 32x32 can handle my 64x64 rect that gets passed as a texture, by offsetting the thread ids in the for loops, (please correct me if I'm wrong). So my assumption was that it does the same for the brush texture. Confusion came when it got unexpected results and started to iterate changing Heightmap resolution, NumThreads, RectSize, ... And found that for example it only works when my Heightmap resolution is set to 2049, my rectsize 64x64, brush texture size 64 and numthreads 32x32 with a double for loop of 2 (as posted in the above). changing Heightmap resolution too 1025 results in it only applying 1/4 of the brush texture in the lerp. as you can see by the sharp corner. (it should lerp from sampled height to calculated height based of the brush texture "alpha")
    upload_2020-1-14_14-30-45.png
    to conclude: I'm confused by the relation between Heightmap resolution, rect / brush size and numthreads. If someone could shed some light on how they affect each other, would be great :)
     
  2. crysicle

    crysicle

    Joined:
    Oct 24, 2018
    Posts:
    80
    I haven't worked with the PaintContext for quite a while, but about a year ago I remember the way it calculated its size by the passed parameters to be shoddy. In certain cases the resolution of the selected area was constantly fluctuating by 1 width/height and really misbehaving when manipulating areas greater than a 2x2 terrains. I moved away from the class and decided to handle the heightmap gathering/scattering by myself so I can't really say much there.

    As for the manipulation of the heightmap:

    If your terrainRenderTexture is 64x64, brush is 64x64, selection width and height are 64x64, and numthreads are [32, 32, 1], then all that should be neccesary is passing the dispatch as:

    Code (CSharp):
    1. terrainPaintShader.Dispatch(terrainPaintShader.FindKernel("CSMain"), Mathf.CeilToInt(selection.width / 32f), Mathf.CeilToInt(selection.height / 32f), 1);
    2.  
    and the shader code as:

    Code (CSharp):
    1. #pragma kernel CSMain
    2.  
    3. half height;
    4. RWTexture2D<half> heightmap;
    5. Texture2D<half4> brush;
    6.  
    7. [numthreads(32, 32, 1)]
    8. void CSMain(uint2 id : SV_DispatchThreadID)
    9. {
    10.     half new_heightmap = lerp(heightmap[id.xy], height, brush[id.xy].w);
    11.     heightmap[id.xy] = clamp(new_heightmap, 0, .5);
    12. }
    currently your 2 for loops are doing what the dispatch should do for you by turning the thread parameters into:

    Code (CSharp):
    1. [code=CSharp]terrainPaintShader.Dispatch(terrainPaintShader.FindKernel("CSMain"), 2, 2, 1);
    2.  
    [/code]

    pretty much asking the shader to process a block of [32,32,1] numthreads as 2x2x1 block which will make the shader run a 64x64x1 block with each thread in parallel.

    For later updates when you decide to manipulate areas which do not match the brush resolution, you'll need to use bilinear or nearest neighbour filtering to sample the data from the brush. This can be done either by making the algorithms yourself inside the shader or using Texture Samplers which you can read up on below.

    https://docs.unity3d.com/Manual/SL-SamplerStates.html
     
    jister likes this.
  3. jister

    jister

    Joined:
    Oct 9, 2009
    Posts:
    1,700
    @crysicle thanks again for the help, that clears up at least one part.
     
  4. jister

    jister

    Joined:
    Oct 9, 2009
    Posts:
    1,700
    Just made the changes and it works perfect now, the distortions are gone.
    So only the relation Heightmap resolution and Rect/Brush size left.
    For it to work correct I have to follow the rule: Heightmap Resolution-1 == RectSizexRectSize
    so the bigger the brush the higher i have to set the Heightmap res, which isn't very optimal. (Since SyncHeightmap is the most expensive part of the system, keeping the resolution down really helps performance)
    I need different sizes of brushes, so i need to break this rule somehow and still keep it working.
    Strange think about it is i don't get how the "SnapShot" (PaintContext) of the HieghtMap with my 64x64 brush has the same amount of pixels as the whole HeightMap?
     
unityunity