ゲームボーイ風イメージエフェクト

2019-11-07

成果物

Unity でシェーダを書いてゲームボーイの液晶感のあるイメージエフェクトを作った:

縮小版

縮小版

動画

モバイルでの動作テスト

  • 2017 年末くらいに出た中堅 Android (HUAWEI Mate 10 lite) で動作確認
  • Android はスペックに対して解像度が高すぎるものが多いので、204 dpi くらいの解像度で出力
  • 快適に動いたので実用には耐えそう

ソースコード

  • 動作環境は Unity 2019.2.0b5

カメラにイメージエフェクト(ポストエフェクト)用のスクリプトをアタッチ:

// ImageEffect.cs
using UnityEngine;

[RequireComponent(typeof(Camera)), ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class ImageEffect : MonoBehaviour {
  public Material material;

  void OnRenderImage(RenderTexture src, RenderTexture dest) {
    Graphics.Blit(src, dest, material);
  }
}
  • Unity では Graphics.Blit() によって、Material の _MainTex にカメラのレンダリング結果が渡る

上記スクリプトにセットした Material に、以下のシェーダを当てる:

Shader "MyPostEffect/GBLike" {
  Properties {
    _MainTex ("Texture", 2D) = "white" {}
    _TileScale ("TileScale", Range(0.001, 0.1)) = 0.01
    _ColorThreshold ("ColorThreshold", Range(0, 2)) = 1
  }
  SubShader {
    Cull Off ZWrite Off ZTest Always

    Pass {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag

      // Properties
      uniform sampler2D _MainTex;
      uniform float4    _MainTex_TexelSize;
      uniform float     _TileScale;
      uniform float     _ColorThreshold;

      // Input
      struct appdata {
        float4 vertex : POSITION;
        float2 uv     : TEXCOORD0;
      };

      // Vertex to Fragment
      struct v2f {
        float4 pos : SV_POSITION;
        float2 uv  : TEXCOORD0;
      };

      //------------------------------------------------------------------------
      // Vertex shader
      //------------------------------------------------------------------------
      v2f vert(appdata v) {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.uv  = v.uv;
        return o;
      }

      //------------------------------------------------------------------------
      // Fragment shader
      //------------------------------------------------------------------------
      fixed4 frag(v2f i) : SV_Target {
        // モザイク
        float widthRatio = _MainTex_TexelSize.x / _MainTex_TexelSize.y;
        float2 scale = float2(_TileScale * widthRatio, _TileScale);
        float2 uv    = (floor(i.uv / scale) + 0.5) * scale;
        fixed4 color = tex2D(_MainTex, uv);

        // グリッド
        float numPixelH = 1 / (_TileScale * widthRatio);
        float numPixelV = 1 / _TileScale;
        float2 grid_uv = float2((i.uv.x * numPixelH) + 0.5, (i.uv.y * numPixelV) + 0.5);
        float2 range = abs(frac(grid_uv) - 0.5);
        float2 gradient = fwidth(grid_uv);
        float2 pixelRange = range / gradient;
        float lineThickness = 0.05;
        float lineWeight = saturate(min(pixelRange.x, pixelRange.y) - lineThickness);
        fixed4 gridColor = fixed4(0.85 * 1.1, 1.0, 0.2 * 1.1, 1.0);

        // 4 階調
        // (※ パフォーマンス上 if による分岐は避けるべきだが、とりあえず)
        half y = dot(color.rgb, half3(0.30, 0.59, 0.11)) * _ColorThreshold;
        fixed c = 0.05;
        if (y > 0.25) { c = 0.35; }
        if (y > 0.50) { c = 0.60; }
        if (y > 0.75) { c = 0.90; }
        color.rgb = float3(c * 0.85, c, c * 0.2);

        return lerp(gridColor, color, lineWeight);
      }
      ENDCG
    }
  }
}

※ 趣味コードなので多少雑なのはご愛嬌

解説

  • モザイク処理と階調化はポストエフェクトではよくあるやつ
  • これだけだとそれっぽくならない。そこで自分の中のゲームボーイ体験の記憶を掘り起こしたところ、 当時の液晶は 「目を近づけるとドット 1 粒 1 粒の区切りが見えた」 というのを思い出した
  • そこでピクセルの境目にグリッドを引くようにしたら「らしく」なった
    • グリッドの部分は適当にコードを書いたが、もっと軽くやれる書き方がありそう。研究中