Update LODs in GrassField instead of in the chunks (better performance) + other performance improvements

This commit is contained in:
Timo Eberl 2024-09-29 22:01:31 +02:00
parent 31b4b88675
commit 534f4367a9
Signed by: Timo
SSH Key Fingerprint: SHA256:swVjhbVzKCLQZNtwPqMEmtOUG3FTydzVrpIKpUZYTQw
11 changed files with 220720 additions and 66368 deletions

View File

@ -49,7 +49,7 @@ Material:
- _AOStrength: 1
- _BendStrength: 1
- _BladeBow: 0.3
- _BladeWidth: 0.05
- _BladeWidth: 0.0325
- _ExtraSawnOnCutGras: 6
- _FieldSize: 5
- _LOD: 0

View File

@ -49,7 +49,7 @@ Material:
- _AOStrength: 1
- _BendStrength: 1
- _BladeBow: 0.3
- _BladeWidth: 0.05
- _BladeWidth: 0.0276
- _ExtraSawnOnCutGras: 6
- _FieldSize: 5
- _LOD: 1

View File

@ -49,7 +49,7 @@ Material:
- _AOStrength: 1
- _BendStrength: 1
- _BladeBow: 0.3
- _BladeWidth: 0.05
- _BladeWidth: 0.047
- _ExtraSawnOnCutGras: 6
- _FieldSize: 5
- _LOD: 2

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
using UnityEngine;
public struct BladeData {
public Vector3 rootPosition;
public Vector3 tipOffset;
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 683d2496822587844a7d37e611591c20
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -5,141 +5,136 @@ using UnityEngine.Serialization;
[RequireComponent(typeof(MeshRenderer)), RequireComponent(typeof(MeshFilter))]
public class GrassChunk : MonoBehaviour {
[SerializeField] private Material materialLOD0;
[SerializeField] private Material _materialLOD0;
public Material MaterialLOD0 {
get { return materialLOD0; }
get { return _materialLOD0; }
set {
materialLOD0 = value;
_materialLOD0 = value;
UpdateLODAssets();
}
}
[SerializeField] private Mesh meshLOD0;
[SerializeField] private Mesh _meshLOD0;
public Mesh MeshLOD0 {
get { return meshLOD0; }
get { return _meshLOD0; }
set {
meshLOD0 = value;
_meshLOD0 = value;
UpdateLODAssets();
UpdateLOD(); // a new mesh may change the AABB which may affect the LOD
}
}
[SerializeField] private bool enableShadowsLOD0;
[SerializeField] private bool _enableShadowsLOD0;
public bool EnableShadowsLOD0 {
get { return enableShadowsLOD0; }
get { return _enableShadowsLOD0; }
set {
enableShadowsLOD0 = value;
_enableShadowsLOD0 = value;
UpdateLODAssets();
}
}
[SerializeField] private Material materialLOD1;
[SerializeField] private Material _materialLOD1;
public Material MaterialLOD1 {
get { return materialLOD1; }
get { return _materialLOD1; }
set {
materialLOD1 = value;
_materialLOD1 = value;
UpdateLODAssets();
}
}
[SerializeField] private Mesh meshLOD1;
[SerializeField] private Mesh _meshLOD1;
public Mesh MeshLOD1 {
get { return meshLOD1; }
get { return _meshLOD1; }
set {
meshLOD1 = value;
_meshLOD1 = value;
UpdateLODAssets();
UpdateLOD(); // a new mesh may change the AABB which may affect the LOD
}
}
[SerializeField] private bool enableShadowsLOD1;
[SerializeField] private bool _enableShadowsLOD1;
public bool EnableShadowsLOD1 {
get { return enableShadowsLOD1; }
get { return _enableShadowsLOD1; }
set {
enableShadowsLOD1 = value;
_enableShadowsLOD1 = value;
UpdateLODAssets();
}
}
[SerializeField] private Material materialLOD2;
[SerializeField] private Material _materialLOD2;
public Material MaterialLOD2 {
get { return materialLOD2; }
get { return _materialLOD2; }
set {
materialLOD2 = value;
_materialLOD2 = value;
UpdateLODAssets();
}
}
[SerializeField] private Mesh meshLOD2;
[SerializeField] private Mesh _meshLOD2;
public Mesh MeshLOD2 {
get { return meshLOD2; }
get { return _meshLOD2; }
set {
meshLOD2 = value;
_meshLOD2 = value;
UpdateLODAssets();
UpdateLOD(); // a new mesh may change the AABB which may affect the LOD
}
}
[SerializeField] private bool enableShadowsLOD2;
[SerializeField] private bool _enableShadowsLOD2;
public bool EnableShadowsLOD2 {
get { return enableShadowsLOD2; }
get { return _enableShadowsLOD2; }
set {
enableShadowsLOD2 = value;
_enableShadowsLOD2 = value;
UpdateLODAssets();
}
}
private int lod = -1; // set to -1 so LOD.set does not exit early when initially called with 0
private int _lod;
public int LOD {
get { return lod; }
get { return _lod; }
set {
if (lod == value) return;
if (_lod == value) return;
lod = value;
_lod = value;
UpdateLODAssets();
}
}
private void Update() {
UpdateLOD();
private MeshRenderer _meshRenderer;
public MeshRenderer MeshRenderer {
get {
if (!_meshRenderer) {
_meshRenderer = GetComponent<MeshRenderer>();
}
return _meshRenderer;
}
private set { _meshRenderer = value; }
}
public void UpdateLOD() {
var meshRenderer = GetComponent<MeshRenderer>();
if (Camera.main) {
// using meshRenderer.bounds may cause a loop where LOD switches constantly
var sqrCamDist = meshRenderer.bounds.SqrDistance(Camera.main.transform.position);
if (sqrCamDist > 20f * 20f) {
LOD = 2;
}
else if (sqrCamDist > 5f * 5f) {
LOD = 1;
}
else {
LOD = 0;
private MeshFilter _meshFilter;
public MeshFilter MeshFilter {
get {
if (!_meshFilter) {
_meshFilter = GetComponent<MeshFilter>();
}
return _meshFilter;
}
private set { _meshFilter = value; }
}
private void UpdateLODAssets() {
var meshRenderer = GetComponent<MeshRenderer>();
var meshFilter = GetComponent<MeshFilter>();
switch (lod) {
switch (_lod) {
case 0:
meshRenderer.sharedMaterial = materialLOD0;
meshRenderer.shadowCastingMode = EnableShadowsLOD0 ? ShadowCastingMode.On : ShadowCastingMode.Off;
meshFilter.sharedMesh = meshLOD0;
MeshRenderer.sharedMaterial = _materialLOD0;
MeshRenderer.shadowCastingMode = EnableShadowsLOD0 ? ShadowCastingMode.On : ShadowCastingMode.Off;
MeshFilter.sharedMesh = _meshLOD0;
break;
case 1:
meshRenderer.sharedMaterial = materialLOD1;
meshRenderer.shadowCastingMode = EnableShadowsLOD1 ? ShadowCastingMode.On : ShadowCastingMode.Off;
meshFilter.sharedMesh = meshLOD1;
MeshRenderer.sharedMaterial = _materialLOD1;
MeshRenderer.shadowCastingMode = EnableShadowsLOD1 ? ShadowCastingMode.On : ShadowCastingMode.Off;
MeshFilter.sharedMesh = _meshLOD1;
break;
case 2:
meshRenderer.sharedMaterial = materialLOD2;
meshRenderer.shadowCastingMode = EnableShadowsLOD2 ? ShadowCastingMode.On : ShadowCastingMode.Off;
meshFilter.sharedMesh = meshLOD2;
MeshRenderer.sharedMaterial = _materialLOD2;
MeshRenderer.shadowCastingMode = EnableShadowsLOD2 ? ShadowCastingMode.On : ShadowCastingMode.Off;
MeshFilter.sharedMesh = _meshLOD2;
break;
}
}

View File

@ -8,65 +8,88 @@ using Random = UnityEngine.Random;
public class GrassField : MonoBehaviour {
public int size = 100;
public int numChunks = 5;
public int bladesPerMeter = 100;
public GrassChunk chunkPrefab;
public bool ignoreHeightForLOD = true;
[Header("LOD 0")]
public Material grassMaterialLOD0;
public int bladesPerMeterLOD0 = 100;
[Range(0f, 1f)] public float densityLOD0 = 1f;
public bool enableShadowsLOD0 = true;
[Header("LOD 1")]
public Material grassMaterialLOD1;
public int bladesPerMeterLOD1 = 50;
[Range(0f, 1f)] public float densityLOD1 = 0.5f;
public bool enableShadowsLOD1 = true;
[Header("LOD 2")]
public Material grassMaterialLOD2;
public int bladesPerMeterLOD2 = 10;
[Range(0f, 1f)] public float densityLOD2 = 0.2f;
public bool enableShadowsLOD2 = false;
private struct BladeData {
public Vector3 rootPosition;
public Vector3 tipOffset;
}
// a 2D array would be cleaner, but it can't be serialized
// serialize it, because the chunks are generated offline and used during runtime
[SerializeField, HideInInspector] private GrassChunk[] _chunks;
// ReSharper disable Unity.PerformanceAnalysis
public void GenerateGrassField() {
Debug.Log("Generating grass field... ");
var bladesLOD0 = GenerateBladesRandom(size * size * bladesPerMeterLOD0);
var bladesLOD1 = GenerateBladesRandom(size * size * bladesPerMeterLOD1);
var bladesLOD2 = GenerateBladesRandom(size * size * bladesPerMeterLOD2);
var bladesLOD0 = GrassMeshGeneration.GenerateBladesRandom(size, size * size * (int)(bladesPerMeter * densityLOD0));
// var bladesLOD1 = GenerateBladesRandom(size, size * size * (int)(bladesPerMeter * densityLOD1));
// var bladesLOD2 = GenerateBladesRandom(size, size * size * (int)(bladesPerMeter * densityLOD2));
var bladesLOD1 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD0, densityLOD1);
var bladesLOD2 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD1, densityLOD2 / densityLOD1);
CreateChunks(bladesLOD0, bladesLOD1, bladesLOD2);
Debug.Log("Generating grass field done");
}
private BladeData[] GenerateBladesRandom(int num) {
var blades = new BladeData[num];
for (int i = 0; i < num; i++) {
blades[i].rootPosition = new Vector3(
Random.Range(0f, size), Random.Range(-0.25f, 0f), Random.Range(0f, size)
);
blades[i].tipOffset = new Vector3(Random.Range(-.8f, .8f), Random.Range(0.7f, 1f), Random.Range(-.8f, .8f));
private void OnEnable() {
if (_chunks.Length != numChunks * numChunks) {
Debug.LogError(this.name + ": Existing chunks does not match numChunks.");
gameObject.SetActive(false);
}
return blades;
}
private BladeData[] GenerateBladesEven() {
var blades = new BladeData[size * size * 100];
private void Update() {
UpdateLODs();
}
for (int x = 0; x < size * 10; x++)
for (int y = 0; y < size * 10; y++) {
var i = x + y * size * 10;
blades[i].rootPosition = new Vector3(x/10.0f, 0.0f, y/10.0f);
blades[i].tipOffset = new Vector3(0.01f, 1.0f, 0.01f);
private void UpdateLODs() {
var cam = Camera.main;
if (!cam) return;
if (_chunks.Length != numChunks * numChunks) return;
var camPos = cam.transform.position - transform.position; // cam position relative to grass field
if (ignoreHeightForLOD) {
camPos.y = 0; // ignore height
}
var chunkSize = ((float)size) / numChunks;
var bounds = new Bounds(
new Vector3(0.5f * chunkSize, 0f, 0.5f * chunkSize),
new Vector3(chunkSize, 0f, chunkSize)
);
for (int x = 0; x < numChunks; x++)
for (int y = 0; y < numChunks; y++) {
var chunkPos = new Vector3(x, 0f, y) * chunkSize;
var localCamPos = camPos - chunkPos; // cam position relative to chunk
return blades;
var sqrCamDist = bounds.SqrDistance(localCamPos);
if (sqrCamDist > 15f * 15f) {
_chunks[x + y * numChunks].LOD = 2;
}
else if (sqrCamDist > 7.5f * 7.5f) {
_chunks[x + y * numChunks].LOD = 1;
}
else {
_chunks[x + y * numChunks].LOD = 0;
}
}
}
private void CreateChunks(BladeData[] bladesLOD0, BladeData[] bladesLOD1, BladeData[] bladesLOD2) {
GameObject chunks;
GameObject chunksContainer;
// If a child called "Chunks" exists, destroy it -> children are also destroyed
var chunksTransform = gameObject.transform.Find("Chunks");
if (chunksTransform != null) {
@ -74,8 +97,8 @@ public class GrassField : MonoBehaviour {
DestroyImmediate(chunksTransform.gameObject);
}
// create GameObject "Chunks" as a container for all GrassChunk objects
chunks = new GameObject("Chunks");
chunks.transform.SetParent(this.transform, false);
chunksContainer = new GameObject("Chunks");
chunksContainer.transform.SetParent(this.transform, false);
var chunkSize = ((float)size) / numChunks;
// bounds of the chunk (relative to chunk) - do not consider y position
@ -83,6 +106,7 @@ public class GrassField : MonoBehaviour {
new Vector3(0.5f * chunkSize, 0.0f, 0.5f * chunkSize),
new Vector3(chunkSize, 10000f, chunkSize)
);
_chunks = new GrassChunk[numChunks * numChunks];
for (int x = 0; x < numChunks; x++)
for (int y = 0; y < numChunks; y++) {
var chunkPos = new Vector3(x, 0.0f, y) * chunkSize;
@ -91,7 +115,7 @@ public class GrassField : MonoBehaviour {
var meshLOD1 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD1, "grass_chunk_" + x + "_" + y + "_lod1");
var meshLOD2 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD2, "grass_chunk_" + x + "_" + y + "_lod2");
var chunk = Instantiate(chunkPrefab, chunks.transform);
var chunk = Instantiate(chunkPrefab, chunksContainer.transform);
chunk.name = "Chunk [" + x + ", " + y +"]";
chunk.transform.localPosition = chunkPos;
@ -104,7 +128,11 @@ public class GrassField : MonoBehaviour {
chunk.EnableShadowsLOD0 = enableShadowsLOD0;
chunk.EnableShadowsLOD1 = enableShadowsLOD1;
chunk.EnableShadowsLOD2 = enableShadowsLOD2;
_chunks[x + y * numChunks] = chunk;
}
UpdateLODs();
}
private Mesh GenerateChunkMesh(Vector3 chunkPos, Bounds chunkBounds, BladeData[] blades, string meshName) {
@ -147,17 +175,28 @@ public class GrassField : MonoBehaviour {
}
private void OnDrawGizmos() {
Gizmos.color = new Color(0f, 0.2f, 1f, 1f);
var chunkSize = ((float)size) / numChunks;
var bounds = new Bounds(
new Vector3(0.5f * chunkSize, 0.0f, 0.5f * chunkSize),
new Vector3(chunkSize, 0f, chunkSize)
);
for (int x = 0; x < numChunks; x++)
for (int y = 0; y < numChunks; y++) {
var chunkPos = new Vector3(x, 0.0f, y) * chunkSize;
Gizmos.DrawWireCube(bounds.center + chunkPos + transform.position, bounds.size);
var tPos = transform.position;
for (int i = 0; i < numChunks + 1; i++) {
Gizmos.color = new Color(0f, 0.3f, 1f, 1f);
Gizmos.DrawLine(
tPos + new Vector3(chunkSize * i, 0.01f, 0),
tPos + new Vector3(chunkSize * i, 0.01f, 0) + new Vector3(0, 0, size)
);
Gizmos.DrawLine(
tPos + new Vector3(0, 0.01f, chunkSize * i),
tPos + new Vector3(0, 0.01f, chunkSize * i) + new Vector3(size, 0, 0)
);
}
var cam = Camera.main;
if (!cam) return;
var center = cam.transform.position;
if (ignoreHeightForLOD) {
center.y = 0f;
}
Gizmos.color = new Color(1.0f, 0.0f, 1f, 2f);
Gizmos.DrawWireSphere(center, 7.5f);
}
}

View File

@ -0,0 +1,44 @@
using System.Collections.Generic;
using UnityEngine;
public class GrassMeshGeneration {
static public BladeData[] GenerateBladesRandom(int size, int num) {
var blades = new BladeData[num];
for (int i = 0; i < num; i++) {
blades[i].rootPosition = new Vector3(
Random.Range(0f, size), Random.Range(-0.25f, 0f), Random.Range(0f, size)
);
blades[i].tipOffset = new Vector3(Random.Range(-.8f, .8f), Random.Range(0.7f, 1f), Random.Range(-.8f, .8f));
}
return blades;
}
// r = 0.5 means that the generated blades will have 50% of the blades of the input blades
static public BladeData[] GenerateBladesReduced(BladeData[] from, float r) {
List<BladeData> blades = new();
for (int i = 0; i < from.Length; i++) {
if (Random.Range(0f, 1f) > r) continue;
BladeData blade = from[i];
blades.Add(blade);
}
return blades.ToArray();
}
static public BladeData[] GenerateBladesEven(int size) {
var blades = new BladeData[size * size * 100];
for (int x = 0; x < size * 10; x++)
for (int y = 0; y < size * 10; y++) {
var i = x + y * size * 10;
blades[i].rootPosition = new Vector3(x/10.0f, 0.0f, y/10.0f);
blades[i].tipOffset = new Vector3(0.01f, 1.0f, 0.01f);
}
return blades;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e61ad39b14108e4999fc96bf87d9d78
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -35,14 +35,6 @@ const int ns = 11;
[MaxVertexCount(4)]
#endif
void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
const float3 basePos = IN[0].vertex.xyz;
const float3 tipPosBladeSpace = IN[0].tipOffset;
const float3 tipPosObjectSpace = basePos + tipPosBladeSpace;
@ -54,21 +46,17 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
const float3 basePosWS = mul(unity_ObjectToWorld, float4(basePos.xyz, 1.0)).xyz;
const float cameraDistance = distance(_WorldSpaceCameraPos, basePosWS);
float randoVal = N21(basePos);
#if _LOD_LOD_0
if (randoVal * 10.0 > max(1, 2 / (cameraDistance / 20)))
{
return;
}
#elif _LOD_LOD_1
if (randoVal * 10.0 > max(1, 5 / (cameraDistance / 20)))
{
return;
}
#endif
// randomly cull grass blades
// float randoVal = N21(basePos);
// #if _LOD_LOD_0
// if (randoVal * 10.0 > max(1, 2 / (cameraDistance / 20))) {
// return;
// }
// #elif _LOD_LOD_1
// if (randoVal * 10.0 > max(1, 5 / (cameraDistance / 20))) {
// return;
// }
// #endif
float halfWidth = _BladeWidth/2;