2020-11-28 16:54:41 +02:00

594 lines
23 KiB
C#

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,List<int>>();
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<int>());
// 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<int>();
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<grassCell>();
var cellIdTilemap = new Dictionary<int,List<int>>();
foreach(var x in tilemap) {
//foreach entry convert indexes into how many cells are needed
var tileCellIDs = new List<int>();
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<int>();
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<int,List<int>>();
foreach(var x in tilemap) occlusionCells.Add(x.Key,new List<int>());
foreach(var x in fullTilemap) if(!occlusionCells.ContainsKey(x)) occlusionCells.Add(x,new List<int>());
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
*/