286 lines
10 KiB
C#
286 lines
10 KiB
C#
// Perfect Culling (C) 2021 Patrick König
|
|
//
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
namespace Koenigz.PerfectCulling
|
|
{
|
|
public static class PerfectCullingUtil
|
|
{
|
|
public static string FormatSeconds(double seconds)
|
|
{
|
|
System.TimeSpan ts = System.TimeSpan.FromSeconds(seconds);
|
|
|
|
return ts.ToString(@"hh\:mm\:ss");
|
|
}
|
|
|
|
public static HashSet<PerfectCullingBakeGroup> CreateBakeGroupsForRenderers(List<Renderer> inputRenderers, System.Func<Renderer, bool> filter, PerfectCullingBakingBehaviour attachedBakingBehaviour = null)
|
|
{
|
|
// Filter all unsupported renderers
|
|
inputRenderers.RemoveAll(rend => filter != null && !filter.Invoke(rend));
|
|
|
|
// We are going to manipulate this. So we clone it just in case. We also use a Hashset to prevent double insertions.
|
|
HashSet<Renderer> sceneRenderers = new HashSet<Renderer>(inputRenderers);
|
|
|
|
// Find all the objects in the scene
|
|
HashSet<LODGroup> lodGroups = new HashSet<LODGroup>(UnityEngine.Object.FindObjectsOfType<LODGroup>());
|
|
HashSet<PerfectCullingMonoGroup> perfectCullingGroups = new HashSet<PerfectCullingMonoGroup>(FindMonoGroupsForBakingBehaviour(attachedBakingBehaviour));
|
|
|
|
lodGroups.RemoveWhere((lodGroup) =>
|
|
{
|
|
foreach (LOD lod in lodGroup.GetLODs())
|
|
{
|
|
foreach (Renderer r in lod.renderers)
|
|
{
|
|
if (sceneRenderers.Contains(r))
|
|
{
|
|
// Keep this LODGroup. We selected its renderers.
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Our output list
|
|
HashSet<PerfectCullingBakeGroup> result = new HashSet<PerfectCullingBakeGroup>(new PerfectCullingBakeGroupComparer());
|
|
|
|
// We are going to remove already processed elements from our renderers list.
|
|
//
|
|
// Priority goes as follow:
|
|
// - PerfectCullingGroup
|
|
// - LODGroup
|
|
// - Anything else
|
|
|
|
// PerfectCullingGroup
|
|
foreach (PerfectCullingMonoGroup perfectCullingGroup in perfectCullingGroups)
|
|
{
|
|
foreach (Renderer renderer in perfectCullingGroup.Renderers)
|
|
{
|
|
sceneRenderers.Remove(renderer);
|
|
}
|
|
|
|
result.Add(new PerfectCullingBakeGroup()
|
|
{
|
|
renderers = perfectCullingGroup.Renderers.ToArray(), // Renderers filters null renderers automatically
|
|
unityBehaviours = perfectCullingGroup.UnityBehaviours.ToArray(), // UnityBehaviours filters null renderers automatically
|
|
groupType = PerfectCullingBakeGroup.GroupType.User
|
|
});
|
|
}
|
|
|
|
// LODGroup
|
|
foreach (LODGroup lodGroup in lodGroups)
|
|
{
|
|
HashSet<Renderer> renderersInLodGroup = new HashSet<Renderer>();
|
|
|
|
foreach (LOD lod in lodGroup.GetLODs())
|
|
{
|
|
foreach (Renderer renderer in lod.renderers)
|
|
{
|
|
if (renderer == null)
|
|
{
|
|
Debug.LogWarning($"Found null renderer in LODGroup: {lodGroup.name}. Selecting the LODGroup might remove the invalid renderer(s) for you.", lodGroup.gameObject);
|
|
|
|
continue;
|
|
}
|
|
|
|
renderersInLodGroup.Add(renderer);
|
|
sceneRenderers.Remove(renderer);
|
|
}
|
|
}
|
|
|
|
if (renderersInLodGroup.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result.Add(new PerfectCullingBakeGroup()
|
|
{
|
|
renderers = renderersInLodGroup.ToArray(),
|
|
groupType = PerfectCullingBakeGroup.GroupType.LOD
|
|
});
|
|
}
|
|
|
|
// Remaining renderers
|
|
foreach (Renderer renderer in sceneRenderers)
|
|
{
|
|
result.Add(new PerfectCullingBakeGroup()
|
|
{
|
|
renderers = new Renderer[] { renderer },
|
|
groupType = PerfectCullingBakeGroup.GroupType.Other
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static PerfectCullingMonoGroup[] FindMonoGroupsForBakingBehaviour(PerfectCullingBakingBehaviour attachedBakingBehaviour)
|
|
{
|
|
return UnityEngine.Object.FindObjectsOfType<PerfectCullingMonoGroup>().Where((group) =>
|
|
{
|
|
if (group.restrictToBehaviours.Length == 0 || attachedBakingBehaviour == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return System.Array.IndexOf(group.restrictToBehaviours, attachedBakingBehaviour) >= 0;
|
|
}).ToArray();
|
|
}
|
|
|
|
public static string FormatNumber(int number)
|
|
{
|
|
if (number >= 100000000) {
|
|
return (number / 1000000f).ToString("0.#M");
|
|
}
|
|
else if (number >= 1000000) {
|
|
return (number / 1000000f).ToString("0.##M");
|
|
}
|
|
else if (number >= 100000) {
|
|
return (number / 1000f).ToString("0.#k");
|
|
}
|
|
else if (number >= 10000) {
|
|
return (number / 1000f).ToString("0.##k");
|
|
}
|
|
|
|
return number.ToString("#,0");
|
|
}
|
|
|
|
public static float FindValidDivisorCloseToUserProvided(float userProvidedDivisor, float volumeSize)
|
|
{
|
|
float bestFit = 0;
|
|
|
|
for (float i = 0; i < volumeSize; i += 1f / 4f)
|
|
{
|
|
if (volumeSize % i == 0)
|
|
{
|
|
if ((bestFit <= 0) ||
|
|
Mathf.Abs(i - userProvidedDivisor) < Mathf.Abs(bestFit - userProvidedDivisor))
|
|
{
|
|
bestFit = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestFit <= 0)
|
|
{
|
|
return volumeSize;
|
|
}
|
|
|
|
return bestFit;
|
|
}
|
|
|
|
// Keywords that we use to assume the material should be transparent.
|
|
private static readonly string[] transparentShaderKeywordHints = new string[]
|
|
{
|
|
"_ALPHATEST_ON",
|
|
"ALPHACLIPPING_ON"
|
|
};
|
|
|
|
public const byte OPAQUE_RENDER_COLOR = 0;
|
|
public const byte TRANSPARENT_RENDER_COLOR = 128;
|
|
|
|
|
|
public static bool IsMaterialTransparent(Material mat)
|
|
{
|
|
#pragma warning disable 162
|
|
if (mat == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!PerfectCullingSettings.Instance.renderTransparency)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check whether this material is forced to render transparent or opaque.
|
|
string nameLower = mat.name.ToLower();
|
|
|
|
if (nameLower.Contains("pc_trans"))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (nameLower.Contains("pc_opaque"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// It's pretty hard to detect transparent materials. Lets use some shader keywords as a hint.
|
|
|
|
foreach (var keyword in transparentShaderKeywordHints)
|
|
{
|
|
if (mat.IsKeywordEnabled(keyword))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return mat.renderQueue >= 2450;
|
|
#pragma warning restore 162
|
|
}
|
|
|
|
// Allows to customize the behaviour easily. For instance could assign it to a different layer, etc. instead.
|
|
public static void ToggleRenderer(Renderer r, bool visible, bool forceNullCheck, ShadowCastingMode defaultShadowCastingMode)
|
|
{
|
|
if (forceNullCheck && r == null)
|
|
{
|
|
// We forced null checks for a reason and are aware. No need to output anything.
|
|
return;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
if (r == null)
|
|
{
|
|
// This is an unexpected null renderer. We want to log an error.
|
|
|
|
Debug.LogError("Encountered null renderer.");
|
|
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
#pragma warning disable 162
|
|
switch (PerfectCullingConstants.ToggleRenderMode)
|
|
{
|
|
case PerfectCullingRenderToggleMode.ToggleRendererComponent:
|
|
r.enabled = visible;
|
|
break;
|
|
|
|
case PerfectCullingRenderToggleMode.ToggleShadowcastMode:
|
|
if (defaultShadowCastingMode == ShadowCastingMode.Off || defaultShadowCastingMode == ShadowCastingMode.TwoSided)
|
|
{
|
|
// We don't care about shadows so we might as well disable the entire Renderer.
|
|
|
|
#if UNITY_2019_3_OR_NEWER
|
|
goto case PerfectCullingRenderToggleMode.ToggleForceRenderingOff;
|
|
#else
|
|
goto case PerfectCullingRenderToggleMode.ToggleRendererComponent;
|
|
#endif
|
|
}
|
|
|
|
r.shadowCastingMode = visible ? defaultShadowCastingMode : ShadowCastingMode.ShadowsOnly;
|
|
break;
|
|
|
|
case PerfectCullingRenderToggleMode.ToggleForceRenderingOff:
|
|
#if !UNITY_2019_3_OR_NEWER
|
|
// Unsupported before Unity 2019. This is the next best thing we can do (performance wise).
|
|
// I don't like that this happens silently but printing a warning when it is unsupported doesn't help either.
|
|
goto case PerfectCullingRenderToggleMode.ToggleShadowcastMode;
|
|
#else
|
|
r.forceRenderingOff = !visible;
|
|
#endif
|
|
break;
|
|
|
|
default:
|
|
throw new System.InvalidOperationException();
|
|
break;
|
|
}
|
|
#pragma warning restore 162
|
|
}
|
|
}
|
|
} |