using UnityEngine; using UnityEngine.UI; namespace Heroes.Battle { /// /// Isolates a camera's post-processing to just what that camera renders /// (Built-in Render Pipeline). /// /// In the Built-in pipeline, stacked cameras that render to the screen share one /// framebuffer, so a second camera's OnRenderImage receives the whole /// accumulated frame (background included) as its source — the effect ends up /// applied to everything. The fix is to render this camera into its own /// RenderTexture instead of the screen, run the effect there, and composite the /// result back over the screen with a full-screen on an /// overlay canvas. /// /// Attach this to the units camera. It sizes the RenderTexture to the screen, /// forces a transparent clear so the background shows through, and keeps /// everything in sync when the resolution changes. Put /// on the same camera; the effect now touches only the units. /// [ExecuteAlways] [RequireComponent(typeof(Camera))] [AddComponentMenu("Heroes/Unit Camera Compositor")] public sealed class UnitCameraCompositor : MonoBehaviour { [Tooltip("Overlay canvas the composite image is drawn on. Left empty -> one is created automatically.")] public Canvas overlayCanvas; [Tooltip("Sorting order of the auto-created overlay canvas (higher draws on top).")] public int canvasSortingOrder = 100; [Tooltip("Downscale factor for the render texture. 1 = full screen resolution. " + "RetroImageEffect already pixelates, so leave at 1 unless you want extra headroom.")] [Range(1, 4)] public int resolutionDivider = 1; private Camera _camera; private RenderTexture _rt; private RawImage _image; private int _rtWidth; private int _rtHeight; private void OnEnable() { _camera = GetComponent(); EnsureComposite(); } private void OnDisable() { if (_camera != null && _camera.targetTexture == _rt) _camera.targetTexture = null; if (_image != null) _image.enabled = false; ReleaseTexture(); } private void Update() { EnsureComposite(); } private void EnsureComposite() { if (_camera == null) _camera = GetComponent(); int div = Mathf.Max(1, resolutionDivider); int w = Mathf.Max(16, Screen.width / div); int h = Mathf.Max(16, Screen.height / div); if (_rt == null || w != _rtWidth || h != _rtHeight) { ReleaseTexture(); _rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32) { name = "UnitCameraRT", hideFlags = HideFlags.HideAndDontSave, }; _rt.Create(); _rtWidth = w; _rtHeight = h; } // Force the camera to render only its own layers into the RT, cleared to // transparent so the background camera shows through the empty areas. _camera.targetTexture = _rt; _camera.clearFlags = CameraClearFlags.SolidColor; _camera.backgroundColor = new Color(0f, 0f, 0f, 0f); EnsureImage(); if (_image != null && _image.texture != _rt) _image.texture = _rt; } private void EnsureImage() { if (_image != null) { _image.enabled = true; return; } if (overlayCanvas == null) { var canvasGo = new GameObject("UnitCompositeCanvas"); canvasGo.hideFlags = HideFlags.DontSave; overlayCanvas = canvasGo.AddComponent(); overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay; overlayCanvas.sortingOrder = canvasSortingOrder; } var imageGo = new GameObject("UnitCompositeImage"); imageGo.hideFlags = HideFlags.DontSave; imageGo.transform.SetParent(overlayCanvas.transform, false); _image = imageGo.AddComponent(); _image.raycastTarget = false; var rt = _image.rectTransform; rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; } private void ReleaseTexture() { if (_rt == null) return; _rt.Release(); if (Application.isPlaying) Destroy(_rt); else DestroyImmediate(_rt); _rt = null; } } }