// Update terrain data
        private void UpdateTerrainHeights(TerrainDesc terrainDesc)
        {
            //System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
            //long startTime = stopwatch.ElapsedMilliseconds;

            // Instantiate Daggerfall terrain
            DaggerfallTerrain dfTerrain = terrainDesc.terrainObject.GetComponent <DaggerfallTerrain>();

            if (dfTerrain)
            {
                dfTerrain.TerrainScale = TerrainScale;
                dfTerrain.MapPixelX    = terrainDesc.mapPixelX;
                dfTerrain.MapPixelY    = terrainDesc.mapPixelY;
                dfTerrain.InstantiateTerrain();
            }

            // Update data for terrain
            dfTerrain.UpdateMapPixelData(terrainTexturing);         // This is most expensive single operation, ~20ms on dev pc
            dfTerrain.UpdateTileMapData();
            dfTerrain.UpdateHeightData();

            // Promote data to live terrain
            dfTerrain.UpdateClimateMaterial();
            dfTerrain.PromoteTerrainData();

            // Only set active again once complete
            terrainDesc.terrainObject.SetActive(true);

            //long totalTime = stopwatch.ElapsedMilliseconds - startTime;
            //DaggerfallUnity.LogMessage(string.Format("Time to update terrain heights: {0}ms", totalTime), true);
        }
        // Update terrain nature
        private void UpdateTerrainNature(TerrainDesc terrainDesc)
        {
            //System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
            //long startTime = stopwatch.ElapsedMilliseconds;

            // Setup billboards
            DaggerfallTerrain        dfTerrain        = terrainDesc.terrainObject.GetComponent <DaggerfallTerrain>();
            DaggerfallBillboardBatch dfBillboardBatch = terrainDesc.billboardBatchObject.GetComponent <DaggerfallBillboardBatch>();

            if (dfTerrain && dfBillboardBatch)
            {
                // Get current climate and nature archive
                DFLocation.ClimateSettings climate = MapsFile.GetWorldClimateSettings(dfTerrain.MapData.worldClimate);
                int natureArchive = climate.NatureArchive;
                if (dfUnity.WorldTime.SeasonValue == WorldTime.Seasons.Winter)
                {
                    // Offset to snow textures
                    natureArchive++;
                }

                dfBillboardBatch.SetMaterial(natureArchive);
                TerrainHelper.LayoutNatureBillboards(dfTerrain, dfBillboardBatch, TerrainScale);
            }

            // Only set active again once complete
            terrainDesc.billboardBatchObject.SetActive(true);

            //long totalTime = stopwatch.ElapsedMilliseconds - startTime;
            //DaggerfallUnity.LogMessage(string.Format("Time to update terrain nature: {0}ms", totalTime), true);
        }
        // Create new location game object
        private GameObject CreateLocationGameObject(int terrain, out DFLocation locationOut)
        {
            locationOut = new DFLocation();

            // Terrain must have a location
            DaggerfallTerrain dfTerrain = terrainArray[terrain].terrainObject.GetComponent <DaggerfallTerrain>();

            if (!dfTerrain.MapData.hasLocation)
            {
                return(null);
            }

            // Get location data
            locationOut = dfUnity.ContentReader.MapFileReader.GetLocation(dfTerrain.MapData.mapRegionIndex, dfTerrain.MapData.mapLocationIndex);
            if (!locationOut.Loaded)
            {
                return(null);
            }

            // Spawn parent game object for new location
            float      height         = dfTerrain.MapData.averageHeight * TerrainScale;
            GameObject locationObject = new GameObject(string.Format("DaggerfallLocation [Region={0}, Name={1}]", locationOut.RegionName, locationOut.Name));

            locationObject.transform.parent   = this.transform;
            locationObject.hideFlags          = HideFlags.HideAndDontSave;
            locationObject.transform.position = terrainArray[terrain].terrainObject.transform.position + new Vector3(0, height, 0);
            DaggerfallLocation dfLocation = locationObject.AddComponent <DaggerfallLocation>() as DaggerfallLocation;

            dfLocation.SetLocation(locationOut, false);

            return(locationObject);
        }
