おもちゃラボ

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ではシェーダを使えば簡単にポストエフェクトが作れるので是非いろいろ試してみて下さい。

確かな力が身につくC#「超」入門を出版しました

2冊目の参考書「確かな力が身につくC#「超」入門」が7/26に出版されました!なんやかんやで1年間かかりました〜

確かな力が身につくC#「超」入門 (Informatics&IDEA)


さて、肝心の参考書の中身の話を少しさせて下さい。書名からわかると思いますが、なんとC#の入門書です。この本は次の3部構成になっています。


第1部: C#の文法とVisual Studioの使い方
第2部: オブジェクト指向の基礎
第3部: Visual C#を使ったWindowsアプリの作成

このように純粋に文法を突き詰めた本ではないのでご注意ください・・・!

各130ページぐらいずつで、合計400Pになります。想定よりも分厚くなってしまいました・・・では、それぞれの内容を簡単に見ていきましょう。

C#の文法とVisual Studioの使い方の章

まずはC#の文法の基礎から始まります。if文やfor文、メソッドの使い方などを説明しています。

「文法は分かったけど使いドコロが分からん!ということにならないよう、本書ではゲームを例にして「使いドコロ」に重点を置いて説明しています。
 
 
f:id:nn_hokuson:20170727191640j:plain

オブジェクト指向の基礎の章

オブジェクト指向の章では、オブジェクト指向を使うときに最もつまづきやすい「クラスの作り方(設計方法)」についてしっかりと説明しています。

f:id:nn_hokuson:20170727194343p:plain:w150(注)本書にネコは出てきません


オブジェクト指向でもゲームを例にしてサンプルプログラムを作ります。ゲームを例にすることで、どうやって考えれば簡単にクラスを抽出できるか?がちゃんと理解できるようになっています。


f:id:nn_hokuson:20170727192526j:plain:w380

また、オブジェクト指向といえばカプセル化・継承・ポリモーフィズムですね。本書でも使いドコロと合わせて詳しく解説しています〜

それぞれ、ゲームを作りながら「なぜこの機能を使うと便利なのか」がしっかりと学べる内容になっています。


f:id:nn_hokuson:20170727192419j:plain

Visual C#を使ったWindowsアプリの作成の章

最後の章では、これまで学んできたC#の知識、オブジェクト指向の知識を使ってVisual C#を使ったWindowsアプリケーションの作り方を解説しています。

一見難しそうに見えるWindowsアプリケーションもイベントハンドラの役割を中心に、3ステップで作成できる方法を紹介しているので、ぜひ参考にしてみて下さい。


f:id:nn_hokuson:20170727192929j:plain


確かな力が身につくC#「超」入門は、これからC#の学習を始めたいかたや、WIndowsアプリケーションの作り方まで一通りの知識を身に着けたいかたには、良き入門書になると思います。

説明していない文法項目も多くありますが、そのぶん分かりやすさに重点を置いて説明していますので、ぜひ本屋で一度手にとっていただけると嬉しいです。

f:id:nn_hokuson:20170727194152p:plain:w200(注)本書にネコは出てきません!(笑)


【Unity】ボタンを押したときに画面クリックは無視する

GetMouseButtonDownメソッドを使ってマウスの入力を取得していると、uGUIのボタンを押したときにも画面クリックを検出してしまいます。

f:id:nn_hokuson:20170712215201g:plain:w450

ボタンがタッチされたときは、画面のタッチは無視する

これでも良い場合もありますが、普通は画面のクリックとボタンのクリックは分離したいですよね〜。
そんなときはEventSystemのIsPointerOverGameObjectメソッドを使います。このメソッドは、ボタンがクリックされたときにだけ反応するので、簡単に画面クリックを無視することが出来ます。

スクリプトは次のようになります。

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

public class TouchController : MonoBehaviour 
{
    void Update () {
        if(EventSystem.current.IsPointerOverGameObject()){
            return;
        }

        if (Input.GetMouseButtonDown (0)){
            
        }
    }
}

GetMouseButtonDownで画面タッチされたことを検知する前に、ボタンのクリックをチェックして、ボタンがクリックされているときにはreturnしています。

Unityエディタで実行すると、ちゃんと動作していることが確認できます。

f:id:nn_hokuson:20170712215427g:plain:w450

この方法はテラシュールブログさんでも紹介されていたのですが、スマートフォンで実行するとなぜか画面のクリックまで検出・・・

tsubakit1.hateblo.jp

スマートフォン対応する

そこで、EventSystemを使ってボタンクリックを検出する部分を次のように修正しました。

#if UNITY_EDITOR
    if(EventSystem.current.IsPointerOverGameObject()){
        return;
    }
#else 
    if (EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId)) {
        return;
    }
#endif

エディタで実行しているか、スマートフォンで実行しているかはマクロで分けています。スマートフォンの場合はIsPointerOverGameObjectメソッドの引数にフィンガーIDを渡している部分だけが異なります。

これを実行してみるとうまくいきましたー!