Smooth LOD transition (density and blade detail) + finetuning

This commit is contained in:
Timo Eberl 2024-10-03 04:31:23 +02:00
parent dce3637ad6
commit 5c2d0c7260
Signed by: Timo
SSH Key Fingerprint: SHA256:swVjhbVzKCLQZNtwPqMEmtOUG3FTydzVrpIKpUZYTQw
7 changed files with 284699 additions and 284675 deletions

File diff suppressed because one or more lines are too long

View File

@ -3,4 +3,5 @@ using UnityEngine;
public struct BladeData {
public Vector3 rootPosition;
public Vector3 tipOffset;
public bool missingInNextLOD;
}

View File

@ -1,15 +1,17 @@
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 bladesPerMeter = 100;
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")]
@ -38,7 +40,7 @@ public class GrassField : MonoBehaviour {
public void GenerateGrassField() {
var bladesLOD0 = GrassMeshGeneration.GenerateBladesRandom(
size, size * size * (int)(bladesPerMeter * densityLOD0)
size, size * size * (int)(bladesPerSquareMeter * densityLOD0)
);
var bladesLOD1 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD0, densityLOD1);
var bladesLOD2 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD1, densityLOD2 / densityLOD1);
@ -161,12 +163,12 @@ public class GrassField : MonoBehaviour {
var chunkSize = ((float)size) / numChunks;
var chunkDiagonalLength = chunkSize * SQRT_2;
var transition0From = 0f;
var transition0To = distanceLOD1;
var transition1From = transition0To + chunkDiagonalLength;
var transition1To = distanceLOD2;
var transition2From = transition1To + chunkDiagonalLength;
var transition2To = distanceLOD3;
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));
@ -234,14 +236,17 @@ public class GrassField : MonoBehaviour {
private Mesh GenerateChunkMesh(Vector3 chunkPos, Bounds chunkBounds, BladeData[] blades, string meshName) {
List<Vector3> rootPositions = new(); // vertices
List<Vector3> tipOffsets = new(); // uv0
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.Add(blade.tipOffset);
tipOffsets_missingInNextLOD.Add(new Vector4(
blade.tipOffset.x, blade.tipOffset.y, blade.tipOffset.z,
blade.missingInNextLOD ? 1f : 0f)
);
indices.Add(rootPositions.Count - 1);
}
}
@ -254,7 +259,7 @@ public class GrassField : MonoBehaviour {
Debug.LogWarning(meshName + " has more than 65536 grass blades. Mesh.indexFormat set to UInt32.");
}
mesh.SetVertices(rootPositions);
mesh.SetUVs(0, tipOffsets); // Vector3 UVs
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)
@ -304,10 +309,12 @@ public class GrassField : MonoBehaviour {
Gizmos.DrawWireSphere(center, distanceLOD3);
Gizmos.color = new Color(1.0f, 1.0f, 0.0f, 1f);
var minDistLOD2 = distanceLOD1 + chunkSize * SQRT_2;
var minDistLOD3 = distanceLOD2 + chunkSize * SQRT_2;
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.DrawWireSphere(center, minDistLOD3);
Gizmos.matrix = oldMatrix;
}

View File

@ -15,12 +15,16 @@ public class GrassMeshGeneration {
return blades;
}
// modifies "from" (marks blades as "missingInNextLOD")
// 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;
if (Random.Range(0f, 1f) > r) {
from[i].missingInNextLOD = true;
continue;
}
BladeData blade = from[i];
blades.Add(blade);

View File