Beispiel #4
0
        public override void PromoteMaterial(DaggerfallTerrain daggerfallTerrain, TerrainMaterialData terrainMaterialData)
        {
            // Get tileset material to "steal" atlas texture for our shader
            Material tileSetMaterial = DaggerfallUnity.Instance.MaterialReader.GetTerrainTilesetMaterial(GetGroundArchive(terrainMaterialData.WorldClimate));

            // Assign textures
            terrainMaterialData.Material.SetTexture(TileUniforms.TileAtlasTex, tileSetMaterial.GetTexture(TileUniforms.TileAtlasTex));
            terrainMaterialData.Material.SetTexture(TileUniforms.TilemapTex, terrainMaterialData.TileMapTexture);
            terrainMaterialData.Material.SetInt(TileUniforms.TilemapDim, MapsFile.WorldMapTileDim);
        }
Beispiel #5
0
        public override void PromoteMaterial(DaggerfallTerrain daggerfallTerrain, TerrainMaterialData terrainMaterialData)
        {
            Material tileMaterial = DaggerfallUnity.Instance.MaterialReader.GetTerrainTextureArrayMaterial(GetGroundArchive(terrainMaterialData.WorldClimate));

            // Assign textures (propagate material settings from tileMaterial to terrainMaterial)
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TileTexArr, tileMaterial.GetTexture(TileTexArrUniforms.TileTexArr));
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TileNormalMapTexArr, tileMaterial.GetTexture(TileTexArrUniforms.TileNormalMapTexArr));
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TileParallaxMapTexArr, tileMaterial.GetTexture(TileTexArrUniforms.TileParallaxMapTexArr));
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TileMetallicGlossMapTexArr, tileMaterial.GetTexture(TileTexArrUniforms.TileMetallicGlossMapTexArr));
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TilemapTex, terrainMaterialData.TileMapTexture);

            // Assign keywords (propagate material keywords from tileMaterial to terrainMaterial)
            AssignKeyWord(KeyWords.NormalMap, tileMaterial, terrainMaterialData.Material);
            AssignKeyWord(KeyWords.HeightMap, tileMaterial, terrainMaterialData.Material);
            AssignKeyWord(KeyWords.MetallicGlossMap, tileMaterial, terrainMaterialData.Material);
        }
Beispiel #6
0
        // Update terrain nature
        private void UpdateTerrainNature(TerrainDesc terrainDesc)
        {
            // Setup billboards
            DaggerfallTerrain        dfTerrain        = terrainDesc.terrainObject.GetComponent <DaggerfallTerrain>();
            DaggerfallBillboardBatch dfBillboardBatch = terrainDesc.billboardBatchObject.GetComponent <DaggerfallBillboardBatch>();

            if (dfTerrain && dfBillboardBatch)
            {
                // Get current climate and nature archive
                int natureArchive = ClimateSwaps.GetNatureArchive(LocalPlayerGPS.ClimateSettings.NatureSet, dfUnity.WorldTime.Now.SeasonValue);
                dfBillboardBatch.SetMaterial(natureArchive);
                TerrainHelper.LayoutNatureBillboards(dfTerrain, dfBillboardBatch, TerrainScale);
            }

            // Only set active again once complete
            terrainDesc.billboardBatchObject.SetActive(true);
        }
Beispiel #7
0
        public override void PromoteMaterial(DaggerfallTerrain daggerfallTerrain, TerrainMaterialData terrainMaterialData)
        {
            Material tileMaterial = DaggerfallUnity.Instance.MaterialReader.GetTerrainTextureArrayMaterial(GetGroundArchive(terrainMaterialData.WorldClimate));

            // Assign textures (propagate material settings from tileMaterial to terrainMaterial)
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TileTexArr, tileMaterial.GetTexture(TileTexArrUniforms.TileTexArr));
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TileNormalMapTexArr, tileMaterial.GetTexture(TileTexArrUniforms.TileNormalMapTexArr));
            if (tileMaterial.IsKeywordEnabled(KeyWords.NormalMap))
            {
                terrainMaterialData.Material.EnableKeyword(KeyWords.NormalMap);
            }
            else
            {
                terrainMaterialData.Material.DisableKeyword(KeyWords.NormalMap);
            }
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TileMetallicGlossMapTexArr, tileMaterial.GetTexture(TileTexArrUniforms.TileMetallicGlossMapTexArr));
            terrainMaterialData.Material.SetTexture(TileTexArrUniforms.TilemapTex, terrainMaterialData.TileMapTexture);
        }
