Example #1
0
    private void OnPreRenderInner()
    {
        // If we're not aligning the mosaic to the transform (but we may be aligning it for scale), save
        // the mosaic alignment now.  This way, when we update the alignment later, we do it relative to now
        // rather than to the last frame.  That makes it so we only adjust for changes that we make to the
        // mosaic below (scaling) and not for other changes to the scene, like the anchor or camera moving.
        if (!FollowAnchor)
        {
            ResetAnchorAlignment();
        }

        // Update the render targets if the window has been resized.
        SetupTextures();

        // Match the helper camera to the main camera.
        MosaicCamera.CopyFrom(ThisCamera);

        // Set the background color to black.
        MosaicCamera.backgroundColor = new Color(0, 0, 0, 0);

        //
        // Render the mosaic texture.
        //

        // Hiding other layers with the layer mask prevents them from casting shadows too.  If
        // ShadowsCastOnMosaic is enabled, hide the objects that aren't being mosaiced
        // by setting them to ShadowsOnly, so we draw only our mosaiced object but it still has shadows
        // cast by other objects.  If it's false, be less intrusive and faster by just setting the
        // cullingMask.  This can look wrong if the whole scene is in shadow, because the mosaiced
        // object will be brighter.
        Dictionary <Renderer, ShadowCastingMode> DisabledRenderers = new Dictionary <Renderer, ShadowCastingMode>();

        if (ShadowsCastOnMosaic)
        {
            List <GameObject> NonMosaicObjects = FindGameObjectsInLayer(MosaicLayer, true);
            foreach (GameObject go in NonMosaicObjects)
            {
                Renderer r = go.GetComponent <Renderer>();
                if (r == null)
                {
                    continue;
                }

                DisabledRenderers[r] = r.shadowCastingMode;
                r.shadowCastingMode  = ShadowCastingMode.ShadowsOnly;
            }
        }
        else
        {
            MosaicCamera.cullingMask = (1 << MosaicLayer);
        }

        // The camera might have a modified viewport to set where it appears on screen.  Ignore this for
        // the MosaicCamera.
        MosaicCamera.rect = new Rect(0, 0, 1, 1);

        // Match the projection matrix to the main camera, so we render the same thing even if
        // the aspect ratio of the RenderTexture isn't exactly the same as the screen.
        MosaicCamera.projectionMatrix = ThisCamera.projectionMatrix;

        // Scale the projection matrix by ActualRenderScale.  If RenderScale is 2 then we're rendering
        // into a texture twice as large as the screen, and we need to scale everything to 50% size.
        MosaicCamera.projectionMatrix *= ScaleMatrix(new Vector3(1 / ActualRenderScaleX, 1 / ActualRenderScaleY, 1));

        MosaicCamera.renderingPath = ThisCamera.renderingPath;
        MosaicCamera.clearFlags    = CameraClearFlags.SolidColor;
        MosaicCamera.targetTexture = Passes[0].Texture;

        // Render the layers into our main temporary texture.  This will be HighResTex.
        MosaicCamera.Render();

        // Now that we're done rendering the mosaic texture, undo any changes we just made to shadowCastingMode.
        foreach (KeyValuePair <Renderer, ShadowCastingMode> SavedShadowMode in DisabledRenderers)
        {
            SavedShadowMode.Key.shadowCastingMode = SavedShadowMode.Value;
        }

        // If either transform or scale anchoring are enabled, update the offset.
        //
        // This is done for scaling and not just alignment.  If we scale the mosaic without also aligning
        // it, the mosaic changing scale is ugly since the mosaic is effectively anchored to the bottom-left.
        if ((FollowAnchor || ScaleMosaicToAnchorDistance) && AnchorTransform != null)
        {
            // See how far the anchor has moved in screen space since the last time ResetAnchorAlignment was
            // called, and shift the mosaic to compensate.
            Vector3 NewScreenPos = MosaicCamera.WorldToScreenPoint(AnchorTransform.transform.position);
            Vector2 NewOffset    = GetOffsetAtScreenPos(NewScreenPos);
            Vector2 OffsetDelta  = NewOffset - PreviousAnchorOffset;
            SetMosaicOffset(MosaicOffset + OffsetDelta);
        }

        // Remember where the anchor is on screen now that we've adjusted it, so we update relative to this
        // next frame.
        if (AnchorTransform != null)
        {
            ResetAnchorAlignment();
        }

        Vector2 OffsetPixels = MosaicOffsetPixels;
        Vector2 OffsetUV     = new Vector2(OffsetPixels.x / Passes[0].Texture.width, OffsetPixels.y / Passes[0].Texture.height);

        // Run each postprocessing pass.
        for (int i = 1; i < Passes.Count; ++i)
        {
            RenderTexture src = Passes[i - 1].Texture;
            RenderTexture dst = Passes[i].Texture;

            // This doesn't happen automatically.  We have to enable sRGBWrite manually.
            GL.sRGBWrite = dst.sRGB;

            ImagePass pass = Passes[i];
            switch (pass.Type)
            {
            case PassType.Downscale:
            {
                RenderTexture SavedActiveRenderTexture = RenderTexture.active;
                RenderTexture.active = dst;

                /*
                 * This pass implements a box filter resize.  This resize is ideal for a mosaic, since
                 * it can resize with very high ratios, where most other resizes only work up to a 50%
                 * reduction and need to be iterated.
                 *
                 * This doesn't do edge weighting.  Each sample in the box has the same weight, even if
                 * it doesn't overlap the box completely.  This would make the filter slower and more
                 * complex, and in our case where we're usually downscaling a lot it doesn't matter.
                 * However, if you're downscaling by a small ratio you may be better with a regular bilinear
                 * blit.
                 *
                 * This only averages on one axis at a time: we resize horizontally and then vertically.
                 * This parallelizes better on the GPU and simplifies the shader, since it only needs one
                 * loop.
                 *
                 * If we're downsampling from 4 pixels to 1:
                 *
                 * ABCD -> E
                 *
                 * The destination pixel is at the center (between B and C), and we want to sample each
                 * source pixel once.  UVStep is be the distance from one pixel to another (from A to B).
                 * UVStart is the distance from the center of the destination pixel (the intersection
                 * between B and C) to the first pixel to sample (the center of A).  We'll sample 4 times,
                 * add them together and divide by the number of samples (4).
                 *
                 * * Optimization: bilinear filtering
                 *
                 * If we're doing box filtering on the X axis, we can also let regular bilinear scaling happen
                 * on the Y axis.  For example, we can resize from 1000x1000 to 100x500 in one pass.  The box
                 * filter will be set to the X axis for the large 10:1 ratio, and bilinear filtering will work
                 * normally on the Y axis.  This lets us halve the amount of texture data we need to process: to
                 * go from 1000x1000 to 100x100, we'll first go to 100x500 (box filter on X) and then 100x100
                 * (box filter on Y).  We don't need to do anything special for this to work, we just set the
                 * texture resolutions accordingly.
                 *
                 * * Optimization: partial bilinear scaling
                 *
                 * If we're downsampling 4 pixels:
                 *
                 * ABCD -> E
                 *
                 * Instead of sampling the center of each pixel A B C D with point sampling, we sample the
                 * intersection of AB and CD with bilinear filtering.  This gives the same result with half
                 * the number of samples.  (The result can vary slightly since we're sampling an integer number
                 * of pixels: if we were sampling 9, we'll be sampling 4 or 5, not 4.5.)
                 *
                 * Note: If the box filter shader isn't supported (this happens on WebGL 1.0), we'll be using
                 * a bilinear scale instead.  That shader doesn't need the properties we're setting up here,
                 * but it'll just ignore them.
                 */

                src.filterMode = FilterMode.Bilinear;

                // Set up ResizeMaterial.  This is a simple texture that we only use for blitting.  Graphics.Blit
                // doesn't give us any control, so we have to do the copies ourself.
                ResizeMaterial.SetTexture("_MainTex", src);

                // See which axis we're resizing on.  We only resize on X or Y in a given pass.
                int AxisIndex = pass.FilterOnX? 0:1;

                Vector2 SrcSize = new Vector2(src.width, src.height);
                Vector2 DstSize = new Vector2(dst.width, dst.height);
                //Vector2 SrcSize = new Vector2(4, 4);
                //Vector2 DstSize = new Vector2(1, 1);

                float      SrcToDstRatio = (float)SrcSize[AxisIndex] / DstSize[AxisIndex];
                const bool BilinearResizeOptimization = true;

                // If we're resampling ABCD -> E, by default the UV will be at the center, between BC.
                Vector2 UVStart = new Vector2(0, 0);
                UVStart[AxisIndex] = -SrcToDstRatio / 2; // left edge of A, in source pixels
                if (BilinearResizeOptimization)
                {
                    UVStart[AxisIndex] += 1.0f; // between AB, in source pixels
                }
                else
                {
                    UVStart[AxisIndex] += 0.5f;           // center of A, in source pixels
                }
                UVStart[AxisIndex] /= SrcSize[AxisIndex]; // convert to UVs

                // Create (x,0) start/steps if we're resizing on X, otherwise (0,x).
                ResizeMaterial.SetVector("UVStart", UVStart);

                // If we step 1 / SrcSize we'll move by one pixel per sample.  Step by 2 / SrcSize to
                // step by two pixels.
                Vector2 UVStep = new Vector2(0, 0);
                UVStep[AxisIndex]  = BilinearResizeOptimization? 2.0f:1.0f;
                UVStep[AxisIndex] /= SrcSize[AxisIndex];
                ResizeMaterial.SetVector("UVStep", UVStep);

                int Samples = (int)Math.Round(SrcToDstRatio);
                if (BilinearResizeOptimization)
                {
                    Samples /= 2;
                }
                Samples = Math.Max(Samples, 1);

                ResizeMaterial.SetInt("Samples", (int)Samples);
                ResizeMaterial.SetFloat("SampleFactor", 1.0f / Samples);

                // Debug.Log(src + " -> " + dst + ", " + SrcToDstRatio + ", " + "start: " + UVStart + ", " + "step: " + UVStep + ", samples " + Samples);

                ResizeMaterial.SetPass(0);

                GL.PushMatrix();
                GL.LoadOrtho();

                Vector2 BottomLeftUV = new Vector2(0, 0);
                Vector2 TopRightUV   = new Vector2(1, 1);

                // Apply scaling and offsets on the first downscale pass.
                if (i == 1)
                {
                    // MosaicRatio is the scale of the mosaic texture.  If this is 0.5, the mosaic will be
                    // half the size of the texture (anchored bottom-left).  Apply this in the first downscale.
                    TopRightUV.x *= 1 / HorizontalMosaicRatio;
                    TopRightUV.y *= 1 / VerticalMosaicRatio;

                    // Apply the mosaic offset in the first downscale by shifting UVs when sampling the render
                    // buffer.  This will be reversed by TextureMatrix.  The render buffer always has pixels
                    // 1:1 to the screen (even if we're expanding it at the edges), so this is always in pixels.
                    BottomLeftUV += new Vector2(OffsetUV.x, OffsetUV.y);
                    TopRightUV   += new Vector2(OffsetUV.x, OffsetUV.y);
                }

                GL.Begin(GL.QUADS);
                GL.TexCoord2(TopRightUV.x, BottomLeftUV.y); GL.Vertex3(1.0f, 0.0f, 0.1f);
                GL.TexCoord2(BottomLeftUV.x, BottomLeftUV.y); GL.Vertex3(0.0f, 0.0f, 0.1f);
                GL.TexCoord2(BottomLeftUV.x, TopRightUV.y);   GL.Vertex3(0.0f, 1.0f, 0.1f);
                GL.TexCoord2(TopRightUV.x, TopRightUV.y);   GL.Vertex3(1.0f, 1.0f, 0.1f);
                GL.End();
                GL.PopMatrix();

                RenderTexture.active = SavedActiveRenderTexture;
                break;
            }

            case PassType.Expand:
                ExpandEdgesMaterial.SetVector("PixelUVStep", new Vector4(1.0f / src.width, 1.0f / src.height, 0, 0));
                src.filterMode = FilterMode.Point;
                Graphics.Blit(src, dst, ExpandEdgesMaterial);
                break;
            }
        }

        // Draw the low-resolution texture with nearest neighbor sampling.
        RenderTexture MosaicTex = Passes[Passes.Count - 1].Texture;

        MosaicTex.filterMode = FilterMode.Point;
        MosaicMaterial.SetTexture("MosaicTex", MosaicTex);

        // Disable material keywords.  We'll set the correct ones below.
        foreach (string keyword in MosaicMaterial.shaderKeywords)
        {
            MosaicMaterial.DisableKeyword(keyword);
        }

        // HighResTex is the texture to sample where the mosaic is masked out.  If we're not rendering
        // in high resolution, this will be the same texture as the mosaic, so masking and alpha won't
        // do anything.
        MosaicMaterial.SetTexture("HighResTex", Passes[0].Texture);
        MosaicMaterial.SetFloat("Alpha", Alpha);

        {
            Matrix4x4 FullTextureMatrix = Matrix4x4.identity;

            // If we rendered the full frame at 2x resolution, use the texture matrix to scale UVs
            // down by 0.5 to compensate.  Scale around the center of the texture, not the origin.
            FullTextureMatrix *= TranslationMatrix(new Vector3(0.5f, 0.5f, 0));
            FullTextureMatrix *= ScaleMatrix(new Vector3(1 / ActualRenderScaleX, 1 / ActualRenderScaleY, 1));
            FullTextureMatrix *= TranslationMatrix(new Vector3(-0.5f, -0.5f, 0));
            MosaicMaterial.SetMatrix("FullTextureMatrix", FullTextureMatrix);

            // OffsetPixels shifted the texture in order to move where the center of the mosaic blocks are.
            // Undo the shifting here, so it isn't actually shifted on screen.
            Matrix4x4 MosaicTextureMatrix = Matrix4x4.identity;
            MosaicTextureMatrix *= ScaleMatrix(new Vector3(HorizontalMosaicRatio, VerticalMosaicRatio, 0));
            MosaicTextureMatrix *= TranslationMatrix(new Vector3(-OffsetUV.x, -OffsetUV.y, 0));

            // If we scaled the full resolution image, the mosaic is affected as well.
            MosaicTextureMatrix *= FullTextureMatrix;

            MosaicMaterial.SetMatrix("MosaicTextureMatrix", MosaicTextureMatrix);
        }

        if (ShowMask)
        {
            MosaicMaterial.EnableKeyword("SHOW_MASK");
        }

        // Select whether we're using the texture masking shader, sphere masking, or no masking.
        if (TextureMasking && MaskingTexture != null)
        {
            MosaicMaterial.EnableKeyword("TEXTURE_MASKING");
            MosaicMaterial.SetTexture("MaskTex", MaskingTexture);
        }

        if (SphereMasking && MaskingSphere != null)
        {
            MosaicMaterial.EnableKeyword("SPHERE_MASKING");

            // MaskSizeInner is how big the mosaic circle should be around MaskingSphere.  Within this
            // distance, the mosaic is 100%.  MaskSizeOuter is the size of the fade-out.  At 0, the
            // mosaic cuts out abruptly.  At 1, it fades out over one world space unit.
            //
            // The transparency of the mosaic scales distance so MaskSizeInner is 1 (100%) and MaskSizeOuter
            // is 0 (0%).  If the distance is less than MaskSizeInner it'll be above 1 and clamped.  This
            // is simply:
            //
            // f = (dist - MaskSizeOuter) / (MaskSizeInner - MaskSizeOuter);
            //
            // To remove the division from the fragment shader, we pass in MaskScaleFactor,
            // which is 1 / (MaskSizeInner - MaskSizeOuter).
            //
            // If the fade is zero, nudge it up slightly to avoid division by zero.
            float MaskSizeInner = 1;
            float MaskSizeOuter = MaskSizeInner + MaskFade;
            if (MaskFade < 0.0001f)
            {
                MaskSizeOuter += 0.0001f;
            }

            MosaicMaterial.SetFloat("MaskSizeOuter", MaskSizeOuter);
            float MaskSizeFactor = 1.0f / (MaskSizeInner - MaskSizeOuter);
            MosaicMaterial.SetFloat("MaskSizeFactor", MaskSizeFactor);
            Matrix4x4 mat = MaskingSphere.transform.worldToLocalMatrix;

            // Halve the size of the mask, since the distance from the center to the edge of
            // the mask sphere is 0.5, not 1:
            mat = Matrix4x4.TRS(
                Vector3.zero,
                Quaternion.AngleAxis(0, new Vector3(1, 0, 0)),
                new Vector3(2, 2, 2)) * mat;

            MosaicMaterial.SetMatrix("MaskMatrix", mat);
        }

        // Find the objects that we're mosaicing, and switch them to the mosaic shader, which
        // will sample the prerendered texture we just made.  This will happen during regular
        // rendering.
        List <GameObject> MosaicObjects = FindGameObjectsInLayer(MosaicLayer);

        foreach (GameObject go in MosaicObjects)
        {
            // Find objects in our layer that have a renderer.
            Renderer renderer = go.GetComponent <Renderer>();
            if (renderer == null)
            {
                continue;
            }

            // Save the original materials so we can restore them in OnPostRender.
            SavedMaterials[renderer] = renderer.materials;

            // Replace all materials on this object with ours.
            Material[] ReplacementMaterials = new Material[renderer.materials.Length];
            for (int i = 0; i < renderer.materials.Length; ++i)
            {
                ReplacementMaterials[i] = MosaicMaterial;
            }
            renderer.materials = ReplacementMaterials;
        }
    }
