How to edit terrain while playing with TerrainPaintUtility?

I want to make explosion craters. The manual is very short and I still couldn’t figure it out. As opposed to direct editing with TerrainData.SetHeightsDelayLOD, terramorphing with TerrainPaintUtility is very tempting: works with multiple terrains and seems to be optimized.

How can I change the terrain with this API? Can you give an example?

It looks like I was able to take the texture of the editable area and apply it back to the terrain. But how to change data in PaintContext? However, I’m not sure if this works at all.

    int size = 10;
    private Rect terrainSelection;
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            RaycastHit hit;
            if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hit, Mathf.Infinity, 1 << LayerMask.NameToLayer("Terrain")))
            {
                Terrain ter = hit.collider.GetComponent<Terrain>();
                terrainSelection = new Rect(hit.point.x, hit.point.z, 10, 10);
                ModifyTerrain(terrainSelection, ter);
            }
        }
    }
    public Texture brushTexture;
    void ModifyTerrain(Rect selectionArea, Terrain terrain)
    {
        PaintContext paintContext = TerrainPaintUtility.BeginPaintHeightmap(terrain, selectionArea, 10);
        TerrainPaintUtility.EndPaintHeightmap(paintContext, "???");
    }

Haven’t used TerrainPaintUtility… doesn’t it still transact against the regular old Terrain data structure / component?

If so, it’s pretty straightforward stuff to blow craters in the ground. Source: I have made craters at runtime.

You’re welcome to look at my cratering code… full source and project linked in the comments at the bottom of the video:

I got it, in TerrainDamager.cs you are copying the heightmap from terrainData and editing it. I also planned this at first until I found ready tool TerrainPaintUtility. You copy the entire heightmap, and I only took a few points in the radius of influence.

That is, you take:

heightmap = terrainData.GetHeights( 0, 0, terrainData.heightmapResolution, terrainData.heightmapResolution);

Instead, I take the points by the size of the radius:

int brushSize = 10;
float[,] heights = ter.terrainData.GetHeights(hitPointRrelativePosX, hitPointRelativePosZ, brushSize, brushSize);

Don’t know if there is any performance difference. Need to run tests.

As for the question of the topic. The point is that I use several terrains combined into a single landscape. If edit the terrain on the border between two terrains, need to edit both height maps according to their positions. I wanted to avoid this hassle, because a ready-made tool already exists. Just need to understand how it works.
It looks like this is a new API since it’s labeled “experimental” so no one even knows about it.

I found a little more information. I seem to be getting closer to solving the mystery of how this feature works. But it still doesn’t work! Something happens in the profiler, but heights of the terrain doesn’t change. Can anyone please help?

private void Update()
    {
        if (Input.GetKey(KeyCode.Mouse0))
        {
            RaycastHit hit;
            if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hit, Mathf.Infinity, 1 << LayerMask.NameToLayer("Terrain")))
            {
                Vector2 vec = new Vector2(hit.point.x, hit.point.z);
                ModifyTerrain2(vec, hit.collider.GetComponent<Terrain>(), 0, 10f, 10f);
            }
        }
    }

    public Texture2D brush;

    public void ModifyTerrain2(Vector2 v2, Terrain ter, float rotation, float brushsize, float alpha)
    {
            Material mat = TerrainPaintUtility.GetBuiltinPaintMaterial();
            BrushTransform brushXform = TerrainPaintUtility.CalculateBrushTransform(ter, v2, brushsize, rotation);
            PaintContext paintContext = TerrainPaintUtility.BeginPaintHeightmap(ter, brushXform.GetBrushXYBounds(), 0);
            Vector4 brushParams = new Vector4(0.5f, alpha, 0.0f, 0.0f);
            mat.SetTexture("_BrushTex", brush);
            mat.SetVector("_BrushParams", brushParams);
            TerrainPaintUtility.SetupTerrainToolMaterialProperties(paintContext, brushXform, mat);
            Graphics.Blit(paintContext.sourceRenderTexture, paintContext.destinationRenderTexture, mat, (int)TerrainPaintUtility.BuiltinPaintMaterialPasses.PaintTexture);
            TerrainPaintUtility.EndPaintHeightmap(paintContext, "aaaaaa");
    }

@Lesnikus5 did you have any success with this in the end? I’m curious to know whether you got it working or not.

No, I thought it was not rational to spend so much time digging into poorly documented functions. I use SetHeight and SetAlphamaps as before.

Although a bit late to the party here, I’ve managed to get a working solution for runtime height deformation using TerrainPaintUtility and thought I’d post it here in case anyone else in future is struggling with Unity’s lacking documentation around the 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”.

3 Likes

Thanks for sharing! How is the performance compared to terrainData.CopyActiveRenderTextureToHeightmap?

Shaun