Runtime "TerrainPaintUtility" Examples?

Does anyone have any examples they wouldn’t mind sharing of the TerrainPaintUtility methods in action?

I’m looking to create in-game terrain editing tools this way, but the documentation is pretty unhelpful in providing examples of their implementation (And by that, I mean not at all, because the documentation lacks examples outright).

EDIT:

Here’s some code I tried based on the Unity documentation. This produces no discernible change to the height map:

public class TerrainTest : MonoBehaviour
{
    private Terrain terrain;
    private Rect terrainSelection;

    void Start()
    {
        terrain = GetComponent<Terrain>();
    }

    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            RaycastHit rayHit;
            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            if (Physics.Raycast(ray, out rayHit))
            {
                Vector3 terrainPoint = new Vector3(rayHit.point.x, rayHit.point.y, rayHit.point.z);

                terrainSelection = new Rect(terrainPoint.x, terrainPoint.z, 10, 10);

                ModifyTerrain(terrainSelection);
            }
        }
    }

    void ModifyTerrain(Rect selectionArea)
    {
        PaintContext paintContext = TerrainPaintUtility.BeginPaintHeightmap(terrain, selectionArea, 10);
        TerrainPaintUtility.EndPaintHeightmap(paintContext, "Literally No Clue What This Does");
    }
}
TerrainData td = GetComponent<Terrain>().terrainData;

Float[,] heightmap = td.GetHeights(td.heightmapWidth, td.heightmapHeight);

//Do some stuff

td.SetHeights(0,0,heightmap);

Run some for loops… And assign it back.

Sorry. Im on my phone… Or i would give more precise example. That might get ya going the right direction though.

1 Like

Thanks for the quick example, I had considered doing it like this but wasn’t sure which one was more performant

I posted this on another thread, but figured I’d send it here too to hopefully help others looking to use TerrainPaintUtility. The issue with your initial implementation, is you need to modify the destination RenderTexture Unity provides you in BeginPaintHeightmap, although due to Unity’s lack of documentation on the subject, it took me a while to find out how to do it using their system.

One of the main issues I discovered was that the materials TerrainPaintUtility provides you with don’t seem to do anything, so instead I create materials using the shaders provided by Unity in the Terrain Tools package. These have a number of variables that need to be setup regarding the brush (Unity’s documentation mentions none of this), such as _BrushTex and _BrushParams.

If you have the Terrain Tools package in your project, you can open and view any of the scripts or shaders to see how they function. I would recommend checking out “Packages/Terrain Tools/Shaders/PaintHeight.shader” (which is the shader I use in the below example) which contains a variety of functions using different shader passes. You can also use some of the other shaders in that folder for different functionality, or write your own of course.

Compared to the Get and Set Heights implementation I was also using originally I was getting much better performance.

Anyway, here is my code for raising and lowering terrain. A negative strength can be used to lower terrain instead.

    public void RaiseTerrain(Vector3 worldPosition, Texture2D brushTexture, float strength, float size, float rotation)
    {
        // Get the terrain you want to edit. This also works between neighbouring tiles, so only need the one terrain
        Terrain terrain = this.terrain;

        // Turn brush world position into terrain UV space (0-1)
        Vector3 localPos = worldPosition - terrain.GetPosition();
        float x = localPos.x / terrain.terrainData.size.x;
        float y = localPos.z / terrain.terrainData.size.z;
        Vector2 terrainSpacePos = new Vector2(x, y);

        // Use brush position, size and rotation to generate bounds
        var brushTransform = TerrainPaintUtility.CalculateBrushTransform(terrain, terrainSpacePos, size, rotation);
        Rect bounds = brushTransform.GetBrushXYBounds();

        // Begin painting
        PaintContext paintContext = TerrainPaintUtility.BeginPaintHeightmap(terrain, bounds);

        // Setup material for editing, including brush texture and other params
        // This is the shader Unity uses for raise and lower and contains a number of other functions for other methods
        // Such as flatten and smooth
        Material material = new Material(Shader.Find("Hidden/TerrainEngine/PaintHeightTool"));
        material.SetTexture("_BrushTex", brushTexture);

        // Index 0: brush strength
        // Index 1: target height (when flattening)
        // Index 2 & 3: used for stamping the terrain for "BRUSH_STAMPHEIGHT" and "BLEND_AMOUNT"
        material.SetVector("_BrushParams", new Vector4(strength, 0, 0, 0));

        // Applies other material properties
        TerrainPaintUtility.SetupTerrainToolMaterialProperties(paintContext, brushTransform, material);

        // Apply brush to destination render texture from material
        int passIndex = 0;  // Pass index refers to which shader pass to use for painting (0 is raise/lower)
                            // See PaintHeight.shader for other options
        Graphics.Blit(paintContext.sourceRenderTexture, paintContext.destinationRenderTexture, material, passIndex);
        TerrainPaintUtility.EndPaintHeightmap(paintContext, "");

        // Sync heightmap to terrain data
        terrain.terrainData.SyncHeightmap();
    }

