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; } } }