Smooth LOD transition (density and blade detail) + finetuning
This commit is contained in:
parent
dce3637ad6
commit
5c2d0c7260
File diff suppressed because one or more lines are too long
@ -3,4 +3,5 @@ using UnityEngine;
|
||||
public struct BladeData {
|
||||
public Vector3 rootPosition;
|
||||
public Vector3 tipOffset;
|
||||
public bool missingInNextLOD;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user