/// <summary> /// Import a GeoTIFF file from disk into a Topography Layer /// </summary> /// <param name="landscape"></param> /// <param name="rawFileList"></param> /// <param name="isMacRAWFormat"></param> /// <param name="showErrors"></param> /// <returns></returns> public static bool ImportGeoTIFFHeightmap(LBLandscape landscape, string filePath, bool showErrors) { bool isSuccessful = false; string methodName = "LBImportTIFF.ImportGeoTIFFHeightmap"; if (landscape == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - landscape is null. Please report"); } } else if (landscape.topographyLayersList == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - topographyLayers is not defined. Please report"); } } else if (string.IsNullOrEmpty(filePath)) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - filePath is not defined. Please report."); } } else if (landscape.landscapeTerrains == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - landscapeTerrains is not defined. Please report"); } } else if (landscape.landscapeTerrains.Length < 1) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - No terrains in landscape"); } } else if (landscape.topographyLayersList.Exists(lyr => lyr.type == LBLayer.LayerType.UnityTerrains)) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - Multiple Unity Terrain Layers are not supported in the same landscape"); } } else { float generationStartTime = Time.realtimeSinceStartup; int numTerrains = landscape.landscapeTerrains.Length; List <LBTerrainData> lbTerrainDataList = new List <LBTerrainData>(); // Track the min/max heights for all the terrains ushort sampleMinHeight = ushort.MaxValue; ushort sampleMaxHeight = ushort.MinValue; for (int index = 0; index < numTerrains; index++) { //Debug.Log("INFO: " + methodName + " importing TIFF for terrain: " + landscape.landscapeTerrains[index].name + " using file: " + filePath); // Import TIFF region for the current terrain LBTerrainData lbTerrainData = ImportHeightmapTIFF(landscape, landscape.landscapeTerrains[index], filePath, ref sampleMinHeight, ref sampleMaxHeight, showErrors); // Don't continue if at least one terrain cannot be imported if (lbTerrainData == null) { break; } else { lbTerrainDataList.Add(lbTerrainData); } // Only create a new layer if all terrains were imported if (lbTerrainDataList.Count == numTerrains) { LBLayer lbLayer = new LBLayer(); if (lbLayer != null) { lbLayer.type = LBLayer.LayerType.UnityTerrains; lbLayer.lbTerrainDataList = lbTerrainDataList; lbLayer.isCheckHeightRangeDiff = true; lbLayer.heightScale = 1.0f; lbLayer.normaliseImage = false; // Insert at the top of the list landscape.topographyLayersList.Insert(0, lbLayer); } if (landscape.showTiming) { Debug.Log("Time taken to import TIFF heightmaps: " + (Time.realtimeSinceStartup - generationStartTime).ToString("00.00") + " seconds."); } isSuccessful = true; } } } return(isSuccessful); }
/// <summary> /// Fix or repair holes in GeoTIFF data. Typically used when GeoTIFF data /// has values > 65000 (which in metres is very unlikely) /// Also used with Include Below Sea Level with OpenTopo GMRT data. /// See notes in ImportHeightmapTIFF() under 32bit SampleFormat.IEEEFP. /// </summary> /// <param name="landscape"></param> /// <param name="lbLayer"></param> /// <param name="showErrors"></param> public static void FixGeoTIFFHeightmap(LBLandscape landscape, LBLayer lbLayer, bool showErrors) { string methodName = "LBImportTIFF.FixGeoTIFFHeightmap"; if (landscape == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - landscape is null. Please report"); } } else if (landscape.topographyLayersList == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - topographyLayers is not defined. Please report"); } } else if (lbLayer == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - lbLayer is not defined. Please report"); } } else if (lbLayer.type != LBLayer.LayerType.UnityTerrains) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - lbLayer is not of type: UnityTerrains. Please report"); } } else if (lbLayer.lbTerrainDataList == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - lbLayer.lbTerrainDataList is not defined. Please report"); } } else if (lbLayer.lbTerrainDataList.Count < 1) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - lbLayer.lbTerrainDataList does not contain any data. Please report"); } } { int numTerrainData = (lbLayer.lbTerrainDataList == null ? 0 : lbLayer.lbTerrainDataList.Count); LBTerrainData lbTerrainData = null; ushort upperClip = 65000; // GRMT data adjustment variables. New sea-level to be 10,000 ushort landCeiling = 65335 - 9999; // The maximum assumed height land values can be // We may need to use LBTerrainData.GetLandscapeScopedHeightmap() to fix holes // across terrain boundaries for (int lbTDataIdx = 0; lbTDataIdx < numTerrainData; lbTDataIdx++) { lbTerrainData = lbLayer.lbTerrainDataList[lbTDataIdx]; if (lbTerrainData != null && lbTerrainData.HasRAWHeightData()) { // Raw data is stored as a 1-dimensional USHORT byte array // To fix holes we int heightmapResolution = lbTerrainData.RawHeightResolution; int byteIndex, byteIndexNearby; ushort heightUShort, heightUShortNearby; ushort sampleMinValue = ushort.MaxValue; ushort sampleMaxValue = ushort.MinValue; for (int z = 0; z < heightmapResolution; z++) { for (int x = 0; x < heightmapResolution; x++) { byteIndex = z * (heightmapResolution * 2) + (x * 2); // LB stores RAW data in little endian (Windows) format, which is more suited to Intel processors. heightUShort = (ushort)((lbTerrainData.rawHeightData[byteIndex + 1] << 8) | lbTerrainData.rawHeightData[byteIndex]); // Global Multi-Resolution Topography (GMRT) data include +ve and -ve data. // As we only import into ushort, the -ve values become values close to 64K if (lbLayer.isBelowSeaLevelDataIncluded) { // WORKAROUD - z==0 and z = max have incorrect values. // Attempt to fix bottom and top edges with some GMRT datasets (not sure why this happens yet...) //if ((z == 0 || z == (heightmapResolution - 1)) && heightUShort == 0u) if ((z == 0) && heightUShort == 0) { ulong total = 0; int pixels = 0; ushort pixelAvg = 0; // Get matrix of nearby pixels for (int pz = -3; pz < 4; pz++) { // Ignore pixels we're trying to fix if (pz == 0) { continue; } // Ensure the pixel is within the data area if (z + pz < 0 || z + pz > heightmapResolution - 1) { continue; } for (int px = -3; px < 4; px++) { // Ignore original value and ensure the pixel is within the data area if ((pz == 0 && px == 0) || (x + px < 0 || x + px > heightmapResolution - 1)) { continue; } byteIndexNearby = (z + pz) * (heightmapResolution * 2) + ((x + px) * 2); heightUShortNearby = (ushort)((lbTerrainData.rawHeightData[byteIndexNearby + 1] << 8) | lbTerrainData.rawHeightData[byteIndexNearby]); // Only include pixels that are not being adjusted if (heightUShortNearby > 0) { pixels++; total += heightUShortNearby; } } } if (pixels > 0) { pixelAvg = Convert.ToUInt16((total / (ulong)pixels)); if (pixelAvg > 0) { heightUShort = pixelAvg; } } } // Raise land values by 10,000 if (heightUShort < landCeiling) { heightUShort += 10000; } else { // The -ve (below sea level) values have become height +ve numbers during the import // process. Move these original -ve number back under the new artifical sea level. heightUShort = (ushort)(10000u - (65535u - (uint)heightUShort)); } // Convert ushort 0-65535 back to 2-bytes // LB stores RAW data in little endian (Windows) format, which is more suited to Intel processors. lbTerrainData.rawHeightData[byteIndex] = System.Convert.ToByte(heightUShort & 255); lbTerrainData.rawHeightData[byteIndex + 1] = System.Convert.ToByte(heightUShort >> 8); // keep track of the min/max values in the input TIFF data if (heightUShort > sampleMaxValue) { sampleMaxValue = heightUShort; } if (heightUShort < sampleMinValue) { sampleMinValue = heightUShort; } } else { // Is this an extreme value? if (heightUShort >= upperClip) { ulong total = 0; int pixels = 0; ushort pixelAvg = 0; // Get matrix of nearby pixels for (int pz = -3; pz < 4; pz++) { // Ensure the pixel is within the data area if (z + pz < 0 || z + pz > heightmapResolution - 1) { continue; } for (int px = -3; px < 4; px++) { // Ignore original value and ensure the pixel is within the data area if ((pz == 0 && px == 0) || (x + px < 0 || x + px > heightmapResolution - 1)) { continue; } // Ensure the pixel is within the data area //if (x + px < 0 || x + px > heightmapResolution - 1) { continue; } byteIndexNearby = (z + pz) * (heightmapResolution * 2) + ((x + px) * 2); heightUShortNearby = (ushort)((lbTerrainData.rawHeightData[byteIndexNearby + 1] << 8) | lbTerrainData.rawHeightData[byteIndexNearby]); // Only include pixels that are not being clipped. Adjacent pixels may also need to be clipped, so we need // to not include them for averaging purposes, else the new pixel updated below will be much higher than the surrounding terrain. if (heightUShortNearby < upperClip) { pixels++; total += heightUShortNearby; } } } if (pixels > 0) { pixelAvg = Convert.ToUInt16((total / (ulong)pixels)); if (pixelAvg > 0) { // Convert ushort 0-65535 back to 2-bytes // LB stores RAW data in little endian (Windows) format, which is more suited to Intel processors. lbTerrainData.rawHeightData[byteIndex] = System.Convert.ToByte(pixelAvg & 255); lbTerrainData.rawHeightData[byteIndex + 1] = System.Convert.ToByte(pixelAvg >> 8); //Debug.Log("x,z " + x + "," + z + " avg: " + pixelAvg); } } else { // keep track of the min/max values in the input TIFF data if (heightUShort > sampleMaxValue) { sampleMaxValue = heightUShort; } if (heightUShort < sampleMinValue) { sampleMinValue = heightUShort; } } } else // Value is not extreme so update min/max values { // keep track of the min/max values in the input TIFF data if (heightUShort > sampleMaxValue) { sampleMaxValue = heightUShort; } if (heightUShort < sampleMinValue) { sampleMinValue = heightUShort; } } } } } // Need to update max height lbTerrainData.rawMinHeight = sampleMinValue; lbTerrainData.rawMaxHeight = sampleMaxValue; } } } }
/// <summary> /// Load a GeoTIFF file from disk into rawHeightData for the given terrain. /// Currently we load the whole TIFF file and find the pixels inside the file /// for each pixel in the terrain. Potentially it would be more efficient to just /// scan the data that applies to the current terrain, howbeit more complex. /// </summary> /// <param name="landscape"></param> /// <param name="terrain"></param> /// <param name="filePath"></param> /// <param name="minHeight"></param> /// <param name="maxHeight"></param> /// <param name="showErrors"></param> /// <returns></returns> public static LBTerrainData ImportHeightmapTIFF(LBLandscape landscape, Terrain terrain, string filePath, ref ushort minHeight, ref ushort maxHeight, bool showErrors) { LBTerrainData lbTerrainData = null; string methodName = "LBImportTIFF.ImportHeightmapTIFF"; if (landscape == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - landscape is null"); } } else if (terrain == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - terrain is null in " + landscape.name); } } else if (terrain.terrainData == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - terrain.terrainData is null for " + landscape.name + "." + terrain.name); } } else if (string.IsNullOrEmpty(filePath)) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - RAW file is not available for " + landscape.name + "." + terrain.name); } } else if (!System.IO.File.Exists(filePath)) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - RAW file does not exist: " + filePath); } } { #if LBIMPORT_TIFF_DEBUG_MODE Debug.Log("INFO: " + methodName + " importing TIFF for terrain: " + terrain.name + " using file: " + filePath); #endif Tiff tiff = null; try { // open the geotiff file in read-only mode (the only other option is write mode - the library does not support rw mode) tiff = Tiff.Open(filePath, "r"); if (tiff == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " could not open TIFF file: " + filePath); } } else if (tiff.IsTiled()) { if (showErrors) { Debug.LogWarning("INFO: " + methodName + " Landscape Builder currently does not support TIFF files that contain tiles"); } } else if (tiff.NumberOfDirectories() < (short)1) { if (showErrors) { Debug.LogWarning("INFO: " + methodName + " the TIFF file does not appear to contain any image data."); } } else { // We assume there is only 1 "directory" within the TIFF (as they can support multiple images or directories within a single file) // Just get the first "directory" by using [0]. int bitsPerSample = tiff.GetField(TiffTag.BITSPERSAMPLE)[0].ToInt(); int imgWidth = tiff.GetField(TiffTag.IMAGEWIDTH)[0].ToInt(); int imgLength = tiff.GetField(TiffTag.IMAGELENGTH)[0].ToInt(); int scanlineSize = tiff.ScanlineSize(); int sampleFormat = tiff.GetFieldDefaulted(TiffTag.SAMPLEFORMAT)[0].ToInt(); int samplesPerPixel = tiff.GetFieldDefaulted(TiffTag.SAMPLESPERPIXEL)[0].ToInt(); #if LBIMPORT_TIFF_DEBUG_MODE int numTiles = tiff.GetField(TiffTag.TILEBYTECOUNTS).Length; int rowsPerStrip = tiff.GetField(TiffTag.ROWSPERSTRIP)[0].ToInt(); int imgCompression = tiff.GetField(TiffTag.COMPRESSION)[0].ToInt(); Debug.Log("INFO: " + methodName + " samplesPerPixel:" + samplesPerPixel + " bitsPerSample:" + bitsPerSample + " imgWidth:" + imgWidth + " imgLength:" + imgLength + " Num Tiles: " + numTiles + " rowsPerStrip:" + rowsPerStrip + " imgCompression: " + Enum.GetName(typeof(Compression), imgCompression) + " SampleFormat: " + Enum.GetName(typeof(SampleFormat), sampleFormat)); #endif // NOTE: Orientation seems to be obsolete and almost always returns TOPLEFT if (imgWidth < 1 || imgLength < 1) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - the TIFF image size is invalid (" + imgWidth + "," + imgLength + ") in " + filePath); } } else if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 32) { if (showErrors) { Debug.LogWarning("INFO: " + methodName + " - Landscape Builder currently only supports 8, 16 or 32bit TIFF files"); } } else if (samplesPerPixel != 1) { if (showErrors) { Debug.LogWarning("INFO: " + methodName + " - Landscape Builder currently only supports GeoTIFF files with 1 sample per pixel. Try using a Image Layer for RGB or RGBA TIFF files."); } } else if (scanlineSize == 0) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " the line size in the TIFF file is zero for file: " + filePath); } } else if (scanlineSize != imgWidth * (bitsPerSample / 8)) { if (showErrors) { Debug.LogWarning("INFO: " + methodName + " the TIFF data scan line size (" + scanlineSize + ") does not seem to match the width and bits per sample: " + imgWidth + "pixels * (" + bitsPerSample + "bits/8)"); } } else { byte[] scanline = new byte[scanlineSize]; if (scanline == null) { if (showErrors) { Debug.LogWarning("INFO: " + methodName + " could not reserve memory to scan TIFF data lines."); } } else { // Attempt to allocate enough space for the whole image int tiffImageBufferSize = imgWidth * imgLength * (bitsPerSample / 8); byte[] tiffImageBuffer = new byte[tiffImageBufferSize]; if (tiffImageBuffer == null) { if (showErrors) { Debug.LogWarning("INFO: " + methodName + " could not reserve memory to hold the TIFF data."); } } else { int tiffImageOffset = 0; // There should be one line for each pixel the TIFF image is in length. for (int sl = 0; sl < imgLength; sl++) { if (tiff.ReadScanline(scanline, sl)) { // Copy the line into the image buffer Buffer.BlockCopy(scanline, 0, tiffImageBuffer, tiffImageOffset, scanlineSize); tiffImageOffset += scanlineSize; } else { if (showErrors) { Debug.LogWarning("INFO: " + methodName + " ReadScanline failed at offset " + tiffImageOffset); } break; } } // Make sure we read in all the data from the first image in the TIFF file if ((tiffImageOffset / scanlineSize) == imgLength) { lbTerrainData = new LBTerrainData(); if (lbTerrainData == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - could not create LBTerrainData instance for " + landscape.name + "." + terrain.name); } } else { TerrainData tData = terrain.terrainData; int heightmapResolution = tData.heightmapResolution; // terrainData name and the Terrain object name may be different if created outside LB // We want the terrainData name, as that is what will be used in LBLandscapeTerrain.HeightmapFromLayers(..). lbTerrainData.sourceTerrainName = terrain.name; lbTerrainData.sourceTerrainDataName = tData.name; lbTerrainData.rawHeightData = null; lbTerrainData.dataSourceName = System.IO.Path.GetFileName(filePath); lbTerrainData.rawSourceWidth = imgWidth; lbTerrainData.rawSourceLength = imgLength; // RAW data is stored as 16-bit, consisting of a little and big endian (8bit + 8bit) lbTerrainData.rawHeightData = new byte[heightmapResolution * heightmapResolution * 2]; byte[] rawHeightDataPixel = new byte[2]; if (lbTerrainData.rawHeightData == null) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " - could not create rawHeightData array for " + landscape.name + "." + terrain.name); } } else { int rawHeightDataLength = lbTerrainData.rawHeightData.Length; // Assume GeoTIFF is in Little Endian (Windows) format. THIS MIGHT BE WRONG... bool isMac = false; int xHeightmap = 0, zHeightmap = 0; float xPos = 0f, zPos = 0f, xPosN = 0f, zPosN = 0f; Vector3 terrainWorldPosition = terrain.transform.position; Vector3 landscapePosition = landscape.gameObject.transform.position; Vector3 heightmapScale = tData.heightmapScale; Vector2 imgCoords = Vector2.zero; ushort sample16bit = 0; int sample32bitInt = 0; float sample32bitF = 0f; ushort sampleMinValue = ushort.MaxValue; ushort sampleMaxValue = ushort.MinValue; // LBTerrainData.rawHeightData is stored as 16bit (2 x 8bit) // We need to convert our TIFF data into this format for (int byteIndex = 0; byteIndex < rawHeightDataLength - 1; byteIndex += 2) { // Get the point in the heightmap (assume the rawHeightData matches the terrain heightmap resolution) // There are twice as many samples in the rawHeightData (16bit) as in the terrain heightmap (single float per sample) zHeightmap = (int)((byteIndex / 2) / heightmapResolution); xHeightmap = (byteIndex / 2) % heightmapResolution; // Get world position of heightmap point xPos = (heightmapScale.x * xHeightmap) + terrainWorldPosition.x - landscapePosition.x; zPos = (heightmapScale.z * zHeightmap) + terrainWorldPosition.z - landscapePosition.z; // Get normalised position in landscape (0.0-1.0) xPosN = xPos / landscape.size.x; zPosN = zPos / landscape.size.y; // Get the position in the TIFF data // z-axis is flipped because TIFF image typically is 0,0 is topleft, but heightmap data has 0,0 at bottomleft. imgCoords = new Vector2(xPosN * (imgWidth - 1), (1f - zPosN) * (imgLength - 1)); // Get the position in the imported TIFF buffer tiffImageOffset = ((int)imgCoords.y * scanlineSize) + (int)imgCoords.x * (bitsPerSample / 8); // Reset destination pixel data rawHeightDataPixel[0] = 0; rawHeightDataPixel[1] = 0; // Convert each pixel to a 16bit sample if (bitsPerSample == 8) { // upsize the sample to 16 bits sample16bit = (ushort)(tiffImageBuffer[tiffImageOffset] * 255); } else if (bitsPerSample == 16) { // This has been validated against the VTBuild application sample16bit = (ushort)(tiffImageBuffer[tiffImageOffset + 1] << 8 | tiffImageBuffer[tiffImageOffset]); //rawHeightDataPixel[0] = tiffImageBuffer[tiffImageOffset]; //rawHeightDataPixel[1] = tiffImageBuffer[tiffImageOffset+1]; } else if (bitsPerSample == 32) { if ((SampleFormat)sampleFormat == SampleFormat.IEEEFP) { sample32bitF = BitConverter.ToSingle(tiffImageBuffer, tiffImageOffset); // Convert to float (single), then cast to ushort. The float data seems to be heights in metres, rather than 0-1 as expected. //sample16bit = (ushort)(BitConverter.ToSingle(tiffImageBuffer, tiffImageOffset)); // GMRT data includes -ve values (below sea level) and +ve values. // -ve values are offset from 64K then can be fixed in editor by ticking Include Below Sealevel and clicking Fix button. if (sample32bitF < 0f) { sample16bit = (ushort)(65535f + sample32bitF); } else { sample16bit = (ushort)sample32bitF; } } else { // Assume INT but could be UINT... sample32bitInt = tiffImageBuffer[tiffImageOffset + 3] << 24 | tiffImageBuffer[tiffImageOffset + 2] << 16 | tiffImageBuffer[tiffImageOffset + 1] << 8 | tiffImageBuffer[tiffImageOffset]; // Downsize to 16 bit - assume values 0 -> (2^32 -1). Convert to 1 -> (2^16 - 1) sample16bit = (ushort)(((sample32bitInt + 1) / 65536) - 1); } } // keep track of the min/max values in the input TIFF data if (sample16bit > sampleMaxValue) { sampleMaxValue = sample16bit; } if (sample16bit < sampleMinValue) { sampleMinValue = sample16bit; } rawHeightDataPixel[0] = System.Convert.ToByte(sample16bit & 255); rawHeightDataPixel[1] = System.Convert.ToByte(sample16bit >> 8); // LB stores RAW data in little endian (Windows) format, which is more suited to Intel processors. if (isMac) { // Mac: big endian lbTerrainData.rawHeightData[byteIndex + 1] = rawHeightDataPixel[0]; lbTerrainData.rawHeightData[byteIndex] = rawHeightDataPixel[1]; } else { // Windows: little endian lbTerrainData.rawHeightData[byteIndex] = rawHeightDataPixel[0]; lbTerrainData.rawHeightData[byteIndex + 1] = rawHeightDataPixel[1]; } } if (sampleMinValue < minHeight) { minHeight = sampleMinValue; } if (sampleMaxValue > maxHeight) { maxHeight = sampleMaxValue; } if (sampleMinValue == ushort.MaxValue) { sampleMinValue = ushort.MinValue; } lbTerrainData.rawMinHeight = sampleMinValue; lbTerrainData.rawMaxHeight = sampleMaxValue; #if LBIMPORT_TIFF_DEBUG_MODE //Debug.Log("INFO: " + methodName + " TIFF min: " + (sampleMinValue < ushort.MaxValue ? sampleMinValue : (ushort)0).ToString() + " max: " + (sampleMaxValue > ushort.MinValue ? sampleMaxValue : (ushort)0).ToString() + " for " + terrain.name); #endif } } } tiffImageBuffer = null; } scanline = null; } } } } catch (Exception ex) { if (showErrors) { Debug.LogWarning("ERROR: " + methodName + " could not process TIFF file. " + ex.Message); } lbTerrainData = null; } finally { // Clean up. if (tiff != null) { tiff.Close(); tiff.Dispose(); tiff = null; } } } return(lbTerrainData); }