Example #2
0
    private void SetupTextures()
    {
        int Width  = ThisCamera.pixelWidth;
        int Height = ThisCamera.pixelHeight;

        // RenderScale is how much larger we should draw the scene than the viewport.  If this is 2, then
        // we'll draw a texture twice as wide and high as the viewport (usually much closer to 1).  We want
        // to be an integer number of pixels larger than the viewport, so the screen is an exact subset of
        // the texture we draw, so antialiasing stays the same.  If the screen is 100x100 and RenderScale
        // tells us to render at 103.5x103.5, round to the nearest even multiple of 2, 104x104x.
        {
            float ExtraPixelsX = RoundToNearest(Width * (RenderScale - 1), 2);
            float ExtraPixelsY = RoundToNearest(Height * (RenderScale - 1), 2);

            ActualRenderScaleX = (Width + ExtraPixelsX) / Width;
            ActualRenderScaleY = (Height + ExtraPixelsY) / Height;

            Width  += (int)ExtraPixelsX;
            Height += (int)ExtraPixelsY;
        }

        // The number of actual horizontal and vertical blocks.  Scale this by RenderScale, so if the scale
        // is 2 (we're drawing a texture twice as big), we draw twice as many blocks and keep the blocks the
        // same size.
        float HorizontalMosaicBlocks = MosaicBlocks * ActualRenderScaleX;

        if (AnchorTransform != null && ScaleMosaicToAnchorDistance)
        {
            // Get the distance from the camera to the anchor, and scale the mosaic size by it.
            // Note that the mosaic alignment to AnchorTransform helps reduce flicker caused by
            // the mosaic resolution changing.
            Vector3 TransformPos = AnchorTransform.transform.position;
            Vector3 CameraPos    = ThisCamera.transform.position;
            float   Distance     = Vector3.Distance(TransformPos, CameraPos);
            HorizontalMosaicBlocks = HorizontalMosaicBlocks * Distance;
        }

        float VerticalMosaicBlocks = HorizontalMosaicBlocks * ActualRenderScaleY;

        // Make sure neither dimension is zero.
        HorizontalMosaicBlocks = Math.Max(1, HorizontalMosaicBlocks);
        VerticalMosaicBlocks   = Math.Max(1, VerticalMosaicBlocks);

        // MosaicBlocks is the number of mosaic blocks we want to display.  However, the screen is probably not
        // square and we want the mosaic blocks to be square.  Adjust the number of blocks to fix this.
        // Use the larger of the two sizes, so the blocks are square.
        float AspectRatio = ((float)Width) / Height;

        if (Width < Height)
        {
            // The screen is taller than it is wide.  Decrease the number of blocks horizontally.
            HorizontalMosaicBlocks = VerticalMosaicBlocks * AspectRatio;
        }
        else
        {
            // The screen is wider than it is tall.  Decrease the number of blocks vertically.
            VerticalMosaicBlocks = HorizontalMosaicBlocks / AspectRatio;
        }

        // There's no point to these being higher than the display resolution.
        HorizontalMosaicBlocks = Math.Min(HorizontalMosaicBlocks, Width);
        VerticalMosaicBlocks   = Math.Min(VerticalMosaicBlocks, Height);

        // Don't allow these to go too low.  If we're only drawing one block, it's easy for MosaicOffset
        // to put too much offscreen, so we'd need a very high RenderScale to compensate.  This probably
        // would only happen from unexpected anchor scaling, so put a sanity limit here.
        HorizontalMosaicBlocks = Math.Max(HorizontalMosaicBlocks, 4);
        VerticalMosaicBlocks   = Math.Max(VerticalMosaicBlocks, 4);

        int CurrentWidth = Width, CurrentHeight = Height;

        // The final number of mosaic blocks (resolution of the mosaic texture):
        int IntegerHorizontalMosaicBlocks = (int)Math.Ceiling(HorizontalMosaicBlocks);
        int IntegerVerticalMosaicBlocks   = (int)Math.Ceiling(VerticalMosaicBlocks);

        // If we're doing a low-resolution render, render at the block size, and we won't have
        // any rescaling passes below.  Snap HorizontalMosaicBlocks/VerticalMosaicBlocks as well
        // in this mode (we won't support fractional mosaic sizes here).
        if (!HighResolutionRender)
        {
            HorizontalMosaicBlocks = CurrentWidth = IntegerHorizontalMosaicBlocks;
            VerticalMosaicBlocks   = CurrentHeight = IntegerVerticalMosaicBlocks;
        }

        // Check if we actually need to recreate textures.
        Setup NewSetup;

        NewSetup.RenderWidth            = CurrentWidth;
        NewSetup.RenderHeight           = CurrentHeight;
        NewSetup.HorizontalMosaicBlocks = HorizontalMosaicBlocks;
        NewSetup.VerticalMosaicBlocks   = VerticalMosaicBlocks;
        NewSetup.ExpandPasses           = ExpandPasses;

        // Match the scene antialiasing level.
#if UNITY_5_6_OR_NEVER
        bool allowMSAA = ThisCamera.allowMSAA;
#else
        bool allowMSAA = false;
#endif

        NewSetup.AntiAliasing = allowMSAA? QualitySettings.antiAliasing:0;
        if (NewSetup.AntiAliasing == 0)
        {
            NewSetup.AntiAliasing = 1; // work around Unity inconsistency
        }
        if (CurrentSetup.Equals(NewSetup))
        {
            return;
        }
        CurrentSetup = NewSetup;

        // Release the temporary textures we previously allocated.  Note that most of the time we come back here
        // to recreate textures it's because the mosaic block size is changing (eg. because the anchor has moved),
        // in which case most of the textures will be the same size, especially the large main render texture.
        // Usually, the only thing that will change is how many low-resolution textures we allocate at the end
        // of the pass list.
        ReleaseTextures();

        // If Unity is rendering into an HDR texture for postprocessing, we want to render HDR too to pass it
        // through.  Do this if we're in linear color space and the camera's allowHDR flag is enabled.
        //
        // If there are no image effects enabled and Camera.forceIntoRenderTexture is false Unity will actually
        // just render sRGB and it'd be better for us to too, but there's no obvious way to ask Unity whether
        // it's rendering a camera into an HDR texture in OnPreRender.
#if UNITY_5_6_OR_NEWER
        bool allowHDR = ThisCamera.allowHDR;
#else
        bool allowHDR = ThisCamera.hdr;
#endif
        RenderTextureFormat format = QualitySettings.activeColorSpace == ColorSpace.Linear && allowHDR?
                                     RenderTextureFormat.DefaultHDR:
                                     RenderTextureFormat.Default;

        // We'll render to the first texture, then blit each texture to the next to progressively
        // downscale it.
        //
        // The first texture is what we render into.  This is also the only texture that needs a depth buffer, and the
        // only one that has antialiasing enabled.
        Passes.Add(new ImagePass(RenderTexture.GetTemporary(CurrentWidth, CurrentHeight, 24, format,
                                                            RenderTextureReadWrite.Default, NewSetup.AntiAliasing),
                                 PassType.Render));


        // If we want 3.5 blocks and we're drawing into a 4x4 texture, we're drawing at 0.875 scale.
        HorizontalMosaicRatio = HorizontalMosaicBlocks / IntegerHorizontalMosaicBlocks;
        VerticalMosaicRatio   = VerticalMosaicBlocks / IntegerVerticalMosaicBlocks;

        // If we're in normal (high-resolution) mode, downscale to the mosaic.  If we're in low-res mode, we're
        // already at mosaic resolution and can skip these passes.
        if (HighResolutionRender)
        {
            if (UsingBoxResize())
            {
                // First resize on X.
                CurrentWidth = IntegerHorizontalMosaicBlocks;

                // This resize step is doing a filter over X and will rescale all the way to the mosaic resolution on
                // X.  While we're doing this, also downscale by up to 50% on Y, since we can do this for free.
                CurrentHeight = Math.Max(CurrentHeight / 2, IntegerVerticalMosaicBlocks);
                ImagePass HorizResizePass = new ImagePass(RenderTexture.GetTemporary(CurrentWidth, CurrentHeight, 24, format), PassType.Downscale);
                HorizResizePass.FilterOnX = true; // box filter on X axis
                Passes.Add(HorizResizePass);

                // Next, resize on Y.
                CurrentHeight = IntegerVerticalMosaicBlocks;
                ImagePass VertResizePass = new ImagePass(RenderTexture.GetTemporary(CurrentWidth, CurrentHeight, 24, format), PassType.Downscale);
                VertResizePass.FilterOnX = false; // box filter on Y axis
                Passes.Add(VertResizePass);
            }
            else
            {
                while (true)
                {
                    // Each pass halves the resolution, except for the last pass which snaps to the
                    // final resolution.
                    CurrentWidth  /= 2;
                    CurrentHeight /= 2;
                    CurrentWidth   = (int)Math.Max(CurrentWidth, IntegerHorizontalMosaicBlocks);
                    CurrentHeight  = (int)Math.Max(CurrentHeight, IntegerVerticalMosaicBlocks);

                    // If we've already reached the target resolution, we're done.
                    if (Passes[Passes.Count - 1].Texture.width == CurrentWidth &&
                        Passes[Passes.Count - 1].Texture.height == CurrentHeight)
                    {
                        break;
                    }

                    Passes.Add(new ImagePass(RenderTexture.GetTemporary(CurrentWidth, CurrentHeight, 24, format), PassType.Downscale));
                }
            }
        }

        // Add the expand pass.
        for (int pass = 0; pass < ExpandPasses; ++pass)
        {
            Passes.Add(new ImagePass(RenderTexture.GetTemporary(CurrentWidth, CurrentHeight, 24, format), PassType.Expand));
        }
    }