Files
Cielonos/Assets/PotaToon/Runtime/Scripts/CharacterShadowUtils.cs
SoulliesOfficial f7af60351b 阶段性完成
2025-12-08 05:27:53 -05:00

467 lines
22 KiB
C#

using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace PotaToon
{
internal static class CharacterShadowUtils
{
private static List<VisibleLight> s_vSpotLights = new List<VisibleLight>(256);
private static List<int> s_vSpotLightIndices = new List<int>(256);
private static int[] s_SpotLightIndices = new int[2]; // Reusable array to prevent GC Alloc
private static List<KeyValuePair<float, int>> s_SortedSpotLights = new List<KeyValuePair<float, int>>(256);
private static Vector3 s_DefaultLightDirection = new Vector3(0.43f, 0.5f, -0.75f).normalized; // (30, -30, 0)
private static VirtualShadowCamera s_ShadowCamera = new VirtualShadowCamera();
internal static VirtualShadowCamera shadowCamera => s_ShadowCamera;
internal static bool isCharShadowValid => shadowCamera.culledRendererCount > 0;
public struct BrightestLightData
{
public Vector3 lightDirection;
public int lightIndex;
public bool isMainLight;
}
public class VirtualShadowCamera
{
public Renderer closestRenderer;
public int culledRendererCount => m_CulledRenderers.Count;
public float distanceCameraToNearestRenderer;
public Vector3 lightDirectionOffset;
public bool overrideLightDirection;
public float maxBoundSize => Mathf.Max(m_TargetBounds.size.x, m_TargetBounds.size.y, m_TargetBounds.size.z);
public float maxScreenRimDistance;
private Bounds m_TargetBounds;
private List<Renderer> m_CulledRenderers = new List<Renderer>();
private float m_NearClipPlane = 0.01f;
private float m_FarClipPlane = 100f;
private Vector3 m_Position;
private Quaternion m_Rotation = Quaternion.identity;
private Vector3[] m_BoundsCorners = new Vector3[8];
private Vector3 m_PrevPosition;
private Quaternion m_PrevRotation;
private bool m_IsTweening;
private int m_PrevActiveCharacters;
private int m_TweenElapsedFrame;
private const int k_TweenTargetFrame = 7;
private const float k_RcpTweenTargetFrame = 1.0f / k_TweenTargetFrame;
private Vector3 m_TweenDestPosition;
private Quaternion m_TweenDestRotation;
public Matrix4x4 projectionMatrix => Matrix4x4.Perspective(45f, 1.0f, m_NearClipPlane, m_FarClipPlane);
internal void Prepare(Camera camera, float cullingDistance)
{
m_TargetBounds = new Bounds();
distanceCameraToNearestRenderer = 0f;
if (PotaToonCharacter.activeRenderers == null)
return;
m_FarClipPlane = GetCullingDistance(camera, cullingDistance);
m_CulledRenderers.Clear();
closestRenderer = null;
distanceCameraToNearestRenderer = float.MaxValue;
foreach (var renderer in PotaToonCharacter.activeRenderers)
{
if (renderer != null)
{
if (IntersectTest(renderer, camera, out var dist))
{
if (dist < distanceCameraToNearestRenderer)
{
closestRenderer = renderer;
distanceCameraToNearestRenderer = dist;
}
m_CulledRenderers.Add(renderer);
}
}
}
}
public Matrix4x4 GetViewMatrix()
{
var viewMatrix = Matrix4x4.TRS(m_Position, m_Rotation, Vector3.one).inverse;
if (SystemInfo.usesReversedZBuffer)
{
viewMatrix.m20 = -viewMatrix.m20;
viewMatrix.m21 = -viewMatrix.m21;
viewMatrix.m22 = -viewMatrix.m22;
viewMatrix.m23 = -viewMatrix.m23;
}
return viewMatrix;
}
public void UpdateCameraTransform(Light light)
{
var currPosition = m_Position;
var currRotation = m_Rotation;
// Ignore z axis since z+ axis in light is used for projection.
var eulerAngles = light.transform.rotation.eulerAngles;
eulerAngles.z = 0f;
m_Rotation = overrideLightDirection ? Quaternion.Euler(lightDirectionOffset) : Quaternion.Euler(eulerAngles + lightDirectionOffset);
if (culledRendererCount <= 0)
return;
m_TargetBounds = new Bounds();
// Initialize
var firstRotatedBoundsCorners = GetAABBCorners(m_CulledRenderers[0].bounds, m_Rotation);
m_TargetBounds.min = firstRotatedBoundsCorners[0];
m_TargetBounds.max = firstRotatedBoundsCorners[0];
foreach (var point in firstRotatedBoundsCorners)
m_TargetBounds.Encapsulate(point);
for (int i = 1; i < m_CulledRenderers.Count; i++)
{
foreach (var point in GetAABBCorners(m_CulledRenderers[i].bounds, m_Rotation))
m_TargetBounds.Encapsulate(point);
}
// 1. Calculate position to cover all renderers
var dir = m_Rotation * Vector3.forward;
var targetBoundsExtents = m_TargetBounds.extents;
var dest = m_TargetBounds.center;
var maxXY = Mathf.Max(targetBoundsExtents.x, targetBoundsExtents.y);
var distance = Mathf.Max(maxXY, targetBoundsExtents.z) + maxXY * 2.0f;
var offset = -dir * distance;
m_Position = dest + offset;
#if UNITY_EDITOR
if (PotaToon.guideWarningEnabled && (m_TargetBounds.size.y < 0.2f || m_TargetBounds.size.y > 3.0f))
{
Debug.LogWarning("<color=#FFDD80>[PotaToon] It looks like the size of the characters is either too small or too big. We recommend that the character's height is greater than 0.5 meters and less than 3 meters (Unity units). Check the Scale Factor in the FBX import settings, the Transform Scale, or the Bounds Size of the Mesh Renderers.\nTo disable this warning, click 'Toolbar/PotaToon/Toggle Debug Warning'. </color>");
}
#endif
// 2. If number of active character has changed, update the view matrix immediately.
if (!Application.isPlaying || PotaToonCharacter.activeCharacters != m_PrevActiveCharacters || PotaToonCharacter.activeCharacters == 1)
{
m_PrevActiveCharacters = PotaToonCharacter.activeCharacters;
ResetTweenVariables(m_Position, m_Rotation);
return;
}
// 3. Otherwise, soft lerp
const float positionThreshold = 1.0f;
const float rotationThreshold = 5.0f;
if (m_IsTweening)
{
bool suddenMovement = Vector3.Distance(m_TweenDestPosition, m_Position) > positionThreshold || Quaternion.Angle(m_TweenDestRotation, m_Rotation) > rotationThreshold;
if (suddenMovement)
ResetTweenVariables(currPosition, currRotation);
}
if (!m_IsTweening)
{
m_TweenDestPosition = m_Position;
m_TweenDestRotation = m_Rotation;
m_Position = m_PrevPosition;
m_Rotation = m_PrevRotation;
m_TweenElapsedFrame = 0;
m_IsTweening = true;
}
TweenTransformIfNeeded();
if (m_TweenElapsedFrame >= k_TweenTargetFrame)
ResetTweenVariables(m_Position, m_Rotation);
}
private void ResetTweenVariables(Vector3 prevPosition, Quaternion prevRotation)
{
m_TweenElapsedFrame = 0;
m_IsTweening = false;
m_PrevPosition = prevPosition;
m_PrevRotation = prevRotation;
}
private void TweenTransformIfNeeded()
{
if (m_IsTweening && m_TweenElapsedFrame < k_TweenTargetFrame)
{
var t = Mathf.Clamp01(m_TweenElapsedFrame * k_RcpTweenTargetFrame);
m_Position = Vector3.Lerp(m_PrevPosition, m_TweenDestPosition, t);
m_Rotation = Quaternion.Lerp(m_PrevRotation, m_TweenDestRotation, t);
m_TweenElapsedFrame++;
}
}
private Vector3[] GetAABBCorners(Bounds aabb, Quaternion rotation)
{
var corners = m_BoundsCorners;
Vector3 extents = aabb.extents;
corners[0] = new Vector3(extents.x, extents.y, extents.z);
corners[1] = new Vector3(extents.x, extents.y, -extents.z);
corners[2] = new Vector3(extents.x, -extents.y, extents.z);
corners[3] = new Vector3(extents.x, -extents.y, -extents.z);
corners[4] = new Vector3(-extents.x, extents.y, extents.z);
corners[5] = new Vector3(-extents.x, extents.y, -extents.z);
corners[6] = new Vector3(-extents.x, -extents.y, extents.z);
corners[7] = new Vector3(-extents.x, -extents.y, -extents.z);
for (int i = 0; i < 8; i++)
{
corners[i] = rotation * corners[i] + aabb.center;
}
Vector3 min = corners[0];
Vector3 max = corners[0];
for (int i = 1; i < 8; i++)
{
min = Vector3.Min(min, corners[i]);
max = Vector3.Max(max, corners[i]);
}
return corners;
}
// Frustum Culling + Distance Culling
private bool IntersectTest(Renderer renderer, Camera camera, out float dist)
{
var bounds = renderer.bounds;
var cameraPosition = camera.transform.position;
dist = Vector3.Distance(cameraPosition, bounds.center);
if (dist > m_FarClipPlane)
return false;
var originalFov = camera.fieldOfView;
camera.fieldOfView = Mathf.Min(179f, originalFov * 1.2f);
var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(camera);
camera.fieldOfView = originalFov;
var intersected = GeometryUtility.TestPlanesAABB(frustumPlanes, bounds);
return intersected;
}
}
internal static bool IfCharShadowUpdateNeeded(in RenderingData renderingData, float cullingDistance)
{
shadowCamera.Prepare(renderingData.cameraData.camera, cullingDistance);
return isCharShadowValid;
}
internal static float GetCullingDistance(Camera camera, float cullingDistance)
{
// [Max FOV / current FOV(1-e5 to 179)]
var dist = 179f / camera.fieldOfView;
// Set the screen rim range to max.
var maxDistance = 4.0f * dist;
shadowCamera.maxScreenRimDistance = maxDistance;
// Always set to max if there's only one active character.
if (PotaToonCharacter.activeCharacters <= 1)
return maxDistance;
return 2.0f * cullingDistance * dist;
}
private static void GetBrightestLightData_Internal(ref NativeArray<VisibleLight> visibleLights, int mainLightIndex, VirtualShadowCamera shadowCamera, bool useBrighestLight, LayerMask followLightLayer, ref BrightestLightData data)
{
data.isMainLight = true;
s_SpotLightIndices[0] = s_SpotLightIndices[1] = -1;
var spotLightIndices = s_SpotLightIndices;
var jobSucceed = CalculateMostIntensiveLightIndices(ref visibleLights, mainLightIndex, followLightLayer, spotLightIndices);
var overridenLightDirection = Quaternion.Euler(shadowCamera.lightDirectionOffset) * Vector3.back;
if (!useBrighestLight || !jobSucceed || (spotLightIndices[0] < 0 && spotLightIndices[1] < 0) || shadowCamera.closestRenderer == null)
{
if (mainLightIndex != -1)
{
shadowCamera.UpdateCameraTransform(visibleLights[mainLightIndex].light);
data.lightDirection = shadowCamera.overrideLightDirection ? overridenLightDirection : -(Quaternion.Euler(shadowCamera.lightDirectionOffset) * visibleLights[mainLightIndex].light.transform.forward);
}
else
{
data.lightDirection = s_DefaultLightDirection;
}
data.lightIndex = mainLightIndex;
return;
}
var lightCount = visibleLights.Length;
var lightOffset = 0;
while (lightOffset < lightCount && visibleLights[lightOffset].lightType == LightType.Directional)
{
lightOffset++;
}
var hasMainLight = 0;
float mainLightStrength = 0f;
int brightestLightIndex = -1;
int brightestSpotLightIndex = -1;
var brightestLightDirection = s_DefaultLightDirection;
// Find stronger light among mainLight & brightest spot light
if (mainLightIndex != -1 && lightOffset != 0)
{
hasMainLight = 1;
brightestSpotLightIndex = spotLightIndices[0] + hasMainLight;
brightestLightIndex = mainLightIndex;
var mainLightColor = visibleLights[mainLightIndex].finalColor;
mainLightStrength = mainLightColor.r * 0.299f + mainLightColor.g * 0.587f + mainLightColor.b * 0.114f;
}
else
{
if (spotLightIndices[0] >= 0)
{
brightestLightIndex = brightestSpotLightIndex = spotLightIndices[0] + hasMainLight;
}
else
{
brightestLightIndex = brightestSpotLightIndex = spotLightIndices[1] + hasMainLight;
}
}
// Replace with the brightest spot light
if (brightestSpotLightIndex >= 0)
{
var spotLight = visibleLights[brightestSpotLightIndex].light;
var target = PotaToonCharacter.headFromActiveRenderers[shadowCamera.closestRenderer];
var dest = target != null ? target.position : shadowCamera.closestRenderer.bounds.center;
var distance = (dest - spotLight.transform.position).magnitude;
var atten = 1f - distance / spotLight.range;
var brightestSpotLightColor = visibleLights[brightestSpotLightIndex].finalColor;
var brightestSpotLightStrength = (brightestSpotLightColor.r * 0.299f + brightestSpotLightColor.g * 0.587f + brightestSpotLightColor.b * 0.114f) * atten * Mathf.Cos(spotLight.spotAngle * Mathf.Deg2Rad);
// Mainlight weight = 10
if (hasMainLight == 1 && mainLightStrength * 10f >= brightestSpotLightStrength)
{
brightestLightIndex = mainLightIndex;
}
else
{
brightestLightIndex = brightestSpotLightIndex;
data.isMainLight = false;
}
}
// Update Light Camera transform (Main or Brighest light)
if (brightestLightIndex >= 0 && brightestLightIndex < visibleLights.Length)
{
shadowCamera.UpdateCameraTransform(visibleLights[brightestLightIndex].light);
brightestLightDirection = shadowCamera.overrideLightDirection ? overridenLightDirection : -(Quaternion.Euler(shadowCamera.lightDirectionOffset) * visibleLights[brightestLightIndex].light.transform.forward);
}
data.lightDirection = brightestLightDirection;
data.lightIndex = brightestSpotLightIndex >= 0 ? brightestSpotLightIndex - hasMainLight : -1;
}
///<returns>
/// [0]: FollowLight, [1]: Additional SpotLight
///</returns>
private static bool CalculateMostIntensiveLightIndices(ref NativeArray<VisibleLight> visibleLights, int mainLightIndex, LayerMask followLayer, int[] charSpotLightIndices)
{
if (!isCharShadowValid || shadowCamera.closestRenderer == null)
{
return false;
}
var lightCount = visibleLights.Length;
var lightOffset = 0;
while (lightOffset < lightCount && visibleLights[lightOffset].lightType == LightType.Directional)
{
lightOffset++;
}
lightCount -= lightOffset;
var directionalLightCount = lightOffset;
if (mainLightIndex != -1 && directionalLightCount != 0) directionalLightCount -= 1;
var subVisibleLights = visibleLights.GetSubArray(lightOffset, lightCount);
s_vSpotLights.Clear();
s_vSpotLightIndices.Clear();
s_SortedSpotLights.Clear();
// Extract spot lights
for (int i = 0; i < subVisibleLights.Length; i++)
{
if (subVisibleLights[i].lightType == LightType.Spot)
{
s_vSpotLightIndices.Add(i + directionalLightCount);
s_vSpotLights.Add(subVisibleLights[i]);
}
}
// Calculate light intensity
for (int i = 0; i < s_vSpotLights.Count; i++)
{
var light = s_vSpotLights[i].light;
var target = PotaToonCharacter.headFromActiveRenderers[shadowCamera.closestRenderer];
var dest = target != null ? target.position : shadowCamera.closestRenderer.bounds.center;
var diff = dest - light.transform.position;
var dirToTarget = Vector3.Normalize(diff);
var L = light.transform.rotation * Vector3.forward;
var dotL = Vector3.Dot(dirToTarget, L);
var distance = diff.magnitude;
var cos = Mathf.Cos(light.spotAngle * 0.5f * Mathf.Deg2Rad);
if (dotL <= cos || distance > light.range)
{
continue;
}
var finalColor = s_vSpotLights[i].finalColor;
var atten = 1f - distance / light.range;
var strength = (finalColor.r * 0.229f + finalColor.g * 0.587f + finalColor.b * 0.114f) * atten * cos;
if (strength > 0.01f)
{
s_SortedSpotLights.Add(new KeyValuePair<float, int>(strength, s_vSpotLightIndices[i]));
}
}
// Sort
s_SortedSpotLights.Sort((x, y) => y.Key.CompareTo(x.Key));
for (int i = 0; i < s_SortedSpotLights.Count; i++)
{
var curr = s_SortedSpotLights[i].Value;
if (curr < subVisibleLights.Length && (followLayer.value & (int)Mathf.Pow(2, subVisibleLights[curr].light.gameObject.layer)) > 0)
{
if (charSpotLightIndices[0] < 0)
charSpotLightIndices[0] = curr;
}
else
{
if (charSpotLightIndices[1] < 0)
charSpotLightIndices[1] = curr;
}
}
return true;
}
/// <summary>
/// 1. if (useBrighestLightOnly == true) : LightIndex = spot or mainLight.
/// 2. if (useBrighestLightOnly == false) : LightIndex = mainLight
/// </summary>
internal static void GetBrightestLightData(ref RenderingData renderingData, bool useBrightestLight, LayerMask followLightLayer, out BrightestLightData data)
{
data = new BrightestLightData();
GetBrightestLightData_Internal(ref renderingData.lightData.visibleLights, renderingData.lightData.mainLightIndex, shadowCamera, useBrightestLight, followLightLayer, ref data);
}
#if UNITY_6000_0_OR_NEWER
#region RenderGraph
/// <summary>
/// 1. if (useBrighestLightOnly == true) : LightIndex = spot or mainLight.
/// 2. if (useBrighestLightOnly == false) : LightIndex = mainLight
/// </summary>
internal static void GetBrightestLightData(UniversalLightData lightData, bool useBrightestLight, LayerMask followLightLayer, out BrightestLightData data)
{
data = new BrightestLightData();
GetBrightestLightData_Internal(ref lightData.visibleLights, lightData.mainLightIndex, shadowCamera, useBrightestLight, followLightLayer, ref data);
}
#endregion
#endif
}
}