diff --git a/Runtime/Prefabs/TreeStreamer.prefab b/Runtime/Prefabs/TreeStreamer.prefab index 00d59a14f649e04d338d116e6a98c6b9d61453bb..9d405552e6a9c22a570272cc3a2588b2ad71a658 100644 --- a/Runtime/Prefabs/TreeStreamer.prefab +++ b/Runtime/Prefabs/TreeStreamer.prefab @@ -9,7 +9,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 8102445642857089983} - - component: {fileID: 5786002106602377772} + - component: {fileID: 591969842533917131} m_Layer: 0 m_Name: TreeStreamer m_TagString: Untagged @@ -32,7 +32,7 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &5786002106602377772 +--- !u!114 &591969842533917131 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -41,21 +41,23 @@ MonoBehaviour: m_GameObject: {fileID: 742251135161503034} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: acbdecd6bc179f647b5703a55ffdafe7, type: 3} + m_Script: {fileID: 11500000, guid: 4643b554a54fe66479884b76c4ca9d39, type: 3} m_Name: m_EditorClassIdentifier: + activeTreeCount: 0 originRDX: 220929.6988732884 originRDY: 450504.48700884776 tileSize: 860.16 treeRoot: {fileID: 0} areaName: DeMarke targetCam: {fileID: 0} + numRings: 5 wgs84Origin: {x: 52.03894, y: 6.348124} - numRings: 4 - requiredTime: 3 - removeDelay: 7 - maxTrees: 10000 - treePrefab: {fileID: 9046900170786976767, guid: 65926329d063fb84ea4ac2e63a6fcaa4, - type: 3} - treenGenerator: {fileID: -3667069338063608550, guid: ac835d606e3e19a48bdf1d863acfcfd0, - type: 3} + requiredTime: 1 + removeDelay: 1 + enableDisableKey: 49 + types: + - type: {fileID: 5556296651694026700, guid: 19df7a28074cb284a8aa8562718310f5, type: 3} + maxTrees: 20000 + - type: {fileID: 9046900170786976767, guid: 65926329d063fb84ea4ac2e63a6fcaa4, type: 3} + maxTrees: 20000 diff --git a/Runtime/Scripts/TreeStreamer2.cs b/Runtime/Scripts/TreeStreamer2.cs index 13ff0693d9057cbd43ca7a9518f1ed98b1ad7440..31f3d4acafe424e9f70aae6518b6f4dee40a4370 100644 --- a/Runtime/Scripts/TreeStreamer2.cs +++ b/Runtime/Scripts/TreeStreamer2.cs @@ -1,9 +1,12 @@ +using Codice.CM.Client.Differences; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Unity.Collections; using UnityEditor; +using UnityEditor.Graphs; using UnityEngine; using UnityEngine.AddressableAssets; @@ -17,20 +20,21 @@ namespace Wander { 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 int lod; + public Vector3 worldPos; + public Dictionary<int, (int offset, int num)> offsetAndNumPerType; } + [Serializable] public class TreeType { public GameObject type; public int maxTrees; - internal List<int> freeSlots; - internal NativeArray<Matrix4x4> positions; + internal int [] currentCount; + internal NativeArray<Matrix4x4> [] positions; } [ReadOnly] public int activeTreeCount; @@ -41,28 +45,28 @@ namespace Wander [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 KeyCode enableDisableKey = KeyCode.Alpha1; 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; + private Vector3 cameraPosition; + private float unityTime; + private System.Random random; public void SetGPSCoord( Dictionary<string, string> boundaryData ) { - var wgs84Lat = double.Parse( boundaryData["gpsx"] ); - var wgs84Lon = double.Parse( boundaryData["gpsy"] ); + 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"] ); @@ -84,6 +88,18 @@ namespace Wander await updateMatricesTask; updateMatricesTask = null; } + foreach (var type in types) + { + if (type.positions == null) + continue; + for (int i = 0;i < 4;i++) + { + if (type.positions[i] != null && type.positions[i].IsCreated) + { + type.positions[i].Dispose(); + } + } + } } private void Start() @@ -91,6 +107,8 @@ namespace Wander if (!Application.isPlaying) return; + random = new System.Random(); + // In case no camera can be found, we have at least a valid position in other thread. cameraPosition = transform.position; @@ -98,7 +116,23 @@ namespace Wander if ( targetCam == null ) { targetCam = Camera.main; - } + } + + // Initially each slot in the matrix arrea is free. + foreach (var type in types) + { + Debug.Assert( type.type != null, "No type specified." ); + type.currentCount = new int[4]; + type.positions = new NativeArray<Matrix4x4>[4]; + for (int i = 0;i<4;i++) + { + type.positions[i] = new NativeArray<Matrix4x4>( type.maxTrees, Allocator.Persistent ); + for (int j = 0;j<type.positions[i].Length;j++) + { + type.positions[i][j] = Matrix4x4.TRS( Vector3.zero, Quaternion.identity, Vector3.zero ); + } + } + } // Load tree database. Addressables.LoadAssetAsync<TextAsset>( "Assets/" + areaName + "/trees.csv" ).Completed += (operation) => @@ -110,17 +144,21 @@ namespace Wander } // Cannot acccess .text in other thread, so obtain ptr first. - var text = operation.Result.text; - Task.Run( () => + var text = operation.Result.text; + updateMatricesTask = Task.Run( () => { - // Read tree database. - ReadTreeDb( text ); - - // Initially all matrix slots are free. - InitializeFreeMatriceSlots(); + try + { + // Read tree database. + ReadTreeDb( text ); - // Update matrices in the background. - UpdateLoopOtherThread(); + // Update matrices in the background. + UpdateLoopOtherThread(); + } + catch (Exception e) + { + Debug.LogException( e ); + } } ); }; @@ -131,7 +169,7 @@ namespace Wander if (!Application.isPlaying) return; - if (Input.GetKeyDown( KeyCode.Alpha1 )) + if (Input.GetKeyDown( enableDisableKey )) { allowSpawning = !allowSpawning; } @@ -141,6 +179,40 @@ namespace Wander unityTime = Time.time; cameraPosition = targetCam.transform.position; } + + RenderTrees(); + } + + void RenderTrees() + { + for (int i = 0;i < types.Count;i++) + { + for (int lod = 0;lod < 4;lod++) + { + if (types[i].type.transform.childCount == lod) + break; + + var tr = types[i].type.transform.GetChild(lod); // LOD0; + var mesh = tr.GetComponent<MeshFilter>().sharedMesh; + var materials = tr.GetComponent<MeshRenderer>().sharedMaterials; + + int totalNum = types[i].currentCount[lod]; + int offset = 0; + while (totalNum > 0) + { + int numToRender = Math.Min( 1023, totalNum ); + + for (int j = 0;j<mesh.subMeshCount;j++) + { + RenderParams rp = new RenderParams(materials[j]); + Graphics.RenderMeshInstanced( rp, mesh, j, types[i].positions[lod], numToRender, offset ); + } + + totalNum -= numToRender; + offset += numToRender; + } + } + } } void ReadTreeDb(string text) @@ -169,36 +241,38 @@ namespace Wander Debug.LogError( "On Line " + line ); } } - } - - void InitializeFreeMatriceSlots() + } + + private void UpdateLoopOtherThread() { - // Initially each slot in the matrix arrea is free. - foreach (var type in types) + while (!isDone) { - type.freeSlots = new(); - for (int i = 0;i < type.maxTrees;i++) + LoadDesiredTiles(); + SpawnTrees(); + DespawnTrees(); + + int newActiveCount = 0; + types.ForEach( t => { - freeTreeSlots.Add( i ); - } + for (int i = 0;i <4;i++) + { + newActiveCount += t.currentCount[i]; + } + }); + activeTreeCount = newActiveCount; + + Thread.Sleep( 100 ); } } - private void UpdateLoopOtherThread() + int CalcLod( Vector3 pos ) { - LoadDesiredTiles(); - SpawnTrees(); - DespawnTrees(); - - activeTreeCount = 0; - types.ForEach( t => { - activeTreeCount += t.maxTrees - t.freeSlots.Count; - } ); + float dist = Vector3.Distance( cameraPosition, pos ); + int lod = Mathf.RoundToInt( dist / 400 ); + return lod; } - - - void LoadIfIsNewTile( Vector2Int centre, int dx, int dy, float time) + void LoadIfIsNewTile( Vector2Int centre, int dx, int dy ) { var coord = centre + new Vector2Int(dx, dy); if (!activeTiles.TryGetValue( coord, out var activeTile )) @@ -207,15 +281,22 @@ namespace Wander { activeTiles.Add( coord, dbTile ); activeTile = dbTile; - activeTile.spawnedTrees.Clear(); - activeTile.spawnIndex = 0; - activeTile.spawned = true; - activeTile.originTime = time; + activeTile.originTime = unityTime; + activeTile.worldPos = new Vector3( dx, 0, dy )*tileSize + new Vector3( tileSize, 0, tileSize ) *0.5f; + activeTile.lod = -1; + if (activeTile.offsetAndNumPerType == null) + { + activeTile.offsetAndNumPerType = new Dictionary<int, (int offset, int num)>(); + } + else + { + activeTile.offsetAndNumPerType.Clear(); + } } } if (activeTile != null) { - activeTile.lastTime = time; + activeTile.lastTime = unityTime; } } @@ -257,73 +338,112 @@ namespace Wander { 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 ); + LoadIfIsNewTile( centreTile, 0, 0 ); for (int c = 1;c <= numRings;c++) { for (int y2 = -c;y2 <= c;y2++) { - LoadIfIsNewTile( centreTile, -c, y2, time ); + LoadIfIsNewTile( centreTile, -c, y2 ); } for (int y2 = -c;y2 <= c;y2++) { - LoadIfIsNewTile( centreTile, c, y2, time ); + LoadIfIsNewTile( centreTile, c, y2 ); } for (int x2 = -c+1;x2 <= c-1 ;x2++) { - LoadIfIsNewTile( centreTile, x2, c, time ); + LoadIfIsNewTile( centreTile, x2, c ); } for (int x2 = -c+1;x2 <= c-1; x2++) { - LoadIfIsNewTile( centreTile, x2, -c, time ); + LoadIfIsNewTile( centreTile, x2, -c ); } } } + void RemoveTileFromTypeArrays( TreeTile tile, int oldLod ) + { + if (oldLod == -1) + return; + + foreach (var spawnees in tile.offsetAndNumPerType) + { + var type = spawnees.Key; + var offset = spawnees.Value.offset; + var numSpawn = spawnees.Value.num; + var numToMove = types[type].currentCount[oldLod] - (offset+numSpawn); + for (int i = 0;i < numToMove;i++) + { + types[type].positions[oldLod][offset+i] = + types[type].positions[oldLod][offset+i+numSpawn]; + } + types[type].currentCount[oldLod] -= numSpawn; + } + + tile.offsetAndNumPerType.Clear(); + } + void SpawnTrees() { foreach (var kvp in activeTiles) { var tile = kvp.Value; - if (Time.time - tile.originTime < requiredTime) + + // Only start spawning trees from tile, if certain time tile is requested. + if (unityTime - 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; + // Only change trees, when lod has changed of tile. + int newLod = Mathf.Clamp( CalcLod( tile.worldPos ), 0, 3 ); + if (newLod == tile.lod) + { + continue; + } + int oldLod = tile.lod; + tile.lod = newLod; + + RemoveTileFromTypeArrays( tile, oldLod ); - var treePrefab = treenGenerator.types[treeType].trees.Random(); - if (treePrefab == null) - continue; + // For each tree in tile, see if the type of tree can be spawned as there are max tree counts per type. + for (int i = 0; i< tile.treeDb.Count; i++) + { + var treeDb = tile.treeDb[i]; + var type = Mathf.RoundToInt( treeDb.z ); - var treePos = new Vector3( (float)(treeDb.x-originRDX), 0, (float)(treeDb.y-originRDY) ); + // If type is invalid, continue to next. Don't spawn this one. + if (type < 0 || type >= types.Count) + { + continue; + } - - 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 ); + // See if free slot available. + if (types[type].currentCount[newLod] < types[type].positions[newLod].Length) + { + // Find out offset on type per lod. + int offset = types[type].currentCount[newLod]++; - } + // Obtain tree position. + var treePos = new Vector3( (float)(treeDb.x-originRDX), 20, (float)(treeDb.y-originRDY) ); + + // Set position on type per lod, using offset. + types[type].positions[newLod][offset] = Matrix4x4.TRS( treePos, Quaternion.Euler( 0, random.Next()%360, 0 ), Vector3.one ); + + // Remember the offset and num spawned per type. Because need to move per type num away, to avoid rendering instances that are not visible (vertex processor is invoked). + if (!tile.offsetAndNumPerType.TryGetValue( type, out var offsetAndType )) + { + tile.offsetAndNumPerType.Add( type, (offset, 1) ); + } + else + { + offsetAndType.num++; + } + } } } } @@ -334,25 +454,23 @@ namespace Wander foreach (var kvp in activeTiles) { var tile = kvp.Value; - if (tile.lastTime - Time.time < -removeDelay) + + // Only despawn tile if certain time not desired anymore. This to avoid flipping in and out. + if (tile.lastTime - unityTime < -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(); + // Remove trees for type arrays. + RemoveTileFromTypeArrays( tile, tile.lod ); - if (removeList==null) - removeList = new(); - removeList.Add( kvp.Key ); - kvp.Value.spawned = false; - } + // Initialize remove list if was not used yet. + if (removeList==null) + removeList = new(); + + // Add this tile to remove list. + removeList.Add( kvp.Key ); } } + // Remove actual inactive tiles. removeList?.ForEach( rm => { activeTiles.Remove( rm );