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