/// <summary> /// Makes the "global" shadow matrix used as the reference point for the cascades. /// </summary> private Matrix MakeGlobalShadowMatrix(Camera camera, Vector3 surfaceToLightDirection) { ResetViewFrustumCorners(); var invViewProj = camera.InverseViewProjection; var frustumCenter = Vector3.Zero; for (var i = 0; i < 8; i++) { this.FrustumCorners[i] = Vector4.Transform(this.FrustumCorners[i], invViewProj).ScaleToVector3(); frustumCenter += this.FrustumCorners[i]; } frustumCenter /= 8.0f; var shadowCameraPos = frustumCenter + surfaceToLightDirection * -0.5f; var shadowCamera = new OrthographicShadowCamera(-0.5f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f); shadowCamera.SetLookAt(shadowCameraPos, frustumCenter, Vector3.Up); var texScaleBias = Matrix.CreateScale(0.5f, -0.5f, 1.0f); texScaleBias.Translation = new Vector3(0.5f, 0.5f, 0.0f); return(shadowCamera.ViewProjection * texScaleBias); }
public void RenderShadowMaps(IEnumerable <Sunlight> lights, IScene geometry, Camera camera) { var originalViewport = this.Device.Viewport; using (this.Device.ShadowMapState()) { foreach (var light in lights) { light.GlobalShadowMatrix = MakeGlobalShadowMatrix(camera, light.SurfaceToLightDirection); for (var cascadeIndex = 0; cascadeIndex < Sunlight.Cascades; cascadeIndex++) { // Set the rendertarget and clear it to white (max distance) this.Device.SetRenderTarget(light.ShadowMap, cascadeIndex); this.Device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.White, 1.0f, 0); // Get the 8 points of the view frustum in world space ResetViewFrustumCorners(); // Get the range this cascade covers var prevSplitDist = cascadeIndex == 0 ? 0.0f : light.CascadeSplits[cascadeIndex - 1]; var splitDist = light.CascadeSplits[cascadeIndex]; var invViewProj = camera.InverseViewProjection; for (var i = 0; i < 8; i++) { this.FrustumCorners[i] = Vector4.Transform(this.FrustumCorners[i], invViewProj).ScaleToVector3(); } // Compute the corners of this slice of the frustum by taking a slice // of the identity frustum for (var i = 0; i < 4; i++) { var cornerRay = this.FrustumCorners[i + 4] - this.FrustumCorners[i]; var nearCornerRay = cornerRay * prevSplitDist; var farCornerRay = cornerRay * splitDist; this.FrustumCorners[i + 4] = this.FrustumCorners[i] + farCornerRay; this.FrustumCorners[i] = this.FrustumCorners[i] + nearCornerRay; } // Calculate the centroid of the view frustum slice var frustumCenter = Vector3.Zero; for (var i = 0; i < 8; i++) { frustumCenter = frustumCenter + this.FrustumCorners[i]; } frustumCenter /= 8.0f; // Calculate the bounding sphere of the view frustum so we can stabilize it later // TODO: can we replace this logic with XNA's bounding sphere? var sphereRadius = 0.0f; for (var i = 0; i < 8; i++) { var dist = (this.FrustumCorners[i] - frustumCenter).Length(); sphereRadius = Math.Max(sphereRadius, dist); } // Slightly 'round' it sphereRadius = (float)Math.Ceiling(sphereRadius * 16.0f) / 16.0f; var maxExtents = new Vector3(sphereRadius); var minExtents = -maxExtents; var cascadeExtents = maxExtents - minExtents; // Compute the position of the shadow camera (the position where we're going to capture our shadow maps from) var shadowCameraPos = frustumCenter + light.SurfaceToLightDirection * -minExtents.Z; var shadowCamera = new OrthographicShadowCamera( minExtents.X, minExtents.Y, maxExtents.X, maxExtents.Y, 0.0f, cascadeExtents.Z); shadowCamera.SetLookAt(shadowCameraPos, frustumCenter, Vector3.Up); // STABILIZE // Create the rounding matrix, by projecting the world-space origin and determining // the fractional offset in texel space var shadowMatrixTemp = shadowCamera.ViewProjection; var shadowOrigin = new Vector4(0.0f, 0.0f, 0.0f, 1.0f); shadowOrigin = Vector4.Transform(shadowOrigin, shadowMatrixTemp); shadowOrigin = shadowOrigin * (Sunlight.Resolution / 2.0f); var roundedOrigin = shadowOrigin.Round(); var roundOffset = roundedOrigin - shadowOrigin; roundOffset = roundOffset * (2.0f / Sunlight.Resolution); roundOffset.Z = 0.0f; roundOffset.W = 0.0f; var shadowProj = shadowCamera.Projection; //shadowProj.r[3] = shadowProj.r[3] + roundOffset; shadowProj.M41 += roundOffset.X; shadowProj.M42 += roundOffset.Y; shadowProj.M43 += roundOffset.Z; shadowProj.M44 += roundOffset.W; shadowCamera.Projection = shadowProj; // STABILIZE geometry.Draw(this.CascadingShadowMapEffect, shadowCamera); this.Device.SetRenderTarget(null); // Calculate the matrix which transforms from [-1, 1] to // [0, 1] space. var texScaleBias = Matrix.CreateScale(0.5f, -0.5f, 1.0f) * Matrix.CreateTranslation(0.5f, 0.5f, 0.0f); var shadowMatrix = shadowCamera.ViewProjection; shadowMatrix = shadowMatrix * texScaleBias; // Store the split distance in terms of view space depth var clipDist = camera.FarPlane - camera.NearPlane; light.CascadeSplitsUV[cascadeIndex] = camera.NearPlane + splitDist * clipDist; // Calculate the position of the lower corner of the cascade partition in the UV space of the // first cascade partition var invCascadeMat = Matrix.Invert(shadowMatrix); var cascadeCorner = Vector4.Transform(Vector3.Zero, invCascadeMat).ScaleToVector3(); cascadeCorner = Vector4.Transform(cascadeCorner, light.GlobalShadowMatrix).ScaleToVector3(); // Do the same for the upper corner var otherCorner = Vector4.Transform(Vector3.One, invCascadeMat).ScaleToVector3(); otherCorner = Vector4.Transform(otherCorner, light.GlobalShadowMatrix).ScaleToVector3(); // Calculate the scale and offset var cascadeScale = Vector3.One / (otherCorner - cascadeCorner); light.CascadeOffsets[cascadeIndex] = new Vector4(-cascadeCorner, 0.0f); light.CascadeScales[cascadeIndex] = new Vector4(cascadeScale, 1.0f); } } } this.Device.Viewport = originalViewport; }