Răsfoiți Sursa

Bugfix: Various directional shadow tweaks and fixes
- Shadows are now significantly more stable with respect to camera movement
- Reduced shadow acne artifacts at far away surfaces
- Exposed a variety of shadow related properties to Camera render settings and the editor
- Geometry behind the light near plane will now be correctly flattened, even with negative depth bias
- Directional projection now correctly follows the view frustum, as well as provides a tighter fit for better quality
- Tweaked shadow bias and soft transition values to reduce light leaking

BearishSun 8 ani în urmă
părinte
comite
1a6d82bf0c

BIN
Data/Engine/Includes/ShadowDepthBase.bslinc.asset


BIN
Data/Engine/Shaders/DeferredDirectionalLight.bsl.asset


BIN
Data/Engine/Shaders/ShadowDepthCube.bsl.asset


BIN
Data/Engine/Shaders/ShadowDepthDirectional.bsl.asset


BIN
Data/Engine/Shaders/ShadowDepthNormal.bsl.asset


BIN
Data/Engine/Shaders/ShadowProject.bsl.asset


BIN
Data/Engine/Shaders/ShadowProjectOmni.bsl.asset


BIN
Data/Engine/Shaders/TiledDeferredLighting.bsl.asset


+ 5 - 22
Data/Raw/Engine/Includes/ShadowDepthBase.bslinc

@@ -47,27 +47,6 @@ mixin ShadowDepthBase
 			return (ndcZ + gNDCZToDeviceZ.y) * gNDCZToDeviceZ.x;
 		}		
 
-		void linearizeDepth(inout float4 clipPos)
-		{	
-			#ifdef CLAMP_TO_NEAR_PLANE
-			float ndcZ = clipPos.z / clipPos.w;
-			float deviceZ = NDCZToDeviceZ(ndcZ);
-			
-			// Clamp to near plane if behind it
-			if (deviceZ < 0)
-			{
-				clipPos.z = DeviceZToNDCZ(0);
-				clipPos.w = 1.0f;
-			}
-			#endif
-
-			// Output linear depth
-			#ifdef LINEAR_DEPTH_RANGE
-				float linearDepth = clipPos.z * gInvDepthRange + gDepthBias;
-				clipPos.z = linearDepth * clipPos.w;
-			#endif
-		}		
-	
 		ShadowVStoFS vsmain(VertexInput_PO input)
 		{
 			ShadowVStoFS output;
@@ -89,7 +68,11 @@ mixin ShadowDepthBase
 				float ndcZ = clipPos.z / clipPos.w;
 				float deviceZ = NDCZToDeviceZ(ndcZ);
 			
+				#ifdef USES_PS
 				if (deviceZ < 0)
+				#else
+				if (deviceZ < -gDepthBias)
+				#endif
 				{
 					clipPos.z = DeviceZToNDCZ(0);
 					clipPos.w = 1.0f;
@@ -102,7 +85,7 @@ mixin ShadowDepthBase
 			#ifdef USES_PS
 				output.shadowPos = clipPos.z;
 			#else // Otherwise apply bias immediately
-				clipPos.z += gDepthBias;
+				clipPos.z = max(0, clipPos.z + gDepthBias);
 			#endif // USES_PS
 			
 			output.position = clipPos;

+ 2 - 1
Source/BansheeCore/BsCorePrerequisites.h

@@ -599,7 +599,8 @@ namespace bs
 		TID_ColorGradingSettings = 30019,
 		TID_DepthOfFieldSettings = 30020,
 		TID_AmbientOcclusionSettings = 30021,
-		TID_ScreenSpaceReflectionsSettings = 30022
+		TID_ScreenSpaceReflectionsSettings = 30022,
+		TID_ShadowSettings = 30023
 	};
 }
 

+ 33 - 0
Source/BansheeCore/RTTI/BsRenderSettingsRTTI.h

@@ -249,6 +249,38 @@ namespace bs
 		}
 	};
 
