おもちゃラボ

Unityで遊びを作ってます

【Unityシェーダ入門】コントラストを調節できるポストエフェクトを作る

今回はゲーム画面のコントラストを調節するポストエフェクトの作り方を紹介します。Photoshopなどでは、スライダを動かすだけでコントラストを調整できますね。この機能をUnityのシェーダで作ってみましょう。

f:id:nn_hokuson:20170801154711j:plain

今回の記事の内容は次のとおりです。

コントラストの原理

まずは、コントラストを高くするというのは、どういう操作なのかを最初に説明します。コントラストを高くする、というのは「暗いところをより暗く、明るいところをより明るくする」ことです。

Photoshopのトーンカーブで説明すると、次の図のようになります。例えば輝度値200の明るいピクセルは、このカーブを通すと輝度値は230になります。逆に輝度値50の暗い部分はこのカーブを通すと輝度値は20になります。

f:id:nn_hokuson:20170801192118p:plain:w320

このようなS字カーブをプログラムで作る場合は、シグモイド曲線が便利です。シグモイド関数は次の数式で表せます。

f:id:nn_hokuson:20170801194003p:plain:w250

a=8の場合、次のような曲線になります。このあと説明しますが、aの値を変化させることでS字カーブを変形することができます。

f:id:nn_hokuson:20170801194140p:plain:w300

最近では、ディープラーニングでパーセプトロンが発火するかどうかの判定に、このシグモイド関数が使われていたりします。

ではシグモイド関数を使って、出力の輝度値を変化させるシェーダプログラムを作っていきましょう。

描画する画像をフックする

最終的に描画する画像にポストエフェクトをかけるために、画面をレンダリングする途中で画像をフックして画像処理します。

プロジェクトウインドウで「PostEffect.cs」というファイルを作成し、次のプログラムを入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PostEffect : MonoBehaviour {

    public Shader shader;
    public Material mat;

    void Start()
    {
        this.mat = new Material (this.shader);
    }

    void OnRenderImage(RenderTexture src, RenderTexture dest )
    {
        Graphics.Blit (src, dest, this.mat);
    }
}

Startメソッドではインスペクタでセットしたシェーダを使って、マテリアルを作成しています。

OnRenderImageメソッドはレンダリングが完了した後に呼び出されるメソッドで、この中でBlitメソッドを使ってポストエフェクトをかけています。Blitメソッドはsrc画像に第三引数で指定したポストエフェクトをかけてdest画像に書き込みます。

詳しくはこちらの記事も合わせて参照下さい。

nn-hokuson.hatenablog.com

スクリプトが保存できたら、PostEffect.csをカメラオブジェクトにアタッチしてください。

f:id:nn_hokuson:20170801192715j:plain

コントラストを高くするシェーダを作る

コントラストを高くするためのシェーダを作ります。プロジェクトウインドウで「右クリック」→「Create」→「Shader」→「Standard Surface Shader」を選択し、作成したファイル名をContrastに変更します。

contrast.shaderを開いて、次のスクリプトを入力して下さい。

Shader "PostEffect/contranst"
{
    Properties{
        _MainTex("MainTex", 2D)=""{}
        _Contrast("Contrast", Range(1, 20))=10
    }

    SubShader
    {
        Pass{
            CGPROGRAM
                #include "UnityCg.cginc"
                #pragma vertex vert_img
                #pragma fragment frag

                sampler2D _MainTex;
                float _Contrast;

                float4 frag(v2f_img i) : COLOR {
                    float4 c = tex2D(_MainTex, i.uv);
                    if( i.uv.x > 0.5 ){
                        c = 1/(1+exp(-_Contrast*(c-0.5)));
                    }
                    return c;
                }
            ENDCG
        }
    }
}

インスペクタからコントラストの強さを変更できるよう、Propertiesブロックに_Contrast変数を宣言しています。

また、フラグメントシェーダで各ピクセルに対してコントラストの計算をしています。コントラストの計算には上で紹介したシグモイド関数を使っています。

cの値が0から1の範囲でS字カーブになるように、expの係数cから0.5を引いていることに注意してください。

c = 1/(1+exp(-_Contrast*(c-0.5)));

_Contrastの値を変更することで、つぎのようにトーンカーブも変化します。macOSにはGrapherという便利なアプリがあるので実験してみてください。

f:id:nn_hokuson:20170801192806p:plain

カメラにコントラスト用のシェーダをセットする

作成したシェーダをを、カメラにアタッチしたPostEffectスクリプトのShader欄にドラッグ&ドロップします。これで、コントラストを調整するポストエフェクトをかける準備ができました。

f:id:nn_hokuson:20170801193110j:plain

