読者です 読者をやめる 読者になる 読者になる

Unityでドット絵シェーダ その2

ImageEffect NPR Unity

ユニティちゃんライセンス

このコンテンツは、『ユニティちゃんライセンス』で提供されています


 Unity5のフリー版が発表されましたね。
Unity4の無料版では使えなかったImageEffectが使えるようになっていますので、かなり表現の幅が広がったのではないかと思います。

 というわけで前回のドット絵シェーダ(イメージエフェクト)を少し改良したのと、多少最適化を行ったので公開します。Unity5.0.0f4で実行しています。



 基本は前回と同様ですが、パラメータをスクリプトから調整できるようにしたのと、
ドットの粒度をシェーダの変更なしで変えられるようにしました。
 以下コード全文です。

DotImageEffect.cs(Cameraに追加するイメージエフェクトコンポーネントスクリプト

using UnityEngine;
using System.Collections;
using UnityStandardAssets.ImageEffects;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
[AddComponentMenu("Image Effects/DotImgeEffect")]
public class DotImageEffect : ImageEffectBase
{
    [Range(1,10)]
    public int Downsample = 3;  // 解像度を落とす回数+1。1回ごとに解像度を半分に落とす。

    [Range(0.001f,0.1f)]
    public float ValidDepthThreshold= 0.01f;  // 深度が近いドットを平均化するときの閾値

    [Range(0,5f)]
    public float EdgeLuminanceThreshold = 0.8f; // エッジ部と判定するための輝度差。この値以上離れていたらエッジとみなす。

    [Range(0, 1f)]
    public float SharpEdge = 0.5f;  // 平均化する際のコントラスト強調パラメータ


    void OnEnable()
    {
        // 深度の取得を有効にする
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    }

    [ImageEffectOpaque]
    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        material.SetFloat("_ValidDepthThreshold", ValidDepthThreshold);
        material.SetFloat("_EdgeLuminanceThreshold", EdgeLuminanceThreshold);
        material.SetFloat("_SharpEdge", SharpEdge);

        // RTのAチャンネルに、深度*(1-α)を格納する。
        // 深度*(1-α)は優先度として作用する。
        var RT = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.ARGBHalf);
        RT.filterMode = FilterMode.Point;
        Graphics.Blit(source, RT,material,0);

        // 1回毎に解像度を半分に落としドット化する
        for (int i = 1; i < Downsample; i++)
        {
            material.SetVector("_PixelSize", new Vector4(1.0f / RT.width, 1.0f / RT.height, 0, 0));
            var RT2 = RenderTexture.GetTemporary(RT.width / 2, RT.height / 2, 0, RenderTextureFormat.ARGBHalf);
            RT2.filterMode = FilterMode.Point;

            // ドット絵変換
            Graphics.Blit(RT, RT2, material, 1);

            // 描画先と描画元を交換
            RenderTexture.ReleaseTemporary(RT);
            RT = RT2;
        }

        Graphics.Blit(RT, destination);
        RenderTexture.ReleaseTemporary(RT);
    }

}

DotImageEffectShader.shader(上記スクリプトのShaderに入れるシェーダ)

Shader "Hidden/DotImageEffectShader" 
{
	Properties{
		_MainTex("", 2D) = "" {}
	}
		SubShader{
		ZTest Always
		ZWrite Off
		Cull Off
		Fog{ Mode Off }


		Pass{

			CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 5.0
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"

			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
			};

			sampler2D _CameraDepthNormalsTexture;
			sampler2D _MainTex;
			float4 _MainTex_ST;

			v2f vert(appdata_img v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}

			float GetDepth(half2 uv)
			{
				float4 depthnormal = tex2D(_CameraDepthNormalsTexture, uv);
				float3 viewNorm;
				float depth;
				DecodeDepthNormal(depthnormal, depth, viewNorm);
				return depth;
			}

			half4 frag(v2f i) : SV_Target
			{
				half4 centerCol = tex2D(_MainTex, i.uv);
				centerCol.a = (1-centerCol.a) * GetDepth(i.uv);
				return centerCol;
			}
				ENDCG
		} // pass0

		Pass{

			CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 5.0
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"


			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			uniform float4 _PixelSize;
			uniform float _ValidDepthThreshold;
			uniform float _EdgeLuminanceThreshold;
			uniform float _SharpEdge;


			v2f vert(appdata_img v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}

			half4 frag(v2f i) : SV_Target
			{
				half4 centerCol = tex2D(_MainTex, i.uv);
				half4 nearCol;
				float4 cols[4];


				i.uv -= 0.5 * _PixelSize.xy;

				// minDepth Detection
				float minDepth = 9999;

				for (int x = 0; x < 2; x++)
				{
					for (int y = 0; y < 2; y++)
					{
						float2 uv2 = i.uv + _PixelSize.xy * float2(x, y);
						float4 c = tex2D(_MainTex, uv2);
						cols[y * 2 + x] = c;

						if (minDepth > c.a)
						{
							minDepth = c.a;
							nearCol = c;
						}
					}
				}

				float4 col = 0;
				float weights = 0;
				float minLum = 9999;
				float maxLum = 0;

				for (int j = 0; j < 4; j++)
				{
					half depth = cols[j].a;

					// 有効ピクセルのみ処理する
					if (abs(depth - minDepth) < _ValidDepthThreshold)
					{
						float Lum = Luminance(cols[j].rgb);
						float weight = pow(Lum - 0.5, 2) * 4;	// 白と黒を強調する
						weight = lerp(1, weight, _SharpEdge);

						col += cols[j] * weight;
						weights += weight;

						minLum = min(minLum, Lum);
						maxLum = max(maxLum, Lum);
					}
				}
				col /= weights;


				// 輝度差が大きい場合は平均化を行わない
				if (maxLum - minLum > _EdgeLuminanceThreshold)
				{
					col = nearCol;
				}

				return col;
			}
				ENDCG
		} // pass1
	}
}

 パス0ではRGB+深度のテクスチャを生成しています。
(後のパスで色と深度を別々にフェッチする必要がなくなります。ただ深度とHDRのためにピクセルのフォーマットをHalfにしています。)
 またその際に(1-α)値を深度値に乗算することで、α値が1に近いほど優先度が高く(深度が0=手前に近付く)なるよう、マテリアル側で調整できるようにしました。


 また、エッジ検出(EdgeDetection)のイメージエフェクトで生成されるエッジの優先度も高くしたいので、
EdgeDetectNormals.shaderの、fragRobert関数内の250行あたり以降に

	float4 col = tex2D(_MainTex, i.uv[0]);
	col.a *= lerp(1, _BgColor.a, edge);
	col.rgb *= lerp(_BgColor.rgb, 1, edge);
	return col;

を挿入して、

  1. 線の色を背景色(Background OptionsのColor)で変えられるように。
  2. α値を、エッジ部はそのままに、そうでない部分を背景色のα値と乗算して優先度を低く。

するように改変しました。
ModeがRoberts Cross Depth Normalsの時のみ有効です。



使用例

 目のマテリアルと隣接している、肌マテリアルのアルファ値を下げて、目が優先的に選ばれるようにしています。
セピアやモノクロは標準のイメージエフェクトを使っています。

f:id:IARIKE:20150307200113j:plain
f:id:IARIKE:20150307200117j:plain
f:id:IARIKE:20150307200119j:plain
f:id:IARIKE:20150307200122j:plain

ソースコードについて

 商用にでもなんでも使ってください。
アセットストアで売られててもムカつくだけで何もしません。