Bladeren bron

More work on shadow mapping

BearishSun 8 jaren geleden
bovenliggende
commit
e315616d5d

+ 1 - 0
Data/Raw/Editor/Shaders/SceneGrid.bsl

@@ -46,6 +46,7 @@ technique SceneGrid
 			float4 worldCameraPos;
 			float gridSpacing;
 			float gridBorderWidth;
+			[color]
 			float4 gridColor;
 			float gridFadeOutStart;
 			float gridFadeOutEnd;

+ 4 - 0
Data/Raw/Engine/DataList.json

@@ -285,6 +285,10 @@
         {
             "Path": "ShadowProjectOmni.bsl",
             "UUID": "50447773-98e9-410c-8f64-eacbb9622c52"
+        },
+        {
+            "Path": "ShadowProjectStencil.bsl",
+            "UUID": "c8625547-f5e7-43df-85cf-4da6e1806cf9"
         }
     ],
     "Skin": [

+ 13 - 0
Data/Raw/Engine/Includes/PerCameraData.bslinc

@@ -20,6 +20,7 @@ mixin PerCameraData
 			// Converts device Z to world Z using this formula: worldZ = (1 / (deviceZ + y)) * x
 			float2 	 gDeviceZToWorldZ;
 			float2	 gNDCZToWorldZ;
+			float2 	 gNDCZToDeviceZ;
 			
 			// x - near plane distance, y - far plane distance
 			float2	 gNearFar;
@@ -46,6 +47,18 @@ mixin PerCameraData
 			return -gNDCZToWorldZ.y + (gNDCZToWorldZ.x / viewZ);
 		}
 		
+		/** Converts Z value from NDC space to device Z value in range [0, 1]. */
+		float NDCZToDeviceZ(float ndcZ)
+		{
+			return (ndcZ + gNDCZToDeviceZ.y) * gNDCZToDeviceZ.x;
+		}
+		
+		/** Converts Z value from device range ([0, 1]) to NDC space. */
+		float DeviceZToNDCZ(float deviceZ)
+		{
+			return deviceZ / gNDCZToDeviceZ.x - gNDCZToDeviceZ.y;
+		}
+		
 		/** Converts position in NDC to UV coordinates mapped to the screen rectangle. */ 
 		float2 NDCToUV(float2 ndcPos)
 		{

+ 11 - 7
Data/Raw/Engine/Includes/ShadowDepthBase.bslinc

@@ -29,18 +29,22 @@ mixin ShadowDepthBase
 
 		void linearizeDepth(inout float4 clipPos)
 		{
+			float ndcZ = clipPos.z / clipPos.w;
+			float deviceZ = NDCZToDeviceZ(ndcZ);
+			
+			#ifdef CLAMP_TO_NEAR_PLANE
 			// Clamp to near plane if behind it
-			if (clipPos.z < 0)
+			if (deviceZ < 0)
 			{
-				clipPos.z = 0.000001f;
+				clipPos.z = DeviceZToNDCZ(0);
 				clipPos.w = 1.0f;
 			}
+			#endif
 
-			// Output linear depth in range [0, 1]
-			// TODO - Handle case for backends using [-1, 1] depth range
-			#if LINEAR_DEPTH_RANGE
-				float linearDepth = clipPos.z * gDepthRange + gDepthBias;
-				clipPos.z = linearDepth * clipPos.w;
+			// Output linear depth
+			#ifdef LINEAR_DEPTH_RANGE
+				float linearDepth = deviceZ * gDepthRange + gDepthBias;
+				clipPos.z = DeviceZToNDCZ(linearDepth) * clipPos.w;
 			#endif
 		}		
 	

+ 2 - 0
Data/Raw/Engine/Shaders/ShadowDepthDirectional.bsl

@@ -1,3 +1,5 @@
+#define LINEAR_DEPTH_RANGE 1
+#define CLAMP_TO_NEAR_PLANE 1
 #include "$ENGINE$\ShadowDepthBase.bslinc"
 
 mixin ShadowDepth

+ 0 - 1
Data/Raw/Engine/Shaders/ShadowDepthNormal.bsl

@@ -1,4 +1,3 @@
-#define LINEAR_DEPTH_RANGE 1
 #include "$ENGINE$\ShadowDepthBase.bslinc"
 
 mixin ShadowDepth

+ 2 - 2
Data/Raw/Engine/Shaders/ShadowProjectOmni.bsl

@@ -116,8 +116,8 @@ technique ShadowProjectOmni
 			// Get position of the receiver in shadow space
 			float4 shadowPos = mul(gFaceVPMatrices[faceIdx], worldPos);
 			
-			float receiverDepth = shadowPos.z / shadowPos.w;
-			float shadowBias = gDepthBias / shadowPos.w;
+			float receiverDepth = NDCZToDeviceZ(shadowPos.z / shadowPos.w);
+			float shadowBias = gDepthBias / -shadowPos.w;
 			
 			float occlusion = 0.0f;
 			#if SHADOW_QUALITY <= 1

+ 40 - 0
Data/Raw/Engine/Shaders/ShadowProjectStencil.bsl

@@ -0,0 +1,40 @@
+#include "$ENGINE$/ShadowProjectionCommon.bslinc"
+
+technique ShadowProjectStencil
+{
+	mixin ShadowProjectionCommon;
+
+	depth
+	{
+		write = false;
+	};
+	
+	blend
+	{
+		target
+		{
+			writemask = empty;
+		};
+	};	
+	
+	raster
+	{
+		cull = none;
+	};
+	
+	#ifdef USE_ZFAIL_STENCIL
+	stencil
+	{
+		enabled = true;
+		front = { keep, incwrap, keep, always };
+		back = { keep, decwrap, keep, always };
+	};
+	#else
+	stencil
+	{
+		enabled = true;
+		front = { keep, keep, incwrap, always };
+		back = { keep, keep, decwrap, always };
+	};	
+	#endif
+};

+ 1 - 1
Documentation/Manuals/Native/quickref.md

@@ -7,7 +7,7 @@ Here are some common conventions used throughout Banshee:
  - Camera is looking towards the negative z axis
  - Screen/window space origin is located in the top left corner
  - Normalized device coordinates origin is located in the bottom left corner
- - Clip space ranges from -1 to 1 for x, y and depth
+ - Normalized device coordinates range from -1 to 1 for x, y and depth
  - Clockwise faces are considered as front facing
  - Matrices are stored in row major format
  - Vectors are interpreted as column vectors

+ 6 - 3
Source/BansheeEngine/Include/BsRendererUtility.h

@@ -140,10 +140,13 @@ namespace bs { namespace ct
 			drawScreenQuad(uv, textureSize, numInstances);
 		}
 
-		/** Returns a stencil mesh used for a point light (a unit sphere). */
-		SPtr<Mesh> getPointLightStencil() const { return mPointLightStencilMesh; }
+		/** Returns a stencil mesh used for a radial light (a unit sphere). */
+		SPtr<Mesh> getRadialLightStencil() const { return mPointLightStencilMesh; }
 
-		/** Returns a stencil mesh used for spot light. Actual vertex positions need to be computed in shader. */
+		/** 
+		 * Returns a stencil mesh used for a spot light. Actual vertex positions need to be computed in shader as this
+		 * method will return uninitialized vertex positions. 
+		 */
 		SPtr<Mesh> getSpotLightStencil() const { return mSpotLightStencilMesh; }
 
 		/** Returns a mesh that can be used for rendering a skybox. */

+ 1 - 0
Source/BansheeSL/BsLexerFX.l

@@ -145,6 +145,7 @@ rsub			{ yylval->intValue = BOV_RevSubtract; return TOKEN_BLENDOPVALUE; }
 min				{ yylval->intValue = BOV_Min; return TOKEN_BLENDOPVALUE; }
 max				{ yylval->intValue = BOV_Max; return TOKEN_BLENDOPVALUE; }
 
+empty			{ yylval->intValue = 0x0; return TOKEN_COLORMASK; }
 R				{ yylval->intValue = 0x1; return TOKEN_COLORMASK; }
 G				{ yylval->intValue = 0x2; return TOKEN_COLORMASK; }
 B				{ yylval->intValue = 0x4; return TOKEN_COLORMASK; }

+ 6 - 10
Source/BansheeSL/Source/BsSLFXCompiler.cpp

@@ -17,10 +17,6 @@
 #define XSC_ENABLE_LANGUAGE_EXT 1
 #include "Xsc/Xsc.h"
 
-//DEBUG ONLY
-#include "BsFileSystem.h"
-#include "BsDataStream.h"
-
 extern "C" {
 #include "BsMMAlloc.h"
 #include "BsParserFX.h"
@@ -504,7 +500,7 @@ namespace bs
 				{
 					GpuParamDataType type = ReflTypeToDataType((Xsc::Reflection::DataType)entry.baseType);
 					if ((entry.flags & Xsc::Reflection::Uniform::Flags::Color) != 0 &&
-						(entry.baseType == GPDT_FLOAT3 || entry.baseType == GPDT_FLOAT4))
+						(type == GPDT_FLOAT3 || type == GPDT_FLOAT4))
 					{
 						type = GPDT_COLOR;
 					}
@@ -526,7 +522,7 @@ namespace bs
 		}
 	}
 
