diff --git a/Runtime/Prefabs/TreeStreamer.prefab b/Runtime/Prefabs/TreeStreamer.prefab index c0f3f70cf7a778b27af7192a4521efdc3e8f9cd1..00d59a14f649e04d338d116e6a98c6b9d61453bb 100644 --- a/Runtime/Prefabs/TreeStreamer.prefab +++ b/Runtime/Prefabs/TreeStreamer.prefab @@ -51,7 +51,11 @@ MonoBehaviour: areaName: DeMarke targetCam: {fileID: 0} wgs84Origin: {x: 52.03894, y: 6.348124} - numRings: 2 + numRings: 4 + requiredTime: 3 removeDelay: 7 + maxTrees: 10000 + treePrefab: {fileID: 9046900170786976767, guid: 65926329d063fb84ea4ac2e63a6fcaa4, + type: 3} treenGenerator: {fileID: -3667069338063608550, guid: ac835d606e3e19a48bdf1d863acfcfd0, type: 3} diff --git a/Runtime/Scripts/DownloadTrees.cs b/Runtime/Scripts/DownloadTrees.cs index 77ff4fd3fac7eac29d8a4947524aaa749a17f90b..6599310fdcba92cf1c9d99c63ccd7be1ca3f8618 100644 --- a/Runtime/Scripts/DownloadTrees.cs +++ b/Runtime/Scripts/DownloadTrees.cs @@ -658,9 +658,9 @@ namespace Wander Debug.Log( "Done with placing/generating trees." ); } - private void OnDrawGizmos() + private void OnDrawGizmosSelected() { - Gizmos.color = Color.green; + Gizmos.color = Color.magenta; float bigTileSize = (float)RDUtils.CalcTileSizeRD(zoom); Gizmos.DrawWireCube( new Vector3( offsX, 0,offsY ) + new Vector3( bigTileSize, 0, bigTileSize )/2, new Vector3( boundsSize, 100, boundsSize ) ); } diff --git a/Runtime/Scripts/TreeStreamer.cs b/Runtime/Scripts/TreeStreamer.cs index c7dda1b26ca843c787c2075be52209b61f65dcd1..156a752079cc27478596a26f02f1868e55b870aa 100644 --- a/Runtime/Scripts/TreeStreamer.cs +++ b/Runtime/Scripts/TreeStreamer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using UnityEditor; @@ -19,13 +20,13 @@ namespace Wander public float lastTime; public bool spawned; public List<Vector3> trees = new(); - // public List<GameObject> spawnedTrees = new(); public int spawnIndex; public Terrain terrain; + public bool triedToObtainTerrain; public List<int> spawnedTrees = new(); - // public NativeArray<RaycastCommand> rayCmds; } + [ReadOnly] public int activeTreeCount; [ReadOnly] public double originRDX; [ReadOnly] public double originRDY; [ReadOnly] public float tileSize; @@ -33,10 +34,11 @@ namespace Wander [ReadOnly] public string areaName = "Steenbergen"; public Camera targetCam; + public bool asTreeInstances = true; + public int numRings = 5; public Vector2 wgs84Origin; - public int numRings = 2; - public float requiredTime = 3; - public float removeDelay = 7; + public float requiredTime = 1; + public float removeDelay = 1; public int maxTrees = 1000; public GameObject treePrefab; public DownloadTrees treenGenerator; @@ -44,18 +46,18 @@ namespace Wander private Dictionary<Vector2Int, TreeTile> treeDatabase = new(); private Dictionary<Vector2Int, TreeTile> activeTiles = new(); private bool loadedTrees; - private bool assignedTerrains; - private bool allowSpawning = false; + private bool allowSpawning = true; private List<int> freeTreeSlots = new(); public void SetGPSCoord( Dictionary<string, string> boundaryData ) { - var wgs84Lat = double.Parse( boundaryData["gpsx"] ); - var wgs84Lon = double.Parse( boundaryData["gpsy"] ); - wgs84Origin = new Vector2( (float)wgs84Lat, (float) wgs84Lon ); - tileSize = float.Parse( boundaryData["tileSize"] ); - areaName = boundaryData["areaName"]; + var wgs84Lat = double.Parse( boundaryData["gpsx"] ); + var wgs84Lon = double.Parse( boundaryData["gpsy"] ); + var boundsSize = float.Parse( boundaryData["boundsSize"] ); + wgs84Origin = new Vector2( (float)wgs84Lat, (float)wgs84Lon ); + tileSize = float.Parse( boundaryData["tileSize"] ); + areaName = boundaryData["areaName"]; // Derived RDUtils.GPS2RD( wgs84Lat, wgs84Lon, out originRDX, out originRDY ); @@ -77,7 +79,7 @@ namespace Wander for ( int i = 0; i < maxTrees; i++ ) { - Instantiate( treePrefab, treeRoot.transform ).SetActive( false ); + Instantiate( treePrefab, treeRoot.transform ).GetComponent<LODGroup>().enabled = false; freeTreeSlots.Add( i ); } @@ -144,29 +146,12 @@ namespace Wander transform.position = targetCam.transform.position; } - AssignTerrains(); LoadDesiredTiles(); SpawnTrees(); DespawnTrees(); - } - } - - void AssignTerrains() - { - if (assignedTerrains) - return; - assignedTerrains = true; - foreach (var item in treeDatabase) - { - var tile = item.Value; - if ( tile.trees.Count > 0 ) - { - var tree = item.Value.trees[0]; - var treePos = new Vector3( (float)(tree.x-originRDX), 0, (float)(tree.y-originRDY) ); - tile.terrain = TerrainFromWorldPos( treePos ); - } - } + activeTreeCount = maxTrees - freeTreeSlots.Count; + } } void LoadIfIsNewTile( Vector2Int centre, int dx, int dy, float time) @@ -177,10 +162,6 @@ namespace Wander if (treeDatabase.TryGetValue( coord, out var dbTile )) { activeTiles.Add( coord, dbTile ); - // if (!activeTile.rayCmds.IsCreated) - { - // activeTile.rayCmds = new NativeArray<RaycastCommand>( activeTile.trees.Count, Allocator.Persistent ); - } activeTile = dbTile; activeTile.spawnedTrees.Clear(); activeTile.spawnIndex = 0; @@ -188,7 +169,6 @@ namespace Wander activeTile.originTime = time; } } - if (activeTile != null) { activeTile.lastTime = time; @@ -213,11 +193,19 @@ namespace Wander bool HeightFromTerrains( TreeTile tile, Vector3 worldPos, out float height ) { height = 0; + + if ( tile.terrain == null && !tile.triedToObtainTerrain ) + { + tile.triedToObtainTerrain = true; + tile.terrain = TerrainFromWorldPos( worldPos ); + } + if ( tile.terrain != null ) { height = tile.terrain.SampleHeight( worldPos ); return true; } + return false; } @@ -227,9 +215,10 @@ namespace Wander float tpy = transform.position.z; float time = Time.time; - var rdX = Mathf.FloorToInt( (float) originRDX + tpx ) / 100; - var rdY = Mathf.FloorToInt( (float) originRDY + tpy ) / 100; + var rdX = Mathf.FloorToInt( (float)(( originRDX + tpx ) * 0.01f )); + var rdY = Mathf.FloorToInt( (float)(( originRDY + tpy ) * 0.01f )); var centreTile = new Vector2Int(rdX, rdY); + LoadIfIsNewTile( centreTile, 0, 0, time ); for (int c = 1;c <= numRings;c++) { @@ -241,11 +230,11 @@ namespace Wander { LoadIfIsNewTile( centreTile, c, y2, time ); } - for (int x2 = -c+1;x2 <= c-1;x2++) + for (int x2 = -c+1;x2 <= c-1 ;x2++) { LoadIfIsNewTile( centreTile, x2, c, time ); } - for (int x2 = -c+1;x2 <= c-1;x2++) + for (int x2 = -c+1;x2 <= c-1; x2++) { LoadIfIsNewTile( centreTile, x2, -c, time ); } @@ -254,12 +243,6 @@ namespace Wander void SpawnTrees() { - var mask = LayerMask.GetMask( "Default" ); - QueryParameters qp = new QueryParameters(); - qp.hitBackfaces = false; - qp.hitMultipleFaces = false; - qp.hitTriggers = QueryTriggerInteraction.Ignore; - qp.layerMask = mask; foreach (var kvp in activeTiles) { var tile = kvp.Value; @@ -284,6 +267,8 @@ namespace Wander continue; var treePos = new Vector3( (float)(treeDb.x-originRDX), 0, (float)(treeDb.y-originRDY) ); + + if ( HeightFromTerrains( tile, treePos, out float height )) { var slot = freeTreeSlots.Last(); @@ -292,51 +277,9 @@ namespace Wander var tree = treeRoot.transform.GetChild( slot ); tree.transform.position = treePos + new Vector3( 0, height-100, 0 ); //var lodGroup = tree.GetComponent<LODGroup>(); - tree.gameObject.SetActive( true ); + tree.gameObject.GetComponent<LODGroup>().enabled = true; } - //LOD [] lods = new LOD[4]; - //lods[0].renderers/ - //lodGroup.SetLODs - - - // RaycastCommand rc = new RaycastCommand(treePos, Vector3.down, 1000, mask, 1); - - - // if ( treePos.RaycastDown( 200, 500, mask, out var hit )) - //if( HeightFromTerrains( treePos, out float h, out Terrain ter )) - //{ - //// InstantiateAsync( treePrefab ).completed += (operation) => - // { - // // if ((operation as AsyncInstantiateOperation<GameObject>).Result.Length <= 0) - // { - // return; - // } - - // //TreeInstance ti = new TreeInstance(); - - - - // var spawnee = Instantiate(treePrefab); - // // var spawnee = (operation as AsyncInstantiateOperation<GameObject>).Result[0]; - // if (treeRoot == null) // Happens when stopped in editor in mean time. - // { - // spawnee.Destroy(); - // return; - // } - // // Tile might have been thrown away in the mean time. - // if (tile.spawned) - // { - // spawnee.transform.parent = treeRoot.transform; - // spawnee.transform.position = treePos + new Vector3( 0, h-100, 0 ); - // tile.spawnedTrees.Add( spawnee ); - // } - // else - // { - // spawnee.Destroy(); - // } - // }; - //} } } } @@ -351,18 +294,13 @@ namespace Wander { if ( tile.spawnedTrees.Count != 0 ) { - for( int i = 0; i< freeTreeSlots.Count; i++ ) + for( int i = 0; i< tile.spawnedTrees.Count; i++ ) { - treeRoot.transform.GetChild( freeTreeSlots[i] ).gameObject.SetActive( false ); + treeRoot.transform.GetChild( tile.spawnedTrees[i] ).gameObject.GetComponent<LODGroup>().enabled = false; } freeTreeSlots.AddRange( tile.spawnedTrees ); - //tile.spawnedTrees.Last().Destroy(); tile.spawnedTrees.Clear(); - //tile.spawnedTrees.RemoveAt( tile.spawnedTrees.Count-1 ); - //tile.spawnIndex--; - } - else - { + if (removeList==null) removeList = new(); removeList.Add( kvp.Key ); diff --git a/Runtime/Scripts/TreeStreamer2.cs b/Runtime/Scripts/TreeStreamer2.cs new file mode 100644 index 0000000000000000000000000000000000000000..13ff0693d9057cbd43ca7a9518f1ed98b1ad7440 --- /dev/null +++ b/Runtime/Scripts/TreeStreamer2.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.Collections; +using UnityEditor; +using UnityEngine; +using UnityEngine.AddressableAssets; + + +namespace Wander +{ + [ExecuteInEditMode] // Need this for SendMessage to be reveived without error in Edit mode. + public class TreeStreamer2 : MonoBehaviour + { + public class TreeTile + { + public float originTime; + public float lastTime; + public bool spawned; + public List<Vector3> treeDb = new(); + public int spawnIndex; + public Terrain terrain; + public bool triedToObtainTerrain; + public List<int> spawnedTrees = new(); + } + + public class TreeType + { + public GameObject type; + public int maxTrees; + internal List<int> freeSlots; + internal NativeArray<Matrix4x4> positions; + } + + [ReadOnly] public int activeTreeCount; + [ReadOnly] public double originRDX; + [ReadOnly] public double originRDY; + [ReadOnly] public float tileSize; + [ReadOnly] public GameObject treeRoot; + [ReadOnly] public string areaName = "Steenbergen"; + + public Camera targetCam; + public bool asTreeInstances = true; + public int numRings = 5; + public Vector2 wgs84Origin; + public float requiredTime = 2; + public float removeDelay = 1; + public List<TreeType> types = new(); + + private bool allowSpawning = true; + private Dictionary<Vector2Int, TreeTile> treeDatabase = new(); + private Dictionary<Vector2Int, TreeTile> activeTiles = new(); + private List<int> freeTreeSlots = new(); + private Task updateMatricesTask; + private bool isDone; + + // Variables copied from mainthread so can be accessed in other thread. + Vector3 cameraPosition; + float unityTime; + + public void SetGPSCoord( Dictionary<string, string> boundaryData ) + { + var wgs84Lat = double.Parse( boundaryData["gpsx"] ); + var wgs84Lon = double.Parse( boundaryData["gpsy"] ); + var boundsSize = float.Parse( boundaryData["boundsSize"] ); + wgs84Origin = new Vector2( (float)wgs84Lat, (float)wgs84Lon ); + tileSize = float.Parse( boundaryData["tileSize"] ); + areaName = boundaryData["areaName"]; + + // Derived + RDUtils.GPS2RD( wgs84Lat, wgs84Lon, out originRDX, out originRDY ); + +#if UNITY_EDITOR + EditorUtility.SetDirty( this ); +#endif + } + + private async void OnDestroy() + { + if ( updateMatricesTask != null ) + { + isDone = true; + await updateMatricesTask; + updateMatricesTask = null; + } + } + + private void Start() + { + if (!Application.isPlaying) + return; + + // In case no camera can be found, we have at least a valid position in other thread. + cameraPosition = transform.position; + + // If no default cam is set, fetch from main. + if ( targetCam == null ) + { + targetCam = Camera.main; + } + + // Load tree database. + Addressables.LoadAssetAsync<TextAsset>( "Assets/" + areaName + "/trees.csv" ).Completed += (operation) => + { + if ( !operation.IsDone || !operation.IsValid() || operation.Result == null ) + { + Debug.LogWarning( "Trees failed to load." ); + return; + } + + // Cannot acccess .text in other thread, so obtain ptr first. + var text = operation.Result.text; + Task.Run( () => + { + // Read tree database. + ReadTreeDb( text ); + + // Initially all matrix slots are free. + InitializeFreeMatriceSlots(); + + // Update matrices in the background. + UpdateLoopOtherThread(); + } ); + }; + + } + + private void Update() + { + if (!Application.isPlaying) + return; + + if (Input.GetKeyDown( KeyCode.Alpha1 )) + { + allowSpawning = !allowSpawning; + } + + if (targetCam != null) + { + unityTime = Time.time; + cameraPosition = targetCam.transform.position; + } + } + + void ReadTreeDb(string text) + { + var lines = text.Split( '\n' ); + for (var line = 1;line<lines.Length-1;line++) + { + try + { + var columns = lines[line].Split( ';' ); + if (columns.Length != 3) + continue; + var rdx = float.Parse( columns[0] ); + var rdy = float.Parse( columns[1] ); + int hectoX = Mathf.FloorToInt(rdx/100); + int hectoY = Mathf.FloorToInt(rdy/100); + if (!treeDatabase.TryGetValue( new Vector2Int( hectoX, hectoY ), out var tile )) + { + treeDatabase.Add( new Vector2Int( hectoX, hectoY ), tile=new() ); + } + tile.treeDb.Add( new Vector3( rdx, rdy, int.Parse( columns[2] ) ) ); + } + catch (Exception e) + { + Debug.LogError( e.Message ); + Debug.LogError( "On Line " + line ); + } + } + } + + void InitializeFreeMatriceSlots() + { + // Initially each slot in the matrix arrea is free. + foreach (var type in types) + { + type.freeSlots = new(); + for (int i = 0;i < type.maxTrees;i++) + { + freeTreeSlots.Add( i ); + } + } + } + + private void UpdateLoopOtherThread() + { + LoadDesiredTiles(); + SpawnTrees(); + DespawnTrees(); + + activeTreeCount = 0; + types.ForEach( t => { + activeTreeCount += t.maxTrees - t.freeSlots.Count; + } ); + } + + + + void LoadIfIsNewTile( Vector2Int centre, int dx, int dy, float time) + { + var coord = centre + new Vector2Int(dx, dy); + if (!activeTiles.TryGetValue( coord, out var activeTile )) + { + if (treeDatabase.TryGetValue( coord, out var dbTile )) + { + activeTiles.Add( coord, dbTile ); + activeTile = dbTile; + activeTile.spawnedTrees.Clear(); + activeTile.spawnIndex = 0; + activeTile.spawned = true; + activeTile.originTime = time; + } + } + if (activeTile != null) + { + activeTile.lastTime = time; + } + } + + Terrain TerrainFromWorldPos( Vector3 worldPos ) + { + var terrains = Terrain.activeTerrains; + for (int i = 0;i < terrains.Length;i++) + { + var t = terrains[i]; + if ( worldPos.x > t.transform.position.x && worldPos.x <= transform.position.x + tileSize && + worldPos.z > t.transform.position.z && worldPos.z <= transform.position.z + tileSize) + { + return t; + } + } + return null; + } + + bool HeightFromTerrains( TreeTile tile, Vector3 worldPos, out float height ) + { + height = 0; + + if ( tile.terrain == null && !tile.triedToObtainTerrain ) + { + tile.triedToObtainTerrain = true; + tile.terrain = TerrainFromWorldPos( worldPos ); + } + + if ( tile.terrain != null ) + { + height = tile.terrain.SampleHeight( worldPos ); + return true; + } + + return false; + } + + void LoadDesiredTiles() + { + float tpx = cameraPosition.x; + float tpy = cameraPosition.z; + float time = unityTime; + + var rdX = Mathf.FloorToInt( (float)(( originRDX + tpx ) * 0.01f )); + var rdY = Mathf.FloorToInt( (float)(( originRDY + tpy ) * 0.01f )); + var centreTile = new Vector2Int(rdX, rdY); + + LoadIfIsNewTile( centreTile, 0, 0, time ); + for (int c = 1;c <= numRings;c++) + { + for (int y2 = -c;y2 <= c;y2++) + { + LoadIfIsNewTile( centreTile, -c, y2, time ); + } + for (int y2 = -c;y2 <= c;y2++) + { + LoadIfIsNewTile( centreTile, c, y2, time ); + } + for (int x2 = -c+1;x2 <= c-1 ;x2++) + { + LoadIfIsNewTile( centreTile, x2, c, time ); + } + for (int x2 = -c+1;x2 <= c-1; x2++) + { + LoadIfIsNewTile( centreTile, x2, -c, time ); + } + } + } + + void SpawnTrees() + { + foreach (var kvp in activeTiles) + { + var tile = kvp.Value; + if (Time.time - tile.originTime < requiredTime) + { + continue; + } + while (tile.spawnIndex < tile.treeDb.Count) + { + if (freeTreeSlots.Count == 0) + break; + + var treeDb = tile.treeDb[tile.spawnIndex]; + tile.spawnIndex++; + + var treeType = Mathf.RoundToInt( treeDb.z ); + if (!(treeType >= 0 && treeType < treenGenerator.types.Count)) + continue; + + var treePrefab = treenGenerator.types[treeType].trees.Random(); + if (treePrefab == null) + continue; + + var treePos = new Vector3( (float)(treeDb.x-originRDX), 0, (float)(treeDb.y-originRDY) ); + + + if ( HeightFromTerrains( tile, treePos, out float height )) + { + var slot = freeTreeSlots.Last(); + freeTreeSlots.RemoveAt( freeTreeSlots.Count-1 ); + tile.spawnedTrees.Add( slot ); + var tree = treeRoot.transform.GetChild( slot ); + tree.transform.position = treePos + new Vector3( 0, height-100, 0 ); + var lg = tree.gameObject.GetComponent<LODGroup>(); + lg.ForceLOD( -1 ); + + } + } + } + } + + void DespawnTrees() + { + List<Vector2Int> removeList = null; + foreach (var kvp in activeTiles) + { + var tile = kvp.Value; + if (tile.lastTime - Time.time < -removeDelay) + { + if ( tile.spawnedTrees.Count != 0 ) + { + for( int i = 0; i< tile.spawnedTrees.Count; i++ ) + { + treeRoot.transform.GetChild( tile.spawnedTrees[i] ).gameObject.GetComponent<LODGroup>().ForceLOD( 4 ); + } + freeTreeSlots.AddRange( tile.spawnedTrees ); + tile.spawnedTrees.Clear(); + + if (removeList==null) + removeList = new(); + removeList.Add( kvp.Key ); + kvp.Value.spawned = false; + } + } + } + + removeList?.ForEach( rm => + { + activeTiles.Remove( rm ); + } ); + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/TreeStreamer2.cs.meta b/Runtime/Scripts/TreeStreamer2.cs.meta new file mode 100644 index 0000000000000000000000000000000000000000..b963e496218c476e9e0ab868e441fd27cd21980e --- /dev/null +++ b/Runtime/Scripts/TreeStreamer2.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4643b554a54fe66479884b76c4ca9d39 \ No newline at end of file