The trick I like to use is reading the _TexelSize
information in the shader to adapt to whatever size of noise or other overlay we're using.
Here's an example using an image effect to get an overlay blend mode:
Shader "Hidden/ScreenspaceOverlay"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_OverlayTex ("Overlay", 2D) = "white" {}
_Effect ("Effect", Range(0, 1)) = 1.0
}
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 screenUV : TEXCOORD0;
float2 overlayUV : TEXCOORD1;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
sampler2D _OverlayTex;
float4 _OverlayTex_TexelSize;
float4 _OverlayTex_ST;
float _Effect;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// Map the scene render normally, 0...1
o.screenUV = v.uv;
// Scale the overlay UV according to the size of the texture,
// so we get 1 overlay texel per screen texel.
o.overlayUV = v.uv * _ScreenParams.xy * _OverlayTex_TexelSize.xy;
// In case you want to use material.SetTextureOffset()
// to scroll or randomize the noise position.
o.overlayUV += _OverlayTex_ST.zw;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// Sample the composed scene render & the overlay texture.
half4 col = tex2D(_MainTex, i.screenUV);
half4 overlay = tex2D(_OverlayTex, i.overlayUV);
// Overlay blend mode.
half4 blended = lerp(
2.0f*col*overlay,
1.0f - 2.0f * (1.0f - col) * (1.0f - overlay),
step(0.5f, col)
);
// Return anything from 0% to 100% of the effect.
return lerp(col, blended, _Effect);
}
ENDCG
}
}
}
The key bit is this line:
o.overlayUV = v.uv * _ScreenParams.xy * _OverlayTex_TexelSize.xy;
Once we have the position on screen in the 0...1 range from left to right, bottom to top (which we get conveniently as the input UV when blitting an image effect - for 3D geometry it's a bit trickier), we just need to
scale it up by _ScreenParams.xy
, which contains the width & height of the current render target in pixels. This gives us the position in pixel coordinates.
scale it down by _OverlayTex_TexelSize.xy
, which contains one divided by the texture width & height. This normalizes the texture coordinates within the texture's sampling space.
(eg. if I'm 256 pixels from the left edge of my screen, and my texture is 128 pixels wide, this will give me 2.0, meaning the image tiles twice horizontally up to this point)
I then use a tiny little script on my camera to glue a material using this shader into the rendering pipeline after the whole scene has been drawn:
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class PostEffector : MonoBehaviour {
public Material effect;
void OnRenderImage(RenderTexture source, RenderTexture destination) {
if (effect == null)
Graphics.Blit(source, destination);
else
Graphics.Blit(source, destination, effect);
}
}