Different shader passes may require some slight changes, or extra parameters. For example, for flattening terrain to a height, I instead use the “Hidden/TerrainTools/SetExactHeight” shader, and need to pass in the target height as the y value in the _BrushParams property (This seems to be a value between 0 and 0.5, for whatever reason).

For smoothing terrain, I used passIndex = 3 to call the correct shader pass and had to pass in an extra variable _SmoothWeights. This is taken directly from Unity’s own implementation in “Packages/Terrain Tools/Editor/TerrainTools/SmoothHeightTool.cs” with direction being a value between -1 and 1. At direction = -1 smoothing will only lower points, while 1 will only raise points, and 0 is either.

Vector4 smoothWeights = new Vector4(
        Mathf.Clamp01(1.0f - Mathf.Abs(direction)),   // centered
        Mathf.Clamp01(-direction),                    // min
        Mathf.Clamp01(direction),                     // max
        0);
material.SetVector("_SmoothWeights", smoothWeights);

There is a separate shader you can use for smoothing which the package uses in it’s implementation, and requires a few extra parameters, but this shader worked for me so I stuck with it.

Last thing of note, brushTexture is expected to be a heightmap, and the shaders read it’s red value, so ensure your brush textures are set up correctly. Here is the texture I am using.
9399464--1315643--TerrainBrush_01.png

I haven’t tried painting anything but height adjustments with TerrainPaintUtility, but hopefully for anyone looking to paint textures or holes, this is enough to get started with. Again, you can check out Unity’s own implementation for their terrain editor in “Packages/Terrain Tools/Editor/TerrainTools”.

2 Likes

@lightfrog94 Thanks for the example.

One thing to note for others is that strength seems to be in 0-1 of the texture since its used in the shader.
You need to convert your meters into texture space. So for instance to raise or lower 1m you need to do 1m / _Terrain.terrainData.heightmapScale.y.
Radius seems to be in meters already.

2 Likes

There’s a lot of different coordinates to keep straight.

It took a lot of trial and error and step-at-a-time work for me to figure it out.

This is kinda the culmination of all the thick technical juice I have on terrain height manipulation:

Matching terrain heights to a collider:

https://forum.unity.com/threads/setting-the-terrain-height-to-match-a-gameobject-by-vertices.1202881/#post-7685134

And of course as referenced elsewhere, my Terrain damager in MakeGeo:

https://discussions.unity.com/t/681462/7

1 Like

Thanks for mentioning that, I forgot to put that in my post.

The terrain system seems to be a mess of inconsistent coordinate system, swapping between world space, local space, and normalised terrain space seemingly at random. It also has some other oddities that make working with it more difficult than it needs to be, such as TerrainData.GetHeights returning a 2d array that is indexed [y,x], reversed from any other coordinates in the engine.

1 Like

I suppose they’re just making sure we’re paying attention… :slight_smile:

But yeah, working with ANY closed engine, when you are up against a new API… just do NOT trust it.

Immediately fabricate super-simple hard-obvious tests EARLY, to avoid confusion later on.

For instance I started out with jamming 0 into every height cell, then a 1 in the center… yep, worked!

Then I put the 1 in what I thought was upper right… yep, works!

Then I put the 1 in what I thought was upper left… WHOA! Nope, LOWER left…

Now 104% of my debugging and engineering efforts were focused freakin’-lasers-on-freakin’-sharks like on finding out what was going on… I did not TOUCH anything else until I proved it worked as I understood it.

Trust no API. Use the debugger.