実行結果は次のようになります。左半分がポストエフェクトをかけていない画面、右半分がポストエフェクトをかけた画面になります。

f:id:nn_hokuson:20170801154711j:plain

コントラストの強さを変更したい場合は、ゲーム実行中にヒエラルキーウインドウでMainCameraを選択し、インスペクタのPostEffectスクリプトからMatをダブルクリックして開き、スライダーで調節できます。

f:id:nn_hokuson:20170801193312p:plain:w300

_Contrastの値を10、12、15と変化させると次のように見栄えが変化します。

f:id:nn_hokuson:20170801161359j:plain:w500

まとめ

今回はコントラストを調整するポストエフェクトを作成しました。Unityではシェーダを使えば簡単にポストエフェクトが作れるので是非いろいろ試してみて下さい。

【Unityシェーダ入門】スパイクノイズを作る

スパイクノイズは、ブランドロゴのイントロとかでよく使われる感じのエフェクトです。といっても伝わりにくいので↓のような感じのものを作ります。

f:id:nn_hokuson:20170609224404g:plain

スパイクノイズという言葉はもともとは電子回路の言葉で、スイッチをオン・オフしたときに生じるパルスノイズです。チャタリングのようなものですね〜。余談でした(笑)

www.marutsu.co.jp

スパイクノイズのエフェクトを作る

画像を横方向に歪めるエフェクトは非常に簡単で、参照するテクスチャのX座標値をY軸の値をもとにずらすだけです。ずらす量は基本的にはsin関数で良いと思います。

f:id:nn_hokuson:20170609222354j:plain:w450

今回は場所によってもう少しランダムなノイズにしたかったので、sin(10*x)*(-(x-1)^2+1)という関数の0〜3の範囲を使います。
この関数はsin関数と二次曲線の値を掛けただけのものですが、これにより山形のノイズが生成できます。

f:id:nn_hokuson:20170609221144p:plain:w400

MacではGrapherというアプリ(デフォルトで入っている)を使うと、簡単にグラフが書けるので、「こういう波形が作りたい」というときに重宝しています。

パルスノイズのシェーダを作る

まずはシェーダファイルとマテリアルを作ります。プロジェクトビューで右クリックし、「Create/Shader/Unlit Shader」を選択してください。「PulseNoise.shader」という名前で保存します。このシェーダを設定した「PulseNoiseマテリアル」も作成します。

f:id:nn_hokuson:20170609222617p:plain:w150

PulseNoise.shaderに次のプログラムを入力してください。

Shader "UI/PulseNoise"
{
    Properties
    {
        _MainTex ("Sprite Texture", 2D) = "white" {}
        _Amount ("Distort", Float) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag  
            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float  _Amount;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                float2 uv = i.uv;
                float x = 2*uv.y;
                uv.x += _Amount*sin(10*x)*(-(x-1)*(x-1)+1);
                fixed4 col = tex2D(_MainTex, uv);
                return col;
            }
            ENDCG
        }
    }
}

フラグメントシェーダで、テクスチャを横方向にずらす処理をしています。テクスチャのy座標の値をもとにx方向にずらす量を計算しています。計算式は上で書いたとおりです。

また、ノイズの強さ自体をインスペクタから操作するため、propertiesブロックに_Amount変数を用意しています。この値を上で計算した値に掛けることで、ノイズ全体の大きさを制御します。

シェーダが書けたら、ノイズをかけたいuGUIのImageのインスペクタのMaterial欄に作成したPulseNoiseマテリアルをドラッグ&ドロップしてください。

f:id:nn_hokuson:20170609222945p:plain:w400

PulseNoiseマテリアルのインスペクタからAmount値を操作するとノイズがかかることが確認できます。

f:id:nn_hokuson:20170609223344g:plain

パルスノイズを発生させるスクリプト

いちいち手作業でノイズを発生させるわけにもいかないので、スクリプトからノイズを発生させましょう。

ノイズを一瞬だけ発生させるため、C#スクリプトから_Amount変数の値を操作します。NoiseControllerを作って次のプログラムを入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class NoiseController : MonoBehaviour {

    IEnumerator GeneratePulseNoise()
    {
        for (int i = 0; i <= 180; i += 30)
        {
            GetComponent<Image> ().material.SetFloat ("_Amount", 0.2f * Mathf.Sin (i * Mathf.Deg2Rad));
            yield return null;
        }
    }

    void Update () {
        if (Input.GetMouseButtonDown (0))
        {    
            StartCoroutine (GeneratePulseNoise ());
        }
    }
}

ここではノイズの大きさをsin波で変化させているだけです。もちろん、Animatorから_Amountの値を操作してもOKです。最終的な実行結果は次のようになります。

