Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

CustomRenderTexture with TerrainPaintUtility

Discussion in 'World Building' started by palten08, Aug 4, 2019.

  1. palten08

    palten08

    Joined:
    Jul 31, 2019
    Posts:
    4
    I assume this is possible, but so far I've only been able to make modifications to the terrain in a rectangle, not the shape of the texture I'm assigning to my CustomRenderTexture (Maybe I'm missing something?)

    Here's what I've tried so far:

    1. Created a gray-scale JPG for my test brush
    2. Added this script to my terrain object:
    Code (CSharp):
    1. public class TerrainTest : MonoBehaviour
    2. {
    3.     [SerializeField] ComputeShader shader;
    4.     public RenderTexture terrainRenderTexture;
    5.     public CustomRenderTexture brush;
    6.     public Texture brushTexture;
    7.     public Vector2 rectSize;
    8.  
    9.     private Rect rect;
    10.     private Terrain terrain;
    11.     private PaintContext paintContext;
    12.  
    13.     void Start()
    14.     {
    15.         terrain = GetComponent<Terrain>();
    16.     }
    17.  
    18.     void Update()
    19.     {
    20.         if (Input.GetMouseButton(0))
    21.         {
    22.             RaycastHit rayHit;
    23.             var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    24.  
    25.             if (Physics.Raycast(ray, out rayHit))
    26.             {
    27.                 Vector2 terrainPoint = new Vector2(rayHit.point.x, rayHit.point.z);
    28.                 rect = new Rect(terrainPoint, rectSize);
    29.  
    30.                 ModifyTerrain(rect);
    31.             }
    32.         }
    33.     }
    34.  
    35.     void ModifyTerrain(Rect selectionRect)
    36.     {
    37.         paintContext = TerrainPaintUtility.BeginPaintHeightmap(terrain, selectionRect);
    38.  
    39.         terrainRenderTexture = paintContext.sourceRenderTexture;
    40.  
    41.         brush = new CustomRenderTexture(terrainRenderTexture.width, terrainRenderTexture.height);
    42.         brush.enableRandomWrite = true;
    43.         brush.format = RenderTextureFormat.R16;
    44.         brush.initializationTexture = brushTexture;
    45.         brush.Create();
    46.  
    47.         Graphics.CopyTexture(terrainRenderTexture, brush);
    48.  
    49.         shader.SetTexture(shader.FindKernel("CSMain"), "Result", brush);
    50.         shader.Dispatch(shader.FindKernel("CSMain"), 1, 1, 1);
    51.  
    52.         Graphics.CopyTexture(brush, paintContext.destinationRenderTexture);
    53.  
    54.         TerrainPaintUtility.EndPaintHeightmap(paintContext, "Terrain");
    55.  
    56.         terrain.terrainData.SyncHeightmap();
    57.     }
    58. }
    3. Used this compute shader (I'm honestly not even sure what this compute shader is actually modifying, I just stumbled across someone else's code while trying to look up examples of TerrainPaintUtility in action and this is the only thing I could find):
    Code (CSharp):
    1.  
    2. #pragma kernel CSMain
    3.  
    4. RWTexture2D<float4> Result;
    5.  
    6. [numthreads(8,8,1)]
    7. void CSMain (uint3 id : SV_DispatchThreadID)
    8. {
    9.     Result[id.xy] += 0.0001; //Still can't quite figure out what on the object that gets passed to this is actually being modified.
    10.     Result[id.xy] = clamp(Result[id.xy], 0, 1);
    11. }
    Is my methodology flawed here? I assumed that TerrainPaintUtility would allow you to "paint" onto the heightmap, thus allowing you to use "brushes" with different shapes, instead of doing things the terrainData.SetHeight() way where you need to find points mathematically (and thus might not let you implement selection areas in shapes other than basic circles and squares)
     
  2. palten08

    palten08

    Joined:
    Jul 31, 2019
    Posts:
    4
    Update: Using "CopyActiveRenderTextureToHeightMap", I was able to apply my brush shape to the terrain, however it still isn't without issue:

    Here's the modification shown on the terrain object itself
    Here's the resultant heightmap

    While I at least was able to see a modification that actually follows the shape outlined in the brush's JPG, it appears that the modification is not additive like I was hoping it would be, it's essentially the same thing as dragging the heightmap into paint and pasting the brush JPG over it, rather than modifying the alpha channel with the alpha values from the brush (As shown here). In addition, the brush itself is all jacked up, so I imagine some settings somewhere are not correct, I just can't identify which ones they might be.

    Here's my code:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Experimental.TerrainAPI;
    3.  
    4. public class TerrainTest : MonoBehaviour
    5. {
    6.     public Texture brushTexture;
    7.     public Vector2 rectSize;
    8.  
    9.     private Vector3 terrainPosition;
    10.     private Rect rect;
    11.     private Terrain terrain;
    12.     private PaintContext paintContext;
    13.  
    14.     void Start()
    15.     {
    16.         terrain = GetComponent<Terrain>();
    17.         terrainPosition = terrain.transform.position;
    18.         Texture2D texture2D = new Texture2D(brushTexture.width, brushTexture.height, TextureFormat.RGBA32, false);
    19.     }
    20.  
    21.     void Update()
    22.     {
    23.         if (Input.GetMouseButton(0))
    24.         {
    25.             RaycastHit rayHit;
    26.             var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    27.  
    28.             if (Physics.Raycast(ray, out rayHit))
    29.             {
    30.                 int terrainCoordinateX = Mathf.FloorToInt(((rayHit.point.x - terrainPosition.x) / terrain.terrainData.size.x) * terrain.terrainData.heightmapResolution);
    31.                 int terrainCoordinateY = Mathf.FloorToInt(((rayHit.point.z - terrainPosition.z) / terrain.terrainData.size.z) * terrain.terrainData.heightmapResolution);
    32.                 ModifyTerrain(terrainCoordinateX, terrainCoordinateY);
    33.             }
    34.         }
    35.     }
    36.  
    37.    
    38.     void ModifyTerrain(int xCoordinate, int yCoordinate)
    39.     {
    40.         RenderTexture currentRenderTexture = RenderTexture.active;
    41.         RenderTexture renderTexture = new RenderTexture(brushTexture.width, brushTexture.height, 32);
    42.         Graphics.Blit(brushTexture, renderTexture);
    43.         RenderTexture.active = renderTexture;
    44.         RectInt sourceRect = new RectInt(new Vector2Int(0, 0), new Vector2Int(renderTexture.width, renderTexture.height));
    45.         terrain.terrainData.CopyActiveRenderTextureToHeightmap(sourceRect, new Vector2Int(xCoordinate, yCoordinate), TerrainHeightmapSyncControl.None);
    46.         terrain.terrainData.SyncHeightmap();
    47.     }
    (I realize the modification function itself is the opposite of performant in this state, but this was just to see if it would work before going any further)
     
  3. crysicle

    crysicle

    Joined:
    Oct 24, 2018
    Posts:
    52
    From the picture and code provided it looks like you're directly assigning the brush texture to the heightmap instead of adding it to the current one, which is why the effect is the one you see. I'm not sure if manipulating RenderTextures is only available in compute shaders, but that's the only way i've found how. In any case, it will have to be done inside some kind of shader because we're looking to manipulate textures. In order to achieve the effect of actually painting on the heightmap you need to copy the current heightmap. This can be done via CopyTexture or maybe even by PaintContext. You then need to send both the brush and the copied heightmap texture to the compute shader and dispatch them with logic similar to this:

    1. Code (CSharp):
      1. #pragma kernel CSMain
      2.  
      3. RWTexture2D<half> heightmap; // Halfs can be used for full precision instead of float because heightmaps are int16 anyway.
      4. Texture2D<half4> brush; // Brush texture which you're using to paint on top of the heightmap
      5. half height_multiplier = 0.01; // Brush intensity
      6.  
      7. [numthreads(32,32,1)] // search a bit more about compute shaders to figure out how to use numthreads properly
      8. void CSMain (uint3 id : SV_DispatchThreadID)
      9. {
      10.     half new_heightmap = heightmap[id.xy].x + brush[id.xy].w * height_multiplier; // Use whichever channel you want from the brush. You'll also have to figure out how to sample proper uv coordinates from the brush texture instead of using "id.xy".
      11.     heightmap[id.xy].x = clamp(new_heightmap , 0, 0.5);    // 0.5, because that's the range devs decided to treat the texture internally. Bugs occur beyond 0.5.
      12. }
    After you generate the new heightmap just copy it back via "CopyActiveRenderTextureToHeightmap".