537 lines
21 KiB
C#

//------------------------------------------------------------------------------------------------------------------
// Volumetric Lights
// Created by Kronnect
//------------------------------------------------------------------------------------------------------------------
using System;
using UnityEngine;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif
namespace VolumetricLights {
public delegate void PropertiesChangedEvent(VolumetricLight volumetricLight);
[ExecuteAlways, RequireComponent(typeof(Light)), AddComponentMenu("Effects/Volumetric Light", 1000)]
[HelpURL("https://kronnect.com/guides/volumetric-lights-2-urp-volumetric-light-parameters/")]
[DefaultExecutionOrder(100)]
public partial class VolumetricLight : MonoBehaviour {
// Events
public event PropertiesChangedEvent OnPropertiesChanged;
// Common
public bool profileSync = true;
public bool useCustomBounds;
public Bounds bounds;
[Tooltip("In enabled, bounds coordinates are relative to the light position")]
public bool boundsInLocalSpace;
public VolumetricLightProfile profile;
public float customRange = 1f;
[Tooltip("Used for point light occlusion orientation and checking camera distance when autoToggle options are enabled. If not assigned, it will try to use the main camera.")]
public Transform targetCamera;
// Area
public bool useCustomSize;
public float areaWidth = 1f, areaHeight = 1f;
[NonSerialized]
public Light lightComp;
const float GOLDEN_RATIO = 0.618033989f;
MeshFilter mf;
[NonSerialized]
public MeshRenderer meshRenderer;
Material fogMat, fogMatLight, fogMatInvisible;
Shader fogMatShader, fogMatInvisibleShader;
Vector4 windDirectionAcum;
bool requireUpdateMaterial;
List<string> keywords;
static Texture2D blueNoiseTex;
float distanceToCameraSqr;
[NonSerialized]
public static Transform mainCamera;
float lastDistanceCheckTime;
#if UNITY_EDITOR
float lastMaterialUpdateTime;
#endif
enum Tribool {
Unknown = 0,
True = 1,
False = -1
}
Tribool wasInRange = Tribool.Unknown;
public static List<VolumetricLight> volumetricLights = new List<VolumetricLight>();
void OnEnable() {
Init();
#if UNITY_EDITOR
// workaround for volumetric effect disappearing when saving the scene
EditorSceneManager.sceneSaving += OnSceneSaving;
#endif
}
void OnSceneSaving (UnityEngine.SceneManagement.Scene scene, string path) {
requireUpdateMaterial = true;
}
public void Init() {
fogMatShader = Shader.Find("VolumetricLights/VolumetricLightURP");
fogMatInvisibleShader = Shader.Find("VolumetricLights/Invisible");
if (!volumetricLights.Contains(this)) {
volumetricLights.Add(this);
}
lightComp = GetComponent<Light>();
if (gameObject.layer == 0) { // if object is in default layer, move it to transparent fx layer
gameObject.layer = 1;
}
SettingsInit();
Refresh();
}
public void Refresh() {
if (!enabled) return;
CheckProfile();
generatedRange = generatedTipRadius = generatedSpotAngle = generatedBaseRadius = -1;
generatedAreaWidth = generatedAreaHeight = generatedAreaFrustumAngle = generatedAreaFrustumMultiplier = 0;
CheckMesh();
CheckShadows();
UpdateMaterialPropertiesNow();
}
private void OnValidate() {
SettingsValidate();
requireUpdateMaterial = true;
}
public void OnDidApplyAnimationProperties() {
requireUpdateMaterial = true;
}
private void OnDisable () {
#if UNITY_EDITOR
EditorSceneManager.sceneSaving -= OnSceneSaving;
#endif
if (volumetricLights.Contains(this)) {
volumetricLights.Remove(this);
}
TurnOff();
}
void TurnOff() {
if (meshRenderer != null) {
meshRenderer.enabled = false;
}
ShadowsDisable();
ParticlesDisable();
}
private void OnDestroy() {
if (fogMatInvisible != null) {
DestroyImmediate(fogMatInvisible);
fogMatInvisible = null;
}
if (meshRenderer != null) {
meshRenderer.enabled = false;
}
ShadowsDispose();
}
void LateUpdate() {
bool isActiveAndEnabled = lightComp.isActiveAndEnabled || alwaysOn;
if (isActiveAndEnabled) {
if (!autoToggle && meshRenderer != null && !meshRenderer.enabled) {
requireUpdateMaterial = true;
}
} else {
if (meshRenderer != null && meshRenderer.enabled) {
TurnOff();
}
return;
}
if (CheckMesh()) {
if (!Application.isPlaying) {
ParticlesDisable();
}
ScheduleShadowCapture();
requireUpdateMaterial = true;
}
#if UNITY_EDITOR
// In editor, check if we need to refresh materials (matrices get lost during domain reloads)
if (!Application.isPlaying && fogMat != null && (enableShadows || enableDustParticles)) {
float currentTime = Time.realtimeSinceStartup;
// If it's been more than 0.5 seconds since last update, force a refresh
// This handles domain reloads and other Unity operations that clear matrices
if (currentTime - lastMaterialUpdateTime > 0.5f) {
requireUpdateMaterial = true;
lastMaterialUpdateTime = currentTime;
}
}
#endif
if (requireUpdateMaterial) {
requireUpdateMaterial = false;
UpdateMaterialPropertiesNow();
#if UNITY_EDITOR
lastMaterialUpdateTime = Time.realtimeSinceStartup;
#endif
}
if (fogMat == null || meshRenderer == null) return;
UpdateVolumeGeometry();
float now = Time.time;
if ((dustAutoToggle || shadowAutoToggle || autoToggle) && (!Application.isPlaying || (now - lastDistanceCheckTime) >= autoToggleCheckInterval)) {
lastDistanceCheckTime = now;
ComputeDistanceToCamera();
}
float brightness = this.brightness;
if (autoToggle) {
float maxDistSqr = distanceDeactivation * distanceDeactivation;
float minDistSqr = distanceStartDimming * distanceStartDimming;
if (minDistSqr > maxDistSqr) minDistSqr = maxDistSqr - 0.00001f;
float dim = 1f - Mathf.Clamp01((distanceToCameraSqr - minDistSqr) / (maxDistSqr - minDistSqr));
brightness *= dim;
Tribool isInRange = dim > 0.0f ? Tribool.True : Tribool.False;
if (isInRange != wasInRange) {
wasInRange = isInRange;
if (isInRange == Tribool.True && !meshRenderer.enabled) requireUpdateMaterial = true;
meshRenderer.enabled = isInRange == Tribool.True;
}
}
UpdateDiffusionTerm();
if (enableDustParticles) {
if (!Application.isPlaying) {
ParticlesResetIfTransformChanged();
}
UpdateParticlesVisibility();
}
fogMat.SetColor(ShaderParams.LightColor, lightComp.color * mediumAlbedo * (lightComp.intensity * brightness));
float deltaTime = Time.deltaTime;
windDirectionAcum.x += windDirection.x * deltaTime;
windDirectionAcum.y += windDirection.y * deltaTime;
windDirectionAcum.z += windDirection.z * deltaTime;
windDirectionAcum.w = animatedBlueNoise ? GOLDEN_RATIO * (Time.frameCount % 480) : 0;
fogMat.SetVector(ShaderParams.WindDirection, windDirectionAcum);
ShadowsUpdate();
}
void ComputeDistanceToCamera() {
if (mainCamera == null) {
mainCamera = targetCamera;
if (mainCamera == null && Camera.main != null) {
mainCamera = Camera.main.transform;
}
if (mainCamera == null) return;
}
Vector3 camPos = mainCamera.position;
Vector3 pos = bounds.center;
distanceToCameraSqr = (camPos - pos).sqrMagnitude;
}
void UpdateDiffusionTerm() {
Vector4 toLightDir = -transform.forward;
toLightDir.w = diffusionIntensity;
fogMat.SetVector(ShaderParams.ToLightDir, toLightDir);
}
public void UpdateVolumeGeometry() {
NormalizeScale();
UpdateVolumeGeometryMaterial(fogMat);
if (enableDustParticles && particleMaterial != null) {
UpdateVolumeGeometryMaterial(particleMaterial);
particleMaterial.SetMatrix(ShaderParams.WorldToLocalMatrix, transform.worldToLocalMatrix);
}
}
void UpdateVolumeGeometryMaterial(Material mat) {
if (mat == null) return;
Transform t = transform;
Vector3 pos = t.position;
Vector4 tipData;
tipData.x = pos.x;
tipData.y = pos.y;
tipData.z = pos.z;
tipData.w = tipRadius;
mat.SetVector(ShaderParams.ConeTipData, tipData);
Vector3 forward = t.forward;
Vector4 coneAxis;
coneAxis.x = forward.x * generatedRange;
coneAxis.y = forward.y * generatedRange;
coneAxis.z = forward.z * generatedRange;
float maxDistSqr = generatedRange * generatedRange;
coneAxis.w = maxDistSqr;
mat.SetVector(ShaderParams.ConeAxis, coneAxis);
float falloff = Mathf.Max(0.0001f, rangeFallOff);
float pointAttenX = -1f / (maxDistSqr * falloff);
float pointAttenY = maxDistSqr / (maxDistSqr * falloff);
mat.SetVector(ShaderParams.ExtraGeoData, new Vector4(generatedBaseRadius, pointAttenX, pointAttenY, generatedRange));
if (!useCustomBounds) {
bounds = meshRenderer.bounds;
}
Bounds adjustedBounds = bounds;
if (useCustomBounds && boundsInLocalSpace) {
adjustedBounds.center += pos;
}
mat.SetVector(ShaderParams.BoundsCenter, adjustedBounds.center);
mat.SetVector(ShaderParams.BoundsExtents, adjustedBounds.extents);
if (generatedType == LightType.Rectangle || generatedType == LightType.Disc) {
if (mf != null && mf.sharedMesh != null) {
Bounds meshBounds = mf.sharedMesh.bounds; // non transformed real bounds
mat.SetVector(ShaderParams.MeshBoundsCenter, meshBounds.center);
mat.SetVector(ShaderParams.MeshBoundsExtents, meshBounds.extents);
}
float baseMultiplierComputed = (generatedAreaFrustumMultiplier - 1f) / generatedRange;
if (generatedType == LightType.Rectangle) {
mat.SetVector(ShaderParams.AreaExtents, new Vector4(areaWidth * 0.5f, areaHeight * 0.5f, generatedRange, baseMultiplierComputed));
} else {
mat.SetVector(ShaderParams.AreaExtents, new Vector4(areaWidth * areaWidth, areaHeight, generatedRange, baseMultiplierComputed));
}
}
}
public void UpdateMaterialProperties() {
requireUpdateMaterial = true;
}
void UpdateMaterialPropertiesNow() {
wasInRange = Tribool.Unknown;
lastDistanceCheckTime = -999;
mainCamera = null;
ComputeDistanceToCamera();
if (this == null || !isActiveAndEnabled || lightComp == null || (!lightComp.isActiveAndEnabled && !alwaysOn)) {
ShadowsDisable();
return;
}
SettingsValidate();
if (meshRenderer == null) {
meshRenderer = GetComponent<MeshRenderer>();
}
if (fogMatLight == null) {
if (meshRenderer != null) {
fogMatLight = meshRenderer.sharedMaterial;
if (fogMatLight != null) {
// ensure this is the correct shader
if (fogMatLight.shader != fogMatShader && fogMatLight.shader != fogMatInvisibleShader) {
fogMatLight = null;
}
if (fogMatLight != null) {
// ensure this material is not used by other lights (can happen if user duplicates another light gameobject in scene)
foreach (VolumetricLight light in volumetricLights) {
if (light != null && light != this && light.meshRenderer != null && light.meshRenderer.sharedMaterial == fogMatLight) {
fogMatLight = null;
if (mf != null) mf.sharedMesh = null;
break;
}
}
}
}
}
if (fogMatLight == null) {
fogMatLight = new Material(fogMatShader);
}
}
fogMat = fogMatLight;
if (fogMat == null) return;
SetFogMaterial();
if (customRange < 0.001f) customRange = 0.001f;
if (meshRenderer != null) {
meshRenderer.sortingLayerID = sortingLayerID;
meshRenderer.sortingOrder = sortingOrder;
}
fogMat.renderQueue = renderQueue;
switch (blendMode) {
case BlendMode.Additive:
fogMat.SetInt(ShaderParams.BlendOp, (int)UnityEngine.Rendering.BlendOp.Add);
fogMat.SetInt(ShaderParams.BlendSrc, (int)UnityEngine.Rendering.BlendMode.One);
fogMat.SetInt(ShaderParams.BlendDest, (int)UnityEngine.Rendering.BlendMode.One);
break;
case BlendMode.Blend:
fogMat.SetInt(ShaderParams.BlendOp, (int)UnityEngine.Rendering.BlendOp.Add);
fogMat.SetInt(ShaderParams.BlendSrc, (int)UnityEngine.Rendering.BlendMode.One);
fogMat.SetInt(ShaderParams.BlendDest, (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
break;
case BlendMode.PreMultiply:
fogMat.SetInt(ShaderParams.BlendOp, (int)UnityEngine.Rendering.BlendOp.Add);
fogMat.SetInt(ShaderParams.BlendSrc, (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
fogMat.SetInt(ShaderParams.BlendDest, (int)UnityEngine.Rendering.BlendMode.One);
break;
case BlendMode.Substractive:
fogMat.SetInt(ShaderParams.BlendOp, (int)UnityEngine.Rendering.BlendOp.ReverseSubtract);
fogMat.SetInt(ShaderParams.BlendSrc, (int)UnityEngine.Rendering.BlendMode.One);
fogMat.SetInt(ShaderParams.BlendDest, (int)UnityEngine.Rendering.BlendMode.One);
break;
}
fogMat.SetTexture(ShaderParams.NoiseTex, noiseTexture);
fogMat.SetFloat(ShaderParams.NoiseStrength, noiseStrength);
fogMat.SetFloat(ShaderParams.NoiseScale, 0.1f / noiseScale);
fogMat.SetFloat(ShaderParams.NoiseFinalMultiplier, noiseFinalMultiplier);
fogMat.SetFloat(ShaderParams.Penumbra, penumbra);
fogMat.SetFloat(ShaderParams.RangeFallOff, rangeFallOff);
fogMat.SetFloat(ShaderParams.Density, density);
fogMat.SetFloat(ShaderParams.NearClipDistance, nearClipDistance);
fogMat.SetVector(ShaderParams.DirectLightData, new Vector4(directLightMultiplier, directLightSmoothSamples, directLightSmoothRadius, 0));
fogMat.SetVector(ShaderParams.FallOff, new Vector4(attenCoefConstant, attenCoefLinear, attenCoefQuadratic, 0));
fogMat.SetVector(ShaderParams.RayMarchSettings, new Vector4(raymarchQuality, dithering * 0.001f, jittering, raymarchMinStep));
fogMat.SetInt(ShaderParams.RayMarchMaxSteps, raymarchMaxSteps);
if (jittering > 0) {
if (blueNoiseTex == null) blueNoiseTex = Resources.Load<Texture2D>("Textures/blueNoiseVL128");
fogMat.SetTexture(ShaderParams.BlueNoiseTexture, blueNoiseTex);
}
fogMat.SetInt(ShaderParams.FlipDepthTexture, flipDepthTexture ? 1 : 0);
if (keywords == null) {
keywords = new List<string>();
} else {
keywords.Clear();
}
if (useBlueNoise) {
keywords.Add(ShaderParams.SKW_BLUENOISE);
}
if (useNoise) {
keywords.Add(ShaderParams.SKW_NOISE);
}
switch (lightComp.type) {
case LightType.Spot:
if (cookieTexture != null) {
keywords.Add(ShaderParams.SKW_SPOT_COOKIE);
fogMat.SetTexture(ShaderParams.CookieTexture, cookieTexture);
fogMat.SetVector(ShaderParams.CookieTexture_ScaleAndSpeed, new Vector4(cookieScale.x, cookieScale.y, cookieSpeed.x, cookieSpeed.y));
fogMat.SetVector(ShaderParams.CookieTexture_Offset, new Vector4(cookieOffset.x, cookieOffset.y, 0, 0));
} else {
keywords.Add(ShaderParams.SKW_SPOT);
}
break;
case LightType.Point: keywords.Add(ShaderParams.SKW_POINT); break;
case LightType.Rectangle: keywords.Add(ShaderParams.SKW_AREA_RECT); break;
case LightType.Disc: keywords.Add(ShaderParams.SKW_AREA_DISC); break;
}
if (attenuationMode == AttenuationMode.Quadratic) {
keywords.Add(ShaderParams.SKW_PHYSICAL_ATTEN);
}
if (castDirectLight) {
keywords.Add(directLightBlendMode == DirectLightBlendMode.Additive ? ShaderParams.SKW_CAST_DIRECT_LIGHT_ADDITIVE : ShaderParams.SKW_CAST_DIRECT_LIGHT_BLEND);
}
if (useCustomBounds) {
keywords.Add(ShaderParams.SKW_CUSTOM_BOUNDS);
}
ShadowsSupportCheck();
if (enableShadows) {
if (usesCubemap) {
keywords.Add(ShaderParams.SKW_SHADOWS_CUBEMAP);
} else if (usesTranslucency) {
keywords.Add(ShaderParams.SKW_SHADOWS_TRANSLUCENCY);
} else {
keywords.Add(ShaderParams.SKW_SHADOWS);
}
}
#if UNITY_2021_3_OR_NEWER
fogMat.enabledKeywords = null;
#endif
fogMat.shaderKeywords = keywords.ToArray();
ParticlesCheckSupport();
if (OnPropertiesChanged != null) {
OnPropertiesChanged(this);
}
}
void SetFogMaterial() {
if (meshRenderer != null) {
if (density <= 0 || mediumAlbedo.a == 0) {
if (fogMatInvisible == null) {
fogMatInvisible = new Material(fogMatInvisibleShader);
fogMatInvisible.hideFlags = HideFlags.DontSave;
}
meshRenderer.sharedMaterial = fogMatInvisible;
} else {
meshRenderer.sharedMaterial = fogMat;
}
}
}
/// <summary>
/// Creates an automatic profile if profile is not set
public void CheckProfile() {
if (profile != null) {
if ("Auto".Equals(profile.name)) {
profile.ApplyTo(this);
profile = null;
} else if (profileSync) {
profile.ApplyTo(this);
}
}
}
/// <summary>
/// Gets bounds in world space
/// </summary>
public Bounds GetBounds() {
Bounds bounds = this.bounds;
if (useCustomBounds && boundsInLocalSpace) {
bounds.center += transform.position;
}
return bounds;
}
/// <summary>
/// Sets bounds in world space
/// </summary>
public void SetBounds(Bounds bounds) {
if (useCustomBounds && boundsInLocalSpace) {
bounds.center -= transform.position;
}
this.bounds = bounds;
UpdateVolumeGeometry();
}
}
}