Sample Code

Dynamic Terrain with Argument Buffers

Demonstrates how to use argument buffers to render a dynamic terrain in real time with a GPU-driven pipeline.

Download

Overview

This sample demonstrates dynamic terrain generation on an outdoor landscape, using argument buffers to select terrain materials, vegetation geometry, and particle effects within a GPU-driven pipeline. The sample creates a landscape with visually distinct areas, called habitats, that differ based on the land’s elevation. These are the habitats in the sample, ordered from highest to lowest elevation:

  • Snow

  • Rock

  • Grass

  • Sand

Screenshot of the sample app running to show the different habitats visually.

Respond to Landscape Alterations

The initial topology of the landscape is determined by a static height map, TerrainHeightMap.png.

_targetHeightmap = CreateTextureWithDevice (device, @"Textures/TerrainHeightMap.png", false, false);

At run time, as you alter the landscape with the provided controls, the sample evaluates the latest topology to determine whether a new habitat should be applied to a new land elevation. If so, the sample updates the argument buffer corresponding to the land with the correct materials and vegetation geometry for the new habitat. The sample renders this new habitat by passing the land elevation value to the EvaluateTerrainAtLocation function.

static void EvaluateTerrainAtLocation(float2 uv,
                                      float3 worldPosition,
                                      texture2d<float> heightMap,
                                      texture2d<float> normalMap,
                                      texture2d<float> propertiesMap,
                                      constant const TerrainParams& terrainParams,
                                      thread float outHabitat[TerrainHabitatTypeCOUNT],
                                      thread float3 &outNormal)

Define an Argument Buffer for Terrain Habitats

The sample defines a custom argument buffer structure, TerrainHabitat, that defines the elements of a terrain habitat.

struct TerrainHabitat
{
#ifndef METAL
    // This struct should not be instantiated in C++ because it contains textures that aren't visible on the CPU
private:
    TerrainHabitat ();
public:
#endif

    float slopeStrength      IAB_INDEX(TerrainHabitat_MemberIds::slopeStrength);
    float slopeThreshold     IAB_INDEX(TerrainHabitat_MemberIds::slopeThreshold);
    float elevationStrength  IAB_INDEX(TerrainHabitat_MemberIds::elevationStrength);
    float elevationThreshold IAB_INDEX(TerrainHabitat_MemberIds::elevationThreshold);
    float specularPower      IAB_INDEX(TerrainHabitat_MemberIds::specularPower);
    float textureScale       IAB_INDEX(TerrainHabitat_MemberIds::textureScale);
    bool  flipNormal         IAB_INDEX(TerrainHabitat_MemberIds::flipNormal);
    
    struct ParticleProperties
    {
        // The fields of this struct must be reflected in TerrainHabitat_MemberIds
        simd::float4    keyTimePoints;
        simd::float4    scaleFactors;
        simd::float4    alphaFactors;
        simd::float4    gravity;
        simd::float4    lightingCoefficients;
        int             doesCollide;
        int             doesRotate;
        int             castShadows;
        int             distanceDependent;
    } particleProperties;
    
#ifdef METAL
    texture2d_array <float,access::sample> diffSpecTextureArray IAB_INDEX(TerrainHabitat_MemberIds::diffSpecTextureArray);
    texture2d_array <float,access::sample> normalTextureArray   IAB_INDEX(TerrainHabitat_MemberIds::normalTextureArray);
#endif
};

Among these elements, elevationStrength and elevationThreshold determine the elevation range in which the habitat is active. Additionally, diffSpecTextureArray and normalTextureArray determine the textures used to render the habitat.

TerrainHabitat is nested within another argument buffer, TerrainParams, that provides a number of slight visual variations for added realism.

struct TerrainParams
{
    TerrainHabitat habitats [TerrainHabitatTypeCOUNT];
    float ambientOcclusionScale    IAB_INDEX(TerrainParams_MemberIds::ambientOcclusionScale);
    float ambientOcclusionContrast IAB_INDEX(TerrainParams_MemberIds::ambientOcclusionContrast);
    float ambientLightScale        IAB_INDEX(TerrainParams_MemberIds::ambientLightScale);
    float atmosphereScale          IAB_INDEX(TerrainParams_MemberIds::atmosphereScale);
};

TerrainHabitat is the specific argument buffer definition for a terrain habitat. However, because this definition is nested within TerrainParams, the TerrainParams objects are the ones passed along the GPU pipeline.

Render Terrain

The sample provides the GPU with the textures corresponding to various habitats. First, the sample calls the useResource:usage: method to indicate which textures are used by the GPU.

