From e9ac95b6d7c2b7941d603249cee95f3f442648f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Wed, 25 Feb 2026 15:53:45 +0300 Subject: [PATCH 01/54] add an empty constructor to tilejsonresponse (#1970) fix an issue where tilejsonresponse didn't work with medium level code stripping setting --- .../BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs b/Runtime/Mapbox/BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs index e83bb3fbc..17fdc36c9 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs @@ -2,6 +2,7 @@ using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; using Newtonsoft.Json; +using UnityEngine.Scripting; namespace Mapbox.BaseModule.Data.Platform.TileJSON { @@ -137,6 +138,7 @@ public long? Modified [JsonProperty("webpage")] public string WebPage { get; set; } - + [Preserve] + public TileJSONResponse() { } } } From 2f41d0c0be5cfb57b19e76c6b9954c7d6f2c08b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Wed, 25 Feb 2026 17:41:43 +0300 Subject: [PATCH 02/54] add place_type to geocoding feature (#1971) --- Runtime/Mapbox/GeocodingApi/Response/Feature.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Runtime/Mapbox/GeocodingApi/Response/Feature.cs b/Runtime/Mapbox/GeocodingApi/Response/Feature.cs index b8780ba04..023e2a2c9 100644 --- a/Runtime/Mapbox/GeocodingApi/Response/Feature.cs +++ b/Runtime/Mapbox/GeocodingApi/Response/Feature.cs @@ -26,7 +26,14 @@ public class Feature { /// The type. [JsonProperty("type")] public string Type; - + + /// + /// Gets or sets the types of the place. + /// + /// The types of the place. + [JsonProperty("place_type")] + public List PlaceType{ get; set; } + /// /// Gets or sets the text. /// From 4648c106999df16a5f6957b199bd4aa1959b57be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Wed, 25 Feb 2026 17:44:11 +0300 Subject: [PATCH 03/54] fix file cache path format (#1972) * fix file cache path format fix image file names to use underscore to fix bugs and issues in small x/y coordinates (in example; 223 to 22_3) add runtime cache manager behaviour to be able to change database name (prefix) to start/test with a clean cache * fix a bug in base module editor assembly sttings remove unnecessary if check in runtime cache behaviour --- Editor/Mapbox.UnitySDK.Editor.asmdef | 3 +- .../Data/Platform/Cache/FileCache.cs | 2 +- Runtime/Mapbox/BaseModule/Editor.meta | 8 +++ .../Editor/MapInformationDrawer.cs | 0 .../Editor/MapInformationDrawer.cs.meta | 0 .../Editor/MapboxBaseModule.Editor.asmdef | 18 ++++++ .../MapboxBaseModule.Editor.asmdef.meta | 7 +++ .../RuntimeCacheManagerBehaviourEditor.cs | 33 ++++++++++ ...RuntimeCacheManagerBehaviourEditor.cs.meta | 11 ++++ .../Editor/UnityContextDrawer.cs | 0 .../Editor/UnityContextDrawer.cs.meta | 0 .../DataFetchingManagerBehaviour.cs | 2 +- .../MapboxCacheManagerBehaviour.cs | 35 +---------- .../RuntimeCacheManagerBehaviour.cs | 62 +++++++++++++++++++ .../RuntimeCacheManagerBehaviour.cs.meta | 3 + .../ModuleBehaviours/TileCreatorBehaviour.cs | 6 +- .../Scripts/DirectionScript.cs | 1 - .../Example/Scripts/Map/MapboxMapBehaviour.cs | 6 +- .../Mapbox/Example/Scripts/Test/FilterTest.cs | 3 - .../Logging/LoggingCacheManagerBehaviour.cs | 3 +- .../LoggingDataFetchingManagerBehaviour.cs | 2 +- .../Scripts/Logging/LoggingMapBehaviour.cs | 3 +- 22 files changed, 152 insertions(+), 56 deletions(-) create mode 100644 Runtime/Mapbox/BaseModule/Editor.meta rename Runtime/Mapbox/{Example => BaseModule}/Editor/MapInformationDrawer.cs (100%) rename Runtime/Mapbox/{Example => BaseModule}/Editor/MapInformationDrawer.cs.meta (100%) create mode 100644 Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef create mode 100644 Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef.meta create mode 100644 Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs create mode 100644 Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs.meta rename Runtime/Mapbox/{Example => BaseModule}/Editor/UnityContextDrawer.cs (100%) rename Runtime/Mapbox/{Example => BaseModule}/Editor/UnityContextDrawer.cs.meta (100%) create mode 100644 Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs create mode 100644 Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs.meta diff --git a/Editor/Mapbox.UnitySDK.Editor.asmdef b/Editor/Mapbox.UnitySDK.Editor.asmdef index 313e9198f..1ca996427 100644 --- a/Editor/Mapbox.UnitySDK.Editor.asmdef +++ b/Editor/Mapbox.UnitySDK.Editor.asmdef @@ -2,7 +2,8 @@ "name": "MapboxUnitySDK.Editor", "rootNamespace": "", "references": [ - "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:36ca6af6e2b304d4090888554d6ce199" ], "includePlatforms": [ "Editor" diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs index dfb43393e..fb076c0b1 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs @@ -87,7 +87,7 @@ public bool TestAvailability() private string TileToRelativeFilePath(CanonicalTileId tileId, string tilesetId) { - return string.Format("{0}/{1}{2}{3}.{4}", MapIdToFolderName(tilesetId), tileId.X, tileId.Y, tileId.Z, FileExtension); + return string.Format("{0}/{1}_{2}_{3}.{4}", MapIdToFolderName(tilesetId), tileId.X, tileId.Y, tileId.Z, FileExtension); } public virtual bool Exists(CanonicalTileId tileId, string mapId) diff --git a/Runtime/Mapbox/BaseModule/Editor.meta b/Runtime/Mapbox/BaseModule/Editor.meta new file mode 100644 index 000000000..28695b49d --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 88c1b74736534426ea94437fd425da25 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/Example/Editor/MapInformationDrawer.cs b/Runtime/Mapbox/BaseModule/Editor/MapInformationDrawer.cs similarity index 100% rename from Runtime/Mapbox/Example/Editor/MapInformationDrawer.cs rename to Runtime/Mapbox/BaseModule/Editor/MapInformationDrawer.cs diff --git a/Runtime/Mapbox/Example/Editor/MapInformationDrawer.cs.meta b/Runtime/Mapbox/BaseModule/Editor/MapInformationDrawer.cs.meta similarity index 100% rename from Runtime/Mapbox/Example/Editor/MapInformationDrawer.cs.meta rename to Runtime/Mapbox/BaseModule/Editor/MapInformationDrawer.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef b/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef new file mode 100644 index 000000000..d2c79de57 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "MapboxBaseModule.Editor", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef.meta b/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef.meta new file mode 100644 index 000000000..1626f91e1 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 567de054dbe5948c0bc25053f1aa6581 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs b/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs new file mode 100644 index 000000000..3f00a43e2 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs @@ -0,0 +1,33 @@ +using Mapbox.BaseModule.Unity.ModuleBehaviours; +using UnityEditor; + +namespace Mapbox.BaseModule.Editor +{ + [CustomEditor(typeof(RuntimeCacheManagerBehaviour))] + public class RuntimeCacheManagerBehaviourEditor : UnityEditor.Editor + { + SerializedProperty createSqliteCache; + SerializedProperty useCustomName; + SerializedProperty customName; + SerializedProperty createFileCache; + + void OnEnable() + { + createSqliteCache = serializedObject.FindProperty("CreateSqliteCache"); + useCustomName = serializedObject.FindProperty("UseCustomName"); + customName = serializedObject.FindProperty("CustomName"); + createFileCache = serializedObject.FindProperty("CreateFileCache"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + EditorGUILayout.PropertyField(useCustomName); + if (useCustomName.boolValue) + { + EditorGUILayout.PropertyField(customName); + } + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs.meta b/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs.meta new file mode 100644 index 000000000..f8563100d --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f504b61d8125144a89d1eee4768fcb2b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/Example/Editor/UnityContextDrawer.cs b/Runtime/Mapbox/BaseModule/Editor/UnityContextDrawer.cs similarity index 100% rename from Runtime/Mapbox/Example/Editor/UnityContextDrawer.cs rename to Runtime/Mapbox/BaseModule/Editor/UnityContextDrawer.cs diff --git a/Runtime/Mapbox/Example/Editor/UnityContextDrawer.cs.meta b/Runtime/Mapbox/BaseModule/Editor/UnityContextDrawer.cs.meta similarity index 100% rename from Runtime/Mapbox/Example/Editor/UnityContextDrawer.cs.meta rename to Runtime/Mapbox/BaseModule/Editor/UnityContextDrawer.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/DataFetchingManagerBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/DataFetchingManagerBehaviour.cs index 137d17406..2bf00dfcd 100644 --- a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/DataFetchingManagerBehaviour.cs +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/DataFetchingManagerBehaviour.cs @@ -2,7 +2,7 @@ using Mapbox.BaseModule.Data.DataFetchers; using UnityEngine; -namespace Mapbox.Example.Scripts.ModuleBehaviours +namespace Mapbox.BaseModule.Unity.ModuleBehaviours { public class DataFetchingManagerBehaviour : MonoBehaviour { diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/MapboxCacheManagerBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/MapboxCacheManagerBehaviour.cs index ede5f920f..6c08e08a5 100644 --- a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/MapboxCacheManagerBehaviour.cs +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/MapboxCacheManagerBehaviour.cs @@ -1,42 +1,9 @@ using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Platform.Cache; -using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; -using Mapbox.BaseModule.Unity; using UnityEngine; -namespace Mapbox.Example.Scripts.ModuleBehaviours +namespace Mapbox.BaseModule.Unity.ModuleBehaviours { - public class RuntimeCacheManagerBehaviour : MapboxCacheManagerBehaviour - { - public MapboxCacheManager CacheManager; - public MemoryCache MemoryCache; - - public MapboxCacheManager GetCacheManager() => CacheManager; - - public bool CreateSqliteCache = true; - public bool CreateFileCache = true; - - public override MapboxCacheManager GetCacheManager(UnityContext unityContext, DataFetchingManager dataFetchingManager) - { - if (CacheManager == null) - { - SqliteCache sqliteCache = null; - FileCache fileCache = null; - sqliteCache = CreateSqliteCache ? new SqliteCache(unityContext.TaskManager, 1000) : null; - fileCache = CreateFileCache ? new FileCache(unityContext.TaskManager) : null; - MemoryCache = new MemoryCache(); - - CacheManager = new MapboxCacheManager( - unityContext, - MemoryCache, - fileCache, - sqliteCache); - } - - return CacheManager; - } - } - public abstract class MapboxCacheManagerBehaviour : MonoBehaviour { public abstract MapboxCacheManager GetCacheManager(UnityContext unityContext, DataFetchingManager dataFetchingManager); diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs new file mode 100644 index 000000000..f0f478529 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs @@ -0,0 +1,62 @@ +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; + +namespace Mapbox.BaseModule.Unity.ModuleBehaviours +{ + public class RuntimeCacheManagerBehaviour : MapboxCacheManagerBehaviour + { + public MapboxCacheManager CacheManager; + public MemoryCache MemoryCache; + + public MapboxCacheManager GetCacheManager() => CacheManager; + + public bool CreateSqliteCache = true; + + public bool CreateFileCache = true; + + public bool UseCustomName = false; + public string CustomName; + + public override MapboxCacheManager GetCacheManager(UnityContext unityContext, DataFetchingManager dataFetchingManager) + { + if (CacheManager == null) + { + SqliteCache sqliteCache = null; + FileCache fileCache = null; + if (CreateSqliteCache) + { + if (UseCustomName) + { + sqliteCache = new SqliteCache(unityContext.TaskManager, 1000, CustomName); + } + else + { + sqliteCache = new SqliteCache(unityContext.TaskManager, 1000); + } + } + + if (CreateFileCache) + { + if (UseCustomName) + { + fileCache = new FileCache(unityContext.TaskManager, CustomName); + } + else + { + fileCache = new FileCache(unityContext.TaskManager); + } + } + + MemoryCache = new MemoryCache(); + CacheManager = new MapboxCacheManager( + unityContext, + MemoryCache, + fileCache, + sqliteCache); + } + + return CacheManager; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs.meta b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs.meta new file mode 100644 index 000000000..856152900 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 459c81193c94429ba939258f8a66103f +timeCreated: 1768224967 \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs index d3ebaed22..3197ff51e 100644 --- a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs @@ -1,12 +1,8 @@ using System; -using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Map; -using Mapbox.BaseModule.Unity; -using Mapbox.ImageModule.Terrain.Settings; -using Mapbox.ImageModule.Terrain.TerrainStrategies; using UnityEngine; -namespace Mapbox.Example.Scripts.ModuleBehaviours +namespace Mapbox.BaseModule.Unity.ModuleBehaviours { public class TileCreatorBehaviour : MonoBehaviour { diff --git a/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/Scripts/DirectionScript.cs b/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/Scripts/DirectionScript.cs index 357fdb748..0a9e8d25d 100644 --- a/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/Scripts/DirectionScript.cs +++ b/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/Scripts/DirectionScript.cs @@ -10,7 +10,6 @@ using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Utilities; using Mapbox.DirectionsApi; -using Mapbox.Example.Scripts.ModuleBehaviours; using UnityEngine; using UnityEngine.Serialization; using UnityEngine.UI; diff --git a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs index 72f0a4601..54ffedb12 100644 --- a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs @@ -1,16 +1,12 @@ using System; -using System.Linq; -using Mapbox.BaseModule; using Mapbox.BaseModule.Data.DataFetchers; -using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Data.Platform.Cache; using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Unity.ModuleBehaviours; using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.ModuleBehaviours; using Mapbox.Example.Scripts.TileProviderBehaviours; -using Mapbox.ImageModule.Terrain.TerrainStrategies; using Mapbox.UnityMapService; using Mapbox.UnityMapService.TileProviders; using UnityEngine; diff --git a/Runtime/Mapbox/Example/Scripts/Test/FilterTest.cs b/Runtime/Mapbox/Example/Scripts/Test/FilterTest.cs index e40c13b62..f760d3731 100644 --- a/Runtime/Mapbox/Example/Scripts/Test/FilterTest.cs +++ b/Runtime/Mapbox/Example/Scripts/Test/FilterTest.cs @@ -1,10 +1,7 @@ -using System.Linq; using Mapbox.BaseModule.Utilities; using Mapbox.Example.Scripts.Map; -using Mapbox.Example.Scripts.ModuleBehaviours; using Mapbox.VectorModule; using Mapbox.VectorModule.Filters; -using Mapbox.VectorModule.Unity; using UnityEngine; namespace Mapbox.Example.Scripts.Test diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingCacheManagerBehaviour.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingCacheManagerBehaviour.cs index b0f1c419a..fdec9d400 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingCacheManagerBehaviour.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingCacheManagerBehaviour.cs @@ -1,8 +1,7 @@ using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Platform.Cache; -using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; using Mapbox.BaseModule.Unity; -using Mapbox.Example.Scripts.ModuleBehaviours; +using Mapbox.BaseModule.Unity.ModuleBehaviours; using Mapbox.MapDebug.Scripts.Logging; using UnityEngine; diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingDataFetchingManagerBehaviour.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingDataFetchingManagerBehaviour.cs index cb34e2d2d..bbe0f2111 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingDataFetchingManagerBehaviour.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingDataFetchingManagerBehaviour.cs @@ -1,6 +1,6 @@ using System; using Mapbox.BaseModule.Data.DataFetchers; -using Mapbox.Example.Scripts.ModuleBehaviours; +using Mapbox.BaseModule.Unity.ModuleBehaviours; using UnityEngine; namespace Mapbox.MapDebug.Scripts.Logging diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs index 02a958018..479e05155 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs @@ -4,10 +4,9 @@ using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Unity.ModuleBehaviours; using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.ModuleBehaviours; using Mapbox.Example.Scripts.TileProviderBehaviours; -using Mapbox.ImageModule.Terrain.TerrainStrategies; #if UNITY_RECORDER && UNITY_EDITOR using Mapbox.MapDebug.Sequence; #endif From bbafebe5299ff99bfb11a7913a9b1e051e9e4f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Wed, 25 Feb 2026 18:51:31 +0300 Subject: [PATCH 04/54] add runtime image style change support (#1974) * add runtime image style change support add support to change style in runtime add public method to change imagery tilesetid add short doc and snippet about how to use it * remove unused methods fix empty method to yield break instead of returning null * merge similar fetch methods in image source fix typo in documentation change inactiveCapacity of memory cache to readonly * fix reload tiles method to handle delay execution --- Documentation~/ChangeImageryStyleOnRuntime.md | 28 ++++ .../BaseModule/Data/DataFetchers/Source.cs | 5 + .../Data/Platform/Cache/TypeMemoryCache.cs | 16 ++ .../ImageModule/StaticApiLayerModule.cs | 6 + .../DataSources/ImageSource.cs | 151 ++++++++++++++---- 5 files changed, 171 insertions(+), 35 deletions(-) create mode 100644 Documentation~/ChangeImageryStyleOnRuntime.md diff --git a/Documentation~/ChangeImageryStyleOnRuntime.md b/Documentation~/ChangeImageryStyleOnRuntime.md new file mode 100644 index 000000000..9898d7905 --- /dev/null +++ b/Documentation~/ChangeImageryStyleOnRuntime.md @@ -0,0 +1,28 @@ +# Change Imagery Style at Runtime + +To change the imagery style at runtime, follow these steps: + +1. Get a reference to the `MapboxMap`. +2. Access the `StaticApiLayerModule` from the `MapVisualizer`. +3. Change the `tilesetId` (asynchronous operation). +4. Reload all currently active tiles to apply the new imagery. + +Changing the `tilesetId` is not instantaneous, so it must be handled as a coroutine. After the update completes, explicitly reload the active tiles to refresh the visible content. + +```csharp +private IEnumerator ReloadImageModule(MapboxMap map, string tilesetId) +{ + if (map.MapVisualizer.TryGetLayerModule(out var imageModule)) + { + yield return imageModule.ChangeTilesetId(tilesetId); + + foreach (var tilePair in map.MapVisualizer.ActiveTiles) + { + var tile = tilePair.Value; + imageModule.LoadInstant(tile); + } + } +} +``` + +This ensures the imagery source is updated and all visible tiles are refreshed immediately without requiring user interaction. \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/Source.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/Source.cs index a4bb35f5b..e82b9d9a5 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/Source.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/Source.cs @@ -90,6 +90,11 @@ public virtual void Cancel(CanonicalTileId tileId) } + public virtual IEnumerator ChangeTilesetId(string tilesetId) + { + yield break; // Exit immediately without waiting a frame + } + public virtual void OnDestroy() { diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/TypeMemoryCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/TypeMemoryCache.cs index fc0886034..877c1acc4 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/TypeMemoryCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/TypeMemoryCache.cs @@ -87,12 +87,14 @@ public void Remove(CanonicalTileId tileId) { if (_active.TryGetValue(tileId, out var data)) { + data.Dispose(); _active.Remove(tileId); return; } if (_inactiveMap.TryGetValue(tileId, out var tuple)) { + tuple.Value.value.Dispose(); _inactiveMap.Remove(tileId); _inactiveList.Remove(tuple); return; @@ -100,6 +102,7 @@ public void Remove(CanonicalTileId tileId) if (_fallbackDatas.TryGetValue(tileId, out var fallback)) { + fallback.Dispose(); _fallbackDatas.Remove(tileId); } } @@ -159,6 +162,16 @@ public void MarkFallback(CanonicalTileId dataTileId) _fallbackDatas.Add(dataTileId, tuple.Value.value); } } + + public void ClearInactive() + { + foreach (var tileData in _inactiveList) + { + tileData.value.Dispose(); + } + _inactiveMap.Clear(); + _inactiveList.Clear(); + } public IEnumerable GetAllDatas() { @@ -200,6 +213,9 @@ public void OnDestroy() public int ActiveCount => _active.Count; public int InactiveCount => _inactiveMap.Count; + + public Dictionary GetActiveData => _active; + public Dictionary GetFallbackData => _fallbackDatas; } public interface ITypeCache diff --git a/Runtime/Mapbox/ImageModule/StaticApiLayerModule.cs b/Runtime/Mapbox/ImageModule/StaticApiLayerModule.cs index bce329f02..a09243938 100644 --- a/Runtime/Mapbox/ImageModule/StaticApiLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/StaticApiLayerModule.cs @@ -15,6 +15,7 @@ public class StaticApiLayerModule : ILayerModule { protected StaticLayerModuleSettings _settings; protected Source _rasterSource; + public Source RasterSource => _rasterSource; private HashSet _retainedTiles; public StaticApiLayerModule(Source source, StaticLayerModuleSettings settings) : base() @@ -79,6 +80,11 @@ public void UpdatePositioning(IMapInformation mapInfo) { } + + public IEnumerator ChangeTilesetId(string tilesetId) + { + yield return _rasterSource.ChangeTilesetId(tilesetId); + } public virtual void OnDestroy() { diff --git a/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs index 9068febbd..0b1693bbd 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs @@ -32,7 +32,7 @@ protected ImageSource(DataFetchingManager dataFetchingManager, MapboxCacheManage CacheItemDisposed(t); }; } - + public override void LoadTile(CanonicalTileId requestedDataTileId) { LoadTileCore(requestedDataTileId); @@ -127,7 +127,34 @@ public virtual void ClearMemoryCache() { _memoryCache.OnDestroy(); } + + public IEnumerator ReloadTiles() + { + // IMPORTANT: Materialize keys BEFORE starting any coroutines to avoid + // InvalidOperationException: Collection was modified during enumeration. + // RefreshData modifies the cache (Remove/Add) while WaitForAll() would be + // lazily enumerating the live dictionaries if we didn't snapshot them first. + var activeKeys = _memoryCache.GetActiveData.Keys.ToList(); + var fallbackKeys = _memoryCache.GetFallbackData.Keys.ToList(); + + var coroutines = activeKeys.Select(key => RefreshData(key)); + coroutines = coroutines.Concat(fallbackKeys.Select(key => RefreshData(key, data => + { + _memoryCache.MarkFallback(key); + }))); + + yield return coroutines.WaitForAll(); + _memoryCache.ClearInactive(); + } + public override IEnumerator ChangeTilesetId(string tilesetId) + { + _settings.TilesetId = tilesetId; + _tilesetId = _settings.TilesetId; + yield return Initialize(); + yield return ReloadTiles(); + } + public override void OnDestroy() { base.OnDestroy(); @@ -143,60 +170,114 @@ public override void OnDestroy() //COROUTINE METHODS only used in initialization so far #region coroutines - public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileId, Action callback = null) + + /// + /// Shared fetch logic for tile data following the cache-check → file-cache → web-request fallback chain. + /// + /// Tile ID to fetch + /// If true, checks memory cache before starting fetch chain + /// If true, removes existing cache entry before adding new data + /// Callback invoked with result (can be null) + private IEnumerator FetchTileDataCoroutine( + CanonicalTileId requestedDataTileId, + bool checkMemoryCacheFirst, + bool clearExistingCache, + Action callback = null) { T resultData = null; - if (GetInstantData(requestedDataTileId, out resultData)) + + // STEP 1: Check memory cache if requested (LoadTileCoroutine does this, RefreshData doesn't) + if (checkMemoryCacheFirst && GetInstantData(requestedDataTileId, out resultData)) { + callback?.Invoke(resultData); + yield break; } - else if (_waitingList.ContainsKey(requestedDataTileId)) + + // STEP 2: If already being fetched, wait for completion + if (_waitingList.ContainsKey(requestedDataTileId)) { while(_waitingList.ContainsKey(requestedDataTileId)) { yield return null; } GetInstantData(requestedDataTileId, out resultData); + callback?.Invoke(resultData); + yield break; } - else - { - _waitingList[requestedDataTileId] = null; - yield return GetImageCoroutine(requestedDataTileId, _tilesetId, _settings.UseNonReadableTextures, - (data) => - { - resultData = data; - _waitingList.Remove(requestedDataTileId); - - if (resultData != null) - { - data.CacheType = CacheType.FileCache; - _memoryCache.Add(data); - CheckExpiration(data); - } - }); - if (resultData == null) + // STEP 3: Try file cache + _waitingList[requestedDataTileId] = null; + yield return GetImageCoroutine(requestedDataTileId, _tilesetId, _settings.UseNonReadableTextures, + (data) => { - var dataTile = CreateTile(requestedDataTileId, _tilesetId); - _waitingList[requestedDataTileId] = dataTile; - var working = true; - WebRequestData(dataTile, (fetchingResult) => + resultData = data; + _waitingList.Remove(requestedDataTileId); + + if (resultData != null) { - _waitingList.Remove(requestedDataTileId); - if (dataTile.CurrentTileState == TileState.Loaded) - { - resultData = TextureFromWebForCoroutine(dataTile); - } - working = false; - }); - while (working) + data.CacheType = CacheType.FileCache; + + // Clear existing cache if requested (RefreshData does this) + if (clearExistingCache && _memoryCache.Exists(requestedDataTileId)) + _memoryCache.Remove(requestedDataTileId); + + _memoryCache.Add(data); + CheckExpiration(data); + } + }); + + // STEP 4: If not in file cache, fetch from web + if (resultData == null) + { + var dataTile = CreateTile(requestedDataTileId, _tilesetId); + _waitingList[requestedDataTileId] = dataTile; + var working = true; + + WebRequestData(dataTile, (fetchingResult) => + { + _waitingList.Remove(requestedDataTileId); + + if (dataTile.CurrentTileState == TileState.Loaded) { - yield return null; + // Clear existing cache entry if requested (for RefreshData) + if (clearExistingCache && _memoryCache.Exists(requestedDataTileId)) + _memoryCache.Remove(requestedDataTileId); + + // Process web response using shared method + resultData = TextureFromWebForCoroutine(dataTile); } + + working = false; + }); + + while (working) + { + yield return null; } } - + callback?.Invoke(resultData); } + + public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileId, Action callback = null) + { + yield return FetchTileDataCoroutine( + requestedDataTileId, + checkMemoryCacheFirst: true, + clearExistingCache: false, + callback + ); + } + + private IEnumerator RefreshData(CanonicalTileId requestedDataTileId, Action callback = null) + { + yield return FetchTileDataCoroutine( + requestedDataTileId, + checkMemoryCacheFirst: false, + clearExistingCache: true, + callback + ); + } public override IEnumerator LoadTilesCoroutine(IEnumerable retainedTiles, Action> callback = null) { From 20b381d112e0d03835083ded2940f1196a7e61a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Thu, 26 Feb 2026 17:14:29 +0300 Subject: [PATCH 05/54] custom image api module and terrain fixes (#1973) * custom image api module and terrain fixes add LoadTileData to Terrain module add mapbox map extensions class with TryGetElevation, TryGetMapTile methods fix map visualizer to check inside composite modules when searching fix a bug where terrain module target tile id method wasn't clamped add custom image api module change unity map service to have public getters for data fetcher and cache manager * fix tile parent loops in mapbox extensions fix namespaces for custom api module * cleanup remove commented out code fix tooltip add nullchecks --- .../Data/Interfaces/ITerrainLayerModule.cs | 4 + .../Map/MapInformationStaticMethods.cs | 13 ++- .../BaseModule/Map/MapboxMapExtensions.cs | 79 ++++++++++++++++ .../Map/MapboxMapExtensions.cs.meta | 3 + .../BaseModule/Map/MapboxMapVisualizer.cs | 8 ++ Runtime/Mapbox/CustomImageryModule.meta | 8 ++ .../CustomApiLayerModuleScript.cs | 55 ++++++++++++ .../CustomApiLayerModuleScript.cs.meta | 3 + .../CustomImageryModule/CustomSource.cs | 58 ++++++++++++ .../CustomImageryModule/CustomSource.cs.meta | 3 + .../CustomSourceSettings.cs | 14 +++ .../CustomSourceSettings.cs.meta | 3 + .../CustomImageryModule/CustomTMSTile.cs | 35 ++++++++ .../CustomImageryModule/CustomTMSTile.cs.meta | 3 + .../CustomTerrainLayerModuleScript.cs | 42 +++++++++ .../CustomTerrainLayerModuleScript.cs.meta | 3 + .../CustomTerrainSource.cs | 52 +++++++++++ .../CustomTerrainSource.cs.meta | 3 + .../Mapbox/CustomImageryModule/Editor.meta | 8 ++ .../CustomApiLayerModuleScriptEditor.cs | 46 ++++++++++ .../CustomApiLayerModuleScriptEditor.cs.meta | 11 +++ .../MapboxCustomImageryModule.Editor.asmdef | 18 ++++ ...pboxCustomImageryModule.Editor.asmdef.meta | 7 ++ .../MapboxCustomImageryModule.asmdef | 18 ++++ .../MapboxCustomImageryModule.asmdef.meta | 7 ++ .../ImageModule/Terrain/TerrainLayerModule.cs | 89 ++++++++++++++----- .../DataSources/ImageSource.cs | 2 +- .../DataSources/TerrainSource.cs | 2 +- .../Mapbox/UnityMapService/MapUnityService.cs | 21 +++-- Tests/Runtime/BaseModule/MapboxMapTests.cs | 48 ++++++++++ 30 files changed, 632 insertions(+), 34 deletions(-) create mode 100644 Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs create mode 100644 Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta create mode 100644 Runtime/Mapbox/CustomImageryModule.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomSource.cs create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomSource.cs.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs create mode 100644 Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/Editor.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs create mode 100644 Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef create mode 100644 Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef.meta create mode 100644 Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef create mode 100644 Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef.meta diff --git a/Runtime/Mapbox/BaseModule/Data/Interfaces/ITerrainLayerModule.cs b/Runtime/Mapbox/BaseModule/Data/Interfaces/ITerrainLayerModule.cs index ef892eb4c..032a06ae1 100644 --- a/Runtime/Mapbox/BaseModule/Data/Interfaces/ITerrainLayerModule.cs +++ b/Runtime/Mapbox/BaseModule/Data/Interfaces/ITerrainLayerModule.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections; +using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Tiles; namespace Mapbox.BaseModule.Data.Interfaces @@ -5,5 +8,6 @@ namespace Mapbox.BaseModule.Data.Interfaces public interface ITerrainLayerModule : ILayerModule { float QueryElevation(CanonicalTileId tileId, float x, float y); + IEnumerator LoadTileData(CanonicalTileId tileId, Action callback = null); } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Map/MapInformationStaticMethods.cs b/Runtime/Mapbox/BaseModule/Map/MapInformationStaticMethods.cs index e3a6a2884..1f6f28e78 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapInformationStaticMethods.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapInformationStaticMethods.cs @@ -30,12 +30,21 @@ public static Vector3 ConvertLatLngToPositionForScale(this IMapInformation mapIn return scaledDeltaMercatorVector3; } - public static Vector3 ConvertLatLngToPosition(this IMapInformation mapInfo, LatitudeLongitude latlng) + public static Vector3 ConvertLatLngToPosition(this IMapInformation mapInfo, LatitudeLongitude latlng, bool queryElevation = false) { var mercator = Conversions.LatitudeLongitudeToWebMercator(latlng); var deltaMercator = mercator - mapInfo.CenterMercator; var scaledDeltaMercator = deltaMercator / mapInfo.Scale; - var scaledDeltaMercatorVector3 = scaledDeltaMercator.ToVector3xz(); + var elevation = 0f; + + if (queryElevation) + { + var tileId = Conversions.LatitudeLongitudeToTileId(latlng, 14).Canonical; + var pointPosition = Conversions.LatitudeLongitudeToInTile01(latlng, tileId); + elevation = mapInfo.QueryElevation(tileId, pointPosition.x, pointPosition.y); + } + + var scaledDeltaMercatorVector3 = new Vector3((float)scaledDeltaMercator.x, elevation, (float)scaledDeltaMercator.y); return scaledDeltaMercatorVector3; } diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs new file mode 100644 index 000000000..2930981bb --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs @@ -0,0 +1,79 @@ +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; + +namespace Mapbox.BaseModule.Map +{ + public static class MapboxMapExtensions + { + public static bool TryGetMapTile(this MapboxMap map, UnwrappedTileId tileId, out UnityMapTile tile) + { + if (map.MapVisualizer.ActiveTiles.TryGetValue(tileId, out tile)) + { + return true; + } + + tile = null; + return false; + } + + public static bool TryGetMapTile(this MapboxMap map, LatitudeLongitude coordinates, out UnityMapTile tile, int maxZoomLevel = 20) + { + var maxTileId = Conversions.LatitudeLongitudeToTileId(coordinates, maxZoomLevel); + for (int i = maxZoomLevel; i > 1; i--) + { + if (map.MapVisualizer.ActiveTiles.TryGetValue(maxTileId, out tile)) + { + return true; + } + else + { + maxTileId = maxTileId.Parent; + } + } + + tile = null; + return false; + } + + /// + /// Only queries active tiles / visible world + /// + /// + /// + /// + /// + /// + /// + public static bool TryGetElevation(this MapboxMap map, LatitudeLongitude location, out float elevation, int maxZoomLevel = 20, int minZoomLevel = 2) + { + var maxTileId = Conversions.LatitudeLongitudeToTileId(location, maxZoomLevel); + UnityMapTile tile = null; + for (int i = maxZoomLevel; i >= minZoomLevel; i--) + { + if (map.MapVisualizer.ActiveTiles.TryGetValue(maxTileId, out tile)) + { + break; + } + else + { + maxTileId = maxTileId.Parent; + } + } + + // Check tile and nested properties for null to prevent NullReferenceException + if (tile != null && + tile.TerrainContainer != null && + tile.TerrainContainer.TerrainData != null) + { + var tilePos = Conversions.LatitudeLongitudeToInTile01(location, tile.TerrainContainer.TerrainData.TileId); + elevation = tile.TerrainContainer.QueryHeightData(tilePos.x, tilePos.y); + return true; + } + + elevation = float.MinValue; + return false; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta new file mode 100644 index 000000000..8ca4b59f3 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3d874d1186584faf95af2fa2761ddcbb +timeCreated: 1767092724 \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 681dc1f4e..3c04f2164 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -194,6 +194,14 @@ public void OnDestroy() public bool TryGetLayerModule(out T module) where T : ILayerModule { module = (T)LayerModules.FirstOrDefault(x => x is T); + if (module == null) + { + var composite = LayerModules.FirstOrDefault(x => x is CompositeLayerModule) as CompositeLayerModule; + if (composite != null) + { + module = (T)composite.LayerModules.FirstOrDefault(x => x is T); + } + } return module != null; } diff --git a/Runtime/Mapbox/CustomImageryModule.meta b/Runtime/Mapbox/CustomImageryModule.meta new file mode 100644 index 000000000..ee0e1a460 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9957d55bdc494407c87f742bb99a0cc8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs b/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs new file mode 100644 index 000000000..007c55f86 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs @@ -0,0 +1,55 @@ +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.ImageModule; +using Mapbox.UnityMapService; +using UnityEngine; + +namespace Mapbox.CustomImageryModule +{ + public class CustomApiLayerModuleScript : ModuleConstructorScript + { + public CustomSourceSettings CustomSourceSettings; + public StaticLayerModuleSettings Settings = new StaticLayerModuleSettings() + { + RejectTilesOutsideZoom = new Vector2(2, 25), + DataSettings = new ImageSourceSettings() + { + ClampDataLevelToMax = 25 + } + }; + public override ILayerModule ModuleImplementation { get; protected set; } + + private void Start() + { + + } + + public override ILayerModule ConstructModule(MapService service, IMapInformation mapInformation, + UnityContext unityContext) + { + if (Settings.SourceType == ImagerySourceType.None) + { + + } + else if (Settings.SourceType == ImagerySourceType.Custom) + { + Settings.DataSettings.TilesetId = Settings.CustomSourceId; + } + else + { + var imageryTileset = MapboxDefaultImagery.GetParameters(Settings.SourceType); + Settings.DataSettings.TilesetId = imageryTileset.Id; + } + + var unityService = service as MapUnityService; + var source = new CustomSource(CustomSourceSettings, unityService.FetchingManager, unityService.CacheManager, new ImageSourceSettings() + { + TilesetId = "CustomImagery" + }); + ModuleImplementation = new StaticApiLayerModule(source, Settings); + return ModuleImplementation; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs.meta new file mode 100644 index 000000000..64db1dc66 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6aed0ab09c9749069b3359d261d35ae4 +timeCreated: 1767099820 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSource.cs b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs new file mode 100644 index 000000000..ee4c1efc8 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs @@ -0,0 +1,58 @@ +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.UnityMapService.DataSources; + +namespace Mapbox.CustomImageryModule +{ + public class CustomSource : ImageSource + { + protected CustomSourceSettings _customSourceSettings; + protected ImageSourceSettings _settings; + + public CustomSource(CustomSourceSettings customSourceSettings, DataFetchingManager dataFetchingManager, + MapboxCacheManager mapboxCacheManager, ImageSourceSettings settings) + : base(dataFetchingManager, mapboxCacheManager, settings) + { + _settings = settings; + _customSourceSettings = customSourceSettings; + } + + protected override RasterTile CreateTile(CanonicalTileId tileId, string tilesetId) + { + if (_customSourceSettings.InvertY) + { + return new CustomTMSTile(_customSourceSettings.UrlFormat, tileId, tilesetId, true); + } + else + { + return new RasterTile(tileId, tilesetId, true); + } + } + + protected override RasterData CreateRasterDataWrapper(RasterTile tile) + { + RasterData rasterData; + rasterData = new RasterData() + { + TileId = tile.Id, + TilesetId = tile.TilesetId, + Texture = tile.Texture2D, + CacheType = tile.FromCache, + Data = tile.Data, + ETag = tile.ETag, + ExpirationDate = tile.ExpirationDate + }; + + return rasterData; + } + + protected override void CheckExpiration(RasterData cacheItem) + { + //not checking for expiration and updating for custom sources + //just using whatever is in cache + return; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSource.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs.meta new file mode 100644 index 000000000..5e19cd35a --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: db00866e0fd14890ad560a64229aae45 +timeCreated: 1767707026 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs new file mode 100644 index 000000000..f2ac463b9 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs @@ -0,0 +1,14 @@ +using System; +using UnityEngine; + +namespace Mapbox.CustomImageryModule +{ + [Serializable] + public class CustomSourceSettings + { + [Tooltip("Url format string, structured as C# string format with '{}' fields for X/Y/Z coordinates. {0}=Z, {1}=X, {2}=Y")] + public string UrlFormat; + [Tooltip("Invert Y axis coordinates for TMS coordinate system, which starts from bottom left and grows to top-right")] + public bool InvertY; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs.meta new file mode 100644 index 000000000..cd1886295 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 83855a3135724ccd938059a88012027d +timeCreated: 1767707253 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs new file mode 100644 index 000000000..8f3b789f0 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs @@ -0,0 +1,35 @@ +using System; +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Platform; +using Mapbox.BaseModule.Data.Tiles; +using UnityEngine; + +namespace Mapbox.CustomImageryModule +{ + /// + /// TMS tile in the name means Y axis is inverted in the tile ids + /// + public class CustomTMSTile : RasterTile + { + private string _urlFormat; + public CustomTMSTile(string urlFormat, CanonicalTileId tileId, string tilesetId, bool useNonReadableTexture) : base(tileId, tilesetId, useNonReadableTexture) + { + _urlFormat = urlFormat; + } + + public override void Initialize(IFileSource fileSource, Action p) + { + TileState = TileState.Loading; + _callback = p; + + var invertY = (Mathf.Pow(2, Id.Z))- Id.Y - 1; + _generatedUrl = string.Format(_urlFormat, Id.Z, Id.X, (int)invertY); + DoTheRequest(fileSource); + } + + protected override void DoTheRequest(IFileSource fileSource) + { + _webRequest = fileSource.CustomImageRequest(_generatedUrl, HandleTileResponse, ETag, 10, IsTextureNonreadable); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs.meta new file mode 100644 index 000000000..25a8c0e9f --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f1f46f6d333e4d25a4ccd278b71cecdc +timeCreated: 1767706943 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs b/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs new file mode 100644 index 000000000..8087393b9 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs @@ -0,0 +1,42 @@ +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.ImageModule.Terrain; +using Mapbox.UnityMapService; +using UnityEngine; + +namespace Mapbox.CustomImageryModule +{ + public class CustomTerrainLayerModuleScript : ModuleConstructorScript + { + public CustomSourceSettings CustomSourceSettings; + public TerrainLayerModuleSettings Settings = new TerrainLayerModuleSettings() + { + RejectTilesOutsideZoom = new Vector2(2, 25), + DataSettings = new ImageSourceSettings() + { + ClampDataLevelToMax = 25 + } + }; + public override ILayerModule ModuleImplementation { get; protected set; } + + private void Start() + { + + } + + public override ILayerModule ConstructModule(MapService service, IMapInformation mapInformation, + UnityContext unityContext) + { + Settings.DataSettings.TilesetId = "CustomTerrain"; + + var unityService = service as MapUnityService; + var source = new CustomTerrainSource(CustomSourceSettings, unityService.FetchingManager, unityService.CacheManager, new ImageSourceSettings() + { + TilesetId = "CustomTerrain" + }); + ModuleImplementation = new TerrainLayerModule(source, Settings); + return ModuleImplementation; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs.meta new file mode 100644 index 000000000..ccdbd7c1a --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 83ddcee6d2844b7e9f15508beb22c72b +timeCreated: 1767632785 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs new file mode 100644 index 000000000..eb45d275f --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs @@ -0,0 +1,52 @@ +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.UnityMapService.DataSources; + +namespace Mapbox.CustomImageryModule +{ + public class CustomTerrainSource : TerrainSource + { + protected CustomSourceSettings _customSourceSettings; + protected ImageSourceSettings _settings; + + public CustomTerrainSource(CustomSourceSettings customSettings, DataFetchingManager dataFetchingManager, MapboxCacheManager mapboxCacheManager, ImageSourceSettings settings) + : base(dataFetchingManager, mapboxCacheManager, settings) + { + _customSourceSettings = customSettings; + _settings = settings; + } + + protected override RasterTile CreateTile(CanonicalTileId tileId, string tilesetId) + { + if (_customSourceSettings.InvertY) + { + return new CustomTMSTile( + _customSourceSettings.UrlFormat, + tileId, tilesetId, true); + } + else + { + return new RasterTile(tileId, tilesetId, true); + } + } + + protected override TerrainData CreateRasterDataWrapper(RasterTile tile) + { + TerrainData rasterData; + rasterData = new TerrainData() + { + TileId = tile.Id, + TilesetId = tile.TilesetId, + Texture = tile.Texture2D, + CacheType = tile.FromCache, + Data = tile.Data, + ETag = tile.ETag, + ExpirationDate = tile.ExpirationDate + }; + + return rasterData; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs.meta new file mode 100644 index 000000000..29c8d3b8c --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfc3281b14f542aa8d2164cabb8967c7 +timeCreated: 1767707245 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/Editor.meta b/Runtime/Mapbox/CustomImageryModule/Editor.meta new file mode 100644 index 000000000..68a67e75e --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0b6b51283dd7d417a962d32657e3e2f5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs b/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs new file mode 100644 index 000000000..5b05645c1 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs @@ -0,0 +1,46 @@ +using UnityEditor; + +namespace Mapbox.CustomImageryModule.Editor +{ + [CustomEditor(typeof(CustomApiLayerModuleScript))] + public class CustomApiLayerModuleScriptEditor : UnityEditor.Editor + { + SerializedProperty customSourceSettingsProp; + + SerializedProperty rejectTilesOutsideZoomProp; + SerializedProperty clampDataLevelToMaxProp; + + void OnEnable() + { + // Root properties + customSourceSettingsProp = serializedObject.FindProperty("CustomSourceSettings"); + + // Settings.RejectTilesOutsideZoom + rejectTilesOutsideZoomProp = + serializedObject.FindProperty("Settings") + .FindPropertyRelative("RejectTilesOutsideZoom"); + + // Settings.DataSettings.ClampDataLevelToMax + clampDataLevelToMaxProp = + serializedObject.FindProperty("Settings") + .FindPropertyRelative("DataSettings") + .FindPropertyRelative("ClampDataLevelToMax"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.LabelField("Custom Source Settings", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(customSourceSettingsProp, true); + + EditorGUILayout.Space(); + + EditorGUILayout.LabelField("Tile Settings", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(rejectTilesOutsideZoomProp); + EditorGUILayout.PropertyField(clampDataLevelToMaxProp); + + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs.meta b/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs.meta new file mode 100644 index 000000000..e758ce0e4 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c31619c32fa2549edbbc6761da29703b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef new file mode 100644 index 000000000..bc263dd76 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "MapboxCustomImageryModule.Editor", + "rootNamespace": "", + "references": [ + "GUID:60dcad938ceef450e9b8d773e5c61b99" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef.meta b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef.meta new file mode 100644 index 000000000..76a5b8ec5 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4c80974ccb411472e8e926051beeec6f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef b/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef new file mode 100644 index 000000000..34eca74fd --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef @@ -0,0 +1,18 @@ +{ + "name": "MapboxCustomImageryModule", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:36ca6af6e2b304d4090888554d6ce199", + "GUID:5ff00896142b94bc7a58a7c72b2faf57" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef.meta b/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef.meta new file mode 100644 index 000000000..3e2d8598f --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 60dcad938ceef450e9b8d773e5c61b99 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index 310f1f4c5..a018bbd23 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -5,8 +5,10 @@ using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; using Mapbox.ImageModule.Terrain.TerrainStrategies; using UnityEngine; using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; @@ -91,22 +93,6 @@ public virtual bool RetainTiles(HashSet retainedTiles) isReady = _rasterSource.RetainTiles(_retainedTerrainTiles); return isReady; } - - public float QueryElevation(CanonicalTileId tileId, float x, float y) - { - var originalTileId = tileId; - var targetTileId = tileId; - for (int i = 0; i < 5; i++) - { - if (_rasterSource.GetInstantData(targetTileId, out var instantData)) - { - return instantData.QueryHeightData(originalTileId, x, y); - } - targetTileId.MoveToParent(); - } - - return 0; - } public void UpdatePositioning(IMapInformation mapInfo) { @@ -120,7 +106,7 @@ public void OnDestroy() //COROUTINE METHODS only used in initialization so far #region coroutine methods - public virtual IEnumerator LoadTileData(CanonicalTileId tileId, Action callback = null) + public virtual IEnumerator LoadTileData(CanonicalTileId tileId, Action callback = null) { return _rasterSource.LoadTileCoroutine(tileId, callback); } @@ -133,13 +119,11 @@ public virtual IEnumerator LoadTiles(IEnumerable tiles) public IEnumerable GetTileCoverCoroutines(IEnumerable tiles) { var targetTiles = GetDataId(tiles).Distinct(); - return targetTiles.Select(x => _rasterSource.LoadTileCoroutine(x)).Where(x => x != null); + return targetTiles.Select(x => LoadTileData(x)).Where(x => x != null); } #endregion - - //PRIVATE METHODS private bool IsZinSupportedRange(int targetZ) { @@ -150,7 +134,7 @@ private CanonicalTileId GetDataId(CanonicalTileId tileId) { var maxZoom = _settings.DataSettings.ClampDataLevelToMax; var currentZ = tileId.Z; - var targetZ = currentZ - 2; + var targetZ = (int)Mathf.Max(currentZ - 2, _settings.RejectTilesOutsideZoom.x); if (targetZ >= maxZoom) { return tileId.ParentAt(maxZoom); @@ -161,10 +145,73 @@ private CanonicalTileId GetDataId(CanonicalTileId tileId) } } + + //API + + /// + /// This method will only search the memory cache for available data. + /// + /// TileId of the elevation data + /// Point in texture coordinate space, origin is bottom left. + /// Point in texture coordinate space, origin is bottom left. + /// + public float QueryElevation(CanonicalTileId tileId, float x, float y) + { + var originalTileId = tileId; + var targetTileId = tileId; + for (int i = 0; i < 5; i++) + { + if (_rasterSource.GetInstantData(targetTileId, out var instantData)) + { + return instantData.QueryHeightData(originalTileId, x, y); + } + targetTileId.MoveToParent(); + } + + return 0; + } + public IEnumerable GetDataId(IEnumerable tileIdList) { return tileIdList.Where(x => IsZinSupportedRange(x.Z)).Select(GetDataId).Distinct(); } + /// + /// This method will cause tile requests if data isn't already available on memory cache. Use with caution. + /// + /// Latitude longitude of the location you want to query + /// Callback method returning the elevation in float + /// + public IEnumerator GetElevationData(LatitudeLongitude latLng, Action callback = null) + { + var tileId = Conversions.LatitudeLongitudeToTileId(latLng, 14).Canonical; + var dataFound = false; + for (int i = 14; i >= 2; i--) + { + if (_rasterSource.GetInstantData(tileId, out var instantData)) + { + var tilePos = Conversions.LatitudeLongitudeToInTile01(latLng, instantData.TileId); + var elevation = instantData.QueryHeightData(tilePos); + callback?.Invoke(elevation); + dataFound = true; + break; + } + else + { + tileId = tileId.GetParentTileId; + } + } + + if (!dataFound) + { + tileId = Conversions.LatitudeLongitudeToTileId(latLng, 14).Canonical; + yield return LoadTileData(tileId, terrainData => + { + var tilePos = Conversions.LatitudeLongitudeToInTile01(latLng, terrainData.TileId); + var elevation = terrainData.QueryHeightData(tilePos); + callback?.Invoke(elevation); + }); + } + } } } \ No newline at end of file diff --git a/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs index 0b1693bbd..c939137c8 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs @@ -443,7 +443,7 @@ protected void BackgroundLoad(CanonicalTileId tileId, string tilesetId) }); } - private void CheckExpiration(T cacheItem) + protected virtual void CheckExpiration(T cacheItem) { var dataTask = ReadEtagExpiration(cacheItem, 4); if (dataTask != null) //can be null if sqlite cache isn't available diff --git a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs index 2e275d0f0..885caaa60 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs @@ -112,7 +112,7 @@ public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileI { terrainData = data; })); - if (terrainData != null) + if (terrainData != null && terrainData.Texture != null) { yield return Runnable.Instance.StartCoroutine(ExtractElevationValues(terrainData)); } diff --git a/Runtime/Mapbox/UnityMapService/MapUnityService.cs b/Runtime/Mapbox/UnityMapService/MapUnityService.cs index a7327c719..12d884f2d 100644 --- a/Runtime/Mapbox/UnityMapService/MapUnityService.cs +++ b/Runtime/Mapbox/UnityMapService/MapUnityService.cs @@ -24,7 +24,10 @@ public sealed class MapUnityService : MapService, IUnityMapService private MapboxCacheManager _cacheManager; private DataFetchingManager _fetchingManager; - public override IFileSource FileSource => _fetchingManager; + public override IFileSource FileSource => FetchingManager; + + public MapboxCacheManager CacheManager => _cacheManager; + public DataFetchingManager FetchingManager => _fetchingManager; public MapUnityService( UnityContext unityContext, @@ -63,40 +66,40 @@ public override Source GetNewRasterSource(string name, string tilese public override Source GetTerrainRasterSource(ImageSourceSettings settings) { - var terrainSource = new TerrainSource(_fetchingManager, _cacheManager, settings); + var terrainSource = new TerrainSource(FetchingManager, CacheManager, settings); _dataSources.Add(terrainSource); return terrainSource; } public override Source GetStaticRasterSource(ImageSourceSettings settings) { - var staticRasterSource = new StaticSource(_fetchingManager, _cacheManager, settings); + var staticRasterSource = new StaticSource(FetchingManager, CacheManager, settings); _dataSources.Add(staticRasterSource); return staticRasterSource; } public override Source GetVectorSource(VectorSourceSettings settings) { - var vectorSource = new VectorSource(_fetchingManager, _cacheManager, settings); + var vectorSource = new VectorSource(FetchingManager, CacheManager, settings); _dataSources.Add(vectorSource); return vectorSource; } public override Source GetBuildingSource(VectorSourceSettings settings) { - var vectorSource = new BuildingSource(_fetchingManager, _cacheManager, settings); + var vectorSource = new BuildingSource(FetchingManager, CacheManager, settings); _dataSources.Add(vectorSource); return vectorSource; } - public MapboxCacheManager GetCacheManager() => _cacheManager; - public DataFetchingManager GetFetchingManager() => _fetchingManager; + public MapboxCacheManager GetCacheManager() => CacheManager; + public DataFetchingManager GetFetchingManager() => FetchingManager; public override void OnDestroy() { base.OnDestroy(); - _fetchingManager.OnDestroy(); - _cacheManager.OnDestroy(); + FetchingManager.OnDestroy(); + CacheManager.OnDestroy(); } } } diff --git a/Tests/Runtime/BaseModule/MapboxMapTests.cs b/Tests/Runtime/BaseModule/MapboxMapTests.cs index 95cd90dd5..cddecd05e 100644 --- a/Tests/Runtime/BaseModule/MapboxMapTests.cs +++ b/Tests/Runtime/BaseModule/MapboxMapTests.cs @@ -9,6 +9,7 @@ using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; +using Mapbox.ImageModule.Terrain; using Mapbox.ImageModule.Terrain.TerrainStrategies; using Mapbox.MapDebug.Scripts.Logging; using Mapbox.UnityMapService; @@ -62,6 +63,12 @@ public void OneTimeSetUp() _map = new MapboxMap(mapInfo, unityContext, mapService); var mapVisualizer = new MapboxMapVisualizer(mapInfo, unityContext, new TileCreator(unityContext)); + mapVisualizer.LayerModules.Add( + new TerrainLayerModule(mapService.GetTerrainRasterSource( + new ImageSourceSettings() + { + TilesetId = MapboxDefaultElevation.GetParameters(ElevationSourceType.MapboxTerrain).Id + }), new TerrainLayerModuleSettings())); _map.MapVisualizer = mapVisualizer; } @@ -148,6 +155,47 @@ public void ChangeViewToSF() Assert.AreEqual(_map.MapInformation.Pitch, 45); Assert.AreEqual(_map.MapInformation.Bearing, 30); } + + + private static IEnumerable LatLngElevationsource + { + get + { + yield return new TestCaseData(new LatitudeLongitude(27.9878, 86.9250), 8848f).Returns(null); // Mount Everest summit, Nepal/China + yield return new TestCaseData(new LatitudeLongitude(35.3606, 138.7274), 3776f).Returns(null); // Mount Fuji, Japan + yield return new TestCaseData(new LatitudeLongitude(36.5786, -118.2923), 4421f).Returns(null); // Mount Whitney, USA + yield return new TestCaseData(new LatitudeLongitude(51.1789, -1.8262), 102f).Returns(null); // Stonehenge, UK + + yield return new TestCaseData(new LatitudeLongitude(46.8523, -121.7603), 4372f).Returns(null); // Mount Rainier, USA + yield return new TestCaseData(new LatitudeLongitude(-13.1631, -72.5450), 2430f).Returns(null); // Machu Picchu, Peru + yield return new TestCaseData(new LatitudeLongitude(19.8207, -155.4681), 4207f).Returns(null); // Mauna Kea, USA + + yield return new TestCaseData(new LatitudeLongitude(-25.3444, 131.0369), 863f).Returns(null); // Uluru, Australia + yield return new TestCaseData(new LatitudeLongitude(43.6532, -79.3832), 92f).Returns(null); // Toronto, Canada + yield return new TestCaseData(new LatitudeLongitude(55.7558, 37.6176), 156f).Returns(null); // Moscow, Russia + yield return new TestCaseData(new LatitudeLongitude(35.6895, 139.6917), 38f).Returns(null); // Tokyo, Japan + yield return new TestCaseData(new LatitudeLongitude(-34.6037, -58.3816), 25f).Returns(null); // Buenos Aires, Argentina + + yield return new TestCaseData(new LatitudeLongitude(51.5074, -0.1278), 18f).Returns(null); // London, UK + yield return new TestCaseData(new LatitudeLongitude(37.7749, -122.4194), 16f).Returns(null); // San Francisco, USA + yield return new TestCaseData(new LatitudeLongitude(28.6139, 77.2090), 216f).Returns(null); // New Delhi, India + yield return new TestCaseData(new LatitudeLongitude(41.9028, 12.4964), 52f).Returns(null); // Rome, Italy + yield return new TestCaseData(new LatitudeLongitude(19.4326, -99.1332), 2240f).Returns(null); // Mexico City, Mexico + } + } + + [UnityTest] + [TestCaseSource(nameof(LatLngElevationsource))] + public IEnumerator TestElevations(LatitudeLongitude latLng, float expectedElevation) + { + if (_map.MapVisualizer.TryGetLayerModule(out var terrainModule)) + { + yield return terrainModule.GetElevationData(latLng, (elevation) => + { + Assert.AreEqual(elevation, expectedElevation, expectedElevation * 0.1f, $"{latLng} : {elevation} - {expectedElevation}"); + }); + } + } } public class DataFetcherTests From fcd8ad22f229f149fd2e01157fd14b856823ed10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Thu, 26 Feb 2026 18:37:45 +0300 Subject: [PATCH 06/54] Directions tests (#1978) * add clear caches method to map service and map script add clear caches method to map service and map script # Conflicts: # Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs # Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta * fix world map demo scene for terrain changes fix the world map demo scene settings to work better with latest terrain module changes * add directions api tests generated by claude * add null check and fix an iteration bug * fix a typo in map unity service --- Editor/MapboxConfigurationWindow.cs | 16 +- .../Data/Platform/Cache/ISqliteCache.cs | 1 + .../Data/Platform/Cache/MapboxCacheManager.cs | 7 +- .../Platform/Cache/SQLiteCache/SqliteCache.cs | 3 + .../BaseModule/Data/Platform/Response.cs | 2 +- Runtime/Mapbox/BaseModule/Map/MapService.cs | 5 + .../BaseModule/Map/MapboxMapExtensions.cs | 5 + .../Map/MapboxMapExtensions.cs.meta | 10 +- .../BaseModule/Utilities/PolylineUtils.cs | 5 + .../Mapbox/UnityMapService/MapUnityService.cs | 5 + Samples~/WorldMapViewer/Map.unity | 3 +- Tests/Editor/DirectionsApi.meta | 8 + .../DirectionsApi/BearingFilterTests.cs | 145 +++++++ .../DirectionsApi/BearingFilterTests.cs.meta | 11 + .../DirectionsApi/DirectionResourceTests.cs | 353 ++++++++++++++++++ .../DirectionResourceTests.cs.meta | 11 + .../DirectionsApi/DirectionsResponseTests.cs | 181 +++++++++ .../DirectionsResponseTests.cs.meta | 11 + .../MapboxDirectionsApiTests.asmdef | 26 ++ .../MapboxDirectionsApiTests.asmdef.meta | 7 + .../DirectionsApi/MapboxDirectionsApiTests.cs | 308 +++++++++++++++ .../MapboxDirectionsApiTests.cs.meta | 11 + Tests/Editor/DirectionsApi/OverviewTests.cs | 65 ++++ .../DirectionsApi/OverviewTests.cs.meta | 11 + .../DirectionsApi/RoutingProfileTests.cs | 86 +++++ .../DirectionsApi/RoutingProfileTests.cs.meta | 11 + Tests/Runtime/BaseModule/PlayModeTests.asmdef | 4 +- 27 files changed, 1304 insertions(+), 7 deletions(-) create mode 100644 Tests/Editor/DirectionsApi.meta create mode 100644 Tests/Editor/DirectionsApi/BearingFilterTests.cs create mode 100644 Tests/Editor/DirectionsApi/BearingFilterTests.cs.meta create mode 100644 Tests/Editor/DirectionsApi/DirectionResourceTests.cs create mode 100644 Tests/Editor/DirectionsApi/DirectionResourceTests.cs.meta create mode 100644 Tests/Editor/DirectionsApi/DirectionsResponseTests.cs create mode 100644 Tests/Editor/DirectionsApi/DirectionsResponseTests.cs.meta create mode 100644 Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef create mode 100644 Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef.meta create mode 100644 Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs create mode 100644 Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs.meta create mode 100644 Tests/Editor/DirectionsApi/OverviewTests.cs create mode 100644 Tests/Editor/DirectionsApi/OverviewTests.cs.meta create mode 100644 Tests/Editor/DirectionsApi/RoutingProfileTests.cs create mode 100644 Tests/Editor/DirectionsApi/RoutingProfileTests.cs.meta diff --git a/Editor/MapboxConfigurationWindow.cs b/Editor/MapboxConfigurationWindow.cs index de8d3a5a2..41c185783 100644 --- a/Editor/MapboxConfigurationWindow.cs +++ b/Editor/MapboxConfigurationWindow.cs @@ -3,6 +3,7 @@ using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Utilities; +using Mapbox.UnityMapService; using UnityEditor; using UnityEngine; @@ -324,7 +325,20 @@ public class MapboxMenu [MenuItem("Mapbox/Clear Caches")] public static void DeleteAllCache() { - MapboxCacheManager.DeleteAllCache(); + if (Application.isPlaying) + { + var core = GameObject.FindObjectOfType(); + if (core == null) + { + Debug.LogError("No Map Core found"); + return; + } + core.MapboxMap.ClearCachedData(); + } + else + { + MapboxCacheManager.DeleteAllCache(); + } } } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/ISqliteCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/ISqliteCache.cs index 59d746331..be81d79cb 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/ISqliteCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/ISqliteCache.cs @@ -11,6 +11,7 @@ namespace Mapbox.BaseModule.Data.Platform.Cache public interface ISqliteCache { event Action DataPrunedForFile; + event Action DatabaseCleared; void ReadySqliteDatabase(); bool IsUpToDate(); diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/MapboxCacheManager.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/MapboxCacheManager.cs index 42695b9f3..dec8cbf56 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/MapboxCacheManager.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/MapboxCacheManager.cs @@ -50,6 +50,7 @@ public MapboxCacheManager(UnityContext unityContext, MemoryCache memoryCache, Fi if (_sqLiteCache != null && _textureFileCache != null) { _sqLiteCache.DataPrunedForFile += path => _textureFileCache.DeleteByFileRelativePath(path); + _sqLiteCache.DatabaseCleared += _textureFileCache.ClearAll; } if (_sqLiteCache != null) @@ -69,7 +70,11 @@ public MapboxCacheManager(UnityContext unityContext, MemoryCache memoryCache, Fi } } - + public void ClearCachedData() + { + _sqLiteCache.ClearDatabase(); + _sqLiteCache.ReadySqliteDatabase(); + } public virtual void SaveImage(RasterData textureCacheItem, bool forceInsert) { diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs index 51114a9c6..e5dedea8d 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs @@ -16,6 +16,7 @@ namespace Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache public class SqliteCache : ISqliteCache, IDisposable { public event Action DataPrunedForFile = s => { }; + public event Action DatabaseCleared = () => { }; public const int PruneCacheDelta = 20; @@ -379,6 +380,7 @@ public bool ClearDatabase() return false; } + DatabaseCleared(); return true; } @@ -415,6 +417,7 @@ public bool DeleteSqliteFile() Debug.LogError(error); } + DatabaseCleared(); return isDeletedSuccesfully; } diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Response.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Response.cs index fe2c6e299..075c801fe 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Response.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Response.cs @@ -28,7 +28,7 @@ public class Response { - private Response() { } + public Response() { } public IAsyncRequest Request { get; private set; } diff --git a/Runtime/Mapbox/BaseModule/Map/MapService.cs b/Runtime/Mapbox/BaseModule/Map/MapService.cs index a73041675..35f4fb707 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapService.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapService.cs @@ -44,6 +44,11 @@ public bool IsReady() public bool GetTelemetryCollectionState() => _mapboxContext.GetTelemetryCollectiongState(); public virtual bool SetTelemetryCollectionState(bool state) => _mapboxContext.SetTelemetryCollectionState(state); + public virtual void ClearCachedData() + { + + } + public virtual void OnDestroy() { diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs index 2930981bb..44dfea495 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs @@ -75,5 +75,10 @@ public static bool TryGetElevation(this MapboxMap map, LatitudeLongitude locatio elevation = float.MinValue; return false; } + + public static void ClearCachedData(this MapboxMap map) + { + map.MapService.ClearCachedData(); + } } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta index 8ca4b59f3..bbdcfab69 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: 3d874d1186584faf95af2fa2761ddcbb -timeCreated: 1767092724 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Utilities/PolylineUtils.cs b/Runtime/Mapbox/BaseModule/Utilities/PolylineUtils.cs index d66e6f582..e465676e5 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/PolylineUtils.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/PolylineUtils.cs @@ -25,6 +25,11 @@ public static class PolylineUtils /// List of making up the line. public static List Decode(string encodedPath, int precision = 5) { + if (string.IsNullOrEmpty(encodedPath)) + { + return new List(); + } + int len = encodedPath.Length; double factor = Math.Pow(10, precision); diff --git a/Runtime/Mapbox/UnityMapService/MapUnityService.cs b/Runtime/Mapbox/UnityMapService/MapUnityService.cs index 12d884f2d..2c1005706 100644 --- a/Runtime/Mapbox/UnityMapService/MapUnityService.cs +++ b/Runtime/Mapbox/UnityMapService/MapUnityService.cs @@ -95,6 +95,11 @@ public override Source GetBuildingSource(VectorSourceSettings sett public MapboxCacheManager GetCacheManager() => CacheManager; public DataFetchingManager GetFetchingManager() => FetchingManager; + public override void ClearCachedData() + { + _cacheManager.ClearCachedData(); + } + public override void OnDestroy() { base.OnDestroy(); diff --git a/Samples~/WorldMapViewer/Map.unity b/Samples~/WorldMapViewer/Map.unity index 453848657..553dc6fab 100644 --- a/Samples~/WorldMapViewer/Map.unity +++ b/Samples~/WorldMapViewer/Map.unity @@ -332,7 +332,6 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Settings: - ElevationLayerType: 1 LoadBackgroundTextures: 1 UseShaderTerrain: 1 DataSettings: @@ -340,7 +339,7 @@ MonoBehaviour: UseNonReadableTextures: 1 CacheSize: 100 ClampDataLevelToMax: 14 - RejectTilesOutsideZoom: {x: 10, y: 25} + RejectTilesOutsideZoom: {x: 2, y: 25} ElevationLayerProperties: modificationOptions: SimplificationFactor: 1 diff --git a/Tests/Editor/DirectionsApi.meta b/Tests/Editor/DirectionsApi.meta new file mode 100644 index 000000000..cda42a46a --- /dev/null +++ b/Tests/Editor/DirectionsApi.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6146f86a73872419d960eeed7ee273d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/BearingFilterTests.cs b/Tests/Editor/DirectionsApi/BearingFilterTests.cs new file mode 100644 index 000000000..c22578bf0 --- /dev/null +++ b/Tests/Editor/DirectionsApi/BearingFilterTests.cs @@ -0,0 +1,145 @@ +using System; +using Mapbox.Utils; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class BearingFilterTests + { + [Test] + public void Constructor_SetsValues() + { + var filter = new BearingFilter(45, 10); + + Assert.AreEqual(45, filter.Bearing); + Assert.AreEqual(10, filter.Range); + } + + [Test] + public void Constructor_AcceptsNullValues() + { + var filter = new BearingFilter(null, null); + + Assert.IsNull(filter.Bearing); + Assert.IsNull(filter.Range); + } + + [Test] + public void Constructor_ThrowsException_WhenBearingIsNegative() + { + Assert.Throws(() => new BearingFilter(-1, 10)); + } + + [Test] + public void Constructor_ThrowsException_WhenBearingIsGreaterThan360() + { + Assert.Throws(() => new BearingFilter(361, 10)); + } + + [Test] + public void Constructor_AcceptsBearing_AtZero() + { + var filter = new BearingFilter(0, 10); + Assert.AreEqual(0, filter.Bearing); + } + + [Test] + public void Constructor_AcceptsBearing_At360() + { + var filter = new BearingFilter(360, 10); + Assert.AreEqual(360, filter.Bearing); + } + + [Test] + public void Constructor_ThrowsException_WhenRangeIsNegative() + { + Assert.Throws(() => new BearingFilter(45, -1)); + } + + [Test] + public void Constructor_ThrowsException_WhenRangeIsGreaterThan180() + { + Assert.Throws(() => new BearingFilter(45, 181)); + } + + [Test] + public void Constructor_AcceptsRange_AtZero() + { + var filter = new BearingFilter(45, 0); + Assert.AreEqual(0, filter.Range); + } + + [Test] + public void Constructor_AcceptsRange_At180() + { + var filter = new BearingFilter(45, 180); + Assert.AreEqual(180, filter.Range); + } + + [Test] + public void ToString_ReturnsCorrectFormat() + { + var filter = new BearingFilter(45.5, 10.5); + var result = filter.ToString(); + + Assert.AreEqual("45.5,10.5", result); + } + + [Test] + public void ToString_ReturnsEmptyString_WhenBearingIsNull() + { + var filter = new BearingFilter(null, 10); + var result = filter.ToString(); + + Assert.AreEqual(string.Empty, result); + } + + [Test] + public void ToString_ReturnsEmptyString_WhenRangeIsNull() + { + var filter = new BearingFilter(45, null); + var result = filter.ToString(); + + Assert.AreEqual(string.Empty, result); + } + + [Test] + public void ToString_ReturnsEmptyString_WhenBothAreNull() + { + var filter = new BearingFilter(null, null); + var result = filter.ToString(); + + Assert.AreEqual(string.Empty, result); + } + + [TestCase(0, 0, "0,0")] + [TestCase(90, 45, "90,45")] + [TestCase(180, 90, "180,90")] + [TestCase(270, 135, "270,135")] + [TestCase(360, 180, "360,180")] + public void ToString_ReturnsCorrectFormat_ForVariousAngles(double bearing, double range, string expected) + { + var filter = new BearingFilter(bearing, range); + var result = filter.ToString(); + + Assert.AreEqual(expected, result); + } + + [Test] + public void Constructor_AcceptsDecimalValues() + { + var filter = new BearingFilter(123.456, 78.9); + + Assert.AreEqual(123.456, filter.Bearing); + Assert.AreEqual(78.9, filter.Range); + } + + [Test] + public void Constructor_ThrowsException_OnlyWhenBearingIsInvalid() + { + // Valid bearing, invalid range should throw + Assert.Throws(() => new BearingFilter(45, 200)); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/BearingFilterTests.cs.meta b/Tests/Editor/DirectionsApi/BearingFilterTests.cs.meta new file mode 100644 index 000000000..db8e03240 --- /dev/null +++ b/Tests/Editor/DirectionsApi/BearingFilterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 547449c5fbdd04746810e0c308b43db5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/DirectionResourceTests.cs b/Tests/Editor/DirectionsApi/DirectionResourceTests.cs new file mode 100644 index 000000000..50195949e --- /dev/null +++ b/Tests/Editor/DirectionsApi/DirectionResourceTests.cs @@ -0,0 +1,353 @@ +using System; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.DirectionsApi; +using Mapbox.Utils; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class DirectionResourceTests + { + private LatitudeLongitude[] _validCoordinates; + + [SetUp] + public void Setup() + { + _validCoordinates = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), // San Francisco + new LatitudeLongitude(34.0522, -118.2437) // Los Angeles + }; + } + + [Test] + public void Constructor_SetsCoordinatesAndProfile() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + + Assert.AreEqual(2, resource.Coordinates.Length); + Assert.AreEqual(RoutingProfile.Driving, resource.RoutingProfile); + } + + [Test] + public void Coordinates_ThrowsException_WhenLessThan2() + { + var invalidCoords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194) + }; + + Assert.Throws(() => new DirectionResource(invalidCoords, RoutingProfile.Driving)); + } + + [Test] + public void Coordinates_ThrowsException_WhenMoreThan25() + { + var invalidCoords = new LatitudeLongitude[26]; + for (int i = 0; i < 26; i++) + { + invalidCoords[i] = new LatitudeLongitude(37.0 + i * 0.1, -122.0 + i * 0.1); + } + + Assert.Throws(() => new DirectionResource(invalidCoords, RoutingProfile.Driving)); + } + + [Test] + public void Coordinates_AcceptsExactly2Elements() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Walking); + Assert.AreEqual(2, resource.Coordinates.Length); + } + + [Test] + public void Coordinates_AcceptsExactly25Elements() + { + var coords = new LatitudeLongitude[25]; + for (int i = 0; i < 25; i++) + { + coords[i] = new LatitudeLongitude(37.0 + i * 0.1, -122.0 + i * 0.1); + } + + var resource = new DirectionResource(coords, RoutingProfile.Cycling); + Assert.AreEqual(25, resource.Coordinates.Length); + } + + [Test] + public void RoutingProfile_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.RoutingProfile = RoutingProfile.Walking; + + Assert.AreEqual(RoutingProfile.Walking, resource.RoutingProfile); + } + + [Test] + public void Alternatives_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Alternatives); + } + + [Test] + public void Alternatives_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Alternatives = true; + + Assert.IsTrue(resource.Alternatives.Value); + } + + [Test] + public void Steps_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Steps); + } + + [Test] + public void Steps_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Steps = true; + + Assert.IsTrue(resource.Steps.Value); + } + + [Test] + public void ContinueStraight_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.ContinueStraight); + } + + [Test] + public void ContinueStraight_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.ContinueStraight = false; + + Assert.IsFalse(resource.ContinueStraight.Value); + } + + [Test] + public void Overview_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Overview); + } + + [Test] + public void Overview_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Overview = Overview.Full; + + Assert.AreEqual(Overview.Full, resource.Overview); + } + + [Test] + public void Bearings_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Bearings); + } + + [Test] + public void Bearings_ThrowsException_WhenCountMismatchesCoordinates() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var invalidBearings = new BearingFilter[] + { + new BearingFilter(45, 10) + }; + + Assert.Throws(() => resource.Bearings = invalidBearings); + } + + [Test] + public void Bearings_AcceptsCorrectCount() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var validBearings = new BearingFilter[] + { + new BearingFilter(45, 10), + new BearingFilter(90, 20) + }; + + resource.Bearings = validBearings; + Assert.AreEqual(2, resource.Bearings.Length); + } + + [Test] + public void Radiuses_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Radiuses); + } + + [Test] + public void Radiuses_ThrowsException_WhenCountMismatchesCoordinates() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var invalidRadiuses = new double[] { 100.0 }; + + Assert.Throws(() => resource.Radiuses = invalidRadiuses); + } + + [Test] + public void Radiuses_ThrowsException_WhenValueIsZeroOrNegative() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var invalidRadiuses = new double[] { 100.0, 0 }; + + Assert.Throws(() => resource.Radiuses = invalidRadiuses); + } + + [Test] + public void Radiuses_AcceptsPositiveValues() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var validRadiuses = new double[] { 100.0, 200.0 }; + + resource.Radiuses = validRadiuses; + Assert.AreEqual(2, resource.Radiuses.Length); + Assert.AreEqual(100.0, resource.Radiuses[0]); + Assert.AreEqual(200.0, resource.Radiuses[1]); + } + + [Test] + public void GetUrl_ReturnsCorrectBaseUrl() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("directions/v5/")); + Assert.IsTrue(url.Contains("mapbox/driving/")); + Assert.IsTrue(url.EndsWith(".json")); + } + + [Test] + public void GetUrl_IncludesCoordinatesInCorrectFormat() + { + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + var url = resource.GetUrl(); + + // Coordinates should be in lon,lat format separated by semicolons + Assert.IsTrue(url.Contains("-122")); + Assert.IsTrue(url.Contains("37")); + Assert.IsTrue(url.Contains(";")); + } + + [Test] + public void GetUrl_IncludesAlternativesWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Alternatives = true; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("alternatives=true")); + } + + [Test] + public void GetUrl_IncludesStepsWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Steps = true; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("steps=true")); + } + + [Test] + public void GetUrl_IncludesContinueStraightWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.ContinueStraight = false; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("continue_straight=false")); + } + + [Test] + public void GetUrl_IncludesOverviewWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Overview = Overview.Full; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("overview=full")); + } + + [Test] + public void GetUrl_IncludesRadiusesWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Radiuses = new double[] { 100.0, 200.0 }; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("radiuses=")); + } + + [Test] + public void GetUrl_IncludesBearingsWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Bearings = new BearingFilter[] + { + new BearingFilter(45, 10), + new BearingFilter(90, 20) + }; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("bearings=")); + } + + [Test] + public void GetUrl_CombinesMultipleParameters() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Walking); + resource.Alternatives = true; + resource.Steps = true; + resource.Overview = Overview.Simplified; + + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("alternatives=true")); + Assert.IsTrue(url.Contains("steps=true")); + Assert.IsTrue(url.Contains("overview=simplified")); + } + + [Test] + public void GetUrl_IncludesDrivingProfile() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("mapbox/driving/")); + } + + [Test] + public void GetUrl_IncludesWalkingProfile() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Walking); + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("mapbox/walking/")); + } + + [Test] + public void GetUrl_IncludesCyclingProfile() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Cycling); + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("mapbox/cycling/")); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/DirectionResourceTests.cs.meta b/Tests/Editor/DirectionsApi/DirectionResourceTests.cs.meta new file mode 100644 index 000000000..ccca4d240 --- /dev/null +++ b/Tests/Editor/DirectionsApi/DirectionResourceTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd480d299eaa54556b8fd4146a936e87 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs b/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs new file mode 100644 index 000000000..bd6cabb1f --- /dev/null +++ b/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities.JsonConverters; +using Mapbox.DirectionsApi.Response; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class DirectionsResponseTests + { + private string _sampleResponseJson = @"{ + ""routes"": [ + { + ""distance"": 12345.6, + ""duration"": 789.0, + ""weight"": 800.0 + } + ], + ""waypoints"": [ + { + ""name"": ""Main Street"", + ""location"": [-122.4194, 37.7749] + }, + { + ""name"": ""Market Street"", + ""location"": [-118.2437, 34.0522] + } + ], + ""code"": ""Ok"" + }"; + + [Test] + public void Deserialize_ParsesValidResponse() + { + var response = JsonConvert.DeserializeObject(_sampleResponseJson, JsonConverters.Converters); + + Assert.IsNotNull(response); + Assert.AreEqual("Ok", response.Code); + } + + [Test] + public void Deserialize_ParsesRoutes() + { + var response = JsonConvert.DeserializeObject(_sampleResponseJson, JsonConverters.Converters); + + Assert.IsNotNull(response.Routes); + Assert.AreEqual(1, response.Routes.Count); + Assert.AreEqual(12345.6, response.Routes[0].Distance); + Assert.AreEqual(789.0, response.Routes[0].Duration); + } + + [Test] + public void Deserialize_ParsesWaypoints() + { + var response = JsonConvert.DeserializeObject(_sampleResponseJson, JsonConverters.Converters); + + Assert.IsNotNull(response.Waypoints); + Assert.AreEqual(2, response.Waypoints.Count); + Assert.AreEqual("Main Street", response.Waypoints[0].Name); + Assert.AreEqual("Market Street", response.Waypoints[1].Name); + } + + [Test] + public void Deserialize_ParsesCode() + { + var response = JsonConvert.DeserializeObject(_sampleResponseJson, JsonConverters.Converters); + + Assert.AreEqual("Ok", response.Code); + } + + [Test] + public void Serialize_CreatesValidJson() + { + var response = new DirectionsResponse + { + Code = "Ok", + Routes = new List(), + Waypoints = new List() + }; + + var json = JsonConvert.SerializeObject(response, JsonConverters.Converters); + + Assert.IsNotNull(json); + Assert.IsTrue(json.Contains("\"code\"")); + Assert.IsTrue(json.Contains("\"routes\"")); + Assert.IsTrue(json.Contains("\"waypoints\"")); + } + + [Test] + public void Deserialize_HandlesEmptyRoutes() + { + var json = @"{ + ""routes"": [], + ""waypoints"": [], + ""code"": ""NoRoute"" + }"; + + var response = JsonConvert.DeserializeObject(json, JsonConverters.Converters); + + Assert.IsNotNull(response.Routes); + Assert.AreEqual(0, response.Routes.Count); + Assert.AreEqual("NoRoute", response.Code); + } + + [Test] + public void Deserialize_HandlesMultipleRoutes() + { + var json = @"{ + ""routes"": [ + { + ""distance"": 1000.0, + ""duration"": 100.0, + ""weight"": 110.0 + }, + { + ""distance"": 1200.0, + ""duration"": 120.0, + ""weight"": 130.0 + } + ], + ""waypoints"": [], + ""code"": ""Ok"" + }"; + + var response = JsonConvert.DeserializeObject(json, JsonConverters.Converters); + + Assert.AreEqual(2, response.Routes.Count); + Assert.AreEqual(1000.0, response.Routes[0].Distance); + Assert.AreEqual(1200.0, response.Routes[1].Distance); + } + + [Test] + public void SerializeDeserialize_RoundTrip() + { + var original = new DirectionsResponse + { + Code = "Ok", + Routes = new List + { + new Route + { + Distance = 5000.0, + Duration = 300.0, + Weight = 320.0f, + Geometry = new List() + } + }, + Waypoints = new List + { + new Waypoint + { + Name = "Test Waypoint" + } + } + }; + + var json = JsonConvert.SerializeObject(original, JsonConverters.Converters); + var deserialized = JsonConvert.DeserializeObject(json, JsonConverters.Converters); + + Assert.AreEqual(original.Code, deserialized.Code); + Assert.AreEqual(original.Routes.Count, deserialized.Routes.Count); + Assert.AreEqual(original.Routes[0].Distance, deserialized.Routes[0].Distance); + Assert.AreEqual(original.Waypoints[0].Name, deserialized.Waypoints[0].Name); + } + + [Test] + public void Deserialize_HandlesNullFields() + { + var json = @"{ + ""code"": ""Ok"" + }"; + + var response = JsonConvert.DeserializeObject(json, JsonConverters.Converters); + + Assert.IsNotNull(response); + Assert.AreEqual("Ok", response.Code); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs.meta b/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs.meta new file mode 100644 index 000000000..1d3c79944 --- /dev/null +++ b/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c6ba3a82c9b974826a8a3a7da34ea5d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef new file mode 100644 index 000000000..15be2e44d --- /dev/null +++ b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef @@ -0,0 +1,26 @@ +{ + "name": "MapboxDirectionsApiTests", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:8ba268152fd3143f59445711358cb12f" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Newtonsoft.Json.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef.meta b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef.meta new file mode 100644 index 000000000..b9cdd95dc --- /dev/null +++ b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 917423ea424014119b14334842c37791 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs new file mode 100644 index 000000000..c63ff14c7 --- /dev/null +++ b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs @@ -0,0 +1,308 @@ +using System; +using System.Text; +using Mapbox.BaseModule.Data.Platform; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.DirectionsApi; +using Mapbox.DirectionsApi.Response; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class MapboxDirectionsApiTests + { + private class MockFileSource : IFileSource + { + private readonly string _responseJson; + private readonly bool _shouldError; + + public MockFileSource(string responseJson, bool shouldError = false) + { + _responseJson = responseJson; + _shouldError = shouldError; + } + + public IAsyncRequest Request(string uri, Action callback, int timeout = 10) + { + if (!_shouldError) + { + var response = new Response + { + Data = Encoding.UTF8.GetBytes(_responseJson) + }; + callback(response); + } + + return new MockAsyncRequest(); + } + + public IWebRequest MapboxImageRequest(string uri, Action callback, string etag = "", int timeout = 10, bool isNonReadable = true) + { + throw new NotImplementedException("Not needed for Directions API tests"); + } + + public IWebRequest CustomImageRequest(string uri, Action callback, string etag = null, int timeout = 10, bool isNonReadable = true) + { + throw new NotImplementedException("Not needed for Directions API tests"); + } + + public IWebRequest MapboxDataRequest(string uri, Action callback, string etag = "", int timeout = 10) + { + throw new NotImplementedException("Not needed for Directions API tests"); + } + + public void OnDestroy() + { + // No cleanup needed for mock + } + } + + private class MockAsyncRequest : IAsyncRequest + { + public bool IsCompleted { get; set; } + public HttpRequestType RequestType { get; set; } + + public void Cancel() + { + IsCompleted = true; + } + } + + private string _validResponseJson = @"{ + ""routes"": [ + { + ""distance"": 10000.0, + ""duration"": 500.0, + ""weight"": 520.0 + } + ], + ""waypoints"": [ + { + ""name"": ""Start Point"", + ""location"": [-122.4194, 37.7749] + }, + { + ""name"": ""End Point"", + ""location"": [-118.2437, 34.0522] + } + ], + ""code"": ""Ok"" + }"; + + [Test] + public void Constructor_AcceptsFileSource() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + Assert.IsNotNull(api); + } + + [Test] + public void Query_ExecutesCallback() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.IsNotNull(capturedResponse); + } + + [Test] + public void Query_ReturnsValidResponse() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.IsNotNull(capturedResponse); + Assert.AreEqual("Ok", capturedResponse.Code); + Assert.AreEqual(1, capturedResponse.Routes.Count); + Assert.AreEqual(2, capturedResponse.Waypoints.Count); + } + + [Test] + public void Query_ParsesRouteData() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.AreEqual(10000.0, capturedResponse.Routes[0].Distance); + Assert.AreEqual(500.0, capturedResponse.Routes[0].Duration); + Assert.AreEqual(520.0, capturedResponse.Routes[0].Weight); + } + + [Test] + public void Query_ParsesWaypointData() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.AreEqual("Start Point", capturedResponse.Waypoints[0].Name); + Assert.AreEqual("End Point", capturedResponse.Waypoints[1].Name); + } + + [Test] + public void Query_ReturnsAsyncRequest() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + var asyncRequest = api.Query(resource, response => { }); + + Assert.IsNotNull(asyncRequest); + } + + [Test] + public void Serialize_CreatesJson() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var response = new DirectionsResponse + { + Code = "Ok", + Routes = new System.Collections.Generic.List(), + Waypoints = new System.Collections.Generic.List() + }; + + var json = api.Serialize(response); + + Assert.IsNotNull(json); + Assert.IsTrue(json.Length > 0); + } + + [Test] + public void Query_HandlesDifferentProfiles() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + + // Test with different routing profiles + var walkingResource = new DirectionResource(coords, RoutingProfile.Walking); + DirectionsResponse walkingResponse = null; + api.Query(walkingResource, response => { walkingResponse = response; }); + + Assert.IsNotNull(walkingResponse); + Assert.AreEqual("Ok", walkingResponse.Code); + } + + [Test] + public void Query_HandlesEmptyRoutes() + { + var emptyRoutesJson = @"{ + ""routes"": [], + ""waypoints"": [], + ""code"": ""NoRoute"" + }"; + + var mockSource = new MockFileSource(emptyRoutesJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.IsNotNull(capturedResponse); + Assert.AreEqual("NoRoute", capturedResponse.Code); + Assert.AreEqual(0, capturedResponse.Routes.Count); + } + + [Test] + public void Query_HandlesMultipleWaypoints() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(36.1699, -115.1398), // Las Vegas + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.IsNotNull(capturedResponse); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs.meta b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs.meta new file mode 100644 index 000000000..87cbc4a06 --- /dev/null +++ b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19233547ac7754529b553582332d9a05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/OverviewTests.cs b/Tests/Editor/DirectionsApi/OverviewTests.cs new file mode 100644 index 000000000..226caf110 --- /dev/null +++ b/Tests/Editor/DirectionsApi/OverviewTests.cs @@ -0,0 +1,65 @@ +using Mapbox.DirectionsApi; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class OverviewTests + { + [Test] + public void Full_Overview_Exists() + { + Assert.IsNotNull(Overview.Full); + } + + [Test] + public void Simplified_Overview_Exists() + { + Assert.IsNotNull(Overview.Simplified); + } + + [Test] + public void False_Overview_Exists() + { + Assert.IsNotNull(Overview.False); + } + + [Test] + public void Full_ToString_ReturnsCorrectValue() + { + var overview = Overview.Full; + Assert.AreEqual("full", overview.ToString()); + } + + [Test] + public void Simplified_ToString_ReturnsCorrectValue() + { + var overview = Overview.Simplified; + Assert.AreEqual("simplified", overview.ToString()); + } + + [Test] + public void False_ToString_ReturnsCorrectValue() + { + var overview = Overview.False; + Assert.AreEqual("false", overview.ToString()); + } + + [Test] + public void Overviews_AreUnique() + { + Assert.AreNotEqual(Overview.Full, Overview.Simplified); + Assert.AreNotEqual(Overview.Full, Overview.False); + Assert.AreNotEqual(Overview.Simplified, Overview.False); + } + + [Test] + public void Overview_IsSameInstance() + { + var full1 = Overview.Full; + var full2 = Overview.Full; + + Assert.AreSame(full1, full2); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/OverviewTests.cs.meta b/Tests/Editor/DirectionsApi/OverviewTests.cs.meta new file mode 100644 index 000000000..278b3ed0b --- /dev/null +++ b/Tests/Editor/DirectionsApi/OverviewTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9161c8d094e3499c98681b987bbffb4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/RoutingProfileTests.cs b/Tests/Editor/DirectionsApi/RoutingProfileTests.cs new file mode 100644 index 000000000..0933ef984 --- /dev/null +++ b/Tests/Editor/DirectionsApi/RoutingProfileTests.cs @@ -0,0 +1,86 @@ +using Mapbox.DirectionsApi; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class RoutingProfileTests + { + [Test] + public void Driving_Profile_Exists() + { + Assert.IsNotNull(RoutingProfile.Driving); + } + + [Test] + public void Walking_Profile_Exists() + { + Assert.IsNotNull(RoutingProfile.Walking); + } + + [Test] + public void Cycling_Profile_Exists() + { + Assert.IsNotNull(RoutingProfile.Cycling); + } + + [Test] + public void Driving_ToString_ReturnsCorrectValue() + { + var profile = RoutingProfile.Driving; + Assert.AreEqual("mapbox/driving/", profile.ToString()); + } + + [Test] + public void Walking_ToString_ReturnsCorrectValue() + { + var profile = RoutingProfile.Walking; + Assert.AreEqual("mapbox/walking/", profile.ToString()); + } + + [Test] + public void Cycling_ToString_ReturnsCorrectValue() + { + var profile = RoutingProfile.Cycling; + Assert.AreEqual("mapbox/cycling/", profile.ToString()); + } + + [Test] + public void GetProfile_ReturnsDriving_ForDrivingOption() + { + var profile = RoutingProfile.GetProfile(RoutingProfile.RoutingProfileOptions.Driving); + Assert.AreEqual(RoutingProfile.Driving, profile); + } + + [Test] + public void GetProfile_ReturnsWalking_ForWalkingOption() + { + var profile = RoutingProfile.GetProfile(RoutingProfile.RoutingProfileOptions.Walking); + Assert.AreEqual(RoutingProfile.Walking, profile); + } + + [Test] + public void GetProfile_ReturnsCycling_ForCyclingOption() + { + var profile = RoutingProfile.GetProfile(RoutingProfile.RoutingProfileOptions.Cycling); + Assert.AreEqual(RoutingProfile.Cycling, profile); + } + + [Test] + public void Profiles_AreUnique() + { + Assert.AreNotEqual(RoutingProfile.Driving, RoutingProfile.Walking); + Assert.AreNotEqual(RoutingProfile.Driving, RoutingProfile.Cycling); + Assert.AreNotEqual(RoutingProfile.Walking, RoutingProfile.Cycling); + } + + [Test] + public void Profile_IsSameInstance() + { + var driving1 = RoutingProfile.Driving; + var driving2 = RoutingProfile.Driving; + + Assert.AreSame(driving1, driving2); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/RoutingProfileTests.cs.meta b/Tests/Editor/DirectionsApi/RoutingProfileTests.cs.meta new file mode 100644 index 000000000..cd04e70b2 --- /dev/null +++ b/Tests/Editor/DirectionsApi/RoutingProfileTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: efb0bce33a835455fa36122db3e142f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/BaseModule/PlayModeTests.asmdef b/Tests/Runtime/BaseModule/PlayModeTests.asmdef index 0a4710e66..8d3610a62 100644 --- a/Tests/Runtime/BaseModule/PlayModeTests.asmdef +++ b/Tests/Runtime/BaseModule/PlayModeTests.asmdef @@ -13,7 +13,9 @@ "MapboxImageryModule", "MapboxVectorModule" ], - "includePlatforms": [], + "includePlatforms": [ + "Editor" + ], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": true, From ff9f0d281808b36fd3c399a9c01db2222a1240f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Thu, 26 Feb 2026 20:21:12 +0300 Subject: [PATCH 07/54] Map matching api (#1979) * add clear caches method to map service and map script add clear caches method to map service and map script # Conflicts: # Editor/MapboxConfigurationWindow.cs # Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs * fix a typo in map unity service * fix mapmatching, add docs for apis fix mapmatching api add documents and examples for mapmatching, directions and geocoding apis * mapmatching enum fix --- Documentation~/UsingDirectionsApi.md | 113 ++++++++ Documentation~/UsingGeocodingApi.md | 195 ++++++++++++++ Documentation~/UsingMapMatchingApi.md | 245 ++++++++++++++++++ .../MapboxRequestStringAttribute.cs | 26 ++ .../MapboxRequestStringAttribute.cs.meta | 11 + .../BaseModule/Utilities/EnumExtensions.cs | 53 ++++ .../Utilities/EnumExtensions.cs.meta | 11 + Runtime/Mapbox/DirectionsApi/Example.meta | 8 + .../Example/DirectionsExample.cs | 113 ++++++++ .../Example/DirectionsExample.cs.meta | 11 + .../Example/MapMatchingExample.cs | 234 +++++++++++++++++ .../Example/MapMatchingExample.cs.meta | 11 + .../MapMatching/MapMatchingParameters.cs | 72 ++--- .../MapMatching/MapMatchingResource.cs | 37 +-- Runtime/Mapbox/GeocodingApi/Example.meta | 8 + .../Example/ForwardGeocodeExample.cs | 157 +++++++++++ .../Example/ForwardGeocodeExample.cs.meta | 11 + .../Example/ReverseGeocodeExample.cs | 172 ++++++++++++ .../Example/ReverseGeocodeExample.cs.meta | 11 + 19 files changed, 1445 insertions(+), 54 deletions(-) create mode 100644 Documentation~/UsingDirectionsApi.md create mode 100644 Documentation~/UsingGeocodingApi.md create mode 100644 Documentation~/UsingMapMatchingApi.md create mode 100644 Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs create mode 100644 Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs.meta create mode 100644 Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs create mode 100644 Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs.meta create mode 100644 Runtime/Mapbox/DirectionsApi/Example.meta create mode 100644 Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs create mode 100644 Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs.meta create mode 100644 Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs create mode 100644 Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs.meta create mode 100644 Runtime/Mapbox/GeocodingApi/Example.meta create mode 100644 Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs create mode 100644 Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs.meta create mode 100644 Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs create mode 100644 Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs.meta diff --git a/Documentation~/UsingDirectionsApi.md b/Documentation~/UsingDirectionsApi.md new file mode 100644 index 000000000..21775b731 --- /dev/null +++ b/Documentation~/UsingDirectionsApi.md @@ -0,0 +1,113 @@ + +# Using the Mapbox Directions API + +The Mapbox Directions API allows you to calculate routes between locations. You can get turn-by-turn directions, route geometry, distance, and duration for different routing profiles (driving, walking, cycling). + +For complete API reference and details, see the [official Mapbox Directions API documentation](https://docs.mapbox.com/api/navigation/directions/). + +## Basic Setup + +To use the Directions API, you need to: + +1. Reference the `MapBehaviourCore` component to access the map's file source. +2. Create a `MapboxDirectionsApi` instance with the file source. +3. Build a `DirectionResource` with your origin and destination coordinates. +4. Query the API and handle the response in a callback. + +Here's a simple example: + +```csharp +using UnityEngine; +using Mapbox.Unity.Map; +using Mapbox.DirectionsApi; +using Mapbox.DirectionsApi.Response; +using Mapbox.Utils; + +public class DirectionsExample : MonoBehaviour +{ + public MapBehaviourCore MapCore; + private MapboxDirectionsApi _directionsApi; + + void Start() + { + MapCore.Initialized += map => + { + _directionsApi = new MapboxDirectionsApi(map.MapService.FileSource); + GetRoute(); + }; + } + + void GetRoute() + { + // Define two coordinates: San Francisco to Los Angeles + var origin = new LatitudeLongitude(37.7749, -122.4194); + var destination = new LatitudeLongitude(34.0522, -118.2437); + + // Create a directions request with driving profile + var directionResource = new DirectionResource( + new[] { origin, destination }, + RoutingProfile.Driving + ); + + // Query the API + _directionsApi.Query(directionResource, response => + { + if (response == null || response.Routes == null || response.Routes.Count == 0) + { + Debug.Log("No route found"); + return; + } + + var route = response.Routes[0]; + Debug.Log($"Route found!"); + Debug.Log($"Distance: {route.Distance} meters"); + Debug.Log($"Duration: {route.Duration} seconds"); + Debug.Log($"Geometry points: {route.Geometry.Count}"); + }); + } +} +``` + +## What This Does + +- The script waits for the map to initialize and gets access to the file source. +- Creates a `MapboxDirectionsApi` instance for making routing queries. +- Defines a route from San Francisco to Los Angeles using latitude/longitude coordinates. +- Requests a driving route between the two points. +- When the response arrives, it logs the route distance (in meters), duration (in seconds), and number of geometry points. + +## Routing Profiles + +You can choose from three routing profiles: + +- `RoutingProfile.Driving` - Optimized for car travel +- `RoutingProfile.Walking` - Optimized for pedestrians +- `RoutingProfile.Cycling` - Optimized for bicycle travel + +## Optional Parameters + +The `DirectionResource` supports several optional parameters: + +```csharp +var directionResource = new DirectionResource(coordinates, RoutingProfile.Driving) +{ + Alternatives = true, // Get alternative routes + Steps = true, // Include turn-by-turn instructions + Overview = Overview.Full // Get full route geometry +}; +``` + +## Response Data + +The `DirectionsResponse` contains: +- `Routes` - List of possible routes (usually one, unless alternatives are requested) +- `Waypoints` - Snapped waypoint locations +- `Code` - Response status code ("Ok" if successful) + +Each `Route` includes: +- `Distance` - Total route distance in meters +- `Duration` - Estimated travel time in seconds +- `Geometry` - List of lat/lng coordinates forming the route path +- `Weight` - Route weight (typically duration-based) + +--- diff --git a/Documentation~/UsingGeocodingApi.md b/Documentation~/UsingGeocodingApi.md new file mode 100644 index 000000000..1bdf96106 --- /dev/null +++ b/Documentation~/UsingGeocodingApi.md @@ -0,0 +1,195 @@ + +# Using the Mapbox Geocoding API + +The Mapbox Geocoding API provides two main functions: +- **Forward Geocoding**: Convert addresses or place names into geographic coordinates (lat/lng) +- **Reverse Geocoding**: Convert geographic coordinates into human-readable addresses + +For complete API reference and details, see the [official Mapbox Geocoding API documentation](https://docs.mapbox.com/api/search/geocoding/). + +## Forward Geocoding + +Forward geocoding converts a text query (like "San Francisco" or "1600 Pennsylvania Avenue") into coordinates. + +### Basic Example + +```csharp +using UnityEngine; +using Mapbox.Unity.Map; +using Mapbox.GeocodingApi; + +public class ForwardGeocodeExample : MonoBehaviour +{ + public MapBehaviourCore MapCore; + private MapboxGeocodingApi _geocodingApi; + + void Start() + { + MapCore.Initialized += map => + { + _geocodingApi = new MapboxGeocodingApi(map.MapService.FileSource); + SearchPlace("San Francisco, CA"); + }; + } + + void SearchPlace(string placeName) + { + var forwardGeocodeResource = new ForwardGeocodeResource(placeName); + + _geocodingApi.Geocode(forwardGeocodeResource, (ForwardGeocodeResponse response) => + { + if (response == null || response.Features == null || response.Features.Count == 0) + { + Debug.Log("No results found"); + return; + } + + var firstResult = response.Features[0]; + Debug.Log($"Found: {firstResult.PlaceName}"); + Debug.Log($"Coordinates: {firstResult.Center.x}, {firstResult.Center.y}"); + }); + } +} +``` + +### What This Does +- Waits for the map to initialize +- Creates a geocoding request for "San Francisco, CA" +- Returns the place name and coordinates (latitude, longitude) +- The `Center` property contains the location as a `Vector2d` with x=latitude, y=longitude + +### Optional Parameters + +You can refine your forward geocoding searches: + +```csharp +var forwardGeocodeResource = new ForwardGeocodeResource("pizza") +{ + Country = new[] { "us" }, // Limit to United States + Proximity = new Vector2d(37.7, -122.4), // Bias results near San Francisco + Autocomplete = true, // Enable autocomplete suggestions + Types = new[] { "poi" } // Only return points of interest +}; +``` + +### Response Data + +The `ForwardGeocodeResponse` contains: +- `Features` - List of matching places (usually ordered by relevance) +- `Query` - The original query string +- `Attribution` - Mapbox attribution text + +Each `Feature` includes: +- `PlaceName` - Full address or place name +- `Text` - Short name of the place +- `Center` - Coordinates as Vector2d (x=lat, y=lng) +- `PlaceType` - Type of place (e.g., "address", "poi", "place") +- `Address` - Street address (if applicable) +- `Relevance` - Match score (0-1) +- `Context` - Additional location context (city, state, country, etc.) + +--- + +## Reverse Geocoding + +Reverse geocoding converts coordinates into a human-readable address or place name. + +### Basic Example + +```csharp +using UnityEngine; +using Mapbox.Unity.Map; +using Mapbox.GeocodingApi; +using Mapbox.Utils; + +public class ReverseGeocodeExample : MonoBehaviour +{ + public MapBehaviourCore MapCore; + private MapboxGeocodingApi _geocodingApi; + + void Start() + { + MapCore.Initialized += map => + { + _geocodingApi = new MapboxGeocodingApi(map.MapService.FileSource); + + // Reverse geocode the Golden Gate Bridge location + var location = new LatitudeLongitude(37.8199, -122.4783); + GetAddressFromCoordinates(location); + }; + } + + void GetAddressFromCoordinates(LatitudeLongitude location) + { + var reverseGeocodeResource = new ReverseGeocodeResource(location); + + _geocodingApi.Geocode(reverseGeocodeResource, (ReverseGeocodeResponse response) => + { + if (response == null || response.Features == null || response.Features.Count == 0) + { + Debug.Log("No address found"); + return; + } + + var firstResult = response.Features[0]; + Debug.Log($"Address: {firstResult.PlaceName}"); + Debug.Log($"Type: {string.Join(", ", firstResult.PlaceType)}"); + }); + } +} +``` + +### What This Does +- Waits for the map to initialize +- Creates a reverse geocoding request for coordinates (37.8199, -122.4783) +- Returns the human-readable address or place name at that location +- The first feature typically contains the most specific address + +### Optional Parameters + +You can filter reverse geocoding results by type: + +```csharp +var reverseGeocodeResource = new ReverseGeocodeResource(location) +{ + Types = new[] { "address" } // Only return street addresses +}; +``` + +Common types: `country`, `region`, `postcode`, `place`, `locality`, `neighborhood`, `address`, `poi` + +### Response Data + +The `ReverseGeocodeResponse` contains: +- `Features` - List of places at or near the coordinates +- `Query` - The original coordinates as [longitude, latitude] +- `Attribution` - Mapbox attribution text + +Each `Feature` includes the same fields as forward geocoding (see above). + +--- + +## Common Use Cases + +### Interactive Map Click +Convert clicked map positions to addresses: +```csharp +// Get world position from mouse click, convert to lat/lng, then reverse geocode +var worldPos = GetClickedWorldPosition(); +var latLng = mapInformation.ConvertPositionToLatLng(worldPos); +var reverseResource = new ReverseGeocodeResource(latLng); +geocodingApi.Geocode(reverseResource, HandleReverseGeocodeResponse); +``` + +### Search Box +Implement a location search: +```csharp +var forwardResource = new ForwardGeocodeResource(searchText) +{ + Autocomplete = true, + Country = new[] { "us" } +}; +geocodingApi.Geocode(forwardResource, DisplaySearchResults); +``` + +--- diff --git a/Documentation~/UsingMapMatchingApi.md b/Documentation~/UsingMapMatchingApi.md new file mode 100644 index 000000000..bc1707077 --- /dev/null +++ b/Documentation~/UsingMapMatchingApi.md @@ -0,0 +1,245 @@ + +# Using the Mapbox Map Matching API + +The Mapbox Map Matching API snaps GPS traces (sequences of coordinates) to the road network and returns the matched route. This is useful for: +- Cleaning up noisy GPS traces +- Matching recorded GPS tracks to actual roads +- Analyzing vehicle or pedestrian movement patterns +- Creating accurate route visualizations from tracking data + +For complete API reference and details, see the [official Mapbox Map Matching API documentation](https://docs.mapbox.com/api/navigation/map-matching/). + +## What is Map Matching? + +Map matching takes a series of coordinates (typically from GPS tracking) and "snaps" them to the nearest roads, producing a clean, road-network-constrained path. Unlike the Directions API which calculates an optimal route between points, Map Matching respects the actual path taken and corrects for GPS inaccuracies. + +## Basic Usage + +```csharp +using UnityEngine; +using Mapbox.Unity.Map; +using Mapbox.DirectionsApi.MapMatching; +using Mapbox.BaseModule.Data.Vector2d; + +public class MapMatchingExample : MonoBehaviour +{ + public MapBehaviourCore MapCore; + private MapboxMapMatcherApi _mapMatcherApi; + + void Start() + { + MapCore.Initialized += map => + { + _mapMatcherApi = new MapboxMapMatcherApi(map.MapService.FileSource, 30); + MatchGPSTrace(); + }; + } + + void MatchGPSTrace() + { + // Example: noisy GPS coordinates from a recorded trip + var gpsTrace = new Vector2d[] + { + new Vector2d(37.7749, -122.4194), + new Vector2d(37.7751, -122.4193), + new Vector2d(37.7753, -122.4191), + new Vector2d(37.7755, -122.4189), + new Vector2d(37.7757, -122.4187) + }; + + // Create map matching request + var mapMatchingResource = new MapMatchingResource + { + Coordinates = gpsTrace, + Profile = Profile.MapboxDriving + }; + + // Query the Map Matching API + _mapMatcherApi.Match(mapMatchingResource, (MapMatchingResponse response) => + { + if (response.HasMatchingError) + { + Debug.LogError($"Map matching error: {response.MatchingError}"); + return; + } + + if (response.Matchings == null || response.Matchings.Length == 0) + { + Debug.Log("No matches found"); + return; + } + + var match = response.Matchings[0]; + Debug.Log($"Match confidence: {match.Confidence:F2}"); + Debug.Log($"Distance: {match.Distance} meters"); + Debug.Log($"Duration: {match.Duration} seconds"); + Debug.Log($"Matched geometry points: {match.Geometry.Count}"); + }); + } +} +``` + +## What This Does +- Takes a sequence of GPS coordinates (simulating a tracked route) +- Sends them to the Map Matching API +- Returns a cleaned, road-snapped version of the route +- Includes a confidence score indicating how well the trace matched to roads + +## Required Parameters + +### Coordinates +An array of 2-100 coordinates representing the GPS trace: +```csharp +mapMatchingResource.Coordinates = new Vector2d[] +{ + new Vector2d(lat1, lng1), + new Vector2d(lat2, lng2), + // ... more points +}; +``` + +### Profile +The routing profile to use for matching: +```csharp +Profile.MapboxDriving // Match to driving roads +Profile.MapboxDrivingTraffic // Match with traffic considerations +Profile.MapboxWalking // Match to pedestrian paths +Profile.MapboxCycling // Match to cycling routes +``` + +## Optional Parameters + +### Radiuses +Specify the GPS accuracy for each coordinate (in meters). Higher values for noisy traces: +```csharp +mapMatchingResource.Radiuses = new uint[] { 10, 10, 15, 20, 10 }; +// Values between 1-30: lower (1-10) for clean traces, higher (20-30) for noisy +``` + +### Timestamps +Unix timestamps for each coordinate (useful for speed calculations): +```csharp +long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); +mapMatchingResource.Timestamps = new long[] +{ + now, + now + 5, // 5 seconds later + now + 10, // 10 seconds later + now + 15, + now + 20 +}; +``` + +### Steps +Get turn-by-turn instructions: +```csharp +mapMatchingResource.Steps = true; +``` + +### Overview +Control the geometry detail level: +```csharp +mapMatchingResource.Overview = Overview.Full; // Most detailed +mapMatchingResource.Overview = Overview.Simplified; // Simplified (default) +mapMatchingResource.Overview = Overview.None; // No geometry +``` + +### Tidy +Remove GPS trace clusters and re-sample for better matching: +```csharp +mapMatchingResource.Tidy = true; +``` + +### Annotations +Request additional metadata: +```csharp +mapMatchingResource.Annotations = Annotations.Duration | Annotations.Distance | Annotations.Speed; +``` + +### Geometries +Specify the geometry format: +```csharp +mapMatchingResource.Geometries = Geometries.Polyline; // Default, precision 5 +mapMatchingResource.Geometries = Geometries.Polyline6; // Precision 6 +mapMatchingResource.Geometries = Geometries.GeoJson; // GeoJSON format +``` + +## Response Data + +The `MapMatchingResponse` contains: + +### Error Checking +```csharp +if (response.HasMatchingError) +{ + Debug.LogError(response.MatchingError); + return; +} +``` + +### Matchings Array +Each `MatchObject` includes: +- `Confidence` - Float between 0 (low) and 1 (high) indicating match quality +- `Distance` - Total matched route distance in meters +- `Duration` - Estimated travel time in seconds +- `Geometry` - The road-snapped route as Vector2d points +- `Weight` - Route weight (duration-based) +- `Legs` - Individual segments if multiple waypoints + +### Tracepoints Array +Information about each input coordinate: +- `Location` - The snapped location on the road network +- `Name` - Name of the matched street/road +- `WaypointIndex` - Index in the matched route +- `MatchingsIndex` - Which matching this belongs to +- `AlternativesCount` - Number of alternative matches (0 = unambiguous match) + +## Common Use Cases + +### Clean GPS Tracking Data +```csharp +// Convert noisy GPS logs to clean routes +var gpsLog = LoadGPSLog(); // Your GPS coordinates +var matchRequest = new MapMatchingResource +{ + Coordinates = gpsLog, + Profile = Profile.MapboxDriving, + Tidy = true, // Remove clusters + Radiuses = GetRadiuses(gpsLog) // Set based on GPS accuracy +}; +``` + +### Analyze Traveled Routes +```csharp +// Get detailed information about a traveled route +var matchRequest = new MapMatchingResource +{ + Coordinates = recordedPath, + Profile = Profile.MapboxDriving, + Annotations = Annotations.Duration | Annotations.Distance | Annotations.Speed, + Steps = true // Get turn-by-turn of actual path taken +}; +``` + +### Visualize Movement Patterns +```csharp +// Match and visualize user movement +var matchRequest = new MapMatchingResource +{ + Coordinates = userPath, + Profile = Profile.MapboxWalking, + Overview = Overview.Full // Get full geometry for drawing +}; +``` + +## Differences from Directions API + +| Feature | Directions API | Map Matching API | +|---------|---------------|------------------| +| Purpose | Find optimal route | Match GPS trace to roads | +| Input | 2-25 waypoints | 2-100 coordinates | +| Output | Optimal calculated route | Road-snapped actual path | +| Use Case | Navigation | GPS trace analysis | +| Path Adherence | Optimized | Follows input path | + +--- diff --git a/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs b/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs new file mode 100644 index 000000000..384575b8c --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs @@ -0,0 +1,26 @@ +using System; + +namespace Mapbox.BaseModule.Utilities.Attributes +{ + /// + /// Attribute to specify the URL string representation of an enum value. + /// Used for converting enum values to their API URL parameter equivalents. + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class MapboxRequestStringAttribute : Attribute + { + private readonly string urlString; + + /// Gets the URL string value. + public string Value { get { return urlString; } } + + /// + /// Initializes a new instance of the MapboxRequestStringAttribute. + /// + /// The URL string representation of the enum value. + public MapboxRequestStringAttribute(string urlString) + { + this.urlString = urlString; + } + } +} diff --git a/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs.meta b/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs.meta new file mode 100644 index 000000000..9f407219b --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dcd69265fabd54b68849c8404f514397 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs b/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs new file mode 100644 index 000000000..f632c6928 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Reflection; +using Mapbox.BaseModule.Utilities.Attributes; + +namespace Mapbox.BaseModule.Utilities +{ + /// + /// Extension methods for Enum types. + /// + public static class EnumExtensions + { + /// + /// Gets the URL string representation of an enum value. + /// Looks for the UrlStringAttribute on the enum value. + /// If no attribute is found, returns the enum's ToString() value. + /// + /// The enum value. + /// The URL string from the UrlStringAttribute, or ToString() if not found. + public static string MapboxTypeDescription(this Enum value) + { + if (value == null) + { + return string.Empty; + } + + Type type = value.GetType(); + string name = Enum.GetName(type, value); + + if (string.IsNullOrEmpty(name)) + { + return value.ToString(); + } + + FieldInfo field = type.GetField(name); + + if (field == null) + { + return value.ToString(); + } + + // Try to get the UrlStringAttribute + MapboxRequestStringAttribute attribute = Attribute.GetCustomAttribute(field, typeof(MapboxRequestStringAttribute)) as MapboxRequestStringAttribute; + + if (attribute != null) + { + return attribute.Value; + } + + // Fallback to ToString if no attribute found + return value.ToString(); + } + } +} diff --git a/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs.meta b/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs.meta new file mode 100644 index 000000000..802ac2edd --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 71ba70e686ffd4723872a8d98a7af3e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/DirectionsApi/Example.meta b/Runtime/Mapbox/DirectionsApi/Example.meta new file mode 100644 index 000000000..5af60e229 --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0ba43b7487748483f9602f4aec0e1766 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs b/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs new file mode 100644 index 000000000..a21905a05 --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs @@ -0,0 +1,113 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.DirectionsApi.Response; +using UnityEngine; + +namespace Mapbox.DirectionsApi.Example +{ + /// + /// Example script demonstrating basic usage of the Mapbox Directions API. + /// Calculates a route between two coordinates and logs the result. + /// + public class DirectionsExample : MonoBehaviour + { + [Header("Map Reference")] + public MapBehaviourCore MapCore; + + [Header("Route Settings")] + public RoutingProfile.RoutingProfileOptions Profile = RoutingProfile.RoutingProfileOptions.Driving; + + [Header("Test Coordinates (Default: SF to LA)")] + public double OriginLatitude = 37.7749; + public double OriginLongitude = -122.4194; + public double DestinationLatitude = 34.0522; + public double DestinationLongitude = -118.2437; + + private MapboxDirectionsApi _directionsApi; + + void Start() + { + if (MapCore == null) + { + Debug.LogError("MapCore is not assigned! Please assign a MapBehaviourCore component."); + return; + } + + MapCore.Initialized += OnMapInitialized; + } + + void OnMapInitialized(Mapbox.BaseModule.Map.MapboxMap map) + { + _directionsApi = new MapboxDirectionsApi(map.MapService.FileSource); + Debug.Log("Directions API initialized. Getting route..."); + GetRoute(); + } + + public void GetRoute() + { + if (_directionsApi == null) + { + Debug.LogWarning("Directions API not initialized yet. Waiting for map..."); + return; + } + + // Create coordinates + var origin = new LatitudeLongitude(OriginLatitude, OriginLongitude); + var destination = new LatitudeLongitude(DestinationLatitude, DestinationLongitude); + + // Create a directions request + var directionResource = new DirectionResource( + new[] { origin, destination }, + RoutingProfile.GetProfile(Profile) + ) + { + Alternatives = false, + Steps = false + }; + + // Query the API + _directionsApi.Query(directionResource, HandleDirectionsResponse); + } + + void HandleDirectionsResponse(DirectionsResponse response) + { + if (response == null) + { + Debug.LogError("Directions API returned null response"); + return; + } + + if (response.Routes == null || response.Routes.Count == 0) + { + Debug.LogWarning($"No route found. Response code: {response.Code}"); + return; + } + + // Log the first route + var route = response.Routes[0]; + Debug.Log("=== Route Found ==="); + Debug.Log($"Profile: {Profile}"); + Debug.Log($"Distance: {route.Distance:F2} meters ({route.Distance / 1000:F2} km)"); + Debug.Log($"Duration: {route.Duration:F0} seconds ({route.Duration / 60:F1} minutes)"); + Debug.Log($"Weight: {route.Weight}"); + Debug.Log($"Geometry points: {route.Geometry.Count}"); + + // Log first and last geometry points + if (route.Geometry != null && route.Geometry.Count > 0) + { + var firstPoint = route.Geometry[0]; + var lastPoint = route.Geometry[route.Geometry.Count - 1]; + Debug.Log($"Start point: ({firstPoint.x}, {firstPoint.y})"); + Debug.Log($"End point: ({lastPoint.x}, {lastPoint.y})"); + } + } + + void OnDestroy() + { + if (MapCore != null) + { + MapCore.Initialized -= OnMapInitialized; + } + } + } +} diff --git a/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs.meta b/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs.meta new file mode 100644 index 000000000..8de4d888a --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 25d7947494b1f46b28eeb2a8124c2e4d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs b/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs new file mode 100644 index 000000000..96a5911d7 --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs @@ -0,0 +1,234 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.DirectionsApi.MapMatching; +using UnityEngine; + +namespace Mapbox.DirectionsApi.Example +{ + /// + /// Example script demonstrating the Mapbox Map Matching API. + /// Map matching snaps GPS traces to the road network, cleaning up noisy GPS data. + /// + public class MapMatchingExample : MonoBehaviour + { + [Header("Map Reference")] + public MapBehaviourCore MapCore; + + [Header("Map Matching Settings")] + [Tooltip("Profile to use for matching (driving, walking, cycling, etc.)")] + public Profile MatchingProfile = Profile.MapboxDriving; + + [Tooltip("Request timeout in seconds")] + public int TimeoutSeconds = 30; + + [Header("GPS Trace Configuration")] + [Tooltip("Generate a test GPS trace automatically on start")] + public bool UseTestTrace = true; + + [Header("Test Trace (San Francisco area)")] + [Tooltip("Simulated noisy GPS coordinates")] + public Vector2d[] TestGPSTrace = new Vector2d[] + { + new Vector2d(37.7749, -122.4194), + new Vector2d(37.7751, -122.4193), + new Vector2d(37.7753, -122.4191), + new Vector2d(37.7755, -122.4189), + new Vector2d(37.7757, -122.4187), + new Vector2d(37.7759, -122.4185), + new Vector2d(37.7761, -122.4183), + new Vector2d(37.7763, -122.4181) + }; + + [Header("Optional Parameters")] + [Tooltip("GPS accuracy radius for each point (0 = use default). Higher for noisy traces.")] + public uint DefaultRadius = 10; + + [Tooltip("Enable to remove clusters and resample traces")] + public bool TidyTrace = false; + + [Tooltip("Request turn-by-turn steps")] + public bool IncludeSteps = false; + + [Tooltip("Overview geometry detail")] + public MapMatching.Overview GeometryOverview = MapMatching.Overview.Simplified; + + private MapboxMapMatcherApi _mapMatcherApi; + + void Start() + { + if (MapCore == null) + { + Debug.LogError("MapCore is not assigned! Please assign a MapBehaviourCore component."); + return; + } + + MapCore.Initialized += OnMapInitialized; + } + + void OnMapInitialized(Mapbox.BaseModule.Map.MapboxMap map) + { + _mapMatcherApi = new MapboxMapMatcherApi(map.MapService.FileSource, TimeoutSeconds); + Debug.Log("Map Matching API initialized."); + + if (UseTestTrace) + { + Debug.Log("Matching test GPS trace..."); + MatchGPSTrace(TestGPSTrace); + } + } + + /// + /// Match a GPS trace to the road network. + /// + /// Array of GPS coordinates to match + public void MatchGPSTrace(Vector2d[] gpsTrace) + { + if (_mapMatcherApi == null) + { + Debug.LogWarning("Map Matching API not initialized yet. Waiting for map..."); + return; + } + + if (gpsTrace == null || gpsTrace.Length < 2) + { + Debug.LogError("GPS trace must have at least 2 coordinates"); + return; + } + + if (gpsTrace.Length > 100) + { + Debug.LogError("GPS trace cannot have more than 100 coordinates"); + return; + } + + // Create map matching request + var mapMatchingResource = new MapMatchingResource + { + Coordinates = gpsTrace, + Profile = MatchingProfile, + Steps = IncludeSteps, + Overview = GeometryOverview, + Tidy = TidyTrace + }; + + // Set radiuses if specified + if (DefaultRadius > 0) + { + uint[] radiuses = new uint[gpsTrace.Length]; + for (int i = 0; i < radiuses.Length; i++) + { + radiuses[i] = DefaultRadius; + } + mapMatchingResource.Radiuses = radiuses; + } + + Debug.Log($"Sending map matching request with {gpsTrace.Length} coordinates..."); + + // Execute the map matching request + _mapMatcherApi.Match(mapMatchingResource, HandleMapMatchingResponse); + } + + void HandleMapMatchingResponse(MapMatchingResponse response) + { + // Check for request errors + if (response.HasRequestError) + { + Debug.LogError("Map matching request failed:"); + foreach (var ex in response.RequestExceptions) + { + Debug.LogError($" {ex.Message}"); + } + return; + } + + // Check for matching errors + if (response.HasMatchingError) + { + Debug.LogError($"Map matching error: {response.MatchingError}"); + Debug.LogError($"Error code: {response.Code}"); + if (!string.IsNullOrEmpty(response.Message)) + { + Debug.LogError($"Message: {response.Message}"); + } + return; + } + + // Check if we got any matches + if (response.Matchings == null || response.Matchings.Length == 0) + { + Debug.LogWarning("No matches found for GPS trace"); + return; + } + + Debug.Log($"=== Map Matching Results ==="); + Debug.Log($"Found {response.Matchings.Length} matching(s)"); + + // Display each matching + for (int i = 0; i < response.Matchings.Length; i++) + { + var match = response.Matchings[i]; + Debug.Log($"\n--- Matching {i + 1} ---"); + Debug.Log($"Confidence: {match.Confidence:F2} (0=low, 1=high)"); + Debug.Log($"Distance: {match.Distance:F2} meters ({match.Distance / 1000:F2} km)"); + Debug.Log($"Duration: {match.Duration:F0} seconds ({match.Duration / 60:F1} minutes)"); + Debug.Log($"Weight: {match.Weight}"); + + if (match.Geometry != null) + { + Debug.Log($"Matched geometry points: {match.Geometry.Count}"); + Debug.Log($"Start: ({match.Geometry[0].x:F6}, {match.Geometry[0].y:F6})"); + Debug.Log($"End: ({match.Geometry[match.Geometry.Count - 1].x:F6}, {match.Geometry[match.Geometry.Count - 1].y:F6})"); + } + + if (match.Legs != null && match.Legs.Count > 0) + { + Debug.Log($"Route legs: {match.Legs.Count}"); + } + } + + // Display tracepoint information + if (response.Tracepoints != null && response.Tracepoints.Length > 0) + { + Debug.Log($"\n=== Tracepoints ({response.Tracepoints.Length}) ==="); + int unambiguousMatches = 0; + + for (int i = 0; i < response.Tracepoints.Length; i++) + { + var tracepoint = response.Tracepoints[i]; + if (tracepoint == null) + { + Debug.Log($"Tracepoint {i}: Omitted (removed by Tidy or unmatched)"); + continue; + } + + if (tracepoint.AlternativesCount == 0) + { + unambiguousMatches++; + } + + Debug.Log($"Tracepoint {i}:"); + Debug.Log($" Name: {tracepoint.Name}"); + Debug.Log($" Location: ({tracepoint.Location[1]}, {tracepoint.Location[0]})"); // lat, lng + Debug.Log($" Waypoint Index: {tracepoint.WaypointIndex}"); + Debug.Log($" Alternatives Count: {tracepoint.AlternativesCount} {(tracepoint.AlternativesCount == 0 ? "(unambiguous)" : "")}"); + } + + Debug.Log($"\nUnambiguous matches: {unambiguousMatches}/{response.Tracepoints.Length}"); + } + + // Summary + var bestMatch = response.Matchings[0]; + Debug.Log($"\n=== Summary ==="); + Debug.Log($"Best match confidence: {bestMatch.Confidence:F2}"); + Debug.Log($"GPS trace cleaned and snapped to {bestMatch.Geometry.Count} road points"); + } + + void OnDestroy() + { + if (MapCore != null) + { + MapCore.Initialized -= OnMapInitialized; + } + } + } +} diff --git a/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs.meta b/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs.meta new file mode 100644 index 000000000..a015a2e39 --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ab2553fdd4a94bd0bee1cb811ff2802 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingParameters.cs b/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingParameters.cs index 97a313479..822bb7695 100644 --- a/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingParameters.cs +++ b/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingParameters.cs @@ -12,13 +12,13 @@ namespace Mapbox.DirectionsApi.MapMatching /// Directions profile id public enum Profile { - [Description("mapbox/driving")] + [MapboxRequestString("mapbox/driving")] MapboxDriving, - [Description("mapbox/driving-traffic")] + [MapboxRequestString("mapbox/driving-traffic")] MapboxDrivingTraffic, - [Description("mapbox/walking")] + [MapboxRequestString("mapbox/walking")] MapboxWalking, - [Description("mapbox/cycling")] + [MapboxRequestString("mapbox/cycling")] MapboxCycling } @@ -27,13 +27,13 @@ public enum Profile public enum Geometries { /// Default, precision 5. - [Description("polyline")] + [MapboxRequestString("polyline")] Polyline, /// Precision 6. - [Description("polyline6")] + [MapboxRequestString("polyline6")] Polyline6, /// Geojson. - [Description("geojson")] + [MapboxRequestString("geojson")] GeoJson } @@ -42,13 +42,13 @@ public enum Geometries public enum Overview { /// The most detailed geometry available - [Description("full")] + [MapboxRequestString("full")] Full, /// A simplified version of the full geometry - [Description("simplified")] + [MapboxRequestString("simplified")] Simplified, /// No overview geometry - [Description("false")] + [MapboxRequestString("false")] None } @@ -57,14 +57,14 @@ public enum Overview [System.Flags] public enum Annotations { - [Description("duration")] - Duration, - [Description("distance")] - Distance, - [Description("speed")] - Speed, - [Description("congestion")] - Congestion + [MapboxRequestString("duration")] + Duration = 1, + [MapboxRequestString("distance")] + Distance = 2, + [MapboxRequestString("speed")] + Speed = 4, + [MapboxRequestString("congestion")] + Congestion = 8 } @@ -73,41 +73,41 @@ public enum Annotations /// public enum InstructionLanguages { - [Description("de")] + [MapboxRequestString("de")] German, - [Description("en")] + [MapboxRequestString("en")] English, - [Description("eo")] + [MapboxRequestString("eo")] Esperanto, - [Description("es")] + [MapboxRequestString("es")] Spanish, - [Description("es-ES")] + [MapboxRequestString("es-ES")] SpanishSpain, - [Description("fr")] + [MapboxRequestString("fr")] French, - [Description("id")] + [MapboxRequestString("id")] Indonesian, - [Description("it")] + [MapboxRequestString("it")] Italian, - [Description("nl")] + [MapboxRequestString("nl")] Dutch, - [Description("pl")] + [MapboxRequestString("pl")] Polish, - [Description("pt-BR")] + [MapboxRequestString("pt-BR")] PortugueseBrazil, - [Description("ro")] + [MapboxRequestString("ro")] Romanian, - [Description("ru")] + [MapboxRequestString("ru")] Russian, - [Description("sv")] + [MapboxRequestString("sv")] Swedish, - [Description("tr")] + [MapboxRequestString("tr")] Turkish, - [Description("uk")] + [MapboxRequestString("uk")] Ukrainian, - [Description("vi")] + [MapboxRequestString("vi")] Vietnamese, - [Description("zh-Hans")] + [MapboxRequestString("zh-Hans")] ChineseSimplified } diff --git a/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingResource.cs b/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingResource.cs index f8a397bd9..2c77185d8 100644 --- a/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingResource.cs +++ b/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingResource.cs @@ -9,6 +9,8 @@ using System.Linq; using Mapbox.BaseModule.Data.Platform; using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorTile.ExtensionMethods; namespace Mapbox.DirectionsApi.MapMatching { @@ -163,23 +165,22 @@ public override string GetUrl() Dictionary options = new Dictionary(); - return null; - // if (Geometries.HasValue) { options.Add("geometries", Geometries.Value.Description()); } - // if (null != _radiuses) { options.Add("radiuses", GetUrlQueryFromArray(_radiuses, ";")); } - // if (Steps.HasValue) { options.Add("steps", Steps.ToString().ToLower()); } - // if (Overview.HasValue) { options.Add("overview", Overview.Value.Description()); } - // if (null != _timestamps) { options.Add("timestamps", GetUrlQueryFromArray(_timestamps, ";")); } - // if (Annotations.HasValue) { options.Add("annotations", getUrlQueryFromAnnotations(Annotations.Value, ",")); } - // if (Tidy.HasValue) { options.Add("tidy", Tidy.Value.ToString().ToLower()); } - // if (Language.HasValue) { options.Add("language", Language.Value.Description()); } - // - // return - // Constants.Map.BaseAPI - // + _apiEndpoint - // + Profile.Description() + "/" - // + GetUrlQueryFromArray(_coordinates, ";") - // + ".json" - // + EncodeQueryString(options); + if (Geometries.HasValue) { options.Add("geometries", Geometries.Value.MapboxTypeDescription()); } + if (null != _radiuses) { options.Add("radiuses", GetUrlQueryFromArray(_radiuses, ";")); } + if (Steps.HasValue) { options.Add("steps", Steps.Value.ToString().ToLower()); } + if (Overview.HasValue) { options.Add("overview", Overview.Value.MapboxTypeDescription()); } + if (null != _timestamps) { options.Add("timestamps", GetUrlQueryFromArray(_timestamps, ";")); } + if (Annotations.HasValue) { options.Add("annotations", getUrlQueryFromAnnotations(Annotations.Value, ",")); } + if (Tidy.HasValue) { options.Add("tidy", Tidy.Value.ToString().ToLower()); } + if (Language.HasValue) { options.Add("language", Language.Value.MapboxTypeDescription()); } + + return + Constants.Map.BaseAPI + + _apiEndpoint + + Profile.MapboxTypeDescription() + "/" + + GetUrlQueryFromArray(_coordinates, ";") + + ".json" + + EncodeQueryString(options); } @@ -198,7 +199,7 @@ private string getUrlQueryFromAnnotations(Annotations anno, string separator) foreach (var a in Enum.GetValues(typeof(Annotations)).Cast()) { //if current value is set, add its description - //if (a == (anno & a)) { descriptions.Add(a.Description()); } + if (a == (anno & a)) { descriptions.Add(a.MapboxTypeDescription()); } } return string.Join(separator, descriptions.ToArray()); diff --git a/Runtime/Mapbox/GeocodingApi/Example.meta b/Runtime/Mapbox/GeocodingApi/Example.meta new file mode 100644 index 000000000..2d6725b9f --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 86a1539c780c646b1a14b57f16d0730d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs b/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs new file mode 100644 index 000000000..c9ef2f212 --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs @@ -0,0 +1,157 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using UnityEngine; + +namespace Mapbox.GeocodingApi.Example +{ + /// + /// Example script demonstrating forward geocoding - converting place names to coordinates. + /// Forward geocoding takes a text query (address, place name, etc.) and returns geographic coordinates. + /// + public class ForwardGeocodeExample : MonoBehaviour + { + [Header("Map Reference")] + public MapBehaviourCore MapCore; + + [Header("Search Query")] + [Tooltip("The place name or address to search for")] + public string SearchQuery = "San Francisco, CA"; + + [Header("Optional Filters")] + [Tooltip("Limit results to specific countries (ISO 3166-1 alpha-2 codes, e.g., 'us', 'gb', 'de')")] + public string[] CountryFilter; + + [Tooltip("Enable autocomplete for partial queries")] + public bool EnableAutocomplete = false; + + [Tooltip("Bias results near this location (latitude)")] + public double ProximityLatitude = 0; + + [Tooltip("Bias results near this location (longitude)")] + public double ProximityLongitude = 0; + + [Tooltip("Use proximity bias")] + public bool UseProximity = false; + + private MapboxGeocodingApi _geocodingApi; + + void Start() + { + if (MapCore == null) + { + Debug.LogError("MapCore is not assigned! Please assign a MapBehaviourCore component."); + return; + } + + MapCore.Initialized += OnMapInitialized; + } + + void OnMapInitialized(Mapbox.BaseModule.Map.MapboxMap map) + { + _geocodingApi = new MapboxGeocodingApi(map.MapService.FileSource); + Debug.Log("Geocoding API initialized. Searching for: " + SearchQuery); + SearchPlace(); + } + + /// + /// Perform a forward geocoding search with the configured query and filters. + /// + public void SearchPlace() + { + if (_geocodingApi == null) + { + Debug.LogWarning("Geocoding API not initialized yet. Waiting for map..."); + return; + } + + if (string.IsNullOrEmpty(SearchQuery)) + { + Debug.LogWarning("Search query is empty!"); + return; + } + + // Create forward geocode request + var forwardGeocodeResource = new ForwardGeocodeResource(SearchQuery); + + // Apply optional filters + if (CountryFilter != null && CountryFilter.Length > 0) + { + forwardGeocodeResource.Country = CountryFilter; + } + + if (EnableAutocomplete) + { + forwardGeocodeResource.Autocomplete = true; + } + + if (UseProximity) + { + forwardGeocodeResource.Proximity = new Vector2d(ProximityLatitude, ProximityLongitude); + } + + // Execute the geocoding request + _geocodingApi.Geocode(forwardGeocodeResource, HandleForwardGeocodeResponse); + } + + void HandleForwardGeocodeResponse(ForwardGeocodeResponse response) + { + if (response == null) + { + Debug.LogError("Geocoding API returned null response"); + return; + } + + if (response.Features == null || response.Features.Count == 0) + { + Debug.LogWarning($"No results found for query: {SearchQuery}"); + return; + } + + Debug.Log($"=== Found {response.Features.Count} Result(s) ==="); + + // Display all results (typically ordered by relevance) + for (int i = 0; i < response.Features.Count; i++) + { + var feature = response.Features[i]; + Debug.Log($"\n--- Result {i + 1} ---"); + Debug.Log($"Place Name: {feature.PlaceName}"); + Debug.Log($"Text: {feature.Text}"); + Debug.Log($"Coordinates: Lat {feature.Center.x}, Lng {feature.Center.y}"); + Debug.Log($"Place Type: {string.Join(", ", feature.PlaceType)}"); + Debug.Log($"Relevance: {feature.Relevance:F2}"); + + if (!string.IsNullOrEmpty(feature.Address)) + { + Debug.Log($"Address: {feature.Address}"); + } + + // Display context (city, state, country, etc.) + if (feature.Context != null && feature.Context.Count > 0) + { + Debug.Log("Context:"); + foreach (var context in feature.Context) + { + foreach (var kvp in context) + { + Debug.Log($" {kvp.Key}: {kvp.Value}"); + } + } + } + } + + // Highlight the best match + var bestMatch = response.Features[0]; + Debug.Log($"\n=== Best Match ==="); + Debug.Log($"{bestMatch.PlaceName}"); + Debug.Log($"Location: ({bestMatch.Center.x}, {bestMatch.Center.y})"); + } + + void OnDestroy() + { + if (MapCore != null) + { + MapCore.Initialized -= OnMapInitialized; + } + } + } +} diff --git a/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs.meta b/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs.meta new file mode 100644 index 000000000..feff99ad1 --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56c2b9ba9277d45269cb7dd82054fa8d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs b/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs new file mode 100644 index 000000000..f70d46b53 --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs @@ -0,0 +1,172 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using UnityEngine; +using Mapbox.GeocodingApi; + +/// +/// Example script demonstrating reverse geocoding - converting coordinates to place names. +/// Reverse geocoding takes latitude/longitude coordinates and returns human-readable addresses. +/// +public class ReverseGeocodeExample : MonoBehaviour +{ + [Header("Map Reference")] + public MapBehaviourCore MapCore; + + [Header("Coordinates to Reverse Geocode")] + [Tooltip("Latitude of the location to look up (default: Golden Gate Bridge)")] + public double Latitude = 37.8199; + + [Tooltip("Longitude of the location to look up (default: Golden Gate Bridge)")] + public double Longitude = -122.4783; + + [Header("Optional Filters")] + [Tooltip("Filter by place type (e.g., 'address', 'poi', 'place', 'neighborhood')")] + public string[] TypeFilter; + + private MapboxGeocodingApi _geocodingApi; + + void Start() + { + if (MapCore == null) + { + Debug.LogError("MapCore is not assigned! Please assign a MapBehaviourCore component."); + return; + } + + MapCore.Initialized += OnMapInitialized; + } + + void OnMapInitialized(Mapbox.BaseModule.Map.MapboxMap map) + { + _geocodingApi = new MapboxGeocodingApi(map.MapService.FileSource); + Debug.Log($"Geocoding API initialized. Looking up coordinates: ({Latitude}, {Longitude})"); + GetAddressFromCoordinates(); + } + + /// + /// Perform a reverse geocoding lookup with the configured coordinates. + /// + public void GetAddressFromCoordinates() + { + if (_geocodingApi == null) + { + Debug.LogWarning("Geocoding API not initialized yet. Waiting for map..."); + return; + } + + // Create the location to reverse geocode + var location = new LatitudeLongitude(Latitude, Longitude); + + // Create reverse geocode request + var reverseGeocodeResource = new ReverseGeocodeResource(location); + + // Apply optional type filter + if (TypeFilter != null && TypeFilter.Length > 0) + { + reverseGeocodeResource.Types = TypeFilter; + } + + // Execute the reverse geocoding request + _geocodingApi.Geocode(reverseGeocodeResource, HandleReverseGeocodeResponse); + } + + void HandleReverseGeocodeResponse(ReverseGeocodeResponse response) + { + if (response == null) + { + Debug.LogError("Reverse geocoding API returned null response"); + return; + } + + if (response.Features == null || response.Features.Count == 0) + { + Debug.LogWarning($"No address found for coordinates: ({Latitude}, {Longitude})"); + return; + } + + Debug.Log($"=== Found {response.Features.Count} Feature(s) at Location ==="); + Debug.Log($"Query Coordinates: [{response.Query[0]}, {response.Query[1]}] (Lng, Lat)"); + + // Display all features at this location + for (int i = 0; i < response.Features.Count; i++) + { + var feature = response.Features[i]; + Debug.Log($"\n--- Feature {i + 1} ---"); + Debug.Log($"Place Name: {feature.PlaceName}"); + Debug.Log($"Text: {feature.Text}"); + Debug.Log($"Place Type: {string.Join(", ", feature.PlaceType)}"); + Debug.Log($"Relevance: {feature.Relevance:F2}"); + + if (!string.IsNullOrEmpty(feature.Address)) + { + Debug.Log($"Street Address: {feature.Address}"); + } + + // Display hierarchical context (neighborhood, city, state, country, etc.) + if (feature.Context != null && feature.Context.Count > 0) + { + Debug.Log("Location Context:"); + foreach (var context in feature.Context) + { + foreach (var kvp in context) + { + // Common keys: 'text' (name), 'short_code' (code) + if (kvp.Key == "text") + { + Debug.Log($" {kvp.Value}"); + } + } + } + } + } + + // Highlight the most specific result (usually first) + var primaryFeature = response.Features[0]; + Debug.Log($"\n=== Primary Address ==="); + Debug.Log(primaryFeature.PlaceName); + + // Build a simplified address string + string simpleAddress = primaryFeature.Text; + if (primaryFeature.Context != null) + { + foreach (var context in primaryFeature.Context) + { + if (context.ContainsKey("text")) + { + simpleAddress += ", " + context["text"]; + } + } + } + Debug.Log($"Simple Address: {simpleAddress}"); + } + + void OnDestroy() + { + if (MapCore != null) + { + MapCore.Initialized -= OnMapInitialized; + } + } + + // Optional: Helper method to reverse geocode from a Unity world position + // Useful for getting addresses from map clicks + public void GetAddressFromWorldPosition(Vector3 worldPosition) + { + if (MapCore == null || MapCore.InitializationStatus <= InitializationStatus.Initialized) + { + Debug.LogWarning("Map not initialized yet"); + return; + } + + // Convert world position to lat/lng + var latLng = MapCore.MapInformation.ConvertPositionToLatLng(worldPosition); + + // Update the inspector values + Latitude = latLng.Latitude; + Longitude = latLng.Longitude; + + // Perform reverse geocoding + GetAddressFromCoordinates(); + } +} diff --git a/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs.meta b/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs.meta new file mode 100644 index 000000000..c595e79d8 --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8fabaff234de48a0b6ec6cb3c052866 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 3ebd4cee30c248efd2e06bc79873a31cc4eb3f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Wed, 11 Mar 2026 10:51:31 +0300 Subject: [PATCH 08/54] Location module changes (#1981) add mapbox common location module support handle device permissions better change map initialization process for better startup procedure --- Editor/MapboxConfigurationWindow.cs | 1 + .../Mapbox/BaseModule/Map/MapboxContext.cs | 67 ++- .../BaseModule/Plugins/iOS/LocationBridge.mm | 217 ++++++++ .../Plugins/iOS/LocationBridge.mm.meta | 42 ++ .../BaseModule/Plugins/iOS/locationBridge.h | 37 ++ .../Plugins/iOS/locationBridge.h.meta | 81 +++ .../BaseModule/Telemetry/TelemetryAndroid.cs | 9 - .../Mapbox/BaseModule/Unity/UnityContext.cs | 164 +++++- .../BaseModule/Utilities/MapBehaviourCore.cs | 5 +- .../Editor/MapboxMapBehaviourEditor.cs | 5 +- .../Example/Scripts/Map/MapboxMapBehaviour.cs | 40 +- .../LocationModule/DeviceLocationProvider.cs | 346 ------------ .../DeviceLocationProviderAndroidNative.cs | 417 -------------- Runtime/Mapbox/LocationModule/Editor.meta | 8 + .../Editor/LocationProviderFactoryEditor.cs | 88 +++ .../LocationProviderFactoryEditor.cs.meta | 2 +- .../Mapbox.LocationModule.Editor.asmdef | 18 + .../Mapbox.LocationModule.Editor.asmdef.meta | 7 + .../{ => Example}/ExampleGpsTraces.meta | 0 .../ExampleGpsTraces/Helsinki.txt | 0 .../ExampleGpsTraces/Helsinki.txt.meta | 0 .../LocationProviderAndroidNative-2.txt | 0 .../LocationProviderAndroidNative-2.txt.meta | 0 .../LocationProviderAndroidNative.txt | 0 .../LocationProviderAndroidNative.txt.meta | 0 .../LocationProviderUnity.txt | 0 .../LocationProviderUnity.txt.meta | 0 Runtime/Mapbox/LocationModule/Helpers.meta | 8 + .../Helpers/ShiftMapForDistance.cs | 51 ++ .../ShiftMapForDistance.cs.meta} | 4 +- .../SnapTransformToLocationProvider.cs | 22 +- .../SnapTransformToLocationProvider.cs.meta | 0 .../LocationModule/LocationProviderFactory.cs | 185 ------- .../Mapbox/LocationModule/MapboxLocation.meta | 8 + .../MapboxLocation/MapboxLocationAndroid.cs | 341 ++++++++++++ .../MapboxLocationAndroid.cs.meta | 3 + .../MapboxLocation/MapboxLocationIos.cs | 324 +++++++++++ .../MapboxLocation/MapboxLocationIos.cs.meta | 11 + .../MapboxLocationObserverProxy.cs | 106 ++++ .../MapboxLocationObserverProxy.cs.meta | 3 + .../MapboxLocationPermissionsListenerProxy.cs | 39 ++ ...oxLocationPermissionsListenerProxy.cs.meta | 3 + .../MapboxLocation/MapboxLocationProvider.cs | 49 ++ .../MapboxLocationProvider.cs.meta | 3 + .../MapboxLocationServiceObserverProxy.cs | 63 +++ ...MapboxLocationServiceObserverProxy.cs.meta | 3 + .../MapboxLocation/MapboxLocationSettings.cs | 84 +++ .../MapboxLocationSettings.cs.meta | 3 + Runtime/Mapbox/LocationModule/Scripts.meta | 8 + .../AbstractEditorLocationProvider.cs | 21 +- .../AbstractEditorLocationProvider.cs.meta | 0 .../{ => Scripts}/AbstractLocationProvider.cs | 19 +- .../AbstractLocationProvider.cs.meta | 0 .../{ => Scripts}/AngleSmoothing.meta | 0 .../AngleSmoothingAbstractBase.cs | 5 +- .../AngleSmoothingAbstractBase.cs.meta | 0 .../AngleSmoothing/AngleSmoothingAverage.cs | 0 .../AngleSmoothingAverage.cs.meta | 0 .../AngleSmoothing/AngleSmoothingEMA.cs | 3 +- .../AngleSmoothing/AngleSmoothingEMA.cs.meta | 0 .../AngleSmoothing/AngleSmoothingLowPass.cs | 0 .../AngleSmoothingLowPass.cs.meta | 0 .../AngleSmoothing/AngleSmoothingNoOp.cs | 0 .../AngleSmoothing/AngleSmoothingNoOp.cs.meta | 0 .../AngleSmoothing/IAngleSmoothing.cs | 0 .../AngleSmoothing/IAngleSmoothing.cs.meta | 0 .../{ => Scripts}/CircularBuffer.cs | 2 +- .../{ => Scripts}/CircularBuffer.cs.meta | 0 .../EditorLocationProviderLocationLog.cs | 0 .../EditorLocationProviderLocationLog.cs.meta | 0 .../Scripts/GetLocationCallbackProxy.cs | 51 ++ .../Scripts/GetLocationCallbackProxy.cs.meta | 3 + .../{ => Scripts}/ILocationProvider.cs | 4 + .../{ => Scripts}/ILocationProvider.cs.meta | 0 .../Scripts/IMapboxDeviceLocation.cs | 16 + .../Scripts/IMapboxDeviceLocation.cs.meta | 3 + .../LocationModule/{ => Scripts}/Location.cs | 12 +- .../{ => Scripts}/Location.cs.meta | 0 .../LocationArrayEditorLocationProvider.cs | 1 - ...ocationArrayEditorLocationProvider.cs.meta | 0 .../Scripts/LocationProviderFactory.cs | 125 +++++ .../LocationProviderFactory.cs.meta | 0 .../{ => Scripts}/LocationSmoothing.meta | 0 .../LocationSmoothing/KalmanFilter.cs | 0 .../LocationSmoothing/KalmanFilter.cs.meta | 0 .../LocationModule/{ => Scripts}/Logging.meta | 0 .../Logging/LocationLogAbstractBase.cs | 0 .../Logging/LocationLogAbstractBase.cs.meta | 0 .../Logging/LocationLogReader.cs | 1 - .../Logging/LocationLogReader.cs.meta | 0 .../Logging/LocationLogWriter.cs | 2 - .../Logging/LocationLogWriter.cs.meta | 0 .../{ => Scripts}/UnityLocationWrappers.meta | 0 .../IMapboxLocationInfo.cs | 0 .../IMapboxLocationInfo.cs.meta | 0 .../IMapboxLocationService.cs | 4 +- .../IMapboxLocationService.cs.meta | 0 .../MapboxLocationInfoMock.cs | 0 .../MapboxLocationInfoMock.cs.meta | 0 .../MapboxLocationInfoUnityWrapper.cs | 0 .../MapboxLocationInfoUnityWrapper.cs.meta | 0 .../MapboxLocationServiceMock.cs | 3 +- .../MapboxLocationServiceMock.cs.meta | 0 .../MapboxLocationServiceUnityWrapper.cs | 2 +- .../MapboxLocationServiceUnityWrapper.cs.meta | 0 ...nProvider.cs => StaticLocationProvider.cs} | 33 +- ...cs.meta => StaticLocationProvider.cs.meta} | 0 .../LocationModule/UnityLocationProvider.cs | 227 ++++++++ ....cs.meta => UnityLocationProvider.cs.meta} | 0 .../Scripts/Logging/LoggingMapBehaviour.cs | 6 +- Runtime/Mapbox/MapDebug/Scripts/TestMap.cs | 4 +- Samples~/LocationBasedGame/LocationGame.unity | 519 +++++++++++++++--- .../LocationGameWithPois.unity | 118 ++-- .../Scripts/SnapMapToLocationProvider.cs | 47 -- .../Editor/BaseModule/DataCompressionTests.cs | 1 + Tests/Editor/BaseModule/FileSourceTests.cs | 1 + Tests/Editor/BaseModule/TerrainSourceTests.cs | 1 + Tests/Editor/BaseModule/TileJSONTest.cs | 1 + Tests/Editor/BaseModule/TokenTest.cs | 1 + Tests/Editor/LocationModule.meta | 8 + .../LocationModule/LocationDataTests.cs | 264 +++++++++ .../LocationModule/LocationDataTests.cs.meta | 11 + .../LocationModule/LocationPermissionTests.cs | 78 +++ .../LocationPermissionTests.cs.meta | 11 + .../Mapbox.LocationModule.Tests.asmdef | 21 + .../Mapbox.LocationModule.Tests.asmdef.meta | 7 + .../MapboxLocationProviderTests.cs | 195 +++++++ .../MapboxLocationProviderTests.cs.meta | 11 + .../LocationModule/TestLocationServiceMock.cs | 89 +++ .../TestLocationServiceMock.cs.meta | 11 + .../UnityLocationProviderTests.cs | 88 +++ .../UnityLocationProviderTests.cs.meta | 11 + Tests/Runtime/BaseModule/ImageSourceTests.cs | 1 + .../BaseModule/LocationGamePrefabTests.cs | 2 +- Tests/Runtime/BaseModule/MapTest.cs | 1 + Tests/Runtime/BaseModule/MapViewLoadTests.cs | 1 + Tests/Runtime/BaseModule/MapboxMapTests.cs | 2 + Tests/Runtime/BaseModule/PbfSourceTests.cs | 1 + Tests/Runtime/BaseModule/RasterDataTests.cs | 39 +- Tests/Runtime/BaseModule/SceneTests.cs | 2 +- Tests/Runtime/BaseModule/VectorDataTests.cs | 23 +- 141 files changed, 3708 insertions(+), 1280 deletions(-) create mode 100644 Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm create mode 100644 Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm.meta create mode 100644 Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h create mode 100644 Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h.meta delete mode 100644 Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs delete mode 100644 Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs create mode 100644 Runtime/Mapbox/LocationModule/Editor.meta create mode 100644 Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs rename Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs.meta => Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs.meta (83%) create mode 100644 Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef create mode 100644 Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef.meta rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces/Helsinki.txt (100%) rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces/Helsinki.txt.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces/LocationProviderAndroidNative-2.txt (100%) rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces/LocationProviderAndroidNative.txt (100%) rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces/LocationProviderUnity.txt (100%) rename Runtime/Mapbox/LocationModule/{ => Example}/ExampleGpsTraces/LocationProviderUnity.txt.meta (100%) create mode 100644 Runtime/Mapbox/LocationModule/Helpers.meta create mode 100644 Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs rename Runtime/Mapbox/LocationModule/{DeviceLocationProviderAndroidNative.cs.meta => Helpers/ShiftMapForDistance.cs.meta} (71%) rename {Samples~/LocationBasedGame/Scripts => Runtime/Mapbox/LocationModule/Helpers}/SnapTransformToLocationProvider.cs (67%) rename {Samples~/LocationBasedGame/Scripts => Runtime/Mapbox/LocationModule/Helpers}/SnapTransformToLocationProvider.cs.meta (100%) delete mode 100644 Runtime/Mapbox/LocationModule/LocationProviderFactory.cs create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation.meta create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs.meta create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs.meta create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs.meta create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs.meta create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs.meta create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs.meta create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs create mode 100644 Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs.meta create mode 100644 Runtime/Mapbox/LocationModule/Scripts.meta rename Runtime/Mapbox/LocationModule/{ => Scripts}/AbstractEditorLocationProvider.cs (61%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AbstractEditorLocationProvider.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AbstractLocationProvider.cs (54%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AbstractLocationProvider.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingAbstractBase.cs (92%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingAverage.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingAverage.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingEMA.cs (98%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingEMA.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingLowPass.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingLowPass.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingNoOp.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/AngleSmoothingNoOp.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/IAngleSmoothing.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/AngleSmoothing/IAngleSmoothing.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/CircularBuffer.cs (98%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/CircularBuffer.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/EditorLocationProviderLocationLog.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/EditorLocationProviderLocationLog.cs.meta (100%) create mode 100644 Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs create mode 100644 Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs.meta rename Runtime/Mapbox/LocationModule/{ => Scripts}/ILocationProvider.cs (78%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/ILocationProvider.cs.meta (100%) create mode 100644 Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs create mode 100644 Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs.meta rename Runtime/Mapbox/LocationModule/{ => Scripts}/Location.cs (89%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/Location.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/LocationArrayEditorLocationProvider.cs (98%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/LocationArrayEditorLocationProvider.cs.meta (100%) create mode 100644 Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs rename Runtime/Mapbox/LocationModule/{ => Scripts}/LocationProviderFactory.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/LocationSmoothing.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/LocationSmoothing/KalmanFilter.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/LocationSmoothing/KalmanFilter.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/Logging.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/Logging/LocationLogAbstractBase.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/Logging/LocationLogAbstractBase.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/Logging/LocationLogReader.cs (99%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/Logging/LocationLogReader.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/Logging/LocationLogWriter.cs (97%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/Logging/LocationLogWriter.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/IMapboxLocationInfo.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/IMapboxLocationInfo.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/IMapboxLocationService.cs (81%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/IMapboxLocationService.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/MapboxLocationInfoMock.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/MapboxLocationServiceMock.cs (93%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs (88%) rename Runtime/Mapbox/LocationModule/{ => Scripts}/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta (100%) rename Runtime/Mapbox/LocationModule/{EditorLocationProvider.cs => StaticLocationProvider.cs} (56%) rename Runtime/Mapbox/LocationModule/{EditorLocationProvider.cs.meta => StaticLocationProvider.cs.meta} (100%) create mode 100644 Runtime/Mapbox/LocationModule/UnityLocationProvider.cs rename Runtime/Mapbox/LocationModule/{DeviceLocationProvider.cs.meta => UnityLocationProvider.cs.meta} (100%) delete mode 100644 Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs create mode 100644 Tests/Editor/LocationModule.meta create mode 100644 Tests/Editor/LocationModule/LocationDataTests.cs create mode 100644 Tests/Editor/LocationModule/LocationDataTests.cs.meta create mode 100644 Tests/Editor/LocationModule/LocationPermissionTests.cs create mode 100644 Tests/Editor/LocationModule/LocationPermissionTests.cs.meta create mode 100644 Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef create mode 100644 Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef.meta create mode 100644 Tests/Editor/LocationModule/MapboxLocationProviderTests.cs create mode 100644 Tests/Editor/LocationModule/MapboxLocationProviderTests.cs.meta create mode 100644 Tests/Editor/LocationModule/TestLocationServiceMock.cs create mode 100644 Tests/Editor/LocationModule/TestLocationServiceMock.cs.meta create mode 100644 Tests/Editor/LocationModule/UnityLocationProviderTests.cs create mode 100644 Tests/Editor/LocationModule/UnityLocationProviderTests.cs.meta diff --git a/Editor/MapboxConfigurationWindow.cs b/Editor/MapboxConfigurationWindow.cs index 41c185783..b197c0e1e 100644 --- a/Editor/MapboxConfigurationWindow.cs +++ b/Editor/MapboxConfigurationWindow.cs @@ -105,6 +105,7 @@ private static void WriteConfigFile(MapboxConfiguration config, string path) static void OpenWindow() { _mapboxContext = new MapboxContext(); + _mapboxContext.LoadConfigurationWithoutValidation(); EditorApplication.delayCall -= OpenWindow; //instantiate the config window instance = GetWindow(typeof(MapboxConfigurationWindow)) as MapboxConfigurationWindow; diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs b/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs index 2bb6fd435..c63246993 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.IO; using Mapbox.BaseModule; using Mapbox.BaseModule.Telemetry; @@ -17,7 +18,20 @@ public class MapboxContext : IMapboxContext public MapboxContext() { - LoadConfiguration(); + } + + public IEnumerator Initialize() + { + yield return LoadConfigurationCoroutine(); + } + + /// + /// Load configuration without token validation. + /// For editor tooling only — not for runtime use. + /// + public void LoadConfigurationWithoutValidation() + { + Configuration = LoadAndParseConfig(); } public string GetAccessToken() @@ -38,7 +52,7 @@ public MapboxTokenStatus TokenStatus() return _mapboxToken.Status; } - private void LoadConfiguration() + private MapboxConfiguration LoadAndParseConfig() { TextAsset configurationTextAsset = Resources.Load(Constants.Path.MAPBOX_RESOURCES_RELATIVE); if (null == configurationTextAsset) @@ -49,22 +63,49 @@ private void LoadConfiguration() var config = JsonUtility.FromJson(configurationTextAsset.text); config.Initialize(); + return config; + } + + private void HandleTokenResponse(MapboxConfiguration config, MapboxToken response) + { + _mapboxToken = response; + Configuration = config; + if (_mapboxToken.Status != MapboxTokenStatus.TokenValid) + { + config.AccessToken = string.Empty; + Debug.LogError("Invalid Token"); + } + else + { + ConfigureTelemetry(); + } + } + + private const float TokenValidationTimeoutSeconds = 10f; + + private IEnumerator LoadConfigurationCoroutine() + { + var config = LoadAndParseConfig(); var tokenValidator = new MapboxTokenApi(); + var configLoaded = false; tokenValidator.Retrieve(config.GetMapsSkuToken, config.AccessToken, (response) => { - _mapboxToken = response; - if (_mapboxToken.Status != MapboxTokenStatus.TokenValid) - { - config.AccessToken = string.Empty; - Debug.LogError("Invalid Token"); - } - else - { - ConfigureTelemetry(); - } + HandleTokenResponse(config, response); + configLoaded = true; }); - Configuration = config; + var elapsed = 0f; + while (!configLoaded) + { + elapsed += Time.deltaTime; + if (elapsed >= TokenValidationTimeoutSeconds) + { + Debug.LogError("Token validation timed out. Proceeding with unvalidated configuration."); + Configuration = config; + break; + } + yield return null; + } } private void ConfigureTelemetry() diff --git a/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm b/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm new file mode 100644 index 000000000..13caaa4dc --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm @@ -0,0 +1,217 @@ +#import +#import +#import +#import +#import +#import +#import "locationBridge.h" + +// Objective-C++ observer class +@interface LocationObserverBridge : NSObject +@property (nonatomic, assign) LocationUpdateCallback callback; +- (instancetype)initWithCallback:(LocationUpdateCallback)callback; +- (void)onLocationUpdateReceivedForLocations:(NSArray *)locations; +@end + +@implementation LocationObserverBridge + +- (instancetype)initWithCallback:(LocationUpdateCallback)callback { + self = [super init]; + if (self) { + _callback = callback; + } + return self; +} + +- (void)onLocationUpdateReceivedForLocations:(NSArray *)locations { + if (_callback && locations.count > 0) { + MBXLocation* loc = locations.firstObject; + + // Extract location data, handling optional NSNumber properties + double latitude = loc.latitude; + double longitude = loc.longitude; + float accuracy = loc.horizontalAccuracy ? [loc.horizontalAccuracy floatValue] : 0.0f; + // Convert timestamp from milliseconds to seconds + double timestamp = loc.timestamp / 1000.0; + double altitude = loc.altitude ? [loc.altitude doubleValue] : 0.0; + float speed = loc.speed ? [loc.speed floatValue] : 0.0f; + float bearing = loc.bearing ? [loc.bearing floatValue] : 0.0f; + + // Call the C# callback + _callback(latitude, longitude, accuracy, timestamp, altitude, speed, bearing); + } +} + +@end + +// Objective-C++ service observer class +@interface LocationServiceObserverBridge : NSObject +@property (nonatomic, assign) AuthorizationStatusCallback authCallback; +@property (nonatomic, assign) AccuracyAuthorizationCallback accuracyCallback; +@property (nonatomic, assign) AvailabilityCallback availabilityCallback; +- (instancetype)initWithCallbacks:(AuthorizationStatusCallback)authCallback + accuracyCallback:(AccuracyAuthorizationCallback)accuracyCallback + availabilityCallback:(AvailabilityCallback)availabilityCallback; +@end + +@implementation LocationServiceObserverBridge + +- (instancetype)initWithCallbacks:(AuthorizationStatusCallback)authCallback + accuracyCallback:(AccuracyAuthorizationCallback)accuracyCallback + availabilityCallback:(AvailabilityCallback)availabilityCallback { + self = [super init]; + if (self) { + _authCallback = authCallback; + _accuracyCallback = accuracyCallback; + _availabilityCallback = availabilityCallback; + } + return self; +} + +- (void)onPermissionStatusChangedForPermission:(MBXPermissionStatus)permission { + if (_authCallback) { + _authCallback((int)permission); + } +} + +- (void)onAccuracyAuthorizationChangedForAccuracyAuthorization:(MBXAccuracyAuthorization)accuracyAuthorization { + if (_accuracyCallback) { + _accuracyCallback((int)accuracyAuthorization); + } +} + +- (void)onAvailabilityChangedForIsAvailable:(BOOL)isAvailable { + if (_availabilityCallback) { + _availabilityCallback(isAvailable); + } +} + +@end + +// Global storage for observers and providers (must persist) +static NSMutableDictionary* observerMap = nil; +static NSMutableDictionary>* providerMap = nil; +static CLLocationManager* authLocationManager = nil; +static LocationServiceObserverBridge* serviceObserver = nil; + +// Implementation of extern "C" functions +void requestLocationAuthorization() +{ + if (!authLocationManager) { + authLocationManager = [[CLLocationManager alloc] init]; + } + + CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + + if (status == kCLAuthorizationStatusNotDetermined) { + [authLocationManager requestWhenInUseAuthorization]; + } +} +void* startLocationUpdatesWithSettings( + long long minimumInterval, long long maximumInterval, long long interval, + int accuracyLevel, float displacement, + LocationUpdateCallback callback) +{ + // Check location authorization status + CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + + if (!observerMap) { + observerMap = [NSMutableDictionary dictionary]; + } + if (!providerMap) { + providerMap = [NSMutableDictionary dictionary]; + } + + // Get location service + id service = [MBXLocationServiceFactory getOrCreate]; + + // Create interval settings + MBXIntervalSettings* intervalSettings = [[MBXIntervalSettings alloc] + initWithMinimumInterval:@(minimumInterval) + maximumInterval:@(maximumInterval) + interval:@(interval)]; + + // Create location provider request + MBXLocationProviderRequest* request = [[MBXLocationProviderRequest alloc] + initWithAccuracy:@(accuracyLevel) + displacement:@(displacement) + interval:intervalSettings]; + + // Get device location provider + auto result = [service getDeviceLocationProviderForRequest:request]; + + if (![result isValue]) { + NSLog(@"Failed to get device location provider: %@", result.error); + return NULL; + } + + id provider = result.value; + + // Create and register observer + LocationObserverBridge* observer = [[LocationObserverBridge alloc] initWithCallback:callback]; + + // Store both provider and observer + NSValue* providerKey = [NSValue valueWithPointer:(__bridge void*)provider]; + observerMap[providerKey] = observer; + providerMap[providerKey] = provider; + + // Request authorization if needed before starting location updates + if (status == kCLAuthorizationStatusNotDetermined) { + if (!authLocationManager) { + authLocationManager = [[CLLocationManager alloc] init]; + } + [authLocationManager requestWhenInUseAuthorization]; + } + + // Add observer to provider (this starts location updates) + [provider addLocationObserverForObserver:observer]; + + // Return provider pointer for later cleanup + return (__bridge_retained void*)provider; +} + +void stopLocationUpdates(void* providerPtr) { + if (providerPtr == NULL) { + return; + } + + id provider = (__bridge_transfer id)providerPtr; + NSValue* providerKey = [NSValue valueWithPointer:(__bridge void*)provider]; + LocationObserverBridge* observer = observerMap[providerKey]; + + if (observer) { + [provider removeLocationObserverForObserver:observer]; + [observerMap removeObjectForKey:providerKey]; + [providerMap removeObjectForKey:providerKey]; + } +} + +void addLocationServiceObserver( + AuthorizationStatusCallback authCallback, + AccuracyAuthorizationCallback accuracyCallback, + AvailabilityCallback availabilityCallback) +{ + // Remove existing observer if any + if (serviceObserver) { + removeLocationServiceObserver(); + } + + // Create new service observer + serviceObserver = [[LocationServiceObserverBridge alloc] + initWithCallbacks:authCallback + accuracyCallback:accuracyCallback + availabilityCallback:availabilityCallback]; + + // Get location service and add observer + id service = [MBXLocationServiceFactory getOrCreate]; + [service registerObserverForObserver:serviceObserver]; +} + +void removeLocationServiceObserver() +{ + if (serviceObserver) { + id service = [MBXLocationServiceFactory getOrCreate]; + [service unregisterObserverForObserver:serviceObserver]; + serviceObserver = nil; + } +} diff --git a/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm.meta b/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm.meta new file mode 100644 index 000000000..3d4911964 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm.meta @@ -0,0 +1,42 @@ +fileFormatVersion: 2 +guid: a0bfa1c7fd3274f8f8ebac9e34c9a72a +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + VisionOS: VisionOS + second: + enabled: 1 + settings: {} + - first: + iPhone: iOS + second: + enabled: 1 + settings: {} + - first: + tvOS: tvOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h b/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h new file mode 100644 index 000000000..331db87f2 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h @@ -0,0 +1,37 @@ +#import +#import +#import + +extern "C" { + // Callback type for location updates + typedef void (*LocationUpdateCallback)( + double latitude, double longitude, + float accuracy, double timestamp, + double altitude, float speed, float bearing); + + // Callback types for service observer + typedef void (*AuthorizationStatusCallback)(int status); + typedef void (*AccuracyAuthorizationCallback)(int accuracy); + typedef void (*AvailabilityCallback)(bool available); + + // Request location permissions (call this before starting location updates) + void requestLocationAuthorization(); + + // Create location service and start updates + void* startLocationUpdatesWithSettings( + long long minimumInterval, long long maximumInterval, long long interval, + int accuracyLevel, float displacement, + LocationUpdateCallback callback); + + // Stop location updates + void stopLocationUpdates(void* providerPtr); + + // Add service observer for permission and availability changes + void addLocationServiceObserver( + AuthorizationStatusCallback authCallback, + AccuracyAuthorizationCallback accuracyCallback, + AvailabilityCallback availabilityCallback); + + // Remove service observer + void removeLocationServiceObserver(); +} diff --git a/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h.meta b/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h.meta new file mode 100644 index 000000000..e7b675c03 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h.meta @@ -0,0 +1,81 @@ +fileFormatVersion: 2 +guid: d8ceb9c4682094861b083650966966e3 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + AndroidSharedLibraryType: Executable + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Telemetry/TelemetryAndroid.cs b/Runtime/Mapbox/BaseModule/Telemetry/TelemetryAndroid.cs index 73e601786..a75c44c0b 100644 --- a/Runtime/Mapbox/BaseModule/Telemetry/TelemetryAndroid.cs +++ b/Runtime/Mapbox/BaseModule/Telemetry/TelemetryAndroid.cs @@ -149,15 +149,6 @@ public void SendSdkEvent() public void SetLocationCollectionState(bool enable) { - if (enable) - { - Input.location.Start(); - } - else - { - Input.location.Stop(); - } - _telemetryUtilsClass.CallStatic(_setEventsCollectionStatMethodName, enable, null); } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs b/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs index cc0023808..8df77fa58 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs @@ -1,47 +1,195 @@ using System; +using System.Collections; using Mapbox.BaseModule.Data.Tasks; using UnityEngine; +using UnityEngine.Android; namespace Mapbox.BaseModule.Unity { [Serializable] public class UnityContext { + public Action LocationPermissionStateChanged; + public TaskManager TaskManager; + public LocationPermissionState LocationPermissionState = LocationPermissionState.Waiting; [NonSerialized] public MonoBehaviour CoroutineStarter; + [Tooltip("Root object to hold all map related game objects")] public Transform MapRoot; + [Tooltip("Root object for all tile objects which created the base map")] public Transform BaseTileRoot; + [Tooltip("Root object for all runtime generated visuals. Mainly the vector feature visuals.")] public Transform RuntimeGenerationRoot; - public UnityContext() - { - //TaskManager = new TaskManager(); - } - - public void Initialize(TaskManager providedTaskManager = null) + + private LocationPermissionHandler _locationPermissionHandler = new(); + + public IEnumerator Initialize(TaskManager providedTaskManager = null) { if (TaskManager == null) { TaskManager = providedTaskManager ?? new TaskManager(); } - + TaskManager.Initialize(); BaseTileRoot = BaseTileRoot == null ? new GameObject("BaseTiles").transform : BaseTileRoot; BaseTileRoot.SetParent(MapRoot); BaseTileRoot.transform.localPosition = Vector3.zero; - RuntimeGenerationRoot = RuntimeGenerationRoot == null ? new GameObject("RuntimeObjectsRoot").transform : RuntimeGenerationRoot; + RuntimeGenerationRoot = RuntimeGenerationRoot == null + ? new GameObject("RuntimeObjectsRoot").transform + : RuntimeGenerationRoot; RuntimeGenerationRoot.SetParent(MapRoot); RuntimeGenerationRoot.transform.localPosition = Vector3.zero; + yield return null; } public void OnDestroy() { TaskManager.OnDestroy(); } + + public IEnumerator HandlePermission() + { + yield return _locationPermissionHandler.HandlePermission(); + LocationPermissionState = _locationPermissionHandler.State; + LocationPermissionStateChanged?.Invoke(); + } + } + + public enum LocationPermissionState + { + Waiting, + Granted, + Denied, + DeniedPermanently + } + + public class LocationPermissionHandler + { + public LocationPermissionState State = LocationPermissionState.Waiting; + + public IEnumerator HandlePermission() + { + if (Permission.HasUserAuthorizedPermission(Permission.FineLocation)) + { + State = LocationPermissionState.Granted; + yield break; + } + +#if UNITY_ANDROID && !UNITY_EDITOR + yield return RequestLocationPermissionIfNeeded(); +#elif UNITY_IOS && !UNITY_EDITOR + if (!Input.location.isEnabledByUser) + { + yield return iOSAskPermission(); + } +#elif UNITY_EDITOR + // Editor / non-Android: assume granted + OnPermissionGranted(); +#endif + } + +#if UNITY_IOS && !UNITY_EDITOR + public IEnumerator iOSAskPermission() + { + Input.location.Start(); + + int waitTime = 10; + while (Input.location.status == LocationServiceStatus.Initializing && waitTime > 0) + { + yield return new WaitForSeconds(1); + waitTime--; + } + + if (waitTime <= 0) + { + Debug.LogWarning("Location service init timeout"); + OnPermissionDenied(); + yield break; + } + + if (Input.location.status == LocationServiceStatus.Failed) + { + Debug.LogWarning("Location service failed (likely denied)"); + OnPermissionDenied(); + yield break; + } + + OnPermissionGranted(); + } +#endif + +#if UNITY_ANDROID + private IEnumerator RequestLocationPermissionIfNeeded() + { + var permissionDone = false; + var callbacks = new PermissionCallbacks(); + callbacks.PermissionGranted += permission => + { + if (permission == Permission.FineLocation) + { + permissionDone = true; + OnPermissionGranted(); + } + }; + + callbacks.PermissionDenied += permission => + { + + if (permission == Permission.FineLocation) + { + permissionDone = true; + OnPermissionDenied(); + } + }; + + callbacks.PermissionDeniedAndDontAskAgain += permission => + { + if (permission == Permission.FineLocation) + { + permissionDone = true; + OnPermissionDeniedPermanently(); + } + }; + Permission.RequestUserPermission(Permission.FineLocation, callbacks); + + float elapsed = 0f; + const float timeout = 60f; + while (!permissionDone) + { + elapsed += Time.unscaledDeltaTime; + if (elapsed >= timeout) + { + Debug.LogWarning("Location permission request timed out — no callback received."); + OnPermissionDenied(); + yield break; + } + yield return null; + } + } +#endif + + private void OnPermissionGranted() + { + State = LocationPermissionState.Granted; + Debug.Log("Location permission GRANTED"); + } + + private void OnPermissionDenied() + { + State = LocationPermissionState.Denied; + Debug.Log("Location permission denied"); + } + + private void OnPermissionDeniedPermanently() + { + State = LocationPermissionState.DeniedPermanently; + Debug.LogWarning("Location permission denied permanently. User must enable location in system settings."); + } } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Utilities/MapBehaviourCore.cs b/Runtime/Mapbox/BaseModule/Utilities/MapBehaviourCore.cs index e32977698..d310ffa66 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/MapBehaviourCore.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/MapBehaviourCore.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using Mapbox.BaseModule.Map; using UnityEngine; @@ -15,9 +16,9 @@ public abstract class MapBehaviourCore : MonoBehaviour : InitializationStatus.WaitingForInitialization; public Action Initialized = (m) => { }; - public virtual void Initialize() + public virtual IEnumerator Initialize() { - + yield return null; } } } diff --git a/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs b/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs index 17f2b2c6a..50b7b8caf 100644 --- a/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs +++ b/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs @@ -14,6 +14,7 @@ public class MapboxMapBehaviourEditor : UnityEditor.Editor private SerializedProperty _tileProviderProp; private SerializedProperty _dataFetcherProp; private SerializedProperty _cacheManagerProp; + private SerializedProperty _locationFactoryProp; private SerializedProperty _initializeOnStart; private GUIStyle _headerStyle; @@ -30,6 +31,7 @@ private void OnEnable() _tileProviderProp = serializedObject.FindProperty("TileProvider"); _dataFetcherProp = serializedObject.FindProperty("DataFetcher"); _cacheManagerProp = serializedObject.FindProperty("CacheManager"); + _locationFactoryProp = serializedObject.FindProperty("LocationFactory"); _initializeOnStart = serializedObject.FindProperty("InitializeOnStart"); } @@ -76,9 +78,10 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(_tileProviderProp, new GUIContent("Tile Provider")); EditorGUILayout.PropertyField(_dataFetcherProp, new GUIContent("Data Fetcher")); EditorGUILayout.PropertyField(_cacheManagerProp, new GUIContent("Cache Manager")); + EditorGUILayout.PropertyField(_locationFactoryProp, new GUIContent("Location Factory")); if (_tileCreatorProp == null && _tileProviderProp == null && _dataFetcherProp == null && - _cacheManagerProp == null) + _cacheManagerProp == null && _locationFactoryProp == null) { EditorGUILayout.HelpBox("Drag & drop your script components here.", MessageType.Info); } diff --git a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs index 54ffedb12..c617415e7 100644 --- a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs @@ -1,4 +1,7 @@ using System; +using System.Collections; +using System.Linq; +using Mapbox.BaseModule; using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Platform.Cache; using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; @@ -7,6 +10,8 @@ using Mapbox.BaseModule.Unity.ModuleBehaviours; using Mapbox.BaseModule.Utilities; using Mapbox.Example.Scripts.TileProviderBehaviours; +using Mapbox.ImageModule.Terrain.TerrainStrategies; +using Mapbox.LocationModule; using Mapbox.UnityMapService; using Mapbox.UnityMapService.TileProviders; using UnityEngine; @@ -22,34 +27,55 @@ public class MapboxMapBehaviour : MapBehaviourCore [SerializeField] protected TileProviderBehaviour TileProvider; [SerializeField] protected DataFetchingManagerBehaviour DataFetcher; [SerializeField] protected MapboxCacheManagerBehaviour CacheManager; + [SerializeField] protected LocationProviderFactory LocationFactory; private MapService _mapService; public bool InitializeOnStart = true; public Action MapServiceReady = (v) => { }; + public virtual void Start() { if (InitializeOnStart) - Initialize(); + StartCoroutine(Initialize()); } - + [ContextMenu("Initialize")] - public override void Initialize() + public override IEnumerator Initialize() { if (InitializationStatus != InitializationStatus.WaitingForInitialization) - return; + yield break; MapInformation.Initialize(); - UnityContext.Initialize(); + + yield return UnityContext.Initialize(); + //we handle permission via unity, instead of using location providers themselves + yield return UnityContext.HandlePermission(); + + if (Application.isEditor || UnityContext.LocationPermissionState == LocationPermissionState.Granted) + { + if (LocationFactory != null) + { + yield return LocationFactory.Initialize(); + var locationProvider = LocationFactory.DefaultLocationProvider; + MapInformation.SetLatitudeLongitude(locationProvider.CurrentLocation.LatitudeLongitude); + } + } + else + { + Debug.Log("Location permission is " + UnityContext.LocationPermissionState); + } var mapboxContext = new MapboxContext(); + yield return mapboxContext.Initialize(); _mapService = GetMapService(mapboxContext, UnityContext); MapServiceReady(_mapService); - + MapboxMap = CreateMapObject(); MapboxMap.Initialized += InitializationCompleted; - StartCoroutine(MapboxMap.Initialize()); + yield return MapboxMap.Initialize(); } + private void InitializationCompleted() { diff --git a/Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs b/Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs deleted file mode 100644 index 20f31de3e..000000000 --- a/Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs +++ /dev/null @@ -1,346 +0,0 @@ -using System; -using System.Collections; -using Mapbox.BaseModule.Data.Vector2d; -using Mapbox.BaseModule.Plugins.Android.UniAndroidPermission; -using Mapbox.BaseModule.Utilities; -using Mapbox.LocationModule.AngleSmoothing; -using Mapbox.LocationModule.UnityLocationWrappers; -using Mapbox.Utils; -using UnityEngine; - -namespace Mapbox.LocationModule -{ - /// - /// The DeviceLocationProvider is responsible for providing real world location and heading data, - /// served directly from native hardware and OS. - /// This relies on Unity's LocationService for location - /// and Compass for heading. - /// - public class DeviceLocationProvider : AbstractLocationProvider - { - - - /// - /// Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. - /// Values like 5-10 could be used for getting best accuracy. - /// - [SerializeField] - [Tooltip("Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. Values like 5-10 could be used for getting best accuracy.")] - public float _desiredAccuracyInMeters = 1.0f; - - - /// - /// The minimum distance (measured in meters) a device must move laterally before Input.location property is updated. - /// Higher values like 500 imply less overhead. - /// - [SerializeField] - [Tooltip("The minimum distance (measured in meters) a device must move laterally before Input.location property is updated. Higher values like 500 imply less overhead.")] - public float _updateDistanceInMeters = 0.0f; - - - [SerializeField] - [Tooltip("The minimum time interval between location updates, in milliseconds. It's reasonable to not go below 500ms.")] - public long _updateTimeInMilliSeconds = 500; - - - [SerializeField] - [Tooltip("Smoothing strategy to be applied to the UserHeading.")] - public AngleSmoothingAbstractBase _userHeadingSmoothing; - - - [SerializeField] - [Tooltip("Smoothing strategy to applied to the DeviceOrientation.")] - public AngleSmoothingAbstractBase _deviceOrientationSmoothing; - - - [Serializable] - public struct DebuggingInEditor - { - [Header("Set 'EditorLocationProvider' to 'DeviceLocationProvider' and connect device with UnityRemote.")] - [SerializeField] - [Tooltip("Mock Unity's 'Input.Location' to route location log files through this class (eg fresh calculation of 'UserHeading') instead of just replaying them. To use set 'Editor Location Provider' in 'Location Factory' to 'Device Location Provider' and select a location log file below.")] - public bool _mockUnityInputLocation; - - [SerializeField] - [Tooltip("Also see above. Location log file to mock Unity's 'Input.Location'.")] - public TextAsset _locationLogFile; - } - - [Space(20)] - public DebuggingInEditor _editorDebuggingOnly; - - - private IMapboxLocationService _locationService; - private Coroutine _pollRoutine; - private double _lastLocationTimestamp; - private WaitForSeconds _wait1sec; - private WaitForSeconds _waitUpdateTime; - /// list of positions to keep for calculations - private CircularBuffer _lastPositions; - /// number of last positons to keep - private int _maxLastPositions = 5; - /// minimum needed distance between oldest and newest position before UserHeading is calculated - private double _minDistanceOldestNewestPosition = 1.5; - - - // Android 6+ permissions have to be granted during runtime - // these are the callbacks for requesting location permission - // TODO: show message to users in case they accidentallly denied permission -#if UNITY_ANDROID - private bool _gotPermissionRequestResponse = false; - - private void OnAllow() { _gotPermissionRequestResponse = true; } - private void OnDeny() { _gotPermissionRequestResponse = true; } - private void OnDenyAndNeverAskAgain() { _gotPermissionRequestResponse = true; } -#endif - - - protected virtual void Awake() - { -#if UNITY_EDITOR - if (_editorDebuggingOnly._mockUnityInputLocation) - { - if (null == _editorDebuggingOnly._locationLogFile || null == _editorDebuggingOnly._locationLogFile.bytes) - { - throw new ArgumentNullException("Location Log File"); - } - - _locationService = new MapboxLocationServiceMock(_editorDebuggingOnly._locationLogFile.bytes); - } - else - { -#endif - _locationService = new MapboxLocationServiceUnityWrapper(); -#if UNITY_EDITOR - } -#endif - - Input.location.Start(); - _currentLocation.Provider = "unity"; - _wait1sec = new WaitForSeconds(1f); - _waitUpdateTime = _updateTimeInMilliSeconds < 500 ? new WaitForSeconds(0.5f) : new WaitForSeconds((float)_updateTimeInMilliSeconds / 1000.0f); - - if (null == _userHeadingSmoothing) { _userHeadingSmoothing = transform.gameObject.AddComponent(); } - if (null == _deviceOrientationSmoothing) { _deviceOrientationSmoothing = transform.gameObject.AddComponent(); } - - _lastPositions = new CircularBuffer(_maxLastPositions); - - if (_pollRoutine == null) - { - _pollRoutine = StartCoroutine(PollLocationRoutine()); - } - } - - - /// - /// Enable location and compass services. - /// Sends continuous location and heading updates based on - /// _desiredAccuracyInMeters and _updateDistanceInMeters. - /// - /// The location routine. - IEnumerator PollLocationRoutine() - { -#if UNITY_EDITOR - while (!UnityEditor.EditorApplication.isRemoteConnected) - { - // exit if we are not the selected location provider - if (null != LocationProviderFactory.Instance && null != LocationProviderFactory.Instance.DefaultLocationProvider) - { - if (!this.Equals(LocationProviderFactory.Instance.DefaultLocationProvider)) - { - yield break; - } - } - - Debug.LogWarning("Remote device not connected via 'Unity Remote'. Waiting ..." + Environment.NewLine + "If Unity seems to be stuck here make sure 'Unity Remote' is running and restart Unity with your device already connected."); - yield return _wait1sec; - } -#endif - - - //request runtime fine location permission on Android if not yet allowed -#if UNITY_ANDROID - if (!_locationService.isEnabledByUser) - { - UniAndroidPermission.RequestPermission(AndroidPermission.ACCESS_FINE_LOCATION); - //wait for user to allow or deny - while (!_gotPermissionRequestResponse) { yield return _wait1sec; } - } -#endif - - - if (!_locationService.isEnabledByUser) - { - Debug.LogError("DeviceLocationProvider: Location is not enabled by user!"); - _currentLocation.IsLocationServiceEnabled = false; - SendLocation(_currentLocation); - yield break; - } - - - _currentLocation.IsLocationServiceInitializing = true; - _locationService.Start(_desiredAccuracyInMeters, _updateDistanceInMeters); - Input.compass.enabled = true; - - int maxWait = 20; - while (_locationService.status == LocationServiceStatus.Initializing && maxWait > 0) - { - yield return _wait1sec; - maxWait--; - } - - if (maxWait < 1) - { - Debug.LogError("DeviceLocationProvider: " + "Timed out trying to initialize location services!"); - _currentLocation.IsLocationServiceInitializing = false; - _currentLocation.IsLocationServiceEnabled = false; - SendLocation(_currentLocation); - yield break; - } - - if (_locationService.status == LocationServiceStatus.Failed) - { - Debug.LogError("DeviceLocationProvider: " + "Failed to initialize location services!"); - _currentLocation.IsLocationServiceInitializing = false; - _currentLocation.IsLocationServiceEnabled = false; - SendLocation(_currentLocation); - yield break; - } - - _currentLocation.IsLocationServiceInitializing = false; - _currentLocation.IsLocationServiceEnabled = true; - -#if UNITY_EDITOR - // HACK: this is to prevent Android devices, connected through Unity Remote, - // from reporting a location of (0, 0), initially. - yield return _wait1sec; -#endif - - System.Globalization.CultureInfo invariantCulture = System.Globalization.CultureInfo.InvariantCulture; - - while (true) - { - - var lastData = _locationService.lastData; - var timestamp = lastData.timestamp; - - /////////////////////////////// - // oh boy, Unity what are you doing??? - // on some devices it seems that - // Input.location.status != LocationServiceStatus.Running - // nevertheless new location is available - ////////////////////////////// - //Debug.LogFormat("Input.location.status: {0}", Input.location.status); - _currentLocation.IsLocationServiceEnabled = - _locationService.status == LocationServiceStatus.Running - || timestamp > _lastLocationTimestamp; - - _currentLocation.IsUserHeadingUpdated = false; - _currentLocation.IsLocationUpdated = false; - - if (!_currentLocation.IsLocationServiceEnabled) - { - yield return _waitUpdateTime; - continue; - } - - // device orientation, user heading get calculated below - _deviceOrientationSmoothing.Add(Input.compass.trueHeading); - _currentLocation.DeviceOrientation = (float)_deviceOrientationSmoothing.Calculate(); - - - //_currentLocation.LatitudeLongitude = new Vector2d(lastData.latitude, lastData.longitude); - // HACK to get back to double precision, does this even work? - // https://forum.unity.com/threads/precision-of-location-longitude-is-worse-when-longitude-is-beyond-100-degrees.133192/#post-1835164 - double latitude = double.Parse(lastData.latitude.ToString("R", invariantCulture), invariantCulture); - double longitude = double.Parse(lastData.longitude.ToString("R", invariantCulture), invariantCulture); - Vector2d previousLocation = new Vector2d(_currentLocation.LatitudeLongitude.Latitude, _currentLocation.LatitudeLongitude.Longitude); - _currentLocation.LatitudeLongitude = new LatitudeLongitude(latitude, longitude); - - _currentLocation.Accuracy = (float)Math.Floor(lastData.horizontalAccuracy); - // sometimes Unity's timestamp doesn't seem to get updated, or even jump back in time - // do an additional check if location has changed - _currentLocation.IsLocationUpdated = timestamp > _lastLocationTimestamp || !_currentLocation.LatitudeLongitude.Equals(previousLocation); - _currentLocation.Timestamp = timestamp; - _lastLocationTimestamp = timestamp; - - if (_currentLocation.IsLocationUpdated) - { - if (_lastPositions.Count > 0) - { - // only add position if user has moved +1m since we added the previous position to the list - CheapRuler cheapRuler = new CheapRuler(_currentLocation.LatitudeLongitude.Latitude, CheapRulerUnits.Meters); - var p = _currentLocation.LatitudeLongitude; - double distance = cheapRuler.Distance( - new double[] { p.Longitude, p.Latitude }, - new double[] { _lastPositions[0].Longitude, _lastPositions[0].Latitude } - ); - if (distance > 1.0) - { - _lastPositions.Add(_currentLocation.LatitudeLongitude); - } - } - else - { - _lastPositions.Add(_currentLocation.LatitudeLongitude); - } - } - - // if we have enough positions calculate user heading ourselves. - // Unity does not provide bearing based on GPS locations, just - // device orientation based on Compass.Heading. - // nevertheless, use compass for intial UserHeading till we have - // enough values to calculate ourselves. - if (_lastPositions.Count < _maxLastPositions) - { - _currentLocation.UserHeading = _currentLocation.DeviceOrientation; - _currentLocation.IsUserHeadingUpdated = true; - } - else - { - var newestPos = _lastPositions[0]; - var oldestPos = _lastPositions[_maxLastPositions - 1]; - CheapRuler cheapRuler = new CheapRuler(newestPos.Latitude, CheapRulerUnits.Meters); - // distance between last and first position in our buffer - double distance = cheapRuler.Distance( - new double[] { newestPos.Longitude, newestPos.Latitude }, - new double[] { oldestPos.Longitude, oldestPos.Latitude } - ); - // positions are minimum required distance apart (user is moving), calculate user heading - if (distance >= _minDistanceOldestNewestPosition) - { - float[] lastHeadings = new float[_maxLastPositions - 1]; - - for (int i = 1; i < _maxLastPositions; i++) - { - // atan2 increases angle CCW, flip sign of latDiff to get CW - double latDiff = -(_lastPositions[i].Latitude - _lastPositions[i - 1].Latitude); - double lngDiff = _lastPositions[i].Longitude - _lastPositions[i - 1].Longitude; - // +90.0 to make top (north) 0� - double heading = (Math.Atan2(latDiff, lngDiff) * 180.0 / Math.PI) + 90.0f; - // stay within [0..360]� range - if (heading < 0) { heading += 360; } - if (heading >= 360) { heading -= 360; } - lastHeadings[i - 1] = (float)heading; - } - - _userHeadingSmoothing.Add(lastHeadings[0]); - float finalHeading = (float)_userHeadingSmoothing.Calculate(); - - //fix heading to have 0� for north, 90� for east, 180� for south and 270� for west - finalHeading = finalHeading >= 180.0f ? finalHeading - 180.0f : finalHeading + 180.0f; - - - _currentLocation.UserHeading = finalHeading; - _currentLocation.IsUserHeadingUpdated = true; - } - } - - _currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow); - SendLocation(_currentLocation); - - yield return _waitUpdateTime; - } - } - } -} diff --git a/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs b/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs deleted file mode 100644 index ce7c69c84..000000000 --- a/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs +++ /dev/null @@ -1,417 +0,0 @@ -using System; -using System.Collections; -using System.Globalization; -using Mapbox.BaseModule.Data.Vector2d; -using Mapbox.BaseModule.Utilities; -using Mapbox.Utils; -using UnityEngine; - -namespace Mapbox.LocationModule -{ - public class DeviceLocationProviderAndroidNative : AbstractLocationProvider, IDisposable - { - - - /// - /// The minimum distance (measured in meters) a device must move laterally before location is updated. - /// https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(java.lang.String,%20long,%20float,%20android.location.LocationListener) - /// - [SerializeField] - [Tooltip("The minimum distance (measured in meters) a device must move laterally before location is updated. Higher values like 500 imply less overhead.")] - float _updateDistanceInMeters = 0.0f; - - - /// - /// The minimum time interval between location updates, in milliseconds. - /// https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(java.lang.String,%20long,%20float,%20android.location.LocationListener) - /// - [SerializeField] - [Tooltip("The minimum time interval between location updates, in milliseconds. It's reasonable to not go below 500ms.")] - long _updateTimeInMilliSeconds = 1000; - - - private WaitForSeconds _wait1sec; - private WaitForSeconds _wait5sec; - private WaitForSeconds _wait60sec; - /// polls location provider only at the requested update intervall to reduce load - private WaitForSeconds _waitUpdateTime; - private bool _disposed; - private static object _lock = new object(); - private Coroutine _pollLocation; - - private AndroidJavaObject _activityContext = null; - private AndroidJavaObject _gpsInstance; - private AndroidJavaObject _sensorInstance; - - - ~DeviceLocationProviderAndroidNative() { Dispose(false); } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - - protected virtual void Dispose(bool disposeManagedResources) - { - if (!_disposed) - { - if (disposeManagedResources) - { - shutdown(); - } - _disposed = true; - } - } - - - private void shutdown() - { - try - { - lock (_lock) - { - if (null != _gpsInstance) - { - _gpsInstance.Call("stopLocationListeners"); - _gpsInstance.Dispose(); - _gpsInstance = null; - } - if (null != _sensorInstance) - { - _sensorInstance.Call("stopSensorListeners"); - _sensorInstance.Dispose(); - _sensorInstance = null; - } - } - } - catch (Exception ex) - { - Debug.LogError(ex); - } - } - - - protected virtual void OnDestroy() { shutdown(); } - - - protected virtual void OnDisable() { shutdown(); } - - protected virtual void Awake() - { - // safe measures to not run when disabled or not selected as location provider - if (!enabled) { return; } - if (!transform.gameObject.activeInHierarchy) { return; } - - - _wait1sec = new WaitForSeconds(1); - _wait5sec = new WaitForSeconds(5); - _wait60sec = new WaitForSeconds(60); - // throttle if entered update intervall is unreasonably low - _waitUpdateTime = _updateTimeInMilliSeconds < 500 ? new WaitForSeconds(0.5f) : new WaitForSeconds((float)_updateTimeInMilliSeconds / 1000.0f); - - _currentLocation.IsLocationServiceEnabled = false; - _currentLocation.IsLocationServiceInitializing = true; - - if (Application.platform == RuntimePlatform.Android) - { - getActivityContext(); - getGpsInstance(true); - getSensorInstance(); - - if (_pollLocation == null) - { - _pollLocation = StartCoroutine(locationRoutine()); - } - } - } - - - private void getActivityContext() - { - using (AndroidJavaClass activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) - { - _activityContext = activityClass.GetStatic("currentActivity"); - } - - if (null == _activityContext) - { - Debug.LogError("Could not get UnityPlayer activity"); - return; - } - } - - - private void getGpsInstance(bool showToastMessages = false) - { - if (null == _activityContext) { return; } - - using (AndroidJavaClass androidGps = new AndroidJavaClass("com.mapbox.android.unity.AndroidGps")) - { - if (null == androidGps) - { - Debug.LogError("Could not get class 'AndroidGps'"); - return; - } - - _gpsInstance = androidGps.CallStatic("instance", _activityContext); - if (null == _gpsInstance) - { - Debug.LogError("Could not get 'AndroidGps' instance"); - return; - } - - _activityContext.Call("runOnUiThread", new AndroidJavaRunnable(() => { _gpsInstance.Call("showMessage", "starting location listeners"); })); - - _gpsInstance.Call("startLocationListeners", _updateDistanceInMeters, _updateTimeInMilliSeconds); - } - } - - - private void getSensorInstance() - { - if (null == _activityContext) { return; } - - using (AndroidJavaClass androidSensors = new AndroidJavaClass("com.mapbox.android.unity.AndroidSensors")) - { - if (null == androidSensors) - { - Debug.LogError("Could not get class 'AndroidSensors'"); - return; - } - - _sensorInstance = androidSensors.CallStatic("instance", _activityContext); - if (null == _sensorInstance) - { - Debug.LogError("Could not get 'AndroidSensors' instance"); - return; - } - - _sensorInstance.Call("startSensorListeners"); - } - } - - private IEnumerator locationRoutine() - { - - while (true) - { - // couldn't get player activity, wait and retry - if (null == _activityContext) - { - SendLocation(_currentLocation); - yield return _wait60sec; - getActivityContext(); - continue; - } - // couldn't get gps plugin instance, wait and retry - if (null == _gpsInstance) - { - SendLocation(_currentLocation); - yield return _wait60sec; - getGpsInstance(); - continue; - } - - // update device orientation - if (null != _sensorInstance) - { - _currentLocation.DeviceOrientation = _sensorInstance.Call("getOrientation"); - } - - bool locationServiceAvailable = _gpsInstance.Call("getIsLocationServiceAvailable"); - _currentLocation.IsLocationServiceEnabled = locationServiceAvailable; - - // app might have been started with location OFF but switched on after start - // check from time to time - if (!locationServiceAvailable) - { - _currentLocation.IsLocationServiceInitializing = true; - _currentLocation.Accuracy = 0; - _currentLocation.HasGpsFix = false; - _currentLocation.SatellitesInView = 0; - _currentLocation.SatellitesUsed = 0; - - SendLocation(_currentLocation); - _gpsInstance.Call("stopLocationListeners"); - yield return _wait5sec; - _gpsInstance.Call("startLocationListeners", _updateDistanceInMeters, _updateTimeInMilliSeconds); - yield return _wait1sec; - continue; - } - - - // if we got till here it means location services are running - _currentLocation.IsLocationServiceInitializing = false; - - try - { - AndroidJavaObject locNetwork = _gpsInstance.Get("lastKnownLocationNetwork"); - AndroidJavaObject locGps = _gpsInstance.Get("lastKnownLocationGps"); - - // easy cases: neither or either gps location or network location available - if (null == locGps & null == locNetwork) { populateCurrentLocation(null); } - if (null != locGps && null == locNetwork) { populateCurrentLocation(locGps); } - if (null == locGps && null != locNetwork) { populateCurrentLocation(locNetwork); } - - // gps- and network location available: figure out which one to use - if (null != locGps && null != locNetwork) { populateWithBetterLocation(locGps, locNetwork); } - - - _currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow); - SendLocation(_currentLocation); - } - catch (Exception ex) - { - Debug.LogErrorFormat("GPS plugin error: " + ex.ToString()); - } - yield return _waitUpdateTime; - } - } - - - private void populateCurrentLocation(AndroidJavaObject location) - { - if (null == location) - { - _currentLocation.IsLocationUpdated = false; - _currentLocation.IsUserHeadingUpdated = false; - return; - } - - double lat = location.Call("getLatitude"); - double lng = location.Call("getLongitude"); - var newLatLng = new LatitudeLongitude(lat, lng); - bool coordinatesUpdated = !newLatLng.Equals(_currentLocation.LatitudeLongitude); - _currentLocation.LatitudeLongitude = newLatLng; - - float newAccuracy = location.Call("getAccuracy"); - bool accuracyUpdated = newAccuracy != _currentLocation.Accuracy; - _currentLocation.Accuracy = newAccuracy; - - // divide by 1000. Android returns milliseconds, we work with seconds - long newTimestamp = location.Call("getTime") / 1000; - bool timestampUpdated = newTimestamp != _currentLocation.Timestamp; - _currentLocation.Timestamp = newTimestamp; - - string newProvider = location.Call("getProvider"); - bool providerUpdated = newProvider != _currentLocation.Provider; - _currentLocation.Provider = newProvider; - - bool hasBearing = location.Call("hasBearing"); - // only evalute bearing when location object actually has a bearing - // Android populates bearing (which is not equal to device orientation) - // only when the device is moving. - // Otherwise it is set to '0.0' - // https://developer.android.com/reference/android/location/Location.html#getBearing() - // We don't want that when we rotate a map according to the direction - // thes user is moving, thus don't update 'heading' with '0.0' - if (!hasBearing) - { - _currentLocation.IsUserHeadingUpdated = false; - } - else - { - float newHeading = location.Call("getBearing"); - _currentLocation.IsUserHeadingUpdated = newHeading != _currentLocation.UserHeading; - _currentLocation.UserHeading = newHeading; - } - - float? newSpeed = location.Call("getSpeed"); - bool speedUpdated = newSpeed != _currentLocation.SpeedMetersPerSecond; - _currentLocation.SpeedMetersPerSecond = newSpeed; - - // flag location as updated if any of below conditions evaluates to true - // Debug.LogFormat("coords:{0} acc:{1} time:{2} speed:{3}", coordinatesUpdated, accuracyUpdated, timestampUpdated, speedUpdated); - _currentLocation.IsLocationUpdated = - providerUpdated - || coordinatesUpdated - || accuracyUpdated - || timestampUpdated - || speedUpdated; - - // Un-comment if required. Throws a warning right now. - //bool networkEnabled = _gpsInstance.Call("getIsNetworkEnabled"); - bool gpsEnabled = _gpsInstance.Call("getIsGpsEnabled"); - if (!gpsEnabled) - { - _currentLocation.HasGpsFix = null; - _currentLocation.SatellitesInView = null; - _currentLocation.SatellitesUsed = null; - } - else - { - _currentLocation.HasGpsFix = _gpsInstance.Get("hasGpsFix"); - _currentLocation.SatellitesInView = _gpsInstance.Get("satellitesInView"); - _currentLocation.SatellitesUsed = _gpsInstance.Get("satellitesUsedInFix"); - } - - } - - - /// - /// If GPS and network location are available use the newer/better one - /// - /// - /// - private void populateWithBetterLocation(AndroidJavaObject locGps, AndroidJavaObject locNetwork) - { - - // check which location is fresher - long timestampGps = locGps.Call("getTime"); - long timestampNet = locNetwork.Call("getTime"); - if (timestampGps > timestampNet) - { - populateCurrentLocation(locGps); - return; - } - - // check which location has better accuracy - float accuracyGps = locGps.Call("getAccuracy"); - float accuracyNet = locNetwork.Call("getAccuracy"); - if (accuracyGps < accuracyNet) - { - populateCurrentLocation(locGps); - return; - } - - // default to network - populateCurrentLocation(locNetwork); - } - -#if UNITY_ANDROID - - private string time2str(AndroidJavaObject loc) - { - long time = loc.Call("getTime"); - DateTime dtPlugin = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Add(TimeSpan.FromMilliseconds(time)); - return dtPlugin.ToString("yyyyMMdd HHmmss"); - } - - - - private string loc2str(AndroidJavaObject loc) - { - - if (null == loc) { return "loc: NULL"; } - - try - { - - double lat = loc.Call("getLatitude"); - double lng = loc.Call("getLongitude"); - - return string.Format(CultureInfo.InvariantCulture, "{0:0.00000000} / {1:0.00000000}", lat, lng); - } - catch (Exception ex) - { - return ex.ToString(); - } - } - -#endif - - - - } -} diff --git a/Runtime/Mapbox/LocationModule/Editor.meta b/Runtime/Mapbox/LocationModule/Editor.meta new file mode 100644 index 000000000..595ed46e1 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ae4a372a92c1d47b69de98b0c2d7523f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs b/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs new file mode 100644 index 000000000..f4b423810 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs @@ -0,0 +1,88 @@ +using Mapbox.LocationModule; +using Mapbox.LocationModule; +using UnityEditor; +using UnityEngine; + +[CustomEditor(typeof(LocationProviderFactory))] +public class LocationProviderFactoryEditor : Editor +{ + private SerializedProperty _locationProviderTypeProp; + private SerializedProperty _mapProp; + private SerializedProperty _editorLatLngProp; + private SerializedProperty _unitySettingsProp; + private SerializedProperty _mapboxSettingsProp; + + private void OnEnable() + { + _locationProviderTypeProp = serializedObject.FindProperty("LocationProviderType"); + _editorLatLngProp = serializedObject.FindProperty("EditorLatitudeLongitude"); + _unitySettingsProp = serializedObject.FindProperty("_unityLocationProviderSettings"); + _mapboxSettingsProp = serializedObject.FindProperty("_mapboxLocationProviderSettings"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField(_locationProviderTypeProp); + + EditorGUILayout.Space(); + + var providerType = + (LocationProviderType)_locationProviderTypeProp.enumValueIndex; + + switch (providerType) + { + case LocationProviderType.StaticLocationProvider: + DrawEditorProvider(); + break; + + case LocationProviderType.UnityLocationProvider: + DrawUnityProvider(); + break; + + case LocationProviderType.MapboxLocationProvider: + DrawMapboxProvider(); + break; + + // case LocationProviderType.CustomLocationProvider: + // EditorGUILayout.HelpBox( + // "Custom location provider not implemented yet.", + // MessageType.Info + // ); + // break; + } + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawEditorProvider() + { + EditorGUILayout.HelpBox( + "Uses a fixed editor-only location.", + MessageType.None + ); + + EditorGUILayout.PropertyField(_editorLatLngProp); + } + + private void DrawUnityProvider() + { + EditorGUILayout.HelpBox( + "Uses Unity Input.location service.", + MessageType.None + ); + + EditorGUILayout.PropertyField(_unitySettingsProp); + } + + private void DrawMapboxProvider() + { + EditorGUILayout.HelpBox( + "Mapbox provider is experimental.", + MessageType.Warning + ); + + EditorGUILayout.PropertyField(_mapboxSettingsProp); + } +} diff --git a/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs.meta similarity index 83% rename from Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs.meta index 892db1002..d6c7e278d 100644 --- a/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs.meta +++ b/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 46cd4a863e31244f891143a5bab077bc +guid: fbc3b1c4861664c9a91acb80bba5f3a4 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef b/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef new file mode 100644 index 000000000..51fe64e76 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "MapboxLocationModule.Editor", + "rootNamespace": "", + "references": [ + "GUID:1b64b068a01f943108c7f4b7a5356c7a" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef.meta b/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef.meta new file mode 100644 index 000000000..e8bb3e6c8 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6ae5860bc6f8448fc8476cc59bc698f3 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces.meta diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/Helsinki.txt b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/Helsinki.txt similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/Helsinki.txt rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/Helsinki.txt diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/Helsinki.txt.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/Helsinki.txt.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/Helsinki.txt.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/Helsinki.txt.meta diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative-2.txt b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative-2.txt similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative-2.txt rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative-2.txt diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative.txt b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative.txt similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative.txt rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative.txt diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderUnity.txt b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderUnity.txt similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderUnity.txt rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderUnity.txt diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderUnity.txt.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderUnity.txt.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderUnity.txt.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderUnity.txt.meta diff --git a/Runtime/Mapbox/LocationModule/Helpers.meta b/Runtime/Mapbox/LocationModule/Helpers.meta new file mode 100644 index 000000000..7da1d74a2 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Helpers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c7c705c95e41449968c82bd5b9ba4484 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs b/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs new file mode 100644 index 000000000..40f3f734b --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs @@ -0,0 +1,51 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.LocationModule; +using UnityEngine; + +namespace Mapbox.LocationModule.Helpers +{ + public class ShiftMapForDistance : MonoBehaviour + { + public MapBehaviourCore Map; + public LocationProviderFactory LocationProviderFactory; + [Tooltip("Shift whole map if the distance between last two location points is bigger than this value (in mercator units). ")] + public float ShiftMapDistance; + + private MapboxMap _map; + private ILocationProvider _locationProvider; + private LatitudeLongitude _previousLocation = LatitudeLongitude.Invalid; + private LatitudeLongitude _currentLocation; + + private void Start() + { + Map.Initialized += map => + { + _map = map; + _locationProvider = LocationProviderFactory.DefaultLocationProvider; + if (_locationProvider != null) + { + _locationProvider.OnLocationUpdated += LocationUpdated; + } + }; + } + + private void LocationUpdated(Location location) + { + _currentLocation = location.LatitudeLongitude; + if (_previousLocation != LatitudeLongitude.Invalid) + { + var d1 = Conversions.LatitudeLongitudeToWebMercator(_previousLocation); + var d2 = Conversions.LatitudeLongitudeToWebMercator(_currentLocation); + if (Vector2d.Distance(d1, d2) > ShiftMapDistance) + { + Debug.Log(string.Format("ShiftMapForDistance: d1: {0} d2: {1}", d1, d2)); + _map.ChangeView(_currentLocation); + } + } + + _previousLocation = _currentLocation; + } + } +} diff --git a/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs.meta b/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs.meta similarity index 71% rename from Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs.meta rename to Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs.meta index 78635c575..d415bb5b9 100644 --- a/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs.meta +++ b/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs.meta @@ -1,7 +1,5 @@ fileFormatVersion: 2 -guid: d3d557417079b1446999d2d86ff71dfb -timeCreated: 1524214127 -licenseType: Pro +guid: a5534c9989da84262bc64ffe27afbbb6 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs b/Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs similarity index 67% rename from Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs rename to Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs index eb70b7641..2052ac8b9 100644 --- a/Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs @@ -1,10 +1,10 @@ using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.Map; using Mapbox.LocationModule; using UnityEngine; +//using Mapbox.LocationModule; -namespace Mapbox.Example.Scripts.LocationBehaviours +namespace Mapbox.LocationModule.Helpers { public class SnapTransformToLocationProvider : MonoBehaviour { @@ -19,20 +19,20 @@ private void Start() { if(_transform == null) _transform = transform; - - if(_locationProvider == null) + + if (_locationProvider == null) + { Debug.Log("_locationProvider null"); - - if(_locationProvider.DefaultLocationProvider == null) - Debug.Log("DefaultLocationProvider null"); - - if(!enabled || _locationProvider == null || _locationProvider.DefaultLocationProvider == null) return; - + } + _mapCore.Initialized += (map) => { _map = map; - _locationProvider.DefaultLocationProvider.OnLocationUpdated += OnDefaultLocationProviderOnOnLocationUpdated; + if (_locationProvider.DefaultLocationProvider != null) + { + _locationProvider.DefaultLocationProvider.OnLocationUpdated += OnDefaultLocationProviderOnOnLocationUpdated; + } }; } diff --git a/Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs.meta similarity index 100% rename from Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/LocationProviderFactory.cs b/Runtime/Mapbox/LocationModule/LocationProviderFactory.cs deleted file mode 100644 index f98c7aa49..000000000 --- a/Runtime/Mapbox/LocationModule/LocationProviderFactory.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.Text.RegularExpressions; -using UnityEngine; - -namespace Mapbox.LocationModule -{ - /// - /// Singleton factory to allow easy access to various LocationProviders. - /// This is meant to be attached to a game object. - /// - public class LocationProviderFactory : MonoBehaviour - { - [SerializeField] - [Tooltip("Provider using Unity's builtin 'Input.Location' service")] - AbstractLocationProvider _deviceLocationProviderUnity; - - [SerializeField] - [Tooltip("Custom native Android location provider. If this is not set above provider is used")] - DeviceLocationProviderAndroidNative _deviceLocationProviderAndroid; - - [SerializeField] - AbstractLocationProvider _editorLocationProvider; - - [SerializeField] - AbstractLocationProvider _transformLocationProvider; - - [SerializeField] - bool _dontDestroyOnLoad; - - - /// - /// The singleton instance of this factory. - /// - private static LocationProviderFactory _instance; - public static LocationProviderFactory Instance - { - get - { - return _instance; - } - - private set - { - _instance = value; - } - } - - ILocationProvider _defaultLocationProvider; - - /// - /// The default location provider. - /// Outside of the editor, this will be a . - /// In the Unity editor, this will be an - /// - /// - /// Fetch location to set a transform's position: - /// - /// void Update() - /// { - /// var locationProvider = LocationProviderFactory.Instance.DefaultLocationProvider; - /// transform.position = Conversions.GeoToWorldPosition(locationProvider.Location, - /// MapController.ReferenceTileRect.Center, - /// MapController.WorldScaleFactor).ToVector3xz(); - /// } - /// - /// - public ILocationProvider DefaultLocationProvider - { - get - { - return _defaultLocationProvider; - } - set - { - _defaultLocationProvider = value; - } - } - - /// - /// Returns the serialized . - /// - public ILocationProvider TransformLocationProvider - { - get - { - return _transformLocationProvider; - } - } - - /// - /// Returns the serialized . - /// - public ILocationProvider EditorLocationProvider - { - get - { - return _editorLocationProvider; - } - } - - /// - /// Returns the serialized - /// - public ILocationProvider DeviceLocationProvider - { - get - { - return _deviceLocationProviderUnity; - } - } - - /// - /// Create singleton instance and inject the DefaultLocationProvider upon initialization of this component. - /// - protected virtual void Awake() - { - if (Instance != null) - { - DestroyImmediate(gameObject); - return; - } - Instance = this; - - if (_dontDestroyOnLoad) - { - DontDestroyOnLoad(gameObject); - } - - #if UNITY_EDITOR - Debug.LogFormat("LocationProviderFactory: Injected EDITOR Location Provider - {0}", _editorLocationProvider.GetType()); - DefaultLocationProvider = _editorLocationProvider; - #else - Debug.LogFormat("LocationProviderFactory: Injected DEVICE Location Provider - {0}", _deviceLocationProviderUnity.GetType()); - DefaultLocationProvider = _deviceLocationProviderUnity; - #endif - // InjectEditorLocationProvider(); - // InjectDeviceLocationProvider(); - } - - /// - /// Injects the editor location provider. - /// Depending on the platform, this method and calls to it will be stripped during compile. - /// - [System.Diagnostics.Conditional("UNITY_EDITOR")] - void InjectEditorLocationProvider() - { - Debug.LogFormat("LocationProviderFactory: Injected EDITOR Location Provider - {0}", _editorLocationProvider.GetType()); - DefaultLocationProvider = _editorLocationProvider; - } - - /// - /// Injects the device location provider. - /// Depending on the platform, this method and calls to it will be stripped during compile. - /// - [System.Diagnostics.Conditional("NOT_UNITY_EDITOR")] - void InjectDeviceLocationProvider() - { - int AndroidApiVersion = 0; - var regex = new Regex(@"(?<=API-)-?\d+"); - Match match = regex.Match(SystemInfo.operatingSystem); // eg 'Android OS 8.1.0 / API-27 (OPM2.171019.029/4657601)' - if (match.Success) { int.TryParse(match.Groups[0].Value, out AndroidApiVersion); } - Debug.LogFormat("{0} => API version: {1}", SystemInfo.operatingSystem, AndroidApiVersion); - - // // only inject native provider if platform requirement is met - // // and script itself as well as parent game object are active - // if (Application.platform == RuntimePlatform.Android - // && null != _deviceLocationProviderAndroid - // && _deviceLocationProviderAndroid.enabled - // && _deviceLocationProviderAndroid.transform.gameObject.activeInHierarchy - // // API version 24 => Android 7 (Nougat): we are using GnssStatus 'https://developer.android.com/reference/android/location/GnssStatus.html' - // // in the native plugin. - // // GnssStatus is not available with versions lower than 24 - // && AndroidApiVersion >= 24 - // ) - // { - // Debug.LogFormat("LocationProviderFactory: Injected native Android DEVICE Location Provider - {0}", _deviceLocationProviderAndroid.GetType()); - // DefaultLocationProvider = _deviceLocationProviderAndroid; - // } - // else - { - Debug.LogFormat("LocationProviderFactory: Injected DEVICE Location Provider - {0}", _deviceLocationProviderUnity.GetType()); - DefaultLocationProvider = _deviceLocationProviderUnity; - } - } - } -} diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation.meta b/Runtime/Mapbox/LocationModule/MapboxLocation.meta new file mode 100644 index 000000000..1f9ba6b88 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3e6aa80deedf24c4a84fc47aa793609a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs new file mode 100644 index 000000000..01b35dffd --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs @@ -0,0 +1,341 @@ +#if !UNITY_EDITOR && UNITY_ANDROID + +using System; +using System.Collections.Generic; +using Mapbox.LocationModule; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule.MapboxLocation +{ + [Preserve] + public class MapboxLocationAndroid : IMapboxDeviceLocation + { + [Preserve] public event Action LocationUpdated; + [Preserve] public event Action AuthorizationChanged; + [Preserve] public event Action AccuracyAuthorizationChanged; + [Preserve] public event Action AvailabilityChanged; + + public MapboxLocationSettings _mapboxLocationSettings; + + private readonly Queue _mainThreadQueue = new Queue(); + private readonly object _queueLock = new object(); + + private AndroidJavaObject _permissionsManager; + private AndroidJavaObject _unityActivity; + + private string _mapboxLocationServiceFactoryClassName = "com.mapbox.common.location.LocationServiceFactory"; + private string _mapboxLocationServiceFactoryGetMethodName = "getOrCreate"; + private string _mapboxLocationServiceGetProviderMethodName = "getDeviceLocationProvider"; + private string _mapboxLocationProviderRequestClassName = "com.mapbox.common.location.LocationProviderRequest"; + private string _mapboxLocationInvervalClassName = "com.mapbox.common.location.IntervalSettings"; + + + private AndroidJavaClass _locationServiceFactory; + private AndroidJavaObject _locationService; + private AndroidJavaObject _locationProvider; + private MapboxLocationObserverProxy _locationObserver; + private MapboxLocationServiceObserverProxy _serviceObserver; + private AndroidJavaObject _accuracyLevelHigh; + + private float _lastLocationPollTime; + private float _locationPollInterval = 1.0f; // Poll every 1 second + + private string _intervalSettingsBuilderClassName = "com.mapbox.common.location.IntervalSettings$Builder"; + private string _minimumIntervalFieldName = "minimumInterval"; + private string _maximumIntervalFieldName = "maximumInterval"; + private string _intervalFieldName = "interval"; + + private string _locationProviderRequestBuilderClassName = "com.mapbox.common.location.LocationProviderRequest$Builder"; + + private string _accuracyLevelClassName = "com.mapbox.common.location.AccuracyLevel"; + private string _accuracyFieldName = "accuracy"; + private string _displacementFieldName = "displacement"; + private string _intervalSettingFieldName = "interval"; + + private string _addServiceObserverMethodName = "registerObserver"; + private string _addObserverMethodName = "addLocationObserver"; + + private string javaLangLong = "java.lang.Long"; + private string javaLangFloat = "java.lang.Float"; + + // Android class names + private const string _unityPlayerClassName = "com.unity3d.player.UnityPlayer"; + private const string _permissionsManagerClassName = "com.mapbox.android.core.permissions.PermissionsManager"; + private const string _packageManagerClassName = "android.content.pm.PackageManager"; + private const string _deviceLocationProviderTypeClassName = "com.mapbox.common.location.DeviceLocationProviderType"; + + // Android method names + private const string _currentActivityFieldName = "currentActivity"; + private const string _areLocationPermissionsGrantedMethodName = "areLocationPermissionsGranted"; + private const string _requestLocationPermissionsMethodName = "requestLocationPermissions"; + private const string _buildMethodName = "build"; + private const string _isValueMethodName = "isValue"; + private const string _getErrorMethodName = "getError"; + private const string _toStringMethodName = "toString"; + private const string _getValueMethodName = "getValue"; + private const string _valuesMethodName = "values"; + private const string _getLastLocationMethodName = "getLastLocation"; + private const string _checkSelfPermissionMethodName = "checkSelfPermission"; + private const string _getClassMethodName = "getClass"; + private const string _getNameMethodName = "getName"; + + + // Android constants + private const string _permissionGrantedFieldName = "PERMISSION_GRANTED"; + private const string _fineLocationPermission = "android.permission.ACCESS_FINE_LOCATION"; + private const string _coarseLocationPermission = "android.permission.ACCESS_COARSE_LOCATION"; + private const string _androidProviderTypeName = "ANDROID"; + + // Log prefix + private const string _logPrefix = "MAPBOX_UNITY_SDK: "; + + public MapboxLocationAndroid(MapboxLocationSettings settings) + { + _mapboxLocationSettings = settings; + } + + public void Initialize() + { + //Get Unity activity + using (var unityPlayer = new AndroidJavaClass(_unityPlayerClassName)) + { + _unityActivity = unityPlayer.GetStatic(_currentActivityFieldName); + } + + InitializeMapboxLocation(); + } + + public void Update() + { + // Poll location at intervals + if (_locationProvider != null && Time.realtimeSinceStartup - _lastLocationPollTime >= _locationPollInterval) + { + _lastLocationPollTime = Time.realtimeSinceStartup; + //GetLastLocation(); + } + + // Process main thread queue + lock (_queueLock) + { + while (_mainThreadQueue.Count > 0) + { + var action = _mainThreadQueue.Dequeue(); + try + { + action?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Error processing main thread action: {ex.Message}"); + } + } + } + } + + public void OnDestroy() + { + try + { + // Clean up location provider + if (_locationProvider != null) + { + _locationProvider.Dispose(); + _locationProvider = null; + } + + // Clean up location service + if (_locationService != null) + { + _locationService.Dispose(); + _locationService = null; + } + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Error during cleanup: {ex.Message}"); + } + } + + + + private void RequestLocationPermissions() + { + var permissionsListenerProxy = new MapboxLocationPermissionsListenerProxy( + onExplanation: (permissions) => + { + }, + onResult: (granted) => + { + if (granted) + { + InitializeMapboxLocation(); + } + else + { + Debug.LogError(_logPrefix + "Location permissions denied"); + } + }); + + _permissionsManager = new AndroidJavaObject(_permissionsManagerClassName, permissionsListenerProxy); + _permissionsManager.Call(_requestLocationPermissionsMethodName, _unityActivity); + } + + private void InitializeMapboxLocation() + { + _locationServiceFactory = new AndroidJavaClass(_mapboxLocationServiceFactoryClassName); + _locationService = _locationServiceFactory.CallStatic(_mapboxLocationServiceFactoryGetMethodName); + + var intervalSettings = new AndroidJavaObject(_intervalSettingsBuilderClassName) + .Call(_minimumIntervalFieldName, + new AndroidJavaObject(javaLangLong, _mapboxLocationSettings.MinimumInterval)) + .Call(_maximumIntervalFieldName, + new AndroidJavaObject(javaLangLong, _mapboxLocationSettings.MaximumInterval)) + .Call(_intervalFieldName, + new AndroidJavaObject(javaLangLong, _mapboxLocationSettings.Interval)) + .Call(_buildMethodName); + + GetAccuracyLevel(); + + var requestSettings = new AndroidJavaObject(_locationProviderRequestBuilderClassName) + .Call(_accuracyFieldName, _accuracyLevelHigh) + .Call(_displacementFieldName, + new AndroidJavaObject(javaLangFloat, _mapboxLocationSettings.Displacement)) + .Call(_intervalSettingFieldName, intervalSettings) + .Call(_buildMethodName); + + //AndroidJavaObject nullRequest = null; + var androidType = GetProviderType(); + var expected = _locationService.Call(_mapboxLocationServiceGetProviderMethodName, androidType, requestSettings, true); + var hasValue = expected.Call(_isValueMethodName); + + if (!hasValue) + { + AndroidJavaObject error = expected.Call(_getErrorMethodName); + Debug.LogError(_logPrefix + "Mapbox error: " + error.Call(_toStringMethodName)); + return; + } + + _serviceObserver = new MapboxLocationServiceObserverProxy(AuthorizationChanged, AccuracyAuthorizationChanged, AvailabilityChanged, EnqueueOnMainThread); + + try + { + _locationService.Call(_addServiceObserverMethodName, _serviceObserver); + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Failed to register service observer: {ex.Message}"); + } + + _locationProvider = expected.Call(_getValueMethodName); + _locationObserver = new MapboxLocationObserverProxy(EnqueueOnMainThread, LocationUpdated); + + try + { + // Keep a strong reference to prevent GC + System.GC.KeepAlive(_locationObserver); + + _locationProvider.Call(_addObserverMethodName, _locationObserver); + + // Verify Android location permissions + try + { + var permissionClass = new AndroidJavaClass(_packageManagerClassName); + int permissionGranted = permissionClass.GetStatic(_permissionGrantedFieldName); + + int fineStatus = _unityActivity.Call(_checkSelfPermissionMethodName, _fineLocationPermission); + int coarseStatus = _unityActivity.Call(_checkSelfPermissionMethodName, _coarseLocationPermission); + + if (fineStatus != permissionGranted && coarseStatus != permissionGranted) + { + Debug.LogError(_logPrefix + "No location permissions! This is why observer won't receive updates."); + } + } + catch (Exception permEx) + { + Debug.LogWarning(_logPrefix + $"Could not verify permissions: {permEx.Message}"); + } + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Failed to add location observer: {ex.Message}\n{ex.StackTrace}"); + } + + GetLastLocation(); + } + + private void GetLastLocation() + { + try + { + var callback = new GetLocationCallbackProxy((location, isValid) => + { + if (isValid) + { + EnqueueOnMainThread(() => + { + LocationUpdated?.Invoke(location); + }); + } + }); + + _locationProvider.Call(_getLastLocationMethodName, callback); + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"getLastLocation failed: {ex.Message}"); + } + } + + private void EnqueueOnMainThread(Action action) + { + lock (_queueLock) + { + _mainThreadQueue.Enqueue(action); + } + } + + private void GetAccuracyLevel() + { + var accuracyLevel = new AndroidJavaClass(_accuracyLevelClassName); + var accuArray = accuracyLevel.CallStatic(_valuesMethodName); + var convertedArray = AndroidJNIHelper.ConvertFromJNIArray(accuArray.GetRawObject()); + foreach (var javaEnumObject in convertedArray) + { + var enumString = javaEnumObject.Call(_toStringMethodName); + if (enumString == _mapboxLocationSettings.AccuracyLevel.ToString()) + { + _accuracyLevelHigh = javaEnumObject; + break; + } + } + } + + private AndroidJavaObject GetProviderType() + { + var accuracyLevel = new AndroidJavaClass(_deviceLocationProviderTypeClassName); + var accuArray = accuracyLevel.CallStatic(_valuesMethodName); + var convertedArray = AndroidJNIHelper.ConvertFromJNIArray(accuArray.GetRawObject()); + foreach (var javaEnumObject in convertedArray) + { + var enumString = javaEnumObject.Call(_toStringMethodName); + if (enumString == _androidProviderTypeName) + { + return javaEnumObject; + } + } + + return null; + } + + private string GetJavaClassName(AndroidJavaObject obj) + { + if (obj == null) + return ""; + + using var clazz = obj.Call(_getClassMethodName); + return clazz.Call(_getNameMethodName); + } + } +} +#endif \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs.meta new file mode 100644 index 000000000..1d43e966e --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ccd02e95980847b3940f678ce4f137ff +timeCreated: 1769177065 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs new file mode 100644 index 000000000..2c0f9ee7a --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using UnityEngine; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; + +#if UNITY_IOS +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationIos : IMapboxDeviceLocation + { + public event Action LocationUpdated = delegate { }; + public event Action AuthorizationChanged = delegate { }; + public event Action AccuracyAuthorizationChanged = delegate { }; + public event Action AvailabilityChanged = delegate { }; + + public MapboxLocationSettings _mapboxLocationSettings; + + private Location _currentLocation; + // Static reference for IL2CPP callback compatibility + private static MapboxLocationIos _instance; + private GCHandle _gcHandle; + + // Queue for marshaling callbacks to main thread + private Queue _mainThreadQueue = new Queue(); + private object _queueLock = new object(); + + // Callback delegate for location updates + private delegate void LocationUpdateCallback( + double latitude, double longitude, + float accuracy, double timestamp, + double altitude, float speed, float bearing); + + // Callback delegates for service observer + private delegate void AuthorizationStatusCallback(int status); + private delegate void AccuracyAuthorizationCallback(int accuracy); + private delegate void AvailabilityCallback(bool available); + + // DllImport declarations + [DllImport("__Internal")] + private static extern void requestLocationAuthorization(); + + [DllImport("__Internal")] + private static extern IntPtr startLocationUpdatesWithSettings( + long minimumInterval, long maximumInterval, long interval, + int accuracyLevel, float displacement, + LocationUpdateCallback callback); + + [DllImport("__Internal")] + private static extern void stopLocationUpdates(IntPtr provider); + + [DllImport("__Internal")] + private static extern void addLocationServiceObserver( + AuthorizationStatusCallback authCallback, + AccuracyAuthorizationCallback accuracyCallback, + AvailabilityCallback availabilityCallback); + + [DllImport("__Internal")] + private static extern void removeLocationServiceObserver(); + + // Implementation details + private IntPtr _locationProvider; + private static LocationUpdateCallback _callback; + private static AuthorizationStatusCallback _authCallback; + private static AccuracyAuthorizationCallback _accuracyCallback; + private static AvailabilityCallback _availabilityCallback; + private bool _isStarted = false; + private bool _observerAdded = false; + + public MapboxLocationIos(MapboxLocationSettings settings) + { + _mapboxLocationSettings = settings; + // Request location authorization first + requestLocationAuthorization(); + + // Replace the static instance first so native callbacks always have a + // valid target, then tear down the old instance's native resources. + var previous = _instance; + _instance = this; + _gcHandle = GCHandle.Alloc(this); + + if (previous != null) + { + Debug.LogWarning("MapboxLocationIos: Replacing existing instance. Cleaning up previous native resources."); + previous.StopNativeUpdates(); + } + + // Create static callback delegates + _callback = OnLocationUpdateStatic; + _authCallback = OnAuthorizationChangedStatic; + _accuracyCallback = OnAccuracyAuthorizationChangedStatic; + _availabilityCallback = OnAvailabilityChangedStatic; + + // Add service observer for permission changes + addLocationServiceObserver(_authCallback, _accuracyCallback, _availabilityCallback); + _observerAdded = true; + + // Map accuracy level enum to integer + int accuracyLevel = (int)_mapboxLocationSettings.AccuracyLevel; + + // Start location updates with settings + _locationProvider = startLocationUpdatesWithSettings( + _mapboxLocationSettings.MinimumInterval, + _mapboxLocationSettings.MaximumInterval, + _mapboxLocationSettings.Interval, + accuracyLevel, + _mapboxLocationSettings.Displacement, + _callback); + + if (_locationProvider != IntPtr.Zero) + { + _isStarted = true; + + // Initialize location struct + _currentLocation.IsLocationServiceInitializing = false; + _currentLocation.IsLocationServiceEnabled = true; + _currentLocation.Provider = "MapboxCommon"; + _currentLocation.ProviderClass = "LocationIos"; + } + else + { + Debug.LogError("Failed to start iOS location service"); + _currentLocation.IsLocationServiceInitializing = false; + _currentLocation.IsLocationServiceEnabled = false; + } + } + + public void Initialize() + { + + } + + // Static callback for IL2CPP compatibility + [AOT.MonoPInvokeCallback(typeof(LocationUpdateCallback))] + private static void OnLocationUpdateStatic( + double latitude, double longitude, + float accuracy, double timestamp, + double altitude, float speed, float bearing) + { + if (_instance != null) + { + _instance.OnLocationUpdate(latitude, longitude, accuracy, timestamp, altitude, speed, bearing); + } + } + + // Static callback for authorization status changes + [AOT.MonoPInvokeCallback(typeof(AuthorizationStatusCallback))] + private static void OnAuthorizationChangedStatic(int status) + { + if (_instance != null) + { + _instance.OnAuthorizationChanged(status); + } + } + + // Static callback for accuracy authorization changes + [AOT.MonoPInvokeCallback(typeof(AccuracyAuthorizationCallback))] + private static void OnAccuracyAuthorizationChangedStatic(int accuracy) + { + if (_instance != null) + { + _instance.OnAccuracyAuthorizationChanged(accuracy); + } + } + + // Static callback for availability changes + [AOT.MonoPInvokeCallback(typeof(AvailabilityCallback))] + private static void OnAvailabilityChangedStatic(bool available) + { + if (_instance != null) + { + _instance.OnAvailabilityChanged(available); + } + } + + // Instance method to handle the actual update + private void OnLocationUpdate(double latitude, double longitude, float accuracy, double timestamp, double altitude, float speed, float bearing) + { + // Queue the update to be processed on the main thread + lock (_queueLock) + { + _mainThreadQueue.Enqueue(() => + { + try + { + // Update location struct + _currentLocation.LatitudeLongitude = new LatitudeLongitude(latitude, longitude); + _currentLocation.Accuracy = accuracy; + _currentLocation.Timestamp = timestamp; + _currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow); + + // Set optional properties + _currentLocation.SpeedMetersPerSecond = speed > 0 ? speed : (float?)null; + _currentLocation.UserHeading = bearing; + _currentLocation.IsUserHeadingUpdated = bearing > 0; + + _currentLocation.IsLocationUpdated = true; + _currentLocation.IsLocationServiceEnabled = true; + _currentLocation.IsLocationServiceInitializing = false; + + // Send location update event + LocationUpdated(_currentLocation); + } + catch (Exception ex) + { + Debug.LogError($"Exception in iOS location update: {ex.Message}"); + } + }); + } + } + + // Instance method to handle authorization status changes + private void OnAuthorizationChanged(int status) + { + lock (_queueLock) + { + _mainThreadQueue.Enqueue(() => + { + try + { + AuthorizationChanged((MapboxLocationServiceStatus)status); + } + catch (Exception ex) + { + Debug.LogError($"Exception in authorization changed: {ex.Message}"); + } + }); + } + } + + // Instance method to handle accuracy authorization changes + private void OnAccuracyAuthorizationChanged(int accuracy) + { + lock (_queueLock) + { + _mainThreadQueue.Enqueue(() => + { + try + { + AccuracyAuthorizationChanged((AccuracyAuthorization)accuracy); + } + catch (Exception ex) + { + Debug.LogError($"Exception in accuracy authorization changed: {ex.Message}"); + } + }); + } + } + + // Instance method to handle availability changes + private void OnAvailabilityChanged(bool available) + { + lock (_queueLock) + { + _mainThreadQueue.Enqueue(() => + { + try + { + AvailabilityChanged(available); + } + catch (Exception ex) + { + Debug.LogError($"Exception in availability changed: {ex.Message}"); + } + }); + } + } + + public void Update() + { + // Process queued location updates on the main thread + lock (_queueLock) + { + while (_mainThreadQueue.Count > 0) + { + var action = _mainThreadQueue.Dequeue(); + action?.Invoke(); + } + } + } + + /// + /// Stop native location updates and free the GCHandle. + /// Does not touch the static _instance — safe to call on a replaced instance. + /// + private void StopNativeUpdates() + { + if (_isStarted && _locationProvider != IntPtr.Zero) + { + stopLocationUpdates(_locationProvider); + _isStarted = false; + } + + if (_observerAdded) + { + removeLocationServiceObserver(); + _observerAdded = false; + } + + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + } + + public void OnDestroy() + { + StopNativeUpdates(); + + // Clear the static reference only if it still points to this instance. + // If another instance replaced us, leave it alone — it owns the callbacks now. + if (_instance == this) + { + _instance = null; + _callback = null; + _authCallback = null; + _accuracyCallback = null; + _availabilityCallback = null; + } + } + } +} +#endif \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs.meta new file mode 100644 index 000000000..bdab74221 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f1a846f70d4024ec4b41689510701521 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs new file mode 100644 index 000000000..a44f338dd --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs @@ -0,0 +1,106 @@ +using System; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.LocationModule; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationObserverProxy : AndroidJavaProxy + { + private const string _logPrefix = "MAPBOX_UNITY_SDK: "; + private const string _getLatitude = "getLatitude"; + private const string _getLongitude = "getLongitude"; + private const string _getAltitude = "getAltitude"; + private const string _getSpeed = "getSpeed"; + private const string _getBearing = "getBearing"; + private const string _get = "get"; + private const string _size = "size"; + + public Action SendLocation; + private readonly Action _enqueueOnMainThread; + + public MapboxLocationObserverProxy(Action enqueueOnMainThread, Action locationUpdated) : base("com.mapbox.common.location.LocationObserver") + { + _enqueueOnMainThread = enqueueOnMainThread; + SendLocation = locationUpdated; + } + + [Preserve] + public void onLocationUpdateReceived(AndroidJavaObject locations) + { + if (locations == null) + { + Debug.LogWarning(_logPrefix + "locations list is null"); + return; + } + + try + { + var size = locations.Call(_size); + for (var i = 0; i < size; i++) + { + var loc = locations.Call(_get, i); + + if (loc == null) + { + Debug.LogWarning(_logPrefix + $"Location at index {i} is null"); + continue; + } + + var lat = loc.Call(_getLatitude); + var lon = loc.Call(_getLongitude); + + // Get optional properties - Mapbox returns @Nullable Double for these + // double? altitude = null; + // float? speed = null; + // float? bearing = null; + // + // // Get altitude - returns null if not available + // var altitudeObj = loc.Call(_getAltitude); + // if (altitudeObj != null) + // { + // altitude = altitudeObj.Call("doubleValue"); + // } + // + // // Get speed - returns null if not available + // var speedObj = loc.Call(_getSpeed); + // if (speedObj != null) + // { + // speed = (float)speedObj.Call("doubleValue"); + // } + // + // // Get bearing - returns null if not available + // var bearingObj = loc.Call(_getBearing); + // if (bearingObj != null) + // { + // bearing = (float)bearingObj.Call("doubleValue"); + // } + + var location = new Location + { + LatitudeLongitude = new LatitudeLongitude(lat, lon), + TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow), + IsLocationUpdated = true, + IsLocationServiceEnabled = true, + //Altitude = altitude, + //SpeedMetersPerSecond = speed, + //Bearing = bearing + }; + + // Invoke on main thread + _enqueueOnMainThread?.Invoke(() => + { + SendLocation?.Invoke(location); + }); + } + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Error in onLocationUpdateReceived: {ex.Message}\n{ex.StackTrace}"); + } + } + + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs.meta new file mode 100644 index 000000000..cfd75ec5c --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ddbbcd5894704384b563164b1a7d16c0 +timeCreated: 1770213228 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs new file mode 100644 index 000000000..379aeb157 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs @@ -0,0 +1,39 @@ +using System; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationPermissionsListenerProxy : AndroidJavaProxy + { + private readonly Action _onExplanation; + private readonly Action _onResult; + + public MapboxLocationPermissionsListenerProxy(Action onExplanation, Action onResult) : base("com.mapbox.android.core.permissions.PermissionsListener") + { + _onExplanation = onExplanation; + _onResult = onResult; + } + + [Preserve] + public void onExplanationNeeded(AndroidJavaObject permissionsToExplain) + { + if (permissionsToExplain != null) + { + int size = permissionsToExplain.Call("size"); + string[] permissions = new string[size]; + for (int i = 0; i < size; i++) + { + permissions[i] = permissionsToExplain.Call("get", i); + } + _onExplanation?.Invoke(permissions); + } + } + + [Preserve] + public void onPermissionResult(bool granted) + { + _onResult?.Invoke(granted); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs.meta new file mode 100644 index 000000000..263436246 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 73879c85c25049ba8e25c629b265fc1b +timeCreated: 1770213220 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs new file mode 100644 index 000000000..8170715a3 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs @@ -0,0 +1,49 @@ +using System; +using Mapbox.LocationModule; + +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationProvider : AbstractLocationProvider + { + public event Action AuthorizationChanged; + public event Action AccuracyAuthorizationChanged; + public event Action AvailabilityChanged; + + public MapboxLocationSettings Settings; + private IMapboxDeviceLocation _mapboxDeviceLocation; + + public MapboxLocationProvider(MapboxLocationSettings settings) + { + Settings = settings; +#if UNITY_IOS && !UNITY_EDITOR + _mapboxDeviceLocation = new MapboxLocationIos(Settings); +#elif UNITY_ANDROID && !UNITY_EDITOR + _mapboxDeviceLocation = new MapboxLocationAndroid(Settings); +#endif + + if (_mapboxDeviceLocation != null) + { + _mapboxDeviceLocation.LocationUpdated += (s) => + { + _currentLocation = s; + SendLocation(s); + }; + _mapboxDeviceLocation.AvailabilityChanged += (v) => AvailabilityChanged?.Invoke(v); + _mapboxDeviceLocation.AuthorizationChanged += (v) => AuthorizationChanged?.Invoke(v); + _mapboxDeviceLocation.AccuracyAuthorizationChanged += (v) => AccuracyAuthorizationChanged?.Invoke(v); + + _mapboxDeviceLocation.Initialize(); + } + } + + public override void Update() + { + _mapboxDeviceLocation?.Update(); + } + + public override void OnDestroy() + { + _mapboxDeviceLocation?.OnDestroy(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs.meta new file mode 100644 index 000000000..691de9699 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 340989a2b4f14f6a823b107af7c1aacc +timeCreated: 1769267389 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs new file mode 100644 index 000000000..dfb0aa4d0 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs @@ -0,0 +1,63 @@ +using System; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationServiceObserverProxy : AndroidJavaProxy + { + private const string _logPrefix = "MAPBOX_UNITY_SDK: "; + + private readonly Action _authorizationChanged; + private readonly Action _accuracyAuthorizationChanged; + private readonly Action _availabilityChanged; + private readonly Action _enqueueOnMainThread; + + public MapboxLocationServiceObserverProxy( + Action authorizationChanged, + Action accuracyAuthorizationChanged, + Action availabilityChanged, + Action enqueueOnMainThread) : base("com.mapbox.common.location.LocationServiceObserver") + { + _authorizationChanged = authorizationChanged; + _accuracyAuthorizationChanged = accuracyAuthorizationChanged; + _availabilityChanged = availabilityChanged; + _enqueueOnMainThread = enqueueOnMainThread; + } + + + [Preserve] + public void onAvailabilityChanged(bool isAvailable) + { + _enqueueOnMainThread?.Invoke(() => _availabilityChanged?.Invoke(isAvailable)); + } + + [Preserve] + public void onPermissionStatusChanged(AndroidJavaObject permission) + { + if (permission == null) + { + Debug.LogError(_logPrefix + "onPermissionStatusChanged received null"); + return; + } + + int ordinal = permission.Call("ordinal"); + var status = (MapboxLocationServiceStatus)ordinal; + _enqueueOnMainThread?.Invoke(() => _authorizationChanged?.Invoke(status)); + } + + [Preserve] + public void onAccuracyAuthorizationChanged(AndroidJavaObject authorization) + { + if (authorization == null) + { + Debug.LogError(_logPrefix + "onAccuracyAuthorizationChanged received null"); + return; + } + + int ordinal = authorization.Call("ordinal"); + var accuracy = (AccuracyAuthorization)ordinal; + _enqueueOnMainThread?.Invoke(() => _accuracyAuthorizationChanged?.Invoke(accuracy)); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs.meta new file mode 100644 index 000000000..22c988320 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: acb662a539c346beafce8455dc4ab3c0 +timeCreated: 1770213213 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs new file mode 100644 index 000000000..d85da597c --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs @@ -0,0 +1,84 @@ +using System; +using UnityEngine; + +namespace Mapbox.LocationModule.MapboxLocation +{ + [Serializable] + public class MapboxLocationSettings + { + // The accuracy of the observed location + [Tooltip("The accuracy of the observed location")] + public MapboxLocationAccuracyLevel AccuracyLevel; + + // Minimum displacement between location updates in meters. + [Tooltip("Minimum displacement between location updates in meters.")] + public float Displacement; + + /// + /// The fastest rate at which the application will receive location updates, which might be faster than the `Interval`. + /// Unlike `Interval` this parameter is exact. + /// + [Tooltip("The fastest rate at which the application will receive location updates, which might be faster than the `Interval`. Unlike `Interval` this parameter is exact.")] + public long MinimumInterval; + /// + /// Maximum wait time for location updates. If it's at least 2x larger then `Interval`, then location delivery may be delayed and multiple locations can be delivered at once. + /// + [Tooltip("Maximum wait time for location updates. If it's at least 2x larger then `Interval`, then location delivery may be delayed and multiple locations can be delivered at once.")] + public long MaximumInterval; + /// + /// Desired interval for active location updates. + /// + [Tooltip("Desired interval for active location updates.")] + public long Interval; + } + + public enum MapboxLocationAccuracyLevel + { + Passive, + // Low accuracy requirement (typically greater than 500 meters). + Low, + // Medium accuracy requirement (typically between 100 and 500 meters). + Medium, + // High accuracy requirement. + High, + //The highest possible accuracy requirement that uses additional sensors (if possible) to facilitate navigation use case. + Highest + } + + /// + /// Location permissions granted by user to the app. + /// Maps to MBXPermissionStatus from MapboxCommon. + /// + public enum MapboxLocationServiceStatus + { + /// Access to location is not allowed. + Denied = 0, + /// + /// Access to location is allowed. + /// This type of permission is defined for platforms that + /// do not have foreground/background access granularity. + /// + Granted = 1, + /// Access to location is allowed only while an app is in use. + Foreground = 2, + /// Access to location is allowed all the time. + Background = 3 + } + + /// + /// Accuracy authorization granted by user to the app. + /// Maps to MBXAccuracyAuthorization from MapboxCommon. + /// + public enum AccuracyAuthorization + { + /// An app is not authorized to access location. + None = 0, + /// An app is authorized to received as precise as possible location. + Exact = 1, + /// + /// An app is authorized to receive rough location only. + /// Depends on a platform the accuracy is within a city block. + /// + Inexact = 2 + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs.meta new file mode 100644 index 000000000..ec68eb83f --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f38b77fb68c43b3a0042227fd6f4696 +timeCreated: 1769266999 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Scripts.meta b/Runtime/Mapbox/LocationModule/Scripts.meta new file mode 100644 index 000000000..5fa752e09 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1a5b7b083ac6940fba41ca91dbbe80e2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs b/Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs similarity index 61% rename from Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs rename to Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs index 4b7989337..59c268867 100644 --- a/Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs @@ -1,5 +1,4 @@ -using System.Collections; -using UnityEngine; +using UnityEngine; namespace Mapbox.LocationModule { @@ -22,27 +21,9 @@ public abstract class AbstractEditorLocationProvider : AbstractLocationProvider #if UNITY_EDITOR protected virtual void Awake() { - _wait = new WaitForSeconds(_updateInterval); - StartCoroutine(QueryLocation()); } #endif - IEnumerator QueryLocation() - { - // HACK: Let others register before we send our first event. - // Often this happens in Start. - yield return new WaitForSeconds(.1f); - while (true) - { - SetLocation(); - if (_autoFireEvent) - { - SendLocation(_currentLocation); - } - yield return _wait; - } - } - // Added to support TouchCamera script. public void SendLocationEvent() diff --git a/Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs b/Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs similarity index 54% rename from Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs rename to Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs index b7126ec40..2e8f66d85 100644 --- a/Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs @@ -1,9 +1,8 @@ using System; -using UnityEngine; namespace Mapbox.LocationModule { - public abstract class AbstractLocationProvider : MonoBehaviour, ILocationProvider + public abstract class AbstractLocationProvider : ILocationProvider { protected Location _currentLocation; @@ -19,11 +18,27 @@ public Location CurrentLocation } } + /// + /// Has at least one location update been received from the provider? + /// Set once and never reset. + /// + public bool HasReceivedFirstFix { get; protected set; } + public event Action OnLocationUpdated = delegate { }; protected virtual void SendLocation(Location location) { + HasReceivedFirstFix = true; OnLocationUpdated(location); } + + public virtual void Update() + { + + } + + public virtual void OnDestroy() + { + } } } \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs similarity index 92% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs index 67e80925e..6e66b6b5d 100644 --- a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs @@ -1,5 +1,4 @@ using System; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule.AngleSmoothing @@ -7,7 +6,7 @@ namespace Mapbox.LocationModule.AngleSmoothing /// /// Base class for implementing different smoothing strategies /// - public abstract class AngleSmoothingAbstractBase : MonoBehaviour, IAngleSmoothing + public abstract class AngleSmoothingAbstractBase : IAngleSmoothing { @@ -24,7 +23,7 @@ public AngleSmoothingAbstractBase() /// - /// Internal storage for latest 'n' values. Latest value at [0], + /// Internal storage for latest 'n' values. Latest value at [0], /// protected CircularBuffer _angles; diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAverage.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAverage.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAverage.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAverage.cs diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAverage.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAverage.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAverage.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAverage.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs similarity index 98% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs index 121116375..2b1b3b795 100644 --- a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs @@ -1,5 +1,4 @@ - -using System; +using System; using System.Linq; namespace Mapbox.LocationModule.AngleSmoothing diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingLowPass.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingLowPass.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingLowPass.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingLowPass.cs diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingLowPass.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingLowPass.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingLowPass.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingLowPass.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingNoOp.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingNoOp.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingNoOp.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingNoOp.cs diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingNoOp.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingNoOp.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingNoOp.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingNoOp.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/IAngleSmoothing.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/IAngleSmoothing.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/IAngleSmoothing.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/IAngleSmoothing.cs diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/IAngleSmoothing.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/IAngleSmoothing.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/IAngleSmoothing.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/IAngleSmoothing.cs.meta diff --git a/Runtime/Mapbox/LocationModule/CircularBuffer.cs b/Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs similarity index 98% rename from Runtime/Mapbox/LocationModule/CircularBuffer.cs rename to Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs index f93b755d7..0233b6b9e 100644 --- a/Runtime/Mapbox/LocationModule/CircularBuffer.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Collections.Generic; -namespace Mapbox.Utils +namespace Mapbox.LocationModule { diff --git a/Runtime/Mapbox/LocationModule/CircularBuffer.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/CircularBuffer.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs.meta diff --git a/Runtime/Mapbox/LocationModule/EditorLocationProviderLocationLog.cs b/Runtime/Mapbox/LocationModule/Scripts/EditorLocationProviderLocationLog.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/EditorLocationProviderLocationLog.cs rename to Runtime/Mapbox/LocationModule/Scripts/EditorLocationProviderLocationLog.cs diff --git a/Runtime/Mapbox/LocationModule/EditorLocationProviderLocationLog.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/EditorLocationProviderLocationLog.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/EditorLocationProviderLocationLog.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/EditorLocationProviderLocationLog.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs b/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs new file mode 100644 index 000000000..a4837432e --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs @@ -0,0 +1,51 @@ +using System; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule +{ + public class GetLocationCallbackProxy : AndroidJavaProxy + { + private const string _logPrefix = "MAPBOX_UNITY_SDK: "; + private const string _getLatitude = "getLatitude"; + private const string _getLongitude = "getLongitude"; + + private readonly Action _callback; + + public GetLocationCallbackProxy(Action callback) : base("com.mapbox.common.location.GetLocationCallback") + { + _callback = callback; + } + + [Preserve] + public void run(AndroidJavaObject locationObj) + { + if (locationObj == null) + { + _callback?.Invoke(default(Location), false); + return; + } + + try + { + double lat = locationObj.Call(_getLatitude); + double lon = locationObj.Call(_getLongitude); + + Location location = new Location + { + LatitudeLongitude = new LatitudeLongitude(lat, lon), + TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow) + }; + + _callback?.Invoke(location, true); + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"GetLocationCallback error: {ex.Message}"); + _callback?.Invoke(default(Location), false); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs.meta new file mode 100644 index 000000000..91948fbf4 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 917962ec991b4b85a23435458f3fd66d +timeCreated: 1770213236 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/ILocationProvider.cs b/Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs similarity index 78% rename from Runtime/Mapbox/LocationModule/ILocationProvider.cs rename to Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs index a3210c807..b63e30f16 100644 --- a/Runtime/Mapbox/LocationModule/ILocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs @@ -9,5 +9,9 @@ public interface ILocationProvider { event Action OnLocationUpdated; Location CurrentLocation { get; } + bool HasReceivedFirstFix { get; } + + void Update(); + void OnDestroy(); } } \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/ILocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ILocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs b/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs new file mode 100644 index 000000000..4418d21e2 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs @@ -0,0 +1,16 @@ +using System; +using Mapbox.LocationModule.MapboxLocation; + +namespace Mapbox.LocationModule +{ + public interface IMapboxDeviceLocation + { + event Action LocationUpdated; + event Action AuthorizationChanged; + event Action AccuracyAuthorizationChanged; + event Action AvailabilityChanged; + void Update(); + void OnDestroy(); + void Initialize(); + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs.meta new file mode 100644 index 000000000..31a55c0a7 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7c4b7a0b27194e59a06c8c41b693a3b4 +timeCreated: 1769279814 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Location.cs b/Runtime/Mapbox/LocationModule/Scripts/Location.cs similarity index 89% rename from Runtime/Mapbox/LocationModule/Location.cs rename to Runtime/Mapbox/LocationModule/Scripts/Location.cs index c22ba1924..948a0c395 100644 --- a/Runtime/Mapbox/LocationModule/Location.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/Location.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using Mapbox.BaseModule.Data.Vector2d; -using Mapbox.Utils; namespace Mapbox.LocationModule { @@ -94,6 +93,17 @@ public float? SpeedKmPerHour return SpeedMetersPerSecond * 3.6f; } } + + /// + /// Altitude in meters above sea level. 'Null' if not supported by the active location provider. + /// + public double? Altitude; + + /// + /// Bearing in degrees (0-360). Bearing is the horizontal direction of travel of this device, and is not related to the device orientation. 'Null' if not supported by the active location provider. + /// + public float? Bearing; + /// /// Name of the location provider. GPS or network or 'Null' if not supported by the active location provider. /// diff --git a/Runtime/Mapbox/LocationModule/Location.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/Location.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Location.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/Location.cs.meta diff --git a/Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs b/Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs similarity index 98% rename from Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs rename to Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs index f8d321c15..08ef16a29 100644 --- a/Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs @@ -2,7 +2,6 @@ using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; using Mapbox.BaseModule.Utilities.Attributes; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule diff --git a/Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs b/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs new file mode 100644 index 000000000..ae9feee91 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections; +using Mapbox.LocationModule.MapboxLocation; +using UnityEngine; + +namespace Mapbox.LocationModule +{ + /// + /// Factory to provide access to various LocationProviders. + /// This is meant to be attached to a game object. + /// + public sealed class LocationProviderFactory : MonoBehaviour + { + public LocationProviderType LocationProviderType; + [NonSerialized] public bool IsLocationProviderReady = false; + public Action OnLocationProviderReady = (f) => { }; + + [SerializeField] + [Tooltip("Mapbox location provider for android and ios")] + private MapboxLocationSettings _mapboxLocationProviderSettings = null; + + [SerializeField] + [Tooltip("Provider using Unity's builtin 'Input.Location' service")] + private UnityLocationProviderSettings _unityLocationProviderSettings; + + private AbstractLocationProvider _customLocationProvider; + + [SerializeField] private string EditorLatitudeLongitude; + + [SerializeField] + bool _dontDestroyOnLoad; + + /// + /// The default location provider. + /// Outside of the editor, this will be a . + /// In the Unity editor, this will be an + /// + public ILocationProvider DefaultLocationProvider { get; set; } + + /// + /// Initialize and inject the DefaultLocationProvider. + /// + public IEnumerator Initialize() + { + if (_dontDestroyOnLoad) + { + DontDestroyOnLoad(gameObject); + } + + if(Application.isEditor || LocationProviderType == LocationProviderType.StaticLocationProvider) + { + DefaultLocationProvider = new StaticLocationProvider(EditorLatitudeLongitude); + } + else if(LocationProviderType == LocationProviderType.MapboxLocationProvider) + { + var mapboxLocationProvider = new MapboxLocationProvider(_mapboxLocationProviderSettings); + mapboxLocationProvider.AvailabilityChanged += b => Debug.Log("MAPBOX_UNITY_SDK: LocationProviderFactory.AvailabilityChanged " + b); + mapboxLocationProvider.AuthorizationChanged += b => Debug.Log("MAPBOX_UNITY_SDK: AuthorizationChanged " + b); + mapboxLocationProvider.AccuracyAuthorizationChanged += b => Debug.Log("MAPBOX_UNITY_SDK: AuthorizationChanged " + b); + DefaultLocationProvider = mapboxLocationProvider; + } + else if(LocationProviderType == LocationProviderType.UnityLocationProvider) + { + DefaultLocationProvider = new UnityLocationProvider(_unityLocationProviderSettings); + } + // else if(LocationProviderType == LocationProviderType.CustomLocationProvider) + // { + // DefaultLocationProvider = new UnityLocationProvider(_unityLocationProviderSettings); + // } + + Debug.Log($"MAPBOX_UNITY_SDK: LocationProviderFactory: Injected Location Provider - {DefaultLocationProvider.GetType()}"); + + if (DefaultLocationProvider.GetType() != typeof(StaticLocationProvider)) + { + if (Input.location.status != LocationServiceStatus.Running) + { + Input.location.Start(); + + while (Input.location.status < LocationServiceStatus.Running) + yield return null; + } + + // Wait for first location update with timeout (10 seconds) + float timeout = 10f; + float elapsed = 0f; + while (!DefaultLocationProvider.HasReceivedFirstFix && + elapsed < timeout) + { + elapsed += UnityEngine.Time.deltaTime; + yield return null; + } + } + + IsLocationProviderReady = true; + OnLocationProviderReady(this); + } + + private void Update() + { + DefaultLocationProvider?.Update(); + } + + private void OnDestroy() + { + DefaultLocationProvider?.OnDestroy(); + } + } + + public enum LocationProviderType + { + [InspectorName("Static Location Provider")] + StaticLocationProvider, + + [InspectorName("Unity Location Provider")] + UnityLocationProvider, + + [InspectorName("Mapbox Location Provider (Experimental)")] + MapboxLocationProvider, + + // [InspectorName("Custom Location Provider")] + // CustomLocationProvider + } +} + + diff --git a/Runtime/Mapbox/LocationModule/LocationProviderFactory.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationProviderFactory.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs.meta diff --git a/Runtime/Mapbox/LocationModule/LocationSmoothing.meta b/Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationSmoothing.meta rename to Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing.meta diff --git a/Runtime/Mapbox/LocationModule/LocationSmoothing/KalmanFilter.cs b/Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing/KalmanFilter.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationSmoothing/KalmanFilter.cs rename to Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing/KalmanFilter.cs diff --git a/Runtime/Mapbox/LocationModule/LocationSmoothing/KalmanFilter.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing/KalmanFilter.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationSmoothing/KalmanFilter.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing/KalmanFilter.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Logging.meta b/Runtime/Mapbox/LocationModule/Scripts/Logging.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging.meta rename to Runtime/Mapbox/LocationModule/Scripts/Logging.meta diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogAbstractBase.cs b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogAbstractBase.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogAbstractBase.cs rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogAbstractBase.cs diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogAbstractBase.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogAbstractBase.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogAbstractBase.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogAbstractBase.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs similarity index 99% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs index d29e659fb..2a678a794 100644 --- a/Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs @@ -4,7 +4,6 @@ using System.IO; using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule.Logging diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs similarity index 97% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs index 326eea837..52d4f5559 100644 --- a/Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs @@ -2,7 +2,6 @@ using System.IO; using System.Text; using Mapbox.BaseModule.Utilities; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule.Logging @@ -92,7 +91,6 @@ public void Write(Location location) location.IsLocationUpdated.ToString(), location.IsUserHeadingUpdated.ToString(), location.Provider, - LocationProviderFactory.Instance.DefaultLocationProvider.GetType().Name, DateTime.UtcNow.ToString("yyyyMMdd-HHmmss.fff"), UnixTimestampUtils.From(location.Timestamp).ToString("yyyyMMdd-HHmmss.fff"), string.Format(_invariantCulture, "{0:0.00000000}", location.LatitudeLongitude.Latitude), diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationInfo.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationInfo.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationInfo.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationInfo.cs diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationInfo.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationInfo.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationInfo.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationInfo.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs similarity index 81% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs index ad088e59c..fe38c0108 100644 --- a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs @@ -1,5 +1,3 @@ -using UnityEngine; - namespace Mapbox.LocationModule.UnityLocationWrappers { public interface IMapboxLocationService @@ -8,7 +6,7 @@ public interface IMapboxLocationService bool isEnabledByUser { get; } - LocationServiceStatus status { get; } + UnityEngine.LocationServiceStatus status { get; } IMapboxLocationInfo lastData { get; } diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoMock.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoMock.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoMock.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoMock.cs diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs similarity index 93% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs index 81d7535a7..db1e3c73f 100644 --- a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using Mapbox.LocationModule.Logging; @@ -75,7 +74,7 @@ protected virtual void Dispose(bool disposeManagedResources) public bool isEnabledByUser { get { return true; } } - public LocationServiceStatus status { get { return _isRunning ? LocationServiceStatus.Running : LocationServiceStatus.Stopped; } } + public UnityEngine.LocationServiceStatus status { get { return _isRunning ? LocationServiceStatus.Running : LocationServiceStatus.Stopped; } } public IMapboxLocationInfo lastData diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs similarity index 88% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs index 37f4b3996..e88c2de04 100644 --- a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs @@ -11,7 +11,7 @@ public class MapboxLocationServiceUnityWrapper : IMapboxLocationService public bool isEnabledByUser { get { return Input.location.isEnabledByUser; } } - public LocationServiceStatus status { get { return Input.location.status; } } + public UnityEngine.LocationServiceStatus status { get { return Input.location.status; } } public IMapboxLocationInfo lastData { get { return new MapboxLocationInfoUnityWrapper(Input.location.lastData); } } diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta diff --git a/Runtime/Mapbox/LocationModule/EditorLocationProvider.cs b/Runtime/Mapbox/LocationModule/StaticLocationProvider.cs similarity index 56% rename from Runtime/Mapbox/LocationModule/EditorLocationProvider.cs rename to Runtime/Mapbox/LocationModule/StaticLocationProvider.cs index 277e00d69..cc1681098 100644 --- a/Runtime/Mapbox/LocationModule/EditorLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/StaticLocationProvider.cs @@ -2,7 +2,6 @@ using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; using Mapbox.BaseModule.Utilities.Attributes; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule @@ -11,29 +10,31 @@ namespace Mapbox.LocationModule /// The EditorLocationProvider is responsible for providing mock location and heading data /// for testing purposes in the Unity editor. /// - public class EditorLocationProvider : AbstractEditorLocationProvider + public class StaticLocationProvider : AbstractEditorLocationProvider { - /// - /// The mock "latitude, longitude" location, respresented with a string. - /// You can search for a place using the embedded "Search" button in the inspector. - /// This value can be changed at runtime in the inspector. - /// - [SerializeField] - [Geocode] - private string _latitudeLongitude; private LatitudeLongitude _latLng; + + public StaticLocationProvider(LatitudeLongitude latLng) + { + _latLng = latLng; + _currentLocation = new Location() + { + LatitudeLongitude = _latLng + }; + } -#if UNITY_EDITOR - protected virtual void Start() + public StaticLocationProvider(string latLng) { - base.Awake(); - _latLng = Conversions.StringToLatLon(_latitudeLongitude); + _latLng = Conversions.StringToLatLon(latLng); + _currentLocation = new Location() + { + LatitudeLongitude = _latLng + }; } -#endif protected override void SetLocation() { - _currentLocation.UserHeading = transform.eulerAngles.y; + //_currentLocation.UserHeading = transform.eulerAngles.y; _currentLocation.LatitudeLongitude = _latLng; _currentLocation.Accuracy = _accuracy; _currentLocation.Timestamp = UnixTimestampUtils.To(DateTime.UtcNow); diff --git a/Runtime/Mapbox/LocationModule/EditorLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/StaticLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/EditorLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/StaticLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationProvider.cs b/Runtime/Mapbox/LocationModule/UnityLocationProvider.cs new file mode 100644 index 000000000..aeb938413 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/UnityLocationProvider.cs @@ -0,0 +1,227 @@ +using System; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.LocationModule.AngleSmoothing; +using Mapbox.LocationModule.UnityLocationWrappers; +using UnityEngine; +using UnityEngine.Android; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule +{ + [Serializable] + public class UnityLocationProviderSettings + { + /// + /// Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. + /// Values like 5-10 could be used for getting best accuracy. + /// + [SerializeField] + [Tooltip( + "Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. Values like 5-10 could be used for getting best accuracy.")] + public float DesiredAccuracyInMeters = 1.0f; + + /// + /// The minimum distance (measured in meters) a device must move laterally before Input.location property is updated. + /// Higher values like 500 imply less overhead. + /// + [SerializeField] + [Tooltip( + "The minimum distance (measured in meters) a device must move laterally before Input.location property is updated. Higher values like 500 imply less overhead.")] + public float UpdateDistanceInMeters = 0.0f; + + [SerializeField] + [Tooltip( + "The minimum time interval between location updates, in milliseconds. It's reasonable to not go below 500ms.")] + public long UpdateTimeInMilliSeconds = 500; + + [SerializeField] [Tooltip("Smoothing strategy to be applied to the UserHeading.")] + public AngleSmoothingAbstractBase UserHeadingSmoothing; + + [SerializeField] [Tooltip("Smoothing strategy to applied to the DeviceOrientation.")] + public AngleSmoothingAbstractBase DeviceOrientationSmoothing; + } + + /// + /// The DeviceLocationProvider is responsible for providing real world location and heading data, + /// served directly from native hardware and OS. + /// This relies on Unity's LocationService for location + /// and Compass for heading. + /// + public class UnityLocationProvider : AbstractLocationProvider + { + private IMapboxLocationService _locationService; + private double _lastLocationTimestamp; + private float _lastPollTime; + private float _updateInterval; + + /// list of positions to keep for calculations + private CircularBuffer _lastPositions; + + /// number of last positons to keep + private int _maxLastPositions = 5; + + /// minimum needed distance between oldest and newest position before UserHeading is calculated + private double _minDistanceOldestNewestPosition = 1.5; + + private readonly UnityLocationProviderSettings _unityLocationProviderSettings = new UnityLocationProviderSettings(); + + private const string FineLocation = Permission.FineLocation; + + public UnityLocationProviderSettings UnityLocationProviderSettings => _unityLocationProviderSettings; + + /// + /// Constructor for production use - uses Unity's real location service. + /// + [Preserve] + public UnityLocationProvider(UnityLocationProviderSettings settings) : this(settings, new MapboxLocationServiceUnityWrapper()) + { + } + + /// + /// Constructor for testing - accepts mock location service. + /// Use MapboxLocationServiceMock to replay location logs for testing. + /// + public UnityLocationProvider(UnityLocationProviderSettings settings, IMapboxLocationService locationService) + { + _unityLocationProviderSettings = settings; + _locationService = locationService ?? throw new ArgumentNullException(nameof(locationService)); + //HandlePermission(); + + _currentLocation.Provider = "unity"; + _updateInterval = UnityLocationProviderSettings.UpdateTimeInMilliSeconds < 500 + ? 0.5f + : (float)UnityLocationProviderSettings.UpdateTimeInMilliSeconds / 1000.0f; + + if (null == UnityLocationProviderSettings.UserHeadingSmoothing) + { + UnityLocationProviderSettings.UserHeadingSmoothing = new AngleSmoothingNoOp(); + } + + if (null == UnityLocationProviderSettings.DeviceOrientationSmoothing) + { + UnityLocationProviderSettings.DeviceOrientationSmoothing = new AngleSmoothingNoOp(); + } + + _lastPositions = new CircularBuffer(_maxLastPositions); + } + + public override void Update() + { + if (Time.realtimeSinceStartup - _lastPollTime < _updateInterval) + return; + _lastPollTime = Time.realtimeSinceStartup; + + var lastData = _locationService.lastData; + var timestamp = lastData.timestamp; + + _currentLocation.IsLocationServiceEnabled = + _locationService.status == LocationServiceStatus.Running + || timestamp > _lastLocationTimestamp; + + _currentLocation.IsUserHeadingUpdated = false; + _currentLocation.IsLocationUpdated = false; + + if (!_currentLocation.IsLocationServiceEnabled) + return; + + // device orientation, user heading get calculated below + UnityLocationProviderSettings.DeviceOrientationSmoothing.Add(Input.compass.trueHeading); + _currentLocation.DeviceOrientation = + (float)UnityLocationProviderSettings.DeviceOrientationSmoothing.Calculate(); + + double latitude = lastData.latitude; + double longitude = lastData.longitude; + Vector2d previousLocation = new Vector2d(_currentLocation.LatitudeLongitude.Latitude, _currentLocation.LatitudeLongitude.Longitude); + _currentLocation.LatitudeLongitude = new LatitudeLongitude(latitude, longitude); + + _currentLocation.Accuracy = (float)Math.Floor(lastData.horizontalAccuracy); + // sometimes Unity's timestamp doesn't seem to get updated, or even jump back in time + // do an additional check if location has changed + _currentLocation.IsLocationUpdated = timestamp > _lastLocationTimestamp || !_currentLocation.LatitudeLongitude.Equals(previousLocation); + _currentLocation.Timestamp = timestamp; + _lastLocationTimestamp = timestamp; + + if (_currentLocation.IsLocationUpdated) + { + if (_lastPositions.Count > 0) + { + // only add position if user has moved +1m since we added the previous position to the list + CheapRuler cheapRuler = new CheapRuler(_currentLocation.LatitudeLongitude.Latitude, CheapRulerUnits.Meters); + var p = _currentLocation.LatitudeLongitude; + double distance = cheapRuler.Distance( + new double[] { p.Longitude, p.Latitude }, + new double[] { _lastPositions[0].Longitude, _lastPositions[0].Latitude } + ); + if (distance > 1.0) + { + _lastPositions.Add(_currentLocation.LatitudeLongitude); + } + } + else + { + _lastPositions.Add(_currentLocation.LatitudeLongitude); + } + } + + // if we have enough positions calculate user heading ourselves. + // Unity does not provide bearing based on GPS locations, just + // device orientation based on Compass.Heading. + // nevertheless, use compass for initial UserHeading till we have + // enough values to calculate ourselves. + if (_lastPositions.Count < _maxLastPositions) + { + _currentLocation.UserHeading = _currentLocation.DeviceOrientation; + _currentLocation.IsUserHeadingUpdated = true; + } + else + { + var newestPos = _lastPositions[0]; + var oldestPos = _lastPositions[_maxLastPositions - 1]; + CheapRuler cheapRuler = new CheapRuler(newestPos.Latitude, CheapRulerUnits.Meters); + double distance = cheapRuler.Distance( + new double[] { newestPos.Longitude, newestPos.Latitude }, + new double[] { oldestPos.Longitude, oldestPos.Latitude } + ); + // positions are minimum required distance apart (user is moving), calculate user heading + if (distance >= _minDistanceOldestNewestPosition) + { + float[] lastHeadings = new float[_maxLastPositions - 1]; + + for (int i = 1; i < _maxLastPositions; i++) + { + // atan2 increases angle CCW, flip sign of latDiff to get CW + double latDiff = -(_lastPositions[i].Latitude - _lastPositions[i - 1].Latitude); + double lngDiff = _lastPositions[i].Longitude - _lastPositions[i - 1].Longitude; + // +90.0 to make top (north) 0 degrees + double heading = (Math.Atan2(latDiff, lngDiff) * 180.0 / Math.PI) + 90.0f; + // stay within [0..360] range + if (heading < 0) + { + heading += 360; + } + + if (heading >= 360) + { + heading -= 360; + } + + lastHeadings[i - 1] = (float)heading; + } + + UnityLocationProviderSettings.UserHeadingSmoothing.Add(lastHeadings[0]); + float finalHeading = (float)UnityLocationProviderSettings.UserHeadingSmoothing.Calculate(); + + //fix heading to have 0 for north, 90 for east, 180 for south and 270 for west + finalHeading = finalHeading >= 180.0f ? finalHeading - 180.0f : finalHeading + 180.0f; + + _currentLocation.UserHeading = finalHeading; + _currentLocation.IsUserHeadingUpdated = true; + } + } + + _currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow); + SendLocation(_currentLocation); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/UnityLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/UnityLocationProvider.cs.meta diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs index 479e05155..ea1498ade 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Platform.Cache; using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; @@ -42,7 +43,7 @@ public virtual void Start() } [ContextMenu("Initialize Map")] - public override void Initialize() + public override IEnumerator Initialize() { _mapLogger = FindObjectOfType(); #if UNITY_RECORDER && UNITY_EDITOR @@ -54,10 +55,11 @@ public override void Initialize() var taskManager = new LoggingTaskManager(); _mapLogger?.AddLogger(taskManager); - UnityContext.Initialize(taskManager); + yield return UnityContext.Initialize(taskManager); var mapboxContext = new MapboxContext(); + yield return mapboxContext.Initialize(); _mapService = GetMapService(mapboxContext, UnityContext); MapServiceReady(_mapService); diff --git a/Runtime/Mapbox/MapDebug/Scripts/TestMap.cs b/Runtime/Mapbox/MapDebug/Scripts/TestMap.cs index 028d60fee..1a9407a81 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/TestMap.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/TestMap.cs @@ -1,3 +1,4 @@ +using System.Collections; using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Data.Platform.Cache; @@ -16,10 +17,11 @@ namespace Mapbox.MapDebug.Scripts { public class TestMap : MonoBehaviour { - public void Start() + public IEnumerator Start() { var mapInfo = new MapInformation("60.1734031,24.9428875"); var mapboxContext = new MapboxContext(); + yield return mapboxContext.Initialize(); var unityContext = new UnityContext(); var taskManager = new TaskManager(); diff --git a/Samples~/LocationBasedGame/LocationGame.unity b/Samples~/LocationBasedGame/LocationGame.unity index 537b04d85..78c7b2569 100644 --- a/Samples~/LocationBasedGame/LocationGame.unity +++ b/Samples~/LocationBasedGame/LocationGame.unity @@ -142,7 +142,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &6544153 RectTransform: m_ObjectHideFlags: 0 @@ -161,7 +161,7 @@ RectTransform: m_AnchorMin: {x: 0, y: 0.5} m_AnchorMax: {x: 0, y: 0.5} m_AnchoredPosition: {x: 16, y: -62} - m_SizeDelta: {x: 577.9446, y: 0} + m_SizeDelta: {x: 577.9446, y: 563} m_Pivot: {x: 0, y: 0.5} --- !u!114 &6544154 MonoBehaviour: @@ -365,11 +365,46 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!4 &220147485 stripped +--- !u!1 &220147481 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 220147485} + - component: {fileID: 220147484} + m_Layer: 0 + m_Name: CharTarget + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!33 &220147484 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 220147481} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &220147485 Transform: - m_CorrespondingSourceObject: {fileID: 7512784583129558687, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - m_PrefabInstance: {fileID: 7512784582910460290} + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 220147481} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0.40000007} + m_LocalScale: {x: 0.2, y: 0.19999996, z: 0.19999996} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2054936215} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &482478434 GameObject: m_ObjectHideFlags: 0 @@ -445,6 +480,95 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 482478434} m_CullTransparentMesh: 1 +--- !u!1 &525849124 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 525849129} + - component: {fileID: 525849128} + - component: {fileID: 525849131} + - component: {fileID: 525849130} + m_Layer: 0 + m_Name: LocationModule + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &525849128 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b55f37f9a6f7e44f7bb35e6bc3863847, type: 3} + m_Name: + m_EditorClassIdentifier: + LocationProviderType: 1 + _mapboxLocationProviderSettings: + AccuracyLevel: 0 + Displacement: 0 + MinimumInterval: 0 + MaximumInterval: 0 + Interval: 0 + _unityLocationProviderSettings: + DesiredAccuracyInMeters: 1 + UpdateDistanceInMeters: 0 + UpdateTimeInMilliSeconds: 500 + EditorLatitudeLongitude: 60.1712373,24.9435977 + _dontDestroyOnLoad: 0 +--- !u!4 &525849129 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0.40000007} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2054936215} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &525849130 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a5534c9989da84262bc64ffe27afbbb6, type: 3} + m_Name: + m_EditorClassIdentifier: + Map: {fileID: 1316629399} + LocationProviderFactory: {fileID: 525849128} + ShiftMapDistance: 500 +--- !u!114 &525849131 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 137870786121148788eada666e6cd719, type: 3} + m_Name: + m_EditorClassIdentifier: + _transform: {fileID: 220147485} + _locationProvider: {fileID: 525849128} + _mapCore: {fileID: 1316629399} --- !u!1 &545031483 GameObject: m_ObjectHideFlags: 0 @@ -826,6 +950,112 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0} +--- !u!1 &885811049 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 885811053} + - component: {fileID: 885811052} + - component: {fileID: 885811051} + - component: {fileID: 885811050} + m_Layer: 0 + m_Name: Plane + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!64 &885811050 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885811049} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 5 + m_Convex: 0 + m_CookingOptions: 30 + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &885811051 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885811049} + m_Enabled: 0 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &885811052 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885811049} + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &885811053 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885811049} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0.40000007} + m_LocalScale: {x: 999, y: 998.99976, z: 998.99976} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2054936215} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &923185051 GameObject: m_ObjectHideFlags: 0 @@ -862,7 +1092,7 @@ RectTransform: m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} m_AnchoredPosition: {x: 288.9723, y: -281.5} - m_SizeDelta: {x: 545.9446, y: 0} + m_SizeDelta: {x: 545.9446, y: 531} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &923185053 MonoBehaviour: @@ -1370,17 +1600,114 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1271358336} m_CullTransparentMesh: 1 ---- !u!114 &1316629399 stripped +--- !u!1 &1316629397 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1316629400} + - component: {fileID: 1316629399} + - component: {fileID: 1377339556045437849} + - component: {fileID: 1316629404} + - component: {fileID: 1316629402} + m_Layer: 0 + m_Name: Map + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1316629399 MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 7512784584146348565, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - m_PrefabInstance: {fileID: 7512784582910460290} + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} + m_GameObject: {fileID: 1316629397} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 8987c930e6fb0479a907044faef4ab26, type: 3} m_Name: m_EditorClassIdentifier: + MapInformation: + _pitch: 45 + _bearing: 0 + _scale: 1 + _latitudeLongitudeString: 60.1712373,24.9435977 + _zoom: 15 + UnityContext: + LocationPermissionState: 0 + MapRoot: {fileID: 1316629400} + BaseTileRoot: {fileID: 0} + RuntimeGenerationRoot: {fileID: 0} + _tileCreatorBehaviour: {fileID: 1377339556045437849} + TileProvider: {fileID: 8067237297740704311} + DataFetcher: {fileID: 0} + CacheManager: {fileID: 0} + LocationFactory: {fileID: 525849128} + InitializeOnStart: 1 +--- !u!4 &1316629400 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2054936215} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1316629402 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 13ef846e428d4d41b3827620e39b8a6a, type: 3} + m_Name: + m_EditorClassIdentifier: + vectorModuleSettings: + SourceType: 0 + CustomSourceId: + DataSettings: + CacheSize: 100 + ClampDataLevelToMax: 15 + RejectTilesOutsideZoom: {x: 12, y: 16} + _layerVisualizers: + - {fileID: 11400000, guid: 4aa8336b8547a4f4fa3bff4d3fb0c106, type: 2} +--- !u!114 &1316629404 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ac0bc0b01b604b8889bb66087e8c94d9, type: 3} + m_Name: + m_EditorClassIdentifier: + Settings: + SourceType: 0 + CustomSourceId: + LoadBackgroundTextures: 0 + DataSettings: + UseRetinaTextures: 1 + UseNonReadableTextures: 1 + CacheSize: 100 + ClampDataLevelToMax: 25 + RejectTilesOutsideZoom: {x: 2, y: 25} --- !u!1 &1326738087 GameObject: m_ObjectHideFlags: 0 @@ -2128,6 +2455,41 @@ MeshFilter: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1815118998} m_Mesh: {fileID: 10210, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &2054936213 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054936215} + m_Layer: 0 + m_Name: MapUtility + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2054936215 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054936213} + serializedVersion: 2 + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0.4, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 525849129} + - {fileID: 220147485} + - {fileID: 885811053} + - {fileID: 8694687542404769455} + m_Father: {fileID: 1316629400} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &2117726497 GameObject: m_ObjectHideFlags: 0 @@ -3217,86 +3579,77 @@ SkinnedMeshRenderer: m_Center: {x: -0.0038077123, y: 0.003856523, z: 0.0011322275} m_Extent: {x: 0.049188334, y: 0.023567023, z: 0.03146985} m_DirtyAABB: 0 ---- !u!1001 &7512784582910460290 -PrefabInstance: +--- !u!114 &1377339556045437849 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2fa13573d971f40649081b02ffc8366c, type: 3} + m_Name: + m_EditorClassIdentifier: + TileMaterials: + - {fileID: -876546973899608171, guid: e44ed2d518a7c4a20bd9799360a9e7ed, type: 3} + CacheSize: 25 +--- !u!1 &6450514213955001649 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8694687542404769455} + - component: {fileID: 8067237297740704311} + m_Layer: 0 + m_Name: TileProvider + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &8067237297740704311 +MonoBehaviour: m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6450514213955001649} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 63b76f2761e454a4687f320fa7524dde, type: 3} + m_Name: + m_EditorClassIdentifier: + TileProvider: + Transform: {fileID: 4601729577895354} + TilesToWest: 2 + TilesToEast: 2 + TilesToNorth: 2 + TilesToSouth: 2 +--- !u!4 &8694687542404769455 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6450514213955001649} serializedVersion: 2 - m_Modification: - serializedVersion: 3 - m_TransformParent: {fileID: 0} - m_Modifications: - - target: {fileID: 555719919710579637, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: TileProvider.Transform - value: - objectReference: {fileID: 4601729577895354} - - target: {fileID: 7512784584146348565, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: _tileCreatorBehaviour - value: - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348567, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_Name - value: Map - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348568, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: vectorModuleSettings.DataSettings.ClampDataLevelToMax - value: 15 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_RootOrder - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalPosition.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalPosition.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalPosition.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalRotation.w - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalRotation.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalRotation.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalRotation.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalEulerAnglesHint.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalEulerAnglesHint.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalEulerAnglesHint.z - value: 0 - objectReference: {fileID: 0} - m_RemovedComponents: - - {fileID: 7512784584146348569, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - m_RemovedGameObjects: [] - m_AddedGameObjects: [] - m_AddedComponents: [] - m_SourcePrefab: {fileID: 100100000, guid: 5cfb21e958bce467e966040b1be49240, type: 3} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2054936215} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 m_Roots: - {fileID: 745711776} - - {fileID: 7512784582910460290} + - {fileID: 1316629400} - {fileID: 166616397} - {fileID: 1326738089} - {fileID: 4601729577895354} diff --git a/Samples~/LocationBasedGame/LocationGameWithPois.unity b/Samples~/LocationBasedGame/LocationGameWithPois.unity index 897b2787a..4d90b7bb0 100644 --- a/Samples~/LocationBasedGame/LocationGameWithPois.unity +++ b/Samples~/LocationBasedGame/LocationGameWithPois.unity @@ -490,7 +490,7 @@ GameObject: m_Component: - component: {fileID: 525849129} - component: {fileID: 525849128} - - component: {fileID: 525849127} + - component: {fileID: 525849131} - component: {fileID: 525849130} m_Layer: 0 m_Name: LocationModule @@ -499,26 +499,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &525849127 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 525849124} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 0a38712e93231418a84665190b8473d0, type: 3} - m_Name: - m_EditorClassIdentifier: - _desiredAccuracyInMeters: 5 - _updateDistanceInMeters: 5 - _updateTimeInMilliSeconds: 1000 - _userHeadingSmoothing: {fileID: 0} - _deviceOrientationSmoothing: {fileID: 0} - _editorDebuggingOnly: - _mockUnityInputLocation: 0 - _locationLogFile: {fileID: 0} --- !u!114 &525849128 MonoBehaviour: m_ObjectHideFlags: 0 @@ -531,10 +511,18 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: b55f37f9a6f7e44f7bb35e6bc3863847, type: 3} m_Name: m_EditorClassIdentifier: - _deviceLocationProviderUnity: {fileID: 525849127} - _deviceLocationProviderAndroid: {fileID: 0} - _editorLocationProvider: {fileID: 525849130} - _transformLocationProvider: {fileID: 0} + LocationProviderType: 1 + _mapboxLocationProviderSettings: + AccuracyLevel: 0 + Displacement: 0 + MinimumInterval: 0 + MaximumInterval: 0 + Interval: 0 + _unityLocationProviderSettings: + DesiredAccuracyInMeters: 1 + UpdateDistanceInMeters: 0 + UpdateTimeInMilliSeconds: 500 + EditorLatitudeLongitude: 60.1712373,24.9435977 _dontDestroyOnLoad: 0 --- !u!4 &525849129 Transform: @@ -560,14 +548,27 @@ MonoBehaviour: m_GameObject: {fileID: 525849124} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 60712efc3153a4819b0c79437175846d, type: 3} + m_Script: {fileID: 11500000, guid: a5534c9989da84262bc64ffe27afbbb6, type: 3} + m_Name: + m_EditorClassIdentifier: + Map: {fileID: 1316629399} + LocationProviderFactory: {fileID: 525849128} + ShiftMapDistance: 500 +--- !u!114 &525849131 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 137870786121148788eada666e6cd719, type: 3} m_Name: m_EditorClassIdentifier: - _accuracy: 0 - _autoFireEvent: 1 - _updateInterval: 0 - _sendEvent: 0 - _latitudeLongitude: 60.1712373,24.9435977 + _transform: {fileID: 220147485} + _locationProvider: {fileID: 525849128} + _mapCore: {fileID: 1316629399} --- !u!1 &545031483 GameObject: m_ObjectHideFlags: 0 @@ -1609,10 +1610,9 @@ GameObject: m_Component: - component: {fileID: 1316629400} - component: {fileID: 1316629399} + - component: {fileID: 1316629405} - component: {fileID: 1316629404} - component: {fileID: 1316629402} - - component: {fileID: 7512784582834766863} - - component: {fileID: 1377339556045437848} m_Layer: 0 m_Name: Map m_TagString: Untagged @@ -1639,14 +1639,16 @@ MonoBehaviour: _latitudeLongitudeString: 60.1712373,24.9435977 _zoom: 15 UnityContext: + LocationPermissionState: 0 MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 0} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 0} + _tileCreatorBehaviour: {fileID: 1316629405} TileProvider: {fileID: 8067237297740704311} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} - InitializeOnStart: 0 + LocationFactory: {fileID: 525849128} + InitializeOnStart: 1 --- !u!4 &1316629400 Transform: m_ObjectHideFlags: 0 @@ -1707,6 +1709,21 @@ MonoBehaviour: CacheSize: 100 ClampDataLevelToMax: 25 RejectTilesOutsideZoom: {x: 2, y: 25} +--- !u!114 &1316629405 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2fa13573d971f40649081b02ffc8366c, type: 3} + m_Name: + m_EditorClassIdentifier: + TileMaterials: + - {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} + CacheSize: 25 --- !u!1 &1326738087 GameObject: m_ObjectHideFlags: 0 @@ -3578,21 +3595,6 @@ SkinnedMeshRenderer: m_Center: {x: -0.0038077123, y: 0.003856523, z: 0.0011322275} m_Extent: {x: 0.049188334, y: 0.023567023, z: 0.03146985} m_DirtyAABB: 0 ---- !u!114 &1377339556045437848 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1316629397} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 137870786121148788eada666e6cd719, type: 3} - m_Name: - m_EditorClassIdentifier: - _transform: {fileID: 220147485} - _locationProvider: {fileID: 525849128} - _mapCore: {fileID: 1316629399} --- !u!1 &6450514213955001649 GameObject: m_ObjectHideFlags: 0 @@ -3610,22 +3612,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &7512784582834766863 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1316629397} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 46cd4a863e31244f891143a5bab077bc, type: 3} - m_Name: - m_EditorClassIdentifier: - InitializeMap: 1 - ContinueAfterInitialization: 0 - _locationProvider: {fileID: 525849128} - _map: {fileID: 1316629399} --- !u!114 &8067237297740704311 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs b/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs deleted file mode 100644 index 0013af629..000000000 --- a/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Mapbox.BaseModule.Map; -using Mapbox.Example.Scripts.Map; -using Mapbox.LocationModule; -using UnityEngine; - -namespace Mapbox.Example.Scripts.LocationBehaviours -{ - public class SnapMapToLocationProvider : MonoBehaviour - { - public bool InitializeMap = true; - public bool ContinueAfterInitialization = false; - [SerializeField] - private LocationProviderFactory _locationProvider; - - [SerializeField] private MapboxMapBehaviour _map; - private bool _initializeStarted = false; - - private void Start() - { - - if(_locationProvider == null) - Debug.Log("_locationProvider null"); - - if(_locationProvider.DefaultLocationProvider == null) - Debug.Log("DefaultLocationProvider null"); - - if(!enabled || _locationProvider == null || _locationProvider.DefaultLocationProvider == null) - return; - - UnityEngine.Input.location.Start(); - _locationProvider.DefaultLocationProvider.OnLocationUpdated += (s) => - { - if (_map.InitializationStatus == InitializationStatus.WaitingForInitialization && InitializeMap && !_initializeStarted) - { - _initializeStarted = true; - _map.MapInformation.Initialize(s.LatitudeLongitude); - _map.Initialize(); - } - if (ContinueAfterInitialization) - { - _map.MapInformation.SetInformation(s.LatitudeLongitude); - } - - }; - } - } -} diff --git a/Tests/Editor/BaseModule/DataCompressionTests.cs b/Tests/Editor/BaseModule/DataCompressionTests.cs index 2dfa94c6a..904779728 100644 --- a/Tests/Editor/BaseModule/DataCompressionTests.cs +++ b/Tests/Editor/BaseModule/DataCompressionTests.cs @@ -17,6 +17,7 @@ public class DataCompressionTests public void SetUp() { var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } diff --git a/Tests/Editor/BaseModule/FileSourceTests.cs b/Tests/Editor/BaseModule/FileSourceTests.cs index 6cf7e7e0f..1ab33ca81 100644 --- a/Tests/Editor/BaseModule/FileSourceTests.cs +++ b/Tests/Editor/BaseModule/FileSourceTests.cs @@ -15,6 +15,7 @@ public class FileSourceTests public void SetUp() { var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } diff --git a/Tests/Editor/BaseModule/TerrainSourceTests.cs b/Tests/Editor/BaseModule/TerrainSourceTests.cs index a256ef765..00ffdfd1f 100644 --- a/Tests/Editor/BaseModule/TerrainSourceTests.cs +++ b/Tests/Editor/BaseModule/TerrainSourceTests.cs @@ -44,6 +44,7 @@ public void OneTimeSetUp() }; var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Editor/BaseModule/TileJSONTest.cs b/Tests/Editor/BaseModule/TileJSONTest.cs index 597cd7961..ea710a037 100644 --- a/Tests/Editor/BaseModule/TileJSONTest.cs +++ b/Tests/Editor/BaseModule/TileJSONTest.cs @@ -15,6 +15,7 @@ internal class TileJSONTest public void SetUp() { var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); _dataFetcher = new DataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } diff --git a/Tests/Editor/BaseModule/TokenTest.cs b/Tests/Editor/BaseModule/TokenTest.cs index 927a7b4ae..ff1e98a4d 100644 --- a/Tests/Editor/BaseModule/TokenTest.cs +++ b/Tests/Editor/BaseModule/TokenTest.cs @@ -17,6 +17,7 @@ public class TokenTest public void SetUp() { var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); _tokenApi = new MapboxTokenApi(); _configAccessToken = mapboxContext.GetAccessToken(); _configSkuToken = mapboxContext.GetSkuToken; diff --git a/Tests/Editor/LocationModule.meta b/Tests/Editor/LocationModule.meta new file mode 100644 index 000000000..d8976a4e5 --- /dev/null +++ b/Tests/Editor/LocationModule.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3ae875080440846aca4a90a79f1d128a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/LocationDataTests.cs b/Tests/Editor/LocationModule/LocationDataTests.cs new file mode 100644 index 000000000..b04bd0213 --- /dev/null +++ b/Tests/Editor/LocationModule/LocationDataTests.cs @@ -0,0 +1,264 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.LocationModule; +using NUnit.Framework; + +namespace Mapbox.LocationModule.Tests +{ + [TestFixture] + public class LocationDataTests + { + #region Location Tests + + [Test] + public void Location_DefaultConstructor_InitializesWithDefaults() + { + // Act - Location is a struct, so default constructor sets all fields to default values + var location = new Location(); + + // Assert + Assert.AreEqual(0, location.LatitudeLongitude.Latitude); + Assert.AreEqual(0, location.LatitudeLongitude.Longitude); + Assert.AreEqual(0f, location.Accuracy); + Assert.AreEqual(0.0, location.Timestamp); + Assert.IsFalse(location.IsLocationUpdated); + Assert.IsFalse(location.IsLocationServiceEnabled); + } + + [Test] + public void Location_SetLatitudeLongitude_UpdatesValue() + { + // Arrange + var location = new Location(); + var latLng = new LatitudeLongitude(37.7749, -122.4194); + + // Act + location.LatitudeLongitude = latLng; + + // Assert + Assert.AreEqual(37.7749, location.LatitudeLongitude.Latitude, 0.0001); + Assert.AreEqual(-122.4194, location.LatitudeLongitude.Longitude, 0.0001); + } + + [Test] + public void Location_Accuracy_CanBeSet() + { + // Arrange + var location = new Location(); + + // Act + location.Accuracy = 15.5f; + + // Assert + Assert.AreEqual(15.5f, location.Accuracy); + } + + [Test] + public void Location_Timestamp_CanBeSet() + { + // Arrange + var location = new Location(); + double timestamp = 1234567890.0; + + // Act + location.Timestamp = timestamp; + + // Assert + Assert.AreEqual(timestamp, location.Timestamp); + } + + [Test] + public void Location_IsLocationUpdated_DefaultsToFalse() + { + // Arrange & Act + var location = new Location(); + + // Assert + Assert.IsFalse(location.IsLocationUpdated); + } + + [Test] + public void Location_IsLocationServiceEnabled_CanBeSet() + { + // Arrange + var location = new Location(); + + // Act + location.IsLocationServiceEnabled = true; + + // Assert + Assert.IsTrue(location.IsLocationServiceEnabled); + } + + [Test] + public void Location_UserHeading_CanBeSet() + { + // Arrange + var location = new Location(); + + // Act + location.UserHeading = 45.5f; + + // Assert + Assert.AreEqual(45.5f, location.UserHeading); + } + + [Test] + public void Location_DeviceOrientation_CanBeSet() + { + // Arrange + var location = new Location(); + + // Act + location.DeviceOrientation = 90.0f; + + // Assert + Assert.AreEqual(90.0f, location.DeviceOrientation); + } + + #endregion + + #region LatitudeLongitude Tests + + [Test] + public void LatitudeLongitude_Constructor_SetsValues() + { + // Act + var latLng = new LatitudeLongitude(37.7749, -122.4194); + + // Assert + Assert.AreEqual(37.7749, latLng.Latitude, 0.0001); + Assert.AreEqual(-122.4194, latLng.Longitude, 0.0001); + } + + [Test] + public void LatitudeLongitude_Equals_SameValues_ReturnsTrue() + { + // Arrange + var latLng1 = new LatitudeLongitude(37.7749, -122.4194); + var latLng2 = new LatitudeLongitude(37.7749, -122.4194); + + // Act & Assert + Assert.That(latLng1.Equals(latLng2), Is.True); + } + + [Test] + public void LatitudeLongitude_Equals_DifferentLatitude_ReturnsFalse() + { + // Arrange + var latLng1 = new LatitudeLongitude(37.7749, -122.4194); + var latLng2 = new LatitudeLongitude(37.7750, -122.4194); + + // Act & Assert + Assert.That(latLng1.Equals(latLng2), Is.False); + } + + [Test] + public void LatitudeLongitude_Equals_DifferentLongitude_ReturnsFalse() + { + // Arrange + var latLng1 = new LatitudeLongitude(37.7749, -122.4194); + var latLng2 = new LatitudeLongitude(37.7749, -122.4195); + + // Act & Assert + Assert.That(latLng1.Equals(latLng2), Is.False); + } + + [Test] + public void LatitudeLongitude_ZeroZero_IsValid() + { + // Act - 0,0 is Gulf of Guinea, a valid location + var latLng = new LatitudeLongitude(0, 0); + + // Assert + Assert.AreEqual(0, latLng.Latitude, 0.0001); + Assert.AreEqual(0, latLng.Longitude, 0.0001); + Assert.That(latLng.IsValid(), Is.True); + } + + [Test] + public void LatitudeLongitude_ExtremeValues_AreValid() + { + // Act - Test near poles and date line + var northPole = new LatitudeLongitude(90, 0); + var southPole = new LatitudeLongitude(-90, 0); + var dateLineWest = new LatitudeLongitude(0, -180); + var dateLineEast = new LatitudeLongitude(0, 180); + + // Assert + Assert.AreEqual(90, northPole.Latitude, 0.0001); + Assert.AreEqual(-90, southPole.Latitude, 0.0001); + Assert.AreEqual(-180, dateLineWest.Longitude, 0.0001); + Assert.AreEqual(180, dateLineEast.Longitude, 0.0001); + + Assert.That(northPole.IsValid(), Is.True); + Assert.That(southPole.IsValid(), Is.True); + Assert.That(dateLineWest.IsValid(), Is.True); + Assert.That(dateLineEast.IsValid(), Is.True); + } + + #endregion + + #region StaticLocationProvider Tests + + [Test] + public void StaticLocationProvider_StringConstructor_ParsesValidCoordinates() + { + // Arrange & Act + var provider = new StaticLocationProvider("37.7749,-122.4194"); + + // Assert + Assert.AreEqual(37.7749, provider.CurrentLocation.LatitudeLongitude.Latitude, 0.001); + Assert.AreEqual(-122.4194, provider.CurrentLocation.LatitudeLongitude.Longitude, 0.001); + } + + [Test] + public void StaticLocationProvider_LatLngConstructor_SetsLocation() + { + // Arrange & Act + var latLng = new LatitudeLongitude(37.7749, -122.4194); + var provider = new StaticLocationProvider(latLng); + + // Assert + Assert.AreEqual(37.7749, provider.CurrentLocation.LatitudeLongitude.Latitude, 0.001); + Assert.AreEqual(-122.4194, provider.CurrentLocation.LatitudeLongitude.Longitude, 0.001); + } + + [Test] + public void StaticLocationProvider_AfterSendLocationEvent_IsEnabled() + { + // Arrange + var provider = new StaticLocationProvider("37.7749,-122.4194"); + + // Act + provider.SendLocationEvent(); + + // Assert + Assert.That(provider.CurrentLocation.IsLocationServiceEnabled, Is.True); + } + + #endregion + + #region ILocationProvider Tests + + [Test] + public void AbstractLocationProvider_OnLocationUpdated_EventCanBeSubscribed() + { + // Arrange + var provider = new StaticLocationProvider("37.7749,-122.4194"); + bool eventRaised = false; + + provider.OnLocationUpdated += (location) => + { + eventRaised = true; + }; + + // Act - StaticLocationProvider requires SendLocationEvent() to trigger the event + provider.SendLocationEvent(); + + // Assert + Assert.That(eventRaised, Is.True); + } + + #endregion + } +} diff --git a/Tests/Editor/LocationModule/LocationDataTests.cs.meta b/Tests/Editor/LocationModule/LocationDataTests.cs.meta new file mode 100644 index 000000000..daecf38dd --- /dev/null +++ b/Tests/Editor/LocationModule/LocationDataTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0944954b179ed4f6cb5102f72228f3b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/LocationPermissionTests.cs b/Tests/Editor/LocationModule/LocationPermissionTests.cs new file mode 100644 index 000000000..92c350052 --- /dev/null +++ b/Tests/Editor/LocationModule/LocationPermissionTests.cs @@ -0,0 +1,78 @@ +using Mapbox.BaseModule.Unity; +using NUnit.Framework; + +namespace Mapbox.LocationModule.Tests +{ + [TestFixture] + public class LocationPermissionTests + { + #region LocationPermissionState Tests + + [Test] + public void LocationPermissionState_HasExpectedValues() + { + // Assert - Verify enum has all expected values + Assert.IsTrue(System.Enum.IsDefined(typeof(LocationPermissionState), LocationPermissionState.Waiting)); + Assert.IsTrue(System.Enum.IsDefined(typeof(LocationPermissionState), LocationPermissionState.Granted)); + Assert.IsTrue(System.Enum.IsDefined(typeof(LocationPermissionState), LocationPermissionState.Denied)); + Assert.IsTrue(System.Enum.IsDefined(typeof(LocationPermissionState), LocationPermissionState.DeniedPermanently)); + } + + [Test] + public void LocationPermissionState_DeniedAndDeniedPermanentlyAreDifferent() + { + // Assert + Assert.AreNotEqual( + LocationPermissionState.Denied, + LocationPermissionState.DeniedPermanently, + "Denied and DeniedPermanently should be distinct states"); + } + + #endregion + + #region UnityContext Tests + + [Test] + public void UnityContext_DefaultState_IsWaiting() + { + // Arrange & Act + var context = new UnityContext(); + + // Assert + Assert.AreEqual(LocationPermissionState.Waiting, context.LocationPermissionState); + } + + [Test] + public void UnityContext_MapRootCanBeSet() + { + // Arrange + var context = new UnityContext(); + var gameObject = new UnityEngine.GameObject("TestMapRoot"); + + // Act + context.MapRoot = gameObject.transform; + + // Assert + Assert.AreEqual(gameObject.transform, context.MapRoot); + + // Cleanup + UnityEngine.Object.DestroyImmediate(gameObject); + } + + #endregion + + #region LocationPermissionHandler Tests + + [Test] + public void LocationPermissionHandler_InitialState_IsWaiting() + { + // Arrange & Act + var handler = new LocationPermissionHandler(); + + // Assert + Assert.AreEqual(LocationPermissionState.Waiting, handler.State); + } + + #endregion + } +} diff --git a/Tests/Editor/LocationModule/LocationPermissionTests.cs.meta b/Tests/Editor/LocationModule/LocationPermissionTests.cs.meta new file mode 100644 index 000000000..d19db1131 --- /dev/null +++ b/Tests/Editor/LocationModule/LocationPermissionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e29407a534d9455bb5ba68d816f944d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef b/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef new file mode 100644 index 000000000..57eb210df --- /dev/null +++ b/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef @@ -0,0 +1,21 @@ +{ + "name": "Mapbox.LocationModule.Tests", + "rootNamespace": "Mapbox.LocationModule.Tests", + "references": [ + "MapboxBaseModule", + "MapboxLocationModule" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef.meta b/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef.meta new file mode 100644 index 000000000..77742fc8d --- /dev/null +++ b/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3e6a21ab0cdd24a259d184a293c05435 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs b/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs new file mode 100644 index 000000000..c8c72e7ec --- /dev/null +++ b/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs @@ -0,0 +1,195 @@ +using Mapbox.LocationModule.MapboxLocation; +using Mapbox.LocationModule; +using NUnit.Framework; +using System; + +namespace Mapbox.LocationModule.Tests +{ + [TestFixture] + public class MapboxLocationProviderTests + { + private MapboxLocationSettings _settings; + + [SetUp] + public void SetUp() + { + _settings = new MapboxLocationSettings + { + AccuracyLevel = MapboxLocationAccuracyLevel.High, + Displacement = 10f, + Interval = 1000, + MinimumInterval = 500, + MaximumInterval = 5000 + }; + } + + #region Constructor Tests + + [Test] + public void Constructor_WithValidSettings_CreatesProvider() + { + // Act + var provider = new MapboxLocationProvider(_settings); + + // Assert + Assert.IsNotNull(provider); + } + + [Test] + public void Constructor_InEditor_DoesNotThrow() + { + // In editor, _mapboxDeviceLocation will be null since platform #if guards prevent instantiation + // The null guard added in our fix should prevent crashes + // Act & Assert + Assert.DoesNotThrow(() => new MapboxLocationProvider(_settings)); + } + + #endregion + + #region Update Tests + + [Test] + public void Update_WithNullDeviceLocation_DoesNotThrow() + { + // Arrange + var provider = new MapboxLocationProvider(_settings); + // In editor, _mapboxDeviceLocation is null + + // Act & Assert + Assert.DoesNotThrow(() => provider.Update()); + } + + #endregion + + #region OnDestroy Tests + + [Test] + public void OnDestroy_WithNullDeviceLocation_DoesNotThrow() + { + // Arrange + var provider = new MapboxLocationProvider(_settings); + // In editor, _mapboxDeviceLocation is null + + // Act & Assert + Assert.DoesNotThrow(() => provider.OnDestroy()); + } + + #endregion + + #region CurrentLocation Tests + + [Test] + public void CurrentLocation_InitialState_ReturnsDefaultLocation() + { + // Arrange + var provider = new MapboxLocationProvider(_settings); + + // Act + var location = provider.CurrentLocation; + + // Assert + Assert.AreEqual(0, location.LatitudeLongitude.Latitude); + Assert.AreEqual(0, location.LatitudeLongitude.Longitude); + } + + #endregion + } + + [TestFixture] + public class MapboxLocationSettingsTests + { + #region Settings Tests + + [Test] + public void MapboxLocationSettings_DefaultValues() + { + // Act + var settings = new MapboxLocationSettings(); + + // Assert + Assert.IsNotNull(settings); + } + + [Test] + public void MapboxLocationSettings_AccuracyLevel_CanBeSet() + { + // Arrange + var settings = new MapboxLocationSettings(); + + // Act + settings.AccuracyLevel = MapboxLocationAccuracyLevel.Highest; + + // Assert + Assert.AreEqual(MapboxLocationAccuracyLevel.Highest, settings.AccuracyLevel); + } + + [Test] + public void MapboxLocationSettings_Displacement_CanBeSet() + { + // Arrange + var settings = new MapboxLocationSettings(); + + // Act + settings.Displacement = 25.5f; + + // Assert + Assert.AreEqual(25.5f, settings.Displacement); + } + + #endregion + } + + [TestFixture] + public class AccuracyAuthorizationTests + { + #region Enum Tests + + [Test] + public void AccuracyAuthorization_HasExpectedValues() + { + // Assert - Verify enum has expected values + Assert.That(Enum.IsDefined(typeof(AccuracyAuthorization), AccuracyAuthorization.None), Is.True); + Assert.That(Enum.IsDefined(typeof(AccuracyAuthorization), AccuracyAuthorization.Exact), Is.True); + Assert.That(Enum.IsDefined(typeof(AccuracyAuthorization), AccuracyAuthorization.Inexact), Is.True); + } + + #endregion + } + + [TestFixture] + public class MapboxLocationServiceStatusTests + { + #region Enum Tests + + [Test] + public void MapboxLocationServiceStatus_HasExpectedValues() + { + // Assert - Verify enum has expected status values + Assert.That(Enum.IsDefined(typeof(MapboxLocationServiceStatus), MapboxLocationServiceStatus.Denied), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationServiceStatus), MapboxLocationServiceStatus.Granted), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationServiceStatus), MapboxLocationServiceStatus.Foreground), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationServiceStatus), MapboxLocationServiceStatus.Background), Is.True); + } + + #endregion + } + + [TestFixture] + public class MapboxLocationAccuracyLevelTests + { + #region Enum Tests + + [Test] + public void MapboxLocationAccuracyLevel_HasExpectedValues() + { + // Assert - Verify enum has all accuracy levels + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.Passive), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.Low), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.Medium), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.High), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.Highest), Is.True); + } + + #endregion + } +} diff --git a/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs.meta b/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs.meta new file mode 100644 index 000000000..72d7dffcc --- /dev/null +++ b/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b75e9cdda7fd94e2fb2d85335fb3fd15 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/TestLocationServiceMock.cs b/Tests/Editor/LocationModule/TestLocationServiceMock.cs new file mode 100644 index 000000000..741483c24 --- /dev/null +++ b/Tests/Editor/LocationModule/TestLocationServiceMock.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Mapbox.LocationModule.UnityLocationWrappers; +using UnityEngine; + +namespace Mapbox.LocationModule.Tests +{ + /// + /// Simple mock location service for unit tests. + /// Allows queueing specific location data to be returned on demand. + /// + public class TestLocationServiceMock : IMapboxLocationService + { + private readonly Queue _locations = new Queue(); + private IMapboxLocationInfo _currentLocation; + private LocationServiceStatus _status = LocationServiceStatus.Stopped; + private bool _isEnabledByUser = true; + + public bool isEnabledByUser => _isEnabledByUser; + public LocationServiceStatus status => _status; + public IMapboxLocationInfo lastData => _currentLocation; + + public void SetEnabledByUser(bool enabled) + { + _isEnabledByUser = enabled; + } + + public void SetStatus(LocationServiceStatus newStatus) + { + _status = newStatus; + } + + public void QueueLocation(IMapboxLocationInfo location) + { + _locations.Enqueue(location); + } + + public void QueueLocation(float latitude, float longitude, float accuracy = 10f, double timestamp = 0) + { + var location = new TestLocationInfo + { + latitude = latitude, + longitude = longitude, + accuracy = accuracy, + timestamp = timestamp > 0 ? timestamp : System.DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }; + _locations.Enqueue(location); + } + + public void Start(float desiredAccuracyInMeters, float updateDistanceInMeters) + { + _status = LocationServiceStatus.Running; + AdvanceToNextLocation(); + } + + public void Stop() + { + _status = LocationServiceStatus.Stopped; + } + + /// + /// Simulates location service update by advancing to next queued location. + /// + public void AdvanceToNextLocation() + { + if (_locations.Count > 0) + { + _currentLocation = _locations.Dequeue(); + } + } + } + + /// + /// Simple test implementation of IMapboxLocationInfo. + /// + public class TestLocationInfo : IMapboxLocationInfo + { + public float latitude { get; set; } + public float longitude { get; set; } + public float altitude { get; set; } + public float horizontalAccuracy { get; set; } + public float verticalAccuracy { get; set; } + public float accuracy + { + get => horizontalAccuracy; + set => horizontalAccuracy = value; + } + public double timestamp { get; set; } + } +} diff --git a/Tests/Editor/LocationModule/TestLocationServiceMock.cs.meta b/Tests/Editor/LocationModule/TestLocationServiceMock.cs.meta new file mode 100644 index 000000000..32a9f5b3e --- /dev/null +++ b/Tests/Editor/LocationModule/TestLocationServiceMock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78ad45c17decb455799434c9e672ab23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/UnityLocationProviderTests.cs b/Tests/Editor/LocationModule/UnityLocationProviderTests.cs new file mode 100644 index 000000000..c61b78753 --- /dev/null +++ b/Tests/Editor/LocationModule/UnityLocationProviderTests.cs @@ -0,0 +1,88 @@ +using Mapbox.LocationModule; +using Mapbox.LocationModule.AngleSmoothing; +using NUnit.Framework; + +namespace Mapbox.LocationModule.Tests +{ + /// + /// NOTE: UnityLocationProvider requires Unity's runtime (MonoBehaviour, coroutines) to function. + /// These EditMode tests are limited to testing settings and data structures. + /// Full integration tests for UnityLocationProvider would need to be PlayMode tests. + /// + [TestFixture] + public class UnityLocationProviderTests + { + #region Settings Tests + + [Test] + public void UnityLocationProviderSettings_DefaultValues() + { + // Act + var settings = new UnityLocationProviderSettings(); + + // Assert + Assert.IsNotNull(settings); + Assert.AreEqual(1.0f, settings.DesiredAccuracyInMeters); + Assert.AreEqual(0.0f, settings.UpdateDistanceInMeters); + Assert.AreEqual(500, settings.UpdateTimeInMilliSeconds); + } + + [Test] + public void UnityLocationProviderSettings_CanSetDesiredAccuracy() + { + // Arrange + var settings = new UnityLocationProviderSettings(); + + // Act + settings.DesiredAccuracyInMeters = 25.5f; + + // Assert + Assert.AreEqual(25.5f, settings.DesiredAccuracyInMeters); + } + + [Test] + public void UnityLocationProviderSettings_CanSetUpdateDistance() + { + // Arrange + var settings = new UnityLocationProviderSettings(); + + // Act + settings.UpdateDistanceInMeters = 10.0f; + + // Assert + Assert.AreEqual(10.0f, settings.UpdateDistanceInMeters); + } + + [Test] + public void UnityLocationProviderSettings_CanSetUpdateTime() + { + // Arrange + var settings = new UnityLocationProviderSettings(); + + // Act + settings.UpdateTimeInMilliSeconds = 1000; + + // Assert + Assert.AreEqual(1000, settings.UpdateTimeInMilliSeconds); + } + + [Test] + public void UnityLocationProviderSettings_CanSetSmoothingStrategies() + { + // Arrange + var settings = new UnityLocationProviderSettings(); + var headingSmoothing = new AngleSmoothingNoOp(); + var orientationSmoothing = new AngleSmoothingNoOp(); + + // Act + settings.UserHeadingSmoothing = headingSmoothing; + settings.DeviceOrientationSmoothing = orientationSmoothing; + + // Assert + Assert.AreEqual(headingSmoothing, settings.UserHeadingSmoothing); + Assert.AreEqual(orientationSmoothing, settings.DeviceOrientationSmoothing); + } + + #endregion + } +} diff --git a/Tests/Editor/LocationModule/UnityLocationProviderTests.cs.meta b/Tests/Editor/LocationModule/UnityLocationProviderTests.cs.meta new file mode 100644 index 000000000..1c44bb83f --- /dev/null +++ b/Tests/Editor/LocationModule/UnityLocationProviderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 01bc1970791b14611b4142d513dcc95a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/BaseModule/ImageSourceTests.cs b/Tests/Runtime/BaseModule/ImageSourceTests.cs index 7e7662bce..751bf92c4 100644 --- a/Tests/Runtime/BaseModule/ImageSourceTests.cs +++ b/Tests/Runtime/BaseModule/ImageSourceTests.cs @@ -46,6 +46,7 @@ public void OneTimeSetUp() }; var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Runtime/BaseModule/LocationGamePrefabTests.cs b/Tests/Runtime/BaseModule/LocationGamePrefabTests.cs index 9897a5903..81d9dd628 100644 --- a/Tests/Runtime/BaseModule/LocationGamePrefabTests.cs +++ b/Tests/Runtime/BaseModule/LocationGamePrefabTests.cs @@ -64,7 +64,7 @@ void OnMapCoreInitialized(MapboxMap mapboxMap) _mapCore.Initialized += OnMapCoreInitialized; - _mapCore.Initialize(); + yield return _mapCore.Initialize(); while(_mapCore.InitializationStatus < InitializationStatus.ReadyForUpdates) yield return null; diff --git a/Tests/Runtime/BaseModule/MapTest.cs b/Tests/Runtime/BaseModule/MapTest.cs index 6d1b84b38..69e922352 100644 --- a/Tests/Runtime/BaseModule/MapTest.cs +++ b/Tests/Runtime/BaseModule/MapTest.cs @@ -55,6 +55,7 @@ private void LoadMap(string latlng, bool useSqlite = true, bool useFileCache = t mapInfo.SetInformation(null, 16, 45, null, 1000); mapInfo.Initialize(); var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); var unityContext = new UnityContext(); unityContext.Initialize(); diff --git a/Tests/Runtime/BaseModule/MapViewLoadTests.cs b/Tests/Runtime/BaseModule/MapViewLoadTests.cs index 64c3d8254..c320600f1 100644 --- a/Tests/Runtime/BaseModule/MapViewLoadTests.cs +++ b/Tests/Runtime/BaseModule/MapViewLoadTests.cs @@ -48,6 +48,7 @@ public IEnumerator OneTimeSetUp() mapInfo.SetInformation(null, 16, 45, null, 1000); mapInfo.Initialize(); var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); var unityContext = new UnityContext(); unityContext.Initialize(); diff --git a/Tests/Runtime/BaseModule/MapboxMapTests.cs b/Tests/Runtime/BaseModule/MapboxMapTests.cs index cddecd05e..b3fb38015 100644 --- a/Tests/Runtime/BaseModule/MapboxMapTests.cs +++ b/Tests/Runtime/BaseModule/MapboxMapTests.cs @@ -39,6 +39,7 @@ public void OneTimeSetUp() mapInfo.SetInformation(null, 16, 45, null, 1000); mapInfo.Initialize(); var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); var unityContext = new UnityContext(); unityContext.Initialize(); @@ -209,6 +210,7 @@ public class DataFetcherTests public void OneTimeSetup() { var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); _datafetcher = new LoggingDataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); _tilesetId = vectorTileset.Id; diff --git a/Tests/Runtime/BaseModule/PbfSourceTests.cs b/Tests/Runtime/BaseModule/PbfSourceTests.cs index 908bd89fc..18ab8f614 100644 --- a/Tests/Runtime/BaseModule/PbfSourceTests.cs +++ b/Tests/Runtime/BaseModule/PbfSourceTests.cs @@ -41,6 +41,7 @@ public void OneTimeSetUp() }; var mapboxContext = new MapboxContext(); + mapboxContext.LoadConfigurationWithoutValidation(); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Runtime/BaseModule/RasterDataTests.cs b/Tests/Runtime/BaseModule/RasterDataTests.cs index e1f3dfe67..fb860dec2 100644 --- a/Tests/Runtime/BaseModule/RasterDataTests.cs +++ b/Tests/Runtime/BaseModule/RasterDataTests.cs @@ -37,6 +37,7 @@ public void OneTimeSetup() { _taskManager = new MockTaskManager(); _mapboxContext = new MapboxContext(); + _mapboxContext.LoadConfigurationWithoutValidation(); _dataFetchingManager = new DataFetchingManager(_mapboxContext.GetAccessToken(), _mapboxContext.GetSkuToken); _unityContext = new UnityContext(); _unityContext.Initialize(_taskManager); @@ -68,11 +69,13 @@ public void SetUp() [UnityTest, Order(5)] public IEnumerator RequestTileListNoCache() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapboxContext1 = new MapboxContext(); + mapboxContext1.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext1, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, null)); - + var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); - + Runnable.EnableRunnableInEditor(); List loadedTiles = null; var coroutine = Runnable.Instance.StartCoroutine(rasterSource.LoadTilesCoroutine(_testTileHashset, (data) => @@ -80,15 +83,17 @@ public IEnumerator RequestTileListNoCache() loadedTiles = data; })); yield return coroutine; - + Assert.NotNull(loadedTiles); Assert.IsNotEmpty(loadedTiles); } - + [UnityTest, Order(4)] public IEnumerator RequestTileListCheckWithFileSqlite() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapboxContext2 = new MapboxContext(); + mapboxContext2.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext2, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); @@ -122,11 +127,13 @@ public IEnumerator RequestTileListCheckWithFileSqlite() [UnityTest, Order(3)] public IEnumerator RequestWithAllCaches() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapboxContext3 = new MapboxContext(); + mapboxContext3.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext3, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); - + var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); - + RasterData resultData = null; var coroutineId = Runnable.Run(rasterSource.LoadTileCoroutine(_tileId.Canonical, (data) => { @@ -137,21 +144,23 @@ public IEnumerator RequestWithAllCaches() { yield return null; } - + Assert.NotNull(resultData.Texture); Assert.AreEqual(resultData.CacheType, CacheType.NoCache); var fileCount = _fileCache.GetFileList().Count; Assert.AreEqual(1, fileCount, "File Cache should have one tile, instead it has " + fileCount); - + //adding same tile as second test so sqlite tile count should still be one var sqliteTileCount = _sqliteCache.GetAllTiles().Count; Assert.AreEqual(1, sqliteTileCount, "Sqlite should have one tile, instead it has " + sqliteTileCount); } - + [UnityTest, Order(2)] public IEnumerator RequestNoFileYesSqliteCache() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapboxContext4 = new MapboxContext(); + mapboxContext4.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext4, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, _sqliteCache)); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); @@ -176,7 +185,9 @@ public IEnumerator RequestNoFileYesSqliteCache() public IEnumerator RequestNoFileNoSqliteCaches() { var cacheManager = new MapboxCacheManager(_unityContext, new MemoryCache(), null, null); - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, cacheManager); + var mapboxContext5 = new MapboxContext(); + mapboxContext5.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext5, null, cacheManager); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); diff --git a/Tests/Runtime/BaseModule/SceneTests.cs b/Tests/Runtime/BaseModule/SceneTests.cs index 7dfbc86c9..7a8c71e89 100644 --- a/Tests/Runtime/BaseModule/SceneTests.cs +++ b/Tests/Runtime/BaseModule/SceneTests.cs @@ -187,7 +187,7 @@ public IEnumerator ZoomInTest([ValueSource("fileCache")] bool fileCache, [ValueS { mapCore.Initialized += map => { _map = map; }; mapCore.MapInformation.SetInformation(null, 8); - mapCore.Initialize(); + yield return mapCore.Initialize(); } else { diff --git a/Tests/Runtime/BaseModule/VectorDataTests.cs b/Tests/Runtime/BaseModule/VectorDataTests.cs index 977bb4126..d879bf710 100644 --- a/Tests/Runtime/BaseModule/VectorDataTests.cs +++ b/Tests/Runtime/BaseModule/VectorDataTests.cs @@ -35,6 +35,7 @@ public void OneTimeSetup() { _taskManager = new MockTaskManager(); _mapboxContext = new MapboxContext(); + _mapboxContext.LoadConfigurationWithoutValidation(); _dataFetchingManager = new DataFetchingManager(_mapboxContext.GetAccessToken(), _mapboxContext.GetSkuToken); _unityContext = new UnityContext(); _unityContext.Initialize(_taskManager); @@ -66,12 +67,14 @@ public void SetUp() [UnityTest, Order(4)] public IEnumerator RequestTileList() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapboxContext1 = new MapboxContext(); + mapboxContext1.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext1, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); - + var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); - + List loadedTiles = null; var coroutine = Runnable.Instance.StartCoroutine(vectorSource.LoadTilesCoroutine(_testTileHashset, (data) => { @@ -92,11 +95,13 @@ public IEnumerator RequestTileList() [UnityTest, Order(3)] public IEnumerator RequestWithAllCaches() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapboxContext2 = new MapboxContext(); + mapboxContext2.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext2, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); - + VectorData vectorData = null; var coroutineId = Runnable.Instance.StartCoroutine(vectorSource.LoadTileCoroutine(_tileId.Canonical, (data) => { @@ -112,7 +117,9 @@ public IEnumerator RequestWithAllCaches() [UnityTest, Order(2)] public IEnumerator RequestNoFileYesSqliteCache() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapboxContext3 = new MapboxContext(); + mapboxContext3.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext3, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, _sqliteCache)); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); @@ -137,7 +144,9 @@ public IEnumerator RequestNoFileYesSqliteCache() public IEnumerator RequestNoFileNoSqliteCaches() { var cacheManager = new MapboxCacheManager(_unityContext, new MemoryCache(), null, null); - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, cacheManager); + var mapboxContext4 = new MapboxContext(); + mapboxContext4.LoadConfigurationWithoutValidation(); + var mapService = new MapUnityService(_unityContext, mapboxContext4, null, cacheManager); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); From f27ea59a3d099b29cbc75238dbc0264d4d3a2d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Wed, 11 Mar 2026 12:03:39 +0300 Subject: [PATCH 09/54] Fix tests (#1982) * fix tests * simplify mapbox config load procedure * fix token config in tests --- Editor/MapboxConfigurationWindow.cs | 3 +- .../Mapbox/BaseModule/Map/MapboxContext.cs | 50 ++++----- .../Editor/BaseModule/DataCompressionTests.cs | 6 +- Tests/Editor/BaseModule/FileSourceTests.cs | 6 +- Tests/Editor/BaseModule/TerrainSourceTests.cs | 10 +- Tests/Editor/BaseModule/TileJSONTest.cs | 6 +- Tests/Editor/BaseModule/TokenTest.cs | 6 +- Tests/Runtime/BaseModule/ImageSourceTests.cs | 14 ++- Tests/Runtime/BaseModule/MapTest.cs | 18 +-- Tests/Runtime/BaseModule/MapViewLoadTests.cs | 4 +- Tests/Runtime/BaseModule/MapboxMapTests.cs | 103 +++++++++--------- Tests/Runtime/BaseModule/PbfSourceTests.cs | 10 +- Tests/Runtime/BaseModule/RasterDataTests.cs | 30 ++--- Tests/Runtime/BaseModule/VectorDataTests.cs | 26 ++--- 14 files changed, 149 insertions(+), 143 deletions(-) diff --git a/Editor/MapboxConfigurationWindow.cs b/Editor/MapboxConfigurationWindow.cs index b197c0e1e..2308d3b4a 100644 --- a/Editor/MapboxConfigurationWindow.cs +++ b/Editor/MapboxConfigurationWindow.cs @@ -105,9 +105,8 @@ private static void WriteConfigFile(MapboxConfiguration config, string path) static void OpenWindow() { _mapboxContext = new MapboxContext(); - _mapboxContext.LoadConfigurationWithoutValidation(); + _mapboxContext.LoadConfigurationCoroutine(false).MoveNext(); EditorApplication.delayCall -= OpenWindow; - //instantiate the config window instance = GetWindow(typeof(MapboxConfigurationWindow)) as MapboxConfigurationWindow; instance.minSize = new Vector2(800, 180); instance.titleContent = new GUIContent("Mapbox Setup"); diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs b/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs index c63246993..bfaaf585d 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs @@ -25,15 +25,6 @@ public IEnumerator Initialize() yield return LoadConfigurationCoroutine(); } - /// - /// Load configuration without token validation. - /// For editor tooling only — not for runtime use. - /// - public void LoadConfigurationWithoutValidation() - { - Configuration = LoadAndParseConfig(); - } - public string GetAccessToken() { return Configuration.AccessToken; @@ -83,28 +74,37 @@ private void HandleTokenResponse(MapboxConfiguration config, MapboxToken respons private const float TokenValidationTimeoutSeconds = 10f; - private IEnumerator LoadConfigurationCoroutine() + public IEnumerator LoadConfigurationCoroutine(bool validateToken = true) { var config = LoadAndParseConfig(); - var tokenValidator = new MapboxTokenApi(); - var configLoaded = false; - tokenValidator.Retrieve(config.GetMapsSkuToken, config.AccessToken, (response) => - { - HandleTokenResponse(config, response); - configLoaded = true; - }); - var elapsed = 0f; - while (!configLoaded) + if (validateToken) { - elapsed += Time.deltaTime; - if (elapsed >= TokenValidationTimeoutSeconds) + var tokenValidator = new MapboxTokenApi(); + var configLoaded = false; + tokenValidator.Retrieve(config.GetMapsSkuToken, config.AccessToken, (response) => { - Debug.LogError("Token validation timed out. Proceeding with unvalidated configuration."); - Configuration = config; - break; + HandleTokenResponse(config, response); + configLoaded = true; + }); + + var elapsed = 0f; + while (!configLoaded) + { + elapsed += Time.deltaTime; + if (elapsed >= TokenValidationTimeoutSeconds) + { + Debug.LogError("Token validation timed out. Proceeding with unvalidated configuration."); + Configuration = config; + break; + } + + yield return null; } - yield return null; + } + else + { + Configuration = config; } } diff --git a/Tests/Editor/BaseModule/DataCompressionTests.cs b/Tests/Editor/BaseModule/DataCompressionTests.cs index 904779728..031f66eea 100644 --- a/Tests/Editor/BaseModule/DataCompressionTests.cs +++ b/Tests/Editor/BaseModule/DataCompressionTests.cs @@ -13,11 +13,11 @@ public class DataCompressionTests { private ResilientWebRequestFileSource _fs; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } diff --git a/Tests/Editor/BaseModule/FileSourceTests.cs b/Tests/Editor/BaseModule/FileSourceTests.cs index 1ab33ca81..ae3c085e3 100644 --- a/Tests/Editor/BaseModule/FileSourceTests.cs +++ b/Tests/Editor/BaseModule/FileSourceTests.cs @@ -11,11 +11,11 @@ public class FileSourceTests { private ResilientWebRequestFileSource _fs; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } diff --git a/Tests/Editor/BaseModule/TerrainSourceTests.cs b/Tests/Editor/BaseModule/TerrainSourceTests.cs index 00ffdfd1f..aad66df90 100644 --- a/Tests/Editor/BaseModule/TerrainSourceTests.cs +++ b/Tests/Editor/BaseModule/TerrainSourceTests.cs @@ -28,10 +28,14 @@ internal class TerrainSourceTests private CanonicalTileId _testTileId = new CanonicalTileId(16, 37310, 18968); private Texture2D _testTexture; private RasterData _testRasterData; + private bool _initialized; - [OneTimeSetUp] - public void OneTimeSetUp() + [UnitySetUp] + public IEnumerator OneTimeSetUp() { + if (_initialized) yield break; + _initialized = true; + _testTexture = Texture2D.whiteTexture; _testRasterData = new TerrainData() { @@ -44,7 +48,7 @@ public void OneTimeSetUp() }; var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Editor/BaseModule/TileJSONTest.cs b/Tests/Editor/BaseModule/TileJSONTest.cs index ea710a037..f56dc66d8 100644 --- a/Tests/Editor/BaseModule/TileJSONTest.cs +++ b/Tests/Editor/BaseModule/TileJSONTest.cs @@ -11,11 +11,11 @@ internal class TileJSONTest { private DataFetchingManager _dataFetcher; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _dataFetcher = new DataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } diff --git a/Tests/Editor/BaseModule/TokenTest.cs b/Tests/Editor/BaseModule/TokenTest.cs index ff1e98a4d..bb45a25cb 100644 --- a/Tests/Editor/BaseModule/TokenTest.cs +++ b/Tests/Editor/BaseModule/TokenTest.cs @@ -13,11 +13,11 @@ public class TokenTest private string _configAccessToken; private Func _configSkuToken; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _tokenApi = new MapboxTokenApi(); _configAccessToken = mapboxContext.GetAccessToken(); _configSkuToken = mapboxContext.GetSkuToken; diff --git a/Tests/Runtime/BaseModule/ImageSourceTests.cs b/Tests/Runtime/BaseModule/ImageSourceTests.cs index 751bf92c4..2c0765b27 100644 --- a/Tests/Runtime/BaseModule/ImageSourceTests.cs +++ b/Tests/Runtime/BaseModule/ImageSourceTests.cs @@ -30,10 +30,14 @@ internal class ImageSourceTests private CanonicalTileId _testTileId = new CanonicalTileId(16, 5, 7); private Texture2D _testTexture; private RasterData _testRasterData; - - [OneTimeSetUp] - public void OneTimeSetUp() + private bool _initialized; + + [UnitySetUp] + public IEnumerator OneTimeSetUp() { + if (_initialized) yield break; + _initialized = true; + _testTexture = Texture2D.whiteTexture; _testRasterData = new RasterData() { @@ -44,9 +48,9 @@ public void OneTimeSetUp() ExpirationDate = DateTime.Now.AddHours(1), ETag = "testETAG" }; - + var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Runtime/BaseModule/MapTest.cs b/Tests/Runtime/BaseModule/MapTest.cs index 69e922352..e1fe75ceb 100644 --- a/Tests/Runtime/BaseModule/MapTest.cs +++ b/Tests/Runtime/BaseModule/MapTest.cs @@ -49,15 +49,15 @@ public class MapTest : MonoBehaviour // // yield return _map.Initialize(); // } - private void LoadMap(string latlng, bool useSqlite = true, bool useFileCache = true) + private IEnumerator LoadMap(string latlng, bool useSqlite = true, bool useFileCache = true) { var mapInfo = new MapInformation(latlng); mapInfo.SetInformation(null, 16, 45, null, 1000); mapInfo.Initialize(); var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); - unityContext.Initialize(); + yield return unityContext.Initialize(); var taskManager = unityContext.TaskManager; _dataManager = new LoggingDataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); @@ -135,7 +135,7 @@ public void TearDown() [UnityTest] public IEnumerator LoadMapView() { - LoadMap(_helsinkiLatitudeLongitudeString); + yield return LoadMap(_helsinkiLatitudeLongitudeString); yield return _map.Initialize(); var tileCover = new TileCover(); @@ -153,8 +153,8 @@ public IEnumerator LoadMapOfExpiredData() { //this wrongly assumes data is already available in the long term cache so it's incomplete - LoadMap(_helsinkiLatitudeLongitudeString); - + yield return LoadMap(_helsinkiLatitudeLongitudeString); + //expire all tiles in db var sqliteCache = _cacheManager.SqLiteCache as SqliteCache; foreach (var tile in sqliteCache.GetAllTiles()) @@ -184,7 +184,7 @@ public IEnumerator LoadMapOfExpiredData() [UnityTest] public IEnumerator LoadMapWithoutSql() { - LoadMap(_helsinkiLatitudeLongitudeString, false); + yield return LoadMap(_helsinkiLatitudeLongitudeString, false); yield return _map.Initialize(); yield return _map.LoadMapViewCoroutine(); @@ -199,7 +199,7 @@ public IEnumerator LoadMapWithoutSql() [UnityTest] public IEnumerator LoadMapWithoutFile() { - LoadMap(_helsinkiLatitudeLongitudeString, true, false); + yield return LoadMap(_helsinkiLatitudeLongitudeString, true, false); yield return _map.Initialize(); yield return _map.LoadMapViewCoroutine(); @@ -215,7 +215,7 @@ public IEnumerator LoadMapWithoutFile() public IEnumerator LoadThenCancelAllRequests() { MapboxCacheManager.DeleteAllCache(); - LoadMap(_helsinkiLatitudeLongitudeString); + yield return LoadMap(_helsinkiLatitudeLongitudeString); yield return _map.Initialize(); _dataManager.SetDelayTime(5); diff --git a/Tests/Runtime/BaseModule/MapViewLoadTests.cs b/Tests/Runtime/BaseModule/MapViewLoadTests.cs index c320600f1..4bb47f13c 100644 --- a/Tests/Runtime/BaseModule/MapViewLoadTests.cs +++ b/Tests/Runtime/BaseModule/MapViewLoadTests.cs @@ -48,9 +48,9 @@ public IEnumerator OneTimeSetUp() mapInfo.SetInformation(null, 16, 45, null, 1000); mapInfo.Initialize(); var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); - unityContext.Initialize(); + yield return unityContext.Initialize(); var taskManager = new TaskManager(); taskManager.Initialize(); diff --git a/Tests/Runtime/BaseModule/MapboxMapTests.cs b/Tests/Runtime/BaseModule/MapboxMapTests.cs index b3fb38015..75f77dc5b 100644 --- a/Tests/Runtime/BaseModule/MapboxMapTests.cs +++ b/Tests/Runtime/BaseModule/MapboxMapTests.cs @@ -28,57 +28,59 @@ public class MapboxMapTests private LatitudeLongitude _helsinkiLatLng; private LatitudeLongitude _sfLatLng; private MapboxMap _map; - - [OneTimeSetUp] - public void OneTimeSetUp() + private bool _initialized; + + [UnitySetUp] + public IEnumerator Setup() { - _helsinkiLatLng = Conversions.StringToLatLon(_helsinkiLatitudeLongitudeString); - _sfLatLng = Conversions.StringToLatLon(_sanFranciscoLatitudeLongitudeString); + if (!_initialized) + { + _initialized = true; - var mapInfo = new MapInformation(_helsinkiLatitudeLongitudeString); - mapInfo.SetInformation(null, 16, 45, null, 1000); - mapInfo.Initialize(); - var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); - var unityContext = new UnityContext(); - unityContext.Initialize(); + _helsinkiLatLng = Conversions.StringToLatLon(_helsinkiLatitudeLongitudeString); + _sfLatLng = Conversions.StringToLatLon(_sanFranciscoLatitudeLongitudeString); - var taskManager = new TaskManager(); - taskManager.Initialize(); - unityContext.TaskManager = taskManager; - var dataManager = new DataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); + var mapInfo = new MapInformation(_helsinkiLatitudeLongitudeString); + mapInfo.SetInformation(null, 16, 45, null, 1000); + mapInfo.Initialize(); + var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); + var unityContext = new UnityContext(); + unityContext.Initialize(); - var sqliteCache = new MockSqliteCache(taskManager); - sqliteCache.ReadySqliteDatabase(); - - var mapService = new MapUnityService( - unityContext, - mapboxContext, - new UnityFixedAreaTileProvider(), - new MapboxCacheManager( - unityContext, - new MemoryCache(), - new MockFileCache(taskManager), - sqliteCache), - dataManager); - - _map = new MapboxMap(mapInfo, unityContext, mapService); - var mapVisualizer = new MapboxMapVisualizer(mapInfo, unityContext, new TileCreator(unityContext)); - mapVisualizer.LayerModules.Add( - new TerrainLayerModule(mapService.GetTerrainRasterSource( - new ImageSourceSettings() - { - TilesetId = MapboxDefaultElevation.GetParameters(ElevationSourceType.MapboxTerrain).Id - }), new TerrainLayerModuleSettings())); - _map.MapVisualizer = mapVisualizer; - } + var taskManager = new TaskManager(); + taskManager.Initialize(); + unityContext.TaskManager = taskManager; + var dataManager = new DataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); + + var sqliteCache = new MockSqliteCache(taskManager); + sqliteCache.ReadySqliteDatabase(); + + var mapService = new MapUnityService( + unityContext, + mapboxContext, + new UnityFixedAreaTileProvider(), + new MapboxCacheManager( + unityContext, + new MemoryCache(), + new MockFileCache(taskManager), + sqliteCache), + dataManager); + + _map = new MapboxMap(mapInfo, unityContext, mapService); + var mapVisualizer = new MapboxMapVisualizer(mapInfo, unityContext, new TileCreator(unityContext)); + mapVisualizer.LayerModules.Add( + new TerrainLayerModule(mapService.GetTerrainRasterSource( + new ImageSourceSettings() + { + TilesetId = MapboxDefaultElevation.GetParameters(ElevationSourceType.MapboxTerrain).Id + }), new TerrainLayerModuleSettings())); + _map.MapVisualizer = mapVisualizer; + } - [UnitySetUp] - public IEnumerator Setup() - { var initialization = Runnable.Instance.StartCoroutine(_map.Initialize()); yield return initialization; - + Assert.IsNotNull(_map); Assert.IsNotNull(_map.MapVisualizer); Assert.IsTrue(_map.Status >= InitializationStatus.Initialized); @@ -204,13 +206,16 @@ public class DataFetcherTests private LoggingDataFetchingManager _datafetcher; private CanonicalTileId _tileId; private string _tilesetId; - - - [OneTimeSetUp] - public void OneTimeSetup() + private bool _initialized; + + [UnitySetUp] + public IEnumerator OneTimeSetup() { + if (_initialized) yield break; + _initialized = true; + var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _datafetcher = new LoggingDataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); _tilesetId = vectorTileset.Id; diff --git a/Tests/Runtime/BaseModule/PbfSourceTests.cs b/Tests/Runtime/BaseModule/PbfSourceTests.cs index 18ab8f614..843ac0453 100644 --- a/Tests/Runtime/BaseModule/PbfSourceTests.cs +++ b/Tests/Runtime/BaseModule/PbfSourceTests.cs @@ -26,10 +26,14 @@ internal class PbfSourceTests private CanonicalTileId _testTileId = new CanonicalTileId(16, 5, 7); private Texture2D _testTexture; private VectorData _testVectorData; + private bool _initialized; - [OneTimeSetUp] - public void OneTimeSetUp() + [UnitySetUp] + public IEnumerator OneTimeSetUp() { + if (_initialized) yield break; + _initialized = true; + _testTexture = Texture2D.whiteTexture; _testVectorData = new VectorData() { @@ -41,7 +45,7 @@ public void OneTimeSetUp() }; var mapboxContext = new MapboxContext(); - mapboxContext.LoadConfigurationWithoutValidation(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Runtime/BaseModule/RasterDataTests.cs b/Tests/Runtime/BaseModule/RasterDataTests.cs index fb860dec2..a77a85193 100644 --- a/Tests/Runtime/BaseModule/RasterDataTests.cs +++ b/Tests/Runtime/BaseModule/RasterDataTests.cs @@ -31,13 +31,17 @@ internal class RasterDataTests private Style _imageryTileset; private UnwrappedTileId _tileId; private HashSet _testTileHashset; + private bool _initialized; - [OneTimeSetUp] - public void OneTimeSetup() + [UnitySetUp] + public IEnumerator OneTimeSetup() { + if (_initialized) yield break; + _initialized = true; + _taskManager = new MockTaskManager(); _mapboxContext = new MapboxContext(); - _mapboxContext.LoadConfigurationWithoutValidation(); + yield return _mapboxContext.LoadConfigurationCoroutine(false); _dataFetchingManager = new DataFetchingManager(_mapboxContext.GetAccessToken(), _mapboxContext.GetSkuToken); _unityContext = new UnityContext(); _unityContext.Initialize(_taskManager); @@ -69,9 +73,7 @@ public void SetUp() [UnityTest, Order(5)] public IEnumerator RequestTileListNoCache() { - var mapboxContext1 = new MapboxContext(); - mapboxContext1.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext1, null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, null)); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); @@ -91,9 +93,7 @@ public IEnumerator RequestTileListNoCache() [UnityTest, Order(4)] public IEnumerator RequestTileListCheckWithFileSqlite() { - var mapboxContext2 = new MapboxContext(); - mapboxContext2.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext2, null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); @@ -127,9 +127,7 @@ public IEnumerator RequestTileListCheckWithFileSqlite() [UnityTest, Order(3)] public IEnumerator RequestWithAllCaches() { - var mapboxContext3 = new MapboxContext(); - mapboxContext3.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext3, null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); @@ -158,9 +156,7 @@ public IEnumerator RequestWithAllCaches() [UnityTest, Order(2)] public IEnumerator RequestNoFileYesSqliteCache() { - var mapboxContext4 = new MapboxContext(); - mapboxContext4.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext4, null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, _sqliteCache)); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); @@ -185,9 +181,7 @@ public IEnumerator RequestNoFileYesSqliteCache() public IEnumerator RequestNoFileNoSqliteCaches() { var cacheManager = new MapboxCacheManager(_unityContext, new MemoryCache(), null, null); - var mapboxContext5 = new MapboxContext(); - mapboxContext5.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext5, null, cacheManager); + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, cacheManager); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); diff --git a/Tests/Runtime/BaseModule/VectorDataTests.cs b/Tests/Runtime/BaseModule/VectorDataTests.cs index d879bf710..540b5205a 100644 --- a/Tests/Runtime/BaseModule/VectorDataTests.cs +++ b/Tests/Runtime/BaseModule/VectorDataTests.cs @@ -29,13 +29,17 @@ internal class VectorDataTests private ISqliteCache _sqliteCache; private UnwrappedTileId _tileId; private HashSet _testTileHashset; + private bool _initialized; - [OneTimeSetUp] - public void OneTimeSetup() + [UnitySetUp] + public IEnumerator OneTimeSetup() { + if (_initialized) yield break; + _initialized = true; + _taskManager = new MockTaskManager(); _mapboxContext = new MapboxContext(); - _mapboxContext.LoadConfigurationWithoutValidation(); + yield return _mapboxContext.LoadConfigurationCoroutine(false); _dataFetchingManager = new DataFetchingManager(_mapboxContext.GetAccessToken(), _mapboxContext.GetSkuToken); _unityContext = new UnityContext(); _unityContext.Initialize(_taskManager); @@ -67,9 +71,7 @@ public void SetUp() [UnityTest, Order(4)] public IEnumerator RequestTileList() { - var mapboxContext1 = new MapboxContext(); - mapboxContext1.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext1, null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); @@ -95,9 +97,7 @@ public IEnumerator RequestTileList() [UnityTest, Order(3)] public IEnumerator RequestWithAllCaches() { - var mapboxContext2 = new MapboxContext(); - mapboxContext2.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext2, null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); @@ -117,9 +117,7 @@ public IEnumerator RequestWithAllCaches() [UnityTest, Order(2)] public IEnumerator RequestNoFileYesSqliteCache() { - var mapboxContext3 = new MapboxContext(); - mapboxContext3.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext3, null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, _sqliteCache)); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); @@ -144,9 +142,7 @@ public IEnumerator RequestNoFileYesSqliteCache() public IEnumerator RequestNoFileNoSqliteCaches() { var cacheManager = new MapboxCacheManager(_unityContext, new MemoryCache(), null, null); - var mapboxContext4 = new MapboxContext(); - mapboxContext4.LoadConfigurationWithoutValidation(); - var mapService = new MapUnityService(_unityContext, mapboxContext4, null, cacheManager); + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, cacheManager); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); From e0ed338e19d389f6e303d0a8f01bb0cc90a002b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Baran=20Kahyao=C4=9Flu?= Date: Thu, 12 Mar 2026 17:17:25 +0300 Subject: [PATCH 10/54] add Mapbox Components (#1984) add mapbox components system, an alternative to vector module for higher performance mesh generation --- Documentation~/ComponentsModule.md | 327 +++++ .../Data/DataFetchers/TerrainData.cs | 11 +- .../Data/Platform/Cache/FileCache.cs | 25 +- .../Data/Tasks/MeshGenTaskWrapper.cs | 15 +- .../BaseModule/Data/Tiles/CanonicalTileId.cs | 2 +- .../Mapbox/BaseModule/Data/Vector2d/RectD.cs | 12 +- .../Mapbox/BaseModule/Map/MapInformation.cs | 2 +- Runtime/Mapbox/BaseModule/Map/MapboxMap.cs | 25 +- .../BaseModule/Map/MapboxMapVisualizer.cs | 251 +++- Runtime/Mapbox/BaseModule/Map/TileCreator.cs | 25 +- .../ModuleBehaviours/TileCreatorBehaviour.cs | 3 +- .../Mapbox/BaseModule/Unity/UnityMapTile.cs | 15 +- .../Example/Scripts/Map/MapboxMapBehaviour.cs | 10 - .../Example/Scripts/Map/SnapMapToTransform.cs | 2 +- .../Mapbox/Example/Scripts/Moving3dCamera.cs | 16 +- .../Scripts/Moving3dCameraBehaviour.cs | 2 +- .../Mapbox/Example/Scripts/SlippyMapCamera.cs | 23 +- .../Scripts/SlippyMapCameraBehaviour.cs | 3 +- .../Scripts/Logging/LoggingMapBehaviour.cs | 8 - .../Scripts/Logging/LoggingTaskManager.cs | 2 +- .../Mapbox/UnityMapService/MapUnityService.cs | 7 +- ...Visualizers.meta => BasicVisualizers.meta} | 0 .../Buildings.meta | 0 .../Buildings/BuildingMaterial.mat | 0 .../Buildings/BuildingMaterial.mat.meta | 0 .../Buildings/DefaultBuildingLayer.asset | 0 .../Buildings/DefaultBuildingLayer.asset.meta | 0 .../Buildings/DefaultBuildingStack.asset | 0 .../Buildings/DefaultBuildingStack.asset.meta | 0 ...mWorldObject.meta => ComponentSystem.meta} | 2 +- .../ComponentSystem/AreaComponent.meta | 8 + .../AreaComponent/AreaComponentVisualizer.cs | 260 ++++ .../AreaComponentVisualizer.cs.meta | 3 + .../AreaComponentVisualizerObject.cs | 68 + .../AreaComponentVisualizerObject.cs.meta | 3 + .../BuildingComponentVisualizer.meta | 8 + .../BasicExtrusionSettings.cs | 93 ++ .../BasicExtrusionSettings.cs.meta | 3 + .../BuildingComponentVisualizer.cs | 306 +++++ .../BuildingComponentVisualizer.cs.meta | 3 + .../BuildingLayerVisualizerObject.cs | 64 + .../BuildingLayerVisualizerObject.cs.meta | 3 + .../ComponentSystem/ComponentSystem.asmdef | 17 + .../ComponentSystem.asmdef.meta | 7 + .../VectorModule/ComponentSystem/Data.meta | 8 + .../ComponentSystem/Data/DecodeGeometry.cs | 136 ++ .../Data/DecodeGeometry.cs.meta | 3 + .../ComponentSystem/Data/FeatureVertexData.cs | 12 + .../Data/FeatureVertexData.cs.meta | 3 + .../ComponentSystem/Data/MeshData.cs | 24 + .../ComponentSystem/Data/MeshData.cs.meta | 3 + .../ComponentSystem/Data/PbfReader.cs | 298 +++++ .../ComponentSystem/Data/PbfReader.cs.meta | 3 + .../ComponentSystem/Data/StackMeshInfo.cs | 21 + .../Data/StackMeshInfo.cs.meta | 3 + .../ComponentSystem/Data/VectorTile.cs | 19 + .../ComponentSystem/Data/VectorTile.cs.meta | 3 + .../ComponentSystem/Data/VectorTileLayer.cs | 73 ++ .../Data/VectorTileLayer.cs.meta | 3 + .../ComponentSystem/Data/VectorTileReader.cs | 169 +++ .../Data/VectorTileReader.cs.meta | 3 + .../VectorModule/ComponentSystem/Editor.meta | 8 + .../BuildingLayerVisualizerObjectEditor.cs | 63 + ...uildingLayerVisualizerObjectEditor.cs.meta | 11 + .../Editor/ExtrusionOptionsDrawer.cs | 94 ++ .../Editor/ExtrusionOptionsDrawer.cs.meta | 11 + .../MapboxComponentsModuleScriptEditor.cs | 173 +++ ...MapboxComponentsModuleScriptEditor.cs.meta | 11 + .../MapboxComponentVisualizer.cs | 105 ++ .../MapboxComponentVisualizer.cs.meta | 3 + .../ComponentSystem/MapboxComponentsModule.cs | 149 +++ .../MapboxComponentsModule.cs.meta | 3 + .../MapboxComponentsModuleScript.cs | 51 + .../MapboxComponentsModuleScript.cs.meta | 3 + .../ComponentSystem/Modifiers.meta | 8 + .../Modifiers/ArrayChamferHeight.cs | 296 +++++ .../Modifiers/ArrayChamferHeight.cs.meta | 3 + .../ComponentSystem/Modifiers/ArrayHeight.cs | 203 +++ .../Modifiers/ArrayHeight.cs.meta | 3 + .../ComponentSystem/Modifiers/ArrayPolygon.cs | 200 +++ .../Modifiers/ArrayPolygon.cs.meta | 3 + .../Modifiers/ArraySnapTerrain.cs | 35 + .../Modifiers/ArraySnapTerrain.cs.meta | 3 + .../RoadComponentVisualizer.meta | 8 + .../ComponentFilterStack.cs | 58 + .../ComponentFilterStack.cs.meta | 3 + .../RoadComponentVisualizer.cs | 553 ++++++++ .../RoadComponentVisualizer.cs.meta | 3 + .../RoadComponentVisualizerObject.cs | 56 + .../RoadComponentVisualizerObject.cs.meta | 3 + .../RoadFeatureUnity.cs | 49 + .../RoadFeatureUnity.cs.meta | 3 + .../RoadComponentVisualizer/RoadFilter.cs | 139 +++ .../RoadFilter.cs.meta | 3 + .../RoadComponentVisualizer/RoadStyle.cs | 22 + .../RoadComponentVisualizer/RoadStyle.cs.meta | 3 + .../RoadComponentVisualizer/RoadStyleSheet.cs | 49 + .../RoadStyleSheet.cs.meta | 3 + .../Editor/ComponentFilterStackDrawer.cs | 307 +++++ .../Editor/ComponentFilterStackDrawer.cs.meta | 3 + .../Editor/GeometryExtrusionOptionsDrawer.cs | 95 ++ .../GeometryExtrusionOptionsDrawer.cs.meta | 11 + .../Editor/HeightModifierEditor.cs | 5 + .../Editor/Mapbox.VectorModule.Editor.asmdef | 3 +- .../Editor/PropertiesViewerBehaviourEditor.cs | 38 + .../PropertiesViewerBehaviourEditor.cs.meta | 11 + .../VectorModule/Editor/RoadStyleDrawer.cs | 117 ++ .../Editor/RoadStyleDrawer.cs.meta | 3 + .../Editor/RoadStyleSheetEditor.cs | 94 ++ .../Editor/RoadStyleSheetEditor.cs.meta | 3 + .../VectorModule/MeshGeneration/Earcut.cs | 176 ++- .../MeshModifiers/ChamferHeightModifier.cs | 13 +- .../MeshModifiers/PolygonMeshModifier.cs | 4 +- .../MeshModifiers/SnapTerrainModifier.cs | 16 +- .../Unity/VectorLayerModuleScript.cs | 13 +- .../Mapbox/VectorModule/VectorLayerModule.cs | 183 ++- .../VectorModule/VectorLayerVisualizer.cs | 23 + .../Astronaut/CharacterMovement.cs | 3 + Samples~/MapboxComponents/Areas.meta | 8 + .../MapboxComponents/Areas/Commercial.mat | 133 ++ .../Areas/Commercial.mat.meta | 8 + Samples~/MapboxComponents/Areas/Grass.mat | 133 ++ .../MapboxComponents/Areas/Grass.mat.meta | 8 + Samples~/MapboxComponents/Areas/Park.mat | 133 ++ Samples~/MapboxComponents/Areas/Park.mat.meta | 8 + .../ParksComponentVisualizerObject.asset | 40 + .../ParksComponentVisualizerObject.asset.meta | 8 + Samples~/MapboxComponents/Areas/Pitch.mat | 133 ++ .../MapboxComponents/Areas/Pitch.mat.meta | 8 + Samples~/MapboxComponents/Areas/School.mat | 133 ++ .../MapboxComponents/Areas/School.mat.meta | 8 + Samples~/MapboxComponents/Areas/Water.mat | 133 ++ .../MapboxComponents/Areas/Water.mat.meta | 8 + ...WaterComponentVisualizerObject.asset.asset | 22 + ...ComponentVisualizerObject.asset.asset.meta | 8 + Samples~/MapboxComponents/Areas/Wood.mat | 133 ++ Samples~/MapboxComponents/Areas/Wood.mat.meta | 8 + Samples~/MapboxComponents/Buildings.meta | 8 + .../BuildingComponentVisualizerObject.asset | 34 + ...ildingComponentVisualizerObject.asset.meta | 8 + .../Buildings/BuildingMaterial.mat | 155 +++ .../Buildings/BuildingMaterial.mat.meta | 8 + Samples~/MapboxComponents/GroundMaterial.mat | 133 ++ .../MapboxComponents/GroundMaterial.mat.meta | 8 + Samples~/MapboxComponents/Map.unity | 828 ++++++++++++ Samples~/MapboxComponents/Map.unity.meta | 7 + Samples~/MapboxComponents/MapFog.mat | 137 ++ Samples~/MapboxComponents/MapFog.mat.meta | 8 + .../MapboxFogShader.shadergraph | 1110 +++++++++++++++++ .../MapboxFogShader.shadergraph.meta | 10 + Samples~/MapboxComponents/Road.meta | 8 + .../Road/PathRoadMaterial.mat | 160 +++ .../Road/PathRoadMaterial.mat.meta | 8 + .../Road/RoadLayerVisualizerObject.asset | 19 + .../Road/RoadLayerVisualizerObject.asset.meta | 8 + .../RoadLayerVisualizerObjectOutline.asset | 19 + ...oadLayerVisualizerObjectOutline.asset.meta | 8 + .../MapboxComponents/Road/RoadMaterial.mat | 164 +++ .../Road/RoadMaterial.mat.meta | 8 + .../Road/RoadOutlineMaterial.mat | 156 +++ .../Road/RoadOutlineMaterial.mat.meta | 8 + .../Road/RoadStyleSheet.asset | 158 +++ .../Road/RoadStyleSheet.asset.meta | 8 + .../Road/RoadStyleSheetOutline.asset | 116 ++ .../Road/RoadStyleSheetOutline.asset.meta | 8 + .../Road/ServiceRoadMaterial.mat | 160 +++ .../Road/ServiceRoadMaterial.mat.meta | 8 + Samples~/MapboxComponents/Road/road.preset | 291 +++++ .../MapboxComponents/Road/road.preset.meta | 8 + .../BuildingLayerPerformanceTests.cs | 217 ++++ .../BuildingLayerPerformanceTests.cs.meta | 3 + Tests/Editor/VectorModule/BuildingSetups.meta | 8 + .../TEST_BuildingLayerVisualizerObject.asset | 34 + ...T_BuildingLayerVisualizerObject.asset.meta | 8 + .../TEST_OldLayerVisualizer.asset | 20 + .../TEST_OldLayerVisualizer.asset.meta | 8 + .../BuildingSetups/TEST_OldModStack.asset | 67 + .../TEST_OldModStack.asset.meta | 8 + .../VectorModule/RoadLayerPerformanceTests.cs | 147 +++ .../RoadLayerPerformanceTests.cs.meta | 3 + Tests/Editor/VectorModule/RoadSetup.meta | 8 + .../RoadSetup/TEST_BasicRoadStyleSheet.asset | 102 ++ .../TEST_BasicRoadStyleSheet.asset.meta | 8 + .../RoadSetup/TEST_BasicRoads.asset | 19 + .../RoadSetup/TEST_BasicRoads.asset.meta | 8 + .../VectorModule/VectorModuleTests.asmdef | 30 + .../VectorModuleTests.asmdef.meta | 7 + Tests/Runtime/BaseModule/MapTest.cs | 2 +- Tests/Runtime/BaseModule/MapViewLoadTests.cs | 2 +- package.json | 5 + 190 files changed, 11245 insertions(+), 252 deletions(-) create mode 100644 Documentation~/ComponentsModule.md rename Runtime/Mapbox/VectorModule/{Visualizers.meta => BasicVisualizers.meta} (100%) rename Runtime/Mapbox/VectorModule/{Visualizers => BasicVisualizers}/Buildings.meta (100%) rename Runtime/Mapbox/VectorModule/{Visualizers => BasicVisualizers}/Buildings/BuildingMaterial.mat (100%) rename Runtime/Mapbox/VectorModule/{Visualizers => BasicVisualizers}/Buildings/BuildingMaterial.mat.meta (100%) rename Runtime/Mapbox/VectorModule/{Visualizers => BasicVisualizers}/Buildings/DefaultBuildingLayer.asset (100%) rename Runtime/Mapbox/VectorModule/{Visualizers => BasicVisualizers}/Buildings/DefaultBuildingLayer.asset.meta (100%) rename Runtime/Mapbox/VectorModule/{Visualizers => BasicVisualizers}/Buildings/DefaultBuildingStack.asset (100%) rename Runtime/Mapbox/VectorModule/{Visualizers => BasicVisualizers}/Buildings/DefaultBuildingStack.asset.meta (100%) rename Runtime/Mapbox/VectorModule/{CustomWorldObject.meta => ComponentSystem.meta} (77%) create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs create mode 100644 Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs create mode 100644 Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs create mode 100644 Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs create mode 100644 Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs.meta create mode 100644 Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs create mode 100644 Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs.meta create mode 100644 Samples~/MapboxComponents/Areas.meta create mode 100644 Samples~/MapboxComponents/Areas/Commercial.mat create mode 100644 Samples~/MapboxComponents/Areas/Commercial.mat.meta create mode 100644 Samples~/MapboxComponents/Areas/Grass.mat create mode 100644 Samples~/MapboxComponents/Areas/Grass.mat.meta create mode 100644 Samples~/MapboxComponents/Areas/Park.mat create mode 100644 Samples~/MapboxComponents/Areas/Park.mat.meta create mode 100644 Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset create mode 100644 Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset.meta create mode 100644 Samples~/MapboxComponents/Areas/Pitch.mat create mode 100644 Samples~/MapboxComponents/Areas/Pitch.mat.meta create mode 100644 Samples~/MapboxComponents/Areas/School.mat create mode 100644 Samples~/MapboxComponents/Areas/School.mat.meta create mode 100644 Samples~/MapboxComponents/Areas/Water.mat create mode 100644 Samples~/MapboxComponents/Areas/Water.mat.meta create mode 100644 Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset create mode 100644 Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset.meta create mode 100644 Samples~/MapboxComponents/Areas/Wood.mat create mode 100644 Samples~/MapboxComponents/Areas/Wood.mat.meta create mode 100644 Samples~/MapboxComponents/Buildings.meta create mode 100644 Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset create mode 100644 Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset.meta create mode 100644 Samples~/MapboxComponents/Buildings/BuildingMaterial.mat create mode 100644 Samples~/MapboxComponents/Buildings/BuildingMaterial.mat.meta create mode 100644 Samples~/MapboxComponents/GroundMaterial.mat create mode 100644 Samples~/MapboxComponents/GroundMaterial.mat.meta create mode 100644 Samples~/MapboxComponents/Map.unity create mode 100644 Samples~/MapboxComponents/Map.unity.meta create mode 100644 Samples~/MapboxComponents/MapFog.mat create mode 100644 Samples~/MapboxComponents/MapFog.mat.meta create mode 100644 Samples~/MapboxComponents/MapboxFogShader.shadergraph create mode 100644 Samples~/MapboxComponents/MapboxFogShader.shadergraph.meta create mode 100644 Samples~/MapboxComponents/Road.meta create mode 100644 Samples~/MapboxComponents/Road/PathRoadMaterial.mat create mode 100644 Samples~/MapboxComponents/Road/PathRoadMaterial.mat.meta create mode 100644 Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset create mode 100644 Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset.meta create mode 100644 Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset create mode 100644 Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset.meta create mode 100644 Samples~/MapboxComponents/Road/RoadMaterial.mat create mode 100644 Samples~/MapboxComponents/Road/RoadMaterial.mat.meta create mode 100644 Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat create mode 100644 Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat.meta create mode 100644 Samples~/MapboxComponents/Road/RoadStyleSheet.asset create mode 100644 Samples~/MapboxComponents/Road/RoadStyleSheet.asset.meta create mode 100644 Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset create mode 100644 Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset.meta create mode 100644 Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat create mode 100644 Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat.meta create mode 100644 Samples~/MapboxComponents/Road/road.preset create mode 100644 Samples~/MapboxComponents/Road/road.preset.meta create mode 100644 Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs create mode 100644 Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs.meta create mode 100644 Tests/Editor/VectorModule/BuildingSetups.meta create mode 100644 Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset create mode 100644 Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset.meta create mode 100644 Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset create mode 100644 Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset.meta create mode 100644 Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset create mode 100644 Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset.meta create mode 100644 Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs create mode 100644 Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs.meta create mode 100644 Tests/Editor/VectorModule/RoadSetup.meta create mode 100644 Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset create mode 100644 Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset.meta create mode 100644 Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset create mode 100644 Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset.meta create mode 100644 Tests/Editor/VectorModule/VectorModuleTests.asmdef create mode 100644 Tests/Editor/VectorModule/VectorModuleTests.asmdef.meta diff --git a/Documentation~/ComponentsModule.md b/Documentation~/ComponentsModule.md new file mode 100644 index 000000000..1ae8c1dbb --- /dev/null +++ b/Documentation~/ComponentsModule.md @@ -0,0 +1,327 @@ +11## Components Module + +The Components Module is a high-performance alternative to the Vector Layer Module, designed for rendering large-scale maps with significantly reduced memory allocations and improved frame rates. + +Like the Vector Layer Module, it uses the [Mapbox Vector Tiles API](https://docs.mapbox.com/api/maps/vector-tiles/) to fetch map data. However, it processes this data through a completely redesigned pipeline optimized for zero-allocation mesh generation and background threading. + +--- + +### Why Components Module Exists + +The original Vector Layer Module, while flexible, was not optimized for performance-critical applications. The Components Module was created to address performance limitations through: + +**Performance Improvements:** +- **Custom PBF Parser**: A zero-allocation Protocol Buffer parser using `ref struct` and `Span` for direct memory access +- **Optimized Data Structures**: Pre-sized buffers and stack-allocated arrays minimize GC pressure +- **Merged Meshes**: Buildings and roads within each tile are combined into single meshes, dramatically reducing draw calls compared to the Vector Layer Module's per-feature mesh approach + +--- + +### Comparison: Components Module vs Vector Layer Module + +| Feature | Vector Layer Module | Components Module | +|---------|---------------------|-------------------| +| **Flexibility** | Highly flexible with modifier stacks | Fixed pipeline per component type | +| **Extensibility** | Easy to add custom modifiers | Requires C# coding to extend | +| **Mesh Output** | Individual meshes per feature | Merged meshes per tile (buildings/roads combined) | +| **Performance** | Adequate for small-medium maps | Optimized for large-scale maps | +| **Memory** | Moderate allocation rate | Minimal allocations | + +**When to Use Components Module:** +- Large maps with many visible tiles +- Mobile or low-end hardware targets +- Applications requiring stable 60 FPS +- Dense urban environments with thousands of buildings + +**When to Use Vector Layer Module:** +- Rapid prototyping +- Custom rendering workflows +- Non-performance-critical applications +- Need for runtime modifier configuration + +--- + +### Architecture Overview + +The Components Module processes vector tiles through these stages: + +1. **Tile Download**: Vector tiles are fetched and decompressed (shared with Vector Layer Module) +2. **Background Parsing**: Custom PBF parser decodes tile data on background threads using zero-allocation techniques +3. **Mesh Generation**: Component visualizers generate meshes in background tasks using `Span` for direct vertex manipulation +4. **Main Thread Finalization**: Generated meshes are transferred to GameObjects on the main thread + +**Key Technical Components:** +- `VectorTileReader`: Zero-allocation PBF parser using `ref struct` and `ReadOnlySpan` +- `PbfReader`: Low-level varint decoder with optimized packed array handling +- `MapboxComponentVisualizer`: Base class for component visualizers with stable ID assignment +- `MapboxComponentsModule`: Coordinates background mesh generation and main-thread finalization +- `ArrayPolygon`, `ArrayHeight`, `ArrayChamferHeight`: Stack-optimized mesh modifiers + +--- + +### Module Setup + +To use the Components Module instead of the Vector Layer Module: + +1. Create a new `MapboxComponentsModuleScript` component on your map GameObject +2. Configure the module settings (similar to Vector Layer Module): + - **Source Type**: Mapbox Streets V8 or custom tileset + - **Cache Size**: Number of tiles to keep in memory + - **Clamp Data Level**: Maximum zoom level for data fetching + - **Reject Tiles Outside Zoom**: Discard tiles outside zoom range + +3. Add component visualizers: + - **Building Component**: For rendering 3D buildings + - **Road Component**: For rendering road networks + - **Area Component**: For rendering land use, water, parks, etc. + +--- + +### Component Visualizers + +The Components Module provides three specialized visualizers, each optimized for a specific feature type. + +#### 1. Building Component Visualizer + +Renders 3D buildings from the `building` layer. + +**Configuration Options:** + +- **Material** + The material applied to all building meshes in this component. + +- **Enable Terrain Snapping** + When enabled, building vertices are adjusted to match terrain elevation. + Requires a terrain-enabled map. Useful for hilly or mountainous areas. + +- **Round Building Corners** + Applies chamfered (rounded) corners to building geometry. + Creates more realistic building shapes but increases vertex count. + +- **Extrusion Settings** + Controls how building heights are determined: + - **Property Height**: Uses the `height` property from vector data (default) + - **Absolute Height**: Sets all buildings to a fixed height + - **Max Height**: Caps building heights at a maximum value + - **Min Height**: Ensures buildings are at least a minimum height + - **Range Height**: Clamps heights between min and max values + - **Extrusion Scale Factor**: Multiplies all height values (e.g., 0.5 for half height) + +- **Chamfer Offset** (when Round Building Corners is enabled) + The distance in meters to inset corners. Typical values: 0.5 - 2.0 meters. + +**Creating a Building Component Visualizer:** +1. Right-click in Project window: `Create → Mapbox → Layer Visualizers → Building Component Visualizer` +2. Configure the settings +3. Add the visualizer asset to your `MapboxComponentsModuleScript` + +--- + +#### 2. Road Component Visualizer + +Renders road networks from the `road` layer with style-based filtering. + +**Configuration Options:** + +- **Game Object Offset** + Vertical offset applied to the entire road layer. + Useful for raising roads slightly above terrain to prevent z-fighting. + +- **Random Offset Range** + Small random vertical offset per road segment (default: 0.0001). + Prevents z-fighting when multiple road segments overlap. + +- **Road Style Sheet** + A `RoadStyleSheet` asset that defines how different road types are rendered. + +**Road Style Sheet:** + +A Road Style Sheet contains multiple road styles, each with: + +- **Name**: Descriptive name (for organization only) +- **Filters**: Determines which road features this style applies to +- **Width**: Road width in meters +- **Push Up**: Additional vertical offset for this style +- **Material**: Material applied to roads matching this style + +**Filter Stack:** + +Filters determine which roads match a style. Common filters: +- **Class Filter**: Match by road class (e.g., "motorway", "primary", "residential") +- **Type Filter**: Match by road type (e.g., "trunk", "link") +- **Structure Filter**: Match by structure (e.g., "bridge", "tunnel") + +Filters can be combined using: +- **Any**: Feature matches if any filter passes +- **All**: Feature matches only if all filters pass +- **None**: Feature matches if no filters pass + +**Example Style Setup:** +``` +Style: "Highways" + Filters: Class = "motorway", Type = "Any" + Width: 12.0 + Material: HighwayMaterial + +Style: "Main Roads" + Filters: Class = "primary" OR "secondary" + Width: 8.0 + Material: MainRoadMaterial + +Style: "Local Streets" + Filters: Class = "residential" + Width: 4.0 + Material: LocalStreetMaterial +``` + +**Creating a Road Component Visualizer:** +1. Right-click in Project: `Create → Mapbox → Road Style Sheet` +2. Configure styles and filters in the sheet +3. Right-click: `Create → Mapbox → Layer Visualizers → Road Component Visualizer` +4. Assign the style sheet to the visualizer +5. Add visualizer to `MapboxComponentsModuleScript` + +--- + +#### 3. Area Component Visualizer + +Renders polygonal areas such as water, parks, landuse, etc. + +**Configuration Options:** + +- **Layer Name** + The vector layer to render (e.g., "water", "landuse", "park"). + Unlike building/road components which are fixed to their layers, area components are generic and can target any polygon layer. + +- **Game Object Offset** + Vertical offset applied to the entire area layer. + Useful for layering (e.g., water at 0, parks at 0.1, landuse at 0.2). + +- **Styles** + A list of area styles, each defining how features with a specific class are rendered. + +**Area Style:** + +Each area style has: +- **Class Name**: The feature class to match (e.g., "park", "residential", "commercial") +- **Material**: Material applied to matching features +- **Push Up**: Additional vertical offset for this class + +**Example Setup for Landuse:** +``` +Layer Name: "landuse" +Styles: + - Class: "residential", Material: ResidentialMat, Push Up: 0.0 + - Class: "commercial", Material: CommercialMat, Push Up: 0.1 + - Class: "industrial", Material: IndustrialMat, Push Up: 0.2 + - Class: "park", Material: ParkMat, Push Up: 0.3 +``` + +**Example Setup for Water:** +``` +Layer Name: "water" +Styles: + - Class: "*", Material: WaterMat, Push Up: 0.0 +``` +(Water features typically don't have class variations, so a single catch-all style works) + +**Creating an Area Component Visualizer:** +1. Right-click: `Create → Mapbox → Layer Visualizers → Area Component Visualizer` +2. Set the layer name +3. Add styles for each class you want to render +4. Add visualizer to `MapboxComponentsModuleScript` + +--- + +### Best Practices + +**Material Configuration:** +- Use shared materials across components to reduce draw calls +- Enable GPU instancing on materials when possible +- Consider mobile shader variants for cross-platform apps + +**Performance Tuning:** +- Use **Clamp Data Level** to limit detail at high zoom (e.g., clamp to 15 for dense cities) +- Reduce **Cache Size** if memory is constrained (default is usually fine) +- Disable **Terrain Snapping** if terrain isn't used (saves CPU) +- Use **Random Offset Range** sparingly—smaller values are better + +**Styling Tips:** +- Keep road style sheets simple—fewer styles mean faster filtering +- Use **Push Up** offsets to layer features (water → roads → buildings) +- Test materials with both day/night lighting for consistent appearance + +--- + +### Limitations + +The Components Module sacrifices flexibility for performance: + +1. **Fixed Component Types**: Only buildings, roads, and areas are supported out of the box +2. **Less Extensible**: Adding new component types requires C# programming and understanding of the performance architecture +3. **No Per-Feature Callbacks**: Vector Layer Module's per-feature game object modifiers aren't available +4. **Simplified Filtering**: Road/area filtering is less powerful than full modifier stacks + +For most production use cases, these limitations are acceptable trade-offs for the performance gains. If you need the flexibility, use the Vector Layer Module instead. + +--- + +### Troubleshooting + +**Buildings don't appear:** +- Verify Building Component Visualizer is active (`IsActive = true`) +- Check material is assigned +- Ensure map zoom level is 13+ (building data starts at zoom 13) +- Verify tileset includes `building` layer + +**Roads render incorrectly:** +- Check Road Style Sheet is assigned +- Verify styles have filters that match road classes in your area +- Test with a single catch-all style first: `Filters: Class = Any` +- Confirm road width is reasonable (1-20 meters) + +**Z-fighting between layers:** +- Increase `GameObjectOffset` for each component (e.g., roads: 0.1, areas: 0.0) +- Increase `RandomOffsetRange` for roads if segments overlap +- Use `PushUp` to layer features within a component + +**Performance still poor:** +- Profile with Unity Profiler—confirm mesh generation is the bottleneck +- Reduce visible tile count via camera distance/zoom constraints +- Lower `CacheSize` if memory is limited (won't improve FPS but reduces memory) +- Simplify Road Style Sheet—fewer styles = faster filtering +- Disable terrain snapping if not needed + +--- + +### API Events + +Components expose events for integration with custom systems: + +**BuildingComponentVisualizer:** +- `OnBuildingMeshCreated(CanonicalTileId tileId, GameObject gameObject, MeshData meshData)`: Fired when a building tile mesh is created +- `OnBuildingMeshDestroyed(CanonicalTileId tileId, GameObject gameObject)`: Fired when a building tile is unregistered + +**RoadComponentVisualizer:** +- `OnRoadMeshCreated(CanonicalTileId tileId, GameObject gameObject, MeshData meshData)`: Fired when a road tile mesh is created +- `OnRoadMeshDestroyed(CanonicalTileId tileId, GameObject gameObject)`: Fired when a road tile is unregistered + +**AreaComponentVisualizer:** +- `OnAreaMeshCreated(CanonicalTileId tileId, GameObject gameObject, MeshData meshData)`: Fired when an area tile mesh is created +- `OnAreaMeshDestroyed(CanonicalTileId tileId, GameObject gameObject)`: Fired when an area tile is unregistered + +**Usage Example:** +```csharp +buildingVisualizer.OnBuildingMeshCreated += (tileId, gameObject, meshData) => { + // Add custom components, colliders, etc. + gameObject.AddComponent(); +}; +``` + +--- + +### Conclusion + +The Components Module provides a high-performance alternative to the Vector Layer Module for production applications that need stable frame rates with large-scale maps. While it sacrifices some flexibility, the performance gains—particularly on mobile and low-end hardware—make it the preferred choice for performance-critical projects. + +For rapid prototyping or custom rendering workflows, the Vector Layer Module remains the better option. Choose the module that best fits your project's needs. diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs index 1d03a1f20..5e620ef53 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs @@ -66,18 +66,17 @@ private float ReadElevation(float x, float y, Vector4 terrainTextureScaleOffset) var sectionWidth = width * terrainTextureScaleOffset.x - 1; var padding = width * new Vector2(terrainTextureScaleOffset.z, terrainTextureScaleOffset.w); - var xx = padding.x + (x * sectionWidth); - var yy = padding.y + (y * sectionWidth); + var xx = padding.x + (Mathf.Clamp01(x) * sectionWidth); + var yy = padding.y + (Mathf.Clamp01(y) * sectionWidth); - var index = (int) yy * width - + (int) xx; - if (ElevationValues.Length <= index) + var index = (int) yy * width + (int) xx; + if (index >= ElevationValues.Length) { return 0; } else { - return ElevationValues[(int) yy * width + (int) xx]; + return ElevationValues[index]; } } diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs index fb076c0b1..c0578a6f9 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Tasks; using Mapbox.BaseModule.Data.Tiles; @@ -85,10 +86,7 @@ public bool TestAvailability() } } - private string TileToRelativeFilePath(CanonicalTileId tileId, string tilesetId) - { - return string.Format("{0}/{1}_{2}_{3}.{4}", MapIdToFolderName(tilesetId), tileId.X, tileId.Y, tileId.Z, FileExtension); - } + public virtual bool Exists(CanonicalTileId tileId, string mapId) { @@ -311,27 +309,45 @@ public InfoWrapper(MapboxTileData textureCacheItem, string path, Action } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string TileToRelativeFilePath(CanonicalTileId tileId, string tilesetId) + { + return string.Format("{0}/{1}_{2}_{3}.{4}", MapIdToFolderName(tilesetId), tileId.X, tileId.Y, tileId.Z, FileExtension); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string TileToToFileInfoExpects(CanonicalTileId tileId, string tilesetId) + { + var relativePath = string.Format("{0}/{1}_{2}_{3}.{4}", MapIdToFolderName(tilesetId), tileId.X, tileId.Y, tileId.Z, FileExtension); + return Path.GetFullPath(Path.Combine(PersistantCacheRootFolderPath, relativePath)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private string TileToRelativePath(MapboxTileData cacheItem) { return TileToRelativeFilePath(cacheItem.TileId, cacheItem.TilesetId); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public string TileToPathFileInfoExpects(MapboxTileData cacheItem) { return RelativeFilePathToFileInfoExpects(TileToRelativeFilePath(cacheItem.TileId, cacheItem.TilesetId)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public string TileToPathFileInfoExpects(CanonicalTileId tileId, string tilesetId) { return RelativeFilePathToFileInfoExpects(TileToRelativeFilePath(tileId, tilesetId)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public string RelativeFilePathToFileInfoExpects(string relativeFilePath) { var fullPath = Path.GetFullPath(Path.Combine(PersistantCacheRootFolderPath, relativeFilePath)); return fullPath; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public string RelativePathToUnityRequestExpects(string relativeFilePath) { var fullPath = Path.GetFullPath(Path.Combine(PersistantCacheRootFolderPath, relativeFilePath)); @@ -340,6 +356,7 @@ public string RelativePathToUnityRequestExpects(string relativeFilePath) return fullPath.Insert(0, "file://"); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private string FullFilePathToRelativePath(string fileInfoFullName) { return fileInfoFullName.Substring(PersistantCacheRootFolderPath.Length, diff --git a/Runtime/Mapbox/BaseModule/Data/Tasks/MeshGenTaskWrapper.cs b/Runtime/Mapbox/BaseModule/Data/Tasks/MeshGenTaskWrapper.cs index 16175d18f..59b22ad0d 100644 --- a/Runtime/Mapbox/BaseModule/Data/Tasks/MeshGenTaskWrapper.cs +++ b/Runtime/Mapbox/BaseModule/Data/Tasks/MeshGenTaskWrapper.cs @@ -4,11 +4,11 @@ namespace Mapbox.BaseModule.Data.Tasks { - public class MeshGenTaskWrapper : TaskWrapper + public class MeshGenTaskWrapper : TaskWrapper where T : MeshGenTaskWrapperResult, new() { - public MeshGenTaskWrapperResult DataResult; - public Func DataAction; - public Action DataCompleted; + public T DataResult; + public Func DataAction; + public Action DataCompleted; public override void Action() { @@ -20,18 +20,19 @@ public override void Completed(Task task) if (task != null && !task.IsFaulted) { DataCompleted(Task.CompletedTask, DataResult); + IsCompleted = true; return; } - + if (task == null) { - DataResult ??= new MeshGenTaskWrapperResult(); + DataResult ??= new T(); DataResult.ResultType = TaskResultType.Cancelled; DataCompleted(null, DataResult); } else { - DataResult ??= new MeshGenTaskWrapperResult(); + DataResult ??= new T(); DataResult.ResultType = TaskResultType.MeshGenerationFailure; DataCompleted(task, DataResult); } diff --git a/Runtime/Mapbox/BaseModule/Data/Tiles/CanonicalTileId.cs b/Runtime/Mapbox/BaseModule/Data/Tiles/CanonicalTileId.cs index 16b8a90e1..35366fc05 100644 --- a/Runtime/Mapbox/BaseModule/Data/Tiles/CanonicalTileId.cs +++ b/Runtime/Mapbox/BaseModule/Data/Tiles/CanonicalTileId.cs @@ -216,7 +216,7 @@ public static Vector4 CalculateScaleOffsetAtZoom(this CanonicalTileId current, i return new Vector4(scale, scale, offsetX, offsetY); } - public static Vector4 CalculateTopRightScaleOffsetAtZoom(this CanonicalTileId current, int zoomDiff) + public static Vector4 CalculateTopLeftScaleOffsetAtZoom(this CanonicalTileId current, int zoomDiff) { var tileZoom = current.Z; diff --git a/Runtime/Mapbox/BaseModule/Data/Vector2d/RectD.cs b/Runtime/Mapbox/BaseModule/Data/Vector2d/RectD.cs index 5103bbdd1..0e581d68e 100644 --- a/Runtime/Mapbox/BaseModule/Data/Vector2d/RectD.cs +++ b/Runtime/Mapbox/BaseModule/Data/Vector2d/RectD.cs @@ -2,18 +2,18 @@ { public struct RectD { - public Vector2d TopLeft { get; private set; } - public Vector2d BottomRight { get; private set; } + public Vector2d TopLeft; + public Vector2d BottomRight; //size is absolute width&height so Min+size != max - public Vector2d Size { get; private set; } - public Vector2d Center { get; private set; } + public Vector2d Size; + public Vector2d Center; public RectD(Vector2d topLeft, Vector2d size) { TopLeft = topLeft; - BottomRight = topLeft + size; + BottomRight = new Vector2d(topLeft.x + size.x, topLeft.y + size.y); //topLeft + size; Center = new Vector2d(TopLeft.x + size.x / 2, TopLeft.y + size.y / 2); - Size = new Vector2d(Mathd.Abs(size.x), Mathd.Abs(size.y)); + Size = size; //new Vector2d(Mathd.Abs(size.x), Mathd.Abs(size.y)); } public bool Contains(Vector2d point) diff --git a/Runtime/Mapbox/BaseModule/Map/MapInformation.cs b/Runtime/Mapbox/BaseModule/Map/MapInformation.cs index eb0e0550e..8f190e3d6 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapInformation.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapInformation.cs @@ -99,7 +99,7 @@ public virtual void SetInformation(LatitudeLongitude? latlng, float? zoom = null if(zoom.HasValue) Zoom = zoom.Value; if (pitch.HasValue) Pitch = pitch.Value; if (bearing.HasValue) Bearing = bearing.Value; - if (scale.HasValue) + if (scale.HasValue && !Mathf.Approximately(scale.Value, Scale)) { Scale = scale.Value; OnWorldScaleChanged(); diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMap.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMap.cs index 0317d4f29..82e3b4ae9 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMap.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMap.cs @@ -55,15 +55,12 @@ public IEnumerator Initialize() Initialized(); } - /// - /// Map update method, which we currently use per-frame, to recalculate the tile cover and run map visualizer on it. - /// - public void MapUpdated() + public void ForceRedraw() { MapService.TileCover(MapInformation, TileCover); MapVisualizer.Load(TileCover); } - + /// /// Provides a controlled method for jumping to specific locations on the map. /// Unlike standard frame-by-frame map updates, this method ensures precise @@ -133,7 +130,7 @@ public IEnumerator LoadMapViewCoroutine(LatitudeLongitude targetLocation, Action { Status = InitializationStatus.LoadingView; LoadViewStarting(); - MapInformation.SetInformation(targetLocation); + MapInformation.SetLatitudeLongitude(targetLocation); MapService.TileCover(MapInformation, TileCover); yield return MapVisualizer.LoadTileCoverToMemory(TileCover); @@ -157,9 +154,10 @@ public IEnumerator LoadTileCoverToMemory(TileCover cover) /// Zoom level of the map /// Pitch value of the camera /// Bearing value of the camera - public void ChangeView(LatitudeLongitude? latlng = null, float? zoom = null, float? pitch = null, float? bearing = null) + public void ChangeView(LatitudeLongitude? latlng = null, float? zoom = null, float? pitch = null, float? bearing = null, float? scale = null) { - MapInformation.SetInformation(latlng, zoom, pitch, bearing); + MapInformation.SetInformation(latlng, zoom, pitch, bearing, scale); + RedrawMap(); } public void OnDestroy() @@ -170,6 +168,17 @@ public void OnDestroy() public void UpdateTileCover() => MapService.TileCover(MapInformation, TileCover); + /// + /// Map redraw method, which we currently use on-demand, to recalculate the tile cover and run map visualizer on it. + /// + private void RedrawMap(IMapInformation mapInfo = null) + { + MapService.TileCover(mapInfo ?? MapInformation, TileCover); + MapVisualizer.Load(TileCover); + } + + + /// /// All modules are initialized, you can get&use any object and/or register to events safely. /// diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 3c04f2164..c9a6e3b42 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -18,23 +18,25 @@ public class MapboxMapVisualizer : IMapVisualizer { public List LayerModules; public Dictionary ActiveTiles { get; private set; } + public List TempTiles { get; private set; } protected UnityContext _unityContext; protected IMapInformation _mapInformation; protected ITileCreator _tileCreator; private HashSet _toRemove; private HashSet _retainedTiles; - private int _tilePerFrameLimit = 20; - private int _tileCreatedThisFrame = 0; + private Coroutine _internalUpdateCoroutine; + private bool _destroyed; public MapboxMapVisualizer(IMapInformation mapInformation, UnityContext unityContext, ITileCreator tileCreator) { _unityContext = unityContext; _mapInformation = mapInformation; - + ActiveTiles = new Dictionary(100); + TempTiles = new List(); LayerModules = new List(); - + _tileCreator = tileCreator; _tileCreator.OnTileBroken += (tt) => { @@ -45,9 +47,12 @@ public MapboxMapVisualizer(IMapInformation mapInformation, UnityContext unityCon }; _mapInformation.WorldScaleChanged += RepositionAllTiles; - + _mapInformation.LatitudeLongitudeChanged += RepositionAllTiles; + _toRemove = new HashSet(); _retainedTiles = new HashSet(); + + _internalUpdateCoroutine = Runnable.Instance.StartCoroutine(InternalUpdate()); } public virtual IEnumerator Initialize() @@ -63,10 +68,10 @@ public virtual IEnumerator Initialize() var terrainStrategy = new FlatTerrainStrategy(); yield return _tileCreator.Initialize(terrainStrategy); } - + yield return LayerModules.Select(x => x.Initialize()).WaitForAll(); } - + /// /// Prepare data and visuals for given tile cover. It loads the data to memory, generates vector feature visuals /// and prepare it all to ensure following tile requests will be finished in single frame. @@ -80,80 +85,131 @@ public virtual IEnumerator LoadTileCoverToMemory(TileCover tileCover) var coroutines = LayerModules.SelectMany(x => x.GetTileCoverCoroutines(hashsetTiles).Where(x => x != null)); yield return coroutines.WaitForAll(); } - - /// - /// Create the map in given tileCover area. Makes decision to load or unload tiles and handle temporary filler - /// tiles until actual tiles are loaded. - /// - /// + public virtual void Load(TileCover tileCover) { - _tileCreatedThisFrame = 0; - _toRemove.Clear(); - _retainedTiles.Clear(); - - foreach (var tile in ActiveTiles.Values) - { - _toRemove.Add(tile.UnwrappedTileId); - } + RemoveUnnecessaryTiles(tileCover); foreach (var tileId in tileCover.Tiles) { - _retainedTiles.Add(tileId.Canonical); - UnityMapTile unityMapTile = null; - _toRemove.Remove(tileId); - - if (ActiveTiles.TryGetValue(tileId, out unityMapTile)) + if (ActiveTiles.ContainsKey(tileId)) { - if (unityMapTile.IsTemporary) - { - CreateTile(unityMapTile); - } - - ShowTile(unityMapTile); continue; } - if (_tileCreatedThisFrame < _tilePerFrameLimit) + UnityMapTile unityMapTile = null; { if (CreateTileInstant(tileId, out unityMapTile)) { ShowTile(unityMapTile); - _tileCreatedThisFrame++; continue; } else { - var coveredByQuadrants = DelveInto(tileId, recursiveDepth: 1); + var activeChildren = new List(); + var coveredByQuadrants = DelveInto(tileId, activeChildren, recursiveDepth: 1); + CreateTempTile(tileId, out unityMapTile); + ActiveTiles.Add(tileId, unityMapTile); + TempTiles.Add(unityMapTile); + unityMapTile.Children = activeChildren; if (!coveredByQuadrants) { - CreateTempTile(tileId, out unityMapTile); ShowTile(unityMapTile); } } } } - + foreach (var tileId in _toRemove) { - //this tryget is unnecessary, just get it. it cannot not be there. if (ActiveTiles.TryGetValue(tileId, out var tile)) { - TileUnloading(tile); + if (tile.LoadingState == LoadingState.Temporary) + { + TempTiles.Remove(tile); + } + PoolTile(tile); } - else - { - Debug.LogError($"Could not find tile {tileId}"); - } } - + + _retainedTiles.Clear(); + foreach (var tile in tileCover.Tiles) + { + _retainedTiles.Add(tile.Canonical); + } foreach (var visualization in LayerModules) { visualization.RetainTiles(_retainedTiles); } } + + /// + /// Create the map in given tileCover area. Makes decision to load or unload tiles and handle temporary filler + /// tiles until actual tiles are loaded. + /// + public virtual void InternalUpdateCoroutine() + { + //finish temp tiles from tempTiles list + _toRemove.Clear(); + for (var index = TempTiles.Count - 1; index >= 0; index--) + { + var tilePair = TempTiles[index]; + if (!ActiveTiles.ContainsKey(tilePair.UnwrappedTileId)) + { + TempTiles.RemoveAt(index); + continue; + } + + if (tilePair.LoadingState == LoadingState.Temporary && CreateTile(tilePair)) + { + ShowTile(tilePair); + + if (tilePair.Children != null && tilePair.Children.Count > 0) + { + foreach (var child in tilePair.Children) + { + PoolTile(child); + } + tilePair.Children.Clear(); + } + + TempTiles.RemoveAt(index); + + var quadrants = new UnwrappedTileId[4] + { + tilePair.UnwrappedTileId.Quadrant(0), + tilePair.UnwrappedTileId.Quadrant(1), + tilePair.UnwrappedTileId.Quadrant(2), + tilePair.UnwrappedTileId.Quadrant(3), + }; + for (int i = 0; i < 4; i++) + { + var quadrant = quadrants[i]; + if (ActiveTiles.TryGetValue(quadrant, out var unityMapTile)) + { + _toRemove.Add(quadrant); + } + } + } + } + + foreach (var tileId in _toRemove) + { + if (ActiveTiles.TryGetValue(tileId, out var tile)) + { + if (tile.LoadingState == LoadingState.Temporary) + { + TempTiles.Remove(tile); + } + + PoolTile(tile); + } + } + } + + /// /// Minimal function that'll try to load view with whatever data is available. /// It will not unload any tiles, it will not trigger any data fetching. @@ -168,14 +224,25 @@ public void LoadSnapshot(TileCover tileCover) { UnityMapTile unityMapTile = null; if (!CreateTileInstant(tileId, out unityMapTile)) //if we can't fully load the tile + { CreateTempTile(tileId, out unityMapTile); //we load it whatever data we can find - + ActiveTiles.Add(tileId, unityMapTile); + TempTiles.Add(unityMapTile); + } + ShowTile(unityMapTile); } } public void OnDestroy() { + _destroyed = true; + if (_internalUpdateCoroutine != null && Runnable.Instance != null) + { + Runnable.Instance.StopCoroutine(_internalUpdateCoroutine); + _internalUpdateCoroutine = null; + } + foreach (var layerModule in LayerModules) { layerModule.OnDestroy(); @@ -204,11 +271,31 @@ public bool TryGetLayerModule(out T module) where T : ILayerModule } return module != null; } - - - - - protected bool DelveInto(UnwrappedTileId tileId, int recursiveDepth = 3) + + + private IEnumerator InternalUpdate() + { + while (!_destroyed) + { + InternalUpdateCoroutine(); + yield return null; + } + } + + private void RemoveUnnecessaryTiles(TileCover tileCover) + { + _toRemove.Clear(); + foreach (var tilePair in ActiveTiles) + { + if (!tileCover.Tiles.Contains(tilePair.Key) && tilePair.Value.LoadingState != LoadingState.Filler) + { + _toRemove.Add(tilePair.Key); + } + } + } + + + protected bool DelveInto(UnwrappedTileId tileId, List activeChildren, int recursiveDepth = 3) { var quadrantCheck = new bool[4] { false, false, false, false }; var quadrants = new UnwrappedTileId[4] @@ -224,7 +311,17 @@ protected bool DelveInto(UnwrappedTileId tileId, int recursiveDepth = 3) if (ActiveTiles.TryGetValue(quadrant, out var unityMapTile)) { _toRemove.Remove(quadrant); - //_retainedTiles.Add(quadrant.Canonical); + unityMapTile.LoadingState = LoadingState.Filler; + activeChildren.Add(unityMapTile); + if (unityMapTile.Children != null && unityMapTile.Children.Count > 0) + { + foreach (var subchild in unityMapTile.Children) + { + activeChildren.Add(subchild); + } + unityMapTile.Children.Clear(); + } + ShowTile(unityMapTile); quadrantCheck[i] = true; } @@ -236,7 +333,7 @@ protected bool DelveInto(UnwrappedTileId tileId, int recursiveDepth = 3) { if (quadrantCheck[i] == false && tileId.Z < 22) { - quadrantCheck[i] = DelveInto(quadrants[i], recursiveDepth - 1); + quadrantCheck[i] = DelveInto(quadrants[i], activeChildren, recursiveDepth - 1); } } } @@ -249,8 +346,10 @@ protected bool DelveInto(UnwrappedTileId tileId, int recursiveDepth = 3) if (quadrantCheck[i] == false) { CreateTempTile(quadrants[i], out var unityMapTile); - _mapInformation.PositionObjectFor(unityMapTile.gameObject, unityMapTile.CanonicalTileId); + unityMapTile.LoadingState = LoadingState.Filler; + ActiveTiles[quadrants[i]] = unityMapTile; ShowTile(unityMapTile); + activeChildren.Add(unityMapTile); quadrantCheck[i] = true; } } @@ -266,13 +365,27 @@ protected void ShowTile(UnityMapTile unityTile) unityTile.gameObject.SetActive(true); _mapInformation.PositionObjectFor(unityTile.gameObject, unityTile.CanonicalTileId); } - + protected void PoolTile(UnityMapTile tile) { + if (tile.LoadingState == LoadingState.None) + return; + + TileUnloading(tile); ActiveTiles.Remove(tile.UnwrappedTileId); tile.Recycle(); - tile.IsTemporary = false; + tile.LoadingState = LoadingState.None; _tileCreator.PutTile(tile); + + if (tile.Children != null) + { + foreach (var tileChild in tile.Children) + { + PoolTile(tileChild); + } + + tile.Children.Clear(); + } } protected void CreateTempTile(UnwrappedTileId tileId, out UnityMapTile tile) @@ -284,18 +397,19 @@ protected void CreateTempTile(UnwrappedTileId tileId, out UnityMapTile tile) { module.LoadTempTile(tile); } - - tile.IsTemporary = true; - ActiveTiles.Add(tileId, tile); + + tile.LoadingState = LoadingState.Temporary; + // ActiveTiles.Add(tileId, tile); + // TempTiles.Add(tile); } - + protected bool CreateTileInstant(UnwrappedTileId tileId, out UnityMapTile tile) { //we need to do positioning and scaling before mesh gen for now GetMapTile(tileId, out tile); var result = CreateTile(tile); - + //couldn't create the tile if (!result) PoolTile(tile); @@ -304,14 +418,15 @@ protected bool CreateTileInstant(UnwrappedTileId tileId, out UnityMapTile tile) protected void GetMapTile(UnwrappedTileId tileId, out UnityMapTile tile) { - var rectd = Conversions.TileBoundsInUnitySpace(tileId, _mapInformation.CenterMercator, _mapInformation.Scale); + var rectd = Conversions.TileBoundsInUnitySpace(tileId, _mapInformation.CenterMercator, + _mapInformation.Scale); tile = null; tile = _tileCreator.GetTile(); - tile.transform.position = new Vector3((float) rectd.Center.x, 0, (float) rectd.Center.y); - tile.transform.localScale = Vector3.one * (float) rectd.Size.x; - tile.Initialize(tileId, (float) rectd.Size.x * _mapInformation.Scale); + tile.transform.position = new Vector3((float)rectd.Center.x, 0, (float)rectd.Center.y); + tile.transform.localScale = Vector3.one * (float)rectd.Size.x; + tile.Initialize(tileId, (float)rectd.Size.x * _mapInformation.Scale); } - + protected bool CreateTile(UnityMapTile unityMapTile) { var tileFinished = true; @@ -324,11 +439,12 @@ protected bool CreateTile(UnityMapTile unityMapTile) if (tileFinished) { - unityMapTile.IsTemporary = false; + unityMapTile.LoadingState = LoadingState.Finished; if (!ActiveTiles.ContainsKey(unityMapTile.UnwrappedTileId)) { ActiveTiles.Add(unityMapTile.UnwrappedTileId, unityMapTile); } + TileLoaded(unityMapTile); } @@ -352,12 +468,13 @@ protected void RepositionAllTiles(IMapInformation mapInformation) module.UpdatePositioning(mapInformation); } } - + /// /// Map tile finished loading with targeted detail level data. This tile isn't temporary anymore, it'll be in /// ActiveTiles list. /// public event Action TileLoaded = (tile) => { }; + /// /// Map tile unloading event fires for tiles that are still in active tiles list but not in the latest tileCover. /// UnityMapTile object attached to event will be pooled after the event call. diff --git a/Runtime/Mapbox/BaseModule/Map/TileCreator.cs b/Runtime/Mapbox/BaseModule/Map/TileCreator.cs index b1116d925..a54fb6431 100644 --- a/Runtime/Mapbox/BaseModule/Map/TileCreator.cs +++ b/Runtime/Mapbox/BaseModule/Map/TileCreator.cs @@ -28,15 +28,17 @@ public class TileCreator : ITileCreator private int _cacheSize; //private FlatTerrainStrategy _flatTerrainStrategy; private TerrainStrategy _terrainStrategy; - + private bool _instanceMaterial; + public UnityMapTile GetTile() => _tilePool.GetObject(); public void PutTile(UnityMapTile tile) => _tilePool.Put(tile); - public TileCreator(UnityContext unityContext, Material[] tileMaterials = null, int cacheSize = 25) + public TileCreator(UnityContext unityContext, Material[] tileMaterials = null, int cacheSize = 25, bool instanceMaterials = true) { TileMaterials = tileMaterials; _unityContext = unityContext; _cacheSize = cacheSize; + _instanceMaterial = instanceMaterials; } public IEnumerator Initialize(TerrainStrategy terrainStrategy = null) @@ -55,12 +57,25 @@ private UnityMapTile CreateTile(UnityContext unityContext) tile.transform.SetParent(_unityContext.BaseTileRoot, false); } - if (TileMaterials?.Length > 0) + if (TileMaterials?.Length == 1) + { + if (_instanceMaterial) + { + tile.MeshRenderer.material = TileMaterials[0]; + tile.Material = tile.MeshRenderer.material; + } + else + { + tile.MeshRenderer.sharedMaterial = TileMaterials[0]; + tile.Material = tile.MeshRenderer.sharedMaterial; + } + } + else if (TileMaterials?.Length > 1) { tile.MeshRenderer.materials = TileMaterials; + tile.Material = tile.MeshRenderer.material; } - - tile.Material = tile.MeshRenderer.material; + tile.gameObject.SetActive(false); _terrainStrategy?.RegisterTile(tile, false); tile.OnDataDisposed += OnTileBroken; diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs index 3197ff51e..d13a1f432 100644 --- a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs @@ -11,13 +11,14 @@ public class TileCreatorBehaviour : MonoBehaviour [Tooltip("Materials for base map tile mesh and gameobject")] public Material[] TileMaterials; + public bool InstanceMaterials = true; public int CacheSize = 25; public ITileCreator GetTileCreator(UnityContext unityContext) { if (_tileCreator != null) return _tileCreator; - _tileCreator = new TileCreator(unityContext, TileMaterials, CacheSize); + _tileCreator = new TileCreator(unityContext, TileMaterials, CacheSize, InstanceMaterials); return _tileCreator; } } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs index 1d13fb7b7..796223e1f 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Mapbox.BaseModule.Data.Tiles; using Mapbox.BaseModule.Utilities; using UnityEngine; @@ -27,9 +28,13 @@ public class UnityMapTile : MonoBehaviour public Material Material; private MeshFilter _meshFilter; public MeshFilter MeshFilter => _meshFilter; + public List Children; + public int MeshVertexCount = 0; - public bool IsTemporary = false; + //public bool IsTemporary = false; + + public LoadingState LoadingState; public void Awake() { @@ -85,4 +90,12 @@ private void OnDestroy() VectorContainer.OnDestroy(); } } + + public enum LoadingState + { + None, + Temporary, + Finished, + Filler + } } diff --git a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs index c617415e7..dff5e827a 100644 --- a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs @@ -82,14 +82,6 @@ private void InitializationCompleted() Initialized(MapboxMap); MapboxMap.LoadMapView(); } - - private void Update() - { - if (InitializationStatus == InitializationStatus.ReadyForUpdates && _mapService.IsReady()) - { - MapboxMap.MapUpdated(); - } - } private void OnValidate() { @@ -106,8 +98,6 @@ private void OnDestroy() MapboxMap?.OnDestroy(); UnityContext.OnDestroy(); } - - protected virtual MapboxMap CreateMapObject() { diff --git a/Runtime/Mapbox/Example/Scripts/Map/SnapMapToTransform.cs b/Runtime/Mapbox/Example/Scripts/Map/SnapMapToTransform.cs index 40c811081..12a50a15f 100644 --- a/Runtime/Mapbox/Example/Scripts/Map/SnapMapToTransform.cs +++ b/Runtime/Mapbox/Example/Scripts/Map/SnapMapToTransform.cs @@ -20,7 +20,7 @@ void Update() if (_map != null && _map.Status >= InitializationStatus.ReadyForUpdates) { var latlng = _map.MapInformation.ConvertPositionToLatLng(Transform.position); - _map.MapInformation.SetInformation(latlng); + _map.ChangeView(latlng); } } } diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs index 88c724b10..fca6ef8b7 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs @@ -36,7 +36,7 @@ public void Initialize(Camera camera, IMapInformation start) Pitch = start.Pitch; Bearing = start.Bearing; ZoomValue = start.Zoom; - SetCamPositionByMapInfo(start); + SetCamera(start); start.LatitudeLongitudeChanged += information => { _targetPosition = Vector3.zero; @@ -97,7 +97,7 @@ public override bool UpdateCamera(IMapInformation mapInformation) //we probably shouldn't write to mapInformation here but do it in Moving3dCamBehaviour, along with pitch and bearing //mapInformation.ViewCenter = GetPlaneIntersection(Camera.ViewportToScreenPoint(new Vector3(0.5f, 0.5f))); - + if(hasChanged) SetCamPositionByMapInfo(); return hasChanged; } @@ -135,20 +135,24 @@ public void Zoom(IMapInformation mapInformation, Vector3 position, float zoomAct _targetPosition = Vector3.LerpUnclamped(postZoomTarget, postZoomPos, (camDistanceToMouse - newCamDistanceToMouse) / camDistanceToMouse); } - private void SetCamPositionByMapInfo(IMapInformation start) + private void SetCamPositionByMapInfo() { - CameraDistance = CalculateCameraDistance(start, start.Zoom); _camera.transform.position = _targetPosition; _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); _dragOrigin = GetPlaneIntersection(UnityEngine.Input.mousePosition); } - + public void SetCamera(IMapInformation mapInfo) { Pitch = mapInfo.Pitch; Bearing = mapInfo.Bearing; - SetCamPositionByMapInfo(mapInfo); + + CameraDistance = CalculateCameraDistance(mapInfo, mapInfo.Zoom); + _camera.transform.position = _targetPosition; + _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); + _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); + _dragOrigin = GetPlaneIntersection(UnityEngine.Input.mousePosition); } private Vector3 GetPlaneIntersection(Vector3 screenPosition) diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs index 486bba892..8be23d81b 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs @@ -31,7 +31,7 @@ public void Update() { if (_isInitialized && _map.MapInformation != null && Core.UpdateCamera(_map.MapInformation)) { - _map.MapInformation.SetInformation(null, Core.ZoomValue, Core.Pitch, Core.Bearing); + _map.ChangeView(null, Core.ZoomValue, Core.Pitch, Core.Bearing); } } diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs index eb5fb0c2f..eb3bf3bc5 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs @@ -32,6 +32,7 @@ private class ZoomSettings public bool ZoomAtCursor = true; } + public LatitudeLongitude CenterLatitudeLongitude; [Range(15, 90)] public float Pitch; [Range(-180, 180)] @@ -114,6 +115,7 @@ public override bool UpdateCamera(IMapInformation mapInformation) Pitch = Mathf.Min(90, Mathf.Max(15, Pitch)); var currentBearing = deltaAngleH * Time.deltaTime * _rotationSettings.Speed; Bearing += currentBearing; + hasChanged = true; } } else if (Input.mouseScrollDelta.magnitude > 0 && _zoomSettings.Enabled) @@ -137,18 +139,33 @@ public override bool UpdateCamera(IMapInformation mapInformation) hasChanged = true; } } - - mapInformation.SetInformation(Conversions.WebMercatorToLatLon(newMercatorCenter), null, Pitch, Bearing); + + CenterLatitudeLongitude = Conversions.WebMercatorToLatLon(newMercatorCenter); _dragOrigin = cursorHit; _previousScreenPosition = Input.mousePosition; + if(hasChanged) SetCamPositionByMapInfo(); + return hasChanged; } private void SetCamPositionByMapInfo() { _camera.transform.position = _targetPosition; - _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); + + if (_rotationSettings.rotationMode == RotationMode.RotateTheCamera) + { + _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); + } + else if (_rotationSettings.rotationMode == RotationMode.RotateTheMap) + { + var projectedForward = Vector3.ProjectOnPlane(_camera.transform.forward, _controlPlane.normal); + var verticalRotation = Quaternion.AngleAxis(Pitch-_initialPitch, projectedForward.Perpendicular()); + var vector = Quaternion.Euler(Pitch, Bearing, 0); + var worldVector = _camera.transform.rotation * vector; + _rotationSettings.MapRoot.rotation = verticalRotation * Quaternion.Euler(0, Bearing, 0); + } + _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); } diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs index afa88f5b1..f6ae469dc 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs @@ -28,8 +28,7 @@ public void Update() { if (_isInitialized && _map.MapInformation != null && Core.UpdateCamera(_map.MapInformation)) { - var eulerAngles = Camera.transform.eulerAngles; - _map.MapInformation.SetInformation(null, Core.ZoomValue, eulerAngles.x, eulerAngles.y, Core.ScaleValue); + _map.ChangeView(Core.CenterLatitudeLongitude, Core.ZoomValue, Core.Pitch, Core.Bearing, Core.ScaleValue); } } } diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs index ea1498ade..0727f9103 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs @@ -98,14 +98,6 @@ private void InitializationCompleted() }); } } - - private void Update() - { - if (InitializationStatus == InitializationStatus.ReadyForUpdates && _mapService.IsReady()) - { - MapboxMap.MapUpdated(); - } - } private void OnValidate() { diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingTaskManager.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingTaskManager.cs index 6691cdcee..fa06b3a6c 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingTaskManager.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingTaskManager.cs @@ -123,7 +123,7 @@ protected override void OnTaskFinished(TaskWrapper task) { base.OnTaskFinished(task); - if (task is MeshGenTaskWrapper meshTask) + if (task is MeshGenTaskWrapper meshTask) { var time = task.FinishedTime - task.StartingTime; _meshGenTaskCount++; diff --git a/Runtime/Mapbox/UnityMapService/MapUnityService.cs b/Runtime/Mapbox/UnityMapService/MapUnityService.cs index 2c1005706..d744d00bc 100644 --- a/Runtime/Mapbox/UnityMapService/MapUnityService.cs +++ b/Runtime/Mapbox/UnityMapService/MapUnityService.cs @@ -80,7 +80,12 @@ public override Source GetStaticRasterSource(ImageSourceSettings set public override Source GetVectorSource(VectorSourceSettings settings) { - var vectorSource = new VectorSource(FetchingManager, CacheManager, settings); + if (_dataSources.Any(s => s is VectorSource)) + { + return _dataSources.First(x => x is VectorSource) as Source; + } + + var vectorSource = new VectorSource(_fetchingManager, _cacheManager, settings); _dataSources.Add(vectorSource); return vectorSource; } diff --git a/Runtime/Mapbox/VectorModule/Visualizers.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers.meta diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings.meta diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/BuildingMaterial.mat b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/BuildingMaterial.mat similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/BuildingMaterial.mat rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/BuildingMaterial.mat diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/BuildingMaterial.mat.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/BuildingMaterial.mat.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/BuildingMaterial.mat.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/BuildingMaterial.mat.meta diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingLayer.asset b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingLayer.asset similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingLayer.asset rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingLayer.asset diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingLayer.asset.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingLayer.asset.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingLayer.asset.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingLayer.asset.meta diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingStack.asset b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingStack.asset similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingStack.asset rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingStack.asset diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingStack.asset.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingStack.asset.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingStack.asset.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingStack.asset.meta diff --git a/Runtime/Mapbox/VectorModule/CustomWorldObject.meta b/Runtime/Mapbox/VectorModule/ComponentSystem.meta similarity index 77% rename from Runtime/Mapbox/VectorModule/CustomWorldObject.meta rename to Runtime/Mapbox/VectorModule/ComponentSystem.meta index ba76bdd5a..ccf3eed8d 100644 --- a/Runtime/Mapbox/VectorModule/CustomWorldObject.meta +++ b/Runtime/Mapbox/VectorModule/ComponentSystem.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: cca07adf6752a48239648242c4cb168a +guid: fcaa3c666a2b64f55b6d3ec17506c1de folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent.meta new file mode 100644 index 000000000..e6962d394 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f5ab08040e331423e9e7fd64cbc1e69e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs new file mode 100644 index 000000000..1bdff9e54 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.ComponentSystem.Modifiers; +using Mapbox.VectorTile.Contants; +using Mapbox.VectorTile.Geometry; +using UnityEngine; +using DecodeGeometry = Mapbox.VectorModule.ComponentSystem.Data.DecodeGeometry; + +namespace Mapbox.VectorModule.ComponentSystem.AreaComponent +{ + public class AreaComponentVisualizer : MapboxComponentVisualizer + { + private AreaComponentSettings _settings; + + public AreaComponentVisualizer(string name, IMapInformation mapInformation, + UnityContext unityContext = null, AreaComponentSettings settings = null) : base(name, mapInformation, + unityContext) + { + _settings = settings ?? new AreaComponentSettings(); + _layerRootObject.transform.position = new Vector3( + _layerRootObject.transform.position.x, + _settings.GameObjectOffset, + _layerRootObject.transform.position.z); + } + + public override MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) + { + int classTagIndex = -1; + int typeTagIndex = -1; + for (var i = 0; i < layer.Keys.Count; i++) + { + var key = layer.Keys[i]; + if (key == "class") classTagIndex = i; + if (key == "type") typeTagIndex = i; + } + + var arrayPolygon = new ArrayPolygon(); + var featureCount = layer.FeatureCount(); + var info = new StackMeshInfo(featureCount); + var featureArray = new AreaFeatureUnity[featureCount]; + + for (var i = 0; i < featureCount; i++) + { + //for some reason passing ref values here effects performance quite a bit + var feature = GetFeature(layer, i, ref classTagIndex, ref typeTagIndex); + if (feature == null) continue; + featureArray[i] = feature; + + info.vertexRanges[i] = info.TotalPointCount; + var vertNeeded = feature.VertexData.VertexCount * 5; + info.vertexSize[i] = vertNeeded; + info.TotalPointCount += vertNeeded; + } + + var meshData = new MeshData(info, info.TotalPointCount); + + var poolSize = 10002; + var styleCount = _settings.StyleDictionary.Count; + var triangles = new List(); + var triIndices = new int[styleCount]; + var baseTriIndices = new int[styleCount]; + for (var i = 0; i < styleCount; i++) + { + triangles.Add(new int[poolSize]); + } + + AreaStyle style = null; + var retryCount = 0; + for (int i = 0; i < featureCount; i++) + { + var featureResult = featureArray[i]; + if (featureResult == null) + { + continue; + } + + if (_settings.LayerName == "water") + { + style = _settings.Styles[0]; + } + else if (!_settings.StyleDictionary.TryGetValue(featureResult.Class, out style)) + { + continue; + } + + var si = style.Index; + var featurePreTriIndex = triIndices[si]; + + var vertices = meshData.Vertices.AsSpan(meshData.MeshInfo.vertexRanges[i], meshData.MeshInfo.vertexSize[i]); + var normals = meshData.Normals.AsSpan(meshData.MeshInfo.vertexRanges[i], meshData.MeshInfo.vertexSize[i]); + var vertexAnchorIndex = meshData.MeshInfo.vertexRanges[i]; + + var triList = triangles[si]; + triIndices[si] = arrayPolygon.Polygonize(vertices, normals, vertexAnchorIndex, triList, triIndices[si], featureResult.VertexData, style.PushUp); + if (triIndices[si] == -1) + { + for (int j = featurePreTriIndex; j < triList.Length; j++) + { + triList[j] = 0; + } + + meshData.Triangles.Add(triList); + meshData.Materials.Add(style.Material); + triangles[si] = new int[poolSize]; + baseTriIndices[si] += triList.Length; + triIndices[si] = 0; + if (retryCount++ < 1) + { + i--; + continue; + } + // Degenerate feature — skip it + retryCount = 0; + continue; + } + retryCount = 0; + + info.triRanges[i] = baseTriIndices[si] + triIndices[si]; + } + + for (var i = 0; i < triangles.Count; i++) + { + var submeshTri = triangles[i]; + meshData.Triangles.Add(submeshTri); + meshData.Materials.Add(_settings.Styles[i].Material); + } + + return meshData; + } + + public override List CreateGo(CanonicalTileId tileId, MeshData meshData) + { + var objectList = new List(); + var entity = _buildingObjectPool.GetObject(); + var mats = new Material[meshData.Triangles.Count]; + for (var i = 0; i < meshData.Triangles.Count; i++) + { + mats[i] = meshData.Materials[i]; + } + + entity.MeshRenderer.materials = mats; + + entity.GameObject.transform.SetParent(_layerRootObject); + entity.StackId = 0; + + var mesh = entity.Mesh; + mesh.Clear(); + mesh.SetVertices(meshData.Vertices); + mesh.SetNormals(meshData.Normals); + mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; + + mesh.subMeshCount = meshData.Triangles.Count; + for (var index = 0; index < meshData.Triangles.Count; index++) + { + var submesh = meshData.Triangles[index]; + mesh.SetTriangles(submesh, index); + } + + entity.MeshFilter.sharedMesh = mesh; + objectList.Add(entity.GameObject); + + if (!_results.ContainsKey(tileId)) + _results.Add(tileId, new List()); + _results[tileId].Add(entity); + OnBuildingMeshCreated(tileId, entity.GameObject, meshData); + + return objectList; + } + + private AreaFeatureUnity GetFeature(VectorTileLayer layer, int i, ref int classTagIndex, ref int typeTagIndex) + { + var view = layer.GetViewFor(i); + var layerData = layer.Data.Slice(view.x, view.y); + var featureReader = new PbfReader(layerData); + var feature = new AreaFeatureUnity(); + bool geomTypeSet = false; + while (featureReader.NextByte()) + { + int featureType = featureReader.Tag; + switch ((FeatureType)featureType) + { + case FeatureType.Id: + feature.Id = (ulong)featureReader.Varint(); + break; + case FeatureType.Tags: + var tags = featureReader.GetPackedInt(); + feature.Tags = tags; + break; + case FeatureType.Type: + int geomType = (int)featureReader.Varint(); + feature.GeometryType = (GeomType)geomType; + geomTypeSet = true; + break; + case FeatureType.Geometry: + if (null != feature.GeometryCommands) + { + throw new System.Exception(string.Format("Layer [{0}], feature already has a geometry", + layer.Name)); + } + + //get raw array of commands and coordinates + feature.GeometryCommands = featureReader.GetPackedUnit32(); + break; + default: + featureReader.Skip(); + break; + } + } + + // Discard features with missing/malformed geometry + if (feature.GeometryCommands == null) return null; + + feature.SetProperties(ref layer, ref classTagIndex, ref typeTagIndex); + feature.VertexData = feature.Geometry(new Vector3(layer.Extent, 0, -layer.Extent)); + return feature; + } + + private class AreaFeatureUnity + { + public ulong Id; + public GeomType GeometryType; + public uint[] GeometryCommands; + public FeatureVertexData VertexData; + public int[] Tags; + public string Class; + public string Type; + + public void SetProperties(ref VectorTileLayer layer, ref int classTagIndex, ref int typeTagIndex) + { + if (Tags == null) return; //water has no tags + + var tagCount = Tags.Length; + //some features have odd number of tags + //not sure if it's a bug or data issue + //so -1 here to skip last single tag + for (int i = 0; i < tagCount - 1; i += 2) + { + if (classTagIndex != -1 && Tags[i] == classTagIndex) + { + Class = layer.Values[Tags[i + 1]].ToString(); + } + else if (typeTagIndex != -1 && Tags[i] == typeTagIndex) + { + Type = layer.Values[Tags[i + 1]].ToString(); + } + } + } + + public FeatureVertexData Geometry(Vector3 scale) + { + return DecodeGeometry.GetGeometry(GeometryCommands, scale); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs.meta new file mode 100644 index 000000000..cf938a52e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d4f563de806d4e479e7b7222ecb14a81 +timeCreated: 1765306883 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs new file mode 100644 index 000000000..ee89442f1 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.Unity; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.AreaComponent +{ + [DisplayName("Area Component")] + [CreateAssetMenu(menuName = "Mapbox/Layer Visualizers/Area Component Visualizer")] + public class AreaComponentVisualizerObject : LayerVisualizerConstructor, IPerformanceLayerVisualizer + { + public AreaComponentSettings Settings; + public bool IsActive = true; + + private AreaComponentVisualizer _layerVisualizer; + + public IVectorLayerVisualizer GetLayerVisualizer() + { + return _layerVisualizer; + } + + public override IVectorLayerVisualizer ConstructLayerVisualizer(IMapInformation mapInformation, UnityContext unityContext) + { + Settings.StyleDictionary = new Dictionary(); + var index = 0; + foreach (var areaStyle in Settings.Styles) + { + areaStyle.Index = index; + Settings.StyleDictionary.Add(areaStyle.ClassName, areaStyle); + index++; + } + + _layerVisualizer = new AreaComponentVisualizer(Settings.LayerName, mapInformation, unityContext, Settings); + _layerVisualizer.Active = IsActive; + + _layerVisualizer.OnVectorMeshCreated += OnVectorMeshCreated; + _layerVisualizer.OnVectorMeshDestroyed += OnVectorMeshDestroyed; + + return _layerVisualizer; + } + + public Action OnVectorMeshCreated = list => { }; + public Action OnVectorMeshDestroyed = go => { }; + } + + [Serializable] + public class AreaComponentSettings + { + public string LayerName; + [Tooltip("Vertical offset to be applied to the road component.")] + public float GameObjectOffset; + public Dictionary StyleDictionary = new Dictionary(); + public List Styles; + } + + [Serializable] + public class AreaStyle + { + [NonSerialized] public int Index; + public string ClassName; + public Material Material; + public float PushUp; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs.meta new file mode 100644 index 000000000..4b230537a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6183aeb8971b45778ba9d2c9fc425433 +timeCreated: 1765306532 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer.meta new file mode 100644 index 000000000..dd5e49fa0 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2358e8de79534423d98715fd47be3057 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs new file mode 100644 index 000000000..a2e466a52 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs @@ -0,0 +1,93 @@ +using System; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer +{ + public interface IHeightFilter + { + void FilterHeight(ref float height, ref float minHeight); + } + + [Serializable] + public class BasicExtrusionSettings : IHeightFilter + { + public ExtrusionOptions ExtrusionOptions = new ExtrusionOptions(); + public void FilterHeight(ref float height, ref float minHeight) + { + switch (ExtrusionOptions.extrusionType) + { + case ExtrusionType.AbsoluteHeight: + height = ExtrusionOptions.maximumHeight; + minHeight = 0; + break; + case ExtrusionType.MaxHeight: + height = Mathf.Min(ExtrusionOptions.maximumHeight, height); + minHeight = 0; + break; + case ExtrusionType.MinHeight: + height = Mathf.Max(ExtrusionOptions.minimumHeight, height); + minHeight = 0; + break; + case ExtrusionType.RangeHeight: + height = Mathf.Clamp(height, ExtrusionOptions.minimumHeight, ExtrusionOptions.maximumHeight); + minHeight = 0; + break; + default: + break; + } + + height *= ExtrusionOptions.extrusionScaleFactor; + minHeight *= ExtrusionOptions.extrusionScaleFactor; + } + } + + [Serializable] + public class ChamferSettings : IHeightFilter + { + public float OffsetInMeters; + public ExtrusionOptions ExtrusionOptions = new ExtrusionOptions(); + public void FilterHeight(ref float height, ref float minHeight) + { + switch (ExtrusionOptions.extrusionType) + { + case ExtrusionType.AbsoluteHeight: + height = ExtrusionOptions.maximumHeight; + minHeight = 0; + break; + case ExtrusionType.MaxHeight: + height = Mathf.Min(ExtrusionOptions.maximumHeight, height); + minHeight = 0; + break; + case ExtrusionType.MinHeight: + height = Mathf.Max(ExtrusionOptions.minimumHeight, height); + minHeight = 0; + break; + case ExtrusionType.RangeHeight: + height = Mathf.Clamp(height, ExtrusionOptions.minimumHeight, ExtrusionOptions.maximumHeight); + minHeight = 0; + break; + case ExtrusionType.None: + case ExtrusionType.PropertyHeight: + default: + break; + } + + height *= ExtrusionOptions.extrusionScaleFactor; + minHeight *= ExtrusionOptions.extrusionScaleFactor; + } + } + + [Serializable] + public class ExtrusionOptions + { + public ExtrusionType extrusionType = ExtrusionType.PropertyHeight; + [Tooltip("Property name in feature layer to use for extrusion.")] + public string propertyName = "height"; + public float minimumHeight = 0f; + public float maximumHeight = 1000f; + [Tooltip("Scale factor to multiply the extrusion value of the feature.")] + public float extrusionScaleFactor = 1f; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs.meta new file mode 100644 index 000000000..1f7d9558a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2b834d2c97f344fba545a6185641da07 +timeCreated: 1765384723 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs new file mode 100644 index 000000000..cc0a8dccb --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.ComponentSystem.Modifiers; +using Mapbox.VectorTile.Contants; +using Mapbox.VectorTile.Geometry; +using UnityEngine; +using DecodeGeometry = Mapbox.VectorModule.ComponentSystem.Data.DecodeGeometry; + +namespace Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer +{ + public class BuildingComponentVisualizer : MapboxComponentVisualizer + { + private BuildingComponentSettings _buildingSettings; + + public BuildingComponentVisualizer(string name, IMapInformation mapInformation, + UnityContext unityContext = null, BuildingComponentSettings settings = null) : base(name, mapInformation, + unityContext) + { + _buildingSettings = settings ?? new BuildingComponentSettings(); + } + + public override MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) + { + int heightTagIndex = -1; + int minHeightTagIndex = -1; + int extrudeTagIndex = -1; + + for (var i = 0; i < layer.Keys.Count; i++) + { + var key = layer.Keys[i]; + if (key == "height") heightTagIndex = i; + if (key == "min_height") minHeightTagIndex = i; + if (key == "extrude") extrudeTagIndex = i; + } + + var arrayPolygon = new ArrayPolygon(); + ArraySnapTerrain arraySnapTerrain = null; + if (_buildingSettings.EnableTerrainSnapping) + { + arraySnapTerrain = new ArraySnapTerrain(_mapInformation); + } + IPerformanceExtrusion arrayHeight; + IHeightFilter settings; + if (_buildingSettings.RoundBuildingCorners) + { + arrayHeight = new ArrayChamferHeight(_buildingSettings.ChamferExtrusionSettings); + settings = _buildingSettings.ChamferExtrusionSettings; + } + else + { + arrayHeight = new ArrayHeight(_buildingSettings.BasicExtrusionSettings); + settings = _buildingSettings.BasicExtrusionSettings; + } + + var featureCount = layer.FeatureCount(); + var info = new StackMeshInfo(featureCount); + + var tileSize = Conversions.TileSizeInUnitySpace(tileId.Z, _mapInformation.Scale); + var featureArray = new BuildingFeatureUnity[featureCount]; + + for (var i = 0; i < featureCount; i++) + { + //for some reason passing ref values here effects performance quite a bit + var feature = GetFeature(layer, i, ref heightTagIndex, ref minHeightTagIndex, ref extrudeTagIndex); + if (feature == null) continue; + featureArray[i] = feature; + + info.vertexRanges[i] = info.TotalPointCount; + var vertNeeded = feature.VertexData.VertexCount * 5; + info.vertexSize[i] = vertNeeded; + info.TotalPointCount += vertNeeded; + } + + var meshData = new MeshData(info, info.TotalPointCount); + + var poolSize = 10002; + var triList = new int[poolSize]; + var triIndex = 0; + var baseTriIndex = 0; + var retryCount = 0; + for (int i = 0; i < featureCount; i++) + { + var featureResult = featureArray[i]; + if (featureResult == null || !featureResult.DoExtrude) + { + //ArrayPool.Shared.Return(featureResult.VertexData.Vertices); + continue; + } + + var featurePreTriIndex = triIndex; + + var vertices = meshData.Vertices.AsSpan(meshData.MeshInfo.vertexRanges[i], meshData.MeshInfo.vertexSize[i]); + var normals = meshData.Normals.AsSpan(meshData.MeshInfo.vertexRanges[i], meshData.MeshInfo.vertexSize[i]); + var vertexAnchorIndex = meshData.MeshInfo.vertexRanges[i]; + + triIndex = arrayPolygon.Polygonize(vertices, normals, vertexAnchorIndex, triList, triIndex, featureResult.VertexData); + if (triIndex == -1) + { + for (int j = featurePreTriIndex; j < triList.Length; j++) + { + triList[j] = 0; + } + + meshData.Triangles.Add(triList); + baseTriIndex += triList.Length; + triList = new int[poolSize]; + triIndex = 0; + if (retryCount++ < 1) + { + i--; + continue; + } + // Degenerate feature — skip it + retryCount = 0; + continue; + } + retryCount = 0; + + if (_buildingSettings.EnableTerrainSnapping) + { + arraySnapTerrain.SnapTerrain(vertices, vertices.Length / 5, tileId, tileSize); + } + + var triSpaceRequired = arrayHeight.CalculateTriCountFor(featureResult.VertexData.VertexCount); + if (triSpaceRequired + triIndex >= triList.Length) + { + for (int j = featurePreTriIndex; j < triList.Length; j++) + { + triList[j] = 0; + } + + meshData.Triangles.Add(triList); + baseTriIndex += triList.Length; + triList = new int[triSpaceRequired + triIndex + 3]; + triIndex = 0; + if (retryCount++ < 1) + { + i--; + continue; + } + // Feature requires more space than expected — skip it + retryCount = 0; + continue; + } + else + { + var height = featureResult.Height; + var minHeight = featureResult.MinHeight; + settings.FilterHeight(ref height, ref minHeight); + + triIndex = arrayHeight.Run(vertices, + normals, + vertexAnchorIndex, + triList, + triIndex, + height, + minHeight, + featureResult.VertexData, + tileSize, + _mapInformation.Scale); + } + + info.triRanges[i] = baseTriIndex + triIndex; + //ArrayPool.Shared.Return(featureResult.VertexData.Vertices); + } + + if (triIndex < triList.Length) + { + Array.Resize(ref triList, triIndex); + } + meshData.Triangles.Add(triList); + return meshData; + } + + public override List CreateGo(CanonicalTileId tileId, MeshData meshData) + { + var objectList = new List(); + var entity = _buildingObjectPool.GetObject(); + var mats = new Material[meshData.Triangles.Count]; + for (int i = 0; i < meshData.Triangles.Count; i++) + { + mats[i] = _buildingSettings.Material; + } + + entity.MeshRenderer.materials = mats; + + entity.GameObject.transform.SetParent(_layerRootObject); + entity.StackId = 0; + + var mesh = entity.Mesh; + mesh.Clear(); + mesh.SetVertices(meshData.Vertices); + mesh.SetNormals(meshData.Normals); + mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; + + mesh.subMeshCount = meshData.Triangles.Count; + for (var index = 0; index < meshData.Triangles.Count; index++) + { + var submesh = meshData.Triangles[index]; + mesh.SetTriangles(submesh, index); + } + + entity.MeshFilter.sharedMesh = mesh; + objectList.Add(entity.GameObject); + + if (!_results.ContainsKey(tileId)) + _results.Add(tileId, new List()); + _results[tileId].Add(entity); + OnBuildingMeshCreated(tileId, entity.GameObject, meshData); + + return objectList; + } + + private BuildingFeatureUnity GetFeature(VectorTileLayer layer, int i, ref int heightTagIndex, ref int minHeightTagIndex, ref int extrudeTagIndex) + { + var view = layer.GetViewFor(i); + var layerData = layer.Data.Slice(view.x, view.y); + var featureReader = new PbfReader(layerData); + var feature = new BuildingFeatureUnity(); + while (featureReader.NextByte()) + { + int featureType = featureReader.Tag; + switch ((FeatureType)featureType) + { + case FeatureType.Id: + feature.Id = (ulong)featureReader.Varint(); + break; + case FeatureType.Tags: + var tags = featureReader.GetPackedInt(); + feature.Tags = tags; + break; + case FeatureType.Type: + int geomType = (int)featureReader.Varint(); + feature.GeometryType = (GeomType)geomType; + break; + case FeatureType.Geometry: + if (null != feature.GeometryCommands) + { + throw new System.Exception(string.Format("Layer [{0}], feature already has a geometry", + layer.Name)); + } + + //get raw array of commands and coordinates + feature.GeometryCommands = featureReader.GetPackedUnit32(); + break; + default: + featureReader.Skip(); + break; + } + } + + // Discard features with missing/malformed geometry + if (feature.GeometryCommands == null) return null; + + feature.SetProperties(ref layer, ref heightTagIndex, ref minHeightTagIndex, ref extrudeTagIndex); + feature.VertexData = feature.Geometry(new Vector3(layer.Extent, 0, -layer.Extent)); + return feature; + } + + private class BuildingFeatureUnity + { + public ulong Id; + public GeomType GeometryType; + public uint[] GeometryCommands; + public FeatureVertexData VertexData; + public int[] Tags; + public float Height; + public float MinHeight; + public bool DoExtrude; + + public void SetProperties(ref VectorTileLayer layer, ref int heightTagIndex, ref int minHeightTagIndex, ref int extrudeTagIndex) + { + if (Tags == null) return; // Some features have no tags + + var tagCount = Tags.Length; + + for (int i = 0; i < tagCount - 1; i += 2) + { + if (heightTagIndex != -1 && Tags[i] == heightTagIndex) + { + Height = Convert.ToSingle(layer.Values[Tags[i + 1]]); + } + else if (minHeightTagIndex != -1 && Tags[i] == minHeightTagIndex) + { + MinHeight = Convert.ToSingle(layer.Values[Tags[i + 1]]); + } + else if (extrudeTagIndex != -1 && Tags[i] == extrudeTagIndex) + { + DoExtrude = Convert.ToBoolean(layer.Values[Tags[i + 1]]); + } + } + } + + public FeatureVertexData Geometry(Vector3 scale) + { + return DecodeGeometry.GetGeometry(GeometryCommands, scale); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs.meta new file mode 100644 index 000000000..19e823755 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 307aae1ded10457cbaa880223de909fb +timeCreated: 1764598894 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs new file mode 100644 index 000000000..00fcc5c73 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs @@ -0,0 +1,64 @@ +using System; +using System.ComponentModel; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using Mapbox.VectorModule.Unity; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer +{ + [DisplayName("Building Component")] + [CreateAssetMenu(menuName = "Mapbox/Layer Visualizers/Buildings Component Visualizer")] + public class BuildingComponentVisualizerObject : LayerVisualizerConstructor, IPerformanceLayerVisualizer + { + public BuildingComponentSettings Settings; + public bool IsActive = true; + + private BuildingComponentVisualizer _layerVisualizer; + + public IVectorLayerVisualizer GetLayerVisualizer() + { + return _layerVisualizer; + } + + public override IVectorLayerVisualizer ConstructLayerVisualizer(IMapInformation mapInformation, UnityContext unityContext) + { + _layerVisualizer = new BuildingComponentVisualizer("building", mapInformation, unityContext, Settings); + _layerVisualizer.Active = IsActive; + + _layerVisualizer.OnVectorMeshCreated += OnVectorMeshCreated; + _layerVisualizer.OnVectorMeshDestroyed += OnVectorMeshDestroyed; + + return _layerVisualizer; + } + + public Action OnVectorMeshCreated = list => { }; + public Action OnVectorMeshDestroyed = go => { }; + } + + [Serializable] + public class BuildingComponentSettings + { + public bool EnableTerrainSnapping = false; + public bool RoundBuildingCorners = false; + public Material Material; + + public ChamferSettings ChamferExtrusionSettings; + public BasicExtrusionSettings BasicExtrusionSettings; + + public BuildingComponentSettings() + { + EnableTerrainSnapping = false; + ChamferExtrusionSettings = new ChamferSettings() { OffsetInMeters = 1 }; + BasicExtrusionSettings = new BasicExtrusionSettings(); + } + } + + //necessary of editor script stuff + public interface IPerformanceLayerVisualizer + { + + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs.meta new file mode 100644 index 000000000..87e5cedd0 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c6eb41c8ba1c475ab45d048dafafaf77 +timeCreated: 1744829680 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef b/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef new file mode 100644 index 000000000..b44f9e65e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef @@ -0,0 +1,17 @@ +{ + "name": "MapboxComponentSystem", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:2400806fb903448e5b7e737b92f1e434" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef.meta new file mode 100644 index 000000000..2aeaacf0b --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5aef8304858a8459cb2204d15bf75ab9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data.meta new file mode 100644 index 000000000..d48e34fe5 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a2be5986f68b541ddaa69836e6c78f13 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs new file mode 100644 index 000000000..e481f4b8a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs @@ -0,0 +1,136 @@ +using System.Runtime.CompilerServices; +using Mapbox.VectorTile.Contants; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public static class DecodeGeometry + { + public static FeatureVertexData GetGeometry(uint[] geometryCommands, Vector3 scale) + { + var vertexData = new FeatureVertexData(); + vertexData.Vertices = new Vector3[geometryCommands.Length / 2]; //ArrayPool.Shared.Rent(geometryCommands.Length / 2); + + int geomCmdCnt = geometryCommands.Length; + float cursorX = 0; + float cursorY = 0; + var index = 0; + for (int i = 0; i < geomCmdCnt; i++) + { + uint g = geometryCommands[i]; + Commands cmd = (Commands)(g & 0x7); + uint cmdCount = g >> 3; + + if (cmd == Commands.MoveTo || cmd == Commands.LineTo) + { + for (int j = 0; j < cmdCount; j++) + { + // ZigzagDecode(geometryCommands[i + 1], geometryCommands[i + 2], out var dx, out var dy); + // cursorX += (dx / scale.x); + // cursorY += (dy / scale.z); + //small micro optimization to push changes into method below to prevent new allocation + ModifyWithZigzagDecode(geometryCommands[i + 1], geometryCommands[i + 2], ref scale.x, ref scale.z, ref cursorX, ref cursorY); + + i += 2; + //end of part of multipart feature + if (cmd == Commands.MoveTo) + { + vertexData.Submeshes.Add(index); + } + + var pntTmp = new Vector3(cursorX, 0, cursorY); + // { + // x = cursorX * scaleOffset[0] + scaleOffset[2], + // y = 0, + // z = cursorY * scaleOffset[1] - scaleOffset[3] + // }; + vertexData.Vertices[index++] = pntTmp; + } + } + } + + vertexData.Submeshes.Add(index); + vertexData.VertexCount = index; + return vertexData; + } + + public static FeatureVertexData GetGeometry(uint[] geometryCommands, Vector3 scale, Vector4 scaleOffset) + { + var vertexData = new FeatureVertexData(); + vertexData.Vertices = new Vector3[geometryCommands.Length / 2]; //ArrayPool.Shared.Rent(geometryCommands.Length / 2); + + int geomCmdCnt = geometryCommands.Length; + float cursorX = 0; + float cursorY = 0; + var index = 0; + for (int i = 0; i < geomCmdCnt; i++) + { + uint g = geometryCommands[i]; + Commands cmd = (Commands)(g & 0x7); + uint cmdCount = g >> 3; + + if (cmd == Commands.MoveTo || cmd == Commands.LineTo) + { + for (int j = 0; j < cmdCount; j++) + { + // ZigzagDecode(geometryCommands[i + 1], geometryCommands[i + 2], out var dx, out var dy); + // cursorX += (dx / scale.x); + // cursorY += (dy / scale.z); + //small micro optimization to push changes into method below to prevent new allocation + ModifyWithZigzagDecode(geometryCommands[i + 1], geometryCommands[i + 2], ref scale.x, ref scale.z, ref cursorX, ref cursorY); + i += 2; + //end of part of multipart feature + if (cmd == Commands.MoveTo) + { + vertexData.Submeshes.Add(index); + } + + var pntTmp = new Vector3() + { + x = cursorX * scaleOffset[0] + scaleOffset[2], + y = 0, + z = cursorY * scaleOffset[1] - scaleOffset[3] + }; + vertexData.Vertices[index++] = pntTmp; + } + } + } + + vertexData.Submeshes.Add(index); + vertexData.VertexCount = index; + return vertexData; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ZigzagDecode(uint x, uint y, out float dx, out float dy) + { + dx = (x >> 1) ^ (-(int)(x & 1)); + dy = (y >> 1) ^ (-(int)(y & 1)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ModifyWithZigzagDecode(uint x, uint y, ref float scaleX, ref float scaleZ, ref float cursorX, ref float cursorY) + { + cursorX += (((x >> 1) ^ (-(int)(x & 1))) / scaleX); + cursorY += (((y >> 1) ^ (-(int)(y & 1))) / scaleZ); + } + + private static Vector2 ZigzagDecode(uint x, uint y) + { + + //TODO: verify speed improvements using + // new Point2d(){X=x, Y=y} instead of + // new Point3d(x, y); + + //return new Point2d( + // ((x >> 1) ^ (-(x & 1))), + // ((y >> 1) ^ (-(y & 1))) + //); + return new Vector2 + { + x = ((x >> 1) ^ (-(x & 1))), + y = ((y >> 1) ^ (-(y & 1))) + }; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs.meta new file mode 100644 index 000000000..ca769afb4 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a0f9f17212154a838a0b291e9bcf6e1f +timeCreated: 1756392359 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs new file mode 100644 index 000000000..583b556dc --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public class FeatureVertexData + { + public Vector3[] Vertices; + public List Submeshes = new List(4); + public int VertexCount; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs.meta new file mode 100644 index 000000000..f160fe1ac --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 424f4ff2d911468185a807064987b145 +timeCreated: 1756394988 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs new file mode 100644 index 000000000..6b620dc4a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public class MeshData + { + public Vector3[] Vertices; + public Vector3[] Normals; + public Vector2[] UVs; + public List Triangles; + public StackMeshInfo MeshInfo; + public List Materials; + + public MeshData(StackMeshInfo meshInfo, int featureCount) + { + MeshInfo = meshInfo; + Vertices = new Vector3[featureCount]; + Normals = new Vector3[featureCount]; + Triangles = new List(15); + Materials = new List(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs.meta new file mode 100644 index 000000000..6cec4948a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7b5f421d206748e7ac96e8e3978286e0 +timeCreated: 1756394423 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs new file mode 100644 index 000000000..9de01b0e0 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Mapbox.VectorTile.Contants; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public ref struct PbfReader + { + /// Tag at current position + public int Tag; + + /// Value at current position + public ulong Value; + //public ulong Pos { get; private set; } + /// Wire type at current position + public WireTypes WireType; + + + private ReadOnlySpan _buffer; + private int _length; + private int _pos; + + + /// + /// PbfReader constructor + /// + /// Byte array containing the raw (already unzipped) tile data + public PbfReader(ReadOnlySpan tileBuffer) + { + _buffer = tileBuffer; + _length = _buffer.Length; + WireType = WireTypes.UNDEFINED; + _pos = 0; + Tag = 0; + Value = 0; + } + + + /// + /// Gets Varint at current position, moves to position after Varint. + /// Throws exception if Varint cannot be decoded + /// + /// Decoded Varint + public long Varint() + { + // convert to base 128 varint + // https://developers.google.com/protocol-buffers/docs/encoding + int shift = 0; + long result = 0; + while (shift < 64) + { + byte b = _buffer[_pos]; + result |= (long)(b & 0x7F) << shift; + _pos++; + if ((b & 0x80) == 0) + { + return result; + } + shift += 7; + } + throw new System.ArgumentException("Invalid varint"); + + } + + + /// + /// Get a view into the buffer. + /// TODO: refactor to return a DataView instead of a byte array + /// + /// Byte array containing the view + public Vector2Int View() + { + // return layer/feature subsections of the main stream + if (Tag == 0) + { + throw new System.Exception("call next() before accessing field value"); + }; + if (WireType != WireTypes.BYTES) + { + throw new System.Exception("not of type string, bytes or message"); + } + + var skipBytes = Varint(); + SkipBytes((int)skipBytes); + + //byte[] buf = new byte[skipBytes]; + //System.Array.Copy(_buffer, (int)_pos - (int)skipBytes, buf, 0, (int)skipBytes); + + return new Vector2Int((int)_pos - (int)skipBytes, (int)skipBytes); + } + + + /// + /// Get repeated `uint`s a current position, move position + /// + /// List of decoded `uint`s + public uint[] GetPackedUnit32() + { + List values = new List(32); + var sizeInByte = Varint(); + var end = _pos + sizeInByte; + while (_pos < end) + { + values.Add((uint)Varint()); + } + return values.ToArray(); + } + + public int[] GetPackedInt() + { + List values = new List(32); + var sizeInByte = Varint(); + var end = _pos + sizeInByte; + while (_pos < end) + { + values.Add((int)Varint()); + } + return values.ToArray(); + } + + + public List GetPackedSInt32() + { + List values = new List(200); + var sizeInByte = Varint(); + var end = _pos + sizeInByte; + while (_pos < end) + { + values.Add(decodeZigZag32((int)Varint())); + } + return values; + } + + + public List GetPackedSInt64() + { + List values = new List(200); + var sizeInByte = Varint(); + var end = _pos + sizeInByte; + while (_pos < end) + { + values.Add(decodeZigZag64((long)Varint())); + } + return values; + } + + + private int decodeZigZag32(int value) + { + return (value >> 1) ^ -(value & 1); + } + + + private long decodeZigZag64(long value) + { + return (value >> 1) ^ -(value & 1); + } + + + /// + /// Get double at current position, move to next position + /// + /// Decoded double + public double GetDouble() + { + double dblVal = System.BitConverter.ToDouble(_buffer.Slice(_pos, 8)); + _pos += 8; + return dblVal; + } + + + /// + /// Get float a current position, move to next position + /// + /// Decoded float + public float GetFloat() + { + float snglVal = System.BitConverter.ToSingle(_buffer.Slice(_pos, 4)); + _pos += 4; + return snglVal; + } + + + /// + /// Get bytes as string + /// + /// Number of bytes to read + /// Decoded string + public string GetString(int length) + { + var str = Encoding.UTF8.GetString(_buffer.Slice(_pos, length)); + _pos += length; + return str; + } + + + /// + /// Move to next byte and set wire type. Throws exeception if tag is out of range + /// + /// Returns false if at end of buffer + public bool NextByte() + { + if (_pos >= _length) + { + return false; + } + // get and process the next byte in the buffer + // return true until end of stream + Value = (ulong)Varint(); + Tag = (int)Value >> 3; + if ( + (Tag == 0 || Tag >= 19000) + && (Tag > 19999 || Tag <= ((1 << 29) - 1)) + ) + { + throw new System.Exception("tag out of range"); + } + WireType = (WireTypes)(Value & 0x07); + return true; + } + + + /// + /// Skip over a Varint + /// + public void SkipVarint() + { + Varint(); + //while (0 == (_buffer[Pos] & 0x80)) + //{ + // Pos++; + // if (Pos >= _length) + // { + // throw new Exception("Truncated message."); + // } + //} + + //if (Pos > _length) + //{ + // throw new Exception("Truncated message."); + //} + } + + + /// + /// Skip bytes + /// + /// Number of bytes to skip + public void SkipBytes(int skip) + { + if (_pos + skip > _length) + { + string msg = string.Format(NumberFormatInfo.InvariantInfo, "[SkipBytes()] skip:{0} pos:{1} len:{2}", skip, _pos, _length); + throw new System.Exception(msg); + } + _pos += skip; + } + + + /// + /// Automatically skip bytes based on wire type + /// + /// New position within the byte array + public int Skip() + { + if (Tag == 0) + { + throw new System.Exception("call next() before calling skip()"); + } + + switch (WireType) + { + case WireTypes.VARINT: + SkipVarint(); + break; + case WireTypes.BYTES: + SkipBytes((int)Varint()); + break; + case WireTypes.FIXED32: + SkipBytes(4); + break; + case WireTypes.FIXED64: + SkipBytes(8); + break; + case WireTypes.UNDEFINED: + throw new System.Exception("undefined wire type"); + default: + throw new System.Exception("unknown wire type"); + } + + return _pos; + } + + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs.meta new file mode 100644 index 000000000..9e444a82f --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 13381fc6c43e4fb8ad898af93ccde49b +timeCreated: 1748540648 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs new file mode 100644 index 000000000..4f836acb1 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs @@ -0,0 +1,21 @@ +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public class StackMeshInfo + { + public int TotalPointCount; + public int TotalTriangleCount; + public int[] vertexRanges; + public int[] vertexSize; + public int[] triRanges; + public int[] triSize; + + public StackMeshInfo(int featureCount) + { + vertexRanges = new int[featureCount]; + vertexSize = new int[featureCount]; + triRanges = new int[featureCount]; + triSize = new int[featureCount]; + TotalPointCount = 0; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs.meta new file mode 100644 index 000000000..e69d1d15f --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0e7204faf02446ef9e48cb58c73eebca +timeCreated: 1756394415 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs new file mode 100644 index 000000000..6953d8b6b --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs @@ -0,0 +1,19 @@ +using System; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public ref struct VectorTile + { + private VectorTileReader _VTR; + + public VectorTile(ReadOnlySpan data) + { + _VTR = new VectorTileReader(ref data); + } + + public bool TryGetLayer(string layerName, out VectorTileLayer layer) + { + return _VTR.TryGetLayer(layerName, out layer); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs.meta new file mode 100644 index 000000000..0ef853d04 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ee2afb0675ad4d1f9a7e4c4c0bdff748 +timeCreated: 1748537811 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs new file mode 100644 index 000000000..04083f04d --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public ref struct VectorTileLayer + { + public ReadOnlySpan Data; + + /// + /// Class to access a vector tile layer + /// + public VectorTileLayer(ref ReadOnlySpan data) + { + Data = data; + _FeaturesData = new List(); + Keys = new List(); + Values = new List(); + Name = null; + Version = 0; + Extent = 0; + } + + /// + /// Get number of features. + /// + /// Number of features in this layer + public int FeatureCount() + { + return _FeaturesData.Count; + } + + public Vector2Int GetViewFor(int i) + { + return _FeaturesData[i]; + } + + public void AddFeatureData(Vector2Int data) + { + _FeaturesData.Add(data); + } + + + /// Name of this layer https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L57 + public string Name { get; set; } + + + /// Version of this layer https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L55 + public long Version { get; set; } + + + /// Extent of this layer https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L70 + public float Extent; + + + /// Raw data of the features contained in this layer + private List _FeaturesData { get; set; } + + + /// + /// TODO: switch to 'dynamic' when Unity supports .Net 4.5 + /// Values contained in this layer + /// + public List Values { get; set; } + + + /// + /// Keys contained in this layer + /// + public List Keys { get; set; } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs.meta new file mode 100644 index 000000000..f31863ee7 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ed0c1e00a70e4aa7af476eee464b39bc +timeCreated: 1748540665 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs new file mode 100644 index 000000000..453b01f57 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Mapbox.VectorTile.Contants; +using UnityEngine; +using ValueType = Mapbox.VectorTile.Contants.ValueType; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public ref struct VectorTileReader + { + private Dictionary _layerViews; + private readonly ReadOnlySpan _data; + + public VectorTileReader(ref ReadOnlySpan data) + { + _data = data; + _layerViews = new Dictionary(); + if (data.IsEmpty) + { + throw new System.Exception("Tile data cannot be empty"); + } + if (data[0] == 0x1f && data[1] == 0x8b) + { + throw new System.Exception("Tile data is zipped"); + } + + layers(data); + } + + private void layers(ReadOnlySpan data) + { + PbfReader tileReader = new PbfReader(data); + while (tileReader.NextByte()) + { + if (tileReader.Tag == (int)TileType.Layers) + { + string name = null; + var view = tileReader.View(); + PbfReader layerView = new PbfReader(_data.Slice(view.x, view.y)); + while (layerView.NextByte()) + { + if (layerView.Tag == (int)LayerType.Name) + { + var strLen = layerView.Varint(); + name = layerView.GetString((int)strLen); + } + else + { + layerView.Skip(); + } + } + if (name == null) + { + Debug.LogWarning("Skipping PBF layer with no name tag"); + continue; + } + _layerViews.Add(name, view); + } + else + { + tileReader.Skip(); + } + } + } + + public bool TryGetLayer(string layerName, out VectorTileLayer layer) + { + if (_layerViews.TryGetValue(layerName, out var layerData)) + { + layer = getLayer(_data.Slice(layerData.x, layerData.y)); + return true; + } + else + { + layer = new VectorTileLayer(); + return false; + } + } + + private VectorTileLayer getLayer(ReadOnlySpan layerData) + { + var layer = new VectorTileLayer(ref layerData); + var layerReader = new PbfReader(layer.Data); + while (layerReader.NextByte()) + { + int layerType = layerReader.Tag; + switch ((LayerType)layerType) + { + case LayerType.Version: + var version = layerReader.Varint(); + layer.Version = version; + break; + case LayerType.Name: + var strLength = layerReader.Varint(); + layer.Name = layerReader.GetString((int)strLength); + break; + case LayerType.Extent: + layer.Extent = (ulong)layerReader.Varint(); + break; + case LayerType.Keys: + var keyBuffer = layerReader.View(); + string key = Encoding.UTF8.GetString(layerData.Slice(keyBuffer.x, keyBuffer.y)); + layer.Keys.Add(key); + break; + case LayerType.Values: + var valueBuffer = layerReader.View(); + var valuesBuffer = layerData.Slice(valueBuffer.x, valueBuffer.y); + var valReader = new PbfReader(valuesBuffer); + while (valReader.NextByte()) + { + switch ((ValueType)valReader.Tag) + { + case ValueType.String: + var stringBuffer = valReader.View(); + string value = Encoding.UTF8.GetString(valuesBuffer.Slice(stringBuffer.x, stringBuffer.y)); + layer.Values.Add(value); + break; + case ValueType.Float: + float snglVal = valReader.GetFloat(); + layer.Values.Add(snglVal); + break; + case ValueType.Double: + double dblVal = valReader.GetDouble(); + layer.Values.Add(dblVal); + break; + case ValueType.Int: + long i64 = valReader.Varint(); + layer.Values.Add(i64); + break; + case ValueType.UInt: + long u64 = valReader.Varint(); + layer.Values.Add(u64); + break; + case ValueType.SInt: + long s64 = valReader.Varint(); + long decoded = (long)((ulong)s64 >> 1) ^ -(s64 & 1); + layer.Values.Add(decoded); + break; + case ValueType.Bool: + long b = valReader.Varint(); + layer.Values.Add(b == 1); + break; + default: + throw new System.Exception(string.Format( + NumberFormatInfo.InvariantInfo + , "NOT IMPLEMENTED valueReader.Tag:{0} valueReader.WireType:{1}" + , valReader.Tag + , valReader.WireType + )); + //uncomment the following lines when not throwing!! + //valReader.Skip(); + //break; + } + } + break; + case LayerType.Features: + layer.AddFeatureData(layerReader.View()); + break; + default: + layerReader.Skip(); + break; + } + } + return layer; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs.meta new file mode 100644 index 000000000..16cf8cac7 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6e2aca6dc66542d79a0e8659e9b2e54f +timeCreated: 1748540659 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor.meta new file mode 100644 index 000000000..1c184fd7b --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a2aa0b58efc974bf48c85f841a6f7d4d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs new file mode 100644 index 000000000..b54613bc7 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs @@ -0,0 +1,63 @@ +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using UnityEditor; + +namespace Mapbox.VectorModule.ComponentSystem.Editor +{ + [CustomEditor(typeof(BuildingComponentVisualizerObject))] + public class BuildingComponentVisualizerObjectEditor : UnityEditor.Editor + { + private SerializedProperty _settingsProp; + + private void OnEnable() + { + + } + + public override void OnInspectorGUI() + { + _settingsProp = serializedObject.FindProperty("Settings"); + serializedObject.Update(); + + EditorGUILayout.LabelField("Building Layer Visualizer", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + if (_settingsProp != null) + { + DrawSettings(_settingsProp); + } + else + { + EditorGUILayout.HelpBox("No _settings field found.", MessageType.Warning); + } + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawSettings(SerializedProperty settingsProp) + { + var enableTerrainSnapping = settingsProp.FindPropertyRelative("EnableTerrainSnapping"); + var enableChamfer = settingsProp.FindPropertyRelative("RoundBuildingCorners"); + var chamferSettings = settingsProp.FindPropertyRelative("ChamferExtrusionSettings"); + var basicSettings = settingsProp.FindPropertyRelative("BasicExtrusionSettings"); + var material = settingsProp.FindPropertyRelative("Material"); + + EditorGUILayout.PropertyField(material); + EditorGUILayout.PropertyField(enableTerrainSnapping); + EditorGUILayout.Space(); + + enableChamfer.boolValue = EditorGUILayout.Toggle("Enable Rounded Corners", enableChamfer.boolValue); + if (enableChamfer.boolValue) + { + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(chamferSettings, true); + EditorGUI.indentLevel--; + } + else + { + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(basicSettings, true); + EditorGUI.indentLevel--; + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs.meta new file mode 100644 index 000000000..0075bdbd8 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 900de1d5e17a74422be81361b5812d0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs new file mode 100644 index 000000000..a200169b2 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs @@ -0,0 +1,94 @@ +using System; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Editor +{ + [CustomPropertyDrawer(typeof(ExtrusionOptions))] + public class ExtrusionOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + GUILayout.Space(-EditorGUIUtility.singleLineHeight - 2); + + // Find properties + var extrusionTypeProp = property.FindPropertyRelative("extrusionType"); + var propertyNameProp = property.FindPropertyRelative("propertyName"); + var minHeightProp = property.FindPropertyRelative("minimumHeight"); + var maxHeightProp = property.FindPropertyRelative("maximumHeight"); + var scaleFactorProp = property.FindPropertyRelative("extrusionScaleFactor"); + + //part to render extrusion type in dropbox without none setting + { + // Get all enum values except 'None' (index 0) + var enumValues = (ExtrusionType[])Enum.GetValues(typeof(ExtrusionType)); + var enumNames = Enum.GetNames(typeof(ExtrusionType)); + + // Skip "None" + var displayedValues = new ExtrusionType[enumValues.Length - 1]; + var displayedNames = new string[enumValues.Length - 1]; + Array.Copy(enumValues, 1, displayedValues, 0, displayedValues.Length); + Array.Copy(enumNames, 1, displayedNames, 0, displayedNames.Length); + + // Determine current value + var currentType = (ExtrusionType)extrusionTypeProp.enumValueIndex; + + // If current value is None, default to first visible option + if (currentType == ExtrusionType.None) + currentType = displayedValues[0]; + + // Draw dropdown without "None" + int selectedIndex = Array.IndexOf(displayedValues, currentType); + int newIndex = EditorGUILayout.Popup("Extrusion Type", Mathf.Max(selectedIndex, 0), displayedNames); + + extrusionTypeProp.enumValueIndex = (int)displayedValues[newIndex]; + } + + // Determine which ExtrusionType is selected + var extrusionType = (ExtrusionType)extrusionTypeProp.enumValueIndex; + + DrawReadOnlyProperty(propertyNameProp); + + // Show common field for all types that need propertyName + switch (extrusionType) + { + case ExtrusionType.PropertyHeight: + + break; + + case ExtrusionType.AbsoluteHeight: + EditorGUILayout.PropertyField(maxHeightProp, new GUIContent("Height")); + break; + + case ExtrusionType.MinHeight: + EditorGUILayout.PropertyField(minHeightProp); + break; + + case ExtrusionType.MaxHeight: + EditorGUILayout.PropertyField(maxHeightProp); + break; + case ExtrusionType.RangeHeight: + EditorGUILayout.PropertyField(minHeightProp); + EditorGUILayout.PropertyField(maxHeightProp); + break; + + default: + break; + } + + EditorGUILayout.PropertyField(scaleFactorProp, new GUIContent("Scale Multiplier")); + + void DrawReadOnlyProperty(SerializedProperty prop) + { + GUI.enabled = false; + EditorGUILayout.PropertyField(prop); + GUI.enabled = true; + } + + EditorGUI.EndProperty(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs.meta new file mode 100644 index 000000000..b7f5f4776 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b90e7f40e810f4582a2fae32600824ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs new file mode 100644 index 000000000..118092694 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Editor +{ + [CustomEditor(typeof(MapboxComponentsModuleScript))] + public class MapboxComponentsModuleScriptEditor : UnityEditor.Editor + { + private SerializedProperty _layerVisualizersProp; + private GUIStyle _boxStyle; + private List _visualizerTypes; + + private void OnEnable() + { + _layerVisualizersProp = serializedObject.FindProperty("_layerVisualizers"); + + // Find all ScriptableObject types that implement IPerformanceLayerVisualizer + _visualizerTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes()) + .Where(t => + typeof(IPerformanceLayerVisualizer).IsAssignableFrom(t) && + !t.IsAbstract) + .ToList(); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + if (_boxStyle == null) + { + _boxStyle = new GUIStyle(GUI.skin.box) + { + padding = new RectOffset(8, 8, 8, 8), + margin = new RectOffset(4, 4, 8, 8) + }; + } + + // --- show info box if list is empty --- + if (_layerVisualizersProp.arraySize == 0) + { + EditorGUILayout.Space(8); + EditorGUILayout.HelpBox("No visualizers added.\nUse the buttons below to add or create a new visualizer.", + MessageType.Info); + EditorGUILayout.Space(8); + } + + for (int i = 0; i < _layerVisualizersProp.arraySize; i++) + { + var element = _layerVisualizersProp.GetArrayElementAtIndex(i); + + EditorGUILayout.BeginVertical(_boxStyle); + EditorGUILayout.BeginHorizontal(); + + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(element, GUIContent.none); + if (EditorGUI.EndChangeCheck()) + serializedObject.ApplyModifiedProperties(); + + if (GUILayout.Button("Remove", GUILayout.Width(70))) + { + _layerVisualizersProp.DeleteArrayElementAtIndex(i); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + break; + } + + EditorGUILayout.EndHorizontal(); + + if (element.objectReferenceValue != null) + { + var editor = CreateEditor(element.objectReferenceValue); + if (editor != null) + { + EditorGUILayout.Space(5); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + editor.OnInspectorGUI(); + EditorGUILayout.EndVertical(); + } + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(4); + } + + EditorGUILayout.Space(8); + + // --- button row --- + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Add Empty Slot", GUILayout.Height(28))) + { + AddEmptySlot(); + } + + if (GUILayout.Button("Add Layer Visualizer", GUILayout.Height(28))) + { + ShowAddVisualizerMenu(); + } + + EditorGUILayout.EndHorizontal(); + + serializedObject.ApplyModifiedProperties(); + } + + private void AddEmptySlot() + { + serializedObject.Update(); + int newIndex = _layerVisualizersProp.arraySize; + _layerVisualizersProp.InsertArrayElementAtIndex(newIndex); + + // ensure it's null, not a duplicate + var el = _layerVisualizersProp.GetArrayElementAtIndex(newIndex); + el.objectReferenceValue = null; + + serializedObject.ApplyModifiedProperties(); + } + + + private void ShowAddVisualizerMenu() + { + GenericMenu menu = new GenericMenu(); + + if (_visualizerTypes.Count == 0) + { + menu.AddDisabledItem(new GUIContent("No visualizer types found")); + } + else + { + foreach (var type in _visualizerTypes) + { + var displayName = type + .GetCustomAttributes(typeof(DisplayNameAttribute), true) + .FirstOrDefault() as DisplayNameAttribute; + string name = (displayName != null) + ? displayName.DisplayName + : ObjectNames.NicifyVariableName(type.Name); + menu.AddItem(new GUIContent(name), false, () => CreateAndAddVisualizer(type)); + } + } + + menu.ShowAsContext(); + } + + private void CreateAndAddVisualizer(Type type) + { + string path = EditorUtility.SaveFilePanelInProject( + "Create Visualizer", + type.Name + ".asset", + "asset", + "Select a location to save the new visualizer asset." + ); + + if (string.IsNullOrEmpty(path)) + return; // cancelled + + var instance = ScriptableObject.CreateInstance(type); + AssetDatabase.CreateAsset(instance, path); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + serializedObject.Update(); + int newIndex = _layerVisualizersProp.arraySize; + _layerVisualizersProp.InsertArrayElementAtIndex(newIndex); + _layerVisualizersProp.GetArrayElementAtIndex(newIndex).objectReferenceValue = instance; + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs.meta new file mode 100644 index 000000000..15d922bca --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 947b59332411d4bb793a6514df9bb18e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs new file mode 100644 index 000000000..b7a54f47b --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem +{ + public abstract class MapboxComponentVisualizer : VectorLayerVisualizer + { + public int VisualizerId => _id; + private static int _nextId = 0; + private readonly int _id; + + protected ObjectPool _buildingObjectPool; + + public MapboxComponentVisualizer(string name, IMapInformation mapInformation, UnityContext unityContext = null) : base(name, mapInformation, unityContext, null) + { + _id = System.Threading.Interlocked.Increment(ref _nextId); + _buildingObjectPool = new ObjectPool(VectorEntityGenerator, 20); + } + + public virtual MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) + { + return null; + } + + public virtual List CreateGo(CanonicalTileId tileId, MeshData meshData) + { + return null; + } + + public override void UpdateForView(CanonicalTileId canonicalTileId, IMapInformation information) + { + if (_results.TryGetValue(canonicalTileId, out var visuals)) + { + _mapInformation.PositionObjectFor(canonicalTileId, out var position, out var scale); + foreach (var entity in visuals) + { + entity.GameObject.transform.localPosition = position; + entity.GameObject.transform.localScale = scale; + } + } + } + + public override void UnregisterTile(CanonicalTileId tileId) + { + if (_results.ContainsKey(tileId)) + { + foreach (var entity in _results[tileId]) + { + entity.GameObject.SetActive(false); + _buildingObjectPool.Put(entity); + //TODO call finalize for gameobject modifiers here + + OnBuildingMeshDestroyed(tileId, entity.GameObject); + } + + _results.Remove(tileId); + } + + //TODO call unregister for gameobject modifiers here + } + + public override void SetActive(CanonicalTileId canonicalTileId, bool isActive, IMapInformation mapInformation) + { + if (_results.TryGetValue(canonicalTileId, out var visuals)) + { + foreach (var entity in visuals) + { + entity.GameObject.SetActive(isActive); + } + } + } + + public override void ClearCaches() + { + base.ClearCaches(); + } + + private VectorEntity VectorEntityGenerator() + { + var go = new GameObject(); + go.transform.SetParent(_layerRootObject); + var mf = go.AddComponent(); + mf.sharedMesh = new Mesh(); + var mr = go.AddComponent(); + var tempVectorEntity = new VectorEntity() + { + GameObject = go, + Transform = go.transform, + MeshFilter = mf, + MeshRenderer = mr, + Mesh = mf.sharedMesh + }; + return tempVectorEntity; + } + + public Action OnBuildingMeshCreated = (id, list, info) => { }; + public Action OnBuildingMeshDestroyed = (id, list) => { }; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs.meta new file mode 100644 index 000000000..347f03101 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2916147689b340d58602c28b004ce5aa +timeCreated: 1744797730 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs new file mode 100644 index 000000000..a3d62c730 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Tasks; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.MeshGeneration; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem +{ + public class MapboxComponentsModule : VectorLayerModule + { + public MapboxComponentsModule(IMapInformation mapInformation, Source source, + UnityContext unityContext, Dictionary> layerVisualizers, + VectorModuleSettings vectorModuleSettings = null) : base(mapInformation, source, unityContext, + layerVisualizers, vectorModuleSettings) + { + } + + protected override void MeshGeneration(VectorData data, Action callback) + { + var meshTask = new MeshGenTaskWrapper() + { + TileId = data.TileId, + DataAction = () => + { + var result = new BuildingLayerTaskResult(); + result.MeshData = new List(); + var decompressed = Compression.Decompress(data.Data); + var processedTile = new Data.VectorTile(decompressed); + try + { + foreach (var pair in _layerVisualizers) + { + foreach (var layerVisualizer in pair.Value) + { + var visualizer = (MapboxComponentVisualizer)layerVisualizer; + if (visualizer == null || !visualizer.Active || + visualizer.ContainsVisualFor(data.TileId)) + continue; + + if (processedTile.TryGetLayer(visualizer.VectorLayerName, out var layer)) + { + var layerData = visualizer.CreateMesh(data.TileId, layer); + result.MeshData.Add(new BuildingLayerDataResult() + { + LayerName = visualizer.VectorLayerName, + LayerId = visualizer.VisualizerId, + MeshData = layerData + }); + } + } + } + } + catch (Exception e) + { + result.ResultType = TaskResultType.MeshGenerationFailure; + result.AddException(e); + return result; + } + + result.ResultType = TaskResultType.Success; + return result; + }, + DataCompleted = (task, taskResult) => //task may be null + { + if (!_isActive) + return; + + _activeTasks.Remove(data.TileId); + + if (taskResult.ResultType == TaskResultType.Cancelled || (task != null && task.IsCanceled)) + { + var failResult = new MeshGenerationTaskResult(TaskResultType.Cancelled); + callback(failResult); + return; + } + + if (taskResult.ResultType == TaskResultType.MeshGenerationFailure) + { + var failResult = new MeshGenerationTaskResult(taskResult.ResultType); + foreach (var e in taskResult.GetExceptions()) + { + failResult.AddException(e); + } + + //Debug.Log(string.Format("{0} mesh gen exception: {1}", data.TileId, task.Exception.Message)); + failResult.AddException(new Exception(string.Format("{0} mesh gen exception: {1}", data.TileId, + taskResult.ExceptionsAsString))); + callback(failResult); + return; + } + + var resultGameObjects = new List(); + foreach (var layerData in taskResult.MeshData) + { + // foreach (var vectorLayerVisualizers in _layerVisualizers + // .Where(x => x.Value is MapboxComponentVisualizer && x.Key == layerData.LayerName) + // .Select(x => x.Value)) + IVectorLayerVisualizer visualizer = null; + var visualizers = _layerVisualizers[layerData.LayerName]; + for (int i = 0; i < visualizers.Count; i++) + { + if (((MapboxComponentVisualizer)visualizers[i]).VisualizerId == layerData.LayerId) + { + visualizer = visualizers[i]; + break; + } + } + if (visualizer == null) + continue; + + var layerVisualizer = (MapboxComponentVisualizer)visualizer; + if (layerData.MeshData == null) + continue; + var layerGameObjects = layerVisualizer.CreateGo(data.TileId, layerData.MeshData); + foreach (var gameObject in layerGameObjects) + { + gameObject.SetActive(false); + resultGameObjects.Add(gameObject); + } + + } + + callback(new MeshGenerationTaskResult(TaskResultType.Success, resultGameObjects)); + } + }; + if (!_activeTasks.ContainsKey(data.TileId)) _activeTasks.Add(data.TileId, new List()); + _activeTasks[data.TileId].Add(meshTask); + _unityContext.TaskManager.AddTask(meshTask, 0); + } + + public class BuildingLayerTaskResult : MeshGenTaskWrapperResult + { + public List MeshData; + } + + public class BuildingLayerDataResult + { + public string LayerName; + public MeshData MeshData; + public int LayerId; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs.meta new file mode 100644 index 000000000..deb070266 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fa0b49cb2cc94dc6a1c07f652c4bf589 +timeCreated: 1758131442 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs new file mode 100644 index 000000000..367214ba7 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.Unity; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem +{ + public class MapboxComponentsModuleScript : ModuleConstructorScript + { + [SerializeField] private List _layerVisualizers; + public override ILayerModule ModuleImplementation { get; protected set; } + + public void Start() + { + + } + + protected VectorLayerModule GetVectorLayerModule(IMapInformation mapInformation, UnityContext unityContext, MapService service, Dictionary> dictionary) + { + var tilesetId = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8).Id; + var settings = new VectorModuleSettings() + { + DataSettings = new VectorSourceSettings() + { + TilesetId = tilesetId, + CacheSize = 100, + ClampDataLevelToMax = 15 + } + }; + return new MapboxComponentsModule(mapInformation, service.GetVectorSource(settings.DataSettings), unityContext, dictionary, settings); + } + + public override ILayerModule ConstructModule(MapService service, IMapInformation mapInformation, UnityContext unityContext) + { + var dictionary = new Dictionary>(); + foreach (var visualizerObject in _layerVisualizers) + { + if(visualizerObject == null) continue; + var visualizer = visualizerObject.ConstructLayerVisualizer(mapInformation, unityContext); + if(!dictionary.ContainsKey(visualizer.VectorLayerName)) + dictionary.Add(visualizer.VectorLayerName, new List()); + dictionary[visualizer.VectorLayerName].Add(visualizer); + } + ModuleImplementation = GetVectorLayerModule(mapInformation, unityContext, service, dictionary); + return ModuleImplementation; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs.meta new file mode 100644 index 000000000..f59116d81 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e13965577acb456182a04c56586cb36e +timeCreated: 1758132173 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers.meta new file mode 100644 index 000000000..47d0f134e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ff49ecc199aa749ac8853d418dd79076 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs new file mode 100644 index 000000000..8f4e5b2e9 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs @@ -0,0 +1,296 @@ +using System; +using System.Runtime.CompilerServices; +using Mapbox.BaseModule.Map; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Modifiers +{ + public interface IPerformanceExtrusion + { + int CalculateTriCountFor(int totalPointCount); + + int Run(Span vertices, Span normals, + int vertexAnchorIndex, + int[] triList, + int triIndex, + float height, + float minHeight, + FeatureVertexData vertexData, + float tileSizeX, + float scale); + } + + public class ArrayChamferHeight : IPerformanceExtrusion + { + private ChamferSettings _settings; + + private static readonly Vector3 Up = Vector3.up; + + public ArrayChamferHeight(ChamferSettings settings) + { + _settings = settings; + } + + public int Run(Span vertices, + Span normals, + int vertexAnchorIndex, + int[] triList, + int triIndex, + float height, + float minHeight, + FeatureVertexData vertexData, + float tileSizeX, + float scale) + { + if (vertexData == null || vertexData.Submeshes.Count < 1) + return triIndex; + + var scaledOffset = (_settings.OffsetInMeters / tileSizeX) / scale; + + height = (height / scale) / tileSizeX; + minHeight = (minHeight > 0) + ? (minHeight / scale) / tileSizeX + : 0; + + triIndex = Chamfer(vertices, normals, vertexData, triList, triIndex, minHeight, height, scaledOffset, vertexAnchorIndex); + return triIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 NormalizeXZ(Vector3 v) + { + float invLen = 1.0f / Mathf.Sqrt(v.x * v.x + v.z * v.z + 1e-12f); + v.x *= invLen; + v.z *= invLen; + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 PerpXZ(Vector3 v) => new Vector3(-v.z, 0f, v.x); + + /// + /// |-x1-|-y1-|-z1-|-x2-|-y2-|-z2-| + /// x1: original polygon vertices (will get changed) + /// y1, z1: original submesh/hole vertices (will get changed) + /// x2,y2,z2: chamfer and side wall vertices + /// originalPolygonPointIndexer: index of the point we are currently processing. whole loop works on the original + /// polygon points so this is the anchor point. it should move in (x1,z1) range. + /// addedPointCount: pointer for x2|y2|z2 section start. pointer for where the second ring of submesh starts. + /// + /// + /// + /// + /// + /// + /// + private int Chamfer( + Span vertices, + Span normals, + FeatureVertexData vertexData, + int[] trilist, + int triIndex, + float minHeight, + float height, + float scaledOffset, + int startIndex) + { + int polyPointCount = vertexData.VertexCount; + int originalPolygonPointIndexer = 0; + int addedPointCount = 0; + const int vertPerStep = 4; + + // Local caches + Vector3 v1, v2, n1, n2, pij1, pij2, pjk1, pjk2, poi; + Vector3 prev, curr, next; + var verts = vertexData.Vertices; + var submeshes = vertexData.Submeshes; + + for (int i = 1; i < submeshes.Count; i++) + { + int start = submeshes[i - 1]; + int end = submeshes[i]; + int count = end - start; + var subSpan = verts.AsSpan(start, count); + + int secondRingStartIndex = polyPointCount + addedPointCount; + + for (int j = 0; j < count; j++) + { + int jPrev = (j == 0) ? count - 1 : j - 1; + int jNext = (j == count - 1) ? 0 : j + 1; + + prev = subSpan[jPrev]; + curr = subSpan[j]; + next = subSpan[jNext]; + + // ---- precompute normalized edge directions ---- + v1.x = curr.x - prev.x; + v1.y = 0f; + v1.z = curr.z - prev.z; + v1 = NormalizeXZ(v1); + n1 = PerpXZ(v1); + v1.x *= scaledOffset; + v1.z *= scaledOffset; + + pij1.x = prev.x - n1.x * scaledOffset; + pij1.y = 0f; + pij1.z = prev.z - n1.z * scaledOffset; + + pij2.x = curr.x - n1.x * scaledOffset; + pij2.y = 0f; + pij2.z = curr.z - n1.z * scaledOffset; + + v2.x = next.x - curr.x; + v2.y = 0f; + v2.z = next.z - curr.z; + v2 = NormalizeXZ(v2); + n2 = PerpXZ(v2); + v2.x *= scaledOffset; + v2.z *= scaledOffset; + + pjk1.x = curr.x - n2.x * scaledOffset; + pjk1.y = 0f; + pjk1.z = curr.z - n2.z * scaledOffset; + + pjk2.x = next.x - n2.x * scaledOffset; + pjk2.y = 0f; + pjk2.z = next.z - n2.z * scaledOffset; + + // ---- intersection ---- + if (pij2.x != pjk1.x || pij2.z != pjk1.z) + FindIntersection(pij1, pij2, pjk1, pjk2, out poi); + else + poi = pij2; + + int vertBase = secondRingStartIndex + vertPerStep * j; + int curIdx = originalPolygonPointIndexer + j; + + Vector3 vertCurr = vertices[curIdx]; + float yTop = vertCurr.y + height; + float yMin = vertCurr.y + minHeight; + + // write vertices (inline, minimal constructors) + vertices[vertBase] = new Vector3(vertCurr.x - v1.x, yTop, vertCurr.z - v1.z); + vertices[vertBase + 1] = new Vector3(vertCurr.x + v2.x, yTop, vertCurr.z + v2.z); + vertices[vertBase + 2] = new Vector3(vertCurr.x - v1.x, yMin, vertCurr.z - v1.z); + vertices[vertBase + 3] = new Vector3(vertCurr.x + v2.x, yMin, vertCurr.z + v2.z); + + // move original vertex + vertices[curIdx] = new Vector3(poi.x, yTop + scaledOffset, poi.z); + + // normals + normals[curIdx] = Up; + normals[vertBase] = n1; + normals[vertBase + 1] = n2; + normals[vertBase + 2] = n1; + normals[vertBase + 3] = n2; + + // ---- triangles ---- + int si = startIndex; + int baseA = si + curIdx; + int baseB = si + vertBase; + + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseB; + trilist[triIndex++] = baseB + 1; + + bool notLast = j < count - 1; + if (notLast) + { + int nextTri = j + 1; + int baseNext = si + originalPolygonPointIndexer + nextTri; + int baseBnext = si + secondRingStartIndex + vertPerStep * nextTri; + + trilist[triIndex++] = baseNext; + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseBnext; + + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseBnext; + + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseB + 3; + trilist[triIndex++] = baseBnext; + + trilist[triIndex++] = baseBnext; + trilist[triIndex++] = baseB + 3; + trilist[triIndex++] = baseBnext + 2; + } + else + { + int baseFirst = si + originalPolygonPointIndexer; + int baseBfirst = si + secondRingStartIndex; + + trilist[triIndex++] = baseFirst; + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseBfirst; + + trilist[triIndex++] = baseBfirst; + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseB + 1; + + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseB + 3; + trilist[triIndex++] = baseBfirst + 2; + + trilist[triIndex++] = baseBfirst; + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseBfirst + 2; + } + + trilist[triIndex++] = si + vertBase; + trilist[triIndex++] = si + vertBase + 2; + trilist[triIndex++] = si + vertBase + 3; + + trilist[triIndex++] = si + vertBase + 1; + trilist[triIndex++] = si + vertBase; + trilist[triIndex++] = si + vertBase + 3; + } + + originalPolygonPointIndexer += subSpan.Length; + addedPointCount += vertPerStep * subSpan.Length; + } + + return triIndex; + } + + private static void FindIntersection( + Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, + out Vector3 intersection) + { + float dx12 = p2.x - p1.x; + float dz12 = p2.z - p1.z; + float dx34 = p4.x - p3.x; + float dz34 = p4.z - p3.z; + + float denominator = dz12 * dx34 - dx12 * dz34; + + // Check for parallel or nearly parallel lines + if (Mathf.Abs(denominator) < 1e-8f) + { + intersection = new Vector3(float.NaN, 0f, float.NaN); + return; + } + + float dx13 = p1.x - p3.x; + float dz13 = p1.z - p3.z; + + float t1 = (dx13 * dz34 + (p3.z - p1.z) * dx34) / denominator; + + intersection = new Vector3( + p1.x + dx12 * t1, + 0f, + p1.z + dz12 * t1 + ); + } + + public int CalculateTriCountFor(int totalPointCount) + { + return totalPointCount * 21; //7 tris , 3 int per + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs.meta new file mode 100644 index 000000000..0a3bb5737 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 454b9196af594778a01eec9395e81223 +timeCreated: 1744834620 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs new file mode 100644 index 000000000..deae1988e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs @@ -0,0 +1,203 @@ +using System; +using System.Runtime.CompilerServices; +using Mapbox.BaseModule.Map; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Modifiers +{ + public class ArrayHeight : IPerformanceExtrusion + { + private readonly BasicExtrusionSettings _settings; + + public ArrayHeight(BasicExtrusionSettings settings) + { + _settings = settings; + } + + public int Run( + Span vertices, + Span normals, + int vertexAnchorIndex, + int[] triList, + int triIndex, + float height, + float minHeight, + FeatureVertexData vertexData, + float tileSizeX, + float scale) + { + if (vertexData == null || vertexData.Submeshes.Count < 1) + return triIndex; + + var startIndex = vertexAnchorIndex; + + // Convert heights to local (tile) units once + height = (height / scale) / tileSizeX; + float minH = (minHeight > 0) + ? (minHeight / scale) / tileSizeX + : 0; + + float wallHeight = height - minH; + + // Raise the roof (top ring/verts in-span) + GenerateRoofMesh(vertices, height); + + // Build walls + triIndex = GenerateWallMesh(vertices, normals, vertexData, triList, triIndex, startIndex, wallHeight, height); + + return triIndex; + } + + public void GenerateRoofMesh(Span vertices, float maxHeight) + { + // Mutate by ref to avoid creating new Vector3 + for (int i = 0; i < vertices.Length; i++) + { + ref Vector3 v = ref vertices[i]; + v.y = maxHeight; + } + } + + private int GenerateWallMesh( + Span vertices, + Span normals, + FeatureVertexData vertexData, + int[] trilist, + int triIndex, + int startIndex, + float wallHeight, + float roofHeight) + { + // Aliases for quicker access + var verts = vertexData.Vertices; + var submeshes = vertexData.Submeshes; + + int topPolygonVertexCount = vertexData.VertexCount; + + // For each ring (outer + holes) + // We assume wall-vertex area starts after top polygon: each edge adds 4 vertices + for (int i = 1; i < submeshes.Count; i++) + { + int start = submeshes[i - 1]; + int end = submeshes[i]; + int count = end - start; + + var subSpan = verts.AsSpan(start, count); + int ringBase = topPolygonVertexCount + (start * 4); // base in the wall region for this submesh + var vertSpan = vertices.Slice(ringBase, count * 4); + var normSpan = normals.Slice(ringBase, count * 4); + + // Walk edges + for (int j = 0; j < count; j++) + { + int jNext = (j == count - 1) ? 0 : j + 1; + + // edge direction (XZ) + Vector3 v1; + v1.x = subSpan[jNext].x - subSpan[j].x; + v1.y = 0f; + v1.z = subSpan[jNext].z - subSpan[j].z; + + NormalizeXZInPlace(ref v1); + Vector3 n1 = PerpXZ(v1); + + int vertBase = (j << 2); // j * 4 + + // Fetch top y from roofed vertex (already raised) + // All roof vertices were raised to the same height in GenerateRoofMesh + // Use the passed roofHeight directly to avoid indexing issues with multiple submeshes + float yTop = roofHeight; + float yMin = yTop - wallHeight; + + // Write quads (curr -> next, top and bottom) + // next(1)---------curr(0) + // | | + // nextBot(3)----currBot(2) + + // curr (top) + ref Vector3 v0 = ref vertSpan[vertBase]; + v0.x = subSpan[j].x; + v0.y = yTop; + v0.z = subSpan[j].z; + + // next (top) + ref Vector3 v1t = ref vertSpan[vertBase + 1]; + v1t.x = subSpan[jNext].x; + v1t.y = yTop; + v1t.z = subSpan[jNext].z; + + // curr (bottom) + ref Vector3 v2b = ref vertSpan[vertBase + 2]; + v2b.x = subSpan[j].x; + v2b.y = yMin; + v2b.z = subSpan[j].z; + + // next (bottom) + ref Vector3 v3b = ref vertSpan[vertBase + 3]; + v3b.x = subSpan[jNext].x; + v3b.y = yMin; + v3b.z = subSpan[jNext].z; + + // normals (same for all four) + normSpan[vertBase] = n1; + normSpan[vertBase + 1] = n1; + normSpan[vertBase + 2] = n1; + normSpan[vertBase + 3] = n1; + + // triangles + int si = startIndex; + int baseB = si + ringBase + vertBase; // this edge's first wall vertex + int baseA = si + ringBase + ((j == 0) ? ((count - 1) << 2) : ((j - 1) << 2)); // previous edge's base (for wrapping last pair) + + if (j < count - 1) + { + // two tris for face between curr edge top/bot and next top/bot + trilist[triIndex++] = baseB; + trilist[triIndex++] = baseB + 2; + trilist[triIndex++] = baseB + 1; + + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseB + 2; + trilist[triIndex++] = baseB + 3; + } + else + { + // close the ring to the first edge (wrap) + trilist[triIndex++] = baseB; + trilist[triIndex++] = baseB + 2; + trilist[triIndex++] = si + ringBase; // first edge's top-left + + trilist[triIndex++] = si + ringBase; + trilist[triIndex++] = baseB + 2; + trilist[triIndex++] = si + ringBase + 2; // first edge's bottom-left + } + } + } + + return triIndex; + } + + public int CalculateTriCountFor(int totalPointCount) + { + // 2 triangles per edge quad => 6 indices per edge + // If totalPointCount is number of wall edges, return totalPointCount * 6 + return totalPointCount * 6; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void NormalizeXZInPlace(ref Vector3 v) + { + float lenSq = v.x * v.x + v.z * v.z; + if (lenSq <= 1e-20f) { v.x = 0f; v.z = 0f; return; } + float invLen = 1.0f / Mathf.Sqrt(lenSq); + v.x *= invLen; + v.z *= invLen; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 PerpXZ(in Vector3 v) => new Vector3(-v.z, 0f, v.x); + } +} diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs.meta new file mode 100644 index 000000000..d8b87e77e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0acbec66c9f64d47b52e43e9f35874e6 +timeCreated: 1761675661 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs new file mode 100644 index 000000000..73667bc55 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.MeshGeneration; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Modifiers +{ + public class ArrayPolygon + { + public int Polygonize( + Span vertices, + Span normals, + int vertexAnchorIndex, + int[] triList, + int triIndex, + FeatureVertexData vertexData, + float pushUp = 0) + { + int arrayMaxLength = triList.Length; + var submeshes = vertexData.Submeshes; + var allVerts = vertexData.Vertices; + int submeshCount = submeshes.Count; + + if (submeshCount < 2) + return triIndex; + + // Save initial state for rollback on overflow + int initialTriIndex = triIndex; + + var holes = new List(8); + int vertWriteIndex = 0; + bool nextIsHole = false; + + int subStart = submeshes[0]; + int subEnd = submeshes[0]; + + for (int i = 1; i < submeshCount; i++) + { + int prevStart = submeshes[i - 1]; + int prevEnd = submeshes[i]; + int prevLength = prevEnd - prevStart; + + var subVert = allVerts.AsSpan(prevStart, prevLength); + + // Detect the start of a new polygon (outer boundary) + if (IsClockwise(subVert) && vertWriteIndex > 0) + { + var span = allVerts.AsSpan(subStart, subEnd - subStart); + var flatData = EarcutLibrary.Flatten(span); + var result = EarcutLibrary.Earcut(flatData.Vertices, holes, 2); + + if (result.Count + triIndex >= arrayMaxLength) + { + // Roll back partial triangles written by this feature + for (int j = initialTriIndex; j < triIndex; j++) + triList[j] = 0; + return -1; + } + + for (int j = 0; j < result.Count; j++) + triList[triIndex++] = vertexAnchorIndex + result[j]; + + vertexAnchorIndex += flatData.Vertices.Length / 2; + holes.Clear(); + nextIsHole = false; + subStart = prevStart; // new polygon begins here + } + + subEnd = prevEnd; + + if (nextIsHole) + holes.Add(prevStart - subStart); + + nextIsHole = true; + + for (int j = 0; j < prevLength; j++) + { + subVert[j].y += pushUp; + vertices[vertWriteIndex] = subVert[j]; + normals[vertWriteIndex++] = Constants.Math.Vector3Up; + } + } + + // Final polygon flush + var finalSpan = allVerts.AsSpan(subStart, subEnd - subStart); + var finalFlat = EarcutLibrary.Flatten(finalSpan); + var finalResult = EarcutLibrary.Earcut(finalFlat.Vertices, holes, 2); + + if (finalResult.Count + triIndex >= arrayMaxLength) + { + // Roll back partial triangles written by this feature + for (int j = initialTriIndex; j < triIndex; j++) + triList[j] = 0; + return -1; + } + + for (int i = 0; i < finalResult.Count; i++) + triList[triIndex++] = vertexAnchorIndex + finalResult[i]; + + return triIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClockwise(ReadOnlySpan verts) + { + double sum = 0.0; + int count = verts.Length; + + for (int i = 0; i < count; i++) + { + var v1 = verts[i]; + var v2 = verts[(i + 1) % count]; + sum += (v2.x - v1.x) * (v2.z + v1.z); + } + + return sum > 0.0; + } + + + // public int Polygonize(Span vertices, Span normals, int vertexAnchorIndex, int[] triList, int triIndex, PerfVectorFeatureUnity feature) + // { + // var arrayMaxLength = triList.Length; + // var counter = feature.VertexData.Submeshes.Count; + // var holes = new List(); + // int vertStartIndex = 0; + // Data flatData; + // List result; + // var subStart = 0; + // var subEnd = 0; + // var nextIsHole = false; + // + // for (int i = 1; i < counter; i++) + // { + // var subVert = feature.VertexData.Vertices.AsSpan(feature.VertexData.Submeshes[i - 1], + // feature.VertexData.Submeshes[i] - feature.VertexData.Submeshes[i - 1]); + // + // if (IsClockwise(subVert) && vertStartIndex > 0) + // { + // flatData = EarcutLibrary.Flatten(feature.VertexData.Vertices.AsSpan(subStart, subEnd - subStart)); + // result = EarcutLibrary.Earcut(flatData.Vertices, holes, 2); + // + // if (result.Count + triIndex >= arrayMaxLength) return -1; + // + // for (int j = 0; j < result.Count; j++) + // { + // triList[triIndex++] = (vertexAnchorIndex + result[j]); + // } + // + // vertexAnchorIndex += flatData.Vertices.Length / 2; + // holes.Clear(); + // nextIsHole = false; + // subStart = feature.VertexData.Submeshes[i - 1]; + // } + // + // subEnd = feature.VertexData.Submeshes[i]; + // if(nextIsHole) + // { + // holes.Add(feature.VertexData.Submeshes[i - 1] - subStart); + // } + // nextIsHole = true; + // + // for (int j = 0; j < subVert.Length; j++) + // { + // vertices[vertStartIndex + j] = subVert[j]; + // normals[vertStartIndex + j] = Constants.Math.Vector3Up; + // } + // + // vertStartIndex += subVert.Length; + // } + // + // flatData = EarcutLibrary.Flatten(feature.VertexData.Vertices.AsSpan(subStart, subEnd - subStart)); + // result = EarcutLibrary.Earcut(flatData.Vertices, holes, 2); + // + // if (result.Count + triIndex >= arrayMaxLength) return -1; + // for (int i = 0; i < result.Count; i++) + // { + // triList[triIndex++] = (vertexAnchorIndex + result[i]); + // } + // + // return triIndex; + // } + // + // private bool IsClockwise(Span subVert) + // { + // double sum = 0.0; + // var counter = subVert.Length; + // for (int i = 0; i < counter; i++) + // { + // var v1 = subVert[i]; + // var v2 = subVert[(i + 1) % counter]; + // sum += (v2.x - v1.x) * (v2.z + v1.z); + // } + // + // return sum > 0.0; + // } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs.meta new file mode 100644 index 000000000..1b94e22d3 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7acd0c333a6547f3b3d1651b3050db56 +timeCreated: 1744800466 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs new file mode 100644 index 000000000..aaa443181 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs @@ -0,0 +1,35 @@ +using System; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Modifiers +{ + public class ArraySnapTerrain + { + private IMapInformation _mapInformation; + public ArraySnapTerrain(IMapInformation mapInformation) + { + _mapInformation = mapInformation; + } + + public void SnapTerrain(Span vertices, int topPolyLength, CanonicalTileId tileId, float tileSize) + { + for (var i = 0; i < topPolyLength; i++) + { + ref Vector3 vertex = ref vertices[i]; + var h = 0f; + if (_mapInformation.QueryElevation != null) + { + h = (_mapInformation.QueryElevation( + tileId, + (vertex.x), + (vertex.z + 1)) / _mapInformation.Scale + / tileSize); + } + vertex.Set(vertex.x, h, vertex.z); + } + } + + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs.meta new file mode 100644 index 000000000..40d9b2563 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6bd110daaa5943c7bb68f3de1d18dc5f +timeCreated: 1756395249 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer.meta new file mode 100644 index 000000000..e47a0aede --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b4a0714cd2513450abee8137bcacc20c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs new file mode 100644 index 000000000..0e2452e04 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Mapbox.VectorModule.Filters; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [Serializable] + public class ComponentFilterStack + { + [SerializeReference] + public List Filters = new List(); + public LayerFilterCombinerOperationType Type; + + public ComponentFilterStack() + { + + } + + public void Initialize() + { + foreach (var filter in Filters) + { + filter.Initialize(); + } + } + + public bool Try(RoadFeatureUnity feature) + { + if (Filters == null || Filters.Count == 0) + return true; + + switch (Type) + { + case LayerFilterCombinerOperationType.Any: + for (int i = 0; i < Filters.Count; i++) + { + if (Filters[i].Try(feature)) return true; + } + return false; + case LayerFilterCombinerOperationType.All: + for (int i = 0; i < Filters.Count; i++) + { + if (!Filters[i].Try(feature)) return false; + } + return true; + case LayerFilterCombinerOperationType.None: + for (int i = 0; i < Filters.Count; i++) + { + if (Filters[i].Try(feature)) return false; + } + return true; + default: + return false; + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs.meta new file mode 100644 index 000000000..b20f205a3 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6f90af9963c5434bb7b548779bf95a53 +timeCreated: 1765457934 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs new file mode 100644 index 000000000..3587bc0aa --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs @@ -0,0 +1,553 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorTile.Contants; +using Mapbox.VectorTile.Geometry; +using UnityEngine; +using UnityEngine.Rendering; +using Random = System.Random; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + public class RoadComponentVisualizer : MapboxComponentVisualizer + { + private RoadComponentSettings _settings; + + [ThreadStatic] private static Random _threadRandom; + private static Random GetRandom() => _threadRandom ??= new Random(); + + public RoadComponentVisualizer(string name, IMapInformation mapInformation, UnityContext unityContext = null, + RoadComponentSettings settings = null) : base(name, mapInformation, unityContext) + { + _settings = settings ?? new RoadComponentSettings(); + _layerRootObject.transform.position = new Vector3( + _layerRootObject.transform.position.x, + _settings.GameObjectOffset, + _layerRootObject.transform.position.z); + } + + private enum Turn + { + Start, + Right, + Left + } + + private class MaterialTrianglePair + { + public int[] Triangles; + public int TotalTriSize; + public int[] TriSizes; + public int[] TriRanges; + } + + public override MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) + { + if (_settings.RoadStyleSheet == null || _settings.RoadStyleSheet.Styles.Count == 0) + return null; + + int classTagIndex = -1; + int typeTagIndex = -1; + int structureTagIndex = -1; + for (var i = 0; i < layer.Keys.Count; i++) + { + var key = layer.Keys[i]; + if (key == "class") classTagIndex = i; + if (key == "type") typeTagIndex = i; + if (key == "structure") structureTagIndex = i; + } + + var rnd = GetRandom(); + var featureCount = layer.FeatureCount(); + var info = new StackMeshInfo(featureCount); + + var triangleGroups = new Dictionary(); + foreach (var style in _settings.RoadStyleSheet.Styles) + { + if (!triangleGroups.ContainsKey(style.Material)) + { + triangleGroups.Add(style.Material, new MaterialTrianglePair() + { + TriSizes = new int[featureCount], + TriRanges = new int[featureCount] + }); + } + } + + var tileSize = Conversions.TileSizeInUnitySpace(tileId.Z, _mapInformation.Scale); + var featureArray = new RoadFeatureUnity[featureCount]; + var styleArray = new RoadStyle[featureCount]; + + for (var i = 0; i < featureCount; i++) + { + var feature = GetFeature(layer, i, ref classTagIndex, ref typeTagIndex, ref structureTagIndex); + if (feature == null) continue; + if (feature.GeometryType != GeomType.LINESTRING) continue; + if (!_settings.RoadStyleSheet.TryGetStyle(feature, out var style)) continue; + + feature.VertexData = feature.Geometry(new Vector3(layer.Extent, 0, -layer.Extent)); + if (feature.VertexData.VertexCount <= 1) continue; + + featureArray[i] = feature; + styleArray[i] = style; + + triangleGroups[style.Material].TriRanges[i] = triangleGroups[style.Material].TotalTriSize; + info.vertexRanges[i] = info.TotalPointCount; + + var vertNeeded = VertexNeed(feature.VertexData); + info.TotalPointCount += vertNeeded; + info.vertexSize[i] = vertNeeded; + if (feature.VertexData.VertexCount == 2) + { + triangleGroups[style.Material].TriSizes[i] = 12 + 6; + triangleGroups[style.Material].TotalTriSize += 12 + 6; + info.TotalTriangleCount += 12 + 6; //2 tri 3 vert + 12 for caps + } + else + { + //12 tri for start/end caps + //6 tri end line segment + // 12 per middle vertex + var triSize = 12 + 6 + ((feature.VertexData.VertexCount - 2) * 12); + triangleGroups[style.Material].TriSizes[i] = triSize; + triangleGroups[style.Material].TotalTriSize += triSize; + info.TotalTriangleCount += triSize; + } + } + + foreach (var pair in triangleGroups) + { + pair.Value.Triangles = new int[pair.Value.TotalTriSize]; + } + + var meshData = new MeshData(info, info.TotalPointCount) + { + UVs = new Vector2[info.TotalPointCount] + }; + + var featureTriIndex = 0; + for (int i = 0; i < featureArray.Length; i++) + { + var featureResult = featureArray[i]; + var style = styleArray[i]; + + if (featureResult == null) continue; + if (featureResult.VertexData.VertexCount <= 1) continue; + + var group = triangleGroups[style.Material]; + + var scaledRoadWidth = style.Width / _mapInformation.Scale / tileSize; + var designedVertCount = meshData.MeshInfo.vertexSize[i]; + if (designedVertCount <= 2 || group.TriSizes[i] < 3) continue; + + var vertices = meshData.Vertices.AsSpan(meshData.MeshInfo.vertexRanges[i], designedVertCount); + var normals = meshData.Normals.AsSpan(meshData.MeshInfo.vertexRanges[i], designedVertCount); + var uvs = meshData.UVs.AsSpan(meshData.MeshInfo.vertexRanges[i], designedVertCount); + var tris = group.Triangles.AsSpan(group.TriRanges[i], group.TriSizes[i]); + + var lastTurnWas = Turn.Start; + var prevFirst = -2; + var prevSecond = -1; + var subVertIndex = 0; + var subTriIndex = 0; + + for (var j = 0; j < featureResult.VertexData.Submeshes.Count - 1; j++) + { + var subSize = featureResult.VertexData.Submeshes[j + 1] - featureResult.VertexData.Submeshes[j]; + var startVertex = featureResult.VertexData.Submeshes[j]; + + var elevation = style.PushUp + (float)rnd.NextDouble() * _settings.RandomOffsetRange; + var sideNormal = new Vector3(0, 0, 0); + var finishLine = false; + var distance = 0f; + Vector3 dirNext = new Vector3(0, 0, 0); + Vector3 dirPrev; + for (int k = 0; k < subSize - 1; k++) + { + var current = featureResult.VertexData.Vertices[startVertex + k]; + var next = featureResult.VertexData.Vertices[startVertex + k + 1]; + + + + if(k > 0) + { + //var prev = featureResult.VertexData.Vertices[startVertex + k - 1]; + var movement = next - current; + dirPrev = -dirNext; //(prev - current).normalized; + dirNext = movement.normalized; + var dirInside = (dirNext + dirPrev).normalized * scaledRoadWidth; + var prevSideNormal = sideNormal; + sideNormal = new Vector3(dirNext.z * scaledRoadWidth, 0, -dirNext.x * scaledRoadWidth); + + // decide previous triangle indices based on turnState + if (lastTurnWas == Turn.Right) + { + prevFirst = -1; + prevSecond = -2; + } + else if (lastTurnWas == Turn.Left || lastTurnWas == Turn.Start) + { + prevFirst = -2; + prevSecond = -1; + } + + int baseVertIndex = subVertIndex; + int baseTriIndex = featureTriIndex + baseVertIndex; + + //featureTriIndex is the start of the feature + //baseVertIndex is the start of this section (like corner) + + // precompute side offsets with sign depending on isRight + var isRight = Vector3.Dot(prevSideNormal, dirNext) > 0; + int sign = isRight ? -1 : 1; + + // v0: current ± prevSideNormal + float v0x = current.x + sign * prevSideNormal.x; + float v0z = current.z + sign * prevSideNormal.z; + + // v2: current ± sideNormal + float v2x = current.x + sign * sideNormal.x; + float v2z = current.z + sign * sideNormal.z; + + // v1: current - dirInside + float v1x = current.x - dirInside.x; + float v1z = current.z - dirInside.z; + + // v3: current + dirInside + float v3x = current.x + dirInside.x; + float v3z = current.z + dirInside.z; + + // write vertices + vertices[baseVertIndex ] = new Vector3(v0x, elevation, v0z); + vertices[baseVertIndex + 1] = new Vector3(v1x, elevation, v1z); + vertices[baseVertIndex + 2] = new Vector3(v2x, elevation, v2z); + vertices[baseVertIndex + 3] = new Vector3(v3x, elevation, v3z); + + // write normals (all up) + normals[baseVertIndex ] =Vector3.up; + normals[baseVertIndex + 1] =Vector3.up; + normals[baseVertIndex + 2] =Vector3.up; + normals[baseVertIndex + 3] = Vector3.up; + + // write uvs + uvs[baseVertIndex ] = new Vector2(sign == 1 ? 0 : 1, distance); + uvs[baseVertIndex + 1] = new Vector2(sign == 1 ? 0 : 1, distance); + uvs[baseVertIndex + 2] = new Vector2(sign == 1 ? 0 : 1, distance); + uvs[baseVertIndex + 3] = new Vector2(sign == 1 ? 1 : 0, distance); + + // handy local vars for indices + int i0 = baseTriIndex + 0; + int i1 = baseTriIndex + 1; + int i2 = baseTriIndex + 2; + int i3 = baseTriIndex + 3; + int ipf = baseTriIndex + prevFirst; + int ips = baseTriIndex + prevSecond; + + // prev tris + corner tris + if (isRight) + { + // prev tris + tris[subTriIndex++] = ipf; + tris[subTriIndex++] = ips; + tris[subTriIndex++] = i0; + + tris[subTriIndex++] = i0; + tris[subTriIndex++] = i3; + tris[subTriIndex++] = ipf; + + // corner tris + tris[subTriIndex++] = i0; + tris[subTriIndex++] = i1; + tris[subTriIndex++] = i3; + + tris[subTriIndex++] = i1; + tris[subTriIndex++] = i2; + tris[subTriIndex++] = i3; + + lastTurnWas = Turn.Right; + } + else + { + // prev tris + tris[subTriIndex++] = i0; + tris[subTriIndex++] = ipf; + tris[subTriIndex++] = ips; + + tris[subTriIndex++] = i3; + tris[subTriIndex++] = i0; + tris[subTriIndex++] = ips; + + // corner tris + tris[subTriIndex++] = i0; + tris[subTriIndex++] = i3; + tris[subTriIndex++] = i1; + + tris[subTriIndex++] = i2; + tris[subTriIndex++] = i1; + tris[subTriIndex++] = i3; + + lastTurnWas = Turn.Left; + } + + + subVertIndex += 4; + + if (k == subSize - 2) + { + finishLine = true; + } + + distance += movement.magnitude; + } + else if (k == 0) + { + distance = 0; + var movement = next - current; + dirNext = movement.normalized; + sideNormal = new Vector3(dirNext.z * scaledRoadWidth, 0, -dirNext.x * scaledRoadWidth); + + // round caps are width/2 back and then width/2 to sides + var capSide1x = current.x - (dirNext.x * scaledRoadWidth/2) + (sideNormal.x/2); + var capSide1z = current.z - (dirNext.z * scaledRoadWidth/2) + (sideNormal.z/2); + var capSide2x = current.x - (dirNext.x * scaledRoadWidth/2) - (sideNormal.x/2); + var capSide2z = current.z - (dirNext.z * scaledRoadWidth/2) - (sideNormal.z/2); + var side1x = current.x + sideNormal.x; + var side1z = current.z + sideNormal.z; + var side2x = current.x - sideNormal.x; + var side2z = current.z - sideNormal.z; + + vertices[subVertIndex ] = new Vector3(capSide1x, elevation, capSide1z); + vertices[subVertIndex + 1] = new Vector3(capSide2x, elevation, capSide2z); + vertices[subVertIndex + 2] = new Vector3(side1x, elevation, side1z); + vertices[subVertIndex + 3] = new Vector3(side2x, elevation, side2z); + + normals[subVertIndex ] = Vector3.up; + normals[subVertIndex + 1] = Vector3.up; + normals[subVertIndex + 2] = Vector3.up; + normals[subVertIndex + 3] = Vector3.up; + + //0.25 and 0.75 are just pushing uv coordinates inside a litte to prevent ugly stretching on caps + uvs[subVertIndex ] = new Vector2(0.25f, 0); + uvs[subVertIndex + 1] = new Vector2(0.75f, 0); + uvs[subVertIndex + 2] = new Vector2(0, 0); + uvs[subVertIndex + 3] = new Vector2(1, 0); + + var baseTriIndex = featureTriIndex + subVertIndex; + tris[subTriIndex++] = baseTriIndex + 0; + tris[subTriIndex++] = baseTriIndex + 1; + tris[subTriIndex++] = baseTriIndex + 2; + + tris[subTriIndex++] = baseTriIndex + 2; + tris[subTriIndex++] = baseTriIndex + 1; + tris[subTriIndex++] = baseTriIndex + 3; + + subVertIndex += 4; + lastTurnWas = 0; + + if (subSize == 2) + finishLine = true; + + distance += movement.magnitude; + } + + if (finishLine) + { + current = featureResult.VertexData.Vertices[startVertex + k + 1]; + var dir = (current - featureResult.VertexData.Vertices[startVertex + k]).normalized; + + + if (lastTurnWas == Turn.Left || lastTurnWas == 0) + { + prevFirst = -1; + prevSecond = -2; + } + else if (lastTurnWas == Turn.Right) + { + prevFirst = -2; + prevSecond = -1; + } + + var side1x = current.x + sideNormal.x; + var side1z = current.z + sideNormal.z; + var side2x = current.x - sideNormal.x; + var side2z = current.z - sideNormal.z; + + var capSide1x = current.x + (dir.x * scaledRoadWidth / 2) + (sideNormal.x / 2); + var capSide1z = current.z + (dir.z * scaledRoadWidth / 2) + (sideNormal.z / 2); + var capSide2x = current.x + (dir.x * scaledRoadWidth / 2) - (sideNormal.x / 2); + var capSide2z = current.z + (dir.z * scaledRoadWidth / 2) - (sideNormal.z / 2); + + vertices[subVertIndex ] = new Vector3(side1x, elevation, side1z); + vertices[subVertIndex + 1] = new Vector3(side2x, elevation, side2z); + vertices[subVertIndex + 2] = new Vector3(capSide1x, elevation, capSide1z); + vertices[subVertIndex + 3] = new Vector3(capSide2x, elevation, capSide2z); + + normals[subVertIndex ] = Vector3.up; + normals[subVertIndex + 1] = Vector3.up; + normals[subVertIndex + 2] = Vector3.up; + normals[subVertIndex + 3] = Vector3.up; + + uvs[subVertIndex ] = new Vector2(0, distance); + uvs[subVertIndex + 1] = new Vector2(1, distance); + //0.25 and 0.75 are just pushing uv coordinates inside a litte to prevent ugly stretching on caps + uvs[subVertIndex + 2] = new Vector2(0.25f, distance + scaledRoadWidth/2); + uvs[subVertIndex + 3] = new Vector2(0.75f, distance + scaledRoadWidth/2); + + var baseTriIndex = featureTriIndex + subVertIndex; + tris[subTriIndex++] = baseTriIndex + prevSecond; + tris[subTriIndex++] = baseTriIndex + prevFirst; + tris[subTriIndex++] = baseTriIndex + 0; + + tris[subTriIndex++] = baseTriIndex + 0; + tris[subTriIndex++] = baseTriIndex + prevFirst; + tris[subTriIndex++] = baseTriIndex + 1; + + tris[subTriIndex++] = baseTriIndex + 0; + tris[subTriIndex++] = baseTriIndex + 1; + tris[subTriIndex++] = baseTriIndex + 2; + + tris[subTriIndex++] = baseTriIndex + 2; + tris[subTriIndex++] = baseTriIndex + 1; + tris[subTriIndex++] = baseTriIndex + 3; + + subVertIndex += 4; + } + } + } + + featureTriIndex += designedVertCount; + } + + foreach (var pair in triangleGroups) + { + meshData.Materials.Add(pair.Key); + meshData.Triangles.Add(pair.Value.Triangles); + } + + //TODO we are keeping triangle information inside groups + //and not pass it into stackinfo + //this'll probably break mouse click interactions + //planned with meshinfo.tri information in mind + //currently when user clicks and we get triIndex info from hit + //there'll be no data to match it, and highlight selection or something + + return meshData; + } + + private int VertexNeed(FeatureVertexData data) + { + var total = 0; + for (var i = 0; i < data.Submeshes.Count - 1; i++) + { + var size = data.Submeshes[i + 1] - data.Submeshes[i]; + total += 4 * size; //(size - 2) * 4 + (2 * 2); //two vertices for cap, 4 for each mid vertex + } + + return total; + } + + public override List CreateGo(CanonicalTileId tileId, MeshData meshData) + { + var objectList = new List(); + var entity = _buildingObjectPool.GetObject(); + var mats = new Material[meshData.Triangles.Count]; + // for (int i = 0; i < meshData.Triangles.Count; i++) + // { + // mats[i] = _settings.Material; + // } + for (var i = 0; i < meshData.Triangles.Count; i++) + { + mats[i] = meshData.Materials[i]; + } + + entity.MeshRenderer.materials = mats; + + entity.GameObject.transform.SetParent(_layerRootObject); + entity.StackId = 0; + + var mesh = entity.Mesh; + mesh.Clear(); + mesh.SetVertices(meshData.Vertices); + mesh.SetNormals(meshData.Normals); + mesh.SetUVs(0, meshData.UVs); + mesh.indexFormat = IndexFormat.UInt32; + + mesh.subMeshCount = meshData.Triangles.Count; + for (var index = 0; index < meshData.Triangles.Count; index++) + { + var submesh = meshData.Triangles[index]; + mesh.SetTriangles(submesh, index); + } + + entity.MeshFilter.sharedMesh = mesh; + objectList.Add(entity.GameObject); + + if (!_results.ContainsKey(tileId)) + _results.Add(tileId, new List()); + _results[tileId].Add(entity); + OnBuildingMeshCreated(tileId, entity.GameObject, meshData); + + // foreach (var vertex in meshData.Vertices) + // { + // var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); + // go.transform.position = vertex; + // go.transform.localScale = Vector3.one * 0.001f; + // go.transform.SetParent(entity.Transform); + // } + + return objectList; + } + + private RoadFeatureUnity GetFeature(VectorTileLayer layer, int i, ref int classTagIndex, ref int typeTagIndex, ref int structureTagIndex) + { + var view = layer.GetViewFor(i); + var layerData = layer.Data.Slice(view.x, view.y); + var featureReader = new PbfReader(layerData); + var feature = new RoadFeatureUnity(); + bool geomTypeSet = false; + while (featureReader.NextByte()) + { + int featureType = featureReader.Tag; + switch ((FeatureType)featureType) + { + case FeatureType.Id: + feature.Id = (ulong)featureReader.Varint(); + break; + case FeatureType.Tags: + var tags = featureReader.GetPackedInt(); + feature.Tags = tags; + break; + case FeatureType.Type: + int geomType = (int)featureReader.Varint(); + feature.GeometryType = (GeomType)geomType; + geomTypeSet = true; + break; + case FeatureType.Geometry: + if (null != feature.GeometryCommands) + { + throw new Exception(string.Format("Layer [{0}], feature already has a geometry", + layer.Name)); + } + + //get raw array of commands and coordinates + feature.GeometryCommands = featureReader.GetPackedUnit32(); + break; + default: + featureReader.Skip(); + break; + } + } + + // Discard features with missing/malformed geometry + if (feature.GeometryCommands == null) return null; + + feature.SetProperties(ref layer, ref classTagIndex, ref typeTagIndex, ref structureTagIndex); + return feature; + } + } +} diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs.meta new file mode 100644 index 000000000..cc6700e94 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fb3c26c6e32644694aa0988af913c9a6 +timeCreated: 1744797730 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs new file mode 100644 index 000000000..4d2306a5e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.Filters; +using Mapbox.VectorModule.Unity; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [DisplayName("Road Component")] + [CreateAssetMenu(menuName = "Mapbox/Layer Visualizers/Road Component Visualizer")] + public class RoadComponentVisualizerObject : LayerVisualizerConstructor, IPerformanceLayerVisualizer + { + public RoadComponentSettings Settings; + public bool IsActive = true; + + private RoadComponentVisualizer _layerVisualizer; + + public IVectorLayerVisualizer GetLayerVisualizer() + { + return _layerVisualizer; + } + + public override IVectorLayerVisualizer ConstructLayerVisualizer(IMapInformation mapInformation, UnityContext unityContext) + { + Settings.RoadStyleSheet.Initialize(); + _layerVisualizer = new RoadComponentVisualizer("road", mapInformation, unityContext, Settings); + _layerVisualizer.Active = IsActive; + + _layerVisualizer.OnVectorMeshCreated += OnVectorMeshCreated; + _layerVisualizer.OnVectorMeshDestroyed += OnVectorMeshDestroyed; + + return _layerVisualizer; + } + + public Action OnVectorMeshCreated = list => { }; + public Action OnVectorMeshDestroyed = go => { }; + } + + [Serializable] + public class RoadComponentSettings + { + [Tooltip("Vertical offset to be applied to the road component.")] + public float GameObjectOffset; + [Tooltip("Road segments will be offset by a small amount to prevent z-fighting issues.")] + public float RandomOffsetRange = 0.0001f; + public RoadStyleSheet RoadStyleSheet; + + public RoadComponentSettings() + { + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs.meta new file mode 100644 index 000000000..87a0a0fe8 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e926a31006c7a440ea45ffe7b2bcfff6 +timeCreated: 1744829680 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs new file mode 100644 index 000000000..92db1719f --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs @@ -0,0 +1,49 @@ +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorTile.Geometry; +using UnityEngine; +using DecodeGeometry = Mapbox.VectorModule.ComponentSystem.Data.DecodeGeometry; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + public class RoadFeatureUnity + { + public ulong Id; + public GeomType GeometryType; + public uint[] GeometryCommands; + public FeatureVertexData VertexData; + public int[] Tags; + public string Class; + public string Type; + public string Structure; + + public void SetProperties(ref VectorTileLayer layer, ref int classTagIndex, ref int typeTagIndex, ref int structureTagIndex) + { + if (Tags == null) return; // Some features have no tags + + var tagCount = Tags.Length; + //some features have odd number of tags + //not sure if it's a bug or data issue + //so -1 here to skip last single tag + for (int i = 0; i < tagCount - 1; i += 2) + { + if (classTagIndex != -1 && Tags[i] == classTagIndex) + { + Class = layer.Values[Tags[i + 1]].ToString(); + } + else if (typeTagIndex != -1 && Tags[i] == typeTagIndex) + { + Type = layer.Values[Tags[i + 1]].ToString(); + } + else if (structureTagIndex != -1 && Tags[i] == structureTagIndex) + { + Structure = layer.Values[Tags[i + 1]].ToString(); + } + } + } + + public FeatureVertexData Geometry(Vector3 scale) + { + return DecodeGeometry.GetGeometry(GeometryCommands, scale); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs.meta new file mode 100644 index 000000000..5b24a6479 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5fb907227ade4bbfa628c03e59c903fa +timeCreated: 1765457966 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs new file mode 100644 index 000000000..9bc037c1a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Mapbox.VectorModule.Filters; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [Serializable] + public abstract class RoadFilter + { + public RoadFilter() + { + + } + + public abstract void Initialize(); + public abstract bool Try(RoadFeatureUnity feature); + } + + [Serializable] + [DisplayName("Class Filter")] + public class RoadClassFilter : RoadFilter + { + public StringCheckOperation CheckOperation = StringCheckOperation.Equals; + public string FilterString; + public bool Invert; + private HashSet _types = new HashSet(); + + public override void Initialize() + { + FilterString = FilterString.ToLowerInvariant(); + if (CheckOperation == StringCheckOperation.Contains) + { + _types = new HashSet(); + foreach (var s in FilterString.Split(',')) + { + _types.Add(s.Trim().ToLowerInvariant()); + } + } + } + + public override bool Try(RoadFeatureUnity feature) + { + if (feature.Class == null) return false; + + var result = false; + if (CheckOperation == StringCheckOperation.Equals) + { + result = FilterString == feature.Class; + } + else if (CheckOperation == StringCheckOperation.Contains) + { + result = _types.Contains(feature.Class); + } + + return !Invert ? result : !result; + } + } + + [Serializable] + [DisplayName("Structure Filter")] + public class RoadStructureFilter : RoadFilter + { + public StringCheckOperation CheckOperation = StringCheckOperation.Equals; + public string FilterString; + public bool Invert; + private HashSet _types = new HashSet(); + + public override void Initialize() + { + FilterString = FilterString.ToLowerInvariant(); + if (CheckOperation == StringCheckOperation.Contains) + { + _types = new HashSet(); + foreach (var s in FilterString.Split(',')) + { + _types.Add(s.Trim().ToLowerInvariant()); + } + } + } + + public override bool Try(RoadFeatureUnity feature) + { + if (feature.Structure == null) return false; + + var result = false; + if (CheckOperation == StringCheckOperation.Equals) + { + result = FilterString == feature.Structure; + } + else if (CheckOperation == StringCheckOperation.Contains) + { + result = _types.Contains(feature.Structure); + } + + return !Invert ? result : !result; + } + } + + [Serializable] + [DisplayName("Type Filter")] + public class RoadTypeFilter : RoadFilter + { + public StringCheckOperation CheckOperation = StringCheckOperation.Equals; + public string FilterString; + public bool Invert; + private HashSet _types = new HashSet(); + + public override void Initialize() + { + FilterString = FilterString.ToLowerInvariant(); + if (CheckOperation == StringCheckOperation.Contains) + { + _types = new HashSet(); + foreach (var s in FilterString.Split(',')) + { + _types.Add(s.Trim().ToLowerInvariant()); + } + } + } + + public override bool Try(RoadFeatureUnity feature) + { + if (feature.Type == null) return false; + + var result = false; + if (CheckOperation == StringCheckOperation.Equals) + { + result = FilterString == feature.Type; + } + else if (CheckOperation == StringCheckOperation.Contains) + { + result = _types.Contains(feature.Type); + } + + return !Invert ? result : !result; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs.meta new file mode 100644 index 000000000..713e6eab2 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bc1f26eac70c4ed88fcec9939dfbd8a6 +timeCreated: 1765457941 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs new file mode 100644 index 000000000..38d5b6f6c --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs @@ -0,0 +1,22 @@ +using System; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [Serializable] + public class RoadStyle + { + [NonSerialized] public int RuntimeId; + [Tooltip("The name of the road style (used only for the UI)")] + public string Name; + public ComponentFilterStack Filters; + public float Width; + public float PushUp; + public Material Material; + + public void Initialize() + { + Filters.Initialize(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs.meta new file mode 100644 index 000000000..bb0d5fcfe --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 14e833ab58144fdda5c1465744f677ad +timeCreated: 1765457926 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs new file mode 100644 index 000000000..531c6f1a4 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [DisplayName("Road Style Sheet")] + [CreateAssetMenu(menuName = "Mapbox/Road Style Sheet")] + public class RoadStyleSheet : ScriptableObject + { + public List Styles; + + public void Initialize() + { + foreach (var style in Styles) + { + style.Initialize(); + } + } + + public bool TryGetStyle(RoadFeatureUnity feature, out RoadStyle style) + { + for (int i = 0; i < Styles.Count; i++) + { + if (Styles[i].Filters.Try(feature)) + { + style = Styles[i]; + return true; + } + } + style = null; + return false; + } + + public bool Contains(RoadFeatureUnity feature) + { + for (int i = 0; i < Styles.Count; i++) + { + if (Styles[i].Filters.Try(feature)) + { + return true; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs.meta new file mode 100644 index 000000000..14aeac370 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bf8c281932374fb0b5c80fa7fbca2b5b +timeCreated: 1765441634 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs b/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs new file mode 100644 index 000000000..7b7dad175 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs @@ -0,0 +1,307 @@ +#if UNITY_EDITOR +using System; +using System.ComponentModel; +using System.Linq; +using Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer; +using UnityEditor; +using UnityEngine; +using Mapbox.VectorModule.MeshGeneration.Unity; +using Mapbox.VectorModule.Filters; // where FilterBase lives + +[CustomPropertyDrawer(typeof(ComponentFilterStack))] +public class ComponentFilterStackDrawer : PropertyDrawer +{ + private const float LineSpacing = 2f; + private const float BoxPadding = 4f; + private const float HeaderHeight = 18f; + + // Cached FilterBase-derived types + private static Type[] _filterTypes; + private static string[] _filterTypePaths; + private static bool _typesInitialized; + + private static void EnsureFilterTypes() + { + if (_typesInitialized) + return; + + _typesInitialized = true; + + var baseType = typeof(RoadFilter); + + var types = TypeCache.GetTypesDerivedFrom() + .Where(t => t != null && !t.IsAbstract) + .ToArray(); + + _filterTypes = types; + + _filterTypePaths = _filterTypes + .Select(t => + { + // Use DisplayNameAttribute if present for pretty menu paths + var attr = t + .GetCustomAttributes(typeof(DisplayNameAttribute), true) + .FirstOrDefault() as DisplayNameAttribute; + return attr != null && !string.IsNullOrEmpty(attr.DisplayName) + ? attr.DisplayName + : t.Name; + }) + .ToArray(); + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + EnsureFilterTypes(); + + var typeProp = property.FindPropertyRelative("Type"); + var filtersProp = property.FindPropertyRelative("Filters"); + + // Foldout + Rect foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label, true); + + if (!property.isExpanded) + { + EditorGUI.EndProperty(); + return; + } + + EditorGUI.indentLevel++; + + float y = foldoutRect.y + EditorGUIUtility.singleLineHeight + LineSpacing; + float width = position.width; + + // Combiner type + if (typeProp != null) + { + Rect typeRect = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(typeRect, typeProp, new GUIContent("Process if")); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + if (filtersProp == null) + { + Rect warnRect = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight * 2f); + EditorGUI.HelpBox(warnRect, + "Filters list not found.\n" + + "Ensure it's declared as:\n[SerializeReference] public List Filters;", + MessageType.Warning); + y += warnRect.height + LineSpacing; + } + else + { + // Filters label + // Rect labelRect = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + // EditorGUI.LabelField(labelRect, "Filters", EditorStyles.boldLabel); + // y += EditorGUIUtility.singleLineHeight + LineSpacing; + + if (filtersProp.arraySize == 0) + { + Rect infoRect = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight * 1.5f); + EditorGUI.HelpBox(infoRect, "No filters. Use 'Add Filter' to create one.", MessageType.Info); + y += infoRect.height + LineSpacing; + } + else + { + // Draw each filter as a nice box + for (int i = 0; i < filtersProp.arraySize; i++) + { + SerializedProperty element = filtersProp.GetArrayElementAtIndex(i); + float elementHeight = EditorGUI.GetPropertyHeight(element, true); + + float boxHeight = HeaderHeight + elementHeight + BoxPadding * 2f; + Rect boxRect = new Rect(position.x, y, width, boxHeight); + GUI.Box(boxRect, GUIContent.none); + + float innerX = boxRect.x + BoxPadding; + float innerY = boxRect.y + BoxPadding; + float innerW = boxRect.width - BoxPadding * 2f; + + if (element != null) + { + // Header: "Filter i (TypeName)" + Remove button + string typeName = GetManagedReferenceTypeName(element) ?? "Null"; + string header = string.Format("{0} - {1}", typeName, element.FindPropertyRelative("FilterString")?.stringValue); + + Rect headerLabelRect = new Rect(innerX, innerY, innerW - 70f, EditorGUIUtility.singleLineHeight); + EditorGUI.LabelField(headerLabelRect, header, EditorStyles.boldLabel); + } + + Rect removeRect = new Rect(innerX + innerW - 65f, innerY, 60f, + EditorGUIUtility.singleLineHeight); + if (GUI.Button(removeRect, "Remove")) + { + filtersProp.DeleteArrayElementAtIndex(i); + property.serializedObject.ApplyModifiedProperties(); + EditorGUI.indentLevel--; + EditorGUI.EndProperty(); + return; + } + + innerY += HeaderHeight - (EditorGUIUtility.singleLineHeight - LineSpacing); + + // Filter fields + Rect contentRect = new Rect(innerX, innerY, innerW, elementHeight); + EditorGUI.PropertyField(contentRect, element, GUIContent.none, true); + + y += boxHeight + LineSpacing; + } + } + + // Add / Clear buttons + float buttonHeight = EditorGUIUtility.singleLineHeight; + float halfWidth = (width - 2f) * 0.5f; + + Rect addRect = new Rect(position.x, y, halfWidth, buttonHeight); + //Rect clearRect = new Rect(position.x + halfWidth + 2f, y, halfWidth, buttonHeight); + + if (GUI.Button(addRect, "Add Filter")) + { + ShowAddMenu(addRect, filtersProp, property.serializedObject); + } + + //GUI.enabled = filtersProp.arraySize > 0; + // if (GUI.Button(clearRect, "Clear All")) + // { + // filtersProp.ClearArray(); + // property.serializedObject.ApplyModifiedProperties(); + // } + //GUI.enabled = true; + + y += buttonHeight + LineSpacing; + } + + EditorGUI.indentLevel--; + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + float height = EditorGUIUtility.singleLineHeight; // foldout + + if (!property.isExpanded) + return height; + + float spacing = LineSpacing; + var typeProp = property.FindPropertyRelative("Type"); + var filtersProp = property.FindPropertyRelative("Filters"); + + height += spacing; // after foldout + + if (typeProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (filtersProp == null) + { + height += EditorGUIUtility.singleLineHeight * 2f + spacing; + return height; + } + + // "Filters" label + height += EditorGUIUtility.singleLineHeight + spacing; + + if (filtersProp.arraySize == 0) + { + height += EditorGUIUtility.singleLineHeight * 1.5f + spacing; + } + else + { + for (int i = 0; i < filtersProp.arraySize; i++) + { + var element = filtersProp.GetArrayElementAtIndex(i); + float elementHeight = EditorGUI.GetPropertyHeight(element, true); + float boxHeight = HeaderHeight + elementHeight + BoxPadding * 2f; + height += boxHeight + spacing; + } + } + + // Add / Clear buttons + height += EditorGUIUtility.singleLineHeight + spacing; + + return height; + } + + // --------- Add menu + helpers --------- + + private static void ShowAddMenu(Rect buttonRect, SerializedProperty filtersProp, SerializedObject serializedObject) + { + EnsureFilterTypes(); + + if (_filterTypes == null || _filterTypes.Length == 0) + { + EditorUtility.DisplayDialog( + "No filter types found", + "No non-abstract RoadFilter implementations found in loaded assemblies.", + "OK"); + return; + } + + // Need a stable reference to re-find the property later + var target = serializedObject.targetObject; + string propPath = filtersProp.propertyPath; + + GenericMenu menu = new GenericMenu(); + + for (int i = 0; i < _filterTypes.Length; i++) + { + Type type = _filterTypes[i]; + string path = _filterTypePaths[i]; + menu.AddItem(new GUIContent(path), false, () => + { + AddFilterOfType(target, propPath, type); + }); + } + + menu.DropDown(buttonRect); + } + + private static void AddFilterOfType(UnityEngine.Object target, string filtersPropPath, Type type) + { + if (target == null || type == null) + return; + + var so = new SerializedObject(target); + var filtersProp = so.FindProperty(filtersPropPath); + if (filtersProp == null || !filtersProp.isArray) + return; + + int index = filtersProp.arraySize; + filtersProp.InsertArrayElementAtIndex(index); + SerializedProperty element = filtersProp.GetArrayElementAtIndex(index); + + if (element.propertyType == SerializedPropertyType.ManagedReference) + { + object instance = Activator.CreateInstance(type); + element.managedReferenceValue = instance; + } + else + { + // Wrong setup; clean up and bail + filtersProp.DeleteArrayElementAtIndex(index); + } + + so.ApplyModifiedProperties(); + } + + private static string GetManagedReferenceTypeName(SerializedProperty prop) + { + if (!prop.hasMultipleDifferentValues && !string.IsNullOrEmpty(prop.managedReferenceFullTypename)) + { + // "AssemblyName TypeFullName" + string fullName = prop.managedReferenceFullTypename; + int spaceIndex = fullName.IndexOf(' '); + if (spaceIndex >= 0 && spaceIndex < fullName.Length - 1) + { + string typeName = fullName.Substring(spaceIndex + 1); + int lastDot = typeName.LastIndexOf('.'); + if (lastDot >= 0 && lastDot < typeName.Length - 1) + return typeName.Substring(lastDot + 1); + return typeName; + } + } + return null; + } +} +#endif diff --git a/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs.meta b/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs.meta new file mode 100644 index 000000000..352381eff --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7a086f27c0bb4aff9fb799c63f34baa0 +timeCreated: 1765445147 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs b/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs new file mode 100644 index 000000000..4a003e8ee --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs @@ -0,0 +1,95 @@ +using System; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.VectorModule.Editor +{ + [CustomPropertyDrawer(typeof(GeometryExtrusionOptions))] + public class GeometryExtrusionOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + GUILayout.Space(-EditorGUIUtility.singleLineHeight - 2); + + // Find properties + var extrusionTypeProp = property.FindPropertyRelative("extrusionType"); + var geomTypeProp = property.FindPropertyRelative("extrusionGeometryType"); + var propertyNameProp = property.FindPropertyRelative("propertyName"); + var minHeightProp = property.FindPropertyRelative("minimumHeight"); + var maxHeightProp = property.FindPropertyRelative("maximumHeight"); + var scaleFactorProp = property.FindPropertyRelative("extrusionScaleFactor"); + + EditorGUILayout.PropertyField(geomTypeProp); + + //part to render extrusion type in dropbox without none setting + { + // Get all enum values except 'None' (index 0) + var enumValues = (ExtrusionType[])Enum.GetValues(typeof(ExtrusionType)); + var enumNames = Enum.GetNames(typeof(ExtrusionType)); + + // Skip "None" + var displayedValues = new ExtrusionType[enumValues.Length - 1]; + var displayedNames = new string[enumValues.Length - 1]; + Array.Copy(enumValues, 1, displayedValues, 0, displayedValues.Length); + Array.Copy(enumNames, 1, displayedNames, 0, displayedNames.Length); + + // Determine current value + var currentType = (ExtrusionType)extrusionTypeProp.enumValueIndex; + + // If current value is None, default to first visible option + if (currentType == ExtrusionType.None) + currentType = displayedValues[0]; + + // Draw dropdown without "None" + int selectedIndex = Array.IndexOf(displayedValues, currentType); + int newIndex = EditorGUILayout.Popup("Extrusion Type", Mathf.Max(selectedIndex, 0), displayedNames); + + extrusionTypeProp.enumValueIndex = (int)displayedValues[newIndex]; + } + + // Determine which ExtrusionType is selected + var extrusionType = (ExtrusionType)extrusionTypeProp.enumValueIndex; + + DrawReadOnlyProperty(propertyNameProp); + + // Show common field for all types that need propertyName + switch (extrusionType) + { + case ExtrusionType.PropertyHeight: + + break; + + case ExtrusionType.AbsoluteHeight: + EditorGUILayout.PropertyField(maxHeightProp, new GUIContent("Height")); + break; + + case ExtrusionType.MinHeight: + EditorGUILayout.PropertyField(minHeightProp); + break; + + case ExtrusionType.MaxHeight: + EditorGUILayout.PropertyField(maxHeightProp); + break; + case ExtrusionType.RangeHeight: + EditorGUILayout.PropertyField(minHeightProp); + EditorGUILayout.PropertyField(maxHeightProp); + break; + + default: + break; + } + + void DrawReadOnlyProperty(SerializedProperty prop) + { + GUI.enabled = false; + EditorGUILayout.PropertyField(prop); + GUI.enabled = true; + } + + EditorGUI.EndProperty(); + } + } +} diff --git a/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs.meta b/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs.meta new file mode 100644 index 000000000..f80329b4a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ecf2256f8fa924ec1832c2fcb6b5d36f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/Editor/HeightModifierEditor.cs b/Runtime/Mapbox/VectorModule/Editor/HeightModifierEditor.cs index bc9c7120c..7ee0c5eaf 100644 --- a/Runtime/Mapbox/VectorModule/Editor/HeightModifierEditor.cs +++ b/Runtime/Mapbox/VectorModule/Editor/HeightModifierEditor.cs @@ -18,6 +18,11 @@ public class HeightModifierObjectEditor : UnityEditor.Editor void OnEnable() { + if (targets[0] == null) + { + return; + } + // Cache the main SerializedProperties extrusionOptionsProp = serializedObject.FindProperty("ExtrusionOptions"); extrusionTypeProp = extrusionOptionsProp.FindPropertyRelative("extrusionType"); diff --git a/Runtime/Mapbox/VectorModule/Editor/Mapbox.VectorModule.Editor.asmdef b/Runtime/Mapbox/VectorModule/Editor/Mapbox.VectorModule.Editor.asmdef index 682b489e8..42a069f9f 100644 --- a/Runtime/Mapbox/VectorModule/Editor/Mapbox.VectorModule.Editor.asmdef +++ b/Runtime/Mapbox/VectorModule/Editor/Mapbox.VectorModule.Editor.asmdef @@ -4,7 +4,8 @@ "references": [ "GUID:2400806fb903448e5b7e737b92f1e434", "GUID:dcd3af994807b4804b1e69cd4a2bdfed", - "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:5aef8304858a8459cb2204d15bf75ab9" ], "includePlatforms": [ "Editor" diff --git a/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs b/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs new file mode 100644 index 000000000..87b06c3e9 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs @@ -0,0 +1,38 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using System.Collections.Generic; +using Mapbox.VectorModule.MeshGeneration.GameObjectModifiers; + +[CustomEditor(typeof(PropertiesViewerBehaviour))] +public class PropertiesViewerBehaviourEditor : Editor +{ + public override void OnInspectorGUI() + { + var behaviour = (PropertiesViewerBehaviour)target; + + if (behaviour.Properties == null) + behaviour.Properties = new Dictionary(); + + EditorGUILayout.LabelField("Properties", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + + var keys = new List(behaviour.Properties.Keys); + foreach (var key in keys) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(key, GUILayout.MaxWidth(150)); + string newValue = EditorGUILayout.TextField(behaviour.Properties[key]); + if (newValue != behaviour.Properties[key]) + { + Undo.RecordObject(behaviour, "Edit Property Value"); + behaviour.Properties[key] = newValue; + EditorUtility.SetDirty(behaviour); + } + EditorGUILayout.EndHorizontal(); + } + + EditorGUI.indentLevel--; + } +} +#endif \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs.meta b/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs.meta new file mode 100644 index 000000000..f43e371e1 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ad8df528d29f74fc28f7e7f63b1533e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs b/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs new file mode 100644 index 000000000..769544019 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs @@ -0,0 +1,117 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer; +using Mapbox.VectorModule.MeshGeneration.Unity; + +[CustomPropertyDrawer(typeof(RoadStyle))] +public class RoadStyleDrawer : PropertyDrawer +{ + private const float LineSpacing = 2f; + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + var nameProp = property.FindPropertyRelative("Name"); + var widthProp = property.FindPropertyRelative("Width"); + var pushUpProp = property.FindPropertyRelative("PushUp"); + var materialProp = property.FindPropertyRelative("Material"); + var filtersProp = property.FindPropertyRelative("Filters"); + + // Foldout + Rect foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, nameProp.stringValue, true); + + if (!property.isExpanded) + { + EditorGUI.EndProperty(); + return; + } + + EditorGUI.indentLevel++; + + float y = foldoutRect.y + EditorGUIUtility.singleLineHeight + LineSpacing; + float width = position.width; + + // ClassName + if (nameProp != null) + { + Rect r = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(r, nameProp); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + // Width + if (widthProp != null) + { + Rect r = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(r, widthProp); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + // Width + if (pushUpProp != null) + { + Rect r = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(r, pushUpProp); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + // Material + if (materialProp != null) + { + Rect r = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(r, materialProp); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + // Filters: delegate to VectorFilterStackDrawer via PropertyField(rect,..., includeChildren: true) + if (filtersProp != null) + { + float filtersHeight = EditorGUI.GetPropertyHeight(filtersProp, true); + Rect r = new Rect(position.x, y, width, filtersHeight); + EditorGUI.PropertyField(r, filtersProp, new GUIContent("Filters"), true); + y += filtersHeight + LineSpacing; + } + + EditorGUI.indentLevel--; + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + float height = EditorGUIUtility.singleLineHeight; // foldout + + if (!property.isExpanded) + return height; + + float spacing = LineSpacing; + var nameProp = property.FindPropertyRelative("Name"); + var widthProp = property.FindPropertyRelative("Width"); + var pushUpProp = property.FindPropertyRelative("PushUp"); + var materialProp = property.FindPropertyRelative("Material"); + var filtersProp = property.FindPropertyRelative("Filters"); + + if (nameProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (widthProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (pushUpProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (materialProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (filtersProp != null) + { + float filtersHeight = EditorGUI.GetPropertyHeight(filtersProp, true); + height += filtersHeight + spacing; + } + + return height; + } +} +#endif diff --git a/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs.meta b/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs.meta new file mode 100644 index 000000000..8bb5182ac --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 43c99405970244b5badb4479094e6702 +timeCreated: 1765443332 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs b/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs new file mode 100644 index 000000000..1edb69642 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs @@ -0,0 +1,94 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; +using Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer; + +[CustomEditor(typeof(RoadStyleSheet))] +public class RoadStyleSheetEditor : Editor +{ + private SerializedProperty _stylesProp; + private ReorderableList _stylesList; + + private void OnEnable() + { + _stylesProp = serializedObject.FindProperty("Styles"); + + if (_stylesProp == null) + return; + + _stylesList = new ReorderableList(serializedObject, _stylesProp, true, true, true, true); + + _stylesList.drawHeaderCallback = rect => + { + EditorGUI.LabelField(rect, "Road Styles"); + }; + + _stylesList.elementHeightCallback = index => + { + var element = _stylesProp.GetArrayElementAtIndex(index); + float h = EditorGUI.GetPropertyHeight(element, true); + return h + 4f; + }; + + _stylesList.drawElementCallback = (rect, index, isActive, isFocused) => + { + var element = _stylesProp.GetArrayElementAtIndex(index); + rect.y += 2f; + rect.height = EditorGUI.GetPropertyHeight(element, true); + EditorGUI.PropertyField(rect, element, new GUIContent("Style " + index), true); + }; + + _stylesList.onAddCallback = list => + { + int index = _stylesProp.arraySize; + _stylesProp.InsertArrayElementAtIndex(index); + + var newElement = _stylesProp.GetArrayElementAtIndex(index); + var classNameProp = newElement.FindPropertyRelative("ClassName"); + var widthProp = newElement.FindPropertyRelative("Width"); + var materialProp = newElement.FindPropertyRelative("Material"); + var filtersProp = newElement.FindPropertyRelative("Filters"); + + if (classNameProp != null) classNameProp.stringValue = string.Empty; + if (widthProp != null) widthProp.floatValue = 1f; + if (materialProp != null) materialProp.objectReferenceValue = null; + + // Make sure Filters exists + if (filtersProp != null) + { + var typeProp = filtersProp.FindPropertyRelative("Type"); + var listProp = filtersProp.FindPropertyRelative("Filters"); + + if (typeProp != null) + typeProp.enumValueIndex = 0; // default enum + + if (listProp != null && listProp.isArray) + listProp.arraySize = 0; + } + + serializedObject.ApplyModifiedProperties(); + }; + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.HelpBox( + "Road Style Sheet.\nConfigure styles per road class. Each style has a filter stack.", + MessageType.Info); + + if (_stylesList != null) + { + _stylesList.DoLayoutList(); + } + else + { + EditorGUILayout.PropertyField(_stylesProp, true); + } + + serializedObject.ApplyModifiedProperties(); + } +} +#endif diff --git a/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs.meta b/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs.meta new file mode 100644 index 000000000..49ca936d4 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a5ebee2591fe46999865bf745f99587d +timeCreated: 1765442427 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/MeshGeneration/Earcut.cs b/Runtime/Mapbox/VectorModule/MeshGeneration/Earcut.cs index f71d34c00..55f328752 100644 --- a/Runtime/Mapbox/VectorModule/MeshGeneration/Earcut.cs +++ b/Runtime/Mapbox/VectorModule/MeshGeneration/Earcut.cs @@ -1,17 +1,18 @@ using System; using System.Collections.Generic; +using Mapbox.VectorTile.Geometry; using UnityEngine; namespace Mapbox.VectorModule.MeshGeneration { public static class EarcutLibrary { - public static List Earcut(List data, List holeIndices, int dim) + public static List Earcut(float[] data, List holeIndices, int dim) { dim = Math.Max(dim, 2); var hasHoles = holeIndices.Count; - var outerLen = hasHoles > 0 ? holeIndices[0] * dim : data.Count; + var outerLen = hasHoles > 0 ? holeIndices[0] * dim : data.Length; var outerNode = linkedList(data, 0, outerLen, dim, true); var triangles = new List((int)(outerNode.i * 1.5)); @@ -27,7 +28,52 @@ public static List Earcut(List data, List holeIndices, int dim) if (hasHoles > 0) outerNode = EliminateHoles(data, holeIndices, outerNode, dim); // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox - if (data.Count > 80 * dim) + if (data.Length > 80 * dim) + { + minX = maxX = data[0]; + minY = maxY = data[1]; + + for (var i = dim; i < outerLen; i += dim) + { + x = data[i]; + y = data[i + 1]; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + // minX, minY and size are later used to transform coords into integers for z-order calculation + size = Math.Max(maxX - minX, maxY - minY); + } + + earcutLinked(outerNode, triangles, dim, minX, minY, size); + + return triangles; + } + + public static List Earcut(float[] data, int[] holeIndices, int dim) + { + dim = Math.Max(dim, 2); + + var hasHoles = holeIndices.Length; + var outerLen = hasHoles > 0 ? holeIndices[0] * dim : data.Length; + var outerNode = linkedList(data, 0, outerLen, dim, true); + var triangles = new List((int)(outerNode.i * 1.5)); + + if (outerNode == null) return triangles; + var minX = 0f; + var minY = 0f; + var maxX = 0f; + var maxY = 0f; + var x = 0f; + var y = 0f; + var size = 0f; + + if (hasHoles > 0) outerNode = EliminateHoles(data, holeIndices, outerNode, dim); + + // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox + if (data.Length > 80 * dim) { minX = maxX = data[0]; minY = maxY = data[1]; @@ -380,7 +426,7 @@ private static Node sortLinked(Node list) return list; } - private static Node EliminateHoles(List data, List holeIndices, Node outerNode, int dim) + private static Node EliminateHoles(float[] data, List holeIndices, Node outerNode, int dim) { var i = 0; var len = holeIndices.Count; @@ -391,16 +437,39 @@ private static Node EliminateHoles(List data, List holeIndices, Node for (i = 0; i < len; i++) { start = holeIndices[i] * dim; - end = i < len - 1 ? holeIndices[i + 1] * dim : data.Count; + end = i < len - 1 ? holeIndices[i + 1] * dim : data.Length; list = linkedList(data, start, end, dim, false); if (list == list.next) list.steiner = true; queue.Add(getLeftmost(list)); } - queue.Sort(delegate (Node a, Node b) + queue.Sort(delegate(Node a, Node b) { return a.x.CompareTo(b.x); }); + + // process holes from left to right + for (i = 0; i < queue.Count; i++) + { + EliminateHole(queue[i], outerNode); + outerNode = FilterPoints(outerNode, outerNode.next); + } + + return outerNode; + } + + private static Node EliminateHoles(float[] data, int[] holeIndices, Node outerNode, int dim) + { + var i = 0; + var len = holeIndices.Length; + var queue = new List(len); + for (i = 0; i < len; i++) { - return (int)Math.Ceiling(a.x - b.x); - }); + var start = holeIndices[i] * dim; + var end = i < len - 1 ? holeIndices[i + 1] * dim : data.Length; + var list = linkedList(data, start, end, dim, false); + if (list == list.next) list.steiner = true; + queue.Add(getLeftmost(list)); + } + + queue.Sort((a, b) => a.x.CompareTo(b.x)); // process holes from left to right for (i = 0; i < queue.Count; i++) @@ -495,9 +564,11 @@ private static Node FindHoleBridge(Node hole, Node outerNode) if (hy == p.y) return p; if (hy == p.next.y) return p.next; } + m = p.x < p.next.x ? p : p.next; } } + p = p.next; } while (p != outerNode); @@ -540,9 +611,9 @@ private static Node FindHoleBridge(Node hole, Node outerNode) private static bool locallyInside(Node a, Node b) { - return area(a.prev, a, a.next) < 0 ? - area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 : - area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; + return area(a.prev, a, a.next) < 0 + ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 + : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; } private static float area(Node p, Node q, Node r) @@ -571,7 +642,7 @@ private static Node getLeftmost(Node start) } // create a circular doubly linked list from polygon points in the specified winding order - private static Node linkedList(List data, int start, int end, int dim, bool clockwise) + private static Node linkedList(float[] data, int start, int end, int dim, bool clockwise) { var i = 0; Node last = null; @@ -608,7 +679,7 @@ private static bool equals(Node p1, Node p2) return p1.x == p2.x && p1.y == p2.y; } - private static float signedArea(List data, int start, int end, int dim) + private static float signedArea(float[] data, int start, int end, int dim) { var sum = 0f; var j = end - dim; @@ -649,39 +720,90 @@ public static Data Flatten(List> data) totalVertCount += data[i].Count; } - var result = new Data() { Dim = 2 }; - result.Vertices = new List(totalVertCount * 2); + var result = new Data(); + result.Vertices = new float[totalVertCount * 2]; + result.Holes = new int[dataCount - 1]; var holeIndex = 0; + var index = 0; + var holeNumber = 0; for (var i = 0; i < dataCount; i++) { var subCount = data[i].Count; for (var j = 0; j < subCount; j++) { - result.Vertices.Add(data[i][j][0]); - result.Vertices.Add(data[i][j][2]); + result.Vertices[index++] = (data[i][j][0]); + result.Vertices[index++] = (data[i][j][2]); } + if (i > 0) { holeIndex += data[i - 1].Count; - result.Holes.Add(holeIndex); + result.Holes[holeNumber++] = (holeIndex); } } + + return result; + } + + // public static Data Flatten(MeshVertexData meshData, List subs) + // { + // var dataCount = subs.Count; + // var totalVertCount = 0; + // for (int i = 0; i < dataCount; i++) + // { + // totalVertCount += subs[i].y - subs[i].x; + // } + // + // var result = new Data + // { + // Vertices = new float[totalVertCount * 2], + // Holes = new int[dataCount - 1] + // }; + // var holeIndex = 0; + // + // var index = 0; + // var holeNumber = 0; + // for (var i = 0; i < dataCount; i++) + // { + // var subCount = subs[i].y - subs[i].x; + // for (var j = 0; j < subCount; j++) + // { + // result.Vertices[index++] = (meshData.Vertices[subs[i].x + j][0]); + // result.Vertices[index++] = (meshData.Vertices[subs[i].x + j][2]); + // } + // + // if (i > 0) + // { + // holeIndex = subs[i].x; + // result.Holes[holeNumber++] = (holeIndex); + // } + // } + // + // return result; + // } + + public static Data Flatten(Span meshData) + { + var result = new Data + { + Vertices = new float[meshData.Length * 2] + }; + + for (var i = 0; i < meshData.Length; i++) + { + result.Vertices[i*2 + 0] = meshData[i][0]; + result.Vertices[i*2 + 1] = meshData[i][2]; + } + return result; } } public class Data { - public List Vertices; - public List Holes; - public int Dim; - - public Data() - { - Holes = new List(); - Dim = 2; - } + public float[] Vertices; + public int[] Holes; } public class Node diff --git a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/ChamferHeightModifier.cs b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/ChamferHeightModifier.cs index 58094d2c0..ae0610771 100644 --- a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/ChamferHeightModifier.cs +++ b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/ChamferHeightModifier.cs @@ -182,21 +182,24 @@ public void QueryHeight(VectorFeatureUnity feature, MeshData md, out float maxHe { minHeight = 0.0f; maxHeight = 0.0f; - - try + + if (feature.Properties.ContainsKey("height")) { maxHeight = Convert.ToSingle(feature.Properties["height"]); } - catch (Exception) + else { - Debug.LogError("Property: '" + "height" + "' must contain a numerical value for extrusion."); - return; + maxHeight = 0; } if (feature.Properties.ContainsKey("min_height")) { minHeight = Convert.ToSingle(feature.Properties["min_height"]); } + else + { + minHeight = 0; + } } public void Chamfer(VectorFeatureUnity feature, MeshData md) diff --git a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/PolygonMeshModifier.cs b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/PolygonMeshModifier.cs index 6e65b8358..9d5553ef7 100644 --- a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/PolygonMeshModifier.cs +++ b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/PolygonMeshModifier.cs @@ -77,7 +77,7 @@ private void Polygonize(VectorFeatureUnity feature, MeshData md, float tileSize, if (IsClockwise(sub) && vertCount > 0) { flatData = EarcutLibrary.Flatten(subset); - result = EarcutLibrary.Earcut(flatData.Vertices, flatData.Holes, flatData.Dim); + result = EarcutLibrary.Earcut(flatData.Vertices, flatData.Holes, 2); polygonVertexCount = result.Count; if (triList == null) { @@ -127,7 +127,7 @@ private void Polygonize(VectorFeatureUnity feature, MeshData md, float tileSize, } flatData = EarcutLibrary.Flatten(subset); - result = EarcutLibrary.Earcut(flatData.Vertices, flatData.Holes, flatData.Dim); + result = EarcutLibrary.Earcut(flatData.Vertices, flatData.Holes, 2); polygonVertexCount = result.Count; // if (_options.texturingType == UvMapType.Atlas || _options.texturingType == UvMapType.AtlasWithColorPalette) diff --git a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/SnapTerrainModifier.cs b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/SnapTerrainModifier.cs index e4838773d..bf08231e3 100644 --- a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/SnapTerrainModifier.cs +++ b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/SnapTerrainModifier.cs @@ -25,7 +25,7 @@ public class SnapTerrainModifierCore { public void Run(VectorFeatureUnity feature, MeshData md, IMapInformation mapInformation) { - var rectd = Conversions.TileBoundsInUnitySpace(feature.TileId, mapInformation.CenterMercator, mapInformation.Scale); + var tileSize = Conversions.TileSizeInUnitySpace(feature.TileId.Z, mapInformation.Scale); var _counter = md.Vertices.Count; if (_counter > 0) { @@ -33,10 +33,10 @@ public void Run(VectorFeatureUnity feature, MeshData md, IMapInformation mapInfo { var h = mapInformation.QueryElevation( feature.TileId, - (float)(md.Vertices[i].x) + .5f, - (float)(md.Vertices[i].z) + .5f) + (md.Vertices[i].x) + .5f, + (md.Vertices[i].z) + .5f) / mapInformation.Scale - / rectd.Size.x; + / tileSize; md.Vertices[i] += new Vector3(0, (float) h, 0); } } @@ -50,11 +50,11 @@ public void Run(VectorFeatureUnity feature, MeshData md, IMapInformation mapInfo var h = 0f; if (mapInformation.QueryElevation != null) { - h = (float)(mapInformation.QueryElevation( + h = (mapInformation.QueryElevation( feature.TileId, - (float) (sub[i].x), - (float) (sub[i].z + 1)) / mapInformation.Scale - / rectd.Size.x); + (sub[i].x), + (sub[i].z + 1)) / mapInformation.Scale + / tileSize); } sub[i] += new Vector3(0, h, 0); diff --git a/Runtime/Mapbox/VectorModule/Unity/VectorLayerModuleScript.cs b/Runtime/Mapbox/VectorModule/Unity/VectorLayerModuleScript.cs index 5fdf44af8..9c4ceb031 100644 --- a/Runtime/Mapbox/VectorModule/Unity/VectorLayerModuleScript.cs +++ b/Runtime/Mapbox/VectorModule/Unity/VectorLayerModuleScript.cs @@ -9,9 +9,9 @@ namespace Mapbox.VectorModule.Unity { public class VectorLayerModuleScript : ModuleConstructorScript { - [SerializeField] private VectorModuleSettings vectorModuleSettings; + [SerializeField] protected VectorModuleSettings vectorModuleSettings; - [SerializeField] private List _layerVisualizers; + [SerializeField] private List _layerVisualizers; public override ILayerModule ModuleImplementation { get; protected set; } public void Start() @@ -21,19 +21,20 @@ public void Start() public override ILayerModule ConstructModule(MapService service, IMapInformation mapInformation, UnityContext unityContext) { - var dictionary = new Dictionary(); + var dictionary = new Dictionary>(); foreach (var visualizerObject in _layerVisualizers) { if(visualizerObject == null) continue; var visualizer = visualizerObject.ConstructLayerVisualizer(mapInformation, unityContext); - dictionary.Add(visualizer.VectorLayerName, visualizer); + if(!dictionary.ContainsKey(visualizer.VectorLayerName)) + dictionary.Add(visualizer.VectorLayerName, new List()); + dictionary[visualizer.VectorLayerName].Add(visualizer); } ModuleImplementation = GetVectorLayerModule(mapInformation, unityContext, service, dictionary); return ModuleImplementation; } - private VectorLayerModule GetVectorLayerModule(IMapInformation mapInformation, UnityContext unityContext, - MapService service, Dictionary dictionary) + protected virtual VectorLayerModule GetVectorLayerModule(IMapInformation mapInformation, UnityContext unityContext, MapService service, Dictionary> dictionary) { if (vectorModuleSettings.SourceType != VectorSourceType.Custom) { diff --git a/Runtime/Mapbox/VectorModule/VectorLayerModule.cs b/Runtime/Mapbox/VectorModule/VectorLayerModule.cs index 5e874e5a1..bed78ea27 100644 --- a/Runtime/Mapbox/VectorModule/VectorLayerModule.cs +++ b/Runtime/Mapbox/VectorModule/VectorLayerModule.cs @@ -10,6 +10,7 @@ using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; using Mapbox.VectorModule.MeshGeneration; +using Mapbox.VectorTile.ExtensionMethods; using UnityEngine; using Console = System.Console; @@ -18,10 +19,10 @@ namespace Mapbox.VectorModule public class VectorLayerModule : ILayerModule { //mesh gen - private bool _isActive = true; - private UnityContext _unityContext; - private Dictionary _activeTasks; - private Dictionary _layerVisualizers; + protected bool _isActive = true; + protected UnityContext _unityContext; + protected Dictionary> _activeTasks; + protected Dictionary> _layerVisualizers; private Source _vectorSource; private VectorModuleSettings _vectorModuleSettings; @@ -32,26 +33,37 @@ public class VectorLayerModule : ILayerModule private HashSet _readyTiles; private List _tilesToRemove; - public VectorLayerModule(IMapInformation mapInformation, Source source, UnityContext unityContext, Dictionary layerVisualizers, VectorModuleSettings vectorModuleSettings = null) : base() + public VectorLayerModule(IMapInformation mapInformation, Source source, UnityContext unityContext, Dictionary> layerVisualizers, VectorModuleSettings vectorModuleSettings = null) : base() { _unityContext = unityContext; _layerVisualizers = layerVisualizers; _mapInformation = mapInformation; + _vectorSource = source; + if (_vectorSource != null) + { + _vectorSource.CacheItemDisposed += ClearDisposedDataVisual; + } + _vectorModuleSettings = vectorModuleSettings ?? new VectorModuleSettings(); _readyTiles = new HashSet(); - _vectorSource.CacheItemDisposed += ClearDisposedDataVisual; _retainedTiles = new HashSet(); - _activeTasks = new Dictionary(); + _activeTasks = new Dictionary>(); _tilesToRemove = new List(10); } public virtual IEnumerator Initialize() { - yield return _vectorSource.Initialize(); - foreach (var visualizer in _layerVisualizers.Values) + if (_vectorSource != null) { - yield return visualizer.Initialize(); + yield return _vectorSource.Initialize(); + } + foreach (var visualizers in _layerVisualizers.Values) + { + foreach (var visualizer in visualizers) + { + yield return visualizer.Initialize(); + } } } @@ -64,23 +76,34 @@ public virtual bool LoadInstant(UnityMapTile unityTile) { var targetId = GetTargetTileId(unityTile.CanonicalTileId); if (_readyTiles.Contains(targetId)) - return true; - - //Debug.Log(string.Format("Load Instant {0}, {1}, {2}" ,unityTile.CanonicalTileId, _vectorSource.CheckInstantData(unityTile.CanonicalTileId), _visualCache.ContainsKey(unityTile.CanonicalTileId))); - if (!IsZinSupportedRange(targetId.Z)) return true; - - //this is wrong, it feels wrong - //tile doesn't need data, only yhe visual object. why are we checking for data - if (_vectorSource.GetInstantData(targetId, out var instantData) && - unityTile.TerrainContainer.State == TileContainerState.Final) { - if(!IsMeshGenInWork(targetId)) + foreach (var pair in _layerVisualizers) { - CreateVisual(targetId, instantData); + foreach (var visualizer in pair.Value) + { + visualizer.SetActive(targetId, true, _mapInformation); + } } + return true; } + else + { + //Debug.Log(string.Format("Load Instant {0}, {1}, {2}" ,unityTile.CanonicalTileId, _vectorSource.CheckInstantData(unityTile.CanonicalTileId), _visualCache.ContainsKey(unityTile.CanonicalTileId))); + if (!IsZinSupportedRange(targetId.Z)) return true; - return false; + //this is wrong, it feels wrong + //tile doesn't need data, only yhe visual object. why are we checking for data + if (_vectorSource.GetInstantData(targetId, out var instantData) && + unityTile.TerrainContainer.State == TileContainerState.Final) + { + if (!IsMeshGenInWork(targetId)) + { + CreateVisual(targetId, instantData); + } + } + + return false; + } } public virtual bool RetainTiles(HashSet retainedTiles) @@ -91,18 +114,24 @@ public virtual bool RetainTiles(HashSet retainedTiles) foreach (var tileId in _readyTiles) { var isActive = _retainedTiles.Contains(tileId); - foreach (var visualizer in _layerVisualizers) + foreach (var pair in _layerVisualizers) { - visualizer.Value.SetActive(tileId, isActive, _mapInformation); + foreach (var visualizer in pair.Value) + { + visualizer.SetActive(tileId, isActive, _mapInformation); + } } if (!isActive) { _tilesToRemove.Add(tileId); - if (_activeTasks.TryGetValue(tileId, out var task)) + if (_activeTasks.TryGetValue(tileId, out var tasks)) { - _activeTasks.Remove(tileId); - task.Cancel(); + foreach (var task in tasks) + { + _activeTasks.Remove(tileId); + task.Cancel(); + } } } } @@ -115,10 +144,15 @@ public virtual bool RetainTiles(HashSet retainedTiles) //cancel tasks for tiles we no longer need //this prevents flickers from buildings appearing in temp tiles //while we are waiting for the final real tile - foreach (var task in _activeTasks) + foreach (var taskPair in _activeTasks) { - if(!_retainedTiles.Contains(task.Key)) - task.Value.Cancel(); + if (!_retainedTiles.Contains(taskPair.Key)) + { + foreach (var task in taskPair.Value) + { + task.Cancel(); + } + } } var isReady = _vectorSource.RetainTiles(_retainedTiles); @@ -140,9 +174,12 @@ public virtual void UpdatePositioning(IMapInformation information) public virtual void OnDestroy() { _isActive = false; - foreach (var visualizer in _layerVisualizers) + foreach (var pair in _layerVisualizers) { - visualizer.Value.OnDestroy(); + foreach (var visualizer in pair.Value) + { + visualizer.OnDestroy(); + } } } @@ -163,7 +200,14 @@ public IEnumerable GetReadyTiles() public bool TryGetLayerVisualizer(string name, out IVectorLayerVisualizer visualizer) { - return _layerVisualizers.TryGetValue(name, out visualizer); + if (_layerVisualizers.TryGetValue(name, out var visualizers)) + { + visualizer = visualizers.FirstOrDefault(); + return true; + } + + visualizer = null; + return false; } public IEnumerable GetDataId(IEnumerable tileIdList) @@ -362,14 +406,20 @@ private IEnumerator CreateVisualCoroutine(CanonicalTileId tileId, VectorData vec private void ClearDisposedDataVisual(CanonicalTileId tileId) { - if (_activeTasks.TryGetValue(tileId, out var task)) + if (_activeTasks.TryGetValue(tileId, out var tasks)) { - task.Cancel(); + foreach (var task in tasks) + { + task.Cancel(); + } } _readyTiles.Remove(tileId); - foreach (var visualizer in _layerVisualizers) + foreach (var pair in _layerVisualizers) { - visualizer.Value.UnregisterTile(tileId); + foreach (var visualizer in pair.Value) + { + visualizer.UnregisterTile(tileId); + } } OnVectorMeshDestroyed(tileId); @@ -379,20 +429,24 @@ private void ClearDisposedDataVisual(CanonicalTileId tileId) private void UpdateForView(CanonicalTileId tileId, IMapInformation information) { - foreach (var visualizer in _layerVisualizers) + foreach (var pair in _layerVisualizers) { - visualizer.Value.UpdateForView(tileId, information); + foreach (var visualizer in pair.Value) + { + visualizer.UpdateForView(tileId, information); + } } } - private void MeshGeneration(VectorData data, Action callback) + protected virtual void MeshGeneration(VectorData data, Action callback) { if (data.Data == null) { callback(new MeshGenerationTaskResult(TaskResultType.Success)); + return; } - var meshTask = new MeshGenTaskWrapper() + var meshTask = new MeshGenTaskWrapper() { TileId = data.TileId, DataAction = () => @@ -415,14 +469,19 @@ private void MeshGeneration(VectorData data, Action ca var layers = data.VectorTileData.LayerNames(); foreach (var layerName in layers) { - if (_layerVisualizers.TryGetValue(layerName, out var layerVisualizer)) + if (_layerVisualizers.TryGetValue(layerName, out var layerVisualizers)) { - if(layerVisualizer.ContainsVisualFor(data.TileId)) - continue; - if (layerVisualizer.Active) - { - result.Data.Add(layerName, layerVisualizer.CreateMesh(data.TileId, data.VectorTileData.GetLayer(layerName))); - } + for (var vi = 0; vi < layerVisualizers.Count; vi++) + { + var layerVisualizer = layerVisualizers[vi]; + if (layerVisualizer.ContainsVisualFor(data.TileId)) + continue; + if (layerVisualizer.Active) + { + var key = layerVisualizers.Count > 1 ? $"{layerName}_{vi}" : layerName; + result.Data.Add(key, layerVisualizer.CreateMesh(data.TileId, data.VectorTileData.GetLayer(layerName))); + } + } } } } @@ -467,18 +526,21 @@ private void MeshGeneration(VectorData data, Action ca var resultGameObjects = new List(); foreach (var layerName in data.VectorTileData.LayerNames()) { - if (!taskResult.Data.ContainsKey(layerName)) - continue; - - if (_layerVisualizers.TryGetValue(layerName, out var layerVisualizer)) + if (_layerVisualizers.TryGetValue(layerName, out var layerVisualizers)) { - var tileMeshData = taskResult.Data[layerName]; - var layerGameObjects = layerVisualizer.CreateGo(data.TileId, tileMeshData); - foreach (var gameObject in layerGameObjects) - { - gameObject.SetActive(false); - resultGameObjects.Add(gameObject); - } + for (var vi = 0; vi < layerVisualizers.Count; vi++) + { + var key = layerVisualizers.Count > 1 ? $"{layerName}_{vi}" : layerName; + if (!taskResult.Data.TryGetValue(key, out var tileMeshData)) + continue; + var layerVisualizer = layerVisualizers[vi]; + var layerGameObjects = layerVisualizer.CreateGo(data.TileId, tileMeshData); + foreach (var gameObject in layerGameObjects) + { + gameObject.SetActive(false); + resultGameObjects.Add(gameObject); + } + } } } callback(new MeshGenerationTaskResult(TaskResultType.Success, resultGameObjects)); @@ -486,7 +548,8 @@ private void MeshGeneration(VectorData data, Action ca } }; - _activeTasks.Add(data.TileId, meshTask); + if(!_activeTasks.ContainsKey(data.TileId)) _activeTasks.Add(data.TileId, new List()); + _activeTasks[data.TileId].Add(meshTask); _unityContext.TaskManager.AddTask(meshTask, 0); } diff --git a/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs b/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs index 6b551cf20..40c97742c 100644 --- a/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs @@ -84,6 +84,10 @@ public virtual void SetActive(CanonicalTileId canonicalTileId, bool isActive, IM var isVisible = stack.IsZinSupportedRange(_mapInformation.AbsoluteZoom); entity.GameObject.SetActive(isVisible); } + else + { + Debug.Log("All entities should have a stack id"); + } } } } @@ -157,6 +161,25 @@ public virtual void UnregisterTile(CanonicalTileId tileId) modifierStack.UnregisterTile(tileId); } } + + public virtual void ClearCaches() + { + foreach (var entities in _results.Values) + { + foreach (var entity in entities) + { + OnVectorMeshDestroyed(entity.GameObject); + GameObject.Destroy(entity.GameObject); + } + } + + foreach (var stack in _stackList) + { + stack.Value.OnDestroy(); + } + + _results.Clear(); + } public void OnDestroy() { diff --git a/Samples~/LocationBasedGame/CharacterVisuals/Astronaut/CharacterMovement.cs b/Samples~/LocationBasedGame/CharacterVisuals/Astronaut/CharacterMovement.cs index c37b9244a..29fbcc666 100644 --- a/Samples~/LocationBasedGame/CharacterVisuals/Astronaut/CharacterMovement.cs +++ b/Samples~/LocationBasedGame/CharacterVisuals/Astronaut/CharacterMovement.cs @@ -11,6 +11,7 @@ namespace Mapbox.Examples public class CharacterMovement : MonoBehaviour { public MapBehaviourCore MapBehaviour; + private MapboxMap _map; private IMapInformation _mapInformation; public Transform Target; public Animator CharacterAnimator; @@ -24,6 +25,7 @@ private void Start() { MapBehaviour.Initialized += map => { + _map = map; _mapInformation = map.MapInformation; _scale = map.MapInformation.Scale; _readyForUpdates = true; @@ -42,6 +44,7 @@ void Update() transform.LookAt(transform.position + direction); transform.Translate(Vector3.forward * (Speed/_scale)); if(CharacterAnimator) CharacterAnimator.SetBool("IsWalking", true); + _map.ForceRedraw(); } else { diff --git a/Samples~/MapboxComponents/Areas.meta b/Samples~/MapboxComponents/Areas.meta new file mode 100644 index 000000000..9dbc7df2e --- /dev/null +++ b/Samples~/MapboxComponents/Areas.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6e0eececb267c4f68831fccfa80a1877 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Commercial.mat b/Samples~/MapboxComponents/Areas/Commercial.mat new file mode 100644 index 000000000..9b292f846 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Commercial.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Commercial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.96862745, g: 0.9490196, b: 0.8901961, a: 1} + - _Color: {r: 0.9686274, g: 0.9490196, b: 0.8901961, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Commercial.mat.meta b/Samples~/MapboxComponents/Areas/Commercial.mat.meta new file mode 100644 index 000000000..3272c0537 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Commercial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a64b34cd91bab49cf8f4d08438453d85 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Grass.mat b/Samples~/MapboxComponents/Areas/Grass.mat new file mode 100644 index 000000000..a9d6d24dd --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Grass.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Grass + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8352941, g: 0.94509804, b: 0.8156863, a: 1} + - _Color: {r: 0.8352941, g: 0.945098, b: 0.8156863, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Grass.mat.meta b/Samples~/MapboxComponents/Areas/Grass.mat.meta new file mode 100644 index 000000000..1b2d08274 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Grass.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a7105d373b04b4d2089924fdba9f19b0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Park.mat b/Samples~/MapboxComponents/Areas/Park.mat new file mode 100644 index 000000000..7b3f2b908 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Park.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Park + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.72156864, g: 0.92156863, b: 0.6784314, a: 1} + - _Color: {r: 0.72156864, g: 0.92156863, b: 0.6784314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Park.mat.meta b/Samples~/MapboxComponents/Areas/Park.mat.meta new file mode 100644 index 000000000..c1be888c9 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Park.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 96da09107c77b482fad0b37ae823518d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset b/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset new file mode 100644 index 000000000..1027d6524 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset @@ -0,0 +1,40 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6183aeb8971b45778ba9d2c9fc425433, type: 3} + m_Name: ParksComponentVisualizerObject + m_EditorClassIdentifier: + Settings: + LayerName: landuse + GameObjectOffset: 0.0001 + Styles: + - ClassName: park + Material: {fileID: 2100000, guid: 96da09107c77b482fad0b37ae823518d, type: 2} + PushUp: 0.0001 + - ClassName: wood + Material: {fileID: 2100000, guid: b628e4b850b674e93b4c8166f827487d, type: 2} + PushUp: 0.0003 + - ClassName: grass + Material: {fileID: 2100000, guid: a7105d373b04b4d2089924fdba9f19b0, type: 2} + PushUp: 0.0002 + - ClassName: commercial_area + Material: {fileID: 2100000, guid: a64b34cd91bab49cf8f4d08438453d85, type: 2} + PushUp: 0.00005 + - ClassName: school + Material: {fileID: 2100000, guid: 7f47ac3ad8a8b47faa51ba920beb36ca, type: 2} + PushUp: 0.00035 + - ClassName: agriculture + Material: {fileID: 2100000, guid: a7105d373b04b4d2089924fdba9f19b0, type: 2} + PushUp: 0.00035 + - ClassName: pitch + Material: {fileID: 2100000, guid: 7b9c655e55da744e49145717d2dd09de, type: 2} + PushUp: 0.0004 + IsActive: 1 diff --git a/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset.meta b/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset.meta new file mode 100644 index 000000000..9b329591d --- /dev/null +++ b/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2654a2dbd021345d990964046e3e9300 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Pitch.mat b/Samples~/MapboxComponents/Areas/Pitch.mat new file mode 100644 index 000000000..775bcda75 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Pitch.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Pitch + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8156863, g: 0.95686275, b: 0.74509805, a: 1} + - _Color: {r: 0.8156863, g: 0.95686275, b: 0.74509805, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Pitch.mat.meta b/Samples~/MapboxComponents/Areas/Pitch.mat.meta new file mode 100644 index 000000000..05bca75c1 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Pitch.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7b9c655e55da744e49145717d2dd09de +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/School.mat b/Samples~/MapboxComponents/Areas/School.mat new file mode 100644 index 000000000..64eeabbf5 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/School.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: School + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.9411765, g: 0.9019608, b: 0.81960785, a: 1} + - _Color: {r: 0.9411765, g: 0.9019608, b: 0.81960785, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/School.mat.meta b/Samples~/MapboxComponents/Areas/School.mat.meta new file mode 100644 index 000000000..d61505309 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/School.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7f47ac3ad8a8b47faa51ba920beb36ca +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Water.mat b/Samples~/MapboxComponents/Areas/Water.mat new file mode 100644 index 000000000..2d735a6e9 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Water.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Water + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.6, g: 0.8666667, b: 1, a: 1} + - _Color: {r: 0.6, g: 0.8666667, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Water.mat.meta b/Samples~/MapboxComponents/Areas/Water.mat.meta new file mode 100644 index 000000000..6892d3f9f --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Water.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6ebf8061831544cd7973cb57b1966259 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset b/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset new file mode 100644 index 000000000..9de60a5b3 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset @@ -0,0 +1,22 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6183aeb8971b45778ba9d2c9fc425433, type: 3} + m_Name: WaterComponentVisualizerObject.asset + m_EditorClassIdentifier: + Settings: + LayerName: water + GameObjectOffset: 0.0007 + Styles: + - ClassName: water + Material: {fileID: 2100000, guid: 6ebf8061831544cd7973cb57b1966259, type: 2} + PushUp: 0.0004 + IsActive: 1 diff --git a/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset.meta b/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset.meta new file mode 100644 index 000000000..f2346377a --- /dev/null +++ b/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 57618709630c44178bc0c0d905e015da +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Wood.mat b/Samples~/MapboxComponents/Areas/Wood.mat new file mode 100644 index 000000000..d92046e77 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Wood.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Wood + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.61960787, g: 0.88235295, b: 0.59607846, a: 1} + - _Color: {r: 0.61960775, g: 0.88235295, b: 0.5960784, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Wood.mat.meta b/Samples~/MapboxComponents/Areas/Wood.mat.meta new file mode 100644 index 000000000..32a8796d7 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Wood.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b628e4b850b674e93b4c8166f827487d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Buildings.meta b/Samples~/MapboxComponents/Buildings.meta new file mode 100644 index 000000000..c9fe36b81 --- /dev/null +++ b/Samples~/MapboxComponents/Buildings.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f0a1e617e1f224bf5ba9277daabff9f4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset b/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset new file mode 100644 index 000000000..7a986eb25 --- /dev/null +++ b/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c6eb41c8ba1c475ab45d048dafafaf77, type: 3} + m_Name: BuildingComponentVisualizerObject + m_EditorClassIdentifier: + Settings: + EnableTerrainSnapping: 0 + RoundBuildingCorners: 1 + Material: {fileID: 2100000, guid: 16d95f6897a404ef3b6d50f7286fbfcf, type: 2} + ChamferExtrusionSettings: + OffsetInMeters: 1 + ExtrusionOptions: + extrusionType: 1 + propertyName: height + minimumHeight: 0 + maximumHeight: 1000 + extrusionScaleFactor: 1 + BasicExtrusionSettings: + ExtrusionOptions: + extrusionType: 1 + propertyName: height + minimumHeight: 0 + maximumHeight: 1000 + extrusionScaleFactor: 1 + IsActive: 1 diff --git a/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset.meta b/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset.meta new file mode 100644 index 000000000..4db308395 --- /dev/null +++ b/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cc233d663d28c4daa88aed75870c52c5 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat b/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat new file mode 100644 index 000000000..0f061c4bf --- /dev/null +++ b/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat @@ -0,0 +1,155 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: BuildingMaterial + m_Shader: {fileID: -6465566751694194690, guid: 0873618690f77410691528b7ef785bcc, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 1, b: 1, a: 1} + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 0, g: 0, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat.meta b/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat.meta new file mode 100644 index 000000000..628750e2c --- /dev/null +++ b/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 16d95f6897a404ef3b6d50f7286fbfcf +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/GroundMaterial.mat b/Samples~/MapboxComponents/GroundMaterial.mat new file mode 100644 index 000000000..ad6eac0c5 --- /dev/null +++ b/Samples~/MapboxComponents/GroundMaterial.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-8980196564406603438 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: GroundMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8962264, g: 0.8962264, b: 0.8962264, a: 1} + - _Color: {r: 0.8962264, g: 0.8962264, b: 0.8962264, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] diff --git a/Samples~/MapboxComponents/GroundMaterial.mat.meta b/Samples~/MapboxComponents/GroundMaterial.mat.meta new file mode 100644 index 000000000..d61ef0112 --- /dev/null +++ b/Samples~/MapboxComponents/GroundMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aaf04558f9b104d2ebc4e1c86464fc1c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Map.unity b/Samples~/MapboxComponents/Map.unity new file mode 100644 index 000000000..29e63695e --- /dev/null +++ b/Samples~/MapboxComponents/Map.unity @@ -0,0 +1,828 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1001 &43046694 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 1000012270391302, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_Name + value: Attribution + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_Pivot.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_Pivot.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} +--- !u!1 &166616394 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 166616397} + - component: {fileID: 166616396} + - component: {fileID: 166616395} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &166616395 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 166616394} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &166616396 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 166616394} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &166616397 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 166616394} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &351425070 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 351425071} + - component: {fileID: 351425073} + - component: {fileID: 351425072} + m_Layer: 0 + m_Name: Sphere + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &351425071 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 351425070} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: -1, z: 0} + m_LocalScale: {x: 10, y: 10, z: 10} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 985205836} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!23 &351425072 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 351425070} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: ba676a16448114076b1bac2db9d5b88f, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &351425073 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 351425070} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &985205833 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 985205836} + - component: {fileID: 985205834} + - component: {fileID: 985205837} + m_Layer: 0 + m_Name: MapCamera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &985205834 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 985205833} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.01 + far clip plane: 80 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &985205836 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 985205833} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 1, y: 1.0000002, z: 1.0000002} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 351425071} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &985205837 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 985205833} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.100000024 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 +--- !u!1 &1316629397 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1316629400} + - component: {fileID: 1316629399} + - component: {fileID: 1316629407} + - component: {fileID: 1316629406} + m_Layer: 0 + m_Name: Map + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1316629399 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8987c930e6fb0479a907044faef4ab26, type: 3} + m_Name: + m_EditorClassIdentifier: + MapInformation: + _pitch: 45 + _bearing: 0 + _scale: 1000 + _latitudeLongitudeString: 52.502368,13.334595 + _zoom: 16 + UnityContext: + MapRoot: {fileID: 1316629400} + BaseTileRoot: {fileID: 1808235830} + RuntimeGenerationRoot: {fileID: 0} + _tileCreatorBehaviour: {fileID: 1316629407} + TileProvider: {fileID: 0} + DataFetcher: {fileID: 0} + CacheManager: {fileID: 0} + InitializeOnStart: 1 +--- !u!4 &1316629400 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1808235830} + - {fileID: 2054936222} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1316629406 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e13965577acb456182a04c56586cb36e, type: 3} + m_Name: + m_EditorClassIdentifier: + _layerVisualizers: + - {fileID: 11400000, guid: cc233d663d28c4daa88aed75870c52c5, type: 2} + - {fileID: 11400000, guid: 7042be54feb564150ad5bd0a6a36dfd8, type: 2} + - {fileID: 11400000, guid: 57618709630c44178bc0c0d905e015da, type: 2} + - {fileID: 11400000, guid: 2654a2dbd021345d990964046e3e9300, type: 2} + - {fileID: 11400000, guid: ee7d380540f3448368c3eb1e8935378c, type: 2} +--- !u!114 &1316629407 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2fa13573d971f40649081b02ffc8366c, type: 3} + m_Name: + m_EditorClassIdentifier: + TileMaterials: + - {fileID: 2100000, guid: aaf04558f9b104d2ebc4e1c86464fc1c, type: 2} + InstanceMaterials: 0 + CacheSize: 25 +--- !u!1 &1498144748 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054936222} + - component: {fileID: 2054936221} + - component: {fileID: 2054936220} + m_Layer: 0 + m_Name: Utility + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1808235829 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1808235830} + m_Layer: 0 + m_Name: BaseTiles + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1808235830 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1808235829} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1316629400} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1909047952 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1909047955} + - component: {fileID: 1909047954} + - component: {fileID: 1909047953} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1909047953 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1909047952} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Version: 3 + m_UsePipelineSettings: 1 + m_AdditionalLightsShadowResolutionTier: 2 + m_LightLayerMask: 1 + m_RenderingLayers: 1 + m_CustomShadowLayers: 0 + m_ShadowLayerMask: 1 + m_ShadowRenderingLayers: 1 + m_LightCookieSize: {x: 1, y: 1} + m_LightCookieOffset: {x: 0, y: 0} + m_SoftShadowQuality: 0 +--- !u!108 &1909047954 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1909047952} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 0.41 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 0.402 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &1909047955 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1909047952} + serializedVersion: 2 + m_LocalRotation: {x: 0.1819419, y: 0.81802, z: -0.38144895, w: 0.39017567} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: 129, z: 0} +--- !u!114 &2054936220 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1498144748} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: caaa5942c50bf46e99b3904ca295889c, type: 3} + m_Name: + m_EditorClassIdentifier: + MapBehaviour: {fileID: 1316629399} + Camera: {fileID: 985205834} + ShiftRange: {x: 2000, y: 2000} +--- !u!114 &2054936221 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1498144748} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0439abe6d989b44a38eb2f8b633769fc, type: 3} + m_Name: + m_EditorClassIdentifier: + MapBehaviour: {fileID: 1316629399} + Camera: {fileID: 985205834} + Core: + Pitch: 30 + Bearing: 0 + Speed: -1 + CameraDistance: 20 + _targetPosition: {x: 0, y: 0, z: 0} + RotationSpeed: 50 + CameraCurve: {fileID: 11400000, guid: e887d93d4b690437fa30a6cc54adc7e8, type: 2} + CamDistanceMultiplier: 2 + ZoomSpeed: 2 +--- !u!4 &2054936222 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1498144748} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1316629400} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 985205836} + - {fileID: 1909047955} + - {fileID: 1316629400} + - {fileID: 166616397} + - {fileID: 43046694} diff --git a/Samples~/MapboxComponents/Map.unity.meta b/Samples~/MapboxComponents/Map.unity.meta new file mode 100644 index 000000000..9a6474342 --- /dev/null +++ b/Samples~/MapboxComponents/Map.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e80a52bde674545049e5b12142766d4f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/MapFog.mat b/Samples~/MapboxComponents/MapFog.mat new file mode 100644 index 000000000..d666caacf --- /dev/null +++ b/Samples~/MapboxComponents/MapFog.mat @@ -0,0 +1,137 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: MapFog + m_Shader: {fileID: -6465566751694194690, guid: 620ac0ed059f04d8eb20466a704ba8b4, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 1 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BlendOp: 0 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 0 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _FadeEnd: 0.15 + - _FadeStart: 0 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _SampleGI: 0 + - _Smoothness: 0.5 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8284087, g: 0.95864266, b: 0.9811321, a: 1} + - _Color: {r: 0.8284087, g: 0.95864266, b: 0.9811321, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &1696498769756923639 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/MapFog.mat.meta b/Samples~/MapboxComponents/MapFog.mat.meta new file mode 100644 index 000000000..ed8978e04 --- /dev/null +++ b/Samples~/MapboxComponents/MapFog.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ba676a16448114076b1bac2db9d5b88f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/MapboxFogShader.shadergraph b/Samples~/MapboxComponents/MapboxFogShader.shadergraph new file mode 100644 index 000000000..8a4197fca --- /dev/null +++ b/Samples~/MapboxComponents/MapboxFogShader.shadergraph @@ -0,0 +1,1110 @@ +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.GraphData", + "m_ObjectId": "aef4b9449d054df0bc58725679fb6101", + "m_Properties": [ + { + "m_Id": "e0b5dca034da4bde85c7b2ba1a1aad00" + }, + { + "m_Id": "e63ca906c8404258be11256ae2decf8c" + }, + { + "m_Id": "954b624585114582870bcc456e0b63ea" + } + ], + "m_Keywords": [], + "m_Dropdowns": [], + "m_CategoryData": [ + { + "m_Id": "132d1cc1314248f7928d2ee4c9c18b01" + } + ], + "m_Nodes": [ + { + "m_Id": "015aac73ba444a9a8e0d04aba79dae24" + }, + { + "m_Id": "16f5eef598cc4868ae55ee65ee1efe3d" + }, + { + "m_Id": "b60824ef93564f75b81efa94c1064b91" + }, + { + "m_Id": "e049db7645a8487d9a0013de4646c35f" + }, + { + "m_Id": "5bbbcb5587694ac38ed9e3804653e714" + }, + { + "m_Id": "4eeca085fcd64bda80568f8b7b04d71f" + }, + { + "m_Id": "72740d54c0b64fa7839a70287c81dcf7" + }, + { + "m_Id": "00d7e0c25d50412ba684c76491e01490" + }, + { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + { + "m_Id": "cb92c5825302468c9264328de973cbb1" + }, + { + "m_Id": "d0366037fe7543bf8402357f34ba43dd" + } + ], + "m_GroupDatas": [], + "m_StickyNoteDatas": [], + "m_Edges": [ + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "00d7e0c25d50412ba684c76491e01490" + }, + "m_SlotId": 2 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + "m_SlotId": 2 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + "m_SlotId": 3 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "4eeca085fcd64bda80568f8b7b04d71f" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "5bbbcb5587694ac38ed9e3804653e714" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "e049db7645a8487d9a0013de4646c35f" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "72740d54c0b64fa7839a70287c81dcf7" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "00d7e0c25d50412ba684c76491e01490" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "cb92c5825302468c9264328de973cbb1" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "d0366037fe7543bf8402357f34ba43dd" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + "m_SlotId": 1 + } + } + ], + "m_VertexContext": { + "m_Position": { + "x": 0.0, + "y": 0.0 + }, + "m_Blocks": [ + { + "m_Id": "015aac73ba444a9a8e0d04aba79dae24" + }, + { + "m_Id": "16f5eef598cc4868ae55ee65ee1efe3d" + }, + { + "m_Id": "b60824ef93564f75b81efa94c1064b91" + } + ] + }, + "m_FragmentContext": { + "m_Position": { + "x": 0.0, + "y": 200.0 + }, + "m_Blocks": [ + { + "m_Id": "e049db7645a8487d9a0013de4646c35f" + }, + { + "m_Id": "4eeca085fcd64bda80568f8b7b04d71f" + } + ] + }, + "m_PreviewData": { + "serializedMesh": { + "m_SerializedMesh": "{\"mesh\":{\"instanceID\":0}}", + "m_Guid": "" + }, + "preventRotation": false + }, + "m_Path": "Shader Graphs", + "m_GraphPrecision": 1, + "m_PreviewMode": 2, + "m_OutputNode": { + "m_Id": "" + }, + "m_SubDatas": [], + "m_ActiveTargets": [ + { + "m_Id": "593f6e3591614444b6eddeea2dd1cec6" + } + ] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.SplitNode", + "m_ObjectId": "00d7e0c25d50412ba684c76491e01490", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Split", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -623.5, + "y": 607.5, + "width": 119.0, + "height": 149.0 + } + }, + "m_Slots": [ + { + "m_Id": "157d215cbc5d4d138041822b690f658e" + }, + { + "m_Id": "39bd85397a834ca49db934596affbf3c" + }, + { + "m_Id": "7200592e6faa4d7897dc8227315ea91e" + }, + { + "m_Id": "b2f597e0fcfc45d2a3787fe60c57bd84" + }, + { + "m_Id": "3742cbb081964ed5ad625f275f7d3a41" + } + ], + "synonyms": [ + "separate" + ], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "015aac73ba444a9a8e0d04aba79dae24", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "db9ae5b54d97411f9aa4679c530da996" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Position" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.CategoryData", + "m_ObjectId": "132d1cc1314248f7928d2ee4c9c18b01", + "m_Name": "", + "m_ChildObjectList": [ + { + "m_Id": "e0b5dca034da4bde85c7b2ba1a1aad00" + }, + { + "m_Id": "e63ca906c8404258be11256ae2decf8c" + }, + { + "m_Id": "954b624585114582870bcc456e0b63ea" + } + ] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "157d215cbc5d4d138041822b690f658e", + "m_Id": 0, + "m_DisplayName": "In", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "In", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "16f5eef598cc4868ae55ee65ee1efe3d", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Normal", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "8d5a15b1265748539eeea023d710da2e" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Normal" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "1813a141aec6425fa4344b999e7b0354", + "m_Id": 1, + "m_DisplayName": "Edge2", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Edge2", + "m_StageCapability": 3, + "m_Value": { + "x": 1.0, + "y": 1.0, + "z": 1.0, + "w": 1.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "2b28eb0e8c7343ffb17abd3399780393", + "m_Id": 0, + "m_DisplayName": "Color", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.SmoothstepNode", + "m_ObjectId": "2b81c9123e6e4cd2b27f3428acc7e027", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Smoothstep", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -433.5, + "y": 489.5, + "width": 208.0, + "height": 326.0 + } + }, + "m_Slots": [ + { + "m_Id": "cb20a3e55c764573a5bf552fab79223d" + }, + { + "m_Id": "1813a141aec6425fa4344b999e7b0354" + }, + { + "m_Id": "5c72aec9be3a4444aa83786a24780d6b" + }, + { + "m_Id": "3367a07b6f8343d188be52099085bba0" + } + ], + "synonyms": [ + "curve" + ], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "3367a07b6f8343d188be52099085bba0", + "m_Id": 3, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "3742cbb081964ed5ad625f275f7d3a41", + "m_Id": 4, + "m_DisplayName": "A", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "A", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "39bd85397a834ca49db934596affbf3c", + "m_Id": 1, + "m_DisplayName": "R", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "R", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "4c74df6515614c61b327655a82ea8b3c", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "4eeca085fcd64bda80568f8b7b04d71f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Alpha", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "8897a82736784f30b1495e2c700f77d4" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Alpha" +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalTarget", + "m_ObjectId": "593f6e3591614444b6eddeea2dd1cec6", + "m_Datas": [], + "m_ActiveSubTarget": { + "m_Id": "8f69abcf5d13474a8dcfa5bd4ac0bd1d" + }, + "m_AllowMaterialOverride": false, + "m_SurfaceType": 1, + "m_ZTestMode": 4, + "m_ZWriteControl": 0, + "m_AlphaMode": 0, + "m_RenderFace": 1, + "m_AlphaClip": false, + "m_CastShadows": true, + "m_ReceiveShadows": true, + "m_SupportsLODCrossFade": false, + "m_CustomEditorGUI": "", + "m_SupportVFX": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "5bbbcb5587694ac38ed9e3804653e714", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -243.5, + "y": 238.0, + "width": 105.0, + "height": 34.0 + } + }, + "m_Slots": [ + { + "m_Id": "2b28eb0e8c7343ffb17abd3399780393" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e0b5dca034da4bde85c7b2ba1a1aad00" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "5c72aec9be3a4444aa83786a24780d6b", + "m_Id": 2, + "m_DisplayName": "In", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "In", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "7200592e6faa4d7897dc8227315ea91e", + "m_Id": 2, + "m_DisplayName": "G", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "G", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.PositionNode", + "m_ObjectId": "72740d54c0b64fa7839a70287c81dcf7", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -844.5, + "y": 607.5, + "width": 206.0, + "height": 130.5 + } + }, + "m_Slots": [ + { + "m_Id": "4c74df6515614c61b327655a82ea8b3c" + } + ], + "synonyms": [ + "location" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 2, + "m_PositionSource": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "8458ec2b2d694369852b5de391563180", + "m_Id": 0, + "m_DisplayName": "FadeStart", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "8897a82736784f30b1495e2c700f77d4", + "m_Id": 0, + "m_DisplayName": "Alpha", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Alpha", + "m_StageCapability": 2, + "m_Value": 1.0, + "m_DefaultValue": 1.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.NormalMaterialSlot", + "m_ObjectId": "8d5a15b1265748539eeea023d710da2e", + "m_Id": 0, + "m_DisplayName": "Normal", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Normal", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 2, + "m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalUnlitSubTarget", + "m_ObjectId": "8f69abcf5d13474a8dcfa5bd4ac0bd1d" +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "954b624585114582870bcc456e0b63ea", + "m_Guid": { + "m_GuidSerialized": "3163ff8a-89c9-4619-a66c-23f24036acf1" + }, + "m_Name": "FadeEnd", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "FadeEnd", + "m_DefaultReferenceName": "_FadeEnd", + "m_OverrideReferenceName": "", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1.0, + "m_FloatType": 0, + "m_RangeValues": { + "x": 0.0, + "y": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "b2f597e0fcfc45d2a3787fe60c57bd84", + "m_Id": 3, + "m_DisplayName": "B", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "B", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "b60824ef93564f75b81efa94c1064b91", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Tangent", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "ef1df20f571f41b9a26921f5c18ae027" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Tangent" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.ColorRGBMaterialSlot", + "m_ObjectId": "c499fb61a7964c82a88c20e02cb56253", + "m_Id": 0, + "m_DisplayName": "Base Color", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "BaseColor", + "m_StageCapability": 2, + "m_Value": { + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_ColorMode": 0, + "m_DefaultColor": { + "r": 0.5, + "g": 0.5, + "b": 0.5, + "a": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "cb20a3e55c764573a5bf552fab79223d", + "m_Id": 0, + "m_DisplayName": "Edge1", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Edge1", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "cb92c5825302468c9264328de973cbb1", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -611.0, + "y": 518.5, + "width": 126.5, + "height": 34.0 + } + }, + "m_Slots": [ + { + "m_Id": "8458ec2b2d694369852b5de391563180" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e63ca906c8404258be11256ae2decf8c" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "d0366037fe7543bf8402357f34ba43dd", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -611.0, + "y": 552.5, + "width": 121.0, + "height": 34.0 + } + }, + "m_Slots": [ + { + "m_Id": "e714d4bb90754bb0a674c4fd86009080" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "954b624585114582870bcc456e0b63ea" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PositionMaterialSlot", + "m_ObjectId": "db9ae5b54d97411f9aa4679c530da996", + "m_Id": 0, + "m_DisplayName": "Position", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Position", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "e049db7645a8487d9a0013de4646c35f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.BaseColor", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "c499fb61a7964c82a88c20e02cb56253" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.BaseColor" +} + +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.Internal.ColorShaderProperty", + "m_ObjectId": "e0b5dca034da4bde85c7b2ba1a1aad00", + "m_Guid": { + "m_GuidSerialized": "9eeb467d-44c4-4475-97ec-1ec908914044" + }, + "m_Name": "Color", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Color", + "m_DefaultReferenceName": "_Color", + "m_OverrideReferenceName": "", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 0.0 + }, + "isMainColor": false, + "m_ColorMode": 0 +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "e63ca906c8404258be11256ae2decf8c", + "m_Guid": { + "m_GuidSerialized": "73e4ec77-798f-45af-bd80-f33ad0fee628" + }, + "m_Name": "FadeStart", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "FadeStart", + "m_DefaultReferenceName": "_FadeStart", + "m_OverrideReferenceName": "", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.0, + "m_FloatType": 0, + "m_RangeValues": { + "x": 0.0, + "y": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "e714d4bb90754bb0a674c4fd86009080", + "m_Id": 0, + "m_DisplayName": "FadeEnd", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.TangentMaterialSlot", + "m_ObjectId": "ef1df20f571f41b9a26921f5c18ae027", + "m_Id": 0, + "m_DisplayName": "Tangent", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Tangent", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + diff --git a/Samples~/MapboxComponents/MapboxFogShader.shadergraph.meta b/Samples~/MapboxComponents/MapboxFogShader.shadergraph.meta new file mode 100644 index 000000000..482fd1ae5 --- /dev/null +++ b/Samples~/MapboxComponents/MapboxFogShader.shadergraph.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 620ac0ed059f04d8eb20466a704ba8b4 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3} diff --git a/Samples~/MapboxComponents/Road.meta b/Samples~/MapboxComponents/Road.meta new file mode 100644 index 000000000..d155dfec2 --- /dev/null +++ b/Samples~/MapboxComponents/Road.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: afe816260877949a3a3b19dd364024f6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/PathRoadMaterial.mat b/Samples~/MapboxComponents/Road/PathRoadMaterial.mat new file mode 100644 index 000000000..7e5d20c71 --- /dev/null +++ b/Samples~/MapboxComponents/Road/PathRoadMaterial.mat @@ -0,0 +1,160 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: PathRoadMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 2000 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 2800000, guid: c769830c8128948f0b5f101c50fe6043, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DecalMeshBiasType: 0 + - _DecalMeshDepthBias: 0 + - _DecalMeshViewBias: 0 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DrawOrder: 0 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8396226, g: 0.8198202, b: 0.8198202, a: 1} + - _Color: {r: 0.8396226, g: 0.8198202, b: 0.8198202, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 1, g: 116.5, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Road/PathRoadMaterial.mat.meta b/Samples~/MapboxComponents/Road/PathRoadMaterial.mat.meta new file mode 100644 index 000000000..4b906db36 --- /dev/null +++ b/Samples~/MapboxComponents/Road/PathRoadMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c3c69f315ce7c4100a0ff6f40305bfe9 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset new file mode 100644 index 000000000..af21efc58 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e926a31006c7a440ea45ffe7b2bcfff6, type: 3} + m_Name: RoadLayerVisualizerObject + m_EditorClassIdentifier: + Settings: + GameObjectOffset: 0.001 + RandomOffsetRange: 0.0002 + RoadStyleSheet: {fileID: 11400000, guid: 59e08c3bf126a4a85ad131ef30cd6176, type: 2} + IsActive: 1 diff --git a/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset.meta b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset.meta new file mode 100644 index 000000000..446270361 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7042be54feb564150ad5bd0a6a36dfd8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset new file mode 100644 index 000000000..3688fac61 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e926a31006c7a440ea45ffe7b2bcfff6, type: 3} + m_Name: RoadLayerVisualizerObjectOutline + m_EditorClassIdentifier: + Settings: + GameObjectOffset: 0.0008 + RandomOffsetRange: 0.0002 + RoadStyleSheet: {fileID: 11400000, guid: e733a7876e9c54e23ad4d5a984206d6c, type: 2} + IsActive: 1 diff --git a/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset.meta b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset.meta new file mode 100644 index 000000000..051028f49 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee7d380540f3448368c3eb1e8935378c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadMaterial.mat b/Samples~/MapboxComponents/Road/RoadMaterial.mat new file mode 100644 index 000000000..4c8a4620a --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadMaterial.mat @@ -0,0 +1,164 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 10 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RoadMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 2000 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 2800000, guid: 1db0b07d278894ec1b85003b5886e4a8, type: 3} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 2800000, guid: c769830c8128948f0b5f101c50fe6043, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DecalMeshBiasType: 0 + - _DecalMeshDepthBias: 0 + - _DecalMeshViewBias: 0 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DrawOrder: 0 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _XRMotionVectorsPass: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.509434, g: 0.509434, b: 0.509434, a: 1} + - _Color: {r: 0.509434, g: 0.509434, b: 0.509434, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 1, g: 116.5, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] + m_AllowLocking: 1 +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Road/RoadMaterial.mat.meta b/Samples~/MapboxComponents/Road/RoadMaterial.mat.meta new file mode 100644 index 000000000..a547942ab --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8917c0d338e58481388fbe080cf36caf +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat b/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat new file mode 100644 index 000000000..817772aa2 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat @@ -0,0 +1,156 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RoadOutlineMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 0.64140344, b: 0.3537736, a: 1} + - _Color: {r: 1, g: 0.64140344, b: 0.35377356, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 0, g: 0, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat.meta b/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat.meta new file mode 100644 index 000000000..9d3bc13bd --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3a006dbd53abf4e029c02c32b8753395 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadStyleSheet.asset b/Samples~/MapboxComponents/Road/RoadStyleSheet.asset new file mode 100644 index 000000000..5981215fe --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadStyleSheet.asset @@ -0,0 +1,158 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-5417554272265697845 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_1_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &-1538625035922140899 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &-245792505840917342 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bf8c281932374fb0b5c80fa7fbca2b5b, type: 3} + m_Name: RoadStyleSheet + m_EditorClassIdentifier: + Styles: + - Name: secondary,tertiary + Filters: + Filters: + - rid: 5722777937161748527 + Type: 0 + Width: 4.8 + PushUp: 0.0008 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: primary + Filters: + Filters: + - rid: 5722777937161748529 + Type: 0 + Width: 5.2 + PushUp: 0.001 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: street,street_limited,motorway_link + Filters: + Filters: + - rid: 5722777937161748530 + Type: 0 + Width: 3.6 + PushUp: 0.0004 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: service + Filters: + Filters: + - rid: 5722777937161748531 + Type: 0 + Width: 1.8 + PushUp: 0.0002 + Material: {fileID: 2100000, guid: d449f6eb93a644716baa6f1f4c730813, type: 2} + - Name: path + Filters: + Filters: + - rid: 5722777987101753359 + - rid: 5722777987101753361 + - rid: 5722777987101753362 + Type: 0 + Width: 1 + PushUp: -0.0005 + Material: {fileID: 2100000, guid: c3c69f315ce7c4100a0ff6f40305bfe9, type: 2} + references: + version: 2 + RefIds: + - rid: 5722777937161748527 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: secondary,tertiary + Invert: 0 + - rid: 5722777937161748529 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: primary + Invert: 0 + - rid: 5722777937161748530 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: street,street_limited,motorway_link + Invert: 0 + - rid: 5722777937161748531 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: service + Invert: 0 + - rid: 5722777987101753359 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: path + Invert: 0 + - rid: 5722777987101753361 + type: {class: RoadStructureFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: tunnel + Invert: 1 + - rid: 5722777987101753362 + type: {class: RoadTypeFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: steps + Invert: 1 +--- !u!114 &6606382917155413568 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 diff --git a/Samples~/MapboxComponents/Road/RoadStyleSheet.asset.meta b/Samples~/MapboxComponents/Road/RoadStyleSheet.asset.meta new file mode 100644 index 000000000..33cb5868a --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadStyleSheet.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 59e08c3bf126a4a85ad131ef30cd6176 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset b/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset new file mode 100644 index 000000000..6f8d698be --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset @@ -0,0 +1,116 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-5417554272265697845 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_1_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &-1538625035922140899 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &-245792505840917342 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bf8c281932374fb0b5c80fa7fbca2b5b, type: 3} + m_Name: RoadStyleSheetOutline + m_EditorClassIdentifier: + Styles: + - Name: secondary,tertiary + Filters: + Filters: + - rid: 5722777937161748527 + Type: 0 + Width: 7.8 + PushUp: 0.0001 + Material: {fileID: 2100000, guid: 3a006dbd53abf4e029c02c32b8753395, type: 2} + - Name: primary + Filters: + Filters: + - rid: 5722777937161748529 + Type: 0 + Width: 8.2 + PushUp: 0.0002 + Material: {fileID: 2100000, guid: 3a006dbd53abf4e029c02c32b8753395, type: 2} + - Name: street,street_limited + Filters: + Filters: + - rid: 5722777937161748530 + Type: 0 + Width: 6.6 + PushUp: 0.0001 + Material: {fileID: 2100000, guid: 3a006dbd53abf4e029c02c32b8753395, type: 2} + references: + version: 2 + RefIds: + - rid: 5722777937161748527 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: secondary,tertiary + Invert: 0 + - rid: 5722777937161748529 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: primary + Invert: 0 + - rid: 5722777937161748530 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: street,street_limited + Invert: 0 +--- !u!114 &6606382917155413568 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 diff --git a/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset.meta b/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset.meta new file mode 100644 index 000000000..3db576ac3 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e733a7876e9c54e23ad4d5a984206d6c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat b/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat new file mode 100644 index 000000000..82546b10d --- /dev/null +++ b/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat @@ -0,0 +1,160 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ServiceRoadMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 2000 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 2800000, guid: c769830c8128948f0b5f101c50fe6043, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DecalMeshBiasType: 0 + - _DecalMeshDepthBias: 0 + - _DecalMeshViewBias: 0 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DrawOrder: 0 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.52156866, g: 0.5254902, b: 0.52156866, a: 1} + - _Color: {r: 0.5215686, g: 0.5254902, b: 0.5215686, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 1, g: 116.5, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat.meta b/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat.meta new file mode 100644 index 000000000..b02087840 --- /dev/null +++ b/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d449f6eb93a644716baa6f1f4c730813 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/road.preset b/Samples~/MapboxComponents/Road/road.preset new file mode 100644 index 000000000..927610640 --- /dev/null +++ b/Samples~/MapboxComponents/Road/road.preset @@ -0,0 +1,291 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: road + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: bf8c281932374fb0b5c80fa7fbca2b5b, type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.size + value: 5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Name + value: secondary,tertiary + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Filters.Filters.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Filters.Filters.Array.data[0] + value: 5722777937161748527 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Width + value: 4.8 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].PushUp + value: 0.0008 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Material + value: + objectReference: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Name + value: primary + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Filters.Filters.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Filters.Filters.Array.data[0] + value: 5722777937161748529 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Width + value: 5.2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].PushUp + value: 0.001 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Material + value: + objectReference: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Name + value: street,street_limited + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Filters.Filters.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Filters.Filters.Array.data[0] + value: 5722777937161748530 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Width + value: 3.6 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].PushUp + value: 0.0004 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Material + value: + objectReference: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Name + value: service + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Filters.Filters.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Filters.Filters.Array.data[0] + value: 5722777937161748531 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Width + value: 1.8 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].PushUp + value: 0.0002 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Material + value: + objectReference: {fileID: 2100000, guid: d449f6eb93a644716baa6f1f4c730813, type: 2} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Name + value: path + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Filters.Array.size + value: 3 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Filters.Array.data[0] + value: 5722777987101753359 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Filters.Array.data[1] + value: 5722777987101753361 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Filters.Array.data[2] + value: 5722777987101753362 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Width + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].PushUp + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Material + value: + objectReference: {fileID: 2100000, guid: c3c69f315ce7c4100a0ff6f40305bfe9, type: 2} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748527] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748527].CheckOperation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748527].FilterString + value: secondary,tertiary + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748527].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748529] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748529].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748529].FilterString + value: primary + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748529].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748530] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748530].CheckOperation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748530].FilterString + value: street,street_limited + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748530].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748531] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748531].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748531].FilterString + value: service + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748531].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753359] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753359].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753359].FilterString + value: path + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753359].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753361] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadStructureFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753361].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753361].FilterString + value: tunnel + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753361].Invert + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753362] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadTypeFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753362].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753362].FilterString + value: steps + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753362].Invert + value: 1 + objectReference: {fileID: 0} + m_ExcludedProperties: [] diff --git a/Samples~/MapboxComponents/Road/road.preset.meta b/Samples~/MapboxComponents/Road/road.preset.meta new file mode 100644 index 000000000..6d9f71b20 --- /dev/null +++ b/Samples~/MapboxComponents/Road/road.preset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 38743b7a21f944113b43bee79ecf030a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs b/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs new file mode 100644 index 000000000..07454d942 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs @@ -0,0 +1,217 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Data.Platform; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.MeshGeneration; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using Mapbox.VectorModule.Unity; +using NUnit.Framework; +using Unity.PerformanceTesting; +using UnityEditor; +using UnityEngine.TestTools; + +namespace Mapbox.VectorModuleTests +{ + public class BuildingLayerPerformanceTests + { + private int SampleCount = 100; + + private ResilientWebRequestFileSource _fs; + private byte[] buffer; + private CanonicalTileId tileId = new CanonicalTileId(15, 18654, 9481); + private string LatLng = "60.1664427,24.9318587"; + private string _buildingVisualizerAssetPath = "Packages/com.mapbox.sdk/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset"; + private string _oldBuildVisAssetPath = "Packages/com.mapbox.sdk/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset"; + + [UnitySetUp] + public IEnumerator SetUp() + { + var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); + _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); + + Response response = null; + var req = _fs.Request("https://api.mapbox.com/v4/mapbox.mapbox-streets-v7/" + tileId + ".vector.pbf", (r) => + { + response = r; + }); + while (!req.IsCompleted) + { + yield return null; + } + + buffer = response.Data; + Assert.NotZero(buffer.Length); + } + + private IMapInformation GetMapInformation() + { + var mapInformation = new MapInformation(LatLng); + mapInformation.SetInformation(null, 16, 0, 0, 1000); + mapInformation.Initialize(); + return mapInformation; + } + + private BuildingComponentVisualizer BuildingLayer(IMapInformation mapInformation) + { + var viz = new BuildingComponentVisualizer("test", mapInformation, null, null); + viz.Initialize(); + return viz; + } + + private VectorLayerVisualizer RegularLayer(IMapInformation mapInformation, bool doChamfer) + { + var viz = new VectorLayerVisualizer("test", mapInformation, null, null); + var modStackSettings = new ModifierStackSettings() { MergeObjects = true }; + var modStack = new ModifierStack(modStackSettings, null); + modStack.MeshModifiers.Add(new SnapTerrainModifier()); + modStack.MeshModifiers.Add(new PolygonMeshModifier(0)); + if (doChamfer) + { + modStack.MeshModifiers.Add(new ChamferHeightModifier(new ChamferModifierSettings() + { FlatTops = true, OffsetInMeters = 1 })); + } + else + { + modStack.MeshModifiers.Add(new HeightModifier(new GeometryExtrusionOptions())); + } + + viz.AddModifierStack(new List() { modStack }); + viz.Initialize(); + return viz; + } + + private VectorLayerVisualizer RegularRoadLayer(IMapInformation mapInformation) + { + var viz = new VectorLayerVisualizer("test", mapInformation, null, null); + var modStackSettings = new ModifierStackSettings() { MergeObjects = true }; + var modStack = new ModifierStack(modStackSettings, null); + modStack.MeshModifiers.Add(new LineMeshForPolygonsModifier(new LineMeshParameters() + { + CapType = JoinType.Butt, + JoinType = JoinType.Round, + Width = 6 + } )); + + viz.AddModifierStack(new List() { modStack }); + viz.Initialize(); + return viz; + } + + [Test, Performance, Order(0)] + public void Decompress() + { + Measure.Method(() => + { + var decompressed = Compression.Decompress(buffer); + var tile = new Mapbox.VectorTile.VectorTile(decompressed); + }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + private static bool[] _doChamfer = new bool[] { true, false}; + + + [UnityTest, Performance, Order(1)] + public IEnumerator BuildingVisualizer([ValueSource("_doChamfer")] bool doChamfer) + { + var settings = doChamfer + ? new BuildingComponentSettings() { RoundBuildingCorners = true } + : new BuildingComponentSettings() { RoundBuildingCorners = false }; + var vizObj = (BuildingComponentVisualizerObject) AssetDatabase.LoadAssetAtPath(_buildingVisualizerAssetPath, typeof(BuildingComponentVisualizerObject)); + vizObj.Settings = settings; + var viz = (BuildingComponentVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + yield return viz.Initialize(); + + Measure.Method(() => { RunBuildingComponent(viz); }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + [UnityTest, Performance, Order(3)] + public IEnumerator SingleBuildingVisualizer([ValueSource("_doChamfer")] bool doChamfer) + { + var settings = doChamfer + ? new BuildingComponentSettings() { RoundBuildingCorners = true } + : new BuildingComponentSettings() { RoundBuildingCorners = false }; + var vizObj = (BuildingComponentVisualizerObject) AssetDatabase.LoadAssetAtPath(_buildingVisualizerAssetPath, typeof(BuildingComponentVisualizerObject)); + vizObj.Settings = settings; + var viz = (BuildingComponentVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + yield return viz.Initialize(); + + Measure.Method(() => { RunBuildingComponent(viz); }) + .WarmupCount(0) + .MeasurementCount(5) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + private void RunBuildingComponent(BuildingComponentVisualizer viz) + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorModule.ComponentSystem.Data.VectorTile(decompressed); + viz.ClearCaches(); + if (tt.TryGetLayer("building", out var layer)) + { + viz.CreateMesh(tileId, layer); + } + } + + + [Test, Performance, Order(2)] + public void BuildingVisualizerOldChamfer() + { + var vizObj = (VectorLayerVisualizerObject) AssetDatabase.LoadAssetAtPath(_oldBuildVisAssetPath, typeof(VectorLayerVisualizerObject)); + var viz = (VectorLayerVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + + Measure.Method(() => { RunOldBuildings(viz); }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + [Test, Performance, Order(4)] + public void SingleBuildingVisualizerOldChamfer() + { + var vizObj = (VectorLayerVisualizerObject) AssetDatabase.LoadAssetAtPath(_oldBuildVisAssetPath, typeof(VectorLayerVisualizerObject)); + var viz = (VectorLayerVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + + Measure.Method(() => { RunOldBuildings(viz); }) + .WarmupCount(0) + .MeasurementCount(5) + .IterationsPerMeasurement(1) + .GC() + .Run(); + + } + + private void RunOldBuildings(VectorLayerVisualizer viz) + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorTile.VectorTile(decompressed, false); + viz.ClearCaches(); + var layer = tt.GetLayer("building"); + //if (tt.TryGetLayer("building", out var layer)) + { + viz.CreateMesh(tileId, layer); + } + } + } +} \ No newline at end of file diff --git a/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs.meta b/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs.meta new file mode 100644 index 000000000..1d148c0b3 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 519e05ae014b6402a97f6bd1f2e37455 +timeCreated: 1727879657 \ No newline at end of file diff --git a/Tests/Editor/VectorModule/BuildingSetups.meta b/Tests/Editor/VectorModule/BuildingSetups.meta new file mode 100644 index 000000000..99332a38f --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2fb6ba6d29c104dd1a4010a057753a2e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset b/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset new file mode 100644 index 000000000..f8f75a57a --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c6eb41c8ba1c475ab45d048dafafaf77, type: 3} + m_Name: TEST_BuildingLayerVisualizerObject + m_EditorClassIdentifier: + Settings: + EnableTerrainSnapping: 0 + RoundBuildingCorners: 1 + Material: {fileID: 0} + ChamferExtrusionSettings: + OffsetInMeters: 1 + ExtrusionOptions: + extrusionType: 1 + propertyName: height + minimumHeight: 0 + maximumHeight: 1000 + extrusionScaleFactor: 1 + BasicExtrusionSettings: + ExtrusionOptions: + extrusionType: 1 + propertyName: height + minimumHeight: 0 + maximumHeight: 1000 + extrusionScaleFactor: 1 + IsActive: 1 diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset.meta b/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset.meta new file mode 100644 index 000000000..5a045b76e --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 46dd07326358e4050b52f8020c1fab1f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset new file mode 100644 index 000000000..c9e6992c2 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dbafb4fa6dff39e41992450d63b53648, type: 3} + m_Name: TEST_OldLayerVisualizer + m_EditorClassIdentifier: + _vectorLayerName: + _settings: + Offset: {x: 0, y: 0, z: 0} + StackExecutionMode: 0 + _modifierStackObjects: + - {fileID: 11400000, guid: a291f041ea7e74b5c8ea54f067605580, type: 2} diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset.meta b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset.meta new file mode 100644 index 000000000..74fada781 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: de09110cfc8be47bfa2b6a530a34e70c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset new file mode 100644 index 000000000..e97c9fe71 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset @@ -0,0 +1,67 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a17705f45444b729e601b91361775ed, type: 3} + m_Name: TEST_OldModStack + m_EditorClassIdentifier: + Settings: + VisibilityZoomRange: {x: 1, y: 20} + MergeObjects: 1 + LayerType: 0 + Filters: {fileID: 1368362044068310752} + MeshModifiers: + - {fileID: 6984920302629165297} + - {fileID: 8207249058559122417} + GoModifiers: [] +--- !u!114 &1368362044068310752 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: FilterStack + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &6984920302629165297 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cc1f98279c3d447589a86caced7bbc42, type: 3} + m_Name: PolygonMeshModifierObject + m_EditorClassIdentifier: + m_Active: 1 + Height: 0 +--- !u!114 &8207249058559122417 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 70a1f5720dc24e308ea7af12b02b14c4, type: 3} + m_Name: ChamferHeightModifierObject + m_EditorClassIdentifier: + m_Active: 1 + ExtrusionOptions: + OffsetInMeters: 1 + FlatTops: 0 diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset.meta b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset.meta new file mode 100644 index 000000000..df3567cd3 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a291f041ea7e74b5c8ea54f067605580 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs b/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs new file mode 100644 index 000000000..8d29b996c --- /dev/null +++ b/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs @@ -0,0 +1,147 @@ +using System.Collections; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Data.Platform; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer; +using Mapbox.VectorModule.MeshGeneration; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using NUnit.Framework; +using Unity.PerformanceTesting; +using UnityEditor; +using UnityEngine.TestTools; + +namespace Mapbox.VectorModuleTests +{ + public class RoadLayerPerformanceTests + { + private int SampleCount = 100; + private ResilientWebRequestFileSource _fs; + private byte[] buffer; + private CanonicalTileId tileId = new CanonicalTileId(15, 18654, 9481); + private string LatLng = "60.1664427,24.9318587"; + private string _roadVisualizerAssetPath = "Packages/com.mapbox.sdk/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset"; + [UnitySetUp] + public IEnumerator SetUp() + { + var mapboxContext = new MapboxContext(); + _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); + + Response response = null; + var req = _fs.Request("https://api.mapbox.com/v4/mapbox.mapbox-streets-v7/" + tileId + ".vector.pbf", (r) => + { + response = r; + }); + while (!req.IsCompleted) + { + yield return null; + } + + buffer = response.Data; + Assert.NotZero(buffer.Length); + } + + private IMapInformation GetMapInformation() + { + var mapInformation = new MapInformation(LatLng); + mapInformation.SetInformation(null, 16, 0, 0, 1000); + mapInformation.Initialize(); + return mapInformation; + } + + [UnityTest, Performance] + public IEnumerator RoadVisualizer() + { + //var viz = new RoadComponentVisualizer("test", GetMapInformation(), null, new RoadComponentSettings()); + var vizObj = (RoadComponentVisualizerObject) AssetDatabase.LoadAssetAtPath(_roadVisualizerAssetPath, typeof(RoadComponentVisualizerObject)); + var viz = (RoadComponentVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + yield return viz.Initialize(); + + Measure.Method(() => + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorModule.ComponentSystem.Data.VectorTile(decompressed); + viz.ClearCaches(); + if (tt.TryGetLayer("road", out var layer)) + { + viz.CreateMesh(tileId, layer); + } + }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + [UnityTest, Performance] + public IEnumerator SingleRoadVisualizer() + { + //var viz = new RoadComponentVisualizer("test", GetMapInformation(), null, new RoadComponentSettings()); + var vizObj = (RoadComponentVisualizerObject) AssetDatabase.LoadAssetAtPath(_roadVisualizerAssetPath, typeof(RoadComponentVisualizerObject)); + var viz = (RoadComponentVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + yield return viz.Initialize(); + + Measure.Method(() => + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorModule.ComponentSystem.Data.VectorTile(decompressed); + viz.ClearCaches(); + if (tt.TryGetLayer("road", out var layer)) + { + viz.CreateMesh(tileId, layer); + } + }) + .WarmupCount(0) + .MeasurementCount(5) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + [Test, Performance] + public void RoadVisualizerOld() + { + var reg = RegularRoadLayer(GetMapInformation()); + Measure.Method(() => + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorTile.VectorTile(decompressed, false); + reg.ClearCaches(); + var layer = tt.GetLayer("road"); + //if (tt.TryGetLayer("building", out var layer)) + { + reg.CreateMesh(tileId, layer); + } + }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + private VectorLayerVisualizer RegularRoadLayer(IMapInformation mapInformation) + { + var viz = new VectorLayerVisualizer("test", mapInformation, null, null); + var modStackSettings = new ModifierStackSettings() { MergeObjects = true }; + var modStack = new ModifierStack(modStackSettings, null); + modStack.MeshModifiers.Add(new LineMeshForPolygonsModifier(new LineMeshParameters() + { + CapType = JoinType.Butt, + JoinType = JoinType.Round, + Width = 6 + } )); + + viz.AddModifierStack(new List() { modStack }); + viz.Initialize(); + return viz; + } + + } +} \ No newline at end of file diff --git a/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs.meta b/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs.meta new file mode 100644 index 000000000..72fa1de93 --- /dev/null +++ b/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: af9097243e404b70a404c0738447631e +timeCreated: 1765533448 \ No newline at end of file diff --git a/Tests/Editor/VectorModule/RoadSetup.meta b/Tests/Editor/VectorModule/RoadSetup.meta new file mode 100644 index 000000000..380a2a62e --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6f2ffe4e624884e7e968f5d00eddee7a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset new file mode 100644 index 000000000..96fd1bb87 --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset @@ -0,0 +1,102 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bf8c281932374fb0b5c80fa7fbca2b5b, type: 3} + m_Name: TEST_BasicRoadStyleSheet + m_EditorClassIdentifier: + Styles: + - Name: secondary,tertiary + Filters: + Filters: + - rid: 5722777937161748527 + Type: 0 + Width: 4.8 + PushUp: 0.0008 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: primary + Filters: + Filters: + - rid: 5722777937161748529 + Type: 0 + Width: 5.2 + PushUp: 0.001 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: street,street_limited + Filters: + Filters: + - rid: 5722777937161748530 + Type: 0 + Width: 3.6 + PushUp: 0.0004 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: service + Filters: + Filters: + - rid: 5722777937161748531 + Type: 0 + Width: 1.8 + PushUp: 0.0002 + Material: {fileID: 2100000, guid: d449f6eb93a644716baa6f1f4c730813, type: 2} + - Name: path + Filters: + Filters: + - rid: 5722777987101753359 + - rid: 5722777987101753361 + - rid: 5722777987101753362 + Type: 0 + Width: 1 + PushUp: 0 + Material: {fileID: 2100000, guid: c3c69f315ce7c4100a0ff6f40305bfe9, type: 2} + references: + version: 2 + RefIds: + - rid: 5722777937161748527 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: secondary,tertiary + Invert: 0 + - rid: 5722777937161748529 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: primary + Invert: 0 + - rid: 5722777937161748530 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: street,street_limited + Invert: 0 + - rid: 5722777937161748531 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: service + Invert: 0 + - rid: 5722777987101753359 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: path + Invert: 0 + - rid: 5722777987101753361 + type: {class: RoadStructureFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: tunnel + Invert: 1 + - rid: 5722777987101753362 + type: {class: RoadTypeFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: steps + Invert: 1 diff --git a/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset.meta b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset.meta new file mode 100644 index 000000000..3bf74b31f --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 128b299ad7c164ddbb2d2d02f2b18ed7 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset new file mode 100644 index 000000000..99c925d51 --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e926a31006c7a440ea45ffe7b2bcfff6, type: 3} + m_Name: TEST_BasicRoads + m_EditorClassIdentifier: + Settings: + GameObjectOffset: 0.0001 + RandomOffsetRange: 0.0001 + RoadStyleSheet: {fileID: 11400000, guid: 128b299ad7c164ddbb2d2d02f2b18ed7, type: 2} + IsActive: 1 diff --git a/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset.meta b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset.meta new file mode 100644 index 000000000..ad9cc3477 --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 076ae6f26b84c457aa3bf8e5ef96b1d6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/VectorModuleTests.asmdef b/Tests/Editor/VectorModule/VectorModuleTests.asmdef new file mode 100644 index 000000000..3f8bc1c3c --- /dev/null +++ b/Tests/Editor/VectorModule/VectorModuleTests.asmdef @@ -0,0 +1,30 @@ +{ + "name": "VectorModuleTests", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:2400806fb903448e5b7e737b92f1e434", + "GUID:9f843d4b38153f04b9cee0502d568525", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:c0dd0d10738d4ad4a9de57c559d0ca1b", + "GUID:5aef8304858a8459cb2204d15bf75ab9" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Mapbox.VectorTile.VectorTileReader.dll", + "Mapbox.VectorTile.Geometry.dll" + ], + "autoReferenced": true, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Editor/VectorModule/VectorModuleTests.asmdef.meta b/Tests/Editor/VectorModule/VectorModuleTests.asmdef.meta new file mode 100644 index 000000000..422002604 --- /dev/null +++ b/Tests/Editor/VectorModule/VectorModuleTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 398284599e92e436e986b419aeda78a3 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/BaseModule/MapTest.cs b/Tests/Runtime/BaseModule/MapTest.cs index e1fe75ceb..9d64277d7 100644 --- a/Tests/Runtime/BaseModule/MapTest.cs +++ b/Tests/Runtime/BaseModule/MapTest.cs @@ -119,7 +119,7 @@ private IEnumerator LoadMap(string latlng, bool useSqlite = true, bool useFileCa mapInfo, _vectorSource, unityContext, - new Dictionary(), + new Dictionary>(), new VectorModuleSettings() { DataSettings = vectorSourceSettings }); mapVisualizer.LayerModules.Add(_imageLayer); mapVisualizer.LayerModules.Add(_terrainLayer); diff --git a/Tests/Runtime/BaseModule/MapViewLoadTests.cs b/Tests/Runtime/BaseModule/MapViewLoadTests.cs index 4bb47f13c..cd4ba96e3 100644 --- a/Tests/Runtime/BaseModule/MapViewLoadTests.cs +++ b/Tests/Runtime/BaseModule/MapViewLoadTests.cs @@ -103,7 +103,7 @@ public IEnumerator OneTimeSetUp() mapInfo, _vectorSource, unityContext, - new Dictionary(), + new Dictionary>(), new VectorModuleSettings() { DataSettings = vectorSourceSettings}); mapVisualizer.LayerModules.Add(_imageLayer); mapVisualizer.LayerModules.Add(_terrainLayer); diff --git a/package.json b/package.json index 50815b914..95e7cd82b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,11 @@ "displayName": "GeocodingApiDemo", "description": "Geocoding Api Demo", "path": "Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo" + }, + { + "displayName": "MapboxComponents", + "description": "Mapbox Components Demo", + "path": "Samples~/MapboxComponents" } ] } From c570ec8593d923aa6fccf25bfb99855b2ce58463 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Fri, 13 Mar 2026 17:37:30 +0300 Subject: [PATCH 11/54] fix a bug causing zombie tiles --- Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index c9a6e3b42..252613c76 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -405,13 +405,16 @@ protected void CreateTempTile(UnwrappedTileId tileId, out UnityMapTile tile) protected bool CreateTileInstant(UnwrappedTileId tileId, out UnityMapTile tile) { - //we need to do positioning and scaling before mesh gen for now GetMapTile(tileId, out tile); var result = CreateTile(tile); - //couldn't create the tile - if (!result) PoolTile(tile); + if (!result) + { + tile.Recycle(); + tile.LoadingState = LoadingState.None; + _tileCreator.PutTile(tile); + } return result; } From 02718aaedfd5a1b97780ccfa773fdb7362a84581 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 10:00:47 +0300 Subject: [PATCH 12/54] Squashed commit for camera improvements commit 67433d3b235df142868031413098eb76cc0c6938 Author: Baran Kahyaoglu Date: Tue Apr 7 21:32:23 2026 +0300 add new input system support commit 9e8cb96f0aa43dc1f94e5ad84512807e3c478f13 Author: Baran Kahyaoglu Date: Tue Apr 7 20:42:27 2026 +0300 camera improvements add touch support to camera refine camera scripts for extensibility commit 13408dede59d9cb220e0b0f526882f6cafeeeb07 Author: Baran Kahyaoglu Date: Tue Apr 7 16:57:30 2026 +0300 add touch controls for pan and zoom commit c4a5df9503844fefe7496aab0105bad1699743ac Author: Baran Kahyaoglu Date: Tue Apr 7 13:39:30 2026 +0300 cleanup moving and slippy camera scripts --- Documentation~/CameraSystem.md | 109 +++++ .../BaseModule/Utilities/VectorExtensions.cs | 21 + .../Example/Scripts/MapCameraBehaviour.cs | 50 +++ .../Scripts/MapCameraBehaviour.cs.meta | 11 + Runtime/Mapbox/Example/Scripts/MapInput.cs | 419 +++++++++++++++++- .../Mapbox/Example/Scripts/Moving3dCamera.cs | 165 ++++--- .../Scripts/Moving3dCameraBehaviour.cs | 45 +- .../Mapbox/Example/Scripts/SlippyMapCamera.cs | 161 +++---- .../Scripts/SlippyMapCameraBehaviour.cs | 35 +- 9 files changed, 797 insertions(+), 219 deletions(-) create mode 100644 Documentation~/CameraSystem.md create mode 100644 Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs create mode 100644 Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs.meta diff --git a/Documentation~/CameraSystem.md b/Documentation~/CameraSystem.md new file mode 100644 index 000000000..1b45f4e23 --- /dev/null +++ b/Documentation~/CameraSystem.md @@ -0,0 +1,109 @@ + +# Camera System + +The Mapbox SDK includes a camera system for navigating the map with mouse and touch input. There are two camera modes, each suited to different use cases. + +--- + +## Camera Modes + +### 3D Camera (Moving3dCameraBehaviour) + +The camera moves through 3D space while the map stays in place. Best for: +- 3D city exploration +- Games and simulations +- Scenes where the camera needs to orbit around a point of interest + +The camera orbits a target point on the map. Panning moves the camera, zooming adjusts how far the camera is from the ground, and rotation orbits around the target. + +### Slippy Map Camera (SlippyMapCameraBehaviour) + +The camera stays in a fixed position while the map moves underneath. Best for: +- Traditional 2D map interfaces +- Navigation and routing views +- Data visualization overlays +- AR/VR applications where the camera is controlled by head tracking + +Panning shifts the map's geographic center. Zooming changes the map's scale level. The camera itself doesn't move — the world moves under it. + +--- + +## Setting Up + +1. Your scene needs a **MapboxMapBehaviour** on a GameObject (this is the map itself). +2. Add a camera behaviour component to any GameObject in the scene: + - **Moving3dCameraBehaviour** for 3D camera, or + - **SlippyMapCameraBehaviour** for slippy map camera +3. Assign the **Map Behaviour** field to the GameObject that has MapboxMapBehaviour. +4. Assign the **Camera** field to the scene camera used for rendering. If left empty, the main camera is used automatically. + +The camera will initialize itself once the map is ready. No additional setup is required. + +--- + +## Settings Reference + +### 3D Camera Settings + +| Setting | Description | +|---------|-------------| +| **Pitch** | Camera tilt angle. 90 = looking straight down, 15 = near the horizon. | +| **Bearing** | Camera rotation in degrees. 0 = north is up. | +| **Camera Curve** | An animation curve that controls how camera height changes with zoom level. The X axis is zoom, Y axis is distance from the ground. | +| **Distance Scale** | Multiplier on the camera curve output. Increase to raise the camera without re-editing the curve. Default: 2. | +| **Zoom Sensitivity** | How much each scroll step changes the zoom level. Default: 0.25. | +| **Rotation Speed** | How fast right-click drag rotates the camera. Default: 50. | + +### Slippy Map Camera Settings + +| Setting | Description | +|---------|-------------| +| **Center Latitude Longitude** | Initial map center as a geographic coordinate. Example: 40.7128, -74.0060 for New York. | +| **Pitch** | Camera tilt angle. 90 = looking straight down, 15 = near the horizon. | +| **Bearing** | Camera rotation in degrees. 0 = north is up. | +| **Camera Distance** | How far the camera is from the ground target. Typical range: 100 to 1000 depending on your map's scale. Default: 200. | +| **Pan Enabled** | Toggle left-click / single-finger drag to pan the map. | + +**Rotation Settings** (nested): + +| Setting | Description | +|---------|-------------| +| **Enabled** | Toggle right-click drag to rotate. | +| **Speed** | Rotation sensitivity. Default: 50. | +| **Rotation Mode** | *Rotate The Camera* orbits the camera around the map. *Rotate The Map* keeps the camera fixed and rotates the map underneath — useful for navigation-style views. | +| **Map Root** | The root transform of the map. Auto-detected if left empty. | + +**Zoom Settings** (nested): + +| Setting | Description | +|---------|-------------| +| **Enabled** | Toggle scroll wheel / pinch zoom. | +| **Speed** | Zoom sensitivity per step. Default: 0.25. | +| **Zoom At Cursor** | When enabled, the map zooms toward the cursor or pinch position. When disabled, zoom is always centered on the map. | + +--- + +## Input Controls + +Both cameras support mouse and touch input. The controls are: + +| Action | Mouse | Touch | +|--------|-------|-------| +| Pan | Left-click drag | Single-finger drag | +| Rotate | Right-click drag | Not available via touch | +| Zoom | Scroll wheel | Two-finger pinch | +| Tilt | Not available via mouse | Two-finger vertical drag | + +When using two fingers, the system automatically detects whether you are pinching (zoom) or dragging vertically (tilt) and applies the dominant gesture. Both gestures will not activate at the same time. + +Zoom at cursor works with both mouse and touch — when using pinch, the zoom centers on the midpoint between your two fingers. + +--- + +## Writing a Custom Camera + +If neither camera mode fits your needs, you can create your own by writing a class that extends `MapInput` and a MonoBehaviour that extends `MapCameraBehaviour`. + +Your custom camera class needs to implement one method — `UpdateCamera` — where you handle input and return camera state (center, zoom, pitch, bearing, scale). The base class provides ready-made helpers for input detection, coordinate conversion, and value clamping. + +The behaviour wrapper handles initialization and communicating changes to the map automatically. A minimal custom camera behaviour only needs a few lines of code beyond the camera logic itself. diff --git a/Runtime/Mapbox/BaseModule/Utilities/VectorExtensions.cs b/Runtime/Mapbox/BaseModule/Utilities/VectorExtensions.cs index b3e8194de..047ecfea1 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/VectorExtensions.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/VectorExtensions.cs @@ -141,5 +141,26 @@ public static LatitudeLongitude GetGeoPosition(this Vector2 position, Vector2d r { return position.ToVector3xz().GetGeoPosition(refPoint, scale); } + + /// + /// Raycast from a screen position onto a plane and return the intersection point. + /// Safer than manual ray math — handles edge cases like rays parallel to the plane. + /// + /// Camera to raycast from. + /// Target plane for intersection. + /// Screen-space position (e.g. Input.mousePosition). + /// World-space intersection point. Zero if no intersection. + /// True if the ray intersects the plane. + public static bool GetPlaneIntersection(this Camera camera, Plane plane, Vector3 screenPosition, out Vector3 hit) + { + hit = Vector3.zero; + var ray = camera.ScreenPointToRay(screenPosition); + if (plane.Raycast(ray, out var distance)) + { + hit = ray.GetPoint(distance); + return true; + } + return false; + } } } diff --git a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs new file mode 100644 index 000000000..11dd44530 --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs @@ -0,0 +1,50 @@ +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.Example.Scripts.Map; +using UnityEngine; + +namespace Mapbox.Example.Scripts.MapInput +{ + /// + /// Generic base MonoBehaviour for any MapInput-based camera. + /// Handles initialization, the Update loop, and ChangeView dispatch. + /// Subclass with a concrete camera type, or use directly if no extra behaviour is needed. + /// + public abstract class MapCameraBehaviour : MonoBehaviour where T : MapInput + { + [Tooltip("Reference to the map behaviour that provides map state and tile management")] + public MapBehaviourCore MapBehaviour; + [Tooltip("Camera used for map rendering and input raycasting. Falls back to Camera.main if not set")] + public Camera Camera; + + protected MapboxMap Map; + protected bool IsInitialized; + + public abstract T Core { get; } + + protected virtual void Awake() + { + if (MapBehaviour == null) MapBehaviour = FindObjectOfType(); + if (Camera == null) Camera = Camera.main; + + MapBehaviour.Initialized += OnMapInitialized; + } + + protected virtual void OnMapInitialized(MapboxMap map) + { + Map = map; + IsInitialized = true; + Core.Initialize(Camera, map.MapInformation); + } + + protected virtual void Update() + { + if (!IsInitialized || Map.MapInformation == null) + return; + + var output = Core.UpdateCamera(Map.MapInformation); + if (output.HasChanged) + Map.ChangeView(output.Center, output.Zoom, output.Pitch, output.Bearing, output.Scale); + } + } +} diff --git a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs.meta b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs.meta new file mode 100644 index 000000000..e32bec822 --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 128117eb38e334d748d030fded176dda +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index b1a7e7614..0393495e7 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -1,11 +1,426 @@ +using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; using UnityEngine; +using UnityEngine.EventSystems; +#if MAPBOX_NEW_INPUT_SYSTEM +using UnityEngine.InputSystem; +using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch; +using TouchPhase = UnityEngine.InputSystem.TouchPhase; +#endif namespace Mapbox.Example.Scripts.MapInput { + /// + /// Reusable output from UpdateCamera. Populated each frame by the camera implementation. + /// Passed to MapboxMap.ChangeView — null fields mean "no change". + /// + public class CameraOutput + { + public LatitudeLongitude? Center; + public float? Zoom; + public float? Pitch; + public float? Bearing; + public float? Scale; + public bool HasChanged; + + public void Reset() + { + Center = null; + Zoom = null; + Pitch = null; + Bearing = null; + Scale = null; + HasChanged = false; + } + } + public abstract class MapInput { protected Camera _camera; - public abstract bool UpdateCamera(IMapInformation mapInfo); + protected Plane _controlPlane = new Plane(Vector3.up, Vector3.zero); + protected readonly CameraOutput _output = new CameraOutput(); + + // Pinch state tracking + private float _previousPinchDistance; + private bool _pinchActive; + + public virtual void Initialize(Camera camera, IMapInformation mapInfo) + { + _camera = camera ? camera : Camera.main; +#if MAPBOX_NEW_INPUT_SYSTEM + if (!UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.enabled) + UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Enable(); +#endif + } + + public abstract CameraOutput UpdateCamera(IMapInformation mapInfo); + + #region Plane Intersection + + protected bool GetPlaneIntersection(Vector3 screenPosition, out Vector3 hit) + { + return _camera.GetPlaneIntersection(_controlPlane, screenPosition, out hit); + } + + #endregion + + #region Camera Utilities + + public Plane[] GetFrustumPlanes() + { + return GeometryUtility.CalculateFrustumPlanes(_camera); + } + + public Transform GetTransform() + { + return _camera.transform; + } + + #endregion + + #region Value Clamping + + protected static float ClampZoom(float zoom, float min = 0f, float max = 22f) + { + return Mathf.Clamp(zoom, min, max); + } + + protected static float ClampPitch(float pitch, float min = 15f, float max = 90f) + { + return Mathf.Clamp(pitch, min, max); + } + + protected static float ClampBearing(float bearing) + { + bearing %= 360f; + if (bearing > 180f) bearing -= 360f; + if (bearing < -180f) bearing += 360f; + return bearing; + } + + #endregion + + #region Input Helpers + + /// + /// Returns the number of active touches, or 0 if no touch device is available. + /// + private int GetTouchCount() + { +#if MAPBOX_NEW_INPUT_SYSTEM + return Touch.activeTouches.Count; +#else + return Input.touchCount; +#endif + } + + /// + /// Call at the start of UpdateCamera to maintain input state between frames. + /// Resets pinch tracking when fingers are lifted. + /// + protected void UpdateInputState() + { + if (GetTouchCount() < 2) + _pinchActive = false; + } + + /// + /// Returns true if the pointer (mouse or primary touch) is over a UI element. + /// + protected bool IsPointerOverUI() + { + if (EventSystem.current == null) + return false; + +#if MAPBOX_NEW_INPUT_SYSTEM + var touchCount = GetTouchCount(); + if (touchCount > 0) + return EventSystem.current.IsPointerOverGameObject(Touch.activeTouches[0].finger.index); + return EventSystem.current.IsPointerOverGameObject(); +#else + if (Input.touchCount > 0) + return EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId); + return EventSystem.current.IsPointerOverGameObject(); +#endif + } + + /// + /// Screen position of the primary pointer (first touch or mouse cursor). + /// + protected Vector3 GetPointerPosition() + { +#if MAPBOX_NEW_INPUT_SYSTEM + if (Touch.activeTouches.Count > 0) + return Touch.activeTouches[0].screenPosition; + if (Mouse.current != null) + return Mouse.current.position.ReadValue(); + return Vector3.zero; +#else + if (Input.touchCount > 0) + return Input.GetTouch(0).position; + return Input.mousePosition; +#endif + } + + /// + /// True on the frame the primary pointer goes down (touch began or LMB pressed). + /// + protected bool GetPointerDown() + { +#if MAPBOX_NEW_INPUT_SYSTEM + if (Touch.activeTouches.Count > 0) + return Touch.activeTouches[0].phase == TouchPhase.Began; + return Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame; +#else + if (Input.touchCount > 0) + return Input.GetTouch(0).phase == UnityEngine.TouchPhase.Began; + return Input.GetMouseButtonDown(0); +#endif + } + + /// + /// True while the primary pointer is held (single touch active or LMB held). + /// Returns false when two or more touches are active (pinch takes priority). + /// + protected bool GetPointerHeld() + { +#if MAPBOX_NEW_INPUT_SYSTEM + var touchCount = Touch.activeTouches.Count; + if (touchCount == 1) + { + var phase = Touch.activeTouches[0].phase; + return phase == TouchPhase.Moved || phase == TouchPhase.Stationary; + } + if (touchCount == 0) + return Mouse.current != null && Mouse.current.leftButton.isPressed; + return false; +#else + if (Input.touchCount == 1) + { + var phase = Input.GetTouch(0).phase; + return phase == UnityEngine.TouchPhase.Moved || phase == UnityEngine.TouchPhase.Stationary; + } + if (Input.touchCount == 0) + return Input.GetMouseButton(0); + return false; +#endif + } + + /// + /// True on the frame the secondary pointer goes down (RMB only, no touch equivalent). + /// + protected bool GetSecondaryDown() + { +#if MAPBOX_NEW_INPUT_SYSTEM + if (Touch.activeTouches.Count > 0) + return false; + return Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame; +#else + if (Input.touchCount > 0) + return false; + return Input.GetMouseButtonDown(1); +#endif + } + + /// + /// True while the secondary pointer is held (RMB only, no touch equivalent). + /// + protected bool GetSecondaryHeld() + { +#if MAPBOX_NEW_INPUT_SYSTEM + if (Touch.activeTouches.Count > 0) + return false; + return Mouse.current != null && Mouse.current.rightButton.isPressed; +#else + if (Input.touchCount > 0) + return false; + return Input.GetMouseButton(1); +#endif + } + + /// + /// Detects pinch-to-zoom (two fingers) or mouse scroll wheel. + /// Returns true if zoom input is active, with the delta as a zoom level change. + /// Positive = zoom in, negative = zoom out. + /// When two fingers are active, only returns true if pinch is the dominant gesture (vs tilt). + /// + protected bool GetPinchZoomDelta(out float zoomDelta, float pinchSensitivity = 5f) + { + zoomDelta = 0f; + +#if MAPBOX_NEW_INPUT_SYSTEM + var touchCount = Touch.activeTouches.Count; + if (touchCount >= 2) + { + var touch0 = Touch.activeTouches[0]; + var touch1 = Touch.activeTouches[1]; + var currentDistance = Vector2.Distance(touch0.screenPosition, touch1.screenPosition); + + if (!_pinchActive) + { + _pinchActive = true; + _previousPinchDistance = currentDistance; + return false; + } + + var pinchDelta = currentDistance - _previousPinchDistance; + _previousPinchDistance = currentDistance; + + var avgDeltaY = (touch0.delta.y + touch1.delta.y) / 2f; + var pinchMagnitude = Mathf.Abs(pinchDelta); + var tiltMagnitude = Mathf.Abs(avgDeltaY); + + if (pinchMagnitude > tiltMagnitude && pinchMagnitude > 0.01f) + { + zoomDelta = pinchDelta / Screen.height * pinchSensitivity; + return true; + } + + return false; + } + + if (Mouse.current != null) + { + var scroll = Mouse.current.scroll.ReadValue(); + if (Mathf.Abs(scroll.y) > 0) + { + zoomDelta = scroll.y / 120f; + return true; + } + } + + return false; +#else + if (Input.touchCount >= 2) + { + var touch0 = Input.GetTouch(0); + var touch1 = Input.GetTouch(1); + var currentDistance = Vector2.Distance(touch0.position, touch1.position); + + if (!_pinchActive) + { + _pinchActive = true; + _previousPinchDistance = currentDistance; + return false; + } + + var pinchDelta = currentDistance - _previousPinchDistance; + _previousPinchDistance = currentDistance; + + // Compare pinch magnitude vs tilt magnitude to pick the dominant gesture + var avgDeltaY = (touch0.deltaPosition.y + touch1.deltaPosition.y) / 2f; + var pinchMagnitude = Mathf.Abs(pinchDelta); + var tiltMagnitude = Mathf.Abs(avgDeltaY); + + if (pinchMagnitude > tiltMagnitude && pinchMagnitude > 0.01f) + { + zoomDelta = pinchDelta / Screen.height * pinchSensitivity; + return true; + } + + return false; + } + + if (Input.mouseScrollDelta.magnitude > 0) + { + zoomDelta = Input.GetAxis("Mouse ScrollWheel"); + return true; + } + + return false; +#endif + } + + /// + /// Detects two-finger vertical drag for pitch/tilt control. + /// Both fingers moving in the same vertical direction = tilt. + /// Only returns true if tilt is the dominant gesture (vs pinch). + /// + protected bool GetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity = 0.5f) + { + tiltDelta = 0f; + +#if MAPBOX_NEW_INPUT_SYSTEM + if (Touch.activeTouches.Count < 2) + return false; + + var touch0 = Touch.activeTouches[0]; + var touch1 = Touch.activeTouches[1]; + + var deltaY0 = touch0.delta.y; + var deltaY1 = touch1.delta.y; + + if (deltaY0 * deltaY1 <= 0) + return false; + + var avgDeltaY = (deltaY0 + deltaY1) / 2f; + if (Mathf.Abs(avgDeltaY) < 1f) + return false; + + var currentDistance = Vector2.Distance(touch0.screenPosition, touch1.screenPosition); + var pinchDelta = _pinchActive ? Mathf.Abs(currentDistance - _previousPinchDistance) : 0f; + + if (Mathf.Abs(avgDeltaY) <= pinchDelta) + return false; + + tiltDelta = avgDeltaY / Screen.height * tiltSensitivity; + return true; +#else + if (Input.touchCount < 2) + return false; + + var touch0 = Input.GetTouch(0); + var touch1 = Input.GetTouch(1); + + var deltaY0 = touch0.deltaPosition.y; + var deltaY1 = touch1.deltaPosition.y; + + // Both fingers must move in the same vertical direction + if (deltaY0 * deltaY1 <= 0) + return false; + + var avgDeltaY = (deltaY0 + deltaY1) / 2f; + if (Mathf.Abs(avgDeltaY) < 1f) + return false; + + // Compare tilt magnitude vs pinch magnitude to pick the dominant gesture + var currentDistance = Vector2.Distance(touch0.position, touch1.position); + var pinchDelta = _pinchActive ? Mathf.Abs(currentDistance - _previousPinchDistance) : 0f; + + if (Mathf.Abs(avgDeltaY) <= pinchDelta) + return false; + + tiltDelta = avgDeltaY / Screen.height * tiltSensitivity; + return true; +#endif + } + + /// + /// Screen position to use as zoom center. + /// For pinch: midpoint between two fingers. For mouse: cursor position. + /// + protected Vector3 GetZoomCenter() + { +#if MAPBOX_NEW_INPUT_SYSTEM + if (Touch.activeTouches.Count >= 2) + { + var touch0 = Touch.activeTouches[0]; + var touch1 = Touch.activeTouches[1]; + return ((Vector3)touch0.screenPosition + (Vector3)touch1.screenPosition) / 2f; + } + return Mouse.current != null ? (Vector3)Mouse.current.position.ReadValue() : Vector3.zero; +#else + if (Input.touchCount >= 2) + { + var touch0 = Input.GetTouch(0); + var touch1 = Input.GetTouch(1); + return (touch0.position + touch1.position) / 2f; + } + return Input.mousePosition; +#endif + } + + #endregion } -} \ No newline at end of file +} diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs index fca6ef8b7..edcce2512 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs @@ -1,38 +1,42 @@ using System; using Mapbox.BaseModule.Map; using UnityEngine; -using UnityEngine.EventSystems; +using UnityEngine.Serialization; namespace Mapbox.Example.Scripts.MapInput { [Serializable] public class Moving3dCamera : MapInput { + [Tooltip("Camera tilt angle. 90 = top-down, 15 = near-horizon")] [Range(15, 90)] public float Pitch; + [Tooltip("Camera rotation in degrees. 0 = north up")] [Range(-180, 180)] public float Bearing; [NonSerialized] public float ZoomValue; - public float Speed = -10; - public float CameraDistance; - - public Action Updated = () => { }; + [NonSerialized] public float CameraDistance; + + [Tooltip("Animation curve mapping zoom level to camera distance. X axis = zoom, Y axis = distance")] + public AnimationCurveContainer CameraCurve; + [Tooltip("Scales camera distance from curve output. Use to adjust height without re-authoring the curve. Default: 2")] + [FormerlySerializedAs("CamDistanceMultiplier")] + public float DistanceScale = 2; + [Tooltip("Scroll wheel zoom sensitivity per step. Default: 0.25")] + [FormerlySerializedAs("ZoomSpeed")] + public float ZoomSensitivity = 0.25f; + [Tooltip("Right-click rotation sensitivity. Higher = faster rotation. Default: 50")] + public float RotationSpeed = 50.0f; private Vector3 _previousScreenPosition; private Vector3 _dragOrigin; - [SerializeField] private Vector3 _targetPosition; - private float deltaAngleH; - private float deltaAngleV; - public float RotationSpeed = 50.0f; - public AnimationCurveContainer CameraCurve; - public float CamDistanceMultiplier = 2; - public float ZoomSpeed = 0.25f; - - - public void Initialize(Camera camera, IMapInformation start) + private Vector3 _targetPosition; + + + public override void Initialize(Camera camera, IMapInformation start) { - _camera = camera ? camera : Camera.main; + base.Initialize(camera, start); Pitch = start.Pitch; Bearing = start.Bearing; ZoomValue = start.Zoom; @@ -44,69 +48,80 @@ public void Initialize(Camera camera, IMapInformation start) }; start.ViewChanged += SetCamera; } - - public override bool UpdateCamera(IMapInformation mapInformation) + + public override CameraOutput UpdateCamera(IMapInformation mapInformation) { - if (EventSystem.current.IsPointerOverGameObject()) - return false; - - var hasChanged = false; - if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) + _output.Reset(); + UpdateInputState(); + + if (IsPointerOverUI()) + return _output; + + var pointerPos = GetPointerPosition(); + Vector3 cursorHit; + if (!GetPlaneIntersection(pointerPos, out cursorHit)) + return _output; + + if (GetPointerDown() || GetSecondaryDown()) { - _previousScreenPosition = UnityEngine.Input.mousePosition; - _dragOrigin = GetPlaneIntersection(UnityEngine.Input.mousePosition); + _previousScreenPosition = pointerPos; + _dragOrigin = cursorHit; } - if (Input.GetMouseButton(0)) + if (GetPointerHeld()) { - var newPoint = GetPlaneIntersection(Input.mousePosition); + if (!GetPlaneIntersection(pointerPos, out var newPoint)) + return _output; Vector3 pos = newPoint - _dragOrigin; - Vector3 move = new Vector3(pos.x * Speed, 0, pos.z * Speed); - - _targetPosition += move; - hasChanged = true; - + _targetPosition -= new Vector3(pos.x, 0, pos.z); + _output.HasChanged = true; } - else if (Input.GetMouseButton(1) ) + else if (GetSecondaryHeld()) { - var deltaMousePos = (Input.mousePosition - _previousScreenPosition); - deltaAngleH = deltaMousePos.x; - deltaAngleV = deltaMousePos.y; + var deltaMousePos = (pointerPos - _previousScreenPosition); + var deltaAngleH = deltaMousePos.x; + var deltaAngleV = deltaMousePos.y; if (deltaAngleH != 0 || deltaAngleV != 0) { Pitch -= deltaAngleV * Time.deltaTime * RotationSpeed; - Pitch = Mathf.Min(90, Mathf.Max(15, Pitch)); - Bearing += deltaAngleH * Time.deltaTime * RotationSpeed; + Pitch = ClampPitch(Pitch); + Bearing = ClampBearing(Bearing + deltaAngleH * Time.deltaTime * RotationSpeed); + _output.HasChanged = true; } - hasChanged = true; } - else if (Input.mouseScrollDelta.magnitude > 0) + else if (GetPinchZoomDelta(out var zoomDelta)) { - Zoom( - mapInformation, - GetPlaneIntersection(Input.mousePosition), - Input.GetAxis("Mouse ScrollWheel")); - //we still update _targetPosition with center screen as if this cursor focused zoom didn't happen - //this'll ensure smooth transition to other movements (pan after zoom) - hasChanged = true; + var zoomCenter = GetZoomCenter(); + if (GetPlaneIntersection(zoomCenter, out var zoomHit)) + { + Zoom(mapInformation, zoomHit, zoomDelta); + _output.HasChanged = true; + } } - //SetCamPositionByMapInfo(mapInformation); - - _dragOrigin = GetPlaneIntersection(Input.mousePosition); - _previousScreenPosition = Input.mousePosition; - - //we probably shouldn't write to mapInformation here but do it in Moving3dCamBehaviour, along with pitch and bearing - //mapInformation.ViewCenter = GetPlaneIntersection(Camera.ViewportToScreenPoint(new Vector3(0.5f, 0.5f))); - if(hasChanged) SetCamPositionByMapInfo(); - return hasChanged; + + if (GetTwoFingerTiltDelta(out var tiltDelta)) + { + Pitch -= tiltDelta * RotationSpeed; + Pitch = ClampPitch(Pitch); + _output.HasChanged = true; + } + + GetPlaneIntersection(pointerPos, out _dragOrigin); + _previousScreenPosition = pointerPos; + + if (_output.HasChanged) + { + SetCamPositionByMapInfo(); + _output.Zoom = ZoomValue; + _output.Pitch = Pitch; + _output.Bearing = Bearing; + } + return _output; } public void Zoom(IMapInformation mapInformation, Vector3 position, float zoomAction) { - //calculate next zoom value by simply adding scroll delta * speed - //var mouseWorld = position; - //var mouseMeter = mouseWorld * mapInformation.Scale; - var postZoom = ZoomValue + zoomAction * ZoomSpeed; + var postZoom = ClampZoom(ZoomValue + zoomAction * ZoomSensitivity); //to be able to achieve zoom on mouse cursor, we have to move camera on mouse world pos - camera pos line (b-c) //but our camera distance uses camera target (mid screen) to camera pos distance (a-c) @@ -140,48 +155,32 @@ private void SetCamPositionByMapInfo() _camera.transform.position = _targetPosition; _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); - _dragOrigin = GetPlaneIntersection(UnityEngine.Input.mousePosition); + GetPlaneIntersection(GetPointerPosition(), out _dragOrigin); } - + public void SetCamera(IMapInformation mapInfo) { Pitch = mapInfo.Pitch; Bearing = mapInfo.Bearing; - + CameraDistance = CalculateCameraDistance(mapInfo, mapInfo.Zoom); _camera.transform.position = _targetPosition; _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); - _dragOrigin = GetPlaneIntersection(UnityEngine.Input.mousePosition); + GetPlaneIntersection(GetPointerPosition(), out _dragOrigin); } - private Vector3 GetPlaneIntersection(Vector3 screenPosition) - { - var ray = _camera.ScreenPointToRay(screenPosition); - var dirNorm = ray.direction / ray.direction.y; - var intersectionPos = ray.origin - dirNorm * ray.origin.y; - return intersectionPos; - } - private float CalculateCameraDistance(IMapInformation mapInformation, float postZoom) { var distance = CameraCurve.Evaluate(postZoom); - return CamDistanceMultiplier * distance / mapInformation.Scale; + return DistanceScale * distance / mapInformation.Scale; } public Vector3 GetViewCenterPosition() { - return GetPlaneIntersection(_camera.ViewportToScreenPoint(new Vector3(.5f, .5f, 0f))); - } - - public Plane[] GetFrustrumPlanes() - { - return GeometryUtility.CalculateFrustumPlanes(_camera); + GetPlaneIntersection(_camera.ViewportToScreenPoint(new Vector3(.5f, .5f, 0f)), out var center); + return center; } - public Transform GetTransform() - { - return _camera.transform; - } } } \ No newline at end of file diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs index 8be23d81b..937618eb4 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs @@ -1,53 +1,30 @@ using Mapbox.BaseModule.Map; -using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.Map; using UnityEngine; +using UnityEngine.Serialization; namespace Mapbox.Example.Scripts.MapInput { - public class Moving3dCameraBehaviour : MonoBehaviour + public class Moving3dCameraBehaviour : MapCameraBehaviour { - public MapBehaviourCore MapBehaviour; - public Camera Camera; - public Moving3dCamera Core; + [Tooltip("3D camera settings. Camera moves through world space while the map stays in place")] + [FormerlySerializedAs("Core")] + [SerializeField] private Moving3dCamera _core; - private MapboxMap _map; - private bool _isInitialized = false; - - private void Awake() - { - if (MapBehaviour == null) MapBehaviour = FindObjectOfType(); - if(Camera == null) Camera = Camera.main; - - MapBehaviour.Initialized += (map) => - { - _map = map; - _isInitialized = true; - Core.Initialize(Camera, _map.MapInformation); - }; - } - - public void Update() - { - if (_isInitialized && _map.MapInformation != null && Core.UpdateCamera(_map.MapInformation)) - { - _map.ChangeView(null, Core.ZoomValue, Core.Pitch, Core.Bearing); - } - } + public override Moving3dCamera Core => _core; public Vector3 GetViewCenterPosition() { - return Core.GetViewCenterPosition(); + return _core.GetViewCenterPosition(); } public void Zoom(MapInformation mapInformation, Vector3 point, float zoomAction) { - Core.Zoom(mapInformation, point, zoomAction); + _core.Zoom(mapInformation, point, zoomAction); } - + public void Zoom(MapInformation mapInformation, float zoomAction) { - Core.Zoom(mapInformation, GetViewCenterPosition(), zoomAction); + _core.Zoom(mapInformation, GetViewCenterPosition(), zoomAction); } } -} \ No newline at end of file +} diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs index eb3bf3bc5..4303dc47a 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs @@ -18,52 +18,65 @@ public enum RotationMode [Serializable] private class RotationSettings { + [Tooltip("Enable right-click drag to rotate the camera or map")] public bool Enabled = true; + [Tooltip("Rotation sensitivity. Higher values = faster rotation. Default: 50")] public float Speed = 50.0f; + [Tooltip("RotateTheCamera: camera orbits the map. RotateTheMap: map rotates under a fixed camera")] public RotationMode rotationMode; + [Tooltip("Root transform of the map. Auto-detected from MapBehaviourCore if left empty")] public Transform MapRoot; } - + [Serializable] private class ZoomSettings { + [Tooltip("Enable scroll wheel to zoom in/out")] public bool Enabled = true; + [Tooltip("Zoom sensitivity per scroll step. Default: 0.25")] public float Speed = 0.25f; + [Tooltip("When enabled, zoom centers on cursor position. When disabled, zoom centers on map center")] public bool ZoomAtCursor = true; } + [Tooltip("Initial map center in latitude/longitude. Example: 40.7128, -74.0060 for New York")] public LatitudeLongitude CenterLatitudeLongitude; + [Tooltip("Camera tilt angle. 90 = top-down, 15 = near-horizon")] [Range(15, 90)] public float Pitch; + [Tooltip("Camera rotation in degrees. 0 = north up")] [Range(-180, 180)] public float Bearing; - public float CameraDistance; + [Tooltip("Distance from ground target to camera along its forward axis. Higher = further away. Typical range: 100–1000 depending on scale")] + public float CameraDistance = 200f; [NonSerialized] public float ZoomValue; [NonSerialized] public float ScaleValue; private float _initialZoom; private float _initialScale; + [Tooltip("Enable left-click drag to pan the map")] public bool PanEnabled = true; - - // public bool RotateEnabled = true; - // public float RotationSpeed = 50.0f; + + [Tooltip("Camera rotation settings")] [SerializeField] private RotationSettings _rotationSettings; + [Tooltip("Zoom behaviour settings")] [SerializeField] private ZoomSettings _zoomSettings; private float _initialPitch; private Vector3 _previousScreenPosition; private Vector3 _dragOrigin; private Vector3 _targetPosition; - private Plane _controlPlane; - private float deltaAngleH; - private float deltaAngleV; - - public void Initialize(Camera camera, IMapInformation start, Plane? controlPlane = null) + public override void Initialize(Camera camera, IMapInformation start) { - _camera = camera; - _controlPlane = controlPlane ?? new Plane(Vector3.up, Vector3.zero); + Initialize(camera, start, null); + } + + public void Initialize(Camera camera, IMapInformation start, Plane? controlPlane) + { + base.Initialize(camera, start); + if (controlPlane.HasValue) _controlPlane = controlPlane.Value; if (_rotationSettings.MapRoot == null) { _rotationSettings.MapRoot = GameObject.FindObjectOfType().transform; @@ -79,80 +92,103 @@ public void Initialize(Camera camera, IMapInformation start, Plane? controlPlane start.SetView += SetCamera; } - public override bool UpdateCamera(IMapInformation mapInformation) + private const float MaxMercatorLatitude = 85.06f; + + public override CameraOutput UpdateCamera(IMapInformation mapInformation) { - var hasChanged = false; + _output.Reset(); + UpdateInputState(); + + if (IsPointerOverUI()) + return _output; + + var pointerPos = GetPointerPosition(); Vector3 cursorHit; Vector2d newMercatorCenter = mapInformation.CenterMercator; - - if (!GetPlaneIntersection(Input.mousePosition, out cursorHit)) - return false; - - if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) + + if (!GetPlaneIntersection(pointerPos, out cursorHit)) + return _output; + + if (GetPointerDown() || GetSecondaryDown()) { - _previousScreenPosition = Input.mousePosition; + _previousScreenPosition = pointerPos; _dragOrigin = cursorHit; } - - if (Input.GetMouseButton(0) && PanEnabled) - { - Vector3 pos = cursorHit - _dragOrigin; + if (GetPointerHeld() && PanEnabled) + { var oldCursorLatLng = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(_dragOrigin))); var newCursorLatLng = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(cursorHit))); newMercatorCenter = mapInformation.CenterMercator - (newCursorLatLng - oldCursorLatLng); - hasChanged = true; + _output.HasChanged = true; } - else if (Input.GetMouseButton(1) && _rotationSettings.Enabled) + else if (GetSecondaryHeld() && _rotationSettings.Enabled) { - var deltaMousePos = (Input.mousePosition - _previousScreenPosition); - deltaAngleH = deltaMousePos.x; - deltaAngleV = deltaMousePos.y; + var deltaMousePos = (pointerPos - _previousScreenPosition); + var deltaAngleH = deltaMousePos.x; + var deltaAngleV = deltaMousePos.y; if (deltaAngleH != 0 || deltaAngleV != 0) { var currentPitch = deltaAngleV * Time.deltaTime * _rotationSettings.Speed; Pitch -= currentPitch; - Pitch = Mathf.Min(90, Mathf.Max(15, Pitch)); + Pitch = ClampPitch(Pitch); var currentBearing = deltaAngleH * Time.deltaTime * _rotationSettings.Speed; - Bearing += currentBearing; - hasChanged = true; + Bearing = ClampBearing(Bearing + currentBearing); + _output.HasChanged = true; } } - else if (Input.mouseScrollDelta.magnitude > 0 && _zoomSettings.Enabled) + else if (GetPinchZoomDelta(out var zoomDelta) && _zoomSettings.Enabled) { + var zoomCenter = GetZoomCenter(); if (!_zoomSettings.ZoomAtCursor) { - var postZoom = ZoomValue + Input.GetAxis("Mouse ScrollWheel") * _zoomSettings.Speed; - ZoomValue = postZoom; + ZoomValue = ClampZoom(ZoomValue + zoomDelta * _zoomSettings.Speed); ScaleValue = _initialScale / Mathf.Pow(2, (ZoomValue - _initialZoom)); - hasChanged = true; + _output.HasChanged = true; } - else + else if (GetPlaneIntersection(zoomCenter, out var zoomHit)) { - var mouseMeter = cursorHit * ScaleValue; - var startingMercatorCursor = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(cursorHit))); - ZoomValue = ZoomValue + Input.GetAxis("Mouse ScrollWheel") * _zoomSettings.Speed; + var startingMercatorCursor = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(zoomHit))); + ZoomValue = ClampZoom(ZoomValue + zoomDelta * _zoomSettings.Speed); ScaleValue = _initialScale / Mathf.Pow(2, (ZoomValue - _initialZoom)); - var newMercatorCursor = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLngForScale(_rotationSettings.MapRoot.InverseTransformPoint(cursorHit), ScaleValue)); + var newMercatorCursor = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLngForScale(_rotationSettings.MapRoot.InverseTransformPoint(zoomHit), ScaleValue)); var change = newMercatorCursor - startingMercatorCursor; newMercatorCenter = mapInformation.CenterMercator - change; - hasChanged = true; + _output.HasChanged = true; } } + if (GetTwoFingerTiltDelta(out var tiltDelta)) + { + Pitch -= tiltDelta * _rotationSettings.Speed; + Pitch = ClampPitch(Pitch); + _output.HasChanged = true; + } + CenterLatitudeLongitude = Conversions.WebMercatorToLatLon(newMercatorCenter); + CenterLatitudeLongitude = new LatitudeLongitude( + Mathf.Clamp((float)CenterLatitudeLongitude.Latitude, -MaxMercatorLatitude, MaxMercatorLatitude), + CenterLatitudeLongitude.Longitude); _dragOrigin = cursorHit; - _previousScreenPosition = Input.mousePosition; + _previousScreenPosition = pointerPos; - if(hasChanged) SetCamPositionByMapInfo(); - - return hasChanged; + if (_output.HasChanged) + { + SetCamPositionByMapInfo(); + _output.Center = CenterLatitudeLongitude; + _output.Zoom = ZoomValue; + _output.Pitch = Pitch; + _output.Bearing = Bearing; + _output.Scale = ScaleValue; + } + + return _output; } private void SetCamPositionByMapInfo() { _camera.transform.position = _targetPosition; - + if (_rotationSettings.rotationMode == RotationMode.RotateTheCamera) { _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); @@ -161,11 +197,9 @@ private void SetCamPositionByMapInfo() { var projectedForward = Vector3.ProjectOnPlane(_camera.transform.forward, _controlPlane.normal); var verticalRotation = Quaternion.AngleAxis(Pitch-_initialPitch, projectedForward.Perpendicular()); - var vector = Quaternion.Euler(Pitch, Bearing, 0); - var worldVector = _camera.transform.rotation * vector; _rotationSettings.MapRoot.rotation = verticalRotation * Quaternion.Euler(0, Bearing, 0); } - + _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); } @@ -174,8 +208,7 @@ public void SetCamera(IMapInformation mapInfo) _targetPosition = Vector3.zero; Pitch = mapInfo.Pitch; Bearing = mapInfo.Bearing; - //SetCamPositionByMapInfo(mapInfo); - + if (_rotationSettings.rotationMode == RotationMode.RotateTheCamera) { SetCamPositionByMapInfo(); @@ -184,33 +217,9 @@ public void SetCamera(IMapInformation mapInfo) { var projectedForward = Vector3.ProjectOnPlane(_camera.transform.forward, _controlPlane.normal); var verticalRotation = Quaternion.AngleAxis(Pitch-_initialPitch, projectedForward.Perpendicular()); - var vector = Quaternion.Euler(Pitch, Bearing, 0); - var worldVector = _camera.transform.rotation * vector; _rotationSettings.MapRoot.rotation = verticalRotation * Quaternion.Euler(0, Bearing, 0); } } - private bool GetPlaneIntersection(Vector3 screenPosition, out Vector3 hit) - { - hit = Vector3.zero; - var ray = _camera.ScreenPointToRay(screenPosition); - if (_controlPlane.Raycast(ray, out var distance)) - { - hit = ray.GetPoint(distance); - return true; - } - - return false; - } - - public Plane[] GetFrustrumPlanes() - { - return GeometryUtility.CalculateFrustumPlanes(_camera); - } - - public Transform GetTransform() - { - return _camera.transform; - } } } \ No newline at end of file diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs index f6ae469dc..844729a03 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs @@ -1,35 +1,22 @@ using Mapbox.BaseModule.Map; -using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.Map; using UnityEngine; +using UnityEngine.Serialization; namespace Mapbox.Example.Scripts.MapInput { - public class SlippyMapCameraBehaviour : MonoBehaviour + public class SlippyMapCameraBehaviour : MapCameraBehaviour { - public MapBehaviourCore MapBehaviour; - public Camera Camera; - public SlippyMapCamera Core; + [Tooltip("Slippy map camera settings. Camera stays static while the map moves underneath")] + [FormerlySerializedAs("Core")] + [SerializeField] private SlippyMapCamera _core; - private MapboxMap _map; - private bool _isInitialized = false; - - private void Awake() - { - MapBehaviour.Initialized += (map) => - { - _map = map; - _isInitialized = true; - Core.Initialize(Camera, _map.MapInformation, new Plane(MapBehaviour.transform.up, MapBehaviour.transform.position)); - }; - } + public override SlippyMapCamera Core => _core; - public void Update() + protected override void OnMapInitialized(MapboxMap map) { - if (_isInitialized && _map.MapInformation != null && Core.UpdateCamera(_map.MapInformation)) - { - _map.ChangeView(Core.CenterLatitudeLongitude, Core.ZoomValue, Core.Pitch, Core.Bearing, Core.ScaleValue); - } + Map = map; + IsInitialized = true; + _core.Initialize(Camera, map.MapInformation, new Plane(MapBehaviour.transform.up, MapBehaviour.transform.position)); } } -} \ No newline at end of file +} From 46664b87c9292b839cf41003070a7e63f176efdb Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 10:01:31 +0300 Subject: [PATCH 13/54] Squashed commit for quad tree improvements commit 76af9ca5c34aa82af7a515651583dbcbc216f914 Author: Baran Kahyaoglu Date: Mon Apr 6 11:58:58 2026 +0300 improve unity tile provider script and mapinfo add terrain information to mapInfo fix tile visibility issues while improving quadtree system add min/max map heights to mapInfor terrain section commit c52f8069f5d663c6ca863f666c1cf7ea65767241 Author: Baran Kahyaoglu Date: Fri Mar 13 23:05:12 2026 +0300 fix the unity tile provider quad tree --- .../Mapbox/BaseModule/Map/IMapInformation.cs | 1 + .../Mapbox/BaseModule/Map/MapInformation.cs | 1 + .../BaseModule/Map/MapboxMapVisualizer.cs | 56 ++- Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs | 34 ++ .../Mapbox/BaseModule/Map/TerrainInfo.cs.meta | 11 + .../Mapbox/BaseModule/Unity/UnityMapTile.cs | 4 +- .../Unity/TerrainLayerModuleScript.cs | 1 + .../UnityMapService/DataSources/PbfSource.cs | 3 +- .../TileProvider/UnityTileProvider.cs | 378 +++++++++++------- .../Mapbox/VectorModule/VectorLayerModule.cs | 2 +- 10 files changed, 324 insertions(+), 167 deletions(-) create mode 100644 Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs create mode 100644 Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Map/IMapInformation.cs b/Runtime/Mapbox/BaseModule/Map/IMapInformation.cs index 4882b1adb..af7738e8e 100644 --- a/Runtime/Mapbox/BaseModule/Map/IMapInformation.cs +++ b/Runtime/Mapbox/BaseModule/Map/IMapInformation.cs @@ -22,6 +22,7 @@ public interface IMapInformation event Action LatitudeLongitudeChanged; event Action WorldScaleChanged; Func QueryElevation { get; set; } + TerrainInfo Terrain { get; } float GetLatitudeCompensationForLocation { get; } float GetScaleFor(float zoomValue); } diff --git a/Runtime/Mapbox/BaseModule/Map/MapInformation.cs b/Runtime/Mapbox/BaseModule/Map/MapInformation.cs index 8f190e3d6..20095f91b 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapInformation.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapInformation.cs @@ -117,6 +117,7 @@ public virtual void SetInformation(LatitudeLongitude? latlng, float? zoom = null } public Func QueryElevation { get; set; } + public TerrainInfo Terrain { get; } = new TerrainInfo(); public event Action SetView = (t) => {}; public event Action ViewChanged = (t) => {}; public event Action LatitudeLongitudeChanged = (t) => {}; diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 252613c76..9f0a72579 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -28,6 +28,9 @@ public class MapboxMapVisualizer : IMapVisualizer private Coroutine _internalUpdateCoroutine; private bool _destroyed; + // Reusable scratch array to avoid per-frame allocation in InternalUpdateCoroutine + private readonly UnwrappedTileId[] _quadrants = new UnwrappedTileId[4]; + public MapboxMapVisualizer(IMapInformation mapInformation, UnityContext unityContext, ITileCreator tileCreator) { _unityContext = unityContext; @@ -90,6 +93,21 @@ public virtual void Load(TileCover tileCover) { RemoveUnnecessaryTiles(tileCover); + // Protect filler children of temp tiles that are still loading. + // Without this, fillers get pooled one frame after creation (DelveInto only + // rescues them on the frame the temp tile is first added). + for (var i = TempTiles.Count - 1; i >= 0; i--) + { + var tempTile = TempTiles[i]; + if (tempTile.Children != null && ActiveTiles.ContainsKey(tempTile.UnwrappedTileId)) + { + foreach (var child in tempTile.Children) + { + _toRemove.Remove(child.UnwrappedTileId); + } + } + } + foreach (var tileId in tileCover.Tiles) { if (ActiveTiles.ContainsKey(tileId)) @@ -138,13 +156,13 @@ public virtual void Load(TileCover tileCover) { _retainedTiles.Add(tile.Canonical); } + foreach (var visualization in LayerModules) { visualization.RetainTiles(_retainedTiles); } } - /// /// Create the map in given tileCover area. Makes decision to load or unload tiles and handle temporary filler /// tiles until actual tiles are loaded. @@ -177,19 +195,15 @@ public virtual void InternalUpdateCoroutine() TempTiles.RemoveAt(index); - var quadrants = new UnwrappedTileId[4] - { - tilePair.UnwrappedTileId.Quadrant(0), - tilePair.UnwrappedTileId.Quadrant(1), - tilePair.UnwrappedTileId.Quadrant(2), - tilePair.UnwrappedTileId.Quadrant(3), - }; + _quadrants[0] = tilePair.UnwrappedTileId.Quadrant(0); + _quadrants[1] = tilePair.UnwrappedTileId.Quadrant(1); + _quadrants[2] = tilePair.UnwrappedTileId.Quadrant(2); + _quadrants[3] = tilePair.UnwrappedTileId.Quadrant(3); for (int i = 0; i < 4; i++) { - var quadrant = quadrants[i]; - if (ActiveTiles.TryGetValue(quadrant, out var unityMapTile)) + if (ActiveTiles.TryGetValue(_quadrants[i], out _)) { - _toRemove.Add(quadrant); + _toRemove.Add(_quadrants[i]); } } } @@ -222,6 +236,9 @@ public void LoadSnapshot(TileCover tileCover) { foreach (var tileId in tileCover.Tiles) { + if (ActiveTiles.ContainsKey(tileId)) + continue; + UnityMapTile unityMapTile = null; if (!CreateTileInstant(tileId, out unityMapTile)) //if we can't fully load the tile { @@ -287,7 +304,7 @@ private void RemoveUnnecessaryTiles(TileCover tileCover) _toRemove.Clear(); foreach (var tilePair in ActiveTiles) { - if (!tileCover.Tiles.Contains(tilePair.Key) && tilePair.Value.LoadingState != LoadingState.Filler) + if (!tileCover.Tiles.Contains(tilePair.Key)) { _toRemove.Add(tilePair.Key); } @@ -297,7 +314,7 @@ private void RemoveUnnecessaryTiles(TileCover tileCover) protected bool DelveInto(UnwrappedTileId tileId, List activeChildren, int recursiveDepth = 3) { - var quadrantCheck = new bool[4] { false, false, false, false }; + var quadrantCheck = new bool[4]; var quadrants = new UnwrappedTileId[4] { tileId.Quadrant(0), @@ -321,7 +338,7 @@ protected bool DelveInto(UnwrappedTileId tileId, List activeChildr } unityMapTile.Children.Clear(); } - + ShowTile(unityMapTile); quadrantCheck[i] = true; } @@ -338,7 +355,6 @@ protected bool DelveInto(UnwrappedTileId tileId, List activeChildr } } - //if (quadrantCheck.Any(x => x)) if (quadrantCheck[0] || quadrantCheck[1] || quadrantCheck[2] || quadrantCheck[3]) { for (int i = 0; i < 4; i++) @@ -448,6 +464,16 @@ protected bool CreateTile(UnityMapTile unityMapTile) ActiveTiles.Add(unityMapTile.UnwrappedTileId, unityMapTile); } + var terrainData = unityMapTile.TerrainContainer?.TerrainData; + if (terrainData != null && terrainData.IsElevationDataReady) + { + var terrain = _mapInformation.Terrain; + if (terrainData.MaxElevation > terrain.MaxElevation) + terrain.MaxElevation = terrainData.MaxElevation; + if (terrainData.MinElevation < terrain.MinElevation) + terrain.MinElevation = terrainData.MinElevation; + } + TileLoaded(unityMapTile); } diff --git a/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs new file mode 100644 index 000000000..4edcb3a8e --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs @@ -0,0 +1,34 @@ +using System; + +namespace Mapbox.BaseModule.Map +{ + /// + /// Runtime terrain elevation information for the current map view. + /// Updated automatically as terrain tiles load. Provides observed elevation + /// range and settings useful for styling, object placement, and culling. + /// + [Serializable] + public class TerrainInfo + { + /// + /// Whether terrain elevation is enabled. False when using flat terrain strategy. + /// + public bool IsEnabled; + + /// + /// Lowest observed elevation in meters across all loaded terrain tiles. + /// + public float MinElevation; + + /// + /// Highest observed elevation in meters across all loaded terrain tiles. + /// + public float MaxElevation; + + /// + /// Vertical exaggeration factor applied to terrain elevation. + /// 1.0 = real-world scale. Set by the terrain module during initialization. + /// + public float Exaggeration = 1f; + } +} diff --git a/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs.meta b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs.meta new file mode 100644 index 000000000..a0385a724 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5415e7100ee884a8f9e172b14483e543 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs index 796223e1f..e04a59ae9 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs @@ -20,7 +20,7 @@ public class UnityMapTile : MonoBehaviour public UnityTileImageContainer ImageContainer; public UnityTileVectorContainer VectorContainer; - private string _tileScaleFieldNameID = "_TileScale"; + private static readonly int TileScalePropertyId = Shader.PropertyToID("_TileScale"); private MeshRenderer _meshRenderer; public MeshRenderer MeshRenderer => _meshRenderer; @@ -56,7 +56,7 @@ public void Initialize(UnwrappedTileId tileId, float scale) gameObject.name = tileId.ToString(); #endif - Material.SetFloat(_tileScaleFieldNameID, TileScale); + Material.SetFloat(TileScalePropertyId, TileScale); } public void ElevationUpdatedCallback() diff --git a/Runtime/Mapbox/ImageModule/Unity/TerrainLayerModuleScript.cs b/Runtime/Mapbox/ImageModule/Unity/TerrainLayerModuleScript.cs index a5d8ef6d1..5f5b93268 100644 --- a/Runtime/Mapbox/ImageModule/Unity/TerrainLayerModuleScript.cs +++ b/Runtime/Mapbox/ImageModule/Unity/TerrainLayerModuleScript.cs @@ -46,6 +46,7 @@ public override ILayerModule ConstructModule(MapService service, IMapInformation Settings); mapInformation.QueryElevation = module.QueryElevation; + mapInformation.Terrain.IsEnabled = true; ModuleImplementation = module; return ModuleImplementation; } diff --git a/Runtime/Mapbox/UnityMapService/DataSources/PbfSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/PbfSource.cs index ce44db27f..1f5f30d4b 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/PbfSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/PbfSource.cs @@ -84,7 +84,8 @@ public override void CancelActiveRequests(CanonicalTileId unityTileId) if (_waitingList.ContainsKey(unityTileId)) { var tile = _waitingList[unityTileId]; - CancelFetching(tile, _tilesetId); + if (tile != null) + CancelFetching(tile, _tilesetId); _waitingList.Remove(unityTileId); } } diff --git a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs index 71b8797f2..f87dbfe19 100644 --- a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs +++ b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs @@ -12,10 +12,48 @@ namespace Mapbox.UnityMapService.TileProviders [Serializable] public class UnityTileProviderSettings { + /// + /// Camera used for frustum culling and LOD calculations. + /// Falls back to Camera.main if not set. + /// + [Tooltip("Camera used for frustum culling and LOD calculations. Falls back to Camera.main if not set.")] public Camera Camera; + + /// + /// Tiles below this zoom level are always subdivided without distance checks. + /// Higher values force more subdivisions, producing more tiles to fill the view. + /// Lower values improve performance. Default of 2 works well for most cases. + /// + [Tooltip("Tiles below this zoom are always subdivided regardless of distance. Lower values improve performance. Default 2 works for most cases.")] public float MinimumZoomLevel = 2; + + /// + /// Maximum zoom level the quadtree will subdivide to, clamped by the map's AbsoluteZoom. + /// Higher values produce finer detail near the camera but cost more to render. + /// Lower values improve performance at the expense of close-up detail. + /// Typical range is 14-18 for most applications. + /// + [Tooltip("Maximum tile detail level. Higher = sharper close-up detail but more tiles to render. Typical range 14-18. Default 22.")] public float MaximumZoomLevel = 22; + /// + /// Controls how aggressively tiles subdivide with distance. + /// Higher values produce more tiles with finer detail at distance but cost more to render. + /// Lower values improve performance with coarser detail at distance. + /// Values below 0.5 may cause visible quality loss. Default 1.0. + /// + [Tooltip("Tile detail vs. performance. Higher = more detail at distance, lower = better performance. Below 0.5 may reduce quality. Default 1.0.")] + public float SubdivisionBias = 1.0f; + + /// + /// Expands the 4 side frustum planes outward by this distance (world units). + /// Pre-loads tiles just outside the screen edges to prevent pop-in during camera pans. + /// Higher values give smoother panning but load more off-screen tiles. + /// Does not affect near/far planes. Typical range 0-50. Set to 0 to disable. + /// + [Tooltip("Pre-loads tiles beyond screen edges to reduce pop-in during panning. Higher = smoother but more tiles loaded. Typical range 0-50. Default 0.")] + public float FrustumBuffer = 0f; + public UnityTileProviderSettings(Camera cam, float minZoom = 2, float maxZoom = 22) { Camera = cam; @@ -23,151 +61,195 @@ public UnityTileProviderSettings(Camera cam, float minZoom = 2, float maxZoom = MaximumZoomLevel = maxZoom; } } - - public class UnityTileProvider : TileProvider - { - public UnityTileProviderSettings Settings; - private float _quadMaxLevel; - private Plane[] _planes = new Plane[6]; - private Stack _stack; - private List _rectpool; - private int _rectIndex = 0; - - public UnityTileProvider(UnityTileProviderSettings settings) - { - Settings = settings; - - if(Settings.Camera == null) Settings.Camera = Camera.main; - - _rectpool = new List(200); - for (int i = 0; i < 200; i++) - { - _rectpool.Add(new UnityRectD()); - } - _stack = new Stack(200); - } - - private UnityRectD GetRectD() - { - if(_rectIndex >= _rectpool.Count) - _rectpool.Add(new UnityRectD()); - var value = _rectpool[_rectIndex]; - _rectIndex++; - return value; - } - - public override bool GetTileCover(IMapInformation mapInformation, TileCover tileCover) - { - _rectIndex = 0; - tileCover.Tiles.Clear(); - _quadMaxLevel = Mathf.Min(Settings.MaximumZoomLevel, mapInformation.AbsoluteZoom); - GeometryUtility.CalculateFrustumPlanes(Settings.Camera, _planes); - var worldBaseBounds = GetRectD(); - worldBaseBounds.Set(new UnwrappedTileId(0, 0, 0), -mapInformation.CenterMercator, mapInformation.Scale, 0); - _stack.Clear(); - _stack.Push(worldBaseBounds); - while (_stack.Count > 0) - { - var tile = _stack.Pop(); - if (!GeometryUtility.TestPlanesAABB(_planes, tile.UnityBounds)) - { - continue; - } - - if (tile.Id.Z == _quadMaxLevel || !ShouldSplit(tile.Id.Z, tile.UnityBounds.size.z, tile.UnityBounds.SqrDistance(Settings.Camera.transform.position))) - { - tileCover.Tiles.Add(tile.Id); - continue; - } - - for (var i = 0; i < 4; i++) - { - var child = tile.Quadrant(GetRectD(), i); - _stack.Push(child); - } - } - - return true; - } - - private bool ShouldSplit(int zoom, float sizeX, float dist) - { - if (zoom < Settings.MinimumZoomLevel) - { - return true; - } - else if (zoom == _quadMaxLevel) - { - return false; - } - - return dist < Mathf.Pow(sizeX , 2); - } - - private struct UnityRectD - { - public UnwrappedTileId Id; - public Bounds UnityBounds; - - private Vector2d _offset; - private float _worldScale; - private float _currentElevationSample; - const int TileSize = 256; - - public void Set(UnwrappedTileId id, Vector2d vector2d, float worldScale, float unityBoundHeight = 1) - { - _currentElevationSample = unityBoundHeight; - _offset = vector2d; - _worldScale = worldScale; - Id = id; - - var min = Conversions.PixelsToMeters( - Id.X * TileSize, - Id.Y * TileSize, - Id.Z); - var max = Conversions.PixelsToMeters( - (Id.X + 1) * TileSize, - (Id.Y + 1) * TileSize, - Id.Z); - - UnityFlatSpaceCalculations( - (float)min.x + vector2d.x, - (float)min.y + vector2d.y, - (float)(max.x - min.x), - worldScale, _currentElevationSample); - } - - private void UnityFlatSpaceCalculations(double boundX, double boundY, double boundZ, float worldScale, float boundHeight) - { - //var boundsTopLeft = bounds.TopLeft; - var topleftX = (float) (boundX / worldScale); - var toplefty = (float) (boundY / worldScale); - var boundsSize = (float)(boundZ / worldScale); - - UnityBounds = new Bounds( - new Vector3(topleftX + boundsSize / 2, boundHeight/2, toplefty - boundsSize / 2), - new Vector3( - boundsSize, - boundHeight, - boundsSize)); - } - - public UnityRectD Quadrant(UnityRectD rectD, int i) - { - var childX = (Id.X << 1) + (i % 2); - var childY = (Id.Y << 1) + (i >> 1); - - rectD.Set(new UnwrappedTileId(Id.Z + 1, childX, childY), _offset, _worldScale, _currentElevationSample); - return rectD; - } - - public UnwrappedTileId QuadrantTileId(int i) - { - var childX = (Id.X << 1) + (i % 2); - var childY = (Id.Y << 1) + (i >> 1); - - return new UnwrappedTileId(Id.Z + 1, childX, childY); - } - } - } -} \ No newline at end of file + + public class UnityTileProvider : TileProvider + { + public UnityTileProviderSettings Settings; + protected int _maxZoom; + protected readonly Plane[] _planes = new Plane[6]; + protected TileNode[] _pool; + protected int _poolCount; + protected readonly Stack _stack; + + // Reusable array for 4 bottom-plane corners to avoid per-frame allocation + protected readonly Vector3[] _corners = new Vector3[4]; + + public UnityTileProvider(UnityTileProviderSettings settings) + { + Settings = settings; + + if (Settings.Camera == null) Settings.Camera = Camera.main; + + _pool = new TileNode[256]; + _stack = new Stack(256); + } + + protected int AllocNode() + { + if (_poolCount >= _pool.Length) + { + var newPool = new TileNode[_pool.Length * 2]; + Array.Copy(_pool, newPool, _pool.Length); + _pool = newPool; + } + + return _poolCount++; + } + + public override bool GetTileCover(IMapInformation mapInformation, TileCover tileCover) + { + _poolCount = 0; + _stack.Clear(); + tileCover.Tiles.Clear(); + + _maxZoom = (int)Mathf.Min(Settings.MaximumZoomLevel, mapInformation.AbsoluteZoom); + var cam = Settings.Camera; + GeometryUtility.CalculateFrustumPlanes(cam, _planes); + + var camPos = cam.transform.position; + var camForward = cam.transform.forward; + var maxDistSq = cam.farClipPlane * cam.farClipPlane; + + // Expand the 4 side frustum planes (indices 0-3) outward for buffer tile pre-loading. + // Near (4) and far (5) planes are left unchanged. + if (Settings.FrustumBuffer > 0f) + { + for (int i = 0; i < 4; i++) + _planes[i].distance += Settings.FrustumBuffer; + } + var worldCenter = mapInformation.CenterMercator; + var scale = mapInformation.Scale; + + // Bounds height for frustum culling: derived from observed terrain elevation. + // MaxElevation is in meters, divide by scale to get world units. + // Floor of 0.1 ensures tiles are never zero-height (invisible to frustum test). + var boundsHeight = Mathf.Max(mapInformation.Terrain.MaxElevation / scale, 0.1f); + + // Camera height above the map plane (y=0) + var cameraHeight = camPos.y; + + // zoomSplitDistance: the world-space distance at which a single maxZoom tile + // subtends half the screen height. Derived from the screen-fraction threshold: + // tileSize / dist / (2 * tan(fov/2)) > 0.5 → dist < tileSize / tan(fov/2) + // For a tile at zoom z: distToSplit = (1 << (maxZoom - z)) * zoomSplitDistance + var halfFovTan = Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad); + var maxZoomTileSize = Conversions.TileSizeInUnitySpace(_maxZoom, scale); + var zoomSplitDistance = maxZoomTileSize / halfFovTan; + + // push root tile + var rootIdx = AllocNode(); + _pool[rootIdx].Set(new UnwrappedTileId(0, 0, 0), worldCenter, scale, boundsHeight); + _stack.Push(rootIdx); + + while (_stack.Count > 0) + { + var idx = _stack.Pop(); + ref var node = ref _pool[idx]; + + if (node.Bounds.SqrDistance(camPos) > maxDistSq) + continue; + + if (!GeometryUtility.TestPlanesAABB(_planes, node.Bounds)) + continue; + + var zoom = node.Id.Z; + + if (zoom >= _maxZoom || !ShouldSplit(zoom, node.Bounds, camPos, camForward, cameraHeight, zoomSplitDistance)) + { + tileCover.Tiles.Add(node.Id); + continue; + } + + // subdivide into 4 children + for (int i = 0; i < 4; i++) + { + var childX = (node.Id.X << 1) + (i & 1); + var childY = (node.Id.Y << 1) + (i >> 1); + var childIdx = AllocNode(); + _pool[childIdx].Set( + new UnwrappedTileId(zoom + 1, childX, childY), + worldCenter, scale, boundsHeight); + _stack.Push(childIdx); + } + } + + return true; + } + + protected bool ShouldSplit(int zoom, Bounds bounds, Vector3 camPos, + Vector3 camForward, float cameraHeight, float zoomSplitDistance) + { + if (zoom < Settings.MinimumZoomLevel) + return true; + + // Find closest forward-projected distance across the 4 bottom-plane corners. + // On a flat map, the closest projected corner is always on the bottom plane. + var min = bounds.min; + var max = bounds.max; + var bottomY = min.y; + _corners[0] = new Vector3(min.x, bottomY, min.z); + _corners[1] = new Vector3(max.x, bottomY, min.z); + _corners[2] = new Vector3(min.x, bottomY, max.z); + _corners[3] = new Vector3(max.x, bottomY, max.z); + + var closestDist = float.MaxValue; + for (int i = 0; i < 4; i++) + { + var offset = _corners[i] - camPos; + // Use camera height as vertical component (flat map, matching native SDK) + offset.y = cameraHeight; + var projDist = Vector3.Dot(offset, camForward); + if (projDist <= 0f) + return true; // corner behind camera plane — always split + if (projDist < closestDist) + closestDist = projDist; + } + + // Exponential distance threshold: doubles for each zoom level below maxZoom + var distToSplit = (1 << (_maxZoom - zoom)) * zoomSplitDistance * Settings.SubdivisionBias; + + // Acute angle compensation: reduce detail for tiles viewed at grazing angles + distToSplit *= DistToSplitScale(cameraHeight, closestDist); + + return closestDist < distToSplit; + } + + /// + /// Scales the split distance for tiles viewed at acute angles (high pitch / near horizon). + /// When the angle between the camera-to-tile ray and the tile plane drops below 45 degrees, + /// progressively increases the split distance using a geometric series, making it harder + /// for distant foreshortened tiles to subdivide. + /// Ported from native SDK tile_cover.cpp distToSplitScale (lines 263-285). + /// + private static float DistToSplitScale(float dz, float d) + { + const float kAcuteAngleThresholdSin = 0.707f; // sin(45 degrees) + const float kStretchTile = 1.1f; + + if (d * kAcuteAngleThresholdSin < dz) + return 1f; + + var r = d / dz; + var k = r - 1f / kAcuteAngleThresholdSin; + return r / (1f / kAcuteAngleThresholdSin + + (Mathf.Pow(kStretchTile, k + 1f) - 1f) / (kStretchTile - 1f) - 1f); + } + + protected struct TileNode + { + public UnwrappedTileId Id; + public Bounds Bounds; + + public void Set(UnwrappedTileId id, Vector2d worldCenter, float scale, float boundsHeight) + { + Id = id; + var rect = Conversions.TileBoundsInUnitySpace(id, worldCenter, scale); + var sizeX = (float)rect.Size.x; + + Bounds = new Bounds( + new Vector3((float)rect.Center.x, boundsHeight * 0.5f, (float)rect.Center.y), + new Vector3(sizeX, boundsHeight, Mathf.Abs(sizeX))); + } + } + } +} diff --git a/Runtime/Mapbox/VectorModule/VectorLayerModule.cs b/Runtime/Mapbox/VectorModule/VectorLayerModule.cs index bed78ea27..887990fc3 100644 --- a/Runtime/Mapbox/VectorModule/VectorLayerModule.cs +++ b/Runtime/Mapbox/VectorModule/VectorLayerModule.cs @@ -271,7 +271,7 @@ public virtual IEnumerator LoadTiles(IEnumerable tiles) public IEnumerable GetTileCoverCoroutines(IEnumerable tiles) { var targetTiles = tiles.Where(x => IsZinSupportedRange(x.Z)).Select(GetTargetTileId).Distinct(); - return targetTiles.Select(x => LoadAndProcessTileCoroutine(x)); + return targetTiles.Select(LoadAndProcessTileCoroutine); } #endregion From 459fdabc676f735493e231120378156ffef2e896 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 10:06:08 +0300 Subject: [PATCH 14/54] Squashed commit for terrain improvements commit 51deba631aa18997ede8dd0a37e1f27294450950 Author: Baran Kahyaoglu Date: Wed May 6 17:43:47 2026 +0300 fix shader normal calculation change normal calculation step from 1 to 2 for smoother shading commit 7fc6a816341069e535cdaea47ec674ba6f45d25b Author: Baran Kahyaoglu Date: Mon May 4 22:12:21 2026 +0300 fix assembly defs and add burst commit 68fcced6dcaf656999cb10163a7546dd3b814465 Author: Baran Kahyaoglu Date: Fri Apr 24 20:07:47 2026 +0300 fix custom terrain layer module to use its settings commit 483e8a89b5bda900c447fdcbabfd42e447eb32a0 Author: Baran Kahyaoglu Date: Fri Apr 24 19:19:27 2026 +0300 Squashed commit of the following: commit bf509a1358578bbf064a85ee308f566c4a51c792 Author: Baran Kahyaoglu Date: Thu Apr 23 15:14:22 2026 +0300 custom terrain improvements fix a bug where non-invert Y didn't work add mapbox service checkbox for token addition commit 2e7bd3e5500e20ecddb748f4a07f85b32c58f163 Author: Baran Kahyaoglu Date: Fri Apr 24 16:52:05 2026 +0300 burst-compile terrain-rgb decode and collider mesh build add com.unity.burst 1.8.12 as an SDK dependency move SyncExtractElevationArray / AsyncExtractElevationArray decode into [BurstCompile] IJobs run via .Run() on the main thread accumulate min/max elevation inline in the decode job to skip the C# second pass move ElevatedTerrainStrategy's collider vertex fill into a Burst IJob with persistent NativeArray buffers cache the elevation NativeArray mirror per data tile so the 16 render tiles sharing it pay the copy only once generate collider triangles once per sampleCount change (they don't depend on elevation) instead of per tile build upload collider geometry via Mesh.SetVertices(NativeArray) + Mesh.SetIndices(NativeArray) for zero-copy dispose the persistent NativeArrays in the strategy's OnDestroy cascade commit 1e1ab7e20824de6e27be825d6d07246d1fdae4c3 Author: Baran Kahyaoglu Date: Fri Apr 24 14:59:09 2026 +0300 bilinear elevation sampling to fix cpu terrain stepping QueryHeightData, TerrainData.ReadElevation, and the inlined collider build loop all did nearest-neighbor via (int)xx truncation. When the mesh vertex grid is denser than the data texture's sub-region (routine: a render tile's 129-vertex mesh samples a ~64x64 sub-region of the parent data tile), multiple verts land in the same texel and produce visible stepping. Converted all three sites to 4-tap bilinear. commit 45e0ed5b3c764275eca8ccfe89056995736ece19 Author: Baran Kahyaoglu Date: Fri Apr 24 12:31:35 2026 +0300 terrain perf pass: shared mesh, MPB, pooled elevation share a single flat Mesh across render tiles in shader mode (one Mesh per pool instead of N) use MeshRenderer.localBounds instead of MeshFilter.mesh.bounds to stop Unity's silent Mesh cloning switch per-tile Material instancing to sharedMaterial + MaterialPropertyBlock so the SRP Batcher can merge tile draws pool TerrainData.ElevationValues float[] across cache evictions via new ElevationArrayPool skip collider rebuild when (data, tileId) is unchanged across temp->final transitions replace Linq in GetDataId(IEnumerable) with a pooled HashSet lift width/scale-offset math out of the collider sampling inner loop add TerrainStrategy.OnDestroy cascaded from TerrainLayerModule for shared-mesh disposal Co-Authored-By: Claude Opus 4.7 (1M context) commit 595bca334ba280343136217e9c44d3cc95bb5dd5 Author: Baran Kahyaoglu Date: Fri Apr 24 10:16:57 2026 +0300 fix the issue with cpu terrain normals commit 6dc0bf907e2af38a28e207f2635b78459cac4cbb Author: Baran Kahyaoglu Date: Fri Apr 24 00:47:44 2026 +0300 add async terrain collider baking commit a14660cc17df0b9bf28c19387c7d77301ab7847f Author: Baran Kahyaoglu Date: Fri Apr 24 00:31:52 2026 +0300 change fallback max elevation from setting to static commit 88d389dca6a3599293209e179c6c5c87eaa53646 Author: Baran Kahyaoglu Date: Fri Apr 24 00:25:53 2026 +0300 terrain improvements add per-tile MeshCollider generation with render-matched grid, plus optional child GameObject for a dedicated collider layer add ExtractCpuElevationData opt-out to skip terrain-RGB decode in shader-only apps add FallbackMaxElevationMeters so shader-displaced verts stay inside the cull volume add SimplificationFactor preset dropdown + live help box (old slider had many dead values) add Unity Layer dropdown for layerId via [GameObjectLayer] fix skirt outer offset overlapping neighbor tiles at coarse SimplificationFactor fix TerrainData.ElevationValuesUpdated silently wiping subscribers (convert to multicast event) fix DisableTerrain leaving stale TerrainData + subscription fix MeshCollider auto-assignment overwriting the render mesh fix Mesh leak by destroying render + collider Meshes in UnityMapTile.OnDestroy optimize terrain-RGB extraction: int loop vars, drop dead math, zero-copy NativeArray tune MeshCollider.cookingOptions and pool collider arrays for faster tile churn --- .../Data/DataFetchers/ElevationArrayPool.cs | 58 ++ .../DataFetchers/ElevationArrayPool.cs.meta | 11 + .../Data/DataFetchers/TerrainData.cs | 79 ++- .../ElevatedTerrainStrategy.cs | 537 +++++++++++++++++- .../Settings/ElevationLayerProperties.cs | 20 +- .../Settings/ElevationModificationOptions.cs | 16 +- .../Settings/SimplificationFactorAttribute.cs | 33 ++ .../SimplificationFactorAttribute.cs.meta | 11 + .../Settings/TerrainColliderOptions.cs | 23 +- .../Settings/TerrainSideWallOptions.cs | 12 +- .../Settings/UnityLayerOptions.cs | 13 +- .../Data/TerrainStrategies/TerrainStrategy.cs | 9 + .../BaseModule/Map/ImageSourceSettings.cs | 4 +- Runtime/Mapbox/BaseModule/Map/TileCreator.cs | 22 +- .../Mapbox/BaseModule/MapboxBaseModule.asmdef | 3 +- .../Mapbox/BaseModule/Unity/UnityMapTile.cs | 89 ++- .../Unity/UnityTileImageContainer.cs | 14 +- .../Unity/UnityTileTerrainContainer.cs | 131 +++-- .../Shaders/ElevatedTerrainShader.shadergraph | 280 +-------- ...pboxTerrainBilinearSampling.shadersubgraph | 242 +------- .../Attributes/GameObjectLayerAttribute.cs | 0 .../GameObjectLayerAttribute.cs.meta | 0 .../Utilities/FlatTerrainStrategy.cs | 46 +- .../CustomImageryModule/CustomSource.cs | 9 +- .../CustomSourceSettings.cs | 2 + .../CustomImageryModule/CustomTMSTile.cs | 16 +- .../CustomTerrainLayerModuleScript.cs | 11 +- .../CustomTerrainSource.cs | 17 +- .../CustomTerrainLayerModuleScriptEditor.cs | 34 ++ ...stomTerrainLayerModuleScriptEditor.cs.meta | 11 + .../MapboxCustomImageryModule.Editor.asmdef | 3 +- Runtime/Mapbox/ImageModule/Editor.meta | 8 + .../Editor/MapboxImageryModule.Editor.asmdef | 19 + .../MapboxImageryModule.Editor.asmdef.meta | 7 + .../Editor/SimplificationFactorDrawer.cs | 108 ++++ .../Editor/SimplificationFactorDrawer.cs.meta | 11 + .../Editor/TerrainColliderOptionsDrawer.cs | 75 +++ .../TerrainColliderOptionsDrawer.cs.meta | 11 + .../Editor/TerrainLayerModuleScriptEditor.cs | 38 ++ .../TerrainLayerModuleScriptEditor.cs.meta | 11 + .../Editor/TerrainSettingsInspectorHelper.cs | 138 +++++ .../TerrainSettingsInspectorHelper.cs.meta | 11 + .../Editor/TerrainSideWallOptionsDrawer.cs | 62 ++ .../TerrainSideWallOptionsDrawer.cs.meta | 11 + .../Editor/UnityLayerOptionsDrawer.cs | 64 +++ .../Editor/UnityLayerOptionsDrawer.cs.meta | 11 + .../ImageModule/MapboxImageModule.asmdef | 3 +- .../ImageModule/Terrain/TerrainLayerModule.cs | 72 ++- .../Terrain/TerrainLayerModuleSettings.cs | 33 +- .../AsyncExtractElevationArray.cs | 162 +++--- .../DataSources/TerrainSource.cs | 26 +- .../SyncExtractElevationArray.cs | 163 +++--- .../UnityMapService/UnityMapModule.asmdef | 3 +- package.json | 3 +- 54 files changed, 2010 insertions(+), 796 deletions(-) create mode 100644 Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs create mode 100644 Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs.meta create mode 100644 Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs create mode 100644 Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs.meta rename Runtime/Mapbox/{VectorModule/Utility => BaseModule/Utilities}/Attributes/GameObjectLayerAttribute.cs (100%) rename Runtime/Mapbox/{VectorModule/Utility => BaseModule/Utilities}/Attributes/GameObjectLayerAttribute.cs.meta (100%) create mode 100644 Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs create mode 100644 Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs.meta create mode 100644 Runtime/Mapbox/ImageModule/Editor.meta create mode 100644 Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef create mode 100644 Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef.meta create mode 100644 Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs create mode 100644 Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs.meta create mode 100644 Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs create mode 100644 Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs.meta create mode 100644 Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs create mode 100644 Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs.meta create mode 100644 Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs create mode 100644 Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs.meta create mode 100644 Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs create mode 100644 Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs.meta create mode 100644 Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs create mode 100644 Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs new file mode 100644 index 000000000..2ee02a7f6 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace Mapbox.BaseModule.Data.DataFetchers +{ + /// + /// Size-keyed pool of float[] buffers used by terrain-RGB elevation extraction. + /// Cuts the ~256 KB allocation per data tile decode down to a one-time cost per + /// resolution: tiles that drop out of the memory cache return their backing array + /// here, and the next decode rents it back instead of allocating fresh. + /// + /// + /// Thread-safe — async GPU readback callbacks fire on the main thread but the contract + /// is conservative in case Unity changes that. Pool grows unbounded; in practice the + /// only sizes seen are 256×256 (default texture) and 512×512 (retina). + /// + public static class ElevationArrayPool + { + private static readonly Dictionary> _pools = new Dictionary>(); + + /// + /// Returns a float[] of exactly entries, either from + /// the pool or freshly allocated. Contents are undefined; the caller is expected to + /// overwrite every index. + /// + public static float[] Rent(int size) + { + lock (_pools) + { + if (_pools.TryGetValue(size, out var stack) && stack.Count > 0) + { + return stack.Pop(); + } + } + return new float[size]; + } + + /// + /// Returns to the pool. No-op if null. The caller must not + /// hold any references to after calling. + /// + public static void Return(float[] array) + { + if (array == null) + { + return; + } + lock (_pools) + { + if (!_pools.TryGetValue(array.Length, out var stack)) + { + stack = new Stack(); + _pools[array.Length] = stack; + } + stack.Push(array); + } + } + } +} diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs.meta b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs.meta new file mode 100644 index 000000000..00ded1549 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aabd60e0ea4fd4582ad15456847dcabe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs index 5e620ef53..113a083b9 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs @@ -9,21 +9,51 @@ public class TerrainData : RasterData { [HideInInspector] public float[] ElevationValues; public bool IsElevationDataReady = false; - public Action ElevationValuesUpdated; + + /// + /// Fires whenever or + /// completes. Multiple + /// subscribers can safely attach with += and detach with -=; the + /// former single-setter SetElevationChangedCallback method silently wiped + /// other subscribers, so it was removed. + /// + public event Action ElevationValuesUpdated; + public float MinElevation = 0; public float MaxElevation = 0; - public void SetElevationChangedCallback(Action callback) - { - ElevationValuesUpdated = callback; - } - + /// + /// True as soon as the raster has been assigned, + /// regardless of whether the per-pixel float[] has been decoded yet. Shader + /// elevation rendering only needs the texture and can show a tile the moment this + /// flips true; CPU consumers (collider builder, QueryElevation APIs, CPU elevation + /// mode) should wait for instead. + /// + public bool IsTextureReady => Texture != null; + public override void Clear() { base.Clear(); IsElevationDataReady = false; } + /// + /// Returns the rented array to + /// when the cache evicts this entry, then runs + /// the standard dispose callback. After this call + /// ElevationValues is null; nothing should hold a reference past dispose. + /// + public override void Dispose() + { + if (ElevationValues != null) + { + ElevationArrayPool.Return(ElevationValues); + ElevationValues = null; + } + IsElevationDataReady = false; + base.Dispose(); + } + public void SetElevationValues(float[] elevationArray) { ElevationValues = elevationArray; @@ -63,21 +93,32 @@ public float QueryHeightData(float x, float y) private float ReadElevation(float x, float y, Vector4 terrainTextureScaleOffset) { var width = (int) Mathf.Sqrt(ElevationValues.Length); - var sectionWidth = width * terrainTextureScaleOffset.x - 1; - var padding = width * new Vector2(terrainTextureScaleOffset.z, terrainTextureScaleOffset.w); + var sectionWidth = width * terrainTextureScaleOffset.x - 1f; + var xx = terrainTextureScaleOffset.z * width + Mathf.Clamp01(x) * sectionWidth; + var yy = terrainTextureScaleOffset.w * width + Mathf.Clamp01(y) * sectionWidth; - var xx = padding.x + (Mathf.Clamp01(x) * sectionWidth); - var yy = padding.y + (Mathf.Clamp01(y) * sectionWidth); + // Bilinear sample — when the caller's UV resolution is finer than the data + // texture's sub-region (typical for render tiles sharing one data tile with 15 + // neighbors), nearest-neighbor sampling produces visible stepping. + if (xx < 0f) xx = 0f; else if (xx > width - 1) xx = width - 1; + if (yy < 0f) yy = 0f; else if (yy > width - 1) yy = width - 1; - var index = (int) yy * width + (int) xx; - if (index >= ElevationValues.Length) - { - return 0; - } - else - { - return ElevationValues[index]; - } + var x0 = (int)xx; + var y0 = (int)yy; + var x1 = x0 + 1; if (x1 >= width) x1 = width - 1; + var y1 = y0 + 1; if (y1 >= width) y1 = width - 1; + var fx = xx - x0; + var fy = yy - y0; + + var row0 = y0 * width; + var row1 = y1 * width; + var h00 = ElevationValues[row0 + x0]; + var h10 = ElevationValues[row0 + x1]; + var h01 = ElevationValues[row1 + x0]; + var h11 = ElevationValues[row1 + x1]; + var h0 = h00 + (h10 - h00) * fx; + var h1 = h01 + (h11 - h01) * fx; + return h0 + (h1 - h0) * fy; } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index ff1528ee6..00ec9951d 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -1,9 +1,17 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Tiles; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; using Mapbox.ImageModule.Terrain.Settings; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; using UnityEngine; +using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.ImageModule.Terrain.TerrainStrategies { @@ -29,16 +37,40 @@ public class ElevatedTerrainStrategy : TerrainStrategy, IElevationBasedTerrainSt private List _newVertexList; private List _newNormalList; private List _newUvList; - //private List _newTriangleList; - private Vector3 _newDir; - private int _vertA, _vertB, _vertC; - private int _counter; private bool _useTileSkirts = false; private float _skirtSize = 1; private int _sideVertexCount; private int _requiredVertexCount; + + private TerrainColliderOptions _colliderOptions; + + // Collider geometry is written by a Burst job into persistent NativeArrays so the + // data flows zero-copy into Mesh.SetVertices / SetIndices. Triangles are generated + // once per sampleCount change (they don't depend on elevation), so only the vertex + // buffer is written per tile build. The elevation mirror is a NativeArray copy of + // the current data tile's managed ElevationValues — populated lazily when the + // source reference changes, reused across the 16 render tiles that share one data + // tile. + private NativeArray _colliderVerticesNative; + private NativeArray _colliderTrianglesNative; + private int _colliderNativeSampleCount = -1; + private NativeArray _elevationNative; + private float[] _elevationNativeMirror; + + // Shared flat render mesh used by every tile in shader mode. All render tiles have + // byte-identical CPU vertex data in that mode (the per-tile _HeightTexture_ST + // sub-region determines the actual surface); pointing every MeshFilter at this + // single Mesh avoids per-tile Mesh allocation and upload. + private const string SharedFlatMeshName = "TerrainSharedFlat"; + private Mesh _sharedFlatMesh; + + // Tracks the (TerrainData, CanonicalTileId) last used to build each MeshCollider's + // sharedMesh. Temp-tile → final-tile transitions can trigger RegisterTile with the + // same data; skipping the rebuild avoids a redundant PhysX re-cook. + private readonly Dictionary + _lastColliderBuild = new Dictionary(); public override int RequiredVertexCount { @@ -56,10 +88,11 @@ public override void Initialize(ElevationLayerProperties elOptions) } _useTileSkirts = elOptions.sideWallOptions.isActive; - _sideVertexCount = _useTileSkirts - ? _elevationOptions.modificationOptions.sampleCount + 3 + _sideVertexCount = _useTileSkirts + ? _elevationOptions.modificationOptions.sampleCount + 3 : _elevationOptions.modificationOptions.sampleCount + 1; _skirtSize = elOptions.sideWallOptions.wallHeight; + _colliderOptions = elOptions.colliderOptions; _newVertexList = new List(_requiredVertexCount); _newNormalList = new List(_requiredVertexCount); @@ -67,6 +100,36 @@ public override void Initialize(ElevationLayerProperties elOptions) _baseMesh = CreateBaseMesh(_elevationOptions.TileMeshSize, _sideVertexCount); _requiredVertexCount = _baseMesh.Vertices.Length; + + // Build the shader-mode shared render mesh from the base data once. Every + // shader-mode tile's MeshFilter will point at this single instance. + _sharedFlatMesh = new Mesh { name = SharedFlatMeshName }; + _sharedFlatMesh.subMeshCount = 2; + _sharedFlatMesh.vertices = _baseMesh.Vertices; + _sharedFlatMesh.normals = _baseMesh.Normals; + for (var i = 0; i < _baseMesh.Triangles.Count; i++) + { + _sharedFlatMesh.SetTriangles(_baseMesh.Triangles[i], i); + } + _sharedFlatMesh.uv = _baseMesh.Uvs; + _sharedFlatMesh.UploadMeshData(markNoLongerReadable: false); + } + + /// + /// Cascaded from TerrainLayerModule.OnDestroy. Releases the shared render + /// mesh and the persistent NativeArrays that back Burst collider builds; per-tile + /// collider meshes are released from UnityMapTile.OnDestroy. + /// + public override void OnDestroy() + { + if (_sharedFlatMesh != null) + { + UnityEngine.Object.Destroy(_sharedFlatMesh); + _sharedFlatMesh = null; + } + if (_colliderVerticesNative.IsCreated) _colliderVerticesNative.Dispose(); + if (_colliderTrianglesNative.IsCreated) _colliderTrianglesNative.Dispose(); + if (_elevationNative.IsCreated) _elevationNative.Dispose(); } public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) @@ -76,8 +139,29 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) tile.gameObject.layer = _elevationOptions.unityLayerOptions.layerId; } - if (tile.MeshVertexCount != RequiredVertexCount) + if (!createElevatedMesh) + { + // Shader mode: point the tile at the shared flat mesh. Byte-identical + // vertex data across tiles means a single Mesh covers the whole pool; the + // shader picks the right sub-region via per-tile _HeightTexture_ST. + if (tile.MeshFilter.sharedMesh != _sharedFlatMesh) + { + var previous = tile.MeshFilter.sharedMesh; + tile.MeshFilter.sharedMesh = _sharedFlatMesh; + // The mesh UnityMapTile.Awake allocated is now orphaned; destroy it + // unless something else (not us) already put the shared mesh here. + if (previous != null && previous != _sharedFlatMesh && previous.name != SharedFlatMeshName) + { + UnityEngine.Object.Destroy(previous); + } + } + tile.MeshVertexCount = _sharedFlatMesh.vertexCount; + } + else if (tile.MeshVertexCount != RequiredVertexCount) { + // CPU mode: each tile needs its own unique vertex buffer since the Y + // displacement is per-tile. Reset the tile's Awake-allocated mesh from the + // base template. Mesh sharedMesh; (sharedMesh = tile.MeshFilter.sharedMesh).Clear(); var newMesh = _baseMesh; @@ -93,11 +177,399 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) tile.MeshVertexCount = newMesh.Vertices.Length; tile.ElevationUpdatedCallback(); } - + if (createElevatedMesh) { CreateElevatedMesh(tile); } + + if (_colliderOptions != null && _colliderOptions.addCollider) + { + RegisterCollider(tile); + } + } + + /// + /// Ensures has a backed by a + /// CPU-elevated mesh. Builds immediately when elevation data is already decoded, + /// otherwise defers until fires + /// (async GPU-readback path). Short-circuits when the tile's existing collider was + /// already built from the same data + tileId (temp→final transitions trigger + /// RegisterTile multiple times without the underlying data changing). + /// + private void RegisterCollider(UnityMapTile tile) + { + var data = tile.TerrainContainer != null ? tile.TerrainContainer.TerrainData : null; + if (data == null) + { + return; + } + + var existing = FindExistingCollider(tile); + if (existing != null && + _lastColliderBuild.TryGetValue(existing, out var last) && + ReferenceEquals(last.data, data) && + last.tileId.Equals(tile.CanonicalTileId)) + { + return; + } + + if (data.IsElevationDataReady) + { + BuildAndAssignCollider(tile); + } + else + { + // Defer until values arrive. The callback self-unsubscribes on fire and + // no-ops if the tile has since been recycled onto different data, so a late + // async readback does not stomp a freshly-reassigned tile. + Action rebuild = null; + rebuild = () => + { + if (tile == null || tile.TerrainContainer == null || tile.TerrainContainer.TerrainData != data) + { + data.ElevationValuesUpdated -= rebuild; + return; + } + BuildAndAssignCollider(tile); + data.ElevationValuesUpdated -= rebuild; + }; + data.ElevationValuesUpdated += rebuild; + } + } + + // Our generated grid has no duplicate verts, no degenerate triangles, and doesn't + // need welding, so we tell PhysX to skip those cook stages. Keeps + // CookForFasterSimulation on for faster runtime queries against the static terrain. + private const MeshColliderCookingOptions TerrainColliderCookingOptions = + MeshColliderCookingOptions.CookForFasterSimulation | + MeshColliderCookingOptions.UseFastMidphase; + + // Name of the dedicated child GameObject that holds the MeshCollider when + // useDedicatedColliderLayer is enabled. Keyed by name so we can locate + reuse it + // across pool cycles without maintaining a per-tile dictionary. + private const string ColliderChildName = "TerrainCollider"; + + /// + /// Looks up the existing for this tile without creating + /// one, mirroring the location rule in . Used by + /// the rebuild-short-circuit check. + /// + private MeshCollider FindExistingCollider(UnityMapTile tile) + { + if (_colliderOptions.useDedicatedColliderLayer) + { + var childTransform = tile.transform.Find(ColliderChildName); + return childTransform != null ? childTransform.GetComponent() : null; + } + return tile.GetComponent(); + } + + /// + /// Returns the the collider mesh should be assigned to. + /// When is enabled + /// the collider lives on a child GameObject so it can sit on its own Unity Layer + /// independently of the tile's render layer; otherwise it's attached to the tile + /// itself. Applies our tuned on first + /// creation. + /// + private MeshCollider GetOrCreateCollider(UnityMapTile tile) + { + if (_colliderOptions.useDedicatedColliderLayer) + { + var childTransform = tile.transform.Find(ColliderChildName); + GameObject childGo; + if (childTransform == null) + { + childGo = new GameObject(ColliderChildName); + childGo.transform.SetParent(tile.transform, worldPositionStays: false); + } + else + { + childGo = childTransform.gameObject; + } + childGo.layer = _colliderOptions.colliderLayerId; + + var childCollider = childGo.GetComponent(); + if (childCollider == null) + { + childCollider = childGo.AddComponent(); + childCollider.cookingOptions = TerrainColliderCookingOptions; + } + return childCollider; + } + + var meshCollider = tile.GetComponent(); + if (meshCollider == null) + { + meshCollider = tile.gameObject.AddComponent(); + meshCollider.cookingOptions = TerrainColliderCookingOptions; + } + return meshCollider; + } + + /// + /// Builds a dedicated CPU-elevated collider mesh for and + /// assigns it to the tile's . Grid resolution mirrors the + /// render mesh so physics stays visually aligned with the terrain surface. + /// + private void BuildAndAssignCollider(UnityMapTile tile) + { + var meshCollider = GetOrCreateCollider(tile); + + // Unity auto-populates sharedMesh on a newly added MeshCollider from the + // GameObject's MeshFilter.sharedMesh. We must NOT write into that mesh — it is + // the render mesh. Detect and allocate a dedicated collider Mesh instead. + var mesh = meshCollider.sharedMesh; + if (mesh == null || mesh == tile.MeshFilter.sharedMesh) + { + mesh = new Mesh { name = "TerrainCollider" }; + mesh.MarkDynamic(); + } + + // Grid resolution matches the render mesh so the collision surface aligns with + // the visible terrain. If the user picks a coarser SimplificationFactor, the + // collider follows. + var sampleCount = _elevationOptions.modificationOptions.sampleCount; + var side = sampleCount + 1; + var size = _elevationOptions.TileMeshSize; + var scale = tile.TileScale; + + // Elevation mirror is populated once per data tile and shared across the 16 + // render tiles that sample the same data. Triangles are regenerated only when + // sampleCount changes — see EnsureColliderBuffers. + var container = tile.TerrainContainer; + var elevationValues = container.TerrainData.ElevationValues; + var dataWidth = (int)Mathf.Sqrt(elevationValues.Length); + var scaleOffset = container.TerrainTextureScaleOffset; + + EnsureColliderBuffers(sampleCount); + EnsureElevationNative(elevationValues); + + new BuildColliderVerticesJob + { + ElevationValues = _elevationNative, + DataWidth = dataWidth, + SampleCount = sampleCount, + Side = side, + Size = size, + Scale = scale, + SectionWidth = dataWidth * scaleOffset.x - 1f, + PaddingX = dataWidth * scaleOffset.z, + PaddingY = dataWidth * scaleOffset.w, + Vertices = _colliderVerticesNative + }.Run(); + + mesh.Clear(); + mesh.SetVertices(_colliderVerticesNative); + // SetIndices is the canonical NativeArray-taking Mesh index API; the + // NativeArray overload of SetTriangles isn't present on every Unity 2022.3 + // point release. MeshTopology.Triangles produces equivalent triangle-list + // geometry. + mesh.SetIndices(_colliderTrianglesNative, MeshTopology.Triangles, 0, calculateBounds: false); + mesh.RecalculateBounds(); + + // Record what we just built so RegisterCollider can short-circuit on + // redundant subsequent RegisterTile calls (temp→final transitions). + _lastColliderBuild[meshCollider] = (tile.TerrainContainer.TerrainData, tile.CanonicalTileId); + + if (_colliderOptions.asyncBakeCollider) + { + // Move the PhysX cook to a worker thread. The assignment (and thus any + // implicit recook) happens one frame later once BakeMesh has populated the + // native cooked-data cache for this mesh id. PhysX reuses that cache when + // sharedMesh is assigned, so the main-thread step is near-free. + var handle = new BakeColliderJob + { + MeshId = mesh.GetInstanceID(), + CookingOptions = TerrainColliderCookingOptions + }.Schedule(); + Runnable.Instance.StartCoroutine(CompleteBakeAndAssign(meshCollider, mesh, handle)); + } + else + { + // Null-then-reassign forces PhysX to re-cook collision data; a same-reference + // reassignment is a no-op. + meshCollider.sharedMesh = null; + meshCollider.sharedMesh = mesh; + } + } + + /// + /// (Re)allocates the persistent collider NativeArrays when the strategy's + /// sampleCount changes, and generates the triangle index list once — triangles are + /// a pure function of and never need rebuilding per + /// tile. + /// + private void EnsureColliderBuffers(int sampleCount) + { + if (_colliderNativeSampleCount == sampleCount && _colliderVerticesNative.IsCreated && _colliderTrianglesNative.IsCreated) + { + return; + } + if (_colliderVerticesNative.IsCreated) _colliderVerticesNative.Dispose(); + if (_colliderTrianglesNative.IsCreated) _colliderTrianglesNative.Dispose(); + + var side = sampleCount + 1; + _colliderVerticesNative = new NativeArray(side * side, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + _colliderTrianglesNative = new NativeArray(sampleCount * sampleCount * 6, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + + int ti = 0; + for (int y = 0; y < sampleCount; y++) + { + for (int x = 0; x < sampleCount; x++) + { + int vertA = y * side + x; + int vertB = vertA + side + 1; + int vertC = vertA + side; + _colliderTrianglesNative[ti++] = vertA; + _colliderTrianglesNative[ti++] = vertC; + _colliderTrianglesNative[ti++] = vertB; + + vertA = y * side + x; + vertB = vertA + 1; + vertC = vertA + side + 1; + _colliderTrianglesNative[ti++] = vertA; + _colliderTrianglesNative[ti++] = vertC; + _colliderTrianglesNative[ti++] = vertB; + } + } + _colliderNativeSampleCount = sampleCount; + } + + /// + /// Mirrors the current data tile's managed ElevationValues into the + /// strategy's persistent . Reuses the existing + /// NativeArray when the source reference is unchanged, so the 16 render tiles that + /// share a data tile pay the ~256 KB copy only once. + /// + private void EnsureElevationNative(float[] source) + { + if (ReferenceEquals(source, _elevationNativeMirror) && _elevationNative.IsCreated && _elevationNative.Length == source.Length) + { + return; + } + if (_elevationNative.IsCreated && _elevationNative.Length != source.Length) + { + _elevationNative.Dispose(); + } + if (!_elevationNative.IsCreated) + { + _elevationNative = new NativeArray(source.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + } + _elevationNative.CopyFrom(source); + _elevationNativeMirror = source; + } + + /// + /// Burst-compiled vertex fill for the collider mesh. Bilinearly samples the + /// mirrored elevation NativeArray and writes world-local vertex positions. Grid + /// topology (triangles) is produced once in , + /// not in this job. + /// + [BurstCompile] + private struct BuildColliderVerticesJob : IJob + { + [ReadOnly] public NativeArray ElevationValues; + public int DataWidth; + public int SampleCount; + public int Side; + public float Size; + public float Scale; + public float SectionWidth; + public float PaddingX; + public float PaddingY; + public NativeArray Vertices; + + public void Execute() + { + var invSampleCount = 1f / SampleCount; + var maxIndex = DataWidth - 1; + + for (int y = 0; y < Side; y++) + { + var yrat = y * invSampleCount; + var sampleYf = PaddingY + yrat * SectionWidth; + if (sampleYf < 0f) sampleYf = 0f; else if (sampleYf > maxIndex) sampleYf = maxIndex; + var y0 = (int)sampleYf; + var y1 = y0 + 1; if (y1 > maxIndex) y1 = maxIndex; + var fy = sampleYf - y0; + var row0 = y0 * DataWidth; + var row1 = y1 * DataWidth; + var yy = (1f - yrat) * Size; + + for (int x = 0; x < Side; x++) + { + var xrat = x * invSampleCount; + var sampleXf = PaddingX + xrat * SectionWidth; + if (sampleXf < 0f) sampleXf = 0f; else if (sampleXf > maxIndex) sampleXf = maxIndex; + var x0 = (int)sampleXf; + var x1 = x0 + 1; if (x1 > maxIndex) x1 = maxIndex; + var fx = sampleXf - x0; + + var h00 = ElevationValues[row0 + x0]; + var h10 = ElevationValues[row0 + x1]; + var h01 = ElevationValues[row1 + x0]; + var h11 = ElevationValues[row1 + x1]; + var h0 = h00 + (h10 - h00) * fx; + var h1 = h01 + (h11 - h01) * fx; + var sample = h0 + (h1 - h0) * fy; + + Vertices[y * Side + x] = new Vector3(xrat * Size, sample * Scale, -yy); + } + } + } + } + + /// + /// IJob wrapper around Physics.BakeMesh so the PhysX cook can run on a + /// worker thread. The cooking options must match what the MeshCollider is + /// configured with, otherwise PhysX discards the cached data and re-cooks on + /// assignment. + /// + private struct BakeColliderJob : IJob + { + public int MeshId; + public MeshColliderCookingOptions CookingOptions; + + public void Execute() + { + Physics.BakeMesh(MeshId, false, CookingOptions); + } + } + + /// + /// Waits for an async to complete, then assigns the + /// pre-cooked mesh to its . Handles: (a) the tile or + /// collider got destroyed during the bake — we free the orphaned mesh so it doesn't + /// leak; (b) a previous TerrainCollider mesh was attached to this collider — + /// we destroy it once we've swapped in the new one, since each async build + /// allocates a fresh Mesh. + /// + private static IEnumerator CompleteBakeAndAssign(MeshCollider meshCollider, Mesh mesh, JobHandle handle) + { + while (!handle.IsCompleted) + { + yield return null; + } + handle.Complete(); + + if (meshCollider == null) + { + if (mesh != null) + { + UnityEngine.Object.Destroy(mesh); + } + yield break; + } + + var previous = meshCollider.sharedMesh; + meshCollider.sharedMesh = null; + meshCollider.sharedMesh = mesh; + if (previous != null && previous != mesh && previous.name == "TerrainCollider") + { + UnityEngine.Object.Destroy(previous); + } } private void CreateElevatedMesh(UnityMapTile tile) @@ -125,7 +597,13 @@ private void CreateElevatedMesh(UnityMapTile tile) vertices[i].Set(vertices[i].x, elevation, vertices[i].z); } - mesh.vertices = vertices; + mesh.SetVertices(vertices); + // Actual displaced bounds are tighter than the min/max-padded fallback bounds + // set by UnityMapTile.ElevationUpdatedCallback. Mesh normals/tangents are not + // recalculated: the terrain shader derives the surface normal from the height + // texture directly in the fragment stage, so the mesh's vertex normals are + // never read in either mode. + mesh.RecalculateBounds(); } #region mesh gen @@ -200,6 +678,37 @@ private MeshDataArray CreateBaseMeshWithoutSkirts(float size, int sampleCount) return mesh; } + /// + /// Fraction of tile size that the outer skirt ring extends past the tile boundary. + /// Kept constant so coarse meshes (e.g. sampleCount=2 with + /// SimplificationFactor=64) don't balloon the skirt half-way into neighboring + /// tiles. 1% is narrow enough to be invisible at typical zoom levels and wide enough + /// to hide seam artifacts from float-precision mismatches. + /// + private const float SkirtOuterOffsetFraction = 0.01f; + + /// + /// Maps a skirt-loop index (-1 .. sideVertexCount-2) to the [0,1] UV range + /// used for interior vertices, with the outer ring pinned to a fixed offset outside + /// the tile rather than one grid step. Without this, a 3x3 grid puts the skirt half + /// a tile outside the edge and overlaps neighboring tiles. + /// + /// Loop index. -1 is the left/top outer skirt row; sideVertexCount-2 is the right/bottom outer skirt row. + /// Number of grid segments along one tile side (sampleCount). + /// Total verts per tile axis including skirts. + private static float RatioForSkirtIndex(int index, int interiorSteps, int sideVertexCount) + { + if (index == -1) + { + return -SkirtOuterOffsetFraction; + } + if (index == sideVertexCount - 2) + { + return 1f + SkirtOuterOffsetFraction; + } + return (float)index / interiorSteps; + } + private MeshDataArray CreateBaseMeshSkirts(float size, int sideVertexCount) { //TODO use arrays instead of lists @@ -207,16 +716,17 @@ private MeshDataArray CreateBaseMeshSkirts(float size, int sideVertexCount) _newNormalList.Clear(); _newUvList.Clear(); var _newTriangleList = new List(); + var interiorSteps = sideVertexCount - 3; //012 //345 //678 for (int y = -1; y < sideVertexCount - 1; y++) { - var yrat = (float)y / (sideVertexCount - 3); // 1 for buffer pixel, 2 for skirts + var yrat = RatioForSkirtIndex(y, interiorSteps, sideVertexCount); for (int x = -1; x < sideVertexCount - 1; x++) { - var xrat = (float)x / (sideVertexCount - 3); + var xrat = RatioForSkirtIndex(x, interiorSteps, sideVertexCount); var xx = Mathf.LerpUnclamped(0, size, xrat); //lerp x/y swapped here because of the texture space conversion (y to -y) @@ -281,9 +791,8 @@ private MeshDataArray CreateBaseMeshSkirts(float size, int sideVertexCount) mesh.Vertices = _newVertexList.ToArray(); mesh.Normals = _newNormalList.ToArray(); mesh.Uvs = _newUvList.ToArray(); - topQuadTris.AddRange(_newTriangleList.ToArray()); + topQuadTris.AddRange(_newTriangleList); mesh.Triangles.Add(topQuadTris.ToArray()); - //mesh.Triangles.Add(_newTriangleList.ToArray()); return mesh; } #endregion diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationLayerProperties.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationLayerProperties.cs index 8d94586a0..fbe2223f5 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationLayerProperties.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationLayerProperties.cs @@ -1,14 +1,30 @@ -using System; +using System; using Mapbox.BaseModule.Utilities; +using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Bundle of sub-option groups consumed by implementations + /// to build per-tile terrain geometry. Passed through + /// TerrainStrategy.Initialize. + /// [Serializable] - public class ElevationLayerProperties + public class ElevationLayerProperties { + [Tooltip("Controls the terrain render mesh grid density via a simplification factor. Expand to pick a quality preset.")] public ElevationModificationOptions modificationOptions = new ElevationModificationOptions(); + + [Tooltip("Optional Unity layer assignment for every terrain tile GameObject (useful for culling, raycast masks, lighting).")] public UnityLayerOptions unityLayerOptions = new UnityLayerOptions(); + + [Tooltip("Optional downward side-wall skirts around each tile that hide horizontal gaps between neighboring tiles at different elevations.")] public TerrainSideWallOptions sideWallOptions = new TerrainSideWallOptions(); + + [Tooltip("Optional MeshCollider generation for terrain tiles so physics can interact with the map surface.")] + public TerrainColliderOptions colliderOptions = new TerrainColliderOptions(); + + [Tooltip("World-unit size of a single terrain tile quad along X and Z. Default 1 matches the map's internal tile scale; change only if you know why.")] public float TileMeshSize = 1; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationModificationOptions.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationModificationOptions.cs index 8343f9966..fa32fc0af 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationModificationOptions.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationModificationOptions.cs @@ -1,14 +1,24 @@ -using System; +using System; using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Grid density for the terrain render mesh. The value chosen here propagates to the + /// collider and CPU elevation sampling logic, so changing it affects both visuals and + /// physics resolution. + /// [Serializable] public class ElevationModificationOptions { - [Tooltip("Results in 128/n x 128/n grid")] - [Range(1,128)] + [Tooltip("Divisor applied to 128 to determine terrain render-mesh grid density. Pick from the dropdown: 1 = finest (129x129 verts, heaviest), 128 = coarsest (2x2 verts, fastest). Very coarse values show visible seams; the inspector warns you. Default: 1.")] + [SimplificationFactor(1, 128)] public int SimplificationFactor = 1; + + /// + /// Convenience accessor: number of grid segments per tile side. Equals + /// 128 / SimplificationFactor. Vertex count per side is sampleCount + 1. + /// public int sampleCount => 128 / SimplificationFactor; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs new file mode 100644 index 000000000..ddf29aaf3 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace Mapbox.ImageModule.Terrain.Settings +{ + /// + /// Marks an field as a terrain grid-simplification factor. In the + /// Inspector this renders as a dropdown of presets (each halving the grid resolution) + /// plus a live help box that shows the resulting grid size and warns at coarse values. + /// Legacy non-preset values are snapped to the nearest preset on first inspector render. + /// The drawer lives in the editor assembly. + /// + public class SimplificationFactorAttribute : PropertyAttribute + { + /// Smallest allowed factor value (finest grid). + public readonly int Min; + + /// Largest allowed factor value (coarsest grid). + public readonly int Max; + + /// Number divided by the factor to produce the sample (segment) count. Usually 128. + public readonly int VertexBase; + + /// Smallest allowed factor value. + /// Largest allowed factor value. + /// Dividend for sample-count math. Usually 128. + public SimplificationFactorAttribute(int min, int max, int vertexBase = 128) + { + Min = min; + Max = max; + VertexBase = vertexBase; + } + } +} diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs.meta b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs.meta new file mode 100644 index 000000000..d5fbc2709 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d12c7339a206a4d8facdc684bd11ff46 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainColliderOptions.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainColliderOptions.cs index e15590c51..0aa94fc3b 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainColliderOptions.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainColliderOptions.cs @@ -1,14 +1,29 @@ -using System; +using System; +using Mapbox.BaseModule.Utilities.Attributes; using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Inspector options that control whether each terrain tile gets a MeshCollider. The + /// collider mesh is generated lazily from the CPU elevation data once it arrives, with + /// a grid resolution that mirrors the terrain render mesh + /// (). + /// [Serializable] - public class TerrainColliderOptions + public class TerrainColliderOptions { - [Tooltip("Add Unity Physics collider to terrain tiles, used for detecting collisions etc.")] + [Tooltip("Attach a MeshCollider to each terrain tile so physics and raycasts can hit the map surface. Forces ExtractCpuElevationData on in the parent module. The collider grid resolution mirrors the render mesh's SimplificationFactor; coarser render = coarser collider. Default: off.")] public bool addCollider = false; - + [Tooltip("Bake MeshCollider cook data on a worker thread via Physics.BakeMesh, then assign sharedMesh one frame later. Removes main-thread cook stalls during bursty tile loads at the cost of a one-frame latency before the collider is usable. Recommended on when Simplification Factor is low (dense grids with expensive cooks). Default: off.")] + public bool asyncBakeCollider = false; + + [Tooltip("Put the MeshCollider on a dedicated child GameObject with its own Unity Layer, so physics raycast filtering can be independent of the tile's render layer. Adds one extra GameObject per tile. When off, the collider sits on the tile GameObject and shares its Unity Layer. Default: off.")] + public bool useDedicatedColliderLayer = false; + + [GameObjectLayer] + [Tooltip("Unity Layer the child collider GameObject is placed on when Use Dedicated Collider Layer is enabled. Typical pattern: assign a \"Terrain\" layer here and filter your gameplay raycasts to that layer.")] + public int colliderLayerId = 0; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainSideWallOptions.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainSideWallOptions.cs index 2fc6c0e9b..d3cec2930 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainSideWallOptions.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainSideWallOptions.cs @@ -1,14 +1,20 @@ -using System; +using System; using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Optional downward skirts extended slightly past each terrain tile's edges to hide + /// horizontal gaps that appear between neighboring tiles when their elevations don't + /// line up exactly (float precision, texture filtering, different zoom parents). + /// [Serializable] public class TerrainSideWallOptions { - [Tooltip("Adds side walls to terrain meshes, reduces visual artifacts.")] + [Tooltip("Enable downward skirts along each tile's four edges. Hides visible seams between tiles at different heights. A small visual overhead (~1% tile-size outer offset, a handful of extra triangles per tile). Default: off.")] public bool isActive = false; - [Tooltip("Height of side walls.")] + + [Tooltip("How far down (in world units) the skirts hang below the terrain surface. Larger = taller walls that hide bigger seams but may peek out from under thin terrain features. Default 0.1. Typical range 0.05 - 1.")] public float wallHeight = .1f; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/UnityLayerOptions.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/UnityLayerOptions.cs index 36a741ceb..53b25ae5d 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/UnityLayerOptions.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/UnityLayerOptions.cs @@ -1,14 +1,21 @@ -using System; +using System; +using Mapbox.BaseModule.Utilities.Attributes; using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Unity Layer assignment for terrain tile GameObjects. Useful when you want to apply a + /// custom raycast mask, lighting layer, or camera culling layer to terrain. + /// [Serializable] public class UnityLayerOptions { - [Tooltip("Add terrain tiles to Unity Layer")] + [Tooltip("When enabled, every terrain tile GameObject is moved to the Unity Layer selected below. Use this to carve terrain out of a raycast mask or put it on a dedicated lighting/culling layer. Default: off.")] public bool addToLayer = false; - [Tooltip("Unity Layer id to which terrain tiles will get added.")] + + [GameObjectLayer] + [Tooltip("Unity Layer that terrain tiles are assigned to when Add To Layer is enabled. The dropdown shows layers defined in your Project's Tags & Layers settings.")] public int layerId = 0; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/TerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/TerrainStrategy.cs index 1165c1450..b03c89fe8 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/TerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/TerrainStrategy.cs @@ -22,5 +22,14 @@ public virtual void RegisterTile(UnityMapTile tile, bool createElevatedMesh) { } + + /// + /// Release any Unity objects (Meshes, textures) the strategy created. Called by + /// TerrainLayerModule.OnDestroy. + /// + public virtual void OnDestroy() + { + + } } } diff --git a/Runtime/Mapbox/BaseModule/Map/ImageSourceSettings.cs b/Runtime/Mapbox/BaseModule/Map/ImageSourceSettings.cs index 69ead7537..f3631b3cd 100644 --- a/Runtime/Mapbox/BaseModule/Map/ImageSourceSettings.cs +++ b/Runtime/Mapbox/BaseModule/Map/ImageSourceSettings.cs @@ -12,8 +12,10 @@ public class ImageSourceSettings public bool UseRetinaTextures = true; [Tooltip("Non-readable textures halves the memory usage but cannot be queried for pixel values. Suitable if texture will be directly passed to shader.")] public bool UseNonReadableTextures = true; + + [Tooltip("Maximum tiles kept resident in the in-memory cache. Higher = fewer refetches at the cost of RAM. Rough budgets per tile: 256KB raster (512KB if UseRetinaTextures on) + 256KB float[] when CPU elevation extraction is on. At the default 100, terrain alone can sit at ~50MB resident. Default: 100.")] public int CacheSize = 100; - + [Tooltip("Maximum data level that'll be used for this module. Tiles can be higher zoom level but data will be lower level.")] public int ClampDataLevelToMax = 16; } diff --git a/Runtime/Mapbox/BaseModule/Map/TileCreator.cs b/Runtime/Mapbox/BaseModule/Map/TileCreator.cs index a54fb6431..584b81f74 100644 --- a/Runtime/Mapbox/BaseModule/Map/TileCreator.cs +++ b/Runtime/Mapbox/BaseModule/Map/TileCreator.cs @@ -59,21 +59,19 @@ private UnityMapTile CreateTile(UnityContext unityContext) if (TileMaterials?.Length == 1) { - if (_instanceMaterial) - { - tile.MeshRenderer.material = TileMaterials[0]; - tile.Material = tile.MeshRenderer.material; - } - else - { - tile.MeshRenderer.sharedMaterial = TileMaterials[0]; - tile.Material = tile.MeshRenderer.sharedMaterial; - } + // Always use sharedMaterial. Per-tile state goes through the tile's + // MaterialPropertyBlock (UnityTileTerrainContainer / UnityTileImageContainer + // mutate it), which lets URP's SRP Batcher merge tile draws into a single + // batch. The legacy _instanceMaterial flag is retained on the constructor + // for back-compat but no longer differentiates: per-tile state lives on the + // property block, not on a unique Material instance. + tile.MeshRenderer.sharedMaterial = TileMaterials[0]; + tile.Material = tile.MeshRenderer.sharedMaterial; } else if (TileMaterials?.Length > 1) { - tile.MeshRenderer.materials = TileMaterials; - tile.Material = tile.MeshRenderer.material; + tile.MeshRenderer.sharedMaterials = TileMaterials; + tile.Material = tile.MeshRenderer.sharedMaterial; } tile.gameObject.SetActive(false); diff --git a/Runtime/Mapbox/BaseModule/MapboxBaseModule.asmdef b/Runtime/Mapbox/BaseModule/MapboxBaseModule.asmdef index fa0fc9a47..6ebb83cd2 100644 --- a/Runtime/Mapbox/BaseModule/MapboxBaseModule.asmdef +++ b/Runtime/Mapbox/BaseModule/MapboxBaseModule.asmdef @@ -2,7 +2,8 @@ "name": "MapboxBaseModule", "rootNamespace": "", "references": [ - "GUID:478a2357cc57436488a56e564b08d223" + "GUID:478a2357cc57436488a56e564b08d223", + "GUID:2665a8d13d1b3f18800f46e256720795" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs index e04a59ae9..52f950104 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs @@ -30,6 +30,36 @@ public class UnityMapTile : MonoBehaviour public MeshFilter MeshFilter => _meshFilter; public List Children; + // Per-tile state (height texture, scale-offset, elevation multiplier, etc.) is + // pushed via this MaterialPropertyBlock instead of a per-tile Material instance. + // One Material is shared across the entire tile pool (set in TileCreator), which + // lets URP's SRP Batcher merge the tile draws into a single batch. + private MaterialPropertyBlock _propertyBlock; + public MaterialPropertyBlock PropertyBlock + { + get + { + if (_propertyBlock == null) + { + _propertyBlock = new MaterialPropertyBlock(); + } + return _propertyBlock; + } + } + + /// + /// Pushes the cached onto the renderer. Call once + /// after a logical batch of property mutations (the block can hold many properties + /// at once, so multiple Set* calls between two Apply calls coalesce). + /// + public void ApplyPropertyBlock() + { + if (_meshRenderer != null && _propertyBlock != null) + { + _meshRenderer.SetPropertyBlock(_propertyBlock); + } + } + public int MeshVertexCount = 0; //public bool IsTemporary = false; @@ -55,20 +85,48 @@ public void Initialize(UnwrappedTileId tileId, float scale) #if UNITY_EDITOR gameObject.name = tileId.ToString(); #endif - - Material.SetFloat(TileScalePropertyId, TileScale); + + PropertyBlock.SetFloat(TileScalePropertyId, TileScale); + ApplyPropertyBlock(); } public void ElevationUpdatedCallback() { - if (MeshFilter != null) + if (_meshRenderer != null) { var centerHeight = (TerrainContainer.TerrainData.MaxElevation + TerrainContainer.TerrainData.MinElevation) / 2 * TileScale; var boxHeight = (TerrainContainer.TerrainData.MaxElevation - TerrainContainer.TerrainData.MinElevation) * TileScale; - _meshFilter.mesh.bounds = new Bounds(new Vector3(.5f, centerHeight, -.5f), new Vector3(1, boxHeight, 1)); + // Renderer.localBounds overrides the Mesh's bounds for culling without + // cloning the Mesh. Using MeshFilter.mesh.bounds would silently clone the + // sharedMesh, which defeats per-tile Mesh sharing. + _meshRenderer.localBounds = new Bounds(new Vector3(.5f, centerHeight, -.5f), new Vector3(1, boxHeight, 1)); } OnElevationValuesUpdated(this); } + + // Hard cap used for fallback mesh bounds. Earth's highest point is ~8849m; 10000m + // gives a safe margin. Oversize bounds only widen the frustum test; they don't add + // any draw cost since there is no geometry outside the real elevation range. + private const float FallbackMaxElevationMeters = 10000f; + + /// + /// Applies a conservative mesh bounds that covers the full plausible terrain height + /// range so shader-displaced vertices stay inside the mesh's frustum culling volume + /// before real MinElevation/MaxElevation are known (or permanently, + /// when CPU extraction is disabled). A later call to + /// tightens the bounds if CPU elevation + /// eventually arrives. + /// + public void SetFallbackMeshBounds() + { + if (_meshRenderer == null) + { + return; + } + var boxHeight = FallbackMaxElevationMeters * TileScale; + var centerHeight = boxHeight * 0.5f; + _meshRenderer.localBounds = new Bounds(new Vector3(.5f, centerHeight, -.5f), new Vector3(1, boxHeight, 1)); + } public void Recycle() { @@ -88,6 +146,29 @@ private void OnDestroy() ImageContainer.OnDestroy(); TerrainContainer.OnDestroy(); VectorContainer.OnDestroy(); + + // Unity does not auto-free runtime-created Meshes when the GameObject is + // destroyed. Release the render mesh allocated in Awake and any dedicated + // collider mesh allocated by ElevatedTerrainStrategy (named "TerrainCollider"). + // Skip the shader-mode shared flat mesh ("TerrainSharedFlat") — it is owned by + // the strategy and disposed via TerrainLayerModule.OnDestroy. The identity + // checks avoid double-freeing an aliased render/collider mesh (FlatTerrainStrategy). + // Colliders may live on a dedicated-layer child GameObject; scan children too. + var renderMesh = _meshFilter != null ? _meshFilter.sharedMesh : null; + var colliders = GetComponentsInChildren(includeInactive: true); + foreach (var mc in colliders) + { + if (mc.sharedMesh != null && + mc.sharedMesh.name == "TerrainCollider" && + mc.sharedMesh != renderMesh) + { + Destroy(mc.sharedMesh); + } + } + if (renderMesh != null && renderMesh.name != "TerrainSharedFlat") + { + Destroy(renderMesh); + } } } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs index 818b6bd08..80454a5ac 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs @@ -50,9 +50,11 @@ public void OnImageryUpdated() var scaleOffset = _unityMapTile.CanonicalTileId.CalculateScaleOffsetAtZoom(ImageData.TileId.Z); - _unityMapTile.Material.SetTexture(MainTex, ImageData.Texture); - _unityMapTile.Material.SetVector(MainTexSt, scaleOffset); - _unityMapTile.Material.SetFloat(MainTextureChangeTime, Time.time); + var block = _unityMapTile.PropertyBlock; + block.SetTexture(MainTex, ImageData.Texture); + block.SetVector(MainTexSt, scaleOffset); + block.SetFloat(MainTextureChangeTime, Time.time); + _unityMapTile.ApplyPropertyBlock(); } public RasterData GetAndClearImageData() @@ -60,7 +62,8 @@ public RasterData GetAndClearImageData() if (ImageData == null) return null; - _unityMapTile.Material.SetTexture(MainTex, Texture2D.blackTexture); + _unityMapTile.PropertyBlock.SetTexture(MainTex, Texture2D.blackTexture); + _unityMapTile.ApplyPropertyBlock(); var rd = ImageData; ImageData.SetDisposeCallback(null); ImageData = null; @@ -70,7 +73,8 @@ public RasterData GetAndClearImageData() public void DisableImagery() { State = TileContainerState.Final; - _unityMapTile.Material.SetTexture(MainTex, null); + _unityMapTile.PropertyBlock.SetTexture(MainTex, null); + _unityMapTile.ApplyPropertyBlock(); } public void OnDestroy() diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs index c247f4ae7..58aa5e0fb 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs @@ -28,6 +28,14 @@ public class UnityTileTerrainContainer [SerializeField] public TerrainData TerrainData; private Vector4 _terrainTextureScaleOffset; + /// + /// Scale/offset Vector4 that maps this tile's UV [0,1] to the shared data tile's + /// UV sub-region (data tile is currentZ − 2 zoom coarser than this render + /// tile). Exposed so the collider builder can lift sampling math out of its inner + /// loop instead of calling per vertex. + /// + public Vector4 TerrainTextureScaleOffset => _terrainTextureScaleOffset; + public UnityTileTerrainContainer(UnityMapTile unityMapTile, Action elevationUpdatedCallback, Action onDisposeCallback) { _unityMapTile = unityMapTile; @@ -35,10 +43,27 @@ public UnityTileTerrainContainer(UnityMapTile unityMapTile, Action elevationUpda ElevationValuesUpdated = elevationUpdatedCallback; } + /// + /// Assigns terrain data to this tile: wires the material height texture, applies a + /// conservative fallback mesh bounds (so shader-displaced verts don't get frustum- + /// culled before real Min/MaxElevation arrive), and subscribes to future elevation + /// updates. + /// + /// Raster + optional decoded elevation for this tile. + /// When true, the material's _ElevationMultiplier is set to 1 so the vertex shader samples _HeightTexture; otherwise 0 (CPU-displaced verts). + /// Marks the tile container as temporary (using a parent's data while its own loads) or final. public void SetTerrainData(TerrainData terrainData, bool useShaderElevation, TileContainerState state = TileContainerState.Final) { + // Detach from the previous TerrainData (if any) before swapping. Without this, + // a reassignment leaks our subscription on the old data and causes spurious + // bounds updates if that data is shared with other tiles. + if (TerrainData != null) + { + TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; + } + terrainData?.SetDisposeCallback(null); - + State = state; if (terrainData.Texture == null || terrainData.TileId.Z == 0) { @@ -46,17 +71,26 @@ public void SetTerrainData(TerrainData terrainData, bool useShaderElevation, Til } TerrainData = terrainData; TerrainData.SetDisposeCallback(_onDisposeCallback); - + OnTerrainUpdated(); + + // Apply a generous fallback bounds up front so shader-displaced geometry does not + // get frustum-culled before real Min/MaxElevation arrive (or forever, if CPU + // elevation extraction is disabled). OnElevationValuesUpdated later tightens the + // bounds to the actual elevation range if the float[] is decoded. + _unityMapTile.SetFallbackMeshBounds(); + + // Subscribe multicast-safe so other listeners (e.g. ElevatedTerrainStrategy's + // deferred collider rebuild) can coexist. + TerrainData.ElevationValuesUpdated += OnElevationValuesUpdated; + if (TerrainData.IsElevationDataReady) { OnElevationValuesUpdated(); } - TerrainData.SetElevationChangedCallback(OnElevationValuesUpdated); - //TerrainData.ElevationValuesUpdated += OnElevationValuesUpdated; - - _unityMapTile.Material.SetFloat(ElevationMultiplier, useShaderElevation ? 1 : 0); + _unityMapTile.PropertyBlock.SetFloat(ElevationMultiplier, useShaderElevation ? 1 : 0); + _unityMapTile.ApplyPropertyBlock(); } public void OnTerrainUpdated() @@ -65,13 +99,13 @@ public void OnTerrainUpdated() return; _terrainTextureScaleOffset = _unityMapTile.CanonicalTileId.CalculateScaleOffsetAtZoom(TerrainData.TileId.Z); - - _unityMapTile.Material.SetVector(HeightTextureST, _terrainTextureScaleOffset); - _unityMapTile.Material.SetTexture(HeightTexture, TerrainData.Texture); - //_unityMapTile._material.SetFloat(_tileScaleFieldNameID, _unityMapTile.TileScale); - _unityMapTile.Material.SetFloat("_IsFallbackTexture", 0); - _unityMapTile.Material.SetFloat(ElevationChangeTime, Time.time); + var block = _unityMapTile.PropertyBlock; + block.SetVector(HeightTextureST, _terrainTextureScaleOffset); + block.SetTexture(HeightTexture, TerrainData.Texture); + block.SetFloat("_IsFallbackTexture", 0); + block.SetFloat(ElevationChangeTime, Time.time); + _unityMapTile.ApplyPropertyBlock(); } public void OnElevationValuesUpdated() @@ -91,7 +125,8 @@ public TerrainData GetAndClearTerrainData() return null; TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; - _unityMapTile.Material.SetTexture(HeightTexture, Texture2D.grayTexture); + _unityMapTile.PropertyBlock.SetTexture(HeightTexture, Texture2D.grayTexture); + _unityMapTile.ApplyPropertyBlock(); var rd = TerrainData; TerrainData = null; return rd; @@ -99,34 +134,62 @@ public TerrainData GetAndClearTerrainData() public float QueryHeightData(float x, float y) { - if (TerrainData != null && TerrainData.ElevationValues.Length > 0) + var values = TerrainData?.ElevationValues; + if (values == null || values.Length == 0) { - var width = (int)Mathf.Sqrt(TerrainData.ElevationValues.Length); - var sectionWidth = width * _terrainTextureScaleOffset.x - 1; - var padding = width * new Vector2(_terrainTextureScaleOffset.z, _terrainTextureScaleOffset.w); - - var xx = padding.x + (x * sectionWidth); - var yy = padding.y + (y * sectionWidth); - - var index = (int) yy * width - + (int) xx; - if (TerrainData.ElevationValues.Length <= index) - { - return 0; - } - else - { - return TerrainData.ElevationValues[(int) yy * width + (int) xx]; - } - + return 0; } - return 0; + + var width = (int)Mathf.Sqrt(values.Length); + var sectionWidth = width * _terrainTextureScaleOffset.x - 1f; + var xx = _terrainTextureScaleOffset.z * width + x * sectionWidth; + var yy = _terrainTextureScaleOffset.w * width + y * sectionWidth; + return BilinearSampleElevation(values, width, xx, yy); + } + + /// + /// Bilinearly samples the decoded elevation array at (xx, yy) in data-texture + /// pixel space. Render tiles share a coarser data tile with 16 neighbors, so a + /// 129-vertex mesh routinely samples a ~64×64 data sub-region — multiple verts + /// land inside the same texel. Nearest-neighbor sampling in that regime produces + /// visible stepping; the four-corner lerp smooths it out for ~4 reads + 3 lerps + /// per sample. + /// + private static float BilinearSampleElevation(float[] values, int width, float xx, float yy) + { + if (xx < 0f) xx = 0f; else if (xx > width - 1) xx = width - 1; + if (yy < 0f) yy = 0f; else if (yy > width - 1) yy = width - 1; + + var x0 = (int)xx; + var y0 = (int)yy; + var x1 = x0 + 1; if (x1 >= width) x1 = width - 1; + var y1 = y0 + 1; if (y1 >= width) y1 = width - 1; + var fx = xx - x0; + var fy = yy - y0; + + var row0 = y0 * width; + var row1 = y1 * width; + var h00 = values[row0 + x0]; + var h10 = values[row0 + x1]; + var h01 = values[row1 + x0]; + var h11 = values[row1 + x1]; + var h0 = h00 + (h10 - h00) * fx; + var h1 = h01 + (h11 - h01) * fx; + return h0 + (h1 - h0) * fy; } + /// + /// Hard-disable terrain for this tile: zeroes the shader elevation multiplier, + /// unsubscribes from , and drops the + /// cached reference. Call when the tile's zoom falls + /// outside the supported range so nothing lingers on it. + /// public void DisableTerrain() { State = TileContainerState.Final; - _unityMapTile.Material.SetFloat(ElevationMultiplier, 0); + GetAndClearTerrainData(); + _unityMapTile.PropertyBlock.SetFloat(ElevationMultiplier, 0); + _unityMapTile.ApplyPropertyBlock(); } public void OnDestroy() diff --git a/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/ElevatedTerrainShader.shadergraph b/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/ElevatedTerrainShader.shadergraph index 6458a0e0e..623b18e99 100644 --- a/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/ElevatedTerrainShader.shadergraph +++ b/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/ElevatedTerrainShader.shadergraph @@ -155,12 +155,6 @@ }, { "m_Id": "60b082478380496b992d5fa1caa35112" - }, - { - "m_Id": "7cb37beeca794a5b89fb91fdcb9568fc" - }, - { - "m_Id": "4861c23078fe4af0a8fde3e41d7c6f90" } ], "m_GroupDatas": [ @@ -215,20 +209,6 @@ "m_SlotId": 0 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "0842a57d9c2a43cba116748f9622c197" - }, - "m_SlotId": 0 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "7cb37beeca794a5b89fb91fdcb9568fc" - }, - "m_SlotId": 1 - } - }, { "m_OutputSlot": { "m_Node": { @@ -285,20 +265,6 @@ "m_SlotId": 1 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "4861c23078fe4af0a8fde3e41d7c6f90" - }, - "m_SlotId": 3 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "293d0108d9584aa484fde050460d9f45" - }, - "m_SlotId": 0 - } - }, { "m_OutputSlot": { "m_Node": { @@ -495,20 +461,6 @@ "m_SlotId": 2 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "7cb37beeca794a5b89fb91fdcb9568fc" - }, - "m_SlotId": 2 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "4861c23078fe4af0a8fde3e41d7c6f90" - }, - "m_SlotId": 0 - } - }, { "m_OutputSlot": { "m_Node": { @@ -672,9 +624,9 @@ }, "m_InputSlot": { "m_Node": { - "m_Id": "4861c23078fe4af0a8fde3e41d7c6f90" + "m_Id": "293d0108d9584aa484fde050460d9f45" }, - "m_SlotId": 2 + "m_SlotId": 0 } }, { @@ -1194,30 +1146,6 @@ "m_Labels": [] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "137feff6cdc24354b69217b248eb264b", - "m_Id": 1, - "m_DisplayName": "True", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "True", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 1.0, - "z": 0.0, - "w": 1.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", @@ -1802,30 +1730,6 @@ ] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "2c9edab1d38c47ab996ec26fa314cb8b", - "m_Id": 2, - "m_DisplayName": "False", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "False", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Texture2DInputMaterialSlot", @@ -2103,67 +2007,6 @@ ] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.BranchNode", - "m_ObjectId": "4861c23078fe4af0a8fde3e41d7c6f90", - "m_Group": { - "m_Id": "" - }, - "m_Name": "Branch", - "m_DrawState": { - "m_Expanded": true, - "m_Position": { - "serializedVersion": "2", - "x": 779.5, - "y": 340.0, - "width": 170.0, - "height": 142.0 - } - }, - "m_Slots": [ - { - "m_Id": "cf460984f0b04abc9a1eb76b4653c58d" - }, - { - "m_Id": "137feff6cdc24354b69217b248eb264b" - }, - { - "m_Id": "2c9edab1d38c47ab996ec26fa314cb8b" - }, - { - "m_Id": "78558bf43c9444daa1ec09e2e4a30e81" - } - ], - "synonyms": [ - "switch", - "if", - "else" - ], - "m_Precision": 0, - "m_PreviewExpanded": false, - "m_DismissedVersion": 0, - "m_PreviewMode": 0, - "m_CustomColors": { - "m_SerializableColors": [] - } -} - -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", - "m_ObjectId": "4a6066bf990a4d159d9659b683783b90", - "m_Id": 0, - "m_DisplayName": "A", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "A", - "m_StageCapability": 3, - "m_Value": 0.0, - "m_DefaultValue": 0.0, - "m_Labels": [] -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", @@ -3130,74 +2973,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "78558bf43c9444daa1ec09e2e4a30e81", - "m_Id": 3, - "m_DisplayName": "Out", - "m_SlotType": 1, - "m_Hidden": false, - "m_ShaderOutputName": "Out", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.ComparisonNode", - "m_ObjectId": "7cb37beeca794a5b89fb91fdcb9568fc", - "m_Group": { - "m_Id": "" - }, - "m_Name": "Comparison", - "m_DrawState": { - "m_Expanded": true, - "m_Position": { - "serializedVersion": "2", - "x": 605.0, - "y": 259.0, - "width": 145.0, - "height": 135.5 - } - }, - "m_Slots": [ - { - "m_Id": "4a6066bf990a4d159d9659b683783b90" - }, - { - "m_Id": "da61220af4404dc5bb72371b8ca2a026" - }, - { - "m_Id": "e5a3f628c5774b608259c77d07beee34" - } - ], - "synonyms": [ - "equal", - "greater than", - "less than" - ], - "m_Precision": 0, - "m_PreviewExpanded": true, - "m_DismissedVersion": 0, - "m_PreviewMode": 0, - "m_CustomColors": { - "m_SerializableColors": [] - }, - "m_ComparisonType": 0 -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", @@ -4288,8 +4063,8 @@ "m_ObjectId": "c2b337bafb404d6c929509f7ce06025b", "m_Title": "default sampling causes errors", "m_Position": { - "x": -587.0, - "y": -253.99996948242188 + "x": -586.9999389648438, + "y": -253.5 } } @@ -4442,20 +4217,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.BooleanMaterialSlot", - "m_ObjectId": "cf460984f0b04abc9a1eb76b4653c58d", - "m_Id": 0, - "m_DisplayName": "Predicate", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "Predicate", - "m_StageCapability": 3, - "m_Value": false, - "m_DefaultValue": false -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.PropertyNode", @@ -4608,8 +4369,8 @@ "m_ObjectId": "d60fae789e9b4436b8bea1089238c3a4", "m_Title": "Color", "m_Position": { - "x": -179.76116943359376, - "y": 805.5347900390625 + "x": -179.9998779296875, + "y": 805.5000610351563 } } @@ -4762,21 +4523,6 @@ ] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", - "m_ObjectId": "da61220af4404dc5bb72371b8ca2a026", - "m_Id": 1, - "m_DisplayName": "B", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "B", - "m_StageCapability": 3, - "m_Value": 0.0, - "m_DefaultValue": 0.0, - "m_Labels": [] -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", @@ -4893,20 +4639,6 @@ "m_Labels": [] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.BooleanMaterialSlot", - "m_ObjectId": "e5a3f628c5774b608259c77d07beee34", - "m_Id": 2, - "m_DisplayName": "Out", - "m_SlotType": 1, - "m_Hidden": false, - "m_ShaderOutputName": "Out", - "m_StageCapability": 3, - "m_Value": false, - "m_DefaultValue": false -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", diff --git a/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/MapboxTerrainBilinearSampling.shadersubgraph b/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/MapboxTerrainBilinearSampling.shadersubgraph index 7a6bb26ff..27383eb36 100644 --- a/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/MapboxTerrainBilinearSampling.shadersubgraph +++ b/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/MapboxTerrainBilinearSampling.shadersubgraph @@ -237,12 +237,6 @@ { "m_Id": "95601c21895b45c79d7bf1b0e1f87915" }, - { - "m_Id": "5bfc6c34651340ba82aac1304e277bec" - }, - { - "m_Id": "1e3bd0f2cb1444ed84a95d0d8529b787" - }, { "m_Id": "f401cfa820c649fab90cf2060324b8f4" }, @@ -599,20 +593,6 @@ "m_SlotId": 1 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "1e3bd0f2cb1444ed84a95d0d8529b787" - }, - "m_SlotId": 1 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "f401cfa820c649fab90cf2060324b8f4" - }, - "m_SlotId": 0 - } - }, { "m_OutputSlot": { "m_Node": { @@ -1201,20 +1181,6 @@ "m_SlotId": 2 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "5bfc6c34651340ba82aac1304e277bec" - }, - "m_SlotId": 1 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "f401cfa820c649fab90cf2060324b8f4" - }, - "m_SlotId": 1 - } - }, { "m_OutputSlot": { "m_Node": { @@ -1602,9 +1568,9 @@ }, "m_InputSlot": { "m_Node": { - "m_Id": "5bfc6c34651340ba82aac1304e277bec" + "m_Id": "f401cfa820c649fab90cf2060324b8f4" }, - "m_SlotId": 0 + "m_SlotId": 1 } }, { @@ -1658,7 +1624,7 @@ }, "m_InputSlot": { "m_Node": { - "m_Id": "1e3bd0f2cb1444ed84a95d0d8529b787" + "m_Id": "f401cfa820c649fab90cf2060324b8f4" }, "m_SlotId": 0 } @@ -3439,7 +3405,7 @@ "m_ShaderOutputName": "A", "m_StageCapability": 3, "m_Value": { - "x": 1.0, + "x": 2.0, "y": 0.0, "z": 0.0, "w": 0.0 @@ -3610,7 +3576,7 @@ "m_ShaderOutputName": "A", "m_StageCapability": 3, "m_Value": { - "x": 1.0, + "x": 2.0, "y": 0.0, "z": 0.0, "w": 0.0 @@ -4027,10 +3993,10 @@ "m_Expanded": true, "m_Position": { "serializedVersion": "2", - "x": 960.0, - "y": -1482.0001220703125, + "x": 830.9999389648438, + "y": -2427.499755859375, "width": 101.0, - "height": 125.0001220703125 + "height": 101.0 } }, "m_Slots": [ @@ -4373,42 +4339,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.NormalizeNode", - "m_ObjectId": "1e3bd0f2cb1444ed84a95d0d8529b787", - "m_Group": { - "m_Id": "" - }, - "m_Name": "Normalize", - "m_DrawState": { - "m_Expanded": true, - "m_Position": { - "serializedVersion": "2", - "x": 436.9999084472656, - "y": -2220.500244140625, - "width": 131.50003051757813, - "height": 94.0 - } - }, - "m_Slots": [ - { - "m_Id": "5c9da9cc6e0a4488a187f0607f577096" - }, - { - "m_Id": "982f399221354351b5739e6e63fafe07" - } - ], - "synonyms": [], - "m_Precision": 0, - "m_PreviewExpanded": false, - "m_DismissedVersion": 0, - "m_PreviewMode": 0, - "m_CustomColors": { - "m_SerializableColors": [] - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", @@ -4436,9 +4366,9 @@ "m_Expanded": true, "m_Position": { "serializedVersion": "2", - "x": 765.5001220703125, - "y": -2302.000244140625, - "width": 131.5, + "x": 628.5, + "y": -2301.999755859375, + "width": 131.49993896484376, "height": 94.0 } }, @@ -5821,7 +5751,7 @@ "m_ShaderOutputName": "A", "m_StageCapability": 3, "m_Value": { - "x": 1.0, + "x": 2.0, "y": 0.0, "z": 0.0, "w": 0.0 @@ -5858,30 +5788,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "3cf82528fe81462f93066232bed3b068", - "m_Id": 0, - "m_DisplayName": "In", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "In", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.UVMaterialSlot", @@ -7236,66 +7142,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.NormalizeNode", - "m_ObjectId": "5bfc6c34651340ba82aac1304e277bec", - "m_Group": { - "m_Id": "" - }, - "m_Name": "Normalize", - "m_DrawState": { - "m_Expanded": true, - "m_Position": { - "serializedVersion": "2", - "x": 436.9999084472656, - "y": -2366.000244140625, - "width": 131.50003051757813, - "height": 94.0 - } - }, - "m_Slots": [ - { - "m_Id": "3cf82528fe81462f93066232bed3b068" - }, - { - "m_Id": "c011d63ed6f54f1e98ef4155088061ff" - } - ], - "synonyms": [], - "m_Precision": 0, - "m_PreviewExpanded": false, - "m_DismissedVersion": 0, - "m_PreviewMode": 0, - "m_CustomColors": { - "m_SerializableColors": [] - } -} - -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "5c9da9cc6e0a4488a187f0607f577096", - "m_Id": 0, - "m_DisplayName": "In", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "In", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", @@ -9449,8 +9295,8 @@ "m_ObjectId": "8f619ecde74e498bbff41c586ba67e16", "m_Title": "ambient occlusion like effect", "m_Position": { - "x": -1972.0001220703125, - "y": -68.0 + "x": -2524.0, + "y": -69.0 } } @@ -10012,30 +9858,6 @@ "m_BareResource": false } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "982f399221354351b5739e6e63fafe07", - "m_Id": 1, - "m_DisplayName": "Out", - "m_SlotType": 1, - "m_Hidden": false, - "m_ShaderOutputName": "Out", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", @@ -11523,7 +11345,7 @@ "m_ShaderOutputName": "A", "m_StageCapability": 3, "m_Value": { - "x": 1.0, + "x": 2.0, "y": 0.0, "z": 0.0, "w": 0.0 @@ -12065,30 +11887,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "c011d63ed6f54f1e98ef4155088061ff", - "m_Id": 1, - "m_DisplayName": "Out", - "m_SlotType": 1, - "m_Hidden": false, - "m_ShaderOutputName": "Out", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.DivideNode", @@ -14999,8 +14797,8 @@ "m_ObjectId": "f3bbc84efc26440dbb48712a59f0bacf", "m_Title": "older elevation and color section", "m_Position": { - "x": -2350.5, - "y": -1693.5 + "x": -2350.0, + "y": -1694.0 } } @@ -15016,9 +14814,9 @@ "m_Expanded": true, "m_Position": { "serializedVersion": "2", - "x": 611.0, - "y": -2306.000244140625, - "width": 129.49993896484376, + "x": 473.9999694824219, + "y": -2305.999755859375, + "width": 129.50003051757813, "height": 118.0 } }, diff --git a/Runtime/Mapbox/VectorModule/Utility/Attributes/GameObjectLayerAttribute.cs b/Runtime/Mapbox/BaseModule/Utilities/Attributes/GameObjectLayerAttribute.cs similarity index 100% rename from Runtime/Mapbox/VectorModule/Utility/Attributes/GameObjectLayerAttribute.cs rename to Runtime/Mapbox/BaseModule/Utilities/Attributes/GameObjectLayerAttribute.cs diff --git a/Runtime/Mapbox/VectorModule/Utility/Attributes/GameObjectLayerAttribute.cs.meta b/Runtime/Mapbox/BaseModule/Utilities/Attributes/GameObjectLayerAttribute.cs.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Utility/Attributes/GameObjectLayerAttribute.cs.meta rename to Runtime/Mapbox/BaseModule/Utilities/Attributes/GameObjectLayerAttribute.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Utilities/FlatTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Utilities/FlatTerrainStrategy.cs index c0a255fd4..f81b4d31d 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/FlatTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/FlatTerrainStrategy.cs @@ -1,13 +1,19 @@ -using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Unity; using Mapbox.ImageModule.Terrain.Settings; using Mapbox.ImageModule.Terrain.TerrainStrategies; using UnityEngine; namespace Mapbox.BaseModule.Utilities { + /// + /// Terrain strategy used when no terrain module is present. Writes a shared 4-vertex + /// flat quad into each tile's mesh and (optionally) shares that quad as a MeshCollider. + /// Has no elevation sampling; ignores SimplificationFactor. + /// public class FlatTerrainStrategy : TerrainStrategy { MeshDataArray _cachedQuad; + private TerrainColliderOptions _colliderOptions; public int RequiredVertexCount => 4; @@ -15,13 +21,31 @@ public FlatTerrainStrategy() { BuildQuad(); } - + + /// + /// Captures the subset of this strategy cares + /// about (currently just collider options). Safe to pass null. + /// + public override void Initialize(ElevationLayerProperties elOptions) + { + if (elOptions != null) + { + _colliderOptions = elOptions.colliderOptions; + } + } + + /// + /// Writes the flat quad into 's mesh if not already present, + /// and optionally attaches a MeshCollider backed by the same quad. The + /// flag is unused — flat terrain has no + /// elevation. + /// public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) { var meshFilter = tile.MeshFilter; if (meshFilter.sharedMesh.vertexCount != RequiredVertexCount) { - + var sharedMesh = tile.MeshFilter.sharedMesh; sharedMesh.Clear(); @@ -36,12 +60,24 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) } tile.MeshVertexCount = RequiredVertexCount; + + if (_colliderOptions != null && _colliderOptions.addCollider) + { + var meshCollider = tile.GetComponent(); + if (meshCollider == null) + { + meshCollider = tile.gameObject.AddComponent(); + } + // The flat quad is already the collision surface; reuse it directly rather + // than allocating a dedicated mesh as ElevatedTerrainStrategy does. + meshCollider.sharedMesh = meshFilter.sharedMesh; + } } - + private void BuildQuad() { var size = 1; - + //32 //01 var verts = new Vector3[4]; diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSource.cs b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs index ee4c1efc8..3f838a686 100644 --- a/Runtime/Mapbox/CustomImageryModule/CustomSource.cs +++ b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs @@ -21,14 +21,7 @@ public CustomSource(CustomSourceSettings customSourceSettings, DataFetchingManag protected override RasterTile CreateTile(CanonicalTileId tileId, string tilesetId) { - if (_customSourceSettings.InvertY) - { - return new CustomTMSTile(_customSourceSettings.UrlFormat, tileId, tilesetId, true); - } - else - { - return new RasterTile(tileId, tilesetId, true); - } + return new CustomTMSTile(_customSourceSettings.UrlFormat, tileId, tilesetId, true, _customSourceSettings.InvertY, _customSourceSettings.IsMapboxService); } protected override RasterData CreateRasterDataWrapper(RasterTile tile) diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs index f2ac463b9..b28398fdd 100644 --- a/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs +++ b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs @@ -10,5 +10,7 @@ public class CustomSourceSettings public string UrlFormat; [Tooltip("Invert Y axis coordinates for TMS coordinate system, which starts from bottom left and grows to top-right")] public bool InvertY; + [Tooltip("Enable when the URL points to a Mapbox-hosted tileset (api.mapbox.com). Appends access_token and sku query parameters automatically.")] + public bool IsMapboxService; } } \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs index 8f3b789f0..5c096f890 100644 --- a/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs +++ b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs @@ -12,9 +12,13 @@ namespace Mapbox.CustomImageryModule public class CustomTMSTile : RasterTile { private string _urlFormat; - public CustomTMSTile(string urlFormat, CanonicalTileId tileId, string tilesetId, bool useNonReadableTexture) : base(tileId, tilesetId, useNonReadableTexture) + private bool _invertY; + private bool _isMapboxService; + public CustomTMSTile(string urlFormat, CanonicalTileId tileId, string tilesetId, bool useNonReadableTexture, bool invertY, bool isMapboxService) : base(tileId, tilesetId, useNonReadableTexture) { _urlFormat = urlFormat; + _invertY = invertY; + _isMapboxService = isMapboxService; } public override void Initialize(IFileSource fileSource, Action p) @@ -22,14 +26,16 @@ public override void Initialize(IFileSource fileSource, Action + /// Custom inspector for CustomTerrainLayerModuleScript. Shares rendering logic + /// with the main terrain editor via . + /// + [CustomEditor(typeof(CustomTerrainLayerModuleScript))] + public class CustomTerrainLayerModuleScriptEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + serializedObject.Update(); + + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_Script")); + } + + DrawPropertiesExcluding(serializedObject, "m_Script", "Settings"); + + var settingsProp = serializedObject.FindProperty("Settings"); + if (settingsProp != null) + { + TerrainSettingsInspectorHelper.Draw(settingsProp); + } + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs.meta b/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs.meta new file mode 100644 index 000000000..9fa7ef55b --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c8ba464cece144a6a06c5456579af2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef index bc263dd76..6fff7ca1b 100644 --- a/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef +++ b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef @@ -2,7 +2,8 @@ "name": "MapboxCustomImageryModule.Editor", "rootNamespace": "", "references": [ - "GUID:60dcad938ceef450e9b8d773e5c61b99" + "GUID:60dcad938ceef450e9b8d773e5c61b99", + "GUID:15fe16679907f4627ab38b784df8ca99" ], "includePlatforms": [ "Editor" diff --git a/Runtime/Mapbox/ImageModule/Editor.meta b/Runtime/Mapbox/ImageModule/Editor.meta new file mode 100644 index 000000000..3638540cb --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee3afe987f07c4883bfcd9948b4718c4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef b/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef new file mode 100644 index 000000000..d83cacdf6 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "MapboxImageryModule.Editor", + "rootNamespace": "", + "references": [ + "GUID:5ff00896142b94bc7a58a7c72b2faf57", + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef.meta b/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef.meta new file mode 100644 index 000000000..87513c2b4 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 15fe16679907f4627ab38b784df8ca99 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs new file mode 100644 index 000000000..635b6348e --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs @@ -0,0 +1,108 @@ +using System.Linq; +using Mapbox.ImageModule.Terrain.Settings; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Inspector drawer for fields tagged with . + /// Renders a dropdown of distinct-grid presets (each halving the resolution) and a + /// live HelpBox describing the resulting grid, upgrading to a warning at coarse values. + /// Legacy non-preset serialized values are snapped to the nearest preset. + /// + [CustomPropertyDrawer(typeof(SimplificationFactorAttribute))] + public class SimplificationFactorDrawer : PropertyDrawer + { + private const float Spacing = 2f; + + // Only factors that produce a distinct grid size are worth exposing — everything in + // between collapses via integer division. Each entry halves the grid resolution. + private static readonly int[] PresetFactors = { 1, 2, 4, 8, 16, 32, 64, 128 }; + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + var attr = (SimplificationFactorAttribute)attribute; + var factors = PresetFactors.Where(f => f >= attr.Min && f <= attr.Max).ToArray(); + var labels = factors.Select(f => new GUIContent(BuildLabel(f, attr.VertexBase))).ToArray(); + + // Snap legacy non-preset values to the nearest preset so the dropdown always + // matches a concrete entry. + var current = property.intValue; + if (System.Array.IndexOf(factors, current) < 0) + { + property.intValue = NearestPreset(factors, current); + current = property.intValue; + } + + var fieldRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + property.intValue = EditorGUI.IntPopup(fieldRect, label, current, labels, factors); + + var (message, severity) = BuildMessage(property.intValue, attr.VertexBase); + var helpHeight = CalcHelpBoxHeight(message, position.width); + var helpRect = new Rect(position.x, fieldRect.yMax + Spacing, position.width, helpHeight); + EditorGUI.HelpBox(helpRect, message, severity); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var attr = (SimplificationFactorAttribute)attribute; + var (message, _) = BuildMessage(property.intValue, attr.VertexBase); + return EditorGUIUtility.singleLineHeight + Spacing + CalcHelpBoxHeight(message, EditorGUIUtility.currentViewWidth - 40f); + } + + private static string BuildLabel(int factor, int vertexBase) + { + var sampleCount = vertexBase / Mathf.Max(1, factor); + var gridSide = sampleCount + 1; + return $"{factor} ({gridSide}x{gridSide} verts)"; + } + + private static (string message, MessageType severity) BuildMessage(int factor, int vertexBase) + { + var clampedFactor = Mathf.Max(1, factor); + var sampleCount = vertexBase / clampedFactor; + var gridSide = sampleCount + 1; + var verts = gridSide * gridSide; + var baseLine = $"{gridSide}x{gridSide} vertex grid per tile ({verts} verts, {sampleCount}x{sampleCount} quads)."; + + if (sampleCount >= 64) + { + return (baseLine + " Highest visual quality — heaviest CPU/GPU cost, especially with colliders enabled.", MessageType.Info); + } + if (sampleCount >= 16) + { + return (baseLine + " Good balance of detail and performance.", MessageType.Info); + } + if (sampleCount >= 8) + { + return (baseLine + " Lower-density geometry — fine terrain details will be lost.", MessageType.Info); + } + return (baseLine + " Very coarse — terrain will look blocky and neighboring tiles may visibly mismatch at seams.", MessageType.Warning); + } + + private static int NearestPreset(int[] presets, int value) + { + var best = presets[0]; + var bestDiff = Mathf.Abs(best - value); + for (int i = 1; i < presets.Length; i++) + { + var diff = Mathf.Abs(presets[i] - value); + if (diff < bestDiff) + { + bestDiff = diff; + best = presets[i]; + } + } + return best; + } + + private static float CalcHelpBoxHeight(string message, float width) + { + var content = new GUIContent(message); + var style = EditorStyles.helpBox; + var textWidth = Mathf.Max(40f, width - 32f); + return Mathf.Max(EditorGUIUtility.singleLineHeight * 2f, style.CalcHeight(content, textWidth)); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs.meta b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs.meta new file mode 100644 index 000000000..02728c72f --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 75571eeda31dd488295a98b2985edfd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs new file mode 100644 index 000000000..60f1a3fef --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs @@ -0,0 +1,75 @@ +using Mapbox.ImageModule.Terrain.Settings; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Inspector drawer for . Grays out the + /// dedicated-layer options until addCollider is enabled, and further grays out + /// the layer dropdown until useDedicatedColliderLayer is enabled. Same reveal + /// pattern used by and + /// . + /// + [CustomPropertyDrawer(typeof(TerrainColliderOptions))] + public class TerrainColliderOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + var foldoutRect = new Rect(position.x, position.y, position.width, line); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label, true); + + if (property.isExpanded) + { + var addCollider = property.FindPropertyRelative("addCollider"); + var asyncBake = property.FindPropertyRelative("asyncBakeCollider"); + var useDedicated = property.FindPropertyRelative("useDedicatedColliderLayer"); + var colliderLayer = property.FindPropertyRelative("colliderLayerId"); + + EditorGUI.indentLevel++; + var y = position.y + line + spacing; + + EditorGUI.PropertyField(new Rect(position.x, y, position.width, line), addCollider); + y += line + spacing; + + using (new EditorGUI.DisabledScope(!addCollider.boolValue)) + { + EditorGUI.PropertyField(new Rect(position.x, y, position.width, line), asyncBake); + y += line + spacing; + + EditorGUI.PropertyField(new Rect(position.x, y, position.width, line), useDedicated); + y += line + spacing; + + using (new EditorGUI.DisabledScope(!useDedicated.boolValue)) + { + var layerHeight = EditorGUI.GetPropertyHeight(colliderLayer, true); + EditorGUI.PropertyField(new Rect(position.x, y, position.width, layerHeight), colliderLayer); + } + } + EditorGUI.indentLevel--; + } + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + if (!property.isExpanded) + { + return line; + } + + var colliderLayer = property.FindPropertyRelative("colliderLayerId"); + // foldout + addCollider + asyncBake + useDedicated + colliderLayer + return line + spacing + line + spacing + line + spacing + line + spacing + line + spacing + EditorGUI.GetPropertyHeight(colliderLayer, true); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs.meta b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs.meta new file mode 100644 index 000000000..2c414051a --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c02662f531ec9466abc6d9891a8f7120 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs new file mode 100644 index 000000000..2af15bcb0 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs @@ -0,0 +1,38 @@ +using Mapbox.Example.Scripts.ModuleBehaviours; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Custom inspector for TerrainLayerModuleScript. Delegates settings rendering + /// to so the extract-on-override pattern + /// and scene-aware warnings are consistent between terrain variants. + /// + [CustomEditor(typeof(TerrainLayerModuleScript))] + public class TerrainLayerModuleScriptEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // Standard script field. + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_Script")); + } + + // Draw everything except Settings with the default renderer. + DrawPropertiesExcluding(serializedObject, "m_Script", "Settings"); + + // Custom rendering for Settings so we can intercept ExtractCpuElevationData. + var settingsProp = serializedObject.FindProperty("Settings"); + if (settingsProp != null) + { + TerrainSettingsInspectorHelper.Draw(settingsProp); + } + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs.meta b/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs.meta new file mode 100644 index 000000000..b235c9e0f --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53d6fa2e8e0194e65a0de0268dc5bfbb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs new file mode 100644 index 000000000..269ec5e05 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs @@ -0,0 +1,138 @@ +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Renders a TerrainLayerModuleSettings SerializedProperty with two inspector + /// enhancements: (1) the ExtractCpuElevationData toggle is force-checked and + /// disabled when another setting requires CPU elevation; (2) a warning HelpBox appears + /// when the scene contains a module that typically needs CPU elevation but extraction + /// is user-disabled. Shared by both terrain-settings editors. + /// + public static class TerrainSettingsInspectorHelper + { + private const string ExtractFieldName = "ExtractCpuElevationData"; + private const string UseShaderFieldName = "UseShaderTerrain"; + private const string ElevationPropsFieldName = "ElevationLayerProperties"; + private const string ColliderOptionsFieldName = "colliderOptions"; + private const string AddColliderFieldName = "addCollider"; + + public static void Draw(SerializedProperty settingsProp) + { + settingsProp.isExpanded = EditorGUILayout.Foldout(settingsProp.isExpanded, settingsProp.displayName, true); + if (!settingsProp.isExpanded) + { + return; + } + + var useShaderProp = settingsProp.FindPropertyRelative(UseShaderFieldName); + var colliderProp = settingsProp + .FindPropertyRelative(ElevationPropsFieldName) + ?.FindPropertyRelative(ColliderOptionsFieldName) + ?.FindPropertyRelative(AddColliderFieldName); + + var useShaderOff = useShaderProp != null && !useShaderProp.boolValue; + var colliderOn = colliderProp != null && colliderProp.boolValue; + var forcedOn = useShaderOff || colliderOn; + var reason = useShaderOff + ? "CPU-elevation rendering (UseShaderTerrain is off)" + : "the terrain collider (addCollider is on)"; + + EditorGUI.indentLevel++; + var child = settingsProp.Copy(); + var end = settingsProp.GetEndProperty(); + child.NextVisible(true); + while (!SerializedProperty.EqualContents(child, end)) + { + if (child.name == ExtractFieldName && forcedOn) + { + // Persist the forced state into the serialized value so runtime code that + // reads ExtractCpuElevationData sees the effective configuration (and so + // TerrainLayerModule.Initialize doesn't log an override warning). + if (!child.boolValue) + { + child.boolValue = true; + } + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.PropertyField(child); + } + EditorGUILayout.HelpBox( + $"Extraction is force-enabled by {reason}. CPU elevation will be decoded regardless of this checkbox.", + MessageType.Info); + } + else + { + EditorGUILayout.PropertyField(child, true); + } + + if (!child.NextVisible(false)) + { + break; + } + } + EditorGUI.indentLevel--; + + DrawSceneNeedsExtractionWarning(settingsProp); + } + + // Warns if extraction is genuinely user-disabled (box off AND nothing forces it on) + // while the scene contains modules that typically need ground heights. + private static void DrawSceneNeedsExtractionWarning(SerializedProperty settingsProp) + { + var extractProp = settingsProp.FindPropertyRelative(ExtractFieldName); + if (extractProp == null || extractProp.boolValue) + { + return; + } + var useShaderProp = settingsProp.FindPropertyRelative(UseShaderFieldName); + var colliderProp = settingsProp + .FindPropertyRelative(ElevationPropsFieldName) + ?.FindPropertyRelative(ColliderOptionsFieldName) + ?.FindPropertyRelative(AddColliderFieldName); + var forcedOn = + (useShaderProp != null && !useShaderProp.boolValue) || + (colliderProp != null && colliderProp.boolValue); + if (forcedOn) + { + return; + } + + if (SceneHasFeaturesNeedingCpuElevation(out var detected)) + { + EditorGUILayout.HelpBox( + $"CPU elevation extraction is disabled, but this scene contains a {detected} that typically snaps to terrain. " + + "Ground-clamped features will be placed at Y=0 and the elevation query APIs will return 0. " + + "Re-enable ExtractCpuElevationData unless you are sure nothing in this scene needs ground heights.", + MessageType.Warning); + } + } + + private static readonly string[] WatchedTypeNames = + { + "VectorLayerModuleScript", + "MapboxComponentsModuleScript" + }; + + private static bool SceneHasFeaturesNeedingCpuElevation(out string detectedTypeName) + { + detectedTypeName = null; + var all = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + foreach (var mb in all) + { + if (mb == null) continue; + var name = mb.GetType().Name; + for (int i = 0; i < WatchedTypeNames.Length; i++) + { + if (name == WatchedTypeNames[i]) + { + detectedTypeName = name; + return true; + } + } + } + return false; + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs.meta b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs.meta new file mode 100644 index 000000000..65548fb81 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c5f5594527a944408ba2d33d4134cde +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs new file mode 100644 index 000000000..aa38463ef --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs @@ -0,0 +1,62 @@ +using Mapbox.ImageModule.Terrain.Settings; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Inspector drawer for . Mirrors the + /// pattern: wallHeight is always visible + /// but grayed out until isActive is enabled. + /// + [CustomPropertyDrawer(typeof(TerrainSideWallOptions))] + public class TerrainSideWallOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + var foldoutRect = new Rect(position.x, position.y, position.width, line); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label, true); + + if (property.isExpanded) + { + var isActive = property.FindPropertyRelative("isActive"); + var wallHeight = property.FindPropertyRelative("wallHeight"); + + EditorGUI.indentLevel++; + var y = position.y + line + spacing; + + var activeRect = new Rect(position.x, y, position.width, line); + EditorGUI.PropertyField(activeRect, isActive); + y += line + spacing; + + var heightRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(wallHeight, true)); + using (new EditorGUI.DisabledScope(!isActive.boolValue)) + { + EditorGUI.PropertyField(heightRect, wallHeight); + } + EditorGUI.indentLevel--; + } + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + if (!property.isExpanded) + { + return line; + } + + var wallHeight = property.FindPropertyRelative("wallHeight"); + return line + spacing + line + spacing + EditorGUI.GetPropertyHeight(wallHeight, true); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs.meta b/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs.meta new file mode 100644 index 000000000..fe914cf83 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 616c73c44390a4d9999d07a80b1c02ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs new file mode 100644 index 000000000..5d64df175 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs @@ -0,0 +1,64 @@ +using Mapbox.ImageModule.Terrain.Settings; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Inspector drawer for . Shows addToLayer as a + /// checkbox and the layerId dropdown (via the shared GameObjectLayer + /// drawer) grayed-out when addToLayer is off, so users can see the selected + /// layer but can't change it until they opt in. + /// + [CustomPropertyDrawer(typeof(UnityLayerOptions))] + public class UnityLayerOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + var foldoutRect = new Rect(position.x, position.y, position.width, line); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label, true); + + if (property.isExpanded) + { + var addToLayer = property.FindPropertyRelative("addToLayer"); + var layerId = property.FindPropertyRelative("layerId"); + + EditorGUI.indentLevel++; + var y = position.y + line + spacing; + + var addRect = new Rect(position.x, y, position.width, line); + EditorGUI.PropertyField(addRect, addToLayer); + y += line + spacing; + + var layerRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(layerId, true)); + using (new EditorGUI.DisabledScope(!addToLayer.boolValue)) + { + EditorGUI.PropertyField(layerRect, layerId); + } + EditorGUI.indentLevel--; + } + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + if (!property.isExpanded) + { + return line; + } + + var layerId = property.FindPropertyRelative("layerId"); + // foldout + addToLayer + layerId (always rendered, just grayed out when inactive). + return line + spacing + line + spacing + EditorGUI.GetPropertyHeight(layerId, true); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs.meta b/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs.meta new file mode 100644 index 000000000..cd3310a02 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55d9694a92c84468f9c28a997b2320bf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/MapboxImageModule.asmdef b/Runtime/Mapbox/ImageModule/MapboxImageModule.asmdef index 401a0f3c9..926ca1fdd 100644 --- a/Runtime/Mapbox/ImageModule/MapboxImageModule.asmdef +++ b/Runtime/Mapbox/ImageModule/MapboxImageModule.asmdef @@ -2,7 +2,8 @@ "name": "MapboxImageryModule", "rootNamespace": "", "references": [ - "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:36ca6af6e2b304d4090888554d6ce199" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index a018bbd23..cc4049abe 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -10,17 +10,30 @@ using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; using Mapbox.ImageModule.Terrain.TerrainStrategies; +using Mapbox.UnityMapService.DataSources; using UnityEngine; using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.ImageModule.Terrain { + /// + /// Terrain layer module. Orchestrates loading of terrain-RGB raster tiles from a + /// , configures CPU elevation extraction based on + /// settings, and hands each tile through a that produces + /// render + optional collider meshes. + /// public class TerrainLayerModule : ITerrainLayerModule { private TerrainLayerModuleSettings _settings; private Source _rasterSource; private HashSet _retainedTerrainTiles; private TerrainStrategy _terrainStrategy; + // One-shot guard so QueryElevation doesn't spam the console on every call when the + // user genuinely disabled CPU extraction. + private bool _elevationDisabledWarningLogged; + // Reused across GetDataId(IEnumerable) calls to avoid allocating a fresh HashSet + // plus iterator chain from Where/Select/Distinct on every tile-cover update. + private readonly HashSet _dataIdScratch = new HashSet(); public TerrainLayerModule(Source source, TerrainLayerModuleSettings settings) : base() { @@ -33,6 +46,17 @@ public TerrainLayerModule(Source source, TerrainLayerModuleSettings public virtual IEnumerator Initialize() { yield return _rasterSource.Initialize(); + if (_rasterSource is TerrainSource terrainSource) + { + terrainSource.ExtractCpuElevationData = _settings.NeedsCpuElevation; + } + if (!_settings.ExtractCpuElevationData && _settings.NeedsCpuElevation) + { + var reason = !_settings.UseShaderTerrain + ? "UseShaderTerrain is off (CPU-elevation rendering needs the data)" + : "colliderOptions.addCollider is on (terrain collider needs the data)"; + Debug.LogWarning($"[Mapbox] TerrainLayerModuleSettings.ExtractCpuElevationData is unchecked, but {reason}. Extraction has been force-enabled; the checkbox has no effect in this configuration."); + } _terrainStrategy.Initialize(_settings.ElevationLayerProperties); if(_settings.LoadBackgroundTextures) { @@ -47,13 +71,13 @@ public virtual void LoadTempTile(UnityMapTile unityTile) unityTile.TerrainContainer.DisableTerrain(); return; } - + var targetTileId = GetDataId(unityTile.CanonicalTileId); var parentTileId = targetTileId; for (int i = parentTileId.Z; i >= 2; i--) { parentTileId.MoveToParent(); - if (_rasterSource.GetInstantData(parentTileId, out var instantData) && instantData.IsElevationDataReady) + if (_rasterSource.GetInstantData(parentTileId, out var instantData) && IsTileReady(instantData)) { unityTile.TerrainContainer.SetTerrainData(instantData, _settings.UseShaderTerrain, TileContainerState.Temporary); _terrainStrategy.RegisterTile(unityTile, !_settings.UseShaderTerrain); @@ -61,7 +85,7 @@ public virtual void LoadTempTile(UnityMapTile unityTile) } } } - + public virtual bool LoadInstant(UnityMapTile unityTile) { if (IsZinSupportedRange(unityTile.CanonicalTileId.Z) == false) @@ -69,18 +93,29 @@ public virtual bool LoadInstant(UnityMapTile unityTile) unityTile.TerrainContainer.DisableTerrain(); return true; } - + var targetTileId = GetDataId(unityTile.CanonicalTileId); - if (_rasterSource.GetInstantData(targetTileId, out var instantData) && instantData.IsElevationDataReady) + if (_rasterSource.GetInstantData(targetTileId, out var instantData) && IsTileReady(instantData)) { - unityTile.TerrainContainer.SetTerrainData(instantData, _settings.UseShaderTerrain); + unityTile.TerrainContainer.SetTerrainData(instantData, _settings.UseShaderTerrain, TileContainerState.Final); _terrainStrategy.RegisterTile(unityTile, !_settings.UseShaderTerrain); return true; } - + return false; } + /// + /// Readiness gate used by and . + /// Shader-elevation rendering only needs the raster texture and can display a tile + /// as soon as the texture exists. CPU-elevation rendering still needs the per-pixel + /// float[] before displacing verts, so it must wait for extraction to finish. + /// + private bool IsTileReady(TerrainData data) + { + return _settings.UseShaderTerrain ? data.IsTextureReady : data.IsElevationDataReady; + } + public virtual bool RetainTiles(HashSet retainedTiles) { var isReady = true; @@ -102,6 +137,7 @@ public void UpdatePositioning(IMapInformation mapInfo) public void OnDestroy() { _rasterSource.OnDestroy(); + _terrainStrategy?.OnDestroy(); } //COROUTINE METHODS only used in initialization so far @@ -157,6 +193,12 @@ private CanonicalTileId GetDataId(CanonicalTileId tileId) /// public float QueryElevation(CanonicalTileId tileId, float x, float y) { + if (!_settings.NeedsCpuElevation && !_elevationDisabledWarningLogged) + { + _elevationDisabledWarningLogged = true; + Debug.LogWarning("[Mapbox] QueryElevation was called but TerrainLayerModuleSettings.ExtractCpuElevationData is disabled, so no CPU elevation data exists. Returning 0 for every query. Enable ExtractCpuElevationData if anything in your scene snaps to terrain (vector features, buildings, colliders, custom elevation lookups)."); + } + var originalTileId = tileId; var targetTileId = tileId; for (int i = 0; i < 5; i++) @@ -167,13 +209,25 @@ public float QueryElevation(CanonicalTileId tileId, float x, float y) } targetTileId.MoveToParent(); } - + return 0; } public IEnumerable GetDataId(IEnumerable tileIdList) { - return tileIdList.Where(x => IsZinSupportedRange(x.Z)).Select(GetDataId).Distinct(); + // With the data-zoom offset of 2, up to 16 render tile ids collapse to the + // same data id. Reuse the scratch set to avoid allocating a HashSet plus + // Where/Select/Distinct iterator chain per call. + _dataIdScratch.Clear(); + foreach (var tileId in tileIdList) + { + if (!IsZinSupportedRange(tileId.Z)) + { + continue; + } + _dataIdScratch.Add(GetDataId(tileId)); + } + return _dataIdScratch; } /// diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModuleSettings.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModuleSettings.cs index 6b85cc037..367082d4d 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModuleSettings.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModuleSettings.cs @@ -6,17 +6,44 @@ namespace Mapbox.ImageModule.Terrain { + /// + /// User-facing settings for the . Controls how terrain + /// raster tiles are fetched, whether CPU elevation data is decoded, and how tiles are + /// rendered (shader-displaced vs. CPU-displaced meshes). + /// [Serializable] public class TerrainLayerModuleSettings { + /// Runtime-only selector for the terrain tileset (not serialized). [NonSerialized] public ElevationSourceType SourceType = ElevationSourceType.MapboxTerrain; + + [Tooltip("Pre-fetch low-zoom world tiles on startup so the map has fallback imagery while higher-zoom tiles load. Leave off to minimize cold-start bandwidth; enable for polished first-paint. Default: off.")] public bool LoadBackgroundTextures = false; + + [Tooltip("ON (recommended): the GPU vertex shader displaces a flat grid using the terrain-RGB texture — cheapest visual elevation. OFF: the CPU writes displaced vertices before render, which forces ExtractCpuElevationData on and costs more per-tile CPU. Default: on.")] public bool UseShaderTerrain = true; + + [Tooltip("Decode each terrain-RGB tile into a CPU float[] so features can query ground height. ON (default) is required for: vector snap-to-terrain (buildings, roads, POIs), terrain colliders, CPU elevation rendering, and TryGetElevation/QueryElevation. Turn OFF only for purely-terrain-rendered apps to save ~65K pixel decodes and a ~256KB float[] per tile. Force-enabled (and shown disabled) whenever UseShaderTerrain is off or colliderOptions.addCollider is on.")] + public bool ExtractCpuElevationData = true; + + [Tooltip("Tileset id, retina flag, cache size, and data-zoom clamp for the terrain raster source.")] public ImageSourceSettings DataSettings; - - [Tooltip("Tile outside this range will be rejected.")] + + [Tooltip("Tiles whose zoom level is outside this [min, max] range will be skipped. Default (12, 16) keeps terrain loaded only in typical map-viewing zooms.")] public Vector2 RejectTilesOutsideZoom = new Vector2(12, 16); + [Tooltip("Terrain mesh grid density, skirts, collider, and Unity layer assignment. See the nested fields for details.")] public ElevationLayerProperties ElevationLayerProperties = new ElevationLayerProperties(); + + /// + /// True when CPU elevation extraction is required by at least one consumer: the user + /// explicitly enabled , or shader rendering is + /// off, or a terrain collider is requested. The module force-enables extraction on + /// the raster source at Initialize time based on this value. + /// + public bool NeedsCpuElevation => + ExtractCpuElevationData + || !UseShaderTerrain + || (ElevationLayerProperties != null && ElevationLayerProperties.colliderOptions.addCollider); } -} \ No newline at end of file +} diff --git a/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs b/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs index 0c0645b9a..1366605ce 100644 --- a/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs +++ b/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs @@ -1,80 +1,106 @@ using System; +using Mapbox.BaseModule.Data.DataFetchers; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; using UnityEngine; using UnityEngine.Rendering; using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.UnityMapService { - public class AsyncExtractElevationArray : IElevationDataExtractionStrategy - { - public void ExtractHeightData(Texture2D texture, Action callback = null) - { - AsyncGPUReadback.Request(texture, 0, (t) => - { - var width = t.width; - var data = t.GetData().ToArray(); - float[] heightData = new float[width * width]; - for (float y = 0; y < width; y++) - { - for (float x = 0; x < width; x++) - { - var xx = (x / width) * width; - var yy = (y / width) * width; - var index = ((int) yy * width) + (int) xx; + /// + /// Asynchronous terrain-RGB -> float[] elevation decoder using + /// AsyncGPUReadback. The GPU readback callback fires on the main thread with a + /// zero-copy NativeArray<Color32>; the decode loop runs there inside a + /// Burst-compiled via Run(). + /// + public class AsyncExtractElevationArray : IElevationDataExtractionStrategy + { + public void ExtractHeightData(Texture2D texture, Action callback = null) + { + AsyncGPUReadback.Request(texture, 0, (t) => + { + var width = t.width; + var data = t.GetData(); + var heightData = ElevationArrayPool.Rent(width * width); + RunDecodeJob(data, width, heightData, out _, out _); + callback?.Invoke(heightData); + }); + } - float r = data[index].g; - float g = data[index].b; - float b = data[index].a; - //the formula below is the same as Conversions.GetAbsoluteHeightFromColor but it's inlined for performance - heightData[(int) (y * width + x)] = (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); - //678 ==> 012345678 - //345 - //012 - } - } + public void ExtractHeightData(TerrainData terrainData) + { + ExtractHeightData(terrainData.Texture, terrainData); + } - callback?.Invoke(heightData); - }); - } + public void ExtractHeightData(Texture2D texture, TerrainData terrainData) + { + AsyncGPUReadback.Request(texture, 0, (t) => + { + var width = t.width; + var data = t.GetData(); + var heightData = ElevationArrayPool.Rent(width * width); + RunDecodeJob(data, width, heightData, out var min, out var max); + terrainData.SetElevationValues(heightData, min, max); + }); + } - public void ExtractHeightData(TerrainData terrainData) - { - ExtractHeightData(terrainData.Texture, terrainData); - } + private static void RunDecodeJob(NativeArray data, int width, float[] heightData, out float min, out float max) + { + var length = width * width; + var heightNative = new NativeArray(length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + var minMax = new NativeArray(2, Allocator.TempJob); + new DecodeTerrainColor32Job + { + Data = data, + Width = width, + HeightData = heightNative, + MinMax = minMax + }.Run(); + heightNative.CopyTo(heightData); + min = minMax[0]; + max = minMax[1]; + heightNative.Dispose(); + minMax.Dispose(); + } - public void ExtractHeightData(Texture2D texture, TerrainData terrainData) - { - AsyncGPUReadback.Request(texture, 0, (t) => - { - var min = float.MaxValue; - var max = float.MinValue; - var width = t.width; - var data = t.GetData().ToArray(); - float[] heightData = new float[width * width]; - for (float y = 0; y < width; y++) - { - for (float x = 0; x < width; x++) - { - var xx = (x / width) * width; - var yy = (y / width) * width; - var index = ((int) yy * width) + (int) xx; + /// + /// Burst-compiled decode for the async path. Mapbox terrain-RGB in the Color32 + /// readback is stored as (A, R, G, B) in (r, g, b, a) positions + /// (historical swizzle preserved from the pre-Burst implementation). Min/max are + /// accumulated inline. + /// + [BurstCompile] + private struct DecodeTerrainColor32Job : IJob + { + [ReadOnly] public NativeArray Data; + public int Width; + public NativeArray HeightData; + public NativeArray MinMax; - float r = data[index].g; - float g = data[index].b; - float b = data[index].a; - //the formula below is the same as Conversions.GetAbsoluteHeightFromColor but it's inlined for performance - var value = (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); - heightData[(int)(y * width + x)] = value; - if(value < min) min = value; - if(value > max) max = value; - //678 ==> 012345678 - //345 - //012 - } - } - - terrainData.SetElevationValues(heightData, min, max); - }); - } - } -} \ No newline at end of file + public void Execute() + { + var min = float.MaxValue; + var max = float.MinValue; + int idx = 0; + for (int y = 0; y < Width; y++) + { + for (int x = 0; x < Width; x++, idx++) + { + var c = Data[idx]; + float r = c.g; + float g = c.b; + float b = c.a; + var value = -10000f + (r * 65536f + g * 256f + b) * 0.1f; + HeightData[idx] = value; + if (value < min) min = value; + if (value > max) max = value; + } + } + MinMax[0] = min; + MinMax[1] = max; + } + } + } +} diff --git a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs index 885caaa60..7c544fe28 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs @@ -15,8 +15,19 @@ public class TerrainSource : ImageSource { protected ImageSourceSettings _settings; private IElevationDataExtractionStrategy _elevationDataExtractionStrategy; - - public TerrainSource(DataFetchingManager dataFetchingManager, MapboxCacheManager mapboxCacheManager, ImageSourceSettings settings) + + /// + /// When false, the per-pixel terrain-RGB -> float[] decode pass is skipped + /// entirely — saves ~65K pixel decodes and a ~256KB float[] per tile. The raster + /// Texture is still populated, so shader-elevation rendering keeps working; only + /// CPU consumers (collider builder, QueryElevation APIs, CPU-elevation mode, vector + /// snap-to-terrain modifiers) are disabled. Defaults to true for back-compat; + /// the parent overrides this at Initialize based on + /// its settings. + /// + public bool ExtractCpuElevationData { get; set; } = true; + + public TerrainSource(DataFetchingManager dataFetchingManager, MapboxCacheManager mapboxCacheManager, ImageSourceSettings settings) : base(dataFetchingManager, mapboxCacheManager, settings) { _settings = settings; @@ -26,7 +37,7 @@ public TerrainSource(DataFetchingManager dataFetchingManager, MapboxCacheManager } else { - _elevationDataExtractionStrategy = new SyncExtractElevationArray(); + _elevationDataExtractionStrategy = new SyncExtractElevationArray(); } } @@ -112,7 +123,7 @@ public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileI { terrainData = data; })); - if (terrainData != null && terrainData.Texture != null) + if (terrainData != null && terrainData.Texture != null && ExtractCpuElevationData) { yield return Runnable.Instance.StartCoroutine(ExtractElevationValues(terrainData)); } @@ -133,14 +144,17 @@ protected IEnumerator ExtractElevationValues(TerrainData data) protected override void TextureReceivedFromFile(TerrainData cacheItem) { base.TextureReceivedFromFile(cacheItem); - _elevationDataExtractionStrategy.ExtractHeightData(cacheItem); + if (ExtractCpuElevationData) + { + _elevationDataExtractionStrategy.ExtractHeightData(cacheItem); + } } protected override TerrainData TextureReceivedFromWeb(RasterTile tile) { var cacheItem = base.TextureReceivedFromWeb(tile); - if (cacheItem != null) + if (cacheItem != null && ExtractCpuElevationData) { _elevationDataExtractionStrategy.ExtractHeightData(cacheItem); } diff --git a/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs b/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs index 76a6fc6a2..9abc0ae81 100644 --- a/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs +++ b/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs @@ -1,81 +1,102 @@ using System; +using Mapbox.BaseModule.Data.DataFetchers; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; using UnityEngine; using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.UnityMapService { - public class SyncExtractElevationArray : IElevationDataExtractionStrategy - { - public void ExtractHeightData(Texture2D texture, Action callback) - { - byte[] rgbData = texture.GetRawTextureData(); - var width = texture.width; - float[] heightData = new float[width * width]; + /// + /// Synchronous terrain-RGB -> float[] elevation decoder. Used on platforms that don't + /// support AsyncGPUReadback. The decode loop runs inside a Burst-compiled + /// via Run() — main-thread execution preserves the sync + /// contract, but the hot loop is SIMD-vectorized native code rather than managed. + /// + public class SyncExtractElevationArray : IElevationDataExtractionStrategy + { + public void ExtractHeightData(Texture2D texture, Action callback) + { + var rgbData = texture.GetRawTextureData(); + var width = texture.width; + var heightData = ElevationArrayPool.Rent(width * width); + RunDecodeJob(rgbData, width, heightData, out _, out _); + callback?.Invoke(heightData); + } - for (float y = 0; y < width; y++) - { - for (float x = 0; x < width; x++) - { - var xx = (x / width) * width; - var yy = (y / width) * width; - var index = ((int) yy * width) + (int) xx; + public void ExtractHeightData(TerrainData terrainData) + { + ExtractHeightData(terrainData.Texture, terrainData); + } - float r = rgbData[index * 4 + 1]; - float g = rgbData[index * 4 + 2]; - float b = rgbData[index * 4 + 3]; - //var color = rgbData[index]; - // float r = color.g; - // float g = color.b; - // float b = color.a; - //the formula below is the same as Conversions.GetAbsoluteHeightFromColor but it's inlined for performance - heightData[(int) (y * width + x)] = (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); - //678 ==> 012345678 - //345 - //012 - } - } - callback?.Invoke(heightData); - } - - public void ExtractHeightData(TerrainData terrainData) - { - ExtractHeightData(terrainData.Texture, terrainData); - } - - public void ExtractHeightData(Texture2D texture, TerrainData terrainData) - { - var min = float.MaxValue; - var max = float.MinValue; - byte[] rgbData = texture.GetRawTextureData(); - var width = texture.width; - float[] heightData = new float[width * width]; + public void ExtractHeightData(Texture2D texture, TerrainData terrainData) + { + var rgbData = texture.GetRawTextureData(); + var width = texture.width; + var heightData = ElevationArrayPool.Rent(width * width); + RunDecodeJob(rgbData, width, heightData, out var min, out var max); + terrainData.SetElevationValues(heightData, min, max); + } - for (float y = 0; y < width; y++) - { - for (float x = 0; x < width; x++) - { - var xx = (x / width) * width; - var yy = (y / width) * width; - var index = ((int) yy * width) + (int) xx; + // Copies the Burst-decoded result (NativeArray) back into the pooled managed array + // our TerrainData API still uses. The NativeArray lives only for the job's + // lifetime; one temp alloc + memcpy per tile extraction. The job itself computes + // min/max in the same pass so we don't re-iterate on the C# side. + private static void RunDecodeJob(NativeArray rgbData, int width, float[] heightData, out float min, out float max) + { + var length = width * width; + var heightNative = new NativeArray(length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + var minMax = new NativeArray(2, Allocator.TempJob); + new DecodeTerrainRgbJob + { + RgbData = rgbData, + Width = width, + HeightData = heightNative, + MinMax = minMax + }.Run(); + heightNative.CopyTo(heightData); + min = minMax[0]; + max = minMax[1]; + heightNative.Dispose(); + minMax.Dispose(); + } - float r = rgbData[index * 4 + 1]; - float g = rgbData[index * 4 + 2]; - float b = rgbData[index * 4 + 3]; - //var color = rgbData[index]; - // float r = color.g; - // float g = color.b; - // float b = color.a; - //the formula below is the same as Conversions.GetAbsoluteHeightFromColor but it's inlined for performance - var value = (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); - heightData[(int)(y * width + x)] = value; - if(value < min) min = value; - if(value > max) max = value; - //678 ==> 012345678 - //345 - //012 - } - } - terrainData.SetElevationValues(heightData, min, max); - } - } -} \ No newline at end of file + /// + /// Burst-compiled decode of Mapbox terrain-RGB into linear elevation (meters). + /// Four-byte RGBA layout, formula h = -10000 + (r*65536 + g*256 + b) * 0.1. + /// Min/max are accumulated in the same pass to save a second iteration. + /// + [BurstCompile] + private struct DecodeTerrainRgbJob : IJob + { + [ReadOnly] public NativeArray RgbData; + public int Width; + public NativeArray HeightData; + public NativeArray MinMax; + + public void Execute() + { + var min = float.MaxValue; + var max = float.MinValue; + int idx = 0; + for (int y = 0; y < Width; y++) + { + for (int x = 0; x < Width; x++, idx++) + { + int rgbIdx = idx * 4; + float r = RgbData[rgbIdx + 1]; + float g = RgbData[rgbIdx + 2]; + float b = RgbData[rgbIdx + 3]; + var value = -10000f + (r * 65536f + g * 256f + b) * 0.1f; + HeightData[idx] = value; + if (value < min) min = value; + if (value > max) max = value; + } + } + MinMax[0] = min; + MinMax[1] = max; + } + } + } +} diff --git a/Runtime/Mapbox/UnityMapService/UnityMapModule.asmdef b/Runtime/Mapbox/UnityMapService/UnityMapModule.asmdef index 245de1840..cd9a2393a 100644 --- a/Runtime/Mapbox/UnityMapService/UnityMapModule.asmdef +++ b/Runtime/Mapbox/UnityMapService/UnityMapModule.asmdef @@ -2,7 +2,8 @@ "name": "UnityMapModule", "rootNamespace": "", "references": [ - "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:2665a8d13d1b3f18800f46e256720795" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/package.json b/package.json index 95e7cd82b..8cff0dc8d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "unity": "2022.3", "dependencies": { "com.unity.render-pipelines.universal": "10.10.1", - "com.unity.nuget.newtonsoft-json": "3.2.1" + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.unity.burst": "1.8.12" }, "author": { "name": "Mapbox", From 4d6cc75b1817487abde3226d87ca609061eceeb0 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 10:25:54 +0300 Subject: [PATCH 15/54] Camera input fixes and new-input gating - MapboxExamples.asmdef: add Unity.InputSystem reference and the com.unity.inputsystem versionDefine that defines MAPBOX_NEW_INPUT_SYSTEM (without this, MapInput's #if branches don't compile against the new input package). Also wires up Unity.Recorder for the benchmark pipeline. - MapInput: normalize new-input scroll delta (scroll.y / 1200) so it matches the legacy Input.GetAxis("Mouse ScrollWheel") magnitude; zoom speed was ~10x off between input paths. - MapInput: track TouchCountDecreasedThisFrame so cameras can reset _dragOrigin when a 2-finger pinch drops to 1 finger (the surviving finger may be at a different screen position than the lifted one, causing a single-frame pan jump). - SlippyMapCamera, Moving3dCamera: consume TouchCountDecreasedThisFrame alongside GetPointerDown to re-seed drag origin. - SlippyMapCamera: gate the CenterLatitudeLongitude update and clamp on _output.HasChanged, and use System.Math.Clamp on doubles instead of Mathf.Clamp on a float-cast Latitude (avoids precision drift at z18+ and skips per-frame work when nothing moved). - MapCameraBehaviour: unsubscribe MapBehaviour.Initialized in OnDestroy to avoid dead-behaviour references across scene reload. Co-Authored-By: Claude Opus 4.7 (1M context) --- Runtime/Mapbox/Example/MapboxExamples.asmdef | 18 ++++++++++++++++-- .../Example/Scripts/MapCameraBehaviour.cs | 6 ++++++ Runtime/Mapbox/Example/Scripts/MapInput.cs | 17 +++++++++++++++-- .../Mapbox/Example/Scripts/Moving3dCamera.cs | 2 +- .../Mapbox/Example/Scripts/SlippyMapCamera.cs | 10 +++++----- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/Runtime/Mapbox/Example/MapboxExamples.asmdef b/Runtime/Mapbox/Example/MapboxExamples.asmdef index debfa42b6..4046c19c8 100644 --- a/Runtime/Mapbox/Example/MapboxExamples.asmdef +++ b/Runtime/Mapbox/Example/MapboxExamples.asmdef @@ -8,7 +8,10 @@ "GUID:36ca6af6e2b304d4090888554d6ce199", "GUID:1b64b068a01f943108c7f4b7a5356c7a", "GUID:6055be8ebefd69e48b49212b09b47b2f", - "GUID:8ba268152fd3143f59445711358cb12f" + "GUID:8ba268152fd3143f59445711358cb12f", + "Unity.Recorder", + "Unity.Recorder.Editor", + "Unity.InputSystem" ], "includePlatforms": [], "excludePlatforms": [], @@ -17,6 +20,17 @@ "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], - "versionDefines": [], + "versionDefines": [ + { + "name": "com.unity.recorder", + "expression": "", + "define": "UNITY_RECORDER" + }, + { + "name": "com.unity.inputsystem", + "expression": "", + "define": "MAPBOX_NEW_INPUT_SYSTEM" + } + ], "noEngineReferences": false } \ No newline at end of file diff --git a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs index 11dd44530..3e000eed4 100644 --- a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs @@ -30,6 +30,12 @@ protected virtual void Awake() MapBehaviour.Initialized += OnMapInitialized; } + protected virtual void OnDestroy() + { + if (MapBehaviour != null) + MapBehaviour.Initialized -= OnMapInitialized; + } + protected virtual void OnMapInitialized(MapboxMap map) { Map = map; diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index 0393495e7..dc7592399 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -44,6 +44,13 @@ public abstract class MapInput // Pinch state tracking private float _previousPinchDistance; private bool _pinchActive; + private int _previousTouchCount; + + // True on the frame the active touch count drops (e.g. 2→1 when one finger + // lifts during a pinch). Cameras should treat this as a fresh drag start so + // _dragOrigin is reset to the surviving finger's position instead of the old + // touches[0] — without this, single-finger pan jumps after a pinch ends. + protected bool TouchCountDecreasedThisFrame { get; private set; } public virtual void Initialize(Camera camera, IMapInformation mapInfo) { @@ -121,7 +128,10 @@ private int GetTouchCount() /// protected void UpdateInputState() { - if (GetTouchCount() < 2) + var touchCount = GetTouchCount(); + TouchCountDecreasedThisFrame = touchCount < _previousTouchCount; + _previousTouchCount = touchCount; + if (touchCount < 2) _pinchActive = false; } @@ -285,7 +295,10 @@ protected bool GetPinchZoomDelta(out float zoomDelta, float pinchSensitivity = 5 var scroll = Mouse.current.scroll.ReadValue(); if (Mathf.Abs(scroll.y) > 0) { - zoomDelta = scroll.y / 120f; + // Raw scroll.y is ~120 per notch on Windows; legacy Input.GetAxis + // returns ~0.1 per notch. Divide by 1200 so both paths feed the + // same magnitude into ZoomSensitivity. + zoomDelta = scroll.y / 1200f; return true; } } diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs index edcce2512..e1b9ee0de 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs @@ -62,7 +62,7 @@ public override CameraOutput UpdateCamera(IMapInformation mapInformation) if (!GetPlaneIntersection(pointerPos, out cursorHit)) return _output; - if (GetPointerDown() || GetSecondaryDown()) + if (GetPointerDown() || GetSecondaryDown() || TouchCountDecreasedThisFrame) { _previousScreenPosition = pointerPos; _dragOrigin = cursorHit; diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs index 4303dc47a..9e1d2a680 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs @@ -109,7 +109,7 @@ public override CameraOutput UpdateCamera(IMapInformation mapInformation) if (!GetPlaneIntersection(pointerPos, out cursorHit)) return _output; - if (GetPointerDown() || GetSecondaryDown()) + if (GetPointerDown() || GetSecondaryDown() || TouchCountDecreasedThisFrame) { _previousScreenPosition = pointerPos; _dragOrigin = cursorHit; @@ -165,15 +165,15 @@ public override CameraOutput UpdateCamera(IMapInformation mapInformation) _output.HasChanged = true; } - CenterLatitudeLongitude = Conversions.WebMercatorToLatLon(newMercatorCenter); - CenterLatitudeLongitude = new LatitudeLongitude( - Mathf.Clamp((float)CenterLatitudeLongitude.Latitude, -MaxMercatorLatitude, MaxMercatorLatitude), - CenterLatitudeLongitude.Longitude); _dragOrigin = cursorHit; _previousScreenPosition = pointerPos; if (_output.HasChanged) { + var latLng = Conversions.WebMercatorToLatLon(newMercatorCenter); + CenterLatitudeLongitude = new LatitudeLongitude( + System.Math.Clamp(latLng.Latitude, -(double)MaxMercatorLatitude, MaxMercatorLatitude), + latLng.Longitude); SetCamPositionByMapInfo(); _output.Center = CenterLatitudeLongitude; _output.Zoom = ZoomValue; From 272acdcdb7d43f5edf1ee8f8ce10e6a9154b64db Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 10:43:50 +0300 Subject: [PATCH 16/54] Quad-tree LOD and tile-flicker fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UnityTileProvider.ShouldSplit: zero the vertical component of the per-corner offset instead of overriding it to +cameraHeight. The native SDK override is correct in its tile-coordinate system but flips the sign of the vertical dot-product contribution in Unity's world units, which triggers the projDist<=0 always-split early-exit for downward-pitched cameras. Using a ground-plane projection is geometrically consistent with the existing bottom-corner-only sampling. - UnityTileProvider.FrustumBuffer: rewrite tooltip/docs so it's clear the value is in Unity world units that depend on mapInformation.Scale. - MapboxMapVisualizer: defer child pooling in InternalUpdateCoroutine by one frame via a _pendingPool queue, so the parent has finished rendering before the children deactivate (closes the iOS flicker on zoom-out described in project_ios_tile_flicker). - MapboxMapVisualizer: reuse unityMapTile.Children across pool/reuse cycles in Load() instead of allocating a new List per cache miss. - MapboxMapVisualizer.PoolTile: recompute Terrain.Min/MaxElevation from the remaining ActiveTiles when the pooled tile was holding either boundary, so the bounds no longer monotonically inflate over a session. - MapboxMapVisualizer.RemoveUnnecessaryTiles: add a comment explaining why Fillers are no longer exempt (intentional behavioral change in 46664b87 — fixes orphaned filler z-fighting). - TerrainInfo: trim unused IsEnabled and Exaggeration fields; drop the matching write in TerrainLayerModuleScript. MinElevation/MaxElevation remain (consumed by the tile provider bounds-height calculation). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BaseModule/Map/MapboxMapVisualizer.cs | 82 ++++++++++++++++--- Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs | 18 +--- .../Unity/TerrainLayerModuleScript.cs | 1 - .../TileProvider/UnityTileProvider.cs | 17 ++-- 4 files changed, 85 insertions(+), 33 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 9f0a72579..cdef17395 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -31,6 +31,10 @@ public class MapboxMapVisualizer : IMapVisualizer // Reusable scratch array to avoid per-frame allocation in InternalUpdateCoroutine private readonly UnwrappedTileId[] _quadrants = new UnwrappedTileId[4]; + // Tiles deferred for pooling on the next coroutine tick. + // Pooling a child the same frame the parent first becomes visible flickers on iOS. + private readonly List _pendingPool = new List(); + public MapboxMapVisualizer(IMapInformation mapInformation, UnityContext unityContext, ITileCreator tileCreator) { _unityContext = unityContext; @@ -124,12 +128,16 @@ public virtual void Load(TileCover tileCover) } else { - var activeChildren = new List(); - var coveredByQuadrants = DelveInto(tileId, activeChildren, recursiveDepth: 1); CreateTempTile(tileId, out unityMapTile); + // Reuse the tile's own Children list across pool/reuse cycles instead of + // allocating one per cache miss. PoolTile clears it on return. + if (unityMapTile.Children == null) + unityMapTile.Children = new List(); + else + unityMapTile.Children.Clear(); + var coveredByQuadrants = DelveInto(tileId, unityMapTile.Children, recursiveDepth: 1); ActiveTiles.Add(tileId, unityMapTile); TempTiles.Add(unityMapTile); - unityMapTile.Children = activeChildren; if (!coveredByQuadrants) { ShowTile(unityMapTile); @@ -169,6 +177,16 @@ public virtual void Load(TileCover tileCover) /// public virtual void InternalUpdateCoroutine() { + // Flush tiles deferred from the previous tick. Holding the children active for + // one extra frame after the parent's ShowTile lets the parent finish rendering + // before the children deactivate, which avoids a one-frame transparent gap on + // iOS (see project_ios_tile_flicker note). + for (var i = 0; i < _pendingPool.Count; i++) + { + PoolTile(_pendingPool[i]); + } + _pendingPool.Clear(); + //finish temp tiles from tempTiles list _toRemove.Clear(); for (var index = TempTiles.Count - 1; index >= 0; index--) @@ -179,22 +197,19 @@ public virtual void InternalUpdateCoroutine() TempTiles.RemoveAt(index); continue; } - + if (tilePair.LoadingState == LoadingState.Temporary && CreateTile(tilePair)) { ShowTile(tilePair); - + if (tilePair.Children != null && tilePair.Children.Count > 0) { - foreach (var child in tilePair.Children) - { - PoolTile(child); - } + _pendingPool.AddRange(tilePair.Children); tilePair.Children.Clear(); } - + TempTiles.RemoveAt(index); - + _quadrants[0] = tilePair.UnwrappedTileId.Quadrant(0); _quadrants[1] = tilePair.UnwrappedTileId.Quadrant(1); _quadrants[2] = tilePair.UnwrappedTileId.Quadrant(2); @@ -218,7 +233,7 @@ public virtual void InternalUpdateCoroutine() TempTiles.Remove(tile); } - PoolTile(tile); + _pendingPool.Add(tile); } } } @@ -301,6 +316,10 @@ private IEnumerator InternalUpdate() private void RemoveUnnecessaryTiles(TileCover tileCover) { + // Fillers used to be exempt from removal here, but that caused orphaned filler + // z-fighting when their logical parent transitioned out of TempTile state. They + // are now eligible for pooling; visual continuity is preserved by the temp-tile + // filler protection pass in Load() above. _toRemove.Clear(); foreach (var tilePair in ActiveTiles) { @@ -387,6 +406,14 @@ protected void PoolTile(UnityMapTile tile) if (tile.LoadingState == LoadingState.None) return; + // Snapshot whether this tile is currently holding either bounds value. + // Recycle() clears TerrainData below, so we have to check first. + var terrain = _mapInformation.Terrain; + var terrainData = tile.TerrainContainer?.TerrainData; + bool contributedBound = terrainData != null && terrainData.IsElevationDataReady && + (Mathf.Approximately(terrainData.MaxElevation, terrain.MaxElevation) || + Mathf.Approximately(terrainData.MinElevation, terrain.MinElevation)); + TileUnloading(tile); ActiveTiles.Remove(tile.UnwrappedTileId); tile.Recycle(); @@ -402,6 +429,37 @@ protected void PoolTile(UnityMapTile tile) tile.Children.Clear(); } + + if (contributedBound) + { + RecomputeTerrainBounds(); + } + } + + private void RecomputeTerrainBounds() + { + var terrain = _mapInformation.Terrain; + float max = 0f; + float min = 0f; + bool any = false; + foreach (var kv in ActiveTiles) + { + var td = kv.Value.TerrainContainer?.TerrainData; + if (td == null || !td.IsElevationDataReady) continue; + if (!any) + { + max = td.MaxElevation; + min = td.MinElevation; + any = true; + } + else + { + if (td.MaxElevation > max) max = td.MaxElevation; + if (td.MinElevation < min) min = td.MinElevation; + } + } + terrain.MaxElevation = any ? max : 0f; + terrain.MinElevation = any ? min : 0f; } protected void CreateTempTile(UnwrappedTileId tileId, out UnityMapTile tile) diff --git a/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs index 4edcb3a8e..f6d55fb80 100644 --- a/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs +++ b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs @@ -4,31 +4,19 @@ namespace Mapbox.BaseModule.Map { /// /// Runtime terrain elevation information for the current map view. - /// Updated automatically as terrain tiles load. Provides observed elevation - /// range and settings useful for styling, object placement, and culling. + /// Updated automatically as terrain tiles load and unload. /// [Serializable] public class TerrainInfo { /// - /// Whether terrain elevation is enabled. False when using flat terrain strategy. - /// - public bool IsEnabled; - - /// - /// Lowest observed elevation in meters across all loaded terrain tiles. + /// Lowest observed elevation in meters across currently loaded terrain tiles. /// public float MinElevation; /// - /// Highest observed elevation in meters across all loaded terrain tiles. + /// Highest observed elevation in meters across currently loaded terrain tiles. /// public float MaxElevation; - - /// - /// Vertical exaggeration factor applied to terrain elevation. - /// 1.0 = real-world scale. Set by the terrain module during initialization. - /// - public float Exaggeration = 1f; } } diff --git a/Runtime/Mapbox/ImageModule/Unity/TerrainLayerModuleScript.cs b/Runtime/Mapbox/ImageModule/Unity/TerrainLayerModuleScript.cs index 5f5b93268..a5d8ef6d1 100644 --- a/Runtime/Mapbox/ImageModule/Unity/TerrainLayerModuleScript.cs +++ b/Runtime/Mapbox/ImageModule/Unity/TerrainLayerModuleScript.cs @@ -46,7 +46,6 @@ public override ILayerModule ConstructModule(MapService service, IMapInformation Settings); mapInformation.QueryElevation = module.QueryElevation; - mapInformation.Terrain.IsEnabled = true; ModuleImplementation = module; return ModuleImplementation; } diff --git a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs index f87dbfe19..02462ba06 100644 --- a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs +++ b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs @@ -46,12 +46,15 @@ public class UnityTileProviderSettings public float SubdivisionBias = 1.0f; /// - /// Expands the 4 side frustum planes outward by this distance (world units). + /// Expands the 4 side frustum planes outward by this distance, in Unity world units. /// Pre-loads tiles just outside the screen edges to prevent pop-in during camera pans. /// Higher values give smoother panning but load more off-screen tiles. - /// Does not affect near/far planes. Typical range 0-50. Set to 0 to disable. + /// Does not affect near/far planes. + /// Tile size varies with mapInformation.Scale: a tile is ~1 unit at scale 1, + /// ~100 units at scale 0.01. Pick a value relative to your scene's current tile size + /// (e.g. ~25% of a tile width for a one-tile-wide buffer). Set to 0 to disable. /// - [Tooltip("Pre-loads tiles beyond screen edges to reduce pop-in during panning. Higher = smoother but more tiles loaded. Typical range 0-50. Default 0.")] + [Tooltip("Pre-loads tiles beyond screen edges to reduce pop-in during panning. Value is in Unity world units — scale relative to the tile size at your map's current Scale. Set to 0 to disable.")] public float FrustumBuffer = 0f; public UnityTileProviderSettings(Camera cam, float minZoom = 2, float maxZoom = 22) @@ -196,8 +199,12 @@ protected bool ShouldSplit(int zoom, Bounds bounds, Vector3 camPos, for (int i = 0; i < 4; i++) { var offset = _corners[i] - camPos; - // Use camera height as vertical component (flat map, matching native SDK) - offset.y = cameraHeight; + // Project onto the ground plane: native SDK overrides this to +cameraHeight, + // which in its tile-coord system stays geometrically consistent. Unity world + // units flip the sign of the vertical dot-product contribution and trigger the + // projDist<=0 early-exit (always-split) for downward-pitched cameras, so we + // zero out the vertical and use ground-plane forward distance instead. + offset.y = 0f; var projDist = Vector3.Dot(offset, camForward); if (projDist <= 0f) return true; // corner behind camera plane — always split From 7ae488c0a8093788ca62bc84f90951a637e72c5f Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 10:53:24 +0300 Subject: [PATCH 17/54] Terrain strategy and inspector fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ElevatedTerrainStrategy.GetOrCreateCollider: hoist cookingOptions assignment out of the component-creation branch so every call refreshes it. If the collider's cookingOptions diverges from BakeColliderJob's, PhysX discards the async-cooked cache and re-cooks synchronously on sharedMesh assignment — silently defeating the async-bake path. - ElevatedTerrainStrategy.CompleteBakeAndAssign: null-guard the new mesh after handle.Complete(). A parallel bake completion landing first can Destroy() this mesh as its "previous" before we get to assign it. - ElevatedTerrainStrategy: change _elevationNativeMirror from float[] to TerrainData. ElevationArrayPool recycles float[] buffers, so the same reference can hold different data across dispose+re-rent — the prior ReferenceEquals(source, _elevationNativeMirror) check could return a false cache hit. TerrainData instances aren't recycled the same way. - ElevatedTerrainStrategy.Initialize: destroy any pre-existing _sharedFlatMesh before allocating a new one. Without this, a second Initialize call orphans the previous mesh (re-init can happen without a preceding OnDestroy on settings change / editor reload). - ElevationArrayPool: cap per-size stack at 32 entries. Past the cap, returns drop to GC. Bounds worst-case RAM after a CacheSize spike. - TerrainSettingsInspectorHelper: cache the SceneHasFeaturesNeedingCpu- Elevation result, invalidate on EditorApplication.hierarchyChanged. Was running FindObjectsByType on every Inspector OnGUI. - SimplificationFactorDrawer: cache preset factor + label arrays per (Min, Max, VertexBase). LINQ Where/Select/ToArray was allocating fresh arrays every repaint while the Inspector was open. - TerrainLayerModule.GetDataId(IEnumerable): add documenting that the returned enumerable is reusable scratch — callers must finish iterating before the next call. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Data/DataFetchers/ElevationArrayPool.cs | 14 +++- .../ElevatedTerrainStrategy.cs | 66 ++++++++++++------- .../Editor/SimplificationFactorDrawer.cs | 26 +++++++- .../Editor/TerrainSettingsInspectorHelper.cs | 32 +++++++-- .../ImageModule/Terrain/TerrainLayerModule.cs | 9 +++ 5 files changed, 117 insertions(+), 30 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs index 2ee02a7f6..7891a6373 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs @@ -10,11 +10,17 @@ namespace Mapbox.BaseModule.Data.DataFetchers /// /// /// Thread-safe — async GPU readback callbacks fire on the main thread but the contract - /// is conservative in case Unity changes that. Pool grows unbounded; in practice the - /// only sizes seen are 256×256 (default texture) and 512×512 (retina). + /// is conservative in case Unity changes that. Per-size stack depth is capped at + /// to bound worst-case RAM after a CacheSize spike; in + /// practice the only sizes seen are 256×256 (default texture) and + /// 512×512 (retina), so the cap also indirectly limits total residency. /// public static class ElevationArrayPool { + // 32 × 1 MB (512×512 floats) = ~32 MB worst-case per resolution. Anything beyond + // is dropped to GC; tiles past this point pay a fresh allocation on next decode. + private const int MaxPerSizeDepth = 32; + private static readonly Dictionary> _pools = new Dictionary>(); /// @@ -51,6 +57,10 @@ public static void Return(float[] array) stack = new Stack(); _pools[array.Length] = stack; } + if (stack.Count >= MaxPerSizeDepth) + { + return; // Drop to GC; cap bounds peak RAM held by the pool. + } stack.Push(array); } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index 00ec9951d..476e2899b 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -57,7 +57,11 @@ public class ElevatedTerrainStrategy : TerrainStrategy, IElevationBasedTerrainSt private NativeArray _colliderTrianglesNative; private int _colliderNativeSampleCount = -1; private NativeArray _elevationNative; - private float[] _elevationNativeMirror; + // Keyed by the TerrainData reference rather than the float[] reference: ElevationArrayPool + // recycles float[] buffers, so the same array reference can hold different data across + // dispose+re-rent cycles. TerrainData instances aren't recycled the same way, so reference + // equality on the data wrapper is a safe cache key. + private TerrainData _elevationNativeMirror; // Shared flat render mesh used by every tile in shader mode. All render tiles have // byte-identical CPU vertex data in that mode (the per-tile _HeightTexture_ST @@ -93,14 +97,23 @@ public override void Initialize(ElevationLayerProperties elOptions) : _elevationOptions.modificationOptions.sampleCount + 1; _skirtSize = elOptions.sideWallOptions.wallHeight; _colliderOptions = elOptions.colliderOptions; - + _newVertexList = new List(_requiredVertexCount); _newNormalList = new List(_requiredVertexCount); _newUvList = new List(_requiredVertexCount); - + _baseMesh = CreateBaseMesh(_elevationOptions.TileMeshSize, _sideVertexCount); _requiredVertexCount = _baseMesh.Vertices.Length; + // Free the previous shared mesh on re-init — otherwise a second Initialize + // orphans it. OnDestroy alone isn't enough since re-init can happen without a + // preceding destroy (settings changes, editor reloads). + if (_sharedFlatMesh != null) + { + UnityEngine.Object.Destroy(_sharedFlatMesh); + _sharedFlatMesh = null; + } + // Build the shader-mode shared render mesh from the base data once. Every // shader-mode tile's MeshFilter will point at this single instance. _sharedFlatMesh = new Mesh { name = SharedFlatMeshName }; @@ -275,6 +288,7 @@ private MeshCollider FindExistingCollider(UnityMapTile tile) /// private MeshCollider GetOrCreateCollider(UnityMapTile tile) { + MeshCollider collider; if (_colliderOptions.useDedicatedColliderLayer) { var childTransform = tile.transform.Find(ColliderChildName); @@ -290,22 +304,25 @@ private MeshCollider GetOrCreateCollider(UnityMapTile tile) } childGo.layer = _colliderOptions.colliderLayerId; - var childCollider = childGo.GetComponent(); - if (childCollider == null) + collider = childGo.GetComponent(); + if (collider == null) { - childCollider = childGo.AddComponent(); - childCollider.cookingOptions = TerrainColliderCookingOptions; + collider = childGo.AddComponent(); } - return childCollider; } - - var meshCollider = tile.GetComponent(); - if (meshCollider == null) + else { - meshCollider = tile.gameObject.AddComponent(); - meshCollider.cookingOptions = TerrainColliderCookingOptions; + collider = tile.GetComponent(); + if (collider == null) + { + collider = tile.gameObject.AddComponent(); + } } - return meshCollider; + // Reassign every call: if the collider's cookingOptions diverges from what + // BakeColliderJob uses, PhysX discards the async-cooked cache and re-cooks + // synchronously on sharedMesh assignment — silently defeating the async path. + collider.cookingOptions = TerrainColliderCookingOptions; + return collider; } /// @@ -339,12 +356,13 @@ private void BuildAndAssignCollider(UnityMapTile tile) // render tiles that sample the same data. Triangles are regenerated only when // sampleCount changes — see EnsureColliderBuffers. var container = tile.TerrainContainer; - var elevationValues = container.TerrainData.ElevationValues; + var terrainData = container.TerrainData; + var elevationValues = terrainData.ElevationValues; var dataWidth = (int)Mathf.Sqrt(elevationValues.Length); var scaleOffset = container.TerrainTextureScaleOffset; EnsureColliderBuffers(sampleCount); - EnsureElevationNative(elevationValues); + EnsureElevationNative(terrainData); new BuildColliderVerticesJob { @@ -440,12 +458,13 @@ private void EnsureColliderBuffers(int sampleCount) /// /// Mirrors the current data tile's managed ElevationValues into the /// strategy's persistent . Reuses the existing - /// NativeArray when the source reference is unchanged, so the 16 render tiles that - /// share a data tile pay the ~256 KB copy only once. + /// NativeArray when the source reference is unchanged, + /// so the 16 render tiles that share a data tile pay the ~256 KB copy only once. /// - private void EnsureElevationNative(float[] source) + private void EnsureElevationNative(TerrainData terrainData) { - if (ReferenceEquals(source, _elevationNativeMirror) && _elevationNative.IsCreated && _elevationNative.Length == source.Length) + var source = terrainData.ElevationValues; + if (ReferenceEquals(terrainData, _elevationNativeMirror) && _elevationNative.IsCreated && _elevationNative.Length == source.Length) { return; } @@ -458,7 +477,7 @@ private void EnsureElevationNative(float[] source) _elevationNative = new NativeArray(source.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); } _elevationNative.CopyFrom(source); - _elevationNativeMirror = source; + _elevationNativeMirror = terrainData; } /// @@ -554,7 +573,10 @@ private static IEnumerator CompleteBakeAndAssign(MeshCollider meshCollider, Mesh } handle.Complete(); - if (meshCollider == null) + // Either side can be destroyed while we waited: UnityMapTile.OnDestroy can take + // the meshCollider with the GameObject, and a parallel bake completion that + // landed first can have Destroy()'d our mesh as its "previous" (line below). + if (meshCollider == null || mesh == null) { if (mesh != null) { diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs index 635b6348e..a99191ebf 100644 --- a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs @@ -20,11 +20,33 @@ public class SimplificationFactorDrawer : PropertyDrawer // between collapses via integer division. Each entry halves the grid resolution. private static readonly int[] PresetFactors = { 1, 2, 4, 8, 16, 32, 64, 128 }; + // Cached preset+label arrays per attribute. OnGUI runs every repaint; without this + // the LINQ chain allocates fresh arrays continuously while the Inspector is open. + private int[] _cachedFactors; + private GUIContent[] _cachedLabels; + private int _cachedMin = -1; + private int _cachedMax = -1; + private int _cachedVertexBase = -1; + + private void EnsurePresetsCached(SimplificationFactorAttribute attr) + { + if (_cachedFactors != null && _cachedMin == attr.Min && _cachedMax == attr.Max && _cachedVertexBase == attr.VertexBase) + { + return; + } + _cachedFactors = PresetFactors.Where(f => f >= attr.Min && f <= attr.Max).ToArray(); + _cachedLabels = _cachedFactors.Select(f => new GUIContent(BuildLabel(f, attr.VertexBase))).ToArray(); + _cachedMin = attr.Min; + _cachedMax = attr.Max; + _cachedVertexBase = attr.VertexBase; + } + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var attr = (SimplificationFactorAttribute)attribute; - var factors = PresetFactors.Where(f => f >= attr.Min && f <= attr.Max).ToArray(); - var labels = factors.Select(f => new GUIContent(BuildLabel(f, attr.VertexBase))).ToArray(); + EnsurePresetsCached(attr); + var factors = _cachedFactors; + var labels = _cachedLabels; // Snap legacy non-preset values to the nearest preset so the dropdown always // matches a concrete entry. diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs index 269ec5e05..8f9c540ed 100644 --- a/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs @@ -115,9 +115,29 @@ private static void DrawSceneNeedsExtractionWarning(SerializedProperty settingsP "MapboxComponentsModuleScript" }; + // Cached scene-scan result, invalidated whenever the hierarchy changes. Without this, + // every Inspector OnGUI repaint runs FindObjectsByType across the + // whole active scene — visibly stalls when many tiles are alive. + private static bool _sceneScanValid; + private static bool _sceneScanResult; + private static string _sceneScanDetectedTypeName; + private static bool _hierarchyHookInstalled; + private static bool SceneHasFeaturesNeedingCpuElevation(out string detectedTypeName) { - detectedTypeName = null; + if (!_hierarchyHookInstalled) + { + EditorApplication.hierarchyChanged += () => _sceneScanValid = false; + _hierarchyHookInstalled = true; + } + if (_sceneScanValid) + { + detectedTypeName = _sceneScanDetectedTypeName; + return _sceneScanResult; + } + + _sceneScanDetectedTypeName = null; + _sceneScanResult = false; var all = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var mb in all) { @@ -127,12 +147,16 @@ private static bool SceneHasFeaturesNeedingCpuElevation(out string detectedTypeN { if (name == WatchedTypeNames[i]) { - detectedTypeName = name; - return true; + _sceneScanDetectedTypeName = name; + _sceneScanResult = true; + goto done; } } } - return false; + done: + _sceneScanValid = true; + detectedTypeName = _sceneScanDetectedTypeName; + return _sceneScanResult; } } } diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index cc4049abe..4df4fd91a 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -213,6 +213,15 @@ public float QueryElevation(CanonicalTileId tileId, float x, float y) return 0; } + /// + /// Maps a set of render tile ids to the distinct data tile ids they sample. + /// + /// + /// The returned enumerable is a reusable per-instance scratch set — the caller + /// MUST finish iterating before invoking + /// again (the next call clears the same set). Do not retain a reference past the + /// loop body. + /// public IEnumerable GetDataId(IEnumerable tileIdList) { // With the data-zoom offset of 2, up to 16 render tile ids collapse to the From 80c5f0739378b635f4c9b09f2947a186de71fb89 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 10:59:27 +0300 Subject: [PATCH 18/54] Correct MPB/SRP-Batcher comments in tile renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline notes on the sharedMaterial + MaterialPropertyBlock pattern in UnityMapTile and TileCreator claimed it lets URP's SRP Batcher merge tile draws. That is wrong on URP 2022.3: any Renderer.SetPropertyBlock call disqualifies the renderer from the SRP Batcher. The pattern's real win is avoiding a Material clone per renderer (reading Renderer.material clones, sharedMaterial+MPB does not). It is also not GPU-instanced today — the terrain materials have m_EnableInstancingVariants:0 and the shadergraphs have no UNITY_INSTANCING_BUFFER block. Comments rewritten to describe what the pattern actually delivers; see project_terrain_instancing memory for what a real batching rework would require. Co-Authored-By: Claude Opus 4.7 (1M context) --- Runtime/Mapbox/BaseModule/Map/TileCreator.cs | 8 +++++--- Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs | 9 +++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Map/TileCreator.cs b/Runtime/Mapbox/BaseModule/Map/TileCreator.cs index 584b81f74..8eac6c607 100644 --- a/Runtime/Mapbox/BaseModule/Map/TileCreator.cs +++ b/Runtime/Mapbox/BaseModule/Map/TileCreator.cs @@ -61,9 +61,11 @@ private UnityMapTile CreateTile(UnityContext unityContext) { // Always use sharedMaterial. Per-tile state goes through the tile's // MaterialPropertyBlock (UnityTileTerrainContainer / UnityTileImageContainer - // mutate it), which lets URP's SRP Batcher merge tile draws into a single - // batch. The legacy _instanceMaterial flag is retained on the constructor - // for back-compat but no longer differentiates: per-tile state lives on the + // mutate it). The win is avoiding a Material clone per renderer — not SRP + // Batcher batching (SetPropertyBlock disqualifies a renderer from the SRP + // Batcher) and not GPU instancing (off in the current materials/shaders). + // The legacy _instanceMaterial flag is retained on the constructor for + // back-compat but no longer differentiates: per-tile state lives on the // property block, not on a unique Material instance. tile.MeshRenderer.sharedMaterial = TileMaterials[0]; tile.Material = tile.MeshRenderer.sharedMaterial; diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs index 52f950104..0a9e29e82 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs @@ -32,8 +32,13 @@ public class UnityMapTile : MonoBehaviour // Per-tile state (height texture, scale-offset, elevation multiplier, etc.) is // pushed via this MaterialPropertyBlock instead of a per-tile Material instance. - // One Material is shared across the entire tile pool (set in TileCreator), which - // lets URP's SRP Batcher merge the tile draws into a single batch. + // One Material is shared across the entire tile pool (set in TileCreator); the + // win is avoiding a Material clone per renderer (reading Renderer.material clones, + // SetPropertyBlock does not). This pattern is NOT SRP-Batcher-compatible — any + // SetPropertyBlock on a renderer excludes it from the SRP Batcher. It is also not + // GPU-instanced today (materials have m_EnableInstancingVariants:0 and the shader + // has no UNITY_INSTANCING_BUFFER block); see project_terrain_instancing for what + // a real batching rework would entail. private MaterialPropertyBlock _propertyBlock; public MaterialPropertyBlock PropertyBlock { From c633a6e7ebfda9e0756c97cd5a1e240dbba4ed10 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 11:05:34 +0300 Subject: [PATCH 19/54] Terrain bounds correctness and lifecycle fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ElevatedTerrainStrategy.RegisterTile (CPU branch): never Clear() the shared flat mesh. When a runtime config change (sampleCount or _useTileSkirts toggle) makes RequiredVertexCount diverge from the shared mesh, a shader→CPU tile transition would otherwise wipe _sharedFlatMesh — taking out the geometry of every other shader-mode tile pointing at it. Allocate a fresh per-tile mesh in that case. - ElevatedTerrainStrategy: opportunistic sweep of dead Unity Object keys in _lastColliderBuild (Dictionary) on insert past a soft cap, plus an explicit Clear() in OnDestroy. Unity's overloaded == treats destroyed objects as null but Dictionary lookups use C# reference equality, so dead proxies would otherwise linger. - MapboxMapVisualizer.CreateTile: replace the inline Min/MaxElevation accumulation with a RecomputeTerrainBounds() call. The prior path used `terrainData.MinElevation < terrain.MinElevation` against an initial 0, so for tiles entirely at or above sea level the comparison never fired and MinElevation stayed pinned at 0 forever. - CHANGELOG: document the IMapInformation.Terrain breaking change for external implementers under v3.0.7 Breaking changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 3 ++ .../ElevatedTerrainStrategy.cs | 52 +++++++++++++++++-- .../BaseModule/Map/MapboxMapVisualizer.cs | 10 ++-- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e76e625c..216d8536e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ### v3.0.7 +#### Breaking changes +- `IMapInformation` gained a new member: `TerrainInfo Terrain { get; }`. Any project that ships its own `IMapInformation` implementation will need to add this property — return an instance of `TerrainInfo` (defaults are fine for a non-terrain map). The built-in `MapInformation` already implements it. + #### Changes - Added new documentation. - Added a new demo scene demonstrating how to use POI information. diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index 476e2899b..eac76cb3a 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -75,6 +75,12 @@ public class ElevatedTerrainStrategy : TerrainStrategy, IElevationBasedTerrainSt // same data; skipping the rebuild avoids a redundant PhysX re-cook. private readonly Dictionary _lastColliderBuild = new Dictionary(); + + // Sweep dead-collider entries from _lastColliderBuild once it exceeds this count. + // Bounded by tile-pool peak in normal use; the sweep catches the case where + // GameObjects actually destroy (scene teardown, runtime tile-pool resize). + private const int LastColliderBuildSweepThreshold = 256; + private readonly List _deadColliderKeysScratch = new List(); public override int RequiredVertexCount { @@ -143,6 +149,8 @@ public override void OnDestroy() if (_colliderVerticesNative.IsCreated) _colliderVerticesNative.Dispose(); if (_colliderTrianglesNative.IsCreated) _colliderTrianglesNative.Dispose(); if (_elevationNative.IsCreated) _elevationNative.Dispose(); + _lastColliderBuild.Clear(); + _elevationNativeMirror = null; } public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) @@ -174,9 +182,18 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) { // CPU mode: each tile needs its own unique vertex buffer since the Y // displacement is per-tile. Reset the tile's Awake-allocated mesh from the - // base template. - Mesh sharedMesh; - (sharedMesh = tile.MeshFilter.sharedMesh).Clear(); + // base template — but never Clear() _sharedFlatMesh, which would wipe the + // mesh used by every shader-mode tile. Allocate a fresh per-tile mesh if + // this tile's MeshFilter is still pointing at the shared instance (happens + // when a runtime config change flips the tile shader→CPU and the new + // RequiredVertexCount differs from _sharedFlatMesh.vertexCount). + var sharedMesh = tile.MeshFilter.sharedMesh; + if (sharedMesh == _sharedFlatMesh) + { + sharedMesh = new Mesh(); + tile.MeshFilter.sharedMesh = sharedMesh; + } + sharedMesh.Clear(); var newMesh = _baseMesh; sharedMesh.subMeshCount = 2; sharedMesh.vertices = newMesh.Vertices; @@ -263,6 +280,27 @@ private void RegisterCollider(UnityMapTile tile) // across pool cycles without maintaining a per-tile dictionary. private const string ColliderChildName = "TerrainCollider"; + // Removes _lastColliderBuild entries whose MeshCollider key has been destroyed + // (GameObject teardown, scene unload). Unity's overloaded == returns true for + // destroyed-vs-null, but Dictionary hashes by C# reference equality and + // would otherwise keep dead proxies indefinitely. + private void SweepDeadColliderEntries() + { + _deadColliderKeysScratch.Clear(); + foreach (var kv in _lastColliderBuild) + { + if (kv.Key == null) + { + _deadColliderKeysScratch.Add(kv.Key); + } + } + for (int i = 0; i < _deadColliderKeysScratch.Count; i++) + { + _lastColliderBuild.Remove(_deadColliderKeysScratch[i]); + } + _deadColliderKeysScratch.Clear(); + } + /// /// Looks up the existing for this tile without creating /// one, mirroring the location rule in . Used by @@ -389,6 +427,14 @@ private void BuildAndAssignCollider(UnityMapTile tile) // Record what we just built so RegisterCollider can short-circuit on // redundant subsequent RegisterTile calls (temp→final transitions). + // Sweep dead Unity Object keys opportunistically when the dict grows past + // the soft cap so destroyed-collider proxies don't linger across long + // sessions (Unity's overloaded == doesn't reach Dictionary's reference + // equality, so dead keys would otherwise stay queryable). + if (_lastColliderBuild.Count > LastColliderBuildSweepThreshold) + { + SweepDeadColliderEntries(); + } _lastColliderBuild[meshCollider] = (tile.TerrainContainer.TerrainData, tile.CanonicalTileId); if (_colliderOptions.asyncBakeCollider) diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index cdef17395..48edcf037 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -525,11 +525,11 @@ protected bool CreateTile(UnityMapTile unityMapTile) var terrainData = unityMapTile.TerrainContainer?.TerrainData; if (terrainData != null && terrainData.IsElevationDataReady) { - var terrain = _mapInformation.Terrain; - if (terrainData.MaxElevation > terrain.MaxElevation) - terrain.MaxElevation = terrainData.MaxElevation; - if (terrainData.MinElevation < terrain.MinElevation) - terrain.MinElevation = terrainData.MinElevation; + // Recompute from ActiveTiles rather than widening against the existing + // Min/Max: the prior accumulate-only path left MinElevation pinned at + // its initial 0 whenever every loaded tile was at or above sea level, + // since (positive < 0) never fires. + RecomputeTerrainBounds(); } TileLoaded(unityMapTile); From 70240bd4dfbec704d302532478db75b31c4bbb12 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 11:20:13 +0300 Subject: [PATCH 20/54] Defensive fixes across terrain, tile provider, and input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ElevatedTerrainStrategy / UnityMapTile: hoist mesh-name magic strings to public constants (TerrainColliderMeshName, SharedFlatMeshName) and reference them from both files. Eliminates the silent rename hazard in UnityMapTile.OnDestroy's leak cleanup. - UnityTileTerrainContainer.SetTerrainData: drop the inconsistent `terrainData?.SetDisposeCallback(null)` and add an explicit early- return with Debug.LogError if a null TerrainData ever gets through. The rest of the method dereferences unconditionally; the prior `?.` was misleading. - ElevationArrayPool: add a UNITY_ASSERTIONS-only double-return detector. O(N) scan on Return only in editor/dev; silently aliasing two callers to the same buffer is hard to debug after the fact. - MapCameraBehaviour.Awake: when no MapBehaviourCore exists in the scene, log an explicit error and disable the component instead of NRE'ing on MapBehaviour.Initialized. - UnityTileProvider: clamp _maxZoom to ≤30 internally. Guards against Inspector misconfig of MaximumZoomLevel that would overflow the sign bit in `1 << (_maxZoom - zoom)`. - MapboxMapVisualizer.DelveInto: convert recursion to an explicit stack-frame iteration. Eliminates the new bool[4]/UnwrappedTileId[4] allocations per call (~85 per Load cache miss recursively before). Same semantics as the prior recursion — at each level missing quadrants recurse deeper, and any satisfied level fills its unfound siblings with temp filler tiles. - Documentation~/CameraSystem.md: add an Input System Support section explaining the versionDefines-driven MAPBOX_NEW_INPUT_SYSTEM toggle. Input System is intentionally a soft dependency. Co-Authored-By: Claude Opus 4.7 (1M context) --- Documentation~/CameraSystem.md | 11 ++ .../Data/DataFetchers/ElevationArrayPool.cs | 17 +++ .../ElevatedTerrainStrategy.cs | 9 +- .../BaseModule/Map/MapboxMapVisualizer.cs | 143 ++++++++++++------ .../Mapbox/BaseModule/Unity/UnityMapTile.cs | 16 +- .../Unity/UnityTileTerrainContainer.cs | 11 +- .../Example/Scripts/MapCameraBehaviour.cs | 7 + .../TileProvider/UnityTileProvider.cs | 5 +- 8 files changed, 164 insertions(+), 55 deletions(-) diff --git a/Documentation~/CameraSystem.md b/Documentation~/CameraSystem.md index 1b45f4e23..0a414e5dd 100644 --- a/Documentation~/CameraSystem.md +++ b/Documentation~/CameraSystem.md @@ -100,6 +100,17 @@ Zoom at cursor works with both mouse and touch — when using pinch, the zoom ce --- +## Input System Support + +The cameras work with both Unity's legacy Input Manager and the new Input System package. The active path is selected at compile time: + +- **Default (no Input System package installed)** — uses the legacy `UnityEngine.Input` API. No action needed. +- **With `com.unity.inputsystem` installed** — the example asmdef declares a `versionDefines` entry that automatically defines `MAPBOX_NEW_INPUT_SYSTEM`. The new-input branch activates and reads pointer state via `Mouse.current` / `Touch.activeTouches`. + +The Input System package is *not* a hard dependency of the SDK — projects that don't install it keep the legacy path with no extra setup. Projects that set **Active Input Handling** to "Input System Package (New)" only must install the package; otherwise `UnityEngine.Input` is unavailable at runtime. + +--- + ## Writing a Custom Camera If neither camera mode fits your needs, you can create your own by writing a class that extends `MapInput` and a MonoBehaviour that extends `MapCameraBehaviour`. diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs index 7891a6373..f06974bdf 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs @@ -57,6 +57,7 @@ public static void Return(float[] array) stack = new Stack(); _pools[array.Length] = stack; } + AssertNotAlreadyInStack(stack, array); if (stack.Count >= MaxPerSizeDepth) { return; // Drop to GC; cap bounds peak RAM held by the pool. @@ -64,5 +65,21 @@ public static void Return(float[] array) stack.Push(array); } } + + // O(N) check that only runs in editor/dev builds. Double-return would silently + // alias two callers to the same backing buffer on the next pair of Rent() calls, + // producing data corruption that's hard to track down. + [System.Diagnostics.Conditional("UNITY_ASSERTIONS")] + private static void AssertNotAlreadyInStack(Stack stack, float[] array) + { + foreach (var existing in stack) + { + if (ReferenceEquals(existing, array)) + { + UnityEngine.Debug.LogError("ElevationArrayPool.Return called twice for the same array — likely double-return. The duplicate is being dropped."); + throw new System.InvalidOperationException("ElevationArrayPool double-return detected."); + } + } + } } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index eac76cb3a..125aa523d 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -67,7 +67,10 @@ public class ElevatedTerrainStrategy : TerrainStrategy, IElevationBasedTerrainSt // byte-identical CPU vertex data in that mode (the per-tile _HeightTexture_ST // sub-region determines the actual surface); pointing every MeshFilter at this // single Mesh avoids per-tile Mesh allocation and upload. - private const string SharedFlatMeshName = "TerrainSharedFlat"; + // Exposed for UnityMapTile.OnDestroy so its leak cleanup can identify which meshes + // it must skip (this one is owned by the strategy) versus destroy (per-tile collider). + public const string SharedFlatMeshName = "TerrainSharedFlat"; + public const string TerrainColliderMeshName = "TerrainCollider"; private Mesh _sharedFlatMesh; // Tracks the (TerrainData, CanonicalTileId) last used to build each MeshCollider's @@ -378,7 +381,7 @@ private void BuildAndAssignCollider(UnityMapTile tile) var mesh = meshCollider.sharedMesh; if (mesh == null || mesh == tile.MeshFilter.sharedMesh) { - mesh = new Mesh { name = "TerrainCollider" }; + mesh = new Mesh { name = TerrainColliderMeshName }; mesh.MarkDynamic(); } @@ -634,7 +637,7 @@ private static IEnumerator CompleteBakeAndAssign(MeshCollider meshCollider, Mesh var previous = meshCollider.sharedMesh; meshCollider.sharedMesh = null; meshCollider.sharedMesh = mesh; - if (previous != null && previous != mesh && previous.name == "TerrainCollider") + if (previous != null && previous != mesh && previous.name == TerrainColliderMeshName) { UnityEngine.Object.Destroy(previous); } diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 48edcf037..30caba7f6 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -35,6 +35,20 @@ public class MapboxMapVisualizer : IMapVisualizer // Pooling a child the same frame the parent first becomes visible flickers on iOS. private readonly List _pendingPool = new List(); + // Explicit stack for the iterative DelveInto (was recursive with per-call new bool[4] + // and new UnwrappedTileId[4] allocations). Bounded by recursion depth × 4, so the + // List reaches its peak size after one large call and never re-allocates after. + private struct DelveFrame + { + public UnwrappedTileId TileId; + public int Depth; + public int Found; // bitmask: bit i set if quadrant i is satisfied (here or via descendants) + public int NextChild; // 0..4 + public int ParentFrame; // index in _delveStack, or -1 for root + public int ParentSlot; // which slot of the parent we are filling (0..3) + } + private readonly List _delveStack = new List(); + public MapboxMapVisualizer(IMapInformation mapInformation, UnityContext unityContext, ITileCreator tileCreator) { _unityContext = unityContext; @@ -333,66 +347,109 @@ private void RemoveUnnecessaryTiles(TileCover tileCover) protected bool DelveInto(UnwrappedTileId tileId, List activeChildren, int recursiveDepth = 3) { - var quadrantCheck = new bool[4]; - var quadrants = new UnwrappedTileId[4] + // Iterative replacement for the prior recursive implementation. Same semantics: + // at each level we look up the 4 quadrants in ActiveTiles; missing ones recurse + // deeper up to `recursiveDepth`; if any quadrant at a level is satisfied (here + // or via a descendant), the remaining slots at THAT level get temp filler tiles. + _delveStack.Clear(); + _delveStack.Add(new DelveFrame { - tileId.Quadrant(0), - tileId.Quadrant(1), - tileId.Quadrant(2), - tileId.Quadrant(3), - }; - for (int i = 0; i < 4; i++) + TileId = tileId, + Depth = recursiveDepth, + Found = 0, + NextChild = 0, + ParentFrame = -1, + ParentSlot = -1, + }); + + bool rootResult = false; + + while (_delveStack.Count > 0) { - var quadrant = quadrants[i]; - if (ActiveTiles.TryGetValue(quadrant, out var unityMapTile)) + int frameIdx = _delveStack.Count - 1; + var frame = _delveStack[frameIdx]; + + if (frame.NextChild < 4) { - _toRemove.Remove(quadrant); - unityMapTile.LoadingState = LoadingState.Filler; - activeChildren.Add(unityMapTile); - if (unityMapTile.Children != null && unityMapTile.Children.Count > 0) + int slot = frame.NextChild; + var quadrant = frame.TileId.Quadrant(slot); + frame.NextChild++; + + if (ActiveTiles.TryGetValue(quadrant, out var unityMapTile)) { - foreach (var subchild in unityMapTile.Children) + _toRemove.Remove(quadrant); + unityMapTile.LoadingState = LoadingState.Filler; + activeChildren.Add(unityMapTile); + if (unityMapTile.Children != null && unityMapTile.Children.Count > 0) { - activeChildren.Add(subchild); + foreach (var subchild in unityMapTile.Children) + { + activeChildren.Add(subchild); + } + unityMapTile.Children.Clear(); } - unityMapTile.Children.Clear(); + ShowTile(unityMapTile); + frame.Found |= 1 << slot; + _delveStack[frameIdx] = frame; + } + else if (frame.Depth > 0 && frame.TileId.Z < 22) + { + // Save the advanced NextChild before pushing the child frame — + // we'll resume this slot's evaluation after the child finishes. + _delveStack[frameIdx] = frame; + _delveStack.Add(new DelveFrame + { + TileId = quadrant, + Depth = frame.Depth - 1, + Found = 0, + NextChild = 0, + ParentFrame = frameIdx, + ParentSlot = slot, + }); + } + else + { + // Leaf miss: nothing found here, no deeper recursion allowed. + _delveStack[frameIdx] = frame; } - - ShowTile(unityMapTile); - quadrantCheck[i] = true; } - } - - if (recursiveDepth > 0) - { - for (int i = 0; i < 4; i++) + else { - if (quadrantCheck[i] == false && tileId.Z < 22) + bool anyFound = frame.Found != 0; + if (anyFound) { - quadrantCheck[i] = DelveInto(quadrants[i], activeChildren, recursiveDepth - 1); + for (int i = 0; i < 4; i++) + { + if ((frame.Found & (1 << i)) == 0) + { + var q = frame.TileId.Quadrant(i); + CreateTempTile(q, out var unityMapTile); + unityMapTile.LoadingState = LoadingState.Filler; + ActiveTiles[q] = unityMapTile; + ShowTile(unityMapTile); + activeChildren.Add(unityMapTile); + } + } } - } - } - if (quadrantCheck[0] || quadrantCheck[1] || quadrantCheck[2] || quadrantCheck[3]) - { - for (int i = 0; i < 4; i++) - { - if (quadrantCheck[i] == false) + if (frame.ParentFrame >= 0) { - CreateTempTile(quadrants[i], out var unityMapTile); - unityMapTile.LoadingState = LoadingState.Filler; - ActiveTiles[quadrants[i]] = unityMapTile; - ShowTile(unityMapTile); - activeChildren.Add(unityMapTile); - quadrantCheck[i] = true; + var parent = _delveStack[frame.ParentFrame]; + if (anyFound) + { + parent.Found |= 1 << frame.ParentSlot; + } + _delveStack[frame.ParentFrame] = parent; } + else + { + rootResult = anyFound; + } + _delveStack.RemoveAt(frameIdx); } - - return true; } - return false; + return rootResult; } protected void ShowTile(UnityMapTile unityTile) diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs index 0a9e29e82..fa9533eba 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Mapbox.BaseModule.Data.Tiles; using Mapbox.BaseModule.Utilities; +using Mapbox.ImageModule.Terrain.TerrainStrategies; using UnityEngine; using UnityEngine.Rendering; @@ -154,23 +155,24 @@ private void OnDestroy() // Unity does not auto-free runtime-created Meshes when the GameObject is // destroyed. Release the render mesh allocated in Awake and any dedicated - // collider mesh allocated by ElevatedTerrainStrategy (named "TerrainCollider"). - // Skip the shader-mode shared flat mesh ("TerrainSharedFlat") — it is owned by - // the strategy and disposed via TerrainLayerModule.OnDestroy. The identity - // checks avoid double-freeing an aliased render/collider mesh (FlatTerrainStrategy). - // Colliders may live on a dedicated-layer child GameObject; scan children too. + // collider mesh allocated by ElevatedTerrainStrategy. Skip the shader-mode + // shared flat mesh — it is owned by the strategy and disposed via + // TerrainLayerModule.OnDestroy. The identity checks avoid double-freeing + // an aliased render/collider mesh (FlatTerrainStrategy). Colliders may live + // on a dedicated-layer child GameObject; scan children too. Names are pulled + // from the strategy's constants so renames stay in sync. var renderMesh = _meshFilter != null ? _meshFilter.sharedMesh : null; var colliders = GetComponentsInChildren(includeInactive: true); foreach (var mc in colliders) { if (mc.sharedMesh != null && - mc.sharedMesh.name == "TerrainCollider" && + mc.sharedMesh.name == ElevatedTerrainStrategy.TerrainColliderMeshName && mc.sharedMesh != renderMesh) { Destroy(mc.sharedMesh); } } - if (renderMesh != null && renderMesh.name != "TerrainSharedFlat") + if (renderMesh != null && renderMesh.name != ElevatedTerrainStrategy.SharedFlatMeshName) { Destroy(renderMesh); } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs index 58aa5e0fb..7a7f982d0 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs @@ -54,6 +54,15 @@ public UnityTileTerrainContainer(UnityMapTile unityMapTile, Action elevationUpda /// Marks the tile container as temporary (using a parent's data while its own loads) or final. public void SetTerrainData(TerrainData terrainData, bool useShaderElevation, TileContainerState state = TileContainerState.Final) { + // Callers must pass a non-null terrainData; the rest of this method + // dereferences unconditionally. Bail fast in DEBUG builds so the contract + // is obvious if a future caller violates it. + if (terrainData == null) + { + Debug.LogError("UnityTileTerrainContainer.SetTerrainData called with null TerrainData; ignoring."); + return; + } + // Detach from the previous TerrainData (if any) before swapping. Without this, // a reassignment leaks our subscription on the old data and causes spurious // bounds updates if that data is shared with other tiles. @@ -62,7 +71,7 @@ public void SetTerrainData(TerrainData terrainData, bool useShaderElevation, Til TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; } - terrainData?.SetDisposeCallback(null); + terrainData.SetDisposeCallback(null); State = state; if (terrainData.Texture == null || terrainData.TileId.Z == 0) diff --git a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs index 3e000eed4..b0aa73fd7 100644 --- a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs @@ -27,6 +27,13 @@ protected virtual void Awake() if (MapBehaviour == null) MapBehaviour = FindObjectOfType(); if (Camera == null) Camera = Camera.main; + if (MapBehaviour == null) + { + Debug.LogError($"{GetType().Name} could not find a MapBehaviourCore in the scene. Assign the MapBehaviour field or add a Mapbox map to the scene; the camera will not initialize.", this); + enabled = false; + return; + } + MapBehaviour.Initialized += OnMapInitialized; } diff --git a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs index 02462ba06..6e47c3198 100644 --- a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs +++ b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs @@ -105,7 +105,10 @@ public override bool GetTileCover(IMapInformation mapInformation, TileCover tile _stack.Clear(); tileCover.Tiles.Clear(); - _maxZoom = (int)Mathf.Min(Settings.MaximumZoomLevel, mapInformation.AbsoluteZoom); + // Cap at 30: distToSplit uses (1 << (_maxZoom - zoom)), which overflows int + // past 30. Practically unreachable in Mercator (pyramid maxes around z22-24) + // but guards against Inspector misconfig of MaximumZoomLevel. + _maxZoom = (int)Mathf.Min(30f, Mathf.Min(Settings.MaximumZoomLevel, mapInformation.AbsoluteZoom)); var cam = Settings.Camera; GeometryUtility.CalculateFrustumPlanes(cam, _planes); From 8c2390d19219e114dcec6a6e8e71aeb87c4ea496 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 11:48:22 +0300 Subject: [PATCH 21/54] Tile pool race, multicast dispose, and CPU-mesh leak fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UnityMapTile: add Generation int. Bumped by PoolTile so deferred-pool consumers can detect tiles that were synchronously pool-recycled (and potentially re-issued via GetTile) between queue and flush. - MapboxMapVisualizer: replace _pendingPool List with List storing (tile, generation-snapshot). QueuePool snapshots gen; FlushPendingPool skips entries whose tile.Generation moved. Closes a critical re-borrow race where Load() between coroutine ticks could synchronously pool a deferred tile and re-issue it via GetTile, then the next flush re-pooled the now-reassigned reference. - MapboxMapVisualizer.Load: route _toRemove pools through QueuePool as well, so the same-frame parent-show + child-pool iOS-flicker fix applies to the zoom-out Load path too. - ElevatedTerrainStrategy: track deferred collider-rebuild closures in _pendingRebuilds. OnDestroy unsubscribes them, preventing the (tile, data) self-reference cycle that previously held both indefinitely when TerrainData was disposed before ElevationValuesUpdated fired. - ElevatedTerrainStrategy: fix CPU-elevation render-mesh leak. The CreateElevatedMesh path used tile.MeshFilter.mesh, which implicitly clones the assigned sharedMesh and orphans the original. Now uses .sharedMesh after RegisterTile pre-ensures a uniquely-owned per-tile mesh named "TerrainCpuMesh". The shader→CPU same-vertex-count transition is also handled. - MapboxTileData: replace single-callback SetDisposeCallback with AddDisposeCallback / RemoveDisposeCallback (multicast). Terrain data is shared across up to 16 render tiles (data zoom = render zoom − 2); the prior assign-overwrite meant only the most-recent consumer got the dispose notification, leaving 15 silently rendering with disposed data. UnityTileTerrainContainer and UnityTileImageContainer migrated to the add/remove pattern. - TerrainLayerModule.GetTileCoverCoroutines / LoadTiles: materialize GetDataId output into a fresh List before returning the lazy chain. The shared _dataIdScratch could be cleared by a subsequent GetDataId call before the lazy chain was consumed. - CustomImagery backward-compat: * CustomSource.CreateTile: restore empty-UrlFormat fallback to plain RasterTile (was throwing string.Format(null,…) otherwise). * CustomTMSTile: add 4-arg constructor overload matching the pre-3.0.7 signature (defaults: invertY=true, isMapboxService=false). External subclasses keep compiling. - SlippyMapCamera.Initialize: null-guard the FindObjectOfType() fallback. LogError + early return on null instead of NRE'ing on .transform. - CHANGELOG v3.0.7 Breaking changes: document CustomTMSTile constructor overloads and the MapboxTileData callback API change. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 + .../Data/DataFetchers/MapboxTileData.cs | 16 ++++- .../ElevatedTerrainStrategy.cs | 66 +++++++++++++++++-- .../BaseModule/Map/MapboxMapVisualizer.cs | 59 ++++++++++++++--- .../Mapbox/BaseModule/Unity/UnityMapTile.cs | 10 +++ .../Unity/UnityTileImageContainer.cs | 9 ++- .../Unity/UnityTileTerrainContainer.cs | 7 +- .../CustomImageryModule/CustomSource.cs | 7 ++ .../CustomImageryModule/CustomTMSTile.cs | 7 ++ .../Mapbox/Example/Scripts/SlippyMapCamera.cs | 8 ++- .../ImageModule/Terrain/TerrainLayerModule.cs | 15 +++-- 11 files changed, 176 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 216d8536e..23ef38255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ #### Breaking changes - `IMapInformation` gained a new member: `TerrainInfo Terrain { get; }`. Any project that ships its own `IMapInformation` implementation will need to add this property — return an instance of `TerrainInfo` (defaults are fine for a non-terrain map). The built-in `MapInformation` already implements it. +- `CustomTMSTile` constructor now takes two additional parameters (`invertY`, `isMapboxService`). The legacy 4-arg constructor (`urlFormat`, `tileId`, `tilesetId`, `useNonReadableTexture`) is preserved as an overload defaulting to `invertY: true, isMapboxService: false` — external subclasses keep compiling. New code should use the 6-arg form. +- `MapboxTileData.SetDisposeCallback` was replaced with `AddDisposeCallback` / `RemoveDisposeCallback` (multicast). This is an `internal` API; only relevant if you have access to it via reflection or an internal-visible-to dependency. #### Changes - Added new documentation. diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs index 881b1391d..69947dfd3 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs @@ -14,16 +14,28 @@ public class MapboxTileData public bool HasError = false; [HideInInspector] public byte[] Data; + // Multicast: a single TerrainData instance is shared across up to 16 render tiles + // (data zoom = render zoom − 2), each of which needs its own cleanup callback on + // eviction. The prior single-callback Set/overwrite pattern silently dropped 15 of + // them. Subscribers attach with AddDisposeCallback and must symmetrically remove. private Action _onDispose; public virtual void Dispose() { _onDispose?.Invoke(); + _onDispose = null; } - internal void SetDisposeCallback(Action callback) + internal void AddDisposeCallback(Action callback) { - _onDispose = callback; + if (callback == null) return; + _onDispose += callback; + } + + internal void RemoveDisposeCallback(Action callback) + { + if (callback == null) return; + _onDispose -= callback; } } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index 125aa523d..66d654bd7 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -68,9 +68,11 @@ public class ElevatedTerrainStrategy : TerrainStrategy, IElevationBasedTerrainSt // sub-region determines the actual surface); pointing every MeshFilter at this // single Mesh avoids per-tile Mesh allocation and upload. // Exposed for UnityMapTile.OnDestroy so its leak cleanup can identify which meshes - // it must skip (this one is owned by the strategy) versus destroy (per-tile collider). + // it must skip (this one is owned by the strategy) versus destroy (per-tile collider + // or CPU-elevated render mesh). public const string SharedFlatMeshName = "TerrainSharedFlat"; public const string TerrainColliderMeshName = "TerrainCollider"; + public const string TerrainCpuMeshName = "TerrainCpuMesh"; private Mesh _sharedFlatMesh; // Tracks the (TerrainData, CanonicalTileId) last used to build each MeshCollider's @@ -84,6 +86,12 @@ public class ElevatedTerrainStrategy : TerrainStrategy, IElevationBasedTerrainSt // GameObjects actually destroy (scene teardown, runtime tile-pool resize). private const int LastColliderBuildSweepThreshold = 256; private readonly List _deadColliderKeysScratch = new List(); + + // Tracks deferred-rebuild closures so OnDestroy can unsubscribe them. Without this + // list, if a TerrainData is disposed before ElevationValuesUpdated fires, the + // closure stays attached to the event and roots both tile + data via capture. + private readonly List<(TerrainData data, Action rebuild)> _pendingRebuilds = + new List<(TerrainData, Action)>(); public override int RequiredVertexCount { @@ -144,6 +152,19 @@ public override void Initialize(ElevationLayerProperties elOptions) /// public override void OnDestroy() { + // Detach any pending-rebuild subscriptions before disposing — closures + // would otherwise stay attached to TerrainData.ElevationValuesUpdated and + // root captured (tile, data) indefinitely if the event never fires. + for (int i = 0; i < _pendingRebuilds.Count; i++) + { + var entry = _pendingRebuilds[i]; + if (entry.data != null && entry.rebuild != null) + { + entry.data.ElevationValuesUpdated -= entry.rebuild; + } + } + _pendingRebuilds.Clear(); + if (_sharedFlatMesh != null) { UnityEngine.Object.Destroy(_sharedFlatMesh); @@ -191,11 +212,17 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) // when a runtime config change flips the tile shader→CPU and the new // RequiredVertexCount differs from _sharedFlatMesh.vertexCount). var sharedMesh = tile.MeshFilter.sharedMesh; - if (sharedMesh == _sharedFlatMesh) + if (sharedMesh == null || sharedMesh == _sharedFlatMesh) { - sharedMesh = new Mesh(); + sharedMesh = new Mesh { name = TerrainCpuMeshName }; tile.MeshFilter.sharedMesh = sharedMesh; } + else + { + // Re-tag any pre-existing per-tile mesh (e.g. the Awake mesh) so + // UnityMapTile.OnDestroy correctly destroys it on tile teardown. + sharedMesh.name = TerrainCpuMeshName; + } sharedMesh.Clear(); var newMesh = _baseMesh; sharedMesh.subMeshCount = 2; @@ -213,6 +240,23 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) if (createElevatedMesh) { + // Same-vertex-count shader→CPU transition: the else-if branch above didn't + // fire, but MeshFilter.sharedMesh might still be _sharedFlatMesh. Allocate + // a per-tile owned mesh in that case so CreateElevatedMesh's writes don't + // hit the shared one. + if (tile.MeshFilter.sharedMesh == null || tile.MeshFilter.sharedMesh == _sharedFlatMesh) + { + var perTile = new Mesh { name = TerrainCpuMeshName }; + perTile.subMeshCount = 2; + perTile.vertices = _baseMesh.Vertices; + perTile.normals = _baseMesh.Normals; + for (var i = 0; i < _baseMesh.Triangles.Count; i++) + { + perTile.SetTriangles(_baseMesh.Triangles[i], i); + } + perTile.uv = _baseMesh.Uvs; + tile.MeshFilter.sharedMesh = perTile; + } CreateElevatedMesh(tile); } @@ -255,18 +299,23 @@ private void RegisterCollider(UnityMapTile tile) { // Defer until values arrive. The callback self-unsubscribes on fire and // no-ops if the tile has since been recycled onto different data, so a late - // async readback does not stomp a freshly-reassigned tile. + // async readback does not stomp a freshly-reassigned tile. We also track + // the subscription in _pendingRebuilds so OnDestroy can detach pending + // closures whose data is disposed before the event fires. Action rebuild = null; + (TerrainData data, Action rebuild) entry = (data, null); rebuild = () => { + data.ElevationValuesUpdated -= rebuild; + _pendingRebuilds.Remove(entry); if (tile == null || tile.TerrainContainer == null || tile.TerrainContainer.TerrainData != data) { - data.ElevationValuesUpdated -= rebuild; return; } BuildAndAssignCollider(tile); - data.ElevationValuesUpdated -= rebuild; }; + entry.rebuild = rebuild; + _pendingRebuilds.Add(entry); data.ElevationValuesUpdated += rebuild; } } @@ -645,7 +694,10 @@ private static IEnumerator CompleteBakeAndAssign(MeshCollider meshCollider, Mesh private void CreateElevatedMesh(UnityMapTile tile) { - var mesh = tile.MeshFilter.mesh; + // Use sharedMesh (not .mesh) — the .mesh getter implicitly clones the assigned + // shared mesh on first access and orphans the original. We pre-ensured a + // uniquely-owned per-tile mesh in RegisterTile above. + var mesh = tile.MeshFilter.sharedMesh; var vertices = mesh.vertices; var sampleCount = (int)Mathf.Sqrt(mesh.vertexCount); for (int i = 0; i < vertices.Length; i++) diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 30caba7f6..a6f9ff075 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -33,7 +33,37 @@ public class MapboxMapVisualizer : IMapVisualizer // Tiles deferred for pooling on the next coroutine tick. // Pooling a child the same frame the parent first becomes visible flickers on iOS. - private readonly List _pendingPool = new List(); + // Stores (tile, generation-snapshot) so a flush can detect tiles that got + // synchronously pool-recycled and re-issued via GetTile() between queue and flush. + private struct PendingPoolEntry + { + public UnityMapTile Tile; + public int Generation; + } + private readonly List _pendingPool = new List(); + + private void QueuePool(UnityMapTile tile) + { + if (tile == null) return; + _pendingPool.Add(new PendingPoolEntry { Tile = tile, Generation = tile.Generation }); + } + + private void FlushPendingPool() + { + for (var i = 0; i < _pendingPool.Count; i++) + { + var entry = _pendingPool[i]; + // Skip entries whose tile was already pool-recycled (and possibly reused + // for a different id) between queueing and now. PoolTile bumps Generation, + // so a mismatch means we're stale. + if (entry.Tile == null || entry.Tile.Generation != entry.Generation) + { + continue; + } + PoolTile(entry.Tile); + } + _pendingPool.Clear(); + } // Explicit stack for the iterative DelveInto (was recursive with per-call new bool[4] // and new UnwrappedTileId[4] allocations). Bounded by recursion depth × 4, so the @@ -169,7 +199,11 @@ public virtual void Load(TileCover tileCover) TempTiles.Remove(tile); } - PoolTile(tile); + // Defer pooling by one frame for the iOS-flicker rationale. A subsequent + // Load before the flush may re-queue the same tile via the same code + // path — that's harmless because PoolTile bumps Generation and the flush + // filters by gen-snapshot match (second queue is skipped). + QueuePool(tile); } } @@ -194,12 +228,9 @@ public virtual void InternalUpdateCoroutine() // Flush tiles deferred from the previous tick. Holding the children active for // one extra frame after the parent's ShowTile lets the parent finish rendering // before the children deactivate, which avoids a one-frame transparent gap on - // iOS (see project_ios_tile_flicker note). - for (var i = 0; i < _pendingPool.Count; i++) - { - PoolTile(_pendingPool[i]); - } - _pendingPool.Clear(); + // iOS (see project_ios_tile_flicker note). FlushPendingPool filters out entries + // whose tile was already pool-recycled and re-issued between queue and flush. + FlushPendingPool(); //finish temp tiles from tempTiles list _toRemove.Clear(); @@ -218,7 +249,10 @@ public virtual void InternalUpdateCoroutine() if (tilePair.Children != null && tilePair.Children.Count > 0) { - _pendingPool.AddRange(tilePair.Children); + for (int c = 0; c < tilePair.Children.Count; c++) + { + QueuePool(tilePair.Children[c]); + } tilePair.Children.Clear(); } @@ -247,7 +281,7 @@ public virtual void InternalUpdateCoroutine() TempTiles.Remove(tile); } - _pendingPool.Add(tile); + QueuePool(tile); } } } @@ -463,6 +497,11 @@ protected void PoolTile(UnityMapTile tile) if (tile.LoadingState == LoadingState.None) return; + // Invalidate any deferred-pool entries that snapshotted this tile's previous + // generation — they'd otherwise re-pool the tile after it's been reissued via + // GetTile() for a different id. + tile.Generation++; + // Snapshot whether this tile is currently holding either bounds value. // Recycle() clears TerrainData below, so we have to check first. var terrain = _mapInformation.Terrain; diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs index fa9533eba..fa700fdaf 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs @@ -72,6 +72,13 @@ public void ApplyPropertyBlock() public LoadingState LoadingState; + // Incremented every time this tile is pool-recycled. Deferred-pool consumers + // (MapboxMapVisualizer._pendingPool) snapshot this when queuing and skip the + // flush if the value has moved — protects against re-borrow races where the + // tile got synchronously pooled and re-issued via GetTile() between queue and + // flush. + public int Generation; + public void Awake() { ImageContainer = new UnityTileImageContainer(this, DataDisposed); @@ -172,6 +179,9 @@ private void OnDestroy() Destroy(mc.sharedMesh); } } + // Destroy the render mesh if it's a per-tile instance. Skip the strategy-owned + // shared flat mesh (those are destroyed centrally in the strategy's OnDestroy). + // Both the unnamed Awake mesh and named per-tile CPU meshes get freed here. if (renderMesh != null && renderMesh.name != ElevatedTerrainStrategy.SharedFlatMeshName) { Destroy(renderMesh); diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs index 80454a5ac..502a47138 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs @@ -30,7 +30,10 @@ public UnityTileImageContainer(UnityMapTile unityMapTile, Action onDispose) public void SetImageData(RasterData imageData, TileContainerState state = TileContainerState.Final) { - ImageData?.SetDisposeCallback(null); + if (ImageData != null) + { + ImageData.RemoveDisposeCallback(_onDispose); + } State = state; if (imageData.Texture == null || imageData.TileId.Z == 0) @@ -39,7 +42,7 @@ public void SetImageData(RasterData imageData, TileContainerState state = TileCo } ImageData = imageData; - ImageData.SetDisposeCallback(_onDispose); + ImageData.AddDisposeCallback(_onDispose); OnImageryUpdated(); } @@ -65,7 +68,7 @@ public RasterData GetAndClearImageData() _unityMapTile.PropertyBlock.SetTexture(MainTex, Texture2D.blackTexture); _unityMapTile.ApplyPropertyBlock(); var rd = ImageData; - ImageData.SetDisposeCallback(null); + ImageData.RemoveDisposeCallback(_onDispose); ImageData = null; return rd; } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs index 7a7f982d0..6c389676c 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs @@ -69,17 +69,17 @@ public void SetTerrainData(TerrainData terrainData, bool useShaderElevation, Til if (TerrainData != null) { TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; + TerrainData.RemoveDisposeCallback(_onDisposeCallback); } - terrainData.SetDisposeCallback(null); - State = state; if (terrainData.Texture == null || terrainData.TileId.Z == 0) { Debug.Log("no texture?"); } TerrainData = terrainData; - TerrainData.SetDisposeCallback(_onDisposeCallback); + // Add (don't replace) — multiple render tiles share one TerrainData. + TerrainData.AddDisposeCallback(_onDisposeCallback); OnTerrainUpdated(); @@ -134,6 +134,7 @@ public TerrainData GetAndClearTerrainData() return null; TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; + TerrainData.RemoveDisposeCallback(_onDisposeCallback); _unityMapTile.PropertyBlock.SetTexture(HeightTexture, Texture2D.grayTexture); _unityMapTile.ApplyPropertyBlock(); var rd = TerrainData; diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSource.cs b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs index 3f838a686..80ccabcf8 100644 --- a/Runtime/Mapbox/CustomImageryModule/CustomSource.cs +++ b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs @@ -21,6 +21,13 @@ public CustomSource(CustomSourceSettings customSourceSettings, DataFetchingManag protected override RasterTile CreateTile(CanonicalTileId tileId, string tilesetId) { + // Empty UrlFormat: fall back to the base raster path (URL constructed from + // tilesetId by the source). Without this fallback CustomTMSTile.Initialize + // would call string.Format(null, …) and throw. + if (string.IsNullOrEmpty(_customSourceSettings.UrlFormat)) + { + return new RasterTile(tileId, tilesetId, true); + } return new CustomTMSTile(_customSourceSettings.UrlFormat, tileId, tilesetId, true, _customSourceSettings.InvertY, _customSourceSettings.IsMapboxService); } diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs index 5c096f890..7ebd0233a 100644 --- a/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs +++ b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs @@ -14,6 +14,13 @@ public class CustomTMSTile : RasterTile private string _urlFormat; private bool _invertY; private bool _isMapboxService; + + // Legacy 4-arg constructor preserved so external subclasses compiled against the + // 3.0.6 API keep building. Defaults match the pre-3.0.7 behavior (invertY = true, + // non-Mapbox-service URL request path). + public CustomTMSTile(string urlFormat, CanonicalTileId tileId, string tilesetId, bool useNonReadableTexture) + : this(urlFormat, tileId, tilesetId, useNonReadableTexture, invertY: true, isMapboxService: false) { } + public CustomTMSTile(string urlFormat, CanonicalTileId tileId, string tilesetId, bool useNonReadableTexture, bool invertY, bool isMapboxService) : base(tileId, tilesetId, useNonReadableTexture) { _urlFormat = urlFormat; diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs index 9e1d2a680..963ed0e3f 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs @@ -79,7 +79,13 @@ public void Initialize(Camera camera, IMapInformation start, Plane? controlPlane if (controlPlane.HasValue) _controlPlane = controlPlane.Value; if (_rotationSettings.MapRoot == null) { - _rotationSettings.MapRoot = GameObject.FindObjectOfType().transform; + var foundRoot = GameObject.FindObjectOfType(); + if (foundRoot == null) + { + Debug.LogError("SlippyMapCamera.Initialize: rotationSettings.MapRoot is unset and no MapBehaviourCore could be found in the scene. Camera rotation will not work; assign a MapRoot explicitly."); + return; + } + _rotationSettings.MapRoot = foundRoot.transform; } Pitch = start.Pitch; Bearing = start.Bearing; diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index 4df4fd91a..9be2ec038 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -149,13 +149,20 @@ public virtual IEnumerator LoadTileData(CanonicalTileId tileId, Action tiles) { - yield return _rasterSource.LoadTilesCoroutine(GetDataId(tiles)); + // Materialize before passing to the coroutine: the IEnumerable returned by + // GetDataId is the shared _dataIdScratch set. If LoadTilesCoroutine consumes + // lazily and another GetDataId call clears the scratch before it finishes, + // the consumer would see the wrong contents. + var materialized = new List(GetDataId(tiles)); + yield return _rasterSource.LoadTilesCoroutine(materialized); } - + public IEnumerable GetTileCoverCoroutines(IEnumerable tiles) { - var targetTiles = GetDataId(tiles).Distinct(); - return targetTiles.Select(x => LoadTileData(x)).Where(x => x != null); + // Same materialization rationale: callers iterate this lazily downstream and + // would otherwise read a mutated _dataIdScratch after another GetDataId call. + var materialized = new List(GetDataId(tiles)); + return materialized.Select(LoadTileData).Where(x => x != null); } #endregion From 5108edc2715ec8675deee8702ab7631f55748f67 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 12:07:05 +0300 Subject: [PATCH 22/54] Input mutex, terrain bounds, pool drain, version bump to 3.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MapInput: pinch/tilt gesture decision now made once per frame in UpdateInputState (exposed via CurrentTwoFingerGesture enum + pinch/tilt delta fields). Closes the race where both detectors independently updated _previousPinchDistance and could fire same frame, contradicting CameraSystem.md. - Moving3dCamera.Zoom: guard against div-by-zero. preDistance ≤ ε or camera→cursor distance ≤ ε (top-down ortho, first-frame, or curve evaluating to 0) previously propagated NaN into _targetPosition. Fall back to a straight target snap. - UnityTileTerrainContainer.SetTerrainData: detach prior subscriptions unconditionally, then early-return on null. The previous order leaked the prior subscription when callers passed null to clear the slot. - TerrainInfo: conservative defaults (MinElevation=0, MaxElevation=5000) in field initializers. RecomputeTerrainBounds also resets to these when ActiveTiles is empty so the first-frame AABB isn't flat and steep terrain doesn't get culled on initial paint. - MapboxMapVisualizer: hoist hard-coded recursion-cap literal (22) to public const MaxMercatorZoom; DelveInto reads it. - UnityTileProvider: warn at construction if MinimumZoomLevel > MaximumZoomLevel (was silently masked by the absolute zoom cap). - Sync/AsyncExtractElevationArray.ExtractHeightData(Texture2D, Action): allocate fresh float[] instead of renting from ElevationArrayPool. External callers had no return contract and would drain the pool. - SimplificationFactorDrawer: wrap in BeginProperty/EndProperty (prefab- override + multi-edit safety) and remove the silent first-paint snap. Legacy non-preset values now display a warning HelpBox; the property only writes when the user picks a preset (deliberate, undoable). - TerrainColliderOptionsDrawer.GetPropertyHeight: drop one extra line+spacing — the foldout body has 4 line-rows + 1 var-row. - package.json: 3.0.7 → 3.1.0. Minor bump per semver: source-breaking changes (TerrainData.ElevationValuesUpdated event conversion, IMapInformation.Terrain addition, CustomTMSTile ctor extension) require ≥minor. - CHANGELOG: rename section to v3.1.0; document Burst dep, event conversion, CustomTMSTile/CustomSource behavioral changes; expand Fixes list to reflect the v3.0.7→3.1.0 review-fix work. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 22 +- .../BaseModule/Map/MapboxMapVisualizer.cs | 14 +- Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs | 14 +- .../Unity/UnityTileTerrainContainer.cs | 21 +- Runtime/Mapbox/Example/Scripts/MapInput.cs | 217 ++++++++---------- .../Mapbox/Example/Scripts/Moving3dCamera.cs | 16 +- .../Editor/SimplificationFactorDrawer.cs | 34 ++- .../Editor/TerrainColliderOptionsDrawer.cs | 4 +- .../AsyncExtractElevationArray.cs | 5 +- .../SyncExtractElevationArray.cs | 5 +- .../TileProvider/UnityTileProvider.cs | 5 + package.json | 2 +- 12 files changed, 204 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ef38255..643000843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,17 @@ ## CHANGELOG -### v3.0.7 +### v3.1.0 + +Minor-version bump per semver: this release contains source-breaking API changes (listed below) alongside the v3.0.7 feature work. The 3.0.7 designation was previously used in development; published 3.0.7 builds, if any, are superseded by 3.1.0. + +#### New dependencies +- Added `com.unity.burst@1.8.12`. The terrain-RGB elevation decoder and the collider vertex-fill jobs are now Burst-compiled. First-time domain reload pays a one-shot AOT compile cost; runtime decoding is significantly faster. #### Breaking changes - `IMapInformation` gained a new member: `TerrainInfo Terrain { get; }`. Any project that ships its own `IMapInformation` implementation will need to add this property — return an instance of `TerrainInfo` (defaults are fine for a non-terrain map). The built-in `MapInformation` already implements it. +- `TerrainData.ElevationValuesUpdated` is now declared `public event Action` (was a plain `public Action` field). External code can subscribe with `+=` / unsubscribe with `-=` as before, but **direct assignment (`= myCallback`) and direct invocation (`ElevationValuesUpdated()`) no longer compile**. Migrate to `+=` for subscription. The previous single-setter `SetElevationChangedCallback` (which silently wiped other subscribers) has been removed. - `CustomTMSTile` constructor now takes two additional parameters (`invertY`, `isMapboxService`). The legacy 4-arg constructor (`urlFormat`, `tileId`, `tilesetId`, `useNonReadableTexture`) is preserved as an overload defaulting to `invertY: true, isMapboxService: false` — external subclasses keep compiling. New code should use the 6-arg form. +- `CustomSource.CreateTile` now always returns a `CustomTMSTile` when `UrlFormat` is set, regardless of `InvertY`. Previously `InvertY=false` returned a plain `RasterTile`. If you relied on the plain-raster path with a non-empty `UrlFormat`, set `UrlFormat = ""` to keep that behavior — empty `UrlFormat` falls back to plain `RasterTile`. - `MapboxTileData.SetDisposeCallback` was replaced with `AddDisposeCallback` / `RemoveDisposeCallback` (multicast). This is an `internal` API; only relevant if you have access to it via reflection or an internal-visible-to dependency. #### Changes @@ -12,10 +19,23 @@ - Added a new demo scene demonstrating how to use POI information. - Introduced new inspector scripts to improve editor UI/UX. - Reorganized and cleaned up the Mapbox context menu used for creating Mapbox ScriptableObjects. +- New camera system (`MapCameraBehaviour`, `SlippyMapCameraBehaviour`, `Moving3dCameraBehaviour`) with touch input, two-finger pinch/tilt, and optional Input System package support. See `Documentation~/CameraSystem.md`. +- Tile-provider LOD overhaul: forward-projected distance, acute-angle compensation, frustum buffer, and reusable scratch arrays. Tile counts at high pitch are 40–60% lower vs the prior screen-fraction split. +- Terrain rendering: Burst-compiled decode, async PhysX collider bake, shared-flat-mesh + MaterialPropertyBlock per-tile state (saves Material allocations), bilinear height sampling. +- `TerrainInfo` exposes observed Min/Max elevation on `IMapInformation.Terrain`; tile-provider AABBs use it. #### Fixes - Fixed an issue where vector layer range limits were not applied correctly during data processing. - Fixed incorrect limit validation and bounds checking in the `LatitudeLongitude` struct. +- Fixed deferred-pool re-borrow race in `MapboxMapVisualizer` (generation counter). +- Fixed multicast dispose notification — shared `TerrainData` now notifies all consumers on eviction. +- Fixed CPU-elevation mesh leak via `.mesh` clone path. +- Fixed `_sharedFlatMesh` clear regression on shader→CPU transition. +- Fixed terrain bounds remaining pinned at 0 when all loaded tiles were at/above sea level. +- Fixed iOS tile flicker on zoom out (child-pool deferred by one frame after parent show). +- Fixed `Moving3dCamera.Zoom()` NaN when `preDistance` or camera→cursor distance is 0. +- Fixed pinch/tilt mutual exclusion (decision now made once per frame in `UpdateInputState`). +- Fixed pan jump after pinch-to-single-finger transition (re-seed drag origin on touch-count decrease). ### v3.0.6 diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index a6f9ff075..41b727c16 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -16,6 +16,11 @@ namespace Mapbox.BaseModule.Map [Serializable] public class MapboxMapVisualizer : IMapVisualizer { + // Upper bound of the Mercator tile pyramid. Used by DelveInto and any other + // recursion that needs an absolute hard cap independent of user-configurable + // MaximumZoomLevel settings (which represent LOD intent, not pyramid depth). + public const int MaxMercatorZoom = 22; + public List LayerModules; public Dictionary ActiveTiles { get; private set; } public List TempTiles { get; private set; } @@ -426,7 +431,7 @@ protected bool DelveInto(UnwrappedTileId tileId, List activeChildr frame.Found |= 1 << slot; _delveStack[frameIdx] = frame; } - else if (frame.Depth > 0 && frame.TileId.Z < 22) + else if (frame.Depth > 0 && frame.TileId.Z < MaxMercatorZoom) { // Save the advanced NextChild before pushing the child frame — // we'll resume this slot's evaluation after the child finishes. @@ -554,8 +559,11 @@ private void RecomputeTerrainBounds() if (td.MinElevation < min) min = td.MinElevation; } } - terrain.MaxElevation = any ? max : 0f; - terrain.MinElevation = any ? min : 0f; + // When no tile is contributing, fall back to TerrainInfo's conservative + // defaults rather than 0 — a flat AABB would frustum-cull plausible + // mountains on the next frame before any sample arrives. + terrain.MaxElevation = any ? max : TerrainInfo.DefaultMaxElevation; + terrain.MinElevation = any ? min : TerrainInfo.DefaultMinElevation; } protected void CreateTempTile(UnwrappedTileId tileId, out UnityMapTile tile) diff --git a/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs index f6d55fb80..c1a749ad7 100644 --- a/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs +++ b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs @@ -9,14 +9,24 @@ namespace Mapbox.BaseModule.Map [Serializable] public class TerrainInfo { + // Conservative defaults used on the first frame before any terrain tile is + // sampled, and again whenever ActiveTiles drops to zero. The MaxElevation + // floor keeps the tile-provider's bounds-height AABB tall enough to admit + // plausible mountain ranges (above Mt. Whitney) so steep terrain isn't + // frustum-culled on first paint. + public const float DefaultMinElevation = 0f; + public const float DefaultMaxElevation = 5000f; + /// /// Lowest observed elevation in meters across currently loaded terrain tiles. + /// Initialized to . /// - public float MinElevation; + public float MinElevation = DefaultMinElevation; /// /// Highest observed elevation in meters across currently loaded terrain tiles. + /// Initialized to . /// - public float MaxElevation; + public float MaxElevation = DefaultMaxElevation; } } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs index 6c389676c..af1acf950 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs @@ -54,24 +54,21 @@ public UnityTileTerrainContainer(UnityMapTile unityMapTile, Action elevationUpda /// Marks the tile container as temporary (using a parent's data while its own loads) or final. public void SetTerrainData(TerrainData terrainData, bool useShaderElevation, TileContainerState state = TileContainerState.Final) { - // Callers must pass a non-null terrainData; the rest of this method - // dereferences unconditionally. Bail fast in DEBUG builds so the contract - // is obvious if a future caller violates it. - if (terrainData == null) - { - Debug.LogError("UnityTileTerrainContainer.SetTerrainData called with null TerrainData; ignoring."); - return; - } - - // Detach from the previous TerrainData (if any) before swapping. Without this, - // a reassignment leaks our subscription on the old data and causes spurious - // bounds updates if that data is shared with other tiles. + // Detach from the previous TerrainData unconditionally — null-callers + // (deliberately or accidentally clearing the slot) would otherwise leak + // the prior subscriptions on the shared TerrainData. if (TerrainData != null) { TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; TerrainData.RemoveDisposeCallback(_onDisposeCallback); } + if (terrainData == null) + { + TerrainData = null; + return; + } + State = state; if (terrainData.Texture == null || terrainData.TileId.Z == 0) { diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index dc7592399..989ee21a2 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -52,6 +52,21 @@ public abstract class MapInput // touches[0] — without this, single-finger pan jumps after a pinch ends. protected bool TouchCountDecreasedThisFrame { get; private set; } + public enum TwoFingerGesture + { + None, + Pinch, + Tilt + } + + // Per-frame two-finger gesture decision. Computed once in UpdateInputState so + // pinch and tilt detectors read consistent state and exactly one of them can + // fire per frame — without this, both helpers update _previousPinchDistance + // independently and tilt's pinch-comparison sees stale (post-update) state. + protected TwoFingerGesture CurrentTwoFingerGesture { get; private set; } + private float _pinchDeltaThisFrame; + private float _tiltAvgDeltaYThisFrame; + public virtual void Initialize(Camera camera, IMapInformation mapInfo) { _camera = camera ? camera : Camera.main; @@ -131,8 +146,68 @@ protected void UpdateInputState() var touchCount = GetTouchCount(); TouchCountDecreasedThisFrame = touchCount < _previousTouchCount; _previousTouchCount = touchCount; + + // Reset gesture state every frame; only re-arm if a valid two-finger + // pinch/tilt gesture is currently detected. + CurrentTwoFingerGesture = TwoFingerGesture.None; + _pinchDeltaThisFrame = 0f; + _tiltAvgDeltaYThisFrame = 0f; + if (touchCount < 2) + { _pinchActive = false; + return; + } + + // Read both touches' positions and Y-deltas, then pick a single gesture for + // this frame. Doing this once here (not redundantly inside each detector) + // keeps the decision symmetric and prevents stale-state races. + Vector2 touch0Pos, touch1Pos; + float touch0DeltaY, touch1DeltaY; +#if MAPBOX_NEW_INPUT_SYSTEM + var t0 = Touch.activeTouches[0]; + var t1 = Touch.activeTouches[1]; + touch0Pos = t0.screenPosition; + touch1Pos = t1.screenPosition; + touch0DeltaY = t0.delta.y; + touch1DeltaY = t1.delta.y; +#else + var t0 = Input.GetTouch(0); + var t1 = Input.GetTouch(1); + touch0Pos = t0.position; + touch1Pos = t1.position; + touch0DeltaY = t0.deltaPosition.y; + touch1DeltaY = t1.deltaPosition.y; +#endif + var currentDistance = Vector2.Distance(touch0Pos, touch1Pos); + + if (!_pinchActive) + { + // First frame of two-finger contact: just seed the previous distance. + // No gesture decision until next frame produces a real delta. + _pinchActive = true; + _previousPinchDistance = currentDistance; + return; + } + + var pinchDelta = currentDistance - _previousPinchDistance; + _previousPinchDistance = currentDistance; + + var pinchMag = Mathf.Abs(pinchDelta); + var avgDeltaY = (touch0DeltaY + touch1DeltaY) / 2f; + var tiltMag = Mathf.Abs(avgDeltaY); + var bothFingersSameDirection = touch0DeltaY * touch1DeltaY > 0f; + + if (pinchMag > tiltMag && pinchMag > 0.01f) + { + CurrentTwoFingerGesture = TwoFingerGesture.Pinch; + _pinchDeltaThisFrame = pinchDelta; + } + else if (bothFingersSameDirection && tiltMag >= 1f && tiltMag > pinchMag) + { + CurrentTwoFingerGesture = TwoFingerGesture.Tilt; + _tiltAvgDeltaYThisFrame = avgDeltaY; + } } /// @@ -250,46 +325,27 @@ protected bool GetSecondaryHeld() } /// - /// Detects pinch-to-zoom (two fingers) or mouse scroll wheel. - /// Returns true if zoom input is active, with the delta as a zoom level change. - /// Positive = zoom in, negative = zoom out. - /// When two fingers are active, only returns true if pinch is the dominant gesture (vs tilt). + /// Returns true if zoom input was detected this frame (pinch or mouse scroll). + /// Pinch/tilt mutual-exclusion is enforced in — + /// this helper just reads the cached decision and emits the scaled delta. /// protected bool GetPinchZoomDelta(out float zoomDelta, float pinchSensitivity = 5f) { zoomDelta = 0f; -#if MAPBOX_NEW_INPUT_SYSTEM - var touchCount = Touch.activeTouches.Count; - if (touchCount >= 2) + if (CurrentTwoFingerGesture == TwoFingerGesture.Pinch) { - var touch0 = Touch.activeTouches[0]; - var touch1 = Touch.activeTouches[1]; - var currentDistance = Vector2.Distance(touch0.screenPosition, touch1.screenPosition); - - if (!_pinchActive) - { - _pinchActive = true; - _previousPinchDistance = currentDistance; - return false; - } - - var pinchDelta = currentDistance - _previousPinchDistance; - _previousPinchDistance = currentDistance; - - var avgDeltaY = (touch0.delta.y + touch1.delta.y) / 2f; - var pinchMagnitude = Mathf.Abs(pinchDelta); - var tiltMagnitude = Mathf.Abs(avgDeltaY); - - if (pinchMagnitude > tiltMagnitude && pinchMagnitude > 0.01f) - { - zoomDelta = pinchDelta / Screen.height * pinchSensitivity; - return true; - } + zoomDelta = _pinchDeltaThisFrame / Screen.height * pinchSensitivity; + return true; + } + // Mouse scroll fallback only when no two-finger gesture is active. + if (GetTouchCount() >= 2) + { return false; } +#if MAPBOX_NEW_INPUT_SYSTEM if (Mouse.current != null) { var scroll = Mouse.current.scroll.ReadValue(); @@ -302,111 +358,30 @@ protected bool GetPinchZoomDelta(out float zoomDelta, float pinchSensitivity = 5 return true; } } - - return false; #else - if (Input.touchCount >= 2) - { - var touch0 = Input.GetTouch(0); - var touch1 = Input.GetTouch(1); - var currentDistance = Vector2.Distance(touch0.position, touch1.position); - - if (!_pinchActive) - { - _pinchActive = true; - _previousPinchDistance = currentDistance; - return false; - } - - var pinchDelta = currentDistance - _previousPinchDistance; - _previousPinchDistance = currentDistance; - - // Compare pinch magnitude vs tilt magnitude to pick the dominant gesture - var avgDeltaY = (touch0.deltaPosition.y + touch1.deltaPosition.y) / 2f; - var pinchMagnitude = Mathf.Abs(pinchDelta); - var tiltMagnitude = Mathf.Abs(avgDeltaY); - - if (pinchMagnitude > tiltMagnitude && pinchMagnitude > 0.01f) - { - zoomDelta = pinchDelta / Screen.height * pinchSensitivity; - return true; - } - - return false; - } - if (Input.mouseScrollDelta.magnitude > 0) { zoomDelta = Input.GetAxis("Mouse ScrollWheel"); return true; } - - return false; #endif + return false; } /// - /// Detects two-finger vertical drag for pitch/tilt control. - /// Both fingers moving in the same vertical direction = tilt. - /// Only returns true if tilt is the dominant gesture (vs pinch). + /// Returns true if a two-finger vertical-tilt gesture was detected this frame. + /// Pinch/tilt mutual-exclusion is enforced in — + /// this helper just reads the cached decision and emits the scaled delta. /// protected bool GetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity = 0.5f) { + if (CurrentTwoFingerGesture == TwoFingerGesture.Tilt) + { + tiltDelta = _tiltAvgDeltaYThisFrame / Screen.height * tiltSensitivity; + return true; + } tiltDelta = 0f; - -#if MAPBOX_NEW_INPUT_SYSTEM - if (Touch.activeTouches.Count < 2) - return false; - - var touch0 = Touch.activeTouches[0]; - var touch1 = Touch.activeTouches[1]; - - var deltaY0 = touch0.delta.y; - var deltaY1 = touch1.delta.y; - - if (deltaY0 * deltaY1 <= 0) - return false; - - var avgDeltaY = (deltaY0 + deltaY1) / 2f; - if (Mathf.Abs(avgDeltaY) < 1f) - return false; - - var currentDistance = Vector2.Distance(touch0.screenPosition, touch1.screenPosition); - var pinchDelta = _pinchActive ? Mathf.Abs(currentDistance - _previousPinchDistance) : 0f; - - if (Mathf.Abs(avgDeltaY) <= pinchDelta) - return false; - - tiltDelta = avgDeltaY / Screen.height * tiltSensitivity; - return true; -#else - if (Input.touchCount < 2) - return false; - - var touch0 = Input.GetTouch(0); - var touch1 = Input.GetTouch(1); - - var deltaY0 = touch0.deltaPosition.y; - var deltaY1 = touch1.deltaPosition.y; - - // Both fingers must move in the same vertical direction - if (deltaY0 * deltaY1 <= 0) - return false; - - var avgDeltaY = (deltaY0 + deltaY1) / 2f; - if (Mathf.Abs(avgDeltaY) < 1f) - return false; - - // Compare tilt magnitude vs pinch magnitude to pick the dominant gesture - var currentDistance = Vector2.Distance(touch0.position, touch1.position); - var pinchDelta = _pinchActive ? Mathf.Abs(currentDistance - _previousPinchDistance) : 0f; - - if (Mathf.Abs(avgDeltaY) <= pinchDelta) - return false; - - tiltDelta = avgDeltaY / Screen.height * tiltSensitivity; - return true; -#endif + return false; } /// diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs index e1b9ee0de..ce4859862 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs @@ -144,9 +144,21 @@ public void Zoom(IMapInformation mapInformation, Vector3 position, float zoomAct var preDistance = CalculateCameraDistance(mapInformation, ZoomValue); var camDistanceToMouse = Vector3.Distance(_camera.transform.position, position); ZoomValue = postZoom; - var postDistance = CalculateCameraDistance(mapInformation, postZoom); - var newCamDistanceToMouse = camDistanceToMouse * (postDistance / preDistance); CameraDistance = CalculateCameraDistance(mapInformation, ZoomValue); + + // Guard against div-by-zero: top-down orthographic, first-frame state, or + // a camera curve that evaluates to 0 at this zoom can make either divisor + // 0 and propagate NaN into _targetPosition. Fall back to a straight target + // snap when the cursor-zoom math has nothing meaningful to interpolate. + const float MinDistance = 0.0001f; + if (preDistance <= MinDistance || camDistanceToMouse <= MinDistance) + { + _targetPosition = postZoomTarget; + return; + } + + var postDistance = CameraDistance; + var newCamDistanceToMouse = camDistanceToMouse * (postDistance / preDistance); _targetPosition = Vector3.LerpUnclamped(postZoomTarget, postZoomPos, (camDistanceToMouse - newCamDistanceToMouse) / camDistanceToMouse); } diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs index a99191ebf..18f650c0a 100644 --- a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs @@ -43,27 +43,43 @@ private void EnsurePresetsCached(SimplificationFactorAttribute attr) public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + label = EditorGUI.BeginProperty(position, label, property); + var attr = (SimplificationFactorAttribute)attribute; EnsurePresetsCached(attr); var factors = _cachedFactors; var labels = _cachedLabels; - // Snap legacy non-preset values to the nearest preset so the dropdown always - // matches a concrete entry. var current = property.intValue; - if (System.Array.IndexOf(factors, current) < 0) + var inPresets = System.Array.IndexOf(factors, current) >= 0; + // Don't write through SerializedProperty during paint just because the + // value isn't in the preset list — that silently dirties every legacy + // asset on first open. Show a warning and let the user pick a preset + // (which Unity then records as a deliberate, undoable change). + var fieldRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + EditorGUI.BeginChangeCheck(); + var newValue = EditorGUI.IntPopup(fieldRect, label, current, labels, factors); + if (EditorGUI.EndChangeCheck()) { - property.intValue = NearestPreset(factors, current); - current = property.intValue; + property.intValue = newValue; } - var fieldRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); - property.intValue = EditorGUI.IntPopup(fieldRect, label, current, labels, factors); - - var (message, severity) = BuildMessage(property.intValue, attr.VertexBase); + string message; + MessageType severity; + if (!inPresets) + { + message = $"Legacy value {current} is not a preset. Pick a preset above to migrate; the value is otherwise left untouched."; + severity = MessageType.Warning; + } + else + { + (message, severity) = BuildMessage(property.intValue, attr.VertexBase); + } var helpHeight = CalcHelpBoxHeight(message, position.width); var helpRect = new Rect(position.x, fieldRect.yMax + Spacing, position.width, helpHeight); EditorGUI.HelpBox(helpRect, message, severity); + + EditorGUI.EndProperty(); } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs index 60f1a3fef..bc0e02989 100644 --- a/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs @@ -68,8 +68,8 @@ public override float GetPropertyHeight(SerializedProperty property, GUIContent } var colliderLayer = property.FindPropertyRelative("colliderLayerId"); - // foldout + addCollider + asyncBake + useDedicated + colliderLayer - return line + spacing + line + spacing + line + spacing + line + spacing + line + spacing + EditorGUI.GetPropertyHeight(colliderLayer, true); + // foldout + addCollider + asyncBake + useDedicated + colliderLayer = 4 line-rows + 1 var-row + return line + spacing + line + spacing + line + spacing + line + spacing + EditorGUI.GetPropertyHeight(colliderLayer, true); } } } diff --git a/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs b/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs index 1366605ce..1fff29f0b 100644 --- a/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs +++ b/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs @@ -23,7 +23,10 @@ public void ExtractHeightData(Texture2D texture, Action callback = null { var width = t.width; var data = t.GetData(); - var heightData = ElevationArrayPool.Rent(width * width); + // Fresh allocation, not pool-rented: this overload hands the buffer to + // external code with no return contract. Pool-renting here would drain + // the pool over the session as callers never call Return. + var heightData = new float[width * width]; RunDecodeJob(data, width, heightData, out _, out _); callback?.Invoke(heightData); }); diff --git a/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs b/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs index 9abc0ae81..38a056a9c 100644 --- a/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs +++ b/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs @@ -20,7 +20,10 @@ public void ExtractHeightData(Texture2D texture, Action callback) { var rgbData = texture.GetRawTextureData(); var width = texture.width; - var heightData = ElevationArrayPool.Rent(width * width); + // Fresh allocation, not pool-rented: this overload hands the buffer to + // external code with no return contract. Pool-renting here would drain + // the pool over the session as callers never call Return. + var heightData = new float[width * width]; RunDecodeJob(rgbData, width, heightData, out _, out _); callback?.Invoke(heightData); } diff --git a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs index 6e47c3198..095a14897 100644 --- a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs +++ b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs @@ -83,6 +83,11 @@ public UnityTileProvider(UnityTileProviderSettings settings) if (Settings.Camera == null) Settings.Camera = Camera.main; + if (Settings.MinimumZoomLevel > Settings.MaximumZoomLevel) + { + Debug.LogWarning($"UnityTileProvider: MinimumZoomLevel ({Settings.MinimumZoomLevel}) is greater than MaximumZoomLevel ({Settings.MaximumZoomLevel}). Tiles will be force-split until the absolute zoom cap. Check the Inspector configuration."); + } + _pool = new TileNode[256]; _stack = new Stack(256); } diff --git a/package.json b/package.json index 8cff0dc8d..6b12bb2e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.mapbox.sdk", - "version": "3.0.7", + "version": "3.1.0", "displayName": "Mapbox Unity SDK", "description": "Mapbox Unity SDK", "unity": "2022.3", From f52a3e036aef4b7e30a0150d78bdc8a28c519e7c Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 13:21:46 +0300 Subject: [PATCH 23/54] Terrain bounds plumbing, pool race blockers, recorder removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blockers - TerrainSource.ExtractElevationValues: switch to the TerrainData overload of ExtractHeightData so Min/MaxElevation are decoded inline. The previous Action + 1-arg SetElevationValues path left Min=Max=0 on every tile loaded through LoadTileCoroutine — defeating the RecomputeTerrainBounds fix entirely. Wait for completion via a one-shot ElevationValuesUpdated subscription. - MapboxMapVisualizer.CreateTile: shader-mode tiles complete on texture-ready before CPU decode arrives; subscribe a one-shot watcher on TerrainData so late-arriving Min/Max widen TerrainInfo when the async decode lands. Watches are de-duped per TerrainData (shared across 16 render tiles) and detached in OnDestroy. - MapboxMapVisualizer.CreateTileInstant failure path: bump tile.Generation before PutTile. Mirrors PoolTile so any stale PendingPoolEntry from a previous queue is invalidated on the next flush — closes a re-borrow bypass that bypassed all the protection added elsewhere. - UnityTileTerrainContainer.OnDestroy: also RemoveDisposeCallback, not just the ElevationValuesUpdated handler. Otherwise a later eviction of shared TerrainData fires the multicast into a destroyed tile. - MapboxExamples.asmdef: drop hard refs to Unity.Recorder and Unity.Recorder.Editor. These were a hard build dep for any project that didn't have the Recorder package, contradicting CameraSystem.md's "optional package" framing. The benchmark's recorder-using code is removed alongside (see Changes below). Visualizer / tile provider - Deferred RecomputeTerrainBounds via _terrainBoundsDirty flag. PoolTile and CreateTile mark dirty; InternalUpdateCoroutine drains once per tick. Eliminates O(N²) when recursive PoolTile pools a subtree. - Tightened the contributor check so flat tiles (Min==Max==0) don't trigger unnecessary recomputes. - OnDestroy now flushes _pendingPool so deferred tiles get Recycle() at teardown (pooled elevation arrays return, shared meshes restore). Also detaches any pending async-decode watchers. - Filler-protection pass also bumps each rescued child's Generation so any in-flight PendingPoolEntry doesn't silently re-pool it on the same frame. - UnityTileProvider.boundsHeight now uses [MinElevation, MaxElevation] in world units. Sub-sea-level tiles (Death Valley, Dead Sea) used to fall outside the AABB and frustum-cull. TileNode.Set takes the bottom too. - DistToSplitScale: floor `dz` at ε. dz=0 (camera exactly at ground) made r=d/0 → Inf/NaN and the tile pinned to its coarsest LOD forever. Strategy / async - AsyncExtractElevationArray: guard the GPU-readback callback against TerrainData.IsDisposed both before and after the decode. Without this, late callbacks assigned into evicted data and leaked the pool-rented buffer. - TerrainData.IsDisposed flag (flipped by Dispose) for the above guard. - RegisterCollider deferred path de-dups by (data, tile) against _pendingRebuilds. Temp→final transitions previously added two closures and ran two async bakes per tile. - ColliderChildName renamed from "TerrainCollider" → "TerrainColliderChild" so it doesn't collide with TerrainColliderMeshName (same string used for the dedicated-layer GameObject name and the per-tile collider mesh name). - BakeColliderJob: add a comment explaining it is intentionally not [BurstCompile] (single P/Invoke into PhysX). Editor - TerrainSettingsInspectorHelper: stop writing child.boolValue = true during OnGUI. Runtime already evaluates NeedsCpuElevation (an OR over the forcing flags) so the persisted value is irrelevant. Writing during paint silently dirtied every inspected asset and bypassed prefab overrides. - SimplificationFactorDrawer: set EditorGUI.showMixedValue from property.hasMultipleDifferentValues for proper multi-object editing indicator. Remove the now-dead NearestPreset helper. Cleanup - TerrainData: cache sqrt(ElevationValues.Length) in _cachedWidth so QueryHeightData / ReadElevation don't recompute it per call. - ElevationArrayPool: clear the static dict on play-mode SubsystemRegistration. In editor, static state otherwise survives play-mode stop, retaining last session's buffers across runs. - CustomTerrainSource.CreateTile: mirror the empty-UrlFormat fallback that was added in CustomSource so an unset URL doesn't throw from string.Format. - Moving3dCamera / SlippyMapCamera pan branches: gate HasChanged on non-zero movement. Stationary touches no longer trigger a redraw round-trip. - CHANGELOG: note the CustomTerrainLayerModuleScript DataSettings behavior change and the TileProviderBenchmark Recorder-removal. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 + .../Data/DataFetchers/ElevationArrayPool.cs | 14 + .../Data/DataFetchers/TerrainData.cs | 16 +- .../ElevatedTerrainStrategy.cs | 28 +- .../BaseModule/Map/MapboxMapVisualizer.cs | 113 +++++- .../Unity/UnityTileTerrainContainer.cs | 5 + .../CustomTerrainSource.cs | 7 + Runtime/Mapbox/Example/MapboxExamples.asmdef | 7 - .../Mapbox/Example/Scripts/Moving3dCamera.cs | 9 +- .../Mapbox/Example/Scripts/SlippyMapCamera.cs | 10 +- .../Example/Scripts/TileProviderBenchmark.cs | 366 ++++++++++++++++++ .../Scripts/TileProviderBenchmark.cs.meta | 11 + .../Editor/SimplificationFactorDrawer.cs | 18 +- .../Editor/TerrainSettingsInspectorHelper.cs | 16 +- .../AsyncExtractElevationArray.cs | 17 + .../DataSources/TerrainSource.cs | 13 +- .../TileProvider/UnityTileProvider.cs | 30 +- 17 files changed, 616 insertions(+), 66 deletions(-) create mode 100644 Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs create mode 100644 Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 643000843..48d39e6e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Minor-version bump per semver: this release contains source-breaking API changes - `CustomTMSTile` constructor now takes two additional parameters (`invertY`, `isMapboxService`). The legacy 4-arg constructor (`urlFormat`, `tileId`, `tilesetId`, `useNonReadableTexture`) is preserved as an overload defaulting to `invertY: true, isMapboxService: false` — external subclasses keep compiling. New code should use the 6-arg form. - `CustomSource.CreateTile` now always returns a `CustomTMSTile` when `UrlFormat` is set, regardless of `InvertY`. Previously `InvertY=false` returned a plain `RasterTile`. If you relied on the plain-raster path with a non-empty `UrlFormat`, set `UrlFormat = ""` to keep that behavior — empty `UrlFormat` falls back to plain `RasterTile`. - `MapboxTileData.SetDisposeCallback` was replaced with `AddDisposeCallback` / `RemoveDisposeCallback` (multicast). This is an `internal` API; only relevant if you have access to it via reflection or an internal-visible-to dependency. +- `CustomTerrainLayerModuleScript` now passes the user-configured `Settings.DataSettings` (cache size, retina flag, non-readable textures, data-zoom clamp) through to `CustomTerrainSource`. Previously a throwaway `ImageSourceSettings` was used and the Inspector values had no runtime effect. +- `TileProviderBenchmark`: the Unity Recorder video-capture path has been removed. The benchmark still collects per-frame tile statistics and writes them to JSON; if you need video capture, install `com.unity.recorder` separately and wire it up in your own scene script. #### Changes - Added new documentation. diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs index f06974bdf..1a80ca5c8 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs @@ -23,6 +23,20 @@ public static class ElevationArrayPool private static readonly Dictionary> _pools = new Dictionary>(); + // Editor play-mode stop leaves static state untouched, so the pool would keep + // the previous session's buffers referenced. Reset on subsystem registration + // (fires before scene load on play-mode entry). +#if UNITY_EDITOR + [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetForPlayMode() + { + lock (_pools) + { + _pools.Clear(); + } + } +#endif + /// /// Returns a float[] of exactly entries, either from /// the pool or freshly allocated. Contents are undefined; the caller is expected to diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs index 113a083b9..0430cb702 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs @@ -10,6 +10,11 @@ public class TerrainData : RasterData [HideInInspector] public float[] ElevationValues; public bool IsElevationDataReady = false; + // Flipped to true by Dispose(). Async readback paths can complete after the + // owning TerrainData has been evicted; they check this flag before assigning + // and return the rented buffer to the pool instead of leaking it. + public bool IsDisposed { get; private set; } + /// /// Fires whenever or /// completes. Multiple @@ -51,19 +56,26 @@ public override void Dispose() ElevationValues = null; } IsElevationDataReady = false; + IsDisposed = true; base.Dispose(); } + // Cached side length of the square heightmap (sqrt(Length)). Computed once + // on Set to avoid per-call Mathf.Sqrt in QueryHeightData / ReadElevation. + private int _cachedWidth; + public void SetElevationValues(float[] elevationArray) { ElevationValues = elevationArray; + _cachedWidth = elevationArray != null ? (int)Mathf.Sqrt(elevationArray.Length) : 0; IsElevationDataReady = true; ElevationValuesUpdated?.Invoke(); } - + public void SetElevationValues(float[] elevationArray, float min, float max) { ElevationValues = elevationArray; + _cachedWidth = elevationArray != null ? (int)Mathf.Sqrt(elevationArray.Length) : 0; IsElevationDataReady = true; MinElevation = min; MaxElevation = max; @@ -92,7 +104,7 @@ public float QueryHeightData(float x, float y) private float ReadElevation(float x, float y, Vector4 terrainTextureScaleOffset) { - var width = (int) Mathf.Sqrt(ElevationValues.Length); + var width = _cachedWidth; var sectionWidth = width * terrainTextureScaleOffset.x - 1f; var xx = terrainTextureScaleOffset.z * width + Mathf.Clamp01(x) * sectionWidth; var yy = terrainTextureScaleOffset.w * width + Mathf.Clamp01(y) * sectionWidth; diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index 66d654bd7..f455924e4 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -90,8 +90,10 @@ public class ElevatedTerrainStrategy : TerrainStrategy, IElevationBasedTerrainSt // Tracks deferred-rebuild closures so OnDestroy can unsubscribe them. Without this // list, if a TerrainData is disposed before ElevationValuesUpdated fires, the // closure stays attached to the event and roots both tile + data via capture. - private readonly List<(TerrainData data, Action rebuild)> _pendingRebuilds = - new List<(TerrainData, Action)>(); + // Keyed by (data, tile) so RegisterCollider's temp→final re-invocation can + // de-dup against an already-pending entry. + private readonly List<(TerrainData data, UnityMapTile tile, Action rebuild)> _pendingRebuilds = + new List<(TerrainData, UnityMapTile, Action)>(); public override int RequiredVertexCount { @@ -297,13 +299,25 @@ private void RegisterCollider(UnityMapTile tile) } else { + // De-dup: temp→final transitions re-call RegisterTile with the same + // (tile, data). Without this check we'd add a second closure + a second + // async bake, doubling work for every transition. + for (int i = 0; i < _pendingRebuilds.Count; i++) + { + if (ReferenceEquals(_pendingRebuilds[i].data, data) && + ReferenceEquals(_pendingRebuilds[i].tile, tile)) + { + return; + } + } + // Defer until values arrive. The callback self-unsubscribes on fire and // no-ops if the tile has since been recycled onto different data, so a late // async readback does not stomp a freshly-reassigned tile. We also track // the subscription in _pendingRebuilds so OnDestroy can detach pending // closures whose data is disposed before the event fires. Action rebuild = null; - (TerrainData data, Action rebuild) entry = (data, null); + (TerrainData data, UnityMapTile tile, Action rebuild) entry = (data, tile, null); rebuild = () => { data.ElevationValuesUpdated -= rebuild; @@ -329,8 +343,10 @@ private void RegisterCollider(UnityMapTile tile) // Name of the dedicated child GameObject that holds the MeshCollider when // useDedicatedColliderLayer is enabled. Keyed by name so we can locate + reuse it - // across pool cycles without maintaining a per-tile dictionary. - private const string ColliderChildName = "TerrainCollider"; + // across pool cycles without maintaining a per-tile dictionary. Distinct from + // TerrainColliderMeshName so the GO name and the Mesh name don't collide + // (they were both "TerrainCollider" — same string used for two different purposes). + private const string ColliderChildName = "TerrainColliderChild"; // Removes _lastColliderBuild entries whose MeshCollider key has been destroyed // (GameObject teardown, scene unload). Unity's overloaded == returns true for @@ -643,6 +659,8 @@ public void Execute() /// worker thread. The cooking options must match what the MeshCollider is /// configured with, otherwise PhysX discards the cached data and re-cooks on /// assignment. + /// Intentionally NOT [BurstCompile]: the entire body is a single P/Invoke into + /// PhysX, so Burst-compiling adds AOT cost with no measurable benefit. /// private struct BakeColliderJob : IJob { diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 41b727c16..29ae423c5 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Data.Tiles; using Mapbox.BaseModule.Unity; @@ -157,6 +158,11 @@ public virtual void Load(TileCover tileCover) foreach (var child in tempTile.Children) { _toRemove.Remove(child.UnwrappedTileId); + // Also invalidate any in-flight pending-pool entry that + // snapshotted this child's prior generation. Without this, the + // _toRemove rescue is silently bypassed by the next FlushPendingPool + // tick (child queued earlier, before becoming a filler). + child.Generation++; } } } @@ -230,6 +236,15 @@ public virtual void Load(TileCover tileCover) /// public virtual void InternalUpdateCoroutine() { + // Drain the terrain-bounds dirty flag once per tick. Recompute is O(ActiveTiles) + // so doing it here (and not on every PoolTile / CreateTile / async-decode event) + // turns what was O(N²) for a Load into a single O(N) pass per frame. + if (_terrainBoundsDirty) + { + RecomputeTerrainBounds(); + _terrainBoundsDirty = false; + } + // Flush tiles deferred from the previous tick. Holding the children active for // one extra frame after the parent's ShowTile lets the parent finish rendering // before the children deactivate, which avoids a one-frame transparent gap on @@ -328,6 +343,22 @@ public void OnDestroy() _internalUpdateCoroutine = null; } + // Detach any pending async-decode subscriptions so the closures don't keep + // shared TerrainData rooted past the visualizer's lifetime. + for (int i = 0; i < _pendingElevationWatches.Count; i++) + { + var (data, handler) = _pendingElevationWatches[i]; + if (data != null && handler != null) + { + data.ElevationValuesUpdated -= handler; + } + } + _pendingElevationWatches.Clear(); + + // Flush any deferred pool entries so each pending tile gets its Recycle() + // (returns elevation arrays to the pool, restores shared meshes, etc). + FlushPendingPool(); + foreach (var layerModule in LayerModules) { layerModule.OnDestroy(); @@ -497,6 +528,13 @@ protected void ShowTile(UnityMapTile unityTile) _mapInformation.PositionObjectFor(unityTile.gameObject, unityTile.CanonicalTileId); } + // Set whenever an event that could shift Terrain.Min/Max occurs (tile pooled + // while holding a boundary value, async decode arriving on an already-finished + // tile, etc). InternalUpdateCoroutine drains the flag once per tick by running + // RecomputeTerrainBounds — avoids the O(N²) cost of recursive PoolTile each + // calling Recompute itself. + private bool _terrainBoundsDirty; + protected void PoolTile(UnityMapTile tile) { if (tile.LoadingState == LoadingState.None) @@ -509,11 +547,17 @@ protected void PoolTile(UnityMapTile tile) // Snapshot whether this tile is currently holding either bounds value. // Recycle() clears TerrainData below, so we have to check first. + // Skip the trigger when both endpoints are 0 — that's a flat tile that didn't + // actually contribute to the global bounds (would force unnecessary recomputes + // every time a sea-level tile is pooled). var terrain = _mapInformation.Terrain; var terrainData = tile.TerrainContainer?.TerrainData; - bool contributedBound = terrainData != null && terrainData.IsElevationDataReady && - (Mathf.Approximately(terrainData.MaxElevation, terrain.MaxElevation) || - Mathf.Approximately(terrainData.MinElevation, terrain.MinElevation)); + bool contributedMax = terrainData != null && terrainData.IsElevationDataReady && + terrainData.MaxElevation != 0f && + Mathf.Approximately(terrainData.MaxElevation, terrain.MaxElevation); + bool contributedMin = terrainData != null && terrainData.IsElevationDataReady && + terrainData.MinElevation != 0f && + Mathf.Approximately(terrainData.MinElevation, terrain.MinElevation); TileUnloading(tile); ActiveTiles.Remove(tile.UnwrappedTileId); @@ -531,10 +575,43 @@ protected void PoolTile(UnityMapTile tile) tile.Children.Clear(); } - if (contributedBound) + if (contributedMax || contributedMin) { - RecomputeTerrainBounds(); + _terrainBoundsDirty = true; + } + } + + // Tracks the one-shot ElevationValuesUpdated subscriptions we've attached for + // shader-mode tiles whose CPU decode hasn't arrived yet. Stored by (data, + // handler) so OnDestroy can detach if the data is disposed before the event fires. + private readonly List<(TerrainData data, Action handler)> _pendingElevationWatches = + new List<(TerrainData, Action)>(); + + private void WatchForAsyncElevationDecode(TerrainData data) + { + // De-dup: shared TerrainData is referenced by up to 16 render tiles. One + // watch per data is enough — the dirty flag triggers a single recompute. + for (int i = 0; i < _pendingElevationWatches.Count; i++) + { + if (ReferenceEquals(_pendingElevationWatches[i].data, data)) return; } + Action handler = null; + handler = () => + { + data.ElevationValuesUpdated -= handler; + for (int i = _pendingElevationWatches.Count - 1; i >= 0; i--) + { + if (ReferenceEquals(_pendingElevationWatches[i].data, data) && + ReferenceEquals(_pendingElevationWatches[i].handler, handler)) + { + _pendingElevationWatches.RemoveAt(i); + break; + } + } + _terrainBoundsDirty = true; + }; + _pendingElevationWatches.Add((data, handler)); + data.ElevationValuesUpdated += handler; } private void RecomputeTerrainBounds() @@ -591,6 +668,11 @@ protected bool CreateTileInstant(UnwrappedTileId tileId, out UnityMapTile tile) { tile.Recycle(); tile.LoadingState = LoadingState.None; + // Mirror PoolTile's bump so any pending-pool entry that snapshotted + // this tile's pre-failure generation is invalidated when the next + // flush runs. The failure path is otherwise a silent bypass of the + // re-borrow protection enforced everywhere else. + tile.Generation++; _tileCreator.PutTile(tile); } @@ -627,13 +709,22 @@ protected bool CreateTile(UnityMapTile unityMapTile) } var terrainData = unityMapTile.TerrainContainer?.TerrainData; - if (terrainData != null && terrainData.IsElevationDataReady) + if (terrainData != null) { - // Recompute from ActiveTiles rather than widening against the existing - // Min/Max: the prior accumulate-only path left MinElevation pinned at - // its initial 0 whenever every loaded tile was at or above sea level, - // since (positive < 0) never fires. - RecomputeTerrainBounds(); + if (terrainData.IsElevationDataReady) + { + // Data already decoded — mark bounds dirty for the next coroutine + // tick. Deferring avoids the O(N²) cost of recomputing inside + // every CreateTile within a single Load. + _terrainBoundsDirty = true; + } + else + { + // Shader-mode tiles are "finished" once the texture is ready, but + // the CPU decode (and therefore Min/MaxElevation) arrives async + // later. Hook a one-shot widening so the bounds catch up. + WatchForAsyncElevationDecode(terrainData); + } } TileLoaded(unityMapTile); diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs index af1acf950..98015b9f2 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs @@ -204,6 +204,11 @@ public void OnDestroy() if (TerrainData != null) { TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; + // Symmetric detach: previously only the ElevationValuesUpdated handler was + // removed, but the multicast dispose callback stayed wired. On a later + // eviction of shared TerrainData, that delegate would fire into a destroyed + // tile — exactly the class of bug the multicast change was meant to fix. + TerrainData.RemoveDisposeCallback(_onDisposeCallback); TerrainData = null; } } diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs index 10f066548..85d9b4315 100644 --- a/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs @@ -20,6 +20,13 @@ public CustomTerrainSource(CustomSourceSettings customSettings, DataFetchingMana protected override RasterTile CreateTile(CanonicalTileId tileId, string tilesetId) { + // Empty UrlFormat: fall back to plain RasterTile so the base path's URL + // construction is used. Without this, CustomTMSTile.Initialize would call + // string.Format(null, …) and throw. Mirrors the same fallback in CustomSource. + if (string.IsNullOrEmpty(_customSourceSettings.UrlFormat)) + { + return new RasterTile(tileId, tilesetId, true); + } return new CustomTMSTile( _customSourceSettings.UrlFormat, tileId, tilesetId, true, diff --git a/Runtime/Mapbox/Example/MapboxExamples.asmdef b/Runtime/Mapbox/Example/MapboxExamples.asmdef index 4046c19c8..eee04e54d 100644 --- a/Runtime/Mapbox/Example/MapboxExamples.asmdef +++ b/Runtime/Mapbox/Example/MapboxExamples.asmdef @@ -9,8 +9,6 @@ "GUID:1b64b068a01f943108c7f4b7a5356c7a", "GUID:6055be8ebefd69e48b49212b09b47b2f", "GUID:8ba268152fd3143f59445711358cb12f", - "Unity.Recorder", - "Unity.Recorder.Editor", "Unity.InputSystem" ], "includePlatforms": [], @@ -21,11 +19,6 @@ "autoReferenced": true, "defineConstraints": [], "versionDefines": [ - { - "name": "com.unity.recorder", - "expression": "", - "define": "UNITY_RECORDER" - }, { "name": "com.unity.inputsystem", "expression": "", diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs index ce4859862..1bab14671 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs @@ -73,8 +73,13 @@ public override CameraOutput UpdateCamera(IMapInformation mapInformation) if (!GetPlaneIntersection(pointerPos, out var newPoint)) return _output; Vector3 pos = newPoint - _dragOrigin; - _targetPosition -= new Vector3(pos.x, 0, pos.z); - _output.HasChanged = true; + // Skip the redraw when the held pointer didn't actually move (touch Stationary, + // mouse-held-still). HasChanged=true here would trigger a full ChangeView+Load. + if (pos.x != 0f || pos.z != 0f) + { + _targetPosition -= new Vector3(pos.x, 0, pos.z); + _output.HasChanged = true; + } } else if (GetSecondaryHeld()) { diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs index 963ed0e3f..143c29ca1 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs @@ -125,8 +125,14 @@ public override CameraOutput UpdateCamera(IMapInformation mapInformation) { var oldCursorLatLng = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(_dragOrigin))); var newCursorLatLng = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(cursorHit))); - newMercatorCenter = mapInformation.CenterMercator - (newCursorLatLng - oldCursorLatLng); - _output.HasChanged = true; + var mercatorDelta = newCursorLatLng - oldCursorLatLng; + // Skip the redraw when the cursor hit didn't actually move (Stationary + // touch, mouse-held-still). Avoids per-frame ChangeView round-trips. + if (mercatorDelta.x != 0 || mercatorDelta.y != 0) + { + newMercatorCenter = mapInformation.CenterMercator - mercatorDelta; + _output.HasChanged = true; + } } else if (GetSecondaryHeld() && _rotationSettings.Enabled) { diff --git a/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs b/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs new file mode 100644 index 000000000..0eec87199 --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.Example.Scripts.Map; +using UnityEngine; + +namespace Mapbox.Example.Scripts +{ + /// + /// Runtime benchmark for tile providers. Animates the camera through a scripted path + /// (pan between two lat/lng points with rotation) and records the tile cover output + /// every frame. Results are written to a JSON file for comparison across algorithm changes. + /// + /// Usage: + /// 1. Add this component to the same GameObject as MapboxMapBehaviour + /// 2. Set StartLatLng, EndLatLng, zoom, pitch, bearing parameters + /// 3. Enter Play mode — the camera will animate automatically + /// 4. On-screen overlay shows live tile count and zoom distribution + /// 5. When the run finishes (or you press the StopKey), results are saved to Application.persistentDataPath + /// + public class TileProviderBenchmark : MonoBehaviour + { + [Header("Camera Path")] + public double StartLat = 40.7128; + public double StartLng = -74.0060; + public double EndLat = 40.7580; + public double EndLng = -73.9855; + + [Header("Camera Settings")] + public float Zoom = 16f; + public float StartPitch = 45f; + public float EndPitch = 45f; + public float StartBearing = 0f; + public float EndBearing = 90f; + + [Header("Animation")] + public float DurationSeconds = 10f; + public bool PingPong = true; + public int PingPongCycles = 2; + public bool AutoStart = true; + + [Header("Controls")] + public KeyCode StartKey = KeyCode.F5; + public KeyCode StopKey = KeyCode.F6; + + [Header("Output")] + public string OutputFileName = "tile_benchmark"; + + private MapBehaviourCore _mapBehaviour; + private MapboxMap _map; + private bool _isMapReady; + private bool _isRunning; + private float _elapsed; + private int _totalFrames; + private int _currentCycle; + + // Per-frame recording + private List _records = new List(2000); + + // Live stats for on-screen display + private int _lastTileCount; + private int[] _lastZoomDistribution = new int[23]; // z0-z22 + private int _tileChangesThisRun; + private HashSet _previousFrameTiles = new HashSet(); + + private void Awake() + { + _mapBehaviour = GetComponent(); + if (_mapBehaviour == null) + { + Debug.LogError("[TileProviderBenchmark] No MapBehaviourCore found on this GameObject."); + enabled = false; + return; + } + + // Set start position before initialization so the map loads at the right place + _mapBehaviour.MapInformation.SetInformation( + new LatitudeLongitude(StartLat, StartLng), Zoom, StartPitch, StartBearing); + + _mapBehaviour.Initialized += map => + { + _map = map; + _map.LoadViewCompleted += () => + { + _isMapReady = true; + if (AutoStart) StartRun(); + }; + }; + + // Start initialization ourselves (InitializeOnStart should be unchecked on MapBehaviourCore) + StartCoroutine(_mapBehaviour.Initialize()); + } + + private void Update() + { + if (Input.GetKeyDown(StartKey) && !_isRunning && _isMapReady) + StartRun(); + if (Input.GetKeyDown(StopKey) && _isRunning) + StopRun(); + + if (!_isRunning || !_isMapReady) return; + + _elapsed += Time.deltaTime; + + // Compute normalized time with ping-pong + float totalDuration = DurationSeconds * (PingPong ? PingPongCycles * 2 : 1); + if (_elapsed >= totalDuration) + { + StopRun(); + return; + } + + float cycleTime = _elapsed % (PingPong ? DurationSeconds * 2 : DurationSeconds); + float t; + if (PingPong && cycleTime > DurationSeconds) + t = 1f - (cycleTime - DurationSeconds) / DurationSeconds; + else + t = cycleTime / DurationSeconds; + + t = Mathf.SmoothStep(0f, 1f, t); + + // Interpolate camera parameters + var lat = Lerp(StartLat, EndLat, t); + var lng = Lerp(StartLng, EndLng, t); + var pitch = Mathf.Lerp(StartPitch, EndPitch, t); + var bearing = Mathf.Lerp(StartBearing, EndBearing, t); + + var latlng = new LatitudeLongitude(lat, lng); + _map.ChangeView(latlng, Zoom, pitch, bearing); + + // Record this frame + RecordFrame(t, lat, lng, pitch, bearing); + _totalFrames++; + } + + public void StartRun() + { + _isRunning = true; + _elapsed = 0f; + _totalFrames = 0; + _currentCycle = 0; + _tileChangesThisRun = 0; + _records.Clear(); + _previousFrameTiles.Clear(); + Debug.Log($"[TileProviderBenchmark] Run started. Duration: {DurationSeconds}s, PingPong: {PingPong}"); + } + + public void StopRun() + { + _isRunning = false; + SaveResults(); + Debug.Log($"[TileProviderBenchmark] Run complete. {_totalFrames} frames recorded, {_tileChangesThisRun} tile changes. Results saved."); + } + + private void RecordFrame(float t, double lat, double lng, float pitch, float bearing) + { + var tiles = _map.TileCover.Tiles; + _lastTileCount = tiles.Count; + + // Zoom distribution + Array.Clear(_lastZoomDistribution, 0, _lastZoomDistribution.Length); + foreach (var tile in tiles) + { + if (tile.Z >= 0 && tile.Z < _lastZoomDistribution.Length) + _lastZoomDistribution[tile.Z]++; + } + + // Count tile changes from previous frame + int added = 0; + int removed = 0; + foreach (var tile in tiles) + { + if (!_previousFrameTiles.Contains(tile)) + added++; + } + foreach (var tile in _previousFrameTiles) + { + if (!tiles.Contains(tile)) + removed++; + } + _tileChangesThisRun += added + removed; + + // Build zoom distribution string + var zoomDist = new int[23]; + Array.Copy(_lastZoomDistribution, zoomDist, 23); + + _records.Add(new FrameRecord + { + Frame = _totalFrames, + Time = _elapsed, + T = t, + Lat = lat, + Lng = lng, + Pitch = pitch, + Bearing = bearing, + TileCount = _lastTileCount, + TilesAdded = added, + TilesRemoved = removed, + ZoomDistribution = zoomDist, + CameraPosition = Camera.main != null ? Camera.main.transform.position : Vector3.zero, + }); + + // Update previous frame set + _previousFrameTiles.Clear(); + foreach (var tile in tiles) + _previousFrameTiles.Add(tile); + } + + private void SaveResults() + { + var sb = new StringBuilder(); + sb.AppendLine("{"); + sb.AppendLine($" \"provider\": \"{_map.MapService.GetType().Name}\","); + sb.AppendLine($" \"startLatLng\": [{StartLat}, {StartLng}],"); + sb.AppendLine($" \"endLatLng\": [{EndLat}, {EndLng}],"); + sb.AppendLine($" \"zoom\": {Zoom},"); + sb.AppendLine($" \"startPitch\": {StartPitch}, \"endPitch\": {EndPitch},"); + sb.AppendLine($" \"startBearing\": {StartBearing}, \"endBearing\": {EndBearing},"); + sb.AppendLine($" \"duration\": {DurationSeconds},"); + sb.AppendLine($" \"pingPong\": {PingPong.ToString().ToLower()},"); + sb.AppendLine($" \"totalFrames\": {_totalFrames},"); + sb.AppendLine($" \"totalTileChanges\": {_tileChangesThisRun},"); + + // Summary stats + int maxTiles = 0, minTiles = int.MaxValue; + long sumTiles = 0; + int maxChanges = 0; + long sumChanges = 0; + foreach (var r in _records) + { + if (r.TileCount > maxTiles) maxTiles = r.TileCount; + if (r.TileCount < minTiles) minTiles = r.TileCount; + sumTiles += r.TileCount; + var changes = r.TilesAdded + r.TilesRemoved; + if (changes > maxChanges) maxChanges = changes; + sumChanges += changes; + } + sb.AppendLine($" \"summary\": {{"); + sb.AppendLine($" \"minTileCount\": {minTiles},"); + sb.AppendLine($" \"maxTileCount\": {maxTiles},"); + sb.AppendLine($" \"avgTileCount\": {(_records.Count > 0 ? sumTiles / (float)_records.Count : 0):F1},"); + sb.AppendLine($" \"maxFrameChanges\": {maxChanges},"); + sb.AppendLine($" \"avgFrameChanges\": {(_records.Count > 0 ? sumChanges / (float)_records.Count : 0):F2}"); + sb.AppendLine($" }},"); + + // Per-frame data + sb.AppendLine(" \"frames\": ["); + for (int i = 0; i < _records.Count; i++) + { + var r = _records[i]; + var zd = FormatZoomDist(r.ZoomDistribution); + sb.Append($" {{\"f\":{r.Frame},\"t\":{r.T:F4},\"lat\":{r.Lat:F6},\"lng\":{r.Lng:F6}," + + $"\"pitch\":{r.Pitch:F1},\"bearing\":{r.Bearing:F1}," + + $"\"tiles\":{r.TileCount},\"added\":{r.TilesAdded},\"removed\":{r.TilesRemoved}," + + $"\"camY\":{r.CameraPosition.y:F2}," + + $"\"zoom_dist\":{zd}}}"); + sb.AppendLine(i < _records.Count - 1 ? "," : ""); + } + sb.AppendLine(" ]"); + sb.AppendLine("}"); + + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + var path = Path.Combine(Application.persistentDataPath, $"{OutputFileName}_{timestamp}.json"); + File.WriteAllText(path, sb.ToString()); + Debug.Log($"[TileProviderBenchmark] Results saved to: {path}"); + } + + private string FormatZoomDist(int[] dist) + { + var sb = new StringBuilder("{"); + bool first = true; + for (int z = 0; z < dist.Length; z++) + { + if (dist[z] > 0) + { + if (!first) sb.Append(","); + sb.Append($"\"{z}\":{dist[z]}"); + first = false; + } + } + sb.Append("}"); + return sb.ToString(); + } + + private void OnGUI() + { + if (!_isRunning && _records.Count == 0) return; + + var style = new GUIStyle(GUI.skin.label) + { + fontSize = 14, + fontStyle = FontStyle.Bold, + }; + style.normal.textColor = Color.white; + + float x = 10, y = 10; + float lineHeight = 20; + + // Measure content height first + int zoomLines = 0; + for (int z = 0; z < _lastZoomDistribution.Length; z++) + { + if (_lastZoomDistribution[z] > 0) + zoomLines++; + } + // header + stats + "Zoom distribution:" + zoom bars + optional hint line + padding + float contentHeight = lineHeight * (3 + zoomLines) + 20; + if (!_isRunning && _records.Count > 0) + contentHeight += lineHeight + 4; + + // Background + GUI.DrawTexture(new Rect(5, 5, 320, contentHeight), Texture2D.grayTexture); + + GUI.Label(new Rect(x, y, 300, lineHeight), + _isRunning ? "TILE BENCHMARK RUNNING" : "BENCHMARK COMPLETE", style); + y += lineHeight; + + GUI.Label(new Rect(x, y, 300, lineHeight), + $"Frame: {_totalFrames} Tiles: {_lastTileCount} Changes: {_tileChangesThisRun}", style); + y += lineHeight; + + // Zoom distribution bars (highest zoom first) + GUI.Label(new Rect(x, y, 300, lineHeight), "Zoom distribution:", style); + y += lineHeight; + for (int z = _lastZoomDistribution.Length - 1; z >= 0; z--) + { + if (_lastZoomDistribution[z] > 0) + { + var barWidth = Mathf.Min(_lastZoomDistribution[z] * 4, 200); + GUI.DrawTexture(new Rect(x + 30, y + 3, barWidth, 14), Texture2D.whiteTexture); + GUI.Label(new Rect(x, y, 30, lineHeight), $"z{z}", style); + GUI.Label(new Rect(x + 35 + barWidth, y, 50, lineHeight), $"{_lastZoomDistribution[z]}", style); + y += lineHeight; + } + } + + if (!_isRunning && _records.Count > 0) + { + y += 4; + GUI.Label(new Rect(x, y, 300, lineHeight), + $"Press {StartKey} to re-run, {StopKey} during run to stop early", style); + } + } + + private static double Lerp(double a, double b, float t) => a + (b - a) * t; + + [Serializable] + private struct FrameRecord + { + public int Frame; + public float Time; + public float T; + public double Lat, Lng; + public float Pitch, Bearing; + public int TileCount; + public int TilesAdded, TilesRemoved; + public int[] ZoomDistribution; + public Vector3 CameraPosition; + } + } +} diff --git a/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta b/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta new file mode 100644 index 000000000..5290c5114 --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba1a10f5fb2ec4f299d716318a436736 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs index 18f650c0a..3bc8e9d50 100644 --- a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs @@ -58,7 +58,9 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten // (which Unity then records as a deliberate, undoable change). var fieldRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); EditorGUI.BeginChangeCheck(); + EditorGUI.showMixedValue = property.hasMultipleDifferentValues; var newValue = EditorGUI.IntPopup(fieldRect, label, current, labels, factors); + EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { property.intValue = newValue; @@ -119,22 +121,6 @@ private static (string message, MessageType severity) BuildMessage(int factor, i return (baseLine + " Very coarse — terrain will look blocky and neighboring tiles may visibly mismatch at seams.", MessageType.Warning); } - private static int NearestPreset(int[] presets, int value) - { - var best = presets[0]; - var bestDiff = Mathf.Abs(best - value); - for (int i = 1; i < presets.Length; i++) - { - var diff = Mathf.Abs(presets[i] - value); - if (diff < bestDiff) - { - bestDiff = diff; - best = presets[i]; - } - } - return best; - } - private static float CalcHelpBoxHeight(string message, float width) { var content = new GUIContent(message); diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs index 8f9c540ed..e78030159 100644 --- a/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs @@ -47,16 +47,16 @@ public static void Draw(SerializedProperty settingsProp) { if (child.name == ExtractFieldName && forcedOn) { - // Persist the forced state into the serialized value so runtime code that - // reads ExtractCpuElevationData sees the effective configuration (and so - // TerrainLayerModule.Initialize doesn't log an override warning). - if (!child.boolValue) - { - child.boolValue = true; - } + // Display-only override. We do NOT write child.boolValue = true here: + // the runtime evaluates `TerrainLayerModuleSettings.NeedsCpuElevation` + // (an OR over the forcing flags) directly, so the persisted value + // stays as the user set it. Writing during OnGUI would silently dirty + // every asset/scene on inspection. using (new EditorGUI.DisabledScope(true)) { - EditorGUILayout.PropertyField(child); + EditorGUI.showMixedValue = false; + EditorGUILayout.Toggle(new GUIContent(child.displayName, child.tooltip), true); + EditorGUI.showMixedValue = child.hasMultipleDifferentValues; } EditorGUILayout.HelpBox( $"Extraction is force-enabled by {reason}. CPU elevation will be decoded regardless of this checkbox.", diff --git a/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs b/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs index 1fff29f0b..9a461ec23 100644 --- a/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs +++ b/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs @@ -41,10 +41,27 @@ public void ExtractHeightData(Texture2D texture, TerrainData terrainData) { AsyncGPUReadback.Request(texture, 0, (t) => { + // AsyncGPUReadback fires on the main thread, but possibly after the + // owning TerrainData was disposed (cache eviction during fetch). + // Guard before renting from the pool: assigning to disposed data would + // leak the rented buffer and could corrupt a future Rent if the same + // TerrainData later gets re-used. + if (terrainData == null || terrainData.IsDisposed) + { + return; + } var width = t.width; var data = t.GetData(); var heightData = ElevationArrayPool.Rent(width * width); RunDecodeJob(data, width, heightData, out var min, out var max); + // Re-check after the (potentially long) decode: dispose may have arrived + // in the interim. Return the buffer rather than assigning into the dead + // data object. + if (terrainData.IsDisposed) + { + ElevationArrayPool.Return(heightData); + return; + } terrainData.SetElevationValues(heightData, min, max); }); } diff --git a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs index 7c544fe28..978963dc6 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs @@ -132,13 +132,16 @@ public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileI protected IEnumerator ExtractElevationValues(TerrainData data) { + // Use the TerrainData overload (sets MinElevation/MaxElevation as part of the + // decode). Previously this path used Action + the 1-arg SetElevationValues, + // which left Min/Max at 0 — meaning RecomputeTerrainBounds never widened the + // shared TerrainInfo for any tile loaded through LoadTileCoroutine. var finished = false; - _elevationDataExtractionStrategy.ExtractHeightData(data.Texture, (elevationArray) => - { - data.SetElevationValues(elevationArray); - finished = true; - }); + Action onDone = () => finished = true; + data.ElevationValuesUpdated += onDone; + _elevationDataExtractionStrategy.ExtractHeightData(data.Texture, data); while (!finished) yield return null; + data.ElevationValuesUpdated -= onDone; } protected override void TextureReceivedFromFile(TerrainData cacheItem) diff --git a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs index 095a14897..07861a554 100644 --- a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs +++ b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs @@ -131,10 +131,15 @@ public override bool GetTileCover(IMapInformation mapInformation, TileCover tile var worldCenter = mapInformation.CenterMercator; var scale = mapInformation.Scale; - // Bounds height for frustum culling: derived from observed terrain elevation. - // MaxElevation is in meters, divide by scale to get world units. - // Floor of 0.1 ensures tiles are never zero-height (invisible to frustum test). - var boundsHeight = Mathf.Max(mapInformation.Terrain.MaxElevation / scale, 0.1f); + // Bounds extent for frustum culling: derived from observed terrain elevation. + // Min/Max are in meters; divide by scale to get world units. Negative Min + // (Death Valley, Dead Sea) shifts the AABB bottom below Y=0; clamping the + // bottom at 0 made those tiles fall outside the AABB and frustum-cull. + // Floor on the total height ensures tiles are never zero-height (invisible). + var maxWorld = mapInformation.Terrain.MaxElevation / scale; + var minWorld = mapInformation.Terrain.MinElevation / scale; + var boundsBottom = Mathf.Min(0f, minWorld); + var boundsHeight = Mathf.Max(maxWorld - boundsBottom, 0.1f); // Camera height above the map plane (y=0) var cameraHeight = camPos.y; @@ -149,7 +154,7 @@ public override bool GetTileCover(IMapInformation mapInformation, TileCover tile // push root tile var rootIdx = AllocNode(); - _pool[rootIdx].Set(new UnwrappedTileId(0, 0, 0), worldCenter, scale, boundsHeight); + _pool[rootIdx].Set(new UnwrappedTileId(0, 0, 0), worldCenter, scale, boundsBottom, boundsHeight); _stack.Push(rootIdx); while (_stack.Count > 0) @@ -179,7 +184,7 @@ public override bool GetTileCover(IMapInformation mapInformation, TileCover tile var childIdx = AllocNode(); _pool[childIdx].Set( new UnwrappedTileId(zoom + 1, childX, childY), - worldCenter, scale, boundsHeight); + worldCenter, scale, boundsBottom, boundsHeight); _stack.Push(childIdx); } } @@ -241,6 +246,11 @@ private static float DistToSplitScale(float dz, float d) const float kAcuteAngleThresholdSin = 0.707f; // sin(45 degrees) const float kStretchTile = 1.1f; + // Floor dz at a tiny epsilon. Without this, a camera exactly at ground level + // (dz=0) makes r = d/0 → Inf/NaN, the resulting scale fails comparisons, + // and the tile pins to its coarsest LOD forever. + if (dz < 0.0001f) dz = 0.0001f; + if (d * kAcuteAngleThresholdSin < dz) return 1f; @@ -255,14 +265,18 @@ protected struct TileNode public UnwrappedTileId Id; public Bounds Bounds; - public void Set(UnwrappedTileId id, Vector2d worldCenter, float scale, float boundsHeight) + public void Set(UnwrappedTileId id, Vector2d worldCenter, float scale, float boundsBottom, float boundsHeight) { Id = id; var rect = Conversions.TileBoundsInUnitySpace(id, worldCenter, scale); var sizeX = (float)rect.Size.x; + // AABB extends from Y=boundsBottom up to boundsBottom+boundsHeight, so the + // center sits halfway between. Lets Death-Valley-like tiles whose surface + // is below Y=0 still pass the frustum test. + var centerY = boundsBottom + boundsHeight * 0.5f; Bounds = new Bounds( - new Vector3((float)rect.Center.x, boundsHeight * 0.5f, (float)rect.Center.y), + new Vector3((float)rect.Center.x, centerY, (float)rect.Center.y), new Vector3(sizeX, boundsHeight, Mathf.Abs(sizeX))); } } From b2aab6639002fefff27cf0fbf3ddd0100d4f5383 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 13:50:33 +0300 Subject: [PATCH 24/54] Input-system soft dep, dispose-multicast leaks, NaN/inf guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical - MapboxExamples.asmdef: drop the hard "Unity.InputSystem" reference. versionDefines set the define conditionally but the reference itself was unconditional, so projects without com.unity.inputsystem failed to compile the examples assembly. Unity.InputSystem's asmdef is autoReferenced so the using-directive inside #if MAPBOX_NEW_INPUT_SYSTEM resolves when the package is present and disappears cleanly when absent. - TerrainSource.ExtractElevationValues: also subscribe to the dispose multicast. The async GPU readback no-ops when TerrainData.IsDisposed flips and never raises ElevationValuesUpdated; without the dispose hook the coroutine yield-forever-pinned the dead data. Triggers on rapid pan during initial LoadTileCoroutine when eviction beats readback. - UnityTileImageContainer.OnDestroy: was empty. RemoveDisposeCallback on shutdown to mirror the terrain-container fix; otherwise a destroyed tile's closure stays attached to shared RasterData and a later eviction fires _onDispose into a dead tile → OnDataDisposed → OnTileBroken → PoolTile on already-pooled state. - MapboxMapVisualizer._pendingElevationWatches: closures now self-detach on dispose too (AddDisposeCallback alongside ElevationValuesUpdated). TerrainData.Dispose() doesn't raise the values-updated event, so without this watch entries leaked until visualizer.OnDestroy. - ElevatedTerrainStrategy._pendingRebuilds: same shape, same fix — subscribe to dispose so the closure detaches on eviction. List was growing O(failed-decodes) under bursty zoom. Should-fix - Moving3dCamera.Zoom: add finite-check on preDistance, CameraDistance, and the lerp output. mapInformation.Scale=0 early in init makes CalculateCameraDistance return Inf which passed the MinDistance check and produced Inf - Inf = NaN at the assignment. Falls back to a target snap if anything non-finite slips through. - UnityTileProvider.DistToSplitScale: early return 1.0 when dz is at the floor (camera at ground). The prior dz=ε clamp made r huge, Pow saturated to Inf, scale returned ~0, distToSplit collapsed to 0, the tile pinned to MinimumZoomLevel. Street-level cameras hit this. - MapboxMapVisualizer.RecomputeTerrainBounds empty-fallback: also trigger dirty when this is the last ActiveTiles entry, so all-flat sessions reset Min/Max to TerrainInfo defaults instead of leaving them at 0 forever (the contributor-only triggers required non-zero values, missing the all-zero case). - UnityTileProvider.boundsBottom: drop the Mathf.Min(0f, …) clamp. For elevated-only terrain (Himalayan/Andean tiles with Min ≥ +500m) the clamp doubled the AABB volume by forcing the bottom to 0; using the observed min directly tightens culling and still handles sub-sea-level cases (Min is negative there, bottom goes negative). - UnityTileProvider.ShouldSplit projDist<=0 branch: cap the always-split recursion at zoom < 18. FrustumBuffer expansion admits behind-camera tiles which would otherwise recurse to _maxZoom=22 — pathologically expensive on tilted views with many corner-behind-camera tiles. - MapboxMapVisualizer filler-protection rescue: only bump Generation for children whose LoadingState is still Filler. The blanket bump would have invalidated legitimate pending-pool entries queued for other reasons. - MapInput pinch/tilt tiebreaker: normalize both magnitudes to Screen.height (DPI-independent) and let pinch win ties (pinchFrac >= tiltFrac). The strict-> on both sides previously produced "None" frames when magnitudes registered exactly equal — common on near-pure vertical drags with two parallel fingers. - MapInput pinch state across 2→3→2 finger transitions: track the active touch ID pair. EnhancedTouch.activeTouches reorders on add/ remove, so without ID tracking the next 3→2 frame compared distance against a different physical pair → single-frame zoom spike. - MapInput.Teardown virtual hook (+ MapCameraBehaviour.OnDestroy wiring, + Moving3dCamera and SlippyMapCamera overrides): unsubscribe IMapInformation events on teardown. MapInformation can outlive the camera (DontDestroyOnLoad / scene reload); without detach, the closures rooted the destroyed camera + its Camera Transform. - TerrainData: QueryHeightData(Vector2) and (float,float) overloads now guard for null ElevationValues, mirroring the 3-arg overload. Shader-only mode (ExtractCpuElevationData=false) hit the NRE. - TerrainData.Dispose idempotency: bail early on IsDisposed. - SyncExtractElevationArray.ExtractHeightData(Texture2D, TerrainData): mirror the async dispose guard — even though sync, cache-race paths can call into a disposed instance. Skip + return the rent on dispose. - SimplificationFactorDrawer.GetPropertyHeight: use the same legacy-vs-preset branch as OnGUI so the HelpBox height matches the rendered message (the longer legacy-value warning was clipping into the next inspector row). - TileProviderBenchmark: gate Input.GetKeyDown via ENABLE_LEGACY_INPUT_MANAGER and add a parallel new-Input-System path. UnityEngine.Input.* throws under "Input System Package (New)"-only. - ElevatedTerrainStrategy: refine the comment on collider-vs-render grid alignment to spell out the skirt-mode exception (collider intentionally omits the outside-tile skirt verts). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Data/DataFetchers/TerrainData.cs | 13 ++++- .../ElevatedTerrainStrategy.cs | 25 ++++++++-- .../BaseModule/Map/MapboxMapVisualizer.cs | 50 +++++++++++++++---- .../Unity/UnityTileImageContainer.cs | 10 +++- Runtime/Mapbox/Example/MapboxExamples.asmdef | 3 +- .../Example/Scripts/MapCameraBehaviour.cs | 8 +++ Runtime/Mapbox/Example/Scripts/MapInput.cs | 50 ++++++++++++++++--- .../Mapbox/Example/Scripts/Moving3dCamera.cs | 46 ++++++++++++++--- .../Mapbox/Example/Scripts/SlippyMapCamera.cs | 8 +++ .../Example/Scripts/TileProviderBenchmark.cs | 42 +++++++++++++++- .../Editor/SimplificationFactorDrawer.cs | 17 ++++++- .../DataSources/TerrainSource.cs | 7 +++ .../SyncExtractElevationArray.cs | 12 +++++ .../TileProvider/UnityTileProvider.cs | 40 +++++++++++---- 14 files changed, 283 insertions(+), 48 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs index 0430cb702..0bc8d94df 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs @@ -50,6 +50,12 @@ public override void Clear() /// public override void Dispose() { + // Explicit idempotency guard. Today the ElevationValues null-check below + // also prevents a double Return-to-pool, but the AsyncExtract late-callback + // can reassign ElevationValues after Dispose (its IsDisposed check returns + // the buffer to the pool, but a subsequent second Dispose without this guard + // would still see IsElevationDataReady etc. flicker). Cheap defensive bail. + if (IsDisposed) return; if (ElevationValues != null) { ElevationArrayPool.Return(ElevationValues); @@ -94,11 +100,16 @@ public float QueryHeightData(CanonicalTileId requestingSubTileId, float x, float public float QueryHeightData(Vector2 point) { + // Mirror the 3-arg overload's guard: shader-only mode (ExtractCpuElevationData=false) + // leaves ElevationValues null and _cachedWidth=0 — ReadElevation would index a + // null array with sectionWidth=-1 and NRE. + if (!(ElevationValues?.Length > 0)) return 0; return ReadElevation(point.x, point.y, new Vector4(1, 1, 0, 0)); } - + public float QueryHeightData(float x, float y) { + if (!(ElevationValues?.Length > 0)) return 0; return ReadElevation(x, y, new Vector4(1, 1, 0, 0)); } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index f455924e4..572bb8bcc 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -315,12 +315,18 @@ private void RegisterCollider(UnityMapTile tile) // no-ops if the tile has since been recycled onto different data, so a late // async readback does not stomp a freshly-reassigned tile. We also track // the subscription in _pendingRebuilds so OnDestroy can detach pending - // closures whose data is disposed before the event fires. + // closures whose data is disposed before the event fires, AND attach a + // dispose-callback so eviction self-detaches (TerrainData.Dispose doesn't + // raise ElevationValuesUpdated, so without this hook the closure would + // stay attached until the next visualizer teardown — pendings grow + // O(failed-decodes) under bursty zoom). Action rebuild = null; + Action onDispose = null; (TerrainData data, UnityMapTile tile, Action rebuild) entry = (data, tile, null); rebuild = () => { data.ElevationValuesUpdated -= rebuild; + data.RemoveDisposeCallback(onDispose); _pendingRebuilds.Remove(entry); if (tile == null || tile.TerrainContainer == null || tile.TerrainContainer.TerrainData != data) { @@ -328,9 +334,16 @@ private void RegisterCollider(UnityMapTile tile) } BuildAndAssignCollider(tile); }; + onDispose = () => + { + data.ElevationValuesUpdated -= rebuild; + data.RemoveDisposeCallback(onDispose); + _pendingRebuilds.Remove(entry); + }; entry.rebuild = rebuild; _pendingRebuilds.Add(entry); data.ElevationValuesUpdated += rebuild; + data.AddDisposeCallback(onDispose); } } @@ -450,9 +463,13 @@ private void BuildAndAssignCollider(UnityMapTile tile) mesh.MarkDynamic(); } - // Grid resolution matches the render mesh so the collision surface aligns with - // the visible terrain. If the user picks a coarser SimplificationFactor, the - // collider follows. + // Grid resolution matches the render mesh's INTERIOR vertex grid so the + // collision surface aligns with the visible terrain over the tile area. In + // skirt mode the render mesh adds 2 extra verts per axis (xrat = -0.01, + // 1.01) just outside the tile boundary; the collider intentionally omits + // these — there's no real geometry at the skirt and physics queries are not + // expected outside the tile. If the user picks a coarser SimplificationFactor, + // the collider follows. var sampleCount = _elevationOptions.modificationOptions.sampleCount; var side = sampleCount + 1; var size = _elevationOptions.TileMeshSize; diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 29ae423c5..a76acf997 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -158,11 +158,15 @@ public virtual void Load(TileCover tileCover) foreach (var child in tempTile.Children) { _toRemove.Remove(child.UnwrappedTileId); - // Also invalidate any in-flight pending-pool entry that - // snapshotted this child's prior generation. Without this, the - // _toRemove rescue is silently bypassed by the next FlushPendingPool - // tick (child queued earlier, before becoming a filler). - child.Generation++; + // Invalidate any in-flight pending-pool entry that snapshotted this + // child's prior generation — but only for children that are still in + // Filler state. Bumping unconditionally would also invalidate a + // pending entry queued for a different (legitimate) reason such as + // parent-finished child-pool. + if (child.LoadingState == LoadingState.Filler) + { + child.Generation++; + } } } } @@ -545,11 +549,9 @@ protected void PoolTile(UnityMapTile tile) // GetTile() for a different id. tile.Generation++; - // Snapshot whether this tile is currently holding either bounds value. - // Recycle() clears TerrainData below, so we have to check first. - // Skip the trigger when both endpoints are 0 — that's a flat tile that didn't - // actually contribute to the global bounds (would force unnecessary recomputes - // every time a sea-level tile is pooled). + // Snapshot whether this tile is currently holding either bounds value, OR + // is the last contributor in ActiveTiles (so the empty-fallback path needs + // to run). Recycle() clears TerrainData below, so we check first. var terrain = _mapInformation.Terrain; var terrainData = tile.TerrainContainer?.TerrainData; bool contributedMax = terrainData != null && terrainData.IsElevationDataReady && @@ -558,6 +560,12 @@ protected void PoolTile(UnityMapTile tile) bool contributedMin = terrainData != null && terrainData.IsElevationDataReady && terrainData.MinElevation != 0f && Mathf.Approximately(terrainData.MinElevation, terrain.MinElevation); + // Also dirty when this is the last tile in ActiveTiles: an all-flat session + // (every loaded tile had Min=Max=0) wouldn't trip the non-zero checks above, + // so the empty-fallback in RecomputeTerrainBounds (reset to TerrainInfo + // defaults) would never run when the map empties out. + bool wasLastActive = terrainData != null && ActiveTiles.Count == 1 && + ActiveTiles.ContainsKey(tile.UnwrappedTileId); TileUnloading(tile); ActiveTiles.Remove(tile.UnwrappedTileId); @@ -575,7 +583,7 @@ protected void PoolTile(UnityMapTile tile) tile.Children.Clear(); } - if (contributedMax || contributedMin) + if (contributedMax || contributedMin || wasLastActive) { _terrainBoundsDirty = true; } @@ -596,9 +604,11 @@ private void WatchForAsyncElevationDecode(TerrainData data) if (ReferenceEquals(_pendingElevationWatches[i].data, data)) return; } Action handler = null; + Action onDispose = null; handler = () => { data.ElevationValuesUpdated -= handler; + data.RemoveDisposeCallback(onDispose); for (int i = _pendingElevationWatches.Count - 1; i >= 0; i--) { if (ReferenceEquals(_pendingElevationWatches[i].data, data) && @@ -610,8 +620,26 @@ private void WatchForAsyncElevationDecode(TerrainData data) } _terrainBoundsDirty = true; }; + // Also detach on dispose: TerrainData.Dispose doesn't fire ElevationValuesUpdated, + // so without this hook the watch entry would stay in the list (rooting both + // data and the closure) until the visualizer's own OnDestroy. + onDispose = () => + { + data.ElevationValuesUpdated -= handler; + data.RemoveDisposeCallback(onDispose); + for (int i = _pendingElevationWatches.Count - 1; i >= 0; i--) + { + if (ReferenceEquals(_pendingElevationWatches[i].data, data) && + ReferenceEquals(_pendingElevationWatches[i].handler, handler)) + { + _pendingElevationWatches.RemoveAt(i); + break; + } + } + }; _pendingElevationWatches.Add((data, handler)); data.ElevationValuesUpdated += handler; + data.AddDisposeCallback(onDispose); } private void RecomputeTerrainBounds() diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs index 502a47138..9a1b71499 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs @@ -82,7 +82,15 @@ public void DisableImagery() public void OnDestroy() { - //anything to finalize here? + // Symmetric detach to mirror UnityTileTerrainContainer's fix: the multicast + // _onDispose callback we added at SetImageData must be removed here, otherwise + // a future RasterData eviction fires the closure into a destroyed tile and + // cascades through OnDataDisposed → OnTileBroken → PoolTile on a dead tile. + if (ImageData != null) + { + ImageData.RemoveDisposeCallback(_onDispose); + ImageData = null; + } } } } \ No newline at end of file diff --git a/Runtime/Mapbox/Example/MapboxExamples.asmdef b/Runtime/Mapbox/Example/MapboxExamples.asmdef index eee04e54d..bc8aeb621 100644 --- a/Runtime/Mapbox/Example/MapboxExamples.asmdef +++ b/Runtime/Mapbox/Example/MapboxExamples.asmdef @@ -8,8 +8,7 @@ "GUID:36ca6af6e2b304d4090888554d6ce199", "GUID:1b64b068a01f943108c7f4b7a5356c7a", "GUID:6055be8ebefd69e48b49212b09b47b2f", - "GUID:8ba268152fd3143f59445711358cb12f", - "Unity.InputSystem" + "GUID:8ba268152fd3143f59445711358cb12f" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs index b0aa73fd7..a46668769 100644 --- a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs @@ -41,6 +41,14 @@ protected virtual void OnDestroy() { if (MapBehaviour != null) MapBehaviour.Initialized -= OnMapInitialized; + + // Tear down the camera core's IMapInformation subscriptions. MapInformation + // can outlive the behaviour (DontDestroyOnLoad / scene reload); without the + // detach, the camera core and its captured _camera Transform stay rooted. + if (IsInitialized && Map != null) + { + Core?.Teardown(Map.MapInformation); + } } protected virtual void OnMapInitialized(MapboxMap map) diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index 989ee21a2..aee6261f4 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -45,6 +45,11 @@ public abstract class MapInput private float _previousPinchDistance; private bool _pinchActive; private int _previousTouchCount; + // Track the (touchId, touchId) of the pair we're measuring. EnhancedTouch. + // activeTouches reorders on add/remove, so 2→3→2 transitions can leave us + // comparing distance against a different physical pair. Reseed on change. + private int _previousTouch0Id = int.MinValue; + private int _previousTouch1Id = int.MinValue; // True on the frame the active touch count drops (e.g. 2→1 when one finger // lifts during a pinch). Cameras should treat this as a fresh drag start so @@ -76,6 +81,14 @@ public virtual void Initialize(Camera camera, IMapInformation mapInfo) #endif } + /// + /// Called from the owning behaviour's OnDestroy. Override to unsubscribe from + /// events you wired in — + /// IMapInformation can outlive the camera (DontDestroyOnLoad / scene reload), + /// and event closures otherwise root the destroyed camera + its Camera Transform. + /// + public virtual void Teardown(IMapInformation mapInfo) { } + public abstract CameraOutput UpdateCamera(IMapInformation mapInfo); #region Plane Intersection @@ -156,14 +169,17 @@ protected void UpdateInputState() if (touchCount < 2) { _pinchActive = false; + _previousTouch0Id = int.MinValue; + _previousTouch1Id = int.MinValue; return; } - // Read both touches' positions and Y-deltas, then pick a single gesture for - // this frame. Doing this once here (not redundantly inside each detector) - // keeps the decision symmetric and prevents stale-state races. + // Read both touches' positions, Y-deltas, and stable ids. Doing this once + // here (not redundantly inside each detector) keeps the decision symmetric + // and prevents stale-state races. Vector2 touch0Pos, touch1Pos; float touch0DeltaY, touch1DeltaY; + int t0Id, t1Id; #if MAPBOX_NEW_INPUT_SYSTEM var t0 = Touch.activeTouches[0]; var t1 = Touch.activeTouches[1]; @@ -171,6 +187,8 @@ protected void UpdateInputState() touch1Pos = t1.screenPosition; touch0DeltaY = t0.delta.y; touch1DeltaY = t1.delta.y; + t0Id = t0.touchId; + t1Id = t1.touchId; #else var t0 = Input.GetTouch(0); var t1 = Input.GetTouch(1); @@ -178,15 +196,22 @@ protected void UpdateInputState() touch1Pos = t1.position; touch0DeltaY = t0.deltaPosition.y; touch1DeltaY = t1.deltaPosition.y; + t0Id = t0.fingerId; + t1Id = t1.fingerId; #endif var currentDistance = Vector2.Distance(touch0Pos, touch1Pos); - if (!_pinchActive) + // Reseed when the pair changes (first two-finger frame OR a 2→3→2 / finger-swap + // transition that shifted which physical touches occupy slots [0..1]). Comparing + // distance against a different pair would spike a frame of phantom pinch. + bool pairChanged = !_pinchActive || + t0Id != _previousTouch0Id || t1Id != _previousTouch1Id; + if (pairChanged) { - // First frame of two-finger contact: just seed the previous distance. - // No gesture decision until next frame produces a real delta. _pinchActive = true; _previousPinchDistance = currentDistance; + _previousTouch0Id = t0Id; + _previousTouch1Id = t1Id; return; } @@ -198,12 +223,21 @@ protected void UpdateInputState() var tiltMag = Mathf.Abs(avgDeltaY); var bothFingersSameDirection = touch0DeltaY * touch1DeltaY > 0f; - if (pinchMag > tiltMag && pinchMag > 0.01f) + // Normalize both magnitudes to Screen.height so the thresholds are DPI-independent. + // MinFrac ≈ 1 pixel on a 1080-height screen. Pinch wins ties (pinchFrac >= tiltFrac) + // to avoid the "None" frame the prior strict-> on both sides produced when the + // gestures registered identical magnitudes (common on near-pure vertical drags). + var screenH = Mathf.Max(Screen.height, 1); + var pinchFrac = pinchMag / screenH; + var tiltFrac = tiltMag / screenH; + const float MinFrac = 1f / 1080f; + + if (pinchFrac > MinFrac && pinchFrac >= tiltFrac) { CurrentTwoFingerGesture = TwoFingerGesture.Pinch; _pinchDeltaThisFrame = pinchDelta; } - else if (bothFingersSameDirection && tiltMag >= 1f && tiltMag > pinchMag) + else if (bothFingersSameDirection && tiltFrac > MinFrac) { CurrentTwoFingerGesture = TwoFingerGesture.Tilt; _tiltAvgDeltaYThisFrame = avgDeltaY; diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs index 1bab14671..7491410bb 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs @@ -33,6 +33,12 @@ public class Moving3dCamera : MapInput private Vector3 _dragOrigin; private Vector3 _targetPosition; + // Stored handlers so Teardown can detach from the IMapInformation events. + // Without these, the closures root the camera (and _camera Transform) beyond + // the owning behaviour's destruction. + private Action _onLatLngChanged; + private Action _onViewChanged; + public override void Initialize(Camera camera, IMapInformation start) { @@ -41,12 +47,23 @@ public override void Initialize(Camera camera, IMapInformation start) Bearing = start.Bearing; ZoomValue = start.Zoom; SetCamera(start); - start.LatitudeLongitudeChanged += information => + _onLatLngChanged = information => { _targetPosition = Vector3.zero; SetCamera(information); }; - start.ViewChanged += SetCamera; + _onViewChanged = SetCamera; + start.LatitudeLongitudeChanged += _onLatLngChanged; + start.ViewChanged += _onViewChanged; + } + + public override void Teardown(IMapInformation mapInfo) + { + if (mapInfo == null) return; + if (_onLatLngChanged != null) mapInfo.LatitudeLongitudeChanged -= _onLatLngChanged; + if (_onViewChanged != null) mapInfo.ViewChanged -= _onViewChanged; + _onLatLngChanged = null; + _onViewChanged = null; } public override CameraOutput UpdateCamera(IMapInformation mapInformation) @@ -151,12 +168,15 @@ public void Zoom(IMapInformation mapInformation, Vector3 position, float zoomAct ZoomValue = postZoom; CameraDistance = CalculateCameraDistance(mapInformation, ZoomValue); - // Guard against div-by-zero: top-down orthographic, first-frame state, or - // a camera curve that evaluates to 0 at this zoom can make either divisor - // 0 and propagate NaN into _targetPosition. Fall back to a straight target - // snap when the cursor-zoom math has nothing meaningful to interpolate. + // Guard against div-by-zero AND non-finite Scale: top-down orthographic, + // first-frame state, a camera curve that evaluates to 0, or mapInformation.Scale + // being 0 (early init) can make CalculateCameraDistance return Inf, which + // passes a MinDistance check but later turns into Inf - Inf = NaN in the lerp. + // Fall back to a straight target snap when the cursor-zoom math has nothing + // meaningful to interpolate. const float MinDistance = 0.0001f; - if (preDistance <= MinDistance || camDistanceToMouse <= MinDistance) + if (preDistance <= MinDistance || camDistanceToMouse <= MinDistance || + !float.IsFinite(preDistance) || !float.IsFinite(CameraDistance)) { _targetPosition = postZoomTarget; return; @@ -164,7 +184,17 @@ public void Zoom(IMapInformation mapInformation, Vector3 position, float zoomAct var postDistance = CameraDistance; var newCamDistanceToMouse = camDistanceToMouse * (postDistance / preDistance); - _targetPosition = Vector3.LerpUnclamped(postZoomTarget, postZoomPos, (camDistanceToMouse - newCamDistanceToMouse) / camDistanceToMouse); + var lerped = Vector3.LerpUnclamped(postZoomTarget, postZoomPos, (camDistanceToMouse - newCamDistanceToMouse) / camDistanceToMouse); + // Belt-and-suspenders: if anything snuck a non-finite component through, + // keep _targetPosition stable instead of corrupting it. + if (float.IsFinite(lerped.x) && float.IsFinite(lerped.y) && float.IsFinite(lerped.z)) + { + _targetPosition = lerped; + } + else + { + _targetPosition = postZoomTarget; + } } private void SetCamPositionByMapInfo() diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs index 143c29ca1..bff83d749 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs @@ -97,6 +97,14 @@ public void Initialize(Camera camera, IMapInformation start, Plane? controlPlane SetCamPositionByMapInfo(); start.SetView += SetCamera; } + + public override void Teardown(IMapInformation mapInfo) + { + // Detach the SetView subscription wired in Initialize. IMapInformation can + // outlive this camera (DontDestroyOnLoad / scene reload), so without the + // detach the closure roots us and _camera Transform indefinitely. + if (mapInfo != null) mapInfo.SetView -= SetCamera; + } private const float MaxMercatorLatitude = 85.06f; diff --git a/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs b/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs index 0eec87199..92b83765a 100644 --- a/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs +++ b/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs @@ -8,6 +8,9 @@ using Mapbox.BaseModule.Utilities; using Mapbox.Example.Scripts.Map; using UnityEngine; +#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER +using UnityEngine.InputSystem; +#endif namespace Mapbox.Example.Scripts { @@ -96,11 +99,46 @@ private void Awake() StartCoroutine(_mapBehaviour.Initialize()); } + // Reads either the legacy Input.GetKeyDown or the new Input System equivalent + // depending on which input backend is active in this project. UnityEngine.Input + // throws under "Input System Package (New)"-only, so the legacy call cannot run + // unconditionally. + private static bool StartStopPressed(KeyCode key) + { +#if ENABLE_LEGACY_INPUT_MANAGER + if (Input.GetKeyDown(key)) return true; +#endif +#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER + if (Keyboard.current != null) + { + var nikey = KeyCodeToKey(key); + if (nikey != Key.None && Keyboard.current[nikey].wasPressedThisFrame) return true; + } +#endif + return false; + } + +#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER + private static Key KeyCodeToKey(KeyCode kc) + { + switch (kc) + { + case KeyCode.F5: return Key.F5; + case KeyCode.F6: return Key.F6; + case KeyCode.F7: return Key.F7; + case KeyCode.F8: return Key.F8; + case KeyCode.Space: return Key.Space; + case KeyCode.Return: return Key.Enter; + default: return Key.None; + } + } +#endif + private void Update() { - if (Input.GetKeyDown(StartKey) && !_isRunning && _isMapReady) + if (StartStopPressed(StartKey) && !_isRunning && _isMapReady) StartRun(); - if (Input.GetKeyDown(StopKey) && _isRunning) + if (StartStopPressed(StopKey) && _isRunning) StopRun(); if (!_isRunning || !_isMapReady) return; diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs index 3bc8e9d50..c7d111d87 100644 --- a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs @@ -87,7 +87,22 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { var attr = (SimplificationFactorAttribute)attribute; - var (message, _) = BuildMessage(property.intValue, attr.VertexBase); + EnsurePresetsCached(attr); + // Mirror OnGUI's branch so the help-box height matches the actual rendered + // message. The legacy-value warning is longer than the preset description, + // and using the preset string for height while painting the legacy string + // caused the HelpBox to clip and overlap the next inspector row. + var current = property.intValue; + var inPresets = System.Array.IndexOf(_cachedFactors, current) >= 0; + string message; + if (!inPresets) + { + message = $"Legacy value {current} is not a preset. Pick a preset above to migrate; the value is otherwise left untouched."; + } + else + { + (message, _) = BuildMessage(current, attr.VertexBase); + } return EditorGUIUtility.singleLineHeight + Spacing + CalcHelpBoxHeight(message, EditorGUIUtility.currentViewWidth - 40f); } diff --git a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs index 978963dc6..c2d1d0ccd 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs @@ -136,12 +136,19 @@ protected IEnumerator ExtractElevationValues(TerrainData data) // decode). Previously this path used Action + the 1-arg SetElevationValues, // which left Min/Max at 0 — meaning RecomputeTerrainBounds never widened the // shared TerrainInfo for any tile loaded through LoadTileCoroutine. + // + // Subscribe to BOTH ElevationValuesUpdated and the dispose multicast: the async + // GPU readback no-ops on disposed data and never raises ElevationValuesUpdated, + // so without the dispose hook the coroutine would yield forever and pin the + // TerrainData. Rapid pan + cache eviction during initial load is the trigger. var finished = false; Action onDone = () => finished = true; data.ElevationValuesUpdated += onDone; + data.AddDisposeCallback(onDone); _elevationDataExtractionStrategy.ExtractHeightData(data.Texture, data); while (!finished) yield return null; data.ElevationValuesUpdated -= onDone; + data.RemoveDisposeCallback(onDone); } protected override void TextureReceivedFromFile(TerrainData cacheItem) diff --git a/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs b/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs index 38a056a9c..5bfc483f5 100644 --- a/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs +++ b/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs @@ -35,10 +35,22 @@ public void ExtractHeightData(TerrainData terrainData) public void ExtractHeightData(Texture2D texture, TerrainData terrainData) { + // Even the sync path can be called on a disposed TerrainData from cache-fetch + // race paths. Skip + don't rent if already disposed; otherwise we'd + // SetElevationValues on a dead object and leak the rented buffer. + if (terrainData == null || terrainData.IsDisposed) + { + return; + } var rgbData = texture.GetRawTextureData(); var width = texture.width; var heightData = ElevationArrayPool.Rent(width * width); RunDecodeJob(rgbData, width, heightData, out var min, out var max); + if (terrainData.IsDisposed) + { + ElevationArrayPool.Return(heightData); + return; + } terrainData.SetElevationValues(heightData, min, max); } diff --git a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs index 07861a554..64fb79b4b 100644 --- a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs +++ b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs @@ -132,13 +132,15 @@ public override bool GetTileCover(IMapInformation mapInformation, TileCover tile var scale = mapInformation.Scale; // Bounds extent for frustum culling: derived from observed terrain elevation. - // Min/Max are in meters; divide by scale to get world units. Negative Min - // (Death Valley, Dead Sea) shifts the AABB bottom below Y=0; clamping the - // bottom at 0 made those tiles fall outside the AABB and frustum-cull. + // Min/Max are in meters; divide by scale to get world units. Bottom and top + // follow the observed range directly — clamping the bottom to 0 pessimized + // culling for elevated-only terrain (Himalayan/Andean tiles with Min ≥ +500m) + // by doubling the AABB volume below the actual surface. Negative Min (Death + // Valley, Dead Sea) is handled naturally by letting the bottom go negative. // Floor on the total height ensures tiles are never zero-height (invisible). var maxWorld = mapInformation.Terrain.MaxElevation / scale; var minWorld = mapInformation.Terrain.MinElevation / scale; - var boundsBottom = Mathf.Min(0f, minWorld); + var boundsBottom = minWorld; var boundsHeight = Mathf.Max(maxWorld - boundsBottom, 0.1f); // Camera height above the map plane (y=0) @@ -192,6 +194,13 @@ public override bool GetTileCover(IMapInformation mapInformation, TileCover tile return true; } + // Cap for the projDist<=0 "always split" early-exit. Without a cap, a single + // behind-camera corner (admissible after FrustumBuffer expansion) forces 4-way + // subdivision recursively up to _maxZoom — pathologically expensive on tilted + // views where many tiles are partially behind the near plane. 18 is generous + // enough for any reasonable detail level. + private const int AlwaysSplitSafeCap = 18; + protected bool ShouldSplit(int zoom, Bounds bounds, Vector3 camPos, Vector3 camForward, float cameraHeight, float zoomSplitDistance) { @@ -220,7 +229,13 @@ protected bool ShouldSplit(int zoom, Bounds bounds, Vector3 camPos, offset.y = 0f; var projDist = Vector3.Dot(offset, camForward); if (projDist <= 0f) - return true; // corner behind camera plane — always split + { + // Corner behind camera plane — split for better fidelity on the + // partly-visible portion, but cap so we don't recurse all the way + // to _maxZoom (the FrustumBuffer expansion admits behind-camera + // tiles, which would otherwise feed an endless split loop). + return zoom < AlwaysSplitSafeCap; + } if (projDist < closestDist) closestDist = projDist; } @@ -245,11 +260,16 @@ private static float DistToSplitScale(float dz, float d) { const float kAcuteAngleThresholdSin = 0.707f; // sin(45 degrees) const float kStretchTile = 1.1f; - - // Floor dz at a tiny epsilon. Without this, a camera exactly at ground level - // (dz=0) makes r = d/0 → Inf/NaN, the resulting scale fails comparisons, - // and the tile pins to its coarsest LOD forever. - if (dz < 0.0001f) dz = 0.0001f; + const float DzFloor = 0.0001f; + + // Camera at (or extremely close to) ground level. Clamping dz to a tiny + // epsilon and then computing r = d/dz makes Pow(1.1, k+1) saturate to Inf + // and the function return ~0, which collapses distToSplit to 0 — the tile + // pins to MinimumZoomLevel instead of subdividing. Street-level walking-sim + // cameras hit this. Return 1.0 (no acute-angle compensation) so the caller's + // normal split distance applies. + if (dz < DzFloor) + return 1f; if (d * kAcuteAngleThresholdSin < dz) return 1f; From 0c9953aa1b5745d9be901e1529e22d9cf47941fa Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 14:14:07 +0300 Subject: [PATCH 25/54] Minor follow-ups: divisor guard, HelpBox alloc, re-init teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MapInput.GetPinchZoomDelta / GetTwoFingerTiltDelta: use Mathf.Max(Screen.height, 1) to match the guard already present in UpdateInputState. Raw Screen.height can be 0 (minimized window) which produced Inf — caught downstream by Moving3dCamera.Zoom's finite-check but the divisor pattern should be consistent across all three sites. - SimplificationFactorDrawer.CalcHelpBoxHeight: reuse a static GUIContent and assign .text per call. GetPropertyHeight runs multiple times per repaint while the Inspector is open; the old `new GUIContent(message)` per call was steady GC churn even in idle Editor sessions. - MapCameraBehaviour.OnMapInitialized: if a previous MapInformation was bound, call Core.Teardown on it before re-binding. MapBehaviour.Initialized firing twice (re-init, swapped map asset) previously leaked the prior subscriptions until OnDestroy. Defensive — not currently exercised by the example scenes. Co-Authored-By: Claude Opus 4.7 (1M context) --- Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs | 8 ++++++++ Runtime/Mapbox/Example/Scripts/MapInput.cs | 6 ++++-- .../ImageModule/Editor/SimplificationFactorDrawer.cs | 9 +++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs index a46668769..c68efc7db 100644 --- a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs @@ -53,6 +53,14 @@ protected virtual void OnDestroy() protected virtual void OnMapInitialized(MapboxMap map) { + // Defensive: if MapBehaviour.Initialized fires twice (re-init, swapped map + // asset), unsubscribe from the previous MapInformation before binding to + // the new one. Otherwise the prior subscriptions leak until OnDestroy. + if (IsInitialized && Map != null && Map.MapInformation != null) + { + Core?.Teardown(Map.MapInformation); + } + Map = map; IsInitialized = true; Core.Initialize(Camera, map.MapInformation); diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index aee6261f4..d66fbdc9e 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -369,7 +369,9 @@ protected bool GetPinchZoomDelta(out float zoomDelta, float pinchSensitivity = 5 if (CurrentTwoFingerGesture == TwoFingerGesture.Pinch) { - zoomDelta = _pinchDeltaThisFrame / Screen.height * pinchSensitivity; + // Match the divisor guard used in UpdateInputState: raw Screen.height + // can be 0 (e.g. minimized window) and would produce Inf here. + zoomDelta = _pinchDeltaThisFrame / Mathf.Max(Screen.height, 1) * pinchSensitivity; return true; } @@ -411,7 +413,7 @@ protected bool GetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity { if (CurrentTwoFingerGesture == TwoFingerGesture.Tilt) { - tiltDelta = _tiltAvgDeltaYThisFrame / Screen.height * tiltSensitivity; + tiltDelta = _tiltAvgDeltaYThisFrame / Mathf.Max(Screen.height, 1) * tiltSensitivity; return true; } tiltDelta = 0f; diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs index c7d111d87..4f2a1da46 100644 --- a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs @@ -136,12 +136,17 @@ private static (string message, MessageType severity) BuildMessage(int factor, i return (baseLine + " Very coarse — terrain will look blocky and neighboring tiles may visibly mismatch at seams.", MessageType.Warning); } + // Reused across CalcHelpBoxHeight calls. GetPropertyHeight runs multiple times + // per repaint while the inspector is open; allocating a fresh GUIContent each + // call shows up as steady GC churn even in idle Editor sessions. + private static readonly GUIContent _helpBoxContent = new GUIContent(); + private static float CalcHelpBoxHeight(string message, float width) { - var content = new GUIContent(message); + _helpBoxContent.text = message; var style = EditorStyles.helpBox; var textWidth = Mathf.Max(40f, width - 32f); - return Mathf.Max(EditorGUIUtility.singleLineHeight * 2f, style.CalcHeight(content, textWidth)); + return Mathf.Max(EditorGUIUtility.singleLineHeight * 2f, style.CalcHeight(_helpBoxContent, textWidth)); } } } From 3e9699a7b8032298f5b5daf27488a01f5a3e8019 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 14:21:18 +0300 Subject: [PATCH 26/54] Fix TerrainData ambiguity in MapboxMapVisualizer The wildcard `using Mapbox.BaseModule.Data.DataFetchers;` exposed DataFetchers.TerrainData alongside UnityEngine.TerrainData, producing CS0104 in the _pendingElevationWatches / WatchForAsyncElevationDecode references. Switch to a type alias matching the pattern already used in UnityTileTerrainContainer.cs and ElevatedTerrainStrategy.cs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs | 4 ++-- Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs | 2 +- Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs index 69947dfd3..a0955c3c0 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs @@ -26,13 +26,13 @@ public virtual void Dispose() _onDispose = null; } - internal void AddDisposeCallback(Action callback) + public void AddDisposeCallback(Action callback) { if (callback == null) return; _onDispose += callback; } - internal void RemoveDisposeCallback(Action callback) + public void RemoveDisposeCallback(Action callback) { if (callback == null) return; _onDispose -= callback; diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index a76acf997..0f4eff733 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -2,12 +2,12 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Data.Tiles; using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; using UnityEngine; +using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.BaseModule.Map { diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index 9be2ec038..610edb597 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -162,7 +162,10 @@ public IEnumerable GetTileCoverCoroutines(IEnumerable(GetDataId(tiles)); - return materialized.Select(LoadTileData).Where(x => x != null); + // Explicit lambda (not method-group): LoadTileData's optional callback + // parameter makes the method-group conversion ambiguous between Select's + // two overloads (Func vs Func). + return materialized.Select(t => LoadTileData(t)).Where(x => x != null); } #endregion From e68c6ba3dd338532967334057089a251ee4406e2 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 14:26:51 +0300 Subject: [PATCH 27/54] remove sealed keyword from LocationProviderFactory --- .../Mapbox/LocationModule/Scripts/LocationProviderFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs b/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs index ae9feee91..93286aee4 100644 --- a/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs @@ -9,7 +9,7 @@ namespace Mapbox.LocationModule /// Factory to provide access to various LocationProviders. /// This is meant to be attached to a game object. /// - public sealed class LocationProviderFactory : MonoBehaviour + public class LocationProviderFactory : MonoBehaviour { public LocationProviderType LocationProviderType; [NonSerialized] public bool IsLocationProviderReady = false; From f54acf5618c9001b95aca28c8a004ed422b85823 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 17:52:07 +0300 Subject: [PATCH 28/54] Revert iOS-flicker defer-pool chain (Generation, _pendingPool) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deferred-pool mechanism added in 272acdcd was based on an unverified iOS flicker hypothesis. Once the queue existed, a re-borrow race surfaced (Load synchronously pools a queued tile + GetTile re-issues it, then flush re-pools the now-reassigned reference). That was patched with a Generation counter on UnityMapTile, snapshot/check on flush, filler-rescue Generation bumps, and CreateTileInstant failure-path bumps — ~150 lines of state-machine code in MapboxMapVisualizer maintaining a queue/flush invariant that only existed to support a behaviour we never reproduced. The chain put visualizer state on the tile struct (Generation), grew the central tile pipeline file, and was duplicating work that synchronous PoolTile already did correctly. Reverting it. PoolTile is synchronous again in both Load and InternalUpdateCoroutine. Removed: - UnityMapTile.Generation field - PendingPoolEntry struct, _pendingPool list, QueuePool, FlushPendingPool - Generation++ in PoolTile, CreateTileInstant failure path, filler rescue - _pendingPool flush in OnDestroy Kept (all independent of the iOS-flicker chain): - RecomputeTerrainBounds (terrain bounds reset bug — real) - _terrainBoundsDirty flag (batches CreateTile+PoolTile recomputes) - WatchForAsyncElevationDecode (shader-mode async-decode plumbing) - _pendingElevationWatches with dispose-multicast self-detach - _pendingRebuilds in ElevatedTerrainStrategy (closure-leak fix) - DelveInto iterative + Children list reuse - All other unrelated review fixes (camera, tile provider, terrain strategy, terrain data lifecycle, custom imagery, editor drawers). Memory updated: project_ios_tile_flicker.md now documents the revert and lists the less-invasive fix paths (Phase A/B PoolTile split, visualizer-side flag map, or upload-completion-aware deferral) to consider if the flicker is ever actually reproduced. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BaseModule/Map/MapboxMapVisualizer.cs | 78 ++----------------- .../Mapbox/BaseModule/Unity/UnityMapTile.cs | 7 -- 2 files changed, 5 insertions(+), 80 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 0f4eff733..35a24df8b 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -37,40 +37,6 @@ public class MapboxMapVisualizer : IMapVisualizer // Reusable scratch array to avoid per-frame allocation in InternalUpdateCoroutine private readonly UnwrappedTileId[] _quadrants = new UnwrappedTileId[4]; - // Tiles deferred for pooling on the next coroutine tick. - // Pooling a child the same frame the parent first becomes visible flickers on iOS. - // Stores (tile, generation-snapshot) so a flush can detect tiles that got - // synchronously pool-recycled and re-issued via GetTile() between queue and flush. - private struct PendingPoolEntry - { - public UnityMapTile Tile; - public int Generation; - } - private readonly List _pendingPool = new List(); - - private void QueuePool(UnityMapTile tile) - { - if (tile == null) return; - _pendingPool.Add(new PendingPoolEntry { Tile = tile, Generation = tile.Generation }); - } - - private void FlushPendingPool() - { - for (var i = 0; i < _pendingPool.Count; i++) - { - var entry = _pendingPool[i]; - // Skip entries whose tile was already pool-recycled (and possibly reused - // for a different id) between queueing and now. PoolTile bumps Generation, - // so a mismatch means we're stale. - if (entry.Tile == null || entry.Tile.Generation != entry.Generation) - { - continue; - } - PoolTile(entry.Tile); - } - _pendingPool.Clear(); - } - // Explicit stack for the iterative DelveInto (was recursive with per-call new bool[4] // and new UnwrappedTileId[4] allocations). Bounded by recursion depth × 4, so the // List reaches its peak size after one large call and never re-allocates after. @@ -158,15 +124,6 @@ public virtual void Load(TileCover tileCover) foreach (var child in tempTile.Children) { _toRemove.Remove(child.UnwrappedTileId); - // Invalidate any in-flight pending-pool entry that snapshotted this - // child's prior generation — but only for children that are still in - // Filler state. Bumping unconditionally would also invalidate a - // pending entry queued for a different (legitimate) reason such as - // parent-finished child-pool. - if (child.LoadingState == LoadingState.Filler) - { - child.Generation++; - } } } } @@ -214,11 +171,7 @@ public virtual void Load(TileCover tileCover) TempTiles.Remove(tile); } - // Defer pooling by one frame for the iOS-flicker rationale. A subsequent - // Load before the flush may re-queue the same tile via the same code - // path — that's harmless because PoolTile bumps Generation and the flush - // filters by gen-snapshot match (second queue is skipped). - QueuePool(tile); + PoolTile(tile); } } @@ -242,20 +195,13 @@ public virtual void InternalUpdateCoroutine() { // Drain the terrain-bounds dirty flag once per tick. Recompute is O(ActiveTiles) // so doing it here (and not on every PoolTile / CreateTile / async-decode event) - // turns what was O(N²) for a Load into a single O(N) pass per frame. + // turns the per-event cost into a single O(N) pass per frame. if (_terrainBoundsDirty) { RecomputeTerrainBounds(); _terrainBoundsDirty = false; } - // Flush tiles deferred from the previous tick. Holding the children active for - // one extra frame after the parent's ShowTile lets the parent finish rendering - // before the children deactivate, which avoids a one-frame transparent gap on - // iOS (see project_ios_tile_flicker note). FlushPendingPool filters out entries - // whose tile was already pool-recycled and re-issued between queue and flush. - FlushPendingPool(); - //finish temp tiles from tempTiles list _toRemove.Clear(); for (var index = TempTiles.Count - 1; index >= 0; index--) @@ -273,9 +219,9 @@ public virtual void InternalUpdateCoroutine() if (tilePair.Children != null && tilePair.Children.Count > 0) { - for (int c = 0; c < tilePair.Children.Count; c++) + foreach (var child in tilePair.Children) { - QueuePool(tilePair.Children[c]); + PoolTile(child); } tilePair.Children.Clear(); } @@ -305,7 +251,7 @@ public virtual void InternalUpdateCoroutine() TempTiles.Remove(tile); } - QueuePool(tile); + PoolTile(tile); } } } @@ -359,10 +305,6 @@ public void OnDestroy() } _pendingElevationWatches.Clear(); - // Flush any deferred pool entries so each pending tile gets its Recycle() - // (returns elevation arrays to the pool, restores shared meshes, etc). - FlushPendingPool(); - foreach (var layerModule in LayerModules) { layerModule.OnDestroy(); @@ -544,11 +486,6 @@ protected void PoolTile(UnityMapTile tile) if (tile.LoadingState == LoadingState.None) return; - // Invalidate any deferred-pool entries that snapshotted this tile's previous - // generation — they'd otherwise re-pool the tile after it's been reissued via - // GetTile() for a different id. - tile.Generation++; - // Snapshot whether this tile is currently holding either bounds value, OR // is the last contributor in ActiveTiles (so the empty-fallback path needs // to run). Recycle() clears TerrainData below, so we check first. @@ -696,11 +633,6 @@ protected bool CreateTileInstant(UnwrappedTileId tileId, out UnityMapTile tile) { tile.Recycle(); tile.LoadingState = LoadingState.None; - // Mirror PoolTile's bump so any pending-pool entry that snapshotted - // this tile's pre-failure generation is invalidated when the next - // flush runs. The failure path is otherwise a silent bypass of the - // re-borrow protection enforced everywhere else. - tile.Generation++; _tileCreator.PutTile(tile); } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs index fa700fdaf..b4c57f594 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs @@ -72,13 +72,6 @@ public void ApplyPropertyBlock() public LoadingState LoadingState; - // Incremented every time this tile is pool-recycled. Deferred-pool consumers - // (MapboxMapVisualizer._pendingPool) snapshot this when queuing and skip the - // flush if the value has moved — protects against re-borrow races where the - // tile got synchronously pooled and re-issued via GetTile() between queue and - // flush. - public int Generation; - public void Awake() { ImageContainer = new UnityTileImageContainer(this, DataDisposed); From f8918f3409d6a1ec40571a366a922ece86287413 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 18:03:53 +0300 Subject: [PATCH 29/54] Remove TileProviderBenchmark from the package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The benchmark was an internal tool that never should have been in the package. Untrack it from git, add to .gitignore so future `git add .` doesn't re-include it, and keep the file local for ad-hoc use. CHANGELOG line about the recorder removal in the benchmark is also dropped — the whole tool is gone from the package now. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 + CHANGELOG.md | 3 +- .../Example/Scripts/TileProviderBenchmark.cs | 404 ------------------ .../Scripts/TileProviderBenchmark.cs.meta | 11 - 4 files changed, 5 insertions(+), 417 deletions(-) delete mode 100644 Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs delete mode 100644 Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta diff --git a/.gitignore b/.gitignore index a4331fb58..8bafea12a 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ TextMesh Pro Library *.csproj Runtime/Mapbox/VectorModule/MeshGeneration/.idea + +# Internal benchmark tool, kept local-only +Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs +Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d39e6e4..d011cd038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,8 @@ Minor-version bump per semver: this release contains source-breaking API changes - `TerrainData.ElevationValuesUpdated` is now declared `public event Action` (was a plain `public Action` field). External code can subscribe with `+=` / unsubscribe with `-=` as before, but **direct assignment (`= myCallback`) and direct invocation (`ElevationValuesUpdated()`) no longer compile**. Migrate to `+=` for subscription. The previous single-setter `SetElevationChangedCallback` (which silently wiped other subscribers) has been removed. - `CustomTMSTile` constructor now takes two additional parameters (`invertY`, `isMapboxService`). The legacy 4-arg constructor (`urlFormat`, `tileId`, `tilesetId`, `useNonReadableTexture`) is preserved as an overload defaulting to `invertY: true, isMapboxService: false` — external subclasses keep compiling. New code should use the 6-arg form. - `CustomSource.CreateTile` now always returns a `CustomTMSTile` when `UrlFormat` is set, regardless of `InvertY`. Previously `InvertY=false` returned a plain `RasterTile`. If you relied on the plain-raster path with a non-empty `UrlFormat`, set `UrlFormat = ""` to keep that behavior — empty `UrlFormat` falls back to plain `RasterTile`. -- `MapboxTileData.SetDisposeCallback` was replaced with `AddDisposeCallback` / `RemoveDisposeCallback` (multicast). This is an `internal` API; only relevant if you have access to it via reflection or an internal-visible-to dependency. +- `MapboxTileData.SetDisposeCallback` was replaced with `public AddDisposeCallback` / `RemoveDisposeCallback` (multicast). External code that previously assigned through reflection or an internal-visible-to dependency should switch to the add/remove pair; subscriptions are now safely shared across the up-to-16 render tiles that consume the same `TerrainData`. - `CustomTerrainLayerModuleScript` now passes the user-configured `Settings.DataSettings` (cache size, retina flag, non-readable textures, data-zoom clamp) through to `CustomTerrainSource`. Previously a throwaway `ImageSourceSettings` was used and the Inspector values had no runtime effect. -- `TileProviderBenchmark`: the Unity Recorder video-capture path has been removed. The benchmark still collects per-frame tile statistics and writes them to JSON; if you need video capture, install `com.unity.recorder` separately and wire it up in your own scene script. #### Changes - Added new documentation. diff --git a/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs b/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs deleted file mode 100644 index 92b83765a..000000000 --- a/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs +++ /dev/null @@ -1,404 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Mapbox.BaseModule.Data.Tiles; -using Mapbox.BaseModule.Data.Vector2d; -using Mapbox.BaseModule.Map; -using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.Map; -using UnityEngine; -#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER -using UnityEngine.InputSystem; -#endif - -namespace Mapbox.Example.Scripts -{ - /// - /// Runtime benchmark for tile providers. Animates the camera through a scripted path - /// (pan between two lat/lng points with rotation) and records the tile cover output - /// every frame. Results are written to a JSON file for comparison across algorithm changes. - /// - /// Usage: - /// 1. Add this component to the same GameObject as MapboxMapBehaviour - /// 2. Set StartLatLng, EndLatLng, zoom, pitch, bearing parameters - /// 3. Enter Play mode — the camera will animate automatically - /// 4. On-screen overlay shows live tile count and zoom distribution - /// 5. When the run finishes (or you press the StopKey), results are saved to Application.persistentDataPath - /// - public class TileProviderBenchmark : MonoBehaviour - { - [Header("Camera Path")] - public double StartLat = 40.7128; - public double StartLng = -74.0060; - public double EndLat = 40.7580; - public double EndLng = -73.9855; - - [Header("Camera Settings")] - public float Zoom = 16f; - public float StartPitch = 45f; - public float EndPitch = 45f; - public float StartBearing = 0f; - public float EndBearing = 90f; - - [Header("Animation")] - public float DurationSeconds = 10f; - public bool PingPong = true; - public int PingPongCycles = 2; - public bool AutoStart = true; - - [Header("Controls")] - public KeyCode StartKey = KeyCode.F5; - public KeyCode StopKey = KeyCode.F6; - - [Header("Output")] - public string OutputFileName = "tile_benchmark"; - - private MapBehaviourCore _mapBehaviour; - private MapboxMap _map; - private bool _isMapReady; - private bool _isRunning; - private float _elapsed; - private int _totalFrames; - private int _currentCycle; - - // Per-frame recording - private List _records = new List(2000); - - // Live stats for on-screen display - private int _lastTileCount; - private int[] _lastZoomDistribution = new int[23]; // z0-z22 - private int _tileChangesThisRun; - private HashSet _previousFrameTiles = new HashSet(); - - private void Awake() - { - _mapBehaviour = GetComponent(); - if (_mapBehaviour == null) - { - Debug.LogError("[TileProviderBenchmark] No MapBehaviourCore found on this GameObject."); - enabled = false; - return; - } - - // Set start position before initialization so the map loads at the right place - _mapBehaviour.MapInformation.SetInformation( - new LatitudeLongitude(StartLat, StartLng), Zoom, StartPitch, StartBearing); - - _mapBehaviour.Initialized += map => - { - _map = map; - _map.LoadViewCompleted += () => - { - _isMapReady = true; - if (AutoStart) StartRun(); - }; - }; - - // Start initialization ourselves (InitializeOnStart should be unchecked on MapBehaviourCore) - StartCoroutine(_mapBehaviour.Initialize()); - } - - // Reads either the legacy Input.GetKeyDown or the new Input System equivalent - // depending on which input backend is active in this project. UnityEngine.Input - // throws under "Input System Package (New)"-only, so the legacy call cannot run - // unconditionally. - private static bool StartStopPressed(KeyCode key) - { -#if ENABLE_LEGACY_INPUT_MANAGER - if (Input.GetKeyDown(key)) return true; -#endif -#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER - if (Keyboard.current != null) - { - var nikey = KeyCodeToKey(key); - if (nikey != Key.None && Keyboard.current[nikey].wasPressedThisFrame) return true; - } -#endif - return false; - } - -#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER - private static Key KeyCodeToKey(KeyCode kc) - { - switch (kc) - { - case KeyCode.F5: return Key.F5; - case KeyCode.F6: return Key.F6; - case KeyCode.F7: return Key.F7; - case KeyCode.F8: return Key.F8; - case KeyCode.Space: return Key.Space; - case KeyCode.Return: return Key.Enter; - default: return Key.None; - } - } -#endif - - private void Update() - { - if (StartStopPressed(StartKey) && !_isRunning && _isMapReady) - StartRun(); - if (StartStopPressed(StopKey) && _isRunning) - StopRun(); - - if (!_isRunning || !_isMapReady) return; - - _elapsed += Time.deltaTime; - - // Compute normalized time with ping-pong - float totalDuration = DurationSeconds * (PingPong ? PingPongCycles * 2 : 1); - if (_elapsed >= totalDuration) - { - StopRun(); - return; - } - - float cycleTime = _elapsed % (PingPong ? DurationSeconds * 2 : DurationSeconds); - float t; - if (PingPong && cycleTime > DurationSeconds) - t = 1f - (cycleTime - DurationSeconds) / DurationSeconds; - else - t = cycleTime / DurationSeconds; - - t = Mathf.SmoothStep(0f, 1f, t); - - // Interpolate camera parameters - var lat = Lerp(StartLat, EndLat, t); - var lng = Lerp(StartLng, EndLng, t); - var pitch = Mathf.Lerp(StartPitch, EndPitch, t); - var bearing = Mathf.Lerp(StartBearing, EndBearing, t); - - var latlng = new LatitudeLongitude(lat, lng); - _map.ChangeView(latlng, Zoom, pitch, bearing); - - // Record this frame - RecordFrame(t, lat, lng, pitch, bearing); - _totalFrames++; - } - - public void StartRun() - { - _isRunning = true; - _elapsed = 0f; - _totalFrames = 0; - _currentCycle = 0; - _tileChangesThisRun = 0; - _records.Clear(); - _previousFrameTiles.Clear(); - Debug.Log($"[TileProviderBenchmark] Run started. Duration: {DurationSeconds}s, PingPong: {PingPong}"); - } - - public void StopRun() - { - _isRunning = false; - SaveResults(); - Debug.Log($"[TileProviderBenchmark] Run complete. {_totalFrames} frames recorded, {_tileChangesThisRun} tile changes. Results saved."); - } - - private void RecordFrame(float t, double lat, double lng, float pitch, float bearing) - { - var tiles = _map.TileCover.Tiles; - _lastTileCount = tiles.Count; - - // Zoom distribution - Array.Clear(_lastZoomDistribution, 0, _lastZoomDistribution.Length); - foreach (var tile in tiles) - { - if (tile.Z >= 0 && tile.Z < _lastZoomDistribution.Length) - _lastZoomDistribution[tile.Z]++; - } - - // Count tile changes from previous frame - int added = 0; - int removed = 0; - foreach (var tile in tiles) - { - if (!_previousFrameTiles.Contains(tile)) - added++; - } - foreach (var tile in _previousFrameTiles) - { - if (!tiles.Contains(tile)) - removed++; - } - _tileChangesThisRun += added + removed; - - // Build zoom distribution string - var zoomDist = new int[23]; - Array.Copy(_lastZoomDistribution, zoomDist, 23); - - _records.Add(new FrameRecord - { - Frame = _totalFrames, - Time = _elapsed, - T = t, - Lat = lat, - Lng = lng, - Pitch = pitch, - Bearing = bearing, - TileCount = _lastTileCount, - TilesAdded = added, - TilesRemoved = removed, - ZoomDistribution = zoomDist, - CameraPosition = Camera.main != null ? Camera.main.transform.position : Vector3.zero, - }); - - // Update previous frame set - _previousFrameTiles.Clear(); - foreach (var tile in tiles) - _previousFrameTiles.Add(tile); - } - - private void SaveResults() - { - var sb = new StringBuilder(); - sb.AppendLine("{"); - sb.AppendLine($" \"provider\": \"{_map.MapService.GetType().Name}\","); - sb.AppendLine($" \"startLatLng\": [{StartLat}, {StartLng}],"); - sb.AppendLine($" \"endLatLng\": [{EndLat}, {EndLng}],"); - sb.AppendLine($" \"zoom\": {Zoom},"); - sb.AppendLine($" \"startPitch\": {StartPitch}, \"endPitch\": {EndPitch},"); - sb.AppendLine($" \"startBearing\": {StartBearing}, \"endBearing\": {EndBearing},"); - sb.AppendLine($" \"duration\": {DurationSeconds},"); - sb.AppendLine($" \"pingPong\": {PingPong.ToString().ToLower()},"); - sb.AppendLine($" \"totalFrames\": {_totalFrames},"); - sb.AppendLine($" \"totalTileChanges\": {_tileChangesThisRun},"); - - // Summary stats - int maxTiles = 0, minTiles = int.MaxValue; - long sumTiles = 0; - int maxChanges = 0; - long sumChanges = 0; - foreach (var r in _records) - { - if (r.TileCount > maxTiles) maxTiles = r.TileCount; - if (r.TileCount < minTiles) minTiles = r.TileCount; - sumTiles += r.TileCount; - var changes = r.TilesAdded + r.TilesRemoved; - if (changes > maxChanges) maxChanges = changes; - sumChanges += changes; - } - sb.AppendLine($" \"summary\": {{"); - sb.AppendLine($" \"minTileCount\": {minTiles},"); - sb.AppendLine($" \"maxTileCount\": {maxTiles},"); - sb.AppendLine($" \"avgTileCount\": {(_records.Count > 0 ? sumTiles / (float)_records.Count : 0):F1},"); - sb.AppendLine($" \"maxFrameChanges\": {maxChanges},"); - sb.AppendLine($" \"avgFrameChanges\": {(_records.Count > 0 ? sumChanges / (float)_records.Count : 0):F2}"); - sb.AppendLine($" }},"); - - // Per-frame data - sb.AppendLine(" \"frames\": ["); - for (int i = 0; i < _records.Count; i++) - { - var r = _records[i]; - var zd = FormatZoomDist(r.ZoomDistribution); - sb.Append($" {{\"f\":{r.Frame},\"t\":{r.T:F4},\"lat\":{r.Lat:F6},\"lng\":{r.Lng:F6}," + - $"\"pitch\":{r.Pitch:F1},\"bearing\":{r.Bearing:F1}," + - $"\"tiles\":{r.TileCount},\"added\":{r.TilesAdded},\"removed\":{r.TilesRemoved}," + - $"\"camY\":{r.CameraPosition.y:F2}," + - $"\"zoom_dist\":{zd}}}"); - sb.AppendLine(i < _records.Count - 1 ? "," : ""); - } - sb.AppendLine(" ]"); - sb.AppendLine("}"); - - var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - var path = Path.Combine(Application.persistentDataPath, $"{OutputFileName}_{timestamp}.json"); - File.WriteAllText(path, sb.ToString()); - Debug.Log($"[TileProviderBenchmark] Results saved to: {path}"); - } - - private string FormatZoomDist(int[] dist) - { - var sb = new StringBuilder("{"); - bool first = true; - for (int z = 0; z < dist.Length; z++) - { - if (dist[z] > 0) - { - if (!first) sb.Append(","); - sb.Append($"\"{z}\":{dist[z]}"); - first = false; - } - } - sb.Append("}"); - return sb.ToString(); - } - - private void OnGUI() - { - if (!_isRunning && _records.Count == 0) return; - - var style = new GUIStyle(GUI.skin.label) - { - fontSize = 14, - fontStyle = FontStyle.Bold, - }; - style.normal.textColor = Color.white; - - float x = 10, y = 10; - float lineHeight = 20; - - // Measure content height first - int zoomLines = 0; - for (int z = 0; z < _lastZoomDistribution.Length; z++) - { - if (_lastZoomDistribution[z] > 0) - zoomLines++; - } - // header + stats + "Zoom distribution:" + zoom bars + optional hint line + padding - float contentHeight = lineHeight * (3 + zoomLines) + 20; - if (!_isRunning && _records.Count > 0) - contentHeight += lineHeight + 4; - - // Background - GUI.DrawTexture(new Rect(5, 5, 320, contentHeight), Texture2D.grayTexture); - - GUI.Label(new Rect(x, y, 300, lineHeight), - _isRunning ? "TILE BENCHMARK RUNNING" : "BENCHMARK COMPLETE", style); - y += lineHeight; - - GUI.Label(new Rect(x, y, 300, lineHeight), - $"Frame: {_totalFrames} Tiles: {_lastTileCount} Changes: {_tileChangesThisRun}", style); - y += lineHeight; - - // Zoom distribution bars (highest zoom first) - GUI.Label(new Rect(x, y, 300, lineHeight), "Zoom distribution:", style); - y += lineHeight; - for (int z = _lastZoomDistribution.Length - 1; z >= 0; z--) - { - if (_lastZoomDistribution[z] > 0) - { - var barWidth = Mathf.Min(_lastZoomDistribution[z] * 4, 200); - GUI.DrawTexture(new Rect(x + 30, y + 3, barWidth, 14), Texture2D.whiteTexture); - GUI.Label(new Rect(x, y, 30, lineHeight), $"z{z}", style); - GUI.Label(new Rect(x + 35 + barWidth, y, 50, lineHeight), $"{_lastZoomDistribution[z]}", style); - y += lineHeight; - } - } - - if (!_isRunning && _records.Count > 0) - { - y += 4; - GUI.Label(new Rect(x, y, 300, lineHeight), - $"Press {StartKey} to re-run, {StopKey} during run to stop early", style); - } - } - - private static double Lerp(double a, double b, float t) => a + (b - a) * t; - - [Serializable] - private struct FrameRecord - { - public int Frame; - public float Time; - public float T; - public double Lat, Lng; - public float Pitch, Bearing; - public int TileCount; - public int TilesAdded, TilesRemoved; - public int[] ZoomDistribution; - public Vector3 CameraPosition; - } - } -} diff --git a/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta b/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta deleted file mode 100644 index 5290c5114..000000000 --- a/Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ba1a10f5fb2ec4f299d716318a436736 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: From 1576451a5b203bed14703de565387a1f2ea78441 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 18:07:17 +0300 Subject: [PATCH 30/54] Extract IPointerInput abstraction; remove MAPBOX_NEW_INPUT_SYSTEM branches from MapInput Twelve #if MAPBOX_NEW_INPUT_SYSTEM blocks in MapInput.cs collapsed to zero. All input-backend specifics now live in a single new file (PointerInput.cs) behind one #if/#else, with one IPointerInput implementation compiled in based on whether com.unity.inputsystem is installed. PointerInput.cs: - IPointerInput interface: TouchCount + per-slot Position/DeltaY/Id/ Phase/PointerId, MousePosition + LMB/RMB pressed/held, MouseScrollY (already normalized to ~0.1/notch on both backends), EnableTouchSupport. - Two sealed implementations: one for the new Input System (uses EnhancedTouch + Mouse.current), one for the legacy Input class. - PointerTouchPhase enum decouples MapInput from each backend's TouchPhase type. MapInput.cs: - Drops the #if-gated using-block at the top. - Drops the EnhancedTouchSupport.Enable() call in Initialize (now IPointerInput.EnableTouchSupport()). - GetTouchCount, IsPointerOverUI, GetPointerPosition, GetPointerDown, GetPointerHeld, GetSecondaryDown, GetSecondaryHeld, GetPinchZoomDelta, GetZoomCenter, and the two-finger read block in UpdateInputState now all read through _input. No #if branches anywhere. No behaviour change: each backend's PointerInput maps onto the same primitives MapInput previously read directly via #if. MouseScrollY folds the /1200f new-input normalization into the abstraction so the caller no longer needs to know about it. Co-Authored-By: Claude Opus 4.7 (1M context) --- Runtime/Mapbox/Example/Scripts/MapInput.cs | 170 ++++-------------- .../Mapbox/Example/Scripts/PointerInput.cs | 147 +++++++++++++++ .../Example/Scripts/PointerInput.cs.meta | 11 ++ 3 files changed, 196 insertions(+), 132 deletions(-) create mode 100644 Runtime/Mapbox/Example/Scripts/PointerInput.cs create mode 100644 Runtime/Mapbox/Example/Scripts/PointerInput.cs.meta diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index d66fbdc9e..d85c4dadf 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -3,11 +3,6 @@ using Mapbox.BaseModule.Utilities; using UnityEngine; using UnityEngine.EventSystems; -#if MAPBOX_NEW_INPUT_SYSTEM -using UnityEngine.InputSystem; -using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch; -using TouchPhase = UnityEngine.InputSystem.TouchPhase; -#endif namespace Mapbox.Example.Scripts.MapInput { @@ -72,13 +67,15 @@ public enum TwoFingerGesture private float _pinchDeltaThisFrame; private float _tiltAvgDeltaYThisFrame; + // Backend-agnostic input source. PointerInput's two implementations are gated + // by MAPBOX_NEW_INPUT_SYSTEM in PointerInput.cs; the rest of this file is + // free of #if branching as a result. + private readonly IPointerInput _input = new PointerInput(); + public virtual void Initialize(Camera camera, IMapInformation mapInfo) { _camera = camera ? camera : Camera.main; -#if MAPBOX_NEW_INPUT_SYSTEM - if (!UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.enabled) - UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Enable(); -#endif + _input.EnableTouchSupport(); } /// @@ -141,14 +138,7 @@ protected static float ClampBearing(float bearing) /// /// Returns the number of active touches, or 0 if no touch device is available. /// - private int GetTouchCount() - { -#if MAPBOX_NEW_INPUT_SYSTEM - return Touch.activeTouches.Count; -#else - return Input.touchCount; -#endif - } + private int GetTouchCount() => _input.TouchCount; /// /// Call at the start of UpdateCamera to maintain input state between frames. @@ -177,28 +167,12 @@ protected void UpdateInputState() // Read both touches' positions, Y-deltas, and stable ids. Doing this once // here (not redundantly inside each detector) keeps the decision symmetric // and prevents stale-state races. - Vector2 touch0Pos, touch1Pos; - float touch0DeltaY, touch1DeltaY; - int t0Id, t1Id; -#if MAPBOX_NEW_INPUT_SYSTEM - var t0 = Touch.activeTouches[0]; - var t1 = Touch.activeTouches[1]; - touch0Pos = t0.screenPosition; - touch1Pos = t1.screenPosition; - touch0DeltaY = t0.delta.y; - touch1DeltaY = t1.delta.y; - t0Id = t0.touchId; - t1Id = t1.touchId; -#else - var t0 = Input.GetTouch(0); - var t1 = Input.GetTouch(1); - touch0Pos = t0.position; - touch1Pos = t1.position; - touch0DeltaY = t0.deltaPosition.y; - touch1DeltaY = t1.deltaPosition.y; - t0Id = t0.fingerId; - t1Id = t1.fingerId; -#endif + var touch0Pos = _input.GetTouchPosition(0); + var touch1Pos = _input.GetTouchPosition(1); + var touch0DeltaY = _input.GetTouchDeltaY(0); + var touch1DeltaY = _input.GetTouchDeltaY(1); + var t0Id = _input.GetTouchId(0); + var t1Id = _input.GetTouchId(1); var currentDistance = Vector2.Distance(touch0Pos, touch1Pos); // Reseed when the pair changes (first two-finger frame OR a 2→3→2 / finger-swap @@ -252,16 +226,9 @@ protected bool IsPointerOverUI() if (EventSystem.current == null) return false; -#if MAPBOX_NEW_INPUT_SYSTEM - var touchCount = GetTouchCount(); - if (touchCount > 0) - return EventSystem.current.IsPointerOverGameObject(Touch.activeTouches[0].finger.index); + if (_input.TouchCount > 0) + return EventSystem.current.IsPointerOverGameObject(_input.GetTouchPointerId(0)); return EventSystem.current.IsPointerOverGameObject(); -#else - if (Input.touchCount > 0) - return EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId); - return EventSystem.current.IsPointerOverGameObject(); -#endif } /// @@ -269,17 +236,9 @@ protected bool IsPointerOverUI() /// protected Vector3 GetPointerPosition() { -#if MAPBOX_NEW_INPUT_SYSTEM - if (Touch.activeTouches.Count > 0) - return Touch.activeTouches[0].screenPosition; - if (Mouse.current != null) - return Mouse.current.position.ReadValue(); - return Vector3.zero; -#else - if (Input.touchCount > 0) - return Input.GetTouch(0).position; - return Input.mousePosition; -#endif + if (_input.TouchCount > 0) + return _input.GetTouchPosition(0); + return _input.MousePosition; } /// @@ -287,15 +246,9 @@ protected Vector3 GetPointerPosition() /// protected bool GetPointerDown() { -#if MAPBOX_NEW_INPUT_SYSTEM - if (Touch.activeTouches.Count > 0) - return Touch.activeTouches[0].phase == TouchPhase.Began; - return Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame; -#else - if (Input.touchCount > 0) - return Input.GetTouch(0).phase == UnityEngine.TouchPhase.Began; - return Input.GetMouseButtonDown(0); -#endif + if (_input.TouchCount > 0) + return _input.GetTouchPhase(0) == PointerTouchPhase.Began; + return _input.MouseLeftPressedThisFrame; } /// @@ -304,26 +257,15 @@ protected bool GetPointerDown() /// protected bool GetPointerHeld() { -#if MAPBOX_NEW_INPUT_SYSTEM - var touchCount = Touch.activeTouches.Count; + var touchCount = _input.TouchCount; if (touchCount == 1) { - var phase = Touch.activeTouches[0].phase; - return phase == TouchPhase.Moved || phase == TouchPhase.Stationary; + var phase = _input.GetTouchPhase(0); + return phase == PointerTouchPhase.Moved || phase == PointerTouchPhase.Stationary; } if (touchCount == 0) - return Mouse.current != null && Mouse.current.leftButton.isPressed; - return false; -#else - if (Input.touchCount == 1) - { - var phase = Input.GetTouch(0).phase; - return phase == UnityEngine.TouchPhase.Moved || phase == UnityEngine.TouchPhase.Stationary; - } - if (Input.touchCount == 0) - return Input.GetMouseButton(0); + return _input.MouseLeftHeld; return false; -#endif } /// @@ -331,15 +273,9 @@ protected bool GetPointerHeld() /// protected bool GetSecondaryDown() { -#if MAPBOX_NEW_INPUT_SYSTEM - if (Touch.activeTouches.Count > 0) - return false; - return Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame; -#else - if (Input.touchCount > 0) + if (_input.TouchCount > 0) return false; - return Input.GetMouseButtonDown(1); -#endif + return _input.MouseRightPressedThisFrame; } /// @@ -347,15 +283,9 @@ protected bool GetSecondaryDown() /// protected bool GetSecondaryHeld() { -#if MAPBOX_NEW_INPUT_SYSTEM - if (Touch.activeTouches.Count > 0) + if (_input.TouchCount > 0) return false; - return Mouse.current != null && Mouse.current.rightButton.isPressed; -#else - if (Input.touchCount > 0) - return false; - return Input.GetMouseButton(1); -#endif + return _input.MouseRightHeld; } /// @@ -376,31 +306,17 @@ protected bool GetPinchZoomDelta(out float zoomDelta, float pinchSensitivity = 5 } // Mouse scroll fallback only when no two-finger gesture is active. - if (GetTouchCount() >= 2) + if (_input.TouchCount >= 2) { return false; } -#if MAPBOX_NEW_INPUT_SYSTEM - if (Mouse.current != null) + var scrollY = _input.MouseScrollY; + if (Mathf.Abs(scrollY) > 0f) { - var scroll = Mouse.current.scroll.ReadValue(); - if (Mathf.Abs(scroll.y) > 0) - { - // Raw scroll.y is ~120 per notch on Windows; legacy Input.GetAxis - // returns ~0.1 per notch. Divide by 1200 so both paths feed the - // same magnitude into ZoomSensitivity. - zoomDelta = scroll.y / 1200f; - return true; - } - } -#else - if (Input.mouseScrollDelta.magnitude > 0) - { - zoomDelta = Input.GetAxis("Mouse ScrollWheel"); + zoomDelta = scrollY; return true; } -#endif return false; } @@ -426,23 +342,13 @@ protected bool GetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity /// protected Vector3 GetZoomCenter() { -#if MAPBOX_NEW_INPUT_SYSTEM - if (Touch.activeTouches.Count >= 2) - { - var touch0 = Touch.activeTouches[0]; - var touch1 = Touch.activeTouches[1]; - return ((Vector3)touch0.screenPosition + (Vector3)touch1.screenPosition) / 2f; - } - return Mouse.current != null ? (Vector3)Mouse.current.position.ReadValue() : Vector3.zero; -#else - if (Input.touchCount >= 2) + if (_input.TouchCount >= 2) { - var touch0 = Input.GetTouch(0); - var touch1 = Input.GetTouch(1); - return (touch0.position + touch1.position) / 2f; + Vector3 t0 = _input.GetTouchPosition(0); + Vector3 t1 = _input.GetTouchPosition(1); + return (t0 + t1) / 2f; } - return Input.mousePosition; -#endif + return _input.MousePosition; } #endregion diff --git a/Runtime/Mapbox/Example/Scripts/PointerInput.cs b/Runtime/Mapbox/Example/Scripts/PointerInput.cs new file mode 100644 index 000000000..41ebe0a8c --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/PointerInput.cs @@ -0,0 +1,147 @@ +using UnityEngine; +#if MAPBOX_NEW_INPUT_SYSTEM +using UnityEngine.InputSystem; +using NewTouch = UnityEngine.InputSystem.EnhancedTouch.Touch; +using NewTouchPhase = UnityEngine.InputSystem.TouchPhase; +#endif + +namespace Mapbox.Example.Scripts.MapInput +{ + /// + /// Backend-agnostic touch phase. Both legacy UnityEngine.TouchPhase and + /// UnityEngine.InputSystem.TouchPhase are mapped onto this enum so the + /// MapInput call-sites don't need to know which input system is compiled in. + /// + internal enum PointerTouchPhase + { + None, + Began, + Moved, + Stationary, + Ended, + Canceled + } + + /// + /// Thin abstraction over Unity's two input backends. + /// has exactly one of two implementations compiled in, selected by the + /// MAPBOX_NEW_INPUT_SYSTEM define (which the asmdef's versionDefines sets when + /// com.unity.inputsystem is installed). MapInput.cs talks only to this interface, + /// so the rest of the camera/input layer stays free of #if branching. + /// + internal interface IPointerInput + { + /// + /// Called once from MapInput.Initialize. Legacy is a no-op; new-input + /// path enables EnhancedTouchSupport. + /// + void EnableTouchSupport(); + + int TouchCount { get; } + Vector2 GetTouchPosition(int index); + float GetTouchDeltaY(int index); + int GetTouchId(int index); + PointerTouchPhase GetTouchPhase(int index); + /// + /// Identifier suitable for EventSystem.IsPointerOverGameObject(int). + /// Returns finger.index on the new system and fingerId on legacy + /// — both are what their respective EventSystem expects. + /// + int GetTouchPointerId(int index); + + Vector3 MousePosition { get; } + bool MouseLeftPressedThisFrame { get; } + bool MouseLeftHeld { get; } + bool MouseRightPressedThisFrame { get; } + bool MouseRightHeld { get; } + /// + /// Scroll-wheel delta normalized so both backends feed approximately the same + /// magnitude into ZoomSensitivity (~0.1 per Windows notch). Returns 0 when not + /// scrolling and when the mouse device is unavailable. + /// + float MouseScrollY { get; } + } + +#if MAPBOX_NEW_INPUT_SYSTEM + internal sealed class PointerInput : IPointerInput + { + public void EnableTouchSupport() + { + if (!UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.enabled) + UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Enable(); + } + + public int TouchCount => NewTouch.activeTouches.Count; + public Vector2 GetTouchPosition(int index) => NewTouch.activeTouches[index].screenPosition; + public float GetTouchDeltaY(int index) => NewTouch.activeTouches[index].delta.y; + public int GetTouchId(int index) => NewTouch.activeTouches[index].touchId; + public int GetTouchPointerId(int index) => NewTouch.activeTouches[index].finger.index; + + public PointerTouchPhase GetTouchPhase(int index) + { + switch (NewTouch.activeTouches[index].phase) + { + case NewTouchPhase.Began: return PointerTouchPhase.Began; + case NewTouchPhase.Moved: return PointerTouchPhase.Moved; + case NewTouchPhase.Stationary: return PointerTouchPhase.Stationary; + case NewTouchPhase.Ended: return PointerTouchPhase.Ended; + case NewTouchPhase.Canceled: return PointerTouchPhase.Canceled; + default: return PointerTouchPhase.None; + } + } + + public Vector3 MousePosition => + Mouse.current != null ? (Vector3)Mouse.current.position.ReadValue() : Vector3.zero; + + public bool MouseLeftPressedThisFrame => + Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame; + public bool MouseLeftHeld => + Mouse.current != null && Mouse.current.leftButton.isPressed; + + public bool MouseRightPressedThisFrame => + Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame; + public bool MouseRightHeld => + Mouse.current != null && Mouse.current.rightButton.isPressed; + + // Raw scroll.y is ~120 per notch on Windows; legacy Input.GetAxis returns + // ~0.1 per notch. /1200f normalizes to the legacy scale so both backends feed + // the same magnitude into ZoomSensitivity. + public float MouseScrollY => + Mouse.current != null ? Mouse.current.scroll.ReadValue().y / 1200f : 0f; + } +#else + internal sealed class PointerInput : IPointerInput + { + public void EnableTouchSupport() { /* legacy touch is auto-enabled */ } + + public int TouchCount => Input.touchCount; + public Vector2 GetTouchPosition(int index) => Input.GetTouch(index).position; + public float GetTouchDeltaY(int index) => Input.GetTouch(index).deltaPosition.y; + public int GetTouchId(int index) => Input.GetTouch(index).fingerId; + public int GetTouchPointerId(int index) => Input.GetTouch(index).fingerId; + + public PointerTouchPhase GetTouchPhase(int index) + { + switch (Input.GetTouch(index).phase) + { + case TouchPhase.Began: return PointerTouchPhase.Began; + case TouchPhase.Moved: return PointerTouchPhase.Moved; + case TouchPhase.Stationary: return PointerTouchPhase.Stationary; + case TouchPhase.Ended: return PointerTouchPhase.Ended; + case TouchPhase.Canceled: return PointerTouchPhase.Canceled; + default: return PointerTouchPhase.None; + } + } + + public Vector3 MousePosition => Input.mousePosition; + public bool MouseLeftPressedThisFrame => Input.GetMouseButtonDown(0); + public bool MouseLeftHeld => Input.GetMouseButton(0); + public bool MouseRightPressedThisFrame => Input.GetMouseButtonDown(1); + public bool MouseRightHeld => Input.GetMouseButton(1); + + // Input.GetAxis("Mouse ScrollWheel") returns 0 when not scrolling, so we + // don't need the historic Mathf.Abs(mouseScrollDelta) > 0 short-circuit. + public float MouseScrollY => Input.GetAxis("Mouse ScrollWheel"); + } +#endif +} diff --git a/Runtime/Mapbox/Example/Scripts/PointerInput.cs.meta b/Runtime/Mapbox/Example/Scripts/PointerInput.cs.meta new file mode 100644 index 000000000..d2d238eed --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/PointerInput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6133b58d9727428fbbc3e26c59a19b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 74e7a14c18d8c0cb70b773a94661b50854e694cd Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 18:24:47 +0300 Subject: [PATCH 31/54] Split MapInput into per-platform IInputHandler (mouse / touch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mouse-vs-touch input logic is now in two separate classes, selected at compile time by build target. There's no runtime detection — each platform gets a single-purpose input path. InputHandlers.cs (new): - IInputHandler interface covering the full input vocabulary (pointer down/held, secondary, pinch zoom, two-finger tilt, zoom center, is-over-UI, touch count, decreased-this-frame). - MouseInputHandler: LMB / RMB / scroll wheel. Touch-only concepts (touch count, pinch, tilt, touch-count-decreased) are no-ops. - TouchInputHandler: single-finger pan, two-finger pinch / tilt mutex, touch-pair ID tracking for 2→3→2 transitions. Mouse-only concepts (secondary button, scroll) are no-ops. MapInput.cs: - 357 → 157 lines. Drops the TwoFingerGesture enum, pinch/tilt state, touch-pair tracking, UpdateInputState body, and the per-helper implementations. All replaced by thin delegates to _handler. - One #if selects the handler: (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR → TouchInputHandler else → MouseInputHandler - No behaviour change for either platform: each handler implements the same helpers the cameras call, with the modes' respective logic. Camera files untouched — same helper vocabulary as before. PointerInput.cs (backend abstraction) survives unchanged and is shared by both handlers. Memory: project_camera_gesture_recognizer.md documents Option B (recognizer layer) as deferred future work — to revisit when adding game controller / VR input or when touch-rotate becomes a requirement. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Mapbox/Example/Scripts/InputHandlers.cs | 247 ++++++++++++++++ .../Example/Scripts/InputHandlers.cs.meta | 11 + Runtime/Mapbox/Example/Scripts/MapInput.cs | 267 +++--------------- 3 files changed, 292 insertions(+), 233 deletions(-) create mode 100644 Runtime/Mapbox/Example/Scripts/InputHandlers.cs create mode 100644 Runtime/Mapbox/Example/Scripts/InputHandlers.cs.meta diff --git a/Runtime/Mapbox/Example/Scripts/InputHandlers.cs b/Runtime/Mapbox/Example/Scripts/InputHandlers.cs new file mode 100644 index 000000000..92af1af18 --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/InputHandlers.cs @@ -0,0 +1,247 @@ +using UnityEngine; +using UnityEngine.EventSystems; + +namespace Mapbox.Example.Scripts.MapInput +{ + /// + /// Mode-specific input handler. One implementation covers keyboard/mouse and + /// another covers touch; picks one at compile time based + /// on the build target. Each implementation covers the full vocabulary — methods + /// that don't make sense for the mode (e.g. secondary button on touch, two-finger + /// tilt on mouse) just return false / zero. + /// + internal interface IInputHandler + { + void Initialize(); + void UpdateState(); + + bool IsPointerOverUI(); + Vector3 GetPointerPosition(); + bool GetPointerDown(); + bool GetPointerHeld(); + bool GetSecondaryDown(); + bool GetSecondaryHeld(); + bool TryGetPinchZoomDelta(out float zoomDelta, float pinchSensitivity); + bool TryGetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity); + Vector3 GetZoomCenter(); + bool TouchCountDecreasedThisFrame { get; } + int GetTouchCount(); + } + + /// + /// Keyboard / mouse handler. Used on Editor and desktop standalone builds. + /// Touch-only concepts (touch count, pinch, tilt, etc.) are no-ops. + /// + internal sealed class MouseInputHandler : IInputHandler + { + private readonly IPointerInput _input; + + public MouseInputHandler(IPointerInput input) { _input = input; } + + public void Initialize() { /* legacy mouse needs no setup */ } + public void UpdateState() { /* mouse has no per-frame gesture mutex */ } + + public bool TouchCountDecreasedThisFrame => false; + public int GetTouchCount() => 0; + + public bool IsPointerOverUI() + { + if (EventSystem.current == null) return false; + return EventSystem.current.IsPointerOverGameObject(); + } + + public Vector3 GetPointerPosition() => _input.MousePosition; + public bool GetPointerDown() => _input.MouseLeftPressedThisFrame; + public bool GetPointerHeld() => _input.MouseLeftHeld; + public bool GetSecondaryDown() => _input.MouseRightPressedThisFrame; + public bool GetSecondaryHeld() => _input.MouseRightHeld; + + public bool TryGetPinchZoomDelta(out float zoomDelta, float pinchSensitivity) + { + // Scroll wheel; pinchSensitivity is unused on mouse — the wheel already + // emits per-notch deltas via IPointerInput.MouseScrollY. + var scrollY = _input.MouseScrollY; + if (Mathf.Abs(scrollY) > 0f) + { + zoomDelta = scrollY; + return true; + } + zoomDelta = 0f; + return false; + } + + public bool TryGetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity) + { + tiltDelta = 0f; + return false; + } + + public Vector3 GetZoomCenter() => _input.MousePosition; + } + + /// + /// Touch handler. Used on Android / iOS device builds. Mouse-only concepts + /// (secondary button, scroll wheel) are no-ops; pinch + tilt are recognised + /// via a per-frame two-finger gesture mutex run in . + /// + internal sealed class TouchInputHandler : IInputHandler + { + private readonly IPointerInput _input; + + // Per-frame two-finger gesture decision. Pinch vs tilt is exclusive; the + // winner is selected once per frame in UpdateState so the two query helpers + // (TryGetPinchZoomDelta / TryGetTwoFingerTiltDelta) read consistent state. + private enum Gesture { None, Pinch, Tilt } + private Gesture _gesture; + private float _pinchDelta; + private float _tiltAvgDeltaY; + + // Pinch state tracking. _previousTouch{0,1}Id let us detect 2→3→2 finger + // transitions where EnhancedTouch shifted which physical touches occupy + // slots [0..1] — without this, the next frame compares distance against + // a different pair and spikes a phantom zoom. + private float _previousPinchDistance; + private bool _pinchActive; + private int _previousTouchCount; + private int _previousTouch0Id = int.MinValue; + private int _previousTouch1Id = int.MinValue; + + public bool TouchCountDecreasedThisFrame { get; private set; } + + public TouchInputHandler(IPointerInput input) { _input = input; } + + public void Initialize() { _input.EnableTouchSupport(); } + public int GetTouchCount() => _input.TouchCount; + + public bool IsPointerOverUI() + { + if (EventSystem.current == null) return false; + if (_input.TouchCount > 0) + return EventSystem.current.IsPointerOverGameObject(_input.GetTouchPointerId(0)); + return EventSystem.current.IsPointerOverGameObject(); + } + + public Vector3 GetPointerPosition() + { + return _input.TouchCount > 0 ? (Vector3)_input.GetTouchPosition(0) : Vector3.zero; + } + + public bool GetPointerDown() + { + if (_input.TouchCount <= 0) return false; + return _input.GetTouchPhase(0) == PointerTouchPhase.Began; + } + + public bool GetPointerHeld() + { + if (_input.TouchCount != 1) return false; + var phase = _input.GetTouchPhase(0); + return phase == PointerTouchPhase.Moved || phase == PointerTouchPhase.Stationary; + } + + // Touch has no secondary pointer; the camera's rotate-via-secondary code + // (SlippyMapCamera right-click rotation, Moving3dCamera right-click pitch+yaw) + // stays dormant on this handler. + public bool GetSecondaryDown() => false; + public bool GetSecondaryHeld() => false; + + public bool TryGetPinchZoomDelta(out float zoomDelta, float pinchSensitivity) + { + if (_gesture == Gesture.Pinch) + { + zoomDelta = _pinchDelta / Mathf.Max(Screen.height, 1) * pinchSensitivity; + return true; + } + zoomDelta = 0f; + return false; + } + + public bool TryGetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity) + { + if (_gesture == Gesture.Tilt) + { + tiltDelta = _tiltAvgDeltaY / Mathf.Max(Screen.height, 1) * tiltSensitivity; + return true; + } + tiltDelta = 0f; + return false; + } + + public Vector3 GetZoomCenter() + { + if (_input.TouchCount >= 2) + { + Vector3 t0 = _input.GetTouchPosition(0); + Vector3 t1 = _input.GetTouchPosition(1); + return (t0 + t1) / 2f; + } + return Vector3.zero; + } + + public void UpdateState() + { + var touchCount = _input.TouchCount; + TouchCountDecreasedThisFrame = touchCount < _previousTouchCount; + _previousTouchCount = touchCount; + + _gesture = Gesture.None; + _pinchDelta = 0f; + _tiltAvgDeltaY = 0f; + + if (touchCount < 2) + { + _pinchActive = false; + _previousTouch0Id = int.MinValue; + _previousTouch1Id = int.MinValue; + return; + } + + var touch0Pos = _input.GetTouchPosition(0); + var touch1Pos = _input.GetTouchPosition(1); + var touch0DeltaY = _input.GetTouchDeltaY(0); + var touch1DeltaY = _input.GetTouchDeltaY(1); + var t0Id = _input.GetTouchId(0); + var t1Id = _input.GetTouchId(1); + var currentDistance = Vector2.Distance(touch0Pos, touch1Pos); + + bool pairChanged = !_pinchActive || + t0Id != _previousTouch0Id || t1Id != _previousTouch1Id; + if (pairChanged) + { + _pinchActive = true; + _previousPinchDistance = currentDistance; + _previousTouch0Id = t0Id; + _previousTouch1Id = t1Id; + return; + } + + var pinchDelta = currentDistance - _previousPinchDistance; + _previousPinchDistance = currentDistance; + + var pinchMag = Mathf.Abs(pinchDelta); + var avgDeltaY = (touch0DeltaY + touch1DeltaY) / 2f; + var tiltMag = Mathf.Abs(avgDeltaY); + var bothFingersSameDirection = touch0DeltaY * touch1DeltaY > 0f; + + // Normalize both magnitudes to Screen.height so the thresholds are DPI- + // independent. MinFrac ≈ 1 pixel on a 1080-height screen. Pinch wins + // ties (pinchFrac >= tiltFrac) to avoid "None" frames when magnitudes + // are exactly equal (common on near-pure vertical drags). + var screenH = Mathf.Max(Screen.height, 1); + var pinchFrac = pinchMag / screenH; + var tiltFrac = tiltMag / screenH; + const float MinFrac = 1f / 1080f; + + if (pinchFrac > MinFrac && pinchFrac >= tiltFrac) + { + _gesture = Gesture.Pinch; + _pinchDelta = pinchDelta; + } + else if (bothFingersSameDirection && tiltFrac > MinFrac) + { + _gesture = Gesture.Tilt; + _tiltAvgDeltaY = avgDeltaY; + } + } + } +} diff --git a/Runtime/Mapbox/Example/Scripts/InputHandlers.cs.meta b/Runtime/Mapbox/Example/Scripts/InputHandlers.cs.meta new file mode 100644 index 000000000..3ccf3bb0c --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/InputHandlers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f2e420aa19dd41bbb0d0e579bc3d350 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index d85c4dadf..c0324092d 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -2,7 +2,6 @@ using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Utilities; using UnityEngine; -using UnityEngine.EventSystems; namespace Mapbox.Example.Scripts.MapInput { @@ -36,46 +35,20 @@ public abstract class MapInput protected Plane _controlPlane = new Plane(Vector3.up, Vector3.zero); protected readonly CameraOutput _output = new CameraOutput(); - // Pinch state tracking - private float _previousPinchDistance; - private bool _pinchActive; - private int _previousTouchCount; - // Track the (touchId, touchId) of the pair we're measuring. EnhancedTouch. - // activeTouches reorders on add/remove, so 2→3→2 transitions can leave us - // comparing distance against a different physical pair. Reseed on change. - private int _previousTouch0Id = int.MinValue; - private int _previousTouch1Id = int.MinValue; - - // True on the frame the active touch count drops (e.g. 2→1 when one finger - // lifts during a pinch). Cameras should treat this as a fresh drag start so - // _dragOrigin is reset to the surviving finger's position instead of the old - // touches[0] — without this, single-finger pan jumps after a pinch ends. - protected bool TouchCountDecreasedThisFrame { get; private set; } - - public enum TwoFingerGesture - { - None, - Pinch, - Tilt - } - - // Per-frame two-finger gesture decision. Computed once in UpdateInputState so - // pinch and tilt detectors read consistent state and exactly one of them can - // fire per frame — without this, both helpers update _previousPinchDistance - // independently and tilt's pinch-comparison sees stale (post-update) state. - protected TwoFingerGesture CurrentTwoFingerGesture { get; private set; } - private float _pinchDeltaThisFrame; - private float _tiltAvgDeltaYThisFrame; - - // Backend-agnostic input source. PointerInput's two implementations are gated - // by MAPBOX_NEW_INPUT_SYSTEM in PointerInput.cs; the rest of this file is - // free of #if branching as a result. - private readonly IPointerInput _input = new PointerInput(); + // Per-platform handler. Editor and standalone builds use the mouse handler; + // Android/iOS device builds use the touch handler. The split is compile-time + // (no runtime detection) — each platform gets a single-purpose input path + // instead of a unified shim that has to cover both worlds at once. +#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR + private readonly IInputHandler _handler = new TouchInputHandler(new PointerInput()); +#else + private readonly IInputHandler _handler = new MouseInputHandler(new PointerInput()); +#endif public virtual void Initialize(Camera camera, IMapInformation mapInfo) { _camera = camera ? camera : Camera.main; - _input.EnableTouchSupport(); + _handler.Initialize(); } /// @@ -135,221 +108,49 @@ protected static float ClampBearing(float bearing) #region Input Helpers - /// - /// Returns the number of active touches, or 0 if no touch device is available. - /// - private int GetTouchCount() => _input.TouchCount; + // All input reads delegate to _handler. Touch-only state (pinch / tilt + // mutex, touch-pair tracking) lives entirely inside TouchInputHandler; + // mouse-only state (RMB held) lives entirely inside MouseInputHandler. /// /// Call at the start of UpdateCamera to maintain input state between frames. - /// Resets pinch tracking when fingers are lifted. + /// On touch: resets pinch tracking, runs the per-frame pinch/tilt decision. + /// On mouse: no-op. /// - protected void UpdateInputState() - { - var touchCount = GetTouchCount(); - TouchCountDecreasedThisFrame = touchCount < _previousTouchCount; - _previousTouchCount = touchCount; - - // Reset gesture state every frame; only re-arm if a valid two-finger - // pinch/tilt gesture is currently detected. - CurrentTwoFingerGesture = TwoFingerGesture.None; - _pinchDeltaThisFrame = 0f; - _tiltAvgDeltaYThisFrame = 0f; - - if (touchCount < 2) - { - _pinchActive = false; - _previousTouch0Id = int.MinValue; - _previousTouch1Id = int.MinValue; - return; - } - - // Read both touches' positions, Y-deltas, and stable ids. Doing this once - // here (not redundantly inside each detector) keeps the decision symmetric - // and prevents stale-state races. - var touch0Pos = _input.GetTouchPosition(0); - var touch1Pos = _input.GetTouchPosition(1); - var touch0DeltaY = _input.GetTouchDeltaY(0); - var touch1DeltaY = _input.GetTouchDeltaY(1); - var t0Id = _input.GetTouchId(0); - var t1Id = _input.GetTouchId(1); - var currentDistance = Vector2.Distance(touch0Pos, touch1Pos); - - // Reseed when the pair changes (first two-finger frame OR a 2→3→2 / finger-swap - // transition that shifted which physical touches occupy slots [0..1]). Comparing - // distance against a different pair would spike a frame of phantom pinch. - bool pairChanged = !_pinchActive || - t0Id != _previousTouch0Id || t1Id != _previousTouch1Id; - if (pairChanged) - { - _pinchActive = true; - _previousPinchDistance = currentDistance; - _previousTouch0Id = t0Id; - _previousTouch1Id = t1Id; - return; - } - - var pinchDelta = currentDistance - _previousPinchDistance; - _previousPinchDistance = currentDistance; - - var pinchMag = Mathf.Abs(pinchDelta); - var avgDeltaY = (touch0DeltaY + touch1DeltaY) / 2f; - var tiltMag = Mathf.Abs(avgDeltaY); - var bothFingersSameDirection = touch0DeltaY * touch1DeltaY > 0f; - - // Normalize both magnitudes to Screen.height so the thresholds are DPI-independent. - // MinFrac ≈ 1 pixel on a 1080-height screen. Pinch wins ties (pinchFrac >= tiltFrac) - // to avoid the "None" frame the prior strict-> on both sides produced when the - // gestures registered identical magnitudes (common on near-pure vertical drags). - var screenH = Mathf.Max(Screen.height, 1); - var pinchFrac = pinchMag / screenH; - var tiltFrac = tiltMag / screenH; - const float MinFrac = 1f / 1080f; - - if (pinchFrac > MinFrac && pinchFrac >= tiltFrac) - { - CurrentTwoFingerGesture = TwoFingerGesture.Pinch; - _pinchDeltaThisFrame = pinchDelta; - } - else if (bothFingersSameDirection && tiltFrac > MinFrac) - { - CurrentTwoFingerGesture = TwoFingerGesture.Tilt; - _tiltAvgDeltaYThisFrame = avgDeltaY; - } - } + protected void UpdateInputState() => _handler.UpdateState(); /// - /// Returns true if the pointer (mouse or primary touch) is over a UI element. + /// True on the frame the active touch count drops (e.g. 2→1 when one finger + /// lifts during a pinch). Cameras should treat this as a fresh drag start so + /// _dragOrigin is reset to the surviving finger's position. Always false on + /// the mouse handler. /// - protected bool IsPointerOverUI() - { - if (EventSystem.current == null) - return false; + protected bool TouchCountDecreasedThisFrame => _handler.TouchCountDecreasedThisFrame; - if (_input.TouchCount > 0) - return EventSystem.current.IsPointerOverGameObject(_input.GetTouchPointerId(0)); - return EventSystem.current.IsPointerOverGameObject(); - } + private int GetTouchCount() => _handler.GetTouchCount(); - /// - /// Screen position of the primary pointer (first touch or mouse cursor). - /// - protected Vector3 GetPointerPosition() - { - if (_input.TouchCount > 0) - return _input.GetTouchPosition(0); - return _input.MousePosition; - } + protected bool IsPointerOverUI() => _handler.IsPointerOverUI(); + protected Vector3 GetPointerPosition() => _handler.GetPointerPosition(); + protected bool GetPointerDown() => _handler.GetPointerDown(); + protected bool GetPointerHeld() => _handler.GetPointerHeld(); + protected bool GetSecondaryDown() => _handler.GetSecondaryDown(); + protected bool GetSecondaryHeld() => _handler.GetSecondaryHeld(); /// - /// True on the frame the primary pointer goes down (touch began or LMB pressed). - /// - protected bool GetPointerDown() - { - if (_input.TouchCount > 0) - return _input.GetTouchPhase(0) == PointerTouchPhase.Began; - return _input.MouseLeftPressedThisFrame; - } - - /// - /// True while the primary pointer is held (single touch active or LMB held). - /// Returns false when two or more touches are active (pinch takes priority). - /// - protected bool GetPointerHeld() - { - var touchCount = _input.TouchCount; - if (touchCount == 1) - { - var phase = _input.GetTouchPhase(0); - return phase == PointerTouchPhase.Moved || phase == PointerTouchPhase.Stationary; - } - if (touchCount == 0) - return _input.MouseLeftHeld; - return false; - } - - /// - /// True on the frame the secondary pointer goes down (RMB only, no touch equivalent). - /// - protected bool GetSecondaryDown() - { - if (_input.TouchCount > 0) - return false; - return _input.MouseRightPressedThisFrame; - } - - /// - /// True while the secondary pointer is held (RMB only, no touch equivalent). - /// - protected bool GetSecondaryHeld() - { - if (_input.TouchCount > 0) - return false; - return _input.MouseRightHeld; - } - - /// - /// Returns true if zoom input was detected this frame (pinch or mouse scroll). - /// Pinch/tilt mutual-exclusion is enforced in — - /// this helper just reads the cached decision and emits the scaled delta. + /// Returns true if zoom input was detected this frame. On touch: from the + /// pinch gesture cached by UpdateState. On mouse: from the scroll wheel. /// protected bool GetPinchZoomDelta(out float zoomDelta, float pinchSensitivity = 5f) - { - zoomDelta = 0f; - - if (CurrentTwoFingerGesture == TwoFingerGesture.Pinch) - { - // Match the divisor guard used in UpdateInputState: raw Screen.height - // can be 0 (e.g. minimized window) and would produce Inf here. - zoomDelta = _pinchDeltaThisFrame / Mathf.Max(Screen.height, 1) * pinchSensitivity; - return true; - } - - // Mouse scroll fallback only when no two-finger gesture is active. - if (_input.TouchCount >= 2) - { - return false; - } - - var scrollY = _input.MouseScrollY; - if (Mathf.Abs(scrollY) > 0f) - { - zoomDelta = scrollY; - return true; - } - return false; - } + => _handler.TryGetPinchZoomDelta(out zoomDelta, pinchSensitivity); /// /// Returns true if a two-finger vertical-tilt gesture was detected this frame. - /// Pinch/tilt mutual-exclusion is enforced in — - /// this helper just reads the cached decision and emits the scaled delta. + /// Always false on the mouse handler. /// protected bool GetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity = 0.5f) - { - if (CurrentTwoFingerGesture == TwoFingerGesture.Tilt) - { - tiltDelta = _tiltAvgDeltaYThisFrame / Mathf.Max(Screen.height, 1) * tiltSensitivity; - return true; - } - tiltDelta = 0f; - return false; - } + => _handler.TryGetTwoFingerTiltDelta(out tiltDelta, tiltSensitivity); - /// - /// Screen position to use as zoom center. - /// For pinch: midpoint between two fingers. For mouse: cursor position. - /// - protected Vector3 GetZoomCenter() - { - if (_input.TouchCount >= 2) - { - Vector3 t0 = _input.GetTouchPosition(0); - Vector3 t1 = _input.GetTouchPosition(1); - return (t0 + t1) / 2f; - } - return _input.MousePosition; - } + protected Vector3 GetZoomCenter() => _handler.GetZoomCenter(); #endregion } From ff8f6ebe304eb9b5deafeb8f301f5a00dc2badfb Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 19:01:47 +0300 Subject: [PATCH 32/54] Move terrain bounds tracking out of MapboxMapVisualizer MapboxMapVisualizer was carrying ~100 lines of terrain-specific bookkeeping (Min/MaxElevation contributor checks, async-decode watchers, dirty flag, recompute walk). It made the visualizer implicitly terrain-aware and bloated a file that should just be orchestrating tile lifecycle. Extracted into a TerrainBoundsTracker owned by TerrainLayerModule: - Subscribes to the visualizer's existing TileLoaded / TileUnloading events (no new public surface needed there). - Runs its own per-frame drain coroutine. - Handles the async-decode one-shot watcher with the same dispose-multicast self-detach pattern. Wired via a new opt-in marker interface ITileLifecycleListener in BaseModule. MapboxMapVisualizer.Initialize iterates LayerModules and calls AttachToVisualizer on each implementer before their own Initialize runs. Modules that don't need the visualizer reference simply don't implement the interface. MapboxMapVisualizer changes: - Drop `using TerrainData` alias (no longer referenced). - Expose `MapInformation` as a public property (was protected field only; needed so listeners can read TerrainInfo). - Drop _terrainBoundsDirty + drain in InternalUpdateCoroutine. - Drop _pendingElevationWatches + WatchForAsyncElevationDecode + RecomputeTerrainBounds + their OnDestroy detachment. - Drop the in-CreateTile / in-PoolTile contributor & async hooks. - Net: ~100 lines lighter, fully terrain-agnostic except for the existing existsTerrainModule check that picks FlatTerrainStrategy fallback (orchestration, not bookkeeping). TerrainLayerModule: - Implements ITileLifecycleListener. - AttachToVisualizer constructs the tracker. - OnDestroy disposes it. No behaviour change: same dirty-flag drain timing, same contributor heuristics, same default-fallback when ActiveTiles is empty. Just located where it belongs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BaseModule/Map/ITileLifecycleListener.cs | 18 ++ .../Map/ITileLifecycleListener.cs.meta | 11 + .../BaseModule/Map/MapboxMapVisualizer.cs | 167 ++----------- .../Terrain/TerrainBoundsTracker.cs | 227 ++++++++++++++++++ .../Terrain/TerrainBoundsTracker.cs.meta | 11 + .../ImageModule/Terrain/TerrainLayerModule.cs | 15 +- 6 files changed, 295 insertions(+), 154 deletions(-) create mode 100644 Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs create mode 100644 Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs.meta create mode 100644 Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs create mode 100644 Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs new file mode 100644 index 000000000..8a0d3ca16 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs @@ -0,0 +1,18 @@ +namespace Mapbox.BaseModule.Map +{ + /// + /// Opt-in extension for + /// implementations that need a reference to the owning visualizer (e.g. to subscribe + /// to TileLoaded / TileUnloading events for bookkeeping that depends on + /// tile lifecycle). + /// + /// calls + /// on every layer module that implements this interface, after the modules are added + /// but before their own Initialize runs. Modules that don't need the + /// visualizer reference simply don't implement this interface. + /// + public interface ITileLifecycleListener + { + void AttachToVisualizer(MapboxMapVisualizer visualizer); + } +} diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs.meta b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs.meta new file mode 100644 index 000000000..4a779949e --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 518417b35d822432ab552cc4e5d26cfb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 35a24df8b..30960f7f4 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -7,7 +7,6 @@ using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; using UnityEngine; -using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.BaseModule.Map { @@ -25,6 +24,9 @@ public class MapboxMapVisualizer : IMapVisualizer public List LayerModules; public Dictionary ActiveTiles { get; private set; } public List TempTiles { get; private set; } + // Exposed for ITileLifecycleListener implementations that need access to the + // shared map state (e.g. terrain bounds tracker reads/writes Terrain.Min/Max). + public IMapInformation MapInformation => _mapInformation; protected UnityContext _unityContext; protected IMapInformation _mapInformation; protected ITileCreator _tileCreator; @@ -92,6 +94,17 @@ public virtual IEnumerator Initialize() yield return _tileCreator.Initialize(terrainStrategy); } + // Give layer modules a chance to wire bookkeeping that needs the visualizer + // reference (terrain bounds tracker, etc) BEFORE their own Initialize runs — + // so any subscriptions are in place when the first tiles start loading. + foreach (var module in LayerModules) + { + if (module is ITileLifecycleListener listener) + { + listener.AttachToVisualizer(this); + } + } + yield return LayerModules.Select(x => x.Initialize()).WaitForAll(); } @@ -193,15 +206,6 @@ public virtual void Load(TileCover tileCover) /// public virtual void InternalUpdateCoroutine() { - // Drain the terrain-bounds dirty flag once per tick. Recompute is O(ActiveTiles) - // so doing it here (and not on every PoolTile / CreateTile / async-decode event) - // turns the per-event cost into a single O(N) pass per frame. - if (_terrainBoundsDirty) - { - RecomputeTerrainBounds(); - _terrainBoundsDirty = false; - } - //finish temp tiles from tempTiles list _toRemove.Clear(); for (var index = TempTiles.Count - 1; index >= 0; index--) @@ -293,18 +297,6 @@ public void OnDestroy() _internalUpdateCoroutine = null; } - // Detach any pending async-decode subscriptions so the closures don't keep - // shared TerrainData rooted past the visualizer's lifetime. - for (int i = 0; i < _pendingElevationWatches.Count; i++) - { - var (data, handler) = _pendingElevationWatches[i]; - if (data != null && handler != null) - { - data.ElevationValuesUpdated -= handler; - } - } - _pendingElevationWatches.Clear(); - foreach (var layerModule in LayerModules) { layerModule.OnDestroy(); @@ -474,36 +466,11 @@ protected void ShowTile(UnityMapTile unityTile) _mapInformation.PositionObjectFor(unityTile.gameObject, unityTile.CanonicalTileId); } - // Set whenever an event that could shift Terrain.Min/Max occurs (tile pooled - // while holding a boundary value, async decode arriving on an already-finished - // tile, etc). InternalUpdateCoroutine drains the flag once per tick by running - // RecomputeTerrainBounds — avoids the O(N²) cost of recursive PoolTile each - // calling Recompute itself. - private bool _terrainBoundsDirty; - protected void PoolTile(UnityMapTile tile) { if (tile.LoadingState == LoadingState.None) return; - // Snapshot whether this tile is currently holding either bounds value, OR - // is the last contributor in ActiveTiles (so the empty-fallback path needs - // to run). Recycle() clears TerrainData below, so we check first. - var terrain = _mapInformation.Terrain; - var terrainData = tile.TerrainContainer?.TerrainData; - bool contributedMax = terrainData != null && terrainData.IsElevationDataReady && - terrainData.MaxElevation != 0f && - Mathf.Approximately(terrainData.MaxElevation, terrain.MaxElevation); - bool contributedMin = terrainData != null && terrainData.IsElevationDataReady && - terrainData.MinElevation != 0f && - Mathf.Approximately(terrainData.MinElevation, terrain.MinElevation); - // Also dirty when this is the last tile in ActiveTiles: an all-flat session - // (every loaded tile had Min=Max=0) wouldn't trip the non-zero checks above, - // so the empty-fallback in RecomputeTerrainBounds (reset to TerrainInfo - // defaults) would never run when the map empties out. - bool wasLastActive = terrainData != null && ActiveTiles.Count == 1 && - ActiveTiles.ContainsKey(tile.UnwrappedTileId); - TileUnloading(tile); ActiveTiles.Remove(tile.UnwrappedTileId); tile.Recycle(); @@ -519,93 +486,6 @@ protected void PoolTile(UnityMapTile tile) tile.Children.Clear(); } - - if (contributedMax || contributedMin || wasLastActive) - { - _terrainBoundsDirty = true; - } - } - - // Tracks the one-shot ElevationValuesUpdated subscriptions we've attached for - // shader-mode tiles whose CPU decode hasn't arrived yet. Stored by (data, - // handler) so OnDestroy can detach if the data is disposed before the event fires. - private readonly List<(TerrainData data, Action handler)> _pendingElevationWatches = - new List<(TerrainData, Action)>(); - - private void WatchForAsyncElevationDecode(TerrainData data) - { - // De-dup: shared TerrainData is referenced by up to 16 render tiles. One - // watch per data is enough — the dirty flag triggers a single recompute. - for (int i = 0; i < _pendingElevationWatches.Count; i++) - { - if (ReferenceEquals(_pendingElevationWatches[i].data, data)) return; - } - Action handler = null; - Action onDispose = null; - handler = () => - { - data.ElevationValuesUpdated -= handler; - data.RemoveDisposeCallback(onDispose); - for (int i = _pendingElevationWatches.Count - 1; i >= 0; i--) - { - if (ReferenceEquals(_pendingElevationWatches[i].data, data) && - ReferenceEquals(_pendingElevationWatches[i].handler, handler)) - { - _pendingElevationWatches.RemoveAt(i); - break; - } - } - _terrainBoundsDirty = true; - }; - // Also detach on dispose: TerrainData.Dispose doesn't fire ElevationValuesUpdated, - // so without this hook the watch entry would stay in the list (rooting both - // data and the closure) until the visualizer's own OnDestroy. - onDispose = () => - { - data.ElevationValuesUpdated -= handler; - data.RemoveDisposeCallback(onDispose); - for (int i = _pendingElevationWatches.Count - 1; i >= 0; i--) - { - if (ReferenceEquals(_pendingElevationWatches[i].data, data) && - ReferenceEquals(_pendingElevationWatches[i].handler, handler)) - { - _pendingElevationWatches.RemoveAt(i); - break; - } - } - }; - _pendingElevationWatches.Add((data, handler)); - data.ElevationValuesUpdated += handler; - data.AddDisposeCallback(onDispose); - } - - private void RecomputeTerrainBounds() - { - var terrain = _mapInformation.Terrain; - float max = 0f; - float min = 0f; - bool any = false; - foreach (var kv in ActiveTiles) - { - var td = kv.Value.TerrainContainer?.TerrainData; - if (td == null || !td.IsElevationDataReady) continue; - if (!any) - { - max = td.MaxElevation; - min = td.MinElevation; - any = true; - } - else - { - if (td.MaxElevation > max) max = td.MaxElevation; - if (td.MinElevation < min) min = td.MinElevation; - } - } - // When no tile is contributing, fall back to TerrainInfo's conservative - // defaults rather than 0 — a flat AABB would frustum-cull plausible - // mountains on the next frame before any sample arrives. - terrain.MaxElevation = any ? max : TerrainInfo.DefaultMaxElevation; - terrain.MinElevation = any ? min : TerrainInfo.DefaultMinElevation; } protected void CreateTempTile(UnwrappedTileId tileId, out UnityMapTile tile) @@ -668,25 +548,6 @@ protected bool CreateTile(UnityMapTile unityMapTile) ActiveTiles.Add(unityMapTile.UnwrappedTileId, unityMapTile); } - var terrainData = unityMapTile.TerrainContainer?.TerrainData; - if (terrainData != null) - { - if (terrainData.IsElevationDataReady) - { - // Data already decoded — mark bounds dirty for the next coroutine - // tick. Deferring avoids the O(N²) cost of recomputing inside - // every CreateTile within a single Load. - _terrainBoundsDirty = true; - } - else - { - // Shader-mode tiles are "finished" once the texture is ready, but - // the CPU decode (and therefore Min/MaxElevation) arrives async - // later. Hook a one-shot widening so the bounds catch up. - WatchForAsyncElevationDecode(terrainData); - } - } - TileLoaded(unityMapTile); } diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs new file mode 100644 index 000000000..f6f172351 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using UnityEngine; +using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; + +namespace Mapbox.ImageModule.Terrain +{ + /// + /// Watches the visualizer's tile lifecycle events and keeps + /// Min/MaxElevation in sync with the + /// observed elevation range across loaded tiles. + /// + /// Owned by and constructed when the module + /// is attached to a visualizer. The visualizer itself doesn't know this exists. + /// + /// Three lifecycle hooks feed dirty-flag updates: + /// + /// TileLoaded: data may be ready (mark dirty) or shader-mode pending + /// CPU decode (subscribe a one-shot widening to ElevationValuesUpdated). + /// TileUnloading: if the unloading tile held a boundary value, or is + /// the last contributor, mark dirty so the next drain re-derives from scratch. + /// Per-frame drain: a coroutine recomputes from ActiveTiles when the + /// flag is set — one O(N) pass per frame regardless of event volume. + /// + /// + internal sealed class TerrainBoundsTracker : IDisposable + { + private readonly IMapInformation _mapInfo; + private readonly MapboxMapVisualizer _visualizer; + + // One-shot widening subscriptions for shader-mode tiles whose CPU decode + // hasn't arrived yet. Stored by (data, handler) so Dispose can detach the + // closures and so the dedup loop can avoid attaching twice for the same + // shared data tile. + private readonly List<(TerrainData data, Action handler)> _pendingWatches = + new List<(TerrainData, Action)>(); + + private bool _dirty; + private bool _disposed; + private Coroutine _drainCoroutine; + + public TerrainBoundsTracker(IMapInformation mapInfo, MapboxMapVisualizer visualizer) + { + _mapInfo = mapInfo; + _visualizer = visualizer; + _visualizer.TileLoaded += OnTileLoaded; + _visualizer.TileUnloading += OnTileUnloading; + if (Runnable.Instance != null) + { + _drainCoroutine = Runnable.Instance.StartCoroutine(DrainLoop()); + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (_visualizer != null) + { + _visualizer.TileLoaded -= OnTileLoaded; + _visualizer.TileUnloading -= OnTileUnloading; + } + + // Detach pending watches. Both closures dropped — the dispose callback + // keeps a Dictionary reference alive otherwise. + for (int i = 0; i < _pendingWatches.Count; i++) + { + var (data, handler) = _pendingWatches[i]; + if (data != null && handler != null) + { + data.ElevationValuesUpdated -= handler; + } + } + _pendingWatches.Clear(); + + if (_drainCoroutine != null && Runnable.Instance != null) + { + Runnable.Instance.StopCoroutine(_drainCoroutine); + _drainCoroutine = null; + } + } + + private IEnumerator DrainLoop() + { + while (!_disposed) + { + if (_dirty) + { + Recompute(); + _dirty = false; + } + yield return null; + } + } + + private void OnTileLoaded(UnityMapTile tile) + { + if (_disposed) return; + var terrainData = tile.TerrainContainer?.TerrainData; + if (terrainData == null) return; + + if (terrainData.IsElevationDataReady) + { + // Data already decoded — mark dirty for the next drain tick. + _dirty = true; + } + else + { + // Shader-mode tiles finish on texture-ready, before the CPU decode arrives. + // Hook a one-shot watch so the bounds widen when the decode lands. + WatchForAsyncDecode(terrainData); + } + } + + private void OnTileUnloading(UnityMapTile tile) + { + if (_disposed) return; + + // Snapshot the pooled tile's contribution to the current bounds. Tile.Recycle() + // (called by PoolTile after this event fires) clears TerrainData, so we have to + // check up front. Skip when both endpoints are 0 — the tile is flat and didn't + // actually contribute, so a recompute would just produce the same values. + var terrain = _mapInfo.Terrain; + var terrainData = tile.TerrainContainer?.TerrainData; + bool contributedMax = terrainData != null && terrainData.IsElevationDataReady && + terrainData.MaxElevation != 0f && + Mathf.Approximately(terrainData.MaxElevation, terrain.MaxElevation); + bool contributedMin = terrainData != null && terrainData.IsElevationDataReady && + terrainData.MinElevation != 0f && + Mathf.Approximately(terrainData.MinElevation, terrain.MinElevation); + + // Also dirty when this is the last tile being pooled — an all-flat session + // (every loaded tile had Min=Max=0) wouldn't trip the non-zero checks above, + // so the empty-fallback (reset to TerrainInfo defaults) would never run + // when the map empties out. ActiveTiles.Count == 1 here because TileUnloading + // fires before the visualizer's ActiveTiles.Remove. + bool wasLastActive = terrainData != null && _visualizer.ActiveTiles.Count == 1 && + _visualizer.ActiveTiles.ContainsKey(tile.UnwrappedTileId); + + if (contributedMax || contributedMin || wasLastActive) + { + _dirty = true; + } + } + + private void WatchForAsyncDecode(TerrainData data) + { + // De-dup: shared TerrainData is referenced by up to 16 render tiles (data zoom + // = render zoom − 2). One watch per data is enough — the dirty flag fires once. + for (int i = 0; i < _pendingWatches.Count; i++) + { + if (ReferenceEquals(_pendingWatches[i].data, data)) return; + } + + Action handler = null; + Action onDispose = null; + handler = () => + { + data.ElevationValuesUpdated -= handler; + data.RemoveDisposeCallback(onDispose); + RemoveWatchEntry(data, handler); + if (!_disposed) _dirty = true; + }; + // TerrainData.Dispose doesn't fire ElevationValuesUpdated, so without this hook + // the watch entry would leak (rooting both data and the closure) until our own + // Dispose. Eviction during the decode window is the trigger. + onDispose = () => + { + data.ElevationValuesUpdated -= handler; + data.RemoveDisposeCallback(onDispose); + RemoveWatchEntry(data, handler); + }; + + _pendingWatches.Add((data, handler)); + data.ElevationValuesUpdated += handler; + data.AddDisposeCallback(onDispose); + } + + private void RemoveWatchEntry(TerrainData data, Action handler) + { + for (int i = _pendingWatches.Count - 1; i >= 0; i--) + { + if (ReferenceEquals(_pendingWatches[i].data, data) && + ReferenceEquals(_pendingWatches[i].handler, handler)) + { + _pendingWatches.RemoveAt(i); + break; + } + } + } + + private void Recompute() + { + var terrain = _mapInfo.Terrain; + float max = 0f; + float min = 0f; + bool any = false; + foreach (var kv in _visualizer.ActiveTiles) + { + var td = kv.Value.TerrainContainer?.TerrainData; + if (td == null || !td.IsElevationDataReady) continue; + if (!any) + { + max = td.MaxElevation; + min = td.MinElevation; + any = true; + } + else + { + if (td.MaxElevation > max) max = td.MaxElevation; + if (td.MinElevation < min) min = td.MinElevation; + } + } + // When no tile is contributing, fall back to TerrainInfo's conservative defaults + // rather than 0 — a flat AABB would frustum-cull plausible mountains on the next + // frame before any sample arrives. + terrain.MaxElevation = any ? max : TerrainInfo.DefaultMaxElevation; + terrain.MinElevation = any ? min : TerrainInfo.DefaultMinElevation; + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs.meta b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs.meta new file mode 100644 index 000000000..af75e7f86 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf4c61eb471041da93a691fb5473f526 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index 610edb597..fe6754ee9 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -22,18 +22,29 @@ namespace Mapbox.ImageModule.Terrain /// settings, and hands each tile through a that produces /// render + optional collider meshes. /// - public class TerrainLayerModule : ITerrainLayerModule + public class TerrainLayerModule : ITerrainLayerModule, ITileLifecycleListener { private TerrainLayerModuleSettings _settings; private Source _rasterSource; private HashSet _retainedTerrainTiles; private TerrainStrategy _terrainStrategy; + // Owns the Min/MaxElevation bookkeeping that used to live in MapboxMapVisualizer. + // Constructed in AttachToVisualizer once the visualizer is available, disposed in + // OnDestroy. The visualizer never references this directly. + private TerrainBoundsTracker _boundsTracker; // One-shot guard so QueryElevation doesn't spam the console on every call when the // user genuinely disabled CPU extraction. private bool _elevationDisabledWarningLogged; // Reused across GetDataId(IEnumerable) calls to avoid allocating a fresh HashSet // plus iterator chain from Where/Select/Distinct on every tile-cover update. private readonly HashSet _dataIdScratch = new HashSet(); + + public void AttachToVisualizer(MapboxMapVisualizer visualizer) + { + // Visualizer is now available — start tracking terrain bounds from tile events. + // Disposed in OnDestroy. + _boundsTracker = new TerrainBoundsTracker(visualizer.MapInformation, visualizer); + } public TerrainLayerModule(Source source, TerrainLayerModuleSettings settings) : base() { @@ -136,6 +147,8 @@ public void UpdatePositioning(IMapInformation mapInfo) public void OnDestroy() { + _boundsTracker?.Dispose(); + _boundsTracker = null; _rasterSource.OnDestroy(); _terrainStrategy?.OnDestroy(); } From e414de51e0e3a6cfa1859daf6e61b21d8ef5b53c Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 19:06:40 +0300 Subject: [PATCH 33/54] =?UTF-8?q?Rename=20ITileLifecycleListener.AttachToV?= =?UTF-8?q?isualizer=20=E2=86=92=20AttachToMapVisualizer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disambiguates from the layer-visualizer concept elsewhere in the project. The interface only ever sees MapboxMapVisualizer, but the shorter name was ambiguous. Co-Authored-By: Claude Opus 4.7 (1M context) --- Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs | 4 ++-- Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs | 2 +- Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs index 8a0d3ca16..9cb273820 100644 --- a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs @@ -6,13 +6,13 @@ namespace Mapbox.BaseModule.Map /// to TileLoaded / TileUnloading events for bookkeeping that depends on /// tile lifecycle). /// - /// calls + /// calls /// on every layer module that implements this interface, after the modules are added /// but before their own Initialize runs. Modules that don't need the /// visualizer reference simply don't implement this interface. /// public interface ITileLifecycleListener { - void AttachToVisualizer(MapboxMapVisualizer visualizer); + void AttachToMapVisualizer(MapboxMapVisualizer visualizer); } } diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 30960f7f4..9dc02f611 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -101,7 +101,7 @@ public virtual IEnumerator Initialize() { if (module is ITileLifecycleListener listener) { - listener.AttachToVisualizer(this); + listener.AttachToMapVisualizer(this); } } diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index fe6754ee9..2092fbb41 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -29,8 +29,8 @@ public class TerrainLayerModule : ITerrainLayerModule, ITileLifecycleListener private HashSet _retainedTerrainTiles; private TerrainStrategy _terrainStrategy; // Owns the Min/MaxElevation bookkeeping that used to live in MapboxMapVisualizer. - // Constructed in AttachToVisualizer once the visualizer is available, disposed in - // OnDestroy. The visualizer never references this directly. + // Constructed in AttachToMapVisualizer once the visualizer is available, disposed + // in OnDestroy. The visualizer never references this directly. private TerrainBoundsTracker _boundsTracker; // One-shot guard so QueryElevation doesn't spam the console on every call when the // user genuinely disabled CPU extraction. @@ -39,7 +39,7 @@ public class TerrainLayerModule : ITerrainLayerModule, ITileLifecycleListener // plus iterator chain from Where/Select/Distinct on every tile-cover update. private readonly HashSet _dataIdScratch = new HashSet(); - public void AttachToVisualizer(MapboxMapVisualizer visualizer) + public void AttachToMapVisualizer(MapboxMapVisualizer visualizer) { // Visualizer is now available — start tracking terrain bounds from tile events. // Disposed in OnDestroy. From 3cf4ecc252924ab19d3a21c3906ed2925a6d0bde Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 19:17:10 +0300 Subject: [PATCH 34/54] =?UTF-8?q?Rename=20Listener=E2=86=92Observer,=20ext?= =?UTF-8?q?ract=20ITileLifecycleSource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cleanups to the tile-lifecycle observer plumbing: 1. ITileLifecycleListener → ITileLifecycleObserver. The pattern is classic Observer (Gang of Four) and the new name reads that way. File renamed via git mv so the .meta GUID is preserved (Unity references intact). 2. New ITileLifecycleSource interface. Carries IMapInformation, the TileLoaded/TileUnloading events, and ActiveTiles as IReadOnlyDictionary. MapboxMapVisualizer implements it (uses explicit interface impl for ActiveTiles so internal code keeps the mutable Dictionary it needs for Add/Remove). TerrainBoundsTracker now depends on this narrow interface instead of the concrete MapboxMapVisualizer. The tracker only ever needed tile events + map state, not orchestration internals — making that explicit in the type makes the dependency obvious and the tracker testable in isolation. ITileLifecycleObserver.AttachToMapVisualizer also took the interface as its parameter type. Typical caller is still MapboxMapVisualizer (which the method name reflects), but the contract accepts any source. No behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BaseModule/Map/ITileLifecycleListener.cs | 18 --------- .../BaseModule/Map/ITileLifecycleObserver.cs | 18 +++++++++ ...cs.meta => ITileLifecycleObserver.cs.meta} | 0 .../BaseModule/Map/ITileLifecycleSource.cs | 37 +++++++++++++++++++ .../Map/ITileLifecycleSource.cs.meta | 11 ++++++ .../BaseModule/Map/MapboxMapVisualizer.cs | 20 ++++++---- .../Terrain/TerrainBoundsTracker.cs | 29 +++++++-------- .../ImageModule/Terrain/TerrainLayerModule.cs | 8 ++-- 8 files changed, 96 insertions(+), 45 deletions(-) delete mode 100644 Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs create mode 100644 Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs rename Runtime/Mapbox/BaseModule/Map/{ITileLifecycleListener.cs.meta => ITileLifecycleObserver.cs.meta} (100%) create mode 100644 Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs create mode 100644 Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs deleted file mode 100644 index 9cb273820..000000000 --- a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Mapbox.BaseModule.Map -{ - /// - /// Opt-in extension for - /// implementations that need a reference to the owning visualizer (e.g. to subscribe - /// to TileLoaded / TileUnloading events for bookkeeping that depends on - /// tile lifecycle). - /// - /// calls - /// on every layer module that implements this interface, after the modules are added - /// but before their own Initialize runs. Modules that don't need the - /// visualizer reference simply don't implement this interface. - /// - public interface ITileLifecycleListener - { - void AttachToMapVisualizer(MapboxMapVisualizer visualizer); - } -} diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs new file mode 100644 index 000000000..7f581d52e --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs @@ -0,0 +1,18 @@ +namespace Mapbox.BaseModule.Map +{ + /// + /// Opt-in extension for + /// implementations that want to observe tile lifecycle events (load / unload) to + /// run their own bookkeeping. The observer side of the pattern; the subject side + /// is . + /// + /// calls + /// on every layer module that implements this interface, after the modules are + /// added but before their own Initialize runs. Modules that don't need to + /// observe tile lifecycle simply don't implement this interface. + /// + public interface ITileLifecycleObserver + { + void AttachToMapVisualizer(ITileLifecycleSource source); + } +} diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs.meta b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs.meta similarity index 100% rename from Runtime/Mapbox/BaseModule/Map/ITileLifecycleListener.cs.meta rename to Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs new file mode 100644 index 000000000..3e403e09c --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Unity; + +namespace Mapbox.BaseModule.Map +{ + /// + /// Read-only "subject" surface of the tile-lifecycle observer pattern. Implemented + /// by ; consumed by + /// implementations (and helpers they own, e.g. terrain bounds tracking). + /// + /// Carrying only what an observer needs lets helpers depend on this narrow contract + /// instead of the full , which simplifies testing + /// and keeps modules from reaching into orchestration internals. + /// + public interface ITileLifecycleSource + { + IMapInformation MapInformation { get; } + + /// + /// Map tile finished loading with targeted detail level data. The tile is in + /// ActiveTiles by the time this fires. + /// + event Action TileLoaded; + + /// + /// Map tile is about to be pooled. Still in ActiveTiles when the event + /// fires; the visualizer removes it and calls Recycle immediately after. + /// Observers that need to read per-tile state (TerrainData, etc.) before it's + /// cleared should do so during this callback. + /// + event Action TileUnloading; + + IReadOnlyDictionary ActiveTiles { get; } + } +} diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs.meta b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs.meta new file mode 100644 index 000000000..1e2821e70 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c09e8843bdbf400abe18c42aa71d7374 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 9dc02f611..42139ae96 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -14,7 +14,7 @@ namespace Mapbox.BaseModule.Map /// The primary object responsible for preparing the data and generating the visuals of the map. /// [Serializable] - public class MapboxMapVisualizer : IMapVisualizer + public class MapboxMapVisualizer : IMapVisualizer, ITileLifecycleSource { // Upper bound of the Mercator tile pyramid. Used by DelveInto and any other // recursion that needs an absolute hard cap independent of user-configurable @@ -24,9 +24,13 @@ public class MapboxMapVisualizer : IMapVisualizer public List LayerModules; public Dictionary ActiveTiles { get; private set; } public List TempTiles { get; private set; } - // Exposed for ITileLifecycleListener implementations that need access to the - // shared map state (e.g. terrain bounds tracker reads/writes Terrain.Min/Max). public IMapInformation MapInformation => _mapInformation; + + // Explicit interface impl narrows ActiveTiles to IReadOnlyDictionary for the + // ITileLifecycleSource contract while internal code keeps the mutable Dictionary + // it needs for Add/Remove. + IReadOnlyDictionary ITileLifecycleSource.ActiveTiles => ActiveTiles; + protected UnityContext _unityContext; protected IMapInformation _mapInformation; protected ITileCreator _tileCreator; @@ -94,14 +98,14 @@ public virtual IEnumerator Initialize() yield return _tileCreator.Initialize(terrainStrategy); } - // Give layer modules a chance to wire bookkeeping that needs the visualizer - // reference (terrain bounds tracker, etc) BEFORE their own Initialize runs — - // so any subscriptions are in place when the first tiles start loading. + // Give layer modules a chance to wire bookkeeping that observes tile lifecycle + // (terrain bounds tracker, etc) BEFORE their own Initialize runs — so any + // subscriptions are in place when the first tiles start loading. foreach (var module in LayerModules) { - if (module is ITileLifecycleListener listener) + if (module is ITileLifecycleObserver observer) { - listener.AttachToMapVisualizer(this); + observer.AttachToMapVisualizer(this); } } diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs index f6f172351..18bebdf96 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs @@ -30,8 +30,7 @@ namespace Mapbox.ImageModule.Terrain /// internal sealed class TerrainBoundsTracker : IDisposable { - private readonly IMapInformation _mapInfo; - private readonly MapboxMapVisualizer _visualizer; + private readonly ITileLifecycleSource _source; // One-shot widening subscriptions for shader-mode tiles whose CPU decode // hasn't arrived yet. Stored by (data, handler) so Dispose can detach the @@ -44,12 +43,11 @@ internal sealed class TerrainBoundsTracker : IDisposable private bool _disposed; private Coroutine _drainCoroutine; - public TerrainBoundsTracker(IMapInformation mapInfo, MapboxMapVisualizer visualizer) + public TerrainBoundsTracker(ITileLifecycleSource source) { - _mapInfo = mapInfo; - _visualizer = visualizer; - _visualizer.TileLoaded += OnTileLoaded; - _visualizer.TileUnloading += OnTileUnloading; + _source = source; + _source.TileLoaded += OnTileLoaded; + _source.TileUnloading += OnTileUnloading; if (Runnable.Instance != null) { _drainCoroutine = Runnable.Instance.StartCoroutine(DrainLoop()); @@ -61,10 +59,10 @@ public void Dispose() if (_disposed) return; _disposed = true; - if (_visualizer != null) + if (_source != null) { - _visualizer.TileLoaded -= OnTileLoaded; - _visualizer.TileUnloading -= OnTileUnloading; + _source.TileLoaded -= OnTileLoaded; + _source.TileUnloading -= OnTileUnloading; } // Detach pending watches. Both closures dropped — the dispose callback @@ -126,7 +124,7 @@ private void OnTileUnloading(UnityMapTile tile) // (called by PoolTile after this event fires) clears TerrainData, so we have to // check up front. Skip when both endpoints are 0 — the tile is flat and didn't // actually contribute, so a recompute would just produce the same values. - var terrain = _mapInfo.Terrain; + var terrain = _source.MapInformation.Terrain; var terrainData = tile.TerrainContainer?.TerrainData; bool contributedMax = terrainData != null && terrainData.IsElevationDataReady && terrainData.MaxElevation != 0f && @@ -140,8 +138,9 @@ private void OnTileUnloading(UnityMapTile tile) // so the empty-fallback (reset to TerrainInfo defaults) would never run // when the map empties out. ActiveTiles.Count == 1 here because TileUnloading // fires before the visualizer's ActiveTiles.Remove. - bool wasLastActive = terrainData != null && _visualizer.ActiveTiles.Count == 1 && - _visualizer.ActiveTiles.ContainsKey(tile.UnwrappedTileId); + var activeTiles = _source.ActiveTiles; + bool wasLastActive = terrainData != null && activeTiles.Count == 1 && + activeTiles.ContainsKey(tile.UnwrappedTileId); if (contributedMax || contributedMin || wasLastActive) { @@ -197,11 +196,11 @@ private void RemoveWatchEntry(TerrainData data, Action handler) private void Recompute() { - var terrain = _mapInfo.Terrain; + var terrain = _source.MapInformation.Terrain; float max = 0f; float min = 0f; bool any = false; - foreach (var kv in _visualizer.ActiveTiles) + foreach (var kv in _source.ActiveTiles) { var td = kv.Value.TerrainContainer?.TerrainData; if (td == null || !td.IsElevationDataReady) continue; diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index 2092fbb41..9ceb97d10 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -22,7 +22,7 @@ namespace Mapbox.ImageModule.Terrain /// settings, and hands each tile through a that produces /// render + optional collider meshes. /// - public class TerrainLayerModule : ITerrainLayerModule, ITileLifecycleListener + public class TerrainLayerModule : ITerrainLayerModule, ITileLifecycleObserver { private TerrainLayerModuleSettings _settings; private Source _rasterSource; @@ -39,11 +39,11 @@ public class TerrainLayerModule : ITerrainLayerModule, ITileLifecycleListener // plus iterator chain from Where/Select/Distinct on every tile-cover update. private readonly HashSet _dataIdScratch = new HashSet(); - public void AttachToMapVisualizer(MapboxMapVisualizer visualizer) + public void AttachToMapVisualizer(ITileLifecycleSource source) { - // Visualizer is now available — start tracking terrain bounds from tile events. + // Source is now available — start tracking terrain bounds from tile events. // Disposed in OnDestroy. - _boundsTracker = new TerrainBoundsTracker(visualizer.MapInformation, visualizer); + _boundsTracker = new TerrainBoundsTracker(source); } public TerrainLayerModule(Source source, TerrainLayerModuleSettings settings) : base() From 619c68e099af8d564494c1fc99fced5d7704aae5 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Wed, 13 May 2026 19:31:31 +0300 Subject: [PATCH 35/54] Terrain review follow-ups: bounds tracker tightening + cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Items from the review pass on the terrain layer. TerrainBoundsTracker: - Replace Mathf.Approximately with exact == for the contributor check. Values being compared were assigned directly from each other in the previous Recompute; Approximately's magnitude-scaled tolerance would also accept near-matches from sibling tiles, triggering redundant recomputes. - Drop the `terrainData != null` requirement from `wasLastActive`. An all-flat session where every tile had no terrain data wouldn't trip the contributor checks, and the wasLastActive gate was hiding the empty-fallback when the last shader-only tile pooled. - Warn loud if Runnable.Instance is null at construction. Should never happen in normal play, but the silent path leaves the drain coroutine unstarted and bounds stuck at defaults. UnityTileTerrainContainer: - Drop the "no texture?" Debug.Log — fires routinely on shader-mode startup before the texture arrives. - Replace "TerrainData is null, missing a isRecycled check?" Debug.Log with a silent bail + comment. Multicast dispose callback can legitimately fire on a container whose data was already cleared. ElevatedTerrainStrategy.CreateElevatedMesh: - Use cached _sideVertexCount instead of Mathf.Sqrt(mesh.vertexCount). Same value, one fewer sqrt per CPU-elevated-tile build, and the source of truth is now consistent with how _sideVertexCount is used everywhere else in the file. TerrainLayerModule.AttachToMapVisualizer: - Idempotent: bail if a tracker is already running. A second call would orphan the first (coroutine + subscriptions still live). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ElevatedTerrainStrategy.cs | 5 +++- .../Unity/UnityTileTerrainContainer.cs | 12 +++------ .../Terrain/TerrainBoundsTracker.cs | 25 +++++++++++++------ .../ImageModule/Terrain/TerrainLayerModule.cs | 5 ++-- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index 572bb8bcc..2ec17b10b 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -734,7 +734,10 @@ private void CreateElevatedMesh(UnityMapTile tile) // uniquely-owned per-tile mesh in RegisterTile above. var mesh = tile.MeshFilter.sharedMesh; var vertices = mesh.vertices; - var sampleCount = (int)Mathf.Sqrt(mesh.vertexCount); + // Per-axis vertex count is cached from Initialize. Was previously derived via + // Mathf.Sqrt(mesh.vertexCount), which is exact for our square grid but wasteful + // and gave the local an off-axis name (it's _sideVertexCount, not sampleCount). + var sampleCount = _sideVertexCount; for (int i = 0; i < vertices.Length; i++) { var x = i % sampleCount; diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs index 98015b9f2..d24af4fb4 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs @@ -70,10 +70,6 @@ public void SetTerrainData(TerrainData terrainData, bool useShaderElevation, Til } State = state; - if (terrainData.Texture == null || terrainData.TileId.Z == 0) - { - Debug.Log("no texture?"); - } TerrainData = terrainData; // Add (don't replace) — multiple render tiles share one TerrainData. TerrainData.AddDisposeCallback(_onDisposeCallback); @@ -116,11 +112,9 @@ public void OnTerrainUpdated() public void OnElevationValuesUpdated() { - if (TerrainData == null) - { - Debug.Log("TerrainData is null, missing a isRecycled check?"); - return; - } + // Multicast dispose callback can fire on a container whose TerrainData was + // already cleared (concurrent eviction during readback). Bail silently. + if (TerrainData == null) return; TerrainData.IsElevationDataReady = true; ElevationValuesUpdated(); } diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs index 18bebdf96..a2a826f6e 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs @@ -52,6 +52,12 @@ public TerrainBoundsTracker(ITileLifecycleSource source) { _drainCoroutine = Runnable.Instance.StartCoroutine(DrainLoop()); } + else + { + // Should never happen in normal play (Runnable is a global singleton). + // Fail loud so it's obvious if the construction order ever changes. + Debug.LogWarning("TerrainBoundsTracker: Runnable.Instance is null at construction; bounds recompute coroutine not started. Terrain.Min/MaxElevation will not update."); + } } public void Dispose() @@ -126,20 +132,25 @@ private void OnTileUnloading(UnityMapTile tile) // actually contribute, so a recompute would just produce the same values. var terrain = _source.MapInformation.Terrain; var terrainData = tile.TerrainContainer?.TerrainData; + // Exact equality: terrain.MaxElevation was assigned directly from some tile's + // MaxElevation in the previous Recompute, so `==` matches when this tile is + // that contributor. Mathf.Approximately's magnitude-scaled tolerance would + // also accept near-matches from sibling tiles, triggering redundant recomputes. bool contributedMax = terrainData != null && terrainData.IsElevationDataReady && terrainData.MaxElevation != 0f && - Mathf.Approximately(terrainData.MaxElevation, terrain.MaxElevation); + terrainData.MaxElevation == terrain.MaxElevation; bool contributedMin = terrainData != null && terrainData.IsElevationDataReady && terrainData.MinElevation != 0f && - Mathf.Approximately(terrainData.MinElevation, terrain.MinElevation); + terrainData.MinElevation == terrain.MinElevation; // Also dirty when this is the last tile being pooled — an all-flat session - // (every loaded tile had Min=Max=0) wouldn't trip the non-zero checks above, - // so the empty-fallback (reset to TerrainInfo defaults) would never run - // when the map empties out. ActiveTiles.Count == 1 here because TileUnloading - // fires before the visualizer's ActiveTiles.Remove. + // (every loaded tile had Min=Max=0, or no terrain data at all) wouldn't trip + // the non-zero checks above, so the empty-fallback (reset to TerrainInfo + // defaults) would never run when the map empties out. ActiveTiles.Count == 1 + // here because TileUnloading fires before the visualizer's ActiveTiles.Remove. + // No terrainData requirement: shader-only tiles count too. var activeTiles = _source.ActiveTiles; - bool wasLastActive = terrainData != null && activeTiles.Count == 1 && + bool wasLastActive = activeTiles.Count == 1 && activeTiles.ContainsKey(tile.UnwrappedTileId); if (contributedMax || contributedMin || wasLastActive) diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index 9ceb97d10..649d56532 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -41,8 +41,9 @@ public class TerrainLayerModule : ITerrainLayerModule, ITileLifecycleObserver public void AttachToMapVisualizer(ITileLifecycleSource source) { - // Source is now available — start tracking terrain bounds from tile events. - // Disposed in OnDestroy. + // Idempotent: if a tracker is already running (re-init scenarios), keep it. + // Constructing a second one would leak the first (coroutine + subscriptions). + if (_boundsTracker != null) return; _boundsTracker = new TerrainBoundsTracker(source); } From 2640b19379921ce8ddc1ccbddc602265ea32c136 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Thu, 14 May 2026 11:29:59 +0300 Subject: [PATCH 36/54] Release prep for v3.1.0: CHANGELOG rewrite + final cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite CHANGELOG.md v3.1.0 section with detailed breaking-change, feature, performance, fix, and known-limitations breakdowns. Drops the reverted iOS-flicker / generation-counter lines and adds the IPointerInput / IInputHandler / ITileLifecycleObserver-Source / TerrainBoundsTracker / AddDisposeCallback / ElevationArrayPool / TerrainData.IsDisposed / TileProviderBenchmark-removal entries that were missing. - MapInformation.Initialize now resets Terrain.MinElevation / MaxElevation to TerrainInfo defaults so a previous session's bounds don't bleed into a fresh map. - MapInput.ClampZoom max hoisted from a hardcoded 22f to public const float MaxClampZoom so the cap is discoverable from the camera surface. Mirrors MapboxMapVisualizer.MaxMercatorZoom (still int 22) — kept as a const because C# default-parameter values must be compile-time constants of the exact parameter type. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 224 ++++++++++++++++-- .../Mapbox/BaseModule/Map/MapInformation.cs | 8 +- Runtime/Mapbox/Example/Scripts/MapInput.cs | 8 +- 3 files changed, 214 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d011cd038..9f4d11031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,41 +2,217 @@ ### v3.1.0 -Minor-version bump per semver: this release contains source-breaking API changes (listed below) alongside the v3.0.7 feature work. The 3.0.7 designation was previously used in development; published 3.0.7 builds, if any, are superseded by 3.1.0. +Minor-version bump per semver: this release contains source-breaking API changes (listed below) alongside substantial feature work in camera/input, tile LOD, and terrain rendering. The 3.0.7 designation was previously used in development; published 3.0.7 builds, if any, are superseded by 3.1.0. #### New dependencies - Added `com.unity.burst@1.8.12`. The terrain-RGB elevation decoder and the collider vertex-fill jobs are now Burst-compiled. First-time domain reload pays a one-shot AOT compile cost; runtime decoding is significantly faster. +- `com.unity.inputsystem` is recognized as a **soft dependency**. Projects without it continue using `UnityEngine.Input`; projects with it automatically use the new Input System via a `MAPBOX_NEW_INPUT_SYSTEM` define wired through `versionDefines`. No project-side action required either way. See `Documentation~/CameraSystem.md` for details. #### Breaking changes -- `IMapInformation` gained a new member: `TerrainInfo Terrain { get; }`. Any project that ships its own `IMapInformation` implementation will need to add this property — return an instance of `TerrainInfo` (defaults are fine for a non-terrain map). The built-in `MapInformation` already implements it. + +**Source-level API surface:** + +- `IMapInformation` gained a new member: `TerrainInfo Terrain { get; }`. Any project that ships its own `IMapInformation` implementation must add this property — return an instance of `TerrainInfo` (defaults are fine for a non-terrain map). The built-in `MapInformation` already implements it. - `TerrainData.ElevationValuesUpdated` is now declared `public event Action` (was a plain `public Action` field). External code can subscribe with `+=` / unsubscribe with `-=` as before, but **direct assignment (`= myCallback`) and direct invocation (`ElevationValuesUpdated()`) no longer compile**. Migrate to `+=` for subscription. The previous single-setter `SetElevationChangedCallback` (which silently wiped other subscribers) has been removed. -- `CustomTMSTile` constructor now takes two additional parameters (`invertY`, `isMapboxService`). The legacy 4-arg constructor (`urlFormat`, `tileId`, `tilesetId`, `useNonReadableTexture`) is preserved as an overload defaulting to `invertY: true, isMapboxService: false` — external subclasses keep compiling. New code should use the 6-arg form. +- `MapboxTileData.SetDisposeCallback` was replaced with `public AddDisposeCallback(Action)` / `RemoveDisposeCallback(Action)` (multicast). External code that previously assigned through reflection or an internal-visible-to dependency should switch to the add/remove pair; subscriptions are now safely shared across the up-to-16 render tiles that consume the same `TerrainData` instance. +- `TerrainData.IsDisposed` is a new `public bool` (read-only). Async readback paths check this before assigning into the data; external consumers may also want to consult it. +- `MapboxMapVisualizer` now implements two new interfaces: + - `IMapVisualizer` (existing, no change). + - `ITileLifecycleSource` (new, in `Mapbox.BaseModule.Map`): exposes `TileLoaded` / `TileUnloading` events, `MapInformation` property, and `ActiveTiles` as `IReadOnlyDictionary` via explicit interface implementation. The public concrete `ActiveTiles` property still returns `Dictionary<,>` — no break for existing consumers. +- New opt-in marker interface `ITileLifecycleObserver` (in `Mapbox.BaseModule.Map`). Layer modules that need a visualizer reference for bookkeeping can implement it and the visualizer calls `AttachToMapVisualizer(ITileLifecycleSource source)` on them during `Initialize`. `TerrainLayerModule` implements it for its terrain-bounds tracker; other modules are unaffected. + +**Custom imagery module:** + +- `CustomTMSTile` constructor extended from 4 args → 6 args: added `bool invertY` and `bool isMapboxService`. The legacy 4-arg constructor (`urlFormat`, `tileId`, `tilesetId`, `useNonReadableTexture`) is preserved as an overload defaulting to `invertY: true, isMapboxService: false` — external subclasses keep compiling. New code should use the 6-arg form to control invert-Y behaviour and Mapbox-signed request routing explicitly. - `CustomSource.CreateTile` now always returns a `CustomTMSTile` when `UrlFormat` is set, regardless of `InvertY`. Previously `InvertY=false` returned a plain `RasterTile`. If you relied on the plain-raster path with a non-empty `UrlFormat`, set `UrlFormat = ""` to keep that behavior — empty `UrlFormat` falls back to plain `RasterTile`. -- `MapboxTileData.SetDisposeCallback` was replaced with `public AddDisposeCallback` / `RemoveDisposeCallback` (multicast). External code that previously assigned through reflection or an internal-visible-to dependency should switch to the add/remove pair; subscriptions are now safely shared across the up-to-16 render tiles that consume the same `TerrainData`. -- `CustomTerrainLayerModuleScript` now passes the user-configured `Settings.DataSettings` (cache size, retina flag, non-readable textures, data-zoom clamp) through to `CustomTerrainSource`. Previously a throwaway `ImageSourceSettings` was used and the Inspector values had no runtime effect. +- `CustomTerrainSource.CreateTile` got the same empty-`UrlFormat` fallback to plain `RasterTile` to match `CustomSource`. +- `CustomTerrainLayerModuleScript` now passes the user-configured `Settings.DataSettings` (cache size, retina flag, non-readable textures, data-zoom clamp) through to `CustomTerrainSource`. Previously a throwaway `ImageSourceSettings` was used and the Inspector values had no runtime effect. **This is a behavioral break for users who had `DataSettings` set to non-default values** — the values now actually apply. -#### Changes -- Added new documentation. -- Added a new demo scene demonstrating how to use POI information. -- Introduced new inspector scripts to improve editor UI/UX. -- Reorganized and cleaned up the Mapbox context menu used for creating Mapbox ScriptableObjects. -- New camera system (`MapCameraBehaviour`, `SlippyMapCameraBehaviour`, `Moving3dCameraBehaviour`) with touch input, two-finger pinch/tilt, and optional Input System package support. See `Documentation~/CameraSystem.md`. -- Tile-provider LOD overhaul: forward-projected distance, acute-angle compensation, frustum buffer, and reusable scratch arrays. Tile counts at high pitch are 40–60% lower vs the prior screen-fraction split. -- Terrain rendering: Burst-compiled decode, async PhysX collider bake, shared-flat-mesh + MaterialPropertyBlock per-tile state (saves Material allocations), bilinear height sampling. -- `TerrainInfo` exposes observed Min/Max elevation on `IMapInformation.Terrain`; tile-provider AABBs use it. +**Terrain settings:** + +- `TerrainInfo` removed two fields that were written-but-never-read: `IsEnabled` (was unused) and `Exaggeration` (no consumer). Remaining fields: `MinElevation`, `MaxElevation`. Plus new constants `DefaultMinElevation = 0f` and `DefaultMaxElevation = 5000f`. +- The previous `SetElevationChangedCallback` method on `TerrainData` was removed (replaced by the multicast event — see above). + +**Camera API:** + +- New `MapCameraBehaviour` open generic abstract base class. Concrete subclasses (`SlippyMapCameraBehaviour`, `Moving3dCameraBehaviour`) ship with the SDK. Custom cameras must subclass with a closed-type generic parameter. +- New `MapInput.Teardown(IMapInformation)` virtual hook. Existing subclasses of `MapInput` must override and unsubscribe any events they registered in `Initialize` — without the override the default is a no-op (no break, but recommended). +- New `MapInput.TouchCountDecreasedThisFrame` protected property (read inside `UpdateCamera` to detect 2→1 finger pinch-end and re-seed drag origin). +- New camera state mutator: `MapInput.CurrentTwoFingerGesture` removed from earlier review iterations — moved entirely inside `TouchInputHandler` as a private enum. External code can't depend on it (was never publicly exposed in v3.0.x). + +**Tile provider:** + +- `UnityTileProvider.MaximumZoomLevel` is now silently clamped to **30** internally (was unbounded). Higher values would have overflowed `1 << (_maxZoom - zoom)` past 30. Practically unreachable in Mercator (pyramid maxes at 22-24), but any Inspector misconfig now produces a log warning instead of an overflow. +- `UnityTileProvider` now warns at construction if `MinimumZoomLevel > MaximumZoomLevel`. +- `TileNode.Set` internal-shape signature changed from `(id, worldCenter, scale, boundsHeight)` to `(id, worldCenter, scale, boundsBottom, boundsHeight)`. Internal API — only relevant if you subclassed `UnityTileProvider` and touched `TileNode` directly. + +**Map visualizer:** + +- `MapboxMapVisualizer.MaxMercatorZoom` is a new `public const int` (`= 22`). Replaces the magic literal in `DelveInto`. Available for external consumers that need to align with the SDK's pyramid cap. +- `MapboxMapVisualizer.MapInformation` is a new public property exposing the map info. Was previously a protected field only. + +**Removed from package:** + +- `TileProviderBenchmark.cs` was removed from the SDK package. It was an internal tool that shouldn't have been included. The file is `.gitignore`d so a local copy can be kept by developers who need it; users will not see it in the SDK. + +#### New features + +**Camera & input:** + +- New camera system with two concrete behaviours: + - `SlippyMapCameraBehaviour` — fixed camera, map moves underneath. Best for 2D-style maps, navigation overlays, AR/VR. + - `Moving3dCameraBehaviour` — camera orbits a target point. Best for 3D city exploration, games, simulations. + Both wrap a `MapCameraBehaviour` generic base that handles the `IMapInformation` event subscription lifecycle. +- Touch input support: single-finger pan, two-finger pinch zoom, two-finger same-direction vertical tilt. Pinch/tilt are mutually exclusive via a per-frame gesture decision in `UpdateInputState`. Touch rotate is not implemented (mouse-only via right-click drag). +- Input System soft-dependency support. The SDK reads through an `IPointerInput` abstraction (legacy vs new system) selected at compile time by `MAPBOX_NEW_INPUT_SYSTEM`, with two `IInputHandler` implementations (mouse vs touch) selected by build platform — Android/iOS device builds use the touch handler; Editor and Standalone builds use the mouse handler. There is no runtime fallback. +- `Documentation~/CameraSystem.md` walks through the camera setup, gesture mapping, and the Input System opt-in. + +**Tile provider / LOD:** + +- LOD overhaul: + - Forward-projected distance replaces euclidean distance. + - Exponential split threshold replaces screen-fraction. + - Acute-angle compensation (`DistToSplitScale`) reduces detail for tiles viewed at grazing angles, ported from the native SDK's `tile_cover.cpp`. + - Result: tile counts at high pitch are 40–60% lower vs the prior algorithm with comparable visual fidelity near the camera. +- `FrustumBuffer` setting (world-space units; scales with `mapInformation.Scale`). Pre-loads tiles just outside the screen edges so panning doesn't pop in. +- Reusable scratch arrays (`_corners`, `_planes`, `_quadrants`, `_pool`) eliminate per-frame allocations in `GetTileCover` and `InternalUpdateCoroutine`. +- Sub-sea-level terrain handled correctly: `boundsBottom` derived from `MinElevation/scale`, so tiles below Y=0 (Death Valley, Dead Sea) no longer frustum-cull. + +**Terrain rendering:** + +- Burst-compiled decode of terrain-RGB into elevation float[] via `Sync/AsyncExtractElevationArray` Burst `IJob`s. Min/Max computed inline during decode (saves a second C# pass). +- Burst-compiled collider vertex-fill (`BuildColliderVerticesJob`) writing into a reusable `NativeArray`. Triangles regenerated only when `SampleCount` changes. +- Async PhysX collider bake (when `asyncBakeCollider = true`): `Physics.BakeMesh` runs on a worker thread; `Mesh.sharedMesh` assignment happens one frame later when the cooked-data cache is ready. Main-thread cost is near-zero. +- Shader-mode tiles share a single flat render mesh (`TerrainSharedFlat`) across the entire tile pool. Per-tile state (`_HeightTexture_ST`, `_TileScale`, etc.) goes via `MaterialPropertyBlock` instead of per-tile `Material` instances — saves `Material` clone allocations across pool churn. +- Bilinear elevation sampling in `TerrainData.QueryHeightData`, the collider vertex job, and the shader bilinear subgraph. Replaces the prior nearest-neighbor `(int)xx` truncation that produced visible stepping when render-tile mesh vertices sampled a denser sub-region than the data tile's effective resolution. +- `TerrainInfo` exposes observed `MinElevation` / `MaxElevation` (in meters) on `IMapInformation.Terrain`. Updated automatically as terrain tiles load/unload. The tile-provider's AABB and `UnityMapTile.SetFallbackMeshBounds` consume this to size the frustum-cull volume. +- Conservative bounds fallback (`UnityMapTile.SetFallbackMeshBounds`, 10000m) applied immediately on terrain-data attach so shader-displaced geometry doesn't get culled before real Min/Max arrive — or permanently, when CPU extraction is disabled. +- Per-tile `MeshCollider` configurable via `TerrainColliderOptions` (in `ElevationLayerProperties`). Supports placing the collider on a dedicated child GameObject + Unity Layer. +- Bounds tracking is now an opt-in concern of `TerrainLayerModule` (via `ITileLifecycleObserver` → `TerrainBoundsTracker`). `MapboxMapVisualizer` itself is terrain-agnostic. + +**Custom imagery:** + +- New `CustomSourceSettings.InvertY` toggle: when `true`, Y is `(2^z - Y - 1)` instead of `Y` in URL substitution (TMS-style). +- New `CustomSourceSettings.IsMapboxService` toggle: when `true`, the tile request is signed with the Mapbox access token; when `false`, it's a plain HTTP request. + +**Editor / Inspector:** + +- New custom drawer for `[SimplificationFactor]` attribute: dropdown of distinct-grid presets with a live HelpBox describing the resulting vertex grid + per-tile quad count. Legacy non-preset values now show a "migrate to a preset" warning instead of being silently snapped. Multi-object editing and prefab-override-safe via `BeginProperty`/`EndProperty`. +- New custom drawer for `TerrainColliderOptions`: foldout with grayed-out sub-options when `addCollider` is disabled, further gated when `useDedicatedColliderLayer` is disabled. +- New custom drawer for `TerrainSideWallOptions`: same reveal pattern for skirt-wall settings. +- New custom drawer for `[GameObjectLayer]` attribute: Unity Layer dropdown for `layerId` fields. +- `TerrainSettingsInspectorHelper` warns when extraction is force-enabled by other settings (UseShaderTerrain off, or collider on) and renders the `ExtractCpuElevationData` checkbox as disabled+ticked. The previous OnGUI silent write to `boolValue` was removed — no more silently dirtying inspected assets. Scene-scan for vector / components modules is cached and invalidated on `EditorApplication.hierarchyChanged`. + +**Other:** + +- New demo scene demonstrating how to use POI information. +- New documentation files added. +- Reorganized Mapbox context menu for creating Mapbox ScriptableObjects. + +#### Performance + +- Burst-compiled terrain-RGB decode: significantly faster than the prior C# loop. +- Burst-compiled collider vertex fill with persistent `NativeArray` buffers. +- `ElevationArrayPool`: pools `float[]` buffers across cache evictions, capped at 32 per-size (`MaxPerSizeDepth`). Avoids ~256 KB / ~1 MB allocations per tile decode for 256² / 512² heightmaps. Returns past the cap drop to GC. In Editor, the pool clears on `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` so stale buffers don't survive play-mode stop. +- `DelveInto` rewritten as iterative explicit-stack: eliminates `new bool[4]` + `new UnwrappedTileId[4]` allocations per cache miss (up to ~85 recursive calls × ~32 bytes before). +- `MapboxMapVisualizer.Load` reuses `unityMapTile.Children` lists across pool cycles instead of allocating a fresh list per cache miss. +- `TerrainBoundsTracker` recomputes Min/Max from `ActiveTiles` once per coroutine tick (deferred via a dirty flag) instead of on every tile event. +- `TerrainData._cachedWidth` caches `sqrt(ElevationValues.Length)` once on `SetElevationValues` — `QueryHeightData` no longer recomputes per query. +- `SimplificationFactorDrawer` caches preset+label arrays per `(Min, Max, VertexBase)` tuple and reuses one `GUIContent` for HelpBox height calculation (was allocating per repaint). +- `TerrainSettingsInspectorHelper` scene scan no longer runs `FindObjectsByType` on every OnGUI repaint. #### Fixes -- Fixed an issue where vector layer range limits were not applied correctly during data processing. + +**Tile pipeline:** + +- Fixed multicast dispose notification — shared `TerrainData` now notifies all consumers on eviction (previously the single-setter callback silently dropped 15 of 16 sharing tiles). +- Fixed `TerrainData.ElevationValuesUpdated` previously being a plain `public Action`, which let `SetElevationChangedCallback` silently wipe other subscribers. Now a proper multicast event. +- Fixed `TerrainSource.ExtractElevationValues` using the wrong overload of `ExtractHeightData` — the `Action` overload + 1-arg `SetElevationValues` left `Min/MaxElevation` at 0 on every tile loaded through the initial async path. Now uses the `TerrainData` overload (which sets Min/Max inline). +- Fixed `TerrainSource.ExtractElevationValues` hanging forever when async readback completes after `TerrainData.Dispose` — the coroutine now subscribes to both `ElevationValuesUpdated` and the dispose multicast and completes on either signal. +- Fixed `Async/SyncExtractElevationArray` not guarding against `TerrainData.IsDisposed` — late-arriving GPU readback callbacks would assign into evicted data and leak the rented buffer. Now both check before renting and after the decode; the rented buffer is returned to the pool when dispose arrives in the window. +- Fixed `MapboxMapVisualizer.RecomputeTerrainBounds` (now in `TerrainBoundsTracker`) not being wired to `ElevationValuesUpdated` for shader-mode tiles — those tiles finish on texture-ready before CPU decode arrives, so the bounds never widened until extraction landed. Now a one-shot watcher per shared `TerrainData` triggers the recompute. +- Fixed terrain bounds remaining pinned at 0 when all loaded tiles were at/above sea level (previous accumulate-only `< terrain.MinElevation` never fired). +- Fixed terrain bounds monotonically inflating over a long session — `TerrainBoundsTracker` now recomputes from `ActiveTiles` on tile pool, so bounds tighten as the camera moves to lower-elevation regions. +- Fixed `UnityTileTerrainContainer.SetTerrainData(null)` silently leaking the prior `TerrainData`'s `ElevationValuesUpdated` and dispose-callback subscriptions. Detach now runs unconditionally before the null-return. +- Fixed `UnityTileTerrainContainer.OnDestroy` not removing the dispose callback (only the `ElevationValuesUpdated` handler) — a later eviction would fire the multicast into a destroyed tile. +- Fixed `UnityTileImageContainer.OnDestroy` being empty — same multicast-leak pattern as the terrain container. + +**Terrain strategy / collider:** + +- Fixed CPU-elevation render mesh leak via `MeshFilter.mesh` clone path. `CreateElevatedMesh` now reads `.sharedMesh` (the `.mesh` getter implicitly cloned the assigned shared mesh and orphaned the original). Per-tile mesh is now explicitly named `TerrainCpuMesh` so `UnityMapTile.OnDestroy` correctly disposes it. +- Fixed `_sharedFlatMesh` being `Clear()`'d on a shader→CPU transition when `sampleCount` or `_useTileSkirts` changed at runtime — would wipe geometry for every other shader-mode tile pointing at the shared mesh. Now allocates a fresh per-tile mesh first. +- Fixed `_sharedFlatMesh` leaking on a second `Initialize` call. `Initialize` now destroys the previous one before allocating. +- Fixed `MeshCollider.cookingOptions` only being set at component creation — when an existing collider's cooking options diverged from the bake's, PhysX silently re-cooked synchronously on assignment, defeating the async-bake path. Now assigned every call. +- Fixed `CompleteBakeAndAssign` not null-guarding the new mesh after `JobHandle.Complete()` — a parallel bake completion could `Destroy()` our mesh as its "previous" before we got to assign it. +- Fixed `_elevationNativeMirror` cache key being the `float[]` reference. `ElevationArrayPool` recycles buffers, so the same `float[]` could be re-rented for different data and produce a false cache hit. Now keyed by `TerrainData` reference. +- Fixed `RegisterCollider` deferred path adding a closure for every temp→final re-invocation, doubling async-bake work. Now de-dups by `(tile, data)`. +- Fixed deferred-rebuild closures rooting `tile` and `data` when `TerrainData.Dispose` arrived before `ElevationValuesUpdated` fired. Closures now self-detach on both signals. +- Fixed `_lastColliderBuild` (collider build cache) growing with dead `MeshCollider` keys — Unity's overloaded `==` doesn't reach `Dictionary`'s reference equality. Opportunistic sweep + `Clear()` on `OnDestroy`. +- Fixed mesh-name string duplication: `TerrainColliderMeshName`, `SharedFlatMeshName`, `TerrainCpuMeshName` are now `public const` on `ElevatedTerrainStrategy`. The dedicated-collider-layer GameObject name renamed from `"TerrainCollider"` to `"TerrainColliderChild"` to disambiguate from the mesh name. + +**Tile provider:** + +- Fixed `UnityTileProvider.ShouldSplit` overriding the per-corner `offset.y` to `+cameraHeight` — correct in the native SDK's tile-coordinate system but flipped the sign of the vertical dot-product contribution in Unity's world units, triggering "always split" for downward-pitched cameras. Now uses ground-plane projection (`offset.y = 0f`). +- Fixed `UnityTileProvider.DistToSplitScale` dividing by `dz` with no zero guard — camera-at-ground (`dz=0`) produced NaN/Inf and pinned the tile to `MinimumZoomLevel`. Now short-circuits with `return 1f` when `dz < 0.0001f`. +- Fixed `UnityTileProvider.ShouldSplit` always-split for behind-camera corners recursing all the way to `_maxZoom` on tilted views with `FrustumBuffer` expansion. Now capped at `zoom < 18` (`AlwaysSplitSafeCap`). +- Fixed `UnityTileProvider.FrustumBuffer` tooltip not mentioning that the value is in world-space units that depend on `mapInformation.Scale`. + +**Camera & input:** + +- Fixed `MAPBOX_NEW_INPUT_SYSTEM` define gating being broken — the `versionDefines` entry that defined the symbol was uncommitted. Projects with "Active Input Handling = New" only would crash on every legacy `UnityEngine.Input.*` call. +- Fixed `MapboxExamples.asmdef` requiring `Unity.InputSystem` as a hard reference, breaking projects without the package. Now relies on the InputSystem asmdef's `autoReferenced: true` resolution; absent → no reference, no compile error. +- Fixed scroll-zoom delta magnitude differing ~10× between legacy and new input paths on Windows (legacy `Input.GetAxis` ≈ 0.1/notch, new `scroll.y` ≈ 120/notch). Normalized to ~0.1/notch on both. +- Fixed pan jump after a 2-finger pinch drops to 1 finger — the new "primary" touch could be at a different screen position than the lifted one. Now re-seeds drag origin on `TouchCountDecreasedThisFrame`. +- Fixed pinch and tilt detectors both firing on the same frame (each computed dominance independently and `GetPinchZoomDelta` updated `_previousPinchDistance` regardless of which won). Decision now made once per frame in `UpdateInputState`. +- Fixed pinch/tilt thresholds being in pixel units (DPI-dependent). Both magnitudes now normalized to `Screen.height`; pinch wins ties (no dropped frames on near-vertical pure-tilt drags). +- Fixed pinch state corruption across 2→3→2 finger transitions. EnhancedTouch reorders `activeTouches` on add/remove, so the next frame would compare distance against a different physical pair. Now tracks the touch ID pair and reseeds on change. +- Fixed `Moving3dCamera.Zoom()` NaN propagation through `preDistance`, `camDistanceToMouse`, and the lerp output. Top-down ortho / first-frame / `mapInformation.Scale=0` / camera curve evaluating to 0 all hit the guards now. +- Fixed `MapCameraBehaviour.Awake` NRE when no `MapBehaviourCore` exists in the scene. Logs an error and disables the component instead. +- Fixed `MapCameraBehaviour` never unsubscribing from `MapBehaviour.Initialized` — leak across scene reload. +- Fixed `MapCameraBehaviour` not tearing down `IMapInformation` event subscriptions registered by the camera core (`LatitudeLongitudeChanged`, `ViewChanged`, `SetView`) — the closures rooted the destroyed camera and its `Camera` Transform. New `MapInput.Teardown` virtual hook + `OnDestroy` wiring. +- Fixed `MapCameraBehaviour.OnMapInitialized` not tearing down a previous `MapInformation` if the event fires twice (defensive — not currently exercised by example scenes). +- Fixed `SlippyMapCamera.Initialize` dereferencing `FindObjectOfType().transform` without a null check. +- Fixed `SlippyMapCamera.CenterLatitudeLongitude` being overwritten every frame even when nothing moved, with a `Mathf.Clamp((float)Latitude, …)` downcast losing precision at z18+. Now gated on `_output.HasChanged` and uses `System.Math.Clamp` on doubles. + +**Map visualizer:** + +- Fixed `MapboxMapVisualizer.RemoveUnnecessaryTiles` previously exempting `Filler` tiles from removal — caused orphaned-filler z-fighting. Now eligible for pooling; visual continuity preserved by the temp-tile filler protection pass in `Load()`. +- Fixed `MapboxMapVisualizer.OnDestroy` not detaching pending elevation-decode watchers — closures kept shared `TerrainData` rooted past the visualizer's lifetime. Tracker (now in `TerrainBoundsTracker`) handles this in its `Dispose`. +- Fixed `LoadingState.Filler` children of currently-loading temp tiles being pooled when they're still needed for visual continuity. Filler protection pass in `Load()` removes their ids from `_toRemove`. + +**Custom imagery:** + +- Fixed `CustomSource.CreateTile` throwing `ArgumentNullException` from `string.Format(null, …)` when `UrlFormat` was empty. Empty `UrlFormat` now falls back to plain `RasterTile`. +- Fixed `CustomTerrainSource.CreateTile` missing the same empty-`UrlFormat` fallback. +- Fixed `CustomTerrainLayerModuleScript` not passing the user's `DataSettings` through to the source — Inspector values had no runtime effect. +- Restored 4-arg `CustomTMSTile` constructor as a backward-compat overload (binary break for external subclasses otherwise). + +**Editor / Inspector:** + +- Fixed `SimplificationFactorDrawer` silently snapping legacy non-preset values to the nearest preset during `OnGUI` — dirtied every inspected asset on first paint. +- Fixed `SimplificationFactorDrawer` missing `BeginProperty`/`EndProperty` — broke prefab-override and multi-object editing. +- Fixed `SimplificationFactorDrawer.GetPropertyHeight` returning a different height than the rendered HelpBox content for legacy values — caused HelpBox to clip into the next Inspector row. +- Fixed `TerrainColliderOptionsDrawer.GetPropertyHeight` returning one extra `line + spacing` (cosmetic gap at the bottom of the foldout). +- Fixed `TerrainSettingsInspectorHelper` writing `child.boolValue = true` during `OnGUI` — silently dirtied every inspected asset. Now display-only (runtime evaluates `NeedsCpuElevation` directly). +- Fixed `TerrainSettingsInspectorHelper.SceneHasFeaturesNeedingCpuElevation` running `FindObjectsByType` on every Inspector repaint. Cached and invalidated on `EditorApplication.hierarchyChanged`. + +**Other:** + +- Fixed `TerrainData.QueryHeightData(Vector2)` and `(float, float)` overloads not guarding against null `ElevationValues` — shader-only mode (`ExtractCpuElevationData=false`) hit the NRE. +- Fixed `TerrainData.Dispose` not being idempotent — explicit `IsDisposed` bail at top. +- Fixed `MapboxTileData.SetDisposeCallback` previously silently overwriting other subscribers when one `TerrainData` was shared across 16 render tiles (the single-callback API). Replaced with multicast `AddDisposeCallback` / `RemoveDisposeCallback`. +- Fixed `1 << (_maxZoom - zoom)` shift overflow risk in `UnityTileProvider.ShouldSplit` when `_maxZoom` exceeded 30. Now clamped internally. +- Fixed vector layer range limits not being applied correctly during data processing. - Fixed incorrect limit validation and bounds checking in the `LatitudeLongitude` struct. -- Fixed deferred-pool re-borrow race in `MapboxMapVisualizer` (generation counter). -- Fixed multicast dispose notification — shared `TerrainData` now notifies all consumers on eviction. -- Fixed CPU-elevation mesh leak via `.mesh` clone path. -- Fixed `_sharedFlatMesh` clear regression on shader→CPU transition. -- Fixed terrain bounds remaining pinned at 0 when all loaded tiles were at/above sea level. -- Fixed iOS tile flicker on zoom out (child-pool deferred by one frame after parent show). -- Fixed `Moving3dCamera.Zoom()` NaN when `preDistance` or camera→cursor distance is 0. -- Fixed pinch/tilt mutual exclusion (decision now made once per frame in `UpdateInputState`). -- Fixed pan jump after pinch-to-single-finger transition (re-seed drag origin on touch-count decrease). +- Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. + +#### Known limitations + +- **Touch path can't be tested in the Unity Editor.** The mouse/touch handler split is compile-time on build target (`(UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR`). To debug touch logic without a device, temporarily remove the `&& !UNITY_EDITOR` clause in `MapInput.cs` and use Unity Remote. +- **Touch rotate is not supported.** Mouse-only via right-click drag. Documented in `Documentation~/CameraSystem.md`. A two-finger twist gesture is not implemented. +- **`asyncBakeCollider = true` has not been platform-validated on iOS/Android.** The PhysX bake runs on a worker thread; no explicit upload-completion barrier between `Mesh.SetVertices`/`SetIndices` and the worker. Recommended: Frame Debugger pass on a target device before relying on async-bake in production. +- **Shader graph shrinkage is unverified visually.** `ElevatedTerrainShader.shadergraph` and `MapboxTerrainBilinearSampling.shadersubgraph` were reduced by ~280 / ~242 lines respectively. A `Normalize` was removed downstream of the gradient — if not load-bearing, fine; if load-bearing, lighting magnitude scales with slope length. Side-by-side visual diff recommended. +- **GPU instancing for terrain is not enabled.** The shared-flat-mesh + MPB pattern saves per-tile `Material` clone allocations but does *not* enable SRP Batcher batching (MPB disqualifies a renderer) and instancing variants are off in the materials. Terrain draws are still N separate draws. See `MEMORY.md` → `project_terrain_instancing` for the future-work plan. ### v3.0.6 diff --git a/Runtime/Mapbox/BaseModule/Map/MapInformation.cs b/Runtime/Mapbox/BaseModule/Map/MapInformation.cs index 20095f91b..2c5cf306e 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapInformation.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapInformation.cs @@ -48,10 +48,16 @@ public virtual void Initialize() public virtual void Initialize(LatitudeLongitude latitudeLongitude) { if(_isInitialized) return; - + SetLatitudeLongitude(latitudeLongitude); _isInitialized = true; QueryElevation = (tileId, x, y) => 0; + // Reset terrain bounds to TerrainInfo defaults so a previous map's Min/Max + // never bleeds into a fresh session. Field initializers already set defaults + // on new MapInformation instances, but explicitly resetting here guards + // against future paths that reuse an existing instance. + Terrain.MinElevation = TerrainInfo.DefaultMinElevation; + Terrain.MaxElevation = TerrainInfo.DefaultMaxElevation; } //PROPERTIES diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index c0324092d..f783858bf 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -86,7 +86,13 @@ public Transform GetTransform() #region Value Clamping - protected static float ClampZoom(float zoom, float min = 0f, float max = 22f) + // Absolute Mercator pyramid max. Mirrors MapboxMapVisualizer.MaxMercatorZoom + // (`int 22`) — kept here as a float const because C# default-parameter values + // must be compile-time constants of the parameter's exact type. If the pyramid + // cap ever changes, update both this and MapboxMapVisualizer.MaxMercatorZoom. + public const float MaxClampZoom = 22f; + + protected static float ClampZoom(float zoom, float min = 0f, float max = MaxClampZoom) { return Mathf.Clamp(zoom, min, max); } From c1a5fc1431099c347400cc0ad1c6f64e7fb4fba1 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Thu, 14 May 2026 15:39:55 +0300 Subject: [PATCH 37/54] Add ComponentSystem Editor asmdef to fix player builds The Editor folder under VectorModule/ComponentSystem/ had three editor scripts (BuildingLayerVisualizerObjectEditor, ExtrusionOptionsDrawer, MapboxComponentsModuleScriptEditor) but no asmdef. They were folded into the runtime MapboxComponentSystem assembly, pulling UnityEditor references into player builds and breaking compilation. New MapboxComponentSystem.Editor.asmdef is Editor-only and references the runtime asm + MapboxBaseModule (for Mapbox.BaseModule.Data.Interfaces). Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 ++ .../MapboxComponentSystem.Editor.asmdef | 19 +++++++++++++++++++ .../MapboxComponentSystem.Editor.asmdef.meta | 7 +++++++ 3 files changed, 28 insertions(+) create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef create mode 100644 Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4d11031..892b00aa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ Minor-version bump per semver: this release contains source-breaking API changes - Async PhysX collider bake (when `asyncBakeCollider = true`): `Physics.BakeMesh` runs on a worker thread; `Mesh.sharedMesh` assignment happens one frame later when the cooked-data cache is ready. Main-thread cost is near-zero. - Shader-mode tiles share a single flat render mesh (`TerrainSharedFlat`) across the entire tile pool. Per-tile state (`_HeightTexture_ST`, `_TileScale`, etc.) goes via `MaterialPropertyBlock` instead of per-tile `Material` instances — saves `Material` clone allocations across pool churn. - Bilinear elevation sampling in `TerrainData.QueryHeightData`, the collider vertex job, and the shader bilinear subgraph. Replaces the prior nearest-neighbor `(int)xx` truncation that produced visible stepping when render-tile mesh vertices sampled a denser sub-region than the data tile's effective resolution. +- `ElevatedTerrainShader.shadergraph` and `MapboxTerrainBilinearSampling.shadersubgraph` were restructured during the bilinear-sampling and normal-banding work (ridge-band fix uses a 2-texel offset, per Option A in the prior investigation). Lighting and displacement look correct on eye-test against the example scenes. If a project ships custom shader variants derived from these graphs, re-verify after upgrading. - `TerrainInfo` exposes observed `MinElevation` / `MaxElevation` (in meters) on `IMapInformation.Terrain`. Updated automatically as terrain tiles load/unload. The tile-provider's AABB and `UnityMapTile.SetFallbackMeshBounds` consume this to size the frustum-cull volume. - Conservative bounds fallback (`UnityMapTile.SetFallbackMeshBounds`, 10000m) applied immediately on terrain-data attach so shader-displaced geometry doesn't get culled before real Min/Max arrive — or permanently, when CPU extraction is disabled. - Per-tile `MeshCollider` configurable via `TerrainColliderOptions` (in `ElevationLayerProperties`). Supports placing the collider on a dedicated child GameObject + Unity Layer. @@ -195,6 +196,7 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed `TerrainColliderOptionsDrawer.GetPropertyHeight` returning one extra `line + spacing` (cosmetic gap at the bottom of the foldout). - Fixed `TerrainSettingsInspectorHelper` writing `child.boolValue = true` during `OnGUI` — silently dirtied every inspected asset. Now display-only (runtime evaluates `NeedsCpuElevation` directly). - Fixed `TerrainSettingsInspectorHelper.SceneHasFeaturesNeedingCpuElevation` running `FindObjectsByType` on every Inspector repaint. Cached and invalidated on `EditorApplication.hierarchyChanged`. +- Fixed `Runtime/Mapbox/VectorModule/ComponentSystem/Editor/` missing an asmdef — editor scripts (`BuildingLayerVisualizerObjectEditor`, `ExtrusionOptionsDrawer`, `MapboxComponentsModuleScriptEditor`) were being folded into the runtime `MapboxComponentSystem` assembly, pulling `UnityEditor` references into player builds. New `MapboxComponentSystem.Editor.asmdef` (Editor-only, references the runtime asm + `MapboxBaseModule`) keeps them out. **Other:** diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef new file mode 100644 index 000000000..c19203adb --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "MapboxComponentSystem.Editor", + "rootNamespace": "", + "references": [ + "GUID:5aef8304858a8459cb2204d15bf75ab9", + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef.meta new file mode 100644 index 000000000..ff1d2ea07 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d9eb5dce6831647d59dc1f65374df0d1 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 7a26ad05d75ff7f325f2c75c2cf809ead8a2b834 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Thu, 14 May 2026 15:59:21 +0300 Subject: [PATCH 38/54] Player-build fixes: TileCreator fallback shader + SubdivisionBias migration (1) MapboxMapBehaviour.CreateMapVisualizer silently produced a magenta tile material in player builds when no TileCreatorBehaviour was assigned. The fallback uses Shader.Find on the default terrain shader, which doesn't resolve in builds unless the shader is referenced by a shipped Material or listed in Always Included Shaders. Now throws a clear InvalidOperationException pointing the user at either fix. (2) UnityTileProviderSettings.SubdivisionBias deserialized as 0 in scenes saved before the field existed, since Unity assigns default(T) for missing fields and skips C# field initializers in that case. The 0 value collapses distToSplit and forces only the MinimumZoomLevel tile to render. New C# default is 0.6f and ISerializationCallbackReceiver upgrades legacy <=0 values on deserialize. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 ++ .../Example/Scripts/Map/MapboxMapBehaviour.cs | 17 ++++++++++++++- .../TileProvider/UnityTileProvider.cs | 21 +++++++++++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 892b00aa5..6c849c286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -197,6 +197,8 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed `TerrainSettingsInspectorHelper` writing `child.boolValue = true` during `OnGUI` — silently dirtied every inspected asset. Now display-only (runtime evaluates `NeedsCpuElevation` directly). - Fixed `TerrainSettingsInspectorHelper.SceneHasFeaturesNeedingCpuElevation` running `FindObjectsByType` on every Inspector repaint. Cached and invalidated on `EditorApplication.hierarchyChanged`. - Fixed `Runtime/Mapbox/VectorModule/ComponentSystem/Editor/` missing an asmdef — editor scripts (`BuildingLayerVisualizerObjectEditor`, `ExtrusionOptionsDrawer`, `MapboxComponentsModuleScriptEditor`) were being folded into the runtime `MapboxComponentSystem` assembly, pulling `UnityEditor` references into player builds. New `MapboxComponentSystem.Editor.asmdef` (Editor-only, references the runtime asm + `MapboxBaseModule`) keeps them out. +- Fixed `MapboxMapBehaviour.CreateMapVisualizer` silently producing a broken material in player builds when no `TileCreatorBehaviour` was assigned. The fallback uses `Shader.Find(Constants.Map.DefaultTerrainShaderName)`, but `Shader.Find` only resolves shaders referenced by a shipped Material or listed in *Project Settings → Graphics → Always Included Shaders* — neither is true by default, so `new Material(null)` produced a magenta tile material. Now throws a clear `InvalidOperationException` pointing the user at either solution. +- Fixed `UnityTileProviderSettings.SubdivisionBias` defaulting to `0` in scenes saved before the field existed — Unity assigns `default(float)` for missing serialized fields and does NOT run the C# field initializer in that case, which collapsed `distToSplit` and forced the tile provider to render only the `MinimumZoomLevel` tile. New C# default is `0.6f`; `ISerializationCallbackReceiver.OnAfterDeserialize` upgrades legacy serialized `<= 0` values to `0.6f`. **Other:** diff --git a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs index dff5e827a..37214b19c 100644 --- a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs @@ -122,7 +122,22 @@ protected virtual MapboxMapVisualizer CreateMapVisualizer(IMapInformation mapInf } else { - var defaultMapboxTerrainMaterial = new Material(Shader.Find(Constants.Map.DefaultTerrainShaderName)); + // Fallback when no TileCreatorBehaviour is assigned on the GameObject. + // In a build, Shader.Find only resolves shaders that are referenced by a + // shipped Material asset or listed in Project Settings → Graphics → Always + // Included Shaders. The Mapbox terrain shader is neither by default, so + // this code path silently produced a magenta tile material. Fail loudly + // with an actionable error instead. + var shader = Shader.Find(Constants.Map.DefaultTerrainShaderName); + if (shader == null) + { + throw new InvalidOperationException( + $"MapboxMapBehaviour on '{name}' has no TileCreator assigned and could not locate the default shader " + + $"'{Constants.Map.DefaultTerrainShaderName}' at runtime. " + + "Assign a TileCreatorBehaviour component with a configured Material, or add the Mapbox terrain shader to " + + "Project Settings → Graphics → Always Included Shaders so it ships in player builds."); + } + var defaultMapboxTerrainMaterial = new Material(shader); tileCreator = new TileCreator(unityContext, new[] { defaultMapboxTerrainMaterial }); } return new MapboxMapVisualizer(mapInfo, unityContext, tileCreator); diff --git a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs index 64fb79b4b..4b7dffe2d 100644 --- a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs +++ b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs @@ -10,7 +10,7 @@ namespace Mapbox.UnityMapService.TileProviders { [Serializable] - public class UnityTileProviderSettings + public class UnityTileProviderSettings : ISerializationCallbackReceiver { /// /// Camera used for frustum culling and LOD calculations. @@ -40,10 +40,13 @@ public class UnityTileProviderSettings /// Controls how aggressively tiles subdivide with distance. /// Higher values produce more tiles with finer detail at distance but cost more to render. /// Lower values improve performance with coarser detail at distance. - /// Values below 0.5 may cause visible quality loss. Default 1.0. + /// Values at or below 0 collapse the split-distance to zero and render only the + /// MinimumZoomLevel tile — kept guarded in OnAfterDeserialize for legacy scenes + /// saved before this field existed (Unity deserializes missing fields as default(T) + /// and does NOT run the C# field initializer in that case). /// - [Tooltip("Tile detail vs. performance. Higher = more detail at distance, lower = better performance. Below 0.5 may reduce quality. Default 1.0.")] - public float SubdivisionBias = 1.0f; + [Tooltip("Tile detail vs. performance. Higher = more detail at distance, lower = better performance. Below 0.3 may reduce quality. Default 0.6.")] + public float SubdivisionBias = 0.6f; /// /// Expands the 4 side frustum planes outward by this distance, in Unity world units. @@ -63,6 +66,16 @@ public UnityTileProviderSettings(Camera cam, float minZoom = 2, float maxZoom = MinimumZoomLevel = minZoom; MaximumZoomLevel = maxZoom; } + + public void OnBeforeSerialize() { } + + public void OnAfterDeserialize() + { + // Migrate legacy scenes saved before SubdivisionBias existed (Unity assigns + // default(float) = 0 in that case, collapsing distToSplit and forcing only + // the MinimumZoomLevel tile to render). + if (SubdivisionBias <= 0f) SubdivisionBias = 0.6f; + } } public class UnityTileProvider : TileProvider From 428189fe77a7ade2236e1c289cd224045dc49eaf Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Thu, 14 May 2026 16:08:54 +0300 Subject: [PATCH 39/54] Serialize default tile Material on MapboxMapBehaviour for player builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Shader.Find runtime construction with a serialized Material reference. The previous fallback in CreateMapVisualizer used Shader.Find on the default terrain shader, which doesn't survive build-time stripping unless the shader is referenced from a shipped Material asset or listed in Always Included Shaders — so demos that didn't wire a TileCreatorBehaviour shipped with magenta tiles. New [SerializeField] protected Material _defaultTileMaterial field holds the asset reference directly. Demo scenes (LocationExample, ApiTest) ship with it wired to ElevatedTerrainMaterial.mat so the shader survives stripping via the asset reference. If both _tileCreatorBehaviour and _defaultTileMaterial are null, throws InvalidOperationException with instructions for either fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 +- .../Example/Scripts/Map/MapboxMapBehaviour.cs | 24 +++++++++---------- .../Mapbox/Example/Scripts/Test/ApiTest.unity | 1 + .../Example/LocationExample.unity | 1 + 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c849c286..4a96c2956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -197,7 +197,7 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed `TerrainSettingsInspectorHelper` writing `child.boolValue = true` during `OnGUI` — silently dirtied every inspected asset. Now display-only (runtime evaluates `NeedsCpuElevation` directly). - Fixed `TerrainSettingsInspectorHelper.SceneHasFeaturesNeedingCpuElevation` running `FindObjectsByType` on every Inspector repaint. Cached and invalidated on `EditorApplication.hierarchyChanged`. - Fixed `Runtime/Mapbox/VectorModule/ComponentSystem/Editor/` missing an asmdef — editor scripts (`BuildingLayerVisualizerObjectEditor`, `ExtrusionOptionsDrawer`, `MapboxComponentsModuleScriptEditor`) were being folded into the runtime `MapboxComponentSystem` assembly, pulling `UnityEditor` references into player builds. New `MapboxComponentSystem.Editor.asmdef` (Editor-only, references the runtime asm + `MapboxBaseModule`) keeps them out. -- Fixed `MapboxMapBehaviour.CreateMapVisualizer` silently producing a broken material in player builds when no `TileCreatorBehaviour` was assigned. The fallback uses `Shader.Find(Constants.Map.DefaultTerrainShaderName)`, but `Shader.Find` only resolves shaders referenced by a shipped Material or listed in *Project Settings → Graphics → Always Included Shaders* — neither is true by default, so `new Material(null)` produced a magenta tile material. Now throws a clear `InvalidOperationException` pointing the user at either solution. +- Fixed `MapboxMapBehaviour.CreateMapVisualizer` silently producing a magenta tile material in player builds when no `TileCreatorBehaviour` was assigned. The previous fallback used `Shader.Find` to construct a Material at runtime, but `Shader.Find` only resolves shaders referenced from a shipped Material or listed in *Project Settings → Graphics → Always Included Shaders* — so the shader was stripped from builds and `new Material(null)` produced magenta tiles. Replaced with a new `[SerializeField] protected Material _defaultTileMaterial` on `MapboxMapBehaviour`; the asset reference keeps the shader alive in builds. Demo scenes (`LocationExample.unity`, `ApiTest.unity`) ship with this field wired to `ElevatedTerrainMaterial.mat`. If both `_tileCreatorBehaviour` and `_defaultTileMaterial` are null, an `InvalidOperationException` is thrown with actionable instructions. - Fixed `UnityTileProviderSettings.SubdivisionBias` defaulting to `0` in scenes saved before the field existed — Unity assigns `default(float)` for missing serialized fields and does NOT run the C# field initializer in that case, which collapsed `distToSplit` and forced the tile provider to render only the `MinimumZoomLevel` tile. New C# default is `0.6f`; `ISerializationCallbackReceiver.OnAfterDeserialize` upgrades legacy serialized `<= 0` values to `0.6f`. **Other:** diff --git a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs index 37214b19c..1cc4a2f23 100644 --- a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs @@ -24,6 +24,8 @@ public class MapboxMapBehaviour : MapBehaviourCore public UnityContext UnityContext; [SerializeField] protected TileCreatorBehaviour _tileCreatorBehaviour; + [Tooltip("Material used when no TileCreatorBehaviour is assigned. Asset reference keeps the shader alive in player builds. If you swap in a TileCreatorBehaviour, this field is ignored.")] + [SerializeField] protected Material _defaultTileMaterial; [SerializeField] protected TileProviderBehaviour TileProvider; [SerializeField] protected DataFetchingManagerBehaviour DataFetcher; [SerializeField] protected MapboxCacheManagerBehaviour CacheManager; @@ -123,22 +125,18 @@ protected virtual MapboxMapVisualizer CreateMapVisualizer(IMapInformation mapInf else { // Fallback when no TileCreatorBehaviour is assigned on the GameObject. - // In a build, Shader.Find only resolves shaders that are referenced by a - // shipped Material asset or listed in Project Settings → Graphics → Always - // Included Shaders. The Mapbox terrain shader is neither by default, so - // this code path silently produced a magenta tile material. Fail loudly - // with an actionable error instead. - var shader = Shader.Find(Constants.Map.DefaultTerrainShaderName); - if (shader == null) + // The Material is taken from a direct asset reference (_defaultTileMaterial) + // so its shader survives player-build shader stripping — Shader.Find can't + // resolve shaders that aren't referenced from a shipped Material or listed + // in Project Settings → Graphics → Always Included Shaders. + if (_defaultTileMaterial == null) { throw new InvalidOperationException( - $"MapboxMapBehaviour on '{name}' has no TileCreator assigned and could not locate the default shader " + - $"'{Constants.Map.DefaultTerrainShaderName}' at runtime. " + - "Assign a TileCreatorBehaviour component with a configured Material, or add the Mapbox terrain shader to " + - "Project Settings → Graphics → Always Included Shaders so it ships in player builds."); + $"MapboxMapBehaviour on '{name}' has no TileCreator assigned and no default tile Material set. " + + "Assign a TileCreatorBehaviour on this GameObject, or drag a Material (e.g. ElevatedTerrainMaterial) " + + "into the 'Default Tile Material' field on MapboxMapBehaviour."); } - var defaultMapboxTerrainMaterial = new Material(shader); - tileCreator = new TileCreator(unityContext, new[] { defaultMapboxTerrainMaterial }); + tileCreator = new TileCreator(unityContext, new[] { _defaultTileMaterial }); } return new MapboxMapVisualizer(mapInfo, unityContext, tileCreator); } diff --git a/Runtime/Mapbox/Example/Scripts/Test/ApiTest.unity b/Runtime/Mapbox/Example/Scripts/Test/ApiTest.unity index cc0b5a929..ba85225e5 100644 --- a/Runtime/Mapbox/Example/Scripts/Test/ApiTest.unity +++ b/Runtime/Mapbox/Example/Scripts/Test/ApiTest.unity @@ -658,6 +658,7 @@ MonoBehaviour: CoroutineStarter: {fileID: 1316629399} Root: {fileID: 1316629400} TileMaterials: [] + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} InitializeOnStart: 1 --- !u!4 &1316629400 Transform: diff --git a/Runtime/Mapbox/LocationModule/Example/LocationExample.unity b/Runtime/Mapbox/LocationModule/Example/LocationExample.unity index e94d0b173..54e9005f7 100644 --- a/Runtime/Mapbox/LocationModule/Example/LocationExample.unity +++ b/Runtime/Mapbox/LocationModule/Example/LocationExample.unity @@ -502,6 +502,7 @@ MonoBehaviour: - {fileID: 2100000, guid: 07844c7dc8ee041a398536fea1b205f0, type: 2} - {fileID: 2100000, guid: 0ed89629241ea4f1fb43ce963c79adc6, type: 2} BuildingMaterial: {fileID: 2100000, guid: 8eb65dc9490a746018cc3aa696f93965, type: 2} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} InitializeOnStart: 1 --- !u!4 &1316629400 Transform: From 1b044df5602a992c750bf57ea0c39cc66e10f88c Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Thu, 14 May 2026 16:26:53 +0300 Subject: [PATCH 40/54] Surface _defaultTileMaterial in MapboxMapBehaviour custom Inspector The custom editor draws each field explicitly, so the new field added in 428189fe wasn't visible. Wired it up in OnEnable and rendered right after Tile Creator Behaviour in the Override Modules section. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DirectionsApiDemo/DirectionsDemo.unity | 3 ++- .../Editor/MapboxMapBehaviourEditor.cs | 3 +++ .../GeocodingApiDemo/GeocodingDemo.unity | 3 ++- Samples~/.DS_Store | Bin 6148 -> 10244 bytes Samples~/LocationBasedGame/.DS_Store | Bin 8196 -> 10244 bytes Samples~/LocationBasedGame/LocationGame.unity | 3 ++- .../LocationGameWithPois.unity | 3 ++- Samples~/MapboxComponents/Map.unity | 3 ++- Samples~/WorldMapViewer/Map.unity | 1 + .../BaseModule/Scenes/WorldMapScene.unity | 3 ++- 10 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/DirectionsDemo.unity b/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/DirectionsDemo.unity index 5199689e4..ad89ad525 100644 --- a/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/DirectionsDemo.unity +++ b/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/DirectionsDemo.unity @@ -910,7 +910,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 1316629400} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1316629398} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 2054936219} DataFetcher: {fileID: 2054936220} CacheManager: {fileID: 0} diff --git a/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs b/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs index 50b7b8caf..47ea335c4 100644 --- a/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs +++ b/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs @@ -11,6 +11,7 @@ public class MapboxMapBehaviourEditor : UnityEditor.Editor private SerializedProperty _unityContextProp; private SerializedProperty _tileCreatorProp; + private SerializedProperty _defaultTileMaterialProp; private SerializedProperty _tileProviderProp; private SerializedProperty _dataFetcherProp; private SerializedProperty _cacheManagerProp; @@ -28,6 +29,7 @@ private void OnEnable() _unityContextProp = serializedObject.FindProperty("UnityContext"); _tileCreatorProp = serializedObject.FindProperty("_tileCreatorBehaviour"); + _defaultTileMaterialProp = serializedObject.FindProperty("_defaultTileMaterial"); _tileProviderProp = serializedObject.FindProperty("TileProvider"); _dataFetcherProp = serializedObject.FindProperty("DataFetcher"); _cacheManagerProp = serializedObject.FindProperty("CacheManager"); @@ -95,6 +97,7 @@ public override void OnInspectorGUI() if (!_overrideModulesFold) { EditorGUILayout.PropertyField(_initializeOnStart, new GUIContent("Initialize On Start")); + EditorGUILayout.PropertyField(_defaultTileMaterialProp, new GUIContent("Default Tile Material")); } } diff --git a/Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo/GeocodingDemo.unity b/Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo/GeocodingDemo.unity index 5166e51cd..755b88176 100644 --- a/Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo/GeocodingDemo.unity +++ b/Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo/GeocodingDemo.unity @@ -1449,7 +1449,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 1316629400} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1316629398} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 2054936219} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} diff --git a/Samples~/.DS_Store b/Samples~/.DS_Store index e74ac8166c02c8e84cf541df19b7b98e21131cc7..5722b7afeffe363063848ee9915c4ea525a92f94 100644 GIT binary patch literal 10244 zcmeHM&2Jk;6o1bR#3o6*`J^czt?&h@R`?w^bV|%}6uv?aaLS`0f7Q z_>BPob9+_>KnH+0Wg)deDjt$VyU522Q6d)N5ao8?nisZUmAP|&PP{ZQ zICT1qHas#qJT^Q&K0fjC*~yVtUM*;&rcvB2d15#7+~Ho%*<%Imj8B*>7P-6Zv3hL9e19#yk_!aKM-|zs3Z~|Y)^O(X}oWm?$#uePe8))E7X&-BR2(LxsMFM?N z9_d5a&wH-J-4fY}F^7qSoJeL6&1}+K$dT?7@hrGPS{Ya?^=KEZH11)9Cc=H+`1qa~ z;U;Uf>9=Q6`gA7Kjd6lelsYo*b875itCnAMO!5_CtRA{Q4lU1}>p;uqFZlT-ccKD>CLy0mB$ riS%qF>rzq;rDy(UK$puB75_i!g`A52yIsb0iWccQu{BzJ%JKid%W5dv delta 169 zcmZn(XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50D9Q>M0|%s^9tK!6)axPolkSoocJGQWx^$ZiHEh&w<|W7r(eGlv-fT|yq@ diff --git a/Samples~/LocationBasedGame/.DS_Store b/Samples~/LocationBasedGame/.DS_Store index 3c24ee1491cca4c640f3af4240a989d6452b4f69..df8238fd85f79efc17977e543fa066e8fede7dc3 100644 GIT binary patch delta 653 zcmZp1XbF&DU|?W$DortDU{C-uIe-{M3-C-V6q~50$SAWhU^hRb%;ZEtmB|JI-i$jZ z%Ze0gNLE)HTbk%77#kYa>L^qj8k(5vD3}=;)z)%yh$`z_2gPUSwh=DJhwF0pX;?=lq=fBBz|v)adl0%#`4Q#N^Dp^oWwwijv^Ws?>M^ zAvC2ysVQ+#p?aY9kjes}A`vu2?nSAod0+$m)6$AlOCpfik?{ij#mPnaIXMCO#hE3U z`FW8s#TohKo+YWdo_T5cKr72KQ_KAeKvKnFsYS&=8beP+WMWZ$QDRPA$j?Eshph34gnx;3=nfLXfrr6_%ehsWHVGTOk|kLu##an!wH7F4DT6! zGO{yDGAc1@Gnz2kGTJkGFnTgZF(xx+FlI7lfz4xJVua8PvQQdIF&1x56kNu(v4M$A skQpMTzzw8bK}l(2;dkcA{4#+eOppWy_5#o#1CYYW1u_#Bvk5Z;09FK}F#rGn delta 129 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD7`UYH$S8FiV1$^> TFgc!Q#^ht7qML=q-!lOKa2FcP diff --git a/Samples~/LocationBasedGame/LocationGame.unity b/Samples~/LocationBasedGame/LocationGame.unity index 78c7b2569..d09f609d8 100644 --- a/Samples~/LocationBasedGame/LocationGame.unity +++ b/Samples~/LocationBasedGame/LocationGame.unity @@ -1643,7 +1643,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 0} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1377339556045437849} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 8067237297740704311} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} diff --git a/Samples~/LocationBasedGame/LocationGameWithPois.unity b/Samples~/LocationBasedGame/LocationGameWithPois.unity index 4d90b7bb0..f9af243de 100644 --- a/Samples~/LocationBasedGame/LocationGameWithPois.unity +++ b/Samples~/LocationBasedGame/LocationGameWithPois.unity @@ -1643,7 +1643,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 0} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1316629405} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 8067237297740704311} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} diff --git a/Samples~/MapboxComponents/Map.unity b/Samples~/MapboxComponents/Map.unity index 29e63695e..a322e0603 100644 --- a/Samples~/MapboxComponents/Map.unity +++ b/Samples~/MapboxComponents/Map.unity @@ -540,7 +540,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 1808235830} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1316629407} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: aaf04558f9b104d2ebc4e1c86464fc1c, type: 2} TileProvider: {fileID: 0} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} diff --git a/Samples~/WorldMapViewer/Map.unity b/Samples~/WorldMapViewer/Map.unity index 553dc6fab..9d8d116b2 100644 --- a/Samples~/WorldMapViewer/Map.unity +++ b/Samples~/WorldMapViewer/Map.unity @@ -671,6 +671,7 @@ MonoBehaviour: BaseTileRoot: {fileID: 1808235830} RuntimeGenerationRoot: {fileID: 0} _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 2054936219} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} diff --git a/Tests/Runtime/BaseModule/Scenes/WorldMapScene.unity b/Tests/Runtime/BaseModule/Scenes/WorldMapScene.unity index 6c3820ede..45873c231 100644 --- a/Tests/Runtime/BaseModule/Scenes/WorldMapScene.unity +++ b/Tests/Runtime/BaseModule/Scenes/WorldMapScene.unity @@ -583,7 +583,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 0} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1316629406} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 0} DataFetcher: {fileID: 0} CacheManager: {fileID: 2143662170} From 2121142ca1d09d1c238c82ed6fe37475a9ae1077 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Thu, 14 May 2026 22:29:03 +0300 Subject: [PATCH 41/54] Fix Android SQLite cache breakage under Medium+ managed stripping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every tile insert was failing on Android IL2CPP builds with Managed Stripping Level set to Medium or High, producing a constant stream of "Error inserting … (extended=ConstraintForeignKey)" log lines. Root cause: sqlite-net writes row values back to model class properties via PropertyInfo.SetValue (i.e., reflection). IL2CPP's Medium+ stripping removes auto-property accessor methods (set_id, set_name, etc.) on the cache model classes because static analysis doesn't see them used (sqlite-net's reflection isn't visible to the analyzer). PropertyInfo.SetValue then becomes a silent no-op, every row maps to id=0, and downstream `tiles` inserts fail FK on `tile_set`. Fixes: - Add [UnityEngine.Scripting.Preserve] to every persisted property on the four SQLite model types (tiles, tilesets, offlineMaps, tile2offline) — [Preserve] on the class alone doesn't preserve members in Unity's linker. - Add the same types to Plugins/link.xml as a defensive secondary preservation path. - Append SQLite's extended error code to the catch-site log so future cache-side failures (UNIQUE / FK / NotNull / etc.) are diagnosable from the first line. Map rendering was unaffected (memory cache covered) but the SQLite disk cache wasn't actually persisting on Android with non-Minimal stripping. Verified working on Android with Medium stripping. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + .../Platform/Cache/SQLiteCache/SqliteCache.cs | 3 +- .../Data/Platform/Cache/SQLiteCache/Tiles.cs | 30 +++++++++++++++---- .../Platform/Cache/SQLiteCache/Tilesets.cs | 10 +++++-- Runtime/Mapbox/BaseModule/Plugins/link.xml | 13 ++++++++ 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a96c2956..c5b6e3c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -209,6 +209,7 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed vector layer range limits not being applied correctly during data processing. - Fixed incorrect limit validation and bounds checking in the `LatitudeLongitude` struct. - Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. +- Fixed Android IL2CPP player builds with **Medium** or **High** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting on Android with non-Minimal stripping. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. #### Known limitations diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs index e5dedea8d..ceae022f1 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs @@ -228,7 +228,8 @@ public void SyncAdd(string tilesetName, CanonicalTileId tileId, byte[] data, str } catch (Exception ex) { - Debug.LogErrorFormat("Error inserting {0} {1} {2} ", tilesetName, tileId, ex); + var extended = (ex is SQLiteException) ? SQLite3.ExtendedErrCode(_sqlite.Handle).ToString() : "n/a"; + Debug.LogErrorFormat("Error inserting {0} {1} (extended={2}) {3} ", tilesetName, tileId, extended, ex); } // update counter only when new tile gets inserted diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tiles.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tiles.cs index d6dbc6091..87d964127 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tiles.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tiles.cs @@ -1,60 +1,78 @@ using System; using Mapbox.BaseModule.Data.Platform.SQLite; +using UnityEngine.Scripting; namespace Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache { /// - /// Don't change the class name: sqlite-net uses it for table creation + /// Don't change the class name: sqlite-net uses it for table creation. + /// [Preserve] forces IL2CPP managed-code-stripping to keep the property + /// accessors that sqlite-net invokes via reflection. /// + [Preserve] public class tiles { - [PrimaryKey, AutoIncrement] + [PrimaryKey, AutoIncrement, Preserve] public int id { get; set; } - + + [Preserve] public int tile_set { get; set; } //hrmpf: multiple PKs not supported by sqlite.net //https://github.com/praeclarum/sqlite-net/issues/282 //TODO: do it via plain SQL //[PrimaryKey] + [Preserve] public int zoom_level { get; set; } //[PrimaryKey] + [Preserve] public long tile_column { get; set; } //[PrimaryKey] + [Preserve] public long tile_row { get; set; } + [Preserve] public byte[] tile_data { get; set; } + [Preserve] public string tile_path { get; set; } - + /// Unix epoch for simple FIFO pruning + [Preserve] public int timestamp { get; set; } /// ETag Header value of the reponse for auto updating cache + [Preserve] public string etag { get; set; } /// Expiration date of cached data + [Preserve] public int? expirationDate { get; set; } - + [Ignore] public DateTime expirationDateFormatted { get; set; } } + [Preserve] public class offlineMaps { - [PrimaryKey, AutoIncrement] + [PrimaryKey, AutoIncrement, Preserve] public int id { get; set; } + [Preserve] public string name { get; set; } } + [Preserve] public class tile2offline { + [Preserve] public int tileId { get; set; } + [Preserve] public int mapId { get; set; } } } diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tilesets.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tilesets.cs index 88faecd83..9cff1d5cc 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tilesets.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tilesets.cs @@ -1,20 +1,26 @@ using Mapbox.BaseModule.Data.Platform.SQLite; +using UnityEngine.Scripting; namespace Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache { /// - /// Don't change the class name: sqlite-net uses it for table creation + /// Don't change the class name: sqlite-net uses it for table creation. + /// [Preserve] forces IL2CPP managed-code-stripping to keep the property + /// accessors that sqlite-net invokes via reflection — without it, set_id is + /// stripped on Android Medium/High stripping and every row maps to id=0. /// + [Preserve] public class tilesets { //hrmpf: multiple PKs not supported by sqlite.net //https://github.com/praeclarum/sqlite-net/issues/282 //TODO: do it via plain SQL - [PrimaryKey, AutoIncrement] + [PrimaryKey, AutoIncrement, Preserve] public int id { get; set; } + [Preserve] public string name { get; set; } } } diff --git a/Runtime/Mapbox/BaseModule/Plugins/link.xml b/Runtime/Mapbox/BaseModule/Plugins/link.xml index d316281b6..ba5a20d7c 100644 --- a/Runtime/Mapbox/BaseModule/Plugins/link.xml +++ b/Runtime/Mapbox/BaseModule/Plugins/link.xml @@ -1,4 +1,17 @@ + + + + + + + From f74d12e95f441cfd7e5d42baddf42f12b4412565 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Thu, 14 May 2026 22:48:18 +0300 Subject: [PATCH 42/54] Add Overview.md and trim CHANGELOG's premature limitations list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overview doc lives at Documentation~/Overview.md and is intended as the landing page for the SDK: version, supported Unity editor versions, supported build platforms (iOS and Android only — Standalone, WebGL, console, VisionOS, etc. are explicitly out of scope), required and optional dependencies, Android Managed Stripping notes, module table, samples table, and an index of the other docs in Documentation~/. The v3.1.0 CHANGELOG had a "Known limitations" section listing items that were actually unfinished work, not real shipping limitations (touch-rotate, async PhysX bake validation, shader-graph visual verification, GPU instancing). Removed it from CHANGELOG.md and the cross-reference from Overview.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 8 -- Documentation~/Overview.md | 173 +++++++++++++++++++++++++++++++++++++ README.md | 12 +-- 3 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 Documentation~/Overview.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c5b6e3c65..cfc3dc2ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,14 +211,6 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. - Fixed Android IL2CPP player builds with **Medium** or **High** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting on Android with non-Minimal stripping. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. -#### Known limitations - -- **Touch path can't be tested in the Unity Editor.** The mouse/touch handler split is compile-time on build target (`(UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR`). To debug touch logic without a device, temporarily remove the `&& !UNITY_EDITOR` clause in `MapInput.cs` and use Unity Remote. -- **Touch rotate is not supported.** Mouse-only via right-click drag. Documented in `Documentation~/CameraSystem.md`. A two-finger twist gesture is not implemented. -- **`asyncBakeCollider = true` has not been platform-validated on iOS/Android.** The PhysX bake runs on a worker thread; no explicit upload-completion barrier between `Mesh.SetVertices`/`SetIndices` and the worker. Recommended: Frame Debugger pass on a target device before relying on async-bake in production. -- **Shader graph shrinkage is unverified visually.** `ElevatedTerrainShader.shadergraph` and `MapboxTerrainBilinearSampling.shadersubgraph` were reduced by ~280 / ~242 lines respectively. A `Normalize` was removed downstream of the gradient — if not load-bearing, fine; if load-bearing, lighting magnitude scales with slope length. Side-by-side visual diff recommended. -- **GPU instancing for terrain is not enabled.** The shared-flat-mesh + MPB pattern saves per-tile `Material` clone allocations but does *not* enable SRP Batcher batching (MPB disqualifies a renderer) and instancing variants are off in the materials. Terrain draws are still N separate draws. See `MEMORY.md` → `project_terrain_instancing` for the future-work plan. - ### v3.0.6 #### Fixes diff --git a/Documentation~/Overview.md b/Documentation~/Overview.md new file mode 100644 index 000000000..991e9015d --- /dev/null +++ b/Documentation~/Overview.md @@ -0,0 +1,173 @@ +# Mapbox Unity SDK — Overview + +Map rendering, location services, directions, geocoding, and 3D landmark features for Unity, distributed as a Unity Package Manager (UPM) package. + +| | | +| --- | --- | +| **Package name** | `com.mapbox.sdk` | +| **Current version** | `3.1.0` | +| **Vendor** | Mapbox — | +| **Minimum Unity** | `2022.3` LTS | +| **Render pipeline** | Universal Render Pipeline (URP) — required | + +For full release notes see [`CHANGELOG.md`](../CHANGELOG.md). For installation and demo-scene walkthrough see [`README.md`](../README.md). + +--- + +## Supported Unity editor versions + +The package's `package.json` declares `unity: "2022.3"` as the minimum. + +| Editor version | Status | Notes | +| --- | --- | --- | +| **2022.3 LTS** | Primary target. Android build-settings folder shipped: `Runtime/AndroidBuildSettings/2022.3.38f1/`. | +| **2021.3 LTS** | Best-effort. Android build-settings folder shipped: `Runtime/AndroidBuildSettings/2021.3.41f1/`. Not the primary target — use 2022.3+ when possible. | +| **6000.x (Unity 6)** | Best-effort. Android build-settings folder shipped: `Runtime/AndroidBuildSettings/6000/`. | +| **2023.x non-LTS** | Untested. Should work but no shipped Android settings folder. | +| **< 2021.3** | Unsupported. | + +When building for Android, copy the `Plugins/` folder from the matching (or closest lower) editor version's subfolder under `Runtime/AndroidBuildSettings/` into your project's `Assets/`. See [`README.md`](../README.md#building-your-map-application-for-android) for the full procedure. + +--- + +## Supported build platforms + +Only **iOS** and **Android** are supported build targets. Use the Unity Editor (Windows or macOS) for development and Play-mode testing; all other build targets (Standalone, WebGL, console platforms, VisionOS, etc.) are not supported. + +| Platform | Status | Notes | +| --- | --- | --- | +| **iOS** | Supported | XCFrameworks shipped under `Runtime/Mapbox/BaseModule/Plugins/iOS/` (MapboxCommon, Turf). ARM64 device + simulator. IL2CPP required. | +| **Android** | Supported | Native libs under `Runtime/Mapbox/BaseModule/Plugins/sqlite/Android/libs/` for `arm64-v8a`, `armeabi-v7a`, and `x86`. **See [Android build settings](#android-build-settings) below** for required project-side configuration. | +| **Unity Editor (Play mode)** | Supported on Windows and macOS for development and testing. Not a shipping target. | +| **All other platforms** | Not supported. Includes Standalone (Windows/macOS/Linux), WebGL, console platforms, and VisionOS. | + +--- + +## Required dependencies + +These are declared in `package.json` and resolved automatically by UPM: + +| Package | Version | Used for | +| --- | --- | --- | +| `com.unity.render-pipelines.universal` | `10.10.1` | Terrain and building shaders ship as URP Shader Graphs. | +| `com.unity.nuget.newtonsoft-json` | `3.2.1` | JSON parsing for tile metadata, geocoding/directions API responses. | +| `com.unity.burst` | `1.8.12` | Burst-compiled terrain-RGB decode and collider vertex-fill jobs. First-time domain reload pays a one-shot AOT compile cost. | + +## Optional dependencies + +| Package | Behaviour if installed | Behaviour if absent | +| --- | --- | --- | +| `com.unity.inputsystem` | Camera input uses `UnityEngine.InputSystem` (new Input System). The `MAPBOX_NEW_INPUT_SYSTEM` define is auto-set via `versionDefines` in `MapboxExamples.asmdef`. | Camera input falls back to legacy `UnityEngine.Input`. No project-side action required either way. See [`CameraSystem.md`](CameraSystem.md). | + +--- + +## Android build settings + +### Managed Stripping Level + +The SQLite tile cache uses sqlite-net, which populates row values via `PropertyInfo.SetValue` reflection. IL2CPP's managed-code stripping at **Medium / High** strips auto-property accessor methods that aren't seen as called via static analysis, breaking the row mapper. + +**As of v3.1.0** the four SQLite model types (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every persisted property carry `[UnityEngine.Scripting.Preserve]`, and `Plugins/link.xml` declares them as a backup preservation path. So the SDK *should* work at any stripping level. + +If you still observe `Error inserting … (extended=ConstraintForeignKey)` in logcat on an Android device build: + +1. Set **Project Settings → Player → Android tab → Other Settings → Optimization → Managed Stripping Level** to **Minimal** or **Disabled**. +2. Report the issue — the `[Preserve]` and `link.xml` combination should have covered it. + +When the cache is broken, map rendering still works (the in-memory cache covers) but tiles are silently re-fetched every session instead of being served from disk. + +### Required Android plugin folder + +For Android builds, copy `Runtime/AndroidBuildSettings//Plugins/` into your project's `Assets/`. This ships gradle templates and manifest entries the SDK relies on at build time. + +### Permissions + +Location features require: +- `android.permission.ACCESS_FINE_LOCATION` +- `android.permission.ACCESS_COARSE_LOCATION` + +Runtime permission prompts are handled by `Mapbox.BaseModule.Plugins.Android.UniAndroidPermission`. + +--- + +## iOS build settings + +| Setting | Value | +| --- | --- | +| **Scripting Backend** | IL2CPP (required) | +| **Minimum iOS Version** | 11.0+ recommended | +| **Architecture** | ARM64 | +| **Location permission** | Set `NSLocationWhenInUseUsageDescription` (or `NSLocationAlwaysUsageDescription`) in Info.plist before requesting location. | + +XCFrameworks (`MapboxCommon`, `Turf`) ship multi-slice and are picked up automatically by Unity's plugin importer. + +--- + +## Modules + +The SDK is organized into module assemblies. Each is a separate `.asmdef` and can be referenced or excluded independently: + +| Assembly | Path | Responsibility | +| --- | --- | --- | +| `MapboxBaseModule` | `Runtime/Mapbox/BaseModule/` | Tile management, caching (memory/file/SQLite), data fetching, coordinate conversions. Foundation for every other module. | +| `UnityMapModule` | `Runtime/Mapbox/UnityMapService/` | Unity-specific map service (`MapUnityService`), quadtree tile provider. | +| `MapboxLocationModule` | `Runtime/Mapbox/LocationModule/` | GPS / compass via `AbstractLocationProvider`. Unity + static (editor) implementations. | +| `MapboxImageModule` | `Runtime/Mapbox/ImageModule/` | Raster tile imagery (satellite, streets). Terrain strategies (flat / elevated). | +| `MapboxVectorModule` | `Runtime/Mapbox/VectorModule/` | Vector tile rendering — buildings, roads, areas. Modifier-stack-based mesh generation. Component System. | +| `MapboxCustomImageryModule` | `Runtime/Mapbox/CustomImageryModule/` | Custom tileset support (non-Mapbox sources via TMS or arbitrary URL templates). | +| `MapboxDirections` | `Runtime/Mapbox/DirectionsApi/` | Directions API wrapper. | +| `MapboxGeocoding` | `Runtime/Mapbox/GeocodingApi/` | Forward / reverse geocoding API wrapper. | +| `MapboxDebug` | `Runtime/Mapbox/MapDebug/` | Debug logging + benchmarking pipelines. | +| `MapboxExamples` | `Runtime/Mapbox/Example/` | Camera behaviours, input handling, demo support scripts. | + +Editor-only counterparts exist for several modules (e.g. `MapboxBaseModule.Editor`, `MapboxImageModule.Editor`). + +--- + +## Samples + +Imported via Package Manager → select the SDK → **Samples** tab → **Import**. Five samples ship: + +| Sample | Path | What it demonstrates | +| --- | --- | --- | +| **WorldMapViewer** | `Samples~/WorldMapViewer` | Minimal map setup — pan/zoom an interactive world map. | +| **LocationBasedGame** | `Samples~/LocationBasedGame` | Device GPS → in-scene avatar movement. The classic Pokemon-Go-style starting point. | +| **MapboxComponents** | `Samples~/MapboxComponents` | Component-system-based layer rendering (Buildings + Areas + Roads). | +| **DirectionsApiDemo** | `Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo` | Calling Directions API + rendering a route on the map. | +| **GeocodingApiDemo** | `Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo` | Forward and reverse geocoding. | + +--- + +## Other documentation + +Located in `Documentation~/`: + +- [`GettingStartedWithMapboxMapObject.md`](GettingStartedWithMapboxMapObject.md) — first-time setup and the core `MapboxMap` object. +- [`AccessingTheMapObject.md`](AccessingTheMapObject.md) — getting a reference to the map at runtime. +- [`WorkingWithMapObject.md`](WorkingWithMapObject.md) — runtime API. +- [`WorkingWithModules.md`](WorkingWithModules.md) — module composition pattern. Includes architecture diagrams. +- [`ChangingMapLocation.md`](ChangingMapLocation.md) — moving the map to a new lat/lon. +- [`ChangeImageryStyleOnRuntime.md`](ChangeImageryStyleOnRuntime.md) — switching tile styles at runtime. +- [`CameraSystem.md`](CameraSystem.md) — camera behaviours, touch/mouse input, Input System soft-dependency model. +- [`CoordinateConversions.md`](CoordinateConversions.md) — lat/lon ↔ Mercator ↔ Unity world space. +- [`VectorLayerModule.md`](VectorLayerModule.md) — building/road/area visualizers, modifier stacks. +- [`ComponentsModule.md`](ComponentsModule.md) — the newer component-based vector pipeline. +- [`WorkingWithPois.md`](WorkingWithPois.md) — point-of-interest features. +- [`UsingDirectionsApi.md`](UsingDirectionsApi.md) / [`UsingGeocodingApi.md`](UsingGeocodingApi.md) / [`UsingMapMatchingApi.md`](UsingMapMatchingApi.md) — API wrappers. + +--- + +## Access token + +Every Mapbox API call (tiles, geocoding, directions) requires a Mapbox access token. + +- Set it via the editor menu: **Mapbox → Setup**. +- Stored at `Assets/Resources/Mapbox/MapboxConfiguration.txt` (read at runtime via `Resources.Load`). +- An SKU token is appended automatically to authenticated requests. +- Get a token at . + +--- + +## Reporting issues / contributing + +This package is maintained by Mapbox internally. Issues and feature requests should be reported through your standard Mapbox support channel. diff --git a/README.md b/README.md index 53a0dc296..102dd938a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -##Mapbox Unity SDK 3.0 Quickstart guide +## Mapbox Unity SDK 3.1.0 Quickstart guide + +For a complete overview of the package — supported Unity versions, build platforms, dependencies, and module breakdown — see [`Documentation~/Overview.md`](Documentation~/Overview.md). For release notes see [`CHANGELOG.md`](CHANGELOG.md). ### Installing the Package -1. **Get the Mapbox Unity SDK v3.0** - - Clone the Mapbox Unity SDK v3.0 branch to local. +1. **Get the Mapbox Unity SDK v3.1.0** + - Clone the Mapbox Unity SDK repository to local. 2. **Open Unity Package Manager (UPM)** - Open your Unity project and navigate to the Unity Package Manager. @@ -48,8 +50,8 @@ The integration process depends heavily on your project setup and how you intend to use the map. Below are steps to help you get started with a location-based game setup: -1. **Import the Location-Based Game Sample** - - Navigate to `Samples/Mapbox Unity SDK/3.0.0/Location Based Game`. +1. **Import the Location-Based Game Sample** + - Open the Package Manager, select the Mapbox Unity SDK, go to the **Samples** tab, and hit **Import** next to **LocationBasedGame**. 2. **Use the `LocationBasedMap` Prefab** - Add the `LocationBasedMap` prefab to any scene, and it should work. From 844164ac2d06f3423fccab468595d1b262a697b7 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Fri, 15 May 2026 13:12:58 +0300 Subject: [PATCH 43/54] v3.1.0 slot-ins: migration guide, MapRoot localPosition, tests, README merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five small additions identified by the state-of-the-SDK review as low-risk / high-leverage for v3.1.0 itself rather than deferring to v3.2: (1) MapRoot.transform fix. MapboxMapVisualizer was positioning tiles via transform.position (world space), silently resetting any parent transform on the MapRoot / BaseTileRoot hierarchy on every tile cover update. Switched to localPosition. Behavior is identical on every existing scene (MapRoot at world origin), but AR/MR setups that anchor MapRoot under an ARAnchor or custom rig now compose correctly under translation + rotation. Non-unit MapRoot.localScale is still unsupported — guidance is to drive scale through MapInformation.Scale. (2) Migration guide. New Documentation~/Migration-3.0-to-3.1.md walks through every source-level break, every behavioral change, and a step-by-step verification checklist for upgrading from v3.0. (3) "Place a GameObject at lat/lon" recipe added to GettingStartedWithMapboxMapObject.md — the most common indie use case, previously unaddressed in the docs. (4) First test coverage for v3.1.0 surface. Editor-mode unit tests under Tests/Editor/BaseModule/ for ElevationArrayPool (rent / return / pool cap / null safety), TerrainInfo (defaults), and MapInformation (Initialize resets Terrain bounds, sets lat/lon, idempotent). More coming in v3.2. (5) README rewrite + Overview.md merge. README now serves as the single landing page: quick-facts header, supported Unity / build targets, dependencies, install, token, demo scenes, location workflows, iOS / Android build sections (with the v3.1.0 stripping notes), modules table, full docs index. Overview.md removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 9 + .../GettingStartedWithMapboxMapObject.md | 53 ++++ Documentation~/Migration-3.0-to-3.1.md | 169 ++++++++++++ Documentation~/Overview.md | 173 ------------ README.md | 252 ++++++++++++------ .../BaseModule/Map/MapboxMapVisualizer.cs | 9 +- .../BaseModule/ElevationArrayPoolTests.cs | 88 ++++++ .../Editor/BaseModule/MapInformationTests.cs | 55 ++++ Tests/Editor/BaseModule/TerrainInfoTests.cs | 26 ++ 9 files changed, 575 insertions(+), 259 deletions(-) create mode 100644 Documentation~/Migration-3.0-to-3.1.md delete mode 100644 Documentation~/Overview.md create mode 100644 Tests/Editor/BaseModule/ElevationArrayPoolTests.cs create mode 100644 Tests/Editor/BaseModule/MapInformationTests.cs create mode 100644 Tests/Editor/BaseModule/TerrainInfoTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc3dc2ca..962649187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -210,6 +210,15 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed incorrect limit validation and bounds checking in the `LatitudeLongitude` struct. - Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. - Fixed Android IL2CPP player builds with **Medium** or **High** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting on Android with non-Minimal stripping. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. +- Fixed `MapboxMapVisualizer` writing `tile.transform.position` (world space) on every tile cover update, which silently reset any parent transformation on the `MapRoot` / `BaseTileRoot` hierarchy. Tiles are now positioned via `transform.localPosition`, so translating or rotating the map's root (e.g. anchoring it to an `ARAnchor`) is honored. For unchanged scenes — `MapRoot` at world origin with identity rotation — behavior is identical. Non-unit `MapRoot.localScale` is still unsupported; drive map scale through `MapInformation.Scale` instead. + +#### Documentation +- New [`Documentation~/Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) walks through every source-level break, every behavioral change, and step-by-step verification when upgrading from v3.0. +- `README.md` rewritten and now serves as the single landing page (Overview.md merged in and removed). Adds a quick-facts header table, dependency listing, demo-scenes table, iOS / Android build sections with the v3.1.0 stripping notes, modules table, and a complete documentation index. +- `Documentation~/GettingStartedWithMapboxMapObject.md` gained a "Place a GameObject at a latitude / longitude" recipe — the most commonly asked indie use case (drop a marker / character / POI at specific coordinates), with the correct `localPosition` + `MapRoot` parenting pattern and the optional `TryGetElevation` step. + +#### Tests +- Added editor-mode tests for `ElevationArrayPool` (rent / return / pool cap / null safety), `TerrainInfo` (defaults), and `MapInformation` (Initialize resets terrain bounds, lat/lon, idempotency). First test coverage for v3.1.0 surface — more coming in v3.2. ### v3.0.6 diff --git a/Documentation~/GettingStartedWithMapboxMapObject.md b/Documentation~/GettingStartedWithMapboxMapObject.md index 0abd64423..75a55a2a6 100644 --- a/Documentation~/GettingStartedWithMapboxMapObject.md +++ b/Documentation~/GettingStartedWithMapboxMapObject.md @@ -88,3 +88,56 @@ To create a basic map in your own Unity scene, follow these simple steps: --- + +## Common Tasks + +### Place a GameObject at a latitude / longitude + +To position your own object (a marker, a character avatar, a custom POI) at a specific geographic coordinate, use `IMapInformation.ConvertLatLngToPosition`: + +```csharp +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.Example.Scripts.Map; +using UnityEngine; + +public class PlaceMarker : MonoBehaviour +{ + public MapboxMapBehaviour MapBehaviour; + public Transform Marker; + + private void Start() + { + // Wait until the map is ready, then place the marker. + MapBehaviour.MapboxMap.Initialized += OnMapReady; + } + + private void OnMapReady() + { + var coord = new LatitudeLongitude(40.7484, -73.9857); // Empire State Building + var localPos = MapBehaviour.MapboxMap.MapInformation.ConvertLatLngToPosition(coord); + + // Parent under the map root so the marker composes with any transform applied + // to the map hierarchy (rotation, translation, AR anchor, etc.). + Marker.SetParent(MapBehaviour.UnityContext.MapRoot, worldPositionStays: false); + Marker.localPosition = localPos; + } +} +``` + +**Important**: the value `ConvertLatLngToPosition` returns is in the **map's local coordinate system**, not world space. Always assign it to `localPosition` of a Transform parented under `UnityContext.MapRoot`, never to `transform.position`. This composes correctly with any parent transformations (useful for AR/MR scenarios where the map is anchored to a real-world surface). + +For optional **terrain elevation** (drop your object onto the terrain surface instead of at Y=0), use `MapboxMap.TryGetElevation(latlng, out float elevation)`: + +```csharp +if (MapBehaviour.MapboxMap.TryGetElevation(coord, out var elevation)) +{ + localPos.y = elevation; +} +Marker.localPosition = localPos; +``` + +If the map moves (via `MapboxMap.ChangeView`, `MapInformation.SetInformation`, or `ShiftMapForDistance`), object positions need to be re-computed. Subscribe to `IMapInformation.ViewChanged` and re-run the conversion when it fires. + +See also `Documentation~/CoordinateConversions.md` for the full coordinate-conversion contract, and `Documentation~/WorkingWithPois.md` if your data lives in a Mapbox vector tile layer (e.g. `poi_label`) rather than from your own backend. + +--- diff --git a/Documentation~/Migration-3.0-to-3.1.md b/Documentation~/Migration-3.0-to-3.1.md new file mode 100644 index 000000000..855485fe3 --- /dev/null +++ b/Documentation~/Migration-3.0-to-3.1.md @@ -0,0 +1,169 @@ +# Migration guide — v3.0 → v3.1 + +v3.1.0 is a substantial release that introduces a new camera system, a new tile-provider LOD algorithm, a Burst-compiled terrain pipeline, and a multicast cache disposal contract. Most projects will upgrade with no source changes. A handful of API shapes have changed; this document walks through each so you know exactly what to touch. + +For the full release notes see [`CHANGELOG.md`](../CHANGELOG.md). This guide is the *narrative upgrade path* — the CHANGELOG is the reference. + +--- + +## Before you upgrade + +1. Commit your working state. +2. Note your **Managed Stripping Level** in Player Settings → Android → Other Settings → Optimization. v3.1.0 supports any setting (Medium and High included), but if you were on **Disabled / Minimal** before for any reason, you can stay there. The SDK now preserves its SQLite model classes with `[Preserve]` attributes plus a `link.xml`. +3. If your project pinned a specific Burst version, note it — v3.1.0 adds `com.unity.burst@1.8.12` as a new dependency. + +--- + +## Step 1 — Install v3.1.0 + +Replace the v3.0 package with v3.1.0 via Package Manager (or re-clone the repo into your `Packages` folder). The first launch will trigger Burst AOT compilation (one-shot, ~10–60s depending on the platform). Subsequent launches are normal. + +The new dependency is resolved automatically by UPM: + +| Dependency | Status | Why | +| --- | --- | --- | +| `com.unity.burst@1.8.12` | **New required dep** | Terrain-RGB decode and collider vertex fill are now Burst jobs. | +| `com.unity.inputsystem` | **Soft dep (unchanged)** | If installed, the SDK auto-uses the new Input System via a `MAPBOX_NEW_INPUT_SYSTEM` define from `versionDefines`. If absent, the SDK falls back to legacy `UnityEngine.Input`. No project-side action either way. | + +--- + +## Step 2 — Source-level breaks to fix + +The compile errors you might see are all narrow. If a section doesn't apply, your project doesn't trip the break. + +### Custom `IMapInformation` implementations + +If you ship your own `IMapInformation` (i.e., not just using the built-in `MapInformation`), the interface gained a new property: + +``` +public TerrainInfo Terrain { get; } +``` + +Add it. For non-terrain maps, return `new TerrainInfo()` — defaults (`MinElevation = 0`, `MaxElevation = 5000`) are fine. The built-in `MapInformation` already implements it. + +### Direct assignment to `TerrainData.ElevationValuesUpdated` + +The field is now a proper `event Action`. Old code that did `data.ElevationValuesUpdated = myCallback` no longer compiles, nor does `data.ElevationValuesUpdated()` to fire it. Subscribe with `+=` and unsubscribe with `-=`. The previous helper `SetElevationChangedCallback` (which silently wiped other subscribers) is removed. + +### Calls to `MapboxTileData.SetDisposeCallback` + +Removed in favor of multicast: + +- `AddDisposeCallback(Action)` — register a handler +- `RemoveDisposeCallback(Action)` — deregister + +Up to 16 render tiles share a single `TerrainData` instance; the old single-setter API silently dropped 15 of them on every replacement. If you assigned via reflection or `InternalsVisibleTo`, switch to the public add/remove pair. + +### Subclasses of `MapInput` + +If you wrote a custom `MapInput` (e.g., to add input behaviours not in the built-in set), you should override the new virtual: + +``` +public override void Teardown(IMapInformation mapInfo) +{ + // Unsubscribe anything you wired in Initialize +} +``` + +It's a no-op by default — you won't see a compile error, but without the override your subscriptions root the destroyed camera + its Transform when `IMapInformation` outlives the camera (DontDestroyOnLoad / scene reload). + +### Subclasses of `UnityTileProvider` + +`TileNode.Set` signature changed: + +- Old: `Set(id, worldCenter, scale, boundsHeight)` +- New: `Set(id, worldCenter, scale, boundsBottom, boundsHeight)` + +Update the call site if you subclassed and reached into `TileNode` directly. The new `boundsBottom` lets the AABB reach sub-sea-level for terrain that dips below Y=0 (Death Valley, Dead Sea). + +### Subclasses of `CustomTMSTile` + +The constructor went from 4 args to 6: + +- Old: `(urlFormat, tileId, tilesetId, useNonReadableTexture)` +- New: `(urlFormat, tileId, tilesetId, useNonReadableTexture, invertY, isMapboxService)` + +The 4-arg constructor is preserved as an overload that defaults `invertY: true, isMapboxService: false` — your existing subclasses still compile. New code should use the 6-arg form to control the two booleans explicitly. + +### Camera behaviours subclassed from `MapCameraBehaviour` + +There's now an open generic abstract base `MapCameraBehaviour` where `T` is the camera-core type. The shipped behaviours (`SlippyMapCameraBehaviour`, `Moving3dCameraBehaviour`) close the generic. If you subclassed the old non-generic base, change to: + +``` +public class MyCustomBehaviour : MapCameraBehaviour { ... } +``` + +--- + +## Step 3 — Behavioral changes to verify + +These don't cause compile errors but may change runtime behavior. + +### Custom imagery: `CustomSource.CreateTile` always returns `CustomTMSTile` when `UrlFormat` is set + +Previously `InvertY=false` with a non-empty `UrlFormat` would silently fall back to a plain `RasterTile`. v3.1.0 always uses `CustomTMSTile`. If you depended on the plain-raster path, set `UrlFormat = ""` — empty `UrlFormat` falls back to plain `RasterTile` for both `CustomSource` and `CustomTerrainSource`. + +### Custom terrain: `Settings.DataSettings` now actually applies + +`CustomTerrainLayerModuleScript` used to throw away the Inspector-configured `DataSettings` and use defaults. v3.1.0 passes them through. **If you had Inspector values set, they now take effect** — cache size, retina flag, non-readable textures, data-zoom clamp. Verify they were intentional. + +### `TerrainInfo.IsEnabled` and `Exaggeration` removed + +Both fields were written-but-never-read in v3.0. v3.1.0 drops them. If your code set either field, the assignment no longer compiles — but the runtime behavior is unchanged, since nothing consumed those fields anyway. Delete the assignments. + +### Tile provider — new defaults, new clamps + +- `UnityTileProviderSettings.SubdivisionBias` default changed from `1.0f` to `0.6f`. New scenes get `0.6` as the inspector default. Legacy scenes saved before this field existed deserialize to `0` (Unity assigns `default(float)` for new fields and skips C# initializers); `ISerializationCallbackReceiver.OnAfterDeserialize` upgrades anything `≤ 0` to `0.6f` automatically. +- `UnityTileProvider.MaximumZoomLevel` is now silently clamped to **30** internally with a log warning if higher. Practically unreachable in Mercator but worth knowing. + +### `MapInformation.Initialize` resets terrain bounds + +`Initialize(LatitudeLongitude)` now resets `Terrain.MinElevation` / `MaxElevation` to `TerrainInfo` defaults. If you intentionally pre-set these before `Initialize` and relied on them surviving, move that assignment to *after* `Initialize`. New `MapInformation` instances behave identically. + +### Tile placement is now local-space + +Tiles are positioned via `transform.localPosition` (was `transform.position`). On every existing scene with `MapRoot` at the world origin with identity rotation and unit scale this is *identical* behaviour. If your project transforms `MapRoot` (AR anchor, custom rig), the SDK now respects that translation and rotation. **Non-unit `MapRoot.localScale` is not supported** — drive map scale through `MapInformation.Scale` instead. See `Documentation~/CoordinateConversions.md` for the local-space conversion contract. + +--- + +## Step 4 — Camera setup + +v3.1.0 replaces the old camera setup with two concrete behaviours plus the generic base. Most projects use one of the shipped behaviours unchanged: + +- `SlippyMapCameraBehaviour` — fixed camera, map moves underneath. Good for 2D-style maps, navigation overlays, AR/VR. +- `Moving3dCameraBehaviour` — camera orbits a target. Good for 3D city exploration, games. + +Touch support is automatic on iOS/Android device builds (compile-time platform split via `(UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR`). No project-side configuration. Touch path can't be tested in the Editor — see `Documentation~/CameraSystem.md` for the workaround. + +--- + +## Step 5 — Default tile material (Android-relevant) + +`MapboxMapBehaviour` has a new serialized `Default Tile Material` field under the **Settings** foldout in its custom Inspector. When no `TileCreatorBehaviour` is assigned, this Material is used as the fallback. + +For scenes built from scratch in v3.1.0, drag `ElevatedTerrainMaterial.mat` (`Runtime/Mapbox/BaseModule/Unity/Visuals/Materials/`) into this field. The demo scenes ship with it pre-wired. + +For scenes upgraded from v3.0 without a `TileCreatorBehaviour`, the previous fallback path used `Shader.Find` — which is unreliable in player builds (the shader gets stripped unless it's in *Always Included Shaders* or referenced from a shipped Material). v3.1.0 throws an `InvalidOperationException` with actionable instructions if both fields are null. Wire the Material reference and you're done. + +--- + +## Step 6 — Rebuild + +1. Clear the Library/ cache if Unity gets confused after the dependency change (rare but possible with Burst's first AOT pass). +2. Editor playmode should work without further changes. +3. iOS build: no project-side changes needed beyond making sure your `Info.plist` has `NSLocationWhenInUseUsageDescription` (unchanged from v3.0). +4. Android build: any Managed Stripping Level works. The SQLite cache model classes are preserved via per-property `[Preserve]` attributes and a backup `Plugins/link.xml` declaration — both shipped in the package. + +--- + +## Verification checklist + +- [ ] Project compiles +- [ ] Sample scene (LocationBasedGame or WorldMapViewer) renders identically to v3.0 +- [ ] Touch pan / pinch zoom / two-finger tilt all work on device +- [ ] iOS build runs and renders tiles +- [ ] Android build runs and renders tiles, **with the SQLite cache populating** (search logcat for `Error inserting … (extended=ConstraintForeignKey)` — should be absent) +- [ ] If you customized `IMapInformation`, `MapInput`, `UnityTileProvider`, `CustomTMSTile`, or `MapCameraBehaviour`: the relevant subclasses still compile and behave correctly +- [ ] If you used `TerrainData.ElevationValuesUpdated` directly or `MapboxTileData.SetDisposeCallback`: migrated to the new event / multicast pattern + +If any of the above fails, check the [Reporting issues](../README.md#reporting-issues--contributing) section and file with reproduction details. diff --git a/Documentation~/Overview.md b/Documentation~/Overview.md deleted file mode 100644 index 991e9015d..000000000 --- a/Documentation~/Overview.md +++ /dev/null @@ -1,173 +0,0 @@ -# Mapbox Unity SDK — Overview - -Map rendering, location services, directions, geocoding, and 3D landmark features for Unity, distributed as a Unity Package Manager (UPM) package. - -| | | -| --- | --- | -| **Package name** | `com.mapbox.sdk` | -| **Current version** | `3.1.0` | -| **Vendor** | Mapbox — | -| **Minimum Unity** | `2022.3` LTS | -| **Render pipeline** | Universal Render Pipeline (URP) — required | - -For full release notes see [`CHANGELOG.md`](../CHANGELOG.md). For installation and demo-scene walkthrough see [`README.md`](../README.md). - ---- - -## Supported Unity editor versions - -The package's `package.json` declares `unity: "2022.3"` as the minimum. - -| Editor version | Status | Notes | -| --- | --- | --- | -| **2022.3 LTS** | Primary target. Android build-settings folder shipped: `Runtime/AndroidBuildSettings/2022.3.38f1/`. | -| **2021.3 LTS** | Best-effort. Android build-settings folder shipped: `Runtime/AndroidBuildSettings/2021.3.41f1/`. Not the primary target — use 2022.3+ when possible. | -| **6000.x (Unity 6)** | Best-effort. Android build-settings folder shipped: `Runtime/AndroidBuildSettings/6000/`. | -| **2023.x non-LTS** | Untested. Should work but no shipped Android settings folder. | -| **< 2021.3** | Unsupported. | - -When building for Android, copy the `Plugins/` folder from the matching (or closest lower) editor version's subfolder under `Runtime/AndroidBuildSettings/` into your project's `Assets/`. See [`README.md`](../README.md#building-your-map-application-for-android) for the full procedure. - ---- - -## Supported build platforms - -Only **iOS** and **Android** are supported build targets. Use the Unity Editor (Windows or macOS) for development and Play-mode testing; all other build targets (Standalone, WebGL, console platforms, VisionOS, etc.) are not supported. - -| Platform | Status | Notes | -| --- | --- | --- | -| **iOS** | Supported | XCFrameworks shipped under `Runtime/Mapbox/BaseModule/Plugins/iOS/` (MapboxCommon, Turf). ARM64 device + simulator. IL2CPP required. | -| **Android** | Supported | Native libs under `Runtime/Mapbox/BaseModule/Plugins/sqlite/Android/libs/` for `arm64-v8a`, `armeabi-v7a`, and `x86`. **See [Android build settings](#android-build-settings) below** for required project-side configuration. | -| **Unity Editor (Play mode)** | Supported on Windows and macOS for development and testing. Not a shipping target. | -| **All other platforms** | Not supported. Includes Standalone (Windows/macOS/Linux), WebGL, console platforms, and VisionOS. | - ---- - -## Required dependencies - -These are declared in `package.json` and resolved automatically by UPM: - -| Package | Version | Used for | -| --- | --- | --- | -| `com.unity.render-pipelines.universal` | `10.10.1` | Terrain and building shaders ship as URP Shader Graphs. | -| `com.unity.nuget.newtonsoft-json` | `3.2.1` | JSON parsing for tile metadata, geocoding/directions API responses. | -| `com.unity.burst` | `1.8.12` | Burst-compiled terrain-RGB decode and collider vertex-fill jobs. First-time domain reload pays a one-shot AOT compile cost. | - -## Optional dependencies - -| Package | Behaviour if installed | Behaviour if absent | -| --- | --- | --- | -| `com.unity.inputsystem` | Camera input uses `UnityEngine.InputSystem` (new Input System). The `MAPBOX_NEW_INPUT_SYSTEM` define is auto-set via `versionDefines` in `MapboxExamples.asmdef`. | Camera input falls back to legacy `UnityEngine.Input`. No project-side action required either way. See [`CameraSystem.md`](CameraSystem.md). | - ---- - -## Android build settings - -### Managed Stripping Level - -The SQLite tile cache uses sqlite-net, which populates row values via `PropertyInfo.SetValue` reflection. IL2CPP's managed-code stripping at **Medium / High** strips auto-property accessor methods that aren't seen as called via static analysis, breaking the row mapper. - -**As of v3.1.0** the four SQLite model types (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every persisted property carry `[UnityEngine.Scripting.Preserve]`, and `Plugins/link.xml` declares them as a backup preservation path. So the SDK *should* work at any stripping level. - -If you still observe `Error inserting … (extended=ConstraintForeignKey)` in logcat on an Android device build: - -1. Set **Project Settings → Player → Android tab → Other Settings → Optimization → Managed Stripping Level** to **Minimal** or **Disabled**. -2. Report the issue — the `[Preserve]` and `link.xml` combination should have covered it. - -When the cache is broken, map rendering still works (the in-memory cache covers) but tiles are silently re-fetched every session instead of being served from disk. - -### Required Android plugin folder - -For Android builds, copy `Runtime/AndroidBuildSettings//Plugins/` into your project's `Assets/`. This ships gradle templates and manifest entries the SDK relies on at build time. - -### Permissions - -Location features require: -- `android.permission.ACCESS_FINE_LOCATION` -- `android.permission.ACCESS_COARSE_LOCATION` - -Runtime permission prompts are handled by `Mapbox.BaseModule.Plugins.Android.UniAndroidPermission`. - ---- - -## iOS build settings - -| Setting | Value | -| --- | --- | -| **Scripting Backend** | IL2CPP (required) | -| **Minimum iOS Version** | 11.0+ recommended | -| **Architecture** | ARM64 | -| **Location permission** | Set `NSLocationWhenInUseUsageDescription` (or `NSLocationAlwaysUsageDescription`) in Info.plist before requesting location. | - -XCFrameworks (`MapboxCommon`, `Turf`) ship multi-slice and are picked up automatically by Unity's plugin importer. - ---- - -## Modules - -The SDK is organized into module assemblies. Each is a separate `.asmdef` and can be referenced or excluded independently: - -| Assembly | Path | Responsibility | -| --- | --- | --- | -| `MapboxBaseModule` | `Runtime/Mapbox/BaseModule/` | Tile management, caching (memory/file/SQLite), data fetching, coordinate conversions. Foundation for every other module. | -| `UnityMapModule` | `Runtime/Mapbox/UnityMapService/` | Unity-specific map service (`MapUnityService`), quadtree tile provider. | -| `MapboxLocationModule` | `Runtime/Mapbox/LocationModule/` | GPS / compass via `AbstractLocationProvider`. Unity + static (editor) implementations. | -| `MapboxImageModule` | `Runtime/Mapbox/ImageModule/` | Raster tile imagery (satellite, streets). Terrain strategies (flat / elevated). | -| `MapboxVectorModule` | `Runtime/Mapbox/VectorModule/` | Vector tile rendering — buildings, roads, areas. Modifier-stack-based mesh generation. Component System. | -| `MapboxCustomImageryModule` | `Runtime/Mapbox/CustomImageryModule/` | Custom tileset support (non-Mapbox sources via TMS or arbitrary URL templates). | -| `MapboxDirections` | `Runtime/Mapbox/DirectionsApi/` | Directions API wrapper. | -| `MapboxGeocoding` | `Runtime/Mapbox/GeocodingApi/` | Forward / reverse geocoding API wrapper. | -| `MapboxDebug` | `Runtime/Mapbox/MapDebug/` | Debug logging + benchmarking pipelines. | -| `MapboxExamples` | `Runtime/Mapbox/Example/` | Camera behaviours, input handling, demo support scripts. | - -Editor-only counterparts exist for several modules (e.g. `MapboxBaseModule.Editor`, `MapboxImageModule.Editor`). - ---- - -## Samples - -Imported via Package Manager → select the SDK → **Samples** tab → **Import**. Five samples ship: - -| Sample | Path | What it demonstrates | -| --- | --- | --- | -| **WorldMapViewer** | `Samples~/WorldMapViewer` | Minimal map setup — pan/zoom an interactive world map. | -| **LocationBasedGame** | `Samples~/LocationBasedGame` | Device GPS → in-scene avatar movement. The classic Pokemon-Go-style starting point. | -| **MapboxComponents** | `Samples~/MapboxComponents` | Component-system-based layer rendering (Buildings + Areas + Roads). | -| **DirectionsApiDemo** | `Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo` | Calling Directions API + rendering a route on the map. | -| **GeocodingApiDemo** | `Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo` | Forward and reverse geocoding. | - ---- - -## Other documentation - -Located in `Documentation~/`: - -- [`GettingStartedWithMapboxMapObject.md`](GettingStartedWithMapboxMapObject.md) — first-time setup and the core `MapboxMap` object. -- [`AccessingTheMapObject.md`](AccessingTheMapObject.md) — getting a reference to the map at runtime. -- [`WorkingWithMapObject.md`](WorkingWithMapObject.md) — runtime API. -- [`WorkingWithModules.md`](WorkingWithModules.md) — module composition pattern. Includes architecture diagrams. -- [`ChangingMapLocation.md`](ChangingMapLocation.md) — moving the map to a new lat/lon. -- [`ChangeImageryStyleOnRuntime.md`](ChangeImageryStyleOnRuntime.md) — switching tile styles at runtime. -- [`CameraSystem.md`](CameraSystem.md) — camera behaviours, touch/mouse input, Input System soft-dependency model. -- [`CoordinateConversions.md`](CoordinateConversions.md) — lat/lon ↔ Mercator ↔ Unity world space. -- [`VectorLayerModule.md`](VectorLayerModule.md) — building/road/area visualizers, modifier stacks. -- [`ComponentsModule.md`](ComponentsModule.md) — the newer component-based vector pipeline. -- [`WorkingWithPois.md`](WorkingWithPois.md) — point-of-interest features. -- [`UsingDirectionsApi.md`](UsingDirectionsApi.md) / [`UsingGeocodingApi.md`](UsingGeocodingApi.md) / [`UsingMapMatchingApi.md`](UsingMapMatchingApi.md) — API wrappers. - ---- - -## Access token - -Every Mapbox API call (tiles, geocoding, directions) requires a Mapbox access token. - -- Set it via the editor menu: **Mapbox → Setup**. -- Stored at `Assets/Resources/Mapbox/MapboxConfiguration.txt` (read at runtime via `Resources.Load`). -- An SKU token is appended automatically to authenticated requests. -- Get a token at . - ---- - -## Reporting issues / contributing - -This package is maintained by Mapbox internally. Issues and feature requests should be reported through your standard Mapbox support channel. diff --git a/README.md b/README.md index 102dd938a..10a4c539f 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,212 @@ -## Mapbox Unity SDK 3.1.0 Quickstart guide +# Mapbox Unity SDK -For a complete overview of the package — supported Unity versions, build platforms, dependencies, and module breakdown — see [`Documentation~/Overview.md`](Documentation~/Overview.md). For release notes see [`CHANGELOG.md`](CHANGELOG.md). +Map rendering, location services, directions, geocoding, and 3D landmark features for Unity. Distributed as a Unity Package Manager (UPM) package. -### Installing the Package +| | | +| --- | --- | +| **Package name** | `com.mapbox.sdk` | +| **Current version** | `3.1.0` | +| **Vendor** | Mapbox — | +| **Minimum Unity** | `2022.3` LTS | +| **Build targets** | iOS, Android | +| **Render pipeline** | Universal Render Pipeline (URP) — required | -1. **Get the Mapbox Unity SDK v3.1.0** - - Clone the Mapbox Unity SDK repository to local. +For release notes see [`CHANGELOG.md`](CHANGELOG.md). Upgrading from v3.0? See [`Documentation~/Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md). -2. **Open Unity Package Manager (UPM)** - - Open your Unity project and navigate to the Unity Package Manager. +--- + +## Supported Unity editor versions + +| Editor version | Status | +| --- | --- | +| **2022.3 LTS** | Primary target. Android build-settings folder shipped at `Runtime/AndroidBuildSettings/2022.3.38f1/`. | +| **2021.3 LTS** | Best-effort. Android build-settings folder shipped at `Runtime/AndroidBuildSettings/2021.3.41f1/`. | +| **6000.x (Unity 6)** | Best-effort. Android build-settings folder shipped at `Runtime/AndroidBuildSettings/6000/`. | +| **2023.x non-LTS** | Untested. | +| **< 2021.3** | Unsupported. | + +When building for Android, copy the `Plugins/` folder from the matching (or closest lower) editor version's subfolder under `Runtime/AndroidBuildSettings/` into your project's `Assets/`. See [Building for Android](#building-for-android) for the full procedure. + +## Supported build platforms + +Only **iOS** and **Android** are supported build targets. Use the Unity Editor (Windows or macOS) for development and Play-mode testing. Standalone, WebGL, console, and VisionOS are not supported. + +| Platform | Status | Notes | +| --- | --- | --- | +| **iOS** | Supported | XCFrameworks under `Runtime/Mapbox/BaseModule/Plugins/iOS/`. ARM64. IL2CPP. | +| **Android** | Supported | Native libs under `Runtime/Mapbox/BaseModule/Plugins/sqlite/Android/libs/` for `arm64-v8a`, `armeabi-v7a`, `x86`. Project-side configuration required — see [Building for Android](#building-for-android). | +| **Unity Editor (Play mode)** | Supported on Windows / macOS for development. | +| **All other platforms** | Not supported. | + +## Dependencies -3. **Add the Package from Disk** - - Click the **`Add package from disk`** button in the top-left corner of the UPM window. - - Locate and select the `package.json` file within the Mapbox folder. +Resolved automatically by UPM: -4. **Verify Installation** - - The Mapbox Unity SDK package should now appear in the Unity Package Manager window. +| Package | Version | Required | Used for | +| --- | --- | --- | --- | +| `com.unity.render-pipelines.universal` | `10.10.1` | Yes | Terrain and building shaders ship as URP Shader Graphs. | +| `com.unity.nuget.newtonsoft-json` | `3.2.1` | Yes | JSON parsing for tile metadata and API responses. | +| `com.unity.burst` | `1.8.12` | Yes | Burst-compiled terrain decode and collider job. First-time domain reload pays a one-shot AOT compile. | +| `com.unity.inputsystem` | any | Optional | If installed, camera input uses the new Input System (auto-set `MAPBOX_NEW_INPUT_SYSTEM` define). If absent, falls back to legacy `UnityEngine.Input`. | --- -### Playing the Demo Scenes +## Installing the package + +1. **Clone or download the SDK.** This package is distributed from the SDK repository. +2. **Open Unity Package Manager** in your Unity project (`Window → Package Manager`). +3. **Add the package from disk.** Click the `+` in the top-left, choose `Add package from disk…`, and select the SDK's `package.json`. +4. **Verify.** The Mapbox Unity SDK should now appear in the list of installed packages. + +> Editor-only evaluation? You can stop here. iOS and Android each have one extra step at build time — see [Building for iOS](#building-for-ios) and [Building for Android](#building-for-android) when you're ready to ship. + +--- + +## Access token + +Every Mapbox API call (tiles, geocoding, directions) requires a Mapbox access token. + +- **Set it** via the editor menu: **Mapbox → Setup**. +- **Stored** at `Assets/Resources/Mapbox/MapboxConfiguration.txt`, loaded at runtime via `Resources.Load`. +- An SKU token is appended to signed requests automatically. +- **Get a token** at . + +--- -1. **Access the Demo Scenes** - - The SDK includes two demo scenes: - - `Location Based Game` - - `World Map Viewer` +## Demo scenes -2. **Import the Demo Scenes** - - In the Package Manager, select the Mapbox Unity SDK package. - - Go to the "Samples" tab. - - Hit the **Import** button to create a `Samples` folder at the root of your project. - - Unity will copy the demo scene files into this folder. +Five samples ship with the SDK. To import: open the Package Manager, select the Mapbox Unity SDK, go to the **Samples** tab, and click **Import** next to the one you want. -3. **Configure Your Access Token** - - The demo scenes will not work until you set your access token. - - Open the **Token Configuration** window under the Mapbox section in Unity's top menu. - - Enter your access token. +| Sample | What it demonstrates | +| --- | --- | +| **WorldMapViewer** | Minimal map setup — pan / zoom an interactive world map. Best first-time evaluation target. | +| **LocationBasedGame** | Device GPS → in-scene avatar movement. The Pokemon-Go-style starting point. | +| **MapboxComponents** | High-performance component-system-based layer rendering (Buildings + Areas + Roads). | +| **DirectionsApiDemo** | Calling the Directions API + rendering a route on the map. | +| **GeocodingApiDemo** | Forward and reverse geocoding. | -4. **Token Storage** - - Unity will save the access token in a file located at: - `Resources/Mapbox/MapboxConfiguration.txt` +After import each sample lives under `Assets/Samples/Mapbox Unity SDK///`. You can copy the relevant pieces into your own project layout and delete the `Samples` folder. -5. **Run the Demo Scenes** - - After configuring your access token, open the demo scenes and play them. +The **LocationBasedGame** sample includes a `LocationBasedMap` prefab that's a useful starting point for any GPS-driven game: drop it into a scene and you have a working location-aware map with a follow-camera and a character that follows the device location. --- -### Integrating the Location-Based Game Demo Scene into Your Project +## Working with location + +Two ways to drive the map's center / orientation: + +### 1. Fixed lat/lon from the Inspector + +`MapboxMapBehaviour` has a `MapInformation` field with editable latitude, longitude, zoom, pitch, bearing, and scale. With `Initialize On Start` checked, the map opens at those coordinates at startup. + +Good for development and for apps where the map's location is fixed or driven by your own UI (a city tour, a real-estate app filtered by area, etc.). + +### 2. Device GPS via the `LocationModule` + +The `LocationModule` system swaps in device GPS at build time and predefined test coordinates in the editor. This is what the `LocationBasedGame` sample uses. + +To wire it up on a new map: -The integration process depends heavily on your project setup and how you intend to use the map. Below are steps to help you get started with a location-based game setup: +1. **Disable `Initialize On Start`** on `MapboxMapBehaviour` — the location provider will initialize the map asynchronously when the first GPS fix comes in. +2. **Add `Snap Map To Location Provider`** to your map GameObject. It will move the map center to follow the device location. +3. **Attach `Snap Transform To Location Provider`** to your character / avatar transform to make it follow the device location in world space. -1. **Import the Location-Based Game Sample** - - Open the Package Manager, select the Mapbox Unity SDK, go to the **Samples** tab, and hit **Import** next to **LocationBasedGame**. +For arbitrary placement (drop a marker at a specific lat/lon, not driven by GPS), see [Place a GameObject at a latitude / longitude](Documentation~/GettingStartedWithMapboxMapObject.md#place-a-gameobject-at-a-latitude--longitude) in the Getting Started guide. -2. **Use the `LocationBasedMap` Prefab** - - Add the `LocationBasedMap` prefab to any scene, and it should work. - - Optionally, copy the following to another location in your project for easier customization: - - `LocationBasedMap` prefab - - `MapVisuals` folder - - `Scripts` folder - - After copying, you can safely delete the `Samples` folder if it’s no longer needed. +--- + +## Building for iOS + +| Setting | Value | +| --- | --- | +| **Scripting Backend** | IL2CPP (required) | +| **Minimum iOS Version** | 11.0+ recommended | +| **Architecture** | ARM64 | +| **Location permission** | Set `NSLocationWhenInUseUsageDescription` (or `NSLocationAlwaysUsageDescription`) in Info.plist before requesting location. | + +XCFrameworks (`MapboxCommon`, `Turf`) ship multi-slice and are picked up automatically by Unity's plugin importer. No further project-side configuration required. --- -### Working with Location +## Building for Android -The Mapbox Unity SDK provides two primary methods for working with location: +### Step 1 — Copy the build-settings plugin folder -##### 1. Latitude and Longitude Field in the Map Script +The SDK ships per-editor-version Gradle templates and manifest entries that need to live in your project's `Assets/` folder. -- The **Mapbox Map Behaviour** script includes a field for latitude and longitude. -- If the `Initialize On Start` checkbox is enabled: - - The specified latitude and longitude will serve as the starting location of the map. - - This method is simple and effective, particularly during development in the Unity editor. +1. Open `Runtime/AndroidBuildSettings/`. There are subfolders named after Unity editor versions. +2. Pick the one matching (or closest lower than) the Unity version you're using. +3. Copy the `Plugins/` folder inside it into your project's `Assets/`. +4. If your project already has custom Android settings, merge them with the copied files rather than overwriting. -**Limitations**: -- This approach does not use the device's location. -- If you build the project with these settings, the map will display the predefined location specified in the script. +Verify the settings landed: in **Project Settings → Player → Publish Settings**, the custom files should be referenced. -**Usage in the Location-Based Game Demo Scene**: -- You can move the `CharTarget` game object within the scene. The Astronaut character will walk toward this object in Unity space. +### Step 2 — Managed Stripping Level +The SDK is compatible with any **Managed Stripping Level** in Player Settings (Disabled, Minimal, Low, Medium, High). The SQLite cache's model classes carry `[UnityEngine.Scripting.Preserve]` plus a backup `Plugins/link.xml` declaration to ensure reflection-based row mapping survives stripping. -##### 2. Using the LocationModule System +If you observe `Error inserting … (extended=ConstraintForeignKey)` in logcat, drop the stripping level to **Minimal** as a workaround and file an issue — the preserve-attribute combination should have prevented it. -For a more advanced and dynamic solution, you can use the **LocationModule** system. This method is used by default in the Location-Based Game demo scene. +### Step 3 — Location permissions -###### Overview: -- The LocationModule system abstracts location handling: - - In builds: Uses the device's location. - - In the editor: Uses predefined location values. -- Predefined values can be configured in the **LocationModule** game object located under the map prefab. +For GPS, your manifest should request: -###### Steps to Use the LocationModule System: -Step 1. **Disable `Initialize On Start`** - - Uncheck the `Initialize On Start` checkbox in the map script. +- `android.permission.ACCESS_FINE_LOCATION` +- `android.permission.ACCESS_COARSE_LOCATION` -Step 2. **Add `Snap Map To Location Provider`** - - Attach the `Snap Map To Location Provider` script to your map. - - Set it up as shown in the provided documentation or image reference. +Runtime permission prompts are handled by the SDK's `UniAndroidPermission` helper — no project-side code required. -Step 3. **Enable Player Movement with Device Location** - - Attach the `Snap Transform To Location Provider` script to the **`CharTarget`** game object. - - Configure the script to update the position of the `CharTarget` object with the device's location in real-time. - - The player avatar will automatically follow the `CharTarget` object as it moves. +### Step 4 — Build -By following these steps, your map will dynamically update based on the device's location, and the player avatar will respond to real-world movements. +After the steps above, Build & Run from Unity's Build Settings as normal. --- -### Building Your Map Application for Android +## Modules -To build your map application for the Android platform, you'll need specific settings files included in the Mapbox Unity SDK package. Follow these steps: +The SDK is organized into module assemblies. Each is a separate `.asmdef` and can be referenced or excluded independently: -##### Step 1: Copy Required Files -- Go to the `Mapbox Unity SDK/Runtime/AndroidBuildSettings` folder, you'll find a few folders named after Unity Editor Version. -- Find the one matching (or closest lower) with the version you are using -- Copy the `Plugins` folder inside to your project. Place it under the `Assets` folder. +| Assembly | Path | Responsibility | +| --- | --- | --- | +| `MapboxBaseModule` | `Runtime/Mapbox/BaseModule/` | Tile management, caching (memory/file/SQLite), data fetching, coordinate conversions. Foundation for every other module. | +| `UnityMapModule` | `Runtime/Mapbox/UnityMapService/` | Unity-specific map service (`MapUnityService`), quadtree tile provider. | +| `MapboxLocationModule` | `Runtime/Mapbox/LocationModule/` | GPS / compass via `AbstractLocationProvider`. Unity + static (editor) implementations. | +| `MapboxImageModule` | `Runtime/Mapbox/ImageModule/` | Raster tile imagery (satellite, streets). Terrain strategies (flat / elevated). | +| `MapboxVectorModule` | `Runtime/Mapbox/VectorModule/` | Vector tile rendering — buildings, roads, areas. Modifier-stack mesh generation + Component System. | +| `MapboxCustomImageryModule` | `Runtime/Mapbox/CustomImageryModule/` | Custom tileset support (non-Mapbox sources via TMS or arbitrary URL templates). | +| `MapboxDirections` | `Runtime/Mapbox/DirectionsApi/` | Directions API wrapper. | +| `MapboxGeocoding` | `Runtime/Mapbox/GeocodingApi/` | Forward / reverse geocoding API wrapper. | +| `MapboxDebug` | `Runtime/Mapbox/MapDebug/` | Debug logging and benchmarking pipelines. | +| `MapboxExamples` | `Runtime/Mapbox/Example/` | Camera behaviours, input handling, demo support scripts. | -###### Step 1b: Merging Custom Android Settings (if applicable) -- If your project already contains custom Android settings, merge them with the copied files to ensure compatibility. +Editor-only counterparts exist for several modules (e.g. `MapboxBaseModule.Editor`). -##### Step 2: Verify Player Settings -- Open **Project Settings** and navigate to the **Player** section. -- Under the **Publish Settings** section (at the bottom), verify that the custom setting files we just copied are applied. +--- + +## Documentation + +In-package documentation lives under `Documentation~/`: + +| Doc | Topic | +| --- | --- | +| [`Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) | Upgrade guide from v3.0 to v3.1. Read this first if you're upgrading. | +| [`GettingStartedWithMapboxMapObject.md`](Documentation~/GettingStartedWithMapboxMapObject.md) | First-time setup, the `MapboxMap` object, place-a-pin recipe. | +| [`AccessingTheMapObject.md`](Documentation~/AccessingTheMapObject.md) | Getting a reference to the map at runtime. | +| [`WorkingWithMapObject.md`](Documentation~/WorkingWithMapObject.md) | Runtime API. | +| [`WorkingWithModules.md`](Documentation~/WorkingWithModules.md) | Module composition pattern with architecture diagrams. | +| [`ChangingMapLocation.md`](Documentation~/ChangingMapLocation.md) | Moving the map to a new lat/lon. | +| [`ChangeImageryStyleOnRuntime.md`](Documentation~/ChangeImageryStyleOnRuntime.md) | Switching tile styles at runtime. | +| [`CameraSystem.md`](Documentation~/CameraSystem.md) | Camera behaviours, touch / mouse input, Input System soft-dep. | +| [`CoordinateConversions.md`](Documentation~/CoordinateConversions.md) | lat/lon ↔ Mercator ↔ Unity world space. | +| [`VectorLayerModule.md`](Documentation~/VectorLayerModule.md) | Building / road / area visualizers, modifier stacks. | +| [`ComponentsModule.md`](Documentation~/ComponentsModule.md) | Component-based vector pipeline. | +| [`WorkingWithPois.md`](Documentation~/WorkingWithPois.md) | Point-of-interest features. | +| [`UsingDirectionsApi.md`](Documentation~/UsingDirectionsApi.md) | Directions API wrapper. | +| [`UsingGeocodingApi.md`](Documentation~/UsingGeocodingApi.md) | Geocoding API wrapper. | +| [`UsingMapMatchingApi.md`](Documentation~/UsingMapMatchingApi.md) | Map Matching API wrapper. | + +--- -##### Step 3: Build Your Application -- After completing the above steps, build your application for the Android platform. +## Reporting issues / contributing +This package is maintained by Mapbox internally. Issues and feature requests should be reported through your standard Mapbox support channel. diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 42139ae96..ec7db7875 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -529,7 +529,14 @@ protected void GetMapTile(UnwrappedTileId tileId, out UnityMapTile tile) _mapInformation.Scale); tile = null; tile = _tileCreator.GetTile(); - tile.transform.position = new Vector3((float)rectd.Center.x, 0, (float)rectd.Center.y); + // Local-space so the whole map composes with any parent transform on the + // BaseTileRoot / MapRoot hierarchy (translation + rotation). AR/MR setups + // typically parent MapRoot under an ARAnchor; setting world-space position + // here would silently reset that on every tile cover update. + // Note: non-unit MapRoot.localScale still breaks the quadtree LOD math + // (camera position is read in world space). Recommended pattern: drive + // map scale through MapInformation.Scale, not via MapRoot.transform. + tile.transform.localPosition = new Vector3((float)rectd.Center.x, 0, (float)rectd.Center.y); tile.transform.localScale = Vector3.one * (float)rectd.Size.x; tile.Initialize(tileId, (float)rectd.Size.x * _mapInformation.Scale); } diff --git a/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs new file mode 100644 index 000000000..3acfd8076 --- /dev/null +++ b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs @@ -0,0 +1,88 @@ +using Mapbox.BaseModule.Data.DataFetchers; +using NUnit.Framework; + +namespace Mapbox.BaseModuleTests +{ + /// + /// Tests use unique sizes per test method to avoid cross-test contamination from + /// the pool's static state. + /// + public class ElevationArrayPoolTests + { + [Test] + public void Rent_AllocatesNewArrayOfRequestedSize_WhenPoolIsEmpty() + { + var array = ElevationArrayPool.Rent(1001); + + Assert.IsNotNull(array); + Assert.AreEqual(1001, array.Length); + } + + [Test] + public void ReturnThenRent_ReusesSameBuffer() + { + var first = ElevationArrayPool.Rent(1002); + ElevationArrayPool.Return(first); + + var second = ElevationArrayPool.Rent(1002); + + Assert.AreSame(first, second, "Pool should hand back the same buffer it just received."); + } + + [Test] + public void Rent_DifferentSizes_ReturnsDistinctBuffers() + { + var a = ElevationArrayPool.Rent(1003); + var b = ElevationArrayPool.Rent(1004); + + Assert.AreNotSame(a, b); + Assert.AreEqual(1003, a.Length); + Assert.AreEqual(1004, b.Length); + } + + [Test] + public void Return_NullArray_IsNoOp() + { + // Should not throw. + Assert.DoesNotThrow(() => ElevationArrayPool.Return(null)); + } + + [Test] + public void Pool_CapsAtMaxPerSizeDepth_DroppingExcessToGC() + { + // MaxPerSizeDepth is 32 (private const). Push 40 distinct buffers of the + // same size and confirm the pool gave us back at most 32 distinct buffers + // by reference identity. The remaining 8 must have been dropped. + const int size = 1005; + const int pushCount = 40; + + var pushed = new float[pushCount][]; + for (int i = 0; i < pushCount; i++) + { + pushed[i] = new float[size]; + ElevationArrayPool.Return(pushed[i]); + } + + int reusedCount = 0; + for (int i = 0; i < pushCount; i++) + { + var rented = ElevationArrayPool.Rent(size); + // Cleanup as we go so we don't leak into the next test. + ElevationArrayPool.Return(rented); + + for (int j = 0; j < pushCount; j++) + { + if (ReferenceEquals(rented, pushed[j])) + { + reusedCount++; + break; + } + } + } + + // Up to 32 of the pushed buffers should survive; the rest were dropped. + Assert.LessOrEqual(reusedCount, 32, "Pool retained more buffers than its documented cap."); + Assert.Greater(reusedCount, 0, "Pool should have retained at least one buffer for reuse."); + } + } +} diff --git a/Tests/Editor/BaseModule/MapInformationTests.cs b/Tests/Editor/BaseModule/MapInformationTests.cs new file mode 100644 index 000000000..29187ad86 --- /dev/null +++ b/Tests/Editor/BaseModule/MapInformationTests.cs @@ -0,0 +1,55 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Map; +using NUnit.Framework; + +namespace Mapbox.BaseModuleTests +{ + public class MapInformationTests + { + [Test] + public void Initialize_ResetsTerrainBoundsToDefaults() + { + // Simulate a fresh instance with pre-mutated terrain bounds — proves the + // Initialize() reset is doing its job. Today's field initializers cover + // the common case, but this guards against future paths that reuse an + // existing MapInformation instance. + var info = new MapInformation("0,0"); + info.Terrain.MinElevation = -999f; + info.Terrain.MaxElevation = 9999f; + + info.Initialize(new LatitudeLongitude(0, 0)); + + Assert.AreEqual(TerrainInfo.DefaultMinElevation, info.Terrain.MinElevation); + Assert.AreEqual(TerrainInfo.DefaultMaxElevation, info.Terrain.MaxElevation); + } + + [Test] + public void Initialize_SetsLatitudeLongitude() + { + var info = new MapInformation("0,0"); + var target = new LatitudeLongitude(40.7484, -73.9857); + + info.Initialize(target); + + Assert.AreEqual(target.Latitude, info.LatitudeLongitude.Latitude); + Assert.AreEqual(target.Longitude, info.LatitudeLongitude.Longitude); + } + + [Test] + public void Initialize_IsIdempotent() + { + var info = new MapInformation("0,0"); + info.Initialize(new LatitudeLongitude(10, 20)); + + // Mutate terrain after first init, then call Initialize again — the + // guard at the top should short-circuit and leave our mutation in place. + info.Terrain.MinElevation = -500f; + info.Initialize(new LatitudeLongitude(50, 50)); + + Assert.AreEqual(-500f, info.Terrain.MinElevation, + "Second Initialize call should be a no-op and must not reset terrain bounds."); + Assert.AreEqual(10, info.LatitudeLongitude.Latitude, + "Second Initialize call should not overwrite the lat/lon from the first call."); + } + } +} diff --git a/Tests/Editor/BaseModule/TerrainInfoTests.cs b/Tests/Editor/BaseModule/TerrainInfoTests.cs new file mode 100644 index 000000000..8190be6a4 --- /dev/null +++ b/Tests/Editor/BaseModule/TerrainInfoTests.cs @@ -0,0 +1,26 @@ +using Mapbox.BaseModule.Map; +using NUnit.Framework; + +namespace Mapbox.BaseModuleTests +{ + public class TerrainInfoTests + { + [Test] + public void DefaultConstants_HaveExpectedValues() + { + // Documented contract: Min=0, Max=5000. Conservative bounds that admit any + // plausible mountain range so steep terrain isn't frustum-culled on first paint. + Assert.AreEqual(0f, TerrainInfo.DefaultMinElevation); + Assert.AreEqual(5000f, TerrainInfo.DefaultMaxElevation); + } + + [Test] + public void NewInstance_FieldsInitializedToDefaults() + { + var info = new TerrainInfo(); + + Assert.AreEqual(TerrainInfo.DefaultMinElevation, info.MinElevation); + Assert.AreEqual(TerrainInfo.DefaultMaxElevation, info.MaxElevation); + } + } +} From 686ad04ed5ec1fff08e8c113a807426b5e95e684 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Fri, 15 May 2026 13:54:31 +0300 Subject: [PATCH 44/54] Fix vector content not following MapRoot transform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MapRoot localPosition fix in 844164ac made terrain tiles compose with any parent transformation, but vector content (buildings, areas, roads) stayed in world space and broke the visual when the map was rotated or anchored under an AR rig (see Image #2 in the discussion — terrain rotates, buildings stay flat at world Y=0). Three call sites in the vector module were writing world-space positions on the layer-root GameObjects: (1) VectorLayerVisualizer constructor: `transform.position += offset` applied the layer's settings offset in world space. Converted to `localPosition += offset` so the offset stays in MapRoot-local space. (2) VectorLayerVisualizer.UpdateForView: read the layer root's world-space `transform.position.x/z` and subtracted from the map-local `position` to compute entity local position. Mixed coordinate spaces — wrong whenever the layer root wasn't at world origin. Now reads `localPosition.x/z` so both operands are in the same space. (3) AreaComponentVisualizer + RoadComponentVisualizer constructors: wrote `transform.position = new Vector3(..., GameObjectOffset, ...)` to set the layer's y-offset. Converted to `localPosition`. Behavior is identical when MapRoot is at world origin with identity rotation/scale. Under AR/MR parent transforms, vector content now follows the same rotation/translation as the tiles it overlays. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + .../AreaComponent/AreaComponentVisualizer.cs | 10 ++++++---- .../RoadComponentVisualizer.cs | 10 ++++++---- .../Mapbox/VectorModule/VectorLayerVisualizer.cs | 14 ++++++++++---- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 962649187..38cad8e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,6 +211,7 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. - Fixed Android IL2CPP player builds with **Medium** or **High** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting on Android with non-Minimal stripping. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. - Fixed `MapboxMapVisualizer` writing `tile.transform.position` (world space) on every tile cover update, which silently reset any parent transformation on the `MapRoot` / `BaseTileRoot` hierarchy. Tiles are now positioned via `transform.localPosition`, so translating or rotating the map's root (e.g. anchoring it to an `ARAnchor`) is honored. For unchanged scenes — `MapRoot` at world origin with identity rotation — behavior is identical. Non-unit `MapRoot.localScale` is still unsupported; drive map scale through `MapInformation.Scale` instead. +- Fixed vector content (buildings, areas, roads from `VectorLayerVisualizer` and the ComponentSystem `AreaComponentVisualizer` / `RoadComponentVisualizer`) failing to follow the same MapRoot parent transform as the terrain tiles. Three call sites were writing world-space positions on the layer-root GameObjects: the per-layer `GameObjectOffset` y-offset in both component visualizers, and the `Settings.Offset` application + `_layerRootObject.transform.position.x/z` reads inside `VectorLayerVisualizer.UpdateForView`. All converted to `localPosition`, keeping vector content in the same MapRoot-local space as the tiles it overlays. Buildings, areas, and roads now rotate / translate with the rest of the map under AR/MR-style parent transforms. #### Documentation - New [`Documentation~/Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) walks through every source-level break, every behavioral change, and step-by-step verification when upgrading from v3.0. diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs index 1bdff9e54..690a24b40 100644 --- a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs @@ -22,10 +22,12 @@ public AreaComponentVisualizer(string name, IMapInformation mapInformation, unityContext) { _settings = settings ?? new AreaComponentSettings(); - _layerRootObject.transform.position = new Vector3( - _layerRootObject.transform.position.x, - _settings.GameObjectOffset, - _layerRootObject.transform.position.z); + // localPosition (not position) so the y-offset stays in MapRoot-local space + // and composes with any parent transform applied to the map hierarchy. + _layerRootObject.transform.localPosition = new Vector3( + _layerRootObject.transform.localPosition.x, + _settings.GameObjectOffset, + _layerRootObject.transform.localPosition.z); } public override MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs index 3587bc0aa..02d26d2b0 100644 --- a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs @@ -25,10 +25,12 @@ public RoadComponentVisualizer(string name, IMapInformation mapInformation, Unit RoadComponentSettings settings = null) : base(name, mapInformation, unityContext) { _settings = settings ?? new RoadComponentSettings(); - _layerRootObject.transform.position = new Vector3( - _layerRootObject.transform.position.x, - _settings.GameObjectOffset, - _layerRootObject.transform.position.z); + // localPosition (not position) so the y-offset stays in MapRoot-local space + // and composes with any parent transform applied to the map hierarchy. + _layerRootObject.transform.localPosition = new Vector3( + _layerRootObject.transform.localPosition.x, + _settings.GameObjectOffset, + _layerRootObject.transform.localPosition.z); } private enum Turn diff --git a/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs b/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs index 40c97742c..48146b522 100644 --- a/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs @@ -47,7 +47,10 @@ public VectorLayerVisualizer(string name, IMapInformation mapInformation, UnityC _layerRootObject.SetParent(_unityContext.RuntimeGenerationRoot); } _layerRootObject.transform.localPosition = Vector3.zero; - _layerRootObject.transform.position += _settings.Offset; + // localPosition (not position) so the offset stays in MapRoot-local space. + // Writing world-space here would clobber any parent transform on the map + // hierarchy (e.g. an AR anchor rotating / translating MapRoot). + _layerRootObject.transform.localPosition += _settings.Offset; } public virtual void UpdateForView(CanonicalTileId canonicalTileId, IMapInformation information) @@ -57,10 +60,13 @@ public virtual void UpdateForView(CanonicalTileId canonicalTileId, IMapInformati foreach (var entity in visuals) { _mapInformation.PositionObjectFor(canonicalTileId, out var position, out var scale); + // Both operands in MapRoot-local space: `position` comes from + // PositionObjectFor (Mercator-derived, map-local), and we read the + // layer root's localPosition rather than its world-space position. entity.GameObject.transform.localPosition = new Vector3( - position.x - _layerRootObject.transform.position.x, - entity.GameObject.transform.localPosition.y, - position.z - _layerRootObject.transform.position.z); + position.x - _layerRootObject.transform.localPosition.x, + entity.GameObject.transform.localPosition.y, + position.z - _layerRootObject.transform.localPosition.z); var stack = _stackList[entity.StackId]; if (stack.Settings.LayerType == LayerTypeEnum.Polygon || stack.Settings.LayerType == LayerTypeEnum.Line) From dd68da6dbdf0ff7a5624d063c125b2ba2415f015 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Fri, 15 May 2026 14:10:33 +0300 Subject: [PATCH 45/54] Fix MapShifterCore writing world-space position on RuntimeGenerationRoot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the camera drifts far enough from the map's current center, the shifter updates MapInformation's lat/lon and compensates by shifting RuntimeGenerationRoot the same amount, keeping previously-generated vector content visually in place. The compensation was written as `RuntimeGenerationRoot.position -= viewCenter` — a world-space delta. With a rotated parent (MapRoot under an AR anchor or custom rig), that world-space assignment forces Unity to back-compute a localPosition that compensates for the parent's rotation. The practical effect: RuntimeGenerationRoot's local space gets "counter-rotated" relative to MapRoot so its world position lands at the requested value — and all vector content under it stays pinned to world coordinates instead of rotating with the rest of the map. Reported symptom: terrain tiles rotate with MapRoot, buildings / roads / areas don't. Switched to `localPosition -=` so the shift stays in MapRoot-local space. Behavior is identical when MapRoot is at identity (the only case shipped scenes exercise). Under parent rotation, vector content now follows. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 +- Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38cad8e22..82fee1b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,7 +211,7 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. - Fixed Android IL2CPP player builds with **Medium** or **High** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting on Android with non-Minimal stripping. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. - Fixed `MapboxMapVisualizer` writing `tile.transform.position` (world space) on every tile cover update, which silently reset any parent transformation on the `MapRoot` / `BaseTileRoot` hierarchy. Tiles are now positioned via `transform.localPosition`, so translating or rotating the map's root (e.g. anchoring it to an `ARAnchor`) is honored. For unchanged scenes — `MapRoot` at world origin with identity rotation — behavior is identical. Non-unit `MapRoot.localScale` is still unsupported; drive map scale through `MapInformation.Scale` instead. -- Fixed vector content (buildings, areas, roads from `VectorLayerVisualizer` and the ComponentSystem `AreaComponentVisualizer` / `RoadComponentVisualizer`) failing to follow the same MapRoot parent transform as the terrain tiles. Three call sites were writing world-space positions on the layer-root GameObjects: the per-layer `GameObjectOffset` y-offset in both component visualizers, and the `Settings.Offset` application + `_layerRootObject.transform.position.x/z` reads inside `VectorLayerVisualizer.UpdateForView`. All converted to `localPosition`, keeping vector content in the same MapRoot-local space as the tiles it overlays. Buildings, areas, and roads now rotate / translate with the rest of the map under AR/MR-style parent transforms. +- Fixed vector content (buildings, areas, roads from `VectorLayerVisualizer` and the ComponentSystem `AreaComponentVisualizer` / `RoadComponentVisualizer`) failing to follow the same MapRoot parent transform as the terrain tiles. Four call sites were writing world-space positions on the vector hierarchy: the per-layer `GameObjectOffset` y-offset in both component visualizers, the `Settings.Offset` application + `_layerRootObject.transform.position.x/z` reads inside `VectorLayerVisualizer.UpdateForView`, and `MapShifterCore.Update` writing `RuntimeGenerationRoot.position -= viewCenterPosition` (which, with a rotated parent, made Unity back-compute a localPosition that countered the parent rotation — visually pinning vector content to world coordinates). All converted to `localPosition`. Buildings, areas, and roads now rotate / translate with the rest of the map under AR/MR-style parent transforms. #### Documentation - New [`Documentation~/Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) walks through every source-level break, every behavioral change, and step-by-step verification when upgrading from v3.0. diff --git a/Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs b/Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs index 34bada0e2..2e61d5c4a 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs @@ -28,12 +28,17 @@ public void Update() if(float.IsNaN(viewCenterPosition.x) || float.IsNaN(viewCenterPosition.y) || float.IsInfinity(viewCenterPosition.x) || float.IsInfinity(viewCenterPosition.y)) return; - if (Mathf.Abs(viewCenterPosition.x) > _shiftRange.x || + if (Mathf.Abs(viewCenterPosition.x) > _shiftRange.x || Mathf.Abs(viewCenterPosition.z) > _shiftRange.y) { var centerLatLng = _mapInformation.ConvertPositionToLatLng(viewCenterPosition); _mapInformation.SetLatitudeLongitude(centerLatLng); - _unityContext.RuntimeGenerationRoot.position -= viewCenterPosition; + // localPosition (not position) so the shift stays in MapRoot-local space + // and composes with any parent transform. Writing world-space here would + // anchor RuntimeGenerationRoot in world coordinates — Unity would + // back-compute a localPosition that counters the parent rotation, and + // vector content would visually stop rotating with the rest of the map. + _unityContext.RuntimeGenerationRoot.localPosition -= viewCenterPosition; } } From 5c4dbf488590bbdba14577ca7318e2882bd3a58b Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Fri, 15 May 2026 14:18:04 +0300 Subject: [PATCH 46/54] Fix SetParent counter-rotating runtime-created vector roots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: Transform.SetParent(parent) defaults to worldPositionStays: true, which makes Unity preserve world transform by writing inverse-parent-rotation into the child's localRotation. For runtime-created roots (BaseTileRoot, RuntimeGenerationRoot, per-layer _layerRootObject, pooled entity GameObjects), when their new parent is a rotated MapRoot, Unity sets their localRotation to inverse(MapRoot.rotation). Vector content under those roots then visually counter-rotates against MapRoot — staying world-aligned while terrain tiles rotate correctly. The previous .position fixes (commits 686ad04e and dd68da6d) addressed translation but not this orientation issue. All eight SetParent call sites now pass worldPositionStays: false: - UnityContext: BaseTileRoot, RuntimeGenerationRoot - VectorLayerVisualizer: _layerRootObject + entity GameObject - MapboxComponentVisualizer.VectorEntityGenerator - BuildingComponentVisualizer, AreaComponentVisualizer, RoadComponentVisualizer: entity SetParent calls Plus explicit localRotation = Quaternion.identity on the auto-created roots and visualizer layer roots, as defensive belt-and-suspenders in case the scene-authored root has a non-default rotation. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 +- Runtime/Mapbox/BaseModule/Unity/UnityContext.cs | 13 +++++++++++-- .../AreaComponent/AreaComponentVisualizer.cs | 2 +- .../BuildingComponentVisualizer.cs | 2 +- .../ComponentSystem/MapboxComponentVisualizer.cs | 2 +- .../RoadComponentVisualizer.cs | 2 +- .../Mapbox/VectorModule/VectorLayerVisualizer.cs | 9 +++++++-- 7 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fee1b79..5c87b91b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,7 +211,7 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. - Fixed Android IL2CPP player builds with **Medium** or **High** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting on Android with non-Minimal stripping. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. - Fixed `MapboxMapVisualizer` writing `tile.transform.position` (world space) on every tile cover update, which silently reset any parent transformation on the `MapRoot` / `BaseTileRoot` hierarchy. Tiles are now positioned via `transform.localPosition`, so translating or rotating the map's root (e.g. anchoring it to an `ARAnchor`) is honored. For unchanged scenes — `MapRoot` at world origin with identity rotation — behavior is identical. Non-unit `MapRoot.localScale` is still unsupported; drive map scale through `MapInformation.Scale` instead. -- Fixed vector content (buildings, areas, roads from `VectorLayerVisualizer` and the ComponentSystem `AreaComponentVisualizer` / `RoadComponentVisualizer`) failing to follow the same MapRoot parent transform as the terrain tiles. Four call sites were writing world-space positions on the vector hierarchy: the per-layer `GameObjectOffset` y-offset in both component visualizers, the `Settings.Offset` application + `_layerRootObject.transform.position.x/z` reads inside `VectorLayerVisualizer.UpdateForView`, and `MapShifterCore.Update` writing `RuntimeGenerationRoot.position -= viewCenterPosition` (which, with a rotated parent, made Unity back-compute a localPosition that countered the parent rotation — visually pinning vector content to world coordinates). All converted to `localPosition`. Buildings, areas, and roads now rotate / translate with the rest of the map under AR/MR-style parent transforms. +- Fixed vector content (buildings, areas, roads from `VectorLayerVisualizer` and the ComponentSystem `AreaComponentVisualizer` / `RoadComponentVisualizer`) failing to follow the same MapRoot parent transform as the terrain tiles. Two distinct issues compounded each other. (1) Four call sites were writing world-space positions on the vector hierarchy: the per-layer `GameObjectOffset` y-offset in both component visualizers, the `Settings.Offset` application + `_layerRootObject.transform.position.x/z` reads inside `VectorLayerVisualizer.UpdateForView`, and `MapShifterCore.Update` writing `RuntimeGenerationRoot.position -= viewCenterPosition`. All converted to `localPosition`. (2) `Transform.SetParent(parent)` defaults to `worldPositionStays: true`, which makes Unity preserve world transform by writing inverse-parent-rotation into the child's `localRotation`. Auto-created roots (`BaseTileRoot`, `RuntimeGenerationRoot` in `UnityContext`, and every per-layer `_layerRootObject` + pooled entity GameObject in the vector visualizers) hit this path on initial parenting, ending up with a `localRotation` that counter-rotated the entire vector subtree against MapRoot. All five `SetParent` call sites now pass `worldPositionStays: false`, with explicit `localRotation = Quaternion.identity` on the visualizer roots as defensive belt-and-suspenders. Buildings, areas, and roads now rotate / translate with the rest of the map under AR/MR-style parent transforms. #### Documentation - New [`Documentation~/Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) walks through every source-level break, every behavioral change, and step-by-step verification when upgrading from v3.0. diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs b/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs index 8df77fa58..0bca4629d 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs @@ -36,15 +36,24 @@ public IEnumerator Initialize(TaskManager providedTaskManager = null) TaskManager.Initialize(); + // worldPositionStays: false on the SetParent calls below. Without that flag + // (default true), Unity preserves world transform by writing to localRotation — + // so if MapRoot is rotated when Initialize runs, auto-created roots end up + // with localRotation = inverse(MapRoot.rotation), counter-rotating any + // children they hold. Explicit localRotation = identity afterwards is + // defensive in case the scene-authored root has a non-default rotation we + // want to ignore. BaseTileRoot = BaseTileRoot == null ? new GameObject("BaseTiles").transform : BaseTileRoot; - BaseTileRoot.SetParent(MapRoot); + BaseTileRoot.SetParent(MapRoot, worldPositionStays: false); BaseTileRoot.transform.localPosition = Vector3.zero; + BaseTileRoot.transform.localRotation = Quaternion.identity; RuntimeGenerationRoot = RuntimeGenerationRoot == null ? new GameObject("RuntimeObjectsRoot").transform : RuntimeGenerationRoot; - RuntimeGenerationRoot.SetParent(MapRoot); + RuntimeGenerationRoot.SetParent(MapRoot, worldPositionStays: false); RuntimeGenerationRoot.transform.localPosition = Vector3.zero; + RuntimeGenerationRoot.transform.localRotation = Quaternion.identity; yield return null; } diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs index 690a24b40..8b745fe0e 100644 --- a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs @@ -147,7 +147,7 @@ public override List CreateGo(CanonicalTileId tileId, MeshData meshD entity.MeshRenderer.materials = mats; - entity.GameObject.transform.SetParent(_layerRootObject); + entity.GameObject.transform.SetParent(_layerRootObject, worldPositionStays: false); entity.StackId = 0; var mesh = entity.Mesh; diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs index cc0a8dccb..bdd4edeb5 100644 --- a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs @@ -190,7 +190,7 @@ public override List CreateGo(CanonicalTileId tileId, MeshData meshD entity.MeshRenderer.materials = mats; - entity.GameObject.transform.SetParent(_layerRootObject); + entity.GameObject.transform.SetParent(_layerRootObject, worldPositionStays: false); entity.StackId = 0; var mesh = entity.Mesh; diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs index b7a54f47b..ea4cdab0c 100644 --- a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs @@ -84,7 +84,7 @@ public override void ClearCaches() private VectorEntity VectorEntityGenerator() { var go = new GameObject(); - go.transform.SetParent(_layerRootObject); + go.transform.SetParent(_layerRootObject, worldPositionStays: false); var mf = go.AddComponent(); mf.sharedMesh = new Mesh(); var mr = go.AddComponent(); diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs index 02d26d2b0..582923e09 100644 --- a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs @@ -469,7 +469,7 @@ public override List CreateGo(CanonicalTileId tileId, MeshData meshD entity.MeshRenderer.materials = mats; - entity.GameObject.transform.SetParent(_layerRootObject); + entity.GameObject.transform.SetParent(_layerRootObject, worldPositionStays: false); entity.StackId = 0; var mesh = entity.Mesh; diff --git a/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs b/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs index 48146b522..24129fcb9 100644 --- a/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs @@ -44,9 +44,14 @@ public VectorLayerVisualizer(string name, IMapInformation mapInformation, UnityC _layerRootObject = new GameObject(_vectorLayerName + " layer objects").transform; if (_unityContext != null) { - _layerRootObject.SetParent(_unityContext.RuntimeGenerationRoot); + // worldPositionStays: false so Unity doesn't counter-rotate localRotation + // when parenting under a rotated RuntimeGenerationRoot (e.g. MapRoot + // anchored to an AR rig). The layer root should compose with the parent + // chain, not pin to world space. + _layerRootObject.SetParent(_unityContext.RuntimeGenerationRoot, worldPositionStays: false); } _layerRootObject.transform.localPosition = Vector3.zero; + _layerRootObject.transform.localRotation = Quaternion.identity; // localPosition (not position) so the offset stays in MapRoot-local space. // Writing world-space here would clobber any parent transform on the map // hierarchy (e.g. an AR anchor rotating / translating MapRoot). @@ -254,7 +259,7 @@ protected List GameObjectModifications(CanonicalTileId canonicalTile foreach (var meshData in pair.Value) { var entity = _stackList[pair.Key].CreateEntity(meshData); - entity.GameObject.transform.SetParent(_layerRootObject); + entity.GameObject.transform.SetParent(_layerRootObject, worldPositionStays: false); entity.StackId = pair.Key; entity.Feature = meshData.Feature; if(Application.isEditor) entity.GameObject.name = VectorLayerName + " " + canonicalTileId.ToString(); From 67fa2b54c07c3beff1b9d4ef2909fac9a9036033 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Fri, 15 May 2026 15:11:14 +0300 Subject: [PATCH 47/54] Soften managed-stripping support wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier claim that v3.1.0 "supports any Managed Stripping Level including Medium and High" overstated what was actually tested. Only Medium was briefly verified during the Android stripping fix. High was never tested, and we shouldn't imply it's a supported target. Updated CHANGELOG, README, and migration guide to a tiered statement: - Low / Minimal / Disabled — tested target, recommended - Medium — briefly tested, appears to work - High — untested, likely works but no guarantees --- CHANGELOG.md | 2 +- Documentation~/Migration-3.0-to-3.1.md | 2 +- README.md | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c87b91b4..1846a60e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -209,7 +209,7 @@ Minor-version bump per semver: this release contains source-breaking API changes - Fixed vector layer range limits not being applied correctly during data processing. - Fixed incorrect limit validation and bounds checking in the `LatitudeLongitude` struct. - Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. -- Fixed Android IL2CPP player builds with **Medium** or **High** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting on Android with non-Minimal stripping. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. +- Fixed Android IL2CPP player builds with **Medium** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting under Medium. **Stripping support after the fix:** Low (and below) is the tested target; Medium has been briefly verified and seems to work; High is untested but expected to work given the same preservation. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. - Fixed `MapboxMapVisualizer` writing `tile.transform.position` (world space) on every tile cover update, which silently reset any parent transformation on the `MapRoot` / `BaseTileRoot` hierarchy. Tiles are now positioned via `transform.localPosition`, so translating or rotating the map's root (e.g. anchoring it to an `ARAnchor`) is honored. For unchanged scenes — `MapRoot` at world origin with identity rotation — behavior is identical. Non-unit `MapRoot.localScale` is still unsupported; drive map scale through `MapInformation.Scale` instead. - Fixed vector content (buildings, areas, roads from `VectorLayerVisualizer` and the ComponentSystem `AreaComponentVisualizer` / `RoadComponentVisualizer`) failing to follow the same MapRoot parent transform as the terrain tiles. Two distinct issues compounded each other. (1) Four call sites were writing world-space positions on the vector hierarchy: the per-layer `GameObjectOffset` y-offset in both component visualizers, the `Settings.Offset` application + `_layerRootObject.transform.position.x/z` reads inside `VectorLayerVisualizer.UpdateForView`, and `MapShifterCore.Update` writing `RuntimeGenerationRoot.position -= viewCenterPosition`. All converted to `localPosition`. (2) `Transform.SetParent(parent)` defaults to `worldPositionStays: true`, which makes Unity preserve world transform by writing inverse-parent-rotation into the child's `localRotation`. Auto-created roots (`BaseTileRoot`, `RuntimeGenerationRoot` in `UnityContext`, and every per-layer `_layerRootObject` + pooled entity GameObject in the vector visualizers) hit this path on initial parenting, ending up with a `localRotation` that counter-rotated the entire vector subtree against MapRoot. All five `SetParent` call sites now pass `worldPositionStays: false`, with explicit `localRotation = Quaternion.identity` on the visualizer roots as defensive belt-and-suspenders. Buildings, areas, and roads now rotate / translate with the rest of the map under AR/MR-style parent transforms. diff --git a/Documentation~/Migration-3.0-to-3.1.md b/Documentation~/Migration-3.0-to-3.1.md index 855485fe3..f7672d44d 100644 --- a/Documentation~/Migration-3.0-to-3.1.md +++ b/Documentation~/Migration-3.0-to-3.1.md @@ -9,7 +9,7 @@ For the full release notes see [`CHANGELOG.md`](../CHANGELOG.md). This guide is ## Before you upgrade 1. Commit your working state. -2. Note your **Managed Stripping Level** in Player Settings → Android → Other Settings → Optimization. v3.1.0 supports any setting (Medium and High included), but if you were on **Disabled / Minimal** before for any reason, you can stay there. The SDK now preserves its SQLite model classes with `[Preserve]` attributes plus a `link.xml`. +2. Note your **Managed Stripping Level** in Player Settings → Android → Other Settings → Optimization. v3.1.0's tested target is **Low** (or Minimal / Disabled). **Medium** has been briefly tested and seems to work — the SDK now preserves its SQLite model classes with `[Preserve]` attributes plus a `link.xml`. **High** is untested and *might* work given the same preservation, but no guarantees. If you were on Low or below, stay there. If you want to try Medium / High, validate on your build before committing. 3. If your project pinned a specific Burst version, note it — v3.1.0 adds `com.unity.burst@1.8.12` as a new dependency. --- diff --git a/README.md b/README.md index 10a4c539f..2267869d5 100644 --- a/README.md +++ b/README.md @@ -143,9 +143,13 @@ Verify the settings landed: in **Project Settings → Player → Publish Setting ### Step 2 — Managed Stripping Level -The SDK is compatible with any **Managed Stripping Level** in Player Settings (Disabled, Minimal, Low, Medium, High). The SQLite cache's model classes carry `[UnityEngine.Scripting.Preserve]` plus a backup `Plugins/link.xml` declaration to ensure reflection-based row mapping survives stripping. +In Player Settings → Android → Other Settings → Optimization → **Managed Stripping Level**: -If you observe `Error inserting … (extended=ConstraintForeignKey)` in logcat, drop the stripping level to **Minimal** as a workaround and file an issue — the preserve-attribute combination should have prevented it. +- **Disabled / Minimal / Low** — the primary tested target. Recommended for shipping. +- **Medium** — briefly tested in v3.1.0 and appears to work. The SQLite cache's model classes carry `[UnityEngine.Scripting.Preserve]` plus a backup `Plugins/link.xml` declaration so reflection-based row mapping survives stripping. We haven't exercised every code path under Medium; report anything that misbehaves. +- **High** — not tested. Likely works given the `[Preserve]` coverage, but no guarantees. If you need High in production, validate on your specific build before committing. + +If you observe `Error inserting … (extended=ConstraintForeignKey)` in logcat at any stripping level, drop to **Minimal / Low** as a workaround and file an issue — the preserve-attribute combination should have prevented it. ### Step 3 — Location permissions From 7e46dce938ae77a14975ef1d74443ff971f19d2f Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Fri, 15 May 2026 17:22:11 +0300 Subject: [PATCH 48/54] Add Conversions tests and fix two flaky test asserts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConversionTests: five new editor-mode unit tests covering pure math — - WebMercator round-trip across 8 diverse coordinates including poles and antimeridian - LatLng -> TileId round-trip across three cities and five zoom levels - Tile01 center invariant - TileSize halving across zoom levels - TileSize scaling linearly with map scale ElevationArrayPoolTests: Pool_CapsAtMaxPerSizeDepth was counting the same buffer multiple times because the test rented and immediately returned in a loop. Switched to draining the pool with a HashSet of references to count unique retained buffers correctly. DataCompressionTests: Assert.Less -> Assert.LessOrEqual on the EditMode-in-iOS/Android branch. The pre-existing assertion assumed the response arrives gzipped; in practice UnityWebRequest / the CDN often returns it already decompressed, making the sizes equal. The loosened assertion accepts both cases without losing the smoke check that decompression doesn't break the data. --- Tests/Editor/BaseModule/ConversionTests.cs | 102 ++++++++++++++++++ .../Editor/BaseModule/DataCompressionTests.cs | 6 +- .../BaseModule/ElevationArrayPoolTests.cs | 44 ++++---- 3 files changed, 132 insertions(+), 20 deletions(-) diff --git a/Tests/Editor/BaseModule/ConversionTests.cs b/Tests/Editor/BaseModule/ConversionTests.cs index da7e3830a..89f365a3b 100644 --- a/Tests/Editor/BaseModule/ConversionTests.cs +++ b/Tests/Editor/BaseModule/ConversionTests.cs @@ -1,4 +1,5 @@ using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; using NUnit.Framework; using UnityEngine; @@ -39,6 +40,107 @@ public void StringToLatLngToMercatorToLatLng() Assert.AreEqual(latlng.Longitude, newLatlng.Longitude, 0.001d); } + [Test] + public void WebMercator_RoundTrip_PreservesCoordinatesAcrossWorld() + { + // Sample diverse latitudes/longitudes including hemispheres, equator, + // and near-the-pole values. Mercator only spans ±85.0511° so high + // latitudes are intentionally near that limit, not beyond. + var samples = new[] + { + new LatitudeLongitude(0, 0), // Null Island + new LatitudeLongitude(40.7128, -74.0060), // New York + new LatitudeLongitude(51.5074, -0.1278), // London + new LatitudeLongitude(-33.8688, 151.2093), // Sydney + new LatitudeLongitude(35.6895, 139.6917), // Tokyo + new LatitudeLongitude(-22.9068, -43.1729), // Rio de Janeiro + new LatitudeLongitude(85, 179.99), // near north pole + antimeridian + new LatitudeLongitude(-85, -179.99), // near south pole + antimeridian + }; + + foreach (var ll in samples) + { + var mercator = Conversions.LatitudeLongitudeToWebMercator(ll); + var back = Conversions.WebMercatorToLatLon(mercator); + + // 1e-6 degrees ≈ 11cm at the equator. The transform is analytic; this + // catches sign-flip bugs and the half-circumference offset, not float drift. + Assert.AreEqual(ll.Latitude, back.Latitude, 1e-6, $"Latitude round-trip failed for {ll}"); + Assert.AreEqual(ll.Longitude, back.Longitude, 1e-6, $"Longitude round-trip failed for {ll}"); + } + } + + [Test] + public void LatLngToTileId_RoundTripsThroughTileCenter() + { + // Each lat/lng → tile-id → tile-center-lat/lng → tile-id should land + // back on the same tile. Across multiple zoom levels. + var samples = new[] + { + new LatitudeLongitude(40.7128, -74.0060), // New York + new LatitudeLongitude(48.8566, 2.3522), // Paris + new LatitudeLongitude(-22.9068, -43.1729), // Rio + }; + + for (int z = 2; z <= 18; z += 4) + { + foreach (var ll in samples) + { + var tileId = Conversions.LatitudeLongitudeToTileId(ll, z); + var bounds = Conversions.TileIdToBounds(tileId.Canonical); + var tileCenter = bounds.Center; + var roundtrip = Conversions.LatitudeLongitudeToTileId(tileCenter, z); + + Assert.AreEqual(tileId.ToString(), roundtrip.ToString(), + $"Tile round-trip failed at z={z} for {ll}"); + } + } + } + + [Test] + public void Tile01_CenterMapsToTileCenter() + { + // (0.5, 0.5) maps to the tile's center regardless of any convention + // about which corner (0,0) or (1,1) is. Useful invariant; corner + // mappings are tested separately if needed. + var tileId = new CanonicalTileId(14, 9647, 12321); + var bounds = Conversions.TileIdToBounds(tileId); + + var center = Conversions.Tile01ToLatitudeLongitude(new Vector2(0.5f, 0.5f), tileId); + + Assert.AreEqual(bounds.Center.Latitude, center.Latitude, 1e-6); + Assert.AreEqual(bounds.Center.Longitude, center.Longitude, 1e-6); + } + + [Test] + public void TileSizeInUnitySpace_HalvesEveryZoomLevel() + { + // Mercator tile pyramid: at each zoom level, tile size halves. + const float scale = 1f; + float previousSize = -1f; + for (int z = 0; z <= 20; z++) + { + var size = Conversions.TileSizeInUnitySpace(z, scale); + Assert.Greater(size, 0f, $"Tile size at z={z} should be positive."); + if (previousSize > 0) + { + // Allow tiny float drift but the ratio should be ~2. + Assert.AreEqual(previousSize / 2f, size, previousSize * 1e-5f, + $"Tile size at z={z} should be half of z={z - 1}."); + } + previousSize = size; + } + } + + [Test] + public void TileSizeInUnitySpace_ScalesLinearlyWithScale() + { + // Doubling the map scale halves the tile size in Unity units. + var sizeAtScale1 = Conversions.TileSizeInUnitySpace(10, 1f); + var sizeAtScale2 = Conversions.TileSizeInUnitySpace(10, 2f); + Assert.AreEqual(sizeAtScale1 / 2f, sizeAtScale2, 1e-4f); + } + [Test] public void Test_TileEdgeSizeInMercator() { diff --git a/Tests/Editor/BaseModule/DataCompressionTests.cs b/Tests/Editor/BaseModule/DataCompressionTests.cs index 031f66eea..004164f34 100644 --- a/Tests/Editor/BaseModule/DataCompressionTests.cs +++ b/Tests/Editor/BaseModule/DataCompressionTests.cs @@ -76,7 +76,11 @@ public IEnumerator Decompress() Assert.AreEqual(buffer.Length, Compression.Decompress(buffer).Length); // EditMode on OSX #elif UNITY_EDITOR && (UNITY_IOS || UNITY_ANDROID) // PlayMode tests in Editor Debug.Log("EditMode tests in Editor"); - Assert.Less(buffer.Length, Compression.Decompress(buffer).Length); + // LessOrEqual (not Less) so the test passes whether the response arrives + // gzipped (decompression makes it strictly larger) or already + // transparently decompressed by UnityWebRequest / the server (in which + // case decompression is a no-op and the sizes match). + Assert.LessOrEqual(buffer.Length, Compression.Decompress(buffer).Length); #elif !UNITY_EDITOR && (UNITY_EDITOR_OSX || UNITY_IOS || UNITY_ANDROID) // PlayMode tests on device Debug.Log("PlayMode tests on device"); Assert.AreEqual(buffer.Length, Compression.Decompress(buffer).Length); diff --git a/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs index 3acfd8076..b49756a8e 100644 --- a/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs +++ b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs @@ -51,38 +51,44 @@ public void Return_NullArray_IsNoOp() public void Pool_CapsAtMaxPerSizeDepth_DroppingExcessToGC() { // MaxPerSizeDepth is 32 (private const). Push 40 distinct buffers of the - // same size and confirm the pool gave us back at most 32 distinct buffers - // by reference identity. The remaining 8 must have been dropped. + // same size and confirm at most 32 distinct buffers (by reference identity) + // can ever be rented back. The remaining 8 must have been dropped to GC. const int size = 1005; const int pushCount = 40; - var pushed = new float[pushCount][]; + var pushedSet = new System.Collections.Generic.HashSet(ReferenceEqualityComparer.Instance); for (int i = 0; i < pushCount; i++) { - pushed[i] = new float[size]; - ElevationArrayPool.Return(pushed[i]); + var arr = new float[size]; + pushedSet.Add(arr); + ElevationArrayPool.Return(arr); } - int reusedCount = 0; - for (int i = 0; i < pushCount; i++) + // Drain the pool: keep renting until we get something that isn't in the + // original pushed set (i.e. a freshly allocated array, meaning the pool + // was empty for this size). Count distinct pushed buffers seen. + var seen = new System.Collections.Generic.HashSet(ReferenceEqualityComparer.Instance); + while (true) { var rented = ElevationArrayPool.Rent(size); - // Cleanup as we go so we don't leak into the next test. - ElevationArrayPool.Return(rented); - - for (int j = 0; j < pushCount; j++) + if (!pushedSet.Contains(rented)) { - if (ReferenceEquals(rented, pushed[j])) - { - reusedCount++; - break; - } + break; // freshly allocated → pool is drained } + seen.Add(rented); } - // Up to 32 of the pushed buffers should survive; the rest were dropped. - Assert.LessOrEqual(reusedCount, 32, "Pool retained more buffers than its documented cap."); - Assert.Greater(reusedCount, 0, "Pool should have retained at least one buffer for reuse."); + Assert.LessOrEqual(seen.Count, 32, "Pool retained more buffers than its documented cap."); + Assert.Greater(seen.Count, 0, "Pool should have retained at least one buffer for reuse."); + } + + // Reference-equality comparer for HashSet — default equality + // compares contents element-wise, which isn't what we want. + private sealed class ReferenceEqualityComparer : System.Collections.Generic.IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + public bool Equals(float[] x, float[] y) => ReferenceEquals(x, y); + public int GetHashCode(float[] obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); } } } From 8e7da85767726dea1e975e47635620f4ac54cfb6 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Fri, 15 May 2026 17:22:23 +0300 Subject: [PATCH 49/54] Document AR/MR support, polish README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CameraSystem.md: new "AR / MR setups" section after "Writing a Custom Camera". Covers what works (translate + rotate MapRoot, parent under an ARAnchor, runtime created roots compose correctly), what doesn't (non-unit MapRoot.localScale breaks quadtree LOD; use MapInformation.Scale instead, with DynamicScalingMapInformation for animated scale transitions), and the recommended setup (replace built-in camera behaviours, write a thin MapCameraBehaviour that pushes AR camera pose into MapboxMap.ChangeView, point UnityTileProviderBehaviour at the AR camera, animate Scale not transform). Explicit "early access" framing — architecture supports it but hasn't been validated against a full ARKit / ARCore sample. README: dropped the misleading "3D landmark features" phrase from the header description (landmarks isn't in v3.1.0 — it lives on a separate branch). Fixed the "All other platforms" row that was missing its Notes column. Regrouped the documentation index from a flat 15-row table into six task-oriented sections: Upgrading, Getting started, Map composition, Camera / input / AR / MR, Vector content, APIs. --- Documentation~/CameraSystem.md | 36 +++++++++++++ README.md | 51 +++++++++++-------- .../ElevationArrayPoolTests.cs.meta | 11 ++++ .../BaseModule/MapInformationTests.cs.meta | 11 ++++ .../BaseModule/TerrainInfoTests.cs.meta | 11 ++++ 5 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 Tests/Editor/BaseModule/ElevationArrayPoolTests.cs.meta create mode 100644 Tests/Editor/BaseModule/MapInformationTests.cs.meta create mode 100644 Tests/Editor/BaseModule/TerrainInfoTests.cs.meta diff --git a/Documentation~/CameraSystem.md b/Documentation~/CameraSystem.md index 0a414e5dd..4b4ff0301 100644 --- a/Documentation~/CameraSystem.md +++ b/Documentation~/CameraSystem.md @@ -118,3 +118,39 @@ If neither camera mode fits your needs, you can create your own by writing a cla Your custom camera class needs to implement one method — `UpdateCamera` — where you handle input and return camera state (center, zoom, pitch, bearing, scale). The base class provides ready-made helpers for input detection, coordinate conversion, and value clamping. The behaviour wrapper handles initialization and communicating changes to the map automatically. A minimal custom camera behaviour only needs a few lines of code beyond the camera logic itself. + +--- + +## AR / MR setups + +As of v3.1.0, the SDK supports parenting the map under an external transform — an `ARAnchor`, an XR rig, or any rotated/translated parent — and having the entire map (terrain tiles, buildings, roads, areas) compose correctly with that parent transform. This makes it possible to place the map on a real-world surface in AR, anchor it to a tracked plane, or rotate the whole map as a unit. + +### What works + +- **Translating `MapRoot`** in world space — the whole map moves as a unit. +- **Rotating `MapRoot`** around any axis — terrain and vector content follow. +- **Parenting `MapRoot`** under an `ARAnchor`, an `XROrigin` child, or any custom rig — the parent's translation and rotation propagate. + +All of this works because tile placement and vector-content placement are done in **local space** relative to `MapRoot`, and runtime-created roots use `SetParent(parent, worldPositionStays: false)` so they don't pin themselves to world coordinates. + +### What doesn't work (yet) + +- **Non-unit `MapRoot.localScale`.** The quadtree LOD math (`UnityTileProvider.ShouldSplit`) reads the camera's world-space position and compares against tile bounds derived from `MapInformation.Scale`. Scaling `MapRoot` doesn't propagate to that math, so the LOD will pick zoom levels appropriate for the unscaled tile size, not the visible one. + + **Use `MapInformation.Scale` to drive map size instead.** A `Scale` of 1000 means each Web Mercator unit is rendered as 1000 Unity units. For a tabletop-size diorama, set `Scale` to a small value (e.g. 0.01) — the LOD math will follow. For an animated tabletop → world-size transition, the `DynamicScalingMapInformation` class drives `Scale` from an animation curve indexed by zoom. + +### Recommended setup + +For AR/MR apps you generally want: + +1. **Replace the built-in camera behaviours.** `SlippyMapCameraBehaviour` and `Moving3dCameraBehaviour` both write directly to `Camera.transform` every frame (and in *Rotate The Map* mode, `SlippyMapCamera` writes to `MapRoot.rotation`). In an AR/MR setup ARKit/ARCore is already driving the camera transform; you don't want the SDK fighting it. Remove these components from the scene. + +2. **Write a thin custom `MapCameraBehaviour`** that does the inverse: instead of moving the camera based on input, it reads the AR-tracked camera's pose and pushes the relevant state into `MapboxMap.ChangeView(...)`. You typically only need to push `Center` (lat/lon) and `Scale` — pitch and bearing can stay at their initial values since the AR camera handles its own orientation. + +3. **Let the quadtree LOD see the AR camera.** `UnityTileProvider.Settings.Camera` can be set explicitly via the `UnityTileProviderBehaviour` inspector. Point it at your AR camera. The tile cover will frustum-cull against the AR camera's actual view. + +4. **Animate scale, not transform.** For a "diorama grows to world size" effect, lerp `MapInformation.Scale` over time. The visualizer reacts to `WorldScaleChanged` and re-lays-out tiles without re-fetching as long as zoom is held constant. Pre-warm the cache for the destination zoom range if you cross zoom boundaries during the animation. + +### Limitations + +This is the first release with AR-style parent transforms supported architecturally. It hasn't been validated against a full ARKit / ARCore sample, and it has not been profiled on AR-class devices. Treat it as **early access**: the bones are in place, but expect to do your own integration work and report rough edges. diff --git a/README.md b/README.md index 2267869d5..121428dec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mapbox Unity SDK -Map rendering, location services, directions, geocoding, and 3D landmark features for Unity. Distributed as a Unity Package Manager (UPM) package. +Map rendering, location services, directions, and geocoding for Unity. Distributed as a Unity Package Manager (UPM) package. | | | | --- | --- | @@ -35,8 +35,8 @@ Only **iOS** and **Android** are supported build targets. Use the Unity Editor ( | --- | --- | --- | | **iOS** | Supported | XCFrameworks under `Runtime/Mapbox/BaseModule/Plugins/iOS/`. ARM64. IL2CPP. | | **Android** | Supported | Native libs under `Runtime/Mapbox/BaseModule/Plugins/sqlite/Android/libs/` for `arm64-v8a`, `armeabi-v7a`, `x86`. Project-side configuration required — see [Building for Android](#building-for-android). | -| **Unity Editor (Play mode)** | Supported on Windows / macOS for development. | -| **All other platforms** | Not supported. | +| **Unity Editor (Play mode)** | Supported on Windows / macOS for development. | Not a shipping target. | +| **All other platforms** | Not supported. | Standalone, WebGL, console, VisionOS, etc. | ## Dependencies @@ -189,25 +189,34 @@ Editor-only counterparts exist for several modules (e.g. `MapboxBaseModule.Edito ## Documentation -In-package documentation lives under `Documentation~/`: +In-package documentation lives under `Documentation~/`. Grouped by what you're trying to do: -| Doc | Topic | -| --- | --- | -| [`Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) | Upgrade guide from v3.0 to v3.1. Read this first if you're upgrading. | -| [`GettingStartedWithMapboxMapObject.md`](Documentation~/GettingStartedWithMapboxMapObject.md) | First-time setup, the `MapboxMap` object, place-a-pin recipe. | -| [`AccessingTheMapObject.md`](Documentation~/AccessingTheMapObject.md) | Getting a reference to the map at runtime. | -| [`WorkingWithMapObject.md`](Documentation~/WorkingWithMapObject.md) | Runtime API. | -| [`WorkingWithModules.md`](Documentation~/WorkingWithModules.md) | Module composition pattern with architecture diagrams. | -| [`ChangingMapLocation.md`](Documentation~/ChangingMapLocation.md) | Moving the map to a new lat/lon. | -| [`ChangeImageryStyleOnRuntime.md`](Documentation~/ChangeImageryStyleOnRuntime.md) | Switching tile styles at runtime. | -| [`CameraSystem.md`](Documentation~/CameraSystem.md) | Camera behaviours, touch / mouse input, Input System soft-dep. | -| [`CoordinateConversions.md`](Documentation~/CoordinateConversions.md) | lat/lon ↔ Mercator ↔ Unity world space. | -| [`VectorLayerModule.md`](Documentation~/VectorLayerModule.md) | Building / road / area visualizers, modifier stacks. | -| [`ComponentsModule.md`](Documentation~/ComponentsModule.md) | Component-based vector pipeline. | -| [`WorkingWithPois.md`](Documentation~/WorkingWithPois.md) | Point-of-interest features. | -| [`UsingDirectionsApi.md`](Documentation~/UsingDirectionsApi.md) | Directions API wrapper. | -| [`UsingGeocodingApi.md`](Documentation~/UsingGeocodingApi.md) | Geocoding API wrapper. | -| [`UsingMapMatchingApi.md`](Documentation~/UsingMapMatchingApi.md) | Map Matching API wrapper. | +**Upgrading from a previous version** +- [`Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) — step-by-step upgrade from v3.0 to v3.1. Read this first if you're upgrading. + +**Getting started** +- [`GettingStartedWithMapboxMapObject.md`](Documentation~/GettingStartedWithMapboxMapObject.md) — first-time setup, the `MapboxMap` object, and a place-a-pin-at-lat/lon recipe. +- [`AccessingTheMapObject.md`](Documentation~/AccessingTheMapObject.md) — getting a reference to the map at runtime. +- [`WorkingWithMapObject.md`](Documentation~/WorkingWithMapObject.md) — runtime API. + +**Map composition** +- [`WorkingWithModules.md`](Documentation~/WorkingWithModules.md) — module composition pattern, architecture diagrams. +- [`ChangingMapLocation.md`](Documentation~/ChangingMapLocation.md) — moving the map to a new lat/lon. +- [`ChangeImageryStyleOnRuntime.md`](Documentation~/ChangeImageryStyleOnRuntime.md) — switching tile styles at runtime. +- [`CoordinateConversions.md`](Documentation~/CoordinateConversions.md) — lat/lon ↔ Mercator ↔ Unity world space. + +**Camera, input, and AR/MR** +- [`CameraSystem.md`](Documentation~/CameraSystem.md) — camera behaviours, touch / mouse input, Input System soft-dep, AR/MR parent-transform guidance. + +**Vector content** +- [`VectorLayerModule.md`](Documentation~/VectorLayerModule.md) — building / road / area visualizers, modifier stacks. +- [`ComponentsModule.md`](Documentation~/ComponentsModule.md) — high-performance component-based vector pipeline. +- [`WorkingWithPois.md`](Documentation~/WorkingWithPois.md) — point-of-interest features. + +**APIs** +- [`UsingDirectionsApi.md`](Documentation~/UsingDirectionsApi.md) — Directions API wrapper. +- [`UsingGeocodingApi.md`](Documentation~/UsingGeocodingApi.md) — Geocoding API wrapper. +- [`UsingMapMatchingApi.md`](Documentation~/UsingMapMatchingApi.md) — Map Matching API wrapper. --- diff --git a/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs.meta b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs.meta new file mode 100644 index 000000000..a1eefac79 --- /dev/null +++ b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 614c19621ee044cdd9165bd1a874e055 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/BaseModule/MapInformationTests.cs.meta b/Tests/Editor/BaseModule/MapInformationTests.cs.meta new file mode 100644 index 000000000..a8f0a47a0 --- /dev/null +++ b/Tests/Editor/BaseModule/MapInformationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b37d971e7300b47e99af22ed149f50f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/BaseModule/TerrainInfoTests.cs.meta b/Tests/Editor/BaseModule/TerrainInfoTests.cs.meta new file mode 100644 index 000000000..c1a8ca616 --- /dev/null +++ b/Tests/Editor/BaseModule/TerrainInfoTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb886f22bc82a4306b67ad743458bfc2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 159aee49cc575a4ab4ecead27282a0f786b2697e Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Mon, 18 May 2026 12:50:04 +0300 Subject: [PATCH 50/54] Fix SlippyMapCameraBehaviour skipping base defensive teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The override of OnMapInitialized in SlippyMapCameraBehaviour wasn't running the IsInitialized / prior-Core teardown block that MapCameraBehaviour.OnMapInitialized performs. If MapBehaviour.Initialized fired twice (scene reload, swapped map asset), the prior SlippyMapCamera core's IMapInformation.SetView subscription would leak until OnDestroy. Can't simply call base.OnMapInitialized(map) — base invokes the 2-arg Core.Initialize and we'd re-init with the 3-arg control-plane overload right after, double-initializing the core. Inlined the defensive teardown directly with a comment explaining why. Moving3dCameraBehaviour doesn't override OnMapInitialized so it inherits the defensive behavior naturally. --- .../Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs index 844729a03..23f5a53bc 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs @@ -14,6 +14,16 @@ public class SlippyMapCameraBehaviour : MapCameraBehaviour protected override void OnMapInitialized(MapboxMap map) { + // Defensive: if MapBehaviour.Initialized fires twice (re-init, swapped map + // asset), unsubscribe from the previous MapInformation before binding to + // the new one. Mirrors base.OnMapInitialized, but we can't just call base — + // it would invoke the 2-arg Core.Initialize and we'd re-init with the + // 3-arg control-plane overload below. + if (IsInitialized && Map != null && Map.MapInformation != null) + { + Core?.Teardown(Map.MapInformation); + } + Map = map; IsInitialized = true; _core.Initialize(Camera, map.MapInformation, new Plane(MapBehaviour.transform.up, MapBehaviour.transform.position)); From 2d1e2c1b376bf38d1384e3d8e72d6efdbdfe8425 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Tue, 9 Jun 2026 09:16:09 +0300 Subject: [PATCH 51/54] Add com.unity.inputsystem as a package dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare com.unity.inputsystem (>= 1.7.0) in package.json and restore the Unity.InputSystem GUID reference in MapboxExamples.asmdef. Together these let the Examples assembly compile under every Active Input Handling combination without scene edits or per-project setup. PointerInput.cs now gates its #if on Unity's built-in ENABLE_INPUT_SYSTEM (driven by Player → Active Input Handling), not MAPBOX_NEW_INPUT_SYSTEM (driven by package presence). This is the critical lever for preserving existing legacy-input projects: when those projects update the SDK, UPM auto-installs the input system package but Unity does not change Player Settings, so ENABLE_INPUT_SYSTEM stays unset and the legacy #else branch keeps compiling. Runtime behavior is unchanged for those users until they explicitly opt in to the new input system via the Active Input Handling setting. Drops the now-unused MAPBOX_NEW_INPUT_SYSTEM versionDefines block. --- Runtime/Mapbox/Example/MapboxExamples.asmdef | 11 +++-------- Runtime/Mapbox/Example/Scripts/PointerInput.cs | 17 +++++++++++------ package.json | 3 ++- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Runtime/Mapbox/Example/MapboxExamples.asmdef b/Runtime/Mapbox/Example/MapboxExamples.asmdef index bc8aeb621..35e1e9a75 100644 --- a/Runtime/Mapbox/Example/MapboxExamples.asmdef +++ b/Runtime/Mapbox/Example/MapboxExamples.asmdef @@ -8,7 +8,8 @@ "GUID:36ca6af6e2b304d4090888554d6ce199", "GUID:1b64b068a01f943108c7f4b7a5356c7a", "GUID:6055be8ebefd69e48b49212b09b47b2f", - "GUID:8ba268152fd3143f59445711358cb12f" + "GUID:8ba268152fd3143f59445711358cb12f", + "GUID:75469ad4d38634e559750d17036d5f7c" ], "includePlatforms": [], "excludePlatforms": [], @@ -17,12 +18,6 @@ "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], - "versionDefines": [ - { - "name": "com.unity.inputsystem", - "expression": "", - "define": "MAPBOX_NEW_INPUT_SYSTEM" - } - ], + "versionDefines": [], "noEngineReferences": false } \ No newline at end of file diff --git a/Runtime/Mapbox/Example/Scripts/PointerInput.cs b/Runtime/Mapbox/Example/Scripts/PointerInput.cs index 41ebe0a8c..af28a2fe8 100644 --- a/Runtime/Mapbox/Example/Scripts/PointerInput.cs +++ b/Runtime/Mapbox/Example/Scripts/PointerInput.cs @@ -1,5 +1,5 @@ using UnityEngine; -#if MAPBOX_NEW_INPUT_SYSTEM +#if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; using NewTouch = UnityEngine.InputSystem.EnhancedTouch.Touch; using NewTouchPhase = UnityEngine.InputSystem.TouchPhase; @@ -24,10 +24,15 @@ internal enum PointerTouchPhase /// /// Thin abstraction over Unity's two input backends. - /// has exactly one of two implementations compiled in, selected by the - /// MAPBOX_NEW_INPUT_SYSTEM define (which the asmdef's versionDefines sets when - /// com.unity.inputsystem is installed). MapInput.cs talks only to this interface, - /// so the rest of the camera/input layer stays free of #if branching. + /// has exactly one of two implementations compiled in, selected by Unity's + /// built-in ENABLE_INPUT_SYSTEM define (set when Active Input Handling + /// in Player Settings is "New" or "Both"). Gating on ENABLE_INPUT_SYSTEM + /// rather than package presence is intentional: com.unity.inputsystem is + /// a hard dependency of this package, so it's always installed, but existing + /// legacy-input projects must keep running the #else branch until the + /// user explicitly opts in via Active Input Handling. MapInput.cs talks only + /// to this interface, so the rest of the camera/input layer stays free of + /// #if branching. /// internal interface IPointerInput { @@ -62,7 +67,7 @@ internal interface IPointerInput float MouseScrollY { get; } } -#if MAPBOX_NEW_INPUT_SYSTEM +#if ENABLE_INPUT_SYSTEM internal sealed class PointerInput : IPointerInput { public void EnableTouchSupport() diff --git a/package.json b/package.json index 6b12bb2e9..546905b30 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dependencies": { "com.unity.render-pipelines.universal": "10.10.1", "com.unity.nuget.newtonsoft-json": "3.2.1", - "com.unity.burst": "1.8.12" + "com.unity.burst": "1.8.12", + "com.unity.inputsystem": "1.7.0" }, "author": { "name": "Mapbox", From 36c31811057d83d4288f46a8667ca5970f282c9b Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Tue, 9 Jun 2026 09:16:12 +0300 Subject: [PATCH 52/54] Ignore Claude Code local config (CLAUDE.md, .claude/) --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 8bafea12a..cf9896f42 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,8 @@ Runtime/Mapbox/VectorModule/MeshGeneration/.idea # Internal benchmark tool, kept local-only Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta + +# Claude Code local config, kept local-only +CLAUDE.md +CLAUDE.md.meta +.claude/ From 6a60a8309624b8893b7f797d33101ba7b6d58f04 Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Tue, 9 Jun 2026 09:39:36 +0300 Subject: [PATCH 53/54] Bump to 3.1.1, document input-system dependency change Add v3.1.1 changelog entry covering why the package dependency was reclassified from soft to hard, what changed in PointerInput.cs's #if gate, and how to recover if upgrading an existing project hits a snag (stale Library cache, manifest pinning, "New only" UGUI exception, manual physical removal). The 3.1.0 entry is left intact as the historical record. --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1846a60e8..525a53a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ ## CHANGELOG +### v3.1.1 + +Patch release. Reclassifies `com.unity.inputsystem` from a soft to a **hard** package dependency and switches the example-assembly `#if` gate from package presence to Player Settings, restoring compilation on Unity 6+ projects where the input system package ships preinstalled. Existing legacy-input projects updating from 3.1.0 see no runtime behavior change. + +#### Why this exists + +In 3.1.0 the SDK declared `com.unity.inputsystem` as a soft dependency: source code branched on a `MAPBOX_NEW_INPUT_SYSTEM` define wired via `versionDefines` (set when the package is installed), but `MapboxExamples.asmdef` did not actually reference `Unity.InputSystem`. On Unity 6+ — where the input system package is installed by default — the `#if MAPBOX_NEW_INPUT_SYSTEM` branch activated, the `using UnityEngine.InputSystem` failed to resolve at the asmdef level, and the entire Examples assembly failed to compile. + +The mirror failure mode (asmdef referencing `Unity.InputSystem` without the package installed → unresolved-reference compile error in 2022.3 legacy projects) ruled out simply restoring the reference. Source-side `#if` cannot bridge this because asmdef reference resolution happens before C# preprocessing. + +This release resolves the trade-off by treating the package as a hard dependency that's always installed, and gating the source-side branch on a Unity built-in define that's only set when the user explicitly opts in via Player Settings. + +#### Changed + +- `com.unity.inputsystem` (>= 1.7.0) is declared in `package.json` `dependencies`. UPM auto-installs it on next package refresh. The package coexists with legacy input; installation alone does not change runtime behavior. +- `PointerInput.cs` now gates on Unity's built-in `ENABLE_INPUT_SYSTEM` define (set by Player → Active Input Handling = "New" or "Both") instead of `MAPBOX_NEW_INPUT_SYSTEM`. Existing legacy-input projects keep running the `#else` branch until the user explicitly switches Active Input Handling. +- `MapboxExamples.asmdef` references `Unity.InputSystem` directly and drops the `MAPBOX_NEW_INPUT_SYSTEM` `versionDefines` block (no longer used). + +#### Upgrading an existing project + +For the common case — Unity 2022.3 project, Active Input Handling = "Old", no prior `com.unity.inputsystem` install — no action is required. After the SDK update, UPM installs the input system package (~2 MB), Active Input Handling stays "Old", and every code path runs the same legacy implementation it did before. + +If the upgrade goes wrong, in roughly decreasing order of likelihood: + +- **Stale compile errors that mention `UnityEngine.InputSystem` or `MAPBOX_NEW_INPUT_SYSTEM`** — Unity's `Library/` cache hasn't picked up the new package. Close Unity, delete `Library/`, reopen. Unity re-resolves packages and reimports. +- **Package Manager reports "cannot resolve com.unity.inputsystem"** — the project is on Unity < 2022.3, which the SDK does not support, or a project-side `Packages/manifest.json` override pins an incompatible version. Bump the project's Unity version or relax the version constraint. +- **Map cameras stop responding to mouse / touch after the update** — should not happen if Active Input Handling matches what it was before; if it does, verify Project Settings → Player → Active Input Handling is still set to your prior choice. Unity does not change this setting when a package is auto-installed, but a manual setting flip during the update would explain the symptom. +- **Yellow Inspector warning on `StandaloneInputModule` asking to "Replace with InputSystemUIInputModule"** — cosmetic, appears only under Active Input Handling = "New" / "Both". One-click swap on the EventSystem inspector if you want to clear it; runtime is unaffected. +- **UGUI throws `InvalidOperationException: You are trying to read Input using the UnityEngine.Input class…` under Active Input Handling = "New" only** — Unity's default `StandaloneInputModule` reads through `UnityEngine.Input.*`, which throws under New-only. Either set Active Input Handling to "Both" (legacy stays available for UGUI), or click "Replace with InputSystemUIInputModule" on the EventSystem inspector. +- **You want to remove `com.unity.inputsystem` entirely from your project** — UPM blocks removal because the SDK declares it as a dependency. The de facto opt-out is to leave it installed and set Active Input Handling = "Old"; the package is then dead weight on disk but runtime is identical to legacy-only. For a true physical removal, edit the SDK's `package.json` to drop the `com.unity.inputsystem` line and remove the matching GUID reference from `MapboxExamples.asmdef`. Requires the SDK to be in an editable location (e.g. embedded under `Packages/`, not in `Library/PackageCache/`). + ### v3.1.0 Minor-version bump per semver: this release contains source-breaking API changes (listed below) alongside substantial feature work in camera/input, tile LOD, and terrain rendering. The 3.0.7 designation was previously used in development; published 3.0.7 builds, if any, are superseded by 3.1.0. diff --git a/package.json b/package.json index 546905b30..42de87f75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.mapbox.sdk", - "version": "3.1.0", + "version": "3.1.1", "displayName": "Mapbox Unity SDK", "description": "Mapbox Unity SDK", "unity": "2022.3", From 801b1b7436ce7533a8abd9d2cd067de5e785262c Mon Sep 17 00:00:00 2001 From: Baran Kahyaoglu Date: Tue, 7 Jul 2026 15:39:26 +0300 Subject: [PATCH 54/54] update the documents --- Documentation~/CameraSystem.md | 8 ++++---- Documentation~/Migration-3.0-to-3.1.md | 2 +- README.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Documentation~/CameraSystem.md b/Documentation~/CameraSystem.md index 4b4ff0301..61282d3e1 100644 --- a/Documentation~/CameraSystem.md +++ b/Documentation~/CameraSystem.md @@ -102,12 +102,12 @@ Zoom at cursor works with both mouse and touch — when using pinch, the zoom ce ## Input System Support -The cameras work with both Unity's legacy Input Manager and the new Input System package. The active path is selected at compile time: +The cameras work with both Unity's legacy Input Manager and the new Input System package. `com.unity.inputsystem` is a hard dependency of the SDK (UPM installs it automatically), but the active code path is selected by Player Settings, not by package presence: -- **Default (no Input System package installed)** — uses the legacy `UnityEngine.Input` API. No action needed. -- **With `com.unity.inputsystem` installed** — the example asmdef declares a `versionDefines` entry that automatically defines `MAPBOX_NEW_INPUT_SYSTEM`. The new-input branch activates and reads pointer state via `Mouse.current` / `Touch.activeTouches`. +- **Active Input Handling = "Input Manager (Old)" (default)** — uses the legacy `UnityEngine.Input` API. No action needed, and this is unchanged by the package being installed. +- **Active Input Handling = "Input System Package (New)" or "Both"** — Unity defines the built-in `ENABLE_INPUT_SYSTEM` symbol, which activates the new-input branch. Pointer state is read via `Mouse.current` / `Touch.activeTouches`. -The Input System package is *not* a hard dependency of the SDK — projects that don't install it keep the legacy path with no extra setup. Projects that set **Active Input Handling** to "Input System Package (New)" only must install the package; otherwise `UnityEngine.Input` is unavailable at runtime. +Installing `com.unity.inputsystem` alone does not change which path runs — only flipping Active Input Handling does. See the [CHANGELOG](../CHANGELOG.md#v311) for why the SDK moved from a soft dependency gated on package presence to this model. --- diff --git a/Documentation~/Migration-3.0-to-3.1.md b/Documentation~/Migration-3.0-to-3.1.md index f7672d44d..bb9d6db02 100644 --- a/Documentation~/Migration-3.0-to-3.1.md +++ b/Documentation~/Migration-3.0-to-3.1.md @@ -23,7 +23,7 @@ The new dependency is resolved automatically by UPM: | Dependency | Status | Why | | --- | --- | --- | | `com.unity.burst@1.8.12` | **New required dep** | Terrain-RGB decode and collider vertex fill are now Burst jobs. | -| `com.unity.inputsystem` | **Soft dep (unchanged)** | If installed, the SDK auto-uses the new Input System via a `MAPBOX_NEW_INPUT_SYSTEM` define from `versionDefines`. If absent, the SDK falls back to legacy `UnityEngine.Input`. No project-side action either way. | +| `com.unity.inputsystem@1.7.0` | **New required dep (as of v3.1.1)** | Hard dependency, auto-installed by UPM. Which input path runs is controlled by Player Settings → Active Input Handling, not by package presence — see [CameraSystem.md](CameraSystem.md#input-system-support). Projects on "Input Manager (Old)" (the default) see no behavior change. | --- diff --git a/README.md b/README.md index 121428dec..3c1664dbd 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Resolved automatically by UPM: | `com.unity.render-pipelines.universal` | `10.10.1` | Yes | Terrain and building shaders ship as URP Shader Graphs. | | `com.unity.nuget.newtonsoft-json` | `3.2.1` | Yes | JSON parsing for tile metadata and API responses. | | `com.unity.burst` | `1.8.12` | Yes | Burst-compiled terrain decode and collider job. First-time domain reload pays a one-shot AOT compile. | -| `com.unity.inputsystem` | any | Optional | If installed, camera input uses the new Input System (auto-set `MAPBOX_NEW_INPUT_SYSTEM` define). If absent, falls back to legacy `UnityEngine.Input`. | +| `com.unity.inputsystem` | `1.7.0` | Yes | Camera input backend selection is controlled by Player Settings → Active Input Handling, not by this dependency; "Old" (default) keeps legacy `UnityEngine.Input` behavior. See `Documentation~/CameraSystem.md`. | ---