@ -10,11 +10,12 @@ float3 _WorldSpaceCameraPosEditor;
struct MeshData {
float4 vertex : POSITION;
float3 tipOffset : TEXCOORD0;
float4 tipOffset_missingInNextLOD : TEXCOORD0;
};
struct v2g {
float4 vertex : SV_POSITION;
float3 tipOffset : TEXCOORD0;
float missingInNextLOD : TEXCOORD1;
};
struct g2f {
@ -29,7 +30,8 @@ struct g2f {
v2g vert (MeshData v) {
v2g o;
o.vertex = v.vertex;
o.tipOffset = v.tipOffset;
o.tipOffset = v.tipOffset_missingInNextLOD.xyz;
o.missingInNextLOD = v.tipOffset_missingInNextLOD.w;
return o;
}
@ -52,8 +54,8 @@ float getCameraDistance(float3 pos) {
#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;
const float3 tipOffset = IN[0].tipOffset;
const float3 tipPosObjectSpace = basePos + tipOffset;
// const float4 basePosClipSpace = UnityObjectToClipPos(float4(basePos, 1));
// const float4 tipPosClipSpace = UnityObjectToClipPos(float4(tipPosObjectSpace, 1));
@ -64,31 +66,32 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
const float cameraDistance = getCameraDistance(basePosWS);
float interpolator = invLerp(_TransitionRange.x, _TransitionRange.y, cameraDistance);
// 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;
g2f o;
// randomly shrink blades that will be removed in the next LOD
if (IN[0].missingInNextLOD > 0.0) {
float widthMultiplier = remap(N21(basePos.xz) * 0.75, 1, 1, 0, interpolator);
widthMultiplier = max(0, widthMultiplier);
halfWidth *= widthMultiplier;
}
// randomly shrink shadow blades to create a smooth transition between shadowed and non-shadowed area
#ifdef IS_IN_SHADOW_PASS
#if _LOD_LOD_1
float widthMultiplier = remap(N21(basePos.xz) * 0.5, 1, 1, 0, interpolator);
widthMultiplier = max(0, widthMultiplier);
halfWidth *= widthMultiplier;
#endif
#endif
g2f o;
float3 previousSegmentCenter = basePos - float3(0,-1,0); // point beneath the basePos
// int numSegments = max(1, (int)(11) - ((cameraDistance) / 8.0) + N21(basePos));
#if _LOD_LOD_0
int numSegments = 11;
int numSegments = lerp(11.99, 5.0, interpolator); // 11 - 5
#elif _LOD_LOD_1
int numSegments = 5;
int numSegments = lerp(5.99, 2.0, interpolator); // 5 - 2
#elif _LOD_LOD_2
int numSegments = 2;
#elif _LOD_LOD_3
@ -96,17 +99,18 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
#endif
for (int i = 0; i < numSegments; i++) {
const float lowerThickness = 0.8;
// LOD 3 has a different algorithm
#if _LOD_LOD_3
{
const float segmentWidth = halfWidth * pow(sin(lowerThickness * 3.141), .8);
const float3 widthOffset = getWidthOffset(segmentWidth, tipPosBladeSpace.xz) * max(1, cameraDistance / 8);
const float3 widthOffset = getWidthOffset(segmentWidth, tipOffset.xz) * max(1, cameraDistance / 10);
float3 segmentCenterBase = basePos;
float3 segmentCenterTip = basePos + tipPosBladeSpace;
const float3 vertLeftBase = segmentCenterBase + widthOffset * 2.0;
const float3 vertRightBase = segmentCenterBase - widthOffset * 2.0;
const float3 vertLeftTip = segmentCenterTip + widthOffset * 0.5;
const float3 vertRightTip = segmentCenterTip - widthOffset * 0.5;
float3 segmentCenterTip = basePos + tipOffset;
const float3 vertLeftBase = segmentCenterBase + widthOffset * 2.5;
const float3 vertRightBase = segmentCenterBase - widthOffset * 2.5;
const float3 vertLeftTip = segmentCenterTip + widthOffset * 0.4;
const float3 vertRightTip = segmentCenterTip - widthOffset * 0.4;
o.pos = UnityObjectToClipPos(vertLeftBase);
#ifdef IS_IN_BASE_PASS
@ -143,15 +147,15 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
}
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;
// generate new verts
const float3 widthOffset = getWidthOffset(segmentWidth, tipPosBladeSpace.xz) * max(1, cameraDistance / 8);
float3 segmentCenter = basePos + bendParabula(tipPosBladeSpace, segmentHeightNormalized, _BendStrength);
const float3 widthOffset = getWidthOffset(segmentWidth, tipOffset.xz) * max(1, cameraDistance / 10);
float3 segmentCenter = basePos + bendParabula(tipOffset, segmentHeightNormalized, _BendStrength);
const float3 segmentCenterSnapshot = segmentCenter;
const float3 vertLeft = segmentCenter + widthOffset;
@ -196,7 +200,7 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
previousSegmentCenter = segmentCenterSnapshot;
}
o.pos = UnityObjectToClipPos(basePos + tipPosBladeSpace * float3(_BendStrength,1,_BendStrength));
o.pos = UnityObjectToClipPos(basePos + tipOffset * float3(_BendStrength,1,_BendStrength));
#ifdef IS_IN_BASE_PASS
o.uv = float2(.5, 1);
TRANSFER_SHADOW(o)
@ -212,12 +216,14 @@ float4 frag(g2f i) : SV_Target{
const float shadow = SHADOW_ATTENUATION(i);
float3 light = float3(i.uv.yyy);
float light = i.uv.y;
light *= max(.35, shadow);
#if _LOD_LOD_2
light *= 0.75; // fake shadow
#if _LOD_LOD_1
light = pow(light, 1 + 0.4 * i.transitionInterpolator);
#elif _LOD_LOD_2
light = pow(light, 1.4); // fake shadow
#elif _LOD_LOD_3
light *= 0.6; // fake shadow
light = pow(light, 1.4) * 0.7; // fake shadow
#endif
float3 albedo;
@ -230,7 +236,7 @@ float4 frag(g2f i) : SV_Target{
#elif _LOD_LOD_3
albedo = _Color3.xyz;
#endif
// albedo = _Color0.xyz;
albedo = _Color0.xyz;
return float4(light * albedo, 1);

View File

@ -1,3 +1,8 @@
float invLerp(float a, float b, float v) {
return clamp( (v-a) / (b-a) , 0, 1);
}
float remap(float iMin, float iMax, float oMin, float oMax, float v) {
const float t = invLerp(iMin, iMax, v);
return lerp(oMin, oMax, t);
}

View File

@ -6,9 +6,9 @@ Optimized rendering of huge grass fields.
- [x] LODs: reduced detail (number of vertices)
- [x] LODs: reduced number of blades
- [ ] Smooth LOD transition: number of blades
- [ ] Smooth LOD transition: detail
- [ ] Thicker blades with increased distance
- [x] Smooth LOD transition: number of blades
- [x] Smooth LOD transition: detail
- [x] Thicker blades with increased distance
- [ ] Cut map
- [ ] Cut map LOD versions
- [ ] Lighting