概要
GrabPass を使うとモザイクのオーバーレイをかけるシェーダーを作成できます.スクリーンスペースでモザイクをかける場合は,フラグメントシェーダーで離散化すればできますが,オブジェクトスペースでかけようとしたときに困ったので,その記録です.
Screen Space のモザイクシェーダー
スクリーンスペースでモザイクをかける場合は,頂点シェーダーでスクリーン座標を計算しておき,フラグメントシェーダーで離散化します.
Shader "Test/Mosaic/Mosaic-Overlay-ScreenSpace"
{
Properties
{
_CellPix ("Cell Pixel Size", Int) = 40
}
SubShader
{
Tags { "Queue"="Transparent+1" "RenderType"="Opaque" }
Cull Off
ZWrite Off
GrabPass { "_TransparentPlus1GrabTex" }
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.5
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 pos : SV_POSITION;
float4 grabPos : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
float _CellPix;
v2f vert (appdata v)
{
v2f o;
// VR 対応
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
// 通常
o.pos = UnityObjectToClipPos(v.vertex);
// クリップ空間座標
o.grabPos = ComputeGrabScreenPos(o.pos);
return o;
}
UNITY_DECLARE_SCREENSPACE_TEXTURE(_TransparentPlus1GrabTex);
float4 frag (v2f i) : SV_Target
{
// VR 対応
UNITY_SETUP_INSTANCE_ID(i);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
// スクリーン座標で離散化 (セルの中央の座標にする)
float2 grab_uv = i.grabPos.xy / i.grabPos.w;
float2 cell_uv_size = _CellPix / _ScreenParams.xy;
float2 digit_grab_uv = (floor(grab_uv / cell_uv_size) + 0.5) * cell_uv_size;
// マクロを使ってサンプリングしないと環境によって壊れるらしい.
float4 tex = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_TransparentPlus1GrabTex, digit_grab_uv);
return tex;
}
ENDHLSL
}
}
}
Object Space のモザイクシェーダー
オブジェクトスペースと書いていますが,正確には UV 空間でのモザイクです.Quad のメッシュはメッシュ頂点のローカル座標と UV 座標の対応が明白なので,それを利用して離散化を行います.オブジェクトスペースで汎用的に離散化を行う方法はわかりませんでした.
Quad メッシュの4頂点のクリップ空間座標を計算しておき,それを UV 座標で線形補間することにより,オブジェクト空間座標とスクリーン座標とを対応付けます.
Shader "Test/Mosaic/Mosaic-Overlay-UvSpace"
{
Properties
{
_CellCnt ("Cell Count", Int) = 40
}
SubShader
{
Tags { "Queue"="Transparent+1" "RenderType"="Opaque" }
Cull Off
ZWrite Off
GrabPass { "_TransparentPlus1GrabTex" }
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.5
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 grabUvPos00 : TEXCOORD1;
float4 grabUvPos10 : TEXCOORD2;
float4 grabUvPos01 : TEXCOORD3;
float4 grabUvPos11 : TEXCOORD4;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
float _CellCnt;
v2f vert (appdata v)
{
v2f o;
// VR 対応
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
// 通常
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
// Quad の 4 頂点のクリップ空間座標を求める.
o.grabUvPos00 = ComputeGrabScreenPos(UnityObjectToClipPos(float4(-0.5,-0.5, 0, 1)));
o.grabUvPos10 = ComputeGrabScreenPos(UnityObjectToClipPos(float4( 0.5,-0.5, 0, 1)));
o.grabUvPos01 = ComputeGrabScreenPos(UnityObjectToClipPos(float4(-0.5, 0.5, 0, 1)));
o.grabUvPos11 = ComputeGrabScreenPos(UnityObjectToClipPos(float4( 0.5, 0.5, 0, 1)));
return o;
}
UNITY_DECLARE_SCREENSPACE_TEXTURE(_TransparentPlus1GrabTex);
float4 frag (v2f i) : SV_Target
{
// VR 対応
UNITY_SETUP_INSTANCE_ID(i);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
// UV 空間で離散化 (セルの中央の座標にする)
float2 digit_uv = (floor(i.uv * _CellCnt) + 0.5) / _CellCnt;
// UV 座標で補完し,digit_uv に対応するクリップ空間座標を計算する.
// クリップ空間座標は w で割る前なら線形なので線形補間できる.
float4 digit_grab_pos = lerp(
lerp(i.grabUvPos00, i.grabUvPos10, digit_uv.x),
lerp(i.grabUvPos01, i.grabUvPos11, digit_uv.x),
digit_uv.y
);
// スクリーン座標
float2 digit_grab_uv = digit_grab_pos.xy / digit_grab_pos.w;
// マクロを使ってサンプリングしないと環境によって壊れるらしい.
float4 tex = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_TransparentPlus1GrabTex, digit_grab_uv);
return tex;
}
ENDHLSL
}
}
}
雑記
モチベーションとしては,noriben さんの LEDパネルシェーダー を改造して,向こう側が LCD で見えるフィルターをつくりたい,というものでした.いい感じのシェーダーができたので満足しています.
最初は ddx, ddy を使ってフラグメントシェーダー内で UV 座標とスクリーン座標の変換をしていましたが,ノイズがひどく,代替案として思いついたのがクリップ空間座標の補間です.線形って便利ですね.