Introduction

In Sunrise Village you can explore a beautiful world full of lush vegetation. The ground and many objects are covered with grass, flowers, ferns and water plants. We call this kind of ground foliage “MicroVegetation“. MicroVegetation is not just static: When you chop down a tree grass regrows at the new empty spot and when you build a house the vegetation is removed from the building’s footprint area. We created a custom system to render and edit MicroVegetation.

This is a series of three articles describing why and how we did this. The first article was about rendering vegetation. This second part explains how we can modify terrain at runtime. And the third post will deal with painting vegetation in the Unity Editor.

Changing Vegetation at Runtime

In the last post I explained how we can render vegetation with MicroVegetationRenderer. The information about what vegetation should be rendered at any point is stored in a ScriptableObject of type MicroVegetationSplatmap.

If we want to modify vegetation at runtime we need to change the data stored in the splat map. This works by patching the splat map. Each splat map patch is basically a tiny splat map which can override a rectangular area of the original splat map. If the patch is removed the information of the original splat map is restored. The image below shows the splat map patch of the player’s home. The painted area of the splat map is the footprint area where we want the vegetation to disappear when the building has been placed. The red areas are masked out – here the original vegetation will stay. In this case the splat map patch would just remove vegetation but of course it could also contain vegetation values that would replace the original.

Splatmap patch of the player’s house

Architecture

This diagram shows the different components of the microvegetation system and how they are connected. MicroVegetationRenderer and MicroVegetationView are MonoBehaviours on a GameObject in a scene or prefab.

MicroVegetationView

The MicroVegetationView class handles the modification of a splat map. It’s a component that can be added on top of MicroVegetationRenderer. The renderer will work without MicroVegetationView for cases when vegetation does not need to be updated at runtime. At startup MicroVegetationView creates a copy of the original splat map so the original state can be restored if a patch is removed. The Inspector of MicroVegetationView shows a preview of the whole splat map and highlights areas that have been modified. Hovering the mouse over a point on the splat map shows some debug information about the point, like which vegetation is growing here.

Microvegetation View in the Inspector

SplatMapPatches

SplatMapPatches are tiny splat maps that contain data to override a rectangular area of the original splat map. Internally it’s an array of bytes that holds the splat map values (the index of the vegetation type in MicroVegetationSet). Additionally the patch contains another array of opacity values to define a mask – so a patch can define any shape, not just a rectangular area.

Each SplatMapPatch is a ScriptableObject. Artists can author SplatMapPatches with a component called SplatMapPatchCreator that uses MicroVegetationRenderer to display and edit the SplatMapPatch – More about the editor in the third part of this article. Each of the objects in our game world can have a SplatMapPatch which is applied to the splat map when the object appears and is removed when the object disappears.

We also store terrain data in the patches so we can override the terrain values with the vegetation.

Splatmap Patch Creator

MicroVegetationService

MicroVegetationService allows global access to the vegetation in the current scene. It’s a ScriptableObject that exists just once and can be injected into any other class. When a new scene is loaded it registers its MicrovegetationViews to the service. The service provides methods to apply and remove SplatMapPatches and to control the visibility of the vegetation. This is the interface of the service:

/// <summary>
/// This service controls the micro vegetation in the game world
/// </summary>
public interface IMicroVegetationService : IBaseService
{
    void SetMicroVegetationVisible(bool visible);
    void ApplySplatMapPatch(SplatMapPatch splatMap, Vector3 position, int rotationStep, bool remove);
}

Applying a patch in detail

This is how MicroVegetationView applies a patch to the splat map. The tricky part is to take the offset and rotation (by 90 degrees) into account. After updating the splat map the renderer is requested to update the MicroVegetationPatches (not to be confused with SplatMapPatch) that render the modified area.

public void ApplySplatMapPatch(SplatMapPatch patch, Vector3 center, int rotationStep, bool remove)
{
    var rendererRect = _renderer.BoundsRect;
 
    bool isRotated = rotationStep % 2 == 1;
 
    int splatMapWidth = _patchedSplatmap.Width;
    int splatMapHeight = _patchedSplatmap.Height;
 
    float worldToSplatX = splatMapWidth / rendererRect.width;
    float worldToSplatY = splatMapHeight / rendererRect.height;
 
    int patchResX = isRotated ? patch.ResolutionY : patch.ResolutionX;
    int patchResY = isRotated ? patch.ResolutionX : patch.ResolutionY;
 
    // patch Size in world units:
    float patchWidth = isRotated ? patch.Length : patch.Width;
    float patchHeight = isRotated ? patch.Width : patch.Length;
    Rect patchRect = new Rect(center.x - patchWidth * 0.5f, center.z - patchHeight * 0.5f, patchWidth, patchHeight);
 
    float worldToPatchX = patchResX / patchWidth;
    float worldToPatchY = patchResY / patchHeight;
    float patchToSplatX = worldToSplatX / worldToPatchX;
    float patchToSplatY = worldToSplatY / worldToPatchY;
 
    // update area in splatmap coordinates:
    int minX = Math.Max(Mathf.FloorToInt(patchRect.min.x * worldToSplatX), 0);
    int maxX = Math.Min(Mathf.FloorToInt(patchRect.max.x * worldToSplatX), splatMapWidth - 1);
    int minY = Math.Max(Mathf.FloorToInt(patchRect.min.y * worldToSplatY), 0);
    int maxY = Math.Min(Mathf.FloorToInt(patchRect.max.y * worldToSplatY), splatMapHeight - 1);
 
    // loop over the pixels of the splatmap
    for (int splatY = minY; splatY <= maxY; splatY++)
    {
        for (int splatX = minX; splatX <= maxX; splatX++)
        {
            // calculate coordinate on the splatmap that we want to sample:
            int patchX = Mathf.FloorToInt((splatX - minX) * patchToSplatX);
            int patchY = Mathf.FloorToInt((splatY - minY) * patchToSplatY);
            switch (rotationStep)
            {
                case 1:
                    patchX = patchY;
                    patchY = patchResX - 1 - patchX;
                    break;
                case 2:
                    patchX = patchResX - 1 - patchX;
                    patchY = patchResY - 1 - patchY;
                    break;
                case 3:
                    patchX = patchResY - 1 - patchY;
                    patchY = patchX;
                    break;
            }
 
            if (patchX < 0 || patchX >= patchResX ||
                patchY < 0 || patchY >= patchResY)
            {
                continue;
            }
 
            if (patch.IsEmptySpot(patchX, patchY))
            {
                continue;
            }
 
            var value = remove ? _originalSplatMap.Get(splatX, splatY) : patch.GetVegetation(patchX, patchY);
            _patchedSplatmap.Set(splatX, splatY, value);
        }
    }
    _renderer.RequestUpdateOfPatchesInArea(patchRect);
}

Wrap up

Modifying vegetation is completely decoupled from rendering in MicroVegetationRenderer. This allows us to use the renderer independently e.g. to render static vegetation or inside the editor. Each MicroVegetationRenderer that we want to handle vegetation changes at runtime gets an additional component MicroVegetationView which can patch the splat map assigned to the renderer. All active views are registered to to a service which can be accessed by the game logic.

In the final part of this series I will explain how we handle painting MicroVegetation in the editor.

InnoGames is hiring! Check out open positions and join our awesome international team in Hamburg at the certified Great Place to Work®.

By Andreas Hackel

Technical Artist @ InnoGames