using System.Collections; using System.Collections.Generic; using UnityEngine; public class GrassLoader : MonoBehaviour { public static GrassLoader instance; void Awake () { instance = this; } [Header("Requiered variables")] public Mesh grassMesh; public Mesh boxMesh; public Material instancedGrassMaterial; public Material instancedConsumeGrassMaterial; public GrassPositionScriptableObject grassStorage; public ComputeShader cullShader; public ComputeShader convertArrayShader; [Header("Tilemap (Occlusion culling)")] public Vector3 bottomLeftCorner; public float cellSideLengh = 1f; public float totalLength = 100f; [Header("3D culling")] public Vector2 distanceBounds; public Texture2D ditherTexture; public float ditherSize; [Header("Manual controls")] public bool update; public bool updateData; public bool bakeCells; public bool bakeOcclusion; [Header("Testing")] public bool debugTilemap1; public bool debugTilemap2; public bool debugTilemap3; public bool debugTilemap4; public bool showTilemap; [Space] public bool doCells; public bool doCulling; public bool doOcclusionCulling; //[Header("Testing")] //public bool update = false; //data types struct grassElement { public Vector3 position; public float rotation; public float size; public float colorBlend;//0-1 public grassElement (Vector3 position, float rotation, float size, float colorBlend) { this.position = position; this.rotation = rotation; this.size = size; this.colorBlend = colorBlend; } //Total size of is 4*3 + 3*4 = 24 } struct grassCell { public uint[] grassElementIndexes;//map to grass elements public grassCell (uint[] grassElementIndexes) { this.grassElementIndexes = grassElementIndexes; } public grassCell(grassCell x) { grassElementIndexes = new uint[x.grassElementIndexes.Length]; for(int i = 0;i < x.grassElementIndexes.Length;i++){ grassElementIndexes[i] = x.grassElementIndexes[i] * 1; } } //Total size = 4*length = must be calculated } struct grassOcclusionCell { public uint grassCellIndex; public uint[] visibleCellIndexes;//Map to occlusion cells public grassOcclusionCell (uint grassCellIndex, uint[] visibleCellIndexes) { this.grassCellIndex = grassCellIndex; this.visibleCellIndexes = visibleCellIndexes; } //Total size = 4 + 4*length = must be calculated } //buffers uint[] arguments = new uint[5] {0,0,0,0,0}; //main ComputeBuffer grassBuffer; ComputeBuffer argumentsBuffer; ComputeBuffer allArgumentsBuffer; //culling ComputeBuffer workBuffer;//Append/Consume buffer for grass elements ComputeBuffer grassCellBuffer; ComputeBuffer cellWorkBuffer; ComputeBuffer convertSizeBuffer; public bool isOn = true; int elementCount = 0; int cellCount = 0; bool startupDone; grassElement[] grassElements; Camera cam; public void BakeCells () { if(grassElements == null) UpdateMainBuffers(); bakeCells = false; //Maps points into cells // 0. Create tilemap var tilemap = new Dictionary>(); int cellsCount = Mathf.CeilToInt(totalLength/cellSideLengh); for(int x = 0;x < cellsCount;x++) for(int y = 0;y < cellsCount;y++) tilemap.Add(x + y*cellsCount,new List()); // 1. Set grassObjects into tiles for(int i = 0;i < elementCount;i++) tilemap[GetTileIndex(grassElements[i].position)].Add(i); //Debug if(debugTilemap1) for(int x = 0;x < cellsCount;x++){ for(int y = 0;y < cellsCount;y++){ var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh + Vector3.one * cellSideLengh/2f; int i = GetTileIndex(pos); if(tilemap.ContainsKey(i)) foreach(var k in tilemap[i]) Debug.DrawLine(grassElements[k].position + Vector3.right* 0.2f, pos,Color.green,100f); } } // 2. Cull empty tiles var removables = new Queue(); foreach(var x in tilemap) if(x.Value.Count == 0) removables.Enqueue(x.Key); foreach(var x in removables) tilemap.Remove(x); //Debug if(debugTilemap2) for(int x = 0;x < cellsCount;x++){ for(int y = 0;y < cellsCount;y++){ var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh + Vector3.one * cellSideLengh/2f + Vector3.up; int i = GetTileIndex(pos); if(tilemap.ContainsKey(i)) foreach(var k in tilemap[i]) {Debug.DrawLine(grassElements[k].position + Vector3.right* 0.2f, pos,Color.magenta,100f); Debug.DrawRay(pos, Vector3.up,Color.magenta,100f);} } } // 3. Get cell average grass count - not needed anymore //float total = 0; float count = 0; //foreach(var x in tilemap) { total += x.Value.Count; count++; } //int mean = (int)(total/count); //TODO 4. Convert tiles into cells of 512 elements (512 bc byte limit is 2048 and 1 index = 4 bytes so 512*4 = 2048) var cells = new List(); var cellIdTilemap = new Dictionary>(); foreach(var x in tilemap) { //foreach entry convert indexes into how many cells are needed var tileCellIDs = new List(); var cellGrassIndexes = x.Value; int leftToProcess = cellGrassIndexes.Count; while(leftToProcess > 0) { //Create new cell var grassIndexArray = new uint[512]; for(int i = 0;i < grassIndexArray.Length;i++) { if(cellGrassIndexes.Count > 0) { grassIndexArray[i] = (uint)(cellGrassIndexes[0]); cellGrassIndexes.RemoveAt(0); leftToProcess--; } else grassIndexArray[i] = 0; } cells.Add( new grassCell(grassIndexArray) ); //Remember id tileCellIDs.Add(cells.Count-1); } //Save cell ids so they can be used in occlusion culling cellIdTilemap.Add(x.Key,tileCellIDs); } //Debug, foreach cell show which cells they map to by each cells connections if(debugTilemap3) for(int x = 0;x < cellsCount;x++){ for(int y = 0;y < cellsCount;y++){ var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh + Vector3.one * cellSideLengh/2f + Vector3.up * 2f; int i = GetTileIndex(pos); if(cellIdTilemap.ContainsKey(i)) { //foreach cell get center foreach(var cellIndex in cellIdTilemap[i]) { var cell = cells[cellIndex]; Vector3 center = Vector3.zero; float count = 0; foreach(var grassIndex in cell.grassElementIndexes) { if(grassIndex > 0) {center += grassElements[grassIndex].position; count++; Debug.DrawLine(grassElements[grassIndex].position, pos,Color.white,100f);} } Debug.DrawLine(center / count, pos,Color.blue,100f); } } } } // 5. Convert cells into array (bc array in a struct is not bittable...) cellCount = cells.Count; var convertedArray = new uint[cellCount * 512]; for(int i = 0;i < cellCount;i++){ var c = cells[i]; for(int j = 0;j < 512;j++) { convertedArray[j + i * 512] = c.grassElementIndexes[j]; if(debugTilemap4) Debug.DrawRay(grassElements[c.grassElementIndexes[j]].position, Vector3.up,Color.magenta,100f); } } //Wipe old buffers if(grassCellBuffer != null) grassCellBuffer.Release(); if(cellWorkBuffer != null) cellWorkBuffer.Release(); if(convertSizeBuffer != null) convertSizeBuffer.Release(); //Create new buffers grassCellBuffer = new ComputeBuffer(cellCount, 2048); cellWorkBuffer = new ComputeBuffer(cellCount, 4, ComputeBufferType.Append); convertSizeBuffer = new ComputeBuffer(1, 12, ComputeBufferType.IndirectArguments); //Upload data grassCellBuffer.SetData(convertedArray); cellWorkBuffer.SetCounterValue(0); convertSizeBuffer.SetData(new uint[]{1,1,1}); //Send buffers to shaders cullShader.SetBuffer(1, "grassBuffer", grassBuffer); cullShader.SetBuffer(1, "grassCellBuffer", grassCellBuffer); cullShader.SetBuffer(1, "outCellIndexBuffer", cellWorkBuffer); //cullShader.SetBuffer(1, "outGrassBuffer", workBuffer); convertArrayShader.SetBuffer(0, "grassCellBuffer", grassCellBuffer); convertArrayShader.SetBuffer(0, "outGrassBuffer", workBuffer); convertArrayShader.SetBuffer(0, "grassCellIndexBuffer", cellWorkBuffer); //Set buffer values cullShader.SetInt("grassCellCount", cellCount); cullShader.SetInt("grassCellSize", Mathf.CeilToInt(cellSideLengh)); cullShader.SetVector("distanceBounts", distanceBounds); cullShader.SetTexture(0, "ditherTex", ditherTexture); cullShader.SetTexture(1, "ditherTex", ditherTexture); cullShader.SetFloat("ditherTexSizeX", ditherTexture.width); cullShader.SetFloat("ditherSize", ditherSize); Debug.Log("GRASS: Created " + cellCount.ToString() + " cells"); } public void BakeOcclusionCells () { /* bakeOcclusion = false; //Creates occlusion cells //OCCLUSION GENERATION (from created cell structs) // 1. Create target low and full objects var grassCollider = new GameObject(); grassCollider.transform.name = "TEMP_GRASS_COLLIDER"; grassCollider.AddComponent(typeof(BoxCollider)); var gMeshFilter = grassCollider.AddComponent(typeof(MeshFilter)) as MeshFilter; var gMeshRenderer = grassCollider.AddComponent(typeof(MeshRenderer)) as MeshRenderer; gMeshFilter.mesh = boxMesh; grassCollider.transform.localScale = new Vector3(cellSideLengh,0.5f,cellSideLengh); var fallableCollider = new GameObject(); fallableCollider.transform.name = "TEMP_FALLABLE_COLLIDER"; fallableCollider.AddComponent(typeof(BoxCollider)); var fMeshFilter = fallableCollider.AddComponent(typeof(MeshFilter)) as MeshFilter; var fMeshRenderer = fallableCollider.AddComponent(typeof(MeshRenderer)) as MeshRenderer; fMeshFilter.mesh = boxMesh; fallableCollider.transform.localScale = new Vector3(cellSideLengh,20f,cellSideLengh); // 2. Foreach cell try to see each other cell, if not already marked visible //Create full tilemap of all possible places where camera might be var fullTilemap = new HashSet(); for(int x = 0;x < cellCount;x++){ for(int y = 0;y < cellCount;y++){ var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh; if(Physics.Raycast(pos + Vector3.up*100f, -Vector3.up, Mathf.Infinity)) fullTilemap.Add(GetTileIndex(pos)); } } var occlusionCells = new Dictionary>(); foreach(var x in tilemap) occlusionCells.Add(x.Key,new List()); foreach(var x in fullTilemap) if(!occlusionCells.ContainsKey(x)) occlusionCells.Add(x,new List()); void PositionColliderAtPostion (Vector3 pos, Transform collider) { RaycastHit hit; if(Physics.Raycast(pos + Vector3.up*100f, -Vector3.up, out hit, Mathf.Infinity)) { collider.position = new Vector3(pos.x,hit.point.y + collider.localScale.y/2f,pos.z); } else collider.position = new Vector3(pos.x,0,pos.z); } bool DoRaycastTest (Vector3 from, Vector3 to, string targetName) { //Will shoot rays 1. from the bottom of from, then from middle and then from top, if hit then returns true, else false bool ShootRays (Vector3 pos) { RaycastHit hit; for(int i = 0;i < 5;i++){ var start = pos + new Vector3(Random.Range(0,1f),Random.Range(0,.5f),Random.Range(0,1f)); var target = to + new Vector3(Random.Range(0,1f),Random.Range(0,.5f),Random.Range(0,1f)); if( Physics.Raycast(start, target-start, out hit, Mathf.Infinity) ) if(hit.transform.name == targetName) return true; } return false; } if(ShootRays(from)) return true; if(ShootRays(from + Vector3.up * 2f)) return true; if(ShootRays(from + Vector3.up * 4f)) return true; return false; } void TestVisibilityBetweenCells (int i, int j) { //0. see if already done var oI = occlusionCells[i]; var oJ = occlusionCells[j]; if(!oI.Contains(j)) { //1. Get both positions var posI = bottomLeftCorner + (i % cellCount) * Vector3.forward * cellSideLengh + Mathf.FloorToInt(i/cellCount) * Vector3.right * cellSideLengh; var posJ = bottomLeftCorner + (j % cellCount) * Vector3.forward * cellSideLengh + Mathf.FloorToInt(j/cellCount) * Vector3.right * cellSideLengh; //2. Try visibility form i -> j PositionColliderAtPostion(posJ, grassCollider.transform); if(DoRaycastTest(posI, posJ, "TEMP_GRASS_COLLIDER")) { //3. If possible then register both as possible oI.Add(j); oJ.Add(i); } } } int k = 0; float t = fullTilemap.Count * tilemap.Count; foreach(var x in fullTilemap) foreach(var y in tilemap) if(x != y.Key) {TestVisibilityBetweenCells(x,y.Key); k++; if(k%30000==0) {yield return 0;Debug.Log((k/t).ToString() + "% done");} } //Do all possible tests Destroy(grassCollider); Destroy(fallableCollider); //Debug show connections //TODO 3. Get cell average seeable length. //TODO 4. Convert all cells into structs, if more seeables then create more structs per cell //TODO Give cell and occulsion data to cullShader //1. Create walkable area //2. Foreach cell in walkable area find all grass cells that it sees //3. Save data to storage */ } public void Setup () { cam = Camera.main; //arguments buffer argumentsBuffer = new ComputeBuffer(1, arguments.Length * sizeof(uint), ComputeBufferType.IndirectArguments); allArgumentsBuffer = new ComputeBuffer(1, arguments.Length * sizeof(uint), ComputeBufferType.IndirectArguments); //Main buffer UpdateMainBuffers(); //Cells BakeCells(); //Occlusion //BakeOcclusionCells(); } void DebugWorkBufferCount () { var tempBuffer = new ComputeBuffer(1, sizeof(uint), ComputeBufferType.IndirectArguments); uint[] x = new uint[1]; ComputeBuffer.CopyCount(workBuffer,tempBuffer,0); tempBuffer.GetData(x); var s = ""; foreach(var k in x) s += " " + k.ToString(); Debug.Log(s); tempBuffer.Release(); } void DebugWorkCellBufferCount () { var tempBuffer = new ComputeBuffer(1, sizeof(uint), ComputeBufferType.IndirectArguments); uint[] x = new uint[1]; ComputeBuffer.CopyCount(cellWorkBuffer,tempBuffer,0); tempBuffer.GetData(x); var s = ""; foreach(var k in x) s += " " + k.ToString(); Debug.Log(s); tempBuffer.Release(); } public void Run () { if(!startupDone) return;//Wait for startup do complete //0. manual update if(update) UpdateMainBuffers(); if(bakeCells) BakeCells(); //1. UpdateCullBuffer if(doCulling) UpdateCullBuffer(); //Draw if(isOn) Graphics.DrawMeshInstancedIndirect(grassMesh, 0, doCulling?instancedConsumeGrassMaterial:instancedGrassMaterial, new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f)), doCulling?argumentsBuffer:allArgumentsBuffer); //Testing if(showTilemap) { int cellsCount = Mathf.CeilToInt(totalLength/cellSideLengh); for(int x = 0;x < cellsCount;x++){ for(int y = 0;y < cellsCount;y++){ var pos = bottomLeftCorner + (x * Vector3.forward + y * Vector3.right)*cellSideLengh; Debug.DrawRay(pos, Vector3.up * 5f,Color.blue); } } } } void UpdateCullBuffer () { //1. Update frustrum culling information var cam = Camera.main; var mat = cam.projectionMatrix * cam.worldToCameraMatrix; var normals = new Vector3[4]; var normalsMatrix = new float[12]; Vector3 temp; //left temp.x = mat.m30 + mat.m00; temp.y = mat.m31 + mat.m01; temp.z = mat.m32 + mat.m02; normals[0] = temp; //right temp.x = mat.m30 - mat.m00; temp.y = mat.m31 - mat.m01; temp.z = mat.m32 - mat.m02; normals[1] = temp; //bottom temp.x = mat.m30 + mat.m10; temp.y = mat.m31 + mat.m11; temp.z = mat.m32 + mat.m12; normals[2] = temp; //top temp.x = mat.m30 - mat.m10; temp.y = mat.m31 - mat.m11; temp.z = mat.m32 - mat.m12; normals[3] = temp; for (int i = 0; i < 4; i++){ //Debug.DrawRay(camera.transform.position, _planes[i].normal * 10f, Color.yellow); normalsMatrix[i + 0] = normals[i].x; normalsMatrix[i + 4] = normals[i].y; normalsMatrix[i + 8] = normals[i].z; } cullShader.SetFloats("cameraPos", cam.transform.position.x, cam.transform.position.y, cam.transform.position.z); cullShader.SetInt("doOcclusion",doOcclusionCulling?1:0); cullShader.SetFloats("cameraFrustumNormals", normalsMatrix); //Reset if(cellWorkBuffer != null) cellWorkBuffer.SetCounterValue(0); workBuffer.SetCounterValue(0); //2. Dispatch culler if(doCells && cellCount > 0 && cellWorkBuffer != null) { //1. Get cell indexes int batchCount = Mathf.CeilToInt(cellCount/64f); cullShader.Dispatch(1,batchCount,1,1); //2. Get object indexes //convertSizeBuffer.SetData(new uint[] {1,1,1}); ComputeBuffer.CopyCount(cellWorkBuffer,convertSizeBuffer,0); convertArrayShader.DispatchIndirect(0,convertSizeBuffer); } else { int batchCount = Mathf.CeilToInt(elementCount/1024f); cullShader.Dispatch(0,batchCount,1,1); } //3. Copy work count -> arguments ComputeBuffer.CopyCount(workBuffer,argumentsBuffer,4); } int GetTileIndex (Vector3 pos) { int cellsCount = Mathf.CeilToInt(totalLength/cellSideLengh); //1. relative pos var p = pos - bottomLeftCorner; //2. safety if(p.x < 0 || p.x > totalLength || p.z < 0 || p.z > totalLength) return 0; //3. get index return Mathf.CeilToInt(p.x/cellSideLengh) + cellsCount * Mathf.CeilToInt(p.z/cellSideLengh); } void UpdateMainBuffers () { startupDone = false; update = false; //Debug.Log("Updating main"); elementCount = grassStorage.points.Count; //Reset buffers if(workBuffer != null) workBuffer.Release(); if(grassBuffer != null) grassBuffer.Release(); grassBuffer = new ComputeBuffer(elementCount, 24); workBuffer = new ComputeBuffer(elementCount, 4, ComputeBufferType.Append); workBuffer.SetCounterValue(0);//effectivly this clears the buffer //Create grass elements from data grassElements = new grassElement[elementCount]; for(int i = 0;i < elementCount;i++){ grassElements[i] = new grassElement( grassStorage.points[i], Random.Range(0f,360f), Random.Range(0.9f,1f), Random.Range(0,1f) ); //Debug.DrawRay(grassStorage.points[i] + Vector3.right* 0.2f, Vector3.up * 10f,Color.yellow,100f); } Debug.Log("GRASS: Working with a total of " + grassElements.Length.ToString() + " grass objects"); //Save data to buffer grassBuffer.SetData(grassElements); distanceBounds = new Vector2(Mathf.Min(distanceBounds.y, distanceBounds.x),Mathf.Max(distanceBounds.y, distanceBounds.x)) ; //Set buffers cullShader.SetInt("grassCount", elementCount); cullShader.SetFloat("maxDistance", distanceBounds.y); cullShader.SetBuffer(0, "grassBuffer", grassBuffer); cullShader.SetBuffer(0, "outGrassBuffer", workBuffer); instancedConsumeGrassMaterial.SetBuffer("grassBuffer", grassBuffer); instancedConsumeGrassMaterial.SetBuffer("inGrassBuffer", workBuffer); instancedGrassMaterial.SetBuffer("grassBuffer", grassBuffer); instancedGrassMaterial.SetFloat("fadeStartDist", distanceBounds.x - cellSideLengh); instancedGrassMaterial.SetFloat("fadeEndDist", distanceBounds.y - cellSideLengh); //Set arguments arguments[0] = (grassMesh != null) ? (uint)grassMesh.GetIndexCount(0) : 0; arguments[1] = (uint)elementCount; argumentsBuffer.SetData(arguments); allArgumentsBuffer.SetData(arguments); if(doCulling) UpdateCullBuffer(); startupDone = true; } void OnDisable() { if(grassBuffer != null) grassBuffer.Release(); grassBuffer = null; if(argumentsBuffer != null) argumentsBuffer.Release(); argumentsBuffer = null; if(allArgumentsBuffer != null) allArgumentsBuffer.Release(); allArgumentsBuffer = null; if(workBuffer != null) workBuffer.Release(); workBuffer = null; if(grassCellBuffer != null) grassCellBuffer.Release(); grassCellBuffer = null; if(cellWorkBuffer != null) cellWorkBuffer.Release(); cellWorkBuffer = null; if(convertSizeBuffer != null) convertSizeBuffer.Release(); convertSizeBuffer = null; } void Start () { Setup(); } void Update () { Run(); } } /* TODOS 1. Occlusion - Switch to quads instead of traingles for grass - Try shape that is more similar to cutout - Add Wind - Second billboard shader - Billboard switch range - Standard cull two outputs - Cell cull two outputs - Cell cull second converter - Free camera - Cell optimizations - Distance dither cull? (optional) 2. Fallable particles (Rain, snow) - Plan 2.1 Rain drop splashes 3. Volumetric fog with moving 3D noise and godrays? 4. Rain puddles, shininess */