+	class BS_CORE_EXPORT ShadowSettingsRTTI : public RTTIType <ShadowSettings, IReflectable, ShadowSettingsRTTI>
+	{
+	private:
+		BS_BEGIN_RTTI_MEMBERS
+			BS_RTTI_MEMBER_PLAIN(directionalShadowDistance, 0)
+			BS_RTTI_MEMBER_PLAIN(numCascades, 1)
+			BS_RTTI_MEMBER_PLAIN(cascadeDistributionExponent, 2)
+			BS_RTTI_MEMBER_PLAIN(shadowFilteringQuality, 3)
+		BS_END_RTTI_MEMBERS
+
+	public:
+		ShadowSettingsRTTI()
+			:mInitMembers(this)
+		{ }
+
+		const String& getRTTIName() override
+		{
+			static String name = "ShadowSettings";
+			return name;
+		}
+
+		UINT32 getRTTIId() override
+		{
+			return TID_ShadowSettings;
+		}
+
+		SPtr<IReflectable> newRTTIObject() override
+		{
+			return bs_shared_ptr_new<ShadowSettings>();
+		}
+	};
+
 	class BS_CORE_EXPORT RenderSettingsRTTI : public RTTIType <RenderSettings, IReflectable, RenderSettingsRTTI>
 	{
 	private:
@@ -270,6 +302,7 @@ namespace bs
 			BS_RTTI_MEMBER_PLAIN(enableShadows, 14)
 			BS_RTTI_MEMBER_PLAIN(overlayOnly, 15)
 			BS_RTTI_MEMBER_PLAIN(enableIndirectLighting, 16)
+			BS_RTTI_MEMBER_REFL(shadowSettings, 17)
 		BS_END_RTTI_MEMBERS
 			
 	public:

+ 1 - 1
Source/BansheeCore/Renderer/BsCamera.cpp

@@ -19,7 +19,7 @@ namespace bs
 	const float CameraBase::INFINITE_FAR_PLANE_ADJUST = 0.00001f;
 
 	CameraBase::CameraBase()
-		: mLayers(0xFFFFFFFFFFFFFFFF), mProjType(PT_PERSPECTIVE), mHorzFOV(Degree(90.0f)), mFarDist(1000.0f)
+		: mLayers(0xFFFFFFFFFFFFFFFF), mProjType(PT_PERSPECTIVE), mHorzFOV(Degree(90.0f)), mFarDist(500.0f)
 		, mNearDist(0.05f), mAspect(1.33333333333333f), mOrthoHeight(5), mPriority(0), mCustomViewMatrix(false)
 		, mCustomProjMatrix(false), mMSAA(1), mFrustumExtentsManuallySet(false), mProjMatrixRS(BsZero), mProjMatrix(BsZero)
 		, mViewMatrix(BsZero), mProjMatrixRSInv(BsZero), mProjMatrixInv(BsZero), mViewMatrixInv(BsZero)

+ 25 - 0
Source/BansheeCore/Renderer/BsRenderSettings.cpp

@@ -108,6 +108,16 @@ namespace bs
 		return ScreenSpaceReflectionsSettings::getRTTIStatic();
 	}
 
+	RTTITypeBase* ShadowSettings::getRTTIStatic()
+	{
+		return ShadowSettingsRTTI::instance();
+	}
+
+	RTTITypeBase* ShadowSettings::getRTTI() const
+	{
+		return ShadowSettings::getRTTIStatic();
+	}
+
 	RenderSettings::RenderSettings()
 		: enableAutoExposure(true), enableTonemapping(true), enableFXAA(true), exposureScale(0.0f), gamma(2.2f)
 		, enableHDR(true), enableLighting(true), enableShadows(true), enableIndirectLighting(true), overlayOnly(false)
@@ -184,6 +194,11 @@ namespace bs
 		bufferSize += rttiGetElemSize(screenSpaceReflections.maxRoughness);
 		bufferSize += rttiGetElemSize(screenSpaceReflections.quality);
 
+		bufferSize += rttiGetElemSize(shadowSettings.directionalShadowDistance);
+		bufferSize += rttiGetElemSize(shadowSettings.numCascades);
+		bufferSize += rttiGetElemSize(shadowSettings.cascadeDistributionExponent);
+		bufferSize += rttiGetElemSize(shadowSettings.shadowFilteringQuality);
+
 		if (buffer == nullptr)
 		{
 			size = bufferSize;
@@ -254,6 +269,11 @@ namespace bs
 		writeDst = rttiWriteElem(screenSpaceReflections.intensity, writeDst);
 		writeDst = rttiWriteElem(screenSpaceReflections.maxRoughness, writeDst);
 		writeDst = rttiWriteElem(screenSpaceReflections.quality, writeDst);
+
+		writeDst = rttiWriteElem(shadowSettings.directionalShadowDistance, writeDst);
+		writeDst = rttiWriteElem(shadowSettings.numCascades, writeDst);
+		writeDst = rttiWriteElem(shadowSettings.cascadeDistributionExponent, writeDst);
+		writeDst = rttiWriteElem(shadowSettings.shadowFilteringQuality, writeDst);
 	}
 
 	void RenderSettings::_setSyncData(UINT8* buffer, UINT32 size)
@@ -317,5 +337,10 @@ namespace bs
 		readSource = rttiReadElem(screenSpaceReflections.intensity, readSource);
 		readSource = rttiReadElem(screenSpaceReflections.maxRoughness, readSource);
 		readSource = rttiReadElem(screenSpaceReflections.quality, readSource);
+
+		readSource = rttiReadElem(shadowSettings.directionalShadowDistance, readSource);
+		readSource = rttiReadElem(shadowSettings.numCascades, readSource);
+		readSource = rttiReadElem(shadowSettings.cascadeDistributionExponent, readSource);
+		readSource = rttiReadElem(shadowSettings.shadowFilteringQuality, readSource);
 	}
 }

+ 49 - 0
Source/BansheeCore/Renderer/BsRenderSettings.h

@@ -399,6 +399,51 @@ namespace bs
 		static RTTITypeBase* getRTTIStatic();
 		RTTITypeBase* getRTTI() const override;
 	};