-	String CrossCompile(const String& hlsl, GpuProgramType type, bool vulkan, bool optionalEntry, UINT32& startBindingSlot,
+	String crossCompile(const String& hlsl, GpuProgramType type, bool vulkan, bool optionalEntry, UINT32& startBindingSlot,
 		SHADER_DESC* shaderDesc = nullptr, Vector<GpuProgramType>* detectedTypes = nullptr)
 	{
 		SPtr<StringStream> input = bs_shared_ptr_new<StringStream>();
@@ -657,13 +653,13 @@ namespace bs
 	// Convert HLSL code to GLSL
 	String HLSLtoGLSL(const String& hlsl, GpuProgramType type, bool vulkan, UINT32& startBindingSlot)
 	{
-		return CrossCompile(hlsl, type, vulkan, false, startBindingSlot);
+		return crossCompile(hlsl, type, vulkan, false, startBindingSlot);
 	}
 
-	void ReflectHLSL(const String& hlsl, SHADER_DESC& shaderDesc, Vector<GpuProgramType>& entryPoints)
+	void reflectHLSL(const String& hlsl, SHADER_DESC& shaderDesc, Vector<GpuProgramType>& entryPoints)
 	{
 		UINT32 dummy = 0;
-		CrossCompile(hlsl, GPT_VERTEX_PROGRAM, false, true, dummy, &shaderDesc, &entryPoints);
+		crossCompile(hlsl, GPT_VERTEX_PROGRAM, false, true, dummy, &shaderDesc, &entryPoints);
 	}
 
 	BSLFXCompileResult BSLFXCompiler::compile(const String& name, const String& source, 
@@ -1629,7 +1625,7 @@ namespace bs
 				// type. If performance is ever important here it could be good to update XShaderCompiler so it can
 				// somehow save the AST and then re-use it for multiple actions.
 				Vector<GpuProgramType> types;
-				ReflectHLSL(glslPassData.code, shaderDesc, types);
+				reflectHLSL(glslPassData.code, shaderDesc, types);
 
 				UINT32 glslBinding = 0;
 				UINT32 vkslBinding = 0;

+ 18 - 12
Source/BansheeUtility/Include/BsAABox.h

@@ -27,7 +27,7 @@ namespace bs
 		|/    |/
 		6-----7
 		*/
-		enum CornerEnum 
+		enum Corner 
 		{
 			FAR_LEFT_BOTTOM = 0,
 			FAR_LEFT_TOP = 1,
@@ -66,7 +66,7 @@ namespace bs
 		void scale(const Vector3& s);
 
 		/** Returns the coordinates of a specific corner. */
-		Vector3 getCorner(CornerEnum cornerToGet) const;
+		Vector3 getCorner(Corner cornerToGet) const;
 
 		/** Merges the two boxes, creating a new bounding box that encapsulates them both. */
 		void merge(const AABox& rhs);
@@ -106,11 +106,11 @@ namespace bs
 		/** Returns true if the plane intersects the bounding box. */
 		bool intersects(const Plane& p) const;
 
-        /** Ray / box intersection, returns a boolean result and nearest distance to intersection. */
-        std::pair<bool, float> intersects(const Ray& ray) const;
+		/** Ray / box intersection, returns a boolean result and nearest distance to intersection. */
+		std::pair<bool, float> intersects(const Ray& ray) const;
 
-        /** Ray / box intersection, returns boolean result and near and far intersection distance. */
-        bool intersects(const Ray& ray, float& d1, float& d2) const;
+		/** Ray / box intersection, returns boolean result and near and far intersection distance. */
+		bool intersects(const Ray& ray, float& d1, float& d2) const;
 
 		/** Center of the box. */
 		Vector3 getCenter() const;
@@ -127,17 +127,23 @@ namespace bs
 		/** Size of the volume in the box. */
 		float getVolume() const;
 
-        /** Returns true if the provided point is inside the bounding box. */
-        bool contains(const Vector3& v) const;
+		/** Returns true if the provided point is inside the bounding box. */
+		bool contains(const Vector3& v) const;
 
-        /** Returns true if the provided bounding box is completely inside the bounding box. */
-        bool contains(const AABox& other) const;
+		/** Returns true if the provided bounding box is completely inside the bounding box. */
+		bool contains(const AABox& other) const;
 
-        bool operator== (const AABox& rhs) const;
-        bool operator!= (const AABox& rhs) const;
+		bool operator== (const AABox& rhs) const;
+		bool operator!= (const AABox& rhs) const;
 
 		static const AABox BOX_EMPTY;
 
+		/** 
+		 * Indices that can be used for rendering a box constructed from 8 corner vertices, using AABox::Corner for 
+		 * mapping. 
+		 */
+		static const UINT32 CUBE_INDICES[36];
+
 	protected:
 		Vector3 mMinimum;
 		Vector3 mMaximum;

+ 9 - 0
Source/BansheeUtility/Include/BsConvexVolume.h

@@ -44,6 +44,15 @@ namespace bs
 		 */
 		bool intersects(const Sphere& sphere) const;
 
+		/**
+		 * Checks if the convex volume contains the provided point.
+		 * 
+		 * @param[in]	p		Point to check.
+		 * @param[in]	expand	Optional value to expand the size of the convex volume by the specified value during the
+		 *						check. Negative values shrink the volume.
+		 */
+		bool contains(const Vector3& p, float expand = 0.0f) const;
+
 		/** Returns the internal set of planes that represent the volume. */
 		Vector<Plane> getPlanes() const { return mPlanes; }
 

+ 28 - 1
Source/BansheeUtility/Source/BsAABox.cpp

@@ -10,6 +10,33 @@ namespace bs
 {
 	const AABox AABox::BOX_EMPTY;
 
+	const UINT32 AABox::CUBE_INDICES[36] =
+	{
+		// Near
+		NEAR_LEFT_BOTTOM, NEAR_LEFT_TOP, NEAR_RIGHT_TOP,
+		NEAR_LEFT_BOTTOM, NEAR_RIGHT_TOP, NEAR_RIGHT_BOTTOM,
+
+		// Far
+		FAR_RIGHT_BOTTOM, FAR_RIGHT_TOP, FAR_LEFT_TOP,
+		FAR_RIGHT_BOTTOM, FAR_LEFT_TOP, FAR_LEFT_BOTTOM,
+
+		// Left
+		FAR_LEFT_BOTTOM, FAR_LEFT_TOP, NEAR_LEFT_TOP,
+		FAR_LEFT_BOTTOM, NEAR_LEFT_TOP, NEAR_LEFT_BOTTOM,
+
+		// Right
+		NEAR_RIGHT_BOTTOM, NEAR_RIGHT_TOP, FAR_RIGHT_TOP,
+		NEAR_RIGHT_BOTTOM, FAR_RIGHT_TOP, FAR_RIGHT_BOTTOM,
+
+		// Top
+		FAR_LEFT_TOP, FAR_RIGHT_TOP, NEAR_RIGHT_TOP,
+		FAR_LEFT_TOP, NEAR_RIGHT_TOP, NEAR_LEFT_TOP,
+
+		// Bottom
+		NEAR_LEFT_BOTTOM, NEAR_RIGHT_BOTTOM, FAR_RIGHT_BOTTOM,
+		NEAR_LEFT_BOTTOM, FAR_RIGHT_BOTTOM, FAR_LEFT_BOTTOM
+	};
+
 	AABox::AABox() 
 		:mMinimum(Vector3::ZERO), mMaximum(Vector3::ONE)
 	{
@@ -52,7 +79,7 @@ namespace bs
 		setExtents(min, max);
 	}
 
-	Vector3 AABox::getCorner(CornerEnum cornerToGet) const
+	Vector3 AABox::getCorner(Corner cornerToGet) const
 	{
 		switch(cornerToGet)
 		{

+ 11 - 0
Source/BansheeUtility/Source/BsConvexVolume.cpp

@@ -128,4 +128,15 @@ namespace bs
 
 		return true;
 	}
+
+	bool ConvexVolume::contains(const Vector3& p, float expand) const
+	{
+		for(auto& plane : mPlanes)
+		{
+			if (plane.getDistance(p) < -expand)
+				return false;
+		}
+
+		return true;
+	}
 }

+ 1 - 1
Source/Examples/ExampleLowLevelRendering/Source/Main.cpp

@@ -373,7 +373,7 @@ namespace bs { namespace ct
 	/////////////////////////////////////////////////////////////////////////////////////
 	void writeBoxVertices(const AABox& box, UINT8* positions, UINT8* uvs, UINT32 stride)
 	{
-		AABox::CornerEnum vertOrder[] =
+		AABox::Corner vertOrder[] =
 		{
 			AABox::NEAR_LEFT_BOTTOM,	AABox::NEAR_RIGHT_BOTTOM,	AABox::NEAR_RIGHT_TOP,		AABox::NEAR_LEFT_TOP,
 			AABox::FAR_RIGHT_BOTTOM,	AABox::FAR_LEFT_BOTTOM,		AABox::FAR_LEFT_TOP,		AABox::FAR_RIGHT_TOP,

+ 1 - 0
Source/RenderBeast/Include/BsRendererView.h

@@ -27,6 +27,7 @@ namespace bs { namespace ct
 		BS_PARAM_BLOCK_ENTRY(Matrix4, gMatScreenToWorld)
 		BS_PARAM_BLOCK_ENTRY(Vector2, gDeviceZToWorldZ)
 		BS_PARAM_BLOCK_ENTRY(Vector2, gNDCZToWorldZ)
+		BS_PARAM_BLOCK_ENTRY(Vector2, gNDCZToDeviceZ)
 		BS_PARAM_BLOCK_ENTRY(Vector2, gNearFar)
 		BS_PARAM_BLOCK_ENTRY(Vector4I, gViewportRectangle)
 		BS_PARAM_BLOCK_ENTRY(Vector4, gClipToUVScaleOffset)

+ 105 - 6
Source/RenderBeast/Include/BsShadowRendering.h

@@ -17,6 +17,7 @@ namespace bs { namespace ct
 	struct FrameInfo;
 	class RendererLight;
 	class RendererScene;
+	struct ShadowInfo;
 
 	/** @addtogroup RenderBeast
 	 *  @{
@@ -91,6 +92,49 @@ namespace bs { namespace ct
 			const SPtr<GpuParamBlockBuffer>& shadowCubeMasks);
 	};
 
+	BS_PARAM_BLOCK_BEGIN(ShadowProjectVertParamsDef)
+		BS_PARAM_BLOCK_ENTRY(Vector4, gPositionAndScale)
+	BS_PARAM_BLOCK_END
+
+	extern ShadowProjectVertParamsDef gShadowProjectVertParamsDef;
+
+	/** Material used for populating the stencil buffer when projecting non-omnidirectional shadows. */
+	template<bool Directional, bool ZFailStencil>
+	class ShadowProjectStencilMat : public RendererMaterial<ShadowProjectStencilMat<Directional, ZFailStencil>>
+	{
+		RMAT_DEF("ShadowProjectStencil.bsl");
+
+	public:
+		ShadowProjectStencilMat();
+
+		/** Binds the material and its parameters to the pipeline. */
+		void bind(const SPtr<GpuParamBlockBuffer>& perCamera);
+
+	private:
+		SPtr<GpuParamBlockBuffer> mVertParams;
+	};
+
+	/** Contains all variations of the ShadowProjectStencilMat material. */
+	class ShadowProjectStencilMaterials
+	{
+	public:
+		/** 
+		 * Binds a shader that can be used for populating the stencil buffer during non-omnidirectional shadow rendering. 
+		 *
+		 * @param[in]	directional		Set to true if shadows from a directional light are being rendered.
+		 * @param[in]	useZFailStencil	If true the material will use z-fail operation to modify the stencil buffer. If 
+		 *								false z-pass will be used instead. Z-pass is a more performant alternative as it
+		 *								doesn't disable hi-z optimization, but it cannot handle the case when the viewer is
+		 *								inside the drawn geometry.
+		 * @param[in]	perCamera		Buffer containing information about the current view.
+		 */
+		void bind(bool directional, bool useZFailStencil, const SPtr<GpuParamBlockBuffer>& perCamera);
+	private:
+		ShadowProjectStencilMat<true, true> mTT;
+		ShadowProjectStencilMat<false, true> mFT;
+		ShadowProjectStencilMat<false, false> mFF;
+	};
+
 	BS_PARAM_BLOCK_BEGIN(ShadowProjectParamsDef)
 		BS_PARAM_BLOCK_ENTRY(Matrix4, gMixedToShadowSpace)
 		BS_PARAM_BLOCK_ENTRY(Vector2, gShadowMapSize)
@@ -99,7 +143,7 @@ namespace bs { namespace ct
 		BS_PARAM_BLOCK_ENTRY(float, gFadePercent)
 		BS_PARAM_BLOCK_ENTRY(float, gFadePlaneDepth)
 		BS_PARAM_BLOCK_ENTRY(float, gInvFadePlaneRange)
-		BS_PARAM_BLOCK_END
+	BS_PARAM_BLOCK_END
 
 	extern ShadowProjectParamsDef gShadowProjectParamsDef;
 
@@ -112,12 +156,13 @@ namespace bs { namespace ct
 	public:
 		ShadowProjectMat();
 
-		/** Binds the material to the pipeline, ready to be used on subsequent draw calls. */
-		void bind(const SPtr<Texture>& shadowMap, const SPtr<GpuParamBlockBuffer>& shadowParams, 
+		/** Binds the material and its parameters to the pipeline. */
+		void bind(const Light& light, const SPtr<Texture>& shadowMap, const SPtr<GpuParamBlockBuffer> shadowParams, 
 			const SPtr<GpuParamBlockBuffer>& perCamera);
 
 	private:
 		SPtr<SamplerState> mSamplerState;
+		SPtr<GpuParamBlockBuffer> mVertParams;
 
 		GBufferParams mGBufferParams;
 
@@ -131,7 +176,7 @@ namespace bs { namespace ct
 		BS_PARAM_BLOCK_ENTRY(float, gInvResolution)
 		BS_PARAM_BLOCK_ENTRY(float, gFadePercent)
 		BS_PARAM_BLOCK_ENTRY(float, gDepthBias)
-		BS_PARAM_BLOCK_END
+	BS_PARAM_BLOCK_END
 
 	extern ShadowProjectOmniParamsDef gShadowProjectOmniParamsDef;
 
@@ -144,12 +189,13 @@ namespace bs { namespace ct
 	public:
 		ShadowProjectOmniMat();
 
-		/** Binds the material to the pipeline, ready to be used on subsequent draw calls. */
-		void bind(const SPtr<Texture>& shadowMap, const SPtr<GpuParamBlockBuffer>& shadowParams,
+		/** Binds the material and its parameters to the pipeline. */
+		void bind(const Light& light, const SPtr<Texture>& shadowMap, const SPtr<GpuParamBlockBuffer> shadowParams, 
 			const SPtr<GpuParamBlockBuffer>& perCamera);
 
 	private:
 		SPtr<SamplerState> mSamplerState;
+		SPtr<GpuParamBlockBuffer> mVertParams;
 
 		GBufferParams mGBufferParams;
 
@@ -168,6 +214,16 @@ namespace bs { namespace ct
 		Rect2 normArea; /**< Normalized shadow map area in [0, 1] range. */
 		UINT32 textureIdx; /**< Index of the texture the shadow map is stored in. */
 
+		float depthNear; /**< Distance to the near plane. */
+		float depthFar; /**< Distance to the far plane. */
+		float depthFade; /**< Distance to the plane at which to start fading out the shadows (only for CSM). */
+		float fadeRange; /**< Distance from the fade plane to the far plane (only for CSM). */
+
+		float depthBias; /**< Bias used to reduce shadow acne. */
+		float depthRange; /**< Length of the range covered by the shadow caster volume. */
+
+		UINT32 cascadeIdx; /**< Index of a cascade. Only relevant for CSM. */
+
 		/** View-projection matrix from the shadow casters point of view. */
 		Matrix4 shadowVPTransform; 
 
@@ -343,6 +399,23 @@ namespace bs { namespace ct
 		void calcShadowMapProperties(const RendererLight& light, RendererScene& scene, UINT32& size, 
 			SmallVector<float, 4>& fadePercents, float& maxFadePercent) const;
 
+		/**
+		 * Draws a mesh representing near and far planes at the provided coordinates. The mesh is constructed using
+		 * normalized device coordinates and requires no perspective transform. Near plane will be drawn using front facing
+		 * triangles, and the far plane will be drawn using back facing triangles.
+		 * 
+		 * @param[in]	near		Location of the near plane, in NDC.
+		 * @param[in]	far			Location of the far plane, in NDC.
+		 * @param[in]	drawNear	If disabled, only the far plane will be drawn.
+		 */
+		void drawNearFarPlanes(float near, float far, bool drawNear = true);
+
+		/** 
+		 * Draws a frustum mesh using the provided vertices as its corners. Corners should be in the order specified
+		 * by AABox::Corner enum.
+		 */
+		void drawFrustum(const std::array<Vector3, 8>& corners);
+
 		/**
 		 * Generates a frustum for a single cascade of a cascaded shadow map. Also outputs spherical bounds of the
 		 * split view frustum.
@@ -379,6 +452,17 @@ namespace bs { namespace ct
 		 */
 		static float getDepthBias(const Light& light, float depthRange, UINT32 mapSize);
 
+		/**
+		 * Calculates a fade transition value that can be used for slowly fading-in the shadow, in order to avoid or reduce
+		 * shadow acne.
+		 *
+		 * @param[in]	light		Light to calculate the fade transition size for.
+		 * @param[in]	depthRange	Range of depths (distance between near and far planes) covered by the shadow.
+		 * @param[in]	mapSize		Size of the shadow map, in pixels.
+		 * @return					Value that determines the size of the fade transition region.
+		 */
+		static float getFadeTransition(const Light& light, float depthRange, UINT32 mapSize);
+
 		/** Size of a single shadow map atlas, in pixels. */
 		static const UINT32 MAX_ATLAS_SIZE;
 
@@ -394,10 +478,15 @@ namespace bs { namespace ct
 		/** Size of the border of a shadow map in a shadow map atlas, in pixels. */
 		static const UINT32 SHADOW_MAP_BORDER;
 
+		/** Percent of the length of a single cascade in a CSM, in which to fade out the cascade. */
+		static const float CASCADE_FRACTION_FADE;
+
 		ShadowDepthNormalMat mDepthNormalMat;
 		ShadowDepthCubeMat mDepthCubeMat;
 		ShadowDepthDirectionalMat mDepthDirectionalMat;
 
+		ShadowProjectStencilMaterials mProjectStencilMaterials;
+
 		UINT32 mShadowMapSize;
 
 		Vector<ShadowMapAtlas> mDynamicShadowMaps;
@@ -410,6 +499,16 @@ namespace bs { namespace ct
 		Vector<LightShadows> mRadialLightShadows;
 		Vector<UINT32> mDirectionalLightShadows;
 
+		SPtr<VertexDeclaration> mPositionOnlyVD;
+
+		// Mesh information used for drawing near & far planes
+		SPtr<IndexBuffer> mPlaneIB;
+		SPtr<VertexBuffer> mPlaneVB;
+
+		// Mesh information used for drawing a shadow frustum
+		SPtr<IndexBuffer> mFrustumIB;
+		SPtr<VertexBuffer> mFrustumVB;
+
 		Vector<bool> mRenderableVisibility; // Transient
 		Vector<ShadowMapOptions> mSpotLightShadowOptions; // Transient
 		Vector<ShadowMapOptions> mRadialLightShadowOptions; // Transient

+ 10 - 5
Source/RenderBeast/Source/BsRendererView.cpp

@@ -333,6 +333,9 @@ namespace bs { namespace ct
 
 	void RendererView::updatePerViewBuffer()
 	{
+		RenderAPI& rapi = RenderAPI::instance();
+		const RenderAPIInfo& rapiInfo = rapi.getAPIInfo();
+
 		Matrix4 viewProj = mProperties.projTransform * mProperties.viewTransform;
 		Matrix4 invViewProj = viewProj.inverse();
 
@@ -342,11 +345,11 @@ namespace bs { namespace ct
 		gPerCameraParamDef.gMatInvViewProj.set(mParamBuffer, invViewProj); // Note: Calculate inverses separately (better precision possibly)
 		gPerCameraParamDef.gMatInvProj.set(mParamBuffer, mProperties.projTransform.inverse());
 
-		// Construct a special inverse view-projection matrix that had projection entries that affect z and w eliminated.
+		// Construct a special inverse view-projection matrix that had projection entries that effect z and w eliminated.
 		// Used to transform a vector(clip_x, clip_y, view_z, view_w), where clip_x/clip_y are in clip space, and 
 		// view_z/view_w in view space, into world space.
 
-		// Only projects z/w coordinates
+		// Only projects z/w coordinates (cancels out with the inverse matrix below)
 		Matrix4 projZ = Matrix4::IDENTITY;
 		projZ[2][2] = mProperties.projTransform[2][2];
 		projZ[2][3] = mProperties.projTransform[2][3];
@@ -359,6 +362,11 @@ namespace bs { namespace ct
 		gPerCameraParamDef.gDeviceZToWorldZ.set(mParamBuffer, getDeviceZTransform(mProperties.projTransform));
 		gPerCameraParamDef.gNDCZToWorldZ.set(mParamBuffer, getNDCZTransform(mProperties.projTransform));
 
+		Vector2 ndcZToDeviceZ;
+		ndcZToDeviceZ.x = 1.0f / (rapiInfo.getMaximumDepthInputValue() - rapiInfo.getMinimumDepthInputValue());
+		ndcZToDeviceZ.y = -rapiInfo.getMinimumDepthInputValue();
+		gPerCameraParamDef.gNDCZToDeviceZ.set(mParamBuffer, ndcZToDeviceZ);
+
 		Vector2 nearFar(mProperties.nearPlane, mProperties.farPlane);
 		gPerCameraParamDef.gNearFar.set(mParamBuffer, nearFar);
 
@@ -378,9 +386,6 @@ namespace bs { namespace ct
 		float rtWidth = mTargetDesc.targetWidth != 0 ? (float)mTargetDesc.targetWidth : 20.0f;
 		float rtHeight = mTargetDesc.targetHeight != 0 ? (float)mTargetDesc.targetHeight : 20.0f;
 
-		RenderAPI& rapi = RenderAPI::instance();
-		const RenderAPIInfo& rapiInfo = rapi.getAPIInfo();
-
 		Vector4 clipToUVScaleOffset;
 		clipToUVScaleOffset.x = halfWidth / rtWidth;
 		clipToUVScaleOffset.y = -halfHeight / rtHeight;

+ 425 - 41
Source/RenderBeast/Source/BsShadowRendering.cpp

@@ -9,6 +9,7 @@
 #include "BsMesh.h"
 #include "BsCamera.h"
 #include "BsBitwise.h"
+#include "BsVertexDataDesc.h"
 
 namespace bs { namespace ct
 {
@@ -86,6 +87,55 @@ namespace bs { namespace ct
 	}
 
 	ShadowProjectParamsDef gShadowProjectParamsDef;
+	ShadowProjectVertParamsDef gShadowProjectVertParamsDef;
+
+	template<bool Directional, bool ZFailStencil>
+	ShadowProjectStencilMat<Directional, ZFailStencil>::ShadowProjectStencilMat()
+	{
+		SPtr<GpuParams> params = mParamsSet->getGpuParams();
+
+		mVertParams = gShadowProjectVertParamsDef.createBuffer();
+		if(params->hasParamBlock(GPT_VERTEX_PROGRAM, "VertParams"))
+			params->setParamBlockBuffer(GPT_VERTEX_PROGRAM, "VertParams", mVertParams);
+	}
+
+	template<bool Directional, bool ZFailStencil>
+	void ShadowProjectStencilMat<Directional, ZFailStencil>::_initDefines(ShaderDefines& defines)
+	{
+		defines.set("NEEDS_TRANSFORM", Directional ? 0 : 1);
+
+		if(ZFailStencil)
+			defines.set("USE_ZFAIL_STENCIL", 1);
+	}
+
+	template<bool Directional, bool ZFailStencil>
+	void ShadowProjectStencilMat<Directional, ZFailStencil>::bind(const SPtr<GpuParamBlockBuffer>& perCamera)
+	{
+		Vector4 lightPosAndScale(0, 0, 0, 0); // Not used
+		gShadowProjectVertParamsDef.gPositionAndScale.set(mVertParams, lightPosAndScale);
+
+		mParamsSet->setParamBlockBuffer("PerCamera", perCamera);
+
+		gRendererUtility().setPass(mMaterial);
+		gRendererUtility().setPassParams(mParamsSet);
+	}
+
+	void ShadowProjectStencilMaterials::bind(bool directional, bool useZFailStencil,
+		const SPtr<GpuParamBlockBuffer>& perCamera)
+	{
+		if(directional)
+		{
+			// Always uses z-fail stencil
+			mTT.bind(perCamera);
+		}
+		else
+		{
+			if (useZFailStencil)
+				mFT.bind(perCamera);
+			else
+				mFF.bind(perCamera);
+		}
+	}
 
 	template<int ShadowQuality, bool Directional, bool MSAA>
 	ShadowProjectMat<ShadowQuality, Directional, MSAA>::ShadowProjectMat()
@@ -105,6 +155,10 @@ namespace bs { namespace ct
 		desc.addressMode.w = TAM_CLAMP;
 
 		mSamplerState = SamplerState::create(desc);
+
+		mVertParams = gShadowProjectVertParamsDef.createBuffer();
+		if(params->hasParamBlock(GPT_VERTEX_PROGRAM, "VertParams"))
+			params->setParamBlockBuffer(GPT_VERTEX_PROGRAM, "VertParams", mVertParams);
 	}
 
 	template<int ShadowQuality, bool Directional, bool MSAA>
@@ -133,15 +187,59 @@ namespace bs { namespace ct
 		defines.set("MSAA_COUNT", MSAA ? 2 : 1); // Actual count doesn't matter, as long as its >1 if enabled
 	}
 
-	template<int ShadowQuality, bool Directional, bool MSAA>
-	void ShadowProjectMat<ShadowQuality, Directional, MSAA>::bind(const SPtr<Texture>& shadowMap, 
-		const SPtr<GpuParamBlockBuffer>& shadowParams, const SPtr<GpuParamBlockBuffer>& perCameraParams)
+	/**
+	 * Converts a point in mixed space (clip_x, clip_y, view_z, view_w) to UV coordinates on a shadow map (x, y),
+	 * and normalized linear depth from the shadow caster's perspective (z).
+	 */
+	Matrix4 createMixedToShadowUVMatrix(const Matrix4& viewP, const Matrix4& viewInvVP, const Rect2& shadowMapArea,
+		float depthRange, const Matrix4& shadowViewProj)
 	{
+		// Projects a point from (clip_x, clip_y, view_z, view_w) into clip space
+		Matrix4 mixedToShadow = Matrix4::IDENTITY;
+		mixedToShadow[2][2] = viewP[2][2];
+		mixedToShadow[2][3] = viewP[2][3];
+		mixedToShadow[3][2] = viewP[3][2];
+		mixedToShadow[3][3] = 0.0f;
+
+		// Projects a point in clip space back to homogeneus world space
+		mixedToShadow = viewInvVP * mixedToShadow;
+
+		// Projects a point in world space to shadow clip space
+		mixedToShadow = shadowViewProj * mixedToShadow;
+		
+		// Convert shadow clip space coordinates to UV coordinates relative to the shadow map rectangle, and normalize
+		// depth
+		RenderAPI& rapi = RenderAPI::instance();
+		const RenderAPIInfo& rapiInfo = rapi.getAPIInfo();
+
+		float flipY = -1.0f;
+		// Either of these flips the Y axis, but if they're both true they cancel out
+		if (rapiInfo.isFlagSet(RenderAPIFeatureFlag::UVYAxisUp) ^ rapiInfo.isFlagSet(RenderAPIFeatureFlag::NDCYAxisDown))
+			flipY = -flipY;
+
+		Matrix4 shadowMapTfrm
+		(
+			shadowMapArea.width * 0.5f, 0, 0, shadowMapArea.x + 0.5f,
+			0, flipY * shadowMapArea.height * 0.5f, 0, shadowMapArea.y + 0.5f,
+			0, 0, 1.0f / depthRange, 0,
+			0, 0, 0, 1
+		);
+
+		return shadowMapTfrm * mixedToShadow;
+	}
+
+	template <int ShadowQuality, bool Directional, bool MSAA>
+	void ShadowProjectMat<ShadowQuality, Directional, MSAA>::bind(const Light& light, const SPtr<Texture>& shadowMap, 
+		const SPtr<GpuParamBlockBuffer> shadowParams, const SPtr<GpuParamBlockBuffer>& perCamera)
+	{
+		Vector4 lightPosAndScale(light.getPosition(), light.getAttenuationRadius());
+		gShadowProjectVertParamsDef.gPositionAndScale.set(mVertParams, lightPosAndScale);
+
 		mShadowMapParam.set(shadowMap);
 		mShadowSamplerParam.set(mSamplerState);
 
 		mParamsSet->setParamBlockBuffer("Params", shadowParams);
-		mParamsSet->setParamBlockBuffer("PerCamera", perCameraParams);
+		mParamsSet->setParamBlockBuffer("PerCamera", perCamera);
 
 		gRendererUtility().setPass(mMaterial);
 		gRendererUtility().setPassParams(mParamsSet);
@@ -181,6 +279,10 @@ TEMPL_INSTANTIATE(3)
 		desc.comparisonFunc = CMPF_GREATER_EQUAL;
 
 		mSamplerState = SamplerState::create(desc);
+
+		mVertParams = gShadowProjectVertParamsDef.createBuffer();
+		if(params->hasParamBlock(GPT_VERTEX_PROGRAM, "VertParams"))
+			params->setParamBlockBuffer(GPT_VERTEX_PROGRAM, "VertParams", mVertParams);
 	}
 
 	template<int ShadowQuality, bool MSAA>
@@ -207,15 +309,18 @@ TEMPL_INSTANTIATE(3)
 		defines.set("MSAA_COUNT", MSAA ? 2 : 1); // Actual count doesn't matter, as long as its >1 if enabled
 	}
 
-	template<int ShadowQuality, bool MSAA>
-	void ShadowProjectOmniMat<ShadowQuality, MSAA>::bind(const SPtr<Texture>& shadowMap, 
-		const SPtr<GpuParamBlockBuffer>& shadowParams, const SPtr<GpuParamBlockBuffer>& perCameraParams)
+	template <int ShadowQuality, bool MSAA>
+	void ShadowProjectOmniMat<ShadowQuality, MSAA>::bind(const Light& light, const SPtr<Texture>& shadowMap, 
+		const SPtr<GpuParamBlockBuffer> shadowParams, const SPtr<GpuParamBlockBuffer>& perCamera)
 	{
+		Vector4 lightPosAndScale(light.getPosition(), light.getAttenuationRadius());
+		gShadowProjectVertParamsDef.gPositionAndScale.set(mVertParams, lightPosAndScale);
+
 		mShadowMapParam.set(shadowMap);
 		mShadowSamplerParam.set(mSamplerState);
 
 		mParamsSet->setParamBlockBuffer("Params", shadowParams);
-		mParamsSet->setParamBlockBuffer("PerCamera", perCameraParams);
+		mParamsSet->setParamBlockBuffer("PerCamera", perCamera);
 
 		gRendererUtility().setPass(mMaterial);
 		gRendererUtility().setPassParams(mParamsSet);
@@ -351,10 +456,62 @@ TEMPL_INSTANTIATE(3)
 	const UINT32 ShadowRendering::MIN_SHADOW_MAP_SIZE = 32;
 	const UINT32 ShadowRendering::SHADOW_MAP_FADE_SIZE = 64;
 	const UINT32 ShadowRendering::SHADOW_MAP_BORDER = 4;
+	const float ShadowRendering::CASCADE_FRACTION_FADE = 0.1f;
 
 	ShadowRendering::ShadowRendering(UINT32 shadowMapSize)
 		: mShadowMapSize(shadowMapSize)
-	{ }
+	{
+		SPtr<VertexDataDesc> vertexDesc = VertexDataDesc::create();
+		vertexDesc->addVertElem(VET_FLOAT3, VES_POSITION);
+
+		mPositionOnlyVD = VertexDeclaration::create(vertexDesc);
+
+		// Create plane index and vertex buffers
+		{
+			VERTEX_BUFFER_DESC vbDesc;
+			vbDesc.numVerts = 8;
+			vbDesc.usage = GBU_DYNAMIC;
+			vbDesc.vertexSize = mPositionOnlyVD->getProperties().getVertexSize(0);
+
+			mPlaneVB = VertexBuffer::create(vbDesc);
+
+			INDEX_BUFFER_DESC ibDesc;
+			ibDesc.indexType = IT_32BIT;
+			ibDesc.numIndices = 12;
+
+			mPlaneIB = IndexBuffer::create(ibDesc);
+
+			UINT32 indices[] =
+			{
+				// Far plane, back facing
+				4, 7, 6,
+				4, 6, 5,
+
+				// Near plane, front facing
+				0, 1, 2,
+				0, 2, 3
+			};
+
+			mPlaneIB->writeData(0, sizeof(indices), indices);
+		}
+
+		// Create frustum index and vertex buffers
+		{
+			VERTEX_BUFFER_DESC vbDesc;
+			vbDesc.numVerts = 8;
+			vbDesc.usage = GBU_DYNAMIC;
+			vbDesc.vertexSize = mPositionOnlyVD->getProperties().getVertexSize(0);
+
+			mFrustumVB = VertexBuffer::create(vbDesc);
+
+			INDEX_BUFFER_DESC ibDesc;
+			ibDesc.indexType = IT_32BIT;
+			ibDesc.numIndices = 36;
+
+			mFrustumIB = IndexBuffer::create(ibDesc);
+			mFrustumIB->writeData(0, sizeof(AABox::CUBE_INDICES), AABox::CUBE_INDICES);
+		}
+	}
 
 	void ShadowRendering::setShadowMapSize(UINT32 size)
 	{
@@ -511,23 +668,100 @@ TEMPL_INSTANTIATE(3)
 		}
 	}
 
+	/**
+	 * Generates a frustum from the provided view-projection matrix.
+	 * 
+	 * @param[in]	invVP			Inverse of the view-projection matrix to use for generating the frustum.
+	 * @param[out]	worldFrustum	Generated frustum planes, in world space.
+	 * @return						Individual vertices of the frustum corners, in world space. Ordered using the
+	 *								AABox::CornerEnum.
+	 */
+	std::array<Vector3, 8> getFrustum(const Matrix4& invVP, ConvexVolume& worldFrustum)
+	{
+		std::array<Vector3, 8> output;
+
+		RenderAPI& rapi = RenderAPI::instance();
+		const RenderAPIInfo& rapiInfo = rapi.getAPIInfo();
+
+		AABox frustumCube(
+			Vector3(-1, -1, rapiInfo.getMinimumDepthInputValue()),
+			Vector3(1, 1, rapiInfo.getMaximumDepthInputValue())
+		);
+
+		for(size_t i = 0; i < output.size(); i++)
+		{
+			Vector3 corner = frustumCube.getCorner((AABox::Corner)i);
+			output[i] = invVP.multiply(corner);
+		}
+
+		Vector<Plane> planes(6);
+		planes[FRUSTUM_PLANE_NEAR] = Plane(output[AABox::NEAR_LEFT_BOTTOM], output[AABox::NEAR_RIGHT_BOTTOM], output[AABox::NEAR_RIGHT_TOP]);
+		planes[FRUSTUM_PLANE_FAR] = Plane(output[AABox::FAR_LEFT_BOTTOM], output[AABox::FAR_LEFT_TOP], output[AABox::FAR_RIGHT_TOP]);
+		planes[FRUSTUM_PLANE_LEFT] = Plane(output[AABox::NEAR_LEFT_BOTTOM], output[AABox::NEAR_LEFT_TOP], output[AABox::FAR_LEFT_TOP]);
+		planes[FRUSTUM_PLANE_RIGHT] = Plane(output[AABox::FAR_RIGHT_TOP], output[AABox::NEAR_RIGHT_TOP], output[AABox::NEAR_RIGHT_BOTTOM]);
+		planes[FRUSTUM_PLANE_TOP] = Plane(output[AABox::NEAR_LEFT_TOP], output[AABox::NEAR_RIGHT_TOP], output[AABox::FAR_RIGHT_TOP]);
+		planes[FRUSTUM_PLANE_BOTTOM] = Plane(output[AABox::NEAR_LEFT_BOTTOM], output[AABox::FAR_LEFT_BOTTOM], output[AABox::FAR_RIGHT_BOTTOM]);
+
+		worldFrustum = ConvexVolume(planes);
+		return output;
+	}
+
 	void ShadowRendering::renderShadowOcclusion(const RendererScene& scene, const RendererLight& rendererLight, 
 		UINT32 viewIdx)
 	{
 		const Light* light = rendererLight.internal;
 		UINT32 lightIdx = light->getRendererId();
 
+		RendererView* view = scene.getSceneInfo().views[viewIdx];
+		auto viewProps = view->getProperties();
+
+		const Matrix4& viewP = viewProps.projTransform;
+		Matrix4 viewInvVP = viewProps.viewProjTransform.inverse();
+
+		SPtr<GpuParamBlockBuffer> perViewBuffer = view->getPerViewBuffer();
+
 		RenderAPI& rapi = RenderAPI::instance();
 		// TODO - Calculate and set a scissor rectangle for the light
 
-		switch (light->getType())
+		SPtr<GpuParamBlockBuffer> shadowParams = gShadowProjectParamsDef.createBuffer();
+		SPtr<GpuParamBlockBuffer> shadowOmniParams = gShadowProjectOmniParamsDef.createBuffer();
+
+		Vector<const ShadowInfo*> shadowInfos;
+
+		if(light->getType() == LightType::Radial)
 		{
-		case LightType::Directional: 
-			// TODO
-			break;
-		case LightType::Radial:
+			const LightShadows& shadows = mRadialLightShadows[lightIdx];
+
+			for(UINT32 i = 0; i < shadows.numShadows; ++i)
+			{
+				UINT32 shadowIdx = shadows.startIdx + i;
+				const ShadowInfo& shadowInfo = mShadowInfos[shadowIdx];
+
+				if (shadowInfo.fadePerView[viewIdx] < 0.005f)
+					continue;
+
+				for(UINT32 j = 0; j < 6; j++)
+					gShadowProjectOmniParamsDef.gFaceVPMatrices.set(shadowOmniParams, shadowInfo.shadowVPTransforms[j], j);
+
+				gShadowProjectOmniParamsDef.gDepthBias.set(shadowOmniParams, shadowInfo.depthBias);
+				gShadowProjectOmniParamsDef.gFadePercent.set(shadowOmniParams, shadowInfo.fadePerView[viewIdx]);
+				gShadowProjectOmniParamsDef.gInvResolution.set(shadowOmniParams, 1.0f / shadowInfo.area.width);
+
+				Vector4 lightPosAndRadius(light->getPosition(), light->getAttenuationRadius());
+				gShadowProjectOmniParamsDef.gLightPosAndRadius.set(shadowOmniParams, lightPosAndRadius);
+
+				// TODO - Bind material & render stencil geometry (also check if inside or outside of the volume?)
+
+				gRendererUtility().draw(gRendererUtility().getRadialLightStencil());
+			}
+		}
+		else // Directional & spot
+		{
+			shadowInfos.clear();
+
+			if(light->getType() == LightType::Spot)
 			{
-				const LightShadows& shadows = mRadialLightShadows[lightIdx];
+				const LightShadows& shadows = mSpotLightShadows[lightIdx];
 				for (UINT32 i = 0; i < shadows.numShadows; ++i)
 				{
 					UINT32 shadowIdx = shadows.startIdx + i;
@@ -536,16 +770,73 @@ TEMPL_INSTANTIATE(3)
 					if (shadowInfo.fadePerView[viewIdx] < 0.005f)
 						continue;
 
-					// TODO - Bind shader and necessary parameters
+					shadowInfos.push_back(&shadowInfo);
+				}
+			}
+			else // Directional
+			{
+				UINT32 mapIdx = mDirectionalLightShadows[lightIdx];
+				ShadowCascadedMap& cascadedMap = mCascadedShadowMaps[mapIdx];
+
+				for (UINT32 i = 0; i < NUM_CASCADE_SPLITS; i++)
+					shadowInfos.push_back(&cascadedMap.getShadowInfo(i));
+			}
+
+			for(auto& shadowInfo : shadowInfos)
+			{
+				Matrix4 mixedToShadowUV = createMixedToShadowUVMatrix(viewP, viewInvVP, shadowInfo->normArea, 
+					shadowInfo->depthRange, shadowInfo->shadowVPTransform);
+
+				Vector2 shadowMapSize((float)shadowInfo->area.width, (float)shadowInfo->area.height);
+				float transitionScale = getFadeTransition(*light, shadowInfo->depthRange, shadowInfo->area.width);
+
+				gShadowProjectParamsDef.gFadePercent.set(shadowParams, shadowInfo->fadePerView[viewIdx]);
+				gShadowProjectParamsDef.gFadePlaneDepth.set(shadowParams, shadowInfo->depthFade);
+				gShadowProjectParamsDef.gInvFadePlaneRange.set(shadowParams, 1.0f / shadowInfo->fadeRange);
+				gShadowProjectParamsDef.gMixedToShadowSpace.set(shadowParams, mixedToShadowUV);
+				gShadowProjectParamsDef.gShadowMapSize.set(shadowParams, shadowMapSize);
+				gShadowProjectParamsDef.gShadowMapSizeInv.set(shadowParams, 1.0f / shadowMapSize);
+				gShadowProjectParamsDef.gSoftTransitionScale.set(shadowParams, transitionScale);
+
+				// Generate a stencil buffer to avoid evaluating pixels without any receiver geometry in the shadow area
+				std::array<Vector3, 8> frustumVertices;
+				if(light->getType() == LightType::Spot)
+				{
+					ConvexVolume shadowFrustum;
+					frustumVertices = getFrustum(shadowInfo->shadowVPTransform.inverse(), shadowFrustum);
+
+					// Check if viewer is inside the frustum. Frustum is slightly expanded so that if the near plane is
+					// intersecting the shadow frustum, it is counted as inside. This needs to be conservative as the code
+					// for handling viewer outside the frustum will not properly render intersections with the near plane.
+					bool viewerInsideFrustum = shadowFrustum.contains(viewProps.viewOrigin, viewProps.nearPlane * 3.0f);
+
+					mProjectStencilMaterials.bind(false, viewerInsideFrustum, perViewBuffer);
+					drawFrustum(frustumVertices);
+				}
+				else
+				{
+					// Need to generate near and far planes to clip the geometry within the current CSM slice.
+					// Note: If the render API supports built-in depth bound tests that could be used instead.
+
+					Vector3 near = viewProps.projTransform.multiply(Vector3(0, 0, shadowInfo->depthNear));
+					Vector3 far = viewProps.projTransform.multiply(Vector3(0, 0, shadowInfo->depthFar));
 
+					mProjectStencilMaterials.bind(true, true, perViewBuffer);
+					drawNearFarPlanes(near.z, far.z, shadowInfo->cascadeIdx != 0);
 				}
+
+				// TODO - Update projection shaders so they test against the stencil buffer, and clear it to zero
+
+				// TODO - Update shaders so they set relevant blend states
+
+				// TODO - Bind material & render. Pick material depending on selected shadow quality, MSAA setting, light type
+				//      - Reduce shadow quality based on distance for CSM shadows
+
+				if(light->getType() == LightType::Spot)
+					drawFrustum(frustumVertices);
+				else
+					gRendererUtility().drawScreenQuad();
 			}
-			break;
-		case LightType::Spot: 
-			// TODO
-			break;
-		default: 
-			break;
 		}
 	}
 
@@ -601,21 +892,33 @@ TEMPL_INSTANTIATE(3)
 		for (int i = 0; i < NUM_CASCADE_SPLITS; ++i)
 		{
 			Sphere frustumBounds;
-			ConvexVolume cascadeCullVolume = getCSMSplitFrustum(*view, lightDir, i, NUM_CASCADE_SPLITS, frustumBounds);
+			ConvexVolume cascadeCullVolume = getCSMSplitFrustum(*view, -lightDir, i, NUM_CASCADE_SPLITS, frustumBounds);
 
 			float orthoSize = frustumBounds.getRadius();
 			Matrix4 proj = Matrix4::projectionOrthographic(-orthoSize, orthoSize, -orthoSize, orthoSize, 0.0f, 1000.0f);
 			RenderAPI::instance().convertProjectionMatrix(proj, proj);
 
+			shadowInfo.cascadeIdx = i;
 			shadowInfo.shadowVPTransform = proj * viewMat;
 
-			float nearDist = 0.05f;
-			float farDist = light->getAttenuationRadius();
-			float depthRange = farDist - nearDist;
-			float depthBias = getDepthBias(*light, depthRange, mapSize);
+			// Determine split range
+			float splitNear = getCSMSplitDistance(*view, i, NUM_CASCADE_SPLITS);
+			float splitFar = getCSMSplitDistance(*view, i + 1, NUM_CASCADE_SPLITS);
+
+			shadowInfo.depthNear = splitNear;
+			shadowInfo.depthFade = splitFar;
+			
+			if ((i + 1) < NUM_CASCADE_SPLITS)
+				shadowInfo.fadeRange = CASCADE_FRACTION_FADE * (shadowInfo.depthFade - shadowInfo.depthNear);
+			else
+				shadowInfo.fadeRange = 0.0f;
 
-			gShadowParamsDef.gDepthBias.set(shadowParamsBuffer, depthBias);
-			gShadowParamsDef.gDepthRange.set(shadowParamsBuffer, depthRange);
+			shadowInfo.depthFar = shadowInfo.depthFade + shadowInfo.fadeRange;
+			shadowInfo.depthRange = shadowInfo.depthFar - shadowInfo.depthNear;
+			shadowInfo.depthBias = getDepthBias(*light, shadowInfo.depthRange, mapSize);
+
+			gShadowParamsDef.gDepthBias.set(shadowParamsBuffer, shadowInfo.depthBias);
+			gShadowParamsDef.gDepthRange.set(shadowParamsBuffer, shadowInfo.depthRange);
 			gShadowParamsDef.gMatViewProj.set(shadowParamsBuffer, shadowInfo.shadowVPTransform);
 
 			rapi.setRenderTarget(shadowMap.getTarget(i));
@@ -660,6 +963,7 @@ TEMPL_INSTANTIATE(3)
 		ShadowInfo mapInfo;
 		mapInfo.fadePerView = options.fadePercents;
 		mapInfo.lightIdx = options.lightIdx;
+		mapInfo.cascadeIdx = -1;
 
 		bool foundSpace = false;
 		for (UINT32 i = 0; i < (UINT32)mDynamicShadowMaps.size(); i++)
@@ -692,10 +996,12 @@ TEMPL_INSTANTIATE(3)
 		rapi.setViewport(mapInfo.normArea);
 		rapi.clearViewport(FBT_DEPTH);
 
-		float nearDist = 0.05f;
-		float farDist = light->getAttenuationRadius();
-		float depthRange = farDist - nearDist;
-		float depthBias = getDepthBias(*light, depthRange, options.mapSize);
+		mapInfo.depthNear = 0.05f;
+		mapInfo.depthFar = light->getAttenuationRadius();
+		mapInfo.depthFade = mapInfo.depthFar;
+		mapInfo.fadeRange = 0.0f;
+		mapInfo.depthRange = mapInfo.depthFar - mapInfo.depthNear;
+		mapInfo.depthBias = getDepthBias(*light, mapInfo.depthRange, options.mapSize);
 
 		Matrix4 view = Matrix4::view(light->getPosition(), light->getRotation());
 		Matrix4 proj = Matrix4::projectionPerspective(light->getSpotAngle(), 1.0f, 0.05f, light->getAttenuationRadius());
@@ -703,8 +1009,8 @@ TEMPL_INSTANTIATE(3)
 
 		mapInfo.shadowVPTransform = proj * view;
 
-		gShadowParamsDef.gDepthBias.set(shadowParamsBuffer, depthBias);
-		gShadowParamsDef.gDepthRange.set(shadowParamsBuffer, depthRange);
+		gShadowParamsDef.gDepthBias.set(shadowParamsBuffer, mapInfo.depthBias);
+		gShadowParamsDef.gDepthRange.set(shadowParamsBuffer, mapInfo.depthRange);
 		gShadowParamsDef.gMatViewProj.set(shadowParamsBuffer, mapInfo.shadowVPTransform);
 
 		mDepthNormalMat.bind(shadowParamsBuffer);
@@ -766,6 +1072,7 @@ TEMPL_INSTANTIATE(3)
 		mapInfo.lightIdx = options.lightIdx;
 		mapInfo.textureIdx = -1;
 		mapInfo.fadePerView = options.fadePercents;
+		mapInfo.cascadeIdx = -1;
 
 		for (UINT32 i = 0; i < (UINT32)mShadowCubemaps.size(); i++)
 		{
@@ -791,18 +1098,20 @@ TEMPL_INSTANTIATE(3)
 
 		ShadowCubemap& cubemap = mShadowCubemaps[mapInfo.textureIdx];
 
-		float nearDist = 0.05f;
-		float farDist = light->getAttenuationRadius();
-		float depthRange = farDist - nearDist;
-		float depthBias = getDepthBias(*light, depthRange, options.mapSize);
+		mapInfo.depthNear = 0.05f;
+		mapInfo.depthFar = light->getAttenuationRadius();
+		mapInfo.depthFade = mapInfo.depthFar;
+		mapInfo.fadeRange = 0.0f;
+		mapInfo.depthRange = mapInfo.depthFar - mapInfo.depthNear;
+		mapInfo.depthBias = getDepthBias(*light, mapInfo.depthRange, options.mapSize);
 
 		Matrix4 proj = Matrix4::projectionPerspective(Degree(90.0f), 1.0f, 0.05f, light->getAttenuationRadius());
 		RenderAPI::instance().convertProjectionMatrix(proj, proj);
 
 		ConvexVolume localFrustum(proj);
 
-		gShadowParamsDef.gDepthBias.set(shadowParamsBuffer, depthBias);
-		gShadowParamsDef.gDepthRange.set(shadowParamsBuffer, depthRange);
+		gShadowParamsDef.gDepthBias.set(shadowParamsBuffer, mapInfo.depthBias);
+		gShadowParamsDef.gDepthRange.set(shadowParamsBuffer, mapInfo.depthRange);
 		gShadowParamsDef.gMatViewProj.set(shadowParamsBuffer, Matrix4::IDENTITY);
 
 		Matrix4 viewOffsetMat = Matrix4::translation(-light->getPosition());
@@ -949,6 +1258,56 @@ TEMPL_INSTANTIATE(3)
 		size = std::max(effectiveMapSize - 2 * SHADOW_MAP_BORDER, 1u);
 	}
 
+	void ShadowRendering::drawNearFarPlanes(float near, float far, bool drawNear)
+	{
+		RenderAPI& rapi = RenderAPI::instance();
+		const RenderAPIInfo& rapiInfo = rapi.getAPIInfo();
+
+		float flipY = rapiInfo.isFlagSet(RenderAPIFeatureFlag::NDCYAxisDown) ? -1.0f : 1.0f;
+
+		// Update VB with new vertices
+		Vector3 vertices[8] =
+		{
+			// Near plane
+			{ -1.0f, -1.0f * flipY, near },
+			{  1.0f, -1.0f * flipY, near },
+			{  1.0f,  1.0f * flipY, near },
+			{ -1.0f,  1.0f * flipY, near },
+
+			// Near plane
+			{ -1.0f, -1.0f * flipY, far },
+			{  1.0f, -1.0f * flipY, far },
+			{  1.0f,  1.0f * flipY, far },
+			{ -1.0f,  1.0f * flipY, far },
+		};
+
+		mPlaneVB->writeData(0, sizeof(vertices), vertices, BWT_DISCARD);
+
+		// Draw the mesh
+		rapi.setVertexDeclaration(mPositionOnlyVD);
+		rapi.setVertexBuffers(0, &mPlaneVB, 1);
+		rapi.setIndexBuffer(mPlaneIB);
+		rapi.setDrawOperation(DOT_TRIANGLE_LIST);
+
+		rapi.drawIndexed(drawNear ? 0 : 6, drawNear ? 12 : 6, 0, drawNear ? 8 : 4);
+	}
+
+	void ShadowRendering::drawFrustum(const std::array<Vector3, 8>& corners)
+	{
+		RenderAPI& rapi = RenderAPI::instance();
+
+		// Update VB with new vertices
+		mPlaneVB->writeData(0, sizeof(Vector3) * 8, corners.data(), BWT_DISCARD);
+
+		// Draw the mesh
+		rapi.setVertexDeclaration(mPositionOnlyVD);
+		rapi.setVertexBuffers(0, &mFrustumVB, 1);
+		rapi.setIndexBuffer(mFrustumIB);
+		rapi.setDrawOperation(DOT_TRIANGLE_LIST);
+
+		rapi.drawIndexed(0, 36, 0, 8);
+	}
+
 	ConvexVolume ShadowRendering::getCSMSplitFrustum(const RendererView& view, const Vector3& lightDir, UINT32 cascade, 
 		UINT32 numCascades, Sphere& outBounds)
 	{
@@ -1159,4 +1518,29 @@ TEMPL_INSTANTIATE(3)
 		
 		return defaultBias * light.getShadowBias() *resolutionScale * rangeScale;
 	}
+
+	float ShadowRendering::getFadeTransition(const Light& light, float depthRange, UINT32 mapSize)
+	{
+		const static float SPOT_LIGHT_SCALE = 1.0f / 50.0f;
+		const static float DIR_LIGHT_SCALE = 20.0f;
+
+		// Note: Currently fade transitions are only used in spot & directional (non omni-directional) lights, so no need
+		// to account for radial light type.
+		if (light.getType() == LightType::Directional)
+		{
+			// Reduce the size of the transition region when shadow map resolution is higher
+			float resolutionScale = 1.0f / (float)mapSize;
+
+			// Reduce the size of the transition region when the depth range is larger
+			float rangeScale = 1.0f / depthRange;
+
+			// Increase the size of the transition region for larger lights
+			float radiusScale = light.getBounds().getRadius();
+
+			return light.getShadowBias() * DIR_LIGHT_SCALE * rangeScale * resolutionScale * radiusScale;
+		}
+		else
+			return light.getShadowBias() * SPOT_LIGHT_SCALE;
+	}
+
 }}