using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace VLB
{
[HelpURL(Consts.HelpUrlConfig)]
public class Config : ScriptableObject
{
///
/// Override the layer on which the procedural geometry is created or not
///
public bool geometryOverrideLayer = Consts.ConfigGeometryOverrideLayerDefault;
///
/// The layer the procedural geometry gameObject is in (only if geometryOverrideLayer is enabled)
///
public int geometryLayerID = Consts.ConfigGeometryLayerIDDefault;
///
/// The tag applied on the procedural geometry gameObject
///
public string geometryTag = Consts.ConfigGeometryTagDefault;
///
/// Determine in which order beams are rendered compared to other objects.
/// This way for example transparent objects are rendered after opaque objects, and so on.
///
public int geometryRenderQueue = (int)Consts.ConfigGeometryRenderQueueDefault;
///
/// Select the Render Pipeline (Built-In or SRP) in use.
///
public RenderPipeline renderPipeline
{
get { return _RenderPipeline; }
set
{
#if UNITY_EDITOR
_RenderPipeline = value;
#else
Debug.LogError("Modifying the RenderPipeline in standalone builds is not permitted");
#endif
}
}
[FormerlySerializedAs("renderPipeline")]
[SerializeField] RenderPipeline _RenderPipeline = Consts.ConfigGeometryRenderPipelineDefault;
///
/// MultiPass: Use the 2 pass shader. Will generate 2 drawcalls per beam.
/// SinglePass: Use the 1 pass shader. Will generate 1 drawcall per beam.
/// GPUInstancing: Dynamically batch multiple beams to combine and reduce draw calls (Feature only supported in Unity 5.6 or above). More info: https://docs.unity3d.com/Manual/GPUInstancing.html
/// SRPBatcher: Use the SRP Batcher to automatically batch multiple beams and reduce draw calls. Only available when using SRP.
///
public RenderingMode renderingMode
{
get { return _RenderingMode; }
set
{
#if UNITY_EDITOR
_RenderingMode = value;
#else
Debug.LogError("Modifying the RenderingMode in standalone builds is not permitted");
#endif
}
}
[FormerlySerializedAs("renderingMode")]
[SerializeField] RenderingMode _RenderingMode = Consts.ConfigGeometryRenderingModeDefault;
public bool IsSRPBatcherSupported()
{
// The SRP Batcher Rendering Mode is only compatible when using a SRP
if (renderPipeline == RenderPipeline.BuiltIn) return false;
// SRP Batcher only works with URP and HDRP
var rp = SRPHelper.renderPipelineType;
return rp == SRPHelper.RenderPipeline.URP || rp == SRPHelper.RenderPipeline.HDRP;
}
///
/// Actual Rendering Mode used on the current platform
///
public RenderingMode actualRenderingMode
{
get
{
#pragma warning disable 0162 // warning CS0162: Unreachable code detected
if (renderingMode == RenderingMode.GPUInstancing && !BatchingHelper.isGpuInstancingSupported) return RenderingMode.SinglePass;
#pragma warning restore 0162
if (renderingMode == RenderingMode.SRPBatcher && !IsSRPBatcherSupported()) return RenderingMode.SinglePass;
if (renderPipeline != RenderPipeline.BuiltIn)
{
// Using a Scriptable Render Pipeline with 'Multi-Pass' Rendering Mode is not supported
if (renderingMode == RenderingMode.MultiPass) return RenderingMode.SinglePass;
}
return renderingMode;
}
}
///
/// Depending on the actual Rendering Mode used, returns true if the single pass shader will be used, false otherwise.
///
public bool useSinglePassShader { get { return actualRenderingMode != RenderingMode.MultiPass; } }
public bool requiresDoubleSidedMesh { get { return useSinglePassShader; } }
///
/// Main shader applied to the cone beam geometry
///
public Shader beamShader
{
get
{
#if UNITY_EDITOR
if(_BeamShader == null)
RefreshShader(RefreshShaderFlags.All);
#endif
return _BeamShader;
}
}
///
/// Depending on the quality of your screen, you might see some artifacts with high contrast visual (like a white beam over a black background).
/// These is a very common problem known as color banding.
/// To help with this issue, the plugin offers a Dithering factor: it smooths the banding by introducing a subtle pattern of noise.
///
public float ditheringFactor = Consts.ConfigDitheringFactor;
///
/// Number of Sides of the shared cone mesh
///
public int sharedMeshSides = Consts.ConfigSharedMeshSides;
///
/// Number of Segments of the shared cone mesh
///
public int sharedMeshSegments = Consts.ConfigSharedMeshSegments;
///
/// Global 3D Noise texture scaling: higher scale make the noise more visible, but potentially less realistic.
///
[Range(Consts.NoiseScaleMin, Consts.NoiseScaleMax)]
public float globalNoiseScale = Consts.NoiseScaleDefault;
///
/// Global World Space direction and speed of the noise scrolling, simulating the fog/smoke movement
///
public Vector3 globalNoiseVelocity = Consts.NoiseVelocityDefault;
///
/// Tag used to retrieve the camera used to compute the fade out factor on beams
///
public string fadeOutCameraTag = Consts.ConfigFadeOutCameraTagDefault;
public Transform fadeOutCameraTransform
{
get
{
if (m_CachedFadeOutCamera == null)
{
ForceUpdateFadeOutCamera();
}
return m_CachedFadeOutCamera;
}
}
///
/// Call this function if you want to manually change the fadeOutCameraTag property at runtime
///
public void ForceUpdateFadeOutCamera()
{
var gao = GameObject.FindGameObjectWithTag(fadeOutCameraTag);
if (gao)
m_CachedFadeOutCamera = gao.transform;
}
///
/// Binary file holding the 3D Noise texture data (a 3D array). Must be exactly Size * Size * Size bytes long.
///
[HighlightNull]
public TextAsset noise3DData = null;
///
/// Size (of one dimension) of the 3D Noise data. Must be power of 2. So if the binary file holds a 32x32x32 texture, this value must be 32.
///
public int noise3DSize = Consts.ConfigNoise3DSizeDefault;
///
/// ParticleSystem prefab instantiated for the Volumetric Dust Particles feature (Unity 5.5 or above)
///
[HighlightNull]
public ParticleSystem dustParticlesPrefab = null;
///
/// Noise texture for dithering feature
///
public Texture2D ditheringNoiseTexture = null;
///
/// Off: do not support having a gradient as color.
/// High Only: support gradient color only for devices with Shader Level = 35 or higher.
/// High and Low: support gradient color for all devices.
///
public FeatureEnabledColorGradient featureEnabledColorGradient = Consts.ConfigFeatureEnabledColorGradientDefault;
///
/// Support 'Soft Intersection with Opaque Geometry' feature or not.
///
public bool featureEnabledDepthBlend = Consts.ConfigFeatureEnabledDefault;
///
/// Support 'Noise 3D' feature or not.
///
public bool featureEnabledNoise3D = Consts.ConfigFeatureEnabledDefault;
///
/// Support 'Dynamic Occlusion' features or not.
///
public bool featureEnabledDynamicOcclusion = Consts.ConfigFeatureEnabledDefault;
///
/// Support 'Mesh Skewing' feature or not.
///
public bool featureEnabledMeshSkewing = Consts.ConfigFeatureEnabledDefault;
///
/// Support 'Shader Accuracy' property set to 'High' or not.
///
public bool featureEnabledShaderAccuracyHigh = Consts.ConfigFeatureEnabledDefault;
// INTERNAL
#pragma warning disable 0414
[SerializeField] int pluginVersion = -1;
[SerializeField] Material _DummyMaterial = null;
#pragma warning restore 0414
[SerializeField] Shader _BeamShader = null;
Transform m_CachedFadeOutCamera = null;
public bool hasRenderPipelineMismatch { get { return (SRPHelper.renderPipelineType == SRPHelper.RenderPipeline.BuiltIn) != (_RenderPipeline == RenderPipeline.BuiltIn); } }
[RuntimeInitializeOnLoadMethod]
static void OnStartup()
{
Instance.m_CachedFadeOutCamera = null;
Instance.RefreshGlobalShaderProperties();
#if UNITY_EDITOR
Instance.RefreshShader(RefreshShaderFlags.All);
#endif
if(Instance.hasRenderPipelineMismatch)
Debug.LogError("It looks like the 'Render Pipeline' is not correctly set in the config. Please make sure to select the proper value depending on your pipeline in use.", Instance);
}
public void Reset()
{
geometryOverrideLayer = Consts.ConfigGeometryOverrideLayerDefault;
geometryLayerID = Consts.ConfigGeometryLayerIDDefault;
geometryTag = Consts.ConfigGeometryTagDefault;
geometryRenderQueue = (int)Consts.ConfigGeometryRenderQueueDefault;
sharedMeshSides = Consts.ConfigSharedMeshSides;
sharedMeshSegments = Consts.ConfigSharedMeshSegments;
globalNoiseScale = Consts.NoiseScaleDefault;
globalNoiseVelocity = Consts.NoiseVelocityDefault;
renderPipeline = Consts.ConfigGeometryRenderPipelineDefault;
renderingMode = Consts.ConfigGeometryRenderingModeDefault;
ditheringFactor = Consts.ConfigDitheringFactor;
fadeOutCameraTag = Consts.ConfigFadeOutCameraTagDefault;
featureEnabledColorGradient = Consts.ConfigFeatureEnabledColorGradientDefault;
featureEnabledDepthBlend = Consts.ConfigFeatureEnabledDefault;
featureEnabledNoise3D = Consts.ConfigFeatureEnabledDefault;
featureEnabledDynamicOcclusion = Consts.ConfigFeatureEnabledDefault;
featureEnabledMeshSkewing = Consts.ConfigFeatureEnabledDefault;
featureEnabledShaderAccuracyHigh = Consts.ConfigFeatureEnabledDefault;
ResetInternalData();
#if UNITY_EDITOR
GlobalMesh.Destroy();
VolumetricLightBeam._EditorSetAllMeshesDirty();
#endif
}
void RefreshGlobalShaderProperties()
{
Shader.SetGlobalFloat(ShaderProperties.GlobalDitheringFactor, ditheringFactor);
Shader.SetGlobalTexture(ShaderProperties.GlobalDitheringNoiseTex, ditheringNoiseTexture);
}
#if UNITY_EDITOR
public void _EditorSetRenderingModeAndRefreshShader(RenderingMode mode)
{
renderingMode = mode;
RefreshShader(RefreshShaderFlags.All);
}
void OnValidate()
{
noise3DSize = Mathf.Max(2, Mathf.NextPowerOfTwo(noise3DSize));
sharedMeshSides = Mathf.Clamp(sharedMeshSides, Consts.GeomSidesMin, Consts.GeomSidesMax);
sharedMeshSegments = Mathf.Clamp(sharedMeshSegments, Consts.GeomSegmentsMin, Consts.GeomSegmentsMax);
ditheringFactor = Mathf.Clamp01(ditheringFactor);
}
void AutoSelectRenderPipeline()
{
var newPipeline = renderPipeline;
switch (SRPHelper.renderPipelineType)
{
case SRPHelper.RenderPipeline.BuiltIn:
newPipeline = RenderPipeline.BuiltIn;
break;
case SRPHelper.RenderPipeline.HDRP:
newPipeline = RenderPipeline.HDRP;
break;
case SRPHelper.RenderPipeline.URP:
case SRPHelper.RenderPipeline.LWRP:
newPipeline = RenderPipeline.URP;
break;
}
if (newPipeline != renderPipeline)
{
renderPipeline = newPipeline;
EditorUtility.SetDirty(this); // make sure to save this property change
RefreshShader(RefreshShaderFlags.All);
}
}
public static void EditorSelectInstance()
{
Selection.activeObject = Config.Instance; // this will create the instance if it doesn't exist
if (Selection.activeObject == null)
Debug.LogError("Cannot find any Config resource");
}
[System.Flags]
public enum RefreshShaderFlags
{
Reference = 1 << 1,
Dummy = 1 << 2,
All = Reference | Dummy,
}
public void RefreshShader(RefreshShaderFlags flags)
{
if (flags.HasFlag(RefreshShaderFlags.Reference))
{
var prevShader = _BeamShader;
var enabledFeatures = new ShaderGenerator.EnabledFeatures
{
dithering = ditheringFactor > 0.0f,
depthBlend = featureEnabledDepthBlend,
noise3D = featureEnabledNoise3D,
colorGradient = featureEnabledColorGradient,
dynamicOcclusion = featureEnabledDynamicOcclusion,
meshSkewing = featureEnabledMeshSkewing,
shaderAccuracyHigh = featureEnabledShaderAccuracyHigh
};
_BeamShader = ShaderGenerator.Generate(_RenderPipeline, actualRenderingMode, enabledFeatures);
if (_BeamShader != prevShader)
{
EditorUtility.SetDirty(this);
}
}
if (flags.HasFlag(RefreshShaderFlags.Dummy) && _BeamShader != null)
{
bool gpuInstanced = actualRenderingMode == RenderingMode.GPUInstancing;
_DummyMaterial = DummyMaterial.Create(_BeamShader, gpuInstanced);
}
if (_DummyMaterial == null)
{
Debug.LogError("No dummy material referenced to VLB config, please try to reset this asset.", this);
}
RefreshGlobalShaderProperties();
}
#endif // UNITY_EDITOR
public void ResetInternalData()
{
#if UNITY_EDITOR
RefreshShader(RefreshShaderFlags.All);
#endif
noise3DData = Resources.Load("Noise3D_64x64x64") as TextAsset;
noise3DSize = Consts.ConfigNoise3DSizeDefault;
dustParticlesPrefab = Resources.Load("DustParticles", typeof(ParticleSystem)) as ParticleSystem;
ditheringNoiseTexture = Resources.Load("VLBDitheringNoise", typeof(Texture2D)) as Texture2D;
}
public ParticleSystem NewVolumetricDustParticles()
{
if (!dustParticlesPrefab)
{
if (Application.isPlaying)
{
Debug.LogError("Failed to instantiate VolumetricDustParticles prefab.");
}
return null;
}
var instance = Instantiate(dustParticlesPrefab);
#if UNITY_5_4_OR_NEWER
instance.useAutoRandomSeed = false;
#endif
instance.name = "Dust Particles";
instance.gameObject.hideFlags = Consts.ProceduralObjectsHideFlags;
instance.gameObject.SetActive(true);
return instance;
}
void OnEnable()
{
HandleBackwardCompatibility(pluginVersion, Version.Current);
pluginVersion = Version.Current;
}
void HandleBackwardCompatibility(int serializedVersion, int newVersion)
{
if (serializedVersion == -1) return; // freshly new spawned config: nothing to do
if (serializedVersion == newVersion) return; // same version: nothing to do
#if UNITY_EDITOR
if (serializedVersion < 1830)
{
AutoSelectRenderPipeline();
}
if (newVersion > serializedVersion)
{
// Import to keep, we have to regenerate the shader each time the plugin is updated
RefreshShader(RefreshShaderFlags.All);
}
#endif
}
// Singleton management
static Config ms_Instance = null;
public static Config Instance { get { return GetInstance(true); } }
#if UNITY_EDITOR
static bool ms_IsCreatingInstance = false;
public bool IsCurrentlyUsedInstance() { return Instance == this; }
public bool HasValidAssetName()
{
if (name.IndexOf(ConfigOverride.kAssetName) != 0)
return false;
return PlatformHelper.IsValidPlatformSuffix(GetAssetSuffix());
}
public string GetAssetSuffix()
{
var fullname = name;
var strToFind = ConfigOverride.kAssetName;
if (fullname.IndexOf(strToFind) == 0) return fullname.Substring(strToFind.Length);
else return "";
}
#endif
private static Config GetInstance(bool assertIfNotFound)
{
#if UNITY_EDITOR
// Do not cache the instance during editing in order to handle new asset created or moved.
if (!Application.isPlaying || ms_Instance == null)
#else
if (ms_Instance == null)
#endif
{
#if UNITY_EDITOR
if (ms_IsCreatingInstance)
{
Debug.LogError(string.Format("Trying to access Config.Instance while creating it. Breaking before infinite loop."));
return null;
}
#endif
{
var newInstance = Resources.Load(ConfigOverride.kAssetName + PlatformHelper.GetCurrentPlatformSuffix());
if (newInstance == null)
newInstance = Resources.Load(ConfigOverride.kAssetName);
#if UNITY_EDITOR
if (newInstance && newInstance != ms_Instance)
{
ms_Instance = newInstance;
newInstance.RefreshGlobalShaderProperties(); // make sure noise textures are properly loaded as soon as the editor is started
}
#endif
ms_Instance = newInstance;
}
if (ms_Instance == null)
{
#if UNITY_EDITOR
ms_IsCreatingInstance = true;
ms_Instance = ConfigOverride.CreateInstanceOverride();
ms_IsCreatingInstance = false;
ms_Instance.AutoSelectRenderPipeline();
if (Application.isPlaying)
ms_Instance.Reset(); // Reset is not automatically when instancing a ScriptableObject when in playmode
#endif
Debug.Assert(!(assertIfNotFound && ms_Instance == null), string.Format("Can't find any resource of type '{0}'. Make sure you have a ScriptableObject of this type in a 'Resources' folder.", typeof(Config)));
}
}
return ms_Instance;
}
}
}