Update LODs in Editor

This commit is contained in:
Timo Eberl 2025-08-28 17:27:00 +02:00
parent 6152e1fc1a
commit d6c0048271
Signed by: Timo
SSH Key Fingerprint: SHA256:swVjhbVzKCLQZNtwPqMEmtOUG3FTydzVrpIKpUZYTQw
5 changed files with 96 additions and 79 deletions

View File

@ -140790,6 +140790,7 @@ GameObject:
- component: {fileID: 963194227}
- component: {fileID: 963194226}
- component: {fileID: 963194229}
- component: {fileID: 963194230}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@ -140864,14 +140865,26 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
serializedVersion: 2
m_LocalRotation: {x: 0.23506844, y: 0.3851214, z: -0.1021131, w: 0.8865649}
m_LocalPosition: {x: 6.34, y: 2.02, z: 3.8}
m_LocalRotation: {x: 0.23339066, y: 0.37273654, z: -0.097438015, w: 0.8928058}
m_LocalPosition: {x: 6.6, y: 3.5, z: 4.2}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 29.7, y: 46.96, z: 0}
m_LocalEulerAnglesHint: {x: 29.3, y: 45.32, z: 0}
--- !u!114 &963194229
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 78c9485abe801d74184e00172d4b42c4, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &963194230
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -140880,7 +140893,7 @@ MonoBehaviour:
m_GameObject: {fileID: 963194225}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 78c9485abe801d74184e00172d4b42c4, type: 3}
m_Script: {fileID: 11500000, guid: d9be733811a3b0342953552aa8d5ab8a, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!43 &963631122
@ -290519,7 +290532,6 @@ MonoBehaviour:
numChunks: 20
bladesPerSquareMeter: 100
chunkPrefab: {fileID: 6414307754184109852, guid: abb47bb4e9622f7428302dfa4730d722, type: 3}
ignoreHeightForLOD: 0
grassMaterialLOD0: {fileID: 2100000, guid: d979d9bbf3760d948a452ba192cbf563, type: 2}
minDistanceLOD0: 3.5
densityLOD0: 1

View File

@ -1,37 +1,30 @@
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;
[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;
[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;
[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;
[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
@ -49,12 +42,12 @@ public class GrassField : MonoBehaviour {
var camPosWorld = Camera.main ? Camera.main.transform.position : Vector3.zero;
UpdateLODs(camPosWorld);
UpdateMaterials(camPosWorld);
UpdateMaterials();
}
public void UpdateLODsAndMaterials() {
var camPosWorld = Camera.main ? Camera.main.transform.position : Vector3.zero;
UpdateMaterials(camPosWorld);
UpdateMaterials();
UpdateLODs(camPosWorld);
}
@ -64,11 +57,12 @@ public class GrassField : MonoBehaviour {
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);
@ -81,26 +75,21 @@ public class GrassField : MonoBehaviour {
UpdateLODsAndMaterials();
}
private void FixedUpdate() {
private void Update() {
#if UNITY_EDITOR
// update LOD distances (might have been changed via inspector)
UpdateLODsAndMaterials();
#else
if (_chunks.Length != numChunks * numChunks) return;
var cam = Camera.main;
if (!cam) return;
var camPosWorld = cam.transform.position;
#if UNITY_EDITOR
// update LOD distances and camera position
UpdateMaterials(camPosWorld);
UpdateLODs(cam.transform.position, false); // while the game is running, only update the chunks near the camera
#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),
@ -109,7 +98,7 @@ public class GrassField : MonoBehaviour {
// 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 }
? new GridRange { fromX = 0, toX = numChunks - 1, fromZ = 0, toZ = numChunks - 1 }
: GetLODGridRange(camPos, chunkSize);
for (int x = range.fromX; x <= range.toX; x++)
@ -142,17 +131,17 @@ public class GrassField : MonoBehaviour {
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.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);
@ -161,28 +150,19 @@ public class GrassField : MonoBehaviour {
return range;
}
private void UpdateMaterials(Vector3 camPosWorld) {
private void UpdateMaterials() {
var chunkSize = ((float)size) / numChunks;
var chunkDiagonalLength = chunkSize * SQRT_2;
var transition0From = minDistanceLOD0;
var transition0To = distanceLOD1;
var transition0To = distanceLOD1;
var transition1From = distanceLOD1 + chunkDiagonalLength;
var transition1To = distanceLOD2;
var transition1To = distanceLOD2;
var transition2From = distanceLOD2 + chunkDiagonalLength;
var transition2To = distanceLOD3;
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(
@ -195,6 +175,7 @@ public class GrassField : MonoBehaviour {
// 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);
@ -216,7 +197,7 @@ public class GrassField : MonoBehaviour {
var meshLOD3 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD3, "grass_chunk_" + x + "_" + y + "_lod3");
var chunk = Instantiate(chunkPrefab, chunksContainer.transform);
chunk.name = "Chunk [" + x + ", " + y +"]";
chunk.name = "Chunk [" + x + ", " + y + "]";
chunk.transform.localPosition = chunkPos;
chunk.MaterialLOD0 = grassMaterialLOD0;
@ -260,6 +241,7 @@ public class GrassField : MonoBehaviour {
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)
@ -272,13 +254,16 @@ public class GrassField : MonoBehaviour {
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)
previousBounds.size + new Vector3(2f * width, height, 2f * width)
);
return mesh;
}
private void OnDrawGizmos() {
// when in editor, update LODs every frame. nothing to do with gizmos
UpdateLODsAndMaterials();
var chunkSize = ((float)size) / numChunks;
var tPos = transform.position;
@ -299,17 +284,12 @@ public class GrassField : MonoBehaviour {
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;
@ -317,7 +297,7 @@ public class GrassField : MonoBehaviour {
Gizmos.DrawWireSphere(center, minDistLOD0);
Gizmos.DrawWireSphere(center, minDistLOD1);
Gizmos.DrawWireSphere(center, minDistLOD2);
Gizmos.matrix = oldMatrix;
}
}

View File

@ -0,0 +1,17 @@
using UnityEngine;
using UnityEditor;
[ExecuteAlways]
public class MainCameraPositionShaderSetter : MonoBehaviour {
private static readonly int pId_MainCameraPosition = Shader.PropertyToID("_MainCameraPosition");
private void OnEnable() {
if (!Camera.main || Camera.main.gameObject != this.gameObject) {
Debug.LogWarning("MainCameraPositionShaderSetter set on object that is not the main camera");
}
}
private void Update() {
Shader.SetGlobalVector(pId_MainCameraPosition, this.transform.position);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d9be733811a3b0342953552aa8d5ab8a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -6,9 +6,9 @@ sampler2D _FrabVarianz, _BladeWidthTex;
float2 _TransitionRange;
#if UNITY_EDITOR
float3 _WorldSpaceCameraPosEditor;
#endif
// Custom variable that is set to the position of the main camera in the scene
// (instead of Unity's _WorldSpaceCameraPos, which uses the scene view camera in editor mode)
float3 _MainCameraPosition;
struct MeshData {
float4 vertex : POSITION;
@ -40,11 +40,7 @@ v2g vert (MeshData v) {
}
float getCameraDistance(float3 pos) {
#if UNITY_EDITOR
return distance(_WorldSpaceCameraPosEditor, pos);
#else
return distance(_WorldSpaceCameraPos, pos);
#endif
return distance(_MainCameraPosition, pos);
}
#if _LOD_LOD_0
@ -92,17 +88,18 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
g2f o;
float3 previousSegmentCenter = basePos - float3(0,-1,0); // point beneath the basePos
int numSegments;
#if _LOD_LOD_0
int numSegments = lerp(11.99, 5.0, interpolator); // 11 - 5
numSegments = lerp(11.99, 5.0, interpolator); // 11 - 5
#elif _LOD_LOD_1
int numSegments = lerp(5.99, 2.0, interpolator); // 5 - 2
numSegments = lerp(5.99, 2.0, interpolator); // 5 - 2
#elif _LOD_LOD_2
int numSegments = 2;
numSegments = 2;
#elif _LOD_LOD_3
int numSegments = 1;
numSegments = 1;
#endif
float3 localCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1.0)).xyz;
float3 localCameraPos = mul(unity_WorldToObject, float4(_MainCameraPosition.xyz, 1.0)).xyz;
for (int i = 0; i < numSegments; i++) {
const float lowerThickness = 0.8;
@ -136,7 +133,7 @@ void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
}
//normalWS = float3(1,1,1) * lookingFromAbove;
//normalWS = basePosWS - _WorldSpaceCameraPos.xyz;
//normalWS = basePosWS - _MainCameraPosition.xyz;
//normalWS = camVec;
o.pos = UnityObjectToClipPos(vertLeftBase);
@ -262,7 +259,7 @@ float4 frag(g2f i) : SV_Target{
half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
float3 lightReflectDirection = reflect(-lightDirection, worldNormal);
float3 viewDirection = normalize(float3(float4(_WorldSpaceCameraPos.xyz, 1.0) - i.worldPos.xyz));
float3 viewDirection = normalize(float3(float4(_MainCameraPosition.xyz, 1.0) - i.worldPos.xyz));
float atten = 1.0;