Beispiel #8
0
        // Create new location game object
        private GameObject CreateLocationGameObject(int terrain, out DFLocation locationOut)
        {
            locationOut = new DFLocation();

            // Terrain must have a location
            DaggerfallTerrain dfTerrain = terrainArray[terrain].terrainObject.GetComponent <DaggerfallTerrain>();

            if (!dfTerrain.MapData.hasLocation)
            {
                return(null);
            }

            // Get location data
            locationOut = dfUnity.ContentReader.MapFileReader.GetLocation(dfTerrain.MapData.mapRegionIndex, dfTerrain.MapData.mapLocationIndex);
            if (!locationOut.Loaded)
            {
                return(null);
            }

            // Get sampled position of height as more accurate than scaled average - thanks Nystul!
            Terrain terrainInstance = dfTerrain.gameObject.GetComponent <Terrain>();
            float   scale           = terrainInstance.terrainData.heightmapScale.x;
            float   xSamplePos      = (TerrainHelper.terrainTileDim - 1) / 2.0f; // get center terrain tile of block
            float   ySamplePos      = (TerrainHelper.terrainTileDim - 1) / 2.0f; // get center terrain tile of block
            Vector3 pos             = new Vector3(xSamplePos * scale, 0, ySamplePos * scale);
            float   height          = terrainInstance.SampleHeight(pos + terrainArray[terrain].terrainObject.transform.position);

            // Spawn parent game object for new location
            //float height = dfTerrain.MapData.averageHeight * TerrainScale;
            GameObject locationObject = new GameObject(string.Format("DaggerfallLocation [Region={0}, Name={1}]", locationOut.RegionName, locationOut.Name));

            locationObject.transform.parent = this.transform;
            //locationObject.hideFlags = HideFlags.HideAndDontSave;
            locationObject.transform.position = terrainArray[terrain].terrainObject.transform.position + new Vector3(0, height, 0);
            DaggerfallLocation dfLocation = locationObject.AddComponent <DaggerfallLocation>() as DaggerfallLocation;

            dfLocation.SetLocation(locationOut, false);

            // Raise event
            RaiseOnCreateLocationGameObjectEvent(dfLocation);

            return(locationObject);
        }
        // Sets terrain neighbours
        // Should only be done after terrain is placed and collected
        private void UpdateNeighbours()
        {
            for (int i = 0; i < terrainArray.Length; i++)
            {
                // Check object exists
                if (!terrainArray[i].terrainObject)
                {
                    continue;
                }

                // Get DaggerfallTerrain
                DaggerfallTerrain dfTerrain = terrainArray[i].terrainObject.GetComponent <DaggerfallTerrain>();
                if (!dfTerrain)
                {
                    continue;
                }

                // Set or clear neighbours
                if (terrainArray[i].active)
                {
                    dfTerrain.LeftNeighbour   = GetTerrain(dfTerrain.MapPixelX - 1, dfTerrain.MapPixelY);
                    dfTerrain.RightNeighbour  = GetTerrain(dfTerrain.MapPixelX + 1, dfTerrain.MapPixelY);
                    dfTerrain.TopNeighbour    = GetTerrain(dfTerrain.MapPixelX, dfTerrain.MapPixelY - 1);
                    dfTerrain.BottomNeighbour = GetTerrain(dfTerrain.MapPixelX, dfTerrain.MapPixelY + 1);
                    dfTerrain.UpdateNeighbours();
                }
                else
                {
                    dfTerrain.LeftNeighbour   = null;
                    dfTerrain.RightNeighbour  = null;
                    dfTerrain.TopNeighbour    = null;
                    dfTerrain.BottomNeighbour = null;
                }

                // Update Unity Terrain
                dfTerrain.UpdateNeighbours();
            }
        }
        // Update terrain nature
        private void UpdateTerrainNature(TerrainDesc terrainDesc)
        {
            //System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
            //long startTime = stopwatch.ElapsedMilliseconds;

            // Setup billboards
            DaggerfallTerrain        dfTerrain        = terrainDesc.terrainObject.GetComponent <DaggerfallTerrain>();
            DaggerfallBillboardBatch dfBillboardBatch = terrainDesc.billboardBatchObject.GetComponent <DaggerfallBillboardBatch>();

            if (dfTerrain && dfBillboardBatch)
            {
                // Get current climate and nature archive
                int natureArchive = ClimateSwaps.GetNatureArchive(LocalPlayerGPS.ClimateSettings.NatureSet, dfUnity.WorldTime.Now.SeasonValue);
                dfBillboardBatch.SetMaterial(natureArchive);
                TerrainHelper.LayoutNatureBillboards(dfTerrain, dfBillboardBatch, TerrainScale);
            }

            // Only set active again once complete
            terrainDesc.billboardBatchObject.SetActive(true);

            //long totalTime = stopwatch.ElapsedMilliseconds - startTime;
            //DaggerfallUnity.LogMessage(string.Format("Time to update terrain nature: {0}ms", totalTime), true);
        }
        // Drops nature flats based on random chance scaled by simple rules
        public static void LayoutNatureBillboards(DaggerfallTerrain dfTerrain, DaggerfallBillboardBatch dfBillboardBatch, float terrainScale, int terrainDist)
        {
            const float maxSteepness      = 50f;        // 50
            const float slopeSinkRatio    = 70f;        // Sink flats slightly into ground as slope increases to prevent floaty trees.
            const float baseChanceOnDirt  = 0.2f;       // 0.2
            const float baseChanceOnGrass = 0.9f;       // 0.4
            const float baseChanceOnStone = 0.05f;      // 0.05

            // Location Rect is expanded slightly to give extra clearance around locations
            const int natureClearance = 4;
            Rect      rect            = dfTerrain.MapData.locationRect;

            if (rect.x > 0 && rect.y > 0)
            {
                rect.xMin -= natureClearance;
                rect.xMax += natureClearance;
                rect.yMin -= natureClearance;
                rect.yMax += natureClearance;
            }
            // Chance scaled based on map pixel height
            // This tends to produce sparser lowlands and denser highlands
            // Adjust or remove clamp range to influence nature generation
            float elevationScale = (dfTerrain.MapData.worldHeight / 128f);

            elevationScale = Mathf.Clamp(elevationScale, 0.4f, 1.0f);

            // Chance scaled by base climate type
            float climateScale = 1.0f;

            DFLocation.ClimateSettings climate = MapsFile.GetWorldClimateSettings(dfTerrain.MapData.worldClimate);
            switch (climate.ClimateType)
            {
            case DFLocation.ClimateBaseType.Desert:             // Just lower desert for now
                climateScale = 0.25f;
                break;
            }
            float chanceOnDirt  = baseChanceOnDirt * elevationScale * climateScale;
            float chanceOnGrass = baseChanceOnGrass * elevationScale * climateScale;
            float chanceOnStone = baseChanceOnStone * elevationScale * climateScale;

            // Get terrain
            Terrain terrain = dfTerrain.gameObject.GetComponent <Terrain>();

            if (!terrain)
            {
                return;
            }

            // Get terrain data
            TerrainData terrainData = terrain.terrainData;

            if (!terrainData)
            {
                return;
            }

            // Remove exiting billboards
            dfBillboardBatch.Clear();
            MeshReplacement.ClearNatureGameObjects(terrain);

            // Seed random with terrain key
            Random.InitState(MakeTerrainKey(dfTerrain.MapPixelX, dfTerrain.MapPixelY));

            // Just layout some random flats spread evenly across entire map pixel area
            // Flats are aligned with tiles, max 16129 billboards per batch
            Vector2 tilePos          = Vector2.zero;
            int     tDim             = MapsFile.WorldMapTileDim;
            int     hDim             = DaggerfallUnity.Instance.TerrainSampler.HeightmapDimension;
            float   scale            = terrainData.heightmapScale.x * (float)hDim / (float)tDim;
            float   maxTerrainHeight = DaggerfallUnity.Instance.TerrainSampler.MaxTerrainHeight;
            float   beachLine        = DaggerfallUnity.Instance.TerrainSampler.BeachElevation;

            for (int y = 0; y < tDim; y++)
            {
                for (int x = 0; x < tDim; x++)
                {
                    // Reject based on steepness
                    float steepness = terrainData.GetSteepness((float)x / tDim, (float)y / tDim);
                    if (steepness > maxSteepness)
                    {
                        continue;
                    }

                    // Reject if inside location rect (expanded slightly to give extra clearance around locations)
                    tilePos.x = x;
                    tilePos.y = y;
                    if (rect.x > 0 && rect.y > 0 && rect.Contains(tilePos))
                    {
                        continue;
                    }

                    // Chance also determined by tile type
                    int tile = dfTerrain.MapData.tilemapSamples[x, y] & 0x3F;
                    if (tile == 1)
                    {   // Dirt
                        if (Random.Range(0f, 1f) > chanceOnDirt)
                        {
                            continue;
                        }
                    }
                    else if (tile == 2)
                    {   // Grass
                        if (Random.Range(0f, 1f) > chanceOnGrass)
                        {
                            continue;
                        }
                    }
                    else if (tile == 3)
                    {   // Stone
                        if (Random.Range(0f, 1f) > chanceOnStone)
                        {
                            continue;
                        }
                    }
                    else
                    {   // Anything else
                        continue;
                    }

                    int   hx     = (int)Mathf.Clamp(hDim * ((float)x / (float)tDim), 0, hDim - 1);
                    int   hy     = (int)Mathf.Clamp(hDim * ((float)y / (float)tDim), 0, hDim - 1);
                    float height = dfTerrain.MapData.heightmapSamples[hy, hx] * maxTerrainHeight;  // x & y swapped in heightmap for TerrainData.SetHeights()

                    // Reject if too close to water
                    if (height < beachLine)
                    {
                        continue;
                    }

                    // Sample height and position billboard
                    Vector3 pos     = new Vector3(x * scale, 0, y * scale);
                    float   height2 = terrain.SampleHeight(pos + terrain.transform.position);
                    pos.y = height2 - (steepness / slopeSinkRatio);

                    // Add to batch unless a mesh replacement is found
                    int record = Random.Range(1, 32);
                    if (terrainDist > 1 || !MeshReplacement.ImportNatureGameObject(dfBillboardBatch.TextureArchive, record, terrain, x, y))
                    {
                        dfBillboardBatch.AddItem(record, pos);
                    }
                    else if (!NatureMeshUsed)
                    {
                        NatureMeshUsed = true;  // Signal that nature mesh has been used to initiate extra terrain updates
                    }
                }
            }

            // Apply new batch
            dfBillboardBatch.Apply();
        }
 //overloaded variant
 void InjectMaterialProperties(DaggerfallTerrain sender)
 {
     InjectMaterialProperties(-1, -1);
 }