+
+	/** Various options that control shadow rendering for a specific view. */
+	struct BS_CORE_EXPORT BS_SCRIPT_EXPORT(m:Rendering) ShadowSettings : public IReflectable
+	{
+		/** 
+		 * Maximum distance that directional light shadows are allowed to render at. Decreasing the distance can yield
+		 * higher quality shadows nearer to the viewer, as the shadow map resolution isn't being used up on far away
+		 * portions of the scene. In world units (meters).
+		 */
+		BS_SCRIPT_EXPORT()
+		float directionalShadowDistance = 250.0f;
+		
+		/** 
+		 * Number of cascades to use for directional shadows. Higher number of cascades increases shadow quality as each
+		 * individual cascade has less area to cover, but can significantly increase performance cost, as well as a minor
+		 * increase in memory cost. Valid range is roughly [1, 6].
+		 */
+		BS_SCRIPT_EXPORT()
+		UINT32 numCascades = 4;
+
+		/**
+		 * Allows you to control how are directional shadow cascades distributed. Value of 1 means the cascades will be
+		 * linearly split, each cascade taking up the same amount of space. Value of 2 means each subsequent split will be
+		 * twice the size of the previous one (meaning cascades closer to the viewer cover a smaller area, and therefore
+		 * yield higher resolution shadows). Higher values increase the size disparity between near and far cascades at
+		 * an exponential rate. Valid range is roughly [1, 4].
+		 */
+		BS_SCRIPT_EXPORT()
+		float cascadeDistributionExponent = 3.0f;
+
+		/**
+		 * Determines the number of samples used for percentage closer shadow map filtering. Higher values yield higher
+		 * quality shadows, at the cost of performance. Valid range is [1, 4].
+		 */
+		BS_SCRIPT_EXPORT()
+		UINT32 shadowFilteringQuality = 4;
+
+		/************************************************************************/
+		/* 								RTTI		                     		*/
+		/************************************************************************/
+	public:
+		friend class ShadowSettingsRTTI;
+		static RTTITypeBase* getRTTIStatic();
+		RTTITypeBase* getRTTI() const override;
+	};
 	
 	/** Settings that control rendering for a specific camera (view). */
 	struct BS_CORE_EXPORT BS_SCRIPT_EXPORT(m:Rendering) RenderSettings : public IReflectable
@@ -509,6 +554,10 @@ namespace bs
 		BS_SCRIPT_EXPORT()
 		bool enableShadows;
 
+		/** Parameters used for customizing shadow rendering. */
+		BS_SCRIPT_EXPORT()
+		ShadowSettings shadowSettings;
+
 		/** Determines if indirect lighting (e.g. from light probes or the sky) is rendered. */
 		BS_SCRIPT_EXPORT()
 		bool enableIndirectLighting;

+ 121 - 4
Source/MBansheeEditor/Inspectors/RenderSettingsInspector.cs

@@ -595,6 +595,101 @@ namespace BansheeEditor
         }
     }
 
+    /// <summary>
+    /// Draws GUI elements for inspecting an <see cref="ShadowSettings"/> object.
+    /// </summary>
+    internal class ShadowSettingsGUI
+    {
+        private ShadowSettings settings;
+
+        private GUIFloatField directionalShadowDistanceField = new GUIFloatField(new LocEdString("Directional shadow distance"));
+        private GUIIntField numCascadesField = new GUIIntField(new LocEdString("Cascade count"));
+        private GUIFloatField cascadeDistributionExponentField = new GUIFloatField(new LocEdString("Cascade distribution exponent"));
+        private GUISliderField filteringQualityField = new GUISliderField(0, 4, new LocEdString("Filtering quality"));
+
+        public Action<ShadowSettings> OnChanged;
+        public Action OnConfirmed;
+
+        /// <summary>
+        /// Current value of the settings object.
+        /// </summary>
+        public ShadowSettings Settings
+        {
+            get { return settings; }
+            set
+            {
+                settings = value;
+
+                directionalShadowDistanceField.Value = value.DirectionalShadowDistance;
+                numCascadesField.Value = (int)value.NumCascades;
+                cascadeDistributionExponentField.Value = value.CascadeDistributionExponent;
+                filteringQualityField.Value = value.ShadowFilteringQuality;
+            }
+        }
+
+        /// <summary>
+        /// Constructs a new set of GUI elements for inspecting the shadow settings object.
+        /// </summary>
+        /// <param name="settings">Initial values to assign to the GUI elements.</param>
+        /// <param name="layout">Layout to append the GUI elements to.</param>
+        public ShadowSettingsGUI(ShadowSettings settings, GUILayout layout)
+        {
+            this.settings = settings;
+
+            directionalShadowDistanceField.OnChanged += x =>
+            {
+                this.settings.DirectionalShadowDistance = x;
+                MarkAsModified();
+                ConfirmModify();
+            };
+
+            numCascadesField.OnChanged += x =>
+            {
+                this.settings.NumCascades = (uint) x;
+                MarkAsModified();
+                ConfirmModify();
+            };
+
+            cascadeDistributionExponentField.OnChanged += x =>
+            {
+                this.settings.CascadeDistributionExponent = x;
+                MarkAsModified();
+                ConfirmModify();
+            };
+
+            filteringQualityField.OnChanged += x =>
+            {
+                this.settings.ShadowFilteringQuality = (uint)x;
+                MarkAsModified();
+                ConfirmModify();
+            };
+
+            filteringQualityField.Step = 1.0f;
+
+            layout.AddElement(directionalShadowDistanceField);
+            layout.AddElement(numCascadesField);
+            layout.AddElement(cascadeDistributionExponentField);
+            layout.AddElement(filteringQualityField);
+        }
+
+        /// <summary>
+        /// Marks the contents of the inspector as modified.
+        /// </summary>
+        private void MarkAsModified()
+        {
+            if (OnChanged != null)
+                OnChanged(settings);
+        }
+
+        /// <summary>
+        /// Confirms any queued modifications.
+        /// </summary>
+        private void ConfirmModify()
+        {
+            if (OnConfirmed != null)
+                OnConfirmed();
+        }
+    }
     /// <summary>
     /// Draws GUI elements for inspecting an <see cref="RenderSettings"/> object.
     /// </summary>
