//#define DEBUG_TILES using UnityEngine; using System; using System.Linq; using System.Text; #if UNITY_WSA && !UNITY_EDITOR using System.Threading.Tasks; #else using System.Threading; #endif using System.IO; using System.Collections; using System.Collections.Generic; namespace WPM { public partial class WorldMapGlobe : MonoBehaviour { class ZoomLevelInfo { public int xMax, yMax; public GameObject tilesContainer; public int zoomLevelHash, yHash; } /// /// This is the minimum zoom level for tiles download. TILE_MIN_ZOOM_LEVEL must stay at 5. A lower value produces tiles that are too big to adapt to the sphere shape resulting in intersection artifacts with higher zoom levels. /// public const int TILE_MIN_ZOOM_LEVEL = 5; /// /// TILE_MAX_ZOOM_LEVEL can be increased if needed /// public const int TILE_MAX_ZOOM_LEVEL = 19; const int TILE_MIN_SIZE = 256; const string PREFIX_MIN_ZOOM_LEVEL = "z5_"; const float TILE_MAX_QUEUE_TIME = 10; const string TILES_ROOT = "Tiles"; readonly int[] tileIndices = { 2, 3, 0, 2, 0, 1 }; readonly Vector2[] tileUV = { Misc.Vector2up, Misc.Vector2one, Misc.Vector2right, Misc.Vector2zero }; readonly Color[][] meshColors = { new Color[] { new Color (1, 0, 0, 0), new Color (1, 0, 0, 0), new Color (1, 0, 0, 0), new Color (1, 0, 0, 0) }, new Color[] { new Color (0, 1, 0, 0), new Color (0, 1, 0, 0), new Color (0, 1, 0, 0), new Color (0, 1, 0, 0) }, new Color[] { new Color (0, 0, 1, 0), new Color (0, 0, 1, 0), new Color (0, 0, 1, 0), new Color (0, 0, 1, 0) }, new Color[] { new Color (0, 0, 0, 1), new Color (0, 0, 0, 1), new Color (0, 0, 0, 1), new Color (0, 0, 0, 1) } }; readonly Vector2[] offsets = { new Vector2 (0, 0), new Vector2 (1f, 0), new Vector2 (0, 1f), new Vector2 (1f, 1f) }; readonly Vector4[] placeHolderUV = { new Vector4 (0, 0.5f, 0.5f, 1f), new Vector4 (0.5f, 0.5f, 1f, 1f), new Vector4 (0, 0f, 0.5f, 0.5f), new Vector4 (0.5f, 0, 1f, 0.5f) }; Color color1000 = new Color(1, 0, 0, 0); int _concurrentLoads; int _currentZoomLevel; int _webDownloads, _cacheLoads, _resourceLoads; long _webDownloadTotalSize, _cacheLoadTotalSize; int _tileSize = 0; string _tileServerCopyrightNotice; string _tileLastError; DateTime _tileLastErrorDate; Dictionary cachedTiles; readonly ZoomLevelInfo[] zoomLevelsInfo = new ZoomLevelInfo[TILE_MAX_ZOOM_LEVEL + 1]; List loadQueue, newQueue; List inactiveTiles; bool shouldCheckTiles, resortLoadQueue; Material tileMatRef, tileMatTransRef; Transform tilesRoot; Renderer northPoleObj, southPoleObj; int subserverSeq; long _tileCurrentCacheUsage; FileInfo[] cachedFiles; float currentTileSize; Plane[] cameraPlanes; float lastDisposalTime; Texture2D currentEarthTexture; int spreadLoadAmongFrames; Camera currentCamera; Vector3 currentCameraPosition, currentCameraForward, globePos; string cachePath; float localScaleFactor; bool firstLoad, requestPaintMainTiles; Vector2 currentLatLon; bool purgedThisSession; /// /// List of tiles currently visible when map is in Tile System Mode /// [NonSerialized] public List visibleTiles = new List(); void InitTileSystem() { _tileServerCopyrightNotice = GetTileServerCopyrightNotice(_tileServer); cachePath = Application.persistentDataPath + "/TilesCache"; if (!Directory.Exists(cachePath)) { Directory.CreateDirectory(cachePath); } if (!Application.isPlaying) return; if (_tileTransparentLayer) { tileMatRef = Resources.Load("Materials/TileOverlayAlpha") as Material; tileMatTransRef = Resources.Load("Materials/TileOverlayTransAlpha") as Material; Color alpha = new Color(1f, 1f, 1f, _tileMaxAlpha); tileMatRef.color = alpha; tileMatTransRef.color = alpha; } else { tileMatRef = Resources.Load("Materials/TileOverlay") as Material; tileMatTransRef = Resources.Load("Materials/TileOverlayTrans") as Material; } cameraPlanes = new Plane[6]; if (_earthRenderer != null && _earthRenderer.sharedMaterial != null) { currentEarthTexture = (Texture2D)_earthRenderer.sharedMaterial.mainTexture; } else { currentEarthTexture = Texture2D.whiteTexture; } _tileSize = 0; InitZoomLevels(); if (loadQueue != null) { loadQueue.Clear(); } else { loadQueue = new List(); } if (inactiveTiles != null) { inactiveTiles.Clear(); } else { inactiveTiles = new List(); } if (cachedTiles != null) { cachedTiles.Clear(); } else { cachedTiles = new Dictionary(); } if (Application.isPlaying && !purgedThisSession) { purgedThisSession = true; PurgeCacheOldFiles(); } if (tilesRoot == null) { tilesRoot = transform.Find(TILES_ROOT); } if (tilesRoot != null) DestroyImmediate(tilesRoot.gameObject); if (tilesRoot == null) { GameObject tilesRootObj = new GameObject(TILES_ROOT); tilesRoot = tilesRootObj.transform; tilesRoot.SetParent(transform, false); } shouldCheckTiles = true; firstLoad = true; } void DestroyTiles() { if (tilesRoot != null) DestroyImmediate(tilesRoot.gameObject); if (northPoleObj != null) { DestroyImmediate(northPoleObj); northPoleObj = null; } if (southPoleObj != null) { DestroyImmediate(southPoleObj); southPoleObj = null; } if (cachedTiles != null) { foreach (KeyValuePair kvp in cachedTiles) { TileInfo ti = kvp.Value; if (ti != null && ti.texture != null && ti.source != TILE_SOURCE.Resources && ti.source != TILE_SOURCE.Unknown && ti.texture != currentEarthTexture) { DestroyImmediate(ti.texture); ti.texture = null; } } cachedTiles.Clear(); } } /// /// Reloads tiles /// public void ResetTiles() { DestroyTiles(); InitTileSystem(); } void PurgeCacheOldFiles() { PurgeCacheOldFiles(_tileMaxLocalCacheSize); } public void TileRecalculateCacheUsage() { _tileCurrentCacheUsage = 0; if (!Directory.Exists(cachePath)) return; DirectoryInfo dir = new DirectoryInfo(cachePath); cachedFiles = dir.GetFiles().OrderBy(p => p.LastAccessTime).ToArray(); for (int k = 0; k < cachedFiles.Length; k++) { _tileCurrentCacheUsage += cachedFiles[k].Length; } } /// /// Purges the cache old files. /// /// Max size is in Mb. void PurgeCacheOldFiles(long maxSize) { _tileCurrentCacheUsage = 0; if (!Directory.Exists(cachePath)) return; DirectoryInfo dir = new DirectoryInfo(cachePath); // Delete old jpg files FileInfo[] jpgs = dir.GetFiles("*.jpg.*"); for (int k = 0; k < jpgs.Length; k++) { jpgs[k].Delete(); } cachedFiles = dir.GetFiles().OrderBy(p => p.LastAccessTime).ToArray(); maxSize *= 1024 * 1024; for (int k = 0; k < cachedFiles.Length; k++) { _tileCurrentCacheUsage += cachedFiles[k].Length; } if (_tileCurrentCacheUsage <= maxSize) return; // Purge files until total size gets under max cache size for (int k = 0; k < cachedFiles.Length; k++) { if (_tilePreloadTiles && cachedFiles[k].Name.StartsWith(PREFIX_MIN_ZOOM_LEVEL)) { continue; } _tileCurrentCacheUsage -= cachedFiles[k].Length; cachedFiles[k].Delete(); if (_tileCurrentCacheUsage <= maxSize) return; } } void InitZoomLevels() { for (int k = 0; k < zoomLevelsInfo.Length; k++) { ZoomLevelInfo zi = new ZoomLevelInfo(); zi.xMax = (int)Mathf.Pow(2, k); zi.yMax = zi.xMax; zi.zoomLevelHash = (int)Mathf.Pow(4, k); zi.yHash = (int)Mathf.Pow(2, k); zoomLevelsInfo[k] = zi; } } void LateUpdateTiles() { if (!Application.isPlaying || cachedTiles == null) return; if (_tilesUnloadInactiveTiles && Time.time - lastDisposalTime > 3) { lastDisposalTime = Time.time; MonitorInactiveTiles(); } if (shouldCheckTiles || flyToActive) { shouldCheckTiles = false; currentCamera = mainCamera; // for optimization purposes currentCameraPosition = currentCamera.transform.position; currentCameraForward = currentCamera.transform.forward; globePos = transform.position; currentLatLon = Conversion.GetLatLonFromUnitSpherePoint(GetCurrentMapLocation()); _currentZoomLevel = GetCenterTileZoomLevel(); ToggleFrontiers(); int startingZoomLevel = TILE_MIN_ZOOM_LEVEL - 1; ZoomLevelInfo zi = zoomLevelsInfo[startingZoomLevel]; int currentLoadQueueSize = loadQueue.Count; for (int k = 0; k < currentLoadQueueSize; k++) { loadQueue[k].visible = false; } GeometryUtility.CalculateFrustumPlanes(currentCamera.projectionMatrix * currentCamera.worldToCameraMatrix, cameraPlanes); visibleTiles.Clear(); localScaleFactor = transform.localScale.x * 0.01f; for (int k = 0; k < zi.xMax; k++) { for (int j = 0; j < zi.yMax; j++) { CheckTiles(null, _currentZoomLevel, k, j, startingZoomLevel, 0); } } if (OnTileRecomputed != null) { OnTileRecomputed(visibleTiles); } int newQueueCount = loadQueue.Count; if (currentLoadQueueSize != newQueueCount) { resortLoadQueue = true; for (int k = 0; k < newQueueCount; k++) { TileInfo ti = loadQueue[k]; Vector3 midPos; midPos.x = (ti.cornerWorldPos[0].x + ti.cornerWorldPos[3].x) * 0.5f; midPos.y = (ti.cornerWorldPos[0].y + ti.cornerWorldPos[3].y) * 0.5f; midPos.z = (ti.cornerWorldPos[0].z + ti.cornerWorldPos[3].z) * 0.5f; ti.distToCamera = FastVector.SqrDistance(ref midPos, ref currentCameraPosition) * ti.zoomLevel; if (!ti.visible) ti.distToCamera += 1e20f; } } if (resortLoadQueue) { resortLoadQueue = false; loadQueue.Sort((TileInfo x, TileInfo y) => { if (x.distToCamera < y.distToCamera) return -1; else if (x.distToCamera > y.distToCamera) return 1; else return 0; }); } // Ensure local cache max size is not exceeded long maxLocalCacheSize = _tileMaxLocalCacheSize * 1024 * 1024; if (cachedFiles != null && _tileCurrentCacheUsage > maxLocalCacheSize) { for (int f = 0; f < cachedFiles.Length; f++) { if (cachedFiles[f] != null && cachedFiles[f].Exists) { if (_tilePreloadTiles && cachedFiles[f].Name.StartsWith(PREFIX_MIN_ZOOM_LEVEL)) { continue; } _tileCurrentCacheUsage -= cachedFiles[f].Length; cachedFiles[f].Delete(); } if (_tileCurrentCacheUsage <= maxLocalCacheSize) break; } } } requestPaintMainTiles = false; CheckTilesContent(); spreadLoadAmongFrames = _tileMaxTileLoadsPerFrame; if (requestPaintMainTiles && firstLoad) { firstLoad = false; LateUpdateTiles(); } if (_tilePreciseRotation) { if (_currentZoomLevel > _tilePreciseRotationZoomLevel && _navigationMode == NAVIGATION_MODE.EARTH_ROTATES) { _navigationMode = NAVIGATION_MODE.CAMERA_ROTATES; } else if (_currentZoomLevel <= _tilePreciseRotationZoomLevel && _navigationMode == NAVIGATION_MODE.CAMERA_ROTATES) { _navigationMode = NAVIGATION_MODE.EARTH_ROTATES; } } } void ToggleFrontiers() { // Check if frontiers/provinces can be shown if (_showFrontiers) { if (_currentZoomLevel < _tileMaxZoomLevelFrontiers) { if (!frontiersLayer.activeSelf) { frontiersLayer.SetActive(true); } } else if (_currentZoomLevel > _tileMaxZoomLevelFrontiers) { if (frontiersLayer.activeSelf) { frontiersLayer.SetActive(false); } } } if (_showInlandFrontiers) { if (_currentZoomLevel < _tileMaxZoomLevelFrontiers) { if (!inlandFrontiersLayer.activeSelf) { inlandFrontiersLayer.SetActive(true); } } else if (_currentZoomLevel > _tileMaxZoomLevelFrontiers) { if (inlandFrontiersLayer.activeSelf) { inlandFrontiersLayer.SetActive(false); } } } } void MonitorInactiveTiles() { int inactiveCount = inactiveTiles.Count; bool changes = false; bool releasedMemory = false; for (int k = 0; k < inactiveCount; k++) { TileInfo ti = inactiveTiles[k]; if (ti == null) { changes = true; continue; } if (ti.gameObject == null || ti.visible || ti.texture == currentEarthTexture || ti.loadStatus != TILE_LOAD_STATUS.Loaded) { inactiveTiles[k] = null; ti.isAddedToInactive = false; changes = true; continue; } if (Time.time - ti.inactiveTime > _tileKeepAlive) { bool relatedTileIsVisible = false; // if a children is visible, do not unload this tile if (ti.children != null) { int cCount = ti.children.Count; for (int c = 0; c < cCount; c++) { TileInfo tiChild = ti.children[c]; if (tiChild.visible) { relatedTileIsVisible = true; break; } } if (relatedTileIsVisible) continue; } // if a parent is visible, do not unload this tile TileInfo tiParent = ti.parent; while (tiParent != null) { if (tiParent.visible) { relatedTileIsVisible = true; break; } tiParent = tiParent.parent; } if (relatedTileIsVisible) continue; // Proceed to unload tile inactiveTiles[k] = null; ti.isAddedToInactive = false; ti.loadStatus = TILE_LOAD_STATUS.Inactive; if (ti.source != TILE_SOURCE.Resources && ti.source != TILE_SOURCE.Unknown && ti.texture != currentEarthTexture) { DestroyImmediate(ti.texture); releasedMemory = true; } // tile is now invisible, setup material for when it appears again: ti.Reset(); ResolvePlaceholderImage(ti.parent); // Reset parentcoords on children if (ti.children != null) { ResolvePlaceholderImage(ti); } changes = true; } } if (changes) { shouldCheckTiles = true; List newInactiveList = new List(); for (int k = 0; k < inactiveCount; k++) { if (inactiveTiles[k] != null) newInactiveList.Add(inactiveTiles[k]); } inactiveTiles.Clear(); inactiveTiles = newInactiveList; if (releasedMemory) { Resources.UnloadUnusedAssets(); GC.Collect(); } } } void CheckTilesContent() { int qCount = loadQueue.Count; bool cleanQueue = false; for (int k = 0; k < qCount; k++) { TileInfo ti = loadQueue[k]; if (ti == null) { cleanQueue = true; continue; } if (ti.loadStatus == TILE_LOAD_STATUS.InQueue) { if (ti.loadDelay > 0) { ti.stage = 10; ti.loadDelay--; continue; } if (ti.visible) { if (_tilePreloadTiles && ti.zoomLevel == TILE_MIN_ZOOM_LEVEL && ReloadTextureFromCacheOrMarkForDownload(ti)) { loadQueue[k] = null; cleanQueue = true; requestPaintMainTiles = true; continue; } if (_concurrentLoads <= _tileMaxConcurrentDownloads) { ti.loadStatus = TILE_LOAD_STATUS.Loading; ti.stage = 20; _concurrentLoads++; StartCoroutine(LoadTileContentBackground(ti)); } } else if (Time.time - ti.queueTime > TILE_MAX_QUEUE_TIME) { ti.loadStatus = TILE_LOAD_STATUS.Inactive; loadQueue[k] = null; cleanQueue = true; } } } if (cleanQueue) { if (newQueue == null) { newQueue = new List(qCount); } else { newQueue.Clear(); } for (int k = 0; k < qCount; k++) { TileInfo ti = loadQueue[k]; if (ti != null) { newQueue.Add(ti); } } List tmp = loadQueue; loadQueue = newQueue; newQueue = tmp; } } void CheckTiles(TileInfo parent, int currentZoomLevel, int xTile, int yTile, int zoomLevel, int subquadIndex) { // Is this tile visible? TileInfo ti; int tileCode = GetTileHashCode(xTile, yTile, zoomLevel); if (!cachedTiles.TryGetValue(tileCode, out ti)) { ti = new TileInfo(xTile, yTile, zoomLevel, subquadIndex, currentEarthTexture); ti.parent = parent; if (parent != null) { if (parent.children == null) { parent.children = new List(); } parent.children.Add(ti); } for (int k = 0; k < 4; k++) { float xt = xTile + offsets[k].x; float yt = yTile + offsets[k].y; ti.latlons[k] = Conversion.GetLatLonFromTile(xt, yt, zoomLevel); } // Defult UVs wrt Earth texture Vector2 latLonTL = ti.latlons[0]; Vector2 latLonBR = ti.latlons[3]; Vector2 uv_tl = new Vector2((latLonTL.y + 180) / 360f, (latLonTL.x + 90) / 180f); Vector2 uv_br = new Vector2((latLonBR.y + 180) / 360f, (latLonBR.x + 90) / 180f); if (uv_tl.x > 0.5f && uv_br.x < 0.5f) { uv_br.x = 1f; } ti.worldTextureCoords = new Vector4(uv_tl.x, uv_br.y, uv_br.x, uv_tl.y); for (int k = 0; k < 4; k++) { Vector3 spherePos = Conversion.GetSpherePointFromLatLon(ti.latlons[k]); ti.spherePos[k] = spherePos; } cachedTiles[tileCode] = ti; } #if DEBUG_TILES if (ti.gameObject != null) { if (ti.gameObject.GetComponent().debug) { int jj = 9; // can put a break point here to debug this tile jj++; } } #endif // Check if any tile corner is visible // Phase I Vector3 tmp = Misc.Vector3zero; bool cornersOccluded = true; Vector3 minWorldPos = Misc.Vector3Max; Vector3 maxWorldPos = Misc.Vector3Min; float globeRadiusSqr = radius * radius; for (int c = 0; c < 4; c++) { Vector3 wpos = transform.TransformPoint(ti.spherePos[c]); ti.cornerWorldPos[c] = wpos; if (wpos.x < minWorldPos.x) minWorldPos.x = wpos.x; if (wpos.y < minWorldPos.y) minWorldPos.y = wpos.y; if (wpos.z < minWorldPos.z) minWorldPos.z = wpos.z; if (wpos.x > maxWorldPos.x) maxWorldPos.x = wpos.x; if (wpos.y > maxWorldPos.y) maxWorldPos.y = wpos.y; if (wpos.z > maxWorldPos.z) maxWorldPos.z = wpos.z; if (cornersOccluded) { FastVector.NormalizedDirection(ref wpos, ref currentCameraPosition, ref tmp); Vector3 st = wpos; FastVector.Add(ref st, ref tmp, localScaleFactor); float magSqr = (st.x - globePos.x) * (st.x - globePos.x) + (st.y - globePos.y) * (st.y - globePos.y) + (st.z - globePos.z) * (st.z - globePos.z); if (magSqr > globeRadiusSqr) { cornersOccluded = false; } } } FastVector.Average(ref minWorldPos, ref maxWorldPos, ref tmp); Vector3 tileMidPoint = tmp; // Check center of quad if (cornersOccluded) { Vector2 latLonTL = ti.latlons[0]; Vector2 latLonBR = ti.latlons[3]; if (currentLatLon.x >= latLonBR.x && currentLatLon.x <= latLonTL.x && currentLatLon.y >= latLonTL.y && currentLatLon.y <= latLonBR.y) { cornersOccluded = false; } } bool insideViewport = false; Rect camRect = currentCamera.pixelRect; float minX = Screen.width, minY = Screen.height; float maxX = 0, maxY = 0; if (!cornersOccluded) { // Phase II float screenRectMinX = camRect.xMin; float screenRectMaxX = camRect.xMax; float screenRectMinY = camRect.yMin; float screenRectMaxY = camRect.yMax; for (int c = 0; c < 4; c++) { Vector3 scrPos = currentCamera.WorldToScreenPoint(ti.cornerWorldPos[c]); insideViewport = insideViewport || (scrPos.z > 0 && scrPos.x >= screenRectMinX && scrPos.x < screenRectMaxX && scrPos.y >= screenRectMinY && scrPos.y < screenRectMaxY); if (scrPos.x < minX) minX = scrPos.x; if (scrPos.x > maxX) maxX = scrPos.x; if (scrPos.y < minY) minY = scrPos.y; if (scrPos.y > maxY) maxY = scrPos.y; } if (!insideViewport) { Vector3 boundsSize = maxWorldPos - minWorldPos; // Add a bit margin. Due to projection, smaller tiles could be a bit outside the area of the parent so we add a bit margin to ensure that // if we disable a parent, we do only when it and all its children are outside of viewport boundsSize.x *= 1.02f; boundsSize.y *= 1.02f; boundsSize.z *= 1.02f; Bounds bounds = new Bounds(tileMidPoint, boundsSize); insideViewport = GeometryUtility.TestPlanesAABB(cameraPlanes, bounds); } } // Ensure the screen bounds are not degenerated (ie. a quad almost on its side which barely results visible) if (insideViewport) { if (maxY - minY < 2 || maxX - minX < 2) { insideViewport = false; } } ti.insideViewport = insideViewport; ti.visible = false; bool tileIsBig = false; if (insideViewport) { if (!ti.created) { CreateTileQuad(ti); } if (!ti.gameObject.activeSelf) { ti.gameObject.SetActive(true); } // Manage hierarchy of tiles FastVector.NormalizedDirection(ref globePos, ref tileMidPoint, ref tmp); float dd = Vector3.Dot(currentCameraForward, tmp); if (dd > -0.8f || currentZoomLevel > 9) { // prevents big seams on initial zooms float aparentSize = Mathf.Max(maxX - minX, maxY - minY); tileIsBig = aparentSize > currentTileSize; } else { tileIsBig = ti.zoomLevel < currentZoomLevel; } if ((tileIsBig || zoomLevel < TILE_MIN_ZOOM_LEVEL) && zoomLevel < _tileMaxZoomLevel) { // Load nested tiles CheckTiles(ti, currentZoomLevel, xTile * 2, yTile * 2, zoomLevel + 1, 0); CheckTiles(ti, currentZoomLevel, xTile * 2 + 1, yTile * 2, zoomLevel + 1, 1); CheckTiles(ti, currentZoomLevel, xTile * 2, yTile * 2 + 1, zoomLevel + 1, 2); CheckTiles(ti, currentZoomLevel, xTile * 2 + 1, yTile * 2 + 1, zoomLevel + 1, 3); ti.StopAnimation(); ti.renderer.enabled = false; } else { ti.visible = true; visibleTiles.Add(ti); // Show tile renderer if (!ti.renderer.enabled) { if (ti.loadStatus != TILE_LOAD_STATUS.Loaded) { ti.renderer.sharedMaterial = ti.parent.transMat; ti.SetAlpha(0); } ti.renderer.enabled = true; if (OnTileBecameVisible != null) { OnTileBecameVisible(ti); } } // If tile is not loaded yet and parent tile is loaded then use that as placeholder texture if (ti.zoomLevel > TILE_MIN_ZOOM_LEVEL && !ti.parent.placeholderImageSet) { ResolvePlaceholderImage(ti.parent); } if (ti.loadStatus == TILE_LOAD_STATUS.Loaded) { if (!ti.hasAnimated) { ti.hasAnimated = true; if (_tilePreloadTiles && ti.zoomLevel <= TILE_MIN_ZOOM_LEVEL) { ti.Animate(0, AnimationEnded); } else { ti.Animate(1f, AnimationEnded); } } } else if (ti.loadStatus == TILE_LOAD_STATUS.Inactive) { ti.loadStatus = TILE_LOAD_STATUS.InQueue; ti.queueTime = Time.time; loadQueue.Add(ti); } if (ti.children != null) { int count = ti.children.Count; for (int k = 0; k < count; k++) { TileInfo tiChild = ti.children[k]; if (tiChild.gameObject != null && tiChild.gameObject.activeSelf) { if (ti.isAnimating || !tiChild.renderer.enabled || tiChild.loadStatus != TILE_LOAD_STATUS.Loaded || ti.texture == currentEarthTexture) { HideTile(tiChild); } else if (!tiChild.isAnimating) { tiChild.Animate(1f, EndChildFadeOut, true); } } } } } } else { HideTile(ti); } #if DEBUG_TILES if (ti.gameObject != null) { TileInfoEx info = ti.gameObject.GetComponent(); info.bigTile = tileIsBig; info.loadStatus = ti.loadStatus; info.visible = ti.visible; info.x = ti.x; info.y = ti.y; info.zoomLevel = ti.zoomLevel; info.placeholderImageSet = ti.parent != null ? ti.parent.placeholderImageSet : false; info.lastFrameUsed = Time.frameCount; info.material = ti.renderer.sharedMaterial; if (ti.parent != null) { info.parentTexture = ti.parent.texture; } } #endif } /// /// Finds a parent with a texture and updates parentTex and parentCoords for all children /// void ResolvePlaceholderImage(TileInfo parent) { if (parent == null) return; int childrenCount = parent.children.Count; // First, if parent is loaded, simple update children if (parent.zoomLevel >= TILE_MIN_ZOOM_LEVEL && parent.loadStatus == TILE_LOAD_STATUS.Loaded) { parent.placeholderImageSet = true; for (int k = 0; k < childrenCount; k++) { TileInfo child = parent.children[k]; child.parentTextureCoords = placeHolderUV[child.subquadIndex]; child.SetPlaceholderImage(parent.texture); } return; } // Parent is not loaded, find a grand parent TileInfo grandParent = parent; while (grandParent.loadStatus != TILE_LOAD_STATUS.Loaded) { grandParent = grandParent.parent; } // Annotate that this placeholder is temporary (set placeholderImageSet to false) parent.placeholderImageSet = false; if (grandParent.zoomLevel < TILE_MIN_ZOOM_LEVEL) { // Reset to Earth texture uvs for (int k = 0; k < childrenCount; k++) { TileInfo child = parent.children[k]; child.parentTextureCoords = child.worldTextureCoords; child.SetPlaceholderImage(grandParent.texture); } } else { // Compute subquad uvs float uv_width = grandParent.worldTextureCoords.z - grandParent.worldTextureCoords.x; float uv_height = grandParent.worldTextureCoords.w - grandParent.worldTextureCoords.y; for (int k = 0; k < childrenCount; k++) { TileInfo child = parent.children[k]; float uv_xmin = (child.worldTextureCoords.x - grandParent.worldTextureCoords.x) / uv_width; float uv_xmax = (child.worldTextureCoords.z - grandParent.worldTextureCoords.x) / uv_width; float uv_ymin = (child.worldTextureCoords.y - grandParent.worldTextureCoords.y) / uv_height; float uv_ymax = (child.worldTextureCoords.w - grandParent.worldTextureCoords.y) / uv_height; child.parentTextureCoords = new Vector4(uv_xmin, uv_ymin, uv_xmax, uv_ymax); child.SetPlaceholderImage(grandParent.texture); } } } #if UNITY_EDITOR private void OnGUI() { if (!_tilesShowDebugInfo || visibleTiles == null) return; GUI.color = Color.red; int debugTilesCount = visibleTiles.Count; for (int k = 0; k < debugTilesCount; k++) { TileInfo ti = visibleTiles[k]; if (!ti.visible) continue; Vector3 pos = (ti.cornerWorldPos[0] + ti.cornerWorldPos[3]) * 0.5f; Vector3 scrPos = _mainCamera.WorldToScreenPoint(pos); if (scrPos.z < 0) continue; scrPos.y = Screen.height - scrPos.y; string text = "ZL:" + ti.zoomLevel + " X:" + ti.x + " Y:" + ti.y + " QI:" + ti.subquadIndex + " " + ti.loadStatus.ToString() + " ST:" + ti.stage; var textSize = GUI.skin.label.CalcSize(new GUIContent(text)); scrPos.x -= textSize.x * 0.5f; GUI.Label(new Rect(scrPos.x, scrPos.y, textSize.x, textSize.y), text); } } #endif void HideTile(TileInfo ti) { if (ti.gameObject != null && ti.gameObject.activeSelf) { ti.gameObject.SetActive(false); ti.visible = false; ti.renderer.enabled = false; if (OnTileBecameInvisible != null) { OnTileBecameInvisible(ti); } if (ti.loadStatus == TILE_LOAD_STATUS.Loaded && ti.zoomLevel > TILE_MIN_ZOOM_LEVEL) { if (!ti.isAddedToInactive) { ti.isAddedToInactive = true; inactiveTiles.Add(ti); } ti.inactiveTime = Time.time; } } } void EndChildFadeOut(TileInfo ti) { ti.hasAnimated = false; HideTile(ti); shouldCheckTiles = true; } void AnimationEnded(TileInfo ti) { shouldCheckTiles = true; } int GetTileHashCode(int x, int y, int zoomLevel) { ZoomLevelInfo zi = zoomLevelsInfo[zoomLevel]; if (zi == null) return 0; int xMax = zi.xMax; x = (x + xMax) % xMax; int hashCode = zi.zoomLevelHash + zi.yHash * y + x; return hashCode; } int GetCenterTileZoomLevel() { // Get screen dimensions of central tile int zoomLevel0 = 1; int zoomLevel1 = TILE_MAX_ZOOM_LEVEL; int zoomLevel = TILE_MIN_ZOOM_LEVEL; Vector2 latLon = Conversion.GetLatLonFromUnitSpherePoint(GetCurrentMapLocation()); int xTile, yTile; currentTileSize = _tileSize > TILE_MIN_SIZE ? _tileSize : TILE_MIN_SIZE; currentTileSize *= 2.25f / _tileResolutionFactor; // (3.0f - _tileResolutionFactor); float dist = 0; Camera cam = currentCamera; Quaternion oldRot = cam.transform.rotation; cam.transform.LookAt(transform.position, cam.transform.up); for (int i = 0; i < 5; i++) { zoomLevel = (zoomLevel0 + zoomLevel1) / 2; Conversion.GetTileFromLatLon(zoomLevel, latLon.x, latLon.y, out xTile, out yTile); Vector2 latLonTL = Conversion.GetLatLonFromTile(xTile, yTile, zoomLevel); Vector2 latLonBR = Conversion.GetLatLonFromTile(xTile + 0.999f, yTile + 0.999f, zoomLevel); Vector3 spherePointTL = Conversion.GetSpherePointFromLatLon(latLonTL); Vector3 spherePointBR = Conversion.GetSpherePointFromLatLon(latLonBR); Vector3 wposTL = cam.WorldToScreenPoint(transform.TransformPoint(spherePointTL)); Vector3 wposBR = cam.WorldToScreenPoint(transform.TransformPoint(spherePointBR)); dist = Mathf.Max(Mathf.Abs(wposBR.x - wposTL.x), Mathf.Abs(wposTL.y - wposBR.y)); if (dist > currentTileSize) { zoomLevel0 = zoomLevel; } else { zoomLevel1 = zoomLevel; } } if (dist > currentTileSize) { zoomLevel++; } cam.transform.rotation = oldRot; zoomLevel = Mathf.Clamp(zoomLevel, TILE_MIN_ZOOM_LEVEL, TILE_MAX_ZOOM_LEVEL); return zoomLevel; } void CreateTileQuad(TileInfo ti) { ZoomLevelInfo zi = zoomLevelsInfo[ti.zoomLevel]; // Create container GameObject parentObj; if (ti.parent == null) { parentObj = zi.tilesContainer; if (parentObj == null) { parentObj = new GameObject("Tiles" + ti.zoomLevel); parentObj.transform.SetParent(tilesRoot, false); zi.tilesContainer = parentObj; } } else { parentObj = ti.parent.gameObject; } // Prepare mesh vertices Vector3[] tileCorners = new Vector3[4]; tileCorners[0] = ti.spherePos[0]; tileCorners[1] = ti.spherePos[1]; tileCorners[2] = ti.spherePos[3]; tileCorners[3] = ti.spherePos[2]; // Setup tile materials TileInfo parent = ti.parent != null ? ti.parent : ti; if (parent.opaqueMat == null) { parent.opaqueMat = Instantiate(tileMatRef); #if UNITY_EDITOR if (_tilesShowDebugInfo) parent.opaqueMat.EnableKeyword("SHOW_BORDER"); #endif } if (parent.transMat == null) { parent.transMat = Instantiate(tileMatTransRef); #if UNITY_EDITOR if (_tilesShowDebugInfo) parent.transMat.EnableKeyword("SHOW_BORDER"); #endif } Material tileMat = ti.zoomLevel < TILE_MIN_ZOOM_LEVEL ? parent.opaqueMat : parent.transMat; if (ti.zoomLevel < TILE_MIN_ZOOM_LEVEL) { ti.loadStatus = TILE_LOAD_STATUS.Loaded; } if (ti.zoomLevel <= TILE_MIN_ZOOM_LEVEL) { ti.ClearPlaceholderImage(); } else { ResolvePlaceholderImage(ti.parent); } ti.texture = currentEarthTexture; ti.source = TILE_SOURCE.Resources; ti.renderer = CreateGameObject(parentObj.transform, "Tile", tileCorners, tileIndices, tileUV, tileMat, ti.subquadIndex); ti.gameObject = ti.renderer.gameObject; ti.renderer.enabled = false; ti.created = true; #if DEBUG_TILES ti.gameObject.AddComponent(); #endif if (OnTileCreated != null) { OnTileCreated(ti); } } internal IEnumerator LoadTileContentBackground(TileInfo ti) { yield return new WaitForEndOfFrame(); string url = GetTileURL(_tileServer, ti); if (string.IsNullOrEmpty(url)) { _concurrentLoads--; Debug.LogError("Tile server url not set. Aborting"); yield break; } ti.stage = 30; long downloadedBytes = 0; string error = null; string filePath = ""; byte[] textureBytes = null; ti.source = TILE_SOURCE.Unknown; // Check if tile is given by external event if (OnTileRequest != null) { if (OnTileRequest(ti.zoomLevel, ti.x, ti.y, out ti.texture, out error) && ti.texture != null) { ti.source = TILE_SOURCE.Resources; } } // Check if tile is in Resources if (ti.source == TILE_SOURCE.Unknown && _tileEnableOfflineTiles) { string path = GetTileResourcePath(ti.x, ti.y, ti.zoomLevel, false); ResourceRequest request = Resources.LoadAsync(path); yield return request; if (request.asset != null) { ti.texture = (Texture2D)request.asset; ti.source = TILE_SOURCE.Resources; } else if (tileOfflineTilesOnly) { ti.texture = tileResourceFallbackTexture; ti.source = TILE_SOURCE.Resources; } } CustomWWW www = null; if (ti.source == TILE_SOURCE.Unknown) { ti.stage = 40; www = getCachedWWW(url, ti); yield return www; if (www.isDone) { error = www.error; } } ti.stage = 50; for (int tries = 0; tries < 100; tries++) { if (spreadLoadAmongFrames > 0) break; yield return new WaitForEndOfFrame(); } spreadLoadAmongFrames--; _concurrentLoads--; ti.stage = 70; if (!string.IsNullOrEmpty(error)) { _tileLastError = "Error getting tile z:" + ti.zoomLevel + " x:" + ti.x + " " + ti.y + ": " + error + " url=" + url + " max timeout:" + _tileDownloadTimeout; _tileLastErrorDate = DateTime.Now; if (_tileDebugErrors) { Debug.Log(_tileLastErrorDate + " " + _tileLastError); } ti.loadStatus = TILE_LOAD_STATUS.InQueue; ti.loadDelay += 10; yield break; } // Load texture ti.stage = 80; if (ti.source != TILE_SOURCE.Resources) { downloadedBytes = www.bytesDownloaded; textureBytes = www.bytes; ti.texture = www.textureNonReadable; www.Dispose(); www = null; // Check texture consistency if (ti.loadedFromCache || _tileEnableLocalCache) { filePath = GetLocalFilePathForURL(url, ti); } if (ti.loadedFromCache && ti.texture.width <= 16) { // Invalid texture in local cache, retry if (File.Exists(filePath)) { File.Delete(filePath); } ti.loadStatus = TILE_LOAD_STATUS.Inactive; ti.queueTime = Time.time; yield break; } } ti.texture.wrapMode = TextureWrapMode.Clamp; _tileSize = ti.texture.width; // Save texture ti.stage = 90; if (_tileEnableLocalCache && ti.source != TILE_SOURCE.Resources && !File.Exists(filePath)) { _tileCurrentCacheUsage += textureBytes.Length; BackgroundSaver saver = new BackgroundSaver(textureBytes, filePath); saver.Start(); } // Update stats switch (ti.source) { case TILE_SOURCE.Cache: _cacheLoads++; _cacheLoadTotalSize += downloadedBytes; break; case TILE_SOURCE.Resources: _resourceLoads++; break; default: _webDownloads++; _webDownloadTotalSize += downloadedBytes; break; } if (loadQueue.Contains(ti)) { loadQueue.Remove(ti); } ti.stage = 95; FinishLoadingTile(ti); } void CreatePole(TileInfo ti) { Vector3 polePos; Vector3 latLon0; string name; bool isNorth = (ti.y == 0); if (isNorth) { if (northPoleObj != null) return; polePos = Misc.Vector3up * 0.5f; latLon0 = ti.latlons[0]; name = "North Pole"; } else { if (southPoleObj != null) return; polePos = Misc.Vector3down * 0.5f; latLon0 = ti.latlons[2]; name = "South Pole"; } Vector3 latLon3 = latLon0; float lonDX = 360f / zoomLevelsInfo[ti.zoomLevel].xMax; latLon3.y += lonDX; int steps = (int)(360f / lonDX); int vertexCount = steps * 3; List vertices = new List(vertexCount); List indices = new List(vertexCount); List uv = new List(vertexCount); for (int k = 0; k < steps; k++) { Vector3 p0 = Conversion.GetSpherePointFromLatLon(latLon0); Vector3 p1 = Conversion.GetSpherePointFromLatLon(latLon3); latLon0 = latLon3; latLon3.y += lonDX; vertices.Add(p0); vertices.Add(p1); vertices.Add(polePos); indices.Add(k * 3); if (isNorth) { indices.Add(k * 3 + 2); indices.Add(k * 3 + 1); } else { indices.Add(k * 3 + 1); indices.Add(k * 3 + 2); } uv.Add(Misc.Vector2zero); uv.Add(Misc.Vector2up); uv.Add(Misc.Vector2right); } Renderer obj = CreateGameObject(tilesRoot.transform, name, vertices.ToArray(), indices.ToArray(), uv.ToArray(), _tilePolesMaterial ? _tilePolesMaterial : ti.parent.opaqueMat, 0); if (isNorth) { northPoleObj = obj; } else { southPoleObj = obj; } } Renderer CreateGameObject(Transform parent, string name, Vector3[] vertices, int[] indices, Vector2[] uv, Material mat, int subquadIndex) { GameObject obj = new GameObject(name, typeof(MeshFilter), typeof(MeshRenderer)); obj.layer = parent.gameObject.layer; obj.transform.SetParent(parent, false); obj.transform.localPosition = Misc.Vector3zero; obj.transform.localScale = Misc.Vector3one; obj.transform.localRotation = Misc.QuaternionZero; Mesh mesh = new Mesh(); mesh.vertices = vertices; mesh.triangles = indices; mesh.uv = uv; Color[] meshColor; if (vertices.Length != 4) { meshColor = new Color[vertices.Length]; for (int k = 0; k < vertices.Length; k++) meshColor[k] = color1000; } else { meshColor = meshColors[subquadIndex]; } mesh.colors = meshColor; MeshFilter mf = obj.GetComponent(); mf.sharedMesh = mesh; MeshRenderer mr = obj.GetComponent(); mr.sharedMaterial = mat; mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; mr.receiveShadows = false; return mr; } class BackgroundSaver { readonly byte[] tex; readonly string filePath; public BackgroundSaver(byte[] tex, string filePath) { this.tex = tex; this.filePath = filePath; } public void Start() { #if UNITY_WSA && !UNITY_EDITOR Task.Run(() => SaveTextureToCache()); #elif UNITY_WEBGL SaveTextureToCache(); #else Thread thread = new Thread(SaveTextureToCache); thread.Start(); #endif } void SaveTextureToCache() { File.WriteAllBytes(filePath, tex); } } StringBuilder filePathStr = new StringBuilder(250); string GetLocalFilePathForURL(string url, TileInfo ti) { filePathStr.Length = 0; filePathStr.Append(cachePath); filePathStr.Append("/z"); filePathStr.Append(ti.zoomLevel); filePathStr.Append("_x"); filePathStr.Append(ti.x); filePathStr.Append("_y"); filePathStr.Append(ti.y); filePathStr.Append("_"); filePathStr.Append(url.GetHashCode()); filePathStr.Append(".png"); return filePathStr.ToString(); } public string GetTileResourcePath(int x, int y, int zoomLevel, bool fullPath = true) { filePathStr.Length = 0; if (fullPath) { filePathStr.Append(_tileResourcePathBase); filePathStr.Append("/"); } filePathStr.Append("Tiles"); filePathStr.Append("/"); filePathStr.Append((int)_tileServer); filePathStr.Append("/z"); filePathStr.Append(zoomLevel); filePathStr.Append("_x"); filePathStr.Append(x); filePathStr.Append("_y"); filePathStr.Append(y); if (fullPath) { filePathStr.Append(".png"); } return filePathStr.ToString(); } CustomWWW getCachedWWW(string url, TileInfo ti) { string filePath = GetLocalFilePathForURL(url, ti); CustomWWW www; bool useCached = false; useCached = _tileEnableLocalCache && System.IO.File.Exists(filePath); if (useCached) { if (!_tilePreloadTiles || !filePath.Contains(PREFIX_MIN_ZOOM_LEVEL)) { //check how old System.DateTime written = File.GetLastWriteTimeUtc(filePath); System.DateTime now = System.DateTime.UtcNow; double totalHours = now.Subtract(written).TotalHours; if (totalHours > 300) { File.Delete(filePath); useCached = false; } } } ti.source = useCached ? TILE_SOURCE.Cache : TILE_SOURCE.Online; if (useCached) { #if UNITY_STANDALONE_WIN || UNITY_WSA string pathforwww = "file:///" + filePath; #else string pathforwww = "file://" + filePath; #endif www = new CustomWWW(pathforwww, 0); } else { www = new CustomWWW(url, _tileDownloadTimeout); } return www; } bool ReloadTextureFromCacheOrMarkForDownload(TileInfo ti) { if (!_tileEnableLocalCache) return false; string url = GetTileURL(_tileServer, ti); if (string.IsNullOrEmpty(url)) { return false; } string filePath = GetLocalFilePathForURL(url, ti); if (System.IO.File.Exists(filePath)) { //check how old if (!_tilePreloadTiles || ti.zoomLevel != TILE_MIN_ZOOM_LEVEL) { System.DateTime written = File.GetLastWriteTimeUtc(filePath); System.DateTime now = System.DateTime.UtcNow; double totalHours = now.Subtract(written).TotalHours; if (totalHours > 300) { File.Delete(filePath); return false; } } } else { return false; } byte[] bb = System.IO.File.ReadAllBytes(filePath); ti.texture = new Texture2D(0, 0); ti.texture.LoadImage(bb); if (ti.texture.width <= 16) { // Invalid texture in local cache, retry if (File.Exists(filePath)) { File.Delete(filePath); } return false; } ti.texture.wrapMode = TextureWrapMode.Clamp; _cacheLoads++; _cacheLoadTotalSize += bb.Length; FinishLoadingTile(ti); return true; } void FinishLoadingTile(TileInfo ti) { // Good to go, update tile info ti.SetTexture(ti.texture); ti.loadStatus = TILE_LOAD_STATUS.Loaded; if (ti.zoomLevel >= TILE_MIN_ZOOM_LEVEL) { if (ti.y == 0 || ti.y == zoomLevelsInfo[ti.zoomLevel].yMax - 1) { CreatePole(ti); } } // Notify children of new placeholder if (ti.children != null) { int childCount = ti.children.Count; for (int k = 0; k < childCount; k++) { TileInfo tiChild = ti.children[k]; if (tiChild != null) { tiChild.placeholderImageSet = false; } } } shouldCheckTiles = true; ti.stage = 99; } } }