LODs: generate meshes with fewer blades + disable shadows

This commit is contained in:
Timo Eberl 2024-09-29 04:03:16 +02:00
parent d4397c6532
commit a4ffd7a234
Signed by: Timo
SSH Key Fingerprint: SHA256:swVjhbVzKCLQZNtwPqMEmtOUG3FTydzVrpIKpUZYTQw
5 changed files with 76758 additions and 28213 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,29 +1,103 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Serialization;
[RequireComponent(typeof(MeshRenderer)), RequireComponent(typeof(MeshFilter))]
public class GrassChunk : MonoBehaviour {
[Header("LOD 0")]
public Material grassMaterialLOD0;
[Header("LOD 1")]
public Material grassMaterialLOD1;
[Header("LOD 2")]
public Material grassMaterialLOD2;
private int lod = -1;
[SerializeField]
[SerializeField] private Material materialLOD0;
public Material MaterialLOD0 {
get { return materialLOD0; }
set {
materialLOD0 = value;
UpdateLODAssets();
}
}
[SerializeField] private Mesh meshLOD0;
public Mesh MeshLOD0 {
get { return meshLOD0; }
set {
meshLOD0 = value;
UpdateLODAssets();
UpdateLOD(); // a new mesh may change the AABB which may affect the LOD
}
}
[SerializeField] private bool enableShadowsLOD0;
public bool EnableShadowsLOD0 {
get { return enableShadowsLOD0; }
set {
enableShadowsLOD0 = value;
UpdateLODAssets();
}
}
[SerializeField] private Material materialLOD1;
public Material MaterialLOD1 {
get { return materialLOD1; }
set {
materialLOD1 = value;
UpdateLODAssets();
}
}
[SerializeField] private Mesh meshLOD1;
public Mesh MeshLOD1 {
get { return meshLOD1; }
set {
meshLOD1 = value;
UpdateLODAssets();
UpdateLOD(); // a new mesh may change the AABB which may affect the LOD
}
}
[SerializeField] private bool enableShadowsLOD1;
public bool EnableShadowsLOD1 {
get { return enableShadowsLOD1; }
set {
enableShadowsLOD1 = value;
UpdateLODAssets();
}
}
[SerializeField] private Material materialLOD2;
public Material MaterialLOD2 {
get { return materialLOD2; }
set {
materialLOD2 = value;
UpdateLODAssets();
}
}
[SerializeField] private Mesh meshLOD2;
public Mesh MeshLOD2 {
get { return meshLOD2; }
set {
meshLOD2 = value;
UpdateLODAssets();
UpdateLOD(); // a new mesh may change the AABB which may affect the LOD
}
}
[SerializeField] private bool enableShadowsLOD2;
public bool EnableShadowsLOD2 {
get { return enableShadowsLOD2; }
set {
enableShadowsLOD2 = value;
UpdateLODAssets();
}
}
private int lod = -1; // set to -1 so LOD.set does not exit early when initially called with 0
public int LOD {
get { return lod; }
set {
if (lod == value) return;
lod = value;
var meshRenderer = GetComponent<MeshRenderer>();
switch (lod) {
case 0: meshRenderer.sharedMaterial = grassMaterialLOD0; break;
case 1: meshRenderer.sharedMaterial = grassMaterialLOD1; break;
case 2: meshRenderer.sharedMaterial = grassMaterialLOD2; break;
}
UpdateLODAssets();
}
}
@ -34,6 +108,7 @@ public class GrassChunk : MonoBehaviour {
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;
@ -47,6 +122,28 @@ public class GrassChunk : MonoBehaviour {
}
}
private void UpdateLODAssets() {
var meshRenderer = GetComponent<MeshRenderer>();
var meshFilter = GetComponent<MeshFilter>();
switch (lod) {
case 0:
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;
break;
case 2:
meshRenderer.sharedMaterial = materialLOD2;
meshRenderer.shadowCastingMode = EnableShadowsLOD2 ? ShadowCastingMode.On : ShadowCastingMode.Off;
meshFilter.sharedMesh = meshLOD2;
break;
}
}
private void OnDrawGizmos() {
Gizmos.color = new Color(1f, 0.7f, 0f, 0.5f);
var meshRenderer = GetComponent<MeshRenderer>();

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using UnityEngine;
using UnityEngine.Serialization;
using Random = UnityEngine.Random;
public class GrassField : MonoBehaviour {
@ -10,10 +11,16 @@ public class GrassField : MonoBehaviour {
public GrassChunk chunkPrefab;
[Header("LOD 0")]
public Material grassMaterialLOD0;
public int bladesPerMeterLOD0 = 100;
public bool enableShadowsLOD0 = true;
[Header("LOD 1")]
public Material grassMaterialLOD1;
public int bladesPerMeterLOD1 = 50;
public bool enableShadowsLOD1 = true;
[Header("LOD 2")]
public Material grassMaterialLOD2;
public int bladesPerMeterLOD2 = 10;
public bool enableShadowsLOD2 = false;
private struct BladeData {
public Vector3 rootPosition;
@ -24,8 +31,10 @@ public class GrassField : MonoBehaviour {
public void GenerateGrassField() {
Debug.Log("Generating grass field... ");
var blades = GenerateBladesRandom(1000000);
CreateChunks(blades);
var bladesLOD0 = GenerateBladesRandom(size * size * bladesPerMeterLOD0);
var bladesLOD1 = GenerateBladesRandom(size * size * bladesPerMeterLOD1);
var bladesLOD2 = GenerateBladesRandom(size * size * bladesPerMeterLOD2);
CreateChunks(bladesLOD0, bladesLOD1, bladesLOD2);
Debug.Log("Generating grass field done");
}
@ -56,7 +65,7 @@ public class GrassField : MonoBehaviour {
return blades;
}
private void CreateChunks(BladeData[] blades) {
private void CreateChunks(BladeData[] bladesLOD0, BladeData[] bladesLOD1, BladeData[] bladesLOD2) {
GameObject chunks;
// If a child called "Chunks" exists, destroy it -> children are also destroyed
var chunksTransform = gameObject.transform.Find("Chunks");
@ -78,54 +87,64 @@ public class GrassField : MonoBehaviour {
for (int y = 0; y < numChunks; y++) {
var chunkPos = new Vector3(x, 0.0f, y) * chunkSize;
List<Vector3> rootPositions = new();
List<Vector3> tipOffsets = new();
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.Add(blade.tipOffset);
indices.Add(rootPositions.Count - 1);
}
}
var mesh = new Mesh();
mesh.name = "grass_chunk[" + x + "," + y + "]";
if (rootPositions.Count > 65536) {
// 16 bit (default) supports 65536 vertices, 32 bit supports 4 billion
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
Debug.LogWarning(mesh.name + " has more than 65536 grass blades. Mesh.indexFormat set to UInt32.");
}
mesh.SetVertices(rootPositions);
mesh.SetUVs(0, tipOffsets); // Vector3 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;
float width = 1f; // a blade should never bend sideways farther than this
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)
);
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 chunk = Instantiate(chunkPrefab, chunks.transform);
chunk.name = "Chunk [" + x + ", " + y +"]";
chunk.transform.localPosition = chunkPos;
chunk.grassMaterialLOD0 = grassMaterialLOD0;
chunk.grassMaterialLOD1 = grassMaterialLOD1;
chunk.grassMaterialLOD2 = grassMaterialLOD2;
var meshFilter = chunk.GetComponent<MeshFilter>();
meshFilter.sharedMesh = mesh;
chunk.UpdateLOD();
chunk.MaterialLOD0 = grassMaterialLOD0;
chunk.MaterialLOD1 = grassMaterialLOD1;
chunk.MaterialLOD2 = grassMaterialLOD2;
chunk.MeshLOD0 = meshLOD0;
chunk.MeshLOD1 = meshLOD1;
chunk.MeshLOD2 = meshLOD2;
chunk.EnableShadowsLOD0 = enableShadowsLOD0;
chunk.EnableShadowsLOD1 = enableShadowsLOD1;
chunk.EnableShadowsLOD2 = enableShadowsLOD2;
}
}
private Mesh GenerateChunkMesh(Vector3 chunkPos, Bounds chunkBounds, BladeData[] blades, string meshName) {
List<Vector3> rootPositions = new(); // vertices
List<Vector3> tipOffsets = 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.Add(blade.tipOffset);
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); // Vector3 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() {
Gizmos.color = new Color(0f, 0.2f, 1f, 1f);

View File

@ -3,15 +3,6 @@
[KeywordEnum(LOD 0, LOD 1, LOD 2)] _LOD("Level of Detail", int) = 0
_BladeWidth("Breite", Range(0.003, .1)) = 0.01
_BendStrength("Bend Strength", Range(0, 1)) = 1
[Header(LOD 0)]
[IntRange] _NumSegments_LOD_0 ("Segments", Range(1,11)) = 11
[Header(LOD 1)]
[IntRange] _NumSegments_LOD_1 ("Segments", Range(1,5)) = 5
[Header(LOD 2)]
[IntRange] _NumSegments_LOD_2 ("Segments", Range(1,1)) = 1
}
SubShader {
LOD 100
@ -26,6 +17,7 @@
#pragma geometry geom
#pragma multi_compile _ SHADOWS_SCREEN
// shader_feature is very similar to multi_compile. Use multi_compile for keywords that are set from code.
#pragma shader_feature _LOD_LOD_0 _LOD_LOD_1 _LOD_LOD_2
#include "UnityCG.cginc"

View File

@ -32,7 +32,7 @@ const int ns = 11;
#elif _LOD_LOD_1
[MaxVertexCount(4 + ( 5 - 1 ) * 2)]
#elif _LOD_LOD_2
[MaxVertexCount(3)]
[MaxVertexCount(4)]
#endif
void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
const float3 basePos = IN[0].vertex.xyz;
@ -55,16 +55,58 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
// int numSegments = max(1, (int)(11) - ((cameraDistance) / 8.0) + N21(basePos));
#if _LOD_LOD_0
int numSegments = _NumSegments_LOD_0;
int numSegments = 11;
#elif _LOD_LOD_1
int numSegments = _NumSegments_LOD_1;
int numSegments = 5;
#elif _LOD_LOD_2
int numSegments = _NumSegments_LOD_2;
int numSegments = 1;
#endif
for (int i = 0; i < numSegments; i++) {
// in "blade space"
const float lowerThickness = 0.8;
#if _LOD_LOD_2
{
const float segmentWidth = halfWidth * pow(sin(lowerThickness * 3.141), .8);
const float3 widthOffset = getWidthOffset(segmentWidth, tipPosBladeSpace.xz) * max(1, cameraDistance / 8);
float3 segmentCenterBase = basePos;
float3 segmentCenterTip = basePos + tipPosBladeSpace;
const float3 vertLeftBase = segmentCenterBase + widthOffset;
const float3 vertRightBase = segmentCenterBase - widthOffset;
const float3 vertLeftTip = segmentCenterTip + widthOffset * 0.4;
const float3 vertRightTip = segmentCenterTip - widthOffset * 0.4;
o.pos = UnityObjectToClipPos(vertLeftBase);
#ifdef IS_IN_BASE_PASS
o.uv = float2(0, 0);
TRANSFER_SHADOW(o)
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertRightBase);
#ifdef IS_IN_BASE_PASS
o.uv = float2(1, 0);
TRANSFER_SHADOW(o)
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertLeftTip);
#ifdef IS_IN_BASE_PASS
o.uv = float2(0, 1);
TRANSFER_SHADOW(o)
#endif
triStream.Append(o);
o.pos = UnityObjectToClipPos(vertRightTip);
#ifdef IS_IN_BASE_PASS
o.uv = float2(1, 1);
TRANSFER_SHADOW(o)
#endif
triStream.Append(o);
}
continue;
#endif
// in "blade space"
const float segmentWidth = halfWidth * pow(sin(lowerThickness * 3.141 * (numSegments - i) / numSegments), .8);
const float segmentHeightNormalized = i / (float)numSegments;
// float nextSegmentHeightNormalized = (i+1) / (float)numSegments;
@ -128,13 +170,18 @@ fixed4 frag(g2f i) : SV_Target{
float3 light = float3(i.uv.yyy);
light *= max(.35, shadow);
#if _LOD_LOD_2
light *= 0.75;
#endif
#if _LOD_LOD_0
float3 albedo = float3(0.3,1.0,0.3);
#elif _LOD_LOD_1
float3 albedo = float3(1.0,0.7,0.3);
albedo = float3(0.3,1.0,0.3);
#elif _LOD_LOD_2
float3 albedo = float3(1.0,0.3,0.1);
albedo = float3(0.3,1.0,0.3);
#endif
return float4(light * albedo, 1);