Beispiel #13
0
 public abstract void PromoteMaterial(DaggerfallTerrain daggerfallTerrain, TerrainMaterialData terrainMaterialData);
Beispiel #14
0
        // Drops nature flats based on random chance scaled by simple rules
        public static void LayoutNatureBillboards(DaggerfallTerrain dfTerrain, DaggerfallBillboardBatch dfBillboardBatch, float terrainScale)
        {
            const float maxSteepness  = 50f;        // 50
            const float chanceOnDirt  = 0.2f;       // 0.2
            const float chanceOnGrass = 0.9f;       // 0.4
            const float chanceOnStone = 0.05f;      // 0.05

            // Get terrain
            Terrain terrain = dfTerrain.gameObject.GetComponent <Terrain>();

            if (!terrain)
            {
                return;
            }

            // Get terrain data
            TerrainData terrainData = terrain.terrainData;

            if (!terrainData)
            {
                return;
            }

            // Remove exiting billboards
            dfBillboardBatch.Clear();

            // Seed random with terrain key
            UnityEngine.Random.seed = MakeTerrainKey(dfTerrain.MapPixelX, dfTerrain.MapPixelY);

            // Just layout some random flats spread evenly across entire map pixel area
            // Flats are aligned with tiles, max 127x127 in billboard batch
            Vector2 tilePos = Vector2.zero;
            float   scale   = terrainData.heightmapScale.x;
            int     dim     = TerrainHelper.terrainTileDim - 1;

            for (int y = 0; y < dim; y++)
            {
                for (int x = 0; x < dim; x++)
                {
                    // Reject based on steepness
                    float steepness = terrainData.GetSteepness((float)x / dim, (float)y / dim);
                    if (steepness > maxSteepness)
                    {
                        continue;
                    }

                    // Reject if inside location rect
                    // Rect is expanded slightly to give extra clearance around locations
                    tilePos.x = x;
                    tilePos.y = y;
                    const int natureClearance = 4;
                    Rect      rect            = dfTerrain.MapData.locationRect;
                    if (rect.x > 0 && rect.y > 0)
                    {
                        rect.xMin -= natureClearance;
                        rect.xMin += natureClearance;
                        rect.yMin -= natureClearance;
                        rect.yMax += natureClearance;
                        if (rect.Contains(tilePos))
                        {
                            continue;
                        }
                    }

                    // Chance scaled based on map pixel height
                    // This tends to produce sparser lowlands and denser highlands
                    // Adjust or remove clamp range to influence nature generation
                    float elevationScale = (dfTerrain.MapData.worldHeight / 128f);
                    elevationScale = Mathf.Clamp(elevationScale, 0.4f, 1.0f);

                    // Chance scaled by base climate type
                    float climateScale = 1.0f;
                    DFLocation.ClimateSettings climate = MapsFile.GetWorldClimateSettings(dfTerrain.MapData.worldClimate);
                    switch (climate.ClimateType)
                    {
                    case DFLocation.ClimateBaseType.Desert:             // Just lower desert for now
                        climateScale = 0.25f;
                        break;
                    }

                    // Chance also determined by tile type
                    WorldSample sample = TerrainHelper.GetSample(ref dfTerrain.MapData.samples, x, y);
                    if (sample.record == 1)
                    {
                        // Dirt
                        if (UnityEngine.Random.Range(0f, 1f) > chanceOnDirt * elevationScale * climateScale)
                        {
                            continue;
                        }
                    }
                    else if (sample.record == 2)
                    {
                        // Grass
                        if (UnityEngine.Random.Range(0f, 1f) > chanceOnGrass * elevationScale * climateScale)
                        {
                            continue;
                        }
                    }
                    else if (sample.record == 3)
                    {
                        // Stone
                        if (UnityEngine.Random.Range(0f, 1f) > chanceOnStone * elevationScale * climateScale)
                        {
                            continue;
                        }
                    }
                    else
                    {
                        // Anything else
                        continue;
                    }

                    // Sample height and position billboard
                    Vector3 pos    = new Vector3(x * scale, 0, y * scale);
                    float   height = terrain.SampleHeight(pos + terrain.transform.position);
                    pos.y = height;

                    // Reject if too close to water
                    float beachLine = DaggerfallUnity.Instance.TerrainSampler.BeachElevation * terrainScale;
                    if (height < beachLine)
                    {
                        continue;
                    }

                    // Add to batch
                    int record = UnityEngine.Random.Range(1, 32);
                    dfBillboardBatch.AddItem(record, pos);
                }
            }

            // Apply new batch
            dfBillboardBatch.Apply();
        }
        // Drops nature flats based on random chance scaled by simple rules
        public static void LayoutNatureBillboards(DaggerfallTerrain dfTerrain, DaggerfallBillboardBatch dfBillboardBatch, float terrainScale)
        {
            const float maxSteepness = 50f;         // 50
            const float chanceOnDirt = 0.2f;        // 0.2
            const float chanceOnGrass = 0.9f;       // 0.4
            const float chanceOnStone = 0.05f;      // 0.05

            // Get terrain
            Terrain terrain = dfTerrain.gameObject.GetComponent<Terrain>();
            if (!terrain)
                return;

            // Get terrain data
            TerrainData terrainData = terrain.terrainData;
            if (!terrainData)
                return;

            // Remove exiting billboards
            dfBillboardBatch.Clear();

            // Seed random with terrain key
            UnityEngine.Random.seed = MakeTerrainKey(dfTerrain.MapPixelX, dfTerrain.MapPixelY);

            // Just layout some random flats spread evenly across entire map pixel area
            // Flats are aligned with tiles, max 127x127 in billboard batch
            Vector2 tilePos = Vector2.zero;
            float scale = terrainData.heightmapScale.x;
            int dim = TerrainHelper.terrainTileDim - 1;
            for (int y = 0; y < dim; y++)
            {
                for (int x = 0; x < dim; x++)
                {
                    // Reject based on steepness
                    float steepness = terrainData.GetSteepness((float)x / dim, (float)y / dim);
                    if (steepness > maxSteepness)
                        continue;

                    // Reject if inside location rect
                    // Rect is expanded slightly to give extra clearance around locations
                    tilePos.x = x;
                    tilePos.y = y;
                    const int natureClearance = 4;
                    Rect rect = dfTerrain.MapData.locationRect;
                    if (rect.x > 0 && rect.y > 0)
                    {
                        rect.xMin -= natureClearance;
                        rect.xMin += natureClearance;
                        rect.yMin -= natureClearance;
                        rect.yMax += natureClearance;
                        if (rect.Contains(tilePos))
                            continue;
                    }

                    // Chance scaled based on map pixel height
                    // This tends to produce sparser lowlands and denser highlands
                    // Adjust or remove clamp range to influence nature generation
                    float elevationScale = (dfTerrain.MapData.worldHeight / 128f);
                    elevationScale = Mathf.Clamp(elevationScale, 0.4f, 1.0f);

                    // Chance scaled by base climate type
                    float climateScale = 1.0f;
                    DFLocation.ClimateSettings climate = MapsFile.GetWorldClimateSettings(dfTerrain.MapData.worldClimate);
                    switch (climate.ClimateType)
                    {
                        case DFLocation.ClimateBaseType.Desert:         // Just lower desert for now
                            climateScale = 0.25f;
                            break;
                    }

                    // Chance also determined by tile type
                    WorldSample sample = TerrainHelper.GetSample(ref dfTerrain.MapData.samples, x, y);
                    if (sample.record == 1)
                    {
                        // Dirt
                        if (UnityEngine.Random.Range(0f, 1f) > chanceOnDirt * elevationScale * climateScale)
                            continue;
                    }
                    else if (sample.record == 2)
                    {
                        // Grass
                        if (UnityEngine.Random.Range(0f, 1f) > chanceOnGrass * elevationScale * climateScale)
                            continue;
                    }
                    else if (sample.record == 3)
                    {
                        // Stone
                        if (UnityEngine.Random.Range(0f, 1f) > chanceOnStone * elevationScale * climateScale)
                            continue;
                    }
                    else
                    {
                        // Anything else
                        continue;
                    }

                    // Sample height and position billboard
                    Vector3 pos = new Vector3(x * scale, 0, y * scale);
                    float height = terrain.SampleHeight(pos + terrain.transform.position);
                    pos.y = height;

                    // Reject if too close to water
                    float beachLine = TerrainHelper.scaledBeachElevation * terrainScale;
                    if (height < beachLine - beachLine * 0.1f)
                        continue;

                    // Add to batch
                    int record = UnityEngine.Random.Range(1, 32);
                    dfBillboardBatch.AddItem(record, pos);
                }
            }

            // Apply new batch
            dfBillboardBatch.Apply();
        }