464 lines
19 KiB
C#
464 lines
19 KiB
C#
//------------------------------------------------------------------------------------------------------------------
|
|
// Volumetric Lights
|
|
// Created by Kronnect
|
|
//------------------------------------------------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
using UnityEngine.Rendering.Universal;
|
|
|
|
namespace VolumetricLights {
|
|
|
|
public partial class VolumetricLight : MonoBehaviour {
|
|
|
|
#region Shadow support
|
|
|
|
const string SHADOW_CAM_NAME = "OcclusionCam";
|
|
|
|
Camera cam;
|
|
RenderTexture rt;
|
|
int camStartFrameCount;
|
|
Vector3 lastCamPos;
|
|
Quaternion lastCamRot;
|
|
bool usesReversedZBuffer;
|
|
static Matrix4x4 textureScaleAndBias;
|
|
Matrix4x4 shadowMatrix;
|
|
bool camTransformChanged;
|
|
bool shouldOrientToCamera;
|
|
RenderTexture shadowCubemap;
|
|
readonly static Vector3[] camFaceDirections = { Vector3.right, Vector3.left, Vector3.up, Vector3.down, Vector3.forward, Vector3.back };
|
|
Material copyDepthIntoCubemap;
|
|
int currentCubemapFace;
|
|
|
|
[NonSerialized]
|
|
public RenderTexture translucentMap;
|
|
[NonSerialized]
|
|
public RTHandle translucencyMapHandle;
|
|
|
|
public bool usesCubemap { get { return shadowBakeMode != ShadowBakeMode.HalfSphere && generatedType == LightType.Point; } }
|
|
bool usesTranslucency { get { return shadowTranslucency && (generatedType == LightType.Spot || generatedType == LightType.Rectangle || generatedType == LightType.Disc); } }
|
|
|
|
void CheckShadows () { // called during initialization to grab a reference of existing cam
|
|
if (cam == null) {
|
|
Transform childCam = transform.Find(SHADOW_CAM_NAME);
|
|
if (childCam != null) {
|
|
cam = childCam.GetComponent<Camera>();
|
|
if (cam == null) {
|
|
// corrupted cam object?
|
|
DestroyImmediate(childCam.gameObject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ShadowsDisable () { // called from OnDisable
|
|
if (cam != null) {
|
|
cam.enabled = false;
|
|
}
|
|
}
|
|
|
|
void ShadowsDispose () { // called from OnDestroy
|
|
if (cam != null) {
|
|
cam.targetTexture = null;
|
|
cam.enabled = false;
|
|
}
|
|
DisposeRTs();
|
|
}
|
|
|
|
void DisposeRTs () {
|
|
if (rt != null) {
|
|
rt.Release();
|
|
DestroyImmediate(rt);
|
|
}
|
|
if (shadowCubemap != null) {
|
|
shadowCubemap.Release();
|
|
DestroyImmediate(shadowCubemap);
|
|
}
|
|
if (translucentMap != null) {
|
|
translucentMap.Release();
|
|
DestroyImmediate(translucentMap);
|
|
}
|
|
}
|
|
|
|
void ShadowsSupportCheck () { // called from UpdateMaterials
|
|
|
|
bool usesCookie = cookieTexture != null && lightComp.type == LightType.Spot;
|
|
if (!enableShadows && !usesCookie) {
|
|
ShadowsDispose();
|
|
return;
|
|
}
|
|
|
|
usesReversedZBuffer = SystemInfo.usesReversedZBuffer;
|
|
|
|
// Setup texture scale and bias matrix
|
|
textureScaleAndBias = Matrix4x4.identity;
|
|
textureScaleAndBias.m00 = 0.5f;
|
|
textureScaleAndBias.m11 = 0.5f;
|
|
textureScaleAndBias.m22 = 0.5f;
|
|
textureScaleAndBias.m03 = 0.5f;
|
|
textureScaleAndBias.m13 = 0.5f;
|
|
textureScaleAndBias.m23 = 0.5f;
|
|
|
|
if (cam == null) {
|
|
Transform childCam = transform.Find(SHADOW_CAM_NAME);
|
|
if (childCam != null) {
|
|
cam = childCam.GetComponent<Camera>();
|
|
if (cam == null) {
|
|
DestroyImmediate(childCam.gameObject);
|
|
}
|
|
}
|
|
if (cam == null) {
|
|
GameObject camObj = new GameObject(SHADOW_CAM_NAME, typeof(Camera));
|
|
camObj.transform.SetParent(transform, false);
|
|
cam = camObj.GetComponent<Camera>();
|
|
cam.depthTextureMode = DepthTextureMode.None;
|
|
cam.clearFlags = CameraClearFlags.Depth;
|
|
cam.allowHDR = false;
|
|
cam.allowMSAA = false;
|
|
}
|
|
}
|
|
|
|
UniversalAdditionalCameraData camData = cam.GetComponent<UniversalAdditionalCameraData>();
|
|
if (camData == null) {
|
|
camData = cam.gameObject.AddComponent<UniversalAdditionalCameraData>();
|
|
}
|
|
if (camData != null) {
|
|
camData.dithering = false;
|
|
camData.renderPostProcessing = false;
|
|
camData.renderShadows = false;
|
|
camData.requiresColorTexture = false;
|
|
camData.requiresDepthTexture = false;
|
|
camData.stopNaN = false;
|
|
camData.volumeLayerMask = 0;
|
|
camData.allowXRRendering = false;
|
|
#if UNITY_2021_3_OR_NEWER
|
|
CheckAndAssignDepthRenderer(camData);
|
|
#endif
|
|
}
|
|
|
|
cam.depth = -100;
|
|
cam.nearClipPlane = shadowNearDistance;
|
|
cam.orthographicSize = Mathf.Max(generatedAreaWidth, generatedAreaHeight);
|
|
|
|
// custom properties per light type
|
|
switch (generatedType) {
|
|
case LightType.Spot:
|
|
cam.transform.localRotation = Quaternion.identity;
|
|
cam.orthographic = false;
|
|
cam.fieldOfView = generatedSpotAngle;
|
|
break;
|
|
|
|
case LightType.Point:
|
|
cam.orthographic = false;
|
|
if (shadowBakeMode != ShadowBakeMode.HalfSphere) {
|
|
cam.fieldOfView = 90f;
|
|
} else {
|
|
cam.fieldOfView = 160f;
|
|
}
|
|
break;
|
|
|
|
case LightType.Rectangle:
|
|
case LightType.Disc:
|
|
cam.transform.localRotation = Quaternion.identity;
|
|
cam.orthographic = true;
|
|
cam.orthographicSize *= generatedAreaFrustumMultiplier;
|
|
break;
|
|
}
|
|
|
|
|
|
if (rt != null && rt.width != (int)shadowResolution) {
|
|
if (cam.targetTexture == rt) {
|
|
cam.targetTexture = null;
|
|
}
|
|
DisposeRTs();
|
|
}
|
|
|
|
if (rt == null) {
|
|
rt = new RenderTexture((int)shadowResolution, (int)shadowResolution, 16, RenderTextureFormat.Depth, RenderTextureReadWrite.Linear);
|
|
rt.antiAliasing = 1;
|
|
rt.useMipMap = false;
|
|
rt.filterMode = FilterMode.Bilinear;
|
|
rt.wrapMode = TextureWrapMode.Clamp;
|
|
}
|
|
|
|
if (usesTranslucency && translucentMap == null) {
|
|
translucentMap = new RenderTexture((int)shadowResolution, (int)shadowResolution, 0, RenderTextureFormat.ARGBHalf);
|
|
translucentMap.antiAliasing = 1;
|
|
translucentMap.useMipMap = false;
|
|
translucentMap.wrapMode = TextureWrapMode.Clamp;
|
|
}
|
|
|
|
if (usesCubemap && shadowCubemap == null) {
|
|
shadowCubemap = new RenderTexture((int)shadowResolution, (int)shadowResolution, 0, RenderTextureFormat.RHalf, RenderTextureReadWrite.Linear);
|
|
shadowCubemap.dimension = TextureDimension.Cube;
|
|
shadowCubemap.antiAliasing = 1;
|
|
shadowCubemap.useMipMap = false;
|
|
}
|
|
|
|
fogMat.SetVector(ShaderParams.ShadowIntensity, new Vector4(shadowIntensity, 1f - shadowIntensity, 0, 0));
|
|
fogMat.SetVector(ShaderParams.ShadowColor, shadowColor);
|
|
|
|
if ((shadowCullingMask & 2) != 0) {
|
|
shadowCullingMask &= ~2; // exclude transparent FX layer
|
|
}
|
|
|
|
cam.cullingMask = shadowCullingMask;
|
|
cam.targetTexture = rt;
|
|
|
|
if (enableShadows) {
|
|
shouldOrientToCamera = true;
|
|
ScheduleShadowCapture();
|
|
} else {
|
|
cam.enabled = false;
|
|
}
|
|
}
|
|
|
|
|
|
public bool CheckCompatiblePipelineArchitecture () {
|
|
UniversalRenderPipelineAsset pipe = (UniversalRenderPipelineAsset)GraphicsSettings.currentRenderPipeline;
|
|
if (pipe == null) return false;
|
|
|
|
// Skip custom depth renderer for Forward+/Deferred+ rendering path as it's incompatible
|
|
bool isCompatible = true;
|
|
#if UNITY_2022_2_OR_NEWER
|
|
if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Direct3D12)
|
|
{
|
|
for (int i = 0; i < pipe.m_RendererDataList.Length; i++)
|
|
{
|
|
UniversalRendererData rendererData = pipe.m_RendererDataList[i] as UniversalRendererData;
|
|
if (rendererData != null)
|
|
{
|
|
if (rendererData.renderingMode == RenderingMode.ForwardPlus)
|
|
{
|
|
isCompatible = false;
|
|
break;
|
|
}
|
|
#if UNITY_6000_1_OR_NEWER
|
|
else if (rendererData.renderingMode == RenderingMode.DeferredPlus)
|
|
{
|
|
isCompatible = false;
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
return isCompatible;
|
|
}
|
|
|
|
|
|
#if UNITY_2021_3_OR_NEWER
|
|
UniversalRendererData depthRendererData;
|
|
void CheckAndAssignDepthRenderer (UniversalAdditionalCameraData camData) {
|
|
UniversalRenderPipelineAsset pipe = (UniversalRenderPipelineAsset)GraphicsSettings.currentRenderPipeline;
|
|
if (pipe == null) return;
|
|
|
|
// Skip custom depth renderer for Forward+/Deferred+ on DX12 rendering path as it's incompatible
|
|
bool skipCustomRenderer = !CheckCompatiblePipelineArchitecture();
|
|
if (skipCustomRenderer) {
|
|
// Use default renderer for Forward+ to avoid shadow issues
|
|
camData.SetRenderer(-1); // -1 uses the default renderer
|
|
return;
|
|
}
|
|
|
|
if (depthRendererData == null) {
|
|
depthRendererData = Resources.Load<UniversalRendererData>("Shaders/VolumetricLightsDepthRenderer");
|
|
if (depthRendererData == null) {
|
|
Debug.LogError("Volumetric Lights Depth Renderer asset not found.");
|
|
return;
|
|
}
|
|
depthRendererData.postProcessData = null;
|
|
}
|
|
int depthRendererIndex = -1;
|
|
for (int k = 0; k < pipe.m_RendererDataList.Length; k++) {
|
|
if (pipe.m_RendererDataList[k] == depthRendererData) {
|
|
depthRendererIndex = k;
|
|
break;
|
|
}
|
|
}
|
|
if (depthRendererIndex < 0) {
|
|
depthRendererIndex = pipe.m_RendererDataList.Length;
|
|
System.Array.Resize<ScriptableRendererData>(ref pipe.m_RendererDataList, depthRendererIndex + 1);
|
|
pipe.m_RendererDataList[depthRendererIndex] = depthRendererData;
|
|
#if UNITY_EDITOR
|
|
UnityEditor.EditorUtility.SetDirty(pipe);
|
|
#endif
|
|
}
|
|
camData.SetRenderer(depthRendererIndex);
|
|
}
|
|
#endif
|
|
/// <summary>
|
|
/// Updates shadows on this volumetric light
|
|
/// </summary>
|
|
public void ScheduleShadowCapture () {
|
|
if (cam == null) return;
|
|
|
|
if (usesCubemap) {
|
|
if (copyDepthIntoCubemap == null) {
|
|
copyDepthIntoCubemap = new Material(Shader.Find("Hidden/VolumetricLights/CopyDepthIntoCubemap"));
|
|
}
|
|
copyDepthIntoCubemap.SetVector(ShaderParams.LightPos, cam.transform.position);
|
|
RenderTexture active = RenderTexture.active;
|
|
|
|
int renderFaceCount = shadowBakeMode == ShadowBakeMode.CubemapOneFacePerFrame && shadowBakeInterval == ShadowBakeInterval.EveryFrame ? 1 : 6;
|
|
for (int k = 0; k < renderFaceCount; k++) {
|
|
int cubemapFace = currentCubemapFace % 6;
|
|
cam.transform.forward = camFaceDirections[cubemapFace];
|
|
cam.Render();
|
|
copyDepthIntoCubemap.SetMatrix(ShaderParams.InvVPMatrix, cam.cameraToWorldMatrix * GL.GetGPUProjectionMatrix(cam.projectionMatrix, false).inverse);
|
|
copyDepthIntoCubemap.SetTexture(ShaderParams.ShadowTexture, rt, RenderTextureSubElement.Depth);
|
|
Graphics.SetRenderTarget(shadowCubemap, 0, (CubemapFace)cubemapFace);
|
|
Graphics.Blit(rt, copyDepthIntoCubemap);
|
|
|
|
currentCubemapFace++;
|
|
}
|
|
cam.enabled = false;
|
|
RenderTexture.active = active;
|
|
|
|
fogMat.SetTexture(ShaderParams.ShadowCubemap, shadowCubemap);
|
|
if (enableDustParticles && particleMaterial != null) {
|
|
particleMaterial.SetTexture(ShaderParams.ShadowCubemap, shadowCubemap);
|
|
}
|
|
if (!fogMat.IsKeywordEnabled(ShaderParams.SKW_SHADOWS_CUBEMAP)) {
|
|
fogMat.EnableKeyword(ShaderParams.SKW_SHADOWS_CUBEMAP);
|
|
}
|
|
} else {
|
|
cam.enabled = true;
|
|
camStartFrameCount = Time.frameCount;
|
|
string shadowKeyword = usesTranslucency ? ShaderParams.SKW_SHADOWS_TRANSLUCENCY : ShaderParams.SKW_SHADOWS;
|
|
if (!fogMat.IsKeywordEnabled(shadowKeyword)) {
|
|
fogMat.EnableKeyword(shadowKeyword);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetupShadowMatrix () {
|
|
|
|
if (usesCubemap) return;
|
|
|
|
ComputeShadowTransform(cam.projectionMatrix, cam.worldToCameraMatrix);
|
|
|
|
fogMat.SetMatrix(ShaderParams.ShadowMatrix, shadowMatrix);
|
|
fogMat.SetTexture(ShaderParams.ShadowTexture, cam.targetTexture, RenderTextureSubElement.Depth);
|
|
fogMat.SetTexture(ShaderParams.TranslucencyTexture, translucentMap);
|
|
|
|
if (enableDustParticles && particleMaterial != null) {
|
|
particleMaterial.SetMatrix(ShaderParams.ShadowMatrix, shadowMatrix);
|
|
particleMaterial.SetTexture(ShaderParams.ShadowTexture, cam.targetTexture, RenderTextureSubElement.Depth);
|
|
particleMaterial.SetVector(ShaderParams.ShadowIntensity, new Vector4(shadowIntensity, 1f - shadowIntensity, 0, 0));
|
|
particleMaterial.SetTexture(ShaderParams.TranslucencyTexture, translucentMap);
|
|
}
|
|
}
|
|
|
|
|
|
void ShadowsUpdate () { // called from Update
|
|
|
|
bool usesCookie = cookieTexture != null && lightComp.type == LightType.Spot;
|
|
if (!enableShadows && !usesCookie) return;
|
|
|
|
if (cam == null) return;
|
|
|
|
int frameCount = Time.frameCount;
|
|
if (!meshRenderer.isVisible && frameCount - camStartFrameCount > 5) {
|
|
if (cam.enabled) {
|
|
ShadowsDisable();
|
|
}
|
|
return;
|
|
}
|
|
|
|
Transform camTransform = cam.transform;
|
|
cam.farClipPlane = generatedRange;
|
|
if (generatedType == LightType.Point) {
|
|
if (shadowBakeMode != ShadowBakeMode.HalfSphere) {
|
|
} else if (shadowOrientation == ShadowOrientation.ToCamera) {
|
|
if (enableShadows && mainCamera != null) {
|
|
// if it's a point light, check if the orientation is target camera and if the angle has changed too much force a shadow update
|
|
if (shadowBakeInterval != ShadowBakeInterval.EveryFrame) {
|
|
if (Vector3.Angle(camTransform.forward, mainCamera.position - lastCamPos) > 45) {
|
|
shouldOrientToCamera = true;
|
|
ScheduleShadowCapture();
|
|
}
|
|
}
|
|
if (shouldOrientToCamera || shadowBakeInterval == ShadowBakeInterval.EveryFrame) {
|
|
shouldOrientToCamera = false;
|
|
camTransform.LookAt(mainCamera.position);
|
|
}
|
|
}
|
|
} else {
|
|
camTransform.forward = shadowDirection;
|
|
}
|
|
}
|
|
|
|
camTransformChanged = false;
|
|
bool rotationChanged = lastCamRot != camTransform.rotation;
|
|
if (lastCamPos != camTransform.position || (rotationChanged && !shadowBakeIgnoreRotationChange)) {
|
|
camTransformChanged = true;
|
|
lastCamPos = camTransform.position;
|
|
lastCamRot = camTransform.rotation;
|
|
}
|
|
|
|
if (enableShadows) {
|
|
ShadowCamUpdate();
|
|
}
|
|
|
|
if (camTransformChanged || rotationChanged || usesCookie || cam.enabled) {
|
|
SetupShadowMatrix();
|
|
}
|
|
}
|
|
|
|
void ShadowCamUpdate () {
|
|
if (shadowAutoToggle) {
|
|
float maxDistSqr = shadowDistanceDeactivation * shadowDistanceDeactivation;
|
|
if (distanceToCameraSqr > maxDistSqr) {
|
|
if (cam.enabled) {
|
|
ShadowsDisable();
|
|
if (fogMat.IsKeywordEnabled(ShaderParams.SKW_SHADOWS)) {
|
|
fogMat.DisableKeyword(ShaderParams.SKW_SHADOWS);
|
|
}
|
|
if (fogMat.IsKeywordEnabled(ShaderParams.SKW_SHADOWS_TRANSLUCENCY)) {
|
|
fogMat.DisableKeyword(ShaderParams.SKW_SHADOWS_TRANSLUCENCY);
|
|
}
|
|
if (fogMat.IsKeywordEnabled(ShaderParams.SKW_SHADOWS_CUBEMAP)) {
|
|
fogMat.DisableKeyword(ShaderParams.SKW_SHADOWS_CUBEMAP);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (shadowBakeInterval == ShadowBakeInterval.OnStart) {
|
|
if (!cam.enabled && camTransformChanged) {
|
|
ScheduleShadowCapture();
|
|
} else if (Time.frameCount > camStartFrameCount + 1) {
|
|
cam.enabled = false;
|
|
}
|
|
} else if (!cam.enabled) {
|
|
ScheduleShadowCapture();
|
|
}
|
|
}
|
|
|
|
void ComputeShadowTransform (Matrix4x4 proj, Matrix4x4 view) {
|
|
// Currently CullResults ComputeDirectionalShadowMatricesAndCullingPrimitives doesn't
|
|
// apply z reversal to projection matrix. We need to do it manually here.
|
|
if (usesReversedZBuffer) {
|
|
proj.m20 = -proj.m20;
|
|
proj.m21 = -proj.m21;
|
|
proj.m22 = -proj.m22;
|
|
proj.m23 = -proj.m23;
|
|
}
|
|
|
|
Matrix4x4 worldToShadow = proj * view;
|
|
|
|
// Apply texture scale and offset to save a MAD in shader.
|
|
shadowMatrix = textureScaleAndBias * worldToShadow;
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
|
|
}
|