534 lines
18 KiB
C#
534 lines
18 KiB
C#
#if GRIFFIN
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using Unity.Collections;
|
|
using Unity.Jobs;
|
|
using UnityEngine.Rendering;
|
|
|
|
namespace Pinwheel.Griffin.Rendering
|
|
{
|
|
public class GGrassRenderer
|
|
{
|
|
public delegate void ConfiguringMaterialHandler(GStylizedTerrain terrain, int prototypeIndex, MaterialPropertyBlock propertyBlock);
|
|
public static event ConfiguringMaterialHandler ConfiguringMaterial;
|
|
|
|
private GStylizedTerrain terrain;
|
|
|
|
private Camera camera;
|
|
private Plane[] frustum;
|
|
private int[] cellCullResults;
|
|
private int[] cellCulledFrameCount;
|
|
private float[] cellSqrDistanceToCam;
|
|
private Bounds[] cellWorldBounds;
|
|
private List<int> visibleCells;
|
|
private List<int> cellToProcess;
|
|
|
|
private const int CULLED = 0;
|
|
private const int VISIBLE = 1;
|
|
private const int FRAME_COUNT_TO_UNLOAD_CELL = 100;
|
|
|
|
private Matrix4x4 normalizedToLocalMatrix;
|
|
private Matrix4x4 localToWorldMatrix;
|
|
private Matrix4x4 normalizedToWorldMatrix;
|
|
|
|
private float grassDistance;
|
|
private Vector3 terrainPosition;
|
|
private Vector3 terrainSize;
|
|
private float cullBias;
|
|
|
|
private GGrassPatch[] cells;
|
|
private GGrassPatchData[] cellsData;
|
|
private GGrassPatchNativeData[] cellsNativeData;
|
|
private List<GGrassPrototype> prototypes;
|
|
private MaterialPropertyBlock[] propertyBlocks;
|
|
private IGGrassMaterialConfigurator materialConfigurator;
|
|
|
|
private Mesh[] baseMeshes;
|
|
private Material[] materials;
|
|
|
|
private const int BATCH_MAX_INSTANCE_COUNT = 1023;
|
|
|
|
private bool willIgnoreCellLimit;
|
|
|
|
public GGrassRenderer(GStylizedTerrain terrain)
|
|
{
|
|
this.terrain = terrain;
|
|
RecalculateCellBounds();
|
|
}
|
|
|
|
private void RecalculateCellBounds()
|
|
{
|
|
GGrassPatch[] cells = terrain.TerrainData.Foliage.GrassPatches;
|
|
for (int i = 0; i < cells.Length; ++i)
|
|
{
|
|
cells[i].RecalculateBounds();
|
|
}
|
|
}
|
|
|
|
public void Render(Camera cam)
|
|
{
|
|
try
|
|
{
|
|
if (GRuntimeSettings.Instance.isEditingGeometry)
|
|
return;
|
|
if (!CheckSystemInstancingAvailable())
|
|
return;
|
|
InitFrame(cam);
|
|
if (CullTerrain())
|
|
return;
|
|
CullCells();
|
|
CalculateAndCacheTransforms();
|
|
BuildBatches();
|
|
ConfigureMaterial();
|
|
Submit();
|
|
}
|
|
catch (GSkipFrameException)
|
|
{ }
|
|
|
|
CleanUpFrame();
|
|
}
|
|
|
|
private bool CheckSystemInstancingAvailable()
|
|
{
|
|
return SystemInfo.supportsInstancing;
|
|
}
|
|
|
|
private void InitFrame(Camera cam)
|
|
{
|
|
if (cullBias != GRuntimeSettings.Instance.renderingDefault.grassCullBias)
|
|
{
|
|
ClearAllCells();
|
|
}
|
|
|
|
cullBias = GRuntimeSettings.Instance.renderingDefault.grassCullBias;
|
|
terrainPosition = terrain.transform.position;
|
|
terrainSize = terrain.TerrainData.Geometry.Size;
|
|
grassDistance = terrain.TerrainData.Rendering.GrassDistance;
|
|
cells = terrain.TerrainData.Foliage.GrassPatches;
|
|
willIgnoreCellLimit =
|
|
cellsData == null ||
|
|
GRuntimeSettings.Instance.isEditingFoliage;
|
|
|
|
if (cellsData == null || cellsData.Length != cells.Length)
|
|
{
|
|
cellsData = new GGrassPatchData[cells.Length];
|
|
}
|
|
for (int i = 0; i < cellsData.Length; ++i)
|
|
{
|
|
if (cellsData[i] == null)
|
|
{
|
|
cellsData[i] = new GGrassPatchData();
|
|
}
|
|
}
|
|
|
|
if (cellsNativeData == null || cellsNativeData.Length != cells.Length)
|
|
{
|
|
cellsNativeData = new GGrassPatchNativeData[cells.Length];
|
|
}
|
|
|
|
if (visibleCells == null)
|
|
{
|
|
visibleCells = new List<int>();
|
|
}
|
|
if (cellToProcess == null)
|
|
{
|
|
cellToProcess = new List<int>();
|
|
}
|
|
|
|
if (terrain.TerrainData.Foliage.Grasses != null)
|
|
{
|
|
prototypes = terrain.TerrainData.Foliage.Grasses.Prototypes;
|
|
if (propertyBlocks == null || propertyBlocks.Length != prototypes.Count)
|
|
{
|
|
propertyBlocks = new MaterialPropertyBlock[prototypes.Count];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
prototypes = new List<GGrassPrototype>();
|
|
}
|
|
|
|
if (materialConfigurator == null)
|
|
{
|
|
materialConfigurator = System.Activator.CreateInstance<GSimpleGrassMaterialConfigurator>();
|
|
}
|
|
|
|
normalizedToLocalMatrix = Matrix4x4.Scale(terrainSize);
|
|
localToWorldMatrix = terrain.transform.localToWorldMatrix;
|
|
normalizedToWorldMatrix = localToWorldMatrix * normalizedToLocalMatrix;
|
|
|
|
camera = cam;
|
|
if (frustum == null)
|
|
{
|
|
frustum = new Plane[6];
|
|
}
|
|
GFrustumUtilities.Calculate(camera, frustum, grassDistance);
|
|
|
|
if (cellCullResults == null || cellCullResults.Length != cells.Length)
|
|
{
|
|
cellCullResults = new int[cells.Length];
|
|
}
|
|
|
|
if (cellCulledFrameCount == null || cellCulledFrameCount.Length != cells.Length)
|
|
{
|
|
cellCulledFrameCount = new int[cells.Length];
|
|
}
|
|
|
|
if (cellSqrDistanceToCam == null || cellSqrDistanceToCam.Length != cells.Length)
|
|
{
|
|
cellSqrDistanceToCam = new float[cells.Length];
|
|
}
|
|
|
|
if (cellWorldBounds == null || cellWorldBounds.Length != cells.Length)
|
|
{
|
|
cellWorldBounds = new Bounds[cells.Length];
|
|
for (int i = 0; i < cells.Length; ++i)
|
|
{
|
|
cellWorldBounds[i] = new Bounds()
|
|
{
|
|
center = normalizedToWorldMatrix.MultiplyPoint(cells[i].bounds.center),
|
|
size = normalizedToWorldMatrix.MultiplyVector(cells[i].bounds.size) + Vector3.one * cullBias
|
|
};
|
|
}
|
|
}
|
|
|
|
GUtilities.EnsureArrayLength(ref baseMeshes, prototypes.Count);
|
|
GUtilities.EnsureArrayLength(ref materials, prototypes.Count);
|
|
for (int i = 0; i < prototypes.Count; ++i)
|
|
{
|
|
baseMeshes[i] = prototypes[i].GetBaseMesh();
|
|
materials[i] =
|
|
prototypes[i].Shape == GGrassShape.DetailObject ?
|
|
prototypes[i].DetailMaterial :
|
|
GGrassMaterialProvider.GetMaterial(terrain.TerrainData.Foliage.EnableInteractiveGrass, prototypes[i].IsBillboard);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <returns>True if the terrain is culled</returns>
|
|
private bool CullTerrain()
|
|
{
|
|
bool prototypeCountTest = prototypes.Count > 0;
|
|
if (!prototypeCountTest)
|
|
return true;
|
|
|
|
bool nonZeroDistanceTest = terrain.TerrainData.Rendering.GrassDistance > 0;
|
|
if (!nonZeroDistanceTest)
|
|
return true;
|
|
|
|
bool frustumTest = GeometryUtility.TestPlanesAABB(frustum, terrain.Bounds);
|
|
if (!frustumTest)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
private void CullCells()
|
|
{
|
|
visibleCells.Clear();
|
|
cellToProcess.Clear();
|
|
|
|
Vector3 camWorldPos = camera.transform.position;
|
|
for (int i = 0; i < cells.Length; ++i)
|
|
{
|
|
if (cells[i].InstanceCount == 0)
|
|
{
|
|
cellCullResults[i] = CULLED;
|
|
}
|
|
else
|
|
{
|
|
int cullResult = GeometryUtility.TestPlanesAABB(frustum, cellWorldBounds[i]) ? VISIBLE : CULLED;
|
|
cellCullResults[i] = cullResult;
|
|
if (cullResult == VISIBLE)
|
|
{
|
|
visibleCells.Add(i);
|
|
}
|
|
}
|
|
|
|
if (cellCullResults[i] == CULLED)
|
|
{
|
|
cellCulledFrameCount[i] += 1;
|
|
}
|
|
else
|
|
{
|
|
cellCulledFrameCount[i] = 0;
|
|
}
|
|
cellSqrDistanceToCam[i] = Vector3.SqrMagnitude(cellWorldBounds[i].center - camWorldPos);
|
|
}
|
|
|
|
for (int i = 0; i < visibleCells.Count; ++i)
|
|
{
|
|
int cellIndex = visibleCells[i];
|
|
if (cellsData[cellIndex].instancedBatches == null)
|
|
{
|
|
cellToProcess.Add(cellIndex);
|
|
if (cellToProcess.Count == GRuntimeSettings.Instance.renderingDefault.grassCellToProcessPerFrame &&
|
|
!willIgnoreCellLimit)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CalculateAndCacheTransforms()
|
|
{
|
|
if (cellToProcess.Count == 0)
|
|
return;
|
|
bool willSkipFrame = false;
|
|
|
|
try
|
|
{
|
|
NativeArray<float> prototypePivotOffset = new NativeArray<float>(prototypes.Count, Allocator.TempJob);
|
|
NativeArray<Vector3> prototypeSize = new NativeArray<Vector3>(prototypes.Count, Allocator.TempJob);
|
|
for (int i = 0; i < prototypes.Count; ++i)
|
|
{
|
|
prototypePivotOffset[i] = prototypes[i].pivotOffset;
|
|
prototypeSize[i] = prototypes[i].size;
|
|
}
|
|
|
|
JobHandle[] handles = new JobHandle[cellToProcess.Count];
|
|
for (int i = 0; i < cellToProcess.Count; ++i)
|
|
{
|
|
int cellIndex = cellToProcess[i];
|
|
GGrassPatch cell = cells[cellIndex];
|
|
GGrassPatchNativeData nativeData = new GGrassPatchNativeData(cell.Instances);
|
|
cellsNativeData[cellIndex] = nativeData;
|
|
|
|
GCalculateGrassTransformJob job = new GCalculateGrassTransformJob()
|
|
{
|
|
instances = nativeData.instances,
|
|
transforms = nativeData.trs,
|
|
prototypePivotOffset = prototypePivotOffset,
|
|
prototypeSize = prototypeSize,
|
|
terrainSize = terrainSize,
|
|
terrainPos = terrainPosition
|
|
};
|
|
//handles[i] = job.Schedule();
|
|
handles[i] = job.Schedule(nativeData.instances.Length, 100);
|
|
}
|
|
|
|
GJobUtilities.CompleteAll(handles);
|
|
|
|
prototypePivotOffset.Dispose();
|
|
prototypeSize.Dispose();
|
|
}
|
|
catch (System.InvalidOperationException)
|
|
{
|
|
willSkipFrame = true;
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
Debug.LogException(e);
|
|
}
|
|
|
|
if (willSkipFrame)
|
|
{
|
|
throw new GSkipFrameException();
|
|
}
|
|
}
|
|
|
|
private void BuildBatches()
|
|
{
|
|
JobHandle[] handles = new JobHandle[cellToProcess.Count];
|
|
for (int i = 0; i < cellToProcess.Count; ++i)
|
|
{
|
|
int cellIndex = cellToProcess[i];
|
|
GGrassPatchNativeData nativeData = cellsNativeData[cellIndex];
|
|
|
|
//GGrassPatch cell = cellToProcess[i];
|
|
GBuildInstancedBatchJob job = new GBuildInstancedBatchJob()
|
|
{
|
|
instances = nativeData.instances,
|
|
batchMetadata = nativeData.metadata,
|
|
maxLength = BATCH_MAX_INSTANCE_COUNT
|
|
};
|
|
handles[i] = job.Schedule();
|
|
}
|
|
|
|
GJobUtilities.CompleteAll(handles);
|
|
|
|
for (int i = 0; i < cellToProcess.Count; ++i)
|
|
{
|
|
int cellIndex = cellToProcess[i];
|
|
CreateInstancedBatches(cellIndex);
|
|
}
|
|
}
|
|
|
|
private void CreateInstancedBatches(int cellIndex)
|
|
{
|
|
GGrassPatchNativeData nativeData = cellsNativeData[cellIndex];
|
|
|
|
NativeArray<Matrix4x4> trs = nativeData.trs;
|
|
NativeArray<Vector3Int> metadata = nativeData.metadata;
|
|
|
|
GInstancedBatch[] batches = new GInstancedBatch[metadata[0].z];
|
|
for (int i = 0; i < batches.Length; ++i)
|
|
{
|
|
int prototypeIndex = metadata[i + 1].x;
|
|
int startIndex = metadata[i + 1].y;
|
|
int length = metadata[i + 1].z;
|
|
int indexLimit = startIndex + length;
|
|
|
|
GInstancedBatch batch = new GInstancedBatch(BATCH_MAX_INSTANCE_COUNT);
|
|
batch.prototypeIndex = prototypeIndex;
|
|
for (int j = startIndex; j < indexLimit; ++j)
|
|
{
|
|
batch.AddTransform(trs[j]);
|
|
}
|
|
|
|
batches[i] = batch;
|
|
}
|
|
|
|
cellsData[cellIndex].instancedBatches = batches;
|
|
}
|
|
|
|
private void ConfigureMaterial()
|
|
{
|
|
for (int i = 0; i < prototypes.Count; ++i)
|
|
{
|
|
if (propertyBlocks[i] == null)
|
|
propertyBlocks[i] = new MaterialPropertyBlock();
|
|
propertyBlocks[i].Clear();
|
|
materialConfigurator.Configure(terrain, i, propertyBlocks[i]);
|
|
if (ConfiguringMaterial != null)
|
|
{
|
|
ConfiguringMaterial.Invoke(terrain, i, propertyBlocks[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Submit()
|
|
{
|
|
for (int i = 0; i < visibleCells.Count; ++i)
|
|
{
|
|
int cellIndex = visibleCells[i];
|
|
Submit(cellIndex);
|
|
}
|
|
}
|
|
|
|
private void Submit(int cellIndex)
|
|
{
|
|
GGrassPatchData data = cellsData[cellIndex];
|
|
if (data == null)
|
|
return;
|
|
GInstancedBatch[] batches = data.instancedBatches;
|
|
if (batches == null)
|
|
return;
|
|
for (int i = 0; i < batches.Length; ++i)
|
|
{
|
|
GInstancedBatch b = batches[i];
|
|
if (b.prototypeIndex >= prototypes.Count)
|
|
continue;
|
|
|
|
GGrassPrototype proto = prototypes[b.prototypeIndex];
|
|
MaterialPropertyBlock propertyBlock = propertyBlocks[b.prototypeIndex];
|
|
|
|
Mesh baseMesh = baseMeshes[b.prototypeIndex];
|
|
if (baseMesh == null)
|
|
continue;
|
|
|
|
Material material = materials[b.prototypeIndex];
|
|
if (material == null || !material.enableInstancing)
|
|
continue;
|
|
|
|
Graphics.DrawMeshInstanced(
|
|
baseMesh,
|
|
0,
|
|
material,
|
|
b.transforms,
|
|
b.instanceCount,
|
|
propertyBlock,
|
|
proto.shadowCastingMode,
|
|
proto.receiveShadow,
|
|
proto.layer,
|
|
camera,
|
|
LightProbeUsage.BlendProbes);
|
|
}
|
|
}
|
|
|
|
private void CleanUpFrame()
|
|
{
|
|
if (cellsNativeData != null)
|
|
{
|
|
for (int i = 0; i < cellsNativeData.Length; ++i)
|
|
{
|
|
if (cellsNativeData[i] != null)
|
|
{
|
|
cellsNativeData[i].Dispose();
|
|
cellsNativeData[i] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
UnloadInactiveCells();
|
|
}
|
|
|
|
private void UnloadInactiveCells()
|
|
{
|
|
float sqrRenderDistance = grassDistance * grassDistance;
|
|
for (int i = 0; i < cells.Length; ++i)
|
|
{
|
|
if (cellCulledFrameCount[i] >= FRAME_COUNT_TO_UNLOAD_CELL &&
|
|
cellSqrDistanceToCam[i] >= sqrRenderDistance)
|
|
{
|
|
ClearCellData(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void ClearCellData(int index)
|
|
{
|
|
if (cellsData != null)
|
|
{
|
|
cellsData[index] = null;
|
|
}
|
|
}
|
|
|
|
private void CalculateCellWorldBounds(int index)
|
|
{
|
|
if (cellWorldBounds != null)
|
|
{
|
|
cellWorldBounds[index] = new Bounds()
|
|
{
|
|
center = normalizedToWorldMatrix.MultiplyPoint(cells[index].bounds.center),
|
|
size = normalizedToWorldMatrix.MultiplyVector(cells[index].bounds.size)
|
|
};
|
|
}
|
|
}
|
|
|
|
internal void ClearAllCells()
|
|
{
|
|
cells = null;
|
|
cellsData = null;
|
|
cellWorldBounds = null;
|
|
RecalculateCellBounds();
|
|
}
|
|
|
|
internal void CleanUp()
|
|
{
|
|
ClearAllCells();
|
|
}
|
|
|
|
internal void OnCellChanged(int index)
|
|
{
|
|
ClearCellData(index);
|
|
CalculateCellWorldBounds(index);
|
|
}
|
|
|
|
internal void OnPatchGridSizeChanged()
|
|
{
|
|
ClearAllCells();
|
|
}
|
|
|
|
internal void OnPrototypeGroupChanged()
|
|
{
|
|
ClearAllCells();
|
|
}
|
|
}
|
|
}
|
|
#endif
|