using System; namespace UnityEngine.Rendering.Universal { public struct ShadowSliceData { public Matrix4x4 viewMatrix; public Matrix4x4 projectionMatrix; public Matrix4x4 shadowTransform; public int offsetX; public int offsetY; public int resolution; public ShadowSplitData splitData; // splitData contains culling information public void Clear() { viewMatrix = Matrix4x4.identity; projectionMatrix = Matrix4x4.identity; shadowTransform = Matrix4x4.identity; offsetX = offsetY = 0; resolution = 1024; } } public static class ShadowUtils { private static readonly bool m_ForceShadowPointSampling; static ShadowUtils() { m_ForceShadowPointSampling = SystemInfo.graphicsDeviceType == GraphicsDeviceType.Metal && GraphicsSettings.HasShaderDefine(Graphics.activeTier, BuiltinShaderDefine.UNITY_METAL_SHADOWS_USE_POINT_FILTERING); } public static bool ExtractDirectionalLightMatrix(ref CullingResults cullResults, ref ShadowData shadowData, int shadowLightIndex, int cascadeIndex, int shadowmapWidth, int shadowmapHeight, int shadowResolution, float shadowNearPlane, out Vector4 cascadeSplitDistance, out ShadowSliceData shadowSliceData, out Matrix4x4 viewMatrix, out Matrix4x4 projMatrix) { bool result = ExtractDirectionalLightMatrix(ref cullResults, ref shadowData, shadowLightIndex, cascadeIndex, shadowmapWidth, shadowmapHeight, shadowResolution, shadowNearPlane, out cascadeSplitDistance, out shadowSliceData); viewMatrix = shadowSliceData.viewMatrix; projMatrix = shadowSliceData.projectionMatrix; return result; } public static bool ExtractDirectionalLightMatrix(ref CullingResults cullResults, ref ShadowData shadowData, int shadowLightIndex, int cascadeIndex, int shadowmapWidth, int shadowmapHeight, int shadowResolution, float shadowNearPlane, out Vector4 cascadeSplitDistance, out ShadowSliceData shadowSliceData) { bool success = cullResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(shadowLightIndex, cascadeIndex, shadowData.mainLightShadowCascadesCount, shadowData.mainLightShadowCascadesSplit, shadowResolution, shadowNearPlane, out shadowSliceData.viewMatrix, out shadowSliceData.projectionMatrix, out shadowSliceData.splitData); cascadeSplitDistance = shadowSliceData.splitData.cullingSphere; shadowSliceData.offsetX = (cascadeIndex % 2) * shadowResolution; shadowSliceData.offsetY = (cascadeIndex / 2) * shadowResolution; shadowSliceData.resolution = shadowResolution; shadowSliceData.shadowTransform = GetShadowTransform(shadowSliceData.projectionMatrix, shadowSliceData.viewMatrix); // It is the culling sphere radius multiplier for shadow cascade blending // If this is less than 1.0, then it will begin to cull castors across cascades shadowSliceData.splitData.shadowCascadeBlendCullingFactor = 1.0f; // If we have shadow cascades baked into the atlas we bake cascade transform // in each shadow matrix to save shader ALU and L/S if (shadowData.mainLightShadowCascadesCount > 1) ApplySliceTransform(ref shadowSliceData, shadowmapWidth, shadowmapHeight); return success; } public static bool ExtractSpotLightMatrix(ref CullingResults cullResults, ref ShadowData shadowData, int shadowLightIndex, out Matrix4x4 shadowMatrix, out Matrix4x4 viewMatrix, out Matrix4x4 projMatrix, out ShadowSplitData splitData) { bool success = cullResults.ComputeSpotShadowMatricesAndCullingPrimitives(shadowLightIndex, out viewMatrix, out projMatrix, out splitData); // returns false if input parameters are incorrect (rare) shadowMatrix = GetShadowTransform(projMatrix, viewMatrix); return success; } public static bool ExtractPointLightMatrix(ref CullingResults cullResults, ref ShadowData shadowData, int shadowLightIndex, CubemapFace cubemapFace, float fovBias, out Matrix4x4 shadowMatrix, out Matrix4x4 viewMatrix, out Matrix4x4 projMatrix, out ShadowSplitData splitData) { bool success = cullResults.ComputePointShadowMatricesAndCullingPrimitives(shadowLightIndex, cubemapFace, fovBias, out viewMatrix, out projMatrix, out splitData); // returns false if input parameters are incorrect (rare) // In native API CullingResults.ComputeSpotShadowMatricesAndCullingPrimitives there is code that inverts the 3rd component of shadow-casting spot light's "world-to-local" matrix (it was so since its original addition to the code base): // https://github.cds.internal.unity3d.com/unity/unity/commit/34813e063526c4be0ef0448dfaae3a911dd8be58#diff-cf0b417fc6bd8ee2356770797e628cd4R331 // (the same transformation has also always been used in the Built-In Render Pipeline) // // However native API CullingResults.ComputePointShadowMatricesAndCullingPrimitives does not contain this transformation. // As a result, the view matrices returned for a point light shadow face, and for a spot light with same direction as that face, have opposite 3rd component. // // This causes normalBias to be incorrectly applied to shadow caster vertices during the point light shadow pass. // To counter this effect, we invert the point light shadow view matrix component here: { viewMatrix.m10 = -viewMatrix.m10; viewMatrix.m11 = -viewMatrix.m11; viewMatrix.m12 = -viewMatrix.m12; viewMatrix.m13 = -viewMatrix.m13; } shadowMatrix = GetShadowTransform(projMatrix, viewMatrix); return success; } public static void RenderShadowSlice(CommandBuffer cmd, ref ScriptableRenderContext context, ref ShadowSliceData shadowSliceData, ref ShadowDrawingSettings settings, Matrix4x4 proj, Matrix4x4 view) { cmd.SetGlobalDepthBias(1.0f, 2.5f); // these values match HDRP defaults (see https://github.com/Unity-Technologies/Graphics/blob/9544b8ed2f98c62803d285096c91b44e9d8cbc47/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDShadowAtlas.cs#L197 ) cmd.SetViewport(new Rect(shadowSliceData.offsetX, shadowSliceData.offsetY, shadowSliceData.resolution, shadowSliceData.resolution)); cmd.SetViewProjectionMatrices(view, proj); context.ExecuteCommandBuffer(cmd); cmd.Clear(); context.DrawShadows(ref settings); cmd.DisableScissorRect(); context.ExecuteCommandBuffer(cmd); cmd.Clear(); cmd.SetGlobalDepthBias(0.0f, 0.0f); // Restore previous depth bias values } public static void RenderShadowSlice(CommandBuffer cmd, ref ScriptableRenderContext context, ref ShadowSliceData shadowSliceData, ref ShadowDrawingSettings settings) { RenderShadowSlice(cmd, ref context, ref shadowSliceData, ref settings, shadowSliceData.projectionMatrix, shadowSliceData.viewMatrix); } public static int GetMaxTileResolutionInAtlas(int atlasWidth, int atlasHeight, int tileCount) { int resolution = Mathf.Min(atlasWidth, atlasHeight); int currentTileCount = atlasWidth / resolution * atlasHeight / resolution; while (currentTileCount < tileCount) { resolution = resolution >> 1; currentTileCount = atlasWidth / resolution * atlasHeight / resolution; } return resolution; } public static void ApplySliceTransform(ref ShadowSliceData shadowSliceData, int atlasWidth, int atlasHeight) { Matrix4x4 sliceTransform = Matrix4x4.identity; float oneOverAtlasWidth = 1.0f / atlasWidth; float oneOverAtlasHeight = 1.0f / atlasHeight; sliceTransform.m00 = shadowSliceData.resolution * oneOverAtlasWidth; sliceTransform.m11 = shadowSliceData.resolution * oneOverAtlasHeight; sliceTransform.m03 = shadowSliceData.offsetX * oneOverAtlasWidth; sliceTransform.m13 = shadowSliceData.offsetY * oneOverAtlasHeight; // Apply shadow slice scale and offset shadowSliceData.shadowTransform = sliceTransform * shadowSliceData.shadowTransform; } public static Vector4 GetShadowBias(ref VisibleLight shadowLight, int shadowLightIndex, ref ShadowData shadowData, Matrix4x4 lightProjectionMatrix, float shadowResolution) { if (shadowLightIndex < 0 || shadowLightIndex >= shadowData.bias.Count) { Debug.LogWarning(string.Format("{0} is not a valid light index.", shadowLightIndex)); return Vector4.zero; } float frustumSize; if (shadowLight.lightType == LightType.Directional) { // Frustum size is guaranteed to be a cube as we wrap shadow frustum around a sphere frustumSize = 2.0f / lightProjectionMatrix.m00; } else if (shadowLight.lightType == LightType.Spot) { // For perspective projections, shadow texel size varies with depth // It will only work well if done in receiver side in the pixel shader. Currently UniversalRP // do bias on caster side in vertex shader. When we add shader quality tiers we can properly // handle this. For now, as a poor approximation we do a constant bias and compute the size of // the frustum as if it was orthogonal considering the size at mid point between near and far planes. // Depending on how big the light range is, it will be good enough with some tweaks in bias frustumSize = Mathf.Tan(shadowLight.spotAngle * 0.5f * Mathf.Deg2Rad) * shadowLight.range; // half-width (in world-space units) of shadow frustum's "far plane" } else if (shadowLight.lightType == LightType.Point) { // [Copied from above case:] // "For perspective projections, shadow texel size varies with depth // It will only work well if done in receiver side in the pixel shader. Currently UniversalRP // do bias on caster side in vertex shader. When we add shader quality tiers we can properly // handle this. For now, as a poor approximation we do a constant bias and compute the size of // the frustum as if it was orthogonal considering the size at mid point between near and far planes. // Depending on how big the light range is, it will be good enough with some tweaks in bias" // Note: HDRP uses normalBias both in HDShadowUtils.CalcGuardAnglePerspective and HDShadowAlgorithms/EvalShadow_NormalBias (receiver bias) float fovBias = Internal.AdditionalLightsShadowCasterPass.GetPointLightShadowFrustumFovBiasInDegrees((int)shadowResolution, (shadowLight.light.shadows == LightShadows.Soft)); // Note: the same fovBias was also used to compute ShadowUtils.ExtractPointLightMatrix float cubeFaceAngle = 90 + fovBias; frustumSize = Mathf.Tan(cubeFaceAngle * 0.5f * Mathf.Deg2Rad) * shadowLight.range; // half-width (in world-space units) of shadow frustum's "far plane" } else { Debug.LogWarning("Only point, spot and directional shadow casters are supported in universal pipeline"); frustumSize = 0.0f; } // depth and normal bias scale is in shadowmap texel size in world space float texelSize = frustumSize / shadowResolution; float depthBias = -shadowData.bias[shadowLightIndex].x * texelSize; float normalBias = -shadowData.bias[shadowLightIndex].y * texelSize; // The current implementation of NormalBias in Universal RP is the same as in Unity Built-In RP (i.e moving shadow caster vertices along normals when projecting them to the shadow map). // This does not work well with Point Lights, which is why NormalBias value is hard-coded to 0.0 in Built-In RP (see value of unity_LightShadowBias.z in FrameDebugger, and native code that sets it: https://github.cds.internal.unity3d.com/unity/unity/blob/a9c916ba27984da43724ba18e70f51469e0c34f5/Runtime/Camera/Shadows.cpp#L1686 ) // We follow the same convention in Universal RP: if (shadowLight.lightType == LightType.Point) normalBias = 0.0f; if (shadowData.supportsSoftShadows && shadowLight.light.shadows == LightShadows.Soft) { // TODO: depth and normal bias assume sample is no more than 1 texel away from shadowmap // This is not true with PCF. Ideally we need to do either // cone base bias (based on distance to center sample) // or receiver place bias based on derivatives. // For now we scale it by the PCF kernel size of non-mobile platforms (5x5) const float kernelRadius = 2.5f; depthBias *= kernelRadius; normalBias *= kernelRadius; } return new Vector4(depthBias, normalBias, 0.0f, 0.0f); } /// /// Extract scale and bias from a fade distance to achieve a linear fading of the fade distance. /// /// Distance at which object should be totally fade /// Normalized distance of fade /// [OUT] Slope of the fading on the fading part /// [OUT] Ordinate of the fading part at abscissa 0 internal static void GetScaleAndBiasForLinearDistanceFade(float fadeDistance, float border, out float scale, out float bias) { // To avoid division from zero // This values ensure that fade within cascade will be 0 and outside 1 if (border < 0.0001f) { float multiplier = 1000f; // To avoid blending if difference is in fractions scale = multiplier; bias = -fadeDistance * multiplier; return; } border = 1 - border; border *= border; // Fade with distance calculation is just a linear fade from 90% of fade distance to fade distance. 90% arbitrarily chosen but should work well enough. float distanceFadeNear = border * fadeDistance; scale = 1.0f / (fadeDistance - distanceFadeNear); bias = -distanceFadeNear / (fadeDistance - distanceFadeNear); } public static void SetupShadowCasterConstantBuffer(CommandBuffer cmd, ref VisibleLight shadowLight, Vector4 shadowBias) { cmd.SetGlobalVector("_ShadowBias", shadowBias); // Light direction is currently used in shadow caster pass to apply shadow normal offset (normal bias). Vector3 lightDirection = -shadowLight.localToWorldMatrix.GetColumn(2); cmd.SetGlobalVector("_LightDirection", new Vector4(lightDirection.x, lightDirection.y, lightDirection.z, 0.0f)); // For punctual lights, computing light direction at each vertex position provides more consistent results (shadow shape does not change when "rotating the point light" for example) Vector3 lightPosition = shadowLight.localToWorldMatrix.GetColumn(3); cmd.SetGlobalVector("_LightPosition", new Vector4(lightPosition.x, lightPosition.y, lightPosition.z, 1.0f)); } public static RenderTexture GetTemporaryShadowTexture(int width, int height, int bits) { var format = Experimental.Rendering.GraphicsFormatUtility.GetDepthStencilFormat(bits, 0); RenderTextureDescriptor rtd = new RenderTextureDescriptor(width, height, Experimental.Rendering.GraphicsFormat.None, format); rtd.shadowSamplingMode = (RenderingUtils.SupportsRenderTextureFormat(RenderTextureFormat.Shadowmap) && (SystemInfo.graphicsDeviceType != GraphicsDeviceType.OpenGLES2)) ? ShadowSamplingMode.CompareDepths : ShadowSamplingMode.None; var shadowTexture = RenderTexture.GetTemporary(rtd); shadowTexture.filterMode = m_ForceShadowPointSampling ? FilterMode.Point : FilterMode.Bilinear; shadowTexture.wrapMode = TextureWrapMode.Clamp; return shadowTexture; } static Matrix4x4 GetShadowTransform(Matrix4x4 proj, Matrix4x4 view) { // Currently CullResults ComputeDirectionalShadowMatricesAndCullingPrimitives doesn't // apply z reversal to projection matrix. We need to do it manually here. if (SystemInfo.usesReversedZBuffer) { proj.m20 = -proj.m20; proj.m21 = -proj.m21; proj.m22 = -proj.m22; proj.m23 = -proj.m23; } Matrix4x4 worldToShadow = proj * view; var textureScaleAndBias = Matrix4x4.identity; textureScaleAndBias.m00 = 0.5f; textureScaleAndBias.m11 = 0.5f; textureScaleAndBias.m22 = 0.5f; textureScaleAndBias.m03 = 0.5f; textureScaleAndBias.m23 = 0.5f; textureScaleAndBias.m13 = 0.5f; // textureScaleAndBias maps texture space coordinates from [-1,1] to [0,1] // Apply texture scale and offset to save a MAD in shader. return textureScaleAndBias * worldToShadow; } } }