LODs: generate meshes with fewer blades + disable shadows
This commit is contained in:
parent
d4397c6532
commit
a4ffd7a234
File diff suppressed because one or more lines are too long
@ -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>();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user