@@ -628,6 +723,8 @@ namespace BansheeEditor
         private AmbientOcclusionSettingsGUI ambientOcclusionGUI;
         private GUIToggle screenSpaceReflectionsFoldout = new GUIToggle("Screen space reflections", EditorStyles.Foldout);
         private ScreenSpaceReflectionsSettingsGUI screenSpaceReflectionsGUI;
+        private GUIToggle shadowsFoldout = new GUIToggle("Shadows", EditorStyles.Foldout);
+        private ShadowSettingsGUI shadowsGUI;
 
         private GUISliderField gammaField = new GUISliderField(1.0f, 3.0f, new LocEdString("Gamma"));
         private GUISliderField exposureScaleField = new GUISliderField(-8.0f, 8.0f, new LocEdString("Exposure scale"));
@@ -639,6 +736,7 @@ namespace BansheeEditor
         private GUILayout depthOfFieldLayout;
         private GUILayout ambientOcclusionLayout;
         private GUILayout screenSpaceReflectionsLayout;
+        private GUILayout shadowsLayout;
 
         public Action<RenderSettings> OnChanged;
         public Action OnConfirmed;
@@ -662,6 +760,7 @@ namespace BansheeEditor
                 depthOfFieldGUI.Settings = value.DepthOfField;
                 ambientOcclusionGUI.Settings = value.AmbientOcclusion;
                 screenSpaceReflectionsGUI.Settings = value.ScreenSpaceReflections;
+                shadowsGUI.Settings = value.ShadowSettings;
                 gammaField.Value = value.Gamma;
                 exposureScaleField.Value = value.ExposureScale;
                 enableHDRField.Value = value.EnableHDR;
@@ -693,10 +792,6 @@ namespace BansheeEditor
             enableLightingField.OnChanged += x => { this.settings.EnableLighting = x; MarkAsModified(); ConfirmModify(); };
             layout.AddElement(enableLightingField);
 
-            // Enable shadows
-            enableShadowsField.OnChanged += x => { this.settings.EnableShadows = x; MarkAsModified(); ConfirmModify(); };
-            layout.AddElement(enableShadowsField);
-
             // Enable indirect lighting
             enableIndirectLightingField.OnChanged += x => { this.settings.EnableIndirectLighting = x; MarkAsModified(); ConfirmModify(); };
             layout.AddElement(enableIndirectLightingField);
@@ -705,6 +800,27 @@ namespace BansheeEditor
             overlayOnlyField.OnChanged += x => { this.settings.OverlayOnly = x; MarkAsModified(); ConfirmModify(); };
             layout.AddElement(overlayOnlyField);
 
+            // Shadows
+            enableShadowsField.OnChanged += x => { this.settings.EnableShadows = x; MarkAsModified(); ConfirmModify(); };
+            layout.AddElement(enableShadowsField);
+
+            shadowsFoldout.OnToggled += x =>
+            {
+                properties.SetBool("shadows_Expanded", x);
+                ToggleFoldoutFields();
+            };
+            layout.AddElement(shadowsFoldout);
+
+            shadowsLayout = layout.AddLayoutX();
+            {
+                shadowsLayout.AddSpace(10);
+
+                GUILayoutY contentsLayout = shadowsLayout.AddLayoutY();
+                shadowsGUI = new ShadowSettingsGUI(settings.ShadowSettings, contentsLayout);
+                shadowsGUI.OnChanged += x => { this.settings.ShadowSettings = x; MarkAsModified(); };
+                shadowsGUI.OnConfirmed += ConfirmModify;
+            }
+
             // Auto exposure
             enableAutoExposureField.OnChanged += x => { this.settings.EnableAutoExposure = x; MarkAsModified(); ConfirmModify(); };
             layout.AddElement(enableAutoExposureField);
@@ -883,6 +999,7 @@ namespace BansheeEditor
             depthOfFieldLayout.Active = properties.GetBool("depthOfField_Expanded");
             ambientOcclusionLayout.Active = properties.GetBool("ambientOcclusion_Expanded");
             screenSpaceReflectionsLayout.Active = properties.GetBool("screenSpaceReflections_Expanded");
+            shadowsLayout.Active = properties.GetBool("shadows_Expanded");
         }
     }
 

+ 0 - 6
Source/RenderBeast/BsRenderBeastOptions.h

