using UnityEngine; namespace Heroes.Battle { /// /// Camera post-processing for the Heroes 3 retro look (Built-in Render Pipeline). /// Add this to the battle Camera. It combines two effects in one blit chain: /// /// 1. Pixelation — the frame is rendered down to a low-resolution /// render target and blown back up with point sampling, producing chunky /// pixels like a low-res sprite game. /// 2. Posterize / limited palette — the upscale pass runs /// Hidden/Heroes/RetroPost, which collapses colours to a few levels /// (and optionally snaps them to a fixed palette texture). /// /// Runs in the editor too () so you can tune it live. /// [ExecuteAlways] [RequireComponent(typeof(Camera))] [AddComponentMenu("Heroes/Retro Image Effect")] public sealed class RetroImageEffect : MonoBehaviour { [Header("Pixelation")] [Tooltip("Enable the low-res downscale/upscale that produces chunky pixels.")] public bool pixelate = true; [Tooltip("Target vertical resolution the scene is squashed to before upscaling. " + "Lower = chunkier. 180-320 is a good Heroes 3 range.")] [Range(64, 720)] public int targetHeight = 240; [Tooltip("Point-sample the downscale too (harsher, more aliased) instead of bilinear.")] public bool pointDownsample = false; [Header("Colour")] [Tooltip("Posterize levels per channel. Fewer = more limited palette. <=1 disables.")] [Range(1, 64)] public int colorLevels = 12; [Range(0f, 2f)] public float saturation = 1.15f; [Range(0.5f, 2f)] public float gamma = 1f; [Header("Fixed palette (optional)")] [Tooltip("Snap every pixel to the nearest colour in this palette. Use a 1-row texture " + "with Point filtering and no compression; leave empty to use posterize only.")] public Texture2D palette; [Tooltip("Number of valid colours in the palette texture (its pixel width).")] public int paletteSize = 0; [Header("Shaders (auto-found if left empty)")] public Shader postShader; private Material _material; private Camera _camera; private void OnEnable() { _camera = GetComponent(); EnsureMaterial(); } private void OnDisable() { if (_material != null) { if (Application.isPlaying) Destroy(_material); else DestroyImmediate(_material); _material = null; } } private void EnsureMaterial() { if (postShader == null) postShader = Shader.Find("Hidden/Heroes/RetroPost"); if (_material == null && postShader != null) { _material = new Material(postShader) { hideFlags = HideFlags.HideAndDontSave }; } } private void OnRenderImage(RenderTexture source, RenderTexture destination) { EnsureMaterial(); if (_material == null) { Graphics.Blit(source, destination); return; } // Push the colour parameters onto the post material. _material.SetFloat("_Levels", colorLevels); _material.SetFloat("_Saturation", saturation); _material.SetFloat("_Gamma", gamma); bool usePalette = palette != null && paletteSize > 0; _material.SetFloat("_UsePalette", usePalette ? 1f : 0f); if (usePalette) { _material.SetTexture("_PaletteTex", palette); _material.SetFloat("_PaletteSize", paletteSize); } if (!pixelate || targetHeight >= source.height) { // No pixelation: colour pass only. Graphics.Blit(source, destination, _material); return; } // Downscale to a low-res buffer, then upscale with point filtering so the // pixels stay crisp. The colour pass runs on the upscale blit. int lowH = Mathf.Max(16, targetHeight); int lowW = Mathf.Max(16, Mathf.RoundToInt(lowH * ((float)source.width / source.height))); var lowRes = RenderTexture.GetTemporary(lowW, lowH, 0, source.format); lowRes.filterMode = pointDownsample ? FilterMode.Point : FilterMode.Bilinear; Graphics.Blit(source, lowRes); // downscale lowRes.filterMode = FilterMode.Point; // crisp upscale Graphics.Blit(lowRes, destination, _material); // upscale + colour RenderTexture.ReleaseTemporary(lowRes); } } }