for (int i = 0; i < _terrainTextures.size(); i++)
{
    [renderEncoder useResource: _terrainTextures[i].diffSpecTextureArray
                         usage: MTLResourceUsageSample | MTLResourceUsageRead];
    [renderEncoder useResource: _terrainTextures[i].normalTextureArray
                         usage: MTLResourceUsageSample | MTLResourceUsageRead];
}

Then, the sample calls the setFragmentBuffer:offset:atIndex: method to set the argument buffer, terrainParamsBuffer, that contains those textures.

[renderEncoder setFragmentBuffer:_terrainParamsBuffer offset:0 atIndex:_iabBufferIndex_PplTerrainMainView];

The sample accesses the argument buffer in the fragment function, terrain_fragment, to output the correct material for the terrain. First, the sample passes the mat parameter into the fragment function.

fragment GBufferFragOut terrain_fragment (
                                          const TerrainVertexOut in [[stage_in]],
                                          constant TerrainParams & mat [[buffer(1)]],
                                          constant AAPLUniforms& globalUniforms [[buffer(2)]],
                                          texture2d<float> heightMap [[texture(0)]],
                                          texture2d<float> normalMap [[texture(1)]],
                                          texture2d<float> propertiesMap [[texture(2)]]
                                          )

Then, the sample passes the current land elevation into the EvaluateTerrainAtLocation function, where the fragment samples the texture corresponding to that elevation.

BrdfProperties curSubLayerBrdf = sample_brdf (
                                              mat.habitats [curLayerIdx].diffSpecTextureArray,
                                              mat.habitats [curLayerIdx].normalTextureArray,
                                              curSubLayerIdx,
                                              mat.habitats [curLayerIdx].textureScale,
                                              mat.habitats [curLayerIdx].specularPower,
                                              mat.habitats [curLayerIdx].flipNormal,
                                              in.worldPosition,
                                              normal,
                                              tangent,
                                              bitangent );

Render Vegetation

The sample passes the terrainParamsBuffer argument buffer to the vegetation render pass via an instance of AAPLTerrainRenderer. This data determines which type of vegetation to render at a given location. First, the sample calls the setBuffer:offset:atIndex: method to set the argument buffer for the vegetation render pass.

[computeEncoder setBuffer:terrain.terrainParamsBuffer offset:0 atIndex:3];

Then, the sample passes the argument buffer into the EvaluateTerrainAtLocation function, which outputs a habitatPercentages value:

EvaluateTerrainAtLocation(uv_pos, world_pos, heightMap,
                          normalMap, propertiesMap, terrainParams,
                          habitatPercentages,
                          worldNormal);

The habitat percentages are processed to select a specific index into the vegetation geometries, determined by the value of pop_idx:

pop_idx = rules[rule_index].populationStartIndex + uint((s / rules[rule_index].densityInHabitat * float(rules[rule_index].populationIndexCount)));

Finally, the sample uses this population index to render an instance of a particular vegetation geometry onto the landscape:

vegetationSpawnInstance(pop_idx, world_matrix, float4(world_pos, radius), globalUniforms, instances, indirect);

Render Particles

The sample passes the terrainParamsBuffer argument buffer to the particle render pass via an instance of AAPLTerrainRenderer. This data determines which type of particles to render at a given location. First, the sample calls the setBuffer:offset:atIndex: method to set the argument buffer for the particles render pass.

[enc setBuffer:[terrain terrainParamsBuffer] offset:0 atIndex:14];

Then, the sample checks the relative percentages of habitat coverage in the altered landscape with the EvaluateTerrainAtLocation function, where the sample passes the 3D position of the particle.

EvaluateTerrainAtLocation (mouseUvPos, mouseWorldPos, heightMap,
                           normalMap, propsMap, terrainParams,
                           habitatPercentages,
                           worldNormal);

The sample chooses the appropriate habitat by selecting the terrain with the highest percentage of habitat coverage.

float highestLevel = 0.f;
for (uint i = 0; i < TerrainHabitatTypeCOUNT; i++)
{
    if (habitatPercentages [i] > highestLevel)
    {
        highestLevel = habitatPercentages [i];
        habitatIndex = i;
    }
}

Finally, the particle’s corresponding habitat material is retrieved from the argument buffer and set onto the new particle.

ParticleData data;
data.habitatIndex = habitatIndex;
data.texture = terrainParams.habitats [habitatIndex].diffSpecTextureArray;

See Also

Cookbook

Deferred Lighting

Demonstrates how to implement a deferred lighting renderer that takes advantage of unique Metal features.

Reflections with Layer Selection

Demonstrates how to use layer selection to reduce the number of render passes needed for a reflective object.

LOD with Function Specialization

Demonstrates how to use specialized functions to select a level of detail based on dynamic conditions.