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; } }
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)); } }