322 lines
14 KiB
C#
322 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Serialization;
|
|
|
|
public class GrassField : MonoBehaviour {
|
|
public int size = 100;
|
|
public int numChunks = 20;
|
|
public int bladesPerSquareMeter = 100;
|
|
public GrassChunk chunkPrefab;
|
|
public bool ignoreHeightForLOD = true;
|
|
[Header("LOD 0")]
|
|
public Material grassMaterialLOD0;
|
|
public float minDistanceLOD0 = 2f;
|
|
[Range(0f, 1f)] public float densityLOD0 = 1f;
|
|
public bool enableShadowsLOD0 = true;
|
|
[Header("LOD 1")]
|
|
public float distanceLOD1 = 5f;
|
|
public Material grassMaterialLOD1;
|
|
[Range(0f, 1f)] public float densityLOD1 = 0.5f;
|
|
public bool enableShadowsLOD1 = true;
|
|
[Header("LOD 2")]
|
|
public float distanceLOD2 = 15f;
|
|
public Material grassMaterialLOD2;
|
|
[Range(0f, 1f)] public float densityLOD2 = 0.25f;
|
|
public bool enableShadowsLOD2 = false;
|
|
[Header("LOD 3")]
|
|
public float distanceLOD3 = 25f;
|
|
public Material grassMaterialLOD3;
|
|
[Range(0f, 1f)] public float densityLOD3 = 0.125f;
|
|
public bool enableShadowsLOD3 = false;
|
|
|
|
private const float SQRT_2 = 1.4142135623730f;
|
|
private static readonly int pId_WorldSpaceCameraPosEditor = Shader.PropertyToID("_WorldSpaceCameraPosEditor");
|
|
private static readonly int pId_TransitionRange = Shader.PropertyToID("_TransitionRange");
|
|
|
|
// 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;
|
|
|
|
public void GenerateGrassField() {
|
|
var bladesLOD0 = GrassMeshGeneration.GenerateBladesRandom(
|
|
size, size * size * (int)(bladesPerSquareMeter * densityLOD0)
|
|
);
|
|
var bladesLOD1 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD0, densityLOD1);
|
|
var bladesLOD2 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD1, densityLOD2 / densityLOD1);
|
|
var bladesLOD3 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD2, densityLOD3 / densityLOD2);
|
|
CreateChunks(bladesLOD0, bladesLOD1, bladesLOD2, bladesLOD3);
|
|
|
|
var camPosWorld = Camera.main ? Camera.main.transform.position : Vector3.zero;
|
|
UpdateLODs(camPosWorld);
|
|
UpdateMaterials(camPosWorld);
|
|
}
|
|
|
|
public void UpdateLODsAndMaterials() {
|
|
var camPosWorld = Camera.main ? Camera.main.transform.position : Vector3.zero;
|
|
UpdateMaterials(camPosWorld);
|
|
UpdateLODs(camPosWorld);
|
|
}
|
|
|
|
private void OnEnable() {
|
|
var chunkSize = ((float)size) / numChunks;
|
|
var minDistLOD2 = distanceLOD1 + chunkSize * SQRT_2;
|
|
if (distanceLOD2 < minDistLOD2) {
|
|
Debug.LogWarning("It is recommended that minDistLOD2 is greater than " + minDistLOD2 + ".");
|
|
}
|
|
var minDistLOD3 = distanceLOD2 + chunkSize * SQRT_2;
|
|
if (distanceLOD3 < minDistLOD3) {
|
|
Debug.LogWarning("It is recommended that minDistLOD3 is greater than " + minDistLOD3 + ".");
|
|
}
|
|
|
|
if (_chunks.Length != numChunks * numChunks) {
|
|
Debug.LogError(this.name + ": Existing chunks does not match numChunks.");
|
|
gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
private void OnDisable() {
|
|
UpdateLODsAndMaterials();
|
|
}
|
|
|
|
private void FixedUpdate() {
|
|
var cam = Camera.main;
|
|
if (!cam) return;
|
|
var camPosWorld = cam.transform.position;
|
|
|
|
#if UNITY_EDITOR
|
|
// update LOD distances and camera position
|
|
UpdateMaterials(camPosWorld);
|
|
#endif
|
|
|
|
if (_chunks.Length != numChunks * numChunks) return;
|
|
UpdateLODs(camPosWorld, false); // while the game is running, only the chunks near the camera are updated
|
|
}
|
|
|
|
private void UpdateLODs(Vector3 camPosWorld, bool forceCompleteCheck = true) {
|
|
var camPos = camPosWorld - transform.position; // cam position relative to grass field
|
|
if (ignoreHeightForLOD) {
|
|
camPos.y = 0;
|
|
}
|
|
|
|
var chunkSize = ((float)size) / numChunks;
|
|
var bounds = new Bounds(
|
|
new Vector3(0.5f * chunkSize, 0f, 0.5f * chunkSize),
|
|
new Vector3(chunkSize, 0f, chunkSize)
|
|
);
|
|
|
|
// small performance difference (0.13ms vs 0.02ms on a 100m 30x30 grid, LOD dist 26.6, camera at corner)
|
|
var range = forceCompleteCheck
|
|
? new GridRange{ fromX = 0, toX = numChunks-1, fromZ = 0, toZ = numChunks-1 }
|
|
: GetLODGridRange(camPos, chunkSize);
|
|
|
|
for (int x = range.fromX; x <= range.toX; x++)
|
|
for (int y = range.fromZ; y <= range.toZ; y++) {
|
|
var chunkPos = new Vector3(x, 0f, y) * chunkSize;
|
|
var localCamPos = camPos - chunkPos; // cam position relative to chunk
|
|
|
|
var sqrCamDist = bounds.SqrDistance(localCamPos);
|
|
if (sqrCamDist > distanceLOD3 * distanceLOD3) {
|
|
_chunks[x + y * numChunks].LOD = 3;
|
|
}
|
|
else if (sqrCamDist > distanceLOD2 * distanceLOD2) {
|
|
_chunks[x + y * numChunks].LOD = 2;
|
|
}
|
|
else if (sqrCamDist > distanceLOD1 * distanceLOD1) {
|
|
_chunks[x + y * numChunks].LOD = 1;
|
|
}
|
|
else {
|
|
_chunks[x + y * numChunks].LOD = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct GridRange {
|
|
public int fromX;
|
|
public int toX;
|
|
public int fromZ;
|
|
public int toZ;
|
|
}
|
|
|
|
private GridRange GetLODGridRange(Vector3 camPos, float chunkSize) {
|
|
GridRange range;
|
|
|
|
var maxCamMove = 3f; // assume that the camera did not move further than this distance
|
|
var fromXWorld = camPos.x - distanceLOD3 - maxCamMove;
|
|
var toXWorld = camPos.x + distanceLOD3 + maxCamMove;
|
|
var fromZWorld = camPos.z - distanceLOD3 - maxCamMove;
|
|
var toZWorld = camPos.z + distanceLOD3 + maxCamMove;
|
|
|
|
range.fromX = Math.Max( 0, (int) (fromXWorld / chunkSize) );
|
|
range.toX = Math.Max( 0, (int) (toXWorld / chunkSize) );
|
|
range.fromZ = Math.Max( 0, (int) (fromZWorld / chunkSize) );
|
|
range.toZ = (int) (toZWorld / chunkSize);
|
|
range.fromX = Math.Clamp(range.fromX, 0, numChunks - 1);
|
|
range.toX = Math.Clamp(range.toX, 0, numChunks - 1);
|
|
range.fromZ = Math.Clamp(range.fromZ, 0, numChunks - 1);
|
|
range.toZ = Math.Clamp(range.toZ, 0, numChunks - 1);
|
|
|
|
return range;
|
|
}
|
|
|
|
private void UpdateMaterials(Vector3 camPosWorld) {
|
|
var chunkSize = ((float)size) / numChunks;
|
|
var chunkDiagonalLength = chunkSize * SQRT_2;
|
|
|
|
var transition0From = minDistanceLOD0;
|
|
var transition0To = distanceLOD1;
|
|
var transition1From = distanceLOD1 + chunkDiagonalLength;
|
|
var transition1To = distanceLOD2;
|
|
var transition2From = distanceLOD2 + chunkDiagonalLength;
|
|
var transition2To = distanceLOD3;
|
|
grassMaterialLOD0.SetVector(pId_TransitionRange, new Vector2(transition0From, transition0To));
|
|
grassMaterialLOD1.SetVector(pId_TransitionRange, new Vector2(transition1From, transition1To));
|
|
grassMaterialLOD2.SetVector(pId_TransitionRange, new Vector2(transition2From, transition2To));
|
|
#if UNITY_EDITOR
|
|
if (ignoreHeightForLOD) {
|
|
camPosWorld.y = 0;
|
|
}
|
|
grassMaterialLOD0.SetVector(pId_WorldSpaceCameraPosEditor, camPosWorld);
|
|
grassMaterialLOD1.SetVector(pId_WorldSpaceCameraPosEditor, camPosWorld);
|
|
grassMaterialLOD2.SetVector(pId_WorldSpaceCameraPosEditor, camPosWorld);
|
|
grassMaterialLOD3.SetVector(pId_WorldSpaceCameraPosEditor, camPosWorld);
|
|
#endif
|
|
}
|
|
|
|
private void CreateChunks(
|
|
BladeData[] bladesLOD0, BladeData[] bladesLOD1, BladeData[] bladesLOD2, BladeData[] bladesLOD3
|
|
) {
|
|
GameObject chunksContainer;
|
|
// If a child called "Chunks" exists, destroy it -> children are also destroyed
|
|
var chunksTransform = gameObject.transform.Find("Chunks");
|
|
if (chunksTransform != null) {
|
|
// Destroy(obj) does not work in edit mode
|
|
DestroyImmediate(chunksTransform.gameObject);
|
|
}
|
|
// create GameObject "Chunks" as a container for all GrassChunk objects
|
|
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
|
|
var chunkBounds = new Bounds(
|
|
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;
|
|
|
|
var meshLOD0 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD0, "grass_chunk_" + x + "_" + y + "_lod0");
|
|
var meshLOD1 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD1, "grass_chunk_" + x + "_" + y + "_lod1");
|
|
var meshLOD2 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD2, "grass_chunk_" + x + "_" + y + "_lod2");
|
|
var meshLOD3 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD3, "grass_chunk_" + x + "_" + y + "_lod3");
|
|
|
|
var chunk = Instantiate(chunkPrefab, chunksContainer.transform);
|
|
chunk.name = "Chunk [" + x + ", " + y +"]";
|
|
chunk.transform.localPosition = chunkPos;
|
|
|
|
chunk.MaterialLOD0 = grassMaterialLOD0;
|
|
chunk.MaterialLOD1 = grassMaterialLOD1;
|
|
chunk.MaterialLOD2 = grassMaterialLOD2;
|
|
chunk.MaterialLOD3 = grassMaterialLOD3;
|
|
chunk.MeshLOD0 = meshLOD0;
|
|
chunk.MeshLOD1 = meshLOD1;
|
|
chunk.MeshLOD2 = meshLOD2;
|
|
chunk.MeshLOD3 = meshLOD3;
|
|
chunk.EnableShadowsLOD0 = enableShadowsLOD0;
|
|
chunk.EnableShadowsLOD1 = enableShadowsLOD1;
|
|
chunk.EnableShadowsLOD2 = enableShadowsLOD2;
|
|
chunk.EnableShadowsLOD3 = enableShadowsLOD3;
|
|
|
|
_chunks[x + y * numChunks] = chunk;
|
|
}
|
|
}
|
|
|
|
private Mesh GenerateChunkMesh(Vector3 chunkPos, Bounds chunkBounds, BladeData[] blades, string meshName) {
|
|
List<Vector3> rootPositions = new(); // vertices
|
|
List<Vector4> tipOffsets_missingInNextLOD = new(); // uv0
|
|
List<int> indices = new();
|
|
foreach (var blade in blades) {
|
|
// position relative to chunk
|
|
var localRootPos = blade.rootPosition - chunkPos;
|
|
if (chunkBounds.Contains(localRootPos)) {
|
|
rootPositions.Add(localRootPos);
|
|
tipOffsets_missingInNextLOD.Add(new Vector4(
|
|
blade.tipOffset.x, blade.tipOffset.y, blade.tipOffset.z,
|
|
blade.missingInNextLOD ? 1f : 0f)
|
|
);
|
|
indices.Add(rootPositions.Count - 1);
|
|
}
|
|
}
|
|
|
|
var mesh = new Mesh();
|
|
mesh.name = meshName;
|
|
if (rootPositions.Count > 65536) {
|
|
// 16 bit (default) supports 65536 vertices, 32 bit supports 4 billion
|
|
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
|
|
Debug.LogWarning(meshName + " has more than 65536 grass blades. Mesh.indexFormat set to UInt32.");
|
|
}
|
|
mesh.SetVertices(rootPositions);
|
|
mesh.SetUVs(0, tipOffsets_missingInNextLOD); // Vector4 UVs
|
|
// AABB gets calculated automatically (considers all root positions)
|
|
mesh.SetIndices(indices, MeshTopology.Points, 0);
|
|
// update the AABB to match the actual grass (not just the root positions)
|
|
// use mesh.bounds rather than meshRenderer.localBounds,
|
|
// because the latter gets overwritten automatically
|
|
var previousBounds = mesh.bounds;
|
|
const float width = 1f; // a blade should never bend sideways farther than this
|
|
const float height = 1f; // a blade should not be higher than this
|
|
mesh.bounds = new Bounds(
|
|
previousBounds.center + new Vector3(0f, height * 0.5f, 0f),
|
|
previousBounds.size + new Vector3(2f*width, height, 2f*width)
|
|
);
|
|
|
|
return mesh;
|
|
}
|
|
|
|
private void OnDrawGizmos() {
|
|
var chunkSize = ((float)size) / numChunks;
|
|
var tPos = transform.position;
|
|
|
|
// grid
|
|
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;
|
|
var oldMatrix = Gizmos.matrix;
|
|
if (ignoreHeightForLOD) {
|
|
center.y = 0.01f;
|
|
// draw a circle instead of a sphere
|
|
Gizmos.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1f, 0.001f, 1f));
|
|
}
|
|
|
|
Gizmos.color = new Color(0f, 0.3f, 1f, 1f);
|
|
Gizmos.DrawWireSphere(center, distanceLOD1);
|
|
Gizmos.DrawWireSphere(center, distanceLOD2);
|
|
Gizmos.DrawWireSphere(center, distanceLOD3);
|
|
|
|
Gizmos.color = new Color(1.0f, 1.0f, 0.0f, 1f);
|
|
var minDistLOD0 = minDistanceLOD0;
|
|
var minDistLOD1 = distanceLOD1 + chunkSize * SQRT_2;
|
|
var minDistLOD2 = distanceLOD2 + chunkSize * SQRT_2;
|
|
Gizmos.DrawWireSphere(center, minDistLOD0);
|
|
Gizmos.DrawWireSphere(center, minDistLOD1);
|
|
Gizmos.DrawWireSphere(center, minDistLOD2);
|
|
|
|
Gizmos.matrix = oldMatrix;
|
|
}
|
|
}
|