void SetDownsampleBlurOffsetParams(BlurParam blurH, BlurParam blurV, int w, int h)
        {
            float invW     = 0.5f / w;
            float invH     = 0.5f / h;
            float offsetX0 = invW * blurH.offset.x;
            float offsetX1 = invW * blurH.offset.y;
            float offsetY0 = invH * blurV.offset.x;
            float offsetY1 = invH * blurV.offset.y;

            m_downsampleShader.SetVector(s_downSampleBlurOffset0ParamID, new Vector4(offsetX0, offsetY0, -offsetX0, -offsetY0));
            m_downsampleShader.SetVector(s_downSampleBlurOffset1ParamID, new Vector4(offsetX0, offsetY1, -offsetX0, -offsetY1));
            m_downsampleShader.SetVector(s_downSampleBlurOffset2ParamID, new Vector4(offsetX1, offsetY0, -offsetX1, -offsetY0));
            m_downsampleShader.SetVector(s_downSampleBlurOffset3ParamID, new Vector4(offsetX1, offsetY1, -offsetX1, -offsetY1));
        }
        void OnPostRender()
        {
#if UNITY_EDITOR
            if (!(Application.isPlaying || m_isRenderingFromUpdate))
            {
                return;
            }
#endif
            m_camera.clearFlags = CameraClearFlags.Nothing;
            if (!m_isVisible)
            {
                return;
            }
            RenderTexture srcRT = m_camera.targetTexture;
            m_camera.targetTexture = null;
            if (m_superSampling != TextureSuperSample.x1 || HasShadowColor())
            {
                m_downsampleShader.color = m_shadowColor;
                // downsample
                RenderTexture dstRT;
                if (0 < m_blurLevel)
                {
                    dstRT            = RenderTexture.GetTemporary(m_textureWidth, m_textureHeight, 0, m_shadowTexture.format, RenderTextureReadWrite.Linear);
                    dstRT.filterMode = FilterMode.Bilinear;
                }
                else
                {
                    dstRT = m_shadowTexture;
                }
                Graphics.SetRenderTarget(dstRT);
                int pass = m_superSampling == TextureSuperSample.x16 ? 0 : 2;
                Graphics.Blit(srcRT, dstRT, m_downsampleShader, HasShadowColor() ? pass + 1 : pass);
                RenderTexture.ReleaseTemporary(srcRT);
                srcRT = dstRT;
            }
            if (0 < m_blurLevel)
            {
                // adjust blur size according to texel aspect
                float texelAspect = (m_projector.aspectRatio * m_textureHeight) / (float)m_textureWidth;
                float blurSizeH   = m_blurSize;
                float blurSizeV   = m_blurSize;
                if (texelAspect < 1.0f)
                {
                    blurSizeV *= texelAspect;
                }
                else
                {
                    blurSizeH /= texelAspect;
                }
                // blur parameters
                BlurParam blurH = GetBlurParam(blurSizeH, m_blurFilter);
                BlurParam blurV = GetBlurParam(blurSizeV, m_blurFilter);
                blurH.tap = (blurH.tap - 3);                 // index of pass
                blurV.tap = (blurV.tap - 3) + 1;             // index of pass
                m_blurShader.SetVector(s_blurOffsetHParamID, blurH.offset);
                m_blurShader.SetVector(s_blurOffsetVParamID, blurV.offset);
                m_blurShader.SetVector(s_blurWeightHParamID, blurH.weight);
                m_blurShader.SetVector(s_blurWeightVParamID, blurV.weight);

                RenderTexture dstRT = RenderTexture.GetTemporary(m_textureWidth, m_textureHeight, 0, m_shadowTexture.format, RenderTextureReadWrite.Linear);
                dstRT.filterMode = FilterMode.Bilinear;
                srcRT.wrapMode   = TextureWrapMode.Clamp;
                dstRT.wrapMode   = TextureWrapMode.Clamp;
                Graphics.Blit(srcRT, dstRT, m_blurShader, blurH.tap);
                if (1 < srcRT.antiAliasing)
                {
                    RenderTexture.ReleaseTemporary(srcRT);
                    srcRT = RenderTexture.GetTemporary(m_textureWidth, m_textureHeight, 0, m_shadowTexture.format, RenderTextureReadWrite.Linear);
                }
                else
                {
                    srcRT.DiscardContents();
                }
                for (int i = 1; i < m_blurLevel - 1; ++i)
                {
                    Graphics.Blit(dstRT, srcRT, m_blurShader, blurV.tap);
                    dstRT.DiscardContents();
                    Graphics.Blit(srcRT, dstRT, m_blurShader, blurH.tap);
                    srcRT.DiscardContents();
                }
                RenderTexture.ReleaseTemporary(srcRT);
                srcRT = m_shadowTexture;
                Graphics.Blit(dstRT, srcRT, m_blurShader, blurV.tap);
                RenderTexture.ReleaseTemporary(dstRT);
            }
            Graphics.SetRenderTarget(m_shadowTexture);
            if (srcRT != m_shadowTexture)
            {
                Graphics.Blit(srcRT, m_downsampleShader, 2);
                if (m_mipLevel == 0)
                {
                    RenderTexture.ReleaseTemporary(srcRT);
                }
            }
            EraseShadowOnBoarder(m_textureWidth, m_textureHeight);
            if (0 < m_mipLevel)
            {
                // setup blur parameters
                BlurParam blurH = new BlurParam(), blurV = new BlurParam();
                if (0.1f < m_mipmapBlurSize)
                {
                    // adjust blur size according to texel aspect
                    float texelAspect = (m_projector.aspectRatio * m_textureHeight) / (float)m_textureWidth;
                    float blurSizeH   = m_mipmapBlurSize;
                    float blurSizeV   = m_mipmapBlurSize;
                    if (texelAspect < 1.0f)
                    {
                        blurSizeV *= texelAspect;
                    }
                    else
                    {
                        blurSizeH /= texelAspect;
                    }
                    // blur parameters
                    if (m_singlePassMipmapBlur)
                    {
                        blurH = GetDownsampleBlurParam(2.0f + 2.0f * blurSizeH, m_blurFilter);
                        blurV = GetDownsampleBlurParam(2.0f + 2.0f * blurSizeV, m_blurFilter);
                        Vector4 weight = new Vector4(blurH.weight.x * blurV.weight.x, blurH.weight.x * blurV.weight.y, blurH.weight.y * blurV.weight.x, blurH.weight.y * blurV.weight.y);
                        float   a      = 0.25f / (weight.x + weight.y + weight.z + weight.w);
                        weight.x = Mathf.Round(255 * a * weight.x) / 255.0f;
                        weight.y = Mathf.Round(255 * a * weight.y) / 255.0f;
                        weight.z = Mathf.Round(255 * a * weight.z) / 255.0f;
                        weight.w = 0.25f - weight.x - weight.y - weight.z;
                        m_downsampleShader.SetVector(s_downSampleBlurWeightParamID, weight);
                    }
                    else
                    {
                        blurH     = GetBlurParam(blurSizeH, m_blurFilter);
                        blurV     = GetBlurParam(blurSizeV, m_blurFilter);
                        blurH.tap = (blurH.tap - 3);                         // index of pass
                        blurV.tap = (blurV.tap - 3) + 1;                     // index of pass
                        m_blurShader.SetVector(s_blurOffsetHParamID, blurH.offset);
                        m_blurShader.SetVector(s_blurOffsetVParamID, blurV.offset);
                        m_blurShader.SetVector(s_blurWeightHParamID, blurH.weight);
                        m_blurShader.SetVector(s_blurWeightVParamID, blurV.weight);
                    }
                }
                int           w      = m_textureWidth >> 1;
                int           h      = m_textureHeight >> 1;
                RenderTexture tempRT = RenderTexture.GetTemporary(w, h, 0, m_shadowTexture.format, RenderTextureReadWrite.Linear);
                tempRT.filterMode = FilterMode.Bilinear;
                bool downSampleWithBlur = m_singlePassMipmapBlur && 0.1f < m_mipmapBlurSize;
                if (downSampleWithBlur)
                {
                    SetDownsampleBlurOffsetParams(blurH, blurV, w, h);
                }
                if (srcRT == m_shadowTexture)
                {
                    if (downSampleWithBlur)
                    {
                        Graphics.Blit(srcRT, tempRT, m_downsampleShader, 5);
                    }
                    else
                    {
                        Graphics.Blit(srcRT, tempRT, m_copyMipmapShader, 1);
                    }
                }
                else
                {
                    Graphics.Blit(srcRT, tempRT, m_downsampleShader, downSampleWithBlur ? 4 : 0);
                    RenderTexture.ReleaseTemporary(srcRT);
                }
                srcRT = tempRT;
                int   i       = 0;
                float falloff = 1.0f;
                for ( ; ;)
                {
                    if (0.1f < m_mipmapBlurSize && !m_singlePassMipmapBlur)
                    {
                        tempRT            = RenderTexture.GetTemporary(w, h, 0, m_shadowTexture.format, RenderTextureReadWrite.Linear);
                        tempRT.filterMode = FilterMode.Bilinear;
                        tempRT.wrapMode   = TextureWrapMode.Clamp;
                        srcRT.wrapMode    = TextureWrapMode.Clamp;
                        Graphics.Blit(srcRT, tempRT, m_blurShader, blurH.tap);
                        srcRT.DiscardContents();
                        Graphics.Blit(tempRT, srcRT, m_blurShader, blurV.tap);
                        RenderTexture.ReleaseTemporary(tempRT);
                    }
                    if (m_mipmapFalloff == MipmapFalloff.Linear)
                    {
                        falloff = ((float)(m_mipLevel - i)) / (m_mipLevel + 1.0f);
                    }
                    else if (m_mipmapFalloff == MipmapFalloff.Custom && m_customMipmapFalloff != null && 0 < m_customMipmapFalloff.Length)
                    {
                        falloff = m_customMipmapFalloff[Mathf.Min(i, m_customMipmapFalloff.Length - 1)];
                    }
                    m_copyMipmapShader.SetFloat(s_falloffParamID, falloff);
                    m_copyMipmapShader.SetFloat(s_falloffParamID, falloff);
                    m_shadowTexture.DiscardContents();                     // To avoid Tiled GPU perf warning. It just tells GPU not to copy back the rendered image to a tile buffer. It won't destroy the rendered image.
                    ++i;
                    Graphics.SetRenderTarget(m_shadowTexture, i);
                    Graphics.Blit(srcRT, m_copyMipmapShader, 0);
                    EraseShadowOnBoarder(w, h);
                    w = Mathf.Max(1, w >> 1);
                    h = Mathf.Max(1, h >> 1);
                    if (i == m_mipLevel || w <= 4 || h <= 4)
                    {
                        RenderTexture.ReleaseTemporary(srcRT);
                        break;
                    }
                    tempRT            = RenderTexture.GetTemporary(w, h, 0, m_shadowTexture.format, RenderTextureReadWrite.Linear);
                    tempRT.filterMode = FilterMode.Bilinear;
                    if (downSampleWithBlur)
                    {
                        SetDownsampleBlurOffsetParams(blurH, blurV, w, h);
                        Graphics.Blit(srcRT, tempRT, m_downsampleShader, 4);
                    }
                    else
                    {
                        Graphics.Blit(srcRT, tempRT, m_downsampleShader, 0);
                    }
                    RenderTexture.ReleaseTemporary(srcRT);
                    srcRT = tempRT;
                }
                while (1 <= w || 1 <= h)
                {
                    ++i;
                    Graphics.SetRenderTarget(m_shadowTexture, i);
                    GL.Clear(false, true, new Color(1, 1, 1, 0));
                    w = w >> 1;
                    h = h >> 1;
                }
            }
        }
        static BlurParam GetDownsampleBlurParam(float blurSize, BlurFilter filter)
        {
            BlurParam param = new BlurParam();

            param.tap = 4;
            if (blurSize < 0.1f)
            {
                param.offset.x = 0.0f;
                param.offset.y = 0.0f;
                param.offset.z = 0.0f;
                param.offset.w = 0.0f;
                param.weight.x = 1.0f;
                param.weight.y = 0.0f;
                param.weight.z = 0.0f;
                param.weight.w = 0.0f;
                return(param);
            }
            // calculate weights
            if (filter == BlurFilter.Gaussian)
            {
                // gaussian filter
                float a           = 1.0f / (2.0f * blurSize * blurSize);
                float totalWeight = 0.0f;
                for (int i = 0; i < param.tap; ++i)
                {
                    float x = i + 0.5f;
                    s_blurWeights[i] = Mathf.Exp(-x * x * a);
                    totalWeight     += 2.0f * s_blurWeights[i];
                }
                float w = 1.0f / totalWeight;
                for (int i = 0; i < param.tap; ++i)
                {
                    s_blurWeights[i] *= w;
                }
            }
            else
            {
                // uniform filter
                float a = 0.5f / blurSize;
                for (int i = 0; i < param.tap; ++i)
                {
                    if (i + 1 <= blurSize)
                    {
                        s_blurWeights[i] = a;
                    }
                    else if (i < blurSize)
                    {
                        s_blurWeights[i] = a * (blurSize - i);
                    }
                    else
                    {
                        s_blurWeights[i] = 0.0f;
                    }
                }
            }
            param.offset.x = 0.5f + s_blurWeights[1] / (s_blurWeights[0] + s_blurWeights[1]);
            param.offset.y = 2.5f + s_blurWeights[3] / (s_blurWeights[2] + s_blurWeights[3]);
            param.offset.z = 0.0f;
            param.offset.w = 0.0f;

            param.weight.x = s_blurWeights[0] + s_blurWeights[1];
            param.weight.y = s_blurWeights[2] + s_blurWeights[3];
            param.weight.z = 0.0f;
            param.weight.w = 0.0f;

            return(param);
        }
        static BlurParam GetBlurParam(float blurSize, BlurFilter filter)
        {
            BlurParam param = new BlurParam();

            if (blurSize < 0.1f)
            {
                param.tap      = 3;
                param.offset.x = 0.0f;
                param.offset.y = 0.0f;
                param.offset.z = 0.0f;
                param.offset.w = 0.0f;
                param.weight.x = 1.0f;
                param.weight.y = 0.0f;
                param.weight.z = 0.0f;
                param.weight.w = 0.0f;
                return(param);
            }
            // calculate weights
            if (filter == BlurFilter.Gaussian)
            {
                // gaussian filter
                float a           = 1.0f / (2.0f * blurSize * blurSize);
                float totalWeight = 1.0f;
                s_blurWeights[0] = 1.0f;
                for (int i = 1; i < s_blurWeights.Length; ++i)
                {
                    s_blurWeights[i] = Mathf.Exp(-i * i * a);
                    totalWeight     += 2.0f * s_blurWeights[i];
                }
                float w = 1.0f / totalWeight;
                for (int i = 0; i < s_blurWeights.Length; ++i)
                {
                    s_blurWeights[i] *= w;
                }
            }
            else
            {
                // uniform filter
                float a = 0.5f / (0.5f + blurSize);
                for (int i = 0; i < s_blurWeights.Length; ++i)
                {
                    if (i <= blurSize)
                    {
                        s_blurWeights[i] = a;
                    }
                    else if (i - 1 < blurSize)
                    {
                        s_blurWeights[i] = a * (blurSize - (i - 1));
                    }
                    else
                    {
                        s_blurWeights[i] = 0.0f;
                    }
                }
            }
            param.offset.x = 1.0f + s_blurWeights[2] / (s_blurWeights[1] + s_blurWeights[2]);
            param.offset.y = 3.0f + s_blurWeights[4] / (s_blurWeights[3] + s_blurWeights[4]);
            param.offset.z = 5.0f + s_blurWeights[6] / (s_blurWeights[5] + s_blurWeights[6]);
            param.offset.w = 0.0f;

            if (s_blurWeights[3] < 0.02f)
            {
                param.tap = 3;
                float a = 0.5f / (0.5f * s_blurWeights[0] + s_blurWeights[1] + s_blurWeights[2]);
                param.weight.x = Mathf.Round(255 * a * s_blurWeights[0]) / 255.0f;
                param.weight.y = 0.5f - 0.5f * param.weight.x;
                param.weight.z = 0.0f;
                param.weight.w = 0.0f;
            }
            else if (s_blurWeights[5] < 0.02f)
            {
                param.tap = 5;
                float a = 0.5f / (0.5f * s_blurWeights[0] + s_blurWeights[1] + s_blurWeights[2] + s_blurWeights[3] + s_blurWeights[4]);
                param.weight.x = Mathf.Round(255 * a * s_blurWeights[0]) / 255.0f;
                param.weight.y = Mathf.Round(255 * a * (s_blurWeights[1] + s_blurWeights[2])) / 255.0f;
                param.weight.z = 0.5f - (0.5f * param.weight.x + param.weight.y);
                param.weight.w = 0.0f;
            }
            else
            {
                param.tap      = 7;
                param.weight.x = Mathf.Round(255 * s_blurWeights[0]) / 255.0f;
                param.weight.y = Mathf.Round(255 * (s_blurWeights[1] + s_blurWeights[2])) / 255.0f;
                param.weight.z = Mathf.Round(255 * (s_blurWeights[3] + s_blurWeights[4])) / 255.0f;
                param.weight.w = 0.5f - (0.5f * param.weight.x + param.weight.y + param.weight.z);
            }
            return(param);
        }