MeshLOD: polish settings fields, bbox material, tracked car count
- Rename all MeshLodSettings public fields to camelCase (consistency with every other settings class in the project). - Replace the proxy-box renderer's borrowed car material with a shared matte Standard material tinted to the car body albedo; cheaper at LOD3 distances and responds correctly to scene lighting at night. - Expose GetTrackedCounts() and display loco/freight breakdown next to the Refresh Now button in the settings panel.
This commit is contained in:
parent
fcfdc6fba0
commit
1b4ab97be5
3 changed files with 80 additions and 45 deletions
|
|
@ -12,6 +12,7 @@ static class MeshLodInjector
|
|||
{
|
||||
static MeshLodSettings _settings = new();
|
||||
static readonly List<WeakReference<Car>> _trackedCars = new();
|
||||
static Material? _bboxMat;
|
||||
|
||||
// Debounced auto-refresh: set by MarkDirty, consumed by CheckAutoRefresh (called from Update).
|
||||
static float _pendingRefreshAt = -1f;
|
||||
|
|
@ -22,9 +23,9 @@ static class MeshLodInjector
|
|||
public static void Init(MeshLodSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
Log.Info($"MeshLOD settings: frac1={settings.Lod1RendererFraction:F2} frac2={settings.Lod2RendererFraction:F2} " +
|
||||
$"d1={settings.Lod1Distance}m d2={settings.Lod2Distance}m d3={settings.Lod3Distance}m " +
|
||||
$"locos={settings.ApplyToLocomotives} freight={settings.ApplyToFreightCars} " +
|
||||
Log.Info($"MeshLOD settings: frac1={settings.lod1RendererFraction:F2} frac2={settings.lod2RendererFraction:F2} " +
|
||||
$"d1={settings.lod1Distance}m d2={settings.lod2Distance}m d3={settings.lod3Distance}m " +
|
||||
$"locos={settings.applyToLocomotives} freight={settings.applyToFreightCars} " +
|
||||
$"lodBias={QualitySettings.lodBias:F2}");
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +33,19 @@ static class MeshLodInjector
|
|||
{
|
||||
_trackedCars.Clear();
|
||||
_pendingRefreshAt = -1f;
|
||||
if (_bboxMat != null) { UnityEngine.Object.Destroy(_bboxMat); _bboxMat = null; }
|
||||
}
|
||||
|
||||
public static (int total, int locos, int freight) GetTrackedCounts()
|
||||
{
|
||||
int total = 0, locos = 0;
|
||||
foreach (var wr in _trackedCars)
|
||||
{
|
||||
if (!wr.TryGetTarget(out Car? car) || car == null) continue;
|
||||
total++;
|
||||
if (car is BaseLocomotive) locos++;
|
||||
}
|
||||
return (total, locos, total - locos);
|
||||
}
|
||||
|
||||
// Called from the UI whenever any setting changes.
|
||||
|
|
@ -57,8 +71,8 @@ static class MeshLodInjector
|
|||
if (car.BodyTransform.GetComponent<LODGroup>() != null) return;
|
||||
|
||||
bool isLoco = car is BaseLocomotive;
|
||||
if ( isLoco && !_settings.ApplyToLocomotives) return;
|
||||
if (!isLoco && !_settings.ApplyToFreightCars) return;
|
||||
if ( isLoco && !_settings.applyToLocomotives) return;
|
||||
if (!isLoco && !_settings.applyToFreightCars) return;
|
||||
|
||||
MeshRenderer[] renderers = CollectRenderers(car.BodyTransform);
|
||||
if (renderers.Length == 0) return;
|
||||
|
|
@ -84,8 +98,8 @@ static class MeshLodInjector
|
|||
RemoveLods(car);
|
||||
|
||||
bool isLoco = car is BaseLocomotive;
|
||||
if ( isLoco && !_settings.ApplyToLocomotives) { skipped++; continue; }
|
||||
if (!isLoco && !_settings.ApplyToFreightCars) { skipped++; continue; }
|
||||
if ( isLoco && !_settings.applyToLocomotives) { skipped++; continue; }
|
||||
if (!isLoco && !_settings.applyToFreightCars) { skipped++; continue; }
|
||||
|
||||
MeshRenderer[] renderers = CollectRenderers(car.BodyTransform);
|
||||
if (renderers.Length == 0) { skipped++; continue; }
|
||||
|
|
@ -135,10 +149,10 @@ static class MeshLodInjector
|
|||
.OrderByDescending(r => BoundsVolume(r.bounds))
|
||||
.ToArray();
|
||||
|
||||
// LOD1: minor cull — keep the top Lod1RendererFraction of renderers
|
||||
int lod1Keep = Mathf.Max(1, Mathf.RoundToInt(sorted.Length * _settings.Lod1RendererFraction));
|
||||
// LOD2: major cull — keep only the top Lod2RendererFraction (must be ≤ lod1Keep)
|
||||
int lod2Keep = Mathf.Clamp(Mathf.RoundToInt(sorted.Length * _settings.Lod2RendererFraction), 1, lod1Keep);
|
||||
// LOD1: minor cull — keep the top lod1RendererFraction of renderers
|
||||
int lod1Keep = Mathf.Max(1, Mathf.RoundToInt(sorted.Length * _settings.lod1RendererFraction));
|
||||
// LOD2: major cull — keep only the top lod2RendererFraction (must be ≤ lod1Keep)
|
||||
int lod2Keep = Mathf.Clamp(Mathf.RoundToInt(sorted.Length * _settings.lod2RendererFraction), 1, lod1Keep);
|
||||
|
||||
Renderer[] lod0Rend = allRenderers.Cast<Renderer>().ToArray();
|
||||
Renderer[] lod1Rend = sorted.Take(lod1Keep).Cast<Renderer>().ToArray();
|
||||
|
|
@ -147,7 +161,7 @@ static class MeshLodInjector
|
|||
// LOD3: 12-triangle proxy box derived from the largest renderer's mesh-local AABB.
|
||||
Bounds localBounds = ComputeMeshLocalBounds(sorted[0], bodyRoot);
|
||||
MeshRenderer bboxMr = CreateBoundingBoxRenderer(bodyRoot, localBounds,
|
||||
sorted[0].sharedMaterial,
|
||||
GetOrCreateBBoxMat(sorted[0].sharedMaterial),
|
||||
sorted[0].gameObject.layer);
|
||||
Renderer[] lod3Rend = new[] { (Renderer)bboxMr };
|
||||
|
||||
|
|
@ -161,9 +175,9 @@ static class MeshLodInjector
|
|||
float lodBias = Mathf.Max(QualitySettings.lodBias, 0.01f);
|
||||
float k = sphereDiameter * lodBias / invFovFactor;
|
||||
|
||||
float lod1H = Mathf.Clamp(k / _settings.Lod1Distance, 0.001f, 0.999f);
|
||||
float lod2H = Mathf.Clamp(k / _settings.Lod2Distance, 0.001f, lod1H * 0.9f);
|
||||
float lod3H = Mathf.Clamp(k / _settings.Lod3Distance, 0.001f, lod2H * 0.9f);
|
||||
float lod1H = Mathf.Clamp(k / _settings.lod1Distance, 0.001f, 0.999f);
|
||||
float lod2H = Mathf.Clamp(k / _settings.lod2Distance, 0.001f, lod1H * 0.9f);
|
||||
float lod3H = Mathf.Clamp(k / _settings.lod3Distance, 0.001f, lod2H * 0.9f);
|
||||
|
||||
var lodGroup = bodyRoot.gameObject.AddComponent<LODGroup>();
|
||||
lodGroup.SetLODs(new LOD[]
|
||||
|
|
@ -253,6 +267,21 @@ static class MeshLodInjector
|
|||
|
||||
// ── Proxy box mesh ────────────────────────────────────────────────────────
|
||||
|
||||
// One matte Standard material shared across all proxy boxes; tinted to the
|
||||
// car body's albedo so it looks approximately correct under any lighting.
|
||||
static Material GetOrCreateBBoxMat(Material? src)
|
||||
{
|
||||
if (_bboxMat != null) return _bboxMat;
|
||||
var shader = Shader.Find("Standard") ?? Shader.Find("Legacy Shaders/Diffuse");
|
||||
_bboxMat = shader != null ? new Material(shader) : new Material(src);
|
||||
_bboxMat.color = src != null && src.HasProperty("_Color")
|
||||
? src.color
|
||||
: new Color(0.45f, 0.45f, 0.45f);
|
||||
if (_bboxMat.HasProperty("_Metallic")) _bboxMat.SetFloat("_Metallic", 0f);
|
||||
if (_bboxMat.HasProperty("_Glossiness")) _bboxMat.SetFloat("_Glossiness", 0.1f);
|
||||
return _bboxMat;
|
||||
}
|
||||
|
||||
static MeshRenderer CreateBoundingBoxRenderer(Transform parent, Bounds localBounds,
|
||||
Material mat, int layer)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ public class MeshLodSettings
|
|||
public bool enabled = false;
|
||||
|
||||
// Fraction of renderers kept at each progressive cull level (sorted by bounds volume desc).
|
||||
public float Lod1RendererFraction = 0.50f; // minor cull: remove tiny details
|
||||
public float Lod2RendererFraction = 0.15f; // major cull: structural geometry only
|
||||
public float lod1RendererFraction = 0.50f; // minor cull: remove tiny details
|
||||
public float lod2RendererFraction = 0.15f; // major cull: structural geometry only
|
||||
|
||||
// Distance in metres for each LOD transition. Converted to Unity's
|
||||
// screenRelativeTransitionHeight at apply time, corrected for lodBias.
|
||||
public float Lod1Distance = 80f; // LOD0 → LOD1 (full detail → minor cull)
|
||||
public float Lod2Distance = 200f; // LOD1 → LOD2 (minor cull → structural)
|
||||
public float Lod3Distance = 600f; // LOD2 → LOD3 (structural → proxy box)
|
||||
public float lod1Distance = 80f; // LOD0 -> LOD1 (full detail -> minor cull)
|
||||
public float lod2Distance = 200f; // LOD1 -> LOD2 (minor cull -> structural)
|
||||
public float lod3Distance = 600f; // LOD2 -> LOD3 (structural -> proxy box)
|
||||
|
||||
public bool ApplyToLocomotives = true;
|
||||
public bool ApplyToFreightCars = true;
|
||||
public bool applyToLocomotives = true;
|
||||
public bool applyToFreightCars = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ static class MeshLodSettingsUI
|
|||
GUILayout.Label("<b>Apply to</b>");
|
||||
GUILayout.Space(4f);
|
||||
|
||||
bool newLocos = GUILayout.Toggle(s.ApplyToLocomotives, " Locomotives");
|
||||
if (newLocos != s.ApplyToLocomotives) { s.ApplyToLocomotives = newLocos; changed = true; }
|
||||
bool newLocos = GUILayout.Toggle(s.applyToLocomotives, " Locomotives");
|
||||
if (newLocos != s.applyToLocomotives) { s.applyToLocomotives = newLocos; changed = true; }
|
||||
|
||||
bool newCars = GUILayout.Toggle(s.ApplyToFreightCars, " Freight / passenger cars");
|
||||
if (newCars != s.ApplyToFreightCars) { s.ApplyToFreightCars = newCars; changed = true; }
|
||||
bool newCars = GUILayout.Toggle(s.applyToFreightCars, " Freight / passenger cars");
|
||||
if (newCars != s.applyToFreightCars) { s.applyToFreightCars = newCars; changed = true; }
|
||||
|
||||
// ── Renderer fractions ────────────────────────────────────────────────
|
||||
GUILayout.Space(10f);
|
||||
|
|
@ -36,35 +36,35 @@ static class MeshLodSettingsUI
|
|||
|
||||
int ex60 = 60; // example car with 60 renderers
|
||||
GUILayout.Label(
|
||||
$" LOD1 (minor cull): {s.Lod1RendererFraction * 100f:F0}% kept " +
|
||||
$"— {Mathf.Max(1, Mathf.RoundToInt(ex60 * s.Lod1RendererFraction))}/{ex60} for a 60-renderer car",
|
||||
$" LOD1 (minor cull): {s.lod1RendererFraction * 100f:F0}% kept " +
|
||||
$"— {Mathf.Max(1, Mathf.RoundToInt(ex60 * s.lod1RendererFraction))}/{ex60} for a 60-renderer car",
|
||||
GUI.skin.label);
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Space(4f);
|
||||
float newF1 = GUILayout.HorizontalSlider(s.Lod1RendererFraction, 0.15f, 0.80f, GUILayout.Width(220f));
|
||||
float newF1 = GUILayout.HorizontalSlider(s.lod1RendererFraction, 0.15f, 0.80f, GUILayout.Width(220f));
|
||||
GUILayout.EndHorizontal();
|
||||
if (Mathf.Abs(newF1 - s.Lod1RendererFraction) > 0.005f)
|
||||
if (Mathf.Abs(newF1 - s.lod1RendererFraction) > 0.005f)
|
||||
{
|
||||
s.Lod1RendererFraction = Mathf.Round(newF1 * 20f) / 20f; // snap to 5% steps
|
||||
if (s.Lod1RendererFraction <= s.Lod2RendererFraction)
|
||||
s.Lod2RendererFraction = Mathf.Max(0.05f, s.Lod1RendererFraction - 0.10f);
|
||||
s.lod1RendererFraction = Mathf.Round(newF1 * 20f) / 20f; // snap to 5% steps
|
||||
if (s.lod1RendererFraction <= s.lod2RendererFraction)
|
||||
s.lod2RendererFraction = Mathf.Max(0.05f, s.lod1RendererFraction - 0.10f);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
GUILayout.Space(4f);
|
||||
GUILayout.Label(
|
||||
$" LOD2 (structural): {s.Lod2RendererFraction * 100f:F0}% kept " +
|
||||
$"— {Mathf.Max(1, Mathf.RoundToInt(ex60 * s.Lod2RendererFraction))}/{ex60} for a 60-renderer car",
|
||||
$" LOD2 (structural): {s.lod2RendererFraction * 100f:F0}% kept " +
|
||||
$"— {Mathf.Max(1, Mathf.RoundToInt(ex60 * s.lod2RendererFraction))}/{ex60} for a 60-renderer car",
|
||||
GUI.skin.label);
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Space(4f);
|
||||
float newF2 = GUILayout.HorizontalSlider(s.Lod2RendererFraction, 0.05f, 0.40f, GUILayout.Width(220f));
|
||||
float newF2 = GUILayout.HorizontalSlider(s.lod2RendererFraction, 0.05f, 0.40f, GUILayout.Width(220f));
|
||||
GUILayout.EndHorizontal();
|
||||
if (Mathf.Abs(newF2 - s.Lod2RendererFraction) > 0.005f)
|
||||
if (Mathf.Abs(newF2 - s.lod2RendererFraction) > 0.005f)
|
||||
{
|
||||
s.Lod2RendererFraction = Mathf.Round(newF2 * 20f) / 20f;
|
||||
if (s.Lod2RendererFraction >= s.Lod1RendererFraction)
|
||||
s.Lod1RendererFraction = Mathf.Min(0.80f, s.Lod2RendererFraction + 0.10f);
|
||||
s.lod2RendererFraction = Mathf.Round(newF2 * 20f) / 20f;
|
||||
if (s.lod2RendererFraction >= s.lod1RendererFraction)
|
||||
s.lod1RendererFraction = Mathf.Min(0.80f, s.lod2RendererFraction + 0.10f);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
@ -78,23 +78,29 @@ static class MeshLodSettingsUI
|
|||
GUI.skin.label);
|
||||
GUILayout.Space(4f);
|
||||
|
||||
DrawDistanceSlider(ref s.Lod1Distance, "LOD0→LOD1 (minor cull)", 30f, 500f, ref changed);
|
||||
DrawDistanceSlider(ref s.lod1Distance, "LOD0→LOD1 (minor cull)", 30f, 500f, ref changed);
|
||||
GUILayout.Space(2f);
|
||||
DrawDistanceSlider(ref s.Lod2Distance, "LOD1→LOD2 (structural)", s.Lod1Distance + 20f, 1000f, ref changed);
|
||||
if (s.Lod2Distance <= s.Lod1Distance) { s.Lod2Distance = s.Lod1Distance + 50f; changed = true; }
|
||||
DrawDistanceSlider(ref s.lod2Distance, "LOD1→LOD2 (structural)", s.lod1Distance + 20f, 1000f, ref changed);
|
||||
if (s.lod2Distance <= s.lod1Distance) { s.lod2Distance = s.lod1Distance + 50f; changed = true; }
|
||||
GUILayout.Space(2f);
|
||||
DrawDistanceSlider(ref s.Lod3Distance, "LOD2→LOD3 (proxy box) ", s.Lod2Distance + 20f, 1500f, ref changed);
|
||||
if (s.Lod3Distance <= s.Lod2Distance) { s.Lod3Distance = s.Lod2Distance + 100f; changed = true; }
|
||||
DrawDistanceSlider(ref s.lod3Distance, "LOD2→LOD3 (proxy box) ", s.lod2Distance + 20f, 1500f, ref changed);
|
||||
if (s.lod3Distance <= s.lod2Distance) { s.lod3Distance = s.lod2Distance + 100f; changed = true; }
|
||||
|
||||
// ── Manual refresh ────────────────────────────────────────────────────
|
||||
GUILayout.Space(10f);
|
||||
GUILayout.Label(" Settings auto-refresh loaded cars ~0.5 s after the last change.", GUI.skin.label);
|
||||
GUILayout.Space(4f);
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Refresh Now", GUILayout.Width(120f)))
|
||||
{
|
||||
MeshLodModule.Persist();
|
||||
MeshLodInjector.RefreshAll();
|
||||
}
|
||||
var (total, locos, freight) = MeshLodInjector.GetTrackedCounts();
|
||||
GUILayout.Label(
|
||||
$" {total} tracked ({locos} loco{(locos == 1 ? "" : "s")}, {freight} car{(freight == 1 ? "" : "s")})",
|
||||
GUI.skin.label);
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.EndVertical();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue