/// <summary> /// Updates map pixel data based on current coordinates. /// Must be called before other data update methods. /// </summary> public void UpdateMapPixelData(TerrainTexturing terrainTexturing = null) { if (!ReadyCheck()) { return; } //System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew(); //long startTime = stopwatch.ElapsedMilliseconds; // Get basic terrain data MapData = TerrainHelper.GetMapPixelData(dfUnity.ContentReader, MapPixelX, MapPixelY); dfUnity.TerrainSampler.GenerateSamples(ref MapData); // Handle terrain with location if (MapData.hasLocation) { TerrainHelper.SetLocationTiles(ref MapData); TerrainHelper.BlendLocationTerrain(ref MapData); } // Set textures if (terrainTexturing != null) { terrainTexturing.AssignTiles(dfUnity.TerrainSampler, ref MapData); } //long totalTime = stopwatch.ElapsedMilliseconds - startTime; //DaggerfallUnity.LogMessage(string.Format("Time to update map pixel data: {0}ms", totalTime), true); }
// Calculate location rect in world units private void CalculateWorldLocationRect() { if (!hasCurrentLocation) { return; } // Convert world coords to map pixel coords then back again // This finds the absolute SW origin of this map pixel in world coords DFPosition mapPixel = CurrentMapPixel; DFPosition worldOrigin = MapsFile.MapPixelToWorldCoord(mapPixel.X, mapPixel.Y); // Find tile offset point using same logic as terrain helper DFPosition tileOrigin = TerrainHelper.GetLocationTerrainTileOrigin(CurrentLocation); // Adjust world origin by tileorigin*2 in world units worldOrigin.X += (tileOrigin.X * 2) * MapsFile.WorldMapTileDim; worldOrigin.Y += (tileOrigin.Y * 2) * MapsFile.WorldMapTileDim; // Get width and height of location in world units int width = currentLocation.Exterior.ExteriorData.Width * MapsFile.WorldMapRMBDim; int height = currentLocation.Exterior.ExteriorData.Height * MapsFile.WorldMapRMBDim; // Set location rect in world coordinates locationWorldRectMinX = worldOrigin.X; locationWorldRectMaxX = worldOrigin.X + width; locationWorldRectMinZ = worldOrigin.Y; locationWorldRectMaxZ = worldOrigin.Y + height; }
// 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); }
// Sets player to gound level at position in specified terrain // Terrain data must already be loaded // LocalGPS must be attached to your player game object private void PositionPlayerToTerrain(int mapPixelX, int mapPixelY, Vector3 position) { // Get terrain key int key = TerrainHelper.MakeTerrainKey(mapPixelX, mapPixelY); if (!terrainIndexDict.ContainsKey(key)) { return; } // Get terrain Terrain terrain = terrainArray[terrainIndexDict[key]].terrainObject.GetComponent <Terrain>(); // Sample height at this position CapsuleCollider collider = LocalPlayerGPS.gameObject.GetComponent <CapsuleCollider>(); if (collider) { Vector3 pos = new Vector3(position.x, 0, position.z); float height = terrain.SampleHeight(pos + terrain.transform.position); pos.y = height + collider.height * 1.5f; // Move player to this position and align to ground using raycast LocalPlayerGPS.transform.position = pos; FixStanding(LocalPlayerGPS.transform, collider.height); } else { throw new Exception("StreamingWorld: Could not find CapsuleCollider peered with LocalPlayerGPS."); } }
/// <summary> /// Helper to get location rect in world coordinates. /// </summary> /// <param name="location">Target location.</param> /// <returns>Location rect in world space. xMin,yMin is SW corner. xMax,yMax is NE corner.</returns> public static Rect GetLocationRect(DFLocation location) { // This finds the absolute SW origin of map pixel in world coords DFPosition mapPixel = MapsFile.LongitudeLatitudeToMapPixel(location.MapTableData.Longitude, location.MapTableData.Latitude); DFPosition worldOrigin = MapsFile.MapPixelToWorldCoord(mapPixel.X, mapPixel.Y); // Find tile offset point using same logic as terrain helper DFPosition tileOrigin = TerrainHelper.GetLocationTerrainTileOrigin(location); // Adjust world origin by tileorigin*2 in world units worldOrigin.X += (tileOrigin.X * 2) * MapsFile.WorldMapTileDim; worldOrigin.Y += (tileOrigin.Y * 2) * MapsFile.WorldMapTileDim; // Get width and height of location in world units int width = location.Exterior.ExteriorData.Width * MapsFile.WorldMapRMBDim; int height = location.Exterior.ExteriorData.Height * MapsFile.WorldMapRMBDim; // Create location rect in world coordinates Rect locationRect = new Rect() { xMin = worldOrigin.X, xMax = worldOrigin.X + width, yMin = worldOrigin.Y, yMax = worldOrigin.Y + height, }; return(locationRect); }
// Destroy any locations outside of range private void CollectLocations(bool collectAll = false) { // Determine which terrains need to be destroyed int mapPixelX, mapPixelY; locationKeysToDestroy.Clear(); foreach (var keyValuePair in locationDict) { TerrainHelper.ReverseTerrainKey(keyValuePair.Key, out mapPixelX, out mapPixelY); if (!IsInRange(mapPixelX, mapPixelY) || collectAll) { locationKeysToDestroy.Add(keyValuePair.Key); } } // Destroy the terrains for (int i = 0; i < locationKeysToDestroy.Count; i++) { int key = locationKeysToDestroy[i]; GameObject locationObject = locationDict[key]; locationObject.SetActive(false); StartCoroutine(DestroyLocationIterative(locationObject)); locationDict.Remove(key); } }
private bool ReadyCheck() { if (isReady) { return(true); } if (dfUnity == null) { dfUnity = DaggerfallUnity.Instance; } // Do nothing if DaggerfallUnity not ready if (!dfUnity.IsReady) { DaggerfallUnity.LogMessage("StreamingWorld: DaggerfallUnity component is not ready. Have you set your Arena2 path?"); return(false); } // Perform initial runtime setup if (Application.isPlaying) { // Fix coastal climate data TerrainHelper.DilateCoastalClimate(dfUnity.ContentReader, 2); // Smooth steep location on steep gradients TerrainHelper.SmoothLocationNeighbourhood(dfUnity.ContentReader); } // Raise ready flag isReady = true; return(true); }
/// <summary> /// Update map pixel data based on current coordinates. (first of a two stage process) /// /// 1) BeginMapPixelDataUpdate - Schedules terrain data update using jobs system. /// 2) CompleteMapPixelDataUpdate - Completes terrain data update using jobs system. /// </summary> /// <param name="terrainTexturing">Instance of ITerrainTexturing implementation class to use.</param> /// <returns>JobHandle of the scheduled jobs</returns> public JobHandle BeginMapPixelDataUpdate(ITerrainTexturing terrainTexturing = null) { // Get basic terrain data. MapData = TerrainHelper.GetMapPixelData(dfUnity.ContentReader, MapPixelX, MapPixelY); // Create data array for heightmap. MapData.heightmapData = new NativeArray <float>(heightmapDim * heightmapDim, Allocator.TempJob); // Create data array for tilemap data. MapData.tilemapData = new NativeArray <byte>(tilemapDim * tilemapDim, Allocator.TempJob); // Create data array for shader tile map data. MapData.tileMap = new NativeArray <Color32>(tilemapDim * tilemapDim, Allocator.TempJob); // Create data array for average & max heights. MapData.avgMaxHeight = new NativeArray <float>(new float[] { 0, float.MinValue }, Allocator.TempJob); // Create list for recording native arrays that need disposal after jobs complete. MapData.nativeArrayList = new List <IDisposable>(); // Generate heightmap samples. (returns when complete) JobHandle generateHeightmapSamplesJobHandle = dfUnity.TerrainSampler.ScheduleGenerateSamplesJob(ref MapData); // Handle location if one is present on terrain. JobHandle blendLocationTerrainJobHandle; if (MapData.hasLocation) { // Schedule job to calc average & max heights. JobHandle calcAvgMaxHeightJobHandle = TerrainHelper.ScheduleCalcAvgMaxHeightJob(ref MapData, generateHeightmapSamplesJobHandle); JobHandle.ScheduleBatchedJobs(); // Set location tiles. TerrainHelper.SetLocationTiles(ref MapData); if (!dfUnity.TerrainSampler.IsLocationTerrainBlended()) { // Schedule job to blend and flatten location heights. (depends on SetLocationTiles being done first) blendLocationTerrainJobHandle = TerrainHelper.ScheduleBlendLocationTerrainJob(ref MapData, calcAvgMaxHeightJobHandle); } else { blendLocationTerrainJobHandle = calcAvgMaxHeightJobHandle; } } else { blendLocationTerrainJobHandle = generateHeightmapSamplesJobHandle; } // Assign tiles for terrain texturing. JobHandle assignTilesJobHandle = (terrainTexturing == null) ? blendLocationTerrainJobHandle : terrainTexturing.ScheduleAssignTilesJob(dfUnity.TerrainSampler, ref MapData, blendLocationTerrainJobHandle); // Update tile map for shader. JobHandle updateTileMapJobHandle = TerrainHelper.ScheduleUpdateTileMapDataJob(ref MapData, assignTilesJobHandle); JobHandle.ScheduleBatchedJobs(); return(updateTileMapJobHandle); }
public override void GenerateSamples(ref MapPixelData mapPixel) { mapPixel.heightmapSamples = new float[HeightmapDimension, HeightmapDimension]; // Populate heightmap float averageHeight = 0; float maxHeight = float.MinValue; for (int y = 0; y < HeightmapDimension; y++) { for (int x = 0; x < HeightmapDimension; x++) { // It is important to use a continuous noise function to avoid gaps between tiles int noisex = mapPixel.mapPixelX * (HeightmapDimension - 1) + x; int noisey = (MapsFile.MaxMapPixelY - mapPixel.mapPixelY) * (HeightmapDimension - 1) + y; float height = TerrainHelper.GetNoise(noisex, noisey, 0.01f, 0.5f, 0.1f, 2) * Scale; mapPixel.heightmapSamples[y, x] = height; // Accumulate averages and max height averageHeight += height; if (height > maxHeight) { maxHeight = height; } } } // Average and max heights are passed back for flattening location areas mapPixel.averageHeight = averageHeight /= (float)(HeightmapDimension * HeightmapDimension); mapPixel.maxHeight = maxHeight; }
// Fully init central terrain so player can be dropped into world as soon as possible private void InitPlayerTerrain() { if (!init) { return; } #if SHOW_LAYOUT_TIMES System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew(); long startTime = stopwatch.ElapsedMilliseconds; #endif CollectLocations(true); int playerTerrainIndex = terrainIndexDict[TerrainHelper.MakeTerrainKey(MapPixelX, MapPixelY)]; UpdateTerrainData(terrainArray[playerTerrainIndex]); terrainArray[playerTerrainIndex].updateData = false; UpdateTerrainNature(terrainArray[playerTerrainIndex]); terrainArray[playerTerrainIndex].updateNature = false; StartCoroutine(UpdateLocation(playerTerrainIndex, false)); terrainArray[playerTerrainIndex].updateLocation = false; #if SHOW_LAYOUT_TIMES long totalTime = stopwatch.ElapsedMilliseconds - startTime; DaggerfallUnity.LogMessage(string.Format("Time to init player terrain: {0}ms", totalTime), true); #endif }
// Finds next available terrain in array private int FindNextAvailableTerrain() { // Evaluate terrain array int found = -1; for (int i = 0; i < terrainArray.Length; i++) { // A null terrain has never been instantiated and is free if (terrainArray[i].terrainObject == null) { found = i; break; } // Inactive terrain object can be evaluated for recycling based // on distance from current map pixel if (!terrainArray[i].active) { // If terrain out of range then recycle if (!IsInRange(terrainArray[i].mapPixelX, terrainArray[i].mapPixelY)) { found = i; break; } } } // Was a terrain found? if (found != -1) { // If we are recycling an inactive terrain, remove it from dictionary first int key = TerrainHelper.MakeTerrainKey(terrainArray[found].mapPixelX, terrainArray[found].mapPixelY); if (terrainIndexDict.ContainsKey(key)) { terrainIndexDict.Remove(key); } return(found); } else { // Unable to find an available terrain // This should never happen unless TerrainDistance too high or maxTerrainArray too low DaggerfallUnity.LogMessage("StreamingWorld: Unable to find free terrain. Check maxTerrainArray is sized appropriately and you are collecting terrains. This can also happen when player movement speed too high.", true); if (Application.isEditor) { Debug.Break(); } else { Application.Quit(); } return(-1); } }
// Gets terrain at map pixel coordinates, or null if not found private Terrain GetTerrain(int mapPixelX, int mapPixelY) { int key = TerrainHelper.MakeTerrainKey(mapPixelX, mapPixelY); if (terrainIndexDict.ContainsKey(key)) { return(terrainArray[terrainIndexDict[key]].terrainObject.GetComponent <Terrain>()); } return(null); }
// Gets transform of the terrain player is standing on private Transform GetPlayerTerrainTransform() { int key = TerrainHelper.MakeTerrainKey(MapPixelX, MapPixelY); if (!terrainIndexDict.ContainsKey(key)) { return(null); } return(terrainArray[terrainIndexDict[key]].terrainObject.transform); }
public void Execute(int index) { // Use cols=x and rows=y for height data int x = JobA.Col(index, hDim); int y = JobA.Row(index, hDim); float rx = (float)x / div; float ry = (float)y / div; int ix = Mathf.FloorToInt(rx); int iy = Mathf.FloorToInt(ry); float sfracx = (float)x / (float)(hDim - 1); float sfracy = (float)y / (float)(hDim - 1); float fracx = (float)(x - ix * div) / div; float fracy = (float)(y - iy * div) / div; float scaledHeight = 0; // Bicubic sample small height map for base terrain elevation x1 = TerrainHelper.CubicInterpolator(shm[JobA.Idx(0, 3, sd)], shm[JobA.Idx(1, 3, sd)], shm[JobA.Idx(2, 3, sd)], shm[JobA.Idx(3, 3, sd)], sfracx); x2 = TerrainHelper.CubicInterpolator(shm[JobA.Idx(0, 2, sd)], shm[JobA.Idx(1, 2, sd)], shm[JobA.Idx(2, 2, sd)], shm[JobA.Idx(3, 2, sd)], sfracx); x3 = TerrainHelper.CubicInterpolator(shm[JobA.Idx(0, 1, sd)], shm[JobA.Idx(1, 1, sd)], shm[JobA.Idx(2, 1, sd)], shm[JobA.Idx(3, 1, sd)], sfracx); x4 = TerrainHelper.CubicInterpolator(shm[JobA.Idx(0, 0, sd)], shm[JobA.Idx(1, 0, sd)], shm[JobA.Idx(2, 0, sd)], shm[JobA.Idx(3, 0, sd)], sfracx); baseHeight = TerrainHelper.CubicInterpolator(x1, x2, x3, x4, sfracy); scaledHeight += baseHeight * baseHeightScale; // Bicubic sample large height map for noise mask over terrain features x1 = TerrainHelper.CubicInterpolator(lhm[JobA.Idx(ix, iy + 0, ld)], lhm[JobA.Idx(ix + 1, iy + 0, ld)], lhm[JobA.Idx(ix + 2, iy + 0, ld)], lhm[JobA.Idx(ix + 3, iy + 0, ld)], fracx); x2 = TerrainHelper.CubicInterpolator(lhm[JobA.Idx(ix, iy + 1, ld)], lhm[JobA.Idx(ix + 1, iy + 1, ld)], lhm[JobA.Idx(ix + 2, iy + 1, ld)], lhm[JobA.Idx(ix + 3, iy + 1, ld)], fracx); x3 = TerrainHelper.CubicInterpolator(lhm[JobA.Idx(ix, iy + 2, ld)], lhm[JobA.Idx(ix + 1, iy + 2, ld)], lhm[JobA.Idx(ix + 2, iy + 2, ld)], lhm[JobA.Idx(ix + 3, iy + 2, ld)], fracx); x4 = TerrainHelper.CubicInterpolator(lhm[JobA.Idx(ix, iy + 3, ld)], lhm[JobA.Idx(ix + 1, iy + 3, ld)], lhm[JobA.Idx(ix + 2, iy + 3, ld)], lhm[JobA.Idx(ix + 3, iy + 3, ld)], fracx); noiseHeight = TerrainHelper.CubicInterpolator(x1, x2, x3, x4, fracy); scaledHeight += noiseHeight * noiseMapScale; // Additional noise mask for small terrain features at ground level int noisex = mapPixelX * (hDim - 1) + x; int noisey = (MapsFile.MaxMapPixelY - mapPixelY) * (hDim - 1) + y; float lowFreq = TerrainHelper.GetNoise(noisex, noisey, 0.3f, 0.5f, 0.5f, 1); float highFreq = TerrainHelper.GetNoise(noisex, noisey, 0.9f, 0.5f, 0.5f, 1); scaledHeight += (lowFreq * highFreq) * extraNoiseScale; // Clamp lower values to ocean elevation if (scaledHeight < scaledOceanElevation) { scaledHeight = scaledOceanElevation; } // Set sample float height = Mathf.Clamp01(scaledHeight / maxTerrainHeight); heightmapData[index] = height; }
/// <summary> /// Returns terrain GameObject from mapPixel, or null if /// no terrain objects are mapped to that pixel /// </summary> public GameObject GetTerrainFromPixel(int mapPixelX, int mapPixelY)//##Lypyl { //Get Key for terrain lookup int key = TerrainHelper.MakeTerrainKey(mapPixelX, mapPixelY); if (terrainIndexDict.ContainsKey(key)) { return(terrainArray[terrainIndexDict[key]].terrainObject); } else { return(null); } }
void CacheTileData(ITerrainSampler terrainSampler, ref MapPixelData mapData) { // Create array if required int dim = MapsFile.WorldMapTileDim + 1; if (tileData == null) { tileData = new int[dim, dim]; } // Populate array with tile metadata for (int y = 0; y < dim; y++) { for (int x = 0; x < dim; x++) { // Height sample for ocean and beach tiles float height = TerrainHelper.GetClampedHeight( ref mapData, terrainSampler.HeightmapDimension, (float)x / (float)dim, (float)y / (float)dim) * terrainSampler.MaxTerrainHeight; // Ocean texture if (height <= terrainSampler.OceanElevation) { tileData[x, y] = water; continue; } // Get latitude and longitude of this tile int latitude = (int)(mapData.mapPixelX * MapsFile.WorldMapTileDim + x); int longitude = (int)(MapsFile.MaxWorldTileCoordZ - mapData.mapPixelY * MapsFile.WorldMapTileDim + y); // Beach texture // Adds a little +/- randomness to threshold so beach line isn't too regular if (height <= terrainSampler.BeachElevation + UnityEngine.Random.Range(-1.5f, 1.5f)) { tileData[x, y] = dirt; continue; } // Set texture tile using weighted noise float weight = 0; weight += NoiseWeight(latitude, longitude); // TODO: Add other weights to influence texture tile generation tileData[x, y] = GetWeightedRecord(weight); } } }
// 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); }
public override void GenerateSamples(ref MapPixelData mapPixel) { DaggerfallUnity dfUnity = DaggerfallUnity.Instance; // Create samples array int dim = TerrainHelper.terrainSampleDim; mapPixel.samples = new WorldSample[dim * dim]; // Populate samples float averageHeight = 0; float maxHeight = float.MinValue; for (int y = 0; y < dim; y++) { for (int x = 0; x < dim; x++) { // It is important to use a continous noise function to avoid gaps between tiles float latitude = mapPixel.mapPixelX * MapsFile.WorldMapTileDim + x; float longitude = MapsFile.MaxWorldTileCoordZ - mapPixel.mapPixelY * MapsFile.WorldMapTileDim + y; float height = TerrainHelper.GetNoise(dfUnity.ContentReader.Noise, latitude, longitude, 0.0025f, 0.9f, 0.7f, 2); height *= baseHeightScale; // Set sample mapPixel.samples[y * dim + x] = new WorldSample() { scaledHeight = height, }; // Accumulate averages and max height averageHeight += height; if (height > maxHeight) { maxHeight = height; } } } // Average and max heights are passed back for flattening location areas mapPixel.averageHeight = averageHeight /= (float)(dim * dim); mapPixel.maxHeight = maxHeight; }
// 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); }
/// <summary> /// Updates map pixel data based on current coordinates. /// Must be called before other data update methods. /// </summary> public void UpdateMapPixelData(TerrainTexturing terrainTexturing = null) { if (!ReadyCheck()) { return; } // Get basic terrain data MapData = TerrainHelper.GetMapPixelData(dfUnity.ContentReader, MapPixelX, MapPixelY); TerrainHelper.GenerateSamples(dfUnity.ContentReader, ref MapData); // Handle terrain with location if (MapData.hasLocation) { TerrainHelper.SetLocationTiles(dfUnity.ContentReader, ref MapData); TerrainHelper.FlattenLocationTerrain(dfUnity.ContentReader, ref MapData); } // Set textures if (terrainTexturing != null) { terrainTexturing.AssignTiles(ref MapData); } }
private IEnumerator UpdateLocation(int index, bool allowYield) { int key = TerrainHelper.MakeTerrainKey(terrainArray[index].mapPixelX, terrainArray[index].mapPixelY); int playerKey = TerrainHelper.MakeTerrainKey(MapPixelX, MapPixelY); bool isPlayerTerrain = (key == playerKey); if (terrainArray[index].active && terrainArray[index].hasLocation && terrainArray[index].updateLocation) { // Disable update flag terrainArray[index].updateLocation = false; // Create location object DFLocation location; GameObject locationObject = CreateLocationGameObject(index, out location); if (locationObject) { // Add location object to dictionary LocationDesc locationDesc = new LocationDesc(); locationDesc.locationObject = locationObject; locationDesc.mapPixelX = terrainArray[index].mapPixelX; locationDesc.mapPixelY = terrainArray[index].mapPixelY; locationList.Add(locationDesc); // Create billboard batch game objects for this location // Streaming world always batches for performance, regardless of options int natureArchive = ClimateSwaps.GetNatureArchive(LocalPlayerGPS.ClimateSettings.NatureSet, dfUnity.WorldTime.Now.SeasonValue); TextureAtlasBuilder miscBillboardAtlas = dfUnity.MaterialReader.MiscBillboardAtlas; DaggerfallBillboardBatch natureBillboardBatch = GameObjectHelper.CreateBillboardBatchGameObject(natureArchive, locationObject.transform); DaggerfallBillboardBatch lightsBillboardBatch = GameObjectHelper.CreateBillboardBatchGameObject(TextureReader.LightsTextureArchive, locationObject.transform); DaggerfallBillboardBatch animalsBillboardBatch = GameObjectHelper.CreateBillboardBatchGameObject(TextureReader.AnimalsTextureArchive, locationObject.transform); DaggerfallBillboardBatch miscBillboardBatch = GameObjectHelper.CreateBillboardBatchGameObject(miscBillboardAtlas.AtlasMaterial, locationObject.transform); // Set hide flags natureBillboardBatch.hideFlags = HideFlags.HideAndDontSave; lightsBillboardBatch.hideFlags = HideFlags.HideAndDontSave; animalsBillboardBatch.hideFlags = HideFlags.HideAndDontSave; miscBillboardBatch.hideFlags = HideFlags.HideAndDontSave; // RMB blocks are laid out in centre of terrain to align with ground int width = location.Exterior.ExteriorData.Width; int height = location.Exterior.ExteriorData.Height; float offsetX = ((8 * RMBLayout.RMBSide) - (width * RMBLayout.RMBSide)) / 2; float offsetZ = ((8 * RMBLayout.RMBSide) - (height * RMBLayout.RMBSide)) / 2; Vector3 origin = new Vector3(offsetX, 2.0f * MeshReader.GlobalScale, offsetZ); // Get location data DaggerfallLocation dfLocation = locationObject.GetComponent <DaggerfallLocation>(); // Perform layout and yield after each block is placed for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Set block origin for billboard batches // This causes next additions to be offset by this position Vector3 blockOrigin = origin + new Vector3((x * RMBLayout.RMBSide), 0, (y * RMBLayout.RMBSide)); natureBillboardBatch.BlockOrigin = blockOrigin; lightsBillboardBatch.BlockOrigin = blockOrigin; animalsBillboardBatch.BlockOrigin = blockOrigin; miscBillboardBatch.BlockOrigin = blockOrigin; // Add block and yield string blockName = dfUnity.ContentReader.BlockFileReader.CheckName(dfUnity.ContentReader.MapFileReader.GetRmbBlockName(ref location, x, y)); GameObject go = GameObjectHelper.CreateRMBBlockGameObject( blockName, false, dfUnity.Option_CityBlockPrefab, natureBillboardBatch, lightsBillboardBatch, animalsBillboardBatch, miscBillboardAtlas, miscBillboardBatch); go.hideFlags = HideFlags.HideAndDontSave; go.transform.parent = locationObject.transform; go.transform.localPosition = blockOrigin; dfLocation.ApplyClimateSettings(); if (allowYield) { yield return(new WaitForEndOfFrame()); } } } // If this is the player terrain we may need to reposition player if (isPlayerTerrain && repositionPlayer) { // Position to location and use start marker for large cities bool useStartMarker = (dfLocation.Summary.LocationType == DFRegion.LocationTypes.TownCity); PositionPlayerToLocation(MapPixelX, MapPixelY, dfLocation, origin, width, height, useStartMarker); repositionPlayer = false; } // Apply billboard batches natureBillboardBatch.Apply(); lightsBillboardBatch.Apply(); animalsBillboardBatch.Apply(); miscBillboardBatch.Apply(); } } else if (terrainArray[index].active) { if (playerKey == key && repositionPlayer) { PositionPlayerToTerrain(MapPixelX, MapPixelY, Vector3.zero); repositionPlayer = false; } } }
// 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(); }
public virtual void LayoutNature(DaggerfallTerrain dfTerrain, DaggerfallBillboardBatch dfBillboardBatch, float terrainScale, int terrainDist) { // Location Rect is expanded slightly to give extra clearance around locations 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(TerrainHelper.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(); }
public void ProcessTaskThread(System.Object stateInfo) { DataForTask dataForTask = stateInfo as DataForTask; // Extract height samples for all chunks float baseHeight, noiseHeight; float x1, x2, x3, x4; int dim = dataForTask.HeightmapDimension; float div = dataForTask.div; byte[,] shm = dataForTask.shm; byte[,] lhm = dataForTask.lhm; // split the work between different tasks running in different threads (thread n computes data elements n, n + numTasks, n + numTasks*2, ...) for (int y = dataForTask.currentTask; y < dim; y += dataForTask.numTasks) { for (int x = 0; x < dim; x++) { float rx = (float)x / div; float ry = (float)y / div; int ix = Mathf.FloorToInt(rx); int iy = Mathf.FloorToInt(ry); float sfracx = (float)x / (float)(dim - 1); float sfracy = (float)y / (float)(dim - 1); float fracx = (float)(x - ix * div) / div; float fracy = (float)(y - iy * div) / div; float scaledHeight = 0; // Bicubic sample small height map for base terrain elevation x1 = TerrainHelper.CubicInterpolator(shm[0, 3], shm[1, 3], shm[2, 3], shm[3, 3], sfracx); x2 = TerrainHelper.CubicInterpolator(shm[0, 2], shm[1, 2], shm[2, 2], shm[3, 2], sfracx); x3 = TerrainHelper.CubicInterpolator(shm[0, 1], shm[1, 1], shm[2, 1], shm[3, 1], sfracx); x4 = TerrainHelper.CubicInterpolator(shm[0, 0], shm[1, 0], shm[2, 0], shm[3, 0], sfracx); baseHeight = TerrainHelper.CubicInterpolator(x1, x2, x3, x4, sfracy); scaledHeight += baseHeight * baseHeightScale; // Bicubic sample large height map for noise mask over terrain features x1 = TerrainHelper.CubicInterpolator(lhm[ix, iy + 0], lhm[ix + 1, iy + 0], lhm[ix + 2, iy + 0], lhm[ix + 3, iy + 0], fracx); x2 = TerrainHelper.CubicInterpolator(lhm[ix, iy + 1], lhm[ix + 1, iy + 1], lhm[ix + 2, iy + 1], lhm[ix + 3, iy + 1], fracx); x3 = TerrainHelper.CubicInterpolator(lhm[ix, iy + 2], lhm[ix + 1, iy + 2], lhm[ix + 2, iy + 2], lhm[ix + 3, iy + 2], fracx); x4 = TerrainHelper.CubicInterpolator(lhm[ix, iy + 3], lhm[ix + 1, iy + 3], lhm[ix + 2, iy + 3], lhm[ix + 3, iy + 3], fracx); noiseHeight = TerrainHelper.CubicInterpolator(x1, x2, x3, x4, fracy); scaledHeight += noiseHeight * noiseMapScale; // Additional noise mask for small terrain features at ground level int noisex = dataForTask.mapPixel.mapPixelX * (dataForTask.HeightmapDimension - 1) + x; int noisey = (MapsFile.MaxMapPixelY - dataForTask.mapPixel.mapPixelY) * (dataForTask.HeightmapDimension - 1) + y; float lowFreq = TerrainHelper.GetNoise(noisex, noisey, 0.3f, 0.5f, 0.5f, 1); float highFreq = TerrainHelper.GetNoise(noisex, noisey, 0.9f, 0.5f, 0.5f, 1); scaledHeight += (lowFreq * highFreq) * extraNoiseScale; // Clamp lower values to ocean elevation if (scaledHeight < scaledOceanElevation) { scaledHeight = scaledOceanElevation; } // Set sample float height = Mathf.Clamp01(scaledHeight / dataForTask.MaxTerrainHeight); dataForTask.mapPixel.heightmapSamples[y, x] = height; } } _doneEvent.Set(); }
// Place a single terrain and mark it for update private void PlaceTerrain(int mapPixelX, int mapPixelY) { // Do nothing if out of range if (mapPixelX < MapsFile.MinMapPixelX || mapPixelX >= MapsFile.MaxMapPixelX || mapPixelY < MapsFile.MinMapPixelY || mapPixelY >= MapsFile.MaxMapPixelY) { return; } // Get terrain key int key = TerrainHelper.MakeTerrainKey(mapPixelX, mapPixelY); // If terrain is available if (terrainIndexDict.ContainsKey(key)) { // Terrain exists, check if active int index = terrainIndexDict[key]; if (terrainArray[index].active) { // Terrain already active in scene, nothing to do return; } else { // Terrain inactive but available, re-activate terrain terrainArray[index].active = true; terrainArray[index].terrainObject.SetActive(true); terrainArray[index].billboardBatchObject.SetActive(true); } return; } // Need to place a new terrain, find next available terrain // This will either find a fresh terrain or recycle an old one int nextTerrain = FindNextAvailableTerrain(); if (nextTerrain == -1) { return; } // Setup new terrain terrainArray[nextTerrain].active = true; terrainArray[nextTerrain].updateHeights = true; terrainArray[nextTerrain].updateNature = true; terrainArray[nextTerrain].mapPixelX = mapPixelX; terrainArray[nextTerrain].mapPixelY = mapPixelY; if (!terrainArray[nextTerrain].terrainObject) { // Create game objects for new terrain // This is only done once then recycled CreateTerrainGameObjects( mapPixelX, mapPixelY, out terrainArray[nextTerrain].terrainObject, out terrainArray[nextTerrain].billboardBatchObject); } // Apply local transform float scale = MapsFile.WorldMapTerrainDim * MeshReader.GlobalScale; int xdif = mapPixelX - mapOrigin.X; int ydif = mapPixelY - mapOrigin.Y; Vector3 localPosition = new Vector3(xdif * scale, 0, -ydif * scale); terrainArray[nextTerrain].terrainObject.transform.localPosition = localPosition; // Add new terrain index to dictionary terrainIndexDict.Add(key, nextTerrain); // Check if terrain has a location, if so it will be added on next update ContentReader.MapSummary mapSummary; if (dfUnity.ContentReader.HasLocation(mapPixelX, mapPixelY, out mapSummary)) { terrainArray[nextTerrain].hasLocation = true; } }
// Update terrain data // Spreads loading across several frames to reduce gameplay stalls // This can also be done using true multi-threading, but at much greater // complexity for only minor visible gains. // Only yields after initial init complete private IEnumerator UpdateTerrains() { // First stage updates terrain heightmaps for (int i = 0; i < terrainArray.Length; i++) { if (terrainArray[i].active && terrainArray[i].updateHeights) { UpdateTerrainHeights(terrainArray[i]); terrainArray[i].updateHeights = false; if (!init) { yield return(new WaitForEndOfFrame()); } } } // Wait for physics update when streaming if (!init) { yield return(new WaitForFixedUpdate()); } // Second stage updates terrain nature for (int i = 0; i < terrainArray.Length; i++) { if (terrainArray[i].active && terrainArray[i].updateNature) { UpdateTerrainNature(terrainArray[i]); terrainArray[i].updateNature = false; if (!init) { yield return(new WaitForEndOfFrame()); } } } // Get key for central player terrain int playerKey = TerrainHelper.MakeTerrainKey(MapPixelX, MapPixelY); // Third stage updates location if present // Vast majority of terrains will not have a location // Locations are not optimised as yet and are quite heavy on drawcalls for (int i = 0; i < terrainArray.Length; i++) { // Get key for this terrain int key = TerrainHelper.MakeTerrainKey(terrainArray[i].mapPixelX, terrainArray[i].mapPixelY); if (terrainArray[i].active && terrainArray[i].hasLocation) { // Create location if not present if (!locationDict.ContainsKey(key)) { // Create location object DFLocation location; GameObject locationObject = CreateLocationGameObject(i, out location); if (!locationObject) { continue; } // Add location object to dictionary locationDict.Add(key, locationObject); // Create location beacon // This is parented to location and shares its lifetime if (AddLocationBeacon) { const float beaconHeight = 900f; const float beaconOffset = (MapsFile.WorldMapTerrainDim * MeshReader.GlobalScale) / 2f; GameObject locationMarker = (GameObject)GameObject.Instantiate(Resources.Load <GameObject>("LocationBeacon")); locationMarker.hideFlags = HideFlags.HideAndDontSave; locationMarker.transform.parent = locationObject.transform; locationMarker.transform.localPosition = new Vector3(beaconOffset, beaconHeight, beaconOffset); } // Add one nature batch for entire location // This is parented to location and shares its lifetime GameObject natureBatchObject = new GameObject("NatureBatch"); natureBatchObject.hideFlags = HideFlags.HideAndDontSave; natureBatchObject.transform.parent = locationObject.transform; natureBatchObject.transform.localPosition = Vector3.zero; DaggerfallBillboardBatch natureBatch = natureBatchObject.AddComponent <DaggerfallBillboardBatch>(); int natureArchive = ClimateSwaps.GetNatureArchive(LocalPlayerGPS.ClimateSettings.NatureSet, dfUnity.WorldTime.Now.SeasonValue); natureBatch.SetMaterial(natureArchive); // RMB blocks are laid out in centre of terrain to align with ground int width = location.Exterior.ExteriorData.Width; int height = location.Exterior.ExteriorData.Height; float offsetX = ((8 * RMBLayout.RMBSide) - (width * RMBLayout.RMBSide)) / 2; float offsetZ = ((8 * RMBLayout.RMBSide) - (height * RMBLayout.RMBSide)) / 2; Vector3 origin = new Vector3(offsetX, 2.0f * MeshReader.GlobalScale, offsetZ); // Perform layout and yield after each block is placed DaggerfallLocation dfLocation = locationObject.GetComponent <DaggerfallLocation>(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Set origin for billboard batch add // This causes next additions to be offset by this position Vector3 blockOrigin = origin + new Vector3((x * RMBLayout.RMBSide), 0, (y * RMBLayout.RMBSide)); natureBatch.origin = blockOrigin; // Add block and yield string blockName = dfUnity.ContentReader.BlockFileReader.CheckName(dfUnity.ContentReader.MapFileReader.GetRmbBlockName(ref location, x, y)); GameObject go = RMBLayout.CreateGameObject(blockName, true, natureBatch); go.hideFlags = HideFlags.HideAndDontSave; go.transform.parent = locationObject.transform; go.transform.localPosition = blockOrigin; dfLocation.ApplyClimateSettings(); if (!init) { yield return(new WaitForEndOfFrame()); } } } // If this is the player terrain we may need to reposition player if (playerKey == key && repositionPlayer) { // Position to location and use start marker for large cities bool useStartMarker = (dfLocation.Summary.LocationType == DFRegion.LocationTypes.TownCity); PositionPlayerToLocation(MapPixelX, MapPixelY, dfLocation, origin, width, height, useStartMarker); repositionPlayer = false; } // Apply nature batch natureBatch.Apply(); } } else if (terrainArray[i].active) { if (playerKey == key && repositionPlayer) { PositionPlayerToTerrain(MapPixelX, MapPixelY, Vector3.zero); repositionPlayer = false; } } } // If this is an init we can use the load time to unload unused assets // Keeps memory usage much lower over time if (init) { Resources.UnloadUnusedAssets(); } // Finish by collecting stale data and setting neighbours CollectTerrains(); CollectLocations(); UpdateNeighbours(); }
public void Execute() { // Convert from rect in tilemap space to interior corners in 0-1 range float xMin = locationRect.xMin / MapsFile.WorldMapTileDim; float xMax = locationRect.xMax / MapsFile.WorldMapTileDim; float yMin = locationRect.yMin / MapsFile.WorldMapTileDim; float yMax = locationRect.yMax / MapsFile.WorldMapTileDim; // Scale values for converting blend space into 0-1 range float leftScale = 1 / xMin; float rightScale = 1 / (1 - xMax); float topScale = 1 / yMin; float bottomScale = 1 / (1 - yMax); // Flatten location area and blend with surrounding heights float strength = 0; float targetHeight = avgMaxHeight[avgHeightIdx]; for (int y = 0; y < hDim; y++) { float v = (float)y / (float)(hDim - 1); bool insideY = (v >= yMin && v <= yMax); for (int x = 0; x < hDim; x++) { float u = (float)x / (float)(hDim - 1); bool insideX = (u >= xMin && u <= xMax); if (insideX || insideY) { if (insideY && u <= xMin) { strength = u * leftScale; } else if (insideY && u >= xMax) { strength = (1 - u) * rightScale; } else if (insideX && v <= yMin) { strength = v * topScale; } else if (insideX && v >= yMax) { strength = (1 - v) * bottomScale; } } else { float xs = 0, ys = 0; if (u <= xMin) { xs = u * leftScale; } else if (u >= xMax) { xs = (1 - u) * rightScale; } if (v <= yMin) { ys = v * topScale; } else if (v >= yMax) { ys = (1 - v) * bottomScale; } strength = TerrainHelper.BilinearInterpolator(0, 0, 0, 1, xs, ys); } int idx = JobA.Idx(y, x, hDim); float height = heightmapData[idx]; if (insideX && insideY) { height = targetHeight; } else { height = Mathf.Lerp(height, targetHeight, strength); } heightmapData[idx] = height; } } }
// Set location tilemap data public static void SetLocationTiles(ref MapPixelData mapPixel) { // Get location DaggerfallUnity dfUnity = DaggerfallUnity.Instance; DFLocation location = dfUnity.ContentReader.MapFileReader.GetLocation(mapPixel.mapRegionIndex, mapPixel.mapLocationIndex); // Position tiles inside terrain area DFPosition tilePos = TerrainHelper.GetLocationTerrainTileOrigin(location); // Full 8x8 locations have "terrain blend space" around walls to smooth down random terrain towards flat area. // This is indicated by texture index > 55 (ground texture range is 0-55), larger values indicate blend space. // We need to know rect of actual city area so we can use blend space outside walls. int xmin = int.MaxValue, ymin = int.MaxValue; int xmax = 0, ymax = 0; // Iterate blocks of this location for (int blockY = 0; blockY < location.Exterior.ExteriorData.Height; blockY++) { for (int blockX = 0; blockX < location.Exterior.ExteriorData.Width; blockX++) { // Get block data DFBlock block; string blockName = dfUnity.ContentReader.MapFileReader.GetRmbBlockName(ref location, blockX, blockY); if (!dfUnity.ContentReader.GetBlock(blockName, out block)) { continue; } // Copy ground tile info for (int tileY = 0; tileY < RMBLayout.RMBTilesPerBlock; tileY++) { for (int tileX = 0; tileX < RMBLayout.RMBTilesPerBlock; tileX++) { DFBlock.RmbGroundTiles tile = block.RmbBlock.FldHeader.GroundData.GroundTiles[tileX, (RMBLayout.RMBTilesPerBlock - 1) - tileY]; int xpos = tilePos.X + blockX * RMBLayout.RMBTilesPerBlock + tileX; int ypos = tilePos.Y + blockY * RMBLayout.RMBTilesPerBlock + tileY; if (tile.TextureRecord < 56) { // Track interior bounds of location tiled area if (xpos < xmin) { xmin = xpos; } if (xpos > xmax) { xmax = xpos; } if (ypos < ymin) { ymin = ypos; } if (ypos > ymax) { ymax = ypos; } // Store texture data from block mapPixel.tilemapData[JobA.Idx(xpos, ypos, MapsFile.WorldMapTileDim)] = tile.TileBitfield == 0 ? byte.MaxValue : tile.TileBitfield; } } } } } // Update location rect with extra clearance int extraClearance = location.MapTableData.LocationType == DFRegion.LocationTypes.TownCity ? 3 : 2; Rect locationRect = new Rect(); locationRect.xMin = xmin - extraClearance; locationRect.xMax = xmax + extraClearance; locationRect.yMin = ymin - extraClearance; locationRect.yMax = ymax + extraClearance; mapPixel.locationRect = locationRect; }
// Set location tilemap data public static void SetLocationTiles(ref MapPixelData mapPixel) { //const int tileDim = 16; //const int chunkDim = 8; DaggerfallUnity dfUnity = DaggerfallUnity.Instance; // Get location DFLocation location = dfUnity.ContentReader.MapFileReader.GetLocation(mapPixel.mapRegionIndex, mapPixel.mapLocationIndex); // Centre location tiles inside terrain area //int startX = ((chunkDim * tileDim) - location.Exterior.ExteriorData.Width * tileDim) / 2; //int startY = ((chunkDim * tileDim) - location.Exterior.ExteriorData.Height * tileDim) / 2; // Position tiles inside terrain area int width = location.Exterior.ExteriorData.Width; int height = location.Exterior.ExteriorData.Height; DFPosition tilePos = TerrainHelper.GetLocationTerrainTileOrigin(width, height); // Full 8x8 locations have "terrain blend space" around walls to smooth down random terrain towards flat area. // This is indicated by texture index > 55 (ground texture range is 0-55), larger values indicate blend space. // We need to know rect of actual city area so we can use blend space outside walls. int xmin = int.MaxValue, ymin = int.MaxValue; int xmax = 0, ymax = 0; // Iterate blocks of this location for (int blockY = 0; blockY < location.Exterior.ExteriorData.Height; blockY++) { for (int blockX = 0; blockX < location.Exterior.ExteriorData.Width; blockX++) { // Get block data DFBlock block; string blockName = dfUnity.ContentReader.MapFileReader.GetRmbBlockName(ref location, blockX, blockY); if (!dfUnity.ContentReader.GetBlock(blockName, out block)) { continue; } // Copy ground tile info for (int tileY = 0; tileY < RMBLayout.RMBTilesPerBlock; tileY++) { for (int tileX = 0; tileX < RMBLayout.RMBTilesPerBlock; tileX++) { DFBlock.RmbGroundTiles tile = block.RmbBlock.FldHeader.GroundData.GroundTiles[tileX, (RMBLayout.RMBTilesPerBlock - 1) - tileY]; int xpos = tilePos.X + blockX * RMBLayout.RMBTilesPerBlock + tileX; int ypos = tilePos.Y + blockY * RMBLayout.RMBTilesPerBlock + tileY; int record = tile.TextureRecord; if (tile.TextureRecord < 56) { // Track interior bounds of location tiled area if (xpos < xmin) { xmin = xpos; } if (xpos > xmax) { xmax = xpos; } if (ypos < ymin) { ymin = ypos; } if (ypos > ymax) { ymax = ypos; } // Store texture data from block mapPixel.tilemapSamples[xpos, ypos].record = record; mapPixel.tilemapSamples[xpos, ypos].flip = tile.IsFlipped; mapPixel.tilemapSamples[xpos, ypos].rotate = tile.IsRotated; mapPixel.tilemapSamples[xpos, ypos].location = true; } } } } } // Update location rect with extra clearance const int extraClearance = 2; Rect locationRect = new Rect(); locationRect.xMin = xmin - extraClearance; locationRect.xMax = xmax + extraClearance; locationRect.yMin = ymin - extraClearance; locationRect.yMax = ymax + extraClearance; mapPixel.locationRect = locationRect; }
public override void GenerateSamples(ref MapPixelData mapPixel) { DaggerfallUnity dfUnity = DaggerfallUnity.Instance; // Create samples arrays mapPixel.tilemapSamples = new TilemapSample[MapsFile.WorldMapTileDim, MapsFile.WorldMapTileDim]; mapPixel.heightmapSamples = new float[HeightmapDimension, HeightmapDimension]; // Divisor ensures continuous 0-1 range of height samples float div = (float)(HeightmapDimension - 1) / 3f; // Read neighbouring height samples for this map pixel int mx = mapPixel.mapPixelX; int my = mapPixel.mapPixelY; byte[,] shm = dfUnity.ContentReader.WoodsFileReader.GetHeightMapValuesRange(mx - 2, my - 2, 4); byte[,] lhm = dfUnity.ContentReader.WoodsFileReader.GetLargeHeightMapValuesRange(mx - 1, my, 3); // Extract height samples for all chunks float averageHeight = 0; float maxHeight = float.MinValue; float baseHeight, noiseHeight; float x1, x2, x3, x4; int dim = HeightmapDimension; mapPixel.heightmapSamples = new float[dim, dim]; for (int y = 0; y < dim; y++) { for (int x = 0; x < dim; x++) { float rx = (float)x / div; float ry = (float)y / div; int ix = Mathf.FloorToInt(rx); int iy = Mathf.FloorToInt(ry); float sfracx = (float)x / (float)(dim - 1); float sfracy = (float)y / (float)(dim - 1); float fracx = (float)(x - ix * div) / div; float fracy = (float)(y - iy * div) / div; float scaledHeight = 0; // Bicubic sample small height map for base terrain elevation x1 = TerrainHelper.CubicInterpolator(shm[0, 3], shm[1, 3], shm[2, 3], shm[3, 3], sfracx); x2 = TerrainHelper.CubicInterpolator(shm[0, 2], shm[1, 2], shm[2, 2], shm[3, 2], sfracx); x3 = TerrainHelper.CubicInterpolator(shm[0, 1], shm[1, 1], shm[2, 1], shm[3, 1], sfracx); x4 = TerrainHelper.CubicInterpolator(shm[0, 0], shm[1, 0], shm[2, 0], shm[3, 0], sfracx); baseHeight = TerrainHelper.CubicInterpolator(x1, x2, x3, x4, sfracy); scaledHeight += baseHeight * baseHeightScale; // Bicubic sample large height map for noise mask over terrain features x1 = TerrainHelper.CubicInterpolator(lhm[ix, iy + 0], lhm[ix + 1, iy + 0], lhm[ix + 2, iy + 0], lhm[ix + 3, iy + 0], fracx); x2 = TerrainHelper.CubicInterpolator(lhm[ix, iy + 1], lhm[ix + 1, iy + 1], lhm[ix + 2, iy + 1], lhm[ix + 3, iy + 1], fracx); x3 = TerrainHelper.CubicInterpolator(lhm[ix, iy + 2], lhm[ix + 1, iy + 2], lhm[ix + 2, iy + 2], lhm[ix + 3, iy + 2], fracx); x4 = TerrainHelper.CubicInterpolator(lhm[ix, iy + 3], lhm[ix + 1, iy + 3], lhm[ix + 2, iy + 3], lhm[ix + 3, iy + 3], fracx); noiseHeight = TerrainHelper.CubicInterpolator(x1, x2, x3, x4, fracy); scaledHeight += noiseHeight * noiseMapScale; // Additional noise mask for small terrain features at ground level int noisex = mapPixel.mapPixelX * (HeightmapDimension - 1) + x; int noisey = (MapsFile.MaxMapPixelY - mapPixel.mapPixelY) * (HeightmapDimension - 1) + y; float lowFreq = TerrainHelper.GetNoise(noisex, noisey, 0.3f, 0.5f, 0.5f, 1); float highFreq = TerrainHelper.GetNoise(noisex, noisey, 0.9f, 0.5f, 0.5f, 1); scaledHeight += (lowFreq * highFreq) * extraNoiseScale; // Clamp lower values to ocean elevation if (scaledHeight < scaledOceanElevation) { scaledHeight = scaledOceanElevation; } // Set sample float height = Mathf.Clamp01(scaledHeight / MaxTerrainHeight); mapPixel.heightmapSamples[y, x] = height; // Accumulate averages and max height averageHeight += height; if (height > maxHeight) { maxHeight = height; } } } // Average and max heights are passed back for locations mapPixel.averageHeight = averageHeight /= (float)(dim * dim); mapPixel.maxHeight = maxHeight; }