f:id:nn_hokuson:20170609224404g:plain

【Unityシェーダ入門】ポリゴンをポイント(点)で表現する

Unityで3Dモデルを普通に描画するとポリゴンが表示されます。ここではポリゴンの頂点だけをポイントで表示する方法を紹介します。

頂点の表示方法を変えるだけなら、なんと2行のスクリプトだけですみます。それぞれの頂点に異なる色をつける場合はシェーダを使っています。

f:id:nn_hokuson:20170606195524j:plain

トポロジをポリゴンからポイントに変更する

3Dモデルの頂点情報はMeshで管理されていて、その頂点をどのように描画するかはMeshTopologyで定義されています。通常は面を描画するためにTrianglesトポロジが選択されています。

MeshTopologyには次のような種類があります。

トポロジ名 形状
Triangles 三角形
Quads 四角形
Lines
LineStrip 線分
Points

ここでは、頂点をポイントで表示するためにMeshTopology.Pointsを指定します。ワイヤーフレームで表示したい場合はMeshTopology.Linesを選択します。

f:id:nn_hokuson:20170606202826p:plain:w450

トポロジを変更してポイントで表示する

トポロジの変更は、スクリプトから簡単に行うことが出来ます。PointControllerという名前でスクリプトを作成し、次のプログラムを入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PointController : MonoBehaviour 
{
	void Start () {
        MeshFilter meshFilter = GetComponent<MeshFilter>();
        meshFilter.mesh.SetIndices(meshFilter.mesh.GetIndices(0),MeshTopology.Points,0);
	}
}

ここではGetComponentを使ってMeshFilterコンポーネントを取得し、メッシュの持つ頂点情報のトポロジをPointsに書き換えて再度保存しています。

スクリプトが入力できたら、作成したスクリプトを3Dモデルにアタッチして実行してみてください。次のように、頂点がポイントで表されたモデルが表示されると思います。

f:id:nn_hokuson:20170606200747j:plain:w500

頂点の色をグラデーションで表示する

ポイントごと(頂点ごと)に色を変えたい場合は、シェーダを書く必要があります。ここでは上からしたに向かってグラデーションになるようにポイントごとに色を付けるシェーダを作ってみましょう。

シェーダファイルと対応するマテリアルを作成します。プロジェクトビューで右クリックし、「Create」→「Shader」→「Unlit Shader」を選択してください。名前はGradationにしました。また、それにGaradationシェーダを選択した状態で「Create→Material」を選択し、対応するCustom_Gradationマテリアルも作成します。

f:id:nn_hokuson:20170606201303p:plain:w200

シェーダには次のプログラムを入力してください。

Shader "Custom/Gradation" {
    Properties {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader {
        Tags { "RenderType"="Transparent" }
        Cull Off ZWrite On Blend SrcAlpha OneMinusSrcAlpha
        Pass {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            
            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldPos : TEXCOORD0;
            };
            
            float4 _Color;

            v2f vert(appdata_base v)
            {
                v2f o;
				float3 n = UnityObjectToWorldNormal(v.normal);
                o.pos = UnityObjectToClipPos (v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;   
     
                return o;
            }
            
            half4 frag (v2f i) : COLOR
            {
            	float4 red  = float4(255.0/255,70.0/255,150.0/255,1);
            	float4 blue = float4(90.0/255,90.0/255,250.0/255,1);
                return lerp(red, blue, i.worldPos.y*0.2);
            }
            ENDCG
        }
    } 
    FallBack Off
}

ここではバーテックスシェーダで、処理している頂点のワールド座標を計算しています。フラグメントシェーダではこのワールド座標のy成分に基づいて頂点の色を決定します。うまく青色から赤色にグラデーションになるよう、Lerpメソッドで補間して計算しています。

実行結果は次のようになりました。

f:id:nn_hokuson:20170606195524j:plain:w500

頂点を動かして砂が飛び散るエフェクト

頂点を法線方向に動かすことで砂が飛び散るようなエフェクトを作ることも出来ます。バーテックスシェーダを次のように変更してください。

v2f vert(appdata_base v)
{
    v2f o;
    float3 n = UnityObjectToWorldNormal(v.normal);
    o.pos = UnityObjectToClipPos (v.vertex) + float4(n * _Displacement, 0);     
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;   
     
    return o;
}

ここでは、ワールド座標系で法線情報を取得し、その方向に_Displacementぶん頂点を外側に動かしています。_Displacementはプロパティブロックで宣言して、インスペクタから操作できるようにしておきました。

f:id:nn_hokuson:20170606202232g:plain