@@ -45,12 +45,6 @@ namespace bs { namespace ct
 		 * shadows far away, but will never increase the resolution past the provided value.
 		 */
 		UINT32 shadowMapSize = 2048;
-
-		/**
-		 * Determines the number of samples used for percentage closer shadow map filtering. Higher values yield higher
-		 * quality shadows. Valid range is [1, 4].
-		 */
-		UINT32 shadowFilteringQuality = 4;
 	};
 
 	/** @} */

+ 1 - 1
Source/RenderBeast/BsRenderCompositor.cpp

@@ -732,7 +732,7 @@ namespace bs { namespace ct
 
 				UINT32 lightIdx = offset + j;
 				const RendererLight& light = *lights[lightIdx];
-				shadowRenderer.renderShadowOcclusion(inputs.view, inputs.options.shadowFilteringQuality, light, gbuffer);
+				shadowRenderer.renderShadowOcclusion(inputs.view, light, gbuffer);
 
 				rapi.setRenderTarget(outputRT, FBT_DEPTH | FBT_STENCIL, RT_COLOR0 | RT_DEPTH_STENCIL);
 				StandardDeferred::instance().renderLight(lightType, light, inputs.view, gbuffer,

+ 1 - 1
Source/RenderBeast/BsRendererView.cpp

@@ -504,7 +504,7 @@ namespace bs { namespace ct
 	}
 
 	RendererViewGroup::RendererViewGroup()
-		:mShadowRenderer(1024)
+		:mShadowRenderer(2048)
 	{ }
 
 	RendererViewGroup::RendererViewGroup(RendererView** views, UINT32 numViews, UINT32 shadowMapSize)

+ 59 - 48
Source/RenderBeast/BsShadowRendering.cpp

@@ -376,17 +376,17 @@ namespace bs { namespace ct
 		return mShadowMap->renderTexture;
 	}
 
-	ShadowCascadedMap::ShadowCascadedMap(UINT32 size)
-		:ShadowMapBase(size)
+	ShadowCascadedMap::ShadowCascadedMap(UINT32 size, UINT32 numCascades)
+		:ShadowMapBase(size), mNumCascades(numCascades), mTargets(numCascades), mShadowInfos(numCascades)
 	{
 		mShadowMap = GpuResourcePool::instance().get(POOLED_RENDER_TEXTURE_DESC::create2D(SHADOW_MAP_FORMAT, size, size, 
-			TU_DEPTHSTENCIL, 0, false, NUM_CASCADE_SPLITS));
+			TU_DEPTHSTENCIL, 0, false, numCascades));
 
 		RENDER_TEXTURE_DESC rtDesc;
 		rtDesc.depthStencilSurface.texture = mShadowMap->texture;
 		rtDesc.depthStencilSurface.numFaces = 1;
 
-		for (UINT32 i = 0; i < NUM_CASCADE_SPLITS; ++i)
+		for (UINT32 i = 0; i < mNumCascades; ++i)
 		{
 			rtDesc.depthStencilSurface.face = i;
 			mTargets[i] = RenderTexture::create(rtDesc);
@@ -929,9 +929,11 @@ namespace bs { namespace ct
 		return shadowMapTfrm * mixedToShadow;
 	}
 
-	void ShadowRendering::renderShadowOcclusion(const RendererView& view, UINT32 shadowQuality, 
-		const RendererLight& rendererLight, GBufferTextures gbuffer) const
+	void ShadowRendering::renderShadowOcclusion(const RendererView& view, const RendererLight& rendererLight, 
+		GBufferTextures gbuffer) const
 	{
+		UINT32 shadowQuality = view.getRenderSettings().shadowSettings.shadowFilteringQuality;
+
 		const Light* light = rendererLight.internal;
 		UINT32 lightIdx = light->getRendererId();
 
@@ -1022,7 +1024,7 @@ namespace bs { namespace ct
 
 					// Render cascades in far to near order.
 					// Note: If rendering other non-cascade maps they should be rendered after cascades.
-					for (INT32 i = NUM_CASCADE_SPLITS - 1; i >= 0; i--)
+					for (INT32 i = cascadedMap.getNumCascades() - 1; i >= 0; i--)
 						shadowInfos.push_back(&cascadedMap.getShadowInfo(i));
 				}
 			}
@@ -1034,7 +1036,7 @@ namespace bs { namespace ct
 				// Depth range scale is already baked into the ortho projection matrix, so avoid doing it here
 				if (isCSM)
 				{
-					// Need to map from NDC depth to [0, 1]
+					// Need to map from API-specific clip space depth to [0, 1] range
 					depthScale = 1.0f / (rapiInfo.getMaximumDepthInputValue() - rapiInfo.getMinimumDepthInputValue());
 					depthOffset = -rapiInfo.getMinimumDepthInputValue() * depthScale;
 				}
@@ -1144,6 +1146,7 @@ namespace bs { namespace ct
 		// of the shadow map. A different approach would be to generate a bounding box and then both adjust the aspect
 		// ratio (and therefore dimensions) of the shadow map, as well as rotate the camera so the visible area best fits
 		// in the map. It remains to be seen if this is viable.
+		//  - Note2: Actually both of these will likely have serious negative impact on shadow stability.
 		const SceneInfo& sceneInfo = scene.getSceneInfo();
 
 		const RendererLight& rendererLight = sceneInfo.directionalLights[lightIdx];
@@ -1163,11 +1166,12 @@ namespace bs { namespace ct
 		shadowInfo.area = Rect2I(0, 0, mapSize, mapSize);
 		shadowInfo.updateNormArea(mapSize);
 
+		UINT32 numCascades = view.getRenderSettings().shadowSettings.numCascades;
 		for (UINT32 i = 0; i < (UINT32)mCascadedShadowMaps.size(); i++)
 		{
 			ShadowCascadedMap& shadowMap = mCascadedShadowMaps[i];
 
-			if (!shadowMap.isUsed() && shadowMap.getSize() == mapSize)
+			if (!shadowMap.isUsed() && shadowMap.getSize() == mapSize && shadowMap.getNumCascades() == numCascades)
 			{
 				shadowInfo.textureIdx = i;
 				shadowMap.markAsUsed();
@@ -1179,7 +1183,7 @@ namespace bs { namespace ct
 		if (shadowInfo.textureIdx == (UINT32)-1)
 		{
 			shadowInfo.textureIdx = (UINT32)mCascadedShadowMaps.size();
-			mCascadedShadowMaps.push_back(ShadowCascadedMap(mapSize));
+			mCascadedShadowMaps.push_back(ShadowCascadedMap(mapSize, numCascades));
 
 			ShadowCascadedMap& shadowMap = mCascadedShadowMaps.back();
 			shadowMap.markAsUsed();
@@ -1188,25 +1192,37 @@ namespace bs { namespace ct
 		ShadowCascadedMap& shadowMap = mCascadedShadowMaps[shadowInfo.textureIdx];
 
 		Quaternion lightRotation(BsIdentity);
-		lightRotation.lookRotation(-tfrm.getRotation().zAxis());
+		lightRotation.lookRotation(lightDir, Vector3::UNIT_Y);
 
-		Matrix4 viewMat = Matrix4::view(tfrm.getPosition(), lightRotation);
-		for (UINT32 i = 0; i < NUM_CASCADE_SPLITS; ++i)
+		for (UINT32 i = 0; i < numCascades; ++i)
 		{
 			Sphere frustumBounds;
-			ConvexVolume cascadeCullVolume = getCSMSplitFrustum(view, -lightDir, i, NUM_CASCADE_SPLITS, frustumBounds);
+			ConvexVolume cascadeCullVolume = getCSMSplitFrustum(view, lightDir, i, numCascades, frustumBounds);
 
-			// Move the light at the boundary of the subject frustum, so we don't waste depth range
-			Vector3 frustumCenterViewSpace = viewMat.multiply(frustumBounds.getCenter());
-			float minSubjectDepth = -frustumCenterViewSpace.z - frustumBounds.getRadius();
-			float maxSubjectDepth = minSubjectDepth + frustumBounds.getRadius() * 2.0f;
+			// Make sure the size of the projected area is in multiples of shadow map pixel size (for stability)
+			float worldUnitsPerTexel = frustumBounds.getRadius() * 2.0f / shadowMap.getSize();
+
+			float orthoSize = floor(frustumBounds.getRadius() * 2.0f / worldUnitsPerTexel) * worldUnitsPerTexel * 0.5f;
+			worldUnitsPerTexel = orthoSize * 2.0f / shadowMap.getSize();
+			
+			// Snap caster origin to the shadow map pixel grid, to ensure shadow map stability
+			Vector3 casterOrigin = frustumBounds.getCenter();
+			Matrix4 shadowView = Matrix4::view(Vector3::ZERO, lightRotation);
+			Vector3 shadowSpaceOrigin = shadowView.multiplyAffine(casterOrigin);
 
-			shadowInfo.depthRange = maxSubjectDepth - minSubjectDepth;
+			Vector2 snapOffset(fmod(shadowSpaceOrigin.x, worldUnitsPerTexel), fmod(shadowSpaceOrigin.y, worldUnitsPerTexel));
+			shadowSpaceOrigin.x -= snapOffset.x;
+			shadowSpaceOrigin.y -= snapOffset.y;
 
-			Vector3 offsetLightPos = tfrm.getPosition() + lightDir * minSubjectDepth;
+			Matrix4 shadowViewInv = shadowView.inverseAffine();
+			casterOrigin = shadowViewInv.multiplyAffine(shadowSpaceOrigin);
+
+			// Move the light so it is centered at the subject frustum, with depth range covering the frustum bounds
+			shadowInfo.depthRange = frustumBounds.getRadius() * 2.0f;
+
+			Vector3 offsetLightPos = casterOrigin - lightDir * frustumBounds.getRadius();
 			Matrix4 offsetViewMat = Matrix4::view(offsetLightPos, lightRotation);
 
-			float orthoSize = frustumBounds.getRadius() * 0.5f;
 			Matrix4 proj = Matrix4::projectionOrthographic(-orthoSize, orthoSize, orthoSize, -orthoSize, 0.0f, 
 				shadowInfo.depthRange);
 
@@ -1216,14 +1232,14 @@ namespace bs { namespace ct
 			shadowInfo.shadowVPTransform = proj * offsetViewMat;
 
 			// Determine split range
-			float splitNear = getCSMSplitDistance(view, i, NUM_CASCADE_SPLITS);
-			float splitFar = getCSMSplitDistance(view, i + 1, NUM_CASCADE_SPLITS);
+			float splitNear = getCSMSplitDistance(view, i, numCascades);
+			float splitFar = getCSMSplitDistance(view, i + 1, numCascades);
 
 			shadowInfo.depthNear = splitNear;
 			shadowInfo.depthFade = splitFar;
 			shadowInfo.subjectBounds = frustumBounds;
 			
-			if ((UINT32)(i + 1) < NUM_CASCADE_SPLITS)
+			if ((UINT32)(i + 1) < numCascades)
 				shadowInfo.fadeRange = CASCADE_FRACTION_FADE * (shadowInfo.depthFade - shadowInfo.depthNear);
 			else
 				shadowInfo.fadeRange = 0.0f;
@@ -1243,11 +1259,11 @@ namespace bs { namespace ct
 			depthDirMat->bind(shadowParamsBuffer);
 
 			// Render all renderables into the shadow map
-			ShadowRenderQueueDirOptions spotOptions(
+			ShadowRenderQueueDirOptions dirOptions(
 				cascadeCullVolume,
 				shadowParamsBuffer);
 			
-			ShadowRenderQueue::execute(scene, frameInfo, spotOptions);
+			ShadowRenderQueue::execute(scene, frameInfo, dirOptions);
 
 			shadowMap.setShadowInfo(i, shadowInfo);
 		}
@@ -1714,9 +1730,8 @@ namespace bs { namespace ct
 		viewPlanes[FRUSTUM_PLANE_TOP] = Plane(frustumVerts[4], frustumVerts[5], frustumVerts[1]);
 		viewPlanes[FRUSTUM_PLANE_BOTTOM] = Plane(frustumVerts[3], frustumVerts[2], frustumVerts[6]);
 
-		Vector<Plane> lightVolume;
-
 		//// Add camera's planes facing towards the lights (forming the back of the volume)
+		Vector<Plane> lightVolume;
 		for(auto& entry : viewPlanes)
 		{
 			if (entry.normal.dot(lightDir) < 0.0f)
@@ -1777,11 +1792,8 @@ namespace bs { namespace ct
 
 	float ShadowRendering::getCSMSplitDistance(const RendererView& view, UINT32 index, UINT32 numCascades)
 	{
-		// Determines the size of each subsequent cascade split. Value of 1 means the cascades will be linearly split.
-		// Value of 2 means each subsequent split will be twice the size of the previous one. Valid range is roughly
-		// [1, 4].
-		// Note: Make this an adjustable property?
-		const static float DISTRIBUTON_EXPONENT = 3.0f;
+		auto& shadowSettings = view.getRenderSettings().shadowSettings;
+		float distributionExponent = shadowSettings.cascadeDistributionExponent;
 
 		// First determine the scale of the split, relative to the entire range
 		float scaleModifier = 1.0f;
@@ -1797,7 +1809,7 @@ namespace bs { namespace ct
 					scale += scaleModifier;
 
 				totalScale += scaleModifier;
-				scaleModifier *= DISTRIBUTON_EXPONENT;
+				scaleModifier *= distributionExponent;
 			}
 
 			scale = scale / totalScale;
@@ -1806,7 +1818,7 @@ namespace bs { namespace ct
 		// Calculate split distance in Z
 		auto& viewProps = view.getProperties();
 		float near = viewProps.nearPlane;
-		float far = viewProps.farPlane;
+		float far = Math::clamp(shadowSettings.directionalShadowDistance, viewProps.nearPlane, viewProps.farPlane);
 
 		return near + (far - near) * scale;
 	}
@@ -1815,29 +1827,28 @@ namespace bs { namespace ct
 	{
 		const static float RADIAL_LIGHT_BIAS = 0.0005f;
 		const static float SPOT_DEPTH_BIAS = 0.01f;
-		const static float DIR_DEPTH_BIAS = 0.5f;
+		const static float DIR_DEPTH_BIAS = 0.001f; // In clip space units
 		const static float DEFAULT_RESOLUTION = 512.0f;
 		
 		// Increase bias if map size smaller than some resolution
-		float resolutionScale;
+		float resolutionScale = 1.0f;
 		
-		if (light.getType() == LightType::Directional)
-			resolutionScale = radius / (float)mapSize;
-		else
+		if (light.getType() != LightType::Directional)
 			resolutionScale = DEFAULT_RESOLUTION / (float)mapSize;
 
 		// Adjust range because in shader we compare vs. clip space depth
-		float rangeScale;
-		if (light.getType() == LightType::Radial)
-			rangeScale = 1.0f;
-		else
+		float rangeScale = 1.0f;
+		if (light.getType() == LightType::Spot)
 			rangeScale = 1.0f / depthRange;
 		
+		auto& apiInfo = RenderAPI::instance().getAPIInfo();
+		float deviceDepthRange = apiInfo.getMaximumDepthInputValue() - apiInfo.getMinimumDepthInputValue();
+
 		float defaultBias = 1.0f;
 		switch(light.getType())
 		{
 		case LightType::Directional: 
-			defaultBias = DIR_DEPTH_BIAS;
+			defaultBias = DIR_DEPTH_BIAS * deviceDepthRange;
 			break;
 		case LightType::Radial: 
 			defaultBias = RADIAL_LIGHT_BIAS;
@@ -1855,7 +1866,7 @@ namespace bs { namespace ct
 	float ShadowRendering::getFadeTransition(const Light& light, float radius, float depthRange, UINT32 mapSize)
 	{
 		const static float SPOT_LIGHT_SCALE = 1000.0f;
-		const static float DIR_LIGHT_SCALE = 5000000.0f;
+		const static float DIR_LIGHT_SCALE = 50000000.0f;
 
 		// Note: Currently fade transitions are only used in spot & directional (non omni-directional) lights, so no need
 		// to account for radial light type.
@@ -1870,9 +1881,9 @@ namespace bs { namespace ct
 			// Increase the size of the transition region for larger lights
 			float radiusScale = radius;
 
-			return light.getShadowBias() * DIR_LIGHT_SCALE * rangeScale * resolutionScale * radiusScale;
+			return DIR_LIGHT_SCALE * rangeScale * resolutionScale * radiusScale;
 		}
 		else
-			return light.getShadowBias() * SPOT_LIGHT_SCALE;
+			return fabs(light.getShadowBias()) * SPOT_LIGHT_SCALE;
 	}
 }}

+ 13 - 13
Source/RenderBeast/BsShadowRendering.h

@@ -23,9 +23,6 @@ namespace bs { namespace ct
 	 *  @{
 	 */
 
-	/** Number of frustum splits when rendering cascaded shadow maps. */
-	const UINT32 NUM_CASCADE_SPLITS = 4;
-
 	BS_PARAM_BLOCK_BEGIN(ShadowParamsDef)
 		BS_PARAM_BLOCK_ENTRY(Matrix4, gMatViewProj)
 		BS_PARAM_BLOCK_ENTRY(Vector2, gNDCZToDeviceZ)
@@ -451,7 +448,10 @@ namespace bs { namespace ct
 	class ShadowCascadedMap : public ShadowMapBase
 	{
 	public:
-		ShadowCascadedMap(UINT32 size);
+		ShadowCascadedMap(UINT32 size, UINT32 numCascades);
+
+		/** Returns the total number of cascades in the cascade shadow map. */
+		UINT32 getNumCascades() const { return mNumCascades; }
 
 		/** Returns a render target that allows rendering into a specific cascade of the cascaded shadow map. */
 		SPtr<RenderTexture> getTarget(UINT32 cascadeIdx) const;
@@ -462,8 +462,9 @@ namespace bs { namespace ct
 		/** @copydoc setShadowInfo */
 		const ShadowInfo& getShadowInfo(UINT32 cascadeIdx) const { return mShadowInfos[cascadeIdx]; }
 	private:
-		SPtr<RenderTexture> mTargets[NUM_CASCADE_SPLITS];
-		ShadowInfo mShadowInfos[NUM_CASCADE_SPLITS];
+		UINT32 mNumCascades;
+		Vector<SPtr<RenderTexture>> mTargets;
+		Vector<ShadowInfo> mShadowInfos;
 	};
 
 	/** Provides functionality for rendering shadow maps. */
@@ -499,8 +500,7 @@ namespace bs { namespace ct
 		 * Renders shadow occlusion values for the specified light, through the provided view, into the currently bound
 		 * render target. The system uses shadow maps rendered by renderShadowMaps().
 		 */
-		void renderShadowOcclusion(const RendererView& view, UINT32 shadowQuality, const RendererLight& light, 
-			GBufferTextures gbuffer) const;
+		void renderShadowOcclusion(const RendererView& view, const RendererLight& light, GBufferTextures gbuffer) const;
 
 		/** Changes the default shadow map size. Will cause all shadow maps to be rebuilt. */
 		void setShadowMapSize(UINT32 size);
@@ -572,11 +572,11 @@ namespace bs { namespace ct
 		 * Finds the distance (along the view direction) of the frustum split for the specified index. Used for cascaded
 		 * shadow maps.
 		 * 
-		 * @param[in]	view			View whose frustum to split.
-		 * @param[in]	index			Index of the split. 0 = near plane.
-		 * @param[in]	numCascades		Maximum number of cascades in the cascaded shadow map. Must be greater than zero
-		 *								and greater or equal to @p index.
-		 * @return						Distance to the split position along the view direction.
+		 * @param[in]	view					View whose frustum to split.
+		 * @param[in]	index					Index of the split. 0 = near plane.
+		 * @param[in]	numCascades				Maximum number of cascades in the cascaded shadow map. Must be greater than
+		 *										zero and greater or equal to @p index.
+		 * @return								Distance to the split position along the view direction.
 		 */
 		static float getCSMSplitDistance(const RendererView& view, UINT32 index, UINT32 numCascades);